jsfs11 commited on
Commit
2a1eee8
·
verified ·
1 Parent(s): 0e5135e

Add 1 files

Browse files
Files changed (1) hide show
  1. index.html +163 -110
index.html CHANGED
@@ -1,5 +1,6 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -24,39 +25,55 @@
24
  border: 3px dashed #9CA3AF;
25
  transition: all 0.3s ease;
26
  }
 
27
  .dropzone.active {
28
  border-color: #3B82F6;
29
  background-color: rgba(59, 130, 246, 0.1);
30
  }
 
31
  .image-container {
32
  transition: transform 0.3s ease;
33
  }
 
34
  .image-container:hover {
35
  transform: scale(1.01);
36
  }
 
37
  .nav-btn {
38
  transition: all 0.2s ease;
39
  }
 
40
  .nav-btn:hover {
41
  transform: scale(1.1);
42
  }
 
43
  .nav-btn:active {
44
  transform: scale(0.95);
45
  }
 
46
  .file-item:hover {
47
  background-color: rgba(59, 130, 246, 0.1);
48
  }
 
49
  .file-item.active {
50
  background-color: rgba(59, 130, 246, 0.2);
51
  border-left: 3px solid #3B82F6;
52
  }
 
53
  @keyframes fadeIn {
54
- from { opacity: 0; }
55
- to { opacity: 1; }
 
 
 
 
 
56
  }
 
57
  .fade-in {
58
  animation: fadeIn 0.3s ease-in-out;
59
  }
 
60
  #fullscreenContainer {
61
  display: none;
62
  position: fixed;
@@ -70,42 +87,51 @@
70
  align-items: center;
71
  flex-direction: column;
72
  }
 
73
  #fullscreenImage {
74
  max-width: 90%;
75
  max-height: 90%;
76
  object-fit: contain;
77
  }
 
78
  #fullscreenControls {
79
  position: absolute;
80
  bottom: 20px;
81
  display: flex;
82
  gap: 10px;
83
  }
 
84
  .sort-option:hover {
85
  background-color: rgba(59, 130, 246, 0.1);
86
  }
87
  </style>
88
  </head>
 
89
  <body class="bg-gray-100 min-h-screen">
90
  <div class="container mx-auto px-4 py-8">
91
  <div class="max-w-6xl mx-auto">
92
  <h1 class="text-3xl font-bold text-center text-gray-800 mb-2">Local Image Viewer</h1>
93
- <p class="text-center text-gray-600 mb-8">View your local WebP, PNG, JPEG, AVIF, and HEIC files with ease</p>
94
-
 
95
  <div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8">
96
  <!-- Dropzone area -->
97
- <div id="dropzone" class="dropzone p-12 text-center cursor-pointer" role="region" aria-label="File drop zone">
 
98
  <div class="flex flex-col items-center justify-center">
99
  <i class="fas fa-images text-5xl text-gray-400 mb-4" aria-hidden="true"></i>
100
  <h3 class="text-xl font-semibold text-gray-700 mb-2">Drag & Drop Images Here</h3>
101
  <p class="text-gray-500 mb-4">or</p>
102
- <button id="browseBtn" class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-6 rounded-lg transition" aria-label="Browse files">
 
 
103
  Browse Files
104
  </button>
105
- <input type="file" id="fileInput" class="hidden" accept=".webp,.png,.jpg,.jpeg,.avif,.heic,.heif" multiple>
 
106
  </div>
107
  </div>
108
-
109
  <!-- Main viewer area (hidden initially) -->
110
  <div id="viewerArea" class="hidden">
111
  <div class="flex flex-col md:flex-row">
@@ -114,16 +140,24 @@
114
  <div class="p-4 border-b border-gray-200 flex justify-between items-center">
115
  <h3 class="font-medium text-gray-700">Files (<span id="fileCount">0</span>)</h3>
116
  <div class="relative">
117
- <button id="sortBtn" class="text-gray-600 hover:text-gray-800" aria-label="Sort options" aria-haspopup="true" aria-expanded="false">
 
118
  <i class="fas fa-sort" aria-hidden="true"></i>
119
  </button>
