CTK 1.2.0
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user