@jjlmoya/utils-audiovisual 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +198 -0
  3. package/src/category/i18n/es.ts +198 -0
  4. package/src/category/i18n/fr.ts +198 -0
  5. package/src/category/index.ts +17 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +4 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +32 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/seo_length.test.ts +22 -0
  21. package/src/tests/tool_validation.test.ts +17 -0
  22. package/src/tool/chromaticLens/bibliography.astro +17 -0
  23. package/src/tool/chromaticLens/component.astro +178 -0
  24. package/src/tool/chromaticLens/i18n/en.ts +246 -0
  25. package/src/tool/chromaticLens/i18n/es.ts +244 -0
  26. package/src/tool/chromaticLens/i18n/fr.ts +244 -0
  27. package/src/tool/chromaticLens/index.ts +43 -0
  28. package/src/tool/chromaticLens/logic.ts +87 -0
  29. package/src/tool/chromaticLens/seo.astro +15 -0
  30. package/src/tool/chromaticLens/style.css +308 -0
  31. package/src/tool/chromaticLens/ui.ts +109 -0
  32. package/src/tool/collageMaker/bibliography.astro +17 -0
  33. package/src/tool/collageMaker/component.astro +302 -0
  34. package/src/tool/collageMaker/i18n/en.ts +233 -0
  35. package/src/tool/collageMaker/i18n/es.ts +231 -0
  36. package/src/tool/collageMaker/i18n/fr.ts +231 -0
  37. package/src/tool/collageMaker/index.ts +51 -0
  38. package/src/tool/collageMaker/logic.ts +134 -0
  39. package/src/tool/collageMaker/seo.astro +15 -0
  40. package/src/tool/collageMaker/style.css +386 -0
  41. package/src/tool/exifCleaner/bibliography.astro +18 -0
  42. package/src/tool/exifCleaner/component.astro +162 -0
  43. package/src/tool/exifCleaner/i18n/en.ts +277 -0
  44. package/src/tool/exifCleaner/i18n/es.ts +277 -0
  45. package/src/tool/exifCleaner/i18n/fr.ts +277 -0
  46. package/src/tool/exifCleaner/index.ts +57 -0
  47. package/src/tool/exifCleaner/logic.ts +135 -0
  48. package/src/tool/exifCleaner/seo.astro +18 -0
  49. package/src/tool/exifCleaner/style.css +289 -0
  50. package/src/tool/exifCleaner/ui.ts +117 -0
  51. package/src/tool/imageCompressor/bibliography.astro +17 -0
  52. package/src/tool/imageCompressor/component.astro +262 -0
  53. package/src/tool/imageCompressor/i18n/en.ts +232 -0
  54. package/src/tool/imageCompressor/i18n/es.ts +230 -0
  55. package/src/tool/imageCompressor/i18n/fr.ts +230 -0
  56. package/src/tool/imageCompressor/index.ts +50 -0
  57. package/src/tool/imageCompressor/logic.ts +79 -0
  58. package/src/tool/imageCompressor/seo.astro +15 -0
  59. package/src/tool/imageCompressor/style.css +503 -0
  60. package/src/tool/printQualityCalculator/bibliography.astro +18 -0
  61. package/src/tool/printQualityCalculator/component.astro +318 -0
  62. package/src/tool/printQualityCalculator/i18n/en.ts +247 -0
  63. package/src/tool/printQualityCalculator/i18n/es.ts +245 -0
  64. package/src/tool/printQualityCalculator/i18n/fr.ts +245 -0
  65. package/src/tool/printQualityCalculator/index.ts +56 -0
  66. package/src/tool/printQualityCalculator/logic.ts +53 -0
  67. package/src/tool/printQualityCalculator/seo.astro +18 -0
  68. package/src/tool/printQualityCalculator/style.css +491 -0
  69. package/src/tool/printQualityCalculator/ui.ts +122 -0
  70. package/src/tool/privacyBlur/bibliography.astro +17 -0
  71. package/src/tool/privacyBlur/component.astro +230 -0
  72. package/src/tool/privacyBlur/i18n/en.ts +238 -0
  73. package/src/tool/privacyBlur/i18n/es.ts +236 -0
  74. package/src/tool/privacyBlur/i18n/fr.ts +236 -0
  75. package/src/tool/privacyBlur/index.ts +49 -0
  76. package/src/tool/privacyBlur/logic.ts +249 -0
  77. package/src/tool/privacyBlur/seo.astro +15 -0
  78. package/src/tool/privacyBlur/style.css +332 -0
  79. package/src/tool/privacyBlur/ui.ts +124 -0
  80. package/src/tool/subtitleSync/bibliography.astro +17 -0
  81. package/src/tool/subtitleSync/component.astro +187 -0
  82. package/src/tool/subtitleSync/i18n/en.ts +241 -0
  83. package/src/tool/subtitleSync/i18n/es.ts +241 -0
  84. package/src/tool/subtitleSync/i18n/fr.ts +241 -0
  85. package/src/tool/subtitleSync/index.ts +49 -0
  86. package/src/tool/subtitleSync/logic.ts +91 -0
  87. package/src/tool/subtitleSync/seo.astro +15 -0
  88. package/src/tool/subtitleSync/style.css +325 -0
  89. package/src/tool/subtitleSync/ui.ts +152 -0
  90. package/src/tool/timelapseCalculator/bibliography.astro +15 -0
  91. package/src/tool/timelapseCalculator/component.astro +148 -0
  92. package/src/tool/timelapseCalculator/i18n/en.ts +169 -0
  93. package/src/tool/timelapseCalculator/i18n/es.ts +169 -0
  94. package/src/tool/timelapseCalculator/i18n/fr.ts +169 -0
  95. package/src/tool/timelapseCalculator/index.ts +52 -0
  96. package/src/tool/timelapseCalculator/logic.ts +46 -0
  97. package/src/tool/timelapseCalculator/seo.astro +18 -0
  98. package/src/tool/timelapseCalculator/style.css +285 -0
  99. package/src/tool/tvDistance/bibliography.astro +17 -0
  100. package/src/tool/tvDistance/component.astro +178 -0
  101. package/src/tool/tvDistance/i18n/en.ts +223 -0
  102. package/src/tool/tvDistance/i18n/es.ts +223 -0
  103. package/src/tool/tvDistance/i18n/fr.ts +223 -0
  104. package/src/tool/tvDistance/index.ts +49 -0
  105. package/src/tool/tvDistance/logic.ts +47 -0
  106. package/src/tool/tvDistance/seo.astro +15 -0
  107. package/src/tool/tvDistance/style.css +435 -0
  108. package/src/tool/tvDistance/ui.ts +66 -0
  109. package/src/tool/videoFrameExtractor/bibliography.astro +17 -0
  110. package/src/tool/videoFrameExtractor/component.astro +285 -0
  111. package/src/tool/videoFrameExtractor/i18n/en.ts +235 -0
  112. package/src/tool/videoFrameExtractor/i18n/es.ts +235 -0
  113. package/src/tool/videoFrameExtractor/i18n/fr.ts +235 -0
  114. package/src/tool/videoFrameExtractor/index.ts +53 -0
  115. package/src/tool/videoFrameExtractor/logic.ts +49 -0
  116. package/src/tool/videoFrameExtractor/seo.astro +15 -0
  117. package/src/tool/videoFrameExtractor/style.css +426 -0
  118. package/src/tool/videoFrameExtractor/ui.ts +179 -0
  119. package/src/tools.ts +25 -0
  120. package/src/types.ts +72 -0
