@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,325 @@
1
+ .ss-root {
2
+ --ss-bg: #fff;
3
+ --ss-bg-elevated: #f8fafc;
4
+ --ss-border: #e2e8f0;
5
+ --ss-text: #0f172a;
6
+ --ss-text-muted: #64748b;
7
+ --ss-accent: #6366f1;
8
+ --ss-accent-alpha: rgba(99, 102, 241, 0.1);
9
+ --ss-accent-alpha-hover: rgba(99, 102, 241, 0.03);
10
+ --ss-shadow: rgba(0, 0, 0, 0.1);
11
+
12
+ padding: 2.5rem 1.5rem;
13
+ max-width: 900px;
14
+ margin: 0 auto;
15
+ }
16
+
17
+ .theme-dark .ss-root {
18
+ --ss-bg: #18181b;
19
+ --ss-bg-elevated: #27272a;
20
+ --ss-border: #3f3f46;
21
+ --ss-text: #f4f4f5;
22
+ --ss-text-muted: #71717a;
23
+ --ss-accent: #818cf8;
24
+ --ss-accent-alpha: rgba(129, 140, 248, 0.12);
25
+ --ss-accent-alpha-hover: rgba(129, 140, 248, 0.04);
26
+ --ss-shadow: rgba(0, 0, 0, 0.3);
27
+ }
28
+
29
+ .ss-card {
30
+ background: var(--ss-bg);
31
+ border: 1px solid var(--ss-border);
32
+ border-radius: 2.5rem;
33
+ padding: 3rem;
34
+ box-shadow: 0 45px 110px -30px var(--ss-shadow);
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: 3rem;
38
+ }
39
+
40
+ .ss-drop {
41
+ border: 3px dashed var(--ss-border);
42
+ padding: 3.5rem 2.5rem;
43
+ border-radius: 2.25rem;
44
+ display: flex;
45
+ flex-direction: column;
46
+ align-items: center;
47
+ gap: 1.25rem;
48
+ cursor: pointer;
49
+ background: var(--ss-accent-alpha-hover);
50
+ transition: all 0.2s ease;
51
+ text-align: center;
52
+ }
53
+
54
+ .ss-drop:hover,
55
+ .ss-drop-active {
56
+ border-color: var(--ss-accent);
57
+ background: var(--ss-accent-alpha);
58
+ }
59
+
60
+ .ss-drop-icon {
61
+ width: 4.5rem;
62
+ height: 4.5rem;
63
+ background: var(--ss-accent-alpha);
64
+ border-radius: 1.25rem;
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+ color: var(--ss-accent);
69
+ }
70
+
71
+ .ss-drop-icon svg {
72
+ width: 2.25rem;
73
+ height: 2.25rem;
74
+ }
75
+
76
+ .ss-drop-title {
77
+ font-size: 1.75rem;
78
+ font-weight: 950;
79
+ color: var(--ss-text);
80
+ margin: 0;
81
+ }
82
+
83
+ .ss-drop-sub {
84
+ font-size: 1rem;
85
+ color: var(--ss-text-muted);
86
+ max-width: 320px;
87
+ margin: 0;
88
+ font-weight: 700;
89
+ }
90
+
91
+ .ss-controls {
92
+ display: grid;
93
+ grid-template-columns: 1fr 1fr;
94
+ gap: 3rem;
95
+ align-items: start;
96
+ }
97
+
98
+ @media (max-width: 800px) {
99
+ .ss-controls {
100
+ grid-template-columns: 1fr;
101
+ }
102
+ }
103
+
104
+ .ss-control-group {
105
+ display: flex;
106
+ flex-direction: column;
107
+ gap: 1.5rem;
108
+ }
109
+
110
+ .ss-control-label {
111
+ font-size: 0.8rem;
112
+ font-weight: 900;
113
+ text-transform: uppercase;
114
+ color: var(--ss-text-muted);
115
+ letter-spacing: 0.1em;
116
+ }
117
+
118
+ .ss-offset-wrap {
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 1rem;
122
+ background: var(--ss-bg-elevated);
123
+ border: 1px solid var(--ss-border);
124
+ border-radius: 1.5rem;
125
+ padding: 0.75rem 1.25rem;
126
+ }
127
+
128
+ .ss-offset-btn {
129
+ width: 2.5rem;
130
+ height: 2.5rem;
131
+ background: none;
132
+ border: none;
133
+ color: var(--ss-text);
134
+ cursor: pointer;
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: center;
138
+ transition: color 0.2s;
139
+ flex-shrink: 0;
140
+ }
141
+
142
+ .ss-offset-btn:hover {
143
+ color: var(--ss-accent);
144
+ }
145
+
146
+ .ss-offset-btn svg {
147
+ width: 1.5rem;
148
+ height: 1.5rem;
149
+ }
150
+
151
+ .ss-offset-input {
152
+ flex: 1;
153
+ background: transparent;
154
+ border: none;
155
+ font-size: 1.75rem;
156
+ font-weight: 950;
157
+ color: var(--ss-text);
158
+ outline: none;
159
+ width: 100px;
160
+ text-align: center;
161
+ }
162
+
163
+ .ss-stats {
164
+ display: flex;
165
+ gap: 2rem;
166
+ justify-content: center;
167
+ margin-top: 1rem;
168
+ }
169
+
170
+ .ss-stat {
171
+ display: flex;
172
+ flex-direction: column;
173
+ align-items: center;
174
+ gap: 0.25rem;
175
+ }
176
+
177
+ .ss-stat-value {
178
+ display: block;
179
+ font-weight: 900;
180
+ color: var(--ss-accent);
181
+ font-size: 1.25rem;
182
+ }
183
+
184
+ .ss-stat-label {
185
+ font-size: 0.7rem;
186
+ font-weight: 800;
187
+ text-transform: uppercase;
188
+ color: var(--ss-text-muted);
189
+ letter-spacing: 0.05em;
190
+ }
191
+
192
+ .ss-status-card {
193
+ display: flex;
194
+ flex-direction: column;
195
+ gap: 1.5rem;
196
+ }
197
+
198
+ .ss-file-row {
199
+ display: flex;
200
+ align-items: center;
201
+ gap: 1.5rem;
202
+ }
203
+
204
+ .ss-file-icon {
205
+ width: 3rem;
206
+ height: 3rem;
207
+ color: var(--ss-accent);
208
+ flex-shrink: 0;
209
+ }
210
+
211
+ .ss-file-info {
212
+ display: flex;
213
+ flex-direction: column;
214
+ gap: 0.25rem;
215
+ min-width: 0;
216
+ }
217
+
218
+ .ss-file-name {
219
+ font-weight: 950;
220
+ color: var(--ss-text);
221
+ font-size: 0.95rem;
222
+ white-space: nowrap;
223
+ overflow: hidden;
224
+ text-overflow: ellipsis;
225
+ }
226
+
227
+ .ss-file-times {
228
+ font-size: 0.8rem;
229
+ font-weight: 700;
230
+ color: var(--ss-text-muted);
231
+ }
232
+
233
+ .ss-btn-primary {
234
+ width: 100%;
235
+ padding: 1.25rem;
236
+ background: var(--ss-accent);
237
+ color: #fff;
238
+ border-radius: 1.5rem;
239
+ font-weight: 950;
240
+ font-size: 1.15rem;
241
+ border: none;
242
+ cursor: pointer;
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: center;
246
+ gap: 0.75rem;
247
+ transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s;
248
+ }
249
+
250
+ .ss-btn-primary:hover {
251
+ transform: translateY(-4px) scale(1.02);
252
+ box-shadow: 0 15px 35px -10px var(--ss-accent);
253
+ }
254
+
255
+ .ss-btn-primary svg {
256
+ width: 1.25rem;
257
+ height: 1.25rem;
258
+ }
259
+
260
+ .ss-previews {
261
+ grid-column: 1 / -1;
262
+ display: grid;
263
+ grid-template-columns: 1fr 1fr;
264
+ gap: 2rem;
265
+ }
266
+
267
+ @media (max-width: 800px) {
268
+ .ss-previews {
269
+ grid-column: auto;
270
+ grid-template-columns: 1fr;
271
+ }
272
+ }
273
+
274
+ .ss-preview-box {
275
+ background: var(--ss-bg-elevated);
276
+ padding: 1.5rem;
277
+ border-radius: 1.75rem;
278
+ border: 1px solid var(--ss-border);
279
+ display: flex;
280
+ flex-direction: column;
281
+ gap: 1rem;
282
+ }
283
+
284
+ .ss-preview-label {
285
+ font-size: 0.75rem;
286
+ font-weight: 950;
287
+ text-transform: uppercase;
288
+ color: var(--ss-text-muted);
289
+ margin: 0;
290
+ letter-spacing: 0.05em;
291
+ }
292
+
293
+ .ss-preview-label-modified {
294
+ color: var(--ss-accent);
295
+ }
296
+
297
+ .ss-preview-scroll {
298
+ max-height: 250px;
299
+ overflow-y: auto;
300
+ display: flex;
301
+ flex-direction: column;
302
+ gap: 1rem;
303
+ }
304
+
305
+ .ss-preview-item {
306
+ font-size: 0.85rem;
307
+ line-height: 1.5;
308
+ }
309
+
310
+ .ss-preview-time {
311
+ font-weight: 950;
312
+ color: var(--ss-accent);
313
+ font-size: 0.75rem;
314
+ margin: 0 0 0.25rem;
315
+ }
316
+
317
+ .ss-preview-text {
318
+ font-weight: 700;
319
+ color: var(--ss-text);
320
+ margin: 0;
321
+ }
322
+
323
+ .ss-hidden {
324
+ display: none;
325
+ }
@@ -0,0 +1,152 @@
1
+ import { parseSRT, shiftSubtitles, generateSRT, msToTime, type SrtItem } from './logic';
2
+
3
+ interface Elements {
4
+ dropZone: HTMLElement;
5
+ fileInput: HTMLInputElement;
6
+ controls: HTMLElement;
7
+ offsetInput: HTMLInputElement;
8
+ downloadBtn: HTMLButtonElement;
9
+ fileName: HTMLElement;
10
+ statLines: HTMLElement;
11
+ statFirst: HTMLElement;
12
+ statLast: HTMLElement;
13
+ previewOriginal: HTMLElement;
14
+ previewModified: HTMLElement;
15
+ }
16
+
17
+ function escapeHtml(text: string): string {
18
+ const map: { [key: string]: string } = {
19
+ "&": "&",
20
+ "<": "&lt;",
21
+ ">": "&gt;",
22
+ '"': "&quot;",
23
+ "'": "&#039;",
24
+ };
25
+ return text.replace(/[&<>"']/g, (m) => map[m]);
26
+ }
27
+
28
+ function renderPreview(container: HTMLElement | null, items: SrtItem[]) {
29
+ if (!container) return;
30
+ container.innerHTML = "";
31
+ items.slice(0, 5).forEach((item) => {
32
+ const div = document.createElement("div");
33
+ div.className = "preview-item";
34
+ div.innerHTML = `
35
+ <p class="preview-item-time">${msToTime(item.start)} --> ${msToTime(item.end)}</p>
36
+ <p class="preview-item-text">${escapeHtml(item.text)}</p>
37
+ `;
38
+ container.appendChild(div);
39
+ });
40
+ }
41
+
42
+ function handleDownload(fileName: string, items: SrtItem[]) {
43
+ const content = generateSRT(items);
44
+ const blob = new Blob([content], { type: "text/srt" });
45
+ const url = URL.createObjectURL(blob);
46
+ const a = document.createElement("a");
47
+ a.href = url;
48
+
49
+ const parts = fileName.split(".");
50
+ if (parts.length > 1) {
51
+ const ext = parts.pop();
52
+ a.download = `${parts.join(".")}_fixed.${ext}`;
53
+ } else {
54
+ a.download = `${fileName}_fixed.srt`;
55
+ }
56
+
57
+ a.click();
58
+ setTimeout(() => URL.revokeObjectURL(url), 100);
59
+ }
60
+
61
+ export function initSubtitleSync() {
62
+ const root = document.getElementById('subtitle-sync-root');
63
+ if (!root) return;
64
+
65
+ const els: Elements = {
66
+ dropZone: root.querySelector('#drop-zone') as HTMLElement,
67
+ fileInput: root.querySelector('#file-input') as HTMLInputElement,
68
+ controls: root.querySelector('#controls-section') as HTMLElement,
69
+ offsetInput: root.querySelector('#offset-input') as HTMLInputElement,
70
+ downloadBtn: root.querySelector('#download-btn') as HTMLButtonElement,
71
+ fileName: root.querySelector('#file-name') as HTMLElement,
72
+ statLines: root.querySelector('#stat-lines') as HTMLElement,
73
+ statFirst: root.querySelector('#stat-first') as HTMLElement,
74
+ statLast: root.querySelector('#stat-last') as HTMLElement,
75
+ previewOriginal: root.querySelector('#preview-original') as HTMLElement,
76
+ previewModified: root.querySelector('#preview-modified') as HTMLElement,
77
+ };
78
+
79
+ if (!els.dropZone || !els.fileInput) return;
80
+
81
+ let originalItems: SrtItem[] = [];
82
+ let currentFileName = "subtitles.srt";
83
+
84
+ const updateUI = () => {
85
+ if (!els.offsetInput || !els.statLines) return;
86
+
87
+ const offsetSec = parseFloat(els.offsetInput.value) || 0;
88
+ const offsetMs = offsetSec * 1000;
89
+ const modifiedItems = shiftSubtitles(originalItems, offsetMs);
90
+
91
+ els.statLines.textContent = modifiedItems.length.toString();
92
+ if (modifiedItems.length > 0) {
93
+ if (els.statFirst) els.statFirst.textContent = msToTime(modifiedItems[0].start);
94
+ if (els.statLast) els.statLast.textContent = msToTime(modifiedItems[modifiedItems.length - 1].end);
95
+ }
96
+
97
+ renderPreview(els.previewOriginal, originalItems);
98
+ renderPreview(els.previewModified, modifiedItems);
99
+
100
+ if (els.downloadBtn) {
101
+ els.downloadBtn.onclick = () => handleDownload(currentFileName, modifiedItems);
102
+ }
103
+ };
104
+
105
+ els.dropZone.onclick = (e) => {
106
+ if ((e.target as HTMLElement).id !== 'file-input') els.fileInput.click();
107
+ };
108
+
109
+ els.dropZone.ondragover = (e) => {
110
+ e.preventDefault();
111
+ els.dropZone.classList.add('drop-zone-active');
112
+ };
113
+
114
+ els.dropZone.ondragleave = () => els.dropZone.classList.remove('drop-zone-active');
115
+
116
+ els.dropZone.ondrop = (e: DragEvent) => {
117
+ e.preventDefault();
118
+ els.dropZone.classList.remove('drop-zone-active');
119
+ const file = e.dataTransfer?.files[0];
120
+ if (file) processFile(file);
121
+ };
122
+
123
+ els.fileInput.onchange = () => {
124
+ const file = els.fileInput.files?.[0];
125
+ if (file) processFile(file);
126
+ };
127
+
128
+ if (els.offsetInput) {
129
+ els.offsetInput.oninput = updateUI;
130
+ }
131
+
132
+ const processFile = (file: File) => {
133
+ currentFileName = file.name;
134
+ if (els.fileName) {
135
+ els.fileName.textContent = file.name;
136
+ els.fileName.classList.remove('hidden');
137
+ }
138
+
139
+ const reader = new FileReader();
140
+ reader.onload = (e) => {
141
+ const text = e.target?.result;
142
+ if (typeof text !== 'string') return;
143
+
144
+ originalItems = parseSRT(text);
145
+ if (originalItems.length > 0 && els.controls) {
146
+ els.controls.classList.remove('hidden');
147
+ updateUI();
148
+ }
149
+ };
150
+ reader.readAsText(file);
151
+ };
152
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { TIMELAPSE_CALCULATOR_TOOL } 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 content = await TIMELAPSE_CALCULATOR_TOOL.entry.i18n[locale]?.();
12
+ const { bibliography } = content || {};
13
+ ---
14
+
15
+ {bibliography && <SharedBibliography links={bibliography} />}
@@ -0,0 +1,148 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { TimelapseUI } from './index';
4
+ import './style.css';
5
+
6
+ interface Props {
7
+ ui: TimelapseUI;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ ---
12
+
13
+ <div class="tlc-root">
14
+ <div class="tlc-grid">
15
+
16
+ <div class="tlc-inputs-panel">
17
+ <h3 class="tlc-panel-title">
18
+ <Icon name="mdi:tune-vertical" class="tlc-panel-icon" />
19
+ {ui.paramsTitle}
20
+ </h3>
21
+
22
+ <div class="tlc-fields">
23
+
24
+ <div class="tlc-field-group">
25
+ <label class="tlc-group-label">{ui.eventDuration}</label>
26
+ <div class="tlc-row">
27
+ <div class="tlc-field">
28
+ <label class="tlc-sub-label" for="event-hours">{ui.hours}</label>
29
+ <input type="number" id="event-hours" value="1" min="0" class="tlc-input tlc-input-indigo" />
30
+ </div>
31
+ <div class="tlc-field">
32
+ <label class="tlc-sub-label" for="event-minutes">{ui.minutes}</label>
33
+ <input type="number" id="event-minutes" value="0" min="0" max="59" class="tlc-input tlc-input-indigo" />
34
+ </div>
35
+ </div>
36
+ </div>
37
+
38
+ <div class="tlc-field-group">
39
+ <label class="tlc-group-label">{ui.videoDuration}</label>
40
+ <div class="tlc-row">
41
+ <div class="tlc-field">
42
+ <label class="tlc-sub-label" for="video-seconds">{ui.seconds}</label>
43
+ <input type="number" id="video-seconds" value="10" min="1" class="tlc-input tlc-input-pink" />
44
+ </div>
45
+ <div class="tlc-field">
46
+ <label class="tlc-sub-label" for="fps-select">{ui.fps}</label>
47
+ <div class="tlc-select-wrapper">
48
+ <select id="fps-select" class="tlc-input tlc-input-pink tlc-select">
49
+ <option value="24">24</option>
50
+ <option value="25">25</option>
51
+ <option value="30">30</option>
52
+ <option value="60">60</option>
53
+ </select>
54
+ <Icon name="mdi:chevron-down" class="tlc-select-arrow" />
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ </div>
61
+ </div>
62
+
63
+ <div class="tlc-results-panel">
64
+ <h3 class="tlc-results-title">
65
+ <Icon name="mdi:equal-box" class="tlc-results-icon" />
66
+ {ui.resultsTitle}
67
+ </h3>
68
+
69
+ <div class="tlc-interval-section">
70
+ <p class="tlc-interval-label">{ui.intervalLabel}</p>
71
+ <div class="tlc-interval-value">
72
+ <span id="result-interval" class="tlc-big-number">15.0</span>
73
+ <span class="tlc-big-unit">{ui.secondsUnit}</span>
74
+ </div>
75
+ </div>
76
+
77
+ <div class="tlc-stats-grid">
78
+ <div class="tlc-stat">
79
+ <p class="tlc-stat-label">{ui.totalPhotos}</p>
80
+ <p id="result-photos" class="tlc-stat-value">240</p>
81
+ </div>
82
+ <div class="tlc-stat">
83
+ <p class="tlc-stat-label">{ui.speed}</p>
84
+ <p id="result-speed" class="tlc-stat-value">360x</p>
85
+ </div>
86
+ <div class="tlc-stat">
87
+ <p class="tlc-stat-label">{ui.shutterSpeed}</p>
88
+ <p id="result-shutter" class="tlc-stat-value tlc-stat-value-sm">~7.5s (180°)</p>
89
+ </div>
90
+ <div class="tlc-stat">
91
+ <p class="tlc-stat-label">{ui.storage}</p>
92
+ <p id="result-storage" class="tlc-stat-value tlc-stat-value-sm">~6 GB</p>
93
+ </div>
94
+ </div>
95
+
96
+ <div class="tlc-rule-info">
97
+ <Icon name="mdi:information-outline" class="tlc-info-icon" />
98
+ <p class="tlc-info-text">{ui.rule180Info}</p>
99
+ </div>
100
+ </div>
101
+
102
+ </div>
103
+ </div>
104
+
105
+ <script>
106
+ import { calculateTimelapse } from './logic';
107
+
108
+ const els = {
109
+ hours: document.getElementById('event-hours') as HTMLInputElement,
110
+ minutes: document.getElementById('event-minutes') as HTMLInputElement,
111
+ seconds: document.getElementById('video-seconds') as HTMLInputElement,
112
+ fps: document.getElementById('fps-select') as HTMLSelectElement,
113
+ interval: document.getElementById('result-interval')!,
114
+ photos: document.getElementById('result-photos')!,
115
+ storage: document.getElementById('result-storage')!,
116
+ speed: document.getElementById('result-speed')!,
117
+ shutter: document.getElementById('result-shutter')!,
118
+ };
119
+
120
+ function formatInterval(interval: number): string {
121
+ return interval < 1 ? interval.toFixed(2) : interval.toFixed(1);
122
+ }
123
+
124
+ function formatShutter(shutterSpeed: number): string {
125
+ return shutterSpeed < 1 ? `1/${Math.round(1 / shutterSpeed)}s` : `${shutterSpeed.toFixed(1)}s`;
126
+ }
127
+
128
+ function updateResultDisplay(r: ReturnType<typeof calculateTimelapse>) {
129
+ els.interval.innerText = formatInterval(r.interval);
130
+ els.photos.innerText = r.totalFrames.toLocaleString('es-ES');
131
+ els.storage.innerText = `~${r.storageGB.toFixed(1)} GB`;
132
+ els.speed.innerText = `${r.speedFactor.toFixed(0)}x`;
133
+ els.shutter.innerText = formatShutter(r.shutterSpeed);
134
+ }
135
+
136
+ // eslint-disable-next-line complexity
137
+ function recalculate() {
138
+ const h = +(els.hours?.value || 0), m = +(els.minutes?.value || 0), s = +(els.seconds?.value || 0), f = +(els.fps?.value || 24);
139
+ updateResultDisplay(calculateTimelapse(h * 3600 + m * 60, s, f));
140
+ }
141
+
142
+ [els.hours, els.minutes, els.seconds, els.fps].forEach(el => {
143
+ el?.addEventListener('input', recalculate);
144
+ el?.addEventListener('change', recalculate);
145
+ });
146
+
147
+ recalculate();
148
+ </script>