18 Commits

Author SHA1 Message Date
Nedifinita
6073ea615e fix: solve the problem that errors may occur during merging 2025-10-14 23:55:42 +08:00
Nedifinita
5533df92b5 feat: add train location tracking functionality 2025-10-12 21:42:01 +08:00
Nedifinita
24c6abd4f3 Merge branch 'flutter' of https://github.com/undef-i/LBJ_Console into flutter 2025-10-08 22:35:47 +08:00
Nedifinita
5b3960f7d6 fix: correct the scrolling status error when adding new records to the merged record group 2025-10-08 22:35:43 +08:00
undef-i
33e790957e Add .gitattributes file 2025-09-30 01:11:08 +08:00
undef-i
88e3636c3f Create .gitattributes 2025-09-30 01:10:34 +08:00
Nedifinita
cc2a495984 feat: improve map initialization process and communication 2025-09-29 20:17:05 +08:00
Nedifinita
6718ef7129 feat: add vector railway map 2025-09-29 18:44:15 +08:00
Nedifinita
bfd05bd249 feat: implement time window algorithm 2025-09-27 01:03:03 +08:00
Nedifinita
8d3366fbf9 refactor 2025-09-27 00:50:12 +08:00
Nedifinita
9b0e9dcacf fix: resolve issue with incorrect deletion of single record 2025-09-27 00:31:51 +08:00
Nedifinita
c3e97332fd feat: add map time filtering function and optimized location processing 2025-09-27 00:14:24 +08:00
Nedifinita
b1d8d5e029 feat: improve record grouping and display 2025-09-26 21:21:37 +08:00
Nedifinita
77501af2f5 fix: update icon 2025-09-26 21:02:14 +08:00
Nedifinita
64401a6ce9 feat: add the function to hide records that are only valid for time. 2025-09-25 22:52:19 +08:00
Nedifinita
72f9dfe17b fix: repair record processing logic 2025-09-25 22:25:51 +08:00
Nedifinita
bf850eed38 refactor: optimize record card rendering logic and animation effects 2025-09-25 22:01:25 +08:00
Nedifinita
56689fc993 feat: optimize record list 2025-09-25 21:45:52 +08:00
59 changed files with 19216 additions and 551 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#000000</color>
</resources>

BIN
assets/icon.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 209 KiB

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

@@ -427,7 +427,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -484,7 +484,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";

View File

@@ -1,122 +1 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

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

