mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 12:52:29 +00:00
Some checks are pending
ship-ci / ci (push) Waiting to run
The v3 Settings "Save" button posts dlc_dir together with demucs_server_url, default_arrangement and av_offset_ms in one request. POST /api/settings validated dlc_dir first and early-returned "DLC directory not found" before it ever processed demucs_server_url, so on a machine whose DLC path doesn't resolve (fresh install, unplugged/network drive, a path carried over from another machine) setting the Demucs server address silently failed — reported in got-feedBack/feedBack-demucs-server#3 (macOS 07-05 nightly). - server: a non-resolving dlc_dir is now recorded as a warning and skipped rather than aborting the whole POST, so the co-submitted keys still persist. The bad path is surfaced via a new additive `warnings` field and folded into `message` so the settings status line still shows it. - client (v3): the Demucs input now autosaves on blur/enter via a single-key persistSetting POST, like every other v3 setting, so it never depends on the coupled Save button. - tests: cover the decoupling and the unchanged happy path. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1298 lines
100 KiB
HTML
1298 lines
100 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>
|
||
<!-- Interface size (Accessibility): apply the saved UI scale to the root
|
||
font-size BEFORE any stylesheet paints, so there is no flash-of-reflow
|
||
on load. Mirrors capabilities/interface-scale.js, which is the
|
||
authoritative owner and re-applies once it parses. Relative % (never a
|
||
px literal) so a raised browser/OS base font size is respected. -->
|
||
<script>
|
||
(function () {
|
||
try {
|
||
var raw = localStorage.getItem('v3-interface-scale');
|
||
if (raw == null) return;
|
||
var v = Math.min(1.5, Math.max(0.85, parseFloat(raw)));
|
||
if (!isFinite(v) || Math.abs(v - 1) < 0.001) return;
|
||
var el = document.documentElement;
|
||
el.style.setProperty('--fb-scale', String(v));
|
||
el.style.fontSize = (v * 100).toFixed(2) + '%';
|
||
} catch (_) { /* file:// or private mode */ }
|
||
})();
|
||
</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/working-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>
|
||
<script src="/static/capabilities/interface-scale.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">
|
||
<img src="/static/v3/brand/feedback-logo-light.png" alt="fee[dB]ack" style="width:100%;height:auto;display:block">
|
||
</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=".feedpak,.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">
|
||
<!-- fb-selectable: Settings is read-only content (paths, device names,
|
||
version, diagnostics, About) the user copies — opt the whole panel
|
||
back in under the v3 non-select default. See static/v3/v3.css. -->
|
||
<div class="fb-settings fb-selectable">
|
||
<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="accessibility">Accessibility</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>
|
||
<!-- Instrument pathway -->
|
||
<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">Instrument pathway</div>
|
||
<div class="fb-srow-desc">Preferred path for the selected instrument. This is remembered per instrument profile.</div>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<select id="setting-instrument-pathway" onchange="setInstrumentPathway(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="songs">Songs</option>
|
||
<option value="practice">Practice</option>
|
||
<option value="learn">Learn</option>
|
||
<option value="studio">Studio</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>
|
||
|
||
<!-- ══ ACCESSIBILITY ═══════════════════════════════════════════ -->
|
||
<div class="fb-tabpanel" data-tab="accessibility">
|
||
<div class="fb-tabpanel-head"><h3>Accessibility</h3></div>
|
||
<div class="fb-srows">
|
||
<!-- Interface size -->
|
||
<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="M4 7V5a1 1 0 011-1h14a1 1 0 011 1v2M9 20h6M12 4v16"/></svg></span>
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Interface size</div>
|
||
<div class="fb-srow-desc">Make the app’s menus, buttons and text larger or smaller. This changes the <strong>app interface</strong> — not the notes on the note highway (set those under <strong>Graphics</strong>). On desktop you can also press <strong>Ctrl +</strong> / <strong>Ctrl −</strong> to zoom the whole window.</div>
|
||
</div>
|
||
</div>
|
||
<div class="fb-seg" id="setting-interface-size" role="group" aria-label="Interface size">
|
||
<button type="button" class="fb-seg-btn" data-scale="0.90" aria-pressed="false" onclick="window.feedBack.scale.set(0.90)">Small</button>
|
||
<button type="button" class="fb-seg-btn" data-scale="1.00" aria-pressed="false" onclick="window.feedBack.scale.set(1.00)">Medium</button>
|
||
<button type="button" class="fb-seg-btn" data-scale="1.15" aria-pressed="false" onclick="window.feedBack.scale.set(1.15)">Large</button>
|
||
<button type="button" class="fb-seg-btn" data-scale="1.30" aria-pressed="false" onclick="window.feedBack.scale.set(1.30)">Extra Large</button>
|
||
</div>
|
||
<p class="fb-srow-desc" style="margin-top:.55rem;">Larger sizes are recommended for large or high-resolution displays.</p>
|
||
<details class="fb-finetune">
|
||
<summary>Fine-tune size</summary>
|
||
<div class="fb-finetune-body">
|
||
<input type="range" id="setting-interface-size-slider" min="85" max="150" step="5" value="100"
|
||
oninput="window.feedBack.scale.set(this.value / 100, { persist: false })"
|
||
onchange="window.feedBack.scale.set(this.value / 100)" class="fb-srow-wide slider-input"
|
||
aria-label="Interface size percent">
|
||
<span class="fb-seg-val"><span id="setting-interface-size-val">100</span>%</span>
|
||
</div>
|
||
</details>
|
||
</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">
|
||
<!-- Autosave on blur/enter via a single-key POST (like every other v3
|
||
setting), so setting the address never depends on the shared Save
|
||
button — whose bundled dlc_dir could otherwise block it. Save button
|
||
kept for discoverability. -->
|
||
<input type="text" id="demucs-server-url" placeholder="http://192.168.1.100:7865"
|
||
onchange="persistSetting('demucs_server_url', this.value.trim())"
|
||
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>
|
||
<!-- Metadata matching (P8 — wired by static/v3/match-review.js) -->
|
||
<div class="fb-srow fb-srow-stack">
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Metadata matching</div>
|
||
<div class="fb-srow-desc">Matches your charts against MusicBrainz in the background to tidy names, years and genres — display only, your files are never modified. Matches at or above the confidence level apply automatically; the rest wait in the library's review queue. Turning this off never disables manual match fixes.</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="enrich-enabled" checked class="rounded border-gray-600 bg-dark-700 text-accent"> Match songs automatically</label>
|
||
<label class="flex items-center gap-2">Auto-apply confidence
|
||
<select id="enrich-threshold" class="bg-dark-700 border border-gray-800 rounded-xl px-2 py-1.5 text-xs text-gray-300 outline-none">
|
||
<option value="0.85">85% — more auto-matches</option>
|
||
<option value="0.9" selected>90% (recommended)</option>
|
||
<option value="0.95">95% — cautious</option>
|
||
<option value="1.01">Always review</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
<!-- R1 scraper options: sources (who may be contacted) ×
|
||
auto-apply fields (what a confident match fills in). -->
|
||
<div class="fb-srow-wide mb-1">
|
||
<div class="text-[10px] uppercase tracking-wide text-gray-500 mb-1">Sources</div>
|
||
<div class="grid grid-cols-2 gap-2 text-xs text-gray-400">
|
||
<label class="flex items-center gap-2"><input type="checkbox" id="enrich-src-musicbrainz" checked class="rounded border-gray-600 bg-dark-700 text-accent"> MusicBrainz — names, years, genres</label>
|
||
<label class="flex items-center gap-2"><input type="checkbox" id="enrich-src-caa" checked class="rounded border-gray-600 bg-dark-700 text-accent"> Cover Art Archive — album covers</label>
|
||
</div>
|
||
</div>
|
||
<div class="fb-srow-wide mb-1">
|
||
<div class="text-[10px] uppercase tracking-wide text-gray-500 mb-1">Auto-apply</div>
|
||
<div class="grid grid-cols-4 gap-2 text-xs text-gray-400">
|
||
<label class="flex items-center gap-2"><input type="checkbox" id="enrich-apply-names" checked class="rounded border-gray-600 bg-dark-700 text-accent"> Names</label>
|
||
<label class="flex items-center gap-2"><input type="checkbox" id="enrich-apply-year" checked class="rounded border-gray-600 bg-dark-700 text-accent"> Year</label>
|
||
<label class="flex items-center gap-2"><input type="checkbox" id="enrich-apply-genres" checked class="rounded border-gray-600 bg-dark-700 text-accent"> Genres</label>
|
||
<label class="flex items-center gap-2"><input type="checkbox" id="enrich-apply-art" checked class="rounded border-gray-600 bg-dark-700 text-accent"> Cover art</label>
|
||
</div>
|
||
<div class="text-[11px] text-gray-600 mt-1">What a confident match may fill in on its own — matches you confirm in the review queue always apply in full.</div>
|
||
</div>
|
||
<!-- Audio fingerprint (AcoustID) — opt-in, default OFF. Wired by match-review.js. -->
|
||
<div class="fb-srow-wide mb-1">
|
||
<div class="text-[10px] uppercase tracking-wide text-gray-500 mb-1">Audio fingerprint (AcoustID)</div>
|
||
<label class="flex items-center gap-2 text-xs text-gray-400 mb-1"><input type="checkbox" id="acoustid-enabled" class="rounded border-gray-600 bg-dark-700 text-accent"> Identify by audio — reads the recording itself for the exact version (studio vs live/extended)</label>
|
||
<input type="text" id="acoustid-api-key" placeholder="AcoustID application key" class="w-full bg-dark-700 border border-gray-800 rounded-xl px-2 py-1.5 text-xs text-gray-300 outline-none">
|
||
<div class="text-[11px] text-gray-600 mt-1">Opt-in. Get a free key at acoustid.org/new-application; the fpcalc (Chromaprint) binary must be on the server's PATH.</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">Review queue order
|
||
<select id="enrich-review-order" class="bg-dark-700 border border-gray-800 rounded-xl px-2 py-1.5 text-xs text-gray-300 outline-none">
|
||
<option value="missing_first" selected>Missing info first</option>
|
||
<option value="artist">Artist A–Z</option>
|
||
<option value="recent">Recently added</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
<div class="fb-srow-control">
|
||
<button id="enrich-match-now" class="bg-dark-600 hover:bg-dark-500 px-5 py-2.5 rounded-xl text-sm text-gray-300 transition">Match Now</button>
|
||
<span id="enrich-status" class="text-xs text-gray-500"></span>
|
||
</div>
|
||
</div>
|
||
<!-- Artist pages (PR-B — wired by static/v3/match-review.js). Sits
|
||
beside the Metadata matching card; the Settings→Library tab
|
||
regroup is a separate PR. -->
|
||
<div class="fb-srow fb-srow-stack">
|
||
<div class="fb-srow-main">
|
||
<div class="fb-srow-title">Artist pages</div>
|
||
<div class="fb-srow-desc">A page for every artist in your library — their songs, albums and your practice progress, built entirely from your local collection. External links (official site, tour dates, videos, social) come from one MusicBrainz lookup per matched artist and always open in your browser — nothing plays in-app, and they stay off until you opt in.</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="artist-pages-enabled" checked class="rounded border-gray-600 bg-dark-700 text-accent"> Artist pages</label>
|
||
<label class="flex items-center gap-2"><input type="checkbox" id="artist-external-links" class="rounded border-gray-600 bg-dark-700 text-accent"> Show external links (opens your browser)</label>
|
||
</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">
|
||
<!-- fb-selectable: now-playing song metadata (title / artist /
|
||
arrangement / tuning) is copy-worthy content, not chrome.
|
||
pointer-events-auto: the #player-hud parent is pointer-events-none
|
||
(so the HUD doesn't eat clicks meant for the highway), which would
|
||
also block the mouse from reaching this text to select it — opt
|
||
just this block back into hit-testing. -->
|
||
<div class="text-sm leading-tight fb-selectable pointer-events-auto">
|
||
<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">
|
||
<div class="v3-upnext-row">
|
||
<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 class="v3-upnext-bar"><div id="v3-upnext-bar-fill" class="v3-upnext-bar-fill"></div></div>
|
||
</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="min-scale-label">Min res</span>
|
||
<select id="min-scale-select" onchange="highway.setMinRenderScale && highway.setMinRenderScale(parseFloat(this.value))" class="v3-pop-select" aria-labelledby="min-scale-label" title="Minimum auto resolution — how far the highway may lower its resolution to hold the frame rate on heavy scenes. 'Full' disables auto-downscaling, but the Quality selector still caps the maximum (so it's only full resolution at Quality = HD).">
|
||
<option value="0.25">25%</option>
|
||
<option value="0.5">50%</option>
|
||
<option value="0.75">75%</option>
|
||
<option value="1">Full</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>
|
||
<!-- Before songs.js: the songs toolbar calls the match-review chip hook
|
||
on build, so the module must already be registered. -->
|
||
<script src="/static/v3/match-review.js"></script>
|
||
<!-- Before songs.js: the drawer art click + card ⋮ "Change cover…" open
|
||
the cover picker (window.__fbOpenImagePicker). -->
|
||
<script src="/static/v3/image-picker.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>
|
||
<script src="/static/v3/interface-size-ui.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/interface-size-nudge.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>
|