Edit modal: zoom + play-from-position + Space toggle
User feedback: 1. 'Wave form je premajhen — zoom' 2. 'Ko nastavimo pozicijo, play od začetka — ne moremo predvajat od tam' NEW Zoom feature: - 5 zoom levels: 1x, 2x, 5x, 10x, 20x - Trim bar wrapped in scrollable container - On zoom: bar width grows to 100*N%, scroll auto-centers on trim region - Higher zoom = more pixels per second = micro-tuning possible (1x: 5px/s, 20x: 100px/s for 4min song) - Active zoom button highlighted accent red NEW Play-from-position: - Click on waveform/trim bar = playhead JUMPS THERE + auto-plays (was: just moved playhead, no play) - Space key = play/pause toggle from current position (works anywhere except in input fields) - '▶ Predvajaj odsek' still does start-to-end of selection - Cleanup keydown listener on modal close Waveform now rendered at 2400x72 (higher res) so zoom looks crisp. User can now: - Zoom 10x to see exact word boundaries in waveform - Click anywhere → instant play from there - Hit Space to toggle while watching
This commit is contained in:
parent
facfd6bd39
commit
47a114ce6a
@ -1054,30 +1054,44 @@
|
||||
|
||||
<!-- iPhone-style trim bar + WAVEFORM spodaj (full width) -->
|
||||
<div style="margin-top:18px;">
|
||||
<!-- Trim bar -->
|
||||
<div id="trim-bar" style="position:relative; height:72px; width:100%; flex-shrink:0; box-sizing:border-box; background:#1a1a1a; border:2px solid #444; border-radius:8px 8px 0 0; overflow:hidden; user-select:none; touch-action:none;">
|
||||
<!-- Waveform image (background) -->
|
||||
<img id="trim-waveform" src="/api/waveform/${jobId}?width=1200&height=72" style="position:absolute; top:0; left:0; width:100%; height:100%; opacity:0.6; pointer-events:none; z-index:0;" onerror="this.style.display='none'">
|
||||
|
||||
<!-- Selected region -->
|
||||
<div id="trim-region" style="position:absolute; top:0; bottom:0; left:${pctOfStr(startInit, videoDuration)}%; right:${(100 - parseFloat(pctOfStr(endInit, videoDuration))).toFixed(2)}%; background:linear-gradient(180deg, rgba(255,107,107,0.35), rgba(255,107,107,0.2)); border-top:4px solid #ff6b6b; border-bottom:4px solid #ff6b6b; z-index:1;"></div>
|
||||
|
||||
<!-- Left handle -->
|
||||
<div id="trim-handle-left" style="position:absolute; top:0; bottom:0; left:calc(${pctOfStr(startInit, videoDuration)}% - 12px); width:24px; background:#ff6b6b; cursor:ew-resize; z-index:3; display:flex; align-items:center; justify-content:center; box-shadow:0 0 12px rgba(255,107,107,0.6);">
|
||||
<div style="width:4px; height:32px; background:#fff; border-radius:2px;"></div>
|
||||
<!-- Zoom controls -->
|
||||
<div style="display:flex; align-items:center; gap:8px; margin-bottom:6px;">
|
||||
<span style="font-size:12px; color:var(--muted);">Zoom:</span>
|
||||
<button class="small ghost zoom-btn" data-zoom="1" style="padding:3px 10px;">1x</button>
|
||||
<button class="small ghost zoom-btn" data-zoom="2" style="padding:3px 10px;">2x</button>
|
||||
<button class="small ghost zoom-btn" data-zoom="5" style="padding:3px 10px;">5x</button>
|
||||
<button class="small ghost zoom-btn" data-zoom="10" style="padding:3px 10px;">10x</button>
|
||||
<button class="small ghost zoom-btn" data-zoom="20" style="padding:3px 10px;">20x</button>
|
||||
<span style="margin-left:14px; font-size:11px; color:var(--muted);">Klik na valove = skoči + predvaja · Space = play/pause</span>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable wrapper -->
|
||||
<div id="trim-scroll" style="width:100%; overflow-x:auto; overflow-y:hidden; background:#0d0d0d; border-radius:8px;">
|
||||
<!-- Trim bar (gets wider on zoom) -->
|
||||
<div id="trim-bar" style="position:relative; height:72px; width:100%; min-width:100%; flex-shrink:0; box-sizing:border-box; background:#1a1a1a; border:2px solid #444; border-radius:6px; overflow:hidden; user-select:none; touch-action:none;">
|
||||
<!-- Waveform image (background) -->
|
||||
<img id="trim-waveform" src="/api/waveform/${jobId}?width=2400&height=72" style="position:absolute; top:0; left:0; width:100%; height:100%; opacity:0.6; pointer-events:none; z-index:0; object-fit:fill;" onerror="this.style.display='none'">
|
||||
|
||||
<!-- Selected region -->
|
||||
<div id="trim-region" style="position:absolute; top:0; bottom:0; left:${pctOfStr(startInit, videoDuration)}%; right:${(100 - parseFloat(pctOfStr(endInit, videoDuration))).toFixed(2)}%; background:linear-gradient(180deg, rgba(255,107,107,0.35), rgba(255,107,107,0.2)); border-top:4px solid #ff6b6b; border-bottom:4px solid #ff6b6b; z-index:1;"></div>
|
||||
|
||||
<!-- Left handle -->
|
||||
<div id="trim-handle-left" style="position:absolute; top:0; bottom:0; left:calc(${pctOfStr(startInit, videoDuration)}% - 12px); width:24px; background:#ff6b6b; cursor:ew-resize; z-index:3; display:flex; align-items:center; justify-content:center; box-shadow:0 0 12px rgba(255,107,107,0.6);">
|
||||
<div style="width:4px; height:32px; background:#fff; border-radius:2px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Right handle -->
|
||||
<div id="trim-handle-right" style="position:absolute; top:0; bottom:0; left:calc(${pctOfStr(endInit, videoDuration)}% - 12px); width:24px; background:#ff6b6b; cursor:ew-resize; z-index:3; display:flex; align-items:center; justify-content:center; box-shadow:0 0 12px rgba(255,107,107,0.6);">
|
||||
<div style="width:4px; height:32px; background:#fff; border-radius:2px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Playhead -->
|
||||
<div id="trim-playhead" style="position:absolute; top:-4px; bottom:-4px; left:0%; width:3px; background:#fff; z-index:2; pointer-events:none; opacity:0.9; box-shadow:0 0 6px #fff;"></div>
|
||||
|
||||
<!-- Time labels -->
|
||||
<div style="position:absolute; bottom:4px; left:8px; font-size:11px; color:#fff; pointer-events:none; z-index:4; text-shadow:0 0 4px #000;">0:00</div>
|
||||
<div style="position:absolute; bottom:4px; right:8px; font-size:11px; color:#fff; pointer-events:none; z-index:4; text-shadow:0 0 4px #000;" id="trim-end-label">${formatTime(videoDuration)}</div>
|
||||
</div>
|
||||
|
||||
<!-- Right handle -->
|
||||
<div id="trim-handle-right" style="position:absolute; top:0; bottom:0; left:calc(${pctOfStr(endInit, videoDuration)}% - 12px); width:24px; background:#ff6b6b; cursor:ew-resize; z-index:3; display:flex; align-items:center; justify-content:center; box-shadow:0 0 12px rgba(255,107,107,0.6);">
|
||||
<div style="width:4px; height:32px; background:#fff; border-radius:2px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Playhead -->
|
||||
<div id="trim-playhead" style="position:absolute; top:-4px; bottom:-4px; left:0%; width:3px; background:#fff; z-index:2; pointer-events:none; opacity:0.8; box-shadow:0 0 4px #fff;"></div>
|
||||
|
||||
<!-- Time labels -->
|
||||
<div style="position:absolute; bottom:4px; left:8px; font-size:11px; color:#fff; pointer-events:none; z-index:4; text-shadow:0 0 4px #000;">0:00</div>
|
||||
<div style="position:absolute; bottom:4px; right:8px; font-size:11px; color:#fff; pointer-events:none; z-index:4; text-shadow:0 0 4px #000;" id="trim-end-label">${formatTime(videoDuration)}</div>
|
||||
</div>
|
||||
|
||||
<!-- Hint -->
|
||||
@ -1212,13 +1226,71 @@
|
||||
document.addEventListener("mouseup", onPointerUp);
|
||||
document.addEventListener("touchend", onPointerUp);
|
||||
|
||||
// Click anywhere on trim bar = seek video
|
||||
// Click anywhere on trim bar = seek + predvajaj OD TJE (ne od trim start)
|
||||
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;
|
||||
if (video) {
|
||||
video.currentTime = t;
|
||||
video.play().catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
// ─── ZOOM logic ───
|
||||
let currentZoom = 1;
|
||||
const trimScroll = document.getElementById("trim-scroll");
|
||||
|
||||
function applyZoom(zoom) {
|
||||
currentZoom = zoom;
|
||||
trimBar.style.width = (100 * zoom) + "%";
|
||||
trimBar.style.minWidth = (100 * zoom) + "%";
|
||||
// Aktiven gumb
|
||||
document.querySelectorAll(".zoom-btn").forEach(b => {
|
||||
if (parseInt(b.dataset.zoom) === zoom) {
|
||||
b.style.background = "var(--accent)";
|
||||
b.style.color = "#fff";
|
||||
} else {
|
||||
b.style.background = "";
|
||||
b.style.color = "";
|
||||
}
|
||||
});
|
||||
// Auto-scroll na sredino trim region
|
||||
setTimeout(() => {
|
||||
if (trimScroll) {
|
||||
const barWidth = trimBar.getBoundingClientRect().width;
|
||||
const centerPct = ((trimStart + trimEnd) / 2) / videoDuration;
|
||||
const scrollTarget = barWidth * centerPct - trimScroll.clientWidth / 2;
|
||||
trimScroll.scrollLeft = Math.max(0, scrollTarget);
|
||||
}
|
||||
renderTrim();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
document.querySelectorAll(".zoom-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
applyZoom(parseInt(btn.dataset.zoom));
|
||||
});
|
||||
});
|
||||
|
||||
// Space tipka = play/pause od trenutne pozicije (brez resetiranja na trim start)
|
||||
const spaceHandler = (e) => {
|
||||
if (e.code === "Space" && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
|
||||
e.preventDefault();
|
||||
if (video) {
|
||||
if (video.paused) {
|
||||
video.play().catch(() => {});
|
||||
} else {
|
||||
video.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", spaceHandler);
|
||||
// Cleanup ob zaprtju modala
|
||||
overlay._cleanup = () => {
|
||||
document.removeEventListener("keydown", spaceHandler);
|
||||
};
|
||||
|
||||
// Update playhead during playback + re-render če videoDuration manjkalo
|
||||
if (video) {
|
||||
video.addEventListener("timeupdate", renderPlayhead);
|
||||
@ -1325,6 +1397,8 @@
|
||||
requestAnimationFrame(() => {
|
||||
renderTrim();
|
||||
renderPlayhead();
|
||||
// Nastavi 1x kot aktiven gumb
|
||||
applyZoom(1);
|
||||
console.log("[EditModal] after renderTrim", {
|
||||
leftStyle: handleL.style.left,
|
||||
rightStyle: handleR.style.left,
|
||||
@ -1433,6 +1507,10 @@
|
||||
function closeModal() {
|
||||
const overlay = document.querySelector(".modal-overlay");
|
||||
if (overlay) {
|
||||
// Cleanup event listeners (npr. Space tipka)
|
||||
if (typeof overlay._cleanup === "function") {
|
||||
try { overlay._cleanup(); } catch (e) {}
|
||||
}
|
||||
// Stop video before removing (prevents memory leak)
|
||||
const video = overlay.querySelector("video");
|
||||
if (video) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user