diff --git a/.gitignore b/.gitignore index 7aaba4f..4369b33 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ /app_config.json /custom_logo /static/theme.css -/transcription_config.yml \ No newline at end of file +/transcription_config.yml + +/messages.json \ No newline at end of file diff --git a/app.py b/app.py index 643ea47..bf2d4f3 100755 --- a/app.py +++ b/app.py @@ -24,6 +24,7 @@ from pathlib import Path import re import qrcode import base64 +import json import search import auth import analytics as a @@ -66,6 +67,80 @@ app.add_url_rule('/admin/search_db_analyzer', view_func=auth.require_admin(sdb.s app.add_url_rule('/admin/search_db_analyzer/query', view_func=auth.require_admin(sdb.search_db_query), methods=['POST']) app.add_url_rule('/admin/search_db_analyzer/folders', view_func=auth.require_admin(sdb.search_db_folders)) +# Messages/News routes +MESSAGES_FILENAME = 'messages.json' + +def load_messages(): + """Load messages from JSON file.""" + try: + with open(MESSAGES_FILENAME, 'r', encoding='utf-8') as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return [] + +def save_messages(messages): + """Save messages to JSON file.""" + with open(MESSAGES_FILENAME, 'w', encoding='utf-8') as f: + json.dump(messages, f, ensure_ascii=False, indent=2) + +@app.route('/api/messages', methods=['GET']) +@auth.require_secret +def get_messages(): + """Get all messages.""" + messages = load_messages() + # Sort by datetime descending (newest first) + messages.sort(key=lambda x: x.get('datetime', ''), reverse=True) + return jsonify(messages) + +@app.route('/api/messages', methods=['POST']) +@auth.require_admin +def create_message(): + """Create a new message.""" + data = request.get_json() + messages = load_messages() + + # Generate new ID + new_id = max([m.get('id', 0) for m in messages], default=0) + 1 + + new_message = { + 'id': new_id, + 'title': data.get('title', ''), + 'content': data.get('content', ''), + 'datetime': data.get('datetime', datetime.now().isoformat()) + } + + messages.append(new_message) + save_messages(messages) + + return jsonify(new_message), 201 + +@app.route('/api/messages/', methods=['PUT']) +@auth.require_admin +def update_message(message_id): + """Update an existing message.""" + data = request.get_json() + messages = load_messages() + + for message in messages: + if message['id'] == message_id: + message['title'] = data.get('title', message['title']) + message['content'] = data.get('content', message['content']) + message['datetime'] = data.get('datetime', message['datetime']) + save_messages(messages) + return jsonify(message) + + return jsonify({'error': 'Message not found'}), 404 + +@app.route('/api/messages/', methods=['DELETE']) +@auth.require_admin +def delete_message(message_id): + """Delete a message.""" + messages = load_messages() + messages = [m for m in messages if m['id'] != message_id] + save_messages(messages) + + return jsonify({'success': True}) + # Grab the HOST_RULE environment variable host_rule = os.getenv("HOST_RULE", "") # Use a regex to extract domain names between backticks in patterns like Host(`something`) diff --git a/readme.md b/readme.md index fb8a0bb..f14973e 100644 --- a/readme.md +++ b/readme.md @@ -9,6 +9,13 @@ This is a self-hosted media sharing and indexing platform designed for secure an - **Primary Folders (Secret Links):** Centralized configuration with full admin control. Suitable for public events. - **Subfolders (Token Links):** Can be shared ad hoc, without central admin access. Ideal for limited-audience events. +- **News/Messages System** + - Blog-style news cards with date/time stamps + - Admin interface for creating, editing, and deleting messages + - Markdown support for rich content formatting + - Separate tab for browsing files and viewing messages + - Chronological display (newest first) + - **Transcription and Search** - Local transcription of audio files - Full-text search through transcripts @@ -139,6 +146,22 @@ docker compose up -d To unlock administrative controls on your device, use the dedicated admin access link. +### 6. Managing Messages/News + +The application includes a news/messages system that allows admins to post announcements and updates: + +- **Viewing Messages**: All users can view messages by clicking the "Nachrichten" tab in the main interface +- **Adding Messages**: Admins can click "Nachricht hinzufügen" to create a new message +- **Editing Messages**: Click the "Bearbeiten" button on any message card +- **Deleting Messages**: Click the "Löschen" button to remove a message + +Messages are stored in `messages.json` and support: +- **Markdown formatting** for rich content (headings, lists, links, code blocks, etc.) +- **Date/time stamps** for chronological organization +- **Blog-style display** with newest messages first + +To pre-populate messages, you can manually edit `messages.json` or copy from `example_config_files/messages.json` as a template. + ## Contribution This app is under active development and contributions are welcome. diff --git a/static/app.css b/static/app.css index cf0740a..01a754e 100644 --- a/static/app.css +++ b/static/app.css @@ -35,11 +35,25 @@ body { } .wrapper { - display: grid; - grid-template-rows: auto 1fr auto; + display: flex; + flex-direction: column; min-height: 100%; padding-bottom: 200px; } + +.wrapper > .admin-nav, +.wrapper > .main-tabs { + flex: 0 0 auto; +} + +.wrapper > main, +.wrapper > section { + flex: 1 1 auto; +} + +.wrapper > footer { + flex: 0 0 auto; +} .container { max-width: 1200px; width: 90%; @@ -367,4 +381,273 @@ footer { .toggle-icon:hover { color: #007bff; +} + +/* Tab Navigation Styles */ +.main-tabs { + background-color: var(--dark-background); + display: flex; + justify-content: center; + padding: 0; + margin: 0; + gap: 2px; + border-bottom: 2px solid #444; +} + +.tab-button { + background: #333; + border: 1px solid #444; + border-bottom: none; + color: #999; + padding: 6px 20px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; + border-radius: 5px 5px 0 0; + margin: 0; + position: relative; + top: 2px; +} + +.tab-button:hover { + color: #fff; + background: #3a3a3a; +} + +.tab-button.active { + color: var(--main-text-color, #000); + background: var(--light-background); + border-color: #444; + border-bottom-color: var(--light-background); + z-index: 1; +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* Messages Section Styles */ +.messages-container { + max-width: 900px; + margin: 0 auto; + padding: 20px 0; +} + +.message-card { + background: var(--card-background, #fff); + border: 1px solid #ddd; + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + transition: box-shadow 0.3s ease; +} + +.message-card:hover { + box-shadow: 0 4px 12px rgba(0,0,0,0.15); +} + +.message-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 2px solid #e0e0e0; +} + +.message-title { + font-size: 1.5em; + font-weight: bold; + color: var(--main-text-color); + margin: 0; +} + +.message-datetime { + color: #666; + font-size: 0.9em; + font-style: italic; +} + +.message-content { + line-height: 1.6; + color: var(--main-text-color); +} + +.message-content .folder-link-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + margin: 4px 0; + background: #f0f4f8; + color: #495057; + border: 1px solid #d1dce5; + border-radius: 6px; + font-size: 0.95em; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.message-content .folder-link-btn:hover { + background: #e3eaf0; + border-color: #b8c5d0; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.12); +} + +.message-content .folder-link-btn:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3); +} + +.message-content .folder-link-btn i { + font-size: 1.1em; +} + +.message-content a { + color: #007bff; + text-decoration: underline; + cursor: pointer; +} + +.message-content a:hover { + color: #0056b3; + text-decoration: underline; +} + +.message-content h1, +.message-content h2, +.message-content h3 { + margin-top: 20px; + margin-bottom: 10px; +} + +.message-content p { + margin-bottom: 10px; +} + +.message-content ul, +.message-content ol { + margin-left: 20px; + margin-bottom: 10px; +} + +.message-content code { + background-color: #f4f4f4; + padding: 2px 6px; + border-radius: 3px; + font-family: 'Courier New', monospace; +} + +.message-content pre { + background-color: #f4f4f4; + padding: 10px; + border-radius: 5px; + overflow-x: auto; +} + +.message-content blockquote { + border-left: 4px solid #ccc; + padding-left: 15px; + color: #666; + font-style: italic; +} + +.message-image { + width: 100%; + max-width: 100%; + height: auto; + border-radius: 8px; + margin: 15px 0; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); +} + +.message-actions { + margin-top: 15px; + padding-top: 10px; + border-top: 1px solid #e0e0e0; + display: flex; + gap: 10px; +} + +.btn-edit, +.btn-delete { + padding: 5px 15px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 14px; + transition: all 0.3s ease; +} + +.btn-edit { + background-color: #007bff; + color: white; +} + +.btn-edit:hover { + background-color: #0056b3; +} + +.btn-delete { + background-color: #dc3545; + color: white; +} + +.btn-delete:hover { + background-color: #c82333; +} + +.no-messages { + text-align: center; + padding: 40px; + color: #999; + font-size: 1.2em; +} + +/* File Browser Styles */ +.file-browser-content { + max-height: 400px; + overflow-y: auto; + border: 1px solid #ddd; + border-radius: 5px; +} + +.file-browser-content .list-group-item { + border-left: none; + border-right: none; + border-radius: 0; +} + +.file-browser-content .list-group-item:first-child { + border-top: none; +} + +.file-browser-content .list-group-item:last-child { + border-bottom: none; +} + +#fileBrowserCollapse { + margin-top: 0; + margin-bottom: 15px; +} + +.file-browser-panel { + background-color: #f5f5f5; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px; + margin-bottom: 10px; +} + +.file-browser-panel h6 { + margin-top: 0; + margin-bottom: 10px; + color: #333; } \ No newline at end of file diff --git a/static/app.js b/static/app.js index f1b3fe6..f503de6 100644 --- a/static/app.js +++ b/static/app.js @@ -116,7 +116,7 @@ function renderContent(data) { if (data.breadcrumbs.length === 1 && data.toplist_enabled) { contentHTML += `
  • 🔥 oft angehört
  • `; } - console.log(data.folder_today, data.folder_yesterday); + data.directories.forEach(dir => { if (admin_enabled && data.breadcrumbs.length != 1 && dir.share) { share_link = `⚙️`; diff --git a/static/messages.js b/static/messages.js new file mode 100644 index 0000000..232cfdb --- /dev/null +++ b/static/messages.js @@ -0,0 +1,471 @@ +// Messages functionality + +let messagesData = []; +let messageModal; +let currentEditingId = null; + +// Initialize messages on page load +document.addEventListener('DOMContentLoaded', function() { + // Initialize Bootstrap modal + const modalElement = document.getElementById('messageModal'); + if (modalElement) { + messageModal = new bootstrap.Modal(modalElement); + } + + // Tab switching + const tabButtons = document.querySelectorAll('.tab-button'); + tabButtons.forEach(button => { + button.addEventListener('click', () => { + const targetTab = button.getAttribute('data-tab'); + + // Update tab buttons + tabButtons.forEach(btn => { + if (btn.getAttribute('data-tab') === targetTab) { + btn.classList.add('active'); + } else { + btn.classList.remove('active'); + } + }); + + // Update tab content + const tabContents = document.querySelectorAll('.tab-content'); + tabContents.forEach(content => { + content.classList.remove('active'); + }); + + const targetContent = document.getElementById(targetTab + '-section'); + if (targetContent) { + targetContent.classList.add('active'); + } + + // Load messages when switching to messages tab + if (targetTab === 'messages') { + loadMessages(); + } + }); + }); + + // Add message button + const addMessageBtn = document.getElementById('add-message-btn'); + if (addMessageBtn) { + addMessageBtn.addEventListener('click', () => { + openMessageModal(); + }); + } + + // Save message button + const saveMessageBtn = document.getElementById('saveMessageBtn'); + if (saveMessageBtn) { + saveMessageBtn.addEventListener('click', () => { + saveMessage(); + }); + } + + // Insert link button + const insertLinkBtn = document.getElementById('insertLinkBtn'); + if (insertLinkBtn) { + insertLinkBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + toggleFileBrowser(); + }); + } + + // Event delegation for file browser actions + const fileBrowserContent = document.getElementById('fileBrowserContent'); + if (fileBrowserContent) { + fileBrowserContent.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + + // Check if button was clicked + const button = e.target.closest('button[data-action="insert-link"]'); + if (button) { + const path = button.getAttribute('data-path'); + const name = button.getAttribute('data-name'); + const type = button.getAttribute('data-type'); + insertLink(path, name, type); + return; + } + + // Check if list item was clicked for navigation + const navItem = e.target.closest('li[data-action="navigate"]'); + if (navItem && !e.target.closest('button')) { + const path = navItem.getAttribute('data-path'); + loadFileBrowser(path); + return; + } + + // Check if list item was clicked for insert (files) + const insertItem = e.target.closest('li[data-action="insert-link"]'); + if (insertItem && !e.target.closest('button')) { + const path = insertItem.getAttribute('data-path'); + const name = insertItem.getAttribute('data-name'); + const type = insertItem.getAttribute('data-type'); + insertLink(path, name, type); + return; + } + }); + } + + // Event delegation for folder links in messages + const messagesContainer = document.getElementById('messages-container'); + if (messagesContainer) { + messagesContainer.addEventListener('click', (e) => { + const folderLink = e.target.closest('button[data-folder-path]'); + if (folderLink) { + e.preventDefault(); + const folderPath = folderLink.getAttribute('data-folder-path'); + + // Update tab buttons without changing hash + const tabButtons = document.querySelectorAll('.tab-button'); + tabButtons.forEach(btn => { + if (btn.getAttribute('data-tab') === 'browse') { + btn.classList.add('active'); + } else { + btn.classList.remove('active'); + } + }); + + // Update tab content + const tabContents = document.querySelectorAll('.tab-content'); + tabContents.forEach(content => { + content.classList.remove('active'); + }); + + const browseSection = document.getElementById('browse-section'); + if (browseSection) { + browseSection.classList.add('active'); + } + + // Load the directory directly without setTimeout + if (typeof loadDirectory === 'function') { + loadDirectory(folderPath); + } + } + }); + } + +}); + +async function loadMessages() { + try { + const response = await fetch('/api/messages'); + if (!response.ok) { + throw new Error('Failed to load messages'); + } + messagesData = await response.json(); + renderMessages(); + } catch (error) { + console.error('Error loading messages:', error); + const container = document.getElementById('messages-container'); + container.innerHTML = '
    Fehler beim Laden der Nachrichten
    '; + } +} + +function renderMessages() { + const container = document.getElementById('messages-container'); + + if (messagesData.length === 0) { + container.innerHTML = '
    Keine Nachrichten vorhanden
    '; + return; + } + + let html = ''; + messagesData.forEach(message => { + const datetime = formatDateTime(message.datetime); + + // Convert folder: protocol to clickable buttons before markdown parsing + let processedContent = message.content || ''; + processedContent = processedContent.replace(/\[([^\]]+)\]\(folder:([^)]+)\)/g, + (match, name, path) => `` + ); + + // Configure marked to allow all links + marked.setOptions({ + breaks: true, + gfm: true + }); + const contentHtml = marked.parse(processedContent); + + html += ` +
    +
    +

    ${escapeHtml(message.title)}

    +
    ${datetime}
    +
    +
    + ${contentHtml} +
    + ${admin_enabled ? ` +
    + + +
    + ` : ''} +
    + `; + }); + + container.innerHTML = html; +} + +function formatDateTime(datetimeStr) { + try { + const date = new Date(datetimeStr); + const options = { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }; + return date.toLocaleDateString('de-DE', options); + } catch (error) { + return datetimeStr; + } +} + +function escapeHtml(text) { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return text.replace(/[&<>"']/g, m => map[m]); +} + +function openMessageModal(messageId = null) { + currentEditingId = messageId; + + const modalTitle = document.getElementById('messageModalLabel'); + const messageIdInput = document.getElementById('messageId'); + const titleInput = document.getElementById('messageTitle'); + const datetimeInput = document.getElementById('messageDateTime'); + const contentInput = document.getElementById('messageContent'); + + if (messageId) { + // Edit mode + const message = messagesData.find(m => m.id === messageId); + if (message) { + modalTitle.textContent = 'Nachricht bearbeiten'; + messageIdInput.value = message.id; + titleInput.value = message.title; + + // Format datetime for input + const date = new Date(message.datetime); + const formattedDate = date.toISOString().slice(0, 16); + datetimeInput.value = formattedDate; + + contentInput.value = message.content; + } + } else { + // Add mode + modalTitle.textContent = 'Neue Nachricht'; + messageIdInput.value = ''; + titleInput.value = ''; + + // Set current datetime + const now = new Date(); + const formattedNow = now.toISOString().slice(0, 16); + datetimeInput.value = formattedNow; + + contentInput.value = ''; + } + + messageModal.show(); +} + +async function saveMessage() { + const messageId = document.getElementById('messageId').value; + const title = document.getElementById('messageTitle').value.trim(); + const datetime = document.getElementById('messageDateTime').value; + const content = document.getElementById('messageContent').value.trim(); + + if (!title || !datetime || !content) { + alert('Bitte füllen Sie alle Felder aus.'); + return; + } + + // Convert datetime-local to ISO string + const datetimeISO = new Date(datetime).toISOString(); + + const data = { + title: title, + datetime: datetimeISO, + content: content + }; + + try { + let response; + if (messageId) { + // Update existing message + response = await fetch(`/api/messages/${messageId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + } else { + // Create new message + response = await fetch('/api/messages', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + } + + if (!response.ok) { + throw new Error('Failed to save message'); + } + + messageModal.hide(); + loadMessages(); + } catch (error) { + console.error('Error saving message:', error); + alert('Fehler beim Speichern der Nachricht.'); + } +} + +function editMessage(messageId) { + openMessageModal(messageId); +} + +async function deleteMessage(messageId) { + if (!confirm('Möchten Sie diese Nachricht wirklich löschen?')) { + return; + } + + try { + const response = await fetch(`/api/messages/${messageId}`, { + method: 'DELETE' + }); + + if (!response.ok) { + throw new Error('Failed to delete message'); + } + + loadMessages(); + } catch (error) { + console.error('Error deleting message:', error); + alert('Fehler beim Löschen der Nachricht.'); + } +} + +// File browser functionality +let currentBrowserPath = ''; +let fileBrowserCollapse = null; + +function toggleFileBrowser() { + const collapse = document.getElementById('fileBrowserCollapse'); + + // Initialize collapse on first use + if (!fileBrowserCollapse) { + fileBrowserCollapse = new bootstrap.Collapse(collapse, { toggle: false }); + } + + if (collapse.classList.contains('show')) { + fileBrowserCollapse.hide(); + } else { + fileBrowserCollapse.show(); + loadFileBrowser(''); + } +} + +async function loadFileBrowser(path) { + currentBrowserPath = path; + const container = document.getElementById('fileBrowserContent'); + const pathDisplay = document.getElementById('currentPath'); + + pathDisplay.textContent = path || '/'; + container.innerHTML = '
    '; + + try { + const response = await fetch(`/api/path/${path}`); + if (!response.ok) { + throw new Error('Failed to load directory'); + } + + const data = await response.json(); + renderFileBrowser(data); + } catch (error) { + console.error('Error loading directory:', error); + container.innerHTML = '
    Fehler beim Laden des Verzeichnisses
    '; + } +} + +function renderFileBrowser(data) { + const container = document.getElementById('fileBrowserContent'); + let html = ''; + container.innerHTML = html; +} + +function insertLink(path, name, type) { + const contentTextarea = document.getElementById('messageContent'); + // For folders, use folder: protocol; for files use /media/ + const url = type === 'folder' ? `folder:${path}` : `/media/${path}`; + const linkText = `[${name}](${url})`; + + // Insert at cursor position + const start = contentTextarea.selectionStart; + const end = contentTextarea.selectionEnd; + const text = contentTextarea.value; + + contentTextarea.value = text.substring(0, start) + linkText + text.substring(end); + contentTextarea.selectionStart = contentTextarea.selectionEnd = start + linkText.length; + contentTextarea.focus(); + + // Optionally close the browser after inserting + fileBrowserCollapse.hide(); +} diff --git a/templates/app.html b/templates/app.html index a98e78d..5c5a0cc 100644 --- a/templates/app.html +++ b/templates/app.html @@ -59,7 +59,15 @@ Ordnerkonfiguration {% endif %} -
    + + + + + +
    @@ -75,6 +83,22 @@
    + +
    +
    + {% if admin_enabled %} +
    + +
    + {% endif %} +
    + +
    +
    +
    +
    @@ -242,6 +266,59 @@
    + + + @@ -249,6 +326,7 @@ +