feat: add vector railway map

This commit is contained in:
Nedifinita
2025-09-29 18:44:15 +08:00
parent bfd05bd249
commit 6718ef7129
14 changed files with 16565 additions and 49 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

3
devtools_options.yaml Normal file
View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@@ -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,
); );
} }
} }

View File

@@ -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,

View File

@@ -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;

View File

@@ -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(() {

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,9 @@ import 'package:share_plus/share_plus.dart';
import 'package:cross_file/cross_file.dart'; 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),
], ],

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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"))
} }

View File

@@ -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"

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 # 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