7 Commits

Author SHA1 Message Date
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
Nedifinita
6718ef7129 feat: add vector railway map 2025-09-29 18:44:15 +08:00
17 changed files with 18151 additions and 273 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) 设备接收并显示列车预警消息,功能包括:
- 接收列车预警消息,支持可选的手机推送通知。
- 监控指定列车的轨迹,在地图上显示。
- 在地图上显示预警消息的 GPS 信息。
- 基于内置数据文件显示机车配属,机车类型和车次类型。
[android](https://github.com/undef-i/LBJ_Console/tree/android) 分支包含项目早期基于 Android 平台的实现代码,已实现基本功能,现已停止开发。
## 数据文件
LBJ Console 依赖以下数据文件,位于 `assets` 目录,用于支持机车配属和车次信息的展示:
- `loco_info.csv`:包含机车配属信息,格式为 `机车型号,机车编号起始值,机车编号结束值,所属铁路局及机务段,备注`
- `loco_type_info.csv`:包含机车类型编码信息,格式为 `机车类型编码前缀,机车类型`
- `train_info.csv`:包含车次类型信息,格式为 `正则表达式,车次类型`

View File

@@ -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

15050
assets/mapbox_map.html vendored Normal file

File diff suppressed because it is too large Load Diff

3
devtools_options.yaml Normal file
View File

@@ -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:

View File

@@ -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<String, dynamic> map) {
@@ -36,6 +37,7 @@ class MergeSettings {
(e) => e.name == map['timeWindow'],
orElse: () => TimeWindow.unlimited,
),
hideUngroupableRecords: (map['hideUngroupableRecords'] ?? 0) == 1,
);
}
}

View File

