Multi-device support, dynamic grid adaptation, keyboard action
- Multi-device: OpenAllDecks(), GET /api/decks, per-deck PageManagers - Dynamic grid: frontend adapts to connected device layout - Keyboard action: new action type for sending key combos via wtype/xdotool - Key recorder UI: click-based modifier toggles + single key capture - Config: remove hardcoded maxKey validation - Screensaver and auto-switch sync across all decks
This commit is contained in:
+22
-1
@@ -10,6 +10,8 @@ document.addEventListener('alpine:init', () => {
|
||||
toast: null,
|
||||
displayOutputs: {},
|
||||
isCapturingKey: false,
|
||||
decks: [],
|
||||
activeDeckSerial: '',
|
||||
|
||||
// ── Edit state ──
|
||||
editing: null,
|
||||
@@ -25,9 +27,22 @@ document.addEventListener('alpine:init', () => {
|
||||
// ── Init ──
|
||||
async init() {
|
||||
await this.loadConfig()
|
||||
await this.loadDecks()
|
||||
setInterval(() => this.pollDisplayOutputs(), 3000)
|
||||
},
|
||||
|
||||
async loadDecks() {
|
||||
try {
|
||||
const res = await fetch('/api/decks')
|
||||
if (res.ok) {
|
||||
this.decks = await res.json()
|
||||
if (this.decks.length > 0 && !this.activeDeckSerial) {
|
||||
this.activeDeckSerial = this.decks[0].serial
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
async pollDisplayOutputs() {
|
||||
if (this.view !== 'grid') return
|
||||
try {
|
||||
@@ -69,12 +84,18 @@ document.addEventListener('alpine:init', () => {
|
||||
return this.pages.map(p => p.name)
|
||||
},
|
||||
|
||||
get activeDeck() {
|
||||
return this.decks.find(d => d.serial === this.activeDeckSerial)
|
||||
|| { keys_x: 5, keys_y: 3, num_keys: 15 }
|
||||
},
|
||||
|
||||
// ── Grid ──
|
||||
get gridKeys() {
|
||||
const page = this.currentPage
|
||||
if (!page) return []
|
||||
const keys = []
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const n = this.activeDeck.num_keys
|
||||
for (let i = 0; i < n; i++) {
|
||||
const kc = page.keys.find(k => k.index === i)
|
||||
keys.push({
|
||||
index: i,
|
||||
|
||||
+9
-1
@@ -45,7 +45,15 @@
|
||||
|
||||
<!-- ═══ Grid View ═══ -->
|
||||
<main x-show="view === 'grid' && !subView">
|
||||
<div class="deck-grid">
|
||||
<div class="deck-selector" x-show="decks.length > 1">
|
||||
<template x-for="d in decks" :key="d.serial">
|
||||
<button class="deck-btn"
|
||||
:class="{ active: d.serial === activeDeckSerial }"
|
||||
@click="activeDeckSerial = d.serial"
|
||||
x-text="d.model + ' (' + d.keys_x + '\u00d7' + d.keys_y + ')'"></button>
|
||||
</template>
|
||||
</div>
|
||||
<div class="deck-grid" :style="'grid-template-columns: repeat(' + activeDeck.keys_x + ', 1fr)'">
|
||||
<template x-for="key in gridKeys" :key="key.index">
|
||||
<button class="key-btn"
|
||||
:class="{ empty: !key.configured }"
|
||||
|
||||
+27
-1
@@ -107,7 +107,6 @@ main {
|
||||
|
||||
.deck-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 8px;
|
||||
padding: 20px;
|
||||
background: var(--surface);
|
||||
@@ -115,6 +114,33 @@ main {
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.deck-selector {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
.deck-btn {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-xs);
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
text-align: center;
|
||||
}
|
||||
.deck-btn:hover { color: var(--text); border-color: var(--border-light); }
|
||||
.deck-btn.active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.key-btn {
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid var(--border);
|
||||
|
||||
Reference in New Issue
Block a user