mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
0ab785ebc3
commit
8e31f888d3
19 files changed
+9092
-6499
No files matched your search
@@ -22,9 +22,14 @@ import (
|
||||
|
||||
// FS WebUI嵌入静态文件
|
||||
//
|
||||
//go:embed index.html ofdgo.css ofdgo.js ofdgo.json ofdgo.svg ofdgo.wasm ofdgo_edit.js ofdgo_font.js ofdgo_wasm.js ofdgo_work.js wasm_exec.js
|
||||
//go:embed index.html ofdgo.css ofdgo.js ofdgo.json ofdgo.svg ofdgo.wasm ofdgo_edit.js ofdgo_font.js ofdgo_host.js ofdgo_wasm.js ofdgo_work.js wasm_exec.js
|
||||
var FS embed.FS
|
||||
|
||||
// NativeFS WebView嵌入静态文件
|
||||
//
|
||||
//go:embed index.html ofdgo.css ofdgo.js ofdgo.json ofdgo.svg ofdgo_edit.js ofdgo_font.js ofdgo_host.js
|
||||
var NativeFS embed.FS
|
||||
|
||||
// Checksum 计算WebUI嵌入资源校验值
|
||||
// 返回: string SHA-256十六进制校验值
|
||||
func Checksum() string {
|
||||
|
||||
+31
-16
@@ -1,5 +1,6 @@
|
||||
import { SVG_NAMESPACE, CanvasEditor, canEditObject, editTextValue, richTextModel, fontFaceStyle, missingGlyphMessage, objectEditReason, selectedText, lineShape, selectionBounds, pagePoint } from "./ofdgo_edit.js";
|
||||
import { FontManager, FontPicker } from "./ofdgo_font.js";
|
||||
import { nativeHost, createEngineWorker, documentBytes, saveFilePicker, directoryPicker } from "./ofdgo_host.js";
|
||||
|
||||
const MM_TO_PX = 96 / 25.4;
|
||||
const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)");
|
||||
@@ -595,6 +596,7 @@ const el = {
|
||||
};
|
||||
|
||||
const fontManager = new FontManager({
|
||||
host: nativeHost,
|
||||
onChange: scheduleFontSync,
|
||||
onPermissionChange: updateFontPermissionHint,
|
||||
});
|
||||
@@ -1388,8 +1390,8 @@ batchElements.Cancel.addEventListener("click", cancelBatch);
|
||||
batchElements.Form.addEventListener("submit", event => { event.preventDefault(); runBatch(); });
|
||||
batchElements.Panel.addEventListener("dragover", event => { event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = batch.running ? "none" : "copy"; });
|
||||
batchElements.Panel.addEventListener("drop", event => { event.preventDefault(); event.stopPropagation(); if (!batch.running) addBatchFiles(event.dataTransfer.files); });
|
||||
batchElements.Destination.querySelector('[value="directory"]').disabled = !window.showDirectoryPicker;
|
||||
batchElements.Destination.value = window.showDirectoryPicker ? "directory" : "archive";
|
||||
batchElements.Destination.querySelector('[value="directory"]').disabled = !nativeHost && !window.showDirectoryPicker;
|
||||
batchElements.Destination.value = nativeHost || window.showDirectoryPicker ? "directory" : "archive";
|
||||
fontBindingElements.Cancel.addEventListener("click", () => fontBindingElements.Panel.close());
|
||||
fontBindingElements.Panel.addEventListener("close", () => { fontBinding.font = null; fontBinding.choices = []; fontBinding.sequence++; fontBindingElements.File.value = ""; });
|
||||
fontBindingElements.Source.addEventListener("change", () => {
|
||||
@@ -1703,6 +1705,7 @@ document.addEventListener("visibilitychange", () => {
|
||||
});
|
||||
|
||||
el.fontDirectoryButton.disabled = !("webkitdirectory" in el.fontDirectoryInput);
|
||||
nativeHost?.installFileInputs(error => showError(error, false));
|
||||
updateSidebarState();
|
||||
boot();
|
||||
|
||||
@@ -2212,7 +2215,7 @@ async function loadImportFile() {
|
||||
el.importStatus.textContent = "";
|
||||
setBusy(true);
|
||||
try {
|
||||
const data = file ? new Uint8Array(await file.arrayBuffer()) : null;
|
||||
const data = file ? await documentBytes(file) : null;
|
||||
if (openSeq !== state.openSeq) return;
|
||||
const info = data ? await openWithCredentials(null, openSeq, data, true) : await callWASM("ofdgoLoadImport", null);
|
||||
if (openSeq !== state.openSeq || !el.importPanel.open) {
|
||||
@@ -2461,7 +2464,7 @@ async function exportManagedVersion() {
|
||||
setBusy(true, "正在另存版本");
|
||||
el.versionStatus.textContent = "正在另存版本";
|
||||
try {
|
||||
const file = window.showSaveFilePicker ? await window.showSaveFilePicker({suggestedName: name, types: [{description: "OFD", accept: {"application/ofd": [".ofd"]}}]}) : null;
|
||||
const file = nativeHost || window.showSaveFilePicker ? await saveFilePicker({suggestedName: name, types: [{description: "OFD", accept: {"application/ofd": [".ofd"]}}]}) : null;
|
||||
if (openSeq !== state.openSeq) return;
|
||||
const result = await callWASM("ofdgoExportVersion", entry.id, file);
|
||||
if (openSeq !== state.openSeq) return;
|
||||
@@ -4126,6 +4129,12 @@ async function boot() {
|
||||
}).catch((err) => showError(err, !state.doc));
|
||||
});
|
||||
}
|
||||
if (nativeHost) await nativeHost.start(files => {
|
||||
if (document.body.hasAttribute("data-busy")) { setStatus("请等待当前操作完成"); return; }
|
||||
if (batchElements.Panel.open) addBatchFiles(files);
|
||||
else if (files.length === 1) openOFD(files[0]);
|
||||
else if (files.length) setStatus("请选择一个文档");
|
||||
}, error => showError(error, !state.doc));
|
||||
try {
|
||||
const remote = remoteDocumentOptions(window.location.hash);
|
||||
if (remote && openSeq === state.openSeq) await openOFD({ name: remote.name }, remote);
|
||||
@@ -4154,6 +4163,11 @@ async function registerOffline() {
|
||||
}
|
||||
|
||||
async function prepareOffline() {
|
||||
if (nativeHost) {
|
||||
el.appPanel.hidden = false;
|
||||
el.offlineStatus.textContent = "可离线";
|
||||
return true;
|
||||
}
|
||||
if (!window.isSecureContext || !("serviceWorker" in navigator) || !("caches" in window) || !("locks" in navigator)) {
|
||||
return true;
|
||||
}
|
||||
@@ -4323,7 +4337,7 @@ async function loadWASM() {
|
||||
state.ready = false;
|
||||
state.wasmExited = false;
|
||||
await new Promise((resolve, reject) => {
|
||||
const worker = new Worker("./ofdgo_wasm.js");
|
||||
const worker = createEngineWorker();
|
||||
wasmWorker = worker;
|
||||
const fail = (err) => {
|
||||
markWASMExited(wasmSeq, err);
|
||||
@@ -4557,7 +4571,7 @@ async function openOFD(file, remote = null) {
|
||||
const source = downloaded ? downloaded.file : file;
|
||||
const format = await callWASM("ofdgoDetectFormat", source);
|
||||
if (openSeq !== state.openSeq) return;
|
||||
let bytes = new Uint8Array(await source.arrayBuffer());
|
||||
let bytes = await documentBytes(source);
|
||||
const name = (downloaded ? downloaded.name : file.name) || `ofdgo.${format}`;
|
||||
let warnings = [];
|
||||
let convertedDoc;
|
||||
@@ -5108,7 +5122,7 @@ async function openWithCredentials(fonts, openSeq, data = state.ofdBytes, import
|
||||
converted.doc = await callWASM("ofdgoOpen", converted.bytes, fonts, state.renderAnnotations, credentials);
|
||||
return converted;
|
||||
}
|
||||
return importing ? await callWASM("ofdgoLoadImport", data.slice(), credentials)
|
||||
return importing ? await callWASM("ofdgoLoadImport", data?.nativeFile ? data : data.slice(), credentials)
|
||||
: await callWASM("ofdgoOpen", data, fonts, state.renderAnnotations, credentials);
|
||||
} catch (err) {
|
||||
if (openSeq !== state.openSeq || !(pdf ? err.code === "pdfPassword" : ["credentialsRequired", "invalidCredentials"].includes(err.code))) throw err;
|
||||
@@ -7529,7 +7543,7 @@ async function downloadResource(attachment, api, label) {
|
||||
updateControls();
|
||||
setBusy(true, `正在读取${label}`, null, `正在读取${label}`);
|
||||
try {
|
||||
const file = window.showSaveFilePicker ? await window.showSaveFilePicker({ suggestedName: attachment.fileName }) : null;
|
||||
const file = nativeHost || window.showSaveFilePicker ? await saveFilePicker({ suggestedName: attachment.fileName }) : null;
|
||||
if (openSeq !== state.openSeq) {
|
||||
return;
|
||||
}
|
||||
@@ -7848,7 +7862,7 @@ async function signDocument(event) {
|
||||
el.signForm.querySelectorAll("input, select, button[type=submit]").forEach(input => { input.disabled = true; });
|
||||
try {
|
||||
const name = `${baseFileName()}_签章.ofd`;
|
||||
const file = window.showSaveFilePicker ? await window.showSaveFilePicker({suggestedName:name, types:[{description:"OFD",accept:{"application/ofd":[".ofd"]}}]}) : null;
|
||||
const file = nativeHost || window.showSaveFilePicker ? await saveFilePicker({suggestedName:name, types:[{description:"OFD",accept:{"application/ofd":[".ofd"]}}]}) : null;
|
||||
if (openSeq !== state.openSeq || state.signCanceled) return;
|
||||
if (canvasEditor.input || canvasEditor.crop || canvasEditor.nudge || canvasEditor.nudgeCommit) {
|
||||
setBusy(false);
|
||||
@@ -8123,7 +8137,7 @@ function batchCall(name, args, onProgress) {
|
||||
function startBatchWorker() {
|
||||
batch.workerError = null;
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker("./ofdgo_wasm.js");
|
||||
const worker = createEngineWorker();
|
||||
batch.worker = worker;
|
||||
const fail = async error => {
|
||||
batch.workerError = error;
|
||||
@@ -8224,13 +8238,13 @@ async function runBatch() {
|
||||
const entries = [];
|
||||
try {
|
||||
if (archive) {
|
||||
if (window.showSaveFilePicker) zipFile = await window.showSaveFilePicker({ suggestedName: "转换结果.zip", types: [{ description: "ZIP", accept: { "application/zip": [".zip"] } }] });
|
||||
if (navigator.storage?.getDirectory) {
|
||||
zipFile = await saveFilePicker({ suggestedName: "转换结果.zip", types: [{ description: "ZIP", accept: { "application/zip": [".zip"] } }] });
|
||||
if (!nativeHost && navigator.storage?.getDirectory) {
|
||||
storage = await navigator.storage.getDirectory();
|
||||
stagingName = `ofdgo-convert-${crypto.randomUUID()}`;
|
||||
destination = await storage.getDirectoryHandle(stagingName, { create: true });
|
||||
}
|
||||
} else destination = await window.showDirectoryPicker({ mode: "readwrite" });
|
||||
} else destination = await directoryPicker();
|
||||
if (batch.canceled) return;
|
||||
if (jobs.some(item => /\.pdf$/i.test(item.file.name) || !["ofd", "txt"].includes(item.format || defaultFormat.value)
|
||||
|| (item.format || defaultFormat.value) === "ofd" && batchElements.FontMode.value === "1")) {
|
||||
@@ -8357,7 +8371,7 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value)
|
||||
updateControls();
|
||||
setBusy(true, "正在生成文件", null, status);
|
||||
try {
|
||||
const file = window.showSaveFilePicker ? await window.showSaveFilePicker({
|
||||
const file = nativeHost || window.showSaveFilePicker ? await saveFilePicker({
|
||||
suggestedName: fileName,
|
||||
types: [{ description: label, accept: { [mime]: [`.${extension}`] } }],
|
||||
}) : null;
|
||||
@@ -11982,11 +11996,11 @@ async function callWASM(name, ...args) {
|
||||
}
|
||||
try {
|
||||
const transfer = [];
|
||||
if (name === "ofdgoOpen") {
|
||||
if (name === "ofdgoOpen" && !nativeHost) {
|
||||
args[0] = args[0].slice();
|
||||
transfer.push(args[0].buffer);
|
||||
}
|
||||
if (name === "ofdgoLoadImport" && args[0]) transfer.push(args[0].buffer);
|
||||
if (name === "ofdgoLoadImport" && args[0] && !nativeHost) transfer.push(args[0].buffer);
|
||||
wasmWorker.postMessage({ id, name, args }, transfer);
|
||||
} catch (err) {
|
||||
wasmRequests.delete(id);
|
||||
@@ -12081,6 +12095,7 @@ function formatSize(value) {
|
||||
}
|
||||
|
||||
function downloadBytes(bytes, mime, name) {
|
||||
if (nativeHost) return nativeHost.download(bytes, mime, name).catch(error => { if (error.name !== "AbortError") showError(error, false); });
|
||||
const blob = new Blob([bytes], { type: mime });
|
||||
const link = document.createElement("a");
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
@@ -159,7 +159,8 @@ export class FontPicker {
|
||||
}
|
||||
|
||||
export class FontManager {
|
||||
constructor({ onChange, onPermissionChange }) {
|
||||
constructor({ onChange, onPermissionChange, host = null }) {
|
||||
this.host = host;
|
||||
this.userFonts = [];
|
||||
this.localFonts = [];
|
||||
this.catalog = [];
|
||||
@@ -267,7 +268,7 @@ export class FontManager {
|
||||
|
||||
async queryLocal() {
|
||||
try {
|
||||
const available = await window.queryLocalFonts();
|
||||
const available = this.host ? await this.host.fonts() : await window.queryLocalFonts();
|
||||
this.catalog = available;
|
||||
this.catalogLoaded = true;
|
||||
this.permission = "granted";
|
||||
@@ -294,6 +295,7 @@ export class FontManager {
|
||||
}
|
||||
|
||||
async read(font) {
|
||||
if (font.nativeFile) return font.nativeFile;
|
||||
if (font.blob) {
|
||||
const blob = await font.blob();
|
||||
return new Uint8Array(await blob.arrayBuffer());
|
||||
@@ -407,10 +409,15 @@ export class FontManager {
|
||||
}
|
||||
|
||||
canReadLocal() {
|
||||
return typeof window.queryLocalFonts === "function";
|
||||
return !!this.host || typeof window.queryLocalFonts === "function";
|
||||
}
|
||||
|
||||
async refreshPermission() {
|
||||
if (this.host) {
|
||||
this.permission = "granted";
|
||||
this.onPermissionChange();
|
||||
return;
|
||||
}
|
||||
if (!this.canReadLocal() || !navigator.permissions?.query) {
|
||||
this.onPermissionChange();
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
export function createEngineWorker() {
|
||||
return nativeHost ? nativeHost.worker() : new Worker("./ofdgo_wasm.js");
|
||||
}
|
||||
|
||||
export async function documentBytes(file) {
|
||||
return file?.nativeFile ? file : new Uint8Array(await file.arrayBuffer());
|
||||
}
|
||||
|
||||
export function saveFilePicker(options) {
|
||||
return nativeHost ? nativeHost.savePicker(options) : window.showSaveFilePicker ? window.showSaveFilePicker(options) : null;
|
||||
}
|
||||
|
||||
export function directoryPicker() {
|
||||
return nativeHost ? nativeHost.directoryPicker() : window.showDirectoryPicker({mode:"readwrite"});
|
||||
}
|
||||
|
||||
class NativeFile {
|
||||
constructor(value) {
|
||||
Object.assign(this, value);
|
||||
this.byteLength = this.size;
|
||||
this.type ||= "application/octet-stream";
|
||||
if (value.temporary) nativeHost?.resources?.register(this, value.nativeFile);
|
||||
}
|
||||
|
||||
async arrayBuffer() {
|
||||
const response = await fetch(this.url);
|
||||
if (!response.ok) throw new Error("读取文件失败");
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
|
||||
async text() {
|
||||
return new TextDecoder().decode(await this.arrayBuffer());
|
||||
}
|
||||
}
|
||||
|
||||
class NativeHandle {
|
||||
constructor(host, value) {
|
||||
this.host = host;
|
||||
Object.assign(this, value);
|
||||
}
|
||||
|
||||
async getFile() {
|
||||
return new NativeFile(await this.host.call("file", {handle:this.nativeHandle}));
|
||||
}
|
||||
|
||||
async getFileHandle(name, options = {}) {
|
||||
return new NativeHandle(this.host, await this.host.call("child", {handle:this.nativeHandle, name, create:!!options.create, directory:false}));
|
||||
}
|
||||
|
||||
async getDirectoryHandle(name, options = {}) {
|
||||
return new NativeHandle(this.host, await this.host.call("child", {handle:this.nativeHandle, name, create:!!options.create, directory:true}));
|
||||
}
|
||||
|
||||
removeEntry(name, options = {}) {
|
||||
return this.host.call("remove", {handle:this.nativeHandle, name, recursive:!!options.recursive});
|
||||
}
|
||||
|
||||
async createWritable() {
|
||||
const {writer} = await this.host.call("write.begin", {handle:this.nativeHandle});
|
||||
return {
|
||||
write: async data => this.host.call("write.data", {writer, data:await this.host.encode(data)}),
|
||||
close: () => this.host.call("write.commit", {writer}),
|
||||
abort: () => this.host.call("write.abort", {writer}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class NativeHost {
|
||||
constructor(bridge) {
|
||||
this.bridge = bridge;
|
||||
this.resources = typeof FinalizationRegistry === "function" ? new FinalizationRegistry(file => {
|
||||
this.call("release", {file}).catch(() => {});
|
||||
}) : null;
|
||||
}
|
||||
|
||||
call(name, value) {
|
||||
return this.bridge.call(`ofdgo.host.${name}`, value);
|
||||
}
|
||||
|
||||
async encode(value, key = "") {
|
||||
if (value?.nativeFile) return {nativeFile:value.nativeFile, size:value.size, name:value.name};
|
||||
if (value?.nativeHandle) return {nativeHandle:value.nativeHandle, name:value.name, kind:value.kind};
|
||||
if (value instanceof Blob || value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
|
||||
if (!(value instanceof Blob)) {
|
||||
const bytes = ArrayBuffer.isView(value) ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) : new Uint8Array(value);
|
||||
if (bytes.byteLength <= 65536 || ["password", "key", "keyPassword", "certificate"].includes(key)) return {nativeBytes:Array.from(bytes)};
|
||||
}
|
||||
const response = await fetch("/__native/input", {method:"POST", body:value});
|
||||
if (!response.ok) throw new Error("传递数据失败");
|
||||
return response.json();
|
||||
}
|
||||
if (Array.isArray(value)) return Promise.all(value.map(item => this.encode(item)));
|
||||
if (value && typeof value === "object") {
|
||||
const result = {};
|
||||
for (const [key, item] of Object.entries(value)) result[key] = await this.encode(item, key);
|
||||
return result;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async decode(value, name = "", key = "") {
|
||||
if (value?.nativeFile) {
|
||||
const file = new NativeFile(value);
|
||||
if (key === "file" || name === "ofdgoConvertPDF" && key === "bytes") return file;
|
||||
try {
|
||||
const bytes = await file.arrayBuffer();
|
||||
return key === "blob" ? new Blob([bytes], {type:value.type}) : new Uint8Array(bytes);
|
||||
} finally {
|
||||
await this.call("release", {file:value.nativeFile});
|
||||
}
|
||||
}
|
||||
if (Array.isArray(value)) return Promise.all(value.map(item => this.decode(item, name)));
|
||||
if (value && typeof value === "object") {
|
||||
const result = {};
|
||||
for (const [field, item] of Object.entries(value)) result[field] = await this.decode(item, name, field);
|
||||
return result;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
worker() {
|
||||
const host = this;
|
||||
let closed = false;
|
||||
let unsubscribe;
|
||||
let pending = Promise.resolve();
|
||||
const canceled = new Set();
|
||||
const emit = data => { if (!closed) worker.onmessage?.({data}); };
|
||||
const ready = host.call("engine.open", {}).then(({engine}) => {
|
||||
if (closed) { host.call("engine.close", {engine}).catch(() => {}); return engine; }
|
||||
unsubscribe = host.bridge.on(`ofdgo.engine.${engine}`, emit);
|
||||
emit({type:"ready"});
|
||||
return engine;
|
||||
}).catch(error => { if (!closed) worker.onerror?.({message:error.message}); throw error; });
|
||||
const worker = {
|
||||
onmessage:null,
|
||||
onerror:null,
|
||||
postMessage(data) {
|
||||
if (closed) return;
|
||||
if (data.type === "cancel") {
|
||||
canceled.add(data.id);
|
||||
ready.then(engine => host.call("engine.cancel", {engine, id:data.id})).catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (data.type === "font") return;
|
||||
pending = pending.then(async () => {
|
||||
const engine = await ready;
|
||||
if (closed) return;
|
||||
if (canceled.has(data.id)) throw Object.assign(new Error("操作已取消"), {name:"AbortError"});
|
||||
const args = await host.encode(data.args);
|
||||
if (closed) return;
|
||||
if (canceled.has(data.id)) throw Object.assign(new Error("操作已取消"), {name:"AbortError"});
|
||||
const result = await host.call("engine.call", {engine, id:data.id, name:data.name, args});
|
||||
emit({id:data.id, ...await host.decode(result, data.name)});
|
||||
}).catch(error => emit({id:data.id, ok:false, error:error.message, code:error.code, canceled:error.name === "AbortError"})).finally(() => canceled.delete(data.id));
|
||||
},
|
||||
terminate() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
unsubscribe?.();
|
||||
ready.then(engine => host.call("engine.close", {engine})).catch(() => {});
|
||||
},
|
||||
};
|
||||
ready.catch(() => {});
|
||||
return worker;
|
||||
}
|
||||
|
||||
async savePicker(options) {
|
||||
return new NativeHandle(this, await this.call("save", options));
|
||||
}
|
||||
|
||||
async directoryPicker() {
|
||||
return new NativeHandle(this, await this.call("directory", {}));
|
||||
}
|
||||
|
||||
async fonts() {
|
||||
return (await this.call("fonts", {})).map(font => ({...font, nativeFile:new NativeFile(font.file)}));
|
||||
}
|
||||
|
||||
async start(onFiles, onError) {
|
||||
this.bridge.on("ofdgo.files", values => onFiles(values.map(value => new NativeFile(value))));
|
||||
this.bridge.on("ofdgo.error", message => onError(new Error(message)));
|
||||
const title = document.querySelector("title");
|
||||
if (title) new MutationObserver(() => this.call("title", {title:document.title}).catch(onError)).observe(title, {childList:true});
|
||||
await this.call("ready", {});
|
||||
}
|
||||
|
||||
installFileInputs(onError) {
|
||||
document.addEventListener("click", async event => {
|
||||
const input = event.target;
|
||||
if (!(input instanceof HTMLInputElement) || input.type !== "file" || input.disabled) return;
|
||||
event.preventDefault();
|
||||
try {
|
||||
const files = (await this.call("open", {multiple:input.multiple, directory:input.webkitdirectory, accept:input.accept})).map(value => new NativeFile(value));
|
||||
if (!files.length) return;
|
||||
const transfer = new DataTransfer();
|
||||
for (const file of files) transfer.items.add(new File([], file.name, {type:file.type}));
|
||||
delete input.files;
|
||||
input.files = transfer.files;
|
||||
Object.defineProperty(input, "files", {configurable:true, get:() => input.value ? files : []});
|
||||
input.dispatchEvent(new Event("input", {bubbles:true}));
|
||||
input.dispatchEvent(new Event("change", {bubbles:true}));
|
||||
} catch (error) { if (error.name !== "AbortError") onError(error); }
|
||||
}, true);
|
||||
}
|
||||
|
||||
async download(data, mime, name) {
|
||||
const handle = await this.savePicker({suggestedName:name});
|
||||
const writer = await handle.createWritable();
|
||||
try { await writer.write(data); await writer.close(); }
|
||||
catch (error) { await writer.abort().catch(() => {}); throw error; }
|
||||
}
|
||||
}
|
||||
|
||||
export const nativeHost = globalThis.guigo ? new NativeHost(globalThis.guigo) : null;
|
||||
@@ -1,6 +1,6 @@
|
||||
const CACHE_PREFIX = `ofdgo:${self.registration.scope}:app:`;
|
||||
const META_CACHE = `${CACHE_PREFIX}meta`;
|
||||
const ASSETS = ["./", "ofdgo.css", "ofdgo.js", "ofdgo.json", "ofdgo.svg", "ofdgo.wasm", "ofdgo_edit.js", "ofdgo_font.js", "ofdgo_wasm.js", "ofdgo_work.js", "wasm_exec.js"]
|
||||
const ASSETS = ["./", "ofdgo.css", "ofdgo.js", "ofdgo.json", "ofdgo.svg", "ofdgo.wasm", "ofdgo_edit.js", "ofdgo_font.js", "ofdgo_host.js", "ofdgo_wasm.js", "ofdgo_work.js", "wasm_exec.js"]
|
||||
.map((path) => new URL(path, self.registration.scope).href);
|
||||
const CLIENT_PREFIX = new URL("__client/", self.registration.scope).href;
|
||||
|
||||
|
||||
Reference in new issue
Block a user