@@ -149,7 +149,7 @@ class TrainRecord {
final lbjClassValue = lbjClass.trim();
final trainValue = train.trim();
if (trainValue == "<NUL>") {
if (trainValue == "<NUL>" || trainValue.contains("-----")) {
return "";
}

File diff suppressed because it is too large Load Diff

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,11 +431,17 @@ 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) {
_historyScreenKey.currentState?.loadRecords();
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;
@@ -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

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math' show sin, cos, sqrt, atan2, pi;
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
@@ -21,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;
@@ -29,14 +30,82 @@ class _MapScreenState extends State<MapScreen> {
bool _isLocationPermissionGranted = false;
Timer? _locationTimer;
String _selectedTimeFilter = 'unlimited';
final Map<String, Duration> _timeFilterOptions = {
'unlimited': Duration.zero,
'1hour': Duration(hours: 1),
'6hours': Duration(hours: 6),
'12hours': Duration(hours: 12),
'24hours': Duration(hours: 24),
'7days': Duration(days: 7),
'30days': Duration(days: 30),
};
@override
void initState() {
super.initState();
_initializeMap();
_loadTrainRecords();
_checkDatabaseSettings();
_loadSettings().then((_) {
_loadTrainRecords().then((_) {
_startLocationUpdates();
if (!_isMapInitialized && (_currentLocation != null || _lastTrainLocation != null || _userLocation != null)) {
_initializeMapPosition();
}
});
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_loadSettings();
_startLocationUpdates();
}
Future<void> _checkDatabaseSettings() async {
try {
final dbInfo = await DatabaseService.instance.getDatabaseInfo();
final settings = await DatabaseService.instance.getAllSettings();
if (settings != null) {
final lat = settings['mapCenterLat'];
final lon = settings['mapCenterLon'];
if (lat != null && lon != null) {
if (lat == 39.9042 && lon == 116.4074) {
} else if (lat == 0.0 && lon == 0.0) {
} else {
final beijingLat = 39.9042;
final beijingLon = 116.4074;
final distance =
_calculateDistance(lat, lon, beijingLat, beijingLon);
if (distance < 50) {}
}
}
}
} catch (e) {}
}
double _calculateDistance(
double lat1, double lon1, double lat2, double lon2) {
const earthRadius = 6371;
final dLat = _degreesToRadians(lat2 - lat1);
final dLon = _degreesToRadians(lon2 - lon1);
final a = sin(dLat / 2) * sin(dLat / 2) +
cos(_degreesToRadians(lat1)) *
cos(_degreesToRadians(lat2)) *
sin(dLon / 2) *
sin(dLon / 2);
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
return earthRadius * c;
}
double _degreesToRadians(double degrees) {
return degrees * pi / 180;
}
@override
@@ -80,19 +149,23 @@ class _MapScreenState extends State<MapScreen> {
try {
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
forceAndroidLocationManager: true,
);
final newLocation = LatLng(position.latitude, position.longitude);
setState(() {
_userLocation = LatLng(position.latitude, position.longitude);
_userLocation = newLocation;
});
} catch (e) {
}
if (!_isMapInitialized) {
_initializeMapPosition();
}
} catch (e) {}
}
void _startLocationUpdates() {
_requestLocationPermission();
_locationTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
if (_isLocationPermissionGranted) {
_getCurrentLocation();
@@ -101,24 +174,22 @@ class _MapScreenState extends State<MapScreen> {
}
Future<void> _forceUpdateLocation() async {
try {
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.best,
forceAndroidLocationManager: true,
);
final newLocation = LatLng(position.latitude, position.longitude);
setState(() {
_userLocation = newLocation;
});
_mapController.move(newLocation, 15.0);
} catch (e) {
}
} catch (e) {}
}
Future<void> _loadSettings() async {
try {
final settings = await DatabaseService.instance.getAllSettings();
@@ -129,14 +200,19 @@ class _MapScreenState extends State<MapScreen> {
_currentZoom = (settings['mapZoomLevel'] as num?)?.toDouble() ?? 10.0;
_currentRotation =
(settings['mapRotation'] as num?)?.toDouble() ?? 0.0;
_selectedTimeFilter =
settings['mapTimeFilter'] as String? ?? 'unlimited';
final lat = (settings['mapCenterLat'] as num?)?.toDouble();
final lon = (settings['mapCenterLon'] as num?)?.toDouble();
if (lat != null && lon != null) {
if (lat != null && lon != null && lat != 0.0 && lon != 0.0) {
_currentLocation = LatLng(lat, lon);
}
});
if (!_isMapInitialized) {
_initializeMapPosition();
}
}
} catch (e) {}
}
@@ -144,20 +220,32 @@ class _MapScreenState extends State<MapScreen> {
Future<void> _saveSettings() async {
try {
final center = _mapController.camera.center;
await DatabaseService.instance.updateSettings({
final isDefaultLocation =
center.latitude == 39.9042 && center.longitude == 116.4074;
final settings = {
'mapRailwayLayerVisible': _railwayLayerVisible ? 1 : 0,
'mapZoomLevel': _currentZoom,
'mapCenterLat': center.latitude,
'mapCenterLon': center.longitude,
'mapRotation': _currentRotation,
});
'mapTimeFilter': _selectedTimeFilter,
};
if (!isDefaultLocation) {
settings['mapCenterLat'] = center.latitude;
settings['mapCenterLon'] = center.longitude;
}
settings['mapSettingsTimestamp'] = DateTime.now().millisecondsSinceEpoch;
await DatabaseService.instance.updateSettings(settings);
} catch (e) {}
}
Future<void> _loadTrainRecords() async {
setState(() => _isLoading = true);
try {
final records = await DatabaseService.instance.getAllRecords();
final records = await _getFilteredRecords();
setState(() {
_trainRecords.clear();
_trainRecords.addAll(records);
@@ -175,13 +263,28 @@ class _MapScreenState extends State<MapScreen> {
}
}
_initializeMapPosition();
if (!_isMapInitialized) {
_initializeMapPosition();
}
});
} catch (e) {
setState(() => _isLoading = false);
}
}
Future<List<TrainRecord>> _getFilteredRecords() async {
if (_selectedTimeFilter == 'unlimited') {
return await DatabaseService.instance.getAllRecords();
} else {
final duration = _timeFilterOptions[_selectedTimeFilter];
if (duration != null && duration != Duration.zero) {
return await DatabaseService.instance
.getRecordsWithinReceivedTimeRange(duration);
}
return await DatabaseService.instance.getAllRecords();
}
}
void _initializeMapPosition() {
if (_isMapInitialized) return;
@@ -189,21 +292,21 @@ class _MapScreenState extends State<MapScreen> {
if (_currentLocation != null) {
targetLocation = _currentLocation;
} else if (_userLocation != null) {
targetLocation = _userLocation;
} else if (_lastTrainLocation != null) {
targetLocation = _lastTrainLocation;
} else if (_userLocation != null) {
targetLocation = _userLocation;
} else {
_isMapInitialized = true;
return;
targetLocation = const LatLng(39.9042, 116.4074);
}
_centerMap(targetLocation!, zoom: _currentZoom);
_centerMap(targetLocation!, zoom: _currentZoom, rotation: _currentRotation);
_isMapInitialized = true;
}
void _centerMap(LatLng location, {double? zoom}) {
void _centerMap(LatLng location, {double? zoom, double? rotation}) {
_mapController.move(location, zoom ?? _currentZoom);
_mapController.rotate(rotation ?? _currentRotation);
}
LatLng? _parseDmsCoordinate(String? positionInfo) {
@@ -228,9 +331,7 @@ class _MapScreenState extends State<MapScreen> {
return LatLng(lat, lng);
}
}
} catch (e) {
print('解析DMS坐标失败: $e');
}
} catch (e) {}
return null;
}
@@ -294,41 +395,27 @@ class _MapScreenState extends State<MapScreen> {
Marker(
point: position,
width: 80,
height: 60,
height: 16,
child: GestureDetector(
onTap: () => position != null
? _showTrainDetailsDialog(record, position)
: null,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(18),
border: Border.all(color: Colors.white, width: 2),
),
child: const Icon(
Icons.train,
color: Colors.white,
size: 18,
),
),
const SizedBox(height: 2),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(2),
color: Colors.black.withOpacity(0.8),
borderRadius: BorderRadius.circular(3),
),
child: Text(
trainDisplay,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontSize: 8,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
@@ -347,7 +434,9 @@ class _MapScreenState extends State<MapScreen> {
}
void _centerToMyLocation() {
_centerMap(_lastTrainLocation ?? const LatLng(39.9042, 116.4074), zoom: 15.0);
if (_userLocation != null) {
_centerMap(_userLocation!, zoom: 15.0, rotation: _currentRotation);
}
}
void _centerToLastTrain() {
@@ -364,11 +453,73 @@ class _MapScreenState extends State<MapScreen> {
}
if (targetPosition != null) {
_centerMap(targetPosition, zoom: 15.0);
_centerMap(targetPosition, zoom: 15.0, rotation: _currentRotation);
}
}
}
void _showTimeFilterDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('时间筛选'),
content: SizedBox(
width: double.minPositive,
child: Column(
mainAxisSize: MainAxisSize.min,
children: _timeFilterOptions.keys.map((key) {
return RadioListTile<String>(
title: Text(_getTimeFilterLabel(key)),
value: key,
groupValue: _selectedTimeFilter,
onChanged: (String? value) {
if (value != null) {
setState(() {
_selectedTimeFilter = value;
});
_loadTrainRecords();
Navigator.pop(context);
}
},
contentPadding: EdgeInsets.zero,
dense: true,
);
}).toList(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
],
);
},
);
}
String _getTimeFilterLabel(String key) {
switch (key) {
case 'unlimited':
return '全部时间';
case '1hour':
return '最近1小时';
case '6hours':
return '最近6小时';
case '12hours':
return '最近12小时';
case '24hours':
return '最近24小时';
case '7days':
return '最近7天';
case '30days':
return '最近30天';
default:
return '未知';
}
}
void _showTrainDetailsDialog(TrainRecord record, LatLng position) {
showModalBottomSheet(
context: context,
@@ -429,16 +580,26 @@ class _MapScreenState extends State<MapScreen> {
child: Column(
children: [
_buildMaterial3DetailRow(
context, "时间", record.formattedTime),
context, "时间", _getDisplayTime(record)),
_buildMaterial3DetailRow(
context, "日期", record.formattedDate),
context, "日期", _getDisplayDate(record)),
_buildMaterial3DetailRow(
context, "类型", record.trainType),
_buildMaterial3DetailRow(context, "速度",
"${record.speed.replaceAll(' ', '')} km/h"),
_buildMaterial3DetailRow(
context, "速度", "${record.speed.replaceAll(' ', '')} km/h"),
context,
"位置",
record.position.trim().endsWith('.')
? '${record.position.trim().substring(0, record.position.trim().length - 1)}K'
: '${record.position.trim()}K'),
_buildMaterial3DetailRow(
context, "位置", record.position.trim().endsWith('.') ? '${record.position.trim().substring(0, record.position.trim().length - 1)}K' : '${record.position.trim()}K'),
_buildMaterial3DetailRow(context, "路线", record.route.trim().endsWith('.') ? record.route.trim().substring(0, record.route.trim().length - 1) : record.route.trim()),
context,
"路线",
record.route.trim().endsWith('.')
? record.route.trim().substring(
0, record.route.trim().length - 1)
: record.route.trim()),
_buildMaterial3DetailRow(
context, "机车", "${record.locoType}-${record.loco}"),
_buildMaterial3DetailRow(context, "坐标",
@@ -461,7 +622,8 @@ class _MapScreenState extends State<MapScreen> {
child: FilledButton(
onPressed: () {
Navigator.pop(context);
_centerMap(position, zoom: 17.0);
_centerMap(position,
zoom: 17.0, rotation: _currentRotation);
},
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -507,6 +669,25 @@ class _MapScreenState extends State<MapScreen> {
);
}
String _getDisplayTime(TrainRecord record) {
if (record.time == "<NUL>" || record.time.isEmpty) {
final receivedTime = record.receivedTimestamp;
return '${receivedTime.hour.toString().padLeft(2, '0')}:${receivedTime.minute.toString().padLeft(2, '0')}:${receivedTime.second.toString().padLeft(2, '0')}';
} else {
return record.time.split("\n")[0];
}
}
String _getDisplayDate(TrainRecord record) {
if (record.time == "<NUL>" || record.time.isEmpty) {
final receivedTime = record.receivedTimestamp;
return '${receivedTime.year}-${receivedTime.month.toString().padLeft(2, '0')}-${receivedTime.day.toString().padLeft(2, '0')}';
} else {
final now = DateTime.now();
return '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
}
}
Widget _buildMaterial3DetailRow(
BuildContext context, String label, String value) {
return Padding(
@@ -546,46 +727,67 @@ class _MapScreenState extends State<MapScreen> {
markers.add(
Marker(
point: _userLocation!,
width: 40,
height: 40,
width: 24,
height: 24,
child: Container(
decoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white, width: 1),
),
child: const Icon(
Icons.my_location,
color: Colors.white,
size: 20,
size: 12,
),
),
),
);
}
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: _lastTrainLocation ?? const LatLng(39.9042, 116.4074),
initialCenter: _currentLocation ??
_lastTrainLocation ??
_userLocation ??
const LatLng(39.9042, 116.4074),
initialZoom: _currentZoom,
initialRotation: _currentRotation,
minZoom: 4.0,
minZoom: 8.0,
maxZoom: 18.0,
onPositionChanged: (MapCamera camera, bool hasGesture) {
if (hasGesture) {
setState(() {
_currentLocation = camera.center;
_currentZoom = camera.zoom;
_currentRotation = camera.rotation;
});
_saveSettings();
}
setState(() {
_currentLocation = camera.center;
_currentZoom = camera.zoom;
_currentRotation = camera.rotation;
});
_saveSettings();
},
),
children: [
@@ -616,6 +818,13 @@ class _MapScreenState extends State<MapScreen> {
top: 40,
child: Column(
children: [
FloatingActionButton.small(
heroTag: 'timeFilter',
backgroundColor: const Color(0xFF1E1E1E),
onPressed: _showTimeFilterDialog,
child: const Icon(Icons.filter_list, color: Colors.white),
),
const SizedBox(height: 8),
FloatingActionButton.small(
heroTag: 'railwayLayer',
backgroundColor: const Color(0xFF1E1E1E),

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();
@@ -32,8 +34,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
bool _notificationsEnabled = true;
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() {
@@ -61,8 +66,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
(settingsMap['backgroundServiceEnabled'] ?? 0) == 1;
_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';
});
}
}
@@ -82,9 +90,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
'backgroundServiceEnabled': _backgroundServiceEnabled ? 1 : 0,
'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
@@ -236,6 +248,66 @@ 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: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('隐藏只有时间有效的记录', style: AppTheme.bodyLarge),
Text('不显示只有时间信息的记录', style: AppTheme.caption),
],
),
Switch(
value: _hideTimeOnlyRecords,
onChanged: (value) {
setState(() {
_hideTimeOnlyRecords = value;
});
_saveSettings();
},
activeColor: Theme.of(context).colorScheme.primary,
),
],
),
],
),
),
@@ -372,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,
),
],
),
],
),
),
@@ -395,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

