feat: Add scrubber with A/B markers for video editing/downloading
This commit is contained in:
@@ -740,6 +740,7 @@ function openVideoEditor(source) {
|
||||
}
|
||||
|
||||
const cuts = []; // [{start,end}] the user is removing
|
||||
let lastMarker = null; // 'A' or 'B'
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'video-editor';
|
||||
@@ -747,14 +748,17 @@ function openVideoEditor(source) {
|
||||
<p class="ve-intro">Mark the parts to <strong>remove</strong>. Everything else is kept and saved as a new offline video.</p>
|
||||
<div class="ve-scrubber-track">
|
||||
<div class="ve-scrubber-bar"></div>
|
||||
<div class="ve-marker ve-marker-a"></div>
|
||||
<div class="ve-marker ve-marker-b"></div>
|
||||
<div class="ve-marker ve-marker-a">A</div>
|
||||
<div class="ve-marker ve-marker-b">B</div>
|
||||
</div>
|
||||
<div class="ve-add-row">
|
||||
<label>From <input type="text" class="ve-from" placeholder="0:30" inputmode="numeric" /></label>
|
||||
<label>To <input type="text" class="ve-to" placeholder="1:15" inputmode="numeric" /></label>
|
||||
<button type="button" class="btn ve-add">Add cut</button>
|
||||
</div>
|
||||
<div class="ve-move-row" style="margin-top:10px">
|
||||
<button type="button" class="btn ve-move">Move video to last clicked marker</button>
|
||||
</div>
|
||||
<div class="ve-error" hidden></div>
|
||||
<div class="ve-cuts"></div>
|
||||
<label class="ve-title-row">Name <input type="text" class="ve-title" maxlength="120" /></label>
|
||||
@@ -763,6 +767,7 @@ function openVideoEditor(source) {
|
||||
const fromEl = body.querySelector('.ve-from');
|
||||
const toEl = body.querySelector('.ve-to');
|
||||
const addBtn = body.querySelector('.ve-add');
|
||||
const moveBtn = body.querySelector('.ve-move');
|
||||
const errEl = body.querySelector('.ve-error');
|
||||
const cutsEl = body.querySelector('.ve-cuts');
|
||||
const titleEl = body.querySelector('.ve-title');
|
||||
@@ -786,11 +791,28 @@ function openVideoEditor(source) {
|
||||
const rect = trackEl.getBoundingClientRect();
|
||||
const percent = (e.clientX - rect.left) / rect.width;
|
||||
const time = Math.round(percent * duration);
|
||||
if (!fromEl.value) fromEl.value = VideoEdit.fmtTime(time);
|
||||
else if (!toEl.value) toEl.value = VideoEdit.fmtTime(time);
|
||||
|
||||
if (!fromEl.value || lastMarker === 'B') {
|
||||
fromEl.value = VideoEdit.fmtTime(time);
|
||||
lastMarker = 'A';
|
||||
} else {
|
||||
toEl.value = VideoEdit.fmtTime(time);
|
||||
lastMarker = 'B';
|
||||
}
|
||||
updateScrubber();
|
||||
};
|
||||
|
||||
markerA.onclick = (e) => { e.stopPropagation(); lastMarker = 'A'; };
|
||||
markerB.onclick = (e) => { e.stopPropagation(); lastMarker = 'B'; };
|
||||
|
||||
moveBtn.onclick = () => {
|
||||
if (lastMarker === 'A' && fromEl.value) {
|
||||
Player.seek(VideoEdit.parseTime(fromEl.value));
|
||||
} else if (lastMarker === 'B' && toEl.value) {
|
||||
Player.seek(VideoEdit.parseTime(toEl.value));
|
||||
}
|
||||
};
|
||||
|
||||
fromEl.oninput = updateScrubber;
|
||||
toEl.oninput = updateScrubber;
|
||||
|
||||
|
||||
@@ -2278,10 +2278,17 @@ input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.25); }
|
||||
.ve-marker {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.ve-marker-a { left: 0; }
|
||||
.ve-marker-b { right: 0; }
|
||||
|
||||
Reference in New Issue
Block a user