From 5ae3356f1463730801ea381664f5a4450dcb1def Mon Sep 17 00:00:00 2001 From: Fritz Date: Wed, 27 May 2026 08:39:07 +1000 Subject: [PATCH] feat: refactor canvas and button redraw utilities for improved code clarity and reusability --- src/App.jsx | 282 +++++++++++++++++++--------------------------------- 1 file changed, 103 insertions(+), 179 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index e21759d..033861a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,6 +4,45 @@ import OBSWebSocket from 'obs-websocket-js' import './App.css' import { getButtonsAt, immutableSetButton, formatClock } from './utils.js' +// ─── Canvas utilities ──────────────────────────────────────── +function makeCanvas(size) { + const canvas = document.createElement('canvas') + canvas.width = size + canvas.height = size + return { canvas, ctx: canvas.getContext('2d') } +} + +function drawTitle(ctx, title, iconSize) { + if (!title) return + const fs = Math.round(iconSize * 0.15) + ctx.font = `bold ${fs}px -apple-system, sans-serif` + ctx.fillStyle = '#ffffff' + ctx.textAlign = 'center' + ctx.textBaseline = 'bottom' + ctx.shadowColor = 'rgba(0,0,0,0.95)' + ctx.shadowBlur = 5 + ctx.shadowOffsetY = 1 + ctx.fillText(title, iconSize / 2, iconSize - Math.round(iconSize * 0.04)) + ctx.shadowColor = 'transparent' + ctx.shadowBlur = 0 + ctx.shadowOffsetY = 0 +} + +// ─── Shared hardware-button redraw helper ───────────────────── +// Iterates all rows×cols slots and calls drawFn(index, config) for each. +function redrawButtons(configs = {}, rows, cols, drawFn) { + for (let i = 0; i < rows * cols; i++) drawFn(i, configs[i]) +} + +// ─── Plugin manifest → category list (used in both ActionsPanel and ActionSection) ── +function pluginCategoriesToList(manifests) { + return manifests.map(p => ({ + id: p.UUID, + name: p.Category || p.Name, + actions: (p.Actions || []).map(a => ({ id: a.UUID, name: a.Name, icon: '🔌' })), + })) +} + const DEVICE_MODEL_NAMES = { originalv2: 'Stream Deck Original V2', mk2: 'Stream Deck MK.2', @@ -31,19 +70,16 @@ async function extractGifFrames(dataUrl, iconSize, title) { const gw = gif.lsd.width const gh = gif.lsd.height - // Native-size composite canvas + // Native-size composite canvas (rectangular — dimensions from GIF header) const native = document.createElement('canvas') native.width = gw native.height = gh const nativeCtx = native.getContext('2d') // Output canvas scaled to iconSize - const out = document.createElement('canvas') - out.width = iconSize - out.height = iconSize - const outCtx = out.getContext('2d') + const { canvas: out, ctx: outCtx } = makeCanvas(iconSize) - // Scratch canvas for patching + // Scratch canvas for patching (resized per-frame in loop) const patch = document.createElement('canvas') const patchCtx = patch.getContext('2d') @@ -63,20 +99,7 @@ async function extractGifFrames(dataUrl, iconSize, title) { outCtx.clearRect(0, 0, iconSize, iconSize) outCtx.drawImage(native, 0, 0, iconSize, iconSize) - if (title) { - const fs = Math.round(iconSize * 0.15) - outCtx.font = `bold ${fs}px -apple-system, sans-serif` - outCtx.fillStyle = '#ffffff' - outCtx.textAlign = 'center' - outCtx.textBaseline = 'bottom' - outCtx.shadowColor = 'rgba(0,0,0,0.95)' - outCtx.shadowBlur = 5 - outCtx.shadowOffsetY = 1 - outCtx.fillText(title, iconSize / 2, iconSize - Math.round(iconSize * 0.04)) - outCtx.shadowColor = 'transparent' - outCtx.shadowBlur = 0 - outCtx.shadowOffsetY = 0 - } + drawTitle(outCtx, title, iconSize) result.push({ rgbaData: Array.from(outCtx.getImageData(0, 0, iconSize, iconSize).data), @@ -274,13 +297,7 @@ function ActionsPanel({ pluginManifests = [] }) { const toggle = id => setExpanded(prev => ({ ...prev, [id]: !prev[id] })) - const pluginCategories = pluginManifests.map(p => ({ - id: p.UUID, - name: p.Category || p.Name, - actions: (p.Actions || []).map(a => ({ id: a.UUID, name: a.Name, icon: '🔌' })), - })) - - const allCategories = [...ACTION_CATEGORIES, ...pluginCategories] + const allCategories = [...ACTION_CATEGORIES, ...pluginCategoriesToList(pluginManifests)] const filtered = allCategories .map(cat => ({ @@ -952,6 +969,16 @@ function dispatchSubAction(sd, act, switchToPage) { return Promise.resolve() } +function RemoveActionButton({ onRemove }) { + return ( + + ) +} + export function ActionSection({ action, onChange, profiles = [], pageCount = 1, onEnterFolder, obsScenes = [], obsInputs = [], obsTransitions = [], obsSceneCollections = [], pluginManifests = [] }) { const [picking, setPicking] = useState(false) @@ -964,11 +991,7 @@ export function ActionSection({ action, onChange, profiles = [], pageCount = 1, Folder - + onChange({ action: null })} />

Press the button on hardware to enter the folder. In the editor, use the button below or click the button in the grid.

{onEnterFolder && ( @@ -992,11 +1015,7 @@ export function ActionSection({ action, onChange, profiles = [], pageCount = 1, Back - + onChange({ action: null })} />

Returns to the parent folder or page when pressed.

@@ -1010,11 +1029,7 @@ export function ActionSection({ action, onChange, profiles = [], pageCount = 1, Sleep - + onChange({ action: null })} />

Puts the deck to sleep. Any button press wakes it.

@@ -1032,11 +1047,7 @@ export function ActionSection({ action, onChange, profiles = [], pageCount = 1, Run Command - + onChange({ action: null })} />