From f8adcf33d3f4486ea0a1bd390e89fdc30ace3779 Mon Sep 17 00:00:00 2001
From: xds
Date: Tue, 17 Feb 2026 18:17:00 +0300
Subject: [PATCH] feat: Implement Web Share API for mobile image sharing and
standardize prompt textarea heights across views.
---
src/views/FlexibleGenerationView.vue | 9 ++--
src/views/IdeaDetailView.vue | 69 +++++++++++++++++++---------
src/views/IdeasView.vue | 4 +-
3 files changed, 53 insertions(+), 29 deletions(-)
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" />
-
+
diff --git a/src/views/IdeasView.vue b/src/views/IdeasView.vue
index 212f6b0..fb86a2b 100644
--- a/src/views/IdeasView.vue
+++ b/src/views/IdeasView.vue
@@ -331,9 +331,9 @@ const handleAssetPickerUpload = async (event) => {
-
+ class="w-full !h-28 bg-slate-800 !text-sm border-white/10 text-white rounded-lg p-2 focus:border-violet-500 focus:ring-1 focus:ring-violet-500/50 transition-all resize-none shadow-inner" />