preview for Liedtext

This commit is contained in:
lelo 2026-01-26 18:21:36 +00:00
parent 6f761435f3
commit 6cc1fb8d56
6 changed files with 282 additions and 5 deletions

View File

@ -15,9 +15,11 @@ with open("app_config.json", 'r') as file:
FILETYPE_GROUPS = {
'audio': ('.mp3', '.wav', '.ogg', '.m4a', '.flac'),
'video': ('.mp4', '.mov', '.mkv', '.avi', '.webm'),
'image': ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff')
'image': ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff'),
'liedtext': ('.sng',)
}
ALL_GROUP_EXTS = tuple(sorted({ext for group in FILETYPE_GROUPS.values() for ext in group}))
ALL_GROUP_KEYS = set(FILETYPE_GROUPS.keys())
def searchcommand():
query = request.form.get("query", "").strip()
@ -88,7 +90,7 @@ def searchcommand():
include_other = 'other' in filetypes
# If not all groups selected, apply filter
if set(filetypes) != {'audio', 'video', 'image', 'other'}:
if set(filetypes) != (ALL_GROUP_KEYS | {'other'}):
clauses = []
if selected_groups:
ext_list = tuple({ext for g in selected_groups for ext in FILETYPE_GROUPS[g]})

View File

@ -457,6 +457,83 @@ footer .audio-player-container {
overflow-x: auto;
}
#transcriptContent .song-sheet {
display: flex;
flex-direction: column;
gap: 12px;
}
#transcriptContent .song-header h2 {
margin: 0;
font-size: 22px;
}
#transcriptContent .song-meta {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px;
padding: 10px 12px;
background: #f8fbff;
border: 1px solid var(--border-color);
border-radius: 10px;
}
#transcriptContent .song-meta-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted-text);
margin-bottom: 4px;
}
#transcriptContent .song-meta-value {
font-weight: 600;
color: var(--brand-ink);
}
#transcriptContent .song-chip-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
#transcriptContent .song-chip {
padding: 2px 8px;
border-radius: 999px;
background: #eef4ff;
color: var(--brand-ink);
font-size: 12px;
font-weight: 600;
}
#transcriptContent .song-section {
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 12px;
background: #fff;
box-shadow: var(--card-shadow);
}
#transcriptContent .song-section h4 {
margin: 0 0 8px;
font-size: 16px;
color: var(--brand-navy);
}
#transcriptContent .song-section p {
margin: 0;
white-space: pre-line;
line-height: 1.6;
}
#transcriptContent .song-translation {
color: var(--muted-text);
}
#transcriptContent .song-small {
font-size: 0.9em;
}
/* Center the reload control */
#directory-controls {

View File

