feat: add vector railway map
This commit is contained in:
@@ -26,7 +26,7 @@ if (flutterVersionName == null) {
|
|||||||
android {
|
android {
|
||||||
namespace = "org.noxylva.lbjconsole.flutter"
|
namespace = "org.noxylva.lbjconsole.flutter"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
ndkVersion = "26.1.10909125"
|
ndkVersion = "28.1.13356709"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
|||||||
15038
assets/mapbox_map.html
Normal file
15038
assets/mapbox_map.html
Normal file
File diff suppressed because it is too large
Load Diff
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
@@ -18,11 +18,12 @@ class MergeSettings {
|
|||||||
final bool enabled;
|
final bool enabled;
|
||||||
final GroupBy groupBy;
|
final GroupBy groupBy;
|
||||||
final TimeWindow timeWindow;
|
final TimeWindow timeWindow;
|
||||||
|
final bool hideUngroupableRecords;
|
||||||
MergeSettings({
|
MergeSettings({
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.groupBy = GroupBy.trainAndLoco,
|
this.groupBy = GroupBy.trainAndLoco,
|
||||||
this.timeWindow = TimeWindow.unlimited,
|
this.timeWindow = TimeWindow.unlimited,
|
||||||
|
this.hideUngroupableRecords = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory MergeSettings.fromMap(Map<String, dynamic> map) {
|
factory MergeSettings.fromMap(Map<String, dynamic> map) {
|
||||||
@@ -36,6 +37,7 @@ class MergeSettings {
|
|||||||
(e) => e.name == map['timeWindow'],
|
(e) => e.name == map['timeWindow'],
|
||||||
orElse: () => TimeWindow.unlimited,
|
orElse: () => TimeWindow.unlimited,
|
||||||
),
|
),
|
||||||
|
hideUngroupableRecords: (map['hideUngroupableRecords'] ?? 0) == 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1383,7 +1383,7 @@ class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
|
|||||||
return FlutterMap(
|
return FlutterMap(
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
onPositionChanged: (position, hasGesture) => _onCameraMove(),
|
onPositionChanged: (position, hasGesture) => _onCameraMove(),
|
||||||
minZoom: 5,
|
minZoom: 8,
|
||||||
maxZoom: 18,
|
maxZoom: 18,
|
||||||
),
|
),
|
||||||
mapController: _mapController,
|
mapController: _mapController,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:lbjconsole/models/merged_record.dart';
|
|||||||
import 'package:lbjconsole/models/train_record.dart';
|
import 'package:lbjconsole/models/train_record.dart';
|
||||||
import 'package:lbjconsole/screens/history_screen.dart';
|
import 'package:lbjconsole/screens/history_screen.dart';
|
||||||
import 'package:lbjconsole/screens/map_screen.dart';
|
import 'package:lbjconsole/screens/map_screen.dart';
|
||||||
|
import 'package:lbjconsole/screens/map_webview_screen.dart';
|
||||||
import 'package:lbjconsole/screens/settings_screen.dart';
|
import 'package:lbjconsole/screens/settings_screen.dart';
|
||||||
import 'package:lbjconsole/services/ble_service.dart';
|
import 'package:lbjconsole/services/ble_service.dart';
|
||||||
import 'package:lbjconsole/services/database_service.dart';
|
import 'package:lbjconsole/services/database_service.dart';
|
||||||
@@ -174,6 +175,7 @@ class MainScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
|
String _mapType = 'webview';
|
||||||
|
|
||||||
late final BLEService _bleService;
|
late final BLEService _bleService;
|
||||||
final NotificationService _notificationService = NotificationService();
|
final NotificationService _notificationService = NotificationService();
|
||||||
@@ -195,8 +197,19 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
|||||||
_initializeServices();
|
_initializeServices();
|
||||||
_checkAndStartBackgroundService();
|
_checkAndStartBackgroundService();
|
||||||
_setupLastReceivedTimeListener();
|
_setupLastReceivedTimeListener();
|
||||||
|
_loadMapType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadMapType() async {
|
||||||
|
final settings = await DatabaseService.instance.getAllSettings() ?? {};
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_mapType = settings['mapType']?.toString() ?? 'webview';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> _checkAndStartBackgroundService() async {
|
Future<void> _checkAndStartBackgroundService() async {
|
||||||
final settings = await DatabaseService.instance.getAllSettings() ?? {};
|
final settings = await DatabaseService.instance.getAllSettings() ?? {};
|
||||||
final backgroundServiceEnabled =
|
final backgroundServiceEnabled =
|
||||||
@@ -231,6 +244,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
|||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
_bleService.onAppResume();
|
_bleService.onAppResume();
|
||||||
|
_loadMapType();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,8 +394,12 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
|||||||
onEditModeChanged: _handleHistoryEditModeChanged,
|
onEditModeChanged: _handleHistoryEditModeChanged,
|
||||||
onSelectionChanged: _handleSelectionChanged,
|
onSelectionChanged: _handleSelectionChanged,
|
||||||
),
|
),
|
||||||
const MapScreen(),
|
_mapType == 'map' ? const MapScreen() : const MapWebViewScreen(),
|
||||||
const SettingsScreen(),
|
SettingsScreen(
|
||||||
|
onSettingsChanged: () {
|
||||||
|
_loadMapType();
|
||||||
|
},
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -399,6 +417,10 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
|||||||
if (_currentIndex == 2 && index == 0) {
|
if (_currentIndex == 2 && index == 0) {
|
||||||
_historyScreenKey.currentState?.reloadRecords();
|
_historyScreenKey.currentState?.reloadRecords();
|
||||||
}
|
}
|
||||||
|
// 如果从设置页面切换到地图页面,重新加载地图类型
|
||||||
|
if (_currentIndex == 2 && index == 1) {
|
||||||
|
_loadMapType();
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
if (_isHistoryEditMode) _isHistoryEditMode = false;
|
if (_isHistoryEditMode) _isHistoryEditMode = false;
|
||||||
_currentIndex = index;
|
_currentIndex = index;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
LatLng? _currentLocation;
|
LatLng? _currentLocation;
|
||||||
LatLng? _lastTrainLocation;
|
LatLng? _lastTrainLocation;
|
||||||
LatLng? _userLocation;
|
LatLng? _userLocation;
|
||||||
double _currentZoom = 12.0;
|
double _currentZoom = 14.0;
|
||||||
double _currentRotation = 0.0;
|
double _currentRotation = 0.0;
|
||||||
|
|
||||||
bool _isMapInitialized = false;
|
bool _isMapInitialized = false;
|
||||||
@@ -51,10 +51,19 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
_loadSettings().then((_) {
|
_loadSettings().then((_) {
|
||||||
_loadTrainRecords().then((_) {
|
_loadTrainRecords().then((_) {
|
||||||
_startLocationUpdates();
|
_startLocationUpdates();
|
||||||
|
if (!_isMapInitialized && (_currentLocation != null || _lastTrainLocation != null || _userLocation != null)) {
|
||||||
|
_initializeMapPosition();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _checkDatabaseSettings() async {
|
Future<void> _checkDatabaseSettings() async {
|
||||||
try {
|
try {
|
||||||
final dbInfo = await DatabaseService.instance.getDatabaseInfo();
|
final dbInfo = await DatabaseService.instance.getDatabaseInfo();
|
||||||
@@ -227,6 +236,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
settings['mapCenterLon'] = center.longitude;
|
settings['mapCenterLon'] = center.longitude;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
settings['mapSettingsTimestamp'] = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
await DatabaseService.instance.updateSettings(settings);
|
await DatabaseService.instance.updateSettings(settings);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
@@ -734,11 +745,31 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final bool isDefaultLocation = _currentLocation == null &&
|
||||||
|
_lastTrainLocation == null &&
|
||||||
|
_userLocation == null;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF121212),
|
backgroundColor: const Color(0xFF121212),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
FlutterMap(
|
if (isDefaultLocation)
|
||||||
|
const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF007ACC)),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'正在加载地图位置...',
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else FlutterMap(
|
||||||
mapController: _mapController,
|
mapController: _mapController,
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
initialCenter: _currentLocation ??
|
initialCenter: _currentLocation ??
|
||||||
@@ -747,7 +778,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
const LatLng(39.9042, 116.4074),
|
const LatLng(39.9042, 116.4074),
|
||||||
initialZoom: _currentZoom,
|
initialZoom: _currentZoom,
|
||||||
initialRotation: _currentRotation,
|
initialRotation: _currentRotation,
|
||||||
minZoom: 4.0,
|
minZoom: 8.0,
|
||||||
maxZoom: 18.0,
|
maxZoom: 18.0,
|
||||||
onPositionChanged: (MapCamera camera, bool hasGesture) {
|
onPositionChanged: (MapCamera camera, bool hasGesture) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|||||||
1162
lib/screens/map_webview_screen.dart
Normal file
1162
lib/screens/map_webview_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,9 @@ import 'package:share_plus/share_plus.dart';
|
|||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:cross_file/cross_file.dart';
|
||||||
|
|
||||||
class SettingsScreen extends StatefulWidget {
|
class SettingsScreen extends StatefulWidget {
|
||||||
const SettingsScreen({super.key});
|
final VoidCallback? onSettingsChanged;
|
||||||
|
|
||||||
|
const SettingsScreen({super.key, this.onSettingsChanged});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||||
@@ -33,8 +35,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
int _recordCount = 0;
|
int _recordCount = 0;
|
||||||
bool _mergeRecordsEnabled = false;
|
bool _mergeRecordsEnabled = false;
|
||||||
bool _hideTimeOnlyRecords = false;
|
bool _hideTimeOnlyRecords = false;
|
||||||
|
bool _hideUngroupableRecords = false;
|
||||||
GroupBy _groupBy = GroupBy.trainAndLoco;
|
GroupBy _groupBy = GroupBy.trainAndLoco;
|
||||||
TimeWindow _timeWindow = TimeWindow.unlimited;
|
TimeWindow _timeWindow = TimeWindow.unlimited;
|
||||||
|
String _mapType = 'map';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -63,8 +67,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_notificationsEnabled = (settingsMap['notificationEnabled'] ?? 1) == 1;
|
_notificationsEnabled = (settingsMap['notificationEnabled'] ?? 1) == 1;
|
||||||
_mergeRecordsEnabled = settings.enabled;
|
_mergeRecordsEnabled = settings.enabled;
|
||||||
_hideTimeOnlyRecords = (settingsMap['hideTimeOnlyRecords'] ?? 0) == 1;
|
_hideTimeOnlyRecords = (settingsMap['hideTimeOnlyRecords'] ?? 0) == 1;
|
||||||
|
_hideUngroupableRecords = settings.hideUngroupableRecords;
|
||||||
_groupBy = settings.groupBy;
|
_groupBy = settings.groupBy;
|
||||||
_timeWindow = settings.timeWindow;
|
_timeWindow = settings.timeWindow;
|
||||||
|
_mapType = settingsMap['mapType']?.toString() ?? 'webview';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,9 +91,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
'notificationEnabled': _notificationsEnabled ? 1 : 0,
|
'notificationEnabled': _notificationsEnabled ? 1 : 0,
|
||||||
'mergeRecordsEnabled': _mergeRecordsEnabled ? 1 : 0,
|
'mergeRecordsEnabled': _mergeRecordsEnabled ? 1 : 0,
|
||||||
'hideTimeOnlyRecords': _hideTimeOnlyRecords ? 1 : 0,
|
'hideTimeOnlyRecords': _hideTimeOnlyRecords ? 1 : 0,
|
||||||
|
'hideUngroupableRecords': _hideUngroupableRecords ? 1 : 0,
|
||||||
'groupBy': _groupBy.name,
|
'groupBy': _groupBy.name,
|
||||||
'timeWindow': _timeWindow.name,
|
'timeWindow': _timeWindow.name,
|
||||||
|
'mapType': _mapType,
|
||||||
});
|
});
|
||||||
|
widget.onSettingsChanged?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -240,6 +249,43 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('地图显示方式', style: AppTheme.bodyLarge),
|
||||||
|
Text('选择地图组件类型', style: AppTheme.caption),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
DropdownButton<String>(
|
||||||
|
value: _mapType,
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'webview',
|
||||||
|
child: Text('矢量铁路地图', style: AppTheme.bodyMedium),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'map',
|
||||||
|
child: Text('栅格铁路地图', style: AppTheme.bodyMedium),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
setState(() {
|
||||||
|
_mapType = value;
|
||||||
|
});
|
||||||
|
_saveSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dropdownColor: AppTheme.secondaryBlack,
|
||||||
|
style: AppTheme.bodyMedium,
|
||||||
|
underline: Container(height: 0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -398,6 +444,29 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
dropdownColor: AppTheme.secondaryBlack,
|
dropdownColor: AppTheme.secondaryBlack,
|
||||||
style: AppTheme.bodyMedium,
|
style: AppTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('隐藏不可分组记录', style: AppTheme.bodyLarge),
|
||||||
|
Text('不显示无法分组的记录', style: AppTheme.caption),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Switch(
|
||||||
|
value: _hideUngroupableRecords,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_hideUngroupableRecords = value;
|
||||||
|
});
|
||||||
|
_saveSettings();
|
||||||
|
},
|
||||||
|
activeColor: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -421,7 +490,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.storage, color: Theme.of(context).colorScheme.primary),
|
Icon(Icons.storage,
|
||||||
|
color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text('数据管理', style: AppTheme.titleMedium),
|
Text('数据管理', style: AppTheme.titleMedium),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class DatabaseService {
|
|||||||
DatabaseService._internal();
|
DatabaseService._internal();
|
||||||
|
|
||||||
static const String _databaseName = 'train_database';
|
static const String _databaseName = 'train_database';
|
||||||
static const _databaseVersion = 4;
|
static const _databaseVersion = 6;
|
||||||
|
|
||||||
static const String trainRecordsTable = 'train_records';
|
static const String trainRecordsTable = 'train_records';
|
||||||
static const String appSettingsTable = 'app_settings';
|
static const String appSettingsTable = 'app_settings';
|
||||||
@@ -21,21 +21,47 @@ class DatabaseService {
|
|||||||
Database? _database;
|
Database? _database;
|
||||||
|
|
||||||
Future<Database> get database async {
|
Future<Database> get database async {
|
||||||
if (_database != null) return _database!;
|
try {
|
||||||
_database = await _initDatabase();
|
if (_database != null) {
|
||||||
return _database!;
|
return _database!;
|
||||||
|
}
|
||||||
|
_database = await _initDatabase();
|
||||||
|
return _database!;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> isDatabaseConnected() async {
|
||||||
|
try {
|
||||||
|
if (_database == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final db = await database;
|
||||||
|
final result = await db.rawQuery('SELECT 1');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Database> _initDatabase() async {
|
Future<Database> _initDatabase() async {
|
||||||
final directory = await getApplicationDocumentsDirectory();
|
try {
|
||||||
final path = join(directory.path, _databaseName);
|
final directory = await getApplicationDocumentsDirectory();
|
||||||
|
final path = join(directory.path, _databaseName);
|
||||||
|
|
||||||
return await openDatabase(
|
final db = await openDatabase(
|
||||||
path,
|
path,
|
||||||
version: _databaseVersion,
|
version: _databaseVersion,
|
||||||
onCreate: _onCreate,
|
onCreate: _onCreate,
|
||||||
onUpgrade: _onUpgrade,
|
onUpgrade: _onUpgrade,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return db;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
||||||
@@ -51,8 +77,15 @@ class DatabaseService {
|
|||||||
try {
|
try {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'ALTER TABLE $appSettingsTable ADD COLUMN mapTimeFilter TEXT NOT NULL DEFAULT "unlimited"');
|
'ALTER TABLE $appSettingsTable ADD COLUMN mapTimeFilter TEXT NOT NULL DEFAULT "unlimited"');
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 5) {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $appSettingsTable ADD COLUMN mapType TEXT NOT NULL DEFAULT "webview"');
|
||||||
|
}
|
||||||
|
if (oldVersion < 6) {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $appSettingsTable ADD COLUMN hideUngroupableRecords INTEGER NOT NULL DEFAULT 0');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +125,7 @@ class DatabaseService {
|
|||||||
mapZoomLevel REAL NOT NULL DEFAULT 10.0,
|
mapZoomLevel REAL NOT NULL DEFAULT 10.0,
|
||||||
mapRailwayLayerVisible INTEGER NOT NULL DEFAULT 1,
|
mapRailwayLayerVisible INTEGER NOT NULL DEFAULT 1,
|
||||||
mapRotation REAL NOT NULL DEFAULT 0.0,
|
mapRotation REAL NOT NULL DEFAULT 0.0,
|
||||||
|
mapType TEXT NOT NULL DEFAULT 'webview',
|
||||||
specifiedDeviceAddress TEXT,
|
specifiedDeviceAddress TEXT,
|
||||||
searchOrderList TEXT NOT NULL DEFAULT '',
|
searchOrderList TEXT NOT NULL DEFAULT '',
|
||||||
autoConnectEnabled INTEGER NOT NULL DEFAULT 1,
|
autoConnectEnabled INTEGER NOT NULL DEFAULT 1,
|
||||||
@@ -101,7 +135,8 @@ class DatabaseService {
|
|||||||
hideTimeOnlyRecords INTEGER NOT NULL DEFAULT 0,
|
hideTimeOnlyRecords INTEGER NOT NULL DEFAULT 0,
|
||||||
groupBy TEXT NOT NULL DEFAULT 'trainAndLoco',
|
groupBy TEXT NOT NULL DEFAULT 'trainAndLoco',
|
||||||
timeWindow TEXT NOT NULL DEFAULT 'unlimited',
|
timeWindow TEXT NOT NULL DEFAULT 'unlimited',
|
||||||
mapTimeFilter TEXT NOT NULL DEFAULT 'unlimited'
|
mapTimeFilter TEXT NOT NULL DEFAULT 'unlimited',
|
||||||
|
hideUngroupableRecords INTEGER NOT NULL DEFAULT 0
|
||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
|
|
||||||
@@ -118,6 +153,7 @@ class DatabaseService {
|
|||||||
'mapZoomLevel': 10.0,
|
'mapZoomLevel': 10.0,
|
||||||
'mapRailwayLayerVisible': 1,
|
'mapRailwayLayerVisible': 1,
|
||||||
'mapRotation': 0.0,
|
'mapRotation': 0.0,
|
||||||
|
'mapType': 'webview',
|
||||||
'searchOrderList': '',
|
'searchOrderList': '',
|
||||||
'autoConnectEnabled': 1,
|
'autoConnectEnabled': 1,
|
||||||
'backgroundServiceEnabled': 0,
|
'backgroundServiceEnabled': 0,
|
||||||
@@ -127,6 +163,7 @@ class DatabaseService {
|
|||||||
'groupBy': 'trainAndLoco',
|
'groupBy': 'trainAndLoco',
|
||||||
'timeWindow': 'unlimited',
|
'timeWindow': 'unlimited',
|
||||||
'mapTimeFilter': 'unlimited',
|
'mapTimeFilter': 'unlimited',
|
||||||
|
'hideUngroupableRecords': 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,12 +177,18 @@ class DatabaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<TrainRecord>> getAllRecords() async {
|
Future<List<TrainRecord>> getAllRecords() async {
|
||||||
final db = await database;
|
try {
|
||||||
final result = await db.query(
|
final db = await database;
|
||||||
trainRecordsTable,
|
final result = await db.query(
|
||||||
orderBy: 'timestamp DESC',
|
trainRecordsTable,
|
||||||
);
|
orderBy: 'timestamp DESC',
|
||||||
return result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
|
);
|
||||||
|
final records =
|
||||||
|
result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
|
||||||
|
return records;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<TrainRecord>> getRecordsWithinTimeRange(Duration duration) async {
|
Future<List<TrainRecord>> getRecordsWithinTimeRange(Duration duration) async {
|
||||||
@@ -162,15 +205,23 @@ class DatabaseService {
|
|||||||
|
|
||||||
Future<List<TrainRecord>> getRecordsWithinReceivedTimeRange(
|
Future<List<TrainRecord>> getRecordsWithinReceivedTimeRange(
|
||||||
Duration duration) async {
|
Duration duration) async {
|
||||||
final db = await database;
|
try {
|
||||||
final cutoffTime = DateTime.now().subtract(duration).millisecondsSinceEpoch;
|
final db = await database;
|
||||||
final result = await db.query(
|
final cutoffTime =
|
||||||
trainRecordsTable,
|
DateTime.now().subtract(duration).millisecondsSinceEpoch;
|
||||||
where: 'receivedTimestamp >= ?',
|
|
||||||
whereArgs: [cutoffTime],
|
final result = await db.query(
|
||||||
orderBy: 'receivedTimestamp DESC',
|
trainRecordsTable,
|
||||||
);
|
where: 'receivedTimestamp >= ?',
|
||||||
return result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
|
whereArgs: [cutoffTime],
|
||||||
|
orderBy: 'receivedTimestamp DESC',
|
||||||
|
);
|
||||||
|
final records =
|
||||||
|
result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
|
||||||
|
return records;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> deleteRecord(String uniqueId) async {
|
Future<int> deleteRecord(String uniqueId) async {
|
||||||
|
|||||||
@@ -2,6 +2,37 @@ import 'package:lbjconsole/models/train_record.dart';
|
|||||||
import 'package:lbjconsole/models/merged_record.dart';
|
import 'package:lbjconsole/models/merged_record.dart';
|
||||||
|
|
||||||
class MergeService {
|
class MergeService {
|
||||||
|
static bool isNeverGroupableRecord(TrainRecord record, GroupBy groupBy) {
|
||||||
|
final train = record.train.trim();
|
||||||
|
final loco = record.loco.trim();
|
||||||
|
|
||||||
|
final hasValidTrain =
|
||||||
|
train.isNotEmpty && train != "<NUL>" && !train.contains("-----");
|
||||||
|
final hasValidLoco = loco.isNotEmpty && loco != "<NUL>";
|
||||||
|
|
||||||
|
switch (groupBy) {
|
||||||
|
case GroupBy.trainOnly:
|
||||||
|
return !hasValidTrain;
|
||||||
|
|
||||||
|
case GroupBy.locoOnly:
|
||||||
|
return !hasValidLoco;
|
||||||
|
|
||||||
|
case GroupBy.trainAndLoco:
|
||||||
|
return !hasValidTrain || !hasValidLoco;
|
||||||
|
|
||||||
|
case GroupBy.trainOrLoco:
|
||||||
|
return !hasValidTrain && !hasValidLoco;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<TrainRecord> filterUngroupableRecords(
|
||||||
|
List<TrainRecord> records, GroupBy groupBy, bool hideUngroupable) {
|
||||||
|
if (!hideUngroupable) return records;
|
||||||
|
return records
|
||||||
|
.where((record) => !isNeverGroupableRecord(record, groupBy))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
static String? _generateGroupKey(TrainRecord record, GroupBy groupBy) {
|
static String? _generateGroupKey(TrainRecord record, GroupBy groupBy) {
|
||||||
final train = record.train.trim();
|
final train = record.train.trim();
|
||||||
final loco = record.loco.trim();
|
final loco = record.loco.trim();
|
||||||
@@ -36,15 +67,19 @@ class MergeService {
|
|||||||
return allRecords;
|
return allRecords;
|
||||||
}
|
}
|
||||||
|
|
||||||
allRecords
|
final filteredRecords = filterUngroupableRecords(
|
||||||
|
allRecords, settings.groupBy, settings.hideUngroupableRecords);
|
||||||
|
|
||||||
|
filteredRecords
|
||||||
.sort((a, b) => b.receivedTimestamp.compareTo(a.receivedTimestamp));
|
.sort((a, b) => b.receivedTimestamp.compareTo(a.receivedTimestamp));
|
||||||
|
|
||||||
if (settings.groupBy == GroupBy.trainOrLoco) {
|
if (settings.groupBy == GroupBy.trainOrLoco) {
|
||||||
return _groupByTrainOrLocoWithTimeWindow(allRecords, settings.timeWindow);
|
return _groupByTrainOrLocoWithTimeWindow(
|
||||||
|
filteredRecords, settings.timeWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
final groupedRecords = <String, List<TrainRecord>>{};
|
final groupedRecords = <String, List<TrainRecord>>{};
|
||||||
for (final record in allRecords) {
|
for (final record in filteredRecords) {
|
||||||
final key = _generateGroupKey(record, settings.groupBy);
|
final key = _generateGroupKey(record, settings.groupBy);
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
groupedRecords.putIfAbsent(key, () => []).add(record);
|
groupedRecords.putIfAbsent(key, () => []).add(record);
|
||||||
@@ -79,8 +114,9 @@ class MergeService {
|
|||||||
final reusedRecords = _reuseDiscardedRecords(
|
final reusedRecords = _reuseDiscardedRecords(
|
||||||
discardedRecords, mergedRecordIds, settings.groupBy);
|
discardedRecords, mergedRecordIds, settings.groupBy);
|
||||||
|
|
||||||
final singleRecords =
|
final singleRecords = filteredRecords
|
||||||
allRecords.where((r) => !mergedRecordIds.contains(r.uniqueId)).toList();
|
.where((r) => !mergedRecordIds.contains(r.uniqueId))
|
||||||
|
.toList();
|
||||||
|
|
||||||
final List<Object> mixedList = [...mergedRecords, ...singleRecords];
|
final List<Object> mixedList = [...mergedRecords, ...singleRecords];
|
||||||
mixedList.sort((a, b) {
|
mixedList.sort((a, b) {
|
||||||
@@ -219,7 +255,6 @@ class MergeService {
|
|||||||
latestRecord: processedGroup.first,
|
latestRecord: processedGroup.first,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
// 处理被丢弃的记录
|
|
||||||
for (final record in group) {
|
for (final record in group) {
|
||||||
if (!processedGroup.contains(record)) {
|
if (!processedGroup.contains(record)) {
|
||||||
singleRecords.add(record);
|
singleRecords.add(record);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import share_plus
|
|||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
import webview_flutter_wkwebview
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
@@ -27,4 +28,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
100
pubspec.lock
100
pubspec.lock
@@ -233,6 +233,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.11"
|
version: "0.7.11"
|
||||||
|
executor_lib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: executor_lib
|
||||||
|
sha256: "95ddf2957d9942d9702855b38dd49677f0ee6a8b77d7b16c0e509c7669d17386"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -656,6 +664,30 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
maplibre_gl:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: maplibre_gl
|
||||||
|
sha256: "5c7b1008396b2a321bada7d986ed60f9423406fbc7bd16f7ce91b385dfa054cd"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.22.0"
|
||||||
|
maplibre_gl_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: maplibre_gl_platform_interface
|
||||||
|
sha256: "08ee0a2d0853ea945a0ab619d52c0c714f43144145cd67478fc6880b52f37509"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.22.0"
|
||||||
|
maplibre_gl_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: maplibre_gl_web
|
||||||
|
sha256: "2b13d4b1955a9a54e38a718f2324e56e4983c080fc6de316f6f4b5458baacb58"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.22.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -896,6 +928,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
|
protobuf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: protobuf
|
||||||
|
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1261,6 +1301,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.1"
|
version: "4.5.1"
|
||||||
|
vector_map_tiles:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: vector_map_tiles
|
||||||
|
sha256: "4dc9243195c1a49c7be82cc1caed0d300242bb94381752af5f6868d9d1404e25"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "8.0.0"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1269,6 +1317,22 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
vector_tile:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_tile
|
||||||
|
sha256: "7ae290246e3a8734422672dbe791d3f7b8ab631734489fc6d405f1cc2080e38c"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
|
vector_tile_renderer:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_tile_renderer
|
||||||
|
sha256: "89746f1108eccbc0b6f33fbbef3fcf394cda3733fc0d5064ea03d53a459b56d3"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "5.2.1"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1309,6 +1373,38 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.3"
|
||||||
|
webview_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: webview_flutter
|
||||||
|
sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "4.13.0"
|
||||||
|
webview_flutter_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webview_flutter_android
|
||||||
|
sha256: "3c4eb4fcc252b40c2b5ce7be20d0481428b70f3ff589b0a8b8aaeb64c6bed701"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "4.10.2"
|
||||||
|
webview_flutter_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webview_flutter_platform_interface
|
||||||
|
sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.14.0"
|
||||||
|
webview_flutter_wkwebview:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webview_flutter_wkwebview
|
||||||
|
sha256: fea63576b3b7e02b2df8b78ba92b48ed66caec2bb041e9a0b1cbd586d5d80bfd
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.23.1"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1350,5 +1446,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.0-0 <4.0.0"
|
dart: ">=3.9.0 <4.0.0"
|
||||||
flutter: ">=3.24.0"
|
flutter: ">=3.35.0"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.4.0-flutter+40
|
version: 0.5.0-flutter+50
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
@@ -54,6 +54,9 @@ dependencies:
|
|||||||
msix: ^3.16.12
|
msix: ^3.16.12
|
||||||
flutter_background_service: ^5.1.0
|
flutter_background_service: ^5.1.0
|
||||||
scrollview_observer: ^1.20.0
|
scrollview_observer: ^1.20.0
|
||||||
|
vector_map_tiles: ^8.0.0
|
||||||
|
maplibre_gl: ^0.22.0
|
||||||
|
webview_flutter: ^4.8.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -84,6 +87,7 @@ flutter:
|
|||||||
- assets/loco_info.csv
|
- assets/loco_info.csv
|
||||||
- assets/train_number_info.csv
|
- assets/train_number_info.csv
|
||||||
- assets/loco_type_info.csv
|
- assets/loco_type_info.csv
|
||||||
|
- assets/mapbox_map.html
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|||||||
Reference in New Issue
Block a user