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 createState() => _HomePage_ReState(); } class _HomePage_ReState extends State { Future _shouldRedirectToSetup() async { List 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"), ) ], ), ), if (false) SizedBox( width: 16, ), if (false) 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( height: 16, ), ShadButton( text: Text("Re-setup"), onPressed: () { Navigator.popAndPushNamed(context, "/setup"); }, ) ], ), ), ); // 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 createState() => _RouteSearchState(); } class _RouteSearchState extends State { TextEditingController controller = TextEditingController(); @override Widget build(BuildContext context) { List 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 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 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 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 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( 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 createState() => _MultiModeSetupState(); } class _MultiModeSetupState extends State { @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 createState() => _MultiModeEnrouteState(); } class _MultiModeEnrouteState extends State { late final Future roomCodeFuture; late ListenerReceipt 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 createState() => _MultiModeJoinState(); } class _MultiModeJoinState extends State { TextEditingController controller = TextEditingController(); @override Widget build(BuildContext context) { return Scaffold( body: Column( children: [ Expanded( child: Container( alignment: Alignment.center, padding: EdgeInsets.all(16), width: double.infinity, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, 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(); if (controller.text.isNotEmpty ? await liveInformation.joinRoom(controller.text) : false) { liveInformation.setRouteVariant(null); Navigator.popAndPushNamed(context, "/multi/enroute"); } else { showShadDialog( context: context, builder: (context) { return ShadDialog( title: const Text("Failed to join group"), content: const Text("Failed to join group. Please check the room code and try again"), actions: [ ShadButton( text: const Text("Close"), onPressed: () { Navigator.pop(context); }, ) ], ); } ); } }, ) ], ), ), 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"); } } ) // todo: This is the new code, couldnt finish it on time so still using the old code /*Text( "Join a group", style: ShadTheme.of(context).textTheme.h2, ), SizedBox( height: 16, ), Row( children: [ Container( width: 150, height: 150, child: ElevatedButton( onPressed: () { }, // border radius style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30) ) ), child: Text( "Join from QR Code", style: ShadTheme.of(context).textTheme.h4, textAlign: TextAlign.center, ), ), ), Container( width: 150, height: 150, child: ElevatedButton( onPressed: () { }, // border radius style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30) ) ), child: Text( "Join from Clipboard", style: ShadTheme.of(context).textTheme.h4, textAlign: TextAlign.center, ), ), ) ], )*/ ] ), ), ), Divider( height: 1 ), NavigationBar() ], ), ); } } class FullscreenDisplay extends StatefulWidget { @override State createState() => _FullscreenDisplayState(); } class _FullscreenDisplayState extends State { @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(); @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(); @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 createState() => _NavigationBarState(); } class _NavigationBarState extends State { @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 _getNearbyRoutes({bool multiMode = false}) { print("Getting nearby routes"); LiveInformation liveInformation = LiveInformation(); BusSequences busSequences = liveInformation.busSequences; List 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 routeCards = []; for (BusRoute route in nearbyRoutes) { routeCards.add(RouteCard(route: route, multiMode: multiMode)); } return routeCards; }