mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
1 parent
3bed2bfe77
commit
b680f0bc27
6 files changed
+1590
-645
No files matched your search
@@ -18,10 +18,10 @@ import "embed"
|
|||||||
|
|
||||||
// FS WebUI嵌入静态文件
|
// FS WebUI嵌入静态文件
|
||||||
//
|
//
|
||||||
//go:embed index.html ofdgo.css ofdgo.ico ofdgo.js ofdgo.json ofdgo.png ofdgo.svg ofdgo_edit.js ofdgo_font.js ofdgo_host.js ofdgo_wasm.js ofdgo_work.js wasm_exec.js
|
//go:embed index.html ofdgo.css ofdgo.ico ofdgo.js ofdgo.json ofdgo.png ofdgo.svg ofdgo_edit.js ofdgo_font.js ofdgo_host.js ofdgo_util.js ofdgo_wasm.js ofdgo_work.js wasm_exec.js
|
||||||
var FS embed.FS
|
var FS embed.FS
|
||||||
|
|
||||||
// NativeFS WebView嵌入静态文件
|
// NativeFS WebView嵌入静态文件
|
||||||
//
|
//
|
||||||
//go:embed index.html ofdgo.css ofdgo.ico ofdgo.js ofdgo.json ofdgo.png ofdgo.svg ofdgo_edit.js ofdgo_font.js ofdgo_host.js
|
//go:embed index.html ofdgo.css ofdgo.ico ofdgo.js ofdgo.json ofdgo.png ofdgo.svg ofdgo_edit.js ofdgo_font.js ofdgo_host.js ofdgo_util.js
|
||||||
var NativeFS embed.FS
|
var NativeFS embed.FS
|
||||||
@@ -2095,7 +2095,7 @@ body[data-hide-meta] .meta-panel {
|
|||||||
.settings-dialog > form {
|
.settings-dialog > form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-height: calc(100dvh - 26px);
|
max-height: calc(100dvh - var(--dialog-inset, 0px) - 26px);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1158
-625
File diff suppressed because it is too large.
Load diff
+114
-16
@@ -19,6 +19,7 @@ class BrowserScheduler {
|
|||||||
this.limit = Math.min(this.capacity, Math.max(1, Math.floor(this.budget / (512 << 20))));
|
this.limit = Math.min(this.capacity, Math.max(1, Math.floor(this.budget / (512 << 20))));
|
||||||
this.workers = new Set();
|
this.workers = new Set();
|
||||||
this.queue = [];
|
this.queue = [];
|
||||||
|
this.sequence = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get memory() {
|
get memory() {
|
||||||
@@ -26,21 +27,34 @@ class BrowserScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
active() {
|
active() {
|
||||||
return [...this.workers].filter(worker => worker.requests.size).length;
|
return [...this.workers].reduce((total, worker) => total + new Set([...worker.operations.values()].map(data => data.engineID)).size, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
owner(worker, data) {
|
||||||
|
return worker.shared ? worker.sessions.get(data.engineID) : worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
priority(task) {
|
||||||
|
return Number(task.worker.batch || this.owner(task.worker, task.data)?.foreground === false) + Number(!!task.data.preload);
|
||||||
}
|
}
|
||||||
|
|
||||||
submit(worker, data, transfer) {
|
submit(worker, data, transfer) {
|
||||||
if (worker.primary) { this.send({worker, data, transfer}); return; }
|
if (data.preload && this.owner(worker, data)?.pagesPaused) {
|
||||||
this.queue.push({worker, data, transfer, queued:performance.now()});
|
worker.receive({data:{engineID:data.engineID, id:data.id, ok:false, canceled:true, error:"预加载已暂停"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.queue.push({worker, data, transfer, queued:performance.now(), order:++this.sequence});
|
||||||
this.drain();
|
this.drain();
|
||||||
}
|
}
|
||||||
|
|
||||||
send(task) {
|
send(task) {
|
||||||
const {worker, data, transfer} = task;
|
const {worker, data, transfer} = task;
|
||||||
worker.requests.set(requestKey(worker, data), performance.now());
|
worker.requests.set(requestKey(worker, data), performance.now());
|
||||||
|
worker.operations.set(requestKey(worker, data), data);
|
||||||
try { worker.worker.postMessage(data, transfer); }
|
try { worker.worker.postMessage(data, transfer); }
|
||||||
catch (error) {
|
catch (error) {
|
||||||
worker.requests.delete(requestKey(worker, data));
|
worker.requests.delete(requestKey(worker, data));
|
||||||
|
worker.operations.delete(requestKey(worker, data));
|
||||||
worker.receive({data:{engineID:data.engineID, id:data.id, ok:false, error:error.message}});
|
worker.receive({data:{engineID:data.engineID, id:data.id, ok:false, error:error.message}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +65,7 @@ class BrowserScheduler {
|
|||||||
const key = requestKey(worker, data);
|
const key = requestKey(worker, data);
|
||||||
const started = worker.requests.get(key);
|
const started = worker.requests.get(key);
|
||||||
worker.requests.delete(key);
|
worker.requests.delete(key);
|
||||||
|
worker.operations.delete(key);
|
||||||
if (this.memory > this.budget) this.limit = Math.max(1, this.limit - 1);
|
if (this.memory > this.budget) this.limit = Math.max(1, this.limit - 1);
|
||||||
else if (started !== undefined && data.ok && performance.now() - started > 100 && this.memory + Math.max(128 << 20, worker.memory) < this.budget) this.limit = Math.min(this.capacity, this.limit + 1);
|
else if (started !== undefined && data.ok && performance.now() - started > 100 && this.memory + Math.max(128 << 20, worker.memory) < this.budget) this.limit = Math.min(this.capacity, this.limit + 1);
|
||||||
}
|
}
|
||||||
@@ -63,31 +78,45 @@ class BrowserScheduler {
|
|||||||
if (this.memory + (64 << 20) <= this.budget) break;
|
if (this.memory + (64 << 20) <= this.budget) break;
|
||||||
if (!worker.requests.size) worker.reclaim?.();
|
if (!worker.requests.size) worker.reclaim?.();
|
||||||
}
|
}
|
||||||
this.queue.sort((a, b) => Number(a.worker.batch) - Number(b.worker.batch) + (a.queued - b.queued) / 2000);
|
this.queue.sort((a, b) => this.priority(a) - this.priority(b) + (a.queued - b.queued) / 2000);
|
||||||
for (let i = 0; i < this.queue.length;) {
|
for (let i = 0; i < this.queue.length;) {
|
||||||
const task = this.queue[i];
|
const task = this.queue[i];
|
||||||
if (task.worker.closed) { this.queue.splice(i, 1); continue; }
|
if (task.worker.closed) { this.queue.splice(i, 1); continue; }
|
||||||
|
if (this.queue.some(previous => previous.worker === task.worker && previous.data.engineID === task.data.engineID && previous.order < task.order)) { i++; continue; }
|
||||||
|
const busy = [...task.worker.operations.values()].some(data => data.engineID === task.data.engineID && data.name !== "ofdgoSaveSnapshot");
|
||||||
|
const snapshot = task.data.name === "ofdgoSaveSnapshot" && [...task.worker.operations.values()].some(data => data.engineID === task.data.engineID && data.name === "ofdgoSaveSnapshot");
|
||||||
const active = this.active();
|
const active = this.active();
|
||||||
if (task.worker.requests.size || active >= this.limit || active && this.memory + (64 << 20) > this.budget) { i++; continue; }
|
const interactive = task.worker.primary && this.owner(task.worker, task.data)?.foreground === true && !task.data.preload;
|
||||||
|
const foregroundActive = [...this.workers].some(worker => [...worker.operations.values()].some(data => data.name !== "ofdgoSaveSnapshot" && worker.primary && this.owner(worker, data)?.foreground === true && !data.preload));
|
||||||
|
const continuing = task.worker.primary && [...task.worker.operations.values()].some(data => data.engineID === task.data.engineID);
|
||||||
|
const available = continuing || active < this.limit || interactive && !foregroundActive && active < this.limit + 1;
|
||||||
|
if (busy || snapshot || !available || !task.worker.primary && active && this.memory + (64 << 20) > this.budget) { i++; continue; }
|
||||||
this.queue.splice(i, 1);
|
this.queue.splice(i, 1);
|
||||||
this.send(task);
|
this.send(task);
|
||||||
}
|
}
|
||||||
} finally { this.draining = false; }
|
} finally { this.draining = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(worker, id) {
|
cancel(worker, data) {
|
||||||
const index = this.queue.findIndex(task => task.worker === worker && task.data.id === id);
|
const index = this.queue.findIndex(task => task.worker === worker && requestKey(worker, task.data) === requestKey(worker, data));
|
||||||
if (index < 0) return false;
|
if (index < 0) return false;
|
||||||
this.queue.splice(index, 1);
|
const task = this.queue.splice(index, 1)[0];
|
||||||
worker.receive({data:{engineID:worker.engineID, id, ok:false, canceled:true, error:"操作已取消"}});
|
worker.receive({data:{engineID:task.data.engineID, id:data.id, ok:false, canceled:true, error:"操作已取消"}});
|
||||||
this.drain();
|
this.drain();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pause(worker, engineID) {
|
||||||
|
const paused = this.queue.filter(task => task.worker === worker && task.data.engineID === engineID && task.data.preload);
|
||||||
|
this.queue = this.queue.filter(task => !paused.includes(task));
|
||||||
|
for (const task of paused) worker.receive({data:{engineID, id:task.data.id, ok:false, canceled:true, error:"预加载已暂停"}});
|
||||||
|
}
|
||||||
|
|
||||||
remove(worker) {
|
remove(worker) {
|
||||||
this.workers.delete(worker);
|
this.workers.delete(worker);
|
||||||
this.queue = this.queue.filter(task => task.worker !== worker);
|
this.queue = this.queue.filter(task => task.worker !== worker);
|
||||||
worker.requests.clear();
|
worker.requests.clear();
|
||||||
|
worker.operations.clear();
|
||||||
this.drain();
|
this.drain();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,6 +131,9 @@ class BrowserWorker {
|
|||||||
this.closingSessions = new Set();
|
this.closingSessions = new Set();
|
||||||
this.batch = !options.primary && !options.page;
|
this.batch = !options.primary && !options.page;
|
||||||
this.requests = new Map();
|
this.requests = new Map();
|
||||||
|
this.operations = new Map();
|
||||||
|
this.foreground = !this.batch;
|
||||||
|
this.pagesPaused = false;
|
||||||
this.memory = 64 << 20;
|
this.memory = 64 << 20;
|
||||||
this.reservation = 0;
|
this.reservation = 0;
|
||||||
this.closed = false;
|
this.closed = false;
|
||||||
@@ -135,6 +167,10 @@ class BrowserWorker {
|
|||||||
}
|
}
|
||||||
if (event.data.type === "closed") {
|
if (event.data.type === "closed") {
|
||||||
if (!this.closingSessions.delete(event.data.engineID)) return;
|
if (!this.closingSessions.delete(event.data.engineID)) return;
|
||||||
|
for (const [key, data] of this.operations) if (data.engineID === event.data.engineID) {
|
||||||
|
this.requests.delete(key);
|
||||||
|
this.operations.delete(key);
|
||||||
|
}
|
||||||
if (!this.sessions.size && !this.closingSessions.size) this.terminate();
|
if (!this.sessions.size && !this.closingSessions.size) this.terminate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -154,11 +190,22 @@ class BrowserWorker {
|
|||||||
if (this.closed) throw new DOMException("引擎已关闭", "AbortError");
|
if (this.closed) throw new DOMException("引擎已关闭", "AbortError");
|
||||||
if (!this.shared) data = {...data, engineID:this.engineID};
|
if (!this.shared) data = {...data, engineID:this.engineID};
|
||||||
if (data.type) {
|
if (data.type) {
|
||||||
if (data.type === "cancel" && engineScheduler.cancel(this, data.id)) return;
|
if (data.type === "cancel" && engineScheduler.cancel(this, data)) return;
|
||||||
this.worker.postMessage(data, transfer);
|
this.worker.postMessage(data, transfer);
|
||||||
} else engineScheduler.submit(this, data, transfer);
|
} else engineScheduler.submit(this, data, transfer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setForeground(foreground) {
|
||||||
|
this.foreground = !!foreground;
|
||||||
|
this.pausePages(!this.foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
pausePages(paused = true) {
|
||||||
|
this.pagesPaused = !!paused;
|
||||||
|
if (this.pagesPaused) engineScheduler.pause(this, this.engineID);
|
||||||
|
engineScheduler.drain();
|
||||||
|
}
|
||||||
|
|
||||||
terminate() {
|
terminate() {
|
||||||
if (this.closed) return;
|
if (this.closed) return;
|
||||||
this.closed = true;
|
this.closed = true;
|
||||||
@@ -180,6 +227,8 @@ class BrowserSession {
|
|||||||
this.runtime = runtime;
|
this.runtime = runtime;
|
||||||
this.engineID = `engine-${++engineSequence}`;
|
this.engineID = `engine-${++engineSequence}`;
|
||||||
this.requests = new Set();
|
this.requests = new Set();
|
||||||
|
this.foreground = true;
|
||||||
|
this.pagesPaused = false;
|
||||||
this.closed = false;
|
this.closed = false;
|
||||||
runtime.sessions.set(this.engineID, this);
|
runtime.sessions.set(this.engineID, this);
|
||||||
runtime.postMessage({type:"open", engineID:this.engineID});
|
runtime.postMessage({type:"open", engineID:this.engineID});
|
||||||
@@ -198,11 +247,22 @@ class BrowserSession {
|
|||||||
this.runtime.postMessage({...data, engineID:this.engineID}, transfer);
|
this.runtime.postMessage({...data, engineID:this.engineID}, transfer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setForeground(foreground) {
|
||||||
|
this.foreground = !!foreground;
|
||||||
|
this.pausePages(!this.foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
pausePages(paused = true) {
|
||||||
|
this.pagesPaused = !!paused;
|
||||||
|
if (this.pagesPaused) engineScheduler.pause(this.runtime, this.engineID);
|
||||||
|
engineScheduler.drain();
|
||||||
|
}
|
||||||
|
|
||||||
terminate() {
|
terminate() {
|
||||||
if (this.closed) return;
|
if (this.closed) return;
|
||||||
this.closed = true;
|
this.closed = true;
|
||||||
this.runtime.sessions.delete(this.engineID);
|
this.runtime.sessions.delete(this.engineID);
|
||||||
for (const id of this.requests) this.runtime.requests.delete(requestKey(this.runtime, {engineID:this.engineID, id}));
|
engineScheduler.queue = engineScheduler.queue.filter(task => task.worker !== this.runtime || task.data.engineID !== this.engineID);
|
||||||
this.requests.clear();
|
this.requests.clear();
|
||||||
if (!this.runtime.closed) {
|
if (!this.runtime.closed) {
|
||||||
this.runtime.closingSessions.add(this.engineID);
|
this.runtime.closingSessions.add(this.engineID);
|
||||||
@@ -248,18 +308,40 @@ class BrowserEngine {
|
|||||||
this.budget = engineScheduler.budget;
|
this.budget = engineScheduler.budget;
|
||||||
this.closed = false;
|
this.closed = false;
|
||||||
this.source = null;
|
this.source = null;
|
||||||
|
this.pageSource = null;
|
||||||
|
this.foreground = true;
|
||||||
|
this.pagesPaused = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
configurePages(source) {
|
configurePages(source) {
|
||||||
|
const current = this.pageSource;
|
||||||
|
const fonts = (source?.fonts || []).map(font => font?.data !== undefined ? {name:font.name, data:font.data} : font);
|
||||||
|
const sameFonts = current && fonts.length === current.fonts.length && fonts.every((font, index) => font === current.fonts[index] || font?.data !== undefined && font.name === current.fonts[index]?.name && font.data === current.fonts[index]?.data);
|
||||||
|
if (source && current && source.file === current.file && (source.document || 0) === current.document && (source.version || "") === current.version && source.annotations === current.annotations && source.revision === current.revision && sameFonts) return;
|
||||||
this.invalidatePages();
|
this.invalidatePages();
|
||||||
if (!source || this.closed) return;
|
if (!source || this.closed) return;
|
||||||
|
this.pageSource = {...source, fonts, document:source.document || 0, version:source.version || ""};
|
||||||
this.source = {...source, file: source.file instanceof Blob ? source.file : new Blob([source.file])};
|
this.source = {...source, file: source.file instanceof Blob ? source.file : new Blob([source.file])};
|
||||||
this.parallelism = engineScheduler.limit;
|
this.parallelism = engineScheduler.limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setForeground(foreground) {
|
||||||
|
this.foreground = !!foreground;
|
||||||
|
this.primary.setForeground(this.foreground);
|
||||||
|
for (const slot of this.slots) slot.worker.setForeground(this.foreground);
|
||||||
|
this.pausePages(!this.foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
pausePages(paused = true) {
|
||||||
|
this.pagesPaused = !!paused;
|
||||||
|
this.primary.pausePages(this.pagesPaused);
|
||||||
|
for (const slot of this.slots) slot.worker.pausePages(this.pagesPaused);
|
||||||
|
}
|
||||||
|
|
||||||
invalidatePages() {
|
invalidatePages() {
|
||||||
this.revision++;
|
this.revision++;
|
||||||
this.source = null;
|
this.source = null;
|
||||||
|
this.pageSource = null;
|
||||||
this.parallelism = 1;
|
this.parallelism = 1;
|
||||||
for (const slot of this.slots) this.closeSlot(slot);
|
for (const slot of this.slots) this.closeSlot(slot);
|
||||||
}
|
}
|
||||||
@@ -274,6 +356,8 @@ class BrowserEngine {
|
|||||||
|
|
||||||
createSlot(source) {
|
createSlot(source) {
|
||||||
const worker = createEngineWorker({page:true, url:this.workerURL});
|
const worker = createEngineWorker({page:true, url:this.workerURL});
|
||||||
|
worker.setForeground(this.foreground);
|
||||||
|
worker.pausePages(this.pagesPaused);
|
||||||
const slot = {worker, pending: new Map(), busy: false, memory: Math.max(128 << 20, this.memory), pages:new Set(), lastPage:0};
|
const slot = {worker, pending: new Map(), busy: false, memory: Math.max(128 << 20, this.memory), pages:new Set(), lastPage:0};
|
||||||
worker.reservation = slot.memory;
|
worker.reservation = slot.memory;
|
||||||
worker.reclaim = () => { if (!slot.busy) this.closeSlot(slot); };
|
worker.reclaim = () => { if (!slot.busy) this.closeSlot(slot); };
|
||||||
@@ -289,7 +373,7 @@ class BrowserEngine {
|
|||||||
if (!request) return;
|
if (!request) return;
|
||||||
slot.pending.delete(data.id);
|
slot.pending.delete(data.id);
|
||||||
if (data.ok) request.resolve(data);
|
if (data.ok) request.resolve(data);
|
||||||
else request.reject(Object.assign(new Error(data.error), {code: data.code}));
|
else request.reject(Object.assign(new Error(data.error), {code:data.code, name:data.canceled ? "AbortError" : "Error"}));
|
||||||
};
|
};
|
||||||
worker.onerror = worker.onmessageerror = () => this.closeSlot(slot, new Error("页面加载失败"));
|
worker.onerror = worker.onmessageerror = () => this.closeSlot(slot, new Error("页面加载失败"));
|
||||||
});
|
});
|
||||||
@@ -303,18 +387,22 @@ class BrowserEngine {
|
|||||||
return slot;
|
return slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
callSlot(slot, name, args) {
|
callSlot(slot, name, args, preload = false) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!this.slots.has(slot)) { reject(new DOMException("页面加载已取消", "AbortError")); return; }
|
if (!this.slots.has(slot)) { reject(new DOMException("页面加载已取消", "AbortError")); return; }
|
||||||
const id = --this.sequence;
|
const id = --this.sequence;
|
||||||
slot.pending.set(id, {resolve, reject});
|
slot.pending.set(id, {resolve, reject});
|
||||||
try { slot.worker.postMessage({id, name, args}); }
|
try { slot.worker.postMessage({id, name, args, preload}); }
|
||||||
catch (error) { slot.pending.delete(id); reject(error); }
|
catch (error) { slot.pending.delete(id); reject(error); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
postMessage(data, transfer) {
|
postMessage(data, transfer) {
|
||||||
if (this.closed) return;
|
if (this.closed) return;
|
||||||
|
if (!data.type && data.preload && this.pagesPaused) {
|
||||||
|
this.onmessage?.({data:{engineID:this.engineID, id:data.id, ok:false, canceled:true, error:"预加载已暂停"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (data.type === "cancel") {
|
if (data.type === "cancel") {
|
||||||
const slot = this.requests.get(data.id);
|
const slot = this.requests.get(data.id);
|
||||||
if (slot) this.closeSlot(slot);
|
if (slot) this.closeSlot(slot);
|
||||||
@@ -339,7 +427,7 @@ class BrowserEngine {
|
|||||||
}
|
}
|
||||||
let slot = preferred || [...this.slots].filter(slot => !slot.busy).sort((a,b) => Math.abs(a.lastPage-data.args[0])-Math.abs(b.lastPage-data.args[0]))[0];
|
let slot = preferred || [...this.slots].filter(slot => !slot.busy).sort((a,b) => Math.abs(a.lastPage-data.args[0])-Math.abs(b.lastPage-data.args[0]))[0];
|
||||||
const memory = engineScheduler.memory;
|
const memory = engineScheduler.memory;
|
||||||
if (!slot && this.slots.size < this.parallelism - 1 && memory + Math.max(128 << 20, this.memory) <= this.budget) slot = this.createSlot(this.source);
|
if (!slot && this.foreground && !this.pagesPaused && this.slots.size < this.parallelism - 1 && memory + Math.max(128 << 20, this.memory) <= this.budget) slot = this.createSlot(this.source);
|
||||||
if (!slot) {
|
if (!slot) {
|
||||||
this.requests.set(data.id, null);
|
this.requests.set(data.id, null);
|
||||||
this.primary.postMessage(data, transfer);
|
this.primary.postMessage(data, transfer);
|
||||||
@@ -349,7 +437,7 @@ class BrowserEngine {
|
|||||||
this.requests.set(data.id, slot);
|
this.requests.set(data.id, slot);
|
||||||
const revision = this.revision;
|
const revision = this.revision;
|
||||||
let fallback = false;
|
let fallback = false;
|
||||||
slot.ready.then(() => this.callSlot(slot, data.name, data.args)).then(result => {
|
slot.ready.then(() => this.callSlot(slot, data.name, data.args, !!data.preload)).then(result => {
|
||||||
if (revision !== this.revision) throw new DOMException("页面加载已取消", "AbortError");
|
if (revision !== this.revision) throw new DOMException("页面加载已取消", "AbortError");
|
||||||
slot.lastPage = data.args[0];
|
slot.lastPage = data.args[0];
|
||||||
slot.pages.delete(slot.lastPage);
|
slot.pages.delete(slot.lastPage);
|
||||||
@@ -542,9 +630,18 @@ class NativeHost {
|
|||||||
}).catch(error => { if (!closed) worker.onerror?.({message:error.message}); throw error; });
|
}).catch(error => { if (!closed) worker.onerror?.({message:error.message}); throw error; });
|
||||||
const worker = {
|
const worker = {
|
||||||
engineID:null,
|
engineID:null,
|
||||||
|
foreground:true,
|
||||||
|
pagesPaused:false,
|
||||||
onmessage:null,
|
onmessage:null,
|
||||||
onerror:null,
|
onerror:null,
|
||||||
configurePages() {},
|
configurePages() {},
|
||||||
|
setForeground(foreground) {
|
||||||
|
worker.foreground = !!foreground;
|
||||||
|
worker.pausePages(!worker.foreground);
|
||||||
|
},
|
||||||
|
pausePages(paused = true) {
|
||||||
|
worker.pagesPaused = !!paused;
|
||||||
|
},
|
||||||
postMessage(data) {
|
postMessage(data) {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
if (data.type === "cancel") {
|
if (data.type === "cancel") {
|
||||||
@@ -563,6 +660,7 @@ class NativeHost {
|
|||||||
const operation = barrier.then(async () => {
|
const operation = barrier.then(async () => {
|
||||||
const engine = await ready;
|
const engine = await ready;
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
|
if (data.preload && worker.pagesPaused) throw new DOMException("预加载已暂停", "AbortError");
|
||||||
controller.signal.throwIfAborted();
|
controller.signal.throwIfAborted();
|
||||||
const args = {engine, id:data.id, name:data.name, args:data.args};
|
const args = {engine, id:data.id, name:data.name, args:data.args};
|
||||||
let result;
|
let result;
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
export class DocumentDialogs {
|
||||||
|
constructor({ document: root = document, targets = [], titlebar = null } = {}) {
|
||||||
|
Object.assign(this, { document: root, targets, titlebar, owner: null, records: new Map(), tabs: new Map(), released: new WeakSet(), inert: new Map(), styles: new Map() });
|
||||||
|
this.backdrop = root.createElement("div");
|
||||||
|
this.backdrop.className = "document-dialog-backdrop";
|
||||||
|
this.backdrop.hidden = true;
|
||||||
|
this.backdrop.setAttribute("aria-hidden", "true");
|
||||||
|
Object.assign(this.backdrop.style, { position: "fixed", inset: "0", background: "rgb(31 36 40 / 18%)", zIndex: "1000" });
|
||||||
|
root.body.append(this.backdrop);
|
||||||
|
root.addEventListener("keydown", event => {
|
||||||
|
if (event.defaultPrevented || this.applicationOpen()) return;
|
||||||
|
const panel = this.current()?.stack.at(-1);
|
||||||
|
if (!panel) return;
|
||||||
|
if (event.key === "Tab" && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
||||||
|
const selector = "button, input, select, textarea, a[href], [tabindex], [contenteditable=true]";
|
||||||
|
const elements = [this.titlebar, panel].filter(Boolean).flatMap(root => [...root.querySelectorAll(selector)])
|
||||||
|
.filter(node => node.tabIndex >= 0 && !node.disabled && !node.closest("[inert]") && node.getClientRects().length);
|
||||||
|
const index = elements.indexOf(root.activeElement), direction = event.shiftKey ? -1 : 1;
|
||||||
|
const target = index < 0 ? event.shiftKey ? elements.at(-1) : elements[0] : elements[(index + direction + elements.length) % elements.length];
|
||||||
|
event.preventDefault();
|
||||||
|
if (target) target.focus();
|
||||||
|
else panel.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key !== "Escape") return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.cancel(panel);
|
||||||
|
}, true);
|
||||||
|
this.backdrop.addEventListener("pointerdown", event => { event.preventDefault(); this.focus(); });
|
||||||
|
root.defaultView.addEventListener("resize", () => this.paint());
|
||||||
|
}
|
||||||
|
|
||||||
|
register(panel) {
|
||||||
|
if (this.records.has(panel)) return panel;
|
||||||
|
const record = { show: panel.show.bind(panel), showModal: panel.showModal.bind(panel), close: panel.close.bind(panel), owner: null, application: false, clicks: new WeakMap(), initial: this.snapshot(panel) };
|
||||||
|
this.records.set(panel, record);
|
||||||
|
panel.showModal = panel.show = () => this.openFor(this.owner, panel);
|
||||||
|
panel.close = value => {
|
||||||
|
if (record.application) record.close(value);
|
||||||
|
else this.closeFor(this.owner, panel, value);
|
||||||
|
};
|
||||||
|
panel.requestClose = value => {
|
||||||
|
if (record.application) {
|
||||||
|
const event = new this.document.defaultView.Event("cancel", { cancelable: true });
|
||||||
|
if (panel.dispatchEvent(event)) record.close(value);
|
||||||
|
} else this.cancel(panel, value);
|
||||||
|
};
|
||||||
|
panel.addEventListener("close", () => {
|
||||||
|
if (record.application) { record.application = false; this.paint(); }
|
||||||
|
});
|
||||||
|
panel.addEventListener("click", event => { record.clicks.set(event, this.owner); }, true);
|
||||||
|
panel.addEventListener("click", event => {
|
||||||
|
if (!record.application && (record.clicks.get(event) !== this.owner || !this.current()?.stack.includes(panel))) event.preventDefault();
|
||||||
|
});
|
||||||
|
panel.addEventListener("submit", event => {
|
||||||
|
if (record.application || event.defaultPrevented) return;
|
||||||
|
const method = event.submitter?.getAttribute("formmethod") || event.target.getAttribute("method");
|
||||||
|
if (method?.toLowerCase() !== "dialog") return;
|
||||||
|
event.preventDefault();
|
||||||
|
this.closeFor(this.owner, panel, event.submitter?.value || "");
|
||||||
|
});
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
current(tab = this.owner) {
|
||||||
|
return tab ? this.tabs.get(tab) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry(tab, panel) {
|
||||||
|
if (!tab || this.released.has(tab)) throw new Error("Document dialog owner is unavailable");
|
||||||
|
this.register(panel);
|
||||||
|
if (!this.tabs.has(tab)) this.tabs.set(tab, { entries: new Map(), stack: [] });
|
||||||
|
const entries = this.tabs.get(tab).entries;
|
||||||
|
if (!entries.has(panel)) entries.set(panel, { snapshot: this.records.get(panel).initial, setup: [], updates: [], resolve: null, promise: null });
|
||||||
|
return entries.get(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOwner(tab) {
|
||||||
|
if (this.owner === tab) return;
|
||||||
|
if (this.owner) this.suspend(this.owner);
|
||||||
|
if (tab) this.restore(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
openFor(tab, panel, setup) {
|
||||||
|
const entry = this.entry(tab, panel);
|
||||||
|
if (setup) entry.setup.push(setup);
|
||||||
|
const state = this.tabs.get(tab);
|
||||||
|
state.stack = state.stack.filter(value => value !== panel);
|
||||||
|
state.stack.push(panel);
|
||||||
|
if (tab !== this.owner) return panel;
|
||||||
|
this.unpaint();
|
||||||
|
this.records.get(panel).owner = tab;
|
||||||
|
this.prepare(entry, panel);
|
||||||
|
if (!panel.open) this.records.get(panel).show();
|
||||||
|
this.paint();
|
||||||
|
this.focus();
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
request(panel, tab = this.owner, setup) {
|
||||||
|
if (!tab || this.released.has(tab)) return Promise.resolve(null);
|
||||||
|
const entry = this.entry(tab, panel);
|
||||||
|
if (entry.promise) return entry.promise;
|
||||||
|
entry.promise = new Promise(resolve => { entry.resolve = resolve; });
|
||||||
|
const promise = entry.promise;
|
||||||
|
this.openFor(tab, panel, setup);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFor(tab, panel, setup) {
|
||||||
|
if (!tab || this.released.has(tab)) return false;
|
||||||
|
const entry = this.current(tab)?.entries.get(panel);
|
||||||
|
if (!entry) return false;
|
||||||
|
if (tab === this.owner && this.records.get(panel).owner === tab) setup(panel);
|
||||||
|
else entry.updates.push(setup);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeFor(tab, panel, value) {
|
||||||
|
const state = this.current(tab), entry = state?.entries.get(panel);
|
||||||
|
if (!state?.stack.includes(panel)) return;
|
||||||
|
const visible = tab === this.owner;
|
||||||
|
if (visible) {
|
||||||
|
this.unpaint();
|
||||||
|
if (value !== undefined) panel.returnValue = String(value);
|
||||||
|
panel.removeAttribute("open");
|
||||||
|
entry.snapshot = this.snapshot(panel);
|
||||||
|
}
|
||||||
|
const result = value === undefined ? visible ? panel.returnValue : entry.snapshot.returnValue : String(value);
|
||||||
|
state.stack = state.stack.filter(value => value !== panel);
|
||||||
|
const resolve = entry.resolve;
|
||||||
|
entry.resolve = entry.promise = null;
|
||||||
|
entry.setup = [];
|
||||||
|
if (visible) {
|
||||||
|
this.paint();
|
||||||
|
panel.dispatchEvent(new this.document.defaultView.Event("close"));
|
||||||
|
this.focus();
|
||||||
|
}
|
||||||
|
resolve?.(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(panel, value) {
|
||||||
|
const event = new this.document.defaultView.Event("cancel", { cancelable: true });
|
||||||
|
if (panel.dispatchEvent(event)) this.closeFor(this.owner, panel, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend(tab) {
|
||||||
|
if (tab !== this.owner) return;
|
||||||
|
this.unpaint();
|
||||||
|
for (const [panel, entry] of this.current(tab)?.entries || []) {
|
||||||
|
const record = this.records.get(panel);
|
||||||
|
if (record.application || record.owner !== tab) continue;
|
||||||
|
entry.snapshot = this.snapshot(panel);
|
||||||
|
panel.removeAttribute("open");
|
||||||
|
}
|
||||||
|
this.owner = null;
|
||||||
|
this.paint();
|
||||||
|
}
|
||||||
|
|
||||||
|
restore(tab) {
|
||||||
|
if (!tab || this.released.has(tab)) return false;
|
||||||
|
if (this.owner === tab) return true;
|
||||||
|
if (this.owner) this.suspend(this.owner);
|
||||||
|
if (!this.tabs.has(tab)) this.tabs.set(tab, { entries: new Map(), stack: [] });
|
||||||
|
this.owner = tab;
|
||||||
|
const state = this.current(tab);
|
||||||
|
for (const [panel, entry] of state.entries) {
|
||||||
|
const record = this.records.get(panel);
|
||||||
|
if (record.application) continue;
|
||||||
|
this.applySnapshot(entry.snapshot, panel);
|
||||||
|
record.owner = tab;
|
||||||
|
}
|
||||||
|
for (const [panel, entry] of state.entries) {
|
||||||
|
if (this.records.get(panel).application) continue;
|
||||||
|
for (const update of entry.updates.splice(0)) update(panel);
|
||||||
|
}
|
||||||
|
for (const panel of state.stack) {
|
||||||
|
this.prepare(state.entries.get(panel), panel);
|
||||||
|
if (!panel.open) this.records.get(panel).show();
|
||||||
|
}
|
||||||
|
this.paint();
|
||||||
|
this.focus(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
release(tab) {
|
||||||
|
if (!tab || this.released.has(tab)) return;
|
||||||
|
const visible = this.owner === tab;
|
||||||
|
if (visible) this.suspend(tab);
|
||||||
|
const state = this.current(tab);
|
||||||
|
this.tabs.delete(tab);
|
||||||
|
this.released.add(tab);
|
||||||
|
for (const entry of state?.entries.values() || []) entry.resolve?.(null);
|
||||||
|
for (const [panel] of state?.entries || []) {
|
||||||
|
const record = this.records.get(panel);
|
||||||
|
if (record.application || record.owner !== tab) continue;
|
||||||
|
this.applySnapshot(record.initial, panel);
|
||||||
|
record.owner = null;
|
||||||
|
}
|
||||||
|
state?.entries.clear();
|
||||||
|
if (state) state.stack.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
showApplication(panel) {
|
||||||
|
this.register(panel);
|
||||||
|
if ([...this.tabs.values()].some(state => state.stack.includes(panel))) throw new Error("Document dialog is already in use");
|
||||||
|
const record = this.records.get(panel);
|
||||||
|
record.application = true;
|
||||||
|
try { record.showModal(); }
|
||||||
|
catch (error) { record.application = false; throw error; }
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationOpen() {
|
||||||
|
return [...this.records.values()].some(record => record.application);
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare(entry, panel) {
|
||||||
|
for (const setup of entry.setup.splice(0)) setup(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot(panel) {
|
||||||
|
const nodes = [panel, ...panel.querySelectorAll("*")].map(node => ({ node,
|
||||||
|
children: [...node.childNodes], attributes: [...node.attributes].filter(attribute => node !== panel || attribute.name !== "open").map(attribute => [attribute.name, attribute.value]),
|
||||||
|
value: "value" in node && node.type !== "file" ? node.value : undefined,
|
||||||
|
checked: node.checked, indeterminate: node.indeterminate, selected: node.selected,
|
||||||
|
files: node.type === "file" ? [...node.files] : undefined, scrollTop: node.scrollTop, scrollLeft: node.scrollLeft,
|
||||||
|
selectionStart: node.selectionStart, selectionEnd: node.selectionEnd, selectionDirection: node.selectionDirection,
|
||||||
|
text: [...node.childNodes].filter(child => child.nodeType === 3).map(child => [child, child.data]) }));
|
||||||
|
const focus = panel.contains(this.document.activeElement) ? this.document.activeElement : null;
|
||||||
|
const range = this.document.getSelection();
|
||||||
|
const selection = !focus?.matches("input, textarea") && range?.anchorNode && panel.contains(range.anchorNode) && panel.contains(range.focusNode)
|
||||||
|
? { anchor: range.anchorNode, start: range.anchorOffset, focus: range.focusNode, end: range.focusOffset } : null;
|
||||||
|
return { nodes, focus, selection, returnValue: panel.returnValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
applySnapshot(snapshot, panel) {
|
||||||
|
panel.removeAttribute("open");
|
||||||
|
for (const item of snapshot.nodes) {
|
||||||
|
const { node, children, attributes } = item;
|
||||||
|
if (node.childNodes.length !== children.length || children.some((child, index) => node.childNodes[index] !== child)) node.replaceChildren(...children);
|
||||||
|
for (const attribute of [...node.attributes]) if (!attributes.some(([name]) => name === attribute.name)) node.removeAttribute(attribute.name);
|
||||||
|
for (const [name, value] of attributes) if (node.getAttribute(name) !== value) node.setAttribute(name, value);
|
||||||
|
for (const [text, value] of item.text) text.data = value;
|
||||||
|
}
|
||||||
|
for (const item of snapshot.nodes) {
|
||||||
|
const { node } = item;
|
||||||
|
if (item.value !== undefined && node.tagName !== "SELECT") node.value = item.value;
|
||||||
|
for (const key of ["checked", "indeterminate", "selected"]) if (item[key] !== undefined) node[key] = item[key];
|
||||||
|
if (item.files !== undefined) {
|
||||||
|
const transfer = new this.document.defaultView.DataTransfer();
|
||||||
|
for (const file of item.files) transfer.items.add(file);
|
||||||
|
node.files = transfer.files;
|
||||||
|
}
|
||||||
|
if (item.selectionStart != null && typeof node.setSelectionRange === "function") node.setSelectionRange(item.selectionStart, item.selectionEnd, item.selectionDirection);
|
||||||
|
node.scrollTop = item.scrollTop;
|
||||||
|
node.scrollLeft = item.scrollLeft;
|
||||||
|
}
|
||||||
|
panel.returnValue = snapshot.returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
unpaint() {
|
||||||
|
for (const [node, value] of this.inert) node.inert = value;
|
||||||
|
this.inert.clear();
|
||||||
|
for (const [panel, styles] of this.styles) for (const [name, [value, priority]] of styles) {
|
||||||
|
if (value) panel.style.setProperty(name, value, priority);
|
||||||
|
else panel.style.removeProperty(name);
|
||||||
|
}
|
||||||
|
this.styles.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
paint() {
|
||||||
|
this.unpaint();
|
||||||
|
const stack = this.current()?.stack || [];
|
||||||
|
const top = stack.at(-1);
|
||||||
|
this.backdrop.hidden = !top;
|
||||||
|
if (!top) return;
|
||||||
|
const inset = Math.max(0, this.titlebar?.getBoundingClientRect().bottom || 0);
|
||||||
|
this.backdrop.style.top = `${inset}px`;
|
||||||
|
this.backdrop.style.zIndex = String(1000 + (stack.length - 1) * 2);
|
||||||
|
const disable = node => {
|
||||||
|
if (!node || node === this.titlebar || node === this.backdrop || this.records.has(node)) return;
|
||||||
|
if (node.contains(this.titlebar) || [...this.records.keys()].some(panel => node.contains(panel))) {
|
||||||
|
for (const child of node.children) disable(child);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.inert.has(node)) this.inert.set(node, node.inert);
|
||||||
|
node.inert = true;
|
||||||
|
};
|
||||||
|
for (const target of typeof this.targets === "function" ? this.targets() : this.targets) disable(target);
|
||||||
|
for (const [index, panel] of stack.entries()) {
|
||||||
|
if (panel !== top) { this.inert.set(panel, panel.inert); panel.inert = true; }
|
||||||
|
const values = { position: "fixed", inset: `${inset}px 0 0`, zIndex: String(1001 + index * 2), maxHeight: `calc(100dvh - ${inset + 24}px)`, "--dialog-inset": `${inset}px` };
|
||||||
|
const styles = new Map();
|
||||||
|
for (const [key, value] of Object.entries(values)) {
|
||||||
|
const name = key.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
|
||||||
|
styles.set(name, [panel.style.getPropertyValue(name), panel.style.getPropertyPriority(name)]);
|
||||||
|
panel.style.setProperty(name, value);
|
||||||
|
}
|
||||||
|
this.styles.set(panel, styles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
focus(restore = false) {
|
||||||
|
const panel = this.current()?.stack.at(-1);
|
||||||
|
if (!panel || this.applicationOpen()) return;
|
||||||
|
const saved = restore ? this.current().entries.get(panel)?.snapshot.focus : null;
|
||||||
|
const target = saved?.isConnected && panel.contains(saved) ? saved : panel.querySelector("[autofocus], button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]") || panel;
|
||||||
|
if (restore || !panel.contains(this.document.activeElement)) target.focus({ preventScroll: true });
|
||||||
|
const selection = restore && this.current().entries.get(panel)?.snapshot.selection;
|
||||||
|
if (selection?.anchor?.isConnected && selection.focus?.isConnected) this.document.getSelection().setBaseAndExtent(selection.anchor, selection.start, selection.focus, selection.end);
|
||||||
|
if (restore) for (const item of this.current().entries.get(panel)?.snapshot.nodes || []) { item.node.scrollTop = item.scrollTop; item.node.scrollLeft = item.scrollLeft; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
const CACHE_PREFIX = `ofdgo:${self.registration.scope}:app:`;
|
const CACHE_PREFIX = `ofdgo:${self.registration.scope}:app:`;
|
||||||
const META_CACHE = `${CACHE_PREFIX}meta`;
|
const META_CACHE = `${CACHE_PREFIX}meta`;
|
||||||
const ASSETS = ["./", "ofdgo.css", "ofdgo.ico", "ofdgo.js", "ofdgo.json", "ofdgo.png", "ofdgo.svg", "ofdgo.wasm", "ofdgo_edit.js", "ofdgo_font.js", "ofdgo_host.js", "ofdgo_wasm.js", "ofdgo_work.js", "wasm_exec.js"]
|
const ASSETS = ["./", "ofdgo.css", "ofdgo.ico", "ofdgo.js", "ofdgo.json", "ofdgo.png", "ofdgo.svg", "ofdgo.wasm", "ofdgo_edit.js", "ofdgo_font.js", "ofdgo_host.js", "ofdgo_util.js", "ofdgo_wasm.js", "ofdgo_work.js", "wasm_exec.js"]
|
||||||
.map((path) => new URL(path, self.registration.scope).href);
|
.map((path) => new URL(path, self.registration.scope).href);
|
||||||
const CLIENT_PREFIX = new URL("__client/", self.registration.scope).href;
|
const CLIENT_PREFIX = new URL("__client/", self.registration.scope).href;
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user