Improve guest photo downloads with preview/original variants
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
const setSearchParamsMock = vi.fn();
|
||||
const pushGuestToastMock = vi.fn();
|
||||
@@ -199,4 +199,40 @@ describe('GalleryScreen', () => {
|
||||
expect(screen.queryByLabelText('AI Magic Edit')).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('uses download_url when downloading from lightbox', async () => {
|
||||
fetchGalleryMock.mockResolvedValue({
|
||||
data: [{
|
||||
id: 123,
|
||||
thumbnail_url: '/storage/demo-thumb.jpg',
|
||||
download_url: '/api/v1/gallery/demo/photos/123/download?signature=abc',
|
||||
likes_count: 2,
|
||||
}],
|
||||
});
|
||||
fetchPhotoMock.mockRejectedValue(Object.assign(new Error('not found'), { status: 404 }));
|
||||
|
||||
const originalCreateElement = document.createElement.bind(document);
|
||||
const clickSpy = vi.fn();
|
||||
let createdLink: HTMLAnchorElement | null = null;
|
||||
|
||||
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName: string) => {
|
||||
const element = originalCreateElement(tagName) as HTMLElement;
|
||||
if (tagName.toLowerCase() === 'a') {
|
||||
createdLink = element as HTMLAnchorElement;
|
||||
(createdLink as any).click = clickSpy;
|
||||
}
|
||||
return element;
|
||||
});
|
||||
|
||||
render(<GalleryScreen />);
|
||||
|
||||
await waitFor(() => expect(fetchGalleryMock).toHaveBeenCalled());
|
||||
const downloadButton = await screen.findByRole('button', { name: /download/i });
|
||||
fireEvent.click(downloadButton);
|
||||
|
||||
expect(clickSpy).toHaveBeenCalled();
|
||||
expect(createdLink?.getAttribute('href')).toBe('/api/v1/gallery/demo/photos/123/download?signature=abc');
|
||||
|
||||
createElementSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,8 @@ type GalleryFilter = 'latest' | 'popular' | 'mine' | 'photobooth';
|
||||
type GalleryTile = {
|
||||
id: number;
|
||||
imageUrl: string;
|
||||
fullUrl?: string | null;
|
||||
downloadUrl?: string | null;
|
||||
likes: number;
|
||||
createdAt?: string | null;
|
||||
ingestSource?: string | null;
|
||||
@@ -42,6 +44,8 @@ type GalleryTile = {
|
||||
type LightboxPhoto = {
|
||||
id: number;
|
||||
imageUrl: string;
|
||||
fullUrl?: string | null;
|
||||
downloadUrl?: string | null;
|
||||
likes: number;
|
||||
isMine?: boolean;
|
||||
taskId?: number | null;
|
||||
@@ -63,6 +67,9 @@ function normalizeImageUrl(src?: string | null) {
|
||||
}
|
||||
|
||||
let cleanPath = src.replace(/^\/+/g, '').replace(/\/+/g, '/');
|
||||
if (cleanPath.startsWith('api/')) {
|
||||
return `/${cleanPath}`;
|
||||
}
|
||||
if (!cleanPath.startsWith('storage/')) {
|
||||
cleanPath = `storage/${cleanPath}`;
|
||||
}
|
||||
@@ -175,14 +182,23 @@ export default function GalleryScreen() {
|
||||
const record = photo as Record<string, unknown>;
|
||||
const id = Number(record.id ?? 0);
|
||||
const likesCount = typeof record.likes_count === 'number' ? record.likes_count : 0;
|
||||
const fullUrl = normalizeImageUrl(
|
||||
(record.full_url as string | null | undefined)
|
||||
?? (record.file_path as string | null | undefined)
|
||||
?? (record.url as string | null | undefined)
|
||||
?? (record.image_url as string | null | undefined)
|
||||
);
|
||||
const imageUrl = normalizeImageUrl(
|
||||
(record.thumbnail_url as string | null | undefined)
|
||||
?? (record.thumbnail_path as string | null | undefined)
|
||||
?? (record.file_path as string | null | undefined)
|
||||
?? (record.full_url as string | null | undefined)
|
||||
?? fullUrl
|
||||
?? (record.url as string | null | undefined)
|
||||
?? (record.image_url as string | null | undefined)
|
||||
);
|
||||
const downloadUrl = normalizeImageUrl(
|
||||
(record.download_url as string | null | undefined)
|
||||
?? fullUrl
|
||||
);
|
||||
const rawTaskId = Number(record.task_id ?? record.taskId ?? 0);
|
||||
const taskId = Number.isFinite(rawTaskId) && rawTaskId > 0 ? rawTaskId : null;
|
||||
const taskLabel =
|
||||
@@ -226,6 +242,8 @@ export default function GalleryScreen() {
|
||||
return {
|
||||
id,
|
||||
imageUrl,
|
||||
fullUrl: fullUrl || null,
|
||||
downloadUrl: downloadUrl || null,
|
||||
likes: likesCount,
|
||||
createdAt: typeof record.created_at === 'string' ? record.created_at : null,
|
||||
ingestSource: typeof record.ingest_source === 'string' ? record.ingest_source : null,
|
||||
@@ -379,14 +397,23 @@ export default function GalleryScreen() {
|
||||
const record = photo as Record<string, unknown>;
|
||||
const id = Number(record.id ?? 0);
|
||||
const likesCount = typeof record.likes_count === 'number' ? record.likes_count : 0;
|
||||
const fullUrl = normalizeImageUrl(
|
||||
(record.full_url as string | null | undefined)
|
||||
?? (record.file_path as string | null | undefined)
|
||||
?? (record.url as string | null | undefined)
|
||||
?? (record.image_url as string | null | undefined)
|
||||
);
|
||||
const imageUrl = normalizeImageUrl(
|
||||
(record.thumbnail_url as string | null | undefined)
|
||||
?? (record.thumbnail_path as string | null | undefined)
|
||||
?? (record.file_path as string | null | undefined)
|
||||
?? (record.full_url as string | null | undefined)
|
||||
?? fullUrl
|
||||
?? (record.url as string | null | undefined)
|
||||
?? (record.image_url as string | null | undefined)
|
||||
);
|
||||
const downloadUrl = normalizeImageUrl(
|
||||
(record.download_url as string | null | undefined)
|
||||
?? fullUrl
|
||||
);
|
||||
if (!id || !imageUrl || existing.has(id)) {
|
||||
return null;
|
||||
}
|
||||
@@ -395,6 +422,8 @@ export default function GalleryScreen() {
|
||||
return {
|
||||
id,
|
||||
imageUrl,
|
||||
fullUrl: fullUrl || null,
|
||||
downloadUrl: downloadUrl || null,
|
||||
likes: likesCount,
|
||||
createdAt: typeof record.created_at === 'string' ? record.created_at : null,
|
||||
ingestSource: typeof record.ingest_source === 'string' ? record.ingest_source : null,
|
||||
@@ -447,6 +476,8 @@ export default function GalleryScreen() {
|
||||
? {
|
||||
id: lightboxSelected.id,
|
||||
imageUrl: lightboxSelected.imageUrl,
|
||||
fullUrl: lightboxSelected.fullUrl ?? null,
|
||||
downloadUrl: lightboxSelected.downloadUrl ?? null,
|
||||
likes: lightboxSelected.likes,
|
||||
isMine: lightboxSelected.isMine,
|
||||
taskId: lightboxSelected.taskId ?? null,
|
||||
@@ -764,9 +795,10 @@ export default function GalleryScreen() {
|
||||
);
|
||||
|
||||
const downloadPhoto = React.useCallback((photo?: LightboxPhoto | null) => {
|
||||
if (!photo?.imageUrl) return;
|
||||
const url = photo?.downloadUrl ?? photo?.fullUrl ?? photo?.imageUrl ?? null;
|
||||
if (!url) return;
|
||||
const link = document.createElement('a');
|
||||
link.href = photo.imageUrl;
|
||||
link.href = url;
|
||||
link.download = `photo-${photo.id}.jpg`;
|
||||
link.rel = 'noreferrer';
|
||||
document.body.appendChild(link);
|
||||
@@ -1498,14 +1530,23 @@ export default function GalleryScreen() {
|
||||
function mapFullPhoto(photo: Record<string, unknown>): LightboxPhoto | null {
|
||||
const id = Number(photo.id ?? 0);
|
||||
if (!id) return null;
|
||||
const imageUrl = normalizeImageUrl(
|
||||
const fullUrl = normalizeImageUrl(
|
||||
(photo.full_url as string | null | undefined)
|
||||
?? (photo.file_path as string | null | undefined)
|
||||
?? (photo.url as string | null | undefined)
|
||||
?? (photo.image_url as string | null | undefined)
|
||||
);
|
||||
const imageUrl = normalizeImageUrl(
|
||||
fullUrl
|
||||
?? (photo.thumbnail_url as string | null | undefined)
|
||||
?? (photo.thumbnail_path as string | null | undefined)
|
||||
?? (photo.url as string | null | undefined)
|
||||
?? (photo.image_url as string | null | undefined)
|
||||
);
|
||||
const downloadUrl = normalizeImageUrl(
|
||||
(photo.download_url as string | null | undefined)
|
||||
?? fullUrl
|
||||
);
|
||||
if (!imageUrl) return null;
|
||||
const taskLabel =
|
||||
typeof photo.task_title === 'string'
|
||||
@@ -1550,6 +1591,8 @@ function mapFullPhoto(photo: Record<string, unknown>): LightboxPhoto | null {
|
||||
return {
|
||||
id,
|
||||
imageUrl,
|
||||
fullUrl: fullUrl || null,
|
||||
downloadUrl: downloadUrl || null,
|
||||
likes: typeof photo.likes_count === 'number' ? photo.likes_count : 0,
|
||||
isMine,
|
||||
taskId,
|
||||
|
||||
@@ -22,6 +22,8 @@ import { GUEST_AI_MAGIC_EDITS_ENABLED } from '../lib/featureFlags';
|
||||
type LightboxPhoto = {
|
||||
id: number;
|
||||
imageUrl: string;
|
||||
fullUrl?: string | null;
|
||||
downloadUrl?: string | null;
|
||||
likes: number;
|
||||
createdAt?: string | null;
|
||||
ingestSource?: string | null;
|
||||
@@ -37,6 +39,9 @@ function normalizeImageUrl(src?: string | null) {
|
||||
}
|
||||
|
||||
let cleanPath = src.replace(/^\/+/g, '').replace(/\/+/g, '/');
|
||||
if (cleanPath.startsWith('api/')) {
|
||||
return `/${cleanPath}`;
|
||||
}
|
||||
if (!cleanPath.startsWith('storage/')) {
|
||||
cleanPath = `storage/${cleanPath}`;
|
||||
}
|
||||
@@ -47,18 +52,29 @@ function normalizeImageUrl(src?: string | null) {
|
||||
function mapPhoto(photo: Record<string, unknown>): LightboxPhoto | null {
|
||||
const id = Number(photo.id ?? 0);
|
||||
if (!id) return null;
|
||||
const imageUrl = normalizeImageUrl(
|
||||
const fullUrl = normalizeImageUrl(
|
||||
(photo.full_url as string | null | undefined)
|
||||
?? (photo.file_path as string | null | undefined)
|
||||
?? (photo.url as string | null | undefined)
|
||||
?? (photo.image_url as string | null | undefined)
|
||||
);
|
||||
const imageUrl = normalizeImageUrl(
|
||||
fullUrl
|
||||
?? (photo.thumbnail_url as string | null | undefined)
|
||||
?? (photo.thumbnail_path as string | null | undefined)
|
||||
?? (photo.url as string | null | undefined)
|
||||
?? (photo.image_url as string | null | undefined)
|
||||
);
|
||||
const downloadUrl = normalizeImageUrl(
|
||||
(photo.download_url as string | null | undefined)
|
||||
?? fullUrl
|
||||
);
|
||||
if (!imageUrl) return null;
|
||||
return {
|
||||
id,
|
||||
imageUrl,
|
||||
fullUrl: fullUrl || null,
|
||||
downloadUrl: downloadUrl || null,
|
||||
likes: typeof photo.likes_count === 'number' ? photo.likes_count : 0,
|
||||
createdAt: typeof photo.created_at === 'string' ? photo.created_at : null,
|
||||
ingestSource: typeof photo.ingest_source === 'string' ? photo.ingest_source : null,
|
||||
@@ -617,7 +633,7 @@ export default function PhotoLightboxScreen() {
|
||||
>
|
||||
<Button
|
||||
unstyled
|
||||
onPress={() => downloadPhoto(selected?.imageUrl ?? null, selected?.id ?? null)}
|
||||
onPress={() => downloadPhoto(selected?.downloadUrl ?? selected?.fullUrl ?? selected?.imageUrl ?? null, selected?.id ?? null)}
|
||||
paddingHorizontal="$3"
|
||||
paddingVertical="$2"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user