Compare commits
4 Commits
494e297224
...
master
Author | SHA1 | Date | |
---|---|---|---|
f656079c7e
|
|||
0c0dafbecf
|
|||
2cce1a3dbf
|
|||
7a0a2dc3e5
|
@@ -210,8 +210,12 @@ export class App {
|
|||||||
<input name="account" placeholder="Account name" type="text" required/>
|
<input name="account" placeholder="Account name" type="text" required/>
|
||||||
<input name="domain" placeholder="Domain" type="text" pattern="([a-zA-Z0-9_-]+\\.)+[a-zA-Z]{2,}" required/>
|
<input name="domain" placeholder="Domain" type="text" pattern="([a-zA-Z0-9_-]+\\.)+[a-zA-Z]{2,}" required/>
|
||||||
<input name="password" placeholder="Password" type="password" required/>
|
<input name="password" placeholder="Password" type="password" required/>
|
||||||
<label><input name="saveAccountName" type="checkbox"/> Save Account Name</label>
|
<fieldset>
|
||||||
<label><input name="keepSession" type="checkbox"/> Keep me signed in</label>
|
<label><input name="saveAccountName" type="checkbox"/> Save Account Name</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label><input name="keepSession" type="checkbox"/> Keep me signed in</label>
|
||||||
|
</fieldset>
|
||||||
<button type="submit">Login</button>
|
<button type="submit">Login</button>
|
||||||
</form>`;
|
</form>`;
|
||||||
|
|
||||||
|
296
src/usimp.ts
296
src/usimp.ts
@@ -1,46 +1,137 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
interface DomainJson {
|
const DNS_RE = /^([a-z0-9_-]+\.)+[a-z]{2,}$/i;
|
||||||
|
const UUID_RE = /^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
|
||||||
|
|
||||||
|
const WEBSOCKET_KEEP_ALIVE = 5 * 60; // [sec]
|
||||||
|
|
||||||
|
type DomainJson = {
|
||||||
name: string,
|
name: string,
|
||||||
id: string,
|
id: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DomainServerJson {
|
function isDomainJson(json: any): json is DomainJson {
|
||||||
|
return (
|
||||||
|
json !== null && typeof json === 'object' && json.constructor === Object &&
|
||||||
|
typeof json.name === 'string' && typeof json.id === 'string' &&
|
||||||
|
DNS_RE.test(json.name) && UUID_RE.test(json.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type DomainServerJson = {
|
||||||
host: string,
|
host: string,
|
||||||
id: string,
|
id: string,
|
||||||
priority: number,
|
priority: number,
|
||||||
weight: number,
|
weight: number,
|
||||||
protocols: {
|
protocols: {
|
||||||
https: number | undefined,
|
https?: number | null,
|
||||||
wss: number | undefined,
|
wss?: number | null,
|
||||||
http: number | undefined,
|
http?: number | null,
|
||||||
ws: number | undefined,
|
ws?: number | null,
|
||||||
udp: number | undefined
|
udp?: number | null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OutputEnvelopeJson {
|
function isDomainServerJson(json: any): json is DomainServerJson {
|
||||||
|
if (
|
||||||
|
json === null || typeof json !== 'object' || json.constructor !== Object ||
|
||||||
|
typeof json.host !== 'string' || typeof json.id !== 'string' || typeof json.priority !== 'number' ||
|
||||||
|
typeof json.weight !== 'number' ||
|
||||||
|
json.protocols === null || typeof json.protocols !== 'object' || json.protocols.constructor !== Object ||
|
||||||
|
!DNS_RE.test(json.host) || !UUID_RE.test(json.id) || json.priority < 0 || json.weight < 0
|
||||||
|
) return false;
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
(json.protocols.https === undefined || json.protocols.https === null || (
|
||||||
|
typeof json.protocols.https === 'number' && json.protocols.https > 0 && json.protocols.https <= 65535
|
||||||
|
)) && (json.protocols.wss === undefined || json.protocols.wss === null || (
|
||||||
|
typeof json.protocols.wss === 'number' && json.protocols.wss > 0 && json.protocols.wss <= 65535
|
||||||
|
)) && (json.protocols.http === undefined || json.protocols.http === null || (
|
||||||
|
typeof json.protocols.http === 'number' && json.protocols.http > 0 && json.protocols.http <= 65535
|
||||||
|
)) && (json.protocols.ws === undefined || json.protocols.ws === null || (
|
||||||
|
typeof json.protocols.ws === 'number' && json.protocols.ws > 0 && json.protocols.ws <= 65535
|
||||||
|
)) && (json.protocols.udp === undefined || json.protocols.udp === null || (
|
||||||
|
typeof json.protocols.udp === 'number' && json.protocols.udp > 0 && json.protocols.udp <= 65535
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type WellKnownJson = {
|
||||||
|
domain: DomainJson,
|
||||||
|
domain_servers: DomainServerJson[],
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWellKnownJson(json: any): json is WellKnownJson {
|
||||||
|
if (
|
||||||
|
json === null || typeof json !== 'object' || json.constructor !== Object ||
|
||||||
|
!json.hasOwnProperty('domain') || !json.hasOwnProperty('domain_servers') ||
|
||||||
|
json.domain_servers === null || typeof json.domain_servers !== 'object' ||
|
||||||
|
json.domain_servers.constructor !== Array
|
||||||
|
) return false;
|
||||||
|
|
||||||
|
return isDomainJson(json.domain) && json.domain_servers.every((item: unknown) => isDomainServerJson(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutputEnvelopeJson<T> = {
|
||||||
status: string,
|
status: string,
|
||||||
requestNr: number,
|
request_nr: number,
|
||||||
data: any,
|
data: T,
|
||||||
action: string | null | undefined,
|
action?: string | null,
|
||||||
error: {
|
error?: {
|
||||||
code: string,
|
code: string,
|
||||||
message: string | null,
|
message: string | null,
|
||||||
description: string | null,
|
description: string | null,
|
||||||
} | null,
|
} | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventJson {
|
function isOutputEnvelopeJson<T>(json: any): json is OutputEnvelopeJson<T> {
|
||||||
data: {
|
if (
|
||||||
message: string,
|
json === null || typeof json !== 'object' || json.constructor !== Object ||
|
||||||
|
typeof json.status !== 'string' || typeof json.request_nr !== 'number' || json.request_nr < 0 ||
|
||||||
|
(typeof json.action !== 'string' && json.action !== null && json.action === undefined) ||
|
||||||
|
json.data === null || typeof json.data !== 'object' || json.data.constructor !== Object
|
||||||
|
) return false;
|
||||||
|
|
||||||
|
if (json.error !== null && json.error !== undefined) {
|
||||||
|
if (
|
||||||
|
typeof json.error !== 'object' || json.error.constructor !== Object ||
|
||||||
|
typeof json.error.code !== 'string' ||
|
||||||
|
(typeof json.error.message !== 'string' && json.error.message !== null) ||
|
||||||
|
(typeof json.error.description !== 'string' && json.error.description !== null)
|
||||||
|
) return false;
|
||||||
}
|
}
|
||||||
id: string,
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WellKnownJson {
|
type EventPushEnvelopeJson = {
|
||||||
domain: DomainJson,
|
events: EventJson[],
|
||||||
domain_servers: DomainServerJson[]
|
}
|
||||||
|
|
||||||
|
function isEventPushEnvelopeJson(json: any): json is EventPushEnvelopeJson {
|
||||||
|
if (
|
||||||
|
json === null || typeof json !== 'object' || json.constructor !== Object ||
|
||||||
|
json.events === null || typeof json.events !== 'object' || json.events.constructor !== Array
|
||||||
|
) return false;
|
||||||
|
|
||||||
|
return json.events.every((item: unknown) => isEventJson(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventJson = {
|
||||||
|
id: string,
|
||||||
|
data: {
|
||||||
|
message: string,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEventJson(json: any): json is EventJson {
|
||||||
|
return !(
|
||||||
|
json === null || typeof json !== 'object' || json.constructor !== Object ||
|
||||||
|
typeof json.id !== 'string' || !UUID_RE.test(json.id) ||
|
||||||
|
json.data === null || typeof json.data !== 'object' || json.data.constructor !== Object ||
|
||||||
|
typeof json.data.message !== 'string'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Domain {
|
export class Domain {
|
||||||
@@ -53,10 +144,9 @@ export class Domain {
|
|||||||
return `[${this.name}]`;
|
return `[${this.name}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(jsonData: DomainJson, domainServers: DomainServerJson[]) {
|
constructor(json: DomainJson, domainServers: DomainServerJson[]) {
|
||||||
// FIXME check values
|
this.name = json.name;
|
||||||
this.name = jsonData.name;
|
this.id = json.id;
|
||||||
this.id = jsonData.id;
|
|
||||||
this.servers = [];
|
this.servers = [];
|
||||||
for (const domainServer of domainServers) {
|
for (const domainServer of domainServers) {
|
||||||
this.servers.push(new DomainServer(domainServer));
|
this.servers.push(new DomainServer(domainServer));
|
||||||
@@ -78,8 +168,12 @@ export class Domain {
|
|||||||
throw new Error("Invalid response");
|
throw new Error("Invalid response");
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: WellKnownJson = await response.json();
|
const data = await response.json();
|
||||||
return new Domain(data.domain, data.domain_servers);
|
if (isWellKnownJson(data)) {
|
||||||
|
return new Domain(data.domain, data.domain_servers);
|
||||||
|
} else {
|
||||||
|
throw Error("Invalid contents in usimp.json");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chooseDomainServer(): DomainServer {
|
chooseDomainServer(): DomainServer {
|
||||||
@@ -112,23 +206,29 @@ export class DomainServer {
|
|||||||
priority: number;
|
priority: number;
|
||||||
weight: number;
|
weight: number;
|
||||||
protocols: {
|
protocols: {
|
||||||
https: number | undefined,
|
https: number | null,
|
||||||
wss: number | undefined,
|
wss: number | null,
|
||||||
http: number | undefined,
|
http: number | null,
|
||||||
ws: number | undefined,
|
ws: number | null,
|
||||||
udp: number | undefined
|
udp: number | null
|
||||||
};
|
};
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return `[${this.host}/${this.priority}/${this.weight}]`;
|
return `[${this.host}/${this.priority}/${this.weight}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(jsonData: DomainServerJson) {
|
constructor(json: DomainServerJson) {
|
||||||
this.host = jsonData.host;
|
this.host = json.host;
|
||||||
this.id = jsonData.id;
|
this.id = json.id;
|
||||||
this.priority = jsonData.priority;
|
this.priority = json.priority;
|
||||||
this.weight = jsonData.weight;
|
this.weight = json.weight;
|
||||||
this.protocols = jsonData.protocols;
|
this.protocols = {
|
||||||
|
https: json.protocols.https || null,
|
||||||
|
wss: json.protocols.wss || null,
|
||||||
|
http: json.protocols.http || null,
|
||||||
|
ws: json.protocols.ws || null,
|
||||||
|
udp: json.protocols.udp || null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,9 +245,51 @@ export class Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Event {
|
export class Event {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(json: EventJson) {
|
||||||
|
this.data = {
|
||||||
|
message: json.data.message,
|
||||||
|
};
|
||||||
|
this.id = json.id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EventHandler = (event: Event) => void;
|
||||||
|
|
||||||
|
export class OutputEnvelope<T> {
|
||||||
|
status: string;
|
||||||
|
requestNr: number;
|
||||||
|
data: T;
|
||||||
|
action: string | null;
|
||||||
|
error: {
|
||||||
|
code: string,
|
||||||
|
message: string | null,
|
||||||
|
description: string | null,
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
constructor(json: OutputEnvelopeJson<T>) {
|
||||||
|
this.status = json.status;
|
||||||
|
this.requestNr = json.request_nr;
|
||||||
|
this.data = json.data;
|
||||||
|
this.action = json.action || null;
|
||||||
|
if (json.error) {
|
||||||
|
this.error = {
|
||||||
|
code: json.error.code,
|
||||||
|
message: json.error.message,
|
||||||
|
description: json.error.description,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.error = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutputEnvelopeHandler<T> = (envelope: OutputEnvelope<T>) => void;
|
||||||
|
|
||||||
export class Session {
|
export class Session {
|
||||||
domain: Domain;
|
domain: Domain;
|
||||||
server: DomainServer | null;
|
server: DomainServer | null;
|
||||||
@@ -155,37 +297,65 @@ export class Session {
|
|||||||
|
|
||||||
httpBaseUrl: string | null;
|
httpBaseUrl: string | null;
|
||||||
websocket: WebSocket | null;
|
websocket: WebSocket | null;
|
||||||
|
websocketPingInterval: number | null;
|
||||||
numRequests: number;
|
numRequests: number;
|
||||||
|
requestNumDiscriminator: number;
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
callback: (a: EventJson) => void,
|
callback: EventHandler,
|
||||||
requestNr: number | undefined,
|
requestNr: number | undefined,
|
||||||
abortController: AbortController | undefined,
|
abortController: AbortController | undefined,
|
||||||
}[];
|
}[];
|
||||||
subscriptionEndpoints: {
|
subscriptionEndpoints: {
|
||||||
callback: (a: EventJson) => void,
|
callback: EventHandler,
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
constructor(domain: Domain) {
|
constructor(domain: Domain) {
|
||||||
this.domain = domain;
|
this.domain = domain;
|
||||||
this.numRequests = 0;
|
this.numRequests = 0;
|
||||||
|
this.requestNumDiscriminator = Session.genRequestNumDiscriminator();
|
||||||
this.server = null;
|
this.server = null;
|
||||||
this.token = null;
|
this.token = null;
|
||||||
this.httpBaseUrl = null;
|
this.httpBaseUrl = null;
|
||||||
this.websocket = null;
|
this.websocket = null;
|
||||||
|
this.websocketPingInterval = null;
|
||||||
this.subscriptions = [];
|
this.subscriptions = [];
|
||||||
this.subscriptionEndpoints = [];
|
this.subscriptionEndpoints = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static genRequestNumDiscriminator(): number {
|
||||||
|
// JS uses 64-bit double precision floating point numbers.
|
||||||
|
// These can accurately represent integers up to 53 bit.
|
||||||
|
// In JS bitwise operators only operate on 32-bit integers.
|
||||||
|
// Here a 48-bit integer number is generated.
|
||||||
|
return Math.floor(Math.random() * 0x1000000) * 0x1000000;
|
||||||
|
}
|
||||||
|
|
||||||
async close(keepEndpoints: boolean = false) {
|
async close(keepEndpoints: boolean = false) {
|
||||||
await this.unsubscribeAll(keepEndpoints);
|
await this.unsubscribeAll(keepEndpoints);
|
||||||
if (this.websocket && (this.websocket.readyState !== WebSocket.CLOSING && this.websocket.readyState !== WebSocket.CLOSED)) {
|
if (this.websocket && (this.websocket.readyState !== WebSocket.CLOSING && this.websocket.readyState !== WebSocket.CLOSED)) {
|
||||||
this.websocket.close();
|
this.closeWebsocket()
|
||||||
this.subscriptions = [];
|
this.subscriptions = [];
|
||||||
this.websocket = null;
|
|
||||||
this.server = null;
|
this.server = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setWebsocketNull() {
|
||||||
|
if (this.websocketPingInterval) clearInterval(this.websocketPingInterval);
|
||||||
|
this.websocket = null;
|
||||||
|
this.websocketPingInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeWebsocket() {
|
||||||
|
if (this.websocket) {
|
||||||
|
this.websocket.close();
|
||||||
|
this.setWebsocketNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasWebsocket(): boolean {
|
||||||
|
return this.websocket !== null;
|
||||||
|
}
|
||||||
|
|
||||||
async sleep() {
|
async sleep() {
|
||||||
await this.close(true);
|
await this.close(true);
|
||||||
}
|
}
|
||||||
@@ -209,8 +379,11 @@ export class Session {
|
|||||||
this.websocket = new WebSocket(`wss://${host}:${protocols.wss}/_usimp/websocket`, ["usimp"]);
|
this.websocket = new WebSocket(`wss://${host}:${protocols.wss}/_usimp/websocket`, ["usimp"]);
|
||||||
this.websocket.addEventListener("error", (error) => {
|
this.websocket.addEventListener("error", (error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
this.websocket = null;
|
this.setWebsocketNull();
|
||||||
});
|
});
|
||||||
|
this.websocketPingInterval = setInterval(() => {
|
||||||
|
if (this.hasWebsocket()) this.send("ping", {})
|
||||||
|
}, WEBSOCKET_KEEP_ALIVE * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("https" in protocols) {
|
if ("https" in protocols) {
|
||||||
@@ -293,17 +466,17 @@ export class Session {
|
|||||||
|
|
||||||
async send(
|
async send(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
data: any,
|
data: Object,
|
||||||
timeout: number = 2000,
|
timeout: number = 2000,
|
||||||
forceHttp: boolean = false,
|
forceHttp: boolean = false,
|
||||||
abortController: AbortController | undefined = undefined,
|
abortController: AbortController | undefined = undefined,
|
||||||
cb: ((a: OutputEnvelopeJson) => void) | undefined = undefined,
|
cb: OutputEnvelopeHandler<unknown> | undefined = undefined,
|
||||||
) {
|
) {
|
||||||
this.numRequests++;
|
this.numRequests++;
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
if (!forceHttp && this.websocket !== null) {
|
if (!forceHttp && this.websocket !== null) {
|
||||||
const req_nr = this.numRequests;
|
const req_nr = this.numRequests + this.requestNumDiscriminator;
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
await this.waitForWebSocket(timeout);
|
await this.waitForWebSocket(timeout);
|
||||||
@@ -314,8 +487,12 @@ export class Session {
|
|||||||
} else {
|
} else {
|
||||||
this.websocket.addEventListener("message", (msg) => {
|
this.websocket.addEventListener("message", (msg) => {
|
||||||
const data = JSON.parse(msg.data);
|
const data = JSON.parse(msg.data);
|
||||||
if (data['request_nr'] === req_nr) {
|
if (data !== null && data.constructor === Object && data['request_nr'] === req_nr) {
|
||||||
cb(data);
|
if (isOutputEnvelopeJson<unknown>(data)) {
|
||||||
|
cb(new OutputEnvelope<unknown>(data));
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid server response");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -366,10 +543,8 @@ export class Session {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (this.websocket !== null) {
|
if (this.hasWebsocket()) this.closeWebsocket();
|
||||||
this.websocket.close();
|
|
||||||
this.websocket = null;
|
|
||||||
}
|
|
||||||
this.httpBaseUrl = null;
|
this.httpBaseUrl = null;
|
||||||
if (this.server !== null) {
|
if (this.server !== null) {
|
||||||
this.domain.invalidServers.push(this.server);
|
this.domain.invalidServers.push(this.server);
|
||||||
@@ -385,7 +560,7 @@ export class Session {
|
|||||||
const resHttp = await this.send("ping", {}, undefined, true);
|
const resHttp = await this.send("ping", {}, undefined, true);
|
||||||
result.http = resHttp.duration;
|
result.http = resHttp.duration;
|
||||||
|
|
||||||
if (this.websocket) {
|
if (this.hasWebsocket()) {
|
||||||
const resWs = await this.send("ping", {});
|
const resWs = await this.send("ping", {});
|
||||||
result.ws = resWs.duration;
|
result.ws = resWs.duration;
|
||||||
}
|
}
|
||||||
@@ -405,7 +580,7 @@ export class Session {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async newEvent(roomId: string, data: any, id: string | undefined = undefined) {
|
async newEvent(roomId: string, data: Object, id: string | undefined = undefined) {
|
||||||
return this.send("new_event", {
|
return this.send("new_event", {
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
events: [{
|
events: [{
|
||||||
@@ -415,18 +590,21 @@ export class Session {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async subscribe(cb: (a: EventJson) => void) {
|
async subscribe(cb: EventHandler) {
|
||||||
this.subscriptionEndpoints.push({callback: cb});
|
this.subscriptionEndpoints.push({callback: cb});
|
||||||
await this._subscribe(cb);
|
await this._subscribe(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _subscribe(cb: (a: EventJson) => void) {
|
private async _subscribe(cb: EventHandler) {
|
||||||
this.numRequests++;
|
if (this.hasWebsocket()) {
|
||||||
if (this.websocket !== null) {
|
|
||||||
const subscription = await this.send('subscribe', {}, 60_000, false, undefined, (res) => {
|
const subscription = await this.send('subscribe', {}, 60_000, false, undefined, (res) => {
|
||||||
if (res.action === 'push') {
|
if (res.action === 'push') {
|
||||||
for (const event of res.data.events) {
|
if (isEventPushEnvelopeJson(res.data)) {
|
||||||
cb(event);
|
for (const event of res.data.events) {
|
||||||
|
cb(new Event(event));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid server response");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -464,7 +642,7 @@ export class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async unsubscribe(requestNr: number | undefined = undefined, abortController: AbortController | undefined = undefined) {
|
private async unsubscribe(requestNr: number | undefined = undefined, abortController: AbortController | undefined = undefined) {
|
||||||
if (this.websocket !== null) {
|
if (this.hasWebsocket()) {
|
||||||
await this.send('unsubscribe', {'request_nr': requestNr})
|
await this.send('unsubscribe', {'request_nr': requestNr})
|
||||||
} else {
|
} else {
|
||||||
if (abortController) abortController.abort('unsubscribe');
|
if (abortController) abortController.abort('unsubscribe');
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: 'Arial', sans-serif;
|
font-family: 'Arial', sans-serif;
|
||||||
--bg-win: rgba(192, 192, 192, 0.25);
|
--bg-win: #C0C0C040;
|
||||||
--bg: rgba(224, 224, 224, 0.5);
|
--bg: #FFFFFF80;
|
||||||
|
--bg-border: #FFFFFFC0;
|
||||||
--fg-soft: rgba(32, 32, 32, 0.5);
|
--fg-soft: rgba(32, 32, 32, 0.5);
|
||||||
--footer-height: 2em;
|
--footer-height: 2em;
|
||||||
}
|
}
|
||||||
@@ -57,15 +58,22 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#windows > * {
|
#windows > * {
|
||||||
padding: 0.5em 1em;
|
padding: 1em 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.window-login,
|
div.window-login,
|
||||||
div.window-welcome {
|
div.window-welcome {
|
||||||
max-width: 650px;
|
|
||||||
margin: calc(max(var(--vh, 1vh) * 25, 8em) - 8em) auto 1em auto;
|
margin: calc(max(var(--vh, 1vh) * 25, 8em) - 8em) auto 1em auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.window-login {
|
||||||
|
max-width: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.window-welcome {
|
||||||
|
max-width: 650px;
|
||||||
|
}
|
||||||
|
|
||||||
div.window-login h1,
|
div.window-login h1,
|
||||||
div.window-welcome h1 {
|
div.window-welcome h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -93,23 +101,23 @@ footer div {
|
|||||||
|
|
||||||
form {
|
form {
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin: 1.5em auto;
|
margin: 1.5em auto 1em auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
form input,
|
form input:is([type=text], [type=password]),
|
||||||
form button,
|
form button,
|
||||||
|
form fieldset,
|
||||||
a.button,
|
a.button,
|
||||||
form div {
|
form div {
|
||||||
border: 1px solid var(--bg);
|
border: 1px solid var(--bg-border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
outline: none;
|
|
||||||
padding: 0.5em 1em;
|
padding: 0.5em 1em;
|
||||||
display: block;
|
display: block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 1em auto;
|
margin: 1em auto;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
transition: border-color 0.125s, background-color 0.125s;
|
transition: border-color 0.0625s, background-color 0.0625s;
|
||||||
}
|
}
|
||||||
|
|
||||||
form input,
|
form input,
|
||||||
@@ -132,8 +140,9 @@ a.button {
|
|||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
form input,
|
form input:is([type=text], [type=password]),
|
||||||
form button {
|
form button,
|
||||||
|
form fieldset {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user