diff --git a/app.py b/app.py index e79b22f..a7d0946 100755 --- a/app.py +++ b/app.py @@ -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) diff --git a/search.py b/search.py index 42c7cd0..41cad76 100644 --- a/search.py +++ b/search.py @@ -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 - ) - diff --git a/static/app.js b/static/app.js index 66206f3..37fed4c 100644 --- a/static/app.js +++ b/static/app.js @@ -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) { `; }); if (data.breadcrumbs.length === 1) { - contentHTML += ``; + contentHTML += ``; } contentHTML += ''; } @@ -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, ' > ') + - '
' + - fileName.replace('.mp3', '') + ''; - - // 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); diff --git a/static/audioplayer.js b/static/audioplayer.js index 11fff2d..219e1d1 100644 --- a/static/audioplayer.js +++ b/static/audioplayer.js @@ -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, ' > ') + + '
' + + fileName.replace('.mp3', '') + ''; + } 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."; + } + }; } \ No newline at end of file diff --git a/static/search.js b/static/search.js new file mode 100644 index 0000000..a66514c --- /dev/null +++ b/static/search.js @@ -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 = '
Suchergebnisse:
'; + + if (data.results && data.results.length > 0) { + data.results.forEach(file => { + const card = document.createElement('div'); + card.className = 'card'; + card.innerHTML = ` +
+

+

+ ${ file.transcript_hits !== undefined + ? `

Treffer im Transkript: ${file.transcript_hits}

` + : `

Downloads: ${file.hitcount}

` + } +
+ `; + resultsDiv.appendChild(card); + }); + } else { + resultsDiv.innerHTML = '

No results found.

'; + } + } + + // 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); \ No newline at end of file diff --git a/templates/app.html b/templates/app.html index 4b3b8f6..1eb2bec 100644 --- a/templates/app.html +++ b/templates/app.html @@ -59,20 +59,134 @@ Ordnerkonfiguration {% endif %} -
- -
-
- +
+
+ +
+
+ +
-
+ + +
+
+ + +
+ + +
+ + +
+

Suchoptionen

+ +
+ + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+ + +
+ +
+ + +
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + +
+ + + +
+ +
+ + +
+
+
+ + +