diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 6a82030..6aff51a 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -5053,6 +5053,7 @@ async function boot() { applyTitleBarInfo(await nativeHost.call("window.info", {})); new ResizeObserver(updateTitleBarRegions).observe(el.documentTitleBar); await nativeHost.start(openNativeFiles, error => showError(error, !state.doc)); + watchApplicationUpdates(); } try { const remote = remoteDocumentOptions(window.location.hash); @@ -5140,18 +5141,20 @@ function watchApplicationUpdates(checksum) { if (force === true) retry = true; return; } - if (document.hidden || !navigator.onLine) { + if (document.hidden || !nativeHost && !navigator.onLine) { return; } checking = true; try { - if (!checksum) { + if (!nativeHost && !checksum) { if (!navigator.serviceWorker.controller) return; checksum = await applicationChecksum("./ofdgo.js"); if (!checksum) return; } - const latest = await applicationChecksum("./", { method: "HEAD", cache: "no-store" }); - if (latest) { + const latest = nativeHost ? await nativeHost.call("update.check", {}) : await applicationChecksum("./", { method: "HEAD", cache: "no-store" }); + if (nativeHost) { + el.refreshAppButton.hidden = !latest; + } else if (latest) { el.refreshAppButton.hidden = latest === checksum; } } catch { @@ -5240,7 +5243,12 @@ function requestOffline(worker, type) { } async function refreshApplication() { - if (nativeHost || state.exporting || document.body.hasAttribute("data-busy")) { + if (nativeHost) { + try { await nativeHost.call("update.open", {}); } + catch (err) { showError(err, false); } + return; + } + if (state.exporting || document.body.hasAttribute("data-busy")) { return; } if (workspace.tabs.some(tab => tab.state.saving)) { diff --git a/internal/webview/webview.go b/internal/webview/webview.go index 1e0e30d..1361051 100644 --- a/internal/webview/webview.go +++ b/internal/webview/webview.go @@ -33,6 +33,7 @@ import ( "strconv" "strings" "sync" + "time" "github.com/xiaoqidun/guigo" "github.com/xiaoqidun/ofdgo" @@ -44,6 +45,9 @@ import ( // applicationID 标识原生应用,与版本及可执行文件路径无关 const applicationID = "me.aite.ofdgo" +const applicationVersionURL = "https://ofdgo.aite.me/" +const applicationDownloadURL = "https://ofdgo.aite.me/releases/" + // Host 连接桌面系统能力和共用文档业务 type Host struct { mu sync.Mutex @@ -102,7 +106,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", "window.info", "window.drag", "window.close"} { + 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", "update.check", "update.open"} { bindings["ofdgo.host."+name] = func(ctx context.Context, args any) (any, error) { return h.call(ctx, name, args) } } static := http.FileServerFS(webuiassets.NativeFS) @@ -326,6 +330,10 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) { return guigo.Result{Value: map[string]any{"engine": id, "parallelism": parallelism, "cacheBytes": cacheBytes}, Discard: func() { _ = h.closeEngine(id) }}, nil case "batch": return map[string]any{"parallelism": h.conversionPool.Parallelism()}, nil + case "update.check": + return applicationUpdate(ctx, build.Version, applicationVersionURL) + case "update.open": + return nil, guigo.OpenURL(applicationDownloadURL) case "engine.close": return nil, h.closeEngine(text("engine")) case "engine.finish": @@ -588,6 +596,35 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) { return nil, fmt.Errorf("未知宿主操作") } +// applicationUpdate 比较官网和当前程序的完整构建版本 +// 入参: ctx 请求上下文, version 当前版本, endpoint 官网地址 +// 返回: bool 是否有更新, error 检查错误 +func applicationUpdate(ctx context.Context, version, endpoint string) (bool, error) { + if version == "" { + return false, nil + } + ctx, cancel := context.WithTimeout(ctx, 8*time.Second) + defer cancel() + request, err := http.NewRequestWithContext(ctx, http.MethodHead, endpoint, nil) + if err != nil { + return false, err + } + request.Header.Set("Cache-Control", "no-store") + response, err := http.DefaultClient.Do(request) + if err != nil { + return false, err + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return false, fmt.Errorf("检查更新失败: %s", response.Status) + } + latest := response.Header.Get("X-OFDGo-Build-Version") + if latest == "" { + return false, errors.New("未提供构建版") + } + return latest != version, nil +} + // dialogFilters 将共用界面的文件类型映射到系统对话框 // 入参: args 对话框参数 // 返回: []guigo.FileFilter 文件类型列表