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

@@ -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),
),
),
),