@ -307,6 +307,8 @@ function renderContent(data) {
currentMusicFiles.push({ path: file.path, index: idx, title: file.name.replace('.mp3', '') });
} else if (file.file_type === 'image') {
symbol = '<i class="bi bi-image"></i>';
} else if ((file.name || '').toLowerCase().endsWith('.sng')) {
symbol = '<i class="bi bi-music-note-list"></i>';
}
const indexAttr = file.file_type === 'music' ? ` data-index="${currentMusicFiles.length - 1}"` : '';
contentHTML += `<li class="file-item">
@ -485,6 +487,12 @@ document.querySelectorAll('.play-file').forEach(link => {
const { fileType, url: relUrl, index } = this.dataset;
const now = Date.now();
const lowerUrl = (relUrl || '').toLowerCase();
if (lowerUrl.endsWith('.sng')) {
openSngModal(relUrl);
return;
}
if (fileType === 'music') {
// If this is the same track already loaded, ignore to avoid extra GETs and unselects.
@ -609,6 +617,167 @@ document.querySelectorAll('.play-file').forEach(link => {
} // End of attachEventListeners function
function escapeHtml(text) {
return (text || '').replace(/[&<>"']/g, (char) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[char]));
}
function formatSngLine(line) {
let safe = escapeHtml(line);
safe = safe.replace(/&lt;dfn&gt;/gi, '<span class="song-translation">');
safe = safe.replace(/&lt;\/dfn&gt;/gi, '</span>');
safe = safe.replace(/&lt;small&gt;/gi, '<span class="song-small">');
safe = safe.replace(/&lt;\/small&gt;/gi, '</span>');
return safe;
}
function parseSngText(rawText) {
const text = (rawText || '').replace(/\r/g, '');
const lines = text.split('\n');
const meta = {};
const sections = [];
let current = null;
let inBody = false;
const pushCurrent = () => {
if (!current) return;
while (current.lines.length && current.lines[current.lines.length - 1].trim() === '') {
current.lines.pop();
}
if (current.title || current.lines.length) {
sections.push(current);
}
current = null;
};
for (const rawLine of lines) {
const line = rawLine.replace(/\uFEFF/g, '');
const trimmed = line.trim();
if (!inBody && trimmed.startsWith('#')) {
const idx = trimmed.indexOf('=');
if (idx > 1) {
const key = trimmed.slice(1, idx).trim();
const value = trimmed.slice(idx + 1).trim();
if (key) meta[key] = value;
}
continue;
}
if (trimmed === '---' || trimmed === '-----' || trimmed === '----') {
inBody = true;
pushCurrent();
continue;
}
if (!inBody && trimmed === '') {
continue;
}
inBody = true;
if (!current) {
if (!trimmed) continue;
current = { title: trimmed, lines: [] };
continue;
}
current.lines.push(line);
}
pushCurrent();
if (!sections.length) {
const bodyLines = lines
.filter(l => l && !l.trim().startsWith('#') && !['---', '-----', '----'].includes(l.trim()));
if (bodyLines.length) {
sections.push({ title: '', lines: bodyLines });
}
}
return { meta, sections };
}
function buildSngHtml(parsed, fallbackTitle) {
const meta = parsed.meta || {};
const sections = parsed.sections || [];
const title = meta.Title || fallbackTitle || 'Liedtext';
const metaRows = [];
const pushMeta = (label, value, asChips = false) => {
if (!value) return;
if (asChips) {
const chips = value
.split(',')
.map(v => v.trim())
.filter(Boolean)
.map(v => `<span class="song-chip">${escapeHtml(v)}</span>`)
.join('');
if (chips) {
metaRows.push(`<div><div class="song-meta-label">${escapeHtml(label)}</div><div class="song-chip-row">${chips}</div></div>`);
}
return;
}
metaRows.push(`<div><div class="song-meta-label">${escapeHtml(label)}</div><div class="song-meta-value">${escapeHtml(value)}</div></div>`);
};
pushMeta('Liederbuch', meta.Songbook);
pushMeta('ID', meta.ChurchSongID);
pushMeta('Stichworte', meta.Keywords, true);
pushMeta('Reihenfolge', meta.VerseOrder, true);
pushMeta('Sprache', meta.LangCount);
let html = '<div class="song-sheet">';
html += `<div class="song-header"><h2>${escapeHtml(title)}</h2></div>`;
if (metaRows.length) {
html += `<div class="song-meta">${metaRows.join('')}</div>`;
}
sections.forEach(section => {
const heading = section.title ? `<h4>${escapeHtml(section.title)}</h4>` : '';
const body = section.lines.map(l => formatSngLine(l)).join('<br>');
html += `<section class="song-section">${heading}<p>${body}</p></section>`;
});
html += '</div>';
return html;
}
function openSngModal(relUrl) {
if (!relUrl) return;
const modal = document.getElementById('transcriptModal');
const content = document.getElementById('transcriptContent');
if (!modal || !content) return;
const encodedPath = encodeSubpath(relUrl);
const filename = decodeURIComponent(relUrl.split('/').pop() || '').replace(/\.sng$/i, '');
content.innerHTML = '<p>Lade Liedtext…</p>';
modal.style.display = 'block';
fetch(`/media/${encodedPath}`)
.then(response => {
if (!response.ok) throw new Error('Laden fehlgeschlagen');
return response.arrayBuffer();
})
.then(buffer => {
let text = '';
try {
text = new TextDecoder('utf-8', { fatal: true }).decode(buffer);
} catch (err) {
text = new TextDecoder('windows-1252').decode(buffer);
}
const parsed = parseSngText(text);
content.innerHTML = buildSngHtml(parsed, filename);
})
.catch(() => {
content.innerHTML = '<p>Der Liedtext konnte nicht geladen werden.</p>';
});
}
window.openSngModal = openSngModal;
function initTranscriptModal() {
const modal = document.getElementById('transcriptModal');
if (!modal || modal.dataset.bound) return;

View File

@ -286,6 +286,10 @@
// Helper to insert entries sorted by time inside an existing day block
function addCalendarEntry(entry) {
if (!entry || !entry.date) return;
if (entry.id) {
const existing = document.querySelector(`.calendar-entry-row[data-id="${entry.id}"]`);
if (existing) return;
}
const block = document.querySelector(`.calendar-day[data-date="${entry.date}"]`);
if (!block) return;
@ -340,6 +344,7 @@
}
async function loadCalendarEntries() {
const fetchToken = (window._calendarFetchToken = (window._calendarFetchToken || 0) + 1);
const { start, end } = getCalendarRange();
const todayIso = toLocalISO(start);
const endIso = toLocalISO(end);
@ -351,6 +356,7 @@
const response = await fetch(`/api/calendar?start=${todayIso}&end=${endIso}`);
if (!response.ok) throw new Error('Fehler beim Laden');
const entries = await response.json();
if (fetchToken !== window._calendarFetchToken) return;
entries.forEach(entry => {
addLocationOption(entry.location);
addCalendarEntry(entry);

View File

@ -21,12 +21,18 @@ function initSearch() {
const parentFolder = file.relative_path.split('/').slice(0, -1).join('/');
const transcriptURL = '/transcript/' + parentFolder + '/Transkription/' + filenameWithoutExtension + '.md';
const isAudio = audioExts.includes((file.filetype || '').toLowerCase());
const isSng = (file.filetype || '').toLowerCase() === '.sng' || (file.filename || '').toLowerCase().endsWith('.sng');
const encodedRelPath = encodeURI(file.relative_path);
card.className = 'card';
const fileAction = isAudio
? `<button class="btn btn-light play-audio-btn" onclick="player.loadTrack('${file.relative_path}')" style="width:100%;"><i class="bi bi-volume-up"></i> ${filenameWithoutExtension}</button>`
: `<a class="btn btn-light download-btn" href="/media/${encodedRelPath}" download style="width:100%;"><i class="bi bi-download"></i> ${file.filename}</a>`;
let fileAction = '';
if (isSng) {
fileAction = `<button class="btn btn-light open-sng-btn" data-path="${file.relative_path}" style="width:100%;"><i class="bi bi-music-note-list"></i> ${filenameWithoutExtension}</button>`;
} else if (isAudio) {
fileAction = `<button class="btn btn-light play-audio-btn" onclick="player.loadTrack('${file.relative_path}')" style="width:100%;"><i class="bi bi-volume-up"></i> ${filenameWithoutExtension}</button>`;
} else {
fileAction = `<a class="btn btn-light download-btn" href="/media/${encodedRelPath}" download style="width:100%;"><i class="bi bi-download"></i> ${file.filename}</a>`;
}
card.innerHTML = `
<div class="card-body">
@ -47,6 +53,7 @@ function initSearch() {
}
attachEventListeners();
attachSearchFolderButtons();
attachSearchSngButtons();
} else {
resultsDiv.innerHTML = `<h5>${total} Treffer</h5><p>Keine Treffer gefunden.</p>`;
}
@ -229,6 +236,18 @@ function attachSearchFolderButtons() {
});
}
function attachSearchSngButtons() {
document.querySelectorAll('.open-sng-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const path = btn.dataset.path;
if (typeof window.openSngModal === 'function') {
window.openSngModal(path);
}
});
});
}
function openFolderAndHighlight(folderPath, filePath) {
const targetFolder = folderPath || '';
// Switch back to main view before loading folder

View File

@ -143,6 +143,10 @@
<input class="form-check-input" type="checkbox" name="filetype" id="filetype-image" value="image">
<label class="form-check-label" for="filetype-image">Bild</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="filetype" id="filetype-liedtext" value="liedtext">
<label class="form-check-label" for="filetype-liedtext">Liedtext</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="filetype" id="filetype-other" value="other">
<label class="form-check-label" for="filetype-other">Sonstige</label>