testdeep123 commited on
Commit
b0a1492
·
verified ·
1 Parent(s): 62c5efa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +498 -679
app.py CHANGED
@@ -1,169 +1,123 @@
1
- import os
2
- import tempfile
3
- import requests
4
- from urllib.parse import urlparse, unquote
5
- from flask import Flask, render_template_string, request, redirect, send_file, jsonify
6
- from huggingface_hub import HfApi, hf_hub_download, upload_file, delete_file
7
 
8
- # Environment
9
- REPO_ID = os.getenv("REPO_ID")
10
- HF_TOKEN = os.getenv("HF_TOKEN")
11
-
12
- app = Flask(__name__)
13
- api = HfApi()
14
-
15
- TEMPLATE = """
16
  <!DOCTYPE html>
17
  <html lang="en">
18
  <head>
19
  <meta charset="UTF-8">
20
  <title>HuggingFace Drive - {{ path or 'Root' }}</title>
21
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
22
- <link rel="preconnect" href="https://fonts.googleapis.com">
23
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
24
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
25
  <style>
26
- /*
27
- =================================
28
- --- REDESIGNED CSS (v2.0) ---
29
- --- Author: AI Assistant ---
30
- --- Theme: Aurora Glass ---
31
- =================================
32
- */
33
-
34
- /* --- 1. Root Variables & Base Styles --- */
35
- :root {
36
- --bg-color: #0d1117;
37
- --glass-bg: rgba(22, 27, 34, 0.6);
38
- --border-color: rgba(255, 255, 255, 0.1);
39
- --border-color-hover: rgba(56, 189, 248, 0.4);
40
- --text-primary: #e6edf3;
41
- --text-secondary: #7d8590;
42
- --accent-primary: #38bdf8; /* Light Sky Blue */
43
- --accent-primary-hover: #7dd3fc;
44
- --accent-success: #2dd4bf; /* Teal */
45
- --accent-danger: #f87171; /* Light Red */
46
- --shadow-color: rgba(0, 0, 0, 0.2);
47
- --glow-color: rgba(56, 189, 248, 0.2);
48
-
49
- --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
50
- --radius-md: 10px;
51
- --radius-sm: 6px;
52
- --transition-fast: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1);
53
- --transition-slow: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
54
- }
55
-
56
  * {
57
  margin: 0;
58
  padding: 0;
59
  box-sizing: border-box;
 
60
  }
61
 
62
- @keyframes aurora {
63
- 0% { background-position: 0% 50%; }
64
- 50% { background-position: 100% 50%; }
65
- 100% { background-position: 0% 50%; }
 
 
 
 
 
 
 
 
 
 
 
 
66
  }
67
 
68
  body {
69
- background-color: var(--bg-color);
70
- background-image: radial-gradient(at 20% 10%, hsla(212,80%,30%,0.3) 0px, transparent 50%),
71
- radial-gradient(at 80% 20%, hsla(280,70%,40%,0.2) 0px, transparent 50%),
72
- radial-gradient(at 50% 80%, hsla(180,80%,35%,0.3) 0px, transparent 50%);
73
- background-attachment: fixed;
74
- background-size: 200% 200%;
75
- animation: aurora 15s ease infinite;
76
- color: var(--text-primary);
77
- font-family: var(--font-family);
78
- line-height: 1.6;
79
  min-height: 100vh;
80
  overflow-x: hidden;
81
  }
82
 
83
- /* --- 2. Main Layout & Header --- */
84
  .container {
85
- max-width: 1600px;
86
  margin: 0 auto;
87
- padding: 2rem;
88
  }
89
 
 
90
  .header {
91
- background: var(--glass-bg);
92
- backdrop-filter: blur(20px);
93
- -webkit-backdrop-filter: blur(20px);
94
- border: 1px solid var(--border-color);
95
- border-radius: var(--radius-md);
96
- padding: 1.5rem;
97
- margin-bottom: 2.5rem;
98
  display: flex;
99
  flex-wrap: wrap;
100
  align-items: center;
101
  justify-content: space-between;
102
- gap: 1.5rem;
103
- box-shadow: 0 8px 32px 0 var(--shadow-color);
 
 
 
 
 
104
  }
105
 
106
  .title {
107
  display: flex;
108
  align-items: center;
109
  gap: 0.75rem;
110
- font-size: 1.5rem;
111
  font-weight: 700;
112
- color: var(--text-primary);
113
  }
114
 
115
  .title-icon {
116
  width: 28px;
117
  height: 28px;
118
- color: var(--accent-primary);
119
- transition: var(--transition-fast);
120
  }
 
121
  .title:hover .title-icon {
122
- transform: rotate(-10deg) scale(1.1);
123
  }
124
 
125
- /* --- 3. Breadcrumb Navigation --- */
126
  .breadcrumb {
127
  display: flex;
128
- align-items: center;
129
  flex-wrap: wrap;
130
- gap: 0.25rem;
131
- background: rgba(0,0,0,0.2);
132
- padding: 0.5rem;
133
- border-radius: var(--radius-sm);
134
  }
135
 
136
  .breadcrumb-item {
137
  display: flex;
138
  align-items: center;
139
  gap: 0.5rem;
140
- padding: 0.4rem 0.8rem;
141
- border-radius: 4px;
142
  cursor: pointer;
143
- color: var(--text-secondary);
144
- transition: var(--transition-fast);
145
- font-size: 0.9rem;
146
- font-weight: 500;
147
  }
148
 
149
  .breadcrumb-item:hover {
150
- background: rgba(255, 255, 255, 0.1);
151
- color: var(--text-primary);
 
152
  }
153
 
154
  .breadcrumb-item.active {
155
- color: var(--accent-primary);
156
- background: rgba(56, 189, 248, 0.1);
157
  }
158
 
159
  .breadcrumb-separator {
160
- color: var(--text-secondary);
161
  user-select: none;
162
- font-size: 1rem;
163
- opacity: 0.5;
164
  }
165
 
166
- /* --- 4. Action Buttons --- */
167
  .actions {
168
  display: flex;
169
  flex-wrap: wrap;
@@ -173,159 +127,178 @@ body {
173
  .btn {
174
  display: inline-flex;
175
  align-items: center;
176
- justify-content: center;
177
  gap: 0.5rem;
178
- padding: 0.7rem 1.25rem;
179
- border: 1px solid transparent;
180
- border-radius: var(--radius-sm);
181
- font-size: 0.9rem;
182
- font-weight: 600;
183
  cursor: pointer;
184
- transition: var(--transition-fast);
185
  text-decoration: none;
186
- white-space: nowrap;
 
187
  }
188
- .btn .icon {
189
- width: 16px;
190
- height: 16px;
 
 
 
 
 
 
 
191
  }
192
 
193
- .btn:hover {
194
- transform: translateY(-2px);
195
  }
196
 
197
  .btn-primary {
198
- background: var(--accent-primary);
199
- color: var(--bg-color);
200
- border-color: var(--accent-primary);
201
  }
 
202
  .btn-primary:hover {
203
- background: var(--accent-primary-hover);
204
- border-color: var(--accent-primary-hover);
205
- box-shadow: 0 4px 15px rgba(56, 189, 248, 0.2);
206
  }
207
 
208
  .btn-secondary {
209
- background: var(--glass-bg);
210
- color: var(--text-primary);
211
- border: 1px solid var(--border-color);
212
  }
 
213
  .btn-secondary:hover {
214
- background: rgba(255, 255, 255, 0.1);
215
- border-color: var(--border-color-hover);
 
216
  }
217
 
218
  .btn-success {
219
- background: var(--accent-success);
220
- color: var(--bg-color);
221
- border-color: var(--accent-success);
222
  }
 
223
  .btn-success:hover {
224
- filter: brightness(1.1);
225
- box-shadow: 0 4px 15px rgba(45, 212, 191, 0.2);
 
226
  }
227
 
228
  .btn-danger {
229
- background: var(--accent-danger);
230
- color: var(--bg-color);
231
- border-color: var(--accent-danger);
232
  }
 
233
  .btn-danger:hover {
234
- filter: brightness(1.1);
235
- box-shadow: 0 4px 15px rgba(248, 113, 113, 0.2);
 
236
  }
237
 
238
  .file-input-wrapper {
239
  position: relative;
 
240
  display: inline-block;
241
  }
 
242
  .file-input {
243
  position: absolute;
244
  left: -9999px;
245
  opacity: 0;
246
  }
247
 
248
- /* --- 5. File Grid & Items --- */
249
- @keyframes fadeInGrid {
250
- from { opacity: 0; transform: translateY(20px); }
251
- to { opacity: 1; transform: translateY(0); }
252
  }
253
 
 
254
  .file-grid {
255
  display: grid;
256
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
257
- gap: 1.5rem;
258
- animation: fadeInGrid 0.5s ease-out forwards;
259
  }
260
 
261
  .file-item {
262
- background: var(--glass-bg);
263
- border: 1px solid var(--border-color);
264
- border-radius: var(--radius-md);
265
- transition: var(--transition-fast);
266
  position: relative;
 
267
  }
268
 
269
  .file-item:hover {
270
- transform: translateY(-5px) scale(1.02);
271
- border-color: var(--border-color-hover);
272
- box-shadow: 0 0 20px 0 var(--glow-color);
273
- z-index: 10;
274
  }
275
 
276
  .file-item-content {
277
- padding: 1rem;
278
  display: flex;
279
  align-items: center;
280
  gap: 1rem;
281
  cursor: pointer;
282
- height: 100%;
283
  }
284
 
285
- .file-icon-wrapper {
286
- flex-shrink: 0;
 
 
 
 
 
 
287
  width: 48px;
288
  height: 48px;
289
- border-radius: var(--radius-sm);
290
- display: grid;
291
- place-items: center;
292
- transition: var(--transition-fast);
293
- }
294
- .file-icon-wrapper .icon {
295
- width: 24px;
296
- height: 24px;
297
  }
298
 
299
- .file-icon-wrapper.dir {
300
- background: rgba(56, 189, 248, 0.1);
301
- color: var(--accent-primary);
302
  }
303
- .file-icon-wrapper.file {
304
- background: rgba(45, 212, 191, 0.1);
305
- color: var(--accent-success);
 
306
  }
307
- .file-item:hover .file-icon-wrapper {
 
308
  transform: scale(1.1);
309
  }
310
 
311
  .file-details {
312
- overflow: hidden;
313
  }
314
 
315
  .file-name {
316
  font-weight: 500;
317
- white-space: nowrap;
318
- overflow: hidden;
319
- text-overflow: ellipsis;
320
- color: var(--text-primary);
321
  }
322
 
323
  .file-meta {
324
- font-size: 0.875rem;
325
- color: var(--text-secondary);
326
  }
327
 
328
- /* --- 6. Dropdown Menu --- */
329
  .dropdown {
330
  position: absolute;
331
  top: 0.5rem;
@@ -335,89 +308,103 @@ body {
335
  .dropdown-toggle {
336
  background: none;
337
  border: none;
338
- color: var(--text-secondary);
339
  cursor: pointer;
340
- width: 32px;
341
- height: 32px;
342
- border-radius: 50%;
343
- display: grid;
344
- place-items: center;
345
- transition: var(--transition-fast);
346
  }
347
- .dropdown-toggle .icon { width: 18px; height: 18px; }
348
 
349
  .dropdown-toggle:hover {
350
- background: rgba(255, 255, 255, 0.1);
351
- color: var(--text-primary);
352
  }
353
 
354
  .dropdown-menu {
355
  position: absolute;
356
- top: calc(100% + 5px);
357
  right: 0;
358
- background: var(--glass-bg);
359
- backdrop-filter: blur(10px);
360
- border: 1px solid var(--border-color);
361
- border-radius: var(--radius-sm);
362
- box-shadow: 0 4px 12px var(--shadow-color);
363
  min-width: 180px;
364
  z-index: 1000;
365
  opacity: 0;
366
  visibility: hidden;
367
- transform: translateY(-10px) scale(0.95);
368
- transform-origin: top right;
369
- transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s;
370
- padding: 0.5rem;
371
  }
372
 
373
  .dropdown.active .dropdown-menu {
374
  opacity: 1;
375
  visibility: visible;
376
- transform: translateY(0) scale(1);
377
  }
378
 
379
  .dropdown-item {
380
  display: flex;
381
  align-items: center;
382
- gap: 0.75rem;
383
  width: 100%;
384
- padding: 0.6rem 0.8rem;
385
  border: none;
386
  background: none;
387
- color: var(--text-primary);
388
  font-size: 0.9rem;
389
- font-weight: 500;
390
  cursor: pointer;
391
- transition: var(--transition-fast);
392
  text-align: left;
393
- border-radius: 4px;
394
  }
395
- .dropdown-item .icon { width: 16px; height: 16px; color: var(--text-secondary); }
396
 
397
  .dropdown-item:hover {
398
- background: rgba(255, 255, 255, 0.1);
399
- color: var(--text-primary);
400
  }
401
- .dropdown-item:hover .icon {
402
- color: var(--text-primary);
 
403
  }
404
 
405
- .dropdown-item.danger { color: var(--accent-danger); }
406
- .dropdown-item.danger:hover { background: rgba(248, 113, 113, 0.1); }
407
- .dropdown-item.danger .icon { color: var(--accent-danger); }
 
 
 
 
 
 
 
408
 
409
- /* --- 7. Modals & Overlays --- */
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  .modal-overlay {
411
  position: fixed;
412
- inset: 0;
413
- background: rgba(13, 17, 23, 0.7);
414
- display: grid;
415
- place-items: center;
 
 
 
 
416
  z-index: 2000;
417
  opacity: 0;
418
  visibility: hidden;
419
- transition: var(--transition-fast);
420
- backdrop-filter: blur(8px);
421
  }
422
 
423
  .modal-overlay.active {
@@ -426,16 +413,16 @@ body {
426
  }
427
 
428
  .modal {
429
- background: var(--glass-bg);
430
- border: 1px solid var(--border-color);
431
- border-radius: var(--radius-md);
432
- box-shadow: 0 16px 60px 0 var(--shadow-color);
433
  width: 90%;
434
  max-width: 450px;
435
  max-height: 90vh;
436
  overflow-y: auto;
437
  transform: scale(0.95);
438
- transition: var(--transition-slow);
 
439
  }
440
 
441
  .modal-overlay.active .modal {
@@ -443,10 +430,9 @@ body {
443
  }
444
 
445
  .modal-title {
446
- padding: 1.5rem 1.5rem 0.5rem;
447
- font-size: 1.25rem;
448
  font-weight: 600;
449
- border-bottom: 1px solid var(--border-color);
450
  }
451
 
452
  .modal-body {
@@ -455,19 +441,19 @@ body {
455
 
456
  .modal-input {
457
  width: 100%;
458
- padding: 0.75rem 1rem;
459
- border: 1px solid var(--border-color);
460
- border-radius: var(--radius-sm);
461
- background: rgba(0,0,0,0.2);
462
- color: var(--text-primary);
463
- font-size: 1rem;
464
- transition: var(--transition-fast);
465
- font-family: var(--font-family);
466
  }
 
467
  .modal-input:focus {
468
  outline: none;
469
- border-color: var(--accent-primary);
470
- box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2);
471
  }
472
 
473
  .modal-actions {
@@ -477,11 +463,14 @@ body {
477
  justify-content: flex-end;
478
  }
479
 
480
- /* --- 8. Loading & Empty States --- */
481
  .upload-overlay {
482
  position: fixed;
483
- inset: 0;
484
- background: rgba(13, 17, 23, 0.85);
 
 
 
485
  display: flex;
486
  flex-direction: column;
487
  align-items: center;
@@ -489,96 +478,171 @@ body {
489
  z-index: 3000;
490
  opacity: 0;
491
  visibility: hidden;
492
- transition: var(--transition-fast);
493
- backdrop-filter: blur(10px);
494
  }
 
495
  .upload-overlay.active {
496
  opacity: 1;
497
  visibility: visible;
498
  }
499
 
500
- @keyframes spin {
501
- to { transform: rotate(360deg); }
502
- }
503
-
504
  .loading-spinner {
505
- width: 40px;
506
- height: 40px;
507
- border: 4px solid var(--border-color);
508
- border-top-color: var(--accent-primary);
509
  border-radius: 50%;
510
- animation: spin 0.8s linear infinite;
 
511
  }
 
512
  .upload-text {
513
- margin-top: 1.5rem;
514
- font-size: 1.1rem;
515
- font-weight: 500;
516
- color: var(--text-secondary);
517
  }
 
518
  .btn .loading-spinner {
519
- width: 18px;
520
- height: 18px;
521
  border-width: 3px;
522
  margin: 0;
523
  }
524
 
525
- .empty-state {
526
- text-align: center;
527
- padding: 4rem 1rem;
528
- color: var(--text-secondary);
529
- border: 2px dashed var(--border-color);
530
- border-radius: var(--radius-md);
531
- margin-top: 2rem;
532
  }
533
- .empty-state .icon {
534
- width: 64px;
535
- height: 64px;
536
- margin: 0 auto 1.5rem;
537
- opacity: 0.4;
 
 
 
 
 
538
  }
539
- .empty-state h3 {
540
- color: var(--text-primary);
541
- font-size: 1.5rem;
542
- margin-bottom: 0.5rem;
 
 
 
 
543
  }
544
 
545
- /* --- 9. Scrollbar & Responsive Design --- */
546
- ::-webkit-scrollbar { width: 12px; }
547
- ::-webkit-scrollbar-track { background: var(--bg-color); }
548
  ::-webkit-scrollbar-thumb {
549
- background-color: var(--border-color);
550
- border-radius: 10px;
551
- border: 3px solid var(--bg-color);
552
  }
553
- ::-webkit-scrollbar-thumb:hover { background-color: var(--text-secondary); }
554
 
 
 
 
 
 
555
  @media (max-width: 1024px) {
556
- .container { padding: 1.5rem; }
557
- .header { flex-direction: column; align-items: stretch; }
558
- .file-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
  }
560
 
561
  @media (max-width: 768px) {
562
- body { animation: none; } /* Disable heavy animation on mobile */
563
- .container { padding: 1rem; }
564
- .header { padding: 1rem; }
565
- .breadcrumb { justify-content: flex-start; }
566
- .actions { flex-direction: column; }
567
- .btn { width: 100%; }
568
- .file-grid { grid-template-columns: 1fr; gap: 1rem; }
569
- .modal { width: 95%; }
570
- .modal-actions { flex-direction: column-reverse; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
  }
572
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  @media (prefers-reduced-motion: reduce) {
574
  * {
575
  animation-duration: 0.01ms !important;
576
- animation-iteration-count: 1 !important;
577
  transition-duration: 0.01ms !important;
578
- scroll-behavior: auto !important;
579
  }
580
  }
581
- </style>
 
582
  </head>
583
  <body>
584
  <!-- Upload Overlay -->
@@ -590,8 +654,9 @@ body {
590
  <div class="container">
591
  <header class="header">
592
  <h1 class="title">
593
- <svg class="title-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
594
- <path d="M20 18H4V9.252a1 1 0 01.382-.78l1.698-1.357A2 2 0 017.152 7H9V5a2 2 0 012-2h2a2 2 0 012 2v2h1.848a2 2 0 011.072.315l1.698 1.357A1 1 0 0120 9.252V18zM18 9.873l-1.41-1.127a1 1 0 00-.536-.158H7.946a1 1 0 00-.536.158L6 9.873V16h12V9.873zM13 5h-2V4a1 1 0 011-1h0a1 1 0 011 1v1z"/>
 
595
  </svg>
596
  HuggingFace Drive
597
  </h1>
@@ -599,7 +664,7 @@ body {
599
  <!-- Breadcrumb -->
600
  <nav class="breadcrumb">
601
  <span class="breadcrumb-item {{ 'active' if not path else '' }}" onclick="nav('')">
602
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
603
  <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"/>
604
  </svg>
605
  Home
@@ -607,10 +672,13 @@ body {
607
  {% if path %}
608
  {% set parts = path.split('/') %}
609
  {% for i in range(parts|length) %}
610
- <span class="breadcrumb-separator">/</span>
611
  {% set current_path = parts[:i+1]|join('/') %}
612
- <span class="breadcrumb-item {{ 'active' if current_path == path else '' }}"
613
  onclick="nav('{{ current_path }}')">
 
 
 
614
  {{ parts[i] }}
615
  </span>
616
  {% endfor %}
@@ -622,8 +690,8 @@ body {
622
  <form action="/upload" method="post" enctype="multipart/form-data" id="uploadForm">
623
  <div class="file-input-wrapper">
624
  <label for="fileInput" class="btn btn-primary">
625
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
626
- <path d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"/>
627
  </svg>
628
  Upload File
629
  </label>
@@ -633,15 +701,15 @@ body {
633
  </form>
634
 
635
  <button class="btn btn-secondary" onclick="showFolderModal('{{ path }}')">
636
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
637
- <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
638
  </svg>
639
  New Folder
640
  </button>
641
 
642
- <button class="btn btn-secondary" onclick="showUrlDownloadModal('{{ path }}')">
643
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
644
- <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.414L6.293 6.707z" clip-rule="evenodd" />
645
  </svg>
646
  Download from URL
647
  </button>
@@ -655,42 +723,50 @@ body {
655
  {% for item in items %}
656
  <div class="file-item">
657
  <div class="file-item-content" onclick="{% if item.type=='dir' %}nav('{{ item.path }}'){% else %}download('{{ item.path }}'){% endif %}">
658
- <div class="file-icon-wrapper {{ item.type }}">
 
659
  {% if item.type == 'dir' %}
660
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
661
  <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
662
  </svg>
663
  {% else %}
664
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
665
- <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"/>
666
  </svg>
667
  {% endif %}
668
  </div>
669
  <div class="file-details">
670
- <div class="file-name" title="{{ item.name }}">{{ item.name }}</div>
671
  <div class="file-meta">{{ item.type|title }}</div>
672
  </div>
 
673
  </div>
674
 
675
  <div class="dropdown">
676
  <button class="dropdown-toggle" onclick="toggleDropdown(event, this)">
677
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
678
  <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"/>
679
  </svg>
680
  </button>
681
  <div class="dropdown-menu">
682
  {% if item.type == 'file' %}
683
  <button class="dropdown-item" onclick="download('{{ item.path }}')">
684
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" 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>
 
 
685
  Download
686
  </button>
687
  {% endif %}
688
  <button class="dropdown-item" onclick="showRenameModal('{{ item.path }}', '{{ item.name }}')">
689
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" /><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd" /></svg>
 
 
690
  Rename
691
  </button>
692
  <button class="dropdown-item danger" onclick="showDeleteModal('{{ item.path }}', '{{ item.name }}')">
693
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" 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 0V8zm4 0a1 1 0 012 0v6a1 1 0 11-2 0V8z" clip-rule="evenodd" /></svg>
 
 
694
  Delete
695
  </button>
696
  </div>
@@ -700,11 +776,11 @@ body {
700
  </div>
701
  {% else %}
702
  <div class="empty-state">
703
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
704
- <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" />
705
  </svg>
706
- <h3>This folder is empty</h3>
707
- <p>Upload a file or create a new folder to get started.</p>
708
  </div>
709
  {% endif %}
710
  </main>
@@ -733,7 +809,7 @@ body {
733
  <div class="modal">
734
  <h3 class="modal-title">Confirm Deletion</h3>
735
  <div class="modal-body">
736
- <p>Are you sure you want to delete <strong id="deleteItemName" style="color: var(--accent-danger);"></strong>? This action cannot be undone.</p>
737
  </div>
738
  <div class="modal-actions">
739
  <button class="btn btn-secondary" onclick="closeModal('deleteModal')">Cancel</button>
@@ -769,8 +845,8 @@ body {
769
  <div class="modal-body">
770
  <input type="text" class="modal-input" id="urlInput" placeholder="https://example.com/file.pdf" style="margin-bottom: 1rem;">
771
  <input type="text" class="modal-input" id="filenameInput" placeholder="Custom filename (optional)">
772
- <p style="font-size: 0.875rem; color: var(--text-secondary); margin-top: 0.75rem;">
773
- The file will be downloaded directly to the current directory in your repository.
774
  </p>
775
  </div>
776
  <div class="modal-actions">
@@ -804,11 +880,11 @@ body {
804
  function toggleDropdown(event, button) {
805
  event.stopPropagation();
806
  const dropdown = button.closest('.dropdown');
807
-
808
  if (activeDropdown && activeDropdown !== dropdown) {
809
  activeDropdown.classList.remove('active');
810
  }
811
-
812
  dropdown.classList.toggle('active');
813
  activeDropdown = dropdown.classList.contains('active') ? dropdown : null;
814
  }
@@ -825,7 +901,7 @@ body {
825
  const btn = document.getElementById(btnId);
826
  const text = document.getElementById(textId);
827
  const spinner = document.getElementById(spinnerId);
828
-
829
  btn.disabled = true;
830
  text.style.display = 'none';
831
  spinner.style.display = 'inline-block';
@@ -835,7 +911,7 @@ body {
835
  const btn = document.getElementById(btnId);
836
  const text = document.getElementById(textId);
837
  const spinner = document.getElementById(spinnerId);
838
-
839
  btn.disabled = false;
840
  text.style.display = 'inline-block';
841
  spinner.style.display = 'none';
@@ -846,7 +922,8 @@ body {
846
  const modal = document.getElementById(modalId);
847
  modal.classList.add('active');
848
  document.body.style.overflow = 'hidden';
849
-
 
850
  const firstInput = modal.querySelector('.modal-input');
851
  if (firstInput) {
852
  setTimeout(() => {
@@ -866,17 +943,17 @@ body {
866
 
867
  // Modal show functions
868
  function showRenameModal(path, currentName) {
869
- closeAllDropdowns();
870
  currentRenamePath = path;
871
  document.getElementById('renameInput').value = currentName;
872
  showModal('renameModal');
 
873
  }
874
 
875
  function showDeleteModal(path, name) {
876
- closeAllDropdowns();
877
  currentDeletePath = path;
878
  document.getElementById('deleteItemName').textContent = name;
879
  showModal('deleteModal');
 
880
  }
881
 
882
  function showFolderModal(path) {
@@ -893,30 +970,33 @@ body {
893
  }
894
 
895
  // Generic action performer
896
- async function performAction(url, body, btnId, textId, spinnerId, modalToClose) {
897
  showSpinner(btnId, textId, spinnerId);
898
-
899
  try {
900
  const response = await fetch(url, {
901
  method: 'POST',
902
  headers: { 'Content-Type': 'application/json' },
903
  body: JSON.stringify(body)
904
  });
905
-
906
  const result = await response.json();
907
-
908
  if (response.ok) {
909
  if (modalToClose) closeModal(modalToClose);
910
- showUploadOverlay(result.message || 'Operation successful!');
911
- setTimeout(() => {
912
- window.location.reload();
913
- }, 1200);
 
 
914
  } else {
915
  throw new Error(result.error || 'Operation failed');
916
  }
917
  } catch (error) {
918
  console.error('Action failed:', error);
919
  alert(`Operation failed: ${error.message}`);
 
920
  hideSpinner(btnId, textId, spinnerId);
921
  }
922
  }
@@ -928,44 +1008,83 @@ body {
928
  alert('Please enter a valid name');
929
  return;
930
  }
931
- performAction('/rename', { old_path: currentRenamePath, new_path: newName }, 'renameConfirmBtn', 'renameBtnText', 'renameBtnSpinner', 'renameModal');
 
 
 
 
 
 
 
 
 
932
  }
933
 
934
  function confirmDelete() {
935
- performAction('/delete', { path: currentDeletePath }, 'deleteConfirmBtn', 'deleteBtnText', 'deleteBtnSpinner', 'deleteModal');
 
 
 
 
 
 
 
 
936
  }
937
 
938
  function confirmCreateFolder() {
939
  const folderName = document.getElementById('folderInput').value.trim();
940
- if (!folderName || folderName.includes('/')) {
941
- alert('Please enter a valid folder name without slashes.');
942
  return;
943
  }
 
944
  const folderPath = currentFolderPath ? `${currentFolderPath}/${folderName}` : folderName;
945
- performAction('/create_folder', { path: folderPath }, 'folderConfirmBtn', 'folderBtnText', 'folderBtnSpinner', 'folderModal');
 
 
 
 
 
 
 
 
946
  }
947
 
948
  function confirmUrlDownload() {
949
  const url = document.getElementById('urlInput').value.trim();
950
  const customFilename = document.getElementById('filenameInput').value.trim();
951
-
952
  if (!url) {
953
  alert('Please enter a valid URL');
954
  return;
955
  }
956
- try { new URL(url); }
957
- catch(e) {
958
- alert('Please enter a valid URL format.');
959
  return;
960
  }
961
-
962
- performAction('/download_url', { url: url, path: currentUrlDownloadPath, filename: customFilename || null }, 'urlDownloadConfirmBtn', 'urlDownloadBtnText', 'urlDownloadBtnSpinner', 'urlDownloadModal');
 
 
 
 
 
 
 
 
 
 
 
 
963
  }
964
 
965
  // Upload overlay management
966
  function showUploadOverlay(message = 'Processing...') {
967
  const overlay = document.getElementById('uploadOverlay');
968
- overlay.querySelector('.upload-text').textContent = message;
 
969
  overlay.classList.add('active');
970
  }
971
 
@@ -975,35 +1094,46 @@ body {
975
 
976
  // Event listeners
977
  document.addEventListener('DOMContentLoaded', function() {
 
978
  document.getElementById('fileInput').addEventListener('change', function(e) {
979
  if (this.files.length > 0) {
980
- showUploadOverlay(`Uploading ${this.files[0].name}...`);
981
  document.getElementById('uploadForm').submit();
982
  }
983
  });
984
 
985
- const setupModalKeyListener = (modalId, confirmFunction) => {
986
- document.getElementById(modalId).addEventListener('keypress', (e) => {
987
- if (e.key === 'Enter' && e.target.tagName === 'INPUT') {
988
- e.preventDefault();
989
- confirmFunction();
990
- }
991
- });
 
 
 
 
992
  };
993
 
994
- setupModalKeyListener('renameModal', confirmRename);
995
- setupModalKeyListener('folderModal', confirmCreateFolder);
996
- setupModalKeyListener('urlDownloadModal', confirmUrlDownload);
 
997
 
 
998
  document.addEventListener('click', (e) => {
 
999
  if (!e.target.closest('.dropdown')) {
1000
  closeAllDropdowns();
1001
  }
 
 
1002
  if (e.target.classList.contains('modal-overlay')) {
1003
  closeModal(e.target.id);
1004
  }
1005
  });
1006
 
 
1007
  document.addEventListener('keydown', (e) => {
1008
  if (e.key === 'Escape') {
1009
  const openModal = document.querySelector('.modal-overlay.active');
@@ -1015,327 +1145,16 @@ body {
1015
  }
1016
  });
1017
  });
 
 
 
 
 
 
 
 
 
 
1018
  </script>
1019
  </body>
1020
- </html>
1021
- """
1022
-
1023
- def list_folder(path=""):
1024
- """List files and folders in the given path"""
1025
- prefix = path.strip("/") + ("/" if path else "")
1026
-
1027
- try:
1028
- all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
1029
- except Exception as e:
1030
- print(f"Error listing files: {e}")
1031
- return []
1032
-
1033
- seen = set()
1034
- items = []
1035
-
1036
- for f in all_files:
1037
- if not f.startswith(prefix):
1038
- continue
1039
-
1040
- rest = f[len(prefix):]
1041
- if "/" in rest:
1042
- # This is a subdirectory
1043
- dir_name = rest.split("/")[0]
1044
- dir_path = (prefix + dir_name).strip("/")
1045
- if dir_path not in seen:
1046
- seen.add(dir_path)
1047
- items.append({
1048
- "type": "dir",
1049
- "name": dir_name,
1050
- "path": dir_path
1051
- })
1052
- else:
1053
- # This is a file
1054
- if rest: # Skip empty filenames
1055
- items.append({
1056
- "type": "file",
1057
- "name": rest,
1058
- "path": (prefix + rest).strip("/")
1059
- })
1060
-
1061
- # Sort directories first, then files, both alphabetically
1062
- items.sort(key=lambda x: (x["type"] != "dir", x["name"].lower()))
1063
- return items
1064
-
1065
- def get_filename_from_url(url, custom_filename=None):
1066
- """Extract filename from URL or use custom filename"""
1067
- if custom_filename:
1068
- return custom_filename
1069
-
1070
- # Parse URL and get the path
1071
- parsed_url = urlparse(url)
1072
- path = unquote(parsed_url.path)
1073
-
1074
- # Get filename from path
1075
- filename = os.path.basename(path)
1076
-
1077
- # If no filename found, generate one
1078
- if not filename or '.' not in filename:
1079
- filename = f"downloaded_file_{int(time.time())}"
1080
-
1081
- return filename
1082
-
1083
- @app.route("/", methods=["GET"])
1084
- def index():
1085
- """Main page - file browser"""
1086
- path = request.args.get("path", "").strip("/")
1087
- items = list_folder(path)
1088
- return render_template_string(TEMPLATE, items=items, path=path)
1089
-
1090
- @app.route("/download", methods=["GET"])
1091
- def download():
1092
- """Download a file from the repository"""
1093
- file_path = request.args.get("path", "")
1094
- if not file_path:
1095
- return "No file path provided", 400
1096
-
1097
- try:
1098
- # Download file to temporary location
1099
- local_path = hf_hub_download(
1100
- repo_id=REPO_ID,
1101
- filename=file_path,
1102
- repo_type="dataset",
1103
- token=HF_TOKEN,
1104
- cache_dir=tempfile.gettempdir()
1105
- )
1106
-
1107
- # Send file to user
1108
- return send_file(
1109
- local_path,
1110
- as_attachment=True,
1111
- download_name=os.path.basename(file_path)
1112
- )
1113
- except Exception as e:
1114
- return f"Error downloading file: {str(e)}", 500
1115
-
1116
- @app.route("/upload", methods=["POST"])
1117
- def upload():
1118
- """Upload a file to the repository"""
1119
- if 'file' not in request.files:
1120
- return "No file provided", 400
1121
-
1122
- file = request.files["file"]
1123
- if file.filename == '':
1124
- return "No file selected", 400
1125
-
1126
- path = request.form.get("path", "").strip("/")
1127
-
1128
- # Create destination path
1129
- dest_path = f"{path}/{file.filename}".strip("/")
1130
-
1131
- # Save file to temporary location
1132
- with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
1133
- file.save(tmp_file.name)
1134
-
1135
- try:
1136
- # Upload to HuggingFace
1137
- upload_file(
1138
- path_or_fileobj=tmp_file.name,
1139
- path_in_repo=dest_path,
1140
- repo_id=REPO_ID,
1141
- repo_type="dataset",
1142
- token=HF_TOKEN
1143
- )
1144
- except Exception as e:
1145
- os.unlink(tmp_file.name)
1146
- return f"Error uploading file: {str(e)}", 500
1147
- finally:
1148
- # Clean up temporary file
1149
- try:
1150
- os.unlink(tmp_file.name)
1151
- except:
1152
- pass
1153
-
1154
- return redirect(f"/?path={path}")
1155
-
1156
- @app.route("/download_url", methods=["POST"])
1157
- def download_url():
1158
- """Download a file from URL and upload to repository"""
1159
- data = request.get_json()
1160
- url = data.get("url", "").strip()
1161
- path = data.get("path", "").strip("/")
1162
- custom_filename = data.get("filename", "").strip()
1163
-
1164
- if not url:
1165
- return jsonify({"error": "No URL provided"}), 400
1166
-
1167
- if not url.startswith(('http://', 'https://')):
1168
- return jsonify({"error": "Invalid URL. Must start with http:// or https://"}), 400
1169
-
1170
- try:
1171
- # Get filename
1172
- filename = get_filename_from_url(url, custom_filename)
1173
- dest_path = f"{path}/{filename}".strip("/")
1174
-
1175
- # Download file from URL
1176
- response = requests.get(url, stream=True, timeout=30)
1177
- response.raise_for_status()
1178
-
1179
- # Check if content-length is reasonable (max 500MB)
1180
- content_length = response.headers.get('content-length')
1181
- if content_length and int(content_length) > 500 * 1024 * 1024:
1182
- return jsonify({"error": "File too large. Maximum size is 500MB"}), 400
1183
-
1184
- # Save to temporary file
1185
- with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
1186
- # Download in chunks
1187
- for chunk in response.iter_content(chunk_size=8192):
1188
- tmp_file.write(chunk)
1189
-
1190
- tmp_file.flush()
1191
-
1192
- # Upload to HuggingFace
1193
- upload_file(
1194
- path_or_fileobj=tmp_file.name,
1195
- path_in_repo=dest_path,
1196
- repo_id=REPO_ID,
1197
- repo_type="dataset",
1198
- token=HF_TOKEN
1199
- )
1200
-
1201
- # Clean up
1202
- os.unlink(tmp_file.name)
1203
-
1204
- return jsonify({"status": "success", "message": "File downloaded and uploaded successfully"})
1205
-
1206
- except requests.exceptions.RequestException as e:
1207
- return jsonify({"error": f"Failed to download from URL: {str(e)}"}), 400
1208
- except Exception as e:
1209
- return jsonify({"error": f"Upload failed: {str(e)}"}), 500
1210
-
1211
- @app.route("/delete", methods=["POST"])
1212
- def delete():
1213
- """Delete a file or folder from the repository"""
1214
- data = request.get_json()
1215
- delete_path = data.get("path", "").strip("/")
1216
-
1217
- if not delete_path:
1218
- return jsonify({"error": "No path provided"}), 400
1219
-
1220
- try:
1221
- all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
1222
- deleted_count = 0
1223
-
1224
- for file_path in all_files:
1225
- # Delete exact match or files within the folder
1226
- if file_path == delete_path or file_path.startswith(delete_path.rstrip("/") + "/"):
1227
- delete_file(
1228
- repo_id=REPO_ID,
1229
- path_in_repo=file_path,
1230
- repo_type="dataset",
1231
- token=HF_TOKEN
1232
- )
1233
- deleted_count += 1
1234
-
1235
- if deleted_count == 0:
1236
- return jsonify({"error": "No files found to delete"}), 404
1237
-
1238
- return jsonify({"status": "success", "message": f"Deleted {deleted_count} file(s)"})
1239
-
1240
- except Exception as e:
1241
- return jsonify({"error": f"Delete failed: {str(e)}"}), 500
1242
-
1243
- @app.route("/create_folder", methods=["POST"])
1244
- def create_folder():
1245
- """Create a new folder by uploading a .keep file"""
1246
- data = request.get_json()
1247
- folder_path = data.get("path", "").strip("/")
1248
-
1249
- if not folder_path:
1250
- return jsonify({"error": "No folder path provided"}), 400
1251
-
1252
- # Create .keep file path
1253
- keep_file_path = f"{folder_path}/.keep"
1254
-
1255
- try:
1256
- # Create temporary empty file
1257
- with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
1258
- tmp_file.write(b"# This file keeps the folder in git\n")
1259
- tmp_file.flush()
1260
-
1261
- # Upload .keep file
1262
- upload_file(
1263
- path_or_fileobj=tmp_file.name,
1264
- path_in_repo=keep_file_path,
1265
- repo_id=REPO_ID,
1266
- repo_type="dataset",
1267
- token=HF_TOKEN
1268
- )
1269
-
1270
- # Clean up
1271
- os.unlink(tmp_file.name)
1272
-
1273
- return jsonify({"status": "success", "message": "Folder created successfully"})
1274
-
1275
- except Exception as e:
1276
- return jsonify({"error": f"Failed to create folder: {str(e)}"}), 500
1277
-
1278
- @app.route("/rename", methods=["POST"])
1279
- def rename():
1280
- """Rename a file or folder"""
1281
- data = request.get_json()
1282
- old_path = data.get("old_path", "").strip("/")
1283
- new_name = data.get("new_path", "").strip()
1284
-
1285
- if not old_path or not new_name:
1286
- return jsonify({"error": "Missing old path or new name"}), 400
1287
-
1288
- try:
1289
- # Get parent directory
1290
- parent_dir = "/".join(old_path.split("/")[:-1]) if "/" in old_path else ""
1291
- new_path = f"{parent_dir}/{new_name}".strip("/")
1292
-
1293
- all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
1294
- renamed_count = 0
1295
-
1296
- for file_path in all_files:
1297
- if file_path == old_path or file_path.startswith(old_path + "/"):
1298
- # Calculate new file path
1299
- relative_path = file_path[len(old_path):].lstrip("/")
1300
- new_file_path = (new_path + "/" + relative_path).strip("/")
1301
-
1302
- # Download original file
1303
- local_path = hf_hub_download(
1304
- repo_id=REPO_ID,
1305
- filename=file_path,
1306
- repo_type="dataset",
1307
- token=HF_TOKEN,
1308
- cache_dir=tempfile.gettempdir()
1309
- )
1310
-
1311
- # Upload with new name
1312
- upload_file(
1313
- path_or_fileobj=local_path,
1314
- path_in_repo=new_file_path,
1315
- repo_id=REPO_ID,
1316
- repo_type="dataset",
1317
- token=HF_TOKEN
1318
- )
1319
-
1320
- # Delete original file
1321
- delete_file(
1322
- repo_id=REPO_ID,
1323
- path_in_repo=file_path,
1324
- repo_type="dataset",
1325
- token=HF_TOKEN
1326
- )
1327
-
1328
- renamed_count += 1
1329
-
1330
- if renamed_count == 0:
1331
- return jsonify({"error": "No files found to rename"}), 404
1332
-
1333
- return jsonify({"status": "success", "message": f"Renamed {renamed_count} file(s)"})
1334
-
1335
- except Exception as e:
1336
- return jsonify({"error": f"Rename failed: {str(e)}"}), 500
1337
-
1338
- if __name__ == "__main__":
1339
- # Add missing import
1340
- import time
1341
- app.run(debug=True, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
1
 
 
 
 
 
 
 
 
 
2
  <!DOCTYPE html>
3
  <html lang="en">
4
  <head>
5
  <meta charset="UTF-8">
6
  <title>HuggingFace Drive - {{ path or 'Root' }}</title>
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
 
 
8
  <style>
9
+ /* Reset and Base Styles */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  * {
11
  margin: 0;
12
  padding: 0;
13
  box-sizing: border-box;
14
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
15
  }
16
 
17
+ :root {
18
+ --bg: #0f172a;
19
+ --bg-secondary: #1e293b;
20
+ --bg-tertiary: #334155;
21
+ --text: #f1f5f9;
22
+ --text-muted: #94a3b8;
23
+ --primary: #3b82f6;
24
+ --primary-hover: #2563eb;
25
+ --success: #10b981;
26
+ --success-hover: #059669;
27
+ --danger: #ef4444;
28
+ --danger-hover: #dc2626;
29
+ --border: #475569;
30
+ --shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
31
+ --radius: 8px;
32
+ --transition: all 0.2s ease;
33
  }
34
 
35
  body {
36
+ background: var(--bg);
37
+ color: var(--text);
38
+ line-height: 1.5;
 
 
 
 
 
 
 
39
  min-height: 100vh;
40
  overflow-x: hidden;
41
  }
42
 
43
+ /* Container */
44
  .container {
45
+ max-width: 1400px;
46
  margin: 0 auto;
47
+ padding: 1.5rem;
48
  }
49
 
50
+ /* Header */
51
  .header {
 
 
 
 
 
 
 
52
  display: flex;
53
  flex-wrap: wrap;
54
  align-items: center;
55
  justify-content: space-between;
56
+ gap: 1rem;
57
+ padding: 1.25rem;
58
+ background: var(--bg-secondary);
59
+ border-radius: var(--radius);
60
+ box-shadow: var(--shadow);
61
+ margin-bottom: 2rem;
62
+ backdrop-filter: blur(10px);
63
  }
64
 
65
  .title {
66
  display: flex;
67
  align-items: center;
68
  gap: 0.75rem;
69
+ font-size: 1.75rem;
70
  font-weight: 700;
71
+ color: var(--primary);
72
  }
73
 
74
  .title-icon {
75
  width: 28px;
76
  height: 28px;
77
+ transition: var(--transition);
 
78
  }
79
+
80
  .title:hover .title-icon {
81
+ transform: rotate(15deg) scale(1.15);
82
  }
83
 
84
+ /* Breadcrumb */
85
  .breadcrumb {
86
  display: flex;
 
87
  flex-wrap: wrap;
88
+ align-items: center;
89
+ gap: 0.5rem;
90
+ font-size: 0.95rem;
 
91
  }
92
 
93
  .breadcrumb-item {
94
  display: flex;
95
  align-items: center;
96
  gap: 0.5rem;
97
+ padding: 0.5rem 1rem;
98
+ border-radius: 6px;
99
  cursor: pointer;
100
+ color: var(--text-muted);
101
+ transition: var(--transition);
 
 
102
  }
103
 
104
  .breadcrumb-item:hover {
105
+ background: var(--bg-tertiary);
106
+ color: var(--text);
107
+ transform: translateY(-2px);
108
  }
109
 
110
  .breadcrumb-item.active {
111
+ color: var(--primary);
112
+ background: rgba(59, 130, 246, 0.15);
113
  }
114
 
115
  .breadcrumb-separator {
116
+ color: var(--text-muted);
117
  user-select: none;
 
 
118
  }
119
 
120
+ /* Actions */
121
  .actions {
122
  display: flex;
123
  flex-wrap: wrap;
 
127
  .btn {
128
  display: inline-flex;
129
  align-items: center;
 
130
  gap: 0.5rem;
131
+ padding: 0.75rem 1.5rem;
132
+ border: none;
133
+ border-radius: var(--radius);
134
+ font-size: 0.95rem;
135
+ font-weight: 500;
136
  cursor: pointer;
137
+ transition: var(--transition);
138
  text-decoration: none;
139
+ position: relative;
140
+ overflow: hidden;
141
  }
142
+
143
+ .btn::before {
144
+ content: '';
145
+ position: absolute;
146
+ top: 0;
147
+ left: -100%;
148
+ width: 100%;
149
+ height: 100%;
150
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
151
+ transition: left 0.4s ease;
152
  }
153
 
154
+ .btn:hover::before {
155
+ left: 100%;
156
  }
157
 
158
  .btn-primary {
159
+ background: var(--primary);
160
+ color: #fff;
 
161
  }
162
+
163
  .btn-primary:hover {
164
+ background: var(--primary-hover);
165
+ transform: translateY(-2px);
166
+ box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
167
  }
168
 
169
  .btn-secondary {
170
+ background: var(--bg-tertiary);
171
+ color: var(--text);
172
+ border: 1px solid var(--border);
173
  }
174
+
175
  .btn-secondary:hover {
176
+ background: var(--bg-secondary);
177
+ border-color: var(--primary);
178
+ transform: translateY(-2px);
179
  }
180
 
181
  .btn-success {
182
+ background: var(--success);
183
+ color: #fff;
 
184
  }
185
+
186
  .btn-success:hover {
187
+ background: var(--success-hover);
188
+ transform: translateY(-2px);
189
+ box-shadow: 0 4px 16px rgba(16, 185, 129, 0.3);
190
  }
191
 
192
  .btn-danger {
193
+ background: var(--danger);
194
+ color: #fff;
 
195
  }
196
+
197
  .btn-danger:hover {
198
+ background: var(--danger-hover);
199
+ transform: translateY(-2px);
200
+ box-shadow: 0 4px 16px rgba(239, 68, 68, 0.3);
201
  }
202
 
203
  .file-input-wrapper {
204
  position: relative;
205
+ overflow: hidden;
206
  display: inline-block;
207
  }
208
+
209
  .file-input {
210
  position: absolute;
211
  left: -9999px;
212
  opacity: 0;
213
  }
214
 
215
+ .icon {
216
+ width: 18px;
217
+ height: 18px;
218
+ flex-shrink: 0;
219
  }
220
 
221
+ /* File Grid */
222
  .file-grid {
223
  display: grid;
224
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
225
+ gap: 1.25rem;
226
+ animation: fadeIn 0.5s ease;
227
  }
228
 
229
  .file-item {
230
+ background: var(--bg-secondary);
231
+ border-radius: var(--radius);
232
+ border: 1px solid var(--border);
233
+ transition: var(--transition);
234
  position: relative;
235
+ overflow: visible;
236
  }
237
 
238
  .file-item:hover {
239
+ transform: translateY(-4px);
240
+ box-shadow: var(--shadow);
241
+ border-color: var(--primary);
242
+ overflow: visible;
243
  }
244
 
245
  .file-item-content {
246
+ padding: 1.25rem;
247
  display: flex;
248
  align-items: center;
249
  gap: 1rem;
250
  cursor: pointer;
251
+ overflow: visible;
252
  }
253
 
254
+ .file-info {
255
+ display: flex;
256
+ align-items: center;
257
+ gap: 1rem;
258
+ flex: 1;
259
+ }
260
+
261
+ .file-icon {
262
  width: 48px;
263
  height: 48px;
264
+ border-radius: 8px;
265
+ display: flex;
266
+ align-items: center;
267
+ justify-content: center;
268
+ transition: var(--transition);
 
 
 
269
  }
270
 
271
+ .file-icon.dir {
272
+ background: rgba(59, 130, 246, 0.1);
273
+ color: var(--primary);
274
  }
275
+
276
+ .file-icon.file {
277
+ background: rgba(16, 185, 129, 0.1);
278
+ color: var(--success);
279
  }
280
+
281
+ .file-item:hover .file-icon {
282
  transform: scale(1.1);
283
  }
284
 
285
  .file-details {
286
+ flex: 1;
287
  }
288
 
289
  .file-name {
290
  font-weight: 500;
291
+ margin-bottom: 0.25rem;
292
+ word-break: break-word;
293
+ font-size: 1rem;
 
294
  }
295
 
296
  .file-meta {
297
+ font-size: 0.85rem;
298
+ color: var(--text-muted);
299
  }
300
 
301
+ /* Dropdown */
302
  .dropdown {
303
  position: absolute;
304
  top: 0.5rem;
 
308
  .dropdown-toggle {
309
  background: none;
310
  border: none;
311
+ color: var(--text-muted);
312
  cursor: pointer;
313
+ padding: 0.5rem;
314
+ border-radius: 6px;
315
+ transition: var(--transition);
 
 
 
316
  }
 
317
 
318
  .dropdown-toggle:hover {
319
+ background: var(--bg-tertiary);
320
+ color: var(--text);
321
  }
322
 
323
  .dropdown-menu {
324
  position: absolute;
325
+ top: 100%;
326
  right: 0;
327
+ background: var(--bg-secondary);
328
+ border: 1px solid var(--border);
329
+ border-radius: var(--radius);
330
+ box-shadow: var(--shadow);
 
331
  min-width: 180px;
332
  z-index: 1000;
333
  opacity: 0;
334
  visibility: hidden;
335
+ transform: translateY(-10px);
336
+ transition: var(--transition);
 
 
337
  }
338
 
339
  .dropdown.active .dropdown-menu {
340
  opacity: 1;
341
  visibility: visible;
342
+ transform: translateY(0);
343
  }
344
 
345
  .dropdown-item {
346
  display: flex;
347
  align-items: center;
348
+ gap: 0.5rem;
349
  width: 100%;
350
+ padding: 0.75rem 1rem;
351
  border: none;
352
  background: none;
353
+ color: var(--text);
354
  font-size: 0.9rem;
 
355
  cursor: pointer;
356
+ transition: var(--transition);
357
  text-align: left;
 
358
  }
 
359
 
360
  .dropdown-item:hover {
361
+ background: var(--bg-tertiary);
 
362
  }
363
+
364
+ .dropdown-item.danger {
365
+ color: var(--danger);
366
  }
367
 
368
+ .dropdown-item.danger:hover {
369
+ background: rgba(239, 68, 68, 0.1);
370
+ }
371
+
372
+ /* Empty State */
373
+ .empty-state {
374
+ text-align: center;
375
+ padding: 3rem 1rem;
376
+ color: var(--text-muted);
377
+ }
378
 
379
+ .empty-state .icon {
380
+ width: 72px;
381
+ height: 72px;
382
+ margin: 0 auto 1rem;
383
+ opacity: 0.6;
384
+ }
385
+
386
+ .empty-state h3 {
387
+ margin-bottom: 0.5rem;
388
+ color: var(--text);
389
+ font-size: 1.25rem;
390
+ }
391
+
392
+ /* Modals */
393
  .modal-overlay {
394
  position: fixed;
395
+ top: 0;
396
+ left: 0;
397
+ right: 0;
398
+ bottom: 0;
399
+ background: rgba(0, 0, 0, 0.75);
400
+ display: flex;
401
+ align-items: center;
402
+ justify-content: center;
403
  z-index: 2000;
404
  opacity: 0;
405
  visibility: hidden;
406
+ transition: var(--transition);
407
+ backdrop-filter: blur(5px);
408
  }
409
 
410
  .modal-overlay.active {
 
413
  }
414
 
415
  .modal {
416
+ background: var(--bg-secondary);
417
+ border-radius: var(--radius);
418
+ box-shadow: var(--shadow);
 
419
  width: 90%;
420
  max-width: 450px;
421
  max-height: 90vh;
422
  overflow-y: auto;
423
  transform: scale(0.95);
424
+ transition: var(--transition);
425
+ border: 1px solid var(--border);
426
  }
427
 
428
  .modal-overlay.active .modal {
 
430
  }
431
 
432
  .modal-title {
433
+ padding: 1.5rem 1.5rem 0;
434
+ font-size: 1.3rem;
435
  font-weight: 600;
 
436
  }
437
 
438
  .modal-body {
 
441
 
442
  .modal-input {
443
  width: 100%;
444
+ padding: 0.75rem;
445
+ border: 1px solid var(--border);
446
+ border-radius: var(--radius);
447
+ background: var(--bg-tertiary);
448
+ color: var(--text);
449
+ font-size: 0.95rem;
450
+ transition: var(--transition);
 
451
  }
452
+
453
  .modal-input:focus {
454
  outline: none;
455
+ border-color: var(--primary);
456
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
457
  }
458
 
459
  .modal-actions {
 
463
  justify-content: flex-end;
464
  }
465
 
466
+ /* Upload Overlay */
467
  .upload-overlay {
468
  position: fixed;
469
+ top: 0;
470
+ left: 0;
471
+ right: 0;
472
+ bottom: 0;
473
+ background: rgba(0, 0, 0, 0.85);
474
  display: flex;
475
  flex-direction: column;
476
  align-items: center;
 
478
  z-index: 3000;
479
  opacity: 0;
480
  visibility: hidden;
481
+ transition: var(--transition);
 
482
  }
483
+
484
  .upload-overlay.active {
485
  opacity: 1;
486
  visibility: visible;
487
  }
488
 
 
 
 
 
489
  .loading-spinner {
490
+ width: 48px;
491
+ height: 48px;
492
+ border: 4px solid var(--border);
493
+ border-top: 4px solid var(--primary);
494
  border-radius: 50%;
495
+ animation: spin 1s linear infinite;
496
+ margin-bottom: 1rem;
497
  }
498
+
499
  .upload-text {
500
+ font-size: 1.2rem;
501
+ color: var(--text);
 
 
502
  }
503
+
504
  .btn .loading-spinner {
505
+ width: 20px;
506
+ height: 20px;
507
  border-width: 3px;
508
  margin: 0;
509
  }
510
 
511
+ /* Animations */
512
+ @keyframes spin {
513
+ to {
514
+ transform: rotate(360deg);
515
+ }
 
 
516
  }
517
+
518
+ @keyframes fadeIn {
519
+ from {
520
+ opacity: 0;
521
+ transform: translateY(20px);
522
+ }
523
+ to {
524
+ opacity: 1;
525
+ transform: translateY(0);
526
+ }
527
  }
528
+
529
+ /* Scrollbar */
530
+ ::-webkit-scrollbar {
531
+ width: 10px;
532
+ }
533
+
534
+ ::-webkit-scrollbar-track {
535
+ background: var(--bg-tertiary);
536
  }
537
 
 
 
 
538
  ::-webkit-scrollbar-thumb {
539
+ background: var(--border);
540
+ border-radius: 5px;
 
541
  }
 
542
 
543
+ ::-webkit-scrollbar-thumb:hover {
544
+ background: var(--text-muted);
545
+ }
546
+
547
+ /* Responsive Design */
548
  @media (max-width: 1024px) {
549
+ .container {
550
+ padding: 1rem;
551
+ }
552
+
553
+ .header {
554
+ flex-direction: column;
555
+ align-items: flex-start;
556
+ gap: 1.5rem;
557
+ }
558
+
559
+ .title {
560
+ justify-content: center;
561
+ width: 100%;
562
+ }
563
+
564
+ .breadcrumb {
565
+ justify-content: center;
566
+ width: 100%;
567
+ }
568
+
569
+ .actions {
570
+ justify-content: center;
571
+ width: 100%;
572
+ }
573
+
574
+ .file-grid {
575
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
576
+ gap: 1rem;
577
+ }
578
  }
579
 
580
  @media (max-width: 768px) {
581
+ .file-grid {
582
+ grid-template-columns: 1fr;
583
+ }
584
+
585
+ .file-item-content {
586
+ padding: 1rem;
587
+ }
588
+
589
+ .file-icon {
590
+ width: 40px;
591
+ height: 40px;
592
+ }
593
+
594
+ .file-name {
595
+ font-size: 0.95rem;
596
+ }
597
+
598
+ .modal {
599
+ width: 95%;
600
+ margin: 1rem;
601
+ }
602
+
603
+ .modal-actions {
604
+ flex-direction: column;
605
+ }
606
+
607
+ .btn {
608
+ width: 100%;
609
+ justify-content: center;
610
+ }
611
  }
612
 
613
+ @media (max-width: 480px) {
614
+ .title {
615
+ font-size: 1.5rem;
616
+ }
617
+
618
+ .breadcrumb-item {
619
+ padding: 0.5rem 0.75rem;
620
+ font-size: 0.9rem;
621
+ }
622
+
623
+ .file-icon {
624
+ width: 36px;
625
+ height: 36px;
626
+ }
627
+
628
+ .file-name {
629
+ font-size: 0.9rem;
630
+ }
631
+
632
+ .actions {
633
+ flex-direction: column;
634
+ }
635
+ }
636
+
637
+ /* Reduced Motion */
638
  @media (prefers-reduced-motion: reduce) {
639
  * {
640
  animation-duration: 0.01ms !important;
 
641
  transition-duration: 0.01ms !important;
 
642
  }
643
  }
644
+
645
+ </style>
646
  </head>
647
  <body>
648
  <!-- Upload Overlay -->
 
654
  <div class="container">
655
  <header class="header">
656
  <h1 class="title">
657
+ <svg class="title-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
658
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"></path>
659
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5a2 2 0 012-2h4a2 2 0 012 2v0a2 2 0 01-2 2H10a2 2 0 01-2-2v0z"></path>
660
  </svg>
661
  HuggingFace Drive
662
  </h1>
 
664
  <!-- Breadcrumb -->
665
  <nav class="breadcrumb">
666
  <span class="breadcrumb-item {{ 'active' if not path else '' }}" onclick="nav('')">
667
+ <svg class="icon" fill="currentColor" viewBox="0 0 20 20">
668
  <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"/>
669
  </svg>
670
  Home
 
672
  {% if path %}
673
  {% set parts = path.split('/') %}
674
  {% for i in range(parts|length) %}
675
+ <span class="breadcrumb-separator">›</span>
676
  {% set current_path = parts[:i+1]|join('/') %}
677
+ <span class="breadcrumb-item {{ 'active' if current_path == path else '' }}"
678
  onclick="nav('{{ current_path }}')">
679
+ <svg class="icon" fill="currentColor" viewBox="0 0 20 20">
680
+ <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
681
+ </svg>
682
  {{ parts[i] }}
683
  </span>
684
  {% endfor %}
 
690
  <form action="/upload" method="post" enctype="multipart/form-data" id="uploadForm">
691
  <div class="file-input-wrapper">
692
  <label for="fileInput" class="btn btn-primary">
693
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
694
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
695
  </svg>
696
  Upload File
697
  </label>
 
701
  </form>
702
 
703
  <button class="btn btn-secondary" onclick="showFolderModal('{{ path }}')">
704
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
705
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
706
  </svg>
707
  New Folder
708
  </button>
709
 
710
+ <button class="btn btn-success" onclick="showUrlDownloadModal('{{ path }}')">
711
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
712
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
713
  </svg>
714
  Download from URL
715
  </button>
 
723
  {% for item in items %}
724
  <div class="file-item">
725
  <div class="file-item-content" onclick="{% if item.type=='dir' %}nav('{{ item.path }}'){% else %}download('{{ item.path }}'){% endif %}">
726
+ <div class="file-info">
727
+ <div class="file-icon {{ item.type }}">
728
  {% if item.type == 'dir' %}
729
+ <svg class="icon" fill="currentColor" viewBox="0 0 20 20">
730
  <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
731
  </svg>
732
  {% else %}
733
+ <svg class="icon" fill="currentColor" viewBox="0 0 20 20">
734
+ <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-2V4z" clip-rule="evenodd"/>
735
  </svg>
736
  {% endif %}
737
  </div>
738
  <div class="file-details">
739
+ <div class="file-name">{{ item.name }}</div>
740
  <div class="file-meta">{{ item.type|title }}</div>
741
  </div>
742
+ </div>
743
  </div>
744
 
745
  <div class="dropdown">
746
  <button class="dropdown-toggle" onclick="toggleDropdown(event, this)">
747
+ <svg class="icon" fill="currentColor" viewBox="0 0 20 20">
748
  <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"/>
749
  </svg>
750
  </button>
751
  <div class="dropdown-menu">
752
  {% if item.type == 'file' %}
753
  <button class="dropdown-item" onclick="download('{{ item.path }}')">
754
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
755
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
756
+ </svg>
757
  Download
758
  </button>
759
  {% endif %}
760
  <button class="dropdown-item" onclick="showRenameModal('{{ item.path }}', '{{ item.name }}')">
761
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
762
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
763
+ </svg>
764
  Rename
765
  </button>
766
  <button class="dropdown-item danger" onclick="showDeleteModal('{{ item.path }}', '{{ item.name }}')">
767
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
768
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
769
+ </svg>
770
  Delete
771
  </button>
772
  </div>
 
776
  </div>
777
  {% else %}
778
  <div class="empty-state">
779
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
780
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
781
  </svg>
782
+ <h3>No files yet</h3>
783
+ <p>Upload your first file or create a folder to get started.</p>
784
  </div>
785
  {% endif %}
786
  </main>
 
809
  <div class="modal">
810
  <h3 class="modal-title">Confirm Deletion</h3>
811
  <div class="modal-body">
812
+ <p>Are you sure you want to delete <strong id="deleteItemName"></strong>? This action cannot be undone.</p>
813
  </div>
814
  <div class="modal-actions">
815
  <button class="btn btn-secondary" onclick="closeModal('deleteModal')">Cancel</button>
 
845
  <div class="modal-body">
846
  <input type="text" class="modal-input" id="urlInput" placeholder="https://example.com/file.pdf" style="margin-bottom: 1rem;">
847
  <input type="text" class="modal-input" id="filenameInput" placeholder="Custom filename (optional)">
848
+ <p style="font-size: 0.875rem; color: var(--text-muted); margin-top: 0.5rem;">
849
+ The file will be downloaded directly to your HuggingFace repository.
850
  </p>
851
  </div>
852
  <div class="modal-actions">
 
880
  function toggleDropdown(event, button) {
881
  event.stopPropagation();
882
  const dropdown = button.closest('.dropdown');
883
+
884
  if (activeDropdown && activeDropdown !== dropdown) {
885
  activeDropdown.classList.remove('active');
886
  }
887
+
888
  dropdown.classList.toggle('active');
889
  activeDropdown = dropdown.classList.contains('active') ? dropdown : null;
890
  }
 
901
  const btn = document.getElementById(btnId);
902
  const text = document.getElementById(textId);
903
  const spinner = document.getElementById(spinnerId);
904
+
905
  btn.disabled = true;
906
  text.style.display = 'none';
907
  spinner.style.display = 'inline-block';
 
911
  const btn = document.getElementById(btnId);
912
  const text = document.getElementById(textId);
913
  const spinner = document.getElementById(spinnerId);
914
+
915
  btn.disabled = false;
916
  text.style.display = 'inline-block';
917
  spinner.style.display = 'none';
 
922
  const modal = document.getElementById(modalId);
923
  modal.classList.add('active');
924
  document.body.style.overflow = 'hidden';
925
+
926
+ // Focus first input in modal
927
  const firstInput = modal.querySelector('.modal-input');
928
  if (firstInput) {
929
  setTimeout(() => {
 
943
 
944
  // Modal show functions
945
  function showRenameModal(path, currentName) {
 
946
  currentRenamePath = path;
947
  document.getElementById('renameInput').value = currentName;
948
  showModal('renameModal');
949
+ closeAllDropdowns();
950
  }
951
 
952
  function showDeleteModal(path, name) {
 
953
  currentDeletePath = path;
954
  document.getElementById('deleteItemName').textContent = name;
955
  showModal('deleteModal');
956
+ closeAllDropdowns();
957
  }
958
 
959
  function showFolderModal(path) {
 
970
  }
971
 
972
  // Generic action performer
973
+ async function performAction(url, body, btnId, textId, spinnerId, modalToClose, successMessage) {
974
  showSpinner(btnId, textId, spinnerId);
975
+
976
  try {
977
  const response = await fetch(url, {
978
  method: 'POST',
979
  headers: { 'Content-Type': 'application/json' },
980
  body: JSON.stringify(body)
981
  });
982
+
983
  const result = await response.json();
984
+
985
  if (response.ok) {
986
  if (modalToClose) closeModal(modalToClose);
987
+ if (successMessage) {
988
+ // Show brief success message
989
+ showUploadOverlay(successMessage);
990
+ setTimeout(() => hideUploadOverlay(), 1500);
991
+ }
992
+ setTimeout(() => window.location.reload(), successMessage ? 1500 : 0);
993
  } else {
994
  throw new Error(result.error || 'Operation failed');
995
  }
996
  } catch (error) {
997
  console.error('Action failed:', error);
998
  alert(`Operation failed: ${error.message}`);
999
+ } finally {
1000
  hideSpinner(btnId, textId, spinnerId);
1001
  }
1002
  }
 
1008
  alert('Please enter a valid name');
1009
  return;
1010
  }
1011
+
1012
+ performAction(
1013
+ '/rename',
1014
+ { old_path: currentRenamePath, new_path: newName },
1015
+ 'renameConfirmBtn',
1016
+ 'renameBtnText',
1017
+ 'renameBtnSpinner',
1018
+ 'renameModal',
1019
+ 'Item renamed successfully!'
1020
+ );
1021
  }
1022
 
1023
  function confirmDelete() {
1024
+ performAction(
1025
+ '/delete',
1026
+ { path: currentDeletePath },
1027
+ 'deleteConfirmBtn',
1028
+ 'deleteBtnText',
1029
+ 'deleteBtnSpinner',
1030
+ 'deleteModal',
1031
+ 'Item deleted successfully!'
1032
+ );
1033
  }
1034
 
1035
  function confirmCreateFolder() {
1036
  const folderName = document.getElementById('folderInput').value.trim();
1037
+ if (!folderName) {
1038
+ alert('Please enter a folder name');
1039
  return;
1040
  }
1041
+
1042
  const folderPath = currentFolderPath ? `${currentFolderPath}/${folderName}` : folderName;
1043
+ performAction(
1044
+ '/create_folder',
1045
+ { path: folderPath },
1046
+ 'folderConfirmBtn',
1047
+ 'folderBtnText',
1048
+ 'folderBtnSpinner',
1049
+ 'folderModal',
1050
+ 'Folder created successfully!'
1051
+ );
1052
  }
1053
 
1054
  function confirmUrlDownload() {
1055
  const url = document.getElementById('urlInput').value.trim();
1056
  const customFilename = document.getElementById('filenameInput').value.trim();
1057
+
1058
  if (!url) {
1059
  alert('Please enter a valid URL');
1060
  return;
1061
  }
1062
+
1063
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
1064
+ alert('Please enter a valid HTTP/HTTPS URL');
1065
  return;
1066
  }
1067
+
1068
+ performAction(
1069
+ '/download_url',
1070
+ {
1071
+ url: url,
1072
+ path: currentUrlDownloadPath,
1073
+ filename: customFilename || null
1074
+ },
1075
+ 'urlDownloadConfirmBtn',
1076
+ 'urlDownloadBtnText',
1077
+ 'urlDownloadBtnSpinner',
1078
+ 'urlDownloadModal',
1079
+ 'File downloaded successfully!'
1080
+ );
1081
  }
1082
 
1083
  // Upload overlay management
1084
  function showUploadOverlay(message = 'Processing...') {
1085
  const overlay = document.getElementById('uploadOverlay');
1086
+ const text = overlay.querySelector('.upload-text');
1087
+ text.textContent = message;
1088
  overlay.classList.add('active');
1089
  }
1090
 
 
1094
 
1095
  // Event listeners
1096
  document.addEventListener('DOMContentLoaded', function() {
1097
+ // File input change handler
1098
  document.getElementById('fileInput').addEventListener('change', function(e) {
1099
  if (this.files.length > 0) {
1100
+ showUploadOverlay('Uploading file...');
1101
  document.getElementById('uploadForm').submit();
1102
  }
1103
  });
1104
 
1105
+ // Modal keyboard handlers
1106
+ const setupModalKeyListener = (inputId, confirmFunction) => {
1107
+ const input = document.getElementById(inputId);
1108
+ if (input) {
1109
+ input.addEventListener('keypress', (e) => {
1110
+ if (e.key === 'Enter') {
1111
+ e.preventDefault();
1112
+ confirmFunction();
1113
+ }
1114
+ });
1115
+ }
1116
  };
1117
 
1118
+ setupModalKeyListener('renameInput', confirmRename);
1119
+ setupModalKeyListener('folderInput', confirmCreateFolder);
1120
+ setupModalKeyListener('urlInput', confirmUrlDownload);
1121
+ setupModalKeyListener('filenameInput', confirmUrlDownload);
1122
 
1123
+ // Global click handler
1124
  document.addEventListener('click', (e) => {
1125
+ // Close dropdowns when clicking outside
1126
  if (!e.target.closest('.dropdown')) {
1127
  closeAllDropdowns();
1128
  }
1129
+
1130
+ // Close modal when clicking overlay
1131
  if (e.target.classList.contains('modal-overlay')) {
1132
  closeModal(e.target.id);
1133
  }
1134
  });
1135
 
1136
+ // Global keyboard handler
1137
  document.addEventListener('keydown', (e) => {
1138
  if (e.key === 'Escape') {
1139
  const openModal = document.querySelector('.modal-overlay.active');
 
1145
  }
1146
  });
1147
  });
1148
+
1149
+ // Prevent form submission on enter in modals (except for specific inputs)
1150
+ document.addEventListener('keydown', function(e) {
1151
+ if (e.key === 'Enter' && e.target.closest('.modal') && e.target.tagName !== 'BUTTON') {
1152
+ const modal = e.target.closest('.modal-overlay');
1153
+ if (modal) {
1154
+ e.preventDefault();
1155
+ }
1156
+ }
1157
+ });
1158
  </script>
1159
  </body>
1160
+ </html>