Improve entries page and latest changes panel, units on events and timeline
All checks were successful
Release / meta (push) Successful in 9s
Release / linux-build (push) Successful in 8m3s
Release / android-build (push) Successful in 19m21s
Release / release-master (push) Successful in 40s
Release / release-dev (push) Successful in 42s

This commit is contained in:
2025-12-23 17:41:21 +00:00
parent 29959f7580
commit 44d79e7c28
16 changed files with 1764 additions and 498 deletions

View File

@@ -184,7 +184,9 @@ class _FieldList extends StatelessWidget {
value: null,
onChanged: (field) {
if (field == null) return;
draft.fields.add(_FieldEntry(field: field));
draft.fields.add(
_FieldEntry(field: field)..unit = _defaultUnitForField(field),
);
onChange();
},
items: availableFields
@@ -224,10 +226,10 @@ class _FieldList extends StatelessWidget {
),
const SizedBox(height: 4),
_FieldInput(
field: field.field,
value: field.value,
onChanged: (val) {
entry: field,
onChanged: (val, {String? unit}) {
field.value = val;
if (unit != null) field.unit = unit;
onChange();
},
),
@@ -253,17 +255,18 @@ class _FieldList extends StatelessWidget {
class _FieldInput extends StatelessWidget {
const _FieldInput({
required this.field,
required this.value,
required this.entry,
required this.onChanged,
});
final EventField field;
final dynamic value;
final ValueChanged<dynamic> onChanged;
final _FieldEntry entry;
final void Function(dynamic value, {String? unit}) onChanged;
@override
Widget build(BuildContext context) {
final field = entry.field;
final value = entry.value;
if (field.enumValues != null && field.enumValues!.isNotEmpty) {
final options = field.enumValues!;
return DropdownButtonFormField<String>(
@@ -293,6 +296,119 @@ class _FieldInput extends StatelessWidget {
);
}
final name = field.name.toLowerCase();
if (name == 'max_speed') {
final unit = entry.unit ?? 'kph';
final isNumber = true;
return Row(
children: [
Expanded(
child: TextFormField(
initialValue: value?.toString(),
onChanged: (val) {
final parsed = double.tryParse(val);
onChanged(isNumber ? parsed : val, unit: unit);
},
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter value',
suffixText: 'kph/mph',
),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 8),
SizedBox(
width: 88,
child: DropdownButtonFormField<String>(
value: unit,
items: const [
DropdownMenuItem(value: 'kph', child: Text('kph')),
DropdownMenuItem(value: 'mph', child: Text('mph')),
],
onChanged: (val) {
if (val == null) return;
onChanged(value, unit: val);
},
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Unit',
),
),
),
],
);
}
if ({
'height',
'length',
'width',
'track_gauge',
}.contains(name)) {
return TextFormField(
initialValue: value?.toString(),
onChanged: (val) {
final parsed = double.tryParse(val);
onChanged(parsed ?? val);
},
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter value',
suffixText: 'mm',
),
keyboardType: TextInputType.number,
);
}
if (name == 'weight') {
return TextFormField(
initialValue: value?.toString(),
onChanged: (val) {
final parsed = double.tryParse(val);
onChanged(parsed ?? val);
},
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter value',
suffixText: 'tonnes',
),
keyboardType: TextInputType.number,
);
}
if (name == 'power') {
return TextFormField(
initialValue: value?.toString(),
onChanged: (val) {
final parsed = double.tryParse(val);
onChanged(parsed ?? val);
},
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter value',
suffixText: 'kW',
),
keyboardType: TextInputType.number,
);
}
if (name == 'tractive_effort') {
return TextFormField(
initialValue: value?.toString(),
onChanged: (val) {
final parsed = double.tryParse(val);
onChanged(parsed ?? val);
},
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter value',
suffixText: 'kN',
),
keyboardType: TextInputType.number,
);
}
final isNumber = type == 'int' || type == 'integer';
return TextFormField(
initialValue: value?.toString(),
@@ -326,6 +442,13 @@ class _EventDraft {
class _FieldEntry {
final EventField field;
dynamic value;
String? unit;
_FieldEntry({required this.field});
}
String? _defaultUnitForField(EventField field) {
final name = field.name.toLowerCase();
if (name == 'max_speed') return 'kph';
return null;
}

View File

@@ -577,7 +577,7 @@ class _TimelineModel {
_ValueSegment(
start: start,
end: end,
value: entry.valueLabel,
value: _formatValueWithUnits(entry),
entry: entry,
),
);
@@ -680,6 +680,53 @@ class _TimelineModel {
}
}
String _formatValueWithUnits(LocoAttrVersion entry) {
final raw = entry.valueLabel;
final code = entry.attrCode.toLowerCase();
final lowerRaw = raw.toLowerCase();
// Avoid double-appending if units already present.
final hasUnits = lowerRaw.contains('mm') ||
lowerRaw.contains('tonne') ||
lowerRaw.contains('kph') ||
lowerRaw.contains('mph');
double? asNumber = double.tryParse(raw);
String formatNumber(double value) {
if (value % 1 == 0) return value.toStringAsFixed(0);
return value.toStringAsFixed(2);
}
switch (code) {
case 'height':
case 'length':
case 'width':
case 'track_gauge':
if (hasUnits) return raw;
return asNumber != null ? '${formatNumber(asNumber)} mm' : '$raw mm';
case 'weight':
if (hasUnits) return raw;
return asNumber != null ? '${formatNumber(asNumber)} tonnes' : '$raw tonnes';
case 'power':
if (hasUnits) return raw;
return asNumber != null ? '${formatNumber(asNumber)} kW' : '$raw kW';
case 'tractive_effort':
if (hasUnits) return raw;
return asNumber != null ? '${formatNumber(asNumber)} kN' : '$raw kN';
case 'max_speed':
if (hasUnits) return raw;
if (asNumber != null) {
// Stored as kph.
final formatted = asNumber % 1 == 0
? asNumber.toStringAsFixed(0)
: asNumber.toStringAsFixed(1);
return '$formatted kph';
}
return '$raw kph';
default:
return raw;
}
}
class _AxisSegment {
final DateTime start;
final DateTime end;
@@ -742,7 +789,15 @@ class _RowCell {
color: Colors.transparent,
);
}
final displayStart = _formatDate(seg.start) ?? '';
final entry = seg.entry;
String displayStart = '';
if (entry != null) {
if ((entry.maskedValidFrom ?? '').trim().isNotEmpty) {
displayStart = entry.maskedValidFrom!.trim();
} else if (entry.validFrom != null) {
displayStart = _formatDate(entry.validFrom) ?? '';
}
}
return _RowCell(
value: seg.value,
rangeLabel: displayStart,