Control flowkey navigation with your piano keys (104-108). This vibecoded script supports focused menu navigation and exercise control.
// ==UserScript==
// @name flowkey MIDI Navigation
// @name:de flowkey MIDI Navigation
// @namespace Violentmonkey Scripts
// @match https://app.flowkey.com/*
// @grant none
// @version 12.5
// @run-at document-start
// @license MIT
// @description Control flowkey navigation with your piano keys (104-108). This vibecoded script supports focused menu navigation and exercise control.
// @description:de Steuere die flowkey-Navigation mit deinen Klaviertasten (104-108). Dieses vibecoded Script unterstützt fokussierte Menü-Navigation und Übungssteuerung.
// ==/UserScript==
(function() {
'use strict';
const KEYS = {
104: 'escape',
105: 'prev',
106: 'next',
107: 'repeat',
108: 'start'
};
function trigger(type) {
// --- 1. ESCAPE MODUS (Note 104) ---
if (type === 'escape') {
const esc = document.querySelector('[data-testid="exit-course"], .player-close-button, [data-testid="BackButton"], .close-button, button[class*="x49qfb"]');
if (esc) {
esc.click();
return;
}
}
const partMenu = document.querySelector('.exercise-parts-menu');
// --- 2. MODUS: Song-Teile Fokus-Navigation (Original 12.2) ---
if (partMenu && (partMenu.offsetWidth > 0)) {
const parts = Array.from(partMenu.querySelectorAll('.exercise-part'));
let currentIndex = parts.findIndex(p => p.classList.contains('selected'));
if (currentIndex === -1) currentIndex = 0;
if (type === 'next' && currentIndex < parts.length - 1) {
parts.forEach(p => p.classList.remove('selected'));
parts[currentIndex + 1].classList.add('selected');
parts[currentIndex + 1].scrollIntoView({ block: 'nearest' });
return;
} else if (type === 'prev' && currentIndex > 0) {
parts.forEach(p => p.classList.remove('selected'));
parts[currentIndex - 1].classList.add('selected');
parts[currentIndex - 1].scrollIntoView({ block: 'nearest' });
return;
} else if (type === 'start') {
const selectedPart = partMenu.querySelector('.exercise-part.selected');
if (selectedPart) selectedPart.click();
return;
}
}
// --- 3. MODUS: Standard Navigation (Original 12.2) ---
let selector = '';
if (type === 'start') {
selector = '.primary-button, [data-testid="PrimaryButton"], .isPrimary';
} else if (type === 'repeat') {
selector = '.secondary-button, [data-testid="SecondaryButton"]';
} else if (type === 'next') {
selector = '[data-testid="ArrowRight"]';
} else if (type === 'prev') {
selector = '[data-testid="ArrowLeft"]';
}
const elements = document.querySelectorAll(selector);
for (const el of elements) {
if (el && (el.offsetWidth > 0 || el.offsetHeight > 0) && el.offsetParent !== null) {
el.click();
break;
}
}
}
function handleMIDI(event) {
const [status, note, velocity] = event.data;
if (velocity > 0 && status >= 144 && status <= 159) {
const action = KEYS[note];
if (action) {
event.stopImmediatePropagation();
event.stopPropagation();
trigger(action);
}
}
}
if (navigator.requestMIDIAccess) {
navigator.requestMIDIAccess().then(access => {
for (const input of access.inputs.values()) {
input.addEventListener('midimessage', handleMIDI, true);
}
});
}
const style = document.createElement('style');
style.innerHTML = `
.buttons, .message .buttons {
display: flex !important;
flex-direction: row-reverse !important;
}
`;
document.documentElement.appendChild(style);
})();