add build step for flutter web, add persistent pagination in the traction list
Some checks failed
Release / meta (push) Successful in 14s
Release / web-build (push) Has been cancelled
Release / release-dev (push) Has been cancelled
Release / release-master (push) Has been cancelled
Release / linux-build (push) Has been cancelled
Release / android-build (push) Has been cancelled
Some checks failed
Release / meta (push) Successful in 14s
Release / web-build (push) Has been cancelled
Release / release-dev (push) Has been cancelled
Release / release-master (push) Has been cancelled
Release / linux-build (push) Has been cancelled
Release / android-build (push) Has been cancelled
This commit is contained in:
@@ -111,12 +111,6 @@ class _LegCardState extends State<LegCard> {
|
||||
subtitle: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = constraints.maxWidth > 520;
|
||||
final timeWidget = _timeWithDelay(
|
||||
context,
|
||||
leg.beginTime,
|
||||
leg.beginDelayMinutes,
|
||||
includeDate: widget.showDate,
|
||||
);
|
||||
final tractionWrap = !_expanded && leg.locos.isNotEmpty
|
||||
? Wrap(
|
||||
spacing: 8,
|
||||
|
||||
@@ -128,10 +128,6 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
}
|
||||
}
|
||||
|
||||
double _manualMilesFromInput(DistanceUnitService units) {
|
||||
return units.milesFromInput(_mileageController.text) ?? 0;
|
||||
}
|
||||
|
||||
double _milesFromInputWithUnit(DistanceUnit unit) {
|
||||
return DistanceFormatter(unit)
|
||||
.parseInputMiles(_mileageController.text.trim()) ??
|
||||
@@ -276,6 +272,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null) {
|
||||
final units = _distanceUnits(context);
|
||||
setState(() {
|
||||
@@ -1536,43 +1533,6 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _timeToggleBlock({
|
||||
required String label,
|
||||
required bool value,
|
||||
required ValueChanged<bool?>? onChanged,
|
||||
required String matchLabel,
|
||||
required bool matchValue,
|
||||
required ValueChanged<bool?>? onMatchChanged,
|
||||
required bool showMatch,
|
||||
Widget? picker,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CheckboxListTile(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(label),
|
||||
),
|
||||
if (showMatch)
|
||||
CheckboxListTile(
|
||||
value: matchValue,
|
||||
onChanged: onMatchChanged,
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.only(left: 12),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(matchLabel),
|
||||
),
|
||||
if (picker != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
picker,
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UpperCaseTextFormatter extends TextInputFormatter {
|
||||
|
||||
@@ -52,6 +52,7 @@ extension _NewEntrySubmitLogic on _NewEntryPageState {
|
||||
if (form == null) return;
|
||||
if (!form.validate()) return;
|
||||
if (!await _validateRequiredFields()) return;
|
||||
if (!mounted) return;
|
||||
final routeStations = _routeResult?.calculatedRoute ?? [];
|
||||
final startVal = _useManualMileage
|
||||
? _startController.text.trim()
|
||||
|
||||
@@ -84,6 +84,13 @@ class _NewTractionPageState extends State<NewTractionPage> {
|
||||
'traction_motors': TextEditingController(),
|
||||
'build_date': TextEditingController(),
|
||||
};
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final data = context.read<DataService>();
|
||||
if (data.locoClasses.isEmpty) {
|
||||
data.fetchClassList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -254,6 +261,10 @@ class _NewTractionPageState extends State<NewTractionPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isActive = _statusIsActive;
|
||||
final data = context.watch<DataService>();
|
||||
final classOptions = [...data.locoClasses]..sort(
|
||||
(a, b) => a.toLowerCase().compareTo(b.toLowerCase()),
|
||||
);
|
||||
final size = MediaQuery.of(context).size;
|
||||
final isNarrow = size.width < 720;
|
||||
final fieldWidth = isNarrow ? double.infinity : 340.0;
|
||||
@@ -269,6 +280,89 @@ class _NewTractionPageState extends State<NewTractionPage> {
|
||||
double? widthOverride,
|
||||
String? Function(String?)? validator,
|
||||
}) {
|
||||
// Special autocomplete for class field using existing loco classes.
|
||||
if (key == 'class' && classOptions.isNotEmpty) {
|
||||
return SizedBox(
|
||||
width: widthOverride ?? fieldWidth,
|
||||
child: Autocomplete<String>(
|
||||
optionsBuilder: (TextEditingValue value) {
|
||||
final query = value.text.trim().toLowerCase();
|
||||
if (query.isEmpty) return classOptions;
|
||||
return classOptions.where(
|
||||
(c) => c.toLowerCase().contains(query),
|
||||
);
|
||||
},
|
||||
onSelected: (selection) {
|
||||
_controllers[key]?.text = selection;
|
||||
_formKey.currentState?.validate();
|
||||
},
|
||||
fieldViewBuilder:
|
||||
(context, textEditingController, focusNode, onFieldSubmitted) {
|
||||
if (textEditingController.text != _controllers[key]?.text) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
if (textEditingController.text != _controllers[key]?.text) {
|
||||
textEditingController.value =
|
||||
_controllers[key]?.value ?? textEditingController.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
return TextFormField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: required ? '$label *' : label,
|
||||
helperText: helper,
|
||||
suffixText: suffixText,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
validator: (val) {
|
||||
if (required && (val == null || val.trim().isEmpty)) {
|
||||
return 'Required';
|
||||
}
|
||||
return validator?.call(val);
|
||||
},
|
||||
onChanged: (_) {
|
||||
_controllers[key]?.text = textEditingController.text;
|
||||
_formKey.currentState?.validate();
|
||||
},
|
||||
onFieldSubmitted: (_) => onFieldSubmitted(),
|
||||
);
|
||||
},
|
||||
optionsViewBuilder: (context, onSelected, options) {
|
||||
final opts = options.toList();
|
||||
if (opts.isEmpty) return const SizedBox.shrink();
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 280,
|
||||
maxWidth: widthOverride ?? fieldWidth,
|
||||
),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: opts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final option = opts[index];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: Text(option),
|
||||
onTap: () => onSelected(option),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: widthOverride ?? fieldWidth,
|
||||
child: TextFormField(
|
||||
|
||||
@@ -37,6 +37,9 @@ class _TractionPageState extends State<TractionPage> {
|
||||
final Map<String, TextEditingController> _dynamicControllers = {};
|
||||
final Map<String, String?> _enumSelections = {};
|
||||
bool _restoredFromPrefs = false;
|
||||
static const int _pageSize = 100;
|
||||
int _lastTractionOffset = 0;
|
||||
String? _lastQuerySignature;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -59,6 +62,9 @@ class _TractionPageState extends State<TractionPage> {
|
||||
Future<void> _initialLoad() async {
|
||||
final data = context.read<DataService>();
|
||||
await _restoreSearchState();
|
||||
if (_lastTractionOffset == 0 && data.traction.length > _pageSize) {
|
||||
_lastTractionOffset = data.traction.length - _pageSize;
|
||||
}
|
||||
data.fetchClassList();
|
||||
data.fetchEventFields();
|
||||
await _refreshTraction();
|
||||
@@ -103,7 +109,29 @@ class _TractionPageState extends State<TractionPage> {
|
||||
dynamicFieldsUsed;
|
||||
}
|
||||
|
||||
Future<void> _refreshTraction({bool append = false}) async {
|
||||
String _tractionQuerySignature(
|
||||
Map<String, dynamic> filters,
|
||||
bool hadOnly,
|
||||
) {
|
||||
final sortedKeys = filters.keys.toList()..sort();
|
||||
final filterSignature = sortedKeys
|
||||
.map((key) => '$key=${filters[key]}')
|
||||
.join('|');
|
||||
final classQuery = (_selectedClass ?? _classController.text).trim();
|
||||
return [
|
||||
'class=$classQuery',
|
||||
'number=${_numberController.text.trim()}',
|
||||
'name=${_nameController.text.trim()}',
|
||||
'mileageFirst=$_mileageFirst',
|
||||
'hadOnly=$hadOnly',
|
||||
'filters=$filterSignature',
|
||||
].join(';');
|
||||
}
|
||||
|
||||
Future<void> _refreshTraction({
|
||||
bool append = false,
|
||||
bool preservePosition = true,
|
||||
}) async {
|
||||
final data = context.read<DataService>();
|
||||
final filters = <String, dynamic>{};
|
||||
final name = _nameController.text.trim();
|
||||
@@ -118,15 +146,49 @@ class _TractionPageState extends State<TractionPage> {
|
||||
}
|
||||
});
|
||||
final hadOnly = !_hasFilters;
|
||||
final signature = _tractionQuerySignature(filters, hadOnly);
|
||||
final queryChanged =
|
||||
_lastQuerySignature != null && signature != _lastQuerySignature;
|
||||
_lastQuerySignature = signature;
|
||||
|
||||
if (queryChanged && !append) {
|
||||
_lastTractionOffset = 0;
|
||||
}
|
||||
|
||||
final shouldPreservePosition = preservePosition &&
|
||||
!append &&
|
||||
!queryChanged &&
|
||||
_lastTractionOffset > 0;
|
||||
|
||||
int limit;
|
||||
int offset;
|
||||
if (append) {
|
||||
offset = data.traction.length;
|
||||
limit = _pageSize;
|
||||
_lastTractionOffset = offset;
|
||||
} else if (shouldPreservePosition) {
|
||||
offset = 0;
|
||||
limit = _pageSize + _lastTractionOffset;
|
||||
} else {
|
||||
offset = 0;
|
||||
limit = _pageSize;
|
||||
}
|
||||
|
||||
await data.fetchTraction(
|
||||
hadOnly: hadOnly,
|
||||
locoClass: _selectedClass ?? _classController.text.trim(),
|
||||
locoNumber: _numberController.text.trim(),
|
||||
offset: append ? data.traction.length : 0,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
append: append,
|
||||
filters: filters,
|
||||
mileageFirst: _mileageFirst,
|
||||
);
|
||||
|
||||
if (!append && !shouldPreservePosition) {
|
||||
_lastTractionOffset = 0;
|
||||
}
|
||||
|
||||
await _persistSearchState();
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,16 @@ extension _TractionPersistence on _TractionPageState {
|
||||
enumValues[entry.key.toString()] = entry.value?.toString();
|
||||
}
|
||||
}
|
||||
final lastOffsetRaw = decoded['lastOffset'];
|
||||
if (lastOffsetRaw is int) {
|
||||
_lastTractionOffset = lastOffsetRaw;
|
||||
} else if (lastOffsetRaw is num) {
|
||||
_lastTractionOffset = lastOffsetRaw.toInt();
|
||||
}
|
||||
final lastSig = decoded['querySignature']?.toString();
|
||||
if (lastSig != null && lastSig.isNotEmpty) {
|
||||
_lastQuerySignature = lastSig;
|
||||
}
|
||||
|
||||
for (final entry in dynamicValues.entries) {
|
||||
_dynamicControllers.putIfAbsent(
|
||||
@@ -76,6 +86,8 @@ extension _TractionPersistence on _TractionPageState {
|
||||
'showAdvancedFilters': _showAdvancedFilters,
|
||||
'dynamic': _dynamicControllers.map((k, v) => MapEntry(k, v.text)),
|
||||
'enum': _enumSelections,
|
||||
'lastOffset': _lastTractionOffset,
|
||||
'querySignature': _lastQuerySignature,
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user