clickable timecode in transcript
This commit is contained in:
parent
566138dedc
commit
643f9b3908
@ -184,6 +184,11 @@ a.show-transcript:hover {
|
||||
.highlight {
|
||||
background-color: yellow;
|
||||
}
|
||||
#transcriptContent code.timecode-link {
|
||||
cursor: pointer;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
|
||||
a.create-share {
|
||||
text-decoration: none;
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
let currentMusicFiles = []; // Array of objects with at least { path, index }
|
||||
let currentMusicIndex = -1; // Index of the current music file
|
||||
let currentTrackPath = "";
|
||||
let activeTranscriptAudio = ""; // Audio file that matches the currently open transcript.
|
||||
|
||||
// Check thumb availability in parallel; sequentially generate missing ones to avoid concurrent creation.
|
||||
function ensureFirstFivePreviews() {
|
||||
@ -70,6 +71,79 @@ function encodeSubpath(subpath) {
|
||||
.join('/');
|
||||
}
|
||||
|
||||
// Convert a timecode string like "00:17" or "01:02:03" into total seconds.
|
||||
function parseTimecode(text) {
|
||||
const parts = text.trim().split(':').map(part => parseInt(part, 10));
|
||||
if (parts.some(Number.isNaN) || parts.length < 2 || parts.length > 3) return null;
|
||||
|
||||
const hasHours = parts.length === 3;
|
||||
const [hours, minutes, seconds] = hasHours ? parts : [0, parts[0], parts[1]];
|
||||
|
||||
if (seconds < 0 || seconds > 59) return null;
|
||||
if (minutes < 0 || (hasHours && minutes > 59)) return null;
|
||||
if (hours < 0) return null;
|
||||
|
||||
return hours * 3600 + minutes * 60 + seconds;
|
||||
}
|
||||
|
||||
// Add click handlers to transcript timecodes so they seek the matching audio.
|
||||
function attachTranscriptTimecodes(audioPath) {
|
||||
const container = document.getElementById('transcriptContent');
|
||||
const targetAudio = audioPath || activeTranscriptAudio;
|
||||
if (!container || !targetAudio) return;
|
||||
|
||||
container.querySelectorAll('code').forEach(codeEl => {
|
||||
if (codeEl.closest('pre')) return;
|
||||
const label = codeEl.textContent.trim();
|
||||
const seconds = parseTimecode(label);
|
||||
if (seconds === null) return;
|
||||
|
||||
codeEl.classList.add('timecode-link');
|
||||
codeEl.title = `Zu ${label} springen`;
|
||||
codeEl.addEventListener('click', () => jumpToTimecode(seconds, targetAudio));
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure we load the right track (if needed) and jump to the requested time.
|
||||
function jumpToTimecode(seconds, relUrl) {
|
||||
if (!relUrl || seconds === null) return;
|
||||
|
||||
const updateTrackState = () => {
|
||||
currentTrackPath = relUrl;
|
||||
const matchIdx = currentMusicFiles.findIndex(file => file.path === relUrl);
|
||||
currentMusicIndex = matchIdx !== -1 ? matchIdx : -1;
|
||||
};
|
||||
|
||||
const seekWhenReady = () => {
|
||||
player.audio.currentTime = Math.min(
|
||||
seconds,
|
||||
Number.isFinite(player.audio.duration) ? player.audio.duration : seconds
|
||||
);
|
||||
player.audio.play();
|
||||
player.updateTimeline();
|
||||
};
|
||||
|
||||
updateTrackState();
|
||||
|
||||
if (player.currentRelUrl === relUrl) {
|
||||
if (player.audio.readyState >= 1) {
|
||||
seekWhenReady();
|
||||
} else {
|
||||
player.audio.addEventListener('loadedmetadata', seekWhenReady, { once: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
player.audio.addEventListener('loadedmetadata', seekWhenReady, { once: true });
|
||||
|
||||
const trackLink = document.querySelector(`.play-file[data-url="${relUrl}"]`);
|
||||
if (trackLink) {
|
||||
trackLink.click();
|
||||
} else {
|
||||
player.loadTrack(relUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Global variable for gallery images (updated from current folder)
|
||||
let currentGalleryImages = [];
|
||||
|
||||
@ -231,7 +305,7 @@ function renderContent(data) {
|
||||
contentHTML += `<li class="file-item">
|
||||
<a href="#" class="play-file"${indexAttr} data-url="${file.path}" data-file-type="${file.file_type}">${symbol} ${file.name.replace('.mp3', '')}</a>`;
|
||||
if (file.has_transcript) {
|
||||
contentHTML += `<a href="#" class="show-transcript" data-url="${file.transcript_url}" title="Show Transcript">📄</a>`;
|
||||
contentHTML += `<a href="#" class="show-transcript" data-url="${file.transcript_url}" data-audio-url="${file.path}" title="Show Transcript">📄</a>`;
|
||||
}
|
||||
contentHTML += `</li>`;
|
||||
});
|
||||
@ -424,6 +498,7 @@ document.querySelectorAll('.play-file').forEach(link => {
|
||||
|
||||
// Update the current music index.
|
||||
currentMusicIndex = index !== undefined ? parseInt(index) : -1;
|
||||
currentTrackPath = relUrl;
|
||||
|
||||
// Mark the clicked item as currently playing.
|
||||
this.closest('.file-item').classList.add('currently-playing');
|
||||
@ -452,7 +527,9 @@ document.querySelectorAll('.play-file').forEach(link => {
|
||||
link.addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
const url = this.getAttribute('data-url');
|
||||
const audioUrl = this.getAttribute('data-audio-url') || '';
|
||||
const highlight = this.getAttribute('highlight');
|
||||
activeTranscriptAudio = audioUrl;
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
@ -479,9 +556,11 @@ document.querySelectorAll('.play-file').forEach(link => {
|
||||
}
|
||||
}
|
||||
document.getElementById('transcriptContent').innerHTML = marked.parse(data);
|
||||
attachTranscriptTimecodes(audioUrl);
|
||||
document.getElementById('transcriptModal').style.display = 'block';
|
||||
})
|
||||
.catch(error => {
|
||||
activeTranscriptAudio = '';
|
||||
document.getElementById('transcriptContent').innerHTML = '<p>Error loading transcription.</p>';
|
||||
document.getElementById('transcriptModal').style.display = 'block';
|
||||
});
|
||||
|
||||
@ -18,7 +18,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<p><button onclick="window.open('/path/${file.relative_path}', '_self');" class="btn btn-light btn-sm" style="width:100%;">📁 ${parentFolder}</button></p>
|
||||
<p class="card-text">Anzahl Downloads: ${file.hitcount}</p>
|
||||
${ file.performance_date !== undefined ? `<p class="card-text">Datum: ${file.performance_date}</p>` : ``}
|
||||
${ file.transcript_hits !== undefined ? `<p class="card-text">Treffer im Transkript: ${file.transcript_hits} <a href="#" class="show-transcript" data-url="${transcriptURL}" highlight="${file.query}">📄</a></p>` : ``}
|
||||
${ file.transcript_hits !== undefined ? `<p class="card-text">Treffer im Transkript: ${file.transcript_hits} <a href="#" class="show-transcript" data-url="${transcriptURL}" data-audio-url="${file.relative_path}" highlight="${file.query}">📄</a></p>` : ``}
|
||||
</div>
|
||||
`;
|
||||
resultsDiv.appendChild(card);
|
||||
@ -162,4 +162,4 @@ function syncThemeColor() {
|
||||
}
|
||||
|
||||
// sync once on load
|
||||
document.addEventListener('DOMContentLoaded', syncThemeColor);
|
||||
document.addEventListener('DOMContentLoaded', syncThemeColor);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user