Atualização de gauge
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user