120
- <div id="sortDropdown" class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg z-10 py-1">
121
- <div class="sort-option px-4 py-2 text-sm text-gray-700 cursor-pointer" data-sort="name-asc" role="menuitem">Name (A-Z)</div>
122
- <div class="sort-option px-4 py-2 text-sm text-gray-700 cursor-pointer" data-sort="name-desc" role="menuitem">Name (Z-A)</div>
123
- <div class="sort-option px-4 py-2 text-sm text-gray-700 cursor-pointer" data-sort="size-asc" role="menuitem">Size (Small to Large)</div>
124
- <div class="sort-option px-4 py-2 text-sm text-gray-700 cursor-pointer" data-sort="size-desc" role="menuitem">Size (Large to Small)</div>
125
- <div class="sort-option px-4 py-2 text-sm text-gray-700 cursor-pointer" data-sort="date-asc" role="menuitem">Date (Oldest First)</div>
126
- <div class="sort-option px-4 py-2 text-sm text-gray-700 cursor-pointer" data-sort="date-desc" role="menuitem">Date (Newest First)</div>
 
 
 
 
 
 
 
127
  </div>
128
  </div>
129
  </div>
@@ -131,51 +165,66 @@
131
  <!-- Files will be listed here -->
132
  </ul>
133
  </div>
134
-
135
  <!-- Main image display -->
136
  <div class="w-full md:w-3/4 p-4 flex flex-col items-center justify-center">
137
  <div class="relative w-full max-w-3xl">
138
  <!-- Navigation buttons -->
139
- <button id="prevBtn" class="nav-btn absolute left-0 top-1/2 -translate-y-1/2 bg-white/80 hover:bg-white text-gray-800 p-3 rounded-full shadow-md ml-4 z-10" aria-label="Previous image">
 
 
140
  <i class="fas fa-chevron-left text-xl" aria-hidden="true"></i>
141
  </button>
142
-
143
- <button id="nextBtn" class="nav-btn absolute right-0 top-1/2 -translate-y-1/2 bg-white/80 hover:bg-white text-gray-800 p-3 rounded-full shadow-md mr-4 z-10" aria-label="Next image">
 
 
144
  <i class="fas fa-chevron-right text-xl" aria-hidden="true"></i>
145
  </button>
146
-
147
  <!-- Image display area -->
148
- <div class="image-container bg-gray-100 rounded-lg overflow-hidden flex items-center justify-center" style="min-height: 400px;">
 
149
  <div id="imageDisplay" class="p-4 w-full h-full flex items-center justify-center">
150
  <p class="text-gray-500">Select an image to view</p>
151
  </div>
152
  </div>
153
-
154
  <!-- Image info -->
155
  <div class="mt-4 bg-gray-50 rounded-lg p-3">
156
  <div class="flex justify-between items-center">
157
  <div>
158
- <h4 id="fileName" class="font-medium text-gray-800 truncate">No image selected</h4>
 
159
  <p id="fileInfo" class="text-sm text-gray-500">-</p>
160
  </div>
161
  <div class="flex space-x-2">
162
- <button id="zoomInBtn" class="nav-btn bg-gray-200 hover:bg-gray-300 text-gray-700 p-2 rounded" aria-label="Zoom in">
 
 
163
  <i class="fas fa-search-plus" aria-hidden="true"></i>
164
  </button>
165
- <button id="zoomOutBtn" class="nav-btn bg-gray-200 hover:bg-gray-300 text-gray-700 p-2 rounded" aria-label="Zoom out">
 
 
166
  <i class="fas fa-search-minus" aria-hidden="true"></i>
167
  </button>
168
- <button id="resetZoomBtn" class="nav-btn bg-gray-200 hover:bg-gray-300 text-gray-700 p-2 rounded" aria-label="Reset zoom">
 
 
169
  <i class="fas fa-expand" aria-hidden="true"></i>
170
  </button>
171
- <button id="fullscreenBtn" class="nav-btn bg-gray-200 hover:bg-gray-300 text-gray-700 p-2 rounded" aria-label="Fullscreen">
 
 
172
  <i class="fas fa-expand-arrows-alt" aria-hidden="true"></i>
173
  </button>
174
  </div>
175
  </div>
176
  <div class="mt-2">
177
  <div class="w-full bg-gray-200 rounded-full h-2">
178
- <div id="progressBar" class="bg-blue-500 h-2 rounded-full" style="width: 0%"></div>
 
179
  </div>
180
  <div class="flex justify-between text-xs text-gray-500 mt-1">
181
  <span id="currentIndex">0</span>
