Skip to content
This repository has been archived by the owner on Jan 10, 2025. It is now read-only.

140-implement-user-settings-view-UI #180

Merged
merged 27 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
58ac4e5
Add Edit Profile View on Settings View
cbetul Jan 5, 2024
3769e47
Add playback speed picker and custom playback input on Settings View
cbetul Jan 5, 2024
e4df05a
Add Playback Speed Picker View on settings
cbetul Jan 5, 2024
ce1da0c
Add Custom Playback Speed View on settings
cbetul Jan 5, 2024
cfbc289
Add login error dialog card view on settings view
cbetul Jan 5, 2024
799c3a7
Add error handler and info text for edit profile screen
cbetul Jan 5, 2024
aa48dba
Fix Linting on Edit Profile View
cbetul Jan 6, 2024
6f20cbc
Fix bug when user selects multiple playback speeds on Settings View
cbetul Jan 6, 2024
09093ff
Display custom playback speeds on playback speed picker list
cbetul Jan 7, 2024
babcd81
Merge branch 'dev' into 140-implement-user-settings-view-ui
cbetul Jan 7, 2024
5a58f96
Add greeting preferences view
cbetul Jan 7, 2024
d947301
Fix update function on Edit Profile View
cbetul Jan 7, 2024
dcb1520
Merge branch 'dev' into 140-implement-user-settings-view-ui
cbetul Jan 7, 2024
ca529d1
Add a playback speeds view for playback speed picker and custom speed…
cbetul Jan 8, 2024
97acb75
Merge branch 'dev' into 140-implement-user-settings-view-ui
cbetul Jan 8, 2024
216ebda
Fix linting error on settings screen view
cbetul Jan 8, 2024
040c11f
Add load settings function on settings screen view
cbetul Jan 8, 2024
4fde827
Edit and modularize the setting views
cbetul Jan 9, 2024
c787080
Fix fetching bug on playback speed view
cbetul Jan 9, 2024
acfa340
Add border radius on Custom Playback Speed input cart
cbetul Jan 9, 2024
93cdfbe
Add theme color on Authentication Error Card View
cbetul Jan 9, 2024
1deb154
Merge branch 'dev' into 140-implement-user-settings-view-ui
cbetul Jan 9, 2024
4d35b20
Add padding on Playback Speed Picker View
cbetul Jan 9, 2024
057a6bc
Add an error text to prevent a negative number from being entered in …
cbetul Jan 10, 2024
072f330
Add a limit to the custom playback speed input view
cbetul Jan 10, 2024
9cc66a2
Add a character limit to the edit profile screen
cbetul Jan 10, 2024
e0ffacb
Add char limitation on Custom Playback Speed View
cbetul Jan 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions lib/base/networking/api/handler/settings_handler.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:convert';

import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pbgrpc.dart';
import 'package:logger/logger.dart';

Expand Down Expand Up @@ -65,4 +67,88 @@ class SettingsHandler {
rethrow;
}
}

