diff --git a/back-end/api/package-lock.json b/back-end/api/package-lock.json
index a9948e31..30a91625 100644
--- a/back-end/api/package-lock.json
+++ b/back-end/api/package-lock.json
@@ -23,6 +23,7 @@
"promised-sqlite3": "^2.1.0",
"socket.io": "^4.7.1",
"sqlite3": "^5.1.6",
+ "uuid": "^9.0.1",
"winston": "^3.10.0"
},
"devDependencies": {
@@ -35,6 +36,7 @@
"@types/passport-jwt": "^3.0.9",
"@types/passport-local": "^1.0.35",
"@types/sqlite3": "^3.1.8",
+ "@types/uuid": "^9.0.4",
"@vercel/ncc": "^0.36.1",
"editorconfig": "^2.0.0",
"eslint": "^8.45.0",
@@ -45,6 +47,7 @@
}
},
"../../lib/models": {
+ "name": "@toa-lib/models",
"version": "1.0.0",
"dependencies": {
"events": "^3.3.0",
@@ -58,6 +61,7 @@
}
},
"../../lib/server": {
+ "name": "@toa-lib/server",
"version": "1.0.0",
"dependencies": {
"@toa-lib/models": "file:../models",
@@ -614,6 +618,12 @@
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.2.tgz",
"integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g=="
},
+ "node_modules/@types/uuid": {
+ "version": "9.0.4",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.4.tgz",
+ "integrity": "sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==",
+ "dev": true
+ },
"node_modules/@vercel/ncc": {
"version": "0.36.1",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.36.1.tgz",
@@ -4312,6 +4322,18 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
diff --git a/back-end/api/package.json b/back-end/api/package.json
index 2b007dcf..688b629a 100644
--- a/back-end/api/package.json
+++ b/back-end/api/package.json
@@ -21,6 +21,7 @@
"@types/passport-jwt": "^3.0.9",
"@types/passport-local": "^1.0.35",
"@types/sqlite3": "^3.1.8",
+ "@types/uuid": "^9.0.4",
"@vercel/ncc": "^0.36.1",
"editorconfig": "^2.0.0",
"eslint": "^8.45.0",
@@ -44,6 +45,7 @@
"promised-sqlite3": "^2.1.0",
"socket.io": "^4.7.1",
"sqlite3": "^5.1.6",
+ "uuid": "^9.0.1",
"winston": "^3.10.0"
}
}
diff --git a/back-end/api/sql/create_global.sql b/back-end/api/sql/create_global.sql
index 65646abd..43d873c8 100644
--- a/back-end/api/sql/create_global.sql
+++ b/back-end/api/sql/create_global.sql
@@ -15,3 +15,17 @@ CREATE TABLE IF NOT EXISTS "event" (
PRIMARY KEY (eventKey),
UNIQUE (eventKey)
);
+
+CREATE TABLE IF NOT EXISTS "socket_clients" (
+ "currentUrl" VARCHAR(255),
+ "ipAddress" VARCHAR(64) NOT NULL,
+ "fieldNumbers" VARCHAR(255),
+ "audienceDisplayChroma" VARCHAR(255),
+ "followerMode" INT NOT NULL,
+ "followerApiHost" VARCHAR(64),
+ "lastSocketId" VARCHAR(64),
+ "connected" INT NOT NULL,
+ "persistantClientId" VARCHAR(64) NOT NULL,
+ PRIMARY KEY (ipAddress, lastSocketId),
+ UNIQUE (ipAddress, lastSocketId)
+);
\ No newline at end of file
diff --git a/back-end/api/src/Server.ts b/back-end/api/src/Server.ts
index 6e0c1006..93d66165 100644
--- a/back-end/api/src/Server.ts
+++ b/back-end/api/src/Server.ts
@@ -22,6 +22,7 @@ import allianceController from './controllers/Alliance.js';
import tournamentController from './controllers/Tournament.js';
import frcFmsController from './controllers/FrcFms.js';
import resultsController from './controllers/Results.js';
+import socketClientsController from './controllers/SocketClients.js';
import { handleCatchAll, handleErrors } from './middleware/ErrorHandler.js';
import logger from './util/Logger.js';
import { initGlobal } from './db/EventDatabase.js';
@@ -69,6 +70,7 @@ app.use('/alliance', allianceController);
app.use('/tournament', tournamentController);
app.use('/frc/fms', frcFmsController);
app.use('/results', resultsController);
+app.use('/socketClients', socketClientsController);
// Define root/testing paths
app.get('/', requireAuth, (req, res) => {
diff --git a/back-end/api/src/controllers/SocketClients.ts b/back-end/api/src/controllers/SocketClients.ts
new file mode 100644
index 00000000..030248d5
--- /dev/null
+++ b/back-end/api/src/controllers/SocketClients.ts
@@ -0,0 +1,108 @@
+import { NextFunction, Request, Response, Router } from 'express';
+import { getDB } from '../db/EventDatabase.js';
+import { v4 as uuidv4 } from 'uuid';
+
+const router = Router();
+
+// Get all displays
+router.get(
+ '/',
+ async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const db = await getDB('global');
+ const clients = await db.selectAll(
+ 'socket_clients'
+ );
+ res.json(clients);
+ } catch (e) {
+ next(e);
+ }
+ }
+);
+
+// New client
+router.post(
+ '/connect',
+ async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ // Check if client already exists
+ const db = await getDB('global');
+ if (req.body.persistantClientId) {
+ req.body.connected = 1;
+ // Update
+ req.body.audienceDisplayChroma = req.body.audienceDisplayChroma.replaceAll('"', '')
+ await db.updateWhere(
+ 'socket_clients',
+ req.body,
+ `persistantClientId = "${req.body.persistantClientId}"`
+ );
+ } else {
+ // Brand new client, create new UUID
+ req.body.persistantClientId = uuidv4();
+ await db.insertValue('socket_clients', [req.body]);
+ }
+
+ res.json(req.body);
+ } catch (e) {
+ next(e);
+ }
+ }
+);
+
+// Client Disconnected
+router.post(
+ '/update/:persistantClientId',
+ async (req: Request, res: Response, next: NextFunction) => {
+ const { persistantClientId } = req.params;
+ try {
+ const db = await getDB('global');
+ await db.updateWhere(
+ 'socket_clients',
+ req.body,
+ `persistantClientId = "${persistantClientId}"`
+ );
+ res.json({ success: true });
+ } catch (e) {
+ next(e);
+ }
+ }
+);
+
+// Client Deleted
+router.post(
+ '/disconnect/:lastSocketId',
+ async (req: Request, res: Response, next: NextFunction) => {
+ const { lastSocketId } = req.params;
+ try {
+ const db = await getDB('global');
+ await db.updateWhere(
+ 'socket_clients',
+ { connected: 0 },
+ `lastSocketId = "${lastSocketId}"`
+ );
+ res.json({ success: true });
+ } catch (e) {
+ next(e);
+ }
+ }
+);
+
+// Client Deleted
+router.delete(
+ '/remove/:persistantClientId',
+ async (req: Request, res: Response, next: NextFunction) => {
+ const { persistantClientId } = req.params;
+ try {
+ const db = await getDB('global');
+ await db.deleteWhere(
+ 'socket_clients',
+ `persistantClientId = "${persistantClientId}"`
+ );
+ res.json({ success: true });
+ } catch (e) {
+ next(e);
+ }
+ }
+);
+
+export default router;
diff --git a/back-end/realtime/package-lock.json b/back-end/realtime/package-lock.json
index 648a49f2..d516ff12 100644
--- a/back-end/realtime/package-lock.json
+++ b/back-end/realtime/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
+ "@toa-lib/client": "file:../../lib/client",
"@toa-lib/models": "file:../../lib/models",
"@toa-lib/server": "file:../../lib/server",
"body-parser": "^1.20.2",
@@ -37,6 +38,18 @@
"utf-8-validate": "^5.0.10"
}
},
+ "../../lib/client": {
+ "version": "1.0.0",
+ "dependencies": {
+ "@toa-lib/models": "file:../models",
+ "socket.io-client": "^4.7.1"
+ },
+ "devDependencies": {
+ "@types/node": "^20.4.2",
+ "@types/socket.io-client": "^3.0.0",
+ "typescript": "^5.1.6"
+ }
+ },
"../../lib/models": {
"name": "@toa-lib/models",
"version": "1.0.0",
@@ -78,6 +91,9 @@
"../../shared": {
"extraneous": true
},
+ "../lib/client": {
+ "extraneous": true
+ },
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -101,6 +117,10 @@
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
+ "node_modules/@toa-lib/client": {
+ "resolved": "../../lib/client",
+ "link": true
+ },
"node_modules/@toa-lib/models": {
"resolved": "../../lib/models",
"link": true
@@ -1472,6 +1492,16 @@
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
+ "@toa-lib/client": {
+ "version": "file:../../lib/client",
+ "requires": {
+ "@toa-lib/models": "file:../models",
+ "@types/node": "^20.4.2",
+ "@types/socket.io-client": "^3.0.0",
+ "socket.io-client": "^4.7.1",
+ "typescript": "^5.1.6"
+ }
+ },
"@toa-lib/models": {
"version": "file:../../lib/models",
"requires": {
diff --git a/back-end/realtime/package.json b/back-end/realtime/package.json
index 268570a1..48748a7e 100644
--- a/back-end/realtime/package.json
+++ b/back-end/realtime/package.json
@@ -23,6 +23,7 @@
"typescript": "^5.1.6"
},
"dependencies": {
+ "@toa-lib/client": "file:../../lib/client",
"@toa-lib/models": "file:../../lib/models",
"@toa-lib/server": "file:../../lib/server",
"body-parser": "^1.20.2",
diff --git a/back-end/realtime/src/Server.ts b/back-end/realtime/src/Server.ts
index fdad0044..a40eb03f 100644
--- a/back-end/realtime/src/Server.ts
+++ b/back-end/realtime/src/Server.ts
@@ -7,6 +7,7 @@ import jwt from "jsonwebtoken";
import { environment as env, getIPv4 } from "@toa-lib/server";
import logger from "./util/Logger.js";
import { assignRooms, initRooms, leaveRooms } from "./rooms/Rooms.js";
+import { clientFetcher } from '@toa-lib/client';
// Setup our environment
env.loadAndSetDefaults(process.env);
@@ -46,6 +47,57 @@ io.on("connection", (socket) => {
`user '${user.username}' (${socket.handshake.address}) connected and verified`
);
+ socket.on("identify", async (data: any) => {
+ try {
+ // Add things
+ data.lastSocketId = socket.id;
+ data.connected = 1;
+ data.ipAddress = socket.handshake.address;
+
+ // Register socket to Database // TODO: Create model for this
+ const settings: any = await clientFetcher(`socketClients/connect`, 'POST', data);
+
+ // Send back the user's data
+ socket.emit("settings", settings);
+ } catch (e) {
+ console.log('Failed to negotiate sockets settings', e);
+ }
+ });
+
+ socket.on("update-socket-client", async (data: any) => {
+ // Update socket client
+ try {
+ // Update Server
+ await clientFetcher(`socketClients/connect`, 'POST', data);
+ // Locate socket by lastSocketId
+ const socketToUpdate = io.sockets.sockets.get(data.lastSocketId);
+ // Update socket
+ socketToUpdate?.emit("settings", data);
+
+ } catch (e) {
+ console.log('Failed to update socket client', e);
+ }
+ });
+
+ socket.on("identify-client", async (data: any) => {
+ // Find socket
+ const socketToIdentify = io.sockets.sockets.get(data.lastSocketId);
+ // Emit message
+ socketToIdentify?.emit("identify-client", data);
+ });
+
+ socket.on("identify-all-clients", async () => {
+ // Get all devices from api
+ const clients: any = await clientFetcher(`socketClients`, 'GET');
+ // Iterate over devices
+ clients.forEach((client: any) => {
+ // Find socket
+ const socketToIdentify = io.sockets.sockets.get(client.lastSocketId);
+ // Emit message
+ socketToIdentify?.emit("identify-client", client);
+ });
+ })
+
socket.on("rooms", (rooms: unknown) => {
if (Array.isArray(rooms) && rooms.every(room => typeof room === "string")) {
logger.info(
@@ -63,6 +115,11 @@ io.on("connection", (socket) => {
logger.info(
`user ${user.username} (${socket.handshake.address}) disconnected: ${reason}`
);
+ try {
+ clientFetcher(`socketClients/disconnect/${socket.id}`, 'POST');
+ } catch (e) {
+ console.log('Failed update socket db info', e);
+ }
leaveRooms(socket);
});
@@ -85,8 +142,7 @@ server.listen(
logger.info(
`[${env.get().nodeEnv.charAt(0).toUpperCase()}][${env
.get()
- .serviceName.toUpperCase()}] Server started on ${host}:${
- env.get().servicePort
+ .serviceName.toUpperCase()}] Server started on ${host}:${env.get().servicePort
}`
);
}
diff --git a/front-end/src/AppRoutes.tsx b/front-end/src/AppRoutes.tsx
index 55b05700..d17453a4 100644
--- a/front-end/src/AppRoutes.tsx
+++ b/front-end/src/AppRoutes.tsx
@@ -26,6 +26,7 @@ const Streaming = lazy(() => import('./apps/Streaming/Streaming'));
import HomeIcon from '@mui/icons-material/Home';
import EventIcon from '@mui/icons-material/Event';
+import AudienceDisplayManager from './apps/AudienceDisplayManager';
export interface AppRoute {
name: string;
path: string;
@@ -164,6 +165,12 @@ const AppRoutes: AppRoute[] = [
path: '/:eventKey/streaming',
group: 0,
element:
+ },
+ {
+ name: 'Audience Display Manager',
+ path: '/:eventKey/audienceDisplayManager',
+ group: 0,
+ element:
}
// {
// name: 'Account Manager',
diff --git a/front-end/src/api/ApiProvider.ts b/front-end/src/api/ApiProvider.ts
index 335f63ce..1a3c6143 100644
--- a/front-end/src/api/ApiProvider.ts
+++ b/front-end/src/api/ApiProvider.ts
@@ -247,6 +247,12 @@ export const useMatchAll = (
export const postFrcFmsSettings = (settings: FMSSettings): Promise =>
clientFetcher(`frc/fms/advancedNetworkingConfig`, 'POST', settings);
+export const deleteSocketClient = (uuid: string): Promise =>
+ clientFetcher(`socketClients/remove/${uuid}`, 'DELETE');
+
+export const updateSocketClient = (uuid: string, data: any): Promise =>
+ clientFetcher(`socketClients/update/${uuid}`, 'POST', data);
+
// Results Syncing
export const resultsSyncMatches = (
eventKey: string,
diff --git a/front-end/src/api/SocketProvider.tsx b/front-end/src/api/SocketProvider.tsx
index 0e76ce0a..566443c6 100644
--- a/front-end/src/api/SocketProvider.tsx
+++ b/front-end/src/api/SocketProvider.tsx
@@ -6,8 +6,15 @@ import {
MatchSocketEvent
} from '@toa-lib/models';
import { Socket } from 'socket.io-client';
-import { useRecoilState } from 'recoil';
-import { socketConnectedAtom } from 'src/stores/NewRecoil';
+import { useRecoilCallback, useRecoilState } from 'recoil';
+import {
+ currentTournamentFieldsAtom,
+ followerModeEnabledAtom,
+ leaderApiHostAtom,
+ socketConnectedAtom,
+ displayChromaKeyAtom
+} from 'src/stores/NewRecoil';
+import { useSnackbar } from 'src/features/hooks/use-snackbar';
let socket: Socket | null = null;
@@ -25,15 +32,67 @@ export const useSocket = (): [
(token: string) => void
] => {
const [connected, setConnected] = useRecoilState(socketConnectedAtom);
+ const { showSnackbar } = useSnackbar();
const setupSocket = (token: string) => {
if (socket) return;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */
socket = createSocket(token);
+ identify();
initEvents();
};
+ const identify = useRecoilCallback(({ snapshot, set }) => async () => {
+ if (socket) {
+ const fields = await snapshot.getPromise(currentTournamentFieldsAtom);
+ const followerModeEnabled = await snapshot.getPromise(
+ followerModeEnabledAtom
+ );
+ const leaderApiHost = await snapshot.getPromise(leaderApiHostAtom);
+ const chromaKey = await snapshot.getPromise(displayChromaKeyAtom);
+
+ // ID Message
+ const persistantClientId = localStorage.getItem('persistantClientId');
+
+ const idMsg = {
+ currentUrl: window.location.href,
+ fieldNumbers: fields.map((d: any) => d.field).join(','),
+ followerMode: followerModeEnabled ? 1 : 0,
+ followerApiHost: leaderApiHost,
+ audienceDisplayChroma: chromaKey.replaceAll('"', ''),
+ persistantClientId
+ };
+
+ socket.on('settings', (data) => {
+ // TODO: Make this get the field names properly
+ set(
+ currentTournamentFieldsAtom,
+ data.fieldNumbers
+ .split(',')
+ .map((d: any) => ({ field: parseInt(d), name: `Field ${d}` }))
+ );
+ set(followerModeEnabledAtom, data.followerMode === 1);
+ // TODO: Setter for this may be broken, investigate this later
+ // set(leaderApiHostAtom, data.followerApiHost);
+ set(displayChromaKeyAtom, data.audienceDisplayChroma);
+ localStorage.setItem('persistantClientId', data.persistantClientId);
+ });
+
+ socket.on('identify-client', () => {
+ showSnackbar(
+ `My unique ID is ${localStorage.getItem(
+ 'persistantClientId'
+ )}, talking on socket ${socket?.id}`,
+ 'success'
+ );
+ });
+
+ socket.emit('identify', idMsg);
+ }
+ // set(currentTournamentFieldsAtom, [])
+ });
+
const initEvents = () => {
if (socket) {
socket.on('connect', () => {
@@ -96,6 +155,21 @@ export async function sendPostResults(): Promise {
socket?.emit(MatchSocketEvent.DISPLAY, 3);
}
+// TODO: Use model for this
+export async function sendUpdateSocketClient(
+ displaySettings: any
+): Promise {
+ socket?.emit('update-socket-client', displaySettings);
+}
+
+export async function requestClientIdentification(data: any): Promise {
+ socket?.emit('identify-client', data);
+}
+
+export async function requestAllClientsIdentification(): Promise {
+ socket?.emit('identify-all-clients');
+}
+
export async function sendUpdateFrcFmsSettings(
hwFingerprint: string
): Promise {
diff --git a/front-end/src/apps/AudienceDisplayManager/AudienceDisplayManager.tsx b/front-end/src/apps/AudienceDisplayManager/AudienceDisplayManager.tsx
new file mode 100644
index 00000000..61e2d2ad
--- /dev/null
+++ b/front-end/src/apps/AudienceDisplayManager/AudienceDisplayManager.tsx
@@ -0,0 +1,202 @@
+import { FC, useState } from 'react';
+import Typography from '@mui/material/Typography';
+import PaperLayout from '@layouts/PaperLayout';
+import { useRecoilState } from 'recoil';
+import { socketClientsAtom } from 'src/stores/NewRecoil';
+import {
+ Button,
+ Checkbox,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ FormControlLabel,
+ IconButton,
+ Table,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ TextField
+} from '@mui/material';
+import {
+ requestAllClientsIdentification,
+ requestClientIdentification,
+ sendUpdateSocketClient
+} from 'src/api/SocketProvider';
+import { Delete, RemoveRedEye } from '@mui/icons-material';
+import { deleteSocketClient } from 'src/api/ApiProvider';
+
+const AudienceDisplayManager: FC = () => {
+ const [clients, setClients] = useRecoilState(socketClientsAtom);
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const [dialogContext, setDialogContext] = useState(null);
+
+ const handleClose = () => {
+ setDialogOpen(false);
+ };
+
+ const openDialog = (context: any) => {
+ setDialogOpen(true);
+ setDialogContext(context);
+ };
+
+ const updateContext = (key: string, value: string | number) => {
+ if (!dialogContext) return;
+ setDialogContext({ ...dialogContext, [key]: value });
+ };
+
+ const saveUpdate = () => {
+ if (!dialogContext) return;
+ setDialogOpen(false);
+ sendUpdateSocketClient(dialogContext);
+ const cpy = [...clients];
+ const id = cpy.findIndex(
+ (e) => e.persistantClientId === dialogContext.persistantClientId
+ );
+ cpy[id] = dialogContext;
+ setClients(cpy);
+ };
+
+ const requestClientToIdentify = (data: any) => {
+ requestClientIdentification(data);
+ };
+
+ const deleteDevice = (id: string) => {
+ deleteSocketClient(id);
+ const cpy = [...clients];
+ const index = cpy.findIndex((e) => e.persistantClientId === id);
+ cpy.splice(index, 1);
+ setClients(cpy);
+ };
+
+ return (
+ Audience Display Manager}
+ padding
+ >
+
+
+
+
+
+ ID
+ IP Address
+ Connetcted
+ Socket ID
+ Last URL
+ Chroma Key
+ Field Numbers
+ Follower Mode Enabled
+ Identify
+ Delete
+
+
+ {clients.map((client) => (
+ openDialog(client)}
+ >
+ {client.persistantClientId}
+ {client.ipAddress}
+ {client.connected ? 'Yes' : 'No'}
+ {client.lastSocketId}
+ {client.currentUrl}
+
+ {client.audienceDisplayChroma.replaceAll('"', '')}
+
+ {client.fieldNumbers}
+ {client.followerMode ? 'Yes' : 'No'}
+
+
+ {
+ requestClientToIdentify(client);
+ e.stopPropagation();
+ }}
+ />
+
+
+
+
+ {
+ deleteDevice(client.persistantClientId);
+ e.stopPropagation();
+ }}
+ />
+
+
+
+ ))}
+
+
+
+ {dialogContext && ( // TODO: make field numbers more pretty
+
+ )}
+
+ );
+};
+
+export default AudienceDisplayManager;
diff --git a/front-end/src/apps/AudienceDisplayManager/index.ts b/front-end/src/apps/AudienceDisplayManager/index.ts
new file mode 100644
index 00000000..5865d341
--- /dev/null
+++ b/front-end/src/apps/AudienceDisplayManager/index.ts
@@ -0,0 +1,3 @@
+import AudienceDisplayManager from './AudienceDisplayManager';
+
+export default AudienceDisplayManager;
diff --git a/front-end/src/apps/Settings/tabs/audience.tsx b/front-end/src/apps/Settings/tabs/audience.tsx
index 66a73d8d..70558b86 100644
--- a/front-end/src/apps/Settings/tabs/audience.tsx
+++ b/front-end/src/apps/Settings/tabs/audience.tsx
@@ -3,16 +3,30 @@ import Box from '@mui/material/Box';
import { useRecoilState } from 'recoil';
import { displayChromaKeyAtom } from 'src/stores/NewRecoil';
import TextSetting from '../components/TextSetting';
+import { updateSocketClient } from 'src/api/ApiProvider';
const AudienceDisplaySettingsTab: FC = () => {
const [chromaKey, setChromaKey] = useRecoilState(displayChromaKeyAtom);
+ let timeout: any = null;
+
+ const update = (key: string | number) => {
+ setChromaKey(key);
+
+ // Don't hammer the server with requests
+ if (timeout) clearTimeout(timeout);
+ timeout = setTimeout(() => {
+ updateSocketClient(localStorage.getItem('persistantClientId') ?? '', {
+ audienceDisplayChroma: key
+ });
+ }, 1000);
+ };
return (
diff --git a/front-end/src/apps/Settings/tabs/main.tsx b/front-end/src/apps/Settings/tabs/main.tsx
index 08ba7f0b..1839f6d7 100644
--- a/front-end/src/apps/Settings/tabs/main.tsx
+++ b/front-end/src/apps/Settings/tabs/main.tsx
@@ -15,6 +15,7 @@ import { TeamKeys } from '@toa-lib/models';
import { MultiSelectSetting } from '../components/MultiSelectSetting';
import TextSetting from '../components/TextSetting';
import { APIOptions } from '@toa-lib/client';
+import { updateSocketClient } from 'src/api/ApiProvider';
const MainSettingsTab: FC = () => {
const [darkMode, setDarkMode] = useRecoilState(darkModeAtom);
@@ -47,6 +48,49 @@ const MainSettingsTab: FC = () => {
APIOptions.host = value.toString();
};
+ let followTimeout: any = null;
+ const updateFollowerMode = (value: boolean) => {
+ handleFollowerModeChange(value);
+
+ // Don't hammer the server with requests
+ if (followTimeout !== null) clearTimeout(followTimeout);
+ followTimeout = setTimeout(() => {
+ updateSocketClient(localStorage.getItem('persistantClientId') ?? '', {
+ followerMode: value ? 1 : 0
+ });
+ }, 1000);
+ };
+
+ let leaderApiHostTimeout: any = null;
+ const updateFollowerApiHost = (value: string | number) => {
+ handleLeaderAddressChange(value);
+
+ // Don't hammer the server with requests
+ if (leaderApiHostTimeout !== null) clearTimeout(leaderApiHostTimeout);
+ leaderApiHostTimeout = setTimeout(() => {
+ updateSocketClient(localStorage.getItem('persistantClientId') ?? '', {
+ followerApiHost: value
+ });
+ }, 1000);
+ };
+
+ let fieldIdTimeout: any = null;
+ const updateFieldControl = (value: any[]) => {
+ handleFieldChange(value);
+
+ // Don't hammer the server with requests
+ const fields = allFields
+ .filter((f) => value.includes(f.name))
+ .map((f) => f.field)
+ .join(',');
+ if (fieldIdTimeout !== null) clearTimeout(fieldIdTimeout);
+ fieldIdTimeout = setTimeout(() => {
+ updateSocketClient(localStorage.getItem('persistantClientId') ?? '', {
+ fieldNumbers: fields
+ });
+ }, 1000);
+ };
+
return (
{
name='Field Control'
value={fieldControl.map((f) => f.name)}
options={allFields.map((f) => f.name)}
- onChange={handleFieldChange}
+ onChange={updateFieldControl}
inline
/>
diff --git a/front-end/src/apps/home/index.tsx b/front-end/src/apps/home/index.tsx
index 699286a8..9d0558aa 100644
--- a/front-end/src/apps/home/index.tsx
+++ b/front-end/src/apps/home/index.tsx
@@ -69,6 +69,10 @@ const HomeApp: FC = () => {
title='Streaming App'
to={`/${event?.eventKey}/streaming`}
/>
+
{/*
*/}
diff --git a/front-end/src/stores/NewRecoil.ts b/front-end/src/stores/NewRecoil.ts
index 6698322a..5114999b 100644
--- a/front-end/src/stores/NewRecoil.ts
+++ b/front-end/src/stores/NewRecoil.ts
@@ -114,6 +114,34 @@ export const displayChromaKeyAtom = atom({
* Recoil state management for frc-fms
*/
// Not public
+const socketClients = selector({
+ key: 'socketClients',
+ get: async () => {
+ try {
+ return await clientFetcher(
+ 'socketClients',
+ 'GET',
+ undefined // , // TODO: Add typeguard
+ // isFMSSettingsArray
+ );
+ } catch (e) {
+ console.log(e);
+ return [];
+ }
+ }
+});
+
+// TODO: Make a model for this
+export const socketClientsAtom = atom({
+ key: 'allSocketClients',
+ default: socketClients
+});
+
+/**
+ * @section Socket Client States
+ * Recoil state management for frc-fms
+ */
+// Not public
const frcFmsSelector = selector({
key: 'fms',
get: async () => {