diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ed02135..b02eddf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -42,5 +42,6 @@ - + + diff --git a/lib/audio_cache.dart b/lib/audio_cache.dart index 450fb02..2bc0fa2 100644 --- a/lib/audio_cache.dart +++ b/lib/audio_cache.dart @@ -15,9 +15,8 @@ class AudioCache { Uint8List? operator [](String key) { // ignore case - key = key.toLowerCase(); for (var k in _audioCache.keys) { - if (k.toLowerCase() == key) { + if (k.toLowerCase() == key.toLowerCase()) { return _audioCache[k]; } } @@ -35,7 +34,7 @@ class AnnouncementCache extends AudioCache { // remove any announcements that are already loaded for (var announcement in announcements) { if (!_audioCache.containsKey(announcement.toLowerCase())) { - _announements.add(announcement); + _announements.add(announcement.toLowerCase()); } } @@ -53,7 +52,7 @@ class AnnouncementCache extends AudioCache { filename = filename.split("/").last; } - if (_announements.contains(filename)) { + if (_announements.contains(filename.toLowerCase())) { _audioCache[filename.toLowerCase()] = file.content; print("Loaded announcement: $filename"); } diff --git a/lib/backend/live_information.dart b/lib/backend/live_information.dart index e15dc45..22e7f42 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/networking.dart'; import 'package:bus_infotainment/backend/modules/synced_time.dart'; import 'package:bus_infotainment/backend/modules/tracker.dart'; import 'package:bus_infotainment/backend/modules/tube_info.dart'; @@ -75,6 +76,8 @@ class LiveInformation { if (!auth.isAuthenticated()) { auth.loadAnonymousUser(); } + + networkingModule = NetworkingModule(); } Future initTrackerModule() async { @@ -90,9 +93,12 @@ class LiveInformation { String? roomCode; String? roomDocumentID; bool isHost = false; + bool inRoom = false; appwrite.RealtimeSubscription? _subscription; RealtimeKeepAliveConnection? _keepAliveConnection; // This is a workaround for a bug in the appwrite SDK + // Local room stuff + ListenerReceipt? _listenerReciept; // Modules // late CommandModule commandModule; This needs to be deprecated @@ -101,6 +107,7 @@ class LiveInformation { late SyncedTimeModule syncedTimeModule; late TrackerModule trackerModule; late TubeStations tubeStations; + late NetworkingModule networkingModule; // Important variables BusRouteVariant? _currentRouteVariant; @@ -114,7 +121,7 @@ class LiveInformation { - Future setRouteVariant(BusRouteVariant? routeVariant) async { + Future setRouteVariant(BusRouteVariant? routeVariant, {bool sendToServer = false}) async { if (routeVariant == null) { _currentRouteVariant = null; @@ -160,6 +167,11 @@ class LiveInformation { print("Failed to update route on server"); } + } + if (inRoom && sendToServer) { + + SendCommand("setroute ${routeVariant.busRoute.routeNumber} ${routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant)}"); + } Continuation: @@ -200,12 +212,13 @@ class LiveInformation { return _currentRouteVariant; } - Future setRouteVariantQuery(String routeNumber, int routeVariantIndex) async { + Future setRouteVariantQuery(String routeNumber, int routeVariantIndex, {bool sendToServer = false}) async { BusRoute route = busSequences.routes[routeNumber]!; BusRouteVariant routeVariant = route.routeVariants.values.toList()[routeVariantIndex]; await setRouteVariant( - routeVariant + routeVariant, + sendToServer: sendToServer ); } @@ -213,174 +226,225 @@ class LiveInformation { // Multi device support Future createRoom(String roomCode) async { - print("Creating room with code $roomCode"); - // Update the room code - this.roomCode = roomCode; - - // Enable host mode - isHost = true; - - // Access the database - final client = auth.client; - final databases = appwrite.Databases(client); - - // Remove any existing documents - final existingDocuments = await databases.listDocuments( - databaseId: "6633e85400036415ab0f", - collectionId: "6633e85d0020f52f3771", - queries: [ - appwrite.Query.search("SessionID", roomCode) - ] - ); - for (var document in existingDocuments.documents) { - await databases.deleteDocument( - databaseId: "6633e85400036415ab0f", - collectionId: "6633e85d0020f52f3771", - documentId: document.$id + { + // Local Room + await networkingModule.startWebSocketServer(); + inRoom = true; + _listenerReciept = networkingModule.onMessageReceived?.addListener( + (p0) { + print("Received local command: $p0"); + ExecuteCommand(p0); + } ); } - // Create the document - final document = await databases.createDocument( - databaseId: "6633e85400036415ab0f", - collectionId: "6633e85d0020f52f3771", - documentId: appwrite.ID.unique(), - data: { - "SessionID": roomCode, - "LastUpdater": auth.userID, - } - ); + { + // Cloud Room + print("Creating room with code $roomCode"); - // Listen for changes - // { Breaks due to bug in appwrite - // final realtime = appwrite.Realtime(client); - // - // if (_subscription != null) { - // _subscription!.close(); - // } - // - // _subscription = realtime.subscribe( - // ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'] - // ); - // _subscription!.stream.listen(ServerListener); - // } - // Listen for changes - if (_keepAliveConnection != null) { - try { - _keepAliveConnection!.close(); - } catch (e) { - print("Failed to close connection... oh well"); + // Update the room code + this.roomCode = roomCode; + + // Enable host mode + isHost = true; + inRoom = true; + // Access the database + final client = auth.client; + final databases = appwrite.Databases(client); + + // Remove any existing documents + final existingDocuments = await databases.listDocuments( + databaseId: "6633e85400036415ab0f", + collectionId: "6633e85d0020f52f3771", + queries: [ + appwrite.Query.search("SessionID", roomCode) + ] + ); + for (var document in existingDocuments.documents) { + await databases.deleteDocument( + databaseId: "6633e85400036415ab0f", + collectionId: "6633e85d0020f52f3771", + documentId: document.$id + ); } + + // Create the document + final document = await databases.createDocument( + databaseId: "6633e85400036415ab0f", + collectionId: "6633e85d0020f52f3771", + documentId: appwrite.ID.unique(), + data: { + "SessionID": roomCode, + "LastUpdater": auth.userID, + } + ); + + // Listen for changes + if (_keepAliveConnection != null) { + try { + _keepAliveConnection!.close(); + } catch (e) { + print("Failed to close connection... oh well"); + } + } + + String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1"; + String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim(); + _keepAliveConnection = RealtimeKeepAliveConnection( + channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'], + onData: ServerListener, + domain: domain, + client: auth.client, + onError: (e) { + print("Workarround Error: $e"); + }, + ); + _keepAliveConnection!.initialize(); + + + // Update the room document ID + roomDocumentID = document.$id; + + print("Created room with code $roomCode"); } - String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1"; - String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim(); - _keepAliveConnection = RealtimeKeepAliveConnection( - channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'], - onData: ServerListener, - domain: domain, - client: auth.client, - onError: (e) { - print("Workarround Error: $e"); - }, - ); - _keepAliveConnection!.initialize(); - - - // Update the room document ID - roomDocumentID = document.$id; - - print("Created room with code $roomCode"); } - Future joinRoom(String roomCode) async { - print("Joining room with code $roomCode"); + Future joinRoom(String infoJson) async { - // Disable host mode - isHost = false; + try { + { + // sync + String routeNumber = jsonDecode(infoJson)["sync"]["route"]; + int routeVariantIndex = jsonDecode(infoJson)["sync"]["routeVariant"]; - // Update the room code - this.roomCode = roomCode; + setRouteVariantQuery(routeNumber, routeVariantIndex); - // Access the database - final client = auth.client; - final databases = appwrite.Databases(client); - - // Get the document - final response = await databases.listDocuments( - databaseId: "6633e85400036415ab0f", - collectionId: "6633e85d0020f52f3771", - queries: [ - appwrite.Query.search("SessionID", roomCode) - ] - ); - - if (response.documents.isEmpty) { - throw Exception("Room not found"); + LiveInformation().announcementModule.queueAnnouncementByRouteVariant(routeVariant: _currentRouteVariant!, sendToServer: false); + } + } catch (e) { + print("Failed to sync route"); } - final document = response.documents.first; + { + // Local Room - // Listen for changes - // { - // final realtime = appwrite.Realtime(client); - // - // if (_subscription != null) { - // _subscription!.close(); - // } - // - // _subscription = realtime.subscribe([ - // 'databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}' - // ]); - // - // _subscription!.stream.listen(ServerListener); - // } - // Listen for changes - if (_keepAliveConnection != null) { - try { - _keepAliveConnection!.close(); - } catch (e) { - print("Failed to close connection... oh well"); + String host = jsonDecode(infoJson)["local"]["host"]; + + if (await networkingModule.connectToWebSocketServer(host)){ + print("Connected to local room at $host"); + + _listenerReciept = networkingModule.onMessageReceived?.addListener( + (p0) { + print("Received local command: $p0"); + ExecuteCommand(p0); + } + ); + inRoom = true; + return; // 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"); } } - String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1"; - String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim(); - _keepAliveConnection = RealtimeKeepAliveConnection( - channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'], - onData: ServerListener, - domain: domain, - client: auth.client, - onError: (e) { - print("Workarround Error: $e"); - }, - ); - _keepAliveConnection!.initialize(); + { + // Cloud Room - // Update the room document ID - roomDocumentID = document.$id; + String roomCode = jsonDecode(infoJson)["cloud"]["roomCode"]; - // Get the current route - try { - String routeNumber = document.data["CurrentRoute"]; - int routeVariantIndex = document.data["CurrentRouteVariant"]; + print("Joining cloud room with code $roomCode"); - await setRouteVariantQuery(routeNumber, routeVariantIndex); - print("Set route to $routeNumber $routeVariantIndex"); - } catch (e) { - print("Failed to set route"); + // Disable host mode + isHost = false; + + // Update the room code + this.roomCode = roomCode; + + // Access the database + final client = auth.client; + final databases = appwrite.Databases(client); + + // Get the document + final response = await databases.listDocuments( + databaseId: "6633e85400036415ab0f", + collectionId: "6633e85d0020f52f3771", + queries: [ + appwrite.Query.search("SessionID", roomCode) + ] + ); + + if (response.documents.isEmpty) { + throw Exception("Room not found"); + } + + final document = response.documents.first; + + // Listen for changes + if (_keepAliveConnection != null) { + try { + _keepAliveConnection!.close(); + } catch (e) { + print("Failed to close connection... oh well"); + } + } + + String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1"; + String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim(); + _keepAliveConnection = RealtimeKeepAliveConnection( + channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'], + onData: ServerListener, + domain: domain, + client: auth.client, + onError: (e) { + print("Workarround Error: $e"); + }, + ); + _keepAliveConnection!.initialize(); + + // Update the room document ID + roomDocumentID = document.$id; + + // Get the current route + try { + String routeNumber = document.data["CurrentRoute"]; + int routeVariantIndex = document.data["CurrentRouteVariant"]; + + await setRouteVariantQuery(routeNumber, routeVariantIndex); + print("Set route to $routeNumber $routeVariantIndex"); + } catch (e) { + print("Failed to set route"); + } + inRoom = true; + print("Joined cloud room with code $roomCode"); } - print("Joined room with code $roomCode"); } Future leaveRoom() async { - if (roomCode == null) { - throw Exception("Not in a room"); + if (!inRoom) { + return; } + { + // Local Room + networkingModule.stopWebSocketServer(); + inRoom = false; + networkingModule.onMessageReceived?.removeListener(_listenerReciept!); + } + + { + // Cloud Room + if (_keepAliveConnection != null) { + _keepAliveConnection!.close(); + _keepAliveConnection = null; + } + } + + inRoom = false; + if (isHost) { // Access the database final client = auth.client; @@ -406,7 +470,7 @@ class LiveInformation { roomCode = null; roomDocumentID = null; isHost = false; - + inRoom = false; _keepAliveConnection?.close(); _keepAliveConnection = null; @@ -414,6 +478,42 @@ class LiveInformation { setRouteVariant(null); } + String generateRoomInfo() { + + // Room Info Example + /* + { + "cloud": { + "roomCode": "6633e85d0020f52f3771" + }, + "local": + { + "host": "ws://192.168.0.123:8080" + }, + "sync": + { + "route": "W11", + "routeVariant": 1 + } + } + */ + + return jsonEncode({ + "cloud": { + "roomCode": roomCode, + }, + "local": { + "host": "ws://${networkingModule.localIP}:8080" + }, + if (_currentRouteVariant != null) + "sync": { + "route": _currentRouteVariant!.busRoute.routeNumber, + "routeVariant": _currentRouteVariant!.busRoute.routeVariants.values.toList().indexOf(_currentRouteVariant!), + } + }); + + } + String? lastCommand; Future ServerListener(appwrite.RealtimeMessage response) async { print("Session update"); @@ -454,8 +554,11 @@ class LiveInformation { // Execute the command List commands = response.payload["Commands"].cast(); - String? command = commands.last; + ExecuteCommand(commands.last); + } + + void ExecuteCommand(String command) { if (command == lastCommand) { return; } @@ -516,6 +619,12 @@ class LiveInformation { ); } + } else if (commandName == "setroute") { + print("Set route command received"); + String routeNumber = commandParts[1]; + int routeVariantIndex = int.parse(commandParts[2]); + + setRouteVariantQuery(routeNumber, routeVariantIndex, sendToServer: false); } } @@ -527,34 +636,48 @@ class LiveInformation { Future SendCommand(String command) async { - final client = auth.client; - final databases = appwrite.Databases(client); + { + // Local Commands - final response = await databases.listDocuments( - databaseId: "6633e85400036415ab0f", - collectionId: "6633e85d0020f52f3771", - queries: [ - appwrite.Query.search("SessionID", roomCode!) - ] - ); + networkingModule.sendMessage(command); + } - List pastCommands = []; - response.documents.first.data["Commands"].forEach((element) { - pastCommands.add(element); - }); + { + // Cloud Commands + + final client = auth.client; + final databases = appwrite.Databases(client); + + final response = await databases.listDocuments( + databaseId: "6633e85400036415ab0f", + collectionId: "6633e85d0020f52f3771", + queries: [ + appwrite.Query.search("SessionID", roomCode!) + ] + ); + + List pastCommands = []; + + response.documents.first.data["Commands"].forEach((element) { + pastCommands.add(element); + }); + + pastCommands.add(command); + + final document = await databases.updateDocument( + databaseId: "6633e85400036415ab0f", + collectionId: "6633e85d0020f52f3771", + documentId: roomDocumentID!, + data: { + "Commands": pastCommands, + "LastUpdater": auth.userID, + } + ); + } + - pastCommands.add(command); - final document = await databases.updateDocument( - databaseId: "6633e85400036415ab0f", - collectionId: "6633e85d0020f52f3771", - documentId: roomDocumentID!, - data: { - "Commands": pastCommands, - "LastUpdater": auth.userID, - } - ); } List _splitCommand(String command) { diff --git a/lib/backend/modules/announcement.dart b/lib/backend/modules/announcement.dart index 6bbff65..53a580e 100644 --- a/lib/backend/modules/announcement.dart +++ b/lib/backend/modules/announcement.dart @@ -27,7 +27,7 @@ class AnnouncementModule extends InfoModule { // Files String _bundleLocation = "assets/ibus_recordings.zip"; Uint8List? _bundleBytes; - void setBundleBytes(Uint8List bytes) { + void setBundleBytes(Uint8List? bytes) { _bundleBytes = bytes; } Future getBundleBytes() async { @@ -35,7 +35,6 @@ class AnnouncementModule extends InfoModule { if (_bundleBytes != null) { return _bundleBytes!; } else { - // Try to load them from shared preferences try { SharedPreferences prefs = await SharedPreferences.getInstance(); @@ -47,17 +46,7 @@ class AnnouncementModule extends InfoModule { } catch (e) { throw Exception("Loading announcements from assets has been deprecated."); } - - - - // if (kIsWeb) { - // throw Exception("Cannot load bundle bytes on web"); - // } - // - // final bytes = await rootBundle.load(_bundleLocation); - // return bytes.buffer.asUint8List(); } - } // Queue @@ -165,7 +154,14 @@ class AnnouncementModule extends InfoModule { } // Configuration - int get defaultAnnouncementDelay => liveInformation.auth.isAuthenticated() ? 1 : 0; + Duration get defaultAnnouncementDelay { + if (liveInformation.inRoom) { + return Duration(milliseconds: 500); + } else { + print("Not in room"); + return Duration.zero; + } + } // Methods Future queueAnnounceByAudioName({ @@ -177,7 +173,9 @@ class AnnouncementModule extends InfoModule { if (sendToServer && _shouldSendToServer()) { - scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay)); + + + scheduledTime ??= liveInformation.syncedTimeModule.Now().add(defaultAnnouncementDelay); String audioNamesString = ""; @@ -232,7 +230,7 @@ class AnnouncementModule extends InfoModule { if (sendToServer && _shouldSendToServer()) { - scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay)); + scheduledTime ??= liveInformation.syncedTimeModule.Now().add(defaultAnnouncementDelay); liveInformation.SendCommand("announce info $infoIndex ${scheduledTime.millisecondsSinceEpoch}"); queueAnnouncementByInfoIndex( @@ -262,7 +260,9 @@ class AnnouncementModule extends InfoModule { if (sendToServer && _shouldSendToServer()) { - scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay)); + print("Sending route announcement to server"); + + scheduledTime ??= liveInformation.syncedTimeModule.Now().add(defaultAnnouncementDelay); String routeNumber = routeVariant.busRoute.routeNumber; int routeVariantIndex = routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant); @@ -326,7 +326,7 @@ class AnnouncementModule extends InfoModule { // Server check bool _shouldSendToServer() { - bool condition = liveInformation.roomCode != null; + bool condition = liveInformation.inRoom; print("Should send to server? " + (condition.toString())); return condition; diff --git a/lib/backend/modules/networking.dart b/lib/backend/modules/networking.dart new file mode 100644 index 0000000..7512dd4 --- /dev/null +++ b/lib/backend/modules/networking.dart @@ -0,0 +1,196 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:network_info_plus/network_info_plus.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart' as io; +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'; + +class NetworkingModule extends InfoModule { + // Host websocket server + String host = "ws://0.0.0.0:8080"; + HttpServer? _server; + WebSocketChannel? _channel; + + // Store connected WebSocket channels + final List _connectedClients = []; + + EventDelegate? onMessageReceived = EventDelegate(); + + NetworkingModule() { + _refresh(); + refreshTimer(); + } + + Future startWebSocketServer() async { + try { + var handler = webSocketHandler((WebSocketChannel webSocket) { + _connectedClients.add(webSocket); // Add the client to the list + print('Client connected: ${webSocket}'); // Log client connection + + webSocket.stream.listen((message) { + // Handle messages from the client here + print('Received message: $message'); + + // Forward message to all clients except the sender + for (var client in _connectedClients) { + if (client != webSocket) { + client.sink.add(message); + } + } + + _onMessageReceived(message); + }, onDone: () { + _connectedClients.remove(webSocket); // Remove client on disconnect + print('Client disconnected: ${webSocket}'); // Log client disconnection + }); + }); + + _server = await io.serve(handler, InternetAddress.anyIPv4, 8080); + print('WebSocket server started at ${_server?.address.address}:${_server?.port}'); + + return true; + } catch (e) { + print('Failed to start WebSocket server: $e'); + return false; + } + } + + bool stopWebSocketServer() { + if (_server == null) { + throw Exception('WebSocket server is not running'); + } + + try { + for (var client in _connectedClients) { + client.sink.close(); + } + _connectedClients.clear(); + _server?.close(force: true); + _server = null; + print('WebSocket server stopped'); + return true; + } catch (e) { + print('Failed to stop WebSocket server: $e'); + return false; + } + } + + Future connectToWebSocketServer(String url) async { + try { + _channel = await WebSocketChannel.connect(Uri.parse(url)); + _channel?.stream.listen((message) { + // Handle messages from the server here + print('Received message from server: $message'); + _onMessageReceived(message); + }); + + print('Connected to WebSocket server at $url'); + return true; + } catch (e) { + print('Failed to connect to WebSocket server: $e'); + return false; + } + } + + bool disconnectFromWebSocketServer() { + if (_channel == null) { + throw Exception('No active WebSocket connection'); + } + + try { + _channel?.sink.close(); + _channel = null; + print('Disconnected from WebSocket server'); + return true; + } catch (e) { + print('Failed to disconnect from WebSocket server: $e'); + return false; + } + } + + bool sendMessage(String message) { + + // If hosting a server, send message to all clients + if (_server != null) { + return sendMessageToClients(message); + } + + if (_channel == null) { + throw Exception('No active WebSocket connection'); + } + + try { + _channel?.sink.add(message); + print('Sent message: $message'); + return true; + } catch (e) { + print('Failed to send message: $e'); + return false; + } + } + + bool sendMessageToClients(String message) { + if (_connectedClients.isEmpty) { + print('No clients connected'); + return false; + } + + try { + for (var client in _connectedClients) { + client.sink.add(message); + } + print('Sent message to all clients: $message'); + return true; + } catch (e) { + print('Failed to send message to clients: $e'); + return false; + } + } + + void _onMessageReceived(String message) { + // Notify all listeners that a message has been received. + onMessageReceived?.trigger(message); + } + + // Useful boilerplate + String _localIP = ""; + String get localIP => _localIP; + + Timer refreshTimer() => Timer.periodic(const Duration(seconds: 10), (timer) { + if (kIsWeb) return; + _refresh(); + }); + + Future _refresh() async { + print("Refreshing network info..."); + { + // Update the local IP address + + // First try NetworkInfo + _localIP = (await NetworkInfo().getWifiIP()) ?? ""; + + // If null, try NetworkInterface + // Only look for ethernet. Wifi would have been found by NetworkInfo + if (_localIP.isEmpty) { + for (var interface in await NetworkInterface.list()) { + if (!interface.name.toLowerCase().contains("eth") || interface.name.contains(" ")) { + continue; + } + + for (var addr in interface.addresses) { + print('Interface ${interface.name} has address ${addr.address}'); + if (addr.type == InternetAddressType.IPv4 && !addr.isLoopback) { + _localIP = addr.address; + break; + } + } + } + } + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 093ba11..9b1c38e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,8 +9,10 @@ import 'package:bus_infotainment/remaster/dashboard.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:shelf/shelf.dart'; import 'package:window_manager/window_manager.dart'; import 'package:bus_infotainment/remaster/InitialStartup.dart' as remaster; +import 'package:shelf/shelf_io.dart' as shelf_io; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -19,10 +21,11 @@ void main() async { await windowManager.ensureInitialized(); WindowOptions options = WindowOptions( - size: Size(411.4, 850.3), + title: 'Bus Infotainment', + ); - windowManager.setAspectRatio(411.4 / 850.3); + // windowManager.setAspectRatio(411.4 / 850.3); await windowManager.waitUntilReadyToShow(options, () async { await windowManager.show(); @@ -30,22 +33,31 @@ void main() async { }); } - + // { + // // Web server test + // + // var handler = const Pipeline().addMiddleware(logRequests()).addHandler((Request request) { + // return Response.ok('Hello, world!'); + // }); + // var server = await shelf_io.serve(handler, '0.0.0.0', 8080); + // server.autoCompress = true; + // + // print('Serving at http://${server.address.host}:${server.port}'); + // + // // get the IP address + // for (var interface in await NetworkInterface.list()) { + // for (var addr in interface.addresses) { + // print('Interface ${interface.name} has address ${addr.address}'); + // print('Try http://${addr.address}:${server.port}'); + // } + // } + // + // } LiveInformation liveInformation = LiveInformation(); await liveInformation.initialize(); runApp(const MyApp()); - - // Disalow screen to turn off on android - await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); - // Disalow landscape mode - await SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); - - } class MyApp extends StatelessWidget { diff --git a/lib/pages/components/ibus_display.dart b/lib/pages/components/ibus_display.dart index a5c37ee..b3891a9 100644 --- a/lib/pages/components/ibus_display.dart +++ b/lib/pages/components/ibus_display.dart @@ -52,7 +52,7 @@ class _ibus_displayState extends State { String _padString(String input){ if (input.length < 30){ - print("Input is too short"); + // print("Input is too short"); return input; } @@ -241,12 +241,14 @@ class _timeComponentState extends State<_timeComponent> { String timeString = "${now.hour % 12}:${now.minute.toString().padLeft(2, "0")} ${now.hour < 12 ? "AM" : "PM"}"; + timeString = timeString.replaceAll("0:", "12:"); + return timeString; } String _padString(String input){ if (input.length < 30){ - print("Input is too short"); + // print("Input is too short"); return input; } diff --git a/lib/pages/display.dart b/lib/pages/display.dart index 144fd60..6e89a23 100644 --- a/lib/pages/display.dart +++ b/lib/pages/display.dart @@ -74,11 +74,6 @@ class _pages_DisplayState extends State { }); // Hide the notification bar and make the app full screen and display over notch - if (widget._tfL_Dataset_TestState.hideUI) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); - } else { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); - } }, ), diff --git a/lib/remaster/DashboardArc.dart b/lib/remaster/DashboardArc.dart index aa90bd7..3190f34 100644 --- a/lib/remaster/DashboardArc.dart +++ b/lib/remaster/DashboardArc.dart @@ -3,9 +3,12 @@ import 'package:bus_infotainment/backend/live_information.dart'; import 'package:bus_infotainment/pages/components/ibus_display.dart'; import 'package:bus_infotainment/remaster/dashboard.dart'; import 'package:bus_infotainment/tfl_datasets.dart'; +import 'package:bus_infotainment/utils/delegates.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_scroll_shadow/flutter_scroll_shadow.dart'; +import 'package:network_info_plus/network_info_plus.dart'; +import 'package:qr_flutter/qr_flutter.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; class ArcDashboard extends StatefulWidget { @@ -17,260 +20,207 @@ class ArcDashboard extends StatefulWidget { class _ArcDashboardState extends State { _closeDialogueChecker closeDialogWidget = _closeDialogueChecker(); + late ListenerReceipt onRouteVariantChange; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + onRouteVariantChange = LiveInformation().routeVariantDelegate.addListener((value) { + print("Route variant changed"); + setState(() { + + }); + }); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + + LiveInformation().routeVariantDelegate.removeListener(onRouteVariantChange); + } + @override Widget build(BuildContext context) { + return PopScope( - // Force landscape mode - Future.delayed(Duration(seconds: 1), () { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeRight, - DeviceOrientation.landscapeLeft, - ]); - }); + onPopInvoked: (isPop) { - return Scaffold( + try { + LiveInformation().leaveRoom(); + print("Left room"); + } catch (e) { + print("Error leaving room: $e"); + } - body: Container( - child: Row( - children: [ + }, - const SizedBox( - width: 10, - ), + child: Scaffold( - Container( - padding: const EdgeInsets.symmetric( - vertical: 10, + body: Container( + child: Row( + + children: [ + + const SizedBox( + width: 10, ), - child: IntrinsicWidth( - child: Column( - children: [ + Container( + padding: const EdgeInsets.symmetric( + vertical: 10, + ), - if (LiveInformation().roomDocumentID != null) - Tooltip( - child: ShadButton( - icon: const Icon(Icons.network_check), + child: IntrinsicWidth( + child: Column( + children: [ + + ShadButton.outline( + icon: const Icon(Icons.menu), width: double.infinity, + border: Border.all( + width: 2, + color: Colors.grey.shade400, + ), + padding: const EdgeInsets.all(0), + borderRadius: const BorderRadius.all(Radius.circular(10)), onPressed: () { + bool multiMode = ModalRoute.of(context)!.settings.name!.contains("multi"); + showShadSheet( context: context, + side: ShadSheetSide.left, + builder: (context) { return ShadSheet( - padding: const EdgeInsets.all(10), - content: Column( - children: [ - Text("Room ID: ${LiveInformation().roomDocumentID}"), - ], + padding: const EdgeInsets.all(0), + content: Container( + width: 225, + height: MediaQuery.of(context).size.height, + padding: const EdgeInsets.all(10), + child: Column( + children: [ + + Expanded( + child: Column( + children: [ + + ShadButton( + text: Text("Rooom Information"), + onPressed: () { + Navigator.pop(context); + showShadSheet( + context: context, + side: ShadSheetSide.left, + 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"), + ) + ); + }, + ) + ], + + ), + ); + } + ); + }, + ) + + ], + ) + ), + + if (!multiMode) + ShadButton( + text: Text("Return to route selection"), + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); + }, + ) + else + ShadButton( + text: Text("Route selection"), + onPressed: () { + Navigator.popAndPushNamed(context, '/multi/routes'); + }, + ) + ], + ), ), ); } ); + }, ), - message: "Room information", - ), - SizedBox( - height: 220, - child: RotatedBox( - quarterTurns: 3, - child: Column( - children: [ - ShadButton( - text: const Text("Manual Announcements"), - width: double.infinity, - borderRadius: const BorderRadius.all(Radius.circular(10)), - onPressed: () { + SizedBox( + height: 220, + child: RotatedBox( + quarterTurns: 3, + child: Column( + children: [ + ShadButton( + text: const Text("Manual Announcements"), + width: double.infinity, + borderRadius: const BorderRadius.all(Radius.circular(10)), + onPressed: () { - List announcements = []; + List announcements = []; - for (var announcement in LiveInformation().announcementModule.manualAnnouncements) { + for (var announcement in LiveInformation().announcementModule.manualAnnouncements) { - announcements.add( - ShadButton( - text: SizedBox( - width: 200-42, - child: Text(announcement.shortName), - ), - onPressed: () { - - if (closeDialogWidget.closeDialog) { - Navigator.pop(context); - } - - LiveInformation().announcementModule.queueAnnouncementByInfoIndex( - infoIndex: LiveInformation().announcementModule.manualAnnouncements.indexOf(announcement), - ); - }, - ) - ); - - } - - print(announcements.length); - - showShadSheet( - context: context, - side: ShadSheetSide.left, - - builder: (context) { - return ShadSheet( - padding: const EdgeInsets.all(0), - - content: Container( - height: MediaQuery.of(context).size.height, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 5, - ), - Container( - padding: const EdgeInsets.symmetric( - vertical: 10, - ), - alignment: Alignment.bottomCenter, - height: double.infinity, - width: 35, - child: RotatedBox( - quarterTurns: 3, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - "Manual Ann'", - style: ShadTheme.of(context).textTheme.h3 - ), - SizedBox( - width: 16, - ), - Container( - width: 1, - height: 200, - color: Colors.grey, - ), - ], - ), - - closeDialogWidget - - ], - ), - ), - ), - Container( - - // width: 220, - height: MediaQuery.of(context).size.height, - - child: Scrollbar( - thumbVisibility: true, - child: SingleChildScrollView( - reverse: true, - child: Container( - margin: const EdgeInsets.fromLTRB( - 0, - 10, - 10, - 10 - ), - child: Column( - children: announcements.reversed.toList(), - ), - ), - ), - ), - ), - ], + announcements.add( + ShadButton( + text: SizedBox( + width: 200-42, + child: Text(announcement.shortName), ), - ), + onPressed: () { + + if (closeDialogWidget.closeDialog) { + Navigator.pop(context); + } + + LiveInformation().announcementModule.queueAnnouncementByInfoIndex( + infoIndex: LiveInformation().announcementModule.manualAnnouncements.indexOf(announcement), + ); + }, + ) ); + } - ); - }, - ), - ShadButton( - text: const Text("Bus Stop Announcements"), - width: double.infinity, - borderRadius: const BorderRadius.all(Radius.circular(10)), - onPressed: () { + print(announcements.length); - showShadSheet( + showShadSheet( context: context, side: ShadSheetSide.left, builder: (context) { - - List announcements = []; - - LiveInformation info = LiveInformation(); - - for (var busStop in info.getRouteVariant()!.busStops) { - - if (info.trackerModule.nearestStop == busStop) { - announcements.add( - ShadButton( - text: SizedBox( - width: 200-42, - child: Text( - "-> ${busStop.formattedStopName}", - overflow: TextOverflow.ellipsis, - ), - ), - backgroundColor: Colors.amber, - onPressed: () { - if (closeDialogWidget.closeDialog) { - Navigator.pop(context); - } - LiveInformation().announcementModule.queueAnnounceByAudioName( - displayText: busStop.formattedStopName, - audioNames: [busStop.getAudioFileName()], - ); - }, - ) - ); - } else { - announcements.add( - ShadButton( - text: SizedBox( - width: 200-42, - child: Text( - busStop.formattedStopName, - overflow: TextOverflow.ellipsis, - ), - ), - onPressed: () { - if (closeDialogWidget.closeDialog) { - Navigator.pop(context); - } - LiveInformation().announcementModule.queueAnnounceByAudioName( - displayText: busStop.formattedStopName, - audioNames: [busStop.getAudioFileName()], - ); - }, - ) - ); - } - } - - ScrollController controller = ScrollController(); - - // Scroll to the current bus stop - WidgetsBinding.instance!.addPostFrameCallback((_) { - - double offset = (info.getRouteVariant()!.busStops.indexOf(info.trackerModule.nearestStop!) * 50); - - // Offset the offset so that its in the middle of the screen - offset -= (MediaQuery.of(context).size.height / 2) - 25; - - // controller.jumpTo(offset); - }); - return ShadSheet( padding: const EdgeInsets.all(0), @@ -297,8 +247,8 @@ class _ArcDashboardState extends State { Row( children: [ Text( - "Bus Stops", - style: ShadTheme.of(context).textTheme.h3 + "Manual Ann'", + style: ShadTheme.of(context).textTheme.h3 ), SizedBox( width: 16, @@ -324,16 +274,14 @@ class _ArcDashboardState extends State { child: Scrollbar( thumbVisibility: true, - controller: controller, child: SingleChildScrollView( reverse: true, - controller: controller, child: Container( margin: const EdgeInsets.fromLTRB( - 0, - 10, - 10, - 10 + 0, + 10, + 10, + 10 ), child: Column( children: announcements.reversed.toList(), @@ -347,108 +295,272 @@ class _ArcDashboardState extends State { ), ); } - ); + ); - }, - ), - ], + }, + ), + ShadButton( + text: const Text("Bus Stop Announcements"), + enabled: LiveInformation().getRouteVariant() != null, + width: double.infinity, + borderRadius: const BorderRadius.all(Radius.circular(10)), + onPressed: () { + + showShadSheet( + context: context, + side: ShadSheetSide.left, + + builder: (context) { + + List announcements = []; + + LiveInformation info = LiveInformation(); + + for (var busStop in info.getRouteVariant()!.busStops) { + + if (info.trackerModule.nearestStop == busStop) { + announcements.add( + ShadButton( + text: SizedBox( + width: 200-42, + child: Text( + "-> ${busStop.formattedStopName}", + overflow: TextOverflow.ellipsis, + ), + ), + backgroundColor: Colors.amber, + onPressed: () { + if (closeDialogWidget.closeDialog) { + Navigator.pop(context); + } + LiveInformation().announcementModule.queueAnnounceByAudioName( + displayText: busStop.formattedStopName, + audioNames: [busStop.getAudioFileName()], + ); + }, + ) + ); + } else { + announcements.add( + ShadButton( + text: SizedBox( + width: 200-42, + child: Text( + busStop.formattedStopName, + overflow: TextOverflow.ellipsis, + ), + ), + onPressed: () { + if (closeDialogWidget.closeDialog) { + Navigator.pop(context); + } + LiveInformation().announcementModule.queueAnnounceByAudioName( + displayText: busStop.formattedStopName, + audioNames: [busStop.getAudioFileName()], + ); + }, + ) + ); + } + } + + ScrollController controller = ScrollController(); + + // Scroll to the current bus stop + WidgetsBinding.instance!.addPostFrameCallback((_) { + + double offset = (info.getRouteVariant()!.busStops.indexOf(info.trackerModule.nearestStop!) * 50); + + // Offset the offset so that its in the middle of the screen + offset -= (MediaQuery.of(context).size.height / 2) - 25; + + // controller.jumpTo(offset); + }); + + return ShadSheet( + padding: const EdgeInsets.all(0), + + content: Container( + height: MediaQuery.of(context).size.height, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 5, + ), + Container( + padding: const EdgeInsets.symmetric( + vertical: 10, + ), + alignment: Alignment.bottomCenter, + height: double.infinity, + width: 35, + child: RotatedBox( + quarterTurns: 3, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + "Bus Stops", + style: ShadTheme.of(context).textTheme.h3 + ), + SizedBox( + width: 16, + ), + Container( + width: 1, + height: 200, + color: Colors.grey, + ), + ], + ), + + closeDialogWidget + + ], + ), + ), + ), + Container( + + // width: 220, + height: MediaQuery.of(context).size.height, + + child: Scrollbar( + thumbVisibility: true, + controller: controller, + child: SingleChildScrollView( + reverse: true, + controller: controller, + child: Container( + margin: const EdgeInsets.fromLTRB( + 0, + 10, + 10, + 10 + ), + child: Column( + children: announcements.reversed.toList(), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + ); + + }, + ), + ], + ) ) + ), + + const ShadButton( + icon: Icon(Icons.stop), + width: double.infinity, + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + + ShadButton( + // text: const Text("Announce Destination"), + icon: const Icon(Icons.bus_alert), + width: double.infinity, + borderRadius: const BorderRadius.all(Radius.circular(10)), + onPressed: () { + LiveInformation info = LiveInformation(); + + BusRouteVariant? routeVariant = info.getRouteVariant(); + + if (routeVariant != null) { + info.announcementModule.queueAnnouncementByRouteVariant( + routeVariant: routeVariant, + sendToServer: ModalRoute.of(context)!.settings.name!.contains("multi") + ); + } + + }, + ), + + + + ], + ), + ), + + ), + + Expanded( + child: Container( + + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(10)), + + ), + + margin: const EdgeInsets.all(10), + padding: const EdgeInsets.all(10), + + width: double.infinity, + height: double.infinity, + + child: Stack( + children: [ + Container( + + alignment: Alignment.center, + + child: ibus_display( + hasBorder: false, + ), + + ), + Container( + + alignment: Alignment.bottomRight, + + child: ShadButton.ghost( + icon: const Icon(Icons.fullscreen), + padding: const EdgeInsets.all(8), + onPressed: () { + Navigator.pushNamed(context, '/display'); + }, + ), + + ), + Container( + + alignment: Alignment.bottomLeft, + + child: ShadButton.ghost( + icon: const Icon(Icons.arrow_back), + padding: const EdgeInsets.all(8), + onPressed: () { + Navigator.popUntil(context, (route) { + return route.settings.name == '/multi' || route.settings.name == '/routes'; + }); + }, + ), + ) - ), - - const ShadButton( - icon: Icon(Icons.stop), - width: double.infinity, - ), - - ShadButton( - // text: const Text("Announce Destination"), - icon: const Icon(Icons.location_on), - width: double.infinity, - borderRadius: const BorderRadius.all(Radius.circular(10)), - onPressed: () { - LiveInformation info = LiveInformation(); - - BusRouteVariant? routeVariant = info.getRouteVariant(); - - if (routeVariant != null) { - info.announcementModule.queueAnnouncementByRouteVariant( - routeVariant: routeVariant, - sendToServer: false - ); - } - - }, - ) - - ], + ], + ), ), - ), + ) - ), - - Expanded( - child: Container( - - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.all(Radius.circular(10)), - - ), - - margin: const EdgeInsets.all(10), - padding: const EdgeInsets.all(10), - - width: double.infinity, - height: double.infinity, - - child: Stack( - children: [ - Container( - - alignment: Alignment.center, - - child: ibus_display( - hasBorder: false, - ), - - ), - Container( - - alignment: Alignment.bottomRight, - - child: ShadButton.ghost( - icon: const Icon(Icons.fullscreen), - padding: const EdgeInsets.all(8), - onPressed: () { - Navigator.pushNamed(context, '/display'); - }, - ), - - ), - Container( - - alignment: Alignment.bottomLeft, - - child: ShadButton.ghost( - icon: const Icon(Icons.arrow_back), - padding: const EdgeInsets.all(8), - onPressed: () { - Navigator.pop(context); - }, - ), - - ) - ], - ), - ), - ) - - ], + ], + ), ), - ), + ), ); } diff --git a/lib/remaster/InitialStartup.dart b/lib/remaster/InitialStartup.dart index 6cb0b1c..e3fc045 100644 --- a/lib/remaster/InitialStartup.dart +++ b/lib/remaster/InitialStartup.dart @@ -154,7 +154,7 @@ class _page2State extends State<_page2> { child: SizedBox( - width: double.infinity, + // width: double.infinity, child: Column( @@ -162,7 +162,6 @@ class _page2State extends State<_page2> { crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( "Permissions", style: TextStyle( @@ -175,105 +174,172 @@ class _page2State extends State<_page2> { height: 16, ), - ShadCard( - width: double.infinity, - title: Text( - "Location", - ), - description: Text( - "Your location is required for automatically updating your nearest bus stop." - ), - content: Container( - child: Column( - children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( - SizedBox( - height: 4, + // mainAxisSize: MainAxisSize.min, + // crossAxisAlignment: CrossAxisAlignment.start, + + children: [ + ShadCard( + width: 300, + height: 200, + title: Text( + "Location", ), - - 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; - } - - return ShadButton( - text: Text(text), - onPressed: () async { - await Permission.location.request(); - setState(() { - - }); - }, - enabled: isEnabled, - backgroundColor: color, - ); - }, + description: Text( + "Your location is required for automatically updating your nearest bus stop." ), - ], - ), - ), - ), + content: Container( + child: Column( + children: [ - SizedBox( - height: 16, - ), + SizedBox( + height: 4, + ), - ShadCard( - width: double.infinity, - title: Text( - "Storage", - ), - description: Text( - "Storage access is required to access recorded announcements." - ), - content: Container( - child: Column( - children: [ + FutureBuilder( + future: Permission.location.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; + } + + return ShadButton( + text: Text(text), + onPressed: () async { + await Permission.location.request(); + setState(() { + + }); + }, + enabled: isEnabled, + backgroundColor: color, + ); + }, + ), + ], + ), ), + ), - FutureBuilder( - future: Permission.manageExternalStorage.isGranted, - builder: (context, val) { - bool isEnabled = true; - String text = "Request permission"; - Color color = Colors.white; + SizedBox( + width: 16, + ), - if (val.hasData) { - isEnabled = !val.data!; - } - if (!isEnabled) { - text = "Permission granted!"; - color = Colors.green.shade400; - } + ShadCard( + width: 300, + height: 200, + title: Text( + "Storage", + ), + description: Text( + "Storage access is required to access recorded announcements." + ), + content: Container( + child: Column( + children: [ - return ShadButton( - text: Text(text), - onPressed: () async { - await Permission.manageExternalStorage.request(); - setState(() { + SizedBox( + height: 4, + ), - }); - }, - enabled: isEnabled, - backgroundColor: color, - ); - }, - ) - ], - ), + FutureBuilder( + future: Permission.manageExternalStorage.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, + ); + }, + ) + ], + ), + ), + ), + + 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, + ); + }, + ) + ], + ), + ), + ), + ], ), ), @@ -306,7 +372,6 @@ class _page2State extends State<_page2> { ); }, ) - ], ), ) diff --git a/lib/remaster/RemasteredMain.dart b/lib/remaster/RemasteredMain.dart index d2709bc..75eeb75 100644 --- a/lib/remaster/RemasteredMain.dart +++ b/lib/remaster/RemasteredMain.dart @@ -4,15 +4,40 @@ 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/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 'WebSocketTest.dart'; class RemasteredApp extends StatelessWidget { @override Widget build(BuildContext context) { - // TODO: implement build + + // Force landscape mode + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + + // Hide navigation bar and status bar + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive, overlays: [ + SystemUiOverlay.bottom, + SystemUiOverlay.top, + ]); + + // Hide the gesture navigation bar + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky, overlays: [ + SystemUiOverlay.bottom, + SystemUiOverlay.top, + ]); + + + return ShadApp( darkTheme: ShadThemeData( brightness: Brightness.dark, @@ -20,16 +45,22 @@ class RemasteredApp extends StatelessWidget { background: Colors.grey.shade900, primary: Colors.grey.shade50, primaryForeground: Colors.grey.shade900, - border: Colors.grey.shade900, + border: Colors.grey.shade400, + input: Colors.grey.shade400, ), - // force dark mode ), themeMode: ThemeMode.dark, + // remove debug banner + debugShowCheckedModeBanner: false, + routes: { '/setup': (context) => InitialStartup(), '/': (context) => HomePage_Re(), - '/routes': (context) => RoutePage(), + + '/routes': (context) => SearchArc(), + '/multi/routes': (context) => RoutePage(), + '/enroute': (context) => ArcDashboard(), '/legacy': (context) => TfL_Dataset_Test(), '/multi': (context) => MultiModeSetup(), @@ -38,6 +69,7 @@ class RemasteredApp extends StatelessWidget { '/multi/register': (context) => MultiModeRegister(), '/display': (context) => FullscreenDisplay(), '/multi/join': (context) => MultiModeJoin(), + '/websocket': (context) => WebSocketWidget(), }, diff --git a/lib/remaster/SearchArc.dart b/lib/remaster/SearchArc.dart new file mode 100644 index 0000000..1d6130b --- /dev/null +++ b/lib/remaster/SearchArc.dart @@ -0,0 +1,346 @@ + + +import 'package:bus_infotainment/backend/live_information.dart'; +import 'package:bus_infotainment/remaster/dashboard.dart'; +import 'package:bus_infotainment/tfl_datasets.dart'; +import 'package:bus_infotainment/utils/OrdinanceSurveyUtils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' hide NavigationBar; +import 'package:flutter/widgets.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; +import 'package:vector_math/vector_math.dart' hide Colors; + +import '../backend/modules/tube_info.dart'; + +class SearchArc extends StatefulWidget { + + @override + State createState() => _SearchArcState(); +} + +class _SearchArcState extends State { + TextEditingController searchController = TextEditingController(); + + @override + Widget build(BuildContext context) { + + List nearbyRoutes = _getNearbyRoutes(context); + List pastRoutes = _getPastRoutes(context); + + List routeCards = []; + + if (searchController.text.isNotEmpty) { + // Detect if the search query is a route number + // Examples of route numbers: 1, A1, 1A, A1A ... + // If it isnt a route number, it is a stop name + // Examples of stop names: "Euston Station", "Baker Street", "Kings Cross" + bool containsNumber = RegExp(r'\d').hasMatch(searchController.text); + + List searchResults = []; + + // Loop through all bus routes + for (BusRoute route in LiveInformation().busSequences.routes.values) { + if (containsNumber) { + if (route.routeNumber.contains(searchController.text) && !searchResults.contains(route)) { + routeCards.add(_getRouteCard(context, route)); + searchResults.add(route); + } + } else { + for (BusRouteVariant variant in route.routeVariants.values) { + for (BusRouteStop stop in variant.busStops) { + if (stop.formattedStopName.toLowerCase().contains( + searchController.text.toLowerCase()) && !searchResults.contains(route)) { + routeCards.add(_getRouteCard(context, route)); + searchResults.add(route); + break; + } + } + } + } + } + + if (routeCards.isEmpty){ + routeCards.add( + Text("No results found", style: ShadTheme.of(context).textTheme.h3,) + ); + } + + } + + return Scaffold( + body: Row( + children: [ + Expanded( + child: Container( + padding: EdgeInsets.fromLTRB(32, 16, 32, 0), + height: MediaQuery.of(context).size.height, + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ShadInput( + placeholder: Text("Search for route or stop (Can be laggy)"), + controller: searchController, + onChanged: (value) { + setState(() { + // Update search results + }); + }, + ), + SizedBox(height: 8), + if (routeCards.isNotEmpty) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Search results", + style: ShadTheme.of(context).textTheme.h3, + ), + SizedBox(height: 10), + Expanded( + child: GridView.extent( + shrinkWrap: true, + maxCrossAxisExtent: 100, + scrollDirection: Axis.vertical, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + children: routeCards, + ), + ), + ], + ), + ) + else + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Nearby routes", + style: ShadTheme.of(context).textTheme.h3, + ), + SizedBox(height: 10), + Expanded( + child: GridView.extent( + shrinkWrap: true, + maxCrossAxisExtent: 100, + scrollDirection: Axis.vertical, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + children: _getNearbyRoutes(context, multiMode: true), + ), + ), + ], + ), + ) + ], + ), + ), + ], + ), + ), + ), + + Container( + width: 2, + color: Colors.grey.shade300, + ), + + RotatedBox( + quarterTurns: 3, + child: NavigationBar(), + ) + ], + ), + ); + } +} + +List _getNearbyRoutes(context, {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(_getRouteCard(context, route, multiMode: multiMode)); + } + + return routeCards; + +} + +Widget _getRouteCard(context, BusRoute route, {bool multiMode = false}) { + + 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 ElevatedButton( + onPressed: () { + showShadSheet( + side: ShadSheetSide.right, + context: context, + builder: (context) { + + List variantWidgets = []; + + for (BusRouteVariant variant in route.routeVariants.values) { + variantWidgets.add( + ShadButton.outline( + text: SizedBox( + + 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: 300, + height: MediaQuery.of(context).size.height - 52, + child: Scrollbar( + thumbVisibility: true, + child: ListView( + shrinkWrap: true, + children: [ + ...variantWidgets + ], + ), + ), + ), + padding: const EdgeInsets.all(8), + ); + + + } + + + ); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + + Text( + "Route \n ${route.routeNumber}", + style: ShadTheme.of(context).textTheme.h4.copyWith( + height: 1.1 + ), + textAlign: TextAlign.center, + ), + if (route.routeNumber.toLowerCase().startsWith("ul")) + Text(rr, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w400, height: 1), textAlign: TextAlign.center,) + + + ], + ), + + style: ElevatedButton.styleFrom( + padding: EdgeInsets.all(10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + );; +} + +List _getPastRoutes(context) { + return []; +} \ No newline at end of file diff --git a/lib/remaster/WebSocketTest.dart b/lib/remaster/WebSocketTest.dart new file mode 100644 index 0000000..8430d20 --- /dev/null +++ b/lib/remaster/WebSocketTest.dart @@ -0,0 +1,189 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:web_socket_channel/status.dart' as status; +import 'package:network_info_plus/network_info_plus.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; + +import '../utils/web_socket_server.dart'; + +class WebSocketWidget extends StatefulWidget { + @override + _WebSocketWidgetState createState() => _WebSocketWidgetState(); +} + +class _WebSocketWidgetState extends State { + WebSocketChannel? _channel; + TextEditingController _controller = TextEditingController(); + TextEditingController _urlController = TextEditingController(text: 'ws://localhost:8080'); + List _messages = []; + Isolate? _serverIsolate; + bool _isServerRunning = false; + String? _localIpAddress; + + @override + void initState() { + super.initState(); + _getLocalIpAddress().then((ip) { + setState(() { + _localIpAddress = ip; + _urlController.text = 'ws://$_localIpAddress:8080'; + }); + }); + } + + Future _getLocalIpAddress() async { + if (kIsWeb) { + return '127.0.0.1'; // Web does not support getting the local IP address + } + + if (Platform.isAndroid || Platform.isIOS) { + final info = NetworkInfo(); + String? ip = await info.getWifiIP(); + return ip ?? '127.0.0.1'; // Fallback to localhost if no IP is found + } + + for (var interface in await NetworkInterface.list()) { + for (var addr in interface.addresses) { + if (addr.type == InternetAddressType.IPv4 && !addr.isLoopback) { + return addr.address; + } + } + } + return '127.0.0.1'; // Fallback to localhost if no IP is found + } + + void _startServer() async { + final receivePort = ReceivePort(); + _serverIsolate = await Isolate.spawn(webSocketServer, receivePort.sendPort); + receivePort.listen((message) { + print(message); + setState(() { + _isServerRunning = true; + }); + }); + } + + void _stopServer() { + _serverIsolate?.kill(priority: Isolate.immediate); + setState(() { + _isServerRunning = false; + }); + } + + void _joinWebSocket() { + if (_channel != null) { + _channel!.sink.close(); + } + setState(() { + _channel = WebSocketChannel.connect(Uri.parse(_urlController.text)); + _channel!.stream.listen((message) { + setState(() { + _messages.add(message); + }); + }); + }); + } + + void _sendMessage(String message) { + if (_channel != null) { + _channel!.sink.add(message); + setState(() { + _messages.add('You: $message'); // Display the sent message + }); + } + } + + void _closeConnection() { + if (_channel != null) { + _channel!.sink.close(status.goingAway); + setState(() { + _channel = null; + _messages.clear(); + }); + } + } + + @override + void dispose() { + _closeConnection(); + _stopServer(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + + // force portrait mode + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + + return Scaffold( + appBar: AppBar( + title: Text('WebSocket Demo'), + actions: [ + IconButton( + icon: Icon(Icons.close), + onPressed: _closeConnection, + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + if (!_isServerRunning) + ElevatedButton( + onPressed: _startServer, + child: Text('Start WebSocket Server'), + ) + else + ElevatedButton( + onPressed: _stopServer, + child: Text('Stop WebSocket Server'), + ), + SizedBox(height: 10), + if (_localIpAddress != null) + Text('Server URL: ws://$_localIpAddress:8080'), + SizedBox(height: 10), + TextField( + controller: _urlController, + decoration: InputDecoration(labelText: 'WebSocket URL'), + onSubmitted: (value) => _joinWebSocket(), + ), + SizedBox(height: 10), + ElevatedButton( + onPressed: _joinWebSocket, + child: Text('Join WebSocket'), + ), + SizedBox(height: 10), + Expanded( + child: ListView.builder( + itemCount: _messages.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(_messages[index]), + ); + }, + ), + ), + TextField( + controller: _controller, + decoration: InputDecoration(labelText: 'Send a message'), + onSubmitted: (text) { + _sendMessage(text); + _controller.clear(); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/remaster/dashboard.dart b/lib/remaster/dashboard.dart index 2547c3e..8ecad6f 100644 --- a/lib/remaster/dashboard.dart +++ b/lib/remaster/dashboard.dart @@ -22,6 +22,7 @@ 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; @@ -65,7 +66,7 @@ class _HomePage_ReState extends State { print("Bundle: $shouldRedirectB"); print("Permissions_indv: $perms"); - return !shouldRedirectA || !shouldRedirectB; + return (!shouldRedirectA || !shouldRedirectB); } @@ -84,105 +85,261 @@ class _HomePage_ReState extends State { @override Widget build(BuildContext context) { + return Scaffold( body: Container( padding: const EdgeInsets.all(16), - alignment: Alignment.center, - child: SizedBox( + child: Column( - width: double.infinity, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, - child: Column( + children: [ - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + Text( + "Choose mode:", + style: ShadTheme.of(context).textTheme.h1.copyWith(), + ), - children: [ + SizedBox( + height: 16, + ), - const Text( - "Choose mode:", - style: TextStyle( - fontSize: 32, - fontWeight: FontWeight.w600, - ) - ), - - const SizedBox( - height: 16, - ), - - 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, + 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)" ), - - ShadButton.secondary( - onPressed: () { - Navigator.pushNamed(context, "/routes"); - }, - text: const Text("Continue"), - ) - - ], - ), - ), - - const SizedBox( - height: 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, + 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"), + ) + ], + ), + ), - ShadButton.secondary( - onPressed: () { - Navigator.pushNamed(context, "/multi"); - }, - 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"), + // ) + // + // ], + // ), + // ) + // + // + // + // ], + // + // ), + // ], + // ), + // ) + // + // ), + // ); } } @@ -194,92 +351,210 @@ class RoutePage extends StatelessWidget { @override Widget build(BuildContext context) { - - return Scaffold( - body: Column( + body: Row( + children: [ - Expanded( - child: Container( - padding: const EdgeInsets.all(16), + Container( + height: double.infinity, - alignment: Alignment.center, + child: Row( - child: SizedBox( + crossAxisAlignment: CrossAxisAlignment.end, - width: double.infinity, + children: [ - child: Column( - - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - - children: [ - - Row( - children: [ - - Text( - "Routes", - style: ShadTheme.of(context).textTheme.h1.copyWith(), - ), - - Expanded( - child: Container(), - ), - - ], + 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 ), - if (!kIsWeb) - Text( - "Nearby routes", - style: ShadTheme.of(context).textTheme.h4, + ), + ), + ), + + Container( + width: 230, + child: Scrollbar( + thumbVisibility: true, + child: GridView.count( + crossAxisCount: 2, + padding: const EdgeInsets.fromLTRB( + 4, + 4, + 4, + 4 ), - 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) - - - ], - + shrinkWrap: true, + children: [ + ..._getNearbyRoutes( + multiMode: ModalRoute.of(context)!.settings.name!.contains("multi") + ) + ], + ), ), ) - + ], ), ), - const Divider( - height: 1, + + Container( + width: 2, + height: double.infinity, + color: Colors.grey.shade400, ), - NavigationBar() + + 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() + // ], + // ), + // ); + } @@ -318,34 +593,42 @@ class _RouteSearchState extends State { children: [ - ShadInput( - placeholder: const Text("Search for a route..."), - controller: controller, - onChanged: (value) { - setState(() { + Padding( + padding: const EdgeInsets.fromLTRB( + 4, + 4, + 4, + 0 + ), + child: ShadInput( + placeholder: const Text("Search for a route..."), + controller: controller, + onChanged: (value) { + setState(() { - }); - }, - ), - - const SizedBox( - height: 4, + }); + }, + ), ), Expanded( child: Scrollbar( - interactive: true, - radius: const Radius.circular(8), - thickness: 8, thumbVisibility: true, - child: GridView.count( - crossAxisCount: 3, + trackVisibility: true, + scrollbarOrientation: ScrollbarOrientation.bottom, + child: GridView.extent( + // padding: const EdgeInsets.all(4), + scrollDirection: Axis.vertical, + maxCrossAxisExtent: 120, children: [ ...routes ], - shrinkWrap: true, ), ), + ), + + SizedBox( + height: 4, ) ], @@ -397,111 +680,100 @@ class RouteCard extends StatelessWidget { - return AspectRatio( - aspectRatio: 1, - child: Container( - child: ShadButton.secondary( - text: Column( - 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)) - ], + return ShadButton.secondary( + text: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Route \n ${route.routeNumber}", + style: ShadTheme.of(context).textTheme.h3.copyWith( + height: 1.1 + ) ), - padding: const EdgeInsets.all(8), - width: 105, - height: 105, + 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.bottom, - context: context, - builder: (context) { + onPressed: () { + showShadSheet( + side: ShadSheetSide.right, + context: context, + builder: (context) { - List variantWidgets = []; + 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) - ], + 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, ), - ), - 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"); - Navigator.pop(context); - ShadToaster.of(context).show( - ShadToast( - title: Text("Route selected"), - description: Text("Set route to ${variant.busRoute.routeNumber} - ${variant.busStops.first.formattedStopName} -> ${variant.busStops.last.formattedStopName}"), - duration: Duration(seconds: 5), - ) - ); - } - - }, - ) - ); - - variantWidgets.add(const SizedBox( - height: 4, - )); - } - - return ShadSheet( - title: Text("Route ${route.routeNumber} - Variants"), - - content: Container( - width: 2000, - constraints: const BoxConstraints( - maxHeight: 400 - ), - alignment: Alignment.center, - child: Scrollbar( - thumbVisibility: true, - child: SingleChildScrollView( - child: Column( - children: [ - ...variantWidgets - ], - ), + 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), - ); + ), + ), + padding: const EdgeInsets.all(8), + ); - } + } - ); - }, - ), - ), + ); + }, ); } @@ -826,89 +1098,96 @@ class _MultiModeSetupState extends State { children: [ - const Text( + Text( "Multi mode options:", - style: TextStyle( - fontSize: 32, - fontWeight: FontWeight.w600, - ) + style: ShadTheme.of(context).textTheme.h1.copyWith(), ), const SizedBox( height: 16, ), - ShadCard( - title: const Text("Host a group"), - width: double.infinity, - description: const Text( - "" - ), - content: Column( + Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, + mainAxisSize: MainAxisSize.min, - children: [ + children: [ - const SizedBox( - height: 4, + ShadCard( + title: const Text("Host a group"), + width: 300, + description: const Text( + "" ), + content: Column( - ShadButton.secondary( - onPressed: () async { + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, - LiveInformation liveInformation = LiveInformation(); + children: [ - Future.delayed(Duration.zero, () { - print("At time of loading: ${liveInformation.auth.status}"); + const SizedBox( + height: 4, + ), - if (liveInformation.auth.status != AuthStatus.AUTHENTICATED) { - Navigator.popAndPushNamed(context, "/multi/login"); - } - }); - await liveInformation.createRoom(liveInformation.auth.userID!); + 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"), - ) + Navigator.pushNamed(context, "/multi/enroute"); + }, + text: const Text("Continue"), + ) - ], - ), - ), - - const SizedBox( - height: 16, - ), - - ShadCard( - title: const Text("Join existing group"), - width: double.infinity, - 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"), - ) + 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"), + ) + + ], + ), + ) + + ], - ], - ), ) ], @@ -1491,63 +1770,37 @@ class _FullscreenDisplayState extends State { // Get the current screen orientation final Orientation orientation = MediaQuery.of(context).orientation; + return Scaffold( - // Make the screen landscape - SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeRight, - DeviceOrientation.landscapeLeft - ]); + body: Container( - return PopScope( - onPopInvoked: (isPop) { - if (isPop) { - // SystemChrome.setPreferredOrientations([ - // DeviceOrientation.portraitUp, - // DeviceOrientation.portraitDown - // ]); + color: Colors.black, + alignment: Alignment.center, - // Set the orientation back to whatever it was before - SystemChrome.setPreferredOrientations(orientation == Orientation.portrait ? [ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown - ] : [ - DeviceOrientation.landscapeRight, - DeviceOrientation.landscapeLeft - ]); - } - }, - child: Scaffold( - - body: Container( - - color: Colors.black, - alignment: Alignment.center, - - child: Row( - children: [ - Expanded( - child: ibus_display( - hasBorder: false, - ), + child: Row( + children: [ + Expanded( + child: ibus_display( + hasBorder: false, ), - Container( + ), + Container( - alignment: Alignment.bottomRight, - - child: ShadButton.ghost( - icon: const Icon(Icons.arrow_back), - padding: const EdgeInsets.all(8), - onPressed: () { - Navigator.pop(context); - }, - ), - ) - ], - ), + alignment: Alignment.bottomRight, + child: ShadButton.ghost( + icon: const Icon(Icons.arrow_back), + padding: const EdgeInsets.all(8), + onPressed: () { + Navigator.pop(context); + }, + ), + ) + ], ), ), + ); } } @@ -1887,7 +2140,7 @@ List _getNearbyRoutes({bool multiMode = false}) { if (kDebugMode) { - // currentVector = OSGrid.toNorthingEasting(51.583781262560926, -0.020359583104595073); + currentVector = OSGrid.toNorthingEasting(51.583781262560926, -0.020359583104595073); } for (BusRoute route in busSequences.routes.values) { diff --git a/lib/utils/audio wrapper.dart b/lib/utils/audio wrapper.dart index 8aee000..daf7ee4 100644 --- a/lib/utils/audio wrapper.dart +++ b/lib/utils/audio wrapper.dart @@ -25,7 +25,7 @@ class AudioWrapper { print("AudioWrapper mode: $mode"); - mode = AudioWrapper_Mode.Web; + // mode = AudioWrapper_Mode.Web; } justaudio.AudioSource _convertSource_JustAudio(AudioWrapperSource source){ diff --git a/lib/utils/web_socket_server.dart b/lib/utils/web_socket_server.dart new file mode 100644 index 0000000..81a6b42 --- /dev/null +++ b/lib/utils/web_socket_server.dart @@ -0,0 +1,38 @@ +import 'dart:isolate'; +import 'dart:async'; +import 'dart:io'; + +import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf/shelf_io.dart' as shelf_io; +import 'package:shelf_web_socket/shelf_web_socket.dart'; +import 'package:web_socket_channel/io.dart'; // Import IOWebSocketChannel + +void webSocketServer(SendPort sendPort) async { + final clients = []; // Use IOWebSocketChannel + + final handler = webSocketHandler((webSocket) { + clients.add(webSocket); + webSocket.stream.listen( + (message) { + print('Received: $message'); + for (var client in clients) { + if (client != webSocket) { + client.sink.add(message); // Broadcast the message to other clients + } + } + }, + onDone: () { + clients.remove(webSocket); + }, + ); + }); + + final server = await shelf_io.serve( + shelf.Pipeline().addMiddleware(shelf.logRequests()).addHandler(handler), + '0.0.0.0', + 8080, + ); + + print('WebSocket server running at ws://${server.address.host}:${server.port}'); + sendPort.send('Server running at ws://${server.address.host}:${server.port}'); +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 22bd301..059bf89 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -11,6 +11,7 @@ import device_info_plus import flutter_web_auth_2 import geolocator_apple import just_audio +import network_info_plus import package_info_plus import path_provider_foundation import rive_common @@ -27,6 +28,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) + NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) RivePlugin.register(with: registry.registrar(forPlugin: "RivePlugin")) diff --git a/pubspec.lock b/pubspec.lock index 48a77aa..4e228f9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -137,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" cookie_jar: dependency: transitive description: @@ -185,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.0.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" device_info_plus: dependency: transitive description: @@ -429,6 +445,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + http_methods: + dependency: transitive + description: + name: http_methods + sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566" + url: "https://pub.dev" + source: hosted + version: "1.1.1" http_parser: dependency: transitive description: @@ -573,6 +597,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" native_qr: dependency: "direct main" description: @@ -581,6 +613,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" + network_info_plus: + dependency: "direct main" + description: + name: network_info_plus + sha256: "5bd4b86e28fed5ed4e6ac7764133c031dfb7d3f46aa2a81b46f55038aa78ecc0" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + network_info_plus_platform_interface: + dependency: transitive + description: + name: network_info_plus_platform_interface + sha256: "2e193d61d3072ac17824638793d3b89c6d581ce90c11604f4ca87311b42f2706" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" ntp: dependency: "direct main" description: @@ -869,6 +925,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + shelf: + dependency: "direct main" + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_router: + dependency: "direct main" + description: + name: shelf_router + sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864 + url: "https://pub.dev" + source: hosted + version: "1.1.4" + shelf_static: + dependency: "direct main" + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: "direct main" + description: + name: shelf_web_socket + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + url: "https://pub.dev" + source: hosted + version: "2.0.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 22dc979..7f24537 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,7 +40,7 @@ dependencies: intl: any text_scroll: ^0.2.0 flutter_map: ^6.1.0 - appwrite: ^12.0.1 + appwrite: ^12.0.3 shared_preferences: ^2.2.2 url_launcher: ^6.2.2 ntp: ^2.0.0 @@ -56,6 +56,12 @@ dependencies: native_qr: ^0.0.3 qr_flutter: ^4.1.0 flutter_scroll_shadow: ^1.2.4 + network_info_plus: ^5.0.3 + shelf: ^1.4.1 + shelf_router: ^1.1.4 + shelf_static: ^1.1.2 + shelf_web_socket: ^2.0.0 +# web_socket_channel: ^3.0.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.