CTK 1.0.0

This commit is contained in:
Carlos Correia
2026-07-14 22:19:26 +01:00
parent afce79b9b9
commit fb39e1f722
7 changed files with 79 additions and 65 deletions

View File

@@ -113,7 +113,7 @@ class _HomeScreenState extends State<HomeScreen> {
final user = response.user;
if (user == null) {
throw StateError('Usuário não encontrado após criar conta.');
throw StateError('Utilizador não encontrado após criar a conta.');
}
// Antes de persistir o perfil (o que já faz o AuthGate considerar a
@@ -137,7 +137,7 @@ class _HomeScreenState extends State<HomeScreen> {
if (!mounted) return;
showPillSnackBar(
context,
'Esta conta não existe mais. Verifique o email ou crie uma nova conta.',
'Esta conta não existe. Verifique o email ou crie uma nova conta.',
);
} on AuthException catch (e) {
if (!mounted) return;
@@ -146,7 +146,7 @@ class _HomeScreenState extends State<HomeScreen> {
if (!mounted) return;
showPillSnackBar(
context,
'Tempo esgotado. Verifique sua conexão e tente novamente.',
'Tempo esgotado. Verifique a sua ligação e tente novamente.',
);
} catch (e) {
if (!mounted) return;
@@ -159,14 +159,14 @@ class _HomeScreenState extends State<HomeScreen> {
String _friendlyAuthError(AuthException e) {
switch (e.code) {
case 'invalid_credentials':
return 'Email ou senha incorretos.';
return 'Email ou palavra-passe incorretos.';
case 'user_not_found':
return 'Usuário não encontrado.';
return 'Utilizador não encontrado.';
case 'email_exists':
case 'user_already_exists':
return 'Este email já está em uso.';
case 'weak_password':
return 'Senha fraca. Use pelo menos 6 caracteres.';
return 'Palavra-passe fraca. Utilize pelo menos 6 caracteres.';
default:
return e.message;
}
@@ -376,14 +376,14 @@ class _AuthForm extends StatelessWidget {
children: [
_AuthTextField(
controller: nameController,
hintText: 'Digite seu nome',
hintText: 'Introduza o seu nome',
icon: Icons.person_outline_rounded,
textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.sentences,
inputFormatters: [CapitalizeFirstLetterFormatter()],
validator: (v) {
final value = (v ?? '').trim();
if (value.isEmpty) return 'Informe seu nome';
if (value.isEmpty) return 'Indique o seu nome';
if (value.length < 2) return 'Nome muito curto';
if (!_namePattern.hasMatch(value)) {
return 'O nome não pode conter números';
@@ -398,13 +398,13 @@ class _AuthForm extends StatelessWidget {
),
_AuthTextField(
controller: emailController,
hintText: 'Digite seu email',
hintText: 'Introduza o seu email',
icon: Icons.mail_outline_rounded,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: (v) {
final value = (v ?? '').trim();
if (value.isEmpty) return 'Informe seu email';
if (value.isEmpty) return 'Indique o seu email';
if (!value.contains('@')) return 'Email inválido';
return null;
},
@@ -412,13 +412,13 @@ class _AuthForm extends StatelessWidget {
const SizedBox(height: 12),
_AuthTextField(
controller: passwordController,
hintText: 'Digite sua senha',
hintText: 'Introduza a sua palavra-passe',
icon: Icons.lock_outline_rounded,
obscureText: true,
textInputAction: TextInputAction.done,
validator: (v) {
final value = v ?? '';
if (value.isEmpty) return 'Informe sua senha';
if (value.isEmpty) return 'Indique a sua palavra-passe';
if (value.length < 6) return 'Mínimo de 6 caracteres';
return null;
},

View File

@@ -813,6 +813,20 @@ class _InicioTab extends StatelessWidget {
),
),
),
const SizedBox(height: 14),
FadeSlideIn(
delay: const Duration(milliseconds: 150),
child: Center(
child: Text(
'Mais funcionalidades em breve',
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 12.5,
color: Colors.black.withValues(alpha: 0.35),
),
),
),
),
const SizedBox(height: 16),
],
),
@@ -1095,7 +1109,7 @@ Future<Map<String, dynamic>?> _requireFirstChild(
) async {
final proceed = await showConfirmDialog(
context,
title: 'Cadastre uma criança',
title: 'Registe uma criança',
message: 'Antes de iniciar o quiz, adicione uma criança ao seu perfil.',
confirmLabel: 'Adicionar criança',
);
@@ -1636,7 +1650,7 @@ class _PerfilTabState extends State<_PerfilTab> {
),
onPressed: () =>
Navigator.of(ctx).pop(ImageSource.camera),
child: const Text('Câmera'),
child: const Text('Câmara'),
),
),
),
@@ -1729,7 +1743,7 @@ class _PerfilTabState extends State<_PerfilTab> {
context,
title: 'Remover criança',
message:
'Tem certeza que deseja remover "$childName"? Essa ação não pode ser desfeita.',
'Tem a certeza que quer remover "$childName"? Esta ação não pode ser desfeita.',
confirmLabel: 'Remover',
confirmColor: const Color(0xFFFF55A7),
);
@@ -2160,7 +2174,7 @@ class _PerfilTabState extends State<_PerfilTab> {
: 'Criança ${i + 1}';
final subtitle = [
if (childAge != null) 'Idade: $childAge',
if (childGender.isNotEmpty) 'Gênero: $childGender',
if (childGender.isNotEmpty) 'Género: $childGender',
].join('');
final bool selected = i == selectedIndex;
@@ -2389,7 +2403,7 @@ class _AddChildSheetState extends State<_AddChildSheet> {
final formOk = _formKey.currentState?.validate() ?? false;
setState(() {
_birthDateError = _birthDate == null
? 'Informe a data de nascimento'
? 'Indique a data de nascimento'
: null;
});
if (!formOk || _birthDate == null) return;
@@ -2442,7 +2456,7 @@ class _AddChildSheetState extends State<_AddChildSheet> {
),
validator: (v) {
final value = (v ?? '').trim();
if (value.isEmpty) return 'Informe o nome';
if (value.isEmpty) return 'Indique o nome';
if (value.length < 2) return 'Nome muito curto';
if (!_namePattern.hasMatch(value)) {
return 'O nome não pode conter números';
@@ -2490,10 +2504,10 @@ class _AddChildSheetState extends State<_AddChildSheet> {
DropdownMenuItem(value: 'Outro', child: Text('Outro')),
],
onChanged: (v) => setState(() => _gender = v),
decoration: const InputDecoration(labelText: 'Gênero'),
decoration: const InputDecoration(labelText: 'Género'),
validator: (v) {
if (v == null || v.trim().isEmpty) {
return 'Selecione o gênero';
return 'Selecione o género';
}
return null;
},

View File

@@ -920,11 +920,11 @@ class Quiz19Screen extends StatelessWidget {
'Qual das seguintes imagens é mais parecida com o perfil do seu filho/a?',
answers: [
QuizAnswer(
title: 'Perfil reto',
description: 'Perfil facial reto',
weight: 1,
title: 'Perfil convexo',
description: 'Perfil facial convexo',
weight: 2,
hideTitle: true,
value: 'perfil_reto',
value: 'perfil_convexo',
imageBuilder: (context) => _zoomCrop(
'assets/mockup_images/1.jpeg',
alignment: const Alignment(0.1, 0.7),
@@ -932,11 +932,11 @@ class Quiz19Screen extends StatelessWidget {
),
),
QuizAnswer(
title: 'Perfil convexo',
description: 'Perfil facial convexo',
weight: 2,
title: 'Perfil reto',
description: 'Perfil facial reto',
weight: 1,
hideTitle: true,
value: 'perfil_convexo',
value: 'perfil_reto',
imageBuilder: (context) => _zoomCrop(
'assets/mockup_images/2.jpeg',
alignment: const Alignment(0.1, 0.7),
@@ -1199,20 +1199,20 @@ class Quiz24Screen extends StatelessWidget {
question:
'Qual das seguintes imagens se assemelha à boca do seu filho/a?',
answers: const [
QuizAnswer(
title: 'Ligeiro desalinhamento',
description: 'Pequeno desalinhamento ou espaço entre alguns dentes',
weight: 1,
hideTitle: true,
value: 'apinhamento_leve',
imagePath: 'assets/mockup_images/16.png',
),
QuizAnswer(
title: 'Apinhamento moderado',
description: 'Dentes rodados/desalinhados de forma mais visível',
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',
weight: 1,
hideTitle: true,
value: 'apinhamento_leve',
imagePath: 'assets/mockup_images/15.png',
),
QuizAnswer(
@@ -1297,20 +1297,20 @@ class Quiz26Screen extends StatelessWidget {
question:
'Qual das seguintes imagens se assemelha ao freio lingual do seu filho/a?',
answers: const [
QuizAnswer(
title: 'Freio lingual correto',
description: 'Língua move-se livremente, sem restrição visível',
weight: 1,
hideTitle: true,
value: 'freio_lingual_correto',
imagePath: 'assets/mockup_images/17.png',
),
QuizAnswer(
title: 'Freio lingual inadequado',
description: 'Freio lingual curto/apertado (língua em coração)',
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',
weight: 1,
hideTitle: true,
value: 'freio_lingual_correto',
imagePath: 'assets/mockup_images/18.jpeg',
),
],
@@ -1389,20 +1389,20 @@ class Quiz28Screen extends StatelessWidget {
question:
'Qual das seguintes imagens se assemelha ao céu da boca do seu filho/a?',
answers: const [
QuizAnswer(
title: 'Posição em U saudável',
description: 'Palato largo, em forma de U',
weight: 1,
hideTitle: true,
value: 'palato_u',
imagePath: 'assets/mockup_images/26.png',
),
QuizAnswer(
title: 'Posição profunda incorreta',
description: 'Palato estreito/profundo, em forma de V',
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',
weight: 1,
hideTitle: true,
value: 'palato_u',
imagePath: 'assets/mockup_images/27.png',
),
],

View File

@@ -79,7 +79,7 @@ class QuizVideoGuideScreen extends StatefulWidget {
required this.youtubeId,
required this.onAdvance,
this.caption =
'Assista para compreender melhor as próximas perguntas do questionário.',
'Veja o vídeo para compreender melhor as próximas perguntas do questionário.',
this.extraHeading,
this.extraSubtitle,
});
@@ -144,7 +144,7 @@ class _QuizVideoGuideScreenState extends State<QuizVideoGuideScreen> {
const SizedBox(height: 4),
const FadeSlideIn(
child: Text(
'Antes de continuar, assista ao vídeo educativo...',
'Antes de continuar, veja o vídeo educativo...',
style: TextStyle(
fontSize: 21,
fontWeight: FontWeight.w900,

View File

@@ -34,10 +34,10 @@ class _SettingsBodyState extends State<SettingsBody> {
context,
title: 'Apagar dados da conta',
message:
'Isso remove permanentemente a sua conta, perfil, crianças '
'cadastradas e fotos — incluindo o login, permitindo criar uma '
'nova conta com o mesmo e-mail depois. Essa ação não pode ser '
'desfeita. Deseja continuar?',
'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',
confirmColor: _accentPink,
);

View File

@@ -253,7 +253,7 @@ class _VideoScreenState extends State<VideoScreen> {
elevation: 0,
scrolledUnderElevation: 0,
title: const Text(
'Videos Educativos',
'Vídeos Educativos',
style: TextStyle(fontWeight: FontWeight.w900),
),
),

View File

@@ -16,8 +16,8 @@ const List<PrivacyConsentItem> kPrivacyConsentItems = [
id: 'health_data',
text:
'Concordo com o processamento dos meus dados pessoais de saúde para '
'ter acesso aos recursos do aplicativo Check-Teeth Kids. Veja mais '
'na Política de Privacidade.',
'ter acesso às funcionalidades da aplicação Check-Teeth Kids. Veja '
'mais na Política de Privacidade.',
),
PrivacyConsentItem(
id: 'privacy_policy',
@@ -26,11 +26,11 @@ const List<PrivacyConsentItem> kPrivacyConsentItems = [
PrivacyConsentItem(
id: 'tracking',
text:
'Autorizo o CK-T-KD a rastrear a minha atividade na app. Podem '
'receber dados como minha faixa etária, status da assinatura, '
'Autorizo o CK-T-KD a monitorizar a minha atividade na app. Podem '
'receber dados como a minha faixa etária, estado da subscrição, '
'momento de abertura da app e identificadores técnicos, conforme '
'descrito na Política de Privacidade. Isso ajuda a CK-T-KD a '
'aprimorar as suas campanhas publicitárias. *',
'descrito na Política de Privacidade. Isto ajuda o CK-T-KD a '
'melhorar as suas campanhas publicitárias. *',
),
];