fix: harden splash screen timer/listener leaks and origin validation
- SplashScreen: track all scheduled timers, clear on unmount (prevents ghost callbacks) - SplashScreen: manage video pause listener lifecycle (add once, remove when done) - SplashScreen: clear videoTimeUpdateInterval on unmount - Chat.vue: validate postMessage origin before accepting ready signal - App.vue: remove shadowed variable re-declaration in onKeyDown Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7b56927c3c
commit
173bf8fc0f
@ -142,8 +142,6 @@ function onKeyDown(e: KeyboardEvent) {
|
||||
}
|
||||
// 's' key activates screensaver when authenticated (skip if typing in input)
|
||||
if (e.key === 's' || e.key === 'S') {
|
||||
const target = e.target as HTMLElement
|
||||
const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable
|
||||
if (!isInput && appStore.isAuthenticated && !screensaverStore.isActive) {
|
||||
e.preventDefault()
|
||||
screensaverStore.activate()
|
||||
|
||||
@ -191,23 +191,35 @@ const isTypingLine3 = ref(false)
|
||||
const isTypingLine4 = ref(false)
|
||||
const videoElement = ref<HTMLVideoElement | null>(null)
|
||||
let introTypingTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
const pendingTimers: ReturnType<typeof setTimeout>[] = []
|
||||
|
||||
function scheduleTimer(fn: () => void, delay: number) {
|
||||
const id = setTimeout(fn, delay)
|
||||
pendingTimers.push(id)
|
||||
return id
|
||||
}
|
||||
|
||||
// Ensure video plays continuously from Welcome Noderunner through logo
|
||||
let videoPauseHandler: ((e: Event) => void) | null = null
|
||||
watch([showWelcome, showLogo], ([welcome, logo]) => {
|
||||
if ((welcome || logo) && videoElement.value) {
|
||||
// Ensure video is playing and doesn't pause
|
||||
if (videoElement.value.paused) {
|
||||
videoElement.value.play().catch(err => {
|
||||
console.warn('Video autoplay failed:', err)
|
||||
})
|
||||
}
|
||||
// Keep video playing - prevent any pauses
|
||||
videoElement.value.addEventListener('pause', (e) => {
|
||||
if (welcome || logo) {
|
||||
e.preventDefault()
|
||||
videoElement.value?.play()
|
||||
// Add pause prevention handler once, remove when no longer needed
|
||||
if (!videoPauseHandler) {
|
||||
videoPauseHandler = () => {
|
||||
if ((showWelcome.value || showLogo.value) && videoElement.value) {
|
||||
videoElement.value.play().catch(() => {})
|
||||
}
|
||||
}
|
||||
}, { once: false })
|
||||
videoElement.value.addEventListener('pause', videoPauseHandler)
|
||||
}
|
||||
} else if (videoPauseHandler && videoElement.value) {
|
||||
videoElement.value.removeEventListener('pause', videoPauseHandler)
|
||||
videoPauseHandler = null
|
||||
}
|
||||
})
|
||||
|
||||
@ -303,40 +315,38 @@ function skipIntro() {
|
||||
isTypingLine4.value = false
|
||||
|
||||
// Start background fade in at 0.3 opacity when welcome appears
|
||||
setTimeout(() => {
|
||||
scheduleTimer(() => {
|
||||
backgroundOpacity.value = 0.3
|
||||
}, 0)
|
||||
|
||||
|
||||
// Continue with welcome fade out after typing (2s) + cursor continues (1.5s) + 3 blinks (1.35s)
|
||||
setTimeout(() => {
|
||||
scheduleTimer(() => {
|
||||
fadeWelcome.value = true
|
||||
typingWelcome.value = false
|
||||
}, 4850)
|
||||
|
||||
|
||||
// Show logo - no zoom, just fade
|
||||
setTimeout(() => {
|
||||
scheduleTimer(() => {
|
||||
showLogo.value = true
|
||||
// Keep background at 0.3 opacity during logo display
|
||||
}, 5500)
|
||||
|
||||
// Hide welcome after logo starts appearing
|
||||
setTimeout(() => {
|
||||
scheduleTimer(() => {
|
||||
showWelcome.value = false
|
||||
}, 6000)
|
||||
|
||||
|
||||
// Fade background to full opacity just before completing (for smooth transition to modal)
|
||||
setTimeout(() => {
|
||||
scheduleTimer(() => {
|
||||
backgroundOpacity.value = 1
|
||||
}, 9000)
|
||||
|
||||
// Complete splash with smooth transition - wait for zoom to complete
|
||||
setTimeout(() => {
|
||||
// Add a small delay to ensure smooth transition
|
||||
setTimeout(() => {
|
||||
showSplash.value = false
|
||||
document.body.classList.add('splash-complete')
|
||||
localStorage.setItem('neode_intro_seen', '1')
|
||||
emit('complete')
|
||||
|
||||
// Complete splash with smooth transition
|
||||
scheduleTimer(() => {
|
||||
scheduleTimer(() => {
|
||||
showSplash.value = false
|
||||
document.body.classList.add('splash-complete')
|
||||
localStorage.setItem('neode_intro_seen', '1')
|
||||
emit('complete')
|
||||
}, 500)
|
||||
}, 9500)
|
||||
}
|
||||
@ -409,24 +419,24 @@ function startAlienIntro() {
|
||||
}
|
||||
backgroundOpacity.value = 0.3
|
||||
|
||||
introTypingTimeout = setTimeout(() => {
|
||||
scheduleTimer(() => {
|
||||
fadeWelcome.value = true
|
||||
typingWelcome.value = false
|
||||
}, 4850)
|
||||
|
||||
introTypingTimeout = setTimeout(() => {
|
||||
scheduleTimer(() => {
|
||||
showLogo.value = true
|
||||
}, 5500)
|
||||
|
||||
introTypingTimeout = setTimeout(() => {
|
||||
scheduleTimer(() => {
|
||||
showWelcome.value = false
|
||||
}, 6000)
|
||||
|
||||
introTypingTimeout = setTimeout(() => {
|
||||
scheduleTimer(() => {
|
||||
backgroundOpacity.value = 1
|
||||
}, 9000)
|
||||
|
||||
introTypingTimeout = setTimeout(() => {
|
||||
scheduleTimer(() => {
|
||||
if (videoElement.value && !videoElement.value.paused) {
|
||||
sessionStorage.setItem('video_intro_currentTime', videoElement.value.currentTime.toString())
|
||||
sessionStorage.setItem('video_intro_wasPlaying', 'true')
|
||||
@ -455,6 +465,14 @@ onBeforeUnmount(() => {
|
||||
clearTimeout(introTypingTimeout)
|
||||
introTypingTimeout = null
|
||||
}
|
||||
// Clear all scheduled timers to prevent firing on unmounted component
|
||||
for (const id of pendingTimers) clearTimeout(id)
|
||||
pendingTimers.length = 0
|
||||
// Clear video time update interval
|
||||
if (videoTimeUpdateInterval) {
|
||||
clearInterval(videoTimeUpdateInterval)
|
||||
videoTimeUpdateInterval = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -83,6 +83,11 @@ function closeChat() {
|
||||
|
||||
function onAiuiMessage(event: MessageEvent) {
|
||||
if (!aiuiUrl.value) return
|
||||
// Validate origin — only accept messages from AIUI
|
||||
try {
|
||||
const expected = new URL(aiuiUrl.value, window.location.origin).origin
|
||||
if (event.origin !== expected) return
|
||||
} catch { return }
|
||||
const msg = event.data
|
||||
if (msg && msg.type === 'ready') {
|
||||
aiuiConnected.value = true
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user