7 Commits

Author SHA1 Message Date
Nedifinita
6073ea615e fix: solve the problem that errors may occur during merging 2025-10-14 23:55:42 +08:00
Nedifinita
5533df92b5 feat: add train location tracking functionality 2025-10-12 21:42:01 +08:00
Nedifinita
24c6abd4f3 Merge branch 'flutter' of https://github.com/undef-i/LBJ_Console into flutter 2025-10-08 22:35:47 +08:00
Nedifinita
5b3960f7d6 fix: correct the scrolling status error when adding new records to the merged record group 2025-10-08 22:35:43 +08:00
undef-i
33e790957e Add .gitattributes file 2025-09-30 01:11:08 +08:00
undef-i
88e3636c3f Create .gitattributes 2025-09-30 01:10:34 +08:00
Nedifinita
cc2a495984 feat: improve map initialization process and communication 2025-09-29 20:17:05 +08:00
9 changed files with 1722 additions and 363 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
assets/* linguist-vendored

View File

@@ -3,15 +3,16 @@
LBJ Console 是一款应用程序,用于通过 BLE 从 [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) 设备接收并显示列车预警消息,功能包括: LBJ Console 是一款应用程序,用于通过 BLE 从 [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) 设备接收并显示列车预警消息,功能包括:
- 接收列车预警消息,支持可选的手机推送通知。 - 接收列车预警消息,支持可选的手机推送通知。
- 监控指定列车的轨迹,在地图上显示。
- 在地图上显示预警消息的 GPS 信息。 - 在地图上显示预警消息的 GPS 信息。
- 基于内置数据文件显示机车配属,机车类型和车次类型。 - 基于内置数据文件显示机车配属,机车类型和车次类型。
[android](https://github.com/undef-i/LBJ_Console/tree/android) 分支包含项目早期基于 Android 平台的实现代码,已实现基本功能,现已停止开发。 [android](https://github.com/undef-i/LBJ_Console/tree/android) 分支包含项目早期基于 Android 平台的实现代码,已实现基本功能,现已停止开发。
## 数据文件 ## 数据文件
LBJ Console 依赖以下数据文件,位于 `assets` 目录,用于支持机车配属和车次信息的展示: LBJ Console 依赖以下数据文件,位于 `assets` 目录,用于支持机车配属和车次信息的展示:
- `loco_info.csv`:包含机车配属信息,格式为 `机车型号,机车编号起始值,机车编号结束值,所属铁路局及机务段,备注` - `loco_info.csv`:包含机车配属信息,格式为 `机车型号,机车编号起始值,机车编号结束值,所属铁路局及机务段,备注`
- `loco_type_info.csv`:包含机车类型编码信息,格式为 `机车类型编码前缀,机车类型` - `loco_type_info.csv`:包含机车类型编码信息,格式为 `机车类型编码前缀,机车类型`
- `train_info.csv`:包含车次类型信息,格式为 `正则表达式,车次类型` - `train_info.csv`:包含车次类型信息,格式为 `正则表达式,车次类型`

View File

@@ -14848,8 +14848,8 @@
el.addEventListener("click", function (e) { el.addEventListener("click", function (e) {
e.stopPropagation(); e.stopPropagation();
showTrainDetails(lat, lng, recordData); showTrainDetails(lat, lng, recordData);
if (window.flutter_inappwebview) { if (window.showTrainDetails) {
window.flutter_inappwebview.callHandler("showTrainDetails", { window.showTrainDetails.postMessage(JSON.stringify({
id: recordData.id || Date.now(), id: recordData.id || Date.now(),
trainNumber: trainNumber || "未知车次", trainNumber: trainNumber || "未知车次",
trainType: recordData.trainType || "未知类型", trainType: recordData.trainType || "未知类型",
@@ -14858,7 +14858,7 @@
latitude: lat, latitude: lat,
longitude: lng, longitude: lng,
timestamp: Date.now(), timestamp: Date.now(),
}); }));
} }
}); });
const marker = new maplibregl.Marker({ const marker = new maplibregl.Marker({
@@ -14931,11 +14931,21 @@
.addTo(map); .addTo(map);
} }
function setCenter(lat, lng, zoom, bearing) { function setCenter(lat, lng, zoom, bearing) {
map.jumpTo({ if (!window.map) {
console.error("[JS] Map object not ready in setCenter");
return;
}
window.map.jumpTo({
center: [lng, lat], center: [lng, lat],
zoom: zoom !== undefined ? zoom : map.getZoom(), zoom: zoom !== undefined ? zoom : window.map.getZoom(),
bearing: bearing !== undefined ? bearing : map.getBearing(), bearing: bearing !== undefined ? bearing : window.map.getBearing(),
}); });
const mapContainer = window.map.getContainer();
if (mapContainer.style.opacity === "0") {
mapContainer.style.opacity = "1";
}
} }
function getMapState() { function getMapState() {
const center = map.getCenter(); const center = map.getCenter();
@@ -14948,17 +14958,19 @@
} }
function setupMapEventListeners() { function setupMapEventListeners() {
if (!map) return; if (!map) return;
map.on("moveend", function () { map.on("move", function () {
const center = map.getCenter(); const center = map.getCenter();
const zoom = map.getZoom(); const zoom = map.getZoom();
const bearing = map.getBearing(); const bearing = map.getBearing();
if (window.flutter_inappwebview) {
window.flutter_inappwebview.callHandler("onMapStateChanged", { if (window.onMapStateChanged) {
const mapState = JSON.stringify({
lat: center.lat, lat: center.lat,
lng: center.lng, lng: center.lng,
zoom: zoom, zoom: zoom,
bearing: bearing, bearing: bearing,
}); });
window.onMapStateChanged.postMessage(mapState);
} }
}); });
} }

View File

@@ -1,9 +1,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:isolate'; import 'dart:isolate';
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
@@ -33,6 +31,10 @@ class HistoryScreenState extends State<HistoryScreen> {
final List<Object> _displayItems = []; final List<Object> _displayItems = [];
bool _isLoading = true; bool _isLoading = true;
bool _isEditMode = false; bool _isEditMode = false;
int? _anchorIndex;
double? _anchorOffset;
double? _oldCardHeight;
double? _oldScrollOffset;
final Set<String> _selectedRecords = {}; final Set<String> _selectedRecords = {};
final Map<String, bool> _expandedStates = {}; final Map<String, bool> _expandedStates = {};
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
@@ -41,7 +43,6 @@ class HistoryScreenState extends State<HistoryScreen> {
late final ChatScrollObserver _chatObserver; late final ChatScrollObserver _chatObserver;
bool _isAtTop = true; bool _isAtTop = true;
MergeSettings _mergeSettings = MergeSettings(); MergeSettings _mergeSettings = MergeSettings();
double _itemHeightCache = 0.0;
final Map<String, double> _mapOptimalZoom = {}; final Map<String, double> _mapOptimalZoom = {};
final Map<String, bool> _mapCalculating = {}; final Map<String, bool> _mapCalculating = {};
@@ -74,15 +75,21 @@ class HistoryScreenState extends State<HistoryScreen> {
super.initState(); super.initState();
_chatObserver = ChatScrollObserver(_observerController) _chatObserver = ChatScrollObserver(_observerController)
..toRebuildScrollViewCallback = () { ..toRebuildScrollViewCallback = () {
setState(() {}); if (mounted) {
setState(() {});
}
}; };
_scrollController.addListener(() { _scrollController.addListener(() {
if (_scrollController.position.atEdge) { if (_scrollController.position.atEdge) {
if (_scrollController.position.pixels == 0) { if (_scrollController.position.pixels == 0) {
if (!_isAtTop) setState(() => _isAtTop = true); if (!_isAtTop) {
setState(() => _isAtTop = true);
}
} }
} else { } else {
if (_isAtTop) setState(() => _isAtTop = false); if (_isAtTop) {
setState(() => _isAtTop = false);
}
} }
}); });
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -109,17 +116,19 @@ class HistoryScreenState extends State<HistoryScreen> {
List<TrainRecord> filteredRecords = allRecords; List<TrainRecord> filteredRecords = allRecords;
if ((settingsMap['hideTimeOnlyRecords'] ?? 0) == 1) { if ((settingsMap['hideTimeOnlyRecords'] ?? 0) == 1) {
int hiddenCount = 0;
int shownCount = 0;
filteredRecords = allRecords.where((record) { filteredRecords = allRecords.where((record) {
bool isFieldMeaningful(String field) { bool isFieldMeaningful(String field) {
if (field.isEmpty) return false; if (field.isEmpty) {
String cleaned = field.replaceAll('<NUL>', '').trim();
if (cleaned.isEmpty) return false;
if (cleaned.runes
.every((r) => r == '*'.runes.first || r == ' '.runes.first))
return false; return false;
}
String cleaned = field.replaceAll('<NUL>', '').trim();
if (cleaned.isEmpty) {
return false;
}
if (cleaned.runes
.every((r) => r == '*'.runes.first || r == ' '.runes.first)) {
return false;
}
return true; return true;
} }
@@ -160,12 +169,6 @@ class HistoryScreenState extends State<HistoryScreen> {
hasLbjClass || hasLbjClass ||
hasTrain; hasTrain;
if (!shouldShow) {
hiddenCount++;
} else {
shownCount++;
}
return shouldShow; return shouldShow;
}).toList(); }).toList();
} }
@@ -192,7 +195,9 @@ class HistoryScreenState extends State<HistoryScreen> {
} }
} }
} catch (e) { } catch (e) {
if (mounted) setState(() => _isLoading = false); if (mounted) {
setState(() => _isLoading = false);
}
} }
} }
@@ -201,57 +206,7 @@ class HistoryScreenState extends State<HistoryScreen> {
final settingsMap = await DatabaseService.instance.getAllSettings() ?? {}; final settingsMap = await DatabaseService.instance.getAllSettings() ?? {};
_mergeSettings = MergeSettings.fromMap(settingsMap); _mergeSettings = MergeSettings.fromMap(settingsMap);
if ((settingsMap['hideTimeOnlyRecords'] ?? 0) == 1) { if ((settingsMap['hideTimeOnlyRecords'] ?? 0) == 1) {}
bool isFieldMeaningful(String field) {
if (field.isEmpty) return false;
String cleaned = field.replaceAll('<NUL>', '').trim();
if (cleaned.isEmpty) return false;
if (cleaned.runes
.every((r) => r == '*'.runes.first || r == ' '.runes.first))
return false;
return true;
}
final hasTrainNumber = isFieldMeaningful(newRecord.fullTrainNumber) &&
!newRecord.fullTrainNumber.contains("-----");
final hasDirection =
newRecord.direction == 1 || newRecord.direction == 3;
final hasLocoInfo = isFieldMeaningful(newRecord.locoType) ||
isFieldMeaningful(newRecord.loco);
final hasRoute = isFieldMeaningful(newRecord.route);
final hasPosition = isFieldMeaningful(newRecord.position);
final hasSpeed =
isFieldMeaningful(newRecord.speed) && newRecord.speed != "NUL";
final hasPositionInfo = isFieldMeaningful(newRecord.positionInfo);
final hasTrainType = isFieldMeaningful(newRecord.trainType) &&
newRecord.trainType != "未知";
final hasLbjClass =
isFieldMeaningful(newRecord.lbjClass) && newRecord.lbjClass != "NA";
final hasTrain = isFieldMeaningful(newRecord.train) &&
!newRecord.train.contains("-----");
if (!hasTrainNumber &&
!hasDirection &&
!hasLocoInfo &&
!hasRoute &&
!hasPosition &&
!hasSpeed &&
!hasPositionInfo &&
!hasTrainType &&
!hasLbjClass &&
!hasTrain) {
return;
}
}
final isNewRecord = !_displayItems.any((item) { final isNewRecord = !_displayItems.any((item) {
if (item is TrainRecord) { if (item is TrainRecord) {
@@ -261,32 +216,147 @@ class HistoryScreenState extends State<HistoryScreen> {
} }
return false; return false;
}); });
if (!isNewRecord) return; if (!isNewRecord) return;
final allRecords = await DatabaseService.instance.getAllRecords();
final items = MergeService.getMixedList(allRecords, _mergeSettings);
if (mounted) { if (mounted) {
if (!_isAtTop) { if (_isAtTop) {
_chatObserver.standby();
}
final hasDataChanged = _hasDataChanged(items);
if (hasDataChanged) {
setState(() { setState(() {
List<TrainRecord> allRecords = [];
Set<String> selectedRecordIds = {};
for (final item in _displayItems) {
if (item is MergedTrainRecord) {
allRecords.addAll(item.records);
if (_selectedRecords.contains(item.records.first.uniqueId)) {
selectedRecordIds.addAll(item.records.map((r) => r.uniqueId));
}
} else if (item is TrainRecord) {
allRecords.add(item);
if (_selectedRecords.contains(item.uniqueId)) {
selectedRecordIds.add(item.uniqueId);
}
}
}
allRecords.insert(0, newRecord);
final mergedItems =
MergeService.getMixedList(allRecords, _mergeSettings);
_displayItems.clear(); _displayItems.clear();
_displayItems.addAll(items); _displayItems.addAll(mergedItems);
_selectedRecords.clear();
for (final item in _displayItems) {
if (item is MergedTrainRecord) {
if (item.records
.any((r) => selectedRecordIds.contains(r.uniqueId))) {
_selectedRecords.addAll(item.records.map((r) => r.uniqueId));
}
} else if (item is TrainRecord) {
if (selectedRecordIds.contains(item.uniqueId)) {
_selectedRecords.add(item.uniqueId);
}
}
}
}); });
if (_scrollController.hasClients) {
_scrollController.jumpTo(0.0);
}
return;
} }
if (_isAtTop && _scrollController.hasClients) { final anchorModel = _observerController.observeFirstItem();
_scrollController.jumpTo(0.0); if (anchorModel == null) {
return;
} }
_anchorIndex = anchorModel.index;
if (_anchorIndex! > 0) {
_anchorOffset = anchorModel.layoutOffset;
} else {
_oldCardHeight = anchorModel.size.height;
_oldScrollOffset = _scrollController.offset;
}
bool isMerge = false;
Object? mergeResult;
final firstItem = _displayItems.first;
List<TrainRecord> tempRecords = [newRecord];
if (firstItem is MergedTrainRecord) {
tempRecords.addAll(firstItem.records);
} else if (firstItem is TrainRecord) {
tempRecords.add(firstItem);
}
final mergeCheckResult =
MergeService.getMixedList(tempRecords, _mergeSettings);
if (mergeCheckResult.length == 1 &&
mergeCheckResult.first is MergedTrainRecord) {
isMerge = true;
mergeResult = mergeCheckResult.first;
}
setState(() {
if (isMerge) {
_displayItems[0] = mergeResult!;
} else {
_displayItems.insert(0, newRecord);
}
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _anchorIndex == null) return;
if (_anchorIndex! > 0) {
final newAnchorIndex = isMerge ? _anchorIndex! : _anchorIndex! + 1;
final newAnchorModel =
_observerController.observeItem(index: newAnchorIndex);
if (newAnchorModel != null && _anchorOffset != null) {
final newOffset = newAnchorModel.layoutOffset;
final delta = newOffset - _anchorOffset!;
if (delta.abs() > 0.1) {
_scrollController.jumpTo(_scrollController.offset + delta);
}
}
} else {
final newAnchorModel = _observerController.observeItem(index: 0);
if (newAnchorModel != null &&
_oldCardHeight != null &&
_oldScrollOffset != null) {
final newHeight = newAnchorModel.size.height;
final heightDelta = newHeight - _oldCardHeight!;
if (heightDelta.abs() > 0.1) {
_scrollController.jumpTo(_oldScrollOffset! + heightDelta);
}
}
}
_anchorIndex = null;
_anchorOffset = null;
_oldCardHeight = null;
_oldScrollOffset = null;
});
} }
} catch (e) {} } catch (e) {}
} }
String _getGroupKeyForRecord(TrainRecord record, MergeSettings settings) {
switch (settings.groupBy) {
case GroupBy.trainOnly:
return record.train.trim();
case GroupBy.locoOnly:
return record.loco.trim();
case GroupBy.trainAndLoco:
return '${record.train.trim()}-${record.loco.trim()}';
case GroupBy.trainOrLoco:
final train = record.train.trim();
if (train.isNotEmpty) return train;
final loco = record.loco.trim();
if (loco.isNotEmpty) return loco;
return '';
}
}
bool _hasDataChanged(List<Object> newItems) { bool _hasDataChanged(List<Object> newItems) {
if (_displayItems.length != newItems.length) return true; if (_displayItems.length != newItems.length) return true;
@@ -303,7 +373,6 @@ class HistoryScreenState extends State<HistoryScreen> {
if (oldItem.records.length != newItem.records.length) return true; if (oldItem.records.length != newItem.records.length) return true;
} }
} }
return false; return false;
} }
@@ -346,6 +415,7 @@ class HistoryScreenState extends State<HistoryScreen> {
mergedRecord.records.any((r) => _selectedRecords.contains(r.uniqueId)); mergedRecord.records.any((r) => _selectedRecords.contains(r.uniqueId));
final isExpanded = _expandedStates[mergedRecord.groupKey] ?? false; final isExpanded = _expandedStates[mergedRecord.groupKey] ?? false;
return Card( return Card(
key: ValueKey(mergedRecord.groupKey),
color: isSelected && _isEditMode color: isSelected && _isEditMode
? const Color(0xFF2E2E2E) ? const Color(0xFF2E2E2E)
: const Color(0xFF1E1E1E), : const Color(0xFF1E1E1E),
@@ -389,7 +459,9 @@ class HistoryScreenState extends State<HistoryScreen> {
} }
}, },
onLongPress: () { onLongPress: () {
if (!_isEditMode) setEditMode(true); if (!_isEditMode) {
setEditMode(true);
}
setState(() { setState(() {
final allIdsInGroup = final allIdsInGroup =
mergedRecord.records.map((r) => r.uniqueId).toSet(); mergedRecord.records.map((r) => r.uniqueId).toSet();
@@ -488,10 +560,8 @@ class HistoryScreenState extends State<HistoryScreen> {
TrainRecord record, TrainRecord latest, GroupBy groupBy) { TrainRecord record, TrainRecord latest, GroupBy groupBy) {
final train = record.train.trim(); final train = record.train.trim();
final loco = record.loco.trim(); final loco = record.loco.trim();
final locoType = record.locoType.trim();
final latestTrain = latest.train.trim(); final latestTrain = latest.train.trim();
final latestLoco = latest.loco.trim(); final latestLoco = latest.loco.trim();
final latestLocoType = latest.locoType.trim();
switch (groupBy) { switch (groupBy) {
case GroupBy.trainOnly: case GroupBy.trainOnly:
@@ -530,77 +600,14 @@ class HistoryScreenState extends State<HistoryScreen> {
} }
} }
double _calculateOptimalZoom(List<LatLng> positions,
{double containerWidth = 400, double containerHeight = 220}) {
if (positions.length == 1) return 17.0;
double minLat = positions[0].latitude;
double maxLat = positions[0].latitude;
double minLng = positions[0].longitude;
double maxLng = positions[0].longitude;
for (final pos in positions) {
minLat = math.min(minLat, pos.latitude);
maxLat = math.max(maxLat, pos.latitude);
minLng = math.min(minLng, pos.longitude);
maxLng = math.max(maxLng, pos.longitude);
}
double latToY(double lat) {
final latRad = lat * math.pi / 180.0;
return math.log(math.tan(latRad) + 1.0 / math.cos(latRad));
}
double lngToX(double lng) {
return lng * math.pi / 180.0;
}
final minX = lngToX(minLng);
final maxX = lngToX(maxLng);
final minY = latToY(minLat);
final maxY = latToY(maxLat);
const worldSize = 2.0 * math.pi;
final widthWorld = (maxX - minX) / worldSize;
final heightWorld = (maxY - minY) / worldSize;
const paddingRatio = 0.8;
final widthZoom =
math.log((containerWidth * paddingRatio) / (widthWorld * 256.0)) /
math.log(2.0);
final heightZoom =
math.log((containerHeight * paddingRatio) / (heightWorld * 256.0)) /
math.log(2.0);
final optimalZoom = math.min(widthZoom, heightZoom);
return math.max(1.0, math.min(20.0, optimalZoom));
}
double _calculateDistance(LatLng pos1, LatLng pos2) {
const earthRadius = 6371000;
final lat1 = pos1.latitude * math.pi / 180;
final lat2 = pos2.latitude * math.pi / 180;
final deltaLat = (pos2.latitude - pos1.latitude) * math.pi / 180;
final deltaLng = (pos2.longitude - pos1.longitude) * math.pi / 180;
final a = math.sin(deltaLat / 2) * math.sin(deltaLat / 2) +
math.cos(lat1) *
math.cos(lat2) *
math.sin(deltaLng / 2) *
math.sin(deltaLng / 2);
final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
return earthRadius * c;
}
String _getLocationInfo(TrainRecord record) { String _getLocationInfo(TrainRecord record) {
List<String> parts = []; List<String> parts = [];
if (record.route.isNotEmpty && record.route != "<NUL>") if (record.route.isNotEmpty && record.route != "<NUL>") {
parts.add(record.route); parts.add(record.route);
if (record.direction != 0) parts.add(record.direction == 1 ? "" : ""); }
if (record.direction != 0) {
parts.add(record.direction == 1 ? "" : "");
}
if (record.position.isNotEmpty && record.position != "<NUL>") { if (record.position.isNotEmpty && record.position != "<NUL>") {
final position = record.position; final position = record.position;
final cleanPosition = position.endsWith('.') final cleanPosition = position.endsWith('.')
@@ -616,7 +623,9 @@ class HistoryScreenState extends State<HistoryScreen> {
.map((record) => _parsePosition(record.positionInfo)) .map((record) => _parsePosition(record.positionInfo))
.whereType<LatLng>() .whereType<LatLng>()
.toList(); .toList();
if (positions.isEmpty) return const SizedBox.shrink(); if (positions.isEmpty) {
return const SizedBox.shrink();
}
final mapId = records.map((r) => r.uniqueId).join('_'); final mapId = records.map((r) => r.uniqueId).join('_');
final bounds = LatLngBounds.fromPoints(positions); final bounds = LatLngBounds.fromPoints(positions);
@@ -674,12 +683,6 @@ class HistoryScreenState extends State<HistoryScreen> {
]); ]);
} }
double _getDefaultZoom(List<LatLng> positions) {
if (positions.length == 1) return 15.0;
if (positions.length < 10) return 12.0;
return 10.0;
}
Future<void> _requestLocationPermission() async { Future<void> _requestLocationPermission() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) { if (!serviceEnabled) {
@@ -695,23 +698,30 @@ class HistoryScreenState extends State<HistoryScreen> {
return; return;
} }
setState(() { if (mounted) {
_isLocationPermissionGranted = true; setState(() {
}); _isLocationPermissionGranted = true;
});
}
_getCurrentLocation(); _getCurrentLocation();
} }
Future<void> _getCurrentLocation() async { Future<void> _getCurrentLocation() async {
try { try {
final locationSettings = AndroidSettings(
accuracy: LocationAccuracy.high,
forceLocationManager: true,
);
Position position = await Geolocator.getCurrentPosition( Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high, locationSettings: locationSettings,
forceAndroidLocationManager: true,
); );
setState(() { if (mounted) {
_currentUserLocation = LatLng(position.latitude, position.longitude); setState(() {
}); _currentUserLocation = LatLng(position.latitude, position.longitude);
});
}
} catch (e) {} } catch (e) {}
} }
@@ -728,13 +738,9 @@ class HistoryScreenState extends State<HistoryScreen> {
Widget _buildRecordCard(TrainRecord record, Widget _buildRecordCard(TrainRecord record,
{bool isSubCard = false, Key? key}) { {bool isSubCard = false, Key? key}) {
final isSelected = _selectedRecords.contains(record.uniqueId); final isSelected = _selectedRecords.contains(record.uniqueId);
final isExpanded =
!isSubCard && (_expandedStates[record.uniqueId] ?? false);
final GlobalKey itemKey = GlobalKey(); return Card(
key: key,
final Widget card = Card(
key: key ?? itemKey,
color: isSelected && _isEditMode color: isSelected && _isEditMode
? const Color(0xFF2E2E2E) ? const Color(0xFF2E2E2E)
: const Color(0xFF1E1E1E), : const Color(0xFF1E1E1E),
@@ -759,10 +765,17 @@ class HistoryScreenState extends State<HistoryScreen> {
} }
widget.onSelectionChanged(); widget.onSelectionChanged();
}); });
} else {
setState(() {
_expandedStates[record.uniqueId] =
!(_expandedStates[record.uniqueId] ?? false);
});
} }
}, },
onLongPress: () { onLongPress: () {
if (!_isEditMode) setEditMode(true); if (!_isEditMode) {
setEditMode(true);
}
setState(() { setState(() {
_selectedRecords.add(record.uniqueId); _selectedRecords.add(record.uniqueId);
widget.onSelectionChanged(); widget.onSelectionChanged();
@@ -776,23 +789,9 @@ class HistoryScreenState extends State<HistoryScreen> {
_buildRecordHeader(record), _buildRecordHeader(record),
_buildPositionAndSpeed(record), _buildPositionAndSpeed(record),
_buildLocoInfo(record), _buildLocoInfo(record),
if (isExpanded) _buildExpandedContent(record), if (_expandedStates[record.uniqueId] ?? false)
_buildExpandedContent(record),
])))); ]))));
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_itemHeightCache <= 0 && itemKey.currentContext != null) {
final RenderBox renderBox =
itemKey.currentContext!.findRenderObject() as RenderBox;
final double realHeight = renderBox.size.height;
if (realHeight > 0) {
setState(() {
_itemHeightCache = realHeight;
});
}
}
});
return card;
} }
Widget _buildRecordHeader(TrainRecord record, {bool isMerged = false}) { Widget _buildRecordHeader(TrainRecord record, {bool isMerged = false}) {
@@ -823,6 +822,7 @@ class HistoryScreenState extends State<HistoryScreen> {
final hasLocoInfo = final hasLocoInfo =
formattedLocoInfo.isNotEmpty && formattedLocoInfo != "<NUL>"; formattedLocoInfo.isNotEmpty && formattedLocoInfo != "<NUL>";
final shouldShowTrainRow = hasTrainNumber || hasDirection || hasLocoInfo; final shouldShowTrainRow = hasTrainNumber || hasDirection || hasLocoInfo;
final hasPosition = _parsePosition(record.positionInfo) != null;
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
@@ -876,7 +876,8 @@ class HistoryScreenState extends State<HistoryScreen> {
])), ])),
if (hasLocoInfo) if (hasLocoInfo)
Text(formattedLocoInfo, Text(formattedLocoInfo,
style: const TextStyle(fontSize: 14, color: Colors.white70)) style:
const TextStyle(fontSize: 14, color: Colors.white70)),
]), ]),
const SizedBox(height: 2) const SizedBox(height: 2)
] ]
@@ -885,7 +886,9 @@ class HistoryScreenState extends State<HistoryScreen> {
Widget _buildLocoInfo(TrainRecord record) { Widget _buildLocoInfo(TrainRecord record) {
final locoInfo = record.locoInfo; final locoInfo = record.locoInfo;
if (locoInfo == null || locoInfo.isEmpty) return const SizedBox.shrink(); if (locoInfo == null || locoInfo.isEmpty) {
return const SizedBox.shrink();
}
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const SizedBox(height: 4), const SizedBox(height: 4),
Text(locoInfo, Text(locoInfo,
@@ -910,8 +913,9 @@ class HistoryScreenState extends State<HistoryScreen> {
.every((r) => r == '*'.runes.first || r == '-'.runes.first) && .every((r) => r == '*'.runes.first || r == '-'.runes.first) &&
speed != "NUL" && speed != "NUL" &&
speed != "<NUL>"; speed != "<NUL>";
if (!isValidRoute && !isValidPosition && !isValidSpeed) if (!isValidRoute && !isValidPosition && !isValidSpeed) {
return const SizedBox.shrink(); return const SizedBox.shrink();
}
return Padding( return Padding(
padding: const EdgeInsets.only(top: 4.0), padding: const EdgeInsets.only(top: 4.0),
child: child:
@@ -1002,8 +1006,11 @@ class HistoryScreenState extends State<HistoryScreen> {
} }
LatLng? _parsePosition(String? positionInfo) { LatLng? _parsePosition(String? positionInfo) {
if (positionInfo == null || positionInfo.isEmpty || positionInfo == '<NUL>') if (positionInfo == null ||
positionInfo.isEmpty ||
positionInfo == '<NUL>') {
return null; return null;
}
try { try {
final parts = positionInfo.trim().split(RegExp(r'\s+')); final parts = positionInfo.trim().split(RegExp(r'\s+'));
if (parts.length >= 2) { if (parts.length >= 2) {
@@ -1022,14 +1029,22 @@ class HistoryScreenState extends State<HistoryScreen> {
double? _parseDmsCoordinate(String dmsStr) { double? _parseDmsCoordinate(String dmsStr) {
try { try {
final degreeIndex = dmsStr.indexOf('°'); final degreeIndex = dmsStr.indexOf('°');
if (degreeIndex == -1) return null; if (degreeIndex == -1) {
return null;
}
final degrees = double.tryParse(dmsStr.substring(0, degreeIndex)); final degrees = double.tryParse(dmsStr.substring(0, degreeIndex));
if (degrees == null) return null; if (degrees == null) {
return null;
}
final minuteIndex = dmsStr.indexOf(''); final minuteIndex = dmsStr.indexOf('');
if (minuteIndex == -1) return degrees; if (minuteIndex == -1) {
return degrees;
}
final minutes = final minutes =
double.tryParse(dmsStr.substring(degreeIndex + 1, minuteIndex)); double.tryParse(dmsStr.substring(degreeIndex + 1, minuteIndex));
if (minutes == null) return degrees; if (minutes == null) {
return degrees;
}
return degrees + (minutes / 60.0); return degrees + (minutes / 60.0);
} catch (e) { } catch (e) {
return null; return null;
@@ -1140,12 +1155,12 @@ class _DelayedMapWithMarker extends StatefulWidget {
final LatLng? currentUserLocation; final LatLng? currentUserLocation;
const _DelayedMapWithMarker({ const _DelayedMapWithMarker({
Key? key, super.key,
required this.position, required this.position,
required this.zoom, required this.zoom,
required this.recordId, required this.recordId,
this.currentUserLocation, this.currentUserLocation,
}) : super(key: key); });
@override @override
State<_DelayedMapWithMarker> createState() => _DelayedMapWithMarkerState(); State<_DelayedMapWithMarker> createState() => _DelayedMapWithMarkerState();
@@ -1175,13 +1190,17 @@ class _DelayedMapWithMarkerState extends State<_DelayedMapWithMarker> {
_mapController.rotate(savedState.bearing); _mapController.rotate(savedState.bearing);
} }
} }
setState(() { if (mounted) {
_isInitializing = false; setState(() {
}); _isInitializing = false;
});
}
} }
void _onCameraMove() { void _onCameraMove() {
if (_isInitializing) return; if (_isInitializing) {
return;
}
final camera = _mapController.camera; final camera = _mapController.camera;
final state = MapState( final state = MapState(
@@ -1280,13 +1299,13 @@ class _DelayedMultiMarkerMap extends StatefulWidget {
final LatLng? currentUserLocation; final LatLng? currentUserLocation;
const _DelayedMultiMarkerMap({ const _DelayedMultiMarkerMap({
Key? key, super.key,
required this.positions, required this.positions,
required this.center, required this.center,
required this.zoom, required this.zoom,
required this.groupKey, required this.groupKey,
this.currentUserLocation, this.currentUserLocation,
}) : super(key: key); });
@override @override
State<_DelayedMultiMarkerMap> createState() => _DelayedMultiMarkerMapState(); State<_DelayedMultiMarkerMap> createState() => _DelayedMultiMarkerMapState();
@@ -1318,13 +1337,17 @@ class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
} else if (mounted) { } else if (mounted) {
_mapController.move(widget.center, widget.zoom); _mapController.move(widget.center, widget.zoom);
} }
setState(() { if (mounted) {
_isInitializing = false; setState(() {
}); _isInitializing = false;
});
}
} }
void _onCameraMove() { void _onCameraMove() {
if (_isInitializing) return; if (_isInitializing) {
return;
}
final camera = _mapController.camera; final camera = _mapController.camera;
final state = MapState( final state = MapState(
@@ -1352,7 +1375,7 @@ class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
height: 24, height: 24,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.red.withOpacity(0.8), color: Colors.red.withAlpha((255 * 0.8).round()),
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 1.5)), border: Border.all(color: Colors.white, width: 1.5)),
child: const Icon(Icons.train, color: Colors.white, size: 12)))), child: const Icon(Icons.train, color: Colors.white, size: 12)))),

View File

@@ -7,6 +7,7 @@ import 'package:lbjconsole/models/train_record.dart';
import 'package:lbjconsole/screens/history_screen.dart'; import 'package:lbjconsole/screens/history_screen.dart';
import 'package:lbjconsole/screens/map_screen.dart'; import 'package:lbjconsole/screens/map_screen.dart';
import 'package:lbjconsole/screens/map_webview_screen.dart'; import 'package:lbjconsole/screens/map_webview_screen.dart';
import 'package:lbjconsole/screens/realtime_screen.dart';
import 'package:lbjconsole/screens/settings_screen.dart'; import 'package:lbjconsole/screens/settings_screen.dart';
import 'package:lbjconsole/services/ble_service.dart'; import 'package:lbjconsole/services/ble_service.dart';
import 'package:lbjconsole/services/database_service.dart'; import 'package:lbjconsole/services/database_service.dart';
@@ -183,10 +184,13 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
StreamSubscription? _connectionSubscription; StreamSubscription? _connectionSubscription;
StreamSubscription? _dataSubscription; StreamSubscription? _dataSubscription;
StreamSubscription? _lastReceivedTimeSubscription; StreamSubscription? _lastReceivedTimeSubscription;
StreamSubscription? _settingsSubscription;
DateTime? _lastReceivedTime; DateTime? _lastReceivedTime;
bool _isHistoryEditMode = false; bool _isHistoryEditMode = false;
final GlobalKey<HistoryScreenState> _historyScreenKey = final GlobalKey<HistoryScreenState> _historyScreenKey =
GlobalKey<HistoryScreenState>(); GlobalKey<HistoryScreenState>();
final GlobalKey<RealtimeScreenState> _realtimeScreenKey =
GlobalKey<RealtimeScreenState>();
@override @override
void initState() { void initState() {
@@ -197,6 +201,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
_initializeServices(); _initializeServices();
_checkAndStartBackgroundService(); _checkAndStartBackgroundService();
_setupLastReceivedTimeListener(); _setupLastReceivedTimeListener();
_setupSettingsListener();
_loadMapType(); _loadMapType();
} }
@@ -209,7 +214,6 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
} }
} }
Future<void> _checkAndStartBackgroundService() async { Future<void> _checkAndStartBackgroundService() async {
final settings = await DatabaseService.instance.getAllSettings() ?? {}; final settings = await DatabaseService.instance.getAllSettings() ?? {};
final backgroundServiceEnabled = final backgroundServiceEnabled =
@@ -231,11 +235,21 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
}); });
} }
void _setupSettingsListener() {
_settingsSubscription =
DatabaseService.instance.onSettingsChanged((settings) {
if (mounted && _currentIndex == 1) {
_realtimeScreenKey.currentState?.loadRecords(scrollToTop: false);
}
});
}
@override @override
void dispose() { void dispose() {
_connectionSubscription?.cancel(); _connectionSubscription?.cancel();
_dataSubscription?.cancel(); _dataSubscription?.cancel();
_lastReceivedTimeSubscription?.cancel(); _lastReceivedTimeSubscription?.cancel();
_settingsSubscription?.cancel();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
@@ -256,6 +270,9 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
if (_historyScreenKey.currentState != null) { if (_historyScreenKey.currentState != null) {
_historyScreenKey.currentState!.addNewRecord(record); _historyScreenKey.currentState!.addNewRecord(record);
} }
if (_realtimeScreenKey.currentState != null) {
_realtimeScreenKey.currentState!.addNewRecord(record);
}
}); });
} }
@@ -302,7 +319,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
backgroundColor: AppTheme.primaryBlack, backgroundColor: AppTheme.primaryBlack,
elevation: 0, elevation: 0,
title: Text( title: Text(
['列车记录', '位置地图', '设置'][_currentIndex], ['列车记录', '数据监控', '位置地图', '设置'][_currentIndex],
style: const TextStyle( style: const TextStyle(
color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
), ),
@@ -394,6 +411,9 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
onEditModeChanged: _handleHistoryEditModeChanged, onEditModeChanged: _handleHistoryEditModeChanged,
onSelectionChanged: _handleSelectionChanged, onSelectionChanged: _handleSelectionChanged,
), ),
RealtimeScreen(
key: _realtimeScreenKey,
),
_mapType == 'map' ? const MapScreen() : const MapWebViewScreen(), _mapType == 'map' ? const MapScreen() : const MapWebViewScreen(),
SettingsScreen( SettingsScreen(
onSettingsChanged: () { onSettingsChanged: () {
@@ -411,14 +431,16 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
), ),
bottomNavigationBar: NavigationBar( bottomNavigationBar: NavigationBar(
backgroundColor: AppTheme.secondaryBlack, backgroundColor: AppTheme.secondaryBlack,
indicatorColor: AppTheme.accentBlue.withOpacity(0.2), indicatorColor: AppTheme.accentBlue.withValues(alpha: 0.2),
selectedIndex: _currentIndex, selectedIndex: _currentIndex,
onDestinationSelected: (index) { onDestinationSelected: (index) {
if (_currentIndex == 2 && index == 0) { if (index == 0) {
_historyScreenKey.currentState?.reloadRecords(); _historyScreenKey.currentState?.reloadRecords();
} }
// 如果从设置页面切换到地图页面,重新加载地图类型 if (index == 1) {
if (_currentIndex == 2 && index == 1) { _realtimeScreenKey.currentState?.loadRecords(scrollToTop: false);
}
if (_currentIndex == 3 && index == 2) {
_loadMapType(); _loadMapType();
} }
setState(() { setState(() {
@@ -429,6 +451,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
destinations: const [ destinations: const [
NavigationDestination( NavigationDestination(
icon: Icon(Icons.directions_railway), label: '列车记录'), icon: Icon(Icons.directions_railway), label: '列车记录'),
NavigationDestination(icon: Icon(Icons.speed), label: '数据监控'),
NavigationDestination(icon: Icon(Icons.location_on), label: '位置地图'), NavigationDestination(icon: Icon(Icons.location_on), label: '位置地图'),
NavigationDestination(icon: Icon(Icons.settings), label: '设置'), NavigationDestination(icon: Icon(Icons.settings), label: '设置'),
], ],

View File

@@ -32,13 +32,41 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
double _currentRotation = 0.0; double _currentRotation = 0.0;
LatLng? _currentLocation; LatLng? _currentLocation;
LatLng? _lastTrainLocation; LatLng? _lastTrainLocation;
bool _isDataLoaded = false;
final Completer<void> _webViewReadyCompleter = Completer<void>();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
_initializeWebView(); _initializeWebView();
_initializeServices(); _startInitialization();
}
Future<void> _startInitialization() async {
setState(() {
_isLoading = true;
});
try {
await _loadSettings();
await _loadTrainRecordsFromDatabase();
await _webViewReadyCompleter.future;
_initializeMapCamera();
_initializeLocation();
_startAutoRefresh();
} catch (e, s) {
print('[Flutter] Init Map WebView Screen Failed: $e\n$s');
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
} }
@override @override
@@ -48,16 +76,7 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
} }
} }
Future<void> _initializeServices() async { Future<void> _initializeWebView() async {
try {
await _loadSettings();
await _loadTrainRecordsFromDatabase();
_initializeLocation();
_startAutoRefresh();
} catch (e) {}
}
void _initializeWebView() {
_controller = WebViewController() _controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted) ..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0xFF121212)) ..setBackgroundColor(const Color(0xFF121212))
@@ -92,9 +111,8 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
_isLoading = false; _isLoading = false;
}); });
if (mounted) { if (!_webViewReadyCompleter.isCompleted) {
_initializeMap(); _webViewReadyCompleter.complete();
_initializeLocation();
} }
}, },
onWebResourceError: (WebResourceError error) { onWebResourceError: (WebResourceError error) {
@@ -107,6 +125,8 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
..loadFlutterAsset('assets/mapbox_map.html') ..loadFlutterAsset('assets/mapbox_map.html')
.then((_) {}) .then((_) {})
.catchError((error) {}); .catchError((error) {});
await Future.delayed(const Duration(milliseconds: 500));
} }
Future<void> _initializeLocation() async { Future<void> _initializeLocation() async {
@@ -266,10 +286,6 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
} else {} } else {}
} else {} } else {}
} else {} } else {}
if (mounted) {
_initializeMapPosition();
}
}); });
Future.delayed(const Duration(milliseconds: 3000), () { Future.delayed(const Duration(milliseconds: 3000), () {
@@ -351,25 +367,38 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
} }
} }
void _initializeMapPosition() { void _initializeMapCamera() {
if (_isMapInitialized) return; if (_isMapInitialized) return;
LatLng? targetLocation; LatLng targetLocation;
double targetZoom = _currentZoom;
double targetRotation = _currentRotation;
if (_lastTrainLocation != null) { if (_currentLocation != null) {
targetLocation = _lastTrainLocation; targetLocation = _currentLocation!;
} else if (_currentPosition != null) { } else if (_lastTrainLocation != null) {
targetLocation = targetLocation = _lastTrainLocation!;
LatLng(_currentPosition!.latitude, _currentPosition!.longitude); targetZoom = 14.0;
} targetRotation = 0.0;
if (targetLocation != null) {
_centerMap(targetLocation,
zoom: _currentZoom, rotation: _currentRotation);
_isMapInitialized = true;
} else { } else {
_isMapInitialized = true; targetLocation = const LatLng(39.9042, 116.4074);
targetZoom = 10.0;
} }
_centerMap(targetLocation, zoom: targetZoom, rotation: targetRotation);
_isMapInitialized = true;
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
_updateUserLocation();
_updateTrainMarkers();
_controller.runJavaScript('''
if (window.MapInterface) {
window.MapInterface.setRailwayVisible($_isRailwayLayerVisible);
}
''');
}
});
} }
void _centerMap(LatLng location, {double? zoom, double? rotation}) { void _centerMap(LatLng location, {double? zoom, double? rotation}) {
@@ -430,91 +459,6 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
} catch (e) {} } catch (e) {}
} }
Future<void> _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) { LatLng? _parseDmsCoordinate(String? positionInfo) {
if (positionInfo == null || if (positionInfo == null ||
positionInfo.isEmpty || positionInfo.isEmpty ||
@@ -943,10 +887,10 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
void _onMapStateChanged(Map<String, dynamic> mapState) { void _onMapStateChanged(Map<String, dynamic> mapState) {
try { try {
final lat = mapState['lat'] as double; final lat = (mapState['lat'] as num).toDouble();
final lng = mapState['lng'] as double; final lng = (mapState['lng'] as num).toDouble();
final zoom = mapState['zoom'] as double; final zoom = (mapState['zoom'] as num).toDouble();
final bearing = mapState['bearing'] as double; final bearing = (mapState['bearing'] as num).toDouble();
setState(() { setState(() {
_currentLocation = LatLng(lat, lng); _currentLocation = LatLng(lat, lng);

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ class DatabaseService {
DatabaseService._internal(); DatabaseService._internal();
static const String _databaseName = 'train_database'; static const String _databaseName = 'train_database';
static const _databaseVersion = 6; static const _databaseVersion = 7;
static const String trainRecordsTable = 'train_records'; static const String trainRecordsTable = 'train_records';
static const String appSettingsTable = 'app_settings'; static const String appSettingsTable = 'app_settings';
@@ -87,6 +87,10 @@ class DatabaseService {
await db.execute( await db.execute(
'ALTER TABLE $appSettingsTable ADD COLUMN hideUngroupableRecords INTEGER NOT NULL DEFAULT 0'); 'ALTER TABLE $appSettingsTable ADD COLUMN hideUngroupableRecords INTEGER NOT NULL DEFAULT 0');
} }
if (oldVersion < 7) {
await db.execute(
'ALTER TABLE $appSettingsTable ADD COLUMN mapSettingsTimestamp INTEGER');
}
} }
Future<void> _onCreate(Database db, int version) async { Future<void> _onCreate(Database db, int version) async {
@@ -136,7 +140,8 @@ class DatabaseService {
groupBy TEXT NOT NULL DEFAULT 'trainAndLoco', groupBy TEXT NOT NULL DEFAULT 'trainAndLoco',
timeWindow TEXT NOT NULL DEFAULT 'unlimited', timeWindow TEXT NOT NULL DEFAULT 'unlimited',
mapTimeFilter TEXT NOT NULL DEFAULT 'unlimited', mapTimeFilter TEXT NOT NULL DEFAULT 'unlimited',
hideUngroupableRecords INTEGER NOT NULL DEFAULT 0 hideUngroupableRecords INTEGER NOT NULL DEFAULT 0,
mapSettingsTimestamp INTEGER
) )
'''); ''');
@@ -164,6 +169,7 @@ class DatabaseService {
'timeWindow': 'unlimited', 'timeWindow': 'unlimited',
'mapTimeFilter': 'unlimited', 'mapTimeFilter': 'unlimited',
'hideUngroupableRecords': 0, 'hideUngroupableRecords': 0,
'mapSettingsTimestamp': null,
}); });
} }
@@ -226,16 +232,28 @@ class DatabaseService {
Future<int> deleteRecord(String uniqueId) async { Future<int> deleteRecord(String uniqueId) async {
final db = await database; final db = await database;
return await db.delete( final result = await db.delete(
trainRecordsTable, trainRecordsTable,
where: 'uniqueId = ?', where: 'uniqueId = ?',
whereArgs: [uniqueId], whereArgs: [uniqueId],
); );
if (result > 0) {
_notifyRecordDeleted([uniqueId]);
}
return result;
} }
Future<int> deleteAllRecords() async { Future<int> deleteAllRecords() async {
final db = await database; final db = await database;
return await db.delete(trainRecordsTable); final result = await db.delete(trainRecordsTable);
if (result > 0) {
_notifyRecordDeleted([]);
}
return result;
} }
Future<int> getRecordCount() async { Future<int> getRecordCount() async {
@@ -273,20 +291,31 @@ class DatabaseService {
Future<int> updateSettings(Map<String, dynamic> settings) async { Future<int> updateSettings(Map<String, dynamic> settings) async {
final db = await database; final db = await database;
return await db.update( final result = await db.update(
appSettingsTable, appSettingsTable,
settings, settings,
where: 'id = 1', where: 'id = 1',
); );
if (result > 0) {
_notifySettingsChanged(settings);
}
return result;
} }
Future<int> setSetting(String key, dynamic value) async { Future<int> setSetting(String key, dynamic value) async {
final db = await database; final db = await database;
return await db.update( final result = await db.update(
appSettingsTable, appSettingsTable,
{key: value}, {key: value},
where: 'id = 1', where: 'id = 1',
); );
if (result > 0) {
final currentSettings = await getAllSettings();
if (currentSettings != null) {
_notifySettingsChanged(currentSettings);
}
}
return result;
} }
Future<List<String>> getSearchOrderList() async { Future<List<String>> getSearchOrderList() async {
@@ -343,6 +372,42 @@ class DatabaseService {
whereArgs: [id], whereArgs: [id],
); );
} }
_notifyRecordDeleted(uniqueIds);
}
final List<Function(List<String>)> _recordDeleteListeners = [];
final List<Function(Map<String, dynamic>)> _settingsListeners = [];
StreamSubscription<void> onRecordDeleted(Function(List<String>) listener) {
_recordDeleteListeners.add(listener);
return Stream.value(null).listen((_) {})
..onData((_) {})
..onDone(() {
_recordDeleteListeners.remove(listener);
});
}
void _notifyRecordDeleted(List<String> deletedIds) {
for (final listener in _recordDeleteListeners) {
listener(deletedIds);
}
}
StreamSubscription<void> onSettingsChanged(
Function(Map<String, dynamic>) listener) {
_settingsListeners.add(listener);
return Stream.value(null).listen((_) {})
..onData((_) {})
..onDone(() {
_settingsListeners.remove(listener);
});
}
void _notifySettingsChanged(Map<String, dynamic> settings) {
for (final listener in _settingsListeners) {
listener(settings);
}
} }
Future<void> close() async { Future<void> close() async {
@@ -398,6 +463,11 @@ class DatabaseService {
} }
}); });
final currentSettings = await getAllSettings();
if (currentSettings != null) {
_notifySettingsChanged(currentSettings);
}
return true; return true;
} catch (e) { } catch (e) {
return false; return false;

View File

@@ -2,7 +2,7 @@ name: lbjconsole
description: "LBJ Console" description: "LBJ Console"
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages. # pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: "none" # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application. # The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43 # A version number is three numbers separated by dots, like 1.2.43
@@ -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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 0.5.0-flutter+50 version: 0.6.1-flutter+61
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@@ -76,7 +76,6 @@ dev_dependencies:
# The following section is specific to Flutter packages. # The following section is specific to Flutter packages.
flutter: flutter:
# The following line ensures that the Material Icons font is # The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in # included with your application, so that you can use the icons in
# the material Icons class. # the material Icons class.
@@ -129,4 +128,4 @@ msix_config:
publisher: CN=noxylva, O=noxylva.org, C=US publisher: CN=noxylva, O=noxylva.org, C=US
logo_path: assets/icon.png logo_path: assets/icon.png
capabilities: bluetooth,internetClient,location capabilities: bluetooth,internetClient,location
certificate_path: keystore.jks certificate_path: keystore.jks