parent
5d839e157b
commit
5c7a30c7f8
@ -0,0 +1,7 @@ |
||||
module.exports = { |
||||
"ignorePatterns": [".eslintrc.js"], |
||||
"parserOptions": { |
||||
"tsconfigRootDir": __dirname, |
||||
"project": ["./tsconfig.json"] |
||||
} |
||||
}; |
@ -1,11 +1,13 @@ |
||||
FROM node:13-alpine |
||||
FROM node:16-alpine |
||||
WORKDIR /app |
||||
|
||||
COPY package.json yarn.lock ./ |
||||
RUN yarn install |
||||
# Install TS manually since it's only included in the parent's package.json |
||||
RUN npm install --global typescript@^4.5.2 |
||||
COPY package.json package-lock.json ./ |
||||
RUN npm ci --no-progress --no-optional |
||||
|
||||
COPY tsconfig.json ./ |
||||
|
||||
COPY src src/ |
||||
COPY data data/ |
||||
CMD ["npm", "run", "app"] |
||||
RUN npm run build |
||||
|
||||
CMD ["npm", "run", "start-compiled"] |
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,210 +1,167 @@ |
||||
import {Socket} from "socket.io"; |
||||
|
||||
import User from "./User"; |
||||
import {getIndex, getNextShot, getTimeline, getTimelineNames, indexForTime} from "./timeline"; |
||||
import {getCurrentTime} from "./util"; |
||||
import { getTimeline, getTimelineNames } from "./timeline"; |
||||
import { getCurrentTime } from "./util"; |
||||
|
||||
export interface RoomOptions { |
||||
seekTime: number |
||||
timelineName: string |
||||
seekTime: number; |
||||
timelineName: string; |
||||
} |
||||
|
||||
export interface TickerMessage { |
||||
user: User; |
||||
message: string; |
||||
} |
||||
|
||||
// FIXME: dedupe with frontend
|
||||
export interface SerializedRoom { |
||||
id: number; |
||||
userCount: number; |
||||
isLeader: boolean; |
||||
running: boolean; |
||||
startTime?: number; |
||||
seekTime: number; |
||||
timelineName: string; |
||||
readyToParticipate: boolean; |
||||
speedFactor: number; |
||||
ticker: string[]; |
||||
users?: { id: string; readyToParticipate: boolean }[]; |
||||
} |
||||
|
||||
export default class Room { |
||||
id: number = 0; |
||||
users: User[] = []; |
||||
leader: User | null = null; |
||||
id = 0; |
||||
users: User[] = []; |
||||
leader: User | null = null; |
||||
|
||||
running = false; |
||||
startTime = 0; |
||||
currentSeconds = 0; |
||||
timelineIndex: number = 0; |
||||
ticker: TickerMessage[] = []; |
||||
|
||||
seekTime: number = 0; |
||||
timelineName: string = 'Centurion'; |
||||
running = false; |
||||
startTime: number | undefined = undefined; |
||||
|
||||
// For debugging purposes
|
||||
speedFactor = 1; |
||||
autoStart = false; |
||||
seekTime = 0; |
||||
timelineName = "Centurion"; |
||||
|
||||
constructor(name: number) { |
||||
this.id = name; |
||||
} |
||||
// For debugging purposes
|
||||
speedFactor = 1; |
||||
|
||||
serialize(user: User) { |
||||
return { |
||||
'id': this.id, |
||||
'userCount': this.users.length, |
||||
'isLeader': this.leader == user, |
||||
'running': this.running, |
||||
'startTime': this.startTime, |
||||
'timelineName': this.timelineName, |
||||
'seekTime': this.seekTime, |
||||
'readyToParticipate': user.readyToParticipate || this.leader == user, |
||||
'speedFactor': this.speedFactor |
||||
} |
||||
} |
||||
constructor(name: number) { |
||||
this.id = name; |
||||
} |
||||
|
||||
serializeTimeline(user: User) { |
||||
return getTimeline(this.timelineName); |
||||
} |
||||
serialize(user?: User) { |
||||
const obj: SerializedRoom = { |
||||
id: this.id, |
||||
userCount: this.users.length, |
||||
isLeader: this.leader === user, |
||||
running: this.running, |
||||
startTime: this.startTime, |
||||
timelineName: this.timelineName, |
||||
seekTime: this.seekTime, |
||||
readyToParticipate: this.getLeader()?.readyToParticipate || false, |
||||
speedFactor: this.speedFactor, |
||||
ticker: this.ticker.map((i) => i.message), |
||||
}; |
||||
|
||||
sync() { |
||||
this.users.forEach(u => u.sync()); |
||||
if (typeof user === "undefined" || this.leader === user) { |
||||
obj["users"] = this.users.map((u) => u.serialize()); |
||||
} |
||||
|
||||
join(user: User) { |
||||
this.users.push(user); |
||||
user.setRoom(this); |
||||
return obj; |
||||
} |
||||
|
||||
if (!this.hasLeader()) { |
||||
this.setLeader(user); |
||||
} |
||||
serializeTimeline() { |
||||
return getTimeline(this.timelineName); |
||||
} |
||||
|
||||
if (this.autoStart) { |
||||
this.seekTime = 2500000; |
||||
this.running = true; |
||||
this.start(); |
||||
} |
||||
sync() { |
||||
this.users.forEach((u) => u.sync()); |
||||
} |
||||
|
||||
this.sync(); |
||||
async join(user: User) { |
||||
this.users.push(user); |
||||
await user.setRoom(this); |
||||
|
||||
if (!this.hasLeader()) { |
||||
this.setLeader(user); |
||||
} |
||||
|
||||
leave(user: User) { |
||||
this.users.splice(this.users.indexOf(user), 1); |
||||
user.setRoom(null); |
||||
this.sync(); |
||||
} |
||||
|
||||
if (this.leader == user) { |
||||
this.setRandomLeader(); |
||||
} |
||||
async leave(user: User) { |
||||
this.removeTickerMessageForUser(user); |
||||
this.users.splice(this.users.indexOf(user), 1); |
||||
await user.setRoom(null); |
||||
|
||||
this.sync(); |
||||
if (this.leader == user) { |
||||
this.setRandomLeader(); |
||||
} |
||||
|
||||
onBeforeDelete() { |
||||
} |
||||
this.sync(); |
||||
} |
||||
|
||||
setOptions(options: any) { |
||||
this.seekTime = Math.max(0, Math.min(options.seekTime, 250 * 60 * 1000)) |
||||
if (getTimelineNames().indexOf(options.timelineName) >= 0) { |
||||
this.timelineName = options.timelineName; |
||||
} |
||||
this.sync() |
||||
setOptions(options: { seekTime: number; timelineName: string }) { |
||||
this.seekTime = Math.max(0, Math.min(options.seekTime, 250 * 60 * 1000)); |
||||
if (getTimelineNames().indexOf(options.timelineName) >= 0) { |
||||
this.timelineName = options.timelineName; |
||||
} |
||||
this.sync(); |
||||
} |
||||
|
||||
start() { |
||||
this.running = true; |
||||
this.startTime = getCurrentTime() - this.seekTime |
||||
start() { |
||||
this.running = true; |
||||
this.startTime = getCurrentTime() - this.seekTime; |
||||
this.sync(); |
||||
} |
||||
|
||||
this.sync(); |
||||
} |
||||
/** |
||||
* |
||||
* @returns {boolean} |
||||
*/ |
||||
hasUsers() { |
||||
return this.users.length !== 0; |
||||
} |
||||
|
||||
run(io: Socket) { |
||||
this.running = true; |
||||
this.startTime = Date.now(); |
||||
|
||||
// io.to(this.id.toString()).emit('timeline', {
|
||||
// 'timeline': {
|
||||
// }
|
||||
// });
|
||||
|
||||
const doTick = () => { |
||||
if (this.users.length === 0) { |
||||
// this room is over.
|
||||
return; |
||||
} |
||||
|
||||
const timestamp = getIndex(this.timelineIndex); |
||||
const nextShot = getNextShot(this.timelineIndex); |
||||
|
||||
if (!timestamp) { |
||||
// We are done.
|
||||
io.to(this.id.toString()).emit('tick_event', { |
||||
tick: { |
||||
current: this.currentSeconds |
||||
} |
||||
}); |
||||
console.log("Done"); |
||||
this.running = false; |
||||
return; |
||||
} |
||||
|
||||
console.log("ticking", this.currentSeconds); |
||||
|
||||
io.to(this.id.toString()).emit('tick_event', { |
||||
tick: { |
||||
current: this.currentSeconds, |
||||
next: timestamp, |
||||
nextShot: nextShot |
||||
} |
||||
}); |
||||
|
||||
if (this.currentSeconds >= timestamp.timestamp) { |
||||
this.timelineIndex += 1; |
||||
} |
||||
|
||||
this.currentSeconds += 1; |
||||
// We spend some time processing, wait a bit less than 1000ms
|
||||
const nextTickTime = this.startTime + (1000 * this.currentSeconds / this.speedFactor); |
||||
const waitTime = nextTickTime - Date.now(); |
||||
console.log("waiting", waitTime); |
||||
setTimeout(doTick, Math.floor(waitTime / this.speedFactor)); |
||||
}; |
||||
|
||||
doTick(); |
||||
setRandomLeader() { |
||||
if (this.hasUsers()) { |
||||
this.leader = this.users[0]; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param io |
||||
* @param {number} time |
||||
*/ |
||||
seek(io: Socket, time: number) { |
||||
this.currentSeconds = time; |
||||
this.startTime = Date.now() - time * 1000; |
||||
this.timelineIndex = indexForTime(this.currentSeconds); |
||||
io.to(this.id.toString()).emit('seek', time); |
||||
} |
||||
hasLeader(): boolean { |
||||
return this.leader != null; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @returns {boolean} |
||||
*/ |
||||
hasUsers() { |
||||
return this.users.length !== 0; |
||||
} |
||||
setLeader(user: User) { |
||||
this.leader = user; |
||||
} |
||||
|
||||
setRandomLeader() { |
||||
if (this.hasUsers()) { |
||||
this.leader = this.users[0]; |
||||
} |
||||
} |
||||
getLeader(): User | null { |
||||
return this.leader; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param id |
||||
* @returns {User|undefined} |
||||
*/ |
||||
getUser(id: string) { |
||||
return this.users.find(u => u.id === id); |
||||
} |
||||
submitTickerMessage(user: User, message: string) { |
||||
message = message.replace("\n", ""); |
||||
|
||||
/** |
||||
* |
||||
* @param {string} id |
||||
*/ |
||||
removeUser(id: string) { |
||||
this.users = this.users.filter(u => u.id !== id); |
||||
} |
||||
this.removeTickerMessageForUser(user); |
||||
|
||||
hasLeader(): boolean { |
||||
return this.leader != null; |
||||
} |
||||
this.ticker.push({ |
||||
user: user, |
||||
message: message, |
||||
}); |
||||
|
||||
setLeader(user: User) { |
||||
this.leader = user; |
||||
} |
||||
this.sync(); |
||||
} |
||||
|
||||
getLeader(): User | null { |
||||
return this.leader; |
||||
removeTickerMessageForUser(user: User) { |
||||
let existing = -1; |
||||
for (let i = 0; i < this.ticker.length; i++) { |
||||
if (this.ticker[i].user === user) { |
||||
existing = i; |
||||
break; |
||||
} |
||||
} |
||||
}; |
||||
if (existing >= 0) { |
||||
this.ticker.splice(existing, 1); |
||||
} |
||||
} |
||||
} |
||||
|
@ -1,140 +1,163 @@ |
||||
import {Socket} from "socket.io"; |
||||
import { Socket } from "socket.io"; |
||||
|
||||
import User from './User' |
||||
import Room, {RoomOptions} from './Room' |
||||
import {getCurrentTime, randomInt} from "./util"; |
||||
import User from "./User"; |
||||
import Room, { RoomOptions } from "./Room"; |
||||
import { getCurrentTime } from "./util"; |
||||
|
||||
export default class Service { |
||||
private roomIdToRooms = new Map<number, Room>(); |
||||
private socketsToUsers = new Map<string, User>(); |
||||
private roomIdToRooms = new Map<number, Room>(); |
||||
private socketsToUsers = new Map<string, User>(); |
||||
|
||||
onSocketConnect(socket: Socket) { |
||||
let user = new User(socket); |
||||
this.socketsToUsers.set(socket.id, user); |
||||
user.sync(); |
||||
get rooms(): Room[] { |
||||
const rooms = []; |
||||
for (const [, room] of this.roomIdToRooms) { |
||||
rooms.push(room); |
||||
} |
||||
|
||||
onSocketDisconnect(socket: Socket) { |
||||
let user = this.getUser(socket); |
||||
return rooms; |
||||
} |
||||
|
||||
if (user.room != null) { |
||||
user.room.leave(user); |
||||
} |
||||
onSocketConnect(socket: Socket) { |
||||
const user = new User(socket); |
||||
this.socketsToUsers.set(socket.id, user); |
||||
user.sync(); |
||||
} |
||||
|
||||
this.deleteEmptyRooms(); |
||||
async onSocketDisconnect(socket: Socket) { |
||||
const user = this.getUser(socket); |
||||
|
||||
user.onDisconnect(); |
||||
if (user.room != null) { |
||||
await user.room.leave(user); |
||||
} |
||||
|
||||
onTimeSync(socket: Socket, requestId: number, clientTime: number) { |
||||
let user = this.getUser(socket); |
||||
this.deleteEmptyRooms(); |
||||
} |
||||
|
||||
let now = getCurrentTime(); |
||||
user.emit('time_sync', { |
||||
'requestId': requestId, |
||||
'clientDiff': now - clientTime, |
||||
'serverTime': now |
||||
}) |
||||
} |
||||
|
||||
onSetRoomOptions(socket: Socket, options: RoomOptions) { |
||||
let user = this.getUser(socket); |
||||
onTimeSync(socket: Socket, requestId: number, clientTime: number) { |
||||
const user = this.getUser(socket); |
||||
|
||||
if (user.room?.getLeader() == user) { |
||||
user.room!!.setOptions(options) |
||||
} |
||||
} |
||||
const now = getCurrentTime(); |
||||
user.emit("time_sync", { |
||||
requestId: requestId, |
||||
clientDiff: now - clientTime, |
||||
serverTime: now, |
||||
}); |
||||
} |
||||
|
||||
onRequestStart(socket: Socket) { |
||||
let user = this.getUser(socket); |
||||
onSetRoomOptions(socket: Socket, options: RoomOptions) { |
||||
const user = this.getUser(socket); |
||||
|
||||
if (user.room?.getLeader() == user) { |
||||
user.room!!.start(); |
||||
} |
||||
if (user.room?.getLeader() == user) { |
||||
user.room.setOptions(options); |
||||
} |
||||
} |
||||
|
||||
onRequestJoin(socket: Socket, roomId: number): boolean { |
||||
let user = this.getUser(socket); |
||||
if (user.room && user.room.id == roomId) return false; |
||||
onRequestStart(socket: Socket) { |
||||
const user = this.getUser(socket); |
||||
|
||||
if (user.room) { |
||||
user.room.leave(user); |
||||
this.deleteEmptyRooms(); |
||||
} |
||||
if (user.room?.getLeader() === user) { |
||||
user.room.start(); |
||||
user.room.sync(); |
||||
} |
||||
} |
||||
|
||||
if (!this.roomIdToRooms.has(roomId)) { |
||||
this.createRoomWithId(roomId); |
||||
} |
||||
async onRequestJoin(socket: Socket, roomId: number) { |
||||
const user = this.getUser(socket); |
||||
if (user.room && user.room.id == roomId) return false; |
||||
|
||||
let room = this.roomIdToRooms.get(roomId)!!; |
||||
room.join(user); |
||||
if (user.room) { |
||||
await user.room.leave(user); |
||||
this.deleteEmptyRooms(); |
||||
} |
||||
|
||||
return true; |
||||
if (!this.roomIdToRooms.has(roomId)) { |
||||
this.createRoomWithId(roomId); |
||||
} |
||||
|
||||
onRequestReady(socket: Socket) { |
||||
let user = this.getUser(socket); |
||||
if (!user.room || user.readyToParticipate) return; |
||||
user.readyToParticipate = true; |
||||
user.sync(); |
||||
const room = this.roomIdToRooms.get(roomId); |
||||
if (!room) { |
||||
return false; |
||||
} |
||||
|
||||
onRequestJoinRandom(socket: Socket) { |
||||
let user = this.getUser(socket); |
||||
await room.join(user); |
||||
|
||||
if (user.room) { |
||||
user.room.leave(user); |
||||
this.deleteEmptyRooms(); |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
const room = this.createRandomRoom(); |
||||
if (!room) throw Error('Too many rooms active'); |
||||
room.join(user); |
||||
} |
||||
onRequestSetReady(socket: Socket) { |
||||
const user = this.getUser(socket); |
||||
if (!user.room || user.readyToParticipate) return; |
||||
user.readyToParticipate = true; |
||||
user.room.sync(); |
||||
} |
||||
|
||||
hasRoomId(roomId: number): boolean { |
||||
return this.roomIdToRooms.has(roomId); |
||||
} |
||||
async onRequestJoinRandom(socket: Socket) { |
||||
const user = this.getUser(socket); |
||||
|
||||
private getUser(socket: Socket): User { |
||||
let user = this.socketsToUsers.get(socket.id); |
||||
if (!user) { |
||||
throw new Error('User not found'); |
||||
} |
||||
return user; |
||||
if (user.room) { |
||||
await user.room.leave(user); |
||||
this.deleteEmptyRooms(); |
||||
} |
||||
|
||||
private deleteEmptyRooms() { |
||||
for (let room of this.roomIdToRooms.values()) { |
||||
if (room.users.length == 0) { |
||||
this.deleteRoom(room); |
||||
} |
||||
} |
||||
} |
||||
const room = this.createRandomRoom(); |
||||
if (!room) throw Error("Too many rooms active"); |
||||
await room.join(user); |
||||
} |
||||
|
||||
hasRoomId(roomId: number): boolean { |
||||
return this.roomIdToRooms.has(roomId); |
||||
} |
||||
|
||||
private createRandomRoom(): Room | null { |
||||
let tries = 0; |
||||
while (tries++ < 1000) { |
||||
const randomId = randomInt(100, Math.max(1000, this.roomIdToRooms.size * 2)); |
||||
if (this.roomIdToRooms.has(randomId)) continue; |
||||
submitTickerMessage(socket: Socket, message: string) { |
||||
const user = this.getUser(socket); |
||||
|
||||
return this.createRoomWithId(randomId); |
||||
} |
||||
return null; |
||||
if (!user.room) { |
||||
throw new Error("User has no room"); |
||||
} |
||||
|
||||
private createRoomWithId(roomId: number): Room { |
||||
if (this.roomIdToRooms.has(roomId)) { |
||||
throw new Error('A room with the given id already exists'); |
||||
} |
||||
user.room.submitTickerMessage(user, message); |
||||
} |
||||
|
||||
let room = new Room(roomId); |
||||
this.roomIdToRooms.set(roomId, room); |
||||
return room; |
||||
private getUser(socket: Socket): User { |
||||
const user = this.socketsToUsers.get(socket.id); |
||||
if (!user) { |
||||
throw new Error("User not found"); |
||||
} |
||||
return user; |
||||
} |
||||
|
||||
private deleteEmptyRooms() { |
||||
for (const room of this.roomIdToRooms.values()) { |
||||
if (room.users.length == 0) { |
||||
this.deleteRoom(room); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private createRandomRoom(): Room | null { |
||||
let tries = 0; |
||||
let i = 1; |
||||
while (tries++ < 1000) { |
||||
if (this.roomIdToRooms.has(i)) { |
||||
i++; |
||||
} else { |
||||
return this.createRoomWithId(i); |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
private deleteRoom(room: Room) { |
||||
this.roomIdToRooms.get(room.id)!!.onBeforeDelete(); |
||||
this.roomIdToRooms.delete(room.id) |
||||
private createRoomWithId(roomId: number): Room { |
||||
if (this.roomIdToRooms.has(roomId)) { |
||||
throw new Error("A room with the given id already exists"); |
||||
} |
||||
|
||||
const room = new Room(roomId); |
||||
this.roomIdToRooms.set(roomId, room); |
||||
return room; |
||||
} |
||||
|
||||
private deleteRoom(room: Room) { |
||||
this.roomIdToRooms.delete(room.id); |
||||
} |
||||
} |
||||
|
@ -1,108 +1,131 @@ |
||||
import {Socket} from "socket.io"; |
||||
import { Socket } from "socket.io"; |
||||
|
||||
import Room from "./Room"; |
||||
import {getTimelineNames} from "./timeline"; |
||||
import Room, { SerializedRoom } from "./Room"; |
||||
import { getTimelineNames } from "./timeline"; |
||||
|
||||
export default class User { |
||||
socket: Socket; |
||||
id: string; |
||||
|
||||
room: Room | null = null; |
||||
readyToParticipate: boolean = false; |
||||
export interface Config { |
||||
availableTimelines: string[]; |
||||
} |
||||
|
||||
constructor(socket: Socket) { |
||||
this.socket = socket; |
||||
this.id = socket.id; |
||||
export default class User { |
||||
socket: Socket; |
||||
id: string; |
||||
|
||||
room: Room | null = null; |
||||
readyToParticipate = false; |
||||
|
||||
constructor(socket: Socket) { |
||||
this.socket = socket; |
||||
this.id = socket.id; |
||||
} |
||||
|
||||
serialize() { |
||||
return { |
||||
id: this.id, |
||||
readyToParticipate: this.readyToParticipate, |
||||
}; |
||||
} |
||||
|
||||
async setRoom(room: Room | null) { |
||||
if (this.room === room) return; |
||||
|
||||
if (this.room !== null) { |
||||
await this.socket.leave(this.room.id.toString()); |
||||
this.readyToParticipate = false; |
||||
} |
||||
|
||||
onDisconnect() { |
||||
if (this.room != null) { |
||||
this.room = room; |
||||
|
||||
} |
||||
if (this.room !== null) { |
||||
await this.socket.join(this.room.id.toString()); |
||||
} |
||||
|
||||
setRoom(room: Room | null) { |
||||
if (this.room === room) return; |
||||
this.sync(); |
||||
} |
||||
|
||||
getConfig() { |
||||
return { |
||||
availableTimelines: getTimelineNames(), |
||||
}; |
||||
} |
||||
|
||||
sentConfig: Config | null = null; |
||||
sentRoom: SerializedRoom | null = null; |
||||
sentTimelineName: string | null = null; |
||||
|
||||
sync() { |
||||
// Config
|
||||
const config = this.getConfig(); |
||||
if (!this.syncEquals(this.sentConfig, config)) { |
||||
this.sentConfig = Object.assign({}, config); |
||||
this.emit("config", { |
||||
config: this.sentConfig, |
||||
}); |
||||
} |
||||
|
||||
if (this.room != null) { |
||||
this.socket.leave(this.room.id.toString()); |
||||
this.readyToParticipate = false; |
||||
} |
||||
// Room
|
||||
if (!this.syncEquals(this.sentRoom, this.room?.serialize(this))) { |
||||
this.sentRoom = this.room |
||||
? (JSON.parse( |
||||
JSON.stringify(this.room.serialize(this)) |
||||
) as SerializedRoom) |
||||
: null; |
||||
|
||||
this.emit("room", { |
||||
room: this.sentRoom, |
||||
}); |
||||
} |
||||
|
||||
this.room = room; |
||||
// Timeline
|
||||
if (!this.syncEquals(this.sentTimelineName, this.room?.timelineName)) { |
||||
this.sentTimelineName = this.room?.timelineName || null; |
||||
this.emit("timeline", { |
||||
timeline: |
||||
this.sentTimelineName == null ? null : this.room?.serializeTimeline(), |
||||
}); |
||||
} |
||||
} |
||||
|
||||
if (this.room != null) { |
||||
this.socket.join(this.room.id.toString()); |
||||
} |
||||
emit(eventName: string, obj: unknown) { |
||||
this.socket.emit(eventName, obj); |
||||
} |
||||
|
||||
this.sync(); |
||||
syncEquals(obj1: unknown, obj2: unknown): boolean { |
||||
if (typeof obj1 !== typeof obj2) { |
||||
return false; |
||||
} |
||||
|
||||
getConfig() { |
||||
return { |
||||
'availableTimelines': getTimelineNames() |
||||
} |
||||
if (typeof obj1 !== "object") { |
||||
// Both are not 'object'
|
||||
return Object.is(obj1, obj2); |
||||
} |
||||
|
||||
sentConfig: any = null; |
||||
sentRoom: any = null; |
||||
sentTimelineName: string | null = null; |
||||
|
||||
sync() { |
||||
// Config
|
||||
let config = this.getConfig(); |
||||
if (!this.syncEquals(this.sentConfig, config)) { |
||||
this.sentConfig = config; |
||||
this.emit('config', { |
||||
'config': this.sentConfig |
||||
}); |
||||
} |
||||
|
||||
// Room
|
||||
if (!this.syncEquals(this.sentRoom, this.room?.serialize(this))) { |
||||
this.sentRoom = this.room?.serialize(this); |
||||
this.emit('room', { |
||||
'room': this.sentRoom |
||||
}) |
||||
} |
||||
|
||||
// Timeline
|
||||
if (!this.syncEquals(this.sentTimelineName, this.room?.timelineName)) { |
||||
this.sentTimelineName = this.room?.timelineName || null; |
||||
this.emit('timeline', { |
||||
'timeline': this.sentTimelineName == null ? null : this.room!!.serializeTimeline(this) |
||||
}) |
||||
} |
||||
if (obj1 === null && obj2 === null) { |
||||
return true; |
||||
} |
||||
|
||||
emit(eventName: string, obj: any) { |
||||
this.socket.emit(eventName, obj); |
||||
if (obj1 === null || obj2 === null) { |
||||
return false; |
||||
} |
||||
|
||||
syncEquals(obj1: any, obj2: any): boolean { |
||||
if (obj1 === undefined && obj2 === undefined) |
||||
return true; |
||||
|
||||
if ((obj1 === undefined && obj2 !== undefined) || (obj1 !== undefined && obj2 === undefined)) |
||||
return false; |
||||
|
||||
if (obj1 === null && obj2 === null) |
||||
return true; |
||||
|
||||
if ((obj1 === null && obj2 !== null) || (obj1 !== null && obj2 === null)) |
||||
return false; |
||||
|
||||
if (typeof (obj1) !== typeof (obj2)) |
||||
return false; |
||||
|
||||
if (typeof (obj1) === 'string' || typeof (obj1) === 'number' || typeof (obj1) === 'boolean') { |
||||
return obj1 === obj2; |
||||
} |
||||
|
||||
if (Object.keys(obj1).length !== Object.keys(obj2).length) return false |
||||
if (typeof obj2 !== "object") { |
||||
// This can not happen ;)
|
||||
throw new TypeError("Obj2 is not object while obj1 is."); |
||||
} |
||||
|
||||
return Object.keys(obj1).every(key => |
||||
obj2.hasOwnProperty(key) && this.syncEquals(obj1[key], obj2[key]) |
||||
); |
||||
if (Object.keys(obj1).length !== Object.keys(obj2).length) { |
||||
return false; |
||||
} |
||||
|
||||
return Object.keys(obj1).every((key: string) => { |
||||
if (!(key in obj1) || !(key in obj2)) { |
||||
return false; |
||||
} |
||||
|
||||
return this.syncEquals( |
||||
obj1[key as keyof object], |
||||
obj2[key as keyof object] |
||||
); |
||||
}); |
||||
} |
||||
} |
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,62 +1,11 @@ |
||||
// @ts-ignore
|
||||
import timeline from '../data/timelines.js'; |
||||
|
||||
import timeline from "./data/timelines"; |
||||
|
||||
export function getTimelineNames(): string[] { |
||||
return timeline.timelines.map((i: any) => i.name) |
||||
return timeline.timelines.map((timeline) => timeline.name); |
||||
} |
||||
|
||||
export function getTimeline(name: string) { |
||||
let t = timeline.timelines.find((i: any) => i.name == name); |
||||
if (!t) return null; |
||||
return t; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param i |
||||
* @returns {*} |
||||
*/ |
||||
export function getIndex(i: number): any { |
||||
if (i >= timeline.length) { |
||||
return; |
||||
} |
||||
|
||||
return timeline[i]; |
||||
} |
||||
|
||||
/** |
||||
* @param {number} i - the index. |
||||
* @returns {{count: number, timestamp: number}|undefined} |
||||
*/ |
||||
export function getNextShot(i: number) { |
||||
for (; i < timeline.length; i++) { |
||||
const time = getIndex(i); |
||||
|
||||
for (let event of time.events) { |
||||
if (event.type === 'shot') { |
||||
return { |
||||
timestamp: time.timestamp, |
||||
count: event.shotCount |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return undefined; |
||||
} |
||||
|
||||
export function indexForTime(seconds: number): number { |
||||
let lastIndex = 0; |
||||
for (let i = 0; i < timeline.length; i++) { |
||||
const time = timeline[i]; |
||||
|
||||
if (time.timestamp >= seconds) { |
||||
return lastIndex; |
||||
} |
||||
|
||||
lastIndex = i; |
||||
} |
||||
|
||||
return -1; |
||||
const t = timeline.timelines.find((t) => t.name == name); |
||||
if (!t) return null; |
||||
return t; |
||||
} |
||||
|
@ -1,8 +1,12 @@ |
||||
{ |
||||
"compilerOptions": { |
||||
"outDir": "build", |
||||
"target": "es6", |
||||
"module": "commonjs", |
||||
"strict": true, |
||||
"esModuleInterop": true |
||||
} |
||||
}, |
||||
"include": [ |
||||
"src" |
||||
] |
||||
} |
||||
|
@ -1,739 +0,0 @@ |
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. |
||||
# yarn lockfile v1 |
||||
|
||||
|
||||
"@types/body-parser@*": |
||||
version "1.19.0" |
||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" |
||||
integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== |
||||
dependencies: |
||||
"@types/connect" "*" |
||||
"@types/node" "*" |
||||
|
||||
"@types/connect@*": |
||||
version "3.4.33" |
||||
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" |
||||
integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== |
||||
dependencies: |
||||
"@types/node" "*" |
||||
|
||||
"@types/express-serve-static-core@*": |
||||
version "4.17.5" |
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.5.tgz#a00ac7dadd746ae82477443e4d480a6a93ea083c" |
||||
integrity sha512-578YH5Lt88AKoADy0b2jQGwJtrBxezXtVe/MBqWXKZpqx91SnC0pVkVCcxcytz3lWW+cHBYDi3Ysh0WXc+rAYw== |
||||
dependencies: |
||||
"@types/node" "*" |
||||
"@types/range-parser" "*" |
||||
|
||||
"@types/express@^4.17.6": |
||||
version "4.17.6" |
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.6.tgz#6bce49e49570507b86ea1b07b806f04697fac45e" |
||||
integrity sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w== |
||||
dependencies: |
||||
"@types/body-parser" "*" |
||||
"@types/express-serve-static-core" "*" |
||||
"@types/qs" "*" |
||||
"@types/serve-static" "*" |
||||
|
||||
"@types/mime@*": |
||||
version "2.0.1" |
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" |
||||
integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== |
||||
|
||||
"@types/node@*": |
||||
version "13.9.2" |
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.2.tgz#ace1880c03594cc3e80206d96847157d8e7fa349" |
||||
integrity sha512-bnoqK579sAYrQbp73wwglccjJ4sfRdKU7WNEZ5FW4K2U6Kc0/eZ5kvXG0JKsEKFB50zrFmfFt52/cvBbZa7eXg== |
||||
|
||||
"@types/qs@*": |
||||
version "6.9.1" |
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.1.tgz#937fab3194766256ee09fcd40b781740758617e7" |
||||
integrity sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw== |
||||
|
||||
"@types/range-parser@*": |
||||
version "1.2.3" |
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" |
||||
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== |
||||
|
||||
"@types/serve-static@*": |
||||
version "1.13.3" |
||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" |
||||
integrity sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g== |
||||
dependencies: |
||||
"@types/express-serve-static-core" "*" |
||||
"@types/mime" "*" |
||||
|
||||
"@types/socket.io@^2.1.4": |
||||
version "2.1.4" |
||||
resolved "https://registry.yarnpkg.com/@types/socket.io/-/socket.io-2.1.4.tgz#674e7bc193c5ccdadd4433f79f3660d31759e9ac" |
||||
integrity sha512-cI98INy7tYnweTsUlp8ocveVdAxENUThO0JsLSCs51cjOP2yV5Mqo5QszMDPckyRRA+PO6+wBgKvGvHUCc23TQ== |
||||
dependencies: |
||||
"@types/node" "*" |
||||
|
||||
accepts@~1.3.4, accepts@~1.3.7: |
||||
version "1.3.7" |
||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" |
||||
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== |
||||
dependencies: |
||||
mime-types "~2.1.24" |
||||
negotiator "0.6.2" |
||||
|
||||
after@0.8.2: |
||||
version "0.8.2" |
||||
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" |
||||
integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= |
||||
|
||||
arg@^4.1.0: |
||||
version "4.1.3" |
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" |
||||
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== |
||||
|
||||
array-flatten@1.1.1: |
||||
version "1.1.1" |
||||
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" |
||||
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= |
||||
|
||||
arraybuffer.slice@~0.0.7: |
||||
version "0.0.7" |
||||
resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" |
||||
integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== |
||||
|
||||
async-limiter@~1.0.0: |
||||
version "1.0.1" |
||||
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" |
||||
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== |
||||
|
||||
backo2@1.0.2: |
||||
version "1.0.2" |
||||
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" |
||||
integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= |
||||
|
||||
base64-arraybuffer@0.1.5: |
||||
version "0.1.5" |
||||
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" |
||||
integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= |
||||
|
||||
base64id@2.0.0: |
||||
version "2.0.0" |
||||
resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" |
||||
integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== |
||||
|
||||
better-assert@~1.0.0: |
||||
version "1.0.2" |
||||
resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" |
||||
integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI= |
||||
dependencies: |
||||
callsite "1.0.0" |
||||
|
||||
blob@0.0.5: |
||||
version "0.0.5" |
||||
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" |
||||
integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== |
||||
|
||||
body-parser@1.19.0: |
||||
version "1.19.0" |
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" |
||||
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== |
||||
dependencies: |
||||
bytes "3.1.0" |
||||
content-type "~1.0.4" |
||||
debug "2.6.9" |
||||
depd "~1.1.2" |
||||
http-errors "1.7.2" |
||||
iconv-lite "0.4.24" |
||||
on-finished "~2.3.0" |
||||
qs "6.7.0" |
||||
raw-body "2.4.0" |
||||
type-is "~1.6.17" |
||||
|
||||
buffer-from@^1.0.0: |
||||
version "1.1.1" |
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" |
||||
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== |
||||
|
||||
bytes@3.1.0: |
||||
version "3.1.0" |
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" |
||||
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== |
||||
|
||||
callsite@1.0.0: |
||||
version "1.0.0" |
||||
resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" |
||||
integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= |
||||
|
||||
component-bind@1.0.0: |
||||
version "1.0.0" |
||||
resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" |
||||
integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= |
||||
|
||||
component-emitter@1.2.1: |
||||
version "1.2.1" |
||||
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" |
||||
integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= |
||||
|
||||
component-inherit@0.0.3: |
||||
version "0.0.3" |
||||
resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" |
||||
integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= |
||||
|
||||
content-disposition@0.5.3: |
||||
version "0.5.3" |
||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" |
||||
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== |
||||
dependencies: |
||||
safe-buffer "5.1.2" |
||||
|
||||
content-type@~1.0.4: |
||||
version "1.0.4" |
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" |
||||
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== |
||||
|
||||
cookie-signature@1.0.6: |
||||
version "1.0.6" |
||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" |
||||
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= |
||||
|
||||
cookie@0.3.1: |
||||
version "0.3.1" |
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" |
||||
integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= |
||||
|
||||
cookie@0.4.0: |
||||
version "0.4.0" |
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" |
||||
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== |
||||
|
||||
debug@2.6.9: |
||||
version "2.6.9" |
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" |
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== |
||||
dependencies: |
||||
ms "2.0.0" |
||||
|
||||
debug@~3.1.0: |
||||
version "3.1.0" |
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" |
||||
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== |
||||
dependencies: |
||||
ms "2.0.0" |
||||
|
||||
debug@~4.1.0: |
||||
version "4.1.1" |
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" |
||||
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== |
||||
dependencies: |
||||
ms "^2.1.1" |
||||
|
||||
depd@~1.1.2: |
||||
version "1.1.2" |
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" |
||||
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= |
||||
|
||||
destroy@~1.0.4: |
||||
version "1.0.4" |
||||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" |
||||
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= |
||||
|
||||
diff@^4.0.1: |
||||
version "4.0.2" |
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" |
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== |
||||
|
||||
ee-first@1.1.1: |
||||
version "1.1.1" |
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" |
||||
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= |
||||
|
||||
encodeurl@~1.0.2: |
||||
version "1.0.2" |
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" |
||||
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= |
||||
|
||||
engine.io-client@~3.4.0: |
||||
version "3.4.0" |
||||
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.0.tgz#82a642b42862a9b3f7a188f41776b2deab643700" |
||||
integrity sha512-a4J5QO2k99CM2a0b12IznnyQndoEvtA4UAldhGzKqnHf42I3Qs2W5SPnDvatZRcMaNZs4IevVicBPayxYt6FwA== |
||||
dependencies: |
||||
component-emitter "1.2.1" |
||||
component-inherit "0.0.3" |
||||
debug "~4.1.0" |
||||
engine.io-parser "~2.2.0" |
||||
has-cors "1.1.0" |
||||
indexof "0.0.1" |
||||
parseqs "0.0.5" |
||||
parseuri "0.0.5" |
||||
ws "~6.1.0" |
||||
xmlhttprequest-ssl "~1.5.4" |
||||
yeast "0.1.2" |
||||
|
||||
engine.io-parser@~2.2.0: |
||||
version "2.2.0" |
||||
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed" |
||||
integrity sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w== |
||||
dependencies: |
||||
after "0.8.2" |
||||
arraybuffer.slice "~0.0.7" |
||||
base64-arraybuffer "0.1.5" |
||||
blob "0.0.5" |
||||
has-binary2 "~1.0.2" |
||||
|
||||
engine.io@~3.4.0: |
||||
version "3.4.0" |
||||
resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.0.tgz#3a962cc4535928c252759a00f98519cb46c53ff3" |
||||
integrity sha512-XCyYVWzcHnK5cMz7G4VTu2W7zJS7SM1QkcelghyIk/FmobWBtXE7fwhBusEKvCSqc3bMh8fNFMlUkCKTFRxH2w== |
||||
dependencies: |
||||
accepts "~1.3.4" |
||||
base64id "2.0.0" |
||||
cookie "0.3.1" |
||||
debug "~4.1.0" |
||||
engine.io-parser "~2.2.0" |
||||
ws "^7.1.2" |
||||
|
||||
escape-html@~1.0.3: |
||||
version "1.0.3" |
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" |
||||
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= |
||||
|
||||
etag@~1.8.1: |
||||
version "1.8.1" |
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" |
||||
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= |
||||
|
||||
express@^4.17.1: |
||||
version "4.17.1" |
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" |
||||
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== |
||||
dependencies: |
||||
accepts "~1.3.7" |
||||
array-flatten "1.1.1" |
||||
body-parser "1.19.0" |
||||
content-disposition "0.5.3" |
||||
content-type "~1.0.4" |
||||
cookie "0.4.0" |
||||
cookie-signature "1.0.6" |
||||
debug "2.6.9" |
||||
depd "~1.1.2" |
||||
encodeurl "~1.0.2" |
||||
escape-html "~1.0.3" |
||||
etag "~1.8.1" |
||||
finalhandler "~1.1.2" |
||||
fresh "0.5.2" |
||||
merge-descriptors "1.0.1" |
||||
methods "~1.1.2" |
||||
on-finished "~2.3.0" |
||||
parseurl "~1.3.3" |
||||
path-to-regexp "0.1.7" |
||||
proxy-addr "~2.0.5" |
||||
qs "6.7.0" |
||||
range-parser "~1.2.1" |
||||
safe-buffer "5.1.2" |
||||
send "0.17.1" |
||||
serve-static "1.14.1" |
||||
setprototypeof "1.1.1" |
||||
statuses "~1.5.0" |
||||
type-is "~1.6.18" |
||||
utils-merge "1.0.1" |
||||
vary "~1.1.2" |
||||
|
||||
finalhandler@~1.1.2: |
||||
version "1.1.2" |
||||
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" |
||||
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== |
||||
dependencies: |
||||
debug "2.6.9" |
||||
encodeurl "~1.0.2" |
||||
escape-html "~1.0.3" |
||||
on-finished "~2.3.0" |
||||
parseurl "~1.3.3" |
||||
statuses "~1.5.0" |
||||
unpipe "~1.0.0" |
||||
|
||||
forwarded@~0.1.2: |
||||
version "0.1.2" |
||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" |
||||
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= |
||||
|
||||
fresh@0.5.2: |
||||
version "0.5.2" |
||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" |
||||
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= |
||||
|
||||
has-binary2@~1.0.2: |
||||
version "1.0.3" |
||||
resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" |
||||
integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== |
||||
dependencies: |
||||
isarray "2.0.1" |
||||
|
||||
has-cors@1.1.0: |
||||
version "1.1.0" |
||||
resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" |
||||
integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= |
||||
|
||||
http-errors@1.7.2: |
||||
version "1.7.2" |
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" |
||||
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== |
||||
dependencies: |
||||
depd "~1.1.2" |
||||
inherits "2.0.3" |
||||
setprototypeof "1.1.1" |
||||
statuses ">= 1.5.0 < 2" |
||||
toidentifier "1.0.0" |
||||
|
||||
http-errors@~1.7.2: |
||||
version "1.7.3" |
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" |
||||
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== |
||||
dependencies: |
||||
depd "~1.1.2" |
||||
inherits "2.0.4" |
||||
setprototypeof "1.1.1" |
||||
statuses ">= 1.5.0 < 2" |
||||
toidentifier "1.0.0" |
||||
|
||||
iconv-lite@0.4.24: |
||||
version "0.4.24" |
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" |
||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== |
||||
dependencies: |
||||
safer-buffer ">= 2.1.2 < 3" |
||||
|
||||
indexof@0.0.1: |
||||
version "0.0.1" |
||||
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" |
||||
integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= |
||||
|
||||
inherits@2.0.3: |
||||
version "2.0.3" |
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" |
||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= |
||||
|
||||
inherits@2.0.4: |
||||
version "2.0.4" |
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" |
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== |
||||
|
||||
ipaddr.js@1.9.1: |
||||
version "1.9.1" |
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" |
||||
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== |
||||
|
||||
isarray@2.0.1: |
||||
version "2.0.1" |
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" |
||||
integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= |
||||
|
||||
make-error@^1.1.1: |
||||
version "1.3.6" |
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" |
||||
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== |
||||
|
||||
media-typer@0.3.0: |
||||
version "0.3.0" |
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" |
||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= |
||||
|
||||
merge-descriptors@1.0.1: |
||||
version "1.0.1" |
||||
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" |
||||
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= |
||||
|
||||
methods@~1.1.2: |
||||
version "1.1.2" |
||||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" |
||||
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= |
||||
|
||||
mime-db@1.43.0: |
||||
version "1.43.0" |
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" |
||||
integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== |
||||
|
||||
mime-types@~2.1.24: |
||||
version "2.1.26" |
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" |
||||
integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== |
||||
dependencies: |
||||
mime-db "1.43.0" |
||||
|
||||
mime@1.6.0: |
||||
version "1.6.0" |
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" |
||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== |
||||
|
||||
ms@2.0.0: |
||||
version "2.0.0" |
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" |
||||
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= |
||||
|
||||
ms@2.1.1: |
||||
version "2.1.1" |
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" |
||||
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== |
||||
|
||||
ms@^2.1.1: |
||||
version "2.1.2" |
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" |
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== |
||||
|
||||
negotiator@0.6.2: |
||||
version "0.6.2" |
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" |
||||
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== |
||||
|
||||
object-component@0.0.3: |
||||
version "0.0.3" |
||||
resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" |
||||
integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE= |
||||
|
||||
on-finished@~2.3.0: |
||||
version "2.3.0" |
||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" |
||||
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= |
||||
dependencies: |
||||
ee-first "1.1.1" |
||||
|
||||
parseqs@0.0.5: |
||||
version "0.0.5" |
||||
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" |
||||
integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0= |
||||
dependencies: |
||||
better-assert "~1.0.0" |
||||
|
||||
parseuri@0.0.5: |
||||
version "0.0.5" |
||||
resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" |
||||
integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo= |
||||
dependencies: |
||||
better-assert "~1.0.0" |
||||
|
||||
parseurl@~1.3.3: |
||||
version "1.3.3" |
||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" |
||||
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== |
||||
|
||||
path-to-regexp@0.1.7: |
||||
version "0.1.7" |
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" |
||||
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= |
||||
|
||||
proxy-addr@~2.0.5: |
||||
version "2.0.6" |
||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" |
||||
integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== |
||||
dependencies: |
||||
forwarded "~0.1.2" |
||||
ipaddr.js "1.9.1" |
||||
|
||||
qs@6.7.0: |
||||
version "6.7.0" |
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" |
||||
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== |
||||
|
||||
range-parser@~1.2.1: |
||||
version "1.2.1" |
||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" |
||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== |
||||
|
||||
raw-body@2.4.0: |
||||
version "2.4.0" |
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" |
||||
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== |
||||
dependencies: |
||||
bytes "3.1.0" |
||||
http-errors "1.7.2" |
||||
iconv-lite "0.4.24" |
||||
unpipe "1.0.0" |
||||
|
||||
safe-buffer@5.1.2: |
||||
version "5.1.2" |
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" |
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== |
||||
|
||||
"safer-buffer@>= 2.1.2 < 3": |
||||
version "2.1.2" |
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" |
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== |
||||
|
||||
send@0.17.1: |
||||
version "0.17.1" |
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" |
||||
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== |
||||
dependencies: |
||||
debug "2.6.9" |
||||
depd "~1.1.2" |
||||
destroy "~1.0.4" |
||||
encodeurl "~1.0.2" |
||||
escape-html "~1.0.3" |
||||
etag "~1.8.1" |
||||
fresh "0.5.2" |
||||
http-errors "~1.7.2" |
||||
mime "1.6.0" |
||||
ms "2.1.1" |
||||
on-finished "~2.3.0" |
||||
range-parser "~1.2.1" |
||||
statuses "~1.5.0" |
||||
|
||||
serve-static@1.14.1: |
||||
version "1.14.1" |
||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" |
||||
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== |
||||
dependencies: |
||||
encodeurl "~1.0.2" |
||||
escape-html "~1.0.3" |
||||
parseurl "~1.3.3" |
||||
send "0.17.1" |
||||
|
||||
setprototypeof@1.1.1: |
||||
version "1.1.1" |
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" |
||||
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== |
||||
|
||||
socket.io-adapter@~1.1.0: |
||||
version "1.1.2" |
||||
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz#ab3f0d6f66b8fc7fca3959ab5991f82221789be9" |
||||
integrity sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g== |
||||
|
||||
socket.io-client@2.3.0: |
||||
version "2.3.0" |
||||
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4" |
||||
integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA== |
||||
dependencies: |
||||
backo2 "1.0.2" |
||||
base64-arraybuffer "0.1.5" |
||||
component-bind "1.0.0" |
||||
component-emitter "1.2.1" |
||||
debug "~4.1.0" |
||||
engine.io-client "~3.4.0" |
||||
has-binary2 "~1.0.2" |
||||
has-cors "1.1.0" |
||||
indexof "0.0.1" |
||||
object-component "0.0.3" |
||||
parseqs "0.0.5" |
||||
parseuri "0.0.5" |
||||
socket.io-parser "~3.3.0" |
||||
to-array "0.1.4" |
||||
|
||||
socket.io-parser@~3.3.0: |
||||
version "3.3.0" |
||||
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f" |
||||
integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng== |
||||
dependencies: |
||||
component-emitter "1.2.1" |
||||
debug "~3.1.0" |
||||
isarray "2.0.1" |
||||
|
||||
socket.io-parser@~3.4.0: |
||||
version "3.4.0" |
||||
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.0.tgz#370bb4a151df2f77ce3345ff55a7072cc6e9565a" |
||||
integrity sha512-/G/VOI+3DBp0+DJKW4KesGnQkQPFmUCbA/oO2QGT6CWxU7hLGWqU3tyuzeSK/dqcyeHsQg1vTe9jiZI8GU9SCQ== |
||||
dependencies: |
||||
component-emitter "1.2.1" |
||||
debug "~4.1.0" |
||||
isarray "2.0.1" |
||||
|
||||
socket.io@^2.3.0: |
||||
version "2.3.0" |
||||
resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb" |
||||
integrity sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg== |
||||
dependencies: |
||||
debug "~4.1.0" |
||||
engine.io "~3.4.0" |
||||
has-binary2 "~1.0.2" |
||||
socket.io-adapter "~1.1.0" |
||||
socket.io-client "2.3.0" |
||||
socket.io-parser "~3.4.0" |
||||
|
||||
source-map-support@^0.5.17: |
||||
version "0.5.19" |
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" |
||||
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== |
||||
dependencies: |
||||
buffer-from "^1.0.0" |
||||
source-map "^0.6.0" |
||||
|
||||
source-map@^0.6.0: |
||||
version "0.6.1" |
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" |
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== |
||||
|
||||
"statuses@>= 1.5.0 < 2", statuses@~1.5.0: |
||||
version "1.5.0" |
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" |
||||
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= |
||||
|
||||
to-array@0.1.4: |
||||
version "0.1.4" |
||||
resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" |
||||
integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= |
||||
|
||||
toidentifier@1.0.0: |
||||
version "1.0.0" |
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" |
||||
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== |
||||
|
||||
ts-node@^8.8.2: |
||||
version "8.9.0" |
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.9.0.tgz#d7bf7272dcbecd3a2aa18bd0b96c7d2f270c15d4" |
||||
integrity sha512-rwkXfOs9zmoHrV8xE++dmNd6ZIS+nmHHCxcV53ekGJrxFLMbp+pizpPS07ARvhwneCIECPppOwbZHvw9sQtU4w== |
||||
dependencies: |
||||
arg "^4.1.0" |
||||
diff "^4.0.1" |
||||
make-error "^1.1.1" |
||||
source-map-support "^0.5.17" |
||||
yn "3.1.1" |
||||
|
||||
type-is@~1.6.17, type-is@~1.6.18: |
||||
version "1.6.18" |
||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" |
||||
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== |
||||
dependencies: |
||||
media-typer "0.3.0" |
||||
mime-types "~2.1.24" |
||||
|
||||
typescript@^3.8.3: |
||||
version "3.8.3" |
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" |
||||
integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== |
||||
|
||||
unpipe@1.0.0, unpipe@~1.0.0: |
||||
version "1.0.0" |
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" |
||||
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= |
||||
|
||||
utils-merge@1.0.1: |
||||
version "1.0.1" |
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" |
||||
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= |
||||
|
||||
vary@~1.1.2: |
||||
version "1.1.2" |
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" |
||||
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= |
||||
|
||||
ws@^7.1.2: |
||||
version "7.2.3" |
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.3.tgz#a5411e1fb04d5ed0efee76d26d5c46d830c39b46" |
||||
integrity sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ== |
||||
|
||||
ws@~6.1.0: |
||||
version "6.1.4" |
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" |
||||
integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA== |
||||
dependencies: |
||||
async-limiter "~1.0.0" |
||||
|
||||
xmlhttprequest-ssl@~1.5.4: |
||||
version "1.5.5" |
||||
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" |
||||
integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= |
||||
|
||||
yeast@0.1.2: |
||||
version "0.1.2" |
||||
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" |
||||
integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= |
||||
|
||||
yn@3.1.1: |
||||
version "3.1.1" |
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" |
||||
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== |
@ -0,0 +1,23 @@ |
||||
module.exports = { |
||||
"ignorePatterns": [".eslintrc.js", "vite.config.ts", "postcss.config.js"], |
||||
"settings": { |
||||
"react": { |
||||
"version": "detect" |
||||
} |
||||
}, |
||||
"parserOptions": { |
||||
"tsconfigRootDir": __dirname, |
||||
"project": ["./tsconfig.json"] |
||||
}, |
||||
"extends": [ |
||||
"plugin:react/recommended" |
||||
], |
||||
"rules": { |
||||
// suppress errors for missing 'import React' in files
|
||||
"react/react-in-jsx-scope": "off", |
||||
// At some point these must be set to "error"
|
||||
"@typescript-eslint/no-unsafe-member-access": "warn", |
||||
"@typescript-eslint/no-unsafe-assignment": "warn", |
||||
"@typescript-eslint/no-unsafe-argument": "warn" |
||||
} |
||||
}; |
@ -1,25 +0,0 @@ |
||||
module.exports = { |
||||
webpack: function (config, env) { |
||||
return config; |
||||
}, |
||||
devServer: function (configFunction) { |
||||
// Return the replacement function for create-react-app to use to generate the Webpack
|
||||
// Development Server config. "configFunction" is the function that would normally have
|
||||
// been used to generate the Webpack Development server config - you can use it to create
|
||||
// a starting configuration to then modify instead of having to create a config from scratch.
|
||||
return function (proxy, allowedHost) { |
||||
// Create the default config by calling configFunction with the proxy/allowedHost parameters
|
||||
const config = configFunction(proxy, allowedHost); |
||||
|
||||
config.proxy = { |
||||
"/socket.io": { |
||||
target: "http://localhost:3001", |
||||
ws: true |
||||
} |
||||
} |
||||
|
||||
// Return your customised Webpack Development Server config.
|
||||
return config; |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="utf-8" /> |
||||
<link rel="icon" href="/harambee.ico" /> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
||||
<meta name="theme-color" content="#304ba3" /> |
||||
<meta |
||||
name="description" |
||||
content="Centurion: Honderd minuten... Honderd shots... Kan jij het aan?" |
||||
/> |
||||
<link rel="apple-touch-icon" href="/logo192.png" /> |
||||
<link rel="manifest" href="/manifest.json" /> |
||||
<title>Centurion</title> |
||||
</head> |
||||
<body> |
||||
<noscript>You need to enable JavaScript to run this app.</noscript> |
||||
<div id="root"></div> |
||||
<script type="module" src="/src/index.tsx"></script> |
||||
</body> |
||||
</html> |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,6 @@ |
||||
module.exports = { |
||||
plugins: [ |
||||
require('autoprefixer'), |
||||
require('postcss-nested'), |
||||
] |
||||
} |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 15 KiB |
@ -1,39 +0,0 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="utf-8" /> |
||||
<link rel="icon" href="%PUBLIC_URL%/harambee.ico" /> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
||||
<meta name="theme-color" content="#000000" /> |
||||
<meta |
||||
name="description" |
||||
content="Web site created using create-react-app" |
||||
/> |
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> |
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> |
||||
<!-- |
||||
Notice the use of %PUBLIC_URL% in the tags above. |
||||
It will be replaced with the URL of the `public` folder during the build. |
||||
Only files inside the `public` folder can be referenced from the HTML. |
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will |
||||
work correctly both with client-side routing and a non-root public URL. |
||||
Learn how to configure a non-root public URL by running `npm run build`. |
||||
--> |
||||
<title>Centurion</title> |
||||
</head> |
||||
<body> |
||||
<noscript>You need to enable JavaScript to run this app.</noscript> |
||||
<div id="root"></div> |
||||
<!-- |
||||
This HTML file is a template. |
||||
If you open it directly in the browser, you will see an empty page. |
||||
|
||||
You can add webfonts, meta tags, or analytics to this file. |
||||
The build step will place the bundled scripts into the <body> tag. |
||||
|
||||
To begin the development, run `npm start` or `yarn start`. |
||||
To create a production bundle, use `npm run build` or `yarn build`. |
||||
--> |
||||
</body> |
||||
</html> |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 229 KiB |
@ -1,2 +1,3 @@ |
||||
# https://www.robotstxt.org/robotstxt.html |
||||
User-agent: * |
||||
Disallow: / |
@ -1,11 +0,0 @@ |
||||
import React from 'react'; |
||||
|
||||
import Centurion from "./Centurion"; |
||||
|
||||
const App = () => { |
||||
return ( |
||||
<Centurion/> |
||||
); |
||||
}; |
||||
|
||||
export default App; |
@ -1,46 +1,26 @@ |
||||
import React from "react"; |
||||
import {Row} from "antd"; |
||||
import React, { useState } from "react"; |
||||
|
||||
import {useRoomRunningAndReadyChanged} from "../lib/Connection"; |
||||
import NextShot from "./NextShot"; |
||||
import { useRoomRunningAndReadyChanged } from "../lib/Connection"; |
||||
import Feed from "./Feed"; |
||||
import ShotsTaken from "./ShotsTaken"; |
||||
import Lobby from "./Lobby"; |
||||
|
||||
import logo from "../img/via-logo.svg"; |
||||
import haramlogo from "../img/harambee_logo.png"; |
||||
import Player from "./Player"; |
||||
|
||||
const Centurion = () => { |
||||
const room = useRoomRunningAndReadyChanged(); |
||||
const showFeed = (room?.running && room.readyToParticipate) || false; |
||||
|
||||
const feedContent = ( |
||||
<React.Fragment> |
||||
<Row> |
||||
<NextShot/> |
||||
<Feed/> |
||||
<ShotsTaken/> |
||||
</Row> |
||||
<Player/> |
||||
</React.Fragment> |
||||
); |
||||
|
||||
const lobbyContent = ( |
||||
<Lobby/> |
||||
); |
||||
const [currentUserReady, setCurrentUserReady] = useState(false); |
||||
const room = useRoomRunningAndReadyChanged(); |
||||
const showFeed = (room?.readyToParticipate && currentUserReady) || false; |
||||
|
||||
return ( |
||||
<> |
||||
<section className="content"> |
||||
{showFeed ? feedContent : lobbyContent} |
||||
</section> |
||||
<footer> |
||||
<img src={haramlogo} className="haram-logo" alt="haramlogo"/> |
||||
<img src={logo} className="via-logo" alt="logo"/> |
||||
</footer> |
||||
</> |
||||
); |
||||
return ( |
||||
<section className="content"> |
||||
{showFeed ? ( |
||||
<Feed /> |
||||
) : ( |
||||
<Lobby |
||||
currentUserReady={currentUserReady} |
||||
onCurrentUserReadyChange={(b: boolean) => setCurrentUserReady(b)} |
||||
/> |
||||
)} |
||||
</section> |
||||
); |
||||
}; |
||||
|
||||
export default Centurion; |
||||
export default Centurion; |
||||
|
@ -1,40 +1,220 @@ |
||||
import React from 'react'; |
||||
import {Col} from "antd" |
||||
import React, { useRef, useState } from "react"; |
||||
import { Col, Row } from "antd"; |
||||
|
||||
import {TimelineItem} from "../types/types"; |
||||
import { |
||||
EVENT_PRIORITY, |
||||
Timeline, |
||||
TimelineItem, |
||||
TimestampEvent, |
||||
} from "../types/types"; |
||||
|
||||
import FeedItem from "./FeedItem" |
||||
import {roomTime, useTimeline} from "../lib/Connection"; |
||||
import {useUpdateAfterDelay} from "../util/hooks"; |
||||
import FeedItem from "./FeedItem"; |
||||
import connection, { |
||||
calculateRoomTime, |
||||
useRoomRunningAndReadyChanged, |
||||
useRoomTime, |
||||
useTimeline, |
||||
} from "../lib/Connection"; |
||||
import { useResize, useUpdateAfterDelay } from "../util/hooks"; |
||||
import CSSTransition from "react-transition-group/CSSTransition"; |
||||
import TransitionGroup from "react-transition-group/TransitionGroup"; |
||||
import NextShot from "./NextShot"; |
||||
import ShotsTaken from "./ShotsTaken"; |
||||
import Player from "./Player"; |
||||
|
||||
const Feed = (props: any) => { |
||||
const timeline = useTimeline(); |
||||
import "../css/feed.css"; |
||||
import FeedTicker from "./FeedTicker"; |
||||
|
||||
useUpdateAfterDelay(500) |
||||
declare global { |
||||
interface Window { |
||||
__feedShakeDebug?: boolean; |
||||
} |
||||
} |
||||
|
||||
let liveFeed: TimelineItem[] = []; |
||||
function getNextItemDelay(timeline: Timeline | null, defaultDelay = 500) { |
||||
if (!timeline) { |
||||
return defaultDelay; |
||||
} |
||||
|
||||
if (timeline != null) { |
||||
liveFeed = timeline.feed.filter(item => { |
||||
return item.timestamp * 1000 <= roomTime() |
||||
}); |
||||
const time = calculateRoomTime(); |
||||
const nextItem = timeline.itemAfterTime(time); |
||||
|
||||
if (!nextItem) { |
||||
return defaultDelay; |
||||
} |
||||
|
||||
const speedFactor = connection.room.get()?.speedFactor || 1; |
||||
return (nextItem.timestamp * 1000 - time) / speedFactor; |
||||
} |
||||
|
||||
/** |
||||
* This map relates back an event to the index of the item displayed from |
||||
* top to bottom. We can use this to calculate opacity. |
||||
*/ |
||||
function calculateKeyToIndexMap(feed: TimelineItem[]) { |
||||
const keyToIndex: Map<string, number> = new Map(); |
||||
|
||||
let totalEvents = 0; |
||||
for (const item of feed) { |
||||
totalEvents += item.events.length; |
||||
} |
||||
|
||||
let totalIdx = 0; |
||||
for (const item of feed) { |
||||
for (let eventIdx = 0; eventIdx < item.events.length; eventIdx++) { |
||||
keyToIndex.set(`${item.timestamp}.${eventIdx}`, totalEvents - ++totalIdx); |
||||
} |
||||
} |
||||
|
||||
return keyToIndex; |
||||
} |
||||
|
||||
/** |
||||
* Makes sure 'shot' is always top-most within an timeline item |
||||
*/ |
||||
function sortEvents(events: TimestampEvent[]) { |
||||
return events.sort((a, b) => { |
||||
return EVENT_PRIORITY.indexOf(b.type) - EVENT_PRIORITY.indexOf(a.type); |
||||
}); |
||||
} |
||||
|
||||
const Feed = () => { |
||||
const timeline = useTimeline(); |
||||
useRoomRunningAndReadyChanged(); |
||||
useResize(); |
||||
useRoomTime(); |
||||
|
||||
return ( |
||||
<Col className="time-feed" span={24} md={16}> |
||||
<TransitionGroup className="feed-reverse"> |
||||
{liveFeed.map((item, i) => |
||||
item.events.map((event, j) => |
||||
<CSSTransition timeout={500} classNames="fade" key={`${item.timestamp}.${j}`}> |
||||
<FeedItem item={event} key={`${item.timestamp}.${j}f`}/> |
||||
</CSSTransition> |
||||
) |
||||
)} |
||||
</TransitionGroup> |
||||
const feedElement = useRef<HTMLDivElement>(null); |
||||
const [lastShotId, setLastShotId] = useState<string | null>(null); |
||||
|
||||
useUpdateAfterDelay(getNextItemDelay(timeline)); |
||||
|
||||
if (!timeline) { |
||||
throw new TypeError("Feed without timeline."); |
||||
} |
||||
|
||||
const time = calculateRoomTime(); |
||||
|
||||
const liveFeed = timeline.feed.filter((item) => { |
||||
return item.timestamp * 1000 <= time; |
||||
}); |
||||
|
||||
const keyToIndex: Map<string, number> = calculateKeyToIndexMap(liveFeed); |
||||
|
||||
const lastEvent = timeline.eventBeforeTime(time, "shot"); |
||||
if (lastEvent && lastShotId !== lastEvent?.id) { |
||||
setLastShotId(lastEvent.id); |
||||
|
||||
if (feedElement.current) { |
||||
// Let the browser first do the heavy dom stuff to avoid lagging our
|
||||
// animation.
|
||||
setTimeout(doSingleShake, 100); |
||||
} |
||||
} |
||||
|
||||
function doSingleShake() { |
||||
if (!feedElement.current) return; |
||||
|
||||
if (feedElement.current.getAnimations().length > 0) return; |
||||
|
||||
if (feedElement.current.classList.contains("feed-shot-shake")) { |
||||
feedElement.current.getAnimations().forEach((i) => i.finish()); |
||||
feedElement.current.classList.remove("feed-shot-shake"); |
||||
setTimeout(doSingleShake, 0); |
||||
return; |
||||
} |
||||
|
||||
feedElement.current.classList.add("feed-shot-shake"); |
||||
const el = feedElement.current; |
||||
const listener = function () { |
||||
el.classList.remove("feed-shot-shake"); |
||||
el.removeEventListener("animationend", listener); |
||||
}; |
||||
el.addEventListener("animationend", listener); |
||||
} |
||||
|
||||
function itemOpacity(itemKey: string) { |
||||
// This is a bit of a hack but works better than trying to do this in css.
|
||||
// This fades out the elements the lower the element is on the screen.
|
||||
|
||||
// Ticker is only visible on large screens, the ticker obstructs some of
|
||||
// the items. If it is not visible we simply take body height, otherwise
|
||||
// we use the height where the ticker starts.
|
||||
const tickerHeight = document |
||||
.querySelector(".ticker-container") |
||||
?.getBoundingClientRect().y; |
||||
const bodyHeight = document.body.clientHeight; |
||||
const totalHeight = tickerHeight ? tickerHeight : bodyHeight; |
||||
|
||||
// Start at top most item, figure out which index is the first
|
||||
// that is NOT visible.
|
||||
const items = document.querySelectorAll(".feed-item-container"); |
||||
let i = items.length - 1; |
||||
for (; i > 0; i--) { |
||||
const rect = items[i].getBoundingClientRect(); |
||||
// If the bottom of this element is below the screen, we declare it
|
||||
// not-visible.
|
||||
if (rect.y + rect.height >= totalHeight) { |
||||
break; |
||||
} |
||||
} |
||||
const totalItemsOnScreen = items.length - i; |
||||
const index = keyToIndex.get(itemKey) ?? 0; |
||||
|
||||
let x = index / (totalItemsOnScreen - 1.8); |
||||
x = Math.min(0.9, Math.max(0, x)); |
||||
return 1 - Math.pow(x, 4); |
||||
} |
||||
|
||||
const debug = true; |
||||
if (debug) { |
||||
if (!window["__feedShakeDebug"]) { |
||||
window["__feedShakeDebug"] = true; |
||||
window.document.documentElement.addEventListener("keydown", (e) => { |
||||
if (e.keyCode === 67) { |
||||
// c
|
||||
doSingleShake(); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
return ( |
||||
<div className="feed" ref={feedElement}> |
||||
<Row className="feed-items"> |
||||
<Col span={12} md={4} className="sider"> |
||||
<NextShot /> |
||||
<Player /> |
||||
</Col> |
||||
<Col span={12} md={{ span: 4, push: 16 }} className="sider"> |
||||
<ShotsTaken /> |
||||
</Col> |
||||
<Col span={24} md={{ span: 16, pull: 4 }}> |
||||
<TransitionGroup className="feed-reverse"> |
||||
{liveFeed.map((item) => |
||||
sortEvents(item.events).map((event, j) => ( |
||||
<CSSTransition |
||||
timeout={300} |
||||
classNames="fade" |
||||
key={`${item.timestamp}.${j}`} |
||||
> |
||||
<div |
||||
className="feed-item-container" |
||||
style={{ opacity: itemOpacity(`${item.timestamp}.${j}`) }} |
||||
> |
||||
<FeedItem item={event} /> |
||||
</div> |
||||
</CSSTransition> |
||||
)) |
||||
)} |
||||
</TransitionGroup> |
||||
</Col> |
||||
); |
||||
</Row> |
||||
<Row className="ticker"> |
||||
<FeedTicker /> |
||||
</Row> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export default Feed; |
||||
|
@ -1,33 +1,38 @@ |
||||
import React, {PureComponent} from 'react'; |
||||
import { Col, Row } from "antd"; |
||||
|
||||
import {TimestampEvent} from "../types/types"; |
||||
import type { TimestampEvent } from "../types/types"; |
||||
|
||||
import '../css/feed.sass' |
||||
import "../css/feed.css"; |
||||
import shot from "../img/shot.png"; |
||||
import song from "../img/song.png"; |
||||
import talk from "../img/talk.png"; |
||||
import time from "../img/time.png"; |
||||
|
||||
export interface FeedItemProps { |
||||
item: TimestampEvent; |
||||
} |
||||
|
||||
const images = { |
||||
shot, song, talk, time |
||||
shot, |
||||
song, |
||||
talk, |
||||
time, |
||||
}; |
||||
|
||||
class FeedItem extends PureComponent<{item: TimestampEvent}> { |
||||
render() { |
||||
return ( |
||||
<div className="feed-item"> |
||||
<div className="feed-item__title"> |
||||
{this.props.item.text[0]} |
||||
</div> |
||||
<div className="feed-item__emoji"> |
||||
<img src={images[this.props.item.type]}/> |
||||
</div> |
||||
<div className="feed-item__desc"> |
||||
{this.props.item.text[1]} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
const FeedItem = ({ item }: FeedItemProps) => { |
||||
return ( |
||||
<Row align="middle" className="feed-item"> |
||||
<Col span={11} className="feed-item__title"> |
||||
{item.text[0]} |
||||
</Col> |
||||
<Col span={2} className="feed-item__emoji"> |
||||
<img alt={item.type} src={images[item.type]} /> |
||||
</Col> |
||||
<Col span={11} className="feed-item__desc"> |
||||
{item.text[1]} |
||||
</Col> |
||||
</Row> |
||||
); |
||||
}; |
||||
|
||||
export default FeedItem; |
||||
|
@ -0,0 +1,95 @@ |
||||
import React, { MouseEvent, useRef, useState } from "react"; |
||||
|
||||
import "../css/feed.css"; |
||||
import connection, { useRoom } from "../lib/Connection"; |
||||
import { Button, Input, Modal } from "antd"; |
||||
import Ticker from "react-ticker"; |
||||
|
||||
const FeedTicker = (props: any) => { |
||||
const room = useRoom(); |
||||
|
||||
const [showTickerMessageModal, setShowTickerMessageModal] = useState(false); |
||||
const [tickerMessage, setTickerMessage] = useState(""); |
||||
const [blockMessageInput, setBlockMessageInput] = useState(false); |
||||
const messageInput = useRef<Input>(null); |
||||
function handleTickerMessageButton(e: MouseEvent) { |
||||
if (blockMessageInput) return; |
||||
setShowTickerMessageModal(true); |
||||
|
||||
setTimeout(function () { |
||||
messageInput.current?.focus(); |
||||
}, 100); |
||||
} |
||||
|
||||
function cancelTickerMessageModal() { |
||||
if (blockMessageInput) return; |
||||
setShowTickerMessageModal(false); |
||||
setTickerMessage(""); |
||||
} |
||||
|
||||
async function okTickerMessageModal() { |
||||
if (blockMessageInput) return; |
||||
if (tickerMessage) { |
||||
setBlockMessageInput(true); |
||||
|
||||
try { |
||||
await connection.submitTickerMessage(tickerMessage); |
||||
setBlockMessageInput(false); |
||||
setShowTickerMessageModal(false); |
||||
setTickerMessage(""); |
||||
if (messageInput.current) { |
||||
messageInput.current.input.value = ""; |
||||
} |
||||
} catch { |
||||
setBlockMessageInput(false); |
||||
} |
||||
} |
||||
} |
||||
|
||||
function getForIndex(index: number) { |
||||
return room?.ticker[index % room.ticker.length]; |
||||
} |
||||
|
||||
return ( |
||||
<div className="ticker-container"> |
||||
<Modal |
||||
title="Stuur berichtje naar de ticker" |
||||
onCancel={cancelTickerMessageModal} |
||||
onOk={okTickerMessageModal} |
||||
visible={showTickerMessageModal} |
||||
> |
||||
<Input |
||||
value={tickerMessage} |
||||
ref={messageInput} |
||||
placeholder="Bericht" |
||||
onChange={(e) => setTickerMessage(e.target.value)} |
||||
onKeyPress={(e) => { |
||||
e.key === "Enter" && okTickerMessageModal(); |
||||
}} |
||||
/> |
||||
</Modal> |
||||
|
||||
<div className="ticker-outer"> |
||||
<Ticker> |
||||
{({ index }) => ( |
||||
<> |
||||
{room?.ticker && ( |
||||
<span className="ticker-item">{getForIndex(index)}</span> |
||||
)} |
||||
</> |
||||
)} |
||||
</Ticker> |
||||
|
||||
<Button |
||||
className="ticker-message-button" |
||||
type="ghost" |
||||
onClick={handleTickerMessageButton} |
||||
> |
||||
+ |
||||
</Button> |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export default FeedTicker; |
@ -1,219 +1,313 @@ |
||||
import React, {MouseEvent, useState} from 'react'; |
||||
import {Button, Card, Col, Divider, Form, Input, InputNumber, Row, Select} from "antd" |
||||
import {red} from '@ant-design/colors'; |
||||
|
||||
import connection, {useConfig, useIsConnected, useRoom} from "../lib/Connection"; |
||||
|
||||
import "../css/lobby.sass"; |
||||
import beer from "../img/beer.png" |
||||
import {RoomOptions} from "../types/types"; |
||||
|
||||
const {Option} = Select; |
||||
|
||||
const Lobby = (props: any) => { |
||||
// Form/control states.
|
||||
const [selectedRoomId, setSelectedRoomId] = useState(1); |
||||
const [seekTime, setSeekTime] = useState(0); |
||||
const [timelineName, setTimelineName] = useState(null); |
||||
const [joiningLobby, setJoiningLobby] = useState(false); |
||||
const [joinLobbyError, setJoinLobbyError] = useState(false); |
||||
|
||||
// Room and logic states.
|
||||
const isConnected = useIsConnected(); |
||||
const room = useRoom(); |
||||
const config = useConfig(); |
||||
|
||||
// @ts-ignore
|
||||
const connectionType = connection.socket.io.engine.transport.name; |
||||
|
||||
let isLeader = room?.isLeader || false; |
||||
let userCount = room?.userCount || 0; |
||||
|
||||
function handleRequestStartClicked(e: MouseEvent) { |
||||
connection.requestStart(seekTime * 1000); |
||||
} |
||||
|
||||
function handleJoin(e: MouseEvent) { |
||||
connection.requestReady(); |
||||
} |
||||
|
||||
function applyRoomId(v: number) { |
||||
connection.requestJoin(v).then(v => { |
||||
setJoiningLobby(false); |
||||
setJoinLobbyError(!v); |
||||
}) |
||||
setJoiningLobby(true) |
||||
} |
||||
import { useState } from "react"; |
||||
import { |
||||
Button, |
||||
Card, |
||||
Col, |
||||
Divider, |
||||
Form, |
||||
Input, |
||||
InputNumber, |
||||
Row, |
||||
Select, |
||||
Badge, |
||||
} from "antd"; |
||||
import { red } from "@ant-design/colors"; |
||||
|
||||
import connection, { |
||||
useConfig, |
||||
useIsConnected, |
||||
useRoom, |
||||
useTimelineSongFileChanged, |
||||
} from "../lib/Connection"; |
||||
|
||||
import "../css/lobby.css"; |
||||
import logo from "../img/via-logo.svg"; |
||||
import haramlogo from "../img/harambee_logo.png"; |
||||
import beer from "../img/beer.png"; |
||||
import { RoomOptions } from "../types/types"; |
||||
|
||||
const { Option } = Select; |
||||
|
||||
export interface PropType { |
||||
currentUserReady: boolean; |
||||
onCurrentUserReadyChange?: (ready: boolean) => void; |
||||
} |
||||
|
||||
const Lobby = (props: PropType) => { |
||||
// Form/control states.
|
||||
const [selectedRoomId, setSelectedRoomId] = useState(1); |
||||
const [seekTime, setSeekTime] = useState(0); |
||||
const [timelineName, setTimelineName] = useState(null); |
||||
const [joiningLobby, setJoiningLobby] = useState(false); |
||||
const [joinLobbyError, setJoinLobbyError] = useState(false); |
||||
const [isPreloading, setIsPreloading] = useState(false); |
||||
const timeline = useTimelineSongFileChanged(); |
||||
|
||||
// Room and logic states.
|
||||
const isConnected = useIsConnected(); |
||||
const room = useRoom(); |
||||
const config = useConfig(); |
||||
|
||||
const isLeader = room?.isLeader || false; |
||||
const userCount = room?.userCount || 0; |
||||
|
||||
async function handleJoin() { |
||||
await preloadAudio(); |
||||
connection.requestSetReady(); |
||||
props.onCurrentUserReadyChange?.(true); |
||||
} |
||||
|
||||
async function applyRoomId(v: number) { |
||||
setJoiningLobby(true); |
||||
await connection.requestJoin(v); |
||||
setJoiningLobby(false); |
||||
setJoinLobbyError(!v); |
||||
} |
||||
|
||||
function handleJoinRandomLobby() { |
||||
connection.requestJoinRandom(); |
||||
setJoinLobbyError(false); |
||||
} |
||||
|
||||
function handleTimelineNameSet(timelineName: any) { |
||||
setTimelineName(timelineName); |
||||
connection.setRoomOptions( |
||||
new RoomOptions( |
||||
seekTime * 1000 || 0, |
||||
timelineName || room?.timelineName || "" |
||||
) |
||||
); |
||||
} |
||||
|
||||
function handleSetSeekTime(seekTime: number) { |
||||
setSeekTime(seekTime); |
||||
connection.setRoomOptions( |
||||
new RoomOptions( |
||||
seekTime * 1000 || 0, |
||||
timelineName || room?.timelineName || "" |
||||
) |
||||
); |
||||
} |
||||
|
||||
function handleJoinRandomLobby() { |
||||
connection.requestJoinRandom() |
||||
setJoinLobbyError(false); |
||||
} |
||||
function preloadAudio(): Promise<boolean> { |
||||
setIsPreloading(true); |
||||
const songFile = timeline?.songFile; |
||||
|
||||
function handleTimelineNameSet(timelineName: any) { |
||||
setTimelineName(timelineName); |
||||
connection.setRoomOptions(new RoomOptions( |
||||
seekTime || 0, |
||||
timelineName || room?.timelineName || '')) |
||||
if (!songFile) { |
||||
return Promise.resolve(false); |
||||
} |
||||
|
||||
function handleSetSeekTime(seekTime: number) { |
||||
setSeekTime(seekTime); |
||||
connection.setRoomOptions(new RoomOptions( |
||||
seekTime * 1000 || 0, |
||||
timelineName || room?.timelineName || '')) |
||||
} |
||||
|
||||
let leaderConfig = ( |
||||
return new Promise<boolean>((resolve) => { |
||||
const audioElement = new Audio(); |
||||
audioElement.addEventListener("canplaythrough", () => { |
||||
// 'canplaythrough' means the browser thinks it has buffered enough to play
|
||||
// until the end.
|
||||
setIsPreloading(false); |
||||
resolve(true); |
||||
}); |
||||
audioElement.src = songFile; |
||||
}); |
||||
} |
||||
|
||||
const leaderConfig = ( |
||||
<Row justify="center"> |
||||
<Col> |
||||
<Form |
||||
layout="horizontal" |
||||
labelCol={{ span: 8 }} |
||||
wrapperCol={{ span: 24 }} |
||||
> |
||||
<Form.Item label="Starttijd"> |
||||
<Input |
||||
type="number" |
||||
suffix="sec" |
||||
value={seekTime} |
||||
onChange={(v) => handleSetSeekTime(parseInt(v.target.value) || 0)} |
||||
/> |
||||
</Form.Item> |
||||
|
||||
<Form.Item label="Nummer"> |
||||
<Select |
||||
defaultValue={(room && room.timelineName) || ""} |
||||
onChange={(e) => handleTimelineNameSet(e)} |
||||
> |
||||
{config && |
||||
config.availableTimelines.map((item, i) => ( |
||||
<Option key={item} value={item}> |
||||
{item} |
||||
</Option> |
||||
))} |
||||
</Select> |
||||
</Form.Item> |
||||
</Form> |
||||
|
||||
<Button |
||||
block |
||||
type="primary" |
||||
loading={isPreloading} |
||||
onClick={handleJoin} |
||||
> |
||||
Start |
||||
</Button> |
||||
</Col> |
||||
</Row> |
||||
); |
||||
|
||||
const nonLeaderConfig = ( |
||||
<Row justify="center"> |
||||
<Col> |
||||
<p> |
||||
{room?.running ? "We luisteren naar" : "We gaan luisteren naar"}{" "} |
||||
<b>{room && room.timelineName}</b> en |
||||
{room?.running && <span> zijn al gestart!</span>} |
||||
{!room?.running && ( |
||||
<span> starten op {(room?.seekTime || 0) / 1000} seconden</span> |
||||
)} |
||||
</p> |
||||
|
||||
<Button |
||||
block |
||||
type="primary" |
||||
disabled={!room || props.currentUserReady} |
||||
loading={isPreloading} |
||||
onClick={handleJoin} |
||||
> |
||||
{room && props.currentUserReady |
||||
? "Wachten op het startsein" |
||||
: "Kom erbij"} |
||||
</Button> |
||||
</Col> |
||||
</Row> |
||||
); |
||||
|
||||
return ( |
||||
<div className="lobby"> |
||||
<Row className="centurion-title" justify="center"> |
||||
<Col span={4} md={4}> |
||||
<img |
||||
src={beer} |
||||
className={`beer beer-flipped ${ |
||||
isConnected ? "connected" : "connecting" |
||||
}`}
|
||||
alt="beer" |
||||
/> |
||||
</Col> |
||||
<Col span={12} md={6}> |
||||
Centurion! |
||||
</Col> |
||||
<Col span={4} md={4}> |
||||
<img |
||||
src={beer} |
||||
className={`beer ${isConnected ? "connected" : "connecting"}`} |
||||
alt="beer" |
||||
/> |
||||
</Col> |
||||
</Row> |
||||
<Row> |
||||
<Col className="hints" span={24}> |
||||
<div>Honderd minuten...</div> |
||||
<div>Honderd shots...</div> |
||||
<div>Kun jij het aan?</div> |
||||
</Col> |
||||
</Row> |
||||
<br /> |
||||
|
||||
{!isConnected && ( |
||||
<Row justify="center"> |
||||
<Col> |
||||
<Form |
||||
layout='horizontal' |
||||
labelCol={{span: 8}} |
||||
wrapperCol={{span: 24}} |
||||
> |
||||
<Form.Item label="Starttijd"> |
||||
<Input |
||||
type="number" |
||||
suffix="sec" |
||||
value={seekTime} |
||||
onChange={v => handleSetSeekTime(parseInt(v.target.value) || 0)}/> |
||||
</Form.Item> |
||||
|
||||
<Form.Item label="Nummer"> |
||||
<Select defaultValue={(room && room.timelineName) || ''} |
||||
onChange={e => handleTimelineNameSet(e)}> |
||||
{config && config.availableTimelines.map((item, i) => |
||||
<Option key={item} value={item}>{item}</Option> |
||||
)} |
||||
</Select> |
||||
</Form.Item> |
||||
|
||||
</Form> |
||||
|
||||
<Button |
||||
block |
||||
type="primary" |
||||
onClick={handleRequestStartClicked}>Start</Button> |
||||
</Col> |
||||
<Col className="lobby-connecting"> |
||||
<h2>Verbinden...</h2> |
||||
</Col> |
||||
</Row> |
||||
) |
||||
)} |
||||
|
||||
let nonLeaderConfig = ( |
||||
{isConnected && ( |
||||
<Row justify="center"> |
||||
<Col> |
||||
<p> |
||||
We gaan luisteren naar <b>{room && room.timelineName}</b> en |
||||
{room?.running && <span> zijn al gestart!</span>} |
||||
{!room?.running && <span> starten op {(room?.seekTime || 0) / 1000} seconden</span>} |
||||
</p> |
||||
|
||||
<Button |
||||
block |
||||
<Col xs={24} sm={16} md={12} xl={10}> |
||||
<Card> |
||||
<h3> |
||||
Huidige lobby: <b>{room ? `#${room.id}` : "Geen lobby"}</b> |
||||
</h3> |
||||
|
||||
{room && ( |
||||
<Row> |
||||
{userCount === 1 ? ( |
||||
<span>Er is één gebruiker aanwezig.</span> |
||||
) : ( |
||||
<span>Er zijn {userCount} gebruikers aanwezig.</span> |
||||
)} |
||||
</Row> |
||||
)} |
||||
|
||||
<Row justify="center"> |
||||
{room?.users?.map((u) => ( |
||||
<Badge |
||||
key={u.id} |
||||
status={u.readyToParticipate ? "success" : "error"} |
||||
/> |
||||
))} |
||||
</Row> |
||||
|
||||
{room && <Row>Deel de link met je vrienden om mee te doen!</Row>} |
||||
|
||||
<Divider /> |
||||
|
||||
{room && (isLeader ? leaderConfig : nonLeaderConfig)} |
||||
|
||||
<Divider /> |
||||
|
||||
<Row justify="center"> |
||||
<Col> |
||||
<InputNumber |
||||
style={{ width: "calc(100% - 150px)" }} |
||||
min={1} |
||||
max={100000} |
||||
value={selectedRoomId || room?.id || 0} |
||||
onChange={(v) => setSelectedRoomId(v || 0)} |
||||
/> |
||||
|
||||
<Button |
||||
style={{ width: "150px" }} |
||||
type="primary" |
||||
disabled={!room || room.readyToParticipate} |
||||
onClick={handleJoin}>{room && room.readyToParticipate ? 'Wachten op het startsein' : 'Kom erbij'}</Button> |
||||
</Col> |
||||
</Row> |
||||
) |
||||
|
||||
// @ts-ignore
|
||||
return ( |
||||
<div className="lobby"> |
||||
<Row> |
||||
<Col className="centurion-title" span={24}> |
||||
<div className="beer-flipped"> |
||||
<img src={beer} className={`beer ${isConnected ? 'connected' : 'connecting'}`} alt="beer"/> |
||||
</div> |
||||
<span className="text">Centurion!</span> |
||||
<img src={beer} className={`beer ${isConnected ? 'connected' : 'connecting'}`} alt="beer"/> |
||||
loading={joiningLobby} |
||||
onClick={async () => { |
||||
await applyRoomId(selectedRoomId); |
||||
}} |
||||
> |
||||
Ga naar die lobby |
||||
</Button> |
||||
|
||||
{joinLobbyError && ( |
||||
<span style={{ color: red[4] }}> |
||||
Die lobby bestaat niet |
||||
</span> |
||||
)} |
||||
</Col> |
||||
</Row> |
||||
<Row> |
||||
<Col className="hints" span={24}> |
||||
<div>Honderd minuten...</div> |
||||
<div>Honderd shots...</div> |
||||
<div>Kun jij het aan?</div> |
||||
</Col> |
||||
</Row> |
||||
<br/> |
||||
</Row> |
||||
|
||||
{!isConnected && |
||||
<Row justify="center"> |
||||
<Col className="lobby-connecting"> |
||||
<h2>Verbinden...</h2> |
||||
</Col> |
||||
</Row> |
||||
} |
||||
|
||||
{isConnected && |
||||
<Row justify="center"> |
||||
<Col xs={24} sm={16} md={12} xl={10} className="lobby-info"> |
||||
<Card> |
||||
<h3>Huidige lobby: <b>{room?.id || 'Geen lobby'}</b></h3> |
||||
|
||||
{/*<span>Verbonden met {connectionType}</span>*/} |
||||
|
||||
{room && |
||||
<span> |
||||
{userCount === 1 ? |
||||
<p>Er is één gebruiker aanwezig.</p> |
||||
: |
||||
<p>Er zijn {userCount} gebruikers aanwezig.</p> |
||||
} |
||||
</span> |
||||
} |
||||
{room && |
||||
<span>Deel de link met je vrienden om mee te doen!</span> |
||||
} |
||||
<br/> |
||||
<br/> |
||||
|
||||
{room && (isLeader ? leaderConfig : nonLeaderConfig)} |
||||
|
||||
<Divider/> |
||||
|
||||
<Row justify="center"> |
||||
<Col> |
||||
<InputNumber |
||||
style={{'width': 'calc(100% - 150px)'}} |
||||
min={1} |
||||
max={100000} |
||||
value={selectedRoomId || room?.id || 0} |
||||
onChange={(v) => setSelectedRoomId(v || 0)}/> |
||||
|
||||
<Button |
||||
style={{'width': '150px'}} |
||||
type="primary" |
||||
loading={joiningLobby} |
||||
onClick={() => { |
||||
applyRoomId(selectedRoomId) |
||||
}}>Verander van lobby</Button> |
||||
|
||||
{joinLobbyError && |
||||
<span style={{color: red[4]}}>Die lobby bestaat niet</span> |
||||
} |
||||
</Col> |
||||
</Row> |
||||
|
||||
<Row justify="center"> |
||||
<span className={'lobby-options-or'}>of</span> |
||||
</Row> |
||||
|
||||
<Row justify="center"> |
||||
<Col> |
||||
<Button type="primary" |
||||
onClick={() => { |
||||
handleJoinRandomLobby() |
||||
}}>Join een willekeurige lobby</Button> |
||||
</Col> |
||||
</Row> |
||||
</Card> |
||||
<Row justify="center"> |
||||
<span className={"lobby-options-or"}>of</span> |
||||
</Row> |
||||
|
||||
<Row justify="center"> |
||||
<Col> |
||||
<Button |
||||
type="primary" |
||||
onClick={() => { |
||||
handleJoinRandomLobby(); |
||||
}} |
||||
> |
||||
Join een nieuwe lobby |
||||
</Button> |
||||
</Col> |
||||
</Row> |
||||
} |
||||
</div> |
||||
); |
||||
</Row> |
||||
</Card> |
||||
</Col> |
||||
</Row> |
||||
)} |
||||
<img src={haramlogo} className="haram-logo" alt="haramlogo"/> |
||||
<img src={logo} className="via-logo" alt="logo" /> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export default Lobby; |
||||
|
@ -1,42 +1,48 @@ |
||||
import React from 'react'; |
||||
import {Col, Progress} from "antd" |
||||
import {roomTime, useTimeline} from "../lib/Connection"; |
||||
import {useUpdateAfterDelay} from "../util/hooks"; |
||||
|
||||
import { Progress } from "antd"; |
||||
import { calculateRoomTime, useTimeline } from "../lib/Connection"; |
||||
import { useUpdateAfterDelay } from "../util/hooks"; |
||||
|
||||
const NextShot = () => { |
||||
const timeline = useTimeline() |
||||
|
||||
useUpdateAfterDelay(1000) |
||||
|
||||
let remainingTime = 0; |
||||
let remainingPercentage = 0; |
||||
|
||||
if (timeline) { |
||||
const time = roomTime(); |
||||
const [current, next] = timeline.itemAtTime(time, 'shot'); |
||||
|
||||
if (current && next) { |
||||
let currentTime = time - current.timestamp * 1000 |
||||
let nextTime = next.timestamp * 1000 - current.timestamp * 1000; |
||||
|
||||
remainingTime = Math.round((nextTime - currentTime) / 1000) |
||||
remainingPercentage = 100 - (currentTime / (nextTime || 1)) * 100; |
||||
} |
||||
} |
||||
|
||||
return ( |
||||
<Col className="sider" span={24} md={4}> |
||||
<h1>Tijd tot volgende shot:</h1> |
||||
<Progress type="circle" |
||||
percent={remainingPercentage} |
||||
format={_ => remainingTime + ' sec.'} |
||||
strokeColor={"#304ba3"} |
||||
strokeWidth={10} |
||||
status="normal"/> |
||||
</Col> |
||||
|
||||
); |
||||
const timeline = useTimeline(); |
||||
|
||||
useUpdateAfterDelay(1000); |
||||
|
||||
if (!timeline) { |
||||
throw new TypeError("NextShot without timeline"); |
||||
} |
||||
|
||||
let remainingTime = 0; |
||||
let remainingPercentage = 0; |
||||
|
||||
const currentRoomTime = calculateRoomTime(); |
||||
|
||||
const nextItem = timeline.itemAfterTime(currentRoomTime, "shot"); |
||||
|
||||
if (nextItem) { |
||||
const prevShotRoomTime = |
||||
(timeline.itemBeforeTime(currentRoomTime, "shot")?.timestamp || 0) * 1000; |
||||
const nextShotRoomTime = nextItem?.timestamp * 1000; |
||||
const totalRoomTimeBetweenShots = nextShotRoomTime - prevShotRoomTime; |
||||
const roomTimeSinceLastShot = currentRoomTime - prevShotRoomTime; |
||||
|
||||
remainingTime = Math.round((nextShotRoomTime - currentRoomTime) / 1000); |
||||
remainingPercentage = |
||||
100 - (roomTimeSinceLastShot / totalRoomTimeBetweenShots) * 100; |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
<h1>Tijd tot volgende shot:</h1> |
||||
<Progress |
||||
type="circle" |
||||
percent={remainingPercentage} |
||||
format={() => `${remainingTime} sec.`} |
||||
strokeColor={"#304ba3"} |
||||
strokeWidth={10} |
||||
status="normal" |
||||
/> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default NextShot; |
||||
|
@ -1,106 +1,211 @@ |
||||
import {roomTime, useRoomRunningAndReadyChanged, useRoomTime, useTimelineSongFileChanged} from "../lib/Connection"; |
||||
import React, {createRef, SyntheticEvent, useRef, useState} from "react"; |
||||
import connection, { |
||||
calculateRoomTime, |
||||
useRoomRunningAndReadyChanged, |
||||
useTimelineSongFileChanged, |
||||
} from "../lib/Connection"; |
||||
import { SyntheticEvent, useEffect, useRef, useState } from "react"; |
||||
|
||||
import '../css/player.sass' |
||||
import {Room} from "../types/types"; |
||||
import {parse as parseQueryString} from "query-string"; |
||||
import { Button, Slider } from "antd"; |
||||
import { SoundFilled, SoundOutlined } from "@ant-design/icons"; |
||||
|
||||
import "../css/player.css"; |
||||
import { Room } from "../types/types"; |
||||
|
||||
const Player = () => { |
||||
const room = useRoomRunningAndReadyChanged(); |
||||
const _ = useRoomTime() |
||||
const timeline = useTimelineSongFileChanged(); |
||||
const room = useRoomRunningAndReadyChanged(); |
||||
const timeline = useTimelineSongFileChanged(); |
||||
|
||||
const player = useRef<HTMLAudioElement>(null); |
||||
const defaultVolume = parseInt(localStorage.getItem("volume") ?? "100"); |
||||
|
||||
const [volume, setVolume] = useState(defaultVolume); |
||||
const [muted, setMuted] = useState(false); |
||||
const [finishedLoading, setFinishedLoading] = useState(false); |
||||
|
||||
const [timesSeeked, setTimesSeeked] = useState(0); |
||||
const [hadError, setHadError] = useState(false); |
||||
|
||||
// If our time synchronisation algorithm thing thinks the time is off by more
|
||||
// than this value, we seek the running player to correct it.
|
||||
const diffSecondsRequiredToSeekRunningPlayer = 0.2; |
||||
|
||||
// Hard cap we are allowed to seek this player. Some browsers are slow or inaccurate
|
||||
// and will always be off. To avoid endless skipping of the song this cap stops seeking the
|
||||
// player.
|
||||
const maxTimesSeekAllow = 25; |
||||
|
||||
useEffect(() => { |
||||
// Need to use an effect since 'player' will only contain a reference after first render.
|
||||
|
||||
if (!timeline) { |
||||
throw new Error("Player without active timeline."); |
||||
} |
||||
|
||||
if (!player.current) { |
||||
throw new Error("No player after mount."); |
||||
} |
||||
|
||||
let player = useRef<HTMLAudioElement>(null) |
||||
player.current.src = timeline.songFile; |
||||
}, [timeline]); |
||||
|
||||
const [timesSeeked, setTimesSeeked] = useState(0); |
||||
const [hadError, setHadError] = useState(false); |
||||
function handlePlayerOnPlay(e: SyntheticEvent) { |
||||
e.preventDefault(); |
||||
// For when the user manually started the player for when autoplay is off.
|
||||
setHadError(false); |
||||
} |
||||
|
||||
// If our time synchronisation algorithm thing thinks the time is off by more
|
||||
// than this value, we seek the running player to correct it.
|
||||
const diffSecondsRequiredToSeekRunningPlayer = 0.20; |
||||
async function handlePlayerPause(e: SyntheticEvent) { |
||||
if (!shouldPlay()) { |
||||
// We should not be playing, pausing is fine.
|
||||
console.log("should not play, paused"); |
||||
return; |
||||
} |
||||
|
||||
// Hard cap we are allowed to seek this player. Some browsers are slow or inaccurate
|
||||
// and will always be off. To avoid endless skipping of the song this cap stops seeking the
|
||||
// player.
|
||||
const maxTimesSeekAllow = 25; |
||||
e.preventDefault(); |
||||
|
||||
const query = parseQueryString(window.location.search); |
||||
if (query.nosound) { |
||||
return null; |
||||
if (room) { |
||||
setPlayerTime(room, true); |
||||
} |
||||
await player.current?.play(); |
||||
} |
||||
|
||||
if (player.current && player.current.dataset.src != timeline!!.songFile) { |
||||
player.current.dataset.src = timeline!!.songFile; |
||||
player.current.src = timeline!!.songFile; |
||||
function handlePlayerCanPlayThrough() { |
||||
if (!finishedLoading) { |
||||
setFinishedLoading(true); |
||||
connection.requestStart(); |
||||
} |
||||
} |
||||
|
||||
function shouldPlay() { |
||||
return ( |
||||
player.current && |
||||
timeline && |
||||
room && |
||||
room.running && |
||||
room.readyToParticipate && |
||||
!player.current.ended |
||||
); |
||||
} |
||||
|
||||
async function startPlaying(manual: boolean) { |
||||
if (!player.current) { |
||||
return; |
||||
} |
||||
|
||||
function handlePlayerOnPlay(e: SyntheticEvent) { |
||||
e.preventDefault(); |
||||
if (player.current.paused && !hadError) { |
||||
setPlayerVolume(volume); |
||||
|
||||
// For when the user manually started the player for when autoplay is off.
|
||||
try { |
||||
await player.current.play(); |
||||
setHadError(false); |
||||
if (shouldPlay()) { |
||||
startPlaying(true) |
||||
} |
||||
} catch (e) { |
||||
console.error("Error playing", e); |
||||
setHadError(true); |
||||
} |
||||
} |
||||
|
||||
function shouldPlay() { |
||||
return player.current && timeline && room && room.running && room.readyToParticipate |
||||
if (!hadError && room) { |
||||
setPlayerTime(room, manual); |
||||
} |
||||
} |
||||
|
||||
function startPlaying(manual: boolean) { |
||||
if (!player.current) return; |
||||
|
||||
if (player.current.paused && !hadError) { |
||||
player.current.play().then(() => { |
||||
setHadError(false); |
||||
}).catch(e => { |
||||
console.error('Error playing', e); |
||||
setHadError(true); |
||||
}) |
||||
} |
||||
|
||||
if (!hadError) { |
||||
setPlayerTime(room!!, manual); |
||||
} |
||||
function setPlayerTime(room: Room, manualAdjustment: boolean) { |
||||
if (!player.current) { |
||||
return; |
||||
} |
||||
|
||||
function setPlayerTime(room: Room, manualAdjustment: boolean) { |
||||
if (!player.current) return; |
||||
|
||||
let targetTime = roomTime() / 1000; |
||||
let diff = player.current.currentTime - targetTime; |
||||
|
||||
if (player.current && Math.abs(diff) > diffSecondsRequiredToSeekRunningPlayer) { |
||||
if (room.speedFactor != 1 || manualAdjustment || timesSeeked < maxTimesSeekAllow) { |
||||
player.current.currentTime = targetTime; |
||||
player.current.playbackRate = Math.max(Math.min(4.0, room.speedFactor), 0.25); |
||||
|
||||
if (!manualAdjustment) { |
||||
setTimesSeeked(timesSeeked + 1); |
||||
} |
||||
} else { |
||||
console.warn('The running player is off, but we\'ve changed the time ' + |
||||
'too often, skipping synchronizing the player.'); |
||||
} |
||||
} |
||||
// Player's currentTime is in seconds, not ms.
|
||||
const targetTime = calculateRoomTime() / 1000; |
||||
const diff = Math.abs(player.current.currentTime - targetTime); |
||||
|
||||
// console.log('PLAYER DIFF', diff,
|
||||
// 'min req to seek: ', diffSecondsRequiredToSeekRunningPlayer,
|
||||
// `(${timesSeeked} / ${maxTimesSeekAllow})`);
|
||||
|
||||
if (diff <= diffSecondsRequiredToSeekRunningPlayer) { |
||||
return; |
||||
} |
||||
|
||||
if (shouldPlay()) { |
||||
startPlaying(false) |
||||
} else { |
||||
if (player.current) { |
||||
player.current.pause(); |
||||
} |
||||
if (timesSeeked >= maxTimesSeekAllow && !manualAdjustment) { |
||||
// If we are adjusting manually we always allow a seek.
|
||||
console.warn( |
||||
"The running player is off, but we've changed the time " + |
||||
"too often, skipping synchronizing the player." |
||||
); |
||||
return; |
||||
} |
||||
|
||||
function render() { |
||||
return ( |
||||
<audio ref={player} className='player' hidden={!hadError} controls={true} onPlay={handlePlayerOnPlay}/> |
||||
) |
||||
player.current.currentTime = targetTime; |
||||
player.current.playbackRate = Math.min(room.speedFactor, 5); |
||||
|
||||
if (!manualAdjustment) { |
||||
setTimesSeeked(timesSeeked + 1); |
||||
} |
||||
|
||||
return render(); |
||||
} |
||||
console.log( |
||||
`Player seeked: diff: ${diff}, target: ${targetTime}, (${timesSeeked} / ${maxTimesSeekAllow})` |
||||
); |
||||
} |
||||
|
||||
function toggleMute() { |
||||
if (player.current != null) { |
||||
player.current.muted = !player.current.muted; |
||||
setMuted(!muted); |
||||
} |
||||
} |
||||
|
||||
function changeVolume(sliderValue: number) { |
||||
setVolume(sliderValue); |
||||
localStorage["volume"] = sliderValue; |
||||
setPlayerVolume(sliderValue); |
||||
} |
||||
|
||||
function setPlayerVolume(value: number) { |
||||
if (player.current) { |
||||
player.current.volume = |
||||
value === 0.0 ? 0.0 : Math.pow(10, (value / 100 - 1) * 2); |
||||
} |
||||
} |
||||
|
||||
if (shouldPlay()) { |
||||
startPlaying(false) |
||||
.then(() => { |
||||
//
|
||||
}) |
||||
.catch((e) => { |
||||
console.error(e); |
||||
}); |
||||
} else { |
||||
if (player.current) { |
||||
player.current.pause(); |
||||
} |
||||
} |
||||
|
||||
return ( |
||||
<div id="audio"> |
||||
<audio |
||||
ref={player} |
||||
className="player" |
||||
controls={true} |
||||
loop={false} |
||||
hidden={!hadError} |
||||
onPause={handlePlayerPause} |
||||
onPlay={handlePlayerOnPlay} |
||||
onCanPlayThrough={handlePlayerCanPlayThrough} |
||||
/> |
||||
<div id="volume-control"> |
||||
<Button onClick={toggleMute} shape="circle"> |
||||
{muted ? <SoundOutlined /> : <SoundFilled />} |
||||
</Button> |
||||
<Slider |
||||
id="volume-slider" |
||||
defaultValue={volume} |
||||
onChange={changeVolume} |
||||
trackStyle={{ backgroundColor: "var(--secondary-color)" }} |
||||
/> |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export default Player; |
||||
|
@ -1,34 +1,41 @@ |
||||
import React from 'react'; |
||||
import {Col, Progress} from "antd" |
||||
import {roomTime, useTimeline} from "../lib/Connection"; |
||||
import {useUpdateAfterDelay} from "../util/hooks"; |
||||
|
||||
import { Progress } from "antd"; |
||||
import { calculateRoomTime, useTimeline } from "../lib/Connection"; |
||||
import { useUpdateAfterDelay } from "../util/hooks"; |
||||
|
||||
const ShotsTaken = () => { |
||||
let timeline = useTimeline(); |
||||
const timeline = useTimeline(); |
||||
useUpdateAfterDelay(1000); |
||||
|
||||
if (!timeline) { |
||||
throw new TypeError("ShotsTaken without timeline"); |
||||
} |
||||
const totalShots = timeline.getTotalShotCount(); |
||||
|
||||
useUpdateAfterDelay(1000); |
||||
let taken = 0; |
||||
|
||||
let taken = 0; |
||||
const time = calculateRoomTime(); |
||||
const prevShot = timeline.eventBeforeTime(time, "shot"); |
||||
|
||||
if (timeline) { |
||||
let [current, _] = timeline.eventAtTime(roomTime(), 'shot'); |
||||
if (current) { |
||||
taken = current.shotCount!!; |
||||
} |
||||
} |
||||
if (prevShot) { |
||||
taken = prevShot.shotCount; |
||||
} else { |
||||
const nextShot = timeline.eventAfterTime(time, "shot"); |
||||
taken = nextShot ? nextShot.shotCount - 1 : taken; |
||||
} |
||||
|
||||
return ( |
||||
<Col className="sider" span={24} md={4}> |
||||
<h1>Shots genomen:</h1> |
||||
<Progress type="circle" |
||||
percent={taken} |
||||
format={_ => taken + ' / 100'} |
||||
status="normal" |
||||
strokeColor={"#304ba3"} |
||||
strokeWidth={10}/> |
||||
</Col> |
||||
); |
||||
return ( |
||||
<> |
||||
<h1>Shots genomen:</h1> |
||||
<Progress |
||||
type="circle" |
||||
percent={(taken / totalShots) * 100} |
||||
format={() => `${taken} / ${totalShots}`} |
||||
status="normal" |
||||
strokeColor={"#304ba3"} |
||||
strokeWidth={10} |
||||
/> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default ShotsTaken; |
||||
|
@ -0,0 +1,133 @@ |
||||
.feed { |
||||
#audio { |
||||
padding-top: 15px; |
||||
padding-bottom: 15px; |
||||
padding: 15px auto; |
||||
} |
||||
|
||||
&.feed-shot-shake { |
||||
animation: shake-out 0.5s ease-out; |
||||
} |
||||
|
||||
@keyframes shake-out { |
||||
0%, |
||||
20% { |
||||
transform: translate3d(-4px, 0, 0); |
||||
} |
||||
10%, |
||||
30% { |
||||
transform: translate3d(4px, 0, 0); |
||||
} |
||||
40% { |
||||
transform: translate3d(-3px, 0, 0); |
||||
} |
||||
50% { |
||||
transform: translate3d(3px, 0, 0); |
||||
} |
||||
60% { |
||||
transform: translate3d(-2px, 0, 0); |
||||
} |
||||
70% { |
||||
transform: translate3d(2px, 0, 0); |
||||
} |
||||
80% { |
||||
transform: translate3d(-1px, 0, 0); |
||||
} |
||||
90% { |
||||
transform: translate3d(1px, 0, 0); |
||||
} |
||||
100% { |
||||
transform: translate3d(0px, 0, 0); |
||||
} |
||||
} |
||||
|
||||
.fade-enter { |
||||
opacity: 1; |
||||
transform: translateY(-30%); |
||||
} |
||||
|
||||
.fade-enter-active { |
||||
transition: opacity 0.3s ease-out, transform 0.3s ease-out; |
||||
opacity: 1; |
||||
transform: translateY(0); |
||||
} |
||||
|
||||
.fade-leave { |
||||
opacity: 1; |
||||
} |
||||
|
||||
.fade-leave.fade-leave-active { |
||||
transition: opacity 0.3s ease-out; |
||||
opacity: 0; |
||||
} |
||||
|
||||
} |
||||
|
||||
.feed-reverse { |
||||
display: flex; |
||||
flex-direction: column-reverse; |
||||
} |
||||
|
||||
.feed-item { |
||||
border: 1px solid #efefef; |
||||
border-radius: 2px; |
||||
padding: 0.35em; |
||||
margin: 0.25em; |
||||
text-align: center; |
||||
font-size: 1.1rem; |
||||
font-weight: bold; |
||||
color: rgba(0, 0, 0, 0.85); |
||||
box-shadow: 0 0 10px -4px rgba(48, 75, 163, 0.2); |
||||
|
||||
@media (min-width: 992px) { |
||||
font-size: 1.5rem; |
||||
} |
||||
} |
||||
|
||||
.feed-item__emoji > img { |
||||
max-width: 100%; |
||||
max-height: 40px; |
||||
} |
||||
|
||||
:root { |
||||
--ticker-height: 3em; |
||||
--feed-height: calc(100% - var(--ticker-height)); |
||||
} |
||||
|
||||
.feed-items { |
||||
overflow: hidden; |
||||
|
||||
@media (min-width: 992px) { |
||||
height: var(--feed-height); |
||||
padding-bottom: var(--ticker-height); |
||||
} |
||||
} |
||||
|
||||
.ticker-container { |
||||
display: none; |
||||
width: 100%; |
||||
height: var(--ticker-height); |
||||
overflow: hidden; |
||||
|
||||
@media (min-width: 992px) { |
||||
display: block; |
||||
} |
||||
|
||||
.ticker-outer { |
||||
width: 100%; |
||||
height: 100%; |
||||
position: relative; |
||||
} |
||||
|
||||
.ticker-message-button { |
||||
position: absolute; |
||||
left: 0; |
||||
bottom: 0; |
||||
background: white; |
||||
} |
||||
|
||||
.ticker-item { |
||||
font-size: 2em; |
||||
margin-right: 2em; |
||||
} |
||||
} |
@ -1,49 +0,0 @@ |
||||
.feed-item |
||||
display: flex |
||||
border: 1px solid #efefef |
||||
border-radius: 4px |
||||
padding: 10px |
||||
margin: 10px |
||||
text-align: center |
||||
font-size: 16pt |
||||
font-weight: bold |
||||
color: rgba(0, 0, 0, 0.85) |
||||
box-shadow: 0 0 10px -4px rgba(48, 75, 163, 0.2) |
||||
|
||||
.feed-item__title |
||||
width: 45% |
||||
|
||||
.feed-item__desc |
||||
width: 45% |
||||
|
||||
.feed-item__emoji |
||||
width: 10% |
||||
|
||||
img |
||||
width: 60% |
||||
max-width: 100% |
||||
max-height: 100% |
||||
|
||||
.feed-reverse |
||||
display: flex |
||||
flex-direction: column-reverse |
||||
|
||||
|
||||
// Animations |
||||
.fade-enter |
||||
opacity: 0.01 |
||||
max-height: 0 |
||||
|
||||
.fade-enter.fade-enter-active |
||||
opacity: 1 |
||||
max-height: 1000px |
||||
transition: opacity 500ms ease-in, max-height 500ms ease-in |
||||
|
||||
.fade-leave |
||||
opacity: 1 |
||||
max-height: 1000px |
||||
|
||||
.fade-leave.fade-leave-active |
||||
opacity: 0.01 |
||||
max-height: 0 |
||||
transition: opacity 300ms ease-in, max-height 300ms ease-in |
@ -0,0 +1,59 @@ |
||||
@import 'antd/es/style/index'; |
||||
|
||||
@font-face { |
||||
font-family: 'SourceSansPro'; |
||||
src: local('SourceSansPro'), url('./SourceSansPro.otf') format('opentype'); |
||||
} |
||||
|
||||
:root { |
||||
--primary-color: #304ba3; |
||||
--secondary-color: #ae2573; |
||||
} |
||||
|
||||
body { |
||||
font-family: "SourceSansPro", sans-serif !important; |
||||
background-color: white; |
||||
} |
||||
|
||||
#root { |
||||
height: 100%; |
||||
} |
||||
|
||||
.feed { |
||||
height: 100%; |
||||
overflow: hidden; |
||||
padding: 0.5em; |
||||
} |
||||
|
||||
.content { |
||||
padding: 1rem; |
||||
height: 100%; |
||||
} |
||||
|
||||
.via-logo { |
||||
z-index: -10000; |
||||
position: fixed; |
||||
right: 0; |
||||
bottom: 0; |
||||
width: auto; |
||||
height: 5em; |
||||
padding: 10px; |
||||
} |
||||
|
||||
.haram-logo { |
||||
z-index: -10000 |
||||
position: fixed; |
||||
left: 0; |
||||
bottom: 0; |
||||
width: auto; |
||||
height: 10em; |
||||
padding: 10px; |
||||
} |
||||
|
||||
.sider { |
||||
text-align: center; |
||||
} |
||||
|
||||
h1 { |
||||
min-height: 3em; |
||||
} |
@ -1,57 +0,0 @@ |
||||
@import '~antd/dist/antd.css' |
||||
|
||||
@font-face |
||||
font-family: 'SourceSansPro' |
||||
src: local('SourceSansPro'), url('./SourceSansPro.otf') format('opentype') |
||||
|
||||
|
||||
$footer-height: 4.5em |
||||
$content-height: calc(100vh - #{$footer-height}) |
||||
|
||||
body |
||||
font-family: 'SourceSansPro', sans-serif |
||||
background-color: white |
||||
|
||||
|
||||
.feed |
||||
max-height: $content-height |
||||
overflow: hidden |
||||
padding: 0.5em |
||||
|
||||
|
||||
.content |
||||
padding: 1rem |
||||
height: $content-height |
||||
|
||||
|
||||
.via-logo |
||||
z-index: -10000 |
||||
position: fixed |
||||
right: 0 |
||||
bottom: 0 |
||||
width: auto |
||||
height: 5em |
||||
padding: 10px |
||||
|
||||
|
||||
.haram-logo |
||||
z-index: -10000 |
||||
position: fixed |
||||
left: 0 |
||||
bottom: 0 |
||||
width: auto |
||||
height: 10em |
||||
padding: 10px |
||||
|
||||
|
||||
.sider |
||||
text-align: center |
||||
|
||||
|
||||
h1 |
||||
min-height: 3em |
||||
|
||||
|
||||
footer |
||||
padding: 0.25em 0.5em |
||||
height: 4.5em |
@ -0,0 +1,76 @@ |
||||
.lobby { |
||||
.centurion-title { |
||||
text-align: center; |
||||
|
||||
font-size: 2.5rem; |
||||
min-height: inherit; |
||||
|
||||
.text { |
||||
padding: 0 2rem; |
||||
} |
||||
|
||||
.beer { |
||||
animation-fill-mode: forwards; |
||||
max-width: 120%; |
||||
max-height: 120%; |
||||
|
||||
&.connecting { |
||||
animation: spin 0.4s ease-in-out 25; |
||||
} |
||||
|
||||
@keyframes spin { |
||||
100% { |
||||
transform: rotate(-360deg); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
.beer-flipped { |
||||
transform: scaleX(-1); |
||||
} |
||||
|
||||
.lobby-connecting { |
||||
margin: 2em 0 0 0; |
||||
text-align: center; |
||||
} |
||||
|
||||
.hints { |
||||
margin: 1rem 0 0 0; |
||||
font-size: 1.5rem; |
||||
text-align: center; |
||||
} |
||||
|
||||
.control { |
||||
font-size: 1.3rem; |
||||
margin: 2em 1em 2em 1em; |
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); |
||||
} |
||||
|
||||
.lobby-options-or { |
||||
display: inline-block; |
||||
position: relative; |
||||
|
||||
margin: 16px 0; |
||||
color: #888; |
||||
|
||||
&:before, |
||||
&:after { |
||||
content: ""; |
||||
width: 1px; |
||||
height: 6px; |
||||
position: absolute; |
||||
left: 50%; |
||||
margin-left: 0.1px; |
||||
background: #bbb; |
||||
} |
||||
|
||||
&:before { |
||||
bottom: 100%; |
||||
} |
||||
|
||||
&:after { |
||||
top: 100%; |
||||
} |
||||
} |
||||
} |
@ -1,62 +0,0 @@ |
||||
.lobby |
||||
.centurion-title |
||||
text-align: center |
||||
|
||||
font-size: 2.5rem |
||||
min-height: inherit |
||||
|
||||
.text |
||||
padding: 0 2rem |
||||
|
||||
.beer |
||||
animation-fill-mode: forwards |
||||
|
||||
&.connecting |
||||
animation: spin 0.4s ease-in-out 25 |
||||
|
||||
@keyframes spin |
||||
100% |
||||
transform: rotate(-360deg) |
||||
|
||||
.beer-flipped |
||||
transform: scaleX(-1) |
||||
display: inline-block |
||||
|
||||
.lobby-connecting |
||||
margin: 2em 0 0 0 |
||||
text-align: center |
||||
|
||||
.hints |
||||
margin: 1rem 0 0 0 |
||||
font-size: 1.5rem |
||||
text-align: center |
||||
|
||||
.lobby-info |
||||
text-align: center |
||||
|
||||
.control |
||||
font-size: 1.3rem |
||||
margin: 2em 1em 2em 1em |
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24) |
||||
|
||||
.lobby-options-or |
||||
display: inline-block |
||||
position: relative |
||||
|
||||
margin: 16px 0 |
||||
color: #888 |
||||
|
||||
&:before, &:after |
||||
content: '' |
||||
width: 1px |
||||
height: 6px |
||||
position: absolute |
||||
left: 50% |
||||
margin-left: 0.1px |
||||
background: #bbb |
||||
|
||||
&:before |
||||
bottom: 100% |
||||
|
||||
&:after |
||||
top: 100% |
@ -0,0 +1,16 @@ |
||||
#volume-control { |
||||
display: flex; |
||||
flex-direction: row; |
||||
justify-content: center; |
||||
} |
||||
|
||||
#volume-control .ant-slider { |
||||
width: 65%; |
||||
padding-left: 10px; |
||||
} |
||||
|
||||
.player { |
||||
position: fixed; |
||||
left: 0; |
||||
bottom: 0; |
||||
} |
@ -1,4 +0,0 @@ |
||||
.player |
||||
position: fixed |
||||
left: 0 |
||||
bottom: 0 |
@ -1,7 +1,6 @@ |
||||
import React from 'react'; |
||||
import ReactDOM from 'react-dom'; |
||||
import './css/index.sass'; |
||||
import ReactDOM from "react-dom"; |
||||
import "./css/index.css"; |
||||
|
||||
import App from './components/App'; |
||||
import Centurion from "./components/Centurion"; |
||||
|
||||
ReactDOM.render(<App/>, document.getElementById('root')); |
||||
ReactDOM.render(<Centurion />, document.getElementById("root")); |
||||
|
@ -1,335 +1,311 @@ |
||||
import io from "socket.io-client"; |
||||
import {useEffect, useState} from "react"; |
||||
import {parse as parseQueryString, stringify as stringifyQueryString} from 'query-string'; |
||||
import io, { Socket } from "socket.io-client"; |
||||
|
||||
import {Config, Room, RoomOptions, Timeline} from "../types/types"; |
||||
import {Sub, useSub} from "../util/sub"; |
||||
import { Config, Room, RoomOptions, Timeline } from "../types/types"; |
||||
import { Sub, useSub } from "../util/sub"; |
||||
|
||||
class Connection { |
||||
url = '/'; |
||||
|
||||
socket: SocketIOClient.Socket; |
||||
|
||||
isConnected = new Sub<boolean>(); |
||||
|
||||
config = new Sub<Config | null>(); |
||||
room = new Sub<Room | null>(); |
||||
timeline = new Sub<Timeline | null>(); |
||||
|
||||
timeSyncIntervals = [500, 1000, 3000, 5000, 10000, 30000]; |
||||
timeSyncs: { [requestId: number]: TimeSyncRequest } = {}; |
||||
timeSyncTimeoutIds: number[] = []; |
||||
timeSyncTooOld = 120000; |
||||
roomTime = new Sub<number>(); |
||||
|
||||
calls: { [id: number]: Call } = {}; |
||||
|
||||
constructor() { |
||||
this.isConnected.set(false); |
||||
|
||||
this.socket = io(this.url, { |
||||
autoConnect: false, |
||||
transports: ['websocket'] |
||||
}); |
||||
url = "/"; |
||||
|
||||
socket: Socket; |
||||
|
||||
isConnected = new Sub<boolean>(); |
||||
|
||||
config = new Sub<Config | null>(); |
||||
room = new Sub<Room | null>(); |
||||
timeline = new Sub<Timeline | null>(); |
||||
|
||||
timeSyncIntervals = [500, 1000, 3000, 5000, 10000, 30000]; |
||||
timeSyncs: { [requestId: number]: TimeSyncRequest } = {}; |
||||
timeSyncTimeoutIds: number[] = []; |
||||
timeSyncTooOld = 120000; |
||||
roomTime = new Sub<number>(); |
||||
|
||||
constructor() { |
||||
this.isConnected.set(false); |
||||
|
||||
this.socket = io(this.url, { |
||||
autoConnect: false, |
||||
transports: ["websocket"], |
||||
}); |
||||
|
||||
this.setupSocketListeners(); |
||||
|
||||
this.connect(); |
||||
|
||||
this.roomTime.set(0); |
||||
} |
||||
|
||||
connect() { |
||||
this.socket.connect(); |
||||
} |
||||
|
||||
setupSocketListeners() { |
||||
this.socket.on("connect", async () => { |
||||
this.isConnected.set(true); |
||||
await this.onConnect(); |
||||
}); |
||||
|
||||
this.socket.on("disconnect", () => { |
||||
this.isConnected.set(false); |
||||
this.onDisconnect(); |
||||
}); |
||||
|
||||
this.socket.on("config", (data: any) => { |
||||
this.config.set(data.config); |
||||
}); |
||||
|
||||
this.socket.on("time_sync", (data: any) => { |
||||
this.timeSyncResponse(data.requestId, data.clientDiff, data.serverTime); |
||||
}); |
||||
|
||||
this.socket.on("room", (data: any) => { |
||||
if (data.room) { |
||||
this.setQueryLobbyId(data.room.id); |
||||
} |
||||
|
||||
this.room.set(data.room); |
||||
}); |
||||
|
||||
this.socket.on("timeline", (data: any) => { |
||||
if (data.timeline) { |
||||
this.timeline.set(new Timeline(data.timeline)); |
||||
} else { |
||||
this.timeline.set(null); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
async onConnect() { |
||||
this.startTimeSync(); |
||||
|
||||
const lobbyId = this.getQueryLobbyId(); |
||||
if (lobbyId) { |
||||
const exists = await this.requestJoin(lobbyId); |
||||
if (!exists) { |
||||
this.setQueryLobbyId(); |
||||
this.requestJoinRandom(); |
||||
} |
||||
} else { |
||||
this.requestJoinRandom(); |
||||
} |
||||
} |
||||
|
||||
this.setupSocketListeners(); |
||||
onDisconnect() { |
||||
this.stopTimeSync(); |
||||
} |
||||
|
||||
this.connect(); |
||||
private getQueryLobbyId(): number | null { |
||||
const query = new URLSearchParams(window.location.search); |
||||
const lobby = query.get("lobby"); |
||||
|
||||
this.roomTime.set(0); |
||||
if (!lobby) { |
||||
return null; |
||||
} |
||||
|
||||
connect() { |
||||
this.socket.connect(); |
||||
} |
||||
const lobbyId = Number.parseInt(query.get("lobby")!.toString()); |
||||
|
||||
setupSocketListeners() { |
||||
this.socket.on('connect', () => { |
||||
this.isConnected.set(true); |
||||
this.onConnect(); |
||||
}) |
||||
|
||||
this.socket.on('disconnect', () => { |
||||
this.isConnected.set(false); |
||||
this.onDisconnect(); |
||||
}) |
||||
|
||||
this.socket.on('config', (data: any) => { |
||||
this.config.set(data.config); |
||||
}) |
||||
|
||||
this.socket.on('time_sync', (data: any) => { |
||||
this.timeSyncResponse(data.requestId, data.clientDiff, data.serverTime); |
||||
}) |
||||
|
||||
this.socket.on('room', (data: any) => { |
||||
if (data.room) { |
||||
this.setQueryLobbyId(data.room.id); |
||||
} |
||||
|
||||
this.room.set(data.room); |
||||
}); |
||||
|
||||
this.socket.on('timeline', (data: any) => { |
||||
if (data.timeline) { |
||||
this.timeline.set(new Timeline(data.timeline)); |
||||
} else { |
||||
this.timeline.set(null); |
||||
} |
||||
}); |
||||
|
||||
this.socket.on('call_response', (data: any) => { |
||||
let call = this.calls[data.id]; |
||||
if (!call) return; |
||||
|
||||
if (data.error) { |
||||
call.callback(data.error, null); |
||||
} else { |
||||
call.callback(null, data.response); |
||||
} |
||||
delete this.calls[data.id]; |
||||
}); |
||||
if (!Number.isSafeInteger(lobbyId) || lobbyId < 1) { |
||||
return null; |
||||
} |
||||
|
||||
onConnect() { |
||||
this.startTimeSync(); |
||||
|
||||
let lobbyId = this.getQueryLobbyId(); |
||||
if (lobbyId) { |
||||
this.requestJoin(lobbyId).then(v => { |
||||
if (!v) { |
||||
this.setQueryLobbyId(null); |
||||
this.requestJoinRandom(); |
||||
} |
||||
}) |
||||
} else { |
||||
this.requestJoinRandom(); |
||||
} |
||||
} |
||||
return lobbyId; |
||||
} |
||||
|
||||
onDisconnect() { |
||||
this.stopTimeSync(); |
||||
} |
||||
private setQueryLobbyId(lobbyId?: number) { |
||||
const newUrl = new URL(window.location.href); |
||||
|
||||
autoStart() : boolean { |
||||
let query = parseQueryString(window.location.search); |
||||
return !!query.autostart; |
||||
if (lobbyId) { |
||||
newUrl.searchParams.set("lobby", String(lobbyId)); |
||||
} else { |
||||
newUrl.searchParams.delete("lobby"); |
||||
} |
||||
|
||||
private getQueryLobbyId(): number | null { |
||||
let query = parseQueryString(window.location.search); |
||||
if (query.lobby) { |
||||
let lobbyId = Number.parseInt(query.lobby.toString()); |
||||
if (Number.isSafeInteger(lobbyId) && lobbyId > 0) { |
||||
return lobbyId |
||||
} |
||||
window.history.pushState({}, "", newUrl.toString()); |
||||
} |
||||
|
||||
setRoomOptions(roomOptions: RoomOptions) { |
||||
this.socket.emit("room_options", roomOptions); |
||||
} |
||||
|
||||
requestStart() { |
||||
this.socket.emit("request_start"); |
||||
} |
||||
|
||||
submitTickerMessage(message: string) { |
||||
return new Promise<void>((resolve, reject) => { |
||||
this.socket.emit( |
||||
"submit_ticker_message", |
||||
message, |
||||
(_: null, err?: string) => { |
||||
if (err) { |
||||
reject(err); |
||||
} else { |
||||
resolve(); |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
private setQueryLobbyId(lobbyId: number | null) { |
||||
let query = parseQueryString(window.location.search); |
||||
if (lobbyId) { |
||||
query.lobby = lobbyId.toString(); |
||||
} else { |
||||
delete query.lobby; |
||||
); |
||||
}); |
||||
} |
||||
|
||||
async requestJoin(roomId: number): Promise<boolean> { |
||||
return new Promise<boolean>((resolve, reject) => { |
||||
this.socket.emit( |
||||
"request_join", |
||||
roomId, |
||||
(err?: string, didJoinRoom?: boolean) => { |
||||
if (err) { |
||||
return reject(err); |
||||
} |
||||
|
||||
return resolve(!!didJoinRoom); |
||||
} |
||||
let newUrl = window.location.protocol + "//" + window.location.host + |
||||
window.location.pathname + (Object.keys(query).length ? ('?' + stringifyQueryString(query)) : ''); |
||||
window.history.pushState({}, '', newUrl); |
||||
} |
||||
|
||||
async call(name: string, params: any) { |
||||
return new Promise<any>((resolve, reject) => { |
||||
let callback = (err: any, res: any) => { |
||||
if (err) { |
||||
return reject(err); |
||||
} |
||||
|
||||
resolve(res); |
||||
}; |
||||
|
||||
let call = new Call(name, params, callback); |
||||
this.calls[call.id] = call; |
||||
this.socket.emit('call', call.id, name, params); |
||||
}); |
||||
} |
||||
|
||||
setRoomOptions(roomOptions: RoomOptions) { |
||||
this.socket.emit('room_options', roomOptions) |
||||
} |
||||
|
||||
requestStart(seekTime: number) { |
||||
this.socket.emit('request_start', { |
||||
seekTime: seekTime |
||||
}); |
||||
} |
||||
|
||||
async requestJoin(roomId: number): Promise<boolean> { |
||||
return this.call('room_exists', {roomId: roomId}).then(v => { |
||||
if (v) { |
||||
this.socket.emit('request_join', roomId, this.autoStart()); |
||||
if (this.autoStart()) { |
||||
this.requestReady(); |
||||
} |
||||
return true; |
||||
} else { |
||||
return false; |
||||
} |
||||
}) |
||||
); |
||||
}); |
||||
} |
||||
|
||||
requestSetReady() { |
||||
this.socket.emit("request_set_ready"); |
||||
} |
||||
|
||||
requestJoinRandom() { |
||||
this.socket.emit("request_join_random"); |
||||
} |
||||
|
||||
startTimeSync() { |
||||
for (let i = 0; i < this.timeSyncIntervals.length; i++) { |
||||
const timeoutId = setTimeout(() => { |
||||
// Only reschedule the last sync interval (i.e. every 30 seconds)
|
||||
const shouldReschedule = i === this.timeSyncIntervals.length - 1; |
||||
this.sendTimeSync(shouldReschedule); |
||||
}, this.timeSyncIntervals[i]); |
||||
|
||||
this.timeSyncTimeoutIds.push(timeoutId); |
||||
} |
||||
} |
||||
|
||||
requestReady() { |
||||
this.socket.emit('request_ready'); |
||||
stopTimeSync() { |
||||
for (let i = 0; i < this.timeSyncTimeoutIds.length; i++) { |
||||
clearTimeout(this.timeSyncTimeoutIds[i]); |
||||
} |
||||
} |
||||
|
||||
requestJoinRandom() { |
||||
this.socket.emit('request_join_random'); |
||||
} |
||||
sendTimeSync(alsoSchedule: boolean) { |
||||
const sync = new TimeSyncRequest(); |
||||
this.socket.emit("time_sync", sync.requestId, Date.now()); |
||||
this.timeSyncs[sync.requestId] = sync; |
||||
|
||||
startTimeSync() { |
||||
for (let i = 0; i < this.timeSyncIntervals.length; i++) { |
||||
let timeoutId = setTimeout(() => { |
||||
this.sendTimeSync(i === this.timeSyncIntervals.length - 1); |
||||
}, this.timeSyncIntervals[i]); |
||||
// @ts-ignore
|
||||
this.timeSyncTimeoutIds.push(timeoutId); |
||||
} |
||||
if (alsoSchedule) { |
||||
setTimeout(() => { |
||||
this.sendTimeSync(true); |
||||
}, this.timeSyncIntervals[this.timeSyncIntervals.length - 1]); |
||||
} |
||||
} |
||||
|
||||
stopTimeSync() { |
||||
for (let i = 0; i < this.timeSyncTimeoutIds.length; i++) { |
||||
clearTimeout(this.timeSyncTimeoutIds[i]); |
||||
} |
||||
timeSyncResponse(requestId: number, clientDiff: number, serverTime: number) { |
||||
const syncReq = this.timeSyncs[requestId]; |
||||
if (!syncReq) { |
||||
return; |
||||
} |
||||
|
||||
sendTimeSync(alsoSchedule: boolean) { |
||||
let sync = new TimeSyncRequest(); |
||||
this.socket.emit('time_sync', sync.requestId, Date.now()); |
||||
this.timeSyncs[sync.requestId] = sync; |
||||
syncReq.response(clientDiff, serverTime); |
||||
|
||||
if (alsoSchedule) { |
||||
setTimeout(() => { |
||||
this.sendTimeSync(true); |
||||
}, this.timeSyncIntervals[this.timeSyncIntervals.length - 1]); |
||||
} |
||||
for (const i in this.timeSyncs) { |
||||
if (this.timeSyncs[i].start < Date.now() - this.timeSyncTooOld) { |
||||
delete this.timeSyncs[i]; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
timeSyncResponse(requestId: number, clientDiff: number, serverTime: number) { |
||||
let syncReq = this.timeSyncs[requestId]; |
||||
if (!syncReq) return |
||||
delete this.timeSyncs[requestId]; |
||||
syncReq.response(clientDiff, serverTime); |
||||
|
||||
for (let i in this.timeSyncs) { |
||||
if (this.timeSyncs[i].start < Date.now() - this.timeSyncTooOld) { |
||||
delete this.timeSyncs[i]; |
||||
break; |
||||
} |
||||
} |
||||
// console.log(this.timeSyncs);
|
||||
// console.log('SERVER TIME', this.serverTimeOffset());
|
||||
|
||||
this.roomTime.set(roomTime()); |
||||
this.roomTime.set(calculateRoomTime()); |
||||
} |
||||
|
||||
serverTime(): number { |
||||
return Date.now() + this.serverTimeOffset(); |
||||
} |
||||
|
||||
serverTimeOffset(): number { |
||||
let num = 0; |
||||
let sum = 0; |
||||
for (const i in this.timeSyncs) { |
||||
const sync = this.timeSyncs[i]; |
||||
if (!sync.ready) continue; |
||||
sum += sync.offset; |
||||
num += 1; |
||||
} |
||||
|
||||
serverTime(): number { |
||||
return Date.now() + this.serverTimeOffset(); |
||||
if (num === 0) { |
||||
return 0; |
||||
} |
||||
|
||||
serverTimeOffset(): number { |
||||
let num = 0; |
||||
let sum = 0; |
||||
for (let i in this.timeSyncs) { |
||||
let sync = this.timeSyncs[i]; |
||||
if (!sync.ready) continue; |
||||
sum += sync.offset; |
||||
num += 1; |
||||
} |
||||
|
||||
if (num === 0) { |
||||
return 0; |
||||
} |
||||
|
||||
return Math.round(sum / num); |
||||
} |
||||
} |
||||
|
||||
let _callId = 0; |
||||
|
||||
class Call { |
||||
id: number; |
||||
name: string; |
||||
params: any; |
||||
callback: (err: any, res: any) => any; |
||||
|
||||
constructor(name: string, params: any, callback: (err: any, res: any) => void) { |
||||
this.name = name; |
||||
this.params = params; |
||||
this.id = _callId++; |
||||
this.callback = callback; |
||||
} |
||||
return Math.round(sum / num); |
||||
} |
||||
} |
||||
|
||||
let _timeSyncId = 0; |
||||
|
||||
class TimeSyncRequest { |
||||
requestId: number; |
||||
start: number; |
||||
offset: number = 0; |
||||
ready = false; |
||||
|
||||
constructor() { |
||||
this.requestId = _timeSyncId++; |
||||
this.start = Date.now(); |
||||
} |
||||
|
||||
response(clientDiff: number, serverTime: number) { |
||||
this.ready = true; |
||||
let now = Date.now(); |
||||
|
||||
let lag = now - this.start; |
||||
this.offset = serverTime - now + lag / 2; |
||||
// console.log('TIME SYNC', 'cdiff:', clientDiff, 'lag:',
|
||||
// lag, 'diff:', serverTime - now, 'offset:', this.offset);
|
||||
} |
||||
requestId: number; |
||||
start: number; |
||||
offset = 0; |
||||
ready = false; |
||||
|
||||
constructor() { |
||||
this.requestId = _timeSyncId++; |
||||
this.start = Date.now(); |
||||
} |
||||
|
||||
response(clientDiff: number, serverTime: number) { |
||||
this.ready = true; |
||||
const now = Date.now(); |
||||
|
||||
const lag = now - this.start; |
||||
this.offset = serverTime - now + lag / 2; |
||||
// console.log('TIME SYNC', 'cdiff:', clientDiff, 'lag:',
|
||||
// lag, 'diff:', serverTime - now, 'offset:', this.offset);
|
||||
} |
||||
} |
||||
|
||||
let connection: Connection = new Connection(); |
||||
// @ts-ignore
|
||||
window['connection'] = connection; |
||||
const connection: Connection = new Connection(); |
||||
export default connection; |
||||
|
||||
export function useRoom(): Room | null { |
||||
return useSub(connection.room); |
||||
return useSub(connection.room); |
||||
} |
||||
|
||||
export function useConfig(): Config | null { |
||||
return useSub(connection.config); |
||||
return useSub(connection.config); |
||||
} |
||||
|
||||
export function useRoomRunningAndReadyChanged(): Room | null { |
||||
return useSub(connection.room, (v) => [v && v.running && v.readyToParticipate]); |
||||
return useSub(connection.room, (v) => [v, v?.running, v?.readyToParticipate]); |
||||
} |
||||
|
||||
export function useTimeline(): Timeline | null { |
||||
return useSub(connection.timeline); |
||||
return useSub(connection.timeline); |
||||
} |
||||
|
||||
export function useTimelineSongFileChanged(): Timeline | null { |
||||
return useSub(connection.timeline, (v) => [v && v.songFile]); |
||||
return useSub(connection.timeline, (v) => [v?.songFile]); |
||||
} |
||||
|
||||
export function useRoomTime(): number { |
||||
return useSub(connection.roomTime); |
||||
return useSub(connection.roomTime) || 0; |
||||
} |
||||
|
||||
export function roomTime(): number { |
||||
let room = connection.room.get(); |
||||
if (!room) return 0; |
||||
return (connection.serverTime() - room.startTime) * room.speedFactor; |
||||
/** |
||||
* Calculates the current room time, adjusted for any possible server time |
||||
* offset and lag. |
||||
*/ |
||||
export function calculateRoomTime(): number { |
||||
const room = connection.room.get(); |
||||
|
||||
if (!room || typeof room.startTime === "undefined") { |
||||
return 0; |
||||
} |
||||
|
||||
return (connection.serverTime() - room.startTime) * room.speedFactor; |
||||
} |
||||
|
||||
export function useIsConnected(): boolean { |
||||
return useSub(connection.isConnected); |
||||
return useSub(connection.isConnected) || false; |
||||
} |
||||
|
@ -1 +0,0 @@ |
||||
/// <reference types="react-scripts" />
|
@ -1,78 +1,125 @@ |
||||
export interface Tick { |
||||
current: number, |
||||
next?: { |
||||
timestamp: number, |
||||
events: TimestampEvent[] |
||||
}, |
||||
nextShot?: { |
||||
timestamp: number, |
||||
count: number |
||||
} |
||||
} |
||||
|
||||
export interface Config { |
||||
availableTimelines: string[] |
||||
availableTimelines: string[]; |
||||
} |
||||
|
||||
export interface Room { |
||||
id: number, |
||||
userCount: number, |
||||
isLeader: boolean, |
||||
running: boolean, |
||||
startTime: number, |
||||
seekTime: number, |
||||
timelineName: string, |
||||
readyToParticipate: boolean, |
||||
speedFactor: number |
||||
id: number; |
||||
userCount: number; |
||||
isLeader: boolean; |
||||
running: boolean; |
||||
startTime?: number; |
||||
seekTime: number; |
||||
timelineName: string; |
||||
readyToParticipate: boolean; |
||||
speedFactor: number; |
||||
ticker: string[]; |
||||
users?: { id: string; readyToParticipate: boolean }[]; |
||||
} |
||||
|
||||
export class RoomOptions { |
||||
seekTime: number |
||||
timelineName: string |
||||
seekTime: number; |
||||
timelineName: string; |
||||
|
||||
constructor(seekTime: number, timelineName: string) { |
||||
this.seekTime = seekTime; |
||||
this.timelineName = timelineName; |
||||
} |
||||
constructor(seekTime: number, timelineName: string) { |
||||
this.seekTime = seekTime; |
||||
this.timelineName = timelineName; |
||||
} |
||||
} |
||||
|
||||
export class Timeline { |
||||
name: string |
||||
songFile: string |
||||
feed: TimelineItem[] |
||||
|
||||
constructor(obj: any) { |
||||
this.name = obj.name; |
||||
this.songFile = obj.songFile; |
||||
this.feed = obj.feed; |
||||
name: string; |
||||
songFile: string; |
||||
feed: TimelineItem[]; |
||||
|
||||
constructor(obj: any) { |
||||
this.name = obj.name; |
||||
this.songFile = obj.songFile; |
||||
this.feed = obj.feed; |
||||
|
||||
this.feed = this.feed.sort((a, b) => a.timestamp - b.timestamp); |
||||
|
||||
// Add string ids to the feed to uniquely identify them in the various
|
||||
// reactive components.
|
||||
for (let i = 0; i < this.feed.length; i++) { |
||||
this.feed[i].id = i.toString(); |
||||
for (let j = 0; j < this.feed[i].events.length; j++) { |
||||
this.feed[i].events[j].id = `${i}:${j}`; |
||||
} |
||||
} |
||||
} |
||||
|
||||
itemAtTime(time: number, type: string = ''): [TimelineItem | null, TimelineItem | null] { |
||||
let feedToSearch = type ? this.feed.filter(i => i.events.some(j => j.type == type)) : this.feed; |
||||
getTotalShotCount(): number { |
||||
let maxShot = 0; |
||||
|
||||
for (let i = 1; i < feedToSearch.length; i++) { |
||||
if (feedToSearch[i].timestamp * 1000 >= time) { |
||||
return [feedToSearch[i - 1], feedToSearch[i]]; |
||||
} |
||||
for (const item of this.feed) { |
||||
for (const event of item.events) { |
||||
if (event.type === "shot") { |
||||
maxShot = Math.max( |
||||
maxShot, |
||||
(event as TimestampEventShot).shotCount || 0 |
||||
); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return maxShot; |
||||
} |
||||
|
||||
itemAfterTime(time: number, type?: EventType): TimelineItem | undefined { |
||||
const feedToSearch = type |
||||
? this.feed.filter((i) => i.events.some((j) => j.type === type)) |
||||
: this.feed; |
||||
return feedToSearch.find((item) => item.timestamp * 1000 > time); |
||||
} |
||||
|
||||
return [feedToSearch[feedToSearch.length - 1], null]; |
||||
itemBeforeTime(time: number, type?: EventType): TimelineItem | undefined { |
||||
const feedToSearch = type |
||||
? this.feed.filter((i) => i.events.some((j) => j.type === type)) |
||||
: this.feed; |
||||
return feedToSearch.reverse().find((item) => item.timestamp * 1000 < time); |
||||
} |
||||
|
||||
eventAfterTime(time: number, type: "shot"): TimestampEventShot | undefined; |
||||
eventAfterTime(time: number, type: EventType): TimestampEvent | undefined { |
||||
const item = this.itemAfterTime(time, type); |
||||
|
||||
if (!item || !item.events.length) { |
||||
return undefined; |
||||
} |
||||
|
||||
eventAtTime(time: number, type: string = ''): [TimestampEvent | null, TimestampEvent | null] { |
||||
let [current, next] = this.itemAtTime(time, type) |
||||
return [current ? (current.events.find(i => i.type == type) || null) : null, |
||||
next ? (next.events.find(i => i.type == type) || null) : null] |
||||
return item.events.find((ev) => ev.type === type); |
||||
} |
||||
|
||||
eventBeforeTime(time: number, type: "shot"): TimestampEventShot | undefined; |
||||
eventBeforeTime(time: number, type: EventType): TimestampEvent | undefined { |
||||
const item = this.itemBeforeTime(time, type); |
||||
|
||||
if (!item || !item.events.length) { |
||||
return undefined; |
||||
} |
||||
|
||||
return item.events.find((ev) => ev.type === type); |
||||
} |
||||
} |
||||
|
||||
export interface TimelineItem { |
||||
timestamp: number, |
||||
events: TimestampEvent[] |
||||
id: string; |
||||
timestamp: number; |
||||
events: TimestampEvent[]; |
||||
} |
||||
|
||||
export interface TimestampEvent { |
||||
type: 'talk' | 'shot' | 'song' | 'time', |
||||
text: string[], |
||||
shotCount?: number |
||||
export const EVENT_PRIORITY: EventType[] = ["shot", "talk", "time", "song"]; |
||||
export type EventType = "talk" | "shot" | "song" | "time"; |
||||
|
||||
interface TimestampEventBase { |
||||
id: string; |
||||
type: EventType; |
||||
text: string[]; |
||||
} |
||||
|
||||
interface TimestampEventShot extends TimestampEventBase { |
||||
type: "shot"; |
||||
shotCount: number; |
||||
} |
||||
|
||||
export type TimestampEvent = TimestampEventBase | TimestampEventShot; |
||||
|
@ -1,11 +1,34 @@ |
||||
import {useEffect, useState} from "react"; |
||||
import { useEffect, useState } from "react"; |
||||
|
||||
export function useUpdateAfterDelay(delay: number) { |
||||
const [_, timedUpdateSet] = useState(0); |
||||
useEffect(() => { |
||||
let timeoutId = setTimeout(() => { |
||||
timedUpdateSet(v => v + 1); |
||||
}, delay); |
||||
return () => clearTimeout(timeoutId) |
||||
}) |
||||
const [, timedUpdateSet] = useState(0); |
||||
useEffect(() => { |
||||
const timeoutId = setTimeout(() => { |
||||
timedUpdateSet((v) => v + 1); |
||||
}, delay); |
||||
return () => clearTimeout(timeoutId); |
||||
}); |
||||
} |
||||
|
||||
export function useResize() { |
||||
const [dimensions, setDimensions] = useState({ |
||||
height: window.innerHeight, |
||||
width: window.innerWidth, |
||||
}); |
||||
|
||||
useEffect(() => { |
||||
const listener = (ev: UIEvent) => { |
||||
setDimensions({ |
||||
height: window.innerHeight, |
||||
width: window.innerWidth, |
||||
}); |
||||
}; |
||||
window.addEventListener("resize", listener); |
||||
|
||||
return () => { |
||||
window.removeEventListener("resize", listener); |
||||
}; |
||||
}); |
||||
|
||||
return dimensions; |
||||
} |
||||
|
@ -1,23 +1,24 @@ |
||||
import { Socket } from "socket.io-client"; |
||||
|
||||
/** |
||||
* Promisify emit. |
||||
* @param event |
||||
* @param arg |
||||
*/ |
||||
export function emit(socket: SocketIOClient.Socket, event: string, arg: any = null) { |
||||
return new Promise((resolve, reject) => { |
||||
const cb = (err: any, res: any) => { |
||||
if (err) { |
||||
return reject(err); |
||||
} |
||||
|
||||
resolve(res); |
||||
}; |
||||
export function emit(socket: Socket, event: string, arg: any = null) { |
||||
return new Promise((resolve, reject) => { |
||||
const cb = (err: any, res: any) => { |
||||
if (err) { |
||||
return reject(err); |
||||
} |
||||
|
||||
if (arg === null || typeof arg === 'undefined') { |
||||
socket.emit(event, cb); |
||||
} else { |
||||
socket.emit(event, arg, cb); |
||||
} |
||||
resolve(res); |
||||
}; |
||||
|
||||
}) |
||||
} |
||||
if (arg === null || typeof arg === "undefined") { |
||||
socket.emit(event, cb); |
||||
} else { |
||||
socket.emit(event, arg, cb); |
||||
} |
||||
}); |
||||
} |
||||
|
@ -1,40 +1,49 @@ |
||||
import {useEffect, useState} from "react"; |
||||
import { useEffect, useState } from "react"; |
||||
|
||||
export class Sub<T> { |
||||
_listeners: ((obj: T) => void)[] = []; |
||||
_current: any = null; |
||||
_listeners: ((obj: T) => void)[] = []; |
||||
_current: T | null = null; |
||||
|
||||
subscribe(listener: any) { |
||||
if (this._listeners.indexOf(listener) < 0) { |
||||
this._listeners.push(listener); |
||||
} |
||||
subscribe(listener: any) { |
||||
if (this._listeners.indexOf(listener) < 0) { |
||||
this._listeners.push(listener); |
||||
} |
||||
} |
||||
|
||||
unsubscribe(listener: any) { |
||||
let index = this._listeners.indexOf(listener); |
||||
if (index >= 0) { |
||||
this._listeners.splice(index, 1); |
||||
} |
||||
unsubscribe(listener: any) { |
||||
const index = this._listeners.indexOf(listener); |
||||
if (index >= 0) { |
||||
this._listeners.splice(index, 1); |
||||
} |
||||
} |
||||
|
||||
get() { |
||||
return this._current; |
||||
} |
||||
get(): T | null { |
||||
return this._current; |
||||
} |
||||
|
||||
set(obj: T) { |
||||
this._current = obj; |
||||
this._listeners.forEach(cb => cb(obj)); |
||||
} |
||||
set(obj: T) { |
||||
this._current = obj; |
||||
this._listeners.forEach((cb) => cb(obj)); |
||||
} |
||||
} |
||||
|
||||
export function useSub<T>(sub: Sub<T>, effectChanges: ((v: T) => any[]) | null = null): T { |
||||
const [currentState, stateSetter] = useState(sub.get()); |
||||
|
||||
useEffect(() => { |
||||
let listener = (obj: T) => stateSetter(obj); |
||||
sub.subscribe(listener); |
||||
return () => sub.unsubscribe(listener); |
||||
}, effectChanges ? effectChanges(currentState) : []); |
||||
|
||||
return currentState; |
||||
export function useSub<T>( |
||||
sub: Sub<T>, |
||||
effectChanges: ((v: T | null) => any[]) | null = null |
||||
): T | null { |
||||
const [currentState, stateSetter] = useState(sub.get()); |
||||
|
||||
useEffect( |
||||
() => { |
||||
const listener = (obj: T) => stateSetter(obj); |
||||
sub.subscribe(listener); |
||||
return () => sub.unsubscribe(listener); |
||||
// This effect uses "sub" from outside of its scope, should it be a dependency?
|
||||
// Ignore the lint warning for now.
|
||||
// eslint-disable-next-line
|
||||
}, |
||||
effectChanges ? effectChanges(currentState) : [] |
||||
); |
||||
|
||||
return currentState; |
||||
} |
||||
|
@ -0,0 +1,46 @@ |
||||
import { defineConfig } from "vite"; |
||||
import react from "@vitejs/plugin-react"; |
||||
import svgr from "vite-plugin-svgr"; |
||||
import vitePluginImp from "vite-plugin-imp"; |
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({ |
||||
// This changes the out put dir from dist to build
|
||||
// comment this out if that isn't relevant for your project
|
||||
build: { |
||||
outDir: "build", |
||||
}, |
||||
plugins: [ |
||||
react(), |
||||
svgr(), |
||||
vitePluginImp({ |
||||
libList: [ |
||||
{ |
||||
libName: "antd", |
||||
style: (name) => `antd/es/${name}/style`, |
||||
}, |
||||
], |
||||
}), |
||||
], |
||||
server: { |
||||
proxy: { |
||||
"/socket.io": { |
||||
target: "http://localhost:3001", |
||||
ws: true, |
||||
}, |
||||
}, |
||||
}, |
||||
resolve: { |
||||
alias: [{ find: /^~/, replacement: "" }], |
||||
}, |
||||
css: { |
||||
preprocessorOptions: { |
||||
less: { |
||||
modifyVars: { |
||||
"primary-color": "#304ba3", |
||||
}, |
||||
javascriptEnabled: true, |
||||
}, |
||||
}, |
||||
}, |
||||
}); |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,44 @@ |
||||
{ |
||||
"name": "centurion", |
||||
"version": "1.0.0", |
||||
"description": "Centurion: Honderd minuten... Honderd shots... Kan jij het aan?", |
||||
"engines": { |
||||
"node": ">=16", |
||||
"npm": ">=7" |
||||
}, |
||||
"workspaces": [ |
||||
"frontend", |
||||
"backend" |
||||
], |
||||
"scripts": { |
||||
"fmt": "prettier -w '**/*.{ts,tsx}'", |
||||
"check-fmt": "prettier --check '**/*.{ts,tsx}'", |
||||
"prepare": "husky install" |
||||
}, |
||||
"repository": { |
||||
"type": "git", |
||||
"url": "git@gitlab.com:studieverenigingvia/ict/centurion.git" |
||||
}, |
||||
"author": "Vereniging Informatiewetenschappen Amsterdam (V.I.A.)", |
||||
"license": "MIT", |
||||
"devDependencies": { |
||||
"@typescript-eslint/eslint-plugin": "^5.6.0", |
||||
"@typescript-eslint/parser": "^5.6.0", |
||||
"eslint": "^8.4.1", |
||||
"eslint-config-react-app": "^6.0.0", |
||||
"eslint-plugin-import": "^2.25.3", |
||||
"eslint-plugin-react": "^7.27.1", |
||||
"eslint-plugin-react-hooks": "^4.3.0", |
||||
"husky": "^7.0.4", |
||||
"lint-staged": "^12.1.2", |
||||
"prettier": "^2.5.1", |
||||
"typescript": "^4.5.2" |
||||
}, |
||||
"lint-staged": { |
||||
"backend/**/*.{ts,tsx}": "eslint --fix", |
||||
"frontend/**/*.{ts,tsx}": "eslint --fix", |
||||
"*.{ts,tsx}": [ |
||||
"prettier -w" |
||||
] |
||||
} |
||||
} |
Loading…
Reference in new issue