From ffde62a7306b2b9499dd0e2e87de4d768a0b9551 Mon Sep 17 00:00:00 2001 From: ImBenji <53883070+YesItsBenji@users.noreply.github.com> Date: Fri, 24 May 2024 20:17:02 +0100 Subject: [PATCH] All dem changes --- android/app/build.gradle | 2 +- lib/backend/live_information.dart | 100 +++++- lib/backend/modules/announcement.dart | 5 +- lib/backend/modules/directconnection.dart | 123 ++++++++ lib/backend/modules/networking.dart | 43 ++- lib/remaster/DashboardArc.dart | 178 +++++++++-- lib/remaster/InitialStartup.dart | 351 +++++++++++++--------- lib/remaster/JoinGroup.dart | 320 ++++++++++++++++++++ lib/remaster/RemasteredMain.dart | 6 +- lib/remaster/dashboard.dart | 28 +- lib/tfl_datasets.dart | 3 +- pubspec.lock | 24 ++ pubspec.yaml | 2 + 13 files changed, 990 insertions(+), 195 deletions(-) create mode 100644 lib/backend/modules/directconnection.dart create mode 100644 lib/remaster/JoinGroup.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 5821719..43b9f34 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -37,7 +37,7 @@ android { applicationId "com.imbenji.bus_infotainment" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion 21 + minSdkVersion 24 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/lib/backend/live_information.dart b/lib/backend/live_information.dart index 22e7f42..cc746fd 100644 --- a/lib/backend/live_information.dart +++ b/lib/backend/live_information.dart @@ -10,6 +10,7 @@ import 'package:bus_infotainment/auth/api_constants.dart'; import 'package:bus_infotainment/auth/auth_api.dart'; import 'package:bus_infotainment/backend/modules/announcement.dart'; import 'package:bus_infotainment/backend/modules/commands.dart'; +import 'package:bus_infotainment/backend/modules/directconnection.dart'; import 'package:bus_infotainment/backend/modules/networking.dart'; import 'package:bus_infotainment/backend/modules/synced_time.dart'; import 'package:bus_infotainment/backend/modules/tracker.dart'; @@ -24,6 +25,15 @@ import 'package:http/http.dart' as http; import 'package:ntp/ntp.dart'; import 'package:permission_handler/permission_handler.dart'; +enum RoomConnectionMethod { + Cloud, + Local, + P2P, + None +} + + + class LiveInformation { static final LiveInformation _singleton = LiveInformation._internal(); @@ -39,11 +49,42 @@ class LiveInformation { { // By default, load the bus sequences from the assets print("Loading bus sequences from assets"); + + String destinations = await rootBundle.loadString("assets/datasets/destinations.json"); + String routes = await rootBundle.loadString("assets/datasets/bus-sequences.csv"); + + // Try to grab the routes from TfL + try { + + http.Response response = await http.get(Uri.parse('https://tfl.gov.uk/bus-sequences.csv')); + + routes = response.body; + + print("Loaded bus sequences from TFL"); + + } catch (e) { + print("Failed to load bus sequences from TFL. Using local copy."); + } + + // Try to grab the destinations from github + try { + + http.Response response = await http.get(Uri.parse('https://raw.githubusercontent.com/RailboundStudios/LondonBusDatasets/main/destinations.json')); + + destinations = response.body; + + print("Loaded destinations from Github"); + + } catch (e) { + print("Failed to load destinations from Github. Using local copy."); + } + busSequences = BusSequences.fromCSV( - await rootBundle.loadString("assets/datasets/destinations.json"), - await rootBundle.loadString("assets/datasets/bus-sequences.csv") + destinations, + routes ); - print("Loaded bus sequences from assets"); + + print("Loaded all datasets"); try { @@ -78,6 +119,11 @@ class LiveInformation { } networkingModule = NetworkingModule(); + + if (defaultTargetPlatform == TargetPlatform.android){ + p2pModule = NearbyServiceWrapper(); + } + } Future initTrackerModule() async { @@ -86,6 +132,9 @@ class LiveInformation { } } + // Multi-device stuff + RoomConnectionMethod connectionMethod = RoomConnectionMethod.None; + // Auth AuthAPI auth = AuthAPI( autoLoad: false, @@ -108,6 +157,7 @@ class LiveInformation { late TrackerModule trackerModule; late TubeStations tubeStations; late NetworkingModule networkingModule; + late NearbyServiceWrapper p2pModule; // Important variables BusRouteVariant? _currentRouteVariant; @@ -234,7 +284,7 @@ class LiveInformation { _listenerReciept = networkingModule.onMessageReceived?.addListener( (p0) { print("Received local command: $p0"); - ExecuteCommand(p0); + executeCommand(p0); } ); } @@ -311,7 +361,9 @@ class LiveInformation { } - Future joinRoom(String infoJson) async { + Future joinRoom(String info) async { + + String infoJson = utf8.decode(base64.decode(info)); try { { @@ -338,11 +390,12 @@ class LiveInformation { _listenerReciept = networkingModule.onMessageReceived?.addListener( (p0) { print("Received local command: $p0"); - ExecuteCommand(p0); + executeCommand(p0); } ); inRoom = true; - return; // We dont need to connect to the cloud room if we are connected to the local room. + connectionMethod = RoomConnectionMethod.Local; + return true; // We dont need to connect to the cloud room if we are connected to the local room. } else { print("Failed to connect to local room at $host"); print("Falling back to cloud room"); @@ -376,7 +429,7 @@ class LiveInformation { ); if (response.documents.isEmpty) { - throw Exception("Room not found"); + return false; } final document = response.documents.first; @@ -417,7 +470,9 @@ class LiveInformation { print("Failed to set route"); } inRoom = true; + connectionMethod = RoomConnectionMethod.Cloud; print("Joined cloud room with code $roomCode"); + return true; } } @@ -498,7 +553,7 @@ class LiveInformation { } */ - return jsonEncode({ + String json = jsonEncode({ "cloud": { "roomCode": roomCode, }, @@ -512,6 +567,10 @@ class LiveInformation { } }); + // Encode in base64 + + return base64Encode(utf8.encode(json)); + } String? lastCommand; @@ -542,7 +601,9 @@ class LiveInformation { // If the route arent the same, then update the route if (routeNumber != _currentRouteVariant!.busRoute.routeNumber || routeVariantIndex != _currentRouteVariant!.busRoute.routeVariants.values.toList().indexOf(_currentRouteVariant!)) { // Set the route - await setRouteVariantQuery(routeNumber, routeVariantIndex); + await setRouteVariantQuery(routeNumber, routeVariantIndex, + sendToServer: false + ); // announce the route // announcementModule.queueAnnouncementByRouteVariant(routeVariant: _currentRouteVariant!); @@ -554,14 +615,17 @@ class LiveInformation { // Execute the command List commands = response.payload["Commands"].cast(); - ExecuteCommand(commands.last); + executeCommand(commands.last); } - void ExecuteCommand(String command) { + void executeCommand(String command) { if (command == lastCommand) { return; } + + print("Executing command: $command"); + lastCommand = command; List commandParts = _splitCommand(command); @@ -636,10 +700,20 @@ class LiveInformation { Future SendCommand(String command) async { + { + // Wfi Direct Commands + + p2pModule.sendMessage(command); + } + { // Local Commands - networkingModule.sendMessage(command); + try { + networkingModule.sendMessage(command); + } catch (e) { + print("Failed to send local command: $e"); + } } diff --git a/lib/backend/modules/announcement.dart b/lib/backend/modules/announcement.dart index 53a580e..c2928d3 100644 --- a/lib/backend/modules/announcement.dart +++ b/lib/backend/modules/announcement.dart @@ -111,7 +111,7 @@ class AnnouncementModule extends InfoModule { } else { if (queue.isNotEmpty) { - await Future.delayed(const Duration(seconds: 5)); + // await Future.delayed(const Duration(seconds: 2)); } } @@ -156,7 +156,7 @@ class AnnouncementModule extends InfoModule { // Configuration Duration get defaultAnnouncementDelay { if (liveInformation.inRoom) { - return Duration(milliseconds: 500); + return Duration(milliseconds: 1000); } else { print("Not in room"); return Duration.zero; @@ -182,7 +182,6 @@ class AnnouncementModule extends InfoModule { for (var audioName in audioNames) { audioNamesString += "\"$audioName\" "; } - liveInformation.SendCommand("announce manual \"$displayText\" $audioNamesString ${scheduledTime.millisecondsSinceEpoch}"); queueAnnounceByAudioName( displayText: displayText, diff --git a/lib/backend/modules/directconnection.dart b/lib/backend/modules/directconnection.dart new file mode 100644 index 0000000..ece64c0 --- /dev/null +++ b/lib/backend/modules/directconnection.dart @@ -0,0 +1,123 @@ +import 'dart:async'; +import 'package:bus_infotainment/backend/live_information.dart'; +import 'package:flutter/foundation.dart'; +import 'package:nearby_service/nearby_service.dart'; +import 'package:nearby_service/src/model/model.dart'; + +class NearbyServiceWrapper { + late NearbyService _nearbyService; + final ValueNotifier isConnected = ValueNotifier(false); + final ValueNotifier connectedDevice = ValueNotifier(null); + + NearbyServiceWrapper() { + _nearbyService = NearbyService.getInstance(); + _initializeService(); + } + + Future _initializeService() async { + await _nearbyService.initialize(); + _listenForIncomingConnections(); + await startDiscovery(); + } + + Future startDiscovery() async { + await _nearbyService.discover(); + } + + Future stopDiscovery() async { + await _nearbyService.stopDiscovery(); + } + + Future> getDiscoveredDevices() async { + + List devices = await _nearbyService.getPeers(); + + // Remove devices that we should avoid + devices.removeWhere((element) => _deviceNamesToAvoid.any((avoid) => element.info.displayName.contains(avoid))); + + return devices; + } + + Future connectToDevice(NearbyDevice device) async { + bool success = await _nearbyService.connect(device); + if (success) { + connectedDevice.value = device; + isConnected.value = true; + _startCommunicationChannel(); + } + } + + Future _startCommunicationChannel() async { + await _nearbyService.startCommunicationChannel( + NearbyCommunicationChannelData( + connectedDevice.value!.info.id, + messagesListener: NearbyServiceMessagesListener( + onCreated: () { + print('Communication channel created'); + }, + onData: (ReceivedNearbyMessage value) { + if (value.content is NearbyMessageTextRequest) { + + String message = (value.content as NearbyMessageTextRequest).value; + + print('Received message: ${message}'); + LiveInformation().executeCommand(message); + } + } + ) + ) + ); + } + + Future sendMessage(String message) async { + if (isConnected.value && connectedDevice.value != null) { + await _nearbyService.send( + OutgoingNearbyMessage( + content: NearbyMessageTextRequest.create(value: message), + receiver: connectedDevice.value!.info, + ) + ); + } + } + + Future disconnect() async { + if (isConnected.value && connectedDevice.value != null) { + await _nearbyService.disconnect(connectedDevice.value); + connectedDevice.value = null; + isConnected.value = false; + } + } + + void _listenForIncomingConnections() { + _nearbyService.getPeersStream().listen((peers) async { + for (var peer in peers) { + + // Lets avoid accidentally connecting to things + if (_deviceNamesToAvoid.any((element) => peer.info.displayName.contains(element))) { + print('Avoiding device: ${peer.info.displayName}'); + continue; + } + + if (!isConnected.value) { + await connectToDevice(peer); + print('Reconnected to: ${peer.info.displayName}'); + } + } + }); + + _nearbyService.getConnectedDeviceStream(connectedDevice.value!).listen((device) { + if (device == null) { + isConnected.value = false; + connectedDevice.value = null; + } + }); + } + + ValueListenable get connectedDeviceNotifier => connectedDevice; + ValueListenable get isConnectedNotifier => isConnected; +} + +// If a device name contains any of these strings, it will be avoided +List _deviceNamesToAvoid = [ + "DIRECT-", // Avoid connecting to printers +]; \ No newline at end of file diff --git a/lib/backend/modules/networking.dart b/lib/backend/modules/networking.dart index 7512dd4..67dd28f 100644 --- a/lib/backend/modules/networking.dart +++ b/lib/backend/modules/networking.dart @@ -9,11 +9,16 @@ import 'package:shelf_web_socket/shelf_web_socket.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:bus_infotainment/backend/modules/info_module.dart'; import 'package:bus_infotainment/utils/delegates.dart'; +import 'package:http/http.dart' as http; class NetworkingModule extends InfoModule { // Host websocket server - String host = "ws://0.0.0.0:8080"; - HttpServer? _server; + + int webSocketPort = 8080; + int httpPort = 8081; + + HttpServer? _sockerServer; + HttpServer? _httpServer; WebSocketChannel? _channel; // Store connected WebSocket channels @@ -50,8 +55,13 @@ class NetworkingModule extends InfoModule { }); }); - _server = await io.serve(handler, InternetAddress.anyIPv4, 8080); - print('WebSocket server started at ${_server?.address.address}:${_server?.port}'); + _sockerServer = await io.serve(handler, InternetAddress.anyIPv4, webSocketPort); + print('WebSocket server started at ${_sockerServer?.address.address}:${_sockerServer?.port}'); + + // Start Http api server + _httpServer = await io.serve((Request request) async { + return Response.ok('bus infotainment server'); + }, InternetAddress.anyIPv4, httpPort); return true; } catch (e) { @@ -61,7 +71,7 @@ class NetworkingModule extends InfoModule { } bool stopWebSocketServer() { - if (_server == null) { + if (_sockerServer == null) { throw Exception('WebSocket server is not running'); } @@ -70,8 +80,12 @@ class NetworkingModule extends InfoModule { client.sink.close(); } _connectedClients.clear(); - _server?.close(force: true); - _server = null; + _sockerServer?.close(force: true); + _sockerServer = null; + + _httpServer?.close(force: true); + _httpServer = null; + print('WebSocket server stopped'); return true; } catch (e) { @@ -82,6 +96,17 @@ class NetworkingModule extends InfoModule { Future connectToWebSocketServer(String url) async { try { + + { + // Verify that the server we are connecting to is running, and is a bus infotainment server + var response = await http.get(Uri.parse('http://$url:$httpPort')); + + if (response.statusCode != 200 || response.body != 'bus infotainment server') { + print('Server at $url is not a bus infotainment server'); + return false; + } + } + _channel = await WebSocketChannel.connect(Uri.parse(url)); _channel?.stream.listen((message) { // Handle messages from the server here @@ -116,7 +141,7 @@ class NetworkingModule extends InfoModule { bool sendMessage(String message) { // If hosting a server, send message to all clients - if (_server != null) { + if (_sockerServer != null) { return sendMessageToClients(message); } @@ -193,4 +218,4 @@ class NetworkingModule extends InfoModule { } } } -} +} \ No newline at end of file diff --git a/lib/remaster/DashboardArc.dart b/lib/remaster/DashboardArc.dart index dbd471f..d5ea7f2 100644 --- a/lib/remaster/DashboardArc.dart +++ b/lib/remaster/DashboardArc.dart @@ -17,6 +17,19 @@ class ArcDashboard extends StatefulWidget { State createState() => _ArcDashboardState(); } +String _rmconString(RoomConnectionMethod method) { + switch (method) { + case RoomConnectionMethod.Cloud: + return "Cloud"; + case RoomConnectionMethod.Local: + return "Local"; + case RoomConnectionMethod.P2P: + return "P2P"; + case RoomConnectionMethod.None: + return "None"; + } +} + class _ArcDashboardState extends State { _closeDialogueChecker closeDialogWidget = _closeDialogueChecker(); @@ -91,6 +104,8 @@ class _ArcDashboardState extends State { onPressed: () { bool multiMode = ModalRoute.of(context)!.settings.name!.contains("multi"); + LiveInformation().p2pModule.startDiscovery(); + showShadSheet( context: context, side: ShadSheetSide.left, @@ -120,28 +135,149 @@ class _ArcDashboardState extends State { builder: (context) { return ShadSheet( padding: const EdgeInsets.all(10), - content: Column( - children: [ - Text("Room ID: ${LiveInformation().roomDocumentID}"), - Text("IP Address: ${LiveInformation().networkingModule.localIP}"), - QrImageView( - data: LiveInformation().generateRoomInfo(), - size: 270, - backgroundColor: Colors.white, - ), - ShadButton( - text: Text("Copy Room Info"), - onPressed: () { - Clipboard.setData(ClipboardData(text: LiveInformation().generateRoomInfo())); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Copied room info to clipboard"), - ) - ); - }, - ) - ], + content: Container( + width: 400, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Room Information", + style: ShadTheme.of(context).textTheme.h2 + ), + + SizedBox( + height: 8, + ), + + if (LiveInformation().isHost) + Container( + decoration: BoxDecoration( + color: Colors.amber, + borderRadius: BorderRadius.circular(10) + ), + width: double.infinity, + padding: EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Hosting room", + style: ShadTheme.of(context).textTheme.h4.copyWith( + height: 0.9 + ) + ), + ], + ) + ) + else + Container( + decoration: BoxDecoration( + color: Colors.amber, + borderRadius: BorderRadius.circular(10) + ), + width: double.infinity, + padding: EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Connected to room via:", + style: ShadTheme.of(context).textTheme.h4.copyWith( + height: 0.9 + ) + ), + SizedBox( + height: 4, + ), + Text( + _rmconString(LiveInformation().connectionMethod) + " connection", + style: ShadTheme.of(context).textTheme.p.copyWith( + height: 0.9 + ) + ), + ], + ) + ), + + SizedBox( + height: 8, + ), + + ShadButton( + text: Text("Show room QR Code"), + borderRadius: BorderRadius.all(Radius.circular(10)), + onPressed: () { + + showShadDialog( + context: context, + builder: (context) { + return ShadDialog( + + title: Text("Room QR Code"), + content: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: QrImageView( + data: LiveInformation().generateRoomInfo(), + size: 200, + backgroundColor: Colors.white, + ), + ), + SizedBox( + width: 10, + ), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + ShadButton( + text: Text("Copy Room Info"), + borderRadius: BorderRadius.all(Radius.circular(10)), + onPressed: () { + Clipboard.setData(ClipboardData(text: LiveInformation().generateRoomInfo())); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Copied room info to clipboard"), + ) + ); + }, + ), + ], + ), + ) + ], + ), + ); + } + ); + + }, + ), + + ShadButton( + text: Text("Make nearby discoverable"), + onPressed: () { + LiveInformation().p2pModule.startDiscovery(); + ShadToaster.of(context).show( + ShadToast( + title: Text("Discoverable"), + description: Text("If it wasnt before, your device is now discoverable to nearby devices"), + duration: const Duration(seconds: 3), + ) + ); + }, + ) + + ], + + ), ), ); } diff --git a/lib/remaster/InitialStartup.dart b/lib/remaster/InitialStartup.dart index e425665..264221c 100644 --- a/lib/remaster/InitialStartup.dart +++ b/lib/remaster/InitialStartup.dart @@ -9,7 +9,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -138,6 +137,12 @@ class _page2State extends State<_page2> { await Permission.location.isGranted ]); + if (defaultTargetPlatform == TargetPlatform.android) { + perms.add( + await Permission.nearbyWifiDevices.isGranted + ); + } + return !perms.contains(false); } @@ -154,8 +159,6 @@ class _page2State extends State<_page2> { child: SizedBox( - // width: double.infinity, - child: Column( mainAxisSize: MainAxisSize.min, @@ -174,172 +177,232 @@ class _page2State extends State<_page2> { height: 16, ), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( + Container( + height: 210, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( - // mainAxisSize: MainAxisSize.min, - // crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisSize: MainAxisSize.min, + // crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ShadCard( - width: 300, - height: 200, - title: Text( - "Location", - ), - description: Text( - "Your location is required for automatically updating your nearest bus stop." - ), - content: Container( - child: Column( - children: [ + children: [ + ShadCard( + width: 300, + height: double.infinity, + title: Text( + "Location", + ), + description: Text( + "Your location is required for automatically updating your nearest bus stop." + ), + content: Container( + child: Column( + children: [ - SizedBox( - height: 4, - ), + SizedBox( + height: 4, + ), - FutureBuilder( - future: Permission.location.isGranted, - builder: (context, val) { - bool isEnabled = true; - String text = "Request permission"; - Color color = Colors.white; + FutureBuilder( + future: Permission.location.isGranted, + builder: (context, val) { + bool isEnabled = true; + String text = "Request permission"; + Color color = Colors.white; - if (val.hasData) { - isEnabled = !val.data!; - } - if (!isEnabled) { - text = "Permission granted!"; - color = Colors.green.shade400; - } + if (val.hasData) { + isEnabled = !val.data!; + } + if (!isEnabled) { + text = "Permission granted!"; + color = Colors.green.shade400; + } - return ShadButton( - text: Text(text), - onPressed: () async { - await Permission.location.request(); - setState(() { + return ShadButton( + text: Text(text), + onPressed: () async { + await Permission.location.request(); + setState(() { - }); - }, - enabled: isEnabled, - backgroundColor: color, - ); - }, - ), - ], + }); + }, + enabled: isEnabled, + backgroundColor: color, + ); + }, + ), + ], + ), ), ), - ), - - SizedBox( - width: 16, - ), - - ShadCard( - width: 300, - height: 200, - title: Text( - "Storage", + if (!kIsWeb) + SizedBox( + width: 16, ), - description: Text( - "Storage access is required to access recorded announcements." - ), - content: Container( - child: Column( - children: [ - SizedBox( - height: 4, - ), + if (!kIsWeb) + ShadCard( + width: 300, + height: double.infinity, + title: Text( + "Storage", + ), + description: Text( + "Storage access is required to access recorded announcements." + ), + content: Container( + child: Column( + children: [ - FutureBuilder( - future: Permission.manageExternalStorage.isGranted, - builder: (context, val) { - bool isEnabled = true; - String text = "Request permission"; - Color color = Colors.white; + SizedBox( + height: 4, + ), - if (val.hasData) { - isEnabled = !val.data!; - } - if (!isEnabled) { - text = "Permission granted!"; - color = Colors.green.shade400; - } + FutureBuilder( + future: Permission.manageExternalStorage.isGranted, + builder: (context, val) { + bool isEnabled = true; + String text = "Request permission"; + Color color = Colors.white; - return ShadButton( - text: Text(text), - onPressed: () async { - await Permission.manageExternalStorage.request(); - setState(() { + if (val.hasData) { + isEnabled = !val.data!; + } + if (!isEnabled) { + text = "Permission granted!"; + color = Colors.green.shade400; + } - }); - }, - enabled: isEnabled, - backgroundColor: color, - ); - }, - ) - ], + return ShadButton( + text: Text(text), + onPressed: () async { + await Permission.manageExternalStorage.request(); + setState(() { + + }); + }, + enabled: isEnabled, + backgroundColor: color, + ); + }, + ) + ], + ), ), ), - ), - /*SizedBox( - width: 16, - ), - - ShadCard( - width: 300, - height: 200, - title: Text( - "Network", + if (defaultTargetPlatform == TargetPlatform.android) + SizedBox( + width: 16, ), - description: Text( - "Network access is required for commincation between devices for multi mode." - ), - content: Container( - child: Column( - children: [ - SizedBox( - height: 4, - ), + if (defaultTargetPlatform == TargetPlatform.android) + ShadCard( + width: 300, + height: double.infinity, + title: Text( + "Nearby Devices", + ), + description: Text( + "Nearby Devices access is required to find nearby devices, and to establish connections with them." + ), + content: Container( + child: Column( + children: [ - FutureBuilder( - future: Permission.nearbyWifiDevices.isGranted, - builder: (context, val) { - bool isEnabled = true; - String text = "Request permission"; - Color color = Colors.white; + SizedBox( + height: 4, + ), - if (val.hasData) { - isEnabled = !val.data!; - } - if (!isEnabled) { - text = "Permission granted!"; - color = Colors.green.shade400; - } + FutureBuilder( + future: Permission.nearbyWifiDevices.isGranted, + builder: (context, val) { + bool isEnabled = true; + String text = "Request permission"; + Color color = Colors.white; - return ShadButton( - text: Text(text), - onPressed: () async { - await Permission.manageExternalStorage.request(); - setState(() { + if (val.hasData) { + isEnabled = !val.data!; + } + if (!isEnabled) { + text = "Permission granted!"; + color = Colors.green.shade400; + } - }); - }, - enabled: isEnabled, - backgroundColor: color, - ); - }, - ) - ], + return ShadButton( + text: Text(text), + onPressed: () async { + await Permission.nearbyWifiDevices.request(); + setState(() { + + }); + }, + enabled: isEnabled, + backgroundColor: color, + ); + }, + ) + ], + ), ), ), - ),*/ - ], + + /*SizedBox( + width: 16, + ), + + ShadCard( + width: 300, + height: 200, + title: Text( + "Network", + ), + description: Text( + "Network access is required for commincation between devices for multi mode." + ), + content: Container( + child: Column( + children: [ + + SizedBox( + height: 4, + ), + + FutureBuilder( + future: Permission.nearbyWifiDevices.isGranted, + builder: (context, val) { + bool isEnabled = true; + String text = "Request permission"; + Color color = Colors.white; + + if (val.hasData) { + isEnabled = !val.data!; + } + if (!isEnabled) { + text = "Permission granted!"; + color = Colors.green.shade400; + } + + return ShadButton( + text: Text(text), + onPressed: () async { + await Permission.manageExternalStorage.request(); + setState(() { + + }); + }, + enabled: isEnabled, + backgroundColor: color, + ); + }, + ) + ], + ), + ), + ),*/ + ], + ), ), ), @@ -388,6 +451,8 @@ class _page3 extends InitialStartupPage { State<_page3> createState() => _page3State(); } + + class _page3State extends State<_page3> { bool _loadingAudio = false; diff --git a/lib/remaster/JoinGroup.dart b/lib/remaster/JoinGroup.dart new file mode 100644 index 0000000..fc967c8 --- /dev/null +++ b/lib/remaster/JoinGroup.dart @@ -0,0 +1,320 @@ + +import 'dart:convert'; + +import 'package:bus_infotainment/backend/live_information.dart'; +import 'package:bus_infotainment/remaster/dashboard.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' hide NavigationBar; +import 'package:native_qr/native_qr.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +class JoinGroup extends StatefulWidget { + + @override + State createState() => _JoinGroupState(); +} + +class _JoinGroupState extends State { + + Future _joinGroup(String data) async { + + if (data.isEmpty) { + ShadToaster.of(context).show( + ShadToast( + title: Text("Error connecting to room"), + description: Text("Nothing was found."), + ) + ); + } + + if (await LiveInformation().joinRoom(data)) { + Navigator.pushNamed(context, "/multi/enroute"); + } else { + ShadToaster.of(context).show( + ShadToast( + title: Text("Error connecting to room"), + description: Text("The room could not be found."), + ) + ); + + } + + } + + @override + Widget build(BuildContext context) { + + if (defaultTargetPlatform == TargetPlatform.android) { + LiveInformation().p2pModule.startDiscovery(); + } + + return Scaffold( + body: Container( + + child: Row( + + children: [ + + Expanded( + + child: Container( + + + + alignment: Alignment.center, + + child: Row( + + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + + children: [ + + Container( + margin: EdgeInsets.all(20), + width: 100, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + + Expanded( + + child: RotatedBox( + quarterTurns: 3, + child: Container( + height: double.infinity, + child: ElevatedButton( + onPressed: () async { + NativeQr nativeQr = NativeQr(); + String? result = await nativeQr.get(); + + _joinGroup(result!); + }, + child: Text( + "Join from QR code", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10) + ), + ) + + ), + ), + ), + ), + + SizedBox( + height: 20, + ), + + Expanded( + + child: RotatedBox( + quarterTurns: 3, + child: Container( + height: double.infinity, + child: ElevatedButton( + onPressed: () { + + }, + child: Text( + "Join from clipboard", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10) + ), + ) + + ), + ), + ), + ) + + ], + + ), + ), + + Container( + width: 2, + color: Colors.grey.shade300, + ), + + Expanded( + child: Stack( + children: [ + Positioned.fill( + child: Container( + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "EXPERIMENTAL", + style: TextStyle( + color: Colors.grey.shade700.withOpacity(0.9), + fontSize: 100, + fontWeight: FontWeight.bold, + height: 1 + ), + ), + Text( + "Working proof of concept - Rewrite iminent", + style: TextStyle( + color: Colors.grey.shade700.withOpacity(0.9), + fontSize: 20, + fontWeight: FontWeight.bold + ), + ), + Text( + "Certain parts may not work as expected. I am aware of all issues.", + style: TextStyle( + color: Colors.grey.shade700.withOpacity(0.9), + fontSize: 20, + fontWeight: FontWeight.bold + ), + ) + ], + ), + ), + ), + Container( + margin: EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Row( + + children: [ + + Text( + "Nearby devices", + style: ShadTheme.of(context).textTheme.h1 + ), + + SizedBox( + width: 20, + ), + + ElevatedButton( + onPressed: () { + setState(() {}); + }, + child: Text("Refresh"), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10) + ), + ) + ) + + ], + + ), + + if (defaultTargetPlatform == TargetPlatform.android) + FutureBuilder( + future: LiveInformation().p2pModule.getDiscoveredDevices(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return CircularProgressIndicator(); + } + if (snapshot.hasError) { + return Text("Error: ${snapshot.error}"); + } + + List peers = []; + + for (var peer in snapshot.data!) { + + print("Info: "); + print(jsonEncode(peer.info.toJson())); + + peers.add( + ElevatedButton( + onPressed: () async { + await LiveInformation().p2pModule.connectToDevice(peer); + LiveInformation().inRoom = true; + LiveInformation().connectionMethod = RoomConnectionMethod.P2P; + Navigator.pushNamed(context, "/multi/enroute"); + }, + child: Text(peer.info.displayName), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10) + ), + ) + ) + ); + } + + print(peers.length); + + return Column( + children: [ + ...peers + ], + ); + }, + ) + else + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("This feature is not available on this platform."), + Text("Please use the QR code or clipboard method.") + ], + ) + ), + ) + + ], + ), + ), + + ], + ), + ) + + ], + + ), + + ), + + ), + + Container( + width: 2, + color: Colors.grey.shade300, + ), + + RotatedBox( + quarterTurns: 3, + child: NavigationBar() + ) + + ], + + ), + + ), + ); + } +} \ No newline at end of file diff --git a/lib/remaster/RemasteredMain.dart b/lib/remaster/RemasteredMain.dart index 75eeb75..4d3eadb 100644 --- a/lib/remaster/RemasteredMain.dart +++ b/lib/remaster/RemasteredMain.dart @@ -4,12 +4,14 @@ import 'package:bus_infotainment/pages/tfl_dataset_test.dart'; import 'package:bus_infotainment/remaster/DashboardArc.dart'; import 'package:bus_infotainment/remaster/InitialStartup.dart'; +import 'package:bus_infotainment/remaster/JoinGroup.dart'; import 'package:bus_infotainment/remaster/SearchArc.dart'; import 'package:bus_infotainment/remaster/dashboard.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:keep_screen_on/keep_screen_on.dart'; import 'WebSocketTest.dart'; @@ -36,6 +38,8 @@ class RemasteredApp extends StatelessWidget { SystemUiOverlay.top, ]); + // Stop the screen from turning off + KeepScreenOn.turnOn(); return ShadApp( @@ -68,7 +72,7 @@ class RemasteredApp extends StatelessWidget { '/multi/login': (context) => MultiModeLogin(), '/multi/register': (context) => MultiModeRegister(), '/display': (context) => FullscreenDisplay(), - '/multi/join': (context) => MultiModeJoin(), + '/multi/join': (context) => JoinGroup(), '/websocket': (context) => WebSocketWidget(), }, diff --git a/lib/remaster/dashboard.dart b/lib/remaster/dashboard.dart index 7c67c4d..774d3f2 100644 --- a/lib/remaster/dashboard.dart +++ b/lib/remaster/dashboard.dart @@ -1671,11 +1671,33 @@ class _MultiModeJoinState extends State { onPressed: () async { LiveInformation liveInformation = LiveInformation(); - liveInformation.setRouteVariant(null); - await liveInformation.joinRoom(controller.text); - Navigator.popAndPushNamed(context, "/multi/enroute"); + 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); + }, + ) + ], + ); + } + ); + + } + + }, ) diff --git a/lib/tfl_datasets.dart b/lib/tfl_datasets.dart index 8514540..922eef0 100644 --- a/lib/tfl_datasets.dart +++ b/lib/tfl_datasets.dart @@ -291,8 +291,9 @@ class BusDestination { Uint8List? audioBytesB = LiveInformation().announcementModule.announcementCache[audioNameB]; Uint8List? audioBytesC = LiveInformation().announcementModule.announcementCache[audioNameC]; - if (audioBytesA != null) return audioBytesA; + if (audioBytesB != null) return audioBytesB; + if (audioBytesA != null) return audioBytesA; if (audioBytesC != null) return audioBytesC; print("No audio bytes found for $name"); diff --git a/pubspec.lock b/pubspec.lock index 4e228f9..15c0928 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -501,6 +501,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1" + keep_screen_on: + dependency: "direct main" + description: + name: keep_screen_on + sha256: "374405358a3229b0e1041b6e390ff4c74e73fbd21075298042044e0c84b5574c" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + keep_screen_on_platform_interface: + dependency: transitive + description: + name: keep_screen_on_platform_interface + sha256: "065a0811407a970027c7530f9b8f36d11c89f36aab85b4b5acdacfe2cf3a8568" + url: "https://pub.dev" + source: hosted + version: "3.0.0" latlong2: dependency: transitive description: @@ -613,6 +629,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" + nearby_service: + dependency: "direct main" + description: + name: nearby_service + sha256: "3575ddf7d093f455a7c1196eb938b6f64bd67f5ab861db6a6cc55c75e256ec0f" + url: "https://pub.dev" + source: hosted + version: "0.0.8" network_info_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7f24537..618b7ff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,8 @@ dependencies: shelf_router: ^1.1.4 shelf_static: ^1.1.2 shelf_web_socket: ^2.0.0 + keep_screen_on: ^3.0.0 + nearby_service: ^0.0.8 # web_socket_channel: ^3.0.0 # The following adds the Cupertino Icons font to your application.