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 'app_colors.dart';
/// Gradiente usado em todas as app bars da aplicação. Direção puramente
/// horizontal (não diagonal) de propósito: a app bar retrátil da Home é
/// alta (~190) enquanto as das outras abas são baixas (~56) um gradiente
@@ -10,7 +12,7 @@ import 'package:flutter/material.dart';
const LinearGradient kAppBarGradient = LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [Color(0xFF1C7A6E), Color(0xFF8FD4BB)],
colors: [AppColors.tealDark, AppColors.tealLight],
);
/// Cor sólida (sem gradiente, de propósito) usada nos botões e cards verdes
@@ -19,12 +21,12 @@ const LinearGradient kAppBarGradient = LinearGradient(
/// mantém-se como [LinearGradient] com as duas cores iguais para não obrigar
/// a mudar todos os `decoration: gradient: kGreenButtonGradient` existentes.
const LinearGradient kGreenButtonGradient = LinearGradient(
colors: [Color(0xFF2F9E94), Color(0xFF2F9E94)],
colors: [AppColors.teal, AppColors.teal],
);
/// Gradiente rosa vivo do card do quiz na Home.
const LinearGradient kPinkHeroGradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFFF55A7), Color(0xFFE83E93)],
colors: [AppColors.pink, AppColors.pinkVivid],
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,12 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
import '../main.dart' show supabase;
import '../widgets/app_gradients.dart';
import '../colors/app_gradients.dart';
import '../strings/quiz_result_strings.dart';
import '../strings/quiz_ui_strings.dart';
import '../widgets/entrance.dart';
import '../widgets/liquid_waves_background.dart';
import '../widgets/tap_bounce.dart';
@@ -106,20 +109,8 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
MaterialPageRoute<void>(
builder: (_) => QuizVideoGuideScreen(
youtubeId: _resultGuideYoutubeId,
heading: 'Visualize o seguinte vídeo',
caption:
'Assista para compreender a importância de um diagnóstico '
'precoce e de realizar tratamento ortodôntico intercetivo, '
'ou seja, uma intervenção realizada na infância, tipicamente '
'durante a fase de dentição mista (presença tanto de dentes '
'de leite como de permanentes).\n\nO seu propósito é '
'identificar, prevenir ou reduzir a gravidade de maloclusões '
'e problemas no desenvolvimento dos maxilares enquanto os '
'ossos e dentes ainda estão em crescimento.\n\nEsta '
'intervenção aproveita a fase de desenvolvimento craniofacial '
'para influenciar o crescimento ósseo, redirecionar a '
'mandíbula, corrigir o alinhamento dos maxilares ou criar '
'espaço para a erupção correta dos dentes permanentes.',
heading: QuizResultStrings.watchTheFollowingVideo,
caption: QuizResultStrings.resultVideoCaption,
onAdvance: (context) =>
Navigator.of(context).popUntil((r) => r.isFirst),
),
@@ -137,7 +128,7 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
body: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))),
Positioned.fill(child: Container(color: AppColors.background)),
const LiquidWavesBackground(),
SafeArea(
child: Center(
@@ -157,12 +148,12 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
const SizedBox(height: 10),
FadeSlideIn(
child: const Text(
'O resultado avaliado é de:',
QuizResultStrings.resultHeading,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
color: Color(0xFFFF55A7),
color: AppColors.pink,
height: 1.2,
),
),
@@ -177,15 +168,16 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
_ResultRing(
value: signs,
max: kSignsMax,
color: const Color(0xFF2F9E94),
color: AppColors.teal,
label:
'Sinais de má\noclusão instalados',
QuizResultStrings.signsRingLabel,
),
_ResultRing(
value: factors,
max: kFactorsMax,
color: const Color(0xFFFF55A7),
label: 'Fatores de\nrisco associados',
color: AppColors.pink,
label:
QuizResultStrings.factorsRingLabel,
),
],
),
@@ -194,14 +186,15 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
FadeSlideIn(
delay: const Duration(milliseconds: 100),
child: _ResultSection(
heading: 'Conclusões:',
body:
'Foram identificados $signs de $kSignsMax '
'sinais de má oclusão já instalada e '
'$factors de $kFactorsMax fatores de '
'risco frequentemente associados a '
'má oclusão.',
bodyColor: const Color(0xFFFF55A7),
heading:
QuizResultStrings.conclusionsHeading,
body: QuizResultStrings.conclusionsBody(
signs: signs,
signsMax: kSignsMax,
factors: factors,
factorsMax: kFactorsMax,
),
bodyColor: AppColors.pink,
bodyWeight: FontWeight.w800,
),
),
@@ -209,19 +202,23 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
FadeSlideIn(
delay: const Duration(milliseconds: 140),
child: _ResultSection(
heading: 'O que deve fazer agora',
heading:
QuizResultStrings.whatToDoNowHeading,
body: recommend
? 'Com base neste resultado, recomendamos a marcação de uma consulta com Odontopediatra ou Ortodontista para uma avaliação presencial.'
: 'Não foram encontrados sinais ou fatores de risco relevantes. Continue a acompanhar a saúde oral do seu filho/a com as consultas de rotina habituais.',
? QuizResultStrings
.whatToDoNowRecommend
: QuizResultStrings
.whatToDoNowNoConcerns,
),
),
const SizedBox(height: 16),
FadeSlideIn(
delay: const Duration(milliseconds: 180),
child: _ResultSection(
heading: 'Importante saber',
body:
'Este resultado não substitui uma avaliação presencial, nem tem função de diagnóstico. O objetivo é ajudá-lo a estar mais atento aos sinais identificados e a não adiar uma consulta que pode fazer a diferença no desenvolvimento oral do seu filho/a.',
heading: QuizResultStrings
.importantToKnowHeading,
body: QuizResultStrings
.importantToKnowBody,
),
),
],
@@ -254,7 +251,7 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
if (!context.mounted) return;
_goToVideo();
},
child: const Text('Avançar'),
child: const Text(QuizUiStrings.advance),
),
),
),

View File

@@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
import '../strings/quiz_ui_strings.dart';
import '../widgets/entrance.dart';
import '../widgets/liquid_waves_background.dart';
import '../widgets/tap_bounce.dart';
const Color _pink = Color(0xFFFF55A7);
const Color _teal = Color(0xFF2F9E94);
const Color _pink = AppColors.pink;
const Color _teal = AppColors.teal;
/// Player do YouTube incorporado (sem AppBar/tela cheia própria) — usado
/// tanto no ecrã intersticial [QuizVideoGuideScreen] como embutido
@@ -78,9 +80,9 @@ class QuizVideoGuideScreen extends StatefulWidget {
super.key,
required this.youtubeId,
required this.onAdvance,
this.heading = 'Antes de continuar, veja o vídeo educativo...',
this.heading = QuizUiStrings.defaultVideoGuideHeading,
this.caption =
'Veja o vídeo para compreender melhor as próximas perguntas do questionário.',
QuizUiStrings.defaultVideoGuideCaption,
this.extraHeading,
this.extraSubtitle,
});
@@ -122,7 +124,7 @@ class _QuizVideoGuideScreenState extends State<QuizVideoGuideScreen> {
body: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))),
Positioned.fill(child: Container(color: AppColors.background)),
const LiquidWavesBackground(),
SafeArea(
child: Padding(
@@ -135,7 +137,7 @@ class _QuizVideoGuideScreenState extends State<QuizVideoGuideScreen> {
child: TextButton(
onPressed: _advance,
child: const Text(
'Pular',
QuizUiStrings.skip,
style: TextStyle(
color: Colors.black45,
fontWeight: FontWeight.w700,
@@ -243,7 +245,7 @@ class _QuizVideoGuideScreenState extends State<QuizVideoGuideScreen> {
),
),
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 '../colors/app_colors.dart';
import '../widgets/app_gradients.dart';
import '../colors/app_gradients.dart';
import '../strings/credits_strings.dart';
import '../widgets/entrance.dart';
const Color _pink = Color(0xFFFF55A7);
const Color _teal = Color(0xFF2F9E94);
const Color _pink = AppColors.pink;
const Color _teal = AppColors.teal;
class _CreditPerson {
const _CreditPerson(this.name, this.role, {this.logoPath});
@@ -25,21 +27,27 @@ class _CreditSection {
}
const List<_CreditSection> _kCreditSections = [
_CreditSection('Criadora original', [
_CreditPerson('Francisca Salgado Ferreira Pacheco Silva', 'Criadora original'),
_CreditSection(CreditsStrings.originalCreatorSection, [
_CreditPerson(
'Francisca Salgado Ferreira Pacheco Silva',
CreditsStrings.originalCreatorRole,
),
]),
_CreditSection('Desenvolvimento informático', [
_CreditPerson('Carlos Correia', 'Desenvolvedor'),
_CreditPerson('Fábio Ceia', 'Desenvolvedor'),
_CreditPerson('Ruben Grandra', 'Desenvolvedor'),
_CreditPerson('Dinis Maria', 'Desenvolvedor'),
_CreditSection(CreditsStrings.developmentSection, [
_CreditPerson('Carlos Correia', CreditsStrings.developerRole),
_CreditPerson('Fábio Ceia', CreditsStrings.developerRole),
_CreditPerson('Ruben Grandra', CreditsStrings.developerRole),
_CreditPerson('Dinis Maria', CreditsStrings.developerRole),
]),
_CreditSection('Orientadores', [
_CreditPerson('Augusta Pureza Alves Silveira', 'Orientador(a)'),
_CreditPerson('Cristina Lopes Cardoso Silva', 'Orientador(a)'),
_CreditPerson('João Carlos Rodrigues L. Miranda', 'Orientador(a)'),
_CreditSection(CreditsStrings.advisorsSection, [
_CreditPerson('Augusta Pureza Alves Silveira', CreditsStrings.advisorRole),
_CreditPerson('Cristina Lopes Cardoso Silva', CreditsStrings.advisorRole),
_CreditPerson(
'João Carlos Rodrigues L. Miranda',
CreditsStrings.advisorRole,
),
]),
_CreditSection('Instituição colaboradora', [
_CreditSection(CreditsStrings.institutionSection, [
_CreditPerson('Universidade Fernando Pessoa', ''),
_CreditPerson(
'Escola Profissional de Vila do Conde',
@@ -66,14 +74,14 @@ class CreditsScreen extends StatelessWidget {
elevation: 0,
scrolledUnderElevation: 0,
title: const Text(
'Criadores e Colaboradores',
CreditsStrings.pageTitle,
style: TextStyle(fontWeight: FontWeight.w900),
),
),
),
),
body: Container(
color: const Color(0xFFFAFAF7),
color: AppColors.background,
child: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 24),
@@ -97,7 +105,7 @@ class CreditsScreen extends StatelessWidget {
),
const SizedBox(height: 16),
const Text(
'Quem fez a Check-Teeth Kids',
CreditsStrings.header,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
import '../strings/privacy_strings.dart';
const Color kPrivacyTeal = Color(0xFF2F9E94);
const Color kPrivacyTeal = AppColors.teal;
/// Um dos itens de consentimento mostrados no ecrã de privacidade. Itens
/// com [required] a false (ex.: recolha de dados técnicos de utilização)
@@ -19,25 +21,14 @@ class PrivacyConsentItem {
/// Texto fixo dos consentimentos de privacidade, mostrado no cadastro.
const List<PrivacyConsentItem> kPrivacyConsentItems = [
PrivacyConsentItem(
id: 'health_data',
text:
'Confirmo que sou encarregado(a) de educação ou responsável legal '
'da criança avaliada e concordo com o processamento dos dados de '
'saúde que introduzo na Check-Teeth Kids, exclusivamente para gerar '
'o resultado da triagem digital.',
),
PrivacyConsentItem(id: 'health_data', text: PrivacyStrings.healthDataConsent),
PrivacyConsentItem(
id: 'privacy_policy',
text:
'Li e aceito os Termos e Condições apresentados anteriormente e as '
'condições de privacidade descritas nesta página.',
text: PrivacyStrings.privacyPolicyConsent,
),
PrivacyConsentItem(
id: 'tracking',
text:
'Ajude-nos a melhorar a Check-Teeth Kids, autorizando a recolha de '
'dados técnicos de utilização.',
text: PrivacyStrings.trackingConsent,
required: false,
),
];
@@ -66,7 +57,7 @@ class PrivacyHeader extends StatelessWidget {
),
const SizedBox(height: 16),
const Text(
'Política de Privacidade',
PrivacyStrings.headerTitle,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,

View File

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