From 6309fd57b2ca3f8410e0a023eda8d752f73ed7ce Mon Sep 17 00:00:00 2001 From: truelockmc <168660625+truelockmc@users.noreply.github.com> Date: Mon, 18 May 2026 16:25:45 +0200 Subject: [PATCH 1/3] update modal now properly shows images --- src/components/UpdateModal.jsx | 61 ++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/src/components/UpdateModal.jsx b/src/components/UpdateModal.jsx index 5bdbdd7..e876f08 100644 --- a/src/components/UpdateModal.jsx +++ b/src/components/UpdateModal.jsx @@ -68,6 +68,42 @@ function renderChangelog(text) { ); continue; } + // standalone image ![alt](url) + const imgMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)$/); + if (imgMatch) { + const [, alt, src] = imgMatch; + elements.push( +
+ {alt} { + e.currentTarget.style.display = "none"; + }} + /> + {alt && ( +
+ {alt} +
+ )} +
, + ); + continue; + } // bullet - or * if (/^[-*] /.test(line)) { elements.push( @@ -114,9 +150,9 @@ function renderChangelog(text) { } function inlineFormat(text) { - // Split on **bold**, `code`, then render + // Split on **bold**, `code`, ![img](url), then render const parts = []; - const re = /(\*\*[^*]+\*\*|`[^`]+`)/g; + const re = /(\*\*[^*]+\*\*|`[^`]+`|!\[[^\]]*\]\([^)]+\))/g; let last = 0, m, k = 0; @@ -130,6 +166,27 @@ function inlineFormat(text) { {raw.slice(2, -2)} , ); + } else if (raw.startsWith("![")) { + const imgInline = raw.match(/^!\[([^\]]*)\]\(([^)]+)\)$/); + if (imgInline) { + const [, alt, src] = imgInline; + parts.push( + {alt} { + e.currentTarget.style.display = "none"; + }} + />, + ); + } } else { parts.push( Date: Mon, 18 May 2026 21:36:29 +0200 Subject: [PATCH 2/3] Update Modal: render HTML img tags, bare URLs, and @mentions in changelog - the update modal now also shows other github formatting's correctly (e.g. italic/bold/code) - images and links are now also correctly displayed in the update modal - macOS updating now differs between x64 and arm64 devices --- src/components/UpdateModal.jsx | 379 +++++++++++++++++++++++++-------- src/utils/updates.js | 8 +- 2 files changed, 294 insertions(+), 93 deletions(-) diff --git a/src/components/UpdateModal.jsx b/src/components/UpdateModal.jsx index e876f08..189282b 100644 --- a/src/components/UpdateModal.jsx +++ b/src/components/UpdateModal.jsx @@ -1,7 +1,155 @@ import { useState, useEffect, useRef } from "react"; -// ── Lightweight markdown → JSX renderer ────────────────────────────────────── -// Handles: ## headings, **bold**, `code`, - bullets, blank-line paragraphs +// ── Inline markdown + GitHub formatter ────────────────────────────── +// Handles: ~~strike~~, **bold**, *italic*, _italic_, `code`, +// ![img](url), [link](url), bare https:// URLs, @mentions +function inlineFormat(text) { + const parts = []; + const re = + /(~~[^~]+~~|\*\*[^*]+\*\*|\*[^*\n]+\*|_[^_\n]+_|`[^`]+`|!\[[^\]]*\]\([^)]+\)|\[[^\]]+\]\([^)]+\)|https?:\/\/[^\s<>"')]+|@[\w-]+)/g; + let last = 0, + m, + k = 0; + + while ((m = re.exec(text)) !== null) { + if (m.index > last) + parts.push({text.slice(last, m.index)}); + const raw = m[0]; + + if (raw.startsWith("~~")) { + parts.push( + + {raw.slice(2, -2)} + , + ); + } else if (raw.startsWith("**")) { + parts.push( + + {raw.slice(2, -2)} + , + ); + } else if (raw.startsWith("*") || raw.startsWith("_")) { + parts.push( + + {raw.slice(1, -1)} + , + ); + } else if (raw.startsWith("`")) { + parts.push( + + {raw.slice(1, -1)} + , + ); + } else if (raw.startsWith("![")) { + const mm = raw.match(/^!\[([^\]]*)\]\(([^)]+)\)$/); + if (mm) { + parts.push( + {mm[1]} { + e.currentTarget.style.display = "none"; + }} + />, + ); + } + } else if (raw.startsWith("[")) { + const mm = raw.match(/^\[([^\]]+)\]\(([^)]+)\)$/); + if (mm) { + parts.push( + { + e.preventDefault(); + window.electron?.openExternal(mm[2]); + }} + style={{ + color: "var(--red)", + textDecoration: "underline", + cursor: "pointer", + }} + > + {mm[1]} + , + ); + } + } else if (raw.startsWith("@")) { + const username = raw.slice(1); + parts.push( + { + e.preventDefault(); + window.electron?.openExternal(`https://github.com/${username}`); + }} + style={{ + color: "var(--red)", + textDecoration: "none", + fontWeight: 500, + cursor: "pointer", + }} + > + {raw} + , + ); + } else if (raw.startsWith("http")) { + // Shorten PR/issue URLs to "#41" + let label = raw; + try { + const u = new URL(raw); + const prMatch = u.pathname.match(/\/(pull|issues?)\/(\d+)$/); + if (prMatch) label = `#${prMatch[2]}`; + else label = u.hostname.replace(/^www\./, "") + u.pathname; + } catch {} + parts.push( + { + e.preventDefault(); + window.electron?.openExternal(raw); + }} + style={{ + color: "var(--red)", + textDecoration: "underline", + cursor: "pointer", + fontSize: "0.95em", + }} + > + {label} + , + ); + } + + last = m.index + raw.length; + } + if (last < text.length) parts.push({text.slice(last)}); + return parts.length ? parts : text; +} + +// ── Block-level markdown renderer ──────────────────────────────────────────── +// Handles: # headings, --- hr, > blockquote, 1. numbered list, +// - /* bullets, ![img](url), blank lines, paragraphs function renderChangelog(text) { if (!text) return null; const lines = text.split("\n"); @@ -30,6 +178,7 @@ function renderChangelog(text) { ); continue; } + // h2 ## if (line.startsWith("## ")) { elements.push( @@ -50,6 +199,7 @@ function renderChangelog(text) { ); continue; } + // h1 # if (line.startsWith("# ")) { elements.push( @@ -68,7 +218,82 @@ function renderChangelog(text) { ); continue; } - // standalone image ![alt](url) + + // horizontal rule --- / *** / ___ (must come before bullet check) + if (/^([-*_])\1{2,}\s*$/.test(line.trim())) { + elements.push( +
, + ); + continue; + } + + // blockquote > … + if (line.startsWith("> ")) { + elements.push( +
+ {inlineFormat(line.slice(2))} +
, + ); + continue; + } + + // HTML img tag: ... + const htmlImgMatch = line.match(/]*\bsrc="([^"]+)"[^>]*>/i); + if (htmlImgMatch) { + const src = htmlImgMatch[1]; + const altMatch = line.match(/\balt="([^"]*)"/i); + const alt = altMatch ? altMatch[1] : ""; + elements.push( +
+ {alt} { + e.currentTarget.style.display = "none"; + }} + /> + {alt && ( +
+ {alt} +
+ )} +
, + ); + continue; + } + + // standalone markdown image ![alt](url) const imgMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)$/); if (imgMatch) { const [, alt, src] = imgMatch; @@ -104,7 +329,40 @@ function renderChangelog(text) { ); continue; } - // bullet - or * + + // numbered list 1. … + const numMatch = line.match(/^(\d+)\. (.*)$/); + if (numMatch) { + elements.push( +
+ + {numMatch[1]}. + + {inlineFormat(numMatch[2])} +
, + ); + continue; + } + + // unordered bullet - or * if (/^[-*] /.test(line)) { elements.push(
); continue; } - // normal paragraph line + + // normal paragraph elements.push(
last) - parts.push({text.slice(last, m.index)}); - const raw = m[0]; - if (raw.startsWith("**")) { - parts.push( - - {raw.slice(2, -2)} - , - ); - } else if (raw.startsWith("![")) { - const imgInline = raw.match(/^!\[([^\]]*)\]\(([^)]+)\)$/); - if (imgInline) { - const [, alt, src] = imgInline; - parts.push( - {alt} { - e.currentTarget.style.display = "none"; - }} - />, - ); - } - } else { - parts.push( - - {raw.slice(1, -1)} - , - ); - } - last = m.index + raw.length; - } - if (last < text.length) parts.push({text.slice(last)}); - return parts.length ? parts : text; -} - // ── Main component ──────────────────────────────────────────────────────────── export default function UpdateModal({ updateInfo, activeDownloads = 0, onClose, }) { - const { latest, url, changelog, assets } = updateInfo; + const { latest, current, url, changelog, assets } = updateInfo; - const [phase, setPhase] = useState("idle"); // idle | detecting | downloading | installing | done | error - const [format, setFormat] = useState(null); // "appimage" | "deb" | "exe" | null + const [phase, setPhase] = useState("idle"); // idle | downloading | installing | done | error + const [format, setFormat] = useState(null); // "appimage" | "deb" | "exe" | "dmg" | "dmg_arm64" | null const [progress, setProgress] = useState(0); const [progressLabel, setProgressLabel] = useState(""); const [errorMsg, setErrorMsg] = useState(""); @@ -283,18 +481,19 @@ export default function UpdateModal({ onClose(); }; - const formatLabel = - { appimage: "AppImage", deb: ".deb package", exe: "Windows installer" }[ - format - ] || "installer"; + const formatLabel = { + appimage: "AppImage", + deb: ".deb package", + exe: "Windows installer", + dmg: "macOS installer", + dmg_arm64: "macOS (Apple Silicon)", + }[format] || "installer"; + + const busy = phase === "downloading" || phase === "installing"; return (
- Version{" "} + {current && ( + <> + v{current} + + + )} { @@ -408,16 +609,13 @@ export default function UpdateModal({