CTK 1.2.1

This commit is contained in:
Carlos Correia
2026-07-30 01:25:31 +01:00
parent fc875d559c
commit 887c62c379
7 changed files with 594 additions and 7 deletions

View File

@@ -16,6 +16,7 @@ import 'consultorios/clinic_favorites_prefs.dart';
import 'consultorios/consultorios_screen.dart';
import 'consultorios/overpass_service.dart';
import 'main.dart' show supabase;
import 'onboarding_prefs.dart';
import 'quiz/quiz1.dart';
import 'quiz/quiz_prefs.dart';
import 'quiz/quiz_progress_prefs.dart';
@@ -29,6 +30,8 @@ import 'colors/app_gradients.dart';
import 'strings/address_strings.dart';
import 'strings/consultorios_strings.dart';
import 'strings/home_strings.dart';
import 'strings/onboarding_strings.dart';
import 'widgets/coach_mark.dart';
import 'widgets/entrance.dart';
import 'widgets/liquid_waves_background.dart';
import 'widgets/name_input_formatter.dart';
@@ -97,6 +100,15 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
String? _clinicsError;
Set<String> _favoriteClinicIds = {};
// Âncoras do tutorial guiado (ver [_startOnboardingTutorial]) — cada uma
// marca o widget real que o passo correspondente do tour vai destacar.
final GlobalKey _gaugesKey = GlobalKey();
final GlobalKey _quizCardKey = GlobalKey();
final GlobalKey _videoCardKey = GlobalKey();
final GlobalKey _navConsultoriosKey = GlobalKey();
final GlobalKey _navPerfilKey = GlobalKey();
final GlobalKey _navAjustesKey = GlobalKey();
@override
void initState() {
super.initState();
@@ -104,8 +116,80 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
_loadInitialProfile();
refreshStats();
_loadFavoriteClinics();
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!mounted) return;
await _maybeShowOnboardingTutorial();
if (!mounted) return;
await _maybeStartPendingQuiz();
});
}
/// Mostra o tutorial guiado automaticamente na primeira vez que o
/// utilizador entra na app — depois disso só reaparece se pedido
/// manualmente em Ajustes (ver [replayOnboardingTutorial]).
Future<void> _maybeShowOnboardingTutorial() async {
final seen = await OnboardingPrefs.hasSeenTutorial();
if (seen || !mounted) return;
await _startOnboardingTutorial();
await OnboardingPrefs.markTutorialSeen();
}
/// Corre o tour de novo a pedido do utilizador (botão "Rever tutorial" em
/// Ajustes) — muda para a aba Início primeiro, já que é onde vivem todos
/// os alvos do tour.
void replayOnboardingTutorial() {
setState(() => _index = 0);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _maybeStartPendingQuiz();
if (mounted) _startOnboardingTutorial();
});
}
Future<void> _startOnboardingTutorial() {
// Pequena pausa para deixar a animação de entrada dos cards da Home
// (FadeSlideIn) terminar antes de medir as suas posições.
return Future<void>.delayed(const Duration(milliseconds: 450), () {
if (!mounted) return Future<void>.value();
return showCoachMarkTour(context, [
CoachMarkStep(
targetKey: _gaugesKey,
title: OnboardingStrings.gaugesTitle,
description: OnboardingStrings.gaugesDescription,
borderRadius: 20,
),
CoachMarkStep(
targetKey: _quizCardKey,
title: OnboardingStrings.quizTitle,
description: OnboardingStrings.quizDescription,
borderRadius: 28,
),
CoachMarkStep(
targetKey: _videoCardKey,
title: OnboardingStrings.videosTitle,
description: OnboardingStrings.videosDescription,
borderRadius: 28,
),
CoachMarkStep(
targetKey: _navConsultoriosKey,
title: OnboardingStrings.clinicsTitle,
description: OnboardingStrings.clinicsDescription,
borderRadius: 40,
padding: 14,
),
CoachMarkStep(
targetKey: _navPerfilKey,
title: OnboardingStrings.profileTitle,
description: OnboardingStrings.profileDescription,
borderRadius: 40,
padding: 14,
),
CoachMarkStep(
targetKey: _navAjustesKey,
title: OnboardingStrings.settingsTitle,
description: OnboardingStrings.settingsDescription,
borderRadius: 40,
padding: 14,
),
]);
});
}
@@ -383,6 +467,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
),
BottomNavigationBarItem(
icon: AnimatedNavIcon(
key: _navConsultoriosKey,
icon: Icons.medical_services_rounded,
selected: _index == 1,
),
@@ -390,6 +475,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
),
BottomNavigationBarItem(
icon: AnimatedNavIcon(
key: _navPerfilKey,
icon: Icons.person_rounded,
selected: _index == 2,
),
@@ -397,6 +483,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
),
BottomNavigationBarItem(
icon: AnimatedNavIcon(
key: _navAjustesKey,
icon: Icons.settings_rounded,
selected: _index == 3,
),
@@ -484,6 +571,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
top: kToolbarHeight + 96,
child: Center(
child: Row(
key: _gaugesKey,
mainAxisSize: MainAxisSize.min,
children: [
_RiskArcGauge(
@@ -679,7 +767,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
refreshStats();
},
),
_ => const SettingsBody(),
_ => SettingsBody(onReplayTutorial: replayOnboardingTutorial),
},
10,
),
@@ -963,6 +1051,7 @@ class _InicioTab extends StatelessWidget {
child: TapBounce(
scale: 0.97,
child: _HeroQuizCard(
key: state?._quizCardKey,
onStartQuiz: () => _startQuiz(context),
),
),
@@ -978,6 +1067,7 @@ class _InicioTab extends StatelessWidget {
child: TapBounce(
scale: 0.97,
child: _VideoLibraryCard(
key: state?._videoCardKey,
watchedCount: state?._watchedVideoCount ?? 0,
onTap: () async {
await Navigator.of(context).push(
@@ -1270,7 +1360,7 @@ Future<Map<String, dynamic>?> _pickChildSheet(
}
class _HeroQuizCard extends StatelessWidget {
const _HeroQuizCard({required this.onStartQuiz});
const _HeroQuizCard({super.key, required this.onStartQuiz});
final VoidCallback onStartQuiz;
@@ -1598,7 +1688,11 @@ class _ClinicsPreview extends StatelessWidget {
/// só ícone e texto. Um único toque (no card ou no botão) leva direto à
/// grelha onde se escolhe qual episódio assistir.
class _VideoLibraryCard extends StatelessWidget {
const _VideoLibraryCard({required this.watchedCount, required this.onTap});
const _VideoLibraryCard({
super.key,
required this.watchedCount,
required this.onTap,
});
final int watchedCount;
final VoidCallback onTap;