Compare commits

..

10 Commits

Author SHA1 Message Date
ImBenji
bd8973ed23 Initial commit 2025-11-28 18:44:30 +00:00
ImBenji
1cbd985c15 Spring Cleaning 2024-07-13 19:25:15 +01:00
ImBenji
ffde62a730 All dem changes 2024-05-24 20:17:02 +01:00
ImBenji
2846a061cc laptop push 2024-05-21 18:12:35 +01:00
ImBenji
639faddfc8 desktop push 2024-05-20 09:06:38 +01:00
ImBenji
e5a8d78bf1 Merge branch 'master' of https://github.com/RailboundStudios/bus_infotainment 2024-05-17 17:38:37 +01:00
ImBenji
3556639acc desktop 2024-05-17 17:38:34 +01:00
ImBenji
fff8de13c0 laptop 2024-05-17 17:36:02 +01:00
ImBenji
1f48f8f4b0 paradigm shift 2024-05-03 14:03:51 +01:00
ImBenji
673891923d minor stuff 2024-05-01 19:49:55 +01:00
34 changed files with 11041 additions and 886 deletions

View File

@@ -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 24
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled = true

View File

@@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="bus_infotainment"
android:label="Bus Infotainment System"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"
@@ -22,6 +22,8 @@
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@@ -32,10 +34,14 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui"/>
</application>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

Binary file not shown.

View File

@@ -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
1 Route Blind 51.4309209 -0.0936496
58 5 Upton Park, Boleyn 51.5305005 0.0384915
59 5 Plaistow, Balaam Street 51.5226426 0.0223686
60 5 Canning Town, Barking Road 51.53044569999999 0.038323
61 5 Canning Town 51.5189494 51.51395705923875 0.0132 0.00827546234713445
62 6 Willesden, Bus Garage 51.5474482 -0.2394372
63 6 Kensal Rise 51.5345071 -0.2250186
64 6 Queen's Park 51.5345448 -0.2043853
216 20 Whipps Cross 51.581499 0.0001219
217 20 Woodford Green 51.6092549 0.0405521
218 20 Woodford Wells 51.6148287 0.0282889
219 20 Loughton 51.655942 51.64195093723499 0.068161 0.05498357119784937
220 20 Debden 51.6042352 51.645776846753535 0.04159529999999999 0.08200009471482733
221 21 Lewisham, Shopping Centre 51.46115090000001 -0.0073177
222 21 Lewisham, Jerrard Street 51.4650045 -0.0164722
223 21 New Cross Gate 51.4749904 -0.0403466
719 69 Stratford 51.5426313 -0.0010369
720 69 Plaistow 51.5268317 0.0308143
721 69 Canning Town, Hermit Road 51.5209368 0.0125057
722 69 Canning Town 51.5189494 51.51395705923875 0.0132 0.00827546234713445
723 70 Chiswick, Business Park 51.4930604 -0.2748696
724 70 Acton, High Street 51.5068505 -0.2673267
725 70 East Acton Lane 51.5123715 -0.2543317
1066 108 Stratford International Bus Station 51.5453665 -0.0099022
1067 108 Bow Church 51.5287753 -0.0167013
1068 108 Poplar, All Saints 51.5105521 -0.0118612
1069 108 Canning Town 51.5189494 51.51395705923875 0.0132 0.00827546234713445
1070 108 North Greenwich 51.4859576 0.007494900000000001
1071 108 East Greenwich 51.4309209 -0.0936496
1072 108 Blackheath, Royal Standard 51.4779946 0.0202001
1128 115 Upton Park, Boleyn 51.5305005 0.0384915
1129 115 Plaistow, Balaam street 51.5260131 0.0238468
1130 115 Canning Town, Barking Road 51.5175196 0.0115068
1131 115 Canning Town 51.5189494 51.51395705923875 0.0132 0.00827546234713445
1132 115 Poplar, All Saints 51.5105521 -0.0118612
1133 115 Limehouse, Burdett Road 51.5209123 -0.0327503
1134 115 Stepney, Arbour Square 51.5140186 -0.0481747
1412 147 East Ham, Newham Town Hall 51.53280239999999 0.0551608
1413 147 Upton Park, Boleyn 51.5305005 0.0384915
1414 147 Prince Regent 51.5178274 0.0321034
1415 147 Canning Town 51.5189494 51.51395705923875 0.0132 0.00827546234713445
1416 148 Camberwell Green 51.4756016 -0.0928896
1417 148 Elephant & Castle 51.4938058 -0.0977932
1418 148 Parliament Square 51.5010421 -0.1268514
1580 167 Barkingside, Fullwell Cross 51.59451199999999 0.08571999999999999
1581 167 Gants Hill 51.5767812 0.0661732
1582 167 Buckhurst Hill 51.627572 0.034513
1583 167 Loughton 51.655942 51.64195093723499 0.068161 0.05498357119784937
1584 168 Hampstead Heath 51.5608294 -0.1629416
1585 168 Chalk Farm 51.5422732 -0.1466907
1586 168 Camden Town 51.5390261 -0.1425516
1984 212 Highams Park 51.6083754 0.0014712
1985 212 Walthamstow, Beacontree Avenue 51.595736 0.0045318
1986 212 Walthamstow Central 51.5830128 -0.019886
1987 212 St James Street 51.5064993 51.581118791593234 -0.1393328 -0.0328839757764101
1988 213 Kingston 51.4116616 -0.2080648
1989 213 Kingston 51.4116616 -0.2080648
1990 213 New Malden, Coombe Road 51.4094312 -0.2586718
2202 241 Plaistow 51.5268317 0.0308143
2203 241 Plaistow Abbey Arms 51.5222296 0.0225853
2204 241 Keir Hardie Estate 51.5678758 -0.0607466
2205 241 Canning Town 51.5189494 51.51395705923875 0.0132 0.00827546234713445
2206 241 Canning Town, Barking Road 51.5198077 0.0168462
2207 242 Homerton Hospital 51.5478609 -0.0425903
2208 242 Clapton Park, Millfields 51.5575011 -0.0424329
2483 274 Baker Street Station 51.5231548 -0.156863
2484 274 Portman Square 51.5161534 -0.1560125
2485 274 Marble Arch 51.5132225 -0.1588937
2486 275 St James Street 51.5064993 51.581118791593234 -0.1393328 -0.0328839757764101
2487 275 Walthamstow Central 51.5830128 -0.019886
2488 275 Walthamstow, Beacontree Avenue 51.595736 0.0045318
2489 275 Mill lane 51.5521985 -0.1932197
2685 300 Plaistow, Greengate Street 51.527444 0.027621
2686 300 Plaistow, Balaam Street 51.529212 0.0243676
2687 300 Canning Town, Barking Road 51.5208364 0.0191093
2688 300 Canning Town 51.5189494 51.51395705923875 0.0132 0.00827546234713445
2689 302 Mill Hill Broadway 51.6129292 -0.2487871
2690 302 Burnt Oak 51.602809 -0.266965
2691 302 Burnt Oak 51.602809 -0.266965
2716 308 Stratford City 51.5440354 -0.0053088
2717 308 Homerton Hospital 51.5478609 -0.0425903
2718 308 Clapton Pond 51.5561061 -0.05490830000000001
2719 309 Canning Town 51.5189494 51.51395705923875 0.0132 0.00827546234713445
2720 309 Aberfeldy Estate 51.4309209 -0.0936496
2721 309 Poplar, All Saints 51.5105521 -0.0118612
2722 309 Poplar, Cordelia Street 51.5137347 -0.0174897
2805 322 Brixton 51.4612794 -0.1156148
2806 322 Clapham North 51.4658813 -0.1413263
2807 322 Clapham Common 51.4589252 -0.1493071
2808 323 Canning Town 51.5189494 51.51395705923875 0.0132 0.00827546234713445
2809 323 East London Mail Centre 51.55633 0.0655092
2810 323 Mile End 51.52354529999999 -0.0330122
2811 324 Stanmore Station 51.617676 -0.311451
2862 330 Upton Park, Boleyn 51.5305005 0.0384915
2863 330 Plaistow, Balaam Street 51.529212 0.0243676
2864 330 Canning Town, Barking Road 51.521274 0.0207211
2865 330 Canning Town 51.5189494 51.51395705923875 0.0132 0.00827546234713445
2866 331 Ruislip 51.5758719 -0.421236
2867 331 Ruislip Lido 51.59114049999999 -0.4304918
2868 331 Northwood Station 51.6112297 -0.423889
3206 397 Crooked Billet, Sainsbury's 51.601088 -0.0159737
3207 397 Chingford Mount 51.6185735 -0.0180318
3208 397 Chingford Station 51.6331421 0.0098588
3209 397 Loughton 51.655942 51.64195093723499 0.068161 0.05498357119784937
3210 397 Debden 51.5417031 51.645776846753535 0.2034589 0.08200009471482733
3211 398 Ruislip 51.5758719 -0.421236
3212 398 Rayners Lane Station 51.5751034 -0.3708618
3213 398 South Harrow 51.5683717 -0.3553483
3521 473 Plaistow 51.5268317 0.0308143
3522 473 Stratford 51.5426313 -0.0010369
3523 474 Canning Town, Barking Road 51.52364720000001 0.0258453
3524 474 Canning Town 51.5189494 51.51395705923875 0.0132 0.00827546234713445
3525 474 London City Airport 51.5048437 0.049518
3526 474 North Woolwich 51.5008658 0.0626916
3527 474 Cyprus 51.5091192 0.0633823
3658 541 Prince Regent 51.4309209 -0.0936496
3659 549 South Woodford 51.5912671 0.0264721
3660 549 Buckhurst Hill 51.627572 0.034513
3661 549 Loughton 51.655942 51.64195093723499 0.068161 0.05498357119784937
3662 601 Thamesmead 51.50575809999999 0.1100586
3663 601 Bexley 51.439933 0.154327
3664 601 Wilmington Schools 51.4309209 -0.0936496
3803 673 Beckton Station 51.5144016 0.06153319999999999
3804 674 Harold Hill, Dagnam Park Square 51.6049149 0.2445527
3805 674 Romford Station 51.57472449999999 0.1826519
3806 675 St James Street 51.5064993 51.581118791593234 -0.1393328 -0.0328839757764101
3807 675 Woodbridge School 51.4309209 -0.0936496
3808 678 Stratford 51.5426313 -0.0010369
3809 678 NO BLIND DESCRIPTION (Departs only) 51.4309209 -0.0936496
4016 EL1 Barking 51.536563 0.075766
4017 EL1 River Road, Waverley Gardens 51.5343317 -0.2891878
4018 EL1 River Road, Waverley Gardens 51.5343317 -0.2891878
4019 EL1 Barking Riverside 51.536563 51.519303731473705 0.075766 0.11590257503355633
4020 EL2 Becontree Heath 51.5609465 0.1488995
4021 EL2 Five Elms 51.3697855 0.0259964
4022 EL2 Bennett's Castle Lane 51.5562949 0.1276031
4031 EL3 Barking 51.536563 0.075766
4032 EL3 Creekmouth 51.517381 0.102234
4033 EL3 Barking 51.536563 0.075766
4034 EL3 Barking Riverside 51.536563 51.519303731473705 0.075766 0.11590257503355633
4035 G1 Streatham, Green Lane 51.4147104 -0.1158693
4036 G1 Streatham, St Leonard's Church 51.4307467 -0.1294977
4037 G1 Tooting Broadway 51.427867 -0.1678142
4411 UL10 Liverpool Street 51.5175001 -0.0826966
4412 UL11 Canary Wharf 51.5054306 -0.0235333
4413 UL11 Stratford 51.5426313 -0.0010369
4414 UL12 Loughton 51.655942 51.64195093723499 0.068161 0.05498357119784937
4415 UL12 Snaresbrook 51.58567859999999 0.0084531
4416 UL12 Leyton 51.5702225 -0.0146938
4417 UL12 Stratford 51.5426313 -0.0010369
4438 UL19 Stonebridge Park 51.5445824 -0.2608244
4439 UL19 Queen's Park 51.5345448 -0.2043853
4440 UL20 Tower Hill 51.5095757 -0.0760083
4441 UL20 Canning Town 51.5189494 51.51395705923875 0.0132 0.00827546234713445
4442 UL20 Stratford 51.5426313 -0.0010369
4443 UL20 Stratford 51.5426313 -0.0010369
4444 UL20 East Ham 51.5333972 0.04991139999999999
4540 W11 Walthamstow Central 51.5830128 -0.019886
4541 W12 Walthamstow, Coppermill Lane 51.5803038 -0.0403725
4542 W12 Walthamstow Central 51.5830128 -0.019886
4543 W12 St James Street 51.5064993 51.581118791593234 -0.1393328 -0.0328839757764101
4544 W12 Whipps Cross 51.5095281 -0.229236
4545 W12 South Woodford 51.5095281 -0.229236
4546 W12 Wanstead 51.5767971 0.0249881
4559 W14 Leyton 51.4964278 -0.2085211
4560 W14 Leyton, Superstores 51.5558965 -0.0093311
4561 W15 Higham Hill, Cogan Avenue 51.6012336 -0.0363719
4562 W15 Forest Road, Palmerston Road 51.5858953 51.588898351903815 -0.0292291 -0.030623848707334166
4563 W15 Walthamstow Central 51.5830128 -0.019886
4564 W15 Leyton, Bakers Arms 51.5749185 -0.013549
4565 W15 Whipps Cross 51.581499 0.0001219
4576 W16 Leyton, Bakers Arms 51.5749185 -0.013549
4577 W16 Leytonstone 51.5649624 0.0088141
4578 W19 Walthamstow, Argall Avenue 51.5701165 -0.0387872
4579 W19 St James Street 51.5064993 51.581118791593234 -0.1393328 -0.0328839757764101
4580 W19 Walthamstow Central 51.5830128 -0.019886
4581 W19 Whipps Cross 51.4255297 -0.2050566
4582 W19 Leytonstone 51.5649624 0.0088141
4704 N15 East Ham, Newham Town Hall 51.53280239999999 0.0551608
4705 N15 Upton Park 51.53471750000001 0.0337596
4706 N15 Canning Town, Barking Road 51.5180017 0.0130984
4707 N15 Canning Town 51.5189494 51.51395705923875 0.0132 0.00827546234713445
4708 N15 Poplar, All Saints 51.5105521 -0.0118612
4709 N15 Limehouse, Burdett Road 51.5150014 -0.0285662
4710 N15 Aldgate 51.5134365 -0.0772463
5151 N550 Cannon Street 51.5119949 -0.091962
5152 N550 Limehouse, Burdett Road 51.5150014 -0.0285662
5153 N550 Poplar, All Saints 51.5105521 -0.0118612
5154 N550 Canning Town 51.5189494 51.51395705923875 0.0132 0.00827546234713445
5155 N551 Trafalgar Square 51.508039 -0.128069
5156 N551 Aldwych 51.5132441 -0.1172819
5157 N551 Aldgate 51.5134365 -0.0772463
5158 N551 Limehouse 51.5110598 -0.0366652
5159 N551 Poplar, All Saints 51.5105521 -0.0118612
5160 N551 Canning Town 51.5189494 51.51395705923875 0.0132 0.00827546234713445
5161 N551 Keir Hardie Estate 51.5678758 -0.0607466
5162 N551 Prince Regent 51.4309209 -0.0936496
5163 N551 Beckton Station 51.5144016 0.06153319999999999

