feat: add route history, map controls, and geofence/FP management
- Position history with polyline trail and date range picker - Map style selector (standard, voyager, light, dark, satellite) persisted via SharedPreferences - Geofence and frequent place CRUD with info cards - Device banner with swipeable carousel - Refresh position button - Widget extraction: map controls, info cards, device banner, modal overlay
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
library legacy_shared;
|
||||
|
||||
export 'src/providers/map_style_provider.dart';
|
||||
export 'src/providers/selected_device_provider.dart';
|
||||
export 'src/widgets/layouts/page_layout.dart';
|
||||
export 'src/components/section_button.dart';
|
||||
export 'src/widgets/pulsing_location_marker.dart';
|
||||
export 'src/widgets/week_day_chips.dart';
|
||||
export 'src/components/menu_button.dart';
|
||||
export 'src/data/models/device_response_model.dart';
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const _mapStyleKey = 'location_map_style';
|
||||
|
||||
enum MapStyle {
|
||||
standard('https://tile.openstreetmap.org/{z}/{x}/{y}.png'),
|
||||
voyager('https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png'),
|
||||
light('https://basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png'),
|
||||
dark('https://basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png'),
|
||||
satellite('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}');
|
||||
|
||||
const MapStyle(this.urlTemplate);
|
||||
final String urlTemplate;
|
||||
}
|
||||
|
||||
final mapStyleProvider =
|
||||
NotifierProvider<MapStyleNotifier, MapStyle>(MapStyleNotifier.new);
|
||||
|
||||
class MapStyleNotifier extends Notifier<MapStyle> {
|
||||
@override
|
||||
MapStyle build() {
|
||||
_load();
|
||||
return MapStyle.standard;
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedName = prefs.getString(_mapStyleKey);
|
||||
if (savedName == null) return;
|
||||
|
||||
final style =
|
||||
MapStyle.values.where((s) => s.name == savedName).firstOrNull;
|
||||
if (style != null && style != state) {
|
||||
state = style;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setStyle(MapStyle style) async {
|
||||
state = style;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_mapStyleKey, style.name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PulsingLocationMarker extends StatefulWidget {
|
||||
final Color? color;
|
||||
|
||||
const PulsingLocationMarker({super.key, this.color});
|
||||
|
||||
@override
|
||||
State<PulsingLocationMarker> createState() => _PulsingLocationMarkerState();
|
||||
}
|
||||
|
||||
class _PulsingLocationMarkerState extends State<PulsingLocationMarker>
|
||||
with TickerProviderStateMixin {
|
||||
late final AnimationController _controller1;
|
||||
late final AnimationController _controller2;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller1 = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
)..repeat();
|
||||
_controller2 = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
);
|
||||
Future.delayed(const Duration(milliseconds: 1000), () {
|
||||
if (mounted) _controller2.repeat();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller1.dispose();
|
||||
_controller2.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = widget.color ?? const Color(0xFF329E95);
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
_buildRipple(_controller1, c),
|
||||
_buildRipple(_controller2, c),
|
||||
Container(
|
||||
width: 18,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
color: c,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 3),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: c.withValues(alpha: 0.4),
|
||||
blurRadius: 6,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRipple(AnimationController controller, Color color) {
|
||||
return AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, child) {
|
||||
final scale = 1.0 + controller.value * 2.5;
|
||||
final opacity = (1.0 - controller.value).clamp(0.0, 0.5);
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
child: Container(
|
||||
width: 18,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color.withValues(alpha: opacity),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ dependencies:
|
||||
dio: ^5.9.0
|
||||
get_it: ^9.0.5
|
||||
flutter_riverpod: ^3.0.3
|
||||
shared_preferences: ^2.5.0
|
||||
freezed_annotation: ^3.1.0
|
||||
freezed: ^3.2.3
|
||||
json_annotation: ^4.9.0
|
||||
|
||||
Reference in New Issue
Block a user