improve sync ui

This commit is contained in:
Robin Appelman 2024-11-24 20:23:51 +01:00
commit ed63ffa394
8 changed files with 129 additions and 51 deletions

View file

@ -3,6 +3,8 @@ export interface AnalyseMenuProps {
onShare: Function; onShare: Function;
canShare: boolean; canShare: boolean;
isShared: boolean; isShared: boolean;
clients: number,
inShared: boolean,
} }
export function AnalyseMenu(props: AnalyseMenuProps) { export function AnalyseMenu(props: AnalyseMenuProps) {
@ -13,17 +15,34 @@ export function AnalyseMenu(props: AnalyseMenuProps) {
style={{width: `${(loc().length * 8)}px`}} style={{width: `${(loc().length * 8)}px`}}
onFocus={(event) => { onFocus={(event) => {
(event.target as HTMLInputElement).select() (event.target as HTMLInputElement).select()
}}/> : ''; }}/> : <span class="share-text">Start a shared session</span>;
const shareButton = () => (props.canShare) ? <div class="analyse-menu"> const clientCount = () => (props.isShared) ?
<button class="share-session" title="Start a shared session" <div class="clients">{props.clients} {(props.clients === 1) ? "spectator" : "spectators"}</div> : [];
onClick={() => {
props.onShare()
}}/>
{shareText}
</div> : '';
return (<div> const shareButton = () => {
if (props.canShare) {
return [
<div class="share">
<button class="share-session" title="Start a shared session"
onClick={() => {
props.onShare()
}}/>
{shareText}
</div>,
clientCount,
]
} else if (props.inShared) {
return <div class="share shared">
You're spectating a session controlled by someone else
</div>
} else {
return [];
}
}
return (<div class="analyse-menu">
{shareButton} {shareButton}
</div>) </div>)
} }

View file

@ -25,13 +25,14 @@ export const Analyser = (props: AnalyseProps) => {
const [scale, setScale] = createSignal<number>(1); const [scale, setScale] = createSignal<number>(1);
const [playing, setPlaying] = createSignal<boolean>(false); const [playing, setPlaying] = createSignal<boolean>(false);
const [sessionName, setSessionName] = createSignal<string>(""); const [sessionName, setSessionName] = createSignal<string>("");
const [clients, setClients] = createSignal<number>(0);
let lastFrameTime = 0; let lastFrameTime = 0;
let playStartTick = 0; let playStartTick = 0;
let playStartTime = 0; let playStartTime = 0;
const onUpdate = (update: StateUpdate) => { const onUpdate = (update: StateUpdate) => {
if (update["tick"]) { if (update.hasOwnProperty("tick")) {
setTick(update["tick"]); setTick(update["tick"]);
} }
if (update.hasOwnProperty("playing")) { if (update.hasOwnProperty("playing")) {
@ -41,6 +42,10 @@ export const Analyser = (props: AnalyseProps) => {
pause(); pause();
} }
} }
if (update.hasOwnProperty("clients")) {
console.log(update["clients"]);
setClients(update["clients"]);
}
} }
let session: Session | null = null; let session: Session | null = null;
@ -137,7 +142,7 @@ export const Analyser = (props: AnalyseProps) => {
const buildings = () => parser.getBuildingsAtTick(tick()); const buildings = () => parser.getBuildingsAtTick(tick());
const kills = parser.getKills(); const kills = parser.getKills();
const playButtonText = () => (playing()) ? '⏸' : '▶️'; const playButtonText = () => (playing()) ? '⏸' : '▶️';
const disabled = session && !session.isOwner(); const inShared = session && !session.isOwner();
const isShared = () => sessionName() !== ''; const isShared = () => sessionName() !== '';
console.log(intervalPerTick); console.log(intervalPerTick);
@ -160,12 +165,15 @@ export const Analyser = (props: AnalyseProps) => {
onShare={() => { onShare={() => {
session = Session.create({ session = Session.create({
tick: tick(), tick: tick(),
playing: playing() playing: playing(),
}); clients: 0,
}, onUpdate);
setSessionName(session.sessionName); setSessionName(session.sessionName);
}} }}
canShare={props.isStored && !disabled} canShare={props.isStored && !inShared}
isShared={isShared()} isShared={isShared()}
clients={clients()}
inShared={inShared}
/> />
<SpecHUD parser={parser} tick={tick()} <SpecHUD parser={parser} tick={tick()}
players={players()} kills={kills}/> players={players()} kills={kills}/>
@ -174,14 +182,14 @@ export const Analyser = (props: AnalyseProps) => {
title={timeTitle()}> title={timeTitle()}>
<input class="play-pause-button" type="button" <input class="play-pause-button" type="button"
value={playButtonText()} value={playButtonText()}
disabled={disabled} disabled={inShared}
onClick={togglePlay} onClick={togglePlay}
/> />
<Timeline parser={parser} tick={tick()} <Timeline parser={parser} tick={tick()}
onSetTick={throttle(50, (tick) => { onSetTick={throttle(50, (tick) => {
setTickNow(tick); setTickNow(tick);
})} })}
disabled={disabled}/> disabled={inShared}/>
</div> </div>
</div> </div>
); );

View file

@ -1,24 +1,22 @@
import {ownKeys} from "solid-js/store/types/store";
const syncUri = 'wss://sync.demos.tf';
export class Session { export class Session {
public readonly owner_token: string | null; public readonly owner_token: string | null;
private socket: WebSocket | null; private socket: WebSocket | null;
public readonly sessionName: string; public readonly sessionName: string;
private initialState: PlaybackState | null; private initialState: PlaybackState | null;
private readonly onState: (StateUpdate) => void | null; private readonly onState: (StateUpdate) => void | null;
private readonly syncUri;
constructor(name: string, owner_token: string | null = null, initialState: PlaybackState | null, onState: (StateUpdate) => void | null = null) { constructor(name: string, owner_token: string | null = null, initialState: PlaybackState | null, onState: (StateUpdate) => void) {
this.owner_token = owner_token; this.owner_token = owner_token;
this.sessionName = name; this.sessionName = name;
this.initialState = initialState; this.initialState = initialState;
this.onState = onState; this.onState = onState;
this.syncUri = document.querySelector('[data-sync]').getAttribute('data-sync');
this.open(); this.open();
} }
public static create(state: PlaybackState): Session { public static create(state: PlaybackState, onState: (StateUpdate) => void): Session {
return new Session(generateToken(), generateToken(), state) return new Session(generateToken(), generateToken(), state, onState)
} }
public static join(name: string, onState: (StateUpdate) => void): Session { public static join(name: string, onState: (StateUpdate) => void): Session {
@ -29,26 +27,36 @@ export class Session {
if (this.socket) { if (this.socket) {
return; return;
} }
this.socket = new WebSocket(syncUri); this.socket = new WebSocket(this.syncUri);
this.socket.onopen = () => { this.socket.onopen = () => {
if (this.socket) { if (this.socket) {
if (this.owner_token) { if (this.owner_token) {
this.socket.send(JSON.stringify({ if (this.initialState) {
type: 'create', this.socket.send(JSON.stringify({
session: this.sessionName, type: 'create',
token: this.owner_token session: this.sessionName,
})); token: this.owner_token
this.socket.send(JSON.stringify({ }));
type: 'tick', this.socket.send(JSON.stringify({
session: this.sessionName, type: 'tick',
tick: this.initialState.tick session: this.sessionName,
})); tick: this.initialState.tick
this.socket.send(JSON.stringify({ }));
type: 'play', this.socket.send(JSON.stringify({
session: this.sessionName, type: 'play',
play: this.initialState.playing session: this.sessionName,
})); play: this.initialState.playing
this.initialState = null; }));
this.initialState = null;
}
this.socket.onmessage = (event) => {
const packet = JSON.parse(event.data) as Packet;
if (packet.type === 'clients') {
this.onState({
clients: packet.count
});
}
}
} else { } else {
this.socket.send(JSON.stringify({ this.socket.send(JSON.stringify({
type: 'join', type: 'join',
@ -62,7 +70,7 @@ export class Session {
}); });
} }
if (packet.type === 'play') { if (packet.type === 'play') {
if (packet.play || packet.tick) { if (packet.play) {
this.onState({ this.onState({
playing: true playing: true
}); });
@ -123,6 +131,7 @@ function generateToken(): string {
export interface PlaybackState { export interface PlaybackState {
tick: number, tick: number,
playing: boolean, playing: boolean,
clients: number,
} }
export type StateUpdate = Partial<PlaybackState>; export type StateUpdate = Partial<PlaybackState>;
@ -147,7 +156,12 @@ export interface PlayPacket {
type: 'play'; type: 'play';
session: string; session: string;
play?: boolean; play?: boolean;
tick?: boolean; //old sync server
} }
export type Packet = JoinPacket | CreatePacket | TickPacket | PlayPacket; export interface ClientsPacket {
type: 'clients';
session: string;
count: number;
}
export type Packet = JoinPacket | CreatePacket | TickPacket | PlayPacket | ClientsPacket;

View file

@ -86,6 +86,8 @@ pub struct SiteConfig {
pub api: String, pub api: String,
#[serde(default = "default_maps")] #[serde(default = "default_maps")]
pub maps: String, pub maps: String,
#[serde(default = "default_sync")]
pub sync: String,
} }
fn default_api() -> String { fn default_api() -> String {
@ -96,6 +98,10 @@ fn default_maps() -> String {
"https://maps.demos.tf/".into() "https://maps.demos.tf/".into()
} }
fn default_sync() -> String {
"wss://sync.demos.tf/".into()
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct TracingConfig { pub struct TracingConfig {
pub endpoint: String, pub endpoint: String,

View file

@ -74,6 +74,7 @@ struct App {
openid: SteamOpenId, openid: SteamOpenId,
api: String, api: String,
maps: String, maps: String,
sync: String,
map_list: MapList, map_list: MapList,
pub session_store: MemoryStore, pub session_store: MemoryStore,
} }
@ -156,6 +157,7 @@ async fn main() -> Result<()> {
.expect("invalid steam login url"), .expect("invalid steam login url"),
api: config.site.api, api: config.site.api,
maps: config.site.maps, maps: config.site.maps,
sync: config.site.sync,
map_list, map_list,
session_store: session_store.clone(), session_store: session_store.clone(),
}); });
@ -518,6 +520,7 @@ async fn viewer(
ViewerPage { ViewerPage {
demo, demo,
maps: &app.maps, maps: &app.maps,
sync: &app.sync,
}, },
session, session,
)) ))

View file

@ -8,6 +8,7 @@ use std::borrow::Cow;
pub struct ViewerPage<'a> { pub struct ViewerPage<'a> {
pub demo: Option<Demo>, pub demo: Option<Demo>,
pub maps: &'a str, pub maps: &'a str,
pub sync: &'a str,
} }
#[derive(Asset)] #[derive(Asset)]
@ -48,8 +49,9 @@ impl Page for ViewerPage<'_> {
let script = ViewerScript::url(); let script = ViewerScript::url();
let style_url = ViewerStyle::url(); let style_url = ViewerStyle::url();
let maps = self.maps; let maps = self.maps;
let sync = self.sync;
html! { html! {
.viewer-page data-maps = (maps) { .viewer-page data-maps = (maps) data-sync = (sync) {
@if let Some(demo) = self.demo.as_ref() { @if let Some(demo) = self.demo.as_ref() {
input type = "hidden" name = "url" value = (demo.url) {} input type = "hidden" name = "url" value = (demo.url) {}
progress.download min = "0" max = "100" value = "0" {} progress.download min = "0" max = "100" value = "0" {}

View file

@ -3,19 +3,31 @@
top: 0; top: 0;
left: 0; left: 0;
opacity: 0.5; opacity: 0.5;
transition: all 0.5s; transition: opacity 0.3s;
& .share {
display: flex;
align-items: center;
&.shared {
padding: 2px 2px 2px 6px;
}
}
&:hover { &:hover {
opacity: 1; opacity: 1;
& span.share-text {
opacity: 1;
}
} }
& .share-session { & .share-session {
background: transparent; background: transparent;
color: var(--primary-color); color: var(--primary-color);
font-size: 200%; font-size: 200%;
width: 32px; width: 24px;
height: 32px; height: 24px;
margin: 10px; margin: 10px;
border: none; border: none;
cursor: pointer; cursor: pointer;
@ -27,7 +39,7 @@
} }
} }
& .share-text { & input.share-text {
color: var(--primary-color); color: var(--primary-color);
background-color: var(--text-secondary); background-color: var(--text-secondary);
padding: 5px; padding: 5px;
@ -39,4 +51,18 @@
outline: none; outline: none;
} }
} }
span.share-text {
color: var(--primary-color);
opacity: 0;
transition: opacity 0.5s;
&:active, &:focus {
outline: none;
}
}
.clients {
margin-left: 10px;
}
} }

View file

@ -1,9 +1,9 @@
.map-holder { .map-holder {
position: fixed; position: fixed;
top: 32px; top: 40px;
left: 0; left: 0;
width: 100%; width: 100%;
height: calc(100% - 32px - 100px); height: calc(100% - 40px - 100px);
} }
.time-control { .time-control {