7 Commits

Author SHA1 Message Date
Nedifinita
7772112658 chore: upgrade AGP and Gradle 2025-11-23 22:30:21 +08:00
Nedifinita
5a232eaa6c Merge branch 'flutter' of https://github.com/undef-i/LBJ_Console into flutter 2025-11-23 21:44:24 +08:00
Nedifinita
a47c6a5745 refactor 2025-11-23 21:43:47 +08:00
undef-i
777efdda45 Merge pull request #3 from zrj24/flutter
add loco_type DF11Z,GX-160
2025-11-23 21:20:39 +08:00
Nedifinita
fe2769f479 chore 2025-11-23 21:18:54 +08:00
zrj24
0dc256b5d7 add loco_type DF11Z,GX-160 2025-11-23 21:06:31 +08:00
Nedifinita
8615e53c85 docs: update README.md 2025-11-01 22:41:59 +08:00
25 changed files with 115 additions and 282 deletions

View File

@@ -38,12 +38,6 @@ jobs:
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
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
run: |
@@ -55,7 +49,6 @@ jobs:
with:
files: |
LBJ_Console_${{ github.ref_name }}_android_release.apk
build/app/outputs/bundle/release/app-release.aab
name: ${{ github.ref_name }}
draft: false
prerelease: false

View File

