From a2b8ce3cdf43c7ca170080ebec8c7c75073b3351 Mon Sep 17 00:00:00 2001 From: Boidushya Date: Fri, 6 Mar 2026 04:25:56 +0530 Subject: [PATCH 01/11] feat: add kawarp as visualizer --- index.html | 1 + js/visualizer.js | 43 ++++++--- js/visualizers/kawarp.js | 200 +++++++++++++++++++++++++++++++++++++++ package-lock.json | 14 ++- package.json | 1 + 5 files changed, 245 insertions(+), 14 deletions(-) create mode 100644 js/visualizers/kawarp.js diff --git a/index.html b/index.html index 07cbe4f..91f22f9 100644 --- a/index.html +++ b/index.html @@ -3662,6 +3662,7 @@ +
diff --git a/js/visualizer.js b/js/visualizer.js index f7af63a..2e5809a 100644 --- a/js/visualizer.js +++ b/js/visualizer.js @@ -4,6 +4,7 @@ import { LCDPreset } from './visualizers/lcd.js'; import { ParticlesPreset } from './visualizers/particles.js'; import { UnknownPleasuresWebGL } from './visualizers/unknown_pleasures_webgl.js'; import { ButterchurnPreset } from './visualizers/butterchurn.js'; +import { KawarpPreset } from './visualizers/kawarp.js'; import { audioContextManager } from './audio-context.js'; export class Visualizer { @@ -23,6 +24,7 @@ export class Visualizer { particles: new ParticlesPreset(), 'unknown-pleasures': new UnknownPleasuresWebGL(), butterchurn: new ButterchurnPreset(), + kawarp: new KawarpPreset(), }; this.activePresetKey = visualizerSettings.getPreset(); @@ -87,10 +89,13 @@ export class Visualizer { const type = preset.contextType || '2d'; const currentType = this._currentContextType; - // If context type changed, we need to recreate the canvas - // (you can't get a different context type from the same canvas) - if (this.ctx && currentType !== type) { - // Clone and replace canvas to get fresh context + // Clone the canvas to get a fresh context when switching context types, + // or when the previous preset grabbed its own context (managesOwnContext) + const needsClone = + (this.ctx && currentType !== type) || + (!this.ctx && currentType && currentType !== type); + + if (needsClone) { const parent = this.canvas.parentElement; const newCanvas = this.canvas.cloneNode(true); parent.replaceChild(newCanvas, this.canvas); @@ -98,6 +103,12 @@ export class Visualizer { this.ctx = null; } + // Kawarp grabs its own WebGL context, so we skip this + if (preset.managesOwnContext) { + this._currentContextType = type; + return; + } + if (this.ctx) return; if (type === 'webgl') { @@ -141,16 +152,19 @@ export class Visualizer { this.audioContext.resume(); } - // Initialize Butterchurn if it's the active preset - if (this.activePresetKey === 'butterchurn' && this.activePreset.lazyInit) { - const sourceNode = audioContextManager.getSourceNode(); - this.activePreset.lazyInit(this.canvas, this.audioContext, sourceNode); - } - + // Set canvas dimensions before preset init so WebGL framebuffers are created at correct size this.resize(); window.addEventListener('resize', this._resizeBound); this.canvas.style.display = 'block'; + // Initialize presets that need lazy init (Butterchurn, Kawarp) + if (this.activePreset.lazyInit) { + const sourceNode = audioContextManager.getSourceNode(); + this.activePreset.lazyInit(this.canvas, this.audioContext, sourceNode).then(() => { + this.resize(); + }); + } + this.animate(); } @@ -281,10 +295,13 @@ export class Visualizer { this.initContext(); this.resize(); - // Initialize Butterchurn if switching to it - if (key === 'butterchurn' && this.presets[key].lazyInit && this.audioContext) { + // Initialize presets that need lazy init (Butterchurn, Kawarp) + if (this.presets[key].lazyInit && this.audioContext) { const sourceNode = audioContextManager.getSourceNode(); - this.presets[key].lazyInit(this.canvas, this.audioContext, sourceNode); + this.presets[key].lazyInit(this.canvas, this.audioContext, sourceNode).then(() => { + // Re-resize after async init so framebuffers match canvas size + this.resize(); + }); } } } diff --git a/js/visualizers/kawarp.js b/js/visualizers/kawarp.js new file mode 100644 index 0000000..a8122ea --- /dev/null +++ b/js/visualizers/kawarp.js @@ -0,0 +1,200 @@ +// js/visualizers/kawarp.js + +const KAWARP_DEFAULTS = { + warpIntensity: 1, + blurPasses: 8, + animationSpeed: 1, + transitionDuration: 1000, + saturation: 1.5, + dithering: 0.008, + scale: 1.25, +}; + +const BEAT_THRESHOLD = 0.75; +const SPEED_MULTIPLIER = 4; +const SCALE_BOOST_PCT = 2; +const SCALE_LERP_UP = 0.5; +const SCALE_LERP_DOWN = 0.12; +const SCALE_THRESHOLD = 0.001; +const ANALYSIS_INTERVAL = 100; + +export class KawarpPreset { + constructor() { + this.name = 'Kawarp'; + this.contextType = 'webgl'; + this.managesOwnContext = true; + + this.kawarp = null; + this.canvas = null; + this.audioElement = null; + this.isInitialized = false; + this._lastCoverUrl = null; + this._currentScale = KAWARP_DEFAULTS.scale; + this._targetScale = KAWARP_DEFAULTS.scale; + this._lastAnalysisTime = 0; + this._coverObserver = null; + + this._onPlay = () => { + if (this.kawarp) this.kawarp.start(); + }; + this._onPause = () => { + if (this.kawarp) this.kawarp.stop(); + }; + } + + async lazyInit(canvas, _audioContext, _sourceNode) { + if (this.isInitialized) { + if (canvas !== this.canvas) { + this._destroyKawarp(); + } else { + this._ensureStarted(); + return; + } + } + + try { + const { Kawarp } = await import('@kawarp/core'); + + this.canvas = canvas; + this.kawarp = new Kawarp(canvas, { ...KAWARP_DEFAULTS }); + + this.audioElement = document.getElementById('audio-player'); + if (this.audioElement) { + this.audioElement.addEventListener('play', this._onPlay); + this.audioElement.addEventListener('pause', this._onPause); + } + + this._observeCoverArt(); + + const coverEl = document.querySelector('.now-playing-bar .cover'); + if (coverEl?.tagName === 'IMG' && coverEl.src) { + this._lastCoverUrl = coverEl.src; + this._loadCover(coverEl.src); + } + + this.kawarp.start(); + this.isInitialized = true; + } catch (error) { + console.error('[Kawarp] Init failed:', error); + } + } + + connectAudio() {} + + _ensureStarted() { + if (!this.kawarp) return; + if (this.kawarp.isPlaying) return; + if (this.audioElement?.paused) return; + this.kawarp.start(); + } + + _observeCoverArt() { + const container = document.querySelector('.now-playing-bar'); + if (!container) return; + + this._coverObserver = new MutationObserver(() => { + const el = document.querySelector('.now-playing-bar .cover'); + const src = el?.tagName === 'IMG' ? el.src : null; + if (!src || src === this._lastCoverUrl) return; + this._lastCoverUrl = src; + if (this.kawarp && this.isInitialized) { + this._loadCover(src); + } + }); + + this._coverObserver.observe(container, { + attributes: true, + attributeFilter: ['src'], + subtree: true, + childList: true, + }); + } + + _loadCover(url) { + // Cache buster forces a fresh CORS request, bypassing the browser's + // cached non-CORS response from the tag (same pattern as ui.js) + const sep = url.includes('?') ? '&' : '?'; + this.kawarp.loadImage(`${url}${sep}not-from-cache-please`).catch((err) => + console.warn('[Kawarp] Failed to load cover:', err), + ); + } + + resize(_w, _h) { + if (this.kawarp) this.kawarp.resize(); + } + + draw(ctx, canvas, analyser, dataArray, stats) { + if (!this.kawarp || !this.isInitialized) return; + + this._ensureStarted(); + + // Beat detection, throttled to every 100ms + const now = performance.now(); + if (analyser && now - this._lastAnalysisTime >= ANALYSIS_INTERVAL) { + const buf = new Uint8Array(analyser.frequencyBinCount); + analyser.getByteTimeDomainData(buf); + + let peak = 0; + for (let i = 0; i < buf.length; i++) { + const a = Math.abs(buf[i] - 128) / 128; + if (a > peak) { + peak = a; + if (peak > BEAT_THRESHOLD) break; + } + } + + const isBeat = peak > BEAT_THRESHOLD; + + this.kawarp.animationSpeed = isBeat + ? KAWARP_DEFAULTS.animationSpeed * SPEED_MULTIPLIER + : KAWARP_DEFAULTS.animationSpeed; + + this._targetScale = isBeat ? KAWARP_DEFAULTS.scale + SCALE_BOOST_PCT / 100 : KAWARP_DEFAULTS.scale; + + this._lastAnalysisTime = now; + } + + // Scale lerp + const diff = this._targetScale - this._currentScale; + if (Math.abs(diff) > SCALE_THRESHOLD) { + const lerp = diff > 0 ? SCALE_LERP_UP : SCALE_LERP_DOWN; + this._currentScale += diff * lerp; + this.kawarp.scale = this._currentScale; + } + + // Blended mode support + if (stats.mode === 'blended') { + canvas.style.opacity = '0.85'; + canvas.style.mixBlendMode = 'screen'; + } else { + canvas.style.opacity = '1'; + canvas.style.mixBlendMode = 'normal'; + } + } + + _destroyKawarp() { + if (this.kawarp) { + this.kawarp.stop(); + this.kawarp.dispose(); + this.kawarp = null; + } + this.canvas = null; + this.isInitialized = false; + } + + destroy() { + if (this._coverObserver) { + this._coverObserver.disconnect(); + this._coverObserver = null; + } + if (this.audioElement) { + this.audioElement.removeEventListener('play', this._onPlay); + this.audioElement.removeEventListener('pause', this._onPause); + this.audioElement = null; + } + this._destroyKawarp(); + this._lastCoverUrl = null; + this._currentScale = KAWARP_DEFAULTS.scale; + this._targetScale = KAWARP_DEFAULTS.scale; + } +} diff --git a/package-lock.json b/package-lock.json index 84aff55..56397ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", + "@kawarp/core": "^1.1.1", "@neutralinojs/lib": "^6.5.0", "butterchurn": "^2.6.7", "butterchurn-presets": "^2.4.7", @@ -23,7 +24,7 @@ "@neutralinojs/neu": "^11.7.0", "eslint": "^9.39.3", "eslint-config-prettier": "^10.1.8", - "globals": "^17.3.0", + "globals": "^17.4.0", "htmlhint": "^1.9.1", "prettier": "^3.8.1", "stylelint": "^16.26.1", @@ -2447,6 +2448,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kawarp/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kawarp/core/-/core-1.1.1.tgz", + "integrity": "sha512-hnJ0CQQAa6o4HPoUE6Tkn6/cqzpA/tRPNDTNqVeoY9rozL37KweAzbypmdrYTBOdyJRR9MvETyxy4hlpenIa/w==", + "license": "AGPL-3.0" + }, "node_modules/@keyv/serialize": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", @@ -10742,6 +10749,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@kawarp/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kawarp/core/-/core-1.1.1.tgz", + "integrity": "sha512-hnJ0CQQAa6o4HPoUE6Tkn6/cqzpA/tRPNDTNqVeoY9rozL37KweAzbypmdrYTBOdyJRR9MvETyxy4hlpenIa/w==" + }, "@keyv/serialize": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", diff --git a/package.json b/package.json index 6e111fc..88a3095 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "dependencies": { "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", + "@kawarp/core": "^1.1.1", "@neutralinojs/lib": "^6.5.0", "butterchurn": "^2.6.7", "butterchurn-presets": "^2.4.7", From 828e192362efe11ac695c915d8e10f6a00e4e3d2 Mon Sep 17 00:00:00 2001 From: edideaur Date: Fri, 6 Mar 2026 08:43:53 +0000 Subject: [PATCH 02/11] fix visualizers --- bun.lock | 10 ++++++++ js/visualizers/butterchurn.js | 46 +++++++++++++++++++---------------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/bun.lock b/bun.lock index cb1af31..37c2b91 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,9 @@ "dependencies": { "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", + "@kawarp/core": "^1.1.1", "@neutralinojs/lib": "^6.5.0", + "appwrite": "^23.0.0", "butterchurn": "^2.6.7", "butterchurn-presets": "^2.4.7", "cookie-session": "^2.1.1", @@ -338,6 +340,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@kawarp/core": ["@kawarp/core@1.1.1", "", {}, "sha512-hnJ0CQQAa6o4HPoUE6Tkn6/cqzpA/tRPNDTNqVeoY9rozL37KweAzbypmdrYTBOdyJRR9MvETyxy4hlpenIa/w=="], + "@keyv/bigmap": ["@keyv/bigmap@1.3.0", "", { "dependencies": { "hashery": "^1.2.0", "hookified": "^1.13.0" }, "peerDependencies": { "keyv": "^5.5.4" } }, "sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg=="], "@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="], @@ -454,6 +458,8 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "appwrite": ["appwrite@23.0.0", "", { "dependencies": { "json-bigint": "1.0.0" } }, "sha512-K11a597npl3jsnxWKzjw163n4GguH4+/zBCOiU15yc1u+7QF0nP9mxsY4JxKrBU6bmQRtgtMTPv/6YOLSwp/QQ=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], @@ -490,6 +496,8 @@ "bcp-47-normalize": ["bcp-47-normalize@2.3.0", "", { "dependencies": { "bcp-47": "^2.0.0", "bcp-47-match": "^2.0.0" } }, "sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q=="], + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -878,6 +886,8 @@ "jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], diff --git a/js/visualizers/butterchurn.js b/js/visualizers/butterchurn.js index e040cd5..8fb8414 100644 --- a/js/visualizers/butterchurn.js +++ b/js/visualizers/butterchurn.js @@ -98,6 +98,7 @@ export class ButterchurnPreset { constructor() { this.name = 'Butterchurn'; this.contextType = 'webgl'; + this.managesOwnContext = true; this.visualizer = null; this.canvas = null; @@ -458,31 +459,34 @@ export class ButterchurnPreset { * Lazy initialization helper for when audio context becomes available */ lazyInit(canvas, audioContext, sourceNode) { - if (!this.isInitialized && canvas && audioContext) { - const gl = - canvas.getContext('webgl2', { - alpha: true, - antialias: true, - preserveDrawingBuffer: true, - }) || - canvas.getContext('webgl', { - alpha: true, - antialias: true, - preserveDrawingBuffer: true, - }); + return new Promise((resolve) => { + if (!this.isInitialized && canvas && audioContext) { + const gl = + canvas.getContext('webgl2', { + alpha: true, + antialias: true, + preserveDrawingBuffer: true, + }) || + canvas.getContext('webgl', { + alpha: true, + antialias: true, + preserveDrawingBuffer: true, + }); - if (gl) { - this.init(canvas, gl, audioContext, null); + if (gl) { + this.init(canvas, gl, audioContext, null); - // Connect audio if sourceNode is provided - if (sourceNode) { - this.connectAudioWithDelay(sourceNode); + // Connect audio if sourceNode is provided + if (sourceNode) { + this.connectAudioWithDelay(sourceNode); + } } + } else if (this.isInitialized && sourceNode) { + // Reconnect if source changed + this.connectAudioWithDelay(sourceNode); } - } else if (this.isInitialized && sourceNode) { - // Reconnect if source changed - this.connectAudioWithDelay(sourceNode); - } + resolve(); + }); } /** From c986921505e2c4897f4a7e4de83e3b262fe8ede6 Mon Sep 17 00:00:00 2001 From: edideaur Date: Fri, 6 Mar 2026 08:56:36 +0000 Subject: [PATCH 03/11] bun install in dockerfile and reload page when visualizer switching to avoid issues --- .devcontainer/devcontainer.json | 2 +- js/visualizer.js | 19 ++++++++++++++----- js/visualizers/butterchurn.js | 11 +++++++++-- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6033fdf..6a08a95 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,7 +14,7 @@ } }, - "postCreateCommand": "bun --version && code --version", + "postCreateCommand": "bun install", "remoteEnv": { "SHELL": "/usr/bin/fish" diff --git a/js/visualizer.js b/js/visualizer.js index 2e5809a..e3f5f3d 100644 --- a/js/visualizer.js +++ b/js/visualizer.js @@ -91,9 +91,7 @@ export class Visualizer { // Clone the canvas to get a fresh context when switching context types, // or when the previous preset grabbed its own context (managesOwnContext) - const needsClone = - (this.ctx && currentType !== type) || - (!this.ctx && currentType && currentType !== type); + const needsClone = (this.ctx && currentType !== type) || (!this.ctx && currentType && currentType !== type); if (needsClone) { const parent = this.canvas.parentElement; @@ -287,19 +285,30 @@ export class Visualizer { setPreset(key) { if (!this.presets[key]) return; + const webglPresets = ['butterchurn', 'kawarp']; + const fromPreset = this.activePresetKey; + const toPreset = key; + + if (webglPresets.includes(fromPreset) && webglPresets.includes(toPreset) && fromPreset !== toPreset) { + visualizerSettings.setPreset(key); + window.location.reload(); + return; + } + if (this.activePreset?.destroy) { this.activePreset.destroy(); } + this._currentContextType = undefined; + this.ctx = null; + this.activePresetKey = key; this.initContext(); this.resize(); - // Initialize presets that need lazy init (Butterchurn, Kawarp) if (this.presets[key].lazyInit && this.audioContext) { const sourceNode = audioContextManager.getSourceNode(); this.presets[key].lazyInit(this.canvas, this.audioContext, sourceNode).then(() => { - // Re-resize after async init so framebuffers match canvas size this.resize(); }); } diff --git a/js/visualizers/butterchurn.js b/js/visualizers/butterchurn.js index 8fb8414..333f953 100644 --- a/js/visualizers/butterchurn.js +++ b/js/visualizers/butterchurn.js @@ -493,15 +493,22 @@ export class ButterchurnPreset { * Cleanup resources */ destroy() { - // Unregister graph change listener if (this._unregisterGraphChange) { this._unregisterGraphChange(); this._unregisterGraphChange = null; } - if (this.visualizer) { + if (this.visualizer && this.canvas) { + const gl = this.canvas.getContext('webgl2') || this.canvas.getContext('webgl'); + if (gl) { + const ext = gl.getExtension('WEBGL_lose_context'); + if (ext) { + ext.loseContext(); + } + } this.visualizer = null; } + this.isInitialized = false; this.canvas = null; this.audioContext = null; From f6bc60bd5e5ccf389fb2072f3e83ea195173ed18 Mon Sep 17 00:00:00 2001 From: edideaur <182119792+edideaur@users.noreply.github.com> Date: Fri, 6 Mar 2026 08:57:13 +0000 Subject: [PATCH 04/11] style: auto-fix linting issues --- index.html | 2 +- js/accounts/auth.js | 25 +++++++++++-------------- js/accounts/config.js | 12 +++++++----- js/ui.js | 8 +++----- js/visualizers/kawarp.js | 6 +++--- 5 files changed, 25 insertions(+), 28 deletions(-) diff --git a/index.html b/index.html index 1431cab..2ae6bc1 100644 --- a/index.html +++ b/index.html @@ -2332,7 +2332,7 @@
-