Add web/ — PTY+WebSocket server, xterm.js PWA client, Dockerfile, and PyInstaller notes
This commit is contained in:
30
web/static/generate_icons.py
Normal file
30
web/static/generate_icons.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Generate solid-colour placeholder PNG icons for the PWA manifest."""
|
||||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _chunk(chunk_type, data):
|
||||
c = chunk_type + data
|
||||
crc = struct.pack(">I", zlib.crc32(c) & 0xFFFFFFFF)
|
||||
return struct.pack(">I", len(data)) + c + crc
|
||||
|
||||
|
||||
def solid_png(width, height, r, g, b):
|
||||
header = b"\x89PNG\r\n\x1a\n"
|
||||
ihdr = _chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0))
|
||||
raw = bytearray()
|
||||
for _ in range(height):
|
||||
raw.append(0)
|
||||
raw.extend(bytes([r, g, b]) * width)
|
||||
idat = _chunk(b"IDAT", zlib.compress(bytes(raw)))
|
||||
iend = _chunk(b"IEND", b"")
|
||||
return header + ihdr + idat + iend
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
dst = Path(__file__).parent
|
||||
for size in (192, 512):
|
||||
path = dst / f"icon-{size}.png"
|
||||
path.write_bytes(solid_png(size, size, 0x1A, 0x1A, 0x2E))
|
||||
print(f"Created {path.name} ({size}x{size})")
|
||||
BIN
web/static/icon-192.png
Normal file
BIN
web/static/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 547 B |
BIN
web/static/icon-512.png
Normal file
BIN
web/static/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
53
web/static/index.html
Normal file
53
web/static/index.html
Normal file
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Kanjin — A text adventure RPG based on the D&D 5e SRD">
|
||||
<meta name="theme-color" content="#1a1a2e">
|
||||
<link rel="manifest" href="/static/manifest.json">
|
||||
<link rel="icon" href="/static/icon-192.png">
|
||||
<link rel="apple-touch-icon" href="/static/icon-192.png">
|
||||
<title>Kanjin</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css" rel="stylesheet">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { width: 100%; height: 100%; background: #1a1a2e; overflow: hidden; }
|
||||
#terminal { width: 100%; height: 100%; padding: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="terminal"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
||||
<script>
|
||||
(function(){
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: 15,
|
||||
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
||||
theme: { background: '#1a1a2e', foreground: '#e0e0e0', cursor: '#e0e0e0' }
|
||||
});
|
||||
const fit = new FitAddon.FitAddon();
|
||||
term.loadAddon(fit);
|
||||
term.open(document.getElementById('terminal'));
|
||||
fit.fit();
|
||||
|
||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const ws = new WebSocket(proto + '//' + location.host + '/ws');
|
||||
|
||||
ws.onopen = () => {
|
||||
term.focus();
|
||||
term.onData(data => ws.send(data));
|
||||
term.onResize(({ cols, rows }) => {
|
||||
ws.send(JSON.stringify({ resize: [cols, rows] }));
|
||||
});
|
||||
};
|
||||
|
||||
ws.onmessage = (e) => { term.write(e.data); };
|
||||
ws.onclose = () => { term.write('\r\n\x1b[33m[Connection closed. Refresh to restart.]\x1b[0m\r\n'); };
|
||||
window.addEventListener('resize', () => fit.fit());
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
13
web/static/manifest.json
Normal file
13
web/static/manifest.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Kanjin",
|
||||
"short_name": "Kanjin",
|
||||
"description": "A text adventure RPG based on the D&D 5e SRD",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#1a1a2e",
|
||||
"theme_color": "#1a1a2e",
|
||||
"icons": [
|
||||
{ "src": "/static/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/static/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
||||
]
|
||||
}
|
||||
22
web/static/sw.js
Normal file
22
web/static/sw.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const CACHE = "kanjin-v1";
|
||||
const PRECACHE = ["/", "/static/manifest.json"];
|
||||
|
||||
self.addEventListener("install", (e) => {
|
||||
e.waitUntil(
|
||||
caches.open(CACHE).then((c) => c.addAll(PRECACHE)).then(() => self.skipWaiting())
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (e) => {
|
||||
e.waitUntil(
|
||||
caches.keys().then((keys) =>
|
||||
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
|
||||
).then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (e) => {
|
||||
e.respondWith(
|
||||
caches.match(e.request).then((r) => r || fetch(e.request))
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user