play files directly in search

This commit is contained in:
lelo 2025-05-10 23:17:24 +02:00
parent 47c5010434
commit de03e6f2b9
8 changed files with 403 additions and 446 deletions

1
app.py
View File

@ -44,7 +44,6 @@ app.add_url_rule('/connections', view_func=a.connections)
app.add_url_rule('/mylinks', view_func=auth.mylinks)
app.add_url_rule('/remove_secret', view_func=auth.remove_secret, methods=['POST'])
app.add_url_rule('/remove_token', view_func=auth.remove_token, methods=['POST'])
app.add_url_rule('/search', view_func=search.search, methods=['GET'])
app.add_url_rule('/searchcommand', view_func=search.searchcommand, methods=['POST'])
app.add_url_rule('/songs_dashboard', view_func=a.songs_dashboard)

View File

@ -5,9 +5,7 @@ import json
app = Flask(__name__)
SEARCH_DB_NAME = 'search.db'
search_db = sqlite3.connect(SEARCH_DB_NAME, check_same_thread=False)
search_db = sqlite3.connect('search.db', check_same_thread=False)
search_db.row_factory = sqlite3.Row
with open("app_config.json", 'r') as file:
@ -100,15 +98,3 @@ def searchcommand():
# Limit results
results = results[:100]
return jsonify(results=results)
def search():
allowed_basefolders = list(session['folders'].keys())
title_short = app_config.get('TITLE_SHORT', 'Default Title')
title_long = app_config.get('TITLE_LONG' , 'Default Title')
return render_template("search.html",
title_short=title_short,
title_long=title_long,
search_folders=allowed_basefolders
)

View File

