feat: Implement Web Share API for mobile image sharing and standardize prompt textarea heights across views.
This commit is contained in:
@@ -1149,9 +1149,8 @@ const confirmAddToAlbum = async () => {
|
||||
@click="clearPrompt" />
|
||||
</div>
|
||||
</div>
|
||||
<Textarea v-model="prompt" rows="3" autoResize
|
||||
placeholder="Describe what you want to create..."
|
||||
class="w-full bg-slate-800 !text-[16px] border-white/10 text-white rounded-xl p-3 focus:border-violet-500 focus:ring-1 focus:ring-violet-500/50 transition-all resize-none shadow-inner" />
|
||||
<Textarea v-model="prompt" rows="2" placeholder="Describe what you want to create..."
|
||||
class="w-full bg-slate-800 !h-28 !text-[16px] border-white/10 text-white rounded-xl p-3 focus:border-violet-500 focus:ring-1 focus:ring-violet-500/50 transition-all resize-none shadow-inner" />
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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,18 +746,13 @@ 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
|
||||
@@ -734,7 +760,7 @@ const downloadSelected = async () => {
|
||||
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" />
|
||||
</div>
|
||||
</div>
|
||||
<Textarea v-model="prompt" rows="2" autoResize
|
||||
placeholder="Describe what you want to create..."
|
||||
class="w-full 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" />
|
||||
<Textarea v-model="prompt" rows="2" placeholder="Describe what you want to create..."
|
||||
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" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-2">
|
||||
|
||||
@@ -331,9 +331,9 @@ const handleAssetPickerUpload = async (event) => {
|
||||
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Prompt</label>
|
||||
<!-- Optional: Add Clear/Improve buttons here if desired, keeping simple for now to match layout -->
|
||||
</div>
|
||||
<Textarea v-model="prompt" rows="2" autoResize
|
||||
<Textarea v-model="prompt" rows="2"
|
||||
placeholder="Describe what you want to create... (Auto-starts new session)"
|
||||
class="w-full 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" />
|
||||
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" />
|
||||
</div>
|
||||
|
||||
<!-- Character & Assets Row -->
|
||||
|
||||
Reference in New Issue
Block a user