tolaptop
This commit is contained in:
@@ -5,9 +5,9 @@ class ApiConstants {
|
|||||||
static const String APPWRITE_PROJECT_ID = "65de530c1c0a7ffc0c3f";
|
static const String APPWRITE_PROJECT_ID = "65de530c1c0a7ffc0c3f";
|
||||||
|
|
||||||
static const String INFO_Q_DATABASE_ID = "65de5cab16717444527b";
|
static const String INFO_Q_DATABASE_ID = "65de5cab16717444527b";
|
||||||
static const String DEST_Q_COLLECTION_ID = "65de9f2f925562a2eda8";
|
static const String MANUAL_Q_COLLECTION_ID = "65de9f2f925562a2eda8";
|
||||||
static const String MANUAL_Q_COLLECTION_ID = "65de9f1b6282fd209bdb";
|
static const String INFORMATION_Q_COLLECTION_ID = "65de9f1b6282fd209bdb";
|
||||||
static const String BUSSTOP_Q_COLLECTION_ID = "65de9ef464bfa5a0693d";
|
static const String DEST_Q_COLLECTION_ID = "65de9ef464bfa5a0693d";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class _ibus_displayState extends State<ibus_display> {
|
|||||||
|
|
||||||
String _padString(String input){
|
String _padString(String input){
|
||||||
|
|
||||||
if (input.length < 40){
|
if (input.length < 30){
|
||||||
print("Input is too short");
|
print("Input is too short");
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import 'package:bus_infotainment/pages/components/ibus_display.dart';
|
import 'package:bus_infotainment/pages/components/ibus_display.dart';
|
||||||
import 'package:bus_infotainment/singletons/live_information.dart';
|
import 'package:bus_infotainment/singletons/live_information.dart';
|
||||||
import 'package:bus_infotainment/tfl_datasets.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/utils/delegates.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
@@ -109,79 +110,102 @@ class pages_Home extends StatelessWidget {
|
|||||||
outlineColor: Colors.white70,
|
outlineColor: Colors.white70,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
defaultBuilder: (context) {
|
||||||
|
BusRouteVariant? routeVariant = LiveInformation().getRouteVariant();
|
||||||
|
if (routeVariant == null) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.grey.shade900,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
"No route selected",
|
||||||
|
style: GoogleFonts.teko(
|
||||||
|
fontSize: 25,
|
||||||
|
color: Colors.white70
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return StopAnnouncementPicker(
|
||||||
|
routeVariant: routeVariant,
|
||||||
|
backgroundColor: Colors.grey.shade900,
|
||||||
|
outlineColor: Colors.white70,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
),
|
),
|
||||||
|
|
||||||
Container(
|
// Container(
|
||||||
|
//
|
||||||
margin: EdgeInsets.all(20),
|
// margin: EdgeInsets.all(20),
|
||||||
|
//
|
||||||
height: 300-45,
|
// height: 300-45,
|
||||||
|
//
|
||||||
child: ListView(
|
// child: ListView(
|
||||||
|
//
|
||||||
scrollDirection: Axis.vertical,
|
// scrollDirection: Axis.vertical,
|
||||||
|
//
|
||||||
children: [
|
// children: [
|
||||||
|
//
|
||||||
ElevatedButton(
|
// ElevatedButton(
|
||||||
onPressed: () {
|
// onPressed: () async {
|
||||||
LiveInformation liveInformation = LiveInformation();
|
// LiveInformation liveInformation = LiveInformation();
|
||||||
liveInformation.announceRouteVariant(liveInformation.getRouteVariant()!);
|
// liveInformation.queueAnnouncement(await liveInformation.getDestinationAnnouncement(liveInformation.getRouteVariant()!, sendToServer: false));
|
||||||
},
|
// },
|
||||||
child: Text("Test announcement"),
|
// child: Text("Test announcement"),
|
||||||
),
|
// ),
|
||||||
|
//
|
||||||
ElevatedButton(
|
// ElevatedButton(
|
||||||
onPressed: () {
|
// onPressed: () {
|
||||||
LiveInformation liveInformation = LiveInformation();
|
// LiveInformation liveInformation = LiveInformation();
|
||||||
liveInformation.updateServer();
|
// liveInformation.updateServer();
|
||||||
},
|
// },
|
||||||
child: Text("Update server"),
|
// child: Text("Update server"),
|
||||||
),
|
// ),
|
||||||
|
//
|
||||||
SizedBox(
|
// SizedBox(
|
||||||
|
//
|
||||||
width: 100,
|
// width: 100,
|
||||||
|
//
|
||||||
child: TextField(
|
// child: TextField(
|
||||||
onChanged: (String value) {
|
// onChanged: (String value) {
|
||||||
LiveInformation liveInformation = LiveInformation();
|
// LiveInformation liveInformation = LiveInformation();
|
||||||
// liveInformation.documentID = value;
|
// // liveInformation.documentID = value;
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
|
//
|
||||||
SizedBox(
|
// SizedBox(
|
||||||
|
//
|
||||||
width: 200,
|
// width: 200,
|
||||||
|
//
|
||||||
child: TextField(
|
// child: TextField(
|
||||||
onSubmitted: (String value) {
|
// onSubmitted: (String value) {
|
||||||
LiveInformation liveInformation = LiveInformation();
|
// LiveInformation liveInformation = LiveInformation();
|
||||||
liveInformation.queueAnnouncement(AnnouncementQueueEntry(
|
// liveInformation.queueAnnouncement(AnnouncementQueueEntry(
|
||||||
displayText: value,
|
// displayText: value,
|
||||||
audioSources: []
|
// audioSources: []
|
||||||
));
|
// ));
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
|
//
|
||||||
ElevatedButton(
|
// ElevatedButton(
|
||||||
onPressed: () {
|
// onPressed: () {
|
||||||
LiveInformation liveInformation = LiveInformation();
|
// LiveInformation liveInformation = LiveInformation();
|
||||||
liveInformation.pullServer();
|
// liveInformation.pullServer();
|
||||||
},
|
// },
|
||||||
child: Text("Pull server"),
|
// child: Text("Pull server"),
|
||||||
),
|
// ),
|
||||||
|
//
|
||||||
],
|
// ],
|
||||||
|
//
|
||||||
),
|
// ),
|
||||||
|
//
|
||||||
),
|
// ),
|
||||||
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -194,7 +218,7 @@ class ManualAnnouncementPicker extends StatefulWidget {
|
|||||||
|
|
||||||
final Color backgroundColor;
|
final Color backgroundColor;
|
||||||
final Color outlineColor;
|
final Color outlineColor;
|
||||||
final List<InformationAnnouncementEntry> announcements;
|
final List<NamedAnnouncementQueueEntry> announcements;
|
||||||
|
|
||||||
const ManualAnnouncementPicker({super.key, required this.backgroundColor, required this.outlineColor, required this.announcements});
|
const ManualAnnouncementPicker({super.key, required this.backgroundColor, required this.outlineColor, required this.announcements});
|
||||||
|
|
||||||
@@ -219,14 +243,16 @@ class _ManualAnnouncementPickerState extends State<ManualAnnouncementPicker> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (InformationAnnouncementEntry announcement in widget.announcements!) {
|
int i = 0;
|
||||||
|
for (NamedAnnouncementQueueEntry announcement in widget.announcements!) {
|
||||||
announcementWidgets.add(
|
announcementWidgets.add(
|
||||||
_ManualAnnouncementEntry(
|
_ManualAnnouncementEntry(
|
||||||
announcement: announcement,
|
announcement: announcement,
|
||||||
index: liveInformation.manualAnnouncements.indexOf(announcement),
|
index: i,
|
||||||
outlineColor: Colors.white70
|
outlineColor: Colors.white70
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -506,10 +532,14 @@ class StopAnnouncementPicker extends ManualAnnouncementPicker {
|
|||||||
outlineColor: outlineColor,
|
outlineColor: outlineColor,
|
||||||
announcements: [
|
announcements: [
|
||||||
for (BusRouteStops stop in routeVariant.busStops)
|
for (BusRouteStops stop in routeVariant.busStops)
|
||||||
InformationAnnouncementEntry(
|
ManualAnnouncementEntry(
|
||||||
shortName: stop.formattedStopName,
|
shortName: stop.formattedStopName,
|
||||||
informationText: stop.formattedStopName,
|
informationText: stop.formattedStopName,
|
||||||
audioSources: []
|
audioSources: [
|
||||||
|
// AudioWrapperByteSource(
|
||||||
|
// LiveInformation().announcementCache[stop.getAudioFileName()]
|
||||||
|
// )
|
||||||
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -521,7 +551,7 @@ int wrap(int i, int j, int length) {
|
|||||||
|
|
||||||
class _ManualAnnouncementEntry extends StatelessWidget {
|
class _ManualAnnouncementEntry extends StatelessWidget {
|
||||||
|
|
||||||
final InformationAnnouncementEntry announcement;
|
final NamedAnnouncementQueueEntry announcement;
|
||||||
final int index;
|
final int index;
|
||||||
final Color outlineColor;
|
final Color outlineColor;
|
||||||
|
|
||||||
|
|||||||
@@ -334,11 +334,19 @@ class _Variant extends StatelessWidget {
|
|||||||
child: const Text("Cancel"),
|
child: const Text("Cancel"),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
LiveInformation liveInformation = LiveInformation();
|
LiveInformation liveInformation = LiveInformation();
|
||||||
liveInformation.setRouteVariant(variant);
|
liveInformation.setRouteVariant(variant);
|
||||||
|
|
||||||
|
liveInformation.queueAnnouncement(
|
||||||
|
await liveInformation.getDestinationAnnouncement(
|
||||||
|
variant,
|
||||||
|
sendToServer: true,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
tfL_Dataset_TestState.setState(() {});
|
tfL_Dataset_TestState.setState(() {});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
// Singleton
|
// Singleton
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:appwrite/appwrite.dart' as appwrite;
|
import 'package:appwrite/appwrite.dart' as appwrite;
|
||||||
import 'package:appwrite/models.dart' as models;
|
import 'package:appwrite/models.dart' as models;
|
||||||
@@ -10,8 +11,10 @@ import 'package:bus_infotainment/auth/auth_api.dart';
|
|||||||
import 'package:bus_infotainment/tfl_datasets.dart';
|
import 'package:bus_infotainment/tfl_datasets.dart';
|
||||||
import 'package:bus_infotainment/utils/audio%20wrapper.dart';
|
import 'package:bus_infotainment/utils/audio%20wrapper.dart';
|
||||||
import 'package:bus_infotainment/utils/delegates.dart';
|
import 'package:bus_infotainment/utils/delegates.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:ntp/ntp.dart';
|
||||||
|
|
||||||
class LiveInformation {
|
class LiveInformation {
|
||||||
|
|
||||||
@@ -61,15 +64,45 @@ class LiveInformation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Timer refreshTimer() => Timer.periodic(Duration(milliseconds: 50), (timer) {
|
Timer refreshTimer() => Timer.periodic(const Duration(milliseconds: 100), (timer) async {
|
||||||
|
await updateNtpOffset();
|
||||||
_handleAnnouncementQueue();
|
_handleAnnouncementQueue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
int ntpOffset = -1;
|
||||||
|
DateTime lastNtpUpdate = DateTime.now().add(const Duration(seconds: -15));
|
||||||
|
/// updates the NTP offset from DateTime.now()
|
||||||
|
Future<void> updateNtpOffset() async {
|
||||||
|
|
||||||
|
// Only update the NTP offset every 10 seconds
|
||||||
|
if (DateTime.now().difference(lastNtpUpdate).inSeconds < 10) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var res = await http.get(Uri.parse('http://worldtimeapi.org/api/timezone/Europe/London'));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var json = jsonDecode(res.body);
|
||||||
|
DateTime time = DateTime.parse(json['datetime']);
|
||||||
|
ntpOffset = time.millisecondsSinceEpoch - DateTime.now().millisecondsSinceEpoch;
|
||||||
|
lastNtpUpdate = DateTime.now();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime getNow() {
|
||||||
|
if (ntpOffset == -1) {
|
||||||
|
throw Exception("NTP offset not set");
|
||||||
|
}
|
||||||
|
return DateTime.now().add(Duration(milliseconds: ntpOffset));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
AudioWrapper audioPlayer = AudioWrapper();
|
AudioWrapper audioPlayer = AudioWrapper();
|
||||||
AnnouncementCache announcementCache = AnnouncementCache();
|
AnnouncementCache announcementCache = AnnouncementCache();
|
||||||
List<AnnouncementQueueEntry> announcementQueue = [];
|
List<AnnouncementQueueEntry> announcementQueue = [];
|
||||||
DateTime lastAnnouncement = DateTime.now();
|
AnnouncementQueueEntry? lastAnnouncement;
|
||||||
|
DateTime lastAnnouncementTimeStamp = DateTime.now().toUtc();
|
||||||
EventDelegate<AnnouncementQueueEntry> announcementDelegate = EventDelegate();
|
EventDelegate<AnnouncementQueueEntry> announcementDelegate = EventDelegate();
|
||||||
String _currentAnnouncement = "*** NO MESSAGE ***";
|
String _currentAnnouncement = "*** NO MESSAGE ***";
|
||||||
|
|
||||||
@@ -78,18 +111,23 @@ class LiveInformation {
|
|||||||
_currentAnnouncement = value;
|
_currentAnnouncement = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isPlayingAnnouncement = false;
|
||||||
void _handleAnnouncementQueue() async {
|
void _handleAnnouncementQueue() async {
|
||||||
|
|
||||||
int timerInterval = 50;
|
int timerInterval = 100;
|
||||||
|
|
||||||
// print("Handling announcement queue");
|
// print("Handling announcement queue");
|
||||||
if (audioPlayer.state != AudioWrapper_State.Playing) {
|
if (!isPlayingAnnouncement) {
|
||||||
if (announcementQueue.isNotEmpty) {
|
if (announcementQueue.isNotEmpty) {
|
||||||
|
|
||||||
|
print("Handling announcement queue");
|
||||||
|
|
||||||
AnnouncementQueueEntry announcement = announcementQueue.first;
|
AnnouncementQueueEntry announcement = announcementQueue.first;
|
||||||
|
|
||||||
|
print("Queue length: ${announcementQueue.length}");
|
||||||
|
|
||||||
{
|
{
|
||||||
DateTime now = DateTime.now();
|
DateTime now = getNow();
|
||||||
if (announcement.scheduledTime != null) {
|
if (announcement.scheduledTime != null) {
|
||||||
int milisecondDifference = abs(now.millisecondsSinceEpoch - announcement.scheduledTime!.millisecondsSinceEpoch);
|
int milisecondDifference = abs(now.millisecondsSinceEpoch - announcement.scheduledTime!.millisecondsSinceEpoch);
|
||||||
// print("Q Difference: ${milisecondDifference}");
|
// print("Q Difference: ${milisecondDifference}");
|
||||||
@@ -97,105 +135,178 @@ class LiveInformation {
|
|||||||
// Account for the time lost by the periodic timer
|
// Account for the time lost by the periodic timer
|
||||||
await Future.delayed(Duration(milliseconds: timerInterval - milisecondDifference));
|
await Future.delayed(Duration(milliseconds: timerInterval - milisecondDifference));
|
||||||
} else {
|
} else {
|
||||||
|
print("Due in: ${milisecondDifference}ms");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
announcementQueue.removeAt(0);
|
||||||
|
lastAnnouncement = announcement;
|
||||||
|
isPlayingAnnouncement = true;
|
||||||
|
|
||||||
|
if (kIsWeb) {
|
||||||
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// announcementDelegate.trigger(announcement);
|
print("Displaying announcement: ${announcement.displayText}");
|
||||||
|
announcementDelegate.trigger(announcement);
|
||||||
_currentAnnouncement = announcement.displayText;
|
_currentAnnouncement = announcement.displayText;
|
||||||
|
|
||||||
lastAnnouncement = DateTime.now();
|
lastAnnouncementTimeStamp = getNow();
|
||||||
|
|
||||||
for (AudioWrapperSource source in announcement.audioSources) {
|
if (announcement.audioSources.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
for (AudioWrapperSource source in announcement.audioSources) {
|
||||||
|
|
||||||
Duration? duration = await audioPlayer.play(source);
|
Duration? duration = await audioPlayer.play(source);
|
||||||
await Future.delayed(duration!);
|
await Future.delayed(duration!);
|
||||||
await Future.delayed(Duration(milliseconds: 150));
|
await Future.delayed(Duration(milliseconds: 150));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
audioPlayer.stop();
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (announcementQueue.isNotEmpty) {
|
||||||
|
await Future.delayed(Duration(seconds: 5));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
audioPlayer.stop();
|
|
||||||
announcementQueue.remove(announcement);
|
isPlayingAnnouncement = false;
|
||||||
print("Queue length: ${announcementQueue.length}");
|
|
||||||
print("Popped announcement queue");
|
print("Popped announcement queue");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void announceRouteVariant(BusRouteVariant routeVariant) async {
|
Future<AnnouncementQueueEntry> _getDestinationAnnouncement(BusRouteVariant routeVariant, {bool sendToServer = false}) async {
|
||||||
if (routeVariant == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String display = "${routeVariant.busRoute.routeNumber} to ${routeVariant.busStops.last.formattedStopName}";
|
String display = "${routeVariant.busRoute.routeNumber} to ${routeVariant.busStops.last.formattedStopName}";
|
||||||
|
|
||||||
String audio_route = "R_${routeVariant.busRoute.routeNumber}_001.mp3";
|
String audio_route = "R_${routeVariant.busRoute.routeNumber}_001.mp3";
|
||||||
String audio_destination = routeVariant.busStops.last.getAudioFileName();
|
String audio_destination = routeVariant.busStops.last.getAudioFileName();
|
||||||
|
|
||||||
print("Audio file: $audio_route");
|
// Cache the audio files
|
||||||
|
|
||||||
await announcementCache.loadAnnouncements([audio_route, audio_destination]);
|
await announcementCache.loadAnnouncements([audio_route, audio_destination]);
|
||||||
|
|
||||||
AudioWrapperSource source_route = AudioWrapperByteSource(announcementCache[audio_route]);
|
AudioWrapperSource source_route = AudioWrapperByteSource(announcementCache[audio_route]);
|
||||||
AudioWrapperSource source_destination = AudioWrapperByteSource(announcementCache[audio_destination]);
|
AudioWrapperSource source_destination = AudioWrapperByteSource(announcementCache[audio_destination]);
|
||||||
|
|
||||||
queueAnnouncement(AnnouncementQueueEntry(
|
return AnnouncementQueueEntry(
|
||||||
displayText: display,
|
sendToServer: sendToServer,
|
||||||
audioSources: [source_route, AudioWrapperAssetSource("audio/to_destination.wav"), source_destination]
|
|
||||||
));
|
|
||||||
|
|
||||||
|
displayText: display,
|
||||||
|
audioSources: [source_route, AudioWrapperAssetSource("audio/to_destination.wav"), source_destination]
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AnnouncementQueueEntry> getDestinationAnnouncement(BusRouteVariant routeVariant, {bool sendToServer = true}) async {
|
||||||
|
return DestinationAnnouncementEntry(
|
||||||
|
routeVariant: routeVariant,
|
||||||
|
audioSources: [],
|
||||||
|
sendToServer: sendToServer,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
late BusSequences busSequences;
|
late BusSequences busSequences;
|
||||||
BusRouteVariant? _currentRouteVariant;
|
BusRouteVariant? _currentRouteVariant;
|
||||||
EventDelegate<BusRouteVariant> routeVariantDelegate = EventDelegate();
|
EventDelegate<BusRouteVariant> routeVariantDelegate = EventDelegate();
|
||||||
|
|
||||||
void setRouteVariant(BusRouteVariant routeVariant) {
|
Future<void> setRouteVariant(BusRouteVariant routeVariant) async {
|
||||||
_currentRouteVariant = routeVariant;
|
_currentRouteVariant = routeVariant;
|
||||||
announceRouteVariant(routeVariant);
|
|
||||||
routeVariantDelegate.trigger(routeVariant);
|
routeVariantDelegate.trigger(routeVariant);
|
||||||
|
|
||||||
|
// cache all of the stop announcements
|
||||||
|
|
||||||
|
List<String> audioFiles = [];
|
||||||
|
|
||||||
|
for (BusRouteStops stop in routeVariant.busStops) {
|
||||||
|
audioFiles.add(stop.getAudioFileName());
|
||||||
|
print("Cached stop audio: ${stop.getAudioFileName()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
await announcementCache.loadAnnouncements(audioFiles);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BusRouteVariant? getRouteVariant() {
|
BusRouteVariant? getRouteVariant() {
|
||||||
return _currentRouteVariant;
|
return _currentRouteVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
void queueAnnouncement(AnnouncementQueueEntry announcement) {
|
void queueAnnouncement(AnnouncementQueueEntry announcement) async {
|
||||||
|
|
||||||
|
|
||||||
// Make sure the timestamp of the announcement is after the last announcement
|
// Make sure the timestamp of the announcement is after the last announcement
|
||||||
// If so, dont queue it
|
// If so, dont queue it
|
||||||
// If timestamp is null, then skip this check
|
// If timestamp is null, then skip this check
|
||||||
if (announcement.timestamp != null && announcement.timestamp!.isBefore(lastAnnouncement)) {
|
if (announcement.timestamp != null && announcement.timestamp!.toUtc().isBefore(lastAnnouncementTimeStamp)) {
|
||||||
print("Announcement is too old");
|
print("Announcement is too old");
|
||||||
|
|
||||||
print("LastAnnouncement: $lastAnnouncement");
|
print("LastAnnouncement: $lastAnnouncementTimeStamp");
|
||||||
print("Announcement: ${announcement.timestamp}");
|
print("Announcement: ${announcement.timestamp}");
|
||||||
|
|
||||||
int difference = announcement.timestamp!.difference(lastAnnouncement).inMilliseconds;
|
int difference = announcement.timestamp!.difference(lastAnnouncementTimeStamp).inMilliseconds;
|
||||||
print("Difference: $difference");
|
print("Difference: $difference");
|
||||||
return;
|
return;
|
||||||
} else if (announcement.timestamp == null) {
|
} else if (announcement.timestamp == null) {
|
||||||
print("Announcement `${announcement.displayText}` does not have timestamp");
|
print("Announcement `${announcement.displayText}` does not have timestamp");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// If there is an announcement in the queue with the same timestamp, dont queue it
|
||||||
|
if (announcementQueue.any((element) => element.timestamp == announcement.timestamp)) {
|
||||||
|
print("Announcement with same timestamp already in queue");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!announcement.sendToServer) {
|
if (!announcement.sendToServer) {
|
||||||
|
|
||||||
|
if (announcement is DestinationAnnouncementEntry) {
|
||||||
|
|
||||||
|
BusRouteVariant routeVariant = announcement.routeVariant;
|
||||||
|
|
||||||
|
if (getRouteVariant() != routeVariant) {
|
||||||
|
setRouteVariant(routeVariant);
|
||||||
|
}
|
||||||
|
|
||||||
|
announcementQueue.add(
|
||||||
|
await _getDestinationAnnouncement(
|
||||||
|
routeVariant,
|
||||||
|
sendToServer: false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
print("Queued destination announcement: ${announcement.displayText}");
|
||||||
|
print("Audios: ${announcement.audioSources.length}");
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
announcementQueue.add(announcement);
|
announcementQueue.add(announcement);
|
||||||
|
print("Queued announcement: ${announcement.displayText} (no server)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final databases = appwrite.Databases(auth.client);
|
final databases = appwrite.Databases(auth.client);
|
||||||
|
|
||||||
if (announcement is InformationAnnouncementEntry) {
|
print("Queuing announcement: ${announcement.displayText} (server)");
|
||||||
|
print("Announcement type: ${announcement.runtimeType}");
|
||||||
|
|
||||||
|
if (announcement.runtimeType == InformationAnnouncementEntry) {
|
||||||
|
announcement as InformationAnnouncementEntry;
|
||||||
|
print("Queing to InformationAnnouncementEntry");
|
||||||
|
|
||||||
// 5 sedonds in the future
|
// 5 sedonds in the future
|
||||||
DateTime scheduledTime = DateTime.now().add(Duration(seconds: 5));
|
DateTime scheduledTime = (await getNow()).add(Duration(seconds: 1));
|
||||||
|
|
||||||
|
|
||||||
final document = databases.createDocument(
|
final document = databases.createDocument(
|
||||||
documentId: appwrite.ID.unique(),
|
documentId: appwrite.ID.unique(),
|
||||||
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
|
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
|
||||||
collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID,
|
collectionId: ApiConstants.INFORMATION_Q_COLLECTION_ID,
|
||||||
data: {
|
data: {
|
||||||
"ManualAnnouncementIndex": manualAnnouncements.indexOf(announcement),
|
"ManualAnnouncementIndex": manualAnnouncements.indexOf(announcement),
|
||||||
"ScheduledTime": scheduledTime.toIso8601String(),
|
"ScheduledTime": scheduledTime.toIso8601String(),
|
||||||
@@ -203,9 +314,73 @@ class LiveInformation {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
print("Queued manual announcement: ${announcement.shortName} (server)");
|
||||||
|
|
||||||
|
} else if (announcement.runtimeType == ManualAnnouncementEntry) {
|
||||||
|
announcement as ManualAnnouncementEntry;
|
||||||
|
print("Queing to ManualAnnouncementEntry");
|
||||||
|
|
||||||
|
// 5 sedonds in the future
|
||||||
|
DateTime scheduledTime = (await getNow()).add(Duration(seconds: 1));
|
||||||
|
|
||||||
|
print("debug2");
|
||||||
|
|
||||||
|
List<String> audioFileNames = [];
|
||||||
|
|
||||||
|
for (AudioWrapperSource source in announcement.audioSources) {
|
||||||
|
|
||||||
|
if (source is AudioWrapperByteSource) {
|
||||||
|
Uint8List? bytes = await source.bytes;
|
||||||
|
|
||||||
|
String? filename = null;
|
||||||
|
|
||||||
|
for (String key in announcementCache.keys) {
|
||||||
|
if (announcementCache[key] == bytes) {
|
||||||
|
filename = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
final document = databases.createDocument(
|
||||||
|
documentId: appwrite.ID.unique(),
|
||||||
|
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
|
||||||
|
collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID,
|
||||||
|
data: {
|
||||||
|
"DisplayText": announcement.displayText,
|
||||||
|
"AudioFileNames": audioFileNames,
|
||||||
|
|
||||||
|
"ScheduledTime": scheduledTime.toIso8601String(),
|
||||||
|
"SessionID": sessionID,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
print("Queued manual announcement: ${announcement.shortName}");
|
print("Queued manual announcement: ${announcement.shortName}");
|
||||||
|
|
||||||
} else if (announcement is AnnouncementQueueEntry) {
|
} else if (announcement.runtimeType == DestinationAnnouncementEntry) {
|
||||||
|
announcement as DestinationAnnouncementEntry;
|
||||||
|
print("Queing to DestinationAnnouncementEntry");
|
||||||
|
|
||||||
|
// 5 sedonds in the future
|
||||||
|
DateTime scheduledTime = (getNow()).add(Duration(seconds: 2));
|
||||||
|
|
||||||
|
final document = databases.createDocument(
|
||||||
|
documentId: appwrite.ID.unique(),
|
||||||
|
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
|
||||||
|
collectionId: ApiConstants.DEST_Q_COLLECTION_ID,
|
||||||
|
data: {
|
||||||
|
"RouteNumber": announcement.routeVariant.busRoute.routeNumber,
|
||||||
|
"RouteVariantIndex": announcement.routeVariant.routeVariant,
|
||||||
|
|
||||||
|
"ScheduledTime": scheduledTime.toIso8601String(),
|
||||||
|
"SessionID": sessionID,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
print("Queued manual announcement: ${announcement.shortName} (server)");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,45 +565,129 @@ class LiveInformation {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final databases = appwrite.Databases(auth.client);
|
|
||||||
|
|
||||||
// Pull the manual queue
|
|
||||||
final manual_q = await databases.listDocuments(
|
|
||||||
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
|
|
||||||
collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID,
|
|
||||||
queries: [
|
|
||||||
appwrite.Query.search("SessionID", sessionID),
|
|
||||||
appwrite.Query.limit(25),
|
|
||||||
appwrite.Query.offset(0),
|
|
||||||
appwrite.Query.orderDesc('\$createdAt')
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
List<AnnouncementQueueEntry> queue = [];
|
List<AnnouncementQueueEntry> queue = [];
|
||||||
|
|
||||||
for (models.Document doc in manual_q.documents) {
|
final databases = appwrite.Databases(auth.client);
|
||||||
int index = doc.data['ManualAnnouncementIndex'];
|
|
||||||
|
|
||||||
InformationAnnouncementEntry announcement_clone = InformationAnnouncementEntry(
|
// Pull the information queue
|
||||||
shortName: manualAnnouncements[index].shortName,
|
{
|
||||||
informationText: manualAnnouncements[index].displayText,
|
final manual_q = await databases.listDocuments(
|
||||||
audioSources: manualAnnouncements[index].audioSources,
|
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
|
||||||
scheduledTime: doc.data["ScheduledTime"] != null ? DateTime.parse(doc.data["ScheduledTime"]) : null,
|
collectionId: ApiConstants.INFORMATION_Q_COLLECTION_ID,
|
||||||
timestamp: DateTime.parse(doc.$createdAt),
|
queries: [
|
||||||
sendToServer: false,
|
appwrite.Query.search("SessionID", sessionID),
|
||||||
|
appwrite.Query.limit(25),
|
||||||
|
appwrite.Query.offset(0),
|
||||||
|
appwrite.Query.orderDesc('\$createdAt')
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// sort the queue by timestamp, so the oldest announcements are at the front
|
for (models.Document doc in manual_q.documents) {
|
||||||
|
int index = doc.data['ManualAnnouncementIndex'];
|
||||||
|
|
||||||
|
InformationAnnouncementEntry announcement_clone =
|
||||||
|
InformationAnnouncementEntry(
|
||||||
|
shortName: manualAnnouncements[index].shortName,
|
||||||
|
informationText: manualAnnouncements[index].displayText,
|
||||||
|
audioSources: manualAnnouncements[index].audioSources,
|
||||||
|
scheduledTime: doc.data["ScheduledTime"] != null
|
||||||
|
? DateTime.parse(doc.data["ScheduledTime"])
|
||||||
|
: null,
|
||||||
|
timestamp: DateTime.parse(doc.$createdAt),
|
||||||
|
sendToServer: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// sort the queue by timestamp, so the oldest announcements are at the front
|
||||||
|
|
||||||
queue.add(announcement_clone);
|
queue.add(announcement_clone);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pull the manual queue
|
||||||
|
{
|
||||||
|
final manual_q = await databases.listDocuments(
|
||||||
|
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
|
||||||
|
collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID,
|
||||||
|
queries: [
|
||||||
|
appwrite.Query.search("SessionID", sessionID),
|
||||||
|
appwrite.Query.limit(25),
|
||||||
|
appwrite.Query.offset(0),
|
||||||
|
appwrite.Query.orderDesc('\$createdAt')
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
for (models.Document doc in manual_q.documents) {
|
||||||
|
|
||||||
|
List<AudioWrapperSource> audioSources = [];
|
||||||
|
|
||||||
|
for (String filename in doc.data["AudioFileNames"]) {
|
||||||
|
audioSources.add(AudioWrapperByteSource(announcementCache[filename]));
|
||||||
|
}
|
||||||
|
|
||||||
|
ManualAnnouncementEntry announcement_clone =
|
||||||
|
ManualAnnouncementEntry(
|
||||||
|
sendToServer: false,
|
||||||
|
|
||||||
|
shortName: "",
|
||||||
|
informationText: doc.data["DisplayText"],
|
||||||
|
|
||||||
|
audioSources: [...audioSources],
|
||||||
|
scheduledTime: doc.data["ScheduledTime"] != null
|
||||||
|
? DateTime.parse(doc.data["ScheduledTime"])
|
||||||
|
: null,
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
// sort the queue by timestamp, so the oldest announcements are at the front
|
||||||
|
|
||||||
|
queue.add(announcement_clone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pull the destination queue
|
||||||
|
{
|
||||||
|
final dest_q = await databases.listDocuments(
|
||||||
|
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
|
||||||
|
collectionId: ApiConstants.DEST_Q_COLLECTION_ID,
|
||||||
|
queries: [
|
||||||
|
appwrite.Query.search("SessionID", sessionID),
|
||||||
|
appwrite.Query.limit(25),
|
||||||
|
appwrite.Query.offset(0),
|
||||||
|
appwrite.Query.orderDesc('\$createdAt')
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
for (models.Document doc in dest_q.documents) {
|
||||||
|
|
||||||
|
BusRoute? route = busSequences.routes[doc.data["RouteNumber"]];
|
||||||
|
|
||||||
|
BusRouteVariant? routeVariant = route!.routeVariants[doc.data["RouteVariantIndex"]];
|
||||||
|
|
||||||
|
|
||||||
|
DestinationAnnouncementEntry announcement_clone =
|
||||||
|
DestinationAnnouncementEntry(
|
||||||
|
routeVariant: routeVariant!,
|
||||||
|
scheduledTime: doc.data["ScheduledTime"] != null
|
||||||
|
? DateTime.parse(doc.data["ScheduledTime"])
|
||||||
|
: null,
|
||||||
|
timestamp: DateTime.parse(doc.$createdAt),
|
||||||
|
sendToServer: false,
|
||||||
|
audioSources: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
// sort the queue by timestamp, so the oldest announcements are at the front
|
||||||
|
|
||||||
|
queue.add(announcement_clone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (AnnouncementQueueEntry entry in queue) {
|
for (AnnouncementQueueEntry entry in queue) {
|
||||||
|
|
||||||
|
// Dont queue announcements that are older than now
|
||||||
|
if (entry.scheduledTime != null && entry.scheduledTime!.isBefore(await getNow())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
queueAnnouncement(entry);
|
queueAnnouncement(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,10 +695,13 @@ class LiveInformation {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
appwrite.RealtimeSubscription? information_q_subscription;
|
||||||
appwrite.RealtimeSubscription? manual_q_subscription;
|
appwrite.RealtimeSubscription? manual_q_subscription;
|
||||||
|
appwrite.RealtimeSubscription? destination_q_subscription;
|
||||||
|
|
||||||
Future<void> setupRealtime() async {
|
Future<void> setupRealtime() async {
|
||||||
|
|
||||||
if (manual_q_subscription != null) {
|
if (information_q_subscription != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,6 +712,15 @@ class LiveInformation {
|
|||||||
// Websocket
|
// Websocket
|
||||||
appwrite.Realtime realtime = appwrite.Realtime(auth.client);
|
appwrite.Realtime realtime = appwrite.Realtime(auth.client);
|
||||||
|
|
||||||
|
information_q_subscription = realtime.subscribe(
|
||||||
|
['databases.${ApiConstants.INFO_Q_DATABASE_ID}.collections.${ApiConstants.INFORMATION_Q_COLLECTION_ID}.documents'],
|
||||||
|
);
|
||||||
|
information_q_subscription?.stream.listen((event) {
|
||||||
|
print("Manual queue entry added");
|
||||||
|
|
||||||
|
pullQueue();
|
||||||
|
});
|
||||||
|
|
||||||
manual_q_subscription = realtime.subscribe(
|
manual_q_subscription = realtime.subscribe(
|
||||||
['databases.${ApiConstants.INFO_Q_DATABASE_ID}.collections.${ApiConstants.MANUAL_Q_COLLECTION_ID}.documents'],
|
['databases.${ApiConstants.INFO_Q_DATABASE_ID}.collections.${ApiConstants.MANUAL_Q_COLLECTION_ID}.documents'],
|
||||||
);
|
);
|
||||||
@@ -459,12 +730,25 @@ class LiveInformation {
|
|||||||
pullQueue();
|
pullQueue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
destination_q_subscription = realtime.subscribe(
|
||||||
|
['databases.${ApiConstants.INFO_Q_DATABASE_ID}.collections.${ApiConstants.DEST_Q_COLLECTION_ID}.documents'],
|
||||||
|
);
|
||||||
|
destination_q_subscription?.stream.listen((event) {
|
||||||
|
print("Destination queue entry added");
|
||||||
|
|
||||||
|
pullQueue();
|
||||||
|
});
|
||||||
|
|
||||||
print("Subscribed to servers");
|
print("Subscribed to servers");
|
||||||
|
|
||||||
await Future.delayed(Duration(seconds: 90));
|
await Future.delayed(Duration(seconds: 90));
|
||||||
|
|
||||||
|
information_q_subscription?.close();
|
||||||
|
information_q_subscription = null;
|
||||||
manual_q_subscription?.close();
|
manual_q_subscription?.close();
|
||||||
manual_q_subscription = null;
|
manual_q_subscription = null;
|
||||||
|
destination_q_subscription?.close();
|
||||||
|
destination_q_subscription = null;
|
||||||
setupRealtime();
|
setupRealtime();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -483,17 +767,37 @@ class AnnouncementQueueEntry {
|
|||||||
AnnouncementQueueEntry({required this.displayText, required this.audioSources, this.sendToServer = true, this.scheduledTime, this.timestamp});
|
AnnouncementQueueEntry({required this.displayText, required this.audioSources, this.sendToServer = true, this.scheduledTime, this.timestamp});
|
||||||
}
|
}
|
||||||
|
|
||||||
class ManualAnnouncementEntry extends AnnouncementQueueEntry {
|
class NamedAnnouncementQueueEntry extends AnnouncementQueueEntry {
|
||||||
final String shortName;
|
final String shortName;
|
||||||
|
|
||||||
ManualAnnouncementEntry({
|
NamedAnnouncementQueueEntry({
|
||||||
required this.shortName,
|
required this.shortName,
|
||||||
|
required String displayText,
|
||||||
|
required List<AudioWrapperSource> audioSources,
|
||||||
|
DateTime? scheduledTime,
|
||||||
|
DateTime? timestamp,
|
||||||
|
bool sendToServer = true,
|
||||||
|
}) : super(
|
||||||
|
displayText: displayText,
|
||||||
|
audioSources: audioSources,
|
||||||
|
sendToServer: sendToServer,
|
||||||
|
scheduledTime: scheduledTime,
|
||||||
|
timestamp: timestamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ManualAnnouncementEntry extends NamedAnnouncementQueueEntry {
|
||||||
|
|
||||||
|
|
||||||
|
ManualAnnouncementEntry({
|
||||||
|
required String shortName,
|
||||||
required String informationText,
|
required String informationText,
|
||||||
required List<AudioWrapperSource> audioSources,
|
required List<AudioWrapperSource> audioSources,
|
||||||
DateTime? scheduledTime,
|
DateTime? scheduledTime,
|
||||||
DateTime? timestamp,
|
DateTime? timestamp,
|
||||||
bool sendToServer = true,
|
bool sendToServer = true,
|
||||||
}) : super(
|
}) : super(
|
||||||
|
shortName: shortName,
|
||||||
displayText: informationText,
|
displayText: informationText,
|
||||||
audioSources: audioSources,
|
audioSources: audioSources,
|
||||||
sendToServer: sendToServer,
|
sendToServer: sendToServer,
|
||||||
@@ -502,17 +806,18 @@ class ManualAnnouncementEntry extends AnnouncementQueueEntry {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class InformationAnnouncementEntry extends AnnouncementQueueEntry {
|
class InformationAnnouncementEntry extends NamedAnnouncementQueueEntry {
|
||||||
final String shortName;
|
|
||||||
|
|
||||||
InformationAnnouncementEntry({
|
InformationAnnouncementEntry({
|
||||||
required this.shortName,
|
required String shortName,
|
||||||
required String informationText,
|
required String informationText,
|
||||||
required List<AudioWrapperSource> audioSources,
|
required List<AudioWrapperSource> audioSources,
|
||||||
DateTime? scheduledTime,
|
DateTime? scheduledTime,
|
||||||
DateTime? timestamp,
|
DateTime? timestamp,
|
||||||
bool sendToServer = true,
|
bool sendToServer = true,
|
||||||
}) : super(
|
}) : super(
|
||||||
|
shortName: shortName,
|
||||||
displayText: informationText,
|
displayText: informationText,
|
||||||
audioSources: audioSources,
|
audioSources: audioSources,
|
||||||
sendToServer: sendToServer,
|
sendToServer: sendToServer,
|
||||||
@@ -521,4 +826,25 @@ class InformationAnnouncementEntry extends AnnouncementQueueEntry {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DestinationAnnouncementEntry extends NamedAnnouncementQueueEntry {
|
||||||
|
|
||||||
|
final BusRouteVariant routeVariant;
|
||||||
|
|
||||||
|
DestinationAnnouncementEntry({
|
||||||
|
required this.routeVariant,
|
||||||
|
required List<AudioWrapperSource> audioSources,
|
||||||
|
DateTime? scheduledTime,
|
||||||
|
DateTime? timestamp,
|
||||||
|
bool sendToServer = true,
|
||||||
|
}) : super(
|
||||||
|
shortName: "Destination",
|
||||||
|
displayText: "${routeVariant.busRoute.routeNumber} to ${routeVariant.busStops.last.formattedStopName}",
|
||||||
|
audioSources: audioSources,
|
||||||
|
sendToServer: sendToServer,
|
||||||
|
scheduledTime: scheduledTime,
|
||||||
|
timestamp: timestamp,
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
var abs = (int value) => value < 0 ? -value : value;
|
var abs = (int value) => value < 0 ? -value : value;
|
||||||
@@ -72,6 +72,7 @@ class _DelegateBuilderState<T> extends State<DelegateBuilder<T>> {
|
|||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -416,6 +416,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
ntp:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: ntp
|
||||||
|
sha256: "198db73e5059b334b50dbe8c626011c26576778ee9fc53f4c55c1d89d08ed2d2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ dependencies:
|
|||||||
appwrite: ^11.0.1
|
appwrite: ^11.0.1
|
||||||
shared_preferences: ^2.2.2
|
shared_preferences: ^2.2.2
|
||||||
url_launcher: ^6.2.2
|
url_launcher: ^6.2.2
|
||||||
|
ntp: ^2.0.0
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
|||||||
Reference in New Issue
Block a user