-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add initial live timing support to GUI
Signed-off-by: eXhumer <[email protected]>
- Loading branch information
Showing
7 changed files
with
300 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { useRef } from 'react'; | ||
|
||
import { useAppSelector } from '../Hook'; | ||
import { F1TVPlatform } from '../Type'; | ||
|
||
import styles from './ContentPlayForm.module.scss'; | ||
|
||
const ContentPlayForm = () => { | ||
const inputRef = useRef<HTMLInputElement>(null); | ||
const platformRef = useRef<HTMLSelectElement>(null); | ||
|
||
const config = useAppSelector(state => state.f1tv.config); | ||
const location = useAppSelector(state => state.f1tv.location); | ||
const isReady = config !== null && location !== null; | ||
|
||
return ( | ||
<div className={`${styles['container']} ${styles['padding']}`}> | ||
<h2>F1TV Status: {isReady ? 'Ready' : 'Initializing'}!</h2> | ||
<form className={styles['container']}> | ||
<input | ||
disabled={!isReady} | ||
ref={inputRef} | ||
type='text' | ||
placeholder='Enter Content ID to view' | ||
onChange={e => { | ||
// remove non digit characters | ||
e.target.value = e.target.value.replace(/\D/g, ''); | ||
}} | ||
/> | ||
<select | ||
ref={platformRef} | ||
defaultValue={F1TVPlatform.WEB_DASH} | ||
disabled={!isReady} | ||
> | ||
{Object | ||
.values(F1TVPlatform) | ||
.map(val => ( | ||
<option | ||
key={val} | ||
value={val} | ||
>{val}</option> | ||
))} | ||
</select> | ||
<button | ||
disabled={!isReady} | ||
type='submit' | ||
onClick={e => { | ||
e.preventDefault(); | ||
|
||
if (inputRef.current) { | ||
const contentId = parseInt(inputRef.current.value); | ||
|
||
if (!isNaN(contentId)) { | ||
inputRef.current.value = ''; | ||
} | ||
|
||
mainWindow.newPlayer(contentId, platformRef.current.value); | ||
} | ||
}} | ||
> Play Content </button> | ||
</form> | ||
<button onClick={() => f1tv.logout()}>Logout</button> | ||
</div> | ||
); | ||
}; | ||
|
||
export default ContentPlayForm; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
.container { | ||
display: flex; | ||
flex-direction: column; | ||
gap: 1rem; | ||
} | ||
|
||
.padding { | ||
padding: 1rem; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
import { HttpTransportType, HubConnection, HubConnectionBuilder, HubConnectionState, LogLevel } from "@microsoft/signalr"; | ||
import { useEffect, useState } from "react"; | ||
|
||
import { deepMerge } from '../../../utils'; | ||
|
||
import styles from './LiveTiming.module.scss'; | ||
|
||
const LiveTiming = () => { | ||
const [connection, setConnection] = useState<HubConnection | null>(null); | ||
const [current, setCurrent] = useState<Record<string, unknown> | null>(null); | ||
const [topics, setTopics] = useState<string[]>([]); | ||
const [newTopic, setNewTopic] = useState<string>(''); | ||
|
||
useEffect(() => { | ||
const newConnection = new HubConnectionBuilder() | ||
.withUrl('https://livetiming.formula1.com/signalrcore', { | ||
transport: HttpTransportType.WebSockets, | ||
}) | ||
.withAutomaticReconnect() | ||
.configureLogging(LogLevel.Debug) | ||
.build(); | ||
|
||
newConnection.onclose(async (err) => { | ||
if (err) { | ||
console.error('Connection closed with error:', err); | ||
return; | ||
} | ||
|
||
console.log('Connection closed', newConnection.connectionId, newConnection.state); | ||
}); | ||
|
||
newConnection.onreconnected(connId => { | ||
console.log('Reconnected to the server!', connId); | ||
}); | ||
|
||
newConnection.onreconnecting(err => { | ||
if (err) { | ||
console.error('Reconnecting to the server failed:', err); | ||
return; | ||
} | ||
|
||
console.log('Reconnecting to the server', newConnection.connectionId, newConnection.state); | ||
}); | ||
|
||
newConnection | ||
.start() | ||
.then(() => { | ||
console.log('Connection ID', newConnection.connectionId, newConnection.state); | ||
setConnection(newConnection); | ||
setCurrent(null); | ||
}) | ||
.catch(err => { | ||
console.error('Failed to start connection:', err); | ||
setConnection(null); | ||
setCurrent(null); | ||
}); | ||
|
||
return () => { | ||
console.log('Stopping connection', newConnection.connectionId); | ||
|
||
newConnection | ||
.stop() | ||
.catch(err => { | ||
console.error('Failed to stop connection:', err); | ||
}) | ||
.finally(() => { | ||
setConnection(null); | ||
setCurrent(null); | ||
}); | ||
}; | ||
}, []); | ||
|
||
useEffect(() => { | ||
if (connection && connection.state === HubConnectionState.Connected) { | ||
const onFeed = (topic: string, update: unknown, timestamp: string) => { | ||
console.log('feed', topic, update, timestamp); | ||
setCurrent(prev => deepMerge(prev, { [topic]: update })); | ||
}; | ||
|
||
connection.on('feed', onFeed); | ||
|
||
return () => { | ||
connection.off('feed', onFeed); | ||
}; | ||
} | ||
}, [connection]); | ||
|
||
useEffect(() => { | ||
if (!connection) | ||
return; | ||
|
||
const currentTopics = Object.keys(current || {}); | ||
const toSubscribe = topics.filter(topic => !currentTopics.includes(topic)); | ||
const toUnsubscribe = currentTopics.filter(topic => !topics.includes(topic)); | ||
|
||
if (toUnsubscribe.length > 0) { | ||
connection | ||
.invoke('Unsubscribe', toUnsubscribe) | ||
.then(() => { | ||
console.log('Unsubscribed', toUnsubscribe); | ||
|
||
setCurrent(prev => { | ||
if (!prev) | ||
return null; | ||
|
||
const next = { ...prev }; | ||
toUnsubscribe.forEach(topic => delete next[topic]); | ||
return next; | ||
}); | ||
}); | ||
} | ||
|
||
if (toSubscribe.length > 0) { | ||
connection | ||
.invoke('Subscribe', toSubscribe) | ||
.then(subscribed => { | ||
console.log('Subscribed', subscribed); | ||
|
||
setCurrent(prev => ({ ...prev, ...subscribed })); | ||
}); | ||
} | ||
}, [connection, topics]); | ||
|
||
const handleAddTopic = () => { | ||
if (newTopic && !topics.includes(newTopic)) { | ||
setTopics(prev => [...prev, newTopic]); | ||
setNewTopic(''); | ||
} | ||
}; | ||
|
||
const handleRemoveTopic = (topicToRemove: string) => { | ||
setTopics(prev => prev.filter(topic => topic !== topicToRemove)); | ||
}; | ||
|
||
return ( | ||
<div className={`${styles['container']} ${styles['padding']}`}> | ||
<h2>Live Timing</h2> | ||
<p>Connection Status: {!connection ? 'Not Connected' : connection.state}</p> | ||
|
||
<div> | ||
<input | ||
type="text" | ||
value={newTopic} | ||
onChange={(e) => setNewTopic(e.target.value)} | ||
placeholder="Enter topic name" | ||
/> | ||
<button onClick={handleAddTopic}>Add Topic</button> | ||
</div> | ||
|
||
<div> | ||
<h3>Active Topics:</h3> | ||
{topics.map(topic => ( | ||
<div key={topic}> | ||
{topic} | ||
<button onClick={() => handleRemoveTopic(topic)}>Remove</button> | ||
</div> | ||
))} | ||
</div> | ||
|
||
<div> | ||
<h3>Current Data:</h3> | ||
<pre>{JSON.stringify(current, undefined, 2)}</pre> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default LiveTiming; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
// @ts-expect-error ignore types for initial and update for now | ||
export const deepMerge = (initial, update) => { | ||
for (const key in update) { | ||
if (key in initial) { | ||
if (typeof initial[key] in ["function", "symbol"]) | ||
throw new Error("invalid value type!"); | ||
|
||
else if (typeof initial[key] === "object") { | ||
if (Array.isArray(initial[key])) { | ||
if (Array.isArray(update[key])) | ||
initial[key] = update[key]; | ||
|
||
else { | ||
if (typeof update[key] !== "object") | ||
throw new Error("data type change between initial and update!"); | ||
|
||
for (const arrKey in update[key]) { | ||
if (parseInt(arrKey) < initial[key].length) { | ||
if (parseInt(arrKey) < 0) | ||
throw new Error("invalid array key!"); | ||
|
||
// @ts-expect-error ignore 'Element implicitly has an 'any' type because index expression is not of type 'number' for now | ||
initial[key][arrKey] = deepMerge(initial[key][arrKey], update[key][arrKey]); | ||
} | ||
|
||
else { | ||
// @ts-expect-error ignore 'Element implicitly has an 'any' type because index expression is not of type 'number' for now | ||
initial[key][arrKey] = update[key][arrKey]; | ||
} | ||
} | ||
initial[key] = deepMerge(initial[key], update[key]); | ||
} | ||
} | ||
|
||
else { | ||
initial[key] = deepMerge(initial[key], update[key]); | ||
} | ||
} | ||
|
||
else | ||
initial[key] = update[key]; | ||
} | ||
|
||
else | ||
initial[key] = update[key]; | ||
} | ||
|
||
return initial; | ||
}; |