diff --git a/src/buttons.json b/src/buttons.json new file mode 100644 index 0000000..1ff7251 --- /dev/null +++ b/src/buttons.json @@ -0,0 +1,51 @@ +{ + "buttons": [ + {"id": "start", "preset": "off"}, + {"id": "grab", "preset": "grab"}, + {"id": "spin1", "preset": "spin1"}, + {"id": "lift", "preset": "lift"}, + {"id": "flare", "preset": "flare"}, + {"id": "hook", "preset": "hook"}, + {"id": "roll1", "preset": "roll1"}, + {"id": "invertsplit", "preset": "invertsplit"}, + {"id": "pose1", "preset": "pose1"}, + {"id": "pose2", "preset": "pose2"}, + {"id": "roll2", "preset": "roll2"}, + {"id": "backbalance1", "preset": "backbalance1"}, + {"id": "beat1", "preset": "beat1"}, + {"id": "pose3", "preset": "pose3"}, + {"id": "roll3", "preset": "roll3"}, + {"id": "crouch", "preset": "crouch"}, + {"id": "pose4", "preset": "pose4"}, + {"id": "roll4", "preset": "roll4"}, + {"id": "backbendsplit", "preset": "backbendsplit"}, + {"id": "backbalance2", "preset": "backbalance2"}, + {"id": "backbalance3", "preset": "backbalance3"}, + {"id": "beat2", "preset": "beat2"}, + {"id": "straddle", "preset": "straddle"}, + {"id": "beat3", "preset": "beat3"}, + {"id": "frontbalance1", "preset": "frontbalance1"}, + {"id": "pose5", "preset": "pose5"}, + {"id": "pose6", "preset": "pose6"}, + {"id": "elbowhang", "preset": "elbowhang"}, + {"id": "elbowhangspin", "preset": "elbowhangspin"}, + {"id": "spin2", "preset": "spin2"}, + {"id": "dismount", "preset": "dismount"}, + {"id": "spin3", "preset": "spin3"}, + {"id": "fluff", "preset": "fluff"}, + {"id": "spin4", "preset": "spin4"}, + {"id": "flare2", "preset": "flare2"}, + {"id": "elbowhangsplit2", "preset": "elbowhangsplit2"}, + {"id": "invert", "preset": "invert"}, + {"id": "roll5", "preset": "roll5"}, + {"id": "backbend", "preset": "backbend"}, + {"id": "pose7", "preset": "pose7"}, + {"id": "roll6", "preset": "roll6"}, + {"id": "seat", "preset": "seat"}, + {"id": "kneehang", "preset": "kneehang"}, + {"id": "legswoop", "preset": "legswoop"}, + {"id": "split", "preset": "split"}, + {"id": "foothang", "preset": "foothang"}, + {"id": "end", "preset": "end"} + ] +} diff --git a/src/static/hoop.css b/src/static/hoop.css new file mode 100644 index 0000000..c85a09f --- /dev/null +++ b/src/static/hoop.css @@ -0,0 +1,187 @@ +body { font-family: sans-serif; margin: 0; padding: 0 0 6.5rem 0; } + +body.button-editor-open { + overflow: hidden; +} + +.header { position: relative; } + +.menu-btn { + width: 100%; + padding: 0.75rem 1rem; + font-size: 1rem; + cursor: pointer; + border: 1px solid #555; + border-radius: 0; + background: #333; + color: #fff; + text-align: left; +} + +.menu { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 10; + flex-direction: column; + border: 1px solid #555; + border-top: none; + background: #2a2a2a; +} + +.menu.open { display: flex; } + +.menu-item { + padding: 0.75rem 1rem; + font-size: 1rem; + cursor: pointer; + border: none; + border-bottom: 1px solid #444; + background: transparent; + color: #fff; + text-align: left; +} + +.menu-item:last-child { border-bottom: none; } +.menu-item:hover { background: #444; } +.menu-item.off { background: #522; } + +.buttons { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1px; + max-height: calc(100vh - 10rem); + overflow-y: auto; +} + +@media (min-width: 768px) { + .buttons { + grid-template-columns: repeat(6, 1fr); + } +} + +.btn { + padding: 0.5rem 0.25rem; + border: 1px solid #555; + border-radius: 0; + background: #444; + color: #fff; + font-size: 1rem; + cursor: pointer; + min-height: 4.15rem; +} + +.btn.selected { + background: #2a7; + border-color: #3b8; +} + +.context-menu { + position: fixed; + z-index: 100; + background: #2a2a2a; + border: 1px solid #555; + min-width: 8rem; +} + +.context-menu-item { + display: block; + width: 100%; + padding: 0.5rem 1rem; + border: none; + background: transparent; + color: #fff; + font-size: 1rem; + text-align: left; + cursor: pointer; +} + +.context-menu-item:hover { background: #444; } + +.next-btn { + position: fixed; + bottom: 0; + left: 0; + right: 0; + width: 100%; + padding: 2rem 1rem; + font-size: 1.25rem; + cursor: pointer; + border: 2px solid #555; + border-radius: 0; + background: #333; + color: #fff; +} + +.preset-editor { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.65); + display: none; + align-items: center; + justify-content: center; + z-index: 200; + padding: 1rem; +} + +.preset-editor.open { + display: flex; +} + +.preset-editor-inner { + width: 100%; + max-width: 28rem; + background: #222; + border: 1px solid #555; + border-radius: 6px; + padding: 1rem 1.25rem 0.75rem; + box-sizing: border-box; +} + +.preset-editor-title { + margin: 0 0 0.75rem; + font-size: 1.25rem; + color: #fff; +} + +.preset-editor-field { + display: flex; + flex-direction: column; + margin-bottom: 0.5rem; + gap: 0.25rem; + font-size: 0.9rem; + color: #ddd; +} + +.preset-editor-field input { + padding: 0.35rem 0.5rem; + border-radius: 4px; + border: 1px solid #555; + background: #111; + color: #fff; + font-size: 0.95rem; +} + +.preset-editor-actions { + display: flex; + gap: 0.5rem; + margin: 0.75rem 0 0.5rem; +} + +.preset-editor-btn { + flex: 1; + padding: 0.5rem 0.75rem; + border-radius: 4px; + border: 1px solid #555; + background: #333; + color: #fff; + font-size: 0.95rem; + cursor: pointer; +} + +.preset-editor-btn.primary { + background: #2a7; + border-color: #3b8; +} diff --git a/src/static/hoop.js b/src/static/hoop.js new file mode 100644 index 0000000..4b5613a --- /dev/null +++ b/src/static/hoop.js @@ -0,0 +1,246 @@ +var ws = null; +var currentEditIndex = -1; + +function getButtonsFromDom() { + var btns = document.querySelectorAll('#buttonsContainer .btn'); + return Array.prototype.map.call(btns, function (el) { + return { + id: el.getAttribute('data-id') || el.textContent.trim(), + preset: el.getAttribute('data-preset') || '' + }; + }); +} + +function renderButtons(buttons) { + var container = document.getElementById('buttonsContainer'); + container.innerHTML = ''; + buttons.forEach(function (btn, idx) { + var el = document.createElement('button'); + el.className = 'btn'; + el.type = 'button'; + el.setAttribute('data-preset', btn.preset); + el.setAttribute('data-id', btn.id); + el.setAttribute('data-index', String(idx)); + el.draggable = true; + el.textContent = btn.id; + container.appendChild(el); + }); + attachButtonListeners(); +} + +function saveButtons(buttons, callback) { + fetch('/api/buttons', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ buttons: buttons }) + }).then(function(r) { return r.json(); }).then(function(data) { + if (callback) callback(data); + }).catch(function() { + if (callback) callback({ ok: false }); + }); +} + +function saveCurrentButtons() { + saveButtons(getButtonsFromDom(), function() {}); +} + +var contextMenuEl = null; +var longPressTimer = null; +var longPressTriggered = false; + +function attachButtonListeners() { + var btns = document.querySelectorAll('#buttonsContainer .btn'); + for (var i = 0; i < btns.length; i++) { + var el = btns[i]; + el.setAttribute('data-index', String(i)); + el.onclick = function(ev) { + if (longPressTriggered) { + ev.preventDefault(); + return; + } + var btn = ev.currentTarget; + sendSelect(btn.getAttribute('data-preset'), btn); + }; + el.oncontextmenu = function(ev) { + ev.preventDefault(); + showButtonContextMenu(ev, ev.currentTarget); + }; + (function(buttonEl) { + var startX, startY; + el.ontouchstart = function(ev) { + if (ev.touches.length !== 1) return; + longPressTriggered = false; + startX = ev.touches[0].clientX; + startY = ev.touches[0].clientY; + longPressTimer = setTimeout(function() { + longPressTimer = null; + longPressTriggered = true; + showButtonContextMenu({ clientX: startX, clientY: startY }, buttonEl); + }, 500); + }; + el.ontouchmove = function(ev) { + if (longPressTimer && ev.touches.length === 1) { + var dx = ev.touches[0].clientX - startX; + var dy = ev.touches[0].clientY - startY; + if (dx * dx + dy * dy > 100) { + clearTimeout(longPressTimer); + longPressTimer = null; + } + } + }; + el.ontouchend = el.ontouchcancel = function() { + if (longPressTimer) { + clearTimeout(longPressTimer); + longPressTimer = null; + } + setTimeout(function() { longPressTriggered = false; }, 400); + }; + })(el); + } +} + +function openNewButtonEditor() { + currentEditIndex = -1; + document.getElementById('buttonEditorTitle').textContent = 'New button'; + document.getElementById('be-label').value = ''; + document.getElementById('be-preset').value = ''; + openButtonEditor(); +} + +function openExistingButtonEditor(index) { + currentEditIndex = index; + var buttons = getButtonsFromDom(); + var btn = buttons[index]; + document.getElementById('buttonEditorTitle').textContent = 'Edit button'; + document.getElementById('be-label').value = btn.id || ''; + document.getElementById('be-preset').value = btn.preset || ''; + openButtonEditor(); +} + +function openButtonEditor() { + var editor = document.getElementById('buttonEditor'); + if (!editor) return; + editor.classList.add('open'); + document.body.classList.add('button-editor-open'); +} + +function closeButtonEditor() { + var editor = document.getElementById('buttonEditor'); + if (!editor) return; + editor.classList.remove('open'); + document.body.classList.remove('button-editor-open'); +} + +function saveButtonFromEditor() { + var label = (document.getElementById('be-label').value || '').trim(); + var preset = (document.getElementById('be-preset').value || '').trim(); + var btn = { id: label || preset, preset: preset || label }; + var buttons = getButtonsFromDom(); + if (currentEditIndex >= 0 && currentEditIndex < buttons.length) buttons[currentEditIndex] = btn; + else buttons.push(btn); + renderButtons(buttons); + saveButtons(buttons, function() {}); + closeButtonEditor(); +} + +function showButtonContextMenu(evOrCoords, buttonEl) { + hideContextMenu(); + var x = evOrCoords.clientX != null ? evOrCoords.clientX : evOrCoords.x; + var y = evOrCoords.clientY != null ? evOrCoords.clientY : evOrCoords.y; + var buttons = getButtonsFromDom(); + var idx = parseInt(buttonEl.getAttribute('data-index'), 10); + contextMenuEl = document.createElement('div'); + contextMenuEl.className = 'context-menu'; + contextMenuEl.style.left = x + 'px'; + contextMenuEl.style.top = y + 'px'; + contextMenuEl.innerHTML = ''; + document.body.appendChild(contextMenuEl); + document.addEventListener('click', hideContextMenuOnce); + contextMenuEl.querySelector('[data-action="edit"]').onclick = function() { + hideContextMenu(); + openExistingButtonEditor(idx); + }; + contextMenuEl.querySelector('[data-action="delete"]').onclick = function() { + hideContextMenu(); + buttons.splice(idx, 1); + renderButtons(buttons); + saveButtons(buttons, function() {}); + }; +} + +function hideContextMenu() { + if (contextMenuEl && contextMenuEl.parentNode) contextMenuEl.parentNode.removeChild(contextMenuEl); + contextMenuEl = null; + document.removeEventListener('click', hideContextMenuOnce); +} + +function hideContextMenuOnce() { + hideContextMenu(); +} + +function connect() { + var proto = location.protocol === "https:" ? "wss:" : "ws:"; + ws = new WebSocket(proto + "//" + location.host + "/ws"); + ws.onclose = function() { setTimeout(connect, 2000); }; + ws.onmessage = function(ev) { + try { + var msg = JSON.parse(ev.data); + if (!msg || msg._t === 'telemetry') return; + if (msg.select != null || msg.pattern != null) { + var preset = msg.pattern || msg.select; + document.querySelectorAll('.buttons .btn').forEach(function(b) { b.classList.remove('selected'); }); + if (preset !== 'off') { + var el = document.querySelector('.buttons .btn[data-preset="' + preset + '"]'); + if (el) el.classList.add('selected'); + } + } + } catch (e) {} + }; +} + +function sendSelect(preset, el) { + var msg = JSON.stringify({ select: preset }); + if (ws && ws.readyState === WebSocket.OPEN) ws.send(msg); + document.querySelectorAll('.buttons .btn').forEach(function(b) { b.classList.remove('selected'); }); + if (el) el.classList.add('selected'); +} + +function toggleMenu() { + document.getElementById('menu').classList.toggle('open'); +} + +function closeMenu() { + document.getElementById('menu').classList.remove('open'); +} + +document.addEventListener('click', function(ev) { + var menu = document.getElementById('menu'); + var menuBtn = document.querySelector('.menu-btn'); + if (menu.classList.contains('open') && menuBtn && !menu.contains(ev.target) && !menuBtn.contains(ev.target)) { + closeMenu(); + } +}); + +function nextPreset() { + var btns = document.querySelectorAll('.buttons .btn'); + if (btns.length === 0) return; + var idx = -1; + for (var i = 0; i < btns.length; i++) { + if (btns[i].classList.contains('selected')) { idx = i; break; } + } + idx = (idx + 1) % btns.length; + var nextEl = btns[idx]; + sendSelect(nextEl.getAttribute('data-preset'), nextEl); +} + +fetch('/api/buttons') + .then(function (r) { return r.json(); }) + .then(function (data) { + var buttons = Array.isArray(data.buttons) ? data.buttons : getButtonsFromDom(); + renderButtons(buttons); + }) + .catch(function () { + renderButtons(getButtonsFromDom()); + }); + +connect(); diff --git a/src/static/main.js b/src/static/main.js index 3f53952..1138712 100644 --- a/src/static/main.js +++ b/src/static/main.js @@ -272,14 +272,27 @@ document.addEventListener("DOMContentLoaded", async function () { // Function to toggle the display of the settings menu function selectSettings() { const settingsMenu = document.getElementById("settings_menu"); - controls = document.getElementById("controls"); + const controls = document.getElementById("controls"); + const hoopMenu = document.getElementById("hoop_menu"); settingsMenu.style.display = "block"; controls.style.display = "none"; + if (hoopMenu) hoopMenu.style.display = "none"; } function selectControls() { const settingsMenu = document.getElementById("settings_menu"); - controls = document.getElementById("controls"); + const controls = document.getElementById("controls"); + const hoopMenu = document.getElementById("hoop_menu"); settingsMenu.style.display = "none"; controls.style.display = "block"; + if (hoopMenu) hoopMenu.style.display = "none"; +} + +function selectHoop() { + const settingsMenu = document.getElementById("settings_menu"); + const controls = document.getElementById("controls"); + const hoopMenu = document.getElementById("hoop_menu"); + settingsMenu.style.display = "none"; + controls.style.display = "none"; + if (hoopMenu) hoopMenu.style.display = "block"; } diff --git a/src/templates/hoop.html b/src/templates/hoop.html new file mode 100644 index 0000000..3601c2e --- /dev/null +++ b/src/templates/hoop.html @@ -0,0 +1,92 @@ +{% args buttons %} + + +
+ + +Mac address: {{mac}}