您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Saves and persists MIDI input/output options for you
// ==UserScript== // @name Multiplayer Piano Optimizations [Input/Output] // @namespace https://tampermonkey.net/ // @version 1.0.2 // @description Saves and persists MIDI input/output options for you // @author zackiboiz // @match *://*.multiplayerpiano.com/* // @match *://*.multiplayerpiano.net/* // @match *://dev.multiplayerpiano.net/* // @match *://*.multiplayerpiano.org/* // @match *://*.multiplayerpiano.dev/* // @match *://piano.mpp.community/* // @match *://mpp.7458.space/* // @match *://qmppv2.qwerty0301.repl.co/* // @match *://mpp.8448.space/* // @match *://mpp.autoplayer.xyz/* // @match *://mpp.hyye.xyz/* // @match *://mpp.smp-meow.net/* // @match *://piano.ourworldofpixels.com/* // @match *://mpp.lapishusky.dev/* // @match *://staging-mpp.sad.ovh/* // @icon https://www.google.com/s2/favicons?sz=64&domain=multiplayerpiano.net // @grant GM_info // @license MIT // @run-at document-start // ==/UserScript== (async () => { const dl = GM_info.script.downloadURL || GM_info.script.updateURL || GM_info.script.homepageURL || ""; const match = dl.match(/greasyfork\.org\/scripts\/(\d+)/); if (!match) { console.warn("Could not find Greasy Fork script ID in downloadURL/updateURL/homepageURL:", dl); } else { const scriptId = match[1]; const localVersion = GM_info.script.version; const apiUrl = `https://greasyfork.org/scripts/${scriptId}.json`; fetch(apiUrl, { mode: "cors", headers: { Accept: "application/json" } }).then(r => { if (!r.ok) throw new Error("Failed to fetch Greasy Fork data."); return r.json(); }).then(data => { const remoteVersion = data.version; if (compareVersions(localVersion, remoteVersion) < 0) { new MPP.Notification({ "m": "notification", "duration": 15000, "title": "Update Available", "html": "<p>A new version of this script is available!</p>" + `<p style="margin-top: 10px;">Script: ${GM_info.script.name}</p>` + `<p>Local: v${localVersion}</p>` + `<p>Latest: v${remoteVersion}</p>` + `<a href="https://greasyfork.org/scripts/${scriptId}" target="_blank" style="position: absolute; right: 0;bottom: 0; margin: 10px; font-size: 0.5rem;">Open Greasy Fork to update?</a>` }) } }).catch(err => console.error("Update check failed:", err)); } function compareVersions(a, b) { const pa = a.split(".").map(n => parseInt(n, 10) || 0); const pb = b.split(".").map(n => parseInt(n, 10) || 0); const len = Math.max(pa.length, pb.length); for (let i = 0; i < len; i++) { if ((pa[i] || 0) < (pb[i] || 0)) return -1; if ((pa[i] || 0) > (pb[i] || 0)) return 1; } return 0; } const STORAGE_KEY = "midiConnections"; const SAVE_DEBOUNCE_MS = 40; function loadMap() { try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : {}; } catch (e) { console.warn("MIDI persist load failed", e); return {}; } } function saveMap(map) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(map)); } catch (e) { console.warn("MIDI persist save failed", e); } } function idKey(kind, id) { return `${kind}:id:${(id || "").trim()}`; } function nameKey(kind, name) { return `${kind}:name:${(name || "").trim()}`; } function tn(s) { return (s || "").trim(); } function getSavedForDevice(map, kind, device) { if (!device) return undefined; if (device.id) { const k = idKey(kind, device.id); if (Object.prototype.hasOwnProperty.call(map, k)) return !!map[k]; } const nk = nameKey(kind, device.name || ""); if (Object.prototype.hasOwnProperty.call(map, nk)) return !!map[nk]; return undefined; } function setSavedForDevice(map, kind, device, enabled) { if (device && device.id) map[idKey(kind, device.id)] = !!enabled; if (device && device.name) map[nameKey(kind, device.name)] = !!enabled; } function applySavedStates(midi) { if (!midi) return; const map = loadMap(); // inputs for (let it = midi.inputs.values(), n = it.next(); n && !n.done; n = it.next()) { const input = n.value; const saved = getSavedForDevice(map, "input", input); if (typeof saved !== "undefined") { try { input.enabled = !!saved; } catch (e) { /* ignore */ } } } // outputs for (let it = midi.outputs.values(), n = it.next(); n && !n.done; n = it.next()) { const output = n.value; const saved = getSavedForDevice(map, "output", output); if (typeof saved !== "undefined") { try { output.enabled = !!saved; } catch (e) { /* ignore */ } } } } function findDevice(midi, identifier, preferredKind) { if (!midi || !identifier) return null; const t = tn(identifier); function tryFindById(kind) { const list = kind === "input" ? midi.inputs.values() : midi.outputs.values(); for (let it = list, r = it.next(); r && !r.done; r = it.next()) { const dev = r.value; if (dev.id && dev.id === identifier) return { dev, kind }; } return null; } function tryFindByName(kind) { const list = kind === "input" ? midi.inputs.values() : midi.outputs.values(); for (let it = list, r = it.next(); r && !r.done; r = it.next()) { const dev = r.value; if (tn(dev.name) === t) return { dev, kind }; } return null; } if (preferredKind === "output") { return tryFindById("output") || tryFindByName("output") || tryFindById("input") || tryFindByName("input"); } else if (preferredKind === "input") { return tryFindById("input") || tryFindByName("input") || tryFindById("output") || tryFindByName("output"); } else { return tryFindById("input") || tryFindByName("input") || tryFindById("output") || tryFindByName("output"); } } function detectKindFromElement(el) { if (!el) return null; try { let ancestor = el; while (ancestor && ancestor !== document.body) { if (ancestor.tagName && /^H\d$/i.test(ancestor.tagName) && /inputs?/i.test(ancestor.textContent || "")) return "input"; if (ancestor.tagName && /^H\d$/i.test(ancestor.tagName) && /outputs?/i.test(ancestor.textContent || "")) return "output"; ancestor = ancestor.parentElement; } const ul = el.closest("ul"); if (ul) { let prev = ul.previousElementSibling; while (prev) { if (prev.tagName && /^H\d$/i.test(prev.tagName)) { if (/inputs?/i.test(prev.textContent || "")) return "input"; if (/outputs?/i.test(prev.textContent || "")) return "output"; } prev = prev.previousElementSibling; } } } catch (e) { } return null; } const lastSaved = new Map(); function shouldSaveKey(key, val) { const now = Date.now(); const prev = lastSaved.get(key); if (prev && prev.val === !!val && (now - prev.ts) < 1000) { return false; } lastSaved.set(key, { val: !!val, ts: now }); return true; } function persistDeviceKindState(kind, device, enabled) { if (!kind || !device) return; const map = loadMap(); if (device.id) { const k = idKey(kind, device.id); if (shouldSaveKey(k, enabled)) map[k] = !!enabled; } if (device.name) { const kn = nameKey(kind, device.name); if (shouldSaveKey(kn, enabled)) map[kn] = !!enabled; } saveMap(map); } function persistByName(kind, name, enabled) { if (!name || !kind) return; const map = loadMap(); const kn = nameKey(kind, name); if (!shouldSaveKey(kn, enabled)) return; map[kn] = !!enabled; saveMap(map); } if (navigator.requestMIDIAccess) { const orig = navigator.requestMIDIAccess.bind(navigator); navigator.requestMIDIAccess = function (options) { return orig(options).then((midi) => { try { applySavedStates(midi); } catch (e) { console.warn("MIDI persist apply error", e); } midi.addEventListener("statechange", () => { setTimeout(() => { try { applySavedStates(midi); } catch (e) { } }, 60); }); const processedElements = new WeakSet(); let saveTimer = null; let pending = []; function flushPending() { if (!pending.length) return; const copy = pending.slice(); pending = []; for (const item of copy) { const { el, displayName } = item; const detectedKind = detectKindFromElement(el) || null; const found = findDevice(midi, displayName, detectedKind || undefined); if (found && found.dev) { persistDeviceKindState(found.kind, found.dev, !!found.dev.enabled); } else { const kindToSave = detectedKind || "input"; const enabledFromClass = !!(el.classList && el.classList.contains("enabled")); persistByName(kindToSave, displayName, enabledFromClass); } } } document.addEventListener("click", function (ev) { const target = ev.target; if (!target || typeof target.closest !== "function") return; const li = target.closest(".connection"); if (!li) return; if (processedElements.has(li)) { // :hiiiperz: } else { processedElements.add(li); setTimeout(() => processedElements.delete(li), 800); } const nameAttr = (li.getAttribute && (li.getAttribute("data-name") || li.getAttribute("title"))) || null; const displayName = tn(nameAttr || li.textContent || ""); pending.push({ el: li, displayName }); if (saveTimer) clearTimeout(saveTimer); saveTimer = setTimeout(() => { saveTimer = null; flushPending(); }, SAVE_DEBOUNCE_MS); }, false); const seen = new WeakSet(); const observer = new MutationObserver((mutations) => { for (const m of mutations) { if (!m.addedNodes || !m.addedNodes.length) continue; for (const node of m.addedNodes) { if (!node || node.nodeType !== 1) continue; const elList = []; if (node.classList && node.classList.contains("connection")) elList.push(node); node.querySelectorAll && node.querySelectorAll(".connection").forEach(x => elList.push(x)); for (const el of elList) { if (seen.has(el)) continue; seen.add(el); const nameAttr = (el.getAttribute && (el.getAttribute("data-name") || el.getAttribute("title"))) || null; const displayName = tn(nameAttr || el.textContent || ""); const kind = detectKindFromElement(el); if (!displayName) continue; const found = findDevice(midi, displayName, kind || undefined); const map = loadMap(); if (found && found.dev) { const saved = getSavedForDevice(map, found.kind, found.dev); if (typeof saved !== "undefined") { try { found.dev.enabled = !!saved; } catch (e) { } } } else if (kind) { const kn = nameKey(kind, displayName); if (Object.prototype.hasOwnProperty.call(map, kn)) { if (map[kn]) el.classList.add("enabled"); else el.classList.remove("enabled"); } } else { // :catkiss: } } } } }); try { const root = document.body || document.documentElement; observer.observe(root, { childList: true, subtree: true }); } catch (e) { /* ignore */ } return midi; }); }; } else { console.log("MIDI persist navigator.requestMIDIAccess not found"); } })();