Ezequield commited on
Commit
b2632b3
·
verified ·
1 Parent(s): b0b1471

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +616 -357
index.html CHANGED
@@ -3,414 +3,673 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>DeviantArt Archiver | MediaFire Zip Tool</title>
7
 
8
- <!-- Modern CSS Framework -->
9
- <script src="https://cdn.tailwindcss.com"></script>
10
 
11
- <!-- Configuration for Tailwind to match original design -->
12
- <script>
13
- tailwind.config = {
14
- theme: {
15
- extend: {
16
- colors: {
17
- brand: {
18
- 50: '#f0f9ff',
19
- 100: '#e0f2fe',
20
- 500: '#0ea5e9',
21
- 600: '#0284c7',
22
- 900: '#0c4a6e',
23
- }
24
- },
25
- animation: {
26
- 'shimmer': 'shimmer 1.5s infinite',
27
- },
28
- keyframes: {
29
- shimmer: {
30
- '0%': { transform: 'translateX(-100%)' },
31
- '100%': { transform: 'translateX(100%)' }
32
- }
33
- }
34
- }
35
- }
36
- }
37
- </script>
38
-
39
- <!-- Icons Library -->
40
- <script src="https://unpkg.com/@phosphor-icons/web"></script>
41
-
42
- <!-- JSZip for Client-side Zipping -->
43
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
44
- <!-- FileSaver for Downloading the Zip -->
45
  <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
46
 
47
  <style>
48
- /* Custom Styles for nuances not covered by Tailwind utilities */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  body {
50
- font-family: 'Inter', system-ui, -apple-system, sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  }
52
 
53
- .glass-panel {
54
- background: rgba(31, 41, 55, 0.5);
55
- backdrop-filter: blur(12px);
56
- -webkit-backdrop-filter: blur(12px);
57
- border: 1px solid rgba(75, 85, 99, 0.4);
58
  }
59
 
60
- /* Smooth transitions for inputs */
61
- .input-group input:focus ~ .icon-highlight,
62
- .input-group input:not(:placeholder-shown) ~ .icon-highlight {
63
- color: #0ea5e9;
 
 
64
  }
65
 
66
- /* Toast Notification Animation */
67
- @keyframes slideInDown {
68
- from { transform: translate(-50%, -100%); opacity: 0; }
69
- to { transform: translate(-50%, 20px); opacity: 1; }
 
 
 
 
 
 
 
 
70
  }
