mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-12 03:41:40 +00:00
fix(onboarding): midi-input multi-provider discovery + home-tour lifecycle (#568)
Addresses Codex review of #526/#528: - midi-input discover(): one provider's enumerate() rejection no longer aborts the whole discovery — other providers (e.g. a native/desktop MIDI provider) are still queried; denial is only reported when NO provider enumerates. - Home tour now waits for a 'v3:dashboard-rendered' event (dashboard.js emits it after the #v3-home innerHTML swap) before attaching Shepherd, instead of a single animation frame that could latch onto pre-render nodes the async dashboard render then replaces. - "Play it now" onboarding now arms the tour (armPendingFirstRun) to run the first time the user returns to v3-home, instead of silently never showing it. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
820a18648a
commit
5d0229fc82
@@ -177,14 +177,31 @@
|
|||||||
async function _discover() {
|
async function _discover() {
|
||||||
if (providers.size === 0) return _unavailable('No MIDI provider registered', _snapshot());
|
if (providers.size === 0) return _unavailable('No MIDI provider registered', _snapshot());
|
||||||
let found = 0;
|
let found = 0;
|
||||||
|
let enumerated = 0;
|
||||||
|
let lastError = null;
|
||||||
for (const provider of providers.values()) {
|
for (const provider of providers.values()) {
|
||||||
if (!provider.handlers.enumerate) continue;
|
if (!provider.handlers.enumerate) continue;
|
||||||
let list;
|
let list;
|
||||||
try { list = await provider.handlers.enumerate(); }
|
try { list = await provider.handlers.enumerate(); }
|
||||||
catch (e) {
|
catch (e) {
|
||||||
// requestMIDIAccess rejection = permission denied / unsupported.
|
// One provider failing (e.g. browser Web-MIDI permission denied,
|
||||||
return _denied(_str(e && e.message, 'MIDI access denied'), _snapshot());
|
// or unsupported) must NOT hide other providers' devices (e.g. a
|
||||||
|
// native/desktop MIDI provider). Record it and keep going; denial
|
||||||
|
// is only reported below when NO provider enumerated successfully.
|
||||||
|
lastError = e;
|
||||||
|
// Drop this provider's now-unverifiable sources so revoked /
|
||||||
|
// unplugged devices don't linger as selectable and fail on a
|
||||||
|
// later open (it just failed to enumerate, so we can't trust its
|
||||||
|
// previous list).
|
||||||
|
for (const [key, s] of Array.from(sources.entries())) {
|
||||||
|
if (s.providerId === provider.id) {
|
||||||
|
_closeSessionInternal(key, 'enumerate-failed');
|
||||||
|
sources.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
enumerated += 1;
|
||||||
const fresh = new Set();
|
const fresh = new Set();
|
||||||
for (const raw of (Array.isArray(list) ? list : [])) {
|
for (const raw of (Array.isArray(list) ? list : [])) {
|
||||||
const sourceId = _str(raw.sourceId || raw.id, '');
|
const sourceId = _str(raw.sourceId || raw.id, '');
|
||||||
@@ -216,6 +233,11 @@
|
|||||||
}
|
}
|
||||||
// Restore a previously-selected source if it reappeared.
|
// Restore a previously-selected source if it reappeared.
|
||||||
if (selectedKey && !sources.has(selectedKey)) { /* keep the preference; it may return later */ }
|
if (selectedKey && !sources.has(selectedKey)) { /* keep the preference; it may return later */ }
|
||||||
|
// Only surface a denial when NO provider could enumerate — otherwise a
|
||||||
|
// single denied/unsupported provider would mask the working ones.
|
||||||
|
if (enumerated === 0 && lastError) {
|
||||||
|
return _denied(_str(lastError && lastError.message, 'MIDI access denied'), _snapshot());
|
||||||
|
}
|
||||||
_emit('sources-changed', { count: found });
|
_emit('sources-changed', { count: found });
|
||||||
_contributeDiagnostics();
|
_contributeDiagnostics();
|
||||||
return _handled(_snapshot({ discovered: found }));
|
return _handled(_snapshot({ discovered: found }));
|
||||||
|
|||||||
@@ -240,6 +240,10 @@
|
|||||||
if (window.v3AudioRouting && typeof window.v3AudioRouting.render === 'function') {
|
if (window.v3AudioRouting && typeof window.v3AudioRouting.render === 'function') {
|
||||||
try { window.v3AudioRouting.render(document.getElementById('v3-audio-routing')); } catch (e) { /* */ }
|
try { window.v3AudioRouting.render(document.getElementById('v3-audio-routing')); } catch (e) { /* */ }
|
||||||
}
|
}
|
||||||
|
// Signal that #v3-home has been (re)built, so the first-run onboarding
|
||||||
|
// tour can wait for the real cards instead of attaching to nodes from a
|
||||||
|
// prior render that this innerHTML swap just replaced.
|
||||||
|
try { document.dispatchEvent(new CustomEvent('v3:dashboard-rendered')); } catch (e) { /* older runtimes */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
function statCard(value, unit, unitColor) {
|
function statCard(value, unit, unitColor) {
|
||||||
|
|||||||
@@ -93,18 +93,47 @@
|
|||||||
try {
|
try {
|
||||||
if (t.hasSeen(TOUR_ID) || t.hasDismissed(TOUR_ID)) return;
|
if (t.hasSeen(TOUR_ID) || t.hasDismissed(TOUR_ID)) return;
|
||||||
} catch (e) { /* private mode — fall through and attempt once */ }
|
} catch (e) { /* private mode — fall through and attempt once */ }
|
||||||
// Make sure the home screen is in view so the spotlight targets exist;
|
// Make sure the home screen is in view so the spotlight targets exist.
|
||||||
// the per-step waitFor handles the async dashboard render.
|
|
||||||
if (typeof window.showScreen === 'function') {
|
if (typeof window.showScreen === 'function') {
|
||||||
try { window.showScreen('v3-home'); } catch (e) { /* best-effort */ }
|
try { window.showScreen('v3-home'); } catch (e) { /* best-effort */ }
|
||||||
}
|
}
|
||||||
// Defer a frame so the 'v3:profile-updated' dashboard re-render has a
|
// Wait for the dashboard to finish (re)rendering #v3-home before
|
||||||
// chance to begin before Shepherd starts polling for the first target.
|
// attaching Shepherd. Starting after a single animation frame can latch
|
||||||
var raf = window.requestAnimationFrame || function (fn) { return setTimeout(fn, 16); };
|
// onto the pre-render #v3-hero / [data-tour] nodes that the async render
|
||||||
raf(function () { try { t.start(TOUR_ID); } catch (e) { /* degrade */ } });
|
// then replaces, breaking the tour. Listen for the render-complete
|
||||||
|
// event; fall back on a timeout in case the render already finished
|
||||||
|
// (no event coming) or never fires.
|
||||||
|
var started = false;
|
||||||
|
var go = function () {
|
||||||
|
if (started) return;
|
||||||
|
started = true;
|
||||||
|
try { document.removeEventListener('v3:dashboard-rendered', go); } catch (e) { /* */ }
|
||||||
|
try { t.start(TOUR_ID); } catch (e) { /* degrade */ }
|
||||||
|
};
|
||||||
|
try { document.addEventListener('v3:dashboard-rendered', go, { once: true }); } catch (e) { /* */ }
|
||||||
|
setTimeout(go, 1200);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.v3OnboardingTour = { startFirstRun: startFirstRun };
|
// Onboarding paths that navigate straight to the player ("Play it now")
|
||||||
|
// can't start the tour immediately — there is no home screen to spotlight
|
||||||
|
// yet. Arm it to run the first time the user lands back on v3-home.
|
||||||
|
function armPendingFirstRun() {
|
||||||
|
var t = window.slopsmithTour;
|
||||||
|
if (!t || typeof t.start !== 'function') return;
|
||||||
|
try {
|
||||||
|
if (t.hasSeen(TOUR_ID) || t.hasDismissed(TOUR_ID)) return;
|
||||||
|
} catch (e) { /* private mode — fall through */ }
|
||||||
|
var sm = window.slopsmith;
|
||||||
|
if (!sm || typeof sm.on !== 'function') return;
|
||||||
|
var onScreen = function (e) {
|
||||||
|
if (!(e && e.detail && e.detail.id === 'v3-home')) return;
|
||||||
|
try { if (typeof sm.off === 'function') sm.off('screen:changed', onScreen); } catch (_) { /* */ }
|
||||||
|
startFirstRun();
|
||||||
|
};
|
||||||
|
sm.on('screen:changed', onScreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.v3OnboardingTour = { startFirstRun: startFirstRun, armPendingFirstRun: armPendingFirstRun };
|
||||||
|
|
||||||
// tour-engine.js assigns window.slopsmithTour at script-eval time, so if it
|
// tour-engine.js assigns window.slopsmithTour at script-eval time, so if it
|
||||||
// is loaded before us register() succeeds immediately; otherwise retry once
|
// is loaded before us register() succeeds immediately; otherwise retry once
|
||||||
|
|||||||
@@ -520,6 +520,12 @@
|
|||||||
if (!editing && !finishOpts.launchingSong &&
|
if (!editing && !finishOpts.launchingSong &&
|
||||||
window.v3OnboardingTour && typeof window.v3OnboardingTour.startFirstRun === 'function') {
|
window.v3OnboardingTour && typeof window.v3OnboardingTour.startFirstRun === 'function') {
|
||||||
try { window.v3OnboardingTour.startFirstRun(); } catch (e) { /* never block onboarding */ }
|
try { window.v3OnboardingTour.startFirstRun(); } catch (e) { /* never block onboarding */ }
|
||||||
|
} else if (!editing && finishOpts.launchingSong &&
|
||||||
|
window.v3OnboardingTour && typeof window.v3OnboardingTour.armPendingFirstRun === 'function') {
|
||||||
|
// "Play it now" navigates straight to the player, so the home
|
||||||
|
// tour can't run now — arm it to fire the first time the user
|
||||||
|
// returns to home, instead of silently never showing.
|
||||||
|
try { window.v3OnboardingTour.armPendingFirstRun(); } catch (e) { /* never block onboarding */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user