import {
bootstrapPluginsAndUi,
checkPluginUpdates,
loadPlugins,
updatePlugin,
} from './js/plugin-loader.js';
import {
_autoMatchViz,
_maybeShowNotationViewHint,
_populateVizPicker,
setViz,
} from './js/viz.js';
import {
exportDiagnostics,
previewDiagnostics,
} from './js/diagnostics-export.js';
import {
_confirmDialog,
_escAttr,
_isElementVisible,
_trapFocusInModal,
esc,
uiPrompt,
} from './js/dom.js';
import {
hwcInitSettingsUI,
initHighwayColors,
} from './js/highway-colors.js';
import {
displayTuningName,
displayTuningTargetDetails,
displayTuningTargets,
effectiveStringCount,
isBassArrangement,
parseRawTuningOffsets,
songTuningContext,
} from './js/tuning-display.js';
import {
exportSettings,
importSettings,
} from './js/settings-io.js';
import { audio } from './js/audio-el.js';
import { S } from './js/player-state.js';
// Side-effect import: the three JUCE shims are IIFEs that install themselves and
// publish through window.*. _resetJuceAudioShimChain is the one binding app.js needs.
import { _resetJuceAudioShimChain } from './js/juce-audio.js';
import {
_clearResumeSession,
_hideResumePill,
_maybeShowResumePill,
_readResumeSession,
_snapshotResumeSession,
resumeLastSession,
} from './js/resume-session.js';
import {
_applyMastery,
_applyMasteryAvailability,
_autoplayExitEnabled,
_countdownBeforeSongEnabled,
_curPlaybackSpeed,
_exitConfirmEnabled,
_resetPlaybackSpeedForNewSong,
_showUpNextEnabled,
_wireSpeedPresetsOnce,
applySpeedPreset,
setMastery,
setSpeed,
} from './js/player-controls.js';
import {
_cancelCountIn,
armCreditsHideOnPlay,
hideCountOverlay,
hideSongCreditsOverlay,
holdCreditsThen,
isCountingIn,
playClick,
scheduleCreditsHide,
showCountOverlay,
showSongCreditsOverlay,
startCountIn,
startSongCountIn,
} from './js/count-in.js';
import {
_loopMutationGen,
clearLoop,
deleteSelectedLoop,
loadSavedLoop,
loadSavedLoops,
loopA,
loopB,
saveCurrentLoop,
setLoop,
setLoopEnd,
setLoopStart,
updateLoopUI,
} from './js/loops.js';
import {
_buildSectionParents,
_ensureSectionPracticeBar,
_hideSectionPracticeBar,
_installSectionPracticeDrawHook,
_maybeRefreshSectionPracticeDuration,
_placeSectionPracticeControlForChrome,
_resetSectionPracticeLog,
_scheduleSectionPracticeRetries,
_sectionPracticeBarContains,
_sectionPracticeBarIsReady,
_sectionPracticePopoverOpen,
_sectionPracticeSourceSections,
_sectionPracticeStartTime,
_setSectionPracticeMode,
_syncSectionPracticeFromLoop,
_updateSectionPracticeHighlight,
invalidateParentCount,
onPhraseNext,
onPhrasePrev,
onSectionParentClick,
onSectionPracticeModeChange,
onSectionPracticeWholeChange,
practiceSection,
renderSectionPracticeBar,
resetSelection,
toggleSectionPracticePopover,
} from './js/section-practice.js';
import { configureHost } from './js/host.js';
import { formatTime } from './js/format.js';
import { L } from './js/library-state.js';
import {
_LIB_FORMAT_KEY,
_LIB_FORMAT_VALUES,
_LIB_SORT_KEY,
_LIB_SORT_VALUES,
_activeLibraryProviderId,
_applyLibFiltersToParams,
_bumpLibNavGeneration,
_getArrangementNamingMode,
_lastLibSelected,
_libNavItems,
_libScrollOnNextRender,
_libraryLocalFilename,
_libraryProviderApi,
_librarySongArtUrl,
_librarySongId,
_librarySyncState,
_moveSelectionInItems,
_onHeaderClick,
_onNamingModeChange,
_pollScanAndRefresh,
_providerSupports,
_readPersistedChoice,
_removeLibCardsForFilename,
_renderLibFilterChips,
_resetLibraryProviderViewState,
_setLibSelection,
_setLibrarySyncState,
_toggleHeader,
_updateLibFiltersBadge,
checkScanAndLoad,
clearLibFilters,
editBtn,
filterFavTreeLetter,
filterFavorites,
filterLibrary,
filterTreeLetter,
fullRescanLibrary,
goFavPage,
goFavTreePage,
goTreePage,
hideScanBanner,
libView,
loadFavorites,
loadLibrary,
loadLibraryProviders,
loadTreeView,
renderGridCards,
renderTreeInto,
rescanLibrary,
setFavView,
setLibView,
setLibraryProvider,
sortFavorites,
sortLibrary,
stopInfiniteScroll,
toggleAllArtists,
toggleAllFavoriteArtists,
toggleFavorite,
toggleLibFilters,
} from './js/library.js';
// The playback transport. These used to BE app.js — they are imported back now, and the
// four modules that reached for them through the host seam import them directly instead.
import {
setPlayButtonState, jucePlayer, _audioTime, _audioDuration, _songEventPayload,
_markPlaybackPaused, _markPlaybackResumed, _emitPlaybackStopped, _emitSongPositionChanged,
_waitForSongReady, _resetAudioSeekState, _audioSeek, togglePlay, seekBy, audioSeekGen,
} from './js/transport.js';
// Demo analytics — real impl set by demo.js; no-op in normal builds
window.feedBackDemoTrack = window.feedBackDemoTrack ?? null;
// ── Global keyboard shortcuts ─────────────────────────────────────────────
//
// `/` focuses the active screen's search input (Library / Favorites);
// `Esc` while focused blurs and clears it. Mirrors the GitHub / Gmail
// convention. The listener bails when the user is already typing in
// any text-accepting element so it can't intercept normal typing —
// including inputs inside the filters drawer, plugin settings, or
// modal dialogs.
function _isTextInput(el) {
if (!el) return false;
const tag = el.tagName;
if (tag === 'INPUT') {
// Some types (button, checkbox, radio, range, ...) don't
// accept text; only intercept the ones that do.
const t = (el.type || 'text').toLowerCase();
return ['text', 'search', 'email', 'url', 'tel', 'password', 'number'].includes(t);
}
if (tag === 'TEXTAREA') return true;
if (tag === 'SELECT') return true;
if (el.isContentEditable) return true;
return false;
}
function _isShortcutHelpKey(e) {
return e.key === '?' || (e.shiftKey && (e.code === 'Slash' || e.key === '/'));
}
function _isShortcutHelpSuppressedTarget(el) {
if (!el) return false;
const tag = el.tagName;
if (tag === 'INPUT') {
const t = (el.type || 'text').toLowerCase();
return ['text', 'search', 'email', 'url', 'tel', 'password', 'number'].includes(t);
}
if (tag === 'TEXTAREA') return true;
if (el.isContentEditable) return true;
if (el.closest && el.closest('#lib-filter-drawer, [role="dialog"], #edit-modal, .feedBack-modal')) return true;
return false;
}
function _activeSearchInput() {
// Pick the search field for whichever screen is currently active.
// No match (e.g. on the player or settings screen) means `/` does
// nothing — the shortcut only fires where a search box exists.
const active = document.querySelector('.screen.active');
if (!active) return null;
if (active.id === 'home') return document.getElementById('lib-filter');
if (active.id === 'favorites') return document.getElementById('fav-filter');
return null;
}
// ── Library keyboard navigation ──────────────────────────────────────────
//
// Arrow keys move a single "selected" item among the visible cards
// (grid view) or song rows (tree view). Enter plays the selected
// song. The selected element gets:
// - native keyboard focus via .focus() so :focus-visible draws the
// accessible ring (announced by screen readers, follows scroll)
// - a `.selected` class that persists when focus drifts elsewhere
// so the user can glance back and still see their place.
//
// Grid columns are inferred from the live computed grid template at
// the moment of navigation, so up/down works correctly across all
// breakpoints (1 / 2 / 3 / 4 cols depending on viewport).
function _gridColumns(container) {
// Count columns by grouping the first row of children by their
// top coordinate. Robust against any grid-template-columns syntax
// (`repeat(...)`, `auto-fit`, named lines, etc.) where naively
// splitting `getComputedStyle().gridTemplateColumns` on whitespace
// would miscount because of spaces inside `repeat(...)` /
// `minmax(...)`. Falls back to 1 when the container is empty
// so callers' max(1, ...) clamps stay valid.
if (!container) return 1;
const children = Array.from(container.children).filter(
c => c && c.offsetParent !== null
);
if (!children.length) return 1;
const firstTop = children[0].getBoundingClientRect().top;
let cols = 0;
for (const c of children) {
// Allow ~1px slop for sub-pixel rounding so two children that
// would visually align still group together.
if (Math.abs(c.getBoundingClientRect().top - firstTop) < 1.5) cols++;
else break;
}
return Math.max(1, cols);
}
// Tracks which list screen launched the player so Esc-from-player
// returns the user to that screen instead of always defaulting to
// the Library (feedBack#126). Reset on every `playSong` call so a
// song launched from a deep-link / plugin screen still gets a sane
// fallback ('home').
let _playerOriginScreen = 'home';
let _settingsOriginScreen = 'home';
function _isInsideInteractiveControl(el) {
// Bail when the user is interacting with anything that has its
// own keyboard semantics — form controls (checkbox / select /
// button) consume arrow keys for their own behavior, and the
// filters drawer is a focus trap of those. Without this guard the
// library's arrow nav would steal arrow presses from a focused
// tuning checkbox or sort dropdown.
if (!el) return false;
const tag = el.tagName;
if (['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(tag)) return true;
if (el.isContentEditable) return true;
if (el.closest && el.closest('#lib-filter-drawer, [role="dialog"], #edit-modal')) return true;
return false;
}
function _isSpaceKey(e) {
return e.key === ' ' || e.key === 'Spacebar';
}
function _shortcutDispatchBlocked(e) {
if (_isTextInput(e.target)) return true;
// Space in Section Practice bar should pause/resume, not toggle checkboxes/buttons.
if (_isSpaceKey(e) && _sectionPracticeBarContains(e.target)) return false;
// While the Section Practice popover is open, Esc just closes it (handled by
// the popover's own keydown listener) — suppress the player-scope
// "back to library" Esc so the user doesn't get bounced out of the player.
if (e.key === 'Escape' && _sectionPracticePopoverOpen()) return true;
// Space on the player screen should always play/pause, even if focus is on a
// sidebar nav link, player rail button, popover control, or any other
// interactive element — the shortcut dispatcher calls preventDefault so the
// focused element won't also activate. Two exceptions keep native Space:
// text inputs (already exempted above), and focus inside a true modal
// dialog (role="dialog" aria-modal="true", or a .feedBack-modal overlay)
// layered over the player — a modal traps interaction, so Space must reach
// its focused control (e.g. the Close button) rather than toggle playback
// behind it. Non-modal player popovers/toasts (loop A/B, arrangement pin,
// role="dialog" aria-modal="false") are not modals and stay covered.
if (_isSpaceKey(e) && _getCurrentContext().isPlayer &&
!(e.target && e.target.closest &&
e.target.closest('[role="dialog"][aria-modal="true"], .feedBack-modal'))) {
return false;
}
// Escape is the universal "back" action and must fire like Space above even
// when a transport/rail control