Compare commits

...

7 Commits

37 changed files with 1841 additions and 204 deletions

View File

@@ -73,6 +73,7 @@ class _ActivityMeterBody extends ConsumerWidget {
context: context,
firstDate: now.subtract(const Duration(days: 365)),
lastDate: now,
builder: legacyPickerThemeBuilder,
);
if (picked != null) {
notifier.selectCustomRange(picked.start, picked.end);

View File

@@ -2,6 +2,7 @@ import 'package:design_system/design_system.dart';
import 'package:device_management/src/core/domain/entities/app_usage_schedule_entity.dart';
import 'package:flutter/material.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:legacy_ui/legacy_ui.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
@@ -55,6 +56,7 @@ class _EditSchedulePeriodSheetState extends State<_EditSchedulePeriodSheet> {
final picked = await showTimePicker(
context: context,
initialTime: isStart ? _start : _end,
builder: legacyPickerThemeBuilder,
);
if (picked == null) return;
setState(() {

View File

@@ -92,6 +92,7 @@ class AppsUseScreen extends ConsumerWidget {
context: context,
firstDate: now.subtract(const Duration(days: 365)),
lastDate: now,
builder: legacyPickerThemeBuilder,
);
if (picked != null) {
notifier.selectCustomRange(picked.start, picked.end);

View File

@@ -215,7 +215,7 @@ class _FilterBar extends StatelessWidget {
? FontWeight.w600
: FontWeight.w500,
color: isSelected
? Colors.black87
? Theme.of(context).colorScheme.onSurface
: Theme.of(context)
.colorScheme
.onSurfaceVariant,
@@ -324,7 +324,7 @@ class _CallTile extends StatelessWidget {
fontSize: 15,
fontWeight: FontWeight.w500,
color: isAccepted
? Colors.black87
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.error,
),
maxLines: 1,

View File

@@ -75,7 +75,7 @@ class _CallWatchDialogState extends ConsumerState<CallWatchDialog> {
textAlign: TextAlign.center,
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
color: Colors.black54,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
SizedBox(height: SizeUtils.getByScreen(small: 12, big: 10)),

View File

@@ -2,6 +2,7 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:legacy_ui/legacy_ui.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
@@ -110,6 +111,7 @@ class _EditPeriodSheetState extends ConsumerState<EditPeriodSheet> {
final picked = await showTimePicker(
context: context,
initialTime: start,
builder: legacyPickerThemeBuilder,
);
if (picked != null) _start.value = picked;
},
@@ -128,6 +130,7 @@ class _EditPeriodSheetState extends ConsumerState<EditPeriodSheet> {
final picked = await showTimePicker(
context: context,
initialTime: end,
builder: legacyPickerThemeBuilder,
);
if (picked != null) _end.value = picked;
},

View File

@@ -48,6 +48,7 @@ class _HealthScreenState extends ConsumerState<HealthScreen>
context: context,
firstDate: now.subtract(const Duration(days: 365)),
lastDate: now,
builder: legacyPickerThemeBuilder,
);
if (picked != null) {
notifier.selectCustomRange(picked.start, picked.end);

View File

@@ -102,7 +102,7 @@ class _SpyCallDialogState extends ConsumerState<SpyCallDialog> {
textAlign: TextAlign.center,
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
color: Colors.black54,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
SizedBox(height: SizeUtils.getByScreen(small: 12, big: 10)),

View File

@@ -1,5 +1,6 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:legacy_ui/legacy_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/providers/device_setup_controller.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/providers/device_setup_form_controllers.dart';
@@ -21,6 +22,7 @@ Future<void> _pickBornAt(
initialDate: safeInitial,
firstDate: DateTime(1900, 1, 1),
lastDate: now,
builder: legacyPickerThemeBuilder,
);
if (picked != null) onPicked(picked);

View File

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

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:ui';
import 'package:latlong2/latlong.dart';
import 'package:legacy_device_state/legacy_device_state.dart';
@@ -15,12 +16,22 @@ class LocationMapController extends _$LocationMapController {
late final SfTrackingRepository _tracking;
Timer? _zoomDebounce;
Timer? _playbackTimer;
Timer? _recenterTimer;
static const Duration _zoomDebounceDelay = Duration(seconds: 1);
static const int _playbackSeconds = 3;
static const int _recenterSeconds = 3;
VoidCallback? onPlaybackTick;
@override
LocationMapState build() {
_tracking = ref.read(sfTrackingProvider);
ref.onDispose(() => _zoomDebounce?.cancel());
ref.onDispose(() {
_zoomDebounce?.cancel();
_playbackTimer?.cancel();
_recenterTimer?.cancel();
});
return const LocationMapState();
}
@@ -56,6 +67,8 @@ class LocationMapController extends _$LocationMapController {
placingMode: PlacingMode.none,
adjustingRadius: false,
previewPoint: null,
editingGeofence: null,
editingFrequentPlace: null,
);
}
@@ -75,13 +88,17 @@ class LocationMapController extends _$LocationMapController {
placingMode: PlacingMode.none,
adjustingRadius: true,
previewPoint: center,
previewRadius: 200,
previewRadius: state.editingGeofence?.radius ?? 200,
);
}
void confirmFrequentPlacePlacement() {
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) {
@@ -123,9 +140,19 @@ class LocationMapController extends _$LocationMapController {
state = state.copyWith(
selectedGeofence: null,
editingGeofence: geofence,
placingMode: PlacingMode.geofence,
previewPoint: LatLng(geofence.latitude, geofence.longitude),
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) {
switch (mode) {
case PlacingMode.geofence:

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
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
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $LocationMapStateCopyWith<LocationMapState> get copyWith => _$LocationMapStateCo
@override
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
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
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;
@useResult
$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
@@ -62,7 +62,7 @@ class _$LocationMapStateCopyWithImpl<$Res>
/// Create a copy of LocationMapState
/// 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(
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
@@ -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 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 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 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,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 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
@@ -121,6 +126,18 @@ $FrequentPlaceEntityCopyWith<$Res>? get selectedFrequentPlace {
/// with the given fields replaced by the non-null parameter values.
@override
@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 {
if (_self.selectedHistoryPosition == 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) {
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();
}
@@ -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) {
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');
}
@@ -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) {
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;
}
@@ -267,7 +284,7 @@ return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_
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;
@@ -279,11 +296,16 @@ class _LocationMapState implements LocationMapState {
@override final GeofenceEntity? selectedGeofence;
@override final GeofenceEntity? editingGeofence;
@override final FrequentPlaceEntity? selectedFrequentPlace;
@override final FrequentPlaceEntity? editingFrequentPlace;
@override final PositionEntity? selectedHistoryPosition;
@override@JsonKey() final bool isFollowing;
@override@JsonKey() final bool actionsExpanded;
@override@JsonKey() final bool frequencyExpanded;
@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
/// with the given fields replaced by the non-null parameter values.
@@ -295,16 +317,16 @@ _$LocationMapStateCopyWith<_LocationMapState> get copyWith => __$LocationMapStat
@override
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
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
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;
@override @useResult
$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
@@ -332,7 +354,7 @@ class __$LocationMapStateCopyWithImpl<$Res>
/// Create a copy of LocationMapState
/// 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(
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
@@ -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 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 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 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,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 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.
@override
@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 {
if (_self.selectedHistoryPosition == null) {
return null;

View File

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

View File

@@ -20,11 +20,15 @@ import 'package:utils/utils.dart';
import 'device_banner.dart';
import 'name_input_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_style_selector.dart';
import 'map_controls/placement_banner.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/geofence_info_card.dart';
import 'map_info_cards/history_position_info_card.dart';
@@ -77,14 +81,17 @@ class _LocationMapState extends ConsumerState<LocationMap>
LocationMapController get _vm =>
ref.read(locationMapControllerProvider.notifier);
Color get _primaryColor =>
context.sfColors.legacyPrimary;
Color get _primaryColor => context.sfColors.legacyPrimary;
@override
void initState() {
super.initState();
_mapController = MapController();
_startMonitoring();
_vm.onPlaybackTick = () {
final state = ref.read(locationMapControllerProvider);
_navigateToHistoryPosition(state.historyNavigationIndex);
};
}
static const _minFrequency = 60;
@@ -94,7 +101,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
final raw = widget.selectedDevice?.settings.frequency ?? 60;
final frequency = raw < _minFrequency ? _minFrequency : raw;
_followTimer = Timer.periodic(Duration(seconds: frequency), (_) {
if (ref.read(selectedDeviceProvider).value?.isDisconnected ?? true) return;
if (ref.read(selectedDeviceProvider).value?.isDisconnected ?? true) {
return;
}
widget.onRefreshPosition();
});
}
@@ -102,6 +111,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
@override
void dispose() {
_followTimer?.cancel();
_vm.onPlaybackTick = null;
_vm.stopPlaybackTimer();
_vm.cancelRecenter();
_moveAnimation?.dispose();
_mapController.dispose();
super.dispose();
@@ -131,12 +143,80 @@ class _LocationMapState extends ConsumerState<LocationMap>
(oldWidget.positionHistory.length != widget.positionHistory.length ||
!oldWidget.showRouteTrail)) {
_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();
final camera = _mapController.camera;
final targetZoom = zoom ?? camera.zoom;
final latTween = Tween<double>(
begin: camera.center.latitude,
end: dest.latitude,
@@ -145,18 +225,12 @@ class _LocationMapState extends ConsumerState<LocationMap>
begin: camera.center.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(
duration: const Duration(milliseconds: 400),
vsync: this,
);
final controller = AnimationController(duration: duration, vsync: this);
_moveAnimation = controller;
final animation = CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
);
final animation = CurvedAnimation(parent: controller, curve: curve);
controller.addListener(() {
_mapController.move(
@@ -170,10 +244,53 @@ class _LocationMapState extends ConsumerState<LocationMap>
status == AnimationStatus.dismissed) {
controller.dispose();
if (_moveAnimation == controller) _moveAnimation = null;
if (!completer.isCompleted) completer.complete();
}
});
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() {
@@ -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 {
if (!await guardDeviceCommand(context, ref)) return;
final success = await ref
@@ -253,7 +410,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
if (!mounted) return;
if (success) {
_followTimer?.cancel();
final safeFrequency = frequency < _minFrequency ? _minFrequency : frequency;
final safeFrequency = frequency < _minFrequency
? _minFrequency
: frequency;
_followTimer = Timer.periodic(Duration(seconds: safeFrequency), (_) {
widget.onRefreshPosition();
});
@@ -274,19 +433,41 @@ class _LocationMapState extends ConsumerState<LocationMap>
if (mapState.placingMode == PlacingMode.geofence) {
_vm.confirmGeofencePlacement(center);
} else {
final editing = mapState.editingFrequentPlace;
_vm.confirmFrequentPlacePlacement();
showNameInputSheet(
context,
ref: ref,
title: context.translate(I18n.locationNewFrequentPlace),
title: context.translate(
editing != null
? I18n.locationEditFrequentPlace
: I18n.locationNewFrequentPlace,
),
hintText: context.translate(I18n.locationHintFrequentPlace),
onSubmit: (name, _) => ref
.read(locationControllerProvider.notifier)
.createFrequentPlace(
name: name,
lat: center.latitude,
lng: center.longitude,
),
initialName: editing?.name,
submitLabel: editing != null
? context.translate(I18n.locationSave)
: null,
onSubmit: (name, _) {
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
? locationState.positionHistory
: [],
hasHiddenGeofences:
!mapState.showGeofences && locationState.geofences.isNotEmpty,
hasHiddenFrequentPlaces:
!mapState.showFrequentPlaces &&
locationState.frequentPlaces.isNotEmpty,
hasHiddenHistory:
!locationState.showRouteTrail &&
locationState.positionHistory.isNotEmpty,
primaryColor: _primaryColor,
onGeofenceTap: (g) {
Navigator.pop(context);
_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) {
Navigator.pop(context);
_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) {
Navigator.pop(context);
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() {
if (widget.positionHistory.isEmpty) {
_openDateRangePicker();
} else {
ref.read(locationControllerProvider.notifier).toggleRouteTrail();
}
_vm.stopPlaybackTimer();
_vm.stopHistoryNavigation();
_openDateRangePicker();
}
Future<void> _openDateRangePicker() async {
@@ -394,12 +639,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
start: now.subtract(const Duration(days: 1)),
end: now,
),
builder: (context, child) => Theme(
data: Theme.of(
context,
).copyWith(colorScheme: ColorScheme.light(primary: _primaryColor)),
child: child!,
),
builder: legacyPickerThemeBuilder,
);
if (picked != null) {
@@ -411,33 +651,31 @@ class _LocationMapState extends ConsumerState<LocationMap>
59,
59,
);
ref
if (!mounted) return;
showInfoDialog(context, I18n.locationHistoryLoading);
await ref
.read(locationControllerProvider.notifier)
.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 {
if (!await guardDeviceCommand(context, ref)) return;
if (!mounted) return;
_vm.clearSelectedFrequentPlace();
showNameInputSheet(
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,
),
);
_vm.startEditingFrequentPlace(fp);
_animatedMove(LatLng(fp.lat, fp.lng), null);
}
List<Widget> _buildMapLayers(LocationMapState mapState) {
@@ -448,6 +686,8 @@ class _LocationMapState extends ConsumerState<LocationMap>
zoom: mapState.mapZoom,
onPositionTap: _vm.selectHistoryPosition,
onClusterTap: _animatedMove,
activeIndex: mapState.historyNavigationIndex,
visibleCount: mapState.historyRevealCount,
)
: null;
@@ -488,17 +728,68 @@ class _LocationMapState extends ConsumerState<LocationMap>
],
),
MarkerLayer(markers: _buildMarkers(mapState, historyLayer)),
if (widget.selectedDevice != null)
Align(
alignment: Alignment.bottomCenter,
child: DeviceBanner(
device: widget.selectedDevice!,
devices: widget.devices,
positions: widget.positions,
onDeviceChanged: widget.onDeviceChanged,
onTap: _centerOnDevice,
if (mapState.isRevealAnimating) ...[
Positioned(
bottom: SizeUtils.getByScreen(small: 16, big: 14),
left: 0,
right: 0,
child: RevealProgressBar(
current: mapState.historyRevealCount ?? 0,
total: widget.positionHistory.length,
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,
height: 40,
child: GestureDetector(
onLongPress: () => _vm.selectFrequentPlace(fp),
onTap: () => _vm.selectFrequentPlace(fp),
child: Container(
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.3),
@@ -567,7 +858,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
width: 36,
height: 36,
child: GestureDetector(
onLongPress: () => _vm.selectGeofence(g),
onTap: () => _vm.selectGeofence(g),
child: Container(
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.3),
@@ -643,7 +934,12 @@ class _LocationMapState extends ConsumerState<LocationMap>
const SizedBox(height: 8),
FrequencySelector(
currentFrequency:
ref.watch(selectedDeviceProvider).value?.settings.frequency ?? 60,
ref
.watch(selectedDeviceProvider)
.value
?.settings
.frequency ??
60,
options: widget.selectedDevice!.capabilities!.location!.options
.where((o) => o > 0 || o == -1)
.toList(),
@@ -656,31 +952,8 @@ class _LocationMapState extends ConsumerState<LocationMap>
Positioned(
top: 12,
right: 12,
child: LayerToggles(
showGeofences: mapState.showGeofences,
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 {
child: GpsControlPanel(
onRefresh: () async {
if (!await guardDeviceCommand(context, ref)) return;
unawaited(
ref.read(sfTrackingProvider).legacyLocationMapRefreshTapped(),
@@ -694,27 +967,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
await showErrorDialog(context, I18n.errorPositions);
}
},
onCenterTap: _centerOnDevice,
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,
);
},
onSettings: () => _showGpsFunctionsDialog(context, ref, mapState),
),
),
];
@@ -791,10 +1044,13 @@ class _LocationMapState extends ConsumerState<LocationMap>
initialZoom: initialZoom,
minZoom: 5,
keepAlive: true,
onPositionChanged: (camera, _) {
onPositionChanged: (camera, hasGesture) {
if (widget.showRouteTrail && widget.positionHistory.length > 2) {
_vm.updateMapZoom(camera.zoom);
}
if (hasGesture) {
_scheduleRecenter();
}
},
),
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_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:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
@@ -120,6 +121,7 @@ class FrequencySelector extends ConsumerWidget {
}
Widget _buildSegmented(BuildContext context, LocationMapController vm) {
final primaryColor = context.sfColors.legacyPrimary;
return Material(
elevation: 2,
borderRadius: BorderRadius.circular(8),
@@ -142,9 +144,7 @@ class FrequencySelector extends ConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 6),
margin: const EdgeInsets.symmetric(vertical: 2),
decoration: BoxDecoration(
color: selected
? Theme.of(context).primaryColor
: Colors.transparent,
color: selected ? primaryColor : Colors.transparent,
borderRadius: BorderRadius.circular(6),
),
child: Center(
@@ -156,7 +156,7 @@ class FrequencySelector extends ConsumerWidget {
fontSize: opt == -1 ? 10 : 12,
fontWeight: FontWeight.w600,
color: selected
? Theme.of(context).colorScheme.surface
? Colors.white
: Theme.of(context).colorScheme.onSurface,
),
),
@@ -167,10 +167,10 @@ class FrequencySelector extends ConsumerWidget {
const SizedBox(height: 2),
GestureDetector(
onTap: vm.collapseFrequency,
child: const SizedBox(
child: SizedBox(
width: 40,
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: () =>
ref.read(mapStyleSelectorExpandedProvider.notifier).set(true),
borderRadius: BorderRadius.circular(8),
child: const SizedBox(
child: SizedBox(
width: 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 ValueChanged<PositionEntity> onPositionTap;
final void Function(LatLng point, double zoom) onClusterTap;
final int activeIndex;
final int? visibleCount;
const RouteHistoryLayer({
super.key,
@@ -28,19 +30,56 @@ class RouteHistoryLayer extends StatelessWidget {
required this.zoom,
required this.onPositionTap,
required this.onClusterTap,
this.activeIndex = -1,
this.visibleCount,
});
List<PositionEntity> get _visiblePositions {
if (visibleCount == null) return positionHistory;
return positionHistory.take(visibleCount!).toList();
}
@override
Widget build(BuildContext context) {
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() {
if (positionHistory.length < 2) return [];
final visible = _visiblePositions;
if (visible.length < 2) return [];
final points = positionHistory
final points = visible
.where(_hasValidCoords)
.map((p) => LatLng(p.latitude, p.longitude))
.toList();
@@ -48,17 +87,18 @@ class RouteHistoryLayer extends StatelessWidget {
if (points.length < 2) return [];
return [
Polyline(points: points, color: routeColor, strokeWidth: 4.0),
Polyline(points: points, color: routeColor, strokeWidth: 5.0),
];
}
List<Marker> _buildDirectionArrows() {
if (positionHistory.length < 2) return [];
final visible = _visiblePositions;
if (visible.length < 2) return [];
final arrows = <Marker>[];
for (int i = 0; i < positionHistory.length - 1; i++) {
final from = positionHistory[i];
final to = positionHistory[i + 1];
for (int i = 0; i < visible.length - 1; i++) {
final from = visible[i];
final to = visible[i + 1];
if (!_hasValidCoords(from) || !_hasValidCoords(to)) continue;
final midLat = (from.latitude + to.latitude) / 2;
@@ -89,14 +129,15 @@ class RouteHistoryLayer extends StatelessWidget {
}
List<Marker> _buildHistoryMarkers() {
if (positionHistory.isEmpty) return [];
final visible = _visiblePositions;
if (visible.isEmpty) return [];
final markers = <Marker>[];
final threshold =
_clusterBaseDegrees / math.pow(2, zoom - _clusterBaseZoom);
final first = positionHistory.first;
final last = positionHistory.last;
final first = visible.first;
final last = visible.last;
markers.add(
_buildEndpointMarker(
@@ -105,7 +146,7 @@ class RouteHistoryLayer extends StatelessWidget {
icon: Icons.play_arrow,
),
);
if (positionHistory.length > 1) {
if (visible.length > 1) {
markers.add(
_buildEndpointMarker(
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,
positionHistory.length - 1,
visible.length - 1,
);
final clustered = <List<int>>[];
final visited = List.filled(intermediates.length, false);
@@ -214,11 +255,11 @@ class RouteHistoryLayer extends StatelessWidget {
}) {
return Marker(
point: LatLng(position.latitude, position.longitude),
width: 18,
height: 18,
width: 28,
height: 28,
child: GestureDetector(
onTap: () => onPositionTap(position),
child: _CircleMarkerIcon(color: color, size: 18),
child: _CircleMarkerIcon(color: color, size: 28),
),
rotate: true,
);

View File

@@ -0,0 +1,311 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:latlong2/latlong.dart';
import 'package:location/src/core/domain/entities/geofence_entity.dart';
import 'package:location/src/core/domain/entities/frequent_place_entity.dart';
import 'package:location/src/features/location/presentation/providers/location_map_controller.dart';
import 'package:location/src/features/location/presentation/providers/location_map_state.dart';
import 'package:sf_shared/testing.dart';
import 'package:sf_tracking/sf_tracking.dart';
const _geofence = GeofenceEntity(
id: 'g1',
name: 'Home',
latitude: 40.0,
longitude: -3.0,
radius: 200,
isActive: true,
createdAt: 0,
);
const _frequentPlace = FrequentPlaceEntity(
id: 'f1',
name: 'School',
lat: 41.0,
lng: -3.5,
createdAt: 0,
);
ProviderContainer buildContainer() {
return makeContainer(
overrides: [
sfTrackingProvider.overrideWithValue(
SfTrackingRepository(clients: const []),
),
],
);
}
LocationMapState readState(ProviderContainer container) =>
container.read(locationMapControllerProvider);
LocationMapController readNotifier(ProviderContainer container) =>
container.read(locationMapControllerProvider.notifier);
void main() {
group('history navigation', () {
test('startHistoryNavigation sets index to 0', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startHistoryNavigation();
expect(readState(container).historyNavigationIndex, 0);
expect(readState(container).historyPlaying, false);
});
test('stopHistoryNavigation resets index to -1', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startHistoryNavigation();
readNotifier(container).stopHistoryNavigation();
expect(readState(container).historyNavigationIndex, -1);
expect(readState(container).historyPlaying, false);
});
test('nextHistoryPosition increments index', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startHistoryNavigation();
readNotifier(container).nextHistoryPosition(10);
expect(readState(container).historyNavigationIndex, 1);
});
test('nextHistoryPosition at end sets historyPlaying false', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startHistoryNavigation();
readNotifier(container).nextHistoryPosition(2);
readNotifier(container).nextHistoryPosition(2);
expect(readState(container).historyNavigationIndex, 1);
expect(readState(container).historyPlaying, false);
});
test('previousHistoryPosition decrements index', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startHistoryNavigation();
readNotifier(container).nextHistoryPosition(10);
readNotifier(container).nextHistoryPosition(10);
readNotifier(container).previousHistoryPosition();
expect(readState(container).historyNavigationIndex, 1);
});
test('previousHistoryPosition at 0 stays at 0', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startHistoryNavigation();
readNotifier(container).previousHistoryPosition();
expect(readState(container).historyNavigationIndex, 0);
});
});
group('history playback', () {
test('toggleHistoryPlayback activates playback', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startHistoryNavigation();
readNotifier(container).toggleHistoryPlayback(10);
expect(readState(container).historyPlaying, true);
});
test('toggleHistoryPlayback deactivates playback', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startHistoryNavigation();
readNotifier(container).toggleHistoryPlayback(10);
readNotifier(container).toggleHistoryPlayback(10);
expect(readState(container).historyPlaying, false);
});
test('toggleHistoryPlayback at end restarts from 0', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startHistoryNavigation();
readNotifier(container).nextHistoryPosition(3);
readNotifier(container).nextHistoryPosition(3);
readNotifier(container).toggleHistoryPlayback(3);
expect(readState(container).historyNavigationIndex, 0);
expect(readState(container).historyPlaying, true);
});
test('stopPlaybackTimer cancels timer', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startHistoryNavigation();
readNotifier(container).toggleHistoryPlayback(10);
readNotifier(container).stopPlaybackTimer();
expect(readState(container).historyPlaying, true);
});
test('startPlaybackTimer sets up timer and stopPlaybackTimer clears it', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startHistoryNavigation();
readNotifier(container).toggleHistoryPlayback(10);
expect(readState(container).historyPlaying, true);
readNotifier(container).stopPlaybackTimer();
readNotifier(container).stopPlaybackTimer();
});
});
group('reveal animation', () {
test('startReveal sets animating true and count 1', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startReveal();
expect(readState(container).isRevealAnimating, true);
expect(readState(container).historyRevealCount, 1);
});
test('updateRevealCount updates count', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startReveal();
readNotifier(container).updateRevealCount(5);
expect(readState(container).historyRevealCount, 5);
});
test('finishReveal clears animation state', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startReveal();
readNotifier(container).finishReveal();
expect(readState(container).isRevealAnimating, false);
expect(readState(container).historyRevealCount, isNull);
});
});
group('geofence editing', () {
test('startEditingGeofence enters placement mode', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startEditingGeofence(_geofence);
final state = readState(container);
expect(state.placingMode, PlacingMode.geofence);
expect(state.editingGeofence, _geofence);
expect(state.previewPoint, LatLng(40.0, -3.0));
expect(state.previewRadius, 200);
expect(state.selectedGeofence, isNull);
});
test('confirmGeofencePlacement preserves editing radius', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startEditingGeofence(_geofence);
readNotifier(container).confirmGeofencePlacement(LatLng(41.0, -2.0));
final state = readState(container);
expect(state.placingMode, PlacingMode.none);
expect(state.adjustingRadius, true);
expect(state.previewPoint, LatLng(41.0, -2.0));
expect(state.previewRadius, 200);
});
test('confirmGeofencePlacement defaults radius 200 for new geofence', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startPlacing(PlacingMode.geofence);
readNotifier(container).confirmGeofencePlacement(LatLng(41.0, -2.0));
expect(readState(container).previewRadius, 200);
});
test('cancelPlacing clears editing state', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startEditingGeofence(_geofence);
readNotifier(container).cancelPlacing();
final state = readState(container);
expect(state.placingMode, PlacingMode.none);
expect(state.editingGeofence, isNull);
expect(state.editingFrequentPlace, isNull);
expect(state.previewPoint, isNull);
});
});
group('frequent place editing', () {
test('startEditingFrequentPlace enters placement mode', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startEditingFrequentPlace(_frequentPlace);
final state = readState(container);
expect(state.placingMode, PlacingMode.frequentPlace);
expect(state.editingFrequentPlace, _frequentPlace);
expect(state.previewPoint, LatLng(41.0, -3.5));
expect(state.selectedFrequentPlace, isNull);
});
test('confirmFrequentPlacePlacement clears editing state', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).startEditingFrequentPlace(_frequentPlace);
readNotifier(container).confirmFrequentPlacePlacement();
final state = readState(container);
expect(state.placingMode, PlacingMode.none);
expect(state.editingFrequentPlace, isNull);
expect(state.previewPoint, isNull);
});
});
group('recenter', () {
test('scheduleRecenter does nothing when not following', () {
final container = buildContainer();
addTearDown(container.dispose);
expect(readState(container).isFollowing, false);
var called = false;
readNotifier(container).scheduleRecenter(() => called = true);
expect(called, false);
});
test('cancelRecenter does not throw', () {
final container = buildContainer();
addTearDown(container.dispose);
readNotifier(container).toggleFollowing();
readNotifier(container).scheduleRecenter(() {});
readNotifier(container).cancelRecenter();
readNotifier(container).cancelRecenter();
});
});
}

View File

@@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_theme/legacy_theme.dart';
@@ -98,7 +97,7 @@ class _AlarmFormSheet extends ConsumerWidget {
style: TextStyle(
color: formState.canSave
? primaryColor
: Colors.grey,
: Theme.of(context).colorScheme.outline,
fontWeight: FontWeight.w600,
fontSize:
SizeUtils.getByScreen(small: 16, big: 17),
@@ -108,20 +107,44 @@ class _AlarmFormSheet extends ConsumerWidget {
],
),
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 18)),
Container(
height: SizeUtils.getByScreen(small: 160, big: 180),
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(16)),
color: Colors.grey.shade50,
),
child: CupertinoTimerPicker(
mode: CupertinoTimerPickerMode.hm,
initialTimerDuration: Duration(
minutes: formState.durationMinutes,
GestureDetector(
onTap: () async {
final picked = await showTimePicker(
context: context,
initialTime: TimeOfDay(
hour: formState.durationMinutes ~/ 60,
minute: formState.durationMinutes % 60,
),
builder: legacyPickerThemeBuilder,
);
if (picked != null) {
notifier.setDurationMinutes(
picked.hour * 60 + picked.minute,
);
}
},
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
vertical: SizeUtils.getByScreen(small: 24, big: 28),
),
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(16)),
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
),
child: Center(
child: Text(
'${(formState.durationMinutes ~/ 60).toString().padLeft(2, '0')}:${(formState.durationMinutes % 60).toString().padLeft(2, '0')}',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w300,
color: primaryColor,
),
),
),
onTimerDurationChanged: (duration) =>
notifier.setDurationMinutes(duration.inMinutes),
),
),
SizedBox(height: SizeUtils.getByScreen(small: 20, big: 22)),
@@ -197,7 +220,9 @@ class _RadioOption extends StatelessWidget {
children: [
Icon(
isSelected ? Icons.radio_button_checked : Icons.radio_button_off,
color: isSelected ? primaryColor : Colors.grey,
color: isSelected
? primaryColor
: Theme.of(context).colorScheme.outline,
size: SizeUtils.getByScreen(small: 22, big: 24),
),
const SizedBox(width: 12),

View File

@@ -11,11 +11,11 @@ import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_shared/sf_shared.dart';
const _languageOptions = <String, String>{
'es': 'español',
'es': 'Español',
'en': 'English',
'pt': 'português',
'it': 'italiano',
'fr': 'français',
'pt': 'Português',
'it': 'Italiano',
'fr': 'Français',
'de': 'Deutsch',
};

View File

@@ -6,3 +6,4 @@ export 'src/widgets/refreshable_error_state.dart';
export 'src/widgets/week_day_chips.dart';
export 'src/providers/map_style_provider.dart';
export 'src/transitions/legacy_transitions.dart';
export 'src/utils/legacy_picker_theme.dart';

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:legacy_theme/legacy_theme.dart';
const _duration = Duration(milliseconds: 300);
const _dialogDuration = Duration(milliseconds: 500);
@@ -8,13 +9,24 @@ Future<T?> showLegacyDialog<T>({
required WidgetBuilder builder,
bool barrierDismissible = true,
}) {
final primaryColor = context.sfColors.legacyPrimary;
final brightness = Theme.of(context).brightness;
return showGeneralDialog<T>(
context: context,
barrierDismissible: barrierDismissible,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration: _dialogDuration,
pageBuilder: (dialogContext, __, ___) => builder(dialogContext),
pageBuilder: (dialogContext, __, ___) {
final colorScheme = brightness == Brightness.dark
? ColorScheme.dark(primary: primaryColor)
: ColorScheme.light(primary: primaryColor);
return Theme(
data: Theme.of(dialogContext).copyWith(colorScheme: colorScheme),
child: builder(dialogContext),
);
},
transitionBuilder: (_, animation, __, child) {
final fadeCurved = CurvedAnimation(
parent: animation,

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:legacy_theme/legacy_theme.dart';
Widget legacyPickerThemeBuilder(BuildContext context, Widget? child) {
final primaryColor = context.sfColors.legacyPrimary;
final brightness = Theme.of(context).brightness;
final colorScheme = brightness == Brightness.dark
? ColorScheme.dark(primary: primaryColor)
: ColorScheme.light(primary: primaryColor);
return Theme(
data: Theme.of(context).copyWith(colorScheme: colorScheme),
child: child!,
);
}

View File

@@ -667,6 +667,22 @@
"locationBannerAddress": "ADRESSE",
"locationBannerDateTime": "DATUM/UHRZEIT",
"locationBannerBattery": "BATTERIE",
"gpsFunctionsTitle": "Einstellungen",
"gpsFunctionList": "Liste anzeigen",
"gpsFunctionAddGeofence": "Sichere Zone hinzufügen",
"gpsFunctionAddFrequentPlace": "Häufigen Ort hinzufügen",
"gpsFunctionShare": "Standort teilen",
"gpsFunctionCenter": "Auf Gerät zentrieren",
"gpsFunctionFollow": "Auf der Karte zentriert halten",
"gpsFunctionStopFollow": "Zentrierung auf der Karte beenden",
"gpsFunctionFrequency": "Ortungsfrequenz",
"locationHistoryLoading": "Positionsverlauf wird geladen...",
"locationHistoryEmpty": "Keine Positionen im ausgewählten Zeitraum gefunden",
"locationHistoryLoaded": "{count} Positionen gefunden",
"locationRevealProgress": "Route wird gezeichnet...",
"locationRevealSkip": "Überspringen",
"locationDeleteGeofenceConfirm": "Möchten Sie diese sichere Zone wirklich löschen?",
"locationDeleteFrequentPlaceConfirm": "Möchten Sie diesen häufigen Ort wirklich löschen?",
"positionUpdated": "Letzte verfügbare Position aktualisiert",
"locationMapStyleLight": "Hell",
"locationMapStyleDark": "Dunkel",
@@ -698,6 +714,7 @@
"locationListFrequentPlaces": "Häufige Orte",
"locationListPositionHistory": "Positionsverlauf",
"locationListNoItems": "Keine Elemente vorhanden",
"locationListHiddenItems": "Es gibt ausgeblendete Elemente.\nAktivieren Sie deren Sichtbarkeit in den Einstellungen, um sie hier zu sehen.",
"locationListAll": "Alle",
"locationHistoryPosition": "Verlaufsposition",
"locationDate": "Datum",

View File

@@ -847,6 +847,22 @@
"locationBannerAddress": "ADDRESS",
"locationBannerDateTime": "DATE/TIME",
"locationBannerBattery": "BATTERY",
"gpsFunctionsTitle": "Settings",
"gpsFunctionList": "View list",
"gpsFunctionAddGeofence": "Add safe zone",
"gpsFunctionAddFrequentPlace": "Add frequent place",
"gpsFunctionShare": "Share location",
"gpsFunctionCenter": "Center on device",
"gpsFunctionFollow": "Keep centered on map",
"gpsFunctionStopFollow": "Stop centering on map",
"gpsFunctionFrequency": "Location frequency",
"locationHistoryLoading": "Loading position history...",
"locationHistoryEmpty": "No positions found in the selected range",
"locationHistoryLoaded": "{count} positions found",
"locationRevealProgress": "Drawing route...",
"locationRevealSkip": "Skip",
"locationDeleteGeofenceConfirm": "Are you sure you want to delete this safe zone?",
"locationDeleteFrequentPlaceConfirm": "Are you sure you want to delete this frequent place?",
"positionUpdated": "Updated to latest available position",
"locationMapStyleLight": "Light",
"locationMapStyleDark": "Dark",
@@ -878,6 +894,7 @@
"locationListFrequentPlaces": "Frequent places",
"locationListPositionHistory": "Position history",
"locationListNoItems": "No items to show",
"locationListHiddenItems": "There are hidden items.\nEnable their visibility from Settings to see them here.",
"locationListAll": "All",
"locationHistoryPosition": "History position",
"locationDate": "Date",

View File

@@ -848,6 +848,22 @@
"locationBannerAddress": "DIRECCIÓN",
"locationBannerDateTime": "FECHA/HORA",
"locationBannerBattery": "BATERÍA",
"gpsFunctionsTitle": "Configuraciones",
"gpsFunctionList": "Ver listado",
"gpsFunctionAddGeofence": "Agregar zona segura",
"gpsFunctionAddFrequentPlace": "Agregar lugar frecuente",
"gpsFunctionShare": "Compartir ubicación",
"gpsFunctionCenter": "Centrar en dispositivo",
"gpsFunctionFollow": "Mantener centrado en el mapa",
"gpsFunctionStopFollow": "Dejar de centrar en el mapa",
"gpsFunctionFrequency": "Frecuencia de localización",
"locationHistoryLoading": "Cargando historial de posiciones...",
"locationHistoryEmpty": "No se encontraron posiciones en el rango seleccionado",
"locationHistoryLoaded": "Se encontraron {count} posiciones",
"locationRevealProgress": "Trazando ruta...",
"locationRevealSkip": "Saltar",
"locationDeleteGeofenceConfirm": "¿Seguro que quieres eliminar esta zona segura?",
"locationDeleteFrequentPlaceConfirm": "¿Seguro que quieres eliminar este lugar frecuente?",
"positionUpdated": "Última posición disponible actualizada",
"locationMapStyleLight": "Claro",
"locationMapStyleDark": "Oscuro",
@@ -879,6 +895,7 @@
"locationListFrequentPlaces": "Lugares frecuentes",
"locationListPositionHistory": "Historial de posiciones",
"locationListNoItems": "No hay elementos para mostrar",
"locationListHiddenItems": "Hay elementos ocultos.\nActiva su visibilidad desde Configuraciones para verlos aquí.",
"locationListAll": "Todos",
"locationHistoryPosition": "Posición del historial",
"locationDate": "Fecha",

View File

@@ -667,6 +667,22 @@
"locationBannerAddress": "ADRESSE",
"locationBannerDateTime": "DATE/HEURE",
"locationBannerBattery": "BATTERIE",
"gpsFunctionsTitle": "Paramètres",
"gpsFunctionList": "Voir la liste",
"gpsFunctionAddGeofence": "Ajouter une zone sûre",
"gpsFunctionAddFrequentPlace": "Ajouter un lieu fréquent",
"gpsFunctionShare": "Partager la position",
"gpsFunctionCenter": "Centrer sur l'appareil",
"gpsFunctionFollow": "Maintenir centré sur la carte",
"gpsFunctionStopFollow": "Arrêter de centrer sur la carte",
"gpsFunctionFrequency": "Fréquence de localisation",
"locationHistoryLoading": "Chargement de l'historique des positions...",
"locationHistoryEmpty": "Aucune position trouvée dans la plage sélectionnée",
"locationHistoryLoaded": "{count} positions trouvées",
"locationRevealProgress": "Tracé de l'itinéraire...",
"locationRevealSkip": "Passer",
"locationDeleteGeofenceConfirm": "Voulez-vous supprimer cette zone sûre ?",
"locationDeleteFrequentPlaceConfirm": "Voulez-vous supprimer ce lieu fréquent ?",
"positionUpdated": "Dernière position disponible mise à jour",
"locationMapStyleLight": "Clair",
"locationMapStyleDark": "Sombre",
@@ -698,6 +714,7 @@
"locationListFrequentPlaces": "Lieux fréquents",
"locationListPositionHistory": "Historique des positions",
"locationListNoItems": "Aucun élément à afficher",
"locationListHiddenItems": "Des éléments sont masqués.\nActivez leur visibilité depuis les Paramètres pour les voir ici.",
"locationListAll": "Tous",
"locationHistoryPosition": "Position de l'historique",
"locationDate": "Date",

View File

@@ -667,6 +667,22 @@
"locationBannerAddress": "INDIRIZZO",
"locationBannerDateTime": "DATA/ORA",
"locationBannerBattery": "BATTERIA",
"gpsFunctionsTitle": "Impostazioni",
"gpsFunctionList": "Visualizza elenco",
"gpsFunctionAddGeofence": "Aggiungi zona sicura",
"gpsFunctionAddFrequentPlace": "Aggiungi luogo frequente",
"gpsFunctionShare": "Condividi posizione",
"gpsFunctionCenter": "Centra sul dispositivo",
"gpsFunctionFollow": "Mantieni centrato sulla mappa",
"gpsFunctionStopFollow": "Smetti di centrare sulla mappa",
"gpsFunctionFrequency": "Frequenza di localizzazione",
"locationHistoryLoading": "Caricamento cronologia posizioni...",
"locationHistoryEmpty": "Nessuna posizione trovata nell'intervallo selezionato",
"locationHistoryLoaded": "{count} posizioni trovate",
"locationRevealProgress": "Tracciamento percorso...",
"locationRevealSkip": "Salta",
"locationDeleteGeofenceConfirm": "Sei sicuro di voler eliminare questa zona sicura?",
"locationDeleteFrequentPlaceConfirm": "Sei sicuro di voler eliminare questo luogo frequente?",
"positionUpdated": "Ultima posizione disponibile aggiornata",
"locationMapStyleLight": "Chiaro",
"locationMapStyleDark": "Scuro",
@@ -698,6 +714,7 @@
"locationListFrequentPlaces": "Luoghi frequenti",
"locationListPositionHistory": "Cronologia posizioni",
"locationListNoItems": "Nessun elemento da mostrare",
"locationListHiddenItems": "Ci sono elementi nascosti.\nAttiva la loro visibilità dalle Impostazioni per vederli qui.",
"locationListAll": "Tutti",
"locationHistoryPosition": "Posizione della cronologia",
"locationDate": "Data",

View File

@@ -667,6 +667,22 @@
"locationBannerAddress": "ENDEREÇO",
"locationBannerDateTime": "DATA/HORA",
"locationBannerBattery": "BATERIA",
"gpsFunctionsTitle": "Configurações",
"gpsFunctionList": "Ver lista",
"gpsFunctionAddGeofence": "Adicionar zona segura",
"gpsFunctionAddFrequentPlace": "Adicionar lugar frequente",
"gpsFunctionShare": "Partilhar localização",
"gpsFunctionCenter": "Centrar no dispositivo",
"gpsFunctionFollow": "Manter centrado no mapa",
"gpsFunctionStopFollow": "Parar de centrar no mapa",
"gpsFunctionFrequency": "Frequência de localização",
"locationHistoryLoading": "A carregar histórico de posições...",
"locationHistoryEmpty": "Nenhuma posição encontrada no intervalo selecionado",
"locationHistoryLoaded": "{count} posições encontradas",
"locationRevealProgress": "Traçando rota...",
"locationRevealSkip": "Saltar",
"locationDeleteGeofenceConfirm": "Tens a certeza que queres eliminar esta zona segura?",
"locationDeleteFrequentPlaceConfirm": "Tens a certeza que queres eliminar este lugar frequente?",
"positionUpdated": "Última posição disponível atualizada",
"locationMapStyleLight": "Claro",
"locationMapStyleDark": "Escuro",
@@ -698,6 +714,7 @@
"locationListFrequentPlaces": "Locais frequentes",
"locationListPositionHistory": "Histórico de posições",
"locationListNoItems": "Nenhum item para mostrar",
"locationListHiddenItems": "Existem itens ocultos.\nAtive a sua visibilidade nas Configurações para os ver aqui.",
"locationListAll": "Todos",
"locationHistoryPosition": "Posição do histórico",
"locationDate": "Data",

View File

@@ -623,6 +623,23 @@ class I18n {
static const String locationBannerAddress = 'locationBannerAddress';
static const String locationBannerDateTime = 'locationBannerDateTime';
static const String locationBannerBattery = 'locationBannerBattery';
static const String gpsFunctionsTitle = 'gpsFunctionsTitle';
static const String gpsFunctionList = 'gpsFunctionList';
static const String gpsFunctionAddGeofence = 'gpsFunctionAddGeofence';
static const String gpsFunctionAddFrequentPlace = 'gpsFunctionAddFrequentPlace';
static const String gpsFunctionShare = 'gpsFunctionShare';
static const String gpsFunctionCenter = 'gpsFunctionCenter';
static const String gpsFunctionFollow = 'gpsFunctionFollow';
static const String gpsFunctionStopFollow = 'gpsFunctionStopFollow';
static const String gpsFunctionFrequency = 'gpsFunctionFrequency';
static const String locationHistoryLoading = 'locationHistoryLoading';
static const String locationHistoryEmpty = 'locationHistoryEmpty';
static const String locationHistoryLoaded = 'locationHistoryLoaded';
static const String locationRevealProgress = 'locationRevealProgress';
static const String locationRevealSkip = 'locationRevealSkip';
static const String locationDeleteGeofenceConfirm = 'locationDeleteGeofenceConfirm';
static const String locationDeleteFrequentPlaceConfirm = 'locationDeleteFrequentPlaceConfirm';
static const String locationListHiddenItems = 'locationListHiddenItems';
static const String positionUpdated = 'positionUpdated';
static const String locationNewFrequentPlace = 'locationNewFrequentPlace';
static const String locationNewGeofence = 'locationNewGeofence';

View File

@@ -1,17 +1,39 @@
import 'dart:async';
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:sf_localizations/sf_localizations.dart';
enum _FeedbackKind { error, success, info }
final _queue = Queue<_FeedbackRequest>();
bool _isShowing = false;
class _FeedbackRequest {
final BuildContext context;
final String messageKey;
final Map<String, dynamic>? args;
final _FeedbackKind kind;
final Duration autoDismiss;
final Completer<void> completer;
_FeedbackRequest({
required this.context,
required this.messageKey,
required this.args,
required this.kind,
required this.autoDismiss,
required this.completer,
});
}
Future<void> showErrorDialog(
BuildContext context,
String messageKey, {
Map<String, dynamic>? args,
Duration autoDismiss = const Duration(seconds: 3),
}) {
return _showPillFeedback(
return _enqueue(
context,
messageKey: messageKey,
args: args,
@@ -26,7 +48,7 @@ Future<void> showSuccessDialog(
Map<String, dynamic>? args,
Duration autoDismiss = const Duration(seconds: 3),
}) {
return _showPillFeedback(
return _enqueue(
context,
messageKey: messageKey,
args: args,
@@ -41,7 +63,7 @@ Future<void> showInfoDialog(
Map<String, dynamic>? args,
Duration autoDismiss = const Duration(seconds: 3),
}) {
return _showPillFeedback(
return _enqueue(
context,
messageKey: messageKey,
args: args,
@@ -50,6 +72,50 @@ Future<void> showInfoDialog(
);
}
Future<void> _enqueue(
BuildContext context, {
required String messageKey,
required Map<String, dynamic>? args,
required _FeedbackKind kind,
required Duration autoDismiss,
}) {
final completer = Completer<void>();
_queue.add(_FeedbackRequest(
context: context,
messageKey: messageKey,
args: args,
kind: kind,
autoDismiss: autoDismiss,
completer: completer,
));
_processQueue();
return completer.future;
}
Future<void> _processQueue() async {
if (_isShowing || _queue.isEmpty) return;
while (_queue.isNotEmpty && !_queue.first.context.mounted) {
_queue.removeFirst().completer.complete();
}
if (_queue.isEmpty) return;
_isShowing = true;
final request = _queue.removeFirst();
await _showPillFeedback(
request.context,
messageKey: request.messageKey,
args: request.args,
kind: request.kind,
autoDismiss: request.autoDismiss,
);
request.completer.complete();
_isShowing = false;
_processQueue();
}
Future<void> _showPillFeedback(
BuildContext context, {
required String messageKey,
@@ -57,6 +123,7 @@ Future<void> _showPillFeedback(
required _FeedbackKind kind,
required Duration autoDismiss,
}) async {
if (!context.mounted) return;
final overlay = Overlay.of(context);
final resolved = context.translate(messageKey, args: args);
@@ -78,6 +145,7 @@ Future<void> _showPillFeedback(
),
};
final completer = Completer<void>();
late final OverlayEntry entry;
late final AnimationController controller;
@@ -93,9 +161,11 @@ Future<void> _showPillFeedback(
);
void dismiss() {
if (completer.isCompleted) return;
controller.reverse().then((_) {
entry.remove();
controller.dispose();
completer.complete();
});
}
@@ -192,8 +262,7 @@ Future<void> _showPillFeedback(
controller.forward();
await Future.delayed(autoDismiss);
if (controller.isAnimating ||
controller.status == AnimationStatus.completed) {
dismiss();
}
dismiss();
return completer.future;
}