Spaces:
Running
Running
Add 1 files
Browse files- index.html +416 -330
index.html
CHANGED
@@ -5,7 +5,7 @@
|
|
5 |
<meta charset="UTF-8">
|
6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
<title>Local Image Viewer</title>
|
8 |
-
<!--
|
9 |
<script src="https://cdn.tailwindcss.com"></script>
|
10 |
<script>
|
11 |
tailwind.config = {
|
@@ -18,7 +18,7 @@
|
|
18 |
}
|
19 |
}
|
20 |
</script>
|
21 |
-
<!--
|
22 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
23 |
<style>
|
24 |
.dropzone {
|
@@ -104,6 +104,17 @@
|
|
104 |
.sort-option:hover {
|
105 |
background-color: rgba(59, 130, 246, 0.1);
|
106 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
</style>
|
108 |
</head>
|
109 |
|
@@ -266,187 +277,183 @@
|
|
266 |
|
267 |
<script>
|
268 |
document.addEventListener('DOMContentLoaded', function () {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
269 |
// DOM elements
|
270 |
-
const
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
|
|
|
|
295 |
|
296 |
// State variables
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
}
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
}
|
326 |
-
|
327 |
-
function unhighlight() {
|
328 |
-
dropzone.classList.remove('active');
|
329 |
-
}
|
330 |
-
|
331 |
-
dropzone.addEventListener('drop', handleDrop, false);
|
332 |
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
handleFiles(droppedFiles);
|
337 |
-
}
|
338 |
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
});
|
343 |
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
|
|
350 |
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
sortBtn.setAttribute('aria-expanded', !isExpanded);
|
366 |
-
});
|
367 |
-
|
368 |
-
document.querySelectorAll('.sort-option').forEach(option => {
|
369 |
-
option.addEventListener('click', (e) => {
|
370 |
-
currentSortMethod = e.target.dataset.sort;
|
371 |
-
sortFiles();
|
372 |
-
updateFileList();
|
373 |
-
showImage(currentFileIndex);
|
374 |
-
sortDropdown.classList.add('hidden');
|
375 |
-
sortBtn.setAttribute('aria-expanded', 'false');
|
376 |
});
|
377 |
-
|
378 |
-
|
379 |
-
// Close dropdown when clicking outside
|
380 |
-
document.addEventListener('click', () => {
|
381 |
-
sortDropdown.classList.add('hidden');
|
382 |
-
sortBtn.setAttribute('aria-expanded', 'false');
|
383 |
-
});
|
384 |
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
390 |
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
|
|
395 |
|
396 |
-
|
|
|
|
|
397 |
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
} else {
|
408 |
-
showPreviousImage();
|
409 |
-
}
|
410 |
-
break;
|
411 |
-
case 'ArrowRight':
|
412 |
-
if (fullscreenContainer.style.display === 'flex') {
|
413 |
-
showNextImage();
|
414 |
-
updateFullscreenImage();
|
415 |
-
} else {
|
416 |
-
showNextImage();
|
417 |
-
}
|
418 |
-
break;
|
419 |
-
case '+':
|
420 |
-
case '=':
|
421 |
-
zoomIn();
|
422 |
-
break;
|
423 |
-
case '-':
|
424 |
-
zoomOut();
|
425 |
-
break;
|
426 |
-
case '0':
|
427 |
-
resetZoom();
|
428 |
-
break;
|
429 |
-
case 'Escape':
|
430 |
-
if (fullscreenContainer.style.display === 'flex') {
|
431 |
-
closeFullscreen();
|
432 |
-
}
|
433 |
-
break;
|
434 |
-
case 'f':
|
435 |
-
case 'F':
|
436 |
-
if (imageDisplay.querySelector('img')) {
|
437 |
-
if (fullscreenContainer.style.display === 'flex') {
|
438 |
-
closeFullscreen();
|
439 |
-
} else {
|
440 |
-
openFullscreen();
|
441 |
-
}
|
442 |
-
}
|
443 |
-
break;
|
444 |
-
}
|
445 |
-
});
|
446 |
|
447 |
-
//
|
448 |
-
|
449 |
-
// Filter for supported image types
|
450 |
const supportedTypes = [
|
451 |
'image/webp',
|
452 |
'image/png',
|
@@ -457,10 +464,7 @@
|
|
457 |
];
|
458 |
|
459 |
const imageFiles = Array.from(newFiles).filter(file => {
|
460 |
-
// Check MIME type first
|
461 |
if (supportedTypes.includes(file.type)) return true;
|
462 |
-
|
463 |
-
// Fallback for file extensions (some browsers might not recognize HEIC/AVIF MIME types)
|
464 |
const extension = file.name.split('.').pop().toLowerCase();
|
465 |
return ['webp', 'png', 'jpg', 'jpeg', 'avif', 'heic', 'heif'].includes(extension);
|
466 |
});
|
@@ -470,65 +474,79 @@
|
|
470 |
return;
|
471 |
}
|
472 |
|
473 |
-
files = imageFiles;
|
474 |
-
currentFileIndex = 0;
|
475 |
-
zoomLevel = 1;
|
476 |
|
477 |
-
// Sort files
|
478 |
sortFiles();
|
479 |
-
|
480 |
-
// Update UI
|
481 |
updateFileList();
|
482 |
-
showImage(currentFileIndex);
|
483 |
-
viewerArea.classList.remove('hidden');
|
484 |
-
|
485 |
-
// Scroll to top
|
486 |
window.scrollTo(0, 0);
|
487 |
-
}
|
488 |
|
489 |
-
// Sort
|
490 |
-
|
491 |
-
switch (currentSortMethod) {
|
492 |
case 'name-asc':
|
493 |
-
files.sort((a, b) => a.name.localeCompare(b.name));
|
494 |
break;
|
495 |
case 'name-desc':
|
496 |
-
files.sort((a, b) => b.name.localeCompare(a.name));
|
497 |
break;
|
498 |
case 'size-asc':
|
499 |
-
files.sort((a, b) => a.size - b.size);
|
500 |
break;
|
501 |
case 'size-desc':
|
502 |
-
files.sort((a, b) => b.size - a.size);
|
503 |
break;
|
504 |
case 'date-asc':
|
505 |
-
files.sort((a, b) => a.lastModified - b.lastModified);
|
506 |
break;
|
507 |
case 'date-desc':
|
508 |
-
files.sort((a, b) => b.lastModified - a.lastModified);
|
509 |
break;
|
510 |
}
|
511 |
|
512 |
-
|
513 |
-
|
514 |
-
currentFileIndex = 0;
|
515 |
}
|
516 |
-
}
|
517 |
|
518 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
|
524 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
525 |
const listItem = document.createElement('li');
|
526 |
-
listItem.className = `file-item cursor-pointer ${index === currentFileIndex ? 'active' : ''}`;
|
527 |
listItem.setAttribute('role', 'listitem');
|
528 |
listItem.innerHTML = `
|
529 |
<div class="flex items-center p-3">
|
530 |
-
<div class="flex-shrink-0 h-10 w-10 rounded
|
531 |
-
<
|
|
|
532 |
</div>
|
533 |
<div class="ml-3 overflow-hidden">
|
534 |
<p class="text-sm font-medium text-gray-900 truncate">${file.name}</p>
|
@@ -537,11 +555,7 @@
|
|
537 |
</div>
|
538 |
`;
|
539 |
|
540 |
-
listItem.addEventListener('click', () =>
|
541 |
-
showImage(index);
|
542 |
-
});
|
543 |
-
|
544 |
-
// Keyboard navigation for file list items
|
545 |
listItem.addEventListener('keydown', (e) => {
|
546 |
if (e.key === 'Enter' || e.key === ' ') {
|
547 |
e.preventDefault();
|
@@ -550,26 +564,35 @@
|
|
550 |
});
|
551 |
|
552 |
listItem.setAttribute('tabindex', '0');
|
553 |
-
fileList.appendChild(listItem);
|
554 |
-
|
555 |
-
//
|
556 |
-
const
|
557 |
-
|
558 |
-
const thumbnails = document.querySelectorAll(`.thumbnail[data-index="${index}"]`);
|
559 |
-
thumbnails.forEach(thumb => {
|
560 |
-
thumb.src = e.target.result;
|
561 |
-
});
|
562 |
-
};
|
563 |
-
reader.readAsDataURL(file);
|
564 |
});
|
565 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
566 |
|
567 |
-
//
|
568 |
-
|
569 |
-
if (index < 0 || index >= files.length) return;
|
570 |
|
571 |
-
currentFileIndex = index;
|
572 |
-
const file = files[index];
|
573 |
|
574 |
// Update active item in file list
|
575 |
document.querySelectorAll('.file-item').forEach((item, i) => {
|
@@ -583,168 +606,228 @@
|
|
583 |
});
|
584 |
|
585 |
// Update progress
|
586 |
-
currentIndex.textContent = index + 1;
|
587 |
-
progressBar.style.width = `${((index + 1) / files.length) * 100}%`;
|
588 |
|
589 |
// Display the image
|
590 |
const reader = new FileReader();
|
591 |
reader.onload = (e) => {
|
592 |
-
imageDisplay.innerHTML = '';
|
593 |
|
594 |
const img = document.createElement('img');
|
595 |
img.src = e.target.result;
|
596 |
img.className = 'max-w-full max-h-[70vh] object-contain fade-in';
|
597 |
-
img.style.transform = `scale(${zoomLevel})`;
|
598 |
img.style.transformOrigin = 'center center';
|
599 |
img.style.transition = 'transform 0.2s ease';
|
600 |
img.setAttribute('alt', `Preview of ${file.name}`);
|
601 |
|
602 |
// Add drag to pan functionality
|
603 |
-
|
604 |
-
|
605 |
-
|
606 |
-
img.addEventListener('
|
607 |
-
|
608 |
-
|
609 |
-
isDragging = true;
|
610 |
-
startX = e.clientX - translateX;
|
611 |
-
startY = e.clientY - translateY;
|
612 |
-
img.style.cursor = 'grabbing';
|
613 |
-
});
|
614 |
-
|
615 |
-
document.addEventListener('mousemove', (e) => {
|
616 |
-
if (!isDragging) return;
|
617 |
-
|
618 |
-
translateX = e.clientX - startX;
|
619 |
-
translateY = e.clientY - startY;
|
620 |
-
img.style.transform = `scale(${zoomLevel}) translate(${translateX}px, ${translateY}px)`;
|
621 |
-
});
|
622 |
-
|
623 |
-
document.addEventListener('mouseup', () => {
|
624 |
-
isDragging = false;
|
625 |
-
img.style.cursor = 'grab';
|
626 |
-
});
|
627 |
-
|
628 |
-
img.addEventListener('mouseenter', () => {
|
629 |
-
if (zoomLevel > 1) {
|
630 |
-
img.style.cursor = 'grab';
|
631 |
-
}
|
632 |
-
});
|
633 |
|
634 |
-
|
635 |
-
img.style.cursor = 'default';
|
636 |
-
});
|
637 |
-
|
638 |
-
imageDisplay.appendChild(img);
|
639 |
|
640 |
// Update file info
|
641 |
-
fileName.textContent = file.name;
|
642 |
-
fileInfo.textContent = `${getFileType(file)} • ${formatFileSize(file.size)}`;
|
643 |
|
644 |
// Load image dimensions after the image is loaded
|
645 |
img.onload = () => {
|
646 |
-
fileInfo.textContent = `${img.naturalWidth}×${img.naturalHeight} • ${getFileType(file)} • ${formatFileSize(file.size)}`;
|
647 |
};
|
648 |
};
|
649 |
reader.readAsDataURL(file);
|
650 |
|
651 |
// Enable/disable navigation buttons
|
652 |
-
prevBtn.disabled = index === 0;
|
653 |
-
nextBtn.disabled = index === files.length - 1;
|
654 |
-
}
|
655 |
-
|
656 |
-
//
|
657 |
-
|
658 |
-
if (
|
659 |
-
|
660 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
661 |
}
|
|
|
662 |
|
663 |
-
|
664 |
-
|
665 |
-
|
666 |
-
|
667 |
-
|
668 |
-
|
669 |
-
|
670 |
-
|
671 |
-
|
672 |
-
case 'heif': return 'HEIC';
|
673 |
-
default: return extension.toUpperCase();
|
674 |
}
|
675 |
-
}
|
|
|
|
|
|
|
|
|
676 |
|
677 |
// Navigation functions
|
678 |
-
|
679 |
-
if (currentFileIndex > 0) {
|
680 |
-
showImage(currentFileIndex - 1);
|
681 |
}
|
682 |
-
}
|
683 |
|
684 |
-
|
685 |
-
if (currentFileIndex < files.length - 1) {
|
686 |
-
showImage(currentFileIndex + 1);
|
687 |
}
|
688 |
-
}
|
689 |
|
690 |
// Zoom functions
|
691 |
-
|
692 |
-
if (zoomLevel < maxZoom) {
|
693 |
-
zoomLevel += zoomStep;
|
694 |
applyZoom();
|
695 |
}
|
696 |
-
}
|
697 |
|
698 |
-
|
699 |
-
if (zoomLevel > minZoom) {
|
700 |
-
zoomLevel -= zoomStep;
|
701 |
applyZoom();
|
702 |
}
|
703 |
-
}
|
704 |
|
705 |
-
|
706 |
-
zoomLevel = 1;
|
|
|
|
|
707 |
applyZoom();
|
708 |
-
}
|
709 |
|
710 |
-
|
711 |
-
const img = imageDisplay.querySelector('img');
|
712 |
if (img) {
|
713 |
-
img.style.transform = `scale(${zoomLevel})`;
|
714 |
|
715 |
// Reset pan position when zooming
|
716 |
-
if (zoomLevel <= 1) {
|
717 |
-
|
|
|
|
|
718 |
}
|
719 |
}
|
720 |
-
}
|
721 |
|
722 |
// Fullscreen functions
|
723 |
-
|
724 |
-
const img = imageDisplay.querySelector('img');
|
725 |
if (!img) return;
|
726 |
|
727 |
-
fullscreenImage.src = img.src;
|
728 |
-
fullscreenContainer.style.display = 'flex';
|
729 |
document.body.style.overflow = 'hidden';
|
730 |
-
fullscreenContainer.setAttribute('aria-hidden', 'false');
|
731 |
-
}
|
732 |
|
733 |
-
|
734 |
-
fullscreenContainer.style.display = 'none';
|
735 |
document.body.style.overflow = '';
|
736 |
-
fullscreenContainer.setAttribute('aria-hidden', 'true');
|
737 |
-
}
|
738 |
|
739 |
-
|
740 |
-
const img = imageDisplay.querySelector('img');
|
741 |
if (img) {
|
742 |
-
fullscreenImage.src = img.src;
|
743 |
}
|
744 |
-
}
|
745 |
|
746 |
-
//
|
747 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
748 |
if (bytes === 0) return '0 Bytes';
|
749 |
|
750 |
const k = 1024;
|
@@ -752,7 +835,10 @@
|
|
752 |
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
753 |
|
754 |
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
755 |
-
}
|
|
|
|
|
|
|
756 |
});
|
757 |
</script>
|
758 |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=jsfs11/local-image-viewer" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
|
|
|
5 |
<meta charset="UTF-8">
|
6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
<title>Local Image Viewer</title>
|
8 |
+
<!-- Tailwind CSS -->
|
9 |
<script src="https://cdn.tailwindcss.com"></script>
|
10 |
<script>
|
11 |
tailwind.config = {
|
|
|
18 |
}
|
19 |
}
|
20 |
</script>
|
21 |
+
<!-- Font Awesome -->
|
22 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
23 |
<style>
|
24 |
.dropzone {
|
|
|
104 |
.sort-option:hover {
|
105 |
background-color: rgba(59, 130, 246, 0.1);
|
106 |
}
|
107 |
+
|
108 |
+
.thumbnail-placeholder {
|
109 |
+
background-color: #f3f4f6;
|
110 |
+
display: flex;
|
111 |
+
align-items: center;
|
112 |
+
justify-content: center;
|
113 |
+
}
|
114 |
+
|
115 |
+
.thumbnail-placeholder i {
|
116 |
+
color: #9ca3af;
|
117 |
+
}
|
118 |
</style>
|
119 |
</head>
|
120 |
|
|
|
277 |
|
278 |
<script>
|
279 |
document.addEventListener('DOMContentLoaded', function () {
|
280 |
+
// Utility functions
|
281 |
+
const debounce = (func, delay) => {
|
282 |
+
let timeoutId;
|
283 |
+
return (...args) => {
|
284 |
+
clearTimeout(timeoutId);
|
285 |
+
timeoutId = setTimeout(() => func.apply(this, args), delay);
|
286 |
+
};
|
287 |
+
};
|
288 |
+
|
289 |
+
const throttle = (func, limit) => {
|
290 |
+
let lastFunc;
|
291 |
+
let lastRan;
|
292 |
+
return function() {
|
293 |
+
const context = this;
|
294 |
+
const args = arguments;
|
295 |
+
if (!lastRan) {
|
296 |
+
func.apply(context, args);
|
297 |
+
lastRan = Date.now();
|
298 |
+
} else {
|
299 |
+
clearTimeout(lastFunc);
|
300 |
+
lastFunc = setTimeout(function() {
|
301 |
+
if ((Date.now() - lastRan) >= limit) {
|
302 |
+
func.apply(context, args);
|
303 |
+
lastRan = Date.now();
|
304 |
+
}
|
305 |
+
}, limit - (Date.now() - lastRan));
|
306 |
+
}
|
307 |
+
};
|
308 |
+
};
|
309 |
+
|
310 |
// DOM elements
|
311 |
+
const elements = {
|
312 |
+
dropzone: document.getElementById('dropzone'),
|
313 |
+
browseBtn: document.getElementById('browseBtn'),
|
314 |
+
fileInput: document.getElementById('fileInput'),
|
315 |
+
viewerArea: document.getElementById('viewerArea'),
|
316 |
+
fileList: document.getElementById('fileList'),
|
317 |
+
imageDisplay: document.getElementById('imageDisplay'),
|
318 |
+
fileName: document.getElementById('fileName'),
|
319 |
+
fileInfo: document.getElementById('fileInfo'),
|
320 |
+
fileCount: document.getElementById('fileCount'),
|
321 |
+
prevBtn: document.getElementById('prevBtn'),
|
322 |
+
nextBtn: document.getElementById('nextBtn'),
|
323 |
+
zoomInBtn: document.getElementById('zoomInBtn'),
|
324 |
+
zoomOutBtn: document.getElementById('zoomOutBtn'),
|
325 |
+
resetZoomBtn: document.getElementById('resetZoomBtn'),
|
326 |
+
fullscreenBtn: document.getElementById('fullscreenBtn'),
|
327 |
+
progressBar: document.getElementById('progressBar'),
|
328 |
+
currentIndex: document.getElementById('currentIndex'),
|
329 |
+
totalImages: document.getElementById('totalImages'),
|
330 |
+
sortBtn: document.getElementById('sortBtn'),
|
331 |
+
sortDropdown: document.getElementById('sortDropdown'),
|
332 |
+
fullscreenContainer: document.getElementById('fullscreenContainer'),
|
333 |
+
fullscreenImage: document.getElementById('fullscreenImage'),
|
334 |
+
fsPrevBtn: document.getElementById('fsPrevBtn'),
|
335 |
+
fsNextBtn: document.getElementById('fsNextBtn'),
|
336 |
+
fsCloseBtn: document.getElementById('fsCloseBtn')
|
337 |
+
};
|
338 |
|
339 |
// State variables
|
340 |
+
const state = {
|
341 |
+
files: [],
|
342 |
+
currentFileIndex: -1,
|
343 |
+
zoomLevel: 1,
|
344 |
+
maxZoom: 3,
|
345 |
+
minZoom: 0.5,
|
346 |
+
zoomStep: 0.1,
|
347 |
+
currentSortMethod: 'name-asc',
|
348 |
+
thumbnailObserver: null,
|
349 |
+
isDragging: false,
|
350 |
+
startX: 0,
|
351 |
+
startY: 0,
|
352 |
+
translateX: 0,
|
353 |
+
translateY: 0
|
354 |
+
};
|
355 |
+
|
356 |
+
// Initialize the app
|
357 |
+
const init = () => {
|
358 |
+
setupEventListeners();
|
359 |
+
setupThumbnailObserver();
|
360 |
+
};
|
361 |
+
|
362 |
+
// Set up all event listeners
|
363 |
+
const setupEventListeners = () => {
|
364 |
+
// Dropzone events
|
365 |
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
366 |
+
elements.dropzone.addEventListener(eventName, preventDefaults, false);
|
367 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
368 |
|
369 |
+
['dragenter', 'dragover'].forEach(eventName => {
|
370 |
+
elements.dropzone.addEventListener(eventName, highlight, false);
|
371 |
+
});
|
|
|
|
|
372 |
|
373 |
+
['dragleave', 'drop'].forEach(eventName => {
|
374 |
+
elements.dropzone.addEventListener(eventName, unhighlight, false);
|
375 |
+
});
|
|
|
376 |
|
377 |
+
elements.dropzone.addEventListener('drop', handleDrop, false);
|
378 |
+
elements.browseBtn.addEventListener('click', () => elements.fileInput.click());
|
379 |
+
elements.fileInput.addEventListener('change', () => {
|
380 |
+
if (elements.fileInput.files.length > 0) {
|
381 |
+
handleFiles(elements.fileInput.files);
|
382 |
+
}
|
383 |
+
});
|
384 |
|
385 |
+
// Navigation events
|
386 |
+
elements.prevBtn.addEventListener('click', showPreviousImage);
|
387 |
+
elements.nextBtn.addEventListener('click', showNextImage);
|
388 |
|
389 |
+
// Zoom events with debouncing
|
390 |
+
elements.zoomInBtn.addEventListener('click', debounce(zoomIn, 100));
|
391 |
+
elements.zoomOutBtn.addEventListener('click', debounce(zoomOut, 100));
|
392 |
+
elements.resetZoomBtn.addEventListener('click', resetZoom);
|
393 |
+
elements.fullscreenBtn.addEventListener('click', openFullscreen);
|
394 |
|
395 |
+
// Sort events
|
396 |
+
elements.sortBtn.addEventListener('click', toggleSortDropdown);
|
397 |
+
document.querySelectorAll('.sort-option').forEach(option => {
|
398 |
+
option.addEventListener('click', handleSortOptionClick);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
399 |
});
|
400 |
+
document.addEventListener('click', closeSortDropdown);
|
|
|
|
|
|
|
|
|
|
|
|
|
401 |
|
402 |
+
// Fullscreen events
|
403 |
+
elements.fsPrevBtn.addEventListener('click', () => {
|
404 |
+
showPreviousImage();
|
405 |
+
updateFullscreenImage();
|
406 |
+
});
|
407 |
+
elements.fsNextBtn.addEventListener('click', () => {
|
408 |
+
showNextImage();
|
409 |
+
updateFullscreenImage();
|
410 |
+
});
|
411 |
+
elements.fsCloseBtn.addEventListener('click', closeFullscreen);
|
412 |
+
|
413 |
+
// Keyboard events
|
414 |
+
document.addEventListener('keydown', handleKeyboardNavigation);
|
415 |
+
};
|
416 |
+
|
417 |
+
// Set up Intersection Observer for lazy loading thumbnails
|
418 |
+
const setupThumbnailObserver = () => {
|
419 |
+
state.thumbnailObserver = new IntersectionObserver((entries) => {
|
420 |
+
entries.forEach(entry => {
|
421 |
+
if (entry.isIntersecting) {
|
422 |
+
const thumbnail = entry.target;
|
423 |
+
const index = parseInt(thumbnail.dataset.index);
|
424 |
+
loadThumbnail(index);
|
425 |
+
state.thumbnailObserver.unobserve(thumbnail);
|
426 |
+
}
|
427 |
+
});
|
428 |
+
}, {
|
429 |
+
root: elements.fileList,
|
430 |
+
rootMargin: '100px',
|
431 |
+
threshold: 0.1
|
432 |
+
});
|
433 |
+
};
|
434 |
|
435 |
+
// Dropzone helper functions
|
436 |
+
const preventDefaults = (e) => {
|
437 |
+
e.preventDefault();
|
438 |
+
e.stopPropagation();
|
439 |
+
};
|
440 |
|
441 |
+
const highlight = () => {
|
442 |
+
elements.dropzone.classList.add('active');
|
443 |
+
};
|
444 |
|
445 |
+
const unhighlight = () => {
|
446 |
+
elements.dropzone.classList.remove('active');
|
447 |
+
};
|
448 |
|
449 |
+
const handleDrop = (e) => {
|
450 |
+
const dt = e.dataTransfer;
|
451 |
+
const droppedFiles = dt.files;
|
452 |
+
handleFiles(droppedFiles);
|
453 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
454 |
|
455 |
+
// File handling
|
456 |
+
const handleFiles = (newFiles) => {
|
|
|
457 |
const supportedTypes = [
|
458 |
'image/webp',
|
459 |
'image/png',
|
|
|
464 |
];
|
465 |
|
466 |
const imageFiles = Array.from(newFiles).filter(file => {
|
|
|
467 |
if (supportedTypes.includes(file.type)) return true;
|
|
|
|
|
468 |
const extension = file.name.split('.').pop().toLowerCase();
|
469 |
return ['webp', 'png', 'jpg', 'jpeg', 'avif', 'heic', 'heif'].includes(extension);
|
470 |
});
|
|
|
474 |
return;
|
475 |
}
|
476 |
|
477 |
+
state.files = imageFiles;
|
478 |
+
state.currentFileIndex = 0;
|
479 |
+
state.zoomLevel = 1;
|
480 |
|
|
|
481 |
sortFiles();
|
|
|
|
|
482 |
updateFileList();
|
483 |
+
showImage(state.currentFileIndex);
|
484 |
+
elements.viewerArea.classList.remove('hidden');
|
|
|
|
|
485 |
window.scrollTo(0, 0);
|
486 |
+
};
|
487 |
|
488 |
+
// Sort functionality
|
489 |
+
const sortFiles = () => {
|
490 |
+
switch (state.currentSortMethod) {
|
491 |
case 'name-asc':
|
492 |
+
state.files.sort((a, b) => a.name.localeCompare(b.name));
|
493 |
break;
|
494 |
case 'name-desc':
|
495 |
+
state.files.sort((a, b) => b.name.localeCompare(a.name));
|
496 |
break;
|
497 |
case 'size-asc':
|
498 |
+
state.files.sort((a, b) => a.size - b.size);
|
499 |
break;
|
500 |
case 'size-desc':
|
501 |
+
state.files.sort((a, b) => b.size - a.size);
|
502 |
break;
|
503 |
case 'date-asc':
|
504 |
+
state.files.sort((a, b) => a.lastModified - b.lastModified);
|
505 |
break;
|
506 |
case 'date-desc':
|
507 |
+
state.files.sort((a, b) => b.lastModified - a.lastModified);
|
508 |
break;
|
509 |
}
|
510 |
|
511 |
+
if (state.currentFileIndex >= 0 && state.files.length > 0) {
|
512 |
+
state.currentFileIndex = 0;
|
|
|
513 |
}
|
514 |
+
};
|
515 |
|
516 |
+
const toggleSortDropdown = (e) => {
|
517 |
+
e.stopPropagation();
|
518 |
+
const isExpanded = elements.sortDropdown.classList.toggle('hidden');
|
519 |
+
elements.sortBtn.setAttribute('aria-expanded', !isExpanded);
|
520 |
+
};
|
521 |
|
522 |
+
const handleSortOptionClick = (e) => {
|
523 |
+
state.currentSortMethod = e.target.dataset.sort;
|
524 |
+
sortFiles();
|
525 |
+
updateFileList();
|
526 |
+
showImage(state.currentFileIndex);
|
527 |
+
closeSortDropdown();
|
528 |
+
};
|
529 |
+
|
530 |
+
const closeSortDropdown = () => {
|
531 |
+
elements.sortDropdown.classList.add('hidden');
|
532 |
+
elements.sortBtn.setAttribute('aria-expanded', 'false');
|
533 |
+
};
|
534 |
+
|
535 |
+
// File list management
|
536 |
+
const updateFileList = () => {
|
537 |
+
elements.fileList.innerHTML = '';
|
538 |
+
elements.fileCount.textContent = state.files.length;
|
539 |
+
elements.totalImages.textContent = state.files.length;
|
540 |
+
|
541 |
+
state.files.forEach((file, index) => {
|
542 |
const listItem = document.createElement('li');
|
543 |
+
listItem.className = `file-item cursor-pointer ${index === state.currentFileIndex ? 'active' : ''}`;
|
544 |
listItem.setAttribute('role', 'listitem');
|
545 |
listItem.innerHTML = `
|
546 |
<div class="flex items-center p-3">
|
547 |
+
<div class="flex-shrink-0 h-10 w-10 rounded overflow-hidden thumbnail-placeholder">
|
548 |
+
<i class="fas fa-image text-lg"></i>
|
549 |
+
<img src="#" alt="Thumbnail" class="h-full w-full object-cover hidden thumbnail" data-index="${index}">
|
550 |
</div>
|
551 |
<div class="ml-3 overflow-hidden">
|
552 |
<p class="text-sm font-medium text-gray-900 truncate">${file.name}</p>
|
|
|
555 |
</div>
|
556 |
`;
|
557 |
|
558 |
+
listItem.addEventListener('click', () => showImage(index));
|
|
|
|
|
|
|
|
|
559 |
listItem.addEventListener('keydown', (e) => {
|
560 |
if (e.key === 'Enter' || e.key === ' ') {
|
561 |
e.preventDefault();
|
|
|
564 |
});
|
565 |
|
566 |
listItem.setAttribute('tabindex', '0');
|
567 |
+
elements.fileList.appendChild(listItem);
|
568 |
+
|
569 |
+
// Observe the thumbnail for lazy loading
|
570 |
+
const thumbnail = listItem.querySelector('.thumbnail');
|
571 |
+
state.thumbnailObserver.observe(thumbnail);
|
|
|
|
|
|
|
|
|
|
|
|
|
572 |
});
|
573 |
+
};
|
574 |
+
|
575 |
+
// Lazy load thumbnail when it comes into view
|
576 |
+
const loadThumbnail = (index) => {
|
577 |
+
const thumbnail = document.querySelector(`.thumbnail[data-index="${index}"]`);
|
578 |
+
if (!thumbnail || thumbnail.src !== '#') return;
|
579 |
+
|
580 |
+
const file = state.files[index];
|
581 |
+
const reader = new FileReader();
|
582 |
+
reader.onload = (e) => {
|
583 |
+
thumbnail.src = e.target.result;
|
584 |
+
thumbnail.classList.remove('hidden');
|
585 |
+
thumbnail.previousElementSibling?.remove();
|
586 |
+
};
|
587 |
+
reader.readAsDataURL(file);
|
588 |
+
};
|
589 |
|
590 |
+
// Image display
|
591 |
+
const showImage = (index) => {
|
592 |
+
if (index < 0 || index >= state.files.length) return;
|
593 |
|
594 |
+
state.currentFileIndex = index;
|
595 |
+
const file = state.files[index];
|
596 |
|
597 |
// Update active item in file list
|
598 |
document.querySelectorAll('.file-item').forEach((item, i) => {
|
|
|
606 |
});
|
607 |
|
608 |
// Update progress
|
609 |
+
elements.currentIndex.textContent = index + 1;
|
610 |
+
elements.progressBar.style.width = `${((index + 1) / state.files.length) * 100}%`;
|
611 |
|
612 |
// Display the image
|
613 |
const reader = new FileReader();
|
614 |
reader.onload = (e) => {
|
615 |
+
elements.imageDisplay.innerHTML = '';
|
616 |
|
617 |
const img = document.createElement('img');
|
618 |
img.src = e.target.result;
|
619 |
img.className = 'max-w-full max-h-[70vh] object-contain fade-in';
|
620 |
+
img.style.transform = `scale(${state.zoomLevel})`;
|
621 |
img.style.transformOrigin = 'center center';
|
622 |
img.style.transition = 'transform 0.2s ease';
|
623 |
img.setAttribute('alt', `Preview of ${file.name}`);
|
624 |
|
625 |
// Add drag to pan functionality
|
626 |
+
img.addEventListener('mousedown', handleImageMouseDown);
|
627 |
+
document.addEventListener('mousemove', throttle(handleImageMouseMove, 16));
|
628 |
+
document.addEventListener('mouseup', handleImageMouseUp);
|
629 |
+
img.addEventListener('mouseenter', handleImageMouseEnter);
|
630 |
+
img.addEventListener('mouseleave', handleImageMouseLeave);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
631 |
|
632 |
+
elements.imageDisplay.appendChild(img);
|
|
|
|
|
|
|
|
|
633 |
|
634 |
// Update file info
|
635 |
+
elements.fileName.textContent = file.name;
|
636 |
+
elements.fileInfo.textContent = `${getFileType(file)} • ${formatFileSize(file.size)}`;
|
637 |
|
638 |
// Load image dimensions after the image is loaded
|
639 |
img.onload = () => {
|
640 |
+
elements.fileInfo.textContent = `${img.naturalWidth}×${img.naturalHeight} • ${getFileType(file)} • ${formatFileSize(file.size)}`;
|
641 |
};
|
642 |
};
|
643 |
reader.readAsDataURL(file);
|
644 |
|
645 |
// Enable/disable navigation buttons
|
646 |
+
elements.prevBtn.disabled = index === 0;
|
647 |
+
elements.nextBtn.disabled = index === state.files.length - 1;
|
648 |
+
};
|
649 |
+
|
650 |
+
// Image drag handlers
|
651 |
+
const handleImageMouseDown = (e) => {
|
652 |
+
if (state.zoomLevel <= 1) return;
|
653 |
+
|
654 |
+
state.isDragging = true;
|
655 |
+
state.startX = e.clientX - state.translateX;
|
656 |
+
state.startY = e.clientY - state.translateY;
|
657 |
+
e.target.style.cursor = 'grabbing';
|
658 |
+
};
|
659 |
+
|
660 |
+
const handleImageMouseMove = (e) => {
|
661 |
+
if (!state.isDragging) return;
|
662 |
+
|
663 |
+
state.translateX = e.clientX - state.startX;
|
664 |
+
state.translateY = e.clientY - state.startY;
|
665 |
+
|
666 |
+
const img = elements.imageDisplay.querySelector('img');
|
667 |
+
if (img) {
|
668 |
+
img.style.transform = `scale(${state.zoomLevel}) translate(${state.translateX}px, ${state.translateY}px)`;
|
669 |
}
|
670 |
+
};
|
671 |
|
672 |
+
const handleImageMouseUp = () => {
|
673 |
+
state.isDragging = false;
|
674 |
+
const img = elements.imageDisplay.querySelector('img');
|
675 |
+
if (img) img.style.cursor = 'grab';
|
676 |
+
};
|
677 |
+
|
678 |
+
const handleImageMouseEnter = (e) => {
|
679 |
+
if (state.zoomLevel > 1) {
|
680 |
+
e.target.style.cursor = 'grab';
|
|
|
|
|
681 |
}
|
682 |
+
};
|
683 |
+
|
684 |
+
const handleImageMouseLeave = (e) => {
|
685 |
+
e.target.style.cursor = 'default';
|
686 |
+
};
|
687 |
|
688 |
// Navigation functions
|
689 |
+
const showPreviousImage = () => {
|
690 |
+
if (state.currentFileIndex > 0) {
|
691 |
+
showImage(state.currentFileIndex - 1);
|
692 |
}
|
693 |
+
};
|
694 |
|
695 |
+
const showNextImage = () => {
|
696 |
+
if (state.currentFileIndex < state.files.length - 1) {
|
697 |
+
showImage(state.currentFileIndex + 1);
|
698 |
}
|
699 |
+
};
|
700 |
|
701 |
// Zoom functions
|
702 |
+
const zoomIn = () => {
|
703 |
+
if (state.zoomLevel < state.maxZoom) {
|
704 |
+
state.zoomLevel += state.zoomStep;
|
705 |
applyZoom();
|
706 |
}
|
707 |
+
};
|
708 |
|
709 |
+
const zoomOut = () => {
|
710 |
+
if (state.zoomLevel > state.minZoom) {
|
711 |
+
state.zoomLevel -= state.zoomStep;
|
712 |
applyZoom();
|
713 |
}
|
714 |
+
};
|
715 |
|
716 |
+
const resetZoom = () => {
|
717 |
+
state.zoomLevel = 1;
|
718 |
+
state.translateX = 0;
|
719 |
+
state.translateY = 0;
|
720 |
applyZoom();
|
721 |
+
};
|
722 |
|
723 |
+
const applyZoom = () => {
|
724 |
+
const img = elements.imageDisplay.querySelector('img');
|
725 |
if (img) {
|
726 |
+
img.style.transform = `scale(${state.zoomLevel}) translate(${state.translateX}px, ${state.translateY}px)`;
|
727 |
|
728 |
// Reset pan position when zooming
|
729 |
+
if (state.zoomLevel <= 1) {
|
730 |
+
state.translateX = 0;
|
731 |
+
state.translateY = 0;
|
732 |
+
img.style.transform = `scale(${state.zoomLevel}) translate(0, 0)`;
|
733 |
}
|
734 |
}
|
735 |
+
};
|
736 |
|
737 |
// Fullscreen functions
|
738 |
+
const openFullscreen = () => {
|
739 |
+
const img = elements.imageDisplay.querySelector('img');
|
740 |
if (!img) return;
|
741 |
|
742 |
+
elements.fullscreenImage.src = img.src;
|
743 |
+
elements.fullscreenContainer.style.display = 'flex';
|
744 |
document.body.style.overflow = 'hidden';
|
745 |
+
elements.fullscreenContainer.setAttribute('aria-hidden', 'false');
|
746 |
+
};
|
747 |
|
748 |
+
const closeFullscreen = () => {
|
749 |
+
elements.fullscreenContainer.style.display = 'none';
|
750 |
document.body.style.overflow = '';
|
751 |
+
elements.fullscreenContainer.setAttribute('aria-hidden', 'true');
|
752 |
+
};
|
753 |
|
754 |
+
const updateFullscreenImage = () => {
|
755 |
+
const img = elements.imageDisplay.querySelector('img');
|
756 |
if (img) {
|
757 |
+
elements.fullscreenImage.src = img.src;
|
758 |
}
|
759 |
+
};
|
760 |
|
761 |
+
// Keyboard navigation
|
762 |
+
const handleKeyboardNavigation = (e) => {
|
763 |
+
if (state.files.length === 0) return;
|
764 |
+
|
765 |
+
switch (e.key) {
|
766 |
+
case 'ArrowLeft':
|
767 |
+
if (elements.fullscreenContainer.style.display === 'flex') {
|
768 |
+
showPreviousImage();
|
769 |
+
updateFullscreenImage();
|
770 |
+
} else {
|
771 |
+
showPreviousImage();
|
772 |
+
}
|
773 |
+
break;
|
774 |
+
case 'ArrowRight':
|
775 |
+
if (elements.fullscreenContainer.style.display === 'flex') {
|
776 |
+
showNextImage();
|
777 |
+
updateFullscreenImage();
|
778 |
+
} else {
|
779 |
+
showNextImage();
|
780 |
+
}
|
781 |
+
break;
|
782 |
+
case '+':
|
783 |
+
case '=':
|
784 |
+
zoomIn();
|
785 |
+
break;
|
786 |
+
case '-':
|
787 |
+
zoomOut();
|
788 |
+
break;
|
789 |
+
case '0':
|
790 |
+
resetZoom();
|
791 |
+
break;
|
792 |
+
case 'Escape':
|
793 |
+
if (elements.fullscreenContainer.style.display === 'flex') {
|
794 |
+
closeFullscreen();
|
795 |
+
}
|
796 |
+
break;
|
797 |
+
case 'f':
|
798 |
+
case 'F':
|
799 |
+
if (elements.imageDisplay.querySelector('img')) {
|
800 |
+
if (elements.fullscreenContainer.style.display === 'flex') {
|
801 |
+
closeFullscreen();
|
802 |
+
} else {
|
803 |
+
openFullscreen();
|
804 |
+
}
|
805 |
+
}
|
806 |
+
break;
|
807 |
+
}
|
808 |
+
};
|
809 |
+
|
810 |
+
// Helper functions
|
811 |
+
const getFileType = (file) => {
|
812 |
+
if (file.type) {
|
813 |
+
const type = file.type.split('/')[1];
|
814 |
+
if (type) return type.toUpperCase();
|
815 |
+
}
|
816 |
+
|
817 |
+
const extension = file.name.split('.').pop().toLowerCase();
|
818 |
+
switch (extension) {
|
819 |
+
case 'jpg':
|
820 |
+
case 'jpeg': return 'JPEG';
|
821 |
+
case 'png': return 'PNG';
|
822 |
+
case 'webp': return 'WEBP';
|
823 |
+
case 'avif': return 'AVIF';
|
824 |
+
case 'heic':
|
825 |
+
case 'heif': return 'HEIC';
|
826 |
+
default: return extension.toUpperCase();
|
827 |
+
}
|
828 |
+
};
|
829 |
+
|
830 |
+
const formatFileSize = (bytes) => {
|
831 |
if (bytes === 0) return '0 Bytes';
|
832 |
|
833 |
const k = 1024;
|
|
|
835 |
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
836 |
|
837 |
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
838 |
+
};
|
839 |
+
|
840 |
+
// Initialize the application
|
841 |
+
init();
|
842 |
});
|
843 |
</script>
|
844 |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=jsfs11/local-image-viewer" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
|