mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 19:29:33 +00:00
fix(tuner): remove unused settings + fix sidebar panel position (#661)
Removes the Floating Button and Tuning Visibility settings sections and finishes retiring their still-live config: drops the disabledTunings menu filter and showFloatingButton gate from screen.js/ui.js and their persistence in routes.py (retired keys are stripped on write). Repositions the tuner panel opened from the v3 sidebar Plugins popover to anchor beside it via the host's stable plugin-control slot API (falling back to the popover id), clamped to the viewport so it can't open off-screen, and re-anchored on resize. Updates tuner config tests to the retired-key behavior; plugins/tuner 1.3.2 -> 1.3.3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9b4bef3fd1
commit
5e78f2f7f7
@@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Changed
|
### Changed
|
||||||
- **Player frame-time hotspots removed (trace-backed) + weak-hardware hardening.** A Chrome performance trace of a 3D-highway session surfaced two core per-frame layout-thrash sources, now fixed: the highway's visibility check read `canvas.offsetParent` every rAF frame (forces style/layout recalc — now sampled every 10th frame with a cached value, force-refreshed on init/canvas-replace/resize/override-clear), and the v3 player chrome loop called `matches(':hover')` per frame and unconditionally rewrote the Up-Next pill's `textContent`/bar width at 6 Hz (now hover-tracked via mouseenter/mouseleave, DOM writes only on value change, progress bar moved from `width` to compositor-only `scaleX`). The 3D highway pre-warms shader programs (`ren.compile`) and deterministic label textures at init — and chart-dependent chord/section label textures on first draw — so first-appearance shader-compile/texture-upload frame spikes move into the load spinner. For weaker hardware: the per-frame renderer bundle is now a single reused object instead of a fresh ~35-field allocation per frame (object identity is stable and meaningless; array fields still swap reference on chart changes), custom viz get `bundle.lowerBoundT`/`bundle.lowerBoundTime` binary-search helpers for visible-window culling, the default 2D highway's beat lines no longer scan every beat in the song per frame, and the 3D highway stops reading `localStorage` per frame (1 Hz poll) and caches its lyrics text-measurement layout per displayed line instead of re-measuring every syllable every frame. A second, throttled-CPU trace pass additionally removed: shader-program re-resolution churn from label texture swaps (`material.needsUpdate` is now only set on a null↔texture transition — swapping between two cached label textures never changes the compiled program), the 3D highway's per-frame `getBoundingClientRect` layout read in its canvas-size self-check (now every 10th frame, still immediate on backing-store change), and the core 60 Hz HUD clock rewriting `textContent` on every tick (now write-on-change, ~1/s). The dominant residual — steady `getParameters` shader-program re-resolution (~4% of throttled main thread) — turned out to be Three r158+'s transparent-DoubleSide two-pass rendering, which sets `material.needsUpdate` twice per object per frame; all 18 of the 3D highway's transparent DoubleSide materials are flat unlit quads (labels, rails, chord frames, lanes), so they now declare `forceSinglePass: true`, eliminating the recompile churn and halving those objects' draw calls.
|
- **Player frame-time hotspots removed (trace-backed) + weak-hardware hardening.** A Chrome performance trace of a 3D-highway session surfaced two core per-frame layout-thrash sources, now fixed: the highway's visibility check read `canvas.offsetParent` every rAF frame (forces style/layout recalc — now sampled every 10th frame with a cached value, force-refreshed on init/canvas-replace/resize/override-clear), and the v3 player chrome loop called `matches(':hover')` per frame and unconditionally rewrote the Up-Next pill's `textContent`/bar width at 6 Hz (now hover-tracked via mouseenter/mouseleave, DOM writes only on value change, progress bar moved from `width` to compositor-only `scaleX`). The 3D highway pre-warms shader programs (`ren.compile`) and deterministic label textures at init — and chart-dependent chord/section label textures on first draw — so first-appearance shader-compile/texture-upload frame spikes move into the load spinner. For weaker hardware: the per-frame renderer bundle is now a single reused object instead of a fresh ~35-field allocation per frame (object identity is stable and meaningless; array fields still swap reference on chart changes), custom viz get `bundle.lowerBoundT`/`bundle.lowerBoundTime` binary-search helpers for visible-window culling, the default 2D highway's beat lines no longer scan every beat in the song per frame, and the 3D highway stops reading `localStorage` per frame (1 Hz poll) and caches its lyrics text-measurement layout per displayed line instead of re-measuring every syllable every frame. A second, throttled-CPU trace pass additionally removed: shader-program re-resolution churn from label texture swaps (`material.needsUpdate` is now only set on a null↔texture transition — swapping between two cached label textures never changes the compiled program), the 3D highway's per-frame `getBoundingClientRect` layout read in its canvas-size self-check (now every 10th frame, still immediate on backing-store change), and the core 60 Hz HUD clock rewriting `textContent` on every tick (now write-on-change, ~1/s). The dominant residual — steady `getParameters` shader-program re-resolution (~4% of throttled main thread) — turned out to be Three r158+'s transparent-DoubleSide two-pass rendering, which sets `material.needsUpdate` twice per object per frame; all 18 of the 3D highway's transparent DoubleSide materials are flat unlit quads (labels, rails, chord frames, lanes), so they now declare `forceSinglePass: true`, eliminating the recompile churn and halving those objects' draw calls.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Tuner: finished the "remove unused settings" cleanup and fixed the sidebar panel position.** The Floating Button and Tuning Visibility settings sections were removed, but their config was still live: `disabledTunings` still filtered the tuner menu (with no UI left to re-enable a hidden tuning — a one-way trap) and `showFloatingButton` still gated the floating launcher. Both are now fully retired — the enforcement paths in `plugins/tuner/screen.js`/`utils/ui.js` and the persistence in `plugins/tuner/routes.py` are gone (and `routes.py` strips the retired keys on write, so stale values are purged). The tuner panel opened from the v3 sidebar Plugins rail popover now anchors beside it via the host's stable plugin-control slot API (falling back to the popover id), is **clamped to the viewport** so it can't open off the right/bottom edge on narrow/short windows, and re-anchors on window resize. `plugins/tuner` → 1.3.3.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **3D Keys Highway: audio-reactive background ambience + score effects (parity slice 4 — completes the keys side of the visual-parity epic).** The gradient sky behind the highway gains the guitar's **background ambience styles** — drifting **Particles**, pitch-class-colored pulsing **Stage lights**, wireframe **Geometric** shapes, or Off — driven by the shared audio-analyser bridge (stems-first on sloppaks, one-shot `#audio` fallback; bass/mid/treble bands, 5 ms cache) with an **Ambience intensity** slider and an **Audio-reactive** toggle. And the score talks back: a **score-FX overlay** canvas draws rising **“+1” pops** off each scored key, an **expanding ring every 10-combo tier**, **milestone bursts** at 25/50/100 streak, and a brief **red flicker** when a 3+ streak breaks (wrong notes and swept misses both count). All settings-gated and on by default (`keys3d_bg_*`), pooled, cleared when idle, torn down with the scene. Butterchurn/image/video remain out of scope. The `#audio` analyser tap is also **shared across visualizers** now (`window.__feedBackAudioTap` — whoever taps first publishes, everyone else adopts, `highway_3d` included), so switching between or splitting the guitar/drum/keys highways can't strand a permanently non-reactive backdrop, and the tap is never created before the page has user activation (a suspended AudioContext would silence live playback). Tests: bg-style id validation + FX defaults (30 total).
|
- **3D Keys Highway: audio-reactive background ambience + score effects (parity slice 4 — completes the keys side of the visual-parity epic).** The gradient sky behind the highway gains the guitar's **background ambience styles** — drifting **Particles**, pitch-class-colored pulsing **Stage lights**, wireframe **Geometric** shapes, or Off — driven by the shared audio-analyser bridge (stems-first on sloppaks, one-shot `#audio` fallback; bass/mid/treble bands, 5 ms cache) with an **Ambience intensity** slider and an **Audio-reactive** toggle. And the score talks back: a **score-FX overlay** canvas draws rising **“+1” pops** off each scored key, an **expanding ring every 10-combo tier**, **milestone bursts** at 25/50/100 streak, and a brief **red flicker** when a 3+ streak breaks (wrong notes and swept misses both count). All settings-gated and on by default (`keys3d_bg_*`), pooled, cleared when idle, torn down with the scene. Butterchurn/image/video remain out of scope. The `#audio` analyser tap is also **shared across visualizers** now (`window.__feedBackAudioTap` — whoever taps first publishes, everyone else adopts, `highway_3d` included), so switching between or splitting the guitar/drum/keys highways can't strand a permanently non-reactive backdrop, and the tap is never created before the page has user activation (a suspended AudioContext would silence live playback). Tests: bg-style id validation + FX defaults (30 total).
|
||||||
- **3D Keys Highway: the anti-plastic pass — lacquered note gems, glossy piano-black keys, a studio environment, scene themes and a gradient sky (parity slice 3).** The note gems move to `MeshPhysicalMaterial` with a full **clearcoat** (roughness 0.32, clearcoat 1.0/0.18, envMapIntensity 0.9): a sharp lacquer highlight over the colored body instead of the old dead matte surface — glass, not plastic. What sells it is **image-based lighting**: the same procedural PMREM "studio" environment as the drum highway (dark room + cool overhead / warm+cool side light strips, no addon dependency) feeds `scene.environment`, so the **black keys finally read as glossy piano black** (roughness 0.55 → 0.22, envMapIntensity 1.3) with visible strip reflections, whites keep an ivory sheen (0.42/0.55), and the highway floor gets a stage sheen (roughness 0.9 → 0.55, metalness 0.15). The flat background becomes a **vertical gradient** (lighter above the horizon → theme color → darker toward the keyboard), and the guitar highway's **11 scene themes** arrive (same names/values — your look carries across instruments; `default` preserves the original keys palette; pitch-class note/key colors are never themed — themes own the scene, Synthesia colors own the notes). Plus **Cinematic lighting** (ambient 0.55/key 1.3, on by default) and a **Glow strength** slider multiplying the note glow, key approach-glow and the sustain consume-flash (0.5 = stock). All live-applying from the Graphics settings (`keys3d_bg_theme` + `keys3d_bg_*`); the PMREM target and gradient texture are disposed with the scene. Tests: theme-table id parity + default-look preservation + fallbacks (28 total).
|
- **3D Keys Highway: the anti-plastic pass — lacquered note gems, glossy piano-black keys, a studio environment, scene themes and a gradient sky (parity slice 3).** The note gems move to `MeshPhysicalMaterial` with a full **clearcoat** (roughness 0.32, clearcoat 1.0/0.18, envMapIntensity 0.9): a sharp lacquer highlight over the colored body instead of the old dead matte surface — glass, not plastic. What sells it is **image-based lighting**: the same procedural PMREM "studio" environment as the drum highway (dark room + cool overhead / warm+cool side light strips, no addon dependency) feeds `scene.environment`, so the **black keys finally read as glossy piano black** (roughness 0.55 → 0.22, envMapIntensity 1.3) with visible strip reflections, whites keep an ivory sheen (0.42/0.55), and the highway floor gets a stage sheen (roughness 0.9 → 0.55, metalness 0.15). The flat background becomes a **vertical gradient** (lighter above the horizon → theme color → darker toward the keyboard), and the guitar highway's **11 scene themes** arrive (same names/values — your look carries across instruments; `default` preserves the original keys palette; pitch-class note/key colors are never themed — themes own the scene, Synthesia colors own the notes). Plus **Cinematic lighting** (ambient 0.55/key 1.3, on by default) and a **Glow strength** slider multiplying the note glow, key approach-glow and the sustain consume-flash (0.5 = stock). All live-applying from the Graphics settings (`keys3d_bg_theme` + `keys3d_bg_*`); the PMREM target and gradient texture are disposed with the scene. Tests: theme-table id parity + default-look preservation + fallbacks (28 total).
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "tuner",
|
"id": "tuner",
|
||||||
"name": "Guitar/Bass Tuner",
|
"name": "Guitar/Bass Tuner",
|
||||||
"version": "1.3.2",
|
"version": "1.3.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"private": false,
|
"private": false,
|
||||||
"script": "screen.js",
|
"script": "screen.js",
|
||||||
|
|||||||
+4
-14
@@ -33,8 +33,6 @@ def setup(app: FastAPI, context: dict):
|
|||||||
"lastInstrument": DEFAULT_INSTRUMENT,
|
"lastInstrument": DEFAULT_INSTRUMENT,
|
||||||
"freeTune": False,
|
"freeTune": False,
|
||||||
"customTunings": {},
|
"customTunings": {},
|
||||||
"disabledTunings": [],
|
|
||||||
"showFloatingButton": True,
|
|
||||||
"visualizationMode": "default",
|
"visualizationMode": "default",
|
||||||
"audioInputMode": "auto",
|
"audioInputMode": "auto",
|
||||||
"autoOpenOnTuningChange": False,
|
"autoOpenOnTuningChange": False,
|
||||||
@@ -51,8 +49,6 @@ def setup(app: FastAPI, context: dict):
|
|||||||
res["lastInstrument"] = str(data.get("lastInstrument", DEFAULT_INSTRUMENT))
|
res["lastInstrument"] = str(data.get("lastInstrument", DEFAULT_INSTRUMENT))
|
||||||
res["freeTune"] = bool(data.get("freeTune", False))
|
res["freeTune"] = bool(data.get("freeTune", False))
|
||||||
res["customTunings"] = data.get("customTunings", {})
|
res["customTunings"] = data.get("customTunings", {})
|
||||||
res["disabledTunings"] = data.get("disabledTunings", [])
|
|
||||||
res["showFloatingButton"] = bool(data.get("showFloatingButton", True))
|
|
||||||
res["visualizationMode"] = str(data.get("visualizationMode", "default"))
|
res["visualizationMode"] = str(data.get("visualizationMode", "default"))
|
||||||
raw_mode = str(data.get("audioInputMode", "auto"))
|
raw_mode = str(data.get("audioInputMode", "auto"))
|
||||||
res["audioInputMode"] = raw_mode if raw_mode in ("auto", "browser") else "auto"
|
res["audioInputMode"] = raw_mode if raw_mode in ("auto", "browser") else "auto"
|
||||||
@@ -64,8 +60,6 @@ def setup(app: FastAPI, context: dict):
|
|||||||
|
|
||||||
if not isinstance(res["customTunings"], dict):
|
if not isinstance(res["customTunings"], dict):
|
||||||
res["customTunings"] = {}
|
res["customTunings"] = {}
|
||||||
if not isinstance(res["disabledTunings"], list):
|
|
||||||
res["disabledTunings"] = []
|
|
||||||
|
|
||||||
# Migrate custom tunings from old flat-list format
|
# Migrate custom tunings from old flat-list format
|
||||||
res["customTunings"] = {
|
res["customTunings"] = {
|
||||||
@@ -73,12 +67,6 @@ def setup(app: FastAPI, context: dict):
|
|||||||
for name, val in res["customTunings"].items()
|
for name, val in res["customTunings"].items()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Strip legacy disabledTunings entries that lack compound "instrument:name" format
|
|
||||||
res["disabledTunings"] = [
|
|
||||||
e for e in res["disabledTunings"]
|
|
||||||
if isinstance(e, str) and ":" in e
|
|
||||||
]
|
|
||||||
|
|
||||||
return res
|
return res
|
||||||
except Exception:
|
except Exception:
|
||||||
return defaults
|
return defaults
|
||||||
@@ -86,8 +74,10 @@ def setup(app: FastAPI, context: dict):
|
|||||||
def _write(data: dict) -> None:
|
def _write(data: dict) -> None:
|
||||||
config_dir.mkdir(parents=True, exist_ok=True)
|
config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
current = _read()
|
current = _read()
|
||||||
# Strip keys that belong to core, not to this plugin's config.
|
# Strip keys that belong to core, not to this plugin's config, plus
|
||||||
for key in ("defaultTunings", "referencePitch"):
|
# retired keys (disabledTunings/showFloatingButton — their settings UI
|
||||||
|
# was removed) so a stale client can't re-persist them.
|
||||||
|
for key in ("defaultTunings", "referencePitch", "disabledTunings", "showFloatingButton"):
|
||||||
data = {k: v for k, v in data.items() if k != key}
|
data = {k: v for k, v in data.items() if k != key}
|
||||||
current.update(data)
|
current.update(data)
|
||||||
config_file.write_text(json.dumps(current, indent=2), encoding="utf-8")
|
config_file.write_text(json.dumps(current, indent=2), encoding="utf-8")
|
||||||
|
|||||||
+1
-11
@@ -42,7 +42,6 @@
|
|||||||
_allTunings: {},
|
_allTunings: {},
|
||||||
referencePitch: 440,
|
referencePitch: 440,
|
||||||
visualizationMode: 'default',
|
visualizationMode: 'default',
|
||||||
showFloatingButton: true,
|
|
||||||
currentSongOffsets: null,
|
currentSongOffsets: null,
|
||||||
currentSongIsBass: false,
|
currentSongIsBass: false,
|
||||||
currentSongStringCount: 0,
|
currentSongStringCount: 0,
|
||||||
@@ -92,10 +91,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Tuning helpers ────────────────────────────────────────────────
|
// ── Tuning helpers ────────────────────────────────────────────────
|
||||||
function _isTuningEnabled(instrument, name) {
|
|
||||||
return !((_state._serverConfig ? _state._serverConfig.disabledTunings : null) || []).includes(instrument + ':' + name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _instrumentForTuning(name) {
|
function _instrumentForTuning(name) {
|
||||||
for (var key in _state._allTunings) {
|
for (var key in _state._allTunings) {
|
||||||
if (_state._allTunings[key] && _state._allTunings[key][name]) return key;
|
if (_state._allTunings[key] && _state._allTunings[key][name]) return key;
|
||||||
@@ -104,11 +99,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _buildTuningsForInstrument(instrument) {
|
function _buildTuningsForInstrument(instrument) {
|
||||||
const all = _state._allTunings[instrument] || {};
|
return { ...(_state._allTunings[instrument] || {}) };
|
||||||
const disabled = (_state._serverConfig ? _state._serverConfig.disabledTunings : null) || [];
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(all).filter(([name]) => !disabled.includes(instrument + ':' + name))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _tuningIdentityKey(songInfo) {
|
function _tuningIdentityKey(songInfo) {
|
||||||
@@ -554,7 +545,6 @@
|
|||||||
_state._serverConfig = config;
|
_state._serverConfig = config;
|
||||||
_state._allTunings = tuningsData.tunings || {};
|
_state._allTunings = tuningsData.tunings || {};
|
||||||
_state.referencePitch = tuningsData.referencePitch || 440;
|
_state.referencePitch = tuningsData.referencePitch || 440;
|
||||||
_state.showFloatingButton = config.showFloatingButton !== false;
|
|
||||||
_state.visualizationMode = config.visualizationMode || 'default';
|
_state.visualizationMode = config.visualizationMode || 'default';
|
||||||
_state.audioInputMode = config.audioInputMode || 'auto';
|
_state.audioInputMode = config.audioInputMode || 'auto';
|
||||||
|
|
||||||
|
|||||||
+1
-146
@@ -1,15 +1,4 @@
|
|||||||
<div class="space-y-6 py-2">
|
<div class="space-y-6 py-2">
|
||||||
<div class="flex items-center justify-between bg-dark-900/50 p-3 rounded-xl border border-gray-800/50">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-sm font-medium text-gray-200">Floating Button</h3>
|
|
||||||
<p class="text-[11px] text-gray-500">Show the tuner button on the main interface.</p>
|
|
||||||
</div>
|
|
||||||
<label class="relative inline-flex items-center cursor-pointer">
|
|
||||||
<input type="checkbox" id="tuner-show-floating" class="sr-only peer" onchange="window._tunerToggleFloating(this.checked)">
|
|
||||||
<div class="w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-accent"></div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between bg-dark-900/50 p-3 rounded-xl border border-gray-800/50">
|
<div class="flex items-center justify-between bg-dark-900/50 p-3 rounded-xl border border-gray-800/50">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-medium text-gray-200">Auto-open on tuning change</h3>
|
<h3 class="text-sm font-medium text-gray-200">Auto-open on tuning change</h3>
|
||||||
@@ -38,14 +27,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-medium text-gray-400 mb-3">Tuning Visibility</h3>
|
|
||||||
<p class="text-xs text-gray-500 mb-4">Toggle which built-in tunings appear in the tuner menu.</p>
|
|
||||||
<div id="tuner-visibility-list" class="space-y-2 pr-2">
|
|
||||||
<!-- Populated by JS -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-4 border-t border-gray-800">
|
|
||||||
<h3 class="text-sm font-medium text-gray-400 mb-3">Custom Tunings</h3>
|
<h3 class="text-sm font-medium text-gray-400 mb-3">Custom Tunings</h3>
|
||||||
<div id="tuner-custom-list" class="space-y-2 mb-4">
|
<div id="tuner-custom-list" class="space-y-2 mb-4">
|
||||||
<!-- Populated by JS -->
|
<!-- Populated by JS -->
|
||||||
@@ -88,16 +69,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
let config = { customTunings: {}, disabledTunings: [], defaultTunings: {}, showFloatingButton: true, audioInputMode: 'auto' };
|
let config = { customTunings: {}, audioInputMode: 'auto' };
|
||||||
let expandedGroups = [];
|
|
||||||
|
|
||||||
var _INSTRUMENT_CAPTIONS = {
|
|
||||||
"guitar-6": "Guitar",
|
|
||||||
"guitar-7": "Guitar 7-string",
|
|
||||||
"guitar-8": "Guitar 8-string",
|
|
||||||
"bass-4": "Bass 4-string",
|
|
||||||
"bass-5": "Bass 5-string"
|
|
||||||
};
|
|
||||||
|
|
||||||
var _instrumentLabels = {
|
var _instrumentLabels = {
|
||||||
"guitar-6": "Guitar 6-string",
|
"guitar-6": "Guitar 6-string",
|
||||||
@@ -112,9 +84,6 @@
|
|||||||
const resp = await fetch('/api/plugins/tuner/config');
|
const resp = await fetch('/api/plugins/tuner/config');
|
||||||
config = await resp.json();
|
config = await resp.json();
|
||||||
|
|
||||||
const floatingToggle = document.getElementById('tuner-show-floating');
|
|
||||||
if (floatingToggle) floatingToggle.checked = config.showFloatingButton !== false;
|
|
||||||
|
|
||||||
const browserAudioToggle = document.getElementById('tuner-force-browser-audio');
|
const browserAudioToggle = document.getElementById('tuner-force-browser-audio');
|
||||||
if (browserAudioToggle) browserAudioToggle.checked = config.audioInputMode === 'browser';
|
if (browserAudioToggle) browserAudioToggle.checked = config.audioInputMode === 'browser';
|
||||||
|
|
||||||
@@ -125,11 +94,6 @@
|
|||||||
} catch (e) { console.error('Tuner settings: load failed', e); }
|
} catch (e) { console.error('Tuner settings: load failed', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
window._tunerToggleFloating = (enabled) => {
|
|
||||||
config.showFloatingButton = enabled;
|
|
||||||
save();
|
|
||||||
};
|
|
||||||
|
|
||||||
window._tunerToggleBrowserAudio = (forceBrowser) => {
|
window._tunerToggleBrowserAudio = (forceBrowser) => {
|
||||||
config.audioInputMode = forceBrowser ? 'browser' : 'auto';
|
config.audioInputMode = forceBrowser ? 'browser' : 'auto';
|
||||||
save();
|
save();
|
||||||
@@ -153,115 +117,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
const visList = document.getElementById('tuner-visibility-list');
|
|
||||||
visList.innerHTML = '';
|
|
||||||
|
|
||||||
const defaultTunings = config.defaultTunings || {};
|
|
||||||
|
|
||||||
Object.keys(defaultTunings).forEach(groupName => {
|
|
||||||
const group = defaultTunings[groupName];
|
|
||||||
const groupTunings = Object.keys(group);
|
|
||||||
const instrument = groupName;
|
|
||||||
// Compound keys for all tunings in this group
|
|
||||||
const compoundKeys = groupTunings.map(n => instrument + ':' + n);
|
|
||||||
|
|
||||||
const groupWrapper = document.createElement('div');
|
|
||||||
groupWrapper.className = 'mb-4';
|
|
||||||
|
|
||||||
const header = document.createElement('div');
|
|
||||||
header.className = 'flex items-center justify-between p-2 mt-2 bg-dark-900/80 rounded-t-lg border-x border-t border-gray-800/50 cursor-pointer hover:bg-dark-900 transition-colors';
|
|
||||||
|
|
||||||
const left = document.createElement('div');
|
|
||||||
left.className = 'flex items-center gap-2';
|
|
||||||
|
|
||||||
const chevron = document.createElement('span');
|
|
||||||
chevron.innerHTML = '<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>';
|
|
||||||
chevron.className = 'text-gray-500 transition-transform duration-200 rotate-0';
|
|
||||||
|
|
||||||
const groupLabel = document.createElement('span');
|
|
||||||
groupLabel.className = 'text-[11px] font-bold text-gray-400 uppercase tracking-wider';
|
|
||||||
groupLabel.textContent = _INSTRUMENT_CAPTIONS[groupName] || groupName;
|
|
||||||
|
|
||||||
left.appendChild(chevron);
|
|
||||||
left.appendChild(groupLabel);
|
|
||||||
|
|
||||||
const groupToggle = document.createElement('input');
|
|
||||||
groupToggle.type = 'checkbox';
|
|
||||||
const allEnabled = compoundKeys.every(k => !config.disabledTunings.includes(k));
|
|
||||||
const someEnabled = compoundKeys.some(k => !config.disabledTunings.includes(k));
|
|
||||||
groupToggle.checked = allEnabled;
|
|
||||||
groupToggle.indeterminate = someEnabled && !allEnabled;
|
|
||||||
groupToggle.className = 'accent-accent';
|
|
||||||
|
|
||||||
groupToggle.onclick = (e) => e.stopPropagation();
|
|
||||||
groupToggle.onchange = () => {
|
|
||||||
if (groupToggle.checked) {
|
|
||||||
config.disabledTunings = config.disabledTunings.filter(k => !compoundKeys.includes(k));
|
|
||||||
} else {
|
|
||||||
compoundKeys.forEach(k => {
|
|
||||||
if (!config.disabledTunings.includes(k)) config.disabledTunings.push(k);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
save();
|
|
||||||
render();
|
|
||||||
};
|
|
||||||
|
|
||||||
header.appendChild(left);
|
|
||||||
header.appendChild(groupToggle);
|
|
||||||
groupWrapper.appendChild(header);
|
|
||||||
|
|
||||||
const groupContainer = document.createElement('div');
|
|
||||||
groupContainer.className = 'border-x border-b border-gray-800/50 rounded-b-lg overflow-hidden';
|
|
||||||
|
|
||||||
const isExpanded = expandedGroups.includes(groupName);
|
|
||||||
if (!isExpanded) {
|
|
||||||
groupContainer.classList.add('hidden');
|
|
||||||
chevron.classList.remove('rotate-90');
|
|
||||||
} else {
|
|
||||||
chevron.classList.add('rotate-90');
|
|
||||||
}
|
|
||||||
|
|
||||||
header.onclick = () => {
|
|
||||||
const idx = expandedGroups.indexOf(groupName);
|
|
||||||
if (idx === -1) {
|
|
||||||
expandedGroups.push(groupName);
|
|
||||||
} else {
|
|
||||||
expandedGroups.splice(idx, 1);
|
|
||||||
}
|
|
||||||
render();
|
|
||||||
};
|
|
||||||
|
|
||||||
groupTunings.forEach((name, idx) => {
|
|
||||||
const compoundKey = instrument + ':' + name;
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.className = `flex items-center justify-between p-2 bg-dark-800/30 hover:bg-dark-800/50 transition-colors ${idx === 0 ? 'border-t-0' : 'border-t border-gray-800/20'}`;
|
|
||||||
|
|
||||||
const label = document.createElement('span');
|
|
||||||
label.className = 'text-xs text-gray-300';
|
|
||||||
label.textContent = name;
|
|
||||||
|
|
||||||
const toggle = document.createElement('input');
|
|
||||||
toggle.type = 'checkbox';
|
|
||||||
toggle.checked = !config.disabledTunings.includes(compoundKey);
|
|
||||||
toggle.className = 'accent-accent';
|
|
||||||
toggle.onchange = () => {
|
|
||||||
if (toggle.checked) {
|
|
||||||
config.disabledTunings = config.disabledTunings.filter(k => k !== compoundKey);
|
|
||||||
} else {
|
|
||||||
if (!config.disabledTunings.includes(compoundKey)) config.disabledTunings.push(compoundKey);
|
|
||||||
}
|
|
||||||
save();
|
|
||||||
render();
|
|
||||||
};
|
|
||||||
|
|
||||||
div.appendChild(label);
|
|
||||||
div.appendChild(toggle);
|
|
||||||
groupContainer.appendChild(div);
|
|
||||||
});
|
|
||||||
groupWrapper.appendChild(groupContainer);
|
|
||||||
visList.appendChild(groupWrapper);
|
|
||||||
});
|
|
||||||
|
|
||||||
const customList = document.getElementById('tuner-custom-list');
|
const customList = document.getElementById('tuner-custom-list');
|
||||||
customList.innerHTML = '';
|
customList.innerHTML = '';
|
||||||
const customNames = Object.keys(config.customTunings);
|
const customNames = Object.keys(config.customTunings);
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ window._tunerUI = function(state, actions) {
|
|||||||
const btn = document.getElementById('tuner-toggle-btn');
|
const btn = document.getElementById('tuner-toggle-btn');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
const isPlayer = document.querySelector('.screen.active')?.id === 'player';
|
const isPlayer = document.querySelector('.screen.active')?.id === 'player';
|
||||||
if (!state.showFloatingButton || isPlayer || window.feedBack?.isPlaying) {
|
if (isPlayer || window.feedBack?.isPlaying) {
|
||||||
btn.classList.add('hidden');
|
btn.classList.add('hidden');
|
||||||
} else {
|
} else {
|
||||||
btn.classList.remove('hidden');
|
btn.classList.remove('hidden');
|
||||||
@@ -727,6 +727,33 @@ window._tunerUI = function(state, actions) {
|
|||||||
|
|
||||||
document.body.appendChild(state.uiContainer);
|
document.body.appendChild(state.uiContainer);
|
||||||
state.uiContainer.addEventListener('click', (e) => e.stopPropagation());
|
state.uiContainer.addEventListener('click', (e) => e.stopPropagation());
|
||||||
|
|
||||||
|
// Re-anchor while the panel is open: the popover it hugs is
|
||||||
|
// vertically centered, so its rect shifts with viewport height.
|
||||||
|
// initUI() runs once (guarded above), so this binds a single listener.
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
if (state.uiContainer && !state.uiContainer.classList.contains('hidden')) positionPanel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the sidebar Plugins popover to anchor the panel beside it.
|
||||||
|
// Prefer the host's stable plugin-control slot API and derive its popover
|
||||||
|
// container; fall back to the known popover id only if that's unavailable.
|
||||||
|
// Returns a visible rect, or null (→ caller uses the fixed fallback slot).
|
||||||
|
function _pluginsPopoverRect() {
|
||||||
|
let pop = null;
|
||||||
|
try {
|
||||||
|
if (window.feedBack?.ui && typeof window.feedBack.ui.playerControlSlot === 'function') {
|
||||||
|
const slot = window.feedBack.ui.playerControlSlot();
|
||||||
|
if (slot instanceof Element) pop = slot.closest('.v3-rail-pop') || slot;
|
||||||
|
}
|
||||||
|
} catch (_e) { /* host slot API failure → fall back to id lookup */ }
|
||||||
|
if (!pop) pop = document.getElementById('v3-rail-pop-plugins');
|
||||||
|
// offsetParent === null covers display:none on the element or any
|
||||||
|
// ancestor (more robust than testing a specific `hidden` class).
|
||||||
|
if (!pop || pop.offsetParent === null) return null;
|
||||||
|
const rect = pop.getBoundingClientRect();
|
||||||
|
return (rect.width || rect.height) ? rect : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function positionPanel() {
|
function positionPanel() {
|
||||||
@@ -746,7 +773,20 @@ window._tunerUI = function(state, actions) {
|
|||||||
.replace('right-0', '')
|
.replace('right-0', '')
|
||||||
.replace('top-full', '')
|
.replace('top-full', '')
|
||||||
.trim();
|
.trim();
|
||||||
state.uiContainer.style.cssText = 'top:5rem;right:11rem';
|
const anchor = _pluginsPopoverRect();
|
||||||
|
if (anchor) {
|
||||||
|
// Anchor to the right of the Plugins popover, clamped to the
|
||||||
|
// viewport so the panel never opens off-screen (right/bottom)
|
||||||
|
// on a narrow or short window.
|
||||||
|
const GAP = 8, MARGIN = 8;
|
||||||
|
const pw = state.uiContainer.offsetWidth || 288; // w-72
|
||||||
|
const ph = state.uiContainer.offsetHeight || 0;
|
||||||
|
const left = Math.max(MARGIN, Math.min(anchor.right + GAP, window.innerWidth - pw - MARGIN));
|
||||||
|
const top = Math.max(MARGIN, Math.min(anchor.top, window.innerHeight - ph - MARGIN));
|
||||||
|
state.uiContainer.style.cssText = `top:${top}px;left:${left}px`;
|
||||||
|
} else {
|
||||||
|
state.uiContainer.style.cssText = 'top:5rem;right:11rem';
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,14 +56,12 @@ function createTunerSandbox(opts) {
|
|||||||
if (_u.includes('/config')) {
|
if (_u.includes('/config')) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
json: () => Promise.resolve({
|
json: () => Promise.resolve({
|
||||||
showFloatingButton: true,
|
|
||||||
visualizationMode: 'default',
|
visualizationMode: 'default',
|
||||||
audioInputMode: 'auto',
|
audioInputMode: 'auto',
|
||||||
autoOpenOnTuningChange: autoOpen,
|
autoOpenOnTuningChange: autoOpen,
|
||||||
lastInstrument: 'guitar-6',
|
lastInstrument: 'guitar-6',
|
||||||
lastTuning: 'Standard',
|
lastTuning: 'Standard',
|
||||||
freeTune: false,
|
freeTune: false,
|
||||||
disabledTunings: [],
|
|
||||||
customTunings: {},
|
customTunings: {},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,16 +54,21 @@ class TestConfigDefaults:
|
|||||||
assert body["lastTuning"] == "Standard"
|
assert body["lastTuning"] == "Standard"
|
||||||
assert body["lastInstrument"] == "guitar-6"
|
assert body["lastInstrument"] == "guitar-6"
|
||||||
assert body["audioInputMode"] == "auto"
|
assert body["audioInputMode"] == "auto"
|
||||||
assert body["showFloatingButton"] is True
|
|
||||||
assert body["visualizationMode"] == "default"
|
assert body["visualizationMode"] == "default"
|
||||||
assert body["customTunings"] == {}
|
assert body["customTunings"] == {}
|
||||||
assert body["disabledTunings"] == []
|
|
||||||
|
|
||||||
def test_get_does_not_include_default_tunings(self, client):
|
def test_get_does_not_include_default_tunings(self, client):
|
||||||
# defaultTunings moved to GET /api/tunings (core tuning.read capability).
|
# defaultTunings moved to GET /api/tunings (core tuning.read capability).
|
||||||
body = client.get("/api/plugins/tuner/config").json()
|
body = client.get("/api/plugins/tuner/config").json()
|
||||||
assert "defaultTunings" not in body
|
assert "defaultTunings" not in body
|
||||||
|
|
||||||
|
def test_get_does_not_include_retired_keys(self, client):
|
||||||
|
# disabledTunings + showFloatingButton were retired along with their
|
||||||
|
# settings UI; the config must no longer expose them.
|
||||||
|
body = client.get("/api/plugins/tuner/config").json()
|
||||||
|
assert "disabledTunings" not in body
|
||||||
|
assert "showFloatingButton" not in body
|
||||||
|
|
||||||
|
|
||||||
class TestConfigPersistence:
|
class TestConfigPersistence:
|
||||||
def test_partial_update_persisted(self, client):
|
def test_partial_update_persisted(self, client):
|
||||||
@@ -109,14 +114,22 @@ class TestConfigPersistence:
|
|||||||
client.post("/api/plugins/tuner/config", json={"autoOpenOnTuningChange": bad})
|
client.post("/api/plugins/tuner/config", json={"autoOpenOnTuningChange": bad})
|
||||||
assert client.get("/api/plugins/tuner/config").json()["autoOpenOnTuningChange"] is False, bad
|
assert client.get("/api/plugins/tuner/config").json()["autoOpenOnTuningChange"] is False, bad
|
||||||
|
|
||||||
def test_disabled_tunings_strips_entries_without_colon(self, client):
|
def test_retired_keys_ignored_and_not_persisted(self, client, config_dir):
|
||||||
client.post("/api/plugins/tuner/config", json={
|
# disabledTunings + showFloatingButton were retired: POSTing them must
|
||||||
"disabledTunings": ["guitar-6:Drop D", "legacy-entry", "bass-4:Standard"]
|
# not break the request, leak back in the response, or hit the file.
|
||||||
|
r = client.post("/api/plugins/tuner/config", json={
|
||||||
|
"disabledTunings": ["guitar-6:Drop D"],
|
||||||
|
"showFloatingButton": False,
|
||||||
|
"lastTuning": "Drop D",
|
||||||
})
|
})
|
||||||
|
assert r.status_code == 200
|
||||||
body = client.get("/api/plugins/tuner/config").json()
|
body = client.get("/api/plugins/tuner/config").json()
|
||||||
assert "legacy-entry" not in body["disabledTunings"]
|
assert "disabledTunings" not in body
|
||||||
assert "guitar-6:Drop D" in body["disabledTunings"]
|
assert "showFloatingButton" not in body
|
||||||
assert "bass-4:Standard" in body["disabledTunings"]
|
assert body["lastTuning"] == "Drop D"
|
||||||
|
saved = json.loads((config_dir / "tuner.json").read_text())
|
||||||
|
assert "disabledTunings" not in saved
|
||||||
|
assert "showFloatingButton" not in saved
|
||||||
|
|
||||||
def test_custom_tuning_old_format_migrated_on_read(self, client, config_dir):
|
def test_custom_tuning_old_format_migrated_on_read(self, client, config_dir):
|
||||||
(config_dir / "tuner.json").write_text(json.dumps({
|
(config_dir / "tuner.json").write_text(json.dumps({
|
||||||
|
|||||||
Reference in New Issue
Block a user