Add auto-update support (tauri-plugin-updater) and Gitea release pipeline

Ed25519-signed updates, checked via a fixed-tag latest.json manifest on
Gitea Releases; scripts/release.sh automates build+sign+publish. Adds
a manual "Nach Updates suchen" trigger plus a silent startup check.
This commit is contained in:
2026-07-28 21:38:49 +02:00
parent 20bfd2cf6b
commit 4b2248655b
11 changed files with 540 additions and 2 deletions
+20
View File
@@ -10,6 +10,8 @@
"dependencies": {
"@tauri-apps/api": "^2.11.1",
"@tauri-apps/plugin-dialog": "^2.7.2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.1",
"clsx": "^2.1.1",
"date-fns": "^4.4.0",
"lucide-react": "^1.27.0",
@@ -1500,6 +1502,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/@tybys/wasm-util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+3 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mail-client",
"private": true,
"version": "0.0.0",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -13,6 +13,8 @@
"dependencies": {
"@tauri-apps/api": "^2.11.1",
"@tauri-apps/plugin-dialog": "^2.7.2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.1",
"clsx": "^2.1.1",
"date-fns": "^4.4.0",
"lucide-react": "^1.27.0",
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env bash
# Builds, signs, and publishes a release to Gitea (git.kanjiv.at/KanjiG/mail-client).
#
# Usage: scripts/release.sh <version> [notes]
# scripts/release.sh 0.2.0 "Fixed X, added Y"
#
# Requires:
# - TAURI_SIGNING_PRIVATE_KEY_PATH pointing at the updater signing key
# (default: ~/.tauri/mail-client-update.key)
# - GITEA_TOKEN or GITEA_PASSWORD env var for authenticating to the Gitea API
# (GITEA_TOKEN preferred — a Gitea access token instead of the account password)
#
# Only builds for the platform it's run on. To publish Windows/macOS builds,
# run this same script on/from those platforms with the same version — it
# appends to the same release rather than overwriting it, and regenerates
# latest.json to include whichever platform artifacts are present so far.
set -euo pipefail
VERSION="${1:?Usage: scripts/release.sh <version> [notes]}"
NOTES="${2:-Release $VERSION}"
REPO_OWNER="KanjiG"
REPO_NAME="mail-client"
GITEA_URL="https://git.kanjiv.at"
TAG="v$VERSION"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_ROOT"
export TAURI_SIGNING_PRIVATE_KEY_PATH="${TAURI_SIGNING_PRIVATE_KEY_PATH:-$HOME/.tauri/mail-client-update.key}"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}"
if [ -z "${GITEA_TOKEN:-}" ] && [ -z "${GITEA_PASSWORD:-}" ]; then
echo "Set GITEA_TOKEN (preferred) or GITEA_PASSWORD before running this." >&2
exit 1
fi
AUTH_HEADER=()
if [ -n "${GITEA_TOKEN:-}" ]; then
AUTH_HEADER=(-H "Authorization: token $GITEA_TOKEN")
else
AUTH_HEADER=(-u "$REPO_OWNER:$GITEA_PASSWORD")
fi
echo "==> Syncing version to $VERSION in package.json / tauri.conf.json"
node -e "
const fs = require('fs');
for (const f of ['package.json', 'src-tauri/tauri.conf.json']) {
const j = JSON.parse(fs.readFileSync(f, 'utf8'));
j.version = '$VERSION';
fs.writeFileSync(f, JSON.stringify(j, null, 2) + '\n');
}
"
sed -i "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
echo "==> Building frontend"
npm run build
echo "==> Building + signing Tauri bundle"
npx tauri build
BUNDLE_DIR="src-tauri/target/release/bundle"
# Figure out which platform we're on and where the updater-relevant artifact +
# signature landed. Tauri's updater only consumes specific bundle formats per
# platform (AppImage on Linux, NSIS on Windows, .app.tar.gz on macOS) — not
# .deb/.rpm, those are for manual installs only.
UNAME="$(uname -s)"
PLATFORM_KEY=""
ARTIFACT=""
case "$UNAME" in
Linux)
PLATFORM_KEY="linux-x86_64"
ARTIFACT="$(find "$BUNDLE_DIR/appimage" -name '*.AppImage' | head -1)"
;;
Darwin)
ARCH="$(uname -m)"
PLATFORM_KEY="darwin-$([ "$ARCH" = "arm64" ] && echo aarch64 || echo x86_64)"
ARTIFACT="$(find "$BUNDLE_DIR/macos" -name '*.app.tar.gz' | head -1)"
;;
MINGW*|MSYS*|CYGWIN*)
PLATFORM_KEY="windows-x86_64"
ARTIFACT="$(find "$BUNDLE_DIR/nsis" -name '*-setup.exe' | head -1)"
;;
*)
echo "Unrecognized platform: $UNAME" >&2
exit 1
;;
esac
if [ -z "$ARTIFACT" ] || [ ! -f "$ARTIFACT" ]; then
echo "Could not find the updater artifact for $PLATFORM_KEY under $BUNDLE_DIR" >&2
exit 1
fi
SIGNATURE="$(cat "$ARTIFACT.sig")"
FILENAME="$(basename "$ARTIFACT")"
echo "==> Ensuring release $TAG exists on Gitea"
RELEASE_JSON="$(curl -s "${AUTH_HEADER[@]}" "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$TAG")"
RELEASE_ID="$(echo "$RELEASE_JSON" | node -pe 'JSON.parse(require("fs").readFileSync(0)).id' 2>/dev/null || echo "")"
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "undefined" ]; then
RELEASE_JSON="$(curl -s "${AUTH_HEADER[@]}" -X POST "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases" \
-H "Content-Type: application/json" \
-d "$(node -pe "JSON.stringify({tag_name: '$TAG', name: '$TAG', body: process.argv[1]})" "$NOTES")")
RELEASE_ID="$(echo "$RELEASE_JSON" | node -pe 'JSON.parse(require("fs").readFileSync(0)).id')"
fi
echo " release id: $RELEASE_ID"
echo "==> Uploading $FILENAME"
curl -s "${AUTH_HEADER[@]}" -X POST \
"$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/$RELEASE_ID/assets?name=$FILENAME" \
-F "attachment=@$ARTIFACT" > /dev/null
# latest.json lives on a SEPARATE, fixed-tag release ("latest-manifest"),
# independent of the versioned release tags above. The updater's endpoint in
# tauri.conf.json points at this fixed tag, not at "latest" — Gitea's support
# for a GitHub-style /releases/latest/download/ alias varies by version, so a
# fixed tag we fully control is more robust than depending on that.
MANIFEST_TAG="latest-manifest"
echo "==> Ensuring $MANIFEST_TAG release exists"
MANIFEST_RELEASE_JSON="$(curl -s "${AUTH_HEADER[@]}" "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$MANIFEST_TAG")"
MANIFEST_RELEASE_ID="$(echo "$MANIFEST_RELEASE_JSON" | node -pe 'JSON.parse(require("fs").readFileSync(0)).id' 2>/dev/null || echo "")"
if [ -z "$MANIFEST_RELEASE_ID" ] || [ "$MANIFEST_RELEASE_ID" = "undefined" ]; then
MANIFEST_RELEASE_JSON="$(curl -s "${AUTH_HEADER[@]}" -X POST "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases" \
-H "Content-Type: application/json" \
-d "$(node -pe "JSON.stringify({tag_name: process.argv[1], name: 'Update manifest (do not download directly)', body: 'Holds latest.json for the in-app updater. Always points at the newest release.'})" "$MANIFEST_TAG")")
MANIFEST_RELEASE_ID="$(echo "$MANIFEST_RELEASE_JSON" | node -pe 'JSON.parse(require("fs").readFileSync(0)).id')"
fi
echo "==> Fetching existing latest.json (if any) to merge platforms"
EXISTING_LATEST="$(curl -sf "$GITEA_URL/$REPO_OWNER/$REPO_NAME/releases/download/$MANIFEST_TAG/latest.json" || echo '{}')"
TMP_LATEST="$(mktemp)"
node -e "
const fs = require('fs');
let existing = {};
try { existing = JSON.parse(\`$EXISTING_LATEST\`); } catch { existing = {}; }
const platforms = existing.platforms || {};
platforms['$PLATFORM_KEY'] = {
signature: \`$SIGNATURE\`,
url: '$GITEA_URL/$REPO_OWNER/$REPO_NAME/releases/download/$TAG/$FILENAME',
};
const manifest = {
version: '$VERSION',
notes: $(node -pe "JSON.stringify(process.argv[1])" "$NOTES"),
pub_date: new Date().toISOString(),
platforms,
};
fs.writeFileSync('$TMP_LATEST', JSON.stringify(manifest, null, 2));
"
# Remove any previous latest.json asset before re-uploading (Gitea won't
# overwrite an existing asset with the same name).
EXISTING_ASSET_ID="$(curl -s "${AUTH_HEADER[@]}" "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/$MANIFEST_RELEASE_ID" \
| node -pe "
const rel = JSON.parse(require('fs').readFileSync(0));
const a = (rel.assets || []).find(x => x.name === 'latest.json');
a ? a.id : ''
" 2>/dev/null || echo "")"
if [ -n "$EXISTING_ASSET_ID" ] && [ "$EXISTING_ASSET_ID" != "undefined" ]; then
curl -s "${AUTH_HEADER[@]}" -X DELETE \
"$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/$MANIFEST_RELEASE_ID/assets/$EXISTING_ASSET_ID" > /dev/null
fi
echo "==> Uploading latest.json to $MANIFEST_TAG"
curl -s "${AUTH_HEADER[@]}" -X POST \
"$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/$MANIFEST_RELEASE_ID/assets?name=latest.json" \
-F "attachment=@$TMP_LATEST" > /dev/null
rm -f "$TMP_LATEST"
echo "==> Done."
echo " Release: $GITEA_URL/$REPO_OWNER/$REPO_NAME/releases/tag/$TAG"
echo " Manifest URL: $GITEA_URL/$REPO_OWNER/$REPO_NAME/releases/download/$MANIFEST_TAG/latest.json"
+272
View File
@@ -117,6 +117,8 @@ dependencies = [
"tauri-plugin-dialog",
"tauri-plugin-log",
"tauri-plugin-notification",
"tauri-plugin-process",
"tauri-plugin-updater",
"thiserror 1.0.69",
"tokio",
"uuid",
@@ -134,6 +136,15 @@ dependencies = [
"security-framework",
]
[[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"
@@ -815,6 +826,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.119",
]
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -1155,6 +1177,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"
@@ -1757,6 +1789,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-tls"
version = "0.6.0"
@@ -2372,6 +2419,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[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"
@@ -2748,6 +2801,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.1",
"objc2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
@@ -2870,6 +2935,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.19",
]
[[package]]
name = "ouroboros"
version = "0.18.5"
@@ -3380,15 +3459,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",
@@ -3424,6 +3508,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 = "rusqlite"
version = "0.31.0"
@@ -3466,6 +3564,32 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
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.1"
@@ -3475,6 +3599,44 @@ 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.23"
@@ -4103,6 +4265,17 @@ dependencies = [
"syn 2.0.119",
]
[[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"
@@ -4320,6 +4493,49 @@ dependencies = [
"url",
]
[[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 0.13.4",
"rustls",
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.19",
"time",
"tokio",
"url",
"windows-sys 0.60.2",
"zip",
]
[[package]]
name = "tauri-runtime"
version = "2.11.3"
@@ -4588,6 +4804,16 @@ dependencies = [
"tokio",
]
[[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.19"
@@ -4901,6 +5127,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"
@@ -5166,6 +5398,15 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webview2-com"
version = "0.38.2"
@@ -5409,6 +5650,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"
@@ -5751,6 +6001,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 = "yansi"
version = "1.0.1"
@@ -5932,6 +6192,18 @@ dependencies = [
"syn 2.0.119",
]
[[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.23"
+2
View File
@@ -25,6 +25,8 @@ tauri = { version = "2.11.3", features = [] }
tauri-plugin-log = "2"
tauri-plugin-dialog = "2"
tauri-plugin-notification = "2"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
tokio = { version = "1", features = ["full"] }
imap = "3.0.0-alpha.15"
native-tls = "0.2"
+3 -1
View File
@@ -12,6 +12,8 @@
"notification:default",
"core:webview:allow-create-webview-window",
"core:window:allow-close",
"core:window:allow-set-focus"
"core:window:allow-set-focus",
"updater:default",
"process:default"
]
}
+2
View File
@@ -27,6 +27,8 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
+9
View File
@@ -28,6 +28,7 @@
"bundle": {
"active": true,
"targets": "all",
"createUpdaterArtifacts": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
@@ -38,5 +39,13 @@
"android": {
"debugApplicationIdSuffix": ".debug"
}
},
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDZCQTc0QThDNzI0RURGODAKUldTQTMwNXlqRXFuYXdsbm5nMUhBMVpzUVRxdTlzcDUvS3g1SGdHSjFPNkh3cW1rSUN5MVlyMkQK",
"endpoints": [
"https://git.kanjiv.at/KanjiG/mail-client/releases/download/latest-manifest/latest.json"
]
}
}
}
+26
View File
@@ -45,6 +45,7 @@ import { api } from './lib/api'
import { parseAddressList } from './lib/addresses'
import { escapeHtml } from './lib/richtext'
import { loadAppSettings, saveAppSettings, type AppSettings } from './lib/settings'
import { checkForUpdate, installUpdate } from './lib/updater'
import {
applyTheme,
loadConversationsEnabled,
@@ -253,6 +254,11 @@ export default function App() {
setTimeout(() => setToast(null), 3500)
}, [])
useEffect(() => {
handleCheckForUpdates(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
applyTheme(theme)
if (theme !== 'system') return
@@ -443,6 +449,25 @@ export default function App() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [view, selectedMessage])
async function handleCheckForUpdates(silent = false) {
const update = await checkForUpdate()
if (!update) {
if (!silent) showToast('Du hast schon die neueste Version')
return
}
if (!window.confirm(`Version ${update.version} ist verfügbar (aktuell: ${update.currentVersion}).\n\n${update.body ?? ''}\n\nJetzt installieren? Die App startet danach neu.`)) {
return
}
showToast('Update wird heruntergeladen…')
try {
await installUpdate(update, (pct) => {
if (pct !== null) showToast(`Update wird installiert… ${pct}%`)
})
} catch (e) {
showToast(`Update fehlgeschlagen: ${e}`)
}
}
function handleRefresh() {
if (offlineMode) {
showToast('Offline-Modus aktiv')
@@ -1818,6 +1843,7 @@ export default function App() {
onSyncGoogleCalendar={handleSyncGoogleCalendar}
onReconnectGoogleAccount={handleReconnectGoogleAccount}
onOpenCaldav={() => setCaldavModalOpen(true)}
onCheckForUpdates={handleCheckForUpdates}
theme={theme}
onThemeChange={handleThemeChange}
onOpenSettings={() => setSettingsModalOpen(true)}
+3
View File
@@ -273,6 +273,7 @@ export function Ribbon({
onSyncGoogleCalendar,
onReconnectGoogleAccount,
onOpenCaldav,
onCheckForUpdates,
theme,
onThemeChange,
onOpenSettings,
@@ -337,6 +338,7 @@ export function Ribbon({
onSyncGoogleCalendar?: (accountId: string) => Promise<void>
onReconnectGoogleAccount?: (accountId: string) => Promise<void>
onOpenCaldav?: () => void
onCheckForUpdates: () => void
theme: Theme
onThemeChange: (t: Theme) => void
onOpenSettings: () => void
@@ -556,6 +558,7 @@ export function Ribbon({
<ClassicRibbonGroup caption="Support">
<HelpMenu onShowShortcuts={onShowShortcuts} />
<AboutMenu />
<ClassicButton icon={RefreshCw} label="Nach Updates suchen" onClick={onCheckForUpdates} />
</ClassicRibbonGroup>
)}
</ClassicRibbonBody>
+27
View File
@@ -0,0 +1,27 @@
import { check, type Update } from '@tauri-apps/plugin-updater'
import { relaunch } from '@tauri-apps/plugin-process'
export async function checkForUpdate(): Promise<Update | null> {
try {
return await check()
} catch {
return null
}
}
export async function installUpdate(update: Update, onProgress: (pct: number | null) => void): Promise<void> {
let total = 0
let downloaded = 0
await update.downloadAndInstall((event) => {
if (event.event === 'Started') {
total = event.data.contentLength ?? 0
onProgress(total > 0 ? 0 : null)
} else if (event.event === 'Progress') {
downloaded += event.data.chunkLength
onProgress(total > 0 ? Math.min(100, Math.round((downloaded / total) * 100)) : null)
} else if (event.event === 'Finished') {
onProgress(100)
}
})
await relaunch()
}