diff --git a/bun.lock b/bun.lock index 4084c0d..0d45d2e 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "name": "ipv4.army", "dependencies": { + "microlight": "^0.0.7", "tsx-dom": "^3.1.0", }, "devDependencies": { @@ -11,7 +12,7 @@ "@types/bun": "latest", }, "peerDependencies": { - "typescript": "^5", + "typescript": "^5.8.3", }, }, }, @@ -40,6 +41,8 @@ "bun-types": ["bun-types@1.2.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-b5ITZMnVdf3m1gMvJHG+gIfeJHiQPJak0f7925Hxu6ZN5VKA8AGy4GZ4lM+Xkn6jtWxg5S3ldWvfmXdvnkp3GQ=="], + "microlight": ["microlight@0.0.7", "", {}, "sha512-kigwsJYoy4mAMkGZpS839/KZ5WWQQm4TzD+eIjR5leS5H0j+EhExvK0Z2Or2ewkBR/t7/AHHhxRyeXi1kurG0g=="], + "tsx-dom": ["tsx-dom@3.1.0", "", { "dependencies": { "tsx-dom-types": "2.1.0" } }, "sha512-PGN7iL6zNC4Jj7bA1groSIz5mFB3Rr+SeoywZk2g4+c9uV8wwzCf+5tFQ8SyZxQIBHech3ueB0KxV3OFieqhOA=="], "tsx-dom-types": ["tsx-dom-types@2.1.0", "", {}, "sha512-pZaMTrMRNom+D1b82K+1cWVMuogXrD/ANI42UYxilw27tF+tDCgj7GrD1XLmCxbHPDO2zxfmFuaz04KIEfWydQ=="], diff --git a/index.ts b/index.ts index 231be62..99a276e 100644 --- a/index.ts +++ b/index.ts @@ -4,25 +4,37 @@ import pkg from "./package.json"; const development = process.env.NODE_ENV === "development"; -require("fs/promises").rm("./dist", { recursive: true, force: true }).catch(() => { +let heartrate = 0; +let lanyard = {}; + +require("node:fs/promises").rm("./dist", { recursive: true, force: true }).catch(() => { // ignore }); -await build({ - entrypoints: ['./src/index.html'], - outdir: './dist', - minify: !development, - sourcemap: (development ? "inline" : "none"), - splitting: true, - publicPath: "/assets/", - loader: { - ".woff2": "file" - }, -}) +const buildWeb = async () => { + return await build({ + entrypoints: ['./src/index.html'], + outdir: './dist', + minify: !development, + sourcemap: (development ? "inline" : "none"), + splitting: true, + publicPath: "/assets/", + loader: { + ".woff2": "file" + }, + }) +} -serve({ +if (!development) { + await buildWeb() +} + +const server = serve({ routes: { "/": async () => { + if (development) { + await buildWeb() + } return new Response(gzipSync(await file("./dist/index.html").arrayBuffer()), { headers: { "Content-Type": "text/html", @@ -109,14 +121,12 @@ serve({ } }) }, - "/api/status": () => { - return new Response(gzipSync(JSON.stringify({ data: process.uptime() })), { - headers: { - "Content-Type": "application/json", - "Cache-Control": "no-cache", - "Content-Encoding": "gzip", - } - }) + "/api/ws": (req, server) => { + if (server.upgrade(req)) { + return; + } + + return new Response("Upgrade failed", { status: 500 }); }, "/api/gc": () => { gc(true) @@ -129,6 +139,109 @@ serve({ }) }, }, + websocket: { + open: async (ws) => { + ws.subscribe("lanyard"); + ws.send(JSON.stringify({ type: "lanyard", data: lanyard }), true); + + ws.subscribe("hyperate"); + ws.send(JSON.stringify({ type: "hyperate", data: { hr: heartrate } }), true); + }, + message: async (ws, message) => { + ws.send(JSON.stringify({ type: "echo", data: message }), true) + }, + close: async (ws) => { + console.log("WebSocket closed", ws.id); + }, + drain: async (ws) => { + console.log("WebSocket drain", ws.id); + }, + }, development, port: 3000, }); + +const lanyardSocket = new WebSocket("wss://lanyard.creations.works/socket"); + +const setLanyard = (data: object) => { + lanyard = data; + + return server.publish("lanyard", JSON.stringify({ type: "lanyard", data }), true); +} + +lanyardSocket.onmessage = ({ data }) => { + data = JSON.parse(data); + + switch (data.op) { + case 0: { + setLanyard(data.d) + break; + } + case 1: { + lanyardSocket.send(JSON.stringify({ + op: 2, + d: { + subscribe_to_id: "1273447359417942128" + } + })) + break; + } + } +} + +const hyperate = new WebSocket( + "wss://app.hyperate.io/socket/websocket?token=wv39nM6iyrNJulvpmMQrimYPIXy2dVrYRjkuHpbRapKT2VSh65ngDGHdCdCtmEN9", +); + +let hrTimeout: ReturnType; + +const setHeartrate = async (hr: number) => { + heartrate = hr; + + return server.publish("hyperate", JSON.stringify({ type: "hyperate", data: { hr } }), true); +} + +const setHrInterval = () => { + hrTimeout = setTimeout(() => { + setHeartrate(0); + }, 6000); +}; + +hyperate.onopen = () => { + hyperate.send( + JSON.stringify({ + topic: "hr:0BCA", + event: "phx_join", + payload: {}, + ref: 0, + }), + ); + + setInterval(() => { + hyperate.send( + JSON.stringify({ + topic: "phoenix", + event: "heartbeat", + payload: {}, + ref: 0, + }), + ); + }, 10000); + + return setHrInterval(); +}; + +hyperate.onmessage = ({ data }) => { + const { event, payload } = JSON.parse(data); + switch (event) { + case "hr_update": { + clearTimeout(hrTimeout); + setHrInterval(); + setHeartrate(payload.hr); + break; + } + default: { + break; + } + } +}; \ No newline at end of file diff --git a/package.json b/package.json index 8b8e032..a5b03ce 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "private": true, "type": "module", "dependencies": { + "microlight": "^0.0.7", "tsx-dom": "^3.1.0" } } \ No newline at end of file diff --git a/src/App.css b/src/App.css index e69de29..7bcd83f 100644 --- a/src/App.css +++ b/src/App.css @@ -0,0 +1,59 @@ +.scanlines { + overflow: hidden; +} + +.scanlines:before, +.scanlines:after { + display: block; + pointer-events: none; + content: ""; + position: absolute; +} + +.scanlines:before { + width: 100%; + height: 2px; + z-index: 2147483649; + background: rgba(0, 0, 0, 0.3); + opacity: 0.75; + animation: scanline 6s linear infinite; +} + +.scanlines:after { + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 2147483648; + background: linear-gradient(to bottom, + transparent 50%, + rgba(0, 0, 0, 0.3) 51%); + background-size: 100% 4px; + animation: scanlines 1s steps(60) infinite; +} + +/* ANIMATE UNIQUE SCANLINE */ +@keyframes scanline { + 0% { + transform: translate3d(0, 200000%, 0); + } +} + +@keyframes scanlines { + 0% { + background-position: 0 50%; + } +} + +div { + margin: 0; + padding: 0; +} + +div.scanlines { + position: absolute; +} + +.microlight>span:nth-child(6) { + color: var(--status-color); +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 7ac8a95..58cdc07 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,15 @@ +import Lanyard from './components/Lanyard'; +import Hyperate from './components/Hyperate'; + export default () => { - return
Hello, World!
+ return
+

seth> cat ./about.txt

+

A Dedicated Backend Developer, with a passion for high-fidelity audio, gaming, and web development.

+ +

seth> curl /tmp/discord-ipc

+

+ +

seth> cat /tmp/heartrate

+

+
} \ No newline at end of file diff --git a/src/Socket.ts b/src/Socket.ts new file mode 100644 index 0000000..c472067 --- /dev/null +++ b/src/Socket.ts @@ -0,0 +1,38 @@ +const { protocol, host } = window.location; + +class Socket extends EventTarget { + private _socket: WebSocket; + + constructor(url: string) { + super(); + + this._socket = new WebSocket(url); + this._socket.onmessage = (event) => { + const { type, data } = JSON.parse(event.data); + + switch (type) { + case "lanyard": { + this.emitLanyard(data); + break; + } + case "hyperate": { + this.emitHyperate(data.hr); + break; + } + } + }; + + setInterval(() => { + this._socket.send("ping"); + }, 10000); + } + + emitLanyard(lanyard: object) { + this.dispatchEvent(new CustomEvent('lanyard', { detail: lanyard })); + } + emitHyperate(heartRate: number) { + this.dispatchEvent(new CustomEvent('hyperate', { detail: heartRate })); + } +} + +export default new Socket(`${protocol.replace("http", "ws")}//${host}/api/ws`); \ No newline at end of file diff --git a/src/components/Hyperate/index.tsx b/src/components/Hyperate/index.tsx new file mode 100644 index 0000000..a8b220b --- /dev/null +++ b/src/components/Hyperate/index.tsx @@ -0,0 +1,19 @@ +import { createRef } from "tsx-dom"; +import microlight from "microlight"; + +import socket from "../../Socket"; + +export default () => { + const paragraph = createRef(); + + socket.addEventListener('hyperate', (event: Event) => { + const heartRate = (event as CustomEvent).detail; + if (paragraph.current) { + paragraph.current.innerText = `${heartRate} BPM`; + } + microlight.reset(); + }); + return
+

0 BPM

+
; +} \ No newline at end of file diff --git a/src/components/Lanyard/index.tsx b/src/components/Lanyard/index.tsx new file mode 100644 index 0000000..01381df --- /dev/null +++ b/src/components/Lanyard/index.tsx @@ -0,0 +1,40 @@ +import { createRef } from "tsx-dom"; +import microlight from "microlight"; + +import socket from "../../Socket"; + +const statusTypes: { [key: string]: string } = { + online: "green", + idle: "yellow", + dnd: "red", + invisible: "inherent", + offline: "inherent", +} + +const activityTypes: { [key: number]: string } = { + 0: "Playing", + 1: "Streaming", + 2: "Listening to", + 3: "Watching", + 4: "Custom", + 5: "Competing in", +} + +export default () => { + const paragraph = createRef(); + + socket.addEventListener('lanyard', (event: Event) => { + const lanyard = (event as CustomEvent).detail; + if (paragraph.current) { + paragraph.current.style = `--status-color: ${statusTypes[lanyard.discord_status]};`; + paragraph.current.innerText = JSON.stringify({ + status: lanyard.discord_status, + activities: lanyard.activities.map((act: { type: number, name: string }) => { return `${activityTypes[act.type]} ${act.name}` }), + }, null, 1); + } + microlight.reset(); + }); + return
+

{JSON.stringify({})}

+
; +} \ No newline at end of file diff --git a/src/index.css b/src/index.css index d4cf443..abb8639 100644 --- a/src/index.css +++ b/src/index.css @@ -1,28 +1,18 @@ @import "./App.css"; -:root { - --color-background-primary: #1a1d1f; - --color-background-secondary: #24282b; - --color-background-tertiary: #222527; - - --color-text-primary: #DEDEDE; - --color-text-secondary: #ACACAC; - --color-link: #7289DA; - --color-link-hover: #4E5D94; - --color-link-active: #5B6EAE; -} - html, head, body { margin: 0; padding: 0; font-family: 'Circular Std', sans-serif; + height: 100vh; + width: 100vw; } body { - background-color: var(--color-background-primary); - color: var(--color-text-primary); - font-size: 16px; - line-height: 1.5; + color: #DEDEDE; + font: 2vh Inconsolata, monospace; + text-shadow: 0 0 5px #C8C8C8; + background: radial-gradient(at bottom right, rgba(0, 150, 0, 0.1) 0%, rgba(0, 0, 0, 1) 100%); } \ No newline at end of file diff --git a/src/index.html b/src/index.html index 157ab42..310bf9d 100644 --- a/src/index.html +++ b/src/index.html @@ -16,7 +16,7 @@ - + diff --git a/src/index.tsx b/src/index.tsx index f15f4f4..b40f99d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,7 +2,9 @@ import "tsx-dom"; import App from './App'; -document.getElementById("font")!.innerText = `@font-face { +const font = document.getElementById("font") +if (font) { + font.innerText = `@font-face { font-family: 'Circular Std'; src: url('/public/font/Black.woff2') format('woff2'); font-weight: black; @@ -65,7 +67,7 @@ document.getElementById("font")!.innerText = `@font-face { font-style: italic; font-display: swap }` - +} document.body.appendChild(); // You're garbage, let me collect you.