Skip to content

Commit 163c6bc

Browse files
feat: complete phases 1, 2, and 3 of diagram previews (theme fallbacks, cachestorage, local neutralino compilation)
1 parent bb3626d commit 163c6bc

5 files changed

Lines changed: 423 additions & 94 deletions

File tree

desktop-app/neutralino.config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"os.showMessageBox",
2121
"os.open",
2222
"os.setTray",
23+
"os.execCommand",
2324
"filesystem.readFile",
2425
"filesystem.writeFile",
2526
"storage.setData",

desktop-app/resources/js/script.js

Lines changed: 136 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3163,15 +3163,17 @@ document.addEventListener("DOMContentLoaded", async function () {
31633163
try {
31643164
const plantumlNodes = queryPreviewRoots(roots, '.plantuml-diagram');
31653165
if (plantumlNodes.length > 0) {
3166-
const renderPlantumlNodes = function() {
3166+
const renderPlantumlNodes = async function() {
31673167
if (context.renderId !== previewRenderGeneration) return;
31683168

3169-
plantumlNodes.forEach(node => {
3169+
for (const node of plantumlNodes) {
31703170
const container = node.closest('.plantuml-container');
31713171
const originalCode = node.getAttribute('data-original-code');
3172-
if (!originalCode) return;
3172+
if (!originalCode) continue;
31733173
const decodedCode = decodeURIComponent(originalCode);
31743174

3175+
if (container) container.classList.add('is-loading');
3176+
31753177
try {
31763178
let modifiedCode = decodedCode;
31773179
if (!modifiedCode.toLowerCase().includes('backgroundcolor')) {
@@ -3191,6 +3193,18 @@ document.addEventListener("DOMContentLoaded", async function () {
31913193
modifiedCode = lines.join('\n');
31923194
}
31933195
}
3196+
3197+
// Try local compile first if in Neutralino
3198+
if (typeof Neutralino !== 'undefined') {
3199+
const localSvg = await compileDiagramLocally('plantuml', modifiedCode);
3200+
if (localSvg) {
3201+
node.innerHTML = localSvg;
3202+
if (container) container.classList.remove('is-loading');
3203+
addPlantumlToolbars();
3204+
continue;
3205+
}
3206+
}
3207+
31943208
const encoded = encodePlantUML(modifiedCode);
31953209
const url = 'https://www.plantuml.com/plantuml/svg/' + encoded;
31963210

@@ -3219,7 +3233,7 @@ document.addEventListener("DOMContentLoaded", async function () {
32193233
node.innerHTML = `<div class="render-error-msg" style="padding: 1.5em; text-align: center; color: var(--text-color);">Error encoding diagram: ${escapeHtml(err.message)}</div>`;
32203234
if (container) container.classList.remove('is-loading');
32213235
}
3222-
});
3236+
}
32233237
};
32243238

32253239
if (typeof pako === 'undefined') {
@@ -3244,7 +3258,7 @@ document.addEventListener("DOMContentLoaded", async function () {
32443258
try {
32453259
const d2Nodes = queryPreviewRoots(roots, '.d2-diagram');
32463260
if (d2Nodes.length > 0) {
3247-
const renderSingleD2Node = function(node) {
3261+
const renderSingleD2Node = async function(node) {
32483262
const container = node.closest('.d2-container');
32493263
const originalCode = node.getAttribute('data-original-code');
32503264
if (!originalCode) return;
@@ -3257,6 +3271,18 @@ document.addEventListener("DOMContentLoaded", async function () {
32573271
if (!modifiedCode.includes('style.fill') && !/style\s*:\s*\{[^}]*fill/.test(modifiedCode)) {
32583272
modifiedCode = `style.fill: transparent\n${modifiedCode}`;
32593273
}
3274+
3275+
// Try local compile first if in Neutralino
3276+
if (typeof Neutralino !== 'undefined') {
3277+
const localSvg = await compileDiagramLocally('d2', modifiedCode);
3278+
if (localSvg) {
3279+
node.innerHTML = localSvg;
3280+
if (container) container.classList.remove('is-loading');
3281+
addD2Toolbars();
3282+
return;
3283+
}
3284+
}
3285+
32603286
const encoded = encodeKrokiD2(modifiedCode);
32613287
const url = 'https://kroki.io/d2/svg/' + encoded;
32623288

@@ -5609,6 +5635,83 @@ document.addEventListener("DOMContentLoaded", async function () {
56095635
}
56105636
}
56115637

5638+
async function compileDiagramLocally(engine, code) {
5639+
if (typeof Neutralino === 'undefined') return null;
5640+
try {
5641+
if (engine === 'd2') {
5642+
const result = await Neutralino.os.execCommand('d2 - -', { stdIn: code });
5643+
if (result && result.exitCode === 0 && result.stdOut) {
5644+
return result.stdOut;
5645+
}
5646+
} else if (engine === 'plantuml') {
5647+
try {
5648+
const result = await Neutralino.os.execCommand('plantuml -pipe -tsvg', { stdIn: code });
5649+
if (result && result.exitCode === 0 && result.stdOut) {
5650+
return result.stdOut;
5651+
}
5652+
} catch (e) {
5653+
const result = await Neutralino.os.execCommand('java -jar plantuml.jar -pipe -tsvg', { stdIn: code });
5654+
if (result && result.exitCode === 0 && result.stdOut) {
5655+
return result.stdOut;
5656+
}
5657+
}
5658+
}
5659+
} catch (e) {
5660+
console.warn(`Local execution for ${engine} failed:`, e);
5661+
}
5662+
return null;
5663+
}
5664+
5665+
async function fetchDiagramPreview(apiUrl) {
5666+
if (typeof caches === 'undefined') {
5667+
const response = await fetch(apiUrl);
5668+
if (!response.ok) throw new Error('Failed to fetch');
5669+
return await response.text();
5670+
}
5671+
const cache = await caches.open('diagram-previews');
5672+
const cachedResponse = await cache.match(apiUrl);
5673+
if (cachedResponse) {
5674+
return await cachedResponse.text();
5675+
}
5676+
const response = await fetch(apiUrl);
5677+
if (!response.ok) throw new Error('Failed to fetch');
5678+
await cache.put(apiUrl, response.clone());
5679+
return await response.text();
5680+
}
5681+
5682+
async function getOrRenderDiagramPreview(template, theme, callback) {
5683+
const cleanCode = getCleanCode(template.code);
5684+
5685+
if (typeof Neutralino !== 'undefined') {
5686+
if (template.category === 'D2') {
5687+
const localSvg = await compileDiagramLocally('d2', cleanCode);
5688+
if (localSvg) {
5689+
callback(localSvg);
5690+
return;
5691+
}
5692+
} else if (template.category === 'PlantUML') {
5693+
const localSvg = await compileDiagramLocally('plantuml', cleanCode);
5694+
if (localSvg) {
5695+
callback(localSvg);
5696+
return;
5697+
}
5698+
}
5699+
}
5700+
5701+
const apiUrl = getDiagramApiUrl(template, theme);
5702+
if (!apiUrl) {
5703+
callback(null);
5704+
return;
5705+
}
5706+
5707+
try {
5708+
const svgText = await fetchDiagramPreview(apiUrl);
5709+
callback(svgText);
5710+
} catch (e) {
5711+
callback(null);
5712+
}
5713+
}
5714+
56125715
async function openDiagramModal() {
56135716
const modal = document.getElementById('diagram-modal');
56145717
const sidebar = modal.querySelector('.diagram-modal-sidebar');
@@ -6388,41 +6491,34 @@ document.addEventListener("DOMContentLoaded", async function () {
63886491
const isMermaidSpecial = (t.id === 'mermaid-sequence' || t.id === 'mermaid-er');
63896492
const titleColor = isMermaidSpecial ? '#ff4081' : 'var(--text-color)';
63906493
const titleWeight = isMermaidSpecial ? 'bold' : 'normal';
6494+
const catClass = t.category.toLowerCase().replace(/\s+/g, '');
63916495

63926496
previewDiv.innerHTML = `
63936497
<div style="display:flex; flex-direction:column; align-items:center; width:100%; height:100%;">
63946498
<div style="font-size:10px; font-weight:${titleWeight}; color:${titleColor}; margin-bottom:4px;">${t.title}</div>
6395-
<div class="diagram-svg-container" style="flex:1; width:100%; display:flex; align-items:center; justify-content:center; overflow:hidden;">
6499+
<div class="diagram-svg-container diagram-svg-${catClass}" style="flex:1; width:100%; display:flex; align-items:center; justify-content:center; overflow:hidden;">
63966500
${t.svg}
63976501
</div>
63986502
</div>
63996503
`;
64006504

64016505
const theme = document.documentElement.getAttribute("data-theme") || "light";
6402-
const apiUrl = getDiagramApiUrl(t, theme);
64036506

6404-
if (apiUrl) {
6405-
const img = document.createElement('img');
6406-
img.style.display = 'none';
6407-
img.style.maxWidth = '100%';
6408-
img.style.maxHeight = '100%';
6409-
img.style.objectFit = 'contain';
6410-
6411-
img.onload = () => {
6507+
getOrRenderDiagramPreview(t, theme, svgText => {
6508+
if (svgText) {
64126509
const svgContainer = previewDiv.querySelector('.diagram-svg-container');
64136510
if (svgContainer) {
6414-
svgContainer.textContent = '';
6415-
img.style.display = 'block';
6416-
svgContainer.appendChild(img);
6511+
svgContainer.innerHTML = svgText;
6512+
const svgEl = svgContainer.querySelector('svg');
6513+
if (svgEl) {
6514+
svgEl.style.maxWidth = '100%';
6515+
svgEl.style.maxHeight = '100%';
6516+
svgEl.style.width = 'auto';
6517+
svgEl.style.height = 'auto';
6518+
}
64176519
}
6418-
};
6419-
6420-
img.onerror = () => {
6421-
console.warn(`Failed to load card preview from API for ${t.id}. Falling back to local SVG.`);
6422-
};
6423-
6424-
img.src = apiUrl;
6425-
}
6520+
}
6521+
});
64266522

64276523
const labelDiv = document.createElement('div');
64286524
labelDiv.className = 'diagram-card-label';
@@ -6440,38 +6536,31 @@ document.addEventListener("DOMContentLoaded", async function () {
64406536
if (previewCode) previewCode.value = t.code.trim();
64416537
confirmBtn.disabled = false;
64426538

6539+
const catClass = t.category.toLowerCase().replace(/\s+/g, '');
64436540
// Render bottom preview container with API image & fallback
64446541
previewContainer.innerHTML = `
6445-
<div class="diagram-svg-container" style="width:100%; height:100%; display:flex; align-items:center; justify-content:center; overflow:hidden;">
6542+
<div class="diagram-svg-container diagram-svg-${catClass}" style="width:100%; height:100%; display:flex; align-items:center; justify-content:center; overflow:hidden;">
64466543
${t.svg}
64476544
</div>
64486545
`;
64496546

64506547
const clickedTheme = document.documentElement.getAttribute("data-theme") || "light";
6451-
const clickedApiUrl = getDiagramApiUrl(t, clickedTheme);
64526548

6453-
if (clickedApiUrl) {
6454-
const previewImg = document.createElement('img');
6455-
previewImg.style.display = 'none';
6456-
previewImg.style.maxWidth = '100%';
6457-
previewImg.style.maxHeight = '100%';
6458-
previewImg.style.objectFit = 'contain';
6459-
6460-
previewImg.onload = () => {
6549+
getOrRenderDiagramPreview(t, clickedTheme, svgText => {
6550+
if (svgText) {
64616551
const svgContainer = previewContainer.querySelector('.diagram-svg-container');
64626552
if (svgContainer) {
6463-
svgContainer.textContent = '';
6464-
previewImg.style.display = 'block';
6465-
svgContainer.appendChild(previewImg);
6553+
svgContainer.innerHTML = svgText;
6554+
const svgEl = svgContainer.querySelector('svg');
6555+
if (svgEl) {
6556+
svgEl.style.maxWidth = '100%';
6557+
svgEl.style.maxHeight = '100%';
6558+
svgEl.style.width = 'auto';
6559+
svgEl.style.height = 'auto';
6560+
}
64666561
}
6467-
};
6468-
6469-
previewImg.onerror = () => {
6470-
console.warn(`Failed to load bottom preview from API for ${t.id}. Falling back to local SVG.`);
6471-
};
6472-
6473-
previewImg.src = clickedApiUrl;
6474-
}
6562+
}
6563+
});
64756564
});
64766565

64776566
grid.appendChild(card);

desktop-app/resources/styles.css

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4705,4 +4705,79 @@ html[data-theme="dark"] .diagram-preview-container {
47054705
.diagram-preview-container {
47064706
height: 120px;
47074707
}
4708+
}
4709+
4710+
/* Theme adaptive fallbacks for offline mock SVGs */
4711+
html[data-theme="dark"] .diagram-svg-container svg text {
4712+
fill: #e5e9f0 !important;
4713+
}
4714+
4715+
html[data-theme="dark"] .diagram-svg-container svg line,
4716+
html[data-theme="dark"] .diagram-svg-container svg path {
4717+
stroke: #81a1c1 !important;
4718+
}
4719+
4720+
/* Specific category mock tuning */
4721+
html[data-theme="dark"] .diagram-svg-mermaid svg rect,
4722+
html[data-theme="dark"] .diagram-svg-mermaid svg circle {
4723+
fill: #2e3440 !important;
4724+
stroke: #81a1c1 !important;
4725+
}
4726+
4727+
html[data-theme="dark"] .diagram-svg-plantuml svg rect,
4728+
html[data-theme="dark"] .diagram-svg-plantuml svg circle,
4729+
html[data-theme="dark"] .diagram-svg-plantuml svg ellipse,
4730+
html[data-theme="dark"] .diagram-svg-plantuml svg polygon {
4731+
fill: #3b2019 !important; /* retro mahogany adaptation */
4732+
stroke: #ff6b6b !important; /* soft red outline */
4733+
}
4734+
html[data-theme="dark"] .diagram-svg-plantuml svg text {
4735+
fill: #ffd8a8 !important; /* soft amber text */
4736+
}
4737+
4738+
html[data-theme="dark"] .diagram-svg-d2 svg rect,
4739+
html[data-theme="dark"] .diagram-svg-d2 svg circle {
4740+
fill: #2d3748 !important;
4741+
stroke: #cbd5e0 !important;
4742+
}
4743+
4744+
html[data-theme="dark"] .diagram-svg-graphviz svg rect,
4745+
html[data-theme="dark"] .diagram-svg-graphviz svg circle,
4746+
html[data-theme="dark"] .diagram-svg-graphviz svg ellipse {
4747+
fill: #2d2625 !important;
4748+
stroke: #a1887f !important;
4749+
}
4750+
4751+
html[data-theme="dark"] .diagram-svg-vegalite svg rect {
4752+
fill: #43a047 !important; /* glowing theme bar chart fill */
4753+
}
4754+
html[data-theme="dark"] .diagram-svg-vegalite svg circle {
4755+
fill: #ab47bc !important;
4756+
}
4757+
html[data-theme="dark"] .diagram-svg-vegalite svg line,
4758+
html[data-theme="dark"] .diagram-svg-vegalite svg path {
4759+
stroke: #e2e8f0 !important;
4760+
}
4761+
4762+
html[data-theme="dark"] .diagram-svg-wavedrom svg path {
4763+
stroke: #ff7043 !important;
4764+
}
4765+
4766+
html[data-theme="dark"] .diagram-svg-markmap svg rect {
4767+
fill: #37474f !important;
4768+
stroke: #90a4ae !important;
4769+
}
4770+
html[data-theme="dark"] .diagram-svg-markmap svg path {
4771+
stroke: #90a4ae !important;
4772+
}
4773+
html[data-theme="dark"] .diagram-svg-markmap svg circle {
4774+
fill: #ff7043 !important;
4775+
}
4776+
4777+
html[data-theme="dark"] .diagram-svg-abcnotation svg line {
4778+
stroke: #555555 !important;
4779+
}
4780+
html[data-theme="dark"] .diagram-svg-abcnotation svg path {
4781+
fill: #eceff4 !important;
4782+
stroke: #eceff4 !important;
47084783
}

0 commit comments

Comments
 (0)