CKT String|Colors

This commit is contained in:
Carlos Correia
2026-07-15 19:13:15 +01:00
parent 3214a07779
commit a8af2c6845
35 changed files with 1390 additions and 688 deletions

View File

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

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'app_colors.dart';
/// Gradiente usado em todas as app bars da aplicação. Direção puramente /// 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 é /// 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 /// 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( const LinearGradient kAppBarGradient = LinearGradient(
begin: Alignment.centerLeft, begin: Alignment.centerLeft,
end: Alignment.centerRight, 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 /// 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 /// mantém-se como [LinearGradient] com as duas cores iguais para não obrigar
/// a mudar todos os `decoration: gradient: kGreenButtonGradient` existentes. /// a mudar todos os `decoration: gradient: kGreenButtonGradient` existentes.
const LinearGradient kGreenButtonGradient = LinearGradient( const LinearGradient kGreenButtonGradient = LinearGradient(
colors: [Color(0xFF2F9E94), Color(0xFF2F9E94)], colors: [AppColors.teal, AppColors.teal],
); );
/// Gradiente rosa vivo do card do quiz na Home. /// Gradiente rosa vivo do card do quiz na Home.
const LinearGradient kPinkHeroGradient = LinearGradient( const LinearGradient kPinkHeroGradient = LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFFFF55A7), Color(0xFFE83E93)], colors: [AppColors.pink, AppColors.pinkVivid],
); );

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'colors/app_colors.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:supabase_flutter/supabase_flutter.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 'main.dart' show supabase;
import 'privacy_gate_prefs.dart'; import 'privacy_gate_prefs.dart';
import 'terms_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/entrance.dart';
import 'widgets/liquid_waves_background.dart'; import 'widgets/liquid_waves_background.dart';
import 'widgets/name_input_formatter.dart'; import 'widgets/name_input_formatter.dart';
import 'widgets/pill_snackbar.dart'; import 'widgets/pill_snackbar.dart';
import 'widgets/tap_bounce.dart'; import 'widgets/tap_bounce.dart';
const Color _teal = Color(0xFF2F9E94); const Color _teal = AppColors.teal;
const Color _pink = Color(0xFFFF55A7); const Color _pink = AppColors.pink;
/// Nomes só podem ter letras (incluindo acentuadas) e espaços — sem números. /// Nomes só podem ter letras (incluindo acentuadas) e espaços — sem números.
final RegExp _namePattern = RegExp(r"^[a-zA-ZÀ-ÖØ-öø-ÿ' -]+$"); final RegExp _namePattern = RegExp(r"^[a-zA-ZÀ-ÖØ-öø-ÿ' -]+$");
@@ -113,7 +115,7 @@ class _HomeScreenState extends State<HomeScreen> {
final user = response.user; final user = response.user;
if (user == null) { 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 // Antes de persistir o perfil (o que já faz o AuthGate considerar a
@@ -137,7 +139,7 @@ class _HomeScreenState extends State<HomeScreen> {
if (!mounted) return; if (!mounted) return;
showPillSnackBar( showPillSnackBar(
context, context,
'Esta conta já não existe. Verifique o email ou crie uma nova conta.', AuthStrings.accountNoLongerExists,
); );
} on AuthException catch (e) { } on AuthException catch (e) {
if (!mounted) return; if (!mounted) return;
@@ -146,11 +148,11 @@ class _HomeScreenState extends State<HomeScreen> {
if (!mounted) return; if (!mounted) return;
showPillSnackBar( showPillSnackBar(
context, context,
'Tempo esgotado. Verifique a sua ligação e tente novamente.', AuthStrings.timeoutError,
); );
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
showPillSnackBar(context, 'Erro: $e'); showPillSnackBar(context, AuthStrings.genericError(e));
} finally { } finally {
if (mounted) setState(() => _loading = false); if (mounted) setState(() => _loading = false);
} }
@@ -159,14 +161,14 @@ class _HomeScreenState extends State<HomeScreen> {
String _friendlyAuthError(AuthException e) { String _friendlyAuthError(AuthException e) {
switch (e.code) { switch (e.code) {
case 'invalid_credentials': case 'invalid_credentials':
return 'Email ou palavra-passe incorretos.'; return AuthStrings.invalidCredentials;
case 'user_not_found': case 'user_not_found':
return 'Utilizador não encontrado.'; return AuthStrings.userNotFound;
case 'email_exists': case 'email_exists':
case 'user_already_exists': case 'user_already_exists':
return 'Este email já está em uso.'; return AuthStrings.emailAlreadyInUse;
case 'weak_password': case 'weak_password':
return 'Palavra-passe fraca. Utilize pelo menos 6 caracteres.'; return AuthStrings.weakPassword;
default: default:
return e.message; return e.message;
} }
@@ -178,7 +180,7 @@ class _HomeScreenState extends State<HomeScreen> {
body: Stack( body: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), Positioned.fill(child: Container(color: AppColors.background)),
const LiquidWavesBackground(), const LiquidWavesBackground(),
SafeArea( SafeArea(
child: LayoutBuilder( child: LayoutBuilder(
@@ -195,7 +197,7 @@ class _HomeScreenState extends State<HomeScreen> {
children: [ children: [
const FadeSlideIn( const FadeSlideIn(
child: Text( child: Text(
'Check-Teeth Kids', AuthStrings.appTitle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 26, fontSize: 26,
@@ -210,7 +212,7 @@ class _HomeScreenState extends State<HomeScreen> {
FadeSlideIn( FadeSlideIn(
delay: const Duration(milliseconds: 80), delay: const Duration(milliseconds: 80),
child: Text( child: Text(
'Deteção e Prevenção da Má Oclusão Infantil', AuthStrings.appSubtitle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 13.5, fontSize: 13.5,
@@ -279,14 +281,14 @@ class _AuthTabSwitch extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: _AuthTab( child: _AuthTab(
label: 'Entrar', label: AuthStrings.login,
selected: isLogin, selected: isLogin,
onTap: () => onChanged(true), onTap: () => onChanged(true),
), ),
), ),
Expanded( Expanded(
child: _AuthTab( child: _AuthTab(
label: 'Criar Conta', label: AuthStrings.createAccount,
selected: !isLogin, selected: !isLogin,
onTap: () => onChanged(false), onTap: () => onChanged(false),
), ),
@@ -376,17 +378,17 @@ class _AuthForm extends StatelessWidget {
children: [ children: [
_AuthTextField( _AuthTextField(
controller: nameController, controller: nameController,
hintText: 'Introduza o seu nome', hintText: AuthStrings.nameHint,
icon: Icons.person_outline_rounded, icon: Icons.person_outline_rounded,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,
inputFormatters: [CapitalizeFirstLetterFormatter()], inputFormatters: [CapitalizeFirstLetterFormatter()],
validator: (v) { validator: (v) {
final value = (v ?? '').trim(); final value = (v ?? '').trim();
if (value.isEmpty) return 'Indique o seu nome'; if (value.isEmpty) return AuthStrings.nameRequired;
if (value.length < 2) return 'Nome muito curto'; if (value.length < 2) return AuthStrings.nameTooShort;
if (!_namePattern.hasMatch(value)) { if (!_namePattern.hasMatch(value)) {
return 'O nome não pode conter números'; return AuthStrings.nameNoNumbers;
} }
return null; return null;
}, },
@@ -398,28 +400,28 @@ class _AuthForm extends StatelessWidget {
), ),
_AuthTextField( _AuthTextField(
controller: emailController, controller: emailController,
hintText: 'Introduza o seu email', hintText: AuthStrings.emailHint,
icon: Icons.mail_outline_rounded, icon: Icons.mail_outline_rounded,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
validator: (v) { validator: (v) {
final value = (v ?? '').trim(); final value = (v ?? '').trim();
if (value.isEmpty) return 'Indique o seu email'; if (value.isEmpty) return AuthStrings.emailRequired;
if (!value.contains('@')) return 'Email inválido'; if (!value.contains('@')) return AuthStrings.emailInvalid;
return null; return null;
}, },
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_AuthTextField( _AuthTextField(
controller: passwordController, controller: passwordController,
hintText: 'Introduza a sua palavra-passe', hintText: AuthStrings.passwordHint,
icon: Icons.lock_outline_rounded, icon: Icons.lock_outline_rounded,
obscureText: true, obscureText: true,
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
validator: (v) { validator: (v) {
final value = v ?? ''; final value = v ?? '';
if (value.isEmpty) return 'Indique a sua palavra-passe'; if (value.isEmpty) return AuthStrings.passwordRequired;
if (value.length < 6) return 'Mínimo de 6 caracteres'; if (value.length < 6) return AuthStrings.passwordTooShort;
return null; return null;
}, },
), ),
@@ -471,7 +473,7 @@ class _AuthForm extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(isLogin ? 'Entrar' : 'Criar Conta'), Text(isLogin ? AuthStrings.login : AuthStrings.createAccount),
const SizedBox(width: 8), const SizedBox(width: 8),
const Icon(Icons.arrow_forward_rounded, size: 18), const Icon(Icons.arrow_forward_rounded, size: 18),
], ],

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'colors/app_colors.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
@@ -17,7 +18,8 @@ import 'screens/video_screen.dart';
import 'watched_videos_prefs.dart'; import 'watched_videos_prefs.dart';
import 'widgets/animated_nav_icon.dart'; import 'widgets/animated_nav_icon.dart';
import 'widgets/app_dialogs.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/entrance.dart';
import 'widgets/liquid_waves_background.dart'; import 'widgets/liquid_waves_background.dart';
import 'widgets/name_input_formatter.dart'; import 'widgets/name_input_formatter.dart';
@@ -58,7 +60,7 @@ class LoggedHomeScreen extends StatefulWidget {
class _LoggedHomeScreenState extends State<LoggedHomeScreen> class _LoggedHomeScreenState extends State<LoggedHomeScreen>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
static const Color _teal = Color(0xFF2F9E94); static const Color _teal = AppColors.teal;
static const String _kPendingQuizScopeKey = 'pending_quiz_scope_v1'; static const String _kPendingQuizScopeKey = 'pending_quiz_scope_v1';
static const double _collapsedAppBarHeight = kToolbarHeight; static const double _collapsedAppBarHeight = kToolbarHeight;
@@ -77,7 +79,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
bool _brushingDailyLimitReached = false; bool _brushingDailyLimitReached = false;
int? _watchedVideoCount; int? _watchedVideoCount;
String _cachedUserName = 'Sem nome'; String _cachedUserName = HomeStrings.noName;
String? _cachedPhotoUrl; String? _cachedPhotoUrl;
@override @override
@@ -268,9 +270,9 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
String _greeting() { String _greeting() {
final hour = DateTime.now().hour; final hour = DateTime.now().hour;
if (hour < 12) return 'Bom dia'; if (hour < 12) return HomeStrings.goodMorning;
if (hour < 18) return 'Boa tarde'; if (hour < 18) return HomeStrings.goodAfternoon;
return 'Boa noite'; return HomeStrings.goodEvening;
} }
/// Fundo comum (cor sólida + ondas decorativas) atrás de qualquer aba, com /// Fundo comum (cor sólida + ondas decorativas) atrás de qualquer aba, com
@@ -279,7 +281,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
return Stack( return Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), Positioned.fill(child: Container(color: AppColors.background)),
const LiquidWavesBackground(), const LiquidWavesBackground(),
SafeArea( SafeArea(
top: false, top: false,
@@ -303,7 +305,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
setState(() => _index = i); setState(() => _index = i);
}, },
backgroundColor: const Color(0xFFFAFAF7), backgroundColor: AppColors.background,
selectedItemColor: _teal, selectedItemColor: _teal,
unselectedItemColor: Colors.black54, unselectedItemColor: Colors.black54,
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
@@ -313,21 +315,21 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
icon: Icons.home_rounded, icon: Icons.home_rounded,
selected: _index == 0, selected: _index == 0,
), ),
label: 'Início', label: HomeStrings.navHome,
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: AnimatedNavIcon( icon: AnimatedNavIcon(
icon: Icons.person_rounded, icon: Icons.person_rounded,
selected: _index == 1, selected: _index == 1,
), ),
label: 'Perfil', label: HomeStrings.navProfile,
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: AnimatedNavIcon( icon: AnimatedNavIcon(
icon: Icons.settings_rounded, icon: Icons.settings_rounded,
selected: _index == 2, selected: _index == 2,
), ),
label: 'Ajustes', label: HomeStrings.navSettings,
), ),
], ],
); );
@@ -415,13 +417,13 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
// quando o número de perguntas muda. // quando o número de perguntas muda.
value: hasScore ? result.signs : null, value: hasScore ? result.signs : null,
max: hasScore ? kSignsMax : null, max: hasScore ? kSignsMax : null,
label: 'Sinais de má\noclusão', label: HomeStrings.signsGaugeLabel,
), ),
const SizedBox(width: 18), const SizedBox(width: 18),
_RiskArcGauge( _RiskArcGauge(
value: hasScore ? result.factors : null, value: hasScore ? result.factors : null,
max: hasScore ? kFactorsMax : null, max: hasScore ? kFactorsMax : null,
label: 'Fatores de risco', label: HomeStrings.factorsGaugeLabel,
), ),
], ],
), ),
@@ -510,7 +512,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
); );
} }
final String title = _index == 1 ? 'Perfil' : 'Configurações'; final String title = _index == 1 ? HomeStrings.navProfile : HomeStrings.settingsTitle;
return Scaffold( return Scaffold(
appBar: PreferredSize( appBar: PreferredSize(
@@ -668,7 +670,7 @@ class _RiskArcGaugePainter extends CustomPainter {
..strokeCap = StrokeCap.round; ..strokeCap = StrokeCap.round;
final progressPaint = Paint() final progressPaint = Paint()
..color = const Color(0xFFFF9AD0) ..color = AppColors.pinkLight
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = strokeWidth ..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round; ..strokeCap = StrokeCap.round;
@@ -754,7 +756,7 @@ class _InicioTab extends StatelessWidget {
children: [ children: [
if (selectedChildName.isNotEmpty) ...[ if (selectedChildName.isNotEmpty) ...[
FadeSlideIn( FadeSlideIn(
child: _HomeSectionLabel('Para $selectedChildName'), child: _HomeSectionLabel(HomeStrings.forChild(selectedChildName)),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
@@ -781,7 +783,7 @@ class _InicioTab extends StatelessWidget {
const SizedBox(height: 20), const SizedBox(height: 20),
FadeSlideIn( FadeSlideIn(
delay: const Duration(milliseconds: 110), delay: const Duration(milliseconds: 110),
child: const _HomeSectionLabel('Vídeos educativos'), child: const _HomeSectionLabel(HomeStrings.educationalVideos),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
FadeSlideIn( FadeSlideIn(
@@ -806,7 +808,7 @@ class _InicioTab extends StatelessWidget {
delay: const Duration(milliseconds: 150), delay: const Duration(milliseconds: 150),
child: Center( child: Center(
child: Text( child: Text(
'Mais funcionalidades em breve', HomeStrings.moreFeaturesSoon,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
fontSize: 12.5, fontSize: 12.5,
@@ -840,7 +842,7 @@ class _InicioTab extends StatelessWidget {
if (context.mounted) { if (context.mounted) {
showPillSnackBar( showPillSnackBar(
context, context,
'Já registou as ${BrushingPrefs.maxPerDay} escovagens de hoje!', HomeStrings.brushingLimitReached(BrushingPrefs.maxPerDay),
); );
} }
return; return;
@@ -848,7 +850,7 @@ class _InicioTab extends StatelessWidget {
await BrushingPrefs.logToday(scope); await BrushingPrefs.logToday(scope);
await state?.refreshStats(); await state?.refreshStats();
if (context.mounted) { if (context.mounted) {
showPillSnackBar(context, 'Escovagem registada!'); showPillSnackBar(context, HomeStrings.brushingLogged);
} }
} }
} }
@@ -945,11 +947,11 @@ class _StatsRow extends StatelessWidget {
scale: 0.97, scale: 0.97,
child: _StatCard( child: _StatCard(
icon: Icons.brush_rounded, icon: Icons.brush_rounded,
iconColor: const Color(0xFFFF55A7), iconColor: AppColors.pink,
value: brushingCount == null value: brushingCount == null
? '--' ? '--'
: '$brushingCount/$weeklyGoal', : '$brushingCount/$weeklyGoal',
label: 'Escovagens esta semana', label: HomeStrings.brushingThisWeek,
done: brushedToday, done: brushedToday,
onTap: onTapBrushing, onTap: onTapBrushing,
), ),
@@ -959,9 +961,9 @@ class _StatsRow extends StatelessWidget {
Expanded( Expanded(
child: _StatCard( child: _StatCard(
icon: Icons.movie_filter_rounded, icon: Icons.movie_filter_rounded,
iconColor: const Color(0xFF8E7CC3), iconColor: AppColors.purple,
value: watchedCount == null ? '--' : '$watchedCount', value: watchedCount == null ? '--' : '$watchedCount',
label: 'Episódios completos', label: HomeStrings.completedEpisodes,
), ),
), ),
], ],
@@ -1022,7 +1024,7 @@ class _StatCard extends StatelessWidget {
width: 16, width: 16,
height: 16, height: 16,
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Color(0xFF2F9E94), color: AppColors.teal,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon( child: const Icon(
@@ -1067,7 +1069,7 @@ Future<Map<String, dynamic>?> _createChildViaSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
showDragHandle: true, showDragHandle: true,
backgroundColor: const Color(0xFFFFE6F1), backgroundColor: AppColors.pinkBackground,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)), borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
), ),
@@ -1084,9 +1086,18 @@ Future<Map<String, dynamic>?> _createChildViaSheet(
.select() .select()
.single(); .single();
return inserted; 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) { } catch (e) {
if (!context.mounted) return null; if (!context.mounted) return null;
showPillSnackBar(context, 'Erro ao adicionar criança: $e'); showPillSnackBar(context, HomeStrings.errorAddingChild(e));
return null; return null;
} }
} }
@@ -1097,9 +1108,9 @@ Future<Map<String, dynamic>?> _requireFirstChild(
) async { ) async {
final proceed = await showConfirmDialog( final proceed = await showConfirmDialog(
context, context,
title: 'Registe uma criança', title: HomeStrings.registerAChild,
message: 'Antes de iniciar o quiz, adicione uma criança ao seu perfil.', message: HomeStrings.registerAChildMessage,
confirmLabel: 'Adicionar criança', confirmLabel: HomeStrings.addChild,
); );
if (proceed != true) return null; if (proceed != true) return null;
@@ -1111,12 +1122,12 @@ Future<Map<String, dynamic>?> _pickChildSheet(
BuildContext context, BuildContext context,
List<Map<String, dynamic>> children, List<Map<String, dynamic>> children,
) { ) {
const Color teal = Color(0xFF2F9E94); const Color teal = AppColors.teal;
return showModalBottomSheet<Map<String, dynamic>?>( return showModalBottomSheet<Map<String, dynamic>?>(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
showDragHandle: true, showDragHandle: true,
backgroundColor: const Color(0xFFFFE6F1), backgroundColor: AppColors.pinkBackground,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)), borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
), ),
@@ -1132,12 +1143,12 @@ Future<Map<String, dynamic>?> _pickChildSheet(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const Text( const Text(
'Para qual criança é o quiz?', HomeStrings.whichChildIsTheQuizFor,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Color(0xFFFF55A7), color: AppColors.pink,
), ),
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
@@ -1149,7 +1160,7 @@ Future<Map<String, dynamic>?> _pickChildSheet(
final name = (c['name'] ?? '').toString(); final name = (c['name'] ?? '').toString();
final age = _childAge(c); final age = _childAge(c);
final label = age != null final label = age != null
? '$name$age anos' ? HomeStrings.childNameWithAge(name, age)
: name; : name;
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(bottom: 10),
@@ -1194,7 +1205,7 @@ Future<Map<String, dynamic>?> _pickChildSheet(
const SizedBox(height: 4), const SizedBox(height: 4),
TextButton( TextButton(
onPressed: () => Navigator.of(ctx).pop(null), 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) { Widget build(BuildContext context) {
return Material( return Material(
elevation: 18, elevation: 18,
shadowColor: const Color(0xFFFF55A7).withValues(alpha: 0.45), shadowColor: AppColors.pink.withValues(alpha: 0.45),
borderRadius: BorderRadius.circular(28), borderRadius: BorderRadius.circular(28),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
color: Colors.transparent, color: Colors.transparent,
@@ -1300,7 +1311,7 @@ class _HeroQuizCard extends StatelessWidget {
), ),
SizedBox(width: 4), SizedBox(width: 4),
Text( Text(
'Avaliação gratuita', HomeStrings.freeAssessmentBadge,
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
@@ -1328,7 +1339,7 @@ class _HeroQuizCard extends StatelessWidget {
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
const Text( const Text(
'Avaliação de má oclusão dentária', HomeStrings.assessmentTitle,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
fontSize: 22, fontSize: 22,
@@ -1338,7 +1349,7 @@ class _HeroQuizCard extends StatelessWidget {
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
Text( Text(
'29 perguntas rápidas · menos de 3 minutos', HomeStrings.assessmentSubtitle,
style: TextStyle( style: TextStyle(
color: Colors.white.withValues(alpha: 0.92), color: Colors.white.withValues(alpha: 0.92),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -1356,7 +1367,7 @@ class _HeroQuizCard extends StatelessWidget {
child: FilledButton.icon( child: FilledButton.icon(
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: Colors.white, backgroundColor: Colors.white,
foregroundColor: const Color(0xFFFF55A7), foregroundColor: AppColors.pink,
shape: const StadiumBorder(), shape: const StadiumBorder(),
elevation: 6, elevation: 6,
shadowColor: Colors.black.withValues(alpha: 0.25), shadowColor: Colors.black.withValues(alpha: 0.25),
@@ -1367,7 +1378,7 @@ class _HeroQuizCard extends StatelessWidget {
), ),
onPressed: onStartQuiz, onPressed: onStartQuiz,
icon: const Icon(Icons.play_arrow_rounded), 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) { Widget build(BuildContext context) {
return Material( return Material(
elevation: 14, elevation: 14,
shadowColor: const Color(0xFF2F9E94).withValues(alpha: 0.38), shadowColor: AppColors.teal.withValues(alpha: 0.38),
borderRadius: BorderRadius.circular(28), borderRadius: BorderRadius.circular(28),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
color: Colors.transparent, color: Colors.transparent,
@@ -1465,7 +1476,7 @@ class _VideoLibraryCard extends StatelessWidget {
), ),
SizedBox(width: 4), SizedBox(width: 4),
Text( Text(
'Biblioteca de vídeos', HomeStrings.videoLibraryBadge,
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
@@ -1477,7 +1488,7 @@ class _VideoLibraryCard extends StatelessWidget {
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
const Text( const Text(
'Vídeos educativos', HomeStrings.educationalVideos,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
fontSize: 22, fontSize: 22,
@@ -1488,8 +1499,11 @@ class _VideoLibraryCard extends StatelessWidget {
const SizedBox(height: 5), const SizedBox(height: 5),
Text( Text(
watchedCount > 0 watchedCount > 0
? '$watchedCount episódio${watchedCount == 1 ? '' : 's'} completo${watchedCount == 1 ? '' : 's'} · ${videoList.length} no total' ? HomeStrings.watchedEpisodesSummary(
: '${videoList.length} episódios sobre saúde oral para toda a família', watchedCount,
videoList.length,
)
: HomeStrings.allEpisodesSummary(videoList.length),
style: TextStyle( style: TextStyle(
color: Colors.white.withValues(alpha: 0.92), color: Colors.white.withValues(alpha: 0.92),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -1503,7 +1517,7 @@ class _VideoLibraryCard extends StatelessWidget {
child: FilledButton.icon( child: FilledButton.icon(
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: Colors.white, backgroundColor: Colors.white,
foregroundColor: const Color(0xFF2F9E94), foregroundColor: AppColors.teal,
shape: const StadiumBorder(), shape: const StadiumBorder(),
textStyle: const TextStyle( textStyle: const TextStyle(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
@@ -1512,7 +1526,7 @@ class _VideoLibraryCard extends StatelessWidget {
), ),
onPressed: onTap, onPressed: onTap,
icon: const Icon(Icons.video_library_rounded), 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<ImageSource>( final source = await showModalBottomSheet<ImageSource>(
context: context, context: context,
showDragHandle: true, showDragHandle: true,
backgroundColor: const Color(0xFFFFE6F1), backgroundColor: AppColors.pinkBackground,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)), borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
), ),
@@ -1610,12 +1624,12 @@ class _PerfilTabState extends State<_PerfilTab> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const Text( const Text(
'Foto de perfil', HomeStrings.profilePhoto,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Color(0xFFFF55A7), color: AppColors.pink,
), ),
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
@@ -1638,7 +1652,7 @@ class _PerfilTabState extends State<_PerfilTab> {
), ),
onPressed: () => onPressed: () =>
Navigator.of(ctx).pop(ImageSource.camera), 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: () => onPressed: () =>
Navigator.of(ctx).pop(ImageSource.gallery), 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, height: 42,
child: TextButton( child: TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: const Color(0xFF2F9E94), foregroundColor: AppColors.teal,
), ),
onPressed: () => Navigator.of(ctx).pop(), 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) { } catch (e) {
if (!context.mounted) return; if (!context.mounted) return;
showPillSnackBar(context, 'Erro ao enviar foto: $e'); showPillSnackBar(context, HomeStrings.errorUploadingPhoto(e));
} finally { } finally {
if (mounted) setState(() => _updatingPhoto = false); if (mounted) setState(() => _updatingPhoto = false);
} }
@@ -1729,11 +1743,10 @@ class _PerfilTabState extends State<_PerfilTab> {
}) async { }) async {
final confirmed = await showConfirmDialog( final confirmed = await showConfirmDialog(
context, context,
title: 'Remover criança', title: HomeStrings.removeChild,
message: message: HomeStrings.removeChildConfirmMessage(childName),
'Tem a certeza que quer remover "$childName"? Esta ação não pode ser desfeita.', confirmLabel: HomeStrings.remove,
confirmLabel: 'Remover', confirmColor: AppColors.pink,
confirmColor: const Color(0xFFFF55A7),
); );
if (confirmed != true) return; if (confirmed != true) return;
@@ -1747,15 +1760,15 @@ class _PerfilTabState extends State<_PerfilTab> {
.select('id'); .select('id');
if (deleted.isEmpty) { if (deleted.isEmpty) {
throw StateError('Sem permissão para remover esta criança.'); throw StateError(HomeStrings.noPermissionToRemoveChild);
} }
widget.onChildSelected(0, null, null); widget.onChildSelected(0, null, null);
await _loadPerfilData(); await _loadPerfilData();
if (context.mounted) showPillSnackBar(context, 'Criança removida'); if (context.mounted) showPillSnackBar(context, HomeStrings.childRemoved);
} catch (e) { } catch (e) {
if (context.mounted) { 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, context: context,
builder: (ctx) { builder: (ctx) {
return AlertDialog( return AlertDialog(
backgroundColor: const Color(0xFFFFE6F1), backgroundColor: AppColors.pinkBackground,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
title: Text( title: Text(
'Meta semanal de $childName', HomeStrings.weeklyGoalOf(childName),
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Color(0xFFFF55A7), color: AppColors.pink,
), ),
), ),
content: TextField( content: TextField(
@@ -1789,14 +1802,14 @@ class _PerfilTabState extends State<_PerfilTab> {
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
autofocus: true, autofocus: true,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Escovagens por semana', labelText: HomeStrings.brushingsPerWeek,
helperText: 'Entre 1 e 21 (até 3 por dia)', helperText: HomeStrings.brushingGoalRange,
), ),
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(ctx).pop(), onPressed: () => Navigator.of(ctx).pop(),
child: const Text('Cancelar'), child: const Text(HomeStrings.cancel),
), ),
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
@@ -1813,7 +1826,7 @@ class _PerfilTabState extends State<_PerfilTab> {
if (value == null || value < 1 || value > 21) return; if (value == null || value < 1 || value > 21) return;
Navigator.of(ctx).pop(value); 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, context: context,
isScrollControlled: true, isScrollControlled: true,
showDragHandle: true, showDragHandle: true,
backgroundColor: const Color(0xFFFFE6F1), backgroundColor: AppColors.pinkBackground,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)), borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
), ),
@@ -1857,7 +1870,9 @@ class _PerfilTabState extends State<_PerfilTab> {
if (!mounted) return; if (!mounted) return;
await _loadPerfilData(); await _loadPerfilData();
if (context.mounted) showPillSnackBar(context, 'Criança adicionada'); if (context.mounted) {
showPillSnackBar(context, HomeStrings.childAdded);
}
if (mounted) { if (mounted) {
setState(() => _addingChild = false); setState(() => _addingChild = false);
@@ -1868,9 +1883,9 @@ class _PerfilTabState extends State<_PerfilTab> {
final addMore = await showConfirmDialog( final addMore = await showConfirmDialog(
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
context, context,
title: 'Adicionar outra criança?', title: HomeStrings.addAnotherChildQuestion,
cancelLabel: 'Agora não', cancelLabel: HomeStrings.notNow,
confirmLabel: 'Adicionar outra', confirmLabel: HomeStrings.addAnother,
); );
if (!mounted) return; if (!mounted) return;
@@ -1886,11 +1901,11 @@ class _PerfilTabState extends State<_PerfilTab> {
if (!mounted || !context.mounted) return; if (!mounted || !context.mounted) return;
showPillSnackBar( showPillSnackBar(
context, context,
'Tempo esgotado ao adicionar. Tente novamente.', HomeStrings.timeoutAdding,
); );
} catch (e) { } catch (e) {
if (!mounted || !context.mounted) return; if (!mounted || !context.mounted) return;
showPillSnackBar(context, 'Erro ao adicionar: $e'); showPillSnackBar(context, HomeStrings.errorAdding(e));
} finally { } finally {
if (mounted) setState(() => _addingChild = false); if (mounted) setState(() => _addingChild = false);
} }
@@ -1902,7 +1917,7 @@ class _PerfilTabState extends State<_PerfilTab> {
final uid = (user?.id ?? '').trim(); final uid = (user?.id ?? '').trim();
final name = (user?.userMetadata?['name'] ?? '').toString().trim(); final name = (user?.userMetadata?['name'] ?? '').toString().trim();
final email = (user?.email ?? '').trim(); final email = (user?.email ?? '').trim();
final shownName = name.isNotEmpty ? name : 'Sem nome'; final shownName = name.isNotEmpty ? name : HomeStrings.noName;
if (uid.isEmpty) { if (uid.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@@ -1912,7 +1927,7 @@ class _PerfilTabState extends State<_PerfilTab> {
return const Center( return const Center(
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 60), 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, width: 76,
height: 76, height: 76,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFFE6F1), color: AppColors.pinkBackground,
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all( border: Border.all(
color: const Color( color: AppColors.teal.withValues(
0xFF2F9E94, alpha: 0.35,
).withValues(alpha: 0.35), ),
width: 2, width: 2,
), ),
), ),
@@ -1992,7 +2007,7 @@ class _PerfilTabState extends State<_PerfilTab> {
const Icon( const Icon(
Icons.person_rounded, Icons.person_rounded,
size: 42, size: 42,
color: Color(0xFF2F9E94), color: AppColors.teal,
), ),
if (_updatingPhoto) if (_updatingPhoto)
Container( Container(
@@ -2020,7 +2035,7 @@ class _PerfilTabState extends State<_PerfilTab> {
width: 26, width: 26,
height: 26, height: 26,
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Color(0xFFFF55A7), color: AppColors.pink,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon( child: const Icon(
@@ -2046,7 +2061,7 @@ class _PerfilTabState extends State<_PerfilTab> {
style: const TextStyle( style: const TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Color(0xFFFF55A7), color: AppColors.pink,
), ),
), ),
if (profileEmail.isNotEmpty) ...[ if (profileEmail.isNotEmpty) ...[
@@ -2078,9 +2093,9 @@ class _PerfilTabState extends State<_PerfilTab> {
child: Row( child: Row(
children: [ children: [
const Text( const Text(
'Meus filhos', HomeStrings.myChildren,
style: TextStyle( style: TextStyle(
color: Color(0xFF2F9E94), color: AppColors.teal,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
fontSize: 15, fontSize: 15,
), ),
@@ -2092,15 +2107,13 @@ class _PerfilTabState extends State<_PerfilTab> {
vertical: 2, vertical: 2,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color( color: AppColors.teal.withValues(alpha: 0.10),
0xFF2F9E94,
).withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
), ),
child: Text( child: Text(
'${children.length}', '${children.length}',
style: const TextStyle( style: const TextStyle(
color: Color(0xFF2F9E94), color: AppColors.teal,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
fontSize: 12, fontSize: 12,
), ),
@@ -2126,18 +2139,18 @@ class _PerfilTabState extends State<_PerfilTab> {
width: 40, width: 40,
height: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFFE6F1), color: AppColors.pinkBackground,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: const Icon( child: const Icon(
Icons.child_care_rounded, Icons.child_care_rounded,
color: Color(0xFFFF55A7), color: AppColors.pink,
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
'Nenhuma criança adicionada ainda.', HomeStrings.noChildrenYet,
style: TextStyle( style: TextStyle(
color: Colors.black.withValues(alpha: 0.62), color: Colors.black.withValues(alpha: 0.62),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -2155,14 +2168,20 @@ class _PerfilTabState extends State<_PerfilTab> {
final childName = (c['name'] ?? '').toString().trim(); final childName = (c['name'] ?? '').toString().trim();
final childAge = _childAge(c); final childAge = _childAge(c);
final childGender = (c['gender'] ?? '').toString().trim(); final childGender = (c['gender'] ?? '').toString().trim();
final childCode = (c['child_code'] ?? '')
.toString()
.trim();
final scopeId = '${uid}_$childId'; final scopeId = '${uid}_$childId';
final title = childName.isNotEmpty final title = childName.isNotEmpty
? childName ? childName
: 'Criança ${i + 1}'; : HomeStrings.childFallbackName(i);
final subtitle = [ final subtitle = [
if (childAge != null) 'Idade: $childAge', if (childAge != null) HomeStrings.ageLabel(childAge),
if (childGender.isNotEmpty) 'Género: $childGender', if (childGender.isNotEmpty)
HomeStrings.genderLabel(childGender),
if (childCode.isNotEmpty)
HomeStrings.codeLabel(childCode),
].join(''); ].join('');
final bool selected = i == selectedIndex; final bool selected = i == selectedIndex;
@@ -2183,14 +2202,12 @@ class _PerfilTabState extends State<_PerfilTab> {
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected color: selected
? const Color(0xFFFFE6F1) ? AppColors.pinkBackground
: Colors.white, : Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all( border: Border.all(
color: selected color: selected
? const Color( ? AppColors.teal.withValues(alpha: 0.45)
0xFF2F9E94,
).withValues(alpha: 0.45)
: Colors.black.withValues(alpha: 0.10), : Colors.black.withValues(alpha: 0.10),
width: selected ? 1.6 : 1, width: selected ? 1.6 : 1,
), ),
@@ -2237,9 +2254,9 @@ class _PerfilTabState extends State<_PerfilTab> {
vertical: 8, vertical: 8,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color( color: AppColors.teal.withValues(
0xFF2F9E94, alpha: 0.10,
).withValues(alpha: 0.10), ),
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
999, 999,
), ),
@@ -2248,7 +2265,7 @@ class _PerfilTabState extends State<_PerfilTab> {
text, text,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Color(0xFF2F9E94), color: AppColors.teal,
), ),
), ),
); );
@@ -2262,9 +2279,9 @@ class _PerfilTabState extends State<_PerfilTab> {
), ),
icon: const Icon( icon: const Icon(
Icons.edit_outlined, Icons.edit_outlined,
color: Color(0xFF2F9E94), color: AppColors.teal,
), ),
tooltip: 'Meta semanal de escovagens', tooltip: HomeStrings.weeklyBrushingGoalTooltip,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
), ),
IconButton( IconButton(
@@ -2275,9 +2292,9 @@ class _PerfilTabState extends State<_PerfilTab> {
), ),
icon: const Icon( icon: const Icon(
Icons.delete_outline_rounded, Icons.delete_outline_rounded,
color: Color(0xFFFF55A7), color: AppColors.pink,
), ),
tooltip: 'Remover', tooltip: HomeStrings.remove,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
), ),
], ],
@@ -2310,7 +2327,7 @@ class _PerfilTabState extends State<_PerfilTab> {
? null ? null
: () => _addAnotherChild(context, uid), : () => _addAnotherChild(context, uid),
icon: const Icon(Icons.add_rounded), 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, height: 46,
child: OutlinedButton.icon( child: OutlinedButton.icon(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFFF55A7), foregroundColor: AppColors.pink,
side: const BorderSide( side: const BorderSide(
color: Color(0xFFFF55A7), color: AppColors.pink,
width: 1.4, width: 1.4,
), ),
shape: const StadiumBorder(), shape: const StadiumBorder(),
@@ -2334,7 +2351,7 @@ class _PerfilTabState extends State<_PerfilTab> {
await supabase.auth.signOut(); await supabase.auth.signOut();
}, },
icon: const Icon(Icons.logout_rounded), 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> { class _AddChildSheetState extends State<_AddChildSheet> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController(); final _nameController = TextEditingController();
final _codeController = TextEditingController();
DateTime? _birthDate; DateTime? _birthDate;
String? _gender; String? _gender;
String? _birthDateError; String? _birthDateError;
@@ -2365,6 +2383,7 @@ class _AddChildSheetState extends State<_AddChildSheet> {
@override @override
void dispose() { void dispose() {
_nameController.dispose(); _nameController.dispose();
_codeController.dispose();
super.dispose(); super.dispose();
} }
@@ -2375,9 +2394,9 @@ class _AddChildSheetState extends State<_AddChildSheet> {
initialDate: _birthDate ?? DateTime(now.year - 5, now.month, now.day), initialDate: _birthDate ?? DateTime(now.year - 5, now.month, now.day),
firstDate: DateTime(now.year - 17, now.month, now.day), firstDate: DateTime(now.year - 17, now.month, now.day),
lastDate: DateTime(now.year - 1, now.month, now.day), lastDate: DateTime(now.year - 1, now.month, now.day),
helpText: 'Data de nascimento', helpText: HomeStrings.birthDate,
cancelText: 'Cancelar', cancelText: HomeStrings.cancel,
confirmText: 'Confirmar', confirmText: HomeStrings.confirm,
locale: const Locale('pt', 'PT'), locale: const Locale('pt', 'PT'),
); );
if (picked == null) return; if (picked == null) return;
@@ -2391,7 +2410,7 @@ class _AddChildSheetState extends State<_AddChildSheet> {
final formOk = _formKey.currentState?.validate() ?? false; final formOk = _formKey.currentState?.validate() ?? false;
setState(() { setState(() {
_birthDateError = _birthDate == null _birthDateError = _birthDate == null
? 'Indique a data de nascimento' ? HomeStrings.birthDateRequired
: null; : null;
}); });
if (!formOk || _birthDate == null) return; if (!formOk || _birthDate == null) return;
@@ -2399,6 +2418,7 @@ class _AddChildSheetState extends State<_AddChildSheet> {
'name': _nameController.text.trim(), 'name': _nameController.text.trim(),
'birth_date': _birthDate!.toIso8601String().split('T').first, 'birth_date': _birthDate!.toIso8601String().split('T').first,
'gender': (_gender ?? '').trim(), 'gender': (_gender ?? '').trim(),
'child_code': _codeController.text.trim(),
}); });
} }
@@ -2413,12 +2433,12 @@ class _AddChildSheetState extends State<_AddChildSheet> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const Text( const Text(
'Adicionar outra criança', HomeStrings.addAnotherChildTitle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Color(0xFFFF55A7), color: AppColors.pink,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -2440,24 +2460,40 @@ class _AddChildSheetState extends State<_AddChildSheet> {
textCapitalization: TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,
inputFormatters: [CapitalizeFirstLetterFormatter()], inputFormatters: [CapitalizeFirstLetterFormatter()],
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Nome da criança', labelText: HomeStrings.childName,
), ),
validator: (v) { validator: (v) {
final value = (v ?? '').trim(); final value = (v ?? '').trim();
if (value.isEmpty) return 'Indique o nome'; if (value.isEmpty) return HomeStrings.nameRequired;
if (value.length < 2) return 'Nome muito curto'; if (value.length < 2) return HomeStrings.nameTooShort;
if (!_namePattern.hasMatch(value)) { if (!_namePattern.hasMatch(value)) {
return 'O nome não pode conter números'; return HomeStrings.nameNoNumbers;
} }
return null; 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( InkWell(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
onTap: _pickBirthDate, onTap: _pickBirthDate,
child: InputDecorator( child: InputDecorator(
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Data de nascimento', labelText: HomeStrings.birthDate,
errorText: _birthDateError, errorText: _birthDateError,
suffixIcon: const Icon( suffixIcon: const Icon(
Icons.calendar_today_rounded, Icons.calendar_today_rounded,
@@ -2466,7 +2502,7 @@ class _AddChildSheetState extends State<_AddChildSheet> {
), ),
child: Text( child: Text(
_birthDate == null _birthDate == null
? 'Selecione a data' ? HomeStrings.selectDate
: '${_birthDate!.day.toString().padLeft(2, '0')}/' : '${_birthDate!.day.toString().padLeft(2, '0')}/'
'${_birthDate!.month.toString().padLeft(2, '0')}/' '${_birthDate!.month.toString().padLeft(2, '0')}/'
'${_birthDate!.year}', '${_birthDate!.year}',
@@ -2482,20 +2518,20 @@ class _AddChildSheetState extends State<_AddChildSheet> {
initialValue: _gender, initialValue: _gender,
items: const [ items: const [
DropdownMenuItem( DropdownMenuItem(
value: 'Masculino', value: HomeStrings.male,
child: Text('Masculino'), child: Text(HomeStrings.male),
), ),
DropdownMenuItem( DropdownMenuItem(
value: 'Feminino', value: HomeStrings.female,
child: Text('Feminino'), child: Text(HomeStrings.female),
), ),
DropdownMenuItem(value: 'Outro', child: Text('Outro')), DropdownMenuItem(value: HomeStrings.other, child: Text(HomeStrings.other)),
], ],
onChanged: (v) => setState(() => _gender = v), onChanged: (v) => setState(() => _gender = v),
decoration: const InputDecoration(labelText: 'Género'), decoration: const InputDecoration(labelText: HomeStrings.gender),
validator: (v) { validator: (v) {
if (v == null || v.trim().isEmpty) { if (v == null || v.trim().isEmpty) {
return 'Selecione o género'; return HomeStrings.genderRequired;
} }
return null; return null;
}, },
@@ -2512,7 +2548,7 @@ class _AddChildSheetState extends State<_AddChildSheet> {
height: 44, height: 44,
child: TextButton( child: TextButton(
onPressed: () => Navigator.of(context).pop(null), 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, onPressed: _submit,
child: const Text('Adicionar'), child: const Text(HomeStrings.add),
), ),
), ),
), ),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'colors/app_colors.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
@@ -48,8 +49,8 @@ class MyApp extends StatelessWidget {
title: 'Check-Teeth Kids', title: 'Check-Teeth Kids',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2F9E94)), colorScheme: ColorScheme.fromSeed(seedColor: AppColors.teal),
scaffoldBackgroundColor: const Color(0xFFFAFAF7), scaffoldBackgroundColor: AppColors.background,
useMaterial3: true, useMaterial3: true,
// Sem isto, o Material 3 aplica por padrão uma sobreposição de cor // Sem isto, o Material 3 aplica por padrão uma sobreposição de cor
// (surfaceTintColor) e uma elevação extra ao rolar (scrolledUnderElevation) // (surfaceTintColor) e uma elevação extra ao rolar (scrolledUnderElevation)

View File

@@ -1,4 +1,7 @@
import 'package:flutter/material.dart'; 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_checklist_screen.dart';
import 'quiz_question_screen.dart'; import 'quiz_question_screen.dart';
@@ -56,11 +59,11 @@ Route<void> quizPageRoute({required WidgetBuilder builder}) {
Route<void> quizStartRoute({String? scopeId}) { Route<void> quizStartRoute({String? scopeId}) {
return quizPageRoute( return quizPageRoute(
builder: (_) => QuizChecklistScreen( builder: (_) => QuizChecklistScreen(
heading: 'Vamos ajudá-lo/a a compreender:', heading: QuizStrings.checklistHeading,
items: const [ items: const [
'Sinais de alerta podem passar despercebidos', QuizStrings.checklistItem1,
'Prevenir é melhor do que tratar', QuizStrings.checklistItem2,
'Quando deve procurar um dentista (urgência vs vigilância)', QuizStrings.checklistItem3,
], ],
onAdvance: (context) => Navigator.of(context).pushReplacement( onAdvance: (context) => Navigator.of(context).pushReplacement(
quizPageRoute(builder: (_) => Quiz1Screen(scopeId: scopeId)), quizPageRoute(builder: (_) => Quiz1Screen(scopeId: scopeId)),
@@ -83,25 +86,25 @@ class Quiz1Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 1/29', title: QuizUiStrings.quizProgress(1, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
question: 'O seu filho/a tem problemas respiratórios diagnosticados?', question: QuizStrings.q1Question,
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Problemas respiratórios diagnosticados', description: QuizStrings.q1YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Sem problemas respiratórios diagnosticados', description: QuizStrings.q1NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
QuizAnswer( QuizAnswer(
title: 'Não sei', title: QuizStrings.dontKnow,
description: 'Não tenho a certeza', description: QuizStrings.dontKnowDescription,
weight: 1, weight: 1,
value: 'nao_sei', value: 'nao_sei',
helpVideoId: 6, helpVideoId: 6,
@@ -127,25 +130,25 @@ class Quiz2Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 2/29', title: QuizUiStrings.quizProgress(2, 29),
fallbackColor: const Color(0xFF2F9E94), fallbackColor: AppColors.teal,
question: 'O seu filho/a respira habitualmente pela boca?', question: QuizStrings.q2Question,
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Respira habitualmente pela boca', description: QuizStrings.q2YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não respira habitualmente pela boca', description: QuizStrings.q2NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
QuizAnswer( QuizAnswer(
title: 'Não sei', title: QuizStrings.dontKnow,
description: 'Não tenho a certeza', description: QuizStrings.dontKnowDescription,
weight: 1, weight: 1,
value: 'nao_sei', value: 'nao_sei',
helpVideoId: 10, helpVideoId: 10,
@@ -171,25 +174,25 @@ class Quiz3Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 3/29', title: QuizUiStrings.quizProgress(3, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
question: 'O seu filho/a ressona habitualmente durante a noite?', question: QuizStrings.q3Question,
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Ressonar habitualmente durante a noite', description: QuizStrings.q3YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não ressona habitualmente', description: QuizStrings.q3NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
QuizAnswer( QuizAnswer(
title: 'Não sei', title: QuizStrings.dontKnow,
description: 'Não tenho a certeza', description: QuizStrings.dontKnowDescription,
weight: 1, weight: 1,
value: 'nao_sei', value: 'nao_sei',
helpVideoId: 8, helpVideoId: 8,
@@ -215,25 +218,25 @@ class Quiz4Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 4/29', title: QuizUiStrings.quizProgress(4, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
question: 'O seu filho/a sente habitualmente o nariz "tapado"?', question: QuizStrings.q4Question,
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Sente habitualmente o nariz tapado', description: QuizStrings.q4YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não sente habitualmente o nariz tapado', description: QuizStrings.q4NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
QuizAnswer( QuizAnswer(
title: 'Não sei', title: QuizStrings.dontKnow,
description: 'Não tenho a certeza', description: QuizStrings.dontKnowDescription,
weight: 1, weight: 1,
value: 'nao_sei', value: 'nao_sei',
helpVideoId: 1, helpVideoId: 1,
@@ -259,27 +262,25 @@ class Quiz5Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 5/29', title: QuizUiStrings.quizProgress(5, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
question: question: QuizStrings.q5Question,
'Durante o sono, o seu filho/a tem habitualmente interrupções da respiração?',
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: description: QuizStrings.q5YesDescription,
'Tem habitualmente interrupções da respiração durante o sono',
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não tem interrupções da respiração durante o sono', description: QuizStrings.q5NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
QuizAnswer( QuizAnswer(
title: 'Não sei', title: QuizStrings.dontKnow,
description: 'Não tenho a certeza', description: QuizStrings.dontKnowDescription,
weight: 1, weight: 1,
value: 'nao_sei', value: 'nao_sei',
helpVideoId: 7, helpVideoId: 7,
@@ -305,19 +306,19 @@ class Quiz6Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 6/29', title: QuizUiStrings.quizProgress(6, 29),
fallbackColor: const Color(0xFF2F9E94), fallbackColor: AppColors.teal,
question: 'O seu filho/a range os dentes com frequência?', question: QuizStrings.q6Question,
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Range os dentes com frequência', description: QuizStrings.q6YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não range os dentes com frequência', description: QuizStrings.q6NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
@@ -342,25 +343,25 @@ class Quiz7Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 7/29', title: QuizUiStrings.quizProgress(7, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
question: 'O seu filho/a habitualmente tem alergias sazonais?', question: QuizStrings.q7Question,
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Habitualmente tem alergias sazonais', description: QuizStrings.q7YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não tem alergias sazonais', description: QuizStrings.q7NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
QuizAnswer( QuizAnswer(
title: 'Não sei', title: QuizStrings.dontKnow,
description: 'Não tenho a certeza', description: QuizStrings.dontKnowDescription,
weight: 1, weight: 1,
value: 'nao_sei', value: 'nao_sei',
helpVideoId: 2, helpVideoId: 2,
@@ -386,25 +387,25 @@ class Quiz8Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 8/29', title: QuizUiStrings.quizProgress(8, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
question: 'O seu filho/a acorda com saliva seca na cara ou na almofada?', question: QuizStrings.q8Question,
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Acorda com saliva seca na cara ou na almofada', description: QuizStrings.q8YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não acorda com saliva seca', description: QuizStrings.q8NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
QuizAnswer( QuizAnswer(
title: 'Não sei', title: QuizStrings.dontKnow,
description: 'Não tenho a certeza', description: QuizStrings.dontKnowDescription,
weight: 1, weight: 1,
value: 'nao_sei', value: 'nao_sei',
helpVideoId: 9, helpVideoId: 9,
@@ -430,25 +431,25 @@ class Quiz9Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 9/29', title: QuizUiStrings.quizProgress(9, 29),
fallbackColor: const Color(0xFF2F9E94), fallbackColor: AppColors.teal,
question: 'O seu filho/a teve ou costuma ter com frequência otites?', question: QuizStrings.q9Question,
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Teve ou costuma ter com frequência otites', description: QuizStrings.q9YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não teve ou não costuma ter otites', description: QuizStrings.q9NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
QuizAnswer( QuizAnswer(
title: 'Não sei', title: QuizStrings.dontKnow,
description: 'Não tenho a certeza', description: QuizStrings.dontKnowDescription,
weight: 1, weight: 1,
value: 'nao_sei', value: 'nao_sei',
helpVideoId: 3, helpVideoId: 3,
@@ -474,25 +475,25 @@ class Quiz10Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 10/29', title: QuizUiStrings.quizProgress(10, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
question: 'O seu filho/a teve ou costuma ter com frequência amigdalites?', question: QuizStrings.q10Question,
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Teve ou costuma ter com frequência amigdalites', description: QuizStrings.q10YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não teve ou não costuma ter amigdalites', description: QuizStrings.q10NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
QuizAnswer( QuizAnswer(
title: 'Não sei', title: QuizStrings.dontKnow,
description: 'Não tenho a certeza', description: QuizStrings.dontKnowDescription,
weight: 1, weight: 1,
value: 'nao_sei', value: 'nao_sei',
helpVideoId: 4, helpVideoId: 4,
@@ -518,26 +519,25 @@ class Quiz11Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 11/29', title: QuizUiStrings.quizProgress(11, 29),
fallbackColor: const Color(0xFF2F9E94), fallbackColor: AppColors.teal,
question: question: QuizStrings.q11Question,
'O seu filho/a teve ou costuma ter com frequência bronquiolites?',
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Teve ou costuma ter com frequência bronquiolites', description: QuizStrings.q11YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não teve ou não costuma ter bronquiolites', description: QuizStrings.q11NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
QuizAnswer( QuizAnswer(
title: 'Não sei', title: QuizStrings.dontKnow,
description: 'Não tenho a certeza', description: QuizStrings.dontKnowDescription,
weight: 1, weight: 1,
value: 'nao_sei', value: 'nao_sei',
helpVideoId: 5, helpVideoId: 5,
@@ -563,19 +563,19 @@ class Quiz12Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 12/29', title: QuizUiStrings.quizProgress(12, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
question: 'O seu filho/a apresenta dificuldades a mastigar?', question: QuizStrings.q12Question,
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Apresenta dificuldades a mastigar', description: QuizStrings.q12YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não apresenta dificuldades a mastigar', description: QuizStrings.q12NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
@@ -600,19 +600,19 @@ class Quiz13Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 13/29', title: QuizUiStrings.quizProgress(13, 29),
fallbackColor: const Color(0xFF2F9E94), fallbackColor: AppColors.teal,
question: 'O seu filho/a habitualmente é lento a comer?', question: QuizStrings.q13Question,
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Habitualmente é lento a comer', description: QuizStrings.q13YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não é lento a comer', description: QuizStrings.q13NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
@@ -637,19 +637,19 @@ class Quiz14Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 14/29', title: QuizUiStrings.quizProgress(14, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
question: 'O seu filho/a habitualmente prefere comer alimentos moles?', question: QuizStrings.q14Question,
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Habitualmente prefere comer alimentos moles', description: QuizStrings.q14YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não prefere alimentos moles', description: QuizStrings.q14NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
@@ -674,25 +674,25 @@ class Quiz15Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 15/29', title: QuizUiStrings.quizProgress(15, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
question: 'Em bebé apenas foi alimentado por biberão?', question: QuizStrings.q15Question,
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Em bebé apenas foi alimentado por biberão', description: QuizStrings.q15YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não foi apenas alimentado por biberão', description: QuizStrings.q15NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
QuizAnswer( QuizAnswer(
title: 'Não sei', title: QuizStrings.dontKnow,
description: 'Não tenho a certeza', description: QuizStrings.dontKnowDescription,
weight: 1, weight: 1,
value: 'nao_sei', value: 'nao_sei',
helpVideoId: 11, helpVideoId: 11,
@@ -718,25 +718,25 @@ class Quiz16Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 16/29', title: QuizUiStrings.quizProgress(16, 29),
fallbackColor: const Color(0xFF2F9E94), fallbackColor: AppColors.teal,
question: 'O seu filho/a usa ou usou chupeta com frequência?', question: QuizStrings.q16Question,
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Usa ou usou chupeta com frequência', description: QuizStrings.q16YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não usa ou não usou chupeta com frequência', description: QuizStrings.q16NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
QuizAnswer( QuizAnswer(
title: 'Não sei', title: QuizStrings.dontKnow,
description: 'Não tenho a certeza', description: QuizStrings.dontKnowDescription,
weight: 1, weight: 1,
value: 'nao_sei', value: 'nao_sei',
helpVideoId: 12, helpVideoId: 12,
@@ -762,25 +762,25 @@ class Quiz17Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 17/29', title: QuizUiStrings.quizProgress(17, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
question: 'O seu filho/a chucha ou já chuchou o dedo com frequência?', question: QuizStrings.q17Question,
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Sim', title: QuizStrings.yes,
description: 'Chucha ou já chuchou o dedo com frequência', description: QuizStrings.q17YesDescription,
weight: 2, weight: 2,
value: 'sim', value: 'sim',
), ),
QuizAnswer( QuizAnswer(
title: 'Não', title: QuizStrings.no,
description: 'Não chucha ou não chuchou o dedo com frequência', description: QuizStrings.q17NoDescription,
weight: 1, weight: 1,
value: 'nao', value: 'nao',
), ),
QuizAnswer( QuizAnswer(
title: 'Não sei', title: QuizStrings.dontKnow,
description: 'Não tenho a certeza', description: QuizStrings.dontKnowDescription,
weight: 1, weight: 1,
value: 'nao_sei', value: 'nao_sei',
helpVideoId: 13, helpVideoId: 13,
@@ -814,23 +814,22 @@ class Quiz18Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 18/29', title: QuizUiStrings.quizProgress(18, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
answerImageAspectRatio: 1.5, answerImageAspectRatio: 1.5,
question: question: QuizStrings.q18Question,
'Qual das seguintes imagens é mais parecida com a postura do seu filho/a?',
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Postura inadequada', title: QuizStrings.q18Answer1Title,
description: 'Postura curvada, ombros e pescoço projetados', description: QuizStrings.q18Answer1Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'postura_inadequada', value: 'postura_inadequada',
imagePath: 'assets/mockup_images/0.1.png', imagePath: 'assets/mockup_images/0.1.png',
), ),
QuizAnswer( QuizAnswer(
title: 'Postura correta', title: QuizStrings.q18Answer2Title,
description: 'Postura ereta, coluna alinhada', description: QuizStrings.q18Answer2Description,
weight: 1, weight: 1,
hideTitle: true, hideTitle: true,
value: 'postura_correta', value: 'postura_correta',
@@ -857,14 +856,13 @@ class Quiz19Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 19/29', title: QuizUiStrings.quizProgress(19, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
question: question: QuizStrings.q19Question,
'Qual das seguintes imagens é mais parecida com o perfil do seu filho/a?',
answers: [ answers: [
QuizAnswer( QuizAnswer(
title: 'Perfil convexo', title: QuizStrings.q19Answer1Title,
description: 'Perfil facial convexo', description: QuizStrings.q19Answer1Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'perfil_convexo', value: 'perfil_convexo',
@@ -875,8 +873,8 @@ class Quiz19Screen extends StatelessWidget {
), ),
), ),
QuizAnswer( QuizAnswer(
title: 'Perfil reto', title: QuizStrings.q19Answer2Title,
description: 'Perfil facial reto', description: QuizStrings.q19Answer2Description,
weight: 1, weight: 1,
hideTitle: true, hideTitle: true,
value: 'perfil_reto', value: 'perfil_reto',
@@ -887,8 +885,8 @@ class Quiz19Screen extends StatelessWidget {
), ),
), ),
QuizAnswer( QuizAnswer(
title: 'Perfil côncavo', title: QuizStrings.q19Answer3Title,
description: 'Perfil facial côncavo', description: QuizStrings.q19Answer3Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'perfil_concavo', value: 'perfil_concavo',
@@ -919,21 +917,21 @@ class Quiz20Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 20/29', title: QuizUiStrings.quizProgress(20, 29),
fallbackColor: const Color(0xFF2F9E94), fallbackColor: AppColors.teal,
question: 'Qual é a posição da boca do seu filho/a habitualmente?', question: QuizStrings.q20Question,
answers: [ answers: [
const QuizAnswer( const QuizAnswer(
title: 'Boca fechada', title: QuizStrings.q20Answer1Title,
description: 'Boca fechada habitualmente', description: QuizStrings.q20Answer1Description,
weight: 1, weight: 1,
hideTitle: true, hideTitle: true,
value: 'boca_fechada', value: 'boca_fechada',
imagePath: 'assets/mockup_images/7.png', imagePath: 'assets/mockup_images/7.png',
), ),
QuizAnswer( QuizAnswer(
title: 'Boca entreaberta', title: QuizStrings.q20Answer2Title,
description: 'Boca entreaberta habitualmente', description: QuizStrings.q20Answer2Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'boca_entreaberta', value: 'boca_entreaberta',
@@ -964,14 +962,13 @@ class Quiz21Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 21/29', title: QuizUiStrings.quizProgress(21, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
question: question: QuizStrings.q21Question,
'Qual das imagens, na zona abaixo dos olhos, se assemelha mais ao seu filho/a?',
answers: [ answers: [
QuizAnswer( QuizAnswer(
title: 'Sem sinais', title: QuizStrings.q21Answer1Title,
description: 'Sem olheiras visíveis abaixo dos olhos', description: QuizStrings.q21Answer1Description,
weight: 1, weight: 1,
hideTitle: true, hideTitle: true,
value: 'sem_olheiras', value: 'sem_olheiras',
@@ -982,8 +979,8 @@ class Quiz21Screen extends StatelessWidget {
), ),
), ),
QuizAnswer( QuizAnswer(
title: 'Sinal de risco', title: QuizStrings.q21Answer2Title,
description: 'Olheiras visíveis abaixo dos olhos', description: QuizStrings.q21Answer2Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'com_olheiras', value: 'com_olheiras',
@@ -1014,15 +1011,14 @@ class Quiz22Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 22/29', title: QuizUiStrings.quizProgress(22, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
isSignQuestion: true, isSignQuestion: true,
question: question: QuizStrings.q22Question,
'Qual das imagens é mais parecida com o queixo do seu filho/a com a boca fechada?',
answers: [ answers: [
QuizAnswer( QuizAnswer(
title: 'Queixo relaxado', title: QuizStrings.q22Answer1Title,
description: 'Queixo liso e relaxado com a boca fechada', description: QuizStrings.q22Answer1Description,
weight: 1, weight: 1,
hideTitle: true, hideTitle: true,
value: 'queixo_correto', value: 'queixo_correto',
@@ -1034,8 +1030,8 @@ class Quiz22Screen extends StatelessWidget {
), ),
), ),
const QuizAnswer( const QuizAnswer(
title: 'Queixo tenso', title: QuizStrings.q22Answer2Title,
description: 'Queixo tenso/franzido com a boca fechada', description: QuizStrings.q22Answer2Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'queixo_tenso', value: 'queixo_tenso',
@@ -1070,31 +1066,30 @@ class Quiz23Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 23/29', title: QuizUiStrings.quizProgress(23, 29),
fallbackColor: const Color(0xFF2F9E94), fallbackColor: AppColors.teal,
isSignQuestion: true, isSignQuestion: true,
question: question: QuizStrings.q23Question,
'Qual das seguintes imagens se assemelha à boca do seu filho/a?',
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Dentição sobreposta', title: QuizStrings.q23Answer1Title,
description: 'Dentes sobrepostos/tortos', description: QuizStrings.q23Answer1Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'dentes_sobrepostos', value: 'dentes_sobrepostos',
imagePath: 'assets/mockup_images/10.png', imagePath: 'assets/mockup_images/10.png',
), ),
QuizAnswer( QuizAnswer(
title: 'Dentição alinhada', title: QuizStrings.q23Answer2Title,
description: 'Dentição bem alinhada, sem apinhamento', description: QuizStrings.q23Answer2Description,
weight: 1, weight: 1,
hideTitle: true, hideTitle: true,
value: 'dentes_alinhados', value: 'dentes_alinhados',
imagePath: 'assets/mockup_images/11.png', imagePath: 'assets/mockup_images/11.png',
), ),
QuizAnswer( QuizAnswer(
title: 'Dentição desalinhada', title: QuizStrings.q23Answer3Title,
description: 'Dentição desalinhada/apinhada', description: QuizStrings.q23Answer3Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'dentes_desalinhados', value: 'dentes_desalinhados',
@@ -1121,31 +1116,30 @@ class Quiz24Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 24/29', title: QuizUiStrings.quizProgress(24, 29),
fallbackColor: const Color(0xFF2F9E94), fallbackColor: AppColors.teal,
isSignQuestion: true, isSignQuestion: true,
question: question: QuizStrings.q24Question,
'Qual das seguintes imagens se assemelha à boca do seu filho/a?',
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Apinhamento moderado', title: QuizStrings.q24Answer1Title,
description: 'Dentes rodados/desalinhados de forma mais visível', description: QuizStrings.q24Answer1Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'apinhamento_moderado', value: 'apinhamento_moderado',
imagePath: 'assets/mockup_images/16.png', imagePath: 'assets/mockup_images/16.png',
), ),
QuizAnswer( QuizAnswer(
title: 'Ligeiro desalinhamento', title: QuizStrings.q24Answer2Title,
description: 'Pequeno desalinhamento ou espaço entre alguns dentes', description: QuizStrings.q24Answer2Description,
weight: 1, weight: 1,
hideTitle: true, hideTitle: true,
value: 'apinhamento_leve', value: 'apinhamento_leve',
imagePath: 'assets/mockup_images/15.png', imagePath: 'assets/mockup_images/15.png',
), ),
QuizAnswer( QuizAnswer(
title: 'Apinhamento acentuado', title: QuizStrings.q24Answer3Title,
description: 'Dentes muito sobrepostos entre si', description: QuizStrings.q24Answer3Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'apinhamento_acentuado', value: 'apinhamento_acentuado',
@@ -1172,22 +1166,21 @@ class Quiz25Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 25/29', title: QuizUiStrings.quizProgress(25, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
question: question: QuizStrings.q25Question,
'Qual das seguintes imagens se assemelha ao freio labial do seu filho/a?',
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Freio labial correto', title: QuizStrings.q25Answer1Title,
description: 'Inserção do freio labial mais alta', description: QuizStrings.q25Answer1Description,
weight: 1, weight: 1,
hideTitle: true, hideTitle: true,
value: 'freio_labial_correto', value: 'freio_labial_correto',
imagePath: 'assets/mockup_images/20.png', imagePath: 'assets/mockup_images/20.png',
), ),
QuizAnswer( QuizAnswer(
title: 'Freio labial inadequado', title: QuizStrings.q25Answer2Title,
description: 'Inserção do freio labial baixa, entre os dentes', description: QuizStrings.q25Answer2Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'freio_labial_inadequado', value: 'freio_labial_inadequado',
@@ -1214,22 +1207,21 @@ class Quiz26Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 26/29', title: QuizUiStrings.quizProgress(26, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
question: question: QuizStrings.q26Question,
'Qual das seguintes imagens se assemelha ao freio lingual do seu filho/a?',
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Freio lingual inadequado', title: QuizStrings.q26Answer1Title,
description: 'Freio lingual curto/apertado (língua em coração)', description: QuizStrings.q26Answer1Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'freio_lingual_inadequado', value: 'freio_lingual_inadequado',
imagePath: 'assets/mockup_images/17.png', imagePath: 'assets/mockup_images/17.png',
), ),
QuizAnswer( QuizAnswer(
title: 'Freio lingual correto', title: QuizStrings.q26Answer2Title,
description: 'Língua move-se livremente, sem restrição visível', description: QuizStrings.q26Answer2Description,
weight: 1, weight: 1,
hideTitle: true, hideTitle: true,
value: 'freio_lingual_correto', value: 'freio_lingual_correto',
@@ -1256,31 +1248,30 @@ class Quiz27Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 27/29', title: QuizUiStrings.quizProgress(27, 29),
fallbackColor: const Color(0xFF2F9E94), fallbackColor: AppColors.teal,
isSignQuestion: true, isSignQuestion: true,
question: question: QuizStrings.q27Question,
'Qual das seguintes imagens se assemelha com a boca do seu filho/a?',
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Dentição desalinhada', title: QuizStrings.q27Answer1Title,
description: 'Dentição desalinhada/apinhada', description: QuizStrings.q27Answer1Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'dentes_desalinhados_3', value: 'dentes_desalinhados_3',
imagePath: 'assets/mockup_images/23.JPEG', imagePath: 'assets/mockup_images/23.JPEG',
), ),
QuizAnswer( QuizAnswer(
title: 'Dentição sobreposta', title: QuizStrings.q27Answer2Title,
description: 'Dentes sobrepostos/tortos', description: QuizStrings.q27Answer2Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'dentes_sobrepostos_3', value: 'dentes_sobrepostos_3',
imagePath: 'assets/mockup_images/24.png', imagePath: 'assets/mockup_images/24.png',
), ),
QuizAnswer( QuizAnswer(
title: 'Dentição alinhada', title: QuizStrings.q27Answer3Title,
description: 'Dentição bem alinhada, sem apinhamento', description: QuizStrings.q27Answer3Description,
weight: 1, weight: 1,
hideTitle: true, hideTitle: true,
value: 'dentes_alinhados_3', value: 'dentes_alinhados_3',
@@ -1307,23 +1298,22 @@ class Quiz28Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 28/29', title: QuizUiStrings.quizProgress(28, 29),
fallbackColor: const Color(0xFF2F9E94), fallbackColor: AppColors.teal,
isSignQuestion: true, isSignQuestion: true,
question: question: QuizStrings.q28Question,
'Qual das seguintes imagens se assemelha à boca do seu filho/a?',
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Dentição alinhada', title: QuizStrings.q28Answer1Title,
description: 'Dentição bem alinhada, sem apinhamento', description: QuizStrings.q28Answer1Description,
weight: 1, weight: 1,
hideTitle: true, hideTitle: true,
value: 'dentes_alinhados_2', value: 'dentes_alinhados_2',
imagePath: 'assets/mockup_images/29.jpeg', imagePath: 'assets/mockup_images/29.jpeg',
), ),
QuizAnswer( QuizAnswer(
title: 'Dentição desalinhada', title: QuizStrings.q28Answer2Title,
description: 'Dentes sobrepostos/apinhados', description: QuizStrings.q28Answer2Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'dentes_desalinhados_2', value: 'dentes_desalinhados_2',
@@ -1350,24 +1340,23 @@ class Quiz29Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 29/29', title: QuizUiStrings.quizProgress(29, 29),
fallbackColor: const Color(0xFFFF55A7), fallbackColor: AppColors.pink,
isSignQuestion: true, isSignQuestion: true,
correctBadgeLabel: 'Posição saudável', correctBadgeLabel: QuizStrings.q29CorrectBadgeLabel,
question: question: QuizStrings.q29Question,
'Qual das seguintes imagens se assemelha ao céu da boca do seu filho/a?',
answers: const [ answers: const [
QuizAnswer( QuizAnswer(
title: 'Posição profunda incorreta', title: QuizStrings.q29Answer1Title,
description: 'Palato estreito/profundo, em forma de V', description: QuizStrings.q29Answer1Description,
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'palato_v', value: 'palato_v',
imagePath: 'assets/mockup_images/26.png', imagePath: 'assets/mockup_images/26.png',
), ),
QuizAnswer( QuizAnswer(
title: 'Posição em U saudável', title: QuizStrings.q29Answer2Title,
description: 'Palato largo, em forma de U', description: QuizStrings.q29Answer2Description,
weight: 1, weight: 1,
hideTitle: true, hideTitle: true,
value: 'palato_u', value: 'palato_u',

View File

@@ -1,11 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
import '../strings/quiz_ui_strings.dart';
import '../widgets/entrance.dart'; import '../widgets/entrance.dart';
import '../widgets/liquid_waves_background.dart'; import '../widgets/liquid_waves_background.dart';
import '../widgets/tap_bounce.dart'; import '../widgets/tap_bounce.dart';
const Color _pink = Color(0xFFFF55A7); const Color _pink = AppColors.pink;
const Color _teal = Color(0xFF2F9E94); const Color _teal = AppColors.teal;
/// Ecrã intersticial informativo, mostrado a meio do quiz para preparar o /// Ecrã intersticial informativo, mostrado a meio do quiz para preparar o
/// utilizador antes de continuar (ex.: logo a seguir a um vídeo-guia) — /// utilizador antes de continuar (ex.: logo a seguir a um vídeo-guia) —
@@ -30,7 +32,7 @@ class QuizChecklistScreen extends StatelessWidget {
body: Stack( body: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), Positioned.fill(child: Container(color: AppColors.background)),
const LiquidWavesBackground(), const LiquidWavesBackground(),
SafeArea( SafeArea(
child: Padding( child: Padding(
@@ -129,7 +131,7 @@ class QuizChecklistScreen extends StatelessWidget {
), ),
), ),
onPressed: () => onAdvance(context), onPressed: () => onAdvance(context),
child: const Text('Avançar'), child: const Text(QuizUiStrings.advance),
), ),
), ),
), ),

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../screens/video_screen.dart'; import '../screens/video_screen.dart';
import '../strings/quiz_ui_strings.dart';
import '../widgets/entrance.dart'; import '../widgets/entrance.dart';
import '../widgets/liquid_waves_background.dart'; import '../widgets/liquid_waves_background.dart';
import '../widgets/tap_bounce.dart'; import '../widgets/tap_bounce.dart';
@@ -82,7 +84,7 @@ class QuizQuestionScreen extends StatefulWidget {
this.suggestedVideoTitle, this.suggestedVideoTitle,
this.fallbackColor, this.fallbackColor,
this.answerImageAspectRatio, this.answerImageAspectRatio,
this.correctBadgeLabel = 'Posição certa', this.correctBadgeLabel = QuizUiStrings.defaultCorrectBadgeLabel,
}); });
final String title; final String title;
@@ -197,7 +199,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
body: Stack( body: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), Positioned.fill(child: Container(color: AppColors.background)),
const LiquidWavesBackground(), const LiquidWavesBackground(),
SafeArea( SafeArea(
child: Column( child: Column(
@@ -222,7 +224,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
padding: EdgeInsets.all(9), padding: EdgeInsets.all(9),
child: Icon( child: Icon(
Icons.arrow_back_rounded, Icons.arrow_back_rounded,
color: Color(0xFF2F9E94), color: AppColors.teal,
size: 20, size: 20,
), ),
), ),
@@ -241,11 +243,11 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
1.0, 1.0,
), ),
minHeight: 8, minHeight: 8,
backgroundColor: const Color( backgroundColor: AppColors.pink.withValues(
0xFFFF55A7, alpha: 0.15,
).withValues(alpha: 0.15), ),
valueColor: const AlwaysStoppedAnimation<Color>( valueColor: const AlwaysStoppedAnimation<Color>(
Color(0xFFFF55A7), AppColors.pink,
), ),
), ),
), ),
@@ -304,7 +306,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
: _FallbackIconBlock( : _FallbackIconBlock(
color: color:
widget.fallbackColor ?? widget.fallbackColor ??
const Color(0xFF2F9E94), AppColors.teal,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
if (hasSuggestedVideo) ...[ if (hasSuggestedVideo) ...[
@@ -317,7 +319,8 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
title: title:
widget widget
.suggestedVideoTitle ?? .suggestedVideoTitle ??
'Vídeo', QuizUiStrings
.videoFallbackTitle,
description: '', description: '',
videoPath: widget videoPath: widget
.suggestedVideoPath, .suggestedVideoPath,
@@ -328,13 +331,13 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
icon: const Icon( icon: const Icon(
Icons Icons
.play_circle_outline_rounded, .play_circle_outline_rounded,
color: Color(0xFF2F9E94), color: AppColors.teal,
), ),
label: Text( label: Text(
widget.suggestedVideoTitle ?? widget.suggestedVideoTitle ??
'Ver vídeo (opcional)', QuizUiStrings.watchOptionalVideo,
style: const TextStyle( style: const TextStyle(
color: Color(0xFF2F9E94), color: AppColors.teal,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
), ),
@@ -347,7 +350,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Color(0xFFFF55A7), color: AppColors.pink,
height: 1.2, height: 1.2,
), ),
), ),
@@ -355,11 +358,11 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
Text( Text(
widget.answerType == widget.answerType ==
QuizAnswerType.number QuizAnswerType.number
? 'Insira o número' ? QuizUiStrings.enterNumber
: widget.answerType == : widget.answerType ==
QuizAnswerType.yesNo QuizAnswerType.yesNo
? 'Escolha uma opção' ? QuizUiStrings.chooseOneOption
: 'Escolha apenas uma opção', : QuizUiStrings.chooseOnlyOneOption,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Colors.black.withValues( color: Colors.black.withValues(
@@ -495,9 +498,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
style: style:
FilledButton.styleFrom( FilledButton.styleFrom(
backgroundColor: backgroundColor:
const Color( AppColors.pink,
0xFFFF55A7,
),
foregroundColor: foregroundColor:
Colors.white, Colors.white,
shape: shape:
@@ -628,8 +629,8 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
}, },
child: Text( child: Text(
widget.isFinal widget.isFinal
? 'Concluir' ? QuizUiStrings.finish
: 'Avançar', : QuizUiStrings.advance,
), ),
), ),
), ),
@@ -643,9 +644,8 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
), ),
TextButton( TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: const Color( foregroundColor:
0xFF2F9E94, AppColors.teal,
),
textStyle: const TextStyle( textStyle: const TextStyle(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
@@ -663,7 +663,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
(route) => route.isFirst, (route) => route.isFirst,
), ),
child: const Text( child: const Text(
'Voltar para homepage', QuizUiStrings.backToHomepage,
), ),
), ),
], ],
@@ -724,7 +724,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Color(0xFF2F9E94), color: AppColors.teal,
), ),
decoration: const InputDecoration( decoration: const InputDecoration(
border: InputBorder.none, border: InputBorder.none,
@@ -750,10 +750,10 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
!_numberDontKnow) ...[ !_numberDontKnow) ...[
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'Um número tão alto assim não é possível.\nO máximo é $_maxTeethCount.', QuizUiStrings.teethCountTooHigh(_maxTeethCount),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
color: Color(0xFFFF55A7), color: AppColors.pink,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
fontSize: 12, fontSize: 12,
), ),
@@ -780,12 +780,12 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: _numberDontKnow color: _numberDontKnow
? const Color(0xFF2F9E94) ? AppColors.teal
: Colors.white.withValues(alpha: 0.70), : Colors.white.withValues(alpha: 0.70),
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
border: Border.all( border: Border.all(
color: _numberDontKnow color: _numberDontKnow
? const Color(0xFF2F9E94) ? AppColors.teal
: Colors.black.withValues(alpha: 0.12), : Colors.black.withValues(alpha: 0.12),
), ),
), ),
@@ -801,7 +801,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
'Não sei', QuizUiStrings.dontKnow,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 13, fontSize: 13,
@@ -828,7 +828,7 @@ class _QuizAnswerTile extends StatelessWidget {
required this.onTap, required this.onTap,
this.imageAspectRatio = 4 / 3, this.imageAspectRatio = 4 / 3,
this.reveal = false, this.reveal = false,
this.correctLabel = 'Posição certa', this.correctLabel = QuizUiStrings.defaultCorrectBadgeLabel,
}); });
final QuizAnswer answer; final QuizAnswer answer;
@@ -848,9 +848,9 @@ class _QuizAnswerTile extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isCorrect = answer.weight == 1; final bool isCorrect = answer.weight == 1;
final borderColor = reveal final borderColor = reveal
? (isCorrect ? const Color(0xFF2F9E94) : const Color(0xFFFF55A7)) ? (isCorrect ? AppColors.teal : AppColors.pink)
: selected : selected
? const Color(0xFF2F9E94) ? AppColors.teal
: Colors.black.withValues(alpha: 0.12); : Colors.black.withValues(alpha: 0.12);
final bg = selected final bg = selected
? Colors.white.withValues(alpha: 0.88) ? Colors.white.withValues(alpha: 0.88)
@@ -934,7 +934,7 @@ class _QuizAnswerTile extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
fontSize: 15, fontSize: 15,
color: Color(0xFF2F9E94), color: AppColors.teal,
), ),
), ),
], ],
@@ -955,7 +955,7 @@ class _QuizAnswerTile extends StatelessWidget {
width: 22, width: 22,
height: 22, height: 22,
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Color(0xFF2F9E94), color: AppColors.teal,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon( child: const Icon(
@@ -981,8 +981,8 @@ class _QuizAnswerTile extends StatelessWidget {
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isCorrect color: isCorrect
? const Color(0xFF2F9E94) ? AppColors.teal
: const Color(0xFFFF55A7), : AppColors.pink,
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@@ -1004,7 +1004,7 @@ class _QuizAnswerTile extends StatelessWidget {
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
isCorrect ? correctLabel : 'Posição inadequada', isCorrect ? correctLabel : QuizUiStrings.incorrectBadgeLabel,
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
@@ -1057,7 +1057,7 @@ class _QuestionReferenceImages extends StatelessWidget {
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
), ),
child: const Text( child: const Text(
'Imagem de referência', QuizUiStrings.referenceImage,
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@@ -1159,12 +1159,12 @@ class _QuizAnswerPill extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final accent = _isYes final accent = _isYes
? const Color(0xFF2F9E94) ? AppColors.teal
: _isNo : _isNo
? const Color(0xFFFF55A7) ? AppColors.pink
: Colors.black.withValues(alpha: 0.35); : Colors.black.withValues(alpha: 0.35);
final borderColor = selected final borderColor = selected
? const Color(0xFF2F9E94) ? AppColors.teal
: Colors.black.withValues(alpha: 0.10); : Colors.black.withValues(alpha: 0.10);
final helpVideo = _helpVideo; final helpVideo = _helpVideo;
final showHelp = selected && helpVideo != null; final showHelp = selected && helpVideo != null;
@@ -1228,7 +1228,7 @@ class _QuizAnswerPill extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 15, fontSize: 15,
color: Color(0xFF2F9E94), color: AppColors.teal,
), ),
), ),
), ),
@@ -1239,11 +1239,11 @@ class _QuizAnswerPill extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: selected color: selected
? const Color(0xFF2F9E94) ? AppColors.teal
: Colors.transparent, : Colors.transparent,
border: Border.all( border: Border.all(
color: selected color: selected
? const Color(0xFF2F9E94) ? AppColors.teal
: Colors.black.withValues(alpha: 0.25), : Colors.black.withValues(alpha: 0.25),
width: 1.6, width: 1.6,
), ),
@@ -1291,7 +1291,7 @@ class _HelpVideoButton extends StatelessWidget {
return TapBounce( return TapBounce(
scale: 0.97, scale: 0.97,
child: Material( child: Material(
color: const Color(0xFF2F9E94).withValues(alpha: 0.10), color: AppColors.teal.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
@@ -1302,23 +1302,23 @@ class _HelpVideoButton extends StatelessWidget {
children: [ children: [
const Icon( const Icon(
Icons.play_circle_fill_rounded, Icons.play_circle_fill_rounded,
color: Color(0xFF2F9E94), color: AppColors.teal,
size: 22, size: 22,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Text( child: Text(
'Não tem a certeza? Veja o "${video.title}" para ajudar a responder', QuizUiStrings.notSureSeeVideo(video.title),
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
fontSize: 12.5, fontSize: 12.5,
color: Color(0xFF2F9E94), color: AppColors.teal,
), ),
), ),
), ),
const Icon( const Icon(
Icons.chevron_right_rounded, Icons.chevron_right_rounded,
color: Color(0xFF2F9E94), color: AppColors.teal,
size: 20, size: 20,
), ),
], ],

View File

@@ -1,9 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
import '../main.dart' show supabase; 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/entrance.dart';
import '../widgets/liquid_waves_background.dart'; import '../widgets/liquid_waves_background.dart';
import '../widgets/tap_bounce.dart'; import '../widgets/tap_bounce.dart';
@@ -106,20 +109,8 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
MaterialPageRoute<void>( MaterialPageRoute<void>(
builder: (_) => QuizVideoGuideScreen( builder: (_) => QuizVideoGuideScreen(
youtubeId: _resultGuideYoutubeId, youtubeId: _resultGuideYoutubeId,
heading: 'Visualize o seguinte vídeo', heading: QuizResultStrings.watchTheFollowingVideo,
caption: caption: QuizResultStrings.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.',
onAdvance: (context) => onAdvance: (context) =>
Navigator.of(context).popUntil((r) => r.isFirst), Navigator.of(context).popUntil((r) => r.isFirst),
), ),
@@ -137,7 +128,7 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
body: Stack( body: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), Positioned.fill(child: Container(color: AppColors.background)),
const LiquidWavesBackground(), const LiquidWavesBackground(),
SafeArea( SafeArea(
child: Center( child: Center(
@@ -157,12 +148,12 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
const SizedBox(height: 10), const SizedBox(height: 10),
FadeSlideIn( FadeSlideIn(
child: const Text( child: const Text(
'O resultado avaliado é de:', QuizResultStrings.resultHeading,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Color(0xFFFF55A7), color: AppColors.pink,
height: 1.2, height: 1.2,
), ),
), ),
@@ -177,15 +168,16 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
_ResultRing( _ResultRing(
value: signs, value: signs,
max: kSignsMax, max: kSignsMax,
color: const Color(0xFF2F9E94), color: AppColors.teal,
label: label:
'Sinais de má\noclusão instalados', QuizResultStrings.signsRingLabel,
), ),
_ResultRing( _ResultRing(
value: factors, value: factors,
max: kFactorsMax, max: kFactorsMax,
color: const Color(0xFFFF55A7), color: AppColors.pink,
label: 'Fatores de\nrisco associados', label:
QuizResultStrings.factorsRingLabel,
), ),
], ],
), ),
@@ -194,14 +186,15 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
FadeSlideIn( FadeSlideIn(
delay: const Duration(milliseconds: 100), delay: const Duration(milliseconds: 100),
child: _ResultSection( child: _ResultSection(
heading: 'Conclusões:', heading:
body: QuizResultStrings.conclusionsHeading,
'Foram identificados $signs de $kSignsMax ' body: QuizResultStrings.conclusionsBody(
'sinais de má oclusão já instalada e ' signs: signs,
'$factors de $kFactorsMax fatores de ' signsMax: kSignsMax,
'risco frequentemente associados a ' factors: factors,
'má oclusão.', factorsMax: kFactorsMax,
bodyColor: const Color(0xFFFF55A7), ),
bodyColor: AppColors.pink,
bodyWeight: FontWeight.w800, bodyWeight: FontWeight.w800,
), ),
), ),
@@ -209,19 +202,23 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
FadeSlideIn( FadeSlideIn(
delay: const Duration(milliseconds: 140), delay: const Duration(milliseconds: 140),
child: _ResultSection( child: _ResultSection(
heading: 'O que deve fazer agora', heading:
QuizResultStrings.whatToDoNowHeading,
body: recommend body: recommend
? 'Com base neste resultado, recomendamos a marcação de uma consulta com Odontopediatra ou Ortodontista para uma avaliação presencial.' ? QuizResultStrings
: '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.', .whatToDoNowRecommend
: QuizResultStrings
.whatToDoNowNoConcerns,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
FadeSlideIn( FadeSlideIn(
delay: const Duration(milliseconds: 180), delay: const Duration(milliseconds: 180),
child: _ResultSection( child: _ResultSection(
heading: 'Importante saber', heading: QuizResultStrings
body: .importantToKnowHeading,
'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.', body: QuizResultStrings
.importantToKnowBody,
), ),
), ),
], ],
@@ -254,7 +251,7 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
if (!context.mounted) return; if (!context.mounted) return;
_goToVideo(); _goToVideo();
}, },
child: const Text('Avançar'), child: const Text(QuizUiStrings.advance),
), ),
), ),
), ),

View File

@@ -1,12 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart'; import 'package:youtube_player_flutter/youtube_player_flutter.dart';
import '../strings/quiz_ui_strings.dart';
import '../widgets/entrance.dart'; import '../widgets/entrance.dart';
import '../widgets/liquid_waves_background.dart'; import '../widgets/liquid_waves_background.dart';
import '../widgets/tap_bounce.dart'; import '../widgets/tap_bounce.dart';
const Color _pink = Color(0xFFFF55A7); const Color _pink = AppColors.pink;
const Color _teal = Color(0xFF2F9E94); const Color _teal = AppColors.teal;
/// Player do YouTube incorporado (sem AppBar/tela cheia própria) — usado /// Player do YouTube incorporado (sem AppBar/tela cheia própria) — usado
/// tanto no ecrã intersticial [QuizVideoGuideScreen] como embutido /// tanto no ecrã intersticial [QuizVideoGuideScreen] como embutido
@@ -78,9 +80,9 @@ class QuizVideoGuideScreen extends StatefulWidget {
super.key, super.key,
required this.youtubeId, required this.youtubeId,
required this.onAdvance, required this.onAdvance,
this.heading = 'Antes de continuar, veja o vídeo educativo...', this.heading = QuizUiStrings.defaultVideoGuideHeading,
this.caption = this.caption =
'Veja o vídeo para compreender melhor as próximas perguntas do questionário.', QuizUiStrings.defaultVideoGuideCaption,
this.extraHeading, this.extraHeading,
this.extraSubtitle, this.extraSubtitle,
}); });
@@ -122,7 +124,7 @@ class _QuizVideoGuideScreenState extends State<QuizVideoGuideScreen> {
body: Stack( body: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), Positioned.fill(child: Container(color: AppColors.background)),
const LiquidWavesBackground(), const LiquidWavesBackground(),
SafeArea( SafeArea(
child: Padding( child: Padding(
@@ -135,7 +137,7 @@ class _QuizVideoGuideScreenState extends State<QuizVideoGuideScreen> {
child: TextButton( child: TextButton(
onPressed: _advance, onPressed: _advance,
child: const Text( child: const Text(
'Pular', QuizUiStrings.skip,
style: TextStyle( style: TextStyle(
color: Colors.black45, color: Colors.black45,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@@ -243,7 +245,7 @@ class _QuizVideoGuideScreenState extends State<QuizVideoGuideScreen> {
), ),
), ),
onPressed: _videoWatched ? _advance : null, onPressed: _videoWatched ? _advance : null,
child: const Text('Avançar'), child: const Text(QuizUiStrings.advance),
), ),
), ),
), ),

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; 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'; import '../widgets/entrance.dart';
const Color _pink = Color(0xFFFF55A7); const Color _pink = AppColors.pink;
const Color _teal = Color(0xFF2F9E94); const Color _teal = AppColors.teal;
class _CreditPerson { class _CreditPerson {
const _CreditPerson(this.name, this.role, {this.logoPath}); const _CreditPerson(this.name, this.role, {this.logoPath});
@@ -25,21 +27,27 @@ class _CreditSection {
} }
const List<_CreditSection> _kCreditSections = [ const List<_CreditSection> _kCreditSections = [
_CreditSection('Criadora original', [ _CreditSection(CreditsStrings.originalCreatorSection, [
_CreditPerson('Francisca Salgado Ferreira Pacheco Silva', 'Criadora original'), _CreditPerson(
'Francisca Salgado Ferreira Pacheco Silva',
CreditsStrings.originalCreatorRole,
),
]), ]),
_CreditSection('Desenvolvimento informático', [ _CreditSection(CreditsStrings.developmentSection, [
_CreditPerson('Carlos Correia', 'Desenvolvedor'), _CreditPerson('Carlos Correia', CreditsStrings.developerRole),
_CreditPerson('Fábio Ceia', 'Desenvolvedor'), _CreditPerson('Fábio Ceia', CreditsStrings.developerRole),
_CreditPerson('Ruben Grandra', 'Desenvolvedor'), _CreditPerson('Ruben Grandra', CreditsStrings.developerRole),
_CreditPerson('Dinis Maria', 'Desenvolvedor'), _CreditPerson('Dinis Maria', CreditsStrings.developerRole),
]), ]),
_CreditSection('Orientadores', [ _CreditSection(CreditsStrings.advisorsSection, [
_CreditPerson('Augusta Pureza Alves Silveira', 'Orientador(a)'), _CreditPerson('Augusta Pureza Alves Silveira', CreditsStrings.advisorRole),
_CreditPerson('Cristina Lopes Cardoso Silva', 'Orientador(a)'), _CreditPerson('Cristina Lopes Cardoso Silva', CreditsStrings.advisorRole),
_CreditPerson('João Carlos Rodrigues L. Miranda', 'Orientador(a)'), _CreditPerson(
'João Carlos Rodrigues L. Miranda',
CreditsStrings.advisorRole,
),
]), ]),
_CreditSection('Instituição colaboradora', [ _CreditSection(CreditsStrings.institutionSection, [
_CreditPerson('Universidade Fernando Pessoa', ''), _CreditPerson('Universidade Fernando Pessoa', ''),
_CreditPerson( _CreditPerson(
'Escola Profissional de Vila do Conde', 'Escola Profissional de Vila do Conde',
@@ -66,14 +74,14 @@ class CreditsScreen extends StatelessWidget {
elevation: 0, elevation: 0,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
title: const Text( title: const Text(
'Criadores e Colaboradores', CreditsStrings.pageTitle,
style: TextStyle(fontWeight: FontWeight.w900), style: TextStyle(fontWeight: FontWeight.w900),
), ),
), ),
), ),
), ),
body: Container( body: Container(
color: const Color(0xFFFAFAF7), color: AppColors.background,
child: SafeArea( child: SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 24), padding: const EdgeInsets.fromLTRB(20, 24, 20, 24),
@@ -97,7 +105,7 @@ class CreditsScreen extends StatelessWidget {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text( const Text(
'Quem fez a Check-Teeth Kids', CreditsStrings.header,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; 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/entrance.dart';
import '../widgets/liquid_waves_background.dart'; import '../widgets/liquid_waves_background.dart';
import '../widgets/tap_bounce.dart'; import '../widgets/tap_bounce.dart';
@@ -22,7 +24,7 @@ class CuriosidadeScreen extends StatelessWidget {
elevation: 0, elevation: 0,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
title: const Text( title: const Text(
'Curiosidades', CuriosidadeStrings.pageTitle,
style: TextStyle(fontWeight: FontWeight.w900), style: TextStyle(fontWeight: FontWeight.w900),
), ),
), ),
@@ -31,7 +33,7 @@ class CuriosidadeScreen extends StatelessWidget {
body: Stack( body: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), Positioned.fill(child: Container(color: AppColors.background)),
const LiquidWavesBackground(), const LiquidWavesBackground(),
SafeArea( SafeArea(
child: Align( child: Align(
@@ -45,9 +47,8 @@ class CuriosidadeScreen extends StatelessWidget {
child: TapBounce( child: TapBounce(
scale: 0.97, scale: 0.97,
child: _CuriosityTopicTile( child: _CuriosityTopicTile(
title: 'Tema X', title: CuriosidadeStrings.topicXTitle,
description: description: CuriosidadeStrings.topicXDescription,
'Aprenda dicas rápidas e simples para cuidar dos dentes no dia a dia.',
), ),
), ),
), ),
@@ -57,8 +58,8 @@ class CuriosidadeScreen extends StatelessWidget {
child: const TapBounce( child: const TapBounce(
scale: 0.97, scale: 0.97,
child: _CuriosityTopicTile( child: _CuriosityTopicTile(
title: 'Tema Y', title: CuriosidadeStrings.topicYTitle,
description: 'Conteúdo em breve.', description: CuriosidadeStrings.contentComingSoon,
), ),
), ),
), ),
@@ -68,8 +69,8 @@ class CuriosidadeScreen extends StatelessWidget {
child: const TapBounce( child: const TapBounce(
scale: 0.97, scale: 0.97,
child: _CuriosityTopicTile( child: _CuriosityTopicTile(
title: 'Tema Z', title: CuriosidadeStrings.topicZTitle,
description: 'Conteúdo em breve.', description: CuriosidadeStrings.contentComingSoon,
), ),
), ),
), ),
@@ -79,8 +80,8 @@ class CuriosidadeScreen extends StatelessWidget {
child: const TapBounce( child: const TapBounce(
scale: 0.97, scale: 0.97,
child: _CuriosityTopicTile( child: _CuriosityTopicTile(
title: 'Tema U', title: CuriosidadeStrings.topicUTitle,
description: 'Conteúdo em breve.', description: CuriosidadeStrings.contentComingSoon,
), ),
), ),
), ),
@@ -114,7 +115,7 @@ class _CuriosityTopicTile extends StatelessWidget {
showModalBottomSheet<void>( showModalBottomSheet<void>(
context: context, context: context,
showDragHandle: true, showDragHandle: true,
backgroundColor: const Color(0xFFFFE6F1), backgroundColor: AppColors.pinkBackground,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)), borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
), ),
@@ -132,7 +133,7 @@ class _CuriosityTopicTile extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Color(0xFFFF55A7), color: AppColors.pink,
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
@@ -174,7 +175,7 @@ class _CuriosityTopicTile extends StatelessWidget {
), ),
), ),
onPressed: () => Navigator.of(ctx).pop(), onPressed: () => Navigator.of(ctx).pop(),
child: const Text('Fechar'), child: const Text(CuriosidadeStrings.close),
), ),
), ),
), ),
@@ -195,12 +196,12 @@ class _CuriosityTopicTile extends StatelessWidget {
width: 36, width: 36,
height: 36, height: 36,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFF55A7).withValues(alpha: 0.12), color: AppColors.pink.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: const Icon( child: const Icon(
Icons.lightbulb_rounded, Icons.lightbulb_rounded,
color: Color(0xFFFF55A7), color: AppColors.pink,
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@@ -209,7 +210,7 @@ class _CuriosityTopicTile extends StatelessWidget {
title, title,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Color(0xFF2F9E94), color: AppColors.teal,
), ),
), ),
), ),

View File

@@ -1,6 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
import '../strings/splash_strings.dart';
class HelloSplashScreen extends StatefulWidget { class HelloSplashScreen extends StatefulWidget {
const HelloSplashScreen({super.key, required this.onFinished, this.duration = const Duration(seconds: 5)}); const HelloSplashScreen({super.key, required this.onFinished, this.duration = const Duration(seconds: 5)});
@@ -76,7 +78,7 @@ class _HelloSplashScreenState extends State<HelloSplashScreen> with TickerProvid
child: Container( child: Container(
width: size.width, width: size.width,
height: size.height, height: size.height,
color: const Color(0xFFFAFAF7), color: AppColors.background,
child: SafeArea( child: SafeArea(
child: Center( child: Center(
child: Column( child: Column(
@@ -85,12 +87,12 @@ class _HelloSplashScreenState extends State<HelloSplashScreen> with TickerProvid
ScaleTransition( ScaleTransition(
scale: _pop, scale: _pop,
child: const Text( child: const Text(
'Olá', SplashStrings.greeting,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 64, fontSize: 64,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Color(0xFFFF9AD0), color: AppColors.pinkLight,
height: 1.0, height: 1.0,
), ),
), ),

View File

@@ -1,9 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
import '../main.dart' show supabase; import '../main.dart' show supabase;
import '../privacy_gate_prefs.dart'; import '../privacy_gate_prefs.dart';
import '../strings/privacy_strings.dart';
import '../terms_gate_prefs.dart'; import '../terms_gate_prefs.dart';
import '../widgets/entrance.dart'; import '../widgets/entrance.dart';
import '../widgets/liquid_waves_background.dart'; import '../widgets/liquid_waves_background.dart';
@@ -87,7 +89,7 @@ class _PrivacyGateScreenState extends State<PrivacyGateScreen> {
body: Stack( body: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), Positioned.fill(child: Container(color: AppColors.background)),
const LiquidWavesBackground(), const LiquidWavesBackground(),
SafeArea( SafeArea(
child: Column( child: Column(
@@ -129,7 +131,7 @@ class _PrivacyGateScreenState extends State<PrivacyGateScreen> {
child: TextButton( child: TextButton(
onPressed: _declining ? null : _acceptAll, onPressed: _declining ? null : _acceptAll,
child: const Text( child: const Text(
'Aceitar tudo', PrivacyStrings.acceptAll,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 15, fontSize: 15,
@@ -250,8 +252,8 @@ class _PrivacyConsentRow extends StatelessWidget {
children: [ children: [
TextSpan( TextSpan(
text: item.required text: item.required
? '(Obrigatório) ' ? PrivacyStrings.requiredPrefix
: '(Opcional) ', : PrivacyStrings.optionalPrefix,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: item.required color: item.required
@@ -302,7 +304,7 @@ class _AdvanceButton extends StatelessWidget {
), ),
), ),
onPressed: enabled ? onPressed : null, onPressed: enabled ? onPressed : null,
child: const Text('Avançar'), child: const Text(PrivacyStrings.advance),
), ),
), ),
); );

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
import '../main.dart' show supabase; import '../main.dart' show supabase;
import '../strings/settings_strings.dart';
import '../widgets/app_dialogs.dart'; import '../widgets/app_dialogs.dart';
import '../widgets/entrance.dart'; import '../widgets/entrance.dart';
import '../widgets/pill_snackbar.dart'; import '../widgets/pill_snackbar.dart';
@@ -8,8 +10,8 @@ import '../widgets/tap_bounce.dart';
import 'credits_screen.dart'; import 'credits_screen.dart';
import 'terms_screen.dart'; import 'terms_screen.dart';
const Color _teal = Color(0xFF2F9E94); const Color _teal = AppColors.teal;
const Color _accentPink = Color(0xFFFF55A7); const Color _accentPink = AppColors.pink;
/// Conteúdo da aba de Configurações, para ser embutido na bottom navigation /// Conteúdo da aba de Configurações, para ser embutido na bottom navigation
/// do LoggedHomeScreen (sem Scaffold/AppBar próprios). /// do LoggedHomeScreen (sem Scaffold/AppBar próprios).
@@ -32,13 +34,9 @@ class _SettingsBodyState extends State<SettingsBody> {
Future<void> _confirmDeleteAccountData() async { Future<void> _confirmDeleteAccountData() async {
final confirmed = await showConfirmDialog( final confirmed = await showConfirmDialog(
context, context,
title: 'Apagar dados da conta', title: SettingsStrings.deleteAccountData,
message: message: SettingsStrings.deleteAccountMessage,
'Isto remove permanentemente a sua conta, perfil, crianças ' confirmLabel: SettingsStrings.delete,
'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',
confirmColor: _accentPink, confirmColor: _accentPink,
); );
@@ -56,7 +54,8 @@ class _SettingsBodyState extends State<SettingsBody> {
final errorMessage = (data is Map) ? data['error']?.toString() : null; final errorMessage = (data is Map) ? data['error']?.toString() : null;
if (response.status != 200 || errorMessage != null) { if (response.status != 200 || errorMessage != null) {
throw StateError( throw StateError(
errorMessage ?? 'Erro ao apagar conta (status ${response.status})', errorMessage ??
SettingsStrings.errorDeletingAccount(response.status),
); );
} }
@@ -64,7 +63,7 @@ class _SettingsBodyState extends State<SettingsBody> {
if (!mounted) return; if (!mounted) return;
Navigator.of(context).popUntil((route) => route.isFirst); Navigator.of(context).popUntil((route) => route.isFirst);
} catch (e) { } catch (e) {
if (mounted) showPillSnackBar(context, 'Erro ao apagar: $e'); if (mounted) showPillSnackBar(context, SettingsStrings.errorDeleting(e));
} finally { } finally {
if (mounted) setState(() => _deletingAccount = false); if (mounted) setState(() => _deletingAccount = false);
} }
@@ -83,18 +82,18 @@ class _SettingsBodyState extends State<SettingsBody> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_SectionLabel('Conta'), _SectionLabel(SettingsStrings.account),
_SettingsCard( _SettingsCard(
children: [ children: [
_InfoTile( _InfoTile(
icon: Icons.person_outline_rounded, icon: Icons.person_outline_rounded,
title: name.isEmpty ? 'Sem nome' : name, title: name.isEmpty ? SettingsStrings.noName : name,
subtitle: email, subtitle: email,
), ),
const Divider(height: 1), const Divider(height: 1),
_ActionTile( _ActionTile(
icon: Icons.logout_rounded, icon: Icons.logout_rounded,
title: 'Sair', title: SettingsStrings.signOut,
onTap: _signOut, onTap: _signOut,
), ),
], ],
@@ -108,12 +107,12 @@ class _SettingsBodyState extends State<SettingsBody> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_SectionLabel('Sobre'), _SectionLabel(SettingsStrings.about),
_SettingsCard( _SettingsCard(
children: [ children: [
_ActionTile( _ActionTile(
icon: Icons.description_outlined, icon: Icons.description_outlined,
title: 'Termos de Serviço', title: SettingsStrings.termsOfService,
onTap: () => Navigator.of(context).push( onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>( MaterialPageRoute<void>(
builder: (_) => const TermsScreen(), builder: (_) => const TermsScreen(),
@@ -123,7 +122,7 @@ class _SettingsBodyState extends State<SettingsBody> {
const Divider(height: 1), const Divider(height: 1),
_ActionTile( _ActionTile(
icon: Icons.diversity_3_outlined, icon: Icons.diversity_3_outlined,
title: 'Criadores e colaboradores', title: SettingsStrings.creatorsAndContributors,
onTap: () => Navigator.of(context).push( onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>( MaterialPageRoute<void>(
builder: (_) => const CreditsScreen(), builder: (_) => const CreditsScreen(),
@@ -133,7 +132,7 @@ class _SettingsBodyState extends State<SettingsBody> {
const Divider(height: 1), const Divider(height: 1),
const _InfoTile( const _InfoTile(
icon: Icons.info_outline_rounded, icon: Icons.info_outline_rounded,
title: 'Versão do app', title: SettingsStrings.appVersion,
subtitle: '1.0.0', subtitle: '1.0.0',
), ),
], ],
@@ -147,12 +146,12 @@ class _SettingsBodyState extends State<SettingsBody> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_SectionLabel('Zona de risco'), _SectionLabel(SettingsStrings.dangerZone),
_SettingsCard( _SettingsCard(
children: [ children: [
_ActionTile( _ActionTile(
icon: Icons.delete_forever_rounded, icon: Icons.delete_forever_rounded,
title: 'Apagar dados da conta', title: SettingsStrings.deleteAccountData,
titleColor: _accentPink, titleColor: _accentPink,
loading: _deletingAccount, loading: _deletingAccount,
onTap: _deletingAccount ? null : _confirmDeleteAccountData, onTap: _deletingAccount ? null : _confirmDeleteAccountData,

View File

@@ -1,8 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
import '../main.dart' show supabase; import '../main.dart' show supabase;
import '../strings/terms_strings.dart';
import '../terms_gate_prefs.dart'; import '../terms_gate_prefs.dart';
import '../widgets/entrance.dart'; import '../widgets/entrance.dart';
import '../widgets/liquid_waves_background.dart'; import '../widgets/liquid_waves_background.dart';
@@ -62,7 +64,7 @@ class _TermsGateScreenState extends State<TermsGateScreen> {
body: Stack( body: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), Positioned.fill(child: Container(color: AppColors.background)),
const LiquidWavesBackground(), const LiquidWavesBackground(),
SafeArea( SafeArea(
child: Column( child: Column(
@@ -173,7 +175,7 @@ class _AcceptCheckboxRow extends StatelessWidget {
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
const Text( const Text(
'Aceitar tudo', TermsStrings.acceptAll,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 15, fontSize: 15,
@@ -212,7 +214,7 @@ class _AdvanceButton extends StatelessWidget {
), ),
), ),
onPressed: enabled ? onPressed : null, onPressed: enabled ? onPressed : null,
child: const Text('Avançar'), child: const Text(TermsStrings.advance),
), ),
), ),
); );

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; 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'; import '../widgets/terms_content.dart';
class TermsScreen extends StatelessWidget { class TermsScreen extends StatelessWidget {
@@ -20,14 +22,14 @@ class TermsScreen extends StatelessWidget {
elevation: 0, elevation: 0,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
title: const Text( title: const Text(
'Termos de Serviço', TermsStrings.pageTitle,
style: TextStyle(fontWeight: FontWeight.w900), style: TextStyle(fontWeight: FontWeight.w900),
), ),
), ),
), ),
), ),
body: Container( body: Container(
color: const Color(0xFFFAFAF7), color: AppColors.background,
child: SafeArea( child: SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 24), padding: const EdgeInsets.fromLTRB(20, 24, 20, 24),

View File

@@ -1,12 +1,14 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart'; import 'package:youtube_player_flutter/youtube_player_flutter.dart';
import '../watched_videos_prefs.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/entrance.dart';
import '../widgets/liquid_waves_background.dart'; import '../widgets/liquid_waves_background.dart';
import '../widgets/pill_snackbar.dart'; import '../widgets/pill_snackbar.dart';
@@ -36,83 +38,80 @@ class VideoData {
final List<VideoData> videoList = [ final List<VideoData> videoList = [
VideoData( VideoData(
id: 1, id: 1,
title: 'Episódio 1', title: VideoStrings.episodeTitle(1),
description: 'Qual a Influência do nariz entupido na má oclusão', description: VideoStrings.episodeDescriptions[0],
youtubeId: 'PJ58CZv4ECw', youtubeId: 'PJ58CZv4ECw',
), ),
VideoData( VideoData(
id: 2, id: 2,
title: 'Episódio 2', title: VideoStrings.episodeTitle(2),
description: 'Qual a Influência das alergias sazionais na má oclusão', description: VideoStrings.episodeDescriptions[1],
youtubeId: 'y4_kWmZtAtg', youtubeId: 'y4_kWmZtAtg',
), ),
VideoData( VideoData(
id: 3, id: 3,
title: 'Episódio 3', title: VideoStrings.episodeTitle(3),
description: 'Qual a Influência das Otites frequentes na má oclusão', description: VideoStrings.episodeDescriptions[2],
youtubeId: 'nD75Y5PuKTo', youtubeId: 'nD75Y5PuKTo',
), ),
VideoData( VideoData(
id: 4, id: 4,
title: 'Episódio 4', title: VideoStrings.episodeTitle(4),
description: 'Qual a Influência das Amigdalites recorrentes na má oclusão', description: VideoStrings.episodeDescriptions[3],
youtubeId: 'yvFllWYeuLw', youtubeId: 'yvFllWYeuLw',
), ),
VideoData( VideoData(
id: 5, id: 5,
title: 'Episódio 5', title: VideoStrings.episodeTitle(5),
description: description: VideoStrings.episodeDescriptions[4],
'Qual a Influência das Bronquiolites recorrentes na má oclusão',
youtubeId: 'DnhUa-T8_Ps', youtubeId: 'DnhUa-T8_Ps',
), ),
VideoData( VideoData(
id: 6, id: 6,
title: 'Episódio 6', title: VideoStrings.episodeTitle(6),
description: 'Qual a Influência dos problemas respitatórios na má oclusão', description: VideoStrings.episodeDescriptions[5],
youtubeId: 'zKt_iwkrjvo', youtubeId: 'zKt_iwkrjvo',
), ),
VideoData( VideoData(
id: 7, id: 7,
title: 'Episódio 7', title: VideoStrings.episodeTitle(7),
description: description: VideoStrings.episodeDescriptions[6],
'Qual a Influência das interrupções respiratórias na má oclusão',
youtubeId: 'NpmQ2brap5A', youtubeId: 'NpmQ2brap5A',
), ),
VideoData( VideoData(
id: 8, id: 8,
title: 'Episódio 8', title: VideoStrings.episodeTitle(8),
description: 'Qual a Influência do ressonar na má oclusão', description: VideoStrings.episodeDescriptions[7],
youtubeId: 'Wj3KYw9pBi0', youtubeId: 'Wj3KYw9pBi0',
), ),
VideoData( VideoData(
id: 9, id: 9,
title: 'Episódio 9', title: VideoStrings.episodeTitle(9),
description: description: VideoStrings.episodeDescriptions[8],
'Qual a Influência de acordar com saliva seca na boca ou na almofada na saúde oral',
youtubeId: 'bOm9t61cT_U', youtubeId: 'bOm9t61cT_U',
), ),
VideoData( VideoData(
id: 10, id: 10,
title: 'Episódio 10', title: VideoStrings.episodeTitle(10),
description: 'Qual a Influência da respiração oral na má oclusão', description: VideoStrings.episodeDescriptions[9],
youtubeId: 'fAitMizbcms', youtubeId: 'fAitMizbcms',
), ),
VideoData( VideoData(
id: 11, id: 11,
title: 'Episódio 11', title: VideoStrings.episodeTitle(11),
description: 'Qual a influência do uso exagerado da chupeta na má oclusão', description: VideoStrings.episodeDescriptions[10],
youtubeId: '6sYoBUjks_I', youtubeId: '6sYoBUjks_I',
), ),
VideoData( VideoData(
id: 12, id: 12,
title: 'Episódio 12', title: VideoStrings.episodeTitle(12),
description: 'Qual a influência do uso exagerado da chupeta na má oclusão', description: VideoStrings.episodeDescriptions[11],
youtubeId: 'eznKrErQbHo', youtubeId: 'eznKrErQbHo',
), ),
VideoData( VideoData(
id: 13, id: 13,
title: 'Episódio 13', title: VideoStrings.episodeTitle(13),
description: 'Qual a influência do hábito de chuchar o dedo na má oclusão', description: VideoStrings.episodeDescriptions[12],
youtubeId: 'VO9CNqHRdeM', youtubeId: 'VO9CNqHRdeM',
), ),
]; ];
@@ -176,7 +175,7 @@ Future<void> showVideoPlayerDialog(
}) { }) {
if (video.youtubeId != null) { if (video.youtubeId != null) {
if (video.youtubeId!.isEmpty) { if (video.youtubeId!.isEmpty) {
showPillSnackBar(context, 'Vídeo ainda não disponível'); showPillSnackBar(context, VideoStrings.videoNotAvailable);
return Future.value(); return Future.value();
} }
return Navigator.of(context).push<void>( return Navigator.of(context).push<void>(
@@ -199,8 +198,8 @@ class VideoScreen extends StatefulWidget {
/// guardar localmente quais episódios ela já assistiu até ao fim. /// guardar localmente quais episódios ela já assistiu até ao fim.
final String? scopeId; final String? scopeId;
static const Color _teal = Color(0xFF2F9E94); static const Color _teal = AppColors.teal;
static const Color _accentPink = Color(0xFFFF55A7); static const Color _accentPink = AppColors.pink;
@override @override
State<VideoScreen> createState() => _VideoScreenState(); State<VideoScreen> createState() => _VideoScreenState();
@@ -253,7 +252,7 @@ class _VideoScreenState extends State<VideoScreen> {
elevation: 0, elevation: 0,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
title: const Text( title: const Text(
'Vídeos Educativos', VideoStrings.pageTitle,
style: TextStyle(fontWeight: FontWeight.w900), style: TextStyle(fontWeight: FontWeight.w900),
), ),
), ),
@@ -262,7 +261,7 @@ class _VideoScreenState extends State<VideoScreen> {
body: Stack( body: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), Positioned.fill(child: Container(color: AppColors.background)),
const LiquidWavesBackground(), const LiquidWavesBackground(),
SafeArea( SafeArea(
child: Padding( child: Padding(
@@ -285,7 +284,7 @@ class _VideoScreenState extends State<VideoScreen> {
child: TextField( child: TextField(
controller: _searchController, controller: _searchController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Pesquisar vídeos...', hintText: VideoStrings.searchHint,
prefixIcon: const Icon( prefixIcon: const Icon(
Icons.search, Icons.search,
color: VideoScreen._teal, color: VideoScreen._teal,
@@ -307,7 +306,7 @@ class _VideoScreenState extends State<VideoScreen> {
child: _filteredVideos.isEmpty child: _filteredVideos.isEmpty
? Center( ? Center(
child: Text( child: Text(
'Nenhum vídeo encontrado', VideoStrings.noVideosFound,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -546,7 +545,7 @@ class _VideoButton extends StatelessWidget {
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
ColoredBox( ColoredBox(
color: const Color(0xFFFFE6F1), color: AppColors.pinkBackground,
child: VideoThumbnail( child: VideoThumbnail(
video: video, video: video,
borderRadius: 0, borderRadius: 0,
@@ -593,7 +592,7 @@ class _VideoButton extends StatelessWidget {
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
), ),
child: Text( child: Text(
'EP. ${video.id}', VideoStrings.episodeBadge(video.id),
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
@@ -731,7 +730,7 @@ class _YoutubePlayerPageState extends State<_YoutubePlayerPage>
child: Scaffold( child: Scaffold(
backgroundColor: value.isFullScreen backgroundColor: value.isFullScreen
? Colors.black ? Colors.black
: const Color(0xFFFAFAF7), : AppColors.background,
appBar: value.isFullScreen appBar: value.isFullScreen
? null ? null
: PreferredSize( : PreferredSize(
@@ -808,7 +807,7 @@ class _YoutubePlayerPageState extends State<_YoutubePlayerPage>
MainAxisAlignment.spaceBetween, MainAxisAlignment.spaceBetween,
children: [ children: [
const Text( const Text(
'Próximos', VideoStrings.nextVideos,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
fontSize: 15, fontSize: 15,
@@ -835,7 +834,7 @@ class _YoutubePlayerPageState extends State<_YoutubePlayerPage>
vertical: 2, vertical: 2,
), ),
child: Text( child: Text(
'Ver mais', VideoStrings.seeMore,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 13, fontSize: 13,
@@ -1033,7 +1032,7 @@ class _VideoPlayerDialogState extends State<_VideoPlayerDialog> {
setState(() { setState(() {
_isInitialized = false; _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, Icons.replay_10_rounded,
color: Colors.white, color: Colors.white,
), ),
tooltip: 'Retroceder 10s', tooltip: VideoStrings.rewind10s,
onPressed: () => _seekBy(const Duration(seconds: -10)), onPressed: () => _seekBy(const Duration(seconds: -10)),
), ),
IconButton( IconButton(
@@ -1211,7 +1210,7 @@ class _VideoControlsState extends State<_VideoControls> {
Icons.forward_10_rounded, Icons.forward_10_rounded,
color: Colors.white, color: Colors.white,
), ),
tooltip: 'Avançar 10s', tooltip: VideoStrings.forward10s,
onPressed: () => _seekBy(const Duration(seconds: 10)), onPressed: () => _seekBy(const Duration(seconds: 10)),
), ),
IconButton( IconButton(
@@ -1294,7 +1293,7 @@ class _FullscreenVideoPlayerState extends State<_FullscreenVideoPlayer> {
setState(() { setState(() {
_isInitialized = false; _isInitialized = false;
}); });
showPillSnackBar(context, 'Erro ao carregar vídeo: $e'); showPillSnackBar(context, VideoStrings.errorLoadingVideo(e));
} }
} }
} }

View File

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

View File

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

View File

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

View File

@@ -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.';
}

View File

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

View File

@@ -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.';
}

View File

@@ -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.';
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
/// Texto do ecrã de boas-vindas ([HelloSplashScreen]).
class SplashStrings {
const SplashStrings._();
static const String greeting = 'Olá';
}

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
import '../strings/common_strings.dart';
import 'tap_bounce.dart'; import 'tap_bounce.dart';
const Color _teal = Color(0xFF2F9E94); const Color _teal = AppColors.teal;
const Color _accentPink = Color(0xFFFF55A7); const Color _accentPink = AppColors.pink;
/// Diálogo de confirmação com a identidade visual do app (título rosa, /// 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. /// botões em pílula), usado para todas as confirmações destrutivas/decisórias.
@@ -11,7 +13,7 @@ Future<bool?> showConfirmDialog(
BuildContext context, { BuildContext context, {
required String title, required String title,
String? message, String? message,
String cancelLabel = 'Cancelar', String cancelLabel = CommonStrings.cancel,
required String confirmLabel, required String confirmLabel,
Color confirmColor = _teal, Color confirmColor = _teal,
}) { }) {

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
/// Notificação em formato de pílula flutuante (fundo branco, texto rosa, /// Notificação em formato de pílula flutuante (fundo branco, texto rosa,
/// cantos totalmente arredondados, com um pequeno "pop" de entrada), usada /// cantos totalmente arredondados, com um pequeno "pop" de entrada), usada
@@ -62,7 +63,7 @@ class _AnimatedPill extends StatelessWidget {
message, message,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
color: Color(0xFFFF55A7), color: AppColors.pink,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 14, fontSize: 14,
), ),

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; 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 /// Um dos itens de consentimento mostrados no ecrã de privacidade. Itens
/// com [required] a false (ex.: recolha de dados técnicos de utilização) /// 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. /// Texto fixo dos consentimentos de privacidade, mostrado no cadastro.
const List<PrivacyConsentItem> kPrivacyConsentItems = [ const List<PrivacyConsentItem> kPrivacyConsentItems = [
PrivacyConsentItem( PrivacyConsentItem(id: 'health_data', text: PrivacyStrings.healthDataConsent),
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( PrivacyConsentItem(
id: 'privacy_policy', id: 'privacy_policy',
text: text: PrivacyStrings.privacyPolicyConsent,
'Li e aceito os Termos e Condições apresentados anteriormente e as '
'condições de privacidade descritas nesta página.',
), ),
PrivacyConsentItem( PrivacyConsentItem(
id: 'tracking', id: 'tracking',
text: text: PrivacyStrings.trackingConsent,
'Ajude-nos a melhorar a Check-Teeth Kids, autorizando a recolha de '
'dados técnicos de utilização.',
required: false, required: false,
), ),
]; ];
@@ -66,7 +57,7 @@ class PrivacyHeader extends StatelessWidget {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text( const Text(
'Política de Privacidade', PrivacyStrings.headerTitle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,

View File

@@ -1,30 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
import '../strings/terms_strings.dart';
const Color kTermsPink = Color(0xFFFF55A7); const Color kTermsPink = AppColors.pink;
/// 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<String> 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.',
];
/// Emblema circular rosa + título "Termos e Condições", reutilizado nos /// Emblema circular rosa + título "Termos e Condições", reutilizado nos
/// dois ecrãs que mostram este conteúdo. /// dois ecrãs que mostram este conteúdo.
@@ -50,7 +28,7 @@ class TermsHeader extends StatelessWidget {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text( const Text(
'Termos e Condições', TermsStrings.headerTitle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
@@ -85,10 +63,10 @@ class TermsBody extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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), if (i > 0) const SizedBox(height: 14),
Text( Text(
kTermsParagraphs[i], TermsStrings.paragraphs[i],
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,