From 24658e138777054adb827c8f5792fd1a5b8211ff Mon Sep 17 00:00:00 2001 From: Achraf Labidi <101757413+GravityDarkLab@users.noreply.github.com> Date: Mon, 29 Jan 2024 20:52:47 +0100 Subject: [PATCH] Disable pinning when user not logged in and add token check (#258) --- .../networking/api/handler/auth_handler.dart | 2 +- .../api/handler/settings_handler.dart | 5 ++-- lib/main.dart | 15 ++++++++--- lib/view_models/stream_view_model.dart | 2 ++ lib/view_models/user_view_model.dart | 26 ++++++++++++++++++- .../course_view/components/course_card.dart | 11 +++++--- .../components/course_section.dart | 9 +++++-- .../course_view/components/pin_button.dart | 6 +++++ .../components/small_stream_card.dart | 16 +++++------- lib/views/course_view/courses_overview.dart | 6 ++--- .../list_courses_view/courses_list_view.dart | 9 +++---- .../pinned_courses_view.dart | 1 + .../custom_playback_speed_view.dart | 4 +-- .../playback_speed_picker_view.dart | 2 +- pubspec.lock | 8 ++++++ pubspec.yaml | 1 + 16 files changed, 87 insertions(+), 36 deletions(-) diff --git a/lib/base/networking/api/handler/auth_handler.dart b/lib/base/networking/api/handler/auth_handler.dart index 108afc1e..612b54c8 100644 --- a/lib/base/networking/api/handler/auth_handler.dart +++ b/lib/base/networking/api/handler/auth_handler.dart @@ -64,7 +64,7 @@ class AuthHandler { _logger.i('JWT token saved successfully for user: $username'); } catch (e) { _logger.e('Error saving JWT token for user: $username, Error: $e'); - throw AppError.tokenSaveError(e); + throw AppError.userError(); } } diff --git a/lib/base/networking/api/handler/settings_handler.dart b/lib/base/networking/api/handler/settings_handler.dart index 476cf84c..81748a51 100644 --- a/lib/base/networking/api/handler/settings_handler.dart +++ b/lib/base/networking/api/handler/settings_handler.dart @@ -64,7 +64,7 @@ class SettingsHandler { /// This method sends a `patchUserSettings` gRPC call to update the user's preferred name on the server. /// * [newName] - The new preferred name. /// Returns `true` if the update was successful. - Future updatePreferredName(String newName) async { + Future updatePreferredName(String newName) async { try { _logger.i('Updating user settings...'); final request = PatchUserSettingsRequest() @@ -77,10 +77,9 @@ class SettingsHandler { _logger.i('User settings updated successfully'); }, ); - return true; } catch (e) { _logger.e('Error updating user settings: $e'); - return false; + rethrow; } } diff --git a/lib/main.dart b/lib/main.dart index 0ec324be..97160994 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,12 +15,12 @@ import 'firebase_options.dart'; final scaffoldMessengerKey = GlobalKey(); Future main() async { + Logger.level = Level.info; WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - Logger.level = Level.debug; runApp( - const ProviderScope( + const ProviderScope( child: App(), ), ); @@ -29,12 +29,19 @@ Future main() async { bool isPushNotificationListenerSet = false; class App extends ConsumerWidget { - const App({super.key}); + + const App({ + super.key, + }); + @override Widget build(BuildContext context, WidgetRef ref) { final userState = ref.watch(userViewModelProvider); + bool isLoggedIn = ref.watch(userViewModelProvider).user != null; + + _handleErrors(ref, userState); _setupNotifications(ref, userState); @@ -45,7 +52,7 @@ class App extends ConsumerWidget { ref.watch(themeModeProvider), // Use the theme mode from the provider navigatorKey: navigatorKey, scaffoldMessengerKey: scaffoldMessengerKey, - home: userState.user == null + home: !isLoggedIn ? const WelcomeScreen() : const NavigationTab(), routes: _buildRoutes(), diff --git a/lib/view_models/stream_view_model.dart b/lib/view_models/stream_view_model.dart index ad128a18..c399c47c 100644 --- a/lib/view_models/stream_view_model.dart +++ b/lib/view_models/stream_view_model.dart @@ -6,6 +6,7 @@ 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 { @@ -111,6 +112,7 @@ class StreamViewModel extends StateNotifier { try { return await StreamHandler(_grpcHandler).fetchThumbnailStreams(streamId); } catch (e) { + Logger().e(e); rethrow; } } diff --git a/lib/view_models/user_view_model.dart b/lib/view_models/user_view_model.dart index cd7b7a71..7be382ad 100644 --- a/lib/view_models/user_view_model.dart +++ b/lib/view_models/user_view_model.dart @@ -12,6 +12,7 @@ import 'package:gocast_mobile/models/error/error_model.dart'; import 'package:gocast_mobile/models/user/user_state_model.dart'; import 'package:gocast_mobile/utils/globals.dart'; import 'package:gocast_mobile/utils/sort_utils.dart'; +import 'package:jwt_decode/jwt_decode.dart'; import 'package:logger/logger.dart'; class UserViewModel extends StateNotifier { @@ -19,7 +20,11 @@ class UserViewModel extends StateNotifier { final GrpcHandler _grpcHandler; - UserViewModel(this._grpcHandler) : super(const UserState()); + UserViewModel(this._grpcHandler) : super(const UserState()){ + // Check if the user is already logged in + _checkToken(); + } + /// Handles basic authentication. /// If the authentication is successful, it navigates to the courses screen. @@ -152,4 +157,23 @@ class UserViewModel extends StateNotifier { state = state.copyWith(selectedSemester: choice); } + Future _checkToken() async { + String token = await _getToken(); + if(token.isNotEmpty && !Jwt.isExpired(token)) { + _logger.i('Token found, fetching user: $token'); + fetchUser(); + }else { + _logger.i('Token not found or expired'); + } + } + + Future _getToken() async { + try { + return await TokenHandler.loadToken('jwt'); + } catch(e){ + Logger().w("Token not found"); + return ''; + } + } + } diff --git a/lib/views/course_view/components/course_card.dart b/lib/views/course_view/components/course_card.dart index 1239abef..792ff385 100644 --- a/lib/views/course_view/components/course_card.dart +++ b/lib/views/course_view/components/course_card.dart @@ -15,6 +15,7 @@ class CourseCard extends StatelessWidget { final Course? course; final Function(Course)? onPinUnpin; final bool? isPinned; + final bool isLoggedIn; //for displaying livestreams final String? subtitle; @@ -30,6 +31,7 @@ class CourseCard extends StatelessWidget { this.course, this.onPinUnpin, this.isPinned, + required this.isLoggedIn, }); @override @@ -65,7 +67,8 @@ class CourseCard extends StatelessWidget { course!, onPinUnpin!, isPinned!, - ) + isLoggedIn, + ), ), ), ); @@ -78,11 +81,13 @@ class CourseCard extends StatelessWidget { Course course, Function(Course) onPinUnpin, bool isPinned, + bool isLoggedIn, ) { + return Slidable( key: ValueKey(course.id), closeOnScroll: true, - endActionPane: ActionPane( + endActionPane: isLoggedIn ? ActionPane( motion: const DrawerMotion(), dragDismissible: true, children: [ @@ -108,7 +113,7 @@ class CourseCard extends StatelessWidget { label: 'Pin', ), ], - ), + ) : null, child: IntrinsicHeight( child: Row( children: [ diff --git a/lib/views/course_view/components/course_section.dart b/lib/views/course_view/components/course_section.dart index a383298f..aa5d35e9 100644 --- a/lib/views/course_view/components/course_section.dart +++ b/lib/views/course_view/components/course_section.dart @@ -7,6 +7,8 @@ import 'package:gocast_mobile/utils/section_kind.dart'; import 'package:gocast_mobile/views/components/view_all_button.dart'; import 'package:gocast_mobile/views/course_view/components/course_card.dart'; import 'package:gocast_mobile/views/course_view/course_detail_view/course_detail_view.dart'; +import 'dart:math' as math; + /// CourseSection /// @@ -26,7 +28,7 @@ import 'package:gocast_mobile/views/course_view/course_detail_view/course_detail class CourseSection extends StatelessWidget { final String sectionTitle; final SectionKind - sectionKind; //0 for livestreams, 1 cor mycourses, 2 for puliccourses + sectionKind; final List courses; final List streams; final VoidCallback? onViewAll; @@ -74,9 +76,11 @@ class CourseSection extends StatelessWidget { Widget _buildCourseList(BuildContext context) { bool isTablet = MediaQuery.of(context).size.width >= 600 ? true : false; + int displayCount = math.min(courses.length, 3); + double cardHeight = 75; return ConstrainedBox( - constraints: BoxConstraints(maxHeight: isTablet ? 600 : 400), + constraints: BoxConstraints(maxHeight: isTablet ? double.infinity : cardHeight * displayCount,), child: ListView.builder( physics: const ClampingScrollPhysics(), shrinkWrap: true, @@ -89,6 +93,7 @@ class CourseSection extends StatelessWidget { final isPinned = userPinned.contains(course); return CourseCard( + isLoggedIn: ref.read(userViewModelProvider).user != null, course: course, isPinned: isPinned, onPinUnpin: (course) => _togglePin(course, isPinned), diff --git a/lib/views/course_view/components/pin_button.dart b/lib/views/course_view/components/pin_button.dart index db9dc320..0c4ab797 100644 --- a/lib/views/course_view/components/pin_button.dart +++ b/lib/views/course_view/components/pin_button.dart @@ -16,6 +16,12 @@ class PinButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + bool isLoggedIn = ref.read(userViewModelProvider).user != null; + if (!isLoggedIn) { + return const Icon( + Icons.push_pin_outlined, + color: Colors.transparent,); + } return StatefulBuilder( builder: (context, setState) { return IconButton( diff --git a/lib/views/course_view/components/small_stream_card.dart b/lib/views/course_view/components/small_stream_card.dart index e894a034..0e280645 100644 --- a/lib/views/course_view/components/small_stream_card.dart +++ b/lib/views/course_view/components/small_stream_card.dart @@ -112,20 +112,19 @@ class SmallStreamCard extends StatelessWidget { } Widget _buildCourseImage() { - // Assuming `path` is now a URL string return Stack( children: [ AspectRatio( - aspectRatio: 16 / 12, // Maintain the same aspect ratio + aspectRatio: 16 / 12, child: ClipRRect( borderRadius: BorderRadius.circular(8.0), - // Keep the rounded corners child: Image.network( - path!, // Use the image URL - fit: BoxFit.cover, // Maintain the cover fit + path!, + fit: BoxFit.cover, loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) - return child; // Image is fully loaded + if (loadingProgress == null) { + return child; + } return Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null @@ -136,17 +135,14 @@ class SmallStreamCard extends StatelessWidget { ); }, errorBuilder: (context, error, stackTrace) { - // Provide a fallback asset image in case of error return Image.asset( AppImages.course1, - // Path to your default/fallback image asset fit: BoxFit.cover, ); }, ), ), ), - // If you have additional overlays like in the thumbnail widget, add them here ], ); } diff --git a/lib/views/course_view/courses_overview.dart b/lib/views/course_view/courses_overview.dart index 27796cba..02993f60 100644 --- a/lib/views/course_view/courses_overview.dart +++ b/lib/views/course_view/courses_overview.dart @@ -45,7 +45,7 @@ class CourseOverviewState extends ConsumerState { Widget build(BuildContext context) { if (isLoading) { // Show a loading spinner when data is being fetched - return Center(child: CircularProgressIndicator()); + return const Center(child: CircularProgressIndicator()); } final userCourses = ref.watch(userViewModelProvider).userCourses ?? []; final publicCourses = ref.watch(userViewModelProvider).publicCourses ?? []; @@ -165,7 +165,7 @@ class CourseOverviewState extends ConsumerState { Future _refreshData() async { setState( - () => isLoading = true); // Set loading to true at the start of refresh + () => isLoading = true,); // Set loading to true at the start of refresh final userViewModelNotifier = ref.read(userViewModelProvider.notifier); await userViewModelNotifier.fetchUserCourses(); @@ -174,6 +174,6 @@ class CourseOverviewState extends ConsumerState { await ref.read(videoViewModelProvider.notifier).fetchLiveThumbnails(); setState(() => - isLoading = false); // Set loading to false once refresh is complete + isLoading = false,); // Set loading to false once refresh is complete } } 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 26f3c6a4..3d338d02 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 @@ -50,9 +50,7 @@ class CoursesList extends ConsumerWidget { final userPinned = ref.watch(pinnedCourseViewModelProvider).userPinned ?? []; List liveCourses = courses.where((course) => liveCourseIds.contains(course.id)).toList(); - return ConstrainedBox( - constraints: BoxConstraints(maxHeight: isTablet ? 600 : 400), - child: ListView.builder( + return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), scrollDirection: Axis.vertical, @@ -61,6 +59,7 @@ class CoursesList extends ConsumerWidget { final course = courses[index]; final isPinned = userPinned.contains(course); return CourseCard( + isLoggedIn: ref.read(userViewModelProvider).user != null, course: course, isPinned: isPinned, onPinUnpin: (course) { @@ -88,8 +87,6 @@ class CoursesList extends ConsumerWidget { ); }, ); - }, - ), - ); + },); } } 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 82d2a899..d52feec6 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 @@ -94,6 +94,7 @@ class PinnedCoursesState extends ConsumerState { final isPinned = userPinned.any((pinnedCourse) => pinnedCourse.id == course.id); return CourseCard( + isLoggedIn: true, course: course, isPinned: isPinned, onPinUnpin: (course) => _togglePin(course, isPinned), diff --git a/lib/views/settings_view/custom_playback_speed_view.dart b/lib/views/settings_view/custom_playback_speed_view.dart index 3999be26..61821214 100644 --- a/lib/views/settings_view/custom_playback_speed_view.dart +++ b/lib/views/settings_view/custom_playback_speed_view.dart @@ -15,7 +15,7 @@ void showAddCustomSpeedDialog( errorMessage = ''; } else { double? parsedValue = double.tryParse(value); - if (parsedValue != null && parsedValue >= 0.25 && parsedValue <= 4.0) { + if (parsedValue != null && parsedValue >= 0.25 && parsedValue <= 2.0) { List splitValue = value.split('.'); if ((splitValue[0].length > 1) || (splitValue.length > 1 && splitValue[1].length > 2)) { @@ -24,7 +24,7 @@ void showAddCustomSpeedDialog( customSpeed = parsedValue; } } else { - errorMessage = 'Please enter a number between\n0.25 and 4.0'; + errorMessage = 'Please enter a number between\n0.25 and 2.0'; } } } diff --git a/lib/views/settings_view/playback_speed_picker_view.dart b/lib/views/settings_view/playback_speed_picker_view.dart index dd21312a..8bb39679 100644 --- a/lib/views/settings_view/playback_speed_picker_view.dart +++ b/lib/views/settings_view/playback_speed_picker_view.dart @@ -7,7 +7,7 @@ void showPlaybackSpeedsPicker( List selectedSpeeds, Function(double, bool) updateSelectedSpeeds, ) { - List defaultSpeeds = List.generate(14, (index) => (index + 1) * 0.25); + List defaultSpeeds = List.generate(8, (index) => (index + 1) * 0.25); if (!selectedSpeeds.contains(1.0)) { selectedSpeeds.add(1.0); diff --git a/pubspec.lock b/pubspec.lock index 29c2264d..8d88fbba 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -400,6 +400,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + jwt_decode: + dependency: "direct main" + description: + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + url: "https://pub.dev" + source: hosted + version: "0.3.1" lints: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b21b954e..eed6e5a3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,7 @@ dependencies: chewie: ^1.7.4 url_launcher: ^6.2.3 path_provider: any + jwt_decode: ^0.3.1 dev_dependencies: flutter_test: