334 lines
10 KiB
Dart
334 lines
10 KiB
Dart
import "package:flutter/material.dart";
|
|
import "package:go_router/go_router.dart";
|
|
import "../models/trip.dart";
|
|
import "../services/brr_export_service.dart";
|
|
|
|
class StationSelectionPage extends StatefulWidget {
|
|
final List<Trip> trips;
|
|
final String fileName;
|
|
final BRROperator operator;
|
|
final String? routeName;
|
|
|
|
const StationSelectionPage({
|
|
super.key,
|
|
required this.trips,
|
|
required this.fileName,
|
|
this.operator = BRROperator.arriva,
|
|
this.routeName,
|
|
});
|
|
|
|
static GoRoute route = GoRoute(
|
|
path: "/station-selection",
|
|
builder: (context, state) {
|
|
final extra = state.extra as Map<String, dynamic>?;
|
|
if (extra == null) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
context.go("/");
|
|
});
|
|
return const SizedBox.shrink();
|
|
}
|
|
return StationSelectionPage(
|
|
trips: (extra["trips"] as List).cast<Trip>(),
|
|
fileName: extra["fileName"] as String,
|
|
operator: extra["operator"] as BRROperator? ?? BRROperator.arriva,
|
|
routeName: extra["routeName"] as String?,
|
|
);
|
|
},
|
|
);
|
|
|
|
@override
|
|
State<StationSelectionPage> createState() => _StationSelectionPageState();
|
|
}
|
|
|
|
class _StationSelectionPageState extends State<StationSelectionPage> {
|
|
String? _selectedStation;
|
|
String? _selectedDirection;
|
|
List<String> _availableStations = [];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_extractStations();
|
|
}
|
|
|
|
void _extractStations() {
|
|
final stationsSet = <String>{};
|
|
for (final trip in widget.trips) {
|
|
stationsSet.addAll(trip.stationOrder);
|
|
}
|
|
_availableStations = stationsSet.toList()..sort();
|
|
}
|
|
|
|
bool _isTerminus(String station) {
|
|
for (final trip in widget.trips) {
|
|
if (trip.stationOrder.isEmpty) continue;
|
|
if (trip.stationOrder.first == station || trip.stationOrder.last == station) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// For a given station, find where buses are headed in each direction
|
|
String? _getTerminus(String station, String direction) {
|
|
for (final trip in widget.trips) {
|
|
if (!trip.stationOrder.contains(station)) continue;
|
|
if (trip.stationOrder.isEmpty) continue;
|
|
|
|
// "departing" = outbound trips, show where they end up
|
|
if (direction == "departing" && trip.direction == "outbound") {
|
|
return trip.stationOrder.last;
|
|
}
|
|
|
|
// "arriving" = inbound trips, show where they end up (opposite terminus)
|
|
if (direction == "arriving" && trip.direction == "inbound") {
|
|
return trip.stationOrder.last;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
String _getDirectionLabel(String direction) {
|
|
if (_selectedStation == null) {
|
|
return direction == "departing" ? "DEPARTING" : "ARRIVING";
|
|
}
|
|
|
|
if (_isTerminus(_selectedStation!)) {
|
|
return direction == "departing" ? "DEPARTURES" : "ARRIVALS";
|
|
}
|
|
|
|
final terminus = _getTerminus(_selectedStation!, direction);
|
|
return "towards\n${terminus ?? "..."}";
|
|
}
|
|
|
|
void _onContinue() {
|
|
if (_selectedStation == null || _selectedDirection == null) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text("Select both a station and direction")),
|
|
);
|
|
return;
|
|
}
|
|
|
|
final filteredTrips = _filterAndUpdateTrips();
|
|
|
|
if (filteredTrips.isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text("No trips found for that selection")),
|
|
);
|
|
return;
|
|
}
|
|
|
|
context.go("/trips", extra: {
|
|
"trips": filteredTrips,
|
|
"fileName": widget.fileName,
|
|
"station": _selectedStation,
|
|
"direction": _selectedDirection,
|
|
"operator": widget.operator,
|
|
if (widget.routeName != null) "routeName": widget.routeName,
|
|
});
|
|
}
|
|
|
|
List<Trip> _filterAndUpdateTrips() {
|
|
final filtered = <Trip>[];
|
|
final isTerminusStation = _isTerminus(_selectedStation!);
|
|
|
|
for (final trip in widget.trips) {
|
|
if (!trip.stationTimes.containsKey(_selectedStation)) continue;
|
|
|
|
if (_selectedDirection != "both") {
|
|
// For terminus stations, the direction logic flips:
|
|
// "departing" from a terminus that's first on inbound = show inbound trips
|
|
// "arriving" at a terminus thats last on outbound = show outbound trips
|
|
final isFirstStation = trip.stationOrder.isNotEmpty && trip.stationOrder.first == _selectedStation;
|
|
final isLastStation = trip.stationOrder.isNotEmpty && trip.stationOrder.last == _selectedStation;
|
|
|
|
if (isTerminusStation) {
|
|
if (_selectedDirection == "departing" && !isFirstStation) continue;
|
|
if (_selectedDirection == "arriving" && !isLastStation) continue;
|
|
} else {
|
|
if (_selectedDirection == "departing" && trip.direction != "outbound") continue;
|
|
if (_selectedDirection == "arriving" && trip.direction != "inbound") continue;
|
|
}
|
|
}
|
|
|
|
final stationTime = trip.stationTimes[_selectedStation];
|
|
if (stationTime != null) {
|
|
filtered.add(trip.copyWith(scheduledTime: stationTime));
|
|
}
|
|
}
|
|
|
|
return filtered;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back, size: 20),
|
|
onPressed: () => context.go("/"),
|
|
),
|
|
title: const Text("SELECT STATION"),
|
|
),
|
|
body: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
|
|
// file info strip
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
color: const Color(0xFF1E1E1E),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.description_outlined, size: 14, color: Color(0xFF777777)),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
widget.fileName,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: Color(0xFF777777),
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
Text(
|
|
"${widget.trips.length} trips",
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: Color(0xFF00A9CE),
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 28),
|
|
|
|
const Text(
|
|
"STATION",
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFF777777),
|
|
letterSpacing: 1.5,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
|
|
DropdownButtonFormField<String>(
|
|
initialValue: _selectedStation,
|
|
dropdownColor: const Color(0xFF252525),
|
|
style: const TextStyle(color: Color(0xFFEEEEEE), fontSize: 15),
|
|
decoration: const InputDecoration(
|
|
hintText: "Choose station...",
|
|
),
|
|
items: _availableStations.map((station) {
|
|
return DropdownMenuItem(
|
|
value: station,
|
|
child: Text(station),
|
|
);
|
|
}).toList(),
|
|
onChanged: (value) => setState(() => _selectedStation = value),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
const Text(
|
|
"DIRECTION",
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFF777777),
|
|
letterSpacing: 1.5,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
|
|
Row(
|
|
children: [
|
|
_DirButton(
|
|
label: _getDirectionLabel("departing"),
|
|
selected: _selectedDirection == "departing",
|
|
onTap: () => setState(() => _selectedDirection = "departing"),
|
|
),
|
|
const SizedBox(width: 10),
|
|
_DirButton(
|
|
label: _getDirectionLabel("arriving"),
|
|
selected: _selectedDirection == "arriving",
|
|
onTap: () => setState(() => _selectedDirection = "arriving"),
|
|
),
|
|
if (widget.operator == BRROperator.stagecoach) ...[
|
|
const SizedBox(width: 10),
|
|
_DirButton(
|
|
label: "BOTH",
|
|
selected: _selectedDirection == "both",
|
|
onTap: () => setState(() => _selectedDirection = "both"),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
|
|
const Spacer(),
|
|
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 54,
|
|
child: ElevatedButton(
|
|
onPressed: _onContinue,
|
|
child: const Text("VIEW TRIPS"),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DirButton extends StatelessWidget {
|
|
final String label;
|
|
final bool selected;
|
|
final VoidCallback onTap;
|
|
|
|
const _DirButton({
|
|
required this.label,
|
|
required this.selected,
|
|
required this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Expanded(
|
|
child: GestureDetector(
|
|
onTap: onTap,
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 150),
|
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
|
|
decoration: BoxDecoration(
|
|
color: selected ? const Color(0xFF002A33) : const Color(0xFF1E1E1E),
|
|
border: Border.all(
|
|
color: selected ? const Color(0xFF00A9CE) : const Color(0xFF2E2E2E),
|
|
width: selected ? 1.5 : 1,
|
|
),
|
|
borderRadius: BorderRadius.circular(3),
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700,
|
|
color: selected ? const Color(0xFF00A9CE) : const Color(0xFF777777),
|
|
letterSpacing: 0.3,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|