fix(content): audio always plays in the bottom-bar player + ecash purchase is explicit two-step
- Owned/purchased and cloud audio never opens a lightbox: PeerFiles' owned-content viewer and the Cloud/CloudFolder preview route audio to the global bottom-bar player (video keeps the lightbox). - Web5 shared-content 'Buy' no longer fires the node-ecash payment on click: it opens a confirmation with balance -> price -> balance-after, and pays only on explicit confirm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6502f13e29
commit
960e41e164
@ -638,6 +638,15 @@ async function handlePlay(path: string, name: string) {
|
||||
}
|
||||
|
||||
function handlePreview(path: string, context: FileBrowserItem[]) {
|
||||
// Audio never opens the lightbox — it belongs to the bottom-bar player.
|
||||
const clicked = context.find(item => item.path === path)
|
||||
if (clicked) {
|
||||
const ext = clicked.name.includes('.') ? clicked.name.split('.').pop()!.toLowerCase() : ''
|
||||
if (getFileCategory(ext, clicked.isDir) === 'audio') {
|
||||
void handlePlay(path, clicked.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
// MediaLightbox filters to media internally; index within that filtered list.
|
||||
const mediaItems = context.filter(item => {
|
||||
const ext = item.name.includes('.') ? item.name.split('.').pop()!.toLowerCase() : ''
|
||||
|
||||
@ -323,9 +323,18 @@ const shareTarget = ref<{ path: string; name: string; isDir: boolean } | null>(n
|
||||
const lightboxIndex = ref<number | null>(null)
|
||||
|
||||
function handlePreview(path: string) {
|
||||
const items = cloudStore.sortedItems
|
||||
// Audio never opens the lightbox — it belongs to the bottom-bar player.
|
||||
const clicked = items.find(item => item.path === path)
|
||||
if (clicked) {
|
||||
const ext = clicked.name.includes('.') ? clicked.name.split('.').pop()!.toLowerCase() : ''
|
||||
if (getFileCategory(ext, clicked.isDir) === 'audio') {
|
||||
void handlePlay(path, clicked.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
// MediaLightbox internally filters items to media only, so startIndex
|
||||
// must be the index within that filtered list
|
||||
const items = cloudStore.sortedItems
|
||||
const mediaItems = items.filter(item => {
|
||||
const ext = item.name.includes('.') ? item.name.split('.').pop()!.toLowerCase() : ''
|
||||
const cat = getFileCategory(ext, item.isDir)
|
||||
|
||||
@ -674,6 +674,13 @@ async function viewOwned(item: CatalogItem) {
|
||||
})
|
||||
if (!res?.data) { purchaseError.value = res?.error || 'Could not open your purchased file'; return }
|
||||
const mime = res.mime_type || item.mime_type
|
||||
// Audio always plays in the global bottom-bar player — never the lightbox
|
||||
// (the blob URL is intentionally not revoked while the bar plays it).
|
||||
if (mime.startsWith('audio/')) {
|
||||
const url = URL.createObjectURL(base64ToBlob(res.data, mime))
|
||||
audioPlayer.play(url, item.filename.split('/').pop() || item.filename)
|
||||
return
|
||||
}
|
||||
if (viewerUrl.value) URL.revokeObjectURL(viewerUrl.value)
|
||||
viewerUrl.value = URL.createObjectURL(base64ToBlob(res.data, mime))
|
||||
viewerMime.value = mime
|
||||
|
||||
@ -235,7 +235,7 @@
|
||||
</button>
|
||||
<button
|
||||
v-else-if="getItemPrice(pItem.access) > 0"
|
||||
@click="purchaseAndDownload(pItem)"
|
||||
@click="requestPurchase(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"
|
||||
>
|
||||
@ -305,6 +305,41 @@
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Purchase confirmation — payment NEVER starts before the user confirms -->
|
||||
<Teleport to="body">
|
||||
<div v-if="pendingPurchase" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="pendingPurchase = null">
|
||||
<div class="glass-card p-6 w-full max-w-sm mx-4" role="dialog" aria-modal="true">
|
||||
<h3 class="text-base font-semibold text-white mb-1">Buy this file</h3>
|
||||
<p class="text-sm text-white/70 truncate mb-4">{{ pendingPurchase.filename.split('/').pop() }}</p>
|
||||
<div class="p-3 bg-white/5 rounded-lg space-y-1.5 mb-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-white/50">Ecash balance</span>
|
||||
<span class="text-sm font-medium text-white/80">{{ pendingBalance === null ? '…' : pendingBalance.toLocaleString() + ' sats' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-white/50">Price</span>
|
||||
<span class="text-sm font-medium text-white/80">−{{ getItemPrice(pendingPurchase.access).toLocaleString() }} sats</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-white/50">Balance after</span>
|
||||
<span class="text-sm font-medium" :class="pendingBalance !== null && pendingBalance < getItemPrice(pendingPurchase.access) ? 'text-red-400' : 'text-white/80'">
|
||||
{{ pendingBalance === null ? '…' : (pendingBalance - getItemPrice(pendingPurchase.access)).toLocaleString() + ' sats' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="pendingBalance !== null && pendingBalance < getItemPrice(pendingPurchase.access)" class="text-xs text-red-400 mb-3">Not enough ecash — fund your wallet, or buy from the Peers page to pay another way.</p>
|
||||
<div class="flex gap-3">
|
||||
<button @click="pendingPurchase = null" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm">Cancel</button>
|
||||
<button
|
||||
@click="confirmPendingPurchase"
|
||||
:disabled="pendingBalance !== null && pendingBalance < getItemPrice(pendingPurchase.access)"
|
||||
class="flex-1 glass-button glass-button-warning px-4 py-2 rounded-lg text-sm font-medium disabled:opacity-50"
|
||||
>Pay from node ecash</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Add Content Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showAddContentModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="showAddContentModal = false" @keydown.escape="showAddContentModal = false">
|
||||
@ -550,6 +585,26 @@ function downloadPeerContent(item: PeerContentItem) {
|
||||
safeClipboardWrite(url)
|
||||
}
|
||||
|
||||
// Purchase is strictly two-step: the Buy click only opens the confirmation
|
||||
// (with the balance impact); nothing is paid until the user confirms there.
|
||||
const pendingPurchase = ref<PeerContentItem | null>(null)
|
||||
const pendingBalance = ref<number | null>(null)
|
||||
|
||||
function requestPurchase(item: PeerContentItem) {
|
||||
if (purchasingId.value) return
|
||||
pendingPurchase.value = item
|
||||
pendingBalance.value = null
|
||||
rpcClient.call<{ balance_sats?: number }>({ method: 'wallet.ecash-balance' })
|
||||
.then((r) => { pendingBalance.value = r?.balance_sats ?? 0 })
|
||||
.catch(() => { pendingBalance.value = null })
|
||||
}
|
||||
|
||||
async function confirmPendingPurchase() {
|
||||
const item = pendingPurchase.value
|
||||
pendingPurchase.value = null
|
||||
if (item) await purchaseAndDownload(item)
|
||||
}
|
||||
|
||||
async function purchaseAndDownload(item: PeerContentItem) {
|
||||
if (!browsePeerOnion.value || purchasingId.value) return
|
||||
const price = getItemPrice(item.access)
|
||||
@ -557,18 +612,6 @@ async function purchaseAndDownload(item: PeerContentItem) {
|
||||
|
||||
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 },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user