diff --git a/apps/client/src/services/link.ts b/apps/client/src/services/link.ts index 8a963768e5..3405161dd5 100644 --- a/apps/client/src/services/link.ts +++ b/apps/client/src/services/link.ts @@ -339,11 +339,24 @@ export function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDo // Enable protocols supported by CKEditor 5 to be clickable. if (utils.isElectron()) { const electron = utils.dynamicRequire("electron"); - electron.shell.openExternal(hrefLink).catch((e: unknown) => { + const reportLinkError = (e: unknown) => { const message = e instanceof Error ? e.message : String(e); logError(`Failed to open link '${hrefLink}': ${message}`); showError(t("link.failed_to_open", { href: hrefLink, message })); - }); + }; + + if (hrefLink.toLowerCase().startsWith("file:")) { + // shell.openExternal mishandles Unicode file:// URLs on Windows; + // convert to a filesystem path and use shell.openPath instead. + // Normalize file://c:/... (2 slashes — drive read as host) to file:///c:/... + const normalized = hrefLink.replace(/^file:\/\/(?=[a-zA-Z]:)/i, "file:///"); + const { fileURLToPath } = utils.dynamicRequire("url"); + electron.shell.openPath(fileURLToPath(normalized)).then((err: string) => { + if (err) reportLinkError(new Error(err)); + }).catch(reportLinkError); + } else { + electron.shell.openExternal(hrefLink).catch(reportLinkError); + } } else { window.open(hrefLink, "_blank"); } diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index 67bbdd0e26..d484ba55ad 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -360,7 +360,8 @@ function copySelectionToClipboard() { type dynamicRequireMappings = { "@electron/remote": typeof import("@electron/remote"), "electron": typeof import("electron"), - "child_process": typeof import("child_process") + "child_process": typeof import("child_process"), + "url": typeof import("url") }; export function dynamicRequire(moduleName: T): Awaited{