testdeep123 commited on
Commit
a88b84c
·
verified ·
1 Parent(s): dc05454

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +305 -341
app.py CHANGED
@@ -12,7 +12,7 @@ api = HfApi()
12
 
13
  TEMPLATE = """
14
  <!DOCTYPE html>
15
- <html>
16
  <head>
17
  <meta charset="UTF-8">
18
  <title>HuggingFace Drive - {{ path or 'Root' }}</title>
@@ -22,18 +22,20 @@ TEMPLATE = """
22
  --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
23
  --danger-gradient: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
24
  --warning-gradient: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
25
- --bg-dark: #0f0f23;
26
- --bg-darker: #0a0a18;
 
 
27
  --border-light: rgba(255, 255, 255, 0.1);
28
- --text-light: #e2e8f0;
29
- --text-lighter: #f1f5f9;
30
- --text-muted: #94a3b8;
31
  --shadow-primary: 0 8px 32px rgba(102, 126, 234, 0.3);
32
- --shadow-danger: 0 8px 32px rgba(239, 68, 68, 0.3);
33
- --shadow-large: 0 20px 60px rgba(0, 0, 0, 0.6);
34
  --radius-lg: 16px;
35
  --radius-md: 12px;
36
  --radius-sm: 8px;
 
37
  }
38
 