File diff suppressed because it is too large Load Diff

View File

@@ -15,9 +15,8 @@ class AudioCache {
Uint8List? operator [](String key) {
// ignore case
key = key.toLowerCase();
for (var k in _audioCache.keys) {
if (k.toLowerCase() == key) {
if (k.toLowerCase() == key.toLowerCase()) {
return _audioCache[k];
}
}
@@ -35,7 +34,7 @@ class AnnouncementCache extends AudioCache {
// remove any announcements that are already loaded
for (var announcement in announcements) {
if (!_audioCache.containsKey(announcement.toLowerCase())) {
_announements.add(announcement);
_announements.add(announcement.toLowerCase());
}
}
@@ -53,7 +52,7 @@ class AnnouncementCache extends AudioCache {
filename = filename.split("/").last;
}
if (_announements.contains(filename)) {
if (_announements.contains(filename.toLowerCase())) {
_audioCache[filename.toLowerCase()] = file.content;
print("Loaded announcement: $filename");
}

View File

@@ -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";

View File

@@ -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 {

View File

@@ -10,18 +10,31 @@ import 'package:bus_infotainment/auth/api_constants.dart';
import 'package:bus_infotainment/auth/auth_api.dart';
import 'package:bus_infotainment/backend/modules/announcement.dart';
import 'package:bus_infotainment/backend/modules/commands.dart';
import 'package:bus_infotainment/backend/modules/directconnection.dart';
import 'package:bus_infotainment/backend/modules/networking.dart';
import 'package:bus_infotainment/backend/modules/synced_time.dart';
import 'package:bus_infotainment/backend/modules/tracker.dart';
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;
import 'package:ntp/ntp.dart';
import 'package:permission_handler/permission_handler.dart';
import '../tfl_datasets.dart';
enum RoomConnectionMethod {
Cloud,
Local,
P2P,
None
}
class LiveInformation {
static final LiveInformation _singleton = LiveInformation._internal();
@@ -37,18 +50,49 @@ class LiveInformation {
{
// By default, load the bus sequences from the assets
print("Loading bus sequences from assets");
String destinations = await rootBundle.loadString("assets/datasets/destinations.json");
String routes = await rootBundle.loadString("assets/datasets/bus-sequences.csv");
// Try to grab the routes from TfL
try {
http.Response response = await http.get(Uri.parse('https://tfl.gov.uk/bus-sequences.csv'));
routes = response.body;
print("Loaded bus sequences from TFL");
} catch (e) {
print("Failed to load bus sequences from TFL. Using local copy.");
}
// Try to grab the destinations from github
try {
http.Response response = await http.get(Uri.parse('https://raw.githubusercontent.com/RailboundStudios/LondonBusDatasets/main/destinations.json'));
destinations = response.body;
print("Loaded destinations from Github");
} catch (e) {
print("Failed to load destinations from Github. Using local copy.");
}
busSequences = BusSequences.fromCSV(
await rootBundle.loadString("assets/datasets/bus-blinds.csv"),
await rootBundle.loadString("assets/datasets/bus-sequences.csv")
destinations,
routes
);
print("Loaded bus sequences from assets");
print("Loaded all datasets");
try {
http.Response response = await http.get(Uri.parse('https://tfl.gov.uk/bus-sequences.csv'));
busSequences = BusSequences.fromCSV(
await rootBundle.loadString("assets/datasets/bus-blinds.csv"),
await rootBundle.loadString("assets/datasets/destinations.json"),
response.body
);
@@ -62,12 +106,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 +115,16 @@ class LiveInformation {
initTrackerModule();
print("Initialised LiveInformation");
if (!auth.isAuthenticated()) {
auth.loadAnonymousUser();
}
networkingModule = NetworkingModule();
if (defaultTargetPlatform == TargetPlatform.android){
p2pModule = NearbyServiceWrapper();
}
}
Future<void> initTrackerModule() async {
@@ -85,22 +133,38 @@ class LiveInformation {
}
}
// Multi-device stuff
RoomConnectionMethod connectionMethod = RoomConnectionMethod.None;
// Auth
AuthAPI auth = AuthAPI();
AuthAPI auth = AuthAPI(
autoLoad: false,
);
String? roomCode;
String? roomDocumentID;
bool isHost = false;
bool inRoom = false;
appwrite.RealtimeSubscription? _subscription;
RealtimeKeepAliveConnection? _keepAliveConnection; // This is a workaround for a bug in the appwrite SDK
// Local room stuff
ListenerReceipt<String>? _listenerReciept;
// Modules
late CommandModule commandModule;
// late CommandModule commandModule; This needs to be deprecated
late BusSequences busSequences;
late AnnouncementModule announcementModule;
late SyncedTimeModule syncedTimeModule;
late TrackerModule trackerModule;
late TubeStations tubeStations;
late NetworkingModule networkingModule;
late NearbyServiceWrapper p2pModule;
// Important variables
BusRouteVariant? _currentRouteVariant;
// Events
EventDelegate<BusRouteVariant> routeVariantDelegate = EventDelegate();
EventDelegate<BusRouteVariant?> routeVariantDelegate = EventDelegate();
// Internal methods
@@ -108,7 +172,59 @@ class LiveInformation {
Future<void> setRouteVariant_Internal(BusRouteVariant routeVariant) async {
Future<void> setRouteVariant(BusRouteVariant? routeVariant, {bool sendToServer = false}) 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");
}
}
if (inRoom && sendToServer) {
SendCommand("setroute ${routeVariant.busRoute.routeNumber} ${routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant)}");
}
Continuation:
// Set the current route variant
_currentRouteVariant = routeVariant;
@@ -135,6 +251,10 @@ class LiveInformation {
]
);
// Display the route variant
announcementModule.queueAnnouncementByRouteVariant(routeVariant: routeVariant);
}
// Public methods
@@ -143,16 +263,503 @@ class LiveInformation {
return _currentRouteVariant;
}
Future<void> setRouteVariant(BusRouteVariant routeVariant) async {
await commandModule.executeCommand(
"setroute ${routeVariant.busRoute.routeNumber} ${routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant)}"
Future<void> setRouteVariantQuery(String routeNumber, int routeVariantIndex, {bool sendToServer = false}) async {
BusRoute route = busSequences.routes[routeNumber]!;
BusRouteVariant routeVariant = route.routeVariants.values.toList()[routeVariantIndex];
await setRouteVariant(
routeVariant,
sendToServer: sendToServer
);
}
// Multi device support
Future<void> createRoom(String roomCode) async {
{
// Local Room
await networkingModule.startWebSocketServer();
inRoom = true;
_listenerReciept = networkingModule.onMessageReceived?.addListener(
(p0) {
print("Received local command: $p0");
executeCommand(p0);
}
);
}
{
// Cloud Room
print("Creating room with code $roomCode");
// Update the room code
this.roomCode = roomCode;
// Enable host mode
isHost = true;
inRoom = true;
// Access the database
final client = auth.client;
final databases = appwrite.Databases(client);
// Remove any existing documents
final existingDocuments = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode)
]
);
for (var document in existingDocuments.documents) {
await databases.deleteDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: document.$id
);
}
// Create the document
final document = await databases.createDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: appwrite.ID.unique(),
data: {
"SessionID": roomCode,
"LastUpdater": auth.userID,
}
);
// Listen for changes
if (_keepAliveConnection != null) {
try {
_keepAliveConnection!.close();
} catch (e) {
print("Failed to close connection... oh well");
}
}
String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1";
String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim();
_keepAliveConnection = RealtimeKeepAliveConnection(
channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'],
onData: ServerListener,
domain: domain,
client: auth.client,
onError: (e) {
print("Workarround Error: $e");
},
);
_keepAliveConnection!.initialize();
// Update the room document ID
roomDocumentID = document.$id;
print("Created room with code $roomCode");
}
}
Future<bool> joinRoom(String info) async {
String infoJson = utf8.decode(base64.decode(info));
try {
{
// sync
String routeNumber = jsonDecode(infoJson)["sync"]["route"];
int routeVariantIndex = jsonDecode(infoJson)["sync"]["routeVariant"];
setRouteVariantQuery(routeNumber, routeVariantIndex);
LiveInformation().announcementModule.queueAnnouncementByRouteVariant(routeVariant: _currentRouteVariant!, sendToServer: false);
}
} catch (e) {
print("Failed to sync route");
}
{
// Local Room
String host = jsonDecode(infoJson)["local"]["host"];
if (await networkingModule.connectToWebSocketServer(host)){
print("Connected to local room at $host");
_listenerReciept = networkingModule.onMessageReceived?.addListener(
(p0) {
print("Received local command: $p0");
executeCommand(p0);
}
);
inRoom = true;
connectionMethod = RoomConnectionMethod.Local;
return true; // We dont need to connect to the cloud room if we are connected to the local room.
} else {
print("Failed to connect to local room at $host");
print("Falling back to cloud room");
}
}
{
// Cloud Room
String roomCode = jsonDecode(infoJson)["cloud"]["roomCode"];
print("Joining cloud 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) {
return false;
}
final document = response.documents.first;
// Listen for changes
if (_keepAliveConnection != null) {
try {
_keepAliveConnection!.close();
} catch (e) {
print("Failed to close connection... oh well");
}
}
String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1";
String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim();
_keepAliveConnection = RealtimeKeepAliveConnection(
channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'],
onData: ServerListener,
domain: domain,
client: auth.client,
onError: (e) {
print("Workarround Error: $e");
},
);
_keepAliveConnection!.initialize();
// Update the room document ID
roomDocumentID = document.$id;
// Get the current route
try {
String routeNumber = document.data["CurrentRoute"];
int routeVariantIndex = document.data["CurrentRouteVariant"];
await setRouteVariantQuery(routeNumber, routeVariantIndex);
print("Set route to $routeNumber $routeVariantIndex");
} catch (e) {
print("Failed to set route");
}
inRoom = true;
connectionMethod = RoomConnectionMethod.Cloud;
print("Joined cloud room with code $roomCode");
return true;
}
}
Future<void> leaveRoom() async {
if (!inRoom) {
return;
}
{
// Local Room
networkingModule.stopWebSocketServer();
inRoom = false;
networkingModule.onMessageReceived?.removeListener(_listenerReciept!);
}
{
// Cloud Room
if (_keepAliveConnection != null) {
_keepAliveConnection!.close();
_keepAliveConnection = null;
}
}
inRoom = false;
if (isHost) {
// Access the database
final client = auth.client;
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
);
}
}
roomCode = null;
roomDocumentID = null;
isHost = false;
inRoom = false;
_keepAliveConnection?.close();
_keepAliveConnection = null;
// Reset stuff
setRouteVariant(null);
}
String generateRoomInfo() {
// Room Info Example
/*
{
"cloud": {
"roomCode": "6633e85d0020f52f3771"
},
"local":
{
"host": "ws://192.168.0.123:8080"
},
"sync":
{
"route": "W11",
"routeVariant": 1
}
}
*/
String json = jsonEncode({
"cloud": {
"roomCode": roomCode,
},
"local": {
"host": "ws://${networkingModule.localIP}:8080"
},
if (_currentRouteVariant != null)
"sync": {
"route": _currentRouteVariant!.busRoute.routeNumber,
"routeVariant": _currentRouteVariant!.busRoute.routeVariants.values.toList().indexOf(_currentRouteVariant!),
}
});
// Encode in base64
return base64Encode(utf8.encode(json));
}
String? lastCommand;
Future<void> 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,
sendToServer: false
);
// announce the route
// announcementModule.queueAnnouncementByRouteVariant(routeVariant: _currentRouteVariant!);
}
} catch (e) {
print("Failed to set route");
}
// Execute the command
List<String> commands = response.payload["Commands"].cast<String>();
executeCommand(commands.last);
}
void executeCommand(String command) {
if (command == lastCommand) {
return;
}
print("Executing command: $command");
lastCommand = command;
List<String> 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.queueAnnouncementByInfoIndex(
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<String> 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
);
}
} else if (commandName == "setroute") {
print("Set route command received");
String routeNumber = commandParts[1];
int routeVariantIndex = int.parse(commandParts[2]);
setRouteVariantQuery(routeNumber, routeVariantIndex, sendToServer: false);
}
}
String _extractId(String input) {
RegExp regExp = RegExp(r'\("user:(.*)"\)');
Match match = regExp.firstMatch(input)!;
return match.group(1)!;
}
Future<void> SendCommand(String command) async {
{
// Wfi Direct Commands
p2pModule.sendMessage(command);
}
{
// Local Commands
try {
networkingModule.sendMessage(command);
} catch (e) {
print("Failed to send local command: $e");
}
}
{
// Cloud Commands
final client = auth.client;
final databases = appwrite.Databases(client);
final response = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode!)
]
);
List<String> 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<String> _splitCommand(String command) {
var regex = RegExp(r'([^\s"]+)|"([^"]*)"');
var matches = regex.allMatches(command);
return matches.map((match) => match.group(0)!.replaceAll('"', '')).toList();
}
@@ -162,8 +769,6 @@ class LiveInformation {
}
class AnnouncementQueueEntry {

View File

@@ -27,7 +27,7 @@ class AnnouncementModule extends InfoModule {
// Files
String _bundleLocation = "assets/ibus_recordings.zip";
Uint8List? _bundleBytes;
void setBundleBytes(Uint8List bytes) {
void setBundleBytes(Uint8List? bytes) {
_bundleBytes = bytes;
}
Future<Uint8List> getBundleBytes() async {
@@ -35,7 +35,6 @@ class AnnouncementModule extends InfoModule {
if (_bundleBytes != null) {
return _bundleBytes!;
} else {
// Try to load them from shared preferences
try {
SharedPreferences prefs = await SharedPreferences.getInstance();
@@ -47,17 +46,7 @@ class AnnouncementModule extends InfoModule {
} catch (e) {
throw Exception("Loading announcements from assets has been deprecated.");
}
// if (kIsWeb) {
// throw Exception("Cannot load bundle bytes on web");
// }
//
// final bytes = await rootBundle.load(_bundleLocation);
// return bytes.buffer.asUint8List();
}
}
// Queue
@@ -74,7 +63,7 @@ class AnnouncementModule extends InfoModule {
final EventDelegate<AnnouncementQueueEntry> 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 +73,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,38 +94,24 @@ 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));
// await Future.delayed(const Duration(seconds: 2));
}
}
@@ -179,7 +154,14 @@ class AnnouncementModule extends InfoModule {
}
// Configuration
int get defaultAnnouncementDelay => liveInformation.auth.isAuthenticated() ? 2 : 0;
Duration get defaultAnnouncementDelay {
if (liveInformation.inRoom) {
return Duration(milliseconds: 1000);
} else {
print("Not in room");
return Duration.zero;
}
}
// Methods
Future<void> queueAnnounceByAudioName({
@@ -189,18 +171,23 @@ class AnnouncementModule extends InfoModule {
bool sendToServer = true
}) async {
if (sendToServer) {
if (sendToServer && _shouldSendToServer()) {
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay));
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(defaultAnnouncementDelay);
String audioNamesString = "";
for (var audioName in audioNames) {
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;
@@ -234,19 +221,23 @@ class AnnouncementModule extends InfoModule {
}
void queueAnnounementByInfoIndex({
void queueAnnouncementByInfoIndex({
int infoIndex = -1,
DateTime? scheduledTime = null,
bool sendToServer = true
}) {
if (sendToServer) {
if (sendToServer && _shouldSendToServer()) {
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay));
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(defaultAnnouncementDelay);
liveInformation.commandModule.executeCommand(
"announce info $infoIndex ${scheduledTime?.millisecondsSinceEpoch ?? ""}"
liveInformation.SendCommand("announce info $infoIndex ${scheduledTime.millisecondsSinceEpoch}");
queueAnnouncementByInfoIndex(
infoIndex: infoIndex,
scheduledTime: scheduledTime,
sendToServer: false
);
print("Announcement sent to server");
return;
}
@@ -266,13 +257,22 @@ class AnnouncementModule extends InfoModule {
bool sendToServer = true
}) async {
if (sendToServer) {
if (sendToServer && _shouldSendToServer()) {
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay));
print("Sending route announcement to server");
liveInformation.commandModule.executeCommand(
"announce dest \"${routeVariant.busRoute.routeNumber}\" ${routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant)} ${scheduledTime?.millisecondsSinceEpoch ?? ""}"
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(defaultAnnouncementDelay);
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");
@@ -293,10 +293,14 @@ class AnnouncementModule extends InfoModule {
print("Checkpoint 5");
await announcementCache.loadAnnouncementsFromBytes(await getBundleBytes(), [audioRoute]);
print("Checkpoint 6");
AudioWrapperSource sourceRoute = !routeNumber.toLowerCase().startsWith("ul") ?
late AudioWrapperSource sourceRoute;
try {
sourceRoute = !routeNumber.toLowerCase().startsWith("ul") ?
AudioWrapperByteSource(announcementCache[audioRoute]!) :
// AudioWrapperByteSource(announcementCache["R_RAIL_REPLACEMENT_SERVICE_001.mp3"]!);
AudioWrapperAssetSource("audio/R_RAIL_REPLACEMENT_SERVICE_001.mp3");
} catch (e) {
sourceRoute = AudioWrapperAssetSource("audio/R_SPECIAL_SERVICE_001.mp3");
}
if (routeNumber.toLowerCase().startsWith("ul")) {
@@ -319,6 +323,14 @@ class AnnouncementModule extends InfoModule {
queue.add(announcement);
}
// Server check
bool _shouldSendToServer() {
bool condition = liveInformation.inRoom;
print("Should send to server? " + (condition.toString()));
return condition;
}
// Constants
final List<NamedAnnouncementQueueEntry> manualAnnouncements = [

View File

@@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:bus_infotainment/auth/api_constants.dart';
import 'package:bus_infotainment/backend/live_information.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:appwrite/appwrite.dart' as appwrite;
@@ -47,17 +48,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<String> 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 +99,10 @@ class CommandModule extends InfoModule {
if (command == "Response:") {
}
else if (command == "initroom") {
// initroom <roomCode>
}
else if (command == "announce") {
@@ -120,7 +145,7 @@ class CommandModule extends InfoModule {
}
} catch (e) {}
liveInformation.announcementModule.queueAnnounementByInfoIndex(
liveInformation.announcementModule.queueAnnouncementByInfoIndex(
infoIndex: InfoIndex,
scheduledTime: scheduledTime,
sendToServer: false
@@ -166,10 +191,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 +237,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");

View File

@@ -0,0 +1,123 @@
import 'dart:async';
import 'package:bus_infotainment/backend/live_information.dart';
import 'package:flutter/foundation.dart';
import 'package:nearby_service/nearby_service.dart';
import 'package:nearby_service/src/model/model.dart';
class NearbyServiceWrapper {
late NearbyService _nearbyService;
final ValueNotifier<bool> isConnected = ValueNotifier(false);
final ValueNotifier<NearbyDevice?> connectedDevice = ValueNotifier(null);
NearbyServiceWrapper() {
_nearbyService = NearbyService.getInstance();
_initializeService();
}
Future<void> _initializeService() async {
await _nearbyService.initialize();
_listenForIncomingConnections();
await startDiscovery();
}
Future<void> startDiscovery() async {
await _nearbyService.discover();
}
Future<void> stopDiscovery() async {
await _nearbyService.stopDiscovery();
}
Future<List<NearbyDevice>> getDiscoveredDevices() async {
List<NearbyDevice> devices = await _nearbyService.getPeers();
// Remove devices that we should avoid
devices.removeWhere((element) => _deviceNamesToAvoid.any((avoid) => element.info.displayName.contains(avoid)));
return devices;
}
Future<void> connectToDevice(NearbyDevice device) async {
bool success = await _nearbyService.connect(device);
if (success) {
connectedDevice.value = device;
isConnected.value = true;
_startCommunicationChannel();
}
}
Future<void> _startCommunicationChannel() async {
await _nearbyService.startCommunicationChannel(
NearbyCommunicationChannelData(
connectedDevice.value!.info.id,
messagesListener: NearbyServiceMessagesListener(
onCreated: () {
print('Communication channel created');
},
onData: (ReceivedNearbyMessage<NearbyMessageContent> value) {
if (value.content is NearbyMessageTextRequest) {
String message = (value.content as NearbyMessageTextRequest).value;
print('Received message: ${message}');
LiveInformation().executeCommand(message);
}
}
)
)
);
}
Future<void> sendMessage(String message) async {
if (isConnected.value && connectedDevice.value != null) {
await _nearbyService.send(
OutgoingNearbyMessage(
content: NearbyMessageTextRequest.create(value: message),
receiver: connectedDevice.value!.info,
)
);
}
}
Future<void> disconnect() async {
if (isConnected.value && connectedDevice.value != null) {
await _nearbyService.disconnect(connectedDevice.value);
connectedDevice.value = null;
isConnected.value = false;
}
}
void _listenForIncomingConnections() {
_nearbyService.getPeersStream().listen((peers) async {
for (var peer in peers) {
// Lets avoid accidentally connecting to things
if (_deviceNamesToAvoid.any((element) => peer.info.displayName.contains(element))) {
print('Avoiding device: ${peer.info.displayName}');
continue;
}
if (!isConnected.value) {
await connectToDevice(peer);
print('Reconnected to: ${peer.info.displayName}');
}
}
});
_nearbyService.getConnectedDeviceStream(connectedDevice.value!).listen((device) {
if (device == null) {
isConnected.value = false;
connectedDevice.value = null;
}
});
}
ValueListenable<NearbyDevice?> get connectedDeviceNotifier => connectedDevice;
ValueListenable<bool> get isConnectedNotifier => isConnected;
}
// If a device name contains any of these strings, it will be avoided
List<String> _deviceNamesToAvoid = [
"DIRECT-", // Avoid connecting to printers
];

View File

@@ -0,0 +1,221 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:network_info_plus/network_info_plus.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:bus_infotainment/backend/modules/info_module.dart';
import 'package:bus_infotainment/utils/delegates.dart';
import 'package:http/http.dart' as http;
class NetworkingModule extends InfoModule {
// Host websocket server
int webSocketPort = 8080;
int httpPort = 8081;
HttpServer? _sockerServer;
HttpServer? _httpServer;
WebSocketChannel? _channel;
// Store connected WebSocket channels
final List<WebSocketChannel> _connectedClients = [];
EventDelegate<String>? onMessageReceived = EventDelegate();
NetworkingModule() {
_refresh();
refreshTimer();
}
Future<bool> startWebSocketServer() async {
try {
var handler = webSocketHandler((WebSocketChannel webSocket) {
_connectedClients.add(webSocket); // Add the client to the list
print('Client connected: ${webSocket}'); // Log client connection
webSocket.stream.listen((message) {
// Handle messages from the client here
print('Received message: $message');
// Forward message to all clients except the sender
for (var client in _connectedClients) {
if (client != webSocket) {
client.sink.add(message);
}
}
_onMessageReceived(message);
}, onDone: () {
_connectedClients.remove(webSocket); // Remove client on disconnect
print('Client disconnected: ${webSocket}'); // Log client disconnection
});
});
_sockerServer = await io.serve(handler, InternetAddress.anyIPv4, webSocketPort);
print('WebSocket server started at ${_sockerServer?.address.address}:${_sockerServer?.port}');
// Start Http api server
_httpServer = await io.serve((Request request) async {
return Response.ok('bus infotainment server');
}, InternetAddress.anyIPv4, httpPort);
return true;
} catch (e) {
print('Failed to start WebSocket server: $e');
return false;
}
}
bool stopWebSocketServer() {
if (_sockerServer == null) {
throw Exception('WebSocket server is not running');
}
try {
for (var client in _connectedClients) {
client.sink.close();
}
_connectedClients.clear();
_sockerServer?.close(force: true);
_sockerServer = null;
_httpServer?.close(force: true);
_httpServer = null;
print('WebSocket server stopped');
return true;
} catch (e) {
print('Failed to stop WebSocket server: $e');
return false;
}
}
Future<bool> connectToWebSocketServer(String url) async {
try {
{
// Verify that the server we are connecting to is running, and is a bus infotainment server
var response = await http.get(Uri.parse('http://$url:$httpPort'));
if (response.statusCode != 200 || response.body != 'bus infotainment server') {
print('Server at $url is not a bus infotainment server');
return false;
}
}
_channel = await WebSocketChannel.connect(Uri.parse(url));
_channel?.stream.listen((message) {
// Handle messages from the server here
print('Received message from server: $message');
_onMessageReceived(message);
});
print('Connected to WebSocket server at $url');
return true;
} catch (e) {
print('Failed to connect to WebSocket server: $e');
return false;
}
}
bool disconnectFromWebSocketServer() {
if (_channel == null) {
throw Exception('No active WebSocket connection');
}
try {
_channel?.sink.close();
_channel = null;
print('Disconnected from WebSocket server');
return true;
} catch (e) {
print('Failed to disconnect from WebSocket server: $e');
return false;
}
}
bool sendMessage(String message) {
// If hosting a server, send message to all clients
if (_sockerServer != null) {
return sendMessageToClients(message);
}
if (_channel == null) {
throw Exception('No active WebSocket connection');
}
try {
_channel?.sink.add(message);
print('Sent message: $message');
return true;
} catch (e) {
print('Failed to send message: $e');
return false;
}
}
bool sendMessageToClients(String message) {
if (_connectedClients.isEmpty) {
print('No clients connected');
return false;
}
try {
for (var client in _connectedClients) {
client.sink.add(message);
}
print('Sent message to all clients: $message');
return true;
} catch (e) {
print('Failed to send message to clients: $e');
return false;
}
}
void _onMessageReceived(String message) {
// Notify all listeners that a message has been received.
onMessageReceived?.trigger(message);
}
// Useful boilerplate
String _localIP = "";
String get localIP => _localIP;
Timer refreshTimer() => Timer.periodic(const Duration(seconds: 10), (timer) {
if (kIsWeb) return;
_refresh();
});
Future<void> _refresh() async {
print("Refreshing network info...");
{
// Update the local IP address
// First try NetworkInfo
_localIP = (await NetworkInfo().getWifiIP()) ?? "";
// If null, try NetworkInterface
// Only look for ethernet. Wifi would have been found by NetworkInfo
if (_localIP.isEmpty) {
for (var interface in await NetworkInterface.list()) {
if (!interface.name.toLowerCase().contains("eth") || interface.name.contains(" ")) {
continue;
}
for (var addr in interface.addresses) {
print('Interface ${interface.name} has address ${addr.address}');
if (addr.type == InternetAddressType.IPv4 && !addr.isLoopback) {
_localIP = addr.address;
break;
}
}
}
}
}
}
}

View File

@@ -5,13 +5,14 @@ import 'dart:typed_data';
import 'package:bus_infotainment/backend/live_information.dart';
import 'package:bus_infotainment/backend/modules/info_module.dart';
import 'package:bus_infotainment/tfl_datasets.dart';
import 'package:bus_infotainment/utils/OrdinanceSurveyUtils.dart';
import 'package:bus_infotainment/utils/audio%20wrapper.dart';
import 'package:flutter/foundation.dart';
import 'package:geolocator/geolocator.dart';
import 'package:vector_math/vector_math.dart';
import '../../tfl_datasets.dart';
class TrackerModule extends InfoModule {
// Constructor
@@ -183,7 +184,6 @@ class TrackerModule extends InfoModule {
print("Closest stop: ${closestStop.formattedStopName} in ${closestDistance.round()} meters");
}
}
double _calculateRelativeDistance(BusRouteStop stop, double latitude, double longitude) {

View File

@@ -1,11 +1,10 @@
import 'package:bus_infotainment/tfl_datasets.dart';
import 'package:bus_infotainment/utils/OrdinanceSurveyUtils.dart';
import 'package:bus_infotainment/utils/audio%20wrapper.dart';
import 'package:vector_math/vector_math.dart';
import '../../tfl_datasets.dart';
class TubeStations {
List<TubeStation> stations = [];
@@ -68,7 +67,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;
}

View File

@@ -9,8 +9,10 @@ import 'package:bus_infotainment/remaster/dashboard.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shelf/shelf.dart';
import 'package:window_manager/window_manager.dart';
import 'package:bus_infotainment/remaster/InitialStartup.dart' as remaster;
import 'package:shelf/shelf_io.dart' as shelf_io;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -19,10 +21,11 @@ void main() async {
await windowManager.ensureInitialized();
WindowOptions options = WindowOptions(
size: Size(411.4, 850.3),
title: 'Bus Infotainment',
);
windowManager.setAspectRatio(411.4 / 850.3);
// windowManager.setAspectRatio(411.4 / 850.3);
await windowManager.waitUntilReadyToShow(options, () async {
await windowManager.show();
@@ -30,22 +33,31 @@ void main() async {
});
}
// {
// // Web server test
//
// var handler = const Pipeline().addMiddleware(logRequests()).addHandler((Request request) {
// return Response.ok('Hello, world!');
// });
// var server = await shelf_io.serve(handler, '0.0.0.0', 8080);
// server.autoCompress = true;
//
// print('Serving at http://${server.address.host}:${server.port}');
//
// // get the IP address
// for (var interface in await NetworkInterface.list()) {
// for (var addr in interface.addresses) {
// print('Interface ${interface.name} has address ${addr.address}');
// print('Try http://${addr.address}:${server.port}');
// }
// }
//
// }
LiveInformation liveInformation = LiveInformation();
await liveInformation.initialize();
runApp(const MyApp());
// Disalow screen to turn off on android
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
// Disalow landscape mode
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
}
class MyApp extends StatelessWidget {

View File

@@ -26,6 +26,7 @@ class _ibus_displayState extends State<ibus_display> {
late final ListenerReceipt<AnnouncementQueueEntry> _receipt;
_ibus_displayState(){
LiveInformation liveInformation = LiveInformation();
@@ -42,12 +43,16 @@ class _ibus_displayState extends State<ibus_display> {
});
topLine = liveInformation.announcementModule.currentAnnouncement?.displayText ?? liveInformation.announcementModule.defaultText;
}
String _padString(String input){
if (input.length < 30){
print("Input is too short");
// print("Input is too short");
return input;
}
@@ -68,6 +73,8 @@ class _ibus_displayState extends State<ibus_display> {
LiveInformation().announcementModule.onAnnouncement.removeListener(_receipt);
super.dispose();
}
@@ -157,25 +164,11 @@ class _ibus_displayState extends State<ibus_display> {
),
Transform.translate(
offset: Offset(0, -6),
offset: Offset(0, -3.5),
child: Container(
alignment: Alignment.center,
width: 32*4*3,
child: TextScroll(
_padString(bottomLine),
velocity: Velocity(pixelsPerSecond: Offset(120, 0)),
style: const TextStyle(
fontSize: 20,
color: Colors.orange,
fontFamily: "ibus",
shadows: [
Shadow(
color: Colors.orange,
blurRadius: 5,
),
],
),
),
child: _timeComponent(),
),
),
],
@@ -200,4 +193,96 @@ class _ibus_displayState extends State<ibus_display> {
),
);
}
}
class _timeComponent extends StatefulWidget {
@override
State<_timeComponent> createState() => _timeComponentState();
}
class _timeComponentState extends State<_timeComponent> {
late Timer timeTimer;
String bottomLine = "";
@override
void initState() {
// TODO: implement initState
super.initState();
bottomLine = _getTime();
timeTimer = Timer.periodic(Duration(seconds: 1), (timer) {
if (mounted){
setState(() {
bottomLine = _getTime();
});
}
});
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
timeTimer.cancel();
}
String _getTime(){
// Get the current time HH:MM AM/PM
DateTime now = DateTime.now();
bool is24Hour = false;
// if 16 then 4...
String timeString = "${now.hour % 12}:${now.minute.toString().padLeft(2, "0")} ${now.hour < 12 ? "AM" : "PM"}";
if (timeString.startsWith("0:")){
timeString = timeString.replaceAll("0:", "12:");
}
return timeString;
}
String _padString(String input){
if (input.length < 30){
// print("Input is too short");
return input;
}
String prefix = "";
String suffix = "";
for (int i = 0; i < 80; i++){
prefix += " ";
}
input = input.replaceAll("©", "(c)");
return prefix + input + suffix;
}
@override
Widget build(BuildContext context) {
return TextScroll(
_padString(bottomLine),
velocity: Velocity(pixelsPerSecond: Offset(120, 0)),
style: const TextStyle(
fontSize: 20,
color: Colors.orange,
fontFamily: "ibus",
shadows: [
Shadow(
color: Colors.orange,
blurRadius: 5,
),
],
),
);
}
}

View File

@@ -74,11 +74,6 @@ class _pages_DisplayState extends State<pages_Display> {
});
// Hide the notification bar and make the app full screen and display over notch
if (widget._tfL_Dataset_TestState.hideUI) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
}
},
),

View File

@@ -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,13 +61,13 @@ 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,
onPressed: (){
LiveInformation liveInformation = LiveInformation();
liveInformation.announcementModule.queueAnnounementByInfoIndex(
liveInformation.announcementModule.queueAnnouncementByInfoIndex(
infoIndex: liveInformation.announcementModule.manualAnnouncements.indexOf(announcement),
sendToServer: true
);
@@ -92,12 +93,12 @@ class pages_Home extends StatelessWidget {
color: Colors.grey.shade900,
),
child: DelegateBuilder<BusRouteVariant>(
child: DelegateBuilder<BusRouteVariant?>(
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<Widget> 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<AnnouncementPicker> createState() => _AnnouncementPickerState();
@@ -411,9 +340,9 @@ class _AnnouncementPickerState extends State<AnnouncementPicker> {
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<AnnouncementPicker> {
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<AnnouncementPicker> {
)
),
),
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<AnnouncementPicker> {
)
),
),
),
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,
),

View File

@@ -1228,11 +1228,11 @@ class _ConsoleState extends State<Console> {
// 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<Console> {
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<Console> {
)
);
}
*/
return Container(
decoration: BoxDecoration(
@@ -1299,7 +1299,7 @@ class _ConsoleState extends State<Console> {
color: Colors.white70,
),
Container(
/*Container(
height: 50,
padding: const EdgeInsets.all(8),
child: TextField(
@@ -1315,7 +1315,7 @@ class _ConsoleState extends State<Console> {
LiveInformation().commandModule.executeCommand(value);
},
),
)
)*/
],

View File

@@ -0,0 +1,734 @@
import 'package:bus_infotainment/backend/live_information.dart';
import 'package:bus_infotainment/pages/components/ibus_display.dart';
import 'package:bus_infotainment/remaster/dashboard.dart';
import 'package:bus_infotainment/tfl_datasets.dart';
import 'package:bus_infotainment/utils/delegates.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_scroll_shadow/flutter_scroll_shadow.dart';
import 'package:network_info_plus/network_info_plus.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
class ArcDashboard extends StatefulWidget {
@override
State<ArcDashboard> createState() => _ArcDashboardState();
}
String _rmconString(RoomConnectionMethod method) {
switch (method) {
case RoomConnectionMethod.Cloud:
return "Cloud";
case RoomConnectionMethod.Local:
return "Local";
case RoomConnectionMethod.P2P:
return "P2P";
case RoomConnectionMethod.None:
return "None";
}
}
class _ArcDashboardState extends State<ArcDashboard> {
_closeDialogueChecker closeDialogWidget = _closeDialogueChecker();
late ListenerReceipt<BusRouteVariant?> onRouteVariantChange;
@override
void initState() {
// TODO: implement initState
super.initState();
onRouteVariantChange = LiveInformation().routeVariantDelegate.addListener((value) {
print("Route variant changed");
setState(() {
});
});
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
LiveInformation().routeVariantDelegate.removeListener(onRouteVariantChange);
}
@override
Widget build(BuildContext context) {
return PopScope(
onPopInvoked: (isPop) {
try {
LiveInformation().leaveRoom();
print("Left room");
} catch (e) {
print("Error leaving room: $e");
}
},
child: Scaffold(
body: Container(
child: Row(
children: [
const SizedBox(
width: 10,
),
Container(
padding: const EdgeInsets.symmetric(
vertical: 10,
),
child: IntrinsicWidth(
child: Column(
children: [
ShadButton.outline(
icon: const Icon(Icons.menu),
width: double.infinity,
border: Border.all(
width: 2,
color: Colors.grey.shade400,
),
padding: const EdgeInsets.all(0),
borderRadius: const BorderRadius.all(Radius.circular(10)),
onPressed: () {
bool multiMode = ModalRoute.of(context)!.settings.name!.contains("multi");
LiveInformation().p2pModule.startDiscovery();
showShadSheet(
context: context,
side: ShadSheetSide.left,
builder: (context) {
return ShadSheet(
padding: const EdgeInsets.all(0),
content: Container(
width: 225,
height: MediaQuery.of(context).size.height,
padding: const EdgeInsets.all(10),
child: Column(
children: [
Expanded(
child: Column(
children: [
if (LiveInformation().inRoom)
ShadButton(
text: Text("Room Information"),
onPressed: () {
Navigator.pop(context);
showShadSheet(
context: context,
side: ShadSheetSide.left,
builder: (context) {
return ShadSheet(
padding: const EdgeInsets.all(10),
content: Container(
width: 400,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Room Information",
style: ShadTheme.of(context).textTheme.h2
),
SizedBox(
height: 8,
),
if (LiveInformation().isHost)
Container(
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.circular(10)
),
width: double.infinity,
padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Hosting room",
style: ShadTheme.of(context).textTheme.h4.copyWith(
height: 0.9
)
),
],
)
)
else
Container(
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.circular(10)
),
width: double.infinity,
padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Connected to room via:",
style: ShadTheme.of(context).textTheme.h4.copyWith(
height: 0.9
)
),
SizedBox(
height: 4,
),
Text(
_rmconString(LiveInformation().connectionMethod) + " connection",
style: ShadTheme.of(context).textTheme.p.copyWith(
height: 0.9
)
),
],
)
),
SizedBox(
height: 8,
),
ShadButton(
text: Text("Show room QR Code"),
borderRadius: BorderRadius.all(Radius.circular(10)),
onPressed: () {
showShadDialog(
context: context,
builder: (context) {
return ShadDialog(
title: Text("Room QR Code"),
content: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: QrImageView(
data: LiveInformation().generateRoomInfo(),
size: 200,
backgroundColor: Colors.white,
),
),
SizedBox(
width: 10,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
ShadButton(
text: Text("Copy Room Info"),
borderRadius: BorderRadius.all(Radius.circular(10)),
onPressed: () {
Clipboard.setData(ClipboardData(text: LiveInformation().generateRoomInfo()));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Copied room info to clipboard"),
)
);
},
),
],
),
)
],
),
);
}
);
},
),
ShadButton(
text: Text("Make nearby discoverable"),
onPressed: () {
LiveInformation().p2pModule.startDiscovery();
ShadToaster.of(context).show(
ShadToast(
title: Text("Discoverable"),
description: Text("If it wasnt before, your device is now discoverable to nearby devices"),
duration: const Duration(seconds: 3),
)
);
},
)
],
),
),
);
}
);
},
)
],
)
),
if (!LiveInformation().inRoom)
ShadButton(
text: Text("Return to route selection"),
onPressed: () {
Navigator.pop(context);
Navigator.pop(context);
},
)
else
ShadButton(
text: Text("Route selection"),
onPressed: () {
Navigator.popAndPushNamed(context, '/multi/routes');
},
)
],
),
),
);
}
);
},
),
SizedBox(
height: 220,
child: RotatedBox(
quarterTurns: 3,
child: Column(
children: [
ShadButton(
text: const Text("Manual Announcements"),
width: double.infinity,
borderRadius: const BorderRadius.all(Radius.circular(10)),
onPressed: () {
List<Widget> announcements = [];
for (var announcement in LiveInformation().announcementModule.manualAnnouncements) {
announcements.add(
ShadButton(
text: SizedBox(
width: 200-42,
child: Text(announcement.shortName),
),
onPressed: () {
if (closeDialogWidget.closeDialog) {
Navigator.pop(context);
}
LiveInformation().announcementModule.queueAnnouncementByInfoIndex(
infoIndex: LiveInformation().announcementModule.manualAnnouncements.indexOf(announcement),
);
},
)
);
}
print(announcements.length);
showShadSheet(
context: context,
side: ShadSheetSide.left,
builder: (context) {
return ShadSheet(
padding: const EdgeInsets.all(0),
content: Container(
height: MediaQuery.of(context).size.height,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 5,
),
Container(
padding: const EdgeInsets.symmetric(
vertical: 10,
),
alignment: Alignment.bottomCenter,
height: double.infinity,
width: 35,
child: RotatedBox(
quarterTurns: 3,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
"Manual Ann'",
style: ShadTheme.of(context).textTheme.h3
),
SizedBox(
width: 16,
),
Container(
width: 1,
height: 200,
color: Colors.grey,
),
],
),
closeDialogWidget
],
),
),
),
Container(
// width: 220,
height: MediaQuery.of(context).size.height,
child: Scrollbar(
thumbVisibility: true,
child: SingleChildScrollView(
reverse: true,
child: Container(
margin: const EdgeInsets.fromLTRB(
0,
10,
10,
10
),
child: Column(
children: announcements.reversed.toList(),
),
),
),
),
),
],
),
),
);
}
);
},
),
ShadButton(
text: const Text("Bus Stop Announcements"),
enabled: LiveInformation().getRouteVariant() != null,
width: double.infinity,
borderRadius: const BorderRadius.all(Radius.circular(10)),
onPressed: () {
showShadSheet(
context: context,
side: ShadSheetSide.left,
builder: (context) {
List<Widget> announcements = [];
LiveInformation info = LiveInformation();
for (var busStop in info.getRouteVariant()!.busStops) {
if (info.trackerModule.nearestStop == busStop) {
announcements.add(
ShadButton(
text: SizedBox(
width: 200-42,
child: Text(
"-> ${busStop.formattedStopName}",
overflow: TextOverflow.ellipsis,
),
),
backgroundColor: Colors.amber,
onPressed: () {
if (closeDialogWidget.closeDialog) {
Navigator.pop(context);
}
LiveInformation().announcementModule.queueAnnounceByAudioName(
displayText: busStop.formattedStopName,
audioNames: [busStop.getAudioFileName()],
);
},
)
);
} else {
announcements.add(
ShadButton(
text: SizedBox(
width: 200-42,
child: Text(
busStop.formattedStopName,
overflow: TextOverflow.ellipsis,
),
),
onPressed: () {
if (closeDialogWidget.closeDialog) {
Navigator.pop(context);
}
LiveInformation().announcementModule.queueAnnounceByAudioName(
displayText: busStop.formattedStopName,
audioNames: [busStop.getAudioFileName()],
);
},
)
);
}
}
ScrollController controller = ScrollController();
// Scroll to the current bus stop
WidgetsBinding.instance!.addPostFrameCallback((_) {
double offset = (info.getRouteVariant()!.busStops.indexOf(info.trackerModule.nearestStop!) * 50);
// Offset the offset so that its in the middle of the screen
offset -= (MediaQuery.of(context).size.height / 2) - 25;
// controller.jumpTo(offset);
});
return ShadSheet(
padding: const EdgeInsets.all(0),
content: Container(
height: MediaQuery.of(context).size.height,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 5,
),
Container(
padding: const EdgeInsets.symmetric(
vertical: 10,
),
alignment: Alignment.bottomCenter,
height: double.infinity,
width: 35,
child: RotatedBox(
quarterTurns: 3,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
"Bus Stops",
style: ShadTheme.of(context).textTheme.h3
),
SizedBox(
width: 16,
),
Container(
width: 1,
height: 200,
color: Colors.grey,
),
],
),
closeDialogWidget
],
),
),
),
Container(
// width: 220,
height: MediaQuery.of(context).size.height,
child: Scrollbar(
thumbVisibility: true,
controller: controller,
child: SingleChildScrollView(
reverse: true,
controller: controller,
child: Container(
margin: const EdgeInsets.fromLTRB(
0,
10,
10,
10
),
child: Column(
children: announcements.reversed.toList(),
),
),
),
),
),
],
),
),
);
}
);
},
),
],
)
)
),
const ShadButton(
icon: Icon(Icons.stop),
width: double.infinity,
enabled: false,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
ShadButton(
// text: const Text("Announce Destination"),
icon: const Icon(Icons.bus_alert),
width: double.infinity,
borderRadius: const BorderRadius.all(Radius.circular(10)),
onPressed: () {
LiveInformation info = LiveInformation();
BusRouteVariant? routeVariant = info.getRouteVariant();
if (routeVariant != null) {
info.announcementModule.queueAnnouncementByRouteVariant(
routeVariant: routeVariant,
sendToServer: ModalRoute.of(context)!.settings.name!.contains("multi")
);
}
},
),
],
),
),
),
Expanded(
child: Container(
decoration: const BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(10),
width: double.infinity,
height: double.infinity,
child: Stack(
children: [
Container(
alignment: Alignment.center,
child: ibus_display(
hasBorder: false,
),
),
Container(
alignment: Alignment.bottomRight,
child: ShadButton.ghost(
icon: const Icon(Icons.fullscreen),
padding: const EdgeInsets.all(8),
onPressed: () {
Navigator.pushNamed(context, '/display');
},
),
),
Container(
alignment: Alignment.bottomLeft,
child: ShadButton.ghost(
icon: const Icon(Icons.arrow_back),
padding: const EdgeInsets.all(8),
onPressed: () {
Navigator.popUntil(context, (route) {
return route.settings.name == '/multi' || route.settings.name == '/routes';
});
},
),
)
],
),
),
)
],
),
),
),
);
}
}
class _closeDialogueChecker extends StatefulWidget {
bool closeDialog = false;
@override
State<_closeDialogueChecker> createState() => _closeDialogueCheckerState();
}
class _closeDialogueCheckerState extends State<_closeDialogueChecker> {
@override
Widget build(BuildContext context) {
// TODO: implement build
return ShadSwitch(
value: widget.closeDialog,
enabled: true,
label: const Text("Close Dialog?"),
onChanged: (value) {
widget.closeDialog = value;
setState(() {
});
},
);
}
}