@@ -188,7 +237,7 @@
188
  </div>
189
  </div>
190
  </div>
191
-
192
  <div class="text-center text-gray-500 text-sm mt-8">
193
  <p>Use arrow keys to navigate between images</p>
194
  <p class="mt-1">Supported formats: WebP, PNG, JPEG, AVIF, HEIC</p>
@@ -200,20 +249,23 @@
200
  <div id="fullscreenContainer" role="dialog" aria-modal="true" aria-label="Fullscreen image viewer">
201
  <img id="fullscreenImage" src="" alt="Fullscreen Image">
202
  <div id="fullscreenControls">
203
- <button id="fsPrevBtn" class="nav-btn bg-white/20 hover:bg-white/40 text-white p-3 rounded-full" aria-label="Previous image">
 
204
  <i class="fas fa-chevron-left text-xl" aria-hidden="true"></i>
205
  </button>
206
- <button id="fsCloseBtn" class="nav-btn bg-white/20 hover:bg-white/40 text-white p-3 rounded-full" aria-label="Close fullscreen">
 
207
  <i class="fas fa-times text-xl" aria-hidden="true"></i>
208
  </button>
209
- <button id="fsNextBtn" class="nav-btn bg-white/20 hover:bg-white/40 text-white p-3 rounded-full" aria-label="Next image">
 
210
  <i class="fas fa-chevron-right text-xl" aria-hidden="true"></i>
211
  </button>
212
  </div>
213
  </div>
214
 
215
  <script>
216
- document.addEventListener('DOMContentLoaded', function() {
217
  // DOM elements
218
  const dropzone = document.getElementById('dropzone');
219
  const browseBtn = document.getElementById('browseBtn');
@@ -240,7 +292,7 @@
240
  const fsPrevBtn = document.getElementById('fsPrevBtn');
241
  const fsNextBtn = document.getElementById('fsNextBtn');
242
  const fsCloseBtn = document.getElementById('fsCloseBtn');
243
-
244
  // State variables
245
  let files = [];
246
  let currentFileIndex = -1;
@@ -249,70 +301,70 @@
249
  const minZoom = 0.5;
250
  const zoomStep = 0.1;
251
  let currentSortMethod = 'name-asc';
252
-
253
  // Event listeners for dropzone
254
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
255
  dropzone.addEventListener(eventName, preventDefaults, false);
256
  });
257
-
258
  function preventDefaults(e) {
259
  e.preventDefault();
260
  e.stopPropagation();
261
  }
262
-
263
  ['dragenter', 'dragover'].forEach(eventName => {
264
  dropzone.addEventListener(eventName, highlight, false);
265
  });
266
-
267
  ['dragleave', 'drop'].forEach(eventName => {
268
  dropzone.addEventListener(eventName, unhighlight, false);
269
  });
270
-
271
  function highlight() {
272
  dropzone.classList.add('active');
273
  }
274
-
275
  function unhighlight() {
276
  dropzone.classList.remove('active');
277
  }
278
-
279
  dropzone.addEventListener('drop', handleDrop, false);
280
-
281
  function handleDrop(e) {
282
  const dt = e.dataTransfer;
283
  const droppedFiles = dt.files;
284
  handleFiles(droppedFiles);
285
  }
286
-
287
  // Browse button click
288
  browseBtn.addEventListener('click', () => {
289
  fileInput.click();
290
  });
291
-
292
  // File input change
293
  fileInput.addEventListener('change', () => {
294
  if (fileInput.files.length > 0) {
295
  handleFiles(fileInput.files);
296
  }
297
  });
298
-
299
  // Navigation buttons
300
  prevBtn.addEventListener('click', showPreviousImage);
301
  nextBtn.addEventListener('click', showNextImage);
302
-
303
  // Zoom buttons
304
  zoomInBtn.addEventListener('click', zoomIn);
305
  zoomOutBtn.addEventListener('click', zoomOut);
306
  resetZoomBtn.addEventListener('click', resetZoom);
307
  fullscreenBtn.addEventListener('click', openFullscreen);
308
-
309
  // Sort functionality
310
  sortBtn.addEventListener('click', (e) => {
311
  e.stopPropagation();
312
  const isExpanded = sortDropdown.classList.toggle('hidden');
313
  sortBtn.setAttribute('aria-expanded', !isExpanded);
314
  });
