set trip to null, not 0
Some checks failed
Release / meta (push) Successful in 8s
Release / linux-build (push) Successful in 1m13s
Release / android-build (push) Successful in 11m2s
Release / release-master (push) Successful in 3s
Release / release-dev (push) Successful in 14s
Release / windows-build (push) Has been cancelled

This commit is contained in:
2025-12-11 20:15:06 +00:00
parent 26b1a4878f
commit 84d50d5a90
4 changed files with 215 additions and 222 deletions

View File

@@ -67,15 +67,10 @@ class _NewEntryPageState extends State<NewEntryPage> {
border: OutlineInputBorder(),
),
items: [
const DropdownMenuItem(
value: null,
child: Text('No trip'),
),
const DropdownMenuItem(value: null, child: Text('No trip')),
...sorted.map(
(t) => DropdownMenuItem(
value: t.tripId,
child: Text(t.tripName),
),
(t) =>
DropdownMenuItem(value: t.tripId, child: Text(t.tripName)),
),
],
onChanged: (val) => setState(() => _selectedTripId = val),
@@ -99,9 +94,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
title: const Text('New Trip'),
content: TextField(
controller: controller,
decoration: const InputDecoration(
labelText: 'Trip name',
),
decoration: const InputDecoration(labelText: 'Trip name'),
autofocus: true,
),
actions: [
@@ -128,7 +121,9 @@ class _NewEntryPageState extends State<NewEntryPage> {
final trips = data.tripList;
final match = trips.firstWhere(
(t) => t.tripName == result,
orElse: () => trips.isNotEmpty ? trips.first : TripSummary(tripId: 0, tripName: result, tripMileage: 0),
orElse: () => trips.isNotEmpty
? trips.first
: TripSummary(tripId: 0, tripName: result, tripMileage: 0),
);
setState(() => _selectedTripId = match.tripId);
} catch (e) {
@@ -168,12 +163,17 @@ class _NewEntryPageState extends State<NewEntryPage> {
selectionMode: true,
selectedKeys: selectedKeys,
onSelect: (loco) {
final markerIndex =
_tractionItems.indexWhere((element) => element.isMarker);
final markerIndex = _tractionItems.indexWhere(
(element) => element.isMarker,
);
final key = '${loco.locoClass}-${loco.number}';
setState(() {
final existingIndex = _tractionItems.indexWhere(
(e) => !e.isMarker && e.loco != null && '${e.loco!.locoClass}-${e.loco!.number}' == key);
(e) =>
!e.isMarker &&
e.loco != null &&
'${e.loco!.locoClass}-${e.loco!.number}' == key,
);
if (existingIndex != -1) {
_tractionItems.removeAt(existingIndex);
} else {
@@ -208,16 +208,17 @@ class _NewEntryPageState extends State<NewEntryPage> {
}
DateTime get _legDateTime => DateTime(
_selectedDate.year,
_selectedDate.month,
_selectedDate.day,
_selectedTime.hour,
_selectedTime.minute,
);
_selectedDate.year,
_selectedDate.month,
_selectedDate.day,
_selectedTime.hour,
_selectedTime.minute,
);
List<Map<String, dynamic>> _buildTractionPayload() {
final markerIndex =
_tractionItems.indexWhere((element) => element.isMarker);
final markerIndex = _tractionItems.indexWhere(
(element) => element.isMarker,
);
final payload = <Map<String, dynamic>>[];
for (var i = 0; i < _tractionItems.length; i++) {
final item = _tractionItems[i];
@@ -262,7 +263,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
if (_useManualMileage) {
final body = {
"leg_trip": _selectedTripId ?? 0,
"leg_trip": _selectedTripId ?? null,
"leg_start": startVal,
"leg_end": endVal,
"leg_begin_time": _legDateTime.toIso8601String(),
@@ -276,12 +277,9 @@ class _NewEntryPageState extends State<NewEntryPage> {
await api.post('/add/manual', body);
} else {
final body = {
"leg_trip": _selectedTripId ?? 0,
"leg_start": startVal,
"leg_end": endVal,
"leg_trip": _selectedTripId ?? null,
"leg_begin_time": _legDateTime.toIso8601String(),
"leg_route": routeStations,
"leg_mileage": mileageVal,
"leg_notes": _notesController.text.trim(),
"leg_headcode": _headcodeController.text.trim(),
"leg_network": _networkController.text.trim(),
@@ -294,15 +292,15 @@ class _NewEntryPageState extends State<NewEntryPage> {
}
try {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Entry submitted')),
);
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Entry submitted')));
_formKey.currentState!.reset();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to submit: $e')),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Failed to submit: $e')));
} finally {
if (mounted) setState(() => _submitting = false);
}
@@ -319,141 +317,133 @@ class _NewEntryPageState extends State<NewEntryPage> {
builder: (context, constraints) {
final twoCol = !isMobile && constraints.maxWidth > 1000;
final detailPanel = _section(
'Details',
[
_buildTripSelector(context),
final detailPanel = _section('Details', [
_buildTripSelector(context),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _pickDate,
icon: const Icon(Icons.calendar_today),
label: Text(DateFormat.yMMMd().format(_selectedDate)),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: _pickTime,
icon: const Icon(Icons.schedule),
label: Text(_selectedTime.format(context)),
),
),
],
),
if (_useManualMileage)
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _pickDate,
icon: const Icon(Icons.calendar_today),
label: Text(DateFormat.yMMMd().format(_selectedDate)),
child: TextFormField(
controller: _startController,
decoration: const InputDecoration(
labelText: 'From',
border: OutlineInputBorder(),
),
validator: (v) => !_useManualMileage
? null
: (v == null || v.isEmpty ? 'Required' : null),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: _pickTime,
icon: const Icon(Icons.schedule),
label: Text(_selectedTime.format(context)),
child: TextFormField(
controller: _endController,
decoration: const InputDecoration(
labelText: 'To',
border: OutlineInputBorder(),
),
validator: (v) => !_useManualMileage
? null
: (v == null || v.isEmpty ? 'Required' : null),
),
),
],
),
if (_useManualMileage)
Row(
children: [
Expanded(
child: TextFormField(
controller: _startController,
decoration: const InputDecoration(
labelText: 'From',
border: OutlineInputBorder(),
),
validator: (v) => !_useManualMileage
? null
: (v == null || v.isEmpty ? 'Required' : null),
),
),
const SizedBox(width: 12),
Expanded(
child: TextFormField(
controller: _endController,
decoration: const InputDecoration(
labelText: 'To',
border: OutlineInputBorder(),
),
validator: (v) => !_useManualMileage
? null
: (v == null || v.isEmpty ? 'Required' : null),
),
),
],
),
TextFormField(
controller: _headcodeController,
decoration: const InputDecoration(
labelText: 'Headcode',
border: OutlineInputBorder(),
),
TextFormField(
controller: _headcodeController,
decoration: const InputDecoration(
labelText: 'Headcode',
border: OutlineInputBorder(),
),
TextFormField(
controller: _networkController,
decoration: const InputDecoration(
labelText: 'Network',
border: OutlineInputBorder(),
),
),
TextFormField(
controller: _networkController,
decoration: const InputDecoration(
labelText: 'Network',
border: OutlineInputBorder(),
),
TextFormField(
controller: _notesController,
maxLines: 3,
decoration: const InputDecoration(
labelText: 'Notes',
border: OutlineInputBorder(),
),
),
TextFormField(
controller: _notesController,
maxLines: 3,
decoration: const InputDecoration(
labelText: 'Notes',
border: OutlineInputBorder(),
),
],
);
),
]);
final tractionPanel = _section(
'Traction',
[
final tractionPanel = _section('Traction', [
Align(
alignment: Alignment.centerLeft,
child: ElevatedButton.icon(
onPressed: _openTractionPicker,
icon: const Icon(Icons.search),
label: const Text('Search traction'),
),
),
_buildTractionList(),
]);
final mileagePanel = _section('Mileage', [
SwitchListTile(
title: const Text('Use manual mileage'),
subtitle: const Text('Turn on to enter mileage manually'),
value: _useManualMileage,
onChanged: (val) {
setState(() {
_useManualMileage = val;
});
},
),
if (_useManualMileage)
TextFormField(
controller: _mileageController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
decoration: const InputDecoration(
labelText: 'Mileage (mi)',
border: OutlineInputBorder(),
),
)
else if (_routeResult != null)
ListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Calculated mileage'),
subtitle: Text(
'${_routeResult!.distance.toStringAsFixed(2)} mi',
),
),
if (!_useManualMileage)
Align(
alignment: Alignment.centerLeft,
child: ElevatedButton.icon(
onPressed: _openTractionPicker,
icon: const Icon(Icons.search),
label: const Text('Search traction'),
onPressed: _openCalculator,
icon: const Icon(Icons.calculate),
label: const Text('Open mileage calculator'),
),
),
_buildTractionList(),
],
);
final mileagePanel = _section(
'Mileage',
[
SwitchListTile(
title: const Text('Use manual mileage'),
subtitle: const Text(
'Turn off to calculate mileage automatically'),
value: _useManualMileage,
onChanged: (val) {
setState(() {
_useManualMileage = val;
});
},
),
if (_useManualMileage)
TextFormField(
controller: _mileageController,
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(
labelText: 'Mileage (mi)',
border: OutlineInputBorder(),
),
)
else if (_routeResult != null)
ListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Calculated mileage'),
subtitle:
Text('${_routeResult!.distance.toStringAsFixed(2)} mi'),
),
if (!_useManualMileage)
Align(
alignment: Alignment.centerLeft,
child: ElevatedButton.icon(
onPressed: _openCalculator,
icon: const Icon(Icons.calculate),
label: const Text('Open mileage calculator'),
),
),
],
);
]);
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
@@ -528,14 +518,18 @@ class _NewEntryPageState extends State<NewEntryPage> {
leading: Icon(Icons.train),
title: Text('Rolling stock marker'),
subtitle: Text(
'Place locomotives above/below. Positions set relative to this.'),
'Place locomotives above/below. Positions set relative to this.',
),
),
);
}
final loco = item.loco!;
final markerIndex =
_tractionItems.indexWhere((element) => element.isMarker);
final pos = index > markerIndex ? -(index - markerIndex) : (markerIndex - 1) - index;
final markerIndex = _tractionItems.indexWhere(
(element) => element.isMarker,
);
final pos = index > markerIndex
? -(index - markerIndex)
: (markerIndex - 1) - index;
return Card(
key: ValueKey('${loco.locoClass}-${loco.number}-$index'),
child: ListTile(
@@ -553,8 +547,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
value: item.powering,
onChanged: (v) {
setState(() {
_tractionItems[index] =
item.copyWith(powering: v);
_tractionItems[index] = item.copyWith(powering: v);
});
},
),
@@ -583,10 +576,9 @@ class _NewEntryPageState extends State<NewEntryPage> {
children: [
Text(
title,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
...children.map(
@@ -616,9 +608,7 @@ class _CalculatorPickerPage extends StatelessWidget {
),
title: const Text('Mileage calculator'),
),
body: RouteCalculator(
onApplyRoute: onResult,
),
body: RouteCalculator(onApplyRoute: onResult),
);
}
}
@@ -628,9 +618,14 @@ class _TractionItem {
final bool powering;
final bool isMarker;
_TractionItem({required this.loco, this.powering = true, this.isMarker = false});
_TractionItem({
required this.loco,
this.powering = true,
this.isMarker = false,
});
factory _TractionItem.marker() => _TractionItem(loco: null, powering: false, isMarker: true);
factory _TractionItem.marker() =>
_TractionItem(loco: null, powering: false, isMarker: true);
_TractionItem copyWith({LocoSummary? loco, bool? powering, bool? isMarker}) {
return _TractionItem(

View File

@@ -171,8 +171,9 @@ class _MyHomePageState extends State<MyHomePage> {
}
void _onItemTapped(int index, int currentIndex) {
if (index < 0 || index >= contentPages.length || index == currentIndex)
if (index < 0 || index >= contentPages.length || index == currentIndex) {
return;
}
context.push(contentPages[index]);
_getIndexFromLocation(contentPages[index]);
}

View File

@@ -140,7 +140,7 @@ class LocoSummary extends Loco {
required String locoType,
required String locoNumber,
required String locoName,
required String locoClass,
required super.locoClass,
required String locoOperator,
String? locoNotes,
String? locoEvn,
@@ -153,43 +153,41 @@ class LocoSummary extends Loco {
this.livery,
this.location,
Map<String, dynamic>? extra,
}) : extra = extra ?? const {},
super(
}) : extra = extra ?? const {},
super(
id: locoId,
type: locoType,
number: locoNumber,
name: locoName,
locoClass: locoClass,
operator: locoOperator,
notes: locoNotes,
evn: locoEvn,
);
factory LocoSummary.fromJson(Map<String, dynamic> json) => LocoSummary(
locoId: json['loco_id'] ?? json['id'] ?? 0,
locoType: json['type'] ?? json['loco_type'] ?? '',
locoNumber: json['number'] ?? json['loco_number'] ?? '',
locoName: json['name'] ?? json['loco_name'] ?? "",
locoClass: json['class'] ?? json['loco_class'] ?? '',
locoOperator: json['operator'] ?? json['loco_operator'] ?? '',
locoNotes: json['notes'],
locoEvn: json['evn'] ?? json['loco_evn'],
mileage: ((json['loco_mileage'] ?? json['mileage']) as num?)
?.toDouble() ??
0,
journeys: (json['loco_journeys'] ?? json['journeys'] ?? 0) is num
? (json['loco_journeys'] ?? json['journeys'] ?? 0).toInt()
: 0,
trips: (json['loco_trips'] ?? json['trips']) is num
? (json['loco_trips'] ?? json['trips']).toInt()
: null,
status: json['status'] ?? json['loco_status'],
domain: json['domain'],
owner: json['owner'] ?? json['loco_owner'],
livery: json['livery'],
location: json['location'],
extra: Map<String, dynamic>.from(json),
);
locoId: json['loco_id'] ?? json['id'] ?? 0,
locoType: json['type'] ?? json['loco_type'] ?? '',
locoNumber: json['number'] ?? json['loco_number'] ?? '',
locoName: json['name'] ?? json['loco_name'] ?? "",
locoClass: json['class'] ?? json['loco_class'] ?? '',
locoOperator: json['operator'] ?? json['loco_operator'] ?? '',
locoNotes: json['notes'],
locoEvn: json['evn'] ?? json['loco_evn'],
mileage:
((json['loco_mileage'] ?? json['mileage']) as num?)?.toDouble() ?? 0,
journeys: (json['loco_journeys'] ?? json['journeys'] ?? 0) is num
? (json['loco_journeys'] ?? json['journeys'] ?? 0).toInt()
: 0,
trips: (json['loco_trips'] ?? json['trips']) is num
? (json['loco_trips'] ?? json['trips']).toInt()
: null,
status: json['status'] ?? json['loco_status'],
domain: json['domain'],
owner: json['owner'] ?? json['loco_owner'],
livery: json['livery'],
location: json['location'],
extra: Map<String, dynamic>.from(json),
);
}
class LeaderboardEntry {
@@ -353,24 +351,23 @@ class TripLeg {
});
factory TripLeg.fromJson(Map<String, dynamic> json) => TripLeg(
id: json['leg_id'],
start: json['leg_start'] ?? '',
end: json['leg_end'] ?? '',
beginTime:
json['leg_begin_time'] != null && json['leg_begin_time'] is String
? DateTime.tryParse(json['leg_begin_time'])
: (json['leg_begin_time'] is DateTime
? json['leg_begin_time']
: null),
network: json['leg_network'],
route: json['leg_route'],
mileage: (json['leg_mileage'] as num?)?.toDouble(),
notes: json['leg_notes'],
locos: (json['locos'] as List?)
?.map((e) => Loco.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
id: json['leg_id'],
start: json['leg_start'] ?? '',
end: json['leg_end'] ?? '',
beginTime:
json['leg_begin_time'] != null && json['leg_begin_time'] is String
? DateTime.tryParse(json['leg_begin_time'])
: (json['leg_begin_time'] is DateTime ? json['leg_begin_time'] : null),
network: json['leg_network'],
route: json['leg_route'],
mileage: (json['leg_mileage'] as num?)?.toDouble(),
notes: json['leg_notes'],
locos:
(json['locos'] as List?)
?.map((e) => Loco.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
}
class TripDetail {
@@ -389,14 +386,14 @@ class TripDetail {
});
factory TripDetail.fromJson(Map<String, dynamic> json) => TripDetail(
id: json['trip_id'] ?? 0,
name: json['trip_name'] ?? '',
mileage: (json['trip_mileage'] as num?)?.toDouble() ?? 0,
legCount: json['leg_count'] ??
((json['trip_legs'] as List?)?.length ?? 0),
legs: (json['trip_legs'] as List?)
?.map((e) => TripLeg.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
id: json['trip_id'] ?? 0,
name: json['trip_name'] ?? '',
mileage: (json['trip_mileage'] as num?)?.toDouble() ?? 0,
legCount: json['leg_count'] ?? ((json['trip_legs'] as List?)?.length ?? 0),
legs:
(json['trip_legs'] as List?)
?.map((e) => TripLeg.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
}