onChange({ action: null })} />
)
const obsHint = Auto-connects to OBS at localhost:4455. Enable WebSocket in OBS → Tools → WebSocket Server Settings.
@@ -1706,12 +1673,7 @@ export function ActionSection({ action, onChange, profiles = [], pageCount = 1,
}
// ── unassigned ──
- 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)]
return (
@@ -2305,10 +2267,7 @@ export default function App() {
return
}
- const canvas = document.createElement('canvas')
- canvas.width = iconSize
- canvas.height = iconSize
- const ctx = canvas.getContext('2d')
+ const { canvas, ctx } = makeCanvas(iconSize)
if (iconDataUrl) {
const img = new Image()
@@ -2320,16 +2279,7 @@ export default function App() {
ctx.fillRect(0, 0, iconSize, iconSize)
}
- if (title) {
- ctx.font = `bold ${Math.round(iconSize * 0.15)}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))
- }
+ drawTitle(ctx, title, iconSize)
const { data } = ctx.getImageData(0, 0, iconSize, iconSize)
window.streamDeck.setButtonIcon(index, new Uint8Array(data.buffer, data.byteOffset, data.byteLength))
@@ -2396,10 +2346,7 @@ export default function App() {
const { format = 'HH:MM', bgColor = '#000000', textColor = '#ffffff' } = config?.action ?? {}
// One canvas/ctx per button instance — reused every tick to avoid GPU context churn
- const canvas = document.createElement('canvas')
- canvas.width = iconSize
- canvas.height = iconSize
- const ctx = canvas.getContext('2d')
+ const { canvas, ctx } = makeCanvas(iconSize)
const drawClock = () => {
if (dynamicButtonsRef.current[index]?.token !== token) return
@@ -2441,10 +2388,7 @@ export default function App() {
const { bgColor = '#000000', textColor = '#00ff88', showCpu = true, showRam = true } = config?.action ?? {}
// One canvas/ctx per button instance — reused every tick to avoid GPU context churn
- const canvas = document.createElement('canvas')
- canvas.width = iconSize
- canvas.height = iconSize
- const ctx = canvas.getContext('2d')
+ const { canvas, ctx } = makeCanvas(iconSize)
const draw = async () => {
if (dynamicButtonsRef.current[index]?.token !== token) return
@@ -2486,10 +2430,7 @@ export default function App() {
const { bgColor = '#000000', textColor = '#00aaff', sink = '@DEFAULT_SINK@' } = config?.action ?? {}
// One canvas/ctx per button instance — reused every tick to avoid GPU context churn
- const canvas = document.createElement('canvas')
- canvas.width = iconSize
- canvas.height = iconSize
- const ctx = canvas.getContext('2d')
+ const { canvas, ctx } = makeCanvas(iconSize)
const draw = async () => {
if (dynamicButtonsRef.current[index]?.token !== token) return
@@ -2537,10 +2478,7 @@ export default function App() {
const { bgColor = '#000000', sink = '@DEFAULT_SINK@' } = config?.action ?? {}
// One canvas/ctx per button instance — reused every tick to avoid GPU context churn
- const canvas = document.createElement('canvas')
- canvas.width = iconSize
- canvas.height = iconSize
- const ctx = canvas.getContext('2d')
+ const { canvas, ctx } = makeCanvas(iconSize)
const draw = async () => {
if (dynamicButtonsRef.current[index]?.token !== token) return
@@ -2582,10 +2520,7 @@ export default function App() {
const { bgColor = '#000000', textColor = '#ffffff', player = '%any' } = config?.action ?? {}
// One canvas/ctx per button instance — reused every tick to avoid GPU context churn
- const canvas = document.createElement('canvas')
- canvas.width = iconSize
- canvas.height = iconSize
- const ctx = canvas.getContext('2d')
+ const { canvas, ctx } = makeCanvas(iconSize)
const draw = async () => {
if (dynamicButtonsRef.current[index]?.token !== token) return
@@ -2655,10 +2590,7 @@ export default function App() {
const { bgColor = '#000000' } = config?.action ?? {}
// One canvas/ctx per button instance — reused every tick to avoid GPU context churn
- const canvas = document.createElement('canvas')
- canvas.width = iconSize
- canvas.height = iconSize
- const ctx = canvas.getContext('2d')
+ const { canvas, ctx } = makeCanvas(iconSize)
const draw = async () => {
if (dynamicButtonsRef.current[index]?.token !== token) return
@@ -2753,12 +2685,9 @@ export default function App() {
setCurrentPage(0)
setFolderPath([])
setSelectedKey(null)
- const rows = deviceRef.current?.rows ?? 3
- const cols = deviceRef.current?.cols ?? 5
- const total = rows * cols
- for (let i = 0; i < total; i++) {
- drawHardwareButtonRef.current(i, page0[i])
- }
+ const rows = deviceRef.current?.rows ?? 3
+ const cols = deviceRef.current?.cols ?? 5
+ redrawButtons(page0, rows, cols, drawHardwareButtonRef.current)
return true
}
const loadProfileDataRef = useRef(null)
@@ -2773,10 +2702,9 @@ export default function App() {
setSelectedKey(null)
setToggledButtons({})
toggledButtonsRef.current = {}
- const newPage = pg[pageIndex] ?? {}
- const rows = deviceRef.current?.rows ?? 3
- const cols = deviceRef.current?.cols ?? 5
- for (let i = 0; i < rows * cols; i++) drawHardwareButtonRef.current(i, newPage[i])
+ const rows = deviceRef.current?.rows ?? 3
+ const cols = deviceRef.current?.cols ?? 5
+ redrawButtons(pg[pageIndex] ?? {}, rows, cols, drawHardwareButtonRef.current)
}
const switchToPageRef = useRef(null)
switchToPageRef.current = switchToPage
@@ -2789,9 +2717,9 @@ export default function App() {
setSelectedKey(null)
setToggledButtons({})
toggledButtonsRef.current = {}
- const rows = deviceRef.current?.rows ?? 3
- const cols = deviceRef.current?.cols ?? 5
- for (let i = 0; i < rows * cols; i++) drawHardwareButtonRef.current(i, undefined)
+ const rows = deviceRef.current?.rows ?? 3
+ const cols = deviceRef.current?.cols ?? 5
+ redrawButtons({}, rows, cols, drawHardwareButtonRef.current)
}
const removePage = (pageIndex) => {
@@ -2806,23 +2734,23 @@ export default function App() {
setSelectedKey(null)
setToggledButtons({})
toggledButtonsRef.current = {}
- const rows = deviceRef.current?.rows ?? 3
- const cols = deviceRef.current?.cols ?? 5
- for (let i = 0; i < rows * cols; i++) drawHardwareButtonRef.current(i, newPage[i])
+ const rows = deviceRef.current?.rows ?? 3
+ const cols = deviceRef.current?.cols ?? 5
+ redrawButtons(newPage, rows, cols, drawHardwareButtonRef.current)
}
// ─────────────────────────────────────────────────────────────
// ── Folder enter / exit ─────────────────────────────────────
const enterFolder = (buttonIndex) => {
- const newPath = [...folderPathRef.current, buttonIndex]
- const newButtons = getButtonsAt(pagesRef.current, currentPageRef.current, newPath)
+ const newPath = [...folderPathRef.current, buttonIndex]
+ const newButtons = getButtonsAt(pagesRef.current, currentPageRef.current, newPath)
setFolderPath(newPath)
setSelectedKey(null)
setToggledButtons({})
toggledButtonsRef.current = {}
- const rows = deviceRef.current?.rows ?? 3
- const cols = deviceRef.current?.cols ?? 5
- for (let i = 0; i < rows * cols; i++) drawHardwareButtonRef.current(i, newButtons[i])
+ const rows = deviceRef.current?.rows ?? 3
+ const cols = deviceRef.current?.cols ?? 5
+ redrawButtons(newButtons, rows, cols, drawHardwareButtonRef.current)
}
const enterFolderRef = useRef(null)
enterFolderRef.current = enterFolder
@@ -2834,9 +2762,9 @@ export default function App() {
setSelectedKey(null)
setToggledButtons({})
toggledButtonsRef.current = {}
- const rows = deviceRef.current?.rows ?? 3
- const cols = deviceRef.current?.cols ?? 5
- for (let i = 0; i < rows * cols; i++) drawHardwareButtonRef.current(i, newButtons[i])
+ const rows = deviceRef.current?.rows ?? 3
+ const cols = deviceRef.current?.cols ?? 5
+ redrawButtons(newButtons, rows, cols, drawHardwareButtonRef.current)
}
const exitFolderRef = useRef(null)
exitFolderRef.current = exitFolder
@@ -3174,12 +3102,8 @@ export default function App() {
// On reconnect: hardware is dark — redraw all buttons from current config
useEffect(() => {
if (!device) return
- const entries = Object.entries(buttonConfigsRef.current)
- if (!entries.length) return // first startup: profile-load effect handles drawing
- const total = (device.rows ?? 3) * (device.cols ?? 5)
- for (let i = 0; i < total; i++) {
- drawHardwareButtonRef.current(i, buttonConfigsRef.current[i])
- }
+ if (!Object.keys(buttonConfigsRef.current).length) return // first startup: profile-load effect handles drawing
+ redrawButtons(buttonConfigsRef.current, device.rows ?? 3, device.cols ?? 5, drawHardwareButtonRef.current)
}, [device]) // eslint-disable-line react-hooks/exhaustive-deps
const rows = device?.rows ?? 3
@@ -3207,9 +3131,9 @@ export default function App() {
setActiveProfile(result.name)
setPages([{}])
setCurrentPage(0)
- const rows = deviceRef.current?.rows ?? 3
- const cols = deviceRef.current?.cols ?? 5
- for (let i = 0; i < rows * cols; i++) drawHardwareButtonRef.current(i, undefined)
+ const rows = deviceRef.current?.rows ?? 3
+ const cols = deviceRef.current?.cols ?? 5
+ redrawButtons({}, rows, cols, drawHardwareButtonRef.current)
}
}}
onDelete={async name => {