Ver a proveniência

Falback popolamento cover

Sviluppo_Carrello_Immagini
Lorenzo Pollutri há 21 horas
ascendente
cometimento
7f0b1d1139
1 ficheiros alterados com 77 adições e 2 eliminações
  1. +77
    -2
      app/admin/page.tsx

+ 77
- 2
app/admin/page.tsx Ver ficheiro

@@ -6,6 +6,8 @@ import { Card, Portal, MediaItem } from '@/types';
const isVideoUrl = (url: string) => /\.(mp4|webm|mov|m4v|ogv)(\?|$)/i.test(url);
const isPdfFile = (file: File) =>
file.type === 'application/pdf' || /\.pdf$/i.test(file.name);
const isVideoFile = (file: File) =>
file.type.startsWith('video/') || /\.(mp4|webm|mov|m4v|ogv)$/i.test(file.name);
async function uploadBlobAsImage(blob: Blob, name: string): Promise<string | null> {
const formData = new FormData();
@@ -15,6 +17,42 @@ async function uploadBlobAsImage(blob: Blob, name: string): Promise<string | nul
return data.url || null;
}
async function extractVideoFrame(file: File): Promise<Blob | null> {
const url = URL.createObjectURL(file);
try {
const video = document.createElement('video');
video.muted = true;
video.playsInline = true;
video.preload = 'metadata';
video.src = url;
await new Promise<void>((resolve, reject) => {
video.addEventListener('loadedmetadata', () => resolve(), { once: true });
video.addEventListener('error', () => reject(new Error('video load error')), { once: true });
});
// Seek slightly past 0 — at exactly 0 some codecs return a black frame
video.currentTime = Math.min(0.1, Math.max(0, video.duration / 10));
await new Promise<void>((resolve, reject) => {
video.addEventListener('seeked', () => resolve(), { once: true });
video.addEventListener('error', () => reject(new Error('video seek error')), { once: true });
});
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
if (!ctx) return null;
ctx.drawImage(video, 0, 0);
return await new Promise<Blob | null>((resolve) =>
canvas.toBlob((b) => resolve(b), 'image/jpeg', 0.85)
);
} finally {
URL.revokeObjectURL(url);
}
}
async function pdfToImageItems(
file: File,
onProgress: (page: number, total: number) => void
@@ -102,6 +140,10 @@ export default function AdminDashboard() {
if (!files || files.length === 0) return;
setUploading(prev => ({ ...prev, extraMedia: true }));
const startedWithoutCover = !isEditing?.imageUrl;
let pendingCover: string | null = null;
const canPromote = () => startedWithoutCover && !pendingCover;
const uploaded: MediaItem[] = [];
for (const file of Array.from(files)) {
try {
@@ -109,14 +151,46 @@ export default function AdminDashboard() {
const items = await pdfToImageItems(file, (page, total) =>
setPdfProgress({ name: file.name, page, total })
);
uploaded.push(...items);
setPdfProgress(null);
if (items.length > 0 && canPromote()) {
// Promote the first PDF page to cover; skip it from the gallery to avoid duplication.
pendingCover = items[0].url;
uploaded.push(...items.slice(1));
} else {
uploaded.push(...items);
}
} else {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/api/upload', { method: 'POST', body: formData });
const data = await res.json();
if (data.url) uploaded.push({ url: data.url });
if (!data.url) continue;
if (isVideoFile(file)) {
// Video always goes to the gallery so users can play it.
uploaded.push({ url: data.url });
// If no cover yet, extract the first frame and use it as the cover.
if (canPromote()) {
try {
const blob = await extractVideoFrame(file);
if (blob) {
const baseName = file.name.replace(/\.[^.]+$/, '').replace(/[^a-zA-Z0-9-_]/g, '_');
const posterUrl = await uploadBlobAsImage(blob, `${baseName}-poster.jpg`);
if (posterUrl) pendingCover = posterUrl;
}
} catch (err) {
console.warn('Could not extract video poster for', file.name, err);
}
}
} else {
// Plain image
if (canPromote()) {
// Promote to cover; skip the gallery to avoid duplication.
pendingCover = data.url;
} else {
uploaded.push({ url: data.url });
}
}
}
} catch (err) {
console.error('Upload failed for', file.name, err);
@@ -127,6 +201,7 @@ export default function AdminDashboard() {
setIsEditing(prev => ({
...prev,
imageUrl: (startedWithoutCover && pendingCover) ? pendingCover : (prev?.imageUrl || ''),
extraMedia: [...(prev?.extraMedia || []), ...uploaded],
}));
setUploading(prev => ({ ...prev, extraMedia: false }));


Carregando…
Cancelar
Guardar