gotoukun510.vercel.app専用イコライザー(navボタン→popover版)
// ==UserScript==
// @name GOTOUkun510 Typing Equalizer
// @namespace https://gotoukun510.vercel.app/
// @version 5.0.1
// @description gotoukun510.vercel.app専用イコライザー(navボタン→popover版)
// @author you
// @match https://gotoukun510.vercel.app/*
// @include https://www.youtube.com/embed/*origin=https%3A%2F%2Fgotoukun510.vercel.app*
// @grant none
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const IS_YOUTUBE = location.hostname === 'www.youtube.com';
const SITE_ORIGIN = 'https://gotoukun510.vercel.app';
// ================================================================
// 共通:プリセット定義
// 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],
// 立体音響系
spatial: [-1, 0, 1, -2, -3, 2, 5, 6, 9, 10],
surround: [ 4, 5, 3, -1, -4, 1, 4, 7, 8, 6],
air: [-2, -1, 0, -3, -2, 1, 4, 8, 11, 13],
};
// ================================================================
// 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, filters = [], gainNode = null;
let connected = false, eqEnabled = true, 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((a, b) => { a.connect(b); return b; });
filters[filters.length - 1].connect(ctx.destination);
}
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 v = document.querySelector('video');
if (v) { connectVideo(v); return; }
const obs = new MutationObserver(() => {
const v2 = document.querySelector('video');
if (v2) { obs.disconnect(); connectVideo(v2); }
});
obs.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(v) {
if (!gainNode) return;
gainNode.gain.setTargetAtTime(v, audioCtx?.currentTime ?? 0, 0.02);
}
function notifyParent(data) { window.parent.postMessage(data, SITE_ORIGIN); }
window.addEventListener('message', (e) => {
if (e.origin !== SITE_ORIGIN) 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);
});
document.readyState === 'loading'
? document.addEventListener('DOMContentLoaded', waitForVideo)
: waitForVideo();
return;
}
// ================================================================
// UI(gotoukun510.vercel.app側)
// ================================================================
const POPOVER_ID = 'yteq-popover';
const NAV_BTN_ID = 'yteq-nav-btn';
const LS_KEY = 'yteq_settings';
const LS_CUSTOM_KEY = 'yteq_custom_presets';
const PRESET_LABELS = {
rhythm: 'RHYTHM', hard_rock: 'ROCK', extreme: 'EXTREME',
pop: 'POP', electronic: 'ELEC', synth: 'SYNTH',
rnb: 'R&B', metal: 'METAL', acoustic: 'ACOUS',
latin: 'LATIN', reggae: 'REGGAE', country: 'CNTRY',
night_mode: 'NIGHT', vocal: 'VOCAL', deep_bass: 'DEEP',
donshari: 'ドンシャリ', flat: 'FLAT',
spatial: '立体音響', surround: 'サラウンド', air: 'エアー',
};
const PRESET_KEYS = Object.keys(PRESET_LABELS);
const BAND_LABELS = ['32Hz','64Hz','125Hz','250Hz','500Hz','1kHz','2kHz','4kHz','8kHz','16kHz'];
// ── 状態 ──────────────────────────────────────────────────
let eqConnected = false;
let eqEnabled = true;
let currentPreset = 'rhythm';
let currentGains = [...PRESETS['rhythm']];
let currentVolume = 100;
let customPresets = [];
let lastSelectedPreset = 'rhythm';
let popoverOpen = false;
// ── 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) {
currentPreset = 'rhythm';
currentGains = [...PRESETS['rhythm']];
lastSelectedPreset = 'rhythm';
return;
}
if (s.preset && PRESETS[s.preset]) currentPreset = 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' &&
(PRESETS[s.lastSelectedPreset] || s.lastSelectedPreset.startsWith('custom:')))
lastSelectedPreset = s.lastSelectedPreset;
else
lastSelectedPreset = currentPreset || 'rhythm';
} catch (e) {
currentPreset = 'rhythm';
currentGains = [...PRESETS['rhythm']];
lastSelectedPreset = 'rhythm';
}
}
function saveCustom() {
try { localStorage.setItem(LS_CUSTOM_KEY, JSON.stringify(customPresets)); } catch (e) {}
}
function loadCustom() {
try {
const c = JSON.parse(localStorage.getItem(LS_CUSTOM_KEY));
if (Array.isArray(c)) customPresets = c;
} catch (e) {}
}
loadCustom();
loadSettings();
// ── embed 送信 ─────────────────────────────────────────────
function sendToEmbed(data) {
const iframe = document.querySelector('iframe[src*="youtube.com/embed"]');
if (iframe) iframe.contentWindow.postMessage({ namespace: 'YTEQ', ...data }, 'https://www.youtube.com');
}
// ── postMessage 受信 ───────────────────────────────────────
window.addEventListener('message', (e) => {
if (e.origin !== 'https://www.youtube.com') return;
if (!e.data) return;
if (e.data.type === 'YTEQ_CONNECTED') {
eqConnected = true;
syncStatus(true);
sendToEmbed({ type: 'SET_PRESET', preset: currentPreset });
sendToEmbed({ type: 'SET_VOLUME', value: currentVolume / 100 });
if (!eqEnabled) sendToEmbed({ type: 'SET_ENABLED', enabled: false });
const base = PRESETS[currentPreset];
if (!base || !base.every((v, i) => v === currentGains[i]))
currentGains.forEach((g, i) => sendToEmbed({ type: 'SET_GAIN', bandIndex: i, gain: g }));
}
if (e.data.type === 'YTEQ_ERROR') syncStatus(false);
if (e.data.type === 'YTEQ_TOGGLED') { eqEnabled = e.data.enabled; syncToggle(); }
if (e.data.type === 'YTEQ_PRESET_CHANGED') { currentPreset = e.data.preset; syncPresets(); }
});
// ================================================================
// アクション(状態変更の単一窓口)
// ================================================================
function doToggle() {
eqEnabled = !eqEnabled;
sendToEmbed({ type: 'TOGGLE' });
syncToggle();
saveSettings();
}
function doSelectPreset(key) {
currentPreset = lastSelectedPreset = key;
currentGains = [...PRESETS[key]];
sendToEmbed({ type: 'SET_PRESET', preset: key });
syncPresets(); syncSliders(); syncReset();
saveSettings();
}
function doBandChange(i, g) {
currentGains[i] = g;
syncBandVal(i, g);
sendToEmbed({ type: 'SET_GAIN', bandIndex: i, gain: g });
syncReset();
saveSettings();
}
function doSetVolume(pct) {
currentVolume = pct;
syncVolLabel();
sendToEmbed({ type: 'SET_VOLUME', value: pct / 100 });
saveSettings();
}
function doReset() {
if (lastSelectedPreset.startsWith('custom:')) {
const p = customPresets[parseInt(lastSelectedPreset.split(':')[1])];
if (p) currentGains = [...p.gains];
} else if (PRESETS[lastSelectedPreset]) {
currentPreset = lastSelectedPreset;
currentGains = [...PRESETS[lastSelectedPreset]];
sendToEmbed({ type: 'SET_PRESET', preset: lastSelectedPreset });
}
syncSliders(); syncPresets(); syncReset();
saveSettings();
}
function doLoadCustom(idx) {
const p = customPresets[idx];
if (!p) return;
currentGains = [...p.gains];
currentPreset = '';
lastSelectedPreset = `custom:${idx}`;
p.gains.forEach((g, i) => sendToEmbed({ type: 'SET_GAIN', bandIndex: i, gain: g }));
syncPresets(); syncSliders(); syncReset();
renderCustomList();
saveSettings();
}
function doDelCustom(idx) {
customPresets.splice(idx, 1);
saveCustom();
renderCustomList();
}
function doSaveCustom() {
const name = prompt('プリセット名を入力:');
if (!name?.trim()) return;
customPresets.push({ name: name.trim(), gains: [...currentGains] });
saveCustom();
renderCustomList();
}
// ================================================================
// 同期ヘルパー
// ================================================================
function syncStatus(ok) {
eqConnected = ok;
const s = document.getElementById('yteq-status');
if (s) s.textContent = ok ? '🟢 Connected' : '⚪ Disconnected';
// nav ボタンに接続状態を dot で反映
const b = document.getElementById(NAV_BTN_ID);
if (b) b.dataset.connected = ok ? '1' : '0';
}
function syncToggle() {
const btn = document.getElementById('yteq-toggle-btn');
if (!btn) return;
btn.textContent = eqEnabled ? 'ON' : 'OFF';
btn.classList.toggle('off', !eqEnabled);
// nav ボタンの色も連動
const nb = document.getElementById(NAV_BTN_ID);
if (nb) nb.classList.toggle('eq-on', eqEnabled);
}
function syncPresets() {
document.querySelectorAll('[data-yteq-preset]').forEach(btn => {
btn.classList.toggle('yteq-active', btn.dataset.yteqPreset === lastSelectedPreset);
});
}
function syncBandVal(i, g) {
const sign = g > 0 ? '+' : '';
const cls = g > 0 ? 'pos' : g < 0 ? 'neg' : '';
const v = document.getElementById(`yteq-val-${i}`);
if (v) { v.textContent = `${sign}${g}`; v.className = `yteq-bval ${cls}`; }
const s = document.getElementById(`yteq-slider-${i}`);
if (s) s.value = g;
}
function syncSliders() {
BAND_LABELS.forEach((_, i) => syncBandVal(i, currentGains[i]));
}
function syncReset() {
let base = null;
if (lastSelectedPreset.startsWith('custom:')) {
base = customPresets[parseInt(lastSelectedPreset.split(':')[1])]?.gains ?? null;
} else if (PRESETS[lastSelectedPreset]) {
base = PRESETS[lastSelectedPreset];
}
const modified = base && !base.every((v, i) => v === currentGains[i]);
const btn = document.getElementById('yteq-eq-reset');
if (btn) btn.classList.toggle('hidden', !modified);
}
function syncVolLabel() {
const v = document.getElementById('yteq-volume-val');
if (v) v.textContent = `${currentVolume}%`;
const s = document.getElementById('yteq-volume-slider');
if (s) s.value = currentVolume;
}
function renderCustomList() {
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-preset-btn" data-yteq-preset="custom:${idx}" data-idx="${idx}">${p.name}</button>
<button class="yteq-custom-del" data-idx="${idx}">✕</button>
</div>
`).join('');
list.querySelectorAll('[data-idx]').forEach(btn => {
const idx = parseInt(btn.dataset.idx);
if (btn.classList.contains('yteq-preset-btn'))
btn.addEventListener('click', () => doLoadCustom(idx));
else
btn.addEventListener('click', () => doDelCustom(idx));
});
syncPresets();
}
// ================================================================
// Popover
// ================================================================
function buildPopover() {
if (document.getElementById(POPOVER_ID)) return;
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-bval ${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-blabel">${label}</span>
</div>`;
}).join('');
const presetsHTML = PRESET_KEYS.map(key =>
`<button class="yteq-preset-btn" data-yteq-preset="${key}">${PRESET_LABELS[key]}</button>`
).join('');
const pop = document.createElement('div');
pop.id = POPOVER_ID;
pop.innerHTML = `
<div id="yteq-header">
<span id="yteq-title">YTyping EQ</span>
<span id="yteq-status">${eqConnected ? '🟢 Connected' : '⚪ Disconnected'}</span>
<button id="yteq-toggle-btn" class="${eqEnabled ? '' : 'off'}">${eqEnabled ? 'ON' : 'OFF'}</button>
</div>
<div id="yteq-body">
<div class="yteq-group-label">PRESET</div>
<div class="yteq-preset-grid">${presetsHTML}</div>
<div id="yteq-eq-section">
<div id="yteq-eq-header">
<span class="yteq-group-label">BANDS</span>
<button id="yteq-eq-reset" class="hidden">RESET</button>
</div>
<div id="yteq-eq-sliders">${slidersHTML}</div>
<div id="yteq-volume-row">
<span>🔊 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 class="yteq-group-label">CUSTOM</span>
<button id="yteq-custom-save">+ Save current</button>
</div>
<div id="yteq-custom-list"></div>
</div>
</div>
`;
document.body.appendChild(pop);
// イベント
pop.querySelector('#yteq-toggle-btn').addEventListener('click', doToggle);
pop.querySelector('#yteq-eq-reset').addEventListener('click', doReset);
pop.querySelector('#yteq-custom-save').addEventListener('click', doSaveCustom);
pop.querySelector('#yteq-volume-slider').addEventListener('input', function () {
doSetVolume(parseInt(this.value));
});
pop.querySelectorAll('[data-yteq-preset]').forEach(btn => {
btn.addEventListener('click', () => doSelectPreset(btn.dataset.yteqPreset));
});
BAND_LABELS.forEach((_, i) => {
pop.querySelector(`#yteq-slider-${i}`).addEventListener('input', function () {
doBandChange(i, parseFloat(this.value));
});
});
renderCustomList();
syncPresets();
syncReset();
// popover 外クリックで閉じる
document.addEventListener('mousedown', (e) => {
if (!popoverOpen) return;
const pop = document.getElementById(POPOVER_ID);
const btn = document.getElementById(NAV_BTN_ID);
if (pop && !pop.contains(e.target) && btn && !btn.contains(e.target)) {
closePopover();
}
}, true);
}
function positionPopover() {
const pop = document.getElementById(POPOVER_ID);
const btn = document.getElementById(NAV_BTN_ID);
if (!pop || !btn) return;
const navEl = document.querySelector('nav');
const btnRect = btn.getBoundingClientRect();
const navRect = navEl ? navEl.getBoundingClientRect() : null;
// nav が左サイドバーの場合 → nav の右端に表示
// nav が上部の場合 → ボタン直下に表示
const navIsVertical = navEl && navEl.offsetWidth < navEl.offsetHeight;
pop.style.position = 'fixed';
pop.style.zIndex = '999999';
if (navIsVertical && navRect) {
// 左サイドバー → 右隣
pop.style.left = `${navRect.right + 8}px`;
pop.style.top = `${Math.min(btnRect.top, window.innerHeight - pop.offsetHeight - 12)}px`;
pop.style.right = 'auto';
pop.style.bottom = 'auto';
} else {
// 上部ナビ → ボタン下
const left = Math.min(btnRect.left, window.innerWidth - pop.offsetWidth - 12);
pop.style.left = `${Math.max(8, left)}px`;
pop.style.top = `${btnRect.bottom + 8}px`;
pop.style.right = 'auto';
pop.style.bottom = 'auto';
}
}
function openPopover() {
buildPopover();
const pop = document.getElementById(POPOVER_ID);
pop.classList.remove('hidden');
popoverOpen = true;
// 位置は次フレームで確定させる(offsetHeight を取るため)
requestAnimationFrame(positionPopover);
document.getElementById(NAV_BTN_ID)?.classList.add('active');
}
function closePopover() {
const pop = document.getElementById(POPOVER_ID);
if (pop) pop.classList.add('hidden');
popoverOpen = false;
document.getElementById(NAV_BTN_ID)?.classList.remove('active');
}
function togglePopover() {
popoverOpen ? closePopover() : openPopover();
}
// ================================================================
// nav ボタン挿入
// ================================================================
function insertNavBtn() {
if (document.getElementById(NAV_BTN_ID)) return;
const nav = document.querySelector('nav');
if (!nav) return;
const btn = document.createElement('button');
btn.id = NAV_BTN_ID;
btn.innerHTML = `
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" style="display:block;">
<rect x="2" y="14" width="3" height="8" rx="1" fill="currentColor" opacity="0.7"/>
<rect x="7" y="8" width="3" height="14" rx="1" fill="currentColor" opacity="0.85"/>
<rect x="12" y="4" width="3" height="18" rx="1" fill="currentColor"/>
<rect x="17" y="10" width="3" height="12" rx="1" fill="currentColor" opacity="0.85"/>
</svg>
<span>EQ</span>
`;
btn.addEventListener('click', (e) => { e.stopPropagation(); togglePopover(); });
// right-nav-icons があればそこへ、なければ nav 末尾へ
const icons = document.getElementById('right-nav-icons');
if (icons) icons.prepend(btn);
else nav.appendChild(btn);
if (eqEnabled) btn.classList.add('eq-on');
}
// ================================================================
// スタイル
// ================================================================
function injectStyles() {
if (document.getElementById('yteq-styles')) return;
const s = document.createElement('style');
s.id = 'yteq-styles';
s.textContent = `
/* ── nav ボタン ────────────────────────────────── */
#yteq-nav-btn {
display: inline-flex; align-items: center; gap: 5px;
padding: 4px 10px; border-radius: 4px; cursor: pointer;
font-size: 12px; font-weight: 700; letter-spacing: 0.05em;
color: #aaaacc; border: 1px solid transparent;
background: transparent; transition: all 0.15s;
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: #3a6a3a; background: #1a2a1a; }
/* ── Popover 本体 ───────────────────────────────── */
#yteq-popover {
position: fixed; 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.7);
user-select: none; width: 340px;
transition: opacity 0.15s, transform 0.15s;
transform-origin: top left;
}
#yteq-popover.hidden {
opacity: 0; pointer-events: none; transform: scale(0.97) translateY(-4px);
}
/* ── ヘッダー ───────────────────────────────────── */
#yteq-header {
display: flex; align-items: center; gap: 8px; padding: 8px 12px;
border-bottom: 1px solid #2a2a3a;
}
#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-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; }
.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;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.yteq-preset-btn:hover { background: #22223a; color: #9a9acc; border-color: #4a4a6a; }
.yteq-preset-btn.yteq-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-reset {
background: #1a1a2a; border: 1px solid #2a2a3a; color: #6a6a9a;
border-radius: 3px; padding: 1px 7px; cursor: pointer; font-size: 9px; font-family: inherit;
transition: all 0.12s;
}
#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-bval { font-size: 7px; color: #6a6a8a; min-width: 20px; text-align: center; }
.yteq-bval.pos { color: #7878cc; }
.yteq-bval.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;
}
.yteq-band-slider::-webkit-slider-thumb:hover { background: #7777ff; }
.yteq-blabel { 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-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-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-item .yteq-preset-btn { flex: 1; text-align: left; padding: 4px 6px; }
.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(s);
}
// ================================================================
// DOM 監視・初期化
// ================================================================
function init() {
injectStyles();
insertNavBtn();
}
function watchDOM() {
const obs = new MutationObserver(() => {
if (!document.getElementById(NAV_BTN_ID)) insertNavBtn();
});
obs.observe(document.documentElement, { childList: true, subtree: false });
if (document.body) obs.observe(document.body, { childList: true, subtree: true });
}
document.readyState === 'loading'
? document.addEventListener('DOMContentLoaded', () => { init(); watchDOM(); })
: (init(), watchDOM());
})();