Tour
This commit is contained in:
parent
956231048e
commit
606106e5ec
10 changed files with 632 additions and 1 deletions
|
|
@ -24,6 +24,7 @@ const _prefsKeyKeepReceiptPhotosLocally = 'keep_receipt_photos_locally';
|
|||
const _prefsKeyStaleLockMinutes = 'stale_lock_minutes';
|
||||
const _prefsKeyKeepMaxQualityReceiptPhotos = 'keep_max_quality_receipt_photos';
|
||||
const _prefsKeyThemeMode = 'theme_mode';
|
||||
const _prefsKeyHasSeenOnboardingTour = 'has_seen_onboarding_tour';
|
||||
|
||||
const defaultStaleLockMinutes = 10;
|
||||
const minStaleLockMinutes = 1;
|
||||
|
|
@ -89,6 +90,17 @@ class AppState extends ChangeNotifier {
|
|||
|
||||
ThemeMode themeMode = ThemeMode.system;
|
||||
|
||||
/// Whether the first-launch guided tour ([OnboardingTour]) has already
|
||||
/// been shown. Defaults to `true` here (not `false`) specifically so
|
||||
/// that widget tests constructing `AppState()` directly and skipping
|
||||
/// [init] — the established pattern throughout this app's test suite,
|
||||
/// to avoid init()'s database/platform-channel dependencies — don't get
|
||||
/// an unexpected full-screen tour overlay blocking every tap. [init]
|
||||
/// overwrites this from the persisted value (defaulting to `false`
|
||||
/// there) for real app startups, where a missing pref genuinely means
|
||||
/// "never shown before."
|
||||
bool hasSeenOnboardingTour = true;
|
||||
|
||||
/// Set if [init] fails. The UI shows this (with a retry option) instead
|
||||
/// of spinning forever — an unhandled exception here previously left
|
||||
/// `isLoading` stuck at true with no feedback at all.
|
||||
|
|
@ -117,6 +129,7 @@ class AppState extends ChangeNotifier {
|
|||
(mode) => mode.name == prefs.getString(_prefsKeyThemeMode),
|
||||
orElse: () => ThemeMode.system,
|
||||
);
|
||||
hasSeenOnboardingTour = prefs.getBool(_prefsKeyHasSeenOnboardingTour) ?? false;
|
||||
} catch (e) {
|
||||
initError = e;
|
||||
isLoading = false;
|
||||
|
|
@ -304,6 +317,15 @@ class AppState extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Called once the first-launch guided tour finishes or is skipped, so
|
||||
/// it never shows again on this device.
|
||||
Future<void> markOnboardingTourSeen() async {
|
||||
hasSeenOnboardingTour = true;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefsKeyHasSeenOnboardingTour, true);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clamped to [minStaleLockMinutes, maxStaleLockMinutes] — see the
|
||||
/// Settings "Advanced" section, which restricts the picker to that range
|
||||
/// anyway; this is a defensive backstop for any other caller.
|
||||
|
|
|
|||
15
lib/services/onboarding_keys.dart
Normal file
15
lib/services/onboarding_keys.dart
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// One [GlobalKey] per on-screen element the first-launch guided tour
|
||||
/// (`lib/widgets/onboarding_tour.dart`, orchestrated from
|
||||
/// `lib/screens/main_shell.dart`) highlights. Each screen that owns one of
|
||||
/// these elements assigns the matching key to the actual widget, so the
|
||||
/// tour can find its on-screen position (via
|
||||
/// `key.currentContext?.findRenderObject()`) without those screens knowing
|
||||
/// anything about the tour itself.
|
||||
class OnboardingKeys {
|
||||
static final receiptsAddButton = GlobalKey(debugLabel: 'onboarding-receipts-add');
|
||||
static final vehiclesAddButton = GlobalKey(debugLabel: 'onboarding-vehicles-add');
|
||||
static final reportDateRange = GlobalKey(debugLabel: 'onboarding-report-date-range');
|
||||
static final cloudStorageCard = GlobalKey(debugLabel: 'onboarding-cloud-storage-card');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue