mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-10-12 10:47:12 +02:00
positioning tutorial
This commit is contained in:
1 parent
eca227032d
commit
656dbc8ce8
15 files changed
+365
-10
No files matched your search
@@ -1,4 +1,10 @@
|
|||||||
{
|
{
|
||||||
|
"2.1.2": {
|
||||||
|
"content": [
|
||||||
|
"• New positioning tutorial - first-time users must drag the map to refine location when creating or editing nodes, helping ensure accurate positioning",
|
||||||
|
"• Tutorial automatically dismisses after moving the map at least 1 meter and never shows again"
|
||||||
|
]
|
||||||
|
},
|
||||||
"2.1.0": {
|
"2.1.0": {
|
||||||
"content": [
|
"content": [
|
||||||
"• Profile tag refinement system - any profile tag with an empty value now shows a dropdown in refine tags",
|
"• Profile tag refinement system - any profile tag with an empty value now shows a dropdown in refine tags",
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ class AppState extends ChangeNotifier {
|
|||||||
late final UploadQueueState _uploadQueueState;
|
late final UploadQueueState _uploadQueueState;
|
||||||
|
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
|
|
||||||
|
// Positioning tutorial state
|
||||||
|
LatLng? _tutorialStartPosition; // Track where the tutorial started
|
||||||
|
VoidCallback? _tutorialCompletionCallback; // Callback when tutorial is completed
|
||||||
Timer? _messageCheckTimer;
|
Timer? _messageCheckTimer;
|
||||||
|
|
||||||
AppState() {
|
AppState() {
|
||||||
@@ -437,6 +441,11 @@ class AppState extends ChangeNotifier {
|
|||||||
target: target,
|
target: target,
|
||||||
refinedTags: refinedTags,
|
refinedTags: refinedTags,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check tutorial completion if position changed
|
||||||
|
if (target != null) {
|
||||||
|
_checkTutorialCompletion(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateEditSession({
|
void updateEditSession({
|
||||||
@@ -455,6 +464,11 @@ class AppState extends ChangeNotifier {
|
|||||||
extractFromWay: extractFromWay,
|
extractFromWay: extractFromWay,
|
||||||
refinedTags: refinedTags,
|
refinedTags: refinedTags,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check tutorial completion if position changed
|
||||||
|
if (target != null) {
|
||||||
|
_checkTutorialCompletion(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For map view to check for pending snap backs
|
// For map view to check for pending snap backs
|
||||||
@@ -462,6 +476,46 @@ class AppState extends ChangeNotifier {
|
|||||||
return _sessionState.consumePendingSnapBack();
|
return _sessionState.consumePendingSnapBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Positioning tutorial methods
|
||||||
|
void registerTutorialCallback(VoidCallback onComplete) {
|
||||||
|
debugPrint('[AppState] Registering tutorial callback');
|
||||||
|
_tutorialCompletionCallback = onComplete;
|
||||||
|
// Record the starting position when tutorial begins
|
||||||
|
if (session?.target != null) {
|
||||||
|
_tutorialStartPosition = session!.target;
|
||||||
|
debugPrint('[AppState] Tutorial start position (add): ${_tutorialStartPosition}');
|
||||||
|
} else if (editSession?.target != null) {
|
||||||
|
_tutorialStartPosition = editSession!.target;
|
||||||
|
debugPrint('[AppState] Tutorial start position (edit): ${_tutorialStartPosition}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearTutorialCallback() {
|
||||||
|
_tutorialCompletionCallback = null;
|
||||||
|
_tutorialStartPosition = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _checkTutorialCompletion(LatLng newPosition) {
|
||||||
|
if (_tutorialCompletionCallback == null || _tutorialStartPosition == null) return;
|
||||||
|
|
||||||
|
// Calculate distance moved
|
||||||
|
final distance = Distance();
|
||||||
|
final distanceMoved = distance.as(LengthUnit.Meter, _tutorialStartPosition!, newPosition);
|
||||||
|
|
||||||
|
debugPrint('[AppState] Tutorial movement check: ${distanceMoved.toStringAsFixed(2)}m (need ${kPositioningTutorialMinMovementMeters}m)');
|
||||||
|
|
||||||
|
if (distanceMoved >= kPositioningTutorialMinMovementMeters) {
|
||||||
|
debugPrint('[AppState] Tutorial completed! Calling callback and marking as complete');
|
||||||
|
// Tutorial completed! Mark as complete and notify callback immediately
|
||||||
|
final callback = _tutorialCompletionCallback;
|
||||||
|
clearTutorialCallback();
|
||||||
|
callback?.call();
|
||||||
|
|
||||||
|
// Mark as complete in background (don't await to avoid delays)
|
||||||
|
ChangelogService().markPositioningTutorialCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void addDirection() {
|
void addDirection() {
|
||||||
_sessionState.addDirection();
|
_sessionState.addDirection();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ const Duration kProximityAlertCooldown = Duration(minutes: 10); // Cooldown betw
|
|||||||
// Node proximity warning configuration (for new/edited nodes that are too close to existing ones)
|
// Node proximity warning configuration (for new/edited nodes that are too close to existing ones)
|
||||||
const double kNodeProximityWarningDistance = 15.0; // meters - distance threshold to show warning
|
const double kNodeProximityWarningDistance = 15.0; // meters - distance threshold to show warning
|
||||||
|
|
||||||
|
// Positioning tutorial configuration
|
||||||
|
const double kPositioningTutorialBlurSigma = 3.0; // Blur strength for sheet overlay
|
||||||
|
const double kPositioningTutorialMinMovementMeters = 1.0; // Minimum map movement to complete tutorial
|
||||||
|
|
||||||
// Navigation route planning configuration
|
// Navigation route planning configuration
|
||||||
const double kNavigationMinRouteDistance = 100.0; // meters - minimum distance between start and end points
|
const double kNavigationMinRouteDistance = 100.0; // meters - minimum distance between start and end points
|
||||||
const double kNavigationDistanceWarningThreshold = 20000.0; // meters - distance threshold for timeout warning (30km)
|
const double kNavigationDistanceWarningThreshold = 20000.0; // meters - distance threshold for timeout warning (30km)
|
||||||
|
|||||||
@@ -446,6 +446,11 @@
|
|||||||
"dontShowAgain": "Diese Anleitung nicht mehr anzeigen",
|
"dontShowAgain": "Diese Anleitung nicht mehr anzeigen",
|
||||||
"gotIt": "Verstanden!"
|
"gotIt": "Verstanden!"
|
||||||
},
|
},
|
||||||
|
"positioningTutorial": {
|
||||||
|
"title": "Position verfeinern",
|
||||||
|
"instructions": "Ziehen Sie die Karte, um die Geräte-Markierung präzise über dem Standort des Überwachungsgeräts zu positionieren.",
|
||||||
|
"hint": "Sie können für bessere Genauigkeit vor der Positionierung hineinzoomen."
|
||||||
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"searchLocation": "Ort suchen",
|
"searchLocation": "Ort suchen",
|
||||||
"searchPlaceholder": "Orte oder Koordinaten suchen...",
|
"searchPlaceholder": "Orte oder Koordinaten suchen...",
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
"dontShowAgain": "Don't show this guide again",
|
"dontShowAgain": "Don't show this guide again",
|
||||||
"gotIt": "Got It!"
|
"gotIt": "Got It!"
|
||||||
},
|
},
|
||||||
|
"positioningTutorial": {
|
||||||
|
"title": "Refine Your Location",
|
||||||
|
"instructions": "Drag the map to position the device marker precisely over the surveillance device's location.",
|
||||||
|
"hint": "You can zoom in for better accuracy before positioning."
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"tagNode": "New Node",
|
"tagNode": "New Node",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
"dontShowAgain": "No mostrar esta guía otra vez",
|
"dontShowAgain": "No mostrar esta guía otra vez",
|
||||||
"gotIt": "¡Entendido!"
|
"gotIt": "¡Entendido!"
|
||||||
},
|
},
|
||||||
|
"positioningTutorial": {
|
||||||
|
"title": "Refinar Ubicación",
|
||||||
|
"instructions": "Arrastra el mapa para posicionar el marcador del dispositivo con precisión sobre la ubicación del dispositivo de vigilancia.",
|
||||||
|
"hint": "Puedes acercar el zoom para obtener mejor precisión antes de posicionar."
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"tagNode": "Nuevo Nodo",
|
"tagNode": "Nuevo Nodo",
|
||||||
"download": "Descargar",
|
"download": "Descargar",
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
"dontShowAgain": "Ne plus afficher ce guide",
|
"dontShowAgain": "Ne plus afficher ce guide",
|
||||||
"gotIt": "Compris !"
|
"gotIt": "Compris !"
|
||||||
},
|
},
|
||||||
|
"positioningTutorial": {
|
||||||
|
"title": "Affiner la Position",
|
||||||
|
"instructions": "Faites glisser la carte pour positionner le marqueur de l'appareil précisément au-dessus de l'emplacement du dispositif de surveillance.",
|
||||||
|
"hint": "Vous pouvez zoomer pour une meilleure précision avant de positionner."
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"tagNode": "Nouveau Nœud",
|
"tagNode": "Nouveau Nœud",
|
||||||
"download": "Télécharger",
|
"download": "Télécharger",
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
"dontShowAgain": "Non mostrare più questa guida",
|
"dontShowAgain": "Non mostrare più questa guida",
|
||||||
"gotIt": "Capito!"
|
"gotIt": "Capito!"
|
||||||
},
|
},
|
||||||
|
"positioningTutorial": {
|
||||||
|
"title": "Affinare la Posizione",
|
||||||
|
"instructions": "Trascina la mappa per posizionare il marcatore del dispositivo precisamente sopra la posizione del dispositivo di sorveglianza.",
|
||||||
|
"hint": "Puoi ingrandire per una maggiore precisione prima di posizionare."
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"tagNode": "Nuovo Nodo",
|
"tagNode": "Nuovo Nodo",
|
||||||
"download": "Scarica",
|
"download": "Scarica",
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
"dontShowAgain": "Não mostrar este guia novamente",
|
"dontShowAgain": "Não mostrar este guia novamente",
|
||||||
"gotIt": "Entendi!"
|
"gotIt": "Entendi!"
|
||||||
},
|
},
|
||||||
|
"positioningTutorial": {
|
||||||
|
"title": "Refinar Posição",
|
||||||
|
"instructions": "Arraste o mapa para posicionar o marcador do dispositivo precisamente sobre a localização do dispositivo de vigilância.",
|
||||||
|
"hint": "Você pode aumentar o zoom para melhor precisão antes de posicionar."
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"tagNode": "Novo Nó",
|
"tagNode": "Novo Nó",
|
||||||
"download": "Baixar",
|
"download": "Baixar",
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
"dontShowAgain": "不再显示此指南",
|
"dontShowAgain": "不再显示此指南",
|
||||||
"gotIt": "明白了!"
|
"gotIt": "明白了!"
|
||||||
},
|
},
|
||||||
|
"positioningTutorial": {
|
||||||
|
"title": "精确定位",
|
||||||
|
"instructions": "拖动地图将设备标记精确定位在监控设备的位置上。",
|
||||||
|
"hint": "您可以在定位前放大地图以获得更高的精度。"
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"tagNode": "新建节点",
|
"tagNode": "新建节点",
|
||||||
"download": "下载",
|
"download": "下载",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class ChangelogService {
|
|||||||
static const String _lastSeenVersionKey = 'last_seen_version';
|
static const String _lastSeenVersionKey = 'last_seen_version';
|
||||||
static const String _hasSeenWelcomeKey = 'has_seen_welcome';
|
static const String _hasSeenWelcomeKey = 'has_seen_welcome';
|
||||||
static const String _hasSeenSubmissionGuideKey = 'has_seen_submission_guide';
|
static const String _hasSeenSubmissionGuideKey = 'has_seen_submission_guide';
|
||||||
|
static const String _hasCompletedPositioningTutorialKey = 'has_completed_positioning_tutorial';
|
||||||
|
|
||||||
Map<String, dynamic>? _changelogData;
|
Map<String, dynamic>? _changelogData;
|
||||||
bool _initialized = false;
|
bool _initialized = false;
|
||||||
@@ -82,6 +83,18 @@ class ChangelogService {
|
|||||||
await prefs.setBool(_hasSeenSubmissionGuideKey, true);
|
await prefs.setBool(_hasSeenSubmissionGuideKey, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if user has completed the positioning tutorial
|
||||||
|
Future<bool> hasCompletedPositioningTutorial() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
return prefs.getBool(_hasCompletedPositioningTutorialKey) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark that user has completed the positioning tutorial
|
||||||
|
Future<void> markPositioningTutorialCompleted() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(_hasCompletedPositioningTutorialKey, true);
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if app version has changed since last launch
|
/// Check if app version has changed since last launch
|
||||||
Future<bool> hasVersionChanged() async {
|
Future<bool> hasVersionChanged() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|||||||
@@ -11,12 +11,83 @@ import '../services/changelog_service.dart';
|
|||||||
import 'refine_tags_sheet.dart';
|
import 'refine_tags_sheet.dart';
|
||||||
import 'proximity_warning_dialog.dart';
|
import 'proximity_warning_dialog.dart';
|
||||||
import 'submission_guide_dialog.dart';
|
import 'submission_guide_dialog.dart';
|
||||||
|
import 'positioning_tutorial_overlay.dart';
|
||||||
|
|
||||||
class AddNodeSheet extends StatelessWidget {
|
class AddNodeSheet extends StatefulWidget {
|
||||||
const AddNodeSheet({super.key, required this.session});
|
const AddNodeSheet({super.key, required this.session});
|
||||||
|
|
||||||
final AddNodeSession session;
|
final AddNodeSession session;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AddNodeSheet> createState() => _AddNodeSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddNodeSheetState extends State<AddNodeSheet> {
|
||||||
|
bool _showTutorial = false;
|
||||||
|
bool _isCheckingTutorial = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_checkTutorialStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkTutorialStatus() async {
|
||||||
|
final hasCompleted = await ChangelogService().hasCompletedPositioningTutorial();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_showTutorial = !hasCompleted;
|
||||||
|
_isCheckingTutorial = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// If tutorial should be shown, register callback with AppState
|
||||||
|
if (_showTutorial) {
|
||||||
|
final appState = context.read<AppState>();
|
||||||
|
appState.registerTutorialCallback(_hideTutorial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Listen for tutorial completion from AppState
|
||||||
|
void _onTutorialCompleted() {
|
||||||
|
_hideTutorial();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Also check periodically if tutorial was completed by another sheet
|
||||||
|
void _recheckTutorialStatus() async {
|
||||||
|
if (_showTutorial) {
|
||||||
|
final hasCompleted = await ChangelogService().hasCompletedPositioningTutorial();
|
||||||
|
if (hasCompleted && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_showTutorial = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _hideTutorial() {
|
||||||
|
debugPrint('[AddNodeSheet] Tutorial completion callback triggered');
|
||||||
|
if (mounted && _showTutorial) {
|
||||||
|
debugPrint('[AddNodeSheet] Hiding tutorial overlay');
|
||||||
|
setState(() {
|
||||||
|
_showTutorial = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
// Clear tutorial callback when widget is disposed
|
||||||
|
if (_showTutorial) {
|
||||||
|
try {
|
||||||
|
context.read<AppState>().clearTutorialCallback();
|
||||||
|
} catch (e) {
|
||||||
|
// Context might be unavailable during disposal, ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _checkProximityAndCommit(BuildContext context, AppState appState, LocalizationService locService) {
|
void _checkProximityAndCommit(BuildContext context, AppState appState, LocalizationService locService) {
|
||||||
_checkSubmissionGuideAndProceed(context, appState, locService);
|
_checkSubmissionGuideAndProceed(context, appState, locService);
|
||||||
}
|
}
|
||||||
@@ -40,14 +111,14 @@ class AddNodeSheet extends StatelessWidget {
|
|||||||
|
|
||||||
void _checkProximityOnly(BuildContext context, AppState appState, LocalizationService locService) {
|
void _checkProximityOnly(BuildContext context, AppState appState, LocalizationService locService) {
|
||||||
// Only check proximity if we have a target location
|
// Only check proximity if we have a target location
|
||||||
if (session.target == null) {
|
if (widget.session.target == null) {
|
||||||
_commitWithoutCheck(context, appState, locService);
|
_commitWithoutCheck(context, appState, locService);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for nearby nodes within the configured distance
|
// Check for nearby nodes within the configured distance
|
||||||
final nearbyNodes = NodeCache.instance.findNodesWithinDistance(
|
final nearbyNodes = NodeCache.instance.findNodesWithinDistance(
|
||||||
session.target!,
|
widget.session.target!,
|
||||||
kNodeProximityWarningDistance,
|
kNodeProximityWarningDistance,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -220,6 +291,7 @@ class AddNodeSheet extends StatelessWidget {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final session = widget.session;
|
||||||
final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList();
|
final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList();
|
||||||
final allowSubmit = appState.isLoggedIn &&
|
final allowSubmit = appState.isLoggedIn &&
|
||||||
submittableProfiles.isNotEmpty &&
|
submittableProfiles.isNotEmpty &&
|
||||||
@@ -246,7 +318,11 @@ class AddNodeSheet extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
fit: StackFit.loose,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -374,6 +450,14 @@ class AddNodeSheet extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Tutorial overlay - show only if tutorial should be shown and we're done checking
|
||||||
|
if (!_isCheckingTutorial && _showTutorial)
|
||||||
|
Positioned.fill(
|
||||||
|
child: PositioningTutorialOverlay(),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,12 +13,66 @@ import 'refine_tags_sheet.dart';
|
|||||||
import 'advanced_edit_options_sheet.dart';
|
import 'advanced_edit_options_sheet.dart';
|
||||||
import 'proximity_warning_dialog.dart';
|
import 'proximity_warning_dialog.dart';
|
||||||
import 'submission_guide_dialog.dart';
|
import 'submission_guide_dialog.dart';
|
||||||
|
import 'positioning_tutorial_overlay.dart';
|
||||||
|
|
||||||
class EditNodeSheet extends StatelessWidget {
|
class EditNodeSheet extends StatefulWidget {
|
||||||
const EditNodeSheet({super.key, required this.session});
|
const EditNodeSheet({super.key, required this.session});
|
||||||
|
|
||||||
final EditNodeSession session;
|
final EditNodeSession session;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EditNodeSheet> createState() => _EditNodeSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||||
|
bool _showTutorial = false;
|
||||||
|
bool _isCheckingTutorial = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_checkTutorialStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkTutorialStatus() async {
|
||||||
|
final hasCompleted = await ChangelogService().hasCompletedPositioningTutorial();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_showTutorial = !hasCompleted;
|
||||||
|
_isCheckingTutorial = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// If tutorial should be shown, register callback with AppState
|
||||||
|
if (_showTutorial) {
|
||||||
|
final appState = context.read<AppState>();
|
||||||
|
appState.registerTutorialCallback(_hideTutorial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _hideTutorial() {
|
||||||
|
debugPrint('[EditNodeSheet] Tutorial completion callback triggered');
|
||||||
|
if (mounted && _showTutorial) {
|
||||||
|
debugPrint('[EditNodeSheet] Hiding tutorial overlay');
|
||||||
|
setState(() {
|
||||||
|
_showTutorial = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
// Clear tutorial callback when widget is disposed
|
||||||
|
if (_showTutorial) {
|
||||||
|
try {
|
||||||
|
context.read<AppState>().clearTutorialCallback();
|
||||||
|
} catch (e) {
|
||||||
|
// Context might be unavailable during disposal, ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _checkProximityAndCommit(BuildContext context, AppState appState, LocalizationService locService) {
|
void _checkProximityAndCommit(BuildContext context, AppState appState, LocalizationService locService) {
|
||||||
_checkSubmissionGuideAndProceed(context, appState, locService);
|
_checkSubmissionGuideAndProceed(context, appState, locService);
|
||||||
}
|
}
|
||||||
@@ -43,9 +97,9 @@ class EditNodeSheet extends StatelessWidget {
|
|||||||
void _checkProximityOnly(BuildContext context, AppState appState, LocalizationService locService) {
|
void _checkProximityOnly(BuildContext context, AppState appState, LocalizationService locService) {
|
||||||
// Check for nearby nodes within the configured distance, excluding the node being edited
|
// Check for nearby nodes within the configured distance, excluding the node being edited
|
||||||
final nearbyNodes = NodeCache.instance.findNodesWithinDistance(
|
final nearbyNodes = NodeCache.instance.findNodesWithinDistance(
|
||||||
session.target,
|
widget.session.target,
|
||||||
kNodeProximityWarningDistance,
|
kNodeProximityWarningDistance,
|
||||||
excludeNodeId: session.originalNode.id,
|
excludeNodeId: widget.session.originalNode.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (nearbyNodes.isNotEmpty) {
|
if (nearbyNodes.isNotEmpty) {
|
||||||
@@ -217,6 +271,7 @@ class EditNodeSheet extends StatelessWidget {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final session = widget.session;
|
||||||
final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList();
|
final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList();
|
||||||
final isSandboxMode = appState.uploadMode == UploadMode.sandbox;
|
final isSandboxMode = appState.uploadMode == UploadMode.sandbox;
|
||||||
final allowSubmit = kEnableNodeEdits &&
|
final allowSubmit = kEnableNodeEdits &&
|
||||||
@@ -245,7 +300,11 @@ class EditNodeSheet extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
fit: StackFit.loose,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -447,6 +506,14 @@ class EditNodeSheet extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Tutorial overlay - show only if tutorial should be shown and we're done checking
|
||||||
|
if (!_isCheckingTutorial && _showTutorial)
|
||||||
|
Positioned.fill(
|
||||||
|
child: PositioningTutorialOverlay(),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -456,7 +523,7 @@ class EditNodeSheet extends StatelessWidget {
|
|||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder: (context) => AdvancedEditOptionsSheet(node: session.originalNode),
|
builder: (context) => AdvancedEditOptionsSheet(node: widget.session.originalNode),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../dev_config.dart';
|
||||||
|
import '../services/localization_service.dart';
|
||||||
|
|
||||||
|
/// Overlay that appears over add/edit node sheets to guide users through
|
||||||
|
/// the positioning tutorial. Shows a blurred background with tutorial text.
|
||||||
|
class PositioningTutorialOverlay extends StatelessWidget {
|
||||||
|
const PositioningTutorialOverlay({
|
||||||
|
super.key,
|
||||||
|
this.onFadeOutComplete,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Called when the fade-out animation completes (if animated)
|
||||||
|
final VoidCallback? onFadeOutComplete;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: LocalizationService.instance,
|
||||||
|
builder: (context, child) {
|
||||||
|
final locService = LocalizationService.instance;
|
||||||
|
|
||||||
|
return ClipRect(
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(
|
||||||
|
sigmaX: kPositioningTutorialBlurSigma,
|
||||||
|
sigmaY: kPositioningTutorialBlurSigma,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.3), // Semi-transparent overlay
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Tutorial icon
|
||||||
|
Icon(
|
||||||
|
Icons.pan_tool_outlined,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Tutorial title
|
||||||
|
Text(
|
||||||
|
locService.t('positioningTutorial.title'),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Tutorial instructions
|
||||||
|
Text(
|
||||||
|
locService.t('positioningTutorial.instructions'),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Additional hint
|
||||||
|
Text(
|
||||||
|
locService.t('positioningTutorial.hint'),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.8),
|
||||||
|
fontSize: 14,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: deflockapp
|
name: deflockapp
|
||||||
description: Map public surveillance infrastructure with OpenStreetMap
|
description: Map public surveillance infrastructure with OpenStreetMap
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 2.1.1+34 # The thing after the + is the version code, incremented with each release
|
version: 2.1.2+35 # The thing after the + is the version code, incremented with each release
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+
|
sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+
|
||||||
|
|||||||
Reference in new issue
Block a user