From fdcc9efb313309f902801736a57b1f0b89017e0c Mon Sep 17 00:00:00 2001 From: Bonchellon Date: Tue, 7 Jul 2026 13:51:42 +0300 Subject: [PATCH] Add non-intrusive auto-updater Tauri updater + process plugins. Checks Gitea Releases endpoint a few seconds after launch; on a newer signed version shows a dismissible bottom-right banner with download progress and auto-relaunch. Public signing key in tauri.conf.json; release flow documented in RELEASING.md. Co-Authored-By: Claude Opus 4.8 --- RELEASING.md | 57 ++++ package-lock.json | 20 ++ package.json | 2 + src-tauri/Cargo.lock | 387 +++++++++++++++++++++++++++- src-tauri/Cargo.toml | 5 + src-tauri/capabilities/default.json | 4 +- src-tauri/src/lib.rs | 11 +- src-tauri/tauri.conf.json | 12 + src/App.css | 29 +++ src/App.tsx | 17 +- src/Updater.tsx | 83 ++++++ 11 files changed, 612 insertions(+), 15 deletions(-) create mode 100644 RELEASING.md create mode 100644 src/Updater.tsx diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..8b23c7d --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,57 @@ +# Выпуск обновления (auto-updater) + +Приложение при запуске тихо проверяет endpoint на Gitea Releases и, если есть +новая **подписанная** версия, показывает ненавязчивый баннер в правом нижнем углу. + +Endpoint (задан в `src-tauri/tauri.conf.json`): +``` +https://gitea.exbytestudios.com/ExbytePublicServices/exbyte-depot-viewer-perforce/releases/download/updater/latest.json +``` + +## Ключи подписи + +- Приватный ключ: `C:\Users\Bonchellon\.tauri\exbyte-depot-updater.key` — **СЕКРЕТ, не коммитить, не терять!** +- Публичный ключ уже вшит в `tauri.conf.json` (`plugins.updater.pubkey`). + +Без приватного ключа собрать валидное обновление невозможно — сделай бэкап ключа +в надёжном месте (менеджер паролей / приватный репозиторий секретов). + +## Как выпустить новую версию + +1. Подними версию в **двух** местах (должны совпадать): + - `package.json` → `"version"` + - `src-tauri/tauri.conf.json` → `"version"` + +2. Собери подписанный инсталлятор: + ```powershell + $env:TAURI_SIGNING_PRIVATE_KEY = Get-Content "$HOME\.tauri\exbyte-depot-updater.key" -Raw + $env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD = "" + npm run tauri build + ``` + Артефакты появятся в `src-tauri/target/release/bundle/nsis/`: + - `exbyte-depot__x64-setup.exe` — инсталлятор + - `exbyte-depot__x64-setup.exe.sig` — подпись (нужна для latest.json) + +3. Создай/обнови на Gitea релиз с тегом **`updater`** и приложи к нему: + - `exbyte-depot__x64-setup.exe` + - `latest.json` (см. ниже) + +4. `latest.json` — вставь версию, дату, содержимое `.sig` в поле `signature`, + и прямую ссылку на `.exe` в `url`: + ```json + { + "version": "0.2.0", + "notes": "Что нового в этой версии", + "pub_date": "2026-07-07T12:00:00Z", + "platforms": { + "windows-x86_64": { + "signature": "<содержимое файла .exe.sig>", + "url": "https://gitea.exbytestudios.com/ExbytePublicServices/exbyte-depot-viewer-perforce/releases/download/v0.2.0/exbyte-depot_0.2.0_x64-setup.exe" + } + } + } + ``` + (сам `.exe` можно положить в релиз с тегом версии `v0.2.0`, а `latest.json` — в релиз `updater`.) + +Готово: у всех, кто открыл приложение, через несколько секунд появится баннер +«Доступно обновление v0.2.0». diff --git a/package-lock.json b/package-lock.json index 3abd84d..fa1e626 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-updater": "^2.10.1", "react": "^19.1.0", "react-dom": "^19.1.0", "three": "^0.185.1" @@ -1462,6 +1464,24 @@ "@tauri-apps/api": "^2.11.0" } }, + "node_modules/@tauri-apps/plugin-process": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz", + "integrity": "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@tauri-apps/plugin-updater": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.1.tgz", + "integrity": "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, "node_modules/@tweenjs/tween.js": { "version": "23.1.3", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", diff --git a/package.json b/package.json index 0e6a362..e279bbf 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-updater": "^2.10.1", "react": "^19.1.0", "react-dom": "^19.1.0", "three": "^0.185.1" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ba6f49d..c7274b5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -47,6 +47,15 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -670,6 +679,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -939,6 +959,8 @@ dependencies = [ "tauri-build", "tauri-plugin-dialog", "tauri-plugin-opener", + "tauri-plugin-process", + "tauri-plugin-updater", ] [[package]] @@ -966,6 +988,16 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1510,6 +1542,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" @@ -1778,6 +1825,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -1967,6 +2044,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" @@ -2215,6 +2298,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.13.0", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + [[package]] name = "objc2-quartz-core" version = "0.3.2" @@ -2289,6 +2384,12 @@ dependencies = [ "libc", ] +[[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" @@ -2305,6 +2406,20 @@ dependencies = [ "pin-project-lite", ] +[[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" @@ -2696,15 +2811,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", @@ -2740,6 +2860,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-hash" version = "2.1.3" @@ -2768,6 +2902,79 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni 0.22.4", + "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" @@ -2783,6 +2990,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" @@ -2840,6 +3056,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.13.0", + "core-foundation", + "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.36.1" @@ -3060,6 +3299,22 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.3" @@ -3172,6 +3427,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" @@ -3255,7 +3516,7 @@ dependencies = [ "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", + "jni 0.21.1", "libc", "log", "ndk", @@ -3288,6 +3549,17 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -3311,7 +3583,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", - "jni", + "jni 0.21.1", "libc", "log", "mime", @@ -3487,6 +3759,49 @@ 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-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", +] + [[package]] name = "tauri-runtime" version = "2.11.3" @@ -3497,7 +3812,7 @@ dependencies = [ "dpi", "gtk", "http", - "jni", + "jni 0.21.1", "objc2", "objc2-ui-kit", "objc2-web-kit", @@ -3520,7 +3835,7 @@ checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" dependencies = [ "gtk", "http", - "jni", + "jni 0.21.1", "log", "objc2", "objc2-app-kit", @@ -3718,6 +4033,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" @@ -4031,6 +4356,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[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" @@ -4274,6 +4605,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -4504,6 +4844,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" @@ -4799,7 +5148,7 @@ dependencies = [ "gtk", "http", "javascriptcore-rs", - "jni", + "jni 0.21.1", "libc", "ndk", "objc2", @@ -4846,6 +5195,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 = "yoke" version = "0.8.3" @@ -4951,6 +5310,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zerotrie" version = "0.2.4" @@ -4984,6 +5349,18 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.14.0", + "memchr", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6f70127..46e01be 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -24,3 +24,8 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" tauri-plugin-dialog = "2.7.1" +# Auto-updater (desktop only — checks Gitea Releases for a signed new version) +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +tauri-plugin-updater = "2" +tauri-plugin-process = "2" + diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 07f4a04..41d7c52 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -10,6 +10,8 @@ "core:window:allow-minimize", "core:window:allow-toggle-maximize", "core:window:allow-close", - "dialog:default" + "dialog:default", + "updater:default", + "process:default" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 76a1990..fb82cd0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -558,9 +558,16 @@ async fn p4_depots(state: State<'_, AppState>) -> Result, String> { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - tauri::Builder::default() + let mut builder = tauri::Builder::default() .plugin(tauri_plugin_opener::init()) - .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_dialog::init()); + #[cfg(desktop)] + { + builder = builder + .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_process::init()); + } + builder .manage(AppState::default()) .invoke_handler(tauri::generate_handler![ p4_clients, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 64bcab7..bc65fdb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -29,6 +29,7 @@ "bundle": { "active": true, "targets": "all", + "createUpdaterArtifacts": true, "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -36,5 +37,16 @@ "icons/icon.icns", "icons/icon.ico" ] + }, + "plugins": { + "updater": { + "endpoints": [ + "https://gitea.exbytestudios.com/ExbytePublicServices/exbyte-depot-viewer-perforce/releases/download/updater/latest.json" + ], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQwODU3MDRBRTI3QkRDNTAKUldSUTNIdmlTbkNGMENZNFZib1lVMWFmOEVacDE2WVVUaGZIVWRTaURrUEJmbUJNajByTmdERkMK", + "windows": { + "installMode": "passive" + } + } } } diff --git a/src/App.css b/src/App.css index b9900b2..5143962 100644 --- a/src/App.css +++ b/src/App.css @@ -201,6 +201,35 @@ body.resizing{cursor:col-resize!important;user-select:none} .ctxitem:hover{background:var(--hover)} .ctxitem.danger{color:var(--del)} .ctxitem.danger:hover{background:rgba(242,99,126,.12)} + +/* non-intrusive update banner (bottom-right) */ +.upd{position:fixed;right:18px;bottom:18px;z-index:400;width:320px;padding:15px 16px;border-radius:14px; + background:var(--elevated);border:1px solid var(--border);box-shadow:0 22px 50px -14px rgba(0,0,0,.55); + animation:updin .35s cubic-bezier(.2,.9,.3,1)} +@keyframes updin{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}} +.upd-head{display:flex;align-items:center;gap:11px} +.upd-ic{width:34px;height:34px;flex:0 0 auto;border-radius:10px;display:grid;place-items:center;color:#fff; + background:linear-gradient(145deg,var(--accent-2),var(--accent-deep));box-shadow:0 0 18px rgba(124,110,246,.5)} +.upd-ic svg{width:18px;height:18px} +.upd-ttl{display:flex;flex-direction:column;gap:1px;min-width:0} +.upd-ttl b{font-size:13.5px} +.upd-ttl span{font-size:11.5px;color:var(--faint);font-variant-numeric:tabular-nums} +.upd-x{margin-left:auto;width:24px;height:24px;flex:0 0 auto;border:none;background:none;color:var(--faint); + border-radius:7px;cursor:pointer;display:grid;place-items:center} +.upd-x:hover{background:var(--hover);color:var(--txt)} +.upd-notes{margin-top:10px;font-size:11.5px;line-height:1.5;color:var(--muted);max-height:70px;overflow:auto;white-space:pre-line} +.upd-notes.err{color:var(--del)} +.upd-prog{margin-top:12px;display:flex;flex-direction:column;gap:7px} +.upd-prog span{font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums} +.upd-track{height:7px;border-radius:5px;background:var(--panel-3);overflow:hidden} +.upd-fill{height:100%;border-radius:5px;background:linear-gradient(90deg,var(--accent-2),var(--accent-deep));transition:width .2s;box-shadow:0 0 10px rgba(124,110,246,.6)} +.upd-actions{margin-top:13px;display:flex;gap:9px} +.upd-later{flex:0 0 auto;padding:8px 14px;border-radius:9px;border:1px solid var(--border);background:var(--panel); + color:var(--muted);font-size:12.5px;cursor:pointer;font-family:var(--font)} +.upd-later:hover{color:var(--txt);border-color:var(--accent)} +.upd-go{flex:1;padding:8px 14px;border:none;border-radius:9px;font-size:12.5px;font-weight:600;color:#fff;cursor:pointer; + background:linear-gradient(135deg,var(--accent-2),var(--accent-deep));box-shadow:0 8px 20px -8px rgba(124,110,246,.7)} +.upd-go:hover{filter:brightness(1.08)} .tzone.push .ic{color:var(--add)} .tzone.push .val{color:var(--add)} .commit .row{display:flex;gap:10px;align-items:center} diff --git a/src/App.tsx b/src/App.tsx index 43baaae..ea240d4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import type { MouseEvent as ReactMouseEvent, CSSProperties } from "react"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWebview } from "@tauri-apps/api/webview"; import ModelViewer from "./ModelViewer"; +import UpdateBanner from "./Updater"; import "./App.css"; import { p4, statusOf, splitPath, kindOf, fmtTime, describeFiles, leaf, saveSession, loadSession, clearSession, saveScope, loadScope, P4Info, OpenedFile, Client, Change, Session, Describe, Dir } from "./p4"; @@ -64,8 +65,9 @@ export default function App() { .catch(() => setPhase("connect")); }, []); // eslint-disable-line + let content; if (phase === "loading") - return ( + content = (
{I.hex}
Exbyte Depot — Perforce
@@ -75,14 +77,15 @@ export default function App() {
Восстановление сессии…
); - - if (phase === "connect") - return { saveSession(s); setInfo(i); setSession(s); setPhase("main"); }} />; + else + content = { setSession(s); saveSession(s); }} + onDisconnect={() => { p4.disconnect(); clearSession(); setInfo(null); setSession(null); setPhase("connect"); }} />; - return { setSession(s); saveSession(s); }} - onDisconnect={() => { p4.disconnect(); clearSession(); setInfo(null); setSession(null); setPhase("connect"); }} />; + return <>{content}; } /* ---------------- connect screen ---------------- */ diff --git a/src/Updater.tsx b/src/Updater.tsx new file mode 100644 index 0000000..bd758b9 --- /dev/null +++ b/src/Updater.tsx @@ -0,0 +1,83 @@ +import { useEffect, useState } from "react"; +import { check, type Update } from "@tauri-apps/plugin-updater"; +import { relaunch } from "@tauri-apps/plugin-process"; + +type Phase = "idle" | "available" | "downloading" | "done" | "error"; + +// Non-intrusive update prompt: quietly checks the Gitea Releases endpoint a few +// seconds after launch. If a newer signed version exists, slides a small card into +// the bottom-right corner. The user chooses when to install — nothing is forced. +export default function UpdateBanner() { + const [update, setUpdate] = useState(null); + const [phase, setPhase] = useState("idle"); + const [pct, setPct] = useState(0); + const [err, setErr] = useState(""); + const [hidden, setHidden] = useState(false); + + useEffect(() => { + let live = true; + const t = setTimeout(async () => { + try { + const u = await check(); + if (live && u) { setUpdate(u); setPhase("available"); } + } catch { + // offline, or no release published yet — stay silent, never nag + } + }, 4000); + return () => { live = false; clearTimeout(t); }; + }, []); + + async function install() { + if (!update) return; + setPhase("downloading"); setPct(0); + let total = 0, got = 0; + try { + await update.downloadAndInstall((e) => { + if (e.event === "Started") total = e.data.contentLength || 0; + else if (e.event === "Progress") { got += e.data.chunkLength; if (total) setPct(Math.min(100, Math.round((got / total) * 100))); } + else if (e.event === "Finished") setPct(100); + }); + setPhase("done"); + await relaunch(); + } catch (e) { setErr(String(e)); setPhase("error"); } + } + + if (hidden || phase === "idle" || !update) return null; + + return ( +
+
+ + + +
+ Доступно обновление + Exbyte Depot v{update.version} +
+ {(phase === "available" || phase === "error") && ( + + )} +
+ + {update.body?.trim() && phase === "available" &&
{update.body.trim()}
} + + {phase === "downloading" && ( +
+
+ Загрузка… {pct}% +
+ )} + {phase === "done" &&
Установлено — перезапускаю…
} + {phase === "error" &&
{err}
} + + {phase === "available" && ( +
+ + +
+ )} +
+ ); +}