diff --git a/lib/colors/app_colors.dart b/lib/colors/app_colors.dart new file mode 100644 index 0000000..2115a96 --- /dev/null +++ b/lib/colors/app_colors.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +/// Paleta de cores da aplicação — todas as cores de marca usadas em mais do +/// que um sítio vivem aqui. Para mudar uma cor em toda a app, mude-a aqui +/// em vez de editar cada ecrã individualmente. +class AppColors { + const AppColors._(); + + /// Verde-azulado principal — botões, ícones e destaques "positivos". + static const Color teal = Color(0xFF2F9E94); + + /// Rosa principal — títulos, destaques e ações "de atenção". + static const Color pink = Color(0xFFFF55A7); + + /// Rosa claro — usado no título do ecrã de boas-vindas. + static const Color pinkLight = Color(0xFFFF9AD0); + + /// Rosa vivo — extremo do gradiente do card de destaque do quiz. + static const Color pinkVivid = Color(0xFFE83E93); + + /// Roxo — usado em pequenos destaques informativos (ex.: ícone de + /// "Episódios completos"). + static const Color purple = Color(0xFF8E7CC3); + + /// Verde-azulado escuro — extremo do gradiente das app bars. + static const Color tealDark = Color(0xFF1C7A6E); + + /// Verde-azulado claro — extremo do gradiente das app bars. + static const Color tealLight = Color(0xFF8FD4BB); + + /// Fundo bege claro usado em quase todos os ecrãs. + static const Color background = Color(0xFFFAFAF7); + + /// Fundo rosa muito claro — usado em folhas modais e cartões suaves. + static const Color pinkBackground = Color(0xFFFFE6F1); +} diff --git a/lib/widgets/app_gradients.dart b/lib/colors/app_gradients.dart similarity index 88% rename from lib/widgets/app_gradients.dart rename to lib/colors/app_gradients.dart index 1fa1380..0ca3b81 100644 --- a/lib/widgets/app_gradients.dart +++ b/lib/colors/app_gradients.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'app_colors.dart'; + /// Gradiente usado em todas as app bars da aplicação. Direção puramente /// horizontal (não diagonal) de propósito: a app bar retrátil da Home é /// alta (~190) enquanto as das outras abas são baixas (~56) — um gradiente @@ -10,7 +12,7 @@ import 'package:flutter/material.dart'; const LinearGradient kAppBarGradient = LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, - colors: [Color(0xFF1C7A6E), Color(0xFF8FD4BB)], + colors: [AppColors.tealDark, AppColors.tealLight], ); /// Cor sólida (sem gradiente, de propósito) usada nos botões e cards verdes @@ -19,12 +21,12 @@ const LinearGradient kAppBarGradient = LinearGradient( /// mantém-se como [LinearGradient] com as duas cores iguais para não obrigar /// a mudar todos os `decoration: gradient: kGreenButtonGradient` existentes. const LinearGradient kGreenButtonGradient = LinearGradient( - colors: [Color(0xFF2F9E94), Color(0xFF2F9E94)], + colors: [AppColors.teal, AppColors.teal], ); /// Gradiente rosa vivo do card do quiz na Home. const LinearGradient kPinkHeroGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFFFF55A7), Color(0xFFE83E93)], + colors: [AppColors.pink, AppColors.pinkVivid], ); diff --git a/lib/home_screen.dart b/lib/home_screen.dart index 0db9b93..a45dab7 100644 --- a/lib/home_screen.dart +++ b/lib/home_screen.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'colors/app_colors.dart'; import 'package:flutter/services.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -8,15 +9,16 @@ import 'auth_gate.dart' show pendingPrivacyUserId, pendingTermsUserId; import 'main.dart' show supabase; import 'privacy_gate_prefs.dart'; import 'terms_gate_prefs.dart'; -import 'widgets/app_gradients.dart'; +import 'colors/app_gradients.dart'; +import 'strings/auth_strings.dart'; import 'widgets/entrance.dart'; import 'widgets/liquid_waves_background.dart'; import 'widgets/name_input_formatter.dart'; import 'widgets/pill_snackbar.dart'; import 'widgets/tap_bounce.dart'; -const Color _teal = Color(0xFF2F9E94); -const Color _pink = Color(0xFFFF55A7); +const Color _teal = AppColors.teal; +const Color _pink = AppColors.pink; /// Nomes só podem ter letras (incluindo acentuadas) e espaços — sem números. final RegExp _namePattern = RegExp(r"^[a-zA-ZÀ-ÖØ-öø-ÿ' -]+$"); @@ -113,7 +115,7 @@ class _HomeScreenState extends State { final user = response.user; if (user == null) { - throw StateError('Utilizador não encontrado após criar a conta.'); + throw StateError(AuthStrings.userNotFoundAfterSignUp); } // Antes de persistir o perfil (o que já faz o AuthGate considerar a @@ -137,7 +139,7 @@ class _HomeScreenState extends State { if (!mounted) return; showPillSnackBar( context, - 'Esta conta já não existe. Verifique o email ou crie uma nova conta.', + AuthStrings.accountNoLongerExists, ); } on AuthException catch (e) { if (!mounted) return; @@ -146,11 +148,11 @@ class _HomeScreenState extends State { if (!mounted) return; showPillSnackBar( context, - 'Tempo esgotado. Verifique a sua ligação e tente novamente.', + AuthStrings.timeoutError, ); } catch (e) { if (!mounted) return; - showPillSnackBar(context, 'Erro: $e'); + showPillSnackBar(context, AuthStrings.genericError(e)); } finally { if (mounted) setState(() => _loading = false); } @@ -159,14 +161,14 @@ class _HomeScreenState extends State { String _friendlyAuthError(AuthException e) { switch (e.code) { case 'invalid_credentials': - return 'Email ou palavra-passe incorretos.'; + return AuthStrings.invalidCredentials; case 'user_not_found': - return 'Utilizador não encontrado.'; + return AuthStrings.userNotFound; case 'email_exists': case 'user_already_exists': - return 'Este email já está em uso.'; + return AuthStrings.emailAlreadyInUse; case 'weak_password': - return 'Palavra-passe fraca. Utilize pelo menos 6 caracteres.'; + return AuthStrings.weakPassword; default: return e.message; } @@ -178,7 +180,7 @@ class _HomeScreenState extends State { body: Stack( clipBehavior: Clip.none, children: [ - Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), + Positioned.fill(child: Container(color: AppColors.background)), const LiquidWavesBackground(), SafeArea( child: LayoutBuilder( @@ -195,7 +197,7 @@ class _HomeScreenState extends State { children: [ const FadeSlideIn( child: Text( - 'Check-Teeth Kids', + AuthStrings.appTitle, textAlign: TextAlign.center, style: TextStyle( fontSize: 26, @@ -210,7 +212,7 @@ class _HomeScreenState extends State { FadeSlideIn( delay: const Duration(milliseconds: 80), child: Text( - 'Deteção e Prevenção da Má Oclusão Infantil', + AuthStrings.appSubtitle, textAlign: TextAlign.center, style: TextStyle( fontSize: 13.5, @@ -279,14 +281,14 @@ class _AuthTabSwitch extends StatelessWidget { children: [ Expanded( child: _AuthTab( - label: 'Entrar', + label: AuthStrings.login, selected: isLogin, onTap: () => onChanged(true), ), ), Expanded( child: _AuthTab( - label: 'Criar Conta', + label: AuthStrings.createAccount, selected: !isLogin, onTap: () => onChanged(false), ), @@ -376,17 +378,17 @@ class _AuthForm extends StatelessWidget { children: [ _AuthTextField( controller: nameController, - hintText: 'Introduza o seu nome', + hintText: AuthStrings.nameHint, icon: Icons.person_outline_rounded, textInputAction: TextInputAction.next, textCapitalization: TextCapitalization.sentences, inputFormatters: [CapitalizeFirstLetterFormatter()], validator: (v) { final value = (v ?? '').trim(); - if (value.isEmpty) return 'Indique o seu nome'; - if (value.length < 2) return 'Nome muito curto'; + if (value.isEmpty) return AuthStrings.nameRequired; + if (value.length < 2) return AuthStrings.nameTooShort; if (!_namePattern.hasMatch(value)) { - return 'O nome não pode conter números'; + return AuthStrings.nameNoNumbers; } return null; }, @@ -398,28 +400,28 @@ class _AuthForm extends StatelessWidget { ), _AuthTextField( controller: emailController, - hintText: 'Introduza o seu email', + hintText: AuthStrings.emailHint, icon: Icons.mail_outline_rounded, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, validator: (v) { final value = (v ?? '').trim(); - if (value.isEmpty) return 'Indique o seu email'; - if (!value.contains('@')) return 'Email inválido'; + if (value.isEmpty) return AuthStrings.emailRequired; + if (!value.contains('@')) return AuthStrings.emailInvalid; return null; }, ), const SizedBox(height: 12), _AuthTextField( controller: passwordController, - hintText: 'Introduza a sua palavra-passe', + hintText: AuthStrings.passwordHint, icon: Icons.lock_outline_rounded, obscureText: true, textInputAction: TextInputAction.done, validator: (v) { final value = v ?? ''; - if (value.isEmpty) return 'Indique a sua palavra-passe'; - if (value.length < 6) return 'Mínimo de 6 caracteres'; + if (value.isEmpty) return AuthStrings.passwordRequired; + if (value.length < 6) return AuthStrings.passwordTooShort; return null; }, ), @@ -471,7 +473,7 @@ class _AuthForm extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Text(isLogin ? 'Entrar' : 'Criar Conta'), + Text(isLogin ? AuthStrings.login : AuthStrings.createAccount), const SizedBox(width: 8), const Icon(Icons.arrow_forward_rounded, size: 18), ], diff --git a/lib/logged_home.dart b/lib/logged_home.dart index d1f29c5..3f16ceb 100644 --- a/lib/logged_home.dart +++ b/lib/logged_home.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'colors/app_colors.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -17,7 +18,8 @@ import 'screens/video_screen.dart'; import 'watched_videos_prefs.dart'; import 'widgets/animated_nav_icon.dart'; import 'widgets/app_dialogs.dart'; -import 'widgets/app_gradients.dart'; +import 'colors/app_gradients.dart'; +import 'strings/home_strings.dart'; import 'widgets/entrance.dart'; import 'widgets/liquid_waves_background.dart'; import 'widgets/name_input_formatter.dart'; @@ -58,7 +60,7 @@ class LoggedHomeScreen extends StatefulWidget { class _LoggedHomeScreenState extends State with SingleTickerProviderStateMixin { - static const Color _teal = Color(0xFF2F9E94); + static const Color _teal = AppColors.teal; static const String _kPendingQuizScopeKey = 'pending_quiz_scope_v1'; static const double _collapsedAppBarHeight = kToolbarHeight; @@ -77,7 +79,7 @@ class _LoggedHomeScreenState extends State bool _brushingDailyLimitReached = false; int? _watchedVideoCount; - String _cachedUserName = 'Sem nome'; + String _cachedUserName = HomeStrings.noName; String? _cachedPhotoUrl; @override @@ -268,9 +270,9 @@ class _LoggedHomeScreenState extends State String _greeting() { final hour = DateTime.now().hour; - if (hour < 12) return 'Bom dia'; - if (hour < 18) return 'Boa tarde'; - return 'Boa noite'; + if (hour < 12) return HomeStrings.goodMorning; + if (hour < 18) return HomeStrings.goodAfternoon; + return HomeStrings.goodEvening; } /// Fundo comum (cor sólida + ondas decorativas) atrás de qualquer aba, com @@ -279,7 +281,7 @@ class _LoggedHomeScreenState extends State return Stack( clipBehavior: Clip.none, children: [ - Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), + Positioned.fill(child: Container(color: AppColors.background)), const LiquidWavesBackground(), SafeArea( top: false, @@ -303,7 +305,7 @@ class _LoggedHomeScreenState extends State HapticFeedback.selectionClick(); setState(() => _index = i); }, - backgroundColor: const Color(0xFFFAFAF7), + backgroundColor: AppColors.background, selectedItemColor: _teal, unselectedItemColor: Colors.black54, type: BottomNavigationBarType.fixed, @@ -313,21 +315,21 @@ class _LoggedHomeScreenState extends State icon: Icons.home_rounded, selected: _index == 0, ), - label: 'Início', + label: HomeStrings.navHome, ), BottomNavigationBarItem( icon: AnimatedNavIcon( icon: Icons.person_rounded, selected: _index == 1, ), - label: 'Perfil', + label: HomeStrings.navProfile, ), BottomNavigationBarItem( icon: AnimatedNavIcon( icon: Icons.settings_rounded, selected: _index == 2, ), - label: 'Ajustes', + label: HomeStrings.navSettings, ), ], ); @@ -415,13 +417,13 @@ class _LoggedHomeScreenState extends State // quando o número de perguntas muda. value: hasScore ? result.signs : null, max: hasScore ? kSignsMax : null, - label: 'Sinais de má\noclusão', + label: HomeStrings.signsGaugeLabel, ), const SizedBox(width: 18), _RiskArcGauge( value: hasScore ? result.factors : null, max: hasScore ? kFactorsMax : null, - label: 'Fatores de risco', + label: HomeStrings.factorsGaugeLabel, ), ], ), @@ -510,7 +512,7 @@ class _LoggedHomeScreenState extends State ); } - final String title = _index == 1 ? 'Perfil' : 'Configurações'; + final String title = _index == 1 ? HomeStrings.navProfile : HomeStrings.settingsTitle; return Scaffold( appBar: PreferredSize( @@ -668,7 +670,7 @@ class _RiskArcGaugePainter extends CustomPainter { ..strokeCap = StrokeCap.round; final progressPaint = Paint() - ..color = const Color(0xFFFF9AD0) + ..color = AppColors.pinkLight ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..strokeCap = StrokeCap.round; @@ -754,7 +756,7 @@ class _InicioTab extends StatelessWidget { children: [ if (selectedChildName.isNotEmpty) ...[ FadeSlideIn( - child: _HomeSectionLabel('Para $selectedChildName'), + child: _HomeSectionLabel(HomeStrings.forChild(selectedChildName)), ), const SizedBox(height: 8), ], @@ -781,7 +783,7 @@ class _InicioTab extends StatelessWidget { const SizedBox(height: 20), FadeSlideIn( delay: const Duration(milliseconds: 110), - child: const _HomeSectionLabel('Vídeos educativos'), + child: const _HomeSectionLabel(HomeStrings.educationalVideos), ), const SizedBox(height: 8), FadeSlideIn( @@ -806,7 +808,7 @@ class _InicioTab extends StatelessWidget { delay: const Duration(milliseconds: 150), child: Center( child: Text( - 'Mais funcionalidades em breve', + HomeStrings.moreFeaturesSoon, style: TextStyle( fontWeight: FontWeight.w700, fontSize: 12.5, @@ -840,7 +842,7 @@ class _InicioTab extends StatelessWidget { if (context.mounted) { showPillSnackBar( context, - 'Já registou as ${BrushingPrefs.maxPerDay} escovagens de hoje!', + HomeStrings.brushingLimitReached(BrushingPrefs.maxPerDay), ); } return; @@ -848,7 +850,7 @@ class _InicioTab extends StatelessWidget { await BrushingPrefs.logToday(scope); await state?.refreshStats(); if (context.mounted) { - showPillSnackBar(context, 'Escovagem registada!'); + showPillSnackBar(context, HomeStrings.brushingLogged); } } } @@ -945,11 +947,11 @@ class _StatsRow extends StatelessWidget { scale: 0.97, child: _StatCard( icon: Icons.brush_rounded, - iconColor: const Color(0xFFFF55A7), + iconColor: AppColors.pink, value: brushingCount == null ? '--' : '$brushingCount/$weeklyGoal', - label: 'Escovagens esta semana', + label: HomeStrings.brushingThisWeek, done: brushedToday, onTap: onTapBrushing, ), @@ -959,9 +961,9 @@ class _StatsRow extends StatelessWidget { Expanded( child: _StatCard( icon: Icons.movie_filter_rounded, - iconColor: const Color(0xFF8E7CC3), + iconColor: AppColors.purple, value: watchedCount == null ? '--' : '$watchedCount', - label: 'Episódios completos', + label: HomeStrings.completedEpisodes, ), ), ], @@ -1022,7 +1024,7 @@ class _StatCard extends StatelessWidget { width: 16, height: 16, decoration: const BoxDecoration( - color: Color(0xFF2F9E94), + color: AppColors.teal, shape: BoxShape.circle, ), child: const Icon( @@ -1067,7 +1069,7 @@ Future?> _createChildViaSheet( context: context, isScrollControlled: true, showDragHandle: true, - backgroundColor: const Color(0xFFFFE6F1), + backgroundColor: AppColors.pinkBackground, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), @@ -1084,9 +1086,18 @@ Future?> _createChildViaSheet( .select() .single(); return inserted; + } on PostgrestException catch (e) { + if (!context.mounted) return null; + showPillSnackBar( + context, + e.code == '23505' + ? HomeStrings.childCodeAlreadyInUse + : HomeStrings.errorAddingChild(e), + ); + return null; } catch (e) { if (!context.mounted) return null; - showPillSnackBar(context, 'Erro ao adicionar criança: $e'); + showPillSnackBar(context, HomeStrings.errorAddingChild(e)); return null; } } @@ -1097,9 +1108,9 @@ Future?> _requireFirstChild( ) async { final proceed = await showConfirmDialog( context, - title: 'Registe uma criança', - message: 'Antes de iniciar o quiz, adicione uma criança ao seu perfil.', - confirmLabel: 'Adicionar criança', + title: HomeStrings.registerAChild, + message: HomeStrings.registerAChildMessage, + confirmLabel: HomeStrings.addChild, ); if (proceed != true) return null; @@ -1111,12 +1122,12 @@ Future?> _pickChildSheet( BuildContext context, List> children, ) { - const Color teal = Color(0xFF2F9E94); + const Color teal = AppColors.teal; return showModalBottomSheet?>( context: context, isScrollControlled: true, showDragHandle: true, - backgroundColor: const Color(0xFFFFE6F1), + backgroundColor: AppColors.pinkBackground, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), @@ -1132,12 +1143,12 @@ Future?> _pickChildSheet( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( - 'Para qual criança é o quiz?', + HomeStrings.whichChildIsTheQuizFor, textAlign: TextAlign.center, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w900, - color: Color(0xFFFF55A7), + color: AppColors.pink, ), ), const SizedBox(height: 14), @@ -1149,7 +1160,7 @@ Future?> _pickChildSheet( final name = (c['name'] ?? '').toString(); final age = _childAge(c); final label = age != null - ? '$name • $age anos' + ? HomeStrings.childNameWithAge(name, age) : name; return Padding( padding: const EdgeInsets.only(bottom: 10), @@ -1194,7 +1205,7 @@ Future?> _pickChildSheet( const SizedBox(height: 4), TextButton( onPressed: () => Navigator.of(ctx).pop(null), - child: const Text('Cancelar'), + child: const Text(HomeStrings.cancel), ), ], ), @@ -1214,7 +1225,7 @@ class _HeroQuizCard extends StatelessWidget { Widget build(BuildContext context) { return Material( elevation: 18, - shadowColor: const Color(0xFFFF55A7).withValues(alpha: 0.45), + shadowColor: AppColors.pink.withValues(alpha: 0.45), borderRadius: BorderRadius.circular(28), clipBehavior: Clip.antiAlias, color: Colors.transparent, @@ -1300,7 +1311,7 @@ class _HeroQuizCard extends StatelessWidget { ), SizedBox(width: 4), Text( - 'Avaliação gratuita', + HomeStrings.freeAssessmentBadge, style: TextStyle( color: Colors.white, fontWeight: FontWeight.w800, @@ -1328,7 +1339,7 @@ class _HeroQuizCard extends StatelessWidget { ), const SizedBox(height: 14), const Text( - 'Avaliação de má oclusão dentária', + HomeStrings.assessmentTitle, style: TextStyle( fontWeight: FontWeight.w900, fontSize: 22, @@ -1338,7 +1349,7 @@ class _HeroQuizCard extends StatelessWidget { ), const SizedBox(height: 5), Text( - '29 perguntas rápidas · menos de 3 minutos', + HomeStrings.assessmentSubtitle, style: TextStyle( color: Colors.white.withValues(alpha: 0.92), fontWeight: FontWeight.w600, @@ -1356,7 +1367,7 @@ class _HeroQuizCard extends StatelessWidget { child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: Colors.white, - foregroundColor: const Color(0xFFFF55A7), + foregroundColor: AppColors.pink, shape: const StadiumBorder(), elevation: 6, shadowColor: Colors.black.withValues(alpha: 0.25), @@ -1367,7 +1378,7 @@ class _HeroQuizCard extends StatelessWidget { ), onPressed: onStartQuiz, icon: const Icon(Icons.play_arrow_rounded), - label: const Text('Iniciar Quiz'), + label: const Text(HomeStrings.startQuiz), ), ), ), @@ -1396,7 +1407,7 @@ class _VideoLibraryCard extends StatelessWidget { Widget build(BuildContext context) { return Material( elevation: 14, - shadowColor: const Color(0xFF2F9E94).withValues(alpha: 0.38), + shadowColor: AppColors.teal.withValues(alpha: 0.38), borderRadius: BorderRadius.circular(28), clipBehavior: Clip.antiAlias, color: Colors.transparent, @@ -1465,7 +1476,7 @@ class _VideoLibraryCard extends StatelessWidget { ), SizedBox(width: 4), Text( - 'Biblioteca de vídeos', + HomeStrings.videoLibraryBadge, style: TextStyle( color: Colors.white, fontWeight: FontWeight.w800, @@ -1477,7 +1488,7 @@ class _VideoLibraryCard extends StatelessWidget { ), const SizedBox(height: 14), const Text( - 'Vídeos educativos', + HomeStrings.educationalVideos, style: TextStyle( fontWeight: FontWeight.w900, fontSize: 22, @@ -1488,8 +1499,11 @@ class _VideoLibraryCard extends StatelessWidget { const SizedBox(height: 5), Text( watchedCount > 0 - ? '$watchedCount episódio${watchedCount == 1 ? '' : 's'} completo${watchedCount == 1 ? '' : 's'} · ${videoList.length} no total' - : '${videoList.length} episódios sobre saúde oral para toda a família', + ? HomeStrings.watchedEpisodesSummary( + watchedCount, + videoList.length, + ) + : HomeStrings.allEpisodesSummary(videoList.length), style: TextStyle( color: Colors.white.withValues(alpha: 0.92), fontWeight: FontWeight.w600, @@ -1503,7 +1517,7 @@ class _VideoLibraryCard extends StatelessWidget { child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: Colors.white, - foregroundColor: const Color(0xFF2F9E94), + foregroundColor: AppColors.teal, shape: const StadiumBorder(), textStyle: const TextStyle( fontWeight: FontWeight.w800, @@ -1512,7 +1526,7 @@ class _VideoLibraryCard extends StatelessWidget { ), onPressed: onTap, icon: const Icon(Icons.video_library_rounded), - label: const Text('Ver vídeos'), + label: const Text(HomeStrings.watchVideos), ), ), ], @@ -1597,7 +1611,7 @@ class _PerfilTabState extends State<_PerfilTab> { final source = await showModalBottomSheet( context: context, showDragHandle: true, - backgroundColor: const Color(0xFFFFE6F1), + backgroundColor: AppColors.pinkBackground, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), @@ -1610,12 +1624,12 @@ class _PerfilTabState extends State<_PerfilTab> { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( - 'Foto de perfil', + HomeStrings.profilePhoto, textAlign: TextAlign.center, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w900, - color: Color(0xFFFF55A7), + color: AppColors.pink, ), ), const SizedBox(height: 14), @@ -1638,7 +1652,7 @@ class _PerfilTabState extends State<_PerfilTab> { ), onPressed: () => Navigator.of(ctx).pop(ImageSource.camera), - child: const Text('Câmara'), + child: const Text(HomeStrings.camera), ), ), ), @@ -1663,7 +1677,7 @@ class _PerfilTabState extends State<_PerfilTab> { ), onPressed: () => Navigator.of(ctx).pop(ImageSource.gallery), - child: const Text('Galeria'), + child: const Text(HomeStrings.gallery), ), ), ), @@ -1673,10 +1687,10 @@ class _PerfilTabState extends State<_PerfilTab> { height: 42, child: TextButton( style: TextButton.styleFrom( - foregroundColor: const Color(0xFF2F9E94), + foregroundColor: AppColors.teal, ), onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Cancelar'), + child: const Text(HomeStrings.cancel), ), ), ], @@ -1716,7 +1730,7 @@ class _PerfilTabState extends State<_PerfilTab> { } } catch (e) { if (!context.mounted) return; - showPillSnackBar(context, 'Erro ao enviar foto: $e'); + showPillSnackBar(context, HomeStrings.errorUploadingPhoto(e)); } finally { if (mounted) setState(() => _updatingPhoto = false); } @@ -1729,11 +1743,10 @@ class _PerfilTabState extends State<_PerfilTab> { }) async { final confirmed = await showConfirmDialog( context, - title: 'Remover criança', - message: - 'Tem a certeza que quer remover "$childName"? Esta ação não pode ser desfeita.', - confirmLabel: 'Remover', - confirmColor: const Color(0xFFFF55A7), + title: HomeStrings.removeChild, + message: HomeStrings.removeChildConfirmMessage(childName), + confirmLabel: HomeStrings.remove, + confirmColor: AppColors.pink, ); if (confirmed != true) return; @@ -1747,15 +1760,15 @@ class _PerfilTabState extends State<_PerfilTab> { .select('id'); if (deleted.isEmpty) { - throw StateError('Sem permissão para remover esta criança.'); + throw StateError(HomeStrings.noPermissionToRemoveChild); } widget.onChildSelected(0, null, null); await _loadPerfilData(); - if (context.mounted) showPillSnackBar(context, 'Criança removida'); + if (context.mounted) showPillSnackBar(context, HomeStrings.childRemoved); } catch (e) { if (context.mounted) { - showPillSnackBar(context, 'Erro ao remover: $e'); + showPillSnackBar(context, HomeStrings.errorRemovingChild(e)); } } } @@ -1773,15 +1786,15 @@ class _PerfilTabState extends State<_PerfilTab> { context: context, builder: (ctx) { return AlertDialog( - backgroundColor: const Color(0xFFFFE6F1), + backgroundColor: AppColors.pinkBackground, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), title: Text( - 'Meta semanal de $childName', + HomeStrings.weeklyGoalOf(childName), style: const TextStyle( fontWeight: FontWeight.w900, - color: Color(0xFFFF55A7), + color: AppColors.pink, ), ), content: TextField( @@ -1789,14 +1802,14 @@ class _PerfilTabState extends State<_PerfilTab> { keyboardType: TextInputType.number, autofocus: true, decoration: const InputDecoration( - labelText: 'Escovagens por semana', - helperText: 'Entre 1 e 21 (até 3 por dia)', + labelText: HomeStrings.brushingsPerWeek, + helperText: HomeStrings.brushingGoalRange, ), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Cancelar'), + child: const Text(HomeStrings.cancel), ), ClipRRect( borderRadius: BorderRadius.circular(999), @@ -1813,7 +1826,7 @@ class _PerfilTabState extends State<_PerfilTab> { if (value == null || value < 1 || value > 21) return; Navigator.of(ctx).pop(value); }, - child: const Text('Guardar'), + child: const Text(HomeStrings.save), ), ), ), @@ -1835,7 +1848,7 @@ class _PerfilTabState extends State<_PerfilTab> { context: context, isScrollControlled: true, showDragHandle: true, - backgroundColor: const Color(0xFFFFE6F1), + backgroundColor: AppColors.pinkBackground, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), @@ -1857,7 +1870,9 @@ class _PerfilTabState extends State<_PerfilTab> { if (!mounted) return; await _loadPerfilData(); - if (context.mounted) showPillSnackBar(context, 'Criança adicionada'); + if (context.mounted) { + showPillSnackBar(context, HomeStrings.childAdded); + } if (mounted) { setState(() => _addingChild = false); @@ -1868,9 +1883,9 @@ class _PerfilTabState extends State<_PerfilTab> { final addMore = await showConfirmDialog( // ignore: use_build_context_synchronously context, - title: 'Adicionar outra criança?', - cancelLabel: 'Agora não', - confirmLabel: 'Adicionar outra', + title: HomeStrings.addAnotherChildQuestion, + cancelLabel: HomeStrings.notNow, + confirmLabel: HomeStrings.addAnother, ); if (!mounted) return; @@ -1886,11 +1901,11 @@ class _PerfilTabState extends State<_PerfilTab> { if (!mounted || !context.mounted) return; showPillSnackBar( context, - 'Tempo esgotado ao adicionar. Tente novamente.', + HomeStrings.timeoutAdding, ); } catch (e) { if (!mounted || !context.mounted) return; - showPillSnackBar(context, 'Erro ao adicionar: $e'); + showPillSnackBar(context, HomeStrings.errorAdding(e)); } finally { if (mounted) setState(() => _addingChild = false); } @@ -1902,7 +1917,7 @@ class _PerfilTabState extends State<_PerfilTab> { final uid = (user?.id ?? '').trim(); final name = (user?.userMetadata?['name'] ?? '').toString().trim(); final email = (user?.email ?? '').trim(); - final shownName = name.isNotEmpty ? name : 'Sem nome'; + final shownName = name.isNotEmpty ? name : HomeStrings.noName; if (uid.isEmpty) { return const SizedBox.shrink(); @@ -1912,7 +1927,7 @@ class _PerfilTabState extends State<_PerfilTab> { return const Center( child: Padding( padding: EdgeInsets.only(top: 60), - child: CircularProgressIndicator(color: Color(0xFF2F9E94)), + child: CircularProgressIndicator(color: AppColors.teal), ), ); } @@ -1970,12 +1985,12 @@ class _PerfilTabState extends State<_PerfilTab> { width: 76, height: 76, decoration: BoxDecoration( - color: const Color(0xFFFFE6F1), + color: AppColors.pinkBackground, shape: BoxShape.circle, border: Border.all( - color: const Color( - 0xFF2F9E94, - ).withValues(alpha: 0.35), + color: AppColors.teal.withValues( + alpha: 0.35, + ), width: 2, ), ), @@ -1992,7 +2007,7 @@ class _PerfilTabState extends State<_PerfilTab> { const Icon( Icons.person_rounded, size: 42, - color: Color(0xFF2F9E94), + color: AppColors.teal, ), if (_updatingPhoto) Container( @@ -2020,7 +2035,7 @@ class _PerfilTabState extends State<_PerfilTab> { width: 26, height: 26, decoration: const BoxDecoration( - color: Color(0xFFFF55A7), + color: AppColors.pink, shape: BoxShape.circle, ), child: const Icon( @@ -2046,7 +2061,7 @@ class _PerfilTabState extends State<_PerfilTab> { style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w900, - color: Color(0xFFFF55A7), + color: AppColors.pink, ), ), if (profileEmail.isNotEmpty) ...[ @@ -2078,9 +2093,9 @@ class _PerfilTabState extends State<_PerfilTab> { child: Row( children: [ const Text( - 'Meus filhos', + HomeStrings.myChildren, style: TextStyle( - color: Color(0xFF2F9E94), + color: AppColors.teal, fontWeight: FontWeight.w900, fontSize: 15, ), @@ -2092,15 +2107,13 @@ class _PerfilTabState extends State<_PerfilTab> { vertical: 2, ), decoration: BoxDecoration( - color: const Color( - 0xFF2F9E94, - ).withValues(alpha: 0.10), + color: AppColors.teal.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(999), ), child: Text( '${children.length}', style: const TextStyle( - color: Color(0xFF2F9E94), + color: AppColors.teal, fontWeight: FontWeight.w900, fontSize: 12, ), @@ -2126,18 +2139,18 @@ class _PerfilTabState extends State<_PerfilTab> { width: 40, height: 40, decoration: BoxDecoration( - color: const Color(0xFFFFE6F1), + color: AppColors.pinkBackground, borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.child_care_rounded, - color: Color(0xFFFF55A7), + color: AppColors.pink, ), ), const SizedBox(width: 12), Expanded( child: Text( - 'Nenhuma criança adicionada ainda.', + HomeStrings.noChildrenYet, style: TextStyle( color: Colors.black.withValues(alpha: 0.62), fontWeight: FontWeight.w600, @@ -2155,14 +2168,20 @@ class _PerfilTabState extends State<_PerfilTab> { final childName = (c['name'] ?? '').toString().trim(); final childAge = _childAge(c); final childGender = (c['gender'] ?? '').toString().trim(); + final childCode = (c['child_code'] ?? '') + .toString() + .trim(); final scopeId = '${uid}_$childId'; final title = childName.isNotEmpty ? childName - : 'Criança ${i + 1}'; + : HomeStrings.childFallbackName(i); final subtitle = [ - if (childAge != null) 'Idade: $childAge', - if (childGender.isNotEmpty) 'Género: $childGender', + if (childAge != null) HomeStrings.ageLabel(childAge), + if (childGender.isNotEmpty) + HomeStrings.genderLabel(childGender), + if (childCode.isNotEmpty) + HomeStrings.codeLabel(childCode), ].join(' • '); final bool selected = i == selectedIndex; @@ -2183,14 +2202,12 @@ class _PerfilTabState extends State<_PerfilTab> { padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: selected - ? const Color(0xFFFFE6F1) + ? AppColors.pinkBackground : Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( color: selected - ? const Color( - 0xFF2F9E94, - ).withValues(alpha: 0.45) + ? AppColors.teal.withValues(alpha: 0.45) : Colors.black.withValues(alpha: 0.10), width: selected ? 1.6 : 1, ), @@ -2237,9 +2254,9 @@ class _PerfilTabState extends State<_PerfilTab> { vertical: 8, ), decoration: BoxDecoration( - color: const Color( - 0xFF2F9E94, - ).withValues(alpha: 0.10), + color: AppColors.teal.withValues( + alpha: 0.10, + ), borderRadius: BorderRadius.circular( 999, ), @@ -2248,7 +2265,7 @@ class _PerfilTabState extends State<_PerfilTab> { text, style: const TextStyle( fontWeight: FontWeight.w900, - color: Color(0xFF2F9E94), + color: AppColors.teal, ), ), ); @@ -2262,9 +2279,9 @@ class _PerfilTabState extends State<_PerfilTab> { ), icon: const Icon( Icons.edit_outlined, - color: Color(0xFF2F9E94), + color: AppColors.teal, ), - tooltip: 'Meta semanal de escovagens', + tooltip: HomeStrings.weeklyBrushingGoalTooltip, visualDensity: VisualDensity.compact, ), IconButton( @@ -2275,9 +2292,9 @@ class _PerfilTabState extends State<_PerfilTab> { ), icon: const Icon( Icons.delete_outline_rounded, - color: Color(0xFFFF55A7), + color: AppColors.pink, ), - tooltip: 'Remover', + tooltip: HomeStrings.remove, visualDensity: VisualDensity.compact, ), ], @@ -2310,7 +2327,7 @@ class _PerfilTabState extends State<_PerfilTab> { ? null : () => _addAnotherChild(context, uid), icon: const Icon(Icons.add_rounded), - label: const Text('Adicionar criança'), + label: const Text(HomeStrings.addChild), ), ), ), @@ -2322,9 +2339,9 @@ class _PerfilTabState extends State<_PerfilTab> { height: 46, child: OutlinedButton.icon( style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFFFF55A7), + foregroundColor: AppColors.pink, side: const BorderSide( - color: Color(0xFFFF55A7), + color: AppColors.pink, width: 1.4, ), shape: const StadiumBorder(), @@ -2334,7 +2351,7 @@ class _PerfilTabState extends State<_PerfilTab> { await supabase.auth.signOut(); }, icon: const Icon(Icons.logout_rounded), - label: const Text('Sair'), + label: const Text(HomeStrings.signOut), ), ), ), @@ -2358,6 +2375,7 @@ class _AddChildSheet extends StatefulWidget { class _AddChildSheetState extends State<_AddChildSheet> { final _formKey = GlobalKey(); final _nameController = TextEditingController(); + final _codeController = TextEditingController(); DateTime? _birthDate; String? _gender; String? _birthDateError; @@ -2365,6 +2383,7 @@ class _AddChildSheetState extends State<_AddChildSheet> { @override void dispose() { _nameController.dispose(); + _codeController.dispose(); super.dispose(); } @@ -2375,9 +2394,9 @@ class _AddChildSheetState extends State<_AddChildSheet> { initialDate: _birthDate ?? DateTime(now.year - 5, now.month, now.day), firstDate: DateTime(now.year - 17, now.month, now.day), lastDate: DateTime(now.year - 1, now.month, now.day), - helpText: 'Data de nascimento', - cancelText: 'Cancelar', - confirmText: 'Confirmar', + helpText: HomeStrings.birthDate, + cancelText: HomeStrings.cancel, + confirmText: HomeStrings.confirm, locale: const Locale('pt', 'PT'), ); if (picked == null) return; @@ -2391,7 +2410,7 @@ class _AddChildSheetState extends State<_AddChildSheet> { final formOk = _formKey.currentState?.validate() ?? false; setState(() { _birthDateError = _birthDate == null - ? 'Indique a data de nascimento' + ? HomeStrings.birthDateRequired : null; }); if (!formOk || _birthDate == null) return; @@ -2399,6 +2418,7 @@ class _AddChildSheetState extends State<_AddChildSheet> { 'name': _nameController.text.trim(), 'birth_date': _birthDate!.toIso8601String().split('T').first, 'gender': (_gender ?? '').trim(), + 'child_code': _codeController.text.trim(), }); } @@ -2413,12 +2433,12 @@ class _AddChildSheetState extends State<_AddChildSheet> { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( - 'Adicionar outra criança', + HomeStrings.addAnotherChildTitle, textAlign: TextAlign.center, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w900, - color: Color(0xFFFF55A7), + color: AppColors.pink, ), ), const SizedBox(height: 12), @@ -2440,24 +2460,40 @@ class _AddChildSheetState extends State<_AddChildSheet> { textCapitalization: TextCapitalization.sentences, inputFormatters: [CapitalizeFirstLetterFormatter()], decoration: const InputDecoration( - labelText: 'Nome da criança', + labelText: HomeStrings.childName, ), validator: (v) { final value = (v ?? '').trim(); - if (value.isEmpty) return 'Indique o nome'; - if (value.length < 2) return 'Nome muito curto'; + if (value.isEmpty) return HomeStrings.nameRequired; + if (value.length < 2) return HomeStrings.nameTooShort; if (!_namePattern.hasMatch(value)) { - return 'O nome não pode conter números'; + return HomeStrings.nameNoNumbers; } return null; }, ), + TextFormField( + controller: _codeController, + textInputAction: TextInputAction.next, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + decoration: const InputDecoration( + labelText: HomeStrings.childCode, + ), + validator: (v) { + final value = (v ?? '').trim(); + if (value.isEmpty) return HomeStrings.childCodeRequired; + return null; + }, + ), InkWell( borderRadius: BorderRadius.circular(8), onTap: _pickBirthDate, child: InputDecorator( decoration: InputDecoration( - labelText: 'Data de nascimento', + labelText: HomeStrings.birthDate, errorText: _birthDateError, suffixIcon: const Icon( Icons.calendar_today_rounded, @@ -2466,7 +2502,7 @@ class _AddChildSheetState extends State<_AddChildSheet> { ), child: Text( _birthDate == null - ? 'Selecione a data' + ? HomeStrings.selectDate : '${_birthDate!.day.toString().padLeft(2, '0')}/' '${_birthDate!.month.toString().padLeft(2, '0')}/' '${_birthDate!.year}', @@ -2482,20 +2518,20 @@ class _AddChildSheetState extends State<_AddChildSheet> { initialValue: _gender, items: const [ DropdownMenuItem( - value: 'Masculino', - child: Text('Masculino'), + value: HomeStrings.male, + child: Text(HomeStrings.male), ), DropdownMenuItem( - value: 'Feminino', - child: Text('Feminino'), + value: HomeStrings.female, + child: Text(HomeStrings.female), ), - DropdownMenuItem(value: 'Outro', child: Text('Outro')), + DropdownMenuItem(value: HomeStrings.other, child: Text(HomeStrings.other)), ], onChanged: (v) => setState(() => _gender = v), - decoration: const InputDecoration(labelText: 'Género'), + decoration: const InputDecoration(labelText: HomeStrings.gender), validator: (v) { if (v == null || v.trim().isEmpty) { - return 'Selecione o género'; + return HomeStrings.genderRequired; } return null; }, @@ -2512,7 +2548,7 @@ class _AddChildSheetState extends State<_AddChildSheet> { height: 44, child: TextButton( onPressed: () => Navigator.of(context).pop(null), - child: const Text('Cancelar'), + child: const Text(HomeStrings.cancel), ), ), ), @@ -2537,7 +2573,7 @@ class _AddChildSheetState extends State<_AddChildSheet> { ), ), onPressed: _submit, - child: const Text('Adicionar'), + child: const Text(HomeStrings.add), ), ), ), diff --git a/lib/main.dart b/lib/main.dart index 676bc3f..ec500c6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'colors/app_colors.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -48,8 +49,8 @@ class MyApp extends StatelessWidget { title: 'Check-Teeth Kids', debugShowCheckedModeBanner: false, theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2F9E94)), - scaffoldBackgroundColor: const Color(0xFFFAFAF7), + colorScheme: ColorScheme.fromSeed(seedColor: AppColors.teal), + scaffoldBackgroundColor: AppColors.background, useMaterial3: true, // Sem isto, o Material 3 aplica por padrão uma sobreposição de cor // (surfaceTintColor) e uma elevação extra ao rolar (scrolledUnderElevation) diff --git a/lib/quiz/quiz1.dart b/lib/quiz/quiz1.dart index ef90ec7..7bc0aa4 100644 --- a/lib/quiz/quiz1.dart +++ b/lib/quiz/quiz1.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; +import '../strings/quiz_strings.dart'; +import '../strings/quiz_ui_strings.dart'; import 'quiz_checklist_screen.dart'; import 'quiz_question_screen.dart'; @@ -56,11 +59,11 @@ Route quizPageRoute({required WidgetBuilder builder}) { Route quizStartRoute({String? scopeId}) { return quizPageRoute( builder: (_) => QuizChecklistScreen( - heading: 'Vamos ajudá-lo/a a compreender:', + heading: QuizStrings.checklistHeading, items: const [ - 'Sinais de alerta podem passar despercebidos', - 'Prevenir é melhor do que tratar', - 'Quando deve procurar um dentista (urgência vs vigilância)', + QuizStrings.checklistItem1, + QuizStrings.checklistItem2, + QuizStrings.checklistItem3, ], onAdvance: (context) => Navigator.of(context).pushReplacement( quizPageRoute(builder: (_) => Quiz1Screen(scopeId: scopeId)), @@ -83,25 +86,25 @@ class Quiz1Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 1/29', - fallbackColor: const Color(0xFFFF55A7), - question: 'O seu filho/a tem problemas respiratórios diagnosticados?', + title: QuizUiStrings.quizProgress(1, 29), + fallbackColor: AppColors.pink, + question: QuizStrings.q1Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Problemas respiratórios diagnosticados', + title: QuizStrings.yes, + description: QuizStrings.q1YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Sem problemas respiratórios diagnosticados', + title: QuizStrings.no, + description: QuizStrings.q1NoDescription, weight: 1, value: 'nao', ), QuizAnswer( - title: 'Não sei', - description: 'Não tenho a certeza', + title: QuizStrings.dontKnow, + description: QuizStrings.dontKnowDescription, weight: 1, value: 'nao_sei', helpVideoId: 6, @@ -127,25 +130,25 @@ class Quiz2Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 2/29', - fallbackColor: const Color(0xFF2F9E94), - question: 'O seu filho/a respira habitualmente pela boca?', + title: QuizUiStrings.quizProgress(2, 29), + fallbackColor: AppColors.teal, + question: QuizStrings.q2Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Respira habitualmente pela boca', + title: QuizStrings.yes, + description: QuizStrings.q2YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não respira habitualmente pela boca', + title: QuizStrings.no, + description: QuizStrings.q2NoDescription, weight: 1, value: 'nao', ), QuizAnswer( - title: 'Não sei', - description: 'Não tenho a certeza', + title: QuizStrings.dontKnow, + description: QuizStrings.dontKnowDescription, weight: 1, value: 'nao_sei', helpVideoId: 10, @@ -171,25 +174,25 @@ class Quiz3Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 3/29', - fallbackColor: const Color(0xFFFF55A7), - question: 'O seu filho/a ressona habitualmente durante a noite?', + title: QuizUiStrings.quizProgress(3, 29), + fallbackColor: AppColors.pink, + question: QuizStrings.q3Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Ressonar habitualmente durante a noite', + title: QuizStrings.yes, + description: QuizStrings.q3YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não ressona habitualmente', + title: QuizStrings.no, + description: QuizStrings.q3NoDescription, weight: 1, value: 'nao', ), QuizAnswer( - title: 'Não sei', - description: 'Não tenho a certeza', + title: QuizStrings.dontKnow, + description: QuizStrings.dontKnowDescription, weight: 1, value: 'nao_sei', helpVideoId: 8, @@ -215,25 +218,25 @@ class Quiz4Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 4/29', - fallbackColor: const Color(0xFFFF55A7), - question: 'O seu filho/a sente habitualmente o nariz "tapado"?', + title: QuizUiStrings.quizProgress(4, 29), + fallbackColor: AppColors.pink, + question: QuizStrings.q4Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Sente habitualmente o nariz tapado', + title: QuizStrings.yes, + description: QuizStrings.q4YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não sente habitualmente o nariz tapado', + title: QuizStrings.no, + description: QuizStrings.q4NoDescription, weight: 1, value: 'nao', ), QuizAnswer( - title: 'Não sei', - description: 'Não tenho a certeza', + title: QuizStrings.dontKnow, + description: QuizStrings.dontKnowDescription, weight: 1, value: 'nao_sei', helpVideoId: 1, @@ -259,27 +262,25 @@ class Quiz5Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 5/29', - fallbackColor: const Color(0xFFFF55A7), - question: - 'Durante o sono, o seu filho/a tem habitualmente interrupções da respiração?', + title: QuizUiStrings.quizProgress(5, 29), + fallbackColor: AppColors.pink, + question: QuizStrings.q5Question, answers: const [ QuizAnswer( - title: 'Sim', - description: - 'Tem habitualmente interrupções da respiração durante o sono', + title: QuizStrings.yes, + description: QuizStrings.q5YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não tem interrupções da respiração durante o sono', + title: QuizStrings.no, + description: QuizStrings.q5NoDescription, weight: 1, value: 'nao', ), QuizAnswer( - title: 'Não sei', - description: 'Não tenho a certeza', + title: QuizStrings.dontKnow, + description: QuizStrings.dontKnowDescription, weight: 1, value: 'nao_sei', helpVideoId: 7, @@ -305,19 +306,19 @@ class Quiz6Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 6/29', - fallbackColor: const Color(0xFF2F9E94), - question: 'O seu filho/a range os dentes com frequência?', + title: QuizUiStrings.quizProgress(6, 29), + fallbackColor: AppColors.teal, + question: QuizStrings.q6Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Range os dentes com frequência', + title: QuizStrings.yes, + description: QuizStrings.q6YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não range os dentes com frequência', + title: QuizStrings.no, + description: QuizStrings.q6NoDescription, weight: 1, value: 'nao', ), @@ -342,25 +343,25 @@ class Quiz7Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 7/29', - fallbackColor: const Color(0xFFFF55A7), - question: 'O seu filho/a habitualmente tem alergias sazonais?', + title: QuizUiStrings.quizProgress(7, 29), + fallbackColor: AppColors.pink, + question: QuizStrings.q7Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Habitualmente tem alergias sazonais', + title: QuizStrings.yes, + description: QuizStrings.q7YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não tem alergias sazonais', + title: QuizStrings.no, + description: QuizStrings.q7NoDescription, weight: 1, value: 'nao', ), QuizAnswer( - title: 'Não sei', - description: 'Não tenho a certeza', + title: QuizStrings.dontKnow, + description: QuizStrings.dontKnowDescription, weight: 1, value: 'nao_sei', helpVideoId: 2, @@ -386,25 +387,25 @@ class Quiz8Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 8/29', - fallbackColor: const Color(0xFFFF55A7), - question: 'O seu filho/a acorda com saliva seca na cara ou na almofada?', + title: QuizUiStrings.quizProgress(8, 29), + fallbackColor: AppColors.pink, + question: QuizStrings.q8Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Acorda com saliva seca na cara ou na almofada', + title: QuizStrings.yes, + description: QuizStrings.q8YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não acorda com saliva seca', + title: QuizStrings.no, + description: QuizStrings.q8NoDescription, weight: 1, value: 'nao', ), QuizAnswer( - title: 'Não sei', - description: 'Não tenho a certeza', + title: QuizStrings.dontKnow, + description: QuizStrings.dontKnowDescription, weight: 1, value: 'nao_sei', helpVideoId: 9, @@ -430,25 +431,25 @@ class Quiz9Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 9/29', - fallbackColor: const Color(0xFF2F9E94), - question: 'O seu filho/a teve ou costuma ter com frequência otites?', + title: QuizUiStrings.quizProgress(9, 29), + fallbackColor: AppColors.teal, + question: QuizStrings.q9Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Teve ou costuma ter com frequência otites', + title: QuizStrings.yes, + description: QuizStrings.q9YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não teve ou não costuma ter otites', + title: QuizStrings.no, + description: QuizStrings.q9NoDescription, weight: 1, value: 'nao', ), QuizAnswer( - title: 'Não sei', - description: 'Não tenho a certeza', + title: QuizStrings.dontKnow, + description: QuizStrings.dontKnowDescription, weight: 1, value: 'nao_sei', helpVideoId: 3, @@ -474,25 +475,25 @@ class Quiz10Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 10/29', - fallbackColor: const Color(0xFFFF55A7), - question: 'O seu filho/a teve ou costuma ter com frequência amigdalites?', + title: QuizUiStrings.quizProgress(10, 29), + fallbackColor: AppColors.pink, + question: QuizStrings.q10Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Teve ou costuma ter com frequência amigdalites', + title: QuizStrings.yes, + description: QuizStrings.q10YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não teve ou não costuma ter amigdalites', + title: QuizStrings.no, + description: QuizStrings.q10NoDescription, weight: 1, value: 'nao', ), QuizAnswer( - title: 'Não sei', - description: 'Não tenho a certeza', + title: QuizStrings.dontKnow, + description: QuizStrings.dontKnowDescription, weight: 1, value: 'nao_sei', helpVideoId: 4, @@ -518,26 +519,25 @@ class Quiz11Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 11/29', - fallbackColor: const Color(0xFF2F9E94), - question: - 'O seu filho/a teve ou costuma ter com frequência bronquiolites?', + title: QuizUiStrings.quizProgress(11, 29), + fallbackColor: AppColors.teal, + question: QuizStrings.q11Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Teve ou costuma ter com frequência bronquiolites', + title: QuizStrings.yes, + description: QuizStrings.q11YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não teve ou não costuma ter bronquiolites', + title: QuizStrings.no, + description: QuizStrings.q11NoDescription, weight: 1, value: 'nao', ), QuizAnswer( - title: 'Não sei', - description: 'Não tenho a certeza', + title: QuizStrings.dontKnow, + description: QuizStrings.dontKnowDescription, weight: 1, value: 'nao_sei', helpVideoId: 5, @@ -563,19 +563,19 @@ class Quiz12Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 12/29', - fallbackColor: const Color(0xFFFF55A7), - question: 'O seu filho/a apresenta dificuldades a mastigar?', + title: QuizUiStrings.quizProgress(12, 29), + fallbackColor: AppColors.pink, + question: QuizStrings.q12Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Apresenta dificuldades a mastigar', + title: QuizStrings.yes, + description: QuizStrings.q12YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não apresenta dificuldades a mastigar', + title: QuizStrings.no, + description: QuizStrings.q12NoDescription, weight: 1, value: 'nao', ), @@ -600,19 +600,19 @@ class Quiz13Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 13/29', - fallbackColor: const Color(0xFF2F9E94), - question: 'O seu filho/a habitualmente é lento a comer?', + title: QuizUiStrings.quizProgress(13, 29), + fallbackColor: AppColors.teal, + question: QuizStrings.q13Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Habitualmente é lento a comer', + title: QuizStrings.yes, + description: QuizStrings.q13YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não é lento a comer', + title: QuizStrings.no, + description: QuizStrings.q13NoDescription, weight: 1, value: 'nao', ), @@ -637,19 +637,19 @@ class Quiz14Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 14/29', - fallbackColor: const Color(0xFFFF55A7), - question: 'O seu filho/a habitualmente prefere comer alimentos moles?', + title: QuizUiStrings.quizProgress(14, 29), + fallbackColor: AppColors.pink, + question: QuizStrings.q14Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Habitualmente prefere comer alimentos moles', + title: QuizStrings.yes, + description: QuizStrings.q14YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não prefere alimentos moles', + title: QuizStrings.no, + description: QuizStrings.q14NoDescription, weight: 1, value: 'nao', ), @@ -674,25 +674,25 @@ class Quiz15Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 15/29', - fallbackColor: const Color(0xFFFF55A7), - question: 'Em bebé apenas foi alimentado por biberão?', + title: QuizUiStrings.quizProgress(15, 29), + fallbackColor: AppColors.pink, + question: QuizStrings.q15Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Em bebé apenas foi alimentado por biberão', + title: QuizStrings.yes, + description: QuizStrings.q15YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não foi apenas alimentado por biberão', + title: QuizStrings.no, + description: QuizStrings.q15NoDescription, weight: 1, value: 'nao', ), QuizAnswer( - title: 'Não sei', - description: 'Não tenho a certeza', + title: QuizStrings.dontKnow, + description: QuizStrings.dontKnowDescription, weight: 1, value: 'nao_sei', helpVideoId: 11, @@ -718,25 +718,25 @@ class Quiz16Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 16/29', - fallbackColor: const Color(0xFF2F9E94), - question: 'O seu filho/a usa ou usou chupeta com frequência?', + title: QuizUiStrings.quizProgress(16, 29), + fallbackColor: AppColors.teal, + question: QuizStrings.q16Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Usa ou usou chupeta com frequência', + title: QuizStrings.yes, + description: QuizStrings.q16YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não usa ou não usou chupeta com frequência', + title: QuizStrings.no, + description: QuizStrings.q16NoDescription, weight: 1, value: 'nao', ), QuizAnswer( - title: 'Não sei', - description: 'Não tenho a certeza', + title: QuizStrings.dontKnow, + description: QuizStrings.dontKnowDescription, weight: 1, value: 'nao_sei', helpVideoId: 12, @@ -762,25 +762,25 @@ class Quiz17Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 17/29', - fallbackColor: const Color(0xFFFF55A7), - question: 'O seu filho/a chucha ou já chuchou o dedo com frequência?', + title: QuizUiStrings.quizProgress(17, 29), + fallbackColor: AppColors.pink, + question: QuizStrings.q17Question, answers: const [ QuizAnswer( - title: 'Sim', - description: 'Chucha ou já chuchou o dedo com frequência', + title: QuizStrings.yes, + description: QuizStrings.q17YesDescription, weight: 2, value: 'sim', ), QuizAnswer( - title: 'Não', - description: 'Não chucha ou não chuchou o dedo com frequência', + title: QuizStrings.no, + description: QuizStrings.q17NoDescription, weight: 1, value: 'nao', ), QuizAnswer( - title: 'Não sei', - description: 'Não tenho a certeza', + title: QuizStrings.dontKnow, + description: QuizStrings.dontKnowDescription, weight: 1, value: 'nao_sei', helpVideoId: 13, @@ -814,23 +814,22 @@ class Quiz18Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 18/29', - fallbackColor: const Color(0xFFFF55A7), + title: QuizUiStrings.quizProgress(18, 29), + fallbackColor: AppColors.pink, answerImageAspectRatio: 1.5, - question: - 'Qual das seguintes imagens é mais parecida com a postura do seu filho/a?', + question: QuizStrings.q18Question, answers: const [ QuizAnswer( - title: 'Postura inadequada', - description: 'Postura curvada, ombros e pescoço projetados', + title: QuizStrings.q18Answer1Title, + description: QuizStrings.q18Answer1Description, weight: 2, hideTitle: true, value: 'postura_inadequada', imagePath: 'assets/mockup_images/0.1.png', ), QuizAnswer( - title: 'Postura correta', - description: 'Postura ereta, coluna alinhada', + title: QuizStrings.q18Answer2Title, + description: QuizStrings.q18Answer2Description, weight: 1, hideTitle: true, value: 'postura_correta', @@ -857,14 +856,13 @@ class Quiz19Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 19/29', - fallbackColor: const Color(0xFFFF55A7), - question: - 'Qual das seguintes imagens é mais parecida com o perfil do seu filho/a?', + title: QuizUiStrings.quizProgress(19, 29), + fallbackColor: AppColors.pink, + question: QuizStrings.q19Question, answers: [ QuizAnswer( - title: 'Perfil convexo', - description: 'Perfil facial convexo', + title: QuizStrings.q19Answer1Title, + description: QuizStrings.q19Answer1Description, weight: 2, hideTitle: true, value: 'perfil_convexo', @@ -875,8 +873,8 @@ class Quiz19Screen extends StatelessWidget { ), ), QuizAnswer( - title: 'Perfil reto', - description: 'Perfil facial reto', + title: QuizStrings.q19Answer2Title, + description: QuizStrings.q19Answer2Description, weight: 1, hideTitle: true, value: 'perfil_reto', @@ -887,8 +885,8 @@ class Quiz19Screen extends StatelessWidget { ), ), QuizAnswer( - title: 'Perfil côncavo', - description: 'Perfil facial côncavo', + title: QuizStrings.q19Answer3Title, + description: QuizStrings.q19Answer3Description, weight: 2, hideTitle: true, value: 'perfil_concavo', @@ -919,21 +917,21 @@ class Quiz20Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 20/29', - fallbackColor: const Color(0xFF2F9E94), - question: 'Qual é a posição da boca do seu filho/a habitualmente?', + title: QuizUiStrings.quizProgress(20, 29), + fallbackColor: AppColors.teal, + question: QuizStrings.q20Question, answers: [ const QuizAnswer( - title: 'Boca fechada', - description: 'Boca fechada habitualmente', + title: QuizStrings.q20Answer1Title, + description: QuizStrings.q20Answer1Description, weight: 1, hideTitle: true, value: 'boca_fechada', imagePath: 'assets/mockup_images/7.png', ), QuizAnswer( - title: 'Boca entreaberta', - description: 'Boca entreaberta habitualmente', + title: QuizStrings.q20Answer2Title, + description: QuizStrings.q20Answer2Description, weight: 2, hideTitle: true, value: 'boca_entreaberta', @@ -964,14 +962,13 @@ class Quiz21Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 21/29', - fallbackColor: const Color(0xFFFF55A7), - question: - 'Qual das imagens, na zona abaixo dos olhos, se assemelha mais ao seu filho/a?', + title: QuizUiStrings.quizProgress(21, 29), + fallbackColor: AppColors.pink, + question: QuizStrings.q21Question, answers: [ QuizAnswer( - title: 'Sem sinais', - description: 'Sem olheiras visíveis abaixo dos olhos', + title: QuizStrings.q21Answer1Title, + description: QuizStrings.q21Answer1Description, weight: 1, hideTitle: true, value: 'sem_olheiras', @@ -982,8 +979,8 @@ class Quiz21Screen extends StatelessWidget { ), ), QuizAnswer( - title: 'Sinal de risco', - description: 'Olheiras visíveis abaixo dos olhos', + title: QuizStrings.q21Answer2Title, + description: QuizStrings.q21Answer2Description, weight: 2, hideTitle: true, value: 'com_olheiras', @@ -1014,15 +1011,14 @@ class Quiz22Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 22/29', - fallbackColor: const Color(0xFFFF55A7), + title: QuizUiStrings.quizProgress(22, 29), + fallbackColor: AppColors.pink, isSignQuestion: true, - question: - 'Qual das imagens é mais parecida com o queixo do seu filho/a com a boca fechada?', + question: QuizStrings.q22Question, answers: [ QuizAnswer( - title: 'Queixo relaxado', - description: 'Queixo liso e relaxado com a boca fechada', + title: QuizStrings.q22Answer1Title, + description: QuizStrings.q22Answer1Description, weight: 1, hideTitle: true, value: 'queixo_correto', @@ -1034,8 +1030,8 @@ class Quiz22Screen extends StatelessWidget { ), ), const QuizAnswer( - title: 'Queixo tenso', - description: 'Queixo tenso/franzido com a boca fechada', + title: QuizStrings.q22Answer2Title, + description: QuizStrings.q22Answer2Description, weight: 2, hideTitle: true, value: 'queixo_tenso', @@ -1070,31 +1066,30 @@ class Quiz23Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 23/29', - fallbackColor: const Color(0xFF2F9E94), + title: QuizUiStrings.quizProgress(23, 29), + fallbackColor: AppColors.teal, isSignQuestion: true, - question: - 'Qual das seguintes imagens se assemelha à boca do seu filho/a?', + question: QuizStrings.q23Question, answers: const [ QuizAnswer( - title: 'Dentição sobreposta', - description: 'Dentes sobrepostos/tortos', + title: QuizStrings.q23Answer1Title, + description: QuizStrings.q23Answer1Description, weight: 2, hideTitle: true, value: 'dentes_sobrepostos', imagePath: 'assets/mockup_images/10.png', ), QuizAnswer( - title: 'Dentição alinhada', - description: 'Dentição bem alinhada, sem apinhamento', + title: QuizStrings.q23Answer2Title, + description: QuizStrings.q23Answer2Description, weight: 1, hideTitle: true, value: 'dentes_alinhados', imagePath: 'assets/mockup_images/11.png', ), QuizAnswer( - title: 'Dentição desalinhada', - description: 'Dentição desalinhada/apinhada', + title: QuizStrings.q23Answer3Title, + description: QuizStrings.q23Answer3Description, weight: 2, hideTitle: true, value: 'dentes_desalinhados', @@ -1121,31 +1116,30 @@ class Quiz24Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 24/29', - fallbackColor: const Color(0xFF2F9E94), + title: QuizUiStrings.quizProgress(24, 29), + fallbackColor: AppColors.teal, isSignQuestion: true, - question: - 'Qual das seguintes imagens se assemelha à boca do seu filho/a?', + question: QuizStrings.q24Question, answers: const [ QuizAnswer( - title: 'Apinhamento moderado', - description: 'Dentes rodados/desalinhados de forma mais visível', + title: QuizStrings.q24Answer1Title, + description: QuizStrings.q24Answer1Description, weight: 2, hideTitle: true, value: 'apinhamento_moderado', imagePath: 'assets/mockup_images/16.png', ), QuizAnswer( - title: 'Ligeiro desalinhamento', - description: 'Pequeno desalinhamento ou espaço entre alguns dentes', + title: QuizStrings.q24Answer2Title, + description: QuizStrings.q24Answer2Description, weight: 1, hideTitle: true, value: 'apinhamento_leve', imagePath: 'assets/mockup_images/15.png', ), QuizAnswer( - title: 'Apinhamento acentuado', - description: 'Dentes muito sobrepostos entre si', + title: QuizStrings.q24Answer3Title, + description: QuizStrings.q24Answer3Description, weight: 2, hideTitle: true, value: 'apinhamento_acentuado', @@ -1172,22 +1166,21 @@ class Quiz25Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 25/29', - fallbackColor: const Color(0xFFFF55A7), - question: - 'Qual das seguintes imagens se assemelha ao freio labial do seu filho/a?', + title: QuizUiStrings.quizProgress(25, 29), + fallbackColor: AppColors.pink, + question: QuizStrings.q25Question, answers: const [ QuizAnswer( - title: 'Freio labial correto', - description: 'Inserção do freio labial mais alta', + title: QuizStrings.q25Answer1Title, + description: QuizStrings.q25Answer1Description, weight: 1, hideTitle: true, value: 'freio_labial_correto', imagePath: 'assets/mockup_images/20.png', ), QuizAnswer( - title: 'Freio labial inadequado', - description: 'Inserção do freio labial baixa, entre os dentes', + title: QuizStrings.q25Answer2Title, + description: QuizStrings.q25Answer2Description, weight: 2, hideTitle: true, value: 'freio_labial_inadequado', @@ -1214,22 +1207,21 @@ class Quiz26Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 26/29', - fallbackColor: const Color(0xFFFF55A7), - question: - 'Qual das seguintes imagens se assemelha ao freio lingual do seu filho/a?', + title: QuizUiStrings.quizProgress(26, 29), + fallbackColor: AppColors.pink, + question: QuizStrings.q26Question, answers: const [ QuizAnswer( - title: 'Freio lingual inadequado', - description: 'Freio lingual curto/apertado (língua em coração)', + title: QuizStrings.q26Answer1Title, + description: QuizStrings.q26Answer1Description, weight: 2, hideTitle: true, value: 'freio_lingual_inadequado', imagePath: 'assets/mockup_images/17.png', ), QuizAnswer( - title: 'Freio lingual correto', - description: 'Língua move-se livremente, sem restrição visível', + title: QuizStrings.q26Answer2Title, + description: QuizStrings.q26Answer2Description, weight: 1, hideTitle: true, value: 'freio_lingual_correto', @@ -1256,31 +1248,30 @@ class Quiz27Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 27/29', - fallbackColor: const Color(0xFF2F9E94), + title: QuizUiStrings.quizProgress(27, 29), + fallbackColor: AppColors.teal, isSignQuestion: true, - question: - 'Qual das seguintes imagens se assemelha com a boca do seu filho/a?', + question: QuizStrings.q27Question, answers: const [ QuizAnswer( - title: 'Dentição desalinhada', - description: 'Dentição desalinhada/apinhada', + title: QuizStrings.q27Answer1Title, + description: QuizStrings.q27Answer1Description, weight: 2, hideTitle: true, value: 'dentes_desalinhados_3', imagePath: 'assets/mockup_images/23.JPEG', ), QuizAnswer( - title: 'Dentição sobreposta', - description: 'Dentes sobrepostos/tortos', + title: QuizStrings.q27Answer2Title, + description: QuizStrings.q27Answer2Description, weight: 2, hideTitle: true, value: 'dentes_sobrepostos_3', imagePath: 'assets/mockup_images/24.png', ), QuizAnswer( - title: 'Dentição alinhada', - description: 'Dentição bem alinhada, sem apinhamento', + title: QuizStrings.q27Answer3Title, + description: QuizStrings.q27Answer3Description, weight: 1, hideTitle: true, value: 'dentes_alinhados_3', @@ -1307,23 +1298,22 @@ class Quiz28Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 28/29', - fallbackColor: const Color(0xFF2F9E94), + title: QuizUiStrings.quizProgress(28, 29), + fallbackColor: AppColors.teal, isSignQuestion: true, - question: - 'Qual das seguintes imagens se assemelha à boca do seu filho/a?', + question: QuizStrings.q28Question, answers: const [ QuizAnswer( - title: 'Dentição alinhada', - description: 'Dentição bem alinhada, sem apinhamento', + title: QuizStrings.q28Answer1Title, + description: QuizStrings.q28Answer1Description, weight: 1, hideTitle: true, value: 'dentes_alinhados_2', imagePath: 'assets/mockup_images/29.jpeg', ), QuizAnswer( - title: 'Dentição desalinhada', - description: 'Dentes sobrepostos/apinhados', + title: QuizStrings.q28Answer2Title, + description: QuizStrings.q28Answer2Description, weight: 2, hideTitle: true, value: 'dentes_desalinhados_2', @@ -1350,24 +1340,23 @@ class Quiz29Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 29/29', - fallbackColor: const Color(0xFFFF55A7), + title: QuizUiStrings.quizProgress(29, 29), + fallbackColor: AppColors.pink, isSignQuestion: true, - correctBadgeLabel: 'Posição saudável', - question: - 'Qual das seguintes imagens se assemelha ao céu da boca do seu filho/a?', + correctBadgeLabel: QuizStrings.q29CorrectBadgeLabel, + question: QuizStrings.q29Question, answers: const [ QuizAnswer( - title: 'Posição profunda incorreta', - description: 'Palato estreito/profundo, em forma de V', + title: QuizStrings.q29Answer1Title, + description: QuizStrings.q29Answer1Description, weight: 2, hideTitle: true, value: 'palato_v', imagePath: 'assets/mockup_images/26.png', ), QuizAnswer( - title: 'Posição em U saudável', - description: 'Palato largo, em forma de U', + title: QuizStrings.q29Answer2Title, + description: QuizStrings.q29Answer2Description, weight: 1, hideTitle: true, value: 'palato_u', diff --git a/lib/quiz/quiz_checklist_screen.dart b/lib/quiz/quiz_checklist_screen.dart index 299dd10..73fa06d 100644 --- a/lib/quiz/quiz_checklist_screen.dart +++ b/lib/quiz/quiz_checklist_screen.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; +import '../strings/quiz_ui_strings.dart'; import '../widgets/entrance.dart'; import '../widgets/liquid_waves_background.dart'; import '../widgets/tap_bounce.dart'; -const Color _pink = Color(0xFFFF55A7); -const Color _teal = Color(0xFF2F9E94); +const Color _pink = AppColors.pink; +const Color _teal = AppColors.teal; /// Ecrã intersticial informativo, mostrado a meio do quiz para preparar o /// utilizador antes de continuar (ex.: logo a seguir a um vídeo-guia) — @@ -30,7 +32,7 @@ class QuizChecklistScreen extends StatelessWidget { body: Stack( clipBehavior: Clip.none, children: [ - Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), + Positioned.fill(child: Container(color: AppColors.background)), const LiquidWavesBackground(), SafeArea( child: Padding( @@ -129,7 +131,7 @@ class QuizChecklistScreen extends StatelessWidget { ), ), onPressed: () => onAdvance(context), - child: const Text('Avançar'), + child: const Text(QuizUiStrings.advance), ), ), ), diff --git a/lib/quiz/quiz_question_screen.dart b/lib/quiz/quiz_question_screen.dart index 2152719..ae17563 100644 --- a/lib/quiz/quiz_question_screen.dart +++ b/lib/quiz/quiz_question_screen.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; import 'package:flutter/services.dart'; import '../screens/video_screen.dart'; +import '../strings/quiz_ui_strings.dart'; import '../widgets/entrance.dart'; import '../widgets/liquid_waves_background.dart'; import '../widgets/tap_bounce.dart'; @@ -82,7 +84,7 @@ class QuizQuestionScreen extends StatefulWidget { this.suggestedVideoTitle, this.fallbackColor, this.answerImageAspectRatio, - this.correctBadgeLabel = 'Posição certa', + this.correctBadgeLabel = QuizUiStrings.defaultCorrectBadgeLabel, }); final String title; @@ -197,7 +199,7 @@ class _QuizQuestionScreenState extends State { body: Stack( clipBehavior: Clip.none, children: [ - Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), + Positioned.fill(child: Container(color: AppColors.background)), const LiquidWavesBackground(), SafeArea( child: Column( @@ -222,7 +224,7 @@ class _QuizQuestionScreenState extends State { padding: EdgeInsets.all(9), child: Icon( Icons.arrow_back_rounded, - color: Color(0xFF2F9E94), + color: AppColors.teal, size: 20, ), ), @@ -241,11 +243,11 @@ class _QuizQuestionScreenState extends State { 1.0, ), minHeight: 8, - backgroundColor: const Color( - 0xFFFF55A7, - ).withValues(alpha: 0.15), + backgroundColor: AppColors.pink.withValues( + alpha: 0.15, + ), valueColor: const AlwaysStoppedAnimation( - Color(0xFFFF55A7), + AppColors.pink, ), ), ), @@ -304,7 +306,7 @@ class _QuizQuestionScreenState extends State { : _FallbackIconBlock( color: widget.fallbackColor ?? - const Color(0xFF2F9E94), + AppColors.teal, ), const SizedBox(height: 10), if (hasSuggestedVideo) ...[ @@ -317,7 +319,8 @@ class _QuizQuestionScreenState extends State { title: widget .suggestedVideoTitle ?? - 'Vídeo', + QuizUiStrings + .videoFallbackTitle, description: '', videoPath: widget .suggestedVideoPath, @@ -328,13 +331,13 @@ class _QuizQuestionScreenState extends State { icon: const Icon( Icons .play_circle_outline_rounded, - color: Color(0xFF2F9E94), + color: AppColors.teal, ), label: Text( widget.suggestedVideoTitle ?? - 'Ver vídeo (opcional)', + QuizUiStrings.watchOptionalVideo, style: const TextStyle( - color: Color(0xFF2F9E94), + color: AppColors.teal, fontWeight: FontWeight.w800, ), ), @@ -347,7 +350,7 @@ class _QuizQuestionScreenState extends State { style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w900, - color: Color(0xFFFF55A7), + color: AppColors.pink, height: 1.2, ), ), @@ -355,11 +358,11 @@ class _QuizQuestionScreenState extends State { Text( widget.answerType == QuizAnswerType.number - ? 'Insira o número' + ? QuizUiStrings.enterNumber : widget.answerType == QuizAnswerType.yesNo - ? 'Escolha uma opção' - : 'Escolha apenas uma opção', + ? QuizUiStrings.chooseOneOption + : QuizUiStrings.chooseOnlyOneOption, textAlign: TextAlign.center, style: TextStyle( color: Colors.black.withValues( @@ -495,9 +498,7 @@ class _QuizQuestionScreenState extends State { style: FilledButton.styleFrom( backgroundColor: - const Color( - 0xFFFF55A7, - ), + AppColors.pink, foregroundColor: Colors.white, shape: @@ -628,8 +629,8 @@ class _QuizQuestionScreenState extends State { }, child: Text( widget.isFinal - ? 'Concluir' - : 'Avançar', + ? QuizUiStrings.finish + : QuizUiStrings.advance, ), ), ), @@ -643,9 +644,8 @@ class _QuizQuestionScreenState extends State { ), TextButton( style: TextButton.styleFrom( - foregroundColor: const Color( - 0xFF2F9E94, - ), + foregroundColor: + AppColors.teal, textStyle: const TextStyle( fontWeight: FontWeight.w800, ), @@ -663,7 +663,7 @@ class _QuizQuestionScreenState extends State { (route) => route.isFirst, ), child: const Text( - 'Voltar para homepage', + QuizUiStrings.backToHomepage, ), ), ], @@ -724,7 +724,7 @@ class _QuizQuestionScreenState extends State { style: const TextStyle( fontSize: 24, fontWeight: FontWeight.w900, - color: Color(0xFF2F9E94), + color: AppColors.teal, ), decoration: const InputDecoration( border: InputBorder.none, @@ -750,10 +750,10 @@ class _QuizQuestionScreenState extends State { !_numberDontKnow) ...[ const SizedBox(height: 10), Text( - 'Um número tão alto assim não é possível.\nO máximo é $_maxTeethCount.', + QuizUiStrings.teethCountTooHigh(_maxTeethCount), textAlign: TextAlign.center, style: const TextStyle( - color: Color(0xFFFF55A7), + color: AppColors.pink, fontWeight: FontWeight.w700, fontSize: 12, ), @@ -780,12 +780,12 @@ class _QuizQuestionScreenState extends State { ), decoration: BoxDecoration( color: _numberDontKnow - ? const Color(0xFF2F9E94) + ? AppColors.teal : Colors.white.withValues(alpha: 0.70), borderRadius: BorderRadius.circular(999), border: Border.all( color: _numberDontKnow - ? const Color(0xFF2F9E94) + ? AppColors.teal : Colors.black.withValues(alpha: 0.12), ), ), @@ -801,7 +801,7 @@ class _QuizQuestionScreenState extends State { ), const SizedBox(width: 6), Text( - 'Não sei', + QuizUiStrings.dontKnow, style: TextStyle( fontWeight: FontWeight.w800, fontSize: 13, @@ -828,7 +828,7 @@ class _QuizAnswerTile extends StatelessWidget { required this.onTap, this.imageAspectRatio = 4 / 3, this.reveal = false, - this.correctLabel = 'Posição certa', + this.correctLabel = QuizUiStrings.defaultCorrectBadgeLabel, }); final QuizAnswer answer; @@ -848,9 +848,9 @@ class _QuizAnswerTile extends StatelessWidget { Widget build(BuildContext context) { final bool isCorrect = answer.weight == 1; final borderColor = reveal - ? (isCorrect ? const Color(0xFF2F9E94) : const Color(0xFFFF55A7)) + ? (isCorrect ? AppColors.teal : AppColors.pink) : selected - ? const Color(0xFF2F9E94) + ? AppColors.teal : Colors.black.withValues(alpha: 0.12); final bg = selected ? Colors.white.withValues(alpha: 0.88) @@ -934,7 +934,7 @@ class _QuizAnswerTile extends StatelessWidget { style: const TextStyle( fontWeight: FontWeight.w900, fontSize: 15, - color: Color(0xFF2F9E94), + color: AppColors.teal, ), ), ], @@ -955,7 +955,7 @@ class _QuizAnswerTile extends StatelessWidget { width: 22, height: 22, decoration: const BoxDecoration( - color: Color(0xFF2F9E94), + color: AppColors.teal, shape: BoxShape.circle, ), child: const Icon( @@ -981,8 +981,8 @@ class _QuizAnswerTile extends StatelessWidget { ), decoration: BoxDecoration( color: isCorrect - ? const Color(0xFF2F9E94) - : const Color(0xFFFF55A7), + ? AppColors.teal + : AppColors.pink, borderRadius: BorderRadius.circular(999), boxShadow: [ BoxShadow( @@ -1004,7 +1004,7 @@ class _QuizAnswerTile extends StatelessWidget { ), const SizedBox(width: 4), Text( - isCorrect ? correctLabel : 'Posição inadequada', + isCorrect ? correctLabel : QuizUiStrings.incorrectBadgeLabel, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w800, @@ -1057,7 +1057,7 @@ class _QuestionReferenceImages extends StatelessWidget { borderRadius: BorderRadius.circular(999), ), child: const Text( - 'Imagem de referência', + QuizUiStrings.referenceImage, style: TextStyle( color: Colors.white, fontWeight: FontWeight.w700, @@ -1159,12 +1159,12 @@ class _QuizAnswerPill extends StatelessWidget { @override Widget build(BuildContext context) { final accent = _isYes - ? const Color(0xFF2F9E94) + ? AppColors.teal : _isNo - ? const Color(0xFFFF55A7) + ? AppColors.pink : Colors.black.withValues(alpha: 0.35); final borderColor = selected - ? const Color(0xFF2F9E94) + ? AppColors.teal : Colors.black.withValues(alpha: 0.10); final helpVideo = _helpVideo; final showHelp = selected && helpVideo != null; @@ -1228,7 +1228,7 @@ class _QuizAnswerPill extends StatelessWidget { style: const TextStyle( fontWeight: FontWeight.w800, fontSize: 15, - color: Color(0xFF2F9E94), + color: AppColors.teal, ), ), ), @@ -1239,11 +1239,11 @@ class _QuizAnswerPill extends StatelessWidget { decoration: BoxDecoration( shape: BoxShape.circle, color: selected - ? const Color(0xFF2F9E94) + ? AppColors.teal : Colors.transparent, border: Border.all( color: selected - ? const Color(0xFF2F9E94) + ? AppColors.teal : Colors.black.withValues(alpha: 0.25), width: 1.6, ), @@ -1291,7 +1291,7 @@ class _HelpVideoButton extends StatelessWidget { return TapBounce( scale: 0.97, child: Material( - color: const Color(0xFF2F9E94).withValues(alpha: 0.10), + color: AppColors.teal.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(14), child: InkWell( borderRadius: BorderRadius.circular(14), @@ -1302,23 +1302,23 @@ class _HelpVideoButton extends StatelessWidget { children: [ const Icon( Icons.play_circle_fill_rounded, - color: Color(0xFF2F9E94), + color: AppColors.teal, size: 22, ), const SizedBox(width: 10), Expanded( child: Text( - 'Não tem a certeza? Veja o "${video.title}" para ajudar a responder', + QuizUiStrings.notSureSeeVideo(video.title), style: const TextStyle( fontWeight: FontWeight.w700, fontSize: 12.5, - color: Color(0xFF2F9E94), + color: AppColors.teal, ), ), ), const Icon( Icons.chevron_right_rounded, - color: Color(0xFF2F9E94), + color: AppColors.teal, size: 20, ), ], diff --git a/lib/quiz/quiz_result.dart b/lib/quiz/quiz_result.dart index 5177311..c357717 100644 --- a/lib/quiz/quiz_result.dart +++ b/lib/quiz/quiz_result.dart @@ -1,9 +1,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; import '../main.dart' show supabase; -import '../widgets/app_gradients.dart'; +import '../colors/app_gradients.dart'; +import '../strings/quiz_result_strings.dart'; +import '../strings/quiz_ui_strings.dart'; import '../widgets/entrance.dart'; import '../widgets/liquid_waves_background.dart'; import '../widgets/tap_bounce.dart'; @@ -106,20 +109,8 @@ class _QuizResultScreenState extends State { MaterialPageRoute( builder: (_) => QuizVideoGuideScreen( youtubeId: _resultGuideYoutubeId, - heading: 'Visualize o seguinte vídeo', - caption: - 'Assista para compreender a importância de um diagnóstico ' - 'precoce e de realizar tratamento ortodôntico intercetivo, ' - 'ou seja, uma intervenção realizada na infância, tipicamente ' - 'durante a fase de dentição mista (presença tanto de dentes ' - 'de leite como de permanentes).\n\nO seu propósito é ' - 'identificar, prevenir ou reduzir a gravidade de maloclusões ' - 'e problemas no desenvolvimento dos maxilares enquanto os ' - 'ossos e dentes ainda estão em crescimento.\n\nEsta ' - 'intervenção aproveita a fase de desenvolvimento craniofacial ' - 'para influenciar o crescimento ósseo, redirecionar a ' - 'mandíbula, corrigir o alinhamento dos maxilares ou criar ' - 'espaço para a erupção correta dos dentes permanentes.', + heading: QuizResultStrings.watchTheFollowingVideo, + caption: QuizResultStrings.resultVideoCaption, onAdvance: (context) => Navigator.of(context).popUntil((r) => r.isFirst), ), @@ -137,7 +128,7 @@ class _QuizResultScreenState extends State { body: Stack( clipBehavior: Clip.none, children: [ - Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), + Positioned.fill(child: Container(color: AppColors.background)), const LiquidWavesBackground(), SafeArea( child: Center( @@ -157,12 +148,12 @@ class _QuizResultScreenState extends State { const SizedBox(height: 10), FadeSlideIn( child: const Text( - 'O resultado avaliado é de:', + QuizResultStrings.resultHeading, textAlign: TextAlign.center, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w900, - color: Color(0xFFFF55A7), + color: AppColors.pink, height: 1.2, ), ), @@ -177,15 +168,16 @@ class _QuizResultScreenState extends State { _ResultRing( value: signs, max: kSignsMax, - color: const Color(0xFF2F9E94), + color: AppColors.teal, label: - 'Sinais de má\noclusão instalados', + QuizResultStrings.signsRingLabel, ), _ResultRing( value: factors, max: kFactorsMax, - color: const Color(0xFFFF55A7), - label: 'Fatores de\nrisco associados', + color: AppColors.pink, + label: + QuizResultStrings.factorsRingLabel, ), ], ), @@ -194,14 +186,15 @@ class _QuizResultScreenState extends State { FadeSlideIn( delay: const Duration(milliseconds: 100), child: _ResultSection( - heading: 'Conclusões:', - body: - 'Foram identificados $signs de $kSignsMax ' - 'sinais de má oclusão já instalada e ' - '$factors de $kFactorsMax fatores de ' - 'risco frequentemente associados a ' - 'má oclusão.', - bodyColor: const Color(0xFFFF55A7), + heading: + QuizResultStrings.conclusionsHeading, + body: QuizResultStrings.conclusionsBody( + signs: signs, + signsMax: kSignsMax, + factors: factors, + factorsMax: kFactorsMax, + ), + bodyColor: AppColors.pink, bodyWeight: FontWeight.w800, ), ), @@ -209,19 +202,23 @@ class _QuizResultScreenState extends State { FadeSlideIn( delay: const Duration(milliseconds: 140), child: _ResultSection( - heading: 'O que deve fazer agora', + heading: + QuizResultStrings.whatToDoNowHeading, body: recommend - ? 'Com base neste resultado, recomendamos a marcação de uma consulta com Odontopediatra ou Ortodontista para uma avaliação presencial.' - : 'Não foram encontrados sinais ou fatores de risco relevantes. Continue a acompanhar a saúde oral do seu filho/a com as consultas de rotina habituais.', + ? QuizResultStrings + .whatToDoNowRecommend + : QuizResultStrings + .whatToDoNowNoConcerns, ), ), const SizedBox(height: 16), FadeSlideIn( delay: const Duration(milliseconds: 180), child: _ResultSection( - heading: 'Importante saber', - body: - 'Este resultado não substitui uma avaliação presencial, nem tem função de diagnóstico. O objetivo é ajudá-lo a estar mais atento aos sinais identificados e a não adiar uma consulta que pode fazer a diferença no desenvolvimento oral do seu filho/a.', + heading: QuizResultStrings + .importantToKnowHeading, + body: QuizResultStrings + .importantToKnowBody, ), ), ], @@ -254,7 +251,7 @@ class _QuizResultScreenState extends State { if (!context.mounted) return; _goToVideo(); }, - child: const Text('Avançar'), + child: const Text(QuizUiStrings.advance), ), ), ), diff --git a/lib/quiz/quiz_video_guide.dart b/lib/quiz/quiz_video_guide.dart index 32aad7a..34515ca 100644 --- a/lib/quiz/quiz_video_guide.dart +++ b/lib/quiz/quiz_video_guide.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; import 'package:youtube_player_flutter/youtube_player_flutter.dart'; +import '../strings/quiz_ui_strings.dart'; import '../widgets/entrance.dart'; import '../widgets/liquid_waves_background.dart'; import '../widgets/tap_bounce.dart'; -const Color _pink = Color(0xFFFF55A7); -const Color _teal = Color(0xFF2F9E94); +const Color _pink = AppColors.pink; +const Color _teal = AppColors.teal; /// Player do YouTube incorporado (sem AppBar/tela cheia própria) — usado /// tanto no ecrã intersticial [QuizVideoGuideScreen] como embutido @@ -78,9 +80,9 @@ class QuizVideoGuideScreen extends StatefulWidget { super.key, required this.youtubeId, required this.onAdvance, - this.heading = 'Antes de continuar, veja o vídeo educativo...', + this.heading = QuizUiStrings.defaultVideoGuideHeading, this.caption = - 'Veja o vídeo para compreender melhor as próximas perguntas do questionário.', + QuizUiStrings.defaultVideoGuideCaption, this.extraHeading, this.extraSubtitle, }); @@ -122,7 +124,7 @@ class _QuizVideoGuideScreenState extends State { body: Stack( clipBehavior: Clip.none, children: [ - Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), + Positioned.fill(child: Container(color: AppColors.background)), const LiquidWavesBackground(), SafeArea( child: Padding( @@ -135,7 +137,7 @@ class _QuizVideoGuideScreenState extends State { child: TextButton( onPressed: _advance, child: const Text( - 'Pular', + QuizUiStrings.skip, style: TextStyle( color: Colors.black45, fontWeight: FontWeight.w700, @@ -243,7 +245,7 @@ class _QuizVideoGuideScreenState extends State { ), ), onPressed: _videoWatched ? _advance : null, - child: const Text('Avançar'), + child: const Text(QuizUiStrings.advance), ), ), ), diff --git a/lib/screens/credits_screen.dart b/lib/screens/credits_screen.dart index 67f64d0..8322f75 100644 --- a/lib/screens/credits_screen.dart +++ b/lib/screens/credits_screen.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; -import '../widgets/app_gradients.dart'; +import '../colors/app_gradients.dart'; +import '../strings/credits_strings.dart'; import '../widgets/entrance.dart'; -const Color _pink = Color(0xFFFF55A7); -const Color _teal = Color(0xFF2F9E94); +const Color _pink = AppColors.pink; +const Color _teal = AppColors.teal; class _CreditPerson { const _CreditPerson(this.name, this.role, {this.logoPath}); @@ -25,21 +27,27 @@ class _CreditSection { } const List<_CreditSection> _kCreditSections = [ - _CreditSection('Criadora original', [ - _CreditPerson('Francisca Salgado Ferreira Pacheco Silva', 'Criadora original'), + _CreditSection(CreditsStrings.originalCreatorSection, [ + _CreditPerson( + 'Francisca Salgado Ferreira Pacheco Silva', + CreditsStrings.originalCreatorRole, + ), ]), - _CreditSection('Desenvolvimento informático', [ - _CreditPerson('Carlos Correia', 'Desenvolvedor'), - _CreditPerson('Fábio Ceia', 'Desenvolvedor'), - _CreditPerson('Ruben Grandra', 'Desenvolvedor'), - _CreditPerson('Dinis Maria', 'Desenvolvedor'), + _CreditSection(CreditsStrings.developmentSection, [ + _CreditPerson('Carlos Correia', CreditsStrings.developerRole), + _CreditPerson('Fábio Ceia', CreditsStrings.developerRole), + _CreditPerson('Ruben Grandra', CreditsStrings.developerRole), + _CreditPerson('Dinis Maria', CreditsStrings.developerRole), ]), - _CreditSection('Orientadores', [ - _CreditPerson('Augusta Pureza Alves Silveira', 'Orientador(a)'), - _CreditPerson('Cristina Lopes Cardoso Silva', 'Orientador(a)'), - _CreditPerson('João Carlos Rodrigues L. Miranda', 'Orientador(a)'), + _CreditSection(CreditsStrings.advisorsSection, [ + _CreditPerson('Augusta Pureza Alves Silveira', CreditsStrings.advisorRole), + _CreditPerson('Cristina Lopes Cardoso Silva', CreditsStrings.advisorRole), + _CreditPerson( + 'João Carlos Rodrigues L. Miranda', + CreditsStrings.advisorRole, + ), ]), - _CreditSection('Instituição colaboradora', [ + _CreditSection(CreditsStrings.institutionSection, [ _CreditPerson('Universidade Fernando Pessoa', ''), _CreditPerson( 'Escola Profissional de Vila do Conde', @@ -66,14 +74,14 @@ class CreditsScreen extends StatelessWidget { elevation: 0, scrolledUnderElevation: 0, title: const Text( - 'Criadores e Colaboradores', + CreditsStrings.pageTitle, style: TextStyle(fontWeight: FontWeight.w900), ), ), ), ), body: Container( - color: const Color(0xFFFAFAF7), + color: AppColors.background, child: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 24, 20, 24), @@ -97,7 +105,7 @@ class CreditsScreen extends StatelessWidget { ), const SizedBox(height: 16), const Text( - 'Quem fez a Check-Teeth Kids', + CreditsStrings.header, textAlign: TextAlign.center, style: TextStyle( fontSize: 20, diff --git a/lib/screens/curiosidade_screen.dart b/lib/screens/curiosidade_screen.dart index 1c6a970..a21dc84 100644 --- a/lib/screens/curiosidade_screen.dart +++ b/lib/screens/curiosidade_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; -import '../widgets/app_gradients.dart'; +import '../colors/app_gradients.dart'; +import '../strings/curiosidade_strings.dart'; import '../widgets/entrance.dart'; import '../widgets/liquid_waves_background.dart'; import '../widgets/tap_bounce.dart'; @@ -22,7 +24,7 @@ class CuriosidadeScreen extends StatelessWidget { elevation: 0, scrolledUnderElevation: 0, title: const Text( - 'Curiosidades', + CuriosidadeStrings.pageTitle, style: TextStyle(fontWeight: FontWeight.w900), ), ), @@ -31,7 +33,7 @@ class CuriosidadeScreen extends StatelessWidget { body: Stack( clipBehavior: Clip.none, children: [ - Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), + Positioned.fill(child: Container(color: AppColors.background)), const LiquidWavesBackground(), SafeArea( child: Align( @@ -45,9 +47,8 @@ class CuriosidadeScreen extends StatelessWidget { child: TapBounce( scale: 0.97, child: _CuriosityTopicTile( - title: 'Tema X', - description: - 'Aprenda dicas rápidas e simples para cuidar dos dentes no dia a dia.', + title: CuriosidadeStrings.topicXTitle, + description: CuriosidadeStrings.topicXDescription, ), ), ), @@ -57,8 +58,8 @@ class CuriosidadeScreen extends StatelessWidget { child: const TapBounce( scale: 0.97, child: _CuriosityTopicTile( - title: 'Tema Y', - description: 'Conteúdo em breve.', + title: CuriosidadeStrings.topicYTitle, + description: CuriosidadeStrings.contentComingSoon, ), ), ), @@ -68,8 +69,8 @@ class CuriosidadeScreen extends StatelessWidget { child: const TapBounce( scale: 0.97, child: _CuriosityTopicTile( - title: 'Tema Z', - description: 'Conteúdo em breve.', + title: CuriosidadeStrings.topicZTitle, + description: CuriosidadeStrings.contentComingSoon, ), ), ), @@ -79,8 +80,8 @@ class CuriosidadeScreen extends StatelessWidget { child: const TapBounce( scale: 0.97, child: _CuriosityTopicTile( - title: 'Tema U', - description: 'Conteúdo em breve.', + title: CuriosidadeStrings.topicUTitle, + description: CuriosidadeStrings.contentComingSoon, ), ), ), @@ -114,7 +115,7 @@ class _CuriosityTopicTile extends StatelessWidget { showModalBottomSheet( context: context, showDragHandle: true, - backgroundColor: const Color(0xFFFFE6F1), + backgroundColor: AppColors.pinkBackground, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), @@ -132,7 +133,7 @@ class _CuriosityTopicTile extends StatelessWidget { style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w900, - color: Color(0xFFFF55A7), + color: AppColors.pink, ), ), const SizedBox(height: 10), @@ -174,7 +175,7 @@ class _CuriosityTopicTile extends StatelessWidget { ), ), onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Fechar'), + child: const Text(CuriosidadeStrings.close), ), ), ), @@ -195,12 +196,12 @@ class _CuriosityTopicTile extends StatelessWidget { width: 36, height: 36, decoration: BoxDecoration( - color: const Color(0xFFFF55A7).withValues(alpha: 0.12), + color: AppColors.pink.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.lightbulb_rounded, - color: Color(0xFFFF55A7), + color: AppColors.pink, ), ), const SizedBox(width: 12), @@ -209,7 +210,7 @@ class _CuriosityTopicTile extends StatelessWidget { title, style: const TextStyle( fontWeight: FontWeight.w900, - color: Color(0xFF2F9E94), + color: AppColors.teal, ), ), ), diff --git a/lib/screens/hello_splash_screen.dart b/lib/screens/hello_splash_screen.dart index e787e60..3e49939 100644 --- a/lib/screens/hello_splash_screen.dart +++ b/lib/screens/hello_splash_screen.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; +import '../strings/splash_strings.dart'; class HelloSplashScreen extends StatefulWidget { const HelloSplashScreen({super.key, required this.onFinished, this.duration = const Duration(seconds: 5)}); @@ -76,7 +78,7 @@ class _HelloSplashScreenState extends State with TickerProvid child: Container( width: size.width, height: size.height, - color: const Color(0xFFFAFAF7), + color: AppColors.background, child: SafeArea( child: Center( child: Column( @@ -85,12 +87,12 @@ class _HelloSplashScreenState extends State with TickerProvid ScaleTransition( scale: _pop, child: const Text( - 'Olá', + SplashStrings.greeting, textAlign: TextAlign.center, style: TextStyle( fontSize: 64, fontWeight: FontWeight.w900, - color: Color(0xFFFF9AD0), + color: AppColors.pinkLight, height: 1.0, ), ), diff --git a/lib/screens/privacy_gate_screen.dart b/lib/screens/privacy_gate_screen.dart index 213eff7..8097f36 100644 --- a/lib/screens/privacy_gate_screen.dart +++ b/lib/screens/privacy_gate_screen.dart @@ -1,9 +1,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; import '../main.dart' show supabase; import '../privacy_gate_prefs.dart'; +import '../strings/privacy_strings.dart'; import '../terms_gate_prefs.dart'; import '../widgets/entrance.dart'; import '../widgets/liquid_waves_background.dart'; @@ -87,7 +89,7 @@ class _PrivacyGateScreenState extends State { body: Stack( clipBehavior: Clip.none, children: [ - Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), + Positioned.fill(child: Container(color: AppColors.background)), const LiquidWavesBackground(), SafeArea( child: Column( @@ -129,7 +131,7 @@ class _PrivacyGateScreenState extends State { child: TextButton( onPressed: _declining ? null : _acceptAll, child: const Text( - 'Aceitar tudo', + PrivacyStrings.acceptAll, style: TextStyle( fontWeight: FontWeight.w800, fontSize: 15, @@ -250,8 +252,8 @@ class _PrivacyConsentRow extends StatelessWidget { children: [ TextSpan( text: item.required - ? '(Obrigatório) ' - : '(Opcional) ', + ? PrivacyStrings.requiredPrefix + : PrivacyStrings.optionalPrefix, style: TextStyle( fontWeight: FontWeight.w900, color: item.required @@ -302,7 +304,7 @@ class _AdvanceButton extends StatelessWidget { ), ), onPressed: enabled ? onPressed : null, - child: const Text('Avançar'), + child: const Text(PrivacyStrings.advance), ), ), ); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index b801798..abd97d7 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; import '../main.dart' show supabase; +import '../strings/settings_strings.dart'; import '../widgets/app_dialogs.dart'; import '../widgets/entrance.dart'; import '../widgets/pill_snackbar.dart'; @@ -8,8 +10,8 @@ import '../widgets/tap_bounce.dart'; import 'credits_screen.dart'; import 'terms_screen.dart'; -const Color _teal = Color(0xFF2F9E94); -const Color _accentPink = Color(0xFFFF55A7); +const Color _teal = AppColors.teal; +const Color _accentPink = AppColors.pink; /// Conteúdo da aba de Configurações, para ser embutido na bottom navigation /// do LoggedHomeScreen (sem Scaffold/AppBar próprios). @@ -32,13 +34,9 @@ class _SettingsBodyState extends State { Future _confirmDeleteAccountData() async { final confirmed = await showConfirmDialog( context, - title: 'Apagar dados da conta', - message: - 'Isto remove permanentemente a sua conta, perfil, crianças ' - 'registadas e fotos — incluindo o login, permitindo criar uma ' - 'nova conta com o mesmo e-mail depois. Esta ação não pode ser ' - 'desfeita. Pretende continuar?', - confirmLabel: 'Apagar', + title: SettingsStrings.deleteAccountData, + message: SettingsStrings.deleteAccountMessage, + confirmLabel: SettingsStrings.delete, confirmColor: _accentPink, ); @@ -56,7 +54,8 @@ class _SettingsBodyState extends State { final errorMessage = (data is Map) ? data['error']?.toString() : null; if (response.status != 200 || errorMessage != null) { throw StateError( - errorMessage ?? 'Erro ao apagar conta (status ${response.status})', + errorMessage ?? + SettingsStrings.errorDeletingAccount(response.status), ); } @@ -64,7 +63,7 @@ class _SettingsBodyState extends State { if (!mounted) return; Navigator.of(context).popUntil((route) => route.isFirst); } catch (e) { - if (mounted) showPillSnackBar(context, 'Erro ao apagar: $e'); + if (mounted) showPillSnackBar(context, SettingsStrings.errorDeleting(e)); } finally { if (mounted) setState(() => _deletingAccount = false); } @@ -83,18 +82,18 @@ class _SettingsBodyState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _SectionLabel('Conta'), + _SectionLabel(SettingsStrings.account), _SettingsCard( children: [ _InfoTile( icon: Icons.person_outline_rounded, - title: name.isEmpty ? 'Sem nome' : name, + title: name.isEmpty ? SettingsStrings.noName : name, subtitle: email, ), const Divider(height: 1), _ActionTile( icon: Icons.logout_rounded, - title: 'Sair', + title: SettingsStrings.signOut, onTap: _signOut, ), ], @@ -108,12 +107,12 @@ class _SettingsBodyState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _SectionLabel('Sobre'), + _SectionLabel(SettingsStrings.about), _SettingsCard( children: [ _ActionTile( icon: Icons.description_outlined, - title: 'Termos de Serviço', + title: SettingsStrings.termsOfService, onTap: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => const TermsScreen(), @@ -123,7 +122,7 @@ class _SettingsBodyState extends State { const Divider(height: 1), _ActionTile( icon: Icons.diversity_3_outlined, - title: 'Criadores e colaboradores', + title: SettingsStrings.creatorsAndContributors, onTap: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => const CreditsScreen(), @@ -133,7 +132,7 @@ class _SettingsBodyState extends State { const Divider(height: 1), const _InfoTile( icon: Icons.info_outline_rounded, - title: 'Versão do app', + title: SettingsStrings.appVersion, subtitle: '1.0.0', ), ], @@ -147,12 +146,12 @@ class _SettingsBodyState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _SectionLabel('Zona de risco'), + _SectionLabel(SettingsStrings.dangerZone), _SettingsCard( children: [ _ActionTile( icon: Icons.delete_forever_rounded, - title: 'Apagar dados da conta', + title: SettingsStrings.deleteAccountData, titleColor: _accentPink, loading: _deletingAccount, onTap: _deletingAccount ? null : _confirmDeleteAccountData, diff --git a/lib/screens/terms_gate_screen.dart b/lib/screens/terms_gate_screen.dart index 6f1eaa1..9175e44 100644 --- a/lib/screens/terms_gate_screen.dart +++ b/lib/screens/terms_gate_screen.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; import '../main.dart' show supabase; +import '../strings/terms_strings.dart'; import '../terms_gate_prefs.dart'; import '../widgets/entrance.dart'; import '../widgets/liquid_waves_background.dart'; @@ -62,7 +64,7 @@ class _TermsGateScreenState extends State { body: Stack( clipBehavior: Clip.none, children: [ - Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), + Positioned.fill(child: Container(color: AppColors.background)), const LiquidWavesBackground(), SafeArea( child: Column( @@ -173,7 +175,7 @@ class _AcceptCheckboxRow extends StatelessWidget { ), const SizedBox(width: 10), const Text( - 'Aceitar tudo', + TermsStrings.acceptAll, style: TextStyle( fontWeight: FontWeight.w800, fontSize: 15, @@ -212,7 +214,7 @@ class _AdvanceButton extends StatelessWidget { ), ), onPressed: enabled ? onPressed : null, - child: const Text('Avançar'), + child: const Text(TermsStrings.advance), ), ), ); diff --git a/lib/screens/terms_screen.dart b/lib/screens/terms_screen.dart index 3b3309f..c3cd62f 100644 --- a/lib/screens/terms_screen.dart +++ b/lib/screens/terms_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; -import '../widgets/app_gradients.dart'; +import '../colors/app_gradients.dart'; +import '../strings/terms_strings.dart'; import '../widgets/terms_content.dart'; class TermsScreen extends StatelessWidget { @@ -20,14 +22,14 @@ class TermsScreen extends StatelessWidget { elevation: 0, scrolledUnderElevation: 0, title: const Text( - 'Termos de Serviço', + TermsStrings.pageTitle, style: TextStyle(fontWeight: FontWeight.w900), ), ), ), ), body: Container( - color: const Color(0xFFFAFAF7), + color: AppColors.background, child: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 24, 20, 24), diff --git a/lib/screens/video_screen.dart b/lib/screens/video_screen.dart index 1917dc2..7d81608 100644 --- a/lib/screens/video_screen.dart +++ b/lib/screens/video_screen.dart @@ -1,12 +1,14 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; import 'package:flutter/services.dart'; import 'package:video_player/video_player.dart'; import 'package:youtube_player_flutter/youtube_player_flutter.dart'; import '../watched_videos_prefs.dart'; -import '../widgets/app_gradients.dart'; +import '../colors/app_gradients.dart'; +import '../strings/video_strings.dart'; import '../widgets/entrance.dart'; import '../widgets/liquid_waves_background.dart'; import '../widgets/pill_snackbar.dart'; @@ -36,83 +38,80 @@ class VideoData { final List videoList = [ VideoData( id: 1, - title: 'Episódio 1', - description: 'Qual a Influência do nariz entupido na má oclusão', + title: VideoStrings.episodeTitle(1), + description: VideoStrings.episodeDescriptions[0], youtubeId: 'PJ58CZv4ECw', ), VideoData( id: 2, - title: 'Episódio 2', - description: 'Qual a Influência das alergias sazionais na má oclusão', + title: VideoStrings.episodeTitle(2), + description: VideoStrings.episodeDescriptions[1], youtubeId: 'y4_kWmZtAtg', ), VideoData( id: 3, - title: 'Episódio 3', - description: 'Qual a Influência das Otites frequentes na má oclusão', + title: VideoStrings.episodeTitle(3), + description: VideoStrings.episodeDescriptions[2], youtubeId: 'nD75Y5PuKTo', ), VideoData( id: 4, - title: 'Episódio 4', - description: 'Qual a Influência das Amigdalites recorrentes na má oclusão', + title: VideoStrings.episodeTitle(4), + description: VideoStrings.episodeDescriptions[3], youtubeId: 'yvFllWYeuLw', ), VideoData( id: 5, - title: 'Episódio 5', - description: - 'Qual a Influência das Bronquiolites recorrentes na má oclusão', + title: VideoStrings.episodeTitle(5), + description: VideoStrings.episodeDescriptions[4], youtubeId: 'DnhUa-T8_Ps', ), VideoData( id: 6, - title: 'Episódio 6', - description: 'Qual a Influência dos problemas respitatórios na má oclusão', + title: VideoStrings.episodeTitle(6), + description: VideoStrings.episodeDescriptions[5], youtubeId: 'zKt_iwkrjvo', ), VideoData( id: 7, - title: 'Episódio 7', - description: - 'Qual a Influência das interrupções respiratórias na má oclusão', + title: VideoStrings.episodeTitle(7), + description: VideoStrings.episodeDescriptions[6], youtubeId: 'NpmQ2brap5A', ), VideoData( id: 8, - title: 'Episódio 8', - description: 'Qual a Influência do ressonar na má oclusão', + title: VideoStrings.episodeTitle(8), + description: VideoStrings.episodeDescriptions[7], youtubeId: 'Wj3KYw9pBi0', ), VideoData( id: 9, - title: 'Episódio 9', - description: - 'Qual a Influência de acordar com saliva seca na boca ou na almofada na saúde oral', + title: VideoStrings.episodeTitle(9), + description: VideoStrings.episodeDescriptions[8], youtubeId: 'bOm9t61cT_U', ), VideoData( id: 10, - title: 'Episódio 10', - description: 'Qual a Influência da respiração oral na má oclusão', + title: VideoStrings.episodeTitle(10), + description: VideoStrings.episodeDescriptions[9], youtubeId: 'fAitMizbcms', ), VideoData( id: 11, - title: 'Episódio 11', - description: 'Qual a influência do uso exagerado da chupeta na má oclusão', + title: VideoStrings.episodeTitle(11), + description: VideoStrings.episodeDescriptions[10], youtubeId: '6sYoBUjks_I', ), VideoData( id: 12, - title: 'Episódio 12', - description: 'Qual a influência do uso exagerado da chupeta na má oclusão', + title: VideoStrings.episodeTitle(12), + description: VideoStrings.episodeDescriptions[11], youtubeId: 'eznKrErQbHo', ), VideoData( id: 13, - title: 'Episódio 13', - description: 'Qual a influência do hábito de chuchar o dedo na má oclusão', + title: VideoStrings.episodeTitle(13), + description: VideoStrings.episodeDescriptions[12], youtubeId: 'VO9CNqHRdeM', ), ]; @@ -176,7 +175,7 @@ Future showVideoPlayerDialog( }) { if (video.youtubeId != null) { if (video.youtubeId!.isEmpty) { - showPillSnackBar(context, 'Vídeo ainda não disponível'); + showPillSnackBar(context, VideoStrings.videoNotAvailable); return Future.value(); } return Navigator.of(context).push( @@ -199,8 +198,8 @@ class VideoScreen extends StatefulWidget { /// guardar localmente quais episódios ela já assistiu até ao fim. final String? scopeId; - static const Color _teal = Color(0xFF2F9E94); - static const Color _accentPink = Color(0xFFFF55A7); + static const Color _teal = AppColors.teal; + static const Color _accentPink = AppColors.pink; @override State createState() => _VideoScreenState(); @@ -253,7 +252,7 @@ class _VideoScreenState extends State { elevation: 0, scrolledUnderElevation: 0, title: const Text( - 'Vídeos Educativos', + VideoStrings.pageTitle, style: TextStyle(fontWeight: FontWeight.w900), ), ), @@ -262,7 +261,7 @@ class _VideoScreenState extends State { body: Stack( clipBehavior: Clip.none, children: [ - Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), + Positioned.fill(child: Container(color: AppColors.background)), const LiquidWavesBackground(), SafeArea( child: Padding( @@ -285,7 +284,7 @@ class _VideoScreenState extends State { child: TextField( controller: _searchController, decoration: InputDecoration( - hintText: 'Pesquisar vídeos...', + hintText: VideoStrings.searchHint, prefixIcon: const Icon( Icons.search, color: VideoScreen._teal, @@ -307,7 +306,7 @@ class _VideoScreenState extends State { child: _filteredVideos.isEmpty ? Center( child: Text( - 'Nenhum vídeo encontrado', + VideoStrings.noVideosFound, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -546,7 +545,7 @@ class _VideoButton extends StatelessWidget { fit: StackFit.expand, children: [ ColoredBox( - color: const Color(0xFFFFE6F1), + color: AppColors.pinkBackground, child: VideoThumbnail( video: video, borderRadius: 0, @@ -593,7 +592,7 @@ class _VideoButton extends StatelessWidget { borderRadius: BorderRadius.circular(999), ), child: Text( - 'EP. ${video.id}', + VideoStrings.episodeBadge(video.id), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w800, @@ -731,7 +730,7 @@ class _YoutubePlayerPageState extends State<_YoutubePlayerPage> child: Scaffold( backgroundColor: value.isFullScreen ? Colors.black - : const Color(0xFFFAFAF7), + : AppColors.background, appBar: value.isFullScreen ? null : PreferredSize( @@ -808,7 +807,7 @@ class _YoutubePlayerPageState extends State<_YoutubePlayerPage> MainAxisAlignment.spaceBetween, children: [ const Text( - 'Próximos', + VideoStrings.nextVideos, style: TextStyle( fontWeight: FontWeight.w900, fontSize: 15, @@ -835,7 +834,7 @@ class _YoutubePlayerPageState extends State<_YoutubePlayerPage> vertical: 2, ), child: Text( - 'Ver mais', + VideoStrings.seeMore, style: TextStyle( fontWeight: FontWeight.w800, fontSize: 13, @@ -1033,7 +1032,7 @@ class _VideoPlayerDialogState extends State<_VideoPlayerDialog> { setState(() { _isInitialized = false; }); - showPillSnackBar(context, 'Erro ao carregar vídeo: $e'); + showPillSnackBar(context, VideoStrings.errorLoadingVideo(e)); } } } @@ -1188,7 +1187,7 @@ class _VideoControlsState extends State<_VideoControls> { Icons.replay_10_rounded, color: Colors.white, ), - tooltip: 'Retroceder 10s', + tooltip: VideoStrings.rewind10s, onPressed: () => _seekBy(const Duration(seconds: -10)), ), IconButton( @@ -1211,7 +1210,7 @@ class _VideoControlsState extends State<_VideoControls> { Icons.forward_10_rounded, color: Colors.white, ), - tooltip: 'Avançar 10s', + tooltip: VideoStrings.forward10s, onPressed: () => _seekBy(const Duration(seconds: 10)), ), IconButton( @@ -1294,7 +1293,7 @@ class _FullscreenVideoPlayerState extends State<_FullscreenVideoPlayer> { setState(() { _isInitialized = false; }); - showPillSnackBar(context, 'Erro ao carregar vídeo: $e'); + showPillSnackBar(context, VideoStrings.errorLoadingVideo(e)); } } } diff --git a/lib/strings/auth_strings.dart b/lib/strings/auth_strings.dart new file mode 100644 index 0000000..f6f7d5d --- /dev/null +++ b/lib/strings/auth_strings.dart @@ -0,0 +1,38 @@ +/// Texto do ecrã de login/criação de conta ([HomeScreen]). +class AuthStrings { + const AuthStrings._(); + + static const String appTitle = 'Check-Teeth Kids'; + static const String appSubtitle = + 'Deteção e Prevenção da Má Oclusão Infantil'; + + static const String login = 'Entrar'; + static const String createAccount = 'Criar Conta'; + + static const String nameHint = 'Introduza o seu nome'; + static const String nameRequired = 'Indique o seu nome'; + static const String nameTooShort = 'Nome muito curto'; + static const String nameNoNumbers = 'O nome não pode conter números'; + + static const String emailHint = 'Introduza o seu email'; + static const String emailRequired = 'Indique o seu email'; + static const String emailInvalid = 'Email inválido'; + + static const String passwordHint = 'Introduza a sua palavra-passe'; + static const String passwordRequired = 'Indique a sua palavra-passe'; + static const String passwordTooShort = 'Mínimo de 6 caracteres'; + + static const String userNotFoundAfterSignUp = + 'Utilizador não encontrado após criar a conta.'; + static const String accountNoLongerExists = + 'Esta conta já não existe. Verifique o email ou crie uma nova conta.'; + static const String timeoutError = + 'Tempo esgotado. Verifique a sua ligação e tente novamente.'; + static const String invalidCredentials = 'Email ou palavra-passe incorretos.'; + static const String userNotFound = 'Utilizador não encontrado.'; + static const String emailAlreadyInUse = 'Este email já está em uso.'; + static const String weakPassword = + 'Palavra-passe fraca. Utilize pelo menos 6 caracteres.'; + + static String genericError(Object e) => 'Erro: $e'; +} diff --git a/lib/strings/common_strings.dart b/lib/strings/common_strings.dart new file mode 100644 index 0000000..26c9414 --- /dev/null +++ b/lib/strings/common_strings.dart @@ -0,0 +1,7 @@ +/// Texto de interface genérico, partilhado por widgets reutilizáveis que não +/// pertencem a um ecrã específico (ex.: [showConfirmDialog]). +class CommonStrings { + const CommonStrings._(); + + static const String cancel = 'Cancelar'; +} diff --git a/lib/strings/credits_strings.dart b/lib/strings/credits_strings.dart new file mode 100644 index 0000000..abef974 --- /dev/null +++ b/lib/strings/credits_strings.dart @@ -0,0 +1,18 @@ +/// Texto fixo do ecrã de créditos ([CreditsScreen]) — os nomes próprios e +/// da instituição em [_kCreditSections] ficam de fora de propósito (não são +/// texto de interface a traduzir, são dados/factos). +class CreditsStrings { + const CreditsStrings._(); + + static const String pageTitle = 'Criadores e Colaboradores'; + static const String header = 'Quem fez a Check-Teeth Kids'; + + static const String originalCreatorSection = 'Criadora original'; + static const String developmentSection = 'Desenvolvimento informático'; + static const String advisorsSection = 'Orientadores'; + static const String institutionSection = 'Instituição colaboradora'; + + static const String originalCreatorRole = 'Criadora original'; + static const String developerRole = 'Desenvolvedor'; + static const String advisorRole = 'Orientador(a)'; +} diff --git a/lib/strings/curiosidade_strings.dart b/lib/strings/curiosidade_strings.dart new file mode 100644 index 0000000..7234e2c --- /dev/null +++ b/lib/strings/curiosidade_strings.dart @@ -0,0 +1,16 @@ +/// Texto do ecrã de curiosidades ([CuriosidadeScreen]). +class CuriosidadeStrings { + const CuriosidadeStrings._(); + + static const String pageTitle = 'Curiosidades'; + static const String close = 'Fechar'; + + static const String topicXTitle = 'Tema X'; + static const String topicXDescription = + 'Aprenda dicas rápidas e simples para cuidar dos dentes no dia a dia.'; + + static const String topicYTitle = 'Tema Y'; + static const String topicZTitle = 'Tema Z'; + static const String topicUTitle = 'Tema U'; + static const String contentComingSoon = 'Conteúdo em breve.'; +} diff --git a/lib/strings/home_strings.dart b/lib/strings/home_strings.dart new file mode 100644 index 0000000..bc6dad8 --- /dev/null +++ b/lib/strings/home_strings.dart @@ -0,0 +1,109 @@ +/// Texto do ecrã principal depois de autenticado ([LoggedHomeScreen] e as +/// suas três abas: Início, Perfil, Ajustes). +class HomeStrings { + const HomeStrings._(); + + static const String noName = 'Sem nome'; + static const String goodMorning = 'Bom dia'; + static const String goodAfternoon = 'Boa tarde'; + static const String goodEvening = 'Boa noite'; + + static const String navHome = 'Início'; + static const String navProfile = 'Perfil'; + static const String navSettings = 'Ajustes'; + static const String settingsTitle = 'Configurações'; + + static const String signsGaugeLabel = 'Sinais de má\noclusão'; + static const String factorsGaugeLabel = 'Fatores de risco'; + + static String forChild(String name) => 'Para $name'; + + static const String educationalVideos = 'Vídeos educativos'; + static const String moreFeaturesSoon = 'Mais funcionalidades em breve'; + + static String brushingLimitReached(int maxPerDay) => + 'Já registou as $maxPerDay escovagens de hoje!'; + static const String brushingLogged = 'Escovagem registada!'; + static const String brushingThisWeek = 'Escovagens esta semana'; + static const String completedEpisodes = 'Episódios completos'; + + static String errorAddingChild(Object e) => 'Erro ao adicionar criança: $e'; + static const String registerAChild = 'Registe uma criança'; + static const String registerAChildMessage = + 'Antes de iniciar o quiz, adicione uma criança ao seu perfil.'; + static const String addChild = 'Adicionar criança'; + static const String whichChildIsTheQuizFor = 'Para qual criança é o quiz?'; + static String childNameWithAge(String name, int age) => '$name • $age anos'; + static const String cancel = 'Cancelar'; + + static const String freeAssessmentBadge = 'Avaliação gratuita'; + static const String assessmentTitle = 'Avaliação de má oclusão dentária'; + static const String assessmentSubtitle = + '29 perguntas rápidas · menos de 3 minutos'; + static const String startQuiz = 'Iniciar Quiz'; + + static const String videoLibraryBadge = 'Biblioteca de vídeos'; + static String watchedEpisodesSummary(int watchedCount, int totalCount) => + '$watchedCount episódio${watchedCount == 1 ? '' : 's'} completo${watchedCount == 1 ? '' : 's'} · $totalCount no total'; + static String allEpisodesSummary(int totalCount) => + '$totalCount episódios sobre saúde oral para toda a família'; + static const String watchVideos = 'Ver vídeos'; + + static const String profilePhoto = 'Foto de perfil'; + static const String camera = 'Câmara'; + static const String gallery = 'Galeria'; + static String errorUploadingPhoto(Object e) => 'Erro ao enviar foto: $e'; + + static const String removeChild = 'Remover criança'; + static String removeChildConfirmMessage(String childName) => + 'Tem a certeza que quer remover "$childName"? Esta ação não pode ser desfeita.'; + static const String remove = 'Remover'; + static const String noPermissionToRemoveChild = + 'Sem permissão para remover esta criança.'; + static const String childRemoved = 'Criança removida'; + static String errorRemovingChild(Object e) => 'Erro ao remover: $e'; + + static String weeklyGoalOf(String childName) => 'Meta semanal de $childName'; + static const String brushingsPerWeek = 'Escovagens por semana'; + static const String brushingGoalRange = 'Entre 1 e 21 (até 3 por dia)'; + static const String save = 'Guardar'; + + static const String childAdded = 'Criança adicionada'; + static const String addAnotherChildQuestion = 'Adicionar outra criança?'; + static const String notNow = 'Agora não'; + static const String addAnother = 'Adicionar outra'; + static const String timeoutAdding = + 'Tempo esgotado ao adicionar. Tente novamente.'; + static String errorAdding(Object e) => 'Erro ao adicionar: $e'; + + static const String myChildren = 'Meus filhos'; + static const String noChildrenYet = 'Nenhuma criança adicionada ainda.'; + static String childFallbackName(int index) => 'Criança ${index + 1}'; + static String ageLabel(int age) => 'Idade: $age'; + static String genderLabel(String gender) => 'Género: $gender'; + static const String weeklyBrushingGoalTooltip = 'Meta semanal de escovagens'; + + static const String signOut = 'Sair'; + static const String addAnotherChildTitle = 'Adicionar outra criança'; + static const String birthDate = 'Data de nascimento'; + static const String confirm = 'Confirmar'; + static const String childName = 'Nome da criança'; + static const String nameRequired = 'Indique o nome'; + static const String nameTooShort = 'Nome muito curto'; + static const String nameNoNumbers = 'O nome não pode conter números'; + static const String selectDate = 'Selecione a data'; + static const String male = 'Masculino'; + static const String female = 'Feminino'; + static const String other = 'Outro'; + static const String gender = 'Género'; + static const String genderRequired = 'Selecione o género'; + static const String birthDateRequired = 'Indique a data de nascimento'; + static const String add = 'Adicionar'; + + static const String childCode = 'Código numérico'; + static const String childCodeRequired = 'Indique o código'; + static const String childCodeDigitsOnly = 'O código só pode conter números'; + static const String childCodeAlreadyInUse = + 'Este código já está atribuído a outra criança'; + static String codeLabel(String code) => 'Cód. $code'; +} diff --git a/lib/strings/privacy_strings.dart b/lib/strings/privacy_strings.dart new file mode 100644 index 0000000..3b1db97 --- /dev/null +++ b/lib/strings/privacy_strings.dart @@ -0,0 +1,23 @@ +/// Texto fixo da Política de Privacidade, mostrado no ecrã de bloqueio no +/// cadastro ([PrivacyGateScreen]). +class PrivacyStrings { + const PrivacyStrings._(); + + static const String headerTitle = 'Política de Privacidade'; + static const String acceptAll = 'Aceitar tudo'; + static const String advance = 'Avançar'; + static const String requiredPrefix = '(Obrigatório) '; + static const String optionalPrefix = '(Opcional) '; + + static const String healthDataConsent = + 'Confirmo que sou encarregado(a) de educação ou responsável legal ' + 'da criança avaliada e concordo com o processamento dos dados de ' + 'saúde que introduzo na Check-Teeth Kids, exclusivamente para gerar ' + 'o resultado da triagem digital.'; + static const String privacyPolicyConsent = + 'Li e aceito os Termos e Condições apresentados anteriormente e as ' + 'condições de privacidade descritas nesta página.'; + static const String trackingConsent = + 'Ajude-nos a melhorar a Check-Teeth Kids, autorizando a recolha de ' + 'dados técnicos de utilização.'; +} diff --git a/lib/strings/quiz_result_strings.dart b/lib/strings/quiz_result_strings.dart new file mode 100644 index 0000000..c8031db --- /dev/null +++ b/lib/strings/quiz_result_strings.dart @@ -0,0 +1,47 @@ +/// Texto do ecrã de resultado do quiz ([QuizResultScreen]) e do vídeo-guia +/// mostrado logo a seguir. +class QuizResultStrings { + const QuizResultStrings._(); + + static const String resultHeading = 'O resultado avaliado é de:'; + static const String signsRingLabel = 'Sinais de má\noclusão instalados'; + static const String factorsRingLabel = 'Fatores de\nrisco associados'; + + static const String conclusionsHeading = 'Conclusões:'; + static String conclusionsBody({ + required int signs, + required int signsMax, + required int factors, + required int factorsMax, + }) => + 'Foram identificados $signs de $signsMax ' + 'sinais de má oclusão já instalada e ' + '$factors de $factorsMax fatores de ' + 'risco frequentemente associados a ' + 'má oclusão.'; + + static const String whatToDoNowHeading = 'O que deve fazer agora'; + static const String whatToDoNowRecommend = + 'Com base neste resultado, recomendamos a marcação de uma consulta com Odontopediatra ou Ortodontista para uma avaliação presencial.'; + static const String whatToDoNowNoConcerns = + 'Não foram encontrados sinais ou fatores de risco relevantes. Continue a acompanhar a saúde oral do seu filho/a com as consultas de rotina habituais.'; + + static const String importantToKnowHeading = 'Importante saber'; + static const String importantToKnowBody = + 'Este resultado não substitui uma avaliação presencial, nem tem função de diagnóstico. O objetivo é ajudá-lo a estar mais atento aos sinais identificados e a não adiar uma consulta que pode fazer a diferença no desenvolvimento oral do seu filho/a.'; + + static const String watchTheFollowingVideo = 'Visualize o seguinte vídeo'; + static const String resultVideoCaption = + 'Assista para compreender a importância de um diagnóstico ' + 'precoce e de realizar tratamento ortodôntico intercetivo, ' + 'ou seja, uma intervenção realizada na infância, tipicamente ' + 'durante a fase de dentição mista (presença tanto de dentes ' + 'de leite como de permanentes).\n\nO seu propósito é ' + 'identificar, prevenir ou reduzir a gravidade de maloclusões ' + 'e problemas no desenvolvimento dos maxilares enquanto os ' + 'ossos e dentes ainda estão em crescimento.\n\nEsta ' + 'intervenção aproveita a fase de desenvolvimento craniofacial ' + 'para influenciar o crescimento ósseo, redirecionar a ' + 'mandíbula, corrigir o alinhamento dos maxilares ou criar ' + 'espaço para a erupção correta dos dentes permanentes.'; +} diff --git a/lib/strings/quiz_strings.dart b/lib/strings/quiz_strings.dart new file mode 100644 index 0000000..7234689 --- /dev/null +++ b/lib/strings/quiz_strings.dart @@ -0,0 +1,254 @@ +/// Todo o texto do quiz de triagem (checklist inicial + 29 perguntas). +/// Para traduzir o quiz, é só editar os valores aqui — nenhum texto de +/// pergunta/resposta deve viver directamente dentro de `quiz1.dart`. +class QuizStrings { + const QuizStrings._(); + + // Ecrã de checklist inicial. + static const String checklistHeading = 'Vamos ajudá-lo/a a compreender:'; + static const String checklistItem1 = + 'Sinais de alerta podem passar despercebidos'; + static const String checklistItem2 = 'Prevenir é melhor do que tratar'; + static const String checklistItem3 = + 'Quando deve procurar um dentista (urgência vs vigilância)'; + + // Rótulos partilhados pelas perguntas Sim/Não. + static const String yes = 'Sim'; + static const String no = 'Não'; + static const String dontKnow = 'Não sei'; + static const String dontKnowDescription = 'Não tenho a certeza'; + + // Pergunta 1: Problemas respiratórios + static const String q1Question = + 'O seu filho/a tem problemas respiratórios diagnosticados?'; + static const String q1YesDescription = + 'Problemas respiratórios diagnosticados'; + static const String q1NoDescription = + 'Sem problemas respiratórios diagnosticados'; + + // Pergunta 2: Respira pela boca + static const String q2Question = + 'O seu filho/a respira habitualmente pela boca?'; + static const String q2YesDescription = 'Respira habitualmente pela boca'; + static const String q2NoDescription = 'Não respira habitualmente pela boca'; + + // Pergunta 3: Ressonar + static const String q3Question = + 'O seu filho/a ressona habitualmente durante a noite?'; + static const String q3YesDescription = + 'Ressonar habitualmente durante a noite'; + static const String q3NoDescription = 'Não ressona habitualmente'; + + // Pergunta 4: Nariz tapado + static const String q4Question = + 'O seu filho/a sente habitualmente o nariz "tapado"?'; + static const String q4YesDescription = 'Sente habitualmente o nariz tapado'; + static const String q4NoDescription = + 'Não sente habitualmente o nariz tapado'; + + // Pergunta 5: Interrupções da respiração + static const String q5Question = + 'Durante o sono, o seu filho/a tem habitualmente interrupções da respiração?'; + static const String q5YesDescription = + 'Tem habitualmente interrupções da respiração durante o sono'; + static const String q5NoDescription = + 'Não tem interrupções da respiração durante o sono'; + + // Pergunta 6: Range os dentes + static const String q6Question = + 'O seu filho/a range os dentes com frequência?'; + static const String q6YesDescription = 'Range os dentes com frequência'; + static const String q6NoDescription = 'Não range os dentes com frequência'; + + // Pergunta 7: Alergias sazonais + static const String q7Question = + 'O seu filho/a habitualmente tem alergias sazonais?'; + static const String q7YesDescription = 'Habitualmente tem alergias sazonais'; + static const String q7NoDescription = 'Não tem alergias sazonais'; + + // Pergunta 8: Saliva seca + static const String q8Question = + 'O seu filho/a acorda com saliva seca na cara ou na almofada?'; + static const String q8YesDescription = + 'Acorda com saliva seca na cara ou na almofada'; + static const String q8NoDescription = 'Não acorda com saliva seca'; + + // Pergunta 9: Otites + static const String q9Question = + 'O seu filho/a teve ou costuma ter com frequência otites?'; + static const String q9YesDescription = + 'Teve ou costuma ter com frequência otites'; + static const String q9NoDescription = 'Não teve ou não costuma ter otites'; + + // Pergunta 10: Amigdalites + static const String q10Question = + 'O seu filho/a teve ou costuma ter com frequência amigdalites?'; + static const String q10YesDescription = + 'Teve ou costuma ter com frequência amigdalites'; + static const String q10NoDescription = + 'Não teve ou não costuma ter amigdalites'; + + // Pergunta 11: Bronquiolites + static const String q11Question = + 'O seu filho/a teve ou costuma ter com frequência bronquiolites?'; + static const String q11YesDescription = + 'Teve ou costuma ter com frequência bronquiolites'; + static const String q11NoDescription = + 'Não teve ou não costuma ter bronquiolites'; + + // Pergunta 12: Dificuldades a mastigar + static const String q12Question = + 'O seu filho/a apresenta dificuldades a mastigar?'; + static const String q12YesDescription = 'Apresenta dificuldades a mastigar'; + static const String q12NoDescription = 'Não apresenta dificuldades a mastigar'; + + // Pergunta 13: Lento a comer + static const String q13Question = 'O seu filho/a habitualmente é lento a comer?'; + static const String q13YesDescription = 'Habitualmente é lento a comer'; + static const String q13NoDescription = 'Não é lento a comer'; + + // Pergunta 14: Prefere alimentos moles + static const String q14Question = + 'O seu filho/a habitualmente prefere comer alimentos moles?'; + static const String q14YesDescription = + 'Habitualmente prefere comer alimentos moles'; + static const String q14NoDescription = 'Não prefere alimentos moles'; + + // Pergunta 15: Alimentado por biberão + static const String q15Question = 'Em bebé apenas foi alimentado por biberão?'; + static const String q15YesDescription = + 'Em bebé apenas foi alimentado por biberão'; + static const String q15NoDescription = 'Não foi apenas alimentado por biberão'; + + // Pergunta 16: Chupeta + static const String q16Question = + 'O seu filho/a usa ou usou chupeta com frequência?'; + static const String q16YesDescription = 'Usa ou usou chupeta com frequência'; + static const String q16NoDescription = + 'Não usa ou não usou chupeta com frequência'; + + // Pergunta 17: Chucha o dedo + static const String q17Question = + 'O seu filho/a chucha ou já chuchou o dedo com frequência?'; + static const String q17YesDescription = + 'Chucha ou já chuchou o dedo com frequência'; + static const String q17NoDescription = + 'Não chucha ou não chuchou o dedo com frequência'; + + // Pergunta 18: Postura (imagem) + static const String q18Question = + 'Qual das seguintes imagens é mais parecida com a postura do seu filho/a?'; + static const String q18Answer1Title = 'Postura inadequada'; + static const String q18Answer1Description = + 'Postura curvada, ombros e pescoço projetados'; + static const String q18Answer2Title = 'Postura correta'; + static const String q18Answer2Description = 'Postura ereta, coluna alinhada'; + + // Pergunta 19: Perfil (imagem) + static const String q19Question = + 'Qual das seguintes imagens é mais parecida com o perfil do seu filho/a?'; + static const String q19Answer1Title = 'Perfil convexo'; + static const String q19Answer1Description = 'Perfil facial convexo'; + static const String q19Answer2Title = 'Perfil reto'; + static const String q19Answer2Description = 'Perfil facial reto'; + static const String q19Answer3Title = 'Perfil côncavo'; + static const String q19Answer3Description = 'Perfil facial côncavo'; + + // Pergunta 20: Boca habitual (imagem) + static const String q20Question = + 'Qual é a posição da boca do seu filho/a habitualmente?'; + static const String q20Answer1Title = 'Boca fechada'; + static const String q20Answer1Description = 'Boca fechada habitualmente'; + static const String q20Answer2Title = 'Boca entreaberta'; + static const String q20Answer2Description = 'Boca entreaberta habitualmente'; + + // Pergunta 21: Zona abaixo dos olhos (imagem) + static const String q21Question = + 'Qual das imagens, na zona abaixo dos olhos, se assemelha mais ao seu filho/a?'; + static const String q21Answer1Title = 'Sem sinais'; + static const String q21Answer1Description = 'Sem olheiras visíveis abaixo dos olhos'; + static const String q21Answer2Title = 'Sinal de risco'; + static const String q21Answer2Description = 'Olheiras visíveis abaixo dos olhos'; + + // Pergunta 22: Queixo com a boca fechada (imagem) + static const String q22Question = + 'Qual das imagens é mais parecida com o queixo do seu filho/a com a boca fechada?'; + static const String q22Answer1Title = 'Queixo relaxado'; + static const String q22Answer1Description = + 'Queixo liso e relaxado com a boca fechada'; + static const String q22Answer2Title = 'Queixo tenso'; + static const String q22Answer2Description = + 'Queixo tenso/franzido com a boca fechada'; + + // Pergunta 23: Boca / dentição (imagem) + static const String q23Question = + 'Qual das seguintes imagens se assemelha à boca do seu filho/a?'; + static const String q23Answer1Title = 'Dentição sobreposta'; + static const String q23Answer1Description = 'Dentes sobrepostos/tortos'; + static const String q23Answer2Title = 'Dentição alinhada'; + static const String q23Answer2Description = + 'Dentição bem alinhada, sem apinhamento'; + static const String q23Answer3Title = 'Dentição desalinhada'; + static const String q23Answer3Description = 'Dentição desalinhada/apinhada'; + + // Pergunta 24: Apinhamento visto de perto (imagem) + static const String q24Question = + 'Qual das seguintes imagens se assemelha à boca do seu filho/a?'; + static const String q24Answer1Title = 'Apinhamento moderado'; + static const String q24Answer1Description = + 'Dentes rodados/desalinhados de forma mais visível'; + static const String q24Answer2Title = 'Ligeiro desalinhamento'; + static const String q24Answer2Description = + 'Pequeno desalinhamento ou espaço entre alguns dentes'; + static const String q24Answer3Title = 'Apinhamento acentuado'; + static const String q24Answer3Description = 'Dentes muito sobrepostos entre si'; + + // Pergunta 25: Freio labial (imagem) + static const String q25Question = + 'Qual das seguintes imagens se assemelha ao freio labial do seu filho/a?'; + static const String q25Answer1Title = 'Freio labial correto'; + static const String q25Answer1Description = 'Inserção do freio labial mais alta'; + static const String q25Answer2Title = 'Freio labial inadequado'; + static const String q25Answer2Description = + 'Inserção do freio labial baixa, entre os dentes'; + + // Pergunta 26: Freio lingual (imagem) + static const String q26Question = + 'Qual das seguintes imagens se assemelha ao freio lingual do seu filho/a?'; + static const String q26Answer1Title = 'Freio lingual inadequado'; + static const String q26Answer1Description = + 'Freio lingual curto/apertado (língua em coração)'; + static const String q26Answer2Title = 'Freio lingual correto'; + static const String q26Answer2Description = + 'Língua move-se livremente, sem restrição visível'; + + // Pergunta 27: Boca / dentição 3 (imagem) + static const String q27Question = + 'Qual das seguintes imagens se assemelha com a boca do seu filho/a?'; + static const String q27Answer1Title = 'Dentição desalinhada'; + static const String q27Answer1Description = 'Dentição desalinhada/apinhada'; + static const String q27Answer2Title = 'Dentição sobreposta'; + static const String q27Answer2Description = 'Dentes sobrepostos/tortos'; + static const String q27Answer3Title = 'Dentição alinhada'; + static const String q27Answer3Description = + 'Dentição bem alinhada, sem apinhamento'; + + // Pergunta 28: Boca / dentição 2 (imagem) + static const String q28Question = + 'Qual das seguintes imagens se assemelha à boca do seu filho/a?'; + static const String q28Answer1Title = 'Dentição alinhada'; + static const String q28Answer1Description = + 'Dentição bem alinhada, sem apinhamento'; + static const String q28Answer2Title = 'Dentição desalinhada'; + static const String q28Answer2Description = 'Dentes sobrepostos/apinhados'; + + // Pergunta 29: Céu da boca (imagem, final) + static const String q29Question = + 'Qual das seguintes imagens se assemelha ao céu da boca do seu filho/a?'; + static const String q29CorrectBadgeLabel = 'Posição saudável'; + static const String q29Answer1Title = 'Posição profunda incorreta'; + static const String q29Answer1Description = + 'Palato estreito/profundo, em forma de V'; + static const String q29Answer2Title = 'Posição em U saudável'; + static const String q29Answer2Description = 'Palato largo, em forma de U'; +} diff --git a/lib/strings/quiz_ui_strings.dart b/lib/strings/quiz_ui_strings.dart new file mode 100644 index 0000000..449972e --- /dev/null +++ b/lib/strings/quiz_ui_strings.dart @@ -0,0 +1,36 @@ +/// Texto fixo da "moldura" do quiz (botões, validações, selos) — separado +/// de [QuizStrings], que tem só o conteúdo das perguntas/respostas. +class QuizUiStrings { + const QuizUiStrings._(); + + static const String defaultCorrectBadgeLabel = 'Posição certa'; + static const String incorrectBadgeLabel = 'Posição inadequada'; + static const String referenceImage = 'Imagem de referência'; + + static const String videoFallbackTitle = 'Vídeo'; + static const String watchOptionalVideo = 'Ver vídeo (opcional)'; + + static const String enterNumber = 'Insira o número'; + static const String chooseOneOption = 'Escolha uma opção'; + static const String chooseOnlyOneOption = 'Escolha apenas uma opção'; + + static const String finish = 'Concluir'; + static const String advance = 'Avançar'; + static const String backToHomepage = 'Voltar para homepage'; + + static const String dontKnow = 'Não sei'; + + static const String skip = 'Pular'; + static const String defaultVideoGuideHeading = + 'Antes de continuar, veja o vídeo educativo...'; + static const String defaultVideoGuideCaption = + 'Veja o vídeo para compreender melhor as próximas perguntas do questionário.'; + + static String teethCountTooHigh(int max) => + 'Um número tão alto assim não é possível.\nO máximo é $max.'; + + static String notSureSeeVideo(String videoTitle) => + 'Não tem a certeza? Veja o "$videoTitle" para ajudar a responder'; + + static String quizProgress(int current, int total) => 'Quiz $current/$total'; +} diff --git a/lib/strings/settings_strings.dart b/lib/strings/settings_strings.dart new file mode 100644 index 0000000..c4c1901 --- /dev/null +++ b/lib/strings/settings_strings.dart @@ -0,0 +1,26 @@ +/// Texto do separador de Ajustes ([SettingsBody]). +class SettingsStrings { + const SettingsStrings._(); + + static const String deleteAccountData = 'Apagar dados da conta'; + static const String deleteAccountMessage = + 'Isto remove permanentemente a sua conta, perfil, crianças ' + 'registadas e fotos — incluindo o login, permitindo criar uma ' + 'nova conta com o mesmo e-mail depois. Esta ação não pode ser ' + 'desfeita. Pretende continuar?'; + static const String delete = 'Apagar'; + static String errorDeletingAccount(int status) => + 'Erro ao apagar conta (status $status)'; + static String errorDeleting(Object e) => 'Erro ao apagar: $e'; + + static const String account = 'Conta'; + static const String noName = 'Sem nome'; + static const String signOut = 'Sair'; + + static const String about = 'Sobre'; + static const String termsOfService = 'Termos de Serviço'; + static const String creatorsAndContributors = 'Criadores e colaboradores'; + static const String appVersion = 'Versão do app'; + + static const String dangerZone = 'Zona de risco'; +} diff --git a/lib/strings/splash_strings.dart b/lib/strings/splash_strings.dart new file mode 100644 index 0000000..2a753be --- /dev/null +++ b/lib/strings/splash_strings.dart @@ -0,0 +1,6 @@ +/// Texto do ecrã de boas-vindas ([HelloSplashScreen]). +class SplashStrings { + const SplashStrings._(); + + static const String greeting = 'Olá'; +} diff --git a/lib/strings/terms_strings.dart b/lib/strings/terms_strings.dart new file mode 100644 index 0000000..180d9e5 --- /dev/null +++ b/lib/strings/terms_strings.dart @@ -0,0 +1,33 @@ +/// Texto fixo dos Termos e Condições, partilhado entre o ecrã de bloqueio +/// no cadastro ([TermsGateScreen]) e o ecrã informativo em Ajustes +/// ([TermsScreen]). +class TermsStrings { + const TermsStrings._(); + + static const String headerTitle = 'Termos e Condições'; + static const String pageTitle = 'Termos de Serviço'; + static const String acceptAll = 'Aceitar tudo'; + static const String advance = 'Avançar'; + + static const List paragraphs = [ + '© 2026, Francisca Salgado Ferreira Pacheco Silva. A aplicação, o método ' + 'de avaliação, arquitetura e conteúdos são propriedade intelectual ' + 'exclusiva da autora.', + 'A Check-Teeth Kids é uma aplicação desenvolvida com o propósito de ' + 'promover a literacia em saúde oral infantil, através de conteúdos ' + 'educativos e de um quiz de triagem digital, com especial enfoque na ' + 'identificação precoce de sinais associados à má oclusão dentária.', + 'Com base nas respostas fornecidas, é apresentado um resultado que ' + 'identifica sinais de má oclusão já instalados e fatores de risco ' + 'associados, destinado a auxiliar o utilizador na decisão de ' + 'procurar uma avaliação clínica especializada.', + 'Esta ferramenta não tem como objetivo realizar diagnósticos clínicos, ' + 'nem substituir a avaliação por um profissional de saúde oral. ' + 'Destina-se exclusivamente a uma triagem inicial e informativa, ' + 'promovendo o encaminhamento atempado para um médico dentista ' + 'especialista (Odontopediatra ou Ortodontista).', + 'Esta aplicação foi desenvolvida no âmbito de um projeto de dissertação ' + 'de Mestrado, encontrando-se numa fase de desenvolvimento e ainda ' + 'sem validação clínica formal concluída.', + ]; +} diff --git a/lib/strings/video_strings.dart b/lib/strings/video_strings.dart new file mode 100644 index 0000000..24a144a --- /dev/null +++ b/lib/strings/video_strings.dart @@ -0,0 +1,35 @@ +/// Texto do ecrã de vídeos educativos ([VideoScreen]) e dos seus players. +class VideoStrings { + const VideoStrings._(); + + static const String pageTitle = 'Vídeos Educativos'; + static const String searchHint = 'Pesquisar vídeos...'; + static const String noVideosFound = 'Nenhum vídeo encontrado'; + static const String videoNotAvailable = 'Vídeo ainda não disponível'; + static String errorLoadingVideo(Object e) => 'Erro ao carregar vídeo: $e'; + static const String rewind10s = 'Retroceder 10s'; + static const String forward10s = 'Avançar 10s'; + static const String nextVideos = 'Próximos'; + static const String seeMore = 'Ver mais'; + static String episodeBadge(int id) => 'EP. $id'; + + static String episodeTitle(int id) => 'Episódio $id'; + + /// Descrições de cada episódio, na mesma ordem dos ids (índice 0 = ep. 1). + static const List episodeDescriptions = [ + 'Qual a Influência do nariz entupido na má oclusão', + 'Qual a Influência das alergias sazionais na má oclusão', + 'Qual a Influência das Otites frequentes na má oclusão', + 'Qual a Influência das Amigdalites recorrentes na má oclusão', + 'Qual a Influência das Bronquiolites recorrentes na má oclusão', + 'Qual a Influência dos problemas respitatórios na má oclusão', + 'Qual a Influência das interrupções respiratórias na má oclusão', + 'Qual a Influência do ressonar na má oclusão', + 'Qual a Influência de acordar com saliva seca na boca ou na almofada ' + 'na saúde oral', + 'Qual a Influência da respiração oral na má oclusão', + 'Qual a influência do uso exagerado da chupeta na má oclusão', + 'Qual a influência do uso exagerado da chupeta na má oclusão', + 'Qual a influência do hábito de chuchar o dedo na má oclusão', + ]; +} diff --git a/lib/widgets/app_dialogs.dart b/lib/widgets/app_dialogs.dart index 3218a6b..6d09efd 100644 --- a/lib/widgets/app_dialogs.dart +++ b/lib/widgets/app_dialogs.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; +import '../strings/common_strings.dart'; import 'tap_bounce.dart'; -const Color _teal = Color(0xFF2F9E94); -const Color _accentPink = Color(0xFFFF55A7); +const Color _teal = AppColors.teal; +const Color _accentPink = AppColors.pink; /// Diálogo de confirmação com a identidade visual do app (título rosa, /// botões em pílula), usado para todas as confirmações destrutivas/decisórias. @@ -11,7 +13,7 @@ Future showConfirmDialog( BuildContext context, { required String title, String? message, - String cancelLabel = 'Cancelar', + String cancelLabel = CommonStrings.cancel, required String confirmLabel, Color confirmColor = _teal, }) { diff --git a/lib/widgets/pill_snackbar.dart b/lib/widgets/pill_snackbar.dart index 32b49a5..e236250 100644 --- a/lib/widgets/pill_snackbar.dart +++ b/lib/widgets/pill_snackbar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; /// Notificação em formato de pílula flutuante (fundo branco, texto rosa, /// cantos totalmente arredondados, com um pequeno "pop" de entrada), usada @@ -62,7 +63,7 @@ class _AnimatedPill extends StatelessWidget { message, textAlign: TextAlign.center, style: const TextStyle( - color: Color(0xFFFF55A7), + color: AppColors.pink, fontWeight: FontWeight.w800, fontSize: 14, ), diff --git a/lib/widgets/privacy_content.dart b/lib/widgets/privacy_content.dart index 63f60dc..4b32530 100644 --- a/lib/widgets/privacy_content.dart +++ b/lib/widgets/privacy_content.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; +import '../strings/privacy_strings.dart'; -const Color kPrivacyTeal = Color(0xFF2F9E94); +const Color kPrivacyTeal = AppColors.teal; /// Um dos itens de consentimento mostrados no ecrã de privacidade. Itens /// com [required] a false (ex.: recolha de dados técnicos de utilização) @@ -19,25 +21,14 @@ class PrivacyConsentItem { /// Texto fixo dos consentimentos de privacidade, mostrado no cadastro. const List kPrivacyConsentItems = [ - PrivacyConsentItem( - id: 'health_data', - text: - 'Confirmo que sou encarregado(a) de educação ou responsável legal ' - 'da criança avaliada e concordo com o processamento dos dados de ' - 'saúde que introduzo na Check-Teeth Kids, exclusivamente para gerar ' - 'o resultado da triagem digital.', - ), + PrivacyConsentItem(id: 'health_data', text: PrivacyStrings.healthDataConsent), PrivacyConsentItem( id: 'privacy_policy', - text: - 'Li e aceito os Termos e Condições apresentados anteriormente e as ' - 'condições de privacidade descritas nesta página.', + text: PrivacyStrings.privacyPolicyConsent, ), PrivacyConsentItem( id: 'tracking', - text: - 'Ajude-nos a melhorar a Check-Teeth Kids, autorizando a recolha de ' - 'dados técnicos de utilização.', + text: PrivacyStrings.trackingConsent, required: false, ), ]; @@ -66,7 +57,7 @@ class PrivacyHeader extends StatelessWidget { ), const SizedBox(height: 16), const Text( - 'Política de Privacidade', + PrivacyStrings.headerTitle, textAlign: TextAlign.center, style: TextStyle( fontSize: 20, diff --git a/lib/widgets/terms_content.dart b/lib/widgets/terms_content.dart index beb62f3..3b6c161 100644 --- a/lib/widgets/terms_content.dart +++ b/lib/widgets/terms_content.dart @@ -1,30 +1,8 @@ import 'package:flutter/material.dart'; +import '../colors/app_colors.dart'; +import '../strings/terms_strings.dart'; -const Color kTermsPink = Color(0xFFFF55A7); - -/// Parágrafos dos Termos e Condições — texto fixo, igual em todo o app -/// (ecrã de bloqueio no cadastro e ecrã informativo em Ajustes). -const List kTermsParagraphs = [ - '© 2026, Francisca Salgado Ferreira Pacheco Silva. A aplicação, o método ' - 'de avaliação, arquitetura e conteúdos são propriedade intelectual ' - 'exclusiva da autora.', - 'A Check-Teeth Kids é uma aplicação desenvolvida com o propósito de ' - 'promover a literacia em saúde oral infantil, através de conteúdos ' - 'educativos e de um quiz de triagem digital, com especial enfoque na ' - 'identificação precoce de sinais associados à má oclusão dentária.', - 'Com base nas respostas fornecidas, é apresentado um resultado que ' - 'identifica sinais de má oclusão já instalados e fatores de risco ' - 'associados, destinado a auxiliar o utilizador na decisão de procurar ' - 'uma avaliação clínica especializada.', - 'Esta ferramenta não tem como objetivo realizar diagnósticos clínicos, ' - 'nem substituir a avaliação por um profissional de saúde oral. ' - 'Destina-se exclusivamente a uma triagem inicial e informativa, ' - 'promovendo o encaminhamento atempado para um médico dentista ' - 'especialista (Odontopediatra ou Ortodontista).', - 'Esta aplicação foi desenvolvida no âmbito de um projeto de dissertação ' - 'de Mestrado, encontrando-se numa fase de desenvolvimento e ainda sem ' - 'validação clínica formal concluída.', -]; +const Color kTermsPink = AppColors.pink; /// Emblema circular rosa + título "Termos e Condições", reutilizado nos /// dois ecrãs que mostram este conteúdo. @@ -50,7 +28,7 @@ class TermsHeader extends StatelessWidget { ), const SizedBox(height: 16), const Text( - 'Termos e Condições', + TermsStrings.headerTitle, textAlign: TextAlign.center, style: TextStyle( fontSize: 20, @@ -85,10 +63,10 @@ class TermsBody extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - for (var i = 0; i < kTermsParagraphs.length; i++) ...[ + for (var i = 0; i < TermsStrings.paragraphs.length; i++) ...[ if (i > 0) const SizedBox(height: 14), Text( - kTermsParagraphs[i], + TermsStrings.paragraphs[i], textAlign: TextAlign.center, style: TextStyle( fontSize: 13,