From 1f48f8f4b0fec411acce6d2444f60d264f84e5b2 Mon Sep 17 00:00:00 2001 From: ImBenji <53883070+YesItsBenji@users.noreply.github.com> Date: Fri, 3 May 2024 14:03:51 +0100 Subject: [PATCH] paradigm shift --- android/app/build.gradle | 4 +- android/app/src/main/AndroidManifest.xml | 5 + assets/datasets/bus-blinds.csv | 60 +- lib/auth/api_constants.dart | 4 +- lib/auth/auth_api.dart | 35 +- lib/backend/live_information.dart | 398 +++++- lib/backend/modules/announcement.dart | 69 +- lib/backend/modules/commands.dart | 117 +- lib/backend/modules/tracker.dart | 1 - lib/backend/modules/tube_info.dart | 2 +- lib/pages/home.dart | 508 +++---- lib/pages/settings.dart | 12 +- lib/remaster/RemasteredMain.dart | 10 + lib/remaster/dashboard.dart | 1615 ++++++++++++++++++++-- lib/workaround/keepalive_realtime.dart | 112 ++ pubspec.lock | 32 + pubspec.yaml | 3 + 17 files changed, 2440 insertions(+), 547 deletions(-) create mode 100644 lib/workaround/keepalive_realtime.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 13f2359..5821719 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -37,8 +37,8 @@ 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 flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion + minSdkVersion 21 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName multiDexEnabled = true diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8d040ad..ed02135 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -22,6 +22,8 @@ android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> + + @@ -32,6 +34,9 @@ + diff --git a/assets/datasets/bus-blinds.csv b/assets/datasets/bus-blinds.csv index 802245e..0141e14 100644 --- a/assets/datasets/bus-blinds.csv +++ b/assets/datasets/bus-blinds.csv @@ -58,7 +58,7 @@ Route,Blind,51.4309209,-0.0936496 5,"Upton Park, Boleyn",51.5305005,0.0384915 5,"Plaistow, Balaam Street",51.5226426,0.0223686 5,"Canning Town, Barking Road",51.53044569999999,0.038323 -5,Canning Town,51.5189494,0.0132 +5,Canning Town,51.51395705923875, 0.00827546234713445 6,"Willesden, Bus Garage",51.5474482,-0.2394372 6,Kensal Rise,51.5345071,-0.2250186 6,Queen's Park,51.5345448,-0.2043853 @@ -216,8 +216,8 @@ Route,Blind,51.4309209,-0.0936496 20,Whipps Cross,51.581499,0.0001219 20,Woodford Green,51.6092549,0.0405521 20,Woodford Wells,51.6148287,0.0282889 -20,Loughton,51.655942,0.068161 -20,Debden,51.6042352,0.04159529999999999 +20,Loughton,51.64195093723499, 0.05498357119784937 +20,Debden,51.645776846753535, 0.08200009471482733 21,"Lewisham, Shopping Centre",51.46115090000001,-0.0073177 21,"Lewisham, Jerrard Street",51.4650045,-0.0164722 21,New Cross Gate,51.4749904,-0.0403466 @@ -719,7 +719,7 @@ Route,Blind,51.4309209,-0.0936496 69,Stratford,51.5426313,-0.0010369 69,Plaistow,51.5268317,0.0308143 69,"Canning Town, Hermit Road",51.5209368,0.0125057 -69,Canning Town,51.5189494,0.0132 +69,Canning Town,51.51395705923875, 0.00827546234713445 70,"Chiswick, Business Park",51.4930604,-0.2748696 70,"Acton, High Street",51.5068505,-0.2673267 70,East Acton Lane,51.5123715,-0.2543317 @@ -1066,7 +1066,7 @@ Route,Blind,51.4309209,-0.0936496 108,Stratford International Bus Station,51.5453665,-0.0099022 108,Bow Church,51.5287753,-0.0167013 108,"Poplar, All Saints",51.5105521,-0.0118612 -108,Canning Town,51.5189494,0.0132 +108,Canning Town,51.51395705923875, 0.00827546234713445 108,North Greenwich,51.4859576,0.007494900000000001 108,East Greenwich,51.4309209,-0.0936496 108,"Blackheath, Royal Standard",51.4779946,0.0202001 @@ -1128,7 +1128,7 @@ Route,Blind,51.4309209,-0.0936496 115,"Upton Park, Boleyn",51.5305005,0.0384915 115,"Plaistow, Balaam street",51.5260131,0.0238468 115,"Canning Town, Barking Road",51.5175196,0.0115068 -115,Canning Town,51.5189494,0.0132 +115,Canning Town,51.51395705923875, 0.00827546234713445 115,"Poplar, All Saints",51.5105521,-0.0118612 115,"Limehouse, Burdett Road",51.5209123,-0.0327503 115,"Stepney, Arbour Square",51.5140186,-0.0481747 @@ -1412,7 +1412,7 @@ Route,Blind,51.4309209,-0.0936496 147,"East Ham, Newham Town Hall",51.53280239999999,0.0551608 147,"Upton Park, Boleyn",51.5305005,0.0384915 147,Prince Regent,51.5178274,0.0321034 -147,Canning Town,51.5189494,0.0132 +147,Canning Town,51.51395705923875, 0.00827546234713445 148,Camberwell Green,51.4756016,-0.0928896 148,Elephant & Castle,51.4938058,-0.0977932 148,Parliament Square,51.5010421,-0.1268514 @@ -1580,7 +1580,7 @@ Route,Blind,51.4309209,-0.0936496 167,"Barkingside, Fullwell Cross",51.59451199999999,0.08571999999999999 167,Gants Hill,51.5767812,0.0661732 167,Buckhurst Hill,51.627572,0.034513 -167,Loughton,51.655942,0.068161 +167,Loughton,51.64195093723499, 0.05498357119784937 168,Hampstead Heath,51.5608294,-0.1629416 168,Chalk Farm,51.5422732,-0.1466907 168,Camden Town,51.5390261,-0.1425516 @@ -1984,7 +1984,7 @@ Route,Blind,51.4309209,-0.0936496 212,Highams Park,51.6083754,0.0014712 212,"Walthamstow, Beacontree Avenue",51.595736,0.0045318 212,Walthamstow Central,51.5830128,-0.019886 -212,St James Street,51.5064993,-0.1393328 +212,St James Street,51.581118791593234, -0.0328839757764101 213,Kingston,51.4116616,-0.2080648 213,Kingston,51.4116616,-0.2080648 213,"New Malden, Coombe Road",51.4094312,-0.2586718 @@ -2202,7 +2202,7 @@ Route,Blind,51.4309209,-0.0936496 241,Plaistow,51.5268317,0.0308143 241,Plaistow Abbey Arms,51.5222296,0.0225853 241,Keir Hardie Estate,51.5678758,-0.0607466 -241,Canning Town,51.5189494,0.0132 +241,Canning Town,51.51395705923875, 0.00827546234713445 241,"Canning Town, Barking Road",51.5198077,0.0168462 242,Homerton Hospital,51.5478609,-0.0425903 242,"Clapton Park, Millfields",51.5575011,-0.0424329 @@ -2483,7 +2483,7 @@ Route,Blind,51.4309209,-0.0936496 274,Baker Street Station,51.5231548,-0.156863 274,Portman Square,51.5161534,-0.1560125 274,Marble Arch,51.5132225,-0.1588937 -275,St James Street,51.5064993,-0.1393328 +275,St James Street,51.581118791593234, -0.0328839757764101 275,Walthamstow Central,51.5830128,-0.019886 275,"Walthamstow, Beacontree Avenue",51.595736,0.0045318 275,Mill lane,51.5521985,-0.1932197 @@ -2685,7 +2685,7 @@ Route,Blind,51.4309209,-0.0936496 300,"Plaistow, Greengate Street",51.527444,0.027621 300,"Plaistow, Balaam Street",51.529212,0.0243676 300,"Canning Town, Barking Road",51.5208364,0.0191093 -300,Canning Town,51.5189494,0.0132 +300,Canning Town,51.51395705923875, 0.00827546234713445 302,Mill Hill Broadway,51.6129292,-0.2487871 302,Burnt Oak,51.602809,-0.266965 302,Burnt Oak,51.602809,-0.266965 @@ -2716,7 +2716,7 @@ Route,Blind,51.4309209,-0.0936496 308,Stratford City,51.5440354,-0.0053088 308,Homerton Hospital,51.5478609,-0.0425903 308,Clapton Pond,51.5561061,-0.05490830000000001 -309,Canning Town,51.5189494,0.0132 +309,Canning Town,51.51395705923875, 0.00827546234713445 309,Aberfeldy Estate,51.4309209,-0.0936496 309,"Poplar, All Saints",51.5105521,-0.0118612 309,"Poplar, Cordelia Street",51.5137347,-0.0174897 @@ -2805,7 +2805,7 @@ Route,Blind,51.4309209,-0.0936496 322,Brixton,51.4612794,-0.1156148 322,Clapham North,51.4658813,-0.1413263 322,Clapham Common,51.4589252,-0.1493071 -323,Canning Town,51.5189494,0.0132 +323,Canning Town,51.51395705923875, 0.00827546234713445 323,East London Mail Centre,51.55633,0.0655092 323,Mile End,51.52354529999999,-0.0330122 324,Stanmore Station,51.617676,-0.311451 @@ -2862,7 +2862,7 @@ Route,Blind,51.4309209,-0.0936496 330,"Upton Park, Boleyn",51.5305005,0.0384915 330,"Plaistow, Balaam Street",51.529212,0.0243676 330,"Canning Town, Barking Road",51.521274,0.0207211 -330,Canning Town,51.5189494,0.0132 +330,Canning Town,51.51395705923875, 0.00827546234713445 331,Ruislip,51.5758719,-0.421236 331,Ruislip Lido,51.59114049999999,-0.4304918 331,Northwood Station,51.6112297,-0.423889 @@ -3206,8 +3206,8 @@ Route,Blind,51.4309209,-0.0936496 397,"Crooked Billet, Sainsbury's",51.601088,-0.0159737 397,Chingford Mount,51.6185735,-0.0180318 397,Chingford Station,51.6331421,0.0098588 -397,Loughton,51.655942,0.068161 -397,Debden,51.5417031,0.2034589 +397,Loughton,51.64195093723499, 0.05498357119784937 +397,Debden,51.645776846753535, 0.08200009471482733 398,Ruislip,51.5758719,-0.421236 398,Rayners Lane Station,51.5751034,-0.3708618 398,South Harrow,51.5683717,-0.3553483 @@ -3521,7 +3521,7 @@ Route,Blind,51.4309209,-0.0936496 473,Plaistow,51.5268317,0.0308143 473,Stratford,51.5426313,-0.0010369 474,"Canning Town, Barking Road",51.52364720000001,0.0258453 -474,Canning Town,51.5189494,0.0132 +474,Canning Town,51.51395705923875, 0.00827546234713445 474,London City Airport,51.5048437,0.049518 474,North Woolwich,51.5008658,0.0626916 474,Cyprus,51.5091192,0.0633823 @@ -3658,7 +3658,7 @@ Route,Blind,51.4309209,-0.0936496 541,Prince Regent,51.4309209,-0.0936496 549,South Woodford,51.5912671,0.0264721 549,Buckhurst Hill,51.627572,0.034513 -549,Loughton,51.655942,0.068161 +549,Loughton,51.64195093723499, 0.05498357119784937 601,Thamesmead,51.50575809999999,0.1100586 601,Bexley,51.439933,0.154327 601,Wilmington Schools,51.4309209,-0.0936496 @@ -3803,7 +3803,7 @@ Route,Blind,51.4309209,-0.0936496 673,Beckton Station,51.5144016,0.06153319999999999 674,"Harold Hill, Dagnam Park Square",51.6049149,0.2445527 674,Romford Station,51.57472449999999,0.1826519 -675,St James Street,51.5064993,-0.1393328 +675,St James Street,51.581118791593234, -0.0328839757764101 675,Woodbridge School,51.4309209,-0.0936496 678,Stratford,51.5426313,-0.0010369 678,NO BLIND DESCRIPTION (Departs only),51.4309209,-0.0936496 @@ -4016,7 +4016,7 @@ EL1,Barking,51.536563,0.075766 EL1,Barking,51.536563,0.075766 EL1,"River Road, Waverley Gardens",51.5343317,-0.2891878 EL1,"River Road, Waverley Gardens",51.5343317,-0.2891878 -EL1,Barking Riverside,51.536563,0.075766 +EL1,Barking Riverside,51.519303731473705, 0.11590257503355633 EL2,Becontree Heath,51.5609465,0.1488995 EL2,Five Elms,51.3697855,0.0259964 EL2,Bennett's Castle Lane,51.5562949,0.1276031 @@ -4031,7 +4031,7 @@ EL3,"Goodmayes, Goodmayes Lane",51.5620979,0.1095257 EL3,Barking,51.536563,0.075766 EL3,Creekmouth,51.517381,0.102234 EL3,Barking,51.536563,0.075766 -EL3,Barking Riverside,51.536563,0.075766 +EL3,Barking Riverside,51.519303731473705, 0.11590257503355633 G1,"Streatham, Green Lane",51.4147104,-0.1158693 G1,"Streatham, St Leonard's Church",51.4307467,-0.1294977 G1,Tooting Broadway,51.427867,-0.1678142 @@ -4411,7 +4411,7 @@ UL10,Hackney Downs,51.5553095,-0.06908249999999999 UL10,Liverpool Street,51.5175001,-0.0826966 UL11,Canary Wharf,51.5054306,-0.0235333 UL11,Stratford,51.5426313,-0.0010369 -UL12,Loughton,51.655942,0.068161 +UL12,Loughton,51.64195093723499, 0.05498357119784937 UL12,Snaresbrook,51.58567859999999,0.0084531 UL12,Leyton,51.5702225,-0.0146938 UL12,Stratford,51.5426313,-0.0010369 @@ -4438,7 +4438,7 @@ UL19,Wembley Central,51.550501,-0.3048409 UL19,Stonebridge Park,51.5445824,-0.2608244 UL19,Queen's Park,51.5345448,-0.2043853 UL20,Tower Hill,51.5095757,-0.0760083 -UL20,Canning Town,51.5189494,0.0132 +UL20,Canning Town,51.51395705923875, 0.00827546234713445 UL20,Stratford,51.5426313,-0.0010369 UL20,Stratford,51.5426313,-0.0010369 UL20,East Ham,51.5333972,0.04991139999999999 @@ -4540,7 +4540,7 @@ W11,"Walthamstow, Crooked Billet",51.601088,-0.0159737 W11,Walthamstow Central,51.5830128,-0.019886 W12,"Walthamstow, Coppermill Lane",51.5803038,-0.0403725 W12,Walthamstow Central,51.5830128,-0.019886 -W12,St James Street,51.5064993,-0.1393328 +W12,St James Street,51.581118791593234, -0.0328839757764101 W12,Whipps Cross,51.5095281,-0.229236 W12,South Woodford,51.5095281,-0.229236 W12,Wanstead,51.5767971,0.0249881 @@ -4559,7 +4559,7 @@ W14,"Leytonstone, Harrow Green",51.5582955,0.007356 W14,Leyton,51.4964278,-0.2085211 W14,"Leyton, Superstores",51.5558965,-0.0093311 W15,"Higham Hill, Cogan Avenue",51.6012336,-0.0363719 -W15,"Forest Road, Palmerston Road",51.5858953,-0.0292291 +W15,"Forest Road, Palmerston Road",51.588898351903815, -0.030623848707334166 W15,Walthamstow Central,51.5830128,-0.019886 W15,"Leyton, Bakers Arms",51.5749185,-0.013549 W15,Whipps Cross,51.581499,0.0001219 @@ -4576,7 +4576,7 @@ W16,"Walthamstow, Wood Street",51.5864747,-0.0026968 W16,"Leyton, Bakers Arms",51.5749185,-0.013549 W16,Leytonstone,51.5649624,0.0088141 W19,"Walthamstow, Argall Avenue",51.5701165,-0.0387872 -W19,St James Street,51.5064993,-0.1393328 +W19,St James Street,51.581118791593234, -0.0328839757764101 W19,Walthamstow Central,51.5830128,-0.019886 W19,Whipps Cross,51.4255297,-0.2050566 W19,Leytonstone,51.5649624,0.0088141 @@ -4704,7 +4704,7 @@ N15,Barking,51.5833519,-0.07649299999999999 N15,"East Ham, Newham Town Hall",51.53280239999999,0.0551608 N15,Upton Park,51.53471750000001,0.0337596 N15,"Canning Town, Barking Road",51.5180017,0.0130984 -N15,Canning Town,51.5189494,0.0132 +N15,Canning Town,51.51395705923875, 0.00827546234713445 N15,"Poplar, All Saints",51.5105521,-0.0118612 N15,"Limehouse, Burdett Road",51.5150014,-0.0285662 N15,Aldgate,51.5134365,-0.0772463 @@ -5151,13 +5151,13 @@ N550,Aldgate,51.5134365,-0.0772463 N550,Cannon Street,51.5119949,-0.091962 N550,"Limehouse, Burdett Road",51.5150014,-0.0285662 N550,"Poplar, All Saints",51.5105521,-0.0118612 -N550,Canning Town,51.5189494,0.0132 +N550,Canning Town,51.51395705923875, 0.00827546234713445 N551,Trafalgar Square,51.508039,-0.128069 N551,Aldwych,51.5132441,-0.1172819 N551,Aldgate,51.5134365,-0.0772463 N551,Limehouse,51.5110598,-0.0366652 N551,"Poplar, All Saints",51.5105521,-0.0118612 -N551,Canning Town,51.5189494,0.0132 +N551,Canning Town,51.51395705923875, 0.00827546234713445 N551,Keir Hardie Estate,51.5678758,-0.0607466 N551,Prince Regent,51.4309209,-0.0936496 N551,Beckton Station,51.5144016,0.06153319999999999 diff --git a/lib/auth/api_constants.dart b/lib/auth/api_constants.dart index 559eb5e..5af85a3 100644 --- a/lib/auth/api_constants.dart +++ b/lib/auth/api_constants.dart @@ -1,8 +1,8 @@ class ApiConstants { - static const String APPWRITE_ENDPOINT = "https://cloud.imbenji.net/v1"; - static const String APPWRITE_PROJECT_ID = "65de530c1c0a7ffc0c3f"; + static const String APPWRITE_ENDPOINT = "https://cloud.appwrite.io/v1"; + static const String APPWRITE_PROJECT_ID = "6633d0e00023502890ed"; static const String INFO_Q_DATABASE_ID = "65de5cab16717444527b"; static const String MANUAL_Q_COLLECTION_ID = "65de9f2f925562a2eda8"; diff --git a/lib/auth/auth_api.dart b/lib/auth/auth_api.dart index 35afc2e..057094f 100644 --- a/lib/auth/auth_api.dart +++ b/lib/auth/auth_api.dart @@ -21,6 +21,7 @@ class AuthAPI extends ChangeNotifier { late final appwrite.Account account; late models.User _currentUser; + late models.Session _currentSession; AuthStatus _status = AuthStatus.UNINITIALIZED; @@ -32,11 +33,16 @@ class AuthAPI extends ChangeNotifier { AuthStatus get status => _status; String? get username => _currentUser.name; String? get email => _currentUser.email; - String? get userID => _currentUser.$id; + String? get userID { + try { + return _currentUser.$id; + } catch (e) { + return _currentSession.$id; + } + } - AuthAPI() { + AuthAPI({bool autoLoad = true}) { init(); - loadUser(); } init() { @@ -45,6 +51,29 @@ class AuthAPI extends ChangeNotifier { .setProject(ApiConstants.APPWRITE_PROJECT_ID) .setSelfSigned(); account = appwrite.Account(client); + try { + account.get().then((value) { + _currentUser = value; + _status = AuthStatus.AUTHENTICATED; + print("Auto loaded user: ${_currentUser.name}"); + print("Auth status: $_status"); + }); + } catch (e) { + + } + } + + loadAnonymousUser() async { + try { + final user = await account.createAnonymousSession(); + _currentSession = user; + _status = AuthStatus.AUTHENTICATED; + + } catch (e) { + _status = AuthStatus.UNAUTHENTICATED; + } finally { + notifyListeners(); + } } loadUser() async { diff --git a/lib/backend/live_information.dart b/lib/backend/live_information.dart index 9c6dc59..e919dc9 100644 --- a/lib/backend/live_information.dart +++ b/lib/backend/live_information.dart @@ -16,6 +16,7 @@ import 'package:bus_infotainment/backend/modules/tube_info.dart'; import 'package:bus_infotainment/tfl_datasets.dart'; import 'package:bus_infotainment/utils/audio%20wrapper.dart'; import 'package:bus_infotainment/utils/delegates.dart'; +import 'package:bus_infotainment/workaround/keepalive_realtime.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; @@ -62,12 +63,6 @@ class LiveInformation { print("Loading tube stations from assets"); tubeStations = TubeStations.fromJson(json.decode(await rootBundle.loadString("assets/datasets/tube_stations.json"))); print("Loaded tube stations from assets"); - - - String sessionID = "test"; - - commandModule = CommandModule(sessionID); - } // Initialise modules @@ -77,6 +72,9 @@ class LiveInformation { initTrackerModule(); print("Initialised LiveInformation"); + if (!auth.isAuthenticated()) { + auth.loadAnonymousUser(); + } } Future initTrackerModule() async { @@ -86,10 +84,18 @@ class LiveInformation { } // Auth - AuthAPI auth = AuthAPI(); + AuthAPI auth = AuthAPI( + autoLoad: false, + ); + String? roomCode; + String? roomDocumentID; + bool isHost = false; + appwrite.RealtimeSubscription? _subscription; + RealtimeKeepAliveConnection? _keepAliveConnection; // This is a workaround for a bug in the appwrite SDK + // Modules - late CommandModule commandModule; + // late CommandModule commandModule; This needs to be deprecated late BusSequences busSequences; late AnnouncementModule announcementModule; late SyncedTimeModule syncedTimeModule; @@ -100,7 +106,7 @@ class LiveInformation { BusRouteVariant? _currentRouteVariant; // Events - EventDelegate routeVariantDelegate = EventDelegate(); + EventDelegate routeVariantDelegate = EventDelegate(); // Internal methods @@ -108,7 +114,54 @@ class LiveInformation { - Future setRouteVariant_Internal(BusRouteVariant routeVariant) async { + Future setRouteVariant(BusRouteVariant? routeVariant) async { + + if (routeVariant == null) { + _currentRouteVariant = null; + + await announcementModule.queueAnnounceByAudioName( + displayText: "*** NO MESSAGE ***", + ); + + routeVariantDelegate.trigger(null); + return; + } + + if (roomCode != null) { + try { + final client = auth.client; + final databases = appwrite.Databases(client); + + final response = await databases.listDocuments( + databaseId: "6633e85400036415ab0f", + collectionId: "6633e85d0020f52f3771", + queries: [ + appwrite.Query.search("SessionID", roomCode!) + ] + ); + + final document = response.documents.first; + + // Check if the route is not the same + if (document.data["CurrentRoute"] != routeVariant.busRoute.routeNumber || document.data["CurrentRouteVariant"] != routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant)) { + final updatedDocument = await databases.updateDocument( + databaseId: "6633e85400036415ab0f", + collectionId: "6633e85d0020f52f3771", + documentId: document.$id, + data: { + "CurrentRoute": routeVariant.busRoute.routeNumber, + "CurrentRouteVariant": routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant), + "LastUpdater": auth.userID, + } + ); + print("Updated route on server"); + } + } catch (e) { + print("Failed to update route on server"); + } + + } + Continuation: // Set the current route variant _currentRouteVariant = routeVariant; @@ -135,6 +188,10 @@ class LiveInformation { ] ); + + // Display the route variant + announcementModule.queueAnnouncementByRouteVariant(routeVariant: routeVariant); + } // Public methods @@ -143,16 +200,329 @@ class LiveInformation { return _currentRouteVariant; } - Future setRouteVariant(BusRouteVariant routeVariant) async { - await commandModule.executeCommand( - "setroute ${routeVariant.busRoute.routeNumber} ${routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant)}" + Future setRouteVariantQuery(String routeNumber, int routeVariantIndex) async { + BusRoute route = busSequences.routes[routeNumber]!; + BusRouteVariant routeVariant = route.routeVariants.values.toList()[routeVariantIndex]; + + await setRouteVariant( + routeVariant ); } +// 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 + ); + } + + // 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 + // { 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"); + } + } + + 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"); + + // 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 + // { + // 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 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"); + } + + print("Joined room with code $roomCode"); + } + + String? lastCommand; + Future ServerListener(appwrite.RealtimeMessage response) async { + print("Session update"); + + // Only do something if the document was created or updated + if (!(response.events.first.contains("create") || response.events.first.contains("update"))) { + return; + } + + // Get the user that caused the event + + String senderID = response.payload["LastUpdater"]; + // If the sender is the same as the client, then ignore the event + if (senderID == auth.userID) { + print("Ignoring event"); + return; + } + + // Check to see if the commands are updated + + try { + // Get the new route + String routeNumber = response.payload["CurrentRoute"]; + int routeVariantIndex = response.payload["CurrentRouteVariant"]; + + // 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); + + // announce the route + // announcementModule.queueAnnouncementByRouteVariant(routeVariant: _currentRouteVariant!); + } + } catch (e) { + print("Failed to set route"); + } + + // Execute the command + List commands = response.payload["Commands"].cast(); + + String? command = commands.last; + + if (command == lastCommand) { + return; + } + lastCommand = command; + + List commandParts = _splitCommand(command); + String commandName = commandParts[0]; + + if (commandName == "announce") { + print("Announce command received"); + String mode = commandParts[1]; + + print ("Command: $command"); + + if (mode == "info") { + print("Announce info command received"); + announcementModule.queueAnnounementByInfoIndex( + sendToServer: false, + infoIndex: int.parse(commandParts[2]), + scheduledTime: DateTime.fromMillisecondsSinceEpoch(int.parse(commandParts[3])), + ); + } else if (mode == "dest") { + print("Announce dest command received"); + + String routeNumber = commandParts[2]; + int routeVariantIndex = int.parse(commandParts[3]); + + announcementModule.queueAnnouncementByRouteVariant( + sendToServer: false, + routeVariant: busSequences.routes[routeNumber]!.routeVariants.values.toList()[routeVariantIndex], + scheduledTime: DateTime.fromMillisecondsSinceEpoch(int.parse(commandParts[4])), + ); + + } else if (mode == "manual") { + print("Announce manual command received"); + + final displayText = commandParts[2]; + + List audioFileNames = commandParts.sublist(3); + try { + if (int.parse(audioFileNames.last) != null) { + audioFileNames.removeLast(); + } + } catch (e) {} + + DateTime scheduledTime = LiveInformation().syncedTimeModule.Now().add(Duration(seconds: 1)); + try { + if (int.parse(commandParts.last) != null) { + scheduledTime = DateTime.fromMillisecondsSinceEpoch(int.parse(commandParts.last)); + } + } catch (e) {} + + announcementModule.queueAnnounceByAudioName( + displayText: displayText, + audioNames: audioFileNames, + scheduledTime: scheduledTime, + sendToServer: false + ); + + } + } + } + + String _extractId(String input) { + RegExp regExp = RegExp(r'\("user:(.*)"\)'); + Match match = regExp.firstMatch(input)!; + return match.group(1)!; + } + + Future SendCommand(String command) async { + + 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, + } + ); + } + + List _splitCommand(String command) { + var regex = RegExp(r'([^\s"]+)|"([^"]*)"'); + var matches = regex.allMatches(command); + return matches.map((match) => match.group(0)!.replaceAll('"', '')).toList(); + } @@ -162,8 +532,6 @@ class LiveInformation { - - } class AnnouncementQueueEntry { diff --git a/lib/backend/modules/announcement.dart b/lib/backend/modules/announcement.dart index ea92535..fa47347 100644 --- a/lib/backend/modules/announcement.dart +++ b/lib/backend/modules/announcement.dart @@ -74,7 +74,7 @@ class AnnouncementModule extends InfoModule { final EventDelegate onAnnouncement = EventDelegate(); // Timer - Timer refreshTimer() => Timer.periodic(const Duration(milliseconds: 200), (timer) async { + Timer refreshTimer() => Timer.periodic(const Duration(milliseconds: 10), (timer) async { if (!isPlaying) { @@ -84,7 +84,7 @@ class AnnouncementModule extends InfoModule { bool proceeding = await _internalAccountForInconsistentTime( announcement: nextAnnouncement, - timerInterval: const Duration(milliseconds: 200), + timerInterval: const Duration(milliseconds: 10), callback: () { queue.removeAt(0); print("Announcement proceeding"); @@ -105,35 +105,21 @@ class AnnouncementModule extends InfoModule { if (currentAnnouncement!.audioSources.isNotEmpty) { - // audioPlayer.loadSource(AudioWrapperAssetSource("assets/audio/5-seconds-of-silence.mp3")); - // audioPlayer.play(); - // await Future.delayed(const Duration(milliseconds: 300)); - // audioPlayer.stop(); + // Prime all of the audio sources to be ready to play + for (AudioWrapperSource source in currentAnnouncement!.audioSources) { + try { + await audioPlayer.loadSource(source); + await Future.delayed((await audioPlayer.play())!); + audioPlayer.stop(); - // try { - for (AudioWrapperSource source in currentAnnouncement!.audioSources) { - try { - await audioPlayer.loadSource(source); - - Duration? duration = await audioPlayer.play(); - await Future.delayed(duration!); - audioPlayer.stop(); - // await Future.delayed(const Duration(milliseconds: 100)); - if (currentAnnouncement?.audioSources.last != source) { - await Future.delayed(const Duration(milliseconds: 100)); - } - } catch (e) { - // Do nothing - // print("Error playing announcement: $e on ${currentAnnouncement?.displayText}"); - await Future.delayed(const Duration(seconds: 1)); + if (currentAnnouncement?.audioSources.last != source) { + await Future.delayed(const Duration(milliseconds: 100)); } + } catch (e) { + await Future.delayed(const Duration(seconds: 1)); } - // audioPlayer.stop(); + } - // } catch (e) { - // // Do nothing - // print("Error playing announcement: $e on ${currentAnnouncement?.displayTex}"); - // } } else { if (queue.isNotEmpty) { await Future.delayed(const Duration(seconds: 5)); @@ -179,7 +165,7 @@ class AnnouncementModule extends InfoModule { } // Configuration - int get defaultAnnouncementDelay => liveInformation.auth.isAuthenticated() ? 2 : 0; + int get defaultAnnouncementDelay => liveInformation.auth.isAuthenticated() ? 1 : 0; // Methods Future queueAnnounceByAudioName({ @@ -199,8 +185,12 @@ class AnnouncementModule extends InfoModule { audioNamesString += "\"$audioName\" "; } - liveInformation.commandModule.executeCommand( - "announce manual \"$displayText\" ${audioNamesString} ${scheduledTime?.millisecondsSinceEpoch ?? ""}" + liveInformation.SendCommand("announce manual \"$displayText\" $audioNamesString ${scheduledTime.millisecondsSinceEpoch}"); + queueAnnounceByAudioName( + displayText: displayText, + audioNames: audioNames, + scheduledTime: scheduledTime, + sendToServer: false ); return; @@ -244,9 +234,13 @@ class AnnouncementModule extends InfoModule { scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay)); - liveInformation.commandModule.executeCommand( - "announce info $infoIndex ${scheduledTime?.millisecondsSinceEpoch ?? ""}" + liveInformation.SendCommand("announce info $infoIndex ${scheduledTime.millisecondsSinceEpoch}"); + queueAnnounementByInfoIndex( + infoIndex: infoIndex, + scheduledTime: scheduledTime, + sendToServer: false ); + print("Announcement sent to server"); return; } @@ -270,9 +264,16 @@ class AnnouncementModule extends InfoModule { scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay)); - liveInformation.commandModule.executeCommand( - "announce dest \"${routeVariant.busRoute.routeNumber}\" ${routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant)} ${scheduledTime?.millisecondsSinceEpoch ?? ""}" + String routeNumber = routeVariant.busRoute.routeNumber; + int routeVariantIndex = routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant); + + liveInformation.SendCommand("announce dest ${routeNumber} ${routeVariantIndex} ${scheduledTime.millisecondsSinceEpoch}"); + queueAnnouncementByRouteVariant( + routeVariant: routeVariant, + scheduledTime: scheduledTime, + sendToServer: false ); + return; } print("Checkpoint 4"); diff --git a/lib/backend/modules/commands.dart b/lib/backend/modules/commands.dart index 5be6b25..c19f1dd 100644 --- a/lib/backend/modules/commands.dart +++ b/lib/backend/modules/commands.dart @@ -47,17 +47,37 @@ class CommandModule extends InfoModule { final databases = appwrite.Databases(client); - if (liveInformation.auth.status == AuthStatus.AUTHENTICATED) { - final document = await databases.createDocument( - databaseId: ApiConstants.INFO_Q_DATABASE_ID, - collectionId: ApiConstants.COMMANDS_COLLECTION_ID, - documentId: appwrite.ID.unique(), + if (true) { + try { + final response = await databases.listDocuments( + databaseId: "6633e85400036415ab0f", + collectionId: "6633e85d0020f52f3771", + queries: [ + appwrite.Query.search("SessionID", liveInformation.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: liveInformation.roomDocumentID!, data: { - "session_id": sessionID, - "command": command, - "client_id": clientID, + "Commands": pastCommands, + "LastUpdater": clientID, } - ); + ); + } catch (e) { + print("Failed to send command"); + + } } _onCommandReceived(CommandInfo(command, clientID)); @@ -78,6 +98,10 @@ class CommandModule extends InfoModule { if (command == "Response:") { + } + else if (command == "initroom") { + // initroom + } else if (command == "announce") { @@ -166,10 +190,41 @@ class CommandModule extends InfoModule { BusRoute route = liveInformation.busSequences.routes[routeNumber]!; BusRouteVariant routeVariant = route.routeVariants.values.toList()[routeVariantIndex]; - liveInformation.setRouteVariant_Internal( + liveInformation.setRouteVariant( routeVariant ); - executeCommand("Response: v \"Client $clientID set its route to ($routeNumber to ${routeVariant.busStops.last.formattedStopName})\""); + + + // Update the server + if (liveInformation.isHost) { + print("Updating server"); + final client = liveInformation.auth.client; + final databases = appwrite.Databases(client); + + final response = await databases.listDocuments( + databaseId: "6633e85400036415ab0f", + collectionId: "6633e85d0020f52f3771", + queries: [ + appwrite.Query.search("SessionID", liveInformation.roomCode!) + ] + ); + + final document = await databases.updateDocument( + databaseId: "6633e85400036415ab0f", + collectionId: "6633e85d0020f52f3771", + documentId: response.documents.first.$id, + data: { + "CurrentRoute": routeNumber, + "CurrentRouteVariant": routeVariantIndex, + } + ); + try { + + print("Updated server"); + } catch (e) { + print("Failed to update server"); + } + } } @@ -181,26 +236,26 @@ class CommandModule extends InfoModule { return; } - final realtime = appwrite.Realtime(LiveInformation().auth.client); - - _subscription = realtime.subscribe( - ['databases.${ApiConstants.INFO_Q_DATABASE_ID}.collections.${ApiConstants.COMMANDS_COLLECTION_ID}.documents'] - ); - _subscription!.stream.listen((event) { - print(jsonEncode(event.payload)); - - // Only do something if the document was created or updated - if (!(event.events.first.contains("create") || event.events.first.contains("update"))) { - return; - } - - final commandInfo = CommandInfo(event.payload['command'], event.payload['client_id']); - - if (commandInfo.clientID != clientID) { - _onCommandReceived(commandInfo); - } - - }); + // final realtime = appwrite.Realtime(LiveInformation().auth.client); + // + // _subscription = realtime.subscribe( + // ['databases.${ApiConstants.INFO_Q_DATABASE_ID}.collections.${ApiConstants.COMMANDS_COLLECTION_ID}.documents'] + // ); + // _subscription!.stream.listen((event) { + // print(jsonEncode(event.payload)); + // + // // Only do something if the document was created or updated + // if (!(event.events.first.contains("create") || event.events.first.contains("update"))) { + // return; + // } + // + // final commandInfo = CommandInfo(event.payload['command'], event.payload['client_id']); + // + // if (commandInfo.clientID != clientID) { + // _onCommandReceived(commandInfo); + // } + // + // }); print("Listening for commands"); diff --git a/lib/backend/modules/tracker.dart b/lib/backend/modules/tracker.dart index 8d31ad9..d09c041 100644 --- a/lib/backend/modules/tracker.dart +++ b/lib/backend/modules/tracker.dart @@ -183,7 +183,6 @@ class TrackerModule extends InfoModule { print("Closest stop: ${closestStop.formattedStopName} in ${closestDistance.round()} meters"); } - } double _calculateRelativeDistance(BusRouteStop stop, double latitude, double longitude) { diff --git a/lib/backend/modules/tube_info.dart b/lib/backend/modules/tube_info.dart index 18291b1..32e167d 100644 --- a/lib/backend/modules/tube_info.dart +++ b/lib/backend/modules/tube_info.dart @@ -68,7 +68,7 @@ class TubeStations { double distance = Vector2(stop.easting.toDouble(), stop.northing.toDouble()).distanceTo(OSGrid.toNorthingEasting(station.latitude, station.longitude)); // if the distance is less than 100m, then we can assume that the bus stop is near the tube station - if (distance < 200) { + if (distance < 400) { for (TubeLine line in station.lines) { lineMatches[line] = lineMatches[line]! + 1; } diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 7945bed..aa09c84 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -11,6 +11,7 @@ import 'package:bus_infotainment/utils/delegates.dart'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; class pages_Home extends StatelessWidget { const pages_Home({super.key}); @@ -60,7 +61,7 @@ class pages_Home extends StatelessWidget { outlineColor: Colors.white70, announcements: [ for (NamedAnnouncementQueueEntry announcement in LiveInformation().announcementModule.manualAnnouncements) - _AnnouncementEntry( + AnnouncementEntry( label: announcement.shortName, index: LiveInformation().announcementModule.manualAnnouncements.indexOf(announcement), outlineColor: Colors.white70, @@ -92,12 +93,12 @@ class pages_Home extends StatelessWidget { color: Colors.grey.shade900, ), - child: DelegateBuilder( + child: DelegateBuilder( delegate: LiveInformation().routeVariantDelegate, builder: (context, routeVariant) { print("rebuilt stop announcement picker"); return StopAnnouncementPicker( - routeVariant: routeVariant, + routeVariant: routeVariant!, backgroundColor: Colors.grey.shade900, outlineColor: Colors.white70, ); @@ -133,11 +134,6 @@ class pages_Home extends StatelessWidget { ElevatedButton( onPressed: () async { LiveInformation liveInformation = LiveInformation(); - final commandModule = liveInformation.commandModule; - - // commandModule.executeCommand( - // "announce dest" - // ); liveInformation.announcementModule.queueAnnouncementByRouteVariant( routeVariant: liveInformation.getRouteVariant()! @@ -147,75 +143,7 @@ class pages_Home extends StatelessWidget { child: Text("Announce current destination"), ), - - // Container( - // - // margin: EdgeInsets.all(20), - // - // height: 300-45, - // - // child: ListView( - // - // scrollDirection: Axis.vertical, - // - // children: [ - // - // ElevatedButton( - // onPressed: () async { - // LiveInformation liveInformation = LiveInformation(); - // liveInformation.queueAnnouncement(await liveInformation.getDestinationAnnouncement(liveInformation.getRouteVariant()!, sendToServer: false)); - // }, - // child: Text("Test announcement"), - // ), - // - // ElevatedButton( - // onPressed: () { - // LiveInformation liveInformation = LiveInformation(); - // liveInformation.updateServer(); - // }, - // child: Text("Update server"), - // ), - // - // SizedBox( - // - // width: 100, - // - // child: TextField( - // onChanged: (String value) { - // LiveInformation liveInformation = LiveInformation(); - // // liveInformation.documentID = value; - // }, - // ), - // ), - // - // SizedBox( - // - // width: 200, - // - // child: TextField( - // onSubmitted: (String value) { - // LiveInformation liveInformation = LiveInformation(); - // liveInformation.queueAnnouncement(AnnouncementQueueEntry( - // displayText: value, - // audioSources: [] - // )); - // }, - // ), - // ), - // - // ElevatedButton( - // onPressed: () { - // LiveInformation liveInformation = LiveInformation(); - // liveInformation.pullServer(); - // }, - // child: Text("Pull server"), - // ), - // - // ], - // - // ), - // - // ), + ], ), @@ -366,8 +294,9 @@ class AnnouncementPicker extends StatefulWidget { final Color backgroundColor; final Color outlineColor; final List announcements; + final String label; - const AnnouncementPicker({super.key, required this.backgroundColor, required this.outlineColor, required this.announcements}); + const AnnouncementPicker({super.key, required this.backgroundColor, required this.outlineColor, required this.announcements, this.label = ""}); @override State createState() => _AnnouncementPickerState(); @@ -411,9 +340,9 @@ class _AnnouncementPickerState extends State { color: widget.backgroundColor, border: Border.all( color: widget.outlineColor, - width: 2 + width: 1 ), - + borderRadius: BorderRadius.circular(8) ), @@ -428,118 +357,26 @@ class _AnnouncementPickerState extends State { child: Column( children: [ - Container( - height: 2, - color: widget.outlineColor, - ), - - if (_currentIndex < announcementWidgets.length) - announcementWidgets[_currentIndex + 0] - else - Container( - height: 50, - decoration: BoxDecoration( - color: widget.backgroundColor, - border: Border.symmetric( - vertical: BorderSide( - color: widget.outlineColor, - width: 2 - ) - ), - ), - ), Container( - height: 2, - color: widget.outlineColor, - ), - - if (_currentIndex + 1 < announcementWidgets.length) - announcementWidgets[_currentIndex + 1] - else - Container( - height: 50, - decoration: BoxDecoration( - color: widget.backgroundColor, - border: Border.symmetric( - vertical: BorderSide( - color: widget.outlineColor, - width: 2 - ) - ), - ), - ), - - Container( - height: 2, - color: widget.outlineColor, - ), - - if (_currentIndex + 2 < announcementWidgets.length) - announcementWidgets[_currentIndex + 2] - else - Container( - height: 50, - decoration: BoxDecoration( - color: widget.backgroundColor, - border: Border.symmetric( - vertical: BorderSide( - color: widget.outlineColor, - width: 2 - ) - ), - ), - ), - - Container( - height: 2, - color: widget.outlineColor, - ), - - if (_currentIndex + 3 < announcementWidgets.length) - announcementWidgets[_currentIndex + 3] - else - Container( - height: 50, - decoration: BoxDecoration( - color: widget.backgroundColor, - border: Border.symmetric( - vertical: BorderSide( - color: widget.outlineColor, - width: 2 - ) - ), - ), - ), - - Container( - height: 2, - color: widget.outlineColor, - ), - - Container( - height: 40, decoration: BoxDecoration( color: widget.backgroundColor, - border: Border.symmetric( - vertical: BorderSide( - color: widget.outlineColor, - width: 2 - ) + border: Border.all( + color: widget.outlineColor, + width: 1 ), + borderRadius: BorderRadius.circular(4) ), - - alignment: Alignment.centerRight, - - child: Row( - - mainAxisSize: MainAxisSize.min, - + // height: 100, + child: Column( children: [ - Container( - width: 40, - height: 40, + + if (_currentIndex < announcementWidgets.length) + announcementWidgets[_currentIndex + 0] + else + Container( + height: 50, decoration: BoxDecoration( color: widget.backgroundColor, border: Border.symmetric( @@ -549,50 +386,18 @@ class _AnnouncementPickerState extends State { ) ), ), - - margin: const EdgeInsets.symmetric( - horizontal: 4 - ), - - child: Container( - child: Stack( - children: [ - Container( - width: 40, - height: 40, - child: Icon( - Icons.arrow_upward, - color: widget.outlineColor, - ), - ), - Positioned.fill( - child: ElevatedButton( - onPressed: () { - _currentIndex = wrap(_currentIndex - 4, 0, announcementWidgets.length, increment: 4); - setState(() {}); - print(_currentIndex); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - surfaceTintColor: Colors.transparent, - foregroundColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(0), - ), - ), - child: const Text(""), - ), - ) - ], - ), - ) - - ), + ), Container( - width: 40, - height: 40, + height: 1, + color: widget.outlineColor, + ), + + if (_currentIndex + 1 < announcementWidgets.length) + announcementWidgets[_currentIndex + 1] + else + Container( + height: 50, decoration: BoxDecoration( color: widget.backgroundColor, border: Border.symmetric( @@ -602,55 +407,200 @@ class _AnnouncementPickerState extends State { ) ), ), + ), - margin: const EdgeInsets.symmetric( - horizontal: 4 - ), - - child: Container( - child: Stack( - children: [ - Container( - width: 40, - height: 40, - child: Icon( - Icons.arrow_downward, - color: widget.outlineColor, - ), - ), - Positioned.fill( - child: ElevatedButton( - onPressed: () { - _currentIndex = wrap(_currentIndex + 4, 0, announcementWidgets.length, increment: 4); - setState(() {}); - print(_currentIndex); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - surfaceTintColor: Colors.transparent, - foregroundColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(0), - ), - ), - child: const Text(""), - ), - ) - ], - ), - ) - + Container( + height: 1, + color: widget.outlineColor, ), - ] + if (_currentIndex + 2 < announcementWidgets.length) + announcementWidgets[_currentIndex + 2] + else + Container( + height: 50, + decoration: BoxDecoration( + color: widget.backgroundColor, + border: Border.symmetric( + vertical: BorderSide( + color: widget.outlineColor, + width: 2 + ) + ), + ), + ), + Container( + height: 1, + color: widget.outlineColor, + ), + + if (_currentIndex + 3 < announcementWidgets.length) + announcementWidgets[_currentIndex + 3] + else + Container( + height: 50, + decoration: BoxDecoration( + color: widget.backgroundColor, + border: Border.symmetric( + vertical: BorderSide( + color: widget.outlineColor, + width: 2 + ) + ), + ), + ), + + Container( + height: 1, + color: widget.outlineColor, + ), + + Container( + height: 40, + decoration: BoxDecoration( + color: widget.backgroundColor, + ), + + child: Row( + + mainAxisSize: MainAxisSize.min, + + children: [ + + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + child: Text( + widget.label, + style: ShadTheme.of(context).textTheme.h4.copyWith( + shadows: [ + Shadow( + color: Colors.blueAccent.shade700, + blurRadius: 8 + ) + ], + color: Colors.blueAccent.shade700 + ) + ), + ), + + Expanded(child: Container()), + + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: widget.backgroundColor, + border: Border.symmetric( + vertical: BorderSide( + color: widget.outlineColor, + width: 1 + ) + ), + ), + + margin: const EdgeInsets.symmetric( + horizontal: 4 + ), + + child: Container( + child: Stack( + children: [ + Container( + width: 40, + height: 40, + child: Icon( + Icons.arrow_upward, + color: widget.outlineColor, + ), + ), + Positioned.fill( + child: ElevatedButton( + onPressed: () { + _currentIndex = wrap(_currentIndex - 4, 0, announcementWidgets.length, increment: 4); + setState(() {}); + print(_currentIndex); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + surfaceTintColor: Colors.transparent, + foregroundColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(0), + ), + ), + child: const Text(""), + ), + ) + ], + ), + ) + + ), + + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: widget.backgroundColor, + border: Border.symmetric( + vertical: BorderSide( + color: widget.outlineColor, + width: 1 + ) + ), + ), + + margin: const EdgeInsets.symmetric( + horizontal: 4 + ), + + child: Container( + child: Stack( + children: [ + Container( + width: 40, + height: 40, + child: Icon( + Icons.arrow_downward, + color: widget.outlineColor, + ), + ), + Positioned.fill( + child: ElevatedButton( + onPressed: () { + _currentIndex = wrap(_currentIndex + 4, 0, announcementWidgets.length, increment: 4); + setState(() {}); + print(_currentIndex); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + surfaceTintColor: Colors.transparent, + foregroundColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(0), + ), + ), + child: const Text(""), + ), + ) + ], + ), + ) + + ), + + ] + + ), + + ), + ], ), - - ), - Container( - height: 2, - color: widget.outlineColor, ), ] @@ -669,13 +619,14 @@ class StopAnnouncementPicker extends AnnouncementPicker { required this.routeVariant, required Color backgroundColor, required Color outlineColor, + String label = "Stops" }) : super( key: key, backgroundColor: backgroundColor, outlineColor: outlineColor, announcements: [ for (BusRouteStop stop in routeVariant.busStops) - _AnnouncementEntry( + AnnouncementEntry( label: stop.formattedStopName, onPressed: () { LiveInformation liveInformation = LiveInformation(); @@ -688,7 +639,8 @@ class StopAnnouncementPicker extends AnnouncementPicker { outlineColor: outlineColor, alert: LiveInformation().announcementModule.announcementCache[stop.getAudioFileName()] == null, ) - ] + ], + label: label ); } @@ -709,7 +661,7 @@ int wrap(int i, int j, int length, {int increment = -1}) { } } -class _AnnouncementEntry extends StatelessWidget { +class AnnouncementEntry extends StatelessWidget { final String label; @@ -719,7 +671,7 @@ class _AnnouncementEntry extends StatelessWidget { bool alert = false; - _AnnouncementEntry({super.key, required this.label, required this.onPressed, required this.index, required this.outlineColor, this.alert = false}); + AnnouncementEntry({super.key, required this.label, required this.onPressed, required this.index, required this.outlineColor, this.alert = false}); @override Widget build(BuildContext context) { @@ -730,12 +682,6 @@ class _AnnouncementEntry extends StatelessWidget { decoration: BoxDecoration( color: Colors.transparent, - border: Border.symmetric( - vertical: BorderSide( - color: outlineColor, - width: 2 - ) - ), ), padding: const EdgeInsets.symmetric( @@ -760,7 +706,7 @@ class _AnnouncementEntry extends StatelessWidget { label, style: GoogleFonts.teko( fontSize: 25, - color: outlineColor, + color: Colors.white, ), overflow: TextOverflow.ellipsis, ), diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 1b86d23..34dbbd8 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -1228,11 +1228,11 @@ class _ConsoleState extends State { // TODO: implement initState super.initState(); - _listenerReceipt = LiveInformation().commandModule.onCommandReceived.addListener((p0) { + /*_listenerReceipt = LiveInformation().commandModule.onCommandReceived.addListener((p0) { print("Command received, updating console"); setState(() {}); - }); + });*/ } @@ -1253,7 +1253,7 @@ class _ConsoleState extends State { Text("Command History:") ); - for (int i = 0; i < LiveInformation().commandModule.commandHistory.length; i++){ + /*for (int i = 0; i < LiveInformation().commandModule.commandHistory.length; i++){ CommandInfo command = LiveInformation().commandModule.commandHistory[i]; commands.add( @@ -1271,7 +1271,7 @@ class _ConsoleState extends State { ) ); } - +*/ return Container( decoration: BoxDecoration( @@ -1299,7 +1299,7 @@ class _ConsoleState extends State { color: Colors.white70, ), - Container( + /*Container( height: 50, padding: const EdgeInsets.all(8), child: TextField( @@ -1315,7 +1315,7 @@ class _ConsoleState extends State { LiveInformation().commandModule.executeCommand(value); }, ), - ) + )*/ ], diff --git a/lib/remaster/RemasteredMain.dart b/lib/remaster/RemasteredMain.dart index 321f56f..694337f 100644 --- a/lib/remaster/RemasteredMain.dart +++ b/lib/remaster/RemasteredMain.dart @@ -1,6 +1,7 @@ +import 'package:bus_infotainment/pages/tfl_dataset_test.dart'; import 'package:bus_infotainment/remaster/InitialStartup.dart'; import 'package:bus_infotainment/remaster/dashboard.dart'; import 'package:flutter/material.dart'; @@ -15,13 +16,22 @@ class RemasteredApp extends StatelessWidget { darkTheme: ShadThemeData( brightness: Brightness.dark, colorScheme: ShadSlateColorScheme.dark(), + // force dark mode ), + themeMode: ThemeMode.dark, routes: { '/setup': (context) => InitialStartup(), '/': (context) => HomePage_Re(), '/routes': (context) => RoutePage(), '/enroute': (context) => EnRoutePage(), + '/legacy': (context) => TfL_Dataset_Test(), + '/multi': (context) => MultiModeSetup(), + '/multi/enroute': (context) => MultiModeEnroute(), + '/multi/login': (context) => MultiModeLogin(), + '/multi/register': (context) => MultiModeRegister(), + '/display': (context) => FullscreenDisplay(), + '/multi/join': (context) => MultiModeJoin(), }, diff --git a/lib/remaster/dashboard.dart b/lib/remaster/dashboard.dart index 39dc6a3..a1c9b2e 100644 --- a/lib/remaster/dashboard.dart +++ b/lib/remaster/dashboard.dart @@ -1,19 +1,29 @@ +import 'package:bus_infotainment/auth/auth_api.dart'; import 'package:bus_infotainment/backend/live_information.dart'; +import 'package:bus_infotainment/backend/modules/tracker.dart'; import 'package:bus_infotainment/pages/components/ibus_display.dart'; +import 'package:bus_infotainment/pages/home.dart'; import 'package:bus_infotainment/tfl_datasets.dart'; +import 'package:bus_infotainment/utils/OrdinanceSurveyUtils.dart'; +import 'package:bus_infotainment/utils/delegates.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'dart:io' show Platform; - - +import 'package:dart_ping/dart_ping.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; + +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:native_qr/native_qr.dart'; + + import 'package:flutter_carousel_widget/flutter_carousel_widget.dart'; -import 'package:google_fonts/google_fonts.dart'; +import 'package:geolocator/geolocator.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; -import 'package:text_scroll/text_scroll.dart'; +import 'package:uuid/uuid.dart'; +import 'package:vector_math/vector_math.dart' hide Colors; import '../backend/modules/tube_info.dart'; @@ -21,6 +31,8 @@ Color rgb(int r, int g, int b) { return Color.fromRGBO(r, g, b, 1); } + + class HomePage_Re extends StatefulWidget { @override @@ -71,11 +83,11 @@ class _HomePage_ReState extends State { @override Widget build(BuildContext context) { - // TODO: implement build + return Scaffold( body: Container( - padding: EdgeInsets.all(16), + padding: const EdgeInsets.all(16), alignment: Alignment.center, @@ -90,7 +102,7 @@ class _HomePage_ReState extends State { children: [ - Text( + const Text( "Choose mode:", style: TextStyle( fontSize: 32, @@ -98,14 +110,14 @@ class _HomePage_ReState extends State { ) ), - SizedBox( + const SizedBox( height: 16, ), ShadCard( - title: Text("Solo mode"), + title: const Text("Solo mode"), width: double.infinity, - description: Text( + description: const Text( "Choose this mode if you are only using this device. (No internet required)" ), content: Column( @@ -115,7 +127,7 @@ class _HomePage_ReState extends State { children: [ - SizedBox( + const SizedBox( height: 4, ), @@ -123,22 +135,22 @@ class _HomePage_ReState extends State { onPressed: () { Navigator.pushNamed(context, "/routes"); }, - text: Text("Continue"), + text: const Text("Continue"), ) ], ), ), - SizedBox( + const SizedBox( height: 16, ), ShadCard( - title: Text("Multi mode"), + title: const Text("Multi mode"), width: double.infinity, - description: Text( - "Choose this mode if you are using multiple devices. (Internet required)" + description: const Text( + "Choose this mode if you are using multiple devices. (Internet required)" ), content: Column( @@ -147,21 +159,23 @@ class _HomePage_ReState extends State { children: [ - SizedBox( + const SizedBox( height: 4, ), ShadButton.secondary( onPressed: () { - Navigator.pushNamed(context, "/application"); + Navigator.pushNamed(context, "/multi"); }, - text: Text("Continue"), + text: const Text("Continue"), ) ], ), ) + + ], ), @@ -183,96 +197,101 @@ class RoutePage extends StatelessWidget { return Scaffold( - body: Container( + body: Column( + children: [ + Expanded( + child: Container( - padding: EdgeInsets.all(16), + padding: const EdgeInsets.all(16), - alignment: Alignment.center, + alignment: Alignment.center, - child: SizedBox( + child: SizedBox( - width: double.infinity, + width: double.infinity, - child: Column( + child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ - Text( - "Routes", - style: ShadTheme.of(context).textTheme.h1, - ), + Row( + children: [ - Text( - "Nearby routes", - style: ShadTheme.of(context).textTheme.h4, - ), + Text( + "Routes", + style: ShadTheme.of(context).textTheme.h1.copyWith(), + ), - ExpandableCarousel( - options: CarouselOptions( + Expanded( + child: Container(), + ), + + ], + ), + + 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) + + + ], ), - items: [ - ShadCard( - title: Text("Route 34"), - content: ShadSelect( - options: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text("Choose a variant"), - ) - ], - selectedOptionBuilder: (BuildContext context, value) { - return Text("Choose a variant"); - }, - ), - padding: EdgeInsets.all(10), - ) - ], - ), - - Divider(), - - RouteSearch() - - ], + ) ), - ) - + ), + const Divider( + height: 1, + ), + NavigationBar() + ], ), ); } - List _getNearbyRoutes() { - final variants = [ - "Walthamstow Central to Barnet Church", - "Walthamstow Central to Barnet Church", - ]; - - List widgets = []; - - for (int i = 0; i < variants.length; i++) { - widgets.add( - ShadOption( - value: i, - child: Text(variants[i]!), - ) - ); - } - - return widgets; - - } } class RouteSearch extends StatefulWidget { + final bool multiMode; + + RouteSearch({required this.multiMode}); + @override State createState() => _RouteSearchState(); } @@ -291,7 +310,7 @@ class _RouteSearchState extends State { continue; } - routes.add(RouteCard(route: route)); + routes.add(RouteCard(route: route, multiMode: widget.multiMode)); } return Expanded( @@ -300,7 +319,7 @@ class _RouteSearchState extends State { children: [ ShadInput( - placeholder: Text("Search for a route..."), + placeholder: const Text("Search for a route..."), controller: controller, onChanged: (value) { setState(() { @@ -309,21 +328,27 @@ class _RouteSearchState extends State { }, ), - SizedBox( + const SizedBox( height: 4, ), Expanded( - child: GridView.count( - crossAxisCount: 3, - children: [ - ...routes - ], + child: Scrollbar( + interactive: true, + radius: const Radius.circular(8), + thickness: 8, + thumbVisibility: true, + child: GridView.count( + crossAxisCount: 3, + children: [ + ...routes + ], + shrinkWrap: true, + ), ), ) ], - ), ); } @@ -332,8 +357,9 @@ class _RouteSearchState extends State { class RouteCard extends StatelessWidget { BusRoute route; + final bool multiMode; - RouteCard({required this.route}); + RouteCard({required this.route, required this.multiMode}); @override Widget build(BuildContext context) { @@ -350,15 +376,44 @@ class RouteCard extends StatelessWidget { ); } + 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 AspectRatio( aspectRatio: 1, child: Container( child: ShadButton.secondary( - text: Text( - "Route \n ${route.routeNumber}", - style: ShadTheme.of(context).textTheme.h3 + 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)) + ], ), - padding: EdgeInsets.all(8), + padding: const EdgeInsets.all(8), width: 105, height: 105, @@ -371,8 +426,6 @@ class RouteCard extends StatelessWidget { List variantWidgets = []; for (BusRouteVariant variant in route.routeVariants.values) { - String variantLabel = "${variant.busStops.first.formattedStopName} -> ${variant.busStops.last.formattedStopName}"; - variantWidgets.add( ShadButton.outline( text: SizedBox( @@ -381,7 +434,7 @@ class RouteCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("${variant.busStops.first.formattedStopName} ->"), - SizedBox( + const SizedBox( height: 2, ), Text(variant.busStops.last.formattedStopName) @@ -390,19 +443,30 @@ class RouteCard extends StatelessWidget { ), width: double.infinity, height: 50, - padding: EdgeInsets.all(8), + padding: const EdgeInsets.all(8), onPressed: () async { LiveInformation liveInformation = LiveInformation(); await liveInformation.setRouteVariant(variant); - liveInformation.announcementModule.queueAnnouncementByRouteVariant(routeVariant: 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), + ) + ); + } - Navigator.pushNamed(context, "/enroute"); }, ) ); - variantWidgets.add(SizedBox( + variantWidgets.add(const SizedBox( height: 4, )); } @@ -412,15 +476,22 @@ class RouteCard extends StatelessWidget { content: Container( width: 2000, + constraints: const BoxConstraints( + maxHeight: 400 + ), alignment: Alignment.center, - child: Column( - children: [ - ...variantWidgets - ], + child: Scrollbar( + thumbVisibility: true, + child: SingleChildScrollView( + child: Column( + children: [ + ...variantWidgets + ], + ), + ), ), ), - padding: EdgeInsets.all(8), - + padding: const EdgeInsets.all(8), ); @@ -443,49 +514,245 @@ class EnRoutePage extends StatelessWidget { Widget build(BuildContext context) { // TODO: implement build return Scaffold( - body: Column( + body: _dash(), + ); + } +} - children: [ +class _dash extends StatelessWidget { - Container( - padding: EdgeInsets.all(8), + @override + Widget build(BuildContext context) { + return Column( + + children: [ + + Container( + padding: const EdgeInsets.all(8), child: ibus_display() - ), + ), - Divider( - height: 1, - ), + const Divider( + height: 1, + ), - ShadButton( - text: Text("Route scanner"), - onPressed: () { - LiveInformation liveInformation = LiveInformation(); + ExpandableCarousel( + items: [ + EasyAnnouncementPicker( + announcements: LiveInformation().announcementModule.manualAnnouncements, + title: "Manual" + ), - TubeLine? line = liveInformation.tubeStations.getClosestLine(liveInformation.getRouteVariant()!); - - ShadToaster.of(context).show( - ShadToast( - title: Text("Closest line"), - description: Text(line == null ? "No line found" : line.name), - duration: Duration(seconds: 5), + Container( + padding: const EdgeInsets.all(8), + child: StopAnnouncementPicker( + routeVariant: LiveInformation().getRouteVariant()!, + backgroundColor: Colors.transparent, + outlineColor: Colors.white, + label: "Bus Stops", ) + ) + + + ], + options: CarouselOptions( + showIndicator: false + + ), + ), + + const Divider( + height: 1, + ), + + Expanded( + child: Container( + padding: const EdgeInsets.all(8), + child: Column( + + children: [ + + Text( + "Quick actions", + style: ShadTheme.of(context).textTheme.h3, + ), + + const SizedBox( + height: 8, + ), + + Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: ShadTheme.of(context).colorScheme.primary, + width: 1 + ) + ), + padding: const EdgeInsets.all(4), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: ShadTheme.of(context).colorScheme.primary, + width: 1 + ) + ), + child: Column( + children: [ + + if (!kIsWeb) + AnnouncementEntry( + label: "Display next stop", + index: 0, + outlineColor: ShadTheme.of(context).colorScheme.primary, + onPressed: () { + LiveInformation liveInformation = LiveInformation(); + TrackerModule trackerModule = liveInformation.trackerModule; + + BusRouteStop? stop = trackerModule.nearestStop; + + if (stop != null) { + liveInformation.announcementModule.queueAnnounceByAudioName(displayText: stop.formattedStopName); + } else { + ShadToaster.of(context).show( + const ShadToast( + title: Text("No bus stop found"), + description: Text("No bus stop found nearby"), + duration: Duration(seconds: 5), + ) + ); + } + }, + ), + + Container( + height: 1, + color: ShadTheme.of(context).colorScheme.primary, + ), + + AnnouncementEntry( + label: "Announce destination", + index: 1, + outlineColor: ShadTheme.of(context).colorScheme.primary, + onPressed: () { + LiveInformation liveInformation = LiveInformation(); + liveInformation.announcementModule.queueAnnouncementByRouteVariant( + routeVariant: + liveInformation.getRouteVariant()! + ); + }, + ), + ], + ), + ), + ) + + // SizedBox( + // height: 8, + // ), + // + // ShadCard( + // title: Text("Stop announcements"), + // width: double.infinity, + // ), + // + // ShadButton( + // text: Text("Route scanner"), + // onPressed: () { + // + // LiveInformation liveInformation = LiveInformation(); + // + // TubeLine? line = liveInformation.tubeStations.getClosestLine(liveInformation.getRouteVariant()!); + // + // ShadToaster.of(context).show( + // ShadToast( + // title: Text("Closest line"), + // description: Text(line == null ? "No line found" : line.name), + // duration: Duration(seconds: 5), + // ) + // ); + // + // }, + // ), + // + // ShadButton( + // text: Text("dest"), + // onPressed: () { + // LiveInformation liveInformation = LiveInformation(); + // liveInformation.announcementModule.queueAnnouncementByRouteVariant(routeVariant: liveInformation.getRouteVariant()!); + // }, + // ), + // + // ShadButton( + // text: Text("Open Legacy dashboard"), + // onPressed: () { + // Navigator.pushNamed(context, "/legacy"); + // }, + // ) + + ], + + ) + ), + ), + + const Divider( + height: 1, + ), + + NavigationBar() + + ], + + ); + } +} + +class EasyAnnouncementPicker extends StatelessWidget { + + late final List announcements; + + late final String title; + Color outlineColor = Colors.white; + + EasyAnnouncementPicker({this.announcements = const [], this.title = "Announcements", this.outlineColor = Colors.white}); + + @override + Widget build(BuildContext context) { + + List announcementWidgets = []; + + for (AnnouncementQueueEntry entry in announcements) { + if (entry is NamedAnnouncementQueueEntry) { + announcementWidgets.add( + AnnouncementEntry( + label: entry.shortName, + onPressed: () { + LiveInformation liveInformation = LiveInformation(); + liveInformation.announcementModule.queueAnnounementByInfoIndex( + infoIndex: liveInformation.announcementModule.manualAnnouncements.indexOf(entry), + sendToServer: true ); }, - ), + index: announcements.indexOf(entry), + outlineColor: outlineColor, - ShadButton( - text: Text("dest"), - onPressed: () { - LiveInformation liveInformation = LiveInformation(); - liveInformation.announcementModule.queueAnnouncementByRouteVariant(routeVariant: liveInformation.getRouteVariant()!); - }, ) + ); + } - ], + } - ), + return AnnouncementPicker( + announcements: announcementWidgets, + backgroundColor: const Color(/*Transparent*/0x00000000), + outlineColor: outlineColor, + label: title, ); } @@ -507,4 +774,1070 @@ ShadSelectFormField( placeholder: Text("Choose a variant"), ), - */ \ No newline at end of file + */ + +class MultiModeSetup extends StatefulWidget { + + @override + State createState() => _MultiModeSetupState(); +} + +class _MultiModeSetupState extends State { + + @override + void initState() { + // Check if the user is logged in + + super.initState(); + + + } + + @override + Widget build(BuildContext context) { + return Scaffold( + + body: Column( + + children: [ + + Expanded( + child: Container( + + alignment: Alignment.center, + padding: const EdgeInsets.all(16), + + child: Column( + + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + + children: [ + + const Text( + "Multi mode options:", + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.w600, + ) + ), + + const SizedBox( + height: 16, + ), + + ShadCard( + title: const Text("Host a group"), + width: double.infinity, + description: const Text( + "" + ), + content: Column( + + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + + children: [ + + const SizedBox( + height: 4, + ), + + ShadButton.secondary( + onPressed: () async { + + LiveInformation liveInformation = LiveInformation(); + + Future.delayed(Duration.zero, () { + print("At time of loading: ${liveInformation.auth.status}"); + + if (liveInformation.auth.status != AuthStatus.AUTHENTICATED) { + Navigator.popAndPushNamed(context, "/multi/login"); + } + }); + await liveInformation.createRoom(liveInformation.auth.userID!); + + + Navigator.pushNamed(context, "/multi/enroute"); + }, + text: const Text("Continue"), + ) + + ], + ), + ), + + const SizedBox( + 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"), + ) + + ], + ), + ) + + ], + + ), + ), + ), + + NavigationBar() + + + + + ], + + ) + + ); + } +} + +class MultiModeEnroute extends StatefulWidget { + + @override + State createState() => _MultiModeEnrouteState(); +} + +class _MultiModeEnrouteState extends State { + + late final Future roomCodeFuture; + + late ListenerReceipt listenerReceipt; + + @override + void initState() { + super.initState(); + + LiveInformation liveInformation = LiveInformation(); + listenerReceipt = liveInformation.routeVariantDelegate.addListener((value) { + setState(() { + + }); + }); + + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + LiveInformation liveInformation = LiveInformation(); + liveInformation.routeVariantDelegate.removeListener(listenerReceipt); + } + + @override + Widget build(BuildContext context) { + + // Set the screen to portrait + + + // Generate random uuid + LiveInformation liveInformation = LiveInformation(); + + + return Scaffold( + + body: Column( + children: [ + + Container( + padding: const EdgeInsets.all(16), + + height: 200, + + child: ShadCard( + title: liveInformation.isHost ? const Text("Hosting group") : const Text("Joined group"), + border: Border.all( + color: Colors.amber, + width: 1 + ), + padding: const EdgeInsets.all(16), + width: double.infinity, + description: liveInformation.isHost ? const Text( + "You are hosting a group. \nShare the room code with others to join" + ) : const Text( + "You have joined a group." + ), + content: Column( + children: [ + const SizedBox( + height: 4, + ), + + FutureBuilder( + future: Future.delayed(const Duration(seconds: 1)), + builder: (context, snapshot) { + return Row( + children: [ + Expanded( + child: ShadButton( + text: Text( + liveInformation.roomCode!, + ), + + icon: const Icon(Icons.copy), + padding: const EdgeInsets.all(8), + onPressed: () { + Clipboard.setData(ClipboardData(text: liveInformation.roomCode!)); + ShadToaster.of(context).show( + const ShadToast( + title: Text("Copied to clipboard"), + description: Text("Room code copied to clipboard"), + duration: Duration(seconds: 5), + ) + ); + }, + ), + ), + ShadButton( + icon: const Icon(Icons.qr_code), + onPressed: () { + showShadDialog( + context: context, + builder: (context) { + return ShadDialog( + title: const Text("QR Code"), + content: Container( + + width: 200, + height: 225, + + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + QrImageView( + data: liveInformation.roomCode!, + size: 200, + backgroundColor: Colors.white, + ), + + const SizedBox( + height: 8, + ), + + Text("Scan QR code to join the group") + + ], + ), + ), + actions: [ + ShadButton( + text: const Text("Close"), + onPressed: () { + Navigator.pop(context); + }, + ) + ], + ); + } + ); + }, + ) + ], + ); + }, + ), + ], + ), + ), + ), + + const Divider( + height: 1, + ), + + Container( + padding: EdgeInsets.all(8), + child: ibus_display() + ), + + const Divider( + height: 1, + ), + + Container( + padding: EdgeInsets.all(8), + child: Text( + "* Swipe left and right below for more options!", + textAlign: TextAlign.center, + ), + ), + + const Divider( + height: 1, + ), + + SizedBox( + height: 16, + ), + + Expanded( + child: FlutterCarousel( + items: [ + Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.white, + width: 1 + ), + borderRadius: BorderRadius.circular(8) + ), + padding: const EdgeInsets.all(4), + margin: const EdgeInsets.only( + left: 16, + right: 16, + ), + width: double.infinity, + + child: Column( + + crossAxisAlignment: CrossAxisAlignment.start, + + children: [ + + Container( + padding: const EdgeInsets.only( + top: 4, + left: 8, + right: 8, + bottom: 4 + ), + child: Text( + "Nearby routes", + style: ShadTheme.of(context).textTheme.h4, + ), + ), + if (!kIsWeb) + Expanded( + child: Scrollbar( + interactive: true, + radius: const Radius.circular(8), + thickness: 8, + thumbVisibility: true, + child: GridView.count( + crossAxisCount: 3, + children: [ + ..._getNearbyRoutes(multiMode: true) + ], + shrinkWrap: true, + ), + ), + ) + + ], + ) + ), + Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.white, + width: 1 + ), + borderRadius: BorderRadius.circular(8) + ), + padding: const EdgeInsets.all(4), + margin: const EdgeInsets.only( + left: 16, + right: 16, + ), + + child: Expanded(child: RouteSearch(multiMode: true,)) + ), + Container( + + decoration: BoxDecoration( + border: Border.all( + width: 1, + color: Colors.white + ), + borderRadius: BorderRadius.all(Radius.circular(8)) + ), + + margin: const EdgeInsets.only( + left: 16, + right: 16, + ), + + padding: EdgeInsets.all(8), + + child: SingleChildScrollView( + child: Column( + children: [ + EasyAnnouncementPicker( + announcements: LiveInformation().announcementModule.manualAnnouncements, + title: "Manual", + outlineColor: ShadTheme.of(context).colorScheme.secondary + ), + + if (liveInformation.getRouteVariant() != null) + SizedBox( + height: 16, + ), + + if (liveInformation.getRouteVariant() != null) + Container( + + child: StopAnnouncementPicker( + routeVariant: LiveInformation().getRouteVariant()!, + backgroundColor: Colors.transparent, + outlineColor: ShadTheme.of(context).colorScheme.secondary, + label: "Bus Stops", + ) + ) + ], + ), + ), + ), + Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: ShadTheme.of(context).colorScheme.primary, + width: 1 + ) + ), + margin: const EdgeInsets.only( + left: 16, + right: 16, + ), + padding: const EdgeInsets.all(4), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: ShadTheme.of(context).colorScheme.primary, + width: 1 + ) + ), + child: Column( + children: [ + + if (!kIsWeb) + AnnouncementEntry( + label: "Display next stop", + index: 0, + outlineColor: ShadTheme.of(context).colorScheme.primary, + onPressed: () { + LiveInformation liveInformation = LiveInformation(); + TrackerModule trackerModule = liveInformation.trackerModule; + + BusRouteStop? stop = trackerModule.nearestStop; + + if (stop != null) { + liveInformation.announcementModule.queueAnnounceByAudioName(displayText: stop.formattedStopName); + } else { + ShadToaster.of(context).show( + const ShadToast( + title: Text("No bus stop found"), + description: Text("No bus stop found nearby"), + duration: Duration(seconds: 5), + ) + ); + } + }, + ), + + if (!kIsWeb) + Container( + height: 1, + color: ShadTheme.of(context).colorScheme.primary, + ), + + AnnouncementEntry( + label: "Announce destination", + index: 1, + outlineColor: ShadTheme.of(context).colorScheme.primary, + onPressed: () { + LiveInformation liveInformation = LiveInformation(); + liveInformation.announcementModule.queueAnnouncementByRouteVariant( + routeVariant: + liveInformation.getRouteVariant()! + ); + }, + ), + + Container( + height: 1, + color: ShadTheme.of(context).colorScheme.primary, + ), + ], + ), + ), + ), + ], + options: CarouselOptions( + showIndicator: false, + viewportFraction: 1, + height: double.infinity, + enableInfiniteScroll: true + ), + ), + ), + + Container( + padding: const EdgeInsets.all(8), + child: ShadButton( + text: const Text("Fullscreen display"), + onPressed: () { + Navigator.pushNamed(context, "/display"); + }, + icon: const Icon(Icons.fullscreen), + width: double.infinity, + ), + ), + const Divider( + height: 1, + ), + NavigationBar() + ], + ), + + ); + } + + +} + +class MultiModeJoin extends StatefulWidget { + + @override + State createState() => _MultiModeJoinState(); +} + +class _MultiModeJoinState extends State { + TextEditingController controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Expanded( + child: Container( + + alignment: Alignment.center, + padding: EdgeInsets.all(16), + + child: Column( + + mainAxisSize: MainAxisSize.min, + + children: [ + ShadCard( + title: const Text("Join a group"), + width: double.infinity, + description: const Text("Enter the room code to join a group"), + content: Column( + children: [ + ShadInputFormField( + controller: controller, + id: "roomCode", + label: const Text("Room code"), + placeholder: const Text("Enter the room code"), + validator: (value) { + if (value.isEmpty) { + return "Please enter the room code"; + } + return null; + }, + ), + ShadButton( + text: const Text("Join"), + width: double.infinity, + onPressed: () async { + LiveInformation liveInformation = LiveInformation(); + + liveInformation.setRouteVariant(null); + + await liveInformation.JoinRoom(controller.text); + + Navigator.popAndPushNamed(context, "/multi/enroute"); + + }, + ) + ], + ), + ), + const SizedBox( + height: 16, + ), + ShadButton.secondary( + text: Text("Scan QR code"), + icon: const Icon(Icons.qr_code), + onPressed: () async { + try { + NativeQr nativeQr = NativeQr(); + String? result = await nativeQr.get(); + controller.text = result!; + } catch (e) { + print("Failed to scan QR code"); + } + } + ) + // ibus_display(), + // Expanded(child: _dash()) + ] + ), + ), + ), + Divider( + height: 1 + ), + NavigationBar() + ], + ), + ); + } +} + +class FullscreenDisplay extends StatefulWidget { + + @override + State createState() => _FullscreenDisplayState(); +} + +class _FullscreenDisplayState extends State { + + + + @override + Widget build(BuildContext context) { + + // Make the screen landscape + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeRight, + DeviceOrientation.landscapeLeft + ]); + + return PopScope( + onPopInvoked: (isPop) { + if (isPop) { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown + ]); + } + }, + child: Scaffold( + + body: Container( + + color: Colors.black, + alignment: Alignment.center, + + child: Row( + children: [ + Expanded( + child: ibus_display( + hasBorder: false, + ), + ), + ShadButton.ghost( + icon: const Icon(Icons.arrow_back), + padding: const EdgeInsets.all(8), + onPressed: () { + Navigator.pop(context); + }, + ) + ], + ), + + ), + + ), + ); + } +} + +class MultiModeLogin extends StatelessWidget { + + final formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + // TODO: implement build + return Scaffold( + + body: Column( + children: [ + Expanded( + child: Container( + + padding: const EdgeInsets.all(16), + alignment: Alignment.centerLeft, + + child: Column( + + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + + children: [ + + Text( + "Login", + style: ShadTheme.of(context).textTheme.h2, + ), + + ShadForm( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ShadInputFormField( + id: "email", + label: const Text("Email"), + autofillHints: const [AutofillHints.email], + placeholder: const Text("Enter your email"), + validator: (value) { + if (value.isEmpty) { + return "Please enter your email"; + } + if (!value.contains("@")) { + return "Please enter a valid email"; + } + return null; + }, + ), + ShadInputFormField( + id: "password", + label: const Text("Password"), + placeholder: const Text("Enter your password"), + autofillHints: const [AutofillHints.password], + obscureText: true, + validator: (value) { + if (value.isEmpty) { + return "Please enter your password"; + } + if (value.length < 8) { + return "Password must be at least 8 characters long"; + } + return null; + }, + ) + ], + ), + ), + + ShadButton( + text: const Text("Login"), + width: double.infinity, + onPressed: () async { + if (formKey.currentState!.validate()) { + formKey.currentState!.save(); + print("Logging in..."); + + LiveInformation liveInformation = LiveInformation(); + await liveInformation.auth.createEmailSession( + email: formKey.currentState!.value["email"], + password: formKey.currentState!.value["password"] + ); + print("Done something"); + print(liveInformation.auth.status); + } + }, + ), + + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Don't have an account?"), + ShadButton.link( + onPressed: () { + Navigator.pushNamed(context, "/multi/register"); + }, + text: const Text("Register"), + ) + ], + ) + + ], + + ), + + ), + ), + const Divider( + height: 1, + ), + NavigationBar() + ], + ), + + ); + } + +} + +class MultiModeRegister extends StatelessWidget { + + final formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + // TODO: implement build + return Scaffold( + + body: Column( + children: [ + Expanded( + child: Container( + + padding: const EdgeInsets.all(16), + alignment: Alignment.centerLeft, + + child: SingleChildScrollView( + child: Column( + + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + + children: [ + + Text( + "Register", + style: ShadTheme.of(context).textTheme.h2, + ), + + ShadForm( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ShadInputFormField( + id: "email", + label: const Text("Email"), + autofillHints: const [AutofillHints.email], + placeholder: const Text("Enter your email"), + validator: (value) { + if (value.isEmpty) { + return "Please enter your email"; + } + if (!value.contains("@")) { + return "Please enter a valid email"; + } + return null; + }, + ), + ShadInputFormField( + id: "password", + label: const Text("Password"), + placeholder: const Text("Enter your password"), + autofillHints: const [AutofillHints.password], + obscureText: true, + validator: (value) { + if (value.isEmpty) { + return "Please enter your password"; + } + if (value.length < 8) { + return "Password must be at least 8 characters long"; + } + return null; + }, + ), + ShadInputFormField( + id: "confirmPassword", + label: const Text("Confirm password"), + placeholder: const Text("Re-enter your password"), + autofillHints: const [AutofillHints.password], + obscureText: true, + validator: (value) { + if (value.isEmpty) { + return "Please enter your password"; + } + if (value.length < 8) { + return "Password must be at least 8 characters long"; + } + if (value != formKey.currentState!.value["password"]) { + return "Passwords do not match"; + } + return null; + }, + ) + ], + ), + ), + + ShadButton( + text: const Text("Register"), + width: double.infinity, + onPressed: () async { + if (formKey.currentState!.validate()) { + formKey.currentState!.save(); + print("Logging in..."); + + LiveInformation liveInformation = LiveInformation(); + await liveInformation.auth.createUser( + displayName: formKey.currentState!.value["email"], + username: formKey.currentState!.value["email"], + email: formKey.currentState!.value["email"], + password: formKey.currentState!.value["password"], + ); + await liveInformation.auth.createEmailSession( + email: formKey.currentState!.value["email"], + password: formKey.currentState!.value["password"] + ); + if (liveInformation.auth.status == AuthStatus.AUTHENTICATED) { + Navigator.pop(context); + Navigator.popAndPushNamed(context, "/multi"); + } else { + ShadToaster.of(context).show( + const ShadToast( + title: Text("Failed to register"), + description: Text("Failed to register with the provided details"), + duration: Duration(seconds: 5), + ) + ); + } + } + }, + ), + + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Have an account?"), + ShadButton.link( + onPressed: () { + Navigator.pushNamed(context, "/multi/login"); + }, + text: const Text("Login"), + ) + ], + ) + + ], + + ), + ), + + ), + ), + const Divider( + height: 1, + ), + NavigationBar() + ], + ), + + ); + } + +} + +class NavigationBar extends StatefulWidget { + @override + State createState() => _NavigationBarState(); +} + +class _NavigationBarState extends State { + @override + Widget build(BuildContext context) { + + // Is the on screen keyboard visible? + bool isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0; + if (isKeyboardVisible) { + return Container(); + } + + return Row( + children: [ + ShadButton.ghost( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon(Icons.arrow_back), + text: const Text("Back"), + padding: const EdgeInsets.all(8), + ), + + ], + ); + } +} + +List _getNearbyRoutes({bool multiMode = false}) { + + print("Getting nearby routes"); + + LiveInformation liveInformation = LiveInformation(); + BusSequences busSequences = liveInformation.busSequences; + + List nearbyRoutes = []; + + Position? currentLocation = liveInformation.trackerModule.position; + + Vector2 currentVector = Vector2(0, 0); + + if (currentLocation == null && !kDebugMode) { + return []; + } else if (currentLocation != null){ + currentVector = OSGrid.toNorthingEasting(currentLocation!.latitude, currentLocation.longitude); + } + + + + if (kDebugMode) { + currentVector = OSGrid.toNorthingEasting(51.583781262560926, -0.020359583104595073); + } + + for (BusRoute route in busSequences.routes.values) { + for (BusRouteVariant variant in route.routeVariants.values) { + for (BusRouteStop stop in variant.busStops) { + + Vector2 stopVector = Vector2(stop.easting.toDouble(), stop.northing.toDouble()); + + double distance = currentVector.distanceTo(stopVector); + + if (distance < 1000) { + nearbyRoutes.add(route); + break; + } + } + if (nearbyRoutes.contains(route)) { + break; + } + } + if (nearbyRoutes.contains(route)) { + continue; + } + } + + List routeCards = []; + + for (BusRoute route in nearbyRoutes) { + routeCards.add(RouteCard(route: route, multiMode: multiMode)); + } + + return routeCards; + +} \ No newline at end of file diff --git a/lib/workaround/keepalive_realtime.dart b/lib/workaround/keepalive_realtime.dart new file mode 100644 index 0000000..c4ec855 --- /dev/null +++ b/lib/workaround/keepalive_realtime.dart @@ -0,0 +1,112 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; + +import 'package:appwrite/appwrite.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; +// import this package https://pub.dev/packages/web_socket_channel + +class RealtimeKeepAliveConnection { + RealtimeKeepAliveConnection({ + required this.channels, + required this.domain, + required this.client, + this.keepAlivePingDuration = const Duration(seconds: 90), + required this.onData, + required this.onError, + }); + + final List channels; + final String domain; + final Duration keepAlivePingDuration; + final Client client; + final Function(RealtimeMessage) onData; + final Function(dynamic) onError; + + // ignore: unused_field + StreamSubscription? _subscription; + WebSocketChannel? _webSocket; + final Stopwatch _stopwatch = Stopwatch(); + bool _keepAlive = true; + bool _sentKeepAlivePing = false; + int reconnectCount = 0; + + Future initialize() async { + await _initRealtime( + onData: _realtimeOnData, + onDone: _realtimeOnDone, + onError: _realtimeOnError, + ); + _heartbeat(); + } + + void close() { + _keepAlive = false; + _subscription!.cancel(); + } + + void _heartbeat() async { + while (_keepAlive) { + await Future.delayed(keepAlivePingDuration); + if (_webSocket != null) { + _sentKeepAlivePing = true; + _webSocket!.sink.add("ping"); + } + } + } + + void _realtimeOnData(RealtimeMessage data) { + log("[$reconnectCount][${_stopwatch.elapsed}] onData"); + onData(data); + } + + void _realtimeOnDone() async { + reconnectCount++; + log("[$reconnectCount][${_stopwatch.elapsed}] onDone"); + if (_keepAlive) { + if (_subscription != null) _subscription!.cancel(); + + _subscription = _subscription = await _initRealtime( + onData: _realtimeOnData, + onDone: _realtimeOnDone, + onError: _realtimeOnError, + ); + } + } + + void _realtimeOnError(dynamic e) { + log("[$reconnectCount][${_stopwatch.elapsed}] onError:$e"); + onError(onError); + } + + Future _initRealtime({ + required Function(RealtimeMessage) onData, + required Function() onDone, + required Function(dynamic) onError, + }) async { + _stopwatch.reset(); + _stopwatch.start(); + String channelParams = channels.map((c) => "channels[]=$c").join('&'); + + String? projectId = client.config['project']; + + final wssUrl = Uri.parse('wss://$domain/realtime?project=$projectId&$channelParams'); + _webSocket = WebSocketChannel.connect(wssUrl); + + Realtime realtime = Realtime(client); + RealtimeSubscription subscriptionRealTime = realtime.subscribe(channels); + + subscriptionRealTime.stream.listen(onData, onDone: onDone, onError: onError); + _subscription = _webSocket!.stream.listen(_handlePingMsg); + } + + void _handlePingMsg(dynamic response) { + var json = jsonDecode(response); + + if (json["type"] == "error" && _sentKeepAlivePing) { + _sentKeepAlivePing = false; + log("Web socket keep-alive heartbeat successful (Reconnect Count: $reconnectCount, Time alive: ${_stopwatch.elapsed})"); + return; + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 1a40d23..11f2fef 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -185,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dart_ping: + dependency: "direct main" + description: + name: dart_ping + sha256: "2f5418d0a5c64e53486caaac78677b25725b1e13c33c5be834ce874ea18bd24f" + url: "https://pub.dev" + source: hosted + version: "9.0.1" device_info_plus: dependency: transitive description: @@ -565,6 +573,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + native_qr: + dependency: "direct main" + description: + name: native_qr + sha256: "0928754b92305eb101a3359014a60ac5e90126534406b4dd6fb7550433978420" + url: "https://pub.dev" + source: hosted + version: "0.0.3" ntp: dependency: "direct main" description: @@ -741,6 +757,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + qr: + dependency: transitive + description: + name: qr + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" rive: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6c3cb57..05acc91 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,9 @@ dependencies: file_picker: ^8.0.0+1 shadcn_ui: ^0.4.1 flutter_carousel_widget: ^2.2.0 + dart_ping: ^9.0.1 + native_qr: ^0.0.3 + qr_flutter: ^4.1.0 # The following adds the Cupertino Icons font to your application.