Web client works!

This commit is contained in:
Hamcha 2019-06-07 15:32:43 +02:00
parent 0b31cc86a2
commit 6abce0dc72
Signed by: hamcha
GPG key ID: A40413D21021EAEE
6 changed files with 382 additions and 20 deletions

View file

@ -1,6 +1,6 @@
<template> <template>
<div id="app"> <div id="app">
<section class="hero"> <section class="hero is-primary">
<div class="hero-body"> <div class="hero-body">
<div class="container"> <div class="container">
<h1 class="title">{{ title }}</h1> <h1 class="title">{{ title }}</h1>
@ -10,38 +10,68 @@
</section> </section>
<div class="container"> <div class="container">
<ConnectForm v-if="!isConnected" /> <ConnectForm v-if="!isConnected" />
<RoomList v-if="inLobby" />
<RoomLog v-if="inRoom" />
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped>
.hero {
margin-bottom: 2rem;
}
</style>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { State } from "vuex-class"; import { State } from "vuex-class";
import ConnectForm from "@/components/ConnectForm.vue"; import ConnectForm from "@/components/ConnectForm.vue";
import RoomList from "@/components/RoomList.vue"; import RoomList from "@/components/RoomList.vue";
import RoomLog from "@/components/RoomLog.vue";
import { ServerState } from "./store/server"; import { ServerState } from "./store/server";
import { RoomState } from "./store/room";
@Component({ @Component({
components: { components: {
ConnectForm, ConnectForm,
RoomList RoomList,
RoomLog
} }
}) })
export default class App extends Vue { export default class App extends Vue {
@State("server") private server!: ServerState; @State("server") private server!: ServerState;
@State("room") private room!: RoomState;
private title!: string;
private subtitle!: string;
private data() { private data() {
return { return {};
title: "Cardgage web client",
subtitle: ""
};
} }
private get isConnected() { private get isConnected(): boolean {
return this.server.connected; return this.server.connected;
} }
private get inLobby(): boolean {
return this.isConnected && !this.room.in_room;
}
private get inRoom(): boolean {
return this.isConnected && this.room.in_room;
}
private get title(): string {
return "Cardgage test client";
}
private get subtitle(): string {
if (this.inRoom && this.room.room) {
return `Inside room "${this.room.room.name}" (${
this.room.room.id
})`;
}
if (this.isConnected) {
return "Browsing " + this.server.server;
}
return "";
}
} }
</script> </script>

View file

@ -1,12 +1,101 @@
<template> <template>
<div class="container"> <div class="container">
<h1>Room list</h1> <b-table
striped
hoverable
:data="rooms"
:columns="columns"
@click="rowClicked"
></b-table>
</div> </div>
</template> </template>
<style lang="scss" scoped>
.b-table {
cursor: pointer;
}
</style>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { State, Action } from "vuex-class";
import { ServerState } from "@/store/server";
import { Room } from "@/store/room";
const columns = [
{
field: "game_id",
label: "Game"
},
{
field: "name",
label: "Name"
},
{
field: "creator",
label: "Creator"
},
{
field: "can_spectate",
label: "Spectators allowed"
},
{
field: "players",
label: "Players",
numeric: true
},
{
field: "spectators",
label: "Spectators",
numeric: true
}
];
interface RoomRow {
id: string;
game_id: string;
name: string;
creator: string;
players: number;
spectators: number;
can_spectate: string;
}
@Component({}) @Component({})
export default class RoomList extends Vue {} export default class RoomList extends Vue {
@State("server") private server!: ServerState;
@Action("joinRoom", { namespace: "server" }) private join: any;
private data() {
return {
columns
};
}
private get rooms(): RoomRow[] {
if (this.server.rooms == null) {
return [];
}
let rooms: RoomRow[] = [];
for (const room of this.server.rooms) {
rooms.push({
id: room.id,
game_id: room.game_id,
name: room.name,
creator: room.creator,
players: room.current_players,
spectators: room.current_spectators,
can_spectate: room.can_spectate ? "Yes" : "No"
});
}
return rooms;
}
private rowClicked(row: RoomRow) {
this.join({
roomid: row.id,
as_spectator: row.can_spectate == "Yes"
});
}
}
</script> </script>

