Electron updater now uses Electron release metadata only Removed legacy Tauri package and migration workflow Replaced Tauri shim usage with the desktop bridge
139 lines
3.7 KiB
TypeScript
139 lines
3.7 KiB
TypeScript
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
|
|
|
type DesktopBridgeGlobal = {
|
|
openExternal?: (url: string) => Promise<unknown>;
|
|
};
|
|
|
|
const parseUrlSafely = (value: string): URL | null => {
|
|
try {
|
|
return new URL(value);
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export const isExternalHttpUrl = (url: string): boolean => {
|
|
const parsed = parseUrlSafely(url.trim());
|
|
if (!parsed) {
|
|
return false;
|
|
}
|
|
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
};
|
|
|
|
export const getExternalFaviconUrl = (url: string): string | null => {
|
|
const parsed = parseUrlSafely(url.trim());
|
|
if (!parsed || (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')) {
|
|
return null;
|
|
}
|
|
|
|
return `https://icons.duckduckgo.com/ip3/${parsed.hostname.toLowerCase()}.ico`;
|
|
};
|
|
|
|
const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1']);
|
|
|
|
/**
|
|
* Returns true when the URL is an http(s) URL pointing at a loopback host
|
|
* (localhost, 127.0.0.1, 0.0.0.0, ::1). Used to decide whether to offer an in-app
|
|
* preview pane instead of opening the system browser.
|
|
*/
|
|
export const isLoopbackHttpUrl = (url: string): boolean => {
|
|
const parsed = parseUrlSafely(url.trim());
|
|
if (!parsed) {
|
|
return false;
|
|
}
|
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
return false;
|
|
}
|
|
return LOOPBACK_HOSTNAMES.has(parsed.hostname.toLowerCase());
|
|
};
|
|
|
|
const LOOPBACK_URL_PATTERN
|
|
// eslint-disable-next-line no-control-regex
|
|
= /\bhttps?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d{2,5})?(?:\/[^\s<>"'`\u0000-\u001f]*)?/gi;
|
|
|
|
/**
|
|
* Extracts loopback http(s) URLs from a free-text string. Returns unique URLs
|
|
* in order of first appearance. Trailing punctuation that is unlikely to be
|
|
* part of a real URL is stripped.
|
|
*/
|
|
export const extractLoopbackUrls = (text: string): string[] => {
|
|
if (!text) {
|
|
return [];
|
|
}
|
|
const matches = text.match(LOOPBACK_URL_PATTERN);
|
|
if (!matches || matches.length === 0) {
|
|
return [];
|
|
}
|
|
const seen = new Set<string>();
|
|
const out: string[] = [];
|
|
for (const raw of matches) {
|
|
const cleaned = raw.replace(/[),.;:!?'"`]+$/g, '');
|
|
if (!cleaned || !isLoopbackHttpUrl(cleaned)) {
|
|
continue;
|
|
}
|
|
if (seen.has(cleaned)) {
|
|
continue;
|
|
}
|
|
seen.add(cleaned);
|
|
out.push(cleaned);
|
|
}
|
|
return out;
|
|
};
|
|
|
|
/**
|
|
* Opens an external URL in the system browser.
|
|
* In desktop runtime, uses the native shell for proper handling.
|
|
* Falls back to window.open() for web runtime.
|
|
*
|
|
* @param url - The URL to open
|
|
* @returns Promise<boolean> - true if the URL was opened successfully
|
|
*/
|
|
export const openExternalUrl = async (url: string): Promise<boolean> => {
|
|
if (typeof window === 'undefined') {
|
|
return false;
|
|
}
|
|
|
|
const target = url.trim();
|
|
if (!target) {
|
|
return false;
|
|
}
|
|
|
|
const parsed = parseUrlSafely(target);
|
|
if (!parsed) {
|
|
return false;
|
|
}
|
|
|
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
return false;
|
|
}
|
|
|
|
const normalizedTarget = parsed.toString();
|
|
|
|
const runtimeApis = getRegisteredRuntimeAPIs();
|
|
if (runtimeApis?.runtime?.isVSCode && runtimeApis.vscode?.openExternalUrl) {
|
|
try {
|
|
await runtimeApis.vscode.openExternalUrl(normalizedTarget);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const desktop = (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__;
|
|
if (desktop?.openExternal) {
|
|
try {
|
|
await desktop.openExternal(normalizedTarget);
|
|
return true;
|
|
} catch {
|
|
// Fall through to window.open
|
|
}
|
|
}
|
|
|
|
try {
|
|
window.open(normalizedTarget, '_blank', 'noopener,noreferrer');
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|