diff options
| author | grothedev <grothedev@gmail.com> | 2025-12-28 15:54:51 -0500 |
|---|---|---|
| committer | grothedev <grothedev@gmail.com> | 2025-12-28 15:54:51 -0500 |
| commit | 119dd4129b91d8a3437a4f4e099b74991ee0044d (patch) | |
| tree | 2824945328419e3d7d9db0500c75c051ffdf2f39 | |
| parent | 163be506b1e7102a7e96f79b7d919c3561095a38 (diff) | |
gallery view and slideshow
| -rw-r--r-- | main.ts | 9 | ||||
| -rw-r--r-- | views.ts | 331 |
2 files changed, 337 insertions, 3 deletions
@@ -11,7 +11,7 @@ import { getMimeType, isViewableInBrowser, } from "./fileUtils.ts"; -import { renderUploadPage, renderBrowsePage, renderError } from "./views.ts"; +import { renderUploadPage, renderBrowsePage, renderGalleryPage, renderError } from "./views.ts"; const app = new Application(); const router = new Router(); @@ -55,6 +55,13 @@ router.get("/browse", async (ctx) => { ctx.response.body = renderBrowsePage(files); }); +router.get("/gallery", async (ctx) => { + logger.info("Page visit", { page: "/gallery", ip: getClientIP(ctx) }); + const files = await listUploadedFiles(); + ctx.response.headers.set("Content-Type", "text/html"); + ctx.response.body = renderGalleryPage(files); +}); + router.post("/upload", async (ctx) => { const ip = getClientIP(ctx); @@ -36,7 +36,10 @@ export function renderUploadPage(chunked = false) { <section> <h5>Shared Files</h5> <p>Welcome to the file sharing service</p> - <center><a href="/browse">Browse Uploaded Files</a></center> + <center> + <a href="/browse">Browse All Files</a> | + <a href="/gallery">View Gallery</a> + </center> </section> <section id="fileupload${chunked ? '-js' : '-nojs'}"> @@ -45,6 +48,7 @@ export function renderUploadPage(chunked = false) { <h6> ${chunked ? '<a href="/">Simple Upload</a>' : '<a href="/chunked">Chunked Upload</a>'} | <a href="/browse">Browse Files</a> + | <a href="/gallery">Gallery</a> </h6> <form id="uploadForm" ${chunked ? '' : 'action="/upload" method="POST" enctype="multipart/form-data"'}> <input type="file" id="fileInput" name="files" multiple ${chunked ? '' : 'required'}> @@ -296,7 +300,8 @@ export function renderBrowsePage(files: Array<{ name: string; size: number; uplo <h6> <a href="/">Simple Upload</a> | <a href="/chunked">Chunked Upload</a> | - <a href="/browse">Browse Files</a> + <a href="/browse">Browse Files</a> | + <a href="/gallery">Gallery</a> </h6> </section> @@ -333,6 +338,328 @@ export function renderBrowsePage(files: Array<{ name: string; size: number; uplo return renderTemplate("Browse Files", content); } +export function renderGalleryPage(files: Array<{ name: string; size: number; uploaded: Date }>) { + // Filter only images and videos + const mediaFiles = files.filter(file => { + const mimeType = getMimeType(file.name); + return mimeType.startsWith("image/") || mimeType.startsWith("video/"); + }); + + const galleryItems = mediaFiles.map((file, index) => { + const mimeType = getMimeType(file.name); + const isVideo = mimeType.startsWith("video/"); + + return ` + <div class="gallery-item" data-index="${index}"> + <a href="#" onclick="openSlideshow(${index}); return false;"> + ${isVideo ? ` + <video src="/download/${file.name}" style="width: 100%; height: 200px; object-fit: cover; border-radius: 4px;"></video> + <div class="video-overlay">▶</div> + ` : ` + <img src="/download/${file.name}" alt="${file.name}" style="width: 100%; height: 200px; object-fit: cover; border-radius: 4px;"> + `} + </a> + <div class="gallery-info"> + <div class="gallery-filename" title="${file.name}">${file.name}</div> + <div class="gallery-meta">${formatFileSize(file.size)} • ${file.uploaded.toLocaleDateString()}</div> + </div> + </div> + `; + }).join(''); + + const content = ` +<section> + <h5>Gallery</h5> + <h6> + <a href="/">Simple Upload</a> | + <a href="/chunked">Chunked Upload</a> | + <a href="/browse">Browse Files</a> | + <a href="/gallery">Gallery</a> + </h6> +</section> + +<section> + <div class="widget"> + ${mediaFiles.length > 0 ? ` + <div class="gallery-grid"> + ${galleryItems} + </div> + ` : ` + <div style="text-align: center; padding: 60px 20px; color: #666;"> + <div style="font-size: 64px; margin-bottom: 20px;">🖼️</div> + <h3>No images or videos yet</h3> + <p>Upload some media files to see them here!</p> + </div> + `} + </div> +</section> + +<!-- Slideshow Modal --> +<div id="slideshow-modal" class="slideshow-modal"> + <span class="slideshow-close" onclick="closeSlideshow()">×</span> + <button class="slideshow-nav slideshow-prev" onclick="changeSlide(-1)">❮</button> + <div class="slideshow-content"> + <div id="slideshow-media-container"></div> + <div class="slideshow-info"> + <div id="slideshow-filename"></div> + <div id="slideshow-counter"></div> + </div> + </div> + <button class="slideshow-nav slideshow-next" onclick="changeSlide(1)">❯</button> +</div> + +<footer> + <p>Last updated December 28th, 2025</p> +</footer>`; + + const mediaFilesJson = JSON.stringify(mediaFiles.map(file => ({ + name: file.name, + isVideo: getMimeType(file.name).startsWith("video/"), + size: formatFileSize(file.size), + uploaded: file.uploaded.toLocaleDateString() + }))); + + const styles = ` + <style> + .gallery-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 20px; + padding: 10px 0; + } + .gallery-item { + position: relative; + background: #f5f5f5; + border-radius: 4px; + overflow: hidden; + transition: transform 0.2s, box-shadow 0.2s; + cursor: pointer; + } + .gallery-item:hover { + transform: translateY(-4px); + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + } + .gallery-item a { + display: block; + position: relative; + text-decoration: none; + } + .video-overlay { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 48px; + color: white; + text-shadow: 0 2px 4px rgba(0,0,0,0.5); + pointer-events: none; + } + .gallery-info { + padding: 10px; + background: white; + } + .gallery-filename { + font-size: 14px; + font-weight: 600; + color: #333; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 4px; + } + .gallery-meta { + font-size: 12px; + color: #666; + } + + /* Slideshow Styles */ + .slideshow-modal { + display: none; + position: fixed; + z-index: 9999; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.95); + } + .slideshow-modal.active { + display: flex; + align-items: center; + justify-content: center; + } + .slideshow-close { + position: absolute; + top: 20px; + right: 40px; + color: #fff; + font-size: 50px; + font-weight: bold; + cursor: pointer; + z-index: 10001; + transition: color 0.3s; + } + .slideshow-close:hover { + color: #bbb; + } + .slideshow-content { + max-width: 90%; + max-height: 90%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + #slideshow-media-container { + display: flex; + align-items: center; + justify-content: center; + max-width: 100%; + max-height: 80vh; + } + #slideshow-media-container img, + #slideshow-media-container video { + max-width: 100%; + max-height: 80vh; + object-fit: contain; + border-radius: 4px; + } + .slideshow-info { + color: white; + text-align: center; + margin-top: 20px; + font-size: 16px; + } + #slideshow-filename { + font-weight: 600; + margin-bottom: 8px; + } + #slideshow-counter { + font-size: 14px; + color: #ccc; + } + .slideshow-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + background-color: rgba(255, 255, 255, 0.2); + color: white; + border: none; + font-size: 40px; + padding: 20px; + cursor: pointer; + transition: background-color 0.3s; + z-index: 10001; + } + .slideshow-nav:hover { + background-color: rgba(255, 255, 255, 0.4); + } + .slideshow-prev { + left: 20px; + } + .slideshow-next { + right: 20px; + } + + @media (max-width: 550px) { + .gallery-grid { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 10px; + } + .gallery-item a img, + .gallery-item a video { + height: 150px !important; + } + .slideshow-close { + top: 10px; + right: 20px; + font-size: 40px; + } + .slideshow-nav { + font-size: 30px; + padding: 15px; + } + .slideshow-prev { + left: 10px; + } + .slideshow-next { + right: 10px; + } + } + </style> + + <script> + const mediaFiles = ${mediaFilesJson}; + let currentSlide = 0; + + function openSlideshow(index) { + currentSlide = index; + showSlide(currentSlide); + document.getElementById('slideshow-modal').classList.add('active'); + document.body.style.overflow = 'hidden'; + } + + function closeSlideshow() { + document.getElementById('slideshow-modal').classList.remove('active'); + document.body.style.overflow = 'auto'; + + // Pause any playing videos + const videos = document.querySelectorAll('#slideshow-media-container video'); + videos.forEach(v => v.pause()); + } + + function changeSlide(direction) { + currentSlide += direction; + + if (currentSlide >= mediaFiles.length) { + currentSlide = 0; + } + if (currentSlide < 0) { + currentSlide = mediaFiles.length - 1; + } + + showSlide(currentSlide); + } + + function showSlide(index) { + const file = mediaFiles[index]; + const container = document.getElementById('slideshow-media-container'); + + if (file.isVideo) { + container.innerHTML = \`<video src="/download/\${file.name}" controls autoplay style="max-width: 100%; max-height: 80vh;"></video>\`; + } else { + container.innerHTML = \`<img src="/download/\${file.name}" alt="\${file.name}" style="max-width: 100%; max-height: 80vh;">\`; + } + + document.getElementById('slideshow-filename').textContent = file.name; + document.getElementById('slideshow-counter').textContent = \`\${index + 1} / \${mediaFiles.length}\`; + } + + // Keyboard navigation + document.addEventListener('keydown', (e) => { + const modal = document.getElementById('slideshow-modal'); + if (!modal.classList.contains('active')) return; + + if (e.key === 'ArrowLeft') { + changeSlide(-1); + } else if (e.key === 'ArrowRight') { + changeSlide(1); + } else if (e.key === 'Escape') { + closeSlideshow(); + } + }); + + // Close on background click + document.getElementById('slideshow-modal').addEventListener('click', (e) => { + if (e.target.id === 'slideshow-modal') { + closeSlideshow(); + } + }); + </script>`; + + return renderTemplate("Gallery", content, styles); +} + export function renderError(error: string, status = 400) { const content = ` <section> |
