Compare commits
7 Commits
v0.4.0-flu
...
v0.6.0-flu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5533df92b5 | ||
|
|
24c6abd4f3 | ||
|
|
5b3960f7d6 | ||
|
|
33e790957e | ||
|
|
88e3636c3f | ||
|
|
cc2a495984 | ||
|
|
6718ef7129 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
assets/* linguist-vendored
|
||||||
@@ -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`:包含车次类型信息,格式为 `正则表达式,车次类型`。
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ if (flutterVersionName == null) {
|
|||||||
android {
|
android {
|
||||||
namespace = "org.noxylva.lbjconsole.flutter"
|
namespace = "org.noxylva.lbjconsole.flutter"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
ndkVersion = "26.1.10909125"
|
ndkVersion = "28.1.13356709"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
|||||||
15050
assets/mapbox_map.html
vendored
Normal file
15050
assets/mapbox_map.html
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal 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:
|
||||||
@@ -18,11 +18,12 @@ class MergeSettings {
|
|||||||
final bool enabled;
|
final bool enabled;
|
||||||
final GroupBy groupBy;
|
final GroupBy groupBy;
|
||||||
final TimeWindow timeWindow;
|
final TimeWindow timeWindow;
|
||||||
|
final bool hideUngroupableRecords;
|
||||||
MergeSettings({
|
MergeSettings({
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.groupBy = GroupBy.trainAndLoco,
|
this.groupBy = GroupBy.trainAndLoco,
|
||||||
this.timeWindow = TimeWindow.unlimited,
|
this.timeWindow = TimeWindow.unlimited,
|
||||||
|
this.hideUngroupableRecords = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory MergeSettings.fromMap(Map<String, dynamic> map) {
|
factory MergeSettings.fromMap(Map<String, dynamic> map) {
|
||||||
@@ -36,6 +37,7 @@ class MergeSettings {
|
|||||||
(e) => e.name == map['timeWindow'],
|
(e) => e.name == map['timeWindow'],
|
||||||
orElse: () => TimeWindow.unlimited,
|
orElse: () => TimeWindow.unlimited,
|
||||||
),
|
),
|
||||||
|
hideUngroupableRecords: (map['hideUngroupableRecords'] ?? 0) == 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,132 @@ 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(() {
|
||||||
_displayItems.clear();
|
bool isMerge = false;
|
||||||
_displayItems.addAll(items);
|
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) {
|
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 +358,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 +400,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 +444,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 +545,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 +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) {
|
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 +608,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 +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 {
|
Future<void> _requestLocationPermission() async {
|
||||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||||
if (!serviceEnabled) {
|
if (!serviceEnabled) {
|
||||||
@@ -695,23 +683,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) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -731,10 +726,8 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
final isExpanded =
|
final isExpanded =
|
||||||
!isSubCard && (_expandedStates[record.uniqueId] ?? false);
|
!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),
|
||||||
@@ -762,7 +755,9 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
if (!_isEditMode) setEditMode(true);
|
if (!_isEditMode) {
|
||||||
|
setEditMode(true);
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedRecords.add(record.uniqueId);
|
_selectedRecords.add(record.uniqueId);
|
||||||
widget.onSelectionChanged();
|
widget.onSelectionChanged();
|
||||||
@@ -778,21 +773,6 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
_buildLocoInfo(record),
|
_buildLocoInfo(record),
|
||||||
if (isExpanded) _buildExpandedContent(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}) {
|
Widget _buildRecordHeader(TrainRecord record, {bool isMerged = false}) {
|
||||||
@@ -885,7 +865,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 +892,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 +985,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 +1008,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 +1134,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 +1169,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 +1278,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 +1316,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 +1354,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)))),
|
||||||
@@ -1383,7 +1385,7 @@ class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
|
|||||||
return FlutterMap(
|
return FlutterMap(
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
onPositionChanged: (position, hasGesture) => _onCameraMove(),
|
onPositionChanged: (position, hasGesture) => _onCameraMove(),
|
||||||
minZoom: 5,
|
minZoom: 8,
|
||||||
maxZoom: 18,
|
maxZoom: 18,
|
||||||
),
|
),
|
||||||
mapController: _mapController,
|
mapController: _mapController,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import 'package:lbjconsole/models/merged_record.dart';
|
|||||||
import 'package:lbjconsole/models/train_record.dart';
|
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/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';
|
||||||
@@ -174,6 +176,7 @@ class MainScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
|
String _mapType = 'webview';
|
||||||
|
|
||||||
late final BLEService _bleService;
|
late final BLEService _bleService;
|
||||||
final NotificationService _notificationService = NotificationService();
|
final NotificationService _notificationService = NotificationService();
|
||||||
@@ -181,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() {
|
||||||
@@ -195,6 +201,17 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
|||||||
_initializeServices();
|
_initializeServices();
|
||||||
_checkAndStartBackgroundService();
|
_checkAndStartBackgroundService();
|
||||||
_setupLastReceivedTimeListener();
|
_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 {
|
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
|
@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();
|
||||||
}
|
}
|
||||||
@@ -231,6 +258,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
|||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
_bleService.onAppResume();
|
_bleService.onAppResume();
|
||||||
|
_loadMapType();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,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),
|
||||||
),
|
),
|
||||||
@@ -380,8 +411,15 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
|||||||
onEditModeChanged: _handleHistoryEditModeChanged,
|
onEditModeChanged: _handleHistoryEditModeChanged,
|
||||||
onSelectionChanged: _handleSelectionChanged,
|
onSelectionChanged: _handleSelectionChanged,
|
||||||
),
|
),
|
||||||
const MapScreen(),
|
RealtimeScreen(
|
||||||
const SettingsScreen(),
|
key: _realtimeScreenKey,
|
||||||
|
),
|
||||||
|
_mapType == 'map' ? const MapScreen() : const MapWebViewScreen(),
|
||||||
|
SettingsScreen(
|
||||||
|
onSettingsChanged: () {
|
||||||
|
_loadMapType();
|
||||||
|
},
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -393,12 +431,18 @@ 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) {
|
||||||
|
_realtimeScreenKey.currentState?.loadRecords(scrollToTop: false);
|
||||||
|
}
|
||||||
|
if (_currentIndex == 3 && index == 2) {
|
||||||
|
_loadMapType();
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
if (_isHistoryEditMode) _isHistoryEditMode = false;
|
if (_isHistoryEditMode) _isHistoryEditMode = false;
|
||||||
_currentIndex = index;
|
_currentIndex = index;
|
||||||
@@ -407,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: '设置'),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
LatLng? _currentLocation;
|
LatLng? _currentLocation;
|
||||||
LatLng? _lastTrainLocation;
|
LatLng? _lastTrainLocation;
|
||||||
LatLng? _userLocation;
|
LatLng? _userLocation;
|
||||||
double _currentZoom = 12.0;
|
double _currentZoom = 14.0;
|
||||||
double _currentRotation = 0.0;
|
double _currentRotation = 0.0;
|
||||||
|
|
||||||
bool _isMapInitialized = false;
|
bool _isMapInitialized = false;
|
||||||
@@ -51,10 +51,19 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
_loadSettings().then((_) {
|
_loadSettings().then((_) {
|
||||||
_loadTrainRecords().then((_) {
|
_loadTrainRecords().then((_) {
|
||||||
_startLocationUpdates();
|
_startLocationUpdates();
|
||||||
|
if (!_isMapInitialized && (_currentLocation != null || _lastTrainLocation != null || _userLocation != null)) {
|
||||||
|
_initializeMapPosition();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _checkDatabaseSettings() async {
|
Future<void> _checkDatabaseSettings() async {
|
||||||
try {
|
try {
|
||||||
final dbInfo = await DatabaseService.instance.getDatabaseInfo();
|
final dbInfo = await DatabaseService.instance.getDatabaseInfo();
|
||||||
@@ -227,6 +236,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
settings['mapCenterLon'] = center.longitude;
|
settings['mapCenterLon'] = center.longitude;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
settings['mapSettingsTimestamp'] = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
await DatabaseService.instance.updateSettings(settings);
|
await DatabaseService.instance.updateSettings(settings);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
@@ -734,11 +745,31 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final bool isDefaultLocation = _currentLocation == null &&
|
||||||
|
_lastTrainLocation == null &&
|
||||||
|
_userLocation == null;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF121212),
|
backgroundColor: const Color(0xFF121212),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
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,
|
mapController: _mapController,
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
initialCenter: _currentLocation ??
|
initialCenter: _currentLocation ??
|
||||||
@@ -747,7 +778,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
const LatLng(39.9042, 116.4074),
|
const LatLng(39.9042, 116.4074),
|
||||||
initialZoom: _currentZoom,
|
initialZoom: _currentZoom,
|
||||||
initialRotation: _currentRotation,
|
initialRotation: _currentRotation,
|
||||||
minZoom: 4.0,
|
minZoom: 8.0,
|
||||||
maxZoom: 18.0,
|
maxZoom: 18.0,
|
||||||
onPositionChanged: (MapCamera camera, bool hasGesture) {
|
onPositionChanged: (MapCamera camera, bool hasGesture) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|||||||
1106
lib/screens/map_webview_screen.dart
Normal file
1106
lib/screens/map_webview_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
1310
lib/screens/realtime_screen.dart
Normal file
1310
lib/screens/realtime_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,9 @@ import 'package:share_plus/share_plus.dart';
|
|||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:cross_file/cross_file.dart';
|
||||||
|
|
||||||
class SettingsScreen extends StatefulWidget {
|
class SettingsScreen extends StatefulWidget {
|
||||||
const SettingsScreen({super.key});
|
final VoidCallback? onSettingsChanged;
|
||||||
|
|
||||||
|
const SettingsScreen({super.key, this.onSettingsChanged});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||||
@@ -33,8 +35,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
int _recordCount = 0;
|
int _recordCount = 0;
|
||||||
bool _mergeRecordsEnabled = false;
|
bool _mergeRecordsEnabled = false;
|
||||||
bool _hideTimeOnlyRecords = false;
|
bool _hideTimeOnlyRecords = false;
|
||||||
|
bool _hideUngroupableRecords = false;
|
||||||
GroupBy _groupBy = GroupBy.trainAndLoco;
|
GroupBy _groupBy = GroupBy.trainAndLoco;
|
||||||
TimeWindow _timeWindow = TimeWindow.unlimited;
|
TimeWindow _timeWindow = TimeWindow.unlimited;
|
||||||
|
String _mapType = 'map';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -63,8 +67,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_notificationsEnabled = (settingsMap['notificationEnabled'] ?? 1) == 1;
|
_notificationsEnabled = (settingsMap['notificationEnabled'] ?? 1) == 1;
|
||||||
_mergeRecordsEnabled = settings.enabled;
|
_mergeRecordsEnabled = settings.enabled;
|
||||||
_hideTimeOnlyRecords = (settingsMap['hideTimeOnlyRecords'] ?? 0) == 1;
|
_hideTimeOnlyRecords = (settingsMap['hideTimeOnlyRecords'] ?? 0) == 1;
|
||||||
|
_hideUngroupableRecords = settings.hideUngroupableRecords;
|
||||||
_groupBy = settings.groupBy;
|
_groupBy = settings.groupBy;
|
||||||
_timeWindow = settings.timeWindow;
|
_timeWindow = settings.timeWindow;
|
||||||
|
_mapType = settingsMap['mapType']?.toString() ?? 'webview';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,9 +91,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
'notificationEnabled': _notificationsEnabled ? 1 : 0,
|
'notificationEnabled': _notificationsEnabled ? 1 : 0,
|
||||||
'mergeRecordsEnabled': _mergeRecordsEnabled ? 1 : 0,
|
'mergeRecordsEnabled': _mergeRecordsEnabled ? 1 : 0,
|
||||||
'hideTimeOnlyRecords': _hideTimeOnlyRecords ? 1 : 0,
|
'hideTimeOnlyRecords': _hideTimeOnlyRecords ? 1 : 0,
|
||||||
|
'hideUngroupableRecords': _hideUngroupableRecords ? 1 : 0,
|
||||||
'groupBy': _groupBy.name,
|
'groupBy': _groupBy.name,
|
||||||
'timeWindow': _timeWindow.name,
|
'timeWindow': _timeWindow.name,
|
||||||
|
'mapType': _mapType,
|
||||||
});
|
});
|
||||||
|
widget.onSettingsChanged?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -240,6 +249,43 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
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(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -398,6 +444,29 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
dropdownColor: AppTheme.secondaryBlack,
|
dropdownColor: AppTheme.secondaryBlack,
|
||||||
style: AppTheme.bodyMedium,
|
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: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.storage, color: Theme.of(context).colorScheme.primary),
|
Icon(Icons.storage,
|
||||||
|
color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text('数据管理', style: AppTheme.titleMedium),
|
Text('数据管理', style: AppTheme.titleMedium),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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 = 4;
|
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';
|
||||||
@@ -21,21 +21,47 @@ class DatabaseService {
|
|||||||
Database? _database;
|
Database? _database;
|
||||||
|
|
||||||
Future<Database> get database async {
|
Future<Database> get database async {
|
||||||
if (_database != null) return _database!;
|
try {
|
||||||
_database = await _initDatabase();
|
if (_database != null) {
|
||||||
return _database!;
|
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 {
|
Future<Database> _initDatabase() async {
|
||||||
final directory = await getApplicationDocumentsDirectory();
|
try {
|
||||||
final path = join(directory.path, _databaseName);
|
final directory = await getApplicationDocumentsDirectory();
|
||||||
|
final path = join(directory.path, _databaseName);
|
||||||
|
|
||||||
return await openDatabase(
|
final db = await openDatabase(
|
||||||
path,
|
path,
|
||||||
version: _databaseVersion,
|
version: _databaseVersion,
|
||||||
onCreate: _onCreate,
|
onCreate: _onCreate,
|
||||||
onUpgrade: _onUpgrade,
|
onUpgrade: _onUpgrade,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return db;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
||||||
@@ -51,8 +77,19 @@ class DatabaseService {
|
|||||||
try {
|
try {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'ALTER TABLE $appSettingsTable ADD COLUMN mapTimeFilter TEXT NOT NULL DEFAULT "unlimited"');
|
'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,
|
mapZoomLevel REAL NOT NULL DEFAULT 10.0,
|
||||||
mapRailwayLayerVisible INTEGER NOT NULL DEFAULT 1,
|
mapRailwayLayerVisible INTEGER NOT NULL DEFAULT 1,
|
||||||
mapRotation REAL NOT NULL DEFAULT 0.0,
|
mapRotation REAL NOT NULL DEFAULT 0.0,
|
||||||
|
mapType TEXT NOT NULL DEFAULT 'webview',
|
||||||
specifiedDeviceAddress TEXT,
|
specifiedDeviceAddress TEXT,
|
||||||
searchOrderList TEXT NOT NULL DEFAULT '',
|
searchOrderList TEXT NOT NULL DEFAULT '',
|
||||||
autoConnectEnabled INTEGER NOT NULL DEFAULT 1,
|
autoConnectEnabled INTEGER NOT NULL DEFAULT 1,
|
||||||
@@ -101,7 +139,9 @@ class DatabaseService {
|
|||||||
hideTimeOnlyRecords INTEGER NOT NULL DEFAULT 0,
|
hideTimeOnlyRecords INTEGER NOT NULL DEFAULT 0,
|
||||||
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,
|
||||||
|
mapSettingsTimestamp INTEGER
|
||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
|
|
||||||
@@ -118,6 +158,7 @@ class DatabaseService {
|
|||||||
'mapZoomLevel': 10.0,
|
'mapZoomLevel': 10.0,
|
||||||
'mapRailwayLayerVisible': 1,
|
'mapRailwayLayerVisible': 1,
|
||||||
'mapRotation': 0.0,
|
'mapRotation': 0.0,
|
||||||
|
'mapType': 'webview',
|
||||||
'searchOrderList': '',
|
'searchOrderList': '',
|
||||||
'autoConnectEnabled': 1,
|
'autoConnectEnabled': 1,
|
||||||
'backgroundServiceEnabled': 0,
|
'backgroundServiceEnabled': 0,
|
||||||
@@ -127,6 +168,8 @@ class DatabaseService {
|
|||||||
'groupBy': 'trainAndLoco',
|
'groupBy': 'trainAndLoco',
|
||||||
'timeWindow': 'unlimited',
|
'timeWindow': 'unlimited',
|
||||||
'mapTimeFilter': 'unlimited',
|
'mapTimeFilter': 'unlimited',
|
||||||
|
'hideUngroupableRecords': 0,
|
||||||
|
'mapSettingsTimestamp': null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,12 +183,18 @@ class DatabaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<TrainRecord>> getAllRecords() async {
|
Future<List<TrainRecord>> getAllRecords() async {
|
||||||
final db = await database;
|
try {
|
||||||
final result = await db.query(
|
final db = await database;
|
||||||
trainRecordsTable,
|
final result = await db.query(
|
||||||
orderBy: 'timestamp DESC',
|
trainRecordsTable,
|
||||||
);
|
orderBy: 'timestamp DESC',
|
||||||
return result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
|
);
|
||||||
|
final records =
|
||||||
|
result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
|
||||||
|
return records;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<TrainRecord>> getRecordsWithinTimeRange(Duration duration) async {
|
Future<List<TrainRecord>> getRecordsWithinTimeRange(Duration duration) async {
|
||||||
@@ -162,29 +211,49 @@ class DatabaseService {
|
|||||||
|
|
||||||
Future<List<TrainRecord>> getRecordsWithinReceivedTimeRange(
|
Future<List<TrainRecord>> getRecordsWithinReceivedTimeRange(
|
||||||
Duration duration) async {
|
Duration duration) async {
|
||||||
final db = await database;
|
try {
|
||||||
final cutoffTime = DateTime.now().subtract(duration).millisecondsSinceEpoch;
|
final db = await database;
|
||||||
final result = await db.query(
|
final cutoffTime =
|
||||||
trainRecordsTable,
|
DateTime.now().subtract(duration).millisecondsSinceEpoch;
|
||||||
where: 'receivedTimestamp >= ?',
|
|
||||||
whereArgs: [cutoffTime],
|
final result = await db.query(
|
||||||
orderBy: 'receivedTimestamp DESC',
|
trainRecordsTable,
|
||||||
);
|
where: 'receivedTimestamp >= ?',
|
||||||
return result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
|
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 {
|
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 {
|
||||||
@@ -222,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 {
|
||||||
@@ -292,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 {
|
||||||
@@ -347,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;
|
||||||
|
|||||||
@@ -2,6 +2,37 @@ import 'package:lbjconsole/models/train_record.dart';
|
|||||||
import 'package:lbjconsole/models/merged_record.dart';
|
import 'package:lbjconsole/models/merged_record.dart';
|
||||||
|
|
||||||
class MergeService {
|
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) {
|
static String? _generateGroupKey(TrainRecord record, GroupBy groupBy) {
|
||||||
final train = record.train.trim();
|
final train = record.train.trim();
|
||||||
final loco = record.loco.trim();
|
final loco = record.loco.trim();
|
||||||
@@ -36,15 +67,19 @@ class MergeService {
|
|||||||
return allRecords;
|
return allRecords;
|
||||||
}
|
}
|
||||||
|
|
||||||
allRecords
|
final filteredRecords = filterUngroupableRecords(
|
||||||
|
allRecords, settings.groupBy, settings.hideUngroupableRecords);
|
||||||
|
|
||||||
|
filteredRecords
|
||||||
.sort((a, b) => b.receivedTimestamp.compareTo(a.receivedTimestamp));
|
.sort((a, b) => b.receivedTimestamp.compareTo(a.receivedTimestamp));
|
||||||
|
|
||||||
if (settings.groupBy == GroupBy.trainOrLoco) {
|
if (settings.groupBy == GroupBy.trainOrLoco) {
|
||||||
return _groupByTrainOrLocoWithTimeWindow(allRecords, settings.timeWindow);
|
return _groupByTrainOrLocoWithTimeWindow(
|
||||||
|
filteredRecords, settings.timeWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
final groupedRecords = <String, List<TrainRecord>>{};
|
final groupedRecords = <String, List<TrainRecord>>{};
|
||||||
for (final record in allRecords) {
|
for (final record in filteredRecords) {
|
||||||
final key = _generateGroupKey(record, settings.groupBy);
|
final key = _generateGroupKey(record, settings.groupBy);
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
groupedRecords.putIfAbsent(key, () => []).add(record);
|
groupedRecords.putIfAbsent(key, () => []).add(record);
|
||||||
@@ -79,8 +114,9 @@ class MergeService {
|
|||||||
final reusedRecords = _reuseDiscardedRecords(
|
final reusedRecords = _reuseDiscardedRecords(
|
||||||
discardedRecords, mergedRecordIds, settings.groupBy);
|
discardedRecords, mergedRecordIds, settings.groupBy);
|
||||||
|
|
||||||
final singleRecords =
|
final singleRecords = filteredRecords
|
||||||
allRecords.where((r) => !mergedRecordIds.contains(r.uniqueId)).toList();
|
.where((r) => !mergedRecordIds.contains(r.uniqueId))
|
||||||
|
.toList();
|
||||||
|
|
||||||
final List<Object> mixedList = [...mergedRecords, ...singleRecords];
|
final List<Object> mixedList = [...mergedRecords, ...singleRecords];
|
||||||
mixedList.sort((a, b) {
|
mixedList.sort((a, b) {
|
||||||
@@ -219,7 +255,6 @@ class MergeService {
|
|||||||
latestRecord: processedGroup.first,
|
latestRecord: processedGroup.first,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
// 处理被丢弃的记录
|
|
||||||
for (final record in group) {
|
for (final record in group) {
|
||||||
if (!processedGroup.contains(record)) {
|
if (!processedGroup.contains(record)) {
|
||||||
singleRecords.add(record);
|
singleRecords.add(record);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import share_plus
|
|||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
import webview_flutter_wkwebview
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
@@ -27,4 +28,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
100
pubspec.lock
100
pubspec.lock
@@ -233,6 +233,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.11"
|
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:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -656,6 +664,30 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
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:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -896,6 +928,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
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:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1261,6 +1301,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.1"
|
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:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1269,6 +1317,22 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
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:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1309,6 +1373,38 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
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:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1350,5 +1446,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.0-0 <4.0.0"
|
dart: ">=3.9.0 <4.0.0"
|
||||||
flutter: ">=3.24.0"
|
flutter: ">=3.35.0"
|
||||||
|
|||||||
11
pubspec.yaml
11
pubspec.yaml
@@ -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.4.0-flutter+40
|
version: 0.6.0-flutter+60
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
@@ -54,6 +54,9 @@ dependencies:
|
|||||||
msix: ^3.16.12
|
msix: ^3.16.12
|
||||||
flutter_background_service: ^5.1.0
|
flutter_background_service: ^5.1.0
|
||||||
scrollview_observer: ^1.20.0
|
scrollview_observer: ^1.20.0
|
||||||
|
vector_map_tiles: ^8.0.0
|
||||||
|
maplibre_gl: ^0.22.0
|
||||||
|
webview_flutter: ^4.8.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -73,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.
|
||||||
@@ -84,6 +86,7 @@ flutter:
|
|||||||
- assets/loco_info.csv
|
- assets/loco_info.csv
|
||||||
- assets/train_number_info.csv
|
- assets/train_number_info.csv
|
||||||
- assets/loco_type_info.csv
|
- assets/loco_type_info.csv
|
||||||
|
- assets/mapbox_map.html
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
@@ -125,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
|
||||||
|
|||||||
Reference in New Issue
Block a user