Loading editor/html/index.html +1 −0 Original line number Diff line number Diff line Loading @@ -351,6 +351,7 @@ </div> <div id="import-html-candidates"></div> </div> <p id="import-html-progress-status" class="set-status" style="display:none"></p> <div class="dialog-actions"> <button id="btn-import-html-back" data-i18n="I18N_BACK" style="display:none">Zurück</button> <button id="btn-import-html-next" data-i18n="I18N_NEXT">Weiter</button> Loading editor/html/js/api.js +11 −0 Original line number Diff line number Diff line Loading @@ -555,6 +555,11 @@ var EditorApi = (function() { return request('POST', '/api/document/parse-html', data); }, // Response is either {status: "ok", images_imported, images_failed, ...} // (nothing to fetch, or media_import wasn't requested) or // {status: "accepted", job_id, images_total, ...} when there were // external images to re-host -- see getImportHtmlStatus for polling // that job to completion. importHtml: function(html, mode, parentUuid, sections, mediaImport, baseUrl, extraCss) { var data = { html: html }; if (mode) data.mode = mode; Loading @@ -566,6 +571,12 @@ var EditorApi = (function() { return request('POST', '/api/document/import-html', data); }, // Polls an import-html media-import job. Response is // {status: "running"|"done", images_total, images_imported, images_failed}. getImportHtmlStatus: function(jobId) { return request('POST', '/api/document/import-html-status', { job_id: jobId }); }, exportHtml: function() { return request('GET', '/api/document/export-html'); }, Loading editor/html/js/editor.js +72 −6 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ var importHtmlPendingSource = ''; // raw HTML staged in the import dialog, once past step 1 var importHtmlPendingCandidates = []; // last parse-html result for the block picker var importHtmlPendingBaseUrl = ''; // set when importHtmlPendingSource was fetched from a URL, see loadImportHtmlFromUrl var importHtmlPollTimer = null; // pending setTimeout for pollImportHtmlJob, cleared on cancel/dialog close var saveDialogMode = 'save'; var projectsCache = []; var currentProjectId = ''; Loading Loading @@ -757,12 +758,16 @@ return { conn_id: connId, album_id: albumId }; } // step is 1, 2, or 'progress' -- the last one is shown while a // background image-import job (see pollImportHtmlJob) is still running: // just the progress line and the Cancel button, everything else hidden. function showImportHtmlStep(step) { document.getElementById('import-html-step1').style.display = step === 1 ? '' : 'none'; document.getElementById('import-html-step2').style.display = step === 2 ? '' : 'none'; document.getElementById('btn-import-html-back').style.display = step === 2 ? '' : 'none'; document.getElementById('btn-import-html-next').style.display = step === 1 ? '' : 'none'; document.getElementById('btn-import-html-confirm').style.display = step === 2 ? '' : 'none'; document.getElementById('import-html-progress-status').style.display = step === 'progress' ? '' : 'none'; } // Parses html for the block picker; if parsing finds nothing to choose Loading Loading @@ -804,6 +809,44 @@ }); } // Polls a media-import job started by handleImportHtml's async path (see // EditorApi.getImportHtmlStatus) until it's done, updating the dialog's // progress line in place. cssJsParts carries the css/js fetch stats from // the initial (already-final) import-html response so the closing alert // can report everything together, same as the synchronous path always // has. function pollImportHtmlJob(jobId, cssJsParts) { var progressEl = document.getElementById('import-html-progress-status'); progressEl.style.display = ''; EditorApi.getImportHtmlStatus(jobId).then(function(status) { progressEl.textContent = I18n.t('I18N_IMPORT_HTML_IMAGES_PROGRESS', 'Bilder werden importiert: {done} von {total}...') .replace('{done}', status.images_imported + status.images_failed) .replace('{total}', status.images_total); if (status.status !== 'done') { importHtmlPollTimer = setTimeout(function() { pollImportHtmlJob(jobId, cssJsParts); }, 1000); return; } importHtmlPollTimer = null; progressEl.style.display = 'none'; document.getElementById('import-html-dialog').close(); var parts = cssJsParts.slice(); if (status.images_imported) parts.push(status.images_imported + ' Bild(er) in MediaDB importiert'); if (status.images_failed) parts.push(status.images_failed + ' Bild(er) fehlgeschlagen'); if (parts.length) alert(parts.join(', ') + '.'); }).catch(function() { // Transient network hiccup polling the status endpoint -- the // job itself keeps running server-side regardless, just retry. importHtmlPollTimer = setTimeout(function() { pollImportHtmlJob(jobId, cssJsParts); }, 1000); }); } function renderImportHtmlCandidates(nodes) { var container = document.getElementById('import-html-candidates'); container.innerHTML = ''; Loading Loading @@ -1116,17 +1159,32 @@ } var mediaImport = getImportHtmlMediaImportOptions(); EditorApi.importHtml(html, null, null, sections, mediaImport, importHtmlPendingBaseUrl, getImportHtmlExtraCss()).then(function(resp) { document.getElementById('import-html-dialog').close(); var cssJsParts = []; if (resp.css_fetched) cssJsParts.push(resp.css_fetched + ' externes Stylesheet geladen'); if (resp.css_failed) cssJsParts.push(resp.css_failed + ' externes Stylesheet fehlgeschlagen'); if (resp.js_fetched) cssJsParts.push(resp.js_fetched + ' externes Skript geladen'); if (resp.js_failed) cssJsParts.push(resp.js_failed + ' externes Skript fehlgeschlagen'); // The widget tree itself is already built either way -- only // externally-hosted images may still be mid-fetch/upload in // the background (see handleImportHtml/ImportImageJob). // Refresh and let the user carry on editing immediately; // images just keep their original (external, directly // renderable) src until the job swaps in the re-hosted one. Canvas.clearSelection(); DocumentTree.clearSelection(); PropertiesPanel.clear(); refreshDocument(); var parts = []; if (resp.status === 'accepted') { showImportHtmlStep('progress'); pollImportHtmlJob(resp.job_id, cssJsParts); return; } document.getElementById('import-html-dialog').close(); var parts = cssJsParts.slice(); if (resp.images_imported) parts.push(resp.images_imported + ' Bild(er) in MediaDB importiert'); if (resp.images_failed) parts.push(resp.images_failed + ' Bild(er) fehlgeschlagen'); if (resp.css_fetched) parts.push(resp.css_fetched + ' externes Stylesheet geladen'); if (resp.css_failed) parts.push(resp.css_failed + ' externes Stylesheet fehlgeschlagen'); if (resp.js_fetched) parts.push(resp.js_fetched + ' externes Skript geladen'); if (resp.js_failed) parts.push(resp.js_failed + ' externes Skript fehlgeschlagen'); if (parts.length) alert(parts.join(', ') + '.'); }).catch(function(err) { alert(I18n.t('I18N_IMPORT_FAILED') + ': ' + (err.error || '')); Loading @@ -1134,6 +1192,14 @@ }); document.getElementById('btn-import-html-cancel').addEventListener('click', function() { // The background image-import job (if any) isn't tied to this // dialog -- it keeps running server-side and its results land in // doc.root whenever it finishes, so leaving it be here is fine; // this just stops polling/updating this now-closed dialog. if (importHtmlPollTimer) { clearTimeout(importHtmlPollTimer); importHtmlPollTimer = null; } document.getElementById('import-html-dialog').close(); }); Loading editor/src/webedit_api.cpp +302 −143 File changed.Preview size limit exceeded, changes collapsed. Show changes editor/src/webedit_api.h +18 −1 Original line number Diff line number Diff line Loading @@ -285,11 +285,17 @@ namespace webedit { // HTML import/export handlers void handleParseHtml(libhttppp::HttpRequest &curreq, const std::string &sessionid); void handleImportHtml(libhttppp::HttpRequest &curreq, const std::string &sessionid); // Polls the background image-import job handleImportHtml starts when // the request includes media_import and there are external images to // fetch/upload -- see ImportImageJob's own doc comment (webedit_api.cpp) // for why that work happens off the request thread instead of // synchronously. void handleImportHtmlStatus(libhttppp::HttpRequest &curreq, const std::string &sessionid); void handleExportHtml(libhttppp::HttpRequest &curreq, const std::string &sessionid); void handleImportCss(libhttppp::HttpRequest &curreq, const std::string &sessionid); // Helpers void sendJson(libhttppp::HttpRequest &curreq, json_object *obj, const std::string &state = "200 OK"); void sendJson(libhttppp::HttpRequest &curreq, json_object *obj, int status = 200); void sendJsonError(libhttppp::HttpRequest &curreq, int status, const std::string &msg); void ensureSessionCookie(libhttppp::HttpRequest &curreq, libhttppp::HttpResponse &resp); std::string getRequestBody(libhttppp::HttpRequest &curreq); Loading Loading @@ -391,6 +397,17 @@ namespace webedit { std::mutex _docsMutex; std::map<std::string, std::unique_ptr<DocumentState>> _documents; // Background image-import job tracking (see ImportImageJob in // webedit_api.cpp) -- held by shared_ptr, not value, since a // std::mutex member makes the struct itself non-movable/non-copyable // and the background thread that updates one needs to keep it alive // independently of whatever handleImportHtmlStatus poll calls come // and go around it. Entries are never erased (same convention as // _documents above) -- a finished job is just a few ints, not worth // the bookkeeping. std::mutex _importJobsMtx; std::map<std::string, std::shared_ptr<struct ImportImageJob>> _importJobs; }; } Loading
editor/html/index.html +1 −0 Original line number Diff line number Diff line Loading @@ -351,6 +351,7 @@ </div> <div id="import-html-candidates"></div> </div> <p id="import-html-progress-status" class="set-status" style="display:none"></p> <div class="dialog-actions"> <button id="btn-import-html-back" data-i18n="I18N_BACK" style="display:none">Zurück</button> <button id="btn-import-html-next" data-i18n="I18N_NEXT">Weiter</button> Loading
editor/html/js/api.js +11 −0 Original line number Diff line number Diff line Loading @@ -555,6 +555,11 @@ var EditorApi = (function() { return request('POST', '/api/document/parse-html', data); }, // Response is either {status: "ok", images_imported, images_failed, ...} // (nothing to fetch, or media_import wasn't requested) or // {status: "accepted", job_id, images_total, ...} when there were // external images to re-host -- see getImportHtmlStatus for polling // that job to completion. importHtml: function(html, mode, parentUuid, sections, mediaImport, baseUrl, extraCss) { var data = { html: html }; if (mode) data.mode = mode; Loading @@ -566,6 +571,12 @@ var EditorApi = (function() { return request('POST', '/api/document/import-html', data); }, // Polls an import-html media-import job. Response is // {status: "running"|"done", images_total, images_imported, images_failed}. getImportHtmlStatus: function(jobId) { return request('POST', '/api/document/import-html-status', { job_id: jobId }); }, exportHtml: function() { return request('GET', '/api/document/export-html'); }, Loading
editor/html/js/editor.js +72 −6 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ var importHtmlPendingSource = ''; // raw HTML staged in the import dialog, once past step 1 var importHtmlPendingCandidates = []; // last parse-html result for the block picker var importHtmlPendingBaseUrl = ''; // set when importHtmlPendingSource was fetched from a URL, see loadImportHtmlFromUrl var importHtmlPollTimer = null; // pending setTimeout for pollImportHtmlJob, cleared on cancel/dialog close var saveDialogMode = 'save'; var projectsCache = []; var currentProjectId = ''; Loading Loading @@ -757,12 +758,16 @@ return { conn_id: connId, album_id: albumId }; } // step is 1, 2, or 'progress' -- the last one is shown while a // background image-import job (see pollImportHtmlJob) is still running: // just the progress line and the Cancel button, everything else hidden. function showImportHtmlStep(step) { document.getElementById('import-html-step1').style.display = step === 1 ? '' : 'none'; document.getElementById('import-html-step2').style.display = step === 2 ? '' : 'none'; document.getElementById('btn-import-html-back').style.display = step === 2 ? '' : 'none'; document.getElementById('btn-import-html-next').style.display = step === 1 ? '' : 'none'; document.getElementById('btn-import-html-confirm').style.display = step === 2 ? '' : 'none'; document.getElementById('import-html-progress-status').style.display = step === 'progress' ? '' : 'none'; } // Parses html for the block picker; if parsing finds nothing to choose Loading Loading @@ -804,6 +809,44 @@ }); } // Polls a media-import job started by handleImportHtml's async path (see // EditorApi.getImportHtmlStatus) until it's done, updating the dialog's // progress line in place. cssJsParts carries the css/js fetch stats from // the initial (already-final) import-html response so the closing alert // can report everything together, same as the synchronous path always // has. function pollImportHtmlJob(jobId, cssJsParts) { var progressEl = document.getElementById('import-html-progress-status'); progressEl.style.display = ''; EditorApi.getImportHtmlStatus(jobId).then(function(status) { progressEl.textContent = I18n.t('I18N_IMPORT_HTML_IMAGES_PROGRESS', 'Bilder werden importiert: {done} von {total}...') .replace('{done}', status.images_imported + status.images_failed) .replace('{total}', status.images_total); if (status.status !== 'done') { importHtmlPollTimer = setTimeout(function() { pollImportHtmlJob(jobId, cssJsParts); }, 1000); return; } importHtmlPollTimer = null; progressEl.style.display = 'none'; document.getElementById('import-html-dialog').close(); var parts = cssJsParts.slice(); if (status.images_imported) parts.push(status.images_imported + ' Bild(er) in MediaDB importiert'); if (status.images_failed) parts.push(status.images_failed + ' Bild(er) fehlgeschlagen'); if (parts.length) alert(parts.join(', ') + '.'); }).catch(function() { // Transient network hiccup polling the status endpoint -- the // job itself keeps running server-side regardless, just retry. importHtmlPollTimer = setTimeout(function() { pollImportHtmlJob(jobId, cssJsParts); }, 1000); }); } function renderImportHtmlCandidates(nodes) { var container = document.getElementById('import-html-candidates'); container.innerHTML = ''; Loading Loading @@ -1116,17 +1159,32 @@ } var mediaImport = getImportHtmlMediaImportOptions(); EditorApi.importHtml(html, null, null, sections, mediaImport, importHtmlPendingBaseUrl, getImportHtmlExtraCss()).then(function(resp) { document.getElementById('import-html-dialog').close(); var cssJsParts = []; if (resp.css_fetched) cssJsParts.push(resp.css_fetched + ' externes Stylesheet geladen'); if (resp.css_failed) cssJsParts.push(resp.css_failed + ' externes Stylesheet fehlgeschlagen'); if (resp.js_fetched) cssJsParts.push(resp.js_fetched + ' externes Skript geladen'); if (resp.js_failed) cssJsParts.push(resp.js_failed + ' externes Skript fehlgeschlagen'); // The widget tree itself is already built either way -- only // externally-hosted images may still be mid-fetch/upload in // the background (see handleImportHtml/ImportImageJob). // Refresh and let the user carry on editing immediately; // images just keep their original (external, directly // renderable) src until the job swaps in the re-hosted one. Canvas.clearSelection(); DocumentTree.clearSelection(); PropertiesPanel.clear(); refreshDocument(); var parts = []; if (resp.status === 'accepted') { showImportHtmlStep('progress'); pollImportHtmlJob(resp.job_id, cssJsParts); return; } document.getElementById('import-html-dialog').close(); var parts = cssJsParts.slice(); if (resp.images_imported) parts.push(resp.images_imported + ' Bild(er) in MediaDB importiert'); if (resp.images_failed) parts.push(resp.images_failed + ' Bild(er) fehlgeschlagen'); if (resp.css_fetched) parts.push(resp.css_fetched + ' externes Stylesheet geladen'); if (resp.css_failed) parts.push(resp.css_failed + ' externes Stylesheet fehlgeschlagen'); if (resp.js_fetched) parts.push(resp.js_fetched + ' externes Skript geladen'); if (resp.js_failed) parts.push(resp.js_failed + ' externes Skript fehlgeschlagen'); if (parts.length) alert(parts.join(', ') + '.'); }).catch(function(err) { alert(I18n.t('I18N_IMPORT_FAILED') + ': ' + (err.error || '')); Loading @@ -1134,6 +1192,14 @@ }); document.getElementById('btn-import-html-cancel').addEventListener('click', function() { // The background image-import job (if any) isn't tied to this // dialog -- it keeps running server-side and its results land in // doc.root whenever it finishes, so leaving it be here is fine; // this just stops polling/updating this now-closed dialog. if (importHtmlPollTimer) { clearTimeout(importHtmlPollTimer); importHtmlPollTimer = null; } document.getElementById('import-html-dialog').close(); }); Loading
editor/src/webedit_api.cpp +302 −143 File changed.Preview size limit exceeded, changes collapsed. Show changes
editor/src/webedit_api.h +18 −1 Original line number Diff line number Diff line Loading @@ -285,11 +285,17 @@ namespace webedit { // HTML import/export handlers void handleParseHtml(libhttppp::HttpRequest &curreq, const std::string &sessionid); void handleImportHtml(libhttppp::HttpRequest &curreq, const std::string &sessionid); // Polls the background image-import job handleImportHtml starts when // the request includes media_import and there are external images to // fetch/upload -- see ImportImageJob's own doc comment (webedit_api.cpp) // for why that work happens off the request thread instead of // synchronously. void handleImportHtmlStatus(libhttppp::HttpRequest &curreq, const std::string &sessionid); void handleExportHtml(libhttppp::HttpRequest &curreq, const std::string &sessionid); void handleImportCss(libhttppp::HttpRequest &curreq, const std::string &sessionid); // Helpers void sendJson(libhttppp::HttpRequest &curreq, json_object *obj, const std::string &state = "200 OK"); void sendJson(libhttppp::HttpRequest &curreq, json_object *obj, int status = 200); void sendJsonError(libhttppp::HttpRequest &curreq, int status, const std::string &msg); void ensureSessionCookie(libhttppp::HttpRequest &curreq, libhttppp::HttpResponse &resp); std::string getRequestBody(libhttppp::HttpRequest &curreq); Loading Loading @@ -391,6 +397,17 @@ namespace webedit { std::mutex _docsMutex; std::map<std::string, std::unique_ptr<DocumentState>> _documents; // Background image-import job tracking (see ImportImageJob in // webedit_api.cpp) -- held by shared_ptr, not value, since a // std::mutex member makes the struct itself non-movable/non-copyable // and the background thread that updates one needs to keep it alive // independently of whatever handleImportHtmlStatus poll calls come // and go around it. Entries are never erased (same convention as // _documents above) -- a finished job is just a few ints, not worth // the bookkeeping. std::mutex _importJobsMtx; std::map<std::string, std::shared_ptr<struct ImportImageJob>> _importJobs; }; }