release search

This commit is contained in:
lelo 2025-04-05 10:12:37 +00:00
parent 57f61b9675
commit b22ff260d3
4 changed files with 199 additions and 45 deletions

View File

@ -1,5 +1,6 @@
import sqlite3 import sqlite3
from flask import Flask, render_template, request, request, jsonify from flask import Flask, render_template, request, request, jsonify
import os
app = Flask(__name__) app = Flask(__name__)
@ -15,7 +16,6 @@ def searchcommand():
cursor = search_db.cursor() cursor = search_db.cursor()
if not include_transcript: if not include_transcript:
# Simple search: all words must be in either relative_path or filename.
conditions = [] conditions = []
params = [] params = []
for word in words: for word in words:
@ -24,6 +24,7 @@ def searchcommand():
sql = "SELECT * FROM files" sql = "SELECT * FROM files"
if conditions: if conditions:
sql += " WHERE " + " AND ".join(conditions) sql += " WHERE " + " AND ".join(conditions)
sql += " ORDER BY hitcount DESC"
cursor.execute(sql, params) cursor.execute(sql, params)
raw_results = cursor.fetchall() raw_results = cursor.fetchall()
results = [dict(row) for row in raw_results] results = [dict(row) for row in raw_results]
@ -46,13 +47,16 @@ def searchcommand():
transcript = result.get("transcript") or "" transcript = result.get("transcript") or ""
total_hits = sum(transcript.lower().count(word.lower()) for word in words) total_hits = sum(transcript.lower().count(word.lower()) for word in words)
result["transcript_hits"] = total_hits result["transcript_hits"] = total_hits
result["transcript"] = None # Remove full transcript if needed. result["transcript"] = None
results.append(result) results.append(result)
# Sort results so files with more transcript hits are on top. # Sort results so files with more transcript hits are on top.
results.sort(key=lambda x: x["transcript_hits"], reverse=True) results.sort(key=lambda x: x["transcript_hits"], reverse=True)
results = results[:100]
return jsonify(results=results) return jsonify(results=results)
def search(): def search():
return render_template('search.html') title_short = os.environ.get('TITLE_SHORT', 'Default Title')
title_long = os.environ.get('TITLE_LONG', 'Default Title')
return render_template("search.html", title_short=title_short, title_long=title_long)

View File

