From b7988eb60860dbd83dec0716f350cd5e3910fbf8 Mon Sep 17 00:00:00 2001 From: 240405 <240405@epvc.pt> Date: Mon, 11 May 2026 21:47:15 +0100 Subject: [PATCH] =?UTF-8?q?Settings,=20corre=C3=A7=C3=A3o=20de=20light/dar?= =?UTF-8?q?kmode=20do=20dispositivo=20(e=20adi=C3=A7=C3=A3o=20da=20escolha?= =?UTF-8?q?=20entre=20modos=20nas=20settings)=20e=20corre=C3=A7=C3=A3o=20d?= =?UTF-8?q?o=20tipo=20de=20letra=20no=20textfield=20do=20chatbot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/providers/theme_provider.dart | 85 +++++ lib/core/routing/app_router.dart | 68 ++-- lib/core/services/theme_service.dart | 95 +++++ .../pages/tutor_chat_page_simple.dart | 2 +- .../widgets/profile_section_widget.dart | 26 +- .../presentation/pages/help_page.dart | 315 ++++++++++++++++ .../presentation/pages/profile_edit_page.dart | 337 +++++++++++++++++ .../presentation/pages/settings_page.dart | 349 ++++++++++++++++++ .../providers/settings_provider.dart | 83 +++++ lib/l10n/app_en.arb | 4 + lib/l10n/app_pt.arb | 4 + lib/main.dart | 9 +- .../presentation/pages/not_found_page.dart | 24 +- 13 files changed, 1342 insertions(+), 59 deletions(-) create mode 100644 lib/core/providers/theme_provider.dart create mode 100644 lib/core/services/theme_service.dart create mode 100644 lib/features/settings/presentation/pages/help_page.dart create mode 100644 lib/features/settings/presentation/pages/profile_edit_page.dart create mode 100644 lib/features/settings/presentation/pages/settings_page.dart create mode 100644 lib/features/settings/presentation/providers/settings_provider.dart diff --git a/lib/core/providers/theme_provider.dart b/lib/core/providers/theme_provider.dart new file mode 100644 index 0000000..115212f --- /dev/null +++ b/lib/core/providers/theme_provider.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../services/theme_service.dart'; + +/// Provider for theme management +final themeProvider = StateNotifierProvider((ref) { + return ThemeNotifier(); +}); + +/// Notifier for managing theme state +class ThemeNotifier extends StateNotifier { + ThemeNotifier() : super(ThemeMode.light) { + _initializeTheme(); + } + + /// Initialize theme from storage + Future _initializeTheme() async { + try { + final storedTheme = await ThemeService.getThemeMode(); + state = storedTheme; + } catch (e) { + state = ThemeMode.light; + } + } + + /// Change theme mode + Future setThemeMode(ThemeMode themeMode) async { + // For now, only allow light mode + // Future: Allow dark mode when available + if (themeMode == ThemeMode.light || ThemeService.isDarkModeAvailable()) { + state = themeMode; + await ThemeService.setThemeMode(themeMode); + } + } + + /// Toggle between light and dark mode (for future use) + Future toggleTheme() async { + if (ThemeService.isDarkModeAvailable()) { + final newTheme = state == ThemeMode.light + ? ThemeMode.dark + : ThemeMode.light; + await setThemeMode(newTheme); + } + } + + /// Reset to default theme + Future resetTheme() async { + await setThemeMode(ThemeMode.light); + } + + /// Check if current theme is dark + bool isDarkMode() { + return state == ThemeMode.dark; + } + + /// Check if current theme is light + bool isLightMode() { + return state == ThemeMode.light; + } + + /// Get current theme as string + String get currentThemeString { + return ThemeService.getThemeModeString(state); + } + + /// Initialize theme from storage (for future use) + Future initializeTheme() async { + final storedTheme = await ThemeService.getStoredThemeMode(); + // Only set if dark mode is available or if it's light mode + if (storedTheme == ThemeMode.light || ThemeService.isDarkModeAvailable()) { + state = storedTheme; + } + } +} + +/// Provider for checking if dark mode is available +final isDarkModeAvailableProvider = Provider((ref) { + return ThemeService.isDarkModeAvailable(); +}); + +/// Provider for current theme string +final themeStringProvider = Provider((ref) { + final theme = ref.watch(themeProvider); + return ThemeService.getThemeModeString(theme); +}); diff --git a/lib/core/routing/app_router.dart b/lib/core/routing/app_router.dart index 1ed1b52..8e5230f 100644 --- a/lib/core/routing/app_router.dart +++ b/lib/core/routing/app_router.dart @@ -1,5 +1,9 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import '../../shared/presentation/pages/not_found_page.dart'; +import '../../features/settings/presentation/pages/settings_page.dart'; +import '../../features/settings/presentation/pages/profile_edit_page.dart'; +import '../../features/settings/presentation/pages/help_page.dart'; import '../../features/auth/presentation/pages/login_page.dart'; import '../../features/auth/presentation/pages/signup_page.dart'; import '../../features/dashboard/presentation/pages/student_dashboard_page.dart'; @@ -10,7 +14,6 @@ import '../../features/quiz/presentation/pages/quiz_page.dart'; import '../../features/profile/presentation/pages/profile_page.dart'; import '../../features/splash/presentation/pages/splash_page.dart'; import '../../features/auth/presentation/pages/role_selection_page.dart'; -import '../../shared/presentation/pages/not_found_page.dart'; /// App Router Configuration class AppRouter { @@ -24,6 +27,7 @@ class AppRouter { static const String quizList = '/quiz'; static const String quiz = '/quiz/:quizId'; static const String profile = '/profile'; + static const String settings = '/settings'; // Nested route paths (without leading slash) static const String tutorNested = 'tutor'; @@ -50,53 +54,28 @@ class AppRouter { builder: (context, state) => const RoleSelectionPage(), ), - // Authentication Routes + // Login GoRoute( path: login, name: 'login', - builder: (context, state) { - final selectedRole = state.uri.queryParameters['role']; - return LoginPage(selectedRole: selectedRole); - }, + builder: (context, state) => const LoginPage(), ), + // Signup GoRoute( path: signup, name: 'signup', - builder: (context, state) { - final selectedRole = state.uri.queryParameters['role']; - return SignupPage(selectedRole: selectedRole); - }, + builder: (context, state) => const SignupPage(), ), - // Dashboard Routes + // Student Dashboard GoRoute( path: studentDashboard, name: 'studentDashboard', builder: (context, state) => const StudentDashboardPage(), - routes: [ - // Nested routes for student features - GoRoute( - path: tutorNested, - name: 'studentTutor', - builder: (context, state) => const TutorChatPageSimple(), - ), - GoRoute( - path: quizListNested, - name: 'quizList', - builder: (context, state) => const QuizListPage(), - ), - GoRoute( - path: quizNested, - name: 'quiz', - builder: (context, state) { - final quizId = state.pathParameters['quizId']!; - return QuizPage(quizId: quizId); - }, - ), - ], ), + // Teacher Dashboard GoRoute( path: teacherDashboard, name: 'teacherDashboard', @@ -131,6 +110,27 @@ class AppRouter { builder: (context, state) => const ProfilePage(), ), + // Settings Route + GoRoute( + path: settings, + name: 'settings', + builder: (context, state) => const SettingsPage(), + routes: [ + // Profile Edit Route + GoRoute( + path: 'profile-edit', + name: 'profileEdit', + builder: (context, state) => const ProfileEditPage(), + ), + // Help Route + GoRoute( + path: 'help', + name: 'help', + builder: (context, state) => const HelpPage(), + ), + ], + ), + // AI Tutor Route (independent) GoRoute( path: tutor, @@ -179,6 +179,10 @@ class AppRouter { context.go(profile); } + static void goToSettings(BuildContext context) { + context.go(settings); + } + static void goBack(BuildContext context) { context.pop(); } diff --git a/lib/core/services/theme_service.dart b/lib/core/services/theme_service.dart new file mode 100644 index 0000000..f48ac0e --- /dev/null +++ b/lib/core/services/theme_service.dart @@ -0,0 +1,95 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter/material.dart'; +import '../utils/logger.dart'; + +/// Service for managing app theme preferences +class ThemeService { + static const String _themeKey = 'app_theme_mode'; + static const ThemeMode _defaultTheme = ThemeMode.light; + + /// Get current theme mode from storage + static Future getThemeMode() async { + try { + final prefs = await SharedPreferences.getInstance(); + final themeString = prefs.getString(_themeKey); + + if (themeString == null) { + return _defaultTheme; + } + + switch (themeString) { + case 'ThemeMode.light': + return ThemeMode.light; + case 'ThemeMode.dark': + return ThemeMode.dark; + case 'ThemeMode.system': + return ThemeMode.system; + default: + return _defaultTheme; + } + } catch (e) { + Logger.error('Error getting theme mode: $e'); + return _defaultTheme; + } + } + + /// Save theme mode to storage + static Future setThemeMode(ThemeMode themeMode) async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_themeKey, themeMode.toString()); + Logger.info('Theme mode saved: ${themeMode.toString()}'); + } catch (e) { + Logger.error('Error saving theme mode: $e'); + } + } + + /// Get theme mode from storage (for future use) + static Future getStoredThemeMode() async { + try { + final prefs = await SharedPreferences.getInstance(); + final themeString = prefs.getString(_themeKey); + + if (themeString == null) { + return _defaultTheme; + } + + switch (themeString) { + case 'ThemeMode.light': + return ThemeMode.light; + case 'ThemeMode.dark': + return ThemeMode.dark; + case 'ThemeMode.system': + return ThemeMode.system; + default: + return _defaultTheme; + } + } catch (e) { + Logger.error('Error getting stored theme mode: $e'); + return _defaultTheme; + } + } + + /// Check if dark mode is available (for future settings) + static bool isDarkModeAvailable() { + // Dark mode is now available + return true; + } + + /// Get theme mode string for display + static String getThemeModeString(ThemeMode themeMode) { + switch (themeMode) { + case ThemeMode.light: + return 'Light Mode'; + case ThemeMode.dark: + return 'Dark Mode'; + case ThemeMode.system: + return 'System Default'; + } + } + + /// Reset theme to default + static Future resetTheme() async { + await setThemeMode(_defaultTheme); + } +} diff --git a/lib/features/ai_tutor/presentation/pages/tutor_chat_page_simple.dart b/lib/features/ai_tutor/presentation/pages/tutor_chat_page_simple.dart index f185225..0c03ffd 100644 --- a/lib/features/ai_tutor/presentation/pages/tutor_chat_page_simple.dart +++ b/lib/features/ai_tutor/presentation/pages/tutor_chat_page_simple.dart @@ -294,7 +294,7 @@ class _TutorChatPageSimpleState extends State controller: _messageController, style: const TextStyle( fontSize: 16, - color: Colors.white, + color: Color(0xFF1A1A1A), // Dark text for visibility fontWeight: FontWeight.w500, ), decoration: InputDecoration( diff --git a/lib/features/dashboard/presentation/widgets/profile_section_widget.dart b/lib/features/dashboard/presentation/widgets/profile_section_widget.dart index d417a29..ed0edaf 100644 --- a/lib/features/dashboard/presentation/widgets/profile_section_widget.dart +++ b/lib/features/dashboard/presentation/widgets/profile_section_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import '../../../../core/services/auth_service.dart'; +import '../../../../core/routing/app_router.dart'; /// Profile section with user info and achievements class ProfileSectionWidget extends StatelessWidget { @@ -88,16 +89,21 @@ class ProfileSectionWidget extends StatelessWidget { ], ), ), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: const Color(0xFFF68D2D).withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.settings, - color: Color(0xFFF68D2D), - size: 20, + GestureDetector( + onTap: () { + AppRouter.goToSettings(context); + }, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFFF68D2D).withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.settings, + color: Color(0xFFF68D2D), + size: 20, + ), ), ), ], diff --git a/lib/features/settings/presentation/pages/help_page.dart b/lib/features/settings/presentation/pages/help_page.dart new file mode 100644 index 0000000..ea2f51a --- /dev/null +++ b/lib/features/settings/presentation/pages/help_page.dart @@ -0,0 +1,315 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/theme/app_colors.dart'; + +/// Help page with user guides +class HelpPage extends StatelessWidget { + const HelpPage({super.key}); + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + if (!didPop) { + context.pop(); + } + }, + child: Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF82C9BD), + Color(0xFF7BA89C), + Color(0xFFF68D2D), + Color(0xFFF8F9FA), + ], + stops: [0.0, 0.2, 0.6, 1.0], + ), + ), + child: SafeArea( + child: Column( + children: [ + // Custom AppBar + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => context.pop(), + ), + const Expanded( + child: Text( + 'Ajuda e Suporte', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + // Help content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildGuideCard( + icon: Icons.school, + title: 'Como usar o AI Tutor', + description: + 'Aprenda a tirar o máximo partido do seu assistente de IA personalizado.', + onTap: () => _showGuide(context, 'AI Tutor'), + ), + const SizedBox(height: 16), + _buildGuideCard( + icon: Icons.quiz, + title: 'Como fazer quizzes', + description: + 'Descubra como participar em quizzes interativos e testar os seus conhecimentos.', + onTap: () => _showGuide(context, 'Quizzes'), + ), + const SizedBox(height: 16), + _buildGuideCard( + icon: Icons.trending_up, + title: 'Ver o seu progresso', + description: + 'Acompanhe a sua evolução de aprendizagem e conquistas.', + onTap: () => _showGuide(context, 'Progresso'), + ), + const SizedBox(height: 16), + _buildGuideCard( + icon: Icons.settings, + title: 'Configurações da app', + description: + 'Personalize a sua experiência de utilização.', + onTap: () => _showGuide(context, 'Configurações'), + ), + const SizedBox(height: 24), + // FAQ Section + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Perguntas Frequentes', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 16), + _buildFAQItem( + question: 'Como posso alterar a minha senha?', + answer: + 'Pode alterar a sua senha através da secção de segurança nas definições.', + ), + const SizedBox(height: 12), + _buildFAQItem( + question: 'Os meus dados estão seguros?', + answer: + 'Sim, utilizamos encriptação de ponta a ponta para proteger todos os seus dados.', + ), + const SizedBox(height: 12), + _buildFAQItem( + question: 'Posso usar a app offline?', + answer: + 'Algumas funcionalidades estão disponíveis offline, mas para o AI Tutor necessita de conexão à internet.', + ), + const SizedBox(height: 12), + _buildFAQItem( + question: 'Como contactar o suporte?', + answer: + 'Pode contactar-nos através do email suporte@teachit.com.', + ), + ], + ), + ), + const SizedBox(height: 24), + // Contact Support + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Ainda precisa de ajuda?', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 8), + const Text( + 'A nossa equipa de suporte está disponível para ajudar.', + style: TextStyle( + fontSize: 14, + color: AppColors.textSecondary, + ), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: () { + // Open email client + }, + icon: const Icon(Icons.email), + label: const Text('Contactar Suporte'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryTeal, + foregroundColor: Colors.white, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildGuideCard({ + required IconData icon, + required String title, + required String description, + required VoidCallback onTap, + }) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(16), + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF82C9BD), Color(0xFF6BA8A0)], + ), + borderRadius: BorderRadius.circular(25), + ), + child: Icon(icon, color: Colors.white, size: 24), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 4), + Text( + description, + style: const TextStyle( + fontSize: 12, + color: AppColors.textSecondary, + ), + ), + ], + ), + ), + const Icon(Icons.chevron_right, color: AppColors.iconInactive), + ], + ), + ), + ); + } + + Widget _buildFAQItem({required String question, required String answer}) { + return ExpansionTile( + title: Text( + question, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ), + ), + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + answer, + style: const TextStyle( + fontSize: 13, + color: AppColors.textSecondary, + ), + ), + ), + ], + ); + } + + void _showGuide(BuildContext context, String guideTitle) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(guideTitle), + content: const Text('Conteúdo do guia em desenvolvimento...'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Fechar'), + ), + ], + ), + ); + } +} diff --git a/lib/features/settings/presentation/pages/profile_edit_page.dart b/lib/features/settings/presentation/pages/profile_edit_page.dart new file mode 100644 index 0000000..faa4cba --- /dev/null +++ b/lib/features/settings/presentation/pages/profile_edit_page.dart @@ -0,0 +1,337 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/services/auth_service.dart'; +import '../../../../core/theme/app_colors.dart'; + +/// Profile edit page for settings +class ProfileEditPage extends ConsumerStatefulWidget { + const ProfileEditPage({super.key}); + + @override + ConsumerState createState() => _ProfileEditPageState(); +} + +class _ProfileEditPageState extends ConsumerState { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _phoneController = TextEditingController(); + final _bioController = TextEditingController(); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _loadUserData(); + } + + @override + void dispose() { + _nameController.dispose(); + _phoneController.dispose(); + _bioController.dispose(); + super.dispose(); + } + + Future _loadUserData() async { + final user = AuthService.currentUser; + if (user != null) { + setState(() { + _nameController.text = user.displayName ?? ''; + }); + } + } + + Future _saveProfile() async { + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() { + _isLoading = true; + }); + + try { + final user = AuthService.currentUser; + if (user != null) { + await user.updateDisplayName(_nameController.text); + await user.reload(); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Perfil atualizado com sucesso!'), + backgroundColor: AppColors.primaryTeal, + ), + ); + context.pop(); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erro ao atualizar perfil: $e'), + backgroundColor: AppColors.error, + ), + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + final user = AuthService.currentUser; + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + if (!didPop) { + context.pop(); + } + }, + child: Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF82C9BD), + Color(0xFF7BA89C), + Color(0xFFF68D2D), + Color(0xFFF8F9FA), + ], + stops: [0.0, 0.2, 0.6, 1.0], + ), + ), + child: SafeArea( + child: Column( + children: [ + // Custom AppBar + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => context.pop(), + ), + const Expanded( + child: Text( + 'Editar Perfil', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + // Form content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Avatar section + Center( + child: Column( + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [ + Color(0xFF82C9BD), + Color(0xFF6BA8A0), + ], + ), + borderRadius: BorderRadius.circular(40), + ), + child: const Icon( + Icons.person, + color: Colors.white, + size: 40, + ), + ), + const SizedBox(height: 16), + Text( + user?.email ?? '', + style: const TextStyle( + color: AppColors.textSecondary, + fontSize: 14, + ), + ), + ], + ), + ), + const SizedBox(height: 32), + + // Name field + const Text( + 'Nome', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: _nameController, + decoration: InputDecoration( + hintText: 'Introduza o seu nome', + filled: true, + fillColor: AppColors.background, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Por favor introduza o seu nome'; + } + return null; + }, + ), + const SizedBox(height: 24), + + // Phone field (optional) + const Text( + 'Telefone (opcional)', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: _phoneController, + keyboardType: TextInputType.phone, + decoration: InputDecoration( + hintText: 'Introduza o seu telefone', + filled: true, + fillColor: AppColors.background, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + const SizedBox(height: 24), + + // Bio field (optional) + const Text( + 'Bio (opcional)', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: _bioController, + maxLines: 3, + decoration: InputDecoration( + hintText: 'Conte um pouco sobre si', + filled: true, + fillColor: AppColors.background, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + const SizedBox(height: 32), + + // Save button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isLoading ? null : _saveProfile, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryTeal, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: _isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation( + Colors.white, + ), + ), + ) + : const Text( + 'Guardar', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/settings/presentation/pages/settings_page.dart b/lib/features/settings/presentation/pages/settings_page.dart new file mode 100644 index 0000000..a3a5250 --- /dev/null +++ b/lib/features/settings/presentation/pages/settings_page.dart @@ -0,0 +1,349 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/theme/app_colors.dart'; +import '../../../../core/services/theme_service.dart'; +import '../../../../core/providers/theme_provider.dart'; + +/// Settings page for app configuration +class SettingsPage extends ConsumerStatefulWidget { + const SettingsPage({super.key}); + + @override + ConsumerState createState() => _SettingsPageState(); +} + +class _SettingsPageState extends ConsumerState { + @override + Widget build(BuildContext context) { + final themeMode = ref.watch(themeProvider); + final isDarkModeAvailable = ThemeService.isDarkModeAvailable(); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + if (!didPop) { + // Navigate to student dashboard on back button + context.go('/student-dashboard'); + } + }, + child: Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF82C9BD), + Color(0xFF7BA89C), + Color(0xFFF68D2D), + Color(0xFFF8F9FA), + ], + stops: [0.0, 0.2, 0.6, 1.0], + ), + ), + child: SafeArea( + child: Column( + children: [ + // Custom AppBar + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => context.go('/student-dashboard'), + ), + const Expanded( + child: Text( + 'Configurações', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + // Settings content + Expanded( + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + // Appearance Section + _buildSection( + title: 'Aparência', + themeMode: themeMode, + children: [ + _buildThemeTile( + context: context, + currentTheme: themeMode, + isDarkModeAvailable: isDarkModeAvailable, + onThemeChanged: (ThemeMode newTheme) async { + await ref + .read(themeProvider.notifier) + .setThemeMode(newTheme); + }, + ), + ], + ), + + const SizedBox(height: 24), + + // Future sections can be added here + _buildSection( + title: 'Notificações', + themeMode: themeMode, + children: [ + _buildToggleTile( + themeMode: themeMode, + title: 'Notificações Push', + subtitle: 'Receber notificações da app', + value: false, // Future implementation + onChanged: null, // Future implementation + enabled: false, // Disabled for now + ), + ], + ), + + const SizedBox(height: 24), + + _buildSection( + title: 'Conta', + themeMode: themeMode, + children: [ + _buildActionTile( + themeMode: themeMode, + title: 'Perfil', + subtitle: 'Gerir informações do perfil', + icon: Icons.person, + onTap: () { + // Navigate to profile edit + context.push('/settings/profile-edit'); + }, + ), + _buildActionTile( + themeMode: themeMode, + title: 'Privacidade', + subtitle: 'Definições de privacidade e segurança', + icon: Icons.security, + onTap: () { + // Navigate to privacy settings + }, + ), + ], + ), + + const SizedBox(height: 24), + + _buildSection( + title: 'Sobre', + themeMode: themeMode, + children: [ + _buildInfoTile( + themeMode: themeMode, + title: 'Versão', + subtitle: '1.0.0', + icon: Icons.info, + ), + _buildActionTile( + themeMode: themeMode, + title: 'Ajuda', + subtitle: 'Obter ajuda e suporte', + icon: Icons.help, + onTap: () { + // Navigate to help + context.push('/settings/help'); + }, + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildSection({ + required String title, + required ThemeMode themeMode, + required List children, + }) { + final isDark = themeMode == ThemeMode.dark; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, bottom: 8), + child: Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white70 : AppColors.textSecondary, + ), + ), + ), + Container( + decoration: BoxDecoration( + color: isDark ? Colors.grey[800] : AppColors.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Column(children: children), + ), + ], + ); + } + + Widget _buildThemeTile({ + required BuildContext context, + required ThemeMode currentTheme, + required bool isDarkModeAvailable, + required Function(ThemeMode) onThemeChanged, + }) { + final isDark = currentTheme == ThemeMode.dark; + return ListTile( + leading: const Icon(Icons.palette, color: AppColors.primaryTeal), + title: Text( + 'Tema', + style: TextStyle(color: isDark ? Colors.white : AppColors.textPrimary), + ), + subtitle: Text( + isDarkModeAvailable + ? 'Atual: ${_getThemeModeString(currentTheme)}' + : 'Light Mode (padrão)', + style: TextStyle( + color: isDarkModeAvailable + ? (isDark ? Colors.white70 : AppColors.textSecondary) + : AppColors.textHint, + ), + ), + trailing: isDarkModeAvailable + ? DropdownButton( + value: currentTheme, + onChanged: (ThemeMode? newValue) { + if (newValue != null) { + onThemeChanged(newValue); + } + }, + items: const [ + DropdownMenuItem( + value: ThemeMode.light, + child: Text('Light Mode'), + ), + DropdownMenuItem( + value: ThemeMode.dark, + child: Text('Dark Mode'), + ), + DropdownMenuItem( + value: ThemeMode.system, + child: Text('System Default'), + ), + ], + ) + : const Icon(Icons.lock, color: AppColors.textHint), + ); + } + + String _getThemeModeString(ThemeMode themeMode) { + switch (themeMode) { + case ThemeMode.light: + return 'Light Mode'; + case ThemeMode.dark: + return 'Dark Mode'; + case ThemeMode.system: + return 'System Default'; + } + } + + Widget _buildToggleTile({ + required ThemeMode themeMode, + required String title, + required String subtitle, + required bool value, + required ValueChanged? onChanged, + bool enabled = true, + }) { + final isDark = themeMode == ThemeMode.dark; + return ListTile( + leading: const Icon(Icons.notifications, color: AppColors.primaryTeal), + title: Text( + title, + style: TextStyle(color: isDark ? Colors.white : AppColors.textPrimary), + ), + subtitle: Text( + subtitle, + style: TextStyle( + color: isDark ? Colors.white70 : AppColors.textSecondary, + ), + ), + trailing: Switch( + value: value, + onChanged: enabled ? onChanged : null, + activeColor: AppColors.primaryTeal, + ), + enabled: enabled, + ); + } + + Widget _buildActionTile({ + required ThemeMode themeMode, + required String title, + required String subtitle, + required IconData icon, + required VoidCallback onTap, + }) { + final isDark = themeMode == ThemeMode.dark; + return ListTile( + leading: Icon(icon, color: AppColors.primaryTeal), + title: Text( + title, + style: TextStyle(color: isDark ? Colors.white : AppColors.textPrimary), + ), + subtitle: Text( + subtitle, + style: TextStyle( + color: isDark ? Colors.white70 : AppColors.textSecondary, + ), + ), + trailing: const Icon(Icons.chevron_right, color: AppColors.iconInactive), + onTap: onTap, + ); + } + + Widget _buildInfoTile({ + required ThemeMode themeMode, + required String title, + required String subtitle, + required IconData icon, + }) { + final isDark = themeMode == ThemeMode.dark; + return ListTile( + leading: Icon(icon, color: AppColors.primaryTeal), + title: Text( + title, + style: TextStyle(color: isDark ? Colors.white : AppColors.textPrimary), + ), + subtitle: Text( + subtitle, + style: TextStyle( + color: isDark ? Colors.white70 : AppColors.textSecondary, + ), + ), + ); + } +} diff --git a/lib/features/settings/presentation/providers/settings_provider.dart b/lib/features/settings/presentation/providers/settings_provider.dart new file mode 100644 index 0000000..9225391 --- /dev/null +++ b/lib/features/settings/presentation/providers/settings_provider.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../core/services/theme_service.dart'; + +/// Settings state +class SettingsState { + final ThemeMode themeMode; + final bool isDarkModeAvailable; + final bool isLoading; + + const SettingsState({ + required this.themeMode, + required this.isDarkModeAvailable, + this.isLoading = false, + }); + + SettingsState copyWith({ + ThemeMode? themeMode, + bool? isDarkModeAvailable, + bool? isLoading, + }) { + return SettingsState( + themeMode: themeMode ?? this.themeMode, + isDarkModeAvailable: isDarkModeAvailable ?? this.isDarkModeAvailable, + isLoading: isLoading ?? this.isLoading, + ); + } +} + +/// Settings notifier +class SettingsNotifier extends StateNotifier { + SettingsNotifier() + : super( + SettingsState( + themeMode: ThemeService.getThemeMode(), + isDarkModeAvailable: ThemeService.isDarkModeAvailable(), + ), + ); + + /// Set theme mode + Future setThemeMode(ThemeMode themeMode) async { + state = state.copyWith(isLoading: true); + await ThemeService.setThemeMode(themeMode); + state = state.copyWith(themeMode: themeMode, isLoading: false); + } + + /// Load settings from storage + Future loadSettings() async { + state = state.copyWith(isLoading: true); + final themeMode = await ThemeService.getStoredThemeMode(); + final isDarkModeAvailable = ThemeService.isDarkModeAvailable(); + state = state.copyWith( + themeMode: themeMode, + isDarkModeAvailable: isDarkModeAvailable, + isLoading: false, + ); + } + + /// Reset settings to default + Future resetSettings() async { + state = state.copyWith(isLoading: true); + await ThemeService.resetTheme(); + state = state.copyWith( + themeMode: ThemeService.getThemeMode(), + isLoading: false, + ); + } +} + +/// Settings provider +final settingsProvider = StateNotifierProvider( + (ref) => SettingsNotifier(), +); + +/// Theme mode provider (convenience) +final themeModeProvider = Provider( + (ref) => ref.watch(settingsProvider).themeMode, +); + +/// Dark mode available provider (convenience) +final isDarkModeAvailableProvider = Provider( + (ref) => ref.watch(settingsProvider).isDarkModeAvailable, +); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c5c8fbe..ce5311e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -97,6 +97,10 @@ "@pageNotFound": { "description": "Page not found title" }, + "pageNotFoundDescription": "The page you are looking for does not exist.", + "@pageNotFoundDescription": { + "description": "Page not found description" + }, "loading": "Loading...", "@loading": { "description": "Loading message" diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 00e3bff..c2bdce3 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -97,6 +97,10 @@ "@pageNotFound": { "description": "Page not found title" }, + "pageNotFoundDescription": "A página que procura não existe.", + "@pageNotFoundDescription": { + "description": "Page not found description" + }, "loading": "A carregar...", "@loading": { "description": "Loading message" diff --git a/lib/main.dart b/lib/main.dart index fdca207..5d70f99 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'core/theme/app_theme.dart'; import 'core/routing/app_router.dart'; import 'core/services/firebase/firebase_service.dart'; +import 'core/providers/theme_provider.dart'; import 'l10n/app_localizations.dart'; void main() async { @@ -15,17 +16,19 @@ void main() async { runApp(const ProviderScope(child: MyApp())); } -class MyApp extends StatelessWidget { +class MyApp extends ConsumerWidget { const MyApp({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final themeMode = ref.watch(themeProvider); + return MaterialApp.router( title: 'AI Study Assistant', debugShowCheckedModeBanner: false, theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, - themeMode: ThemeMode.system, + themeMode: themeMode, // Use theme from provider (currently always light) routerConfig: AppRouter.router, // Internationalization configuration diff --git a/lib/shared/presentation/pages/not_found_page.dart b/lib/shared/presentation/pages/not_found_page.dart index e8237f2..20b40fd 100644 --- a/lib/shared/presentation/pages/not_found_page.dart +++ b/lib/shared/presentation/pages/not_found_page.dart @@ -1,41 +1,39 @@ import 'package:flutter/material.dart'; import '../../../../core/theme/app_colors.dart'; +import '../../../../l10n/app_localizations.dart'; class NotFoundPage extends StatelessWidget { const NotFoundPage({super.key}); @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return Scaffold( backgroundColor: AppColors.background, appBar: AppBar( - title: const Text('Page Not Found'), + title: Text(l10n.pageNotFound), backgroundColor: AppColors.surface, foregroundColor: AppColors.textPrimary, elevation: 0, ), - body: const Center( + body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.error_outline, - size: 64, - color: AppColors.error, - ), - SizedBox(height: 16), + const Icon(Icons.error_outline, size: 64, color: AppColors.error), + const SizedBox(height: 16), Text( - 'Page Not Found', - style: TextStyle( + l10n.pageNotFound, + style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), - SizedBox(height: 8), + const SizedBox(height: 8), Text( - 'The page you are looking for does not exist.', - style: TextStyle( + 'A página que procura não existe.', + style: const TextStyle( fontSize: 16, color: AppColors.textSecondary, ),