CKT String|Colors
This commit is contained in:
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user