diff --git a/bun.lock b/bun.lock index ab0d7fd..2ab1014 100644 --- a/bun.lock +++ b/bun.lock @@ -16,6 +16,7 @@ "@sveltejs/kit": "^2.49.1", "@sveltejs/vite-plugin-svelte": "^6.2.1", "@tailwindcss/vite": "^4.1.17", + "sharp": "^0.34.5", "svelte": "^5.45.6", "svelte-check": "^4.3.4", "tailwindcss": "^4.1.17", diff --git a/package.json b/package.json index 30b68fc..6469232 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,34 @@ { - "name": "steve.photo", - "private": true, - "version": "0.1.0", - "type": "module", - "scripts": { - "dev": "vite dev", - "build": "vite build", - "deploy": "bun run build && wrangler deploy --minify", - "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" - }, - "devDependencies": { - "@sveltejs/adapter-auto": "^7.0.0", - "@sveltejs/adapter-cloudflare": "^7.2.6", - "@sveltejs/kit": "^2.49.1", - "@sveltejs/vite-plugin-svelte": "^6.2.1", - "@tailwindcss/vite": "^4.1.17", - "svelte": "^5.45.6", - "svelte-check": "^4.3.4", - "tailwindcss": "^4.1.17", - "typescript": "^5.9.3", - "vite": "^7.2.6" - }, - "dependencies": { - "exifr": "^7.1.3", - "feed": "^5.2.0", - "from": "^0.1.7", - "import": "^0.0.6" - } + "name": "steve.photo", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "deploy": "bun run build && wrangler deploy --minify", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/adapter-cloudflare": "^7.2.6", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@tailwindcss/vite": "^4.1.17", + "sharp": "^0.34.5", + "svelte": "^5.45.6", + "svelte-check": "^4.3.4", + "tailwindcss": "^4.1.17", + "typescript": "^5.9.3", + "vite": "^7.2.6" + }, + "dependencies": { + "exifr": "^7.1.3", + "feed": "^5.2.0", + "from": "^0.1.7", + "import": "^0.0.6" + } } diff --git a/schema.sql b/schema.sql index aafb546..9398948 100644 --- a/schema.sql +++ b/schema.sql @@ -13,7 +13,8 @@ CREATE TABLE IF NOT EXISTS photos ( focal_length TEXT, iso TEXT, make TEXT, - tags TEXT + tags TEXT, + blur_data TEXT ); CREATE INDEX idx_photos_date ON photos(date DESC); diff --git a/src/lib/components/ProgressiveImage.svelte b/src/lib/components/ProgressiveImage.svelte index eb03fbc..3e02f9c 100644 --- a/src/lib/components/ProgressiveImage.svelte +++ b/src/lib/components/ProgressiveImage.svelte @@ -3,11 +3,13 @@ src, thumb, alt, + blurData, class: className = "", }: { src: string; thumb: string; alt: string; + blurData?: string; class?: string; } = $props(); @@ -33,6 +35,8 @@ img.onload = null; }; }); + + let placeholderSrc = $derived(blurData || thumb);
{ focalLength: row.focal_length, iso: row.iso, make: row.make, + blurData: row.blur_data as string, })); return { photos, total, pageSize: PAGE_SIZE }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 857eb4b..82a0855 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -92,6 +92,7 @@ class="max-w-full h-auto block" src={image.image} thumb={image.thumb} + blurData={image.blurData} alt={image.title} /> @@ -124,6 +125,7 @@ class="w-full h-full block" src={image.image} thumb={image.thumb} + blurData={image.blurData} alt={image.title} /> diff --git a/src/routes/admin/+page.server.ts b/src/routes/admin/+page.server.ts index d3414f5..ddda95f 100644 --- a/src/routes/admin/+page.server.ts +++ b/src/routes/admin/+page.server.ts @@ -60,6 +60,7 @@ export const actions: Actions = { const focalLength = formData.get("focalLength") as string; const iso = formData.get("iso") as string; const make = formData.get("make") as string; + const blurData = formData.get("blur_data") as string; if (!file || !title || !date) { return fail(400, { error: "File, title, and date are required" }); @@ -114,8 +115,8 @@ export const actions: Actions = { // Insert into database await db .prepare( - `INSERT INTO photos (slug, title, date, image_key, thumb_key, camera, lens, aperture, exposure, focal_length, iso, make) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + `INSERT INTO photos (slug, title, date, image_key, thumb_key, camera, lens, aperture, exposure, focal_length, iso, make, blur_data) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .bind( slug, @@ -130,6 +131,7 @@ export const actions: Actions = { focalLength || null, iso || null, make || null, + blurData || null, ) .run(); diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte index 55c8e15..7b379b6 100644 --- a/src/routes/admin/+page.svelte +++ b/src/routes/admin/+page.svelte @@ -8,6 +8,7 @@ let selectedFile = $state(null); let previewUrl = $state(null); let thumbnailBlob = $state(null); + let blurData = $state(""); let title = $state(""); let date = $state(""); @@ -71,8 +72,9 @@ console.error("Failed to read EXIF data:", err); } - // Generate thumbnail + // Generate thumbnail and blur placeholder await generateThumbnail(file); + await generateBlurData(file); } async function generateThumbnail(file: File): Promise { @@ -118,6 +120,29 @@ }); } + async function generateBlurData(file: File): Promise { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + const targetWidth = 20; + const aspectRatio = img.height / img.width; + const targetHeight = Math.round(targetWidth * aspectRatio); + + const canvas = document.createElement("canvas"); + canvas.width = targetWidth; + canvas.height = targetHeight; + + const ctx = canvas.getContext("2d"); + if (ctx) { + ctx.drawImage(img, 0, 0, targetWidth, targetHeight); + blurData = canvas.toDataURL("image/jpeg", 0.3); + } + resolve(); + }; + img.src = URL.createObjectURL(file); + }); + } + function handleSubmit() { isLoading = true; } @@ -139,6 +164,7 @@ previewUrl = null; } thumbnailBlob = null; + blurData = ""; title = ""; date = ""; camera = ""; @@ -248,6 +274,7 @@ +
{/if} diff --git a/src/routes/api/photos/+server.ts b/src/routes/api/photos/+server.ts index c920b09..3d1b270 100644 --- a/src/routes/api/photos/+server.ts +++ b/src/routes/api/photos/+server.ts @@ -28,6 +28,7 @@ export const GET: RequestHandler = async ({ url, platform }) => { focalLength: row.focal_length as string, iso: row.iso as string, make: row.make as string, + blurData: row.blur_data as string, })); return json({ photos }); diff --git a/src/routes/photo/[slug]/+page.server.ts b/src/routes/photo/[slug]/+page.server.ts index 4a90543..c345f3f 100644 --- a/src/routes/photo/[slug]/+page.server.ts +++ b/src/routes/photo/[slug]/+page.server.ts @@ -28,6 +28,7 @@ export const load: PageServerLoad = async ({ platform, params }) => { iso: result.iso as string, make: result.make as string, tags: [], + blurData: result.blur_data as string, }; return { photo }; diff --git a/src/routes/photo/[slug]/+page.svelte b/src/routes/photo/[slug]/+page.svelte index 6c00d0d..27f2cc1 100644 --- a/src/routes/photo/[slug]/+page.svelte +++ b/src/routes/photo/[slug]/+page.svelte @@ -48,6 +48,7 @@ function handleKeydown(event) { class="max-w-full h-auto block" src={data.photo.image} thumb={data.photo.thumb} + blurData={data.photo.blurData} alt={data.photo.title} />