39
  * {
@@ -43,71 +45,68 @@ TEMPLATE = """
43
  }
44
 
45
  body {
46
- background: linear-gradient(135deg, var(--bg-dark) 0%, var(--bg-darker) 100%);
 
 
47
  color: var(--text-light);
48
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
49
  min-height: 100vh;
50
  line-height: 1.5;
51
  }
52
- .file-item {
53
- background: rgba(255, 255, 255, 0.05);
54
- backdrop-filter: blur(10px);
55
- border: 1px solid var(--border-light);
56
- border-radius: var(--radius-lg);
57
- padding: 16px;
58
- transition: all 0.3s ease;
59
- cursor: pointer;
60
- position: relative;
61
- overflow: visible !important;
62
- margin: 40px 10px !important;
63
- }
64
  .container {
65
- max-width: 1200px;
66
  margin: 0 auto;
67
- padding: 20px;
68
  }
69
 
70
  .header {
71
- background: rgba(255, 255, 255, 0.05);
72
  backdrop-filter: blur(10px);
73
  border-radius: var(--radius-lg);
74
  padding: 24px;
75
- margin-bottom: 24px;
76
  border: 1px solid var(--border-light);
77
  box-shadow: var(--shadow-large);
78
  }
79
 
80
  .title {
81
- font-size: 2rem;
82
  font-weight: 700;
83
  margin-bottom: 16px;
84
  background: var(--primary-gradient);
85
  -webkit-background-clip: text;
86
  -webkit-text-fill-color: transparent;
87
  background-clip: text;
 
 
 
88
  }
89
 
90
  .breadcrumb {
91
  display: flex;
92
  align-items: center;
93
  gap: 8px;
94
- margin-bottom: 20px;
95
  flex-wrap: wrap;
96
  }
97
 
98
  .breadcrumb-item {
99
- background: rgba(255, 255, 255, 0.1);
100
  color: var(--text-muted);
101
  padding: 6px 12px;
102
- border-radius: 20px;
103
  font-size: 0.875rem;
104
  cursor: pointer;
105
- transition: all 0.3s ease;
106
- border: 1px solid var(--border-light);
107
  white-space: nowrap;
108
  max-width: 200px;
109
  overflow: hidden;
110
  text-overflow: ellipsis;
 
 
 
111
  }
112
 
113
  .breadcrumb-item:hover {
@@ -119,81 +118,96 @@ TEMPLATE = """
119
  .breadcrumb-item.active {
120
  background: var(--primary-gradient);
121
  color: white;
 
 
122
  }
123
 
124
  .breadcrumb-separator {
125
  color: var(--text-muted);
126
- font-size: 0.875rem;
127
  }
128
 
129
  .actions {
130
  display: flex;
131
  gap: 12px;
132
- margin-bottom: 24px;
133
  flex-wrap: wrap;
134
  }
135
 
136
- .upload-container {
137
- position: relative;
138
- overflow: hidden;
139
- display: inline-block;
140
- }
141
-
142
- .file-input {
143
- position: absolute;
144
- left: -9999px;
145
- opacity: 0;
146
- }
147
-
148
  .btn {
149
  background: var(--primary-gradient);
150
  color: white;
151
  border: none;
152
- padding: 12px 24px;
153
  border-radius: var(--radius-md);
154
  cursor: pointer;
155
  font-weight: 600;
156
- font-size: 0.875rem;
157
- transition: all 0.3s ease;
158
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
159
  border: 1px solid var(--border-light);
160
  display: inline-flex;
161
  align-items: center;
 
162
  gap: 8px;
163
  }
164
 
165
  .btn:hover {
166
  transform: translateY(-2px);
167
- box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
 
 
 
 
 
 
 
168
  }
169
 
170
  .btn-secondary {
171
  background: rgba(255, 255, 255, 0.1);
172
  color: var(--text-lighter);
173
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
174
  }
175
 
176
  .btn-secondary:hover {
177
  background: rgba(255, 255, 255, 0.15);
178
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
179
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
  .file-grid {
182
  display: grid;
183
- gap: 16px;
184
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
185
  }
186
 
187
  .file-item {
188
- background: rgba(255, 255, 255, 0.05);
189
  backdrop-filter: blur(10px);
190
  border: 1px solid var(--border-light);
191
  border-radius: var(--radius-lg);
192
- padding: 16px;
193
- transition: all 0.3s ease;
194
- cursor: pointer;
195
  position: relative;
196
- overflow: hidden;
197
  }
198
 
199
  .file-item:hover {
@@ -202,12 +216,17 @@ TEMPLATE = """
202
  border-color: rgba(102, 126, 234, 0.3);
203
  }
204
 
 
 
 
 
 
 
 
205
  .file-content {
206
  display: flex;
207
  align-items: center;
208
  justify-content: space-between;
209
- position: relative;
210
- z-index: 2;
211
  gap: 12px;
212
  }
213
 
@@ -226,18 +245,18 @@ TEMPLATE = """
226
  display: flex;
227
  align-items: center;
228
  justify-content: center;
229
- border-radius: var(--radius-sm);
230
  background: rgba(255, 255, 255, 0.1);
 
231
  flex-shrink: 0;
232
  }
233
-
234
  .folder-icon {
235
  background: var(--warning-gradient);
236
  }
237
 
238
  .file-name {
239
  font-weight: 500;
240
- font-size: 0.95rem;
241
  color: var(--text-lighter);
242
  white-space: nowrap;
243
  overflow: hidden;
@@ -245,19 +264,18 @@ TEMPLATE = """
245
  }
246
 
247
  .dropdown {
248
- position: relative;
249
- flex-shrink: 0;
 
250
  }
251
-
252
  .dropdown-toggle {
253
  background: rgba(255, 255, 255, 0.1);
254
- border: none;
255
  color: var(--text-muted);
256
- padding: 8px;
257
- border-radius: var(--radius-sm);
258
  cursor: pointer;
259
- transition: all 0.3s ease;
260
- font-size: 1.25rem;
261
  width: 36px;
262
  height: 36px;
263
  display: flex;
@@ -268,22 +286,22 @@ TEMPLATE = """
268
  .dropdown-toggle:hover {
269
  background: rgba(255, 255, 255, 0.2);
270
  color: var(--text-lighter);
 
271
  }
272
 
273
  .dropdown-menu {
274
  position: absolute;
275
- top: 100%;
276
  right: 0;
277
- background: rgba(15, 15, 35, 0.98);
278
- backdrop-filter: blur(20px);
279
  border: 1px solid var(--border-light);
280
  border-radius: var(--radius-md);
281
  padding: 8px;
282
- min-width: 160px;
283
  opacity: 0;
284
  visibility: hidden;
285
  transform: translateY(-10px);
286
- transition: all 0.3s ease;
287
  z-index: 1000;
288
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
289
  }
@@ -297,12 +315,13 @@ TEMPLATE = """
297
  .dropdown-item {
298
  display: flex;
299
  align-items: center;
300
- gap: 8px;
301
  padding: 10px 12px;
302
  border-radius: var(--radius-sm);
303
  cursor: pointer;
304
- transition: all 0.2s ease;
305
  font-size: 0.875rem;
 
306
  color: var(--text-muted);
307
  }
308
 
@@ -331,7 +350,7 @@ TEMPLATE = """
331
  z-index: 2000;
332
  opacity: 0;
333
  visibility: hidden;
334
- transition: all 0.3s ease;
335
  }
336
 
337
  .modal-overlay.active {
@@ -340,18 +359,17 @@ TEMPLATE = """
340
  }
341
 
342
  .modal {
343
- background: rgba(15, 15, 35, 0.98);
344
- backdrop-filter: blur(20px);
345
  border: 1px solid var(--border-light);
346
  border-radius: var(--radius-lg);
347
  padding: 24px;
348
  max-width: 450px;
349
  width: 90%;
350
- transform: scale(0.9);
351
- transition: all 0.3s ease;
352
  box-shadow: var(--shadow-large);
353
  }
354
-
355
  .modal-overlay.active .modal {
356
  transform: scale(1);
357
  }
@@ -370,20 +388,19 @@ TEMPLATE = """
370
 
371
  .modal-input {
372
  width: 100%;
373
- background: rgba(255, 255, 255, 0.1);
374
- border: 1px solid rgba(255, 255, 255, 0.2);
375
  border-radius: var(--radius-md);
376
  padding: 12px 16px;
377
  color: var(--text-lighter);
378
  font-size: 1rem;
379
- margin-bottom: 20px;
380
- transition: all 0.3s ease;
381
  }
382
 
383
  .modal-input:focus {
384
  outline: none;
385
  border-color: #667eea;
386
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
387
  }
388
 
389
  .modal-actions {
@@ -391,106 +408,116 @@ TEMPLATE = """
391
  gap: 12px;
392
  justify-content: flex-end;
393
  }
394
-
395
- .btn-danger {
396
- background: var(--danger-gradient);
397
- box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);
 
398
  }
399
-
400
- .btn-danger:hover {
401
- box-shadow: 0 8px 25px rgba(239, 68, 68, 0.4);
402
- }
403
-
404
- /* New Folder Modal */
405
- #folderModal .modal {
406
- max-width: 400px;
407
- }
408
-
409
- /* Loading Animation */
410
- .loading {
411
  display: inline-block;
412
  width: 20px;
413
  height: 20px;
414
  border: 3px solid rgba(255, 255, 255, 0.3);
415
  border-radius: 50%;
416
- border-top-color: #667eea;
417
  animation: spin 1s ease-in-out infinite;
418
  }
 
 
 
419
 
420
  @keyframes spin {
421
  to { transform: rotate(360deg); }
422
  }
423
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  /* Responsive Design */
425
  @media (max-width: 768px) {
426
  .container {
427
- padding: 16px;
 
 
 
428
  }
429
-
430
  .file-grid {
431
  grid-template-columns: 1fr;
 
432
  }
433
-
434
  .actions {
435
  flex-direction: column;
436
- align-items: flex-start;
 
 
 
437
  }
438
-
439
  .breadcrumb {
440
  gap: 6px;
441
  }
442
-
443
  .breadcrumb-separator {
444
  display: none;
445
  }
446
-
447
- .breadcrumb-item {
448
- max-width: 150px;
449
- }
450
-
451
- .modal {
452
- padding: 20px;
453
- }
454
-
455
  .modal-actions {
456
  flex-direction: column-reverse;
457
- gap: 8px;
458
  }
459
-
460
  .modal-actions .btn {
461
- width: 100%;
462
  }
463
  }
464
-
465
- /* Custom Scrollbar */
466
- ::-webkit-scrollbar {
467
- width: 8px;
468
- height: 8px;
469
- }
470
-
471
- ::-webkit-scrollbar-track {
472
- background: rgba(255, 255, 255, 0.1);
473
- }
474
-
475
- ::-webkit-scrollbar-thumb {
476
- background: rgba(102, 126, 234, 0.5);
477
- border-radius: 4px;
478
- }
479
-
480
- ::-webkit-scrollbar-thumb:hover {
481
- background: rgba(102, 126, 234, 0.7);
482
- }
483
  </style>
484
  </head>
485
  <body>
 
 
 
 
 
 
 
486
  <div class="container">
487
- <div class="header">
488
- <h1 class="title">🚀 HuggingFace Drive</h1>
 
 
 
489
 
490
  <!-- Breadcrumb Navigation -->
491
- <div class="breadcrumb">
492
  <span class="breadcrumb-item {{ 'active' if not path else '' }}" onclick="nav('')">
493
- 🏠 Home
 
494
  </span>
495
  {% if path %}
496
  {% set parts = path.split('/') %}
@@ -499,65 +526,77 @@ TEMPLATE = """
499
  {% set current_path = parts[:i+1]|join('/') %}
500
  <span class="breadcrumb-item {{ 'active' if current_path == path else '' }}"
501
  onclick="nav('{{ current_path }}')">
502
- 📁 {{ parts[i] }}
 
503
  </span>
504
  {% endfor %}
505
  {% endif %}
506
- </div>
507
-
508
  <!-- Actions -->
509
  <div class="actions">
510
- <form action="/upload" method="post" enctype="multipart/form-data" class="upload-container">
511
- <input type="file" name="file" required class="file-input" id="fileInput">
512
- <input type="hidden" name="path" value="{{ path }}">
513
- <label for="fileInput" class="btn">
514
- 📤 Upload File
515
- </label>
 
 
 
516
  </form>
517
  <button class="btn btn-secondary" onclick="showFolderModal('{{ path }}')">
518
- 📁 New Folder
 
519
  </button>
520
  </div>
521
- </div>
522
-
523
  <!-- File Grid -->
524
- <div class="file-grid">
525
  {% for item in items %}
526
  <div class="file-item">
527
- <div class="file-content">
528
- <div class="file-info" onclick="{% if item.type=='dir' %}nav('{{ item.path }}'){% else %}download('{{ item.path }}'){% endif %}">
529
- <div class="file-icon {{ 'folder-icon' if item.type=='dir' else '' }}">
530
- {{ '📁' if item.type=='dir' else '📄' }}
 
 
 
 
 
 
 
531
  </div>
532
- <div class="file-name" title="{{ item.name }}">{{ item.name }}</div>
533
  </div>
534
-
535
- <div class="dropdown">
536
- <button class="dropdown-toggle" onclick="toggleDropdown(this)">⋮</button>
537
- <div class="dropdown-menu">
538
- {% if item.type == 'file' %}
539
- <div class="dropdown-item" onclick="download('{{ item.path }}')">
540
- 📥 Download
541
- </div>
542
- {% endif %}
543
- <div class="dropdown-item" onclick="showRenameModal('{{ item.path }}', '{{ item.name }}')">
544
- ✏�� Rename
545
- </div>
546
- <div class="dropdown-item danger" onclick="showDeleteModal('{{ item.path }}', '{{ item.name }}')">
547
- 🗑️ Delete
548
  </div>
 
 
 
 
 
 
 
 
549
  </div>
550
  </div>
551
  </div>
552
  </div>
553
  {% endfor %}
554
- </div>
555
  </div>
556
-
557
- <!-- Rename Modal -->
558
  <div class="modal-overlay" id="renameModal">
559
- <div class="modal">
560
- <h3 class="modal-title">Rename Item</h3>
561
  <div class="modal-body">
562
  <input type="text" class="modal-input" id="renameInput" placeholder="Enter new name" autocomplete="off">
563
  </div>
@@ -565,16 +604,14 @@ TEMPLATE = """
565
  <button class="btn btn-secondary" onclick="closeModal('renameModal')">Cancel</button>
566
  <button class="btn" onclick="confirmRename()" id="renameConfirmBtn">
567
  <span id="renameBtnText">Rename</span>
568
- <span id="renameBtnSpinner" class="loading" style="display: none;"></span>
569
  </button>
570
  </div>
571
  </div>
572
  </div>
573
-
574
- <!-- Delete Confirmation Modal -->
575
  <div class="modal-overlay" id="deleteModal">
576
- <div class="modal">
577
- <h3 class="modal-title">Confirm Deletion</h3>
578
  <div class="modal-body">
579
  <p>Are you sure you want to delete <strong id="deleteItemName"></strong>? This action cannot be undone.</p>
580
  </div>
@@ -582,16 +619,14 @@ TEMPLATE = """
582
  <button class="btn btn-secondary" onclick="closeModal('deleteModal')">Cancel</button>
583
  <button class="btn btn-danger" onclick="confirmDelete()" id="deleteConfirmBtn">
584
  <span id="deleteBtnText">Delete</span>
585
- <span id="deleteBtnSpinner" class="loading" style="display: none;"></span>
586
  </button>
587
  </div>
588
  </div>
589
  </div>
590
-
591
- <!-- New Folder Modal -->
592
  <div class="modal-overlay" id="folderModal">
593
- <div class="modal">
594
- <h3 class="modal-title">Create New Folder</h3>
595
  <div class="modal-body">
596
  <input type="text" class="modal-input" id="folderInput" placeholder="Enter folder name" autocomplete="off">
597
  </div>
@@ -599,7 +634,7 @@ TEMPLATE = """
599
  <button class="btn btn-secondary" onclick="closeModal('folderModal')">Cancel</button>
600
  <button class="btn" onclick="confirmCreateFolder()" id="folderConfirmBtn">
601
  <span id="folderBtnText">Create</span>
602
- <span id="folderBtnSpinner" class="loading" style="display: none;"></span>
603
  </button>
604
  </div>
605
  </div>
@@ -611,22 +646,18 @@ TEMPLATE = """
611
  let currentFolderPath = '';
612
  let activeDropdown = null;
613
 
614
- // Navigation
615
  function nav(p) {
616
  location.href = '/?path=' + encodeURIComponent(p);
617
  }
618
-
619
- // File download
620
  function download(path) {
621
  window.open('/download?path=' + encodeURIComponent(path), '_blank');
622
  }
623
 
624
- // Dropdown toggle
625
- function toggleDropdown(button) {
626
  event.stopPropagation();
627
  const dropdown = button.closest('.dropdown');
628
 
629
- // Close other dropdowns
630
  if (activeDropdown && activeDropdown !== dropdown) {
631
  activeDropdown.classList.remove('active');
632
  }
@@ -635,7 +666,28 @@ TEMPLATE = """
635
  activeDropdown = dropdown.classList.contains('active') ? dropdown : null;
636
  }
637
 
638
- // Show rename modal
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
  function showRenameModal(path, currentName) {
640
  currentRenamePath = path;
641
  document.getElementById('renameInput').value = currentName;
@@ -647,7 +699,6 @@ TEMPLATE = """
647
  closeAllDropdowns();
648
  }
649
 
650
- // Show delete modal
651
  function showDeleteModal(path, name) {
652
  currentDeletePath = path;
653
  document.getElementById('deleteItemName').textContent = name;
@@ -655,7 +706,6 @@ TEMPLATE = """
655
  closeAllDropdowns();
656
  }
657
 
658
- // Show folder creation modal
659
  function showFolderModal(path) {
660
  currentFolderPath = path;
661
  document.getElementById('folderInput').value = '';
@@ -663,175 +713,89 @@ TEMPLATE = """
663
  setTimeout(() => document.getElementById('folderInput').focus(), 100);
664
  }
665
 
666
- // Show modal
667
- function showModal(modalId) {
668
- document.getElementById(modalId).classList.add('active');
669
- document.body.style.overflow = 'hidden';
670
- }
671
-
672
- // Close modal
673
- function closeModal(modalId) {
674
- document.getElementById(modalId).classList.remove('active');
675
- document.body.style.overflow = '';
676
- }
677
-
678
- // Close all dropdowns
679
  function closeAllDropdowns() {
680
- document.querySelectorAll('.dropdown.active').forEach(dropdown => {
681
- dropdown.classList.remove('active');
682
- });
683
- activeDropdown = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
684
  }
685
 
686
- // Confirm rename action
687
- async function confirmRename() {
688
  const newName = document.getElementById('renameInput').value.trim();
689
  if (!newName) {
690
  alert('Please enter a valid name');
691
  return;
692
  }
693
-
694
- const btn = document.getElementById('renameConfirmBtn');
695
- const btnText = document.getElementById('renameBtnText');
696
- const spinner = document.getElementById('renameBtnSpinner');
697
-
698
- btn.disabled = true;
699
- btnText.style.display = 'none';
700
- spinner.style.display = 'inline-block';
701
-
702
- try {
703
- const response = await fetch('/rename', {
704
- method: 'POST',
705
- headers: { 'Content-Type': 'application/json' },
706
- body: JSON.stringify({
707
- old_path: currentRenamePath,
708
- new_path: newName
709
- })
710
- });
711
-
712
- if (response.ok) {
713
- closeModal('renameModal');
714
- location.reload();
715
- } else {
716
- alert('Failed to rename item');
717
- }
718
- } catch (error) {
719
- console.error('Rename failed:', error);
720
- alert('An error occurred while renaming');
721
- } finally {
722
- btn.disabled = false;
723
- btnText.style.display = 'inline-block';
724
- spinner.style.display = 'none';
725
- }
726
  }
727
 
728
- // Confirm delete action
729
- async function confirmDelete() {
730
- const btn = document.getElementById('deleteConfirmBtn');
731
- const btnText = document.getElementById('deleteBtnText');
732
- const spinner = document.getElementById('deleteBtnSpinner');
733
-
734
- btn.disabled = true;
735
- btnText.style.display = 'none';
736
- spinner.style.display = 'inline-block';
737
-
738
- try {
739
- const response = await fetch('/delete', {
740
- method: 'POST',
741
- headers: { 'Content-Type': 'application/json' },
742
- body: JSON.stringify({ path: currentDeletePath })
743
- });
744
-
745
- if (response.ok) {
746
- closeModal('deleteModal');
747
- location.reload();
748
- } else {
749
- alert('Failed to delete item');
750
- }
751
- } catch (error) {
752
- console.error('Delete failed:', error);
753
- alert('An error occurred while deleting');
754
- } finally {
755
- btn.disabled = false;
756
- btnText.style.display = 'inline-block';
757
- spinner.style.display = 'none';
758
- }
759
  }
760
 
761
- // Confirm folder creation
762
- async function confirmCreateFolder() {
763
  const folderName = document.getElementById('folderInput').value.trim();
764
  if (!folderName) {
765
  alert('Please enter a folder name');
766
  return;
767
  }
768
-
769
- const btn = document.getElementById('folderConfirmBtn');
770
- const btnText = document.getElementById('folderBtnText');
771
- const spinner = document.getElementById('folderBtnSpinner');
772
-
773
- btn.disabled = true;
774
- btnText.style.display = 'none';
775
- spinner.style.display = 'inline-block';
776
-
777
- try {
778
- const folderPath = currentFolderPath ? `${currentFolderPath}/${folderName}` : folderName;
779
- const response = await fetch('/create_folder', {
780
- method: 'POST',
781
- headers: { 'Content-Type': 'application/json' },
782
- body: JSON.stringify({ path: folderPath })
783
- });
784
-
785
- if (response.ok) {
786
- closeModal('folderModal');
787
- location.reload();
788
- } else {
789
- alert('Failed to create folder');
790
- }
791
- } catch (error) {
792
- console.error('Create folder failed:', error);
793
- alert('An error occurred while creating folder');
794
- } finally {
795
- btn.disabled = false;
796
- btnText.style.display = 'inline-block';
797
- spinner.style.display = 'none';
798
- }
799
  }
800
 
801
- // Close dropdowns when clicking outside
802
- document.addEventListener('click', function(e) {
803
  if (!e.target.closest('.dropdown')) {
804
  closeAllDropdowns();
805
  }
806
-
807
- // Close modals when clicking on overlay
808
  if (e.target.classList.contains('modal-overlay')) {
809
  closeModal(e.target.id);
810
  }
811
  });
812
 
813
- // Handle file input change for better UX
814
  document.getElementById('fileInput').addEventListener('change', function(e) {
815
- if (e.target.files.length > 0) {
816
- e.target.closest('form').submit();
817
- }
818
- });
819
-
820
- // Handle Enter key in modals
821
- document.getElementById('renameInput').addEventListener('keypress', function(e) {
822
- if (e.key === 'Enter') {
823
- confirmRename();
824
  }
825
  });
 
 
 
 
 
 
 
 
 
826
 
827
- document.getElementById('folderInput').addEventListener('keypress', function(e) {
828
- if (e.key === 'Enter') {
829
- confirmCreateFolder();
830
- }
831
- });
832
 
833
- // Focus management for accessibility
834
- document.addEventListener('keydown', function(e) {
835
  if (e.key === 'Escape') {
836
  const openModal = document.querySelector('.modal-overlay.active');
837
  if (openModal) {
 
12
 
13
  TEMPLATE = """
14
  <!DOCTYPE html>
15
+ <html lang="en">
16
  <head>
17
  <meta charset="UTF-8">
18
  <title>HuggingFace Drive - {{ path or 'Root' }}</title>
 
22
  --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
23
  --danger-gradient: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
24
  --warning-gradient: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
25
+ --bg-dark: #111827;
26
+ --bg-darker: #0d1117;
27
+ --bg-card: rgba(31, 41, 55, 0.5);
28
+ --bg-modal: #1f2937;
29
  --border-light: rgba(255, 255, 255, 0.1);
30
+ --text-light: #e5e7eb;
31
+ --text-lighter: #f9fafb;
32
+ --text-muted: #9ca3af;
33
  --shadow-primary: 0 8px 32px rgba(102, 126, 234, 0.3);
34
+ --shadow-large: 0 20px 60px rgba(0, 0, 0, 0.5);
 
35
  --radius-lg: 16px;
36
  --radius-md: 12px;
37
  --radius-sm: 8px;
38
+ --transition-speed: 0.3s;
39
  }
40
 
41
  * {
 
45
  }
46
 
47
  body {
48
+ background-color: var(--bg-darker);
49
+ background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.05) 1px, transparent 0);
50
+ background-size: 25px 25px;
51
  color: var(--text-light);
52
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
53
  min-height: 100vh;
54
  line-height: 1.5;
55
  }
56
+
 
 
 
 
 
 
 
 
 
 
 
57
  .container {
58
+ max-width: 1280px;
59
  margin: 0 auto;
60
+ padding: 40px 24px;
61
  }
62
 
63
  .header {
64
+ background: var(--bg-card);
65
  backdrop-filter: blur(10px);
66
  border-radius: var(--radius-lg);
67
  padding: 24px;
68
+ margin-bottom: 32px;
69
  border: 1px solid var(--border-light);
70
  box-shadow: var(--shadow-large);
71
  }
72
 
73
  .title {
74
+ font-size: 2.25rem;
75
  font-weight: 700;
76
  margin-bottom: 16px;
77
  background: var(--primary-gradient);
78
  -webkit-background-clip: text;
79
  -webkit-text-fill-color: transparent;
80
  background-clip: text;
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 12px;
84
  }
85
 
86
  .breadcrumb {
87
  display: flex;
88
  align-items: center;
89
  gap: 8px;
90
+ margin-bottom: 24px;
91
  flex-wrap: wrap;
92
  }
93
 
94
  .breadcrumb-item {
95
+ background: rgba(255, 255, 255, 0.05);
96
  color: var(--text-muted);
97
  padding: 6px 12px;
98
+ border-radius: var(--radius-sm);
99
  font-size: 0.875rem;
100
  cursor: pointer;
101
+ transition: all var(--transition-speed) ease;
102
+ border: 1px solid transparent;
103
  white-space: nowrap;
104
  max-width: 200px;
105
  overflow: hidden;
106
  text-overflow: ellipsis;
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 6px;
110
  }
111
 
112
  .breadcrumb-item:hover {
 
118
  .breadcrumb-item.active {
119
  background: var(--primary-gradient);
120
  color: white;
121
+ font-weight: 600;
122
+ border-color: rgba(255,255,255,0.2);
123
  }
124
 
125
  .breadcrumb-separator {
126
  color: var(--text-muted);
 
127
  }
128
 
129
  .actions {
130
  display: flex;
131
  gap: 12px;
 
132
  flex-wrap: wrap;
133
  }
134
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  .btn {
136
  background: var(--primary-gradient);
137
  color: white;
138
  border: none;
139
+ padding: 12px 20px;
140
  border-radius: var(--radius-md);
141
  cursor: pointer;
142
  font-weight: 600;
143
+ font-size: 0.9rem;
144
+ transition: all var(--transition-speed) ease;
145
+ box-shadow: 0 4px 15px rgba(0,0,0,0.2);
146
  border: 1px solid var(--border-light);
147
  display: inline-flex;
148
  align-items: center;
149
+ justify-content: center;
150
  gap: 8px;
151
  }
152
 
153
  .btn:hover {
154
  transform: translateY(-2px);
155
+ box-shadow: var(--shadow-primary);
156
+ }
157
+
158
+ .btn:disabled {
159
+ opacity: 0.6;
160
+ cursor: not-allowed;
161
+ transform: translateY(0);
162
+ box-shadow: none;
163
  }
164
 
165
  .btn-secondary {
166
  background: rgba(255, 255, 255, 0.1);
167
  color: var(--text-lighter);
 
168
  }
169
 
170
  .btn-secondary:hover {
171
  background: rgba(255, 255, 255, 0.15);
172
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
173
  }
174
+
175
+ .btn-danger {
176
+ background: var(--danger-gradient);
177
+ }
178
+ .btn-danger:hover {
179
+ box-shadow: 0 8px 25px rgba(239, 68, 68, 0.4);
180
+ }
181
+
182
+ .file-input-wrapper {
183
+ position: relative;
184
+ overflow: hidden;
185
+ display: inline-block;
186
+ }
187
+
188
+ .file-input {
189
+ position: absolute;
190
+ left: 0;
191
+ top: 0;
192
+ width: 100%;
193
+ height: 100%;
194
+ opacity: 0;
195
+ cursor: pointer;
196
+ }
197
 
198
  .file-grid {
199
  display: grid;
200
+ gap: 24px;
201
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
202
  }
203
 
204
  .file-item {
205
+ background: var(--bg-card);
206
  backdrop-filter: blur(10px);
207
  border: 1px solid var(--border-light);
208
  border-radius: var(--radius-lg);
209
+ transition: all var(--transition-speed) ease;
 
 
210
  position: relative;
 
211
  }
212
 
213
  .file-item:hover {
 
216
  border-color: rgba(102, 126, 234, 0.3);
217
  }
218
 
219
+ .file-item-clickable {
220
+ display: block;
221
+ padding: 16px;
222
+ cursor: pointer;
223
+ text-decoration: none;
224
+ }
225
+
226
  .file-content {
227
  display: flex;
228
  align-items: center;
229
  justify-content: space-between;
 
 
230
  gap: 12px;
231
  }
232
 
 
245
  display: flex;
246
  align-items: center;
247
  justify-content: center;
248
+ border-radius: var(--radius-md);
249
  background: rgba(255, 255, 255, 0.1);
250
+ color: var(--text-lighter);
251
  flex-shrink: 0;
252
  }
253
+
254
  .folder-icon {
255
  background: var(--warning-gradient);
256
  }
257
 
258
  .file-name {
259
  font-weight: 500;
 
260
  color: var(--text-lighter);
261
  white-space: nowrap;
262
  overflow: hidden;
 
264
  }
265
 
266
  .dropdown {
267
+ position: absolute;
268
+ top: 12px;
269
+ right: 12px;
270
  }
271
+
272
  .dropdown-toggle {
273
  background: rgba(255, 255, 255, 0.1);
274
+ border: 1px solid transparent;
275
  color: var(--text-muted);
276
+ border-radius: var(--radius-md);
 
277
  cursor: pointer;
278
+ transition: all var(--transition-speed) ease;
 
279
  width: 36px;
280
  height: 36px;
281
  display: flex;
 
286
  .dropdown-toggle:hover {
287
  background: rgba(255, 255, 255, 0.2);
288
  color: var(--text-lighter);
289
+ border-color: var(--border-light);
290
  }
291
 
292
  .dropdown-menu {
293
  position: absolute;
294
+ top: calc(100% + 8px);
295
  right: 0;
296
+ background: var(--bg-modal);
 
297
  border: 1px solid var(--border-light);
298
  border-radius: var(--radius-md);
299
  padding: 8px;
300
+ min-width: 180px;
301
  opacity: 0;
302
  visibility: hidden;
303
  transform: translateY(-10px);
304
+ transition: all var(--transition-speed) ease;
305
  z-index: 1000;
306
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
307
  }
 
315
  .dropdown-item {
316
  display: flex;
317
  align-items: center;
318
+ gap: 10px;
319
  padding: 10px 12px;
320
  border-radius: var(--radius-sm);
321
  cursor: pointer;
322
+ transition: background-color 0.2s ease, color 0.2s ease;
323
  font-size: 0.875rem;
324
+ font-weight: 500;
325
  color: var(--text-muted);
326
  }
327
 
 
350
  z-index: 2000;
351
  opacity: 0;
352
  visibility: hidden;
353
+ transition: all var(--transition-speed) ease;
354
  }
355
 
356
  .modal-overlay.active {
 
359
  }
360
 
361
  .modal {
362
+ background: var(--bg-modal);
 
363
  border: 1px solid var(--border-light);
364
  border-radius: var(--radius-lg);
365
  padding: 24px;
366
  max-width: 450px;
367
  width: 90%;
368
+ transform: scale(0.95);
369
+ transition: all var(--transition-speed) ease;
370
  box-shadow: var(--shadow-large);
371
  }
372
+
373
  .modal-overlay.active .modal {
374
  transform: scale(1);
375
  }
 
388
 
389
  .modal-input {
390
  width: 100%;
391
+ background: rgba(255, 255, 255, 0.05);
392
+ border: 1px solid var(--border-light);
393
  border-radius: var(--radius-md);
394
  padding: 12px 16px;
395
  color: var(--text-lighter);
396
  font-size: 1rem;
397
+ transition: all var(--transition-speed) ease;
 
398
  }
399
 
400
  .modal-input:focus {
401
  outline: none;
402
  border-color: #667eea;
403
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
404
  }
405
 
406
  .modal-actions {
 
408
  gap: 12px;
409
  justify-content: flex-end;
410
  }
411
+
412
+ /* Utility & Helper Classes */
413
+ .icon {
414
+ width: 1.25em;
415
+ height: 1.25em;
416
  }
417
+ .loading-spinner {
 
 
 
 
 
 
 
 
 
 
 
418
  display: inline-block;
419
  width: 20px;
420
  height: 20px;
421
  border: 3px solid rgba(255, 255, 255, 0.3);
422
  border-radius: 50%;
423
+ border-top-color: #fff;
424
  animation: spin 1s ease-in-out infinite;
425
  }
426
+ .btn .loading-spinner {
427
+ border-top-color: #fff;
428
+ }
429
 
430
  @keyframes spin {
431
  to { transform: rotate(360deg); }
432
  }
433
 
434
+ .upload-overlay {
435
+ position: fixed;
436
+ top: 0;
437
+ left: 0;
438
+ width: 100%;
439
+ height: 100%;
440
+ background: rgba(0, 0, 0, 0.8);
441
+ backdrop-filter: blur(8px);
442
+ display: flex;
443
+ flex-direction: column;
444
+ align-items: center;
445
+ justify-content: center;
446
+ z-index: 9999;
447
+ color: var(--text-lighter);
448
+ gap: 16px;
449
+ opacity: 0;
450
+ visibility: hidden;
451
+ transition: all var(--transition-speed) ease;
452
+ }
453
+ .upload-overlay.active {
454
+ opacity: 1;
455
+ visibility: visible;
456
+ }
457
+ .upload-overlay .loading-spinner {
458
+ width: 48px;
459
+ height: 48px;
460
+ border-width: 4px;
461
+ }
462
+ .upload-overlay-text {
463
+ font-size: 1.25rem;
464
+ font-weight: 500;
465
+ }
466
+
467
  /* Responsive Design */
468
  @media (max-width: 768px) {
469
  .container {
470
+ padding: 24px 16px;
471
+ }
472
+ .title {
473
+ font-size: 1.75rem;
474
  }
 
475
  .file-grid {
476
  grid-template-columns: 1fr;
477
+ gap: 16px;
478
  }
 
479
  .actions {
480
  flex-direction: column;
481
+ align-items: stretch;
482
+ }
483
+ .btn {
484
+ width: 100%;
485
  }
 
486
  .breadcrumb {
487
  gap: 6px;
488
  }
 
489
  .breadcrumb-separator {
490
  display: none;
491
  }
 
 
 
 
 
 
 
 
 
492
  .modal-actions {
493
  flex-direction: column-reverse;
 
494
  }
 
495
  .modal-actions .btn {
496
+ width: 100%;
497
  }
498
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  </style>
500
  </head>
501
  <body>
502
+
503
+ <!-- Upload Overlay -->
504
+ <div class="upload-overlay" id="uploadOverlay">
505
+ <div class="loading-spinner"></div>
506
+ <p class="upload-overlay-text">Uploading...</p>
507
+ </div>
508
+
509
  <div class="container">
510
+ <header class="header">
511
+ <h1 class="title">
512
+ <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15.59 2.76a1 1 0 0 1 .45 1.08L14.5 11h-5l-1.54-7.16a1 1 0 0 1 .45-1.08l7-4.04a1 1 0 0 1 1.18.01z"></path><path d="M12 11v10"></path><path d="M12 21a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"></path><path d="M6 11H2.5a1 1 0 0 0-1 .9l-1.45 7.1A1 1 0 0 0 1 21h4.05"></path><path d="M22.9 11.9a1 1 0 0 0-1-.9H18"></path><path d="M19.45 20.01A1 1 0 0 0 20 21h3a1 1 0 0 0 .9-1.1l-1.45-7.1z"></path></svg>
513
+ <span>HuggingFace Drive</span>
514
+ </h1>
515
 
516
  <!-- Breadcrumb Navigation -->
517
+ <nav class="breadcrumb">
518
  <span class="breadcrumb-item {{ 'active' if not path else '' }}" onclick="nav('')">
519
+ <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" /></svg>
520
+ Home
521
  </span>
522
  {% if path %}
523
  {% set parts = path.split('/') %}
 
526
  {% set current_path = parts[:i+1]|join('/') %}
527
  <span class="breadcrumb-item {{ 'active' if current_path == path else '' }}"
528
  onclick="nav('{{ current_path }}')">
529
+ <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"><path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" /></svg>
530
+ {{ parts[i] }}
531
  </span>
532
  {% endfor %}
533
  {% endif %}
534
+ </nav>
 
535
  <!-- Actions -->
536
  <div class="actions">
537
+ <form action="/upload" method="post" enctype="multipart/form-data" id="uploadForm">
538
+ <div class="file-input-wrapper">
539
+ <label for="fileInput" class="btn">
540
+ <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM6.293 6.707a1 1 0 010-1.414l3-3a1 1 0 011.414 0l3 3a1 1 0 01-1.414 1.414L11 5.414V13a1 1 0 11-2 0V5.414L7.707 6.707a1 1 0 01-1.414 0z" clip-rule="evenodd" /></svg>
541
+ Upload File
542
+ </label>
543
+ <input type="file" name="file" required class="file-input" id="fileInput">
544
+ <input type="hidden" name="path" value="{{ path }}">
545
+ </div>
546
  </form>
547
  <button class="btn btn-secondary" onclick="showFolderModal('{{ path }}')">
548
+ <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"><path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" /></svg>
549
+ New Folder
550
  </button>
551
  </div>
552
+ </header>
 
553
  <!-- File Grid -->
554
+ <main class="file-grid">
555
  {% for item in items %}
556
  <div class="file-item">
557
+ <div class="file-item-clickable" onclick="{% if item.type=='dir' %}nav('{{ item.path }}'){% else %}download('{{ item.path }}'){% endif %}">
558
+ <div class="file-content">
559
+ <div class="file-info">
560
+ <div class="file-icon {{ 'folder-icon' if item.type=='dir' }}">
561
+ {% if item.type == 'dir' %}
562
+ <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"><path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" /></svg>
563
+ {% else %}
564
+ <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd" /></svg>
565
+ {% endif %}
566
+ </div>
567
+ <div class="file-name" title="{{ item.name }}">{{ item.name }}</div>
568
  </div>
 
569
  </div>
570
+ </div>
571
+ <div class="dropdown">
572
+ <button class="dropdown-toggle" onclick="toggleDropdown(event, this)">
573
+ <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"><path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" /></svg>
574
+ </button>
575
+ <div class="dropdown-menu">
576
+ {% if item.type == 'file' %}
577
+ <div class="dropdown-item" onclick="download('{{ item.path }}')">
578
+ <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
579
+ Download
 
 
 
 
580
  </div>
581
+ {% endif %}
582
+ <div class="dropdown-item" onclick="showRenameModal('{{ item.path }}', '{{ item.name }}')">
583
+ <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" /></svg>
584
+ Rename
585
+ </div>
586
+ <div class="dropdown-item danger" onclick="showDeleteModal('{{ item.path }}', '{{ item.name }}')">
587
+ <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" /></svg>
588
+ Delete
589
  </div>
590
  </div>
591
  </div>
592
  </div>
593
  {% endfor %}
594
+ </main>
595
  </div>
596
+ <!-- Modals -->
 
597
  <div class="modal-overlay" id="renameModal">
598
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="renameModalTitle">
599
+ <h3 class="modal-title" id="renameModalTitle">Rename Item</h3>
600
  <div class="modal-body">
601
  <input type="text" class="modal-input" id="renameInput" placeholder="Enter new name" autocomplete="off">
602
  </div>
 
604
  <button class="btn btn-secondary" onclick="closeModal('renameModal')">Cancel</button>
605
  <button class="btn" onclick="confirmRename()" id="renameConfirmBtn">
606
  <span id="renameBtnText">Rename</span>
607
+ <span id="renameBtnSpinner" class="loading-spinner" style="display: none;"></span>
608
  </button>
609
  </div>
610
  </div>
611
  </div>
 
 
612
  <div class="modal-overlay" id="deleteModal">
613
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="deleteModalTitle">
614
+ <h3 class="modal-title" id="deleteModalTitle">Confirm Deletion</h3>
615
  <div class="modal-body">
616
  <p>Are you sure you want to delete <strong id="deleteItemName"></strong>? This action cannot be undone.</p>
617
  </div>
 
619
  <button class="btn btn-secondary" onclick="closeModal('deleteModal')">Cancel</button>
620
  <button class="btn btn-danger" onclick="confirmDelete()" id="deleteConfirmBtn">
621
  <span id="deleteBtnText">Delete</span>
622
+ <span id="deleteBtnSpinner" class="loading-spinner" style="display: none;"></span>
623
  </button>
624
  </div>
625
  </div>
626
  </div>
 
 
627
  <div class="modal-overlay" id="folderModal">
628
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="folderModalTitle">
629
+ <h3 class="modal-title" id="folderModalTitle">Create New Folder</h3>
630
  <div class="modal-body">
631
  <input type="text" class="modal-input" id="folderInput" placeholder="Enter folder name" autocomplete="off">
632
  </div>
 
634
  <button class="btn btn-secondary" onclick="closeModal('folderModal')">Cancel</button>
635
  <button class="btn" onclick="confirmCreateFolder()" id="folderConfirmBtn">
636
  <span id="folderBtnText">Create</span>
637
+ <span id="folderBtnSpinner" class="loading-spinner" style="display: none;"></span>
638
  </button>
639
  </div>
640
  </div>
 
646
  let currentFolderPath = '';
647
  let activeDropdown = null;
648
 
 
649
  function nav(p) {
650
  location.href = '/?path=' + encodeURIComponent(p);
651
  }
652
+
 
653
  function download(path) {
654
  window.open('/download?path=' + encodeURIComponent(path), '_blank');
655
  }
656
 
657
+ function toggleDropdown(event, button) {
 
658
  event.stopPropagation();
659
  const dropdown = button.closest('.dropdown');
660
 
 
661
  if (activeDropdown && activeDropdown !== dropdown) {
662
  activeDropdown.classList.remove('active');
663
  }
 
666
  activeDropdown = dropdown.classList.contains('active') ? dropdown : null;
667
  }
668
 
669
+ const showSpinner = (btnId, textId, spinnerId) => {
670
+ document.getElementById(btnId).disabled = true;
671
+ document.getElementById(textId).style.display = 'none';
672
+ document.getElementById(spinnerId).style.display = 'inline-block';
673
+ }
674
+
675
+ const hideSpinner = (btnId, textId, spinnerId) => {
676
+ document.getElementById(btnId).disabled = false;
677
+ document.getElementById(textId).style.display = 'inline-block';
678
+ document.getElementById(spinnerId).style.display = 'none';
679
+ }
680
+
681
+ function showModal(modalId) {
682
+ document.getElementById(modalId).classList.add('active');
683
+ document.body.style.overflow = 'hidden';
684
+ }
685
+
686
+ function closeModal(modalId) {
687
+ document.getElementById(modalId).classList.remove('active');
688
+ document.body.style.overflow = '';
689
+ }
690
+
691
  function showRenameModal(path, currentName) {
692
  currentRenamePath = path;
693
  document.getElementById('renameInput').value = currentName;
 
699
  closeAllDropdowns();
700
  }
701
 
 
702
  function showDeleteModal(path, name) {
703
  currentDeletePath = path;
704
  document.getElementById('deleteItemName').textContent = name;
 
706
  closeAllDropdowns();
707
  }
708
 
 
709
  function showFolderModal(path) {
710
  currentFolderPath = path;
711
  document.getElementById('folderInput').value = '';
 
713
  setTimeout(() => document.getElementById('folderInput').focus(), 100);
714
  }
715
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716
  function closeAllDropdowns() {
717
+ if (activeDropdown) {
718
+ activeDropdown.classList.remove('active');
719
+ activeDropdown = null;
720
+ }
721
+ }
722
+
723
+ async function performAction(url, body, btnId, textId, spinnerId, modalToClose) {
724
+ showSpinner(btnId, textId, spinnerId);
725
+ try {
726
+ const response = await fetch(url, {
727
+ method: 'POST',
728
+ headers: { 'Content-Type': 'application/json' },
729
+ body: JSON.stringify(body)
730
+ });
731
+ if (response.ok) {
732
+ if (modalToClose) closeModal(modalToClose);
733
+ location.reload();
734
+ } else {
735
+ const error = await response.text();
736
+ alert(`Operation failed: ${error}`);
737
+ }
738
+ } catch (error) {
739
+ console.error('Action failed:', error);
740
+ alert('An unexpected error occurred.');
741
+ } finally {
742
+ hideSpinner(btnId, textId, spinnerId);
743
+ }
744
  }
745
 
746
+ function confirmRename() {
 
747
  const newName = document.getElementById('renameInput').value.trim();
748
  if (!newName) {
749
  alert('Please enter a valid name');
750
  return;
751
  }
752
+ performAction('/rename', { old_path: currentRenamePath, new_path: newName }, 'renameConfirmBtn', 'renameBtnText', 'renameBtnSpinner', 'renameModal');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753
  }
754
 
755
+ function confirmDelete() {
756
+ performAction('/delete', { path: currentDeletePath }, 'deleteConfirmBtn', 'deleteBtnText', 'deleteBtnSpinner', 'deleteModal');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
757
  }
758
 
759
+ function confirmCreateFolder() {
 
760
  const folderName = document.getElementById('folderInput').value.trim();
761
  if (!folderName) {
762
  alert('Please enter a folder name');
763
  return;
764
  }
765
+ const folderPath = currentFolderPath ? `${currentFolderPath}/${folderName}` : folderName;
766
+ performAction('/create_folder', { path: folderPath }, 'folderConfirmBtn', 'folderBtnText', 'folderBtnSpinner', 'folderModal');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
767
  }
768
 
769
+ // Event Listeners
770
+ document.addEventListener('click', (e) => {
771
  if (!e.target.closest('.dropdown')) {
772
  closeAllDropdowns();
773
  }
 
 
774
  if (e.target.classList.contains('modal-overlay')) {
775
  closeModal(e.target.id);
776
  }
777
  });
778
 
 
779
  document.getElementById('fileInput').addEventListener('change', function(e) {
780
+ if (this.files.length > 0) {
781
+ document.getElementById('uploadOverlay').classList.add('active');
782
+ document.getElementById('uploadForm').submit();
 
 
 
 
 
 
783
  }
784
  });
785
+
786
+ const setupModalKeyListener = (inputId, confirmFunction) => {
787
+ document.getElementById(inputId).addEventListener('keypress', (e) => {
788
+ if (e.key === 'Enter') {
789
+ e.preventDefault();
790
+ confirmFunction();
791
+ }
792
+ });
793
+ };
794
 
795
+ setupModalKeyListener('renameInput', confirmRename);
796
+ setupModalKeyListener('folderInput', confirmCreateFolder);
 
 
 
797
 
798
+ document.addEventListener('keydown', (e) => {
 
799
  if (e.key === 'Escape') {
800
  const openModal = document.querySelector('.modal-overlay.active');
801
  if (openModal) {