feat(settings): implement base settings controller and specific controllers for display mode, email, language, notifications, privacy, security, and text size
- Added BaseSettingsController to manage loading states and error handling. - Created DisplayModeController to handle theme settings with persistence. - Implemented EmailController for managing email preferences and verification. - Developed LanguageController for language selection and persistence. - Added NotificationsController to manage notification preferences. - Created PrivacyController for privacy settings management. - Implemented SecurityController for security settings including 2FA and backup codes. - Developed TextSizeController to manage text size settings. - Created SettingsController to aggregate all sub-controllers and manage user settings. - Added UI components for displaying officer and user profile details.
This commit is contained in:
parent
61fb40bc0f
commit
c6931619de
|
@ -1,13 +1,20 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';
|
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';
|
||||||
import 'package:sigap/app.dart';
|
import 'package:sigap/navigation_menu.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/bindings/personalization_bindings.dart';
|
||||||
|
import 'package:sigap/src/utils/theme/theme.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
// Make sure to initialize bindings first
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// Register navigation controller early since it's needed for NavigationMenu
|
||||||
|
Get.put(NavigationController(), permanent: true);
|
||||||
|
|
||||||
// Make sure status bar is properly set
|
// Make sure status bar is properly set
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
@ -41,6 +48,22 @@ Future<void> main() async {
|
||||||
|
|
||||||
MapboxOptions.setAccessToken(mapboxAccesToken);
|
MapboxOptions.setAccessToken(mapboxAccesToken);
|
||||||
|
|
||||||
runApp(const App());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GetMaterialApp(
|
||||||
|
title: 'SIGAP',
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
theme: TAppTheme.lightTheme,
|
||||||
|
darkTheme: TAppTheme.darkTheme,
|
||||||
|
themeMode: ThemeMode.system,
|
||||||
|
initialBinding: PersonalizationBindings(),
|
||||||
|
home: const NavigationMenu(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,8 +9,8 @@ class NavigationMenu extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Using GetX controller to manage navigation state
|
// Ensure NavigationController is registered in a binding first, then use find
|
||||||
final controller = Get.put(NavigationController());
|
final controller = Get.find<NavigationController>();
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
|
@ -14,7 +14,7 @@ class UserRepository extends GetxController {
|
||||||
static UserRepository get instance => Get.find();
|
static UserRepository get instance => Get.find();
|
||||||
|
|
||||||
final _supabase = SupabaseService.instance.client;
|
final _supabase = SupabaseService.instance.client;
|
||||||
final _logger = Get.put(Logger());
|
final _logger = Get.find<Logger>();
|
||||||
|
|
||||||
// Get current user ID
|
// Get current user ID
|
||||||
String? get currentUserId => SupabaseService.instance.currentUserId;
|
String? get currentUserId => SupabaseService.instance.currentUserId;
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/repositories/officers_repository.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/repositories/profile_repository.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/repositories/users_repository.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/profile/officer_profile_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/profile/profile_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/display_mode_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/email_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/language_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/notifications_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/privacy_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/security_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/settings_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/text_size_controller.dart';
|
||||||
|
|
||||||
|
class PersonalizationBindings extends Bindings {
|
||||||
|
@override
|
||||||
|
void dependencies() {
|
||||||
|
// Register Logger if not already registered
|
||||||
|
if (!Get.isRegistered<Logger>()) {
|
||||||
|
Get.lazyPut<Logger>(() => Logger(), fenix: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register repositories
|
||||||
|
_registerRepositories();
|
||||||
|
|
||||||
|
// Register profile controllers
|
||||||
|
_registerProfileControllers();
|
||||||
|
|
||||||
|
// Register settings controllers
|
||||||
|
_registerSettingsControllers();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _registerRepositories() {
|
||||||
|
// Register repositories with fenix: true to keep them alive when not in use
|
||||||
|
// but recreate them if they were destroyed
|
||||||
|
Get.lazyPut<UserRepository>(() => UserRepository(), fenix: true);
|
||||||
|
|
||||||
|
Get.lazyPut<ProfileRepository>(() => ProfileRepository(), fenix: true);
|
||||||
|
|
||||||
|
Get.lazyPut<OfficerRepository>(() => OfficerRepository(), fenix: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _registerProfileControllers() {
|
||||||
|
// Register profile-related controllers
|
||||||
|
Get.lazyPut<ProfileController>(() => ProfileController(), fenix: true);
|
||||||
|
|
||||||
|
Get.lazyPut<OfficerProfileController>(
|
||||||
|
() => OfficerProfileController(),
|
||||||
|
fenix: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _registerSettingsControllers() {
|
||||||
|
// Register main settings controller
|
||||||
|
Get.lazyPut<SettingsController>(() => SettingsController(), fenix: true);
|
||||||
|
|
||||||
|
// Register individual settings feature controllers
|
||||||
|
Get.lazyPut<DisplayModeController>(
|
||||||
|
() => DisplayModeController(),
|
||||||
|
fenix: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
Get.lazyPut<LanguageController>(() => LanguageController(), fenix: true);
|
||||||
|
|
||||||
|
Get.lazyPut<NotificationsController>(
|
||||||
|
() => NotificationsController(),
|
||||||
|
fenix: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
Get.lazyPut<PrivacyController>(() => PrivacyController(), fenix: true);
|
||||||
|
|
||||||
|
Get.lazyPut<SecurityController>(() => SecurityController(), fenix: true);
|
||||||
|
|
||||||
|
Get.lazyPut<EmailController>(() => EmailController(), fenix: true);
|
||||||
|
|
||||||
|
Get.lazyPut<TextSizeController>(() => TextSizeController(), fenix: true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
abstract class BaseProfileController extends GetxController {
|
||||||
|
// Common state variables
|
||||||
|
final RxBool isLoading = false.obs;
|
||||||
|
final RxString errorMessage = ''.obs;
|
||||||
|
final RxBool hasChanges = false.obs;
|
||||||
|
|
||||||
|
// Form key for validation
|
||||||
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
// Loading state management
|
||||||
|
void setLoading(bool loading) {
|
||||||
|
isLoading.value = loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
void setError(String message) {
|
||||||
|
errorMessage.value = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear error
|
||||||
|
void clearError() {
|
||||||
|
errorMessage.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error dialog
|
||||||
|
void showError(String title, String message) {
|
||||||
|
Get.snackbar(
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
backgroundColor: Colors.red.withOpacity(0.8),
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
void showSuccess(String title, String message) {
|
||||||
|
Get.snackbar(
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
backgroundColor: Colors.green.withOpacity(0.8),
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common validation methods
|
||||||
|
String? validateRequiredField(String? value, String fieldName) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '$fieldName is required';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? validateEmail(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Email is required';
|
||||||
|
}
|
||||||
|
if (!GetUtils.isEmail(value)) {
|
||||||
|
return 'Enter a valid email address';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? validatePhone(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Phone number is required';
|
||||||
|
}
|
||||||
|
if (!GetUtils.isPhoneNumber(value)) {
|
||||||
|
return 'Enter a valid phone number';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? validateName(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Name is required';
|
||||||
|
}
|
||||||
|
if (value.length < 2) {
|
||||||
|
return 'Name is too short';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abstract methods to be implemented by subclasses
|
||||||
|
Future<bool> saveChanges();
|
||||||
|
void discardChanges();
|
||||||
|
Future<void> loadData();
|
||||||
|
}
|
|
@ -0,0 +1,240 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/repositories/officers_repository.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/profile/base_profile_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/profile/profile_controller.dart';
|
||||||
|
|
||||||
|
class OfficerProfileController extends BaseProfileController {
|
||||||
|
// Use find to get previously registered repository
|
||||||
|
final _officerRepository = Get.find<OfficerRepository>();
|
||||||
|
|
||||||
|
// Observable state variables
|
||||||
|
final Rx<OfficerModel?> officer = Rx<OfficerModel?>(null);
|
||||||
|
final RxString unitName = ''.obs;
|
||||||
|
final RxString patrolUnitName = ''.obs;
|
||||||
|
final RxBool isValidOfficer = true.obs; // Track if officer profile is valid
|
||||||
|
final RxBool isEditMode = false.obs;
|
||||||
|
|
||||||
|
// Form controllers for edit mode
|
||||||
|
late TextEditingController rankController;
|
||||||
|
late TextEditingController positionController;
|
||||||
|
late TextEditingController nrpController;
|
||||||
|
late TextEditingController phoneController;
|
||||||
|
|
||||||
|
// Original officer data for change tracking
|
||||||
|
OfficerModel? originalOfficer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
|
||||||
|
// Initialize text controllers
|
||||||
|
rankController = TextEditingController();
|
||||||
|
positionController = TextEditingController();
|
||||||
|
nrpController = TextEditingController();
|
||||||
|
phoneController = TextEditingController();
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
rankController.dispose();
|
||||||
|
positionController.dispose();
|
||||||
|
nrpController.dispose();
|
||||||
|
phoneController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load officer data
|
||||||
|
@override
|
||||||
|
Future<void> loadData() async {
|
||||||
|
await loadOfficerData();
|
||||||
|
if (isEditMode.value) {
|
||||||
|
_loadOfficerDataToForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load officer data
|
||||||
|
Future<void> loadOfficerData() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Get officer data from profile controller if available
|
||||||
|
final profileController = Get.find<ProfileController>();
|
||||||
|
if (profileController.officer.value != null) {
|
||||||
|
officer.value = profileController.officer.value;
|
||||||
|
originalOfficer = profileController.officer.value;
|
||||||
|
} else {
|
||||||
|
// Fetch officer data directly
|
||||||
|
final officerData = await _officerRepository.getOfficerData();
|
||||||
|
officer.value = officerData;
|
||||||
|
originalOfficer = officerData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional metadata could be fetched here, like unit name
|
||||||
|
// This would require additional repository methods
|
||||||
|
|
||||||
|
if (officer.value != null) {
|
||||||
|
// Example: You might fetch unit name from another repository
|
||||||
|
// unitName.value = await _unitsRepository.getUnitName(officer.value!.unitId);
|
||||||
|
unitName.value = officer.value?.unitId ?? 'Unknown Unit';
|
||||||
|
patrolUnitName.value = officer.value?.patrolUnitId ?? 'No Patrol Unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
isValidOfficer.value = _validateOfficerProfile();
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load officer data: ${e.toString()}');
|
||||||
|
isValidOfficer.value = false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle edit mode
|
||||||
|
void toggleEditMode() {
|
||||||
|
isEditMode.value = !isEditMode.value;
|
||||||
|
if (isEditMode.value) {
|
||||||
|
_loadOfficerDataToForm();
|
||||||
|
} else {
|
||||||
|
hasChanges.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load officer data to form controllers
|
||||||
|
void _loadOfficerDataToForm() {
|
||||||
|
if (officer.value != null) {
|
||||||
|
rankController.text = officer.value!.rank ?? '';
|
||||||
|
positionController.text = officer.value!.position ?? '';
|
||||||
|
nrpController.text = officer.value!.nrp?.toString() ?? '';
|
||||||
|
phoneController.text = officer.value!.phone ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup listeners
|
||||||
|
_setupTextChangeListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up listeners to track changes
|
||||||
|
void _setupTextChangeListeners() {
|
||||||
|
void onTextChanged() {
|
||||||
|
checkForChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
rankController.addListener(onTextChanged);
|
||||||
|
positionController.addListener(onTextChanged);
|
||||||
|
nrpController.addListener(onTextChanged);
|
||||||
|
phoneController.addListener(onTextChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for changes in form fields
|
||||||
|
void checkForChanges() {
|
||||||
|
if (officer.value == null) {
|
||||||
|
hasChanges.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasChanges.value =
|
||||||
|
rankController.text != (officer.value!.rank ?? '') ||
|
||||||
|
positionController.text != (officer.value!.position ?? '') ||
|
||||||
|
nrpController.text != (officer.value!.nrp?.toString() ?? '') ||
|
||||||
|
phoneController.text != (officer.value!.phone ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate officer profile has required fields
|
||||||
|
bool _validateOfficerProfile() {
|
||||||
|
if (officer.value == null) return false;
|
||||||
|
|
||||||
|
// Check required fields
|
||||||
|
return officer.value!.nrp != null &&
|
||||||
|
officer.value!.name != null &&
|
||||||
|
officer.value!.unitId != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh officer data
|
||||||
|
Future<void> refreshOfficerData() async {
|
||||||
|
await loadOfficerData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get officer by ID
|
||||||
|
Future<OfficerModel?> getOfficerById(String officerId) async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
return await _officerRepository.getOfficerById(officerId);
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to get officer: ${e.toString()}');
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate NRP for officers
|
||||||
|
String? validateNRP(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'NRP is required';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save officer changes
|
||||||
|
@override
|
||||||
|
Future<bool> saveChanges() async {
|
||||||
|
if (!formKey.currentState!.validate()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
if (officer.value != null) {
|
||||||
|
dynamic nrpValue = nrpController.text.trim();
|
||||||
|
// Try to convert to int if it's a number
|
||||||
|
if (int.tryParse(nrpValue) != null) {
|
||||||
|
nrpValue = int.parse(nrpValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create updated officer model
|
||||||
|
final updatedOfficer = officer.value!.copyWith(
|
||||||
|
rank: rankController.text.trim(),
|
||||||
|
position: positionController.text.trim(),
|
||||||
|
nrp: nrpValue,
|
||||||
|
phone: phoneController.text.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save to repository
|
||||||
|
final result = await _officerRepository.updateOfficer(updatedOfficer);
|
||||||
|
if (result != null) {
|
||||||
|
officer.value = result;
|
||||||
|
originalOfficer = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshOfficerData();
|
||||||
|
showSuccess('Success', 'Officer profile updated successfully');
|
||||||
|
hasChanges.value = false;
|
||||||
|
isEditMode.value = false;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
showError('Error', 'No officer profile to update');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to save changes: ${e.toString()}');
|
||||||
|
showError(
|
||||||
|
'Error',
|
||||||
|
'Could not save officer profile changes. ${e.toString()}',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard changes
|
||||||
|
@override
|
||||||
|
void discardChanges() {
|
||||||
|
_loadOfficerDataToForm();
|
||||||
|
hasChanges.value = false;
|
||||||
|
isEditMode.value = false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/repositories/officers_repository.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/repositories/profile_repository.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/repositories/users_repository.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/profile/officer_profile_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/profile/profile_controller.dart';
|
||||||
|
|
||||||
|
class ProfileBinding extends Bindings {
|
||||||
|
@override
|
||||||
|
void dependencies() {
|
||||||
|
// Register repositories if they aren't already
|
||||||
|
if (!Get.isRegistered<UserRepository>()) {
|
||||||
|
Get.lazyPut(() => UserRepository());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Get.isRegistered<ProfileRepository>()) {
|
||||||
|
Get.lazyPut(() => ProfileRepository());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Get.isRegistered<OfficerRepository>()) {
|
||||||
|
Get.lazyPut(() => OfficerRepository());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register controllers
|
||||||
|
Get.lazyPut(() => ProfileController(), fenix: true);
|
||||||
|
Get.lazyPut(() => OfficerProfileController(), fenix: true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,321 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/models/models/profile_model.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/models/models/users_model.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/repositories/officers_repository.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/repositories/profile_repository.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/repositories/users_repository.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/profile/base_profile_controller.dart';
|
||||||
|
|
||||||
|
class ProfileController extends BaseProfileController {
|
||||||
|
// Repositories
|
||||||
|
final _userRepository = Get.find<UserRepository>();
|
||||||
|
final _profileRepository = Get.find<ProfileRepository>();
|
||||||
|
final _officerRepository = Get.find<OfficerRepository>();
|
||||||
|
|
||||||
|
// Observable state variables
|
||||||
|
final Rx<UserModel?> user = Rx<UserModel?>(null);
|
||||||
|
final Rx<ProfileModel?> profile = Rx<ProfileModel?>(null);
|
||||||
|
final Rx<OfficerModel?> officer = Rx<OfficerModel?>(null);
|
||||||
|
final RxBool isOfficer = false.obs;
|
||||||
|
final RxBool isInitialized = false.obs;
|
||||||
|
final RxBool isEditMode = false.obs;
|
||||||
|
|
||||||
|
// Form controllers for edit mode
|
||||||
|
late TextEditingController firstNameController;
|
||||||
|
late TextEditingController lastNameController;
|
||||||
|
late TextEditingController bioController;
|
||||||
|
late TextEditingController phoneController;
|
||||||
|
late TextEditingController birthPlaceController;
|
||||||
|
late Rx<DateTime?> birthDate = Rx<DateTime?>(null);
|
||||||
|
|
||||||
|
// Original data for comparison
|
||||||
|
ProfileModel? originalProfile;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
|
||||||
|
// Initialize text controllers
|
||||||
|
firstNameController = TextEditingController();
|
||||||
|
lastNameController = TextEditingController();
|
||||||
|
bioController = TextEditingController();
|
||||||
|
phoneController = TextEditingController();
|
||||||
|
birthPlaceController = TextEditingController();
|
||||||
|
|
||||||
|
// Load initial data
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
// Dispose controllers
|
||||||
|
firstNameController.dispose();
|
||||||
|
lastNameController.dispose();
|
||||||
|
bioController.dispose();
|
||||||
|
phoneController.dispose();
|
||||||
|
birthPlaceController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load data from repositories
|
||||||
|
@override
|
||||||
|
Future<void> loadData() async {
|
||||||
|
await fetchUserProfile();
|
||||||
|
if (isEditMode.value) {
|
||||||
|
_loadDataToFormFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload profile data
|
||||||
|
Future<void> refreshProfile() async {
|
||||||
|
await fetchUserProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the user profile data
|
||||||
|
Future<void> fetchUserProfile() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
clearError();
|
||||||
|
|
||||||
|
// Check if user is an officer
|
||||||
|
isOfficer.value = await _userRepository.isCurrentUserOfficer();
|
||||||
|
|
||||||
|
// Fetch the appropriate data based on the user type
|
||||||
|
if (isOfficer.value) {
|
||||||
|
await fetchOfficerData();
|
||||||
|
} else {
|
||||||
|
await fetchRegularUserData();
|
||||||
|
}
|
||||||
|
|
||||||
|
isInitialized.value = true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load profile: ${e.toString()}');
|
||||||
|
showError(
|
||||||
|
'Profile Error',
|
||||||
|
'Could not load profile data. ${e.toString()}',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch data for regular users
|
||||||
|
Future<void> fetchRegularUserData() async {
|
||||||
|
try {
|
||||||
|
// Get user and profile data
|
||||||
|
final userData = await _userRepository.getCurrentUserData();
|
||||||
|
user.value = userData;
|
||||||
|
|
||||||
|
// If profile exists in user data, use it
|
||||||
|
if (userData.profile != null) {
|
||||||
|
profile.value = userData.profile;
|
||||||
|
originalProfile = userData.profile;
|
||||||
|
} else {
|
||||||
|
// Otherwise, fetch profile separately
|
||||||
|
final profileData = await _profileRepository.getProfileData();
|
||||||
|
profile.value = profileData;
|
||||||
|
originalProfile = profileData;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load user data: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch data for officers
|
||||||
|
Future<void> fetchOfficerData() async {
|
||||||
|
try {
|
||||||
|
// Get officer data
|
||||||
|
final officerData = await _officerRepository.getOfficerData();
|
||||||
|
officer.value = officerData;
|
||||||
|
|
||||||
|
// Get additional user data
|
||||||
|
final userData = await _userRepository.getCurrentUserData();
|
||||||
|
user.value = userData;
|
||||||
|
profile.value = userData.profile;
|
||||||
|
originalProfile = userData.profile;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load officer data: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle edit mode
|
||||||
|
void toggleEditMode() {
|
||||||
|
isEditMode.value = !isEditMode.value;
|
||||||
|
if (isEditMode.value) {
|
||||||
|
_loadDataToFormFields();
|
||||||
|
} else {
|
||||||
|
hasChanges.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load current data into form fields
|
||||||
|
void _loadDataToFormFields() {
|
||||||
|
if (profile.value != null) {
|
||||||
|
firstNameController.text = profile.value!.firstName ?? '';
|
||||||
|
lastNameController.text = profile.value!.lastName ?? '';
|
||||||
|
bioController.text = profile.value!.bio ?? '';
|
||||||
|
birthPlaceController.text = profile.value!.placeOfBirth ?? '';
|
||||||
|
birthDate.value = profile.value!.birthDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.value != null) {
|
||||||
|
phoneController.text = user.value!.phone ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup listeners to track changes
|
||||||
|
_setupTextChangeListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup listeners for text controllers to track changes
|
||||||
|
void _setupTextChangeListeners() {
|
||||||
|
void onTextChanged() {
|
||||||
|
_checkForChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
firstNameController.addListener(onTextChanged);
|
||||||
|
lastNameController.addListener(onTextChanged);
|
||||||
|
bioController.addListener(onTextChanged);
|
||||||
|
phoneController.addListener(onTextChanged);
|
||||||
|
birthPlaceController.addListener(onTextChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for changes in form data
|
||||||
|
void _checkForChanges() {
|
||||||
|
if (profile.value == null) {
|
||||||
|
hasChanges.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check profile fields
|
||||||
|
bool profileChanged =
|
||||||
|
firstNameController.text != (profile.value!.firstName ?? '') ||
|
||||||
|
lastNameController.text != (profile.value!.lastName ?? '') ||
|
||||||
|
bioController.text != (profile.value!.bio ?? '') ||
|
||||||
|
birthPlaceController.text != (profile.value!.placeOfBirth ?? '') ||
|
||||||
|
birthDate.value != profile.value!.birthDate;
|
||||||
|
|
||||||
|
// Check phone
|
||||||
|
bool phoneChanged = false;
|
||||||
|
if (user.value != null) {
|
||||||
|
phoneChanged = phoneController.text != (user.value!.phone ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
hasChanges.value = profileChanged || phoneChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set birth date
|
||||||
|
void setBirthDate(DateTime? date) {
|
||||||
|
birthDate.value = date;
|
||||||
|
_checkForChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch profile by user ID
|
||||||
|
Future<void> fetchProfileByUserId(String userId) async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
clearError();
|
||||||
|
|
||||||
|
// Get user data
|
||||||
|
final userData = await _userRepository.getUserById(userId);
|
||||||
|
user.value = userData;
|
||||||
|
profile.value = userData.profile;
|
||||||
|
originalProfile = userData.profile;
|
||||||
|
|
||||||
|
// Check if user is an officer
|
||||||
|
final isUserOfficer = userData.isOfficer;
|
||||||
|
isOfficer.value = isUserOfficer;
|
||||||
|
|
||||||
|
// If user is an officer, fetch officer data
|
||||||
|
if (isUserOfficer) {
|
||||||
|
try {
|
||||||
|
final officerData = await _officerRepository.getOfficerById(userId);
|
||||||
|
officer.value = officerData;
|
||||||
|
} catch (e) {
|
||||||
|
// Handle case when officer data doesn't exist yet
|
||||||
|
officer.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load profile: ${e.toString()}');
|
||||||
|
showError(
|
||||||
|
'Profile Error',
|
||||||
|
'Could not load user profile. ${e.toString()}',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload avatar
|
||||||
|
Future<String?> uploadAvatar(String imagePath) async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
final avatarUrl = await _profileRepository.uploadAvatar(imagePath);
|
||||||
|
await refreshProfile(); // Reload profile to get the new avatar
|
||||||
|
return avatarUrl;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to upload avatar: ${e.toString()}');
|
||||||
|
showError(
|
||||||
|
'Upload Failed',
|
||||||
|
'Could not upload profile picture. ${e.toString()}',
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save changes
|
||||||
|
@override
|
||||||
|
Future<bool> saveChanges() async {
|
||||||
|
if (!formKey.currentState!.validate()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Update profile data
|
||||||
|
if (profile.value != null) {
|
||||||
|
final updatedProfile = profile.value!.copyWith(
|
||||||
|
firstName: firstNameController.text.trim(),
|
||||||
|
lastName: lastNameController.text.trim(),
|
||||||
|
bio: bioController.text.trim(),
|
||||||
|
placeOfBirth: birthPlaceController.text.trim(),
|
||||||
|
birthDate: birthDate.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _profileRepository.updateProfile(updatedProfile);
|
||||||
|
profile.value = updatedProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update phone if changed
|
||||||
|
final phone = phoneController.text.trim();
|
||||||
|
if (user.value != null && phone != user.value!.phone) {
|
||||||
|
await _userRepository.updateUserPhone(phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshProfile();
|
||||||
|
hasChanges.value = false;
|
||||||
|
isEditMode.value = false;
|
||||||
|
showSuccess('Success', 'Profile updated successfully');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to save changes: ${e.toString()}');
|
||||||
|
showError('Error', 'Could not save profile changes. ${e.toString()}');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard changes
|
||||||
|
@override
|
||||||
|
void discardChanges() {
|
||||||
|
_loadDataToFormFields();
|
||||||
|
hasChanges.value = false;
|
||||||
|
isEditMode.value = false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
abstract class BaseSettingsController extends GetxController {
|
||||||
|
final RxBool isLoading = false.obs;
|
||||||
|
final RxString errorMessage = ''.obs;
|
||||||
|
|
||||||
|
// Method to handle loading state
|
||||||
|
void setLoading(bool loading) {
|
||||||
|
isLoading.value = loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to handle errors
|
||||||
|
void setError(String message) {
|
||||||
|
errorMessage.value = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to clear errors
|
||||||
|
void clearError() {
|
||||||
|
errorMessage.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings to storage (to be implemented by subclasses)
|
||||||
|
Future<bool> saveSettings();
|
||||||
|
|
||||||
|
// Load settings from storage (to be implemented by subclasses)
|
||||||
|
Future<void> loadSettings();
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart';
|
||||||
|
|
||||||
|
class DisplayModeController extends BaseSettingsController {
|
||||||
|
// Available theme modes
|
||||||
|
final List<String> themeOptions = [
|
||||||
|
'System default',
|
||||||
|
'Light mode',
|
||||||
|
'Dark mode',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Current selected theme mode
|
||||||
|
final RxString selectedThemeMode = 'System default'.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change theme mode
|
||||||
|
void changeThemeMode(String mode) {
|
||||||
|
if (!themeOptions.contains(mode)) return;
|
||||||
|
|
||||||
|
selectedThemeMode.value = mode;
|
||||||
|
_applyTheme(mode);
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the theme based on selected mode
|
||||||
|
void _applyTheme(String mode) {
|
||||||
|
ThemeMode themeMode;
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case 'Light mode':
|
||||||
|
themeMode = ThemeMode.light;
|
||||||
|
break;
|
||||||
|
case 'Dark mode':
|
||||||
|
themeMode = ThemeMode.dark;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
themeMode = ThemeMode.system;
|
||||||
|
}
|
||||||
|
|
||||||
|
Get.changeThemeMode(themeMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> saveSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Save the theme setting to persistent storage
|
||||||
|
final prefs = Get.find<dynamic>(); // Replace with your storage solution
|
||||||
|
// await prefs.setString('theme_mode', selectedThemeMode.value);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to save theme settings: ${e.toString()}');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> loadSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Load the theme setting from persistent storage
|
||||||
|
final prefs = Get.find<dynamic>(); // Replace with your storage solution
|
||||||
|
// final savedTheme = await prefs.getString('theme_mode') ?? 'System default';
|
||||||
|
// selectedThemeMode.value = savedTheme;
|
||||||
|
|
||||||
|
// Apply the loaded theme
|
||||||
|
_applyTheme(selectedThemeMode.value);
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load theme settings: ${e.toString()}');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart';
|
||||||
|
|
||||||
|
class EmailController extends BaseSettingsController {
|
||||||
|
// Email settings
|
||||||
|
final RxString currentEmail = "anitarose@example.com".obs;
|
||||||
|
final RxBool receiveNewsletter = true.obs;
|
||||||
|
final RxBool receivePromotions = false.obs;
|
||||||
|
|
||||||
|
// Email verification status
|
||||||
|
final RxBool isEmailVerified = true.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle email preference
|
||||||
|
void toggleEmailPreference(String preference, bool value) {
|
||||||
|
switch (preference) {
|
||||||
|
case 'receive_newsletter':
|
||||||
|
receiveNewsletter.value = value;
|
||||||
|
break;
|
||||||
|
case 'receive_promotions':
|
||||||
|
receivePromotions.value = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change primary email
|
||||||
|
Future<bool> changeEmail(String newEmail, String password) async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Implement email change logic with API
|
||||||
|
// Normally would validate password and update email
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
if (newEmail.isEmpty || !GetUtils.isEmail(newEmail)) {
|
||||||
|
setError('Please enter a valid email address.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEmail.value = newEmail;
|
||||||
|
isEmailVerified.value = false; // New email needs verification
|
||||||
|
await saveSettings();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to change email: ${e.toString()}');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send verification email
|
||||||
|
Future<bool> sendVerificationEmail() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Implement send verification email logic with API
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to send verification email: ${e.toString()}');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> saveSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Save email settings to persistent storage
|
||||||
|
final prefs = Get.find<dynamic>(); // Replace with your storage solution
|
||||||
|
// await prefs.setString('current_email', currentEmail.value);
|
||||||
|
// await prefs.setBool('receive_newsletter', receiveNewsletter.value);
|
||||||
|
// await prefs.setBool('receive_promotions', receivePromotions.value);
|
||||||
|
// await prefs.setBool('is_email_verified', isEmailVerified.value);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to save email settings: ${e.toString()}');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> loadSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Load email settings from persistent storage
|
||||||
|
final prefs = Get.find<dynamic>(); // Replace with your storage solution
|
||||||
|
// currentEmail.value = prefs.getString('current_email') ?? "anitarose@example.com";
|
||||||
|
// receiveNewsletter.value = prefs.getBool('receive_newsletter') ?? true;
|
||||||
|
// receivePromotions.value = prefs.getBool('receive_promotions') ?? false;
|
||||||
|
// isEmailVerified.value = prefs.getBool('is_email_verified') ?? true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load email settings: ${e.toString()}');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart';
|
||||||
|
|
||||||
|
class LanguageController extends BaseSettingsController {
|
||||||
|
// Available languages with their locale codes
|
||||||
|
final List<Map<String, String>> availableLanguages = [
|
||||||
|
{'name': 'English (US)', 'code': 'en_US'},
|
||||||
|
{'name': 'Bahasa Indonesia', 'code': 'id_ID'},
|
||||||
|
{'name': 'Español', 'code': 'es_ES'},
|
||||||
|
{'name': 'Français', 'code': 'fr_FR'},
|
||||||
|
{'name': '日本語', 'code': 'ja_JP'},
|
||||||
|
{'name': '한국어', 'code': 'ko_KR'},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Current selected language
|
||||||
|
final RxString selectedLanguage = 'English (US)'.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change language
|
||||||
|
void changeLanguage(String language) {
|
||||||
|
final languageItem = availableLanguages.firstWhere(
|
||||||
|
(lang) => lang['name'] == language,
|
||||||
|
orElse: () => {'name': 'English (US)', 'code': 'en_US'},
|
||||||
|
);
|
||||||
|
|
||||||
|
selectedLanguage.value = language;
|
||||||
|
final String localeCode = languageItem['code'] ?? 'en_US';
|
||||||
|
final List<String> localeParts = localeCode.split('_');
|
||||||
|
|
||||||
|
// Apply the language change
|
||||||
|
Get.updateLocale(
|
||||||
|
Locale(localeParts[0], localeParts.length > 1 ? localeParts[1] : ''),
|
||||||
|
);
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> saveSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Save the language setting to persistent storage
|
||||||
|
final prefs = Get.find<dynamic>(); // Replace with your storage solution
|
||||||
|
// await prefs.setString('selected_language', selectedLanguage.value);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to save language settings: ${e.toString()}');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> loadSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Load the language setting from persistent storage
|
||||||
|
final prefs = Get.find<dynamic>(); // Replace with your storage solution
|
||||||
|
// final savedLanguage = await prefs.getString('selected_language') ?? 'English (US)';
|
||||||
|
// selectedLanguage.value = savedLanguage;
|
||||||
|
|
||||||
|
// Apply the loaded language
|
||||||
|
if (selectedLanguage.value != 'English (US)') {
|
||||||
|
changeLanguage(selectedLanguage.value);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load language settings: ${e.toString()}');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart';
|
||||||
|
|
||||||
|
class NotificationsController extends BaseSettingsController {
|
||||||
|
// Notification settings
|
||||||
|
final RxBool messagesNotify = true.obs;
|
||||||
|
final RxBool commentsNotify = true.obs;
|
||||||
|
final RxBool followersNotify = true.obs;
|
||||||
|
final RxBool mentionsNotify = false.obs;
|
||||||
|
final RxBool systemNotify = true.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle a notification setting
|
||||||
|
void toggleNotification(String type, bool value) {
|
||||||
|
switch (type) {
|
||||||
|
case 'messages':
|
||||||
|
messagesNotify.value = value;
|
||||||
|
break;
|
||||||
|
case 'comments':
|
||||||
|
commentsNotify.value = value;
|
||||||
|
break;
|
||||||
|
case 'followers':
|
||||||
|
followersNotify.value = value;
|
||||||
|
break;
|
||||||
|
case 'mentions':
|
||||||
|
mentionsNotify.value = value;
|
||||||
|
break;
|
||||||
|
case 'system':
|
||||||
|
systemNotify.value = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> saveSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Save notification settings to persistent storage
|
||||||
|
final prefs = Get.find<dynamic>(); // Replace with your storage solution
|
||||||
|
// await prefs.setBool('notify_messages', messagesNotify.value);
|
||||||
|
// await prefs.setBool('notify_comments', commentsNotify.value);
|
||||||
|
// await prefs.setBool('notify_followers', followersNotify.value);
|
||||||
|
// await prefs.setBool('notify_mentions', mentionsNotify.value);
|
||||||
|
// await prefs.setBool('notify_system', systemNotify.value);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to save notification settings: ${e.toString()}');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> loadSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Load notification settings from persistent storage
|
||||||
|
final prefs = Get.find<dynamic>(); // Replace with your storage solution
|
||||||
|
// messagesNotify.value = prefs.getBool('notify_messages') ?? true;
|
||||||
|
// commentsNotify.value = prefs.getBool('notify_comments') ?? true;
|
||||||
|
// followersNotify.value = prefs.getBool('notify_followers') ?? true;
|
||||||
|
// mentionsNotify.value = prefs.getBool('notify_mentions') ?? false;
|
||||||
|
// systemNotify.value = prefs.getBool('notify_system') ?? true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load notification settings: ${e.toString()}');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart';
|
||||||
|
|
||||||
|
class PrivacyController extends BaseSettingsController {
|
||||||
|
// Privacy settings
|
||||||
|
final RxBool showActivityStatus = true.obs;
|
||||||
|
final RxBool allowComments = false.obs;
|
||||||
|
final RxBool shareUsageData = true.obs;
|
||||||
|
final RxBool allowPersonalizedAds = false.obs;
|
||||||
|
|
||||||
|
// Profile visibility options
|
||||||
|
final RxString profileVisibility = 'Public'.obs;
|
||||||
|
final List<String> visibilityOptions = ['Public', 'Friends', 'Private'];
|
||||||
|
|
||||||
|
// Messaging permissions
|
||||||
|
final RxString messagingPermission = 'Everyone'.obs;
|
||||||
|
final List<String> messagingOptions = ['Everyone', 'Friends', 'Nobody'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle privacy setting
|
||||||
|
void togglePrivacySetting(String setting, bool value) {
|
||||||
|
switch (setting) {
|
||||||
|
case 'activity_status':
|
||||||
|
showActivityStatus.value = value;
|
||||||
|
break;
|
||||||
|
case 'allow_comments':
|
||||||
|
allowComments.value = value;
|
||||||
|
break;
|
||||||
|
case 'share_usage_data':
|
||||||
|
shareUsageData.value = value;
|
||||||
|
break;
|
||||||
|
case 'personalized_ads':
|
||||||
|
allowPersonalizedAds.value = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change profile visibility
|
||||||
|
void changeProfileVisibility(String visibility) {
|
||||||
|
if (visibilityOptions.contains(visibility)) {
|
||||||
|
profileVisibility.value = visibility;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change messaging permissions
|
||||||
|
void changeMessagingPermission(String permission) {
|
||||||
|
if (messagingOptions.contains(permission)) {
|
||||||
|
messagingPermission.value = permission;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> saveSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Save privacy settings to persistent storage
|
||||||
|
final prefs = Get.find<dynamic>(); // Replace with your storage solution
|
||||||
|
// await prefs.setBool('show_activity_status', showActivityStatus.value);
|
||||||
|
// await prefs.setBool('allow_comments', allowComments.value);
|
||||||
|
// await prefs.setBool('share_usage_data', shareUsageData.value);
|
||||||
|
// await prefs.setBool('allow_personalized_ads', allowPersonalizedAds.value);
|
||||||
|
// await prefs.setString('profile_visibility', profileVisibility.value);
|
||||||
|
// await prefs.setString('messaging_permission', messagingPermission.value);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to save privacy settings: ${e.toString()}');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> loadSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Load privacy settings from persistent storage
|
||||||
|
final prefs = Get.find<dynamic>(); // Replace with your storage solution
|
||||||
|
// showActivityStatus.value = prefs.getBool('show_activity_status') ?? true;
|
||||||
|
// allowComments.value = prefs.getBool('allow_comments') ?? false;
|
||||||
|
// shareUsageData.value = prefs.getBool('share_usage_data') ?? true;
|
||||||
|
// allowPersonalizedAds.value = prefs.getBool('allow_personalized_ads') ?? false;
|
||||||
|
// profileVisibility.value = prefs.getString('profile_visibility') ?? 'Public';
|
||||||
|
// messagingPermission.value = prefs.getString('messaging_permission') ?? 'Everyone';
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load privacy settings: ${e.toString()}');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart';
|
||||||
|
|
||||||
|
class SecurityController extends BaseSettingsController {
|
||||||
|
// Security settings
|
||||||
|
final RxBool requireBiometric = true.obs;
|
||||||
|
final RxBool enable2FA = false.obs;
|
||||||
|
final RxBool sendEmailAlerts = true.obs;
|
||||||
|
final RxBool sendPushNotification = false.obs;
|
||||||
|
|
||||||
|
// Passcode information
|
||||||
|
final RxBool hasPasscode = false.obs;
|
||||||
|
final RxString passcodeLastChanged = ''.obs;
|
||||||
|
|
||||||
|
// 2FA backup codes
|
||||||
|
final RxList<String> backupCodes = <String>[].obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle security setting
|
||||||
|
void toggleSecuritySetting(String setting, bool value) {
|
||||||
|
switch (setting) {
|
||||||
|
case 'require_biometric':
|
||||||
|
requireBiometric.value = value;
|
||||||
|
break;
|
||||||
|
case 'enable_2fa':
|
||||||
|
enable2FA.value = value;
|
||||||
|
break;
|
||||||
|
case 'send_email_alerts':
|
||||||
|
sendEmailAlerts.value = value;
|
||||||
|
break;
|
||||||
|
case 'send_push_notification':
|
||||||
|
sendPushNotification.value = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new backup codes (normally would call an API)
|
||||||
|
Future<bool> generateBackupCodes() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Simulate API call to generate backup codes
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
// Generate random codes for demo purposes
|
||||||
|
final List<String> codes = List.generate(
|
||||||
|
8,
|
||||||
|
(_) => List.generate(
|
||||||
|
6,
|
||||||
|
(_) =>
|
||||||
|
(0 + (9 - 0) * (DateTime.now().microsecondsSinceEpoch % 10))
|
||||||
|
.toString(),
|
||||||
|
).join(''),
|
||||||
|
);
|
||||||
|
|
||||||
|
backupCodes.value = codes;
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to generate backup codes: ${e.toString()}');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change passcode
|
||||||
|
Future<bool> changePasscode(
|
||||||
|
String currentPasscode,
|
||||||
|
String newPasscode,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Implement passcode change logic
|
||||||
|
// Normally would validate the current passcode and save the new one
|
||||||
|
|
||||||
|
hasPasscode.value = true;
|
||||||
|
passcodeLastChanged.value = DateTime.now().toIso8601String();
|
||||||
|
await saveSettings();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to change passcode: ${e.toString()}');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> saveSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Save security settings to persistent storage
|
||||||
|
final prefs = Get.find<dynamic>(); // Replace with your storage solution
|
||||||
|
// await prefs.setBool('require_biometric', requireBiometric.value);
|
||||||
|
// await prefs.setBool('enable_2fa', enable2FA.value);
|
||||||
|
// await prefs.setBool('send_email_alerts', sendEmailAlerts.value);
|
||||||
|
// await prefs.setBool('send_push_notification', sendPushNotification.value);
|
||||||
|
// await prefs.setBool('has_passcode', hasPasscode.value);
|
||||||
|
// await prefs.setString('passcode_last_changed', passcodeLastChanged.value);
|
||||||
|
// await prefs.setStringList('backup_codes', backupCodes);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to save security settings: ${e.toString()}');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> loadSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Load security settings from persistent storage
|
||||||
|
final prefs = Get.find<dynamic>(); // Replace with your storage solution
|
||||||
|
// requireBiometric.value = prefs.getBool('require_biometric') ?? true;
|
||||||
|
// enable2FA.value = prefs.getBool('enable_2fa') ?? false;
|
||||||
|
// sendEmailAlerts.value = prefs.getBool('send_email_alerts') ?? true;
|
||||||
|
// sendPushNotification.value = prefs.getBool('send_push_notification') ?? false;
|
||||||
|
// hasPasscode.value = prefs.getBool('has_passcode') ?? false;
|
||||||
|
// passcodeLastChanged.value = prefs.getString('passcode_last_changed') ?? '';
|
||||||
|
// backupCodes.value = prefs.getStringList('backup_codes') ?? [];
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load security settings: ${e.toString()}');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/models/models/profile_model.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/models/models/users_model.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/display_mode_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/email_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/language_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/notifications_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/privacy_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/security_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/text_size_controller.dart';
|
||||||
|
|
||||||
|
class SettingsController extends BaseSettingsController {
|
||||||
|
// User info
|
||||||
|
final Rx<UserModel?> currentUser = Rx<UserModel?>(null);
|
||||||
|
final Rx<ProfileModel?> userProfile = Rx<ProfileModel?>(null);
|
||||||
|
|
||||||
|
// Current tab index
|
||||||
|
final RxInt selectedTabIndex = 0.obs;
|
||||||
|
|
||||||
|
// References to sub-controllers
|
||||||
|
late final DisplayModeController displayModeController;
|
||||||
|
late final LanguageController languageController;
|
||||||
|
late final NotificationsController notificationsController;
|
||||||
|
late final PrivacyController privacyController;
|
||||||
|
late final SecurityController securityController;
|
||||||
|
late final EmailController emailController;
|
||||||
|
late final TextSizeController textSizeController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
initializeControllers();
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void initializeControllers() {
|
||||||
|
// Initialize all sub-controllers
|
||||||
|
displayModeController = Get.find<DisplayModeController>();
|
||||||
|
languageController = Get.find<LanguageController>();
|
||||||
|
notificationsController = Get.find<NotificationsController>();
|
||||||
|
privacyController = Get.find<PrivacyController>();
|
||||||
|
securityController = Get.find<SecurityController>();
|
||||||
|
emailController = Get.find<EmailController>();
|
||||||
|
textSizeController = Get.find<TextSizeController>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void changeTab(int index) {
|
||||||
|
selectedTabIndex.value = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log out user
|
||||||
|
Future<bool> logout() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Implement logout logic
|
||||||
|
// Clear user session, tokens, etc.
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
// Clear user data
|
||||||
|
currentUser.value = null;
|
||||||
|
userProfile.value = null;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to log out: ${e.toString()}');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> saveSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Save all sub-controller settings
|
||||||
|
await displayModeController.saveSettings();
|
||||||
|
await languageController.saveSettings();
|
||||||
|
await notificationsController.saveSettings();
|
||||||
|
await privacyController.saveSettings();
|
||||||
|
await securityController.saveSettings();
|
||||||
|
await emailController.saveSettings();
|
||||||
|
await textSizeController.saveSettings();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to save settings: ${e.toString()}');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> loadSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Load user data
|
||||||
|
// Normally would fetch from an API or local storage
|
||||||
|
|
||||||
|
// For demo purposes, create a dummy user
|
||||||
|
// currentUser.value = UserModel(/* ... */);
|
||||||
|
// userProfile.value = ProfileModel(/* ... */);
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load user settings: ${e.toString()}');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart';
|
||||||
|
|
||||||
|
class TextSizeController extends BaseSettingsController {
|
||||||
|
// Text size settings
|
||||||
|
final RxDouble textScaleFactor = 1.0.obs;
|
||||||
|
|
||||||
|
// Min and max scale values
|
||||||
|
final double minTextScale = 0.8;
|
||||||
|
final double maxTextScale = 1.4;
|
||||||
|
|
||||||
|
// Preset text sizes
|
||||||
|
final List<Map<String, dynamic>> textSizePresets = [
|
||||||
|
{'label': 'Small', 'scale': 0.8},
|
||||||
|
{'label': 'Normal', 'scale': 1.0},
|
||||||
|
{'label': 'Large', 'scale': 1.2},
|
||||||
|
{'label': 'Extra Large', 'scale': 1.4},
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change text size
|
||||||
|
void changeTextSize(double scale) {
|
||||||
|
if (scale < minTextScale) scale = minTextScale;
|
||||||
|
if (scale > maxTextScale) scale = maxTextScale;
|
||||||
|
|
||||||
|
textScaleFactor.value = scale;
|
||||||
|
_applyTextScale();
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply text scale to app
|
||||||
|
void _applyTextScale() {
|
||||||
|
// In a real app, you would need to update the MediaQuery data to affect the entire app
|
||||||
|
// This is a simplified version for demonstration
|
||||||
|
|
||||||
|
// MediaQuery approach (would require wrapping MaterialApp with a builder)
|
||||||
|
// final MediaQueryData mediaQuery = MediaQuery.of(Get.context!);
|
||||||
|
// final MediaQueryData updatedMediaQuery = mediaQuery.copyWith(textScaleFactor: textScaleFactor.value);
|
||||||
|
|
||||||
|
// Since we can't directly modify MediaQuery here, we'll use this as a placeholder
|
||||||
|
// The actual implementation would depend on your app's architecture
|
||||||
|
debugPrint('Applied text scale factor: ${textScaleFactor.value}');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> saveSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Save text size setting to persistent storage
|
||||||
|
final prefs = Get.find<dynamic>(); // Replace with your storage solution
|
||||||
|
// await prefs.setDouble('text_scale_factor', textScaleFactor.value);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to save text size settings: ${e.toString()}');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> loadSettings() async {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// TODO: Load text size setting from persistent storage
|
||||||
|
final prefs = Get.find<dynamic>(); // Replace with your storage solution
|
||||||
|
// final savedScale = prefs.getDouble('text_scale_factor') ?? 1.0;
|
||||||
|
// textScaleFactor.value = savedScale;
|
||||||
|
|
||||||
|
// Apply the loaded text scale
|
||||||
|
_applyTextScale();
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load text size settings: ${e.toString()}');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,221 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/models/models/profile_model.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/models/models/users_model.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/pages/profile/widgets/officer_profile_detail.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/pages/profile/widgets/user_profile_detail.dart';
|
||||||
|
|
||||||
|
class ProfileScreen extends StatelessWidget {
|
||||||
|
final UserModel? user;
|
||||||
|
final ProfileModel? profile;
|
||||||
|
final OfficerModel? officer;
|
||||||
|
final bool isCurrentUser;
|
||||||
|
|
||||||
|
const ProfileScreen({
|
||||||
|
super.key,
|
||||||
|
this.user,
|
||||||
|
this.profile,
|
||||||
|
this.officer,
|
||||||
|
this.isCurrentUser = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final screenHeight = MediaQuery.of(context).size.height;
|
||||||
|
|
||||||
|
// For demo purposes, using dummy data if no data is provided
|
||||||
|
final displayName =
|
||||||
|
profile?.fullName ?? user?.profile?.fullName ?? 'Anita Rose';
|
||||||
|
final location = _getLocationString();
|
||||||
|
final avatarUrl = profile?.avatar ?? user?.profile?.avatar;
|
||||||
|
final bio =
|
||||||
|
profile?.bio ??
|
||||||
|
'I am currently pursuing a major in Management Economics and Finance at the University of Guelph, Ontario, Canada. Please let me know if I can help you in any way';
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(
|
||||||
|
0xFFF2F2F7,
|
||||||
|
), // Light gray iOS-like background
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Icons.arrow_back, color: theme.iconTheme.color),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
CircleAvatar(
|
||||||
|
backgroundColor: theme.primaryColor.withOpacity(0.1),
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(Icons.person_outline, color: theme.primaryColor),
|
||||||
|
onPressed: () {
|
||||||
|
// Show profile options
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
// Top section with avatar, name, and location
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(height: screenHeight * 0.05),
|
||||||
|
// Profile Avatar
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
spreadRadius: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 50,
|
||||||
|
backgroundColor: Colors.grey.shade300,
|
||||||
|
backgroundImage:
|
||||||
|
avatarUrl != null ? NetworkImage(avatarUrl) : null,
|
||||||
|
child:
|
||||||
|
avatarUrl == null
|
||||||
|
? const Icon(
|
||||||
|
Icons.person,
|
||||||
|
size: 50,
|
||||||
|
color: Colors.white,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Name
|
||||||
|
Text(
|
||||||
|
displayName,
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
// Location with icon
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.location_on_outlined,
|
||||||
|
size: 16,
|
||||||
|
color: theme.hintColor,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
location,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Bottom sheet with profile details
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
top: screenHeight * 0.32, // Adjust position to show top content
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.cardColor,
|
||||||
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
top: Radius.circular(25),
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, -2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
// Center gray line indicator for draggable bottom sheet look
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
height: 4,
|
||||||
|
width: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.withOpacity(0.3),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// About Section
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('About', style: theme.textTheme.titleLarge),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
bio,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
height: 1.5,
|
||||||
|
color: theme.textTheme.bodyMedium?.color
|
||||||
|
?.withOpacity(0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// User-specific information
|
||||||
|
officer != null
|
||||||
|
? OfficerProfileDetail(officer: officer!)
|
||||||
|
: UserProfileDetail(user: user, profile: profile),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getLocationString() {
|
||||||
|
if (officer?.placeOfBirth != null) {
|
||||||
|
return officer!.placeOfBirth!;
|
||||||
|
} else if (profile?.placeOfBirth != null) {
|
||||||
|
return profile!.placeOfBirth!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to address if available
|
||||||
|
if (profile?.address != null) {
|
||||||
|
final address = profile!.address!;
|
||||||
|
final city = address['city'];
|
||||||
|
final country = address['country'];
|
||||||
|
|
||||||
|
if (city != null && country != null) {
|
||||||
|
return '$city, $country';
|
||||||
|
} else if (city != null) {
|
||||||
|
return city;
|
||||||
|
} else if (country != null) {
|
||||||
|
return country;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Ontario, Canada'; // Default fallback
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.dart';
|
||||||
|
|
||||||
|
class OfficerProfileDetail extends StatelessWidget {
|
||||||
|
final OfficerModel officer;
|
||||||
|
|
||||||
|
const OfficerProfileDetail({super.key, required this.officer});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 8, 20, 30),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
Text('Official Information', style: theme.textTheme.titleLarge),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Officer information
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
'NRP',
|
||||||
|
officer.nrp?.toString() ?? 'Not Available',
|
||||||
|
),
|
||||||
|
|
||||||
|
if (officer.rank != null)
|
||||||
|
_buildInfoRow(context, 'Rank', officer.rank!),
|
||||||
|
|
||||||
|
if (officer.position != null)
|
||||||
|
_buildInfoRow(context, 'Position', officer.position!),
|
||||||
|
|
||||||
|
_buildInfoRow(context, 'Email', officer.email ?? 'Not Available'),
|
||||||
|
|
||||||
|
if (officer.phone != null)
|
||||||
|
_buildInfoRow(context, 'Phone', officer.phone!),
|
||||||
|
|
||||||
|
if (officer.dateOfBirth != null)
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
'Date of Birth',
|
||||||
|
DateFormat('dd MMMM yyyy').format(officer.dateOfBirth!),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (officer.placeOfBirth != null)
|
||||||
|
_buildInfoRow(context, 'Place of Birth', officer.placeOfBirth!),
|
||||||
|
|
||||||
|
if (officer.unitId != null)
|
||||||
|
_buildInfoRow(context, 'Unit ID', officer.unitId!),
|
||||||
|
|
||||||
|
if (officer.validUntil != null)
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
'Valid Until',
|
||||||
|
DateFormat('dd MMMM yyyy').format(officer.validUntil!),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (officer.role != null)
|
||||||
|
_buildInfoRow(context, 'Role', officer.role!.name ?? 'Officer'),
|
||||||
|
|
||||||
|
// Status information if banned
|
||||||
|
if (officer.isBanned) ...[
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.warning_amber_rounded, color: Colors.red, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text('Status Information', style: theme.textTheme.titleLarge),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
_buildInfoRow(context, 'Status', 'Banned', valueColor: Colors.red),
|
||||||
|
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
'Reason',
|
||||||
|
officer.bannedReason ?? 'No reason provided',
|
||||||
|
),
|
||||||
|
|
||||||
|
if (officer.bannedUntil != null)
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
'Banned Until',
|
||||||
|
DateFormat('dd MMMM yyyy, HH:mm').format(officer.bannedUntil!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoRow(
|
||||||
|
BuildContext context,
|
||||||
|
String label,
|
||||||
|
String value, {
|
||||||
|
Color? valueColor,
|
||||||
|
}) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.hintColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(color: valueColor),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Divider(height: 1, color: theme.dividerColor.withOpacity(0.3)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/models/models/profile_model.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/models/models/users_model.dart';
|
||||||
|
|
||||||
|
class UserProfileDetail extends StatelessWidget {
|
||||||
|
final UserModel? user;
|
||||||
|
final ProfileModel? profile;
|
||||||
|
|
||||||
|
const UserProfileDetail({super.key, this.user, this.profile});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 8, 20, 30),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text('Personal Information', style: theme.textTheme.titleLarge),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// User information
|
||||||
|
if (profile?.nik != null)
|
||||||
|
_buildInfoRow(context, 'NIK', profile!.nik!),
|
||||||
|
|
||||||
|
_buildInfoRow(context, 'Email', user?.email ?? 'Not Available'),
|
||||||
|
|
||||||
|
if (user?.phone != null)
|
||||||
|
_buildInfoRow(context, 'Phone', user!.phone!),
|
||||||
|
|
||||||
|
if (profile?.birthDate != null)
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
'Birth Date',
|
||||||
|
DateFormat('dd MMMM yyyy').format(profile!.birthDate!),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (profile?.placeOfBirth != null)
|
||||||
|
_buildInfoRow(context, 'Place of Birth', profile!.placeOfBirth!),
|
||||||
|
|
||||||
|
if (user?.lastSignInAt != null)
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
'Last Sign In',
|
||||||
|
DateFormat('dd MMM yyyy, HH:mm').format(user!.lastSignInAt!),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (user?.role != null)
|
||||||
|
_buildInfoRow(context, 'Role', user!.role!.name ?? 'User'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoRow(BuildContext context, String label, String value) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.hintColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(value, style: theme.textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Divider(height: 1, color: theme.dividerColor.withOpacity(0.3)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/settings_controller.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/pages/profile/profile_screen.dart';
|
||||||
import 'package:sigap/src/features/personalization/presentasion/pages/settings/widgets/contact_screen.dart';
|
import 'package:sigap/src/features/personalization/presentasion/pages/settings/widgets/contact_screen.dart';
|
||||||
import 'package:sigap/src/features/personalization/presentasion/pages/settings/widgets/display_mode_setting.dart';
|
import 'package:sigap/src/features/personalization/presentasion/pages/settings/widgets/display_mode_setting.dart';
|
||||||
import 'package:sigap/src/features/personalization/presentasion/pages/settings/widgets/email_setting.dart';
|
import 'package:sigap/src/features/personalization/presentasion/pages/settings/widgets/email_setting.dart';
|
||||||
|
@ -19,6 +22,8 @@ class SettingsScreen extends StatefulWidget {
|
||||||
class _SettingsScreenState extends State<SettingsScreen>
|
class _SettingsScreenState extends State<SettingsScreen>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
|
// Use find instead of implicit creation with Get.put
|
||||||
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -44,33 +49,45 @@ class _SettingsScreenState extends State<SettingsScreen>
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Profile Section
|
// Profile Section - Now clickable
|
||||||
Container(
|
InkWell(
|
||||||
padding: const EdgeInsets.all(20),
|
onTap: () {
|
||||||
color: theme.scaffoldBackgroundColor,
|
// Navigate to the ProfileScreen when tapped
|
||||||
child: Row(
|
Navigator.push(
|
||||||
children: [
|
context,
|
||||||
CircleAvatar(
|
MaterialPageRoute(
|
||||||
radius: 30,
|
builder:
|
||||||
backgroundColor: theme.hintColor,
|
(context) => const ProfileScreen(isCurrentUser: true),
|
||||||
child: Icon(
|
|
||||||
Icons.person,
|
|
||||||
size: 35,
|
|
||||||
color: theme.scaffoldBackgroundColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 15),
|
);
|
||||||
Expanded(
|
},
|
||||||
child: Column(
|
child: Container(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: const EdgeInsets.all(20),
|
||||||
children: [
|
color: theme.scaffoldBackgroundColor,
|
||||||
Text('Anita Rose', style: theme.textTheme.titleLarge),
|
child: Row(
|
||||||
Text('anitarose', style: theme.textTheme.bodySmall),
|
children: [
|
||||||
],
|
CircleAvatar(
|
||||||
|
radius: 30,
|
||||||
|
backgroundColor: theme.hintColor,
|
||||||
|
child: Icon(
|
||||||
|
Icons.person,
|
||||||
|
size: 35,
|
||||||
|
color: theme.scaffoldBackgroundColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 15),
|
||||||
Icon(Icons.chevron_right, color: theme.hintColor),
|
Expanded(
|
||||||
],
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Anita Rose', style: theme.textTheme.titleLarge),
|
||||||
|
Text('anitarose', style: theme.textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(Icons.chevron_right, color: theme.hintColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/display_mode_controller.dart';
|
||||||
import 'package:sigap/src/features/personalization/presentasion/pages/settings/widgets/base_detail_screen.dart';
|
import 'package:sigap/src/features/personalization/presentasion/pages/settings/widgets/base_detail_screen.dart';
|
||||||
|
|
||||||
class DisplayModeSettingsScreen extends StatefulWidget {
|
class DisplayModeSettingsScreen extends StatefulWidget {
|
||||||
|
@ -10,20 +12,47 @@ class DisplayModeSettingsScreen extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DisplayModeSettingsScreenState extends State<DisplayModeSettingsScreen> {
|
class _DisplayModeSettingsScreenState extends State<DisplayModeSettingsScreen> {
|
||||||
String selectedMode = 'System default';
|
// Use GetX controller
|
||||||
|
final DisplayModeController _controller = Get.find<DisplayModeController>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BaseDetailScreen(
|
return BaseDetailScreen(
|
||||||
title: 'Display Mode',
|
title: 'Display Mode',
|
||||||
content: ListView(
|
content: Obx(() {
|
||||||
children: [
|
// Show loading indicator if controller is loading
|
||||||
_buildSectionHeader('Theme'),
|
if (_controller.isLoading.value) {
|
||||||
_buildRadioItem('System default'),
|
return const Center(child: CircularProgressIndicator());
|
||||||
_buildRadioItem('Light mode'),
|
}
|
||||||
_buildRadioItem('Dark mode'),
|
|
||||||
],
|
// Show error message if there is one
|
||||||
),
|
if (_controller.errorMessage.value.isNotEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Error: ${_controller.errorMessage.value}',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => _controller.loadSettings(),
|
||||||
|
child: Text('Retry'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show normal content
|
||||||
|
return ListView(
|
||||||
|
children: [
|
||||||
|
_buildSectionHeader('Theme'),
|
||||||
|
..._controller.themeOptions.map((mode) => _buildRadioItem(mode)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +73,7 @@ class _DisplayModeSettingsScreenState extends State<DisplayModeSettingsScreen> {
|
||||||
Widget _buildRadioItem(String mode) {
|
Widget _buildRadioItem(String mode) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
IconData icon;
|
IconData icon;
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'System default':
|
case 'System default':
|
||||||
icon = Icons.brightness_auto;
|
icon = Icons.brightness_auto;
|
||||||
|
@ -58,30 +88,28 @@ class _DisplayModeSettingsScreenState extends State<DisplayModeSettingsScreen> {
|
||||||
icon = Icons.brightness_auto;
|
icon = Icons.brightness_auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
return InkWell(
|
return Obx(
|
||||||
onTap: () {
|
() => InkWell(
|
||||||
setState(() {
|
onTap: () => _controller.changeThemeMode(mode),
|
||||||
selectedMode = mode;
|
child: Container(
|
||||||
});
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||||
},
|
child: Row(
|
||||||
child: Container(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
Icon(icon, size: 24, color: theme.iconTheme.color),
|
||||||
child: Row(
|
const SizedBox(width: 15),
|
||||||
children: [
|
Expanded(child: Text(mode, style: theme.textTheme.bodyLarge)),
|
||||||
Icon(icon, size: 24, color: theme.iconTheme.color),
|
Radio<String>(
|
||||||
const SizedBox(width: 15),
|
value: mode,
|
||||||
Expanded(child: Text(mode, style: theme.textTheme.bodyLarge)),
|
groupValue: _controller.selectedThemeMode.value,
|
||||||
Radio<String>(
|
activeColor: theme.primaryColor,
|
||||||
value: mode,
|
onChanged: (value) {
|
||||||
groupValue: selectedMode,
|
if (value != null) {
|
||||||
activeColor: theme.primaryColor,
|
_controller.changeThemeMode(value);
|
||||||
onChanged: (value) {
|
}
|
||||||
setState(() {
|
},
|
||||||
selectedMode = value!;
|
),
|
||||||
});
|
],
|
||||||
},
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue