feat(location): add history playback, GPS functions dialog and UX improvements

This commit is contained in:
2026-04-26 08:25:05 +02:00
parent e901b22981
commit 04b8d1609c
14 changed files with 1248 additions and 172 deletions

View File

@@ -34,7 +34,7 @@ final class LocationControllerProvider
} }
String _$locationControllerHash() => String _$locationControllerHash() =>
r'43534cd92b74ec5fabc9a43c6ef8398846855cf4'; r'324461865e3f1deb94ef6bba29cb3a200e5fe14f';
abstract class _$LocationController extends $AsyncNotifier<LocationState> { abstract class _$LocationController extends $AsyncNotifier<LocationState> {
FutureOr<LocationState> build(); FutureOr<LocationState> build();

View File

@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:legacy_device_state/legacy_device_state.dart'; import 'package:legacy_device_state/legacy_device_state.dart';
@@ -15,12 +16,22 @@ class LocationMapController extends _$LocationMapController {
late final SfTrackingRepository _tracking; late final SfTrackingRepository _tracking;
Timer? _zoomDebounce; Timer? _zoomDebounce;
Timer? _playbackTimer;
Timer? _recenterTimer;
static const Duration _zoomDebounceDelay = Duration(seconds: 1); static const Duration _zoomDebounceDelay = Duration(seconds: 1);
static const int _playbackSeconds = 3;
static const int _recenterSeconds = 3;
VoidCallback? onPlaybackTick;
@override @override
LocationMapState build() { LocationMapState build() {
_tracking = ref.read(sfTrackingProvider); _tracking = ref.read(sfTrackingProvider);
ref.onDispose(() => _zoomDebounce?.cancel()); ref.onDispose(() {
_zoomDebounce?.cancel();
_playbackTimer?.cancel();
_recenterTimer?.cancel();
});
return const LocationMapState(); return const LocationMapState();
} }
@@ -56,6 +67,8 @@ class LocationMapController extends _$LocationMapController {
placingMode: PlacingMode.none, placingMode: PlacingMode.none,
adjustingRadius: false, adjustingRadius: false,
previewPoint: null, previewPoint: null,
editingGeofence: null,
editingFrequentPlace: null,
); );
} }
@@ -75,13 +88,17 @@ class LocationMapController extends _$LocationMapController {
placingMode: PlacingMode.none, placingMode: PlacingMode.none,
adjustingRadius: true, adjustingRadius: true,
previewPoint: center, previewPoint: center,
previewRadius: 200, previewRadius: state.editingGeofence?.radius ?? 200,
); );
} }
void confirmFrequentPlacePlacement() { void confirmFrequentPlacePlacement() {
unawaited(_tracking.legacyLocationPointConfirmed('frequent_place')); unawaited(_tracking.legacyLocationPointConfirmed('frequent_place'));
state = state.copyWith(placingMode: PlacingMode.none, previewPoint: null); state = state.copyWith(
placingMode: PlacingMode.none,
previewPoint: null,
editingFrequentPlace: null,
);
} }
void updatePreviewRadius(double radius) { void updatePreviewRadius(double radius) {
@@ -123,9 +140,19 @@ class LocationMapController extends _$LocationMapController {
state = state.copyWith( state = state.copyWith(
selectedGeofence: null, selectedGeofence: null,
editingGeofence: geofence, editingGeofence: geofence,
placingMode: PlacingMode.geofence,
previewPoint: LatLng(geofence.latitude, geofence.longitude), previewPoint: LatLng(geofence.latitude, geofence.longitude),
previewRadius: geofence.radius, previewRadius: geofence.radius,
adjustingRadius: true, );
}
void startEditingFrequentPlace(FrequentPlaceEntity place) {
unawaited(_tracking.legacyLocationFrequentPlaceSelected());
state = state.copyWith(
selectedFrequentPlace: null,
editingFrequentPlace: place,
placingMode: PlacingMode.frequentPlace,
previewPoint: LatLng(place.lat, place.lng),
); );
} }
@@ -191,6 +218,96 @@ class LocationMapController extends _$LocationMapController {
}); });
} }
void startReveal() {
state = state.copyWith(isRevealAnimating: true, historyRevealCount: 1);
}
void updateRevealCount(int count) {
state = state.copyWith(historyRevealCount: count);
}
void finishReveal() {
state = state.copyWith(isRevealAnimating: false, historyRevealCount: null);
}
void startHistoryNavigation() {
state = state.copyWith(historyNavigationIndex: 0, historyPlaying: false);
}
void stopHistoryNavigation() {
state = state.copyWith(historyNavigationIndex: -1, historyPlaying: false);
}
void setHistoryIndex(int index) {
state = state.copyWith(historyNavigationIndex: index);
}
void nextHistoryPosition(int total) {
if (state.historyNavigationIndex < total - 1) {
state = state.copyWith(
historyNavigationIndex: state.historyNavigationIndex + 1,
);
} else {
state = state.copyWith(historyPlaying: false);
}
}
void previousHistoryPosition() {
if (state.historyNavigationIndex > 0) {
state = state.copyWith(
historyNavigationIndex: state.historyNavigationIndex - 1,
);
}
}
void toggleHistoryPlayback(int total) {
if (state.historyPlaying) {
stopPlaybackTimer();
state = state.copyWith(historyPlaying: false);
} else {
final atEnd = state.historyNavigationIndex >= total - 1;
state = state.copyWith(
historyNavigationIndex: atEnd ? 0 : state.historyNavigationIndex,
historyPlaying: true,
);
startPlaybackTimer(total);
}
}
void startPlaybackTimer(int total) {
_playbackTimer?.cancel();
_playbackTimer = Timer.periodic(const Duration(seconds: _playbackSeconds), (
_,
) {
if (!state.historyPlaying) {
_playbackTimer?.cancel();
return;
}
nextHistoryPosition(total);
onPlaybackTick?.call();
if (!state.historyPlaying) {
_playbackTimer?.cancel();
}
});
}
void stopPlaybackTimer() {
_playbackTimer?.cancel();
_playbackTimer = null;
}
void scheduleRecenter(VoidCallback onRecenter) {
if (!state.isFollowing) return;
_recenterTimer?.cancel();
_recenterTimer = Timer(const Duration(seconds: _recenterSeconds), () {
if (state.isFollowing) onRecenter();
});
}
void cancelRecenter() {
_recenterTimer?.cancel();
}
String _modeName(PlacingMode mode) { String _modeName(PlacingMode mode) {
switch (mode) { switch (mode) {
case PlacingMode.geofence: case PlacingMode.geofence:

View File

@@ -42,7 +42,7 @@ final class LocationMapControllerProvider
} }
String _$locationMapControllerHash() => String _$locationMapControllerHash() =>
r'c6eea4cec7a9a66546e9b66baf384edbb6e320f2'; r'565ab5147dd5ac16cf80210c55e28316bafe2f5a';
abstract class _$LocationMapController extends $Notifier<LocationMapState> { abstract class _$LocationMapController extends $Notifier<LocationMapState> {
LocationMapState build(); LocationMapState build();

View File

@@ -22,10 +22,15 @@ abstract class LocationMapState with _$LocationMapState {
GeofenceEntity? selectedGeofence, GeofenceEntity? selectedGeofence,
GeofenceEntity? editingGeofence, GeofenceEntity? editingGeofence,
FrequentPlaceEntity? selectedFrequentPlace, FrequentPlaceEntity? selectedFrequentPlace,
FrequentPlaceEntity? editingFrequentPlace,
PositionEntity? selectedHistoryPosition, PositionEntity? selectedHistoryPosition,
@Default(false) bool isFollowing, @Default(false) bool isFollowing,
@Default(false) bool actionsExpanded, @Default(false) bool actionsExpanded,
@Default(false) bool frequencyExpanded, @Default(false) bool frequencyExpanded,
@Default(_defaultZoom) double mapZoom, @Default(_defaultZoom) double mapZoom,
@Default(-1) int historyNavigationIndex,
@Default(false) bool historyPlaying,
@Default(false) bool isRevealAnimating,
int? historyRevealCount,
}) = _LocationMapState; }) = _LocationMapState;
} }

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$LocationMapState { mixin _$LocationMapState {
bool get showGeofences; bool get showFrequentPlaces; PlacingMode get placingMode; bool get adjustingRadius; double get previewRadius; LatLng? get previewPoint; GeofenceEntity? get selectedGeofence; GeofenceEntity? get editingGeofence; FrequentPlaceEntity? get selectedFrequentPlace; PositionEntity? get selectedHistoryPosition; bool get isFollowing; bool get actionsExpanded; bool get frequencyExpanded; double get mapZoom; bool get showGeofences; bool get showFrequentPlaces; PlacingMode get placingMode; bool get adjustingRadius; double get previewRadius; LatLng? get previewPoint; GeofenceEntity? get selectedGeofence; GeofenceEntity? get editingGeofence; FrequentPlaceEntity? get selectedFrequentPlace; FrequentPlaceEntity? get editingFrequentPlace; PositionEntity? get selectedHistoryPosition; bool get isFollowing; bool get actionsExpanded; bool get frequencyExpanded; double get mapZoom; int get historyNavigationIndex; bool get historyPlaying; bool get isRevealAnimating; int? get historyRevealCount;
/// Create a copy of LocationMapState /// Create a copy of LocationMapState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $LocationMapStateCopyWith<LocationMapState> get copyWith => _$LocationMapStateCo
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LocationMapState&&(identical(other.showGeofences, showGeofences) || other.showGeofences == showGeofences)&&(identical(other.showFrequentPlaces, showFrequentPlaces) || other.showFrequentPlaces == showFrequentPlaces)&&(identical(other.placingMode, placingMode) || other.placingMode == placingMode)&&(identical(other.adjustingRadius, adjustingRadius) || other.adjustingRadius == adjustingRadius)&&(identical(other.previewRadius, previewRadius) || other.previewRadius == previewRadius)&&(identical(other.previewPoint, previewPoint) || other.previewPoint == previewPoint)&&(identical(other.selectedGeofence, selectedGeofence) || other.selectedGeofence == selectedGeofence)&&(identical(other.editingGeofence, editingGeofence) || other.editingGeofence == editingGeofence)&&(identical(other.selectedFrequentPlace, selectedFrequentPlace) || other.selectedFrequentPlace == selectedFrequentPlace)&&(identical(other.selectedHistoryPosition, selectedHistoryPosition) || other.selectedHistoryPosition == selectedHistoryPosition)&&(identical(other.isFollowing, isFollowing) || other.isFollowing == isFollowing)&&(identical(other.actionsExpanded, actionsExpanded) || other.actionsExpanded == actionsExpanded)&&(identical(other.frequencyExpanded, frequencyExpanded) || other.frequencyExpanded == frequencyExpanded)&&(identical(other.mapZoom, mapZoom) || other.mapZoom == mapZoom)); return identical(this, other) || (other.runtimeType == runtimeType&&other is LocationMapState&&(identical(other.showGeofences, showGeofences) || other.showGeofences == showGeofences)&&(identical(other.showFrequentPlaces, showFrequentPlaces) || other.showFrequentPlaces == showFrequentPlaces)&&(identical(other.placingMode, placingMode) || other.placingMode == placingMode)&&(identical(other.adjustingRadius, adjustingRadius) || other.adjustingRadius == adjustingRadius)&&(identical(other.previewRadius, previewRadius) || other.previewRadius == previewRadius)&&(identical(other.previewPoint, previewPoint) || other.previewPoint == previewPoint)&&(identical(other.selectedGeofence, selectedGeofence) || other.selectedGeofence == selectedGeofence)&&(identical(other.editingGeofence, editingGeofence) || other.editingGeofence == editingGeofence)&&(identical(other.selectedFrequentPlace, selectedFrequentPlace) || other.selectedFrequentPlace == selectedFrequentPlace)&&(identical(other.editingFrequentPlace, editingFrequentPlace) || other.editingFrequentPlace == editingFrequentPlace)&&(identical(other.selectedHistoryPosition, selectedHistoryPosition) || other.selectedHistoryPosition == selectedHistoryPosition)&&(identical(other.isFollowing, isFollowing) || other.isFollowing == isFollowing)&&(identical(other.actionsExpanded, actionsExpanded) || other.actionsExpanded == actionsExpanded)&&(identical(other.frequencyExpanded, frequencyExpanded) || other.frequencyExpanded == frequencyExpanded)&&(identical(other.mapZoom, mapZoom) || other.mapZoom == mapZoom)&&(identical(other.historyNavigationIndex, historyNavigationIndex) || other.historyNavigationIndex == historyNavigationIndex)&&(identical(other.historyPlaying, historyPlaying) || other.historyPlaying == historyPlaying)&&(identical(other.isRevealAnimating, isRevealAnimating) || other.isRevealAnimating == isRevealAnimating)&&(identical(other.historyRevealCount, historyRevealCount) || other.historyRevealCount == historyRevealCount));
} }
@override @override
int get hashCode => Object.hash(runtimeType,showGeofences,showFrequentPlaces,placingMode,adjustingRadius,previewRadius,previewPoint,selectedGeofence,editingGeofence,selectedFrequentPlace,selectedHistoryPosition,isFollowing,actionsExpanded,frequencyExpanded,mapZoom); int get hashCode => Object.hashAll([runtimeType,showGeofences,showFrequentPlaces,placingMode,adjustingRadius,previewRadius,previewPoint,selectedGeofence,editingGeofence,selectedFrequentPlace,editingFrequentPlace,selectedHistoryPosition,isFollowing,actionsExpanded,frequencyExpanded,mapZoom,historyNavigationIndex,historyPlaying,isRevealAnimating,historyRevealCount]);
@override @override
String toString() { String toString() {
return 'LocationMapState(showGeofences: $showGeofences, showFrequentPlaces: $showFrequentPlaces, placingMode: $placingMode, adjustingRadius: $adjustingRadius, previewRadius: $previewRadius, previewPoint: $previewPoint, selectedGeofence: $selectedGeofence, editingGeofence: $editingGeofence, selectedFrequentPlace: $selectedFrequentPlace, selectedHistoryPosition: $selectedHistoryPosition, isFollowing: $isFollowing, actionsExpanded: $actionsExpanded, frequencyExpanded: $frequencyExpanded, mapZoom: $mapZoom)'; return 'LocationMapState(showGeofences: $showGeofences, showFrequentPlaces: $showFrequentPlaces, placingMode: $placingMode, adjustingRadius: $adjustingRadius, previewRadius: $previewRadius, previewPoint: $previewPoint, selectedGeofence: $selectedGeofence, editingGeofence: $editingGeofence, selectedFrequentPlace: $selectedFrequentPlace, editingFrequentPlace: $editingFrequentPlace, selectedHistoryPosition: $selectedHistoryPosition, isFollowing: $isFollowing, actionsExpanded: $actionsExpanded, frequencyExpanded: $frequencyExpanded, mapZoom: $mapZoom, historyNavigationIndex: $historyNavigationIndex, historyPlaying: $historyPlaying, isRevealAnimating: $isRevealAnimating, historyRevealCount: $historyRevealCount)';
} }
@@ -45,11 +45,11 @@ abstract mixin class $LocationMapStateCopyWith<$Res> {
factory $LocationMapStateCopyWith(LocationMapState value, $Res Function(LocationMapState) _then) = _$LocationMapStateCopyWithImpl; factory $LocationMapStateCopyWith(LocationMapState value, $Res Function(LocationMapState) _then) = _$LocationMapStateCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, FrequentPlaceEntity? editingFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom, int historyNavigationIndex, bool historyPlaying, bool isRevealAnimating, int? historyRevealCount
}); });
$GeofenceEntityCopyWith<$Res>? get selectedGeofence;$GeofenceEntityCopyWith<$Res>? get editingGeofence;$FrequentPlaceEntityCopyWith<$Res>? get selectedFrequentPlace;$PositionEntityCopyWith<$Res>? get selectedHistoryPosition; $GeofenceEntityCopyWith<$Res>? get selectedGeofence;$GeofenceEntityCopyWith<$Res>? get editingGeofence;$FrequentPlaceEntityCopyWith<$Res>? get selectedFrequentPlace;$FrequentPlaceEntityCopyWith<$Res>? get editingFrequentPlace;$PositionEntityCopyWith<$Res>? get selectedHistoryPosition;
} }
/// @nodoc /// @nodoc
@@ -62,7 +62,7 @@ class _$LocationMapStateCopyWithImpl<$Res>
/// Create a copy of LocationMapState /// Create a copy of LocationMapState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? showGeofences = null,Object? showFrequentPlaces = null,Object? placingMode = null,Object? adjustingRadius = null,Object? previewRadius = null,Object? previewPoint = freezed,Object? selectedGeofence = freezed,Object? editingGeofence = freezed,Object? selectedFrequentPlace = freezed,Object? selectedHistoryPosition = freezed,Object? isFollowing = null,Object? actionsExpanded = null,Object? frequencyExpanded = null,Object? mapZoom = null,}) { @pragma('vm:prefer-inline') @override $Res call({Object? showGeofences = null,Object? showFrequentPlaces = null,Object? placingMode = null,Object? adjustingRadius = null,Object? previewRadius = null,Object? previewPoint = freezed,Object? selectedGeofence = freezed,Object? editingGeofence = freezed,Object? selectedFrequentPlace = freezed,Object? editingFrequentPlace = freezed,Object? selectedHistoryPosition = freezed,Object? isFollowing = null,Object? actionsExpanded = null,Object? frequencyExpanded = null,Object? mapZoom = null,Object? historyNavigationIndex = null,Object? historyPlaying = null,Object? isRevealAnimating = null,Object? historyRevealCount = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
showGeofences: null == showGeofences ? _self.showGeofences : showGeofences // ignore: cast_nullable_to_non_nullable showGeofences: null == showGeofences ? _self.showGeofences : showGeofences // ignore: cast_nullable_to_non_nullable
as bool,showFrequentPlaces: null == showFrequentPlaces ? _self.showFrequentPlaces : showFrequentPlaces // ignore: cast_nullable_to_non_nullable as bool,showFrequentPlaces: null == showFrequentPlaces ? _self.showFrequentPlaces : showFrequentPlaces // ignore: cast_nullable_to_non_nullable
@@ -73,12 +73,17 @@ as double,previewPoint: freezed == previewPoint ? _self.previewPoint : previewPo
as LatLng?,selectedGeofence: freezed == selectedGeofence ? _self.selectedGeofence : selectedGeofence // ignore: cast_nullable_to_non_nullable as LatLng?,selectedGeofence: freezed == selectedGeofence ? _self.selectedGeofence : selectedGeofence // ignore: cast_nullable_to_non_nullable
as GeofenceEntity?,editingGeofence: freezed == editingGeofence ? _self.editingGeofence : editingGeofence // ignore: cast_nullable_to_non_nullable as GeofenceEntity?,editingGeofence: freezed == editingGeofence ? _self.editingGeofence : editingGeofence // ignore: cast_nullable_to_non_nullable
as GeofenceEntity?,selectedFrequentPlace: freezed == selectedFrequentPlace ? _self.selectedFrequentPlace : selectedFrequentPlace // ignore: cast_nullable_to_non_nullable as GeofenceEntity?,selectedFrequentPlace: freezed == selectedFrequentPlace ? _self.selectedFrequentPlace : selectedFrequentPlace // ignore: cast_nullable_to_non_nullable
as FrequentPlaceEntity?,editingFrequentPlace: freezed == editingFrequentPlace ? _self.editingFrequentPlace : editingFrequentPlace // ignore: cast_nullable_to_non_nullable
as FrequentPlaceEntity?,selectedHistoryPosition: freezed == selectedHistoryPosition ? _self.selectedHistoryPosition : selectedHistoryPosition // ignore: cast_nullable_to_non_nullable as FrequentPlaceEntity?,selectedHistoryPosition: freezed == selectedHistoryPosition ? _self.selectedHistoryPosition : selectedHistoryPosition // ignore: cast_nullable_to_non_nullable
as PositionEntity?,isFollowing: null == isFollowing ? _self.isFollowing : isFollowing // ignore: cast_nullable_to_non_nullable as PositionEntity?,isFollowing: null == isFollowing ? _self.isFollowing : isFollowing // ignore: cast_nullable_to_non_nullable
as bool,actionsExpanded: null == actionsExpanded ? _self.actionsExpanded : actionsExpanded // ignore: cast_nullable_to_non_nullable as bool,actionsExpanded: null == actionsExpanded ? _self.actionsExpanded : actionsExpanded // ignore: cast_nullable_to_non_nullable
as bool,frequencyExpanded: null == frequencyExpanded ? _self.frequencyExpanded : frequencyExpanded // ignore: cast_nullable_to_non_nullable as bool,frequencyExpanded: null == frequencyExpanded ? _self.frequencyExpanded : frequencyExpanded // ignore: cast_nullable_to_non_nullable
as bool,mapZoom: null == mapZoom ? _self.mapZoom : mapZoom // ignore: cast_nullable_to_non_nullable as bool,mapZoom: null == mapZoom ? _self.mapZoom : mapZoom // ignore: cast_nullable_to_non_nullable
as double, as double,historyNavigationIndex: null == historyNavigationIndex ? _self.historyNavigationIndex : historyNavigationIndex // ignore: cast_nullable_to_non_nullable
as int,historyPlaying: null == historyPlaying ? _self.historyPlaying : historyPlaying // ignore: cast_nullable_to_non_nullable
as bool,isRevealAnimating: null == isRevealAnimating ? _self.isRevealAnimating : isRevealAnimating // ignore: cast_nullable_to_non_nullable
as bool,historyRevealCount: freezed == historyRevealCount ? _self.historyRevealCount : historyRevealCount // ignore: cast_nullable_to_non_nullable
as int?,
)); ));
} }
/// Create a copy of LocationMapState /// Create a copy of LocationMapState
@@ -121,6 +126,18 @@ $FrequentPlaceEntityCopyWith<$Res>? get selectedFrequentPlace {
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$FrequentPlaceEntityCopyWith<$Res>? get editingFrequentPlace {
if (_self.editingFrequentPlace == null) {
return null;
}
return $FrequentPlaceEntityCopyWith<$Res>(_self.editingFrequentPlace!, (value) {
return _then(_self.copyWith(editingFrequentPlace: value));
});
}/// Create a copy of LocationMapState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$PositionEntityCopyWith<$Res>? get selectedHistoryPosition { $PositionEntityCopyWith<$Res>? get selectedHistoryPosition {
if (_self.selectedHistoryPosition == null) { if (_self.selectedHistoryPosition == null) {
return null; return null;
@@ -211,10 +228,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, FrequentPlaceEntity? editingFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom, int historyNavigationIndex, bool historyPlaying, bool isRevealAnimating, int? historyRevealCount)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _LocationMapState() when $default != null: case _LocationMapState() when $default != null:
return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.frequencyExpanded,_that.mapZoom);case _: return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.editingFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.frequencyExpanded,_that.mapZoom,_that.historyNavigationIndex,_that.historyPlaying,_that.isRevealAnimating,_that.historyRevealCount);case _:
return orElse(); return orElse();
} }
@@ -232,10 +249,10 @@ return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, FrequentPlaceEntity? editingFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom, int historyNavigationIndex, bool historyPlaying, bool isRevealAnimating, int? historyRevealCount) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _LocationMapState(): case _LocationMapState():
return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.frequencyExpanded,_that.mapZoom);case _: return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.editingFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.frequencyExpanded,_that.mapZoom,_that.historyNavigationIndex,_that.historyPlaying,_that.isRevealAnimating,_that.historyRevealCount);case _:
throw StateError('Unexpected subclass'); throw StateError('Unexpected subclass');
} }
@@ -252,10 +269,10 @@ return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, FrequentPlaceEntity? editingFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom, int historyNavigationIndex, bool historyPlaying, bool isRevealAnimating, int? historyRevealCount)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _LocationMapState() when $default != null: case _LocationMapState() when $default != null:
return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.frequencyExpanded,_that.mapZoom);case _: return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.editingFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.frequencyExpanded,_that.mapZoom,_that.historyNavigationIndex,_that.historyPlaying,_that.isRevealAnimating,_that.historyRevealCount);case _:
return null; return null;
} }
@@ -267,7 +284,7 @@ return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_
class _LocationMapState implements LocationMapState { class _LocationMapState implements LocationMapState {
const _LocationMapState({this.showGeofences = true, this.showFrequentPlaces = true, this.placingMode = PlacingMode.none, this.adjustingRadius = false, this.previewRadius = 200.0, this.previewPoint, this.selectedGeofence, this.editingGeofence, this.selectedFrequentPlace, this.selectedHistoryPosition, this.isFollowing = false, this.actionsExpanded = false, this.frequencyExpanded = false, this.mapZoom = _defaultZoom}); const _LocationMapState({this.showGeofences = true, this.showFrequentPlaces = true, this.placingMode = PlacingMode.none, this.adjustingRadius = false, this.previewRadius = 200.0, this.previewPoint, this.selectedGeofence, this.editingGeofence, this.selectedFrequentPlace, this.editingFrequentPlace, this.selectedHistoryPosition, this.isFollowing = false, this.actionsExpanded = false, this.frequencyExpanded = false, this.mapZoom = _defaultZoom, this.historyNavigationIndex = -1, this.historyPlaying = false, this.isRevealAnimating = false, this.historyRevealCount});
@override@JsonKey() final bool showGeofences; @override@JsonKey() final bool showGeofences;
@@ -279,11 +296,16 @@ class _LocationMapState implements LocationMapState {
@override final GeofenceEntity? selectedGeofence; @override final GeofenceEntity? selectedGeofence;
@override final GeofenceEntity? editingGeofence; @override final GeofenceEntity? editingGeofence;
@override final FrequentPlaceEntity? selectedFrequentPlace; @override final FrequentPlaceEntity? selectedFrequentPlace;
@override final FrequentPlaceEntity? editingFrequentPlace;
@override final PositionEntity? selectedHistoryPosition; @override final PositionEntity? selectedHistoryPosition;
@override@JsonKey() final bool isFollowing; @override@JsonKey() final bool isFollowing;
@override@JsonKey() final bool actionsExpanded; @override@JsonKey() final bool actionsExpanded;
@override@JsonKey() final bool frequencyExpanded; @override@JsonKey() final bool frequencyExpanded;
@override@JsonKey() final double mapZoom; @override@JsonKey() final double mapZoom;
@override@JsonKey() final int historyNavigationIndex;
@override@JsonKey() final bool historyPlaying;
@override@JsonKey() final bool isRevealAnimating;
@override final int? historyRevealCount;
/// Create a copy of LocationMapState /// Create a copy of LocationMapState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -295,16 +317,16 @@ _$LocationMapStateCopyWith<_LocationMapState> get copyWith => __$LocationMapStat
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LocationMapState&&(identical(other.showGeofences, showGeofences) || other.showGeofences == showGeofences)&&(identical(other.showFrequentPlaces, showFrequentPlaces) || other.showFrequentPlaces == showFrequentPlaces)&&(identical(other.placingMode, placingMode) || other.placingMode == placingMode)&&(identical(other.adjustingRadius, adjustingRadius) || other.adjustingRadius == adjustingRadius)&&(identical(other.previewRadius, previewRadius) || other.previewRadius == previewRadius)&&(identical(other.previewPoint, previewPoint) || other.previewPoint == previewPoint)&&(identical(other.selectedGeofence, selectedGeofence) || other.selectedGeofence == selectedGeofence)&&(identical(other.editingGeofence, editingGeofence) || other.editingGeofence == editingGeofence)&&(identical(other.selectedFrequentPlace, selectedFrequentPlace) || other.selectedFrequentPlace == selectedFrequentPlace)&&(identical(other.selectedHistoryPosition, selectedHistoryPosition) || other.selectedHistoryPosition == selectedHistoryPosition)&&(identical(other.isFollowing, isFollowing) || other.isFollowing == isFollowing)&&(identical(other.actionsExpanded, actionsExpanded) || other.actionsExpanded == actionsExpanded)&&(identical(other.frequencyExpanded, frequencyExpanded) || other.frequencyExpanded == frequencyExpanded)&&(identical(other.mapZoom, mapZoom) || other.mapZoom == mapZoom)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _LocationMapState&&(identical(other.showGeofences, showGeofences) || other.showGeofences == showGeofences)&&(identical(other.showFrequentPlaces, showFrequentPlaces) || other.showFrequentPlaces == showFrequentPlaces)&&(identical(other.placingMode, placingMode) || other.placingMode == placingMode)&&(identical(other.adjustingRadius, adjustingRadius) || other.adjustingRadius == adjustingRadius)&&(identical(other.previewRadius, previewRadius) || other.previewRadius == previewRadius)&&(identical(other.previewPoint, previewPoint) || other.previewPoint == previewPoint)&&(identical(other.selectedGeofence, selectedGeofence) || other.selectedGeofence == selectedGeofence)&&(identical(other.editingGeofence, editingGeofence) || other.editingGeofence == editingGeofence)&&(identical(other.selectedFrequentPlace, selectedFrequentPlace) || other.selectedFrequentPlace == selectedFrequentPlace)&&(identical(other.editingFrequentPlace, editingFrequentPlace) || other.editingFrequentPlace == editingFrequentPlace)&&(identical(other.selectedHistoryPosition, selectedHistoryPosition) || other.selectedHistoryPosition == selectedHistoryPosition)&&(identical(other.isFollowing, isFollowing) || other.isFollowing == isFollowing)&&(identical(other.actionsExpanded, actionsExpanded) || other.actionsExpanded == actionsExpanded)&&(identical(other.frequencyExpanded, frequencyExpanded) || other.frequencyExpanded == frequencyExpanded)&&(identical(other.mapZoom, mapZoom) || other.mapZoom == mapZoom)&&(identical(other.historyNavigationIndex, historyNavigationIndex) || other.historyNavigationIndex == historyNavigationIndex)&&(identical(other.historyPlaying, historyPlaying) || other.historyPlaying == historyPlaying)&&(identical(other.isRevealAnimating, isRevealAnimating) || other.isRevealAnimating == isRevealAnimating)&&(identical(other.historyRevealCount, historyRevealCount) || other.historyRevealCount == historyRevealCount));
} }
@override @override
int get hashCode => Object.hash(runtimeType,showGeofences,showFrequentPlaces,placingMode,adjustingRadius,previewRadius,previewPoint,selectedGeofence,editingGeofence,selectedFrequentPlace,selectedHistoryPosition,isFollowing,actionsExpanded,frequencyExpanded,mapZoom); int get hashCode => Object.hashAll([runtimeType,showGeofences,showFrequentPlaces,placingMode,adjustingRadius,previewRadius,previewPoint,selectedGeofence,editingGeofence,selectedFrequentPlace,editingFrequentPlace,selectedHistoryPosition,isFollowing,actionsExpanded,frequencyExpanded,mapZoom,historyNavigationIndex,historyPlaying,isRevealAnimating,historyRevealCount]);
@override @override
String toString() { String toString() {
return 'LocationMapState(showGeofences: $showGeofences, showFrequentPlaces: $showFrequentPlaces, placingMode: $placingMode, adjustingRadius: $adjustingRadius, previewRadius: $previewRadius, previewPoint: $previewPoint, selectedGeofence: $selectedGeofence, editingGeofence: $editingGeofence, selectedFrequentPlace: $selectedFrequentPlace, selectedHistoryPosition: $selectedHistoryPosition, isFollowing: $isFollowing, actionsExpanded: $actionsExpanded, frequencyExpanded: $frequencyExpanded, mapZoom: $mapZoom)'; return 'LocationMapState(showGeofences: $showGeofences, showFrequentPlaces: $showFrequentPlaces, placingMode: $placingMode, adjustingRadius: $adjustingRadius, previewRadius: $previewRadius, previewPoint: $previewPoint, selectedGeofence: $selectedGeofence, editingGeofence: $editingGeofence, selectedFrequentPlace: $selectedFrequentPlace, editingFrequentPlace: $editingFrequentPlace, selectedHistoryPosition: $selectedHistoryPosition, isFollowing: $isFollowing, actionsExpanded: $actionsExpanded, frequencyExpanded: $frequencyExpanded, mapZoom: $mapZoom, historyNavigationIndex: $historyNavigationIndex, historyPlaying: $historyPlaying, isRevealAnimating: $isRevealAnimating, historyRevealCount: $historyRevealCount)';
} }
@@ -315,11 +337,11 @@ abstract mixin class _$LocationMapStateCopyWith<$Res> implements $LocationMapSta
factory _$LocationMapStateCopyWith(_LocationMapState value, $Res Function(_LocationMapState) _then) = __$LocationMapStateCopyWithImpl; factory _$LocationMapStateCopyWith(_LocationMapState value, $Res Function(_LocationMapState) _then) = __$LocationMapStateCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, FrequentPlaceEntity? editingFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom, int historyNavigationIndex, bool historyPlaying, bool isRevealAnimating, int? historyRevealCount
}); });
@override $GeofenceEntityCopyWith<$Res>? get selectedGeofence;@override $GeofenceEntityCopyWith<$Res>? get editingGeofence;@override $FrequentPlaceEntityCopyWith<$Res>? get selectedFrequentPlace;@override $PositionEntityCopyWith<$Res>? get selectedHistoryPosition; @override $GeofenceEntityCopyWith<$Res>? get selectedGeofence;@override $GeofenceEntityCopyWith<$Res>? get editingGeofence;@override $FrequentPlaceEntityCopyWith<$Res>? get selectedFrequentPlace;@override $FrequentPlaceEntityCopyWith<$Res>? get editingFrequentPlace;@override $PositionEntityCopyWith<$Res>? get selectedHistoryPosition;
} }
/// @nodoc /// @nodoc
@@ -332,7 +354,7 @@ class __$LocationMapStateCopyWithImpl<$Res>
/// Create a copy of LocationMapState /// Create a copy of LocationMapState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? showGeofences = null,Object? showFrequentPlaces = null,Object? placingMode = null,Object? adjustingRadius = null,Object? previewRadius = null,Object? previewPoint = freezed,Object? selectedGeofence = freezed,Object? editingGeofence = freezed,Object? selectedFrequentPlace = freezed,Object? selectedHistoryPosition = freezed,Object? isFollowing = null,Object? actionsExpanded = null,Object? frequencyExpanded = null,Object? mapZoom = null,}) { @override @pragma('vm:prefer-inline') $Res call({Object? showGeofences = null,Object? showFrequentPlaces = null,Object? placingMode = null,Object? adjustingRadius = null,Object? previewRadius = null,Object? previewPoint = freezed,Object? selectedGeofence = freezed,Object? editingGeofence = freezed,Object? selectedFrequentPlace = freezed,Object? editingFrequentPlace = freezed,Object? selectedHistoryPosition = freezed,Object? isFollowing = null,Object? actionsExpanded = null,Object? frequencyExpanded = null,Object? mapZoom = null,Object? historyNavigationIndex = null,Object? historyPlaying = null,Object? isRevealAnimating = null,Object? historyRevealCount = freezed,}) {
return _then(_LocationMapState( return _then(_LocationMapState(
showGeofences: null == showGeofences ? _self.showGeofences : showGeofences // ignore: cast_nullable_to_non_nullable showGeofences: null == showGeofences ? _self.showGeofences : showGeofences // ignore: cast_nullable_to_non_nullable
as bool,showFrequentPlaces: null == showFrequentPlaces ? _self.showFrequentPlaces : showFrequentPlaces // ignore: cast_nullable_to_non_nullable as bool,showFrequentPlaces: null == showFrequentPlaces ? _self.showFrequentPlaces : showFrequentPlaces // ignore: cast_nullable_to_non_nullable
@@ -343,12 +365,17 @@ as double,previewPoint: freezed == previewPoint ? _self.previewPoint : previewPo
as LatLng?,selectedGeofence: freezed == selectedGeofence ? _self.selectedGeofence : selectedGeofence // ignore: cast_nullable_to_non_nullable as LatLng?,selectedGeofence: freezed == selectedGeofence ? _self.selectedGeofence : selectedGeofence // ignore: cast_nullable_to_non_nullable
as GeofenceEntity?,editingGeofence: freezed == editingGeofence ? _self.editingGeofence : editingGeofence // ignore: cast_nullable_to_non_nullable as GeofenceEntity?,editingGeofence: freezed == editingGeofence ? _self.editingGeofence : editingGeofence // ignore: cast_nullable_to_non_nullable
as GeofenceEntity?,selectedFrequentPlace: freezed == selectedFrequentPlace ? _self.selectedFrequentPlace : selectedFrequentPlace // ignore: cast_nullable_to_non_nullable as GeofenceEntity?,selectedFrequentPlace: freezed == selectedFrequentPlace ? _self.selectedFrequentPlace : selectedFrequentPlace // ignore: cast_nullable_to_non_nullable
as FrequentPlaceEntity?,editingFrequentPlace: freezed == editingFrequentPlace ? _self.editingFrequentPlace : editingFrequentPlace // ignore: cast_nullable_to_non_nullable
as FrequentPlaceEntity?,selectedHistoryPosition: freezed == selectedHistoryPosition ? _self.selectedHistoryPosition : selectedHistoryPosition // ignore: cast_nullable_to_non_nullable as FrequentPlaceEntity?,selectedHistoryPosition: freezed == selectedHistoryPosition ? _self.selectedHistoryPosition : selectedHistoryPosition // ignore: cast_nullable_to_non_nullable
as PositionEntity?,isFollowing: null == isFollowing ? _self.isFollowing : isFollowing // ignore: cast_nullable_to_non_nullable as PositionEntity?,isFollowing: null == isFollowing ? _self.isFollowing : isFollowing // ignore: cast_nullable_to_non_nullable
as bool,actionsExpanded: null == actionsExpanded ? _self.actionsExpanded : actionsExpanded // ignore: cast_nullable_to_non_nullable as bool,actionsExpanded: null == actionsExpanded ? _self.actionsExpanded : actionsExpanded // ignore: cast_nullable_to_non_nullable
as bool,frequencyExpanded: null == frequencyExpanded ? _self.frequencyExpanded : frequencyExpanded // ignore: cast_nullable_to_non_nullable as bool,frequencyExpanded: null == frequencyExpanded ? _self.frequencyExpanded : frequencyExpanded // ignore: cast_nullable_to_non_nullable
as bool,mapZoom: null == mapZoom ? _self.mapZoom : mapZoom // ignore: cast_nullable_to_non_nullable as bool,mapZoom: null == mapZoom ? _self.mapZoom : mapZoom // ignore: cast_nullable_to_non_nullable
as double, as double,historyNavigationIndex: null == historyNavigationIndex ? _self.historyNavigationIndex : historyNavigationIndex // ignore: cast_nullable_to_non_nullable
as int,historyPlaying: null == historyPlaying ? _self.historyPlaying : historyPlaying // ignore: cast_nullable_to_non_nullable
as bool,isRevealAnimating: null == isRevealAnimating ? _self.isRevealAnimating : isRevealAnimating // ignore: cast_nullable_to_non_nullable
as bool,historyRevealCount: freezed == historyRevealCount ? _self.historyRevealCount : historyRevealCount // ignore: cast_nullable_to_non_nullable
as int?,
)); ));
} }
@@ -392,6 +419,18 @@ $FrequentPlaceEntityCopyWith<$Res>? get selectedFrequentPlace {
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$FrequentPlaceEntityCopyWith<$Res>? get editingFrequentPlace {
if (_self.editingFrequentPlace == null) {
return null;
}
return $FrequentPlaceEntityCopyWith<$Res>(_self.editingFrequentPlace!, (value) {
return _then(_self.copyWith(editingFrequentPlace: value));
});
}/// Create a copy of LocationMapState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$PositionEntityCopyWith<$Res>? get selectedHistoryPosition { $PositionEntityCopyWith<$Res>? get selectedHistoryPosition {
if (_self.selectedHistoryPosition == null) { if (_self.selectedHistoryPosition == null) {
return null; return null;

View File

@@ -14,8 +14,15 @@ class LocationListSheet extends ConsumerStatefulWidget {
final List<FrequentPlaceEntity> frequentPlaces; final List<FrequentPlaceEntity> frequentPlaces;
final List<PositionEntity> positionHistory; final List<PositionEntity> positionHistory;
final Color primaryColor; final Color primaryColor;
final bool hasHiddenGeofences;
final bool hasHiddenFrequentPlaces;
final bool hasHiddenHistory;
final ValueChanged<GeofenceEntity> onGeofenceTap; final ValueChanged<GeofenceEntity> onGeofenceTap;
final ValueChanged<GeofenceEntity>? onGeofenceEdit;
final ValueChanged<GeofenceEntity>? onGeofenceDelete;
final ValueChanged<FrequentPlaceEntity> onFrequentPlaceTap; final ValueChanged<FrequentPlaceEntity> onFrequentPlaceTap;
final ValueChanged<FrequentPlaceEntity>? onFrequentPlaceEdit;
final ValueChanged<FrequentPlaceEntity>? onFrequentPlaceDelete;
final ValueChanged<PositionEntity> onHistoryTap; final ValueChanged<PositionEntity> onHistoryTap;
const LocationListSheet({ const LocationListSheet({
@@ -24,8 +31,15 @@ class LocationListSheet extends ConsumerStatefulWidget {
required this.frequentPlaces, required this.frequentPlaces,
required this.positionHistory, required this.positionHistory,
required this.primaryColor, required this.primaryColor,
this.hasHiddenGeofences = false,
this.hasHiddenFrequentPlaces = false,
this.hasHiddenHistory = false,
required this.onGeofenceTap, required this.onGeofenceTap,
this.onGeofenceEdit,
this.onGeofenceDelete,
required this.onFrequentPlaceTap, required this.onFrequentPlaceTap,
this.onFrequentPlaceEdit,
this.onFrequentPlaceDelete,
required this.onHistoryTap, required this.onHistoryTap,
}); });
@@ -34,6 +48,11 @@ class LocationListSheet extends ConsumerStatefulWidget {
} }
class _LocationListSheetState extends ConsumerState<LocationListSheet> { class _LocationListSheetState extends ConsumerState<LocationListSheet> {
bool get _hasHiddenItems =>
widget.hasHiddenGeofences ||
widget.hasHiddenFrequentPlaces ||
widget.hasHiddenHistory;
List<PositionEntity> _filterHistory(String? selectedType) { List<PositionEntity> _filterHistory(String? selectedType) {
if (selectedType == null) return widget.positionHistory; if (selectedType == null) return widget.positionHistory;
return widget.positionHistory return widget.positionHistory
@@ -90,6 +109,12 @@ class _LocationListSheetState extends ConsumerState<LocationListSheet> {
color: Colors.blue, color: Colors.blue,
icon: Icons.shield, icon: Icons.shield,
onTap: () => widget.onGeofenceTap(g), onTap: () => widget.onGeofenceTap(g),
onEdit: widget.onGeofenceEdit != null
? () => widget.onGeofenceEdit!(g)
: null,
onDelete: widget.onGeofenceDelete != null
? () => widget.onGeofenceDelete!(g)
: null,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -111,6 +136,12 @@ class _LocationListSheetState extends ConsumerState<LocationListSheet> {
color: Colors.orange, color: Colors.orange,
icon: Icons.home_rounded, icon: Icons.home_rounded,
onTap: () => widget.onFrequentPlaceTap(fp), onTap: () => widget.onFrequentPlaceTap(fp),
onEdit: widget.onFrequentPlaceEdit != null
? () => widget.onFrequentPlaceEdit!(fp)
: null,
onDelete: widget.onFrequentPlaceDelete != null
? () => widget.onFrequentPlaceDelete!(fp)
: null,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -136,11 +167,15 @@ class _LocationListSheetState extends ConsumerState<LocationListSheet> {
padding: const EdgeInsets.only(top: 40), padding: const EdgeInsets.only(top: 40),
child: Center( child: Center(
child: Text( child: Text(
context.translate(I18n.locationListNoItems), _hasHiddenItems
? context.translate(
I18n.locationListHiddenItems)
: context.translate(I18n.locationListNoItems),
style: const TextStyle( style: const TextStyle(
color: Colors.grey, color: Colors.grey,
fontSize: 14, fontSize: 14,
), ),
textAlign: TextAlign.center,
), ),
), ),
), ),
@@ -273,6 +308,8 @@ class _LocationListSheetState extends ConsumerState<LocationListSheet> {
required Color color, required Color color,
required IconData icon, required IconData icon,
required VoidCallback onTap, required VoidCallback onTap,
VoidCallback? onEdit,
VoidCallback? onDelete,
}) { }) {
return Card( return Card(
margin: const EdgeInsets.only(bottom: 6), margin: const EdgeInsets.only(bottom: 6),
@@ -302,11 +339,41 @@ class _LocationListSheetState extends ConsumerState<LocationListSheet> {
subtitle, subtitle,
style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant),
), ),
trailing: Icon( trailing: onEdit != null || onDelete != null
Icons.chevron_right, ? Row(
color: Theme.of(context).colorScheme.outline, mainAxisSize: MainAxisSize.min,
size: 20, children: [
), if (onEdit != null)
GestureDetector(
onTap: onEdit,
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(
Icons.edit_outlined,
size: 18,
color: color,
),
),
),
if (onDelete != null)
GestureDetector(
onTap: onDelete,
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(
Icons.delete_outline,
size: 18,
color: Theme.of(context).colorScheme.error,
),
),
),
],
)
: Icon(
Icons.chevron_right,
color: Theme.of(context).colorScheme.outline,
size: 20,
),
onTap: onTap, onTap: onTap,
), ),
); );

View File

@@ -20,11 +20,15 @@ import 'package:utils/utils.dart';
import 'device_banner.dart'; import 'device_banner.dart';
import 'name_input_sheet.dart'; import 'name_input_sheet.dart';
import 'location_list_sheet.dart'; import 'location_list_sheet.dart';
import 'map_controls/layer_toggles.dart'; import 'map_controls/gps_control_panel.dart';
import 'map_controls/history_player.dart';
import 'map_controls/map_action_button.dart';
import 'map_controls/gps_functions_dialog.dart';
import 'map_controls/map_actions_panel.dart'; import 'map_controls/map_actions_panel.dart';
import 'map_controls/map_style_selector.dart'; import 'map_controls/map_style_selector.dart';
import 'map_controls/placement_banner.dart'; import 'map_controls/placement_banner.dart';
import 'map_controls/radius_slider_bar.dart'; import 'map_controls/radius_slider_bar.dart';
import 'map_controls/reveal_progress_bar.dart';
import 'map_info_cards/frequent_place_info_card.dart'; import 'map_info_cards/frequent_place_info_card.dart';
import 'map_info_cards/geofence_info_card.dart'; import 'map_info_cards/geofence_info_card.dart';
import 'map_info_cards/history_position_info_card.dart'; import 'map_info_cards/history_position_info_card.dart';
@@ -77,14 +81,17 @@ class _LocationMapState extends ConsumerState<LocationMap>
LocationMapController get _vm => LocationMapController get _vm =>
ref.read(locationMapControllerProvider.notifier); ref.read(locationMapControllerProvider.notifier);
Color get _primaryColor => Color get _primaryColor => context.sfColors.legacyPrimary;
context.sfColors.legacyPrimary;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_mapController = MapController(); _mapController = MapController();
_startMonitoring(); _startMonitoring();
_vm.onPlaybackTick = () {
final state = ref.read(locationMapControllerProvider);
_navigateToHistoryPosition(state.historyNavigationIndex);
};
} }
static const _minFrequency = 60; static const _minFrequency = 60;
@@ -94,7 +101,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
final raw = widget.selectedDevice?.settings.frequency ?? 60; final raw = widget.selectedDevice?.settings.frequency ?? 60;
final frequency = raw < _minFrequency ? _minFrequency : raw; final frequency = raw < _minFrequency ? _minFrequency : raw;
_followTimer = Timer.periodic(Duration(seconds: frequency), (_) { _followTimer = Timer.periodic(Duration(seconds: frequency), (_) {
if (ref.read(selectedDeviceProvider).value?.isDisconnected ?? true) return; if (ref.read(selectedDeviceProvider).value?.isDisconnected ?? true) {
return;
}
widget.onRefreshPosition(); widget.onRefreshPosition();
}); });
} }
@@ -102,6 +111,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
@override @override
void dispose() { void dispose() {
_followTimer?.cancel(); _followTimer?.cancel();
_vm.onPlaybackTick = null;
_vm.stopPlaybackTimer();
_vm.cancelRecenter();
_moveAnimation?.dispose(); _moveAnimation?.dispose();
_mapController.dispose(); _mapController.dispose();
super.dispose(); super.dispose();
@@ -131,12 +143,80 @@ class _LocationMapState extends ConsumerState<LocationMap>
(oldWidget.positionHistory.length != widget.positionHistory.length || (oldWidget.positionHistory.length != widget.positionHistory.length ||
!oldWidget.showRouteTrail)) { !oldWidget.showRouteTrail)) {
_fitHistoryBounds(); _fitHistoryBounds();
if (oldWidget.positionHistory.length != widget.positionHistory.length) {
Future(() {
_vm.startHistoryNavigation();
_startRevealAnimation();
});
}
} }
} }
void _animatedMove(LatLng dest, double zoom) { void _animatedMove(
LatLng dest,
double? zoom, {
Duration duration = const Duration(milliseconds: 400),
Curve curve = Curves.easeInOut,
}) {
_animatedMoveAsync(dest, zoom, duration: duration, curve: curve);
}
Future<void> _startRevealAnimation() async {
final positions = widget.positionHistory
.where((p) => p.latitude != 0 || p.longitude != 0)
.toList();
if (positions.length <= 1) return;
_vm.startReveal();
await _animatedMoveAsync(
LatLng(positions.first.latitude, positions.first.longitude),
18.0,
duration: const Duration(milliseconds: 1000),
curve: Curves.easeOutCubic,
);
for (var i = 1; i < positions.length; i++) {
if (!mounted) return;
final state = ref.read(locationMapControllerProvider);
if (!state.isRevealAnimating) return;
_vm.updateRevealCount(i + 1);
await _animatedMoveAsync(
LatLng(positions[i].latitude, positions[i].longitude),
null,
duration: const Duration(milliseconds: 800),
curve: Curves.easeInOutCubic,
);
if (!mounted) return;
if (!ref.read(locationMapControllerProvider).isRevealAnimating) return;
await Future.delayed(const Duration(milliseconds: 200));
}
if (!mounted) return;
_vm.finishReveal();
await Future.delayed(const Duration(milliseconds: 500));
if (!mounted) return;
_fitHistoryBounds();
}
void _skipRevealAnimation() {
_moveAnimation?.dispose();
_moveAnimation = null;
_vm.finishReveal();
Future(_vm.startHistoryNavigation);
_fitHistoryBounds();
}
Future<void> _animatedMoveAsync(
LatLng dest,
double? zoom, {
Duration duration = const Duration(milliseconds: 400),
Curve curve = Curves.easeInOut,
}) {
final completer = Completer<void>();
_moveAnimation?.dispose(); _moveAnimation?.dispose();
final camera = _mapController.camera; final camera = _mapController.camera;
final targetZoom = zoom ?? camera.zoom;
final latTween = Tween<double>( final latTween = Tween<double>(
begin: camera.center.latitude, begin: camera.center.latitude,
end: dest.latitude, end: dest.latitude,
@@ -145,18 +225,12 @@ class _LocationMapState extends ConsumerState<LocationMap>
begin: camera.center.longitude, begin: camera.center.longitude,
end: dest.longitude, end: dest.longitude,
); );
final zoomTween = Tween<double>(begin: camera.zoom, end: zoom); final zoomTween = Tween<double>(begin: camera.zoom, end: targetZoom);
final controller = AnimationController( final controller = AnimationController(duration: duration, vsync: this);
duration: const Duration(milliseconds: 400),
vsync: this,
);
_moveAnimation = controller; _moveAnimation = controller;
final animation = CurvedAnimation( final animation = CurvedAnimation(parent: controller, curve: curve);
parent: controller,
curve: Curves.easeInOut,
);
controller.addListener(() { controller.addListener(() {
_mapController.move( _mapController.move(
@@ -170,10 +244,53 @@ class _LocationMapState extends ConsumerState<LocationMap>
status == AnimationStatus.dismissed) { status == AnimationStatus.dismissed) {
controller.dispose(); controller.dispose();
if (_moveAnimation == controller) _moveAnimation = null; if (_moveAnimation == controller) _moveAnimation = null;
if (!completer.isCompleted) completer.complete();
} }
}); });
controller.forward(); controller.forward();
return completer.future;
}
bool _isHistoryNavigating(LocationMapState mapState) =>
widget.showRouteTrail &&
widget.positionHistory.isNotEmpty &&
mapState.historyNavigationIndex >= 0 &&
mapState.historyNavigationIndex < widget.positionHistory.length;
void _navigateToHistoryPosition(int index) {
if (index < 0 || index >= widget.positionHistory.length) return;
final position = widget.positionHistory[index];
if (position.latitude == 0 && position.longitude == 0) return;
_animatedMove(LatLng(position.latitude, position.longitude), null);
}
Future<void> _handleToggleFollow(LocationMapState mapState) async {
final willActivate = !mapState.isFollowing;
if (willActivate && !await guardDeviceCommand(context, ref)) return;
if (!mounted) return;
_vm.toggleFollowing();
unawaited(
ref
.read(sfTrackingProvider)
.legacyLocationMapFollowToggled(willActivate),
);
if (willActivate && widget.selectedPosition != null) {
_centerOnDevice();
}
}
void _togglePlayback() {
_vm.toggleHistoryPlayback(widget.positionHistory.length);
final state = ref.read(locationMapControllerProvider);
if (state.historyPlaying) {
_navigateToHistoryPosition(state.historyNavigationIndex);
}
}
void _scheduleRecenter() {
if (widget.selectedPosition == null) return;
_vm.scheduleRecenter(_centerOnDevice);
} }
void _centerOnDevice() { void _centerOnDevice() {
@@ -245,6 +362,46 @@ class _LocationMapState extends ConsumerState<LocationMap>
); );
} }
Future<void> _showGpsFunctionsDialog(
BuildContext context,
WidgetRef ref,
LocationMapState mapState,
) async {
final result = await showGpsFunctionsDialog(
context,
isFollowing: mapState.isFollowing,
showGeofences: mapState.showGeofences,
showFrequentPlaces: mapState.showFrequentPlaces,
showRouteTrail: widget.showRouteTrail,
hasPositionHistory: widget.positionHistory.isNotEmpty,
hasPosition: widget.selectedPosition != null,
);
if (result == null || !mounted) return;
switch (result) {
case GpsFunction.list:
_showListSheet();
case GpsFunction.addGeofence:
_vm.startPlacing(PlacingMode.geofence);
case GpsFunction.addFrequentPlace:
_vm.startPlacing(PlacingMode.frequentPlace);
case GpsFunction.share:
_shareLocation();
case GpsFunction.centerOnDevice:
_centerOnDevice();
case GpsFunction.toggleFollow:
_handleToggleFollow(mapState);
case GpsFunction.toggleGeofences:
_vm.toggleGeofences();
case GpsFunction.toggleFrequentPlaces:
_vm.toggleFrequentPlaces();
case GpsFunction.positionHistory:
_handleHistoryTap();
case GpsFunction.toggleRouteTrail:
ref.read(locationControllerProvider.notifier).toggleRouteTrail();
}
}
Future<void> _updateFrequency(int frequency) async { Future<void> _updateFrequency(int frequency) async {
if (!await guardDeviceCommand(context, ref)) return; if (!await guardDeviceCommand(context, ref)) return;
final success = await ref final success = await ref
@@ -253,7 +410,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
if (!mounted) return; if (!mounted) return;
if (success) { if (success) {
_followTimer?.cancel(); _followTimer?.cancel();
final safeFrequency = frequency < _minFrequency ? _minFrequency : frequency; final safeFrequency = frequency < _minFrequency
? _minFrequency
: frequency;
_followTimer = Timer.periodic(Duration(seconds: safeFrequency), (_) { _followTimer = Timer.periodic(Duration(seconds: safeFrequency), (_) {
widget.onRefreshPosition(); widget.onRefreshPosition();
}); });
@@ -274,19 +433,41 @@ class _LocationMapState extends ConsumerState<LocationMap>
if (mapState.placingMode == PlacingMode.geofence) { if (mapState.placingMode == PlacingMode.geofence) {
_vm.confirmGeofencePlacement(center); _vm.confirmGeofencePlacement(center);
} else { } else {
final editing = mapState.editingFrequentPlace;
_vm.confirmFrequentPlacePlacement(); _vm.confirmFrequentPlacePlacement();
showNameInputSheet( showNameInputSheet(
context, context,
ref: ref, ref: ref,
title: context.translate(I18n.locationNewFrequentPlace), title: context.translate(
editing != null
? I18n.locationEditFrequentPlace
: I18n.locationNewFrequentPlace,
),
hintText: context.translate(I18n.locationHintFrequentPlace), hintText: context.translate(I18n.locationHintFrequentPlace),
onSubmit: (name, _) => ref initialName: editing?.name,
.read(locationControllerProvider.notifier) submitLabel: editing != null
.createFrequentPlace( ? context.translate(I18n.locationSave)
name: name, : null,
lat: center.latitude, onSubmit: (name, _) {
lng: center.longitude, if (editing != null) {
), return ref
.read(locationControllerProvider.notifier)
.updateFrequentPlace(
id: editing.id,
name: name,
lat: center.latitude,
lng: center.longitude,
wifiList: editing.wifiList,
);
}
return ref
.read(locationControllerProvider.notifier)
.createFrequentPlace(
name: name,
lat: center.latitude,
lng: center.longitude,
);
},
); );
} }
} }
@@ -356,15 +537,57 @@ class _LocationMapState extends ConsumerState<LocationMap>
positionHistory: locationState.showRouteTrail positionHistory: locationState.showRouteTrail
? locationState.positionHistory ? locationState.positionHistory
: [], : [],
hasHiddenGeofences:
!mapState.showGeofences && locationState.geofences.isNotEmpty,
hasHiddenFrequentPlaces:
!mapState.showFrequentPlaces &&
locationState.frequentPlaces.isNotEmpty,
hasHiddenHistory:
!locationState.showRouteTrail &&
locationState.positionHistory.isNotEmpty,
primaryColor: _primaryColor, primaryColor: _primaryColor,
onGeofenceTap: (g) { onGeofenceTap: (g) {
Navigator.pop(context); Navigator.pop(context);
_animatedMove(LatLng(g.latitude, g.longitude), _defaultZoom); _animatedMove(LatLng(g.latitude, g.longitude), _defaultZoom);
}, },
onGeofenceEdit: (g) {
Navigator.pop(context);
_vm.selectGeofence(g);
},
onGeofenceDelete: (g) async {
Navigator.pop(context);
final confirmed = await _confirmDelete(
context,
I18n.locationDeleteGeofenceConfirm,
);
if (!confirmed || !mounted) return;
if (!await guardDeviceCommand(context, ref)) return;
if (!mounted) return;
ref
.read(locationControllerProvider.notifier)
.deleteGeofence(id: g.id);
},
onFrequentPlaceTap: (fp) { onFrequentPlaceTap: (fp) {
Navigator.pop(context); Navigator.pop(context);
_animatedMove(LatLng(fp.lat, fp.lng), _defaultZoom); _animatedMove(LatLng(fp.lat, fp.lng), _defaultZoom);
}, },
onFrequentPlaceEdit: (fp) {
Navigator.pop(context);
_onEditFrequentPlace(fp);
},
onFrequentPlaceDelete: (fp) async {
Navigator.pop(context);
final confirmed = await _confirmDelete(
context,
I18n.locationDeleteFrequentPlaceConfirm,
);
if (!confirmed || !mounted) return;
if (!await guardDeviceCommand(context, ref)) return;
if (!mounted) return;
ref
.read(locationControllerProvider.notifier)
.deleteFrequentPlace(id: fp.id);
},
onHistoryTap: (p) { onHistoryTap: (p) {
Navigator.pop(context); Navigator.pop(context);
if (p.latitude != 0 || p.longitude != 0) { if (p.latitude != 0 || p.longitude != 0) {
@@ -375,12 +598,34 @@ class _LocationMapState extends ConsumerState<LocationMap>
); );
} }
Future<bool> _confirmDelete(BuildContext context, String messageKey) async {
final result = await showLegacyDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.translate(I18n.delete)),
content: Text(context.translate(messageKey)),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text(context.translate(I18n.cancel)),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
),
child: Text(context.translate(I18n.delete)),
),
],
),
);
return result == true;
}
void _handleHistoryTap() { void _handleHistoryTap() {
if (widget.positionHistory.isEmpty) { _vm.stopPlaybackTimer();
_openDateRangePicker(); _vm.stopHistoryNavigation();
} else { _openDateRangePicker();
ref.read(locationControllerProvider.notifier).toggleRouteTrail();
}
} }
Future<void> _openDateRangePicker() async { Future<void> _openDateRangePicker() async {
@@ -394,12 +639,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
start: now.subtract(const Duration(days: 1)), start: now.subtract(const Duration(days: 1)),
end: now, end: now,
), ),
builder: (context, child) => Theme( builder: legacyPickerThemeBuilder,
data: Theme.of(
context,
).copyWith(colorScheme: ColorScheme.light(primary: _primaryColor)),
child: child!,
),
); );
if (picked != null) { if (picked != null) {
@@ -411,33 +651,31 @@ class _LocationMapState extends ConsumerState<LocationMap>
59, 59,
59, 59,
); );
ref if (!mounted) return;
showInfoDialog(context, I18n.locationHistoryLoading);
await ref
.read(locationControllerProvider.notifier) .read(locationControllerProvider.notifier)
.loadPositionHistory(from: picked.start, to: to); .loadPositionHistory(from: picked.start, to: to);
if (!mounted) return;
final history =
ref.read(locationControllerProvider).value?.positionHistory ?? [];
if (history.isEmpty) {
await showErrorDialog(context, I18n.locationHistoryEmpty);
} else {
await showSuccessDialog(
context,
I18n.locationHistoryLoaded,
args: {'count': history.length},
);
}
} }
} }
Future<void> _onEditFrequentPlace(FrequentPlaceEntity fp) async { Future<void> _onEditFrequentPlace(FrequentPlaceEntity fp) async {
if (!await guardDeviceCommand(context, ref)) return; if (!await guardDeviceCommand(context, ref)) return;
if (!mounted) return; if (!mounted) return;
_vm.clearSelectedFrequentPlace(); _vm.startEditingFrequentPlace(fp);
showNameInputSheet( _animatedMove(LatLng(fp.lat, fp.lng), null);
context,
ref: ref,
title: context.translate(I18n.locationEditFrequentPlace),
hintText: context.translate(I18n.locationHintFrequentPlace),
initialName: fp.name,
submitLabel: context.translate(I18n.locationSave),
onSubmit: (name, _) => ref
.read(locationControllerProvider.notifier)
.updateFrequentPlace(
id: fp.id,
name: name,
lat: fp.lat,
lng: fp.lng,
wifiList: fp.wifiList,
),
);
} }
List<Widget> _buildMapLayers(LocationMapState mapState) { List<Widget> _buildMapLayers(LocationMapState mapState) {
@@ -448,6 +686,8 @@ class _LocationMapState extends ConsumerState<LocationMap>
zoom: mapState.mapZoom, zoom: mapState.mapZoom,
onPositionTap: _vm.selectHistoryPosition, onPositionTap: _vm.selectHistoryPosition,
onClusterTap: _animatedMove, onClusterTap: _animatedMove,
activeIndex: mapState.historyNavigationIndex,
visibleCount: mapState.historyRevealCount,
) )
: null; : null;
@@ -488,17 +728,68 @@ class _LocationMapState extends ConsumerState<LocationMap>
], ],
), ),
MarkerLayer(markers: _buildMarkers(mapState, historyLayer)), MarkerLayer(markers: _buildMarkers(mapState, historyLayer)),
if (widget.selectedDevice != null) if (mapState.isRevealAnimating) ...[
Align( Positioned(
alignment: Alignment.bottomCenter, bottom: SizeUtils.getByScreen(small: 16, big: 14),
child: DeviceBanner( left: 0,
device: widget.selectedDevice!, right: 0,
devices: widget.devices, child: RevealProgressBar(
positions: widget.positions, current: mapState.historyRevealCount ?? 0,
onDeviceChanged: widget.onDeviceChanged, total: widget.positionHistory.length,
onTap: _centerOnDevice, primaryColor: _primaryColor,
onSkip: _skipRevealAnimation,
), ),
), ),
] else if (_isHistoryNavigating(mapState)) ...[
Positioned(
bottom: SizeUtils.getByScreen(small: 16, big: 14),
left: 0,
right: 0,
child: HistoryPlayer(
currentIndex: mapState.historyNavigationIndex,
total: widget.positionHistory.length,
isPlaying: mapState.historyPlaying,
currentPosition:
widget.positionHistory[mapState.historyNavigationIndex],
onPrevious: () {
_vm.previousHistoryPosition();
final state = ref.read(locationMapControllerProvider);
_navigateToHistoryPosition(state.historyNavigationIndex);
},
onNext: () {
_vm.nextHistoryPosition(widget.positionHistory.length);
final state = ref.read(locationMapControllerProvider);
_navigateToHistoryPosition(state.historyNavigationIndex);
},
onPlayPause: _togglePlayback,
onClose: () {
_vm.stopPlaybackTimer();
_vm.stopHistoryNavigation();
},
),
),
] else ...[
if (widget.selectedPosition != null)
Positioned(
bottom: SizeUtils.getByScreen(small: 190, big: 186),
right: 16,
child: MapActionButton(
icon: Icons.my_location,
onTap: _centerOnDevice,
),
),
if (widget.selectedDevice != null)
Align(
alignment: Alignment.bottomCenter,
child: DeviceBanner(
device: widget.selectedDevice!,
devices: widget.devices,
positions: widget.positions,
onDeviceChanged: widget.onDeviceChanged,
onTap: _centerOnDevice,
),
),
],
]; ];
} }
@@ -538,7 +829,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
width: 40, width: 40,
height: 40, height: 40,
child: GestureDetector( child: GestureDetector(
onLongPress: () => _vm.selectFrequentPlace(fp), onTap: () => _vm.selectFrequentPlace(fp),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.3), color: Colors.orange.withValues(alpha: 0.3),
@@ -567,7 +858,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
width: 36, width: 36,
height: 36, height: 36,
child: GestureDetector( child: GestureDetector(
onLongPress: () => _vm.selectGeofence(g), onTap: () => _vm.selectGeofence(g),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.3), color: Colors.blue.withValues(alpha: 0.3),
@@ -643,7 +934,12 @@ class _LocationMapState extends ConsumerState<LocationMap>
const SizedBox(height: 8), const SizedBox(height: 8),
FrequencySelector( FrequencySelector(
currentFrequency: currentFrequency:
ref.watch(selectedDeviceProvider).value?.settings.frequency ?? 60, ref
.watch(selectedDeviceProvider)
.value
?.settings
.frequency ??
60,
options: widget.selectedDevice!.capabilities!.location!.options options: widget.selectedDevice!.capabilities!.location!.options
.where((o) => o > 0 || o == -1) .where((o) => o > 0 || o == -1)
.toList(), .toList(),
@@ -656,31 +952,8 @@ class _LocationMapState extends ConsumerState<LocationMap>
Positioned( Positioned(
top: 12, top: 12,
right: 12, right: 12,
child: LayerToggles( child: GpsControlPanel(
showGeofences: mapState.showGeofences, onRefresh: () async {
showFrequentPlaces: mapState.showFrequentPlaces,
showRouteTrail: widget.showRouteTrail,
hasPositionHistory: widget.positionHistory.isNotEmpty,
isLoadingHistory: widget.isLoadingHistory,
onGeofencesToggled: _vm.toggleGeofences,
onFrequentPlacesToggled: _vm.toggleFrequentPlaces,
onHistoryTapped: _handleHistoryTap,
onHistoryLongPressed: _openDateRangePicker,
),
),
Positioned(
bottom: SizeUtils.getByScreen(small: 120, big: 110),
right: 12,
child: MapActionsPanel(
actionsExpanded: mapState.actionsExpanded,
hasPosition: widget.selectedPosition != null,
isFollowing: mapState.isFollowing,
onToggleExpanded: _vm.toggleActionsExpanded,
onListTap: _showListSheet,
onAddGeofence: () => _vm.startPlacing(PlacingMode.geofence),
onAddFrequentPlace: () => _vm.startPlacing(PlacingMode.frequentPlace),
onShareTap: _shareLocation,
onRefreshTap: () async {
if (!await guardDeviceCommand(context, ref)) return; if (!await guardDeviceCommand(context, ref)) return;
unawaited( unawaited(
ref.read(sfTrackingProvider).legacyLocationMapRefreshTapped(), ref.read(sfTrackingProvider).legacyLocationMapRefreshTapped(),
@@ -694,27 +967,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
await showErrorDialog(context, I18n.errorPositions); await showErrorDialog(context, I18n.errorPositions);
} }
}, },
onCenterTap: _centerOnDevice, onSettings: () => _showGpsFunctionsDialog(context, ref, mapState),
onToggleFollow: () async {
final willActivate = !mapState.isFollowing;
if (willActivate && !await guardDeviceCommand(context, ref)) return;
_vm.toggleFollowing();
unawaited(
ref
.read(sfTrackingProvider)
.legacyLocationMapFollowToggled(willActivate),
);
if (willActivate && widget.selectedPosition != null) {
_centerOnDevice();
}
if (!mounted) return;
await showSuccessDialog(
context,
willActivate
? I18n.locationMapFollowEnabled
: I18n.locationMapFollowDisabled,
);
},
), ),
), ),
]; ];
@@ -791,10 +1044,13 @@ class _LocationMapState extends ConsumerState<LocationMap>
initialZoom: initialZoom, initialZoom: initialZoom,
minZoom: 5, minZoom: 5,
keepAlive: true, keepAlive: true,
onPositionChanged: (camera, _) { onPositionChanged: (camera, hasGesture) {
if (widget.showRouteTrail && widget.positionHistory.length > 2) { if (widget.showRouteTrail && widget.positionHistory.length > 2) {
_vm.updateMapZoom(camera.zoom); _vm.updateMapZoom(camera.zoom);
} }
if (hasGesture) {
_scheduleRecenter();
}
}, },
), ),
children: _buildMapLayers(mapState), children: _buildMapLayers(mapState),

