ytyping.net専用イコライザー
// ==UserScript==
// @name YTyping Equalizer
// @namespace https://ytyping.net/
// @version 3.0.2
// @description ytyping.net専用イコライザー
// @author you
// @match https://ytyping.net/*
// @include https://www.youtube.com/embed/*origin=https%3A%2F%2Fytyping.net*
// @grant none
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const IS_YOUTUBE = location.hostname === 'www.youtube.com';
// ================================================================
// 共通:プリセット定義
// 32Hz, 64Hz, 125Hz, 250Hz, 500Hz, 1kHz, 2kHz, 4kHz, 8kHz, 16kHz
// ================================================================
const PRESETS = {
hard_rock: [ 8, 9, 7, 3, 0, -4, -2, 4, 7, 8],
extreme: [12, 13, 11, 6, 1, -6, -3, 6, 11, 13],
pop: [ 2, 3, 4, 5, 4, 3, 3, 4, 5, 4],
electronic: [ 9, 8, 5, 0, -3, -5, 2, 6, 9, 11],
synth: [13, 12, 7, -1, -5, -8, 3, 8, 12, 14],
rnb: [ 8, 9, 8, 4, 2, 2, 2, 4, 5, 5],
metal: [ 8, 9, 6, 1, -2, -6, -2, 5, 8, 10],
acoustic: [ 3, 4, 4, 3, 3, 4, 5, 5, 6, 5],
latin: [ 5, 6, 5, 3, 1, 1, 3, 5, 6, 7],
reggae: [ 9, 10, 7, 2, -1, 3, 1, 0, 3, 3],
country: [ 3, 4, 4, 3, 2, 3, 5, 6, 7, 6],
night_mode: [ 2, 3, 5, 6, 6, 5, 4, 2, 1, -1],
vocal: [-2, -1, 1, 3, 5, 8, 7, 5, 3, 1],
rhythm: [ 5, 8, 5, -3, -4, -2, 3, 5, 6, 4],
deep_bass: [15, 14, 11, 6, 1, -2, -3, -2, -1, 0],
donshari: [14, 13, 9, 2, -4,-12, -6, 4, 11, 14],
flat: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
};
// ================================================================
// Audio Engine(YouTube embed側)
// ================================================================
if (IS_YOUTUBE) {
const EQ_BANDS = [
{ type: 'lowshelf', freq: 32, q: 1.0 },
{ type: 'peaking', freq: 64, q: 1.4 },
{ type: 'peaking', freq: 125, q: 1.4 },
{ type: 'peaking', freq: 250, q: 1.4 },
{ type: 'peaking', freq: 500, q: 1.4 },
{ type: 'peaking', freq: 1000, q: 1.4 },
{ type: 'peaking', freq: 2000, q: 1.4 },
{ type: 'peaking', freq: 4000, q: 1.4 },
{ type: 'peaking', freq: 8000, q: 1.4 },
{ type: 'highshelf', freq: 16000, q: 1.0 },
];
let audioCtx = null;
let filters = [];
let gainNode = null;
let connected = false;
let eqEnabled = true;
let currentPreset = 'rhythm';
function buildEQ(ctx) {
audioCtx = ctx;
gainNode = ctx.createGain();
gainNode.gain.value = 1.0;
filters = EQ_BANDS.map((band, i) => {
const f = ctx.createBiquadFilter();
f.type = band.type;
f.frequency.value = band.freq;
f.Q.value = band.q;
f.gain.value = PRESETS[currentPreset][i];
return f;
});
gainNode.connect(filters[0]);
filters.reduce((prev, curr) => { prev.connect(curr); return curr; });
filters[filters.length - 1].connect(ctx.destination);
console.log(`[YTyping EQ] built: ${currentPreset}`);
}
function connectVideo(video) {
if (connected) return;
try {
const ctx = new AudioContext();
buildEQ(ctx);
let source;
try {
source = ctx.createMediaElementSource(video);
} catch (e) {
if (e.name === 'InvalidStateError') {
notifyParent({ type: 'YTEQ_ERROR', message: 'already_connected' });
return;
}
throw e;
}
source.connect(gainNode);
connected = true;
notifyParent({ type: 'YTEQ_CONNECTED' });
const tryResume = () => { if (ctx.state === 'suspended') ctx.resume(); };
if (!video.paused) tryResume();
video.addEventListener('play', tryResume);
document.addEventListener('click', tryResume, { once: true });
} catch (e) {
notifyParent({ type: 'YTEQ_ERROR', message: e.message });
}
}
function waitForVideo() {
const existing = document.querySelector('video');
if (existing) { connectVideo(existing); return; }
const observer = new MutationObserver(() => {
const video = document.querySelector('video');
if (video) { observer.disconnect(); connectVideo(video); }
});
observer.observe(document.documentElement, { childList: true, subtree: true });
}
function setEnabled(enabled) {
eqEnabled = enabled;
const t = audioCtx?.currentTime ?? 0;
filters.forEach((f, i) => {
f.gain.setTargetAtTime(enabled ? PRESETS[currentPreset][i] : 0, t, 0.02);
});
notifyParent({ type: 'YTEQ_TOGGLED', enabled });
}
function applyPreset(name) {
if (!PRESETS[name]) return;
currentPreset = name;
if (!eqEnabled) return;
const t = audioCtx?.currentTime ?? 0;
PRESETS[name].forEach((g, i) => {
if (filters[i]) filters[i].gain.setTargetAtTime(g, t, 0.02);
});
notifyParent({ type: 'YTEQ_PRESET_CHANGED', preset: name });
}
function setVolume(value) {
if (!gainNode) return;
gainNode.gain.setTargetAtTime(value, audioCtx?.currentTime ?? 0, 0.02);
}
function notifyParent(data) {
window.parent.postMessage(data, 'https://ytyping.net');
}
window.addEventListener('message', (e) => {
if (e.origin !== 'https://ytyping.net') return;
if (!e.data || e.data.namespace !== 'YTEQ') return;
const { type, bandIndex, gain } = e.data;
if (type === 'TOGGLE') setEnabled(!eqEnabled);
if (type === 'SET_ENABLED') setEnabled(!!e.data.enabled);
if (type === 'SET_VOLUME') setVolume(e.data.value);
if (type === 'SET_PRESET') applyPreset(e.data.preset);
if (type === 'SET_GAIN' && filters[bandIndex] !== undefined) {
filters[bandIndex].gain.setTargetAtTime(gain, audioCtx?.currentTime ?? 0, 0.01);
}
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', waitForVideo);
} else {
waitForVideo();
}
return; // Audio Engine ここまで
}
// ================================================================
// UI(ytyping.net側)
// ================================================================
const PANEL_ID = 'yteq-panel';
const NAV_BTN_ID = 'yteq-nav-btn';
let panelVisible = false;
const PRESET_LABELS = {
hard_rock: 'ROCK', extreme: 'EXTREME', pop: 'POP',
electronic: 'ELEC', synth: 'SYNTH', rnb: 'R&B',
metal: 'METAL', acoustic: 'ACOUS', latin: 'LATIN',
reggae: 'REGGAE', country: 'COUNTRY', night_mode: 'NIGHT',
vocal: 'VOCAL', rhythm: 'RHYTHM', deep_bass: 'DEEP',
donshari: 'ドンシャリ', flat: 'FLAT',
};
const PRESET_GROUPS = [
{ label: null, keys: ['rhythm','hard_rock','extreme','pop','electronic','synth','rnb','metal','acoustic','latin','reggae','country','night_mode','vocal','deep_bass','donshari','flat'] },
];
const BAND_LABELS = ['32Hz','64Hz','125Hz','250Hz','500Hz','1kHz','2kHz','4kHz','8kHz','16kHz'];
const LS_KEY = 'yteq_settings';
const LS_CUSTOM_KEY = 'yteq_custom_presets';
// ─── localStorage 保存/読み込み ───────────────────────────
function saveSettings() {
try {
localStorage.setItem(LS_KEY, JSON.stringify({
preset: currentPreset,
gains: currentGains,
volume: currentVolume,
enabled: eqEnabled,
lastSelectedPreset,
}));
} catch (e) {}
}
function loadSettings() {
try {
const s = JSON.parse(localStorage.getItem(LS_KEY));
if (!s) return;
if (s.preset && PRESETS[s.preset]) { currentPreset = s.preset; lastSelectedPreset = s.preset; }
if (Array.isArray(s.gains) && s.gains.length === 10) currentGains = s.gains;
if (typeof s.volume === 'number') currentVolume = s.volume;
if (typeof s.enabled === 'boolean') eqEnabled = s.enabled;
if (typeof s.lastSelectedPreset === 'string') lastSelectedPreset = s.lastSelectedPreset;
} catch (e) {}
}
function saveCustomPresets() {
try {
localStorage.setItem(LS_CUSTOM_KEY, JSON.stringify(customPresets));
} catch (e) {}
}
function loadCustomPresets() {
try {
const c = JSON.parse(localStorage.getItem(LS_CUSTOM_KEY));
if (Array.isArray(c)) customPresets = c;
} catch (e) {}
}
let eqConnected = false;
let eqEnabled = true;
let currentPreset = 'rhythm';
let currentGains = [...PRESETS['rhythm']];
let currentVolume = 100;
let customPresets = [];
let lastSelectedPreset = 'rhythm'; // 最後に選択したプリセットキー('custom:N' or preset key) // [{ name, gains }, ...]
// 起動時にlocalStorageから復元
loadCustomPresets();
loadSettings();
function sendToEmbed(data) {
const iframe = document.querySelector('iframe[src*="youtube.com/embed"]');
if (!iframe) return;
iframe.contentWindow.postMessage({ namespace: 'YTEQ', ...data }, 'https://www.youtube.com');
}
window.addEventListener('message', (e) => {
if (e.origin !== 'https://www.youtube.com') return;
if (!e.data) return;
if (e.data.type === 'YTEQ_CONNECTED') {
eqConnected = true;
updateStatus(true);
// 保存済み設定をembedに送信して反映
sendToEmbed({ type: 'SET_PRESET', preset: currentPreset });
sendToEmbed({ type: 'SET_VOLUME', value: currentVolume / 100 });
if (!eqEnabled) sendToEmbed({ type: 'SET_ENABLED', enabled: false });
// 個別にgainが変わっている場合(カスタム状態)は各バンドを送信
const presetGains = PRESETS[currentPreset];
if (!presetGains.every((v, i) => v === currentGains[i])) {
currentGains.forEach((g, i) => sendToEmbed({ type: 'SET_GAIN', bandIndex: i, gain: g }));
}
}
if (e.data.type === 'YTEQ_ERROR') { updateStatus(false); }
if (e.data.type === 'YTEQ_TOGGLED') { eqEnabled = e.data.enabled; updateToggleBtn(); }
if (e.data.type === 'YTEQ_PRESET_CHANGED') { currentPreset = e.data.preset; updatePresetButtons(); }
});
function createUI() {
if (document.getElementById(PANEL_ID)) return;
const style = document.createElement('style');
style.textContent = `
#yteq-panel {
position: fixed; bottom: 20px; right: 20px; z-index: 999999;
background: #0f0f14; border: 1px solid #2a2a3a; border-radius: 10px;
font-family: 'JetBrains Mono', 'Consolas', monospace; font-size: 11px;
color: #c8c8e0; box-shadow: 0 8px 32px rgba(0,0,0,0.6);
user-select: none; width: 340px;
transition: opacity 0.2s, transform 0.2s;
}
#yteq-panel.hidden {
opacity: 0; pointer-events: none;
transform: translateY(8px);
}
#yteq-nav-btn {
display: inline-flex; align-items: center; justify-content: center;
padding: 4px 8px; border-radius: 4px; cursor: pointer;
font-size: 12px; font-weight: 700; letter-spacing: 0.05em;
color: #aaaacc; border: 1px solid transparent;
transition: all 0.15s; background: transparent;
font-family: 'JetBrains Mono', 'Consolas', monospace;
}
#yteq-nav-btn:hover { color: #ffffff; background: rgba(255,255,255,0.08); }
#yteq-nav-btn.active { color: #9999ff; border-color: #5555bb; background: #1e1e3a; }
#yteq-nav-btn.eq-on { color: #66cc66; }
#yteq-nav-btn.eq-on.active { color: #66cc66; border-color: #5555bb; background: #1e1e3a; }
#yteq-header {
display: flex; align-items: center; gap: 8px; padding: 8px 12px;
border-bottom: 1px solid #2a2a3a; cursor: move;
}
#yteq-title { font-weight: 700; font-size: 12px; flex: 1; letter-spacing: 0.05em; }
#yteq-status { font-size: 10px; opacity: 0.7; }
#yteq-toggle-btn {
background: #1a2a1a; border: 1px solid #3a6a3a; color: #66cc66;
border-radius: 4px; padding: 2px 8px; cursor: pointer;
font-size: 10px; font-family: inherit; font-weight: 700;
letter-spacing: 0.05em; transition: all 0.15s;
}
#yteq-toggle-btn.off { background: #1a1a1a; border-color: #3a3a3a; color: #555; }
#yteq-toggle-btn:hover { opacity: 0.8; }
#yteq-body { padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
.yteq-group-label { font-size: 9px; color: #4a4a6a; letter-spacing: 0.08em; margin-bottom: 3px; }
.yteq-preset-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 3px; margin-bottom: 2px; }
.yteq-preset-btn {
background: #1a1a2a; border: 1px solid #2a2a3a; color: #6a6a9a;
border-radius: 4px; padding: 4px 2px; cursor: pointer;
font-size: 9px; font-family: inherit; font-weight: 700;
letter-spacing: 0.04em; text-align: center; transition: all 0.12s;
}
.yteq-preset-btn:hover { background: #22223a; color: #9a9acc; border-color: #4a4a6a; }
.yteq-preset-btn.active { background: #1e1e3a; border-color: #5555bb; color: #9999ff; }
#yteq-eq-section { border-top: 1px solid #1e1e2e; padding-top: 8px; }
#yteq-eq-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
#yteq-eq-label { font-size: 9px; color: #4a4a6a; letter-spacing: 0.08em; }
#yteq-eq-reset {
background: #1a1a2a; border: 1px solid #2a2a3a; color: #6a6a9a;
border-radius: 3px; padding: 1px 7px; cursor: pointer;
font-size: 9px; font-family: inherit;
}
#yteq-eq-reset:hover { background: #22223a; color: #9a9acc; }
#yteq-eq-reset.hidden { display: none; }
#yteq-eq-sliders { display: flex; gap: 2px; align-items: flex-end; justify-content: space-between; }
.yteq-band { display: flex; flex-direction: column; align-items: center; gap: 3px; flex: 1; }
.yteq-band-val { font-size: 7px; color: #6a6a8a; min-width: 20px; text-align: center; }
.yteq-band-val.pos { color: #7878cc; }
.yteq-band-val.neg { color: #aa6666; }
.yteq-band-slider {
-webkit-appearance: none; writing-mode: vertical-lr; direction: rtl;
width: 20px; height: 90px; background: transparent; cursor: pointer;
}
.yteq-band-slider::-webkit-slider-runnable-track { width: 3px; background: #2a2a3a; border-radius: 2px; }
.yteq-band-slider::-webkit-slider-thumb {
-webkit-appearance: none; width: 11px; height: 11px;
background: #5555cc; border-radius: 50%; border: 2px solid #8888ff;
margin-left: -4px; transition: background 0.1s;
}
.yteq-band-slider::-webkit-slider-thumb:hover { background: #7777ff; }
.yteq-band-label { font-size: 7px; color: #4a4a6a; text-align: center; }
#yteq-volume-row {
display: flex; align-items: center; gap: 8px; font-size: 10px; color: #6a6a8a;
padding-top: 8px; border-top: 1px solid #1e1e2e;
}
#yteq-volume-label { white-space: nowrap; }
#yteq-volume-val { min-width: 34px; text-align: right; color: #9a9acc; font-weight: 700; }
#yteq-volume-slider {
-webkit-appearance: none; flex: 1; height: 3px;
background: #2a2a3a; border-radius: 2px; outline: none; cursor: pointer;
}
#yteq-volume-slider::-webkit-slider-thumb {
-webkit-appearance: none; width: 13px; height: 13px;
background: #6666cc; border-radius: 50%; border: 2px solid #9999ff;
}
#yteq-volume-slider::-webkit-slider-thumb:hover { background: #8888ff; }
#yteq-custom-section { border-top: 1px solid #1e1e2e; padding-top: 8px; }
#yteq-custom-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
#yteq-custom-label { font-size: 9px; color: #4a4a6a; letter-spacing: 0.08em; }
#yteq-custom-save {
background: #1a2a1a; border: 1px solid #3a6a3a; color: #66cc66;
border-radius: 3px; padding: 1px 7px; cursor: pointer;
font-size: 9px; font-family: inherit; font-weight: 700;
}
#yteq-custom-save:hover { background: #223a22; }
#yteq-custom-list { display: flex; flex-direction: column; gap: 3px; }
.yteq-custom-item {
display: flex; align-items: center; gap: 4px;
}
.yteq-custom-btn {
flex: 1; background: #1a1a2a; border: 1px solid #2a2a3a; color: #6a6a9a;
border-radius: 4px; padding: 4px 6px; cursor: pointer;
font-size: 9px; font-family: inherit; font-weight: 700;
text-align: left; transition: all 0.12s; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap;
}
.yteq-custom-btn:hover { background: #22223a; color: #9a9acc; border-color: #4a4a6a; }
.yteq-custom-btn.active { background: #1e1e3a; border-color: #5555bb; color: #9999ff; }
.yteq-custom-del {
background: transparent; border: 1px solid #3a2a2a; color: #664444;
border-radius: 3px; padding: 2px 5px; cursor: pointer;
font-size: 9px; font-family: inherit; flex-shrink: 0;
transition: all 0.12s;
}
.yteq-custom-del:hover { background: #2a1a1a; color: #cc6666; border-color: #6a3a3a; }
#yteq-custom-empty { font-size: 9px; color: #3a3a5a; text-align: center; padding: 4px 0; }
`;
document.head.appendChild(style);
const panel = document.createElement('div');
panel.id = PANEL_ID;
const groupsHTML = PRESET_GROUPS.map(group => {
const btns = group.keys.map(key =>
`<button class="yteq-preset-btn${key === lastSelectedPreset ? ' active' : ''}" data-preset="${key}">${PRESET_LABELS[key]}</button>`
).join('');
return `<div>
${group.label ? `<div class="yteq-group-label">${group.label}</div>` : ''}
<div class="yteq-preset-grid">${btns}</div>
</div>`;
}).join('');
const slidersHTML = BAND_LABELS.map((label, i) => {
const g = currentGains[i];
const sign = g > 0 ? '+' : '';
const cls = g > 0 ? 'pos' : g < 0 ? 'neg' : '';
return `<div class="yteq-band">
<span class="yteq-band-val ${cls}" id="yteq-val-${i}">${sign}${g}</span>
<input class="yteq-band-slider" type="range" id="yteq-slider-${i}" min="-15" max="15" step="0.5" value="${g}" />
<span class="yteq-band-label">${label}</span>
</div>`;
}).join('');
panel.innerHTML = `
<div id="yteq-header">
<span id="yteq-title">YTyping EQ</span>
<span id="yteq-status">⚪ Disconnected</span>
<button id="yteq-toggle-btn">ON</button>
</div>
<div id="yteq-body">
${groupsHTML}
<div id="yteq-eq-section">
<div id="yteq-eq-header">
<span id="yteq-eq-label">BANDS</span>
<button id="yteq-eq-reset">RESET</button>
</div>
<div id="yteq-eq-sliders">${slidersHTML}</div>
<div id="yteq-volume-row">
<span id="yteq-volume-label">🔊 VOL</span>
<input id="yteq-volume-slider" type="range" min="0" max="200" step="1" value="${currentVolume}" />
<span id="yteq-volume-val">${currentVolume}%</span>
</div>
</div>
<div id="yteq-custom-section">
<div id="yteq-custom-header">
<span id="yteq-custom-label">CUSTOM</span>
<button id="yteq-custom-save">+ Save current</button>
</div>
<div id="yteq-custom-list"></div>
</div>
</div>
`;
makeDraggable(panel, panel.querySelector('#yteq-header'));
document.body.appendChild(panel);
if (!panelVisible) panel.classList.add('hidden');
// カスタムプリセット保存
panel.querySelector('#yteq-custom-save').addEventListener('click', () => {
const name = prompt('Enter preset name:');
if (!name || !name.trim()) return;
customPresets.push({ name: name.trim(), gains: [...currentGains] });
saveCustomPresets();
renderCustomPresets();
});
renderCustomPresets();
panel.querySelectorAll('.yteq-preset-btn').forEach(btn => {
btn.addEventListener('click', () => {
const preset = btn.dataset.preset;
currentPreset = preset;
lastSelectedPreset = preset;
currentGains = [...PRESETS[preset]];
sendToEmbed({ type: 'SET_PRESET', preset });
updatePresetButtons();
updateSliders();
updateResetBtn();
saveSettings();
});
});
BAND_LABELS.forEach((_, i) => {
const slider = panel.querySelector(`#yteq-slider-${i}`);
const valEl = panel.querySelector(`#yteq-val-${i}`);
slider.addEventListener('input', () => {
const g = parseFloat(slider.value);
currentGains[i] = g;
const sign = g > 0 ? '+' : '';
valEl.textContent = `${sign}${g}`;
valEl.className = `yteq-band-val ${g > 0 ? 'pos' : g < 0 ? 'neg' : ''}`;
updateResetBtn();
sendToEmbed({ type: 'SET_GAIN', bandIndex: i, gain: g });
saveSettings();
});
});
panel.querySelector('#yteq-eq-reset').addEventListener('click', () => {
// lastSelectedPresetがカスタムか組み込みかで分岐
if (lastSelectedPreset.startsWith('custom:')) {
const idx = parseInt(lastSelectedPreset.split(':')[1]);
const p = customPresets[idx];
if (p) { currentGains = [...p.gains]; }
} else if (PRESETS[lastSelectedPreset]) {
currentPreset = lastSelectedPreset;
currentGains = [...PRESETS[lastSelectedPreset]];
sendToEmbed({ type: 'SET_PRESET', preset: lastSelectedPreset });
}
updateSliders();
updatePresetButtons();
updateResetBtn();
saveSettings();
});
const volSlider = panel.querySelector('#yteq-volume-slider');
const volVal = panel.querySelector('#yteq-volume-val');
volSlider.addEventListener('input', () => {
const pct = parseInt(volSlider.value);
currentVolume = pct;
volVal.textContent = `${pct}%`;
sendToEmbed({ type: 'SET_VOLUME', value: pct / 100 });
saveSettings();
});
panel.querySelector('#yteq-toggle-btn').addEventListener('click', () => {
eqEnabled = !eqEnabled;
sendToEmbed({ type: 'TOGGLE' });
updateToggleBtn();
updateNavBtn();
saveSettings();
});
if (eqConnected) updateStatus(true);
updateToggleBtn();
updateResetBtn();
}
function renderCustomPresets() {
const list = document.getElementById('yteq-custom-list');
if (!list) return;
if (customPresets.length === 0) {
list.innerHTML = '<div id="yteq-custom-empty">No saved presets</div>';
return;
}
list.innerHTML = customPresets.map((p, idx) => `
<div class="yteq-custom-item">
<button class="yteq-custom-btn${JSON.stringify(p.gains) === JSON.stringify(currentGains) ? ' active' : ''}"
data-idx="${idx}">${p.name}</button>
<button class="yteq-custom-del" data-idx="${idx}">✕</button>
</div>
`).join('');
list.querySelectorAll('.yteq-custom-btn').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.idx);
const p = customPresets[idx];
if (!p) return;
currentGains = [...p.gains];
currentPreset = '';
lastSelectedPreset = `custom:${idx}`;
updatePresetButtons();
updateSliders();
updateResetBtn();
renderCustomPresets();
p.gains.forEach((g, i) => sendToEmbed({ type: 'SET_GAIN', bandIndex: i, gain: g }));
saveSettings();
});
});
list.querySelectorAll('.yteq-custom-del').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.idx);
customPresets.splice(idx, 1);
saveCustomPresets();
renderCustomPresets();
});
});
}
function updateSliders() {
BAND_LABELS.forEach((_, i) => {
const slider = document.querySelector(`#yteq-slider-${i}`);
const valEl = document.querySelector(`#yteq-val-${i}`);
if (!slider || !valEl) return;
const g = currentGains[i];
slider.value = g;
const sign = g > 0 ? '+' : '';
valEl.textContent = `${sign}${g}`;
valEl.className = `yteq-band-val ${g > 0 ? 'pos' : g < 0 ? 'neg' : ''}`;
});
}
function updatePresetButtons() {
document.querySelectorAll('.yteq-preset-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.preset === lastSelectedPreset);
});
document.querySelectorAll('.yteq-custom-btn').forEach(btn => {
const idx = parseInt(btn.dataset.idx);
btn.classList.toggle('active', lastSelectedPreset === `custom:${idx}`);
});
}
function updateResetBtn() {
const btn = document.getElementById('yteq-eq-reset');
if (!btn) return;
// lastSelectedPresetの値と現在のgainsが一致するか確認
let baseGains = null;
if (lastSelectedPreset.startsWith('custom:')) {
const idx = parseInt(lastSelectedPreset.split(':')[1]);
baseGains = customPresets[idx]?.gains ?? null;
} else if (PRESETS[lastSelectedPreset]) {
baseGains = PRESETS[lastSelectedPreset];
}
const isModified = baseGains && !baseGains.every((v, i) => v === currentGains[i]);
btn.classList.toggle('hidden', !isModified);
}
function updateStatus(ok) {
const el = document.getElementById('yteq-status');
if (!el) return;
el.textContent = ok ? '🟢 Connected' : '⚪ Disconnected';
}
function updateToggleBtn() {
const btn = document.getElementById('yteq-toggle-btn');
if (!btn) return;
btn.textContent = eqEnabled ? 'ON' : 'OFF';
btn.classList.toggle('off', !eqEnabled);
}
function updateNavBtn() {
const btn = document.getElementById(NAV_BTN_ID);
if (!btn) return;
btn.classList.toggle('eq-on', eqEnabled);
}
function makeDraggable(el, handle) {
let ox = 0, oy = 0, mx = 0, my = 0;
handle.addEventListener('mousedown', e => {
e.preventDefault();
mx = e.clientX; my = e.clientY;
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
function onMove(e) {
ox = mx - e.clientX; oy = my - e.clientY;
mx = e.clientX; my = e.clientY;
el.style.top = `${el.offsetTop - oy}px`;
el.style.left = `${el.offsetLeft - ox}px`;
el.style.right = 'auto'; el.style.bottom = 'auto';
}
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
}
function togglePanel() {
panelVisible = !panelVisible;
const panel = document.getElementById(PANEL_ID);
const btn = document.getElementById(NAV_BTN_ID);
if (panel) panel.classList.toggle('hidden', !panelVisible);
if (btn) btn.classList.toggle('active', panelVisible);
}
function insertNavBtn() {
if (document.getElementById(NAV_BTN_ID)) return;
const navIcons = document.getElementById('right-nav-icons');
if (!navIcons) return;
const btn = document.createElement('button');
btn.id = NAV_BTN_ID;
btn.textContent = 'EQ';
btn.classList.toggle('active', panelVisible);
btn.addEventListener('click', togglePanel);
navIcons.prepend(btn);
updateNavBtn();
}
function watchDOM() {
const observer = new MutationObserver(() => {
if (!document.getElementById(PANEL_ID)) createUI();
insertNavBtn();
});
observer.observe(document.documentElement, { childList: true, subtree: false });
if (document.body) observer.observe(document.body, { childList: true, subtree: true });
}
// DOMが準備できたらUI初期化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => { createUI(); insertNavBtn(); watchDOM(); });
} else {
createUI();
insertNavBtn();
watchDOM();
}
})();