@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,426 @@
1
+ .vfe-root {
2
+ --vfe-bg: #fff;
3
+ --vfe-bg-muted: #f8fafc;
4
+ --vfe-bg-glass: #fff;
5
+ --vfe-glass-border: #e2e8f0;
6
+ --vfe-glass-text: #6366f1;
7
+ --vfe-glass-muted: #94a3b8;
8
+ --vfe-glass-btn-bg: #f8fafc;
9
+ --vfe-glass-btn-border: #e2e8f0;
10
+ --vfe-glass-btn-text: #1e293b;
11
+ --vfe-batch-bg: #f1f5f9;
12
+ --vfe-border: #e2e8f0;
13
+ --vfe-text: #1e293b;
14
+ --vfe-text-muted: #94a3b8;
15
+ --vfe-primary: #6366f1;
16
+ --vfe-primary-light: rgba(99, 102, 241, 0.1);
17
+ --vfe-shadow: 0 25px 60px rgba(0,0,0,0.08);
18
+
19
+ max-width: 860px;
20
+ margin: 0 auto;
21
+ padding: 1rem;
22
+ }
23
+
24
+ .theme-dark .vfe-root {
25
+ --vfe-bg: #18181b;
26
+ --vfe-bg-muted: #09090b;
27
+ --vfe-bg-glass: #27272a;
28
+ --vfe-glass-border: #3f3f46;
29
+ --vfe-glass-text: #818cf8;
30
+ --vfe-glass-muted: #71717a;
31
+ --vfe-glass-btn-bg: #3f3f46;
32
+ --vfe-glass-btn-border: #52525b;
33
+ --vfe-glass-btn-text: #f4f4f5;
34
+ --vfe-batch-bg: #1c1c1f;
35
+ --vfe-border: #27272a;
36
+ --vfe-text: #f4f4f5;
37
+ --vfe-text-muted: #71717a;
38
+ --vfe-primary: #818cf8;
39
+ --vfe-primary-light: rgba(129, 140, 248, 0.12);
40
+ --vfe-shadow: 0 25px 60px rgba(0,0,0,0.4);
41
+ }
42
+
43
+ .vfe-premium-card {
44
+ background: var(--vfe-bg);
45
+ border: 1px solid var(--vfe-border);
46
+ border-radius: 1.5rem;
47
+ box-shadow: var(--vfe-shadow);
48
+ overflow: hidden;
49
+ }
50
+
51
+ .vfe-uploader-box {
52
+ padding: 4rem 2rem;
53
+ display: flex;
54
+ flex-direction: column;
55
+ align-items: center;
56
+ text-align: center;
57
+ gap: 0.625rem;
58
+ cursor: pointer;
59
+ border: 3px dashed var(--vfe-border);
60
+ border-radius: 1.5rem;
61
+ margin: 1rem;
62
+ transition: border-color 0.2s, background 0.2s;
63
+ }
64
+
65
+ .vfe-uploader-box:hover,
66
+ .vfe-dragover {
67
+ border-color: var(--vfe-primary);
68
+ background: var(--vfe-primary-light);
69
+ }
70
+
71
+ .vfe-uploader-icon {
72
+ width: 5rem;
73
+ height: 5rem;
74
+ background: var(--vfe-primary-light);
75
+ border-radius: 1.25rem;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ color: var(--vfe-primary);
80
+ margin-bottom: 0.5rem;
81
+ }
82
+
83
+ .vfe-uploader-icon svg {
84
+ width: 2.5rem;
85
+ height: 2.5rem;
86
+ }
87
+
88
+ .vfe-uploader-text h3 {
89
+ font-size: 1.5rem;
90
+ font-weight: 800;
91
+ color: var(--vfe-text);
92
+ margin: 0 0 0.25rem;
93
+ }
94
+
95
+ .vfe-uploader-text p {
96
+ color: var(--vfe-text-muted);
97
+ font-size: 0.95rem;
98
+ margin: 0;
99
+ }
100
+
101
+ .vfe-privacy-note {
102
+ font-size: 0.7rem;
103
+ font-weight: 700;
104
+ text-transform: uppercase;
105
+ letter-spacing: 0.08em;
106
+ color: var(--vfe-text-muted);
107
+ margin-top: 0.5rem;
108
+ }
109
+
110
+ .vfe-player-container {
111
+ display: flex;
112
+ flex-direction: column;
113
+ }
114
+
115
+ .vfe-video-wrapper video {
116
+ width: 100%;
117
+ display: block;
118
+ max-height: 65vh;
119
+ background: #000;
120
+ }
121
+
122
+ .vfe-controls-glass {
123
+ background: var(--vfe-bg-glass);
124
+ border: 1px solid var(--vfe-glass-border);
125
+ border-radius: 1rem;
126
+ margin: 0.75rem;
127
+ padding: 1.25rem;
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 1rem;
131
+ }
132
+
133
+ .vfe-time-row {
134
+ display: flex;
135
+ justify-content: space-between;
136
+ font-size: 0.875rem;
137
+ font-weight: 700;
138
+ color: var(--vfe-glass-text);
139
+ }
140
+
141
+ .vfe-scrubber {
142
+ width: 100%;
143
+ height: 4px;
144
+ accent-color: var(--vfe-primary);
145
+ cursor: pointer;
146
+ border-radius: 9999px;
147
+ }
148
+
149
+ .vfe-actions-row {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 0.625rem;
153
+ }
154
+
155
+ .vfe-btn-main {
156
+ display: inline-flex;
157
+ align-items: center;
158
+ gap: 0.4rem;
159
+ font-weight: 700;
160
+ font-size: 0.85rem;
161
+ border: none;
162
+ border-radius: 0.625rem;
163
+ cursor: pointer;
164
+ transition: all 0.15s;
165
+ white-space: nowrap;
166
+ text-decoration: none;
167
+ }
168
+
169
+ .vfe-btn-control {
170
+ padding: 0.5rem 0.875rem;
171
+ background: var(--vfe-glass-btn-bg);
172
+ color: var(--vfe-glass-btn-text);
173
+ border: 1px solid var(--vfe-glass-btn-border);
174
+ flex: 1;
175
+ justify-content: center;
176
+ }
177
+
178
+ .vfe-btn-control:hover {
179
+ border-color: var(--vfe-primary);
180
+ color: var(--vfe-primary);
181
+ }
182
+
183
+ .vfe-btn-icon-only {
184
+ flex: none;
185
+ width: 50px;
186
+ justify-content: center;
187
+ }
188
+
189
+ .vfe-btn-capture {
190
+ padding: 0.625rem 1.25rem;
191
+ background: var(--vfe-primary);
192
+ color: #fff;
193
+ flex: 1;
194
+ justify-content: center;
195
+ box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35);
196
+ }
197
+
198
+ .vfe-btn-capture:hover {
199
+ filter: brightness(1.1);
200
+ }
201
+
202
+ .vfe-btn-capture:disabled {
203
+ opacity: 0.5;
204
+ cursor: not-allowed;
205
+ }
206
+
207
+ .vfe-btn-batch {
208
+ padding: 0.45rem 1rem;
209
+ font-size: 0.8rem;
210
+ flex: none;
211
+ }
212
+
213
+ .vfe-btn-main svg {
214
+ width: 1.1rem;
215
+ height: 1.1rem;
216
+ flex-shrink: 0;
217
+ }
218
+
219
+ .vfe-batch-panel {
220
+ background: var(--vfe-batch-bg);
221
+ border-radius: 0.875rem;
222
+ padding: 0.875rem 1rem;
223
+ border: 1px solid var(--vfe-glass-btn-border);
224
+ display: flex;
225
+ flex-direction: column;
226
+ gap: 0.75rem;
227
+ }
228
+
229
+ .vfe-batch-header {
230
+ display: flex;
231
+ align-items: center;
232
+ gap: 0.5rem;
233
+ font-size: 0.8rem;
234
+ font-weight: 700;
235
+ color: var(--vfe-primary);
236
+ }
237
+
238
+ .vfe-batch-header svg {
239
+ width: 1rem;
240
+ height: 1rem;
241
+ }
242
+
243
+ .vfe-batch-controls {
244
+ display: flex;
245
+ align-items: center;
246
+ gap: 0.625rem;
247
+ flex-wrap: wrap;
248
+ }
249
+
250
+ .vfe-batch-input-group {
251
+ display: flex;
252
+ align-items: center;
253
+ gap: 0.375rem;
254
+ color: var(--vfe-glass-btn-text);
255
+ font-size: 0.8rem;
256
+ font-weight: 700;
257
+ text-transform: uppercase;
258
+ letter-spacing: 0.05em;
259
+ background: var(--vfe-bg);
260
+ border: 1px solid var(--vfe-glass-btn-border);
261
+ border-radius: 0.5rem;
262
+ padding: 0.4rem 0.75rem;
263
+ }
264
+
265
+ .vfe-batch-input-group input {
266
+ width: 48px;
267
+ background: transparent;
268
+ border: none;
269
+ color: var(--vfe-glass-btn-text);
270
+ font-size: 0.95rem;
271
+ font-weight: 800;
272
+ text-align: center;
273
+ outline: none;
274
+ }
275
+
276
+ .vfe-gallery-minimal {
277
+ background: var(--vfe-bg);
278
+ border-top: 1px solid var(--vfe-border);
279
+ }
280
+
281
+ .vfe-gallery-header {
282
+ display: flex;
283
+ justify-content: space-between;
284
+ align-items: center;
285
+ padding: 1rem 1.25rem 0.75rem;
286
+ }
287
+
288
+ .vfe-gallery-header h4 {
289
+ font-size: 0.7rem;
290
+ font-weight: 800;
291
+ text-transform: uppercase;
292
+ letter-spacing: 0.1em;
293
+ color: var(--vfe-text-muted);
294
+ margin: 0;
295
+ }
296
+
297
+ .vfe-gallery-minimal .vfe-btn-control {
298
+ background: var(--vfe-bg-muted);
299
+ color: var(--vfe-text);
300
+ border-color: var(--vfe-border);
301
+ flex: none;
302
+ }
303
+
304
+ .vfe-gallery-minimal .vfe-btn-control:hover {
305
+ border-color: var(--vfe-primary);
306
+ color: var(--vfe-primary);
307
+ }
308
+
309
+ .vfe-frame-footer .vfe-btn-control {
310
+ background: var(--vfe-bg-muted);
311
+ color: var(--vfe-text-muted);
312
+ border-color: var(--vfe-border);
313
+ flex: none;
314
+ padding: 0.25rem 0.4rem;
315
+ }
316
+
317
+ .vfe-btn-sm {
318
+ padding: 0.35rem 0.625rem;
319
+ font-size: 0.75rem;
320
+ }
321
+
322
+ .vfe-frames-scroll {
323
+ display: flex;
324
+ gap: 0.75rem;
325
+ overflow-x: auto;
326
+ padding: 0 1.25rem 1.25rem;
327
+ scrollbar-width: thin;
328
+ }
329
+
330
+ .vfe-gallery-empty-text {
331
+ padding: 1.5rem;
332
+ text-align: center;
333
+ color: var(--vfe-text-muted);
334
+ font-size: 0.85rem;
335
+ width: 100%;
336
+ margin: 0;
337
+ }
338
+
339
+ .vfe-frame-card {
340
+ flex-shrink: 0;
341
+ width: 160px;
342
+ background: var(--vfe-bg-muted);
343
+ border: 1px solid var(--vfe-border);
344
+ border-radius: 0.75rem;
345
+ overflow: hidden;
346
+ transition: border-color 0.15s;
347
+ }
348
+
349
+ .vfe-frame-card:hover {
350
+ border-color: var(--vfe-primary);
351
+ }
352
+
353
+ .vfe-frame-thumb {
354
+ width: 100%;
355
+ aspect-ratio: 16/9;
356
+ object-fit: cover;
357
+ display: block;
358
+ cursor: zoom-in;
359
+ }
360
+
361
+ .vfe-frame-footer {
362
+ display: flex;
363
+ justify-content: space-between;
364
+ align-items: center;
365
+ padding: 0.4rem 0.625rem;
366
+ }
367
+
368
+ .vfe-frame-time {
369
+ font-size: 0.7rem;
370
+ font-weight: 800;
371
+ color: var(--vfe-primary);
372
+ }
373
+
374
+ .vfe-lightbox {
375
+ position: fixed;
376
+ inset: 0;
377
+ z-index: 9999;
378
+ background: rgba(0, 0, 0, 0.95);
379
+ backdrop-filter: blur(20px);
380
+ display: none;
381
+ align-items: center;
382
+ justify-content: center;
383
+ }
384
+
385
+ .vfe-lightbox-open {
386
+ display: flex;
387
+ }
388
+
389
+ .vfe-lightbox-content {
390
+ position: relative;
391
+ display: flex;
392
+ flex-direction: column;
393
+ align-items: center;
394
+ gap: 1.5rem;
395
+ max-width: 90vw;
396
+ }
397
+
398
+ .vfe-lightbox-close {
399
+ position: absolute;
400
+ top: -3rem;
401
+ right: 0;
402
+ font-size: 2.5rem;
403
+ color: rgba(255, 255, 255, 0.6);
404
+ cursor: pointer;
405
+ line-height: 1;
406
+ }
407
+
408
+ .vfe-lightbox-close:hover {
409
+ color: #fff;
410
+ }
411
+
412
+ .vfe-lightbox-img {
413
+ max-width: 100%;
414
+ max-height: 70vh;
415
+ border-radius: 0.75rem;
416
+ box-shadow: 0 32px 80px rgba(0, 0, 0, 1);
417
+ }
418
+
419
+ .vfe-lightbox .vfe-btn-capture {
420
+ padding: 0.875rem 2rem;
421
+ font-size: 0.95rem;
422
+ }
423
+
424
+ .vfe-hidden {
425
+ display: none;
426
+ }
@@ -0,0 +1,179 @@
1
+ import { captureFrameFromVideo, captureFrameAtTime, formatTime, type CapturedFrame } from './logic';
2
+ import type { VideoFrameExtractorUI } from './index';
3
+
4
+ export function initVideoFrameExtractor() {
5
+ const root = document.getElementById('video-extractor-root');
6
+ if (!root) return;
7
+
8
+ const labels = JSON.parse(root.dataset.ui || '{}') as VideoFrameExtractorUI;
9
+
10
+ const videoInput = root.querySelector('#video-input') as HTMLInputElement;
11
+ const uploadSection = root.querySelector('#upload-section') as HTMLElement;
12
+ const playerArea = root.querySelector('#player-area') as HTMLElement;
13
+ const video = root.querySelector('#mainVideo') as HTMLVideoElement;
14
+
15
+ const playBtn = root.querySelector('#play-btn') as HTMLElement;
16
+ const playLabel = root.querySelector('#play-label') as HTMLElement;
17
+ const playIcon = root.querySelector('#play-icon') as HTMLElement;
18
+
19
+ const captureBtn = root.querySelector('#capture-btn') as HTMLElement;
20
+ const nextFrameBtn = root.querySelector('#next-frame-btn') as HTMLElement;
21
+ const prevFrameBtn = root.querySelector('#prev-frame-btn') as HTMLElement;
22
+
23
+ const scrubber = root.querySelector('#scrubber') as HTMLInputElement;
24
+ const timeNow = root.querySelector('#time-now') as HTMLElement;
25
+ const timeTotal = root.querySelector('#time-total') as HTMLElement;
26
+
27
+ const batchInterval = root.querySelector('#batch-interval') as HTMLInputElement;
28
+ const batchStartBtn = root.querySelector('#batch-start-btn') as HTMLButtonElement;
29
+ const batchLabel = root.querySelector('#batch-label') as HTMLElement;
30
+
31
+ const framesGrid = root.querySelector('#frames-grid') as HTMLElement;
32
+ const galleryEmpty = root.querySelector('#gallery-empty') as HTMLElement;
33
+ const downloadAllBtn = root.querySelector('#download-all-btn') as HTMLElement;
34
+ const resetBtn = root.querySelector('#reset-btn') as HTMLElement;
35
+
36
+ const lightbox = document.getElementById('lightbox') as HTMLElement;
37
+ const lightboxImg = document.getElementById('lightbox-img') as HTMLImageElement;
38
+ const lightboxDown = document.getElementById('lightbox-down') as HTMLAnchorElement;
39
+
40
+ let captured: CapturedFrame[] = [];
41
+ const FRAME_STEP = 1 / 24;
42
+
43
+ const updateControls = () => {
44
+ timeNow.textContent = formatTime(video.currentTime);
45
+ scrubber.value = video.currentTime.toString();
46
+ };
47
+
48
+ const handleVideoSelect = (file: File) => {
49
+ const url = URL.createObjectURL(file);
50
+ video.src = url;
51
+ uploadSection.classList.add('hidden');
52
+ playerArea.classList.remove('hidden');
53
+ video.load();
54
+ };
55
+
56
+ const addFrameToUI = (frame: CapturedFrame) => {
57
+ captured.push(frame);
58
+ galleryEmpty.classList.add('hidden');
59
+ downloadAllBtn.classList.remove('hidden');
60
+
61
+ const card = document.createElement('div');
62
+ card.className = "frame-card";
63
+ card.innerHTML = `
64
+ <img src="${frame.url}" class="frame-thumb" alt="Frame ${frame.timestamp}" />
65
+ <button class="del-btn-mini" data-id="${frame.id}">×</button>
66
+ <div class="frame-info">
67
+ <span class="frame-time">${formatTime(frame.timestamp)}</span>
68
+ <a href="${frame.url}" download="frame_${frame.timestamp.toFixed(2)}.webp" class="frame-download">
69
+ <svg style="width: 1.25rem; height: 1.25rem;" viewBox="0 0 24 24"><path fill="currentColor" d="M19,3H5V5H19V3M19,19H5V21H19V19M12,17L18,11H14V7H10V11H6L12,17Z" /></svg>
70
+ </a>
71
+ </div>
72
+ `;
73
+
74
+ (card.querySelector('.frame-thumb') as HTMLElement).onclick = () => {
75
+ lightboxImg.src = frame.url;
76
+ lightboxDown.href = frame.url;
77
+ lightbox.classList.add('active');
78
+ };
79
+
80
+ const delBtn = card.querySelector('.del-btn-mini') as HTMLElement;
81
+ delBtn.onclick = (e) => {
82
+ e.stopPropagation();
83
+ captured = captured.filter(f => f.id !== delBtn.dataset.id);
84
+ card.remove();
85
+ if (captured.length === 0) {
86
+ galleryEmpty.classList.remove('hidden');
87
+ downloadAllBtn.classList.add('hidden');
88
+ }
89
+ };
90
+
91
+ framesGrid.prepend(card);
92
+ };
93
+
94
+ video.onloadedmetadata = () => {
95
+ timeTotal.textContent = formatTime(video.duration);
96
+ scrubber.max = video.duration.toString();
97
+ };
98
+
99
+ video.ontimeupdate = updateControls;
100
+
101
+ scrubber.oninput = () => {
102
+ video.currentTime = parseFloat(scrubber.value);
103
+ };
104
+
105
+ playBtn.onclick = () => {
106
+ if (video.paused) {
107
+ video.play();
108
+ playIcon.classList.add('hidden');
109
+ playLabel.textContent = labels.pauseLabel;
110
+ } else {
111
+ video.pause();
112
+ playIcon.classList.remove('hidden');
113
+ playLabel.textContent = labels.playLabel;
114
+ }
115
+ };
116
+
117
+ captureBtn.onclick = () => {
118
+ const frame = captureFrameFromVideo(video);
119
+ if (frame) addFrameToUI(frame);
120
+ };
121
+
122
+ nextFrameBtn.onclick = () => {
123
+ video.pause();
124
+ video.currentTime = Math.min(video.duration, video.currentTime + FRAME_STEP);
125
+ };
126
+
127
+ prevFrameBtn.onclick = () => {
128
+ video.pause();
129
+ video.currentTime = Math.max(0, video.currentTime - FRAME_STEP);
130
+ };
131
+
132
+ batchStartBtn.onclick = async () => {
133
+ const interval = parseFloat(batchInterval.value);
134
+ if (isNaN(interval) || interval <= 0) return;
135
+
136
+ batchStartBtn.disabled = true;
137
+ const originalText = batchLabel.textContent;
138
+ batchLabel.textContent = labels.batchProcessing;
139
+
140
+ const duration = video.duration;
141
+ for (let t = 0; t <= duration; t += interval) {
142
+ const frame = await captureFrameAtTime(video, t);
143
+ if (frame) addFrameToUI(frame);
144
+ }
145
+
146
+ batchStartBtn.disabled = false;
147
+ batchLabel.textContent = originalText;
148
+ };
149
+
150
+ downloadAllBtn.onclick = () => {
151
+ captured.forEach((frame, i) => {
152
+ setTimeout(() => {
153
+ const link = document.createElement('a');
154
+ link.href = frame.url;
155
+ link.download = `frame_${i}.webp`;
156
+ link.click();
157
+ }, i * 200);
158
+ });
159
+ };
160
+
161
+ resetBtn.onclick = () => {
162
+ video.pause();
163
+ video.src = "";
164
+ playerArea.classList.add('hidden');
165
+ uploadSection.classList.remove('hidden');
166
+ captured = [];
167
+ framesGrid.innerHTML = "";
168
+ galleryEmpty.classList.remove('hidden');
169
+ downloadAllBtn.classList.add('hidden');
170
+ };
171
+
172
+ uploadSection.onclick = () => videoInput.click();
173
+ videoInput.onchange = () => {
174
+ if (videoInput.files?.[0]) handleVideoSelect(videoInput.files[0]);
175
+ };
176
+
177
+ (root.querySelector('.lightbox-close') as HTMLElement).onclick = () => lightbox.classList.remove('active');
178
+ lightbox.onclick = (e) => { if (e.target === lightbox) lightbox.classList.remove('active'); };
179
+ }
package/src/tools.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { ToolDefinition } from './types';
2
+ import { TIMELAPSE_CALCULATOR_TOOL } from './tool/timelapseCalculator';
3
+ import { EXIF_CLEANER_TOOL } from './tool/exifCleaner';
4
+ import { SUBTITLE_SYNC_TOOL } from './tool/subtitleSync';
5
+ import { PRIVACY_BLUR_TOOL } from './tool/privacyBlur';
6
+ import { CHROMATIC_LENS_TOOL } from './tool/chromaticLens';
7
+ import { PRINT_QUALITY_CALCULATOR_TOOL } from './tool/printQualityCalculator';
8
+ import { TV_DISTANCE_TOOL } from './tool/tvDistance';
9
+ import { IMAGE_COMPRESSOR_TOOL } from './tool/imageCompressor';
10
+ import { COLLAGE_MAKER_TOOL } from './tool/collageMaker';
11
+ import { VIDEO_FRAME_EXTRACTOR_TOOL } from './tool/videoFrameExtractor';
12
+
13
+ export const ALL_TOOLS: ToolDefinition[] = [
14
+ TIMELAPSE_CALCULATOR_TOOL,
15
+ EXIF_CLEANER_TOOL,
16
+ SUBTITLE_SYNC_TOOL,
17
+ PRIVACY_BLUR_TOOL,
18
+ CHROMATIC_LENS_TOOL,
19
+ PRINT_QUALITY_CALCULATOR_TOOL,
20
+ TV_DISTANCE_TOOL,
21
+ IMAGE_COMPRESSOR_TOOL,
22
+ COLLAGE_MAKER_TOOL,
23
+ VIDEO_FRAME_EXTRACTOR_TOOL,
24
+ ];
25
+
package/src/types.ts ADDED
@@ -0,0 +1,72 @@
1
+ import type { SEOSection } from '@jjlmoya/utils-shared';
2
+ import type { WithContext, Thing } from 'schema-dts';
3
+
4
+ export type { SEOSection };
5
+
6
+ export type KnownLocale =
7
+ | 'ar' | 'da' | 'de' | 'en' | 'es' | 'fi'
8
+ | 'fr' | 'it' | 'ja' | 'ko' | 'nb' | 'nl'
9
+ | 'pl' | 'pt' | 'ru' | 'sv' | 'tr' | 'zh';
10
+
11
+ export interface FAQItem {
12
+ question: string;
13
+ answer: string;
14
+ }
15
+
16
+ export interface BibliographyEntry {
17
+ name: string;
18
+ url: string;
19
+ }
20
+
21
+ export interface HowToStep {
22
+ name: string;
23
+ text: string;
24
+ }
25
+
26
+ export interface ToolLocaleContent<TUI extends Record<string, string> = Record<string, string>> {
27
+ slug: string;
28
+ title: string;
29
+ description: string;
30
+ ui: TUI;
31
+ seo: SEOSection[];
32
+ faqTitle?: string;
33
+ faq: FAQItem[];
34
+ bibliographyTitle?: string;
35
+ bibliography: BibliographyEntry[];
36
+ howTo: HowToStep[];
37
+ schemas: WithContext<Thing>[];
38
+ }
39
+
40
+ export interface CategoryLocaleContent {
41
+ slug: string;
42
+ title: string;
43
+ description: string;
44
+ seo: SEOSection[];
45
+ }
46
+
47
+ export type LocaleLoader<T> = () => Promise<T>;
48
+
49
+ export type LocaleMap<T> = Partial<Record<KnownLocale, LocaleLoader<T>>>;
50
+
51
+ export interface AudiovisualToolEntry<TUI extends Record<string, string> = Record<string, string>> {
52
+ id: string;
53
+ icons: {
54
+ bg: string;
55
+ fg: string;
56
+ };
57
+ i18n: LocaleMap<ToolLocaleContent<TUI>>;
58
+ }
59
+
60
+ export interface AudiovisualCategoryEntry {
61
+ icon: string;
62
+ tools: AudiovisualToolEntry[];
63
+ i18n: LocaleMap<CategoryLocaleContent>;
64
+ }
65
+
66
+ export interface ToolDefinition {
67
+ entry: AudiovisualToolEntry;
68
+ Component: unknown;
69
+ SEOComponent: unknown;
70
+ BibliographyComponent: unknown;
71
+ }
72
+