clickable timecode in transcript

This commit is contained in:
lelo 2026-01-14 16:38:34 +00:00
parent 566138dedc
commit 643f9b3908
3 changed files with 87 additions and 3 deletions

View File

@ -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;

View File

@ -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">&#128196;</a>`;
contentHTML += `<a href="#" class="show-transcript" data-url="${file.transcript_url}" data-audio-url="${file.path}" title="Show Transcript">&#128196;</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';
});

View File

@ -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}">&#128196;</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}">&#128196;</a></p>` : ``}
</div>
`;
resultsDiv.appendChild(card);
@ -162,4 +162,4 @@ function syncThemeColor() {
}
// sync once on load
document.addEventListener('DOMContentLoaded', syncThemeColor);
document.addEventListener('DOMContentLoaded', syncThemeColor);