feat: botfights container app + mobile gamepad + indeedhub fixes
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled
- Promote botfights from external proxy to container app (port 9100) - Add /app/botfights/ nginx proxy rules (HTTP + HTTPS) - Add ARCHY_EMBEDDED env var to botfights container config - Add BOTFIGHTS_IMAGE to image-versions.sh - Add mobile gamepad overlay (D-pad + A/B + START/SELECT) for botfights arcade mode, sends postMessage arcade-input to iframe - Remove old /ext/botfights/ and port 8901 external proxy blocks - IndeeHub: add post-install nginx patching for NIP-07 provider injection - IndeeHub: fix docker image references to registry (was localhost) - IndeeHub: update port 7777 -> 7778 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,8 +38,13 @@
|
||||
@open-new-tab-and-back="openNewTabAndBack"
|
||||
/>
|
||||
|
||||
<!-- Mobile bottom browser bar — part of flex layout, doesn't overlay content -->
|
||||
<div class="md:hidden app-session-mobile-bar">
|
||||
<!-- Mobile: gamepad for botfights, browser bar for everything else -->
|
||||
<MobileGamepad
|
||||
v-if="isMobile && appId === 'botfights'"
|
||||
:iframe-ref="iframeRef ?? null"
|
||||
:player="1"
|
||||
/>
|
||||
<div v-else class="md:hidden app-session-mobile-bar">
|
||||
<button class="app-session-bar-btn" aria-label="Back" @click="iframeGoBack">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
@@ -81,6 +86,7 @@ import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
import NostrIdentityPicker from '@/components/NostrIdentityPicker.vue'
|
||||
import AppSessionHeader from './appSession/AppSessionHeader.vue'
|
||||
import AppSessionFrame from './appSession/AppSessionFrame.vue'
|
||||
import MobileGamepad from './appSession/MobileGamepad.vue'
|
||||
import {
|
||||
type DisplayMode, DISPLAY_MODE_KEY, NEW_TAB_APPS, IFRAME_BLOCKED_APPS,
|
||||
resolveAppUrl, resolveAppTitle,
|
||||
|
||||
216
neode-ui/src/views/appSession/MobileGamepad.vue
Normal file
216
neode-ui/src/views/appSession/MobileGamepad.vue
Normal file
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<!-- Mobile gamepad overlay — NES-styled D-pad + action buttons.
|
||||
Sends postMessage({ type: 'arcade-input', key, player, action }) to iframe. -->
|
||||
<div class="mobile-gamepad">
|
||||
<!-- D-Pad (left side) -->
|
||||
<div class="gamepad-dpad">
|
||||
<button
|
||||
class="dpad-btn dpad-up"
|
||||
@touchstart.prevent="down('ArrowUp')"
|
||||
@touchend.prevent="up('ArrowUp')"
|
||||
@touchcancel.prevent="up('ArrowUp')"
|
||||
aria-label="Up"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4l-6 8h12z"/></svg>
|
||||
</button>
|
||||
<button
|
||||
class="dpad-btn dpad-left"
|
||||
@touchstart.prevent="down('ArrowLeft')"
|
||||
@touchend.prevent="up('ArrowLeft')"
|
||||
@touchcancel.prevent="up('ArrowLeft')"
|
||||
aria-label="Left"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 12l8-6v12z"/></svg>
|
||||
</button>
|
||||
<div class="dpad-center" />
|
||||
<button
|
||||
class="dpad-btn dpad-right"
|
||||
@touchstart.prevent="down('ArrowRight')"
|
||||
@touchend.prevent="up('ArrowRight')"
|
||||
@touchcancel.prevent="up('ArrowRight')"
|
||||
aria-label="Right"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20 12l-8-6v12z"/></svg>
|
||||
</button>
|
||||
<button
|
||||
class="dpad-btn dpad-down"
|
||||
@touchstart.prevent="down('ArrowDown')"
|
||||
@touchend.prevent="up('ArrowDown')"
|
||||
@touchcancel.prevent="up('ArrowDown')"
|
||||
aria-label="Down"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 20l6-8H6z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Center: START / SELECT -->
|
||||
<div class="gamepad-meta">
|
||||
<button
|
||||
class="meta-btn"
|
||||
@touchstart.prevent="tap('Escape')"
|
||||
aria-label="Select"
|
||||
>SEL</button>
|
||||
<button
|
||||
class="meta-btn"
|
||||
@touchstart.prevent="tap('Enter')"
|
||||
aria-label="Start"
|
||||
>START</button>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons (right side) -->
|
||||
<div class="gamepad-actions">
|
||||
<button
|
||||
class="action-btn action-b"
|
||||
@touchstart.prevent="down('b')"
|
||||
@touchend.prevent="up('b')"
|
||||
@touchcancel.prevent="up('b')"
|
||||
aria-label="Kick"
|
||||
>B</button>
|
||||
<button
|
||||
class="action-btn action-a"
|
||||
@touchstart.prevent="down('a')"
|
||||
@touchend.prevent="up('a')"
|
||||
@touchcancel.prevent="up('a')"
|
||||
aria-label="Punch"
|
||||
>A</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
iframeRef: HTMLIFrameElement | null
|
||||
player?: number
|
||||
}>()
|
||||
|
||||
function send(key: string, action: 'down' | 'up') {
|
||||
props.iframeRef?.contentWindow?.postMessage(
|
||||
{ type: 'arcade-input', key, player: props.player ?? 1, action },
|
||||
'*'
|
||||
)
|
||||
}
|
||||
|
||||
function down(key: string) { send(key, 'down') }
|
||||
function up(key: string) { send(key, 'up') }
|
||||
function tap(key: string) { send(key, 'down'); setTimeout(() => send(key, 'up'), 80) }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-gamepad {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
padding: 12px 20px;
|
||||
padding-bottom: calc(12px + var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)));
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
/* ── D-Pad ── */
|
||||
.gamepad-dpad {
|
||||
display: grid;
|
||||
grid-template-columns: 48px 48px 48px;
|
||||
grid-template-rows: 48px 48px 48px;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dpad-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.dpad-btn:active {
|
||||
background: rgba(251, 146, 60, 0.3);
|
||||
color: white;
|
||||
}
|
||||
.dpad-btn svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.dpad-up { grid-column: 2; grid-row: 1; }
|
||||
.dpad-left { grid-column: 1; grid-row: 2; }
|
||||
.dpad-center {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.dpad-right { grid-column: 3; grid-row: 2; }
|
||||
.dpad-down { grid-column: 2; grid-row: 3; }
|
||||
|
||||
/* ── Meta buttons (START / SELECT) ── */
|
||||
.gamepad-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-btn {
|
||||
padding: 6px 16px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.meta-btn:active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* ── Action buttons (A / B) ── */
|
||||
.gamepad-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid;
|
||||
transition: background 0.1s, transform 0.1s;
|
||||
}
|
||||
.action-btn:active {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
.action-a {
|
||||
background: rgba(251, 146, 60, 0.2);
|
||||
border-color: rgba(251, 146, 60, 0.5);
|
||||
color: #fb923c;
|
||||
}
|
||||
.action-a:active {
|
||||
background: rgba(251, 146, 60, 0.45);
|
||||
}
|
||||
|
||||
.action-b {
|
||||
background: rgba(96, 165, 250, 0.2);
|
||||
border-color: rgba(96, 165, 250, 0.5);
|
||||
color: #60a5fa;
|
||||
}
|
||||
.action-b:active {
|
||||
background: rgba(96, 165, 250, 0.45);
|
||||
}
|
||||
</style>
|
||||
@@ -41,14 +41,14 @@ export const APP_PORTS: Record<string, number> = {
|
||||
'nostr-vpn': 8201,
|
||||
'fips': 8202,
|
||||
'routstr': 8200,
|
||||
'indeedhub': 7777,
|
||||
'indeedhub': 7778,
|
||||
'botfights': 9100,
|
||||
'dwn': 3100,
|
||||
'endurain': 8080,
|
||||
}
|
||||
|
||||
/** Apps that need nginx proxy for iframe embedding.
|
||||
* IndeedHub loads via direct port 7777 -- deploy script removes X-Frame-Options
|
||||
* IndeedHub loads via /app/indeedhub/ proxy for nostr-provider.js injection
|
||||
* from the container's internal nginx so iframe works on all servers. */
|
||||
export const PROXY_APPS: Record<string, string> = {}
|
||||
|
||||
@@ -87,6 +87,7 @@ export const HTTPS_PROXY_PATHS: Record<string, string> = {
|
||||
'penpot': '/app/penpot/',
|
||||
'grafana': '/app/grafana/',
|
||||
'indeedhub': '/app/indeedhub/',
|
||||
'botfights': '/app/botfights/',
|
||||
'routstr': '/app/routstr/',
|
||||
'nostr-vpn': '/app/nostr-vpn/',
|
||||
'fips': '/app/fips/',
|
||||
@@ -143,7 +144,7 @@ export function resolveAppUrl(id: string, routeQueryPath?: string): string {
|
||||
const proxyPath = PROXY_APPS[id]
|
||||
if (proxyPath) return `${window.location.origin}${proxyPath}`
|
||||
|
||||
// IndeedHub: always direct port (X-Frame-Options removed by deploy script)
|
||||
// IndeedHub: direct port access (nostr-provider.js baked into container image)
|
||||
if (id === 'indeedhub') {
|
||||
const port = APP_PORTS[id]
|
||||
if (port) {
|
||||
|
||||
@@ -27,7 +27,7 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
||||
{ id: 'electrumx', title: 'ElectrumX', version: '1.18.0', description: 'Electrum protocol server. Index the blockchain for fast wallet lookups, privately.', icon: '/assets/img/app-icons/electrumx.webp', author: 'Luke Childs', dockerImage: `${R}/electrumx:v1.18.0`, repoUrl: 'https://github.com/spesmilo/electrumx' },
|
||||
{ id: 'fedimint', title: 'Fedimint', version: '0.10.0', description: 'Federated Bitcoin mint. Private, scalable Bitcoin through federated guardians.', icon: '/assets/img/app-icons/fedimint.png', author: 'Fedimint', dockerImage: `${R}/fedimintd:v0.10.0`, repoUrl: 'https://github.com/fedimint/fedimint' },
|
||||
{ id: 'nostr-rs-relay', title: 'Nostr Relay', version: '0.9.0', category: 'nostr', description: 'Your own Nostr relay. Store events locally, relay for friends, publish over Tor.', icon: '/assets/img/app-icons/nostr-rs-relay.svg', author: 'scsiblade', dockerImage: `${R}/nostr-rs-relay:0.9.0`, repoUrl: 'https://sr.ht/~gheartsfield/nostr-rs-relay/' },
|
||||
{ id: 'indeedhub', title: 'Indeehub', version: '0.1.0', description: 'Bitcoin documentary streaming with Nostr identity. Stream sovereignty content.', icon: '/assets/img/app-icons/indeedhub.png', author: 'Indeehub Team', dockerImage: 'localhost/indeedhub:latest', repoUrl: 'https://github.com/indeedhub/indeedhub' },
|
||||
{ id: 'indeedhub', title: 'Indeehub', version: '0.1.0', description: 'Bitcoin documentary streaming with Nostr identity. Stream sovereignty content.', icon: '/assets/img/app-icons/indeedhub.png', author: 'Indeehub Team', dockerImage: 'git.tx1138.com/lfg2025/indeedhub:latest', repoUrl: 'https://github.com/indeedhub/indeedhub' },
|
||||
{ id: 'dwn', title: 'Decentralized Web Node', version: '0.4.0', description: 'Own your data with DID-based access control. Sync across devices, sovereign.', icon: '/assets/img/app-icons/dwn.svg', author: 'TBD', dockerImage: `${R}/dwn-server:main`, repoUrl: 'https://github.com/TBD54566975/dwn-server' },
|
||||
{ id: 'nostr-vpn', title: 'Nostr VPN', version: '0.3.7', category: 'networking', description: 'Tailscale-style mesh VPN with Nostr control plane. Peer discovery and key exchange over relays, WireGuard tunnels.', icon: '/assets/img/app-icons/nostr-vpn.svg', author: 'Martti Malmi', dockerImage: `${R}/nostr-vpn:v0.3.7`, repoUrl: 'https://github.com/mmalmi/nostr-vpn' },
|
||||
{ id: 'fips', title: 'FIPS', version: '0.1.0', category: 'networking', description: 'Free Internetworking Peering System. Self-organizing encrypted mesh network with Nostr identity.', icon: '/assets/img/app-icons/fips.svg', author: 'Jim Corgan', dockerImage: `${R}/fips:v0.1.0`, repoUrl: 'https://github.com/jmcorgan/fips' },
|
||||
|
||||
@@ -390,7 +390,7 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
||||
description: 'Bitcoin documentary streaming platform with Nostr identity sign-in. Stream God Bless Bitcoin and other educational content about sovereignty and decentralized technology.',
|
||||
icon: '/assets/img/app-icons/indeedhub.png',
|
||||
author: 'Indeehub Team',
|
||||
dockerImage: 'localhost/indeedhub:latest',
|
||||
dockerImage: 'git.tx1138.com/lfg2025/indeedhub:latest',
|
||||
manifestUrl: undefined,
|
||||
repoUrl: 'https://github.com/indeedhub/indeedhub'
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user