@jjlmoya/utils-audiovisual 1.18.0 → 1.20.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.
- package/package.json +1 -1
- package/src/category/i18n/de.ts +1 -1
- package/src/category/i18n/fr.ts +1 -1
- package/src/category/i18n/ru.ts +1 -1
- package/src/category/index.ts +2 -0
- package/src/entries.ts +4 -1
- package/src/index.ts +1 -0
- package/src/tests/diacritics_density.test.ts +118 -0
- package/src/tests/inverted_punctuation.test.ts +84 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/no_en_dash.test.ts +70 -0
- package/src/tests/script_density.test.ts +94 -0
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/chromaticLens/i18n/de.ts +6 -6
- package/src/tool/chromaticLens/i18n/fr.ts +3 -3
- package/src/tool/chromaticLens/i18n/pl.ts +1 -1
- package/src/tool/chromaticLens/i18n/ru.ts +10 -10
- package/src/tool/chromaticLens/i18n/zh.ts +2 -2
- package/src/tool/collageMaker/i18n/de.ts +6 -6
- package/src/tool/collageMaker/i18n/fr.ts +4 -4
- package/src/tool/collageMaker/i18n/pl.ts +5 -5
- package/src/tool/collageMaker/i18n/ru.ts +12 -12
- package/src/tool/collageMaker/i18n/sv.ts +3 -3
- package/src/tool/collageMaker/i18n/zh.ts +1 -1
- package/src/tool/depthOfFieldCalculator/i18n/de.ts +3 -3
- package/src/tool/depthOfFieldCalculator/i18n/en.ts +7 -7
- package/src/tool/depthOfFieldCalculator/i18n/es.ts +2 -2
- package/src/tool/depthOfFieldCalculator/i18n/fr.ts +6 -6
- package/src/tool/depthOfFieldCalculator/i18n/id.ts +2 -2
- package/src/tool/depthOfFieldCalculator/i18n/it.ts +2 -2
- package/src/tool/depthOfFieldCalculator/i18n/ja.ts +1 -1
- package/src/tool/depthOfFieldCalculator/i18n/ko.ts +1 -1
- package/src/tool/depthOfFieldCalculator/i18n/nl.ts +2 -2
- package/src/tool/depthOfFieldCalculator/i18n/pl.ts +2 -2
- package/src/tool/depthOfFieldCalculator/i18n/pt.ts +2 -2
- package/src/tool/depthOfFieldCalculator/i18n/ru.ts +6 -6
- package/src/tool/depthOfFieldCalculator/i18n/sv.ts +2 -2
- package/src/tool/depthOfFieldCalculator/i18n/tr.ts +1 -1
- package/src/tool/depthOfFieldCalculator/i18n/zh.ts +3 -3
- package/src/tool/exifCleaner/i18n/de.ts +8 -8
- package/src/tool/exifCleaner/i18n/fr.ts +11 -11
- package/src/tool/exifCleaner/i18n/pl.ts +6 -6
- package/src/tool/exifCleaner/i18n/ru.ts +14 -14
- package/src/tool/exifCleaner/i18n/zh.ts +3 -3
- package/src/tool/imageCompressor/i18n/de.ts +16 -16
- package/src/tool/imageCompressor/i18n/fr.ts +8 -8
- package/src/tool/imageCompressor/i18n/pl.ts +1 -1
- package/src/tool/imageCompressor/i18n/ru.ts +9 -9
- package/src/tool/imageCompressor/i18n/zh.ts +1 -1
- package/src/tool/printQualityCalculator/component.astro +118 -110
- package/src/tool/printQualityCalculator/i18n/de.ts +5 -5
- package/src/tool/printQualityCalculator/i18n/fr.ts +11 -11
- package/src/tool/printQualityCalculator/i18n/pl.ts +4 -4
- package/src/tool/printQualityCalculator/i18n/ru.ts +11 -11
- package/src/tool/printQualityCalculator/i18n/zh.ts +4 -4
- package/src/tool/printQualityCalculator/print-quality-calculator-pixels-to-cm-dpi.css +193 -40
- package/src/tool/privacyBlur/i18n/de.ts +5 -5
- package/src/tool/privacyBlur/i18n/fr.ts +9 -9
- package/src/tool/privacyBlur/i18n/ru.ts +6 -6
- package/src/tool/subtitleSync/i18n/de.ts +1 -1
- package/src/tool/subtitleSync/i18n/fr.ts +9 -9
- package/src/tool/subtitleSync/i18n/ru.ts +6 -6
- package/src/tool/subtitleSync/i18n/sv.ts +4 -4
- package/src/tool/timelapseCalculator/i18n/fr.ts +6 -6
- package/src/tool/timelapseCalculator/i18n/ru.ts +3 -3
- package/src/tool/timelapseCalculator/i18n/zh.ts +4 -4
- package/src/tool/tvDistance/i18n/fr.ts +2 -2
- package/src/tool/tvDistance/i18n/ru.ts +9 -9
- package/src/tool/tvDistance/i18n/zh.ts +4 -4
- package/src/tool/videoFrameExtractor/i18n/fr.ts +8 -8
- package/src/tool/videoFrameExtractor/i18n/ru.ts +2 -2
- package/src/tool/videoMerger/bibliography.astro +17 -0
- package/src/tool/videoMerger/bibliography.ts +16 -0
- package/src/tool/videoMerger/component.astro +400 -0
- package/src/tool/videoMerger/entry.ts +51 -0
- package/src/tool/videoMerger/i18n/de.ts +205 -0
- package/src/tool/videoMerger/i18n/en.ts +205 -0
- package/src/tool/videoMerger/i18n/es.ts +205 -0
- package/src/tool/videoMerger/i18n/fr.ts +205 -0
- package/src/tool/videoMerger/i18n/id.ts +205 -0
- package/src/tool/videoMerger/i18n/it.ts +205 -0
- package/src/tool/videoMerger/i18n/ja.ts +205 -0
- package/src/tool/videoMerger/i18n/ko.ts +205 -0
- package/src/tool/videoMerger/i18n/nl.ts +205 -0
- package/src/tool/videoMerger/i18n/pl.ts +205 -0
- package/src/tool/videoMerger/i18n/pt.ts +205 -0
- package/src/tool/videoMerger/i18n/ru.ts +205 -0
- package/src/tool/videoMerger/i18n/sv.ts +205 -0
- package/src/tool/videoMerger/i18n/tr.ts +205 -0
- package/src/tool/videoMerger/i18n/zh.ts +205 -0
- package/src/tool/videoMerger/index.ts +11 -0
- package/src/tool/videoMerger/logic.ts +263 -0
- package/src/tool/videoMerger/online-video-merger.css +440 -0
- package/src/tool/videoMerger/seo.astro +15 -0
- package/src/tools.ts +2 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { VideoMergerUI } from './index';
|
|
3
|
+
import './online-video-merger.css';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: VideoMergerUI;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="vm-root" id="vm-root" data-ui={JSON.stringify(ui)}>
|
|
13
|
+
<div class="vm-premium-card">
|
|
14
|
+
|
|
15
|
+
<div id="vm-upload-box" class="vm-uploader-box">
|
|
16
|
+
<input type="file" id="vm-file-input" accept="video/*" multiple class="vm-hidden" />
|
|
17
|
+
<div class="vm-uploader-icon">
|
|
18
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
19
|
+
<path d="M17 10.5V7a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3.5l4 4v-11zM14 13h-3v3H9v-3H6v-2h3V8h2v3h3z"/>
|
|
20
|
+
</svg>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="vm-uploader-text">
|
|
23
|
+
<h3>{ui.uploadTitle}</h3>
|
|
24
|
+
<p>{ui.uploadFormats}</p>
|
|
25
|
+
</div>
|
|
26
|
+
<span class="vm-privacy-note">{ui.privacyNote}</span>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div id="vm-workspace" class="vm-hidden">
|
|
30
|
+
|
|
31
|
+
<div class="vm-list-section">
|
|
32
|
+
<div class="vm-list-header">
|
|
33
|
+
<h4>{ui.listTitle}</h4>
|
|
34
|
+
<button id="vm-add-more" class="vm-btn-main vm-btn-secondary vm-btn-sm" style="flex:none; width:auto; padding: 0.4rem 0.875rem;">
|
|
35
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
36
|
+
<span>{ui.addMoreBtn}</span>
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div id="vm-list-container" class="vm-list-container">
|
|
41
|
+
<div class="vm-empty-state">{ui.emptyList}</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="vm-controls-glass">
|
|
46
|
+
<h4 class="vm-options-title">{ui.optionsTitle}</h4>
|
|
47
|
+
|
|
48
|
+
<div class="vm-options-grid">
|
|
49
|
+
<div class="vm-select-group">
|
|
50
|
+
<label>{ui.optionResolution}</label>
|
|
51
|
+
<select id="vm-res-select">
|
|
52
|
+
<option value="1920x1080">1920x1080 (Full HD 16:9)</option>
|
|
53
|
+
<option value="1280x720">1280x720 (HD 16:9)</option>
|
|
54
|
+
<option value="3840x2160">3840x2160 (4K UHD 16:9)</option>
|
|
55
|
+
<option value="640x360">640x360 (nHD 16:9)</option>
|
|
56
|
+
<option value="1080x1920">1080x1920 (Vertical 9:16)</option>
|
|
57
|
+
</select>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="vm-select-group">
|
|
61
|
+
<label>{ui.optionFps}</label>
|
|
62
|
+
<select id="vm-fps-select">
|
|
63
|
+
<option value="30">30 FPS</option>
|
|
64
|
+
<option value="24">24 FPS</option>
|
|
65
|
+
<option value="60">60 FPS</option>
|
|
66
|
+
</select>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<p class="vm-quality-note">{ui.optionsQualityNote}</p>
|
|
71
|
+
|
|
72
|
+
<div id="vm-resolution-warning" class="vm-warning-box vm-hidden">
|
|
73
|
+
<svg viewBox="0 0 24 24" fill="currentColor" style="width: 1.25rem; height: 1.25rem; flex-shrink: 0;"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>
|
|
74
|
+
<span id="vm-resolution-warning-text"></span>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div id="vm-progress-wrapper" class="vm-progress-wrapper vm-hidden" style="margin-top: 1.25rem;">
|
|
79
|
+
<div class="vm-progress-header">
|
|
80
|
+
<span id="vm-status-label">{ui.mergingStatus}</span>
|
|
81
|
+
<span id="vm-progress-percent">0%</span>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="vm-progress-bar">
|
|
84
|
+
<div id="vm-progress-fill" class="vm-progress-fill"></div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="vm-actions-row" style="margin-top: 1.5rem;">
|
|
89
|
+
<button id="vm-merge-btn" class="vm-btn-main vm-btn-primary">
|
|
90
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
91
|
+
<span id="vm-merge-text">{ui.mergeBtn}</span>
|
|
92
|
+
</button>
|
|
93
|
+
|
|
94
|
+
<a id="vm-download-btn" href="" download="merged_video.webm" class="vm-btn-main vm-btn-primary vm-hidden" style="background-color: var(--vm-success); box-shadow: 0 4px 14px rgba(16, 185, 129, 0.3);">
|
|
95
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M5 20h14v-2H5m14-9h-4V3H9v6H5l7 7z"/></svg>
|
|
96
|
+
<span>{ui.downloadBtn}</span>
|
|
97
|
+
</a>
|
|
98
|
+
|
|
99
|
+
<button id="vm-reset-btn" class="vm-btn-main vm-btn-secondary" style="flex:none; width:auto;">
|
|
100
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M17.65 6.35A7.96 7.96 0 0 0 12 4a8 8 0 0 0-8 8 8 8 0 0 0 8 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18a6 6 0 0 1-6-6 6 6 0 0 1 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4z"/></svg>
|
|
101
|
+
<span>{ui.resetBtn}</span>
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<script>
|
|
111
|
+
import { VideoMergerEngine, getVideoMetadata, formatTime } from './logic';
|
|
112
|
+
import type { VideoItem } from './logic';
|
|
113
|
+
|
|
114
|
+
function init() {
|
|
115
|
+
const root = document.getElementById('vm-root');
|
|
116
|
+
if (!root) return;
|
|
117
|
+
|
|
118
|
+
const fileInput = root.querySelector('#vm-file-input') as HTMLInputElement;
|
|
119
|
+
const uploadBox = root.querySelector('#vm-upload-box') as HTMLElement;
|
|
120
|
+
const workspace = root.querySelector('#vm-workspace') as HTMLElement;
|
|
121
|
+
const addMoreBtn = root.querySelector('#vm-add-more') as HTMLElement;
|
|
122
|
+
const listContainer = root.querySelector('#vm-list-container') as HTMLElement;
|
|
123
|
+
|
|
124
|
+
const resSelect = root.querySelector('#vm-res-select') as HTMLSelectElement;
|
|
125
|
+
const fpsSelect = root.querySelector('#vm-fps-select') as HTMLSelectElement;
|
|
126
|
+
|
|
127
|
+
const progressWrapper = root.querySelector('#vm-progress-wrapper') as HTMLElement;
|
|
128
|
+
const statusLabel = root.querySelector('#vm-status-label') as HTMLElement;
|
|
129
|
+
const progressPercent = root.querySelector('#vm-progress-percent') as HTMLElement;
|
|
130
|
+
const progressBar = root.querySelector('.vm-progress-bar') as HTMLElement;
|
|
131
|
+
|
|
132
|
+
const mergeBtn = root.querySelector('#vm-merge-btn') as HTMLButtonElement;
|
|
133
|
+
const downloadBtn = root.querySelector('#vm-download-btn') as HTMLAnchorElement;
|
|
134
|
+
const resetBtn = root.querySelector('#vm-reset-btn') as HTMLButtonElement;
|
|
135
|
+
const resolutionWarning = root.querySelector('#vm-resolution-warning') as HTMLElement;
|
|
136
|
+
const resolutionWarningText = root.querySelector('#vm-resolution-warning-text') as HTMLElement;
|
|
137
|
+
|
|
138
|
+
let items: VideoItem[] = [];
|
|
139
|
+
const engine = new VideoMergerEngine();
|
|
140
|
+
|
|
141
|
+
function checkResolutions() {
|
|
142
|
+
const resParts = resSelect.value.split('x');
|
|
143
|
+
const width = parseInt(resParts[0] || '1920');
|
|
144
|
+
const height = parseInt(resParts[1] || '1080');
|
|
145
|
+
|
|
146
|
+
let hasMismatch = false;
|
|
147
|
+
for (const item of items) {
|
|
148
|
+
if (item.width !== width || item.height !== height) {
|
|
149
|
+
hasMismatch = true;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (hasMismatch) {
|
|
155
|
+
resolutionWarning.classList.remove('vm-hidden');
|
|
156
|
+
resolutionWarningText.textContent = root!.dataset.ui ? JSON.parse(root!.dataset.ui).resolutionWarning : 'Advertencia: Resoluciones distintas';
|
|
157
|
+
} else {
|
|
158
|
+
resolutionWarning.classList.add('vm-hidden');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
resSelect.onchange = () => {
|
|
163
|
+
checkResolutions();
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
function renderList() {
|
|
167
|
+
listContainer.innerHTML = '';
|
|
168
|
+
if (items.length === 0) {
|
|
169
|
+
workspace.classList.add('vm-hidden');
|
|
170
|
+
uploadBox.classList.remove('vm-hidden');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
mergeBtn.disabled = items.length < 2;
|
|
175
|
+
checkResolutions();
|
|
176
|
+
|
|
177
|
+
items.forEach((item, index) => {
|
|
178
|
+
const card = document.createElement('div');
|
|
179
|
+
card.className = 'vm-item-card';
|
|
180
|
+
card.innerHTML = `
|
|
181
|
+
<span class="vm-item-index">${index + 1}</span>
|
|
182
|
+
<div class="vm-item-details">
|
|
183
|
+
<h5 class="vm-item-name">${item.name}</h5>
|
|
184
|
+
<div class="vm-item-meta">
|
|
185
|
+
<span>${formatTime(item.duration)}</span>
|
|
186
|
+
<span>${item.width} × ${item.height}</span>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="vm-item-controls">
|
|
190
|
+
<button class="vm-btn-icon vm-move-up" ${index === 0 ? 'disabled' : ''}>
|
|
191
|
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>
|
|
192
|
+
</button>
|
|
193
|
+
<button class="vm-btn-icon vm-move-down" ${index === items.length - 1 ? 'disabled' : ''}>
|
|
194
|
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>
|
|
195
|
+
</button>
|
|
196
|
+
<button class="vm-btn-icon vm-danger vm-delete">
|
|
197
|
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
`;
|
|
201
|
+
|
|
202
|
+
const moveUp = card.querySelector('.vm-move-up') as HTMLElement;
|
|
203
|
+
const moveDown = card.querySelector('.vm-move-down') as HTMLElement;
|
|
204
|
+
const delBtn = card.querySelector('.vm-delete') as HTMLElement;
|
|
205
|
+
|
|
206
|
+
moveUp.onclick = () => {
|
|
207
|
+
if (index > 0) {
|
|
208
|
+
const temp = items[index];
|
|
209
|
+
const prev = items[index - 1];
|
|
210
|
+
if (temp && prev) {
|
|
211
|
+
items[index] = prev;
|
|
212
|
+
items[index - 1] = temp;
|
|
213
|
+
renderList();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
moveDown.onclick = () => {
|
|
219
|
+
if (index < items.length - 1) {
|
|
220
|
+
const temp = items[index];
|
|
221
|
+
const next = items[index + 1];
|
|
222
|
+
if (temp && next) {
|
|
223
|
+
items[index] = next;
|
|
224
|
+
items[index + 1] = temp;
|
|
225
|
+
renderList();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
delBtn.onclick = () => {
|
|
231
|
+
URL.revokeObjectURL(item.url);
|
|
232
|
+
items.splice(index, 1);
|
|
233
|
+
renderList();
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
listContainer.appendChild(card);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function handleFiles(files: FileList) {
|
|
241
|
+
uploadBox.classList.add('vm-hidden');
|
|
242
|
+
workspace.classList.remove('vm-hidden');
|
|
243
|
+
|
|
244
|
+
for (let i = 0; i < files.length; i++) {
|
|
245
|
+
const file = files[i];
|
|
246
|
+
if (!file) continue;
|
|
247
|
+
try {
|
|
248
|
+
const meta = await getVideoMetadata(file);
|
|
249
|
+
items.push({
|
|
250
|
+
id: Math.random().toString(36).substring(2, 9),
|
|
251
|
+
file,
|
|
252
|
+
url: URL.createObjectURL(file),
|
|
253
|
+
name: file.name,
|
|
254
|
+
duration: meta.duration,
|
|
255
|
+
width: meta.width,
|
|
256
|
+
height: meta.height
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (items.length === 1) {
|
|
260
|
+
const value = `${meta.width}x${meta.height}`;
|
|
261
|
+
const exists = Array.from(resSelect.options).some(opt => opt.value === value);
|
|
262
|
+
if (!exists) {
|
|
263
|
+
const opt = document.createElement('option');
|
|
264
|
+
opt.value = value;
|
|
265
|
+
opt.textContent = `${meta.width}×${meta.height} (Original)`;
|
|
266
|
+
resSelect.appendChild(opt);
|
|
267
|
+
}
|
|
268
|
+
resSelect.value = value;
|
|
269
|
+
}
|
|
270
|
+
} catch (e) {
|
|
271
|
+
console.error(e);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
renderList();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
uploadBox.onclick = () => fileInput.click();
|
|
278
|
+
fileInput.onchange = () => {
|
|
279
|
+
if (fileInput.files) {
|
|
280
|
+
handleFiles(fileInput.files);
|
|
281
|
+
fileInput.value = '';
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
addMoreBtn.onclick = () => fileInput.click();
|
|
286
|
+
|
|
287
|
+
root.ondragover = (e) => { e.preventDefault(); uploadBox.classList.add('vm-dragover'); };
|
|
288
|
+
root.ondragleave = () => uploadBox.classList.remove('vm-dragover');
|
|
289
|
+
root.ondrop = (e) => {
|
|
290
|
+
e.preventDefault();
|
|
291
|
+
uploadBox.classList.remove('vm-dragover');
|
|
292
|
+
if (e.dataTransfer?.files) handleFiles(e.dataTransfer.files);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
mergeBtn.onclick = async () => {
|
|
296
|
+
if (items.length < 2) return;
|
|
297
|
+
|
|
298
|
+
const resParts = resSelect.value.split('x');
|
|
299
|
+
const width = parseInt(resParts[0] || '1920');
|
|
300
|
+
const height = parseInt(resParts[1] || '1080');
|
|
301
|
+
const fps = parseInt(fpsSelect.value);
|
|
302
|
+
|
|
303
|
+
mergeBtn.disabled = true;
|
|
304
|
+
resetBtn.disabled = true;
|
|
305
|
+
addMoreBtn.classList.add('vm-hidden');
|
|
306
|
+
|
|
307
|
+
const controlButtons = listContainer.querySelectorAll('.vm-btn-icon') as NodeListOf<HTMLButtonElement>;
|
|
308
|
+
controlButtons.forEach(btn => btn.disabled = true);
|
|
309
|
+
|
|
310
|
+
progressWrapper.classList.remove('vm-hidden');
|
|
311
|
+
downloadBtn.classList.add('vm-hidden');
|
|
312
|
+
|
|
313
|
+
engine.setItems(items);
|
|
314
|
+
|
|
315
|
+
progressBar.innerHTML = '';
|
|
316
|
+
const numVideos = items.length;
|
|
317
|
+
const segmentFills: HTMLElement[] = [];
|
|
318
|
+
for (let k = 0; k < numVideos; k++) {
|
|
319
|
+
const segment = document.createElement('div');
|
|
320
|
+
segment.className = 'vm-progress-segment';
|
|
321
|
+
const fill = document.createElement('div');
|
|
322
|
+
fill.className = 'vm-progress-segment-fill';
|
|
323
|
+
segment.appendChild(fill);
|
|
324
|
+
progressBar.appendChild(segment);
|
|
325
|
+
segmentFills.push(fill);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const blob = await engine.merge({ width, height, fps }, (progress, name, index) => {
|
|
330
|
+
const globalProgress = Math.floor(((index * 100) + progress) / numVideos);
|
|
331
|
+
progressPercent.textContent = `${globalProgress}%`;
|
|
332
|
+
|
|
333
|
+
for (let k = 0; k < numVideos; k++) {
|
|
334
|
+
const fill = segmentFills[k];
|
|
335
|
+
if (fill) {
|
|
336
|
+
if (k < index) {
|
|
337
|
+
fill.style.width = '100%';
|
|
338
|
+
} else if (k === index) {
|
|
339
|
+
fill.style.width = `${progress}%`;
|
|
340
|
+
} else {
|
|
341
|
+
fill.style.width = '0%';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
statusLabel.textContent = `${root.dataset.ui ? JSON.parse(root.dataset.ui).mergingStatus : 'Uniendo...'} (${name})`;
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const resultUrl = URL.createObjectURL(blob);
|
|
350
|
+
|
|
351
|
+
progressPercent.textContent = '100%';
|
|
352
|
+
for (let k = 0; k < numVideos; k++) {
|
|
353
|
+
const fill = segmentFills[k];
|
|
354
|
+
if (fill) fill.style.width = '100%';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
downloadBtn.href = resultUrl;
|
|
358
|
+
downloadBtn.classList.remove('vm-hidden');
|
|
359
|
+
mergeBtn.classList.add('vm-hidden');
|
|
360
|
+
} catch (err) {
|
|
361
|
+
console.error(err);
|
|
362
|
+
} finally {
|
|
363
|
+
resetBtn.disabled = false;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
resetBtn.onclick = () => {
|
|
368
|
+
items.forEach(item => URL.revokeObjectURL(item.url));
|
|
369
|
+
items = [];
|
|
370
|
+
|
|
371
|
+
const downloadUrl = downloadBtn.href;
|
|
372
|
+
if (downloadUrl) URL.revokeObjectURL(downloadUrl);
|
|
373
|
+
|
|
374
|
+
progressWrapper.classList.add('vm-hidden');
|
|
375
|
+
progressPercent.textContent = '0%';
|
|
376
|
+
progressBar.innerHTML = '<div id="vm-progress-fill" class="vm-progress-fill"></div>';
|
|
377
|
+
|
|
378
|
+
mergeBtn.disabled = false;
|
|
379
|
+
mergeBtn.classList.remove('vm-hidden');
|
|
380
|
+
downloadBtn.classList.add('vm-hidden');
|
|
381
|
+
addMoreBtn.classList.remove('vm-hidden');
|
|
382
|
+
|
|
383
|
+
resSelect.value = '1920x1080';
|
|
384
|
+
const defaultValues = ['1920x1080', '1280x720', '3840x2160', '640x360', '1080x1920'];
|
|
385
|
+
for (let i = resSelect.options.length - 1; i >= 0; i--) {
|
|
386
|
+
const val = resSelect.options[i]?.value;
|
|
387
|
+
if (val && !defaultValues.includes(val)) {
|
|
388
|
+
resSelect.remove(i);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
resolutionWarning.classList.add('vm-hidden');
|
|
392
|
+
|
|
393
|
+
fileInput.value = '';
|
|
394
|
+
renderList();
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
init();
|
|
399
|
+
document.addEventListener('astro:page-load', init);
|
|
400
|
+
</script>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AudiovisualToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
|
|
3
|
+
export interface VideoMergerUI {
|
|
4
|
+
uploadTitle: string;
|
|
5
|
+
uploadFormats: string;
|
|
6
|
+
privacyNote: string;
|
|
7
|
+
addMoreBtn: string;
|
|
8
|
+
mergeBtn: string;
|
|
9
|
+
mergingStatus: string;
|
|
10
|
+
downloadBtn: string;
|
|
11
|
+
resetBtn: string;
|
|
12
|
+
emptyList: string;
|
|
13
|
+
listTitle: string;
|
|
14
|
+
optionsTitle: string;
|
|
15
|
+
optionResolution: string;
|
|
16
|
+
optionFps: string;
|
|
17
|
+
optionsQualityNote: string;
|
|
18
|
+
faqTitle: string;
|
|
19
|
+
bibliographyTitle: string;
|
|
20
|
+
resolutionWarning: string;
|
|
21
|
+
[key: string]: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type VideoMergerLocaleContent = ToolLocaleContent<VideoMergerUI>;
|
|
25
|
+
|
|
26
|
+
export const videoMerger: AudiovisualToolEntry<VideoMergerUI> = {
|
|
27
|
+
id: 'online-video-merger',
|
|
28
|
+
icons: {
|
|
29
|
+
bg: 'mdi:video-vintage',
|
|
30
|
+
fg: 'mdi:video-plus',
|
|
31
|
+
},
|
|
32
|
+
i18n: {
|
|
33
|
+
de: async () => (await import('./i18n/de')).content as unknown as VideoMergerLocaleContent,
|
|
34
|
+
en: async () => (await import('./i18n/en')).content as unknown as VideoMergerLocaleContent,
|
|
35
|
+
es: async () => (await import('./i18n/es')).content as unknown as VideoMergerLocaleContent,
|
|
36
|
+
fr: async () => (await import('./i18n/fr')).content as unknown as VideoMergerLocaleContent,
|
|
37
|
+
id: async () => (await import('./i18n/id')).content as unknown as VideoMergerLocaleContent,
|
|
38
|
+
it: async () => (await import('./i18n/it')).content as unknown as VideoMergerLocaleContent,
|
|
39
|
+
ja: async () => (await import('./i18n/ja')).content as unknown as VideoMergerLocaleContent,
|
|
40
|
+
ko: async () => (await import('./i18n/ko')).content as unknown as VideoMergerLocaleContent,
|
|
41
|
+
nl: async () => (await import('./i18n/nl')).content as unknown as VideoMergerLocaleContent,
|
|
42
|
+
pl: async () => (await import('./i18n/pl')).content as unknown as VideoMergerLocaleContent,
|
|
43
|
+
pt: async () => (await import('./i18n/pt')).content as unknown as VideoMergerLocaleContent,
|
|
44
|
+
ru: async () => (await import('./i18n/ru')).content as unknown as VideoMergerLocaleContent,
|
|
45
|
+
sv: async () => (await import('./i18n/sv')).content as unknown as VideoMergerLocaleContent,
|
|
46
|
+
tr: async () => (await import('./i18n/tr')).content as unknown as VideoMergerLocaleContent,
|
|
47
|
+
zh: async () => (await import('./i18n/zh')).content as unknown as VideoMergerLocaleContent,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export { bibliography } from './bibliography';
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
2
|
+
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
|
+
import type { VideoMergerUI, VideoMergerLocaleContent } from '../index';
|
|
4
|
+
|
|
5
|
+
const slug = 'videos-zusammenfuegen-online';
|
|
6
|
+
const title = 'Videos online zusammenfügen: Mehrere Videos schnell & kostenlos';
|
|
7
|
+
const description = 'Fügen Sie mehrere Videos kostenlos, online und lokal zu einem zusammen. Keine Wasserzeichen, keine Uploads, 100% privat in Ihrem Browser.';
|
|
8
|
+
|
|
9
|
+
const ui: VideoMergerUI = {
|
|
10
|
+
uploadTitle: "Laden Sie Ihre Videodateien hoch",
|
|
11
|
+
uploadFormats: "Ziehen Sie mehrere Videos hierher oder klicken Sie zur Auswahl",
|
|
12
|
+
privacyNote: "Ihre Videos werden 100% lokal verarbeitet. Nichts wird ins Internet hochgeladen.",
|
|
13
|
+
addMoreBtn: "Weitere Videos hinzufügen",
|
|
14
|
+
mergeBtn: "Videos jetzt zusammenfügen",
|
|
15
|
+
mergingStatus: "Videos werden zusammengefügt...",
|
|
16
|
+
downloadBtn: "Zusammengefügtes Video herunterladen",
|
|
17
|
+
resetBtn: "Neu beginnen",
|
|
18
|
+
emptyList: "Ziehen Sie Videos hierher oder wählen Sie sie aus, um zu beginnen.",
|
|
19
|
+
listTitle: "Reihenfolge der zusammenzufügenden Videos",
|
|
20
|
+
optionsTitle: "Ausgabeeinstellungen",
|
|
21
|
+
optionResolution: "Auflösung",
|
|
22
|
+
optionFps: "Bilder pro Sekunde (FPS)",
|
|
23
|
+
optionsQualityNote: "Die endgültige Auflösung wird unter Beibehaltung des ursprünglichen Seitenverhältnisses im Letterbox-Format angepasst, falls die Videos unterschiedliche Abmessungen haben.",
|
|
24
|
+
faqTitle: "Häufig gestellte Fragen zum Zusammenfügen von Videos",
|
|
25
|
+
bibliographyTitle: "Bibliografie & Referenzen",
|
|
26
|
+
resolutionWarning: "Achtung: Einige Videos haben unterschiedliche Auflösungen und werden automatisch angepasst."
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const faq: VideoMergerLocaleContent['faq'] = [
|
|
30
|
+
{
|
|
31
|
+
question: "Ist es sicher, meine Videos in dieses Tool hochzuladen?",
|
|
32
|
+
answer: "Ja, es ist absolut sicher. Das Tool arbeitet zu 100% lokal in Ihrem Browser. Ihre Videos werden niemals über das Internet übertragen oder auf einem Server gespeichert.",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
question: "Was passiert, wenn meine Videos unterschiedliche Abmessungen haben?",
|
|
36
|
+
answer: "Unser Tool skaliert die Videos automatisch auf die gewählte Ausgabeauflösung. Bei nicht exakt übereinstimmenden Proportionen werden schwarze Ränder (Letterboxing) hinzugefügt, um das ursprüngliche Seitenverhältnis zu erhalten.",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
question: "Bleibt der Ton der Videos beim Zusammenfügen erhalten?",
|
|
40
|
+
answer: "Ja. Die Tonspuren jedes Videos werden erfasst und sequenziell in perfekter Synchronisation mit jedem Bildsegment gemischt.",
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const howTo: VideoMergerLocaleContent['howTo'] = [
|
|
45
|
+
{
|
|
46
|
+
name: "Videos auswählen oder ziehen",
|
|
47
|
+
text: "Laden Sie alle Videodateien, die Sie zusammenfügen möchten, direkt von Ihrem Computer oder Mobilgerät hoch.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "Reihenfolge festlegen",
|
|
51
|
+
text: "Ordnen Sie die hochgeladenen Videos mithilfe der Auf- und Ab-Schaltflächen in der Liste an, um die Wiedergabereihenfolge zu bestimmen.",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "Optionen anpassen",
|
|
55
|
+
text: "Wählen Sie die Ausgabeauflösung und die Bilder pro Sekunde (FPS) des zusammengefügten Videos.",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "Zusammenfügen und exportieren",
|
|
59
|
+
text: "Drücken Sie die Schaltfläche 'Zusammenfügen'. Warten Sie, bis die Echtzeitverarbeitung abgeschlossen ist, und laden Sie die resultierende Datei herunter.",
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const seo: VideoMergerLocaleContent['seo'] = [
|
|
64
|
+
{
|
|
65
|
+
type: 'summary',
|
|
66
|
+
title: '100% lokales professionelles Zusammenfügen von Videos',
|
|
67
|
+
items: [
|
|
68
|
+
'Echtzeitverarbeitung direkt in Ihrem Browser',
|
|
69
|
+
'Unterstützt mehrere Videos unterschiedlicher Größe und Formate (MP4, WEBM, MOV)',
|
|
70
|
+
'Wählbare Ausgabeauflösung (720p, 1080p, 2K, 4K)',
|
|
71
|
+
'Audiospuren perfekt sequenziell kombiniert'
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
{ type: 'title', text: 'Videos online kostenlos zusammenfügen: Grenzenlose Einfachheit und Privatsphäre', level: 2 },
|
|
75
|
+
{ type: 'paragraph', html: 'Fragen Sie sich, <strong>wie man zwei Videos zu einem zusammenfügt</strong>, ohne Komplikationen? Sie müssen keine schweren Programme oder kostenpflichtige Anwendungen mehr herunterladen. Unser Tool zum <strong>kostenlosen Online-Zusammenfügen von Videos</strong> ermöglicht es Ihnen, in Sekundenschnelle alle benötigten Clips zu verbinden. Da es zu 100% lokal funktioniert, müssen Sie Ihre Dateien auf keinen Server hochladen, was absolute Privatsphäre garantiert und es Ihnen ermöglicht, <strong>große Videos ohne Wartezeiten beim Hochladen zusammenzufügen</strong>.' },
|
|
76
|
+
|
|
77
|
+
{ type: 'title', text: 'Videos ohne Wasserzeichen verbinden', level: 3 },
|
|
78
|
+
{ type: 'paragraph', html: 'Einer der größten Nachteile anderer Anwendungen ist, dass sie Ihre Inhalte ruinieren. Mit uns können Sie <strong>Videos online ohne Wasserzeichen verbinden</strong>. Die heruntergeladene Datei wird genau Ihre Kreation sein - sauber, professionell und bereit zum Teilen auf YouTube, Instagram, TikTok oder für den persönlichen Gebrauch.' },
|
|
79
|
+
|
|
80
|
+
{ type: 'stats', items: [
|
|
81
|
+
{ value: '100%', label: 'Privat und Lokal', icon: 'mdi:shield-check' },
|
|
82
|
+
{ value: '0MB', label: 'Kein Upload', icon: 'mdi:upload-off' },
|
|
83
|
+
{ value: '4K', label: 'Maximale Auflösung', icon: 'mdi:video-high-definition' }
|
|
84
|
+
], columns: 3 },
|
|
85
|
+
|
|
86
|
+
{ type: 'title', text: 'Häufige Anwendungsfälle zum Zusammenfügen von Videos', level: 3 },
|
|
87
|
+
{ type: 'comparative', items: [
|
|
88
|
+
{
|
|
89
|
+
title: 'Social Media',
|
|
90
|
+
description: 'Schnelles Zusammenfügen von Stories, TikToks oder Reels',
|
|
91
|
+
icon: 'mdi:instagram',
|
|
92
|
+
points: [
|
|
93
|
+
'Verbinden Sie kleine, mit dem Handy aufgenommene Clips',
|
|
94
|
+
'Bereiten Sie sequenzielle Inhalte für Instagram oder YouTube vor',
|
|
95
|
+
'Zusammenfügen ohne störende Wasserzeichen'
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
title: 'Präsentationen',
|
|
100
|
+
description: 'Verbinden Sie Einführungen und Demo-Aufnahmen in einer Datei',
|
|
101
|
+
icon: 'mdi:presentation',
|
|
102
|
+
points: [
|
|
103
|
+
'Fügen Sie ein animiertes Intro mit dem Hauptteil zusammen',
|
|
104
|
+
'Verbinden Sie kurze Software-Demos',
|
|
105
|
+
'Sauberer Export im Standard-MP4/WEBM-Format'
|
|
106
|
+
],
|
|
107
|
+
highlight: true
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
title: 'Familienkompilationen',
|
|
111
|
+
description: 'Fügen Sie mehrere Videos von Urlauben oder Feiern zusammen',
|
|
112
|
+
icon: 'mdi:home-heart',
|
|
113
|
+
points: [
|
|
114
|
+
'Erstellen Sie ein einzelnes Video mit allen Party-Momenten',
|
|
115
|
+
'Gruppieren Sie Reiseerinnerungen chronologisch',
|
|
116
|
+
'Einfach zu teilen als eine einzige Datei'
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
title: 'Lernvideos',
|
|
121
|
+
description: 'Verbinden Sie kurze Trainingskapitel oder Lektionen',
|
|
122
|
+
icon: 'mdi:school',
|
|
123
|
+
points: [
|
|
124
|
+
'Gruppieren Sie kleine, unabhängige Tutorials',
|
|
125
|
+
'Fügen Sie Ihren Lektionen einen Schlussclip hinzu',
|
|
126
|
+
'Strukturieren Sie Ihren Kurs professionell'
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
], columns: 2 },
|
|
130
|
+
|
|
131
|
+
{ type: 'title', text: 'Die beste App zum Zusammenfügen von Videos im Browser', level: 2 },
|
|
132
|
+
{ type: 'paragraph', html: 'Dies ist nicht nur eine weitere Webseite; es ist eine echte <strong>App zum Zusammenfügen von Videos</strong>, die dank moderner HTML5-Video-APIs direkt in Ihrem Browser arbeitet. Sie können <strong>MP4-Videos zusammenfügen</strong>, WEBM und mehr, Auflösungen mischen (unser System wendet automatisch Letterboxing an, wenn die Abmessungen variieren) und die Bilder pro Sekunde (FPS) Ihres finalen Exports wählen.' },
|
|
133
|
+
|
|
134
|
+
{ type: 'title', text: 'Vergleich der Zusammenfügungsansätze', level: 3 },
|
|
135
|
+
{ type: 'table', headers: ['Funktionen', 'Unser lokales Tool', 'Klassische Online-Konverter', 'Professionelle Editoren'], rows: [
|
|
136
|
+
['Privatsphäre', 'Vollständig (auf Ihrem Gerät)', 'Gering (Dateien müssen hochgeladen werden)', 'Vollständig (installiert)'],
|
|
137
|
+
['Netzwerkverbrauch', 'Null (kein Upload)', 'Sehr hoch (Up- und Download)', 'Null'],
|
|
138
|
+
['Wasserzeichen', 'NEIN (100% sauber)', 'Ja (in kostenlosen Versionen)', 'NEIN (bei gekaufter Lizenz)'],
|
|
139
|
+
['Preis', '100% Kostenlos', 'Kostenlos mit Limits oder Abo', 'Meist teuer'],
|
|
140
|
+
['Lernkurve', 'Sehr flach (Ziehen, ordnen, zusammenfügen)', 'Flach', 'Sehr steil (erfordert Training)']
|
|
141
|
+
] },
|
|
142
|
+
|
|
143
|
+
{ type: 'proscons', items: [
|
|
144
|
+
{
|
|
145
|
+
pro: 'Garantierte Privatsphäre: Keine Datei verlässt Ihr Gerät',
|
|
146
|
+
con: 'Die Geschwindigkeit beim Zusammenfügen großer Videos hängt vom RAM und Prozessor Ihres Geräts ab'
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
pro: 'Professionelle Ergebnisse: 100% kostenlos, keine Registrierung, keine Wasserzeichen',
|
|
150
|
+
con: 'Beim Verbinden von Videos mit unterschiedlichen Abmessungen werden schwarze Ränder (Letterboxing) angewendet'
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
pro: 'Vielseitigkeit: Schneller Export mit effizienter Kodierung für Web und Social Media',
|
|
154
|
+
con: 'Erlaubt keine komplexen 3D-Übergänge oder kinoreife visuelle Effekte zwischen Clips'
|
|
155
|
+
}
|
|
156
|
+
], title: 'Vorteile und Überlegungen' },
|
|
157
|
+
|
|
158
|
+
{ type: 'title', text: 'Beginnen Sie noch heute mit dem Zusammenfügen', level: 2 },
|
|
159
|
+
{ type: 'paragraph', html: 'Es gibt keine Ausreden mehr für hunderte unorganisierte Videofragmente. Laden Sie Ihre Dateien hoch, platzieren Sie sie in der gewünschten Reihenfolge und drücken Sie die Taste. Entdecken Sie den schnellsten, sichersten und privatsten Weg, <strong>Videos online zusammenzufügen</strong>.' }
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
163
|
+
'@context': 'https://schema.org',
|
|
164
|
+
'@type': 'FAQPage',
|
|
165
|
+
mainEntity: faq.map((item) => ({
|
|
166
|
+
'@type': 'Question',
|
|
167
|
+
name: item.question,
|
|
168
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
169
|
+
})),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const howToSchema: WithContext<HowTo> = {
|
|
173
|
+
'@context': 'https://schema.org',
|
|
174
|
+
'@type': 'HowTo',
|
|
175
|
+
name: title,
|
|
176
|
+
description,
|
|
177
|
+
step: howTo.map((step) => ({
|
|
178
|
+
'@type': 'HowToStep',
|
|
179
|
+
name: step.name,
|
|
180
|
+
text: step.text,
|
|
181
|
+
})),
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
185
|
+
'@context': 'https://schema.org',
|
|
186
|
+
'@type': 'SoftwareApplication',
|
|
187
|
+
name: title,
|
|
188
|
+
description,
|
|
189
|
+
applicationCategory: 'UtilitiesApplication',
|
|
190
|
+
operatingSystem: 'Web',
|
|
191
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
|
192
|
+
inLanguage: 'de',
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const content: VideoMergerLocaleContent = {
|
|
196
|
+
slug,
|
|
197
|
+
title,
|
|
198
|
+
description,
|
|
199
|
+
ui,
|
|
200
|
+
seo,
|
|
201
|
+
faq,
|
|
202
|
+
bibliography,
|
|
203
|
+
howTo,
|
|
204
|
+
schemas: [faqSchema as any, howToSchema as any, appSchema],
|
|
205
|
+
};
|