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[]) {
|
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.
|
// MediaLightbox filters to media internally; index within that filtered list.
|
||||||
const mediaItems = context.filter(item => {
|
const mediaItems = context.filter(item => {
|
||||||
const ext = item.name.includes('.') ? item.name.split('.').pop()!.toLowerCase() : ''
|
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)
|
const lightboxIndex = ref<number | null>(null)
|
||||||
|
|
||||||
function handlePreview(path: string) {
|
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
|
// MediaLightbox internally filters items to media only, so startIndex
|
||||||
// must be the index within that filtered list
|
// must be the index within that filtered list
|
||||||
const items = cloudStore.sortedItems
|
|
||||||
const mediaItems = items.filter(item => {
|
const mediaItems = items.filter(item => {
|
||||||
const ext = item.name.includes('.') ? item.name.split('.').pop()!.toLowerCase() : ''
|
const ext = item.name.includes('.') ? item.name.split('.').pop()!.toLowerCase() : ''
|
||||||
const cat = getFileCategory(ext, item.isDir)
|
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 }
|
if (!res?.data) { purchaseError.value = res?.error || 'Could not open your purchased file'; return }
|
||||||
const mime = res.mime_type || item.mime_type
|
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)
|
if (viewerUrl.value) URL.revokeObjectURL(viewerUrl.value)
|
||||||
viewerUrl.value = URL.createObjectURL(base64ToBlob(res.data, mime))
|
viewerUrl.value = URL.createObjectURL(base64ToBlob(res.data, mime))
|
||||||
viewerMime.value = mime
|
viewerMime.value = mime
|
||||||
|
|||||||
@ -235,7 +235,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else-if="getItemPrice(pItem.access) > 0"
|
v-else-if="getItemPrice(pItem.access) > 0"
|
||||||
@click="purchaseAndDownload(pItem)"
|
@click="requestPurchase(pItem)"
|
||||||
:disabled="purchasingId === pItem.id"
|
: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"
|
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>
|
</div>
|
||||||
</Teleport>
|
</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 -->
|
<!-- Add Content Modal -->
|
||||||
<Teleport to="body">
|
<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">
|
<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)
|
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) {
|
async function purchaseAndDownload(item: PeerContentItem) {
|
||||||
if (!browsePeerOnion.value || purchasingId.value) return
|
if (!browsePeerOnion.value || purchasingId.value) return
|
||||||
const price = getItemPrice(item.access)
|
const price = getItemPrice(item.access)
|
||||||
@ -557,18 +612,6 @@ async function purchaseAndDownload(item: PeerContentItem) {
|
|||||||
|
|
||||||
purchasingId.value = item.id
|
purchasingId.value = item.id
|
||||||
try {
|
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 }>({
|
const result = await rpcClient.call<{ data?: string; error?: string }>({
|
||||||
method: 'content.download-peer-paid',
|
method: 'content.download-peer-paid',
|
||||||
params: { onion: browsePeerOnion.value, content_id: item.id, price_sats: price },
|
params: { onion: browsePeerOnion.value, content_id: item.id, price_sats: price },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user