@ -84,13 +84,13 @@ li {
} }
/* mouse symbol for links */ /* mouse symbol for links */
div.directory-item, li.directory-item, li.file-item, div.directory-item, li.directory-item, li.file-item, li.link-item,
div.directory-item a, li.directory-item a, li.file-item a { div.directory-item a, li.directory-item a, li.file-item a, li.link-item a {
cursor: pointer; cursor: pointer;
} }
/* Directory Items (in a list) */ /* Directory Items (in a list) */
.directory-item { .directory-item, .link-item {
padding: 15px; padding: 15px;
} }
@ -122,6 +122,7 @@ div.directory-item a, li.directory-item a, li.file-item a {
/* Link Styles */ /* Link Styles */
.directory-link, .directory-link,
.link-link,
a.play-file { a.play-file {
color: #34495e; color: #34495e;
text-decoration: none; text-decoration: none;

View File

@ -63,6 +63,9 @@ function renderContent(data) {
data.directories.forEach(dir => { data.directories.forEach(dir => {
contentHTML += `<li class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></li>`; contentHTML += `<li class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></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 += '</ul>'; contentHTML += '</ul>';
} }
} }

View File

@ -8,73 +8,108 @@
<meta property="og:image" content="https://app.bethaus-speyer.de/static/icons/logo-200x200.png" /> <meta property="og:image" content="https://app.bethaus-speyer.de/static/icons/logo-200x200.png" />
<meta property="og:url" content="https://app.bethaus-speyer.de" /> <meta property="og:url" content="https://app.bethaus-speyer.de" />
<title>Dateisuche</title> <title>{{ title_short }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <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="description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft.">
<meta name="author" content="Bethaus Speyer"> <meta name="author" content="Bethaus Speyer">
<link rel="icon" href="/static/icons/logo-192x192.png" type="image/png" sizes="192x192"> <link rel="icon" href="/static/icons/logo-192x192.png" type="image/png" sizes="192x192">
<!-- Bootstrap CSS for modern styling -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- Web App Manifest -->
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}"> <link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<style>
body { <!-- Android Theme Color -->
background-color: #f8f9fa; <meta name="theme-color" content="#34495e">
}
.search-container { <!-- Apple-specific tags -->
margin-top: 50px; <link rel="touch-icon" href="{{ url_for('static', filename='icons/icon-192x192.png') }}">
} <meta name="mobile-web-app-capable" content="yes">
.card { <meta name="mobile-web-app-status-bar-style" content="default">
margin-bottom: 20px; <meta name="mobile-web-app-title" content="Gottesdienste">
}
</style> <!-- Your CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
<!-- Bootstrap CSS for modern styling -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
body { background-color: #f8f9fa; }
.site-header { width: 100%; }
.card { margin-bottom: 20px; }
</style>
</head> </head>
<body> <body>
<header class="site-header"> <header class="site-header">
<a href="#"> <a href="/">
<img src="/static/logoW.png" alt="Logo" class="logo"> <img src="/static/logoW.png" alt="Logo" class="logo">
</a> </a>
<h1>Suche</h1> <h1>{{ title_long }}</h1>
</header> </header>
<div class="container search-container"> <div class="container search-container">
<h1>Suche</h1>
<form id="searchForm" method="post" class="mb-4"> <form id="searchForm" method="post" class="mb-4">
<div class="mb-3"> <div class="mb-3">
<label for="query" class="form-label">Suchwörter:</label> <label for="query" class="form-label">Suchwörter:</label>
<input type="text" id="query" name="query" class="form-control" required> <input type="text" id="query" name="query" class="form-control" required>
</div> </div>
<!-- Radio Button for Kategorie -->
<div class="mb-3">
<label class="form-label">Kategorie:</label>
<div>
<div class="form-check form-check-inline">
<!-- "Alles" is default if nothing is selected -->
<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>
<!-- Checkbox for transcript search -->
<div class="form-check mb-3"> <div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="includeTranscript" name="includeTranscript"> <input type="checkbox" class="form-check-input" id="includeTranscript" name="includeTranscript">
<label class="form-check-label" for="includeTranscript">Im Transkript suchen</label> <label class="form-check-label" for="includeTranscript">Im Transkript suchen</label>
</div> </div>
<button type="submit" class="btn btn-primary">Suchen</button> <button type="submit" class="btn btn-primary">Suchen</button>
<!-- Clear Button -->
<button type="button" id="clearBtn" class="btn btn-secondary ms-2">neue Suche</button>
<!-- Back Button -->
<button type="button" id="backBtn" class="btn btn-secondary ms-2">zurück</button>
</form> </form>
<!-- Container for AJAX-loaded results --> <!-- Container for AJAX-loaded results -->
<div id="results"></div> <div id="results"></div>
</div> </div>
<script> <script>
document.getElementById('searchForm').addEventListener('submit', function(e) { document.addEventListener('DOMContentLoaded', function() {
e.preventDefault();
const query = document.getElementById('query').value.trim(); // Function to render search results from a response object
const includeTranscript = document.getElementById('includeTranscript').checked; function renderResults(data) {
// Prepare form data
const formData = new FormData();
formData.append('query', query);
formData.append('includeTranscript', includeTranscript);
fetch('/searchcommand', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
const resultsDiv = document.getElementById('results'); const resultsDiv = document.getElementById('results');
resultsDiv.innerHTML = ''; // Clear previous results resultsDiv.innerHTML = ''; // Clear previous results
if (data.results && data.results.length > 0) { if (data.results && data.results.length > 0) {
data.results.forEach(file => { data.results.forEach(file => {
// Create a card element for each result
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'card'; card.className = 'card';
card.innerHTML = ` card.innerHTML = `
@ -83,7 +118,10 @@ document.getElementById('searchForm').addEventListener('submit', function(e) {
<a href="/path/${file.relative_path}" target="_blank">${file.filename}</a> <a href="/path/${file.relative_path}" target="_blank">${file.filename}</a>
</h5> </h5>
<h6 class="card-subtitle mb-2 text-muted">${file.relative_path}</h6> <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>` : ''} ${ file.transcript_hits !== undefined
? `<p class="card-text">Treffer im Transkript: ${file.transcript_hits}</p>`
: `<p class="card-text">Downloads: ${file.hitcount}</p>`
}
</div> </div>
`; `;
resultsDiv.appendChild(card); resultsDiv.appendChild(card);
@ -91,9 +129,117 @@ document.getElementById('searchForm').addEventListener('submit', function(e) {
} else { } else {
resultsDiv.innerHTML = '<p>No results found.</p>'; resultsDiv.innerHTML = '<p>No results found.</p>';
} }
}) }
.catch(error => {
console.error('Error:', error); // 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 : '';
// Append the category to the search query if selected
const fullQuery = category ? query + " " + category : query;
// 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
const formData = new FormData();
formData.append('query', fullQuery);
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("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('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 = '/';
}); });
}); });
</script> </script>