feat: botfights, discover, mobile gamepad, content handler, package config updates

Miscellaneous improvements: botfights manifest, discover page curated
apps, mobile gamepad enhancements, content HTTP handler, package
install config updates, health monitor tweaks, shared content UI,
container specs and image version updates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-11 23:11:41 -04:00
co-authored by Claude Opus 4.6
parent 24f122f35a
commit bb14490fb7
23 changed files with 782 additions and 75 deletions
+68 -1
View File
@@ -213,12 +213,29 @@
</div>
</div>
<button
v-if="isMediaType(pItem.mime_type)"
v-if="isMediaType(pItem.mime_type) && getItemPrice(pItem.access) === 0"
@click="streamPeerContent(pItem)"
class="px-3 py-1.5 text-xs rounded-lg bg-orange-500/20 text-orange-400 hover:bg-orange-500/30 transition-colors shrink-0"
>
{{ t('web5.stream') }}
</button>
<button
v-else-if="getItemPrice(pItem.access) > 0"
@click="purchaseAndDownload(pItem)"
:disabled="purchasingId === pItem.id"
class="px-3 py-1.5 text-xs rounded-lg bg-orange-500/20 text-orange-400 hover:bg-orange-500/30 transition-colors shrink-0 flex items-center gap-1"
>
<template v-if="purchasingId === pItem.id">
<div class="w-3 h-3 border-2 border-orange-400/30 border-t-orange-400 rounded-full animate-spin"></div>
Paying...
</template>
<template v-else>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
Buy {{ getItemPrice(pItem.access) }} sats
</template>
</button>
<button
v-else
@click="downloadPeerContent(pItem)"
@@ -370,6 +387,9 @@ const browsingPeerContent = ref(false)
const browsePeerError = ref('')
const peerContentItems = ref<PeerContentItem[]>([])
// Purchase flow
const purchasingId = ref<string | null>(null)
// Streaming player
const streamingItem = ref<PeerContentItem | null>(null)
const streamUrl = ref('')
@@ -513,6 +533,53 @@ function downloadPeerContent(item: PeerContentItem) {
safeClipboardWrite(url)
}
async function purchaseAndDownload(item: PeerContentItem) {
if (!browsePeerOnion.value || purchasingId.value) return
const price = getItemPrice(item.access)
if (price <= 0) return
purchasingId.value = item.id
try {
// Check balance first
try {
const balRes = await rpcClient.call<{ balance_sats?: number }>({ method: 'wallet.ecash-balance' })
const balance = balRes?.balance_sats ?? 0
if (balance < price) {
emit('toast', `Insufficient ecash balance (${balance} sats). Need ${price} sats.`)
return
}
} catch {
// Balance check failed — try the purchase anyway
}
const result = await rpcClient.call<{ data?: string; error?: string }>({
method: 'content.download-peer-paid',
params: { onion: browsePeerOnion.value, content_id: item.id, price_sats: price },
timeout: 120000,
})
if (result?.data) {
const blob = new Blob(
[Uint8Array.from(atob(result.data), c => c.charCodeAt(0))],
{ type: item.mime_type },
)
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = item.filename.split('/').pop() || item.filename
a.click()
URL.revokeObjectURL(url)
emit('toast', `Downloaded for ${price} sats`)
} else {
emit('toast', 'Purchase failed — no data received')
}
} catch (e: unknown) {
emit('toast', e instanceof Error ? e.message : 'Purchase failed')
} finally {
purchasingId.value = null
}
}
function closePlayer() {
if (audioPlayerRef.value) {
audioPlayerRef.value.pause()