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

View File

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

View File

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

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.1.0+1 version: 0.1.1+1
environment: environment:
sdk: ^3.8.1 sdk: ^3.8.1