paradigm shift

This commit is contained in:
ImBenji
2024-05-03 14:03:51 +01:00
parent 673891923d
commit 1f48f8f4b0
17 changed files with 2440 additions and 547 deletions

View File

@@ -1,8 +1,8 @@
class ApiConstants {
static const String APPWRITE_ENDPOINT = "https://cloud.imbenji.net/v1";
static const String APPWRITE_PROJECT_ID = "65de530c1c0a7ffc0c3f";
static const String APPWRITE_ENDPOINT = "https://cloud.appwrite.io/v1";
static const String APPWRITE_PROJECT_ID = "6633d0e00023502890ed";
static const String INFO_Q_DATABASE_ID = "65de5cab16717444527b";
static const String MANUAL_Q_COLLECTION_ID = "65de9f2f925562a2eda8";

View File

@@ -21,6 +21,7 @@ class AuthAPI extends ChangeNotifier {
late final appwrite.Account account;
late models.User _currentUser;
late models.Session _currentSession;
AuthStatus _status = AuthStatus.UNINITIALIZED;
@@ -32,11 +33,16 @@ class AuthAPI extends ChangeNotifier {
AuthStatus get status => _status;
String? get username => _currentUser.name;
String? get email => _currentUser.email;
String? get userID => _currentUser.$id;
String? get userID {
try {
return _currentUser.$id;
} catch (e) {
return _currentSession.$id;
}
}
AuthAPI() {
AuthAPI({bool autoLoad = true}) {
init();
loadUser();
}
init() {
@@ -45,6 +51,29 @@ class AuthAPI extends ChangeNotifier {
.setProject(ApiConstants.APPWRITE_PROJECT_ID)
.setSelfSigned();
account = appwrite.Account(client);
try {
account.get().then((value) {
_currentUser = value;
_status = AuthStatus.AUTHENTICATED;
print("Auto loaded user: ${_currentUser.name}");
print("Auth status: $_status");
});
} catch (e) {
}
}
loadAnonymousUser() async {
try {
final user = await account.createAnonymousSession();
_currentSession = user;
_status = AuthStatus.AUTHENTICATED;
} catch (e) {
_status = AuthStatus.UNAUTHENTICATED;
} finally {
notifyListeners();
}
}
loadUser() async {

View File

@@ -16,6 +16,7 @@ import 'package:bus_infotainment/backend/modules/tube_info.dart';
import 'package:bus_infotainment/tfl_datasets.dart';
import 'package:bus_infotainment/utils/audio%20wrapper.dart';
import 'package:bus_infotainment/utils/delegates.dart';
import 'package:bus_infotainment/workaround/keepalive_realtime.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
@@ -62,12 +63,6 @@ class LiveInformation {
print("Loading tube stations from assets");
tubeStations = TubeStations.fromJson(json.decode(await rootBundle.loadString("assets/datasets/tube_stations.json")));
print("Loaded tube stations from assets");
String sessionID = "test";
commandModule = CommandModule(sessionID);
}
// Initialise modules
@@ -77,6 +72,9 @@ class LiveInformation {
initTrackerModule();
print("Initialised LiveInformation");
if (!auth.isAuthenticated()) {
auth.loadAnonymousUser();
}
}
Future<void> initTrackerModule() async {
@@ -86,10 +84,18 @@ class LiveInformation {
}
// Auth
AuthAPI auth = AuthAPI();
AuthAPI auth = AuthAPI(
autoLoad: false,
);
String? roomCode;
String? roomDocumentID;
bool isHost = false;
appwrite.RealtimeSubscription? _subscription;
RealtimeKeepAliveConnection? _keepAliveConnection; // This is a workaround for a bug in the appwrite SDK
// Modules
late CommandModule commandModule;
// late CommandModule commandModule; This needs to be deprecated
late BusSequences busSequences;
late AnnouncementModule announcementModule;
late SyncedTimeModule syncedTimeModule;
@@ -100,7 +106,7 @@ class LiveInformation {
BusRouteVariant? _currentRouteVariant;
// Events
EventDelegate<BusRouteVariant> routeVariantDelegate = EventDelegate();
EventDelegate<BusRouteVariant?> routeVariantDelegate = EventDelegate();
// Internal methods
@@ -108,7 +114,54 @@ class LiveInformation {
Future<void> setRouteVariant_Internal(BusRouteVariant routeVariant) async {
Future<void> setRouteVariant(BusRouteVariant? routeVariant) async {
if (routeVariant == null) {
_currentRouteVariant = null;
await announcementModule.queueAnnounceByAudioName(
displayText: "*** NO MESSAGE ***",
);
routeVariantDelegate.trigger(null);
return;
}
if (roomCode != null) {
try {
final client = auth.client;
final databases = appwrite.Databases(client);
final response = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode!)
]
);
final document = response.documents.first;
// Check if the route is not the same
if (document.data["CurrentRoute"] != routeVariant.busRoute.routeNumber || document.data["CurrentRouteVariant"] != routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant)) {
final updatedDocument = await databases.updateDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: document.$id,
data: {
"CurrentRoute": routeVariant.busRoute.routeNumber,
"CurrentRouteVariant": routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant),
"LastUpdater": auth.userID,
}
);
print("Updated route on server");
}
} catch (e) {
print("Failed to update route on server");
}
}
Continuation:
// Set the current route variant
_currentRouteVariant = routeVariant;
@@ -135,6 +188,10 @@ class LiveInformation {
]
);
// Display the route variant
announcementModule.queueAnnouncementByRouteVariant(routeVariant: routeVariant);
}
// Public methods
@@ -143,16 +200,329 @@ class LiveInformation {
return _currentRouteVariant;
}
Future<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) async {
BusRoute route = busSequences.routes[routeNumber]!;
BusRouteVariant routeVariant = route.routeVariants.values.toList()[routeVariantIndex];
await setRouteVariant(
routeVariant
);
}
// Multi device support
Future<void> createRoom(String roomCode) async {
print("Creating room with code $roomCode");
// Update the room code
this.roomCode = roomCode;
// Enable host mode
isHost = true;
// Access the database
final client = auth.client;
final databases = appwrite.Databases(client);
// Remove any existing documents
final existingDocuments = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode)
]
);
for (var document in existingDocuments.documents) {
await databases.deleteDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: document.$id
);
}
// Create the document
final document = await databases.createDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: appwrite.ID.unique(),
data: {
"SessionID": roomCode,
"LastUpdater": auth.userID,
}
);
// Listen for changes
// { Breaks due to bug in appwrite
// final realtime = appwrite.Realtime(client);
//
// if (_subscription != null) {
// _subscription!.close();
// }
//
// _subscription = realtime.subscribe(
// ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}']
// );
// _subscription!.stream.listen(ServerListener);
// }
// Listen for changes
if (_keepAliveConnection != null) {
try {
_keepAliveConnection!.close();
} catch (e) {
print("Failed to close connection... oh well");
}
}
String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1";
String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim();
_keepAliveConnection = RealtimeKeepAliveConnection(
channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'],
onData: ServerListener,
domain: domain,
client: auth.client,
onError: (e) {
print("Workarround Error: $e");
},
);
_keepAliveConnection!.initialize();
// Update the room document ID
roomDocumentID = document.$id;
print("Created room with code $roomCode");
}
Future<void> JoinRoom(String roomCode) async {
print("Joining room with code $roomCode");
// Disable host mode
isHost = false;
// Update the room code
this.roomCode = roomCode;
// Access the database
final client = auth.client;
final databases = appwrite.Databases(client);
// Get the document
final response = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode)
]
);
if (response.documents.isEmpty) {
throw Exception("Room not found");
}
final document = response.documents.first;
// Listen for changes
// {
// final realtime = appwrite.Realtime(client);
//
// if (_subscription != null) {
// _subscription!.close();
// }
//
// _subscription = realtime.subscribe([
// 'databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'
// ]);
//
// _subscription!.stream.listen(ServerListener);
// }
// Listen for changes
if (_keepAliveConnection != null) {
try {
_keepAliveConnection!.close();
} catch (e) {
print("Failed to close connection... oh well");
}
}
String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1";
String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim();
_keepAliveConnection = RealtimeKeepAliveConnection(
channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'],
onData: ServerListener,
domain: domain,
client: auth.client,
onError: (e) {
print("Workarround Error: $e");
},
);
_keepAliveConnection!.initialize();
// Update the room document ID
roomDocumentID = document.$id;
// Get the current route
try {
String routeNumber = document.data["CurrentRoute"];
int routeVariantIndex = document.data["CurrentRouteVariant"];
await setRouteVariantQuery(routeNumber, routeVariantIndex);
print("Set route to $routeNumber $routeVariantIndex");
} catch (e) {
print("Failed to set route");
}
print("Joined room with code $roomCode");
}
String? lastCommand;
Future<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);
// 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>();
String? command = commands.last;
if (command == lastCommand) {
return;
}
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.queueAnnounementByInfoIndex(
sendToServer: false,
infoIndex: int.parse(commandParts[2]),
scheduledTime: DateTime.fromMillisecondsSinceEpoch(int.parse(commandParts[3])),
);
} else if (mode == "dest") {
print("Announce dest command received");
String routeNumber = commandParts[2];
int routeVariantIndex = int.parse(commandParts[3]);
announcementModule.queueAnnouncementByRouteVariant(
sendToServer: false,
routeVariant: busSequences.routes[routeNumber]!.routeVariants.values.toList()[routeVariantIndex],
scheduledTime: DateTime.fromMillisecondsSinceEpoch(int.parse(commandParts[4])),
);
} else if (mode == "manual") {
print("Announce manual command received");
final displayText = commandParts[2];
List<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
);
}
}
}
String _extractId(String input) {
RegExp regExp = RegExp(r'\("user:(.*)"\)');
Match match = regExp.firstMatch(input)!;
return match.group(1)!;
}
Future<void> SendCommand(String command) async {
final client = auth.client;
final databases = appwrite.Databases(client);
final response = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode!)
]
);
List<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 +532,6 @@ class LiveInformation {
}
class AnnouncementQueueEntry {

View File

@@ -74,7 +74,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 +84,7 @@ class AnnouncementModule extends InfoModule {
bool proceeding = await _internalAccountForInconsistentTime(
announcement: nextAnnouncement,
timerInterval: const Duration(milliseconds: 200),
timerInterval: const Duration(milliseconds: 10),
callback: () {
queue.removeAt(0);
print("Announcement proceeding");
@@ -105,35 +105,21 @@ class AnnouncementModule extends InfoModule {
if (currentAnnouncement!.audioSources.isNotEmpty) {
// audioPlayer.loadSource(AudioWrapperAssetSource("assets/audio/5-seconds-of-silence.mp3"));
// audioPlayer.play();
// await Future.delayed(const Duration(milliseconds: 300));
// audioPlayer.stop();
// Prime all of the audio sources to be ready to play
for (AudioWrapperSource source in currentAnnouncement!.audioSources) {
try {
await audioPlayer.loadSource(source);
await Future.delayed((await audioPlayer.play())!);
audioPlayer.stop();
// try {
for (AudioWrapperSource source in currentAnnouncement!.audioSources) {
try {
await audioPlayer.loadSource(source);
Duration? duration = await audioPlayer.play();
await Future.delayed(duration!);
audioPlayer.stop();
// await Future.delayed(const Duration(milliseconds: 100));
if (currentAnnouncement?.audioSources.last != source) {
await Future.delayed(const Duration(milliseconds: 100));
}
} catch (e) {
// Do nothing
// print("Error playing announcement: $e on ${currentAnnouncement?.displayText}");
await Future.delayed(const Duration(seconds: 1));
if (currentAnnouncement?.audioSources.last != source) {
await Future.delayed(const Duration(milliseconds: 100));
}
} catch (e) {
await Future.delayed(const Duration(seconds: 1));
}
// audioPlayer.stop();
}
// } catch (e) {
// // Do nothing
// print("Error playing announcement: $e on ${currentAnnouncement?.displayTex}");
// }
} else {
if (queue.isNotEmpty) {
await Future.delayed(const Duration(seconds: 5));
@@ -179,7 +165,7 @@ class AnnouncementModule extends InfoModule {
}
// Configuration
int get defaultAnnouncementDelay => liveInformation.auth.isAuthenticated() ? 2 : 0;
int get defaultAnnouncementDelay => liveInformation.auth.isAuthenticated() ? 1 : 0;
// Methods
Future<void> queueAnnounceByAudioName({
@@ -199,8 +185,12 @@ class AnnouncementModule extends InfoModule {
audioNamesString += "\"$audioName\" ";
}
liveInformation.commandModule.executeCommand(
"announce manual \"$displayText\" ${audioNamesString} ${scheduledTime?.millisecondsSinceEpoch ?? ""}"
liveInformation.SendCommand("announce manual \"$displayText\" $audioNamesString ${scheduledTime.millisecondsSinceEpoch}");
queueAnnounceByAudioName(
displayText: displayText,
audioNames: audioNames,
scheduledTime: scheduledTime,
sendToServer: false
);
return;
@@ -244,9 +234,13 @@ class AnnouncementModule extends InfoModule {
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay));
liveInformation.commandModule.executeCommand(
"announce info $infoIndex ${scheduledTime?.millisecondsSinceEpoch ?? ""}"
liveInformation.SendCommand("announce info $infoIndex ${scheduledTime.millisecondsSinceEpoch}");
queueAnnounementByInfoIndex(
infoIndex: infoIndex,
scheduledTime: scheduledTime,
sendToServer: false
);
print("Announcement sent to server");
return;
}
@@ -270,9 +264,16 @@ class AnnouncementModule extends InfoModule {
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay));
liveInformation.commandModule.executeCommand(
"announce dest \"${routeVariant.busRoute.routeNumber}\" ${routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant)} ${scheduledTime?.millisecondsSinceEpoch ?? ""}"
String routeNumber = routeVariant.busRoute.routeNumber;
int routeVariantIndex = routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant);
liveInformation.SendCommand("announce dest ${routeNumber} ${routeVariantIndex} ${scheduledTime.millisecondsSinceEpoch}");
queueAnnouncementByRouteVariant(
routeVariant: routeVariant,
scheduledTime: scheduledTime,
sendToServer: false
);
return;
}
print("Checkpoint 4");

View File

@@ -47,17 +47,37 @@ class CommandModule extends InfoModule {
final databases = appwrite.Databases(client);
if (liveInformation.auth.status == AuthStatus.AUTHENTICATED) {
final document = await databases.createDocument(
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
collectionId: ApiConstants.COMMANDS_COLLECTION_ID,
documentId: appwrite.ID.unique(),
if (true) {
try {
final response = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", liveInformation.roomCode!)
]
);
List<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 +98,10 @@ class CommandModule extends InfoModule {
if (command == "Response:") {
}
else if (command == "initroom") {
// initroom <roomCode>
}
else if (command == "announce") {
@@ -166,10 +190,41 @@ class CommandModule extends InfoModule {
BusRoute route = liveInformation.busSequences.routes[routeNumber]!;
BusRouteVariant routeVariant = route.routeVariants.values.toList()[routeVariantIndex];
liveInformation.setRouteVariant_Internal(
liveInformation.setRouteVariant(
routeVariant
);
executeCommand("Response: v \"Client $clientID set its route to ($routeNumber to ${routeVariant.busStops.last.formattedStopName})\"");
// Update the server
if (liveInformation.isHost) {
print("Updating server");
final client = liveInformation.auth.client;
final databases = appwrite.Databases(client);
final response = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", liveInformation.roomCode!)
]
);
final document = await databases.updateDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: response.documents.first.$id,
data: {
"CurrentRoute": routeNumber,
"CurrentRouteVariant": routeVariantIndex,
}
);
try {
print("Updated server");
} catch (e) {
print("Failed to update server");
}
}
}
@@ -181,26 +236,26 @@ class CommandModule extends InfoModule {
return;
}
final realtime = appwrite.Realtime(LiveInformation().auth.client);
_subscription = realtime.subscribe(
['databases.${ApiConstants.INFO_Q_DATABASE_ID}.collections.${ApiConstants.COMMANDS_COLLECTION_ID}.documents']
);
_subscription!.stream.listen((event) {
print(jsonEncode(event.payload));
// Only do something if the document was created or updated
if (!(event.events.first.contains("create") || event.events.first.contains("update"))) {
return;
}
final commandInfo = CommandInfo(event.payload['command'], event.payload['client_id']);
if (commandInfo.clientID != clientID) {
_onCommandReceived(commandInfo);
}
});
// final realtime = appwrite.Realtime(LiveInformation().auth.client);
//
// _subscription = realtime.subscribe(
// ['databases.${ApiConstants.INFO_Q_DATABASE_ID}.collections.${ApiConstants.COMMANDS_COLLECTION_ID}.documents']
// );
// _subscription!.stream.listen((event) {
// print(jsonEncode(event.payload));
//
// // Only do something if the document was created or updated
// if (!(event.events.first.contains("create") || event.events.first.contains("update"))) {
// return;
// }
//
// final commandInfo = CommandInfo(event.payload['command'], event.payload['client_id']);
//
// if (commandInfo.clientID != clientID) {
// _onCommandReceived(commandInfo);
// }
//
// });
print("Listening for commands");

View File

@@ -183,7 +183,6 @@ class TrackerModule extends InfoModule {
print("Closest stop: ${closestStop.formattedStopName} in ${closestDistance.round()} meters");
}
}
double _calculateRelativeDistance(BusRouteStop stop, double latitude, double longitude) {

View File

@@ -68,7 +68,7 @@ class TubeStations {
double distance = Vector2(stop.easting.toDouble(), stop.northing.toDouble()).distanceTo(OSGrid.toNorthingEasting(station.latitude, station.longitude));
// if the distance is less than 100m, then we can assume that the bus stop is near the tube station
if (distance < 200) {
if (distance < 400) {
for (TubeLine line in station.lines) {
lineMatches[line] = lineMatches[line]! + 1;
}

View File

@@ -11,6 +11,7 @@ import 'package:bus_infotainment/utils/delegates.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
class pages_Home extends StatelessWidget {
const pages_Home({super.key});
@@ -60,7 +61,7 @@ class pages_Home extends StatelessWidget {
outlineColor: Colors.white70,
announcements: [
for (NamedAnnouncementQueueEntry announcement in LiveInformation().announcementModule.manualAnnouncements)
_AnnouncementEntry(
AnnouncementEntry(
label: announcement.shortName,
index: LiveInformation().announcementModule.manualAnnouncements.indexOf(announcement),
outlineColor: Colors.white70,
@@ -92,12 +93,12 @@ class pages_Home extends StatelessWidget {
color: Colors.grey.shade900,
),
child: DelegateBuilder<BusRouteVariant>(
child: DelegateBuilder<BusRouteVariant?>(
delegate: LiveInformation().routeVariantDelegate,
builder: (context, routeVariant) {
print("rebuilt stop announcement picker");
return StopAnnouncementPicker(
routeVariant: routeVariant,
routeVariant: routeVariant!,
backgroundColor: Colors.grey.shade900,
outlineColor: Colors.white70,
);
@@ -133,11 +134,6 @@ class pages_Home extends StatelessWidget {
ElevatedButton(
onPressed: () async {
LiveInformation liveInformation = LiveInformation();
final commandModule = liveInformation.commandModule;
// commandModule.executeCommand(
// "announce dest"
// );
liveInformation.announcementModule.queueAnnouncementByRouteVariant(
routeVariant: liveInformation.getRouteVariant()!
@@ -147,75 +143,7 @@ class pages_Home extends StatelessWidget {
child: Text("Announce current destination"),
),
// Container(
//
// margin: EdgeInsets.all(20),
//
// height: 300-45,
//
// child: ListView(
//
// scrollDirection: Axis.vertical,
//
// children: [
//
// ElevatedButton(
// onPressed: () async {
// LiveInformation liveInformation = LiveInformation();
// liveInformation.queueAnnouncement(await liveInformation.getDestinationAnnouncement(liveInformation.getRouteVariant()!, sendToServer: false));
// },
// child: Text("Test announcement"),
// ),
//
// ElevatedButton(
// onPressed: () {
// LiveInformation liveInformation = LiveInformation();
// liveInformation.updateServer();
// },
// child: Text("Update server"),
// ),
//
// SizedBox(
//
// width: 100,
//
// child: TextField(
// onChanged: (String value) {
// LiveInformation liveInformation = LiveInformation();
// // liveInformation.documentID = value;
// },
// ),
// ),
//
// SizedBox(
//
// width: 200,
//
// child: TextField(
// onSubmitted: (String value) {
// LiveInformation liveInformation = LiveInformation();
// liveInformation.queueAnnouncement(AnnouncementQueueEntry(
// displayText: value,
// audioSources: []
// ));
// },
// ),
// ),
//
// ElevatedButton(
// onPressed: () {
// LiveInformation liveInformation = LiveInformation();
// liveInformation.pullServer();
// },
// child: Text("Pull server"),
// ),
//
// ],
//
// ),
//
// ),
],
),
@@ -366,8 +294,9 @@ class AnnouncementPicker extends StatefulWidget {
final Color backgroundColor;
final Color outlineColor;
final List<Widget> announcements;
final String label;
const AnnouncementPicker({super.key, required this.backgroundColor, required this.outlineColor, required this.announcements});
const AnnouncementPicker({super.key, required this.backgroundColor, required this.outlineColor, required this.announcements, this.label = ""});
@override
State<AnnouncementPicker> createState() => _AnnouncementPickerState();
@@ -411,9 +340,9 @@ class _AnnouncementPickerState extends State<AnnouncementPicker> {
color: widget.backgroundColor,
border: Border.all(
color: widget.outlineColor,
width: 2
width: 1
),
borderRadius: BorderRadius.circular(8)
),
@@ -428,118 +357,26 @@ class _AnnouncementPickerState extends State<AnnouncementPicker> {
child: Column(
children: [
Container(
height: 2,
color: widget.outlineColor,
),
if (_currentIndex < announcementWidgets.length)
announcementWidgets[_currentIndex + 0]
else
Container(
height: 50,
decoration: BoxDecoration(
color: widget.backgroundColor,
border: Border.symmetric(
vertical: BorderSide(
color: widget.outlineColor,
width: 2
)
),
),
),
Container(
height: 2,
color: widget.outlineColor,
),
if (_currentIndex + 1 < announcementWidgets.length)
announcementWidgets[_currentIndex + 1]
else
Container(
height: 50,
decoration: BoxDecoration(
color: widget.backgroundColor,
border: Border.symmetric(
vertical: BorderSide(
color: widget.outlineColor,
width: 2
)
),
),
),
Container(
height: 2,
color: widget.outlineColor,
),
if (_currentIndex + 2 < announcementWidgets.length)
announcementWidgets[_currentIndex + 2]
else
Container(
height: 50,
decoration: BoxDecoration(
color: widget.backgroundColor,
border: Border.symmetric(
vertical: BorderSide(
color: widget.outlineColor,
width: 2
)
),
),
),
Container(
height: 2,
color: widget.outlineColor,
),
if (_currentIndex + 3 < announcementWidgets.length)
announcementWidgets[_currentIndex + 3]
else
Container(
height: 50,
decoration: BoxDecoration(
color: widget.backgroundColor,
border: Border.symmetric(
vertical: BorderSide(
color: widget.outlineColor,
width: 2
)
),
),
),
Container(
height: 2,
color: widget.outlineColor,
),
Container(
height: 40,
decoration: BoxDecoration(
color: widget.backgroundColor,
border: Border.symmetric(
vertical: BorderSide(
color: widget.outlineColor,
width: 2
)
border: Border.all(
color: widget.outlineColor,
width: 1
),
borderRadius: BorderRadius.circular(4)
),
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
// height: 100,
child: Column(
children: [
Container(
width: 40,
height: 40,
if (_currentIndex < announcementWidgets.length)
announcementWidgets[_currentIndex + 0]
else
Container(
height: 50,
decoration: BoxDecoration(
color: widget.backgroundColor,
border: Border.symmetric(
@@ -549,50 +386,18 @@ class _AnnouncementPickerState extends State<AnnouncementPicker> {
)
),
),
margin: const EdgeInsets.symmetric(
horizontal: 4
),
child: Container(
child: Stack(
children: [
Container(
width: 40,
height: 40,
child: Icon(
Icons.arrow_upward,
color: widget.outlineColor,
),
),
Positioned.fill(
child: ElevatedButton(
onPressed: () {
_currentIndex = wrap(_currentIndex - 4, 0, announcementWidgets.length, increment: 4);
setState(() {});
print(_currentIndex);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
foregroundColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
),
child: const Text(""),
),
)
],
),
)
),
),
Container(
width: 40,
height: 40,
height: 1,
color: widget.outlineColor,
),
if (_currentIndex + 1 < announcementWidgets.length)
announcementWidgets[_currentIndex + 1]
else
Container(
height: 50,
decoration: BoxDecoration(
color: widget.backgroundColor,
border: Border.symmetric(
@@ -602,55 +407,200 @@ class _AnnouncementPickerState extends State<AnnouncementPicker> {
)
),
),
),
margin: const EdgeInsets.symmetric(
horizontal: 4
),
child: Container(
child: Stack(
children: [
Container(
width: 40,
height: 40,
child: Icon(
Icons.arrow_downward,
color: widget.outlineColor,
),
),
Positioned.fill(
child: ElevatedButton(
onPressed: () {
_currentIndex = wrap(_currentIndex + 4, 0, announcementWidgets.length, increment: 4);
setState(() {});
print(_currentIndex);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
foregroundColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
),
child: const Text(""),
),
)
],
),
)
Container(
height: 1,
color: widget.outlineColor,
),
]
if (_currentIndex + 2 < announcementWidgets.length)
announcementWidgets[_currentIndex + 2]
else
Container(
height: 50,
decoration: BoxDecoration(
color: widget.backgroundColor,
border: Border.symmetric(
vertical: BorderSide(
color: widget.outlineColor,
width: 2
)
),
),
),
Container(
height: 1,
color: widget.outlineColor,
),
if (_currentIndex + 3 < announcementWidgets.length)
announcementWidgets[_currentIndex + 3]
else
Container(
height: 50,
decoration: BoxDecoration(
color: widget.backgroundColor,
border: Border.symmetric(
vertical: BorderSide(
color: widget.outlineColor,
width: 2
)
),
),
),
Container(
height: 1,
color: widget.outlineColor,
),
Container(
height: 40,
decoration: BoxDecoration(
color: widget.backgroundColor,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
),
child: Text(
widget.label,
style: ShadTheme.of(context).textTheme.h4.copyWith(
shadows: [
Shadow(
color: Colors.blueAccent.shade700,
blurRadius: 8
)
],
color: Colors.blueAccent.shade700
)
),
),
Expanded(child: Container()),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: widget.backgroundColor,
border: Border.symmetric(
vertical: BorderSide(
color: widget.outlineColor,
width: 1
)
),
),
margin: const EdgeInsets.symmetric(
horizontal: 4
),
child: Container(
child: Stack(
children: [
Container(
width: 40,
height: 40,
child: Icon(
Icons.arrow_upward,
color: widget.outlineColor,
),
),
Positioned.fill(
child: ElevatedButton(
onPressed: () {
_currentIndex = wrap(_currentIndex - 4, 0, announcementWidgets.length, increment: 4);
setState(() {});
print(_currentIndex);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
foregroundColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
),
child: const Text(""),
),
)
],
),
)
),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: widget.backgroundColor,
border: Border.symmetric(
vertical: BorderSide(
color: widget.outlineColor,
width: 1
)
),
),
margin: const EdgeInsets.symmetric(
horizontal: 4
),
child: Container(
child: Stack(
children: [
Container(
width: 40,
height: 40,
child: Icon(
Icons.arrow_downward,
color: widget.outlineColor,
),
),
Positioned.fill(
child: ElevatedButton(
onPressed: () {
_currentIndex = wrap(_currentIndex + 4, 0, announcementWidgets.length, increment: 4);
setState(() {});
print(_currentIndex);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
foregroundColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
),
child: const Text(""),
),
)
],
),
)
),
]
),
),
],
),
),
Container(
height: 2,
color: widget.outlineColor,
),
]
@@ -669,13 +619,14 @@ class StopAnnouncementPicker extends AnnouncementPicker {
required this.routeVariant,
required Color backgroundColor,
required Color outlineColor,
String label = "Stops"
}) : super(
key: key,
backgroundColor: backgroundColor,
outlineColor: outlineColor,
announcements: [
for (BusRouteStop stop in routeVariant.busStops)
_AnnouncementEntry(
AnnouncementEntry(
label: stop.formattedStopName,
onPressed: () {
LiveInformation liveInformation = LiveInformation();
@@ -688,7 +639,8 @@ class StopAnnouncementPicker extends AnnouncementPicker {
outlineColor: outlineColor,
alert: LiveInformation().announcementModule.announcementCache[stop.getAudioFileName()] == null,
)
]
],
label: label
);
}
@@ -709,7 +661,7 @@ int wrap(int i, int j, int length, {int increment = -1}) {
}
}
class _AnnouncementEntry extends StatelessWidget {
class AnnouncementEntry extends StatelessWidget {
final String label;
@@ -719,7 +671,7 @@ class _AnnouncementEntry extends StatelessWidget {
bool alert = false;
_AnnouncementEntry({super.key, required this.label, required this.onPressed, required this.index, required this.outlineColor, this.alert = false});
AnnouncementEntry({super.key, required this.label, required this.onPressed, required this.index, required this.outlineColor, this.alert = false});
@override
Widget build(BuildContext context) {
@@ -730,12 +682,6 @@ class _AnnouncementEntry extends StatelessWidget {
decoration: BoxDecoration(
color: Colors.transparent,
border: Border.symmetric(
vertical: BorderSide(
color: outlineColor,
width: 2
)
),
),
padding: const EdgeInsets.symmetric(
@@ -760,7 +706,7 @@ class _AnnouncementEntry extends StatelessWidget {
label,
style: GoogleFonts.teko(
fontSize: 25,
color: outlineColor,
color: Colors.white,
),
overflow: TextOverflow.ellipsis,
),

View File

@@ -1228,11 +1228,11 @@ class _ConsoleState extends State<Console> {
// TODO: implement initState
super.initState();
_listenerReceipt = LiveInformation().commandModule.onCommandReceived.addListener((p0) {
/*_listenerReceipt = LiveInformation().commandModule.onCommandReceived.addListener((p0) {
print("Command received, updating console");
setState(() {});
});
});*/
}
@@ -1253,7 +1253,7 @@ class _ConsoleState extends State<Console> {
Text("Command History:")
);
for (int i = 0; i < LiveInformation().commandModule.commandHistory.length; i++){
/*for (int i = 0; i < LiveInformation().commandModule.commandHistory.length; i++){
CommandInfo command = LiveInformation().commandModule.commandHistory[i];
commands.add(
@@ -1271,7 +1271,7 @@ class _ConsoleState extends State<Console> {
)
);
}
*/
return Container(
decoration: BoxDecoration(
@@ -1299,7 +1299,7 @@ class _ConsoleState extends State<Console> {
color: Colors.white70,
),
Container(
/*Container(
height: 50,
padding: const EdgeInsets.all(8),
child: TextField(
@@ -1315,7 +1315,7 @@ class _ConsoleState extends State<Console> {
LiveInformation().commandModule.executeCommand(value);
},
),
)
)*/
],

View File

@@ -1,6 +1,7 @@
import 'package:bus_infotainment/pages/tfl_dataset_test.dart';
import 'package:bus_infotainment/remaster/InitialStartup.dart';
import 'package:bus_infotainment/remaster/dashboard.dart';
import 'package:flutter/material.dart';
@@ -15,13 +16,22 @@ class RemasteredApp extends StatelessWidget {
darkTheme: ShadThemeData(
brightness: Brightness.dark,
colorScheme: ShadSlateColorScheme.dark(),
// force dark mode
),
themeMode: ThemeMode.dark,
routes: {
'/setup': (context) => InitialStartup(),
'/': (context) => HomePage_Re(),
'/routes': (context) => RoutePage(),
'/enroute': (context) => EnRoutePage(),
'/legacy': (context) => TfL_Dataset_Test(),
'/multi': (context) => MultiModeSetup(),
'/multi/enroute': (context) => MultiModeEnroute(),
'/multi/login': (context) => MultiModeLogin(),
'/multi/register': (context) => MultiModeRegister(),
'/display': (context) => FullscreenDisplay(),
'/multi/join': (context) => MultiModeJoin(),
},

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,112 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:appwrite/appwrite.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
// import this package https://pub.dev/packages/web_socket_channel
class RealtimeKeepAliveConnection {
RealtimeKeepAliveConnection({
required this.channels,
required this.domain,
required this.client,
this.keepAlivePingDuration = const Duration(seconds: 90),
required this.onData,
required this.onError,
});
final List<String> channels;
final String domain;
final Duration keepAlivePingDuration;
final Client client;
final Function(RealtimeMessage) onData;
final Function(dynamic) onError;
// ignore: unused_field
StreamSubscription<dynamic>? _subscription;
WebSocketChannel? _webSocket;
final Stopwatch _stopwatch = Stopwatch();
bool _keepAlive = true;
bool _sentKeepAlivePing = false;
int reconnectCount = 0;
Future initialize() async {
await _initRealtime(
onData: _realtimeOnData,
onDone: _realtimeOnDone,
onError: _realtimeOnError,
);
_heartbeat();
}
void close() {
_keepAlive = false;
_subscription!.cancel();
}
void _heartbeat() async {
while (_keepAlive) {
await Future.delayed(keepAlivePingDuration);
if (_webSocket != null) {
_sentKeepAlivePing = true;
_webSocket!.sink.add("ping");
}
}
}
void _realtimeOnData(RealtimeMessage data) {
log("[$reconnectCount][${_stopwatch.elapsed}] onData");
onData(data);
}
void _realtimeOnDone() async {
reconnectCount++;
log("[$reconnectCount][${_stopwatch.elapsed}] onDone");
if (_keepAlive) {
if (_subscription != null) _subscription!.cancel();
_subscription = _subscription = await _initRealtime(
onData: _realtimeOnData,
onDone: _realtimeOnDone,
onError: _realtimeOnError,
);
}
}
void _realtimeOnError(dynamic e) {
log("[$reconnectCount][${_stopwatch.elapsed}] onError:$e");
onError(onError);
}
Future _initRealtime({
required Function(RealtimeMessage) onData,
required Function() onDone,
required Function(dynamic) onError,
}) async {
_stopwatch.reset();
_stopwatch.start();
String channelParams = channels.map((c) => "channels[]=$c").join('&');
String? projectId = client.config['project'];
final wssUrl = Uri.parse('wss://$domain/realtime?project=$projectId&$channelParams');
_webSocket = WebSocketChannel.connect(wssUrl);
Realtime realtime = Realtime(client);
RealtimeSubscription subscriptionRealTime = realtime.subscribe(channels);
subscriptionRealTime.stream.listen(onData, onDone: onDone, onError: onError);
_subscription = _webSocket!.stream.listen(_handlePingMsg);
}
void _handlePingMsg(dynamic response) {
var json = jsonDecode(response);
if (json["type"] == "error" && _sentKeepAlivePing) {
_sentKeepAlivePing = false;
log("Web socket keep-alive heartbeat successful (Reconnect Count: $reconnectCount, Time alive: ${_stopwatch.elapsed})");
return;
}
}
}