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