Compare commits
10 Commits
c2ebac5bb5
...
bd8973ed23
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd8973ed23 | ||
|
|
1cbd985c15 | ||
|
|
ffde62a730 | ||
|
|
2846a061cc | ||
|
|
639faddfc8 | ||
|
|
e5a8d78bf1 | ||
|
|
3556639acc | ||
|
|
fff8de13c0 | ||
|
|
1f48f8f4b0 | ||
|
|
673891923d |
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
BIN
assets/audio/R_SPECIAL_SERVICE_001.mp3
Normal file
BIN
assets/audio/R_SPECIAL_SERVICE_001.mp3
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
|
5274
assets/datasets/destinations.json
Normal file
5274
assets/datasets/destinations.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
123
lib/backend/modules/directconnection.dart
Normal file
123
lib/backend/modules/directconnection.dart
Normal 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
|
||||
];
|
||||
221
lib/backend/modules/networking.dart
Normal file
221
lib/backend/modules/networking.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
)
|
||||
)*/
|
||||
|
||||
],
|
||||
|
||||
|
||||
734
lib/remaster/DashboardArc.dart
Normal file
734
lib/remaster/DashboardArc.dart
Normal 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(() {
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
320
lib/remaster/JoinGroup.dart
Normal 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()
|
||||
)
|
||||
|
||||
],
|
||||
|
||||
),
|
||||
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
348
lib/remaster/SearchArc.dart
Normal 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 [];
|
||||
}
|
||||
189
lib/remaster/WebSocketTest.dart
Normal file
189
lib/remaster/WebSocketTest.dart
Normal 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
@@ -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");
|
||||
|
||||
@@ -25,7 +25,7 @@ class AudioWrapper {
|
||||
|
||||
print("AudioWrapper mode: $mode");
|
||||
|
||||
mode = AudioWrapper_Mode.Web;
|
||||
// mode = AudioWrapper_Mode.Web;
|
||||
}
|
||||
|
||||
justaudio.AudioSource _convertSource_JustAudio(AudioWrapperSource source){
|
||||
|
||||
38
lib/utils/web_socket_server.dart
Normal file
38
lib/utils/web_socket_server.dart
Normal 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}');
|
||||
}
|
||||
112
lib/workaround/keepalive_realtime.dart
Normal file
112
lib/workaround/keepalive_realtime.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
|
||||
204
pubspec.lock
204
pubspec.lock
@@ -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:
|
||||
|
||||
17
pubspec.yaml
17
pubspec.yaml
@@ -40,7 +40,7 @@ dependencies:
|
||||
intl: any
|
||||
text_scroll: ^0.2.0
|
||||
flutter_map: ^6.1.0
|
||||
appwrite: ^12.0.1
|
||||
appwrite: ^12.0.3
|
||||
shared_preferences: ^2.2.2
|
||||
url_launcher: ^6.2.2
|
||||
ntp: ^2.0.0
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user