28 Commits
Author SHA1 Message Date
fbachus 9d5f0957f9 cleanup: remove unused code blocks that were already commented out 2026-08-09 15:41:01 +02:00
fbachus 4c4ff536f0 feat(ui): declutter appBar information for journeyDetails 2026-08-09 15:38:42 +02:00
fbachus cec21ecde5 cleanup: reorder dependency imports 2026-08-09 15:12:38 +02:00
fbachus b5272e5052 feat(ui): put homepage title back into appbar on top 2026-08-09 15:09:12 +02:00
fbachus 5b073b9e94 feat(ui): add dynamic color Scheme (based on wallpaper) if supported 2026-08-09 15:05:09 +02:00
fbachus 317abbdc8e feat(ui): add dark theme 2026-08-09 14:46:25 +02:00
fbachus c45422da3a feat(ui): increase clarity on journey overview 2026-08-09 14:42:42 +02:00
fbachus 7d96cd14b1 feat(database): return newest journeys first 2026-08-08 01:10:50 +02:00
fbachus ee21ecd2d7 fixup! feat(build): import database platform depending on platform 2026-08-01 03:05:43 +02:00
fbachus 5a81ba4aa2 fix(journey): correct calculation of duration 2026-08-01 03:03:41 +02:00
fbachus 2013721569 feat(ui): rework JourneyDetailScreen 2026-08-01 02:59:24 +02:00
fbachus 18de9a9504 feat(ui): add better descriptors 2026-08-01 01:47:55 +02:00
fbachus 84c8c267b9 fix(navigation): view all journey leads back to journey overview 2026-08-01 01:46:31 +02:00
fbachus 5fbd690faa feat(build): import database platform depending on platform 2026-08-01 01:06:11 +02:00
fbachus 77949f6da6 fix(api_handler): fix wrong date being shown for journeys going past midnight 2026-08-01 00:54:33 +02:00
fbachus 2b7b943ea2 doc(readme): clarify what to do with .env 2026-07-31 22:20:19 +02:00
fbachus 6dfb8d7c09 doc(readme): make the README remotely useful 2026-07-31 22:06:28 +02:00
fbachus d4fbcc460f fix(ui): move up 'view all journeys' button to be unobstructed 2026-07-31 18:33:27 +02:00
fbachus ed3d1a3d32 feat(ui): improve navigation and information 2026-07-31 16:36:33 +02:00
fbachus 87b2033a5c fix(app): enable internet access for release build 2026-07-31 15:56:15 +02:00
fbachus 48e359a315 doc(diagrams): add some diagrams in mermaid format 2026-07-31 04:10:02 +02:00
fbachus ee43064903 doc(requirements): assess things done to those intended 2026-07-31 04:10:02 +02:00
fbachus f5757213e8 cleanup(database): remove unused foreign keys that weren't ref PK's anyway 2026-07-31 04:10:02 +02:00
fbachus fefa3fd365 cleanup(api_handler): remove todo comments 2026-07-31 04:10:02 +02:00
fbachus 1cf594ed42 feat(route building): add time constraint for departure requests 2026-07-31 04:10:02 +02:00
fbachus 5ead82b9c6 build(mobile): deactivate sqlite debugging lib used for desktop 2026-07-30 22:03:26 +02:00
fbachus 0372185e96 fix(database): unify vehicle id column name 2026-07-30 22:02:21 +02:00
fbachus 168ff7b9a4 fix(database): rename last occurences of 'route' to 'journey' 2026-07-30 20:25:41 +02:00
14 changed files with 443 additions and 181 deletions
+37 -10
View File
@@ -1,17 +1,44 @@
# oeffimaster # oeffimaster
A new Flutter project. Aggregating information to create your own routes in public transit effortlessly.
## Getting Started
This project is a starting point for a Flutter application. ## Goal of this project
Do you ever get a route from google maps and see a 13 minute switch time, for a train that comes every ten minutes? Do you then prompt another app with better results. What if you'd normally have to wait 9 minutes for your train, but the previous train is late, so in reality you can switch with only a minutes worth of waiting? Now you can get an earlier train for the next connection, and suddenly the 3rd-fastest connection in google's algorithm becomes the fastest, and you saved 10 minutes - or could have, if you knew.
My app will not solve this issue.
As someone who is familiar with good chunks of the cities public transport network, I often enough *think* I can get faster to my target than the routing served by commonplace apps, but I have a hard time of knowing that for sure. Other apps allow some degree of comparing routes to one another, but it's not a priority, and editing any part of it is not supported at all.
Oeffimaster aims to make that type of information easily accessible and aggregate it in the most helpful way, allowing comparison and editing of routes.
A few resources to get you started if this is your first Flutter project: ## Usage
I am currently providing artifacts for linux and android. To make your own builds, you will need an api key for your network.
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) ## Build the project
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the ### Get the API key
[online documentation](https://docs.flutter.dev/), which offers tutorials, For the VBB network in Berlin/Brandenburg
samples, guidance on mobile development, and a full API reference.
1. Installing flutter: https://docs.flutter.dev/install/quick
2. Get an API key for VBB: https://unternehmen.vbb.de/digitale-services/api/
It will take some time, but they will send you an API key and the url to access it.
this should work for other public transit networks using the HAFAS api, but you will have to test this yourself
### The build part
Create a `.env` in the project root and add the following to it
```env
VBB_API_KEY={YOUR_API_KEY}
VBB_API_URL={YOUR_API_URL}
```
For local builds and connected devices simply run `flutter run` in your terminal.
To build for linux:
```bash
flutter build linux -DVBB_API_KEY={API_KEY} -DVBB_API_URL={API_URL}
```
To build for android:
```
flutter build apk -DVBB_API_KEY={API_KEY} -DVBB_API_URL={API_URL}
```
+1
View File
@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<application <application
android:label="oeffimaster" android:label="oeffimaster"
android:name="${applicationName}" android:name="${applicationName}"
+64
View File
@@ -0,0 +1,64 @@
erDiagram
journey||--|{routesegments: "consists of"
routesegments||--||segment: "sorts"
segment||--|{departure: "consists of"
departure}|--|{station: "originates from"
departure}|--||vehicle: uses
vehicle||..|{line: "shared data"
segment}|--||vehicle: uses
stationlines}|..||station: "general connections"
stationlines}|..||line: groups
journey {
int id PK
string startstation
string endstation
}
routesegments {
int id PK
int journey FK
int segment FK
int segmentorder
}
segment {
int id PK
string ref
int startid FK
int endid FK
string vehicleid FK
}
departure {
int id PK
string ref
string direction
string vehicle FK
string vehiclename
string station FK
string stationname
string arrivaltime
string departuretime
}
station {
string id PK
string name
}
stationlines {
int id PK
string stationid FK
string lineid FK
}
vehicle {
string vehicleid PK
string name
string line FK
}
line {
string lineid PK
string name
string mode
string destination
string direction
string fgcolor
string bgcolor
}
+8
View File
@@ -0,0 +1,8 @@
flowchart TD
Start-->stat[Search Station]-->selstat[Select station]
selstat-->seldep["`Select departure:
vehicle, direction & time`"]
seldep-->selarr["`Select arrival station:
when & where to exit`"] --> sw{switch?} --yes-->seldep
sw--"no"-->fin([journey details])
+19
View File
@@ -0,0 +1,19 @@
sequenceDiagram
participant api@{ "type": "entity"} as public transport API
participant ah as api_handler
participant ui as UI
participant th as transport_helper
participant db@{ "type":"database"} as database
ui-->>ah: find Station
ah-->>api: GET request w/ stationname
api-->>ah: return json
ah-->>th: provide json Map
th-->>db: store object
db-->>th: return id
th-->>ah: return object
ah-->>ui: display data
+10 -10
View File
@@ -1,10 +1,10 @@
- running mobile app - [x] running mobile app
- users should be able to - [ ] users should be able to
- plan public transport in the VBB transit network - [~] plan public transport in the VBB transit network
- create and customise routes quickly and on-the-fly - [~] create and customise routes quickly and on-the-fly
- edit routes from anywhere, where respective route information can be seen - [ ] edit routes from anywhere, where respective route information can be seen
- compare routes easily, especially between different endpoints - [ ] compare routes easily, especially between different endpoints
- the app should offer up the information most relevant to current route planning - [~] the app should offer up the information most relevant to current route planning
- between route steps, there should be no more than 3 taps required, optimally even just one(1), scrolling excluded - [x] between route steps, there should be no more than 3 taps required, optimally even just one(1), scrolling excluded
- e.g. only lines that connect to a station should be shown - [x] e.g. only lines that connect to a station should be shown
- stations that a line goes through should be available as next node/ step for planning - [x] stations that a line goes through should be available as next node/ step for planning
+6 -9
View File
@@ -5,20 +5,13 @@ import 'dart:core';
import 'dart:convert'; import 'dart:convert';
import 'transport_helper.dart'; import 'transport_helper.dart';
import 'utilities.dart';
class ApiHandler { class ApiHandler {
// TODO: Split next line for better Error Handling // TODO: Split next line for better Error Handling
static final String baseUrl = dotenv.get("VBB_API_URL"); static final String baseUrl = dotenv.get("VBB_API_URL");
static final String accessID = dotenv.get("VBB_API_KEY"); static final String accessID = dotenv.get("VBB_API_KEY");
// TODO: function calls for the following API calls
// - [x] location.name: findStations
// - [ ] location.nearbystops
// - [ ] location.details
// - [ ] departureBoard
// - [ ] multiDepartureBoard
// - [ ] journeyDetails
//static Future<Map<String, dynamic>> findStations(String locationName) async { //static Future<Map<String, dynamic>> findStations(String locationName) async {
static Future<List<Station>> findStations( static Future<List<Station>> findStations(
String locationName, { String locationName, {
@@ -79,6 +72,7 @@ class ApiHandler {
static Future<List<Departure>> departures( static Future<List<Departure>> departures(
String stationId, { String stationId, {
int? maxDepartures = -1, int? maxDepartures = -1,
DateTime? datetime,
int? duration = 0, int? duration = 0,
}) async { }) async {
String urlPath = '/api/fahrinfo/latest/departureBoard'; String urlPath = '/api/fahrinfo/latest/departureBoard';
@@ -87,7 +81,10 @@ class ApiHandler {
Uri url = Uri.https(baseUrl, urlPath, { Uri url = Uri.https(baseUrl, urlPath, {
'accessId': accessID, 'accessId': accessID,
'id': stationString, 'id': stationString,
////'time': /current time //time from which departures will be shown 'date': dateOf(datetime ?? DateTime.now()),
'time': timeOf(
datetime ?? DateTime.now(),
), //time from which departures will be shown
'duration': duration.toString(), //interval time in minutes 'duration': duration.toString(), //interval time in minutes
'maxJourneys': maxDepartures.toString(), 'maxJourneys': maxDepartures.toString(),
//'type': 'S', // Station/Stops only //'type': 'S', // Station/Stops only
+199 -92
View File
@@ -1,36 +1,53 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.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 'api_handler.dart';
import 'transport_helper.dart'; import 'transport_helper.dart';
import 'dart:async'; import 'utilities.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; //for testing on desktop
void main() async { void main() async {
await dotenv.load(); await dotenv.load();
const String dbName = "oeffimasterTransportDb"; const String dbName = "oeffimasterTransportDb";
databaseFactory = databaseFactoryFfi; // for debugging and desktop test
if (Platform.isLinux) {
databaseFactory = databaseFactoryFfi; // for debugging and desktop test
}
DbHelper.initDb(dbName); DbHelper.initDb(dbName);
runApp(const MyApp()); runApp(const MyApp());
} }
final _defaultLightScheme = ColorScheme.fromSeed(seedColor: Colors.yellow);
final _defaultDarkScheme = ColorScheme.fromSeed(
seedColor: Colors.yellow,
brightness: Brightness.dark,
);
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
// This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return DynamicColorBuilder(
title: 'Oeffimaster', builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
theme: ThemeData( return MaterialApp(
colorScheme: .fromSeed(seedColor: Colors.yellow), theme: ThemeData(
), useMaterial3: true,
// home: const MyHomePage(title: 'Oeffimaster'), //this is what matters colorScheme: lightDynamic ?? _defaultLightScheme,
home: const JourneyOverviewPage(), ),
// routes: { darkTheme: ThemeData(
// //'/': (context) => const MyHomePage(title: 'Oeffimaster'), useMaterial3: true,
// '/stations': (context) => const ChooseStationPage(), colorScheme: darkDynamic ?? _defaultDarkScheme,
// }, ),
themeMode: ThemeMode.system,
home: const JourneyOverviewPage(),
title: 'Oeffimaster',
);
},
); );
} }
} }
@@ -45,9 +62,7 @@ class MyHomePage extends StatefulWidget {
class _MyHomePageState extends State<MyHomePage> { class _MyHomePageState extends State<MyHomePage> {
int _counter = 0; int _counter = 0;
//String API_KEY = ApiHandler.accessID;
String API_KEY = "no telly"; String API_KEY = "no telly";
//Future<Map<String, dynamic>> ?id;
List<Station> stationList = []; List<Station> stationList = [];
String id = ''; String id = '';
@@ -60,37 +75,14 @@ class _MyHomePageState extends State<MyHomePage> {
@override @override
Widget build(BuildContext context) { 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( return Scaffold(
appBar: AppBar( 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), title: Text(widget.title),
), ),
body: Center( body: Center(
child: Column( child: Column(
mainAxisAlignment: .center, mainAxisAlignment: .center,
children: [ 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( ElevatedButton(
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
@@ -104,6 +96,8 @@ class _MyHomePageState extends State<MyHomePage> {
), ),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer,
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
@@ -162,7 +156,6 @@ class _ChooseStationPageState extends State<ChooseStationPage> {
void _search(String? query) { void _search(String? query) {
if (query?.isEmpty ?? true) return; if (query?.isEmpty ?? true) return;
// do something with query
setState(() { setState(() {
findStationsByName(query!); findStationsByName(query!);
}); });
@@ -177,27 +170,18 @@ class _ChooseStationPageState extends State<ChooseStationPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('RoutePicker')), appBar: AppBar(title: const Text('Start a Journey')),
body: Center( body: Center(
child: Column( child: Column(
children: [ children: [
// ElevatedButton(
// onPressed: () {
// showResult(ApiHandler.findStations('S Biesdorf'));
// },
// child: Text('Search for \'S Biesdorf\''),
// ),
SearchAnchor( SearchAnchor(
builder: (BuildContext context, SearchController controller) { builder: (BuildContext context, SearchController controller) {
return SearchBar( return SearchBar(
controller: controller, controller: controller,
hintText: 'Search for a Station', hintText: 'Search your start Station',
padding: const WidgetStatePropertyAll<EdgeInsets>( padding: const WidgetStatePropertyAll<EdgeInsets>(
EdgeInsets.symmetric(horizontal: 16.0), EdgeInsets.symmetric(horizontal: 16.0),
), ),
// onTap: () {
// controller.openView();
// },
onChanged: _onSearchChanged, onChanged: _onSearchChanged,
onSubmitted: _search, onSubmitted: _search,
leading: const Icon(Icons.search), leading: const Icon(Icons.search),
@@ -234,9 +218,6 @@ class _ChooseStationPageState extends State<ChooseStationPage> {
} }
void _handleStationTapped(Station station) { void _handleStationTapped(Station station) {
// setState(() {
// _selectedStation = station;
// });
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@@ -273,9 +254,11 @@ class StationList extends StatelessWidget {
class ChooseDeparturePage extends StatefulWidget { class ChooseDeparturePage extends StatefulWidget {
final Station startStation; final Station startStation;
final int? journeyId; final int? journeyId;
final Departure? arrival;
const ChooseDeparturePage({ const ChooseDeparturePage({
super.key, super.key,
this.journeyId, this.journeyId,
this.arrival,
required this.startStation, required this.startStation,
}); });
@@ -288,7 +271,10 @@ class _ChooseDeparturePageState extends State<ChooseDeparturePage> {
Journey? singleJourney; Journey? singleJourney;
void loadDepartures(Station startStation) async { void loadDepartures(Station startStation) async {
departureList = await ApiHandler.departures(startStation.id); departureList = await ApiHandler.departures(
startStation.id,
datetime: widget.arrival?.arrivalTime ?? DateTime.now(),
);
setState(() {}); setState(() {});
} }
@@ -315,12 +301,27 @@ class _ChooseDeparturePageState extends State<ChooseDeparturePage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
'RoutePicker for ${widget.startStation.name} // ${widget.journeyId}', 'Departures for ${widget.startStation.name}',
), ),
), ),
body: Center( body: Center(
child: Column( child: Column(
children: [ children: [
Builder(
builder: (context) {
if (widget.arrival != null) {
return ListTile(
title: Text(
'arrive at ${widget.arrival?.arrivalTime}',
),
);
} else {
return ListTile(
title: Text(''),
);
}
},
),
Expanded( Expanded(
child: SizedBox( child: SizedBox(
height: 400, height: 400,
@@ -330,24 +331,24 @@ class _ChooseDeparturePageState extends State<ChooseDeparturePage> {
), ),
), ),
), ),
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) { void _handleDepartureTapped(Departure departure) {
// setState(() {
// _selectedDeparture = departure;
// });
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@@ -422,7 +423,7 @@ class _ChooseArrivalPageState extends State<ChooseArrivalPage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
'Arrivals for ${widget.departure.vehicleName}// ${widget.journeyId}', 'Arrivals for ${widget.departure.vehicleName}',
), ),
), ),
body: Center( body: Center(
@@ -445,9 +446,6 @@ class _ChooseArrivalPageState extends State<ChooseArrivalPage> {
} }
void _handleArrivalTapped(Departure departure, Departure arrival) async { void _handleArrivalTapped(Departure departure, Departure arrival) async {
// setState(() {
// _selectedDeparture = departure;
// });
departure.dbInsert; departure.dbInsert;
arrival.dbInsert; arrival.dbInsert;
Segment newSegment = await Segment.fromDepartures(departure, arrival); Segment newSegment = await Segment.fromDepartures(departure, arrival);
@@ -470,6 +468,7 @@ class _ChooseArrivalPageState extends State<ChooseArrivalPage> {
builder: (context) => ChooseDeparturePage( builder: (context) => ChooseDeparturePage(
startStation: startStation, startStation: startStation,
journeyId: journeyId, journeyId: journeyId,
arrival: arrival,
), ),
), ),
); );
@@ -526,9 +525,44 @@ class _JourneyDetailPageState extends State<JourneyDetailPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Connection Details \n$duration')), appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Connection Details'),
Text(
'Duration: ${widget.singleJourney.duration.toString().split(".")[0]}h',
textScaler: TextScaler.linear(0.7),
),
],
),
),
body: Center( body: Center(
child: SegmentList(journey: widget.singleJourney), 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'),
),
),
],
),
), ),
); );
} }
@@ -547,9 +581,39 @@ class SegmentList extends StatelessWidget {
return ListView( return ListView(
children: [ children: [
for (Segment seg in journey.segments) for (Segment seg in journey.segments)
ListTile( Card(
title: Text(seg.start.direction), child: Column(
subtitle: Text(seg.start.departureTime.toString()), 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),
),
],
),
), ),
], ],
); );
@@ -592,6 +656,9 @@ class _JourneyOverviewPageState extends State<JourneyOverviewPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(
title: Text("Your Journeys"),
),
body: Center( body: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -648,21 +715,12 @@ class _JourneyOverviewPageState extends State<JourneyOverviewPage> {
); );
}, },
), ),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChooseStationPage(),
),
);
},
child: const Text('Start a route'),
),
], ],
), ),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer,
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
@@ -700,9 +758,58 @@ class JourneyList extends StatelessWidget {
return ListView( return ListView(
children: [ children: [
for (Journey jour in journeys) for (Journey jour in journeys)
ListTile( Card(
title: Text('${jour.startStation} to ${jour.endStation}'), margin: EdgeInsets.only(
onTap: () => onTapped(jour), 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),
),
), ),
], ],
); );
+54 -44
View File
@@ -1,4 +1,5 @@
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'utilities.dart';
// import './state_helper.dart'; // import './state_helper.dart';
// import './db_helper.dart'; // import './db_helper.dart';
@@ -41,11 +42,11 @@ class DbHelper {
} }
} }
/// this class covers two database tables: "route" and "routesegments" /// this class covers two database tables: "journey" and "routesegments"
/// while route will for now only hold an id, routesegments will store the /// while journey will for now only hold an id, routesegments will store the
/// m-n/ 1-n relations between routes and segments - the segments contain the most /// m-n/ 1-n relations between journeys and segments - the segments contain the most
/// relevant info, while routesegments stores which route the segments belong to /// relevant info, while routesegments stores which journey the segments belong to
/// and in which order they make up a route /// and in which order they make up a journey
class Journey { class Journey {
static const String tableName = "journey"; static const String tableName = "journey";
// TODO: add last_viewed, last_modified and such // TODO: add last_viewed, last_modified and such
@@ -63,7 +64,7 @@ class Journey {
required this.duration, required this.duration,
}); });
/// toMap for Journey but it's just route ids for now /// toMap for Journey but it's just journey ids for now
Map<String, Object?> _toMapJourney() { Map<String, Object?> _toMapJourney() {
return {"id": id, "startstation": startStation, "endstation": endStation}; return {"id": id, "startstation": startStation, "endstation": endStation};
} }
@@ -74,7 +75,7 @@ class Journey {
List<Map<String, Object?>> tmp = []; List<Map<String, Object?>> tmp = [];
for (final (segmentIdx, segment) in segments.indexed) { for (final (segmentIdx, segment) in segments.indexed) {
tmp.add({ tmp.add({
"route": id, "journey": id,
"segment": segment.id, "segment": segment.id,
"segmentorder": segmentIdx, "segmentorder": segmentIdx,
}); });
@@ -91,16 +92,18 @@ class Journey {
} }
factory Journey.fromMap( factory Journey.fromMap(
Map<String, Object?> routeMaps, Map<String, Object?> journeyMaps,
List<Segment> segmentMaps, List<Segment> segmentMaps,
) { ) {
return Journey( Journey journey = Journey(
id: routeMaps["id"] as int, id: journeyMaps["id"] as int,
startStation: routeMaps["startstation"] as String?, startStation: journeyMaps["startstation"] as String?,
endStation: routeMaps["endstation"] as String?, endStation: journeyMaps["endstation"] as String?,
segments: segmentMaps, segments: segmentMaps,
duration: Duration(minutes: 0), duration: Duration(minutes: 0),
); );
journey.sync();
return journey;
} }
// from trip API request // from trip API request
@@ -134,14 +137,14 @@ class Journey {
if (segments.isNotEmpty) { if (segments.isNotEmpty) {
startStation = segments.first.start.stationName; startStation = segments.first.start.stationName;
endStation = segments.last.end.stationName; endStation = segments.last.end.stationName;
duration = segments.first.start.departureTime.difference( duration = segments.last.end.arrivalTime.difference(
segments.last.end.arrivalTime, segments.first.start.departureTime,
); );
} else { } else {
// if the route has no segments yet, at least the startStation should remain, // if the journey has no segments yet, at least the startStation should remain,
// otherwise delete the thing // otherwise delete the thing
// having a startStation makes the first step faster at least // having a startStation makes the first step faster at least
// no need to put a mostly empty route into the db though // no need to put a mostly empty journey into the db though
endStation = null; endStation = null;
duration = Duration(minutes: 0); duration = Duration(minutes: 0);
} }
@@ -187,9 +190,7 @@ class Journey {
database.execute("""CREATE TABLE IF NOT EXISTS '$tableName'( database.execute("""CREATE TABLE IF NOT EXISTS '$tableName'(
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
startstation TEXT, startstation TEXT,
endstation TEXT, endstation TEXT
FOREIGN KEY (startstation) REFERENCES station(name)
FOREIGN KEY (endstation) REFERENCES station(name)
); );
"""); """);
} }
@@ -199,7 +200,7 @@ class Journey {
String segment = Segment.tableName; String segment = Segment.tableName;
database.execute("""CREATE TABLE IF NOT EXISTS routesegments( database.execute("""CREATE TABLE IF NOT EXISTS routesegments(
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
route INTEGER, journey INTEGER,
segment INTEGER, segment INTEGER,
segmentorder INTEGER, -- TODO: implement sorting segmentorder INTEGER, -- TODO: implement sorting
FOREIGN KEY('$tableName') REFERENCES '$tableName'(id), FOREIGN KEY('$tableName') REFERENCES '$tableName'(id),
@@ -216,16 +217,15 @@ class Journey {
where: "id = ?", where: "id = ?",
whereArgs: [id], whereArgs: [id],
); );
assert(result.length < 2, "found more than 1 route for id $id"); assert(result.length < 2, "found more than 1 journey for id $id");
Journey route = Journey.fromMap(result.first, await routeSegments); Journey journey = Journey.fromMap(result.first, await routeSegments);
return route; return journey;
} }
/// gets routesegments and hands them to the Journey.fromMap factory, works async /// gets routesegments and hands them to the Journey.fromMap factory, works async
static Future<Journey> fromMapWrapper(Map<String, Object?> journeyMap) async { static Future<Journey> fromMapWrapper(Map<String, Object?> journeyMap) async {
Future<List<Segment>> segs = _dbGetJourneySegments(journeyMap["id"] as int); Future<List<Segment>> segs = _dbGetJourneySegments(journeyMap["id"] as int);
Journey res = Journey.fromMap(journeyMap, await segs); Journey res = Journey.fromMap(journeyMap, await segs);
res.sync();
return res; return res;
} }
@@ -236,7 +236,12 @@ class Journey {
static Future<List<Journey>> dbGetAll({int num = 40, int offset = 0}) async { static Future<List<Journey>> dbGetAll({int num = 40, int offset = 0}) async {
//final batch = DbHelper.db.batch; // later //final batch = DbHelper.db.batch; // later
Database database = DbHelper.db; Database database = DbHelper.db;
var tmp = await database.query(tableName, limit: num, offset: offset); var tmp = await database.query(
tableName,
limit: num,
offset: offset,
orderBy: 'id DESC',
);
Future<List<Journey>> journeys = Future.wait( Future<List<Journey>> journeys = Future.wait(
tmp.map((jour) => fromMapWrapper(jour)), tmp.map((jour) => fromMapWrapper(jour)),
); );
@@ -244,14 +249,14 @@ class Journey {
} }
static Future<List<Segment>> _dbGetJourneySegments( static Future<List<Segment>> _dbGetJourneySegments(
int routeId, int journeyId,
) async { ) async {
Database database = DbHelper.db; Database database = DbHelper.db;
String segment = Segment.tableName; String segment = Segment.tableName;
var tmp = await database.rawQuery(""" var tmp = await database.rawQuery("""
SELECT * FROM '$segment' SELECT * FROM '$segment'
JOIN routesegments ON routesegments.segment = '$segment'.id JOIN routesegments ON routesegments.segment = '$segment'.id
WHERE routesegments.route = '$routeId' WHERE routesegments.journey = '$journeyId'
ORDER BY routesegments.segmentorder ASC; ORDER BY routesegments.segmentorder ASC;
"""); """);
@@ -273,15 +278,15 @@ class Journey {
return id_tmp; return id_tmp;
} }
static void dbAppendJourneySegment(int routeId, int segmentid) { static void dbAppendJourneySegment(int journeyId, int segmentid) {
Database database = DbHelper.db; Database database = DbHelper.db;
// generate segmentorder to be 0 if no segment exists for route, otherwise // generate segmentorder to be 0 if no segment exists for journey, otherwise
// take highest segmentorder+1 for given route // take highest segmentorder+1 for given journey
database.rawInsert( database.rawInsert(
"""INSERT INTO routesegments(route, segment, segmentorder) VALUES """INSERT INTO routesegments(journey, segment, segmentorder) VALUES
($routeId, $segmentid, CASE WHEN EXISTS ( ($journeyId, $segmentid, CASE WHEN EXISTS (
SELECT segmentorder FROM routesegments r WHERE r.route=$routeId) THEN ( SELECT segmentorder FROM routesegments r WHERE r.journey=$journeyId) THEN (
SELECT segmentorder AS so FROM routesegments r WHERE r.route=$routeId SELECT segmentorder AS so FROM routesegments r WHERE r.journey=$journeyId
ORDER BY so DESC LIMIT 1) +1 ELSE 0 END); ORDER BY so DESC LIMIT 1) +1 ELSE 0 END);
""", """,
); );
@@ -322,7 +327,7 @@ class Journey {
database.update( database.update(
"routesegments", "routesegments",
routeSegments[i], routeSegments[i],
where: "route = ? AND segmentoder = ?", where: "journey = ? AND segmentoder = ?",
whereArgs: [id, i], whereArgs: [id, i],
); );
} }
@@ -342,7 +347,7 @@ class Journey {
Database database = DbHelper.db; Database database = DbHelper.db;
database.delete( database.delete(
"routesegments", "routesegments",
where: "route = ? AND segmentorder = ?", where: "journey = ? AND segmentorder = ?",
whereArgs: [id, idx], // TODO: test the index whereArgs: [id, idx], // TODO: test the index
); );
} }
@@ -351,7 +356,7 @@ class Journey {
Database database = DbHelper.db; Database database = DbHelper.db;
database.delete( database.delete(
"routesegments", "routesegments",
where: "route = ? AND segmentorder = ?", where: "journey = ? AND segmentorder = ?",
whereArgs: [id, segments.length - 1], // TODO: test the index whereArgs: [id, segments.length - 1], // TODO: test the index
); );
} }
@@ -361,7 +366,7 @@ class Journey {
if (position <= segments.length) { if (position <= segments.length) {
database.delete( database.delete(
"routesegments", "routesegments",
where: "route = ? AND segmentorder >= ?", where: "journey = ? AND segmentorder >= ?",
whereArgs: [id, position], whereArgs: [id, position],
); );
} }
@@ -493,7 +498,7 @@ class Segment {
FROM $tableName s FROM $tableName s
JOIN $departure start ON s.startid = start.id JOIN $departure start ON s.startid = start.id
JOIN $departure end ON s.endid = end.id JOIN $departure end ON s.endid = end.id
JOIN $vehicle v ON s.vehicleid = v.id JOIN $vehicle v ON s.vehicleid = v.vehicleid
where s.id = $id; where s.id = $id;
""", """,
); );
@@ -526,7 +531,7 @@ class Segment {
"departuretime": seg["end_departuretime"] as String, "departuretime": seg["end_departuretime"] as String,
}; };
Map<String, String> vehicleMap = { Map<String, String> vehicleMap = {
"id": seg["vehicleid"] as String, "vehicleid": seg["vehicleid"] as String,
"name": seg["vehiclename"] as String, "name": seg["vehiclename"] as String,
"line": seg["vehicleline"] as String, "line": seg["vehicleline"] as String,
}; };
@@ -735,7 +740,7 @@ class Vehicle {
Map<String, Object?> _toMap() { Map<String, Object?> _toMap() {
return { return {
"id": vehicleid, "vehicleid": vehicleid,
"name": name, "name": name,
"line": vehicleLineId, "line": vehicleLineId,
}; };
@@ -745,7 +750,7 @@ class Vehicle {
Map<String, Object?> map, Map<String, Object?> map,
) { ) {
return Vehicle( return Vehicle(
vehicleid: map["id"] as String, vehicleid: map["vehicleid"] as String,
name: map["name"] as String, name: map["name"] as String,
vehicleLineId: map["line"] as String, vehicleLineId: map["line"] as String,
); );
@@ -775,7 +780,7 @@ class Vehicle {
Database database = DbHelper.db; Database database = DbHelper.db;
var vehicleMap = await database.query( var vehicleMap = await database.query(
tableName, tableName,
where: "id = ?", where: "vehicleid = ?",
whereArgs: [vehicleId], whereArgs: [vehicleId],
); );
return Vehicle.fromMap(vehicleMap.first); return Vehicle.fromMap(vehicleMap.first);
@@ -792,7 +797,12 @@ class Vehicle {
void dbUpdate() async { void dbUpdate() async {
Database database = DbHelper.db; Database database = DbHelper.db;
database.update(tableName, _toMap(), where: "id=?", whereArgs: [vehicleid]); database.update(
tableName,
_toMap(),
where: "vehicleid=?",
whereArgs: [vehicleid],
);
} }
void dbDelete() async { void dbDelete() async {
+13
View File
@@ -0,0 +1,13 @@
String timeOf(DateTime fullDT) {
// doing it this way so we always get two digits on the hour
String full = fullDT.toString();
List<String> splits = full.split(" ")[1].split(":");
String time = '${splits[0]}:${splits[1]}';
return time;
}
String dateOf(DateTime fullDT) {
String full = fullDT.toString();
String date = full.split(" ")[0];
return date;
}
@@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
} }
+1
View File
@@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
+22 -14
View File
@@ -65,6 +65,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.9" version: "1.0.9"
dynamic_color:
dependency: "direct main"
description:
name: dynamic_color
sha256: "4aeb76de323e8eb660bab5557d826ad7ebbf1434a598f0c6aa8a11a9696a6757"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -215,10 +223,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: native_toolchain_c name: native_toolchain_c
sha256: f59351d28f49520cd3a74eb1f41c5f19ae15e53c65a3231d14af672e46510a96 sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.1" version: "0.19.2"
path: path:
dependency: transitive dependency: transitive
description: description:
@@ -292,26 +300,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqflite_common name: sqflite_common
sha256: cce558075afe2a83f3fd7fc123acd6b090683e4f23910d44fbb31ecd7800b014 sha256: "5bf6a55c166e73bf651ba7ec3ed486e577620e3dc8f3a9c6a258a8031b624590"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.9" version: "2.5.11"
sqflite_common_ffi: sqflite_common_ffi:
dependency: "direct dev" dependency: "direct main"
description: description:
name: sqflite_common_ffi name: sqflite_common_ffi
sha256: "3ddad0ec96ad411d5fea45b4912c3cd5743436c9e1890c26a6e688a32d901cae" sha256: "5ccd38136edb9beb3213f6927775d52db70dfdadcdb28dad1f625ca9f2b9824f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.1" version: "2.4.2"
sqflite_darwin: sqflite_darwin:
dependency: transitive dependency: transitive
description: description:
name: sqflite_darwin name: sqflite_darwin
sha256: "164a5d73ab87a134566057219988bafde837029a64264e61f1f04376ef3cfcd2" sha256: c86ca18b8f666bbf903924687fe21cc16fc385d086005067e26619ca530bef9f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.3" version: "2.4.3+1"
sqflite_platform_interface: sqflite_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -321,13 +329,13 @@ packages:
source: hosted source: hosted
version: "2.4.1" version: "2.4.1"
sqlite3: sqlite3:
dependency: "direct dev" dependency: "direct main"
description: description:
name: sqlite3 name: sqlite3
sha256: "9488c7d2cdb1091c91cacf7e207cff81b28bff8e366f042bad3afe7d34afe189" sha256: "64b2c63c8232dd20d14b34105a81ebfd74320442e8451f836179ec89986aa478"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.2" version: "3.5.1"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -356,10 +364,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: synchronized name: synchronized
sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153" sha256: "61894a1956de6b4fc1aefd0892e109514a1a706cbece3ac59decd90ff5a7a423"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.1" version: "3.4.1+1"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
+3
View File
@@ -37,6 +37,9 @@ dependencies:
flutter_dotenv: ^6.0.1 flutter_dotenv: ^6.0.1
http: ^1.6.0 http: ^1.6.0
sqflite: ^2.4.2+1 sqflite: ^2.4.2+1
sqlite3: ^3.3.0
sqflite_common_ffi: ^2.4.0+3
dynamic_color: ^1.9.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter