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;
canShare: boolean;
isShared: boolean;
clients: number,
inShared: boolean,
}
export function AnalyseMenu(props: AnalyseMenuProps) {
@ -13,17 +15,34 @@ export function AnalyseMenu(props: AnalyseMenuProps) {
style={{width: `${(loc().length * 8)}px`}}
onFocus={(event) => {
(event.target as HTMLInputElement).select()
}}/> : '';
}}/> : <span class="share-text">Start a shared session</span>;
const shareButton = () => (props.canShare) ? <div class="analyse-menu">
<button class="share-session" title="Start a shared session"
onClick={() => {
props.onShare()
}}/>
{shareText}
</div> : '';
const clientCount = () => (props.isShared) ?
<div class="clients">{props.clients} {(props.clients === 1) ? "spectator" : "spectators"}</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}
</div>)
}

View file

@ -25,13 +25,14 @@ export const Analyser = (props: AnalyseProps) => {
const [scale, setScale] = createSignal<number>(1);
const [playing, setPlaying] = createSignal<boolean>(false);
const [sessionName, setSessionName] = createSignal<string>("");
const [clients, setClients] = createSignal<number>(0);
let lastFrameTime = 0;
let playStartTick = 0;
let playStartTime = 0;
const onUpdate = (update: StateUpdate) => {
if (update["tick"]) {
if (update.hasOwnProperty("tick")) {
setTick(update["tick"]);
}
if (update.hasOwnProperty("playing")) {
@ -41,6 +42,10 @@ export const Analyser = (props: AnalyseProps) => {
pause();
}
}
if (update.hasOwnProperty("clients")) {
console.log(update["clients"]);
setClients(update["clients"]);
}
}
let session: Session | null = null;
@ -137,7 +142,7 @@ export const Analyser = (props: AnalyseProps) => {
const buildings = () => parser.getBuildingsAtTick(tick());
const kills = parser.getKills();
const playButtonText = () => (playing()) ? '⏸' : '▶️';
const disabled = session && !session.isOwner();
const inShared = session && !session.isOwner();
const isShared = () => sessionName() !== '';
console.log(intervalPerTick);
@ -160,12 +165,15 @@ export const Analyser = (props: AnalyseProps) => {
onShare={() => {
session = Session.create({
tick: tick(),
playing: playing()
});
playing: playing(),
clients: 0,
}, onUpdate);
setSessionName(session.sessionName);
}}
canShare={props.isStored && !disabled}
canShare={props.isStored && !inShared}
isShared={isShared()}
clients={clients()}
inShared={inShared}
/>
<SpecHUD parser={parser} tick={tick()}
players={players()} kills={kills}/>
@ -174,14 +182,14 @@ export const Analyser = (props: AnalyseProps) => {
title={timeTitle()}>
<input class="play-pause-button" type="button"
value={playButtonText()}
disabled={disabled}
disabled={inShared}
onClick={togglePlay}
/>
<Timeline parser={parser} tick={tick()}
onSetTick={throttle(50, (tick) => {
setTickNow(tick);
})}
disabled={disabled}/>
disabled={inShared}/>
</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 {
public readonly owner_token: string | null;
private socket: WebSocket | null;
public readonly sessionName: string;
private initialState: PlaybackState | 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.sessionName = name;
this.initialState = initialState;
this.onState = onState;
this.syncUri = document.querySelector('[data-sync]').getAttribute('data-sync');
this.open();
}
public static create(state: PlaybackState): Session {
return new Session(generateToken(), generateToken(), state)
public static create(state: PlaybackState, onState: (StateUpdate) => void): Session {
return new Session(generateToken(), generateToken(), state, onState)
}
public static join(name: string, onState: (StateUpdate) => void): Session {
@ -29,26 +27,36 @@ export class Session {
if (this.socket) {
return;
}
this.socket = new WebSocket(syncUri);
this.socket = new WebSocket(this.syncUri);
this.socket.onopen = () => {
if (this.socket) {
if (this.owner_token) {
this.socket.send(JSON.stringify({
type: 'create',
session: this.sessionName,
token: this.owner_token
}));
this.socket.send(JSON.stringify({
type: 'tick',
session: this.sessionName,
tick: this.initialState.tick
}));
this.socket.send(JSON.stringify({
type: 'play',
session: this.sessionName,
play: this.initialState.playing
}));
this.initialState = null;
if (this.initialState) {
this.socket.send(JSON.stringify({
type: 'create',
session: this.sessionName,
token: this.owner_token
}));
this.socket.send(JSON.stringify({
type: 'tick',
session: this.sessionName,
tick: this.initialState.tick
}));
this.socket.send(JSON.stringify({
type: 'play',
session: this.sessionName,
play: this.initialState.playing
}));
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 {
this.socket.send(JSON.stringify({
type: 'join',
@ -62,7 +70,7 @@ export class Session {
});
}
if (packet.type === 'play') {
if (packet.play || packet.tick) {
if (packet.play) {
this.onState({
playing: true
});
@ -123,6 +131,7 @@ function generateToken(): string {
export interface PlaybackState {
tick: number,
playing: boolean,
clients: number,
}
export type StateUpdate = Partial<PlaybackState>;
@ -147,7 +156,12 @@ export interface PlayPacket {
type: 'play';
session: string;
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,
#[serde(default = "default_maps")]
pub maps: String,
#[serde(default = "default_sync")]
pub sync: String,
}
fn default_api() -> String {
@ -96,6 +98,10 @@ fn default_maps() -> String {
"https://maps.demos.tf/".into()
}
fn default_sync() -> String {
"wss://sync.demos.tf/".into()
}
#[derive(Debug, Deserialize)]
pub struct TracingConfig {
pub endpoint: String,

View file

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

View file

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

View file

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