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 + + Update {dialogContext.persistantClientId} + + + updateContext('audienceDisplayChroma', e.target.value) + } + /> + updateContext('fieldNumbers', e.target.value)} + /> + + updateContext('followerMode', e.target.checked ? 1 : 0) + } + /> + } + label='Enable Follower Mode' + /> + updateContext('followerApiHost', e.target.value)} + /> + + + + + + + )} +
+ ); +}; + +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 () => {