@@ -0,0 +1,386 @@
1
+ .cm-root {
2
+ --cm-bg: #fff;
3
+ --cm-bg-muted: #f8fafc;
4
+ --cm-border: #e2e8f0;
5
+ --cm-text: #0f172a;
6
+ --cm-text-muted: #64748b;
7
+ --cm-primary: #6366f1;
8
+ --cm-primary-light: #eef2ff;
9
+ --cm-shadow: rgba(0,0,0,0.07);
10
+ --cm-radius: 1rem;
11
+
12
+ max-width: 1100px;
13
+ margin: 0 auto;
14
+ padding: 1rem;
15
+ }
16
+
17
+ .theme-dark .cm-root {
18
+ --cm-bg: #1e293b;
19
+ --cm-bg-muted: #0f172a;
20
+ --cm-border: #334155;
21
+ --cm-text: #f1f5f9;
22
+ --cm-text-muted: #94a3b8;
23
+ --cm-primary: #818cf8;
24
+ --cm-primary-light: #1e1b4b;
25
+ --cm-shadow: rgba(0,0,0,0.4);
26
+ }
27
+
28
+ .cm-card {
29
+ background: var(--cm-bg);
30
+ border: 1px solid var(--cm-border);
31
+ border-radius: var(--cm-radius);
32
+ box-shadow: 0 4px 24px var(--cm-shadow);
33
+ overflow: hidden;
34
+ }
35
+
36
+ .cm-top-row {
37
+ display: grid;
38
+ grid-template-columns: 300px 1fr;
39
+ gap: 0;
40
+ }
41
+
42
+ @media (max-width: 700px) {
43
+ .cm-top-row {
44
+ grid-template-columns: 1fr;
45
+ }
46
+ }
47
+
48
+ .cm-left-col {
49
+ padding: 1.25rem;
50
+ border-right: 1px solid var(--cm-border);
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: 1rem;
54
+ }
55
+
56
+ @media (max-width: 700px) {
57
+ .cm-left-col {
58
+ border-right: none;
59
+ border-bottom: 1px solid var(--cm-border);
60
+ }
61
+ }
62
+
63
+ .cm-drop-zone {
64
+ position: relative;
65
+ background: var(--cm-bg-muted);
66
+ border: 2px dashed var(--cm-border);
67
+ border-radius: 0.75rem;
68
+ padding: 1.5rem 1rem;
69
+ text-align: center;
70
+ cursor: pointer;
71
+ transition: border-color 0.2s, background 0.2s;
72
+ display: flex;
73
+ flex-direction: column;
74
+ align-items: center;
75
+ gap: 0.35rem;
76
+ color: var(--cm-primary);
77
+ }
78
+
79
+ .cm-drop-zone input[type="file"] {
80
+ position: absolute;
81
+ inset: 0;
82
+ opacity: 0;
83
+ cursor: pointer;
84
+ width: 100%;
85
+ height: 100%;
86
+ }
87
+
88
+ .cm-drop-zone:hover,
89
+ .cm-drop-zone-over {
90
+ border-color: var(--cm-primary);
91
+ background: var(--cm-primary-light);
92
+ }
93
+
94
+ .cm-drop-title {
95
+ font-size: 0.9rem;
96
+ font-weight: 700;
97
+ color: var(--cm-text);
98
+ margin: 0;
99
+ }
100
+
101
+ .cm-drop-sub {
102
+ font-size: 0.75rem;
103
+ color: var(--cm-text-muted);
104
+ margin: 0;
105
+ }
106
+
107
+ .cm-drop-link {
108
+ color: var(--cm-primary);
109
+ font-weight: 600;
110
+ cursor: pointer;
111
+ }
112
+
113
+ .cm-section-label {
114
+ display: block;
115
+ font-size: 0.65rem;
116
+ font-weight: 700;
117
+ text-transform: uppercase;
118
+ letter-spacing: 0.08em;
119
+ color: var(--cm-text-muted);
120
+ }
121
+
122
+ .cm-section-header {
123
+ display: flex;
124
+ justify-content: space-between;
125
+ align-items: center;
126
+ }
127
+
128
+ .cm-badge {
129
+ background: var(--cm-primary-light);
130
+ color: var(--cm-primary);
131
+ font-size: 0.65rem;
132
+ font-weight: 700;
133
+ padding: 0.15rem 0.45rem;
134
+ border-radius: 9999px;
135
+ }
136
+
137
+ .cm-thumbs {
138
+ display: grid;
139
+ grid-template-columns: repeat(auto-fill, minmax(48px, 1fr));
140
+ gap: 0.35rem;
141
+ margin-top: 0.5rem;
142
+ }
143
+
144
+ .cm-thumb {
145
+ position: relative;
146
+ aspect-ratio: 1;
147
+ border-radius: 0.4rem;
148
+ overflow: hidden;
149
+ border: 2px solid transparent;
150
+ transition: border-color 0.15s;
151
+ }
152
+
153
+ .cm-thumb:hover {
154
+ border-color: var(--cm-primary);
155
+ }
156
+
157
+ .cm-thumb img {
158
+ width: 100%;
159
+ height: 100%;
160
+ object-fit: cover;
161
+ display: block;
162
+ }
163
+
164
+ .cm-thumb-num {
165
+ position: absolute;
166
+ bottom: 2px;
167
+ left: 3px;
168
+ font-size: 0.55rem;
169
+ font-weight: 800;
170
+ color: var(--cm-bg);
171
+ text-shadow: 0 1px 2px rgba(0,0,0,0.8);
172
+ line-height: 1;
173
+ }
174
+
175
+ .cm-thumb-del {
176
+ position: absolute;
177
+ top: 2px;
178
+ right: 2px;
179
+ width: 16px;
180
+ height: 16px;
181
+ background: rgba(239,68,68,0.9);
182
+ color: var(--cm-bg);
183
+ border: none;
184
+ border-radius: 50%;
185
+ cursor: pointer;
186
+ display: none;
187
+ align-items: center;
188
+ justify-content: center;
189
+ padding: 0;
190
+ transition: background 0.15s;
191
+ }
192
+
193
+ .cm-thumb:hover .cm-thumb-del {
194
+ display: flex;
195
+ }
196
+
197
+ .cm-thumb-del:hover {
198
+ background: #dc2626;
199
+ }
200
+
201
+ .cm-preview-col {
202
+ padding: 1.25rem;
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: 0.5rem;
206
+ min-height: 260px;
207
+ }
208
+
209
+ .cm-preview-placeholder {
210
+ flex: 1;
211
+ background: var(--cm-bg-muted);
212
+ border: 2px dashed var(--cm-border);
213
+ border-radius: 0.75rem;
214
+ display: flex;
215
+ flex-direction: column;
216
+ align-items: center;
217
+ justify-content: center;
218
+ gap: 0.5rem;
219
+ color: var(--cm-text-muted);
220
+ }
221
+
222
+ .cm-preview-placeholder p {
223
+ font-size: 0.8rem;
224
+ margin: 0;
225
+ }
226
+
227
+ .cm-canvas {
228
+ width: 100%;
229
+ height: auto;
230
+ border-radius: 0.75rem;
231
+ border: 1px solid var(--cm-border);
232
+ display: block;
233
+ }
234
+
235
+ .cm-dims-badge {
236
+ font-size: 0.65rem;
237
+ font-weight: 600;
238
+ color: var(--cm-text-muted);
239
+ background: var(--cm-bg-muted);
240
+ border: 1px solid var(--cm-border);
241
+ border-radius: 9999px;
242
+ padding: 0.15rem 0.5rem;
243
+ }
244
+
245
+ .cm-section-divider {
246
+ border-top: 1px solid var(--cm-border);
247
+ padding: 1rem 1.25rem;
248
+ display: flex;
249
+ flex-direction: column;
250
+ gap: 0.75rem;
251
+ }
252
+
253
+ .cm-layouts {
254
+ display: grid;
255
+ grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
256
+ gap: 0.4rem;
257
+ }
258
+
259
+ .cm-layout-btn {
260
+ display: flex;
261
+ flex-direction: column;
262
+ align-items: center;
263
+ gap: 0.2rem;
264
+ padding: 0.5rem 0.25rem;
265
+ background: var(--cm-bg-muted);
266
+ border: 2px solid var(--cm-border);
267
+ border-radius: 0.625rem;
268
+ cursor: pointer;
269
+ transition: border-color 0.15s, background 0.15s, opacity 0.15s;
270
+ color: var(--cm-text-muted);
271
+ position: relative;
272
+ }
273
+
274
+ .cm-layout-btn span {
275
+ font-size: 0.55rem;
276
+ font-weight: 600;
277
+ text-align: center;
278
+ line-height: 1.2;
279
+ }
280
+
281
+ .cm-layout-btn:hover:not(:disabled) {
282
+ border-color: var(--cm-primary);
283
+ color: var(--cm-primary);
284
+ background: var(--cm-primary-light);
285
+ }
286
+
287
+ .cm-layout-btn-active {
288
+ border-color: var(--cm-primary);
289
+ background: var(--cm-primary-light);
290
+ color: var(--cm-primary);
291
+ }
292
+
293
+ .cm-layout-btn-disabled {
294
+ opacity: 0.45;
295
+ cursor: not-allowed;
296
+ }
297
+
298
+ .cm-layout-need {
299
+ position: absolute;
300
+ top: 2px;
301
+ right: 4px;
302
+ font-size: 0.55rem;
303
+ font-weight: 800;
304
+ color: var(--cm-text-muted);
305
+ background: var(--cm-bg);
306
+ border: 1px solid var(--cm-border);
307
+ border-radius: 9999px;
308
+ padding: 0 3px;
309
+ line-height: 1.4;
310
+ }
311
+
312
+ .cm-settings-inline {
313
+ display: flex;
314
+ flex-wrap: wrap;
315
+ align-items: flex-end;
316
+ gap: 1rem;
317
+ }
318
+
319
+ .cm-setting {
320
+ display: flex;
321
+ flex-direction: column;
322
+ gap: 0.3rem;
323
+ }
324
+
325
+ .cm-setting-label {
326
+ font-size: 0.7rem;
327
+ font-weight: 600;
328
+ color: var(--cm-text-muted);
329
+ }
330
+
331
+ .cm-slider {
332
+ accent-color: var(--cm-primary);
333
+ width: 120px;
334
+ }
335
+
336
+ .cm-color-row {
337
+ display: flex;
338
+ align-items: center;
339
+ gap: 0.5rem;
340
+ }
341
+
342
+ .cm-color-swatch {
343
+ width: 32px;
344
+ height: 32px;
345
+ border: 2px solid var(--cm-border);
346
+ border-radius: 0.4rem;
347
+ cursor: pointer;
348
+ padding: 2px;
349
+ background: transparent;
350
+ }
351
+
352
+ .cm-color-code {
353
+ font-size: 0.7rem;
354
+ font-weight: 600;
355
+ color: var(--cm-text-muted);
356
+ font-variant-numeric: tabular-nums;
357
+ }
358
+
359
+ .cm-download-btn {
360
+ display: flex;
361
+ align-items: center;
362
+ gap: 0.4rem;
363
+ padding: 0.6rem 1.25rem;
364
+ background: linear-gradient(135deg, var(--cm-primary), #8b5cf6);
365
+ color: var(--cm-bg);
366
+ border: none;
367
+ border-radius: 0.625rem;
368
+ font-size: 0.875rem;
369
+ font-weight: 700;
370
+ cursor: pointer;
371
+ transition: opacity 0.2s, transform 0.1s;
372
+ box-shadow: 0 4px 12px rgba(99,102,241,0.3);
373
+ white-space: nowrap;
374
+ margin-left: auto;
375
+ }
376
+
377
+ .cm-download-btn:disabled {
378
+ opacity: 0.4;
379
+ cursor: not-allowed;
380
+ box-shadow: none;
381
+ }
382
+
383
+ .cm-download-btn:not(:disabled):hover {
384
+ transform: translateY(-1px);
385
+ box-shadow: 0 6px 18px rgba(99,102,241,0.4);
386
+ }
@@ -0,0 +1,18 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { exifCleaner, type ExifCleanerLocaleContent } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const localeContentLoader = (exifCleaner.i18n as Record<string, () => Promise<ExifCleanerLocaleContent>>)[locale];
12
+ const content = localeContentLoader ? await localeContentLoader() : null;
13
+ if (!content) return null;
14
+
15
+ const { bibliography } = content;
16
+ ---
17
+
18
+ <SharedBibliography links={bibliography} />
@@ -0,0 +1,162 @@
1
+ ---
2
+ import type { ExifCleanerUI } from './index';
3
+ import './style.css';
4
+
5
+ interface Props {
6
+ ui: ExifCleanerUI;
7
+ }
8
+
9
+ const { ui } = Astro.props;
10
+ ---
11
+
12
+ <div class="ec-root" id="ec-root" data-ui={JSON.stringify(ui)}>
13
+ <div class="ec-card">
14
+
15
+ <div id="ec-initial" class="ec-drop">
16
+ <input type="file" id="ec-file" accept="image/jpeg,image/png,image/webp" class="ec-hidden" />
17
+ <div class="ec-drop-icon">
18
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
19
+ <path d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zm-4.35 3.96h-3v3H9v-3H6v-2h3V8h2v3h3v2z"/>
20
+ </svg>
21
+ </div>
22
+ <h3 class="ec-title">{ui.dropTitle}</h3>
23
+ <p class="ec-subtitle">{ui.dropSubtitle}</p>
24
+
25
+ <div class="ec-badges">
26
+ <div class="ec-badge">
27
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" style="color:#10b981">
28
+ <path d="M12 1C6.48 1 2 5.48 2 11s4.48 10 10 10 10-4.48 10-10S17.52 1 12 1zm-2 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z"/>
29
+ </svg>
30
+ <span>{ui.privacySecureLabel}</span>
31
+ </div>
32
+ <div class="ec-badge">
33
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" style="color:#f59e0b">
34
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z"/>
35
+ </svg>
36
+ <span>{ui.offlineLabel}</span>
37
+ </div>
38
+ </div>
39
+ </div>
40
+
41
+ <div id="ec-processing" class="ec-processing ec-hidden">
42
+ <div class="ec-spinner"></div>
43
+ <p class="ec-processing-text">{ui.processingText}</p>
44
+ </div>
45
+
46
+ <div id="ec-result" class="ec-result ec-hidden">
47
+ <div class="ec-result-layout">
48
+ <div class="ec-preview-col">
49
+ <img id="ec-preview" class="ec-preview-img" src="#" alt="" />
50
+ <div id="ec-metadata" class="ec-metadata"></div>
51
+ </div>
52
+
53
+ <div class="ec-actions-col">
54
+ <button id="ec-download" class="ec-btn ec-btn-primary">
55
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
56
+ <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
57
+ </svg>
58
+ <span>{ui.downloadButton}</span>
59
+ </button>
60
+ <button id="ec-reset" class="ec-btn ec-btn-secondary">
61
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
62
+ <path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/>
63
+ </svg>
64
+ <span>{ui.resetButton}</span>
65
+ </button>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ </div>
71
+ </div>
72
+
73
+ <script>
74
+ import { extractExif, createCleanImage } from './logic';
75
+ import type { ExifCleanerUI } from './index';
76
+
77
+ function renderMetadata(tags: Record<string, string | number | boolean | undefined>, container: HTMLElement, riskTitle: string) {
78
+ const keys = Object.keys(tags);
79
+ if (keys.length === 0) {
80
+ container.innerHTML = '<div class="ec-no-metadata">No se encontraron metadatos.</div>';
81
+ return;
82
+ }
83
+ let html = `<div class="ec-metadata-title">${riskTitle}</div><ul class="ec-metadata-list">`;
84
+ if (tags.GPSLocation) html += `<li><span>GPS:</span> <span>${tags.GPSLocation}</span></li>`;
85
+ if (tags.Model) html += `<li><span>CÁMARA:</span> <span>${tags.Model}</span></li>`;
86
+ if (tags.DateTimeOriginal) html += `<li><span>FECHA:</span> <span>${tags.DateTimeOriginal}</span></li>`;
87
+ container.innerHTML = html + '</ul>';
88
+ }
89
+
90
+ function init() {
91
+ const root = document.getElementById('ec-root');
92
+ if (!root) return;
93
+
94
+ const ui = JSON.parse(root.dataset.ui ?? '{}') as ExifCleanerUI;
95
+ const initial = root.querySelector('#ec-initial') as HTMLElement;
96
+ const processing = root.querySelector('#ec-processing') as HTMLElement;
97
+ const result = root.querySelector('#ec-result') as HTMLElement;
98
+ const fileInput = root.querySelector('#ec-file') as HTMLInputElement;
99
+ const preview = root.querySelector('#ec-preview') as HTMLImageElement;
100
+ const metadata = root.querySelector('#ec-metadata') as HTMLElement;
101
+ const downloadBtn = root.querySelector('#ec-download') as HTMLButtonElement;
102
+ const resetBtn = root.querySelector('#ec-reset') as HTMLButtonElement;
103
+
104
+ async function processFile(file: File) {
105
+ processing.classList.remove('ec-hidden');
106
+ initial.classList.add('ec-hidden');
107
+
108
+ const reader = new FileReader();
109
+ reader.onload = async (e) => {
110
+ const exifData = await extractExif(file);
111
+ const img = new Image();
112
+ img.onload = async () => {
113
+ preview.src = img.src;
114
+ renderMetadata(exifData, metadata, ui.privacyRiskTitle);
115
+ const blob = await createCleanImage(img);
116
+
117
+ downloadBtn.onclick = () => {
118
+ const url = URL.createObjectURL(blob);
119
+ const a = document.createElement('a');
120
+ a.href = url;
121
+ a.download = `CLEAN_${file.name.split('.')[0]}.webp`;
122
+ a.click();
123
+ setTimeout(() => URL.revokeObjectURL(url), 100);
124
+ };
125
+
126
+ processing.classList.add('ec-hidden');
127
+ result.classList.remove('ec-hidden');
128
+ };
129
+ img.src = e.target?.result as string;
130
+ };
131
+ reader.readAsDataURL(file);
132
+ }
133
+
134
+ root.addEventListener('click', (e) => {
135
+ const target = e.target as HTMLElement;
136
+ if (!target.closest('button') && result.classList.contains('ec-hidden')) {
137
+ fileInput.click();
138
+ }
139
+ });
140
+
141
+ root.addEventListener('dragover', (e) => { e.preventDefault(); root.classList.add('ec-drop-active'); });
142
+ root.addEventListener('dragleave', () => root.classList.remove('ec-drop-active'));
143
+ root.addEventListener('drop', (e) => {
144
+ e.preventDefault();
145
+ root.classList.remove('ec-drop-active');
146
+ if (e.dataTransfer?.files[0]) processFile(e.dataTransfer.files[0]);
147
+ });
148
+
149
+ fileInput.addEventListener('change', () => {
150
+ if (fileInput.files?.[0]) processFile(fileInput.files[0]);
151
+ });
152
+
153
+ resetBtn.addEventListener('click', () => {
154
+ result.classList.add('ec-hidden');
155
+ initial.classList.remove('ec-hidden');
156
+ fileInput.value = '';
157
+ });
158
+ }
159
+
160
+ init();
161
+ document.addEventListener('astro:page-load', init);
162
+ </script>