Compare commits
4 Commits
v0.2.0-flu
...
v0.3.0-flu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64401a6ce9 | ||
|
|
72f9dfe17b | ||
|
|
bf850eed38 | ||
|
|
56689fc993 |
@@ -149,7 +149,7 @@ class TrainRecord {
|
|||||||
final lbjClassValue = lbjClass.trim();
|
final lbjClassValue = lbjClass.trim();
|
||||||
final trainValue = train.trim();
|
final trainValue = train.trim();
|
||||||
|
|
||||||
if (trainValue == "<NUL>") {
|
if (trainValue == "<NUL>" || trainValue.contains("-----")) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:scrollview_observer/scrollview_observer.dart';
|
||||||
import '../models/merged_record.dart';
|
import '../models/merged_record.dart';
|
||||||
import '../services/database_service.dart';
|
import '../services/database_service.dart';
|
||||||
import '../models/train_record.dart';
|
import '../models/train_record.dart';
|
||||||
@@ -31,6 +35,9 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
final Set<String> _selectedRecords = {};
|
final Set<String> _selectedRecords = {};
|
||||||
final Map<String, bool> _expandedStates = {};
|
final Map<String, bool> _expandedStates = {};
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
final ListObserverController _observerController =
|
||||||
|
ListObserverController(controller: null)..cacheJumpIndexOffset = false;
|
||||||
|
late final ChatScrollObserver _chatObserver;
|
||||||
bool _isAtTop = true;
|
bool _isAtTop = true;
|
||||||
MergeSettings _mergeSettings = MergeSettings();
|
MergeSettings _mergeSettings = MergeSettings();
|
||||||
double _itemHeightCache = 0.0;
|
double _itemHeightCache = 0.0;
|
||||||
@@ -53,9 +60,17 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> reloadRecords() async {
|
||||||
|
await loadRecords(scrollToTop: false);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_chatObserver = ChatScrollObserver(_observerController)
|
||||||
|
..toRebuildScrollViewCallback = () {
|
||||||
|
setState(() {});
|
||||||
|
};
|
||||||
_scrollController.addListener(() {
|
_scrollController.addListener(() {
|
||||||
if (_scrollController.position.atEdge) {
|
if (_scrollController.position.atEdge) {
|
||||||
if (_scrollController.position.pixels == 0) {
|
if (_scrollController.position.pixels == 0) {
|
||||||
@@ -73,6 +88,7 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
|
_observerController.controller?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +97,71 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
final allRecords = await DatabaseService.instance.getAllRecords();
|
final allRecords = await DatabaseService.instance.getAllRecords();
|
||||||
final settingsMap = await DatabaseService.instance.getAllSettings() ?? {};
|
final settingsMap = await DatabaseService.instance.getAllSettings() ?? {};
|
||||||
_mergeSettings = MergeSettings.fromMap(settingsMap);
|
_mergeSettings = MergeSettings.fromMap(settingsMap);
|
||||||
final items = MergeService.getMixedList(allRecords, _mergeSettings);
|
|
||||||
|
List<TrainRecord> filteredRecords = allRecords;
|
||||||
|
if ((settingsMap['hideTimeOnlyRecords'] ?? 0) == 1) {
|
||||||
|
int hiddenCount = 0;
|
||||||
|
int shownCount = 0;
|
||||||
|
|
||||||
|
filteredRecords = allRecords.where((record) {
|
||||||
|
bool isFieldMeaningful(String field) {
|
||||||
|
if (field.isEmpty) return false;
|
||||||
|
String cleaned = field.replaceAll('<NUL>', '').trim();
|
||||||
|
if (cleaned.isEmpty) return false;
|
||||||
|
if (cleaned.runes
|
||||||
|
.every((r) => r == '*'.runes.first || r == ' '.runes.first))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasTrainNumber = isFieldMeaningful(record.fullTrainNumber) &&
|
||||||
|
!record.fullTrainNumber.contains("-----");
|
||||||
|
|
||||||
|
final hasDirection = record.direction == 1 || record.direction == 3;
|
||||||
|
|
||||||
|
final hasLocoInfo = isFieldMeaningful(record.locoType) ||
|
||||||
|
isFieldMeaningful(record.loco);
|
||||||
|
|
||||||
|
final hasRoute = isFieldMeaningful(record.route);
|
||||||
|
|
||||||
|
final hasPosition = isFieldMeaningful(record.position);
|
||||||
|
|
||||||
|
final hasSpeed =
|
||||||
|
isFieldMeaningful(record.speed) && record.speed != "NUL";
|
||||||
|
|
||||||
|
final hasPositionInfo = isFieldMeaningful(record.positionInfo);
|
||||||
|
|
||||||
|
final hasTrainType =
|
||||||
|
isFieldMeaningful(record.trainType) && record.trainType != "未知";
|
||||||
|
|
||||||
|
final hasLbjClass =
|
||||||
|
isFieldMeaningful(record.lbjClass) && record.lbjClass != "NA";
|
||||||
|
|
||||||
|
final hasTrain = isFieldMeaningful(record.train) &&
|
||||||
|
!record.train.contains("-----");
|
||||||
|
|
||||||
|
final shouldShow = hasTrainNumber ||
|
||||||
|
hasDirection ||
|
||||||
|
hasLocoInfo ||
|
||||||
|
hasRoute ||
|
||||||
|
hasPosition ||
|
||||||
|
hasSpeed ||
|
||||||
|
hasPositionInfo ||
|
||||||
|
hasTrainType ||
|
||||||
|
hasLbjClass ||
|
||||||
|
hasTrain;
|
||||||
|
|
||||||
|
if (!shouldShow) {
|
||||||
|
hiddenCount++;
|
||||||
|
} else {
|
||||||
|
shownCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldShow;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
final items = MergeService.getMixedList(filteredRecords, _mergeSettings);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final hasDataChanged = _hasDataChanged(items);
|
final hasDataChanged = _hasDataChanged(items);
|
||||||
@@ -112,6 +192,58 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
final settingsMap = await DatabaseService.instance.getAllSettings() ?? {};
|
final settingsMap = await DatabaseService.instance.getAllSettings() ?? {};
|
||||||
_mergeSettings = MergeSettings.fromMap(settingsMap);
|
_mergeSettings = MergeSettings.fromMap(settingsMap);
|
||||||
|
|
||||||
|
if ((settingsMap['hideTimeOnlyRecords'] ?? 0) == 1) {
|
||||||
|
bool isFieldMeaningful(String field) {
|
||||||
|
if (field.isEmpty) return false;
|
||||||
|
String cleaned = field.replaceAll('<NUL>', '').trim();
|
||||||
|
if (cleaned.isEmpty) return false;
|
||||||
|
if (cleaned.runes
|
||||||
|
.every((r) => r == '*'.runes.first || r == ' '.runes.first))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasTrainNumber = isFieldMeaningful(newRecord.fullTrainNumber) &&
|
||||||
|
!newRecord.fullTrainNumber.contains("-----");
|
||||||
|
|
||||||
|
final hasDirection =
|
||||||
|
newRecord.direction == 1 || newRecord.direction == 3;
|
||||||
|
|
||||||
|
final hasLocoInfo = isFieldMeaningful(newRecord.locoType) ||
|
||||||
|
isFieldMeaningful(newRecord.loco);
|
||||||
|
|
||||||
|
final hasRoute = isFieldMeaningful(newRecord.route);
|
||||||
|
|
||||||
|
final hasPosition = isFieldMeaningful(newRecord.position);
|
||||||
|
|
||||||
|
final hasSpeed =
|
||||||
|
isFieldMeaningful(newRecord.speed) && newRecord.speed != "NUL";
|
||||||
|
|
||||||
|
final hasPositionInfo = isFieldMeaningful(newRecord.positionInfo);
|
||||||
|
|
||||||
|
final hasTrainType = isFieldMeaningful(newRecord.trainType) &&
|
||||||
|
newRecord.trainType != "未知";
|
||||||
|
|
||||||
|
final hasLbjClass =
|
||||||
|
isFieldMeaningful(newRecord.lbjClass) && newRecord.lbjClass != "NA";
|
||||||
|
|
||||||
|
final hasTrain = isFieldMeaningful(newRecord.train) &&
|
||||||
|
!newRecord.train.contains("-----");
|
||||||
|
|
||||||
|
if (!hasTrainNumber &&
|
||||||
|
!hasDirection &&
|
||||||
|
!hasLocoInfo &&
|
||||||
|
!hasRoute &&
|
||||||
|
!hasPosition &&
|
||||||
|
!hasSpeed &&
|
||||||
|
!hasPositionInfo &&
|
||||||
|
!hasTrainType &&
|
||||||
|
!hasLbjClass &&
|
||||||
|
!hasTrain) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final isNewRecord = !_displayItems.any((item) {
|
final isNewRecord = !_displayItems.any((item) {
|
||||||
if (item is TrainRecord) {
|
if (item is TrainRecord) {
|
||||||
return item.uniqueId == newRecord.uniqueId;
|
return item.uniqueId == newRecord.uniqueId;
|
||||||
@@ -123,44 +255,29 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
|
|
||||||
if (!isNewRecord) return;
|
if (!isNewRecord) return;
|
||||||
|
|
||||||
|
final allRecords = await DatabaseService.instance.getAllRecords();
|
||||||
|
final items = MergeService.getMixedList(allRecords, _mergeSettings);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final previousScrollOffset = _scrollController.hasClients ? _scrollController.offset : 0.0;
|
if (!_isAtTop) {
|
||||||
final previousItemCount = _displayItems.length;
|
_chatObserver.standby();
|
||||||
|
}
|
||||||
final allRecords = await DatabaseService.instance.getAllRecords();
|
|
||||||
final items = MergeService.getMixedList(allRecords, _mergeSettings);
|
|
||||||
|
|
||||||
setState(() {
|
final hasDataChanged = _hasDataChanged(items);
|
||||||
_displayItems.clear();
|
if (hasDataChanged) {
|
||||||
_displayItems.addAll(items);
|
setState(() {
|
||||||
});
|
_displayItems.clear();
|
||||||
|
_displayItems.addAll(items);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (_scrollController.hasClients) {
|
if (_isAtTop && _scrollController.hasClients) {
|
||||||
if (_isAtTop) {
|
_scrollController.jumpTo(0.0);
|
||||||
_scrollController.jumpTo(0.0);
|
|
||||||
} else {
|
|
||||||
final newItemCount = items.length;
|
|
||||||
final itemDifference = newItemCount - previousItemCount;
|
|
||||||
|
|
||||||
if (itemDifference > 0 && previousScrollOffset > 0) {
|
|
||||||
final itemHeight = _getEstimatedItemHeight();
|
|
||||||
final adjustedOffset = previousScrollOffset + (itemDifference * itemHeight);
|
|
||||||
|
|
||||||
_scrollController.jumpTo(adjustedOffset.clamp(0.0, _scrollController.position.maxScrollExtent));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
double _getEstimatedItemHeight() {
|
|
||||||
if (_itemHeightCache > 0) {
|
|
||||||
return _itemHeightCache;
|
|
||||||
}
|
|
||||||
return 85.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _hasDataChanged(List<Object> newItems) {
|
bool _hasDataChanged(List<Object> newItems) {
|
||||||
if (_displayItems.length != newItems.length) return true;
|
if (_displayItems.length != newItems.length) return true;
|
||||||
|
|
||||||
@@ -194,8 +311,12 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
Text('暂无记录', style: TextStyle(color: Colors.white, fontSize: 18))
|
Text('暂无记录', style: TextStyle(color: Colors.white, fontSize: 18))
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListViewObserver(
|
||||||
|
controller: _observerController,
|
||||||
|
child: ListView.builder(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
|
physics: ChatObserverClampingScrollPhysics(observer: _chatObserver),
|
||||||
|
shrinkWrap: _chatObserver.isShrinkWrap,
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
itemCount: _displayItems.length,
|
itemCount: _displayItems.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@@ -203,10 +324,12 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
if (item is MergedTrainRecord) {
|
if (item is MergedTrainRecord) {
|
||||||
return _buildMergedRecordCard(item);
|
return _buildMergedRecordCard(item);
|
||||||
} else if (item is TrainRecord) {
|
} else if (item is TrainRecord) {
|
||||||
return _buildRecordCard(item);
|
return _buildRecordCard(item, key: ValueKey(item.uniqueId));
|
||||||
}
|
}
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
});
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMergedRecordCard(MergedTrainRecord mergedRecord) {
|
Widget _buildMergedRecordCard(MergedTrainRecord mergedRecord) {
|
||||||
@@ -506,15 +629,16 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
return 10.0;
|
return 10.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRecordCard(TrainRecord record, {bool isSubCard = false}) {
|
Widget _buildRecordCard(TrainRecord record,
|
||||||
|
{bool isSubCard = false, Key? key}) {
|
||||||
final isSelected = _selectedRecords.contains(record.uniqueId);
|
final isSelected = _selectedRecords.contains(record.uniqueId);
|
||||||
final isExpanded =
|
final isExpanded =
|
||||||
!isSubCard && (_expandedStates[record.uniqueId] ?? false);
|
!isSubCard && (_expandedStates[record.uniqueId] ?? false);
|
||||||
|
|
||||||
final GlobalKey itemKey = GlobalKey();
|
final GlobalKey itemKey = GlobalKey();
|
||||||
|
|
||||||
final Widget card = Card(
|
final Widget card = Card(
|
||||||
key: itemKey,
|
key: key ?? itemKey,
|
||||||
color: isSelected && _isEditMode
|
color: isSelected && _isEditMode
|
||||||
? const Color(0xFF2E2E2E)
|
? const Color(0xFF2E2E2E)
|
||||||
: const Color(0xFF1E1E1E),
|
: const Color(0xFF1E1E1E),
|
||||||
@@ -541,15 +665,23 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
});
|
});
|
||||||
} else if (!isSubCard) {
|
} else if (!isSubCard) {
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
setState(() {
|
final shouldUpdate =
|
||||||
_expandedStates[record.uniqueId] = false;
|
_expandedStates[record.uniqueId] == true ||
|
||||||
_mapOptimalZoom.remove(record.uniqueId);
|
_mapOptimalZoom.containsKey(record.uniqueId) ||
|
||||||
_mapCalculating.remove(record.uniqueId);
|
_mapCalculating.containsKey(record.uniqueId);
|
||||||
});
|
if (shouldUpdate) {
|
||||||
|
setState(() {
|
||||||
|
_expandedStates[record.uniqueId] = false;
|
||||||
|
_mapOptimalZoom.remove(record.uniqueId);
|
||||||
|
_mapCalculating.remove(record.uniqueId);
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
if (_expandedStates[record.uniqueId] != true) {
|
||||||
_expandedStates[record.uniqueId] = true;
|
setState(() {
|
||||||
});
|
_expandedStates[record.uniqueId] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -568,12 +700,13 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
_buildRecordHeader(record),
|
_buildRecordHeader(record),
|
||||||
_buildPositionAndSpeed(record),
|
_buildPositionAndSpeed(record),
|
||||||
_buildLocoInfo(record),
|
_buildLocoInfo(record),
|
||||||
if (isExpanded) _buildExpandedContent(record)
|
if (isExpanded) _buildExpandedContent(record),
|
||||||
]))));
|
]))));
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (_itemHeightCache <= 0 && itemKey.currentContext != null) {
|
if (_itemHeightCache <= 0 && itemKey.currentContext != null) {
|
||||||
final RenderBox renderBox = itemKey.currentContext!.findRenderObject() as RenderBox;
|
final RenderBox renderBox =
|
||||||
|
itemKey.currentContext!.findRenderObject() as RenderBox;
|
||||||
final double realHeight = renderBox.size.height;
|
final double realHeight = renderBox.size.height;
|
||||||
if (realHeight > 0) {
|
if (realHeight > 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -582,14 +715,12 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRecordHeader(TrainRecord record, {bool isMerged = false}) {
|
Widget _buildRecordHeader(TrainRecord record, {bool isMerged = false}) {
|
||||||
final trainType = record.trainType;
|
final trainType = record.trainType;
|
||||||
final trainDisplay =
|
|
||||||
record.fullTrainNumber.isEmpty ? "未知列车" : record.fullTrainNumber;
|
|
||||||
String formattedLocoInfo = "";
|
String formattedLocoInfo = "";
|
||||||
if (record.locoType.isNotEmpty && record.loco.isNotEmpty) {
|
if (record.locoType.isNotEmpty && record.loco.isNotEmpty) {
|
||||||
final shortLoco = record.loco.length > 5
|
final shortLoco = record.loco.length > 5
|
||||||
@@ -601,6 +732,22 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
} else if (record.loco.isNotEmpty) {
|
} else if (record.loco.isNotEmpty) {
|
||||||
formattedLocoInfo = record.loco;
|
formattedLocoInfo = record.loco;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (record.fullTrainNumber.isEmpty && formattedLocoInfo.isEmpty) {
|
||||||
|
return Text(
|
||||||
|
(record.time == "<NUL>" || record.time.isEmpty)
|
||||||
|
? record.receivedTimestamp.toString().split(".")[0]
|
||||||
|
: record.time.split("\n")[0],
|
||||||
|
style: const TextStyle(fontSize: 11, color: Colors.grey),
|
||||||
|
overflow: TextOverflow.ellipsis);
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasTrainNumber = record.fullTrainNumber.isNotEmpty;
|
||||||
|
final hasDirection = record.direction == 1 || record.direction == 3;
|
||||||
|
final hasLocoInfo =
|
||||||
|
formattedLocoInfo.isNotEmpty && formattedLocoInfo != "<NUL>";
|
||||||
|
final shouldShowTrainRow = hasTrainNumber || hasDirection || hasLocoInfo;
|
||||||
|
|
||||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
@@ -616,43 +763,47 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
style: const TextStyle(fontSize: 11, color: Colors.grey),
|
style: const TextStyle(fontSize: 11, color: Colors.grey),
|
||||||
overflow: TextOverflow.ellipsis))
|
overflow: TextOverflow.ellipsis))
|
||||||
]),
|
]),
|
||||||
const SizedBox(height: 2),
|
if (shouldShowTrainRow) ...[
|
||||||
Row(
|
const SizedBox(height: 2),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
Flexible(
|
children: [
|
||||||
child: Row(
|
Flexible(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
Flexible(
|
children: [
|
||||||
child: Text(trainDisplay,
|
if (hasTrainNumber)
|
||||||
style: const TextStyle(
|
Flexible(
|
||||||
fontSize: 20,
|
child: Text(record.fullTrainNumber,
|
||||||
fontWeight: FontWeight.bold,
|
style: const TextStyle(
|
||||||
color: Colors.white),
|
fontSize: 20,
|
||||||
overflow: TextOverflow.ellipsis)),
|
fontWeight: FontWeight.bold,
|
||||||
const SizedBox(width: 6),
|
color: Colors.white),
|
||||||
if (record.direction == 1 || record.direction == 3)
|
overflow: TextOverflow.ellipsis)),
|
||||||
Container(
|
if (hasTrainNumber && hasDirection)
|
||||||
width: 20,
|
const SizedBox(width: 6),
|
||||||
height: 20,
|
if (hasDirection)
|
||||||
decoration: BoxDecoration(
|
Container(
|
||||||
color: Colors.white,
|
width: 20,
|
||||||
borderRadius: BorderRadius.circular(2)),
|
height: 20,
|
||||||
child: Center(
|
decoration: BoxDecoration(
|
||||||
child: Text(record.direction == 1 ? "下" : "上",
|
color: Colors.white,
|
||||||
style: const TextStyle(
|
borderRadius: BorderRadius.circular(2)),
|
||||||
fontSize: 12,
|
child: Center(
|
||||||
fontWeight: FontWeight.bold,
|
child: Text(record.direction == 1 ? "下" : "上",
|
||||||
color: Colors.black))))
|
style: const TextStyle(
|
||||||
])),
|
fontSize: 12,
|
||||||
if (formattedLocoInfo.isNotEmpty && formattedLocoInfo != "<NUL>")
|
fontWeight: FontWeight.bold,
|
||||||
Text(formattedLocoInfo,
|
color: Colors.black))))
|
||||||
style: const TextStyle(fontSize: 14, color: Colors.white70))
|
])),
|
||||||
]),
|
if (hasLocoInfo)
|
||||||
const SizedBox(height: 2)
|
Text(formattedLocoInfo,
|
||||||
|
style: const TextStyle(fontSize: 14, color: Colors.white70))
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 2)
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
|||||||
selectedIndex: _currentIndex,
|
selectedIndex: _currentIndex,
|
||||||
onDestinationSelected: (index) {
|
onDestinationSelected: (index) {
|
||||||
if (_currentIndex == 2 && index == 0) {
|
if (_currentIndex == 2 && index == 0) {
|
||||||
_historyScreenKey.currentState?.loadRecords();
|
_historyScreenKey.currentState?.reloadRecords();
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
if (_isHistoryEditMode) _isHistoryEditMode = false;
|
if (_isHistoryEditMode) _isHistoryEditMode = false;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
bool _notificationsEnabled = true;
|
bool _notificationsEnabled = true;
|
||||||
int _recordCount = 0;
|
int _recordCount = 0;
|
||||||
bool _mergeRecordsEnabled = false;
|
bool _mergeRecordsEnabled = false;
|
||||||
|
bool _hideTimeOnlyRecords = false;
|
||||||
GroupBy _groupBy = GroupBy.trainAndLoco;
|
GroupBy _groupBy = GroupBy.trainAndLoco;
|
||||||
TimeWindow _timeWindow = TimeWindow.unlimited;
|
TimeWindow _timeWindow = TimeWindow.unlimited;
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
(settingsMap['backgroundServiceEnabled'] ?? 0) == 1;
|
(settingsMap['backgroundServiceEnabled'] ?? 0) == 1;
|
||||||
_notificationsEnabled = (settingsMap['notificationEnabled'] ?? 1) == 1;
|
_notificationsEnabled = (settingsMap['notificationEnabled'] ?? 1) == 1;
|
||||||
_mergeRecordsEnabled = settings.enabled;
|
_mergeRecordsEnabled = settings.enabled;
|
||||||
|
_hideTimeOnlyRecords = (settingsMap['hideTimeOnlyRecords'] ?? 0) == 1;
|
||||||
_groupBy = settings.groupBy;
|
_groupBy = settings.groupBy;
|
||||||
_timeWindow = settings.timeWindow;
|
_timeWindow = settings.timeWindow;
|
||||||
});
|
});
|
||||||
@@ -82,6 +84,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
'backgroundServiceEnabled': _backgroundServiceEnabled ? 1 : 0,
|
'backgroundServiceEnabled': _backgroundServiceEnabled ? 1 : 0,
|
||||||
'notificationEnabled': _notificationsEnabled ? 1 : 0,
|
'notificationEnabled': _notificationsEnabled ? 1 : 0,
|
||||||
'mergeRecordsEnabled': _mergeRecordsEnabled ? 1 : 0,
|
'mergeRecordsEnabled': _mergeRecordsEnabled ? 1 : 0,
|
||||||
|
'hideTimeOnlyRecords': _hideTimeOnlyRecords ? 1 : 0,
|
||||||
'groupBy': _groupBy.name,
|
'groupBy': _groupBy.name,
|
||||||
'timeWindow': _timeWindow.name,
|
'timeWindow': _timeWindow.name,
|
||||||
});
|
});
|
||||||
@@ -236,6 +239,29 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('隐藏只有时间有效的记录', style: AppTheme.bodyLarge),
|
||||||
|
Text('不显示只有时间信息的记录', style: AppTheme.caption),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Switch(
|
||||||
|
value: _hideTimeOnlyRecords,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_hideTimeOnlyRecords = value;
|
||||||
|
});
|
||||||
|
_saveSettings();
|
||||||
|
},
|
||||||
|
activeColor: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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 = 1;
|
static const _databaseVersion = 2;
|
||||||
|
|
||||||
static const String trainRecordsTable = 'train_records';
|
static const String trainRecordsTable = 'train_records';
|
||||||
static const String appSettingsTable = 'app_settings';
|
static const String appSettingsTable = 'app_settings';
|
||||||
@@ -34,9 +34,17 @@ class DatabaseService {
|
|||||||
path,
|
path,
|
||||||
version: _databaseVersion,
|
version: _databaseVersion,
|
||||||
onCreate: _onCreate,
|
onCreate: _onCreate,
|
||||||
|
onUpgrade: _onUpgrade,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
||||||
|
if (oldVersion < 2) {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $appSettingsTable ADD COLUMN hideTimeOnlyRecords INTEGER NOT NULL DEFAULT 0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onCreate(Database db, int version) async {
|
Future<void> _onCreate(Database db, int version) async {
|
||||||
await db.execute('''
|
await db.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS $trainRecordsTable (
|
CREATE TABLE IF NOT EXISTS $trainRecordsTable (
|
||||||
@@ -79,6 +87,7 @@ class DatabaseService {
|
|||||||
backgroundServiceEnabled INTEGER NOT NULL DEFAULT 0,
|
backgroundServiceEnabled INTEGER NOT NULL DEFAULT 0,
|
||||||
notificationEnabled INTEGER NOT NULL DEFAULT 0,
|
notificationEnabled INTEGER NOT NULL DEFAULT 0,
|
||||||
mergeRecordsEnabled INTEGER NOT NULL DEFAULT 0,
|
mergeRecordsEnabled 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'
|
||||||
)
|
)
|
||||||
@@ -102,6 +111,7 @@ class DatabaseService {
|
|||||||
'backgroundServiceEnabled': 0,
|
'backgroundServiceEnabled': 0,
|
||||||
'notificationEnabled': 0,
|
'notificationEnabled': 0,
|
||||||
'mergeRecordsEnabled': 0,
|
'mergeRecordsEnabled': 0,
|
||||||
|
'hideTimeOnlyRecords': 0,
|
||||||
'groupBy': 'trainAndLoco',
|
'groupBy': 'trainAndLoco',
|
||||||
'timeWindow': 'unlimited',
|
'timeWindow': 'unlimited',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class MergeService {
|
|||||||
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();
|
||||||
final hasTrain = train.isNotEmpty && train != "<NUL>";
|
final hasTrain = train.isNotEmpty && train != "<NUL>" && !train.contains("-----");
|
||||||
final hasLoco = loco.isNotEmpty && loco != "<NUL>";
|
final hasLoco = loco.isNotEmpty && loco != "<NUL>";
|
||||||
|
|
||||||
switch (groupBy) {
|
switch (groupBy) {
|
||||||
|
|||||||
@@ -920,6 +920,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.28.0"
|
version: "0.28.0"
|
||||||
|
scrollview_observer:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: scrollview_observer
|
||||||
|
sha256: c2f713509f18f88f637b2084b47a90c91fb1ef066d5d82d2cf3194d8509dc6ab
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.26.2"
|
||||||
share_plus:
|
share_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.2.0-flutter+20 # versionName: 0.2.0-flutter, versionCode: 3
|
version: 0.3.0-flutter+30
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
@@ -53,6 +53,7 @@ dependencies:
|
|||||||
package_info_plus: ^8.1.2
|
package_info_plus: ^8.1.2
|
||||||
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
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user