71
- @keyframes fadeOut {
72
- from { opacity: 1; }
73
- to { opacity: 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  .toast {
76
- animation: slideInDown 0.3s ease-out forwards;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  }
78
- .toast.hiding {
79
- animation: fadeOut 0.3s ease-in forwards;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  }
81
  </style>
82
  </head>
83
- <body class="bg-gray-900 text-gray-100 min-h-screen flex flex-col selection:bg-brand-500 selection:text-white">
84
-
85
- <!-- Header -->
86
- <header class="border-b border-gray-800 bg-gray-900/50 backdrop-blur-sm sticky top-0 z-50">
87
- <div class="container mx-auto px-4 h-16 flex items-center justify-between">
88
- <div class="flex items-center gap-3">
89
- <div class="w-9 h-9 bg-gradient-to-br from-brand-500 to-purple-600 rounded-lg flex items-center justify-center font-bold text-white shadow-lg shadow-brand-500/20">
90
- <i class="ph ph-archive-box text-xl"></i>
91
- </div>
92
- <span class="font-semibold text-lg tracking-tight hidden sm:block text-gray-100">DA Zipper</span>
93
- </div>
94
 
95
- <nav class="flex items-center">
96
- <a
97
- href="https://huggingface.co/spaces/akhaliq/anycoder"
98
- target="_blank"
99
- rel="noopener noreferrer"
100
- class="text-sm font-medium text-gray-400 hover:text-brand-400 transition-colors duration-200 flex items-center gap-1.5 group"
101
- >
102
- Built with anycoder
103
- <i class="ph ph-arrow-square-out group-hover:translate-x-0.5 transition-transform"></i>
104
- </a>
105
- </nav>
106
  </div>
 
 
 
107
  </header>
108
 
109
- <!-- Main Content -->
110
- <main class="flex-grow container mx-auto px-4 py-8 md:py-12 flex flex-col items-center justify-center">
111
-
112
- <div class="w-full max-w-2xl">
113
- <div class="text-center mb-10 space-y-2">
114
- <h1 class="text-4xl md:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-brand-400 via-brand-500 to-purple-500 drop-shadow-sm">
115
- DeviantArt Archiver
116
- </h1>
117
- <p class="text-gray-400 text-lg md:text-base max-w-lg mx-auto leading-relaxed">
118
- Extract media from DeviantArt links, package them into a secure ZIP, and simulate cloud upload.
119
- </p>
120
  </div>
121
 
122
- <!-- Downloader Component -->
123
- <div id="downloader-card" class="glass-panel rounded-2xl p-6 md:p-8 shadow-2xl relative overflow-hidden">
124
- <!-- Decorative background glow -->
125
- <div class="absolute -top-24 -right-24 w-48 h-48 bg-brand-500/10 rounded-full blur-3xl pointer-events-none"></div>
126
- <div class="absolute -bottom-24 -left-24 w-48 h-48 bg-purple-500/10 rounded-full blur-3xl pointer-events-none"></div>
127
-
128
- <form id="process-form" class="space-y-6 relative z-10">
129
- <div class="input-group relative">
130
- <label for="url" class="block text-sm font-medium text-gray-300 mb-2 ml-1">
131
- DeviantArt Artwork URL
132
- </label>
133
- <div class="relative group">
134
- <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
135
- <i class="ph ph-link text-gray-500 text-lg icon-highlight transition-colors duration-300"></i>
136
- </div>
137
- <input
138
- type="url"
139
- id="url"
140
- placeholder="https://www.deviantart.com/..."
141
- autocomplete="off"
142
- class="w-full bg-gray-900/80 border border-gray-600 rounded-lg pl-10 pr-10 py-3.5 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-brand-500/50 focus:border-brand-500 transition-all duration-200"
143
- />
144
- <button
145
- type="button"
146
- id="clear-btn"
147
- class="hidden absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-white transition-colors cursor-pointer"
148
- >
149
- <i class="ph ph-x-circle text-lg"></i>
150
- </button>
151
- </div>
152
- <p class="mt-2 text-xs text-gray-500 ml-1 flex items-center gap-1">
153
- <i class="ph ph-info"></i>
154
- Direct link to the artwork page required.
155
- </p>
156
- </div>
157
-
158
- <button
159
- type="submit"
160
- id="submit-btn"
161
- class="w-full group relative overflow-hidden bg-gradient-to-r from-brand-600 to-brand-500 hover:from-brand-500 hover:to-brand-400 text-white font-semibold py-3.5 px-6 rounded-lg shadow-lg shadow-brand-500/25 transform hover:-translate-y-0.5 transition-all duration-200"
162
- >
163
- <span class="relative z-10 flex items-center justify-center gap-2">
164
- <i class="ph ph-download-simple text-lg"></i>
165
- <span id="btn-text">Download & Zip</span>
166
- </span>
167
- <!-- Button shine effect -->
168
- <div class="absolute top-0 -inset-full h-full w-1/2 z-5 block transform -skew-x-12 bg-gradient-to-r from-transparent to-white opacity-20 group-hover:animate-shimmer" />
169
- </button>
170
- </form>
171
 
172
- <!-- Progress Section -->
173
- <div id="progress-section" class="hidden mt-8 space-y-5">
174
- <div class="flex items-center justify-between text-sm">
175
- <span class="text-gray-400 font-medium">Processing Status</span>
176
- <span id="status-text" class="text-brand-400 font-bold animate-pulse">Initializing...</span>
177
- </div>
178
-
179
- <!-- Progress Bar -->
180
- <div class="h-2.5 w-full bg-gray-700/50 rounded-full overflow-hidden relative">
181
- <div id="progress-bar" class="h-full bg-gradient-to-r from-brand-600 via-purple-500 to-brand-400 w-0 transition-all duration-500 ease-out rounded-full shadow-[0_0_10px_rgba(14,165,233,0.5)]"></div>
182
- </div>
183
 
184
- <!-- Step Indicators -->
185
- <div class="grid grid-cols-4 gap-2 text-xs text-center text-gray-500 font-medium uppercase tracking-wider">
186
- <div class="step" id="step-1">Fetch</div>
187
- <div class="step" id="step-2">Pack</div>
188
- <div class="step" id="step-3">Upload</div>
189
- <div class="step" id="step-4">Done</div>
190
- </div>
 
 
 
 
 
 
 
 
191
  </div>
 
192
 
193
- <!-- Result Section -->
194
- <div id="result-section" class="hidden mt-8 animate-[fadeIn_0.5s_ease-out]">
195
- <div class="p-4 bg-green-900/10 border border-green-700/30 rounded-xl flex flex-col sm:flex-row items-start sm:items-center gap-4">
196
- <div class="flex-shrink-0 w-10 h-10 rounded-full bg-green-500/10 flex items-center justify-center">
197
- <i class="ph ph-check-circle text-2xl text-green-400"></i>
198
- </div>
199
- <div class="flex-grow text-center sm:text-left">
200
- <h4 class="text-green-400 font-semibold mb-0.5">Archive Ready</h4>
201
- <p class="text-sm text-gray-400">
202
- ZIP file generated successfully.
203
- </p>
204
- </div>
205
- <a
206
- id="download-link"
207
- href="#"
208
- class="flex-shrink-0 inline-flex items-center gap-2 bg-green-600 hover:bg-green-500 text-white text-sm font-bold py-2.5 px-5 rounded-lg transition-all shadow-lg shadow-green-900/20 active:scale-95"
209
- >
210
- <i class="ph ph-file-archive text-lg"></i>
211
- Download ZIP
212
- </a>
213
- </div>
214
  </div>
 
215
 
 
 
 
 
 
 
 
 
216
  </div>
217
- </div>
218
  </main>
219
 
220
- <!-- Footer -->
221
- <footer class="border-t border-gray-800 bg-gray-900 py-6 mt-auto">
222
- <div class="container mx-auto px-4 text-center text-gray-500 text-sm">
223
- <p>&copy; <span id="year"></span> DeviantArt Tool. All rights reserved.</p>
224
- </div>
225
  </footer>
226
 
227
  <!-- Toast Container -->
228
- <div id="toast-container" class="fixed top-4 left-1/2 transform -translate-x-1/2 z-[100] flex flex-col gap-2 pointer-events-none"></div>
229
 
230
  <script>
231
- // --- Logic Implementation ---
232
-
233
- document.addEventListener('DOMContentLoaded', () => {
234
- // Set Year
235
- document.getElementById('year').textContent = new Date().getFullYear();
236
-
237
- // Elements
238
- const form = document.getElementById('process-form');
239
- const urlInput = document.getElementById('url');
240
- const clearBtn = document.getElementById('clear-btn');
241
- const submitBtn = document.getElementById('submit-btn');
242
- const btnText = document.getElementById('btn-text');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
- const progressSection = document.getElementById('progress-section');
245
- const progressBar = document.getElementById('progress-bar');
246
- const statusText = document.getElementById('status-text');
247
 
248
- const resultSection = document.getElementById('result-section');
249
- const downloadLink = document.getElementById('download-link');
250
-
251
- // State
252
- let isProcessing = false;
253
- let generatedBlob = null;
254
-
255
- // Input Handling
256
- urlInput.addEventListener('input', (e) => {
257
- if (e.target.value.length > 0) {
258
- clearBtn.classList.remove('hidden');
259
- } else {
260
- clearBtn.classList.add('hidden');
261
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  });
263
 
264
- clearBtn.addEventListener('click', () => {
265
- urlInput.value = '';
266
- urlInput.focus();
267
- clearBtn.classList.add('hidden');
268
- resetUI();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  });
270
 
271
- // Validation Helper
272
- const isValidUrl = (string) => {
273
- try {
274
- const url = new URL(string);
275
- return url.hostname.includes('deviantart.com');
276
- } catch (_) {
277
- return false;
278
- }
279
- };
280
-
281
- const showToast = (message, type = 'error') => {
282
- const container = document.getElementById('toast-container');
283
- const toast = document.createElement('div');
284
 
285
- const bgColor = type === 'error' ? 'bg-red-500' : 'bg-green-500';
286
- const icon = type === 'error' ? 'ph-warning-circle' : 'ph-check-circle';
287
-
288
- toast.className = `toast pointer-events-auto flex items-center gap-3 px-4 py-3 rounded-lg shadow-xl text-white font-medium text-sm ${bgColor}`;
289
- toast.innerHTML = `
290
- <i class="ph ${icon} text-lg"></i>
291
- <span>${message}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  `;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
- container.appendChild(toast);
295
-
296
- // Remove after 3 seconds
297
- setTimeout(() => {
298
- toast.classList.add('hiding');
299
- toast.addEventListener('animationend', () => toast.remove());
300
- }, 3000);
301
- };
302
-
303
- const resetUI = () => {
304
- resultSection.classList.add('hidden');
305
- progressSection.classList.add('hidden');
306
- progressBar.style.width = '0%';
307
- document.querySelectorAll('.step').forEach(el => el.classList.remove('text-brand-400', 'text-white'));
308
- generatedBlob = null;
309
- };
310
-
311
- const updateProgress = (percent, status, stepIndex) => {
312
- progressBar.style.width = `${percent}%`;
313
- statusText.textContent = status;
314
 
315
- if (stepIndex !== null) {
316
- const step = document.getElementById(`step-${stepIndex}`);
317
- if (step) {
318
- step.classList.add('text-brand-400');
319
- // Make previous steps white
320
- for(let i=1; i<stepIndex; i++) {
321
- document.getElementById(`step-${i}`).classList.remove('text-brand-400');
322
- document.getElementById(`step-${i}`).classList.add('text-white');
323
- }
324
- }
325
- }
326
- };
327
-
328
- // --- Main Process Logic ---
329
- form.addEventListener('submit', async (e) => {
330
- e.preventDefault();
331
- const url = urlInput.value.trim();
332
-
333
- if (!url) {
334
- showToast('Please enter a DeviantArt URL');
335
- return;
336
- }
337
-
338
- if (!isValidUrl(url)) {
339
- showToast('Please enter a valid DeviantArt URL');
340
- return;
341
- }
342
-
343
- if (isProcessing) return;
344
- isProcessing = true;
345
-
346
- // UI Updates
347
- submitBtn.disabled = true;
348
- submitBtn.classList.add('opacity-75', 'cursor-not-allowed');
349
- btnText.textContent = 'Processing...';
350
- resetUI();
351
- progressSection.classList.remove('hidden');
352
-
353
- try {
354
- // --- SIMULATION STEP 1: Fetch & Analyze ---
355
- updateProgress(15, 'Connecting to DeviantArt...', 1);
356
- await new Promise(r => setTimeout(r, 1000)); // Simulate network
357
-
358
- updateProgress(35, 'Analyzing media content...', 1);
359
- await new Promise(r => setTimeout(r, 800));
360
-
361
- // --- SIMULATION STEP 2: Zipping ---
362
- updateProgress(50, 'Compressing images...', 2);
363
- await new Promise(r => setTimeout(r, 600));
364
-
365
- // Generate a REAL Zip file client-side
366
- const zip = new JSZip();
367
-
368
- // Add a text file with metadata
369
- zip.file("source_url.txt", `Original URL: ${url}\nDownloaded: ${new Date().toLocaleString()}`);
370
-
371
- // Add a placeholder image (Base64) to simulate content
372
- // In a real backend scenario, this would be the actual downloaded image
373
- const mockImageContent = "Mock Image Content for Demo";
374
- zip.file("artwork_info.txt", "Due to browser security (CORS), we cannot scrape external sites directly from a purely client-side HTML file without a proxy. This file proves the ZIP generation works.");
375
-
376
- const content = await zip.generateAsync({type:"blob"});
377
- generatedBlob = content;
378
-
379
- updateProgress(80, 'Generating ZIP archive...', 2);
380
- await new Promise(r => setTimeout(r, 800));
381
-
382
- // --- SIMULATION STEP 3: Upload ---
383
- updateProgress(90, 'Uploading to MediaFire...', 3);
384
- await new Promise(r => setTimeout(r, 1200));
385
-
386
- // --- FINISH ---
387
- updateProgress(100, 'Complete!', 4);
388
- document.getElementById('step-4').classList.add('text-green-400');
389
-
390
- // Show Result
391
- progressSection.classList.add('hidden');
392
- resultSection.classList.remove('hidden');
393
-
394
- // Setup Download Link
395
- const downloadUrl = URL.createObjectURL(generatedBlob);
396
- downloadLink.href = downloadUrl;
397
- downloadLink.download = `deviantart_archive_${Date.now()}.zip`;
398
-
399
- showToast('ZIP file generated successfully!', 'success');
400
-
401
- } catch (error) {
402
- console.error(error);
403
- showToast('An error occurred during processing.');
404
- statusText.textContent = 'Error occurred';
405
- statusText.classList.add('text-red-500');
406
- } finally {
407
- isProcessing = false;
408
- submitBtn.disabled = false;
409
- submitBtn.classList.remove('opacity-75', 'cursor-not-allowed');
410
- btnText.textContent = 'Download & Zip';
411
- }
412
- });
413
  });
 
 
 
 
414
  </script>
415
  </body>
416
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Text to Zip Blender</title>
7
 
8
+ <!-- Icons -->
9
+ <link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
10
 
11
+ <!-- JSZip Library -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
13
+ <!-- FileSaver Library -->
14
  <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
15
 
16
  <style>
17
+ :root {
18
+ --bg-color: #0f172a;
19
+ --panel-bg: rgba(30, 41, 59, 0.7);
20
+ --panel-border: rgba(148, 163, 184, 0.1);
21
+ --primary: #6366f1;
22
+ --primary-hover: #4f46e5;
23
+ --accent: #ec4899;
24
+ --text-main: #f8fafc;
25
+ --text-muted: #94a3b8;
26
+ --danger: #ef4444;
27
+ --success: #10b981;
28
+ --glass-blur: 12px;
29
+ --radius: 16px;
30
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
31
+ }
32
+
33
+ * {
34
+ box-sizing: border-box;
35
+ margin: 0;
36
+ padding: 0;
37
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
38
+ }
39
+
40
  body {
41
+ background-color: var(--bg-color);
42
+ background-image:
43
+ radial-gradient(at 0% 0%, hsla(253,16%,7%,1) 0, transparent 50%),
44
+ radial-gradient(at 50% 0%, hsla(225,39%,30%,1) 0, transparent 50%),
45
+ radial-gradient(at 100% 0%, hsla(339,49%,30%,1) 0, transparent 50%);
46
+ color: var(--text-main);
47
+ min-height: 100vh;
48
+ display: flex;
49
+ flex-direction: column;
50
+ overflow-x: hidden;
51
+ }
52
+
53
+ /* --- Header --- */
54
+ header {
55
+ padding: 1.5rem 2rem;
56
+ display: flex;
57
+ justify-content: space-between;
58
+ align-items: center;
59
+ backdrop-filter: blur(var(--glass-blur));
60
+ border-bottom: 1px solid var(--panel-border);
61
+ position: sticky;
62
+ top: 0;
63
+ z-index: 100;
64
+ }
65
+
66
+ .logo {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: 0.75rem;
70
+ font-size: 1.5rem;
71
+ font-weight: 700;
72
+ background: linear-gradient(to right, var(--primary), var(--accent));
73
+ -webkit-background-clip: text;
74
+ -webkit-text-fill-color: transparent;
75
+ }
76
+
77
+ .logo i {
78
+ -webkit-text-fill-color: var(--accent);
79
+ font-size: 1.75rem;
80
+ }
81
+
82
+ .anycoder-link {
83
+ font-size: 0.9rem;
84
+ color: var(--text-muted);
85
+ text-decoration: none;
86
+ padding: 0.5rem 1rem;
87
+ border: 1px solid var(--panel-border);
88
+ border-radius: 20px;
89
+ transition: var(--transition);
90
+ background: rgba(255,255,255,0.05);
91
+ }
92
+
93
+ .anycoder-link:hover {
94
+ color: var(--text-main);
95
+ background: rgba(255,255,255,0.1);
96
+ border-color: var(--primary);
97
+ }
98
+
99
+ /* --- Main Layout --- */
100
+ main {
101
+ flex: 1;
102
+ max-width: 1400px;
103
+ margin: 0 auto;
104
+ width: 100%;
105
+ padding: 2rem;
106
+ display: grid;
107
+ grid-template-columns: 1fr 1fr;
108
+ gap: 2rem;
109
+ }
110
+
111
+ /* --- Panels --- */
112
+ .panel {
113
+ background: var(--panel-bg);
114
+ border: 1px solid var(--panel-border);
115
+ border-radius: var(--radius);
116
+ padding: 2rem;
117
+ backdrop-filter: blur(var(--glass-blur));
118
+ display: flex;
119
+ flex-direction: column;
120
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
121
+ }
122
+
123
+ .panel-header {
124
+ margin-bottom: 1.5rem;
125
+ display: flex;
126
+ justify-content: space-between;
127
+ align-items: center;
128
+ }
129
+
130
+ .panel-title {
131
+ font-size: 1.25rem;
132
+ font-weight: 600;
133
+ display: flex;
134
+ align-items: center;
135
+ gap: 0.5rem;
136
+ }
137
+
138
+ /* --- Form Elements --- */
139
+ .input-group {
140
+ margin-bottom: 1.25rem;
141
+ }
142
+
143
+ label {
144
+ display: block;
145
+ margin-bottom: 0.5rem;
146
+ font-size: 0.9rem;
147
+ color: var(--text-muted);
148
+ font-weight: 500;
149
+ }
150
+
151
+ input[type="text"], textarea {
152
+ width: 100%;
153
+ background: rgba(15, 23, 42, 0.6);
154
+ border: 1px solid var(--panel-border);
155
+ border-radius: 8px;
156
+ padding: 0.75rem 1rem;
157
+ color: var(--text-main);
158
+ font-size: 1rem;
159
+ transition: var(--transition);
160
+ }
161
+
162
+ input[type="text"]:focus, textarea:focus {
163
+ outline: none;
164
+ border-color: var(--primary);
165
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
166
+ }
167
+
168
+ textarea {
169
+ min-height: 200px;
170
+ resize: vertical;
171
+ font-family: 'Consolas', 'Monaco', monospace;
172
+ line-height: 1.5;
173
+ }
174
+
175
+ .btn {
176
+ display: inline-flex;
177
+ align-items: center;
178
+ justify-content: center;
179
+ gap: 0.5rem;
180
+ padding: 0.75rem 1.5rem;
181
+ border-radius: 8px;
182
+ font-weight: 600;
183
+ cursor: pointer;
184
+ border: none;
185
+ transition: var(--transition);
186
+ font-size: 1rem;
187
+ }
188
+
189
+ .btn-primary {
190
+ background: linear-gradient(135deg, var(--primary), var(--primary-hover));
191
+ color: white;
192
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
193
+ }
194
+
195
+ .btn-primary:hover {
196
+ transform: translateY(-2px);
197
+ box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
198
+ }
199
+
200
+ .btn-block {
201
+ width: 100%;
202
+ }
203
+
204
+ .btn-icon {
205
+ padding: 0.5rem;
206
+ border-radius: 6px;
207
+ background: rgba(255,255,255,0.05);
208
+ color: var(--text-muted);
209
  }
210
 
211
+ .btn-icon:hover {
212
+ background: rgba(255,255,255,0.1);
213
+ color: var(--text-main);
 
 
214
  }
215
 
216
+ .btn-danger-icon {
217
+ color: var(--danger);
218
+ }
219
+
220
+ .btn-danger-icon:hover {
221
+ background: rgba(239, 68, 68, 0.1);
222
  }
223
 
224
+ /* --- File List (The Blender Bowl) --- */
225
+ .blender-bowl {
226
+ flex: 1;
227
+ background: rgba(15, 23, 42, 0.4);
228
+ border-radius: 12px;
229
+ border: 1px dashed var(--panel-border);
230
+ padding: 1rem;
231
+ overflow-y: auto;
232
+ display: flex;
233
+ flex-direction: column;
234
+ gap: 0.75rem;
235
+ min-height: 300px;
236
  }
237
+
238
+ .empty-state {
239
+ text-align: center;
240
+ color: var(--text-muted);
241
+ margin-top: 4rem;
242
+ display: flex;
243
+ flex-direction: column;
244
+ align-items: center;
245
+ gap: 1rem;
246
+ }
247
+
248
+ .empty-state i {
249
+ font-size: 3rem;
250
+ opacity: 0.5;
251
+ }
252
+
253
+ .file-item {
254
+ display: flex;
255
+ align-items: center;
256
+ justify-content: space-between;
257
+ background: rgba(255, 255, 255, 0.03);
258
+ border: 1px solid var(--panel-border);
259
+ padding: 0.75rem 1rem;
260
+ border-radius: 8px;
261
+ animation: slideIn 0.3s ease-out;
262
+ transition: var(--transition);
263
+ }
264
+
265
+ .file-item:hover {
266
+ background: rgba(255, 255, 255, 0.06);
267
+ transform: translateX(4px);
268
+ }
269
+
270
+ .file-info {
271
+ display: flex;
272
+ align-items: center;
273
+ gap: 0.75rem;
274
+ }
275
+
276
+ .file-icon {
277
+ color: var(--primary);
278
+ }
279
+
280
+ .file-name {
281
+ font-weight: 500;
282
+ font-size: 0.95rem;
283
+ }
284
+
285
+ .file-size {
286
+ font-size: 0.75rem;
287
+ color: var(--text-muted);
288
+ margin-left: 0.5rem;
289
  }
290
+
291
+ /* --- Footer --- */
292
+ footer {
293
+ text-align: center;
294
+ padding: 2rem;
295
+ color: var(--text-muted);
296
+ font-size: 0.85rem;
297
+ border-top: 1px solid var(--panel-border);
298
+ margin-top: auto;
299
+ }
300
+
301
+ /* --- Toast Notification --- */
302
+ .toast-container {
303
+ position: fixed;
304
+ bottom: 2rem;
305
+ right: 2rem;
306
+ z-index: 1000;
307
+ display: flex;
308
+ flex-direction: column;
309
+ gap: 1rem;
310
+ }
311
+
312
  .toast {
313
+ background: rgba(30, 41, 59, 0.95);
314
+ border-left: 4px solid var(--primary);
315
+ color: white;
316
+ padding: 1rem 1.5rem;
317
+ border-radius: 8px;
318
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
319
+ display: flex;
320
+ align-items: center;
321
+ gap: 0.75rem;
322
+ animation: slideInRight 0.3s cubic-bezier(0.2, 1, 0.3, 1);
323
+ backdrop-filter: blur(4px);
324
+ min-width: 300px;
325
+ }
326
+
327
+ .toast.success { border-color: var(--success); }
328
+ .toast.error { border-color: var(--danger); }
329
+
330
+ .toast i { font-size: 1.25rem; }
331
+ .toast.success i { color: var(--success); }
332
+ .toast.error i { color: var(--danger); }
333
+
334
+ /* --- Animations --- */
335
+ @keyframes slideIn {
336
+ from { opacity: 0; transform: translateY(10px); }
337
+ to { opacity: 1; transform: translateY(0); }
338
+ }
339
+
340
+ @keyframes slideInRight {
341
+ from { opacity: 0; transform: translateX(50px); }
342
+ to { opacity: 1; transform: translateX(0); }
343
+ }
344
+
345
+ @keyframes fadeOut {
346
+ to { opacity: 0; transform: translateY(10px); }
347
+ }
348
+
349
+ /* --- Scrollbar --- */
350
+ ::-webkit-scrollbar {
351
+ width: 8px;
352
+ }
353
+ ::-webkit-scrollbar-track {
354
+ background: rgba(15, 23, 42, 0.5);
355
+ }
356
+ ::-webkit-scrollbar-thumb {
357
+ background: rgba(148, 163, 184, 0.3);
358
+ border-radius: 4px;
359
  }
360
+ ::-webkit-scrollbar-thumb:hover {
361
+ background: rgba(148, 163, 184, 0.5);
362
+ }
363
+
364
+ /* --- Responsive --- */
365
+ @media (max-width: 900px) {
366
+ main {
367
+ grid-template-columns: 1fr;
368
+ }
369
+ }
370
+
371
+ @media (max-width: 600px) {
372
+ header {
373
+ padding: 1rem;
374
+ }
375
+ .logo span {
376
+ display: none;
377
+ }
378
+ main {
379
+ padding: 1rem;
380
+ }
381
+ .panel {
382
+ padding: 1.5rem;
383
+ }
384
  }
385
  </style>
386
  </head>
387
+ <body>
 
 
 
 
 
 
 
 
 
 
388
 
389
+ <header>
390
+ <div class="logo">
391
+ <i class="ri-blender-line"></i>
392
+ <span>TextToZip</span>
 
 
 
 
 
 
 
393
  </div>
394
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
395
+ Built with anycoder
396
+ </a>
397
  </header>
398
 
399
+ <main>
400
+ <!-- Input Section -->
401
+ <section class="panel">
402
+ <div class="panel-header">
403
+ <h2 class="panel-title">
404
+ <i class="ri-file-text-line"></i> Add Ingredients
405
+ </h2>
 
 
 
 
406
  </div>
407
 
408
+ <div class="input-group">
409
+ <label for="fileName">File Name</label>
410
+ <input type="text" id="fileName" placeholder="e.g., hello.txt, config.json, style.css">
411
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
 
413
+ <div class="input-group">
414
+ <label for="fileContent">Content</label>
415
+ <textarea id="fileContent" placeholder="Type or paste your text content here..."></textarea>
416
+ </div>
 
 
 
 
 
 
 
417
 
418
+ <button class="btn btn-primary btn-block" id="addBtn">
419
+ <i class="ri-add-line"></i> Add to Blender
420
+ </button>
421
+ </section>
422
+
423
+ <!-- Preview Section -->
424
+ <section class="panel">
425
+ <div class="panel-header">
426
+ <h2 class="panel-title">
427
+ <i class="ri-folder-zip-line"></i> Blender Bowl
428
+ </h2>
429
+ <div style="display: flex; gap: 0.5rem;">
430
+ <button class="btn btn-icon" id="clearBtn" title="Clear All">
431
+ <i class="ri-delete-bin-line"></i>
432
+ </button>
433
  </div>
434
+ </div>
435
 
436
+ <div class="blender-bowl" id="fileList">
437
+ <div class="empty-state" id="emptyState">
438
+ <i class="ri-inbox-line"></i>
439
+ <p>The bowl is empty.<br>Add some text files to start blending.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  </div>
441
+ </div>
442
 
443
+ <div style="margin-top: 1.5rem;">
444
+ <div class="input-group">
445
+ <label for="zipName">Output ZIP Filename</label>
446
+ <input type="text" id="zipName" value="blended_files.zip">
447
+ </div>
448
+ <button class="btn btn-primary btn-block" id="downloadBtn" style="background: linear-gradient(135deg, var(--accent), #be185d);">
449
+ <i class="ri-download-cloud-2-line"></i> Blend & Download ZIP
450
+ </button>
451
  </div>
452
+ </section>
453
  </main>
454
 
455
+ <footer>
456
+ &copy; 2023 Text to Zip Blender. Pure HTML/CSS/JS Application.
 
 
 
457
  </footer>
458
 
459
  <!-- Toast Container -->
460
+ <div class="toast-container" id="toastContainer"></div>
461
 
462
  <script>
463
+ // --- State Management ---
464
+ const state = {
465
+ files: [] // Array of { id, name, content, size }
466
+ };
467
+
468
+ // --- DOM Elements ---
469
+ const els = {
470
+ fileName: document.getElementById('fileName'),
471
+ fileContent: document.getElementById('fileContent'),
472
+ addBtn: document.getElementById('addBtn'),
473
+ fileList: document.getElementById('fileList'),
474
+ emptyState: document.getElementById('emptyState'),
475
+ clearBtn: document.getElementById('clearBtn'),
476
+ downloadBtn: document.getElementById('downloadBtn'),
477
+ zipName: document.getElementById('zipName'),
478
+ toastContainer: document.getElementById('toastContainer')
479
+ };
480
+
481
+ // --- Helpers ---
482
+ function generateId() {
483
+ return Date.now().toString(36) + Math.random().toString(36).substr(2);
484
+ }
485
+
486
+ function formatSize(bytes) {
487
+ if (bytes === 0) return '0 B';
488
+ const k = 1024;
489
+ const sizes = ['B', 'KB', 'MB'];
490
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
491
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
492
+ }
493
+
494
+ function showToast(message, type = 'success') {
495
+ const toast = document.createElement('div');
496
+ toast.className = `toast ${type}`;
497
 
498
+ const icon = type === 'success' ? 'ri-checkbox-circle-line' : 'ri-error-warning-line';
 
 
499
 
500
+ toast.innerHTML = `
501
+ <i class="${icon}"></i>
502
+ <span>${message}</span>
503
+ `;
504
+
505
+ els.toastContainer.appendChild(toast);
506
+
507
+ // Remove after 3 seconds
508
+ setTimeout(() => {
509
+ toast.style.animation = 'fadeOut 0.3s forwards';
510
+ toast.addEventListener('animationend', () => {
511
+ toast.remove();
512
+ });
513
+ }, 3000);
514
+ }
515
+
516
+ // --- Core Functions ---
517
+
518
+ function addFile() {
519
+ const name = els.fileName.value.trim();
520
+ const content = els.fileContent.value;
521
+
522
+ // Validation
523
+ if (!name) {
524
+ showToast('Please enter a file name.', 'error');
525
+ els.fileName.focus();
526
+ return;
527
+ }
528
+ if (!content) {
529
+ showToast('Content cannot be empty.', 'error');
530
+ els.fileContent.focus();
531
+ return;
532
+ }
533
+
534
+ // Check for duplicate names
535
+ const exists = state.files.some(f => f.name === name);
536
+ if (exists) {
537
+ showToast(`A file named "${name}" already exists.`, 'error');
538
+ return;
539
+ }
540
+
541
+ // Calculate size (UTF-16 bytes)
542
+ const size = new Blob([content]).size;
543
+
544
+ // Add to state
545
+ state.files.push({
546
+ id: generateId(),
547
+ name: name,
548
+ content: content,
549
+ size: size
550
  });
551
 
552
+ // Reset inputs
553
+ els.fileName.value = '';
554
+ els.fileContent.value = '';
555
+
556
+ // UX: Keep focus on filename for rapid entry
557
+ els.fileName.focus();
558
+
559
+ renderList();
560
+ showToast('File added to the bowl!');
561
+ }
562
+
563
+ function removeFile(id) {
564
+ state.files = state.files.filter(f => f.id !== id);
565
+ renderList();
566
+ }
567
+
568
+ function clearAll() {
569
+ if (state.files.length === 0) return;
570
+ if(confirm('Are you sure you want to empty the blender bowl?')) {
571
+ state.files = [];
572
+ renderList();
573
+ showToast('Bowl cleared.', 'success');
574
+ }
575
+ }
576
+
577
+ function renderList() {
578
+ // Toggle Empty State
579
+ if (state.files.length === 0) {
580
+ els.emptyState.style.display = 'flex';
581
+ } else {
582
+ els.emptyState.style.display = 'none';
583
+ }
584
+
585
+ // Clear current list (except empty state)
586
+ Array.from(els.fileList.children).forEach(child => {
587
+ if (child.id !== 'emptyState') child.remove();
588
  });
589
 
590
+ // Render items
591
+ state.files.forEach(file => {
592
+ const item = document.createElement('div');
593
+ item.className = 'file-item';
 
 
 
 
 
 
 
 
 
594
 
595
+ // Determine icon based on extension (simple heuristic)
596
+ let iconClass = 'ri-file-text-line';
597
+ if (file.name.endsWith('.json')) iconClass = 'ri-braces-line';
598
+ if (file.name.endsWith('.html')) iconClass = 'ri-html5-line';
599
+ if (file.name.endsWith('.css')) iconClass = 'ri-css3-line';
600
+ if (file.name.endsWith('.js')) iconClass = 'ri-javascript-line';
601
+ if (file.name.endsWith('.md')) iconClass = 'ri-markdown-line';
602
+ if (file.name.endsWith('.xml') || file.name.endsWith('.svg')) iconClass = 'ri-code-s-slash-line';
603
+
604
+ item.innerHTML = `
605
+ <div class="file-info">
606
+ <i class="${iconClass} file-icon"></i>
607
+ <div>
608
+ <div class="file-name">${file.name}</div>
609
+ <span class="file-size">${formatSize(file.size)}</span>
610
+ </div>
611
+ </div>
612
+ <button class="btn btn-icon btn-danger-icon" onclick="removeFile('${file.id}')" title="Remove">
613
+ <i class="ri-close-line"></i>
614
+ </button>
615
  `;
616
+ els.fileList.appendChild(item);
617
+ });
618
+ }
619
+
620
+ async function generateZip() {
621
+ if (state.files.length === 0) {
622
+ showToast('The bowl is empty. Add files first.', 'error');
623
+ return;
624
+ }
625
+
626
+ let zipName = els.zipName.value.trim() || 'download.zip';
627
+ if (!zipName.endsWith('.zip')) {
628
+ zipName += '.zip';
629
+ }
630
+
631
+ const btnOriginalText = els.downloadBtn.innerHTML;
632
+ els.downloadBtn.innerHTML = '<i class="ri-loader-4-line ri-spin"></i> Blending...';
633
+ els.downloadBtn.disabled = true;
634
+
635
+ try {
636
+ const zip = new JSZip();
637
+
638
+ // Add files to zip
639
+ state.files.forEach(file => {
640
+ zip.file(file.name, file.content);
641
+ });
642
 
643
+ // Generate zip
644
+ const content = await zip.generateAsync({ type: "blob" });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
645
 
646
+ // Trigger download
647
+ saveAs(content, zipName);
648
+
649
+ showToast('ZIP file blended successfully!');
650
+
651
+ } catch (err) {
652
+ console.error(err);
653
+ showToast('Error creating ZIP file.', 'error');
654
+ } finally {
655
+ els.downloadBtn.innerHTML = btnOriginalText;
656
+ els.downloadBtn.disabled = false;
657
+ }
658
+ }
659
+
660
+ // --- Event Listeners ---
661
+ els.addBtn.addEventListener('click', addFile);
662
+
663
+ // Allow Ctrl+Enter to submit
664
+ els.fileContent.addEventListener('keydown', (e) => {
665
+ if (e.ctrlKey && e.key === 'Enter') {
666
+ addFile();
667
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
668
  });
669
+
670
+ els.clearBtn.addEventListener('click', clearAll);
671
+ els.downloadBtn.addEventListener('click', generateZip);
672
+
673
  </script>
674
  </body>
675
  </html>