@@ -1,15 +1,17 @@
# 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 信息。
- 基于内置数据文件显示机车配属,机车类型和车次类型。
- [WIP] 从 RTL-TCP 获取数据
- 连接 RTL-TCP 服务器获取预警消息
[android](https://github.com/undef-i/LBJ_Console/tree/android) 分支包含项目早期基于 Android 平台的实现代码,已实现基本功能,现已停止开发。
本项目为个人业余项目,代码质量和实现细节可能不尽如人意,敬请见谅。
## 数据文件
LBJ Console 依赖以下数据文件,位于 `assets` 目录,用于支持机车配属和车次信息的展示:

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
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

View File

@@ -18,7 +18,7 @@ pluginManagement {
plugins {
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
}

View File

@@ -57,6 +57,7 @@
151,NJ2
152,东风7G
153,NDJ3
156,东风11Z
157,FXN3D
158,东风11G
160,HXN3
@@ -146,3 +147,4 @@
334,CJ5
335,CJ6
400,GCD-1000J
403,GX-160
1 001 解放
57 151 NJ2
58 152 东风7G
59 153 NDJ3
60 156 东风11Z
61 157 FXN3D
62 158 东风11G
63 160 HXN3
147 334 CJ5
148 335 CJ6
149 400 GCD-1000J
150 403 GX-160

View File

@@ -3,9 +3,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:lbjconsole/screens/main_screen.dart';
import 'package:lbjconsole/util/train_type_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/database_service.dart';
import 'package:lbjconsole/services/background_service.dart';
void main() async {

View File

@@ -45,7 +45,7 @@ class MapState {
@override
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

View File

@@ -340,22 +340,6 @@ class HistoryScreenState extends State<HistoryScreen> {
} catch (e) {}
}
String _getGroupKeyForRecord(TrainRecord record, MergeSettings settings) {
switch (settings.groupBy) {
case GroupBy.trainOnly:
return record.train.trim();
case GroupBy.locoOnly:
return record.loco.trim();
case GroupBy.trainAndLoco:
return '${record.train.trim()}-${record.loco.trim()}';
case GroupBy.trainOrLoco:
final train = record.train.trim();
if (train.isNotEmpty) return train;
final loco = record.loco.trim();
if (loco.isNotEmpty) return loco;
return '';
}
}
bool _hasDataChanged(List<Object> newItems) {
if (_displayItems.length != newItems.length) return true;

View File

@@ -3,8 +3,6 @@ import 'package:flutter/services.dart';
import 'dart:async';
import 'dart:developer' as developer;
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/map_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');
_settingsSubscription =
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) {
final rtlTcpEnabled = (settings?['rtlTcpEnabled'] ?? 0) == 1;
final rtlTcpEnabled = (settings['rtlTcpEnabled'] ?? 0) == 1;
if (rtlTcpEnabled != _rtlTcpEnabled) {
setState(() {
_rtlTcpEnabled = rtlTcpEnabled;
});
if (rtlTcpEnabled) {
final host = settings?['rtlTcpHost']?.toString() ?? '127.0.0.1';
final port = settings?['rtlTcpPort']?.toString() ?? '14423';
final host = settings['rtlTcpHost']?.toString() ?? '127.0.0.1';
final port = settings['rtlTcpPort']?.toString() ?? '14423';
_connectToRtlTcp(host, port);
} else {
_rtlTcpConnectionSubscription?.cancel();
@@ -661,11 +659,12 @@ class _PixelPerfectBluetoothDialogState
Future<void> _startScan() async {
if (_scanState == _ScanState.scanning) return;
if (mounted)
if (mounted) {
setState(() {
_devices.clear();
_scanState = _ScanState.scanning;
});
}
await widget.bleService.startScan(
timeout: const Duration(seconds: 8),
onScanResults: (devices) {
@@ -700,7 +699,7 @@ class _PixelPerfectBluetoothDialogState
Widget build(BuildContext context) {
final isConnected = widget.bleService.isConnected;
return AlertDialog(
title: Text(widget.rtlTcpEnabled ? 'RTL-TCP 模式' : '蓝牙设备'),
title: Text(widget.rtlTcpEnabled ? 'RTL-TCP 服务器' : '蓝牙设备'),
content: SizedBox(
width: double.maxFinite,
child: SingleChildScrollView(
@@ -784,7 +783,7 @@ class _PixelPerfectBluetoothDialogState
.titleMedium
?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text('$currentAddress',
Text(currentAddress,
style: TextStyle(color: isConnected ? Colors.green : Colors.grey)),
const SizedBox(height: 16),
if (_lastReceivedTime != null && isConnected) ...[

View File

@@ -1,5 +1,5 @@
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_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
@@ -26,19 +26,19 @@ class _MapScreenState extends State<MapScreen> {
double _currentRotation = 0.0;
bool _isMapInitialized = false;
bool _isFollowingLocation = false;
final bool _isFollowingLocation = false;
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),
'1hour': const Duration(hours: 1),
'6hours': const Duration(hours: 6),
'12hours': const Duration(hours: 12),
'24hours': const Duration(hours: 24),
'7days': const Duration(days: 7),
'30days': const Duration(days: 30),
};
@override
@@ -81,8 +81,8 @@ class _MapScreenState extends State<MapScreen> {
if (lat == 39.9042 && lon == 116.4074) {
} else if (lat == 0.0 && lon == 0.0) {
} else {
final beijingLat = 39.9042;
final beijingLon = 116.4074;
const beijingLat = 39.9042;
const beijingLon = 116.4074;
final distance =
_calculateDistance(lat, lon, beijingLat, beijingLon);
@@ -411,7 +411,7 @@ class _MapScreenState extends State<MapScreen> {
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.8),
color: Colors.black.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(3),
),
child: Text(
@@ -572,8 +572,8 @@ class _MapScreenState extends State<MapScreen> {
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.3),
.surfaceContainerHighest
.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(16),
),
child: Padding(

View File

@@ -32,7 +32,7 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
double _currentRotation = 0.0;
LatLng? _currentLocation;
LatLng? _lastTrainLocation;
bool _isDataLoaded = false;
final bool _isDataLoaded = false;
final Completer<void> _webViewReadyCompleter = Completer<void>();
@override
@@ -299,7 +299,7 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
_updateTrainMarkers();
}
});
} catch (e, stackTrace) {
} catch (e) {
setState(() {
_isLoading = false;
});

View File

@@ -30,7 +30,7 @@ class RealtimeScreenState extends State<RealtimeScreen> {
List<LatLng> _selectedGroupRoute = [];
List<Marker> _mapMarkers = [];
bool _showMap = true;
Set<String> _selectedGroupKeys = {};
final Set<String> _selectedGroupKeys = {};
LatLng? _userLocation;
bool _isLocationPermissionGranted = false;
Timer? _locationTimer;
@@ -376,8 +376,7 @@ class RealtimeScreenState extends State<RealtimeScreen> {
}
LatLng? _parsePositionFromRecord(TrainRecord record) {
if (record.positionInfo == null ||
record.positionInfo.isEmpty ||
if (record.positionInfo.isEmpty ||
record.positionInfo == '<NUL>') {
return null;
}
@@ -916,8 +915,8 @@ class RealtimeScreenState extends State<RealtimeScreen> {
flex: 1,
child: FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: const LatLng(35.8617, 104.1954),
options: const MapOptions(
initialCenter: LatLng(35.8617, 104.1954),
initialZoom: 2.0,
),
children: [

View File

@@ -4,17 +4,13 @@ import 'dart:io';
import 'package:lbjconsole/models/merged_record.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/themes/app_theme.dart';
import 'package:url_launcher/url_launcher.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:share_plus/share_plus.dart';
import 'package:cross_file/cross_file.dart';
class SettingsScreen extends StatefulWidget {
final VoidCallback? onSettingsChanged;
@@ -73,17 +69,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
Icon(Icons.wifi,
color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 12),
Text('RTL-TCP 接收', style: AppTheme.titleMedium),
const Text('RTL-TCP ', style: AppTheme.titleMedium),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('启用RTL-TCP接收', style: AppTheme.bodyLarge),
Text('启用 RTL-TCP', style: AppTheme.bodyLarge),
],
),
Switch(
@@ -94,7 +90,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
});
_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,
color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 12),
Text('蓝牙设备', style: AppTheme.titleMedium),
const Text('蓝牙设备', style: AppTheme.titleMedium),
],
),
const SizedBox(height: 16),
TextField(
controller: _deviceNameController,
decoration: InputDecoration(
labelText: '设备名称 (用于自动连接)',
labelText: '设备名称',
hintText: '输入设备名称',
labelStyle: const TextStyle(color: Colors.white70),
hintStyle: const TextStyle(color: Colors.white54),
@@ -329,14 +325,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
Icon(Icons.settings,
color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 12),
Text('应用设置', style: AppTheme.titleMedium),
const Text('应用设置', style: AppTheme.titleMedium),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('后台保活服务', style: AppTheme.bodyLarge),
@@ -356,7 +352,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
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(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('通知服务', style: AppTheme.bodyLarge),
@@ -378,7 +374,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
});
_saveSettings();
},
activeColor: Theme.of(context).colorScheme.primary,
activeThumbColor: Theme.of(context).colorScheme.primary,
),
],
),
@@ -386,7 +382,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('地图组件类型', style: AppTheme.bodyLarge),
@@ -394,7 +390,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
DropdownButton<String>(
value: _mapType,
items: [
items: const [
DropdownMenuItem(
value: 'webview',
child: Text('矢量铁路地图', style: AppTheme.bodyMedium),
@@ -422,7 +418,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('隐藏只有时间有效的记录', style: AppTheme.bodyLarge),
@@ -436,7 +432,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
});
_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,
color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 12),
Text('记录合并', style: AppTheme.titleMedium),
const Text('记录合并', style: AppTheme.titleMedium),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('启用记录合并', style: AppTheme.bodyLarge),
@@ -484,7 +480,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
});
_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,
children: [
const SizedBox(height: 16),
Text('分组方式', style: AppTheme.bodyLarge),
const Text('分组方式', style: AppTheme.bodyLarge),
const SizedBox(height: 8),
DropdownButtonFormField<GroupBy>(
value: _groupBy,
items: [
initialValue: _groupBy,
items: const [
DropdownMenuItem(
value: GroupBy.trainOnly,
child: Text('仅车次号', style: AppTheme.bodyMedium)),
@@ -532,11 +528,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
style: AppTheme.bodyMedium,
),
const SizedBox(height: 16),
Text('时间窗口', style: AppTheme.bodyLarge),
const Text('时间窗口', style: AppTheme.bodyLarge),
const SizedBox(height: 8),
DropdownButtonFormField<TimeWindow>(
value: _timeWindow,
items: [
initialValue: _timeWindow,
items: const [
DropdownMenuItem(
value: TimeWindow.oneHour,
child: Text('1小时内', style: AppTheme.bodyMedium)),
@@ -579,7 +575,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('隐藏不可分组记录', style: AppTheme.bodyLarge),
@@ -593,7 +589,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
});
_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,
color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 12),
Text('数据管理', style: AppTheme.titleMedium),
const Text('数据管理', style: AppTheme.titleMedium),
],
),
const SizedBox(height: 16),
@@ -637,7 +633,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildActionButton(
icon: Icons.file_download,
title: '导入数据',
subtitle: '从JSON文件导入记录和设置',
subtitle: ' JSON 文件导入记录和设置',
onTap: _importData,
),
const SizedBox(height: 12),
@@ -705,7 +701,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
],
),
),
Icon(
const Icon(
Icons.chevron_right,
color: Colors.white54,
size: 20,
@@ -760,7 +756,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (exportedPath != null) {
final file = File(exportedPath);
final fileName = file.path.split(Platform.pathSeparator).last;
await Share.shareXFiles(
[XFile(file.path)],
@@ -954,11 +949,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: [
Icon(Icons.info, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 12),
Text('关于', style: AppTheme.titleMedium),
const Text('关于', style: AppTheme.titleMedium),
],
),
const SizedBox(height: 16),
Text('LBJ Console', style: AppTheme.titleMedium),
const Text('LBJ Console', style: AppTheme.titleMedium),
const SizedBox(height: 8),
FutureBuilder<String>(
future: _getAppVersion(),
@@ -979,7 +974,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
await launchUrl(url);
}
},
child: Text(
child: const Text(
'https://github.com/undef-i/LBJConsole',
style: AppTheme.caption,
),

View File

@@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:ui';
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:lbjconsole/services/ble_service.dart';
@@ -107,7 +106,7 @@ class BackgroundService {
_notificationId,
'LBJ Console',
'蓝牙连接监控中',
NotificationDetails(
const NotificationDetails(
android: AndroidNotificationDetails(
_notificationChannelId,
_notificationChannelName,
@@ -146,7 +145,7 @@ class BackgroundService {
_notificationId,
'LBJ Console',
isConnected ? '蓝牙已连接 - $deviceStatus' : '蓝牙未连接 - 自动重连中',
NotificationDetails(
const NotificationDetails(
android: AndroidNotificationDetails(
_notificationChannelId,
_notificationChannelName,

View File

@@ -145,7 +145,9 @@ class BLEService {
if (isConnected ||
_isConnecting ||
_isManualDisconnect ||
_isAutoConnectBlocked) return;
_isAutoConnectBlocked) {
return;
}
for (var device in allFoundDevices) {
if (_shouldAutoConnectTo(device)) {
@@ -168,10 +170,13 @@ class BLEService {
final deviceAddress = device.remoteId.str;
if (_targetDeviceName.isNotEmpty &&
deviceName.toLowerCase() == _targetDeviceName.toLowerCase())
deviceName.toLowerCase() == _targetDeviceName.toLowerCase()) {
return true;
}
if (_lastKnownDeviceAddress != null &&
_lastKnownDeviceAddress == deviceAddress) return true;
_lastKnownDeviceAddress == deviceAddress) {
return true;
}
return false;
}

View File

@@ -27,7 +27,7 @@ class DatabaseService {
}
_database = await _initDatabase();
return _database!;
} catch (e, stackTrace) {
} catch (e) {
rethrow;
}
}
@@ -38,8 +38,6 @@ class DatabaseService {
return false;
}
final db = await database;
final result = await db.rawQuery('SELECT 1');
return true;
} catch (e) {
return false;
@@ -59,7 +57,7 @@ class DatabaseService {
);
return db;
} catch (e, stackTrace) {
} catch (e) {
rethrow;
}
}
@@ -206,7 +204,7 @@ class DatabaseService {
final records =
result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
return records;
} catch (e, stackTrace) {
} catch (e) {
rethrow;
}
}
@@ -239,7 +237,7 @@ class DatabaseService {
final records =
result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
return records;
} catch (e, stackTrace) {
} catch (e) {
rethrow;
}
}

View File

@@ -1,4 +1,3 @@
import 'package:lbjconsole/util/loco_type_util.dart';
class LocoTypeService {
static final LocoTypeService _instance = LocoTypeService._internal();

View File

@@ -111,7 +111,7 @@ class MergeService {
}
});
final reusedRecords = _reuseDiscardedRecords(
_reuseDiscardedRecords(
discardedRecords, mergedRecordIds, settings.groupBy);
final singleRecords = filteredRecords
@@ -283,100 +283,4 @@ class MergeService {
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

@@ -20,7 +20,7 @@ class NotificationService {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
final InitializationSettings initializationSettings =
const InitializationSettings initializationSettings =
InitializationSettings(
android: initializationSettingsAndroid,
);
@@ -61,7 +61,7 @@ class NotificationService {
return;
}
final String title = '列车信息';
const String title = '列车信息';
final String body = _buildNotificationContent(record);
final AndroidNotificationDetails androidPlatformChannelSpecifics =

View File

@@ -149,8 +149,9 @@ class _LbJState {
break;
case _lbjSyncAddr:
if (numeric.length >= 5)
if (numeric.length >= 5) {
time = "${numeric.substring(1, 3)}:${numeric.substring(3, 5)}";
}
break;
}
}
@@ -167,11 +168,11 @@ class _LbJState {
String gpsPosition = "";
if (posLatDeg.isNotEmpty && posLatMin.isNotEmpty) {
gpsPosition = "${posLatDeg}°${posLatMin}";
gpsPosition = "$posLatDeg°$posLatMin";
}
if (posLonDeg.isNotEmpty && posLonMin.isNotEmpty) {
gpsPosition +=
(gpsPosition.isEmpty ? "" : " ") + "${posLonDeg}°${posLonMin}";
"${gpsPosition.isEmpty ? "" : " "}$posLonDeg°$posLonMin";
}
String kmPosition = positionKm.replaceAll(' <NUL>', '');

View File

@@ -9,13 +9,11 @@ class AppTheme {
canvasColor: Colors.black,
cardColor: const Color(0xFF121212),
primaryColor: Colors.blue,
colorScheme: ColorScheme.dark(
colorScheme: const ColorScheme.dark(
primary: Colors.blue,
secondary: Colors.blueAccent,
surface: const Color(0xFF121212),
background: Colors.black,
surface: Color(0xFF121212),
onSurface: Colors.white,
onBackground: Colors.white,
),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.black,
@@ -67,16 +65,16 @@ class AppTheme {
thickness: 1,
),
switchTheme: SwitchThemeData(
thumbColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
thumbColor: WidgetStateProperty.resolveWith<Color?>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return Colors.blue;
}
return Colors.grey;
}),
trackColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
trackColor: WidgetStateProperty.resolveWith<Color?>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return Colors.blue.withOpacity(0.5);
}
return Colors.grey.withOpacity(0.5);

View File

@@ -1,4 +1,3 @@
import 'dart:convert';
import 'package:flutter/services.dart';
class LocoInfoUtil {

View File

@@ -1,4 +1,3 @@
import 'dart:convert';
import 'package:flutter/services.dart';
class TrainTypeUtil {

View File

@@ -241,6 +241,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: transitive
description:
@@ -529,21 +537,13 @@ packages:
source: hosted
version: "2.3.2"
hive:
dependency: "direct main"
dependency: transitive
description:
name: hive
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: "direct dev"
description:
@@ -688,30 +688,6 @@ 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:
@@ -760,14 +736,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: transitive
description:
@@ -960,14 +928,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: transitive
description:
@@ -1028,18 +988,18 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: "9f9f3d372d4304723e6136663bb291c0b93f5e4c8a4a6314347f481a33bda2b1"
sha256: "46a46fd64659eff15f4638bbe19de43f9483f0e0bf024a9fb6b3582064bacc7b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.7"
version: "2.4.17"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.4"
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:

View File

@@ -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.8.0-flutter+80
version: 0.8.1-flutter+81
environment:
sdk: ^3.5.4
@@ -34,10 +34,8 @@ dependencies:
cupertino_icons: ^1.0.8
flutter_blue_plus: ^1.31.15
permission_handler: ^11.3.1
provider: ^6.1.2
shared_preferences: ^2.3.2
hive: ^2.2.3
hive_flutter: ^1.1.0
path: ^1.9.0
path_provider: ^2.1.4
intl: ^0.19.0
@@ -46,6 +44,7 @@ dependencies:
latlong2: ^0.9.1
geolocator: ^13.0.4
geolocator_android: 4.6.1
url_launcher: ^6.2.5
sqflite: ^2.3.3+1
share_plus: ^10.0.0
@@ -55,7 +54,6 @@ dependencies:
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
gbk_codec: ^0.4.0
@@ -72,6 +70,7 @@ dev_dependencies:
hive_generator: ^2.0.1
build_runner: ^2.4.6
flutter_launcher_icons: ^0.14.1
dependency_validator: ^4.1.3
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec