iPhone-style trim bar: drag handles instead of sliders
User feedback: 'tako kot imajo na iphonu - potegnem iz leve in iz desne za na konec... reel pa more biti že v stanju postavljen' Replaced 2 separate range sliders with iPhone-style trim bar: - Single horizontal bar showing full video duration - 2 draggable handles (left = start, right = end) - Selected region highlighted in accent color - Live playhead during playback - Mouse + touch support - Click anywhere on bar = seek to that position - Initial state: handles positioned at auto-selected clip range (just fine-tune left/right, no need to set from scratch) formatTime helper for nice m:ss.c display.
This commit is contained in:
parent
b4294e7113
commit
9dba2a1185
@ -986,6 +986,14 @@
|
||||
}
|
||||
|
||||
// ─── EDIT MODAL ─────────────────────────────────────
|
||||
function formatTime(sec) {
|
||||
if (!isFinite(sec) || sec < 0) sec = 0;
|
||||
const m = Math.floor(sec / 60);
|
||||
const s = Math.floor(sec % 60);
|
||||
const cs = Math.floor((sec % 1) * 10);
|
||||
return `${m}:${String(s).padStart(2, "0")}.${cs}`;
|
||||
}
|
||||
|
||||
async function openEditModal(jobId, title) {
|
||||
// Fetch transcript + clip range
|
||||
let data;
|
||||
@ -1013,41 +1021,56 @@
|
||||
<button class="modal-close" title="Zapri (ESC)">×</button>
|
||||
<div class="modal-title" style="margin-bottom:12px;">✏️ Edit: ${escapeHtml(title)}</div>
|
||||
|
||||
<video id="edit-video" src="/api/source-video/${jobId}" controls preload="metadata" style="width:100%; max-height:50vh; background:#000;"></video>
|
||||
<video id="edit-video" src="/api/source-video/${jobId}" preload="metadata" style="width:100%; max-height:50vh; background:#000; border-radius:6px;"></video>
|
||||
|
||||
<div style="display:grid; gap:14px; margin-top:14px;">
|
||||
<div>
|
||||
<label style="display:flex; justify-content:space-between; font-size:13px;">
|
||||
<span>Začetek</span>
|
||||
<span><b id="edit-start-val">${startInit.toFixed(1)}s</b> · trajanje: <b id="edit-duration">${(endInit-startInit).toFixed(1)}s</b></span>
|
||||
</label>
|
||||
<input type="range" id="edit-start" min="0" max="${videoDuration.toFixed(1)}" step="0.1" value="${startInit.toFixed(1)}" style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:flex; justify-content:space-between; font-size:13px;">
|
||||
<span>Konec</span>
|
||||
<span id="edit-end-val">${endInit.toFixed(1)}s</span>
|
||||
</label>
|
||||
<input type="range" id="edit-end" min="0" max="${videoDuration.toFixed(1)}" step="0.1" value="${endInit.toFixed(1)}" style="width:100%;">
|
||||
</div>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<button class="small" onclick="seekEditVideo('start')">▶ Predvajaj od začetka</button>
|
||||
<button class="small ghost" onclick="seekEditVideo('end')">↪ Skoči na konec</button>
|
||||
<!-- iPhone-style trim bar -->
|
||||
<div id="trim-bar" style="position:relative; height:64px; margin-top:14px; background:rgba(255,255,255,0.05); border-radius:8px; overflow:hidden; user-select:none; touch-action:none;">
|
||||
<!-- Selected region (highlighted) -->
|
||||
<div id="trim-region" style="position:absolute; top:0; bottom:0; background:linear-gradient(180deg, rgba(255,107,107,0.25), rgba(255,107,107,0.15)); border-top:3px solid var(--accent); border-bottom:3px solid var(--accent); z-index:1;"></div>
|
||||
|
||||
<!-- Left handle -->
|
||||
<div id="trim-handle-left" style="position:absolute; top:0; bottom:0; width:18px; background:var(--accent); cursor:ew-resize; z-index:3; display:flex; align-items:center; justify-content:center; box-shadow:0 0 8px rgba(0,0,0,0.4);">
|
||||
<div style="width:3px; height:24px; background:#fff; border-radius:2px;"></div>
|
||||
</div>
|
||||
|
||||
<details style="margin-top:8px;">
|
||||
<summary style="cursor:pointer; font-size:13px; color:var(--muted);">📝 Edit napise (kliknite vrstico za popravek)</summary>
|
||||
<div id="edit-segments" style="max-height:30vh; overflow:auto; margin-top:10px; padding:10px; background:rgba(255,255,255,0.03); border-radius:6px;">
|
||||
${segments.filter(s => s.start < endInit && s.end > startInit).map((s, i) => `
|
||||
<div class="seg-row" data-idx="${i}" style="margin-bottom:6px; padding:6px; background:rgba(255,255,255,0.04); border-radius:4px;">
|
||||
<span style="font-size:11px; color:var(--muted); margin-right:8px;">[${s.start.toFixed(1)}s]</span>
|
||||
<input type="text" data-orig="${escapeHtml(s.text || '')}" data-start="${s.start}" data-end="${s.end}" value="${escapeHtml(s.text || '').replace(/\\n/g, ' ').trim()}" style="width:calc(100% - 80px); padding:4px 8px; font-size:13px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); border-radius:3px; color:#fff;">
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
</details>
|
||||
<!-- Right handle -->
|
||||
<div id="trim-handle-right" style="position:absolute; top:0; bottom:0; width:18px; background:var(--accent); cursor:ew-resize; z-index:3; display:flex; align-items:center; justify-content:center; box-shadow:0 0 8px rgba(0,0,0,0.4);">
|
||||
<div style="width:3px; height:24px; background:#fff; border-radius:2px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Playhead (current video position) -->
|
||||
<div id="trim-playhead" style="position:absolute; top:-3px; bottom:-3px; width:2px; background:#fff; z-index:2; pointer-events:none; opacity:0.7;"></div>
|
||||
|
||||
<!-- Time labels -->
|
||||
<div style="position:absolute; bottom:4px; left:8px; font-size:10px; color:var(--muted); pointer-events:none; z-index:4;">0:00</div>
|
||||
<div style="position:absolute; bottom:4px; right:8px; font-size:10px; color:var(--muted); pointer-events:none; z-index:4;" id="trim-end-label">${formatTime(videoDuration)}</div>
|
||||
</div>
|
||||
|
||||
<!-- Time display + controls -->
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-top:12px; gap:12px;">
|
||||
<div style="font-size:14px;">
|
||||
<span style="color:var(--muted);">Začetek:</span> <b id="edit-start-val">${formatTime(startInit)}</b>
|
||||
<span style="color:var(--muted); margin-left:12px;">Konec:</span> <b id="edit-end-val">${formatTime(endInit)}</b>
|
||||
<span style="color:var(--muted); margin-left:12px;">Trajanje:</span> <b id="edit-duration">${(endInit-startInit).toFixed(1)}s</b>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<button class="small ghost" onclick="seekEditVideo('start')" title="Predvajaj od začetka">▶ Začetek</button>
|
||||
<button class="small ghost" onclick="seekEditVideo('end')" title="Skoči na konec">↪ Konec</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details style="margin-top:14px;">
|
||||
<summary style="cursor:pointer; font-size:13px; color:var(--muted);">📝 Edit napise (kliknite vrstico za popravek)</summary>
|
||||
<div id="edit-segments" style="max-height:30vh; overflow:auto; margin-top:10px; padding:10px; background:rgba(255,255,255,0.03); border-radius:6px;">
|
||||
${segments.filter(s => s.start < endInit && s.end > startInit).map((s, i) => `
|
||||
<div class="seg-row" data-idx="${i}" style="margin-bottom:6px; padding:6px; background:rgba(255,255,255,0.04); border-radius:4px;">
|
||||
<span style="font-size:11px; color:var(--muted); margin-right:8px;">[${s.start.toFixed(1)}s]</span>
|
||||
<input type="text" data-orig="${escapeHtml(s.text || '')}" data-start="${s.start}" data-end="${s.end}" value="${escapeHtml(s.text || '').replace(/\\n/g, ' ').trim()}" style="width:calc(100% - 80px); padding:4px 8px; font-size:13px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); border-radius:3px; color:#fff;">
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="modal-actions" style="margin-top:18px;">
|
||||
<button class="primary" id="edit-save-btn">✅ Shrani in re-render</button>
|
||||
<button onclick="closeModal()">Prekliči</button>
|
||||
@ -1068,48 +1091,125 @@
|
||||
document.addEventListener("keydown", escHandler);
|
||||
overlay.querySelector(".modal-close").addEventListener("click", closeModal);
|
||||
|
||||
// Slider handlers
|
||||
// ─── iPhone-style trim bar logic ───
|
||||
const video = document.getElementById("edit-video");
|
||||
const startSlider = document.getElementById("edit-start");
|
||||
const endSlider = document.getElementById("edit-end");
|
||||
const trimBar = document.getElementById("trim-bar");
|
||||
const trimRegion = document.getElementById("trim-region");
|
||||
const handleL = document.getElementById("trim-handle-left");
|
||||
const handleR = document.getElementById("trim-handle-right");
|
||||
const playhead = document.getElementById("trim-playhead");
|
||||
const startVal = document.getElementById("edit-start-val");
|
||||
const endVal = document.getElementById("edit-end-val");
|
||||
const durVal = document.getElementById("edit-duration");
|
||||
|
||||
function updateUI() {
|
||||
const s = parseFloat(startSlider.value);
|
||||
const e = parseFloat(endSlider.value);
|
||||
startVal.textContent = s.toFixed(1) + "s";
|
||||
endVal.textContent = e.toFixed(1) + "s";
|
||||
durVal.textContent = (e - s).toFixed(1) + "s";
|
||||
// State
|
||||
let trimStart = startInit;
|
||||
let trimEnd = endInit;
|
||||
let dragging = null; // 'left' / 'right' / null
|
||||
|
||||
function pctOf(t) {
|
||||
return (t / videoDuration) * 100;
|
||||
}
|
||||
|
||||
startSlider.addEventListener("input", () => {
|
||||
if (parseFloat(startSlider.value) >= parseFloat(endSlider.value)) {
|
||||
startSlider.value = parseFloat(endSlider.value) - 0.5;
|
||||
function timeFromPx(px) {
|
||||
const rect = trimBar.getBoundingClientRect();
|
||||
const pct = Math.max(0, Math.min(1, (px - rect.left) / rect.width));
|
||||
return pct * videoDuration;
|
||||
}
|
||||
|
||||
function renderTrim() {
|
||||
const lPct = pctOf(trimStart);
|
||||
const rPct = pctOf(trimEnd);
|
||||
trimRegion.style.left = lPct + '%';
|
||||
trimRegion.style.right = (100 - rPct) + '%';
|
||||
handleL.style.left = `calc(${lPct}% - 9px)`;
|
||||
handleR.style.left = `calc(${rPct}% - 9px)`;
|
||||
startVal.textContent = formatTime(trimStart);
|
||||
endVal.textContent = formatTime(trimEnd);
|
||||
durVal.textContent = (trimEnd - trimStart).toFixed(1) + "s";
|
||||
}
|
||||
|
||||
function renderPlayhead() {
|
||||
if (!video) return;
|
||||
const t = video.currentTime || 0;
|
||||
playhead.style.left = `calc(${pctOf(t)}% - 1px)`;
|
||||
}
|
||||
|
||||
// Mouse + touch start
|
||||
function onPointerDown(which, e) {
|
||||
e.preventDefault();
|
||||
dragging = which;
|
||||
document.body.style.cursor = 'ew-resize';
|
||||
}
|
||||
|
||||
// Mouse + touch move
|
||||
function onPointerMove(e) {
|
||||
if (!dragging) return;
|
||||
const x = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
let t = timeFromPx(x);
|
||||
// Constraints
|
||||
if (dragging === 'left') {
|
||||
t = Math.max(0, Math.min(t, trimEnd - 1));
|
||||
trimStart = t;
|
||||
if (video) video.currentTime = t;
|
||||
} else if (dragging === 'right') {
|
||||
t = Math.max(trimStart + 1, Math.min(t, videoDuration));
|
||||
trimEnd = t;
|
||||
if (video) video.currentTime = t;
|
||||
}
|
||||
updateUI();
|
||||
if (video) video.currentTime = parseFloat(startSlider.value);
|
||||
});
|
||||
endSlider.addEventListener("input", () => {
|
||||
if (parseFloat(endSlider.value) <= parseFloat(startSlider.value)) {
|
||||
endSlider.value = parseFloat(startSlider.value) + 0.5;
|
||||
}
|
||||
updateUI();
|
||||
if (video) video.currentTime = parseFloat(endSlider.value);
|
||||
renderTrim();
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
dragging = null;
|
||||
document.body.style.cursor = '';
|
||||
}
|
||||
|
||||
handleL.addEventListener("mousedown", (e) => onPointerDown('left', e));
|
||||
handleR.addEventListener("mousedown", (e) => onPointerDown('right', e));
|
||||
handleL.addEventListener("touchstart", (e) => onPointerDown('left', e));
|
||||
handleR.addEventListener("touchstart", (e) => onPointerDown('right', e));
|
||||
|
||||
document.addEventListener("mousemove", onPointerMove);
|
||||
document.addEventListener("touchmove", onPointerMove);
|
||||
document.addEventListener("mouseup", onPointerUp);
|
||||
document.addEventListener("touchend", onPointerUp);
|
||||
|
||||
// Click anywhere on trim bar = seek video
|
||||
trimBar.addEventListener("click", (e) => {
|
||||
if (e.target === handleL || e.target === handleR || handleL.contains(e.target) || handleR.contains(e.target)) return;
|
||||
const t = timeFromPx(e.clientX);
|
||||
if (video) video.currentTime = t;
|
||||
});
|
||||
|
||||
// Update playhead during playback
|
||||
if (video) {
|
||||
video.addEventListener("timeupdate", renderPlayhead);
|
||||
video.addEventListener("loadedmetadata", () => {
|
||||
// Če videoDuration ni bilo podanih, vzemi iz videa
|
||||
if (!data.video_duration && video.duration) {
|
||||
// Recalculate render
|
||||
renderTrim();
|
||||
renderPlayhead();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.seekEditVideo = function(which) {
|
||||
if (!video) return;
|
||||
const t = which === "start" ? parseFloat(startSlider.value) : parseFloat(endSlider.value);
|
||||
const t = which === "start" ? trimStart : trimEnd;
|
||||
video.currentTime = t;
|
||||
if (which === "start") video.play();
|
||||
};
|
||||
|
||||
// Initial render
|
||||
renderTrim();
|
||||
renderPlayhead();
|
||||
|
||||
// Save button
|
||||
document.getElementById("edit-save-btn").addEventListener("click", async () => {
|
||||
const start = parseFloat(startSlider.value);
|
||||
const end = parseFloat(endSlider.value);
|
||||
const start = trimStart;
|
||||
const end = trimEnd;
|
||||
if (end - start < 5) {
|
||||
alert("Trajanje mora biti vsaj 5s");
|
||||
return;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user