feat(功能特性): 新增多文件标签编辑
build.yaml / build (push) Successful in 44s

This commit is contained in:
xiaoqidun committed 2026-10-22 13:15:00 +08:00
1 parent 5183ba2ab0
commit 50da1d9144
10 files changed
+1358 -594

No files matched your search

+62 -30
View File
@@ -37,42 +37,74 @@ const (
// platformValue 浏览器参数值
type platformValue = js.Value
// wasmEngines 管理共享资源的独立浏览器业务会话
type wasmEngines struct {
resources *ofdgo.Resources
engines map[string]*businessEngine
}
// RunWASM 注册浏览器WASM接口并阻塞运行
func RunWASM() {
resources, _ := ofdgo.NewResources(ofdgo.ResourceOptions{Parallelism: 1, MemoryLimit: 64 << 20, CacheBytes: 48 << 20, SnapshotBytes: 32 << 20})
e := &businessEngine{resources: resources}
asynchronous := map[string]bool{
"ofdgoDetectFormat": true,
"ofdgoExportVersion": true,
"ofdgoConvertPDF": true,
"ofdgoConvertFile": true,
"ofdgoPackFiles": true,
"ofdgoExportPage": true,
"ofdgoExportDocument": true,
"ofdgoExportAttachment": true,
"ofdgoExportMedia": true,
"ofdgoResources": true,
"ofdgoExportResource": true,
"ofdgoExportImages": true,
"ofdgoImportImages": true,
"ofdgoImportPages": true,
"ofdgoImportDocuments": true,
"ofdgoSaveDocument": true,
"ofdgoPrepareSave": true,
"ofdgoSaveSnapshot": true,
"ofdgoSetEncryption": true,
"ofdgoSaveSigned": true,
}
for name, callback := range e.businessCommands() {
if asynchronous[name] {
registerAsyncCallback(name, callback)
} else {
registerCallback(name, callback)
}
}
engines := &wasmEngines{resources: resources, engines: make(map[string]*businessEngine)}
registerCallback("ofdgoEngineOpen", engines.open)
registerAsyncCallback("ofdgoEngineCall", engines.call)
registerCallback("ofdgoEngineClose", engines.close)
select {}
}
// open 创建具有稳定标识的独立业务会话
// 入参: args 会话标识
// 返回: any 空结果, error 参数或会话错误
func (w *wasmEngines) open(args []js.Value) (any, error) {
if len(args) != 1 || args[0].Type() != js.TypeString || args[0].String() == "" {
return nil, fmt.Errorf("missing engine ID")
}
id := args[0].String()
if w.engines[id] != nil {
return nil, fmt.Errorf("engine already exists: %s", id)
}
w.engines[id] = &businessEngine{resources: w.resources}
return nil, nil
}
// call 按会话标识分派业务命令,不提供默认会话
// 入参: args 会话标识、命令及业务参数
// 返回: any 业务结果, error 调用错误
func (w *wasmEngines) call(args []js.Value) (any, error) {
if len(args) < 2 || args[0].Type() != js.TypeString || args[1].Type() != js.TypeString {
return nil, fmt.Errorf("missing engine ID or operation")
}
e := w.engines[args[0].String()]
if e == nil {
return nil, fmt.Errorf("unknown engine: %s", args[0].String())
}
fn := e.businessCommands()[args[1].String()]
if fn == nil {
return nil, fmt.Errorf("unknown operation: %s", args[1].String())
}
return fn(args[2:])
}
// close 释放会话的文档、编辑器、导入及剪贴板,不关闭共享资源
// 入参: args 会话标识
// 返回: any 空结果, error 参数或关闭错误
func (w *wasmEngines) close(args []js.Value) (any, error) {
if len(args) != 1 || args[0].Type() != js.TypeString || args[0].String() == "" {
return nil, fmt.Errorf("missing engine ID")
}
id := args[0].String()
e := w.engines[id]
if e == nil {
return nil, nil
}
delete(w.engines, id)
e.clearImport()
err := e.currentSession.Close()
*e = businessEngine{}
return nil, err
}
// awaitExport 等待浏览器处理导出检查点
// 入参: fn 浏览器回调, args 回调参数
// 返回: error 写入错误或取消原因
+98 -15
View File
@@ -50,6 +50,7 @@ type Host struct {
files *fileStore
engines map[string]*webui.NativeEngine
engineStops map[string]func() bool
closingEngines map[string]*engineClose
conversionPool *ofdgo.ConversionPool
resources *ofdgo.Resources
cancels map[string]context.CancelFunc
@@ -59,6 +60,14 @@ type Host struct {
fontFiles map[string]string
fontError error
pendingPaths []string
frontendReady bool
closeAllowed bool
}
// engineClose 保存并发关闭共享的完成通知及结果
type engineClose struct {
done chan struct{}
err error
}
// resourceReader 保存资源读取位置及关闭操作
@@ -92,7 +101,7 @@ func Run(ctx context.Context, paths []string, dataDir string) error {
h.conversionPool = h.resources.ConversionPool()
defer h.close()
bindings := make(map[string]guigo.HandlerFunc)
for _, name := range []string{"engine.open", "engine.close", "engine.call", "engine.start", "engine.cancel", "engine.finish", "batch", "open", "save", "directory", "file", "release", "drop.release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "title"} {
for _, name := range []string{"engine.open", "engine.close", "engine.call", "engine.start", "engine.cancel", "engine.finish", "batch", "open", "save", "directory", "file", "release", "drop.release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "title", "window.info", "window.drag", "window.close"} {
bindings["ofdgo.host."+name] = func(ctx context.Context, args any) (any, error) { return h.call(ctx, name, args) }
}
static := http.FileServerFS(webuiassets.NativeFS)
@@ -124,6 +133,18 @@ func Run(ctx context.Context, paths []string, dataDir string) error {
h.mu.Unlock()
}
options.OnDrop = h.openPaths
options.OnTitleBarChanged = func(info guigo.TitleBarInfo) {
h.emit("ofdgo.window", info)
}
options.OnClosing = func(ctx context.Context) bool {
h.mu.Lock()
allowed := h.closeAllowed || !h.frontendReady
h.mu.Unlock()
if !allowed {
h.emit("ofdgo.closing", nil)
}
return allowed
}
options.OnError = func(err error) {
h.emit("ofdgo.error", err.Error())
}
@@ -514,10 +535,47 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) {
return nil, nil
}
return nil, window.SetTitle(text("title"))
case "window.info", "window.drag", "window.close":
h.mu.Lock()
window := h.window
h.mu.Unlock()
if window == nil {
return nil, fmt.Errorf("窗口尚未就绪")
}
if name == "window.info" {
return window.TitleBarInfo()
}
if name == "window.close" {
h.mu.Lock()
h.closeAllowed = true
h.mu.Unlock()
return nil, window.Close()
}
values, ok := args["regions"].([]any)
if !ok {
return nil, fmt.Errorf("拖动区域无效")
}
regions := make([]guigo.Rect, 0, len(values))
for _, value := range values {
item, ok := value.(map[string]any)
if !ok {
return nil, fmt.Errorf("拖动区域无效")
}
x, xOK := item["x"].(float64)
y, yOK := item["y"].(float64)
width, widthOK := item["width"].(float64)
height, heightOK := item["height"].(float64)
if !xOK || !yOK || !widthOK || !heightOK {
return nil, fmt.Errorf("拖动区域无效")
}
regions = append(regions, guigo.Rect{X: x, Y: y, Width: width, Height: height})
}
return nil, window.SetDragRegions(regions)
case "ready":
h.mu.Lock()
paths := h.pendingPaths
h.pendingPaths = nil
h.frontendReady = true
h.mu.Unlock()
if len(paths) != 0 {
h.openPaths(paths)
@@ -764,7 +822,21 @@ func (h *Host) loadFont(ctx context.Context, name string) ([]byte, error) {
func (h *Host) closeEngine(id string) error {
var results []any
h.mu.Lock()
if closing := h.closingEngines[id]; closing != nil {
h.mu.Unlock()
<-closing.done
return closing.err
}
engine := h.engines[id]
if engine == nil {
h.mu.Unlock()
return nil
}
closing := &engineClose{done: make(chan struct{})}
if h.closingEngines == nil {
h.closingEngines = make(map[string]*engineClose)
}
h.closingEngines[id] = closing
delete(h.engines, id)
if stop := h.engineStops[id]; stop != nil {
stop()
@@ -785,10 +857,13 @@ func (h *Host) closeEngine(id string) error {
for _, result := range results {
h.discard(result)
}
if engine != nil {
return engine.Close()
}
return nil
err := engine.Close()
h.mu.Lock()
closing.err = err
delete(h.closingEngines, id)
close(closing.done)
h.mu.Unlock()
return err
}
// close 取消任务并释放当前窗口持有的会话
@@ -797,17 +872,18 @@ func (h *Host) close() {
for _, cancel := range h.cancels {
cancel()
}
engines := h.engines
engines := make([]string, 0, len(h.engines)+len(h.closingEngines))
for id := range h.engines {
engines = append(engines, id)
}
for id := range h.closingEngines {
engines = append(engines, id)
}
results := h.batchResults
h.batchResults = nil
for _, stop := range h.engineStops {
stop()
}
h.engineStops = nil
h.engines = make(map[string]*webui.NativeEngine)
h.mu.Unlock()
for _, engine := range engines {
_ = engine.Close()
for _, id := range engines {
_ = h.closeEngine(id)
}
for _, result := range results {
h.discard(result)
@@ -842,8 +918,15 @@ func windowOptions(dataDir string) (guigo.Options, error) {
return guigo.Options{}, err
}
return guigo.Options{
AppID: applicationID,
Title: "OFDGo - OFD编辑器",
AppID: applicationID,
Title: "OFDGo - OFD编辑器",
TitleBar: guigo.TitleBarContent,
TitleBarColors: guigo.TitleBarColors{
Background: "#e8edf0",
Foreground: "#1f2428",
InactiveBackground: "#eef1f4",
InactiveForeground: "#66737f",
},
Icon: icon,
Width: 1440,
Height: 900,
+4 -3
View File
@@ -333,17 +333,18 @@ func (s *fileStore) resource(id string) (io.ReaderAt, int64, func() error, error
return file, info.Size(), file.Close, nil
}
// release 释放内存结果或临时文件并取消页面清理回调
// release 释放读取快照、内存结果或临时文件,保留在途读取及源文件
// 入参: id 资源标识
func (s *fileStore) release(id string) {
s.mu.Lock()
defer s.mu.Unlock()
if entry := s.entries[id]; entry != nil && (entry.data != nil || entry.temporary) {
if entry := s.entries[id]; entry != nil && (entry.data != nil || entry.temporary || entry.file != nil) {
delete(s.entries, id)
entry.retired = true
if entry.stop != nil {
entry.stop()
}
if entry.file != nil {
if entry.file != nil && entry.readers == 0 {
entry.file.Close()
}
if entry.temporary {