jsfs11 commited on
Commit
79d7410
·
verified ·
1 Parent(s): 518128a

Add 2 files

Browse files
Files changed (2) hide show
  1. README.md +7 -5
  2. index.html +1043 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Local Image Viewer V2
3
- emoji: 🚀
4
- colorFrom: indigo
5
- colorTo: gray
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: local-image-viewer-v2
3
+ emoji: 🐳
4
+ colorFrom: purple
5
+ colorTo: pink
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,1043 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </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">
7
+ <title>Local Image Viewer</title>
8
+ <!-- Tailwind CSS -->
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script>
11
+ tailwind.config = {
12
+ theme: {
13
+ extend: {
14
+ colors: {
15
+ primary: '#6366F1',
16
+ secondary: '#8B5CF6',
17
+ accent: '#EC4899',
18
+ dark: '#1E293B',
19
+ light: '#F8FAFC'
20
+ },
21
+ fontFamily: {
22
+ sans: ['Inter', 'sans-serif'],
23
+ },
24
+ }
25
+ }
26
+ }
27
+ </script>
28
+ <!-- Font Awesome -->
29
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
30
+ <!-- Google Fonts -->
31
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
32
+ <style>
33
+ :root {
34
+ --transition-speed: 0.3s;
35
+ }
36
+
37
+ body {
38
+ font-family: 'Inter', sans-serif;
39
+ background-color: #F1F5F9;
40
+ }
41
+
42
+ .dropzone {
43
+ border: 3px dashed #CBD5E1;
44
+ transition: all var(--transition-speed) ease;
45
+ background-color: rgba(248, 250, 252, 0.7);
46
+ backdrop-filter: blur(4px);
47
+ }
48
+
49
+ .dropzone.active {
50
+ border-color: #6366F1;
51
+ background-color: rgba(99, 102, 241, 0.1);
52
+ box-shadow: 0 4px 15px rgba(99, 102, 241, 0.1);
53
+ }
54
+
55
+ .image-container {
56
+ transition: transform var(--transition-speed) ease;
57
+ box-shadow: 0 4px 25px rgba(0, 0, 0, 0.05);
58
+ background: linear-gradient(135deg, #F8FAFC 0%, #E2E8F0 100%);
59
+ }
60
+
61
+ .image-container:hover {
62
+ transform: translateY(-2px);
63
+ }
64
+
65
+ .nav-btn {
66
+ transition: all 0.2s ease;
67
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
68
+ }
69
+
70
+ .nav-btn:hover {
71
+ transform: scale(1.1);
72
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
73
+ }
74
+
75
+ .nav-btn:active {
76
+ transform: scale(0.95);
77
+ }
78
+
79
+ .file-item {
80
+ transition: all var(--transition-speed) ease;
81
+ }
82
+
83
+ .file-item:hover {
84
+ background-color: rgba(99, 102, 241, 0.05);
85
+ transform: translateX(2px);
86
+ }
87
+
88
+ .file-item.active {
89
+ background-color: rgba(99, 102, 241, 0.1);
90
+ border-left: 3px solid #6366F1;
91
+ }
92
+
93
+ @keyframes fadeIn {
94
+ from {
95
+ opacity: 0;
96
+ transform: translateY(10px);
97
+ }
98
+ to {
99
+ opacity: 1;
100
+ transform: translateY(0);
101
+ }
102
+ }
103
+
104
+ .fade-in {
105
+ animation: fadeIn 0.4s ease-out forwards;
106
+ }
107
+
108
+ #fullscreenContainer {
109
+ display: none;
110
+ position: fixed;
111
+ top: 0;
112
+ left: 0;
113
+ width: 100%;
114
+ height: 100%;
115
+ background-color: rgba(15, 23, 42, 0.95);
116
+ z-index: 1000;
117
+ justify-content: center;
118
+ align-items: center;
119
+ flex-direction: column;
120
+ }
121
+
122
+ #fullscreenImage {
123
+ max-width: 90%;
124
+ max-height: 90%;
125
+ object-fit: contain;
126
+ transform: translate3d(0, 0, 0);
127
+ }
128
+
129
+ #fullscreenControls {
130
+ position: absolute;
131
+ bottom: 20px;
132
+ display: flex;
133
+ gap: 10px;
134
+ }
135
+
136
+ .sort-option:hover {
137
+ background-color: rgba(99, 102, 241, 0.05);
138
+ }
139
+
140
+ .thumbnail-placeholder {
141
+ background: linear-gradient(135deg, #E2E8F0 0%, #CBD5E1 100%);
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ border-radius: 4px;
146
+ }
147
+
148
+ .thumbnail-placeholder i {
149
+ color: #94A3B8;
150
+ }
151
+
152
+ .progress-track {
153
+ background-color: #E2E8F0;
154
+ }
155
+
156
+ .progress-thumb {
157
+ background: linear-gradient(90deg, #6366F1 0%, #8B5CF6 100%);
158
+ }
159
+
160
+ .btn-primary {
161
+ background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
162
+ color: white;
163
+ transition: all var(--transition-speed) ease;
164
+ box-shadow: 0 4px 6px rgba(99, 102, 241, 0.2);
165
+ }
166
+
167
+ .btn-primary:hover {
168
+ transform: translateY(-2px);
169
+ box-shadow: 0 6px 12px rgba(99, 102, 241, 0.3);
170
+ }
171
+
172
+ .btn-primary:active {
173
+ transform: translateY(0);
174
+ }
175
+
176
+ .glass-effect {
177
+ background: rgba(255, 255, 255, 0.2);
178
+ backdrop-filter: blur(8px);
179
+ -webkit-backdrop-filter: blur(8px);
180
+ border: 1px solid rgba(255, 255, 255, 0.2);
181
+ }
182
+
183
+ .zoom-controls {
184
+ background: rgba(248, 250, 252, 0.8);
185
+ backdrop-filter: blur(4px);
186
+ border-radius: 8px;
187
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
188
+ }
189
+
190
+ .file-list-container {
191
+ scrollbar-width: thin;
192
+ scrollbar-color: #6366F1 #E2E8F0;
193
+ }
194
+
195
+ .file-list-container::-webkit-scrollbar {
196
+ width: 6px;
197
+ }
198
+
199
+ .file-list-container::-webkit-scrollbar-track {
200
+ background: #E2E8F0;
201
+ }
202
+
203
+ .file-list-container::-webkit-scrollbar-thumb {
204
+ background-color: #6366F1;
205
+ border-radius: 3px;
206
+ }
207
+
208
+ .gpu-accelerate {
209
+ transform: translate3d(0, 0, 0);
210
+ will-change: transform;
211
+ }
212
+ </style>
213
+ </head>
214
+
215
+ <body class="bg-light min-h-screen">
216
+ <div class="container mx-auto px-4 py-8">
217
+ <div class="max-w-6xl mx-auto">
218
+ <div class="text-center mb-8">
219
+ <h1 class="text-4xl font-bold text-dark mb-2">Local Image Viewer</h1>
220
+ <p class="text-lg text-slate-600">View your local WebP, PNG, JPEG, AVIF, and HEIC files with ease</p>
221
+ </div>
222
+
223
+ <div class="bg-white rounded-xl shadow-xl overflow-hidden mb-8">
224
+ <!-- Dropzone area -->
225
+ <div id="dropzone" class="dropzone p-12 text-center cursor-pointer rounded-xl" role="region"
226
+ aria-label="File drop zone">
227
+ <div class="flex flex-col items-center justify-center">
228
+ <div class="relative mb-6">
229
+ <div class="absolute inset-0 bg-primary opacity-10 rounded-full blur-md"></div>
230
+ <i class="fas fa-images text-5xl text-primary relative z-10" aria-hidden="true"></i>
231
+ </div>
232
+ <h3 class="text-xl font-semibold text-dark mb-2">Drag & Drop Images Here</h3>
233
+ <p class="text-slate-500 mb-4">or</p>
234
+ <button id="browseBtn"
235
+ class="btn-primary font-medium py-3 px-8 rounded-lg transition"
236
+ aria-label="Browse files">
237
+ Browse Files
238
+ </button>
239
+ <input type="file" id="fileInput" class="hidden"
240
+ accept=".webp,.png,.jpg,.jpeg,.avif,.heic,.heif" multiple>
241
+ </div>
242
+ </div>
243
+
244
+ <!-- Main viewer area (hidden initially) -->
245
+ <div id="viewerArea" class="hidden">
246
+ <div class="flex flex-col md:flex-row h-[70vh]">
247
+ <!-- Sidebar with file list -->
248
+ <div class="w-full md:w-1/4 bg-slate-50 border-r border-slate-200 file-list-container overflow-y-auto">
249
+ <div class="p-4 border-b border-slate-200 flex justify-between items-center bg-white">
250
+ <h3 class="font-medium text-dark">Files (<span id="fileCount">0</span>)</h3>
251
+ <div class="relative">
252
+ <button id="sortBtn" class="text-slate-600 hover:text-primary transition-colors"
253
+ aria-label="Sort options" aria-haspopup="true" aria-expanded="false">
254
+ <i class="fas fa-sort" aria-hidden="true"></i>
255
+ </button>
256
+ <div id="sortDropdown"
257
+ class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg z-10 py-1 border border-slate-200">
258
+ <div class="sort-option px-4 py-2 text-sm text-slate-700 cursor-pointer hover:bg-slate-50 transition-colors"
259
+ data-sort="name-asc" role="menuitem">Name (A-Z)</div>
260
+ <div class="sort-option px-4 py-2 text-sm text-slate-700 cursor-pointer hover:bg-slate-50 transition-colors"
261
+ data-sort="name-desc" role="menuitem">Name (Z-A)</div>
262
+ <div class="sort-option px-4 py-2 text-sm text-slate-700 cursor-pointer hover:bg-slate-50 transition-colors"
263
+ data-sort="size-asc" role="menuitem">Size (Small to Large)</div>
264
+ <div class="sort-option px-4 py-2 text-sm text-slate-700 cursor-pointer hover:bg-slate-50 transition-colors"
265
+ data-sort="size-desc" role="menuitem">Size (Large to Small)</div>
266
+ <div class="sort-option px-4 py-2 text-sm text-slate-700 cursor-pointer hover:bg-slate-50 transition-colors"
267
+ data-sort="date-asc" role="menuitem">Date (Oldest First)</div>
268
+ <div class="sort-option px-4 py-2 text-sm text-slate-700 cursor-pointer hover:bg-slate-50 transition-colors"
269
+ data-sort="date-desc" role="menuitem">Date (Newest First)</div>
270
+ </div>
271
+ </div>
272
+ </div>
273
+ <ul id="fileList" class="divide-y divide-slate-200" role="list">
274
+ <!-- Files will be listed here -->
275
+ </ul>
276
+ </div>
277
+
278
+ <!-- Main image display -->
279
+ <div class="w-full md:w-3/4 p-4 flex flex-col items-center justify-center bg-white">
280
+ <div class="relative w-full h-full max-w-4xl">
281
+ <!-- Navigation buttons -->
282
+ <button id="prevBtn"
283
+ class="nav-btn absolute left-0 top-1/2 -translate-y-1/2 bg-white hover:bg-primary text-primary hover:text-white p-3 rounded-full shadow-md ml-4 z-10 glass-effect"
284
+ aria-label="Previous image">
285
+ <i class="fas fa-chevron-left text-xl" aria-hidden="true"></i>
286
+ </button>
287
+
288
+ <button id="nextBtn"
289
+ class="nav-btn absolute right-0 top-1/2 -translate-y-1/2 bg-white hover:bg-primary text-primary hover:text-white p-3 rounded-full shadow-md mr-4 z-10 glass-effect"
290
+ aria-label="Next image">
291
+ <i class="fas fa-chevron-right text-xl" aria-hidden="true"></i>
292
+ </button>
293
+
294
+ <!-- Image display area -->
295
+ <div class="image-container bg-gradient-to-br from-slate-50 to-slate-100 rounded-xl overflow-hidden flex items-center justify-center h-full w-full gpu-accelerate">
296
+ <div id="imageDisplay" class="p-4 w-full h-full flex items-center justify-center">
297
+ <p class="text-slate-400">Select an image to view</p>
298
+ </div>
299
+ </div>
300
+
301
+ <!-- Image info -->
302
+ <div class="mt-4 bg-slate-50 rounded-lg p-4 zoom-controls">
303
+ <div class="flex justify-between items-center">
304
+ <div class="max-w-[70%]">
305
+ <h4 id="fileName" class="font-medium text-dark truncate">No image selected</h4>
306
+ <p id="fileInfo" class="text-sm text-slate-500">-</p>
307
+ </div>
308
+ <div class="flex space-x-2">
309
+ <button id="zoomInBtn"
310
+ class="nav-btn bg-white hover:bg-primary hover:text-white text-slate-700 p-2 rounded transition-colors"
311
+ aria-label="Zoom in">
312
+ <i class="fas fa-search-plus" aria-hidden="true"></i>
313
+ </button>
314
+ <button id="zoomOutBtn"
315
+ class="nav-btn bg-white hover:bg-primary hover:text-white text-slate-700 p-2 rounded transition-colors"
316
+ aria-label="Zoom out">
317
+ <i class="fas fa-search-minus" aria-hidden="true"></i>
318
+ </button>
319
+ <button id="resetZoomBtn"
320
+ class="nav-btn bg-white hover:bg-primary hover:text-white text-slate-700 p-2 rounded transition-colors"
321
+ aria-label="Reset zoom">
322
+ <i class="fas fa-expand" aria-hidden="true"></i>
323
+ </button>
324
+ <button id="fullscreenBtn"
325
+ class="nav-btn bg-white hover:bg-primary hover:text-white text-slate-700 p-2 rounded transition-colors"
326
+ aria-label="Fullscreen">
327
+ <i class="fas fa-expand-arrows-alt" aria-hidden="true"></i>
328
+ </button>
329
+ </div>
330
+ </div>
331
+ <div class="mt-3">
332
+ <div class="w-full progress-track rounded-full h-2">
333
+ <div id="progressBar" class="progress-thumb h-2 rounded-full"
334
+ style="width: 0%"></div>
335
+ </div>
336
+ <div class="flex justify-between text-xs text-slate-500 mt-1">
337
+ <span id="currentIndex">0</span>
338
+ <span id="totalImages">0</span>
339
+ </div>
340
+ </div>
341
+ </div>
342
+ </div>
343
+ </div>
344
+ </div>
345
+ </div>
346
+ </div>
347
+
348
+ <div class="text-center text-slate-500 text-sm mt-8">
349
+ <p>Use arrow keys to navigate between images • Press F for fullscreen</p>
350
+ <p class="mt-1">Supported formats: WebP, PNG, JPEG, AVIF, HEIC</p>
351
+ </div>
352
+ </div>
353
+ </div>
354
+
355
+ <!-- Fullscreen container -->
356
+ <div id="fullscreenContainer" role="dialog" aria-modal="true" aria-label="Fullscreen image viewer">
357
+ <img id="fullscreenImage" src="" alt="Fullscreen Image" class="gpu-accelerate">
358
+ <div id="fullscreenControls">
359
+ <button id="fsPrevBtn" class="nav-btn bg-white/20 hover:bg-white/40 text-white p-3 rounded-full glass-effect"
360
+ aria-label="Previous image">
361
+ <i class="fas fa-chevron-left text-xl" aria-hidden="true"></i>
362
+ </button>
363
+ <button id="fsCloseBtn" class="nav-btn bg-white/20 hover:bg-white/40 text-white p-3 rounded-full glass-effect"
364
+ aria-label="Close fullscreen">
365
+ <i class="fas fa-times text-xl" aria-hidden="true"></i>
366
+ </button>
367
+ <button id="fsNextBtn" class="nav-btn bg-white/20 hover:bg-white/40 text-white p-3 rounded-full glass-effect"
368
+ aria-label="Next image">
369
+ <i class="fas fa-chevron-right text-xl" aria-hidden="true"></i>
370
+ </button>
371
+ </div>
372
+ </div>
373
+
374
+ <script>
375
+ document.addEventListener('DOMContentLoaded', function () {
376
+ // Performance optimization utilities
377
+ const throttleRAF = (func) => {
378
+ let running = false;
379
+ return function() {
380
+ if (!running) {
381
+ running = true;
382
+ window.requestAnimationFrame(() => {
383
+ func.apply(this, arguments);
384
+ running = false;
385
+ });
386
+ }
387
+ };
388
+ };
389
+
390
+ const debounce = (func, delay) => {
391
+ let timeoutId;
392
+ return (...args) => {
393
+ clearTimeout(timeoutId);
394
+ timeoutId = setTimeout(() => func.apply(this, args), delay);
395
+ };
396
+ };
397
+
398
+ // Memory leak prevention
399
+ const cleanupHandlers = [];
400
+ const addCleanupHandler = (handler) => cleanupHandlers.push(handler);
401
+
402
+ // Clean up all event listeners when the page unloads
403
+ window.addEventListener('beforeunload', () => {
404
+ cleanupHandlers.forEach(handler => handler());
405
+ });
406
+
407
+ // DOM elements
408
+ const elements = {
409
+ dropzone: document.getElementById('dropzone'),
410
+ browseBtn: document.getElementById('browseBtn'),
411
+ fileInput: document.getElementById('fileInput'),
412
+ viewerArea: document.getElementById('viewerArea'),
413
+ fileList: document.getElementById('fileList'),
414
+ imageDisplay: document.getElementById('imageDisplay'),
415
+ fileName: document.getElementById('fileName'),
416
+ fileInfo: document.getElementById('fileInfo'),
417
+ fileCount: document.getElementById('fileCount'),
418
+ prevBtn: document.getElementById('prevBtn'),
419
+ nextBtn: document.getElementById('nextBtn'),
420
+ zoomInBtn: document.getElementById('zoomInBtn'),
421
+ zoomOutBtn: document.getElementById('zoomOutBtn'),
422
+ resetZoomBtn: document.getElementById('resetZoomBtn'),
423
+ fullscreenBtn: document.getElementById('fullscreenBtn'),
424
+ progressBar: document.getElementById('progressBar'),
425
+ currentIndex: document.getElementById('currentIndex'),
426
+ totalImages: document.getElementById('totalImages'),
427
+ sortBtn: document.getElementById('sortBtn'),
428
+ sortDropdown: document.getElementById('sortDropdown'),
429
+ fullscreenContainer: document.getElementById('fullscreenContainer'),
430
+ fullscreenImage: document.getElementById('fullscreenImage'),
431
+ fsPrevBtn: document.getElementById('fsPrevBtn'),
432
+ fsNextBtn: document.getElementById('fsNextBtn'),
433
+ fsCloseBtn: document.getElementById('fsCloseBtn')
434
+ };
435
+
436
+ // State variables
437
+ const state = {
438
+ files: [],
439
+ currentFileIndex: -1,
440
+ zoomLevel: 1,
441
+ maxZoom: 3,
442
+ minZoom: 0.5,
443
+ zoomStep: 0.1,
444
+ currentSortMethod: 'name-asc',
445
+ thumbnailObserver: null,
446
+ isDragging: false,
447
+ startX: 0,
448
+ startY: 0,
449
+ translateX: 0,
450
+ translateY: 0,
451
+ activeImage: null
452
+ };
453
+
454
+ // Initialize the app
455
+ const init = () => {
456
+ setupEventListeners();
457
+ setupThumbnailObserver();
458
+ };
459
+
460
+ // Set up all event listeners
461
+ const setupEventListeners = () => {
462
+ // Dropzone events
463
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
464
+ elements.dropzone.addEventListener(eventName, preventDefaults, false);
465
+ addCleanupHandler(() => {
466
+ elements.dropzone.removeEventListener(eventName, preventDefaults, false);
467
+ });
468
+ });
469
+
470
+ ['dragenter', 'dragover'].forEach(eventName => {
471
+ elements.dropzone.addEventListener(eventName, highlight, false);
472
+ addCleanupHandler(() => {
473
+ elements.dropzone.removeEventListener(eventName, highlight, false);
474
+ });
475
+ });
476
+
477
+ ['dragleave', 'drop'].forEach(eventName => {
478
+ elements.dropzone.addEventListener(eventName, unhighlight, false);
479
+ addCleanupHandler(() => {
480
+ elements.dropzone.removeEventListener(eventName, unhighlight, false);
481
+ });
482
+ });
483
+
484
+ elements.dropzone.addEventListener('drop', handleDrop, false);
485
+ addCleanupHandler(() => {
486
+ elements.dropzone.removeEventListener('drop', handleDrop, false);
487
+ });
488
+
489
+ elements.browseBtn.addEventListener('click', () => elements.fileInput.click());
490
+ elements.fileInput.addEventListener('change', () => {
491
+ if (elements.fileInput.files.length > 0) {
492
+ handleFiles(elements.fileInput.files);
493
+ }
494
+ });
495
+ addCleanupHandler(() => {
496
+ elements.browseBtn.removeEventListener('click', () => elements.fileInput.click());
497
+ elements.fileInput.removeEventListener('change', () => {
498
+ if (elements.fileInput.files.length > 0) {
499
+ handleFiles(elements.fileInput.files);
500
+ }
501
+ });
502
+ });
503
+
504
+ // Navigation events
505
+ elements.prevBtn.addEventListener('click', showPreviousImage);
506
+ elements.nextBtn.addEventListener('click', showNextImage);
507
+ addCleanupHandler(() => {
508
+ elements.prevBtn.removeEventListener('click', showPreviousImage);
509
+ elements.nextBtn.removeEventListener('click', showNextImage);
510
+ });
511
+
512
+ // Zoom events with debouncing
513
+ elements.zoomInBtn.addEventListener('click', debounce(zoomIn, 100));
514
+ elements.zoomOutBtn.addEventListener('click', debounce(zoomOut, 100));
515
+ elements.resetZoomBtn.addEventListener('click', resetZoom);
516
+ elements.fullscreenBtn.addEventListener('click', openFullscreen);
517
+ addCleanupHandler(() => {
518
+ elements.zoomInBtn.removeEventListener('click', debounce(zoomIn, 100));
519
+ elements.zoomOutBtn.removeEventListener('click', debounce(zoomOut, 100));
520
+ elements.resetZoomBtn.removeEventListener('click', resetZoom);
521
+ elements.fullscreenBtn.removeEventListener('click', openFullscreen);
522
+ });
523
+
524
+ // Sort events
525
+ elements.sortBtn.addEventListener('click', toggleSortDropdown);
526
+ document.querySelectorAll('.sort-option').forEach(option => {
527
+ option.addEventListener('click', handleSortOptionClick);
528
+ });
529
+ document.addEventListener('click', closeSortDropdown);
530
+ addCleanupHandler(() => {
531
+ elements.sortBtn.removeEventListener('click', toggleSortDropdown);
532
+ document.querySelectorAll('.sort-option').forEach(option => {
533
+ option.removeEventListener('click', handleSortOptionClick);
534
+ });
535
+ document.removeEventListener('click', closeSortDropdown);
536
+ });
537
+
538
+ // Fullscreen events
539
+ elements.fsPrevBtn.addEventListener('click', () => {
540
+ showPreviousImage();
541
+ updateFullscreenImage();
542
+ });
543
+ elements.fsNextBtn.addEventListener('click', () => {
544
+ showNextImage();
545
+ updateFullscreenImage();
546
+ });
547
+ elements.fsCloseBtn.addEventListener('click', closeFullscreen);
548
+ addCleanupHandler(() => {
549
+ elements.fsPrevBtn.removeEventListener('click', () => {
550
+ showPreviousImage();
551
+ updateFullscreenImage();
552
+ });
553
+ elements.fsNextBtn.removeEventListener('click', () => {
554
+ showNextImage();
555
+ updateFullscreenImage();
556
+ });
557
+ elements.fsCloseBtn.removeEventListener('click', closeFullscreen);
558
+ });
559
+
560
+ // Keyboard events
561
+ const keyboardHandler = handleKeyboardNavigation;
562
+ document.addEventListener('keydown', keyboardHandler);
563
+ addCleanupHandler(() => {
564
+ document.removeEventListener('keydown', keyboardHandler);
565
+ });
566
+ };
567
+
568
+ // Set up Intersection Observer for lazy loading thumbnails
569
+ const setupThumbnailObserver = () => {
570
+ state.thumbnailObserver = new IntersectionObserver((entries) => {
571
+ entries.forEach(entry => {
572
+ if (entry.isIntersecting) {
573
+ const thumbnail = entry.target;
574
+ const index = parseInt(thumbnail.dataset.index);
575
+ loadThumbnail(index);
576
+ state.thumbnailObserver.unobserve(thumbnail);
577
+ }
578
+ });
579
+ }, {
580
+ root: elements.fileList,
581
+ rootMargin: '100px',
582
+ threshold: 0.1
583
+ });
584
+
585
+ addCleanupHandler(() => {
586
+ if (state.thumbnailObserver) {
587
+ state.thumbnailObserver.disconnect();
588
+ }
589
+ });
590
+ };
591
+
592
+ // Dropzone helper functions
593
+ const preventDefaults = (e) => {
594
+ e.preventDefault();
595
+ e.stopPropagation();
596
+ };
597
+
598
+ const highlight = () => {
599
+ elements.dropzone.classList.add('active');
600
+ };
601
+
602
+ const unhighlight = () => {
603
+ elements.dropzone.classList.remove('active');
604
+ };
605
+
606
+ const handleDrop = (e) => {
607
+ const dt = e.dataTransfer;
608
+ const droppedFiles = dt.files;
609
+ handleFiles(droppedFiles);
610
+ };
611
+
612
+ // File handling
613
+ const handleFiles = (newFiles) => {
614
+ const supportedTypes = [
615
+ 'image/webp',
616
+ 'image/png',
617
+ 'image/jpeg',
618
+ 'image/avif',
619
+ 'image/heic',
620
+ 'image/heif'
621
+ ];
622
+
623
+ const imageFiles = Array.from(newFiles).filter(file => {
624
+ if (supportedTypes.includes(file.type)) return true;
625
+ const extension = file.name.split('.').pop().toLowerCase();
626
+ return ['webp', 'png', 'jpg', 'jpeg', 'avif', 'heic', 'heif'].includes(extension);
627
+ });
628
+
629
+ if (imageFiles.length === 0) {
630
+ alert('No supported image files found. Please upload WebP, PNG, JPEG, AVIF, or HEIC files.');
631
+ return;
632
+ }
633
+
634
+ // Clean up previous files and resources
635
+ if (state.activeImage) {
636
+ cleanupImageDragHandlers(state.activeImage);
637
+ }
638
+
639
+ state.files = imageFiles;
640
+ state.currentFileIndex = 0;
641
+ state.zoomLevel = 1;
642
+ state.translateX = 0;
643
+ state.translateY = 0;
644
+
645
+ sortFiles();
646
+ updateFileList();
647
+ showImage(state.currentFileIndex);
648
+ elements.viewerArea.classList.remove('hidden');
649
+ window.scrollTo(0, 0);
650
+ };
651
+
652
+ // Clean up image drag handlers
653
+ const cleanupImageDragHandlers = (img) => {
654
+ img.removeEventListener('mousedown', handleImageMouseDown);
655
+ img.removeEventListener('mouseenter', handleImageMouseEnter);
656
+ img.removeEventListener('mouseleave', handleImageMouseLeave);
657
+ };
658
+
659
+ // Sort functionality
660
+ const sortFiles = () => {
661
+ switch (state.currentSortMethod) {
662
+ case 'name-asc':
663
+ state.files.sort((a, b) => a.name.localeCompare(b.name));
664
+ break;
665
+ case 'name-desc':
666
+ state.files.sort((a, b) => b.name.localeCompare(a.name));
667
+ break;
668
+ case 'size-asc':
669
+ state.files.sort((a, b) => a.size - b.size);
670
+ break;
671
+ case 'size-desc':
672
+ state.files.sort((a, b) => b.size - a.size);
673
+ break;
674
+ case 'date-asc':
675
+ state.files.sort((a, b) => a.lastModified - b.lastModified);
676
+ break;
677
+ case 'date-desc':
678
+ state.files.sort((a, b) => b.lastModified - a.lastModified);
679
+ break;
680
+ }
681
+
682
+ if (state.currentFileIndex >= 0 && state.files.length > 0) {
683
+ state.currentFileIndex = 0;
684
+ }
685
+ };
686
+
687
+ const toggleSortDropdown = (e) => {
688
+ e.stopPropagation();
689
+ const isExpanded = elements.sortDropdown.classList.toggle('hidden');
690
+ elements.sortBtn.setAttribute('aria-expanded', !isExpanded);
691
+ };
692
+
693
+ const handleSortOptionClick = (e) => {
694
+ state.currentSortMethod = e.target.dataset.sort;
695
+ sortFiles();
696
+ updateFileList();
697
+ showImage(state.currentFileIndex);
698
+ closeSortDropdown();
699
+ };
700
+
701
+ const closeSortDropdown = () => {
702
+ elements.sortDropdown.classList.add('hidden');
703
+ elements.sortBtn.setAttribute('aria-expanded', 'false');
704
+ };
705
+
706
+ // File list management
707
+ const updateFileList = () => {
708
+ elements.fileList.innerHTML = '';
709
+ elements.fileCount.textContent = state.files.length;
710
+ elements.totalImages.textContent = state.files.length;
711
+
712
+ state.files.forEach((file, index) => {
713
+ const listItem = document.createElement('li');
714
+ listItem.className = `file-item cursor-pointer ${index === state.currentFileIndex ? 'active' : ''}`;
715
+ listItem.setAttribute('role', 'listitem');
716
+ listItem.innerHTML = `
717
+ <div class="flex items-center p-3">
718
+ <div class="flex-shrink-0 h-10 w-10 rounded overflow-hidden thumbnail-placeholder">
719
+ <i class="fas fa-image text-lg"></i>
720
+ <img src="#" alt="Thumbnail" class="h-full w-full object-cover hidden thumbnail" data-index="${index}">
721
+ </div>
722
+ <div class="ml-3 overflow-hidden">
723
+ <p class="text-sm font-medium text-dark truncate">${file.name}</p>
724
+ <p class="text-sm text-slate-500">${formatFileSize(file.size)}</p>
725
+ </div>
726
+ </div>
727
+ `;
728
+
729
+ listItem.addEventListener('click', () => showImage(index));
730
+ listItem.addEventListener('keydown', (e) => {
731
+ if (e.key === 'Enter' || e.key === ' ') {
732
+ e.preventDefault();
733
+ showImage(index);
734
+ }
735
+ });
736
+
737
+ listItem.setAttribute('tabindex', '0');
738
+ elements.fileList.appendChild(listItem);
739
+
740
+ // Observe the thumbnail for lazy loading
741
+ const thumbnail = listItem.querySelector('.thumbnail');
742
+ state.thumbnailObserver.observe(thumbnail);
743
+ });
744
+ };
745
+
746
+ // Lazy load thumbnail when it comes into view
747
+ const loadThumbnail = (index) => {
748
+ const thumbnail = document.querySelector(`.thumbnail[data-index="${index}"]`);
749
+ if (!thumbnail || thumbnail.src !== '#') return;
750
+
751
+ const file = state.files[index];
752
+ const reader = new FileReader();
753
+ reader.onload = (e) => {
754
+ thumbnail.src = e.target.result;
755
+ thumbnail.classList.remove('hidden');
756
+ thumbnail.previousElementSibling?.remove();
757
+ };
758
+ reader.readAsDataURL(file);
759
+ };
760
+
761
+ // Image display
762
+ const showImage = (index) => {
763
+ if (index < 0 || index >= state.files.length) return;
764
+
765
+ // Clean up previous image handlers
766
+ if (state.activeImage) {
767
+ cleanupImageDragHandlers(state.activeImage);
768
+ }
769
+
770
+ state.currentFileIndex = index;
771
+ const file = state.files[index];
772
+
773
+ // Update active item in file list
774
+ document.querySelectorAll('.file-item').forEach((item, i) => {
775
+ if (i === index) {
776
+ item.classList.add('active');
777
+ item.setAttribute('aria-selected', 'true');
778
+ } else {
779
+ item.classList.remove('active');
780
+ item.setAttribute('aria-selected', 'false');
781
+ }
782
+ });
783
+
784
+ // Update progress
785
+ elements.currentIndex.textContent = index + 1;
786
+ elements.progressBar.style.width = `${((index + 1) / state.files.length) * 100}%`;
787
+
788
+ // Display the image
789
+ const reader = new FileReader();
790
+ reader.onload = (e) => {
791
+ elements.imageDisplay.innerHTML = '';
792
+
793
+ const img = document.createElement('img');
794
+ img.src = e.target.result;
795
+ img.className = 'max-w-full max-h-[70vh] object-contain fade-in gpu-accelerate';
796
+ img.style.transform = `scale(${state.zoomLevel}) translate3d(${state.translateX}px, ${state.translateY}px, 0)`;
797
+ img.style.transformOrigin = 'center center';
798
+ img.style.transition = 'transform 0.2s ease';
799
+ img.setAttribute('alt', `Preview of ${file.name}`);
800
+
801
+ // Add drag to pan functionality
802
+ img.addEventListener('mousedown', handleImageMouseDown);
803
+ img.addEventListener('mouseenter', handleImageMouseEnter);
804
+ img.addEventListener('mouseleave', handleImageMouseLeave);
805
+
806
+ elements.imageDisplay.appendChild(img);
807
+ state.activeImage = img;
808
+
809
+ // Update file info
810
+ elements.fileName.textContent = file.name;
811
+ elements.fileInfo.textContent = `${getFileType(file)} • ${formatFileSize(file.size)}`;
812
+
813
+ // Load image dimensions after the image is loaded
814
+ img.onload = () => {
815
+ elements.fileInfo.textContent = `${img.naturalWidth}×${img.naturalHeight} • ${getFileType(file)} • ${formatFileSize(file.size)}`;
816
+ };
817
+ };
818
+ reader.readAsDataURL(file);
819
+
820
+ // Enable/disable navigation buttons
821
+ elements.prevBtn.disabled = index === 0;
822
+ elements.nextBtn.disabled = index === state.files.length - 1;
823
+ };
824
+
825
+ // Image drag handlers with throttle
826
+ const handleImageMouseDown = (e) => {
827
+ if (state.zoomLevel <= 1) return;
828
+
829
+ state.isDragging = true;
830
+ state.startX = e.clientX - state.translateX;
831
+ state.startY = e.clientY - state.translateY;
832
+ e.target.style.cursor = 'grabbing';
833
+
834
+ // Add global mouse move and up handlers
835
+ const mouseMoveHandler = throttleRAF(handleImageMouseMove);
836
+ const mouseUpHandler = handleImageMouseUp;
837
+
838
+ document.addEventListener('mousemove', mouseMoveHandler);
839
+ document.addEventListener('mouseup', mouseUpHandler);
840
+
841
+ // Clean up these handlers when done
842
+ const cleanup = () => {
843
+ document.removeEventListener('mousemove', mouseMoveHandler);
844
+ document.removeEventListener('mouseup', mouseUpHandler);
845
+ };
846
+
847
+ addCleanupHandler(cleanup);
848
+ };
849
+
850
+ const handleImageMouseMove = (e) => {
851
+ if (!state.isDragging) return;
852
+
853
+ state.translateX = e.clientX - state.startX;
854
+ state.translateY = e.clientY - state.startY;
855
+
856
+ const img = elements.imageDisplay.querySelector('img');
857
+ if (img) {
858
+ img.style.transform = `scale(${state.zoomLevel}) translate3d(${state.translateX}px, ${state.translateY}px, 0)`;
859
+ }
860
+ };
861
+
862
+ const handleImageMouseUp = () => {
863
+ state.isDragging = false;
864
+ const img = elements.imageDisplay.querySelector('img');
865
+ if (img) img.style.cursor = 'grab';
866
+ };
867
+
868
+ const handleImageMouseEnter = (e) => {
869
+ if (state.zoomLevel > 1) {
870
+ e.target.style.cursor = 'grab';
871
+ }
872
+ };
873
+
874
+ const handleImageMouseLeave = (e) => {
875
+ e.target.style.cursor = 'default';
876
+ };
877
+
878
+ // Navigation functions
879
+ const showPreviousImage = () => {
880
+ if (state.currentFileIndex > 0) {
881
+ showImage(state.currentFileIndex - 1);
882
+ }
883
+ };
884
+
885
+ const showNextImage = () => {
886
+ if (state.currentFileIndex < state.files.length - 1) {
887
+ showImage(state.currentFileIndex + 1);
888
+ }
889
+ };
890
+
891
+ // Zoom functions
892
+ const zoomIn = () => {
893
+ if (state.zoomLevel < state.maxZoom) {
894
+ state.zoomLevel += state.zoomStep;
895
+ applyZoom();
896
+ }
897
+ };
898
+
899
+ const zoomOut = () => {
900
+ if (state.zoomLevel > state.minZoom) {
901
+ state.zoomLevel -= state.zoomStep;
902
+ applyZoom();
903
+ }
904
+ };
905
+
906
+ const resetZoom = () => {
907
+ state.zoomLevel = 1;
908
+ state.translateX = 0;
909
+ state.translateY = 0;
910
+ applyZoom();
911
+ };
912
+
913
+ const applyZoom = () => {
914
+ const img = elements.imageDisplay.querySelector('img');
915
+ if (img) {
916
+ img.style.transform = `scale(${state.zoomLevel}) translate3d(${state.translateX}px, ${state.translateY}px, 0)`;
917
+
918
+ // Reset pan position when zooming
919
+ if (state.zoomLevel <= 1) {
920
+ state.translateX = 0;
921
+ state.translateY = 0;
922
+ img.style.transform = `scale(${state.zoomLevel}) translate3d(0, 0, 0)`;
923
+ }
924
+
925
+ // Update cursor based on zoom level
926
+ if (state.zoomLevel > 1) {
927
+ img.style.cursor = 'grab';
928
+ } else {
929
+ img.style.cursor = 'default';
930
+ }
931
+ }
932
+ };
933
+
934
+ // Fullscreen functions
935
+ const openFullscreen = () => {
936
+ const img = elements.imageDisplay.querySelector('img');
937
+ if (!img) return;
938
+
939
+ elements.fullscreenImage.src = img.src;
940
+ elements.fullscreenContainer.style.display = 'flex';
941
+ document.body.style.overflow = 'hidden';
942
+ elements.fullscreenContainer.setAttribute('aria-hidden', 'false');
943
+ };
944
+
945
+ const closeFullscreen = () => {
946
+ elements.fullscreenContainer.style.display = 'none';
947
+ document.body.style.overflow = '';
948
+ elements.fullscreenContainer.setAttribute('aria-hidden', 'true');
949
+ };
950
+
951
+ const updateFullscreenImage = () => {
952
+ const img = elements.imageDisplay.querySelector('img');
953
+ if (img) {
954
+ elements.fullscreenImage.src = img.src;
955
+ }
956
+ };
957
+
958
+ // Keyboard navigation
959
+ const handleKeyboardNavigation = (e) => {
960
+ if (state.files.length === 0) return;
961
+
962
+ switch (e.key) {
963
+ case 'ArrowLeft':
964
+ if (elements.fullscreenContainer.style.display === 'flex') {
965
+ showPreviousImage();
966
+ updateFullscreenImage();
967
+ } else {
968
+ showPreviousImage();
969
+ }
970
+ break;
971
+ case 'ArrowRight':
972
+ if (elements.fullscreenContainer.style.display === 'flex') {
973
+ showNextImage();
974
+ updateFullscreenImage();
975
+ } else {
976
+ showNextImage();
977
+ }
978
+ break;
979
+ case '+':
980
+ case '=':
981
+ zoomIn();
982
+ break;
983
+ case '-':
984
+ zoomOut();
985
+ break;
986
+ case '0':
987
+ resetZoom();
988
+ break;
989
+ case 'Escape':
990
+ if (elements.fullscreenContainer.style.display === 'flex') {
991
+ closeFullscreen();
992
+ }
993
+ break;
994
+ case 'f':
995
+ case 'F':
996
+ if (elements.imageDisplay.querySelector('img')) {
997
+ if (elements.fullscreenContainer.style.display === 'flex') {
998
+ closeFullscreen();
999
+ } else {
1000
+ openFullscreen();
1001
+ }
1002
+ }
1003
+ break;
1004
+ }
1005
+ };
1006
+
1007
+ // Helper functions
1008
+ const getFileType = (file) => {
1009
+ if (file.type) {
1010
+ const type = file.type.split('/')[1];
1011
+ if (type) return type.toUpperCase();
1012
+ }
1013
+
1014
+ const extension = file.name.split('.').pop().toLowerCase();
1015
+ switch (extension) {
1016
+ case 'jpg':
1017
+ case 'jpeg': return 'JPEG';
1018
+ case 'png': return 'PNG';
1019
+ case 'webp': return 'WEBP';
1020
+ case 'avif': return 'AVIF';
1021
+ case 'heic':
1022
+ case 'heif': return 'HEIC';
1023
+ default: return extension.toUpperCase();
1024
+ }
1025
+ };
1026
+
1027
+ const formatFileSize = (bytes) => {
1028
+ if (bytes === 0) return '0 Bytes';
1029
+
1030
+ const k = 1024;
1031
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
1032
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1033
+
1034
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1035
+ };
1036
+
1037
+ // Initialize the application
1038
+ init();
1039
+ });
1040
+ </script>
1041
+ <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-v2" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
1042
+
1043
+ </html>