View File

@@ -0,0 +1,77 @@
import 'package:legacy_theme/legacy_theme.dart';
import 'package:flutter/material.dart';
class GpsControlPanel extends StatelessWidget {
final VoidCallback onRefresh;
final VoidCallback onSettings;
const GpsControlPanel({
super.key,
required this.onRefresh,
required this.onSettings,
});
@override
Widget build(BuildContext context) {
final primaryColor = context.sfColors.legacyPrimary;
return Container(
decoration: BoxDecoration(
color: primaryColor,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
padding: const EdgeInsets.all(6),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_PanelButton(
icon: Icons.refresh,
onTap: onRefresh,
),
const SizedBox(height: 4),
_PanelButton(
icon: Icons.settings,
onTap: onSettings,
),
],
),
);
}
}
class _PanelButton extends StatelessWidget {
final IconData icon;
final VoidCallback onTap;
const _PanelButton({
required this.icon,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
color: Colors.white,
size: 24,
),
),
);
}
}

View File

@@ -0,0 +1,179 @@
import 'package:legacy_theme/legacy_theme.dart';
import 'package:legacy_ui/legacy_ui.dart';
import 'package:flutter/material.dart';
import 'package:sf_localizations/sf_localizations.dart';
enum GpsFunction {
list,
addGeofence,
addFrequentPlace,
share,
centerOnDevice,
toggleFollow,
toggleGeofences,
toggleFrequentPlaces,
positionHistory,
toggleRouteTrail,
}
Future<GpsFunction?> showGpsFunctionsDialog(
BuildContext context, {
required bool isFollowing,
required bool showGeofences,
required bool showFrequentPlaces,
required bool showRouteTrail,
required bool hasPositionHistory,
required bool hasPosition,
}) {
return showLegacyDialog<GpsFunction>(
context: context,
builder: (dialogContext) => _GpsFunctionsDialog(
isFollowing: isFollowing,
showGeofences: showGeofences,
showFrequentPlaces: showFrequentPlaces,
showRouteTrail: showRouteTrail,
hasPositionHistory: hasPositionHistory,
hasPosition: hasPosition,
),
);
}
class _GpsFunctionsDialog extends StatelessWidget {
final bool isFollowing;
final bool showGeofences;
final bool showFrequentPlaces;
final bool showRouteTrail;
final bool hasPositionHistory;
final bool hasPosition;
const _GpsFunctionsDialog({
required this.isFollowing,
required this.showGeofences,
required this.showFrequentPlaces,
required this.showRouteTrail,
required this.hasPositionHistory,
required this.hasPosition,
});
@override
Widget build(BuildContext context) {
final primaryColor = context.sfColors.legacyPrimary;
return AlertDialog(
title: Center(
child: Text(
context.translate(I18n.gpsFunctionsTitle),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: primaryColor,
),
),
),
contentPadding: const EdgeInsets.only(top: 12, bottom: 8),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_FunctionTile(
icon: Icons.list_alt,
label: context.translate(I18n.gpsFunctionList),
onTap: () => Navigator.pop(context, GpsFunction.list),
),
_FunctionTile(
icon: Icons.add_location_alt,
label: context.translate(I18n.gpsFunctionAddGeofence),
onTap: () => Navigator.pop(context, GpsFunction.addGeofence),
visibilityIcon: showGeofences,
onVisibilityTap: () =>
Navigator.pop(context, GpsFunction.toggleGeofences),
),
_FunctionTile(
icon: Icons.add_home,
label: context.translate(I18n.gpsFunctionAddFrequentPlace),
onTap: () =>
Navigator.pop(context, GpsFunction.addFrequentPlace),
visibilityIcon: showFrequentPlaces,
onVisibilityTap: () =>
Navigator.pop(context, GpsFunction.toggleFrequentPlaces),
),
if (hasPosition)
_FunctionTile(
icon: Icons.share,
label: context.translate(I18n.gpsFunctionShare),
onTap: () => Navigator.pop(context, GpsFunction.share),
),
if (hasPosition)
_FunctionTile(
icon: isFollowing ? Icons.gps_fixed : Icons.gps_not_fixed,
label: context.translate(
isFollowing
? I18n.gpsFunctionStopFollow
: I18n.gpsFunctionFollow,
),
onTap: () => Navigator.pop(context, GpsFunction.toggleFollow),
),
_FunctionTile(
icon: Icons.route,
label: context.translate(I18n.locationLayerHistory),
onTap: () =>
Navigator.pop(context, GpsFunction.positionHistory),
visibilityIcon:
hasPositionHistory ? showRouteTrail : null,
onVisibilityTap: hasPositionHistory
? () =>
Navigator.pop(context, GpsFunction.toggleRouteTrail)
: null,
),
],
),
),
);
}
}
class _FunctionTile extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
final bool? visibilityIcon;
final VoidCallback? onVisibilityTap;
const _FunctionTile({
required this.icon,
required this.label,
required this.onTap,
this.visibilityIcon,
this.onVisibilityTap,
});
@override
Widget build(BuildContext context) {
final primaryColor = context.sfColors.legacyPrimary;
return ListTile(
dense: true,
leading: Icon(icon, color: primaryColor, size: 22),
title: Text(
label,
style: const TextStyle(fontSize: 14),
),
trailing: visibilityIcon != null && onVisibilityTap != null
? GestureDetector(
onTap: onVisibilityTap,
child: Padding(
padding: const EdgeInsets.all(4),
child: Icon(
visibilityIcon! ? Icons.visibility : Icons.visibility_off,
size: 22,
color: visibilityIcon!
? primaryColor
: Theme.of(context).colorScheme.outline,
),
),
)
: const Icon(Icons.chevron_right, size: 20),
onTap: onTap,
);
}
}

View File

@@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:legacy_device_state/legacy_device_state.dart';
import 'package:legacy_theme/legacy_theme.dart';
const _playbackIntervalSeconds = 3;
class HistoryPlayer extends StatelessWidget {
final int currentIndex;
final int total;
final bool isPlaying;
final PositionEntity currentPosition;
final VoidCallback onPrevious;
final VoidCallback onNext;
final VoidCallback onPlayPause;
final VoidCallback onClose;
const HistoryPlayer({
super.key,
required this.currentIndex,
required this.total,
required this.isPlaying,
required this.currentPosition,
required this.onPrevious,
required this.onNext,
required this.onPlayPause,
required this.onClose,
});
@override
Widget build(BuildContext context) {
final primaryColor = context.sfColors.legacyPrimary;
final date = DateTime.fromMillisecondsSinceEpoch(
currentPosition.positionDate,
);
final dateText = DateFormat('dd/MM/yyyy HH:mm').format(date);
final addressParts = [
currentPosition.address?.street,
currentPosition.address?.city,
].where((s) => s != null && s.isNotEmpty && s != 'Unknown').join(', ');
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Icon(Icons.location_on, color: primaryColor, size: 18),
const SizedBox(width: 6),
Expanded(
child: Text(
addressParts.isNotEmpty
? addressParts
: currentPosition.type,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
GestureDetector(
onTap: onClose,
child: Padding(
padding: const EdgeInsets.all(4),
child: Icon(
Icons.close,
size: 20,
color: Theme.of(context).colorScheme.outline,
),
),
),
],
),
const SizedBox(height: 2),
Row(
children: [
const SizedBox(width: 24),
Text(
'$dateText · ${currentPosition.type}',
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: currentIndex > 0 ? onPrevious : null,
icon: const Icon(Icons.skip_previous),
color: primaryColor,
iconSize: 28,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
),
const SizedBox(width: 8),
GestureDetector(
onTap: onPlayPause,
child: Stack(
alignment: Alignment.center,
children: [
if (isPlaying)
_CountdownRing(
key: ValueKey(currentIndex),
primaryColor: primaryColor,
),
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: primaryColor,
shape: BoxShape.circle,
),
child: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 26,
),
),
],
),
),
const SizedBox(width: 8),
IconButton(
onPressed: currentIndex < total - 1 ? onNext : null,
icon: const Icon(Icons.skip_next),
color: primaryColor,
iconSize: 28,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
),
],
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: total > 1 ? currentIndex / (total - 1) : 0,
backgroundColor:
Theme.of(context).colorScheme.outline.withValues(alpha: 0.2),
valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
borderRadius: BorderRadius.circular(2),
minHeight: 3,
),
const SizedBox(height: 4),
Text(
'${currentIndex + 1} / $total',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
);
}
}
class _CountdownRing extends StatelessWidget {
final Color primaryColor;
const _CountdownRing({super.key, required this.primaryColor});
@override
Widget build(BuildContext context) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 1.0, end: 0.0),
duration: const Duration(seconds: _playbackIntervalSeconds),
builder: (context, value, _) => SizedBox(
width: 48,
height: 48,
child: CircularProgressIndicator(
value: value,
strokeWidth: 2.5,
valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
backgroundColor: primaryColor.withValues(alpha: 0.2),
),
),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:location/src/features/location/presentation/providers/location_map_controller.dart'; import 'package:location/src/features/location/presentation/providers/location_map_controller.dart';
import 'package:sf_localizations/sf_localizations.dart'; import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart'; import 'package:utils/utils.dart';
@@ -120,6 +121,7 @@ class FrequencySelector extends ConsumerWidget {
} }
Widget _buildSegmented(BuildContext context, LocationMapController vm) { Widget _buildSegmented(BuildContext context, LocationMapController vm) {
final primaryColor = context.sfColors.legacyPrimary;
return Material( return Material(
elevation: 2, elevation: 2,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
@@ -142,9 +144,7 @@ class FrequencySelector extends ConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 6), padding: const EdgeInsets.symmetric(horizontal: 6),
margin: const EdgeInsets.symmetric(vertical: 2), margin: const EdgeInsets.symmetric(vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected color: selected ? primaryColor : Colors.transparent,
? Theme.of(context).primaryColor
: Colors.transparent,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: Center( child: Center(
@@ -156,7 +156,7 @@ class FrequencySelector extends ConsumerWidget {
fontSize: opt == -1 ? 10 : 12, fontSize: opt == -1 ? 10 : 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: selected color: selected
? Theme.of(context).colorScheme.surface ? Colors.white
: Theme.of(context).colorScheme.onSurface, : Theme.of(context).colorScheme.onSurface,
), ),
), ),
@@ -167,10 +167,10 @@ class FrequencySelector extends ConsumerWidget {
const SizedBox(height: 2), const SizedBox(height: 2),
GestureDetector( GestureDetector(
onTap: vm.collapseFrequency, onTap: vm.collapseFrequency,
child: const SizedBox( child: SizedBox(
width: 40, width: 40,
height: 24, height: 24,
child: Icon(Icons.close, size: 16, color: Colors.black54), child: Icon(Icons.close, size: 16, color: primaryColor),
), ),
), ),
], ],

View File

@@ -34,10 +34,10 @@ class MapStyleSelector extends ConsumerWidget {
onTap: () => onTap: () =>
ref.read(mapStyleSelectorExpandedProvider.notifier).set(true), ref.read(mapStyleSelectorExpandedProvider.notifier).set(true),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: const SizedBox( child: SizedBox(
width: 40, width: 40,
height: 40, height: 40,
child: Icon(Icons.layers, size: 22, color: Colors.grey), child: Icon(Icons.layers, size: 22, color: primaryColor),
), ),
), ),
); );

View File

@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:sf_localizations/sf_localizations.dart';
class RevealProgressBar extends StatelessWidget {
final int current;
final int total;
final Color primaryColor;
final VoidCallback onSkip;
const RevealProgressBar({
super.key,
required this.current,
required this.total,
required this.primaryColor,
required this.onSkip,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.12),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: Row(
children: [
Icon(Icons.route, color: primaryColor, size: 20),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.translate(I18n.locationRevealProgress),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: total > 1 ? current / total : 0,
backgroundColor: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.2),
valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
borderRadius: BorderRadius.circular(2),
minHeight: 3,
),
const SizedBox(height: 2),
Text(
'$current / $total',
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(width: 10),
GestureDetector(
onTap: onSkip,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
context.translate(I18n.locationRevealSkip),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: primaryColor,
),
),
),
),
],
),
);
}
}

View File

@@ -21,6 +21,8 @@ class RouteHistoryLayer extends StatelessWidget {
final double zoom; final double zoom;
final ValueChanged<PositionEntity> onPositionTap; final ValueChanged<PositionEntity> onPositionTap;
final void Function(LatLng point, double zoom) onClusterTap; final void Function(LatLng point, double zoom) onClusterTap;
final int activeIndex;
final int? visibleCount;
const RouteHistoryLayer({ const RouteHistoryLayer({
super.key, super.key,
@@ -28,19 +30,56 @@ class RouteHistoryLayer extends StatelessWidget {
required this.zoom, required this.zoom,
required this.onPositionTap, required this.onPositionTap,
required this.onClusterTap, required this.onClusterTap,
this.activeIndex = -1,
this.visibleCount,
}); });
List<PositionEntity> get _visiblePositions {
if (visibleCount == null) return positionHistory;
return positionHistory.take(visibleCount!).toList();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MarkerLayer( return MarkerLayer(
markers: [..._buildDirectionArrows(), ..._buildHistoryMarkers()], markers: [
..._buildDirectionArrows(),
..._buildHistoryMarkers(),
if (activeIndex >= 0 && activeIndex < positionHistory.length)
_buildActiveMarker(positionHistory[activeIndex]),
],
);
}
Marker _buildActiveMarker(PositionEntity position) {
return Marker(
point: LatLng(position.latitude, position.longitude),
width: 40,
height: 40,
child: Container(
decoration: BoxDecoration(
color: routeArrowColor,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 3),
boxShadow: [
BoxShadow(
color: routeArrowColor.withValues(alpha: 0.5),
blurRadius: 8,
spreadRadius: 2,
),
],
),
child: const Icon(Icons.navigation, color: Colors.white, size: 20),
),
rotate: true,
); );
} }
List<Polyline> buildRouteSegments() { List<Polyline> buildRouteSegments() {
if (positionHistory.length < 2) return []; final visible = _visiblePositions;
if (visible.length < 2) return [];
final points = positionHistory final points = visible
.where(_hasValidCoords) .where(_hasValidCoords)
.map((p) => LatLng(p.latitude, p.longitude)) .map((p) => LatLng(p.latitude, p.longitude))
.toList(); .toList();
@@ -48,17 +87,18 @@ class RouteHistoryLayer extends StatelessWidget {
if (points.length < 2) return []; if (points.length < 2) return [];
return [ return [
Polyline(points: points, color: routeColor, strokeWidth: 4.0), Polyline(points: points, color: routeColor, strokeWidth: 5.0),
]; ];
} }
List<Marker> _buildDirectionArrows() { List<Marker> _buildDirectionArrows() {
if (positionHistory.length < 2) return []; final visible = _visiblePositions;
if (visible.length < 2) return [];
final arrows = <Marker>[]; final arrows = <Marker>[];
for (int i = 0; i < positionHistory.length - 1; i++) { for (int i = 0; i < visible.length - 1; i++) {
final from = positionHistory[i]; final from = visible[i];
final to = positionHistory[i + 1]; final to = visible[i + 1];
if (!_hasValidCoords(from) || !_hasValidCoords(to)) continue; if (!_hasValidCoords(from) || !_hasValidCoords(to)) continue;
final midLat = (from.latitude + to.latitude) / 2; final midLat = (from.latitude + to.latitude) / 2;
@@ -89,14 +129,15 @@ class RouteHistoryLayer extends StatelessWidget {
} }
List<Marker> _buildHistoryMarkers() { List<Marker> _buildHistoryMarkers() {
if (positionHistory.isEmpty) return []; final visible = _visiblePositions;
if (visible.isEmpty) return [];
final markers = <Marker>[]; final markers = <Marker>[];
final threshold = final threshold =
_clusterBaseDegrees / math.pow(2, zoom - _clusterBaseZoom); _clusterBaseDegrees / math.pow(2, zoom - _clusterBaseZoom);
final first = positionHistory.first; final first = visible.first;
final last = positionHistory.last; final last = visible.last;
markers.add( markers.add(
_buildEndpointMarker( _buildEndpointMarker(
@@ -105,7 +146,7 @@ class RouteHistoryLayer extends StatelessWidget {
icon: Icons.play_arrow, icon: Icons.play_arrow,
), ),
); );
if (positionHistory.length > 1) { if (visible.length > 1) {
markers.add( markers.add(
_buildEndpointMarker( _buildEndpointMarker(
position: last, position: last,
@@ -115,11 +156,11 @@ class RouteHistoryLayer extends StatelessWidget {
); );
} }
if (positionHistory.length <= 2) return markers; if (visible.length <= 2) return markers;
final intermediates = positionHistory.sublist( final intermediates = visible.sublist(
1, 1,
positionHistory.length - 1, visible.length - 1,
); );
final clustered = <List<int>>[]; final clustered = <List<int>>[];
final visited = List.filled(intermediates.length, false); final visited = List.filled(intermediates.length, false);
@@ -214,11 +255,11 @@ class RouteHistoryLayer extends StatelessWidget {
}) { }) {
return Marker( return Marker(
point: LatLng(position.latitude, position.longitude), point: LatLng(position.latitude, position.longitude),
width: 18, width: 28,
height: 18, height: 28,
child: GestureDetector( child: GestureDetector(
onTap: () => onPositionTap(position), onTap: () => onPositionTap(position),
child: _CircleMarkerIcon(color: color, size: 18), child: _CircleMarkerIcon(color: color, size: 28),
), ),
rotate: true, rotate: true,
); );