Compare commits
7 Commits
v0.8.0-flu
...
v0.8.1-flu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7772112658 | ||
|
|
5a232eaa6c | ||
|
|
a47c6a5745 | ||
|
|
777efdda45 | ||
|
|
fe2769f479 | ||
|
|
0dc256b5d7 | ||
|
|
8615e53c85 |
7
.github/workflows/flutter_build.yml
vendored
7
.github/workflows/flutter_build.yml
vendored
@@ -38,12 +38,6 @@ jobs:
|
|||||||
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: Build App Bundle
|
|
||||||
run: flutter build appbundle --release
|
|
||||||
env:
|
|
||||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
|
||||||
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
|
||||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Rename APK with tag
|
- name: Rename APK with tag
|
||||||
run: |
|
run: |
|
||||||
@@ -55,7 +49,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
LBJ_Console_${{ github.ref_name }}_android_release.apk
|
LBJ_Console_${{ github.ref_name }}_android_release.apk
|
||||||
build/app/outputs/bundle/release/app-release.aab
|
|
||||||
name: ${{ github.ref_name }}
|
name: ${{ github.ref_name }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
# LBJ_Console
|
# LBJ_Console
|
||||||
|
|
||||||
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 信息。
|
||||||
- 基于内置数据文件显示机车配属,机车类型和车次类型。
|
- 基于内置数据文件显示机车配属,机车类型和车次类型。
|
||||||
- [WIP] 从 RTL-TCP 获取数据。
|
- 连接 RTL-TCP 服务器获取预警消息。
|
||||||
|
|
||||||
[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` 目录,用于支持机车配属和车次信息的展示:
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-all.zip
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "8.6.0" apply false
|
id "com.android.application" version "8.13.0" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
assets/loco_type_info.csv
vendored
2
assets/loco_type_info.csv
vendored
@@ -57,6 +57,7 @@
|
|||||||
151,NJ2
|
151,NJ2
|
||||||
152,东风7G
|
152,东风7G
|
||||||
153,NDJ3
|
153,NDJ3
|
||||||
|
156,东风11Z
|
||||||
157,FXN3D
|
157,FXN3D
|
||||||
158,东风11G
|
158,东风11G
|
||||||
160,HXN3
|
160,HXN3
|
||||||
@@ -146,3 +147,4 @@
|
|||||||
334,CJ5
|
334,CJ5
|
||||||
335,CJ6
|
335,CJ6
|
||||||
400,GCD-1000J
|
400,GCD-1000J
|
||||||
|
403,GX-160
|
||||||
|
@@ -3,9 +3,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|||||||
import 'package:lbjconsole/screens/main_screen.dart';
|
import 'package:lbjconsole/screens/main_screen.dart';
|
||||||
import 'package:lbjconsole/util/train_type_util.dart';
|
import 'package:lbjconsole/util/train_type_util.dart';
|
||||||
import 'package:lbjconsole/util/loco_info_util.dart';
|
import 'package:lbjconsole/util/loco_info_util.dart';
|
||||||
import 'package:lbjconsole/util/loco_type_util.dart';
|
|
||||||
import 'package:lbjconsole/services/loco_type_service.dart';
|
import 'package:lbjconsole/services/loco_type_service.dart';
|
||||||
import 'package:lbjconsole/services/database_service.dart';
|
|
||||||
import 'package:lbjconsole/services/background_service.dart';
|
import 'package:lbjconsole/services/background_service.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class MapState {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'MapState(zoom: ' + zoom.toString() + ', centerLat: ' + centerLat.toString() + ', centerLng: ' + centerLng.toString() + ', bearing: ' + bearing.toString() + ')';
|
return 'MapState(zoom: $zoom, centerLat: $centerLat, centerLng: $centerLng, bearing: $bearing)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -340,22 +340,6 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
} 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;
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import 'package:flutter/services.dart';
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer' as developer;
|
import 'dart:developer' as developer;
|
||||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
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/history_screen.dart';
|
||||||
import 'package:lbjconsole/screens/map_screen.dart';
|
import 'package:lbjconsole/screens/map_screen.dart';
|
||||||
import 'package:lbjconsole/screens/map_webview_screen.dart';
|
import 'package:lbjconsole/screens/map_webview_screen.dart';
|
||||||
@@ -307,17 +305,17 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
|||||||
developer.log('rtl_tcp: setup_listener');
|
developer.log('rtl_tcp: setup_listener');
|
||||||
_settingsSubscription =
|
_settingsSubscription =
|
||||||
DatabaseService.instance.onSettingsChanged((settings) {
|
DatabaseService.instance.onSettingsChanged((settings) {
|
||||||
developer.log('rtl_tcp: settings_changed: enabled=${(settings?['rtlTcpEnabled'] ?? 0) == 1}, host=${settings?['rtlTcpHost']?.toString() ?? '127.0.0.1'}, port=${settings?['rtlTcpPort']?.toString() ?? '14423'}');
|
developer.log('rtl_tcp: settings_changed: enabled=${(settings['rtlTcpEnabled'] ?? 0) == 1}, host=${settings['rtlTcpHost']?.toString() ?? '127.0.0.1'}, port=${settings['rtlTcpPort']?.toString() ?? '14423'}');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final rtlTcpEnabled = (settings?['rtlTcpEnabled'] ?? 0) == 1;
|
final rtlTcpEnabled = (settings['rtlTcpEnabled'] ?? 0) == 1;
|
||||||
if (rtlTcpEnabled != _rtlTcpEnabled) {
|
if (rtlTcpEnabled != _rtlTcpEnabled) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_rtlTcpEnabled = rtlTcpEnabled;
|
_rtlTcpEnabled = rtlTcpEnabled;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (rtlTcpEnabled) {
|
if (rtlTcpEnabled) {
|
||||||
final host = settings?['rtlTcpHost']?.toString() ?? '127.0.0.1';
|
final host = settings['rtlTcpHost']?.toString() ?? '127.0.0.1';
|
||||||
final port = settings?['rtlTcpPort']?.toString() ?? '14423';
|
final port = settings['rtlTcpPort']?.toString() ?? '14423';
|
||||||
_connectToRtlTcp(host, port);
|
_connectToRtlTcp(host, port);
|
||||||
} else {
|
} else {
|
||||||
_rtlTcpConnectionSubscription?.cancel();
|
_rtlTcpConnectionSubscription?.cancel();
|
||||||
@@ -661,11 +659,12 @@ class _PixelPerfectBluetoothDialogState
|
|||||||
|
|
||||||
Future<void> _startScan() async {
|
Future<void> _startScan() async {
|
||||||
if (_scanState == _ScanState.scanning) return;
|
if (_scanState == _ScanState.scanning) return;
|
||||||
if (mounted)
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_devices.clear();
|
_devices.clear();
|
||||||
_scanState = _ScanState.scanning;
|
_scanState = _ScanState.scanning;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
await widget.bleService.startScan(
|
await widget.bleService.startScan(
|
||||||
timeout: const Duration(seconds: 8),
|
timeout: const Duration(seconds: 8),
|
||||||
onScanResults: (devices) {
|
onScanResults: (devices) {
|
||||||
@@ -700,7 +699,7 @@ class _PixelPerfectBluetoothDialogState
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isConnected = widget.bleService.isConnected;
|
final isConnected = widget.bleService.isConnected;
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(widget.rtlTcpEnabled ? 'RTL-TCP 模式' : '蓝牙设备'),
|
title: Text(widget.rtlTcpEnabled ? 'RTL-TCP 服务器' : '蓝牙设备'),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: double.maxFinite,
|
width: double.maxFinite,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
@@ -784,7 +783,7 @@ class _PixelPerfectBluetoothDialogState
|
|||||||
.titleMedium
|
.titleMedium
|
||||||
?.copyWith(fontWeight: FontWeight.bold)),
|
?.copyWith(fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text('$currentAddress',
|
Text(currentAddress,
|
||||||
style: TextStyle(color: isConnected ? Colors.green : Colors.grey)),
|
style: TextStyle(color: isConnected ? Colors.green : Colors.grey)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (_lastReceivedTime != null && isConnected) ...[
|
if (_lastReceivedTime != null && isConnected) ...[
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' show sin, cos, sqrt, atan2, pi;
|
import 'dart:math' show sin, cos, sqrt, atan2;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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';
|
||||||
@@ -26,19 +26,19 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
double _currentRotation = 0.0;
|
double _currentRotation = 0.0;
|
||||||
|
|
||||||
bool _isMapInitialized = false;
|
bool _isMapInitialized = false;
|
||||||
bool _isFollowingLocation = false;
|
final bool _isFollowingLocation = false;
|
||||||
bool _isLocationPermissionGranted = false;
|
bool _isLocationPermissionGranted = false;
|
||||||
Timer? _locationTimer;
|
Timer? _locationTimer;
|
||||||
|
|
||||||
String _selectedTimeFilter = 'unlimited';
|
String _selectedTimeFilter = 'unlimited';
|
||||||
final Map<String, Duration> _timeFilterOptions = {
|
final Map<String, Duration> _timeFilterOptions = {
|
||||||
'unlimited': Duration.zero,
|
'unlimited': Duration.zero,
|
||||||
'1hour': Duration(hours: 1),
|
'1hour': const Duration(hours: 1),
|
||||||
'6hours': Duration(hours: 6),
|
'6hours': const Duration(hours: 6),
|
||||||
'12hours': Duration(hours: 12),
|
'12hours': const Duration(hours: 12),
|
||||||
'24hours': Duration(hours: 24),
|
'24hours': const Duration(hours: 24),
|
||||||
'7days': Duration(days: 7),
|
'7days': const Duration(days: 7),
|
||||||
'30days': Duration(days: 30),
|
'30days': const Duration(days: 30),
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -81,8 +81,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
if (lat == 39.9042 && lon == 116.4074) {
|
if (lat == 39.9042 && lon == 116.4074) {
|
||||||
} else if (lat == 0.0 && lon == 0.0) {
|
} else if (lat == 0.0 && lon == 0.0) {
|
||||||
} else {
|
} else {
|
||||||
final beijingLat = 39.9042;
|
const beijingLat = 39.9042;
|
||||||
final beijingLon = 116.4074;
|
const beijingLon = 116.4074;
|
||||||
final distance =
|
final distance =
|
||||||
_calculateDistance(lat, lon, beijingLat, beijingLon);
|
_calculateDistance(lat, lon, beijingLat, beijingLon);
|
||||||
|
|
||||||
@@ -411,7 +411,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withOpacity(0.8),
|
color: Colors.black.withValues(alpha: 0.8),
|
||||||
borderRadius: BorderRadius.circular(3),
|
borderRadius: BorderRadius.circular(3),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -572,8 +572,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.surfaceVariant
|
.surfaceContainerHighest
|
||||||
.withOpacity(0.3),
|
.withValues(alpha: 0.3),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
|
|||||||
double _currentRotation = 0.0;
|
double _currentRotation = 0.0;
|
||||||
LatLng? _currentLocation;
|
LatLng? _currentLocation;
|
||||||
LatLng? _lastTrainLocation;
|
LatLng? _lastTrainLocation;
|
||||||
bool _isDataLoaded = false;
|
final bool _isDataLoaded = false;
|
||||||
final Completer<void> _webViewReadyCompleter = Completer<void>();
|
final Completer<void> _webViewReadyCompleter = Completer<void>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -299,7 +299,7 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
|
|||||||
_updateTrainMarkers();
|
_updateTrainMarkers();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, stackTrace) {
|
} catch (e) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class RealtimeScreenState extends State<RealtimeScreen> {
|
|||||||
List<LatLng> _selectedGroupRoute = [];
|
List<LatLng> _selectedGroupRoute = [];
|
||||||
List<Marker> _mapMarkers = [];
|
List<Marker> _mapMarkers = [];
|
||||||
bool _showMap = true;
|
bool _showMap = true;
|
||||||
Set<String> _selectedGroupKeys = {};
|
final Set<String> _selectedGroupKeys = {};
|
||||||
LatLng? _userLocation;
|
LatLng? _userLocation;
|
||||||
bool _isLocationPermissionGranted = false;
|
bool _isLocationPermissionGranted = false;
|
||||||
Timer? _locationTimer;
|
Timer? _locationTimer;
|
||||||
@@ -376,8 +376,7 @@ class RealtimeScreenState extends State<RealtimeScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LatLng? _parsePositionFromRecord(TrainRecord record) {
|
LatLng? _parsePositionFromRecord(TrainRecord record) {
|
||||||
if (record.positionInfo == null ||
|
if (record.positionInfo.isEmpty ||
|
||||||
record.positionInfo.isEmpty ||
|
|
||||||
record.positionInfo == '<NUL>') {
|
record.positionInfo == '<NUL>') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -916,8 +915,8 @@ class RealtimeScreenState extends State<RealtimeScreen> {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
child: FlutterMap(
|
child: FlutterMap(
|
||||||
mapController: _mapController,
|
mapController: _mapController,
|
||||||
options: MapOptions(
|
options: const MapOptions(
|
||||||
initialCenter: const LatLng(35.8617, 104.1954),
|
initialCenter: LatLng(35.8617, 104.1954),
|
||||||
initialZoom: 2.0,
|
initialZoom: 2.0,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -4,17 +4,13 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:lbjconsole/models/merged_record.dart';
|
import 'package:lbjconsole/models/merged_record.dart';
|
||||||
import 'package:lbjconsole/services/database_service.dart';
|
import 'package:lbjconsole/services/database_service.dart';
|
||||||
import 'package:lbjconsole/services/ble_service.dart';
|
|
||||||
import 'package:lbjconsole/services/background_service.dart';
|
import 'package:lbjconsole/services/background_service.dart';
|
||||||
import 'package:lbjconsole/themes/app_theme.dart';
|
import 'package:lbjconsole/themes/app_theme.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:cross_file/cross_file.dart';
|
|
||||||
|
|
||||||
class SettingsScreen extends StatefulWidget {
|
class SettingsScreen extends StatefulWidget {
|
||||||
final VoidCallback? onSettingsChanged;
|
final VoidCallback? onSettingsChanged;
|
||||||
@@ -73,17 +69,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Icon(Icons.wifi,
|
Icon(Icons.wifi,
|
||||||
color: Theme.of(context).colorScheme.primary),
|
color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text('RTL-TCP 接收', style: AppTheme.titleMedium),
|
const Text('RTL-TCP 源', style: AppTheme.titleMedium),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
const Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('启用RTL-TCP接收', style: AppTheme.bodyLarge),
|
Text('启用 RTL-TCP 源', style: AppTheme.bodyLarge),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Switch(
|
Switch(
|
||||||
@@ -94,7 +90,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
});
|
});
|
||||||
_saveSettings();
|
_saveSettings();
|
||||||
},
|
},
|
||||||
activeColor: Theme.of(context).colorScheme.primary,
|
activeThumbColor: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -273,14 +269,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Icon(Icons.bluetooth,
|
Icon(Icons.bluetooth,
|
||||||
color: Theme.of(context).colorScheme.primary),
|
color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text('蓝牙设备', style: AppTheme.titleMedium),
|
const Text('蓝牙设备', style: AppTheme.titleMedium),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _deviceNameController,
|
controller: _deviceNameController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '设备名称 (用于自动连接)',
|
labelText: '设备名称',
|
||||||
hintText: '输入设备名称',
|
hintText: '输入设备名称',
|
||||||
labelStyle: const TextStyle(color: Colors.white70),
|
labelStyle: const TextStyle(color: Colors.white70),
|
||||||
hintStyle: const TextStyle(color: Colors.white54),
|
hintStyle: const TextStyle(color: Colors.white54),
|
||||||
@@ -329,14 +325,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Icon(Icons.settings,
|
Icon(Icons.settings,
|
||||||
color: Theme.of(context).colorScheme.primary),
|
color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text('应用设置', style: AppTheme.titleMedium),
|
const Text('应用设置', style: AppTheme.titleMedium),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
const Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('后台保活服务', style: AppTheme.bodyLarge),
|
Text('后台保活服务', style: AppTheme.bodyLarge),
|
||||||
@@ -356,7 +352,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
await BackgroundService.stopService();
|
await BackgroundService.stopService();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
activeColor: Theme.of(context).colorScheme.primary,
|
activeThumbColor: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -364,7 +360,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
const Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('通知服务', style: AppTheme.bodyLarge),
|
Text('通知服务', style: AppTheme.bodyLarge),
|
||||||
@@ -378,7 +374,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
});
|
});
|
||||||
_saveSettings();
|
_saveSettings();
|
||||||
},
|
},
|
||||||
activeColor: Theme.of(context).colorScheme.primary,
|
activeThumbColor: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -386,7 +382,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
const Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('地图组件类型', style: AppTheme.bodyLarge),
|
Text('地图组件类型', style: AppTheme.bodyLarge),
|
||||||
@@ -394,7 +390,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
DropdownButton<String>(
|
DropdownButton<String>(
|
||||||
value: _mapType,
|
value: _mapType,
|
||||||
items: [
|
items: const [
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: 'webview',
|
value: 'webview',
|
||||||
child: Text('矢量铁路地图', style: AppTheme.bodyMedium),
|
child: Text('矢量铁路地图', style: AppTheme.bodyMedium),
|
||||||
@@ -422,7 +418,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
const Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('隐藏只有时间有效的记录', style: AppTheme.bodyLarge),
|
Text('隐藏只有时间有效的记录', style: AppTheme.bodyLarge),
|
||||||
@@ -436,7 +432,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
});
|
});
|
||||||
_saveSettings();
|
_saveSettings();
|
||||||
},
|
},
|
||||||
activeColor: Theme.of(context).colorScheme.primary,
|
activeThumbColor: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -463,14 +459,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Icon(Icons.merge_type,
|
Icon(Icons.merge_type,
|
||||||
color: Theme.of(context).colorScheme.primary),
|
color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text('记录合并', style: AppTheme.titleMedium),
|
const Text('记录合并', style: AppTheme.titleMedium),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
const Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('启用记录合并', style: AppTheme.bodyLarge),
|
Text('启用记录合并', style: AppTheme.bodyLarge),
|
||||||
@@ -484,7 +480,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
});
|
});
|
||||||
_saveSettings();
|
_saveSettings();
|
||||||
},
|
},
|
||||||
activeColor: Theme.of(context).colorScheme.primary,
|
activeThumbColor: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -494,11 +490,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text('分组方式', style: AppTheme.bodyLarge),
|
const Text('分组方式', style: AppTheme.bodyLarge),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
DropdownButtonFormField<GroupBy>(
|
DropdownButtonFormField<GroupBy>(
|
||||||
value: _groupBy,
|
initialValue: _groupBy,
|
||||||
items: [
|
items: const [
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: GroupBy.trainOnly,
|
value: GroupBy.trainOnly,
|
||||||
child: Text('仅车次号', style: AppTheme.bodyMedium)),
|
child: Text('仅车次号', style: AppTheme.bodyMedium)),
|
||||||
@@ -532,11 +528,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
style: AppTheme.bodyMedium,
|
style: AppTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text('时间窗口', style: AppTheme.bodyLarge),
|
const Text('时间窗口', style: AppTheme.bodyLarge),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
DropdownButtonFormField<TimeWindow>(
|
DropdownButtonFormField<TimeWindow>(
|
||||||
value: _timeWindow,
|
initialValue: _timeWindow,
|
||||||
items: [
|
items: const [
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: TimeWindow.oneHour,
|
value: TimeWindow.oneHour,
|
||||||
child: Text('1小时内', style: AppTheme.bodyMedium)),
|
child: Text('1小时内', style: AppTheme.bodyMedium)),
|
||||||
@@ -579,7 +575,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
const Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('隐藏不可分组记录', style: AppTheme.bodyLarge),
|
Text('隐藏不可分组记录', style: AppTheme.bodyLarge),
|
||||||
@@ -593,7 +589,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
});
|
});
|
||||||
_saveSettings();
|
_saveSettings();
|
||||||
},
|
},
|
||||||
activeColor: Theme.of(context).colorScheme.primary,
|
activeThumbColor: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -623,7 +619,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Icon(Icons.storage,
|
Icon(Icons.storage,
|
||||||
color: Theme.of(context).colorScheme.primary),
|
color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text('数据管理', style: AppTheme.titleMedium),
|
const Text('数据管理', style: AppTheme.titleMedium),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -637,7 +633,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_buildActionButton(
|
_buildActionButton(
|
||||||
icon: Icons.file_download,
|
icon: Icons.file_download,
|
||||||
title: '导入数据',
|
title: '导入数据',
|
||||||
subtitle: '从JSON文件导入记录和设置',
|
subtitle: '从 JSON 文件导入记录和设置',
|
||||||
onTap: _importData,
|
onTap: _importData,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -705,7 +701,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Icon(
|
const Icon(
|
||||||
Icons.chevron_right,
|
Icons.chevron_right,
|
||||||
color: Colors.white54,
|
color: Colors.white54,
|
||||||
size: 20,
|
size: 20,
|
||||||
@@ -760,7 +756,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
|
|
||||||
if (exportedPath != null) {
|
if (exportedPath != null) {
|
||||||
final file = File(exportedPath);
|
final file = File(exportedPath);
|
||||||
final fileName = file.path.split(Platform.pathSeparator).last;
|
|
||||||
|
|
||||||
await Share.shareXFiles(
|
await Share.shareXFiles(
|
||||||
[XFile(file.path)],
|
[XFile(file.path)],
|
||||||
@@ -954,11 +949,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Icon(Icons.info, color: Theme.of(context).colorScheme.primary),
|
Icon(Icons.info, color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text('关于', style: AppTheme.titleMedium),
|
const Text('关于', style: AppTheme.titleMedium),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text('LBJ Console', style: AppTheme.titleMedium),
|
const Text('LBJ Console', style: AppTheme.titleMedium),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
FutureBuilder<String>(
|
FutureBuilder<String>(
|
||||||
future: _getAppVersion(),
|
future: _getAppVersion(),
|
||||||
@@ -979,7 +974,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
await launchUrl(url);
|
await launchUrl(url);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(
|
child: const Text(
|
||||||
'https://github.com/undef-i/LBJConsole',
|
'https://github.com/undef-i/LBJConsole',
|
||||||
style: AppTheme.caption,
|
style: AppTheme.caption,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'dart:io';
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||||
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
|
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:lbjconsole/services/ble_service.dart';
|
import 'package:lbjconsole/services/ble_service.dart';
|
||||||
|
|
||||||
@@ -107,7 +106,7 @@ class BackgroundService {
|
|||||||
_notificationId,
|
_notificationId,
|
||||||
'LBJ Console',
|
'LBJ Console',
|
||||||
'蓝牙连接监控中',
|
'蓝牙连接监控中',
|
||||||
NotificationDetails(
|
const NotificationDetails(
|
||||||
android: AndroidNotificationDetails(
|
android: AndroidNotificationDetails(
|
||||||
_notificationChannelId,
|
_notificationChannelId,
|
||||||
_notificationChannelName,
|
_notificationChannelName,
|
||||||
@@ -146,7 +145,7 @@ class BackgroundService {
|
|||||||
_notificationId,
|
_notificationId,
|
||||||
'LBJ Console',
|
'LBJ Console',
|
||||||
isConnected ? '蓝牙已连接 - $deviceStatus' : '蓝牙未连接 - 自动重连中',
|
isConnected ? '蓝牙已连接 - $deviceStatus' : '蓝牙未连接 - 自动重连中',
|
||||||
NotificationDetails(
|
const NotificationDetails(
|
||||||
android: AndroidNotificationDetails(
|
android: AndroidNotificationDetails(
|
||||||
_notificationChannelId,
|
_notificationChannelId,
|
||||||
_notificationChannelName,
|
_notificationChannelName,
|
||||||
|
|||||||
@@ -145,7 +145,9 @@ class BLEService {
|
|||||||
if (isConnected ||
|
if (isConnected ||
|
||||||
_isConnecting ||
|
_isConnecting ||
|
||||||
_isManualDisconnect ||
|
_isManualDisconnect ||
|
||||||
_isAutoConnectBlocked) return;
|
_isAutoConnectBlocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (var device in allFoundDevices) {
|
for (var device in allFoundDevices) {
|
||||||
if (_shouldAutoConnectTo(device)) {
|
if (_shouldAutoConnectTo(device)) {
|
||||||
@@ -168,10 +170,13 @@ class BLEService {
|
|||||||
final deviceAddress = device.remoteId.str;
|
final deviceAddress = device.remoteId.str;
|
||||||
|
|
||||||
if (_targetDeviceName.isNotEmpty &&
|
if (_targetDeviceName.isNotEmpty &&
|
||||||
deviceName.toLowerCase() == _targetDeviceName.toLowerCase())
|
deviceName.toLowerCase() == _targetDeviceName.toLowerCase()) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
if (_lastKnownDeviceAddress != null &&
|
if (_lastKnownDeviceAddress != null &&
|
||||||
_lastKnownDeviceAddress == deviceAddress) return true;
|
_lastKnownDeviceAddress == deviceAddress) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class DatabaseService {
|
|||||||
}
|
}
|
||||||
_database = await _initDatabase();
|
_database = await _initDatabase();
|
||||||
return _database!;
|
return _database!;
|
||||||
} catch (e, stackTrace) {
|
} catch (e) {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,8 +38,6 @@ class DatabaseService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final db = await database;
|
|
||||||
final result = await db.rawQuery('SELECT 1');
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
@@ -59,7 +57,7 @@ class DatabaseService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return db;
|
return db;
|
||||||
} catch (e, stackTrace) {
|
} catch (e) {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,7 +204,7 @@ class DatabaseService {
|
|||||||
final records =
|
final records =
|
||||||
result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
|
result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
|
||||||
return records;
|
return records;
|
||||||
} catch (e, stackTrace) {
|
} catch (e) {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,7 +237,7 @@ class DatabaseService {
|
|||||||
final records =
|
final records =
|
||||||
result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
|
result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
|
||||||
return records;
|
return records;
|
||||||
} catch (e, stackTrace) {
|
} catch (e) {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:lbjconsole/util/loco_type_util.dart';
|
|
||||||
|
|
||||||
class LocoTypeService {
|
class LocoTypeService {
|
||||||
static final LocoTypeService _instance = LocoTypeService._internal();
|
static final LocoTypeService _instance = LocoTypeService._internal();
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class MergeService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
final reusedRecords = _reuseDiscardedRecords(
|
_reuseDiscardedRecords(
|
||||||
discardedRecords, mergedRecordIds, settings.groupBy);
|
discardedRecords, mergedRecordIds, settings.groupBy);
|
||||||
|
|
||||||
final singleRecords = filteredRecords
|
final singleRecords = filteredRecords
|
||||||
@@ -283,100 +283,4 @@ class MergeService {
|
|||||||
return result;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class NotificationService {
|
|||||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||||
|
|
||||||
final InitializationSettings initializationSettings =
|
const InitializationSettings initializationSettings =
|
||||||
InitializationSettings(
|
InitializationSettings(
|
||||||
android: initializationSettingsAndroid,
|
android: initializationSettingsAndroid,
|
||||||
);
|
);
|
||||||
@@ -61,7 +61,7 @@ class NotificationService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String title = '列车信息';
|
const String title = '列车信息';
|
||||||
final String body = _buildNotificationContent(record);
|
final String body = _buildNotificationContent(record);
|
||||||
|
|
||||||
final AndroidNotificationDetails androidPlatformChannelSpecifics =
|
final AndroidNotificationDetails androidPlatformChannelSpecifics =
|
||||||
|
|||||||
@@ -149,8 +149,9 @@ class _LbJState {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case _lbjSyncAddr:
|
case _lbjSyncAddr:
|
||||||
if (numeric.length >= 5)
|
if (numeric.length >= 5) {
|
||||||
time = "${numeric.substring(1, 3)}:${numeric.substring(3, 5)}";
|
time = "${numeric.substring(1, 3)}:${numeric.substring(3, 5)}";
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,11 +168,11 @@ class _LbJState {
|
|||||||
|
|
||||||
String gpsPosition = "";
|
String gpsPosition = "";
|
||||||
if (posLatDeg.isNotEmpty && posLatMin.isNotEmpty) {
|
if (posLatDeg.isNotEmpty && posLatMin.isNotEmpty) {
|
||||||
gpsPosition = "${posLatDeg}°${posLatMin}′";
|
gpsPosition = "$posLatDeg°$posLatMin′";
|
||||||
}
|
}
|
||||||
if (posLonDeg.isNotEmpty && posLonMin.isNotEmpty) {
|
if (posLonDeg.isNotEmpty && posLonMin.isNotEmpty) {
|
||||||
gpsPosition +=
|
gpsPosition +=
|
||||||
(gpsPosition.isEmpty ? "" : " ") + "${posLonDeg}°${posLonMin}′";
|
"${gpsPosition.isEmpty ? "" : " "}$posLonDeg°$posLonMin′";
|
||||||
}
|
}
|
||||||
|
|
||||||
String kmPosition = positionKm.replaceAll(' <NUL>', '');
|
String kmPosition = positionKm.replaceAll(' <NUL>', '');
|
||||||
|
|||||||
@@ -9,13 +9,11 @@ class AppTheme {
|
|||||||
canvasColor: Colors.black,
|
canvasColor: Colors.black,
|
||||||
cardColor: const Color(0xFF121212),
|
cardColor: const Color(0xFF121212),
|
||||||
primaryColor: Colors.blue,
|
primaryColor: Colors.blue,
|
||||||
colorScheme: ColorScheme.dark(
|
colorScheme: const ColorScheme.dark(
|
||||||
primary: Colors.blue,
|
primary: Colors.blue,
|
||||||
secondary: Colors.blueAccent,
|
secondary: Colors.blueAccent,
|
||||||
surface: const Color(0xFF121212),
|
surface: Color(0xFF121212),
|
||||||
background: Colors.black,
|
|
||||||
onSurface: Colors.white,
|
onSurface: Colors.white,
|
||||||
onBackground: Colors.white,
|
|
||||||
),
|
),
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
@@ -67,16 +65,16 @@ class AppTheme {
|
|||||||
thickness: 1,
|
thickness: 1,
|
||||||
),
|
),
|
||||||
switchTheme: SwitchThemeData(
|
switchTheme: SwitchThemeData(
|
||||||
thumbColor: MaterialStateProperty.resolveWith<Color?>(
|
thumbColor: WidgetStateProperty.resolveWith<Color?>(
|
||||||
(Set<MaterialState> states) {
|
(Set<WidgetState> states) {
|
||||||
if (states.contains(MaterialState.selected)) {
|
if (states.contains(WidgetState.selected)) {
|
||||||
return Colors.blue;
|
return Colors.blue;
|
||||||
}
|
}
|
||||||
return Colors.grey;
|
return Colors.grey;
|
||||||
}),
|
}),
|
||||||
trackColor: MaterialStateProperty.resolveWith<Color?>(
|
trackColor: WidgetStateProperty.resolveWith<Color?>(
|
||||||
(Set<MaterialState> states) {
|
(Set<WidgetState> states) {
|
||||||
if (states.contains(MaterialState.selected)) {
|
if (states.contains(WidgetState.selected)) {
|
||||||
return Colors.blue.withOpacity(0.5);
|
return Colors.blue.withOpacity(0.5);
|
||||||
}
|
}
|
||||||
return Colors.grey.withOpacity(0.5);
|
return Colors.grey.withOpacity(0.5);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class LocoInfoUtil {
|
class LocoInfoUtil {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class TrainTypeUtil {
|
class TrainTypeUtil {
|
||||||
|
|||||||
66
pubspec.lock
66
pubspec.lock
@@ -241,6 +241,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"
|
||||||
|
dependency_validator:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: dependency_validator
|
||||||
|
sha256: "3a23914cacac37d0cdce067d0576fce18bf5951338616f036a20604c97dba0f7"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.3"
|
||||||
executor_lib:
|
executor_lib:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -529,21 +537,13 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.2"
|
||||||
hive:
|
hive:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: hive
|
name: hive
|
||||||
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
|
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.3"
|
version: "2.2.3"
|
||||||
hive_flutter:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: hive_flutter
|
|
||||||
sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.0"
|
|
||||||
hive_generator:
|
hive_generator:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -688,30 +688,6 @@ 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:
|
||||||
@@ -760,14 +736,6 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.16.12"
|
version: "3.16.12"
|
||||||
nested:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: nested
|
|
||||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.0"
|
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -960,14 +928,6 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
provider:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: provider
|
|
||||||
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "6.1.5"
|
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1028,18 +988,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: "9f9f3d372d4304723e6136663bb291c0b93f5e4c8a4a6314347f481a33bda2b1"
|
sha256: "46a46fd64659eff15f4638bbe19de43f9483f0e0bf024a9fb6b3582064bacc7b"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.7"
|
version: "2.4.17"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_foundation
|
name: shared_preferences_foundation
|
||||||
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
|
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.4"
|
version: "2.5.6"
|
||||||
shared_preferences_linux:
|
shared_preferences_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -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.8.0-flutter+80
|
version: 0.8.1-flutter+81
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
@@ -34,10 +34,8 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
flutter_blue_plus: ^1.31.15
|
flutter_blue_plus: ^1.31.15
|
||||||
permission_handler: ^11.3.1
|
permission_handler: ^11.3.1
|
||||||
provider: ^6.1.2
|
|
||||||
shared_preferences: ^2.3.2
|
shared_preferences: ^2.3.2
|
||||||
hive: ^2.2.3
|
|
||||||
hive_flutter: ^1.1.0
|
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
path_provider: ^2.1.4
|
path_provider: ^2.1.4
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
@@ -46,6 +44,7 @@ dependencies:
|
|||||||
latlong2: ^0.9.1
|
latlong2: ^0.9.1
|
||||||
geolocator: ^13.0.4
|
geolocator: ^13.0.4
|
||||||
geolocator_android: 4.6.1
|
geolocator_android: 4.6.1
|
||||||
|
|
||||||
url_launcher: ^6.2.5
|
url_launcher: ^6.2.5
|
||||||
sqflite: ^2.3.3+1
|
sqflite: ^2.3.3+1
|
||||||
share_plus: ^10.0.0
|
share_plus: ^10.0.0
|
||||||
@@ -55,7 +54,6 @@ dependencies:
|
|||||||
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
|
vector_map_tiles: ^8.0.0
|
||||||
maplibre_gl: ^0.22.0
|
|
||||||
webview_flutter: ^4.8.0
|
webview_flutter: ^4.8.0
|
||||||
gbk_codec: ^0.4.0
|
gbk_codec: ^0.4.0
|
||||||
|
|
||||||
@@ -72,6 +70,7 @@ dev_dependencies:
|
|||||||
hive_generator: ^2.0.1
|
hive_generator: ^2.0.1
|
||||||
build_runner: ^2.4.6
|
build_runner: ^2.4.6
|
||||||
flutter_launcher_icons: ^0.14.1
|
flutter_launcher_icons: ^0.14.1
|
||||||
|
dependency_validator: ^4.1.3
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user