Settings, correção de light/darkmode do dispositivo (e adição da escolha entre modos nas settings) e correção do tipo de letra no textfield do chatbot

This commit is contained in:
2026-05-11 21:47:15 +01:00
parent 9faab9b74e
commit b7988eb608
13 changed files with 1342 additions and 59 deletions

View File

@@ -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<ThemeNotifier, ThemeMode>((ref) {
return ThemeNotifier();
});
/// Notifier for managing theme state
class ThemeNotifier extends StateNotifier<ThemeMode> {
ThemeNotifier() : super(ThemeMode.light) {
_initializeTheme();
}
/// Initialize theme from storage
Future<void> _initializeTheme() async {
try {
final storedTheme = await ThemeService.getThemeMode();
state = storedTheme;
} catch (e) {
state = ThemeMode.light;
}
}
/// Change theme mode
Future<void> 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<void> toggleTheme() async {
if (ThemeService.isDarkModeAvailable()) {
final newTheme = state == ThemeMode.light
? ThemeMode.dark
: ThemeMode.light;
await setThemeMode(newTheme);
}
}
/// Reset to default theme
Future<void> 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<void> 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<bool>((ref) {
return ThemeService.isDarkModeAvailable();
});
/// Provider for current theme string
final themeStringProvider = Provider<String>((ref) {
final theme = ref.watch(themeProvider);
return ThemeService.getThemeModeString(theme);
});

View File

@@ -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();
}

View File

@@ -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<ThemeMode> 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<void> 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<ThemeMode> 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<void> resetTheme() async {
await setThemeMode(_defaultTheme);
}
}

View File

@@ -294,7 +294,7 @@ class _TutorChatPageSimpleState extends State<TutorChatPageSimple>
controller: _messageController,
style: const TextStyle(
fontSize: 16,
color: Colors.white,
color: Color(0xFF1A1A1A), // Dark text for visibility
fontWeight: FontWeight.w500,
),
decoration: InputDecoration(

View File

@@ -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,7 +89,11 @@ class ProfileSectionWidget extends StatelessWidget {
],
),
),
Container(
GestureDetector(
onTap: () {
AppRouter.goToSettings(context);
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFFF68D2D).withOpacity(0.1),
@@ -100,6 +105,7 @@ class ProfileSectionWidget extends StatelessWidget {
size: 20,
),
),
),
],
),
const SizedBox(height: 20),

View File

@@ -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'),
),
],
),
);
}
}

View File

@@ -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<ProfileEditPage> createState() => _ProfileEditPageState();
}
class _ProfileEditPageState extends ConsumerState<ProfileEditPage> {
final _formKey = GlobalKey<FormState>();
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<void> _loadUserData() async {
final user = AuthService.currentUser;
if (user != null) {
setState(() {
_nameController.text = user.displayName ?? '';
});
}
}
Future<void> _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<Color>(
Colors.white,
),
),
)
: const Text(
'Guardar',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
),
),
],
),
),
),
),
);
}
}

View File

@@ -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<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends ConsumerState<SettingsPage> {
@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<Widget> 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<ThemeMode>(
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<bool>? 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,
),
),
);
}
}

View File

@@ -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<SettingsState> {
SettingsNotifier()
: super(
SettingsState(
themeMode: ThemeService.getThemeMode(),
isDarkModeAvailable: ThemeService.isDarkModeAvailable(),
),
);
/// Set theme mode
Future<void> 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<void> 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<void> resetSettings() async {
state = state.copyWith(isLoading: true);
await ThemeService.resetTheme();
state = state.copyWith(
themeMode: ThemeService.getThemeMode(),
isLoading: false,
);
}
}
/// Settings provider
final settingsProvider = StateNotifierProvider<SettingsNotifier, SettingsState>(
(ref) => SettingsNotifier(),
);
/// Theme mode provider (convenience)
final themeModeProvider = Provider<ThemeMode>(
(ref) => ref.watch(settingsProvider).themeMode,
);
/// Dark mode available provider (convenience)
final isDarkModeAvailableProvider = Provider<bool>(
(ref) => ref.watch(settingsProvider).isDarkModeAvailable,
);

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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,
),