Roadbound-BRR/lib/pages/station_selection_page.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,
),
),
),
),
);
}
}