@@ -20,9 +20,9 @@ class BackgroundService {
if (_isInitialized) return;
final service = FlutterBackgroundService();
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
if (Platform.isAndroid) {
const AndroidNotificationChannel channel = AndroidNotificationChannel(
_notificationChannelId,
@@ -34,10 +34,12 @@ class BackgroundService {
playSound: false,
);
await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(channel);
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
}
await service.configure(
androidConfiguration: AndroidConfiguration(
onStart: _onStart,
@@ -81,8 +83,9 @@ class BackgroundService {
if (service is AndroidServiceInstance) {
await Future.delayed(const Duration(seconds: 1));
if (await service.isForegroundService()) {
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
final flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
try {
const AndroidNotificationChannel channel = AndroidNotificationChannel(
_notificationChannelId,
@@ -94,8 +97,10 @@ class BackgroundService {
playSound: false,
);
await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(channel);
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
await flutterLocalNotificationsPlugin.show(
_notificationId,
@@ -122,10 +127,7 @@ class BackgroundService {
),
),
);
print('前台服务通知显示成功');
} catch (e) {
print('前台服务通知显示失败: $e');
}
} catch (e) {}
}
}
@@ -136,8 +138,9 @@ class BackgroundService {
final bleService = BLEService();
final isConnected = bleService.isConnected;
final deviceStatus = bleService.deviceStatus;
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
final flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
await flutterLocalNotificationsPlugin.show(
_notificationId,
'LBJ Console',
@@ -163,9 +166,7 @@ class BackgroundService {
),
),
);
} catch (e) {
print('前台服务通知更新失败: $e');
}
} catch (e) {}
}
}
});
@@ -179,7 +180,7 @@ class BackgroundService {
static Future<void> startService() async {
await initialize();
final service = FlutterBackgroundService();
if (Platform.isAndroid) {
final isRunning = await service.isRunning();
if (!isRunning) {
@@ -208,4 +209,4 @@ class BackgroundService {
service.invoke('setAsBackground');
}
}
}
}

View File

@@ -319,6 +319,10 @@ class BLEService {
'${now.millisecondsSinceEpoch}_${Random().nextInt(9999)}';
recordData['receivedTimestamp'] = now.millisecondsSinceEpoch;
if (!recordData.containsKey('timestamp')) {
recordData['timestamp'] = now.millisecondsSinceEpoch;
}
_lastReceivedTime = now;
_lastReceivedTimeController.add(_lastReceivedTime);
@@ -326,9 +330,7 @@ class BLEService {
_dataController.add(trainRecord);
DatabaseService.instance.insertRecord(trainRecord);
}
} catch (e) {
print("$TAG: JSON Decode Error: $e, Data: $jsonData");
}
} catch (e) {}
}
void _updateConnectionState(bool connected, String status) {

View File

@@ -13,7 +13,7 @@ class DatabaseService {
DatabaseService._internal();
static const String _databaseName = 'train_database';
static const _databaseVersion = 1;
static const _databaseVersion = 7;
static const String trainRecordsTable = 'train_records';
static const String appSettingsTable = 'app_settings';
@@ -21,20 +21,76 @@ 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,
);
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 {
if (oldVersion < 2) {
await db.execute(
'ALTER TABLE $appSettingsTable ADD COLUMN hideTimeOnlyRecords INTEGER NOT NULL DEFAULT 0');
}
if (oldVersion < 3) {
await db.execute(
'ALTER TABLE $appSettingsTable ADD COLUMN mapTimeFilter TEXT NOT NULL DEFAULT "unlimited"');
}
if (oldVersion < 4) {
try {
await db.execute(
'ALTER TABLE $appSettingsTable ADD COLUMN mapTimeFilter TEXT NOT NULL DEFAULT "unlimited"');
} 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');
}
}
Future<void> _onCreate(Database db, int version) async {
@@ -73,14 +129,19 @@ 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,
backgroundServiceEnabled INTEGER NOT NULL DEFAULT 0,
notificationEnabled INTEGER NOT NULL DEFAULT 0,
mergeRecordsEnabled INTEGER NOT NULL DEFAULT 0,
hideTimeOnlyRecords INTEGER NOT NULL DEFAULT 0,
groupBy TEXT NOT NULL DEFAULT 'trainAndLoco',
timeWindow TEXT NOT NULL DEFAULT 'unlimited'
timeWindow TEXT NOT NULL DEFAULT 'unlimited',
mapTimeFilter TEXT NOT NULL DEFAULT 'unlimited',
hideUngroupableRecords INTEGER NOT NULL DEFAULT 0,
mapSettingsTimestamp INTEGER
)
''');
@@ -97,13 +158,18 @@ class DatabaseService {
'mapZoomLevel': 10.0,
'mapRailwayLayerVisible': 1,
'mapRotation': 0.0,
'mapType': 'webview',
'searchOrderList': '',
'autoConnectEnabled': 1,
'backgroundServiceEnabled': 0,
'notificationEnabled': 0,
'mergeRecordsEnabled': 0,
'hideTimeOnlyRecords': 0,
'groupBy': 'trainAndLoco',
'timeWindow': 'unlimited',
'mapTimeFilter': 'unlimited',
'hideUngroupableRecords': 0,
'mapSettingsTimestamp': null,
});
}
@@ -117,26 +183,77 @@ class DatabaseService {
}
Future<List<TrainRecord>> getAllRecords() async {
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 {
final db = await database;
final cutoffTime = DateTime.now().subtract(duration).millisecondsSinceEpoch;
final result = await db.query(
trainRecordsTable,
where: 'timestamp >= ?',
whereArgs: [cutoffTime],
orderBy: 'timestamp DESC',
);
return result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
}
Future<List<TrainRecord>> getRecordsWithinReceivedTimeRange(
Duration duration) async {
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 {
@@ -174,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 {
@@ -244,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 {
@@ -299,6 +463,11 @@ class DatabaseService {
}
});
final currentSettings = await getAllSettings();
if (currentSettings != null) {
_notifySettingsChanged(currentSettings);
}
return true;
} catch (e) {
return false;

View File

@@ -0,0 +1,84 @@
import 'dart:async';
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
class LocationService {
static final LocationService _instance = LocationService._internal();
factory LocationService() => _instance;
LocationService._internal();
static LocationService get instance => _instance;
LatLng? _currentLocation;
Timer? _locationTimer;
bool _isLocationPermissionGranted = false;
final StreamController<LatLng?> _locationStreamController =
StreamController<LatLng?>.broadcast();
Stream<LatLng?> get locationStream => _locationStreamController.stream;
LatLng? get currentLocation => _currentLocation;
bool get isLocationPermissionGranted => _isLocationPermissionGranted;
Future<void> initialize() async {
await _requestLocationPermission();
if (_isLocationPermissionGranted) {
await _getCurrentLocation();
_startLocationUpdates();
}
}
Future<void> _requestLocationPermission() async {
try {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return;
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.deniedForever) {
return;
}
_isLocationPermissionGranted = true;
} catch (e) {}
}
Future<void> _getCurrentLocation() async {
if (!_isLocationPermissionGranted) return;
try {
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
forceAndroidLocationManager: true,
);
_currentLocation = LatLng(position.latitude, position.longitude);
_locationStreamController.add(_currentLocation);
} catch (e) {}
}
void _startLocationUpdates() {
_locationTimer?.cancel();
_locationTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
if (_isLocationPermissionGranted) {
_getCurrentLocation();
}
});
}
Future<void> forceUpdateLocation() async {
if (!_isLocationPermissionGranted) {
await _requestLocationPermission();
}
await _getCurrentLocation();
}
void dispose() {
_locationTimer?.cancel();
_locationStreamController.close();
}
}

View File

@@ -9,7 +9,7 @@ class MapStateService {
MapStateService._internal();
static const String _tableName = 'record_map_states';
final Map<String, MapState> _memoryCache = {};
Future<void> _ensureTableExists() async {
@@ -34,10 +34,10 @@ class MapStateService {
Future<void> saveMapState(String key, MapState state) async {
try {
_memoryCache[key] = state;
final db = await DatabaseService.instance.database;
await _ensureTableExists();
await db.insert(
_tableName,
{
@@ -47,9 +47,7 @@ class MapStateService {
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
} catch (e) {
print('保存地图状态失败: $e');
}
} catch (e) {}
}
Future<MapState?> getMapState(String key) async {
@@ -60,7 +58,7 @@ class MapStateService {
try {
final db = await DatabaseService.instance.database;
await _ensureTableExists();
final result = await db.query(
_tableName,
where: 'key = ?',
@@ -74,16 +72,14 @@ class MapStateService {
_memoryCache[key] = state;
return state;
}
} catch (e) {
print('读取地图状态失败: $e');
}
} catch (e) {}
return null;
}
Future<void> deleteMapState(String key) async {
_memoryCache.remove(key);
try {
final db = await DatabaseService.instance.database;
await db.delete(
@@ -91,23 +87,19 @@ class MapStateService {
where: 'key = ?',
whereArgs: [key],
);
} catch (e) {
print('删除地图状态失败: $e');
}
} catch (e) {}
}
Future<void> clearAllMapStates() async {
_memoryCache.clear();
try {
final db = await DatabaseService.instance.database;
await db.delete(_tableName);
} catch (e) {
print('清空地图状态失败: $e');
}
} catch (e) {}
}
void clearMemoryCache() {
_memoryCache.clear();
}
}
}

View File

@@ -2,10 +2,42 @@ 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();
final hasTrain = train.isNotEmpty && train != "<NUL>";
final hasTrain =
train.isNotEmpty && train != "<NUL>" && !train.contains("-----");
final hasLoco = loco.isNotEmpty && loco != "<NUL>";
switch (groupBy) {
@@ -14,8 +46,13 @@ class MergeService {
case GroupBy.locoOnly:
return hasLoco ? loco : null;
case GroupBy.trainOrLoco:
if (hasTrain) return train;
if (hasLoco) return loco;
if (hasTrain && hasLoco) {
return "train:$train|loco:$loco";
} else if (hasTrain) {
return "train:$train";
} else if (hasLoco) {
return "loco:$loco";
}
return null;
case GroupBy.trainAndLoco:
return (hasTrain && hasLoco) ? "${train}_$loco" : null;
@@ -30,17 +67,19 @@ class MergeService {
return allRecords;
}
final now = DateTime.now();
final validRecords = settings.timeWindow.duration == null
? allRecords
: allRecords
.where((r) =>
now.difference(r.receivedTimestamp) <=
settings.timeWindow.duration!)
.toList();
final filteredRecords = filterUngroupableRecords(
allRecords, settings.groupBy, settings.hideUngroupableRecords);
filteredRecords
.sort((a, b) => b.receivedTimestamp.compareTo(a.receivedTimestamp));
if (settings.groupBy == GroupBy.trainOrLoco) {
return _groupByTrainOrLocoWithTimeWindow(
filteredRecords, settings.timeWindow);
}
final groupedRecords = <String, List<TrainRecord>>{};
for (final record in validRecords) {
for (final record in filteredRecords) {
final key = _generateGroupKey(record, settings.groupBy);
if (key != null) {
groupedRecords.putIfAbsent(key, () => []).add(record);
@@ -49,25 +88,35 @@ class MergeService {
final List<MergedTrainRecord> mergedRecords = [];
final Set<String> mergedRecordIds = {};
final List<TrainRecord> discardedRecords = [];
groupedRecords.forEach((key, group) {
if (group.length >= 2) {
group
.sort((a, b) => b.receivedTimestamp.compareTo(a.receivedTimestamp));
final latestRecord = group.first;
final processedGroup = _applyTimeWindow(group, settings.timeWindow);
if (processedGroup.length >= 2) {
mergedRecords.add(MergedTrainRecord(
groupKey: key,
records: group,
latestRecord: latestRecord,
records: processedGroup,
latestRecord: processedGroup.first,
));
for (final record in group) {
for (final record in processedGroup) {
mergedRecordIds.add(record.uniqueId);
}
}
for (final record in group) {
if (!processedGroup.contains(record)) {
discardedRecords.add(record);
}
}
});
final singleRecords =
allRecords.where((r) => !mergedRecordIds.contains(r.uniqueId)).toList();
final reusedRecords = _reuseDiscardedRecords(
discardedRecords, mergedRecordIds, settings.groupBy);
final singleRecords = filteredRecords
.where((r) => !mergedRecordIds.contains(r.uniqueId))
.toList();
final List<Object> mixedList = [...mergedRecords, ...singleRecords];
mixedList.sort((a, b) {
@@ -82,4 +131,252 @@ class MergeService {
return mixedList;
}
static List<TrainRecord> _applyTimeWindow(
List<TrainRecord> group, TimeWindow timeWindow) {
if (timeWindow.duration == null) {
return group;
}
group.sort((a, b) => a.receivedTimestamp.compareTo(b.receivedTimestamp));
while (group.length > 1) {
final timeSpan = group.last.receivedTimestamp
.difference(group.first.receivedTimestamp);
if (timeSpan <= timeWindow.duration!) {
break;
}
group.removeAt(0);
}
group.sort((a, b) => b.receivedTimestamp.compareTo(a.receivedTimestamp));
return group;
}
static List<TrainRecord> _reuseDiscardedRecords(
List<TrainRecord> discardedRecords,
Set<String> mergedRecordIds,
GroupBy groupBy) {
final reusedRecords = <TrainRecord>[];
for (final record in discardedRecords) {
if (mergedRecordIds.contains(record.uniqueId)) continue;
final key = _generateGroupKey(record, groupBy);
if (key != null) {
reusedRecords.add(record);
}
}
return reusedRecords;
}
static List<Object> _groupByTrainOrLocoWithTimeWindow(
List<TrainRecord> records, TimeWindow timeWindow) {
final List<MergedTrainRecord> mergedRecords = [];
final List<TrainRecord> singleRecords = [];
final Set<String> usedRecordIds = {};
for (int i = 0; i < records.length; i++) {
final record = records[i];
if (usedRecordIds.contains(record.uniqueId)) continue;
final group = <TrainRecord>[record];
for (int j = i + 1; j < records.length; j++) {
final otherRecord = records[j];
if (usedRecordIds.contains(otherRecord.uniqueId)) continue;
final recordTrain = record.train.trim();
final otherTrain = otherRecord.train.trim();
final recordLoco = record.loco.trim();
final otherLoco = otherRecord.loco.trim();
final trainMatch = recordTrain.isNotEmpty &&
recordTrain != "<NUL>" &&
!recordTrain.contains("-----") &&
otherTrain.isNotEmpty &&
otherTrain != "<NUL>" &&
!otherTrain.contains("-----") &&
recordTrain == otherTrain;
final locoMatch = recordLoco.isNotEmpty &&
recordLoco != "<NUL>" &&
otherLoco.isNotEmpty &&
otherLoco != "<NUL>" &&
recordLoco == otherLoco;
final bothTrainEmpty = (recordTrain.isEmpty ||
recordTrain == "<NUL>" ||
recordTrain.contains("----")) &&
(otherTrain.isEmpty ||
otherTrain == "<NUL>" ||
otherTrain.contains("----"));
if (trainMatch || locoMatch || (bothTrainEmpty && locoMatch)) {
group.add(otherRecord);
}
}
final processedGroup = _applyTimeWindow(group, timeWindow);
if (processedGroup.length >= 2) {
for (final record in processedGroup) {
usedRecordIds.add(record.uniqueId);
}
final firstRecord = processedGroup.first;
final train = firstRecord.train.trim();
final loco = firstRecord.loco.trim();
String uniqueGroupKey;
if (train.isNotEmpty &&
train != "<NUL>" &&
!train.contains("-----") &&
loco.isNotEmpty &&
loco != "<NUL>") {
uniqueGroupKey = "train_or_loco:${train}_$loco";
} else if (train.isNotEmpty &&
train != "<NUL>" &&
!train.contains("-----") &&
loco.isEmpty) {
uniqueGroupKey = "train_or_loco:train:$train";
} else if (loco.isNotEmpty && loco != "<NUL>") {
uniqueGroupKey = "train_or_loco:loco:$loco";
} else {
uniqueGroupKey = "train_or_loco:group_${mergedRecords.length}";
}
mergedRecords.add(MergedTrainRecord(
groupKey: uniqueGroupKey,
records: processedGroup,
latestRecord: processedGroup.first,
));
} else {
for (final record in group) {
if (!processedGroup.contains(record)) {
singleRecords.add(record);
usedRecordIds.add(record.uniqueId);
}
}
if (processedGroup.isNotEmpty) {
singleRecords.add(processedGroup.first);
usedRecordIds.add(processedGroup.first.uniqueId);
}
}
}
final List<Object> result = [...mergedRecords, ...singleRecords];
result.sort((a, b) {
final aTime = a is MergedTrainRecord
? a.latestRecord.receivedTimestamp
: (a as TrainRecord).receivedTimestamp;
final bTime = b is MergedTrainRecord
? b.latestRecord.receivedTimestamp
: (b as TrainRecord).receivedTimestamp;
return bTime.compareTo(aTime);
});
return result;
}
static List<Object> _groupByTrainOrLoco(List<TrainRecord> records) {
final List<MergedTrainRecord> mergedRecords = [];
final List<TrainRecord> singleRecords = [];
final Set<String> usedRecordIds = {};
for (int i = 0; i < records.length; i++) {
final record = records[i];
if (usedRecordIds.contains(record.uniqueId)) continue;
final group = <TrainRecord>[record];
for (int j = i + 1; j < records.length; j++) {
final otherRecord = records[j];
if (usedRecordIds.contains(otherRecord.uniqueId)) continue;
final recordTrain = record.train.trim();
final otherTrain = otherRecord.train.trim();
final recordLoco = record.loco.trim();
final otherLoco = otherRecord.loco.trim();
final trainMatch = recordTrain.isNotEmpty &&
recordTrain != "<NUL>" &&
!recordTrain.contains("-----") &&
otherTrain.isNotEmpty &&
otherTrain != "<NUL>" &&
!otherTrain.contains("-----") &&
recordTrain == otherTrain;
final locoMatch = recordLoco.isNotEmpty &&
recordLoco != "<NUL>" &&
otherLoco.isNotEmpty &&
otherLoco != "<NUL>" &&
recordLoco == otherLoco;
final bothTrainEmpty = (recordTrain.isEmpty ||
recordTrain == "<NUL>" ||
recordTrain.contains("----")) &&
(otherTrain.isEmpty ||
otherTrain == "<NUL>" ||
otherTrain.contains("----"));
if (trainMatch || locoMatch || (bothTrainEmpty && locoMatch)) {
group.add(otherRecord);
}
}
if (group.length >= 2) {
for (final record in group) {
usedRecordIds.add(record.uniqueId);
}
final firstRecord = group.first;
final train = firstRecord.train.trim();
final loco = firstRecord.loco.trim();
String uniqueGroupKey;
if (train.isNotEmpty &&
train != "<NUL>" &&
!train.contains("-----") &&
loco.isNotEmpty &&
loco != "<NUL>") {
uniqueGroupKey = "train_or_loco:${train}_$loco";
} else if (train.isNotEmpty &&
train != "<NUL>" &&
!train.contains("-----")) {
uniqueGroupKey = "train_or_loco:train:$train";
} else if (loco.isNotEmpty && loco != "<NUL>") {
uniqueGroupKey = "train_or_loco:loco:$loco";
} else {
uniqueGroupKey = "train_or_loco:group_${mergedRecords.length}";
}
mergedRecords.add(MergedTrainRecord(
groupKey: uniqueGroupKey,
records: group,
latestRecord: group.first,
));
} else {
singleRecords.add(record);
usedRecordIds.add(record.uniqueId);
}
}
final List<Object> result = [...mergedRecords, ...singleRecords];
result.sort((a, b) {
final aTime = a is MergedTrainRecord
? a.latestRecord.receivedTimestamp
: (a as TrainRecord).receivedTimestamp;
final bTime = b is MergedTrainRecord
? b.latestRecord.receivedTimestamp
: (b as TrainRecord).receivedTimestamp;
return bTime.compareTo(aTime);
});
return result;
}
}

View File

@@ -90,20 +90,32 @@ class NotificationService {
String _buildNotificationContent(TrainRecord record) {
final buffer = StringBuffer();
buffer.writeln('车次: ${record.fullTrainNumber}');
buffer.writeln('线路: ${record.route}');
buffer.writeln('方向: ${record.directionText}');
buffer.write(record.fullTrainNumber);
if (_isValidValue(record.route)) {
buffer.write(' ${record.route}');
}
if (_isValidValue(record.directionText)) {
buffer.write(' ${record.directionText}');
}
if (_isValidValue(record.positionInfo)) {
buffer.write(' ${record.positionInfo}');
}
buffer.writeln();
if (_isValidValue(record.locoType) && _isValidValue(record.loco)) {
final shortLoco = record.loco.length > 5
? record.loco.substring(record.loco.length - 5)
: record.loco;
buffer.write('${record.locoType}-$shortLoco');
} else if (_isValidValue(record.locoType)) {
buffer.write(record.locoType);
} else if (_isValidValue(record.loco)) {
buffer.write(record.loco);
}
if (_isValidValue(record.speed)) {
buffer.writeln('速度: ${record.speed} km/h');
buffer.write(' ${record.speed}km/h');
}
if (_isValidValue(record.positionInfo)) {
buffer.writeln('位置: ${record.positionInfo}');
}
buffer.writeln('时间: ${record.formattedTime}');
return buffer.toString().trim();
}

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:
@@ -358,6 +366,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.1"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
@@ -648,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:
@@ -888,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:
@@ -920,6 +968,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.28.0"
scrollview_observer:
dependency: "direct main"
description:
name: scrollview_observer
sha256: c2f713509f18f88f637b2084b47a90c91fb1ef066d5d82d2cf3194d8509dc6ab
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.26.2"
share_plus:
dependency: "direct main"
description:
@@ -1245,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:
@@ -1253,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:
@@ -1293,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:
@@ -1334,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.2.0-flutter+20 # versionName: 0.2.0-flutter, versionCode: 3
version: 0.6.1-flutter+61
environment:
sdk: ^3.5.4
@@ -53,6 +53,10 @@ dependencies:
package_info_plus: ^8.1.2
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:
@@ -66,12 +70,12 @@ dev_dependencies:
flutter_lints: ^4.0.0
hive_generator: ^2.0.1
build_runner: ^2.4.6
flutter_launcher_icons: ^0.14.1
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# 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.
@@ -82,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
@@ -109,6 +114,13 @@ flutter:
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
flutter_launcher_icons:
android: true
ios: true
image_path: "assets/icon.png"
adaptive_icon_background: "#000000"
adaptive_icon_foreground: "assets/icon.png"
msix_config:
display_name: LBJ Console
publisher_display_name: Noxylva
@@ -116,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