Loading editor/html/css/editor.css +65 −0 Original line number Diff line number Diff line Loading @@ -675,3 +675,68 @@ dialog label { margin-left: auto; font-weight: bold; } /* Media Browser */ #media-browser-dialog { width: 700px; max-width: 90vw; max-height: 80vh; } .media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; max-height: 50vh; overflow-y: auto; padding: 8px 0; } .media-grid-item { border: 2px solid transparent; border-radius: 6px; cursor: pointer; overflow: hidden; background: var(--bg-secondary, #2a2a3a); display: flex; flex-direction: column; align-items: center; } .media-grid-item:hover { border-color: var(--accent, #89b4fa); } .media-grid-item.selected { border-color: var(--accent, #89b4fa); box-shadow: 0 0 0 2px var(--accent, #89b4fa); } .media-grid-item img { width: 100%; height: 100px; object-fit: cover; } .media-grid-item .media-name { font-size: 0.75rem; padding: 4px; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; } .media-browse-field { display: flex; gap: 4px; } .media-browse-field input { flex: 1; } .media-browse-field button { white-space: nowrap; } editor/html/index.html +19 −0 Original line number Diff line number Diff line Loading @@ -232,6 +232,24 @@ </div> </dialog> <!-- Media Browser Dialog --> <dialog id="media-browser-dialog"> <h3>Media Browser</h3> <div class="set-field"> <label data-i18n="I18N_CONNECTION">Verbindung</label> <select id="media-conn-select"></select> </div> <div class="set-field"> <label>Album</label> <select id="media-album-select"><option value="">-- Album --</option></select> </div> <div id="media-grid" class="media-grid"></div> <span id="media-status" class="set-status"></span> <div class="dialog-actions"> <button id="btn-media-close" data-i18n="Close">Schließen</button> </div> </dialog> <dialog id="ai-dialog"> <h3 data-i18n="I18N_AI">KI-Assistent</h3> <div class="set-field"> Loading Loading @@ -336,6 +354,7 @@ <script src="js/tree.js"></script> <script src="js/properties.js"></script> <script src="js/preview.js"></script> <script src="js/media-browser.js"></script> <script src="js/editor.js"></script> </body> </html> editor/html/js/api.js +14 −0 Original line number Diff line number Diff line Loading @@ -233,6 +233,20 @@ var EditorApi = (function() { }); }, // Media browse (via blog connection) mediaListAlbums: function(connId) { return request('POST', '/api/connection/media/' + connId, { command: 'media_list_albums' }); }, mediaListMedia: function(connId, albumId) { return request('POST', '/api/connection/media/' + connId, { command: 'media_list_media', album_id: albumId }); }, // HTML import/export importHtml: function(html) { return request('POST', '/api/document/import-html', { html: html }); Loading editor/html/js/media-browser.js 0 → 100644 +172 −0 Original line number Diff line number Diff line /******************************************************************************* * blogi-webedit Media Browser * Lets the user browse media from connected blog servers (MediaDB). *******************************************************************************/ var MediaBrowser = (function() { 'use strict'; var _onSelect = null; // callback(mediaItem) var _dialog = null; var _connSelect = null; var _albumSelect = null; var _grid = null; var _status = null; var _bound = false; function _ensureElements() { _dialog = document.getElementById('media-browser-dialog'); _connSelect = document.getElementById('media-conn-select'); _albumSelect = document.getElementById('media-album-select'); _grid = document.getElementById('media-grid'); _status = document.getElementById('media-status'); } function _bind() { if (_bound) return; _bound = true; document.getElementById('btn-media-close').addEventListener('click', function() { _dialog.close(); }); _connSelect.addEventListener('change', function() { var connId = _connSelect.value; if (connId) _loadAlbums(connId); }); _albumSelect.addEventListener('change', function() { var connId = _connSelect.value; var albumId = _albumSelect.value; if (connId && albumId) _loadMedia(connId, albumId); }); } function open(onSelect) { _onSelect = onSelect; _ensureElements(); _bind(); _grid.innerHTML = ''; _albumSelect.innerHTML = '<option value="">-- Album --</option>'; _status.textContent = ''; // Load connections _connSelect.innerHTML = '<option value="">...</option>'; EditorApi.listConnections().then(function(resp) { var conns = resp.connections || []; _connSelect.innerHTML = ''; if (conns.length === 0) { _connSelect.innerHTML = '<option value="">No connections</option>'; return; } for (var i = 0; i < conns.length; i++) { var opt = document.createElement('option'); opt.value = conns[i].id; var label = conns[i].name; if (conns[i].group) label = conns[i].group + ' / ' + label; opt.textContent = label; _connSelect.appendChild(opt); } // Auto-load albums for first connection if (conns.length > 0) { _connSelect.value = conns[0].id; _loadAlbums(conns[0].id); } }); _dialog.showModal(); } function _loadAlbums(connId) { _albumSelect.innerHTML = '<option value="">...</option>'; _grid.innerHTML = ''; _status.textContent = ''; EditorApi.mediaListAlbums(connId).then(function(resp) { var albums = resp.albums || []; _albumSelect.innerHTML = '<option value="">-- Album --</option>'; for (var i = 0; i < albums.length; i++) { var opt = document.createElement('option'); opt.value = albums[i].id; var label = albums[i].name; if (albums[i].store) label += ' (' + albums[i].store + ')'; if (albums[i].media_count !== undefined) label += ' [' + albums[i].media_count + ']'; opt.textContent = label; _albumSelect.appendChild(opt); } if (albums.length === 0) { _status.textContent = 'No albums found'; } }).catch(function(err) { _status.textContent = 'Error: ' + (err.error || 'Unknown'); _status.className = 'set-status error'; }); } function _loadMedia(connId, albumId) { _grid.innerHTML = ''; _status.textContent = 'Loading...'; EditorApi.mediaListMedia(connId, albumId).then(function(resp) { var media = resp.media || []; _status.textContent = ''; _grid.innerHTML = ''; if (media.length === 0) { _status.textContent = 'No media in this album'; return; } for (var i = 0; i < media.length; i++) { _grid.appendChild(_createMediaItem(media[i])); } }).catch(function(err) { _status.textContent = 'Error: ' + (err.error || 'Unknown'); _status.className = 'set-status error'; }); } function _createMediaItem(item) { var div = document.createElement('div'); div.className = 'media-grid-item'; div.dataset.id = item.id; if (item.kind === 'image' || item.content_type.indexOf('image') === 0) { var img = document.createElement('img'); img.src = item.url + '?w=150&h=150'; img.alt = item.filename; img.loading = 'lazy'; div.appendChild(img); } else { var icon = document.createElement('div'); icon.style.cssText = 'width:100%;height:100px;display:flex;align-items:center;justify-content:center;font-size:2rem;'; icon.textContent = item.kind === 'video' ? '\uD83C\uDFA5' : '\uD83D\uDCC4'; div.appendChild(icon); } var name = document.createElement('div'); name.className = 'media-name'; name.textContent = item.filename; name.title = item.filename; div.appendChild(name); div.addEventListener('click', function() { // Remove selected from others var prev = _grid.querySelector('.selected'); if (prev) prev.classList.remove('selected'); div.classList.add('selected'); if (_onSelect) { _onSelect(item); } _dialog.close(); }); return div; } return { open: open }; })(); editor/html/js/properties.js +27 −0 Original line number Diff line number Diff line Loading @@ -113,6 +113,33 @@ var PropertiesPanel = (function() { input.value = value; input.rows = 6; if (field.placeholder) input.placeholder = field.placeholder; } else if (field.type === 'media_browse') { var wrapper = document.createElement('div'); wrapper.className = 'media-browse-field'; input = document.createElement('input'); input.type = 'text'; input.name = field.key; input.value = value; input.placeholder = 'Media ID'; var browseBtn = document.createElement('button'); browseBtn.type = 'button'; browseBtn.textContent = '\uD83D\uDDBC\uFE0F'; browseBtn.title = 'Browse media'; (function(inp) { browseBtn.addEventListener('click', function() { if (typeof MediaBrowser !== 'undefined') { MediaBrowser.open(function(mediaItem) { inp.value = mediaItem.id; inp.dispatchEvent(new Event('change')); }); } }); })(input); wrapper.appendChild(input); wrapper.appendChild(browseBtn); group.appendChild(wrapper); form.appendChild(group); continue; } else { input = document.createElement('input'); input.type = 'text'; Loading Loading
editor/html/css/editor.css +65 −0 Original line number Diff line number Diff line Loading @@ -675,3 +675,68 @@ dialog label { margin-left: auto; font-weight: bold; } /* Media Browser */ #media-browser-dialog { width: 700px; max-width: 90vw; max-height: 80vh; } .media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; max-height: 50vh; overflow-y: auto; padding: 8px 0; } .media-grid-item { border: 2px solid transparent; border-radius: 6px; cursor: pointer; overflow: hidden; background: var(--bg-secondary, #2a2a3a); display: flex; flex-direction: column; align-items: center; } .media-grid-item:hover { border-color: var(--accent, #89b4fa); } .media-grid-item.selected { border-color: var(--accent, #89b4fa); box-shadow: 0 0 0 2px var(--accent, #89b4fa); } .media-grid-item img { width: 100%; height: 100px; object-fit: cover; } .media-grid-item .media-name { font-size: 0.75rem; padding: 4px; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; } .media-browse-field { display: flex; gap: 4px; } .media-browse-field input { flex: 1; } .media-browse-field button { white-space: nowrap; }
editor/html/index.html +19 −0 Original line number Diff line number Diff line Loading @@ -232,6 +232,24 @@ </div> </dialog> <!-- Media Browser Dialog --> <dialog id="media-browser-dialog"> <h3>Media Browser</h3> <div class="set-field"> <label data-i18n="I18N_CONNECTION">Verbindung</label> <select id="media-conn-select"></select> </div> <div class="set-field"> <label>Album</label> <select id="media-album-select"><option value="">-- Album --</option></select> </div> <div id="media-grid" class="media-grid"></div> <span id="media-status" class="set-status"></span> <div class="dialog-actions"> <button id="btn-media-close" data-i18n="Close">Schließen</button> </div> </dialog> <dialog id="ai-dialog"> <h3 data-i18n="I18N_AI">KI-Assistent</h3> <div class="set-field"> Loading Loading @@ -336,6 +354,7 @@ <script src="js/tree.js"></script> <script src="js/properties.js"></script> <script src="js/preview.js"></script> <script src="js/media-browser.js"></script> <script src="js/editor.js"></script> </body> </html>
editor/html/js/api.js +14 −0 Original line number Diff line number Diff line Loading @@ -233,6 +233,20 @@ var EditorApi = (function() { }); }, // Media browse (via blog connection) mediaListAlbums: function(connId) { return request('POST', '/api/connection/media/' + connId, { command: 'media_list_albums' }); }, mediaListMedia: function(connId, albumId) { return request('POST', '/api/connection/media/' + connId, { command: 'media_list_media', album_id: albumId }); }, // HTML import/export importHtml: function(html) { return request('POST', '/api/document/import-html', { html: html }); Loading
editor/html/js/media-browser.js 0 → 100644 +172 −0 Original line number Diff line number Diff line /******************************************************************************* * blogi-webedit Media Browser * Lets the user browse media from connected blog servers (MediaDB). *******************************************************************************/ var MediaBrowser = (function() { 'use strict'; var _onSelect = null; // callback(mediaItem) var _dialog = null; var _connSelect = null; var _albumSelect = null; var _grid = null; var _status = null; var _bound = false; function _ensureElements() { _dialog = document.getElementById('media-browser-dialog'); _connSelect = document.getElementById('media-conn-select'); _albumSelect = document.getElementById('media-album-select'); _grid = document.getElementById('media-grid'); _status = document.getElementById('media-status'); } function _bind() { if (_bound) return; _bound = true; document.getElementById('btn-media-close').addEventListener('click', function() { _dialog.close(); }); _connSelect.addEventListener('change', function() { var connId = _connSelect.value; if (connId) _loadAlbums(connId); }); _albumSelect.addEventListener('change', function() { var connId = _connSelect.value; var albumId = _albumSelect.value; if (connId && albumId) _loadMedia(connId, albumId); }); } function open(onSelect) { _onSelect = onSelect; _ensureElements(); _bind(); _grid.innerHTML = ''; _albumSelect.innerHTML = '<option value="">-- Album --</option>'; _status.textContent = ''; // Load connections _connSelect.innerHTML = '<option value="">...</option>'; EditorApi.listConnections().then(function(resp) { var conns = resp.connections || []; _connSelect.innerHTML = ''; if (conns.length === 0) { _connSelect.innerHTML = '<option value="">No connections</option>'; return; } for (var i = 0; i < conns.length; i++) { var opt = document.createElement('option'); opt.value = conns[i].id; var label = conns[i].name; if (conns[i].group) label = conns[i].group + ' / ' + label; opt.textContent = label; _connSelect.appendChild(opt); } // Auto-load albums for first connection if (conns.length > 0) { _connSelect.value = conns[0].id; _loadAlbums(conns[0].id); } }); _dialog.showModal(); } function _loadAlbums(connId) { _albumSelect.innerHTML = '<option value="">...</option>'; _grid.innerHTML = ''; _status.textContent = ''; EditorApi.mediaListAlbums(connId).then(function(resp) { var albums = resp.albums || []; _albumSelect.innerHTML = '<option value="">-- Album --</option>'; for (var i = 0; i < albums.length; i++) { var opt = document.createElement('option'); opt.value = albums[i].id; var label = albums[i].name; if (albums[i].store) label += ' (' + albums[i].store + ')'; if (albums[i].media_count !== undefined) label += ' [' + albums[i].media_count + ']'; opt.textContent = label; _albumSelect.appendChild(opt); } if (albums.length === 0) { _status.textContent = 'No albums found'; } }).catch(function(err) { _status.textContent = 'Error: ' + (err.error || 'Unknown'); _status.className = 'set-status error'; }); } function _loadMedia(connId, albumId) { _grid.innerHTML = ''; _status.textContent = 'Loading...'; EditorApi.mediaListMedia(connId, albumId).then(function(resp) { var media = resp.media || []; _status.textContent = ''; _grid.innerHTML = ''; if (media.length === 0) { _status.textContent = 'No media in this album'; return; } for (var i = 0; i < media.length; i++) { _grid.appendChild(_createMediaItem(media[i])); } }).catch(function(err) { _status.textContent = 'Error: ' + (err.error || 'Unknown'); _status.className = 'set-status error'; }); } function _createMediaItem(item) { var div = document.createElement('div'); div.className = 'media-grid-item'; div.dataset.id = item.id; if (item.kind === 'image' || item.content_type.indexOf('image') === 0) { var img = document.createElement('img'); img.src = item.url + '?w=150&h=150'; img.alt = item.filename; img.loading = 'lazy'; div.appendChild(img); } else { var icon = document.createElement('div'); icon.style.cssText = 'width:100%;height:100px;display:flex;align-items:center;justify-content:center;font-size:2rem;'; icon.textContent = item.kind === 'video' ? '\uD83C\uDFA5' : '\uD83D\uDCC4'; div.appendChild(icon); } var name = document.createElement('div'); name.className = 'media-name'; name.textContent = item.filename; name.title = item.filename; div.appendChild(name); div.addEventListener('click', function() { // Remove selected from others var prev = _grid.querySelector('.selected'); if (prev) prev.classList.remove('selected'); div.classList.add('selected'); if (_onSelect) { _onSelect(item); } _dialog.close(); }); return div; } return { open: open }; })();
editor/html/js/properties.js +27 −0 Original line number Diff line number Diff line Loading @@ -113,6 +113,33 @@ var PropertiesPanel = (function() { input.value = value; input.rows = 6; if (field.placeholder) input.placeholder = field.placeholder; } else if (field.type === 'media_browse') { var wrapper = document.createElement('div'); wrapper.className = 'media-browse-field'; input = document.createElement('input'); input.type = 'text'; input.name = field.key; input.value = value; input.placeholder = 'Media ID'; var browseBtn = document.createElement('button'); browseBtn.type = 'button'; browseBtn.textContent = '\uD83D\uDDBC\uFE0F'; browseBtn.title = 'Browse media'; (function(inp) { browseBtn.addEventListener('click', function() { if (typeof MediaBrowser !== 'undefined') { MediaBrowser.open(function(mediaItem) { inp.value = mediaItem.id; inp.dispatchEvent(new Event('change')); }); } }); })(input); wrapper.appendChild(input); wrapper.appendChild(browseBtn); group.appendChild(wrapper); form.appendChild(group); continue; } else { input = document.createElement('input'); input.type = 'text'; Loading