@ -3,6 +3,26 @@ let currentMusicFiles = []; // Array of objects with at least { path, index }
let currentMusicIndex = -1; // Index of the current music file
let currentTrackPath = "";
// Cache common DOM elements
const mainContainer = document.querySelector('main');
const searchContainer = document.querySelector('search');
const footer = document.querySelector('footer');
console.log(mainContainer, searchContainer, footer);
function viewSearch() {
// Hide the main container and show the search container
mainContainer.style.display = 'none';
searchContainer.style.display = 'block';
}
function viewMain() {
// Hide the search container and show the main container
searchContainer.style.display = 'none';
mainContainer.style.display = 'block';
}
// Helper function: decode each segment then re-encode to avoid double encoding.
function encodeSubpath(subpath) {
if (!subpath) return '';
@ -100,7 +120,7 @@ function renderContent(data) {
</li>`;
});
if (data.breadcrumbs.length === 1) {
contentHTML += `<li class="link-item" onclick="window.location.href='/search'"><a href="/search" class="link-link">🔎 Suche</a></li>`;
contentHTML += `<li class="link-item" onclick="viewSearch()"><a onclick="viewSearch() class="link-link">🔎 Suche</a></li>`;
}
contentHTML += '</ul>';
}
@ -236,15 +256,8 @@ function attachEventListeners() {
});
});
// Cache common DOM elements
const nowPlayingInfo = document.getElementById('nowPlayingInfo');
const audioPlayer = document.getElementById('globalAudio');
const playerButton = document.querySelector('.player-button');
const audioPlayerContainer = document.getElementById('audioPlayerContainer');
const footer = document.querySelector('footer');
// Global variable to store the current fetch's AbortController.
let currentFetchController = null;
document.querySelectorAll('.play-file').forEach(link => {
link.addEventListener('click', async function (event) {
@ -261,85 +274,16 @@ document.querySelectorAll('.play-file').forEach(link => {
// Update the current music index.
currentMusicIndex = index !== undefined ? parseInt(index) : -1;
// Display the audio player container.
audioPlayerContainer.style.display = "block";
// Mark the clicked item as currently playing.
this.closest('.file-item').classList.add('currently-playing');
// Abort any previous fetch if still running.
if (currentFetchController) {
currentFetchController.abort();
}
currentFetchController = new AbortController();
startPlaying(relUrl);
// Pause the audio and clear its source.
audioPlayer.pause();
audioPlayer.src = '';
// Set a timeout to display a loader message if needed.
const loaderTimeout = setTimeout(() => {
playerButton.innerHTML = playIcon;
nowPlayingInfo.textContent = "Wird geladen...";
}, 500);
footer.style.display = 'flex';
const mediaUrl = `/media/${relUrl}`;
try {
// Perform a HEAD request to verify media availability.
const response = await fetch(mediaUrl, { method: 'HEAD', signal: currentFetchController.signal });
clearTimeout(loaderTimeout);
if (response.status === 403) {
nowPlayingInfo.textContent = "Fehler: Zugriff verweigert.";
window.location.href = '/';
return;
} else if (!response.ok) {
nowPlayingInfo.textContent = `Fehler: Unerwarteter Status (${response.status}).`;
console.error('Unexpected response status:', response.status);
return;
}
// Delay preloading to avoid blocking playback.
setTimeout(preload_audio, 1000);
// Set the media URL, load, and play the audio.
audioPlayer.src = mediaUrl;
audioPlayer.load();
await audioPlayer.play();
currentTrackPath = relUrl;
playerButton.innerHTML = pauseIcon;
// Process file path for display.
const pathParts = relUrl.split('/');
const folderName = pathParts[pathParts.length - 2];
const fileName = pathParts.pop();
const pathStr = pathParts.join('/');
// Update Media Session metadata if available.
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: currentMusicFiles[currentMusicIndex].title,
artist: folderName,
artwork: [
{ src: '/icons/logo-192x192.png', sizes: '192x192', type: 'image/png' }
]
});
}
nowPlayingInfo.innerHTML = pathStr.replace(/\//g, ' > ') +
'<br><span style="font-size: larger; font-weight: bold;">' +
fileName.replace('.mp3', '') + '</span>';
// Delay preloading to avoid blocking playback.
setTimeout(preload_audio, 1000);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Previous fetch aborted.');
} else {
console.error('Error fetching media:', error);
nowPlayingInfo.textContent = "Fehler: Netzwerkproblem oder ungültige URL.";
}
}
} else if (fileType === 'image') {
// Open the gallery modal for image files.
openGalleryModal(relUrl);

View File

@ -1,6 +1,11 @@
// read the CSS variable from :root (or any selector)
const cssVar = getComputedStyle(document.documentElement).getPropertyValue('--dark-background').trim();
// player DOM elements
const nowPlayingInfo = document.getElementById('nowPlayingInfo');
const audioPlayer = document.getElementById('globalAudio');
const audioPlayerContainer = document.getElementById('audioPlayerContainer');
const playerButton = document.querySelector('.player-button'),
audio = document.querySelector('audio'),
timeline = document.querySelector('.timeline'),
@ -118,4 +123,83 @@ async function downloadAudio() {
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// Global variable to store the current fetch's AbortController.
let currentFetchController = null;
async function startPlaying(relUrl) {
// Pause the audio and clear its source.
audioPlayer.pause();
audioPlayer.src = '';
// Display the audio player container.
audioPlayerContainer.style.display = "block";
// Set a timeout to display a loader message if needed.
const loaderTimeout = setTimeout(() => {
playerButton.innerHTML = playIcon;
nowPlayingInfo.textContent = "Wird geladen...";
}, 500);
footer.style.display = 'flex';
// Abort any previous fetch if still running.
if (currentFetchController) {
currentFetchController.abort();
}
currentFetchController = new AbortController();
const mediaUrl = `/media/${relUrl}`;
try {
// Perform a HEAD request to verify media availability.
const response = await fetch(mediaUrl, { method: 'HEAD', signal: currentFetchController.signal });
clearTimeout(loaderTimeout);
if (response.status === 403) {
nowPlayingInfo.textContent = "Fehler: Zugriff verweigert.";
window.location.href = '/';
return;
} else if (!response.ok) {
nowPlayingInfo.textContent = `Fehler: Unerwarteter Status (${response.status}).`;
console.error('Unexpected response status:', response.status);
return;
}
// Set the media URL, load, and play the audio.
audioPlayer.src = mediaUrl;
audioPlayer.load();
await audioPlayer.play();
currentTrackPath = relUrl;
playerButton.innerHTML = pauseIcon;
// Process file path for display.
const pathParts = relUrl.split('/');
const folderName = pathParts[pathParts.length - 2];
const fileName = pathParts.pop();
const pathStr = pathParts.join('/');
// Update Media Session metadata if available.
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: fileName.replace(/\.[^/.]+$/, ''), // remove extension
artist: folderName,
artwork: [
{ src: '/icons/logo-192x192.png', sizes: '192x192', type: 'image/png' }
]
});
}
nowPlayingInfo.innerHTML = pathStr.replace(/\//g, ' > ') +
'<br><span style="font-size: larger; font-weight: bold;">' +
fileName.replace('.mp3', '') + '</span>';
} catch (error) {
if (error.name === 'AbortError') {
console.log('Previous fetch aborted.');
} else {
console.error('Error fetching media:', error);
nowPlayingInfo.textContent = "Fehler: Netzwerkproblem oder ungültige URL.";
}
};
}

162
static/search.js Normal file
View File

@ -0,0 +1,162 @@
document.addEventListener('DOMContentLoaded', function() {
// Function to render search results from a response object
function renderResults(data) {
const resultsDiv = document.getElementById('results');
resultsDiv.innerHTML = '<h5>Suchergebnisse:</h5>';
if (data.results && data.results.length > 0) {
data.results.forEach(file => {
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
<div class="card-body">
<p><button class="btn btn-light" onclick="startPlaying('${file.relative_path}')" style="width:100%;">🔊 ${file.filename}</button></p>
<p><button onclick="window.open('/path/${file.relative_path}', '_self');" class="btn btn-light btn-sm" style="width:100%;">📁 ${file.relative_path}</button></p>
${ file.transcript_hits !== undefined
? `<p class="card-text">Treffer im Transkript: ${file.transcript_hits}</p>`
: `<p class="card-text">Downloads: ${file.hitcount}</p>`
}
</div>
`;
resultsDiv.appendChild(card);
});
} else {
resultsDiv.innerHTML = '<p>No results found.</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 the checkbox state for "Im Transkript suchen"
const previousIncludeTranscript = localStorage.getItem("searchIncludeTranscript");
if (previousIncludeTranscript !== null) {
document.getElementById('includeTranscript').checked = (previousIncludeTranscript === 'true');
}
// Form submission event
document.getElementById('searchForm').addEventListener('submit', function(e) {
e.preventDefault();
const query = document.getElementById('query').value.trim();
const includeTranscript = document.getElementById('includeTranscript').checked;
// Get the selected category radio button, if any
const categoryRadio = document.querySelector('input[name="category"]:checked');
const category = categoryRadio ? categoryRadio.value : '';
// 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);
fetch('/searchcommand', {
method: 'POST',
body: formData
})
.then(response => response.json())
.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("searchIncludeTranscript", includeTranscript);
})
.catch(error => {
console.error('Error:', error);
});
});
// Clear button event handler
document.getElementById('clearBtn').addEventListener('click', function() {
// Remove stored items
localStorage.removeItem("searchResponse");
localStorage.removeItem("searchQuery");
localStorage.removeItem("searchCategory");
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('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 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
document.addEventListener('DOMContentLoaded', syncThemeColor);

View File

@ -59,20 +59,134 @@
<a href="{{ url_for('folder_secret_config_editor') }}" id="edit-folder-config" >Ordnerkonfiguration</a>
</div>
{% endif %}
<div class="container">
<div id="breadcrumbs" class="breadcrumb"></div>
<div id="content"></div>
<div id="directory-controls">
<button id="reload-button" onclick="reloadDirectory()" title="Reload Directory">
<svg width="70px" height="70px" viewBox="0 0 24 24"xmlns="http://www.w3.org/2000/svg">
<path d="M16.2721 3.13079L17.4462 6.15342C17.6461 6.66824 17.3909 7.24768 16.8761 7.44764L13.8535 8.6217" stroke="#CCC" stroke-width="2.5" stroke-linecap="round"/>
<path d="M7.61555 20.5111L6.93391 17.3409C6.81782 16.801 7.16142 16.2692 7.70136 16.1531L10.8715 15.4714" stroke="#CCC" stroke-width="2.5" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.9153 7.86755C17.6501 7.5052 17.8749 6.55584 17.263 6.01119C16.4869 5.32046 15.5626 4.77045 14.5169 4.41551C10.3331 2.99538 5.79017 5.23581 4.37005 9.41964C4.01511 10.4653 3.88883 11.5335 3.96442 12.5696C4.02402 13.3867 4.9141 13.7862 5.64883 13.4239V13.4239C6.17327 13.1652 6.44536 12.5884 6.44406 12.0037C6.44274 11.4136 6.53712 10.8132 6.73739 10.2232C7.71372 7.34681 10.837 5.80651 13.7134 6.78285C14.3034 6.98311 14.8371 7.27371 15.3045 7.63397C15.7677 7.99095 16.3909 8.12619 16.9153 7.86755V7.86755ZM6.97575 16.1145C7.50019 15.8558 8.12343 15.991 8.58656 16.348C9.05394 16.7083 9.58773 16.9989 10.1777 17.1992C13.0541 18.1755 16.1774 16.6352 17.1537 13.7588C17.354 13.1688 17.4483 12.5684 17.447 11.9783C17.4457 11.3936 17.7178 10.8168 18.2423 10.5581V10.5581C18.977 10.1958 19.8671 10.5953 19.9267 11.4124C20.0022 12.4485 19.876 13.5167 19.521 14.5624C18.1009 18.7462 13.558 20.9866 9.37418 19.5665C8.32849 19.2116 7.4042 18.6615 6.62812 17.9708C6.01616 17.4262 6.24102 16.4768 6.97575 16.1145V16.1145Z" fill="#CCC"/>
</svg>
</button>
<main>
<div class="container">
<div id="breadcrumbs" class="breadcrumb"></div>
<div id="content"></div>
<div id="directory-controls">
<button id="reload-button" onclick="reloadDirectory()" title="Reload Directory">
<svg width="70px" height="70px" viewBox="0 0 24 24"xmlns="http://www.w3.org/2000/svg">
<path d="M16.2721 3.13079L17.4462 6.15342C17.6461 6.66824 17.3909 7.24768 16.8761 7.44764L13.8535 8.6217" stroke="#CCC" stroke-width="2.5" stroke-linecap="round"/>
<path d="M7.61555 20.5111L6.93391 17.3409C6.81782 16.801 7.16142 16.2692 7.70136 16.1531L10.8715 15.4714" stroke="#CCC" stroke-width="2.5" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.9153 7.86755C17.6501 7.5052 17.8749 6.55584 17.263 6.01119C16.4869 5.32046 15.5626 4.77045 14.5169 4.41551C10.3331 2.99538 5.79017 5.23581 4.37005 9.41964C4.01511 10.4653 3.88883 11.5335 3.96442 12.5696C4.02402 13.3867 4.9141 13.7862 5.64883 13.4239V13.4239C6.17327 13.1652 6.44536 12.5884 6.44406 12.0037C6.44274 11.4136 6.53712 10.8132 6.73739 10.2232C7.71372 7.34681 10.837 5.80651 13.7134 6.78285C14.3034 6.98311 14.8371 7.27371 15.3045 7.63397C15.7677 7.99095 16.3909 8.12619 16.9153 7.86755V7.86755ZM6.97575 16.1145C7.50019 15.8558 8.12343 15.991 8.58656 16.348C9.05394 16.7083 9.58773 16.9989 10.1777 17.1992C13.0541 18.1755 16.1774 16.6352 17.1537 13.7588C17.354 13.1688 17.4483 12.5684 17.447 11.9783C17.4457 11.3936 17.7178 10.8168 18.2423 10.5581V10.5581C18.977 10.1958 19.8671 10.5953 19.9267 11.4124C20.0022 12.4485 19.876 13.5167 19.521 14.5624C18.1009 18.7462 13.558 20.9866 9.37418 19.5665C8.32849 19.2116 7.4042 18.6615 6.62812 17.9708C6.01616 17.4262 6.24102 16.4768 6.97575 16.1145V16.1145Z" fill="#CCC"/>
</svg>
</button>
</div>
</div>
</div>
</main>
<search style="display: none;">
<div class="container">
<form id="searchForm" method="post" class="mb-4">
<!-- Suchwörter -->
<div class="mb-3">
<label for="query" class="h5 form-label">Suchwörter:</label>
<input type="text" id="query" name="query" class="form-control" required>
</div>
<!-- Toggle für Suchoptionen -->
<div class="d-flex align-items-center mb-2">
<h2 class="h5 mb-0">Suchoptionen</h2>
<button class="btn btn-sm btn-link p-0"
type="button"
data-bs-toggle="collapse"
data-bs-target="#searchOptions"
aria-expanded="false"
aria-controls="searchOptions"
aria-label="Toggle Suchoptionen">
<i class="bi bi-plus-lg"></i>
</button>
</div>
<!-- Suchoptionen einklappbar -->
<div id="searchOptions" class="collapse border rounded p-3 mb-3">
<!-- Kategorie -->
<div class="mb-3">
<label class="form-label">Kategorie:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="none" value="" checked>
<label class="form-check-label" for="none">Alles</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="lied" value="Lied">
<label class="form-check-label" for="lied">Lied</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="gedicht" value="Gedicht">
<label class="form-check-label" for="gedicht">Gedicht</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="predigt" value="Predigt">
<label class="form-check-label" for="predigt">Predigt</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="chor" value="chor">
<label class="form-check-label" for="chor">Chor</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="orchester" value="orchester">
<label class="form-check-label" for="orchester">Orchester</label>
</div>
</div>
</div>
<hr>
<!-- In Ordner suchen -->
<div class="mb-3">
<label for="folder" class="form-label">In Ordner suchen:</label>
<select id="folder" name="folder" class="form-select">
<option value="">Alle</option>
{% for folder in search_folders %}
<option value="{{ folder }}">{{ folder }}</option>
{% endfor %}
</select>
</div>
<hr>
<!-- Transkript durchsuchen -->
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="includeTranscript" name="includeTranscript">
<label class="form-check-label" for="includeTranscript">Im Transkript suchen</label>
</div>
<hr>
<!-- Zeitraum -->
<div class="row g-2 mb-3">
<div class="col-12 col-md-6">
<label for="datefrom" class="form-label">Datum von:</label>
<input type="date" id="datefrom" name="datefrom" class="form-control">
</div>
<div class="col-12 col-md-6">
<label for="dateto" class="form-label">Datum bis:</label>
<input type="date" id="dateto" name="dateto" class="form-control">
</div>
</div>
</div>
<!-- Ende Suchoptionen -->
<!-- Buttons -->
<div class="mb-3">
<button type="submit" class="btn btn-primary">Suchen</button>
<button type="button" id="clearBtn" class="btn btn-secondary ms-2">zurücksetzen</button>
<button type="button" id="backBtn" class="btn btn-secondary ms-2">beenden</button>
</div>
</form>
<!-- AJAX-loaded results -->
<div id="results"></div>
</div>
</search>
<!-- Global Audio Player in Footer -->
<footer>
<div class="audio-player-container" id="audioPlayerContainer">
@ -124,9 +238,12 @@
<div id="gallery-loader"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Load main app JS first, then gallery JS -->
<script src="{{ url_for('static', filename='app.js') }}"></script>
<script src="{{ url_for('static', filename='gallery.js') }}"></script>
<script src="{{ url_for('static', filename='search.js') }}"></script>
<script src="{{ url_for('static', filename='audioplayer.js') }}"></script>
<script>
if ('serviceWorker' in navigator) {

View File

@ -22,6 +22,7 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='theme.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
<script src="{{ url_for('static', filename='functions.js') }}"></script>
{% block head_extra %}{% endblock %}
</head>
@ -56,6 +57,5 @@
{% block scripts %}{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='functions.js') }}"></script>
</body>
</html>

View File

@ -1,335 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta property="og:title" content="{{ title_long }}" />
<meta property="og:description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft." />
<meta property="og:image" content="/icon/logo-200x200.png" />
<title>{{ title_short }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft.">
<meta name="author" content="{{ title_short }}">
<link rel="icon" href="/icon/logo-192x192.png" type="image/png" sizes="192x192">
<!-- Web App Manifest -->
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<!-- Android Theme Color -->
<meta name="theme-color" content="#000">
<!-- Apple-specific tags -->
<link rel="touch-icon" href="{{ url_for('static', filename='icons/icon-192x192.png') }}">
<meta name="mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-status-bar-style" content="default">
<meta name="mobile-web-app-title" content="Gottesdienste">
<!-- Bootstrap CSS for modern styling -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<!-- Your CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='theme.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
.btn {
margin-bottom: 10px;
}
.search-container {
padding: 15px;
}
</style>
</head>
<body>
<header class="site-header">
<a href="/">
<img src="/custom_logo/logoW.png" alt="Logo" class="logo">
</a>
<h1>{{ title_long }}</h1>
</header>
<div class="search-container">
<h1>Suche</h1>
<form id="searchForm" method="post" class="mb-4">
<!-- Suchwörter -->
<div class="mb-3">
<label for="query" class="h5 form-label">Suchwörter:</label>
<input type="text" id="query" name="query" class="form-control" required>
</div>
<!-- Toggle für Suchoptionen -->
<div class="d-flex align-items-center mb-2">
<h2 class="h5 mb-0">Suchoptionen</h2>
<button class="btn btn-sm btn-link p-0"
type="button"
data-bs-toggle="collapse"
data-bs-target="#searchOptions"
aria-expanded="false"
aria-controls="searchOptions"
aria-label="Toggle Suchoptionen">
<i class="bi bi-plus-lg"></i>
</button>
</div>
<!-- Suchoptionen einklappbar -->
<div id="searchOptions" class="collapse border rounded p-3 mb-3">
<!-- Kategorie -->
<div class="mb-3">
<label class="form-label">Kategorie:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="none" value="" checked>
<label class="form-check-label" for="none">Alles</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="lied" value="Lied">
<label class="form-check-label" for="lied">Lied</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="gedicht" value="Gedicht">
<label class="form-check-label" for="gedicht">Gedicht</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="predigt" value="Predigt">
<label class="form-check-label" for="predigt">Predigt</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="chor" value="chor">
<label class="form-check-label" for="chor">Chor</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="orchester" value="orchester">
<label class="form-check-label" for="orchester">Orchester</label>
</div>
</div>
</div>
<hr>
<!-- In Ordner suchen -->
<div class="mb-3">
<label for="folder" class="form-label">In Ordner suchen:</label>
<select id="folder" name="folder" class="form-select">
<option value="">Alle</option>
{% for folder in search_folders %}
<option value="{{ folder }}">{{ folder }}</option>
{% endfor %}
</select>
</div>
<hr>
<!-- Transkript durchsuchen -->
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="includeTranscript" name="includeTranscript">
<label class="form-check-label" for="includeTranscript">Im Transkript suchen</label>
</div>
<hr>
<!-- Zeitraum -->
<div class="row g-2 mb-3">
<div class="col-12 col-md-6">
<label for="datefrom" class="form-label">Datum von:</label>
<input type="date" id="datefrom" name="datefrom" class="form-control">
</div>
<div class="col-12 col-md-6">
<label for="dateto" class="form-label">Datum bis:</label>
<input type="date" id="dateto" name="dateto" class="form-control">
</div>
</div>
</div>
<!-- Ende Suchoptionen -->
<!-- Buttons -->
<div class="mb-3">
<button type="submit" class="btn btn-primary">Suchen</button>
<button type="button" id="clearBtn" class="btn btn-secondary ms-2">zurücksetzen</button>
<button type="button" id="backBtn" class="btn btn-secondary ms-2">beenden</button>
</div>
</form>
<!-- AJAX-loaded results -->
<div id="results"></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Function to render search results from a response object
function renderResults(data) {
const resultsDiv = document.getElementById('results');
resultsDiv.innerHTML = ''; // Clear previous results
if (data.results && data.results.length > 0) {
data.results.forEach(file => {
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
<div class="card-body">
<h5 class="card-title">
<a href="/path/${file.relative_path}" target="_blank">${file.filename}</a>
</h5>
<h6 class="card-subtitle mb-2 text-muted">${file.relative_path}</h6>
${ file.transcript_hits !== undefined
? `<p class="card-text">Treffer im Transkript: ${file.transcript_hits}</p>`
: `<p class="card-text">Downloads: ${file.hitcount}</p>`
}
</div>
`;
resultsDiv.appendChild(card);
});
} else {
resultsDiv.innerHTML = '<p>No results found.</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 the checkbox state for "Im Transkript suchen"
const previousIncludeTranscript = localStorage.getItem("searchIncludeTranscript");
if (previousIncludeTranscript !== null) {
document.getElementById('includeTranscript').checked = (previousIncludeTranscript === 'true');
}
// Form submission event
document.getElementById('searchForm').addEventListener('submit', function(e) {
e.preventDefault();
const query = document.getElementById('query').value.trim();
const includeTranscript = document.getElementById('includeTranscript').checked;
// Get the selected category radio button, if any
const categoryRadio = document.querySelector('input[name="category"]:checked');
const category = categoryRadio ? categoryRadio.value : '';
// 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);
fetch('/searchcommand', {
method: 'POST',
body: formData
})
.then(response => response.json())
.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("searchIncludeTranscript", includeTranscript);
})
.catch(error => {
console.error('Error:', error);
});
});
// Clear button event handler
document.getElementById('clearBtn').addEventListener('click', function() {
// Remove stored items
localStorage.removeItem("searchResponse");
localStorage.removeItem("searchQuery");
localStorage.removeItem("searchCategory");
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('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 = '/';
});
});
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
document.addEventListener('DOMContentLoaded', syncThemeColor);
</script>
<!-- Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>