fix(01-14): route Paid Files pictures/videos into the app lightbox with a visible wait (UIFIX-04/UIFIX-06)
Demo images / Build & push demo images (push) Successful in 3m47s
Demo images / Build & push demo images (push) Successful in 3m47s
Cloud.vue's viewPaidItem() called window.open() instead of the in-app MediaLightbox, and its content.owned-get fetch had a 60s timeout with no loading indicator and a swallowed catch. Moves the fetch/decode/route logic into a new usePaidItemViewer composable: image/video route to a second MediaLightbox instance fed a synthetic FileBrowserItem, audio still goes to the global bottom-bar player, and anything with no in-app viewer keeps today's browser-tab fallback. The Paid Files row now shows an "Opening…" spinner (matching PeerFiles' existing treatment) for the fetch's duration, becomes non-interactive to prevent double-fetch, and a real error surfaces through the view's existing alert-error block instead of an empty catch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3288a02df8
commit
bc9a210c75
@@ -161,8 +161,9 @@
|
||||
<div
|
||||
v-for="it in paidItems"
|
||||
:key="it.onion + it.content_id"
|
||||
class="glass-card p-3 flex items-center gap-3 cursor-pointer hover:bg-white/5 transition-colors"
|
||||
@click="viewPaidItem(it)"
|
||||
class="glass-card p-3 flex items-center gap-3 transition-colors"
|
||||
:class="paidItemViewer.opening.value === paidItemKey(it) ? 'cursor-default' : 'cursor-pointer hover:bg-white/5'"
|
||||
@click="paidItemViewer.opening.value === paidItemKey(it) ? null : viewPaidItem(it)"
|
||||
>
|
||||
<span class="text-xl shrink-0">{{ it.mime_type.startsWith('image/') ? '🖼️' : it.mime_type.startsWith('video/') ? '🎬' : it.mime_type.startsWith('audio/') ? '🎵' : '📄' }}</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
@@ -173,7 +174,14 @@
|
||||
<span v-if="it.purchased_at"> · {{ new Date(it.purchased_at).toLocaleDateString() }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-[10px] px-2 py-0.5 rounded-full bg-emerald-400/15 text-emerald-300 shrink-0">Paid</span>
|
||||
<span
|
||||
v-if="paidItemViewer.opening.value === paidItemKey(it)"
|
||||
class="text-[10px] px-2 py-0.5 rounded-full bg-white/10 text-white/60 shrink-0 flex items-center gap-1.5"
|
||||
>
|
||||
<span class="w-3 h-3 border-2 border-white/20 border-t-white/80 rounded-full animate-spin"></span>
|
||||
Opening…
|
||||
</span>
|
||||
<span v-else class="text-[10px] px-2 py-0.5 rounded-full bg-emerald-400/15 text-emerald-300 shrink-0">Paid</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -398,6 +406,18 @@
|
||||
:stream-url="cloudStore.streamUrl"
|
||||
@close="lightboxIndex = null"
|
||||
/>
|
||||
|
||||
<!-- Media viewer for purchased items (UIFIX-04) — synthetic items fed by
|
||||
usePaidItemViewer, resolved from the already-fetched blob URL. -->
|
||||
<MediaLightbox
|
||||
v-if="paidItemViewer.lightboxIndex.value !== null"
|
||||
:items="paidItemViewer.lightboxItems.value"
|
||||
:start-index="paidItemViewer.lightboxIndex.value"
|
||||
:show="paidItemViewer.lightboxIndex.value !== null"
|
||||
:fetch-blob-url="paidItemViewer.resolveBlobUrl"
|
||||
:stream-url="paidItemViewer.resolveBlobUrl"
|
||||
@close="paidItemViewer.closeLightbox()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -412,6 +432,7 @@ import { fileBrowserClient, type FileBrowserItem } from '@/api/filebrowser-clien
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { getFileCategory } from '../composables/useFileType'
|
||||
import { useAudioPlayer } from '../composables/useAudioPlayer'
|
||||
import { usePaidItemViewer, paidItemKey } from '../composables/usePaidItemViewer'
|
||||
import FileCard from '../components/cloud/FileCard.vue'
|
||||
import ShareModal from '../components/cloud/ShareModal.vue'
|
||||
import MediaLightbox from '../components/cloud/MediaLightbox.vue'
|
||||
@@ -469,29 +490,13 @@ const paidLoading = computed(() => paidResource.entry.loadState === 'loading')
|
||||
function loadPaidItems() {
|
||||
return paidResource.refresh()
|
||||
}
|
||||
// Fetch, decode, route-to-viewer state for a purchased item (UIFIX-04
|
||||
// lightbox routing + UIFIX-06 loading/error surfacing). See the composable
|
||||
// for the mime-routing rules and URL-ownership contract.
|
||||
const paidItemViewer = usePaidItemViewer()
|
||||
async function viewPaidItem(it: PaidItem) {
|
||||
try {
|
||||
const res = await rpcClient.call<{ data_base64?: string; data?: string; mime_type?: string }>({
|
||||
method: 'content.owned-get',
|
||||
params: { onion: it.onion, content_id: it.content_id },
|
||||
timeout: 60000,
|
||||
})
|
||||
const b64 = res.data_base64 || res.data
|
||||
if (!b64) return
|
||||
const bin = atob(b64)
|
||||
const arr = new Uint8Array(bin.length)
|
||||
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i)
|
||||
const mime = res.mime_type || it.mime_type
|
||||
const url = URL.createObjectURL(new Blob([arr], { type: mime }))
|
||||
// Music ALWAYS plays in the global bottom-bar player — never a popup/
|
||||
// lightbox (blob URL stays alive for the bar; it owns playback now).
|
||||
if (mime.startsWith('audio/')) {
|
||||
audioPlayer.play(url, it.filename.split('/').pop() || it.filename)
|
||||
return
|
||||
}
|
||||
window.open(url, '_blank', 'noopener')
|
||||
setTimeout(() => URL.revokeObjectURL(url), 60000)
|
||||
} catch { /* viewer is best-effort; the file is also in the user's folders */ }
|
||||
await paidItemViewer.open(it)
|
||||
if (paidItemViewer.error.value) loadError.value = paidItemViewer.error.value
|
||||
}
|
||||
watch(activeTab, (t) => { if (t === 'paid') void loadPaidItems() })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user