View File

@@ -9,7 +9,6 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -138,6 +137,12 @@ class _page2State extends State<_page2> {
await Permission.location.isGranted
]);
// if (defaultTargetPlatform == TargetPlatform.android) {
// perms.add(
// await Permission.nearbyWifiDevices.isGranted
// );
// }
return !perms.contains(false);
}
@@ -154,15 +159,12 @@ class _page2State extends State<_page2> {
child: SizedBox(
width: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Permissions",
style: TextStyle(
@@ -175,103 +177,230 @@ class _page2State extends State<_page2> {
height: 16,
),
ShadCard(
width: double.infinity,
title: Text(
"Location",
),
description: Text(
"Your location is required for automatically updating your nearest bus stop."
),
content: Container(
child: Column(
Container(
height: 210,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
// mainAxisSize: MainAxisSize.min,
// crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShadCard(
width: 300,
height: double.infinity,
title: Text(
"Location",
),
description: Text(
"Your location is required for automatically updating your nearest bus stop."
),
content: Container(
child: Column(
children: [
SizedBox(
height: 4,
),
FutureBuilder(
future: Permission.location.isGranted,
builder: (context, val) {
bool isEnabled = true;
String text = "Request permission";
Color color = Colors.white;
if (val.hasData) {
isEnabled = !val.data!;
}
if (!isEnabled) {
text = "Permission granted!";
color = Colors.green.shade400;
}
return ShadButton(
text: Text(text),
onPressed: () async {
await Permission.location.request();
setState(() {
});
},
enabled: isEnabled,
backgroundColor: color,
);
},
),
],
),
),
),
if (!kIsWeb)
SizedBox(
height: 4,
width: 16,
),
FutureBuilder(
future: Permission.location.isGranted,
builder: (context, val) {
bool isEnabled = true;
String text = "Request permission";
Color color = Colors.white;
if (!kIsWeb)
ShadCard(
width: 300,
height: double.infinity,
title: Text(
"Storage",
),
description: Text(
"Storage access is required to access recorded announcements."
),
content: Container(
child: Column(
children: [
if (val.hasData) {
isEnabled = !val.data!;
}
if (!isEnabled) {
text = "Permission granted!";
color = Colors.green.shade400;
}
SizedBox(
height: 4,
),
return ShadButton(
text: Text(text),
onPressed: () async {
await Permission.location.request();
setState(() {
FutureBuilder(
future: Permission.manageExternalStorage.isGranted,
builder: (context, val) {
bool isEnabled = true;
String text = "Request permission";
Color color = Colors.white;
});
},
enabled: isEnabled,
backgroundColor: color,
);
},
if (val.hasData) {
isEnabled = !val.data!;
}
if (!isEnabled) {
text = "Permission granted!";
color = Colors.green.shade400;
}
return ShadButton(
text: Text(text),
onPressed: () async {
await Permission.manageExternalStorage.request();
setState(() {
});
},
enabled: isEnabled,
backgroundColor: color,
);
},
)
],
),
),
),
],
),
),
),
SizedBox(
height: 16,
),
ShadCard(
width: double.infinity,
title: Text(
"Storage",
),
description: Text(
"Storage access is required to access recorded announcements."
),
content: Container(
child: Column(
children: [
if (defaultTargetPlatform == TargetPlatform.android)
SizedBox(
height: 4,
width: 16,
),
FutureBuilder(
future: Permission.manageExternalStorage.isGranted,
builder: (context, val) {
bool isEnabled = true;
String text = "Request permission";
Color color = Colors.white;
if (defaultTargetPlatform == TargetPlatform.android && false)
ShadCard(
width: 300,
height: double.infinity,
title: Text(
"Nearby Devices",
),
description: Text(
"Nearby Devices access is required to find nearby devices, and to establish connections with them."
),
content: Container(
child: Column(
children: [
if (val.hasData) {
isEnabled = !val.data!;
}
if (!isEnabled) {
text = "Permission granted!";
color = Colors.green.shade400;
}
SizedBox(
height: 4,
),
return ShadButton(
text: Text(text),
onPressed: () async {
await Permission.manageExternalStorage.request();
setState(() {
FutureBuilder(
future: Permission.nearbyWifiDevices.isGranted,
builder: (context, val) {
bool isEnabled = true;
String text = "Request permission";
Color color = Colors.white;
});
},
enabled: isEnabled,
backgroundColor: color,
);
},
)
if (val.hasData) {
isEnabled = !val.data!;
}
if (!isEnabled) {
text = "Permission granted!";
color = Colors.green.shade400;
}
return ShadButton(
text: Text(text),
onPressed: () async {
await Permission.nearbyWifiDevices.request();
setState(() {
});
},
enabled: isEnabled,
backgroundColor: color,
);
},
)
],
),
),
),
/*SizedBox(
width: 16,
),
ShadCard(
width: 300,
height: 200,
title: Text(
"Network",
),
description: Text(
"Network access is required for commincation between devices for multi mode."
),
content: Container(
child: Column(
children: [
SizedBox(
height: 4,
),
FutureBuilder(
future: Permission.nearbyWifiDevices.isGranted,
builder: (context, val) {
bool isEnabled = true;
String text = "Request permission";
Color color = Colors.white;
if (val.hasData) {
isEnabled = !val.data!;
}
if (!isEnabled) {
text = "Permission granted!";
color = Colors.green.shade400;
}
return ShadButton(
text: Text(text),
onPressed: () async {
await Permission.manageExternalStorage.request();
setState(() {
});
},
enabled: isEnabled,
backgroundColor: color,
);
},
)
],
),
),
),*/
],
),
),
@@ -306,7 +435,6 @@ class _page2State extends State<_page2> {
);
},
)
],
),
)
@@ -323,6 +451,8 @@ class _page3 extends InitialStartupPage {
State<_page3> createState() => _page3State();
}
class _page3State extends State<_page3> {
bool _loadingAudio = false;
@@ -503,6 +633,8 @@ class _page3State extends State<_page3> {
text: Text(text),
onPressed: () async {
LiveInformation().initTrackerModule();
showShadDialog(
context: context,
builder: (context) => ShadDialog.alert(

320
lib/remaster/JoinGroup.dart Normal file
View File

@@ -0,0 +1,320 @@
import 'dart:convert';
import 'package:bus_infotainment/backend/live_information.dart';
import 'package:bus_infotainment/remaster/dashboard.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide NavigationBar;
import 'package:native_qr/native_qr.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
class JoinGroup extends StatefulWidget {
@override
State<JoinGroup> createState() => _JoinGroupState();
}
class _JoinGroupState extends State<JoinGroup> {
Future<void> _joinGroup(String data) async {
if (data.isEmpty) {
ShadToaster.of(context).show(
ShadToast(
title: Text("Error connecting to room"),
description: Text("Nothing was found."),
)
);
}
if (await LiveInformation().joinRoom(data)) {
Navigator.pushNamed(context, "/multi/enroute");
} else {
ShadToaster.of(context).show(
ShadToast(
title: Text("Error connecting to room"),
description: Text("The room could not be found."),
)
);
}
}
@override
Widget build(BuildContext context) {
if (defaultTargetPlatform == TargetPlatform.android) {
LiveInformation().p2pModule.startDiscovery();
}
return Scaffold(
body: Container(
child: Row(
children: [
Expanded(
child: Container(
alignment: Alignment.center,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.all(20),
width: 100,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: RotatedBox(
quarterTurns: 3,
child: Container(
height: double.infinity,
child: ElevatedButton(
onPressed: () async {
NativeQr nativeQr = NativeQr();
String? result = await nativeQr.get();
_joinGroup(result!);
},
child: Text(
"Join from QR code",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)
),
)
),
),
),
),
SizedBox(
height: 20,
),
Expanded(
child: RotatedBox(
quarterTurns: 3,
child: Container(
height: double.infinity,
child: ElevatedButton(
onPressed: () {
},
child: Text(
"Join from clipboard",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)
),
)
),
),
),
)
],
),
),
Container(
width: 2,
color: Colors.grey.shade300,
),
Expanded(
child: Stack(
children: [
Positioned.fill(
child: Container(
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"EXPERIMENTAL",
style: TextStyle(
color: Colors.grey.shade700.withOpacity(0.9),
fontSize: 100,
fontWeight: FontWeight.bold,
height: 1
),
),
Text(
"Working proof of concept - Rewrite iminent",
style: TextStyle(
color: Colors.grey.shade700.withOpacity(0.9),
fontSize: 20,
fontWeight: FontWeight.bold
),
),
Text(
"Certain parts may not work as expected. I am aware of all issues.",
style: TextStyle(
color: Colors.grey.shade700.withOpacity(0.9),
fontSize: 20,
fontWeight: FontWeight.bold
),
)
],
),
),
),
Container(
margin: EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
"Nearby devices",
style: ShadTheme.of(context).textTheme.h1
),
SizedBox(
width: 20,
),
ElevatedButton(
onPressed: () {
setState(() {});
},
child: Text("Refresh"),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)
),
)
)
],
),
if (defaultTargetPlatform == TargetPlatform.android)
FutureBuilder(
future: LiveInformation().p2pModule.getDiscoveredDevices(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
}
List<Widget> peers = [];
for (var peer in snapshot.data!) {
print("Info: ");
print(jsonEncode(peer.info.toJson()));
peers.add(
ElevatedButton(
onPressed: () async {
await LiveInformation().p2pModule.connectToDevice(peer);
LiveInformation().inRoom = true;
LiveInformation().connectionMethod = RoomConnectionMethod.P2P;
Navigator.pushNamed(context, "/multi/enroute");
},
child: Text(peer.info.displayName),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)
),
)
)
);
}
print(peers.length);
return Column(
children: [
...peers
],
);
},
)
else
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("This feature is not available on this platform."),
Text("Please use the QR code or clipboard method.")
],
)
),
)
],
),
),
],
),
)
],
),
),
),
Container(
width: 2,
color: Colors.grey.shade300,
),
RotatedBox(
quarterTurns: 3,
child: NavigationBar()
)
],
),
),
);
}
}

