diff --git a/src/views/FlexibleGenerationView.vue b/src/views/FlexibleGenerationView.vue index 8a3c452..1ea7012 100644 --- a/src/views/FlexibleGenerationView.vue +++ b/src/views/FlexibleGenerationView.vue @@ -1027,7 +1027,7 @@ const confirmAddToAlbum = async () => { {{ item.status - }}... + }}... {{ item.progress }}% @@ -1088,7 +1088,7 @@ const confirmAddToAlbum = async () => { @click.stop="reuseAsset(item)" />
{{ item.prompt - }}
+ }} @@ -1149,9 +1149,8 @@ const confirmAddToAlbum = async () => { @click="clearPrompt" /> - + diff --git a/src/views/IdeaDetailView.vue b/src/views/IdeaDetailView.vue index 13773d0..4906432 100644 --- a/src/views/IdeaDetailView.vue +++ b/src/views/IdeaDetailView.vue @@ -674,16 +674,47 @@ const downloadSelected = async () => { const projectId = localStorage.getItem('active_project_id') if (projectId) headers['X-Project-ID'] = projectId - // Multi-file ZIP (if > 1) + const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) || navigator.maxTouchPoints > 1 + const canShare = isMobile && navigator.canShare && navigator.share + + if (canShare) { + // Attempt to fetch all files and share + try { + const files = [] + for (const assetId of ids) { + const url = API_URL + '/assets/' + assetId + const resp = await fetch(url, { headers }) + const blob = await resp.blob() + const mime = blob.type || 'image/png' + const ext = mime.split('/')[1] || 'png' + files.push(new File([blob], `image-${assetId}.${ext}`, { type: mime })) + } + + if (navigator.canShare({ files })) { + await navigator.share({ files }) + toast.add({ severity: 'success', summary: 'Shared', detail: `${files.length} images shared`, life: 2000 }) + return // Success, exit + } + } catch (shareError) { + console.warn('Share failed or canceled, falling back to zip if multiple', shareError) + // Fallthrough to zip/single download logic if share fails (e.g. user cancelled or limit reached) + // actually if user canceled, we probably shouldn't zip. But hard to distinguish. + // If it was a real error, zip might be better. + if (shareError.name !== 'AbortError' && ids.length > 1) { + toast.add({ severity: 'info', summary: 'Sharing failed', detail: 'Creating zip archive instead...', life: 2000 }) + } else { + return // Stop if just cancelled + } + } + } + + // Fallback: ZIP for multiple, Direct for single (if sharing skipped/failed) if (ids.length > 1) { const zip = new JSZip() - // Sanitize idea name for filename (fallback to 'session' if no name) const safeName = (currentIdea.value?.name || 'session').replace(/[^a-z0-9_\- ]/gi, '').trim().replace(/\s+/g, '_').toLowerCase() const folderName = `${safeName}_assets` - let successCount = 0 - // Sequential fetch to avoid overwhelming network but could be parallel await Promise.all(ids.map(async (assetId) => { try { const url = API_URL + '/assets/' + assetId @@ -715,26 +746,21 @@ const downloadSelected = async () => { toast.add({ severity: 'success', summary: 'Archived', detail: `${successCount} images saved to zip`, life: 3000 }) } else { - // Single File + // Single File Download (Desktop or Mobile Fallback) const assetId = ids[0] const url = API_URL + '/assets/' + assetId const resp = await fetch(url, { headers }) const blob = await resp.blob() - - // Use share sheet only on mobile, direct download on desktop const file = new File([blob], assetId + '.png', { type: blob.type || 'image/png' }) - const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) || navigator.maxTouchPoints > 1 - if (isMobile && navigator.canShare && navigator.canShare({ files: [file] })) { - await navigator.share({ files: [file] }) - } else { - const a = document.createElement('a') - a.href = URL.createObjectURL(file) - a.download = file.name - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - URL.revokeObjectURL(a.href) - } + + const a = document.createElement('a') + a.href = URL.createObjectURL(file) + a.download = file.name + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(a.href) + toast.add({ severity: 'success', summary: 'Downloaded', detail: `Image saved`, life: 2000 }) } } catch (e) { @@ -1055,9 +1081,8 @@ watch(viewMode, (v) => { @click="clearPrompt" /> - +