feat: setup project
This commit is contained in:
@@ -0,0 +1 @@
|
||||
VITE_WS_URL=ws://localhost:3000/ws
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Idle XBOCT — overlay</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@idle/overlay",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@idle/render": "workspace:*",
|
||||
"@idle/shared": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
"svelte": "^5.0.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { mountWorld, type WorldHandle } from '@idle/render';
|
||||
import { connectWorld, type ConnectHandlers, type NetStatus } from './net';
|
||||
|
||||
const STATUS_TEXT: Record<NetStatus, string> = {
|
||||
connecting: 'подключение…',
|
||||
online: 'онлайн',
|
||||
reconnecting: 'переподключение…',
|
||||
};
|
||||
|
||||
let host = $state<HTMLDivElement | undefined>(undefined);
|
||||
let status = $state<NetStatus>('connecting');
|
||||
|
||||
onMount(() => {
|
||||
let world: WorldHandle | undefined;
|
||||
let disposeNet: (() => void) | undefined;
|
||||
|
||||
if (host) {
|
||||
void mountWorld(host, { backgroundAlpha: 0 }).then((w) => {
|
||||
world = w;
|
||||
const handlers: ConnectHandlers = {
|
||||
onStatus: (s) => {
|
||||
status = s;
|
||||
},
|
||||
onSnapshot: (snap) => world?.applySnapshot(snap.avatars),
|
||||
};
|
||||
disposeNet = connectWorld(handlers);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
disposeNet?.();
|
||||
world?.destroy();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="world" bind:this={host}></div>
|
||||
<div class="badge">{STATUS_TEXT[status]}</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
}
|
||||
.world {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
font: 12px/1.4 monospace;
|
||||
color: #9fb0c3;
|
||||
background: rgba(16, 19, 26, 0.55);
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
/* прозрачность обязательна: канвас лежит поверх картинки стрима в OBS */
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { mount } from 'svelte';
|
||||
import './app.css';
|
||||
import App from './App.svelte';
|
||||
|
||||
export default mount(App, { target: document.getElementById('app')! });
|
||||
@@ -0,0 +1,61 @@
|
||||
import { PROTOCOL_VERSION, type ClientMessage, type ServerMessage } from '@idle/shared';
|
||||
|
||||
export type Snapshot = Extract<ServerMessage, { t: 'snapshot' }>;
|
||||
export type NetStatus = 'connecting' | 'online' | 'reconnecting';
|
||||
|
||||
export function wsUrl(): string {
|
||||
const fromEnv = import.meta.env.VITE_WS_URL;
|
||||
if (typeof fromEnv === 'string' && fromEnv.length > 0) return fromEnv;
|
||||
// сборка раздаётся самим сервером — тот же origin
|
||||
const scheme = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
return `${scheme}://${location.host}/ws`;
|
||||
}
|
||||
|
||||
export interface ConnectHandlers {
|
||||
onStatus: (status: NetStatus) => void;
|
||||
onSnapshot: (snap: Snapshot) => void;
|
||||
}
|
||||
|
||||
/** Подключение к миру с автопереподключением; возвращает функцию закрытия. */
|
||||
export function connectWorld(handlers: ConnectHandlers): () => void {
|
||||
let ws: WebSocket | null = null;
|
||||
let disposed = false;
|
||||
let attempt = 0;
|
||||
|
||||
const open = (): void => {
|
||||
if (disposed) return;
|
||||
handlers.onStatus(attempt === 0 ? 'connecting' : 'reconnecting');
|
||||
ws = new WebSocket(wsUrl());
|
||||
|
||||
ws.onopen = () => {
|
||||
attempt = 0;
|
||||
const hello: ClientMessage = { t: 'hello', v: PROTOCOL_VERSION };
|
||||
ws?.send(JSON.stringify(hello));
|
||||
};
|
||||
ws.onmessage = (ev) => {
|
||||
let msg: ServerMessage;
|
||||
try {
|
||||
msg = JSON.parse(String(ev.data)) as ServerMessage;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (msg.t === 'welcome') {
|
||||
handlers.onStatus('online');
|
||||
} else if (msg.t === 'snapshot') {
|
||||
handlers.onSnapshot(msg);
|
||||
}
|
||||
};
|
||||
ws.onclose = () => {
|
||||
if (disposed) return;
|
||||
attempt += 1;
|
||||
setTimeout(open, Math.min(500 * 2 ** Math.min(attempt, 4), 8000));
|
||||
};
|
||||
ws.onerror = () => ws?.close();
|
||||
};
|
||||
|
||||
open();
|
||||
return () => {
|
||||
disposed = true;
|
||||
ws?.close();
|
||||
};
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src", "vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
server: { port: 5173, strictPort: true },
|
||||
});
|
||||
Reference in New Issue
Block a user