diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index ceeced1..1ddaedf 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,367 +1,426 @@
{ if (e.target === e.currentTarget) activePhoto = null; }}
- onkeydown={handleKeyDown}
- ontouchstart={handleTouchStart}
- ontouchmove={handleTouchMove}
- ontouchend={handleTouchEnd}
- >
-
-
- {activeIndex + 1}
/ {photos.length}
+
{ if (e.target === e.currentTarget) activePhoto = null; }}
+ onkeydown={handleKeyDown}
+ ontouchstart={handleTouchStart}
+ ontouchmove={handleTouchMove}
+ ontouchend={handleTouchEnd}
+ >
+
+
+ {activeIndex + 1} / {photos.length}
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+ {#if !isZoomed}
+
-
+
+ {/if}
-
-
-
-
-
-
-
+
+
-
- {#if !isZoomed}
-
-
-
- {/if}
-
-
-
-
-
-
+
+
{/if}
\ No newline at end of file
diff --git a/src/routes/api/download/+server.js b/src/routes/api/download/+server.js
index 0a022f4..3ceac0f 100644
--- a/src/routes/api/download/+server.js
+++ b/src/routes/api/download/+server.js
@@ -5,7 +5,9 @@ import { env } from '$env/dynamic/private';
export async function GET({ url }) {
const id = url.searchParams.get('id');
- if (!id) throw error(400, 'Missing photo identity ID');
+ const gallery = url.searchParams.get('gallery');
+
+ if (!id || !gallery) throw error(400, 'Missing identifier parameters.');
try {
const s3 = new S3Client({
@@ -17,23 +19,19 @@ export async function GET({ url }) {
}
});
- const fullSizeKey = `images/${id}`;
+ const fullSizeKey = `${gallery}/images/${id}`;
- // Force the browser to treat this as an attachment download
const command = new GetObjectCommand({
Bucket: env.B2_BUCKET_NAME,
Key: fullSizeKey,
ResponseContentDisposition: `attachment; filename="${id}"`
});
- // Generate a quick 60-second valid link
const downloadUrl = await getSignedUrl(s3, command, { expiresIn: 60 });
-
- // Redirect browser directly to the download stream
throw redirect(307, downloadUrl);
} catch (err) {
if (err.status === 307) throw err;
- console.error('Server download redirection failed:', err);
- throw error(500, 'Could not generate download routing asset.');
+ console.error(err);
+ throw error(500, 'Could not map download routing path.');
}
}
\ No newline at end of file
diff --git a/src/routes/api/photos/+server.js b/src/routes/api/photos/+server.js
index cee2ccb..108cfe7 100644
--- a/src/routes/api/photos/+server.js
+++ b/src/routes/api/photos/+server.js
@@ -5,6 +5,7 @@ import { env } from '$env/dynamic/private';
export async function GET({ url }) {
const nextToken = url.searchParams.get('next') || undefined;
+ const activeGallery = url.searchParams.get('gallery') || '';
try {
const s3 = new S3Client({
@@ -18,10 +19,33 @@ export async function GET({ url }) {
const tokenDuration = parseInt(env.B2_TOKEN_DURATION, 10) || 600;
- // Fetch a batch of files from the thumbs/ folder
+ // --- PHASE 1: DYNAMICALLY DISCOVER GALLERIES ---
+ // We use a delimiter to find the top-level virtual folders
+ const discoveryCommand = new ListObjectsV2Command({
+ Bucket: env.B2_BUCKET_NAME,
+ Delimiter: '/'
+ });
+
+ const discoveryResponse = await s3.send(discoveryCommand);
+ // CommonPrefixes contains the top-level folders (e.g., "vacation-2026/", "wedding/")
+ const prefixes = discoveryResponse.CommonPrefixes || [];
+
+ const galleries = prefixes
+ .map(p => p.Prefix.replace('/', ''))
+ .filter(name => name.length > 0)
+ .sort((a, b) => a.localeCompare(b)); // Sort alphabetically
+
+ // Fallback to a default if no gallery parameter is provided yet
+ const selectedGallery = activeGallery || galleries[0] || '';
+
+ if (!selectedGallery) {
+ return json({ photos: [], galleries: [], nextContinuationToken: null });
+ }
+
+ // --- PHASE 2: FETCH PHOTOS FOR SELECTED GALLERY ---
const listCommand = new ListObjectsV2Command({
Bucket: env.B2_BUCKET_NAME,
- Prefix: 'thumbs/',
+ Prefix: `${selectedGallery}/thumbs/`,
MaxKeys: 40,
ContinuationToken: nextToken
});
@@ -29,22 +53,20 @@ export async function GET({ url }) {
const listResponse = await s3.send(listCommand);
const contents = listResponse.Contents || [];
- // Strict Filter: Only allow actual .jpg or .jpeg extensions
const imageFiles = contents.filter(file => {
const lowerKey = file.Key.toLowerCase();
return lowerKey.endsWith('.jpg') || lowerKey.endsWith('.jpeg');
});
- // Generate presigned URLs for validated items
const photos = await Promise.all(
imageFiles.map(async (file) => {
- const id = file.Key.replace('thumbs/', '');
- const fullSizeKey = `images/${id}`;
+ // Clean up ID tracking relative to the dynamic folder path
+ const id = file.Key.replace(`${selectedGallery}/thumbs/`, '');
+ const fullSizeKey = `${selectedGallery}/images/${id}`;
const thumbCommand = new GetObjectCommand({ Bucket: env.B2_BUCKET_NAME, Key: file.Key });
const fullCommand = new GetObjectCommand({ Bucket: env.B2_BUCKET_NAME, Key: fullSizeKey });
- // 10 minutes = 600 seconds
const [thumbUrl, fullUrl] = await Promise.all([
getSignedUrl(s3, thumbCommand, { expiresIn: tokenDuration }),
getSignedUrl(s3, fullCommand, { expiresIn: tokenDuration })
@@ -56,10 +78,12 @@ export async function GET({ url }) {
return json({
photos,
+ galleries,
+ currentGallery: selectedGallery,
nextContinuationToken: listResponse.NextContinuationToken || null
});
} catch (err) {
console.error(err);
- throw error(500, 'Failed to process images from storage.');
+ throw error(500, 'Failed to map multi-gallery structures.');
}
}
\ No newline at end of file