356 lines
16 KiB
JavaScript
356 lines
16 KiB
JavaScript
function initSearch() {
|
|
const form = document.getElementById('searchForm');
|
|
if (!form || form.dataset.bound) return;
|
|
form.dataset.bound = '1';
|
|
const esc = (value) => {
|
|
if (window.escapeHtml) return window.escapeHtml(value);
|
|
return String(value ?? '').replace(/[&<>"']/g, (char) => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
}[char]));
|
|
};
|
|
const encodePath = (path) => {
|
|
const raw = String(path || '');
|
|
if (typeof encodeSubpath === 'function') return encodeSubpath(raw);
|
|
return encodeURI(raw);
|
|
};
|
|
|
|
// Function to render search results from a response object
|
|
function renderResults(data) {
|
|
const resultsDiv = document.getElementById('results');
|
|
const results = Array.isArray(data.results) ? data.results : [];
|
|
const total = Number.isFinite(data.total) ? data.total : results.length;
|
|
const shown = results.length;
|
|
const remaining = Math.max(0, total - shown);
|
|
resultsDiv.innerHTML = `<h5>${total} Treffer</h5>`;
|
|
|
|
const audioExts = ['.mp3', '.wav', '.ogg', '.m4a', '.flac'];
|
|
|
|
if (results.length > 0) {
|
|
results.forEach(file => {
|
|
const card = document.createElement('div');
|
|
const relativePath = String(file.relative_path || '');
|
|
const filename = String(file.filename || '');
|
|
const filenameWithoutExtension = filename.split('.').slice(0, -1).join('.');
|
|
const parentFolder = relativePath.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' || filename.toLowerCase().endsWith('.sng');
|
|
const encodedRelPath = encodePath(relativePath);
|
|
const fulltextType = file.fulltext_type || 'transcript';
|
|
const fulltextUrl = file.fulltext_url || transcriptURL;
|
|
const fulltextLabel = 'Treffer im Volltext';
|
|
const fulltextAction = fulltextType === 'sng'
|
|
? `<a href="#" class="open-sng-link" data-path="${esc(relativePath)}" title="Liedtext öffnen"><i class="bi bi-journal-text"></i></a>`
|
|
: `<a href="#" class="show-transcript" data-url="${esc(fulltextUrl)}" data-audio-url="${esc(relativePath)}" highlight="${esc(file.query || '')}"><i class="bi bi-journal-text"></i></a>`;
|
|
|
|
card.className = 'card';
|
|
let fileAction = '';
|
|
if (isSng) {
|
|
fileAction = `<button class="btn btn-light open-sng-btn" data-path="${esc(relativePath)}" style="width:100%;"><i class="bi bi-music-note-list"></i> ${esc(filenameWithoutExtension)}</button>`;
|
|
} else if (isAudio) {
|
|
fileAction = `<button class="btn btn-light play-audio-btn" data-path="${esc(relativePath)}" style="width:100%;"><i class="bi bi-volume-up"></i> ${esc(filenameWithoutExtension)}</button>`;
|
|
} else {
|
|
fileAction = `<a class="btn btn-light download-btn" href="/media/${encodedRelPath}" download style="width:100%;"><i class="bi bi-download"></i> ${esc(filename)}</a>`;
|
|
}
|
|
|
|
const hitcount = Number(file.hitcount);
|
|
const safeHitcount = Number.isFinite(hitcount) ? hitcount : 0;
|
|
const transcriptHits = Number(file.transcript_hits);
|
|
const safeTranscriptHits = Number.isFinite(transcriptHits) ? transcriptHits : 0;
|
|
const perfDate = (file.performance_date !== undefined && file.performance_date !== null)
|
|
? String(file.performance_date).trim()
|
|
: '';
|
|
|
|
card.innerHTML = `
|
|
<div class="card-body">
|
|
<p>${fileAction}</p>
|
|
<p><button class="btn btn-light btn-sm folder-open-btn" data-folder="${esc(parentFolder)}" data-file="${esc(relativePath)}" style="width:100%;"><i class="bi bi-folder"></i> ${esc(parentFolder || 'Ordner')}</button></p>
|
|
<p class="card-text">Anzahl Downloads: ${safeHitcount}</p>
|
|
${ perfDate !== '' ? `<p class="card-text">Datum: ${esc(perfDate)}</p>` : ``}
|
|
${ file.transcript_hits !== undefined ? `<p class="card-text">${fulltextLabel}: ${safeTranscriptHits} ${fulltextAction}</p>` : ``}
|
|
</div>
|
|
`;
|
|
resultsDiv.appendChild(card);
|
|
});
|
|
if (remaining > 0) {
|
|
const more = document.createElement('p');
|
|
more.className = 'text-muted';
|
|
more.textContent = `und weitere ${remaining} Treffer`;
|
|
resultsDiv.appendChild(more);
|
|
}
|
|
attachEventListeners();
|
|
attachSearchFolderButtons();
|
|
attachSearchAudioButtons();
|
|
attachSearchSngButtons();
|
|
attachSearchFulltextButtons();
|
|
} else {
|
|
resultsDiv.innerHTML = `<h5>${total} Treffer</h5><p>Keine Treffer gefunden.</p>`;
|
|
}
|
|
}
|
|
|
|
// Restore previous search response if available from localStorage
|
|
const previousResponse = localStorage.getItem("searchResponse");
|
|
if (previousResponse) {
|
|
try {
|
|
const data = JSON.parse(previousResponse);
|
|
renderResults(data);
|
|
} catch (e) {
|
|
console.error('Error parsing searchResponse from localStorage:', e);
|
|
}
|
|
}
|
|
|
|
// Restore previous search word (Suchwort) if available
|
|
const previousQuery = localStorage.getItem("searchQuery");
|
|
if (previousQuery) {
|
|
document.getElementById('query').value = previousQuery;
|
|
}
|
|
|
|
// Restore previous selected category if available, otherwise default remains "Alles"
|
|
const previousCategory = localStorage.getItem("searchCategory");
|
|
if (previousCategory !== null) {
|
|
const radio = document.querySelector('input[name="category"][value="' + previousCategory + '"]');
|
|
if (radio) {
|
|
radio.checked = true;
|
|
}
|
|
}
|
|
|
|
// Restore previously selected filetypes (multi-select). Default to audio if none stored.
|
|
const previousFiletypes = localStorage.getItem("searchFiletypes");
|
|
if (previousFiletypes) {
|
|
try {
|
|
const list = JSON.parse(previousFiletypes);
|
|
document.querySelectorAll('input[name=\"filetype\"]').forEach(cb => {
|
|
cb.checked = list.includes(cb.value);
|
|
});
|
|
} catch (e) {
|
|
console.error('Error parsing stored filetypes', e);
|
|
document.getElementById('filetype-audio').checked = true;
|
|
}
|
|
} else {
|
|
document.getElementById('filetype-audio').checked = true;
|
|
}
|
|
|
|
// Restore the checkbox state for "Im Transkript suchen"
|
|
const previousIncludeTranscript = localStorage.getItem("searchIncludeTranscript");
|
|
if (previousIncludeTranscript !== null) {
|
|
document.getElementById('includeTranscript').checked = (previousIncludeTranscript === 'true');
|
|
}
|
|
|
|
// Form submission event
|
|
form.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const query = document.getElementById('query').value.trim();
|
|
const includeTranscript = document.getElementById('includeTranscript').checked;
|
|
const spinnerTimer = setTimeout(() => {
|
|
if (typeof showSpinner === 'function') showSpinner();
|
|
}, 200);
|
|
|
|
// Get the selected category radio button, if any
|
|
const categoryRadio = document.querySelector('input[name="category"]:checked');
|
|
const category = categoryRadio ? categoryRadio.value : '';
|
|
|
|
// Get selected filetypes (allow multiple). Default to audio if none selected.
|
|
const filetypeCheckboxes = document.querySelectorAll('input[name=\"filetype\"]');
|
|
let filetypes = Array.from(filetypeCheckboxes).filter(cb => cb.checked).map(cb => cb.value);
|
|
if (filetypes.length === 0) {
|
|
// enforce audio as default when user unchecked all
|
|
document.getElementById('filetype-audio').checked = true;
|
|
filetypes = ['audio'];
|
|
}
|
|
|
|
// Prevent accidental re-selection of already selected radio buttons
|
|
const radios = document.querySelectorAll('input[name="category"]');
|
|
radios.forEach(radio => {
|
|
radio.addEventListener('mousedown', function(e) {
|
|
this.wasChecked = this.checked;
|
|
});
|
|
radio.addEventListener('click', function(e) {
|
|
if (this.wasChecked) {
|
|
this.checked = false;
|
|
this.wasChecked = false;
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Prepare form data for the fetch request
|
|
// Send the query and the category as separate parameters.
|
|
const formData = new FormData();
|
|
formData.append('query', query);
|
|
formData.append('category', category);
|
|
formData.append('folder', document.getElementById('folder').value);
|
|
formData.append('datefrom', document.getElementById('datefrom').value);
|
|
formData.append('dateto', document.getElementById('dateto').value);
|
|
formData.append('includeTranscript', includeTranscript);
|
|
filetypes.forEach(ft => formData.append('filetype', ft));
|
|
|
|
const settleSpinner = () => {
|
|
clearTimeout(spinnerTimer);
|
|
if (typeof hideSpinner === 'function') hideSpinner();
|
|
};
|
|
|
|
const fetchPromise = fetch('/searchcommand', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(async response => {
|
|
let data = {};
|
|
try {
|
|
data = await response.json();
|
|
} catch (err) {
|
|
data = {};
|
|
}
|
|
if (!response.ok) {
|
|
const message = (data && data.error) ? data.error : `Search failed (${response.status})`;
|
|
throw new Error(message);
|
|
}
|
|
return data;
|
|
})
|
|
.then(data => {
|
|
// Render the results
|
|
renderResults(data);
|
|
// Store the raw response in localStorage
|
|
try {
|
|
localStorage.setItem("searchResponse", JSON.stringify(data));
|
|
} catch (e) {
|
|
console.error('Error saving searchResponse to localStorage:', e);
|
|
}
|
|
// Save the search word, selected category, and checkbox state in localStorage
|
|
localStorage.setItem("searchQuery", query);
|
|
localStorage.setItem("searchCategory", category);
|
|
localStorage.setItem("searchFiletypes", JSON.stringify(filetypes));
|
|
localStorage.setItem("searchIncludeTranscript", includeTranscript);
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
const resultsDiv = document.getElementById('results');
|
|
if (resultsDiv) {
|
|
resultsDiv.innerHTML = `<div class="alert alert-danger">${esc(error.message || 'Search failed')}</div>`;
|
|
}
|
|
});
|
|
// Always clear/hide spinner once the request settles
|
|
if (typeof fetchPromise.finally === 'function') {
|
|
fetchPromise.finally(settleSpinner);
|
|
} else {
|
|
fetchPromise.then(settleSpinner, settleSpinner);
|
|
}
|
|
});
|
|
|
|
// Clear button event handler
|
|
document.getElementById('clearBtn').addEventListener('click', function() {
|
|
// Remove stored items
|
|
localStorage.removeItem("searchResponse");
|
|
localStorage.removeItem("searchQuery");
|
|
localStorage.removeItem("searchCategory");
|
|
localStorage.removeItem("searchFiletypes");
|
|
localStorage.removeItem("folder");
|
|
localStorage.removeItem("datefrom");
|
|
localStorage.removeItem("dateto");
|
|
localStorage.removeItem("searchIncludeTranscript");
|
|
// Reset form fields to defaults
|
|
document.getElementById('query').value = '';
|
|
document.querySelector('input[name="category"][value=""]').checked = true;
|
|
const otherRadios = document.querySelectorAll('input[name="category"]:not([value=""])');
|
|
otherRadios.forEach(radio => radio.checked = false);
|
|
document.getElementById('filetype-audio').checked = true;
|
|
const otherFiletypeBoxes = document.querySelectorAll('input[name=\"filetype\"]:not([value=\"audio\"])');
|
|
otherFiletypeBoxes.forEach(cb => cb.checked = false);
|
|
document.getElementById('folder').value = ''; // Reset to "Alle"
|
|
document.getElementById('datefrom').value = ''; // Reset date from
|
|
document.getElementById('dateto').value = ''; // Reset date to
|
|
document.getElementById('includeTranscript').checked = false;
|
|
// Clear the results div
|
|
document.getElementById('results').innerHTML = '';
|
|
});
|
|
|
|
// Back button event handler - redirect to the root path
|
|
document.getElementById('backBtn').addEventListener('click', function() {
|
|
// window.location.href = '/';
|
|
viewMain();
|
|
});
|
|
}
|
|
|
|
function attachSearchFolderButtons() {
|
|
document.querySelectorAll('.folder-open-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const folder = btn.dataset.folder;
|
|
const file = btn.dataset.file;
|
|
openFolderAndHighlight(folder, file);
|
|
});
|
|
});
|
|
}
|
|
|
|
function attachSearchAudioButtons() {
|
|
document.querySelectorAll('.play-audio-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const path = btn.dataset.path;
|
|
if (typeof player !== 'undefined' && player && typeof player.loadTrack === 'function') {
|
|
player.loadTrack(path);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
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 attachSearchFulltextButtons() {
|
|
document.querySelectorAll('.open-sng-link').forEach(link => {
|
|
link.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const path = link.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
|
|
viewMain();
|
|
loadDirectory(targetFolder).then(() => {
|
|
const cssValue = (window.CSS && typeof window.CSS.escape === 'function')
|
|
? window.CSS.escape(String(filePath || ''))
|
|
: String(filePath || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
const target = document.querySelector(`.play-file[data-url="${cssValue}"]`);
|
|
if (target) {
|
|
target.classList.add('search-highlight');
|
|
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
});
|
|
}
|
|
|
|
function syncThemeColor() {
|
|
// read the CSS variable from :root (or any selector)
|
|
const cssVar = getComputedStyle(document.documentElement)
|
|
.getPropertyValue('--dark-background').trim();
|
|
if (cssVar) {
|
|
document
|
|
.querySelector('meta[name="theme-color"]')
|
|
.setAttribute('content', cssVar);
|
|
}
|
|
}
|
|
|
|
// sync once on load
|
|
// syncThemeColor handled by app init.
|
|
|
|
|
|
window.initSearch = initSearch;
|