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  + const imgMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)$/); + if (imgMatch) { + const [, alt, src] = imgMatch; + elements.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`,
+// , [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(
+
{
+ 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, , 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 
+
+ // 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(
+
+
{
+ e.currentTarget.style.display = "none";
+ }}
+ />
+ {alt && (
+
+ {alt}
+
+ )}
+ ,
+ );
+ continue;
+ }
+
+ // standalone markdown image 
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(
-
{
- 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({