diff --git a/lib/base/networking/api/handler/api_handler.dart b/lib/base/networking/api/handler/api_handler.dart index 495ef04e..28da2116 100644 --- a/lib/base/networking/api/handler/api_handler.dart +++ b/lib/base/networking/api/handler/api_handler.dart @@ -1,37 +1,29 @@ import 'dart:convert'; - import 'package:dio/dio.dart'; import 'package:gocast_mobile/models/error/error_model.dart'; import 'package:logger/logger.dart'; +/// Handles HTTP communication for the application. class ApiHandler { static final Logger _logger = Logger(); - /// Handles an HTTP response. + /// Handles an HTTP error response. /// - /// This method checks the status code of the HTTP response and throws an [AppError] if necessary. - /// It uses the `handleHttpStatus` method to check the status code and throw the appropriate error. + /// This method takes a [response] and handles the error based on the status code. /// - /// The `apiMessage` from the response is passed to the `handleHttpStatus` method and used for the error message - /// of the [AppError.argumentError] thrown for a 400 status code. + /// Throws an [AppError] if the status code is not in the 2xx range. static void handleHttpResponse(Response response) { _logger .i('Received HTTP response with status code: ${response.statusCode}'); - if (response.data != null && response.data != '') { try { - // Attempt to decode the response body var responseBody = jsonDecode(response.data); String? apiMessage = responseBody['message']; - - // Log the extracted message if (apiMessage != null) { _logger.i('API message: $apiMessage'); } - handleHttpStatus(response.statusCode, apiMessage); } catch (e) { - // Log any JSON decoding errors _logger.e('Error decoding response data: $e'); } } else { @@ -40,58 +32,30 @@ class ApiHandler { } } - /// Handles the HTTP status code of a response and throws an [AppError] if necessary. - /// - /// This method checks the HTTP status code and throws an [AppError] for certain status codes. - /// If the status code is null or not in the range of 100 to 399, it throws an [AppError]. - /// For status codes 400, 401, 403, 404, and 500, it throws specific [AppError]s. - /// For all other status codes, it throws an [AppError.unknownError]. - /// - /// The method also accepts an optional `apiMessage` parameter. If provided, this message - /// is used for the error message of the [AppError.argumentError] thrown for a 400 status code. + /// Handles an HTTP status code. /// - /// - 1xx-3xx: No error is thrown - /// - 400: [AppError.argumentError] is thrown with `apiMessage` as the error message - /// - 401: [AppError.authenticationError] is thrown - /// - 403: [AppError.forbidden] is thrown - /// - 404: [AppError.notFound] is thrown - /// - 500: [AppError.internalServerError] is thrown - /// - Other: [AppError.unknownError] is thrown + /// This method takes a [statusCode] and [apiMessage] and throws an [AppError] + /// based on the status code. static void handleHttpStatus(int? statusCode, String? apiMessage) { - // Log the received status code and API message - _logger - .d('Handling HTTP status code: $statusCode, API message: $apiMessage'); - + _logger.i('Handling HTTP status code: $statusCode, API message: $apiMessage'); if (statusCode == null) { - _logger.e('Status code is null'); throw AppError.unknownError("Status code is null"); } - - if (statusCode >= 100 && statusCode < 400) { - // Log successful response - _logger.i('Successful HTTP response with status code: $statusCode'); - return; - } - // Handle error status codes - switch (statusCode) { - case 400: - _logger.w('HTTP 400 Bad Request: $apiMessage'); - throw AppError.argumentError(apiMessage ?? "Bad Request"); - case 401: - _logger.w('HTTP 401 Unauthorized'); - throw AppError.authenticationError(); - case 403: - _logger.w('HTTP 403 Forbidden'); - throw AppError.forbidden(); - case 404: - _logger.w('HTTP 404 Not Found'); - throw AppError.notFound(); - case 500: - _logger.e('HTTP 500 Internal Server Error'); - throw AppError.internalServerError(); - default: - _logger.e('Unknown error with status code: $statusCode'); - throw AppError.unknownError("Status code: $statusCode"); + if (statusCode < 100 || statusCode >= 400) { + switch (statusCode) { + case 400: + throw AppError.argumentError(apiMessage ?? "Bad Request"); + case 401: + throw AppError.authenticationError(); + case 403: + throw AppError.forbidden(); + case 404: + throw AppError.notFound(); + case 500: + throw AppError.internalServerError(); + default: + throw AppError.unknownError("Status code: $statusCode"); + } } } } diff --git a/lib/base/networking/api/handler/auth_handler.dart b/lib/base/networking/api/handler/auth_handler.dart index 4712d48a..108afc1e 100644 --- a/lib/base/networking/api/handler/auth_handler.dart +++ b/lib/base/networking/api/handler/auth_handler.dart @@ -12,23 +12,23 @@ import 'package:gocast_mobile/providers.dart'; import 'package:gocast_mobile/utils/globals.dart'; import 'package:logger/logger.dart'; -/// Handles authentication for the application. +///Authentication handler for the application. class AuthHandler { static final Logger _logger = Logger(); static bool isLoginSuccessful = false; - /// Performs basic authentication. + /// the basic authentication method for internal Users /// - /// This method sends a POST request to the basic login URL with the given - /// username and password. If the request is successful, it saves the JWT token. + /// This method takes a [username] and [password] and sends a POST request to the server. + /// The server will respond with a JWT token that will be saved in the cookie jar. /// - /// Throws an [AppError] if a network error occurs or if no JWT-cookie is set. + /// Throws an [AppError] if a network error occurs or if the gRPC call fails. static Future basicAuth( String username, String password, ) async { - var url = Routes.basicLogin; _logger.i('Starting basic authentication for user: $username'); + var url = Routes.basicLogin; final cookieJar = CookieJar(); final dio = Dio( BaseOptions( @@ -36,27 +36,28 @@ class AuthHandler { validateStatus: (status) { return status! < 500; }, - receiveTimeout: const Duration(seconds: 5), + receiveTimeout: const Duration(seconds: 3), ), ); dio.interceptors.add(CookieManager(cookieJar)); - final formData = FormData.fromMap({ 'username': username, 'password': password, }); - + Response response; + try { + response = await dio.post(url, data: formData); + _logger.i('Received HTTP response with status code: ${response.statusCode}'); + } catch (e) { + _logger.e('Error during basic authentication for user: $username, Error: $e'); + throw AppError.userError(); + } try { - final response = await dio.post(url, data: formData); ApiHandler.handleHttpResponse(response); _logger.i('Authentication successful for user: $username'); } catch (e) { _logger.e('Authentication failed for user: $username, Error: $e'); - // Throw the error so it can be caught and handled by the caller of basicAuth - throw AppError.networkError(e); } - - // Save jwt token try { List cookies = await cookieJar.loadForRequest(Uri.parse(url)); await TokenHandler.saveTokenFromCookies('jwt', cookies); @@ -67,13 +68,16 @@ class AuthHandler { } } + /// the SSO authentication method for all Users + /// + /// This method takes a [context] and [ref] and sends a POST request to the server. + /// The server will respond with a JWT token that will be saved in the cookie jar. + /// + /// Throws an [AppError] if a network error occurs or if the gRPC call fails. static Future ssoAuth(BuildContext context, WidgetRef ref) async { final viewModel = ref.read(userViewModelProvider.notifier); - _logger.i('Starting SSO authentication'); viewModel.setLoading(true); // Set loading state - _logger.i('Loading SSO login page'); - try { await navigatorKey.currentState?.push( MaterialPageRoute( @@ -101,6 +105,7 @@ class AuthHandler { } } + ///Helper method to build the web view for SSO authentication static Widget _buildWebView() { _logger.i('Building web view'); return webview.InAppWebView( @@ -109,6 +114,7 @@ class AuthHandler { ); } + ///Helper method to handle the web view load stop event static Future _onWebViewLoadStop( webview.InAppWebViewController controller, Uri? url, @@ -124,21 +130,23 @@ class AuthHandler { navigatorKey.currentState?.pushReplacementNamed('/welcome'); _logger.i('SSO authentication completed, redirected to app'); } else if (url != null) { - _logger.d('Web view loaded URL: $url'); + _logger.i('Web view loaded URL: $url'); } else { - _logger.w('URL is null in web view onLoadStop'); + _logger.e('URL is null in web view onLoadStop'); } } catch (e) { _logger.e('Error during SSO authentication: $e'); - throw AppError.networkError(e); + throw AppError.userError(); } } + ///Helper method to handle the cookie retrieval + /// + /// This method takes a [url] and retrieves the JWT token from the cookies. static Future _handleCookieRetrieval(Uri url) async { try { final cookieManager = webview.CookieManager.instance(); List cookies = await cookieManager.getCookies(url: url); - _logger.d('Retrieved cookies from URL: $url'); webview.Cookie? jwtCookie; for (var cookie in cookies) { if (cookie.name == 'jwt') { diff --git a/lib/base/networking/api/handler/bookmarks_handler.dart b/lib/base/networking/api/handler/bookmarks_handler.dart index 0f83d1e5..40fcf008 100644 --- a/lib/base/networking/api/handler/bookmarks_handler.dart +++ b/lib/base/networking/api/handler/bookmarks_handler.dart @@ -3,44 +3,37 @@ import 'package:logger/logger.dart'; import 'grpc_handler.dart'; -/// Handles bookmark-related data operations. -/// -/// This class is responsible for fetching and posting bookmark-related data, such as fetching user bookmarks and adding a bookmark. class BooKMarkHandler { static final Logger _logger = Logger(); final GrpcHandler _grpcHandler; - /// Creates a new instance of the `BookmarkHandler` class. - /// - /// The [GrpcHandler] is required. BooKMarkHandler(this._grpcHandler); - /// Fetches the current user's bookmarks. + /// Fetches user bookmarks. /// - /// This method sends a `getUserBookmarks` gRPC call to fetch the user's - /// bookmarks. + /// This method sends a `getUserBookmarks` gRPC call to fetch the user's bookmarks. /// - /// Returns a [List] instance that represents the user's bookmarks. + /// returns a [List] instance that represents the user's bookmarks. Future> fetchUserBookmarks() async { _logger.i('Fetching user bookmarks'); return _grpcHandler.callGrpcMethod( (client) async { final response = await client.getUserBookmarks(GetBookmarksRequest()); - _logger.i('User bookmarks fetched successfully'); _logger.d('User bookmarks: ${response.bookmarks}'); return response.bookmarks; }, ); } + /// Adds a bookmark for the current user. /// - /// Sends a `putUserBookmark` gRPC call with the given [bookmarkData] to add a new bookmark. - /// Logs the action of saving and provides details of the saved bookmark. + /// Sends a `putUserBookmark` gRPC call with the given [bookmarkData] to add a bookmark. + /// Logs the action of adding and provides details of the added bookmark. /// /// [bookmarkData]: The data of the bookmark to be added, encapsulated in a `BookmarkData` object. /// - /// Returns [Bookmark]: The `Bookmark` instance representing the newly added bookmark. + /// returns [Bookmark]: The `Bookmark` instance representing the added bookmark. Future addToBookmark(BookmarkData bookmarkData) async { var request = PutBookmarkRequest( description: bookmarkData.description, @@ -52,21 +45,12 @@ class BooKMarkHandler { return _grpcHandler.callGrpcMethod( (client) async { final response = await client.putUserBookmark(request); - _logger.i('User bookmark saved successfully'); _logger.d('User bookmark: ${response.bookmark}'); return response.bookmark; }, ); } - /// Removes a bookmark for the current user. - /// - /// Sends a `deleteUserBookmark` gRPC call to remove a bookmark identified by [bookmarkID]. - /// Logs the successful removal of the bookmark. - /// - /// [bookmarkID]: The unique identifier of the bookmark to be removed. - /// - /// Returns [bool]: `true` if the bookmark was removed successfully, `false` otherwise. Future removeFromBookmarks(int bookmarkID) async { var request = DeleteBookmarkRequest(bookmarkID: bookmarkID); try { @@ -105,7 +89,6 @@ class BooKMarkHandler { return _grpcHandler.callGrpcMethod( (client) async { final response = await client.patchUserBookmark(request); - _logger.i('User bookmark updated successfully'); _logger.d('User bookmark: ${response.bookmark}'); return response.bookmark; }, diff --git a/lib/base/networking/api/handler/chat_handler.dart b/lib/base/networking/api/handler/chat_handler.dart index d5343084..2ceeb49a 100644 --- a/lib/base/networking/api/handler/chat_handler.dart +++ b/lib/base/networking/api/handler/chat_handler.dart @@ -9,10 +9,15 @@ class ChatHandlers { ChatHandlers(this._grpcHandler); + /// Fetches user chats. + /// + /// This method sends a `getUserChats` gRPC call to fetch the user's chats. + /// Takes a [streamID] parameter to fetch the user's chats for a specific stream. + /// returns a [List] instance that represents the user's chats. Future> getChatMessages(int streamID) async { - _logger.i('Fetching chat messages'); return _grpcHandler.callGrpcMethod( (client) async { + _logger.i('Fetching chat messages'); final response = await client .getChatMessages(GetChatMessagesRequest(streamID: streamID)); _logger.d('Chat messages: ${response.messages}'); @@ -21,8 +26,14 @@ class ChatHandlers { ); } + /// Post a chat message. + /// + /// This method sends a `postChatMessage` gRPC call to post a chat message. + /// Takes a [streamID] parameter to post a chat message for a specific stream. + /// Takes a [message] parameter to post a chat message. + /// + /// returns a [ChatMessage] instance that represents the posted chat message. Future postChatMessage(int streamID, String message) async { - _logger.i('Posting chat message'); return _grpcHandler.callGrpcMethod( (client) async { final response = await client.postChatMessage( @@ -37,10 +48,17 @@ class ChatHandlers { ); } + /// Post a message Reaction. + /// + /// This method sends a `postChatReaction` gRPC call to post a chat reaction. + /// Takes a [messageID] parameter to post a chat reaction for a specific message. + /// Takes a [streamID] parameter to post a chat reaction for a specific stream. + /// Takes a [emoji] parameter to post a chat reaction. + /// + /// returns a [ChatReaction] instance that represents the posted chat reaction. Future postMessageReaction(int messageID, int streamID, String emoji,) async { - _logger.i('Posting chat reaction'); return _grpcHandler.callGrpcMethod( (client) async { final response = await client.postChatReaction( @@ -56,10 +74,15 @@ class ChatHandlers { ); } + /// Delete a message Reaction. + /// + /// This method sends a `deleteChatReaction` gRPC call to delete a chat reaction. + /// Takes a [messageID] parameter to delete a chat reaction for a specific message. + /// Takes a [streamID] parameter to delete a chat reaction for a specific stream. + /// Takes a [reactionID] parameter to delete a chat reaction. Future deleteMessageReaction(int messageID, int streamID, int reactionID,) async { - _logger.i('Deleting chat reaction'); return _grpcHandler.callGrpcMethod( (client) async { await client.deleteChatReaction( @@ -74,10 +97,17 @@ class ChatHandlers { ); } + /// Post a chat reply. + /// + /// This method sends a `postChatReply` gRPC call to post a chat reply. + /// Takes a [messageID] parameter to post a chat reply for a specific message. + /// Takes a [streamID] parameter to post a chat reply for a specific stream. + /// Takes a [message] parameter to post a chat reply. + /// + /// returns a [ChatMessage] instance that represents the posted chat reply. Future postChatReply(int messageID, int streamID, String message,) async { - _logger.i('Posting chat reply'); return _grpcHandler.callGrpcMethod( (client) async { final response = await client.postChatReply( @@ -93,8 +123,12 @@ class ChatHandlers { ); } + /// Mark the Chat as resolved. + /// + /// This method sends a `markChatMessageAsResolved` gRPC call to mark the chat as resolved. + /// Takes a [messageID] parameter to mark the chat as resolved for a specific message. + /// Takes a [streamID] parameter to mark the chat as resolved for a specific stream. Future markChatMessageAsResolved(int messageID, int streamID) async { - _logger.i('Marking chat message as resolved'); return _grpcHandler.callGrpcMethod( (client) async { await client.markChatMessageAsResolved( @@ -109,7 +143,6 @@ class ChatHandlers { } Future markChatMessageAsUnresolved(int messageID, int streamID) async { - _logger.i('Marking chat message as unresolved'); return _grpcHandler.callGrpcMethod( (client) async { await client.markChatMessageAsUnresolved( diff --git a/lib/base/networking/api/handler/course_handler.dart b/lib/base/networking/api/handler/course_handler.dart index 77831a57..10b1e4d1 100644 --- a/lib/base/networking/api/handler/course_handler.dart +++ b/lib/base/networking/api/handler/course_handler.dart @@ -4,15 +4,14 @@ import 'package:gocast_mobile/base/networking/api/handler/user_handler.dart'; import 'package:logger/logger.dart'; import 'package:tuple/tuple.dart'; -/// Handles course-related data operations. -/// -/// This class is responsible for fetching and posting course-related data, such as fetching public courses and semesters. + class CourseHandler { static final Logger _logger = Logger(); final GrpcHandler _grpcHandler; CourseHandler(this._grpcHandler); + /// Fetches the public courses. Future> fetchPublicCourses() async { _logger.i('Fetching public courses'); return _grpcHandler.callGrpcMethod( @@ -24,22 +23,20 @@ class CourseHandler { }, ); } - +/// fetches the semesters and the current semester. Future, Semester>> fetchSemesters() async { _logger.i('Fetching semesters'); - final response = await _grpcHandler.callGrpcMethod( (client) async { return await client.getSemesters(GetSemestersRequest()); }, ); - - _logger.d('Semesters: ${response.semesters}'); - _logger.d('Current Semester: ${response.current}'); - + _logger.i('Semesters: ${response.semesters}'); + _logger.i('Current Semester: ${response.current}'); return Tuple2(response.semesters, response.current); } + /// Fetches all the courses. (public + user) Future> fetchAllCourses() async { List userCourses = await UserHandler(_grpcHandler).fetchUserCourses(); diff --git a/lib/base/networking/api/handler/grpc_handler.dart b/lib/base/networking/api/handler/grpc_handler.dart index 1b33d039..88d082f0 100644 --- a/lib/base/networking/api/handler/grpc_handler.dart +++ b/lib/base/networking/api/handler/grpc_handler.dart @@ -19,7 +19,6 @@ class GrpcHandler { /// The [host] and [port] are required. GrpcHandler(this.host, this.port) { _logger.i('Creating GrpcHandler: Connecting to gRPC server at $host:$port'); - _channel = ClientChannel( host, port: port, @@ -44,13 +43,21 @@ class GrpcHandler { ) async { _logger.d('callGrpcMethod: Initiating gRPC call'); try { - String token = await TokenHandler.loadToken('jwt'); - final metadata = { - 'grpcgateway-cookie': 'jwt=$token', - }; - - final callOptions = CallOptions(metadata: metadata); - + String token = ''; + try { + token = await TokenHandler.loadToken('jwt'); + }catch(e) { + token = ''; + } + CallOptions callOptions; + if(token.isNotEmpty) { + final metadata = { + 'grpcgateway-cookie': 'jwt=$token', + }; + callOptions = CallOptions(metadata: metadata); + }else { + callOptions = CallOptions(); + } return await grpcMethod(APIClient(_channel, options: callOptions)); } on SocketException catch (socketException) { _logger diff --git a/lib/base/networking/api/handler/notification_handler.dart b/lib/base/networking/api/handler/notification_handler.dart index 7dbb7cc1..fef88428 100644 --- a/lib/base/networking/api/handler/notification_handler.dart +++ b/lib/base/networking/api/handler/notification_handler.dart @@ -2,16 +2,10 @@ import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; import 'package:gocast_mobile/base/networking/api/handler/grpc_handler.dart'; import 'package:logger/logger.dart'; -/// Handles Notification-related data operations. -/// -/// This class is responsible for fetching and posting notification-related data, such as fetching feature Notification and banner Alerts. class NotificationHandler { static final Logger _logger = Logger(); final GrpcHandler _grpcHandler; - /// Creates a new instance of the `NotificationHandler` class. - /// - /// The [GrpcHandler] is required. NotificationHandler(this._grpcHandler); /// Registers a new device for push notificaitons. diff --git a/lib/base/networking/api/handler/pinned_handler.dart b/lib/base/networking/api/handler/pinned_handler.dart index bfad5b46..f4da402a 100644 --- a/lib/base/networking/api/handler/pinned_handler.dart +++ b/lib/base/networking/api/handler/pinned_handler.dart @@ -20,7 +20,6 @@ class PinnedHandler { return _grpcHandler.callGrpcMethod( (client) async { final response = await client.getUserPinned(GetUserPinnedRequest()); - _logger.i('User pinned fetched successfully'); _logger.d('User pinned: ${response.courses}'); return response.courses; }, diff --git a/lib/base/networking/api/handler/poll_handler.dart b/lib/base/networking/api/handler/poll_handler.dart index 128b578c..c43393c9 100644 --- a/lib/base/networking/api/handler/poll_handler.dart +++ b/lib/base/networking/api/handler/poll_handler.dart @@ -10,6 +10,12 @@ class PollHandlers { PollHandlers(this._grpcHandler); + /// Fetches polls for a stream. + /// + /// This method sends a `getPolls` gRPC call to fetch the polls for a stream. + /// Takes a [streamID] parameter to fetch the polls for a specific stream. + /// + /// returns a [List] instance that represents the polls for a stream. Future> getPolls(int streamID) async { _logger.i('Fetching polls for streamID: $streamID'); return _grpcHandler.callGrpcMethod( @@ -22,6 +28,11 @@ class PollHandlers { ); } + /// Post a poll vote. + /// + /// This method sends a `postPollVote` gRPC call to post a poll vote. + /// Takes a [streamID] parameter to post a poll vote for a specific stream. + /// Takes a [pollOptionID] parameter to post a poll vote for a specific poll option. Future postPollVote(int streamID, int pollOptionID) async { _logger.i( 'Posting poll vote for streamID: $streamID, pollOptionID: $pollOptionID', @@ -36,9 +47,7 @@ class PollHandlers { _logger.i( 'Poll vote posted successfully for option $pollOptionID in stream $streamID', ); - // Assuming PostPollVoteResponse doesn't have a field to return, just logging the success }, ); } -// Add any additional poll-related methods here, similar to the ChatHandlers class } diff --git a/lib/base/networking/api/handler/settings_handler.dart b/lib/base/networking/api/handler/settings_handler.dart index c1793574..476cf84c 100644 --- a/lib/base/networking/api/handler/settings_handler.dart +++ b/lib/base/networking/api/handler/settings_handler.dart @@ -5,9 +5,7 @@ import 'package:logger/logger.dart'; import 'grpc_handler.dart'; -/// Handles user settings-related data operations. -/// -/// This class is responsible for fetching and updating user settings. + class SettingsHandler { static final Logger _logger = Logger(); final GrpcHandler _grpcHandler; @@ -57,7 +55,7 @@ class SettingsHandler { return true; } catch (e) { _logger.e('Error updating user settings: $e'); - rethrow; + return false; } } @@ -82,7 +80,7 @@ class SettingsHandler { return true; } catch (e) { _logger.e('Error updating user settings: $e'); - rethrow; + return false; } } @@ -108,7 +106,7 @@ class SettingsHandler { return true; } catch (e) { _logger.e('Error updating user settings: $e'); - rethrow; + return false; } } diff --git a/lib/base/networking/api/handler/stream_handler.dart b/lib/base/networking/api/handler/stream_handler.dart index 92ef6de5..8504b02d 100644 --- a/lib/base/networking/api/handler/stream_handler.dart +++ b/lib/base/networking/api/handler/stream_handler.dart @@ -68,7 +68,6 @@ class StreamHandler { /// /// Takes [streamId] as a parameter. /// Returns a [String] instance that represents the thumbnail stream. - Future fetchThumbnailStreams(int streamId) async { _logger.i('Fetching thumbnail stream'); return _grpcHandler.callGrpcMethod( @@ -87,7 +86,6 @@ class StreamHandler { /// /// Takes [streamId] as a parameter. /// Returns a [String] instance that represents the thumbnail VOD. - Future fetchThumbnailVOD(int streamId) async { _logger.i('Fetching thumbnail VOD'); return _grpcHandler.callGrpcMethod( @@ -106,7 +104,6 @@ class StreamHandler { /// /// Takes [streamId] as a parameter. /// Returns a [Progress] instance that represents the progress of the stream. - Future fetchProgress(int streamId) async { _logger.i('Fetching progress'); try { @@ -130,7 +127,6 @@ class StreamHandler { /// This method sends a `putProgress` gRPC call to update the progress of a stream. /// /// Takes [streamId] and [progress] as parameters. - Future putProgress(streamId, Progress progress) async { _logger.i('Updating progress'); await _grpcHandler.callGrpcMethod( diff --git a/lib/base/networking/api/handler/token_handler.dart b/lib/base/networking/api/handler/token_handler.dart index 8c6c9fe4..cf970fe8 100644 --- a/lib/base/networking/api/handler/token_handler.dart +++ b/lib/base/networking/api/handler/token_handler.dart @@ -31,7 +31,6 @@ class TokenHandler { } } _logger.w("Token not found in cookies."); - // Handle error when no jwt cookie is found throw AppError.authenticationError(); } @@ -107,7 +106,7 @@ class TokenHandler { _logger.i('Token successfully deleted for key: $key'); } catch (e) { _logger.e('Error deleting token: $e'); - throw AppError.authenticationError(); + throw AppError.notFound(); } } } diff --git a/lib/base/networking/api/handler/user_handler.dart b/lib/base/networking/api/handler/user_handler.dart index 9faa3acc..16bf9795 100644 --- a/lib/base/networking/api/handler/user_handler.dart +++ b/lib/base/networking/api/handler/user_handler.dart @@ -25,7 +25,6 @@ class UserHandler { return _grpcHandler.callGrpcMethod( (client) async { final response = await client.getUser(GetUserRequest()); - _logger.i('User details fetched successfully'); _logger.d('User details: ${response.user}'); return response.user; }, @@ -43,7 +42,6 @@ class UserHandler { return _grpcHandler.callGrpcMethod( (client) async { final response = await client.getUserCourses(GetUserCoursesRequest()); - _logger.i('User courses fetched successfully'); _logger.d('User courses: ${response.courses}'); return response.courses; }, @@ -62,7 +60,6 @@ class UserHandler { (client) async { final response = await client.getUserAdminCourses(GetUserAdminRequest()); - _logger.i('User admin courses fetched successfully'); _logger.d('User admin courses: ${response.courses}'); return response.courses; }, @@ -81,7 +78,6 @@ class UserHandler { return _grpcHandler.callGrpcMethod( (client) async { final response = await client.getUserSettings(GetUserSettingsRequest()); - _logger.i('User settings fetched successfully'); _logger.d('User settings: ${response.userSettings}'); return response.userSettings; }, diff --git a/lib/models/course/pinned_course_state_model.dart b/lib/models/course/pinned_course_state_model.dart new file mode 100644 index 00000000..63150dee --- /dev/null +++ b/lib/models/course/pinned_course_state_model.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pbgrpc.dart'; +import 'package:gocast_mobile/models/error/error_model.dart'; + +@immutable +class PinnedCourseState { + final bool? isLoading; + final List? userPinned; + final AppError? error; + final List? displayedPinnedCourses; + final List? semesters; + final String? selectedSemester; + final List? semestersAsString; + final Semester? current; + final String? currentAsString; + + const PinnedCourseState({ + this.isLoading = false, + this.userPinned, + this.error, + this.displayedPinnedCourses, + this.semesters, + this.selectedSemester = 'All', + this.semestersAsString, + this.current, + this.currentAsString, + }); + + PinnedCourseState copyWith({ + bool? isLoading, + List? userPinned, + AppError? error, + List? displayedPinnedCourses, + List? semesters, + String? selectedSemester, + List? semestersAsString, + Semester? current, + String? currentAsString, + }) { + return PinnedCourseState( + isLoading: isLoading ?? this.isLoading, + userPinned: userPinned ?? this.userPinned, + error: error ?? this.error, + displayedPinnedCourses: displayedPinnedCourses ?? this.displayedPinnedCourses, + semesters: semesters ?? this.semesters, + selectedSemester: selectedSemester ?? this.selectedSemester, + semestersAsString: semestersAsString ?? this.semestersAsString, + current: current ?? this.current, + currentAsString: currentAsString ?? this.currentAsString, + ); + } + + PinnedCourseState clearError() { + return PinnedCourseState( + isLoading: isLoading, + userPinned: userPinned, + error: null, + displayedPinnedCourses: displayedPinnedCourses, + semesters: semesters, + selectedSemester: selectedSemester, + semestersAsString: semestersAsString, + current: current, + currentAsString: currentAsString, + ); + } + +} \ No newline at end of file diff --git a/lib/models/error/error_model.dart b/lib/models/error/error_model.dart index 1ae04caf..de8ccfcf 100644 --- a/lib/models/error/error_model.dart +++ b/lib/models/error/error_model.dart @@ -71,5 +71,9 @@ class AppError implements Exception { /// Represents an unknown error. factory AppError.unknownError(String? message) => - AppError('❓ An unknown error occurred {message: $message}'); + AppError('❓An unknown error occurred {message: $message}'); + + factory AppError.userError() => AppError('🥱Username or password are incorrect'); + + factory AppError.notificationNotAvailableYet() => AppError('🔕Notification not available yet, Set the FireBase keys first'); } diff --git a/lib/models/user/user_state_model.dart b/lib/models/user/user_state_model.dart index 72c96590..d3fc525e 100644 --- a/lib/models/user/user_state_model.dart +++ b/lib/models/user/user_state_model.dart @@ -7,7 +7,6 @@ class UserState { final bool isLoading; final User? user; final List? userCourses; - final List? userPinned; final List? userBookmarks; final List? publicCourses; final AppError? error; @@ -18,13 +17,11 @@ class UserState { final Semester? current; final String? currentAsString; final List? displayedCourses; - final List? displayedPinnedCourses; const UserState({ this.isLoading = false, this.user, this.userCourses, - this.userPinned, this.userBookmarks, this.publicCourses, this.error, @@ -35,14 +32,12 @@ class UserState { this.current, this.currentAsString, this.displayedCourses, - this.displayedPinnedCourses, }); UserState copyWith({ bool? isLoading, User? user, List? userCourses, - List? userPinned, List? userBookmarks, List? publicCourses, AppError? error, @@ -53,13 +48,11 @@ class UserState { Semester? current, String? currentAsString, List? displayedCourses, - List? displayedPinnedCourses, }) { return UserState( isLoading: isLoading ?? this.isLoading, user: user ?? this.user, userCourses: userCourses ?? this.userCourses, - userPinned: userPinned ?? this.userPinned, userBookmarks: userBookmarks ?? this.userBookmarks, publicCourses: publicCourses ?? this.publicCourses, error: error ?? this.error, @@ -70,8 +63,6 @@ class UserState { current: current ?? this.current, currentAsString: currentAsString ?? this.currentAsString, displayedCourses: displayedCourses ?? this.displayedCourses, - displayedPinnedCourses: - displayedPinnedCourses ?? this.displayedPinnedCourses, ); } @@ -79,20 +70,17 @@ class UserState { bool? isLoading, User? user, List? userCourses, - List? userPinned, List? userBookmarks, List? publicCourses, AppError? error, List? downloadedCourses, List? displayedCourses, - List? displayedPinnedCourses, List? semesters, }) { return UserState( isLoading: isLoading ?? this.isLoading, user: user ?? this.user, userCourses: userCourses ?? this.userCourses, - userPinned: userPinned ?? this.userPinned, userBookmarks: userBookmarks ?? this.userBookmarks, publicCourses: publicCourses ?? this.publicCourses, error: null, @@ -103,8 +91,24 @@ class UserState { current: current, currentAsString: currentAsString, displayedCourses: displayedCourses ?? this.displayedCourses, - displayedPinnedCourses: - displayedPinnedCourses ?? this.displayedPinnedCourses, + ); + } + + UserState reset() { + return const UserState( + isLoading: false, + user: null, + userCourses: null, + userBookmarks: null, + publicCourses: null, + error: null, + downloadedCourses: null, + semesters: null, + selectedSemester: 'All', + semestersAsString: null, + current: null, + currentAsString: null, + displayedCourses: null, ); } } diff --git a/lib/providers.dart b/lib/providers.dart index 660d69f1..7de218d8 100644 --- a/lib/providers.dart +++ b/lib/providers.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gocast_mobile/config/app_config.dart'; import 'package:gocast_mobile/models/chat/chat_state_model.dart'; import 'package:gocast_mobile/models/course/course_state_model.dart'; +import 'package:gocast_mobile/models/course/pinned_course_state_model.dart'; import 'package:gocast_mobile/models/notifications/notification_state_model.dart'; import 'package:gocast_mobile/models/poll/poll_state_model.dart'; import 'package:gocast_mobile/models/settings/setting_state_model.dart'; @@ -11,6 +12,7 @@ import 'package:gocast_mobile/view_models/chat_view_model.dart'; import 'package:gocast_mobile/view_models/course_view_model.dart'; import 'package:gocast_mobile/view_models/notification_view_model.dart'; import 'package:gocast_mobile/view_models/download_view_model.dart'; +import 'package:gocast_mobile/view_models/pinned_view_model.dart'; import 'package:gocast_mobile/view_models/poll_view_model.dart'; import 'package:gocast_mobile/view_models/setting_view_model.dart'; import 'package:gocast_mobile/view_models/stream_view_model.dart'; @@ -52,6 +54,11 @@ final courseViewModelProvider = (ref) => CourseViewModel(ref.watch(grpcHandlerProvider)), ); +final pinnedCourseViewModelProvider = + StateNotifierProvider( + (ref) => PinnedViewModel(ref.watch(grpcHandlerProvider)), +); + final downloadViewModelProvider = StateNotifierProvider((ref) { return DownloadViewModel(); diff --git a/lib/view_models/chat_view_model.dart b/lib/view_models/chat_view_model.dart index 4a5f9b9e..cf8ce7cd 100644 --- a/lib/view_models/chat_view_model.dart +++ b/lib/view_models/chat_view_model.dart @@ -4,10 +4,8 @@ import 'package:gocast_mobile/base/networking/api/handler/chat_handler.dart'; import 'package:gocast_mobile/base/networking/api/handler/grpc_handler.dart'; import 'package:gocast_mobile/models/error/error_model.dart'; import 'package:gocast_mobile/models/chat/chat_state_model.dart'; -import 'package:logger/logger.dart'; class ChatViewModel extends StateNotifier { - final Logger _logger = Logger(); final GrpcHandler _grpcHandler; ChatViewModel(this._grpcHandler) : super(const ChatState()); @@ -20,7 +18,6 @@ class ChatViewModel extends StateNotifier { await ChatHandlers(_grpcHandler).getChatMessages(streamId); state = state.copyWith(messages: messages, isLoading: false); } catch (e) { - _logger.e(e); state = state.copyWith( error: e as AppError, isLoading: false, @@ -36,7 +33,6 @@ class ChatViewModel extends StateNotifier { await ChatHandlers(_grpcHandler).postChatMessage(streamId, message); state = state.addMessage(chatMessage); } catch (e) { - _logger.e(e); if (_isRateLimitError(e)) { state = state.copyWith(isRateLimitReached: true); await Future.delayed(const Duration(seconds: 10)); @@ -45,7 +41,7 @@ class ChatViewModel extends StateNotifier { } } else if (_isCoolDownError(e)) { state = state.copyWith(isCoolDown: true); - await Future.delayed(const Duration(seconds: 60)); + await Future.delayed(const Duration(seconds: 30)); if (mounted) { state = state.copyWith(isCoolDown: false); } @@ -62,7 +58,6 @@ class ChatViewModel extends StateNotifier { .postMessageReaction(messageId, streamId, emoji); state = state.addReaction(reaction); } catch (e) { - _logger.e(e); state = state.copyWith(error: e as AppError); } } @@ -73,7 +68,6 @@ class ChatViewModel extends StateNotifier { await ChatHandlers(_grpcHandler) .deleteMessageReaction(messageId, streamId, reactionId); } catch (e) { - _logger.e(e); state = state.copyWith(error: e as AppError); } } @@ -85,7 +79,6 @@ class ChatViewModel extends StateNotifier { .postChatReply(messageId, streamId, message); state = state.addReply(replay); } catch (e) { - _logger.e(e); state = state.copyWith(error: e as AppError); } } @@ -95,7 +88,6 @@ class ChatViewModel extends StateNotifier { await ChatHandlers(_grpcHandler) .markChatMessageAsResolved(messageId, streamId); } catch (e) { - _logger.e(e); state = state.copyWith(error: e as AppError); } } @@ -105,7 +97,6 @@ class ChatViewModel extends StateNotifier { await ChatHandlers(_grpcHandler) .markChatMessageAsUnresolved(messageId, streamId); } catch (e) { - _logger.e(e); state = state.copyWith(error: e as AppError); } } diff --git a/lib/view_models/course_view_model.dart b/lib/view_models/course_view_model.dart index 1f116bbe..cb7f676c 100644 --- a/lib/view_models/course_view_model.dart +++ b/lib/view_models/course_view_model.dart @@ -4,10 +4,8 @@ import 'package:gocast_mobile/base/networking/api/handler/course_handler.dart'; import 'package:gocast_mobile/base/networking/api/handler/grpc_handler.dart'; import 'package:gocast_mobile/models/course/course_state_model.dart'; import 'package:gocast_mobile/models/error/error_model.dart'; -import 'package:logger/logger.dart'; class CourseViewModel extends StateNotifier { - final Logger _logger = Logger(); final GrpcHandler _grpcHandler; CourseViewModel(this._grpcHandler) : super(const CourseState()); @@ -19,7 +17,6 @@ class CourseViewModel extends StateNotifier { state = state.copyWith(allCourses: courses, isLoading: false); return courses; } catch (e) { - _logger.e(e); state = state.copyWith(error: e as AppError, isLoading: false); return []; } diff --git a/lib/view_models/notification_view_model.dart b/lib/view_models/notification_view_model.dart index bf1b8949..1b653549 100644 --- a/lib/view_models/notification_view_model.dart +++ b/lib/view_models/notification_view_model.dart @@ -27,9 +27,12 @@ class NotificationViewModel extends StateNotifier { // Request permission from user await _firebaseMessaging.requestPermission(); - // Fetch device_token - final deviceToken = await _firebaseMessaging.getToken(); - + String? deviceToken; + try { + deviceToken = await _firebaseMessaging.getToken(); + }catch(e){ + throw AppError.notificationNotAvailableYet(); + } // Send device_token to API if (deviceToken != null) await postDeviceToken(deviceToken); diff --git a/lib/view_models/pinned_view_model.dart b/lib/view_models/pinned_view_model.dart new file mode 100644 index 00000000..34baae90 --- /dev/null +++ b/lib/view_models/pinned_view_model.dart @@ -0,0 +1,118 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; +import 'package:gocast_mobile/base/networking/api/handler/grpc_handler.dart'; +import 'package:gocast_mobile/base/networking/api/handler/pinned_handler.dart'; +import 'package:gocast_mobile/models/course/pinned_course_state_model.dart'; +import 'package:gocast_mobile/models/error/error_model.dart'; +import 'package:gocast_mobile/utils/sort_utils.dart'; +import 'package:logger/logger.dart'; + +class PinnedViewModel extends StateNotifier { + final Logger _logger = Logger(); + + final GrpcHandler _grpcHandler; + + PinnedViewModel(this._grpcHandler) : super(const PinnedCourseState()); + + Future fetchUserPinned() async { + state = state.copyWith(isLoading: true); + try { + var courses = await PinnedHandler(_grpcHandler).fetchUserPinned(); + state = state.copyWith(userPinned: courses, isLoading: false); + setUpDisplayedPinnedCourses(state.userPinned ?? []); + } catch (e) { + _logger.e(e); + state = state.copyWith(error: e as AppError, isLoading: false); + } + } + + + Future pinCourse(int courseID) async { + state = state.copyWith(isLoading: true); + try { + bool success = await PinnedHandler(_grpcHandler).pinCourse(courseID); + if (success) { + await fetchUserPinned(); + } else { + _logger.e('Failed to pin course'); + } + state = state.copyWith(isLoading: false); + return success; + } catch (e) { + _logger.e('Error pinning course: $e'); + state = state.copyWith(error: e as AppError, isLoading: false); + return false; + } + } + + + Future unpinCourse(int courseID) async { + state = state.copyWith(isLoading: true); + try { + bool success = await PinnedHandler(_grpcHandler).unpinCourse(courseID); + if (success) { + await fetchUserPinned(); + _logger.i('Course unpinned successfully'); + } else { + _logger.e('Failed to unpin course'); + } + state = state.copyWith(isLoading: false); + return success; + } catch (e) { + state = state.copyWith(error: e as AppError, isLoading: false); + return false; + } + } + + void updateSelectedPinnedSemester(String? semester, List allCourses) { + state = state.copyWith(selectedSemester: semester); + updatedDisplayedPinnedCourses( + CourseUtils.filterCoursesBySemester( + allCourses, + state.selectedSemester ?? 'All', + ), + ); + } + + void updatedDisplayedPinnedCourses(List displayedPinnedCourses) { + state = state.copyWith(displayedPinnedCourses: displayedPinnedCourses); + } + + void setUpDisplayedPinnedCourses(List allCourses) { + CourseUtils.sortCourses(allCourses, 'Newest First'); + updatedDisplayedPinnedCourses( + CourseUtils.filterCoursesBySemester( + allCourses, + state.selectedSemester ?? 'All', + ), + ); + } + + bool isCoursePinned(int id) { + if (state.userPinned == null) { + return false; + } + for (var course in state.userPinned!) { + if (course.id == id) { + return true; + } + } + return false; + } + + void setLoading(bool loading) { + state = state.copyWith(isLoading: loading); + } + + void setSemestersAsString(List semesters) { + state = state.copyWith( + semestersAsString: CourseUtils.convertAndSortSemesters(semesters, true), + ); + } + + void setSelectedSemester(String choice) { + state = state.copyWith(selectedSemester: choice); + } + + +} \ No newline at end of file diff --git a/lib/view_models/poll_view_model.dart b/lib/view_models/poll_view_model.dart index f9a55723..ce055acf 100644 --- a/lib/view_models/poll_view_model.dart +++ b/lib/view_models/poll_view_model.dart @@ -3,10 +3,8 @@ import 'package:gocast_mobile/base/networking/api/handler/poll_handler.dart'; import 'package:gocast_mobile/base/networking/api/handler/grpc_handler.dart'; import 'package:gocast_mobile/models/error/error_model.dart'; import 'package:gocast_mobile/models/poll/poll_state_model.dart'; // Make sure to create this file -import 'package:logger/logger.dart'; class PollViewModel extends StateNotifier { - final Logger _logger = Logger(); final GrpcHandler _grpcHandler; PollViewModel(this._grpcHandler) : super(const PollState()); @@ -18,7 +16,6 @@ class PollViewModel extends StateNotifier { final polls = await PollHandlers(_grpcHandler).getPolls(streamId); state = state.copyWith(polls: polls, isLoading: false); } catch (e) { - _logger.e(e); state = state.copyWith(error: e as AppError, isLoading: false); } } @@ -27,7 +24,6 @@ class PollViewModel extends StateNotifier { try { await PollHandlers(_grpcHandler).postPollVote(streamId, pollOptionId); } catch (e) { - _logger.e(e); state = state.copyWith(error: e as AppError); } } diff --git a/lib/view_models/setting_view_model.dart b/lib/view_models/setting_view_model.dart index 400f6491..4a7f9390 100644 --- a/lib/view_models/setting_view_model.dart +++ b/lib/view_models/setting_view_model.dart @@ -9,37 +9,25 @@ import 'package:logger/logger.dart'; import 'package:shared_preferences/shared_preferences.dart'; class SettingViewModel extends StateNotifier { - final Logger _logger = Logger(); final GrpcHandler _grpcHandler; SettingViewModel(this._grpcHandler) : super(const SettingState()); Future fetchUserSettings() async { - try { - _logger.i('Fetching user settings..'); final userSettings = await SettingsHandler(_grpcHandler).fetchUserSettings(); state = state.copyWith(userSettings: userSettings); - _logger.i('User settings fetched successfully'); - } catch (e) { - _logger.e('Error fetching user settings: $e'); - } } Future updateUserSettings(List updatedSettings) async { - _logger.i('Updating user settings..'); - try { + final success = await SettingsHandler(_grpcHandler) .updateUserSettings(updatedSettings); if (success) { state = state.copyWith(userSettings: updatedSettings); - _logger.i('User settings updated successfully'); } else { - _logger.e('Failed to update user settings'); + Logger().e('Failed to update user settings'); } - } catch (e) { - _logger.e('Error updating user settings: $e'); - } } Future loadPreferences() async { @@ -100,12 +88,8 @@ class SettingViewModel extends StateNotifier { } Future updatePreferredGreeting(String newGreeting) async { - try { await SettingsHandler(_grpcHandler).updateGreeting(newGreeting); await fetchUserSettings(); - } catch (e) { - _logger.e('Error updating greeting: $e'); - } } Future updatePreferredName(String newName) async { @@ -114,7 +98,6 @@ class SettingViewModel extends StateNotifier { await fetchUserSettings(); return true; } catch (e) { - _logger.e('Error updating preferred name: $e'); return false; } } diff --git a/lib/view_models/stream_view_model.dart b/lib/view_models/stream_view_model.dart index e09f507b..ad128a18 100644 --- a/lib/view_models/stream_view_model.dart +++ b/lib/view_models/stream_view_model.dart @@ -6,24 +6,20 @@ import 'package:gocast_mobile/base/networking/api/handler/stream_handler.dart'; import 'package:gocast_mobile/models/error/error_model.dart'; import 'package:gocast_mobile/models/video/stream_state_model.dart'; import 'package:gocast_mobile/utils/sort_utils.dart'; -import 'package:logger/logger.dart'; import 'package:tuple/tuple.dart'; class StreamViewModel extends StateNotifier { - final Logger _logger = Logger(); final GrpcHandler _grpcHandler; StreamViewModel(this._grpcHandler) : super(const StreamState()); Future fetchCourseStreams(int courseID) async { - _logger.i('Fetching streams'); state = state.copyWith(isLoading: true); try { final streams = await StreamHandler(_grpcHandler).fetchCourseStreams(courseID); state = state.copyWith(streams: streams, isLoading: false); } catch (e) { - _logger.e(e); state = state.copyWith(error: e as AppError, isLoading: false); } } @@ -104,7 +100,6 @@ class StreamViewModel extends StateNotifier { ? fetchStreamThumbnail(stream.id) : fetchVODThumbnail(stream.id)); } catch (e) { - _logger.e('Error fetching thumbnail for stream ID ${stream.id}: $e'); return '/thumb-fallback.png'; // Fallback thumbnail } } @@ -114,10 +109,8 @@ class StreamViewModel extends StateNotifier { /// [streamId] - The identifier of the stream. Future fetchStreamThumbnail(int streamId) async { try { - _logger.i('Fetching thumbnail for live stream ID: $streamId'); return await StreamHandler(_grpcHandler).fetchThumbnailStreams(streamId); } catch (e) { - _logger.e('Error fetching thumbnail for live stream: $e'); rethrow; } } @@ -127,34 +120,28 @@ class StreamViewModel extends StateNotifier { /// [streamId] - The identifier of the stream. Future fetchVODThumbnail(int streamId) async { try { - _logger.i('Fetching thumbnail for VOD stream ID: $streamId'); return await StreamHandler(_grpcHandler).fetchThumbnailVOD(streamId); } catch (e) { - _logger.e('Error fetching thumbnail for VOD stream: $e'); rethrow; } } Future fetchStream(int streamId) async { - _logger.i('Fetching stream'); state = state.copyWith(isLoading: true); try { final stream = await StreamHandler(_grpcHandler).fetchStream(streamId); state = state.copyWith(streams: [stream], isLoading: false); } catch (e) { - _logger.e(e); state = state.copyWith(error: e as AppError, isLoading: false); } } Future fetchLiveNowStreams() async { - _logger.i('Fetching live now stream'); state = state.copyWith(isLoading: true); try { final streams = await StreamHandler(_grpcHandler).fetchLiveNowStreams(); state = state.copyWith(liveStreams: streams, isLoading: false); } catch (e) { - _logger.e(e); state = state.copyWith(error: e as AppError, isLoading: false); } } @@ -185,25 +172,21 @@ class StreamViewModel extends StateNotifier { } Future updateProgress(int streamId, Progress progress) async { - _logger.i('Updating progress'); state = state.copyWith(isLoading: true); try { await StreamHandler(_grpcHandler).putProgress(streamId, progress); state = state.copyWith(isLoading: false, progress: progress); } catch (e) { - _logger.e(e); state = state.copyWith(error: e as AppError, isLoading: false); } } Future markAsWatched(int streamId) async { - _logger.i('Marking stream as watched'); state = state.copyWith(isLoading: true); try { await StreamHandler(_grpcHandler).markAsWatched(streamId); state = state.copyWith(isLoading: false, isWatched: true); } catch (e) { - _logger.e(e); state = state.copyWith(error: e as AppError, isLoading: false); } } diff --git a/lib/view_models/user_view_model.dart b/lib/view_models/user_view_model.dart index b41442ea..cd7b7a71 100644 --- a/lib/view_models/user_view_model.dart +++ b/lib/view_models/user_view_model.dart @@ -4,10 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pbgrpc.dart'; import 'package:gocast_mobile/base/networking/api/handler/auth_handler.dart'; -import 'package:gocast_mobile/base/networking/api/handler/bookmarks_handler.dart'; import 'package:gocast_mobile/base/networking/api/handler/course_handler.dart'; import 'package:gocast_mobile/base/networking/api/handler/grpc_handler.dart'; -import 'package:gocast_mobile/base/networking/api/handler/pinned_handler.dart'; import 'package:gocast_mobile/base/networking/api/handler/token_handler.dart'; import 'package:gocast_mobile/base/networking/api/handler/user_handler.dart'; import 'package:gocast_mobile/models/error/error_model.dart'; @@ -27,20 +25,15 @@ class UserViewModel extends StateNotifier { /// If the authentication is successful, it navigates to the courses screen. /// If the authentication fails, it shows an error message. Future handleBasicLogin(String email, String password) async { - state = state.copyWith(isLoading: true); + state = state.reset(); try { - _logger.i('Logging in user with email: $email'); + state = state.copyWith(isLoading: true); await AuthHandler.basicAuth(email, password); await fetchUser(); - _logger.i('Logged in user with basic auth'); - if (state.user != null) { - _logger.i('Logged in user ${state.user} with basic auth'); navigatorKey.currentState?.pushNamed('/navigationTab'); } - state = state.copyWith(isLoading: false); } catch (e) { - _logger.e(e); state = state.copyWith(error: e as AppError, isLoading: false); } } @@ -62,7 +55,6 @@ class UserViewModel extends StateNotifier { Future fetchUser() async { try { - _logger.i('Fetching user'); var user = await UserHandler(_grpcHandler).fetchUser(); state = state.copyWith(user: user, isLoading: false); } catch (e) { @@ -71,37 +63,15 @@ class UserViewModel extends StateNotifier { } } - Future fetchUserBookmarks() async { - state = state.copyWith(isLoading: true); - try { - _logger.i('Fetching user bookmarks'); - var bookmarks = await BooKMarkHandler(_grpcHandler).fetchUserBookmarks(); - state = state.copyWith(userBookmarks: bookmarks, isLoading: false); - } catch (e) { - _logger.e(e); - state = state.copyWith(error: e as AppError, isLoading: false); - } - } Future logout() async { await TokenHandler.deleteToken('jwt'); await TokenHandler.deleteToken('device_token'); - state = const UserState(); // Resets the state to its initial value + await TokenHandler.deleteToken('jwt_token'); + state=state.reset(); _logger.i('Logged out user and cleared tokens.'); } - bool isCoursePinned(int id) { - if (state.userPinned == null) { - return false; - } - for (var course in state.userPinned!) { - if (course.id == id) { - return true; - } - } - return false; - } - void setLoading(bool loading) { state = state.copyWith(isLoading: loading); } @@ -109,7 +79,6 @@ class UserViewModel extends StateNotifier { Future fetchSemesters() async { state = state.copyWith(isLoading: true); try { - _logger.i('Fetching Semesters'); var semesters = await CourseHandler(_grpcHandler).fetchSemesters(); semesters.item1.add(semesters.item2); state = state.copyWith(current: semesters.item2, isLoading: false); @@ -125,22 +94,9 @@ class UserViewModel extends StateNotifier { } } - Future fetchUserPinned() async { - state = state.copyWith(isLoading: true); - try { - var courses = await PinnedHandler(_grpcHandler).fetchUserPinned(); - state = state.copyWith(userPinned: courses, isLoading: false); - setUpDisplayedPinnedCourses(state.userPinned ?? []); - } catch (e) { - _logger.e(e); - state = state.copyWith(error: e as AppError, isLoading: false); - } - } - Future fetchPublicCourses() async { state = state.copyWith(isLoading: true); try { - _logger.i('Fetching public courses'); var courses = await CourseHandler(_grpcHandler).fetchPublicCourses(); state = state.copyWith(publicCourses: courses, isLoading: false); setUpDisplayedCourses(state.publicCourses ?? []); @@ -152,7 +108,6 @@ class UserViewModel extends StateNotifier { Future fetchUserCourses() async { state = state.copyWith(isLoading: true); try { - _logger.i('Fetching user courses'); var courses = await UserHandler(_grpcHandler).fetchUserCourses(); state = state.copyWith(userCourses: courses, isLoading: false); setUpDisplayedCourses(state.userCourses ?? []); @@ -162,46 +117,6 @@ class UserViewModel extends StateNotifier { } } - Future pinCourse(int courseID) async { - state = state.copyWith(isLoading: true); - try { - _logger.i('Pinning course with id: $courseID'); - bool success = await PinnedHandler(_grpcHandler).pinCourse(courseID); - if (success) { - await fetchUserPinned(); - _logger.i('Course pinned successfully'); - } else { - _logger.e('Failed to pin course'); - } - state = state.copyWith(isLoading: false); - return success; - } catch (e) { - _logger.e('Error pinning course: $e'); - state = state.copyWith(error: e as AppError, isLoading: false); - return false; - } - } - - Future unpinCourse(int courseID) async { - state = state.copyWith(isLoading: true); - try { - _logger.i('Unpinning course with id: $courseID'); - bool success = await PinnedHandler(_grpcHandler).unpinCourse(courseID); - if (success) { - await fetchUserPinned(); - _logger.i('Course unpinned successfully'); - } else { - _logger.e('Failed to unpin course'); - } - state = state.copyWith(isLoading: false); - return success; - } catch (e) { - _logger.e('Error unpinning course: $e'); - state = state.copyWith(error: e as AppError, isLoading: false); - return false; - } - } - void updateSelectedSemester(String? semester, List allCourses) { state = state.copyWith(selectedSemester: semester); updatedDisplayedCourses( @@ -212,16 +127,6 @@ class UserViewModel extends StateNotifier { ); } - void updateSelectedPinnedSemester(String? semester, List allCourses) { - state = state.copyWith(selectedSemester: semester); - updatedDisplayedPinnedCourses( - CourseUtils.filterCoursesBySemester( - allCourses, - state.selectedSemester ?? 'All', - ), - ); - } - void setSemestersAsString(List semesters) { state = state.copyWith( semestersAsString: CourseUtils.convertAndSortSemesters(semesters, true), @@ -232,9 +137,6 @@ class UserViewModel extends StateNotifier { state = state.copyWith(displayedCourses: displayedCourses); } - void updatedDisplayedPinnedCourses(List displayedPinnedCourses) { - state = state.copyWith(displayedPinnedCourses: displayedPinnedCourses); - } void setUpDisplayedCourses(List allCourses) { CourseUtils.sortCourses(allCourses, 'Newest First'); @@ -246,13 +148,8 @@ class UserViewModel extends StateNotifier { ); } - void setUpDisplayedPinnedCourses(List allCourses) { - CourseUtils.sortCourses(allCourses, 'Newest First'); - updatedDisplayedPinnedCourses( - CourseUtils.filterCoursesBySemester( - allCourses, - state.selectedSemester ?? 'All', - ), - ); + void setSelectedSemester(String choice) { + state = state.copyWith(selectedSemester: choice); } + } diff --git a/lib/views/chat_view/chat_view_state.dart b/lib/views/chat_view/chat_view_state.dart index b4017809..37601d35 100644 --- a/lib/views/chat_view/chat_view_state.dart +++ b/lib/views/chat_view/chat_view_state.dart @@ -162,7 +162,7 @@ class ChatViewState extends ConsumerState { suffix: GestureDetector( onTap: () => postMessage(context, ref, controller.text), child: _isCooldownActive - ? const CupertinoActivityIndicator() + ? const Icon(CupertinoIcons.arrow_up_circle_fill, color: CupertinoColors.systemGrey,) : const Icon(CupertinoIcons.arrow_up_circle_fill, color: CupertinoColors.activeBlue,), ), decoration: BoxDecoration( diff --git a/lib/views/components/filter_popup_menu_button.dart b/lib/views/components/filter_popup_menu_button.dart index 93993ce7..7e22ce19 100644 --- a/lib/views/components/filter_popup_menu_button.dart +++ b/lib/views/components/filter_popup_menu_button.dart @@ -14,18 +14,23 @@ class FilterPopupMenuButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + // Assuming both selectedSemester and pinnedSelectedSemester are always the same, + // we can just use one of them for the check mark logic. final selectedSemester = ref.read(userViewModelProvider).selectedSemester; - final selectedSortedOption = - ref.read(videoViewModelProvider).selectedFilterOption; + final selectedSortedOption = ref.read(videoViewModelProvider).selectedFilterOption; return PopupMenuButton( onSelected: (choice) { + // When an item is selected, update both providers to ensure they stay in sync + ref.read(userViewModelProvider.notifier).setSelectedSemester(choice); + ref.read(pinnedCourseViewModelProvider.notifier).setSelectedSemester(choice); onClick(choice); }, itemBuilder: (BuildContext context) { return filterOptions.map((choice) { - bool isSelected = - selectedSemester == choice || selectedSortedOption == choice; + // Check if the item is selected + bool isSelected = selectedSemester == choice || selectedSortedOption == choice; + return PopupMenuItem( value: choice, child: ListTile( @@ -33,8 +38,7 @@ class FilterPopupMenuButton extends ConsumerWidget { title: Text(choice), trailing: Opacity( opacity: isSelected ? 1.0 : 0.0, - child: - Icon(Icons.check, color: Theme.of(context).iconTheme.color), + child: Icon(Icons.check, color: Theme.of(context).iconTheme.color), ), ), ); @@ -43,4 +47,7 @@ class FilterPopupMenuButton extends ConsumerWidget { icon: Icon(Icons.filter_list, color: Theme.of(context).iconTheme.color), ); } + + + } diff --git a/lib/views/course_view/components/course_section.dart b/lib/views/course_view/components/course_section.dart index 1ae8bdff..aa874bb2 100644 --- a/lib/views/course_view/components/course_section.dart +++ b/lib/views/course_view/components/course_section.dart @@ -85,6 +85,7 @@ class CourseSection extends StatelessWidget { itemBuilder: (BuildContext context, int index) { final course = courses[index]; final userPinned = ref.watch(userViewModelProvider).userPinned ?? []; + final isPinned = userPinned.contains(course); return CourseCard( course: course, @@ -112,17 +113,17 @@ class CourseSection extends StatelessWidget { } Future _togglePin(Course course, bool isPinned) async { - final viewModel = ref.read(userViewModelProvider.notifier); + final pinnedViewModel = ref.read(pinnedCourseViewModelProvider.notifier); if (isPinned) { - await viewModel.unpinCourse(course.id); + await pinnedViewModel.unpinCourse(course.id); } else { - await viewModel.pinCourse(course.id); + await pinnedViewModel.pinCourse(course.id); } await _refreshPinnedCourses(); } Future _refreshPinnedCourses() async { - await ref.read(userViewModelProvider.notifier).fetchUserPinned(); + await ref.read(pinnedCourseViewModelProvider.notifier).fetchUserPinned(); } Widget _buildCoursesTitle(BuildContext context) { diff --git a/lib/views/course_view/components/pin_button.dart b/lib/views/course_view/components/pin_button.dart index 508b6680..db9dc320 100644 --- a/lib/views/course_view/components/pin_button.dart +++ b/lib/views/course_view/components/pin_button.dart @@ -24,11 +24,12 @@ class PinButton extends ConsumerWidget { color: Theme.of(context).colorScheme.primary, ), onPressed: () async { - final viewModel = ref.read(userViewModelProvider.notifier); + final pinnedViewModel = + ref.read(pinnedCourseViewModelProvider.notifier); if (isInitiallyPinned) { - await viewModel.unpinCourse(courseId); + await pinnedViewModel.unpinCourse(courseId); } else { - await viewModel.pinCourse(courseId); + await pinnedViewModel.pinCourse(courseId); } setState(() {}); // Trigger rebuild to update icon onPinStatusChanged(); diff --git a/lib/views/course_view/course_detail_view/course_detail_view.dart b/lib/views/course_view/course_detail_view/course_detail_view.dart index ec245992..61422703 100644 --- a/lib/views/course_view/course_detail_view/course_detail_view.dart +++ b/lib/views/course_view/course_detail_view/course_detail_view.dart @@ -250,7 +250,7 @@ class CourseDetailState extends ConsumerState { } bool _checkPinStatus() { - final userPinned = ref.watch(userViewModelProvider).userPinned ?? []; + final userPinned = ref.watch(pinnedCourseViewModelProvider).userPinned ?? []; // Iterate over the userPinned list and check if courseId matches for (var course in userPinned) { if (course.id == widget.courseId) { diff --git a/lib/views/course_view/courses_overview.dart b/lib/views/course_view/courses_overview.dart index 75f988d7..bb5d6b0b 100644 --- a/lib/views/course_view/courses_overview.dart +++ b/lib/views/course_view/courses_overview.dart @@ -23,6 +23,8 @@ class CourseOverviewState extends ConsumerState { void initState() { super.initState(); final userViewModelNotifier = ref.read(userViewModelProvider.notifier); + final pinnedViewModelNotifier = + ref.read(pinnedCourseViewModelProvider.notifier); final videoViewModelNotifier = ref.read(videoViewModelProvider.notifier); Future.microtask(() async { diff --git a/lib/views/course_view/list_courses_view/courses_list_view.dart b/lib/views/course_view/list_courses_view/courses_list_view.dart index f6a706fe..26f3c6a4 100644 --- a/lib/views/course_view/list_courses_view/courses_list_view.dart +++ b/lib/views/course_view/list_courses_view/courses_list_view.dart @@ -47,7 +47,7 @@ class CoursesList extends ConsumerWidget { ) { final liveStreams = ref.watch(videoViewModelProvider).liveStreams ?? []; var liveCourseIds = liveStreams.map((stream) => stream.courseID).toSet(); - final userPinned = ref.watch(userViewModelProvider).userPinned ?? []; + final userPinned = ref.watch(pinnedCourseViewModelProvider).userPinned ?? []; List liveCourses = courses.where((course) => liveCourseIds.contains(course.id)).toList(); return ConstrainedBox( @@ -64,12 +64,12 @@ class CoursesList extends ConsumerWidget { course: course, isPinned: isPinned, onPinUnpin: (course) { - final userViewModelNotifier = - ref.read(userViewModelProvider.notifier); + final pinnedViewModelNotifier = + ref.read(pinnedCourseViewModelProvider.notifier); if (isPinned) { - userViewModelNotifier.unpinCourse(course.id); + pinnedViewModelNotifier.unpinCourse(course.id); } else { - userViewModelNotifier.pinCourse(course.id); + pinnedViewModelNotifier.pinCourse(course.id); } }, title: course.name, diff --git a/lib/views/course_view/pinned_courses_view/pinned_courses_view.dart b/lib/views/course_view/pinned_courses_view/pinned_courses_view.dart index 4b07d883..82d2a899 100644 --- a/lib/views/course_view/pinned_courses_view/pinned_courses_view.dart +++ b/lib/views/course_view/pinned_courses_view/pinned_courses_view.dart @@ -1,3 +1,4 @@ + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; @@ -28,48 +29,50 @@ class PinnedCoursesState extends ConsumerState { } void _initializeCourses() { - final userViewModelNotifier = ref.read(userViewModelProvider.notifier); + final pinnedCourseViewModelNotifier = + ref.read(pinnedCourseViewModelProvider.notifier); Future.microtask(() async { - await userViewModelNotifier.fetchUserPinned(); + await pinnedCourseViewModelNotifier.fetchUserPinned(); }); } Future _refreshPinnedCourses() async { - await ref.read(userViewModelProvider.notifier).fetchUserPinned(); + await ref.read(pinnedCourseViewModelProvider.notifier).fetchUserPinned(); } void filterCoursesBySemester(String selectedSemester) { - var userPinned = ref.watch(userViewModelProvider).userPinned ?? []; + var userPinned = ref.watch(pinnedCourseViewModelProvider).userPinned ?? []; ref - .read(userViewModelProvider.notifier) + .read(pinnedCourseViewModelProvider.notifier) .updateSelectedPinnedSemester(selectedSemester, userPinned); } void _searchCourses() { - final userViewModelNotifier = ref.read(userViewModelProvider.notifier); + final pinnedViewModelNotifier = + ref.read(pinnedCourseViewModelProvider.notifier); final searchInput = searchController.text.toLowerCase(); var displayedCourses = - ref.watch(userViewModelProvider).displayedPinnedCourses ?? []; + ref.watch(pinnedCourseViewModelProvider).displayedPinnedCourses ?? []; if (!isSearchInitialized) { temp = List.from(displayedCourses); isSearchInitialized = true; } if (searchInput.isEmpty) { - userViewModelNotifier.updatedDisplayedPinnedCourses(temp); + pinnedViewModelNotifier.updatedDisplayedPinnedCourses(temp); isSearchInitialized = false; } else { displayedCourses = displayedCourses.where((course) { return course.name.toLowerCase().contains(searchInput) || course.slug.toLowerCase().contains(searchInput); }).toList(); - userViewModelNotifier.updatedDisplayedPinnedCourses(displayedCourses); + pinnedViewModelNotifier.updatedDisplayedPinnedCourses(displayedCourses); } } @override Widget build(BuildContext context) { final userPinned = - ref.watch(userViewModelProvider).displayedPinnedCourses ?? []; + ref.watch(pinnedCourseViewModelProvider).displayedPinnedCourses ?? []; final liveStreams = ref.watch(videoViewModelProvider).liveStreams ?? []; var liveCourseIds = liveStreams.map((stream) => stream.courseID).toSet(); List liveCourses = userPinned @@ -120,11 +123,11 @@ class PinnedCoursesState extends ConsumerState { } Future _togglePin(Course course, bool isPinned) async { - final viewModel = ref.read(userViewModelProvider.notifier); + final pinnedViewModel = ref.read(pinnedCourseViewModelProvider.notifier); if (isPinned) { - await viewModel.unpinCourse(course.id); + await pinnedViewModel.unpinCourse(course.id); } else { - await viewModel.pinCourse(course.id); + await pinnedViewModel.pinCourse(course.id); } await _refreshPinnedCourses(); } diff --git a/lib/views/login_view/internal_login_view.dart b/lib/views/login_view/internal_login_view.dart index a0168ad2..981c5d37 100644 --- a/lib/views/login_view/internal_login_view.dart +++ b/lib/views/login_view/internal_login_view.dart @@ -125,12 +125,16 @@ class InternalLoginScreenState extends ConsumerState { ), ), onPressed: () { - final viewModel = ref.read(userViewModelProvider.notifier); - viewModel.handleBasicLogin( + final userStateNotifier = ref.read(userViewModelProvider.notifier); + userStateNotifier.handleBasicLogin( usernameController.text, passwordController.text, ); - ref.read(settingViewModelProvider.notifier).fetchUserSettings(); + if ( userState.user != null) { + ref.read(settingViewModelProvider.notifier).fetchUserSettings(); + } else { + userState.copyWith(isLoading: false); + } }, child: userState.isLoading ? const CircularProgressIndicator(color: Colors.white, strokeWidth: 2) diff --git a/lib/views/video_view/utils/custom_video_control_bar.dart b/lib/views/video_view/utils/custom_video_control_bar.dart index 6a160d83..6b9f2369 100644 --- a/lib/views/video_view/utils/custom_video_control_bar.dart +++ b/lib/views/video_view/utils/custom_video_control_bar.dart @@ -81,9 +81,9 @@ class CustomVideoControlBar extends StatelessWidget { return Consumer( builder: (context, ref, child) { - final userViewModel = ref.read(userViewModelProvider.notifier); + final pinnedViewModel = ref.read(pinnedCourseViewModelProvider.notifier); final downloadViewModel = ref.read(downloadViewModelProvider.notifier); - final isPinned = userViewModel.isCoursePinned(currentStream.courseID); + final isPinned = pinnedViewModel.isCoursePinned(currentStream.courseID); final isDownloaded = downloadViewModel.isStreamDownloaded(currentStream.id); @@ -117,10 +117,10 @@ class CustomVideoControlBar extends StatelessWidget { if (choice == 'Pin Course') { isPinned ? await ref - .read(userViewModelProvider.notifier) + .read(pinnedCourseViewModelProvider.notifier) .unpinCourse(currentStream.courseID) : await ref - .read(userViewModelProvider.notifier) + .read(pinnedCourseViewModelProvider.notifier) .pinCourse(currentStream.courseID); } else if (choice == 'Download') { _showDownloadOptions(context);