Enhance user interaction and audio feedback in CLI and login components
- Updated keydown event handling in App.vue to improve keyboard navigation. - Enhanced AppSwitcher.vue with a status indicator for online presence. - Integrated new sci-fi typing sound effect in useLoginSounds.ts for a more engaging user experience during login. - Modified login handling functions in Login.vue to include sound feedback on setup and login actions. - Added CLI store integration to play navigation sounds when opening and closing the CLI.
This commit is contained in:
parent
59210a7927
commit
c9f6e6b8ae
@ -142,7 +142,7 @@ const isReady = ref(false)
|
||||
* - User is on a direct route (refresh/bookmark)
|
||||
*/
|
||||
onMounted(async () => {
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
window.addEventListener('keydown', onKeyDown, true)
|
||||
window.addEventListener('mousemove', onUserActivity)
|
||||
window.addEventListener('mousedown', onUserActivity)
|
||||
window.addEventListener('keydown', onUserActivity)
|
||||
@ -162,7 +162,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', onKeyDown)
|
||||
window.removeEventListener('keydown', onKeyDown, true)
|
||||
window.removeEventListener('mousemove', onUserActivity)
|
||||
window.removeEventListener('mousedown', onUserActivity)
|
||||
window.removeEventListener('keydown', onUserActivity)
|
||||
|
||||
@ -2,15 +2,22 @@
|
||||
<div class="relative" ref="containerRef">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 px-2 py-1.5 rounded-lg text-white/90 hover:bg-white/10 hover:text-white transition-colors min-w-0"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-lg glass-card text-white/90 hover:bg-white/10 hover:text-white transition-colors min-w-0 border border-white/10"
|
||||
@click="showDropdown = !showDropdown"
|
||||
>
|
||||
<img
|
||||
src="/assets/img/logo-archipelago.svg"
|
||||
alt="Archipelago"
|
||||
class="w-6 h-6 shrink-0 object-contain"
|
||||
class="w-5 h-5 shrink-0 object-contain opacity-90"
|
||||
/>
|
||||
<span class="text-sm font-medium truncate max-w-[120px] sm:max-w-[140px]">Archipelago CLI</span>
|
||||
<span class="text-sm font-medium truncate max-w-[100px] sm:max-w-[120px]">Archipelago CLI</span>
|
||||
<div class="flex items-center gap-1.5 shrink-0 pl-1 border-l border-white/20">
|
||||
<div class="relative">
|
||||
<div class="w-2 h-2 rounded-full bg-green-400"></div>
|
||||
<div class="absolute inset-0 w-2 h-2 rounded-full bg-green-400 animate-ping opacity-50"></div>
|
||||
</div>
|
||||
<span class="text-xs text-white/80">Online</span>
|
||||
</div>
|
||||
<svg class="w-4 h-4 text-white/50 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
|
||||
@ -10,6 +10,7 @@ import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useControllerStore } from '@/stores/controller'
|
||||
import { useSpotlightStore } from '@/stores/spotlight'
|
||||
import { useCLIStore } from '@/stores/cli'
|
||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
import { playNavSound } from '@/composables/useNavSounds'
|
||||
|
||||
@ -166,6 +167,12 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
if (useCLIStore().isOpen) {
|
||||
useCLIStore().close()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
if (isInsideContainer(activeEl)) {
|
||||
const container = activeEl.closest('[data-controller-container]') as HTMLElement | null
|
||||
|
||||
@ -172,6 +172,47 @@ export function playTypingTick() {
|
||||
a.play().catch(() => {})
|
||||
}
|
||||
|
||||
/** Sci-fi typing sound - short synth blip + click per key, for login/password fields */
|
||||
export function playSciFiTypingTick() {
|
||||
const ctx = getContext()
|
||||
if (!ctx) return
|
||||
|
||||
try {
|
||||
if (ctx.state === 'suspended') ctx.resume()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const t = ctx.currentTime
|
||||
|
||||
// Main tone
|
||||
const osc = ctx.createOscillator()
|
||||
const gain = ctx.createGain()
|
||||
osc.type = 'sine'
|
||||
osc.frequency.setValueAtTime(880, t)
|
||||
osc.frequency.exponentialRampToValueAtTime(660, t + 0.03)
|
||||
gain.gain.setValueAtTime(0, t)
|
||||
gain.gain.linearRampToValueAtTime(0.08, t + 0.005)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.06)
|
||||
osc.connect(gain)
|
||||
gain.connect(ctx.destination)
|
||||
osc.start(t)
|
||||
osc.stop(t + 0.06)
|
||||
|
||||
// Click - short transient at key press
|
||||
const clickOsc = ctx.createOscillator()
|
||||
const clickGain = ctx.createGain()
|
||||
clickOsc.type = 'square'
|
||||
clickOsc.frequency.setValueAtTime(1200, t)
|
||||
clickGain.gain.setValueAtTime(0, t)
|
||||
clickGain.gain.linearRampToValueAtTime(0.04, t + 0.001)
|
||||
clickGain.gain.exponentialRampToValueAtTime(0.001, t + 0.012)
|
||||
clickOsc.connect(clickGain)
|
||||
clickGain.connect(ctx.destination)
|
||||
clickOsc.start(t)
|
||||
clickOsc.stop(t + 0.012)
|
||||
}
|
||||
|
||||
/** Gaming-style boot thud - soft impact when dashboard loads */
|
||||
export function playDashboardLoadOomph() {
|
||||
const ctx = getContext()
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { playNavSound } from '@/composables/useNavSounds'
|
||||
|
||||
export const useCLIStore = defineStore('cli', () => {
|
||||
const isOpen = ref(false)
|
||||
|
||||
function open() {
|
||||
isOpen.value = true
|
||||
playNavSound('action')
|
||||
}
|
||||
|
||||
function close() {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { playNavSound } from '@/composables/useNavSounds'
|
||||
|
||||
const RECENT_ITEMS_KEY = 'archipelago-spotlight-recent'
|
||||
const MAX_RECENT_ITEMS = 8
|
||||
@ -49,6 +50,7 @@ export const useSpotlightStore = defineStore('spotlight', () => {
|
||||
isOpen.value = true
|
||||
selectedIndex.value = 0
|
||||
loadRecentItems()
|
||||
playNavSound('action')
|
||||
}
|
||||
|
||||
function close() {
|
||||
|
||||
@ -1,28 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-8 flex items-start justify-between">
|
||||
<div>
|
||||
<div class="min-h-[4.5rem]">
|
||||
<h1 class="text-3xl font-bold text-white mb-2 drop-shadow-[0_2px_8px_rgba(0,0,0,0.6)]">
|
||||
{{ line1Text }}<span v-if="showCaretLine1" class="typing-caret"></span>
|
||||
</h1>
|
||||
<p class="text-white/80">
|
||||
{{ line2Text }}<span v-if="showCaretLine2" class="typing-caret"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
<div class="min-h-[4.5rem]">
|
||||
<h1 class="text-3xl font-bold text-white mb-2 drop-shadow-[0_2px_8px_rgba(0,0,0,0.6)]">
|
||||
{{ line1Text }}<span v-if="showCaretLine1" class="typing-caret"></span>
|
||||
</h1>
|
||||
<p class="text-white/80">
|
||||
{{ line2Text }}<span v-if="showCaretLine2" class="typing-caret"></span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- Compact Status Indicator - click to trigger screensaver (dev) -->
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 px-4 py-2 glass-card rounded-lg cursor-pointer hover:bg-white/5 transition-colors"
|
||||
@click="screensaverStore.activate()"
|
||||
>
|
||||
<div class="relative">
|
||||
<div class="w-2 h-2 rounded-full bg-green-400"></div>
|
||||
<div class="absolute inset-0 w-2 h-2 rounded-full bg-green-400 animate-ping opacity-75"></div>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-white">Online</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Section Overviews - 2advanced staggered animation (hidden until typing starts, then animate with typing) -->
|
||||
@ -277,13 +263,11 @@
|
||||
import { computed, ref, watch, onBeforeUnmount } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { useScreensaverStore } from '../stores/screensaver'
|
||||
import { useLoginTransitionStore } from '../stores/loginTransition'
|
||||
import { PackageState } from '../types/api'
|
||||
import { playTypingSound } from '@/composables/useLoginSounds'
|
||||
|
||||
const store = useAppStore()
|
||||
const screensaverStore = useScreensaverStore()
|
||||
const loginTransition = useLoginTransitionStore()
|
||||
|
||||
const LINE1 = "Welcome Noderunner"
|
||||
|
||||
@ -41,7 +41,8 @@
|
||||
type="password"
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
||||
placeholder="Enter a password (min 8 characters)"
|
||||
@keyup.enter="handleSetup"
|
||||
@keydown="onPasswordKeydown"
|
||||
@keyup.enter="handleSetupWithSound"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
@ -56,13 +57,14 @@
|
||||
type="password"
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
||||
placeholder="Confirm your password"
|
||||
@keyup.enter="handleSetup"
|
||||
@keydown="onPasswordKeydown"
|
||||
@keyup.enter="handleSetupWithSound"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="handleSetup"
|
||||
@click="handleSetupWithSound"
|
||||
:disabled="loading || !password || password !== confirmPassword"
|
||||
class="w-full glass-button px-6 py-3 rounded-lg font-medium transition-all hover:bg-black/70 hover:border-white/30 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
@ -89,13 +91,14 @@
|
||||
type="password"
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
||||
placeholder="Enter your password"
|
||||
@keyup.enter="handleLogin"
|
||||
@keydown="onPasswordKeydown"
|
||||
@keyup.enter="handleLoginWithSound"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="handleLogin"
|
||||
@click="handleLoginWithSound"
|
||||
:disabled="loading || !password"
|
||||
class="w-full glass-button px-6 py-3 rounded-lg font-medium transition-all hover:bg-black/70 hover:border-white/30 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
@ -136,7 +139,7 @@ import AnimatedLogo from '@/components/AnimatedLogo.vue'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { useLoginTransitionStore } from '../stores/loginTransition'
|
||||
import { rpcClient } from '../api/rpc-client'
|
||||
import { startSynthwave, stopSynthwave, playLoginSuccessWhoosh } from '@/composables/useLoginSounds'
|
||||
import { startSynthwave, stopSynthwave, playLoginSuccessWhoosh, playPop, playSciFiTypingTick } from '@/composables/useLoginSounds'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useAppStore()
|
||||
@ -174,6 +177,13 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
|
||||
function handleSetupWithSound() {
|
||||
if (!loading.value && password.value && password.value === confirmPassword.value) {
|
||||
playPop()
|
||||
}
|
||||
handleSetup()
|
||||
}
|
||||
|
||||
async function handleSetup() {
|
||||
if (!password.value || password.value.length < 8) {
|
||||
error.value = 'Password must be at least 8 characters'
|
||||
@ -210,6 +220,19 @@ async function handleSetup() {
|
||||
}
|
||||
}
|
||||
|
||||
function onPasswordKeydown(e: KeyboardEvent) {
|
||||
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
playSciFiTypingTick()
|
||||
}
|
||||
}
|
||||
|
||||
function handleLoginWithSound() {
|
||||
if (!loading.value && password.value) {
|
||||
playPop()
|
||||
}
|
||||
handleLogin()
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
if (!password.value) return
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user