View file

@ -0,0 +1,70 @@
<template>
<div class="container">
<b-table striped hoverable :data="rows" :columns="columns"></b-table>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { State } from "vuex-class";
import { RoomState } from "@/store/room";
const columns = [
{
field: "time",
label: "Time",
width: 300
},
{
field: "type",
label: "Type",
width: 100
},
{
field: "message",
label: "Message"
}
];
interface LogRow {
time: string;
type: string;
message: string;
}
@Component({})
export default class RoomLog extends Vue {
@State("room") private room!: RoomState;
private data() {
return {
columns
};
}
private get rows(): LogRow[] {
return this.room.messages.map(v => {
let fullType = v.type;
let msg = "* no message *";
if (v.type == "message" && v.message) {
fullType += "/" + v.message.type;
if (v.message.message && v.message.message != "") {
msg = v.message.message;
}
}
if (v.type == "event" && v.event) {
fullType += "/" + v.event.type;
if (v.event.message && v.event.message != "") {
msg = v.event.message;
}
}
return {
time: v.time,
type: fullType,
message: msg
};
});
}
}
</script>

View file

@ -0,0 +1,65 @@
import { Room, RoomServerMessage, RoomMessage } from "./store/room";
export interface RoomConnectionData {
ws_url: string;
auth_token: string;
}
export type MessageHandler = (msg: RoomServerMessage) => void;
export default class RoomClient {
private ws: WebSocket;
public info: Room;
private onMessage?: MessageHandler;
private buffer: RoomServerMessage[];
constructor(_ws: WebSocket, _info: Room) {
this.info = _info;
this.buffer = [];
this.ws = _ws;
this.ws.addEventListener("message", this._received.bind(this));
}
private _received(ev: MessageEvent) {
let data = JSON.parse(ev.data);
if (this.onMessage) {
return this.onMessage(data);
}
// Save messages in a buffer if no handler is set
this.buffer.push(data);
}
public setMessageHandler(handler: MessageHandler) {
// Set as handler for all future messages
this.onMessage = handler;
// If we have messages in our buffer, send them over
for (const msg of this.buffer) {
handler(msg);
}
// Empty buffer
this.buffer = [];
}
public static connect(wsdata: RoomConnectionData): Promise<RoomClient> {
return new Promise((resolve, reject) => {
let ws = new WebSocket(wsdata.ws_url);
const onMessage = (ev: MessageEvent) => {
// Unregister handler
ws.removeEventListener("message", onMessage);
// Parse message
let data = JSON.parse(ev.data);
if ("error" in data) {
reject(data.error);
}
let client = new RoomClient(ws, data.event.room);
resolve(client);
};
ws.addEventListener("message", onMessage);
ws.addEventListener("open", ev => {
// Send authentication
ws.send(JSON.stringify({ token: wsdata.auth_token }));
});
});
}
}

View file

