import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:mileograph_flutter/objects/objects.dart'; import 'package:mileograph_flutter/services/api_service.dart'; import 'package:mileograph_flutter/services/data_service.dart'; import './route_summary_widget.dart'; class StationAutocomplete extends StatefulWidget { const StationAutocomplete({ super.key, required this.allStations, this.initialValue, required this.onChanged, }); final List allStations; final String? initialValue; final ValueChanged onChanged; @override State createState() => _StationAutocompleteState(); } class _StationAutocompleteState extends State { late final TextEditingController _controller; // Simulated list of over 10,000 stations final List stations = List.generate(10000, (i) => 'Station $i'); @override void initState() { super.initState(); _controller = TextEditingController(text: widget.initialValue ?? ''); _controller.addListener(() { widget.onChanged(_controller.text); }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Autocomplete( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text.isEmpty) { return const Iterable.empty(); } final query = textEditingValue.text.toLowerCase(); final matches = widget.allStations .map((s) => s.name) .where((name) => name.toLowerCase().contains(query)) .toList(); matches.sort((a, b) => a.length.compareTo(b.length)); return matches.take(10); }, onSelected: (String selection) { _controller.text = selection; widget.onChanged(selection); }, fieldViewBuilder: (context, textEditingController, focusNode, onFieldSubmitted) { textEditingController.value = _controller.value; return TextField( controller: textEditingController, focusNode: focusNode, textInputAction: TextInputAction.done, onSubmitted: (_) { final query = textEditingController.text.toLowerCase(); final matches = widget.allStations .map((s) => s.name) .where((name) => name.toLowerCase().contains(query)) .toList(); if (matches.isNotEmpty) { matches.sort((a, b) => a.length.compareTo(b.length)); final firstMatch = matches.first; _controller.text = firstMatch; widget.onChanged(firstMatch); focusNode.unfocus(); // optionally close keyboard } }, decoration: const InputDecoration( labelText: 'Select station', border: OutlineInputBorder(), ), ); }, ); } } class RouteCalculator extends StatefulWidget { const RouteCalculator({ super.key, this.onDistanceComputed, this.onApplyRoute, this.initialStations, }); final ValueChanged? onDistanceComputed; final ValueChanged? onApplyRoute; final List? initialStations; @override State createState() => _RouteCalculatorState(); } class _RouteCalculatorState extends State { List allStations = []; RouteResult? _routeResult; RouteResult? get result => _routeResult; String? _errorMessage; bool _fetched = false; @override void didChangeDependencies() { super.didChangeDependencies(); if (!_fetched) { _fetched = true; if (widget.initialStations != null && widget.initialStations!.isNotEmpty) { context.read().stations = List.from(widget.initialStations!); } WidgetsBinding.instance.addPostFrameCallback((_) async { final data = context.read(); final result = await data.fetchStations(); if (mounted) { setState(() => allStations = result); } }); } } Future _calculateRoute(List stations) async { setState(() { _errorMessage = null; _routeResult = null; }); final api = context.read(); // context is valid here final res = await api.post('/route/distance2', { 'route': stations.where((s) => s.trim().isNotEmpty).toList(), }); if (res['error'] == false) { setState(() { _routeResult = RouteResult.fromJson(res); }); final distance = (_routeResult?.distance ?? 0); widget.onDistanceComputed?.call(distance); } else { setState(() { _errorMessage = RouteError.fromJson(res["error_obj"][0]).msg; }); } } void _addStation() { final data = context.read(); setState(() { data.stations.add(''); }); } void _removeStation(int index) { final data = context.read(); setState(() { data.stations.removeAt(index); }); } void _updateStation(int index, String value) { final data = context.read(); setState(() { data.stations[index] = value; }); } @override Widget build(BuildContext context) { final data = context.watch(); return Column( children: [ Expanded( child: ReorderableListView( buildDefaultDragHandles: false, padding: const EdgeInsets.symmetric(vertical: 8), onReorder: (oldIndex, newIndex) { if (newIndex > oldIndex) newIndex -= 1; setState(() { final moved = data.stations.removeAt(oldIndex); data.stations.insert(newIndex, moved); }); }, children: List.generate(data.stations.length, (index) { return Container( key: ValueKey('$index-${data.stations[index]}'), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ ReorderableDragStartListener( index: index, child: const Padding( padding: EdgeInsets.only(top: 28), child: Icon(Icons.drag_indicator), ), ), const SizedBox(width: 8), Expanded( child: Card( child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ Expanded( child: StationAutocomplete( allStations: allStations, initialValue: data.stations[index], onChanged: (val) => _updateStation(index, val), ), ), IconButton( icon: const Icon(Icons.close), onPressed: () => _removeStation(index), ), ], ), ), ), ), ], ), ); }), ), ), if (_errorMessage != null) Padding( padding: const EdgeInsets.all(8.0), child: Text( _errorMessage!, style: TextStyle(color: Theme.of(context).colorScheme.error), ), ) else if (_routeResult != null) ...[ RouteSummaryWidget( distance: _routeResult!.distance, onDetailsPressed: () { final result = _routeResult; if (result == null) return; context.push('/calculator/details', extra: result); }, ), if (widget.onApplyRoute != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: ElevatedButton.icon( onPressed: () => widget.onApplyRoute!(_routeResult!), icon: const Icon(Icons.check), label: const Text('Apply to entry'), ), ), ] else SizedBox.shrink(), const SizedBox(height: 10), LayoutBuilder( builder: (context, constraints) { double screenWidth = constraints.maxWidth; return Padding( padding: EdgeInsets.only(right: screenWidth < 450 ? 70 : 0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton.icon( icon: const Icon(Icons.add), label: const Text('Add Station'), onPressed: _addStation, ), const SizedBox(width: 16), ElevatedButton.icon( icon: const Icon(Icons.route), label: const Text('Calculate Route'), onPressed: () async { await _calculateRoute(data.stations); }, ), ], ), ); }, ), const SizedBox(height: 16), ], ); } } Widget debugPanel(List stations) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 8, children: [ const Text( 'Current Order:', style: TextStyle(fontWeight: FontWeight.bold), ), ...stations.map((s) => Chip(label: Text(s.isEmpty ? '' : s))), ], ), ); }