jsfs11 commited on
Commit
27e87f8
·
verified ·
1 Parent(s): 2a1eee8

Add 1 files

Browse files
Files changed (1) hide show
  1. 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
- <!-- Local Tailwind CSS -->
9
  <script src="https://cdn.tailwindcss.com"></script>
10
  <script>
11
  tailwind.config = {
@@ -18,7 +18,7 @@
18
  }
19
  }
20
  </script>
21
- <!-- Local 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,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 dropzone = document.getElementById('dropzone');
271
- const browseBtn = document.getElementById('browseBtn');
272
- const fileInput = document.getElementById('fileInput');
273
- const viewerArea = document.getElementById('viewerArea');
274
- const fileList = document.getElementById('fileList');
275
- const imageDisplay = document.getElementById('imageDisplay');
276
- const fileName = document.getElementById('fileName');
277
- const fileInfo = document.getElementById('fileInfo');
278
- const fileCount = document.getElementById('fileCount');
279
- const prevBtn = document.getElementById('prevBtn');
280
- const nextBtn = document.getElementById('nextBtn');
281
- const zoomInBtn = document.getElementById('zoomInBtn');
282
- const zoomOutBtn = document.getElementById('zoomOutBtn');
283
- const resetZoomBtn = document.getElementById('resetZoomBtn');
284
- const fullscreenBtn = document.getElementById('fullscreenBtn');
285
- const progressBar = document.getElementById('progressBar');
286
- const currentIndex = document.getElementById('currentIndex');
287
- const totalImages = document.getElementById('totalImages');
288
- const sortBtn = document.getElementById('sortBtn');
289
- const sortDropdown = document.getElementById('sortDropdown');
290
- const fullscreenContainer = document.getElementById('fullscreenContainer');
291
- const fullscreenImage = document.getElementById('fullscreenImage');
292
- const fsPrevBtn = document.getElementById('fsPrevBtn');
293
- const fsNextBtn = document.getElementById('fsNextBtn');
294
- const fsCloseBtn = document.getElementById('fsCloseBtn');
 
 
295
 
296
  // State variables
297
- let files = [];
298
- let currentFileIndex = -1;
299
- let zoomLevel = 1;
300
- const maxZoom = 3;
301
- const minZoom = 0.5;
302
- const zoomStep = 0.1;
303
- let currentSortMethod = 'name-asc';
304
-
305
- // Event listeners for dropzone
306
- ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
307
- dropzone.addEventListener(eventName, preventDefaults, false);
308
- });
309
-
310
- function preventDefaults(e) {
311
- e.preventDefault();
312
- e.stopPropagation();
313
- }
314
-
315
- ['dragenter', 'dragover'].forEach(eventName => {
316
- dropzone.addEventListener(eventName, highlight, false);
317
- });
318
-
319
- ['dragleave', 'drop'].forEach(eventName => {
320
- dropzone.addEventListener(eventName, unhighlight, false);
321
- });
322
-
323
- function highlight() {
324
- dropzone.classList.add('active');
325
- }
326
-
327
- function unhighlight() {
328
- dropzone.classList.remove('active');
329
- }
330
-
331
- dropzone.addEventListener('drop', handleDrop, false);
332
 
333
- function handleDrop(e) {
334
- const dt = e.dataTransfer;
335
- const droppedFiles = dt.files;
336
- handleFiles(droppedFiles);
337
- }
338
 
339
- // Browse button click
340
- browseBtn.addEventListener('click', () => {
341
- fileInput.click();
342
- });
343
 
344
- // File input change
345
- fileInput.addEventListener('change', () => {
346
- if (fileInput.files.length > 0) {
347
- handleFiles(fileInput.files);
348
- }
349
- });
 
350
 
351
- // Navigation buttons
352
- prevBtn.addEventListener('click', showPreviousImage);
353
- nextBtn.addEventListener('click', showNextImage);
354
 
355
- // Zoom buttons
356
- zoomInBtn.addEventListener('click', zoomIn);
357
- zoomOutBtn.addEventListener('click', zoomOut);
358
- resetZoomBtn.addEventListener('click', resetZoom);
359
- fullscreenBtn.addEventListener('click', openFullscreen);
360
 
