diff --git a/lib/base/networking/api/handler/settings_handler.dart b/lib/base/networking/api/handler/settings_handler.dart index 4ff55e93..4eeff322 100644 --- a/lib/base/networking/api/handler/settings_handler.dart +++ b/lib/base/networking/api/handler/settings_handler.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pbgrpc.dart'; import 'package:logger/logger.dart'; @@ -65,4 +67,88 @@ class SettingsHandler { rethrow; } } + + /// Parses playback speeds from the user settings. + List parsePlaybackSpeeds(List? userSettings) { + final playbackSpeedSetting = userSettings?.firstWhere( + (setting) => setting.type == UserSettingType.CUSTOM_PLAYBACK_SPEEDS, + orElse: () => UserSetting(value: jsonEncode([])), + ); + + if (playbackSpeedSetting != null && playbackSpeedSetting.value.isNotEmpty) { + try { + final List speedsJson = jsonDecode(playbackSpeedSetting.value); + return speedsJson + .where((item) => item['enabled'] == true) + .map((item) => double.parse(item['speed'].toString())) + .toList(); + } catch (e) { + _logger.e('Error parsing playback speeds: $e'); + return []; + } + } + return []; + } + + /// Updates the preferred greeting in user settings. + Future updatePreferredGreeting( + String newGreeting, List currentSettings) async { + try { + var greetingSetting = currentSettings.firstWhere( + (setting) => setting.type == UserSettingType.GREETING, + orElse: () => + UserSetting(type: UserSettingType.GREETING, value: newGreeting), + ); + greetingSetting.value = newGreeting; + + if (!currentSettings.contains(greetingSetting)) { + currentSettings.add(greetingSetting); + } + await updateUserSettings(currentSettings); + return true; + } catch (e) { + _logger.e('Error updating greeting: $e'); + return false; + } + } + + /// Updates the preferred name in user settings. + Future updatePreferredName( + String newName, List currentSettings) async { + try { + var newSetting = + UserSetting(type: UserSettingType.PREFERRED_NAME, value: newName); + await updateUserSettings([newSetting]); + return true; + } catch (e) { + _logger.e('Error updating preferred name: $e'); + return false; + } + } + + /// Updates the selected speeds in user settings. + Future updateSelectedSpeeds( + double speed, bool isSelected, List currentSettings) async { + var playbackSpeedSetting = currentSettings.firstWhere( + (setting) => setting.type == UserSettingType.CUSTOM_PLAYBACK_SPEEDS, + orElse: () => UserSetting( + type: UserSettingType.CUSTOM_PLAYBACK_SPEEDS, + value: jsonEncode([]), + ), + ); + + List updatedSpeeds = parsePlaybackSpeeds([playbackSpeedSetting]); + if (isSelected && !updatedSpeeds.contains(speed)) { + updatedSpeeds.add(speed); + } else if (!isSelected) { + updatedSpeeds.remove(speed); + } + + List> speedsList = updatedSpeeds + .map((speed) => {"speed": speed, "enabled": true}) + .toList(); + playbackSpeedSetting.value = jsonEncode(speedsList); + + await updateUserSettings(currentSettings); + } } diff --git a/lib/models/user/user_state_model.dart b/lib/models/user/user_state_model.dart index 2486f456..1d41f590 100644 --- a/lib/models/user/user_state_model.dart +++ b/lib/models/user/user_state_model.dart @@ -15,6 +15,9 @@ class UserState { final List? bannerAlerts; final AppError? error; final List? downloadedCourses; + final bool isDarkMode; + final bool isPushNotificationsEnabled; + final bool isDownloadWithWifiOnly; const UserState({ this.isLoading = false, @@ -28,6 +31,9 @@ class UserState { this.bannerAlerts, this.error, this.downloadedCourses, + this.isDarkMode = false, + this.isPushNotificationsEnabled = true, + this.isDownloadWithWifiOnly = true, }); UserState copyWith({ @@ -42,6 +48,9 @@ class UserState { List? bannerAlerts, AppError? error, List? downloadedCourses, + bool? isDarkMode, + bool? isPushNotificationsEnabled, + bool? isDownloadWithWifiOnly, }) { return UserState( isLoading: isLoading ?? this.isLoading, @@ -55,6 +64,11 @@ class UserState { bannerAlerts: bannerAlerts ?? this.bannerAlerts, error: error ?? this.error, downloadedCourses: downloadedCourses ?? this.downloadedCourses, + isDarkMode: isDarkMode ?? this.isDarkMode, + isPushNotificationsEnabled: + isPushNotificationsEnabled ?? this.isPushNotificationsEnabled, + isDownloadWithWifiOnly: + isDownloadWithWifiOnly ?? this.isDownloadWithWifiOnly, ); } diff --git a/lib/view_models/user_view_model.dart b/lib/view_models/user_view_model.dart index 8b370796..a2a9348b 100644 --- a/lib/view_models/user_view_model.dart +++ b/lib/view_models/user_view_model.dart @@ -8,6 +8,7 @@ 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/settings_handler.dart'; import 'package:gocast_mobile/base/networking/api/handler/token_handler.dart'; import 'package:gocast_mobile/base/networking/api/handler/user_handler.dart'; @@ -15,7 +16,9 @@ import 'package:gocast_mobile/models/error/error_model.dart'; import 'package:gocast_mobile/models/user/user_state_model.dart'; import 'package:logger/logger.dart'; import 'package:gocast_mobile/base/networking/api/handler/notification_handler.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../providers.dart'; import '../utils/globals.dart'; class UserViewModel extends StateNotifier { @@ -34,6 +37,7 @@ class UserViewModel extends StateNotifier { _logger.i('Logging in user with email: $email'); await AuthHandler.basicAuth(email, password); await fetchUser(); + await fetchUserSettings(); _logger.i('Logged in user with basic auth'); if (state.user != null) { @@ -139,15 +143,10 @@ class UserViewModel extends StateNotifier { Future fetchUserSettings() async { try { _logger.i('Fetching user settings..'); - final response = await _grpcHandler.callGrpcMethod( - (client) async { - final response = - await client.getUserSettings(GetUserSettingsRequest()); - return response.userSettings; - }, - ); + final userSettings = + await SettingsHandler(_grpcHandler).fetchUserSettings(); + state = state.copyWith(userSettings: userSettings); _logger.i('User settings fetched successfully'); - state = state.copyWith(userSettings: response); } catch (e) { _logger.e('Error fetching user settings: $e'); } @@ -156,16 +155,14 @@ class UserViewModel extends StateNotifier { Future updateUserSettings(List updatedSettings) async { _logger.i('Updating user settings..'); try { - final request = PatchUserSettingsRequest() - ..userSettings.addAll(updatedSettings); - - await _grpcHandler.callGrpcMethod( - (client) async { - await client.patchUserSettings(request); - }, - ); - state = state.copyWith(userSettings: updatedSettings); - _logger.i('User settings updated successfully'); + 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'); + } } catch (e) { _logger.e('Error updating user settings: $e'); } @@ -234,4 +231,81 @@ class UserViewModel extends StateNotifier { void setLoading(bool isLoading) { state = state.copyWith(isLoading: isLoading); } + + Future loadPreferences() async { + final prefs = await SharedPreferences.getInstance(); + await loadThemePreference(prefs); + await loadNotificationPreference(prefs); + await fetchUserSettings(); + } + + Future loadThemePreference(SharedPreferences prefs) async { + final themePreference = prefs.getString('themeMode') ?? 'light'; + state = state.copyWith(isDarkMode: themePreference == 'dark'); + } + + Future saveThemePreference(String theme, WidgetRef ref) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('themeMode', theme); + + state = state.copyWith(isDarkMode: theme == 'dark'); + + ref.read(themeModeProvider.notifier).state = + theme == 'dark' ? ThemeMode.dark : ThemeMode.light; + } + + Future loadNotificationPreference(SharedPreferences prefs) async { + final notificationPreference = prefs.getBool('notifications') ?? true; + state = state.copyWith(isPushNotificationsEnabled: notificationPreference); + } + + Future saveNotificationPreference(bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('notifications', value); + state = state.copyWith(isPushNotificationsEnabled: value); + } + + Future saveDownloadWifiOnlyPreference(bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('downloadWifiOnly', value); + state = state.copyWith(isDownloadWithWifiOnly: value); + } + + // This static method can stay within UserViewModel as it doesn't interact with the API + static List getDefaultSpeeds() { + return List.generate(14, (i) => (i + 1) * 0.25); + } + + Future updatePreferredGreeting(String newGreeting) async { + try { + await SettingsHandler(_grpcHandler) + .updatePreferredGreeting(newGreeting, state.userSettings ?? []); + await fetchUserSettings(); + } catch (e) { + _logger.e('Error updating greeting: $e'); + } + } + + Future updatePreferredName(String newName) async { + try { + await SettingsHandler(_grpcHandler) + .updatePreferredName(newName, state.userSettings ?? []); + await fetchUserSettings(); + return true; + } catch (e) { + _logger.e('Error updating preferred name: $e'); + return false; + } + } + + Future updateSelectedSpeeds(double speed, bool isSelected) async { + await SettingsHandler(_grpcHandler) + .updateSelectedSpeeds(speed, isSelected, state.userSettings ?? []); + await fetchUserSettings(); + } + + List parsePlaybackSpeeds() { + return SettingsHandler(_grpcHandler) + .parsePlaybackSpeeds(state.userSettings); + } } diff --git a/lib/views/settings_view/authentication_error_card_view.dart b/lib/views/settings_view/authentication_error_card_view.dart new file mode 100644 index 00000000..b0aa1e41 --- /dev/null +++ b/lib/views/settings_view/authentication_error_card_view.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gocast_mobile/providers.dart'; + +Future showAuthenticationErrorCard( + BuildContext context, + WidgetRef ref, +) async { + final userState = ref.read(userViewModelProvider); + bool isAuthenticated = userState.user != null; + + if (!isAuthenticated) { + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + backgroundColor: Theme.of(context).colorScheme.onPrimary, + title: const Text('Authentication Required'), + content: const Text('Please log in to access this feature.'), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ); + }, + ); + } + return isAuthenticated; +} diff --git a/lib/views/settings_view/custom_playback_speed_view.dart b/lib/views/settings_view/custom_playback_speed_view.dart new file mode 100644 index 00000000..cfe4a981 --- /dev/null +++ b/lib/views/settings_view/custom_playback_speed_view.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; + +void showAddCustomSpeedDialog( + BuildContext context, + Function(double) onSpeedAdded, +) { + double customSpeed = 1.0; + String errorMessage = ''; + + void validateAndSetSpeed(String value) { + errorMessage = ''; + + if (value.isEmpty) { + errorMessage = ''; + } else { + double? parsedValue = double.tryParse(value); + if (parsedValue != null && parsedValue >= 0.25 && parsedValue <= 4.0) { + List splitValue = value.split('.'); + if ((splitValue[0].length > 1) || + (splitValue.length > 1 && splitValue[1].length > 2)) { + errorMessage = 'Number is too long'; + } else { + customSpeed = parsedValue; + } + } else { + errorMessage = 'Please enter a number between\n0.25 and 4.0'; + } + } + } + + showDialog( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + backgroundColor: Theme.of(context).colorScheme.onPrimary, + title: const Text('Add Custom Speed'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + decoration: InputDecoration( + hintText: 'Enter speed (e.g., 1.7)', + errorText: errorMessage.isNotEmpty ? errorMessage : null, + ), + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + onChanged: (value) { + setState(() { + validateAndSetSpeed(value); + }); + }, + ), + ], + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + onPressed: errorMessage.isEmpty && + customSpeed >= 0.25 && + customSpeed <= 4.0 + ? () { + Navigator.of(context).pop(); + onSpeedAdded(customSpeed); + } + : null, + child: const Text('Add'), + ), + ], + ); + }, + ); + }, + ); +} diff --git a/lib/views/settings_view/edit_profile_screen_view.dart b/lib/views/settings_view/edit_profile_screen_view.dart new file mode 100644 index 00000000..6c2f2c19 --- /dev/null +++ b/lib/views/settings_view/edit_profile_screen_view.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gocast_mobile/providers.dart'; + +class EditProfileScreen extends ConsumerStatefulWidget { + const EditProfileScreen({super.key}); + + @override + EditProfileScreenState createState() => EditProfileScreenState(); +} + +class EditProfileScreenState extends ConsumerState { + final TextEditingController preferredNameController = TextEditingController(); + String infoText = ''; + bool isError = false; + + @override + void initState() { + super.initState(); + preferredNameController.addListener(_updateCharacterCount); + } + + @override + void dispose() { + preferredNameController.removeListener(_updateCharacterCount); + preferredNameController.dispose(); + super.dispose(); + } + + void _updateCharacterCount() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Edit Profile'), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Preferred Name', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + TextField( + controller: preferredNameController, + maxLength: 50, + decoration: InputDecoration( + hintText: 'Enter your preferred name', + border: const OutlineInputBorder(), + counterText: '${preferredNameController.text.length}/50', + ), + ), + const SizedBox(height: 16), + RichText( + text: TextSpan( + style: TextStyle( + color: Theme.of(context).colorScheme.scrim.withOpacity(0.50), + ), + children: const [ + TextSpan(text: 'You can change this '), + TextSpan( + text: 'once every three months.', + style: TextStyle( + fontStyle: FontStyle.italic, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + onPressed: () => _onSaveButtonPressed(), + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Text('Save'), + ), + ), + const SizedBox(height: 16), + Text( + infoText, + style: TextStyle( + color: isError + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.secondary, + ), + ), + ], + ), + ), + ); + } + + void _onSaveButtonPressed() { + if (preferredNameController.text.isNotEmpty) { + _updatePreferredName(preferredNameController.text); + } else { + setState(() { + infoText = 'Please enter a preferred name'; + isError = true; + }); + } + } + + Future _updatePreferredName(String name) async { + try { + bool success = await ref + .read(userViewModelProvider.notifier) + .updatePreferredName(name); + if (success) { + setState(() { + infoText = 'Preferred name saved: $name'; + isError = false; + }); + } else { + setState(() { + infoText = 'Error updating preferred name'; + isError = true; + }); + } + } catch (e) { + setState(() { + infoText = 'An error occurred'; + isError = true; + }); + } + } +} diff --git a/lib/views/settings_view/playback_speed_picker_view.dart b/lib/views/settings_view/playback_speed_picker_view.dart new file mode 100644 index 00000000..ea2f35b5 --- /dev/null +++ b/lib/views/settings_view/playback_speed_picker_view.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +void showPlaybackSpeedsPicker( + BuildContext context, + WidgetRef ref, + List selectedSpeeds, + Function(double, bool) updateSelectedSpeeds, +) { + List defaultSpeeds = List.generate(14, (index) => (index + 1) * 0.25); + + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setModalState) { + return Padding( + padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0), + child: Column( + children: [ + Expanded( + child: ListView( + children: [ + for (var speed in defaultSpeeds) + _buildSpeedTile( + speed: speed, + isSelected: selectedSpeeds.contains(speed), + onTap: (double speed, bool isSelected) { + setModalState(() { + isSelected + ? selectedSpeeds.add(speed) + : selectedSpeeds.remove(speed); + updateSelectedSpeeds(speed, isSelected); + }); + }, + ), + if (selectedSpeeds + .any((speed) => !defaultSpeeds.contains(speed))) ...[ + const Divider(), + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + 'Custom Playback Speeds', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + for (var speed in selectedSpeeds + .where((speed) => !defaultSpeeds.contains(speed))) + _buildSpeedTile( + speed: speed, + isSelected: true, + onTap: (double speed, bool isSelected) { + setModalState(() { + isSelected + ? selectedSpeeds.add(speed) + : selectedSpeeds.remove(speed); + updateSelectedSpeeds(speed, isSelected); + }); + }, + ), + ], + ], + ), + ), + ], + ), + ); + }, + ); + }, + ); +} + +Widget _buildSpeedTile({ + required double speed, + required bool isSelected, + required Function(double, bool) onTap, +}) { + return ListTile( + title: Text('${speed}x'), + trailing: isSelected ? const Icon(Icons.check) : const SizedBox(), + onTap: () => onTap(speed, !isSelected), + ); +} diff --git a/lib/views/settings_view/playback_speed_settings_view.dart b/lib/views/settings_view/playback_speed_settings_view.dart new file mode 100644 index 00000000..f13eb5dc --- /dev/null +++ b/lib/views/settings_view/playback_speed_settings_view.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gocast_mobile/providers.dart'; +import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; +import 'package:gocast_mobile/views/settings_view/playback_speed_picker_view.dart'; +import 'package:gocast_mobile/views/settings_view/custom_playback_speed_view.dart'; +import 'package:gocast_mobile/views/settings_view/authentication_error_card_view.dart'; + +class PlaybackSpeedSettings extends ConsumerWidget { + const PlaybackSpeedSettings({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userState = ref.watch(userViewModelProvider); + final selectedPlaybackSpeeds = + _parsePlaybackSpeeds(userState.userSettings, ref); + + return Column( + children: [ + _buildPlaybackSpeedsTile(context, ref, selectedPlaybackSpeeds), + _buildCustomPlaybackSpeedsTile(context, ref), + ], + ); + } + + ListTile _buildPlaybackSpeedsTile( + BuildContext context, + WidgetRef ref, + List playbackSpeeds, + ) { + return ListTile( + title: const Text('Playback Speeds'), + trailing: const Icon(Icons.arrow_forward_ios), + onTap: () => showPlaybackSpeedsPicker( + context, + ref, + playbackSpeeds, + (double speed, bool isSelected) { + _updateSelectedSpeeds(context, ref, speed, isSelected); + }, + ), + ); + } + + ListTile _buildCustomPlaybackSpeedsTile(BuildContext context, WidgetRef ref) { + return ListTile( + title: const Text('Add Custom Playback Speed'), + onTap: () { + showAddCustomSpeedDialog(context, (double customSpeed) { + _updateSelectedSpeeds(context, ref, customSpeed, true); + }); + }, + ); + } + + void _updateSelectedSpeeds( + BuildContext context, + WidgetRef ref, + double speed, + bool isSelected, + ) async { + bool isAuthenticated = await showAuthenticationErrorCard(context, ref); + if (!isAuthenticated) return; + + await ref + .read(userViewModelProvider.notifier) + .updateSelectedSpeeds(speed, isSelected); + } + + List _parsePlaybackSpeeds( + List? userSettings, + WidgetRef ref, + ) { + return ref.read(userViewModelProvider.notifier).parsePlaybackSpeeds(); + } +} diff --git a/lib/views/settings_view/preferred_greeting_view.dart b/lib/views/settings_view/preferred_greeting_view.dart new file mode 100644 index 00000000..3cea485c --- /dev/null +++ b/lib/views/settings_view/preferred_greeting_view.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gocast_mobile/providers.dart'; +import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; +import 'package:gocast_mobile/views/settings_view/authentication_error_card_view.dart'; + +class PreferredGreetingView extends ConsumerWidget { + const PreferredGreetingView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userState = ref.watch(userViewModelProvider); + final currentGreeting = userState.userSettings + ?.firstWhere( + (setting) => setting.type == UserSettingType.GREETING, + orElse: () => UserSetting(value: 'Default Greeting'), + ) + .value ?? + 'Default Greeting'; + + return ListTile( + title: const Text('Preferred Greeting'), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildGreetingRadioOption(context, ref, 'Servus', currentGreeting), + _buildGreetingRadioOption(context, ref, 'Moin', currentGreeting), + ], + ), + ); + } + + Widget _buildGreetingRadioOption( + BuildContext context, + WidgetRef ref, + String greeting, + String currentGreeting, + ) { + bool isSelected = greeting == currentGreeting; + + return Row( + children: [ + Radio( + value: greeting, + groupValue: currentGreeting, + onChanged: (String? newValue) async { + if (newValue != null) { + bool isAuthenticated = + await showAuthenticationErrorCard(context, ref); + if (isAuthenticated) { + await ref + .read(userViewModelProvider.notifier) + .updatePreferredGreeting(newValue); + } + } + }, + ), + Text( + greeting, + style: TextStyle( + fontSize: isSelected ? 15 : 13, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected ? Theme.of(context).colorScheme.primary : null, + ), + ), + ], + ); + } +} diff --git a/lib/views/settings_view/settings_screen_view.dart b/lib/views/settings_view/settings_screen_view.dart index 509e5c35..3180c7b7 100644 --- a/lib/views/settings_view/settings_screen_view.dart +++ b/lib/views/settings_view/settings_screen_view.dart @@ -2,7 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gocast_mobile/providers.dart'; import 'package:gocast_mobile/views/on_boarding_view/welcome_screen_view.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:gocast_mobile/views/settings_view/playback_speed_settings_view.dart'; +import 'package:gocast_mobile/views/settings_view/preferred_greeting_view.dart'; +import 'package:gocast_mobile/views/settings_view/edit_profile_screen_view.dart'; +import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; +import 'package:gocast_mobile/views/settings_view/authentication_error_card_view.dart'; class SettingsScreen extends ConsumerStatefulWidget { const SettingsScreen({super.key}); @@ -12,91 +16,88 @@ class SettingsScreen extends ConsumerStatefulWidget { } class _SettingsScreenState extends ConsumerState { - bool isDarkMode = false; - bool isPushNotificationsEnabled = false; - bool isDownloadOverWifiOnly = false; + final GlobalKey _scaffoldKey = GlobalKey(); @override void initState() { super.initState(); - _loadThemePreference(); - } - - Future _loadThemePreference() async { - final prefs = await SharedPreferences.getInstance(); - final themePreference = prefs.getString('themeMode') ?? 'light'; - setState(() { - isDarkMode = themePreference == 'dark'; - }); - } - - Future _saveThemePreference(String theme) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString('themeMode', theme); + ref.read(userViewModelProvider.notifier).loadPreferences(); } @override Widget build(BuildContext context) { - final themeMode = ref.watch(themeModeProvider); - isDarkMode = themeMode == ThemeMode.dark; - bool isTablet = MediaQuery.of(context).size.width >= 600 ? true : false; - - if (!isTablet) { - return Scaffold( - appBar: _buildAppBar(context), - body: _buildSettingsOverview(), - ); - } else { - return Drawer( - width: MediaQuery.of(context).size.width * 0.45, - child: _buildSettingsOverview(), - ); - } - } - - ListView _buildSettingsOverview() { - return ListView( - children: [ - _buildProfileTile(), - const Divider(), - _buildSectionTitle('Account Settings'), - _buildEditableListTile('Edit profile', () { - // TODO: Navigate to edit profile screen - }), - _buildSwitchListTile( - title: 'Push notifications', - value: isPushNotificationsEnabled, - onChanged: (value) => - setState(() => isPushNotificationsEnabled = value), - ), - _buildSwitchListTile( - title: 'Dark mode', - value: isDarkMode, - onChanged: (value) { - setState(() => isDarkMode = value); - ref.read(themeModeProvider.notifier).state = - value ? ThemeMode.dark : ThemeMode.light; - _saveThemePreference(value ? 'dark' : 'light'); - }, - ), - _buildSwitchListTile( - title: 'Download Over Wi-Fi only', - value: isDownloadOverWifiOnly, - onChanged: (value) => setState(() => isDownloadOverWifiOnly = value), - ), - _buildLogoutTile(context), - const Divider(), - _buildSectionTitle('More'), - _buildNavigableListTile('About us', () { - // TODO: Navigate to about us screen - }), - _buildNavigableListTile('Privacy policy', () { - // TODO: Navigate to privacy policy screen - }), - _buildNavigableListTile('Terms and conditions', () { - // TODO: Navigate to terms and conditions screen - }), - ], + final userState = ref.watch(userViewModelProvider); + + return Scaffold( + key: _scaffoldKey, + appBar: _buildAppBar(context), + body: Consumer( + builder: (context, watch, _) { + return ListView( + children: [ + _buildProfileTile(userState), + const Divider(), + _buildSectionTitle('Account Settings'), + _buildEditableListTile('Edit profile', () async { + bool isAuthenticated = + await showAuthenticationErrorCard(context, ref); + if (isAuthenticated && mounted) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const EditProfileScreen(), + ), + ); + } + }), + const PreferredGreetingView(), + _buildSwitchListTile( + title: 'Push notifications', + value: userState.isPushNotificationsEnabled, + onChanged: (value) { + ref + .read(userViewModelProvider.notifier) + .saveNotificationPreference(value); + }, + ref: ref, + ), + _buildSwitchListTile( + title: 'Dark mode', + value: userState.isDarkMode, + onChanged: (value) { + ref + .read(userViewModelProvider.notifier) + .saveThemePreference(value ? 'dark' : 'light', ref); + }, + ref: ref, + ), + _buildSwitchListTile( + title: 'Download Over Wi-Fi only', + value: userState.isDownloadWithWifiOnly, + onChanged: (value) { + ref + .read(userViewModelProvider.notifier) + .saveDownloadWifiOnlyPreference(value); + }, + ref: ref, + ), + _buildSectionTitle('Video Playback Speed'), + const PlaybackSpeedSettings(), + _buildLogoutTile(context), + const Divider(), + _buildSectionTitle('More'), + _buildNavigableListTile('About us', () { + // TODO: Navigate to about us screen + }), + _buildNavigableListTile('Privacy policy', () { + // TODO: Navigate to privacy policy screen + }), + _buildNavigableListTile('Terms and conditions', () { + // TODO: Navigate to terms and conditions screen + }), + ], + ); + }, + ), ); } @@ -110,30 +111,36 @@ class _SettingsScreenState extends ConsumerState { ); } - ListTile _buildProfileTile() { + ListTile _buildProfileTile(userState) { + final preferredNameSetting = userState.userSettings?.firstWhere( + (setting) => setting.type == UserSettingType.PREFERRED_NAME, + orElse: () => UserSetting(value: ''), + ); + final preferredName = preferredNameSetting?.value ?? ''; + final userName = userState.user?.name ?? 'Guest'; + + final greetingSetting = userState.userSettings?.firstWhere( + (setting) => setting.type == UserSettingType.GREETING, + orElse: () => UserSetting(value: 'Hi'), + ); + + final preferredGreeting = greetingSetting?.value ?? 'Hi'; + String displayName = preferredName.isNotEmpty ? preferredName : userName; + return ListTile( leading: const CircleAvatar( backgroundImage: AssetImage('assets/images/profile_temp.png'), ), - title: Text( - ref.read(userViewModelProvider).user?.name ?? 'Guest', - ), - onTap: () { - // TODO: Navigate to profile edit screen - }, + title: Text('$preferredGreeting, $displayName'), ); } - Padding _buildSectionTitle(String title) { + Widget _buildSectionTitle(String title) { return Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(8.0), child: Text( title, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black, - ), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ); } @@ -150,13 +157,14 @@ class _SettingsScreenState extends ConsumerState { required String title, required bool value, required ValueChanged onChanged, + required WidgetRef ref, }) { return ListTile( title: Text(title), trailing: Switch( value: value, onChanged: onChanged, - inactiveTrackColor: Colors.grey, + inactiveTrackColor: Theme.of(context).colorScheme.background, ), onTap: () => onChanged(!value), ); @@ -164,9 +172,9 @@ class _SettingsScreenState extends ConsumerState { ListTile _buildLogoutTile(BuildContext context) { return ListTile( - title: const Text( + title: Text( 'Log out', - style: TextStyle(color: Colors.red), + style: TextStyle(color: Theme.of(context).colorScheme.error), ), onTap: () { ref.read(userViewModelProvider.notifier).logout();