feat: companion app improvements and intro overlay
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 39m1s
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 39m1s
Android: NES controller/keyboard enhancements, WebSocket reconnect, portrait mode. Backend: remote input handler updates. UI: companion intro overlay on dashboard, relay improvements. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -99,7 +99,7 @@ function mapKey(xdotoolKey: string): string {
|
||||
}
|
||||
|
||||
function handleMessage(data: string) {
|
||||
let msg: { t: string; k?: string; x?: number; y?: number; b?: number }
|
||||
let msg: { t: string; k?: string; x?: number; y?: number; b?: number; p?: number }
|
||||
try {
|
||||
msg = JSON.parse(data)
|
||||
} catch {
|
||||
@@ -114,6 +114,18 @@ function handleMessage(data: string) {
|
||||
case 'k': {
|
||||
if (!msg.k) break
|
||||
const key = mapKey(msg.k)
|
||||
// Dispatch player-tagged event for arcade/game apps (iframe postMessage or direct listeners)
|
||||
const player = msg.p ?? 0 // 0 = untagged/broadcast, 1 = P1, 2 = P2
|
||||
document.dispatchEvent(new CustomEvent('arcade-input', {
|
||||
detail: { key, player, type: 'down' },
|
||||
bubbles: true,
|
||||
}))
|
||||
// Also post to any iframe that might be listening (containerized apps like BotFights)
|
||||
const iframe = document.querySelector('iframe') as HTMLIFrameElement | null
|
||||
if (iframe?.contentWindow) {
|
||||
iframe.contentWindow.postMessage({ type: 'arcade-input', key, player, action: 'down' }, '*')
|
||||
}
|
||||
// Keep existing keydown/keyup for backward compat with non-arcade UI navigation
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
|
||||
document.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }))
|
||||
break
|
||||
|
||||
98
neode-ui/src/components/CompanionIntroOverlay.vue
Normal file
98
neode-ui/src/components/CompanionIntroOverlay.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="overlay-fade">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 flex items-end sm:items-center justify-center p-4 z-[3000]"
|
||||
@click.self="dismiss"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" />
|
||||
<div
|
||||
class="glass-card p-5 w-full max-w-sm relative z-10 mb-20 sm:mb-0"
|
||||
@click.stop
|
||||
>
|
||||
<!-- Icon -->
|
||||
<div class="flex justify-center mb-3">
|
||||
<div class="w-12 h-12 rounded-xl bg-orange-500/15 border border-orange-500/30 flex items-center justify-center">
|
||||
<svg class="w-7 h-7 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<rect x="3" y="7" width="18" height="11" rx="3" stroke-width="1.5" />
|
||||
<rect x="7.5" y="10" width="2" height="5" rx="0.5" fill="currentColor" />
|
||||
<rect x="6" y="11.5" width="5" height="2" rx="0.5" fill="currentColor" />
|
||||
<circle cx="16" cy="11" r="1.2" fill="currentColor" />
|
||||
<circle cx="14" cy="13.5" r="1.2" fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-white text-center mb-2">Remote Companion</h3>
|
||||
<p class="text-sm text-white/60 text-center mb-4 leading-relaxed">
|
||||
Control your node from another device. Install the
|
||||
<span class="text-orange-400 font-medium">Archipelago</span>
|
||||
companion app on your phone, connect to the same network, and use it as a
|
||||
gamepad or keyboard.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-2 text-xs text-white/40">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-5 h-5 rounded-full bg-white/10 flex items-center justify-center text-white/50 text-[10px] font-bold">1</span>
|
||||
<span>Install the APK on your phone</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-5 h-5 rounded-full bg-white/10 flex items-center justify-center text-white/50 text-[10px] font-bold">2</span>
|
||||
<span>Enter your node address and password</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-5 h-5 rounded-full bg-white/10 flex items-center justify-center text-white/50 text-[10px] font-bold">3</span>
|
||||
<span>Use D-pad & buttons or keyboard to control apps</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="mt-4 w-full py-2.5 rounded-lg bg-orange-500/20 border border-orange-500/30
|
||||
text-orange-400 text-sm font-medium hover:bg-orange-500/30 transition-colors"
|
||||
@click="dismiss"
|
||||
>
|
||||
Got it
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const STORAGE_KEY = 'neode_companion_intro_seen'
|
||||
|
||||
const visible = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
if (localStorage.getItem(STORAGE_KEY) !== '1') {
|
||||
// Delay slightly so it doesn't compete with login animation
|
||||
setTimeout(() => { visible.value = true }, 5000)
|
||||
}
|
||||
} catch {
|
||||
// localStorage unavailable
|
||||
}
|
||||
})
|
||||
|
||||
function dismiss() {
|
||||
visible.value = false
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, '1')
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.overlay-fade-enter-active { transition: opacity 0.3s ease; }
|
||||
.overlay-fade-leave-active { transition: opacity 0.2s ease; }
|
||||
.overlay-fade-enter-from,
|
||||
.overlay-fade-leave-to { opacity: 0; }
|
||||
.overlay-fade-enter-active .glass-card { transition: transform 0.3s ease; }
|
||||
.overlay-fade-enter-from .glass-card { transform: translateY(20px); }
|
||||
</style>
|
||||
@@ -121,6 +121,9 @@
|
||||
|
||||
<!-- Health Notifications Toast -->
|
||||
<HealthNotifications />
|
||||
|
||||
<!-- First-use companion intro overlay -->
|
||||
<CompanionIntroOverlay />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -137,6 +140,7 @@ import DashboardSidebar from '@/views/dashboard/DashboardSidebar.vue'
|
||||
import DashboardMobileNav from '@/views/dashboard/DashboardMobileNav.vue'
|
||||
import ConnectionBanner from '@/views/dashboard/ConnectionBanner.vue'
|
||||
import HealthNotifications from '@/views/dashboard/HealthNotifications.vue'
|
||||
import CompanionIntroOverlay from '@/components/CompanionIntroOverlay.vue'
|
||||
import { useRouteTransitions, isDetailRoute, ROUTE_BACKGROUNDS } from '@/views/dashboard/useRouteTransitions'
|
||||
import '@/views/dashboard/dashboard-styles.css'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user