315
-
316
  document.querySelectorAll('.sort-option').forEach(option => {
317
  option.addEventListener('click', (e) => {
318
  currentSortMethod = e.target.dataset.sort;
@@ -323,31 +375,31 @@
323
  sortBtn.setAttribute('aria-expanded', 'false');
324
  });
325
  });
326
-
327
  // Close dropdown when clicking outside
328
  document.addEventListener('click', () => {
329
  sortDropdown.classList.add('hidden');
330
  sortBtn.setAttribute('aria-expanded', 'false');
331
  });
332
-
333
  // Fullscreen functionality
334
  fsPrevBtn.addEventListener('click', () => {
335
  showPreviousImage();
336
  updateFullscreenImage();
337
  });
338
-
339
  fsNextBtn.addEventListener('click', () => {
340
  showNextImage();
341
  updateFullscreenImage();
342
  });
343
-
344
  fsCloseBtn.addEventListener('click', closeFullscreen);
345
-
346
  // Keyboard navigation
347
  document.addEventListener('keydown', (e) => {
348
  if (files.length === 0) return;
349
-
350
- switch(e.key) {
351
  case 'ArrowLeft':
352
  if (fullscreenContainer.style.display === 'flex') {
353
  showPreviousImage();
@@ -391,52 +443,52 @@
391
  break;
392
  }
393
  });
394
-
395
  // Handle dropped or selected files
396
  function handleFiles(newFiles) {
397
  // Filter for supported image types
398
  const supportedTypes = [
399
- 'image/webp',
400
- 'image/png',
401
  'image/jpeg',
402
  'image/avif',
403
  'image/heic',
404
  'image/heif'
405
  ];
406
-
407
  const imageFiles = Array.from(newFiles).filter(file => {
408
  // Check MIME type first
409
  if (supportedTypes.includes(file.type)) return true;
410
-
411
  // Fallback for file extensions (some browsers might not recognize HEIC/AVIF MIME types)
412
  const extension = file.name.split('.').pop().toLowerCase();
413
  return ['webp', 'png', 'jpg', 'jpeg', 'avif', 'heic', 'heif'].includes(extension);
414
  });
415
-
416
  if (imageFiles.length === 0) {
417
  alert('No supported image files found. Please upload WebP, PNG, JPEG, AVIF, or HEIC files.');
418
  return;
419
  }
420
-
421
  files = imageFiles;
422
  currentFileIndex = 0;
423
  zoomLevel = 1;
424
-
425
  // Sort files
426
  sortFiles();
427
-
428
  // Update UI
429
  updateFileList();
430
  showImage(currentFileIndex);
431
  viewerArea.classList.remove('hidden');
432
-
433
  // Scroll to top
434
  window.scrollTo(0, 0);
435
  }
436
-
437
  // Sort files based on current sort method
438
  function sortFiles() {
439
- switch(currentSortMethod) {
440
  case 'name-asc':
441
  files.sort((a, b) => a.name.localeCompare(b.name));
442
  break;
@@ -456,19 +508,19 @@
456
  files.sort((a, b) => b.lastModified - a.lastModified);
457
  break;
458
  }
459
-
460
  // Update current file index to maintain selection
461
  if (currentFileIndex >= 0 && files.length > 0) {
462
  currentFileIndex = 0;
463
  }
464
  }
465
-
466
  // Update the file list sidebar
467
  function updateFileList() {
468
  fileList.innerHTML = '';
469
  fileCount.textContent = files.length;
470
  totalImages.textContent = files.length;
471
-
472
  files.forEach((file, index) => {
473
  const listItem = document.createElement('li');
474
  listItem.className = `file-item cursor-pointer ${index === currentFileIndex ? 'active' : ''}`;
@@ -484,11 +536,11 @@
484
  </div>
485
  </div>
486
  `;
487
-
488
  listItem.addEventListener('click', () => {
489
  showImage(index);
490
  });
491
-
492
  // Keyboard navigation for file list items
493
  listItem.addEventListener('keydown', (e) => {
494
  if (e.key === 'Enter' || e.key === ' ') {
@@ -496,10 +548,10 @@
496
  showImage(index);
497
  }
498
  });
499
-
500
  listItem.setAttribute('tabindex', '0');
501
  fileList.appendChild(listItem);
502
-
503
  // Load thumbnail
504
  const reader = new FileReader();
505
  reader.onload = (e) => {
@@ -511,14 +563,14 @@
511
  reader.readAsDataURL(file);
512
  });
513
  }
514
-
515
  // Show image at specified index
516
  function showImage(index) {
517
  if (index < 0 || index >= files.length) return;
518
-
519
  currentFileIndex = index;
520
  const file = files[index];
521
-
522
  // Update active item in file list
523
  document.querySelectorAll('.file-item').forEach((item, i) => {
524
  if (i === index) {
@@ -529,16 +581,16 @@
529
  item.setAttribute('aria-selected', 'false');
530
  }
531
  });
532
-
533
  // Update progress
534
  currentIndex.textContent = index + 1;
535
  progressBar.style.width = `${((index + 1) / files.length) * 100}%`;
536
-
537
  // Display the image
538
  const reader = new FileReader();
539
  reader.onload = (e) => {
540
  imageDisplay.innerHTML = '';
541
-
542
  const img = document.createElement('img');
543
  img.src = e.target.result;
544
  img.className = 'max-w-full max-h-[70vh] object-contain fade-in';
@@ -546,71 +598,71 @@
546
  img.style.transformOrigin = 'center center';
547
  img.style.transition = 'transform 0.2s ease';
548
  img.setAttribute('alt', `Preview of ${file.name}`);
549
-
550
  // Add drag to pan functionality
551
  let isDragging = false;
552
  let startX, startY, translateX = 0, translateY = 0;
553
-
554
  img.addEventListener('mousedown', (e) => {
555
  if (zoomLevel <= 1) return;
556
-
557
  isDragging = true;
558
  startX = e.clientX - translateX;
559
  startY = e.clientY - translateY;
560
  img.style.cursor = 'grabbing';
561
  });
562
-
563
  document.addEventListener('mousemove', (e) => {
564
  if (!isDragging) return;
565
-
566
  translateX = e.clientX - startX;
567
  translateY = e.clientY - startY;
568
  img.style.transform = `scale(${zoomLevel}) translate(${translateX}px, ${translateY}px)`;
569
  });
570
-
571
  document.addEventListener('mouseup', () => {
572
  isDragging = false;
573
  img.style.cursor = 'grab';
574
  });
575
-
576
  img.addEventListener('mouseenter', () => {
577
  if (zoomLevel > 1) {
578
  img.style.cursor = 'grab';
579
  }
580
  });
581
-
582
  img.addEventListener('mouseleave', () => {
583
  img.style.cursor = 'default';
584
  });
585
-
586
  imageDisplay.appendChild(img);
587
-
588
  // Update file info
589
  fileName.textContent = file.name;
590
  fileInfo.textContent = `${getFileType(file)} • ${formatFileSize(file.size)}`;
591
-
592
  // Load image dimensions after the image is loaded
593
  img.onload = () => {
594
  fileInfo.textContent = `${img.naturalWidth}×${img.naturalHeight} • ${getFileType(file)} • ${formatFileSize(file.size)}`;
595
  };
596
  };
597
  reader.readAsDataURL(file);
598
-
599
  // Enable/disable navigation buttons
600
  prevBtn.disabled = index === 0;
601
  nextBtn.disabled = index === files.length - 1;
602
  }
603
-
604
  // Helper to get file type
605
  function getFileType(file) {
606
  if (file.type) {
607
  const type = file.type.split('/')[1];
608
  if (type) return type.toUpperCase();
609
  }
610
-
611
  // Fallback for file extension
612
  const extension = file.name.split('.').pop().toLowerCase();
613
- switch(extension) {
614
  case 'jpg':
615
  case 'jpeg': return 'JPEG';
616
  case 'png': return 'PNG';
@@ -621,20 +673,20 @@
621
  default: return extension.toUpperCase();
622
  }
623
  }
624
-
625
  // Navigation functions
626
  function showPreviousImage() {
627
  if (currentFileIndex > 0) {
628
  showImage(currentFileIndex - 1);
629
  }
630
  }
631
-
632
  function showNextImage() {
633
  if (currentFileIndex < files.length - 1) {
634
  showImage(currentFileIndex + 1);
635
  }
636
  }
637
-
638
  // Zoom functions
639
  function zoomIn() {
640
  if (zoomLevel < maxZoom) {
@@ -642,66 +694,67 @@
642
  applyZoom();
643
  }
644
  }
645
-
646
  function zoomOut() {
647
  if (zoomLevel > minZoom) {
648
  zoomLevel -= zoomStep;
649
  applyZoom();
650
  }
651
  }
652
-
653
  function resetZoom() {
654
  zoomLevel = 1;
655
  applyZoom();
656
  }
657
-
658
  function applyZoom() {
659
  const img = imageDisplay.querySelector('img');
660
  if (img) {
661
  img.style.transform = `scale(${zoomLevel})`;
662
-
663
  // Reset pan position when zooming
664
  if (zoomLevel <= 1) {
665
  img.style.transform = `scale(${zoomLevel}) translate(0, 0)`;
666
  }
667
  }
668
  }
669
-
670
  // Fullscreen functions
671
  function openFullscreen() {
672
  const img = imageDisplay.querySelector('img');
673
  if (!img) return;
674
-
675
  fullscreenImage.src = img.src;
676
  fullscreenContainer.style.display = 'flex';
677
  document.body.style.overflow = 'hidden';
678
  fullscreenContainer.setAttribute('aria-hidden', 'false');
679
  }
680
-
681
  function closeFullscreen() {
682
  fullscreenContainer.style.display = 'none';
683
  document.body.style.overflow = '';
684
  fullscreenContainer.setAttribute('aria-hidden', 'true');
685
  }
686
-
687
  function updateFullscreenImage() {
688
  const img = imageDisplay.querySelector('img');
689
  if (img) {
690
  fullscreenImage.src = img.src;
691
  }
692
  }
693
-
694
  // Helper function to format file size
695
  function formatFileSize(bytes) {
696
  if (bytes === 0) return '0 Bytes';
697
-
698
  const k = 1024;
699
  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
700
  const i = Math.floor(Math.log(bytes) / Math.log(k));
701
-
702
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
703
  }
704
  });
705
  </script>
706
  <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>
 
707
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
25
  border: 3px dashed #9CA3AF;
26
  transition: all 0.3s ease;
27
  }
28
+
29
  .dropzone.active {
30
  border-color: #3B82F6;
31
  background-color: rgba(59, 130, 246, 0.1);
32
  }
33
+
34
  .image-container {
35
  transition: transform 0.3s ease;
36
  }
37
+
38
  .image-container:hover {
39
  transform: scale(1.01);
40
  }
41
+
42
  .nav-btn {
43
  transition: all 0.2s ease;
44
  }
45
+
46
  .nav-btn:hover {
47
  transform: scale(1.1);
48
  }
49
+
50
  .nav-btn:active {
51
  transform: scale(0.95);
52
  }
53
+
54
  .file-item:hover {
55
  background-color: rgba(59, 130, 246, 0.1);
56
  }
57
+
58
  .file-item.active {
59
  background-color: rgba(59, 130, 246, 0.2);
60
  border-left: 3px solid #3B82F6;
61
  }
62
+
63
  @keyframes fadeIn {
64
+ from {
65
+ opacity: 0;
66
+ }
67
+
68
+ to {
69
+ opacity: 1;
70
+ }
71
  }
72
+
73
  .fade-in {
74
  animation: fadeIn 0.3s ease-in-out;
75
  }
76
+
77
  #fullscreenContainer {
78
  display: none;
79
  position: fixed;
 
87
  align-items: center;
88
  flex-direction: column;
89
  }
90
+
91
  #fullscreenImage {
92
  max-width: 90%;
93
  max-height: 90%;
94
  object-fit: contain;
95
  }
96
+
97
  #fullscreenControls {
98
  position: absolute;
99
  bottom: 20px;
100
  display: flex;
101
  gap: 10px;
102
  }
103
+
104
  .sort-option:hover {
105
  background-color: rgba(59, 130, 246, 0.1);
106
  }
107
  </style>
108
  </head>
109
+
110
  <body class="bg-gray-100 min-h-screen">
111
  <div class="container mx-auto px-4 py-8">
112
  <div class="max-w-6xl mx-auto">
113
  <h1 class="text-3xl font-bold text-center text-gray-800 mb-2">Local Image Viewer</h1>
114
+ <p class="text-center text-gray-600 mb-8">View your local WebP, PNG, JPEG, AVIF, and HEIC files with ease
115
+ </p>
116
+
117
  <div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8">
118
  <!-- Dropzone area -->
119
+ <div id="dropzone" class="dropzone p-12 text-center cursor-pointer" role="region"
120
+ aria-label="File drop zone">
121
  <div class="flex flex-col items-center justify-center">
122
  <i class="fas fa-images text-5xl text-gray-400 mb-4" aria-hidden="true"></i>
123
  <h3 class="text-xl font-semibold text-gray-700 mb-2">Drag & Drop Images Here</h3>
124
  <p class="text-gray-500 mb-4">or</p>
125
+ <button id="browseBtn"
126
+ class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-6 rounded-lg transition"
127
+ aria-label="Browse files">
128
  Browse Files
129
  </button>
130
+ <input type="file" id="fileInput" class="hidden"
131
+ accept=".webp,.png,.jpg,.jpeg,.avif,.heic,.heif" multiple>
132
  </div>
133
  </div>
134
+
135
  <!-- Main viewer area (hidden initially) -->
136
  <div id="viewerArea" class="hidden">
137
  <div class="flex flex-col md:flex-row">
 
140
  <div class="p-4 border-b border-gray-200 flex justify-between items-center">
141
  <h3 class="font-medium text-gray-700">Files (<span id="fileCount">0</span>)</h3>
142
  <div class="relative">
143
+ <button id="sortBtn" class="text-gray-600 hover:text-gray-800"
144
+ aria-label="Sort options" aria-haspopup="true" aria-expanded="false">
145
  <i class="fas fa-sort" aria-hidden="true"></i>
146
  </button>
147
+ <div id="sortDropdown"
148
+ class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg z-10 py-1">
149
+ <div class="sort-option px-4 py-2 text-sm text-gray-700 cursor-pointer"
150
+ data-sort="name-asc" role="menuitem">Name (A-Z)</div>
151
+ <div class="sort-option px-4 py-2 text-sm text-gray-700 cursor-pointer"
152
+ data-sort="name-desc" role="menuitem">Name (Z-A)</div>
153
+ <div class="sort-option px-4 py-2 text-sm text-gray-700 cursor-pointer"
154
+ data-sort="size-asc" role="menuitem">Size (Small to Large)</div>
155
+ <div class="sort-option px-4 py-2 text-sm text-gray-700 cursor-pointer"
156
+ data-sort="size-desc" role="menuitem">Size (Large to Small)</div>
157
+ <div class="sort-option px-4 py-2 text-sm text-gray-700 cursor-pointer"
158
+ data-sort="date-asc" role="menuitem">Date (Oldest First)</div>
159
+ <div class="sort-option px-4 py-2 text-sm text-gray-700 cursor-pointer"
160
+ data-sort="date-desc" role="menuitem">Date (Newest First)</div>
161
  </div>
162
  </div>
163
  </div>
 
165
  <!-- Files will be listed here -->
166
  </ul>
167
  </div>
168
+
169
  <!-- Main image display -->
170
  <div class="w-full md:w-3/4 p-4 flex flex-col items-center justify-center">
171
  <div class="relative w-full max-w-3xl">
172
  <!-- Navigation buttons -->
173
+ <button id="prevBtn"
174
+ class="nav-btn absolute left-0 top-1/2 -translate-y-1/2 bg-white/80 hover:bg-white text-gray-800 p-3 rounded-full shadow-md ml-4 z-10"
175
+ aria-label="Previous image">
176
  <i class="fas fa-chevron-left text-xl" aria-hidden="true"></i>
177
  </button>
178
+
179
+ <button id="nextBtn"
180
+ class="nav-btn absolute right-0 top-1/2 -translate-y-1/2 bg-white/80 hover:bg-white text-gray-800 p-3 rounded-full shadow-md mr-4 z-10"
181
+ aria-label="Next image">
182
  <i class="fas fa-chevron-right text-xl" aria-hidden="true"></i>
183
  </button>
184
+
185
  <!-- Image display area -->
186
+ <div class="image-container bg-gray-100 rounded-lg overflow-hidden flex items-center justify-center"
187
+ style="min-height: 400px;">
188
  <div id="imageDisplay" class="p-4 w-full h-full flex items-center justify-center">
189
  <p class="text-gray-500">Select an image to view</p>
190
  </div>
191
  </div>
192
+
193
  <!-- Image info -->
194
  <div class="mt-4 bg-gray-50 rounded-lg p-3">
195
  <div class="flex justify-between items-center">
196
  <div>
197
+ <h4 id="fileName" class="font-medium text-gray-800 truncate">No image
198
+ selected</h4>
199
  <p id="fileInfo" class="text-sm text-gray-500">-</p>
200
  </div>
201
  <div class="flex space-x-2">
202
+ <button id="zoomInBtn"
203
+ class="nav-btn bg-gray-200 hover:bg-gray-300 text-gray-700 p-2 rounded"
204
+ aria-label="Zoom in">
205
  <i class="fas fa-search-plus" aria-hidden="true"></i>
206
  </button>
207
+ <button id="zoomOutBtn"
208
+ class="nav-btn bg-gray-200 hover:bg-gray-300 text-gray-700 p-2 rounded"
209
+ aria-label="Zoom out">
210
  <i class="fas fa-search-minus" aria-hidden="true"></i>
211
  </button>
212
+ <button id="resetZoomBtn"
213
+ class="nav-btn bg-gray-200 hover:bg-gray-300 text-gray-700 p-2 rounded"
214
+ aria-label="Reset zoom">
215
  <i class="fas fa-expand" aria-hidden="true"></i>
216
  </button>
217
+ <button id="fullscreenBtn"
218
+ class="nav-btn bg-gray-200 hover:bg-gray-300 text-gray-700 p-2 rounded"
219
+ aria-label="Fullscreen">
220
  <i class="fas fa-expand-arrows-alt" aria-hidden="true"></i>
221
  </button>
222
  </div>
223
  </div>
224
  <div class="mt-2">
225
  <div class="w-full bg-gray-200 rounded-full h-2">
226
+ <div id="progressBar" class="bg-blue-500 h-2 rounded-full"
227
+ style="width: 0%"></div>
228
  </div>
229
  <div class="flex justify-between text-xs text-gray-500 mt-1">
230
  <span id="currentIndex">0</span>
 
237
  </div>
238
  </div>
239
  </div>
240
+
241
  <div class="text-center text-gray-500 text-sm mt-8">
242
  <p>Use arrow keys to navigate between images</p>
243
  <p class="mt-1">Supported formats: WebP, PNG, JPEG, AVIF, HEIC</p>
 
249
  <div id="fullscreenContainer" role="dialog" aria-modal="true" aria-label="Fullscreen image viewer">
250
  <img id="fullscreenImage" src="" alt="Fullscreen Image">
251
  <div id="fullscreenControls">
252
+ <button id="fsPrevBtn" class="nav-btn bg-white/20 hover:bg-white/40 text-white p-3 rounded-full"
253
+ aria-label="Previous image">
254
  <i class="fas fa-chevron-left text-xl" aria-hidden="true"></i>
255
  </button>
256
+ <button id="fsCloseBtn" class="nav-btn bg-white/20 hover:bg-white/40 text-white p-3 rounded-full"
257
+ aria-label="Close fullscreen">
258
  <i class="fas fa-times text-xl" aria-hidden="true"></i>
259
  </button>
260
+ <button id="fsNextBtn" class="nav-btn bg-white/20 hover:bg-white/40 text-white p-3 rounded-full"
261
+ aria-label="Next image">
262
  <i class="fas fa-chevron-right text-xl" aria-hidden="true"></i>
263
  </button>
264
  </div>
265
  </div>
266
 
267
  <script>
268
+ document.addEventListener('DOMContentLoaded', function () {
269
  // DOM elements
270
  const dropzone = document.getElementById('dropzone');
271
  const browseBtn = document.getElementById('browseBtn');
 
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;
 
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;
 
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();
 
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',
453
  'image/jpeg',
454
  'image/avif',
455
  'image/heic',
456
  'image/heif'
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
  });
467
+
468
  if (imageFiles.length === 0) {
469
  alert('No supported image files found. Please upload WebP, PNG, JPEG, AVIF, or HEIC files.');
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;
 
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' : ''}`;
 
536
  </div>
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 === ' ') {
 
548
  showImage(index);
549
  }
550
  });
551
+
552
  listItem.setAttribute('tabindex', '0');
553
  fileList.appendChild(listItem);
554
+
555
  // Load thumbnail
556
  const reader = new FileReader();
557
  reader.onload = (e) => {
 
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) => {
576
  if (i === index) {
 
581
  item.setAttribute('aria-selected', 'false');
582
  }
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';
 
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';
 
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) {
 
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;
751
  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
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>
759
+
760
  </html>