@@ -1,9 +1,7 @@
import 'dart:math' as math;
import 'dart:isolate';
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:geolocator/geolocator.dart';
@@ -33,6 +31,10 @@ class HistoryScreenState extends State<HistoryScreen> {
final List<Object> _displayItems = [];
bool _isLoading = true;
bool _isEditMode = false;
int? _anchorIndex;
double? _anchorOffset;
double? _oldCardHeight;
double? _oldScrollOffset;
final Set<String> _selectedRecords = {};
final Map<String, bool> _expandedStates = {};
final ScrollController _scrollController = ScrollController();
@@ -41,7 +43,6 @@ class HistoryScreenState extends State<HistoryScreen> {
late final ChatScrollObserver _chatObserver;
bool _isAtTop = true;
MergeSettings _mergeSettings = MergeSettings();
double _itemHeightCache = 0.0;
final Map<String, double> _mapOptimalZoom = {};
final Map<String, bool> _mapCalculating = {};
@@ -74,15 +75,21 @@ class HistoryScreenState extends State<HistoryScreen> {
super.initState();
_chatObserver = ChatScrollObserver(_observerController)
..toRebuildScrollViewCallback = () {
setState(() {});
if (mounted) {
setState(() {});
}
};
_scrollController.addListener(() {
if (_scrollController.position.atEdge) {
if (_scrollController.position.pixels == 0) {
if (!_isAtTop) setState(() => _isAtTop = true);
if (!_isAtTop) {
setState(() => _isAtTop = true);
}
}
} else {
if (_isAtTop) setState(() => _isAtTop = false);
if (_isAtTop) {
setState(() => _isAtTop = false);
}
}
});
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -109,17 +116,19 @@ class HistoryScreenState extends State<HistoryScreen> {
List<TrainRecord> filteredRecords = allRecords;
if ((settingsMap['hideTimeOnlyRecords'] ?? 0) == 1) {
int hiddenCount = 0;
int shownCount = 0;
filteredRecords = allRecords.where((record) {
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))
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;
}
@@ -160,12 +169,6 @@ class HistoryScreenState extends State<HistoryScreen> {
hasLbjClass ||
hasTrain;
if (!shouldShow) {
hiddenCount++;
} else {
shownCount++;
}
return shouldShow;
}).toList();
}
@@ -192,7 +195,9 @@ class HistoryScreenState extends State<HistoryScreen> {
}
}
} 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() ?? {};
_mergeSettings = MergeSettings.fromMap(settingsMap);
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;
}
}
if ((settingsMap['hideTimeOnlyRecords'] ?? 0) == 1) {}
final isNewRecord = !_displayItems.any((item) {
if (item is TrainRecord) {
@@ -261,32 +216,132 @@ class HistoryScreenState extends State<HistoryScreen> {
}
return false;
});
if (!isNewRecord) return;
final allRecords = await DatabaseService.instance.getAllRecords();
final items = MergeService.getMixedList(allRecords, _mergeSettings);
if (mounted) {
if (!_isAtTop) {
_chatObserver.standby();
}
final hasDataChanged = _hasDataChanged(items);
if (hasDataChanged) {
if (_isAtTop) {
setState(() {
_displayItems.clear();
_displayItems.addAll(items);
bool isMerge = false;
Object? mergeResult;
if (_displayItems.isNotEmpty) {
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;
}
}
if (isMerge) {
_displayItems[0] = mergeResult!;
} else {
_displayItems.insert(0, newRecord);
}
});
if (_scrollController.hasClients) {
_scrollController.jumpTo(0.0);
}
return;
}
if (_isAtTop && _scrollController.hasClients) {
_scrollController.jumpTo(0.0);
final anchorModel = _observerController.observeFirstItem();
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) {}
}
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) {
if (_displayItems.length != newItems.length) return true;
@@ -303,7 +358,6 @@ class HistoryScreenState extends State<HistoryScreen> {
if (oldItem.records.length != newItem.records.length) return true;
}
}
return false;
}
@@ -346,6 +400,7 @@ class HistoryScreenState extends State<HistoryScreen> {
mergedRecord.records.any((r) => _selectedRecords.contains(r.uniqueId));
final isExpanded = _expandedStates[mergedRecord.groupKey] ?? false;
return Card(
key: ValueKey(mergedRecord.groupKey),
color: isSelected && _isEditMode
? const Color(0xFF2E2E2E)
: const Color(0xFF1E1E1E),
@@ -389,7 +444,9 @@ class HistoryScreenState extends State<HistoryScreen> {
}
},
onLongPress: () {
if (!_isEditMode) setEditMode(true);
if (!_isEditMode) {
setEditMode(true);
}
setState(() {
final allIdsInGroup =
mergedRecord.records.map((r) => r.uniqueId).toSet();
@@ -488,10 +545,8 @@ class HistoryScreenState extends State<HistoryScreen> {
TrainRecord record, TrainRecord latest, GroupBy groupBy) {
final train = record.train.trim();
final loco = record.loco.trim();
final locoType = record.locoType.trim();
final latestTrain = latest.train.trim();
final latestLoco = latest.loco.trim();
final latestLocoType = latest.locoType.trim();
switch (groupBy) {
case GroupBy.trainOnly:
@@ -530,77 +585,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) {
List<String> parts = [];
if (record.route.isNotEmpty && record.route != "<NUL>")
if (record.route.isNotEmpty && record.route != "<NUL>") {
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>") {
final position = record.position;
final cleanPosition = position.endsWith('.')
@@ -616,7 +608,9 @@ class HistoryScreenState extends State<HistoryScreen> {
.map((record) => _parsePosition(record.positionInfo))
.whereType<LatLng>()
.toList();
if (positions.isEmpty) return const SizedBox.shrink();
if (positions.isEmpty) {
return const SizedBox.shrink();
}
final mapId = records.map((r) => r.uniqueId).join('_');
final bounds = LatLngBounds.fromPoints(positions);
@@ -674,12 +668,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 {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
@@ -695,23 +683,30 @@ class HistoryScreenState extends State<HistoryScreen> {
return;
}
setState(() {
_isLocationPermissionGranted = true;
});
if (mounted) {
setState(() {
_isLocationPermissionGranted = true;
});
}
_getCurrentLocation();
}
Future<void> _getCurrentLocation() async {
try {
final locationSettings = AndroidSettings(
accuracy: LocationAccuracy.high,
forceLocationManager: true,
);
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
forceAndroidLocationManager: true,
locationSettings: locationSettings,
);
setState(() {
_currentUserLocation = LatLng(position.latitude, position.longitude);
});
if (mounted) {
setState(() {
_currentUserLocation = LatLng(position.latitude, position.longitude);
});
}
} catch (e) {}
}
@@ -731,10 +726,8 @@ class HistoryScreenState extends State<HistoryScreen> {
final isExpanded =
!isSubCard && (_expandedStates[record.uniqueId] ?? false);
final GlobalKey itemKey = GlobalKey();
final Widget card = Card(
key: key ?? itemKey,
return Card(
key: key,
color: isSelected && _isEditMode
? const Color(0xFF2E2E2E)
: const Color(0xFF1E1E1E),
@@ -762,7 +755,9 @@ class HistoryScreenState extends State<HistoryScreen> {
}
},
onLongPress: () {
if (!_isEditMode) setEditMode(true);
if (!_isEditMode) {
setEditMode(true);
}
setState(() {
_selectedRecords.add(record.uniqueId);
widget.onSelectionChanged();
@@ -778,21 +773,6 @@ class HistoryScreenState extends State<HistoryScreen> {
_buildLocoInfo(record),
if (isExpanded) _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}) {
@@ -885,7 +865,9 @@ class HistoryScreenState extends State<HistoryScreen> {
Widget _buildLocoInfo(TrainRecord record) {
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: [
const SizedBox(height: 4),
Text(locoInfo,
@@ -910,8 +892,9 @@ class HistoryScreenState extends State<HistoryScreen> {
.every((r) => r == '*'.runes.first || r == '-'.runes.first) &&
speed != "NUL" &&
speed != "<NUL>";
if (!isValidRoute && !isValidPosition && !isValidSpeed)
if (!isValidRoute && !isValidPosition && !isValidSpeed) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(top: 4.0),
child:
@@ -1002,8 +985,11 @@ class HistoryScreenState extends State<HistoryScreen> {
}
LatLng? _parsePosition(String? positionInfo) {
if (positionInfo == null || positionInfo.isEmpty || positionInfo == '<NUL>')
if (positionInfo == null ||
positionInfo.isEmpty ||
positionInfo == '<NUL>') {
return null;
}
try {
final parts = positionInfo.trim().split(RegExp(r'\s+'));
if (parts.length >= 2) {
@@ -1022,14 +1008,22 @@ class HistoryScreenState extends State<HistoryScreen> {
double? _parseDmsCoordinate(String dmsStr) {
try {
final degreeIndex = dmsStr.indexOf('°');
if (degreeIndex == -1) return null;
if (degreeIndex == -1) {
return null;
}
final degrees = double.tryParse(dmsStr.substring(0, degreeIndex));
if (degrees == null) return null;
if (degrees == null) {
return null;
}
final minuteIndex = dmsStr.indexOf('');
if (minuteIndex == -1) return degrees;
if (minuteIndex == -1) {
return degrees;
}
final minutes =
double.tryParse(dmsStr.substring(degreeIndex + 1, minuteIndex));
if (minutes == null) return degrees;
if (minutes == null) {
return degrees;
}
return degrees + (minutes / 60.0);
} catch (e) {
return null;
@@ -1140,12 +1134,12 @@ class _DelayedMapWithMarker extends StatefulWidget {
final LatLng? currentUserLocation;
const _DelayedMapWithMarker({
Key? key,
super.key,
required this.position,
required this.zoom,
required this.recordId,
this.currentUserLocation,
}) : super(key: key);
});
@override
State<_DelayedMapWithMarker> createState() => _DelayedMapWithMarkerState();
@@ -1175,13 +1169,17 @@ class _DelayedMapWithMarkerState extends State<_DelayedMapWithMarker> {
_mapController.rotate(savedState.bearing);
}
}
setState(() {
_isInitializing = false;
});
if (mounted) {
setState(() {
_isInitializing = false;
});
}
}
void _onCameraMove() {
if (_isInitializing) return;
if (_isInitializing) {
return;
}
final camera = _mapController.camera;
final state = MapState(
@@ -1280,13 +1278,13 @@ class _DelayedMultiMarkerMap extends StatefulWidget {
final LatLng? currentUserLocation;
const _DelayedMultiMarkerMap({
Key? key,
super.key,
required this.positions,
required this.center,
required this.zoom,
required this.groupKey,
this.currentUserLocation,
}) : super(key: key);
});
@override
State<_DelayedMultiMarkerMap> createState() => _DelayedMultiMarkerMapState();
@@ -1318,13 +1316,17 @@ class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
} else if (mounted) {
_mapController.move(widget.center, widget.zoom);
}
setState(() {
_isInitializing = false;
});
if (mounted) {
setState(() {
_isInitializing = false;
});
}
}
void _onCameraMove() {
if (_isInitializing) return;
if (_isInitializing) {
return;
}
final camera = _mapController.camera;
final state = MapState(
@@ -1352,7 +1354,7 @@ class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
height: 24,
child: Container(
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.8),
color: Colors.red.withAlpha((255 * 0.8).round()),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 1.5)),
child: const Icon(Icons.train, color: Colors.white, size: 12)))),
@@ -1383,7 +1385,7 @@ class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
return FlutterMap(
options: MapOptions(
onPositionChanged: (position, hasGesture) => _onCameraMove(),
minZoom: 5,
minZoom: 8,
maxZoom: 18,
),
mapController: _mapController,

View File

@@ -6,6 +6,8 @@ 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/realtime_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 +176,7 @@ class MainScreen extends StatefulWidget {
class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
int _currentIndex = 0;
String _mapType = 'webview';
late final BLEService _bleService;
final NotificationService _notificationService = NotificationService();
@@ -181,10 +184,13 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
StreamSubscription? _connectionSubscription;
StreamSubscription? _dataSubscription;
StreamSubscription? _lastReceivedTimeSubscription;
StreamSubscription? _settingsSubscription;
DateTime? _lastReceivedTime;
bool _isHistoryEditMode = false;
final GlobalKey<HistoryScreenState> _historyScreenKey =
GlobalKey<HistoryScreenState>();
final GlobalKey<RealtimeScreenState> _realtimeScreenKey =
GlobalKey<RealtimeScreenState>();
@override
void initState() {
@@ -195,6 +201,17 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
_initializeServices();
_checkAndStartBackgroundService();
_setupLastReceivedTimeListener();
_setupSettingsListener();
_loadMapType();
}
Future<void> _loadMapType() async {
final settings = await DatabaseService.instance.getAllSettings() ?? {};
if (mounted) {
setState(() {
_mapType = settings['mapType']?.toString() ?? 'webview';
});
}
}
Future<void> _checkAndStartBackgroundService() async {
@@ -218,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
void dispose() {
_connectionSubscription?.cancel();
_dataSubscription?.cancel();
_lastReceivedTimeSubscription?.cancel();
_settingsSubscription?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@@ -231,6 +258,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_bleService.onAppResume();
_loadMapType();
}
}
@@ -242,6 +270,9 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
if (_historyScreenKey.currentState != null) {
_historyScreenKey.currentState!.addNewRecord(record);
}
if (_realtimeScreenKey.currentState != null) {
_realtimeScreenKey.currentState!.addNewRecord(record);
}
});
}
@@ -288,7 +319,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
backgroundColor: AppTheme.primaryBlack,
elevation: 0,
title: Text(
['列车记录', '位置地图', '设置'][_currentIndex],
['列车记录', '数据监控', '位置地图', '设置'][_currentIndex],
style: const TextStyle(
color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
),
@@ -380,8 +411,15 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
onEditModeChanged: _handleHistoryEditModeChanged,
onSelectionChanged: _handleSelectionChanged,
),
const MapScreen(),
const SettingsScreen(),
RealtimeScreen(
key: _realtimeScreenKey,
),
_mapType == 'map' ? const MapScreen() : const MapWebViewScreen(),
SettingsScreen(
onSettingsChanged: () {
_loadMapType();
},
),
];
return Scaffold(
@@ -393,12 +431,18 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
),
bottomNavigationBar: NavigationBar(
backgroundColor: AppTheme.secondaryBlack,
indicatorColor: AppTheme.accentBlue.withOpacity(0.2),
indicatorColor: AppTheme.accentBlue.withValues(alpha: 0.2),
selectedIndex: _currentIndex,
onDestinationSelected: (index) {
if (_currentIndex == 2 && index == 0) {
if (index == 0) {
_historyScreenKey.currentState?.reloadRecords();
}
if (index == 1) {
_realtimeScreenKey.currentState?.loadRecords(scrollToTop: false);
}
if (_currentIndex == 3 && index == 2) {
_loadMapType();
}
setState(() {
if (_isHistoryEditMode) _isHistoryEditMode = false;
_currentIndex = index;
@@ -407,6 +451,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
destinations: const [
NavigationDestination(
icon: Icon(Icons.directions_railway), label: '列车记录'),
NavigationDestination(icon: Icon(Icons.speed), label: '数据监控'),
NavigationDestination(icon: Icon(Icons.location_on), label: '位置地图'),
NavigationDestination(icon: Icon(Icons.settings), label: '设置'),
],

View File

@@ -22,7 +22,7 @@ class _MapScreenState extends State<MapScreen> {
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<MapScreen> {
_loadSettings().then((_) {
_loadTrainRecords().then((_) {
_startLocationUpdates();
if (!_isMapInitialized && (_currentLocation != null || _lastTrainLocation != null || _userLocation != null)) {
_initializeMapPosition();
}
});
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_loadSettings();
}
Future<void> _checkDatabaseSettings() async {
try {
final dbInfo = await DatabaseService.instance.getDatabaseInfo();
@@ -227,6 +236,8 @@ class _MapScreenState extends State<MapScreen> {
settings['mapCenterLon'] = center.longitude;
}
settings['mapSettingsTimestamp'] = DateTime.now().millisecondsSinceEpoch;
await DatabaseService.instance.updateSettings(settings);
} catch (e) {}
}
@@ -734,11 +745,31 @@ class _MapScreenState extends State<MapScreen> {
);
}
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>(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<MapScreen> {
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(() {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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<SettingsScreen> createState() => _SettingsScreenState();
@@ -33,8 +35,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
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<SettingsScreen> {
_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<SettingsScreen> {
'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<SettingsScreen> {
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('地图显示方式', style: AppTheme.bodyLarge),
Text('选择地图组件类型', style: AppTheme.caption),
],
),
DropdownButton<String>(
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<SettingsScreen> {
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<SettingsScreen> {
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),
],

View File

@@ -13,7 +13,7 @@ class DatabaseService {
DatabaseService._internal();
static const String _databaseName = 'train_database';
static const _databaseVersion = 4;
static const _databaseVersion = 7;
static const String trainRecordsTable = 'train_records';
static const String appSettingsTable = 'app_settings';
@@ -21,21 +21,47 @@ class DatabaseService {
Database? _database;
Future<Database> 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<bool> 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<Database> _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<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
@@ -51,8 +77,19 @@ 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');
}
if (oldVersion < 7) {
await db.execute(
'ALTER TABLE $appSettingsTable ADD COLUMN mapSettingsTimestamp INTEGER');
}
}
@@ -92,6 +129,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 +139,9 @@ 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,
mapSettingsTimestamp INTEGER
)
''');
@@ -118,6 +158,7 @@ class DatabaseService {
'mapZoomLevel': 10.0,
'mapRailwayLayerVisible': 1,
'mapRotation': 0.0,
'mapType': 'webview',
'searchOrderList': '',
'autoConnectEnabled': 1,
'backgroundServiceEnabled': 0,
@@ -127,6 +168,8 @@ class DatabaseService {
'groupBy': 'trainAndLoco',
'timeWindow': 'unlimited',
'mapTimeFilter': 'unlimited',
'hideUngroupableRecords': 0,
'mapSettingsTimestamp': null,
});
}
@@ -140,12 +183,18 @@ class DatabaseService {
}
Future<List<TrainRecord>> 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<List<TrainRecord>> getRecordsWithinTimeRange(Duration duration) async {
@@ -162,29 +211,49 @@ class DatabaseService {
Future<List<TrainRecord>> 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<int> deleteRecord(String uniqueId) async {
final db = await database;
return await db.delete(
final result = await db.delete(
trainRecordsTable,
where: 'uniqueId = ?',
whereArgs: [uniqueId],
);
if (result > 0) {
_notifyRecordDeleted([uniqueId]);
}
return result;
}
Future<int> deleteAllRecords() async {
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 {
@@ -222,20 +291,31 @@ class DatabaseService {
Future<int> updateSettings(Map<String, dynamic> settings) async {
final db = await database;
return await db.update(
final result = await db.update(
appSettingsTable,
settings,
where: 'id = 1',
);
if (result > 0) {
_notifySettingsChanged(settings);
}
return result;
}
Future<int> setSetting(String key, dynamic value) async {
final db = await database;
return await db.update(
final result = await db.update(
appSettingsTable,
{key: value},
where: 'id = 1',
);
if (result > 0) {
final currentSettings = await getAllSettings();
if (currentSettings != null) {
_notifySettingsChanged(currentSettings);
}
}
return result;
}
Future<List<String>> getSearchOrderList() async {
@@ -292,6 +372,42 @@ class DatabaseService {
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 {
@@ -347,6 +463,11 @@ class DatabaseService {
}
});
final currentSettings = await getAllSettings();
if (currentSettings != null) {
_notifySettingsChanged(currentSettings);
}
return true;
} catch (e) {
return false;

View File

@@ -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 != "<NUL>" && !train.contains("-----");
final hasValidLoco = loco.isNotEmpty && loco != "<NUL>";
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<TrainRecord> filterUngroupableRecords(
List<TrainRecord> 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 = <String, List<TrainRecord>>{};
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<Object> 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);

View File

@@ -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"))
}

View File

@@ -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"

View File

@@ -2,7 +2,7 @@ name: lbjconsole
description: "LBJ Console"
# The following line prevents the package from being accidentally published to
# 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.
# 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
# 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.6.0-flutter+60
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:
@@ -73,7 +76,6 @@ dev_dependencies:
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
@@ -84,6 +86,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
@@ -125,4 +128,4 @@ msix_config:
publisher: CN=noxylva, O=noxylva.org, C=US
logo_path: assets/icon.png
capabilities: bluetooth,internetClient,location
certificate_path: keystore.jks
certificate_path: keystore.jks