summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgrothedev <grothedev@gmail.com>2025-12-28 15:54:51 -0500
committergrothedev <grothedev@gmail.com>2025-12-28 15:54:51 -0500
commit119dd4129b91d8a3437a4f4e099b74991ee0044d (patch)
tree2824945328419e3d7d9db0500c75c051ffdf2f39
parent163be506b1e7102a7e96f79b7d919c3561095a38 (diff)
gallery view and slideshow
-rw-r--r--main.ts9
-rw-r--r--views.ts331
2 files changed, 337 insertions, 3 deletions
diff --git a/main.ts b/main.ts
index fdfe5f0..f8bf32b 100644
--- a/main.ts
+++ b/main.ts
@@ -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);
diff --git a/views.ts b/views.ts
index ede9724..bfd089c 100644
--- a/views.ts
+++ b/views.ts
@@ -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()">&times;</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>