CTK 1.2.0

This commit is contained in:
Carlos Correia
2026-07-30 00:53:51 +01:00
parent 89b27e5e86
commit fc875d559c
7 changed files with 298 additions and 311 deletions

View File

@@ -83,9 +83,6 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
QuizResultData? _lastResult;
int? _brushingWeekCount;
int _weeklyGoal = BrushingPrefs.defaultWeeklyGoal;
bool _brushingDailyLimitReached = false;
int? _watchedVideoCount;
String _cachedUserName = HomeStrings.noName;
@@ -124,32 +121,21 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
setState(() => _favoriteClinicIds = ids);
}
/// Recarrega os dados locais de escovagem e vídeos assistidos da criança
/// atualmente selecionada. Chamado ao trocar de criança, ao editar a meta
/// semanal e depois de registar uma escovagem ou um vídeo completo.
/// Recarrega a contagem de vídeos assistidos da criança atualmente
/// selecionada. Chamado ao trocar de criança e depois de assistir um vídeo
/// completo.
Future<void> refreshStats() async {
final scope = (_selectedChildScopeId ?? '').trim();
if (scope.isEmpty) {
if (!mounted) return;
setState(() {
_brushingWeekCount = null;
_weeklyGoal = BrushingPrefs.defaultWeeklyGoal;
_brushingDailyLimitReached = false;
_watchedVideoCount = null;
});
setState(() => _watchedVideoCount = null);
return;
}
final weekCount = await BrushingPrefs.getWeekCount(scope);
final goal = await BrushingPrefs.getWeeklyGoal(scope);
final dailyLimitReached = await BrushingPrefs.hasReachedDailyLimit(scope);
final watchedCount = await WatchedVideosPrefs.getWatchedCount(scope);
if (!mounted) return;
setState(() {
_brushingWeekCount = weekCount;
_weeklyGoal = goal;
_brushingDailyLimitReached = dailyLimitReached;
_watchedVideoCount = watchedCount;
});
}
@@ -458,64 +444,68 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
bottom: Radius.circular(40),
),
),
flexibleSpace: FlexibleSpaceBar(
titlePadding: EdgeInsets.zero,
background: ClipRRect(
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(40),
),
child: Container(
decoration: const BoxDecoration(gradient: kAppBarGradient),
child: Stack(
fit: StackFit.expand,
children: [
if (hasScore)
Positioned(
left: 0,
right: 0,
top: kToolbarHeight + 50,
child: Center(
child: Text(
(_selectedChildName ?? '').trim(),
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w800,
color: Colors.white.withValues(alpha: 0.92),
fontSize: 14,
),
),
),
),
//posição da app bar relativamente ao nome
// Nota: o gradiente é pintado diretamente no slot `flexibleSpace`
// em vez de usar `FlexibleSpaceBar.background` — o FlexibleSpaceBar
// aplica um fade de opacidade e um deslocamento em parallax ao seu
// `background` à medida que a barra colapsa, o que fazia o
// gradiente parecer diferente (e perder o border radius) a meio do
// scroll. Colocando o gradiente diretamente aqui, ele ocupa sempre
// exatamente a altura atual da barra, sem fade nem parallax.
flexibleSpace: ClipRRect(
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(40),
),
child: Container(
decoration: const BoxDecoration(gradient: kAppBarGradient),
child: Stack(
fit: StackFit.expand,
children: [
if (hasScore)
Positioned(
left: 0,
right: 0,
top: kToolbarHeight + 96,
top: kToolbarHeight + 50,
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_RiskArcGauge(
// Usa sempre o máximo atual do quiz (não
// o que foi gravado na última avaliação)
// para não mostrar um denominador antigo
// quando o número de perguntas muda.
value: hasScore ? result.signs : null,
max: hasScore ? kSignsMax : null,
label: HomeStrings.signsGaugeLabel,
),
const SizedBox(width: 18),
_RiskArcGauge(
value: hasScore ? result.factors : null,
max: hasScore ? kFactorsMax : null,
label: HomeStrings.factorsGaugeLabel,
),
],
child: Text(
(_selectedChildName ?? '').trim(),
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w800,
color: Colors.white.withValues(alpha: 0.92),
fontSize: 14,
),
),
),
),
],
),
//posição da app bar relativamente ao nome
Positioned(
left: 0,
right: 0,
top: kToolbarHeight + 96,
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_RiskArcGauge(
// Usa sempre o máximo atual do quiz (não
// o que foi gravado na última avaliação)
// para não mostrar um denominador antigo
// quando o número de perguntas muda.
value: hasScore ? result.signs : null,
max: hasScore ? kSignsMax : null,
label: HomeStrings.signsGaugeLabel,
),
const SizedBox(width: 18),
_RiskArcGauge(
value: hasScore ? result.factors : null,
max: hasScore ? kFactorsMax : null,
label: HomeStrings.factorsGaugeLabel,
),
],
),
),
),
],
),
),
),
@@ -977,18 +967,6 @@ class _InicioTab extends StatelessWidget {
),
),
),
const SizedBox(height: 16),
FadeSlideIn(
delay: const Duration(milliseconds: 70),
child: _StatsRow(
brushingCount: state?._brushingWeekCount,
weeklyGoal:
state?._weeklyGoal ?? BrushingPrefs.defaultWeeklyGoal,
brushedToday: state?._brushingDailyLimitReached ?? false,
watchedCount: state?._watchedVideoCount,
onTapBrushing: () => _logBrushing(context, state, scopeId),
),
),
const SizedBox(height: 20),
FadeSlideIn(
delay: const Duration(milliseconds: 110),
@@ -1068,34 +1046,6 @@ class _InicioTab extends StatelessWidget {
),
);
}
Future<void> _logBrushing(
BuildContext context,
_LoggedHomeScreenState? state,
String? scopeId,
) async {
final scope = (scopeId ?? '').trim();
if (scope.isEmpty) {
final uid = (supabase.auth.currentUser?.id ?? '').trim();
if (uid.isEmpty) return;
await _requireFirstChild(context, uid);
return;
}
if (!(await BrushingPrefs.canLogMore(scope))) {
if (context.mounted) {
showPillSnackBar(
context,
HomeStrings.brushingLimitReached(BrushingPrefs.maxPerDay),
);
}
return;
}
await BrushingPrefs.logToday(scope);
await state?.refreshStats();
if (context.mounted) {
showPillSnackBar(context, HomeStrings.brushingLogged);
}
}
}
/// Faz [child] "respirar" (escala sobe e desce suavemente, em loop) — usado
@@ -1164,146 +1114,6 @@ class _HomeSectionLabel extends StatelessWidget {
}
}
class _StatsRow extends StatelessWidget {
const _StatsRow({
required this.brushingCount,
required this.weeklyGoal,
required this.brushedToday,
required this.watchedCount,
required this.onTapBrushing,
});
final int? brushingCount;
final int weeklyGoal;
final bool brushedToday;
final int? watchedCount;
final VoidCallback onTapBrushing;
@override
Widget build(BuildContext context) {
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: TapBounce(
scale: 0.97,
child: _StatCard(
icon: Icons.brush_rounded,
iconColor: AppColors.pink,
value: brushingCount == null
? '--'
: '$brushingCount/$weeklyGoal',
label: HomeStrings.brushingThisWeek,
done: brushedToday,
onTap: onTapBrushing,
),
),
),
const SizedBox(width: 12),
Expanded(
child: _StatCard(
icon: Icons.movie_filter_rounded,
iconColor: AppColors.purple,
value: watchedCount == null ? '--' : '$watchedCount',
label: HomeStrings.completedEpisodes,
),
),
],
),
);
}
}
class _StatCard extends StatelessWidget {
const _StatCard({
required this.icon,
required this.iconColor,
required this.value,
required this.label,
this.onTap,
this.done = false,
});
final IconData icon;
final Color iconColor;
final String value;
final String label;
final VoidCallback? onTap;
final bool done;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
elevation: 8,
shadowColor: Colors.black.withValues(alpha: 0.10),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
clipBehavior: Clip.none,
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: iconColor.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: iconColor, size: 20),
),
if (done)
Positioned(
top: -4,
right: -4,
child: Container(
width: 16,
height: 16,
decoration: const BoxDecoration(
color: AppColors.teal,
shape: BoxShape.circle,
),
child: const Icon(
Icons.check_rounded,
size: 11,
color: Colors.white,
),
),
),
],
),
const SizedBox(height: 10),
Text(
value,
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 20,
),
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
fontSize: 11.5,
fontWeight: FontWeight.w600,
color: Colors.black.withValues(alpha: 0.6),
),
),
],
),
),
),
);
}
}
Future<Map<String, dynamic>?> _createChildViaSheet(
BuildContext context,
String uid,