View File

@@ -1,27 +1,79 @@
import 'package:bus_infotainment/pages/tfl_dataset_test.dart';
import 'package:bus_infotainment/remaster/DashboardArc.dart';
import 'package:bus_infotainment/remaster/InitialStartup.dart';
import 'package:bus_infotainment/remaster/JoinGroup.dart';
import 'package:bus_infotainment/remaster/SearchArc.dart';
import 'package:bus_infotainment/remaster/dashboard.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:window_manager/window_manager.dart';
import 'package:keep_screen_on/keep_screen_on.dart';
import 'WebSocketTest.dart';
class RemasteredApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
// Force landscape mode
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
// Hide navigation bar and status bar
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive, overlays: [
SystemUiOverlay.bottom,
SystemUiOverlay.top,
]);
// Hide the gesture navigation bar
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky, overlays: [
SystemUiOverlay.bottom,
SystemUiOverlay.top,
]);
// Stop the screen from turning off
KeepScreenOn.turnOn();
return ShadApp(
darkTheme: ShadThemeData(
brightness: Brightness.dark,
colorScheme: ShadSlateColorScheme.dark(),
colorScheme: ShadSlateColorScheme.dark(
background: Colors.grey.shade900,
primary: Colors.grey.shade50,
primaryForeground: Colors.grey.shade900,
border: Colors.grey.shade400,
input: Colors.grey.shade400,
),
),
themeMode: ThemeMode.dark,
// remove debug banner
debugShowCheckedModeBanner: false,
routes: {
'/setup': (context) => InitialStartup(),
'/': (context) => HomePage_Re(),
'/routes': (context) => RoutePage(),
'/enroute': (context) => EnRoutePage(),
'/routes': (context) => SearchArc(),
'/multi/routes': (context) => RoutePage(),
'/enroute': (context) => ArcDashboard(),
'/legacy': (context) => TfL_Dataset_Test(),
'/multi': (context) => MultiModeSetup(),
'/multi/enroute': (context) => ArcDashboard(),
'/multi/login': (context) => MultiModeLogin(),
'/multi/register': (context) => MultiModeRegister(),
'/display': (context) => FullscreenDisplay(),
'/multi/join': (context) => JoinGroup(),
'/websocket': (context) => WebSocketWidget(),
},

348
lib/remaster/SearchArc.dart Normal file
View File

@@ -0,0 +1,348 @@
import 'package:bus_infotainment/backend/live_information.dart';
import 'package:bus_infotainment/remaster/dashboard.dart';
import 'package:bus_infotainment/tfl_datasets.dart';
import 'package:bus_infotainment/utils/OrdinanceSurveyUtils.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide NavigationBar;
import 'package:flutter/widgets.dart';
import 'package:geolocator/geolocator.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:vector_math/vector_math.dart' hide Colors;
import '../backend/modules/tube_info.dart';
class SearchArc extends StatefulWidget {
@override
State<SearchArc> createState() => _SearchArcState();
}
class _SearchArcState extends State<SearchArc> {
TextEditingController searchController = TextEditingController();
@override
Widget build(BuildContext context) {
List<Widget> nearbyRoutes = _getNearbyRoutes(context);
List<Widget> pastRoutes = _getPastRoutes(context);
List<Widget> routeCards = [];
if (searchController.text.isNotEmpty) {
// Detect if the search query is a route number
// Examples of route numbers: 1, A1, 1A, A1A ...
// If it isnt a route number, it is a stop name
// Examples of stop names: "Euston Station", "Baker Street", "Kings Cross"
bool containsNumber = RegExp(r'\d').hasMatch(searchController.text);
List<BusRoute> searchResults = [];
// Loop through all bus routes
for (BusRoute route in LiveInformation().busSequences.routes.values) {
if (containsNumber) {
if (route.routeNumber.contains(searchController.text) && !searchResults.contains(route)) {
routeCards.add(_getRouteCard(context, route));
searchResults.add(route);
}
} else {
for (BusRouteVariant variant in route.routeVariants.values) {
for (BusRouteStop stop in variant.busStops) {
if (stop.formattedStopName.toLowerCase().contains(
searchController.text.toLowerCase()) && !searchResults.contains(route)) {
routeCards.add(_getRouteCard(context, route));
searchResults.add(route);
break;
}
}
}
}
}
if (routeCards.isEmpty){
routeCards.add(
Text("No results found", style: ShadTheme.of(context).textTheme.h3,)
);
}
}
return Scaffold(
body: Row(
children: [
Expanded(
child: Container(
padding: EdgeInsets.fromLTRB(32, 16, 32, 0),
height: MediaQuery.of(context).size.height,
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShadInput(
placeholder: Text("Search for route or stop (Can be laggy)"),
controller: searchController,
onChanged: (value) {
setState(() {
// Update search results
});
},
),
SizedBox(height: 8),
if (routeCards.isNotEmpty)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Search results",
style: ShadTheme.of(context).textTheme.h3,
),
SizedBox(height: 10),
Expanded(
child: GridView.extent(
shrinkWrap: true,
maxCrossAxisExtent: 100,
scrollDirection: Axis.vertical,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
children: routeCards,
),
),
],
),
)
else
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Nearby routes",
style: ShadTheme.of(context).textTheme.h3,
),
SizedBox(height: 10),
Expanded(
child: GridView.extent(
shrinkWrap: true,
maxCrossAxisExtent: 100,
scrollDirection: Axis.vertical,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
children: _getNearbyRoutes(context, multiMode: true),
),
),
],
),
)
],
),
),
],
),
),
),
Container(
width: 2,
color: Colors.grey.shade300,
),
RotatedBox(
quarterTurns: 3,
child: NavigationBar(),
)
],
),
);
}
}
List<Widget> _getNearbyRoutes(context, {bool multiMode = false}) {
print("Getting nearby routes");
LiveInformation liveInformation = LiveInformation();
BusSequences busSequences = liveInformation.busSequences;
List<BusRoute> nearbyRoutes = [];
Position? currentLocation = liveInformation.trackerModule.position;
Vector2 currentVector = Vector2(0, 0);
if (currentLocation == null && !kDebugMode) {
return [];
} else if (currentLocation != null){
currentVector = OSGrid.toNorthingEasting(currentLocation!.latitude, currentLocation.longitude);
}
if (kDebugMode) {
currentVector = OSGrid.toNorthingEasting(51.583781262560926, -0.020359583104595073);
}
for (BusRoute route in busSequences.routes.values) {
for (BusRouteVariant variant in route.routeVariants.values) {
for (BusRouteStop stop in variant.busStops) {
Vector2 stopVector = Vector2(stop.easting.toDouble(), stop.northing.toDouble());
double distance = currentVector.distanceTo(stopVector);
if (distance < 1000) {
nearbyRoutes.add(route);
break;
}
}
if (nearbyRoutes.contains(route)) {
break;
}
}
if (nearbyRoutes.contains(route)) {
continue;
}
}
List<Widget> routeCards = [];
for (BusRoute route in nearbyRoutes) {
routeCards.add(_getRouteCard(context, route, multiMode: multiMode));
}
return routeCards;
}
Widget _getRouteCard(context, BusRoute route, {bool multiMode = false}) {
String rr = "";
if (route.routeNumber.toLowerCase().startsWith("ul")) {
rr = "Rail replacement";
TubeLine? line = LiveInformation().tubeStations.getClosestLine(route.routeVariants.values.first);
rr = line?.name ?? rr;
if (!["London Overground", "DLR", "Rail replacement", "Elizabeth Line"].contains(rr)) {
rr += " line";
}
if (rr == "Hammersmith and City line") {
rr = "Hammersmith & City";
}
}
return ElevatedButton(
onPressed: () {
showShadSheet(
side: ShadSheetSide.right,
context: context,
builder: (context) {
List<Widget> variantWidgets = [];
for (BusRouteVariant variant in route.routeVariants.values) {
variantWidgets.add(
ShadButton.outline(
text: Container(
width: 274,
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("${variant.busStops.first.formattedStopName} ->"),
const SizedBox(
height: 2,
),
Text(variant.busStops.last.formattedStopName)
],
),
),
width: double.infinity,
height: 50,
padding: const EdgeInsets.all(8),
onPressed: () async {
LiveInformation liveInformation = LiveInformation();
await liveInformation.setRouteVariant(variant);
if (!multiMode) {
Navigator.popAndPushNamed(context, "/enroute");
} else {
Navigator.popAndPushNamed(context, "/multi/enroute");
}
},
)
);
variantWidgets.add(const SizedBox(
height: 4,
));
}
return ShadSheet(
title: Text("Route ${route.routeNumber} - Variants"),
content: Container(
width: 300,
height: MediaQuery.of(context).size.height - 52,
child: Scrollbar(
thumbVisibility: true,
child: ListView(
shrinkWrap: true,
children: [
...variantWidgets
],
),
),
),
padding: const EdgeInsets.all(8),
);
}
);
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Route \n ${route.routeNumber}",
style: ShadTheme.of(context).textTheme.h4.copyWith(
height: 1.1
),
textAlign: TextAlign.center,
),
if (route.routeNumber.toLowerCase().startsWith("ul"))
Text(rr, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w400, height: 1), textAlign: TextAlign.center,)
],
),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.all(10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);;
}
List<Widget> _getPastRoutes(context) {
return [];
}

