diff --git a/app/routes/__pycache__/admin.cpython-314.pyc b/app/routes/__pycache__/admin.cpython-314.pyc index 1347eac..179ef02 100644 Binary files a/app/routes/__pycache__/admin.cpython-314.pyc and b/app/routes/__pycache__/admin.cpython-314.pyc differ diff --git a/app/routes/admin.py b/app/routes/admin.py index 208a0cd..651277b 100644 --- a/app/routes/admin.py +++ b/app/routes/admin.py @@ -11,6 +11,21 @@ from app.utils.helpers import load_configs, load_default_sounds, load_soundboard admin_bp = Blueprint('admin', __name__) logger = logging.getLogger(__name__) + +def _list_sound_files(subdir=None): + sound_dir = current_app.config['SOUNDS_DIR'] + search_dir = os.path.join(sound_dir, subdir) if subdir else sound_dir + extensions = {'.mp3', '.ogg', '.wav', '.flac'} + files = [] + if not os.path.isdir(search_dir): + return files + for root, _, filenames in os.walk(search_dir): + for filename in filenames: + if os.path.splitext(filename)[1].lower() in extensions: + full_path = os.path.join(root, filename) + files.append(os.path.relpath(full_path, search_dir)) + return sorted(files, key=str.lower) + @admin_bp.route('/') def admin(): configs = load_configs() @@ -33,7 +48,7 @@ def admin_edit_config(filename): raise ValueError("Kein Inhalt übermittelt") # Validierung: versuche zu parsen - json.loads(new_content) + config_data = json.loads(new_content) with open(path, 'w', encoding='utf-8') as f: f.write(new_content) @@ -49,9 +64,14 @@ def admin_edit_config(filename): error_msg = f"Fehler beim Speichern: {str(e)}" # Bei Fehler: zurück zum Formular mit Fehlermeldung - with open(path, 'r', encoding='utf-8') as f: - content = f.read() - return render_template('admin_edit.html', filename=filename, content=content, error=error_msg) + content = request.form.get('config_content', '') + try: + config_data = json.loads(content) + except (json.JSONDecodeError, TypeError): + config_data = {} + return render_template('admin_edit.html', filename=filename, content=content, + config_data=config_data, sound_files=_list_sound_files(), + error=error_msg) # GET: Formular laden if not os.path.exists(path): @@ -59,9 +79,9 @@ def admin_edit_config(filename): with open(path, 'r', encoding='utf-8') as f: content = f.read() - - return render_template('admin_edit.html', filename=filename, content=content) - pass + config_data = json.loads(content) + return render_template('admin_edit.html', filename=filename, content=content, + config_data=config_data, sound_files=_list_sound_files()) @admin_bp.route('/theme/edit/', methods=['GET', 'POST']) @@ -87,15 +107,25 @@ def admin_edit_theme(filename): except Exception as e: logger.error(f"Speicherfehler Theme {filename}: {e}") error_msg = f"Fehler beim Speichern: {str(e)}" - with open(path, 'r', encoding='utf-8') as f: - content = f.read() - return render_template('admin_edit_theme.html', filename=filename, content=content, error=error_msg) + content = request.form.get('config_content', '') + try: + theme_data = json.loads(content) + except (json.JSONDecodeError, TypeError): + theme_data = {} + theme_sound_dir = os.path.dirname(filename) + return render_template('admin_edit_theme.html', filename=filename, content=content, + theme_data=theme_data, sound_files=_list_sound_files(theme_sound_dir), + error=error_msg) if not os.path.exists(path): return "Theme nicht gefunden", 404 with open(path, 'r', encoding='utf-8') as f: content = f.read() - return render_template('admin_edit_theme.html', filename=filename, content=content) + theme_data = json.loads(content) + theme_sound_dir = os.path.dirname(filename) + return render_template('admin_edit_theme.html', filename=filename, content=content, + theme_data=theme_data, sound_files=_list_sound_files(theme_sound_dir), + theme_sound_dir=theme_sound_dir) @admin_bp.route('/delete/', methods=['POST']) def admin_delete_config(filename): diff --git a/other/mkcontrol-app.service b/other/mkcontrol-app.service index d9ed990..eeece95 100644 --- a/other/mkcontrol-app.service +++ b/other/mkcontrol-app.service @@ -12,6 +12,8 @@ WorkingDirectory=/opt/mkcontrol-app ExecStart=/opt/mkcontrol-app/.venv/bin/python3 /opt/mkcontrol-app/app.py Restart=always +Environment="MKCONTROL_AUDIO_DEVICE=default" + StandardOutput=file:/var/log/mkcontrol/mkcontrol.log StandardError=file:/var/log/mkcontrol/mkcontrol_error.log diff --git a/static/css/custom.css b/static/css/custom.css index cf64b84..b5c556d 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -58,4 +58,39 @@ pre code::before { text-align: right; width: 3em; user-select: none; -} \ No newline at end of file +} + +/* Visuelle Konfigurationseditoren */ +.editor-shell { --ink:#20201d; --paper:#fffdf8; --red:#9b2c20; max-width:1320px; margin:0 auto; } +.editor-hero { display:flex; justify-content:space-between; align-items:center; gap:2rem; padding:2.2rem; margin-bottom:1.5rem; border-radius:1.25rem; color:var(--ink); background:radial-gradient(circle at 90% 10%,rgba(255,255,255,.8),transparent 32%),linear-gradient(135deg,#f1c86b,#e98e62); box-shadow:0 18px 45px rgba(72,47,24,.16); } +.editor-hero.soundboard-hero { color:white; background:radial-gradient(circle at 85% 15%,rgba(255,255,255,.16),transparent 28%),linear-gradient(135deg,#173f48,#276f68 55%,#d89b46); } +.editor-hero h1 { margin:.15rem 0; font-size:clamp(2rem,4vw,3.4rem); font-family:Georgia,'Times New Roman',serif; } +.editor-hero p { margin:0; opacity:.78; } +.editor-kicker { text-transform:uppercase; letter-spacing:.16em; font-size:.74rem; font-weight:800; } +.editor-panel { padding:1.5rem; border:1px solid #dedbd2; border-radius:1.1rem; background:var(--paper); box-shadow:0 8px 24px rgba(39,37,31,.07); } +.editor-section-title { display:flex; align-items:center; gap:.85rem; margin-bottom:1.25rem; } +.editor-section-title.compact { margin-bottom:1rem; } +.editor-section-title>span { display:grid; place-items:center; flex:0 0 2.4rem; height:2.4rem; border-radius:50%; color:white; background:var(--red); font-weight:800; } +.editor-section-title h2,.lane-heading h2 { margin:0; font-size:1.25rem; font-weight:800; } +.editor-section-title p,.lane-heading p { margin:0; color:#77736a; font-size:.9rem; } +.component-palette { display:grid; gap:.75rem; } +.component-chip { padding:1rem; border:1px solid transparent; border-radius:.9rem; cursor:grab; box-shadow:0 5px 12px rgba(0,0,0,.08); transition:transform .15s,border-color .15s; } +.component-chip:hover { transform:translateY(-2px); border-color:#333; } +.component-chip strong,.component-chip small,.sound-kind strong,.sound-kind small { display:block; } +.component-chip.motor { background:#f5c86e; }.component-chip.light { background:#fff4b8; }.component-chip.fogger { background:#d8e5e5; }.component-chip.switch { background:#e7ded2; } +.channel-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1rem; } +.channel-slot { min-height:190px; padding:1rem; border:2px dashed #cfcac0; border-radius:1rem; background:#faf8f2; transition:border-color .15s,background .15s,transform .15s; } +.channel-slot.drag-over,.lane-dropzone.drag-over { border-color:#9b2c20; background:#fff2df; transform:scale(1.01); } +.channel-slot.occupied { border-style:solid; }.channel-slot.occupied.motor { border-color:#d79b2e; }.channel-slot.occupied.light { border-color:#e6c84d; }.channel-slot.occupied.fogger { border-color:#7ca7aa; }.channel-slot.occupied.switch { border-color:#9a8874; } +.empty-channel { min-height:155px; display:flex; flex-direction:column; align-items:center; justify-content:center; color:#8a867d; } +.empty-channel small { margin-top:.35rem; }.port-badge { display:grid; place-items:center; width:2.25rem; height:2.25rem; border-radius:.65rem; color:white; background:#282722; font-weight:900; } +.channel-slot-head { display:flex; align-items:center; gap:.65rem; margin-bottom:1rem; }.channel-slot-head .btn-close { margin-left:auto; }.component-type { text-transform:uppercase; letter-spacing:.08em; font-size:.75rem; font-weight:800; color:#666; } +.channel-fields { display:grid; gap:.65rem; }.channel-options { display:flex; flex-wrap:wrap; gap:.75rem; font-size:.85rem; } +.editor-field { display:block; }.editor-field>span { display:block; margin-bottom:.28rem; font-size:.78rem; font-weight:700; color:#656158; } +.sound-entry-list { display:grid; gap:.8rem; }.sound-entry-card { display:flex; align-items:flex-start; gap:1rem; padding:1rem; border:1px solid #dedbd2; border-radius:.9rem; background:white; }.sound-entry-index { display:grid; place-items:center; flex:0 0 2rem; height:2rem; border-radius:50%; background:#292823; color:white; font-weight:800; }.sound-entry-fields { flex:1; display:grid; gap:.6rem; }.editor-empty { padding:2rem; border:2px dashed #d8d4ca; border-radius:.9rem; text-align:center; color:#888278; } +.sound-palette { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1rem; }.sound-kind { padding:1rem; border:0; border-radius:1rem; text-align:left; cursor:grab; }.sound-kind.background { background:#cce8df; }.sound-kind.effect { background:#f7d083; }.sound-kind.random { background:#d9d7ef; } +.theme-lanes { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1rem; align-items:start; }.sound-lane { overflow:hidden; border:1px solid #d9d5cb; border-radius:1rem; background:#f8f6f0; }.lane-heading { display:flex; gap:.8rem; align-items:center; padding:1rem; }.lane-icon { display:grid; place-items:center; width:2.5rem; height:2.5rem; border-radius:.8rem; background:#292823; color:white; font-size:1.25rem; }.lane-dropzone { min-height:230px; padding:.8rem; border:2px dashed transparent; transition:.15s; }.lane-empty { display:grid; place-items:center; min-height:190px; border:2px dashed #d4d0c6; border-radius:.8rem; color:#928d82; } +.theme-sound-card { padding:1rem; margin-bottom:.8rem; border:1px solid #d7d3ca; border-radius:.85rem; background:white; box-shadow:0 5px 14px rgba(0,0,0,.06); cursor:grab; }.theme-card-head { display:flex; align-items:center; gap:.5rem; margin-bottom:.8rem; }.theme-card-head strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }.theme-card-head .btn-close { margin-left:auto; }.drag-handle { color:#989287; font-size:1.2rem; } + +@media (max-width:991px) { .editor-hero { align-items:flex-start; flex-direction:column; }.theme-lanes { grid-template-columns:1fr; }.sound-palette { grid-template-columns:1fr; } } +@media (max-width:767px) { .channel-grid { grid-template-columns:1fr; }.editor-panel,.editor-hero { padding:1.1rem; }.sound-entry-card { flex-wrap:wrap; }.sound-entry-fields { flex-basis:calc(100% - 3rem); } } diff --git a/static/js/admin-config-editors.js b/static/js/admin-config-editors.js new file mode 100644 index 0000000..5598069 --- /dev/null +++ b/static/js/admin-config-editors.js @@ -0,0 +1,308 @@ +(() => { + 'use strict'; + + const shell = document.querySelector('[data-editor]'); + if (!shell) return; + + const parseScript = (id, fallback) => { + try { return JSON.parse(document.getElementById(id)?.textContent || ''); } + catch (_) { return fallback; } + }; + const clone = value => JSON.parse(JSON.stringify(value)); + const editorType = shell.dataset.editor; + const initialData = parseScript('editor-data', {}); + const soundFiles = parseScript('sound-files-data', []); + const hiddenJson = document.getElementById('config_content'); + const rawEditor = document.getElementById('raw-json-editor'); + const rawError = document.getElementById('raw-json-error'); + const rawModal = new bootstrap.Modal(document.getElementById('rawJsonModal')); + let state = clone(initialData); + + const escapeHtml = value => String(value ?? '') + .replaceAll('&', '&').replaceAll('<', '<') + .replaceAll('>', '>').replaceAll('"', '"'); + + const soundOptions = () => soundFiles + .map(file => ``).join(''); + + function cleanState(value) { + if (Array.isArray(value)) return value.map(cleanState); + if (value && typeof value === 'object') { + return Object.fromEntries(Object.entries(value) + .filter(([key]) => !key.startsWith('__')) + .map(([key, child]) => [key, cleanState(child)])); + } + return value; + } + + function syncHidden() { + hiddenJson.value = JSON.stringify(cleanState(state), null, 2); + } + + function field(label, value, key, type = 'text', extra = '') { + return ``; + } + + function initRawEditor(render) { + document.getElementById('show-raw-json').addEventListener('click', () => { + syncHidden(); + rawEditor.value = hiddenJson.value; + rawError.textContent = ''; + rawModal.show(); + }); + document.getElementById('apply-raw-json').addEventListener('click', () => { + try { + state = JSON.parse(rawEditor.value); + rawError.textContent = ''; + render(); + syncHidden(); + rawModal.hide(); + } catch (error) { + rawError.textContent = `Ungültiges JSON: ${error.message}`; + } + }); + } + + function initLocomotiveEditor() { + const grid = document.getElementById('channel-grid'); + const soundList = document.getElementById('loco-sound-list'); + const ports = ['A', 'B', 'C', 'D', 'E', 'F']; + let draggedComponent = null; + + const componentDefaults = { + motor: { type: 'motor', name: 'Motor', invert: false, negative_only: false }, + light: { type: 'light', name: 'Licht', on_value: 1, off_value: 0, negative_only: false }, + fogger: { type: 'fogger', name: 'Dampf', on_value: -1, off_value: 0, negative_only: true }, + switch: { type: 'switch', name: 'Schalter', on_value: 1, off_value: 0, negative_only: false } + }; + + const activePorts = () => state.hub_type === '6channel' ? ports : ports.slice(0, 4); + const channelFor = port => (state.channels || []).find(channel => channel.port === port); + + function setComponent(port, type) { + state.channels ||= []; + const old = channelFor(port); + const next = { ...(componentDefaults[type] || componentDefaults.switch), port }; + if (old?.type === type) Object.assign(next, old); + state.channels = state.channels.filter(channel => channel.port !== port); + state.channels.push(next); + renderChannels(); + } + + function renderChannelFields(channel) { + const binary = channel.type !== 'motor'; + return ` + ${field('Bezeichnung', channel.name || '', 'name')} +
+ + +
+ ${binary ? `
${field('Wert EIN', channel.on_value ?? 1, 'on_value', 'number', 'step="0.1"')}
${field('Wert AUS', channel.off_value ?? 0, 'off_value', 'number', 'step="0.1"')}
` : ''} + `; + } + + function renderChannels() { + document.getElementById('channel-count-label').textContent = state.hub_type === '6channel' ? 'Kanäle A–F' : 'Kanäle A–D'; + grid.innerHTML = ''; + activePorts().forEach(port => { + const channel = channelFor(port); + const card = document.createElement('article'); + card.className = `channel-slot ${channel ? `occupied ${channel.type}` : ''}`; + card.dataset.port = port; + card.innerHTML = channel ? ` +
${port}${escapeHtml(channel.type)}
+
${renderChannelFields(channel)}
+ ` : `
${port}Kanal ${port}Bauteil hier ablegen
`; + + card.addEventListener('dragover', event => { event.preventDefault(); card.classList.add('drag-over'); }); + card.addEventListener('dragleave', () => card.classList.remove('drag-over')); + card.addEventListener('drop', event => { + event.preventDefault(); card.classList.remove('drag-over'); + const type = event.dataTransfer.getData('text/component') || draggedComponent; + if (type) setComponent(port, type); + }); + card.querySelector('.remove-channel')?.addEventListener('click', () => { + state.channels = (state.channels || []).filter(item => item.port !== port); + renderChannels(); + }); + card.querySelectorAll('[data-field]').forEach(input => input.addEventListener('input', () => { + const key = input.dataset.field; + channel[key] = input.type === 'checkbox' ? input.checked : input.type === 'number' ? Number(input.value) : input.value; + })); + grid.appendChild(card); + }); + } + + function addLocoSound(sound = {}) { + state.sounds ||= []; + state.sounds.push({ id: '', file: '', name: 'Neuer Sound', description: '', ...sound }); + renderSounds(); + } + + function renderSounds() { + state.sounds ||= []; + soundList.innerHTML = state.sounds.length ? '' : '
Noch keine Lok-Sounds. Über „+ Sound“ einen Eintrag hinzufügen.
'; + state.sounds.forEach((sound, index) => { + const row = document.createElement('article'); + row.className = 'sound-entry-card'; + row.innerHTML = `
${index + 1}
+
${field('Name', sound.name || '', 'name')}
${field('ID', sound.id || '', 'id')}
+ ${field('Beschreibung', sound.description || '', 'description')} +
`; + row.querySelectorAll('[data-field]').forEach(input => input.addEventListener('input', () => { sound[input.dataset.field] = input.value; })); + row.querySelector('.remove-sound').addEventListener('click', () => { state.sounds.splice(index, 1); renderSounds(); }); + soundList.appendChild(row); + }); + if (!document.getElementById('sound-files')) soundList.insertAdjacentHTML('beforeend', `${soundOptions()}`); + } + + function render() { + state.channels ||= []; + state.sounds ||= []; + document.getElementById('cfg-name').value = state.name || ''; + document.getElementById('cfg-hub-id').value = state.hub_id ?? 0; + document.getElementById('cfg-hub-type').value = state.hub_type || '4channel'; + document.getElementById('cfg-image').value = state.image || ''; + renderChannels(); renderSounds(); + } + + document.querySelectorAll('[data-component]').forEach(chip => { + chip.addEventListener('dragstart', event => { draggedComponent = chip.dataset.component; event.dataTransfer.setData('text/component', draggedComponent); }); + chip.addEventListener('dragend', () => { draggedComponent = null; }); + chip.addEventListener('click', () => { + const freePort = activePorts().find(port => !channelFor(port)); + if (freePort) setComponent(freePort, chip.dataset.component); + }); + }); + document.getElementById('cfg-name').addEventListener('input', event => { state.name = event.target.value; }); + document.getElementById('cfg-hub-id').addEventListener('input', event => { state.hub_id = Number(event.target.value); }); + document.getElementById('cfg-image').addEventListener('input', event => { state.image = event.target.value; }); + document.getElementById('cfg-hub-type').addEventListener('change', event => { state.hub_type = event.target.value; renderChannels(); }); + document.getElementById('add-loco-sound').addEventListener('click', () => addLocoSound()); + document.getElementById('config-form').addEventListener('submit', () => { + const valid = new Set(activePorts()); + state.channels = (state.channels || []).filter(channel => valid.has(channel.port)).sort((a, b) => a.port.localeCompare(b.port)); + syncHidden(); + }); + initRawEditor(render); render(); syncHidden(); + } + + function initSoundboardEditor() { + const laneKeys = ['backgrounds', 'sounds', 'random_pool']; + let dragged = null; + let sequence = 0; + + const ensureIds = () => laneKeys.forEach(key => { + state[key] ||= []; + state[key].forEach(item => { if (!item.__editorId) item.__editorId = `sound-${++sequence}`; }); + }); + + function defaultSound(kind) { + const base = { __editorId: `sound-${++sequence}`, id: '', name: 'Neuer Sound', file: '', description: '' }; + if (kind === 'background') return { ...base, loop: true, channel: 0 }; + if (kind === 'sound') return { ...base, channel: 1 }; + return { ...base }; + } + + function addSound(kind, targetKey) { + const key = targetKey || (kind === 'background' ? 'backgrounds' : kind === 'random' ? 'random_pool' : 'sounds'); + state[key].push(defaultSound(kind)); renderLanes(); + } + + function findItem(id) { + for (const key of laneKeys) { + const index = state[key].findIndex(item => item.__editorId === id); + if (index >= 0) return { key, index, item: state[key][index] }; + } + return null; + } + + function moveItem(id, targetKey, targetIndex = null) { + const found = findItem(id); if (!found) return; + state[found.key].splice(found.index, 1); + if (targetKey === 'backgrounds') found.item.loop = true; + else delete found.item.loop; + if (targetIndex === null || targetIndex >= state[targetKey].length) state[targetKey].push(found.item); + else state[targetKey].splice(Math.max(0, targetIndex), 0, found.item); + renderLanes(); + } + + function renderCard(item, key, index) { + const card = document.createElement('article'); + card.className = 'theme-sound-card'; card.draggable = true; card.dataset.id = item.__editorId; + const optional = key === 'random_pool' ? `
${field('Eigenes Limit', item.max_per_hour ?? '', 'max_per_hour', 'number', 'min="1" placeholder="Theme"')}
${field('Zeitraum (Min.)', item.window_minutes ?? '', 'window_minutes', 'number', 'min="1" placeholder="Theme"')}
` : ''; + card.innerHTML = `
⠿${escapeHtml(item.name || item.id || 'Neuer Sound')}
+
${field('Anzeigename', item.name || '', 'name')}
${field('ID', item.id || '', 'id')}
+ + ${field('Beschreibung', item.description || '', 'description')} + ${key !== 'random_pool' ? field('Audio-Kanal', item.channel ?? (key === 'backgrounds' ? 0 : 1), 'channel', 'number', 'min="0" max="15"') : ''} + ${optional}`; + card.addEventListener('dragstart', event => { dragged = { type: 'existing', id: item.__editorId }; event.dataTransfer.setData('text/sound-id', item.__editorId); }); + card.addEventListener('dragend', () => { dragged = null; }); + card.addEventListener('dragover', event => { event.preventDefault(); event.stopPropagation(); card.classList.add('drag-over'); }); + card.addEventListener('dragleave', () => card.classList.remove('drag-over')); + card.addEventListener('drop', event => { + event.preventDefault(); event.stopPropagation(); card.classList.remove('drag-over'); + const existingId = event.dataTransfer.getData('text/sound-id'); + const kind = event.dataTransfer.getData('text/sound-kind'); + if (existingId && existingId !== item.__editorId) moveItem(existingId, key, index); + else if (kind) { + const created = defaultSound(kind); + state[key].splice(index, 0, created); + renderLanes(); + } + }); + card.querySelectorAll('[data-field]').forEach(input => input.addEventListener('input', () => { + const fieldName = input.dataset.field; + if (input.type === 'number') item[fieldName] = input.value === '' ? undefined : Number(input.value); + else item[fieldName] = input.value; + if (fieldName === 'name') card.querySelector('.theme-card-head strong').textContent = input.value || item.id || 'Neuer Sound'; + })); + card.querySelector('.remove-theme-sound').addEventListener('click', () => { state[key].splice(index, 1); renderLanes(); }); + return card; + } + + function renderLanes() { + ensureIds(); + document.querySelectorAll('[data-dropzone]').forEach(zone => { + const key = zone.dataset.dropzone; zone.innerHTML = ''; + if (!state[key].length) zone.innerHTML = '
Klangelement hier ablegen
'; + state[key].forEach((item, index) => zone.appendChild(renderCard(item, key, index))); + }); + if (!document.getElementById('theme-sound-files')) document.body.insertAdjacentHTML('beforeend', `${soundOptions()}`); + } + + function render() { + ensureIds(); + document.getElementById('theme-name').value = state.name || ''; + document.getElementById('theme-limit').value = state.random_limit_per_hour ?? 2; + document.getElementById('theme-window').value = state.random_window_minutes ?? 60; + renderLanes(); + } + + document.querySelectorAll('[data-sound-kind]').forEach(button => { + button.addEventListener('dragstart', event => { dragged = { type: 'new', kind: button.dataset.soundKind }; event.dataTransfer.setData('text/sound-kind', button.dataset.soundKind); }); + button.addEventListener('dragend', () => { dragged = null; }); + button.addEventListener('click', () => addSound(button.dataset.soundKind)); + }); + document.querySelectorAll('[data-dropzone]').forEach(zone => { + zone.addEventListener('dragover', event => { event.preventDefault(); zone.classList.add('drag-over'); }); + zone.addEventListener('dragleave', () => zone.classList.remove('drag-over')); + zone.addEventListener('drop', event => { + event.preventDefault(); zone.classList.remove('drag-over'); + const target = zone.dataset.dropzone; + const existingId = event.dataTransfer.getData('text/sound-id'); + const kind = event.dataTransfer.getData('text/sound-kind'); + if (existingId) moveItem(existingId, target); else if (kind) addSound(kind, target); + }); + }); + document.getElementById('theme-name').addEventListener('input', event => { state.name = event.target.value; }); + document.getElementById('theme-limit').addEventListener('input', event => { state.random_limit_per_hour = Number(event.target.value); }); + document.getElementById('theme-window').addEventListener('input', event => { state.random_window_minutes = Number(event.target.value); }); + document.getElementById('theme-form').addEventListener('submit', syncHidden); + initRawEditor(render); render(); syncHidden(); + } + + if (editorType === 'locomotive') initLocomotiveEditor(); + if (editorType === 'soundboard') initSoundboardEditor(); +})(); diff --git a/templates/admin.html b/templates/admin.html index 62b7c38..29eb9f9 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -232,8 +232,7 @@ let json; try { json = JSON.parse(text); } catch (_) { throw new Error('Serverantwort ist kein JSON:\n'+text); } if (!json.success) throw new Error(json.message || 'Fehler'); - alert('Konfiguration angelegt: ' + json.filename); - location.reload(); + location.href = `/admin/edit/${encodeURIComponent(json.filename)}`; } catch (e) { alert('Fehler: ' + e.message); } @@ -255,8 +254,7 @@ let json; try { json = JSON.parse(text); } catch (_) { throw new Error('Serverantwort ist kein JSON:\n'+text); } if (!json.success) throw new Error(json.message || 'Fehler'); - alert('Theme angelegt: ' + json.path); - location.reload(); + location.href = `/admin/theme/edit/${json.path.split('/').map(encodeURIComponent).join('/')}`; } catch (e) { alert('Fehler: ' + e.message); } diff --git a/templates/admin_edit.html b/templates/admin_edit.html index 0769747..b1d29d6 100644 --- a/templates/admin_edit.html +++ b/templates/admin_edit.html @@ -1,44 +1,75 @@ {% extends "base.html" %} -{% block title %}Bearbeite {{ filename }}{% endblock %} +{% block title %}Lok-Editor – {{ filename }}{% endblock %} {% block content %} -
-

Bearbeite Konfiguration

-

{{ filename }}

- - {% if error %} -