Merge pull request 'fix(intro): reliable first-visit cinematic — audible oomph, no splash hijack on deep routes, e2e suite' (#91) from intro-reliability-video-perf into main
Demo images / Build & push demo images (push) Successful in 2m34s
Demo images / Build & push demo images (push) Successful in 2m34s
This commit was merged in pull request #91.
This commit is contained in:
+33
-10
@@ -400,14 +400,19 @@ onMounted(async () => {
|
||||
// onboarded node).
|
||||
let replayRequested = sessionStorage.getItem('archipelago_replay_intro') === '1'
|
||||
if (replayRequested) sessionStorage.removeItem('archipelago_replay_intro')
|
||||
// The route object still holds the router's START_LOCATION ('/') here —
|
||||
// the initial navigation resolves asynchronously — so a deep-route load
|
||||
// (e.g. a direct /login visit) would masquerade as a root boot. The URL
|
||||
// bar is the only truthful source this early.
|
||||
const bootPath = window.location.pathname
|
||||
// Public demo: every fresh boot at the root (first visit or a browser
|
||||
// refresh) starts with the typing splash for the full effect. In-session SPA
|
||||
// navigation never remounts App, and deep-route refreshes keep their place.
|
||||
const { IS_DEMO } = await import('@/composables/useDemoIntro')
|
||||
if (IS_DEMO && route.path === '/') replayRequested = true
|
||||
if (IS_DEMO && bootPath === '/') replayRequested = true
|
||||
let onboardingComplete: boolean | null = localStorage.getItem('neode_onboarding_complete') === '1' ? true : null
|
||||
const splashCandidate = !seenIntro
|
||||
&& (fromBoot || (route.path === '/' && import.meta.env.VITE_DEV_MODE !== 'boot'))
|
||||
&& (fromBoot || (bootPath === '/' && import.meta.env.VITE_DEV_MODE !== 'boot'))
|
||||
|
||||
if (splashCandidate && onboardingComplete !== true) {
|
||||
try {
|
||||
@@ -427,22 +432,29 @@ onMounted(async () => {
|
||||
|
||||
if (shouldShowIntroSplash({
|
||||
seenIntro,
|
||||
routePath: route.path,
|
||||
routePath: bootPath,
|
||||
fromBoot,
|
||||
devMode: import.meta.env.VITE_DEV_MODE,
|
||||
onboardingComplete,
|
||||
replayRequested,
|
||||
})) {
|
||||
// The intro is definitely playing — unmute the whole cinematic (speech,
|
||||
// synthwave, pops) even on browsers whose localStorage says onboarding is
|
||||
// complete; the sound gate otherwise silences replays.
|
||||
const { enableCinematicSounds } = await import('@/composables/useLoginSounds')
|
||||
enableCinematicSounds()
|
||||
// Kick off the intro video download NOW: the splash's <video> element only
|
||||
// mounts ~20s in (after the typing sequence), and on a cold cache the file
|
||||
// would otherwise start fetching mid-sequence and stutter.
|
||||
// would otherwise start fetching mid-sequence and stutter. A detached
|
||||
// <video preload=auto> is used because Chromium does not support
|
||||
// <link rel=preload as=video> — the media cache then serves the splash's
|
||||
// element, which uses the identical URL.
|
||||
try {
|
||||
const link = document.createElement('link')
|
||||
link.rel = 'preload'
|
||||
link.as = 'video'
|
||||
link.type = 'video/mp4'
|
||||
link.href = '/assets/video/video-intro.mp4?v=8'
|
||||
document.head.appendChild(link)
|
||||
const warm = document.createElement('video')
|
||||
warm.muted = true
|
||||
warm.preload = 'auto'
|
||||
warm.src = '/assets/video/video-intro.mp4?v=8'
|
||||
;(window as unknown as { __introVideoWarm?: HTMLVideoElement }).__introVideoWarm = warm
|
||||
} catch { /* ignore */ }
|
||||
// Coming from boot screen — show the full splash intro (Enter to Exit → typing → logo)
|
||||
showSplash.value = true
|
||||
@@ -516,6 +528,17 @@ async function handleSplashComplete() {
|
||||
return
|
||||
}
|
||||
|
||||
// Demo: the cinematic always continues into the onboarding intro page —
|
||||
// the mock backend reports "onboarded", which would otherwise route
|
||||
// straight to /login and cut the sequence short.
|
||||
{
|
||||
const { IS_DEMO } = await import('@/composables/useDemoIntro')
|
||||
if (IS_DEMO) {
|
||||
router.push('/onboarding/intro').catch(() => {})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { checkOnboardingStatus } = await import('@/composables/useOnboarding')
|
||||
const seenOnboarding = await checkOnboardingStatus()
|
||||
|
||||
@@ -8,14 +8,27 @@
|
||||
* exempt and continue to play regardless.
|
||||
*/
|
||||
|
||||
/** One-way, per-page-load override: when App.vue decides to run the intro
|
||||
* splash (demo fresh boot, prod first install, Replay Intro), the WHOLE
|
||||
* cinematic — speech, synthwave, pops — must be audible even though
|
||||
* localStorage already says onboarding is complete. Without this the
|
||||
* replayed intro runs silent (lost "Welcome Noderunner" + song). */
|
||||
let cinematicMode = false
|
||||
export function enableCinematicSounds() {
|
||||
cinematicMode = true
|
||||
}
|
||||
|
||||
/** True when the node has not yet completed onboarding — i.e. we're
|
||||
* still in the first-install cinematic. Reads the localStorage cache
|
||||
* set by useOnboarding (which is re-seeded from the backend on each
|
||||
* successful check), so this stays correct after a browser clear
|
||||
* once the onboarding-complete probe runs. Sound calls that fire
|
||||
* before that probe completes will fall through silent on an already-
|
||||
* onboarded node — which is exactly what we want. */
|
||||
* still in the first-install cinematic — or when the intro is being
|
||||
* deliberately replayed this page load (see enableCinematicSounds).
|
||||
* Reads the localStorage cache set by useOnboarding (which is
|
||||
* re-seeded from the backend on each successful check), so this stays
|
||||
* correct after a browser clear once the onboarding-complete probe
|
||||
* runs. Sound calls that fire before that probe completes will fall
|
||||
* through silent on an already-onboarded node — which is exactly what
|
||||
* we want: ordinary re-logins stay quiet. */
|
||||
function isFirstInstallPhase(): boolean {
|
||||
if (cinematicMode) return true
|
||||
try {
|
||||
return localStorage.getItem('neode_onboarding_complete') !== '1'
|
||||
} catch {
|
||||
@@ -260,7 +273,10 @@ export function playKeyboardTypingSound() {
|
||||
*/
|
||||
export function playDashboardLoadOomph(force = false) {
|
||||
if (!force && !isFirstInstallPhase()) return
|
||||
const ctx = getContext()
|
||||
// The login→dashboard route change runs stopAllAudio(), which CLOSES the
|
||||
// AudioContext — so the context must be recreated here, not just fetched.
|
||||
// The login click's sticky user activation lets the fresh context run.
|
||||
const ctx = ensureContext()
|
||||
if (!ctx) return
|
||||
|
||||
try {
|
||||
|
||||
@@ -69,12 +69,22 @@ onMounted(() => {
|
||||
}, 2100)
|
||||
})
|
||||
|
||||
/** Any exit from the intro INTO login is the end of the cinematic — hand the
|
||||
* login the one-shot finale flag so the dashboard plays its full first-entry
|
||||
* reveal (zoom + oomph). OnboardingDone sets the same flag at the end of the
|
||||
* full wizard; these are the shortcut exits that used to skip it (which is
|
||||
* why the demo never got the big entrance). */
|
||||
function armOnboardingFinale() {
|
||||
try { sessionStorage.setItem('archy_onboarding_finale', '1') } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function goToOptions() {
|
||||
playNavSound('action')
|
||||
// Demo: skip the onboarding wizard (seed/identity setup) entirely — go straight
|
||||
// to login, which is prefilled with the demo password.
|
||||
if (isDemo) {
|
||||
localStorage.setItem('neode_onboarding_complete', '1')
|
||||
armOnboardingFinale()
|
||||
router.push('/login').catch(() => {})
|
||||
return
|
||||
}
|
||||
@@ -89,6 +99,7 @@ function goToRestore() {
|
||||
function goToLogin() {
|
||||
playNavSound('action')
|
||||
localStorage.setItem('neode_onboarding_complete', '1')
|
||||
armOnboardingFinale()
|
||||
router.push('/login').catch(() => {})
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user