mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-24 05:41:53 +00:00
* fix(player): make Escape a reliable Back; resumable + optionally-confirmed song exit Escape didn't always leave a song: clicking a transport control (play/FF/RW/ restart) left that <button> focused, and _shortcutDispatchBlocked() bails the shortcut dispatcher for any focused INPUT/SELECT/TEXTAREA/BUTTON — so the player-scope Escape=Back shortcut never fired until the user clicked empty canvas to blur the control. Space already had a player-screen carve-out (#593); Escape did not. That asymmetry was the bug. Phase 1 — focus fix: generalize the Space carve-out in _shortcutDispatchBlocked to Escape, on the player AND settings screens (both register Escape=Back; settings had the identical latent bug). The earlier guards still win: text inputs are exempted first, the Section Practice popover already claims Escape, and a true modal (role=dialog aria-modal=true / .feedBack-modal) still traps it. Plugins' player-scope Escape shortcuts are fixed identically. Phase 2 — resume: leaving the player snapshots {song, arrangement, position, speed} to localStorage; a non-blocking "Resume practice" pill offers it back on the next non-player screen / next launch. playSong() gains a {resume} option that restores speed + seeks to the saved position on song:ready instead of the normal autostart. Conservative (ignores <3s / near-end), cleared on natural song-end and once consumed, expires after 24h. Phase 3 — opt-in "Ask before leaving a song" (Gameplay tab, default OFF). A true-modal confirm with monotonic Escape (the second Escape leaves) and Space/Enter = Leave. The player Escape shortcut and the v3 close button route through window.requestExitSong(); auto-exit on song-end and a results screen's own Close stay unguarded. Design rationale: a multi-seat design charrette (engagement, learning-design, operability, codebase-reality) — leaving a song should be reliable and recoverable, not gated; the confirm is opt-in only. Tests: tests/browser/{keyboard-shortcuts,resume-session,exit-confirm}.spec.ts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * test(browser): suppress first-run onboarding in keyboard/resume/exit specs The first-run onboarding overlay (#v3-onboarding) is a modal that intercepts pointer/keyboard events; on a fresh profile it covers the player and breaks any test that presses Escape or clicks. Stub GET /api/profile to an onboarded profile in each beforeEach so the app behaves like a returning user (the state these tests assume). Also tighten the Section Practice Escape test to assert the guarantee the fix actually provides — Escape does not exit the song while the popover is open (the line-447 guard wins over the carve-out) — rather than asserting the popover's own close handler fires, which isn't wired for a synthetic bar. Verified locally against a worktree server (Chromium): all 16 new specs pass (5 Escape + 6 resume + 5 exit-confirm) plus the existing #593 Space tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(player): exit-confirm — Escape cancels back to song, pause on open/resume on stay Refinements from tester feedback on the exit-confirm (default stays OFF): - Escape on the open prompt now = Stay (dismiss + return to the song), matching every other modal and the generic _confirmDialog (Esc=cancel). A second Escape therefore returns to the song instead of leaving it. Leaving stays the explicit, default-focused "Leave" button, so Space/Enter/click = "just get me out" (the OP's "Space always hits leave"). - Opening the prompt PAUSES the song (via the canonical togglePlay path, HTML5 + _juceMode) so it isn't running/being scored behind the modal; Stay resumes exactly what we paused. Guards: cancel any count-in on open; resume only if we paused (wasPlaying), only if still the same live song on the player (_audioSeekGen unchanged), and never auto-resume a song the user had paused. - Trap Tab inside the dialog; backdrop click was already Stay. Specs: exit-confirm.spec.ts updated — the monotonic "second Escape leaves" test becomes "second Escape stays", plus a backdrop-click-stays test. The audio pause/resume itself is verified manually on web + desktop (the mock song has no backing track); these specs lock the navigation + keyboard semantics. NOTE: the pause/resume adds a new pause→resume cycle on the desktop JUCE transport (known play/pause-desync path) — smoke-test on the desktop build before merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(player): accurate exit-confirm copy + keep resume snapshot on failed load Two review follow-ups on the Escape/resume/confirm work: - Settings copy said "a second Escape (or Space/Enter) still leaves", but Escape dismisses the confirm (Stay) like every other modal — only Space/Enter/Leave exit. Corrected the Gameplay-tab description so it matches the implementation (and the committed exit-confirm specs). - resumeLastSession() cleared the snapshot BEFORE awaiting playSong(), so a transient load/connect failure permanently lost the Resume pill with no retry. Clear only after the load resolves; on failure keep the snapshot (and drop the pending in-memory resume) so the pill re-offers it on the next non-player screen. All 16 Escape/resume/exit-confirm Playwright specs still pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
1122 lines
84 KiB
HTML
1122 lines
84 KiB
HTML
<!DOCTYPE html>
|
||
<!--
|
||
fee[dB]ack v0.3.0 shell (FEEDBACK_UI=v3 / GET /v3).
|
||
|
||
This is a re-chromed copy of the legacy static/index.html: the v0.3.0
|
||
sidebar + topbar replace the (hidden) legacy navbar, new #v3-* screens are
|
||
added, and all the legacy screens (#home library, #favorites, #settings,
|
||
#player, #audio, plugin nav containers) are kept verbatim so static/app.js
|
||
boots UNMODIFIED and the whole engine — player/highway, plugin loader,
|
||
capabilities, audio, library, settings — is reused as-is. Navigation is the
|
||
shared window.showScreen across both #v3-* and legacy/#plugin-* screens.
|
||
See ~/Repositories/feedBack-feedback-v030/prompts/12-app-shell.md.
|
||
-->
|
||
<html lang="en" class="dark scroll-smooth">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta name="theme-color" content="#0f172a">
|
||
<meta name="description" content="fee[dB]ack — practice loop for your guitar library: play along, track accuracy, level up.">
|
||
<title>fee[dB]ack</title>
|
||
<!-- Splitscreen pop-out windows (`?ssFollower=1`) get driven into follower
|
||
mode by the splitscreen plugin AFTER it loads, which is well after
|
||
the parser has rendered v3-sidebar / v3-topbar / #player-controls /
|
||
the v3 player rail. Without an early flag the popup flashes the
|
||
normal v3 chrome for hundreds of ms before the plugin gets a chance
|
||
to hide it. Tag the document root before ANY external script parses
|
||
and pair it with a style block that suppresses every chrome element
|
||
immediately. The splitscreen plugin's own ss-follower body class
|
||
takes over once it boots, mirroring this rule set so the two stay
|
||
in sync (see the plugin's screen.js chrome-hide CSS).
|
||
|
||
IMPORTANT: do NOT hide #v3-main here. #player lives *inside* #v3-main
|
||
(it's the scroll container that holds every .screen), so a
|
||
`display:none` on #v3-main also blanks #player — the one surface we
|
||
need visible. app.js's follower bootstrap already activated #player
|
||
(app.js:9716), and the plugin's ss-follower rules likewise keep
|
||
#player shown and never touch #v3-main; matching that keeps the
|
||
empty player chrome painted through the boot window instead of a
|
||
blank popup. Hide the chrome *inside* #v3-main (topbar, non-player
|
||
screens, rail) individually, leaving #v3-main as a transparent
|
||
flex container. -->
|
||
<style>
|
||
html.ss-follower-pre #v3-sidebar,
|
||
html.ss-follower-pre #v3-topbar,
|
||
html.ss-follower-pre #v3-railzone,
|
||
html.ss-follower-pre [id^="v3-rail-pop-"],
|
||
html.ss-follower-pre #player-controls,
|
||
html.ss-follower-pre #player-hud,
|
||
html.ss-follower-pre #section-map,
|
||
html.ss-follower-pre #navbar,
|
||
html.ss-follower-pre .screen:not(#player) { display: none !important; }
|
||
html.ss-follower-pre body { margin: 0; overflow: hidden; }
|
||
</style>
|
||
<script>
|
||
(function () {
|
||
try {
|
||
if (new URLSearchParams(location.search).get('ssFollower') === '1') {
|
||
document.documentElement.classList.add('ss-follower-pre');
|
||
}
|
||
} catch (_) { /* file:// or sandboxed iframe */ }
|
||
})();
|
||
</script>
|
||
<!-- fee[dB]ack mark (the [dB] motif). SVG primary, PNG fallback. -->
|
||
<link rel="icon" type="image/svg+xml" href="/static/v3/brand/favicon.svg">
|
||
<link rel="icon" type="image/png" sizes="32x32" href="/static/v3/brand/favicon-32.png">
|
||
<link rel="apple-touch-icon" href="/static/v3/brand/icon-192.png">
|
||
<link rel="manifest" href="/static/v3/manifest.json">
|
||
<!-- Tailwind utility classes are served from a prebuilt static
|
||
stylesheet (regenerated by scripts/build-tailwind.sh). The old
|
||
Play CDN (cdn.tailwindcss.com) JIT scanned the DOM ~1.8x/sec
|
||
on the main thread, dropping ~26% of frames with the 3D
|
||
highway running — see feedBack-desktop#110. Theme extensions
|
||
(dark/accent/gold colors, Inter font) live in tailwind.config.js. -->
|
||
<link rel="stylesheet" href="/static/tailwind.min.css">
|
||
<!-- UI font: Rubik (Google Fonts, OFL). Applied via fontFamily.display in tailwind.config.js. -->
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="/static/style.css">
|
||
<link rel="stylesheet" href="/static/vendor/shepherd.css">
|
||
<link rel="stylesheet" href="/static/tour-engine.css">
|
||
<!-- v0.3.0 shell styles (radial-gradient bg, custom scrollbars). -->
|
||
<link rel="stylesheet" href="/static/v3/v3.css">
|
||
<!-- Diagnostics console capture must wrap console.* before any other
|
||
script logs anything; load it as early as possible. See
|
||
docs/diagnostics-bundle-spec.md (feedBack#166). -->
|
||
<script src="/static/diagnostics.js"></script>
|
||
<script src="/static/capabilities.js"></script>
|
||
<script src="/static/capabilities/library.js"></script>
|
||
<script src="/static/capabilities/tuning.js"></script>
|
||
<script src="/static/capabilities/audio-session.js"></script>
|
||
<script src="/static/capabilities/audio-effects.js"></script>
|
||
<script src="/static/capabilities/playback.js"></script>
|
||
<!-- fee[dB]ack v0.3.0: ui.library-card-injection capability (plugin card actions). -->
|
||
<script src="/static/capabilities/library-card-actions.js"></script>
|
||
<script src="/static/capabilities/visualization.js"></script>
|
||
<script src="/static/capabilities/note-detection.js"></script>
|
||
<script src="/static/capabilities/midi-input.js"></script>
|
||
</head>
|
||
<body class="h-screen flex overflow-hidden bg-fb-sidebar text-fb-text font-display">
|
||
|
||
<!-- fee[dB]ack v0.3.0 sidebar. Built by shell.js (HOME / LIBRARY groups);
|
||
the inline brand is the no-JS fallback. -->
|
||
<aside id="v3-sidebar" class="w-64 border-r border-fb-border/50 flex-col shrink-0 hidden md:flex">
|
||
<div id="v3-brand" class="p-6">
|
||
<span class="font-extrabold tracking-tight text-fb-text text-xl">fee<span class="text-fb-primary">[dB]</span>ack</span>
|
||
</div>
|
||
<nav id="v3-nav" class="flex-1 overflow-y-auto px-3 pb-6 space-y-6" aria-label="Primary"></nav>
|
||
</aside>
|
||
|
||
<!-- Main scroll region: topbar + v0.3.0 screens + (reused) legacy screens. -->
|
||
<main id="v3-main" class="flex-1 overflow-y-auto relative">
|
||
<!-- Topbar (built by shell.js): secondary nav, search, Support, badge cluster. -->
|
||
<header id="v3-topbar"></header>
|
||
|
||
<!-- New v0.3.0 screens. Empty here; filled by later prompts (dashboard 13,
|
||
plugins 19, profile/playlists/saved 15/16). #v3-home is the default. -->
|
||
<div id="v3-home" class="screen active"></div>
|
||
<div id="v3-songs" class="screen"></div>
|
||
<div id="v3-plugins" class="screen"></div>
|
||
<div id="v3-profile" class="screen"></div>
|
||
<div id="v3-progress" class="screen"></div>
|
||
<div id="v3-shop" class="screen"></div>
|
||
<div id="v3-playlists" class="screen"></div>
|
||
<div id="v3-saved" class="screen"></div>
|
||
<div id="v3-feedbarcade" class="screen"></div>
|
||
<!-- Lessons: native v3 tutorials screen lands in a later prompt; until
|
||
the tutorials plugin is installed this shows a placeholder rather
|
||
than dumping to the Plugins page. -->
|
||
<div id="v3-lessons" class="screen">
|
||
<div class="max-w-3xl mx-auto px-6 md:px-8 pb-8">
|
||
<div class="bg-fb-card/80 backdrop-blur rounded-xl p-8 border border-fb-border/50 text-center">
|
||
<div class="text-4xl mb-3">🎓</div>
|
||
<h3 class="text-xl font-bold text-fb-text mb-1">Lessons</h3>
|
||
<p class="text-fb-textDim text-sm">Guided lessons are coming soon. They’ll appear here once the tutorials plugin is installed.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Legacy top navbar — hidden in v3 (the sidebar/topbar replace it) but kept
|
||
in the DOM so app.js's #nav-plugins / #mobile-nav-plugins injection and the
|
||
navbar-scroll handler still resolve their elements. -->
|
||
<nav id="navbar" class="hidden fixed top-0 w-full z-50 transition-all duration-300">
|
||
<div class="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
|
||
<div class="flex items-end gap-1.5">
|
||
<a href="#" onclick="showScreen('home');return false" class="text-xl font-bold bg-gradient-to-r from-accent-light to-purple-400 bg-clip-text text-transparent">
|
||
FeedBack
|
||
</a>
|
||
<span id="app-version" class="text-xs text-gray-600 mb-0.5"></span>
|
||
</div>
|
||
<div class="hidden md:flex items-center gap-8">
|
||
<a href="#" onclick="showScreen('home');return false" class="text-sm text-gray-400 hover:text-white transition">Library</a>
|
||
<a href="#" onclick="showScreen('favorites');return false" class="text-sm text-gray-400 hover:text-white transition">Favorites</a>
|
||
<a href="#" onclick="document.getElementById('upload-songs-file').click();return false" class="text-sm text-gray-400 hover:text-white transition">Upload</a>
|
||
<span id="nav-plugins" class="contents"></span>
|
||
<a href="#" onclick="showScreen('settings');return false" class="text-sm text-gray-400 hover:text-white transition">Settings</a>
|
||
</div>
|
||
<!-- Mobile menu -->
|
||
<button onclick="document.getElementById('mobile-menu').classList.toggle('hidden')" class="md:hidden text-gray-400">
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
|
||
</button>
|
||
</div>
|
||
<div id="mobile-menu" class="hidden md:hidden bg-dark-800/95 backdrop-blur border-t border-gray-800">
|
||
<div class="px-6 py-4 flex flex-col gap-3">
|
||
<a href="#" onclick="showScreen('home');this.parentElement.parentElement.classList.add('hidden');return false" class="text-gray-400 hover:text-white">Library</a>
|
||
<a href="#" onclick="showScreen('favorites');this.parentElement.parentElement.classList.add('hidden');return false" class="text-gray-400 hover:text-white">Favorites</a>
|
||
<a href="#" onclick="document.getElementById('upload-songs-file').click();this.parentElement.parentElement.classList.add('hidden');return false" class="text-gray-400 hover:text-white">Upload</a>
|
||
<span id="mobile-nav-plugins" class="flex flex-col gap-2 border-t border-b border-gray-800 py-2 my-1">
|
||
<span class="text-xs text-gray-600 uppercase tracking-wider">Plugins</span>
|
||
</span>
|
||
<a href="#" onclick="showScreen('settings');this.parentElement.parentElement.classList.add('hidden');return false" class="text-gray-400 hover:text-white">Settings</a>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- Hidden file input shared by the navbar "Upload" link. Kept at body
|
||
level so it stays reachable regardless of which screen is active. -->
|
||
<input type="file" id="upload-songs-file" accept=".sloppak" multiple class="hidden" onchange="uploadSongs(this.files); this.value=''">
|
||
|
||
<!-- ══ HOME (Hero + Library) — reused as the v3 "Songs" screen ════════ -->
|
||
<div id="home" class="screen">
|
||
<!-- Library -->
|
||
<section id="library-section" class="max-w-7xl mx-auto px-6 pt-24 pb-16">
|
||
<div id="alpha-warning-banner" class="hidden mb-6 px-4 py-3 bg-amber-900/30 border border-amber-500/30 rounded-xl flex items-start gap-3" role="status">
|
||
<span class="text-amber-400 text-lg leading-none mt-0.5" aria-hidden="true">⚠</span>
|
||
<div class="text-sm text-amber-100">
|
||
<strong class="text-amber-300">Heads up — this is an alpha build.</strong>
|
||
Some things may be broken or change without warning. If you hit a bug, please file an issue. Thanks for trying it out!
|
||
</div>
|
||
</div>
|
||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-10">
|
||
<div>
|
||
<h2 id="lib-title" class="text-3xl font-bold text-white">Your Library</h2>
|
||
<p class="text-gray-500 mt-1" id="lib-count"></p>
|
||
</div>
|
||
<div class="flex gap-3 w-full md:w-auto flex-wrap">
|
||
<select id="lib-provider" onchange="setLibraryProvider(this.value)"
|
||
aria-label="Library source"
|
||
class="bg-dark-700 border border-gray-800 rounded-xl px-3 py-2.5 text-sm text-gray-300 outline-none" title="Library source">
|
||
<option value="local">My Library</option>
|
||
</select>
|
||
<!-- View toggle -->
|
||
<div class="flex bg-dark-700 border border-gray-800 rounded-xl overflow-hidden">
|
||
<button id="view-grid-btn" onclick="setLibView('grid')" class="px-3 py-2.5 text-sm transition" title="Grid view">
|
||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 16 16"><rect x="1" y="1" width="6" height="6" rx="1"/><rect x="9" y="1" width="6" height="6" rx="1"/><rect x="1" y="9" width="6" height="6" rx="1"/><rect x="9" y="9" width="6" height="6" rx="1"/></svg>
|
||
</button>
|
||
<button id="view-tree-btn" onclick="setLibView('tree')" class="px-3 py-2.5 text-sm transition" title="Artist/Album view">
|
||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 16 16"><rect x="1" y="1" width="14" height="3" rx="1"/><rect x="3" y="6" width="12" height="3" rx="1"/><rect x="3" y="11" width="12" height="3" rx="1"/></svg>
|
||
</button>
|
||
</div>
|
||
<!-- Grid controls -->
|
||
<select id="lib-sort" onchange="sortLibrary()"
|
||
class="lib-grid-ctrl bg-dark-700 border border-gray-800 rounded-xl px-3 py-2.5 text-sm text-gray-300 outline-none">
|
||
<option value="artist">Artist A-Z</option>
|
||
<option value="artist-desc">Artist Z-A</option>
|
||
<option value="title">Title A-Z</option>
|
||
<option value="title-desc">Title Z-A</option>
|
||
<option value="recent">Recently Added</option>
|
||
<option value="year-desc">Year (newest)</option>
|
||
<option value="year">Year (oldest)</option>
|
||
<option value="tuning">Tuning</option>
|
||
</select>
|
||
<!-- Format filter (shared) -->
|
||
<select id="lib-format" onchange="sortLibrary()"
|
||
class="bg-dark-700 border border-gray-800 rounded-xl px-3 py-2.5 text-sm text-gray-300 outline-none" title="Filter by format">
|
||
<option value="">All formats</option>
|
||
<option value="sloppak">Feedpak</option>
|
||
<option value="loose">Folder</option>
|
||
</select>
|
||
<!-- Tree controls -->
|
||
<button onclick="toggleAllArtists(true)" class="lib-tree-ctrl bg-dark-700 border border-gray-800 rounded-xl px-3 py-2.5 text-sm text-gray-400 hover:text-white transition">Expand All</button>
|
||
<button onclick="toggleAllArtists(false)" class="lib-tree-ctrl bg-dark-700 border border-gray-800 rounded-xl px-3 py-2.5 text-sm text-gray-400 hover:text-white transition">Collapse All</button>
|
||
<!-- Filters drawer toggle (feedBack#129) -->
|
||
<button onclick="toggleLibFilters()" id="btn-lib-filters"
|
||
class="bg-dark-700 border border-gray-800 hover:border-accent/40 rounded-xl px-4 py-2.5 text-sm text-gray-300 transition flex items-center gap-2"
|
||
title="Filter by parts, tuning, lyrics">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4h18M6 12h12M10 20h4"/></svg>
|
||
<span>Filters</span>
|
||
<span id="lib-filters-count" class="hidden bg-accent/30 text-accent-light text-xs font-semibold rounded-full px-1.5 py-0.5 min-w-[1.25rem] text-center">0</span>
|
||
</button>
|
||
<!-- Shared -->
|
||
<input type="text" id="lib-filter" placeholder="Search songs..." oninput="filterLibrary()"
|
||
class="bg-dark-700 border border-gray-800 rounded-xl px-4 py-2.5 text-sm text-gray-300 placeholder-gray-600 focus:border-accent/50 focus:ring-1 focus:ring-accent/30 outline-none flex-1 md:w-60 transition">
|
||
</div>
|
||
</div>
|
||
<!-- Active-filter chip row (only visible when filters are set, feedBack#129) -->
|
||
<div id="lib-filter-chips" class="hidden flex flex-wrap gap-2 mb-5"></div>
|
||
<div id="lib-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
||
<!-- Cards populated by JS -->
|
||
</div>
|
||
<div id="lib-tree" class="space-y-2 hidden">
|
||
<!-- Tree populated by JS -->
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ══ Filters drawer (feedBack#129/#69/#22) ═════════════════════ -->
|
||
<div id="lib-filter-overlay" class="fixed inset-0 bg-black/40 z-40 hidden"
|
||
onclick="toggleLibFilters(false)"></div>
|
||
<aside id="lib-filter-drawer"
|
||
class="fixed top-0 right-0 h-full w-full sm:w-96 bg-dark-800 border-l border-gray-800 z-50 transform translate-x-full transition-transform duration-200 overflow-y-auto">
|
||
<div class="p-6 space-y-6">
|
||
<div class="flex items-center justify-between">
|
||
<h3 class="text-lg font-semibold text-white">Filters</h3>
|
||
<button onclick="toggleLibFilters(false)" class="text-gray-500 hover:text-white" title="Close">
|
||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<section>
|
||
<div class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2">Arrangements</div>
|
||
<p class="text-xs text-gray-600 mb-3">Click cycles: any → require → exclude</p>
|
||
<div id="filter-arrangements" class="flex flex-wrap gap-2"></div>
|
||
</section>
|
||
|
||
<section id="filter-stems-section">
|
||
<div class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2">Stems <span class="text-gray-600 normal-case font-normal">(sloppak)</span></div>
|
||
<p class="text-xs text-gray-600 mb-3">Click cycles: any → require → exclude</p>
|
||
<div id="filter-stems" class="flex flex-wrap gap-2"></div>
|
||
</section>
|
||
|
||
<section>
|
||
<div class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2">Lyrics</div>
|
||
<div id="filter-lyrics" class="flex flex-wrap gap-2"></div>
|
||
</section>
|
||
|
||
<section>
|
||
<details>
|
||
<summary class="cursor-pointer flex items-center justify-between text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2">
|
||
<span>Tuning</span>
|
||
<span id="filter-tunings-summary" class="text-gray-600 normal-case font-normal text-xs">All tunings</span>
|
||
</summary>
|
||
<div id="filter-tunings" class="mt-3 space-y-1 max-h-64 overflow-y-auto pr-1"></div>
|
||
</details>
|
||
</section>
|
||
|
||
<div class="flex items-center justify-between pt-4 border-t border-gray-800">
|
||
<button onclick="clearLibFilters()" class="text-sm text-gray-400 hover:text-white transition">Clear all</button>
|
||
<button onclick="toggleLibFilters(false)" class="bg-accent hover:bg-accent-light px-4 py-2 rounded-lg text-sm font-medium text-white transition">Done</button>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
|
||
<!-- ══ FAVORITES ════════════════════════════════════════════════════ -->
|
||
<div id="favorites" class="screen">
|
||
<section class="max-w-7xl mx-auto px-6 pt-24 pb-16">
|
||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-10">
|
||
<div>
|
||
<h2 class="text-3xl font-bold text-white">Favorites</h2>
|
||
<p class="text-gray-500 mt-1" id="fav-count"></p>
|
||
</div>
|
||
<div class="flex gap-3 w-full md:w-auto flex-wrap">
|
||
<div class="flex bg-dark-700 border border-gray-800 rounded-xl overflow-hidden">
|
||
<button id="fav-view-grid-btn" onclick="setFavView('grid')" class="px-3 py-2.5 text-sm transition" title="Grid view">
|
||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 16 16"><rect x="1" y="1" width="6" height="6" rx="1"/><rect x="9" y="1" width="6" height="6" rx="1"/><rect x="1" y="9" width="6" height="6" rx="1"/><rect x="9" y="9" width="6" height="6" rx="1"/></svg>
|
||
</button>
|
||
<button id="fav-view-tree-btn" onclick="setFavView('tree')" class="px-3 py-2.5 text-sm transition" title="Artist/Album view">
|
||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 16 16"><rect x="1" y="1" width="14" height="3" rx="1"/><rect x="3" y="6" width="12" height="3" rx="1"/><rect x="3" y="11" width="12" height="3" rx="1"/></svg>
|
||
</button>
|
||
</div>
|
||
<select id="fav-sort" onchange="sortFavorites()"
|
||
class="fav-grid-ctrl bg-dark-700 border border-gray-800 rounded-xl px-3 py-2.5 text-sm text-gray-300 outline-none">
|
||
<option value="artist">Artist A-Z</option>
|
||
<option value="artist-desc">Artist Z-A</option>
|
||
<option value="title">Title A-Z</option>
|
||
<option value="title-desc">Title Z-A</option>
|
||
<option value="recent">Recently Added</option>
|
||
<option value="tuning">Tuning</option>
|
||
</select>
|
||
<button onclick="toggleAllFavoriteArtists(true)" class="fav-tree-ctrl bg-dark-700 border border-gray-800 rounded-xl px-3 py-2.5 text-sm text-gray-400 hover:text-white transition">Expand All</button>
|
||
<button onclick="toggleAllFavoriteArtists(false)" class="fav-tree-ctrl bg-dark-700 border border-gray-800 rounded-xl px-3 py-2.5 text-sm text-gray-400 hover:text-white transition">Collapse All</button>
|
||
<input type="text" id="fav-filter" placeholder="Search favorites..." oninput="filterFavorites()"
|
||
class="bg-dark-700 border border-gray-800 rounded-xl px-4 py-2.5 text-sm text-gray-300 placeholder-gray-600 focus:border-accent/50 focus:ring-1 focus:ring-accent/30 outline-none flex-1 md:w-60 transition">
|
||
</div>
|
||
</div>
|
||
<div id="fav-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
||
</div>
|
||
<div id="fav-tree" class="space-y-2 hidden">
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<!-- ══ Plugin screens injected dynamically by loadPlugins() ══════════ -->
|
||
|
||
<!-- ══ SETTINGS ═══════════════════════════════════════════════════════ -->
|
||
<div id="settings" class="screen">
|
||
<div class="fb-settings">
|
||
<button onclick="showScreen('home')" class="fb-settings-back">
|
||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg> Home
|
||
</button>
|
||
<h2 class="fb-settings-title">Settings</h2>
|
||
|
||
<!-- Tab bar — click handling + active-tab persistence wired by
|
||
static/v3/settings.js. data-tab keys match the .fb-tabpanel
|
||
below; the fallback "Plugins" tab hosts uncategorized plugin
|
||
panels (manifest settings.category routes the others). -->
|
||
<div class="fb-tabbar" id="settings-tabbar">
|
||
<button type="button" class="fb-tab" data-tab="gameplay">Gameplay</button>
|
||
<button type="button" class="fb-tab" data-tab="audio">Audio</button>
|
||
<button type="button" class="fb-tab" data-tab="graphics">Graphics</button>
|
||
<button type="button" class="fb-tab" data-tab="keybinds">Keybinds</button>
|
||
<button type="button" class="fb-tab" data-tab="progression">Progression</button>
|
||
<button type="button" class="fb-tab" data-tab="mic">Mic</button>
|
||
<button type="button" class="fb-tab" data-tab="plugins">Plugins</button>
|
||
<button type="button" class="fb-tab" data-tab="system">System</button>
|
||
</div>
|
||
|
||
<!-- ══ GAMEPLAY ════════════════════════════════════════════════ -->
|
||
<div class="fb-tabpanel" data-tab="gameplay">
|
||
<div class="fb-tabpanel-head">
|
||
<h3>Gameplay Settings</h3>
|
||
<button type="button" class="fb-reset-btn" data-reset="gameplay">
|
||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||
Reset Gameplay Settings
|
||
</button>
|
||
</div>
|
||
<div class="fb-srows">
|
||
<!-- Left-handed -->
|
||
<div class="fb-srow">
|
||
<span class="fb-srow-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4M16 17H4m0 0l4 4m-4-4l4-4"/></svg></span>
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Left-handed mode</div>
|
||
<div class="fb-srow-desc">Invert frets on the note highway for left-handed players.</div>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<label class="fb-switch">
|
||
<input type="checkbox" id="setting-lefty" onchange="highway.setLefty(this.checked)">
|
||
<span class="fb-switch-track"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<!-- Default arrangement -->
|
||
<div class="fb-srow">
|
||
<span class="fb-srow-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></span>
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Default arrangement</div>
|
||
<div class="fb-srow-desc">Which arrangement loads first when you open a song.</div>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<select id="default-arrangement" onchange="persistSetting('default_arrangement', this.value)"
|
||
class="bg-dark-700 border border-gray-800 rounded-xl px-3 py-2.5 text-sm text-gray-300 outline-none">
|
||
<option value="">Most notes (auto)</option>
|
||
<option value="Lead">Lead</option>
|
||
<option value="Rhythm">Rhythm</option>
|
||
<option value="Bass">Bass</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<!-- Arrangement routes (naming mode) -->
|
||
<div class="fb-srow">
|
||
<span class="fb-srow-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 4m0 13V4m0 0L9 7"/></svg></span>
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Arrangement routes</div>
|
||
<div class="fb-srow-desc">How arrangement variants are labelled across the app.</div>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<select id="arrangement-naming-mode" onchange="_onNamingModeChange(this.value)"
|
||
class="bg-dark-700 border border-gray-800 rounded-xl px-3 py-2.5 text-sm text-gray-300 outline-none">
|
||
<option value="smart">Smart (Lead, Alt. Lead, Rhythm, Bass…)</option>
|
||
<option value="legacy">Legacy (Combo, Bass)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<!-- A/V sync offset -->
|
||
<div class="fb-srow fb-srow-stack">
|
||
<div style="display:flex; align-items:center; gap:1rem;">
|
||
<span class="fb-srow-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></span>
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">A/V sync offset: <span id="setting-av-offset-val" style="margin-left:.35rem;">0</span> ms</div>
|
||
<div class="fb-srow-desc">Positive = audio plays ahead of visual notes; raise to catch the highway up. Adjust live with the [ and ] keys (Shift for ±50 ms). Auto-saves.</div>
|
||
</div>
|
||
</div>
|
||
<input type="range" id="setting-av-offset" min="-1000" max="1000" step="1" value="0"
|
||
oninput="setAvOffsetMs(this.value)" class="fb-srow-wide slider-input">
|
||
</div>
|
||
<!-- Note highway speed (master difficulty) -->
|
||
<div class="fb-srow fb-srow-stack">
|
||
<div style="display:flex; align-items:center; gap:1rem;">
|
||
<span class="fb-srow-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg></span>
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Note highway speed: <span id="setting-highway-speed-val" style="margin-left:.35rem;">100</span>%</div>
|
||
<div class="fb-srow-desc">Master difficulty — lower simplifies the chart (fewer notes); 100% plays the full arrangement.</div>
|
||
</div>
|
||
</div>
|
||
<input type="range" id="setting-highway-speed" min="0" max="100" step="5" value="100"
|
||
oninput="setMastery(this.value)" class="fb-srow-wide slider-input">
|
||
</div>
|
||
<!-- Miss penalty (stub) -->
|
||
<div class="fb-srow">
|
||
<span class="fb-srow-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M5.07 19H19a2 2 0 001.71-3L13.71 4a2 2 0 00-3.42 0L3.34 16a2 2 0 001.73 3z"/></svg></span>
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Miss penalty <span class="fb-stub-note">Not yet active</span></div>
|
||
<div class="fb-srow-desc">How harshly missed notes are scored. Saved now; scoring wiring lands in a later release.</div>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<select id="setting-miss-penalty" onchange="persistSetting('miss_penalty', this.value)"
|
||
class="bg-dark-700 border border-gray-800 rounded-xl px-3 py-2.5 text-sm text-gray-300 outline-none">
|
||
<option value="none">None</option>
|
||
<option value="low">Low</option>
|
||
<option value="medium">Standard</option>
|
||
<option value="high">High</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<!-- Countdown before song -->
|
||
<div class="fb-srow">
|
||
<span class="fb-srow-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m-3 5a9 9 0 100-18 9 9 0 000 18z"/></svg></span>
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Countdown before song</div>
|
||
<div class="fb-srow-desc">Play a four-beat count-in before a song starts so you can get ready.</div>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<label class="fb-switch">
|
||
<input type="checkbox" id="setting-countdown-before-song" onchange="setCountdownBeforeSong(this.checked)">
|
||
<span class="fb-switch-track"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<!-- Fail behavior (stub) -->
|
||
<div class="fb-srow">
|
||
<span class="fb-srow-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg></span>
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Fail behavior <span class="fb-stub-note">Not yet active</span></div>
|
||
<div class="fb-srow-desc">What happens when you fail a section. Saved now; gameplay wiring lands in a later release.</div>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<select id="setting-fail-behavior" onchange="persistSetting('fail_behavior', this.value)"
|
||
class="bg-dark-700 border border-gray-800 rounded-xl px-3 py-2.5 text-sm text-gray-300 outline-none">
|
||
<option value="continue">Continue playing</option>
|
||
<option value="restart">Restart section</option>
|
||
<option value="stop">Stop song</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<!-- Autoplay & auto-exit -->
|
||
<div class="fb-srow">
|
||
<span class="fb-srow-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></span>
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Autoplay & auto-exit</div>
|
||
<div class="fb-srow-desc">Start songs/lessons automatically and return to the menu when the score screen closes.</div>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<label class="fb-switch">
|
||
<input type="checkbox" id="setting-autoplay-exit" checked onchange="setAutoplayExit(this.checked)">
|
||
<span class="fb-switch-track"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<!-- "Up Next" pill -->
|
||
<div class="fb-srow">
|
||
<span class="fb-srow-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"/></svg></span>
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Show “Up Next”</div>
|
||
<div class="fb-srow-desc">Display the upcoming-section pill in the top-right of the player during playback.</div>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<label class="fb-switch">
|
||
<input type="checkbox" id="setting-show-upnext" checked onchange="setShowUpNext(this.checked)">
|
||
<span class="fb-switch-track"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<!-- Ask before leaving a song -->
|
||
<div class="fb-srow">
|
||
<span class="fb-srow-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg></span>
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Ask before leaving a song</div>
|
||
<div class="fb-srow-desc">Confirm before Escape (or the player’s ✕) exits a song. Off by default — Escape leaves instantly. With it on, a confirm appears; Space, Enter, or “Leave” exits, while Escape dismisses it.</div>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<label class="fb-switch">
|
||
<input type="checkbox" id="setting-confirm-exit" onchange="setConfirmExitSong(this.checked)">
|
||
<span class="fb-switch-track"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══ AUDIO ═══════════════════════════════════════════════════ -->
|
||
<div class="fb-tabpanel" data-tab="audio">
|
||
<div class="fb-tabpanel-head"><h3>Audio Settings</h3></div>
|
||
<div class="fb-srows">
|
||
<!-- Live guitar tone source -->
|
||
<div class="fb-srow fb-srow-stack">
|
||
<div style="display:flex; align-items:center; gap:1rem;">
|
||
<span class="fb-srow-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072M19 5a9 9 0 010 14M5 9v6h4l5 5V4L9 9H5z"/></svg></span>
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Live guitar tone source</div>
|
||
<div class="fb-srow-desc">Choose External/Spark if your guitar tone comes from hardware like Spark LIVE. fee[dB]ack still scores your playing but won't warn that no internal amp tone is loaded.</div>
|
||
</div>
|
||
</div>
|
||
<select id="setting-live-guitar-tone-source"
|
||
class="fb-srow-wide bg-dark-700 border border-gray-800 rounded-xl px-3 py-2.5 text-sm text-gray-300 outline-none">
|
||
<option value="internal">fee[dB]ack internal tone</option>
|
||
<option value="external_hardware">External amp / hardware pedalboard</option>
|
||
<option value="spark_control_x">Spark LIVE + Spark Control X</option>
|
||
</select>
|
||
</div>
|
||
<!-- Demucs server -->
|
||
<div class="fb-srow fb-srow-stack">
|
||
<div style="display:flex; align-items:center; gap:1rem;">
|
||
<span class="fb-srow-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"/></svg></span>
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Demucs server (for stem separation)</div>
|
||
<div class="fb-srow-desc">Optional. Run <a href="https://github.com/got-feedback/feedBack-demucs-server" target="_blank" rel="noopener" class="text-accent hover:text-accent-light underline">feedBack-demucs-server</a> on a GPU machine to offload stem splitting and avoid resource exhaustion on the host.</div>
|
||
</div>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<input type="text" id="demucs-server-url" placeholder="http://192.168.1.100:7865"
|
||
class="bg-dark-700 border border-gray-800 rounded-xl px-4 py-2.5 text-sm text-gray-300 placeholder-gray-600 focus:border-accent/50 outline-none">
|
||
<button onclick="saveSettings()" class="bg-accent hover:bg-accent-light px-6 py-2.5 rounded-xl text-sm font-semibold text-white transition">Save</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══ GRAPHICS ════════════════════════════════════════════════ -->
|
||
<div class="fb-tabpanel" data-tab="graphics">
|
||
<div class="fb-tabpanel-head"><h3>Graphics Settings</h3></div>
|
||
<!-- highway_3d (manifest settings.category="graphics") mounts here. -->
|
||
<div id="plugin-settings-graphics" class="space-y-3"></div>
|
||
<p class="fb-tabpanel-empty" data-empty-for="plugin-settings-graphics">No graphics plugins are installed.</p>
|
||
</div>
|
||
|
||
<!-- ══ KEYBINDS ════════════════════════════════════════════════ -->
|
||
<div class="fb-tabpanel" data-tab="keybinds">
|
||
<div class="fb-tabpanel-head"><h3>Keyboard Shortcuts</h3></div>
|
||
<!-- Populated by settings.js from the live shortcut registry. -->
|
||
<div id="settings-keybinds"></div>
|
||
</div>
|
||
|
||
<!-- ══ PROGRESSION ═════════════════════════════════════════════ -->
|
||
<div class="fb-tabpanel" data-tab="progression">
|
||
<div class="fb-tabpanel-head"><h3>Progression Settings</h3></div>
|
||
<div id="plugin-settings-progression" class="space-y-3"></div>
|
||
<p class="fb-tabpanel-empty" data-empty-for="plugin-settings-progression">No progression plugins are installed.</p>
|
||
</div>
|
||
|
||
<!-- ══ MIC ═════════════════════════════════════════════════════ -->
|
||
<div class="fb-tabpanel" data-tab="mic">
|
||
<div class="fb-tabpanel-head"><h3>Microphone & Detection</h3></div>
|
||
<div id="plugin-settings-mic" class="space-y-3"></div>
|
||
<p class="fb-tabpanel-empty" data-empty-for="plugin-settings-mic">No microphone/detection plugins are installed.</p>
|
||
</div>
|
||
|
||
<!-- ══ PLUGINS (fallback for uncategorized panels) ═════════════ -->
|
||
<div class="fb-tabpanel" data-tab="plugins">
|
||
<div class="fb-tabpanel-head"><h3>Plugins</h3></div>
|
||
<div class="fb-srows">
|
||
<div class="fb-srow fb-srow-stack">
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Plugin updates</div>
|
||
<div class="fb-srow-desc">Check installed plugins for newer versions.</div>
|
||
</div>
|
||
<div class="fb-srow-control" style="justify-content:flex-start;">
|
||
<button onclick="checkPluginUpdates()" id="btn-check-updates" class="bg-dark-600 hover:bg-dark-500 px-5 py-2.5 rounded-xl text-sm text-gray-300 transition">Check for Updates</button>
|
||
<span id="updates-status" class="text-xs text-gray-500"></span>
|
||
</div>
|
||
<div id="plugin-updates-list" class="space-y-2 fb-srow-wide"></div>
|
||
</div>
|
||
</div>
|
||
<!-- Per-plugin collapsible sections injected here (uncategorized). -->
|
||
<div id="plugin-settings" class="space-y-3" style="margin-top:.6rem;"></div>
|
||
</div>
|
||
|
||
<!-- ══ SYSTEM ══════════════════════════════════════════════════ -->
|
||
<div class="fb-tabpanel" data-tab="system">
|
||
<div class="fb-tabpanel-head"><h3>System</h3></div>
|
||
<div class="fb-srows">
|
||
<!-- App Updates — desktop-only; setupAppUpdates() unhides. -->
|
||
<div id="app-updates-block" class="hidden fb-srow fb-srow-stack">
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">App updates</div>
|
||
<div class="fb-srow-desc">Velopack auto-update channel for the desktop app.</div>
|
||
</div>
|
||
<div class="fb-srow-control" style="flex-wrap:wrap;">
|
||
<select id="app-update-channel"
|
||
class="bg-dark-700 border border-gray-800 rounded-xl px-3 py-2.5 text-sm text-gray-300 outline-none">
|
||
<option value="stable">Stable</option>
|
||
<option value="rc">Release candidate</option>
|
||
<option value="beta">Beta</option>
|
||
<option value="alpha">Alpha</option>
|
||
</select>
|
||
<button id="app-update-check-now"
|
||
class="bg-accent hover:bg-accent-light px-4 py-2.5 rounded-xl text-sm font-medium text-white transition disabled:opacity-50">
|
||
Check for updates
|
||
</button>
|
||
</div>
|
||
<p id="app-update-status" class="text-xs text-gray-500 fb-srow-wide">Loading updater status…</p>
|
||
<p id="app-update-linux-note" class="hidden text-xs text-yellow-300 fb-srow-wide">
|
||
Auto-update is not available on Linux —
|
||
<a href="https://github.com/got-feedback/feedback-desktop/releases" target="_blank" rel="noopener" class="text-accent hover:text-accent-light underline">download new versions from GitHub Releases</a>.
|
||
</p>
|
||
</div>
|
||
<!-- Library folder path -->
|
||
<div class="fb-srow fb-srow-stack">
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Library folder path</div>
|
||
<div class="fb-srow-desc">The folder fee[dB]ack scans for your song library.</div>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<input type="text" id="dlc-path" placeholder="/path/to/your/library"
|
||
class="bg-dark-700 border border-gray-800 rounded-xl px-4 py-2.5 text-sm text-gray-300 placeholder-gray-600 focus:border-accent/50 outline-none">
|
||
<button onclick="pickDlcFolder()" id="btn-pick-dlc" class="hidden bg-dark-600 hover:bg-dark-500 px-4 py-2.5 rounded-xl text-sm text-gray-300 transition whitespace-nowrap">📂 Browse</button>
|
||
<button onclick="saveSettings()" class="bg-accent hover:bg-accent-light px-6 py-2.5 rounded-xl text-sm font-semibold text-white transition">Save</button>
|
||
</div>
|
||
</div>
|
||
<!-- Library rescan -->
|
||
<div class="fb-srow fb-srow-stack">
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Library</div>
|
||
<div class="fb-srow-desc">Rescan checks for new songs. Full Rescan clears the cache and re-imports everything.</div>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<button onclick="rescanLibrary()" id="btn-rescan" class="bg-dark-600 hover:bg-dark-500 px-5 py-2.5 rounded-xl text-sm text-gray-300 transition">Rescan Library</button>
|
||
<button onclick="fullRescanLibrary()" id="btn-full-rescan" class="bg-dark-600 hover:bg-red-900/30 px-5 py-2.5 rounded-xl text-sm text-gray-400 transition">Full Rescan</button>
|
||
<span id="rescan-status" class="text-xs text-gray-500"></span>
|
||
</div>
|
||
</div>
|
||
<!-- Backup -->
|
||
<div class="fb-srow fb-srow-stack">
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Backup</div>
|
||
<div class="fb-srow-desc">Export bundles server config, browser preferences, and opted-in plugin data into one JSON file. Import overwrites current settings and reloads.</div>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<button onclick="exportSettings()" id="btn-export-settings" class="bg-dark-600 hover:bg-dark-500 px-5 py-2.5 rounded-xl text-sm text-gray-300 transition">Export Settings</button>
|
||
<button onclick="document.getElementById('import-settings-file').click()" id="btn-import-settings" class="bg-dark-600 hover:bg-dark-500 px-5 py-2.5 rounded-xl text-sm text-gray-300 transition">Import Settings</button>
|
||
<input type="file" id="import-settings-file" accept="application/json,.json" class="hidden" onchange="importSettings(this.files[0]); this.value=''">
|
||
<span id="backup-status" class="text-xs text-gray-500"></span>
|
||
</div>
|
||
</div>
|
||
<!-- Diagnostics -->
|
||
<div class="fb-srow fb-srow-stack">
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Diagnostics</div>
|
||
<div class="fb-srow-desc">Bundles server logs, hardware info, plugin inventory, and the browser console transcript into one zip for bug reports. Redaction strips DLC paths, song filenames, and IP addresses by default.</div>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-2 mb-1 text-xs text-gray-400 fb-srow-wide">
|
||
<label class="flex items-center gap-2"><input type="checkbox" id="diag-incl-system" checked class="rounded border-gray-600 bg-dark-700 text-accent"> System info</label>
|
||
<label class="flex items-center gap-2"><input type="checkbox" id="diag-incl-hardware" checked class="rounded border-gray-600 bg-dark-700 text-accent"> Hardware (CPU/GPU/RAM)</label>
|
||
<label class="flex items-center gap-2"><input type="checkbox" id="diag-incl-logs" checked class="rounded border-gray-600 bg-dark-700 text-accent"> Server logs (last 5 MB)</label>
|
||
<label class="flex items-center gap-2"><input type="checkbox" id="diag-incl-console" checked class="rounded border-gray-600 bg-dark-700 text-accent"> Browser console + errors</label>
|
||
<label class="flex items-center gap-2"><input type="checkbox" id="diag-incl-plugins" checked class="rounded border-gray-600 bg-dark-700 text-accent"> Plugin diagnostics</label>
|
||
<label class="flex items-center gap-2"><input type="checkbox" id="diag-redact" checked class="rounded border-gray-600 bg-dark-700 text-accent"> Redact paths & song names</label>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<button onclick="previewDiagnostics()" id="btn-diag-preview" class="bg-dark-600 hover:bg-dark-500 px-5 py-2.5 rounded-xl text-sm text-gray-300 transition">Preview Bundle</button>
|
||
<button onclick="exportDiagnostics()" id="btn-diag-export" class="bg-dark-600 hover:bg-dark-500 px-5 py-2.5 rounded-xl text-sm text-gray-300 transition">Export Diagnostics</button>
|
||
<span id="diag-status" class="text-xs text-gray-500"></span>
|
||
</div>
|
||
<div id="diag-preview" class="hidden mt-1 bg-dark-700 border border-gray-800 rounded-xl p-3 text-xs text-gray-400 max-h-96 overflow-auto fb-srow-wide"></div>
|
||
</div>
|
||
<!-- About -->
|
||
<div class="fb-srow fb-srow-stack">
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">About</div>
|
||
<div class="fb-srow-desc">
|
||
fee[dB]ack <span id="app-version-about" class="text-gray-500"></span> · Licensed under
|
||
<a id="about-license-link" href="https://github.com/got-feedback/feedback/blob/main/LICENSE" target="_blank" rel="noopener" class="text-accent hover:text-accent-light underline">GNU AGPL v3.0</a> ·
|
||
<a id="about-source-link" href="https://github.com/got-feedback/feedback" target="_blank" rel="noopener" class="text-accent hover:text-accent-light underline">Source code repository</a>.
|
||
Free software — if you run a modified version that interacts with users over a network, you must make the modified source available to those users.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="settings-status" class="text-sm text-gray-500"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Global audio element (outside player so it's always accessible) -->
|
||
<audio id="audio" preload="auto"></audio>
|
||
<script>
|
||
// Web Audio API fallback for iOS WKWebView which can't play WAV via <audio>
|
||
(function() {
|
||
var _waCtx = null, _waSource = null, _waStartTime = 0, _waBuffer = null, _waPlaying = false;
|
||
// Monotonic load token + the in-flight request, so a newer load()
|
||
// supersedes (and aborts) an older one rather than being dropped while
|
||
// the previous fetch/decode is still running.
|
||
var _waLoadSeq = 0, _waXhr = null;
|
||
var audioEl = document.getElementById('audio');
|
||
|
||
window._webAudioFallback = {
|
||
load: function(url, cb) {
|
||
if (!url) return;
|
||
if (!_waCtx) _waCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||
var mySeq = ++_waLoadSeq;
|
||
if (_waXhr) { try { _waXhr.abort(); } catch (e) {} }
|
||
console.log('[WebAudio] Loading: ' + url);
|
||
var xhr = new XMLHttpRequest();
|
||
_waXhr = xhr;
|
||
xhr.open('GET', url, true);
|
||
xhr.responseType = 'arraybuffer';
|
||
xhr.onload = function() {
|
||
if (mySeq !== _waLoadSeq) return; // a newer load() superseded us
|
||
_waCtx.decodeAudioData(xhr.response, function(decoded) {
|
||
if (mySeq !== _waLoadSeq) return; // superseded during decode
|
||
_waBuffer = decoded;
|
||
_waXhr = null;
|
||
console.log('[WebAudio] Decoded: ' + decoded.duration.toFixed(1) + 's');
|
||
if (cb) cb();
|
||
}, function(e) {
|
||
if (mySeq === _waLoadSeq) _waXhr = null;
|
||
console.error('[WebAudio] Decode error:', e);
|
||
});
|
||
};
|
||
xhr.onerror = function() { if (mySeq === _waLoadSeq) _waXhr = null; };
|
||
xhr.send();
|
||
},
|
||
play: function() {
|
||
if (!_waBuffer || !_waCtx) return false;
|
||
this.stop();
|
||
if (_waCtx.state === 'suspended') _waCtx.resume();
|
||
var src = _waCtx.createBufferSource();
|
||
_waSource = src;
|
||
src.buffer = _waBuffer;
|
||
// AudioBufferSourceNode has no preservesPitch equivalent so changing playbackRate here also changes pitch
|
||
src.playbackRate.value = audioEl.playbackRate || 1;
|
||
src.connect(_waCtx.destination);
|
||
// Clear playing state when the buffer ends naturally (stop() also
|
||
// clears it). Guard on identity so a stale source ending after a
|
||
// new one started can't flip the flag off.
|
||
src.onended = function() { if (_waSource === src) _waPlaying = false; };
|
||
_waStartTime = _waCtx.currentTime;
|
||
src.start(0);
|
||
_waPlaying = true;
|
||
console.log('[WebAudio] Playing');
|
||
return true;
|
||
},
|
||
stop: function() {
|
||
if (_waSource) { try { _waSource.stop(); } catch(e){} _waSource = null; }
|
||
_waPlaying = false;
|
||
},
|
||
getTime: function() {
|
||
if (!_waPlaying || !_waCtx) return 0;
|
||
return _waCtx.currentTime - _waStartTime;
|
||
},
|
||
isActive: function() { return _waPlaying; },
|
||
isReady: function() { return !!_waBuffer; },
|
||
getDuration: function() { return _waBuffer ? _waBuffer.duration : 0; }
|
||
};
|
||
})();
|
||
</script>
|
||
|
||
<!-- ══ PLAYER ═════════════════════════════════════════════════════════ -->
|
||
<!-- v3 player chrome (P22): persistent top HUD + Up-Next pill, hover-reveal
|
||
left rail with feature popovers, and an auto-hiding bottom transport.
|
||
Every control keeps its legacy element id + global handler — only the
|
||
layout/skin changes, so app.js/highway.js drive it unmodified. Behavior
|
||
lives in static/v3/player-chrome.js. -->
|
||
<div id="player" class="screen">
|
||
<canvas id="highway"></canvas>
|
||
|
||
<div id="v3-venue-mood-fx" class="venue-mood-fx hidden" aria-hidden="true">
|
||
<div class="venue-mood-lights" aria-hidden="true"></div>
|
||
<div class="venue-mood-crowd" aria-hidden="true"></div>
|
||
<div class="venue-mood-haze" aria-hidden="true"></div>
|
||
</div>
|
||
|
||
<div id="v3-venue-scene-wash" class="v3-venue-scene-wash hidden" aria-hidden="true"></div>
|
||
<div id="v3-venue-mode-badge" class="v3-venue-mode-badge hidden" aria-live="polite">
|
||
<span class="v3-venue-mode-badge-label">Venue mode — 3D scene assets coming next</span>
|
||
</div>
|
||
|
||
<!-- Top HUD — persistent (song info, time, Up Next) -->
|
||
<div id="player-hud" class="absolute top-0 left-0 right-0 flex justify-between items-start px-5 py-4 pointer-events-none z-20">
|
||
<div class="text-sm leading-tight">
|
||
<div><span id="hud-artist" class="text-gray-300"></span> — <span id="hud-title" class="text-white font-semibold"></span></div>
|
||
<div id="hud-arrangement" class="text-gray-500 text-xs mt-0.5"></div>
|
||
<div id="hud-tuning" class="text-gray-500 text-xs mt-0.5"></div>
|
||
<div id="hud-tuning-targets" class="text-gray-500 text-xs mt-0.5"></div>
|
||
</div>
|
||
<div class="flex flex-col items-end gap-2">
|
||
<div class="text-right">
|
||
<div id="hud-time" class="text-sm text-gray-400 tabular-nums"></div>
|
||
<div id="hud-avoffset" class="text-xs text-gray-500 tabular-nums hidden" title="A/V offset — [ and ] to adjust, Shift for ±50 ms">A/V 0 ms</div>
|
||
</div>
|
||
<div id="v3-live-performance-hud" class="v3-live-performance-hud hidden is-idle" aria-live="polite" aria-atomic="true" aria-label="Live performance">
|
||
<div class="v3-live-performance-heading">
|
||
<span class="v3-live-performance-tag">LIVE</span>
|
||
<span id="v3-live-performance-percent" class="v3-live-performance-percent">—</span>
|
||
</div>
|
||
<div id="v3-live-performance-hits" class="v3-live-performance-hits">Waiting for notes</div>
|
||
<div id="v3-live-performance-streak" class="v3-live-performance-streak">Streak 0</div>
|
||
<div id="v3-live-performance-state" class="v3-live-performance-state" aria-hidden="true"></div>
|
||
</div>
|
||
<div id="v3-upnext" class="v3-upnext hidden">
|
||
<span class="text-gray-400">Up Next:</span>
|
||
<span id="v3-upnext-name" class="v3-upnext-name"></span>
|
||
<span id="v3-upnext-eta" class="text-gray-400 text-xs"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Left rail (hover-revealed) + feature popovers -->
|
||
<div id="v3-railzone" class="v3-railzone">
|
||
<nav id="v3-player-rail" class="v3-rail" aria-label="Player tools">
|
||
<button class="v3-rail-icon" type="button" data-rail="viz" aria-haspopup="true" aria-expanded="false" aria-controls="v3-rail-pop-viz" title="Visualization & quality" aria-label="Visualization & quality">
|
||
<span class="v3-rail-border"></span>
|
||
<svg class="v3-rail-svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M19,3H5C3.89,3 3,3.89 3,5V19C3,20.11 3.89,21 5,21H19C20.11,21 21,20.11 21,19V5C21,3.89 20.11,3 19,3M12,5A2,2 0 0,1 14,7A2,2 0 0,1 12,9A2,2 0 0,1 10,7A2,2 0 0,1 12,5M7,5A2,2 0 0,1 9,7A2,2 0 0,1 7,9A2,2 0 0,1 5,7A2,2 0 0,1 7,5M17,5A2,2 0 0,1 19,7A2,2 0 0,1 17,9A2,2 0 0,1 15,7A2,2 0 0,1 17,5M12,18A3,3 0 0,1 9,15A3,3 0 0,1 12,12A3,3 0 0,1 15,15A3,3 0 0,1 12,18Z"/></svg>
|
||
</button>
|
||
<button class="v3-rail-icon" type="button" data-rail="audio" aria-haspopup="true" aria-expanded="false" aria-controls="v3-rail-pop-audio" title="Audio routing" aria-label="Audio routing">
|
||
<span class="v3-rail-border"></span>
|
||
<svg class="v3-rail-svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M16,7V3H14V7H10V3H8V7H8C7,7 6,8 6,9V14.5L9.5,18V21H14.5V18L18,14.5V9C18,8 17,7 16,7Z"/></svg>
|
||
</button>
|
||
<button class="v3-rail-icon" type="button" data-rail="mixer" aria-haspopup="true" aria-expanded="false" aria-controls="v3-rail-pop-mixer" title="Mixer" aria-label="Mixer">
|
||
<span class="v3-rail-border"></span>
|
||
<svg class="v3-rail-svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M5,3H19V5H5V3M5,7H19V9H5V7M5,11H19V13H5V11M5,15H19V17H5V15M5,19H19V21H5V19Z"/></svg>
|
||
</button>
|
||
<button class="v3-rail-icon" type="button" data-rail-action="lyrics" aria-pressed="false" title="Toggle lyrics" aria-label="Toggle lyrics">
|
||
<span class="v3-rail-border"></span>
|
||
<svg class="v3-rail-svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M12,2A3,3 0 0,1 15,5V11A3,3 0 0,1 12,14A3,3 0 0,1 9,11V5A3,3 0 0,1 12,2M19,11C19,14.53 16.39,17.44 13,17.93V21H11V17.93C7.61,17.44 5,14.53 5,11H7A5,5 0 0,0 12,16A5,5 0 0,0 17,11H19Z"/></svg>
|
||
</button>
|
||
<button class="v3-rail-icon" type="button" data-rail="plugins" aria-haspopup="true" aria-expanded="false" aria-controls="v3-rail-pop-plugins" title="Plugin controls" aria-label="Plugin controls">
|
||
<span class="v3-rail-border"></span>
|
||
<span class="v3-rail-badge" id="v3-plugin-count" hidden></span>
|
||
<svg class="v3-rail-svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.5,11H19V7C19,5.89 18.1,5 17,5H13V3.5A2.5,2.5 0 0,0 10.5,1A2.5,2.5 0 0,0 8,3.5V5H4A2,2 0 0,0 2,7V10.8H3.5C5,10.8 6.2,12 6.2,13.5C6.2,15 5,16.2 3.5,16.2H2V20A2,2 0 0,0 4,22H7.8V20.5C7.8,19 9,17.8 10.5,17.8C12,17.8 13.2,19 13.2,20.5V22H17A2,2 0 0,0 19,20V16H20.5A2.5,2.5 0 0,0 23,13.5A2.5,2.5 0 0,0 20.5,11Z"/></svg>
|
||
</button>
|
||
<span class="v3-rail-dot" aria-hidden="true"></span>
|
||
<button class="v3-rail-icon" type="button" data-rail="advanced" aria-haspopup="true" aria-expanded="false" aria-controls="v3-rail-pop-advanced" title="Advanced settings" aria-label="Advanced settings">
|
||
<span class="v3-rail-border v3-rail-border-gear"></span>
|
||
<svg class="v3-rail-svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11.03L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.47,5.34 14.87,5.09L14.49,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.51,2.42L9.13,5.09C8.53,5.34 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11.03C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.53,18.66 9.13,18.91L9.51,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.49,21.58L14.87,18.91C15.47,18.66 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"/></svg>
|
||
</button>
|
||
</nav>
|
||
|
||
<!-- Plugin-controls slot: the host re-homes any controls a plugin
|
||
injects into #player-controls (the auto-hiding transport) into
|
||
this stable, always-reachable popover. See player-chrome.js
|
||
(rehoming MutationObserver). -->
|
||
<div id="v3-rail-pop-plugins" class="v3-rail-pop hidden" role="group" aria-label="Plugin controls">
|
||
<div class="v3-pop-label">Plugin controls</div>
|
||
<div id="v3-plugin-controls-slot" class="v3-plugin-slot"></div>
|
||
<div id="v3-plugin-slot-empty" class="v3-pop-empty">No plugin controls for this song.</div>
|
||
</div>
|
||
|
||
<div id="v3-rail-pop-viz" class="v3-rail-pop hidden" role="group" aria-label="Visualization & quality">
|
||
<div class="v3-pop-row">
|
||
<span class="v3-pop-label" id="viz-picker-label">Visualization</span>
|
||
<select id="viz-picker" onchange="setViz(this.value)" class="v3-pop-select" aria-labelledby="viz-picker-label" title="Visualization">
|
||
<option value="auto">Auto (match arrangement)</option>
|
||
<option value="default">Classic 2D Highway</option>
|
||
<!-- Additional entries populated on load from /api/plugins (feedBack#36).
|
||
The bundled 3D Highway plugin (plugins/highway_3d/) registers as
|
||
`highway_3d` and is the default on fresh installs — see
|
||
_populateVizPicker() in app.js. -->
|
||
</select>
|
||
</div>
|
||
<div class="v3-pop-row">
|
||
<span class="v3-pop-label">Quality</span>
|
||
<select id="quality-select" onchange="highway.setRenderScale(parseFloat(this.value))" class="v3-pop-select">
|
||
<option value="1">HD</option>
|
||
<option value="0.75">Medium</option>
|
||
<option value="0.5">Low</option>
|
||
</select>
|
||
</div>
|
||
<div class="v3-pop-row">
|
||
<span class="v3-pop-label" id="scoreboard-label">Scoreboard</span>
|
||
<select id="scoreboard-select" onchange="setScoreboard(this.value)" class="v3-pop-select" aria-labelledby="scoreboard-label" title="Highway scoreboard">
|
||
<option value="core" selected>Streak</option>
|
||
<option value="detailed">Detailed</option>
|
||
<option value="off">Off</option>
|
||
</select>
|
||
</div>
|
||
<div class="v3-pop-row">
|
||
<span class="v3-pop-label" id="venue-motion-label">Venue Motion</span>
|
||
<select id="venue-motion-select" class="v3-pop-select" aria-labelledby="venue-motion-label" title="Venue Motion">
|
||
<option value="off">Off</option>
|
||
<option value="subtle" selected>Subtle</option>
|
||
<option value="full">Full</option>
|
||
</select>
|
||
</div>
|
||
<div class="v3-pop-row">
|
||
<span class="v3-pop-label" id="venue-mood-fx-label">Venue Mood FX</span>
|
||
<select id="venue-mood-fx-select" class="v3-pop-select" aria-labelledby="venue-mood-fx-label" title="Venue Mood FX">
|
||
<option value="off">Off</option>
|
||
<option value="subtle" selected>Subtle</option>
|
||
<option value="full">Full</option>
|
||
</select>
|
||
</div>
|
||
<p id="venue-viz-mode-hint" class="hidden text-xs text-cyan-300/90 px-1 pb-1 leading-snug">Venue uses 3D Highway with a small-club stage scene behind the fretboard.</p>
|
||
<p id="venue-viz-load-failed-hint" class="hidden text-xs text-amber-400/90 px-1 pb-1 leading-snug">Venue scene assets could not load — playing as plain 3D Highway. Check static/assets/venue/themes/small-club/.</p>
|
||
<p id="venue-mood-fx-3d-hint" class="hidden text-xs text-amber-400/90 px-1 pb-1 leading-snug">Use Venue for the reactive crowd/stage version.</p>
|
||
<p class="text-xs text-gray-500 px-1 pb-1 leading-snug">Venue Motion adds subtle background parallax only — the note highway stays fixed. Mood FX controls performance tint overlays.</p>
|
||
</div>
|
||
|
||
<div id="v3-rail-pop-audio" class="v3-rail-pop hidden" role="group" aria-label="Audio routing">
|
||
<div id="v3-rail-audio-routing"></div>
|
||
<div class="v3-pop-row mt-2">
|
||
<span class="v3-pop-label">Guitar tone</span>
|
||
<select id="player-live-guitar-tone-source"
|
||
class="v3-pop-select max-w-[210px]">
|
||
<option value="internal">fee[dB]ack internal tone</option>
|
||
<option value="external_hardware">External amp / hardware pedalboard</option>
|
||
<option value="spark_control_x">Spark LIVE + Spark Control X</option>
|
||
</select>
|
||
</div>
|
||
<p class="text-xs text-gray-500 px-1 pb-1 leading-snug">External/Spark: hardware supplies tone; no internal amp warning.</p>
|
||
</div>
|
||
|
||
<div id="v3-rail-pop-mixer" class="v3-rail-pop hidden" role="group" aria-label="Mixer">
|
||
<div id="mixer-control">
|
||
<div id="mixer-anchor" class="relative">
|
||
<button id="btn-mixer" type="button" onclick="window.feedBack.audio.toggleMixer()" class="v3-pop-btn" aria-haspopup="true" aria-expanded="false" aria-controls="mixer-popover" title="Audio mixer">Mixer ▾</button>
|
||
<div id="mixer-popover" class="hidden absolute left-0 top-full mt-2 z-50 bg-dark-700 border border-gray-800 rounded-xl shadow-xl" role="group" aria-label="Audio mixer"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="v3-rail-pop-advanced" class="v3-rail-pop hidden" role="group" aria-label="Advanced settings">
|
||
<div class="v3-pop-row">
|
||
<span class="v3-pop-label">Arrangement</span>
|
||
<span class="flex items-center gap-1">
|
||
<select id="arr-select" onchange="changeArrangement(this.value)" class="v3-pop-select max-w-[130px]"></select>
|
||
<button id="arr-default-pin" type="button" onclick="pinCurrentArrangementDefault()" aria-pressed="false" aria-label="Select an arrangement to make it the default" class="v3-pop-pin" title="Select an arrangement to make it the default">☆</button>
|
||
</span>
|
||
</div>
|
||
<div class="v3-pop-row">
|
||
<span class="v3-pop-label" id="mastery-slider-label">Difficulty</span>
|
||
<span class="flex items-center gap-2">
|
||
<input type="range" id="mastery-slider" min="0" max="100" value="100" step="5" oninput="setMastery(this.value)" class="accent-accent slider-input" title="Master difficulty — low = simpler chart, high = full" aria-labelledby="mastery-slider-label">
|
||
<span id="mastery-label" class="v3-pop-val">100%</span>
|
||
</span>
|
||
</div>
|
||
<div class="v3-pop-row">
|
||
<span class="v3-pop-label" id="player-av-offset-slider-label">A/V sync (ms)</span>
|
||
<span class="flex items-center gap-2">
|
||
<input type="range" id="player-av-offset-slider" min="-1000" max="1000" value="0" step="1" oninput="setAvOffsetMs(this.value)" class="accent-accent slider-input" title="A/V sync offset (ms) — positive = audio plays ahead of visuals. [ and ] adjust ±10 ms (Shift = ±50). Double-click to reset." ondblclick="setAvOffsetMs(0)" aria-labelledby="player-av-offset-slider-label">
|
||
<span id="player-av-offset-label" class="v3-pop-val tabular-nums">+0ms</span>
|
||
</span>
|
||
</div>
|
||
<div class="v3-pop-row">
|
||
<span class="v3-pop-label">Loop</span>
|
||
<span class="flex items-center gap-1 flex-wrap">
|
||
<button onclick="setLoopStart()" id="btn-loop-a" class="v3-pop-btn" title="Set loop start at current time">A</button>
|
||
<button onclick="setLoopEnd()" id="btn-loop-b" class="v3-pop-btn" title="Set loop end at current time">B</button>
|
||
<button onclick="saveCurrentLoop()" id="btn-loop-save" class="v3-pop-btn hidden" title="Save this loop">Save</button>
|
||
<button onclick="clearLoop()" id="btn-loop-clear" class="v3-pop-btn hidden" title="Clear loop">✕</button>
|
||
<span id="loop-label" class="text-xs text-gray-500"></span>
|
||
</span>
|
||
</div>
|
||
<div class="v3-pop-row">
|
||
<select id="saved-loops" onchange="loadSavedLoop(this.value)" class="v3-pop-select max-w-[160px] hidden">
|
||
<option value="">Saved Loops</option>
|
||
</select>
|
||
<button onclick="deleteSelectedLoop()" id="btn-loop-delete" class="v3-pop-btn hidden" title="Delete selected loop">✕</button>
|
||
</div>
|
||
<!-- Editor ⇄ 3D Highway round-trip (mirrors the v2 loop cluster). -->
|
||
<div class="v3-pop-row">
|
||
<span class="v3-pop-label">Editor</span>
|
||
<span class="flex items-center gap-1 flex-wrap">
|
||
<button onclick="editRegionInEditor()" id="btn-edit-region" class="v3-pop-btn hidden" title="Edit this region in the Song Editor">✎ Edit region</button>
|
||
<button onclick="returnToEditorFromHighway()" id="btn-return-editor" class="v3-pop-btn hidden" title="Return to the editor where you left off">↩ Editor</button>
|
||
</span>
|
||
</div>
|
||
<button onclick="requestExitSong()" class="v3-pop-close" title="Close player">✕ Close player</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bottom transport — auto-hides on idle, reveals on mouse-move.
|
||
Children tagged data-v3-native are the host's own controls; the
|
||
plugin-rehoming shim (player-chrome.js) relocates any OTHER child
|
||
(a plugin's injected control) into the Plugins rail popover. -->
|
||
<div id="player-controls" class="v3-transport">
|
||
<!-- Canonical lyrics toggle: hidden here (not in the rail zone) so a
|
||
legacy plugin that inserts next to #btn-lyrics (e.g. lyrics_karaoke)
|
||
lands in #player-controls, where the shim re-homes it. Hidden via
|
||
inline style because highway.setOnLyricsChange() rewrites its
|
||
className on every toggle (a `hidden` class would be stripped). -->
|
||
<button onclick="highway.toggleLyrics()" id="btn-lyrics" data-v3-native style="display:none" aria-hidden="true" tabindex="-1">Lyrics ✓</button>
|
||
<div class="v3-speed-cluster" data-v3-native>
|
||
<div class="v3-speed">
|
||
<span class="v3-speed-bars" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span>
|
||
<input type="range" id="speed-slider" min="15" max="150" value="100" step="5" oninput="setSpeed(this.value/100)" class="v3-speed-slider accent-accent" aria-label="Playback speed">
|
||
<span id="speed-label" class="v3-speed-label">1.0x</span>
|
||
</div>
|
||
<div id="speed-presets" class="v3-speed-presets" role="group" aria-label="Practice speed presets" title="Speed preset buttons">
|
||
<button type="button" class="v3-speed-preset-btn v3-speed-preset-active" data-speed-preset="100">100</button>
|
||
<button type="button" class="v3-speed-preset-btn" data-speed-preset="90">90</button>
|
||
<button type="button" class="v3-speed-preset-btn" data-speed-preset="80" data-sweet-spot="1">80</button>
|
||
<button type="button" class="v3-speed-preset-btn" data-speed-preset="75" data-sweet-spot="1">75</button>
|
||
<button type="button" class="v3-speed-preset-btn" data-speed-preset="70" data-sweet-spot="1">70</button>
|
||
<button type="button" class="v3-speed-preset-btn" data-speed-preset="60">60</button>
|
||
<button type="button" class="v3-speed-preset-btn" data-speed-preset="50">50</button>
|
||
</div>
|
||
</div>
|
||
<div class="v3-transport-mid" data-v3-native>
|
||
<button onclick="seekBy(-5)" class="v3-seek" title="Seek Back 5s" aria-label="Seek Back 5s"><img src="/static/svg/rw.svg" class="button-icon-svg" alt="" aria-hidden="true" /></button>
|
||
<button type="button" onclick="restartCurrentSong()" class="v3-seek" title="Restart song" aria-label="Restart song">↺</button>
|
||
<button onclick="togglePlay()" id="btn-play" class="v3-play" aria-label="Play" title="Play" aria-pressed="false"><img src="/static/svg/play.svg" class="button-icon-svg" alt="" aria-hidden="true" /></button>
|
||
<button onclick="seekBy(5)" class="v3-seek" title="Seek Forward 5s" aria-label="Seek Forward 5s"><img src="/static/svg/ff.svg" class="button-icon-svg" alt="" aria-hidden="true" /></button>
|
||
</div>
|
||
<span class="v3-chevrons" data-v3-native aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></span>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
<!-- /#v3-main -->
|
||
|
||
<script src="/static/highway.js"></script>
|
||
<script src="/static/vendor/lottie.min.js"></script>
|
||
<script src="/static/lottie-api.js"></script>
|
||
<script src="/static/app.js"></script>
|
||
<script src="/static/audio-mixer.js"></script>
|
||
<script src="/static/vendor/shepherd.min.js"></script>
|
||
<script src="/static/tour-engine.js"></script>
|
||
<!-- fee[dB]ack v0.3.0 shell: brand helper, then the shell (sidebar/topbar/
|
||
routing). Loaded after app.js/audio-mixer so window.showScreen and
|
||
window.feedBack(.audio) exist; dashboard.js is filled in prompt 13. -->
|
||
<script src="/static/v3/brand.js"></script>
|
||
<script src="/static/v3/shell.js"></script>
|
||
<!-- Progression (spec 010): theme-core before profile.js so the equipped
|
||
theme/avatar frame apply with the first badge render; progression-core
|
||
registers the `progression` capability owner + window.v3Progression. -->
|
||
<script src="/static/v3/theme-core.js"></script>
|
||
<script src="/static/v3/progression-core.js"></script>
|
||
<script src="/static/v3/notifications.js"></script>
|
||
<script src="/static/v3/profile.js"></script>
|
||
<script src="/static/v3/progress.js"></script>
|
||
<script src="/static/v3/shop.js"></script>
|
||
<script src="/static/v3/tuner-core.js"></script>
|
||
<script src="/static/v3/badges.js"></script>
|
||
<script src="/static/v3/stats-recorder.js"></script>
|
||
<script src="/static/v3/live-performance-hud.js"></script>
|
||
<script src="/static/v3/scoreboard-pref.js"></script>
|
||
<script src="/static/v3/venue-viz.js"></script>
|
||
<script src="/static/v3/venue-instrument-pov.js"></script>
|
||
<!-- venue-mood-fx must load before venue-scene-3d: the scene bridge reads
|
||
window.v3VenueMoodFx.getMotion() synchronously at boot when the saved
|
||
viz is 'venue'; loading it after falls back to 'subtle' and ignores a
|
||
saved 'off'/'full' motion preference on first paint. -->
|
||
<script src="/static/v3/venue-mood-fx.js"></script>
|
||
<script src="/static/v3/venue-scene-3d.js"></script>
|
||
<script src="/static/v3/playlists.js"></script>
|
||
<script src="/static/v3/audio-routing.js"></script>
|
||
<script src="/static/v3/live-guitar-tone-source.js"></script>
|
||
<script src="/static/v3/pedal-cables.js"></script>
|
||
<script src="/static/v3/plugins-page.js"></script>
|
||
<script src="/static/v3/card-actions-core.js"></script>
|
||
<script src="/static/v3/songs.js"></script>
|
||
<script src="/static/v3/lessons.js"></script>
|
||
<script src="/static/v3/dashboard.js"></script>
|
||
<script src="/static/v3/settings.js"></script>
|
||
<!-- First-run home tour: spotlights the home cards via the shared tour
|
||
engine (tour-engine.js, loaded above). Auto-runs once after onboarding
|
||
(triggered from profile.js finish()); replayable from the "?" menu. -->
|
||
<script src="/static/v3/onboarding-tour.js"></script>
|
||
<script src="/static/v3/feedbarcade.js"></script>
|
||
<script src="/static/v3/player-chrome.js"></script>
|
||
<script>
|
||
// Navbar scroll effect
|
||
window.addEventListener('scroll', () => {
|
||
const nav = document.getElementById('navbar');
|
||
if (window.scrollY > 50) {
|
||
nav.classList.add('bg-dark-900/80', 'backdrop-blur-lg', 'border-b', 'border-gray-800/50');
|
||
} else {
|
||
nav.classList.remove('bg-dark-900/80', 'backdrop-blur-lg', 'border-b', 'border-gray-800/50');
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|