feat(tauri): add signed updater

This commit is contained in:
Danila Poyarkov 2026-05-01 12:18:53 +03:00
parent 2b3898aace
commit b248e0c53a
25 changed files with 601 additions and 31 deletions

View file

@ -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')

3
.gitignore vendored
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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=="],

330
desktop/Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -9,6 +9,8 @@
"dialog:default",
"dialog:allow-save",
"dialog:allow-open",
"updater:default",
"process:default",
{
"identifier": "fs:allow-read-file",
"allow": [{ "path": "**" }]

View file

@ -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());
})

View file

@ -69,6 +69,11 @@ pub fn install_app_menu<R: tauri::Runtime>(app: &mut tauri::App<R>) -> tauri::Re
Some("About OpenPencil"),
None,
)?)
.item(
&MenuItemBuilder::new("Check for Updates…")
.id("check-updates")
.build(app)?,
)
.separator()
.item(&PredefinedMenuItem::services(app, None)?)
.separator()

View file

@ -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"
}
}
}
}

1
desktop/updater.key.pub Normal file
View file

@ -0,0 +1 @@
dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDI2NDdCODAwQkQxMkU0QzUKUldURjVCSzlBTGhISnJORk1HOVlXREVDbXJycWs5U2xxSVBYa1dLMzRoYTVGRVYrUEhmTS9tWWwK

View file

@ -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",

View file

@ -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}')
})

View file

@ -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."
}
}

View file

@ -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."
}
}

View file

@ -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."
}
}

View file

@ -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."
}
}

View file

@ -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."
}
}

View file

@ -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."
}
}

View file

@ -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 的已签名版本。"
}
}

View file

@ -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)
})
</script>

View file

@ -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<Record<string, () => 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 })

121
src/app/shell/updater.ts Normal file
View file

@ -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<UpdaterMessages>
}
let startupCheckStarted = false
let updateCheckInFlight: Promise<void> | 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<UpdaterMessages>) {
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<UpdaterMessages>) {
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`
}

View file

@ -29,9 +29,9 @@ const { copy, copied } = useClipboard({ copiedDuring: 1500 })
<ToastDescription class="min-w-0 flex-1 select-text">
{{ t.message }}<span v-if="t.count > 1" class="ml-1.5 opacity-70">×{{ t.count }}</span>
</ToastDescription>
<Tip v-if="t.variant === 'error'" :label="copied ? 'Copied!' : 'Copy error'">
<Tip v-if="t.variant !== 'default'" :label="copied ? 'Copied!' : 'Copy message'">
<button
data-test-id="toast-copy-error"
data-test-id="toast-copy-message"
class="mt-0.5 shrink-0 cursor-pointer rounded p-0.5 opacity-70 hover:opacity-100"
@click="copy(t.message)"
>
@ -40,7 +40,7 @@ const { copy, copied } = useClipboard({ copiedDuring: 1500 })
</button>
</Tip>
<ToastClose
v-if="t.variant === 'error'"
v-if="t.variant !== 'default'"
data-test-id="toast-close"
class="mt-0.5 shrink-0 cursor-pointer rounded p-0.5 opacity-70 hover:opacity-100"
>