Atualização de gauge

This commit is contained in:
Carlos Correia
2026-07-14 18:23:03 +01:00
parent 4fcf3a475a
commit 5b25d9bef4
22 changed files with 973 additions and 1441 deletions

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:lottie/lottie.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'dart:async';
import 'dart:math' as math;
@@ -19,6 +18,7 @@ import 'widgets/animated_nav_icon.dart';
import 'widgets/app_dialogs.dart';
import 'widgets/app_gradients.dart';
import 'widgets/entrance.dart';
import 'widgets/liquid_waves_background.dart';
import 'widgets/name_input_formatter.dart';
import 'widgets/pill_snackbar.dart';
import 'widgets/tap_bounce.dart';
@@ -61,7 +61,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
static const String _kPendingQuizScopeKey = 'pending_quiz_scope_v1';
static const double _collapsedAppBarHeight = kToolbarHeight;
static const double _expandedAppBarHeight = 190;
static const double _expandedAppBarHeight = 216;
static const double _nameOnlyAppBarHeight = 160;
int _index = 0;
@@ -70,8 +70,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
String? _selectedChildName;
String? _selectedChildScopeId;
int? _lastScore;
int? _lastMaxScore;
QuizResultData? _lastResult;
int? _brushingWeekCount;
int _weeklyGoal = BrushingPrefs.defaultWeeklyGoal;
@@ -204,8 +203,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
final uid = supabase.auth.currentUser?.id;
final String? userId = (uid ?? '').trim().isEmpty ? null : uid;
int? score;
int? max;
QuizResultData? result;
if (scope.isNotEmpty && userId != null) {
final String childId = scope.startsWith('${userId}_')
@@ -218,11 +216,20 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
.select()
.eq('id', childId)
.maybeSingle();
final s = childDoc?['last_score'];
final m = childDoc?['last_max_score'];
if (s is int && m is int) {
score = s;
max = m;
final signs = childDoc?['last_signs'];
final signsMax = childDoc?['last_signs_max'];
final factors = childDoc?['last_factors'];
final factorsMax = childDoc?['last_factors_max'];
if (signs is int &&
signsMax is int &&
factors is int &&
factorsMax is int) {
result = QuizResultData(
signs: signs,
signsMax: signsMax,
factors: factors,
factorsMax: factorsMax,
);
}
} catch (_) {
// no-op
@@ -230,29 +237,20 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
}
}
if (score != null && max != null) {
if (result != null) {
if (!mounted) return;
setState(() {
_lastScore = score;
_lastMaxScore = max;
});
setState(() => _lastResult = result);
return;
}
if (scope.isNotEmpty) {
score = await QuizPrefs.getLastScoreForScope(scope);
max = await QuizPrefs.getLastMaxScoreForScope(scope);
result = await QuizPrefs.getLastResultForScope(scope);
} else if (userId != null) {
score = await QuizPrefs.getLastScoreForUser(userId);
max = await QuizPrefs.getLastMaxScoreForUser(userId);
result = await QuizPrefs.getLastResultForUser(userId);
} else {
score = await QuizPrefs.getLastScore();
max = await QuizPrefs.getLastMaxScore();
result = await QuizPrefs.getLastResult();
}
if (!mounted) return;
setState(() {
_lastScore = score;
_lastMaxScore = max;
});
setState(() => _lastResult = result);
}
void selectChild(String? name, String? scopeId) {
@@ -282,27 +280,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
clipBehavior: Clip.none,
children: [
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))),
Positioned(
left: -size.width * 0.40,
bottom: -size.width * 0.45,
child: IgnorePointer(
child: SizedBox(
width: size.width * 1.05,
height: size.width * 1.05,
child: Transform.rotate(
angle: 35 * math.pi / 180,
child: Opacity(
opacity: 0.95,
child: Lottie.asset(
'lottie/Liquid waves.json',
fit: BoxFit.cover,
repeat: true,
),
),
),
),
),
),
const LiquidWavesBackground(),
SafeArea(
top: false,
child: Align(
@@ -331,7 +309,10 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: AnimatedNavIcon(icon: Icons.home_rounded, selected: _index == 0),
icon: AnimatedNavIcon(
icon: Icons.home_rounded,
selected: _index == 0,
),
label: 'Início',
),
BottomNavigationBarItem(
@@ -356,10 +337,8 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
final int? score = _lastScore;
final int? maxScore = _lastMaxScore;
final bool hasScore = score != null && maxScore != null && maxScore > 0;
final int percent = hasScore ? ((score / maxScore) * 100).round() : 0;
final QuizResultData? result = _lastResult;
final bool hasScore = result != null;
final shownName = _cachedUserName;
if (_index == 0) {
@@ -386,7 +365,9 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
foregroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(40)),
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(40),
),
),
flexibleSpace: FlexibleSpaceBar(
titlePadding: EdgeInsets.zero,
@@ -434,14 +415,29 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
),
),
),
//posição da app bar relativamente ao nome
//posição da app bar relativamente ao nome
if (hasScore)
Positioned(
left: 0,
right: 0,
top: kToolbarHeight + 68,
top: kToolbarHeight + 60,
child: Center(
child: _RiskArcGauge(percent: percent),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_RiskArcGauge(
value: result.signs,
max: result.signsMax,
label: 'Sinais',
),
const SizedBox(width: 18),
_RiskArcGauge(
value: result.factors,
max: result.factorsMax,
label: 'Fatores de risco',
),
],
),
),
),
],
@@ -491,9 +487,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
_greeting(),
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.white.withValues(
alpha: 0.85,
),
color: Colors.white.withValues(alpha: 0.85),
fontSize: 12,
),
),
@@ -588,47 +582,72 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
}
class _RiskArcGauge extends StatelessWidget {
const _RiskArcGauge({required this.percent});
const _RiskArcGauge({
required this.value,
required this.max,
required this.label,
});
final int percent;
final int value;
final int max;
final String label;
@override
Widget build(BuildContext context) {
final clamped = percent.clamp(0, 100);
final progress = max > 0 ? (value / max).clamp(0, 1).toDouble() : 0.0;
return TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 700),
curve: Curves.easeOutCubic,
tween: Tween<double>(begin: 0, end: clamped / 100),
builder: (context, value, _) {
final shown = (value * 100).round();
tween: Tween<double>(begin: 0, end: progress),
builder: (context, animatedProgress, _) {
return SizedBox(
width: 120,
height: 60,
width: 108,
height: 84,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(
Positioned(
top: 0,
left: 0,
right: 0,
height: 60,
child: CustomPaint(
painter: _RiskArcGaugePainter(progress: value),
painter: _RiskArcGaugePainter(progress: animatedProgress),
),
),
Positioned(
top: 38,
left: 6,
left: 0,
right: 0,
child: Center(
child: Text(
'$shown%',
'$value/$max',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontSize: 17,
fontWeight: FontWeight.w900,
height: 1,
),
),
),
),
Positioned(
top: 58,
left: 0,
right: 0,
child: Text(
label,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.85),
fontSize: 10.5,
fontWeight: FontWeight.w700,
height: 1.15,
letterSpacing: 0.1,
),
),
),
],
),
);
@@ -749,7 +768,9 @@ class _InicioTab extends StatelessWidget {
FadeSlideIn(
child: TapBounce(
scale: 0.97,
child: _HeroQuizCard(onStartQuiz: () => _startQuiz(context)),
child: _HeroQuizCard(
onStartQuiz: () => _startQuiz(context),
),
),
),
const SizedBox(height: 16),
@@ -757,7 +778,8 @@ class _InicioTab extends StatelessWidget {
delay: const Duration(milliseconds: 70),
child: _StatsRow(
brushingCount: state?._brushingWeekCount,
weeklyGoal: state?._weeklyGoal ?? BrushingPrefs.defaultWeeklyGoal,
weeklyGoal:
state?._weeklyGoal ?? BrushingPrefs.defaultWeeklyGoal,
brushedToday: state?._brushingDailyLimitReached ?? false,
watchedCount: state?._watchedVideoCount,
onTapBrushing: () => _logBrushing(context, state, scopeId),
@@ -1008,7 +1030,10 @@ class _StatCard extends StatelessWidget {
const SizedBox(height: 10),
Text(
value,
style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 20),
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 20,
),
),
const SizedBox(height: 2),
Text(
@@ -1306,7 +1331,7 @@ class _HeroQuizCard extends StatelessWidget {
),
const SizedBox(height: 5),
Text(
'26 perguntas rápidas · menos de 3 minutos',
'27 perguntas rápidas · menos de 3 minutos',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.92),
fontWeight: FontWeight.w600,
@@ -1552,10 +1577,8 @@ class _PerfilTabState extends State<_PerfilTab> {
}
}
Future<(int?, int?)> _loadScoreForScope(String scopeId) async {
final score = await QuizPrefs.getLastScoreForScope(scopeId);
final max = await QuizPrefs.getLastMaxScoreForScope(scopeId);
return (score, max);
Future<QuizResultData?> _loadScoreForScope(String scopeId) {
return QuizPrefs.getLastResultForScope(scopeId);
}
Future<void> _pickAndUploadProfilePhoto(
@@ -1854,7 +1877,10 @@ class _PerfilTabState extends State<_PerfilTab> {
}
} on TimeoutException {
if (!mounted || !context.mounted) return;
showPillSnackBar(context, 'Tempo esgotado ao adicionar. Tente novamente.');
showPillSnackBar(
context,
'Tempo esgotado ao adicionar. Tente novamente.',
);
} catch (e) {
if (!mounted || !context.mounted) return;
showPillSnackBar(context, 'Erro ao adicionar: $e');
@@ -2191,16 +2217,13 @@ class _PerfilTabState extends State<_PerfilTab> {
),
),
const SizedBox(width: 10),
FutureBuilder<(int?, int?)>(
FutureBuilder<QuizResultData?>(
future: _loadScoreForScope(scopeId),
builder: (context, snap) {
final tuple = snap.data;
final s = tuple?.$1;
final m = tuple?.$2;
final text =
(s == null || m == null || m <= 0)
final result = snap.data;
final text = result == null
? '--'
: '${(((s / m) * 100).round()).clamp(0, 100)}%';
: '${result.signs}/${result.signsMax} · ${result.factors}/${result.factorsMax}';
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,