From 6718ef712928ec9e757acb3fdc1a0691d0b0d422 Mon Sep 17 00:00:00 2001 From: Nedifinita Date: Mon, 29 Sep 2025 18:44:15 +0800 Subject: [PATCH] feat: add vector railway map --- android/app/build.gradle | 2 +- assets/mapbox_map.html | 15038 ++++++++++++++++ devtools_options.yaml | 3 + lib/models/merged_record.dart | 4 +- lib/screens/history_screen.dart | 2 +- lib/screens/main_screen.dart | 26 +- lib/screens/map_screen.dart | 37 +- lib/screens/map_webview_screen.dart | 1162 ++ lib/screens/settings_screen.dart | 74 +- lib/services/database_service.dart | 111 +- lib/services/merge_service.dart | 47 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 100 +- pubspec.yaml | 6 +- 14 files changed, 16565 insertions(+), 49 deletions(-) create mode 100644 assets/mapbox_map.html create mode 100644 devtools_options.yaml create mode 100644 lib/screens/map_webview_screen.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index c5b46c3..edfdc22 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,7 +26,7 @@ if (flutterVersionName == null) { android { namespace = "org.noxylva.lbjconsole.flutter" compileSdk = 36 - ndkVersion = "26.1.10909125" + ndkVersion = "28.1.13356709" compileOptions { sourceCompatibility = JavaVersion.VERSION_11 diff --git a/assets/mapbox_map.html b/assets/mapbox_map.html new file mode 100644 index 0000000..eb47f2e --- /dev/null +++ b/assets/mapbox_map.html @@ -0,0 +1,15038 @@ + + + + + LBJ Console Railway Map + + + + + + +
+ + + diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/models/merged_record.dart b/lib/models/merged_record.dart index 6365f27..da75a5b 100644 --- a/lib/models/merged_record.dart +++ b/lib/models/merged_record.dart @@ -18,11 +18,12 @@ class MergeSettings { final bool enabled; final GroupBy groupBy; final TimeWindow timeWindow; - + final bool hideUngroupableRecords; MergeSettings({ this.enabled = true, this.groupBy = GroupBy.trainAndLoco, this.timeWindow = TimeWindow.unlimited, + this.hideUngroupableRecords = false, }); factory MergeSettings.fromMap(Map map) { @@ -36,6 +37,7 @@ class MergeSettings { (e) => e.name == map['timeWindow'], orElse: () => TimeWindow.unlimited, ), + hideUngroupableRecords: (map['hideUngroupableRecords'] ?? 0) == 1, ); } } diff --git a/lib/screens/history_screen.dart b/lib/screens/history_screen.dart index 128dfa2..28a47c4 100644 --- a/lib/screens/history_screen.dart +++ b/lib/screens/history_screen.dart @@ -1383,7 +1383,7 @@ class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> { return FlutterMap( options: MapOptions( onPositionChanged: (position, hasGesture) => _onCameraMove(), - minZoom: 5, + minZoom: 8, maxZoom: 18, ), mapController: _mapController, diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 5f4a606..7829bb1 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -6,6 +6,7 @@ import 'package:lbjconsole/models/merged_record.dart'; import 'package:lbjconsole/models/train_record.dart'; import 'package:lbjconsole/screens/history_screen.dart'; import 'package:lbjconsole/screens/map_screen.dart'; +import 'package:lbjconsole/screens/map_webview_screen.dart'; import 'package:lbjconsole/screens/settings_screen.dart'; import 'package:lbjconsole/services/ble_service.dart'; import 'package:lbjconsole/services/database_service.dart'; @@ -174,6 +175,7 @@ class MainScreen extends StatefulWidget { class _MainScreenState extends State with WidgetsBindingObserver { int _currentIndex = 0; + String _mapType = 'webview'; late final BLEService _bleService; final NotificationService _notificationService = NotificationService(); @@ -195,8 +197,19 @@ class _MainScreenState extends State with WidgetsBindingObserver { _initializeServices(); _checkAndStartBackgroundService(); _setupLastReceivedTimeListener(); + _loadMapType(); } + Future _loadMapType() async { + final settings = await DatabaseService.instance.getAllSettings() ?? {}; + if (mounted) { + setState(() { + _mapType = settings['mapType']?.toString() ?? 'webview'; + }); + } + } + + Future _checkAndStartBackgroundService() async { final settings = await DatabaseService.instance.getAllSettings() ?? {}; final backgroundServiceEnabled = @@ -231,6 +244,7 @@ class _MainScreenState extends State with WidgetsBindingObserver { void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { _bleService.onAppResume(); + _loadMapType(); } } @@ -380,8 +394,12 @@ class _MainScreenState extends State with WidgetsBindingObserver { onEditModeChanged: _handleHistoryEditModeChanged, onSelectionChanged: _handleSelectionChanged, ), - const MapScreen(), - const SettingsScreen(), + _mapType == 'map' ? const MapScreen() : const MapWebViewScreen(), + SettingsScreen( + onSettingsChanged: () { + _loadMapType(); + }, + ), ]; return Scaffold( @@ -399,6 +417,10 @@ class _MainScreenState extends State with WidgetsBindingObserver { if (_currentIndex == 2 && index == 0) { _historyScreenKey.currentState?.reloadRecords(); } + // 如果从设置页面切换到地图页面,重新加载地图类型 + if (_currentIndex == 2 && index == 1) { + _loadMapType(); + } setState(() { if (_isHistoryEditMode) _isHistoryEditMode = false; _currentIndex = index; diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 80b3087..f9cf471 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -22,7 +22,7 @@ class _MapScreenState extends State { LatLng? _currentLocation; LatLng? _lastTrainLocation; LatLng? _userLocation; - double _currentZoom = 12.0; + double _currentZoom = 14.0; double _currentRotation = 0.0; bool _isMapInitialized = false; @@ -51,10 +51,19 @@ class _MapScreenState extends State { _loadSettings().then((_) { _loadTrainRecords().then((_) { _startLocationUpdates(); + if (!_isMapInitialized && (_currentLocation != null || _lastTrainLocation != null || _userLocation != null)) { + _initializeMapPosition(); + } }); }); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _loadSettings(); + } + Future _checkDatabaseSettings() async { try { final dbInfo = await DatabaseService.instance.getDatabaseInfo(); @@ -227,6 +236,8 @@ class _MapScreenState extends State { settings['mapCenterLon'] = center.longitude; } + settings['mapSettingsTimestamp'] = DateTime.now().millisecondsSinceEpoch; + await DatabaseService.instance.updateSettings(settings); } catch (e) {} } @@ -734,11 +745,31 @@ class _MapScreenState extends State { ); } + final bool isDefaultLocation = _currentLocation == null && + _lastTrainLocation == null && + _userLocation == null; + return Scaffold( backgroundColor: const Color(0xFF121212), body: Stack( children: [ - FlutterMap( + if (isDefaultLocation) + const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Color(0xFF007ACC)), + ), + SizedBox(height: 16), + Text( + '正在加载地图位置...', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ], + ), + ) + else FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: _currentLocation ?? @@ -747,7 +778,7 @@ class _MapScreenState extends State { const LatLng(39.9042, 116.4074), initialZoom: _currentZoom, initialRotation: _currentRotation, - minZoom: 4.0, + minZoom: 8.0, maxZoom: 18.0, onPositionChanged: (MapCamera camera, bool hasGesture) { setState(() { diff --git a/lib/screens/map_webview_screen.dart b/lib/screens/map_webview_screen.dart new file mode 100644 index 0000000..45c6778 --- /dev/null +++ b/lib/screens/map_webview_screen.dart @@ -0,0 +1,1162 @@ +import 'dart:convert'; +import 'dart:math' as math; +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:geolocator/geolocator.dart'; +import 'dart:async'; +import '../models/train_record.dart'; +import 'package:lbjconsole/services/database_service.dart'; +import 'package:latlong2/latlong.dart'; + +class MapWebViewScreen extends StatefulWidget { + const MapWebViewScreen({super.key}); + + @override + State createState() => MapWebViewScreenState(); +} + +class MapWebViewScreenState extends State + with WidgetsBindingObserver { + late final WebViewController _controller; + Position? _currentPosition; + List _trainRecords = []; + bool _isRailwayLayerVisible = true; + bool _isLoading = true; + String _timeFilter = 'unlimited'; + Timer? _refreshTimer; + Timer? _locationUpdateTimer; + + bool _isMapInitialized = false; + bool _isLocationPermissionGranted = false; + double _currentZoom = 14.0; + double _currentRotation = 0.0; + LatLng? _currentLocation; + LatLng? _lastTrainLocation; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _initializeWebView(); + _initializeServices(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _reloadSettingsIfNeeded(); + } + } + + Future _initializeServices() async { + try { + await _loadSettings(); + await _loadTrainRecordsFromDatabase(); + _initializeLocation(); + _startAutoRefresh(); + } catch (e) {} + } + + void _initializeWebView() { + _controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0xFF121212)) + ..addJavaScriptChannel( + 'showTrainDetails', + onMessageReceived: (JavaScriptMessage message) { + try { + final trainData = jsonDecode(message.message); + _showTrainDetailsDialog(trainData); + } catch (e) {} + }, + ) + ..addJavaScriptChannel( + 'onMapStateChanged', + onMessageReceived: (JavaScriptMessage message) { + try { + final mapState = jsonDecode(message.message); + _onMapStateChanged(mapState); + } catch (e) {} + }, + ) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) {}, + onPageStarted: (String url) { + setState(() { + _isLoading = true; + }); + }, + onPageFinished: (String url) { + setState(() { + _isLoading = false; + }); + + if (mounted) { + _initializeMap(); + _initializeLocation(); + } + }, + onWebResourceError: (WebResourceError error) { + setState(() { + _isLoading = false; + }); + }, + ), + ) + ..loadFlutterAsset('assets/mapbox_map.html') + .then((_) {}) + .catchError((error) {}); + } + + Future _initializeLocation() async { + try { + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('请开启定位服务')), + ); + } + return; + } + + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + + if (permission == LocationPermission.whileInUse || + permission == LocationPermission.always) { + Position position = await Geolocator.getCurrentPosition( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.best, + timeLimit: Duration(seconds: 15), + ), + ); + + setState(() { + _currentPosition = position; + _isLocationPermissionGranted = true; + }); + + _updateUserLocation(); + + _startLocationUpdates(); + } else if (permission == LocationPermission.deniedForever) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('定位权限被永久拒绝,请在设置中开启')), + ); + } + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('定位权限被拒绝,请在设置中开启')), + ); + } + } + } catch (e) {} + } + + void _startLocationUpdates() { + _locationUpdateTimer = Timer.periodic(const Duration(seconds: 30), (timer) { + if (_isLocationPermissionGranted && mounted) { + _updateCurrentLocation(); + } else {} + }); + } + + Future _updateCurrentLocation() async { + try { + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + return; + } + + Position position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.best, + forceAndroidLocationManager: true, + ); + + setState(() { + _currentPosition = position; + }); + + _updateUserLocation(); + } catch (e) { + if (e.toString().contains('PERMISSION_DENIED') || + e.toString().contains('permission')) { + _checkAndRequestPermissions(); + } + } + } + + Future _checkAndRequestPermissions() async { + try { + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + + if (permission == LocationPermission.whileInUse || + permission == LocationPermission.always) { + setState(() { + _isLocationPermissionGranted = true; + }); + } else { + setState(() { + _isLocationPermissionGranted = false; + }); + } + } catch (e) {} + } + + Future _loadTrainRecordsFromDatabase() async { + try { + final isConnected = await DatabaseService.instance.isDatabaseConnected(); + if (!isConnected) {} + + List records; + + if (_timeFilter == 'unlimited') { + records = await DatabaseService.instance.getAllRecords(); + } else { + final duration = _getTimeFilterDuration(_timeFilter); + if (duration != null && duration != Duration.zero) { + records = await DatabaseService.instance + .getRecordsWithinReceivedTimeRange(duration); + } else { + records = await DatabaseService.instance.getAllRecords(); + } + } + + if (records.isNotEmpty) { + for (int i = 0; i < math.min(3, records.length); i++) { + final record = records[i]; + final coords = record.getCoordinates(); + } + } + + setState(() { + _trainRecords = records; + _isLoading = false; + + if (_trainRecords.isNotEmpty) { + final validRecords = [ + ..._getValidRecords(), + ..._getValidDmsRecords() + ]; + if (validRecords.isNotEmpty) { + final lastRecord = validRecords.first; + LatLng? position; + + final dmsPosition = _parseDmsCoordinate(lastRecord.positionInfo); + if (dmsPosition != null) { + position = dmsPosition; + } else { + final coords = lastRecord.getCoordinates(); + if (coords['lat'] != 0.0 && coords['lng'] != 0.0) { + position = LatLng(coords['lat']!, coords['lng']!); + } + } + + if (position != null) { + _lastTrainLocation = position; + } else {} + } else {} + } else {} + + if (mounted) { + _initializeMapPosition(); + } + }); + + Future.delayed(const Duration(milliseconds: 3000), () { + if (mounted) { + _updateTrainMarkers(); + } + }); + + Future.delayed(const Duration(milliseconds: 5000), () { + if (mounted) { + _updateTrainMarkers(); + } + }); + } catch (e, stackTrace) { + setState(() { + _isLoading = false; + }); + } + } + + Duration? _getTimeFilterDuration(String filter) { + switch (filter) { + case '1hour': + return const Duration(hours: 1); + case '6hours': + return const Duration(hours: 6); + case '12hours': + return const Duration(hours: 12); + case '24hours': + return const Duration(hours: 24); + case '7days': + return const Duration(days: 7); + case '30days': + return const Duration(days: 30); + default: + return null; + } + } + + void _startAutoRefresh() { + _refreshTimer = Timer.periodic(const Duration(seconds: 10), (timer) { + if (mounted) { + _loadTrainRecordsFromDatabase(); + _reloadSettingsIfNeeded(); + } + }); + } + + void _reloadSettingsIfNeeded() async { + try { + final settings = await DatabaseService.instance.getAllSettings(); + if (settings != null) { + final newTimeFilter = + settings['mapTimeFilter'] as String? ?? 'unlimited'; + if (newTimeFilter != _timeFilter) { + if (mounted) { + setState(() { + _timeFilter = newTimeFilter; + }); + } + _loadTrainRecordsFromDatabase(); + } + } + } catch (e) {} + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _reloadSettingsIfNeeded(); + } + + void _loadTrainRecords() async { + setState(() => _isLoading = true); + try { + await _loadTrainRecordsFromDatabase(); + } catch (e) { + setState(() => _isLoading = false); + } + } + + void _initializeMapPosition() { + if (_isMapInitialized) return; + + LatLng? targetLocation; + + if (_lastTrainLocation != null) { + targetLocation = _lastTrainLocation; + } else if (_currentPosition != null) { + targetLocation = + LatLng(_currentPosition!.latitude, _currentPosition!.longitude); + } + + if (targetLocation != null) { + _centerMap(targetLocation, + zoom: _currentZoom, rotation: _currentRotation); + _isMapInitialized = true; + } else { + _isMapInitialized = true; + } + } + + void _centerMap(LatLng location, {double? zoom, double? rotation}) { + final targetZoom = zoom ?? _currentZoom; + final targetRotation = rotation ?? _currentRotation; + + _controller.runJavaScript(''' + (function() { + if (window.MapInterface) { + try { + window.MapInterface.setCenter(${location.latitude}, ${location.longitude}, $targetZoom, $targetRotation); + } catch (error) { + } + } + })(); + ''').then((result) {}).catchError((error) {}); + } + + Future _loadSettings() async { + try { + final settings = await DatabaseService.instance.getAllSettings(); + if (settings != null) { + setState(() { + _isRailwayLayerVisible = + (settings['mapRailwayLayerVisible'] as int?) == 1; + _currentZoom = (settings['mapZoomLevel'] as num?)?.toDouble() ?? 10.0; + _currentRotation = + (settings['mapRotation'] as num?)?.toDouble() ?? 0.0; + _timeFilter = settings['mapTimeFilter'] as String? ?? 'unlimited'; + + final lat = (settings['mapCenterLat'] as num?)?.toDouble(); + final lon = (settings['mapCenterLon'] as num?)?.toDouble(); + + if (lat != null && lon != null && lat != 0.0 && lon != 0.0) { + _currentLocation = LatLng(lat, lon); + } + }); + } + } catch (e) {} + } + + Future _saveSettings() async { + try { + final settings = { + 'mapRailwayLayerVisible': _isRailwayLayerVisible ? 1 : 0, + 'mapZoomLevel': _currentZoom, + 'mapRotation': _currentRotation, + 'mapTimeFilter': _timeFilter, + 'mapSettingsTimestamp': DateTime.now().millisecondsSinceEpoch, + }; + + if (_currentLocation != null) { + settings['mapCenterLat'] = _currentLocation!.latitude; + settings['mapCenterLon'] = _currentLocation!.longitude; + } + + await DatabaseService.instance.updateSettings(settings); + } catch (e) {} + } + + Future _initializeMap() async { + try { + LatLng? targetLocation; + double targetZoom = 14.0; + double targetRotation = _currentRotation; + + if (_lastTrainLocation != null) { + targetLocation = _lastTrainLocation; + targetZoom = 14.0; + targetRotation = 0.0; + } else if (_currentPosition != null) { + targetLocation = + LatLng(_currentPosition!.latitude, _currentPosition!.longitude); + targetZoom = 16.0; + targetRotation = 0.0; + } + + if (targetLocation != null) { + _centerMap(targetLocation, zoom: targetZoom, rotation: targetRotation); + + try { + await _controller.runJavaScript(''' + (function() { + if (window.map && window.map.getContainer) { + window.map.getContainer().style.opacity = '1'; + return true; + } else { + return false; + } + })(); + '''); + } catch (e) { + print('显示地图失败: $e'); + Future.delayed(const Duration(milliseconds: 500), () { + _controller.runJavaScript(''' + (function() { + if (window.map && window.map.getContainer) { + window.map.getContainer().style.opacity = '1'; + console.log('延迟显示地图成功'); + } + })(); + '''); + }); + } + } + + setState(() { + _isMapInitialized = true; + }); + } catch (e, stackTrace) {} + + Future.delayed(const Duration(milliseconds: 1000), () { + if (mounted && _isMapInitialized) { + _updateTrainMarkers(); + + _controller.runJavaScript(''' + (function() { + if (window.MapInterface) { + try { + window.MapInterface.setRailwayVisible($_isRailwayLayerVisible); + console.log('初始化铁路图层状态: $_isRailwayLayerVisible'); + } catch (error) { + console.error('初始化铁路图层失败:', error); + } + } + })(); + '''); + } + }); + + Future.delayed(const Duration(milliseconds: 3000), () { + if (mounted && _isMapInitialized) { + _updateTrainMarkers(); + } + }); + + Future.delayed(const Duration(seconds: 5), () { + if (mounted && _isLoading) { + setState(() { + _isLoading = false; + }); + } + }); + } + + LatLng? _parseDmsCoordinate(String? positionInfo) { + if (positionInfo == null || + positionInfo.isEmpty || + positionInfo == '') { + return null; + } + + try { + final parts = positionInfo.trim().split(' '); + if (parts.length >= 2) { + final latStr = parts[0]; + final lngStr = parts[1]; + + final lat = _parseDmsString(latStr); + final lng = _parseDmsString(lngStr); + + if (lat != null && + lng != null && + (lat.abs() > 0.001 || lng.abs() > 0.001)) { + return LatLng(lat, lng); + } + } + } catch (e) {} + + return null; + } + + double? _parseDmsString(String dmsStr) { + try { + final degreeIndex = dmsStr.indexOf('°'); + if (degreeIndex == -1) return null; + + final degrees = double.tryParse(dmsStr.substring(0, degreeIndex)); + if (degrees == null) return null; + + final minuteIndex = dmsStr.indexOf('′'); + if (minuteIndex == -1) return degrees; + + final minutes = + double.tryParse(dmsStr.substring(degreeIndex + 1, minuteIndex)); + if (minutes == null) return degrees; + + return degrees + (minutes / 60.0); + } catch (e) { + return null; + } + } + + List _getValidRecords() { + return _trainRecords.where((record) { + final coords = record.getCoordinates(); + return coords['lat'] != 0.0 && coords['lng'] != 0.0; + }).toList(); + } + + List _getValidDmsRecords() { + return _trainRecords.where((record) { + return _parseDmsCoordinate(record.positionInfo) != null; + }).toList(); + } + + void _updateTrainMarkers() { + if (_trainRecords.isEmpty) { + return; + } + + if (!_isMapInitialized) { + Future.delayed(const Duration(seconds: 2), () { + if (mounted && _trainRecords.isNotEmpty) { + _updateTrainMarkers(); + } + }); + return; + } + + final validRecords = [..._getValidRecords(), ..._getValidDmsRecords()]; + + final trainData = validRecords + .map((record) { + LatLng? position; + + final dmsPosition = _parseDmsCoordinate(record.positionInfo); + if (dmsPosition != null) { + position = dmsPosition; + } else { + final coords = record.getCoordinates(); + if (coords['lat'] != 0.0 && coords['lng'] != 0.0) { + position = LatLng(coords['lat']!, coords['lng']!); + } + } + + if (position != null) { + return { + 'lat': position.latitude, + 'lng': position.longitude, + 'fullTrainNumber': record.fullTrainNumber, + 'time': record.time, + 'speed': record.speed, + 'position': record.position, + 'route': record.route, + 'locoType': record.locoType, + 'loco': record.loco, + 'timestamp': record.timestamp, + }; + } + return null; + }) + .where((data) => data != null) + .toList(); + + trainData.sort((a, b) { + final aTimestamp = a?['timestamp']; + final bTimestamp = b?['timestamp']; + + if (aTimestamp == null || bTimestamp == null) return 0; + + if (aTimestamp is DateTime && bTimestamp is DateTime) { + return bTimestamp.compareTo(aTimestamp); + } + + if (aTimestamp is int && bTimestamp is int) { + return bTimestamp.compareTo(aTimestamp); + } + + if (aTimestamp is String && bTimestamp is String) { + return bTimestamp.compareTo(aTimestamp); + } + + return 0; + }); + + final jsonTrainData = trainData + .map((train) { + if (train == null) return null; + + final trainCopy = Map.from(train); + + if (trainCopy['timestamp'] is DateTime) { + trainCopy['timestamp'] = + (trainCopy['timestamp'] as DateTime).millisecondsSinceEpoch; + } + + return trainCopy; + }) + .where((train) => train != null) + .toList(); + + if (trainData.isEmpty) { + return; + } + + if (trainData.isNotEmpty) { + final latestTrain = trainData.first; + if (latestTrain != null) { + _lastTrainLocation = LatLng((latestTrain['lat'] as num).toDouble(), + (latestTrain['lng'] as num).toDouble()); + } + } + + _controller.runJavaScript(''' + (function() { + try { + if (window.MapInterface && window.MapInterface.updateTrainMarkers) { + window.MapInterface.updateTrainMarkers(${jsonEncode(jsonTrainData)}); + + if (!window.trainMarkersUpdated && ${trainData.length} > 0) { + window.trainMarkersUpdated = true; + setTimeout(function() { + if (window.MapInterface && window.MapInterface.fitBounds) { + window.MapInterface.fitBounds(); + } + }, 1000); + } + } else { + console.error('MapInterface 未定义或updateTrainMarkers方法不存在'); + } + } catch (error) { + console.error('更新列车标记失败:', error); + } + })(); + ''').then((result) {}).catchError((error) {}); + } + + void _updateUserLocation() { + if (_currentPosition != null) { + _controller.runJavaScript(''' + (function() { + try { + if (window.MapInterface && window.MapInterface.setUserLocation) { + window.MapInterface.setUserLocation(${_currentPosition!.latitude}, ${_currentPosition!.longitude}); + + if (!window.userLocationUpdated) { + window.userLocationUpdated = true; + } + } else { + console.error('MapInterface 未定义或setUserLocation方法不存在'); + } + } catch (error) { + console.error('更新用户位置失败:', error); + } + })(); + ''').then((_) {}).catchError((error) {}); + } else {} + } + + void _toggleRailwayLayer() { + setState(() { + _isRailwayLayerVisible = !_isRailwayLayerVisible; + }); + + _controller.runJavaScript(''' + (function() { + if (window.MapInterface) { + try { + window.MapInterface.setRailwayVisible($_isRailwayLayerVisible); + console.log('铁路图层切换成功: $_isRailwayLayerVisible'); + } catch (error) { + console.error('切换铁路图层失败:', error); + } + } else { + console.error('MapInterface 未定义'); + } + })(); + ''').then((result) {}).catchError((error) {}); + } + + void _showTrainDetailsDialog(Map train) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: const Color(0xFF1E1E1E), + title: Text( + train['trainNumber'] ?? '未知车次', + style: const TextStyle(color: Colors.white), + ), + content: SingleChildScrollView( + child: ListBody( + children: [ + _buildDetailRow('车次', train['trainNumber'] ?? '未知'), + _buildDetailRow('类型', train['trainType'] ?? '未知'), + _buildDetailRow('速度', '${train['speed'] ?? 0} km/h'), + _buildDetailRow('方向', '${train['direction'] ?? 0}°'), + _buildDetailRow( + '经度', train['longitude']?.toStringAsFixed(6) ?? '未知'), + _buildDetailRow( + '纬度', train['latitude']?.toStringAsFixed(6) ?? '未知'), + _buildDetailRow('时间', _getDisplayTime(train['timestamp'])), + _buildDetailRow('日期', _getDisplayDate(train['timestamp'])), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('关闭', style: TextStyle(color: Colors.white)), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + _controller.runJavaScript(''' + if (window.MapInterface) { + window.MapInterface.setCenter(${train['latitude']}, ${train['longitude']}, 16); + } + '''); + }, + child: + const Text('定位', style: TextStyle(color: Color(0xFF007ACC))), + ), + ], + ); + }, + ); + } + + Widget _buildDetailRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 80, + child: Text( + '$label:', + style: const TextStyle( + color: Colors.grey, + fontSize: 14, + ), + ), + ), + Expanded( + child: Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + ), + ], + ), + ); + } + + String _getDisplayTime(int? timestamp) { + if (timestamp == null) return '未知'; + final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp); + return '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}:${dateTime.second.toString().padLeft(2, '0')}'; + } + + String _getDisplayDate(int? timestamp) { + if (timestamp == null) return '未知'; + final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp); + return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')}'; + } + + void _showTimeFilterDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text('时间筛选'), + children: [ + SimpleDialogOption( + onPressed: () { + setState(() { + _timeFilter = 'unlimited'; + }); + Navigator.of(context).pop(); + _saveSettingsAndReload(); + }, + child: const Text('无限制'), + ), + SimpleDialogOption( + onPressed: () { + setState(() { + _timeFilter = '1hour'; + }); + Navigator.of(context).pop(); + _saveSettingsAndReload(); + }, + child: const Text('最近1小时'), + ), + SimpleDialogOption( + onPressed: () { + setState(() { + _timeFilter = '6hours'; + }); + Navigator.of(context).pop(); + _saveSettingsAndReload(); + }, + child: const Text('最近6小时'), + ), + SimpleDialogOption( + onPressed: () { + setState(() { + _timeFilter = '12hours'; + }); + Navigator.of(context).pop(); + _saveSettingsAndReload(); + }, + child: const Text('最近12小时'), + ), + SimpleDialogOption( + onPressed: () { + setState(() { + _timeFilter = '24hours'; + }); + Navigator.of(context).pop(); + _saveSettingsAndReload(); + }, + child: const Text('最近24小时'), + ), + SimpleDialogOption( + onPressed: () { + setState(() { + _timeFilter = '7days'; + }); + Navigator.of(context).pop(); + _saveSettingsAndReload(); + }, + child: const Text('最近7天'), + ), + SimpleDialogOption( + onPressed: () { + setState(() { + _timeFilter = '30days'; + }); + Navigator.of(context).pop(); + _saveSettingsAndReload(); + }, + child: const Text('最近30天'), + ), + ], + ); + }, + ); + } + + void _saveSettingsAndReload() async { + try { + await _saveSettings(); + _loadTrainRecordsFromDatabase(); + } catch (e) {} + } + + String _getTimeFilterLabel() { + switch (_timeFilter) { + case '1hour': + return '1小时'; + case '6hours': + return '6小时'; + case '12hours': + return '12小时'; + case '24hours': + return '24小时'; + case '7days': + return '7天'; + case '30days': + return '30天'; + default: + return '无限制'; + } + } + + void _onMapStateChanged(Map mapState) { + try { + final lat = mapState['lat'] as double; + final lng = mapState['lng'] as double; + final zoom = mapState['zoom'] as double; + final bearing = mapState['bearing'] as double; + + setState(() { + _currentLocation = LatLng(lat, lng); + _currentZoom = zoom; + _currentRotation = bearing; + }); + + Future.microtask(() { + if (mounted) { + _saveSettings(); + } + }); + } catch (e) {} + } + + void _centerToUserLocation() async { + if (_currentPosition != null) { + final userLocation = + LatLng(_currentPosition!.latitude, _currentPosition!.longitude); + _centerMap(userLocation, zoom: 15.0); + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('正在获取当前位置...')), + ); + } + + try { + await _forceUpdateLocation(); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('获取位置失败: $e')), + ); + } + } + } + } + + void _centerToLastTrain() { + if (_trainRecords.isNotEmpty && _lastTrainLocation != null) { + _centerMap(_lastTrainLocation!, zoom: 15.0); + } + } + + Future _forceUpdateLocation() async { + try { + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('请开启定位服务')), + ); + } + return; + } + + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + + if (permission == LocationPermission.deniedForever) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('定位权限被永久拒绝,请在设置中开启')), + ); + } + return; + } + + if (permission != LocationPermission.whileInUse && + permission != LocationPermission.always) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('定位权限不足')), + ); + } + return; + } + + Position position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.best, + forceAndroidLocationManager: true, + timeLimit: const Duration(seconds: 20), + ); + + final newLocation = LatLng(position.latitude, position.longitude); + setState(() { + _currentPosition = position; + _isLocationPermissionGranted = true; + }); + + _centerMap(newLocation, zoom: 16.0); + + _updateUserLocation(); + } catch (e) {} + } + + void _refreshMap() { + _loadTrainRecordsFromDatabase(); + + if (_isLocationPermissionGranted) { + _forceUpdateLocation(); + } else { + _updateUserLocation(); + } + + _controller.runJavaScript(''' + if (window.MapInterface) { + window.MapInterface.getTrainMarkersCount(); + } + '''); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _saveSettings(); + + _refreshTimer?.cancel(); + _locationUpdateTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF121212), + body: Stack( + children: [ + WebViewWidget(controller: _controller), + if (_isLoading) + Container( + color: const Color(0xFF121212).withValues(alpha: 0.8), + child: const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Color(0xFF007ACC)), + ), + ), + ), + Positioned( + right: 16, + bottom: 80, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FloatingActionButton( + heroTag: "time_filter", + mini: true, + backgroundColor: const Color(0xFF1E1E1E), + onPressed: () { + _reloadSettingsIfNeeded(); + _showTimeFilterDialog(); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.filter_list, + color: Colors.white, size: 18), + Text( + _getTimeFilterLabel(), + style: + const TextStyle(color: Colors.white, fontSize: 8), + ), + ], + ), + ), + const SizedBox(height: 8), + FloatingActionButton( + heroTag: "refresh", + mini: true, + backgroundColor: const Color(0xFF1E1E1E), + onPressed: _refreshMap, + child: const Icon(Icons.refresh, color: Colors.white), + ), + const SizedBox(height: 8), + FloatingActionButton( + heroTag: "layers", + mini: true, + backgroundColor: const Color(0xFF1E1E1E), + onPressed: _toggleRailwayLayer, + child: Icon( + _isRailwayLayerVisible + ? Icons.layers + : Icons.layers_outlined, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + FloatingActionButton( + heroTag: "location", + mini: true, + backgroundColor: const Color(0xFF1E1E1E), + onPressed: _centerToUserLocation, + child: const Icon(Icons.my_location, color: Colors.white), + ), + const SizedBox(height: 8), + FloatingActionButton( + heroTag: "last_train", + mini: true, + backgroundColor: const Color(0xFF1E1E1E), + onPressed: _centerToLastTrain, + child: const Icon(Icons.train, color: Colors.white), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index e542207..5645f48 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -17,7 +17,9 @@ import 'package:share_plus/share_plus.dart'; import 'package:cross_file/cross_file.dart'; class SettingsScreen extends StatefulWidget { - const SettingsScreen({super.key}); + final VoidCallback? onSettingsChanged; + + const SettingsScreen({super.key, this.onSettingsChanged}); @override State createState() => _SettingsScreenState(); @@ -33,8 +35,10 @@ class _SettingsScreenState extends State { int _recordCount = 0; bool _mergeRecordsEnabled = false; bool _hideTimeOnlyRecords = false; + bool _hideUngroupableRecords = false; GroupBy _groupBy = GroupBy.trainAndLoco; TimeWindow _timeWindow = TimeWindow.unlimited; + String _mapType = 'map'; @override void initState() { @@ -63,8 +67,10 @@ class _SettingsScreenState extends State { _notificationsEnabled = (settingsMap['notificationEnabled'] ?? 1) == 1; _mergeRecordsEnabled = settings.enabled; _hideTimeOnlyRecords = (settingsMap['hideTimeOnlyRecords'] ?? 0) == 1; + _hideUngroupableRecords = settings.hideUngroupableRecords; _groupBy = settings.groupBy; _timeWindow = settings.timeWindow; + _mapType = settingsMap['mapType']?.toString() ?? 'webview'; }); } } @@ -85,9 +91,12 @@ class _SettingsScreenState extends State { 'notificationEnabled': _notificationsEnabled ? 1 : 0, 'mergeRecordsEnabled': _mergeRecordsEnabled ? 1 : 0, 'hideTimeOnlyRecords': _hideTimeOnlyRecords ? 1 : 0, + 'hideUngroupableRecords': _hideUngroupableRecords ? 1 : 0, 'groupBy': _groupBy.name, 'timeWindow': _timeWindow.name, + 'mapType': _mapType, }); + widget.onSettingsChanged?.call(); } @override @@ -240,6 +249,43 @@ class _SettingsScreenState extends State { ], ), const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('地图显示方式', style: AppTheme.bodyLarge), + Text('选择地图组件类型', style: AppTheme.caption), + ], + ), + DropdownButton( + value: _mapType, + items: [ + DropdownMenuItem( + value: 'webview', + child: Text('矢量铁路地图', style: AppTheme.bodyMedium), + ), + DropdownMenuItem( + value: 'map', + child: Text('栅格铁路地图', style: AppTheme.bodyMedium), + ), + ], + onChanged: (value) { + if (value != null) { + setState(() { + _mapType = value; + }); + _saveSettings(); + } + }, + dropdownColor: AppTheme.secondaryBlack, + style: AppTheme.bodyMedium, + underline: Container(height: 0), + ), + ], + ), + const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -398,6 +444,29 @@ class _SettingsScreenState extends State { dropdownColor: AppTheme.secondaryBlack, style: AppTheme.bodyMedium, ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('隐藏不可分组记录', style: AppTheme.bodyLarge), + Text('不显示无法分组的记录', style: AppTheme.caption), + ], + ), + Switch( + value: _hideUngroupableRecords, + onChanged: (value) { + setState(() { + _hideUngroupableRecords = value; + }); + _saveSettings(); + }, + activeColor: Theme.of(context).colorScheme.primary, + ), + ], + ), ], ), ), @@ -421,7 +490,8 @@ class _SettingsScreenState extends State { children: [ Row( children: [ - Icon(Icons.storage, color: Theme.of(context).colorScheme.primary), + Icon(Icons.storage, + color: Theme.of(context).colorScheme.primary), const SizedBox(width: 12), Text('数据管理', style: AppTheme.titleMedium), ], diff --git a/lib/services/database_service.dart b/lib/services/database_service.dart index d9a3a16..482212e 100644 --- a/lib/services/database_service.dart +++ b/lib/services/database_service.dart @@ -13,7 +13,7 @@ class DatabaseService { DatabaseService._internal(); static const String _databaseName = 'train_database'; - static const _databaseVersion = 4; + static const _databaseVersion = 6; static const String trainRecordsTable = 'train_records'; static const String appSettingsTable = 'app_settings'; @@ -21,21 +21,47 @@ class DatabaseService { Database? _database; Future get database async { - if (_database != null) return _database!; - _database = await _initDatabase(); - return _database!; + try { + if (_database != null) { + return _database!; + } + _database = await _initDatabase(); + return _database!; + } catch (e, stackTrace) { + rethrow; + } + } + + Future isDatabaseConnected() async { + try { + if (_database == null) { + return false; + } + + final db = await database; + final result = await db.rawQuery('SELECT 1'); + return true; + } catch (e) { + return false; + } } Future _initDatabase() async { - final directory = await getApplicationDocumentsDirectory(); - final path = join(directory.path, _databaseName); + try { + final directory = await getApplicationDocumentsDirectory(); + final path = join(directory.path, _databaseName); - return await openDatabase( - path, - version: _databaseVersion, - onCreate: _onCreate, - onUpgrade: _onUpgrade, - ); + final db = await openDatabase( + path, + version: _databaseVersion, + onCreate: _onCreate, + onUpgrade: _onUpgrade, + ); + + return db; + } catch (e, stackTrace) { + rethrow; + } } Future _onUpgrade(Database db, int oldVersion, int newVersion) async { @@ -51,8 +77,15 @@ class DatabaseService { try { await db.execute( 'ALTER TABLE $appSettingsTable ADD COLUMN mapTimeFilter TEXT NOT NULL DEFAULT "unlimited"'); - } catch (e) { - } + } catch (e) {} + } + if (oldVersion < 5) { + await db.execute( + 'ALTER TABLE $appSettingsTable ADD COLUMN mapType TEXT NOT NULL DEFAULT "webview"'); + } + if (oldVersion < 6) { + await db.execute( + 'ALTER TABLE $appSettingsTable ADD COLUMN hideUngroupableRecords INTEGER NOT NULL DEFAULT 0'); } } @@ -92,6 +125,7 @@ class DatabaseService { mapZoomLevel REAL NOT NULL DEFAULT 10.0, mapRailwayLayerVisible INTEGER NOT NULL DEFAULT 1, mapRotation REAL NOT NULL DEFAULT 0.0, + mapType TEXT NOT NULL DEFAULT 'webview', specifiedDeviceAddress TEXT, searchOrderList TEXT NOT NULL DEFAULT '', autoConnectEnabled INTEGER NOT NULL DEFAULT 1, @@ -101,7 +135,8 @@ class DatabaseService { hideTimeOnlyRecords INTEGER NOT NULL DEFAULT 0, groupBy TEXT NOT NULL DEFAULT 'trainAndLoco', timeWindow TEXT NOT NULL DEFAULT 'unlimited', - mapTimeFilter TEXT NOT NULL DEFAULT 'unlimited' + mapTimeFilter TEXT NOT NULL DEFAULT 'unlimited', + hideUngroupableRecords INTEGER NOT NULL DEFAULT 0 ) '''); @@ -118,6 +153,7 @@ class DatabaseService { 'mapZoomLevel': 10.0, 'mapRailwayLayerVisible': 1, 'mapRotation': 0.0, + 'mapType': 'webview', 'searchOrderList': '', 'autoConnectEnabled': 1, 'backgroundServiceEnabled': 0, @@ -127,6 +163,7 @@ class DatabaseService { 'groupBy': 'trainAndLoco', 'timeWindow': 'unlimited', 'mapTimeFilter': 'unlimited', + 'hideUngroupableRecords': 0, }); } @@ -140,12 +177,18 @@ class DatabaseService { } Future> getAllRecords() async { - final db = await database; - final result = await db.query( - trainRecordsTable, - orderBy: 'timestamp DESC', - ); - return result.map((json) => TrainRecord.fromDatabaseJson(json)).toList(); + try { + final db = await database; + final result = await db.query( + trainRecordsTable, + orderBy: 'timestamp DESC', + ); + final records = + result.map((json) => TrainRecord.fromDatabaseJson(json)).toList(); + return records; + } catch (e, stackTrace) { + rethrow; + } } Future> getRecordsWithinTimeRange(Duration duration) async { @@ -162,15 +205,23 @@ class DatabaseService { Future> getRecordsWithinReceivedTimeRange( Duration duration) async { - final db = await database; - final cutoffTime = DateTime.now().subtract(duration).millisecondsSinceEpoch; - final result = await db.query( - trainRecordsTable, - where: 'receivedTimestamp >= ?', - whereArgs: [cutoffTime], - orderBy: 'receivedTimestamp DESC', - ); - return result.map((json) => TrainRecord.fromDatabaseJson(json)).toList(); + try { + final db = await database; + final cutoffTime = + DateTime.now().subtract(duration).millisecondsSinceEpoch; + + final result = await db.query( + trainRecordsTable, + where: 'receivedTimestamp >= ?', + whereArgs: [cutoffTime], + orderBy: 'receivedTimestamp DESC', + ); + final records = + result.map((json) => TrainRecord.fromDatabaseJson(json)).toList(); + return records; + } catch (e, stackTrace) { + rethrow; + } } Future deleteRecord(String uniqueId) async { diff --git a/lib/services/merge_service.dart b/lib/services/merge_service.dart index a2264e6..2be92de 100644 --- a/lib/services/merge_service.dart +++ b/lib/services/merge_service.dart @@ -2,6 +2,37 @@ import 'package:lbjconsole/models/train_record.dart'; import 'package:lbjconsole/models/merged_record.dart'; class MergeService { + static bool isNeverGroupableRecord(TrainRecord record, GroupBy groupBy) { + final train = record.train.trim(); + final loco = record.loco.trim(); + + final hasValidTrain = + train.isNotEmpty && train != "" && !train.contains("-----"); + final hasValidLoco = loco.isNotEmpty && loco != ""; + + switch (groupBy) { + case GroupBy.trainOnly: + return !hasValidTrain; + + case GroupBy.locoOnly: + return !hasValidLoco; + + case GroupBy.trainAndLoco: + return !hasValidTrain || !hasValidLoco; + + case GroupBy.trainOrLoco: + return !hasValidTrain && !hasValidLoco; + } + } + + static List filterUngroupableRecords( + List records, GroupBy groupBy, bool hideUngroupable) { + if (!hideUngroupable) return records; + return records + .where((record) => !isNeverGroupableRecord(record, groupBy)) + .toList(); + } + static String? _generateGroupKey(TrainRecord record, GroupBy groupBy) { final train = record.train.trim(); final loco = record.loco.trim(); @@ -36,15 +67,19 @@ class MergeService { return allRecords; } - allRecords + final filteredRecords = filterUngroupableRecords( + allRecords, settings.groupBy, settings.hideUngroupableRecords); + + filteredRecords .sort((a, b) => b.receivedTimestamp.compareTo(a.receivedTimestamp)); if (settings.groupBy == GroupBy.trainOrLoco) { - return _groupByTrainOrLocoWithTimeWindow(allRecords, settings.timeWindow); + return _groupByTrainOrLocoWithTimeWindow( + filteredRecords, settings.timeWindow); } final groupedRecords = >{}; - for (final record in allRecords) { + for (final record in filteredRecords) { final key = _generateGroupKey(record, settings.groupBy); if (key != null) { groupedRecords.putIfAbsent(key, () => []).add(record); @@ -79,8 +114,9 @@ class MergeService { final reusedRecords = _reuseDiscardedRecords( discardedRecords, mergedRecordIds, settings.groupBy); - final singleRecords = - allRecords.where((r) => !mergedRecordIds.contains(r.uniqueId)).toList(); + final singleRecords = filteredRecords + .where((r) => !mergedRecordIds.contains(r.uniqueId)) + .toList(); final List mixedList = [...mergedRecords, ...singleRecords]; mixedList.sort((a, b) { @@ -219,7 +255,6 @@ class MergeService { latestRecord: processedGroup.first, )); } else { - // 处理被丢弃的记录 for (final record in group) { if (!processedGroup.contains(record)) { singleRecords.add(record); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index def0774..24e5887 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -15,6 +15,7 @@ import share_plus import shared_preferences_foundation import sqflite_darwin import url_launcher_macos +import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) @@ -27,4 +28,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index db97a25..175eb98 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -233,6 +233,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.7.11" + executor_lib: + dependency: transitive + description: + name: executor_lib + sha256: "95ddf2957d9942d9702855b38dd49677f0ee6a8b77d7b16c0e509c7669d17386" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" fake_async: dependency: transitive description: @@ -656,6 +664,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" + maplibre_gl: + dependency: "direct main" + description: + name: maplibre_gl + sha256: "5c7b1008396b2a321bada7d986ed60f9423406fbc7bd16f7ce91b385dfa054cd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.22.0" + maplibre_gl_platform_interface: + dependency: transitive + description: + name: maplibre_gl_platform_interface + sha256: "08ee0a2d0853ea945a0ab619d52c0c714f43144145cd67478fc6880b52f37509" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.22.0" + maplibre_gl_web: + dependency: transitive + description: + name: maplibre_gl_web + sha256: "2b13d4b1955a9a54e38a718f2324e56e4983c080fc6de316f6f4b5458baacb58" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.22.0" matcher: dependency: transitive description: @@ -896,6 +928,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" provider: dependency: "direct main" description: @@ -1261,6 +1301,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.5.1" + vector_map_tiles: + dependency: "direct main" + description: + name: vector_map_tiles + sha256: "4dc9243195c1a49c7be82cc1caed0d300242bb94381752af5f6868d9d1404e25" + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.0.0" vector_math: dependency: transitive description: @@ -1269,6 +1317,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" + vector_tile: + dependency: transitive + description: + name: vector_tile + sha256: "7ae290246e3a8734422672dbe791d3f7b8ab631734489fc6d405f1cc2080e38c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + vector_tile_renderer: + dependency: transitive + description: + name: vector_tile_renderer + sha256: "89746f1108eccbc0b6f33fbbef3fcf394cda3733fc0d5064ea03d53a459b56d3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.1" vm_service: dependency: transitive description: @@ -1309,6 +1373,38 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.3" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.13.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "3c4eb4fcc252b40c2b5ce7be20d0481428b70f3ff589b0a8b8aaeb64c6bed701" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.10.2" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.14.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: fea63576b3b7e02b2df8b78ba92b48ed66caec2bb041e9a0b1cbd586d5d80bfd + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.23.1" win32: dependency: transitive description: @@ -1350,5 +1446,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0-0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 1c2da19..3134fd5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.4.0-flutter+40 +version: 0.5.0-flutter+50 environment: sdk: ^3.5.4 @@ -54,6 +54,9 @@ dependencies: msix: ^3.16.12 flutter_background_service: ^5.1.0 scrollview_observer: ^1.20.0 + vector_map_tiles: ^8.0.0 + maplibre_gl: ^0.22.0 + webview_flutter: ^4.8.0 dev_dependencies: flutter_test: @@ -84,6 +87,7 @@ flutter: - assets/loco_info.csv - assets/train_number_info.csv - assets/loco_type_info.csv + - assets/mapbox_map.html # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images