361
- // Sort functionality
362
- sortBtn.addEventListener('click', (e) => {
363
- e.stopPropagation();
364
- const isExpanded = sortDropdown.classList.toggle('hidden');
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
- // Fullscreen functionality
386
- fsPrevBtn.addEventListener('click', () => {
387
- showPreviousImage();
388
- updateFullscreenImage();
389
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
- fsNextBtn.addEventListener('click', () => {
392
- showNextImage();
393
- updateFullscreenImage();
394
- });
 
395
 
396
- fsCloseBtn.addEventListener('click', closeFullscreen);
 
 
397
 
398
- // Keyboard navigation
399
- document.addEventListener('keydown', (e) => {
400
- if (files.length === 0) return;
401
 
402
- switch (e.key) {
403
- case 'ArrowLeft':
404
- if (fullscreenContainer.style.display === 'flex') {
405
- showPreviousImage();
406
- updateFullscreenImage();
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
- // Handle dropped or selected files
448
- function handleFiles(newFiles) {
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 files based on current sort method
490
- function sortFiles() {
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
- // Update current file index to maintain selection
513
- if (currentFileIndex >= 0 && files.length > 0) {
514
- currentFileIndex = 0;
515
  }
516
- }
517
 
518
- // Update the file list sidebar
519
- function updateFileList() {
520
- fileList.innerHTML = '';
521
- fileCount.textContent = files.length;
522
- totalImages.textContent = files.length;
523
 
524
- files.forEach((file, index) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 bg-gray-200 overflow-hidden">
531
- <img src="#" alt="Thumbnail" class="h-full w-full object-cover thumbnail" data-index="${index}">
 
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
- // Load thumbnail
556
- const reader = new FileReader();
557
- reader.onload = (e) => {
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
- // Show image at specified index
568
- function showImage(index) {
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
- let isDragging = false;
604
- let startX, startY, translateX = 0, translateY = 0;
605
-
606
- img.addEventListener('mousedown', (e) => {
607
- if (zoomLevel <= 1) return;
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
- img.addEventListener('mouseleave', () => {
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
- // Helper to get file type
657
- function getFileType(file) {
658
- if (file.type) {
659
- const type = file.type.split('/')[1];
660
- if (type) return type.toUpperCase();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
661
  }
 
662
 
663
- // Fallback for file extension
664
- const extension = file.name.split('.').pop().toLowerCase();
665
- switch (extension) {
666
- case 'jpg':
667
- case 'jpeg': return 'JPEG';
668
- case 'png': return 'PNG';
669
- case 'webp': return 'WEBP';
670
- case 'avif': return 'AVIF';
671
- case 'heic':
672
- case 'heif': return 'HEIC';
673
- default: return extension.toUpperCase();
674
  }
675
- }
 
 
 
 
676
 
677
  // Navigation functions
678
- function showPreviousImage() {
679
- if (currentFileIndex > 0) {
680
- showImage(currentFileIndex - 1);
681
  }
682
- }
683
 
684
- function showNextImage() {
685
- if (currentFileIndex < files.length - 1) {
686
- showImage(currentFileIndex + 1);
687
  }
688
- }
689
 
690
  // Zoom functions
691
- function zoomIn() {
692
- if (zoomLevel < maxZoom) {
693
- zoomLevel += zoomStep;
694
  applyZoom();
695
  }
696
- }
697
 
698
- function zoomOut() {
699
- if (zoomLevel > minZoom) {
700
- zoomLevel -= zoomStep;
701
  applyZoom();
702
  }
703
- }
704
 
705
- function resetZoom() {
706
- zoomLevel = 1;
 
 
707
  applyZoom();
708
- }
709
 
710
- function applyZoom() {
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
- img.style.transform = `scale(${zoomLevel}) translate(0, 0)`;
 
 
718
  }
719
  }
720
- }
721
 
722
  // Fullscreen functions
723
- function openFullscreen() {
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
- function closeFullscreen() {
734
- fullscreenContainer.style.display = 'none';
735
  document.body.style.overflow = '';
736
- fullscreenContainer.setAttribute('aria-hidden', 'true');
737
- }
738
 
739
- function updateFullscreenImage() {
740
- const img = imageDisplay.querySelector('img');
741
  if (img) {
742
- fullscreenImage.src = img.src;
743
  }
744
- }
745
 
746
- // Helper function to format file size
747
- function formatFileSize(bytes) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>