From b248e0c53a7064288a8bb6d463a67a13de2621d0 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Fri, 1 May 2026 12:18:53 +0300 Subject: [PATCH] feat(tauri): add signed updater --- .github/workflows/build.yml | 3 + .gitignore | 3 + AGENTS.md | 7 +- CHANGELOG.md | 1 + bun.lock | 6 + desktop/Cargo.lock | 330 +++++++++++++++++++++++++++- desktop/Cargo.toml | 8 +- desktop/capabilities/default.json | 2 + desktop/src/lib.rs | 2 + desktop/src/menu.rs | 5 + desktop/tauri.conf.json | 10 + desktop/updater.key.pub | 1 + package.json | 2 + packages/vue/src/i18n/messages.ts | 14 +- packages/vue/src/locales/de.json | 14 +- packages/vue/src/locales/es.json | 14 +- packages/vue/src/locales/fr.json | 14 +- packages/vue/src/locales/it.json | 14 +- packages/vue/src/locales/pl.json | 14 +- packages/vue/src/locales/ru.json | 14 +- packages/vue/src/locales/zh-CN.json | 17 +- src/App.vue | 5 +- src/app/shell/menu/use.ts | 5 +- src/app/shell/updater.ts | 121 ++++++++++ src/components/AppToast.vue | 6 +- 25 files changed, 601 insertions(+), 31 deletions(-) create mode 100644 desktop/updater.key.pub create mode 100644 src/app/shell/updater.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d5d3015b3..b22b3c85f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,6 +109,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || '' }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD || '' }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_ID: ${{ secrets.APPLE_ID }} @@ -121,6 +122,8 @@ jobs: releaseName: ${{ startsWith(github.ref, 'refs/tags/v') && format('OpenPencil {0}', github.ref_name) || '' }} releaseBody: ${{ steps.release_body.outputs.body || steps.changelog.outputs.release_notes }} releaseDraft: true + uploadUpdaterJson: true + uploadUpdaterSignatures: true publish-npm: if: startsWith(github.ref, 'refs/tags/v') diff --git a/.gitignore b/.gitignore index 97e0df5cc..4cfd1edeb 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ node_modules dist dist-ssr *.local +# Tauri updater private signing keys +/desktop/*.key +!/desktop/*.key.pub # Copied from node_modules at build time public/canvaskit.wasm diff --git a/AGENTS.md b/AGENTS.md index aa1165164..a46e857fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,15 +94,16 @@ The app editor session (`src/app/editor/session/create.ts`) is a thin Vue wrappe ### How to release -1. Update version in `package.json`, `packages/core/package.json`, `packages/cli/package.json`, `desktop/tauri.conf.json` +1. Update version in `package.json`, `packages/core/package.json`, `packages/cli/package.json`, `packages/mcp/package.json`, `packages/vue/package.json`, `desktop/tauri.conf.json`, and `desktop/Cargo.toml` 2. Update `CHANGELOG.md` — move "Unreleased" items under new version heading with date 3. Commit: `Release v0.x.y` 4. Tag: `git tag v0.x.y && git push --tags` -5. The `build.yml` workflow triggers on `v*` tags and: +5. Ensure GitHub release secrets include `TAURI_SIGNING_PRIVATE_KEY` (and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` if the updater key is password-protected); the public updater key is configured in `desktop/tauri.conf.json`. +6. The `build.yml` workflow triggers on `v*` tags and: - Builds Tauri binaries for macOS (arm64 + x64), Windows (x64 + arm64), Linux (x64) - Creates a draft GitHub Release with all platform binaries - Publishes `@open-pencil/core`, `@open-pencil/cli`, `@open-pencil/mcp`, and `@open-pencil/vue` to npm with provenance -6. Go to GitHub Releases → edit the draft → paste changelog section → publish +7. Go to GitHub Releases → edit the draft → paste changelog section → publish ### CI workflows diff --git a/CHANGELOG.md b/CHANGELOG.md index deee09f13..918918d93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Split the canvas into separate scene and overlay render layers so rulers, labels, selections, and input overlays are isolated from scene rendering. - Use `@use-gesture/vanilla` for wheel gesture lifecycle handling and faster trackpad zoom behavior. - Add auto-layout inspector controls for min/max dimensions, Auto gap distribution, wrap cross-axis gap, and two-axis padding controls. +- Add signed Tauri updater configuration, release artifacts, startup update checks, and a native Check for Updates menu item. ### Fixes diff --git a/bun.lock b/bun.lock index 24e3f76d3..e09d756a5 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,9 @@ "@tauri-apps/plugin-dialog": "^2.6.0", "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-shell": "^2.3.5", + "@tauri-apps/plugin-updater": "^2.10.1", "@unhead/vue": "^2.1.10", "@use-gesture/vanilla": "^10.3.1", "@vueuse/core": "^14.2.1", @@ -1042,8 +1044,12 @@ "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ=="], + "@tauri-apps/plugin-process": ["@tauri-apps/plugin-process@2.3.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA=="], + "@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.5", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg=="], + "@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.10.1", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index dc1f7475c..d0c99e748 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -1000,6 +1000,17 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1658,6 +1669,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -2112,6 +2138,7 @@ checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.11.0", "libc", + "redox_syscall 0.7.4", ] [[package]] @@ -2199,6 +2226,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2523,6 +2556,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + [[package]] name = "objc2-quartz-core" version = "0.3.2" @@ -2594,7 +2639,7 @@ dependencies = [ [[package]] name = "open_pencil" -version = "0.2.2" +version = "0.11.8" dependencies = [ "fix-path-env", "font-kit", @@ -2605,11 +2650,19 @@ dependencies = [ "tauri-plugin-dialog", "tauri-plugin-fs", "tauri-plugin-opener", + "tauri-plugin-process", "tauri-plugin-shell", - "zip", + "tauri-plugin-updater", + "zip 2.4.2", "zstd", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -2636,6 +2689,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "pango" version = "0.18.3" @@ -2685,7 +2752,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] @@ -3152,6 +3219,15 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -3226,15 +3302,20 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -3270,6 +3351,20 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -3292,6 +3387,79 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -3307,6 +3475,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -3364,6 +3541,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.24.0" @@ -3664,7 +3864,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.18", "tracing", "wasm-bindgen", "web-sys", @@ -3743,6 +3943,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3860,6 +4066,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -4061,6 +4278,16 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" +dependencies = [ + "tauri", + "tauri-plugin", +] + [[package]] name = "tauri-plugin-shell" version = "2.3.5" @@ -4082,6 +4309,39 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-updater" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af" +dependencies = [ + "base64 0.22.1", + "dirs", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "rustls", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.18", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip 4.6.1", +] + [[package]] name = "tauri-runtime" version = "2.10.0" @@ -4302,6 +4562,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4597,6 +4867,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -4896,6 +5172,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -5126,6 +5411,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -5554,6 +5848,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yeslogic-fontconfig-sys" version = "6.0.0" @@ -5690,6 +5994,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" @@ -5738,6 +6048,18 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.13.0", + "memchr", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 2e49ddaf1..c2ba7004e 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "open_pencil" -version = "0.2.2" -description = "A Tauri App" -authors = ["you"] +version = "0.11.8" +description = "OpenPencil desktop app" +authors = ["Danila Poyarkov"] edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -30,4 +30,6 @@ zstd = "0.13" zip = { version = "2", default-features = false } font-kit = "0.14.3" fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs", branch = "dev" } +tauri-plugin-updater = "2" +tauri-plugin-process = "2" diff --git a/desktop/capabilities/default.json b/desktop/capabilities/default.json index 216c636fd..c668295c9 100644 --- a/desktop/capabilities/default.json +++ b/desktop/capabilities/default.json @@ -9,6 +9,8 @@ "dialog:default", "dialog:allow-save", "dialog:allow-open", + "updater:default", + "process:default", { "identifier": "fs:allow-read-file", "allow": [{ "path": "**" }] diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index 6e953e882..71ff63846 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -23,6 +23,8 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_process::init()) .on_menu_event(|app, event| { handle_menu_event(app, event.id().0.as_str()); }) diff --git a/desktop/src/menu.rs b/desktop/src/menu.rs index a5211ed97..991e8207c 100644 --- a/desktop/src/menu.rs +++ b/desktop/src/menu.rs @@ -69,6 +69,11 @@ pub fn install_app_menu(app: &mut tauri::App) -> tauri::Re Some("About OpenPencil"), None, )?) + .item( + &MenuItemBuilder::new("Check for Updates…") + .id("check-updates") + .build(app)?, + ) .separator() .item(&PredefinedMenuItem::services(app, None)?) .separator() diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index 1177a81cb..9868cc3b8 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -24,6 +24,7 @@ "bundle": { "active": true, "targets": "all", + "createUpdaterArtifacts": true, "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -34,5 +35,14 @@ "macOS": { "signingIdentity": "Developer ID Application: Danila Poyarkov (N7D7M3Q3CD)" } + }, + "plugins": { + "updater": { + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDI2NDdCODAwQkQxMkU0QzUKUldURjVCSzlBTGhISnJORk1HOVlXREVDbXJycWs5U2xxSVBYa1dLMzRoYTVGRVYrUEhmTS9tWWwK", + "endpoints": ["https://github.com/open-pencil/open-pencil/releases/latest/download/latest.json"], + "windows": { + "installMode": "passive" + } + } } } diff --git a/desktop/updater.key.pub b/desktop/updater.key.pub new file mode 100644 index 000000000..9407f1430 --- /dev/null +++ b/desktop/updater.key.pub @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDI2NDdCODAwQkQxMkU0QzUKUldURjVCSzlBTGhISnJORk1HOVlXREVDbXJycWs5U2xxSVBYa1dLMzRoYTVGRVYrUEhmTS9tWWwK \ No newline at end of file diff --git a/package.json b/package.json index 257e98945..d13f935b9 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,9 @@ "@tauri-apps/plugin-dialog": "^2.6.0", "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-shell": "^2.3.5", + "@tauri-apps/plugin-updater": "^2.10.1", "@unhead/vue": "^2.1.10", "@use-gesture/vanilla": "^10.3.1", "@vueuse/core": "^14.2.1", diff --git a/packages/vue/src/i18n/messages.ts b/packages/vue/src/i18n/messages.ts index 2af3207d9..b60c6ad49 100644 --- a/packages/vue/src/i18n/messages.ts +++ b/packages/vue/src/i18n/messages.ts @@ -45,6 +45,7 @@ export const menuMessages = i18n('menu', { zoomOut: 'Zoom out', profiler: 'Performance profiler', language: 'Language', + checkUpdates: 'Check for updates…', moveToPage: 'Move to page', createInstance: 'Create instance', @@ -267,5 +268,16 @@ export const dialogMessages = i18n('dialogs', { connected: 'Connected', search: 'Search…', noResults: 'No results', - share: 'Share' + share: 'Share', + appUpToDate: 'OpenPencil is up to date', + updateAvailableTitle: 'Update OpenPencil', + updateAvailable: params('OpenPencil {version} is available.'), + updateInstallPrompt: + 'Download and install it now? The app will restart after the update is installed.', + downloadingUpdate: params('Downloading OpenPencil {version}'), + updateInstalledTitle: 'Update installed', + updateInstalled: params('OpenPencil {version} was installed{size}. Restarting now.'), + updateUnavailable: + 'Updates are not available yet. Publish a signed release with latest.json first.', + updateCheckFailed: params('Could not check for updates: {error}') }) diff --git a/packages/vue/src/locales/de.json b/packages/vue/src/locales/de.json index 98ed324ed..f2c832979 100644 --- a/packages/vue/src/locales/de.json +++ b/packages/vue/src/locales/de.json @@ -38,7 +38,8 @@ "copyAsPNG": "Als PNG kopieren", "copyAsJSX": "Als JSX kopieren", "copyNodeId": "Knoten-ID kopieren", - "copyXPath": "XPath kopieren" + "copyXPath": "XPath kopieren", + "checkUpdates": "Nach Updates suchen…" }, "commands": { "undo": "Rückgängig", @@ -209,6 +210,15 @@ "connected": "Verbunden", "search": "Suchen…", "noResults": "Keine Ergebnisse", - "share": "Teilen" + "share": "Teilen", + "appUpToDate": "OpenPencil ist auf dem neuesten Stand", + "updateAvailableTitle": "OpenPencil aktualisieren", + "updateAvailable": "OpenPencil {version} ist verfügbar.", + "updateInstallPrompt": "Jetzt herunterladen und installieren? Die App wird nach der Installation neu gestartet.", + "downloadingUpdate": "OpenPencil {version} wird heruntergeladen", + "updateInstalledTitle": "Update installiert", + "updateInstalled": "OpenPencil {version} wurde installiert{size}. Neustart jetzt.", + "updateCheckFailed": "Updates konnten nicht geprüft werden: {error}", + "updateUnavailable": "Updates sind noch nicht verfügbar. Veröffentliche zuerst ein signiertes Release mit latest.json." } } diff --git a/packages/vue/src/locales/es.json b/packages/vue/src/locales/es.json index 123125631..da9b866a6 100644 --- a/packages/vue/src/locales/es.json +++ b/packages/vue/src/locales/es.json @@ -38,7 +38,8 @@ "copyAsPNG": "Copiar como PNG", "copyAsJSX": "Copiar como JSX", "copyNodeId": "Copiar ID del nodo", - "copyXPath": "Copiar XPath" + "copyXPath": "Copiar XPath", + "checkUpdates": "Buscar actualizaciones…" }, "commands": { "undo": "Deshacer", @@ -209,6 +210,15 @@ "connected": "Conectado", "search": "Buscar…", "noResults": "Sin resultados", - "share": "Compartir" + "share": "Compartir", + "appUpToDate": "OpenPencil está actualizado", + "updateAvailableTitle": "Actualizar OpenPencil", + "updateAvailable": "OpenPencil {version} está disponible.", + "updateInstallPrompt": "¿Descargar e instalar ahora? La app se reiniciará después de instalar la actualización.", + "downloadingUpdate": "Descargando OpenPencil {version}", + "updateInstalledTitle": "Actualización instalada", + "updateInstalled": "OpenPencil {version} se instaló{size}. Reiniciando ahora.", + "updateCheckFailed": "No se pudo buscar actualizaciones: {error}", + "updateUnavailable": "Las actualizaciones aún no están disponibles. Publica primero una versión firmada con latest.json." } } diff --git a/packages/vue/src/locales/fr.json b/packages/vue/src/locales/fr.json index b2d076c91..d092f13a4 100644 --- a/packages/vue/src/locales/fr.json +++ b/packages/vue/src/locales/fr.json @@ -38,7 +38,8 @@ "copyAsPNG": "Copier en PNG", "copyAsJSX": "Copier en JSX", "copyNodeId": "Copier l'ID du nœud", - "copyXPath": "Copier le XPath" + "copyXPath": "Copier le XPath", + "checkUpdates": "Rechercher des mises à jour…" }, "commands": { "undo": "Annuler", @@ -209,6 +210,15 @@ "connected": "Connecté", "search": "Rechercher…", "noResults": "Aucun résultat", - "share": "Partager" + "share": "Partager", + "appUpToDate": "OpenPencil est à jour", + "updateAvailableTitle": "Mettre à jour OpenPencil", + "updateAvailable": "OpenPencil {version} est disponible.", + "updateInstallPrompt": "Télécharger et installer maintenant ? L’app redémarrera après l’installation de la mise à jour.", + "downloadingUpdate": "Téléchargement d’OpenPencil {version}", + "updateInstalledTitle": "Mise à jour installée", + "updateInstalled": "OpenPencil {version} a été installé{size}. Redémarrage maintenant.", + "updateCheckFailed": "Impossible de rechercher des mises à jour : {error}", + "updateUnavailable": "Les mises à jour ne sont pas encore disponibles. Publiez d’abord une version signée avec latest.json." } } diff --git a/packages/vue/src/locales/it.json b/packages/vue/src/locales/it.json index a0a023633..1430a07bf 100644 --- a/packages/vue/src/locales/it.json +++ b/packages/vue/src/locales/it.json @@ -38,7 +38,8 @@ "copyAsPNG": "Copia come PNG", "copyAsJSX": "Copia come JSX", "copyNodeId": "Copia ID nodo", - "copyXPath": "Copia XPath" + "copyXPath": "Copia XPath", + "checkUpdates": "Controlla aggiornamenti…" }, "commands": { "undo": "Annulla", @@ -209,6 +210,15 @@ "connected": "Connesso", "search": "Cerca…", "noResults": "Nessun risultato", - "share": "Condividi" + "share": "Condividi", + "appUpToDate": "OpenPencil è aggiornato", + "updateAvailableTitle": "Aggiorna OpenPencil", + "updateAvailable": "OpenPencil {version} è disponibile.", + "updateInstallPrompt": "Scaricare e installare ora? L’app verrà riavviata dopo l’installazione dell’aggiornamento.", + "downloadingUpdate": "Download di OpenPencil {version}", + "updateInstalledTitle": "Aggiornamento installato", + "updateInstalled": "OpenPencil {version} è stato installato{size}. Riavvio in corso.", + "updateCheckFailed": "Impossibile controllare gli aggiornamenti: {error}", + "updateUnavailable": "Gli aggiornamenti non sono ancora disponibili. Pubblica prima una release firmata con latest.json." } } diff --git a/packages/vue/src/locales/pl.json b/packages/vue/src/locales/pl.json index eaf8fc417..12373873a 100644 --- a/packages/vue/src/locales/pl.json +++ b/packages/vue/src/locales/pl.json @@ -38,7 +38,8 @@ "copyAsPNG": "Kopiuj jako PNG", "copyAsJSX": "Kopiuj jako JSX", "copyNodeId": "Kopiuj ID węzła", - "copyXPath": "Kopiuj XPath" + "copyXPath": "Kopiuj XPath", + "checkUpdates": "Sprawdź aktualizacje…" }, "commands": { "undo": "Cofnij", @@ -209,6 +210,15 @@ "connected": "Połączono", "search": "Szukaj…", "noResults": "Brak wyników", - "share": "Udostępnij" + "share": "Udostępnij", + "appUpToDate": "OpenPencil jest aktualny", + "updateAvailableTitle": "Zaktualizuj OpenPencil", + "updateAvailable": "OpenPencil {version} jest dostępny.", + "updateInstallPrompt": "Pobrać i zainstalować teraz? Aplikacja uruchomi się ponownie po zainstalowaniu aktualizacji.", + "downloadingUpdate": "Pobieranie OpenPencil {version}", + "updateInstalledTitle": "Aktualizacja zainstalowana", + "updateInstalled": "OpenPencil {version} został zainstalowany{size}. Ponowne uruchamianie.", + "updateCheckFailed": "Nie udało się sprawdzić aktualizacji: {error}", + "updateUnavailable": "Aktualizacje nie są jeszcze dostępne. Najpierw opublikuj podpisane wydanie z latest.json." } } diff --git a/packages/vue/src/locales/ru.json b/packages/vue/src/locales/ru.json index fd9ebb9a3..8d82760e9 100644 --- a/packages/vue/src/locales/ru.json +++ b/packages/vue/src/locales/ru.json @@ -38,7 +38,8 @@ "copyAsPNG": "Копировать как PNG", "copyAsJSX": "Копировать как JSX", "copyNodeId": "Копировать ID узла", - "copyXPath": "Копировать XPath" + "copyXPath": "Копировать XPath", + "checkUpdates": "Проверить обновления…" }, "commands": { "undo": "Отменить", @@ -209,6 +210,15 @@ "connected": "Подключено", "search": "Поиск…", "noResults": "Ничего не найдено", - "share": "Поделиться" + "share": "Поделиться", + "appUpToDate": "OpenPencil обновлён до последней версии", + "updateAvailableTitle": "Обновить OpenPencil", + "updateAvailable": "Доступен OpenPencil {version}.", + "updateInstallPrompt": "Скачать и установить сейчас? После установки обновления приложение перезапустится.", + "downloadingUpdate": "Загрузка OpenPencil {version}", + "updateInstalledTitle": "Обновление установлено", + "updateInstalled": "OpenPencil {version} установлен{size}. Перезапуск.", + "updateCheckFailed": "Не удалось проверить обновления: {error}", + "updateUnavailable": "Обновления пока недоступны. Сначала опубликуйте подписанный релиз с latest.json." } } diff --git a/packages/vue/src/locales/zh-CN.json b/packages/vue/src/locales/zh-CN.json index b65b56a20..d3e0cab2b 100644 --- a/packages/vue/src/locales/zh-CN.json +++ b/packages/vue/src/locales/zh-CN.json @@ -38,7 +38,8 @@ "copyAsPNG": "复制为 PNG", "copyAsJSX": "复制为 JSX", "copyNodeId": "复制节点 ID", - "copyXPath": "复制 XPath" + "copyXPath": "复制 XPath", + "checkUpdates": "检查更新…" }, "commands": { "undo": "撤销", @@ -89,7 +90,7 @@ "autoLayout": "自动布局", "alignment": "对齐", "appearance": "外观", - "sizingFill": "填充", + "fill": "填充", "stroke": "描边", "effects": "效果", "export": "导出", @@ -158,6 +159,7 @@ "rows": "行", "sizingFixed": "固定", "sizingHug": "适应内容", + "sizingFill": "填充", "sizingHugShort": "适应", "sizingFillShort": "填充", "sizingFillFr": "填充 (fr)", @@ -208,6 +210,15 @@ "connected": "已连接", "search": "搜索…", "noResults": "无结果", - "share": "分享" + "share": "分享", + "appUpToDate": "OpenPencil 已是最新版本", + "updateAvailableTitle": "更新 OpenPencil", + "updateAvailable": "OpenPencil {version} 可用。", + "updateInstallPrompt": "现在下载并安装?更新安装后应用将重启。", + "downloadingUpdate": "正在下载 OpenPencil {version}", + "updateInstalledTitle": "更新已安装", + "updateInstalled": "OpenPencil {version} 已安装{size}。正在重启。", + "updateCheckFailed": "无法检查更新:{error}", + "updateUnavailable": "更新尚不可用。请先发布带有 latest.json 的已签名版本。" } } diff --git a/src/App.vue b/src/App.vue index 03f9976de..eb57f5815 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,20 +3,23 @@ import { onMounted } from 'vue' import { useHead } from '@unhead/vue' import { TooltipProvider } from 'reka-ui' -import { provideEditor } from '@open-pencil/vue' +import { provideEditor, useI18n } from '@open-pencil/vue' import AppToast from '@/components/AppToast.vue' import { useEditorStore } from '@/app/editor/active-store' import { toast } from '@/app/shell/ui' import { useAppTheme } from '@/app/shell/theme' +import { scheduleStartupUpdateCheck } from '@/app/shell/updater' useHead({ titleTemplate: (title) => (title ? `${title} — OpenPencil` : 'OpenPencil') }) const store = useEditorStore() +const { dialogs } = useI18n() provideEditor(store) useAppTheme() onMounted(() => { toast.setupGlobalErrorHandler() + scheduleStartupUpdateCheck(dialogs) }) diff --git a/src/app/shell/menu/use.ts b/src/app/shell/menu/use.ts index 1abd86aac..970ea092a 100644 --- a/src/app/shell/menu/use.ts +++ b/src/app/shell/menu/use.ts @@ -3,9 +3,10 @@ import { tryOnScopeDispose } from '@vueuse/core' import { useEditorStore } from '@/app/editor/active-store' import { importFileDialog, openFileDialog } from '@/app/shell/menu/files' import { useAppTheme } from '@/app/shell/theme' +import { checkForAppUpdate } from '@/app/shell/updater' import { createTab, closeTab, activeTab } from '@/app/tabs' import { IS_TAURI } from '@/constants' -import { useEditorCommands } from '@open-pencil/vue' +import { useEditorCommands, useI18n } from '@open-pencil/vue' import type { EditorCommandId } from '@open-pencil/vue' @@ -55,6 +56,7 @@ export function useMenu() { let unlisten: (() => void) | undefined const { setTheme } = useAppTheme() + const { dialogs } = useI18n() const { runCommand } = useEditorCommands() const actions: Partial void>> = { @@ -90,6 +92,7 @@ export function useMenu() { 'toggle-ui': () => { store.state.showUI = !store.state.showUI }, + 'check-updates': () => void checkForAppUpdate({ messages: dialogs }), 'text.bold': () => { const node = store.selectedNodes.value.find((item) => item.type === 'TEXT') updateSelectedText({ fontWeight: node && node.fontWeight >= 700 ? 400 : 700 }) diff --git a/src/app/shell/updater.ts b/src/app/shell/updater.ts new file mode 100644 index 000000000..3afbeaef2 --- /dev/null +++ b/src/app/shell/updater.ts @@ -0,0 +1,121 @@ +import { promiseTimeout } from '@vueuse/core' + +import { toast } from '@/app/shell/ui' +import { IS_TAURI } from '@/constants' + +import type { Ref } from 'vue' + +const STARTUP_UPDATE_CHECK_DELAY_MS = 2500 + +interface UpdaterMessages { + appUpToDate: string + updateAvailableTitle: string + updateAvailable: (params: { version: string }) => string + updateInstallPrompt: string + downloadingUpdate: (params: { version: string }) => string + updateInstalledTitle: string + updateInstalled: (params: { version: string; size: string }) => string + updateUnavailable: string + updateCheckFailed: (params: { error: string }) => string +} + +interface UpdateCheckOptions { + silent?: boolean + messages: Ref +} + +let startupCheckStarted = false +let updateCheckInFlight: Promise | null = null + +export async function checkForAppUpdate(options: UpdateCheckOptions) { + if (!IS_TAURI) return + if (updateCheckInFlight) return updateCheckInFlight + + const { silent = false, messages } = options + updateCheckInFlight = runUpdateCheck(silent, messages).finally(() => { + updateCheckInFlight = null + }) + return updateCheckInFlight +} + +export function scheduleStartupUpdateCheck(messages: Ref) { + if (startupCheckStarted || !IS_TAURI) return + startupCheckStarted = true + void promiseTimeout(STARTUP_UPDATE_CHECK_DELAY_MS).then(() => + checkForAppUpdate({ silent: true, messages }) + ) +} + +async function runUpdateCheck(silent: boolean, messages: Ref) { + try { + const [{ check }, { confirm, message }, { relaunch }] = await Promise.all([ + import('@tauri-apps/plugin-updater'), + import('@tauri-apps/plugin-dialog'), + import('@tauri-apps/plugin-process') + ]) + + const update = await check() + const t = messages.value + + if (!update) { + if (!silent) toast.info(t.appUpToDate) + return + } + + const details = [ + t.updateAvailable({ version: update.version }), + update.body ? `\n${update.body}` : '', + `\n${t.updateInstallPrompt}` + ].join('') + + const shouldInstall = await confirm(details, { + title: t.updateAvailableTitle, + kind: 'info' + }) + + if (!shouldInstall) return + + let downloaded = 0 + let contentLength: number | undefined + toast.info(t.downloadingUpdate({ version: update.version })) + + await update.downloadAndInstall((event) => { + if (event.event === 'Started') { + contentLength = event.data.contentLength + return + } + if (event.event === 'Progress') { + downloaded += event.data.chunkLength + } + }) + + const sizeLabel = contentLength + ? ` (${formatBytes(downloaded)} of ${formatBytes(contentLength)})` + : '' + await message(t.updateInstalled({ version: update.version, size: sizeLabel }), { + title: t.updateInstalledTitle, + kind: 'info' + }) + await relaunch() + } catch (error) { + if (!silent) { + const message = error instanceof Error ? error.message : String(error) + toast.warning( + isMissingUpdateManifestError(message) + ? messages.value.updateUnavailable + : messages.value.updateCheckFailed({ error: message }) + ) + } + } +} + +function isMissingUpdateManifestError(message: string) { + return message.toLowerCase().includes('valid release json') +} + +function formatBytes(bytes: number) { + if (bytes < 1024) return `${bytes} B` + const kib = bytes / 1024 + if (kib < 1024) return `${kib.toFixed(1)} KiB` + return `${(kib / 1024).toFixed(1)} MiB` +} diff --git a/src/components/AppToast.vue b/src/components/AppToast.vue index 7dbd6a745..fbfd0b548 100644 --- a/src/components/AppToast.vue +++ b/src/components/AppToast.vue @@ -29,9 +29,9 @@ const { copy, copied } = useClipboard({ copiedDuring: 1500 }) {{ t.message }}×{{ t.count }} - +