Files
Bus-Infotainment--IBus-/lib/remaster/dashboard.dart
2024-05-20 09:06:38 +01:00

2176 lines
66 KiB
Dart

import 'package:bus_infotainment/auth/auth_api.dart';
import 'package:bus_infotainment/backend/live_information.dart';
import 'package:bus_infotainment/backend/modules/tracker.dart';
import 'package:bus_infotainment/pages/components/ibus_display.dart';
import 'package:bus_infotainment/pages/home.dart';
import 'package:bus_infotainment/tfl_datasets.dart';
import 'package:bus_infotainment/utils/OrdinanceSurveyUtils.dart';
import 'package:bus_infotainment/utils/delegates.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:dart_ping/dart_ping.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:native_qr/native_qr.dart';
import 'package:flutter_carousel_widget/flutter_carousel_widget.dart';
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
import 'package:vector_math/vector_math.dart' hide Colors;
import '../backend/modules/tube_info.dart';
Color rgb(int r, int g, int b) {
return Color.fromRGBO(r, g, b, 1);
}
class HomePage_Re extends StatefulWidget {
@override
State<HomePage_Re> createState() => _HomePage_ReState();
}
class _HomePage_ReState extends State<HomePage_Re> {
Future<bool> _shouldRedirectToSetup() async {
List<bool> perms = [];
perms.addAll([
await Permission.manageExternalStorage.isGranted,
await Permission.location.isGranted
]);
bool shouldRedirectA = !perms.contains(false);
bool shouldRedirectB = true;
try {
Uint8List bytes = await LiveInformation().announcementModule.getBundleBytes();
shouldRedirectB = true;
} catch (e) {
print("Failed to load bundle");
shouldRedirectB = false;
}
print("Should redirect to setup: ${shouldRedirectA || shouldRedirectB}");
print("Permissions: $shouldRedirectA");
print("Bundle: $shouldRedirectB");
print("Permissions_indv: $perms");
return (!shouldRedirectA || !shouldRedirectB);
}
@override
void initState() {
super.initState();
_shouldRedirectToSetup().then((value) {
if (value) {
Navigator.pushNamed(context, "/setup");
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
"Choose mode:",
style: ShadTheme.of(context).textTheme.h1.copyWith(),
),
SizedBox(
height: 16,
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
ShadCard(
title: const Text("Solo mode"),
width: 300,
description: const Text(
"Choose this mode if you are only using this device. (No internet required)"
),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 4,
),
ShadButton.secondary(
onPressed: () {
Navigator.pushNamed(context, "/routes");
},
text: const Text("Continue"),
)
],
),
),
SizedBox(
width: 16,
),
ShadCard(
title: const Text("Multi mode"),
width: 300,
description: const Text(
"Choose this mode if you are using multiple devices. (Internet required)"
),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 4,
),
ShadButton.secondary(
onPressed: () {
Navigator.pushNamed(context, "/multi");
},
text: const Text("Continue"),
)
],
),
),
SizedBox(
width: 16,
),
ShadCard(
title: const Text("Setup"),
width: 300,
description: const Text(
"This button is only for debug mode. If you see this button in production, please contact support."
),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 4,
),
ShadButton.secondary(
onPressed: () async {
LiveInformation().announcementModule.setBundleBytes(null);
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.remove("AnnouncementsFileLocation");
Navigator.pushNamed(context, "/setup");
},
text: const Text("Continue"),
)
],
),
),
ShadCard(
title: const Text("Websocket test"),
width: 300,
description: const Text(
"This button is only for debug mode. If you see this button in production, please contact support."
),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 4,
),
ShadButton.secondary(
onPressed: () {
Navigator.pushNamed(context, "/websocket");
},
text: const Text("Continue"),
)
],
),
)
],
),
)
],
),
),
);
// return Scaffold(
// body: Container(
//
// padding: const EdgeInsets.all(16),
//
// alignment: Alignment.center,
//
// child: SizedBox(
//
// // width: double.infinity,
// height: double.infinity,
//
// child: Column(
// children: [
//
// const Text(
// "Choose mode:",
// style: TextStyle(
// fontSize: 32,
// fontWeight: FontWeight.w600,
// )
// ),
//
// Row(
//
// mainAxisSize: MainAxisSize.min,
// crossAxisAlignment: CrossAxisAlignment.start,
//
// children: [
//
// Text("Test"),
//
// ShadCard(
// title: const Text("Solo mode"),
// width: double.infinity,
// description: const Text(
// "Choose this mode if you are only using this device. (No internet required)"
// ),
// content: Column(
//
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisSize: MainAxisSize.min,
//
// children: [
//
// const SizedBox(
// height: 4,
// ),
//
// ShadButton.secondary(
// onPressed: () {
// Navigator.pushNamed(context, "/routes");
// },
// text: const Text("Continue"),
// )
//
// ],
// ),
// ),
//
// const SizedBox(
// width: 16,
// ),
//
// ShadCard(
// title: const Text("Multi mode"),
// width: double.infinity,
// description: const Text(
// "Choose this mode if you are using multiple devices. (Internet required)"
// ),
// content: Column(
//
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisSize: MainAxisSize.min,
//
// children: [
//
// const SizedBox(
// height: 4,
// ),
//
// ShadButton.secondary(
// onPressed: () {
// Navigator.pushNamed(context, "/multi");
// },
// text: const Text("Continue"),
// )
//
// ],
// ),
// )
//
//
//
// ],
//
// ),
// ],
// ),
// )
//
// ),
// );
}
}
class RoutePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
Container(
height: double.infinity,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
padding: const EdgeInsets.fromLTRB(
8,
8,
0,
8
),
child: RotatedBox(
quarterTurns: 3,
child: Text(
"Routes - Nearby",
style: ShadTheme.of(context).textTheme.h3.copyWith(
height: 0.8
),
),
),
),
Container(
width: 230,
child: Scrollbar(
thumbVisibility: true,
child: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.fromLTRB(
4,
4,
4,
4
),
shrinkWrap: true,
children: [
..._getNearbyRoutes(
multiMode: ModalRoute.of(context)!.settings.name!.contains("multi")
)
],
),
),
)
],
),
),
Container(
width: 2,
height: double.infinity,
color: Colors.grey.shade400,
),
Expanded(
child: Container(
height: double.infinity,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
padding: const EdgeInsets.fromLTRB(
8,
8,
8,
8
),
child: RotatedBox(
quarterTurns: 3,
child: Text(
"Routes - All",
style: ShadTheme.of(context).textTheme.h3.copyWith(
height: 0.8
),
),
),
),
Expanded(
child: RouteSearch(
multiMode: ModalRoute.of(context)!.settings.name!.contains("multi"),
),
),
Container(
width: 2,
height: double.infinity,
color: Colors.grey.shade400,
),
RotatedBox(
quarterTurns: 3,
child: NavigationBar()
)
],
),
),
)
],
),
);
// return Scaffold(
// body: Column(
// children: [
// Expanded(
// child: Container(
//
// padding: const EdgeInsets.all(16),
//
// alignment: Alignment.center,
//
// child: SizedBox(
//
// width: double.infinity,
//
// child: Column(
//
// mainAxisSize: MainAxisSize.max,
// crossAxisAlignment: CrossAxisAlignment.start,
//
// children: [
//
// Row(
// children: [
//
// Text(
// "Routes",
// style: ShadTheme.of(context).textTheme.h1.copyWith(),
// ),
//
// Expanded(
// child: Container(),
// ),
//
// ],
// ),
// if (!kIsWeb)
// Text(
// "Nearby routes",
// style: ShadTheme.of(context).textTheme.h4,
// ),
// if (!kIsWeb)
// FlutterCarousel(
// options: CarouselOptions(
// // height: 130,
// viewportFraction: 0.33,
// aspectRatio: 3 / 1,
// enableInfiniteScroll: true,
// initialPage: 1,
// autoPlay: true,
// autoPlayInterval: const Duration(seconds: 2),
// pauseAutoPlayOnTouch: true,
// pauseAutoPlayOnManualNavigate: true,
// showIndicator: false,
// slideIndicator: const CircularSlideIndicator(),
// autoPlayAnimationDuration: const Duration(milliseconds: 800),
// autoPlayCurve: Curves.bounceOut,
//
//
// ),
// items: [
// ..._getNearbyRoutes()
// ],
// ),
//
// const Divider(),
//
// RouteSearch(multiMode: false)
//
//
// ],
//
// ),
// )
//
// ),
// ),
// const Divider(
// height: 1,
// ),
// NavigationBar()
// ],
// ),
// );
}
}
class RouteSearch extends StatefulWidget {
final bool multiMode;
RouteSearch({required this.multiMode});
@override
State<RouteSearch> createState() => _RouteSearchState();
}
class _RouteSearchState extends State<RouteSearch> {
TextEditingController controller = TextEditingController();
@override
Widget build(BuildContext context) {
List<Widget> routes = [];
for (BusRoute route in LiveInformation().busSequences.routes.values.toList()) {
if (controller.text.isNotEmpty && !route.routeNumber.toLowerCase().contains(controller.text.toLowerCase())) {
continue;
}
routes.add(RouteCard(route: route, multiMode: widget.multiMode));
}
return Expanded(
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(
4,
4,
4,
0
),
child: ShadInput(
placeholder: const Text("Search for a route..."),
controller: controller,
onChanged: (value) {
setState(() {
});
},
),
),
Expanded(
child: Scrollbar(
thumbVisibility: true,
trackVisibility: true,
scrollbarOrientation: ScrollbarOrientation.bottom,
child: GridView.extent(
// padding: const EdgeInsets.all(4),
scrollDirection: Axis.vertical,
maxCrossAxisExtent: 120,
children: [
...routes
],
),
),
),
SizedBox(
height: 4,
)
],
),
);
}
}
class RouteCard extends StatelessWidget {
BusRoute route;
final bool multiMode;
RouteCard({required this.route, required this.multiMode});
@override
Widget build(BuildContext context) {
// TODO: implement build
Map<String, Widget> variants = {};
for (BusRouteVariant variant in route.routeVariants.values) {
String variantLabel = "${variant.busStops.first.formattedStopName} -> ${variant.busStops.last.formattedStopName}";
variants[variantLabel] = ShadOption(
value: variant.routeVariant.toString(),
child: Text(variantLabel),
);
}
String rr = "";
if (route.routeNumber.toLowerCase().startsWith("ul")) {
rr = "Rail replacement";
TubeLine? line = LiveInformation().tubeStations.getClosestLine(route.routeVariants.values.first);
rr = line?.name ?? rr;
if (!["London Overground", "DLR", "Rail replacement", "Elizabeth Line"].contains(rr)) {
rr += " line";
}
if (rr == "Hammersmith and City line") {
rr = "Hammersmith & City";
}
}
return ShadButton.secondary(
text: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Route \n ${route.routeNumber}",
style: ShadTheme.of(context).textTheme.h3.copyWith(
height: 1.1
)
),
if (route.routeNumber.toLowerCase().startsWith("ul"))
Text(rr, style: const TextStyle(fontSize: 8))
],
),
padding: const EdgeInsets.all(8),
width: 100,
height: 100,
size: ShadButtonSize.icon,
onPressed: () {
showShadSheet(
side: ShadSheetSide.right,
context: context,
builder: (context) {
List<Widget> variantWidgets = [];
for (BusRouteVariant variant in route.routeVariants.values) {
variantWidgets.add(
ShadButton.outline(
text: SizedBox(
width: 800-490,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("${variant.busStops.first.formattedStopName} ->"),
const SizedBox(
height: 2,
),
Text(variant.busStops.last.formattedStopName)
],
),
),
width: double.infinity,
height: 50,
padding: const EdgeInsets.all(8),
onPressed: () async {
LiveInformation liveInformation = LiveInformation();
await liveInformation.setRouteVariant(variant);
if (!multiMode) {
Navigator.popAndPushNamed(context, "/enroute");
} else {
Navigator.popAndPushNamed(context, "/multi/enroute");
}
},
)
);
variantWidgets.add(const SizedBox(
height: 4,
));
}
return ShadSheet(
title: Text("Route ${route.routeNumber} - Variants"),
content: Container(
width: 350,
constraints: const BoxConstraints(
maxHeight: 400
),
alignment: Alignment.center,
child: Scrollbar(
thumbVisibility: true,
child: SingleChildScrollView(
child: Column(
children: [
...variantWidgets
],
),
),
),
),
padding: const EdgeInsets.all(8),
);
}
);
},
);
}
}
class EnRoutePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
body: _dash(),
);
}
}
class _dash extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(8),
child: ibus_display()
),
const Divider(
height: 1,
),
ExpandableCarousel(
items: [
EasyAnnouncementPicker(
announcements: LiveInformation().announcementModule.manualAnnouncements,
title: "Manual"
),
Container(
padding: const EdgeInsets.all(8),
child: StopAnnouncementPicker(
routeVariant: LiveInformation().getRouteVariant()!,
backgroundColor: Colors.transparent,
outlineColor: Colors.white,
label: "Bus Stops",
)
)
],
options: CarouselOptions(
showIndicator: false
),
),
const Divider(
height: 1,
),
Expanded(
child: Container(
padding: const EdgeInsets.all(8),
child: Column(
children: [
Text(
"Quick actions",
style: ShadTheme.of(context).textTheme.h3,
),
const SizedBox(
height: 8,
),
Container(
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: ShadTheme.of(context).colorScheme.primary,
width: 1
)
),
padding: const EdgeInsets.all(4),
child: Container(
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: ShadTheme.of(context).colorScheme.primary,
width: 1
)
),
child: Column(
children: [
if (!kIsWeb)
AnnouncementEntry(
label: "Display next stop",
index: 0,
outlineColor: ShadTheme.of(context).colorScheme.primary,
onPressed: () {
LiveInformation liveInformation = LiveInformation();
TrackerModule trackerModule = liveInformation.trackerModule;
BusRouteStop? stop = trackerModule.nearestStop;
if (stop != null) {
liveInformation.announcementModule.queueAnnounceByAudioName(displayText: stop.formattedStopName);
} else {
ShadToaster.of(context).show(
const ShadToast(
title: Text("No bus stop found"),
description: Text("No bus stop found nearby"),
duration: Duration(seconds: 5),
)
);
}
},
),
Container(
height: 1,
color: ShadTheme.of(context).colorScheme.primary,
),
AnnouncementEntry(
label: "Announce destination",
index: 1,
outlineColor: ShadTheme.of(context).colorScheme.primary,
onPressed: () {
LiveInformation liveInformation = LiveInformation();
liveInformation.announcementModule.queueAnnouncementByRouteVariant(
routeVariant:
liveInformation.getRouteVariant()!
);
},
),
],
),
),
),
SizedBox(
height: 8,
),
Container(
padding: const EdgeInsets.all(8),
child: ShadButton(
text: const Text("Fullscreen display"),
onPressed: () {
Navigator.pushNamed(context, "/display");
},
icon: const Icon(Icons.fullscreen),
width: double.infinity,
),
),
//
// ShadCard(
// title: Text("Stop announcements"),
// width: double.infinity,
// ),
//
// ShadButton(
// text: Text("Route scanner"),
// onPressed: () {
//
// LiveInformation liveInformation = LiveInformation();
//
// TubeLine? line = liveInformation.tubeStations.getClosestLine(liveInformation.getRouteVariant()!);
//
// ShadToaster.of(context).show(
// ShadToast(
// title: Text("Closest line"),
// description: Text(line == null ? "No line found" : line.name),
// duration: Duration(seconds: 5),
// )
// );
//
// },
// ),
//
// ShadButton(
// text: Text("dest"),
// onPressed: () {
// LiveInformation liveInformation = LiveInformation();
// liveInformation.announcementModule.queueAnnouncementByRouteVariant(routeVariant: liveInformation.getRouteVariant()!);
// },
// ),
//
// ShadButton(
// text: Text("Open Legacy dashboard"),
// onPressed: () {
// Navigator.pushNamed(context, "/legacy");
// },
// )
],
)
),
),
const Divider(
height: 1,
),
NavigationBar()
],
);
}
}
class EasyAnnouncementPicker extends StatelessWidget {
late final List<AnnouncementQueueEntry> announcements;
late final String title;
Color outlineColor = Colors.white;
EasyAnnouncementPicker({this.announcements = const [], this.title = "Announcements", this.outlineColor = Colors.white});
@override
Widget build(BuildContext context) {
List<Widget> announcementWidgets = [];
for (AnnouncementQueueEntry entry in announcements) {
if (entry is NamedAnnouncementQueueEntry) {
announcementWidgets.add(
AnnouncementEntry(
label: entry.shortName,
onPressed: () {
LiveInformation liveInformation = LiveInformation();
liveInformation.announcementModule.queueAnnouncementByInfoIndex(
infoIndex: liveInformation.announcementModule.manualAnnouncements.indexOf(entry),
sendToServer: true
);
},
index: announcements.indexOf(entry),
outlineColor: outlineColor,
)
);
}
}
return AnnouncementPicker(
announcements: announcementWidgets,
backgroundColor: const Color(/*Transparent*/0x00000000),
outlineColor: outlineColor,
label: title,
);
}
}
/*
ShadSelectFormField<String>(
options: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("Choose a variant"),
),
...variants.values
],
selectedOptionBuilder: (context, value) => value ==
'none'
? const Text('Select a verified email to display')
: Text(variants.keys.toList()[int.parse(value)-1]!),
placeholder: Text("Choose a variant"),
),
*/
class MultiModeSetup extends StatefulWidget {
@override
State<MultiModeSetup> createState() => _MultiModeSetupState();
}
class _MultiModeSetupState extends State<MultiModeSetup> {
@override
void initState() {
// Check if the user is logged in
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Multi mode options:",
style: ShadTheme.of(context).textTheme.h1.copyWith(),
),
const SizedBox(
height: 16,
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
ShadCard(
title: const Text("Host a group"),
width: 300,
description: const Text(
""
),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 4,
),
ShadButton.secondary(
onPressed: () async {
LiveInformation liveInformation = LiveInformation();
Future.delayed(Duration.zero, () {
print("At time of loading: ${liveInformation.auth.status}");
if (liveInformation.auth.status != AuthStatus.AUTHENTICATED) {
Navigator.popAndPushNamed(context, "/multi/login");
}
});
await liveInformation.createRoom(liveInformation.auth.userID!);
Navigator.pushNamed(context, "/multi/enroute");
},
text: const Text("Continue"),
)
],
),
),
const SizedBox(
width: 16,
),
ShadCard(
title: const Text("Join existing group"),
width: 300,
description: const Text(
""
),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 4,
),
ShadButton.secondary(
onPressed: () {
Navigator.pushNamed(context, "/multi/join");
},
text: const Text("Continue"),
)
],
),
)
],
)
],
),
),
),
NavigationBar()
],
)
);
}
}
class MultiModeEnroute extends StatefulWidget {
@override
State<MultiModeEnroute> createState() => _MultiModeEnrouteState();
}
class _MultiModeEnrouteState extends State<MultiModeEnroute> {
late final Future<void> roomCodeFuture;
late ListenerReceipt<BusRouteVariant?> listenerReceipt;
@override
void initState() {
super.initState();
LiveInformation liveInformation = LiveInformation();
listenerReceipt = liveInformation.routeVariantDelegate.addListener((value) {
setState(() {
});
});
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
LiveInformation liveInformation = LiveInformation();
liveInformation.routeVariantDelegate.removeListener(listenerReceipt);
}
@override
Widget build(BuildContext context) {
// Set the screen to portrait
// Generate random uuid
LiveInformation liveInformation = LiveInformation();
return PopScope(
canPop: false,
onPopInvoked: (didPop) {
if (didPop){
print("Compensating for pop");
liveInformation.leaveRoom();
return;
}
// return;
// Ask the user to confirm if they want to leave the group
showShadDialog(
context: context,
builder: (context) {
return ShadDialog(
title: const Text("Leave group?"),
content: const Text("Are you sure you want to leave the group?"),
actions: [
ShadButton(
text: const Text("Leave"),
onPressed: () {
liveInformation.leaveRoom();
Navigator.pop(context);
Navigator.pop(context);
},
),
ShadButton(
text: const Text("Cancel"),
onPressed: () {
Navigator.pop(context);
},
)
],
);
}
);
},
child: Scaffold(
body: Column(
children: [
const Divider(
height: 1,
),
Container(
padding: EdgeInsets.all(8),
child: ibus_display()
),
const Divider(
height: 1,
),
Container(
padding: EdgeInsets.all(8),
child: Text(
"* Swipe left and right below for more options!",
textAlign: TextAlign.center,
),
),
const Divider(
height: 1,
),
SizedBox(
height: 16,
),
Expanded(
child: FlutterCarousel(
items: [
Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.white,
width: 1
),
borderRadius: BorderRadius.circular(8)
),
padding: const EdgeInsets.all(4),
margin: const EdgeInsets.only(
left: 16,
right: 16,
),
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!kIsWeb)
Container(
padding: const EdgeInsets.only(
top: 4,
left: 8,
right: 8,
bottom: 4
),
child: Text(
"Nearby routes",
style: ShadTheme.of(context).textTheme.h4,
),
),
if (!kIsWeb)
Expanded(
child: Scrollbar(
interactive: true,
radius: const Radius.circular(8),
thickness: 8,
thumbVisibility: true,
child: GridView.count(
crossAxisCount: 3,
children: [
..._getNearbyRoutes(multiMode: true)
],
shrinkWrap: true,
),
),
)
],
)
),
Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.white,
width: 1
),
borderRadius: BorderRadius.circular(8)
),
padding: const EdgeInsets.all(4),
margin: const EdgeInsets.only(
left: 16,
right: 16,
),
child: Expanded(child: RouteSearch(multiMode: true,))
),
Container(
decoration: BoxDecoration(
border: Border.all(
width: 1,
color: Colors.white
),
borderRadius: BorderRadius.all(Radius.circular(8))
),
margin: const EdgeInsets.only(
left: 16,
right: 16,
),
padding: EdgeInsets.all(8),
child: SingleChildScrollView(
child: Column(
children: [
EasyAnnouncementPicker(
announcements: LiveInformation().announcementModule.manualAnnouncements,
title: "Manual",
outlineColor: ShadTheme.of(context).colorScheme.secondary
),
if (liveInformation.getRouteVariant() != null)
SizedBox(
height: 16,
),
if (liveInformation.getRouteVariant() != null)
Container(
child: StopAnnouncementPicker(
routeVariant: LiveInformation().getRouteVariant()!,
backgroundColor: Colors.transparent,
outlineColor: ShadTheme.of(context).colorScheme.secondary,
label: "Bus Stops",
)
)
],
),
),
),
Container(
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: ShadTheme.of(context).colorScheme.primary,
width: 1
)
),
margin: const EdgeInsets.only(
left: 16,
right: 16,
),
padding: const EdgeInsets.all(4),
child: Container(
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: ShadTheme.of(context).colorScheme.primary,
width: 1
)
),
child: Column(
children: [
if (!kIsWeb)
AnnouncementEntry(
label: "Display next stop",
index: 0,
outlineColor: ShadTheme.of(context).colorScheme.primary,
onPressed: () {
LiveInformation liveInformation = LiveInformation();
TrackerModule trackerModule = liveInformation.trackerModule;
BusRouteStop? stop = trackerModule.nearestStop;
if (stop != null) {
liveInformation.announcementModule.queueAnnounceByAudioName(displayText: stop.formattedStopName);
} else {
ShadToaster.of(context).show(
const ShadToast(
title: Text("No bus stop found"),
description: Text("No bus stop found nearby"),
duration: Duration(seconds: 5),
)
);
}
},
),
if (!kIsWeb)
Container(
height: 1,
color: ShadTheme.of(context).colorScheme.primary,
),
AnnouncementEntry(
label: "Announce destination",
index: 1,
outlineColor: ShadTheme.of(context).colorScheme.primary,
onPressed: () {
LiveInformation liveInformation = LiveInformation();
liveInformation.announcementModule.queueAnnouncementByRouteVariant(
routeVariant:
liveInformation.getRouteVariant()!
);
},
),
Container(
height: 1,
color: ShadTheme.of(context).colorScheme.primary,
),
],
),
),
),
],
options: CarouselOptions(
showIndicator: false,
viewportFraction: 1,
height: double.infinity,
enableInfiniteScroll: true
),
),
),
Container(
padding: const EdgeInsets.all(8),
child: ShadButton(
text: const Text("Fullscreen display"),
onPressed: () {
Navigator.pushNamed(context, "/display");
},
icon: const Icon(Icons.fullscreen),
width: double.infinity,
),
),
const Divider(
height: 1,
),
Container(
padding: const EdgeInsets.all(16),
// height: 200,
child: ShadCard(
title: liveInformation.isHost ? const Text("Currently hosting group") : const Text("Successfully joined group"),
border: Border.all(
color: Colors.amber,
width: 1
),
padding: const EdgeInsets.all(16),
width: double.infinity,
description: liveInformation.isHost ? const Text(
"You are hosting a group. \nShare the room code with others to join"
) : const Text(
"You have joined a group."
),
content: Column(
children: [
const SizedBox(
height: 4,
),
FutureBuilder(
future: Future.delayed(const Duration(seconds: 1)),
builder: (context, snapshot) {
return Row(
children: [
Expanded(
child: ShadButton(
text: Text(
liveInformation.roomCode!,
),
icon: const Icon(Icons.copy),
padding: const EdgeInsets.all(8),
onPressed: () {
Clipboard.setData(ClipboardData(text: liveInformation.roomCode!));
ShadToaster.of(context).show(
const ShadToast(
title: Text("Copied to clipboard"),
description: Text("Room code copied to clipboard"),
duration: Duration(seconds: 5),
)
);
},
),
),
ShadButton(
icon: const Icon(Icons.qr_code),
onPressed: () {
showShadDialog(
context: context,
builder: (context) {
return ShadDialog(
title: const Text("QR Code"),
content: Container(
width: 200,
height: 225,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
QrImageView(
data: liveInformation.roomCode!,
size: 200,
backgroundColor: Colors.white,
),
const SizedBox(
height: 8,
),
Text("Scan QR code to join the group")
],
),
),
actions: [
ShadButton(
text: const Text("Close"),
onPressed: () {
Navigator.pop(context);
},
)
],
);
}
);
},
)
],
);
},
),
],
),
),
),
const Divider(
height: 1,
),
NavigationBar()
],
),
),
);
}
}
class MultiModeJoin extends StatefulWidget {
@override
State<MultiModeJoin> createState() => _MultiModeJoinState();
}
class _MultiModeJoinState extends State<MultiModeJoin> {
TextEditingController controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ShadCard(
title: const Text("Join a group"),
width: double.infinity,
description: const Text("Enter the room code to join a group"),
content: Column(
children: [
ShadInputFormField(
controller: controller,
id: "roomCode",
label: const Text("Room code"),
placeholder: const Text("Enter the room code"),
validator: (value) {
if (value.isEmpty) {
return "Please enter the room code";
}
return null;
},
),
ShadButton(
text: const Text("Join"),
width: double.infinity,
onPressed: () async {
LiveInformation liveInformation = LiveInformation();
liveInformation.setRouteVariant(null);
await liveInformation.joinRoom(controller.text);
Navigator.popAndPushNamed(context, "/multi/enroute");
},
)
],
),
),
const SizedBox(
height: 16,
),
ShadButton.secondary(
text: Text("Scan QR code"),
icon: const Icon(Icons.qr_code),
onPressed: () async {
try {
NativeQr nativeQr = NativeQr();
String? result = await nativeQr.get();
controller.text = result!;
} catch (e) {
print("Failed to scan QR code");
}
}
)
// ibus_display(),
// Expanded(child: _dash())
]
),
),
),
Divider(
height: 1
),
NavigationBar()
],
),
);
}
}
class FullscreenDisplay extends StatefulWidget {
@override
State<FullscreenDisplay> createState() => _FullscreenDisplayState();
}
class _FullscreenDisplayState extends State<FullscreenDisplay> {
@override
Widget build(BuildContext context) {
// Get the current screen orientation
final Orientation orientation = MediaQuery.of(context).orientation;
return Scaffold(
body: Container(
color: Colors.black,
alignment: Alignment.center,
child: Row(
children: [
Expanded(
child: ibus_display(
hasBorder: false,
),
),
Container(
alignment: Alignment.bottomRight,
child: ShadButton.ghost(
icon: const Icon(Icons.arrow_back),
padding: const EdgeInsets.all(8),
onPressed: () {
Navigator.pop(context);
},
),
)
],
),
),
);
}
}
class MultiModeLogin extends StatelessWidget {
final formKey = GlobalKey<ShadFormState>();
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
body: Column(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Login",
style: ShadTheme.of(context).textTheme.h2,
),
ShadForm(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ShadInputFormField(
id: "email",
label: const Text("Email"),
autofillHints: const [AutofillHints.email],
placeholder: const Text("Enter your email"),
validator: (value) {
if (value.isEmpty) {
return "Please enter your email";
}
if (!value.contains("@")) {
return "Please enter a valid email";
}
return null;
},
),
ShadInputFormField(
id: "password",
label: const Text("Password"),
placeholder: const Text("Enter your password"),
autofillHints: const [AutofillHints.password],
obscureText: true,
validator: (value) {
if (value.isEmpty) {
return "Please enter your password";
}
if (value.length < 8) {
return "Password must be at least 8 characters long";
}
return null;
},
)
],
),
),
ShadButton(
text: const Text("Login"),
width: double.infinity,
onPressed: () async {
if (formKey.currentState!.validate()) {
formKey.currentState!.save();
print("Logging in...");
LiveInformation liveInformation = LiveInformation();
await liveInformation.auth.createEmailSession(
email: formKey.currentState!.value["email"],
password: formKey.currentState!.value["password"]
);
print("Done something");
print(liveInformation.auth.status);
}
},
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Don't have an account?"),
ShadButton.link(
onPressed: () {
Navigator.pushNamed(context, "/multi/register");
},
text: const Text("Register"),
)
],
)
],
),
),
),
const Divider(
height: 1,
),
NavigationBar()
],
),
);
}
}
class MultiModeRegister extends StatelessWidget {
final formKey = GlobalKey<ShadFormState>();
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
body: Column(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.centerLeft,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Register",
style: ShadTheme.of(context).textTheme.h2,
),
ShadForm(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ShadInputFormField(
id: "email",
label: const Text("Email"),
autofillHints: const [AutofillHints.email],
placeholder: const Text("Enter your email"),
validator: (value) {
if (value.isEmpty) {
return "Please enter your email";
}
if (!value.contains("@")) {
return "Please enter a valid email";
}
return null;
},
),
ShadInputFormField(
id: "password",
label: const Text("Password"),
placeholder: const Text("Enter your password"),
autofillHints: const [AutofillHints.password],
obscureText: true,
validator: (value) {
if (value.isEmpty) {
return "Please enter your password";
}
if (value.length < 8) {
return "Password must be at least 8 characters long";
}
return null;
},
),
ShadInputFormField(
id: "confirmPassword",
label: const Text("Confirm password"),
placeholder: const Text("Re-enter your password"),
autofillHints: const [AutofillHints.password],
obscureText: true,
validator: (value) {
if (value.isEmpty) {
return "Please enter your password";
}
if (value.length < 8) {
return "Password must be at least 8 characters long";
}
if (value != formKey.currentState!.value["password"]) {
return "Passwords do not match";
}
return null;
},
)
],
),
),
ShadButton(
text: const Text("Register"),
width: double.infinity,
onPressed: () async {
if (formKey.currentState!.validate()) {
formKey.currentState!.save();
print("Logging in...");
LiveInformation liveInformation = LiveInformation();
await liveInformation.auth.createUser(
displayName: formKey.currentState!.value["email"],
username: formKey.currentState!.value["email"],
email: formKey.currentState!.value["email"],
password: formKey.currentState!.value["password"],
);
await liveInformation.auth.createEmailSession(
email: formKey.currentState!.value["email"],
password: formKey.currentState!.value["password"]
);
if (liveInformation.auth.status == AuthStatus.AUTHENTICATED) {
Navigator.pop(context);
Navigator.popAndPushNamed(context, "/multi");
} else {
ShadToaster.of(context).show(
const ShadToast(
title: Text("Failed to register"),
description: Text("Failed to register with the provided details"),
duration: Duration(seconds: 5),
)
);
}
}
},
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Have an account?"),
ShadButton.link(
onPressed: () {
Navigator.pushNamed(context, "/multi/login");
},
text: const Text("Login"),
)
],
)
],
),
),
),
),
const Divider(
height: 1,
),
NavigationBar()
],
),
);
}
}
class NavigationBar extends StatefulWidget {
final Widget? content;
NavigationBar({this.content = null});
@override
State<NavigationBar> createState() => _NavigationBarState();
}
class _NavigationBarState extends State<NavigationBar> {
@override
Widget build(BuildContext context) {
// Is the on screen keyboard visible?
bool isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
if (isKeyboardVisible) {
return Container();
}
return Row(
children: [
ShadButton.ghost(
onPressed: () {
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back),
text: const Text("Back"),
padding: const EdgeInsets.all(8),
),
],
);
}
}
List<Widget> _getNearbyRoutes({bool multiMode = false}) {
print("Getting nearby routes");
LiveInformation liveInformation = LiveInformation();
BusSequences busSequences = liveInformation.busSequences;
List<BusRoute> nearbyRoutes = [];
Position? currentLocation = liveInformation.trackerModule.position;
Vector2 currentVector = Vector2(0, 0);
if (currentLocation == null && !kDebugMode) {
return [];
} else if (currentLocation != null){
currentVector = OSGrid.toNorthingEasting(currentLocation!.latitude, currentLocation.longitude);
}
if (kDebugMode) {
currentVector = OSGrid.toNorthingEasting(51.583781262560926, -0.020359583104595073);
}
for (BusRoute route in busSequences.routes.values) {
for (BusRouteVariant variant in route.routeVariants.values) {
for (BusRouteStop stop in variant.busStops) {
Vector2 stopVector = Vector2(stop.easting.toDouble(), stop.northing.toDouble());
double distance = currentVector.distanceTo(stopVector);
if (distance < 1000) {
nearbyRoutes.add(route);
break;
}
}
if (nearbyRoutes.contains(route)) {
break;
}
}
if (nearbyRoutes.contains(route)) {
continue;
}
}
List<Widget> routeCards = [];
for (BusRoute route in nearbyRoutes) {
routeCards.add(RouteCard(route: route, multiMode: multiMode));
}
return routeCards;
}