/// Parses playback speeds from the user settings.
List<double> parsePlaybackSpeeds(List<UserSetting>? 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<dynamic> 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<bool> updatePreferredGreeting(
String newGreeting, List<UserSetting> 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<bool> updatePreferredName(
String newName, List<UserSetting> 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<void> updateSelectedSpeeds(
double speed, bool isSelected, List<UserSetting> currentSettings) async {
var playbackSpeedSetting = currentSettings.firstWhere(
(setting) => setting.type == UserSettingType.CUSTOM_PLAYBACK_SPEEDS,
orElse: () => UserSetting(
type: UserSettingType.CUSTOM_PLAYBACK_SPEEDS,
value: jsonEncode([]),
),
);

List<double> updatedSpeeds = parsePlaybackSpeeds([playbackSpeedSetting]);
if (isSelected && !updatedSpeeds.contains(speed)) {
updatedSpeeds.add(speed);
} else if (!isSelected) {
updatedSpeeds.remove(speed);
}

List<Map<String, dynamic>> speedsList = updatedSpeeds
.map((speed) => {"speed": speed, "enabled": true})
.toList();
playbackSpeedSetting.value = jsonEncode(speedsList);

await updateUserSettings(currentSettings);
}
}
14 changes: 14 additions & 0 deletions lib/models/user/user_state_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class UserState {
final List<BannerAlert>? bannerAlerts;
final AppError? error;
final List<Course>? downloadedCourses;
final bool isDarkMode;
final bool isPushNotificationsEnabled;
final bool isDownloadWithWifiOnly;

const UserState({
this.isLoading = false,
Expand All @@ -28,6 +31,9 @@ class UserState {
this.bannerAlerts,
this.error,
this.downloadedCourses,
this.isDarkMode = false,
this.isPushNotificationsEnabled = true,
this.isDownloadWithWifiOnly = true,
});

UserState copyWith({
Expand All @@ -42,6 +48,9 @@ class UserState {
List<BannerAlert>? bannerAlerts,
AppError? error,
List<Course>? downloadedCourses,
bool? isDarkMode,
bool? isPushNotificationsEnabled,
bool? isDownloadWithWifiOnly,
}) {
return UserState(
isLoading: isLoading ?? this.isLoading,
Expand All @@ -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,
);
}

Expand Down
110 changes: 92 additions & 18 deletions lib/view_models/user_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ 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';
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<UserState> {
Expand All @@ -34,6 +37,7 @@ class UserViewModel extends StateNotifier<UserState> {
_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) {
Expand Down Expand Up @@ -139,15 +143,10 @@ class UserViewModel extends StateNotifier<UserState> {
Future<void> 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');
}
Expand All @@ -156,16 +155,14 @@ class UserViewModel extends StateNotifier<UserState> {
Future<void> updateUserSettings(List<UserSetting> 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');
}
Expand Down Expand Up @@ -234,4 +231,81 @@ class UserViewModel extends StateNotifier<UserState> {
void setLoading(bool isLoading) {
state = state.copyWith(isLoading: isLoading);
}

Future<void> loadPreferences() async {
final prefs = await SharedPreferences.getInstance();
await loadThemePreference(prefs);
await loadNotificationPreference(prefs);
await fetchUserSettings();
}

Future<void> loadThemePreference(SharedPreferences prefs) async {
final themePreference = prefs.getString('themeMode') ?? 'light';
state = state.copyWith(isDarkMode: themePreference == 'dark');
}

Future<void> 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<void> loadNotificationPreference(SharedPreferences prefs) async {
final notificationPreference = prefs.getBool('notifications') ?? true;
state = state.copyWith(isPushNotificationsEnabled: notificationPreference);
}

Future<void> saveNotificationPreference(bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('notifications', value);
state = state.copyWith(isPushNotificationsEnabled: value);
}

Future<void> 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<double> getDefaultSpeeds() {
return List<double>.generate(14, (i) => (i + 1) * 0.25);
}

Future<void> updatePreferredGreeting(String newGreeting) async {
try {
await SettingsHandler(_grpcHandler)
.updatePreferredGreeting(newGreeting, state.userSettings ?? []);
await fetchUserSettings();
} catch (e) {
_logger.e('Error updating greeting: $e');
}
}

Future<bool> 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<void> updateSelectedSpeeds(double speed, bool isSelected) async {
await SettingsHandler(_grpcHandler)
.updateSelectedSpeeds(speed, isSelected, state.userSettings ?? []);
await fetchUserSettings();
}

List<double> parsePlaybackSpeeds() {
return SettingsHandler(_grpcHandler)
.parsePlaybackSpeeds(state.userSettings);
}
}
34 changes: 34 additions & 0 deletions lib/views/settings_view/authentication_error_card_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:gocast_mobile/providers.dart';

Future<bool> 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: <Widget>[
TextButton(
child: const Text('OK'),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
}
return isAuthenticated;
}
Loading