generated from dellevin/template
555
This commit is contained in:
@@ -280,6 +280,56 @@
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
border-radius: 8px; pointer-events: none;
|
||||
}
|
||||
#segmentOverlay {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
border-radius: 8px; pointer-events: none; z-index: 1;
|
||||
}
|
||||
.seg-gray {
|
||||
position: absolute; top: 0; height: 100%;
|
||||
background: rgba(0,0,0,0.18); pointer-events: none;
|
||||
}
|
||||
#playCursor {
|
||||
position: absolute; top: 0; height: 100%; width: 10px;
|
||||
transform: translateX(-5px); z-index: 5;
|
||||
pointer-events: auto; cursor: col-resize;
|
||||
display: none;
|
||||
}
|
||||
#playCursor::after {
|
||||
content: ''; position: absolute; left: 50%; top: 0; bottom: 0;
|
||||
width: 2px; transform: translateX(-50%);
|
||||
background: #e53935; box-shadow: 0 0 4px rgba(229,57,53,0.4);
|
||||
pointer-events: none;
|
||||
}
|
||||
#cursorHandle {
|
||||
position: absolute; left: 50%; top: -6px; transform: translateX(-50%);
|
||||
width: 12px; height: 12px; border-radius: 50%;
|
||||
background: #e53935; border: 2px solid #fff;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.25);
|
||||
pointer-events: none;
|
||||
}
|
||||
.player-controls {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 6px 12px; margin-top: 4px;
|
||||
background: #fff; border: 1px solid #e0e0e0; border-radius: 8px;
|
||||
font-size: 12px; color: #555;
|
||||
}
|
||||
.player-controls button {
|
||||
width: 30px; height: 30px; border: none; border-radius: 50%;
|
||||
background: #0078d4; color: #fff; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 12px; transition: background 0.15s;
|
||||
}
|
||||
.player-controls button:hover { background: #005fa3; }
|
||||
.player-controls .p-time { font-variant-numeric: tabular-nums; min-width: 36px; text-align: center; }
|
||||
.player-controls input[type="range"] {
|
||||
flex: 1; height: 4px; -webkit-appearance: none; appearance: none;
|
||||
background: #e0e0e0; border-radius: 2px; outline: none; cursor: pointer;
|
||||
}
|
||||
.player-controls input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none; appearance: none;
|
||||
width: 14px; height: 14px; border-radius: 50%;
|
||||
background: #0078d4; cursor: pointer;
|
||||
}
|
||||
.cut-marker {
|
||||
position: absolute; top: 0; width: 24px; height: 100%;
|
||||
transform: translateX(-50%); z-index: 2; cursor: grab;
|
||||
@@ -431,6 +481,12 @@
|
||||
</div>
|
||||
<div id="timelineArea" style="display:none;">
|
||||
<div class="timeline-wrap" id="timelineBar"></div>
|
||||
<div class="player-controls" id="playerControls" style="display:none;">
|
||||
<button onclick="playerToggle()" title="播放/暂停"><i id="playerIcon" class="fas fa-play"></i></button>
|
||||
<span class="p-time" id="playerTime">0:00</span>
|
||||
<input type="range" id="playerSeekBar" min="0" max="1000" value="0" step="1">
|
||||
<span class="p-time" id="playerTotal">0:00</span>
|
||||
</div>
|
||||
<div id="redoWrap" style="display:none;text-align:center;margin-top:8px;">
|
||||
<button class="action-btn slice" style="width:auto;padding:8px 20px;font-size:13px;" onclick="redoManualSlice()">
|
||||
<i class="fas fa-redo"></i> 切割点已更改,点击重新切割
|
||||
@@ -466,6 +522,14 @@
|
||||
let manualSegChanged = false; // 用户是否手动调整过勾选
|
||||
let manualSegInit = false; // 新建预览时重置勾选
|
||||
|
||||
// 播放器状态
|
||||
var playerAudio = null;
|
||||
var playerPlaying = false;
|
||||
var _playerRAF = null;
|
||||
var _playerStartSec = 0;
|
||||
var _playerEndSec = 0;
|
||||
var _playerMode = 'full'; // 'full' 全曲 | 'seg' 片段
|
||||
|
||||
function showMessage(text, type, duration) {
|
||||
type = type || 'info'; duration = duration || 3000;
|
||||
var t = document.createElement('div');
|
||||
@@ -648,14 +712,6 @@
|
||||
document.getElementById('sliceList').innerHTML = html;
|
||||
}
|
||||
|
||||
function playPreviewSeg(index) {
|
||||
if (!taskId || !window._previewRanges || !window._previewRanges[index]) return;
|
||||
if (currentAudio) { currentAudio.pause(); currentAudio = null; }
|
||||
var r = window._previewRanges[index];
|
||||
var audio = new Audio('/audio-slicer/preview-range/' + taskId + '?start=' + r[0] + '&end=' + r[1]);
|
||||
audio.play();
|
||||
currentAudio = audio;
|
||||
}
|
||||
|
||||
function toggleSegment(idx) {
|
||||
var pos = selectedSegments.indexOf(idx);
|
||||
@@ -696,6 +752,7 @@
|
||||
if (el) el.textContent = '已选 ' + selectedSegments.length + '/' + total;
|
||||
var btnSlice = document.getElementById('btnSlice');
|
||||
if (btnSlice) btnSlice.disabled = selectedSegments.length === 0;
|
||||
_updateSegmentOverlay();
|
||||
}
|
||||
|
||||
async function doSlice() {
|
||||
@@ -790,13 +847,8 @@
|
||||
document.getElementById('sliceList').innerHTML = html;
|
||||
}
|
||||
|
||||
|
||||
var currentAudio = null;
|
||||
function playSlice(index) {
|
||||
if (currentAudio) { currentAudio.pause(); currentAudio = null; }
|
||||
var audio = new Audio('/audio-slicer/download/' + taskId + '/' + index);
|
||||
audio.play();
|
||||
currentAudio = audio;
|
||||
}
|
||||
|
||||
function downloadSlice(index) {
|
||||
var a = document.createElement('a');
|
||||
@@ -902,8 +954,6 @@
|
||||
// 新预览时重置勾选
|
||||
if (manualSegInit) {
|
||||
manualSegInit = false;
|
||||
manualSegSelected = [];
|
||||
manualSegChanged = false;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
@@ -918,8 +968,8 @@
|
||||
}
|
||||
|
||||
var boundaries = [0].concat(cutPoints).concat([audioDuration]);
|
||||
// 确保勾选数组长度匹配
|
||||
if (manualSegSelected.length !== boundaries.length - 1) {
|
||||
// 仅初始化时全选;用户已手动调整勾选则保留
|
||||
if (manualSegSelected.length !== boundaries.length - 1 && !manualSegChanged) {
|
||||
manualSegSelected = [];
|
||||
for (var j = 0; j < boundaries.length - 1; j++) manualSegSelected.push(j);
|
||||
}
|
||||
@@ -965,13 +1015,6 @@
|
||||
markCutPointsChanged();
|
||||
}
|
||||
|
||||
var previewAudio = null;
|
||||
function playPreview(start, end) {
|
||||
if (!taskId) return;
|
||||
if (previewAudio) { previewAudio.pause(); previewAudio = null; }
|
||||
previewAudio = new Audio('/audio-slicer/preview-range/' + taskId + '?start=' + start + '&end=' + end);
|
||||
previewAudio.play();
|
||||
}
|
||||
|
||||
function toggleManualSeg(idx) {
|
||||
var pos = manualSegSelected.indexOf(idx);
|
||||
@@ -988,6 +1031,7 @@
|
||||
if (row) row.className = 'seg-row' + (pos < 0 ? ' selected' : ' dimmed');
|
||||
updateManualSegCount();
|
||||
updateManualBtn();
|
||||
_updateSegmentOverlay();
|
||||
}
|
||||
|
||||
function selectManualSegments() {
|
||||
@@ -995,6 +1039,7 @@
|
||||
for (var i = 0; i < cutPoints.length + 1; i++) manualSegSelected.push(i);
|
||||
manualSegChanged = true;
|
||||
renderCutList();
|
||||
_updateSegmentOverlay();
|
||||
}
|
||||
|
||||
|
||||
@@ -1030,7 +1075,9 @@
|
||||
|
||||
bar.className = readOnly ? 'readonly' : '';
|
||||
|
||||
var html = '<canvas id="waveformCanvas"></canvas>';
|
||||
var html = '<canvas id="waveformCanvas"></canvas>'
|
||||
+ '<div id="segmentOverlay"></div>'
|
||||
+ '<div id="playCursor"><div id="cursorHandle"></div></div>';
|
||||
// 时间刻度(在波形条上方)
|
||||
var step = Math.max(1, Math.ceil(audioDuration / 10));
|
||||
for (var t = 0; t <= audioDuration; t += step) {
|
||||
@@ -1050,20 +1097,62 @@
|
||||
|
||||
// 绘制波形
|
||||
if (waveformPeaks) drawWaveform();
|
||||
_updateSegmentOverlay();
|
||||
|
||||
if (readOnly) return; // 只读模式不绑定交互事件
|
||||
// 播放控制条
|
||||
var pc = document.getElementById('playerControls');
|
||||
if (pc) { pc.style.display = audioDuration > 0 ? '' : 'none'; }
|
||||
var pt = document.getElementById('playerTotal');
|
||||
if (pt) pt.textContent = formatTime(audioDuration);
|
||||
_showCursorAtCurrent();
|
||||
|
||||
// 时间轴空白区域点击 → 添加切割点
|
||||
// 绑定事件(只绑一次)
|
||||
if (!bar._bound) {
|
||||
bar._bound = true;
|
||||
|
||||
// 时间轴点击:只读模式 → seek,手动模式 → 添加切割点
|
||||
bar.addEventListener('click', function(e) {
|
||||
if (bar._suppressClick) { bar._suppressClick = false; return; }
|
||||
if (bar.classList.contains('readonly')) return;
|
||||
if (e.target.closest('.cut-marker')) return;
|
||||
if (e.target.closest('.cut-marker') || e.target.closest('#playCursor')) return;
|
||||
var rect = bar.getBoundingClientRect();
|
||||
var sec = (e.clientX - rect.left) / rect.width * audioDuration;
|
||||
if (bar.classList.contains('readonly')) {
|
||||
playerSeekTo(sec);
|
||||
return;
|
||||
}
|
||||
// 手动模式:不允许在灰色片段内添加切割点
|
||||
var ranges = _getSegmentRanges();
|
||||
var sel = _getSelectedSet();
|
||||
if (ranges.length && sel.length) {
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
if (sec >= ranges[i][0] && sec <= ranges[i][1]) {
|
||||
if (sel.indexOf(i) < 0) return;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sec > 0.5 && sec < audioDuration - 0.5) addCutPoint(sec);
|
||||
});
|
||||
|
||||
// 播放游标拖拽
|
||||
_bindCursorDrag(bar);
|
||||
|
||||
// 进度条拖拽 seek
|
||||
var seekBar = document.getElementById('playerSeekBar');
|
||||
if (seekBar) {
|
||||
seekBar.addEventListener('input', function() {
|
||||
var sec = (parseFloat(this.value) / 1000) * audioDuration;
|
||||
var cursor = document.getElementById('playCursor');
|
||||
var timeEl = document.getElementById('playerTime');
|
||||
if (cursor) { cursor.style.display = 'block'; cursor.style.left = (sec / audioDuration * 100).toFixed(2) + '%'; }
|
||||
if (timeEl) timeEl.textContent = formatTime(sec);
|
||||
if (playerAudio) {
|
||||
var rel = sec - _playerStartSec;
|
||||
var dur = _playerEndSec - _playerStartSec;
|
||||
if (rel >= 0 && rel <= dur) playerAudio.currentTime = rel;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 为每个标记绑定拖动
|
||||
@@ -1099,6 +1188,245 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── 片段灰色覆盖(取消选中的片段变灰) ──
|
||||
function _getSegmentRanges() {
|
||||
// 手动模式:从 cutPoints 计算
|
||||
if (cutMode === 'manual') {
|
||||
if (cutPoints.length > 0) {
|
||||
var b = [0].concat(cutPoints.slice().sort(function(a,b){return a-b;})).concat([audioDuration]);
|
||||
var ranges = [];
|
||||
for (var i = 0; i < b.length - 1; i++) ranges.push([b[i], b[i+1]]);
|
||||
return ranges;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
// 自动模式:用预览区间
|
||||
if (window._previewRanges && window._previewRanges.length) {
|
||||
return window._previewRanges;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function _getSelectedSet() {
|
||||
if (manualSegSelected && manualSegSelected.length > 0 && cutPoints.length > 0) {
|
||||
return manualSegSelected;
|
||||
}
|
||||
return selectedSegments;
|
||||
}
|
||||
|
||||
function _updateSegmentOverlay() {
|
||||
var overlay = document.getElementById('segmentOverlay');
|
||||
if (!overlay || audioDuration <= 0) return;
|
||||
var ranges = _getSegmentRanges();
|
||||
if (!ranges.length) { overlay.innerHTML = ''; return; }
|
||||
var sel = _getSelectedSet();
|
||||
var html = '';
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
if (sel.indexOf(i) < 0) {
|
||||
var left = (ranges[i][0] / audioDuration * 100).toFixed(2);
|
||||
var width = ((ranges[i][1] - ranges[i][0]) / audioDuration * 100).toFixed(2);
|
||||
html += '<div class="seg-gray" style="left:' + left + '%;width:' + width + '%;"></div>';
|
||||
}
|
||||
}
|
||||
overlay.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── 播放器系统 ──
|
||||
function _showCursorAtCurrent() {
|
||||
var cursor = document.getElementById('playCursor');
|
||||
if (!cursor) return;
|
||||
if (playerAudio) {
|
||||
var sec = _playerStartSec + playerAudio.currentTime;
|
||||
cursor.style.left = (sec / audioDuration * 100).toFixed(2) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
function _playerTick() {
|
||||
var cursor = document.getElementById('playCursor');
|
||||
var timeEl = document.getElementById('playerTime');
|
||||
if (!playerAudio || !cursor) return;
|
||||
var sec = _playerStartSec + playerAudio.currentTime;
|
||||
cursor.style.left = (sec / audioDuration * 100).toFixed(2) + '%';
|
||||
if (timeEl) timeEl.textContent = formatTime(sec);
|
||||
var seekBar = document.getElementById('playerSeekBar');
|
||||
if (seekBar && audioDuration > 0) seekBar.value = (sec / audioDuration * 1000).toFixed(0);
|
||||
_playerRAF = requestAnimationFrame(_playerTick);
|
||||
}
|
||||
|
||||
function playerStartTick() {
|
||||
playerStopTick();
|
||||
var cursor = document.getElementById('playCursor');
|
||||
if (cursor) cursor.style.display = 'block';
|
||||
_playerRAF = requestAnimationFrame(_playerTick);
|
||||
}
|
||||
|
||||
function playerStopTick() {
|
||||
if (_playerRAF) { cancelAnimationFrame(_playerRAF); _playerRAF = null; }
|
||||
}
|
||||
|
||||
function playerSeekTo(sec) {
|
||||
sec = Math.max(0, Math.min(audioDuration, sec));
|
||||
var cursor = document.getElementById('playCursor');
|
||||
var timeEl = document.getElementById('playerTime');
|
||||
if (cursor) {
|
||||
cursor.style.display = 'block';
|
||||
cursor.style.left = (sec / audioDuration * 100).toFixed(2) + '%';
|
||||
}
|
||||
if (timeEl) timeEl.textContent = formatTime(sec);
|
||||
if (playerAudio && !playerAudio.paused) {
|
||||
var rel = sec - _playerStartSec;
|
||||
if (rel >= 0 && rel <= _playerEndSec - _playerStartSec) {
|
||||
playerAudio.currentTime = rel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function playerToggle() {
|
||||
if (playerPlaying) {
|
||||
playerPause();
|
||||
} else {
|
||||
playerPlay();
|
||||
}
|
||||
}
|
||||
|
||||
function playerPlay() {
|
||||
if (!taskId || audioDuration <= 0) return;
|
||||
if (playerAudio && !playerAudio.paused) return;
|
||||
// 恢复暂停的音频
|
||||
if (playerAudio && playerAudio.paused && playerAudio.currentTime > 0 && playerAudio.duration > 0) {
|
||||
playerAudio.play();
|
||||
playerPlaying = true;
|
||||
playerStartTick();
|
||||
_updatePlayerIcon();
|
||||
return;
|
||||
}
|
||||
// 新建全曲播放
|
||||
_playerStopAll();
|
||||
_playerMode = 'full';
|
||||
_playerStartSec = 0;
|
||||
_playerEndSec = audioDuration;
|
||||
playerAudio = new Audio('/audio-slicer/preview-range/' + taskId + '?start=0&end=' + audioDuration);
|
||||
playerAudio.addEventListener('ended', function() {
|
||||
playerPlaying = false;
|
||||
playerStopTick();
|
||||
_updatePlayerIcon();
|
||||
});
|
||||
playerAudio.play();
|
||||
playerPlaying = true;
|
||||
playerStartTick();
|
||||
_updatePlayerIcon();
|
||||
}
|
||||
|
||||
function playerPause() {
|
||||
if (playerAudio) playerAudio.pause();
|
||||
playerPlaying = false;
|
||||
playerStopTick();
|
||||
_updatePlayerIcon();
|
||||
}
|
||||
|
||||
function _playerStopAll() {
|
||||
playerStopTick();
|
||||
if (playerAudio) {
|
||||
playerAudio.pause();
|
||||
playerAudio.src = '';
|
||||
playerAudio = null;
|
||||
}
|
||||
playerPlaying = false;
|
||||
_updatePlayerIcon();
|
||||
if (currentAudio) { currentAudio.pause(); currentAudio = null; }
|
||||
}
|
||||
|
||||
function _updatePlayerIcon() {
|
||||
var icon = document.getElementById('playerIcon');
|
||||
if (icon) icon.className = playerPlaying ? 'fas fa-pause' : 'fas fa-play';
|
||||
}
|
||||
|
||||
// 播放片段(自动模式列表按钮)
|
||||
function playPreviewSeg(index) {
|
||||
if (!taskId || !window._previewRanges || !window._previewRanges[index]) return;
|
||||
_playerStopAll();
|
||||
var r = window._previewRanges[index];
|
||||
_playerMode = 'seg';
|
||||
_playerStartSec = r[0];
|
||||
_playerEndSec = r[1];
|
||||
playerAudio = new Audio('/audio-slicer/preview-range/' + taskId + '?start=' + r[0] + '&end=' + r[1]);
|
||||
playerAudio.addEventListener('ended', function() {
|
||||
playerPlaying = false;
|
||||
playerStopTick();
|
||||
_updatePlayerIcon();
|
||||
});
|
||||
playerAudio.play();
|
||||
playerPlaying = true;
|
||||
playerStartTick();
|
||||
_updatePlayerIcon();
|
||||
}
|
||||
|
||||
// 播放片段(手动模式列表按钮)
|
||||
function playPreview(start, end) {
|
||||
if (!taskId) return;
|
||||
_playerStopAll();
|
||||
_playerMode = 'seg';
|
||||
_playerStartSec = start;
|
||||
_playerEndSec = end;
|
||||
playerAudio = new Audio('/audio-slicer/preview-range/' + taskId + '?start=' + start + '&end=' + end);
|
||||
playerAudio.addEventListener('ended', function() {
|
||||
playerPlaying = false;
|
||||
playerStopTick();
|
||||
_updatePlayerIcon();
|
||||
});
|
||||
playerAudio.play();
|
||||
playerPlaying = true;
|
||||
playerStartTick();
|
||||
_updatePlayerIcon();
|
||||
}
|
||||
|
||||
// 播放已切片结果
|
||||
function playSlice(index) {
|
||||
_playerStopAll();
|
||||
var audio = new Audio('/audio-slicer/download/' + taskId + '/' + index);
|
||||
audio.play();
|
||||
currentAudio = audio;
|
||||
}
|
||||
|
||||
// 游标拖拽
|
||||
function _bindCursorDrag(bar) {
|
||||
var cursor = document.getElementById('playCursor');
|
||||
if (!cursor) return;
|
||||
var dragging = false;
|
||||
|
||||
cursor.addEventListener('pointerdown', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragging = true;
|
||||
cursor.setPointerCapture(e.pointerId);
|
||||
});
|
||||
|
||||
cursor.addEventListener('pointermove', function(e) {
|
||||
if (!dragging) return;
|
||||
var rect = bar.getBoundingClientRect();
|
||||
var sec = (e.clientX - rect.left) / rect.width * audioDuration;
|
||||
sec = Math.max(0, Math.min(audioDuration, sec));
|
||||
cursor.style.left = (sec / audioDuration * 100).toFixed(2) + '%';
|
||||
var timeEl = document.getElementById('playerTime');
|
||||
if (timeEl) timeEl.textContent = formatTime(sec);
|
||||
// 同步 audio currentTime
|
||||
if (playerAudio) {
|
||||
var rel = sec - _playerStartSec;
|
||||
var dur = _playerEndSec - _playerStartSec;
|
||||
if (rel >= 0 && rel <= dur) {
|
||||
playerAudio.currentTime = rel;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cursor.addEventListener('pointerup', function(e) {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
cursor.releasePointerCapture(e.pointerId);
|
||||
bar._suppressClick = true;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadWaveform() {
|
||||
if (!taskId) return;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user