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; final user = response.user;
if (user == null) { 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 // 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; if (!mounted) return;
showPillSnackBar( showPillSnackBar(
context, 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) { } on AuthException catch (e) {
if (!mounted) return; if (!mounted) return;
@@ -146,7 +146,7 @@ class _HomeScreenState extends State<HomeScreen> {
if (!mounted) return; if (!mounted) return;
showPillSnackBar( showPillSnackBar(
context, context,
'Tempo esgotado. Verifique sua conexão e tente novamente.', 'Tempo esgotado. Verifique a sua ligação e tente novamente.',
); );
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
@@ -159,14 +159,14 @@ class _HomeScreenState extends State<HomeScreen> {
String _friendlyAuthError(AuthException e) { String _friendlyAuthError(AuthException e) {
switch (e.code) { switch (e.code) {
case 'invalid_credentials': case 'invalid_credentials':
return 'Email ou senha incorretos.'; return 'Email ou palavra-passe incorretos.';
case 'user_not_found': case 'user_not_found':
return 'Usuário não encontrado.'; return 'Utilizador não encontrado.';
case 'email_exists': case 'email_exists':
case 'user_already_exists': case 'user_already_exists':
return 'Este email já está em uso.'; return 'Este email já está em uso.';
case 'weak_password': case 'weak_password':
return 'Senha fraca. Use pelo menos 6 caracteres.'; return 'Palavra-passe fraca. Utilize pelo menos 6 caracteres.';
default: default:
return e.message; return e.message;
} }
@@ -376,14 +376,14 @@ class _AuthForm extends StatelessWidget {
children: [ children: [
_AuthTextField( _AuthTextField(
controller: nameController, controller: nameController,
hintText: 'Digite seu nome', hintText: 'Introduza o seu nome',
icon: Icons.person_outline_rounded, icon: Icons.person_outline_rounded,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,
inputFormatters: [CapitalizeFirstLetterFormatter()], inputFormatters: [CapitalizeFirstLetterFormatter()],
validator: (v) { validator: (v) {
final value = (v ?? '').trim(); final value = (v ?? '').trim();
if (value.isEmpty) return 'Informe seu nome'; if (value.isEmpty) return 'Indique o seu nome';
if (value.length < 2) return 'Nome muito curto'; if (value.length < 2) return 'Nome muito curto';
if (!_namePattern.hasMatch(value)) { if (!_namePattern.hasMatch(value)) {
return 'O nome não pode conter números'; return 'O nome não pode conter números';
@@ -398,13 +398,13 @@ class _AuthForm extends StatelessWidget {
), ),
_AuthTextField( _AuthTextField(
controller: emailController, controller: emailController,
hintText: 'Digite seu email', hintText: 'Introduza o seu email',
icon: Icons.mail_outline_rounded, icon: Icons.mail_outline_rounded,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
validator: (v) { validator: (v) {
final value = (v ?? '').trim(); final value = (v ?? '').trim();
if (value.isEmpty) return 'Informe seu email'; if (value.isEmpty) return 'Indique o seu email';
if (!value.contains('@')) return 'Email inválido'; if (!value.contains('@')) return 'Email inválido';
return null; return null;
}, },
@@ -412,13 +412,13 @@ class _AuthForm extends StatelessWidget {
const SizedBox(height: 12), const SizedBox(height: 12),
_AuthTextField( _AuthTextField(
controller: passwordController, controller: passwordController,
hintText: 'Digite sua senha', hintText: 'Introduza a sua palavra-passe',
icon: Icons.lock_outline_rounded, icon: Icons.lock_outline_rounded,
obscureText: true, obscureText: true,
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
validator: (v) { validator: (v) {
final value = v ?? ''; final value = v ?? '';
if (value.isEmpty) return 'Informe sua senha'; if (value.isEmpty) return 'Indique a sua palavra-passe';
if (value.length < 6) return 'Mínimo de 6 caracteres'; if (value.length < 6) return 'Mínimo de 6 caracteres';
return null; 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), const SizedBox(height: 16),
], ],
), ),
@@ -1095,7 +1109,7 @@ Future<Map<String, dynamic>?> _requireFirstChild(
) async { ) async {
final proceed = await showConfirmDialog( final proceed = await showConfirmDialog(
context, context,
title: 'Cadastre uma criança', title: 'Registe uma criança',
message: 'Antes de iniciar o quiz, adicione uma criança ao seu perfil.', message: 'Antes de iniciar o quiz, adicione uma criança ao seu perfil.',
confirmLabel: 'Adicionar criança', confirmLabel: 'Adicionar criança',
); );
@@ -1636,7 +1650,7 @@ class _PerfilTabState extends State<_PerfilTab> {
), ),
onPressed: () => onPressed: () =>
Navigator.of(ctx).pop(ImageSource.camera), 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, context,
title: 'Remover criança', title: 'Remover criança',
message: 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', confirmLabel: 'Remover',
confirmColor: const Color(0xFFFF55A7), confirmColor: const Color(0xFFFF55A7),
); );
@@ -2160,7 +2174,7 @@ class _PerfilTabState extends State<_PerfilTab> {
: 'Criança ${i + 1}'; : 'Criança ${i + 1}';
final subtitle = [ final subtitle = [
if (childAge != null) 'Idade: $childAge', if (childAge != null) 'Idade: $childAge',
if (childGender.isNotEmpty) 'Gênero: $childGender', if (childGender.isNotEmpty) 'Género: $childGender',
].join(''); ].join('');
final bool selected = i == selectedIndex; final bool selected = i == selectedIndex;
@@ -2389,7 +2403,7 @@ class _AddChildSheetState extends State<_AddChildSheet> {
final formOk = _formKey.currentState?.validate() ?? false; final formOk = _formKey.currentState?.validate() ?? false;
setState(() { setState(() {
_birthDateError = _birthDate == null _birthDateError = _birthDate == null
? 'Informe a data de nascimento' ? 'Indique a data de nascimento'
: null; : null;
}); });
if (!formOk || _birthDate == null) return; if (!formOk || _birthDate == null) return;
@@ -2442,7 +2456,7 @@ class _AddChildSheetState extends State<_AddChildSheet> {
), ),
validator: (v) { validator: (v) {
final value = (v ?? '').trim(); 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 (value.length < 2) return 'Nome muito curto';
if (!_namePattern.hasMatch(value)) { if (!_namePattern.hasMatch(value)) {
return 'O nome não pode conter números'; return 'O nome não pode conter números';
@@ -2490,10 +2504,10 @@ class _AddChildSheetState extends State<_AddChildSheet> {
DropdownMenuItem(value: 'Outro', child: Text('Outro')), DropdownMenuItem(value: 'Outro', child: Text('Outro')),
], ],
onChanged: (v) => setState(() => _gender = v), onChanged: (v) => setState(() => _gender = v),
decoration: const InputDecoration(labelText: 'Gênero'), decoration: const InputDecoration(labelText: 'Género'),
validator: (v) { validator: (v) {
if (v == null || v.trim().isEmpty) { if (v == null || v.trim().isEmpty) {
return 'Selecione o gênero'; return 'Selecione o género';
} }
return null; 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?', 'Qual das seguintes imagens é mais parecida com o perfil do seu filho/a?',
answers: [ answers: [
QuizAnswer( QuizAnswer(
title: 'Perfil reto', title: 'Perfil convexo',
description: 'Perfil facial reto', description: 'Perfil facial convexo',
weight: 1, weight: 2,
hideTitle: true, hideTitle: true,
value: 'perfil_reto', value: 'perfil_convexo',
imageBuilder: (context) => _zoomCrop( imageBuilder: (context) => _zoomCrop(
'assets/mockup_images/1.jpeg', 'assets/mockup_images/1.jpeg',
alignment: const Alignment(0.1, 0.7), alignment: const Alignment(0.1, 0.7),
@@ -932,11 +932,11 @@ class Quiz19Screen extends StatelessWidget {
), ),
), ),
QuizAnswer( QuizAnswer(
title: 'Perfil convexo', title: 'Perfil reto',
description: 'Perfil facial convexo', description: 'Perfil facial reto',
weight: 2, weight: 1,
hideTitle: true, hideTitle: true,
value: 'perfil_convexo', value: 'perfil_reto',
imageBuilder: (context) => _zoomCrop( imageBuilder: (context) => _zoomCrop(
'assets/mockup_images/2.jpeg', 'assets/mockup_images/2.jpeg',
alignment: const Alignment(0.1, 0.7), alignment: const Alignment(0.1, 0.7),
@@ -1199,20 +1199,20 @@ class Quiz24Screen extends StatelessWidget {
question: question:
'Qual das seguintes imagens se assemelha à boca do seu filho/a?', 'Qual das seguintes imagens se assemelha à boca do seu filho/a?',
answers: const [ 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( QuizAnswer(
title: 'Apinhamento moderado', title: 'Apinhamento moderado',
description: 'Dentes rodados/desalinhados de forma mais visível', description: 'Dentes rodados/desalinhados de forma mais visível',
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'apinhamento_moderado', 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', imagePath: 'assets/mockup_images/15.png',
), ),
QuizAnswer( QuizAnswer(
@@ -1297,20 +1297,20 @@ class Quiz26Screen extends StatelessWidget {
question: question:
'Qual das seguintes imagens se assemelha ao freio lingual do seu filho/a?', 'Qual das seguintes imagens se assemelha ao freio lingual do seu filho/a?',
answers: const [ 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( QuizAnswer(
title: 'Freio lingual inadequado', title: 'Freio lingual inadequado',
description: 'Freio lingual curto/apertado (língua em coração)', description: 'Freio lingual curto/apertado (língua em coração)',
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'freio_lingual_inadequado', 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', imagePath: 'assets/mockup_images/18.jpeg',
), ),
], ],
@@ -1389,20 +1389,20 @@ class Quiz28Screen extends StatelessWidget {
question: question:
'Qual das seguintes imagens se assemelha ao céu da boca do seu filho/a?', 'Qual das seguintes imagens se assemelha ao céu da boca do seu filho/a?',
answers: const [ 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( QuizAnswer(
title: 'Posição profunda incorreta', title: 'Posição profunda incorreta',
description: 'Palato estreito/profundo, em forma de V', description: 'Palato estreito/profundo, em forma de V',
weight: 2, weight: 2,
hideTitle: true, hideTitle: true,
value: 'palato_v', 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', imagePath: 'assets/mockup_images/27.png',
), ),
], ],

View File

@@ -79,7 +79,7 @@ class QuizVideoGuideScreen extends StatefulWidget {
required this.youtubeId, required this.youtubeId,
required this.onAdvance, required this.onAdvance,
this.caption = 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.extraHeading,
this.extraSubtitle, this.extraSubtitle,
}); });
@@ -144,7 +144,7 @@ class _QuizVideoGuideScreenState extends State<QuizVideoGuideScreen> {
const SizedBox(height: 4), const SizedBox(height: 4),
const FadeSlideIn( const FadeSlideIn(
child: Text( child: Text(
'Antes de continuar, assista ao vídeo educativo...', 'Antes de continuar, veja o vídeo educativo...',
style: TextStyle( style: TextStyle(
fontSize: 21, fontSize: 21,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,

View File

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

View File

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

View File

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