feat(player): global autoplay & auto-exit option (songs + lessons) (#558)

* fix(v3): pedal click opens the plugin's screen, not its settings

The v3 Pedalboard's settingsTarget() resolved settings-first, so a
plugin that ships both a screen and a settings panel (notably the
bundled Audio Engine) could only ever reach its settings from the
pedalboard — its actual page was unreachable.

Flip to screen-first (stompbox metaphor: step on the pedal, see the
pedal), falling back to settings when there is no screen. Keep a
settings fallback in openPluginSettings() when a declared screen
isn't mounted yet (installing/failed) so settings-bearing plugins are
never stranded on a toast. Drive the pedal aria-label off the same
target so it never promises the wrong surface. Update the unit test
contract to screen > settings > none.

Fixes #555

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(player): global autoplay & auto-exit option (songs + lessons)

Single Settings toggle (autoplayExit, default ON) that auto-starts a song
once it's ready and returns to the launching menu when it ends. Auto-exit
defers while a results/score overlay is on top (heuristic + holdAutoExit()
contract) so a scoring plugin's screen drives the exit. Player origin is now
context-aware (lessons return to the lessons screen via setReturnScreen()),
fixing lesson completion bouncing to the library.

Core-only; songs and lessons share the playSong -> highway path. Adds a
read-only window.slopsmith.autoplayExit getter + holdAutoExit()/setReturnScreen()
for plugins. Unit tests for the pure helpers (_autoplayExitEnabled,
_resolvePlayerOrigin, _resultsOverlayVisible).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos
2026-06-22 11:04:03 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent f79efe2516
commit 4dc5936712
7 changed files with 369 additions and 14 deletions
+7
View File
@@ -402,6 +402,13 @@
<span class="text-sm text-gray-300">Left-handed <span class="text-gray-500">(invert frets on the note highway)</span></span>
</label>
</div>
<div>
<label class="flex items-center gap-3 cursor-pointer select-none">
<input type="checkbox" id="setting-autoplay-exit" checked onchange="setAutoplayExit(this.checked)"
class="rounded border-gray-600 bg-dark-700 text-accent focus:ring-accent/40">
<span class="text-sm text-gray-300">Autoplay &amp; auto-exit <span class="text-gray-500">(start songs/lessons automatically and return to the menu when the score screen closes)</span></span>
</label>
</div>
<div>
<label class="text-sm font-medium text-gray-400 mb-2 block">Default Arrangement</label>
<select id="default-arrangement"
+27
View File
@@ -218,6 +218,33 @@
// ── navigation into the plugin's own lesson view ─────────────────────---
function launchLesson(packId, lessonId) {
if (!packId || !lessonId) return;
// When the lesson's song auto-exits (or the player is closed), return
// to the lessons catalog to pick the next one — not the song library.
// playSong() consumes this one-shot override even though the external
// tutorials plugin owns the actual playSong call. The legitimate path
// is lessons → plugin-tutorials → player; if the launch is abandoned
// (plugin missing, deep-link fails, user backs out) the override would
// otherwise linger and mis-route the next library song. So clear it on
// the first navigation that leaves the launch flow (any screen other
// than the tutorials waypoint or the player that consumes it).
try {
if (window.slopsmith && typeof window.slopsmith.setReturnScreen === 'function') {
window.slopsmith.setReturnScreen('v3-lessons');
if (sm && typeof sm.on === 'function' && typeof sm.off === 'function') {
const clearStale = (e) => {
const id = e && e.detail && e.detail.id;
if (id === 'plugin-tutorials') return; // expected waypoint — keep waiting
// 'player' means playSong already consumed the override;
// anything else means the launch was abandoned.
if (id !== 'player' && window.slopsmith._nextReturnScreen === 'v3-lessons') {
window.slopsmith.setReturnScreen(null);
}
sm.off('screen:changed', clearStale);
};
sm.on('screen:changed', clearStale);
}
}
} catch (_e) { /* non-fatal */ }
// navigate() stores nav params AND calls showScreen(); the tutorials
// plugin's init() reads getNavParams() to deep-link to {packId, lessonId}.
if (sm && typeof sm.navigate === 'function') {