Implementar Web Share Target API para compartir fotos con la PWA
Some checks failed
build-and-deploy / build-and-deploy (push) Has been cancelled

- Agregar share_target al manifest de la PWA
- Crear endpoint /api/share-target para recibir archivos compartidos
- Guardar archivos temporalmente en /public/temp-shared
- Modificar UserProfileForm para aceptar imágenes externas
- Detectar automáticamente imágenes compartidas y procesarlas
- Crear endpoint /api/share-target/cleanup para limpiar temporales
- Mostrar toast informativo al recibir imagen compartida
- Redirigir automáticamente al formulario de perfil
- Soportar compartir desde galería, otras apps, etc.
This commit is contained in:
2025-10-17 18:29:00 -06:00
parent ced637a7a9
commit 5bb5e5092e
17 changed files with 367 additions and 1 deletions

View File

@@ -19,7 +19,8 @@
<!-- Formulario de edición de perfil o Lista de aplicaciones -->
<UserProfileForm
v-if="showProfileForm"
@close="showProfileForm = false"
:shared-image-url="sharedImageUrl"
@close="handleCloseProfileForm"
/>
<AuthApplicationsList v-else />
@@ -51,10 +52,38 @@
<script setup lang="ts">
const { isAuthenticated } = useAuthentik()
const { isNight } = useTheme()
const route = useRoute()
const router = useRouter()
// Estado para mostrar formulario de edición
const showProfileForm = ref(false)
// URL de imagen compartida (si existe)
const sharedImageUrl = ref<string | null>(null)
// Detectar si se compartió una imagen
onMounted(() => {
const sharedToken = route.query.shared as string
const sharedFile = route.query.file as string
if (sharedToken && sharedFile) {
// Construir URL del archivo temporal
sharedImageUrl.value = `/temp-shared/${sharedFile}`
// Abrir automáticamente el formulario de perfil
showProfileForm.value = true
// Limpiar query params de la URL sin recargar
router.replace({ query: {} })
}
})
// Manejar cierre del formulario
const handleCloseProfileForm = () => {
showProfileForm.value = false
sharedImageUrl.value = null
}
// Configurar meta tags para PWA
useHead({
htmlAttrs: {

View File

@@ -420,6 +420,11 @@
const { user } = useAuthentik()
const toast = useToast()
// Props
const props = defineProps<{
sharedImageUrl?: string | null
}>()
// Emits
const emit = defineEmits(['close'])
@@ -544,6 +549,13 @@ if (import.meta.client) {
onUnmounted(() => {
window.removeEventListener('beforeunload', handleBeforeUnload)
})
// Detectar y procesar imagen compartida automáticamente
watch(() => props.sharedImageUrl, async (newUrl) => {
if (newUrl && import.meta.client) {
await processSharedImage(newUrl)
}
}, { immediate: true })
}
// Enviar formulario
@@ -802,6 +814,55 @@ const processImageFile = async (file: File) => {
const blob = new Blob([file], { type: file.type })
await handleAvatarCapture(blob)
}
// Procesar imagen compartida desde Web Share Target
const processSharedImage = async (imageUrl: string) => {
try {
// Mostrar notificación
toast.add({
title: 'Imagen compartida recibida',
description: 'Procesando imagen para tu avatar...',
color: 'info',
icon: 'i-heroicons-photo'
})
// Descargar la imagen del servidor
const response = await fetch(imageUrl)
if (!response.ok) {
throw new Error('No se pudo cargar la imagen compartida')
}
const blob = await response.blob()
// Validar que sea una imagen
if (!blob.type.startsWith('image/')) {
throw new Error('El archivo compartido no es una imagen válida')
}
// Procesar y subir la imagen
await handleAvatarCapture(blob)
// Limpiar archivo temporal del servidor
try {
await $fetch('/api/share-target/cleanup', {
method: 'POST',
body: { imageUrl }
})
} catch (cleanupError) {
console.error('Error limpiando archivo temporal:', cleanupError)
// No es crítico si falla la limpieza
}
} catch (error: any) {
console.error('Error procesando imagen compartida:', error)
toast.add({
title: 'Error',
description: error.message || 'No se pudo procesar la imagen compartida',
color: 'error',
icon: 'i-heroicons-exclamation-triangle'
})
}
}
</script>
<style scoped>