AvaloniaStack/Avalonia-PC/Views/MainWindow.BridgeScript.cs

340 lines
11 KiB
C#
Raw Permalink Normal View History

namespace Avalonia_PC.Views
{
public partial class MainWindow
{
private const string BridgeScript = """
if (!window.__appBridgeInstalled) {
window.__appBridgeInstalled = true;
window.isWebView2 = true;
const pending = new Map();
const tryOpenDevTools = () => {
window.invokeCSharpAction(JSON.stringify({ kind: 'app-open-devtools' }));
};
window.__dispatchAppResponse = function(jsonStr) {
const payload = JSON.parse(jsonStr);
const responseId = payload.id ?? payload.Id;
const entry = pending.get(responseId);
if (!entry) return;
pending.delete(responseId);
entry.resolve(new Response(payload.body ?? payload.Body ?? '', {
status: payload.statusCode ?? payload.StatusCode ?? 200,
statusText: payload.statusMessage ?? payload.StatusMessage ?? 'OK',
headers: payload.headers ?? payload.Headers ?? { 'Content-Type': 'application/json' }
}));
};
const nativeFetch = window.fetch ? window.fetch.bind(window) : null;
const NativeXMLHttpRequest = window.XMLHttpRequest;
const sendAppBridgeRequest = ({ requestUrl, method, headers, body, timeoutMs = 30000 }) => {
const id = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
const responsePromise = new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
pending.delete(id);
reject(new Error(`Timed out waiting for ${requestUrl}`));
}, timeoutMs);
pending.set(id, {
resolve: response => { clearTimeout(timeoutId); resolve(response); },
reject: error => { clearTimeout(timeoutId); reject(error); }
});
});
window.invokeCSharpAction(JSON.stringify({
kind: 'app-request',
id,
url: requestUrl,
method,
headers,
body
}));
return responsePromise;
};
document.addEventListener('keydown', event => {
if (event.key === 'F12' || (event.ctrlKey && event.shiftKey && (event.key === 'I' || event.key === 'i'))) {
event.preventDefault();
tryOpenDevTools();
}
}, true);
document.addEventListener('contextmenu', event => {
if (event.shiftKey) {
event.preventDefault();
tryOpenDevTools();
}
}, true);
window.fetch = async (input, init) => {
const request = input instanceof Request ? input : null;
const requestUrl = typeof input === 'string' || input instanceof URL
? input.toString()
: request?.url;
if (!requestUrl || !requestUrl.startsWith('app://')) {
if (!nativeFetch) throw new Error('window.fetch is not available.');
return nativeFetch(input, init);
}
const combinedHeaders = new Headers(request?.headers);
if (init?.headers) {
new Headers(init.headers).forEach((value, key) => combinedHeaders.set(key, value));
}
const headers = {};
combinedHeaders.forEach((value, key) => headers[key] = value);
let body = init?.body;
if (body === undefined && request) {
body = await request.clone().text();
}
if (body && typeof body !== 'string') {
body = await new Response(body).text();
}
return sendAppBridgeRequest({
requestUrl,
method: init?.method ?? request?.method ?? 'GET',
headers,
body: body ?? null,
timeoutMs: 30000
});
};
class BridgeXMLHttpRequest {
constructor() {
this._native = new NativeXMLHttpRequest();
this._isAppRequest = false;
this._requestUrl = '';
this._method = 'GET';
this._headers = {};
this._responseHeaders = {};
this._responseHeadersRaw = '';
this._aborted = false;
this.readyState = 0;
this.status = 0;
this.statusText = '';
this.response = null;
this.responseText = '';
this.responseType = '';
this.responseURL = '';
this.timeout = 0;
this.withCredentials = false;
this.onreadystatechange = null;
this.onload = null;
this.onerror = null;
this.ontimeout = null;
this.onabort = null;
this.onloadend = null;
this.upload = {
addEventListener: () => {},
removeEventListener: () => {}
};
this._native.onreadystatechange = () => {
if (this._isAppRequest) {
return;
}
this.readyState = this._native.readyState;
this.status = this._native.status;
this.statusText = this._native.statusText;
this.responseURL = this._native.responseURL ?? '';
this.response = this._native.response;
this.responseText = this._native.responseText ?? '';
this._raiseReadyStateChange();
};
this._native.onload = event => {
if (!this._isAppRequest && typeof this.onload === 'function') {
this.onload(event);
}
};
this._native.onerror = event => {
if (!this._isAppRequest && typeof this.onerror === 'function') {
this.onerror(event);
}
};
this._native.ontimeout = event => {
if (!this._isAppRequest && typeof this.ontimeout === 'function') {
this.ontimeout(event);
}
};
this._native.onabort = event => {
if (!this._isAppRequest && typeof this.onabort === 'function') {
this.onabort(event);
}
};
this._native.onloadend = event => {
if (!this._isAppRequest && typeof this.onloadend === 'function') {
this.onloadend(event);
}
};
}
open(method, url, async = true, user, password) {
const requestUrl = typeof url === 'string' || url instanceof URL
? url.toString()
: `${url ?? ''}`;
this._requestUrl = requestUrl;
this._method = method ?? 'GET';
this._isAppRequest = requestUrl.startsWith('app://');
this._headers = {};
this._responseHeaders = {};
this._responseHeadersRaw = '';
this._aborted = false;
if (!this._isAppRequest) {
this._native.open(method, url, async, user, password);
return;
}
this.readyState = 1;
this._raiseReadyStateChange();
}
setRequestHeader(name, value) {
if (!this._isAppRequest) {
this._native.setRequestHeader(name, value);
return;
}
this._headers[name] = value;
}
getAllResponseHeaders() {
if (!this._isAppRequest) {
return this._native.getAllResponseHeaders();
}
return this._responseHeadersRaw;
}
getResponseHeader(name) {
if (!this._isAppRequest) {
return this._native.getResponseHeader(name);
}
return this._responseHeaders[name.toLowerCase()] ?? null;
}
overrideMimeType(mimeType) {
if (!this._isAppRequest && typeof this._native.overrideMimeType === 'function') {
this._native.overrideMimeType(mimeType);
}
}
abort() {
if (!this._isAppRequest) {
this._native.abort();
return;
}
this._aborted = true;
if (typeof this.onabort === 'function') {
this.onabort();
}
if (typeof this.onloadend === 'function') {
this.onloadend();
}
}
async send(body = null) {
if (!this._isAppRequest) {
this._native.send(body);
return;
}
let requestBody = body;
if (requestBody && typeof requestBody !== 'string') {
requestBody = await new Response(requestBody).text();
}
try {
const response = await sendAppBridgeRequest({
requestUrl: this._requestUrl,
method: this._method,
headers: this._headers,
body: requestBody ?? null,
timeoutMs: this.timeout > 0 ? this.timeout : 30000
});
if (this._aborted) {
return;
}
this.status = response.status;
this.statusText = response.statusText;
this.responseURL = this._requestUrl;
this._responseHeaders = {};
this._responseHeadersRaw = '';
response.headers.forEach((value, key) => {
this._responseHeaders[key.toLowerCase()] = value;
this._responseHeadersRaw += `${key}: ${value}\r\n`;
});
const text = await response.text();
this.responseText = text;
this.response = this.responseType === 'json'
? (text ? JSON.parse(text) : null)
: text;
this.readyState = 4;
this._raiseReadyStateChange();
if (typeof this.onload === 'function') {
this.onload();
}
if (typeof this.onloadend === 'function') {
this.onloadend();
}
} catch (error) {
if (this._aborted) {
return;
}
this.status = 0;
this.statusText = '';
this.readyState = 4;
this._raiseReadyStateChange();
const errorMessage = error?.message ?? '';
if (errorMessage.includes('Timed out waiting') && typeof this.ontimeout === 'function') {
this.ontimeout(error);
} else if (typeof this.onerror === 'function') {
this.onerror(error);
}
if (typeof this.onloadend === 'function') {
this.onloadend();
}
}
}
_raiseReadyStateChange() {
if (typeof this.onreadystatechange === 'function') {
this.onreadystatechange();
}
}
}
window.XMLHttpRequest = BridgeXMLHttpRequest;
}
""";
}
}