diff --git a/search.py b/search.py index 387028f..e08211e 100644 --- a/search.py +++ b/search.py @@ -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]}) diff --git a/static/app.css b/static/app.css index e53a3e5..c25e1d0 100644 --- a/static/app.css +++ b/static/app.css @@ -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 { diff --git a/static/app.js b/static/app.js index 1fbdf42..8142c57 100644 --- a/static/app.js +++ b/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 = ''; + } else if ((file.name || '').toLowerCase().endsWith('.sng')) { + symbol = ''; } const indexAttr = file.file_type === 'music' ? ` data-index="${currentMusicFiles.length - 1}"` : ''; contentHTML += `
${body}
Lade Liedtext…
'; + 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 = 'Der Liedtext konnte nicht geladen werden.
'; + }); +} + +window.openSngModal = openSngModal; + function initTranscriptModal() { const modal = document.getElementById('transcriptModal'); if (!modal || modal.dataset.bound) return; diff --git a/static/calendar.js b/static/calendar.js index 843f58f..ce9e09f 100644 --- a/static/calendar.js +++ b/static/calendar.js @@ -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); diff --git a/static/search.js b/static/search.js index 9173b18..741651f 100644 --- a/static/search.js +++ b/static/search.js @@ -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 - ? `` - : ` ${file.filename}`; + let fileAction = ''; + if (isSng) { + fileAction = ``; + } else if (isAudio) { + fileAction = ``; + } else { + fileAction = ` ${file.filename}`; + } card.innerHTML = `Keine Treffer gefunden.
`; } @@ -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 diff --git a/templates/app.html b/templates/app.html index 53956bb..5e26aa0 100644 --- a/templates/app.html +++ b/templates/app.html @@ -143,6 +143,10 @@