View File

@@ -0,0 +1,189 @@
import 'dart:async';
import 'dart:isolate';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/status.dart' as status;
import 'package:network_info_plus/network_info_plus.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import '../utils/web_socket_server.dart';
class WebSocketWidget extends StatefulWidget {
@override
_WebSocketWidgetState createState() => _WebSocketWidgetState();
}
class _WebSocketWidgetState extends State<WebSocketWidget> {
WebSocketChannel? _channel;
TextEditingController _controller = TextEditingController();
TextEditingController _urlController = TextEditingController(text: 'ws://localhost:8080');
List<String> _messages = [];
Isolate? _serverIsolate;
bool _isServerRunning = false;
String? _localIpAddress;
@override
void initState() {
super.initState();
_getLocalIpAddress().then((ip) {
setState(() {
_localIpAddress = ip;
_urlController.text = 'ws://$_localIpAddress:8080';
});
});
}
Future<String> _getLocalIpAddress() async {
if (kIsWeb) {
return '127.0.0.1'; // Web does not support getting the local IP address
}
if (Platform.isAndroid || Platform.isIOS) {
final info = NetworkInfo();
String? ip = await info.getWifiIP();
return ip ?? '127.0.0.1'; // Fallback to localhost if no IP is found
}
for (var interface in await NetworkInterface.list()) {
for (var addr in interface.addresses) {
if (addr.type == InternetAddressType.IPv4 && !addr.isLoopback) {
return addr.address;
}
}
}
return '127.0.0.1'; // Fallback to localhost if no IP is found
}
void _startServer() async {
final receivePort = ReceivePort();
_serverIsolate = await Isolate.spawn(webSocketServer, receivePort.sendPort);
receivePort.listen((message) {
print(message);
setState(() {
_isServerRunning = true;
});
});
}
void _stopServer() {
_serverIsolate?.kill(priority: Isolate.immediate);
setState(() {
_isServerRunning = false;
});
}
void _joinWebSocket() {
if (_channel != null) {
_channel!.sink.close();
}
setState(() {
_channel = WebSocketChannel.connect(Uri.parse(_urlController.text));
_channel!.stream.listen((message) {
setState(() {
_messages.add(message);
});
});
});
}
void _sendMessage(String message) {
if (_channel != null) {
_channel!.sink.add(message);
setState(() {
_messages.add('You: $message'); // Display the sent message
});
}
}
void _closeConnection() {
if (_channel != null) {
_channel!.sink.close(status.goingAway);
setState(() {
_channel = null;
_messages.clear();
});
}
}
@override
void dispose() {
_closeConnection();
_stopServer();
super.dispose();
}
@override
Widget build(BuildContext context) {
// force portrait mode
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
return Scaffold(
appBar: AppBar(
title: Text('WebSocket Demo'),
actions: [
IconButton(
icon: Icon(Icons.close),
onPressed: _closeConnection,
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
if (!_isServerRunning)
ElevatedButton(
onPressed: _startServer,
child: Text('Start WebSocket Server'),
)
else
ElevatedButton(
onPressed: _stopServer,
child: Text('Stop WebSocket Server'),
),
SizedBox(height: 10),
if (_localIpAddress != null)
Text('Server URL: ws://$_localIpAddress:8080'),
SizedBox(height: 10),
TextField(
controller: _urlController,
decoration: InputDecoration(labelText: 'WebSocket URL'),
onSubmitted: (value) => _joinWebSocket(),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: _joinWebSocket,
child: Text('Join WebSocket'),
),
SizedBox(height: 10),
Expanded(
child: ListView.builder(
itemCount: _messages.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(_messages[index]),
);
},
),
),
TextField(
controller: _controller,
decoration: InputDecoration(labelText: 'Send a message'),
onSubmitted: (text) {
_sendMessage(text);
_controller.clear();
},
),
],
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:bus_infotainment/audio_cache.dart';
@@ -16,28 +17,24 @@ class BusSequences extends InfoModule {
Map<String, BusDestination> destinations = {};
BusSequences.fromCSV(String destinationsCSV, String busSequencesCSV) {
BusSequences.fromCSV(String destinationsJson, String busSequencesCSV) {
// Init the bus destinations
List<List<String>> destinationRows = CsvConverter().convert(destinationsCSV);
destinationRows.removeAt(0);
Map<String, dynamic> destinationData = jsonDecode(destinationsJson);
print("Destination rows: ${destinationRows.length}");
print("Destination rows: ${destinationData.length}");
for (int i = 0; i < destinationRows.length; i++) {
for (String destinationName in destinationData.keys) {
try {
Map<String, dynamic> destinationDetails = destinationData[destinationName];
List<dynamic> entries = destinationRows[i];
// print("Parsing destination row $i: $entries");
String blind = destinationName;
String routeNumber = entries[0].toString();
BusRoute route = routes.containsKey(routeNumber) ? routes[routeNumber]! : BusRoute(routeNumber: routeNumber);
String blind = entries[1].toString();
double lat = double.parse(entries[2].toString());
double long = double.parse(entries[3].toString());
List<String> location = destinationDetails['Location'].split(', ');
double lat = double.parse(location[0]);
double long = double.parse(location[1]);
Vector2 grid = OSGrid.toNorthingEasting(lat, long);
@@ -46,11 +43,10 @@ class BusSequences extends InfoModule {
destination.easting = grid.x;
destination.northing = grid.y;
route.destinations.add(destination);
routes[routeNumber] = route;
destinations[blind] = destination;
} catch (e) {}
} catch (e) {
print("Error parsing destination: $e");
}
}
print("Loaded ${destinations.length} destinations");
@@ -295,8 +291,9 @@ class BusDestination {
Uint8List? audioBytesB = LiveInformation().announcementModule.announcementCache[audioNameB];
Uint8List? audioBytesC = LiveInformation().announcementModule.announcementCache[audioNameC];
if (audioBytesA != null) return audioBytesA;
if (audioBytesB != null) return audioBytesB;
if (audioBytesA != null) return audioBytesA;
if (audioBytesC != null) return audioBytesC;
print("No audio bytes found for $name");

View File

@@ -25,7 +25,7 @@ class AudioWrapper {
print("AudioWrapper mode: $mode");
mode = AudioWrapper_Mode.Web;
// mode = AudioWrapper_Mode.Web;
}
justaudio.AudioSource _convertSource_JustAudio(AudioWrapperSource source){

View File

@@ -0,0 +1,38 @@
import 'dart:isolate';
import 'dart:async';
import 'dart:io';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:web_socket_channel/io.dart'; // Import IOWebSocketChannel
void webSocketServer(SendPort sendPort) async {
final clients = <IOWebSocketChannel>[]; // Use IOWebSocketChannel
final handler = webSocketHandler((webSocket) {
clients.add(webSocket);
webSocket.stream.listen(
(message) {
print('Received: $message');
for (var client in clients) {
if (client != webSocket) {
client.sink.add(message); // Broadcast the message to other clients
}
}
},
onDone: () {
clients.remove(webSocket);
},
);
});
final server = await shelf_io.serve(
shelf.Pipeline().addMiddleware(shelf.logRequests()).addHandler(handler),
'0.0.0.0',
8080,
);
print('WebSocket server running at ws://${server.address.host}:${server.port}');
sendPort.send('Server running at ws://${server.address.host}:${server.port}');
}

View File

@@ -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<String> channels;
final String domain;
final Duration keepAlivePingDuration;
final Client client;
final Function(RealtimeMessage) onData;
final Function(dynamic) onError;
// ignore: unused_field
StreamSubscription<dynamic>? _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;
}
}
}

View File

@@ -11,6 +11,7 @@ import device_info_plus
import flutter_web_auth_2
import geolocator_apple
import just_audio
import network_info_plus
import package_info_plus
import path_provider_foundation
import rive_common
@@ -27,6 +28,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
RivePlugin.register(with: registry.registrar(forPlugin: "RivePlugin"))

View File

@@ -105,14 +105,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
boxy:
dependency: transitive
description:
name: boxy
sha256: eaa774ff591191b86f2eb8e8eae878aec50604404b74388a27e655cf3aadf758
url: "https://pub.dev"
source: hosted
version: "2.2.0"
characters:
dependency: transitive
description:
@@ -145,6 +137,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.18.0"
convert:
dependency: transitive
description:
name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
cookie_jar:
dependency: transitive
description:
@@ -185,6 +185,22 @@ 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"
dbus:
dependency: transitive
description:
name: dbus
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
device_info_plus:
dependency: transitive
description:
@@ -299,6 +315,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.19"
flutter_scroll_shadow:
dependency: "direct main"
description:
name: flutter_scroll_shadow
sha256: c0509c642c5077654301fab1fb2260adc94c82a407c60e64162974b4366e7874
url: "https://pub.dev"
source: hosted
version: "1.2.4"
flutter_shaders:
dependency: transitive
description:
@@ -421,6 +445,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.1"
http_methods:
dependency: transitive
description:
name: http_methods
sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
http_parser:
dependency: transitive
description:
@@ -433,10 +465,10 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.18.1"
version: "0.19.0"
just_audio:
dependency: "direct main"
description:
@@ -469,6 +501,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1"
keep_screen_on:
dependency: "direct main"
description:
name: keep_screen_on
sha256: "374405358a3229b0e1041b6e390ff4c74e73fbd21075298042044e0c84b5574c"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
keep_screen_on_platform_interface:
dependency: transitive
description:
name: keep_screen_on_platform_interface
sha256: "065a0811407a970027c7530f9b8f36d11c89f36aab85b4b5acdacfe2cf3a8568"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
latlong2:
dependency: transitive
description:
@@ -481,26 +529,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
url: "https://pub.dev"
source: hosted
version: "10.0.0"
version: "10.0.4"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "3.0.3"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "3.0.1"
lints:
dependency: transitive
description:
@@ -529,10 +577,10 @@ packages:
dependency: transitive
description:
name: lucide_icons_flutter
sha256: "43b11fa16f243529c538bcb4a1af014c03c177056c0d46039bc4f665320428c6"
sha256: "6126f30f3236acd7744f8535c2756322e72e96e00f5a94c83afde4abcc917bba"
url: "https://pub.dev"
source: hosted
version: "1.0.8"
version: "1.0.9"
matcher:
dependency: transitive
description:
@@ -553,10 +601,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev"
source: hosted
version: "1.11.0"
version: "1.12.0"
mgrs_dart:
dependency: transitive
description:
@@ -565,6 +613,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
mime:
dependency: transitive
description:
name: mime
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
url: "https://pub.dev"
source: hosted
version: "1.0.5"
native_qr:
dependency: "direct main"
description:
name: native_qr
sha256: "0928754b92305eb101a3359014a60ac5e90126534406b4dd6fb7550433978420"
url: "https://pub.dev"
source: hosted
version: "0.0.3"
nearby_service:
dependency: "direct main"
description:
name: nearby_service
sha256: "3575ddf7d093f455a7c1196eb938b6f64bd67f5ab861db6a6cc55c75e256ec0f"
url: "https://pub.dev"
source: hosted
version: "0.0.8"
network_info_plus:
dependency: "direct main"
description:
name: network_info_plus
sha256: "5bd4b86e28fed5ed4e6ac7764133c031dfb7d3f46aa2a81b46f55038aa78ecc0"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
network_info_plus_platform_interface:
dependency: transitive
description:
name: network_info_plus_platform_interface
sha256: "2e193d61d3072ac17824638793d3b89c6d581ce90c11604f4ca87311b42f2706"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
nm:
dependency: transitive
description:
name: nm
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
ntp:
dependency: "direct main"
description:
@@ -741,22 +837,38 @@ 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:
name: rive
sha256: "95690a0fb4f6e195c53b217ab3cc0e0b0f443c670adbdee9d57d636a36b82b18"
sha256: "255ab7892a77494458846cecee1376a017e64fd6b4130b51ec21424d12ffa6fe"
url: "https://pub.dev"
source: hosted
version: "0.13.2"
version: "0.13.4"
rive_common:
dependency: transitive
description:
name: rive_common
sha256: "3eee68fcab3e0882090cea5a8cf7acea7967f469a34a2580322575603b094435"
sha256: "3a0d95f529d52caef535d8ff32d75629ca37f7ab4707b13c83e9552a322557bc"
url: "https://pub.dev"
source: hosted
version: "0.4.5"
version: "0.4.8"
rxdart:
dependency: transitive
description:
@@ -777,10 +889,10 @@ packages:
dependency: "direct main"
description:
name: shadcn_ui
sha256: df9aedbd18bd60160c1c54717eef47fe4f90422f843070ccbc32786193803f7d
sha256: d0dce618cbceea8fa96cb58d0ae84dc4fef7f1d95f8838123736ef0a88868473
url: "https://pub.dev"
source: hosted
version: "0.4.1"
version: "0.4.6"
shared_preferences:
dependency: "direct main"
description:
@@ -837,6 +949,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shelf:
dependency: "direct main"
description:
name: shelf
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
url: "https://pub.dev"
source: hosted
version: "1.4.1"
shelf_router:
dependency: "direct main"
description:
name: shelf_router
sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864
url: "https://pub.dev"
source: hosted
version: "1.1.4"
shelf_static:
dependency: "direct main"
description:
name: shelf_static
sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e
url: "https://pub.dev"
source: hosted
version: "1.1.2"
shelf_web_socket:
dependency: "direct main"
description:
name: shelf_web_socket
sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
sky_engine:
dependency: transitive
description: flutter
@@ -902,10 +1046,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev"
source: hosted
version: "0.6.1"
version: "0.7.0"
text_scroll:
dependency: "direct main"
description:
@@ -1062,10 +1206,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev"
source: hosted
version: "13.0.0"
version: "14.2.1"
web:
dependency: transitive
description:

View File

@@ -40,7 +40,7 @@ dependencies:
intl: any
text_scroll: ^0.2.0
flutter_map: ^6.1.0
appwrite: ^12.0.1
appwrite: ^12.0.3
shared_preferences: ^2.2.2
url_launcher: ^6.2.2
ntp: ^2.0.0
@@ -52,7 +52,18 @@ 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
flutter_scroll_shadow: ^1.2.4
network_info_plus: ^5.0.3
shelf: ^1.4.1
shelf_router: ^1.1.4
shelf_static: ^1.1.2
shelf_web_socket: ^2.0.0
keep_screen_on: ^3.0.0
nearby_service: ^0.0.8
# web_socket_channel: ^3.0.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
@@ -93,6 +104,8 @@ flutter:
- assets/audio/R_RAIL_REPLACEMENT_SERVICE_001.mp3
- assets/datasets/tube_stations.json
- assets/audio/rail_replacement/
- assets/audio/R_SPECIAL_SERVICE_001.mp3
- assets/datasets/destinations.json
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg