preview for Liedtext
This commit is contained in:
parent
6f761435f3
commit
6cc1fb8d56
@ -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]})
|
||||
|
||||
@ -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 {
|
||||
|
||||
169
static/app.js
169
static/app.js
@ -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) => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}[char]));
|
||||
}
|
||||
|
||||
function formatSngLine(line) {
|
||||
let safe = escapeHtml(line);
|
||||
safe = safe.replace(/<dfn>/gi, '<span class="song-translation">');
|
||||
safe = safe.replace(/<\/dfn>/gi, '</span>');
|
||||
safe = safe.replace(/<small>/gi, '<span class="song-small">');
|
||||
safe = safe.replace(/<\/small>/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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user