summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.envrc1
-rw-r--r--.gitignore6
-rw-r--r--Makefile54
-rw-r--r--assets/icon.pngbin0 -> 983 bytes
-rw-r--r--assets/iosevka-comfy-regular.ttfbin0 -> 7811628 bytes
-rw-r--r--assets/mrblue.pngbin0 -> 771 bytes
-rw-r--r--assets/mrgreen.pngbin0 -> 714 bytes
-rw-r--r--assets/voice/0.wavbin0 -> 26334 bytes
-rw-r--r--assets/voice/1.wavbin0 -> 27102 bytes
-rw-r--r--assets/voice/2.wavbin0 -> 27102 bytes
-rw-r--r--assets/voice/3.wavbin0 -> 26334 bytes
-rw-r--r--assets/voice/4.wavbin0 -> 24030 bytes
-rw-r--r--assets/voice/5.wavbin0 -> 28642 bytes
-rw-r--r--assets/voice/6.wavbin0 -> 27874 bytes
-rw-r--r--config/deploy.m44
-rw-r--r--config/extension.js2
-rw-r--r--config/test.m46
-rw-r--r--extension/background.js23
-rw-r--r--extension/main.css0
-rw-r--r--extension/manifest.dhall30
-rw-r--r--flake.lock293
-rw-r--r--flake.nix53
-rw-r--r--main.css115
-rw-r--r--src/Audio.js30
-rw-r--r--src/Audio.purs10
-rw-r--r--src/Auth.js45
-rw-r--r--src/Auth.purs29
-rw-r--r--src/Config.js4
-rw-r--r--src/Config.purs6
-rw-r--r--src/Main.purs157
-rw-r--r--src/Model.js108
-rw-r--r--src/Model.purs10
-rw-r--r--src/UI.js2
-rw-r--r--src/UI.purs13
-rw-r--r--templates/button.html17
-rw-r--r--templates/index.html40
-rw-r--r--templates/obs.html18
37 files changed, 1076 insertions, 0 deletions
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..3550a30
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..335239a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+.direnv
+.psc-ide-port
+main.js
+node_modules
+output/*
+dist/* \ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..b178e8f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,54 @@
+.PHONY: all dist deploy extension clean
+
+TEMPLATES=$(shell ls templates)
+
+all: dist $(addprefix dist/test/,$(TEMPLATES)) dist/test/assets dist/test/main.js dist/test/main.css
+
+deploy: dist $(addprefix dist/deploy/,$(TEMPLATES)) dist/deploy/assets dist/deploy/main.js dist/deploy/main.css
+ rsync -av dist/deploy/ "pub.colonq.computer:~/public_html/"
+
+dist:
+ mkdir -p dist/test
+ mkdir -p dist/deploy
+
+main.js: $(shell find src)
+ purs-nix bundle
+
+dist/%/assets: $(shell find assets) dist
+ rm -rf $@
+ mkdir -p $@
+ cp -r assets/* $@
+
+dist/%/main.js: main.js dist
+ cp $< $@
+
+dist/extension/main.css: extension/main.css dist
+ cp $< $@
+
+dist/%/main.css: main.css dist
+ cp $< $@
+
+dist/%/index.html: config/%.m4 dist index-template.html
+ sh -c "m4 $< >$@"
+
+
+define GEN_RULE
+dist/%/$(template): config/%.m4 templates/$(template)
+ sh -c "m4 $$^ >$$@"
+endef
+$(foreach template,$(TEMPLATES), $(eval $(GEN_RULE)))
+
+extension: dist dist/extension/assets dist/extension/manifest.json dist/extension/background.js dist/extension/main.js dist/extension/main.css dist/extension/config.js
+
+dist/extension/manifest.json: extension/manifest.dhall
+ dhall-to-json <$< >$@
+
+dist/extension/config.js: config/extension.js
+ cp $< $@
+
+dist/extension/%: extension/%
+ cp $< $@
+
+clean:
+ rm main.js
+ rm -r dist/
diff --git a/assets/icon.png b/assets/icon.png
new file mode 100644
index 0000000..0431e3f
--- /dev/null
+++ b/assets/icon.png
Binary files differ
diff --git a/assets/iosevka-comfy-regular.ttf b/assets/iosevka-comfy-regular.ttf
new file mode 100644
index 0000000..b474adc
--- /dev/null
+++ b/assets/iosevka-comfy-regular.ttf
Binary files differ
diff --git a/assets/mrblue.png b/assets/mrblue.png
new file mode 100644
index 0000000..8cf2a0b
--- /dev/null
+++ b/assets/mrblue.png
Binary files differ
diff --git a/assets/mrgreen.png b/assets/mrgreen.png
new file mode 100644
index 0000000..1f1edca
--- /dev/null
+++ b/assets/mrgreen.png
Binary files differ
diff --git a/assets/voice/0.wav b/assets/voice/0.wav
new file mode 100644
index 0000000..3b4884d
--- /dev/null
+++ b/assets/voice/0.wav
Binary files differ
diff --git a/assets/voice/1.wav b/assets/voice/1.wav
new file mode 100644
index 0000000..7701e71
--- /dev/null
+++ b/assets/voice/1.wav
Binary files differ
diff --git a/assets/voice/2.wav b/assets/voice/2.wav
new file mode 100644
index 0000000..619c442
--- /dev/null
+++ b/assets/voice/2.wav
Binary files differ
diff --git a/assets/voice/3.wav b/assets/voice/3.wav
new file mode 100644
index 0000000..bccbcbf
--- /dev/null
+++ b/assets/voice/3.wav
Binary files differ
diff --git a/assets/voice/4.wav b/assets/voice/4.wav
new file mode 100644
index 0000000..503c30c
--- /dev/null
+++ b/assets/voice/4.wav
Binary files differ
diff --git a/assets/voice/5.wav b/assets/voice/5.wav
new file mode 100644
index 0000000..a0288ec
--- /dev/null
+++ b/assets/voice/5.wav
Binary files differ
diff --git a/assets/voice/6.wav b/assets/voice/6.wav
new file mode 100644
index 0000000..11aceee
--- /dev/null
+++ b/assets/voice/6.wav
Binary files differ
diff --git a/config/deploy.m4 b/config/deploy.m4
new file mode 100644
index 0000000..97fc61f
--- /dev/null
+++ b/config/deploy.m4
@@ -0,0 +1,4 @@
+define(`CONFIG_SUBST', `
+globalThis.mode = 0;
+globalThis.apiServer = "https://api.colonq.computer/api";
+')
diff --git a/config/extension.js b/config/extension.js
new file mode 100644
index 0000000..44b69d3
--- /dev/null
+++ b/config/extension.js
@@ -0,0 +1,2 @@
+globalThis.mode = 1;
+globalThis.apiServer = "https://api.colonq.computer/api";
diff --git a/config/test.m4 b/config/test.m4
new file mode 100644
index 0000000..6d41963
--- /dev/null
+++ b/config/test.m4
@@ -0,0 +1,6 @@
+define(`CONFIG_SUBST', `
+globalThis.mode = 0;
+globalThis.apiServer = "http://localhost:8000/api";
+globalThis.clientID = "q486jugzn2my4iw6l181o006ugye4j"
+globalThis.authRedirectURL = "http://localhost:8000";
+')
diff --git a/extension/background.js b/extension/background.js
new file mode 100644
index 0000000..a0f4c91
--- /dev/null
+++ b/extension/background.js
@@ -0,0 +1,23 @@
+/* Retrieve any previously set cookie and send to content script */
+
+function getActiveTab() {
+ return browser.tabs.query({active: true, currentWindow: true});
+}
+
+function cookieUpdate() {
+ getActiveTab().then((tabs) => {
+ // get any previously set cookie for the current tab
+ let gettingCookies = browser.cookies.get({
+ url: tabs[0].url,
+ name: "name"
+ });
+ gettingCookies.then((cookie) => {
+ browser.tabs.sendMessage(tabs[0].id, cookie.value);
+ });
+ });
+}
+
+// update when the tab is updated
+browser.tabs.onUpdated.addListener(cookieUpdate);
+// update when the tab is activated
+browser.tabs.onActivated.addListener(cookieUpdate);
diff --git a/extension/main.css b/extension/main.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/extension/main.css
diff --git a/extension/manifest.dhall b/extension/manifest.dhall
new file mode 100644
index 0000000..cf9e7a1
--- /dev/null
+++ b/extension/manifest.dhall
@@ -0,0 +1,30 @@
+{ manifest_version = 2
+, name = "computerspotting"
+, version = "1.0"
+, description = "spot the computer"
+, icons =
+ [ { mapKey = "48", mapValue = "assets/mrgreen.png" }
+ ]
+, web_accessible_resources =
+ [ "mrgreen.png"
+ , "mrblue.png"
+ ]
+, permissions =
+ [ "webRequest"
+ , "webRequestBlocking"
+ , "tabs"
+ , "cookies"
+ , "*://*.twitch.tv/*"
+ , "*://api.colonq.computer/*"
+ ]
+, background =
+ { scripts = ["background.js"]
+ }
+, content_scripts =
+ [ { matches = ["*://*.twitch.tv/*"]
+ , js = ["config.js", "main.js"]
+ , css = ["main.css"]
+ , run_at = "document_end"
+ }
+ ]
+} \ No newline at end of file
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..a770068
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,293 @@
+{
+ "nodes": {
+ "docs-search": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1675992564,
+ "narHash": "sha256-Tk9VSogFHXtXe9O9vuCEfM/PV/S7plMIO0I++fCZn7U=",
+ "owner": "purs-nix",
+ "repo": "purescript-docs-search",
+ "rev": "35822b1d6ce65b1a07f80dd9e2caf15c3ee83e2c",
+ "type": "github"
+ },
+ "original": {
+ "owner": "purs-nix",
+ "repo": "purescript-docs-search",
+ "type": "github"
+ }
+ },
+ "flake-utils": {
+ "locked": {
+ "lastModified": 1644229661,
+ "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "flake-utils_2": {
+ "locked": {
+ "lastModified": 1618217525,
+ "narHash": "sha256-WGrhVczjXTiswQaoxQ+0PTfbLNeOQM6M36zvLn78AYg=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "c6169a2772643c4a93a0b5ac1c61e296cba68544",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "flake-utils_3": {
+ "locked": {
+ "lastModified": 1618217525,
+ "narHash": "sha256-WGrhVczjXTiswQaoxQ+0PTfbLNeOQM6M36zvLn78AYg=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "c6169a2772643c4a93a0b5ac1c61e296cba68544",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "get-flake": {
+ "locked": {
+ "lastModified": 1644686428,
+ "narHash": "sha256-zkhYsURWFrvEZLkIoBeqFBzSu+cA2u5mo6M8vq9LN7M=",
+ "owner": "ursi",
+ "repo": "get-flake",
+ "rev": "703f15558daa56dfae19d1858bb3046afe68831a",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ursi",
+ "repo": "get-flake",
+ "type": "github"
+ }
+ },
+ "lint-utils": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": [
+ "purs-nix",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1707777931,
+ "narHash": "sha256-PsPb5xMBZ9dPDP04o9vqKEUIEG80Z84/74fPuOMs0ZI=",
+ "owner": "homotopic",
+ "repo": "lint-utils",
+ "rev": "5f11e3e51d8f1aa4ed62a89e90f05953931e105a",
+ "type": "github"
+ },
+ "original": {
+ "owner": "homotopic",
+ "repo": "lint-utils",
+ "type": "github"
+ }
+ },
+ "make-shell": {
+ "locked": {
+ "lastModified": 1634940815,
+ "narHash": "sha256-P69OmveboXzS+es1vQGS4bt+ckwbeIExqxfGLjGuJqA=",
+ "owner": "ursi",
+ "repo": "nix-make-shell",
+ "rev": "8add91681170924e4d0591b22f294aee3f5516f9",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ursi",
+ "ref": "1",
+ "repo": "nix-make-shell",
+ "type": "github"
+ }
+ },
+ "make-shell_2": {
+ "locked": {
+ "lastModified": 1634940815,
+ "narHash": "sha256-P69OmveboXzS+es1vQGS4bt+ckwbeIExqxfGLjGuJqA=",
+ "owner": "ursi",
+ "repo": "nix-make-shell",
+ "rev": "8add91681170924e4d0591b22f294aee3f5516f9",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ursi",
+ "ref": "1",
+ "repo": "nix-make-shell",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1730643484,
+ "narHash": "sha256-FZN70acnL7V0gfmtLTdsi4J1o51bfaBYDTi3E7b7z3U=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "4ae2e647537bcdbb82265469442713d066675275",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs_2": {
+ "locked": {
+ "lastModified": 1704161960,
+ "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "63143ac2c9186be6d9da6035fa22620018c85932",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs_3": {
+ "locked": {
+ "lastModified": 1656549732,
+ "narHash": "sha256-eILutFZGjfk2bEzfim8S/qyYc//0S1KsCeO+OWbtoR0=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "d3248619647234b5dc74a6921bcdf6dd8323eb22",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "parsec": {
+ "locked": {
+ "lastModified": 1635533376,
+ "narHash": "sha256-/HrG0UPGnI5VdkhrNrpDiM2+nhdL6lD/bqyGtYv0QDE=",
+ "owner": "nprindle",
+ "repo": "nix-parsec",
+ "rev": "1bf25dd9c5de1257a1c67de3c81c96d05e8beb5e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nprindle",
+ "repo": "nix-parsec",
+ "type": "github"
+ }
+ },
+ "ps-tools": {
+ "inputs": {
+ "make-shell": "make-shell_2",
+ "nixpkgs": "nixpkgs_3",
+ "utils": "utils"
+ },
+ "locked": {
+ "lastModified": 1704567308,
+ "narHash": "sha256-WbFPIkKLtyQOPBUjintckKIYnfs7MvIbmfVsLRSAPlc=",
+ "owner": "purs-nix",
+ "repo": "purescript-tools",
+ "rev": "ac626313141cbee78f06eb3c5e90359f695aef9b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "purs-nix",
+ "repo": "purescript-tools",
+ "type": "github"
+ }
+ },
+ "purs-nix": {
+ "inputs": {
+ "docs-search": "docs-search",
+ "get-flake": "get-flake",
+ "lint-utils": "lint-utils",
+ "make-shell": "make-shell",
+ "nixpkgs": "nixpkgs_2",
+ "parsec": "parsec",
+ "ps-tools": "ps-tools",
+ "utils": "utils_2"
+ },
+ "locked": {
+ "lastModified": 1725998053,
+ "narHash": "sha256-qS5eWGoM/gLkC5WnEXCLUIts/kc4zzPTw5GtlYIE5Eo=",
+ "owner": "purs-nix",
+ "repo": "purs-nix",
+ "rev": "7a4d5856d4381d0ff1919b2fca073bf261ca4daf",
+ "type": "github"
+ },
+ "original": {
+ "owner": "purs-nix",
+ "ref": "ps-0.15",
+ "repo": "purs-nix",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs",
+ "ps-tools": [
+ "purs-nix",
+ "ps-tools"
+ ],
+ "purs-nix": "purs-nix"
+ }
+ },
+ "utils": {
+ "inputs": {
+ "flake-utils": "flake-utils_2"
+ },
+ "locked": {
+ "lastModified": 1656044990,
+ "narHash": "sha256-f01BB7CaOyntOab9XnpH9HD63rGcnu2iyL4M2ubs5F8=",
+ "owner": "ursi",
+ "repo": "flake-utils",
+ "rev": "f53b674a2c90f6202a2f4cd491aba121775490b5",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ursi",
+ "ref": "8",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "utils_2": {
+ "inputs": {
+ "flake-utils": "flake-utils_3"
+ },
+ "locked": {
+ "lastModified": 1656044990,
+ "narHash": "sha256-f01BB7CaOyntOab9XnpH9HD63rGcnu2iyL4M2ubs5F8=",
+ "owner": "ursi",
+ "repo": "flake-utils",
+ "rev": "f53b674a2c90f6202a2f4cd491aba121775490b5",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ursi",
+ "ref": "8",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..44fd451
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,53 @@
+{
+ description = "bundt - frontend for fig";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+ ps-tools.follows = "purs-nix/ps-tools";
+ purs-nix.url = "github:purs-nix/purs-nix/ps-0.15";
+ };
+
+ outputs = { self, nixpkgs, ... }@inputs:
+ let
+ system = "x86_64-linux";
+ pkgs = nixpkgs.legacyPackages.${system};
+ ps-tools = inputs.ps-tools.legacyPackages.${system};
+ purs-nix = inputs.purs-nix { inherit system; };
+
+ purescript = purs-nix.purs {
+ dependencies = [
+ "console"
+ "effect"
+ "prelude"
+ "random"
+ "refs"
+ "web-html"
+ "web-dom"
+ "web-uievents"
+ "canvas"
+ "argonaut"
+ "fetch"
+ "fetch-argonaut"
+ ];
+ dir = ./.;
+ srcs = [ "src" ];
+ };
+ bundt = purescript.bundle {};
+ in {
+ devShells.x86_64-linux.default = pkgs.mkShell {
+ buildInputs = [
+ pkgs.nodejs
+ (purescript.command {})
+ ps-tools.for-0_15.purescript-language-server
+ purs-nix.esbuild
+ purs-nix.purescript
+ pkgs.m4
+ pkgs.dhall
+ pkgs.dhall-json
+ ];
+ };
+ packages.x86_64-linux = {
+ default = bundt;
+ };
+ };
+}
diff --git a/main.css b/main.css
new file mode 100644
index 0000000..7714273
--- /dev/null
+++ b/main.css
@@ -0,0 +1,115 @@
+@font-face {
+ font-family: "Iosevka Comfy";
+ src: url("assets/iosevka-comfy-regular.ttf") format("truetype");
+}
+
+html {
+ font-family: "Iosevka Comfy";
+ font-size: 16pt;
+}
+
+html, body {
+ overflow: hidden;
+ margin: 0 !important;
+ padding: 0 !important;
+ height: 100%;
+ width: 100%;
+}
+
+body.lcolonq-index {
+ font-family: "Iosevka Comfy";
+ font-weight: bold;
+ color: black;
+ user-select: none;
+ image-rendering: pixelated;
+ background-color: #eeeeee;
+ background-size: 40px 40px;
+ background-image:
+ linear-gradient(to right, grey 1px, transparent 1px),
+ linear-gradient(to bottom, grey 1px, transparent 1px);
+}
+
+body.lcolonq-obs {
+ background-color: rbga(0,0,0,0);
+}
+
+#lcolonq-title {
+ position: absolute;
+ top: 2rem;
+ bottom: 0px;
+ left: 0px;
+ right: 0px;
+ text-align: center;
+ font-size: 20vw;
+}
+
+.lcolonq-letter {
+ display: inline-block;
+}
+
+.lcolonq-letter:hover {
+ color: #333333;
+}
+
+#lcolonq-subtitle {
+ font-size: 5vw;
+ margin-top: -5vw;
+ margin-right: 12vw;
+ text-align: right;
+}
+
+#lcolonq-header {
+ background-color: #cccccf;
+ position: absolute;
+ left: 0px;
+ right: 0px;
+ top: 0px;
+ height: 2rem;
+ border-bottom: 0.2rem ridge;
+}
+
+#lcolonq-header a {
+ color: black;
+}
+
+#lcolonq-header marquee {
+ color: black;
+ padding-top: 0.4rem;
+}
+
+#lcolonq-footer {
+ background-color: #cccccf;
+ position: absolute;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
+ height: 2rem;
+ border-top: 0.2rem groove;
+}
+
+#lcolonq-footer a {
+ display: inline-block;
+ color: black;
+ margin-top: 0.2rem;
+ margin-left: 0.1rem;
+ margin-right: 0.1rem;
+ padding: 0.1rem;
+ text-decoration: none;
+ border: 0.1rem outset;
+}
+
+#lcolonq-footer a:active {
+ border: 0.1rem inset;
+}
+
+#lcolonq-canvas {
+ position: absolute;
+ bottom: 2rem;
+ left: 0px;
+ width: 832px;
+ height: 832px;
+}
+
+.right {
+ float: right;
+}
diff --git a/src/Audio.js b/src/Audio.js
new file mode 100644
index 0000000..2a9ae54
--- /dev/null
+++ b/src/Audio.js
@@ -0,0 +1,30 @@
+let initialized = false;
+let ctx = null;
+let voiceTracks = null;
+
+function initializeCtx() {
+ if (!initialized) {
+ try {
+ initialized = true;
+ ctx = new window.AudioContext();
+ voiceTracks = [
+ document.getElementById("lcolonq-audio-voice-0"),
+ document.getElementById("lcolonq-audio-voice-1"),
+ document.getElementById("lcolonq-audio-voice-2"),
+ document.getElementById("lcolonq-audio-voice-3"),
+ document.getElementById("lcolonq-audio-voice-4"),
+ document.getElementById("lcolonq-audio-voice-5"),
+ document.getElementById("lcolonq-audio-voice-6"),
+ ];
+ } catch (e) {
+ initialized = false;
+ }
+ }
+}
+
+export const _playVoice = (b) => (i) => () => {
+ if (b) initializeCtx();
+ try {
+ if (initialized) voiceTracks[i].play();
+ } catch (e) {}
+};
diff --git a/src/Audio.purs b/src/Audio.purs
new file mode 100644
index 0000000..8f1f319
--- /dev/null
+++ b/src/Audio.purs
@@ -0,0 +1,10 @@
+module Audio where
+
+import Prelude
+
+import Effect (Effect)
+import Effect.Class (class MonadEffect, liftEffect)
+
+foreign import _playVoice :: Boolean -> Int -> Effect Unit
+playVoice :: forall m. MonadEffect m => Boolean -> Int -> m Unit
+playVoice b i = liftEffect $ _playVoice b i
diff --git a/src/Auth.js b/src/Auth.js
new file mode 100644
index 0000000..7254c40
--- /dev/null
+++ b/src/Auth.js
@@ -0,0 +1,45 @@
+function generateNonce() {
+ var arr = new Uint8Array(20);
+ window.crypto.getRandomValues(arr);
+ return Array.from(arr, b => b.toString(16).padStart(2, "0")).join("");
+}
+
+export const _startTwitchAuth = (clientID) => (redirectURL) => () => {
+ const nonce = generateNonce();
+ document.cookie = `authnonce=${nonce}; path=/; max-age=3000`;
+ window.location.href =
+ `https://id.twitch.tv/oauth2/authorize?response_type=id_token`
+ + `&client_id=${clientID}`
+ + `&redirect_uri=${redirectURL}`
+ + `&scope=openid`
+ + `&nonce=${nonce}`
+ + `&claims=${JSON.stringify({id_token: {preferred_username: null}})}`
+ ;
+};
+
+function getFragmentQuery() {
+ let query = new Map();
+ const hashQuery = document.location.hash.slice(1).split("&");
+ for (let equals of hashQuery) {
+ const pair = equals.split("=");
+ query.set(decodeURIComponent(pair[0]), decodeURIComponent(pair[1]));
+ }
+ return query;
+}
+
+export const _getToken = (Just) => (Nothing) => (pair) => () => {
+ const frag = getFragmentQuery();
+ const token = frag.get("id_token");
+ if (token) {
+ document.cookie = `id_token=${token}; path=/; SameSite=Strict`;
+ }
+ let id_token = null;
+ let authnonce = null;
+ for (let c of document.cookie.split("; ")) {
+ const [k, v] = c.split("=");
+ if (k === "id_token") id_token = v;
+ else if (k === "authnonce") authnonce = v;
+ }
+ if (id_token && authnonce) return Just(pair(id_token)(authnonce));
+ return Nothing;
+};
diff --git a/src/Auth.purs b/src/Auth.purs
new file mode 100644
index 0000000..2a53629
--- /dev/null
+++ b/src/Auth.purs
@@ -0,0 +1,29 @@
+module Auth where
+
+import Prelude
+
+import Config (authRedirectURL, clientID)
+import Data.Array (fold)
+import Data.Maybe (Maybe(..))
+import Data.Tuple (Tuple(..))
+import Effect (Effect)
+import Effect.Class (class MonadEffect, liftEffect)
+
+foreign import _startTwitchAuth :: String -> String -> Effect Unit
+startTwitchAuth :: forall m. MonadEffect m => m Unit
+startTwitchAuth = liftEffect $ _startTwitchAuth clientID authRedirectURL
+
+type AuthInfo = Tuple String String
+foreign import _getToken :: forall a. (a -> Maybe a) -> Maybe a -> (a -> a -> Tuple a a) -> Effect (Maybe (Tuple String String))
+getToken :: forall m. MonadEffect m => m (Maybe AuthInfo)
+getToken = liftEffect $ _getToken Just Nothing Tuple
+
+authHeader :: AuthInfo -> String
+authHeader (Tuple t n) =
+ fold
+ [ "FIG-TWITCH token=\""
+ , t
+ , "\", nonce=\""
+ , n
+ , "\""
+ ]
diff --git a/src/Config.js b/src/Config.js
new file mode 100644
index 0000000..25259c7
--- /dev/null
+++ b/src/Config.js
@@ -0,0 +1,4 @@
+export const mode = globalThis.mode;
+export const apiServer = globalThis.apiServer;
+export const clientID = globalThis.clientID;
+export const authRedirectURL = globalThis.authRedirectURL;
diff --git a/src/Config.purs b/src/Config.purs
new file mode 100644
index 0000000..536f163
--- /dev/null
+++ b/src/Config.purs
@@ -0,0 +1,6 @@
+module Config where
+
+foreign import mode :: Int
+foreign import apiServer :: String
+foreign import clientID :: String
+foreign import authRedirectURL :: String
diff --git a/src/Main.purs b/src/Main.purs
new file mode 100644
index 0000000..bb5fa57
--- /dev/null
+++ b/src/Main.purs
@@ -0,0 +1,157 @@
+module Main where
+
+import Prelude
+
+import Audio as Audio
+import Auth (AuthInfo, authHeader, getToken, startTwitchAuth)
+import Config as Config
+import Data.Array (head)
+import Data.Array as Array
+import Data.Foldable (fold)
+import Data.Maybe (Maybe(..))
+import Data.Traversable (for, for_)
+import Data.Tuple (Tuple(..))
+import Effect (Effect)
+import Effect.Aff (Aff, launchAff_)
+import Effect.Class (class MonadEffect, liftEffect)
+import Effect.Console (log)
+import Effect.Exception (throw)
+import Fetch (fetch)
+import Model (startModel)
+import UI as UI
+import Web.DOM as DOM
+import Web.DOM.DOMTokenList as DOM.DTL
+import Web.DOM.Document (doctype)
+import Web.DOM.Document as DOM.Doc
+import Web.DOM.Element as DOM.El
+import Web.DOM.Node as DOM.Node
+import Web.DOM.NodeList as DOM.NL
+import Web.DOM.NonElementParentNode as DOM.NEP
+import Web.DOM.ParentNode as DOM.P
+import Web.DOM.Text as DOM.Text
+import Web.Event.Event as Ev
+import Web.Event.EventTarget as Ev.Tar
+import Web.HTML as HTML
+import Web.HTML.HTMLDocument as HTML.Doc
+import Web.HTML.Window as HTML.Win
+
+maybeToArray :: forall a. Maybe a -> Array a
+maybeToArray (Just x) = [x]
+maybeToArray Nothing = []
+
+byId :: forall m. MonadEffect m => String -> m DOM.Element
+byId i = do
+ w <- liftEffect HTML.window
+ d <- liftEffect $ HTML.Doc.toDocument <$> HTML.Win.document w
+ liftEffect (DOM.NEP.getElementById i (DOM.Doc.toNonElementParentNode d)) >>= case _ of
+ Nothing -> liftEffect $ throw $ "could not find element with id: " <> i
+ Just e -> pure e
+
+queryAll :: forall m. MonadEffect m => String -> m (Array DOM.Element)
+queryAll q = do
+ w <- liftEffect HTML.window
+ d <- liftEffect $ HTML.Doc.toDocument <$> HTML.Win.document w
+ nl <- liftEffect (DOM.P.querySelectorAll (DOM.P.QuerySelector q) (DOM.Doc.toParentNode d))
+ ns <- liftEffect $ DOM.NL.toArray nl
+ pure $ fold $ (maybeToArray <<< DOM.El.fromNode) <$> ns
+
+query :: forall m . MonadEffect m => String -> m DOM.Element
+query q = do
+ queryAll q >>= head >>> case _ of
+ Nothing -> liftEffect $ throw $ "could not find element matching query: " <> q
+ Just x -> pure x
+
+listen :: forall m. MonadEffect m => DOM.Element -> String -> (Ev.Event -> Effect Unit) -> m Unit
+listen e ev f = do
+ l <- liftEffect $ Ev.Tar.eventListener f
+ liftEffect $ Ev.Tar.addEventListener (Ev.EventType ev) l false $ DOM.El.toEventTarget e
+
+create :: forall m. MonadEffect m => String -> Array String -> Array DOM.Element -> m DOM.Element
+create tag classes children = do
+ w <- liftEffect HTML.window
+ d <- liftEffect $ HTML.Doc.toDocument <$> HTML.Win.document w
+ el <- liftEffect $ DOM.Doc.createElement tag d
+ cl <- liftEffect $ DOM.El.classList el
+ for_ classes \c ->
+ liftEffect $ DOM.DTL.add cl c
+ for_ children \c ->
+ appendElement el c
+ pure el
+
+appendElement :: forall m. MonadEffect m => DOM.Element -> DOM.Element -> m Unit
+appendElement parent child = liftEffect $ DOM.Node.appendChild (DOM.El.toNode child) (DOM.El.toNode parent)
+
+appendText :: forall m. MonadEffect m => DOM.Element -> String -> m Unit
+appendText parent s = do
+ w <- liftEffect HTML.window
+ d <- liftEffect $ HTML.Doc.toDocument <$> HTML.Win.document w
+ n <- liftEffect $ DOM.Doc.createTextNode s d
+ liftEffect $ DOM.Node.appendChild (DOM.Text.toNode n) (DOM.El.toNode parent)
+
+setText :: forall m. MonadEffect m => DOM.Element -> String -> m Unit
+setText e s = liftEffect $ DOM.Node.setTextContent s $ DOM.El.toNode e
+
+updateSubtitle :: Aff Unit
+updateSubtitle = do
+ subtitle <- byId "lcolonq-subtitle"
+ { text: catchphrase } <- fetch (Config.apiServer <> "/catchphrase") {}
+ catchphrase >>= setText subtitle
+
+checkAuth :: AuthInfo -> Aff String
+checkAuth auth = do
+ { text: resp } <-
+ fetch (Config.apiServer <> "/check")
+ { headers:
+ { "Authorization": authHeader auth
+ }
+ }
+ resp
+
+mainHomepage :: Effect Unit
+mainHomepage = launchAff_ do
+ liftEffect $ log "hi"
+ startModel
+ marq <- byId "lcolonq-marquee"
+ { text: motd } <- fetch (Config.apiServer <> "/motd") {}
+ motd >>= setText marq
+
+ getToken >>= case _ of
+ Just a@(Tuple t n) -> do
+ liftEffect $ log t
+ liftEffect $ log n
+ checkAuth a >>= log >>> liftEffect
+ _ -> pure unit
+
+ updateSubtitle
+ subtitle <- byId "lcolonq-subtitle"
+ listen subtitle "click" \_ev -> do
+ startTwitchAuth
+ launchAff_ updateSubtitle
+
+ for_ (Array.range 0 6) \i -> do
+ letter <- byId $ "lcolonq-letter-" <> show i
+ listen letter "click" \_ev -> do
+ Audio.playVoice true i
+ listen letter "mouseover" \_ev -> do
+ Audio.playVoice false i
+
+mainExtension :: Effect Unit
+mainExtension = launchAff_ do
+ liftEffect $ log "hello from extension"
+ UI.setInterval 1000.0 do
+ e <- query ".chat-scrollable-area__message-container"
+ new <- create "div" [".chat-line__message"] []
+ appendText new "test"
+ appendElement e new
+
+mainObs :: Effect Unit
+mainObs = launchAff_ do
+ startModel
+
+main :: Effect Unit
+main = case Config.mode of
+ 0 -> mainHomepage
+ 1 -> mainExtension
+ 2 -> mainObs
+ -- 3 -> mainButton
+ _ -> throw "unknown mode"
diff --git a/src/Model.js b/src/Model.js
new file mode 100644
index 0000000..797bae4
--- /dev/null
+++ b/src/Model.js
@@ -0,0 +1,108 @@
+let canvas = document.getElementById("lcolonq-canvas");
+let socket = null;
+let currentFrame = null;
+
+async function decompress(blob) {
+ let ds = new DecompressionStream("gzip");
+ let stream = blob.stream();
+ let out = await new Response(stream.pipeThrough(ds));
+ return out.arrayBuffer();
+}
+
+function readCell(dv, base) {
+ let cell = {};
+ let o = base;
+ if (dv.getUint8(o) == 0) {
+ return [{type: "bg"}, o + 1];
+ } else {
+ cell.type = "fg";
+ cell.custom = dv.getUint8(o + 1);
+ cell.r = dv.getUint8(o + 2);
+ cell.g = dv.getUint8(o + 3);
+ cell.b = dv.getUint8(o + 4);
+ cell.g0 = dv.getUint32(o + 5);
+ if (dv.getUint8(o + 9) == 0) {
+ return [cell, o + 10];
+ } else {
+ cell.g1 = dv.getUint32(o + 10);
+ return [cell, o + 14];
+ }
+ }
+}
+
+function readKeyframe(dv, base) {
+ let ret = [];
+ let o = base;
+ for (let idx = 0; idx < (64 * 64); ++idx) {
+ let res = readCell(dv, o);
+ ret.push(res[0]);
+ o = res[1];
+ }
+ currentFrame = ret;
+}
+
+function readDiff(dv, base) {
+ if (currentFrame) {
+ let len = dv.getUint32(base);
+ let o = base + 4;
+ for (let idx = 0; idx < len; ++idx) {
+ let x = dv.getUint8(o);
+ let y = dv.getUint8(o + 1);
+ let c = readCell(dv, o + 2);
+ currentFrame[x + (y * 64)] = c[0];
+ o = c[1];
+ }
+ }
+}
+
+function readPacket(dv) {
+ if (dv.getUint8(0) == 0) {
+ readKeyframe(dv, 1);
+ } else {
+ readDiff(dv, 1);
+ }
+}
+
+function renderCellCanvas(ctx, x, y, c) {
+ if (c && c.type === "fg") {
+ let msg = c.g1 ? String.fromCodePoint(c.g0, c.g1) : String.fromCodePoint(c.g0);
+ if (msg.trim().length) {
+ ctx.fillStyle = "black";
+ ctx.fillRect(13 * y, 13 * x, 13, 13);
+ ctx.fillStyle = `rgba(${c.r}, ${c.g}, ${c.b}, 1.0)`;
+ ctx.fillText(msg, 13 * y, 13 * x + 10);
+ }
+ }
+}
+
+function renderCanvas() {
+ if (canvas.width != canvas.clientWidth) {
+ canvas.width = canvas.clientWidth;
+ }
+ if (canvas.height != canvas.clientHeight) {
+ canvas.height = canvas.clientHeight;
+ }
+ if (currentFrame) {
+ let ctx = canvas.getContext("2d");
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.font = "12px Iosevka Comfy";
+ for (let y = 0; y < 64; ++y) {
+ for (let x = 0; x < 64; ++x) {
+ renderCellCanvas(ctx, x, y, currentFrame[(x * 64) + y]);
+ }
+ }
+ }
+}
+
+export const _startModel = () => {
+ socket = new WebSocket("wss://colonq.computer/bullfrog/api/channel/listen/model");
+ socket.addEventListener("open", (ev) => {
+ console.log("connected");
+ });
+ socket.addEventListener("message", async (ev) => {
+ let arr = await decompress(ev.data);
+ let view = new DataView(arr);
+ readPacket(view);
+ renderCanvas();
+ });
+};
diff --git a/src/Model.purs b/src/Model.purs
new file mode 100644
index 0000000..a9f2888
--- /dev/null
+++ b/src/Model.purs
@@ -0,0 +1,10 @@
+module Model where
+
+import Prelude
+
+import Effect (Effect)
+import Effect.Class (class MonadEffect, liftEffect)
+
+foreign import _startModel :: Effect Unit
+startModel :: forall m. MonadEffect m => m Unit
+startModel = liftEffect _startModel
diff --git a/src/UI.js b/src/UI.js
new file mode 100644
index 0000000..0b00e5d
--- /dev/null
+++ b/src/UI.js
@@ -0,0 +1,2 @@
+export const _cheatLog = (a) => () => console.log(a);
+export const _setInterval = (delay) => (f) => () => setInterval(f, delay);
diff --git a/src/UI.purs b/src/UI.purs
new file mode 100644
index 0000000..49d797a
--- /dev/null
+++ b/src/UI.purs
@@ -0,0 +1,13 @@
+module UI where
+
+import Prelude
+import Effect (Effect)
+import Effect.Class (class MonadEffect, liftEffect)
+
+foreign import _cheatLog :: forall a. a -> Effect Unit
+cheatLog :: forall m a. MonadEffect m => a -> m Unit
+cheatLog x = liftEffect $ _cheatLog x
+
+foreign import _setInterval :: Number -> Effect Unit -> Effect Unit
+setInterval :: forall m. MonadEffect m => Number -> Effect Unit -> m Unit
+setInterval d f = liftEffect $ _setInterval d f
diff --git a/templates/button.html b/templates/button.html
new file mode 100644
index 0000000..e2f9fb0
--- /dev/null
+++ b/templates/button.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+ <title>LCOLONQ Browser Source</title>
+ <link rel="icon" href="./assets/mrgreen.png">
+ <link rel="stylesheet" type="text/css" href="./main.css">
+ <script type="module">
+CONFIG_SUBST
+ globalThis.mode = 3;
+ </script>
+ <script type="module" src="./main.js"></script>
+ </head>
+ <body class="lcolonq-button">
+ </body>
+</html>
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..8827694
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+ <title>LCOLONQ</title>
+ <link rel="icon" href="./assets/mrgreen.png">
+ <link rel="stylesheet" type="text/css" href="./main.css">
+ <script type="module">
+CONFIG_SUBST
+ </script>
+ <script type="module" src="./main.js"></script>
+ </head>
+ <body class="lcolonq-index">
+ <!-- RINGBEARER -->
+ <canvas id="lcolonq-canvas"></canvas>
+ <div id="lcolonq-title">
+ <span class="lcolonq-letter" id="lcolonq-letter-0">L</span><span class="lcolonq-letter" id="lcolonq-letter-1">C</span><span class="lcolonq-letter" id="lcolonq-letter-2">O</span><span class="lcolonq-letter" id="lcolonq-letter-3">L</span><span class="lcolonq-letter" id="lcolonq-letter-4">O</span><span class="lcolonq-letter" id="lcolonq-letter-5">N</span><span class="lcolonq-letter" id="lcolonq-letter-6">Q</span>
+ <div id="lcolonq-subtitle">
+ </div>
+ </div>
+ <div id="lcolonq-header">
+ <a href="https://twitch.tv/LCOLONQ">
+ <marquee id="lcolonq-marquee">
+ </marquee>
+ </a>
+ </div>
+ <div id="lcolonq-footer">
+ <a href="https://pub.colonq.computer/~llll/cgi-bin/ring?me=llll&offset=-1">back</a>
+ <a class="right" href="https://pub.colonq.computer/~llll/cgi-bin/ring?me=llll&offset=1">next</a>
+ </div>
+ <audio id="lcolonq-audio-voice-0" src="assets/voice/0.wav"></audio>
+ <audio id="lcolonq-audio-voice-1" src="assets/voice/1.wav"></audio>
+ <audio id="lcolonq-audio-voice-2" src="assets/voice/2.wav"></audio>
+ <audio id="lcolonq-audio-voice-3" src="assets/voice/3.wav"></audio>
+ <audio id="lcolonq-audio-voice-4" src="assets/voice/4.wav"></audio>
+ <audio id="lcolonq-audio-voice-5" src="assets/voice/5.wav"></audio>
+ <audio id="lcolonq-audio-voice-6" src="assets/voice/6.wav"></audio>
+ </body>
+</html>
diff --git a/templates/obs.html b/templates/obs.html
new file mode 100644
index 0000000..999035a
--- /dev/null
+++ b/templates/obs.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+ <title>LCOLONQ Browser Source</title>
+ <link rel="icon" href="./assets/mrgreen.png">
+ <link rel="stylesheet" type="text/css" href="./main.css">
+ <script type="module">
+CONFIG_SUBST
+ globalThis.mode = 2;
+ </script>
+ <script type="module" src="./main.js"></script>
+ </head>
+ <body class="lcolonq-obs">
+ <canvas id="lcolonq-canvas"></canvas>
+ </body>
+</html>