@ -1,19 +1,83 @@
import { Module } from "vuex"; import { Module, MutationTree, ActionTree } from "vuex";
import { AppState } from "@/store/index"; import { AppState } from "@/store/index";
import RoomClient from "@/roomclient";
const namespaced: boolean = true; const namespaced: boolean = true;
export interface Room {
id: string;
game_id: string;
name: string;
creator: string;
players: string[];
spectators: string[];
current_players: number;
current_spectators: number;
can_spectate: boolean;
tags: string[];
}
export interface RoomEvent {
type: string;
player?: string;
role?: string;
room?: Room;
message?: string;
}
export interface RoomMessage {
from?: string;
to?: string;
channel: string;
type: string;
data: Object;
message?: string;
}
export interface RoomServerMessage {
time: "string";
type: "event" | "message";
event?: RoomEvent;
message?: RoomMessage;
}
export interface RoomState { export interface RoomState {
in_room: boolean; in_room: boolean;
room: string; room: Room | null;
client: RoomClient | null;
messages: RoomServerMessage[];
} }
const state: RoomState = { const state: RoomState = {
in_room: false, in_room: false,
room: "" room: null,
client: null,
messages: []
};
const mutations: MutationTree<RoomState> = {
joinedRoom(state: RoomState, ws: RoomClient) {
state.client = ws;
state.in_room = true;
state.room = ws.info;
},
messageReceived(state: RoomState, msg: RoomServerMessage) {
state.messages.push(msg);
}
};
const actions: ActionTree<RoomState, AppState> = {
setClient({ commit }, ws: RoomClient) {
ws.setMessageHandler(msg => {
commit("messageReceived", msg);
});
commit("joinedRoom", ws);
}
}; };
export const room: Module<RoomState, AppState> = { export const room: Module<RoomState, AppState> = {
namespaced, namespaced,
state state,
mutations,
actions
}; };

View file

@ -1,22 +1,28 @@
import { Module, ActionTree, MutationTree, GetterTree } from "vuex"; import { Module, ActionTree, MutationTree, GetterTree } from "vuex";
import { AppState } from "@/store/index"; import { AppState } from "@/store/index";
import { Room } from "./room";
import RoomClient from "@/roomclient";
const namespaced: boolean = true; const namespaced: boolean = true;
export interface ServerState { export interface ServerState {
server: string; server: string;
joining: boolean;
connecting: boolean; connecting: boolean;
connected: boolean; connected: boolean;
rooms: Object | null; rooms: Room[] | null;
connectionError: string; connectionError: string;
joinError: string;
} }
const state: ServerState = { const state: ServerState = {
connecting: false, connecting: false,
joining: false,
connected: false, connected: false,
server: "", server: "",
rooms: null, rooms: null,
connectionError: "" connectionError: "",
joinError: ""
}; };
const mutations: MutationTree<ServerState> = { const mutations: MutationTree<ServerState> = {
@ -26,9 +32,13 @@ const mutations: MutationTree<ServerState> = {
state.connecting = true; state.connecting = true;
}, },
beginJoin(state: ServerState) {
state.joining = true;
},
connectionDone( connectionDone(
state: ServerState, state: ServerState,
payload: { addr: string; rooms: Object } payload: { addr: string; rooms: Room[] }
) { ) {
state.connected = true; state.connected = true;
state.server = payload.addr; state.server = payload.addr;
@ -38,6 +48,11 @@ const mutations: MutationTree<ServerState> = {
connectionFailed(state: ServerState, err: Error) { connectionFailed(state: ServerState, err: Error) {
state.connecting = false; state.connecting = false;
state.connectionError = err.message; state.connectionError = err.message;
},
joinFailed(state: ServerState, err: Error) {
state.joining = false;
state.joinError = err.message;
} }
}; };
@ -48,10 +63,39 @@ const actions: ActionTree<ServerState, AppState> = {
try { try {
let req = await fetch(`${addr}/api/lobby/room/list`); let req = await fetch(`${addr}/api/lobby/room/list`);
let data = await req.json(); let data = await req.json();
commit("connectionDone", { addr, rooms: data }); commit("connectionDone", { addr, rooms: data.rooms });
} catch (err) { } catch (err) {
commit("connectionFailed", err); commit("connectionFailed", err);
} }
},
async joinRoom(
{ state, commit, dispatch, rootState },
{ roomid, as_spectator }
) {
commit("beginJoin");
// Try joining room
try {
// Ask lobby server for permission
let req = await fetch(`${state.server}/api/lobby/room/join`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({
room_id: roomid,
player_name: rootState.playerName,
as_player: !as_spectator
})
});
let data = await req.json();
// We haz permission, let's contact the room server
let ws = await RoomClient.connect(data);
dispatch("room/setClient", ws, { root: true });
} catch (err) {
commit("joinFailed", err);
}
} }
}; };