import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; //for testing on desktop import 'dart:async'; import 'dart:io'; import 'api_handler.dart'; import 'transport_helper.dart'; import 'utilities.dart'; void main() async { await dotenv.load(); const String dbName = "oeffimasterTransportDb"; if (Platform.isLinux) { databaseFactory = databaseFactoryFfi; // for debugging and desktop test } DbHelper.initDb(dbName); runApp(const MyApp()); } final _defaultLightScheme = ColorScheme.fromSeed(seedColor: Colors.yellow); final _defaultDarkScheme = ColorScheme.fromSeed( seedColor: Colors.yellow, brightness: Brightness.dark, ); class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return DynamicColorBuilder( builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { return MaterialApp( theme: ThemeData( useMaterial3: true, colorScheme: lightDynamic ?? _defaultLightScheme, ), darkTheme: ThemeData( useMaterial3: true, colorScheme: darkDynamic ?? _defaultDarkScheme, ), themeMode: ThemeMode.system, home: const JourneyOverviewPage(), title: 'Oeffimaster', ); }, ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { int _counter = 0; //String API_KEY = ApiHandler.accessID; String API_KEY = "no telly"; //Future> ?id; List stationList = []; String id = ''; // TODO :find a better way to show the list void showResult(Future> futureId) async { stationList = await futureId; id = stationList[0].id; setState(() {}); } @override Widget build(BuildContext context) { // This method is rerun every time setState is called, for instance as done // by the _incrementCounter method above. // // The Flutter framework has been optimized to make rerunning build methods // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( //backgroundColor: Theme.of(context).colorScheme.inversePrimary, //backgroundColor: Colors.amber, // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: .center, children: [ // const Text('You have pushed the button this many times:'), // Text( // '$_counter', // style: Theme.of(context).textTheme.headlineMedium, // ), // Text('API key from ENV is $API_KEY'), // MaterialButton( // onPressed: () { // showResult(ApiHandler.findStations('S Biesdorf')); // }, // child: Text('Search for \'S Biesdorf\''), // ), // Text(id), ElevatedButton( onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (context) => ChooseStationPage()), ); }, child: const Text('Start a route'), ), ], ), ), floatingActionButton: FloatingActionButton( backgroundColor: Theme.of(context).colorScheme.primaryContainer, foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (context) => ChooseStationPage()), ); }, tooltip: 'Start a route', child: const Icon(Icons.add), ), ); } } class ChooseStationPage extends StatefulWidget { const ChooseStationPage({super.key}); @override State createState() => _ChooseStationPageState(); } class _ChooseStationPageState extends State { Station? _selectedStation; List stationList = []; Timer? _debounce; void findNearbyStations() async { stationList = await ApiHandler.nearbyStations( // TODO: replace with location "52.55", "13.55", maxStations: 20, ); } void findStationsByName( String name, { int? resultnum, }) async { stationList = await ApiHandler.findStations(name); } void showResult(Future> futureId) async { stationList = await futureId; setState(() {}); } // Source - https://stackoverflow.com/a/52930197 // Posted by Jannie Theunissen, modified by community. See post 'Timeline' for change history // Retrieved 2026-07-30, License - CC BY-SA 4.0 void _onSearchChanged(String? query) { if (_debounce?.isActive ?? false) _debounce?.cancel(); _debounce = Timer(const Duration(milliseconds: 400), () { _search(query); }); } void _search(String? query) { if (query?.isEmpty ?? true) return; // do something with query setState(() { findStationsByName(query!); }); } @override void dispose() { _debounce?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Start a Journey')), body: Center( child: Column( children: [ // ElevatedButton( // onPressed: () { // showResult(ApiHandler.findStations('S Biesdorf')); // }, // child: Text('Search for \'S Biesdorf\''), // ), SearchAnchor( builder: (BuildContext context, SearchController controller) { return SearchBar( controller: controller, hintText: 'Search your start Station', padding: const WidgetStatePropertyAll( EdgeInsets.symmetric(horizontal: 16.0), ), // onTap: () { // controller.openView(); // }, onChanged: _onSearchChanged, onSubmitted: _search, leading: const Icon(Icons.search), ); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return List.generate(5, (int index) { final String item = 'item $index'; return ListTile( title: Text(item), onTap: () { setState(() { controller.closeView(item); }); }, ); }); }, ), Expanded( child: SizedBox( height: 200, child: StationList( stationList: stationList, onTapped: _handleStationTapped, ), ), ), ], ), ), ); } void _handleStationTapped(Station station) { // setState(() { // _selectedStation = station; // }); Navigator.push( context, MaterialPageRoute( builder: (context) => ChooseDeparturePage(startStation: station), ), ); } } class StationList extends StatelessWidget { final List stationList; final ValueChanged onTapped; StationList({ super.key, required this.stationList, required this.onTapped, }); @override Widget build(BuildContext context) { return ListView( children: [ for (Station station in stationList) ListTile( title: Text(station.name), onTap: () => onTapped(station), ), ], ); } } class ChooseDeparturePage extends StatefulWidget { final Station startStation; final int? journeyId; final Departure? arrival; const ChooseDeparturePage({ super.key, this.journeyId, this.arrival, required this.startStation, }); @override State createState() => _ChooseDeparturePageState(); } class _ChooseDeparturePageState extends State { List departureList = []; Journey? singleJourney; void loadDepartures(Station startStation) async { departureList = await ApiHandler.departures( startStation.id, datetime: widget.arrival?.arrivalTime ?? DateTime.now(), ); setState(() {}); } void loadJourney(int journeyId) async { singleJourney = await Journey.dbGet(journeyId); Navigator.push( context, MaterialPageRoute( builder: (context) => JourneyDetailPage( singleJourney: singleJourney as Journey, ), ), ); } @override initState() { super.initState(); loadDepartures(widget.startStation); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( 'Departures for ${widget.startStation.name}', ), ), body: Center( child: Column( children: [ Builder( builder: (context) { if (widget.arrival != null) { return ListTile( title: Text( 'arrive at ${widget.arrival?.arrivalTime}', ), ); } else { return ListTile( title: Text(''), ); } }, ), Expanded( child: SizedBox( height: 400, child: DepartureList( departureList: departureList, onTapped: _handleDepartureTapped, ), ), ), // ElevatedButton( // onPressed: () { // if (widget.journeyId != null) { // loadJourney(widget.journeyId as int); // } // }, // child: const Text('Finish Route'), // ), ], ), ), floatingActionButton: FloatingActionButton( backgroundColor: Theme.of(context).colorScheme.primaryContainer, foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, onPressed: () { if (widget.journeyId != null) { loadJourney(widget.journeyId as int); } }, tooltip: 'Finish route', child: const Icon(Icons.check), ), ); } void _handleDepartureTapped(Departure departure) { // setState(() { // _selectedDeparture = departure; // }); Navigator.push( context, MaterialPageRoute( builder: (context) => ChooseArrivalPage( departure: departure, journeyId: widget.journeyId, ), ), ); } } class DepartureList extends StatelessWidget { final List departureList; final ValueChanged onTapped; const DepartureList({ super.key, required this.departureList, required this.onTapped, }); @override Widget build(BuildContext context) { return ListView( children: [ for (Departure departure in departureList) ListTile( title: Text('${departure.vehicleName}: ${departure.direction}'), subtitle: Text(departure.departureTime.toString()), onTap: () => onTapped(departure), ), ], ); } } class ChooseArrivalPage extends StatefulWidget { final Departure departure; final int? journeyId; const ChooseArrivalPage({ super.key, this.journeyId, required this.departure, }); @override State createState() => _ChooseArrivalPageState(); } class _ChooseArrivalPageState extends State { List arrivalList = []; Departure? _selectedDeparture; void loadArrivals(String ref) async { arrivalList = await ApiHandler.tripDetail( ref, startStationId: widget.departure.stationId, ); setState(() {}); } @override initState() { super.initState(); loadArrivals(widget.departure.ref); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( 'Arrivals for ${widget.departure.vehicleName}', ), ), body: Center( child: Column( children: [ Expanded( child: SizedBox( height: 400, child: ArrivalList( departure: widget.departure, arrivalList: arrivalList, onTapped: _handleArrivalTapped, ), ), ), ], ), ), ); } void _handleArrivalTapped(Departure departure, Departure arrival) async { // setState(() { // _selectedDeparture = departure; // }); departure.dbInsert; arrival.dbInsert; Segment newSegment = await Segment.fromDepartures(departure, arrival); Station startStation = await Station.dbGet(arrival.stationId); int journeyId; if (widget.journeyId == null) { Journey journey = await Journey.fromSegment(newSegment); journeyId = journey.id as int; } else { Segment segment = newSegment; Journey.dbAppendJourneySegment( widget.journeyId as int, segment.id as int, ); journeyId = widget.journeyId as int; } Navigator.push( context, MaterialPageRoute( builder: (context) => ChooseDeparturePage( startStation: startStation, journeyId: journeyId, arrival: arrival, ), ), ); } } class ArrivalList extends StatelessWidget { final Departure departure; final List arrivalList; final Function onTapped; const ArrivalList({ super.key, required this.departure, required this.arrivalList, required this.onTapped, }); @override Widget build(BuildContext context) { return ListView( children: [ for (Departure arrival in arrivalList) ListTile( title: Text(arrival.stationName), subtitle: Text(arrival.arrivalTime.toString()), onTap: () => onTapped(departure, arrival), ), ], ); } } class JourneyDetailPage extends StatefulWidget { final Journey singleJourney; const JourneyDetailPage({ super.key, required this.singleJourney, }); @override State createState() => _JourneyDetailPageState(); } class _JourneyDetailPageState extends State { Duration duration = Duration(minutes: 0); @override initState() { super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: ListTile( title: Text( '${widget.singleJourney.startStation} → ${widget.singleJourney.endStation}', ), subtitle: Text( '${widget.singleJourney.duration.toString().split(".")[0]}h', ), ), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( child: SizedBox( height: 400, child: SegmentList(journey: widget.singleJourney), ), ), Container( margin: const EdgeInsets.only(bottom: 60.0), child: ElevatedButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => JourneyOverviewPage(), ), ); }, child: const Text('View all Journeys'), ), ), ], ), ), ); } } class SegmentList extends StatelessWidget { final Journey journey; const SegmentList({ super.key, required this.journey, }); @override Widget build(BuildContext context) { return ListView( children: [ for (Segment seg in journey.segments) Card( child: Column( mainAxisSize: .min, children: [ Container( padding: EdgeInsets.only(left: 10, top: 10), child: Row( children: [ Text( seg.vehicle.name, textScaler: TextScaler.linear(1.1), ), Icon(Icons.arrow_right_alt), Text('${seg.start.direction}'), ], ), ), ListTile( leading: Text( timeOf(seg.start.departureTime), textScaler: TextScaler.linear(1.4), ), title: Text(seg.start.stationName), ), ListTile( leading: Text( timeOf(seg.end.arrivalTime), textScaler: TextScaler.linear(1.4), ), title: Text(seg.end.stationName), ), ], ), ), ], ); } } class JourneyOverviewPage extends StatefulWidget { const JourneyOverviewPage({super.key}); @override State createState() => _JourneyOverviewPageState(); } class _JourneyOverviewPageState extends State { List allJourneys = []; @override initState() { super.initState(); } // void retry() async { // // retry with setstate // await Future.delayed(Duration(milliseconds: 1300)); // setState(() {}); // } Future> loadJourneys() async { // TODO: implement batch query to get a list (all) of journeys List journeys; if (allJourneys.isEmpty) { await Future.delayed(Duration(milliseconds: 500)); journeys = await Journey.dbGetAll(); allJourneys = journeys; } else { journeys = allJourneys; } return journeys; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Your Journeys"), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FutureBuilder( future: loadJourneys(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Column( mainAxisAlignment: MainAxisAlignment.center, spacing: 16, children: [ const CircularProgressIndicator(), const Text('Loading Journeys...'), ], ); } if (snapshot.hasError) { return Column( children: [ Text('couldn\'t load Journeys'), ElevatedButton( onPressed: () { Future.delayed(Duration(milliseconds: 300)); setState(() {}); }, child: const Text('retry'), ), ], ); } if (snapshot.hasData == false) { return Column( children: [ Text('No journeys found'), ElevatedButton( onPressed: () { Future.delayed(Duration(milliseconds: 300)); setState(() {}); }, child: const Text('retry'), ), ], ); } return Expanded( child: SizedBox( height: 1, child: JourneyList( journeys: snapshot.data!, onTapped: _handleJourneyTapped, ), ), ); }, ), // ElevatedButton( // onPressed: () { // Navigator.push( // context, // MaterialPageRoute( // builder: (context) => ChooseStationPage(), // ), // ); // }, // child: const Text('Start a route'), // ), ], ), ), floatingActionButton: FloatingActionButton( backgroundColor: Theme.of(context).colorScheme.primaryContainer, foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (context) => ChooseStationPage()), ); }, tooltip: 'Start a route', child: const Icon(Icons.add), ), ); } void _handleJourneyTapped(Journey journey) async { Navigator.push( context, MaterialPageRoute( builder: (context) => JourneyDetailPage(singleJourney: journey), ), ); } } class JourneyList extends StatelessWidget { final List journeys; final Function onTapped; const JourneyList({ super.key, required this.journeys, required this.onTapped, }); @override Widget build(BuildContext context) { return ListView( children: [ for (Journey jour in journeys) Card( margin: EdgeInsets.only( left: 3.0, top: 3.0, bottom: 3.0, right: 3.0, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular(0.0), ), ), elevation: 0.3, clipBehavior: .hardEdge, child: InkWell( child: Column( mainAxisSize: .min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: EdgeInsets.only( left: 15.0, top: 7.0, bottom: 7.0, right: 10.0, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${jour.startStation}', textScaler: TextScaler.linear(1.2), ), Row( children: [ Icon( Icons.arrow_right_alt, size: 25.0, ), Text( '${jour.endStation}', textScaler: TextScaler.linear(1.2), ), ], ), ], ), ), ], ), onTap: () => onTapped(jour), ), ), ], ); } }