mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-05-07 06:13:22 +00:00
Upgrade to Tauri v2 (#693)
Some checks are pending
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Some checks are pending
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
parent
c3276df727
commit
6ee5a1945b
39
.github/workflows/build-and-release.yml
vendored
39
.github/workflows/build-and-release.yml
vendored
@ -704,30 +704,31 @@ jobs:
|
|||||||
if: matrix.platform == 'ubuntu-22.04' && contains(matrix.rust_target, 'x86_64')
|
if: matrix.platform == 'ubuntu-22.04' && contains(matrix.rust_target, 'x86_64')
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libfuse2
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2
|
||||||
|
|
||||||
- name: Setup dependencies (Ubuntu-specific, ARM)
|
- name: Setup dependencies (Ubuntu-specific, ARM)
|
||||||
if: matrix.platform == 'ubuntu-22.04-arm' && contains(matrix.rust_target, 'aarch64')
|
if: matrix.platform == 'ubuntu-22.04-arm' && contains(matrix.rust_target, 'aarch64')
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libfuse2
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2
|
||||||
|
|
||||||
- name: Setup Tauri (Unix)
|
- name: Setup Tauri (Unix)
|
||||||
if: matrix.platform != 'windows-latest'
|
if: matrix.platform != 'windows-latest'
|
||||||
run: |
|
run: |
|
||||||
if ! cargo tauri --version > /dev/null 2>&1; then
|
if ! cargo tauri --version 2>/dev/null | grep -Eq '^tauri-cli 2\.'; then
|
||||||
cargo install --version 1.6.2 tauri-cli
|
cargo install tauri-cli --version "^2.0.0" --locked --force
|
||||||
else
|
else
|
||||||
echo "Tauri is already installed"
|
echo "Tauri CLI v2 is already installed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Tauri (Windows)
|
- name: Setup Tauri (Windows)
|
||||||
if: matrix.platform == 'windows-latest'
|
if: matrix.platform == 'windows-latest'
|
||||||
run: |
|
run: |
|
||||||
if (-not (cargo tauri --version 2>$null)) {
|
$tauriVersion = cargo tauri --version 2>$null
|
||||||
cargo install --version 1.6.2 tauri-cli
|
if (-not $tauriVersion -or $tauriVersion -notmatch '^tauri-cli 2\.') {
|
||||||
|
cargo install tauri-cli --version "^2.0.0" --locked --force
|
||||||
} else {
|
} else {
|
||||||
Write-Output "Tauri is already installed"
|
Write-Output "Tauri CLI v2 is already installed"
|
||||||
}
|
}
|
||||||
|
|
||||||
- name: Delete previous artifact, which may exist due to caching (macOS)
|
- name: Delete previous artifact, which may exist due to caching (macOS)
|
||||||
@ -771,8 +772,8 @@ jobs:
|
|||||||
echo "Running PR test build without updater bundle signing"
|
echo "Running PR test build without updater bundle signing"
|
||||||
bundles="${{ matrix.tauri_bundle_pr }}"
|
bundles="${{ matrix.tauri_bundle_pr }}"
|
||||||
else
|
else
|
||||||
export TAURI_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY"
|
export TAURI_SIGNING_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY"
|
||||||
export TAURI_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD"
|
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd runtime
|
cd runtime
|
||||||
@ -790,8 +791,8 @@ jobs:
|
|||||||
Write-Output "Running PR test build without updater bundle signing"
|
Write-Output "Running PR test build without updater bundle signing"
|
||||||
$bundles = "${{ matrix.tauri_bundle_pr }}"
|
$bundles = "${{ matrix.tauri_bundle_pr }}"
|
||||||
} else {
|
} else {
|
||||||
$env:TAURI_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY"
|
$env:TAURI_SIGNING_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY"
|
||||||
$env:TAURI_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD"
|
$env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD"
|
||||||
}
|
}
|
||||||
|
|
||||||
cd runtime
|
cd runtime
|
||||||
@ -883,14 +884,14 @@ jobs:
|
|||||||
# Find and process files in the artifacts directory:
|
# Find and process files in the artifacts directory:
|
||||||
find "$GITHUB_WORKSPACE/artifacts" -type f | while read -r FILE; do
|
find "$GITHUB_WORKSPACE/artifacts" -type f | while read -r FILE; do
|
||||||
|
|
||||||
if [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz" ]]; then
|
if [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz.sig" ]]; then
|
||||||
TARGET_NAME="MindWork AI Studio_x64.app.tar.gz"
|
|
||||||
elif [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz.sig" ]]; then
|
|
||||||
TARGET_NAME="MindWork AI Studio_x64.app.tar.gz.sig"
|
TARGET_NAME="MindWork AI Studio_x64.app.tar.gz.sig"
|
||||||
elif [[ "$FILE" == *"osx-arm64"* && "$FILE" == *".tar.gz" ]]; then
|
elif [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz" ]]; then
|
||||||
TARGET_NAME="MindWork AI Studio_aarch64.app.tar.gz"
|
TARGET_NAME="MindWork AI Studio_x64.app.tar.gz"
|
||||||
elif [[ "$FILE" == *"osx-arm64"* && "$FILE" == *".tar.gz.sig" ]]; then
|
elif [[ "$FILE" == *"osx-arm64"* && "$FILE" == *".tar.gz.sig" ]]; then
|
||||||
TARGET_NAME="MindWork AI Studio_aarch64.app.tar.gz.sig"
|
TARGET_NAME="MindWork AI Studio_aarch64.app.tar.gz.sig"
|
||||||
|
elif [[ "$FILE" == *"osx-arm64"* && "$FILE" == *".tar.gz" ]]; then
|
||||||
|
TARGET_NAME="MindWork AI Studio_aarch64.app.tar.gz"
|
||||||
else
|
else
|
||||||
TARGET_NAME="$(basename "$FILE")"
|
TARGET_NAME="$(basename "$FILE")"
|
||||||
TARGET_NAME=$(echo "$TARGET_NAME" | sed "s/_${VERSION}//")
|
TARGET_NAME=$(echo "$TARGET_NAME" | sed "s/_${VERSION}//")
|
||||||
@ -941,9 +942,9 @@ jobs:
|
|||||||
platform="linux-x86_64"
|
platform="linux-x86_64"
|
||||||
elif [[ "$sig_file" == *"aarch64.AppImage"* ]]; then
|
elif [[ "$sig_file" == *"aarch64.AppImage"* ]]; then
|
||||||
platform="linux-aarch64"
|
platform="linux-aarch64"
|
||||||
elif [[ "$sig_file" == *"x64-setup.nsis"* ]]; then
|
elif [[ "$sig_file" == *"x64-setup"* ]]; then
|
||||||
platform="windows-x86_64"
|
platform="windows-x86_64"
|
||||||
elif [[ "$sig_file" == *"arm64-setup.nsis"* ]]; then
|
elif [[ "$sig_file" == *"arm64-setup"* ]]; then
|
||||||
platform="windows-aarch64"
|
platform="windows-aarch64"
|
||||||
else
|
else
|
||||||
echo "Platform not recognized: '$sig_file'"
|
echo "Platform not recognized: '$sig_file'"
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -169,3 +169,6 @@ orleans.codegen.cs
|
|||||||
|
|
||||||
# Ignore GitHub Copilot migration files:
|
# Ignore GitHub Copilot migration files:
|
||||||
**/copilot.data.migration.*.xml
|
**/copilot.data.migration.*.xml
|
||||||
|
|
||||||
|
# Tauri generated schemas/manifests
|
||||||
|
/runtime/gen/
|
||||||
|
|||||||
@ -245,7 +245,7 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
Console.WriteLine("- Start building the Rust runtime ...");
|
Console.WriteLine("- Start building the Rust runtime ...");
|
||||||
|
|
||||||
var pathRuntime = Environment.GetRustRuntimeDirectory();
|
var pathRuntime = Environment.GetRustRuntimeDirectory();
|
||||||
var rustBuildOutput = await this.ReadCommandOutput(pathRuntime, "cargo", "tauri build --bundles none", true);
|
var rustBuildOutput = await this.ReadCommandOutput(pathRuntime, "cargo", "tauri build --no-bundle", true);
|
||||||
var rustBuildOutputLines = rustBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
|
var rustBuildOutputLines = rustBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
|
||||||
var foundRustIssue = false;
|
var foundRustIssue = false;
|
||||||
foreach (var buildOutputLine in rustBuildOutputLines)
|
foreach (var buildOutputLine in rustBuildOutputLines)
|
||||||
|
|||||||
@ -13,6 +13,7 @@ public partial class Changelog
|
|||||||
|
|
||||||
public static readonly Log[] LOGS =
|
public static readonly Log[] LOGS =
|
||||||
[
|
[
|
||||||
|
new (237, "v26.5.2, build 237 (2026-05-06 16:38 UTC)", "v26.5.2.md"),
|
||||||
new (236, "v26.5.1, build 236 (2026-05-06 13:06 UTC)", "v26.5.1.md"),
|
new (236, "v26.5.1, build 236 (2026-05-06 13:06 UTC)", "v26.5.1.md"),
|
||||||
new (235, "v26.4.1, build 235 (2026-04-17 17:25 UTC)", "v26.4.1.md"),
|
new (235, "v26.4.1, build 235 (2026-04-17 17:25 UTC)", "v26.4.1.md"),
|
||||||
new (234, "v26.2.2, build 234 (2026-02-22 14:16 UTC)", "v26.2.2.md"),
|
new (234, "v26.2.2, build 234 (2026-02-22 14:16 UTC)", "v26.2.2.md"),
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
# v26.5.2, build 237 (2026-05-xx xx:xx UTC)
|
# v26.5.2, build 237 (2026-05-06 16:38 UTC)
|
||||||
|
- Updated the underlying Tauri framework from version 1 to the latest version 2. Please do not install this prerelease manually. Production versions such as v26.4.1 will ignore this update. We are using this prerelease to test the clean update path for the migration from the Tauri v1 framework to the Tauri v2 framework. After a successful test, this prerelease will be removed.
|
||||||
|
|||||||
1
app/MindWork AI Studio/wwwroot/changelog/v26.5.3.md
Normal file
1
app/MindWork AI Studio/wwwroot/changelog/v26.5.3.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# v26.5.3, build 238 (2026-05-xx xx:xx UTC)
|
||||||
12
metadata.txt
12
metadata.txt
@ -1,12 +1,12 @@
|
|||||||
26.5.1
|
26.5.2
|
||||||
2026-05-06 13:06:02 UTC
|
2026-05-06 16:38:01 UTC
|
||||||
236
|
237
|
||||||
9.0.116 (commit fb4af7e1b3)
|
9.0.116 (commit fb4af7e1b3)
|
||||||
9.0.15 (commit 4250c8399a)
|
9.0.15 (commit 4250c8399a)
|
||||||
1.93.1 (commit 01f6ddf75)
|
1.95.0 (commit 59807616e)
|
||||||
8.15.0
|
8.15.0
|
||||||
1.8.3
|
2.11.1
|
||||||
ece329140e4, release
|
bcf15e91881, release
|
||||||
osx-arm64
|
osx-arm64
|
||||||
144.0.7543.0
|
144.0.7543.0
|
||||||
1.17.1
|
1.17.1
|
||||||
3043
runtime/Cargo.lock
generated
3043
runtime/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,16 +1,19 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mindwork-ai-studio"
|
name = "mindwork-ai-studio"
|
||||||
version = "26.5.1"
|
version = "26.5.2"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
description = "MindWork AI Studio"
|
description = "MindWork AI Studio"
|
||||||
authors = ["Thorsten Sommer"]
|
authors = ["Thorsten Sommer"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "1.5.6", features = [] }
|
tauri-build = { version = "2.6.1", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "1.8.3", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog", "global-shortcut"] }
|
tauri = { version = "2.11.1", features = [] }
|
||||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
tauri-plugin-window-state = { version = "2.4.1" }
|
||||||
|
tauri-plugin-shell = "2.3.5"
|
||||||
|
tauri-plugin-dialog = "2.7.1"
|
||||||
|
tauri-plugin-opener = "2.5.4"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
keyring = { version = "3.6.2", features = ["apple-native", "windows-native", "sync-secret-service"] }
|
keyring = { version = "3.6.2", features = ["apple-native", "windows-native", "sync-secret-service"] }
|
||||||
@ -32,7 +35,7 @@ pbkdf2 = "0.12.2"
|
|||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
rcgen = { version = "0.14.7", features = ["pem"] }
|
rcgen = { version = "0.14.7", features = ["pem"] }
|
||||||
file-format = "0.28.0"
|
file-format = "0.29.0"
|
||||||
calamine = "0.34.0"
|
calamine = "0.34.0"
|
||||||
pdfium-render = "0.8.37"
|
pdfium-render = "0.8.37"
|
||||||
sys-locale = "0.3.2"
|
sys-locale = "0.3.2"
|
||||||
@ -45,17 +48,17 @@ sysinfo = "0.38.4"
|
|||||||
# Fixes security vulnerability downstream, where the upstream is not fixed yet:
|
# Fixes security vulnerability downstream, where the upstream is not fixed yet:
|
||||||
time = "0.3.47" # -> Rocket
|
time = "0.3.47" # -> Rocket
|
||||||
bytes = "1.11.1" # -> almost every dependency
|
bytes = "1.11.1" # -> almost every dependency
|
||||||
tar = "0.4.45" # -> Tauri v1
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
# See issue https://github.com/tauri-apps/tauri/issues/4470
|
# See issue https://github.com/tauri-apps/tauri/issues/4470
|
||||||
reqwest = { version = "0.13.2", features = ["native-tls-vendored"] }
|
reqwest = { version = "0.13.2", features = ["native-tls-vendored"] }
|
||||||
|
|
||||||
# Fixes security vulnerability downstream, where the upstream is not fixed yet:
|
|
||||||
openssl = "0.10.76" # -> reqwest, Tauri v1
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
windows-registry = "0.6.1"
|
windows-registry = "0.6.1"
|
||||||
|
|
||||||
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
|
tauri-plugin-global-shortcut = "2"
|
||||||
|
tauri-plugin-updater = "2.10.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
|||||||
@ -53,6 +53,18 @@ fn update_cargo_toml(cargo_path: &str, version: &str) {
|
|||||||
let cargo_toml_lines = cargo_toml.lines();
|
let cargo_toml_lines = cargo_toml.lines();
|
||||||
let mut new_cargo_toml = String::new();
|
let mut new_cargo_toml = String::new();
|
||||||
|
|
||||||
|
// Return early when the version already matches to avoid unnecessary rewrites.
|
||||||
|
let current_version = cargo_toml.lines().find_map(|line| {
|
||||||
|
let trimmed = line.trim_start();
|
||||||
|
let rest = trimmed.strip_prefix("\"version\": ")?;
|
||||||
|
let quoted = rest.strip_prefix('"')?;
|
||||||
|
let end_idx = quoted.find('"')?;
|
||||||
|
Some("ed[..end_idx])
|
||||||
|
});
|
||||||
|
if current_version == Some(version) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for line in cargo_toml_lines {
|
for line in cargo_toml_lines {
|
||||||
if line.starts_with("version = ") {
|
if line.starts_with("version = ") {
|
||||||
new_cargo_toml.push_str(&format!("version = \"{version}\""));
|
new_cargo_toml.push_str(&format!("version = \"{version}\""));
|
||||||
@ -67,6 +79,19 @@ fn update_cargo_toml(cargo_path: &str, version: &str) {
|
|||||||
|
|
||||||
fn update_tauri_conf(tauri_conf_path: &str, version: &str) {
|
fn update_tauri_conf(tauri_conf_path: &str, version: &str) {
|
||||||
let tauri_conf = std::fs::read_to_string(tauri_conf_path).unwrap();
|
let tauri_conf = std::fs::read_to_string(tauri_conf_path).unwrap();
|
||||||
|
|
||||||
|
// Return early when the version already matches to avoid unnecessary rewrites.
|
||||||
|
let current_version = tauri_conf.lines().find_map(|line| {
|
||||||
|
let trimmed = line.trim_start();
|
||||||
|
let rest = trimmed.strip_prefix("\"version\": ")?;
|
||||||
|
let quoted = rest.strip_prefix('"')?;
|
||||||
|
let end_idx = quoted.find('"')?;
|
||||||
|
Some("ed[..end_idx])
|
||||||
|
});
|
||||||
|
if current_version == Some(version) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let tauri_conf_lines = tauri_conf.lines();
|
let tauri_conf_lines = tauri_conf.lines();
|
||||||
let mut new_tauri_conf = String::new();
|
let mut new_tauri_conf = String::new();
|
||||||
|
|
||||||
@ -75,7 +100,7 @@ fn update_tauri_conf(tauri_conf_path: &str, version: &str) {
|
|||||||
// "version": "0.1.0-alpha.0"
|
// "version": "0.1.0-alpha.0"
|
||||||
// Please notice, that the version number line might have a leading tab, etc.
|
// Please notice, that the version number line might have a leading tab, etc.
|
||||||
if line.contains("\"version\": ") {
|
if line.contains("\"version\": ") {
|
||||||
new_tauri_conf.push_str(&format!("\t\"version\": \"{version}\""));
|
new_tauri_conf.push_str(&format!(" \"version\": \"{version}\","));
|
||||||
} else {
|
} else {
|
||||||
new_tauri_conf.push_str(line);
|
new_tauri_conf.push_str(line);
|
||||||
}
|
}
|
||||||
|
|||||||
34
runtime/capabilities/default.json
Normal file
34
runtime/capabilities/default.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default capability for MindWork AI Studio",
|
||||||
|
"remote": {
|
||||||
|
"urls": [
|
||||||
|
"http://localhost:*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"updater:default",
|
||||||
|
"opener:default",
|
||||||
|
"shell:allow-open",
|
||||||
|
{
|
||||||
|
"identifier": "shell:allow-spawn",
|
||||||
|
"allow": [
|
||||||
|
{
|
||||||
|
"name": "mindworkAIStudioServer",
|
||||||
|
"sidecar": true,
|
||||||
|
"args": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "qdrant",
|
||||||
|
"sidecar": true,
|
||||||
|
"args": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -9,9 +9,12 @@ use rocket::serde::json::Json;
|
|||||||
use rocket::serde::Serialize;
|
use rocket::serde::Serialize;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use strum_macros::Display;
|
use strum_macros::Display;
|
||||||
use tauri::updater::UpdateResponse;
|
use tauri::{DragDropEvent,RunEvent, Manager, WindowEvent, generate_context};
|
||||||
use tauri::{FileDropEvent, GlobalShortcutManager, UpdaterEvent, RunEvent, Manager, PathResolver, Window, WindowEvent, generate_context};
|
use tauri::path::PathResolver;
|
||||||
use tauri::api::dialog::blocking::FileDialogBuilder;
|
use tauri::WebviewWindow;
|
||||||
|
use tauri_plugin_updater::{UpdaterExt, Update};
|
||||||
|
use tauri_plugin_global_shortcut::GlobalShortcutExt;
|
||||||
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
use crate::api_token::APIToken;
|
use crate::api_token::APIToken;
|
||||||
@ -24,10 +27,10 @@ use crate::qdrant::{cleanup_qdrant, start_qdrant_server, stop_qdrant_server};
|
|||||||
use crate::dotnet::create_startup_env_file;
|
use crate::dotnet::create_startup_env_file;
|
||||||
|
|
||||||
/// The Tauri main window.
|
/// The Tauri main window.
|
||||||
static MAIN_WINDOW: Lazy<Mutex<Option<Window>>> = Lazy::new(|| Mutex::new(None));
|
pub static MAIN_WINDOW: Lazy<Mutex<Option<WebviewWindow>>> = Lazy::new(|| Mutex::new(None));
|
||||||
|
|
||||||
/// The update response coming from the Tauri updater.
|
/// The update response coming from the Tauri updater.
|
||||||
static CHECK_UPDATE_RESPONSE: Lazy<Mutex<Option<UpdateResponse<tauri::Wry>>>> = Lazy::new(|| Mutex::new(None));
|
static CHECK_UPDATE_RESPONSE: Lazy<Mutex<Option<Update>>> = Lazy::new(|| Mutex::new(None));
|
||||||
|
|
||||||
/// The event broadcast sender for Tauri events.
|
/// The event broadcast sender for Tauri events.
|
||||||
static EVENT_BROADCAST: Lazy<Mutex<Option<broadcast::Sender<Event>>>> = Lazy::new(|| Mutex::new(None));
|
static EVENT_BROADCAST: Lazy<Mutex<Option<broadcast::Sender<Event>>>> = Lazy::new(|| Mutex::new(None));
|
||||||
@ -35,6 +38,9 @@ static EVENT_BROADCAST: Lazy<Mutex<Option<broadcast::Sender<Event>>>> = Lazy::ne
|
|||||||
/// Stores the currently registered global shortcuts (name -> shortcut string).
|
/// Stores the currently registered global shortcuts (name -> shortcut string).
|
||||||
static REGISTERED_SHORTCUTS: Lazy<Mutex<HashMap<Shortcut, String>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
static REGISTERED_SHORTCUTS: Lazy<Mutex<HashMap<Shortcut, String>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
|
/// Stores the localhost origin of the Blazor app after the .NET server is ready.
|
||||||
|
static APPROVED_APP_URL: Lazy<Mutex<Option<tauri::Url>>> = Lazy::new(|| Mutex::new(None));
|
||||||
|
|
||||||
/// Enum identifying global keyboard shortcuts.
|
/// Enum identifying global keyboard shortcuts.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display)]
|
||||||
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
|
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
|
||||||
@ -76,10 +82,34 @@ pub fn start_tauri() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let app = tauri::Builder::default()
|
let app = tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.plugin(
|
||||||
|
tauri::plugin::Builder::<tauri::Wry, ()>::new("external-link-handler")
|
||||||
|
.on_navigation(|webview, url| {
|
||||||
|
if !should_open_in_system_browser(webview, url) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
match webview.app_handle().opener().open_url(url.as_str(), None::<&str>) {
|
||||||
|
Ok(_) => {
|
||||||
|
info!(Source = "Tauri"; "Opening external URL in system browser: {url}");
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
error!(Source = "Tauri"; "Failed to open external URL '{url}' in system browser: {error}");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||||
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
|
|
||||||
// Get the main window:
|
// Get the main window:
|
||||||
let window = app.get_window("main").expect("Failed to get main window.");
|
let window = app.get_webview_window("main").expect("Failed to get main window.");
|
||||||
|
|
||||||
// Register a callback for window events, such as file drops. We have to use
|
// Register a callback for window events, such as file drops. We have to use
|
||||||
// this handler in addition to the app event handler, because file drop events
|
// this handler in addition to the app event handler, because file drop events
|
||||||
@ -100,27 +130,27 @@ pub fn start_tauri() {
|
|||||||
*MAIN_WINDOW.lock().unwrap() = Some(window);
|
*MAIN_WINDOW.lock().unwrap() = Some(window);
|
||||||
|
|
||||||
info!(Source = "Bootloader Tauri"; "Setup is running.");
|
info!(Source = "Bootloader Tauri"; "Setup is running.");
|
||||||
let data_path = app.path_resolver().app_local_data_dir().unwrap();
|
let data_path = app.path().app_local_data_dir().unwrap();
|
||||||
let data_path = data_path.join("data");
|
let data_path = data_path.join("data");
|
||||||
|
|
||||||
// Get and store the data and config directories:
|
// Get and store the data and config directories:
|
||||||
DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the data directory.")).unwrap();
|
DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the data directory.")).unwrap();
|
||||||
CONFIG_DIRECTORY.set(app.path_resolver().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap();
|
CONFIG_DIRECTORY.set(app.path().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap();
|
||||||
|
|
||||||
if is_dev() {
|
if is_dev() {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
create_startup_env_file();
|
create_startup_env_file();
|
||||||
} else {
|
} else {
|
||||||
cleanup_dotnet_server();
|
cleanup_dotnet_server();
|
||||||
start_dotnet_server();
|
start_dotnet_server(app.handle().clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup_qdrant();
|
cleanup_qdrant();
|
||||||
start_qdrant_server(app.path_resolver());
|
start_qdrant_server(app.handle().clone());
|
||||||
|
|
||||||
info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {data_path:?}");
|
info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {data_path:?}");
|
||||||
switch_to_file_logging(data_path).map_err(|e| error!("Failed to switch logging to file: {e}")).unwrap();
|
switch_to_file_logging(data_path).map_err(|e| error!("Failed to switch logging to file: {e}")).unwrap();
|
||||||
set_pdfium_path(app.path_resolver());
|
set_pdfium_path(app.path());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@ -129,7 +159,7 @@ pub fn start_tauri() {
|
|||||||
.expect("Error while running Tauri application");
|
.expect("Error while running Tauri application");
|
||||||
|
|
||||||
// The app event handler:
|
// The app event handler:
|
||||||
app.run(|app_handle, event| {
|
app.run(|_app_handle, event| {
|
||||||
if !matches!(event, RunEvent::MainEventsCleared) {
|
if !matches!(event, RunEvent::MainEventsCleared) {
|
||||||
debug!(Source = "Tauri"; "Tauri event received: location=app event handler , event={event:?}");
|
debug!(Source = "Tauri"; "Tauri event received: location=app event handler , event={event:?}");
|
||||||
}
|
}
|
||||||
@ -149,54 +179,6 @@ pub fn start_tauri() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RunEvent::Updater(updater_event) => {
|
|
||||||
match updater_event {
|
|
||||||
UpdaterEvent::UpdateAvailable { body, date, version } => {
|
|
||||||
let body_len = body.len();
|
|
||||||
info!(Source = "Tauri"; "Updater: update available: body size={body_len} time={date:?} version={version}");
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdaterEvent::Pending => {
|
|
||||||
info!(Source = "Tauri"; "Updater: update is pending!");
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdaterEvent::DownloadProgress { chunk_length, content_length: _ } => {
|
|
||||||
trace!(Source = "Tauri"; "Updater: downloading chunk of {chunk_length} bytes");
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdaterEvent::Downloaded => {
|
|
||||||
info!(Source = "Tauri"; "Updater: update has been downloaded!");
|
|
||||||
warn!(Source = "Tauri"; "Try to stop the .NET server now...");
|
|
||||||
|
|
||||||
if is_prod() {
|
|
||||||
stop_dotnet_server();
|
|
||||||
stop_qdrant_server();
|
|
||||||
} else {
|
|
||||||
warn!(Source = "Tauri"; "Development environment detected; do not stop the .NET server.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdaterEvent::Updated => {
|
|
||||||
info!(Source = "Tauri"; "Updater: app has been updated");
|
|
||||||
warn!(Source = "Tauri"; "Try to restart the app now...");
|
|
||||||
|
|
||||||
if is_prod() {
|
|
||||||
app_handle.restart();
|
|
||||||
} else {
|
|
||||||
warn!(Source = "Tauri"; "Development environment detected; do not restart the app.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdaterEvent::AlreadyUpToDate => {
|
|
||||||
info!(Source = "Tauri"; "Updater: app is already up to date");
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdaterEvent::Error(error) => {
|
|
||||||
warn!(Source = "Tauri"; "Updater: failed to update: {error}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RunEvent::ExitRequested { .. } => {
|
RunEvent::ExitRequested { .. } => {
|
||||||
warn!(Source = "Tauri"; "Run event: exit was requested.");
|
warn!(Source = "Tauri"; "Run event: exit was requested.");
|
||||||
stop_qdrant_server();
|
stop_qdrant_server();
|
||||||
@ -217,6 +199,46 @@ pub fn start_tauri() {
|
|||||||
warn!(Source = "Tauri"; "Tauri app was stopped.");
|
warn!(Source = "Tauri"; "Tauri app was stopped.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_local_host(host: Option<&str>) -> bool {
|
||||||
|
matches!(host, Some("localhost") | Some("127.0.0.1") | Some("::1") | Some("[::1]"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_local_http_url(url: &tauri::Url) -> bool {
|
||||||
|
matches!(url.scheme(), "http" | "https") && is_local_host(url.host_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn same_origin(left: &tauri::Url, right: &tauri::Url) -> bool {
|
||||||
|
left.scheme() == right.scheme()
|
||||||
|
&& left.host_str() == right.host_str()
|
||||||
|
&& left.port_or_known_default() == right.port_or_known_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_open_in_system_browser<R: tauri::Runtime>(webview: &tauri::Webview<R>, url: &tauri::Url) -> bool {
|
||||||
|
match url.scheme() {
|
||||||
|
"mailto" | "tel" => return true,
|
||||||
|
"http" | "https" => {},
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(approved_app_url) = APPROVED_APP_URL.lock().unwrap().as_ref() {
|
||||||
|
if same_origin(approved_app_url, url) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_local_http_url(url) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(current_url) = webview.url() {
|
||||||
|
if same_origin(¤t_url, url) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!is_local_host(url.host_str())
|
||||||
|
}
|
||||||
|
|
||||||
/// Our event API endpoint for Tauri events. We try to send an endless stream of events to the client.
|
/// Our event API endpoint for Tauri events. We try to send an endless stream of events to the client.
|
||||||
/// If no events are available for a certain time, we send a ping event to keep the connection alive.
|
/// If no events are available for a certain time, we send a ping event to keep the connection alive.
|
||||||
/// When the client disconnects, the stream is closed. But we try to not lose events in between.
|
/// When the client disconnects, the stream is closed. But we try to not lose events in between.
|
||||||
@ -303,23 +325,21 @@ impl Event {
|
|||||||
/// Creates an Event instance from a Tauri WindowEvent.
|
/// Creates an Event instance from a Tauri WindowEvent.
|
||||||
pub fn from_window_event(window_event: &WindowEvent) -> Self {
|
pub fn from_window_event(window_event: &WindowEvent) -> Self {
|
||||||
match window_event {
|
match window_event {
|
||||||
WindowEvent::FileDrop(drop_event) => {
|
WindowEvent::DragDrop(drop_event) => {
|
||||||
match drop_event {
|
match drop_event {
|
||||||
FileDropEvent::Hovered(files) => Event::new(TauriEventType::FileDropHovered,
|
DragDropEvent::Enter { paths, .. } => Event::new(
|
||||||
files.iter().map(|f| f.to_string_lossy().to_string()).collect(),
|
TauriEventType::FileDropHovered,
|
||||||
|
paths.iter().map(|p| p.display().to_string()).collect(),
|
||||||
),
|
),
|
||||||
|
|
||||||
FileDropEvent::Dropped(files) => Event::new(TauriEventType::FileDropDropped,
|
DragDropEvent::Drop { paths, .. } => Event::new(
|
||||||
files.iter().map(|f| f.to_string_lossy().to_string()).collect(),
|
TauriEventType::FileDropDropped,
|
||||||
|
paths.iter().map(|p| p.display().to_string()).collect(),
|
||||||
),
|
),
|
||||||
|
|
||||||
FileDropEvent::Cancelled => Event::new(TauriEventType::FileDropCanceled,
|
DragDropEvent::Leave => Event::new(TauriEventType::FileDropCanceled, Vec::new()),
|
||||||
Vec::new(),
|
|
||||||
),
|
|
||||||
|
|
||||||
_ => Event::new(TauriEventType::Unknown,
|
_ => Event::new(TauriEventType::Unknown, Vec::new()),
|
||||||
Vec::new(),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -380,6 +400,12 @@ pub async fn change_location_to(url: &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Ok(parsed_url) = tauri::Url::parse(url) {
|
||||||
|
if is_local_http_url(&parsed_url) {
|
||||||
|
*APPROVED_APP_URL.lock().unwrap() = Some(parsed_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let js_location_change = format!("window.location = '{url}';");
|
let js_location_change = format!("window.location = '{url}';");
|
||||||
let main_window = main_window_spawn_clone.lock().unwrap();
|
let main_window = main_window_spawn_clone.lock().unwrap();
|
||||||
let location_change_result = main_window.as_ref().unwrap().eval(js_location_change.as_str());
|
let location_change_result = main_window.as_ref().unwrap().eval(js_location_change.as_str());
|
||||||
@ -402,46 +428,67 @@ pub async fn check_for_update(_token: APIToken) -> Json<CheckUpdateResponse> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let app_handle = MAIN_WINDOW.lock().unwrap().as_ref().unwrap().app_handle();
|
let app_handle = {
|
||||||
let response = app_handle.updater().check().await;
|
let main_window = MAIN_WINDOW.lock().unwrap();
|
||||||
|
match main_window.as_ref() {
|
||||||
|
Some(window) => window.app_handle().clone(),
|
||||||
|
None => {
|
||||||
|
error!(Source = "Updater"; "Cannot check updates: main window not available.");
|
||||||
|
return Json(CheckUpdateResponse {
|
||||||
|
update_is_available: false,
|
||||||
|
error: true,
|
||||||
|
new_version: String::from(""),
|
||||||
|
changelog: String::from(""),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let response = match app_handle.updater() {
|
||||||
|
Ok(updater) => updater.check().await,
|
||||||
|
Err(e) => {
|
||||||
|
warn!(Source = "Updater"; "Failed to get updater instance: {e}");
|
||||||
|
return Json(CheckUpdateResponse {
|
||||||
|
update_is_available: false,
|
||||||
|
error: true,
|
||||||
|
new_version: String::from(""),
|
||||||
|
changelog: String::from(""),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
Ok(update_response) => match update_response.is_update_available() {
|
Ok(Some(update)) => {
|
||||||
true => {
|
let body_len = update.body.as_ref().map_or(0, |body| body.len());
|
||||||
*CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update_response.clone());
|
let date = update.date;
|
||||||
let new_version = update_response.latest_version();
|
let new_version = update.version.clone();
|
||||||
info!(Source = "Updater"; "An update to version '{new_version}' is available.");
|
info!(Source = "Tauri"; "Updater: update available: body size={body_len} time={date:?} version={new_version}");
|
||||||
let changelog = update_response.body();
|
let changelog = update.body.clone().unwrap_or_default();
|
||||||
|
*CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update);
|
||||||
Json(CheckUpdateResponse {
|
Json(CheckUpdateResponse {
|
||||||
update_is_available: true,
|
update_is_available: true,
|
||||||
error: false,
|
error: false,
|
||||||
new_version: new_version.to_string(),
|
new_version,
|
||||||
changelog: match changelog {
|
changelog,
|
||||||
Some(c) => c.to_string(),
|
|
||||||
None => String::from(""),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
|
Ok(None) => {
|
||||||
false => {
|
info!(Source = "Tauri"; "Updater: app is already up to date");
|
||||||
info!(Source = "Updater"; "No updates are available.");
|
|
||||||
Json(CheckUpdateResponse {
|
Json(CheckUpdateResponse {
|
||||||
update_is_available: false,
|
update_is_available: false,
|
||||||
error: false,
|
error: false,
|
||||||
new_version: String::from(""),
|
new_version: String::from(""),
|
||||||
changelog: String::from(""),
|
changelog: String::from(""),
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
},
|
|
||||||
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(Source = "Updater"; "Failed to check for updates: {e}.");
|
warn!(Source = "Tauri"; "Updater: failed to update: {e}");
|
||||||
Json(CheckUpdateResponse {
|
Json(CheckUpdateResponse {
|
||||||
update_is_available: false,
|
update_is_available: false,
|
||||||
error: true,
|
error: true,
|
||||||
new_version: String::from(""),
|
new_version: String::from(""),
|
||||||
changelog: String::from(""),
|
changelog: String::from(""),
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -463,9 +510,51 @@ pub async fn install_update(_token: APIToken) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let cloned_response_option = CHECK_UPDATE_RESPONSE.lock().unwrap().clone();
|
let cloned_response_option = CHECK_UPDATE_RESPONSE.lock().unwrap().clone();
|
||||||
|
let app_handle = MAIN_WINDOW
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.map(|window| window.app_handle().clone());
|
||||||
|
|
||||||
match cloned_response_option {
|
match cloned_response_option {
|
||||||
Some(update_response) => {
|
Some(update_response) => {
|
||||||
update_response.download_and_install().await.unwrap();
|
info!(Source = "Tauri"; "Updater: update is pending!");
|
||||||
|
let result = update_response.download_and_install(
|
||||||
|
|chunk_length, _content_length| {
|
||||||
|
trace!(Source = "Tauri"; "Updater: downloading chunk of {chunk_length} bytes");
|
||||||
|
},
|
||||||
|
|| {
|
||||||
|
info!(Source = "Tauri"; "Updater: update has been downloaded!");
|
||||||
|
warn!(Source = "Tauri"; "Try to stop the .NET server now...");
|
||||||
|
|
||||||
|
if is_prod() {
|
||||||
|
stop_dotnet_server();
|
||||||
|
stop_qdrant_server();
|
||||||
|
} else {
|
||||||
|
warn!(Source = "Tauri"; "Development environment detected; do not stop the .NET server.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => {
|
||||||
|
info!(Source = "Tauri"; "Updater: app has been updated");
|
||||||
|
warn!(Source = "Tauri"; "Try to restart the app now...");
|
||||||
|
|
||||||
|
if is_prod() {
|
||||||
|
if let Some(handle) = app_handle {
|
||||||
|
handle.restart();
|
||||||
|
} else {
|
||||||
|
warn!(Source = "Tauri"; "Cannot restart after update: main window not available.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!(Source = "Tauri"; "Development environment detected; do not restart the app.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!(Source = "Tauri"; "Updater: failed to update: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
None => {
|
None => {
|
||||||
@ -474,269 +563,6 @@ pub async fn install_update(_token: APIToken) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Let the user select a directory.
|
|
||||||
#[post("/select/directory?<title>", data = "<previous_directory>")]
|
|
||||||
pub fn select_directory(
|
|
||||||
_token: APIToken,
|
|
||||||
title: &str,
|
|
||||||
previous_directory: Option<Json<PreviousDirectory>>,
|
|
||||||
) -> Json<DirectorySelectionResponse> {
|
|
||||||
let folder_path = match previous_directory {
|
|
||||||
Some(previous) => {
|
|
||||||
let previous_path = previous.path.as_str();
|
|
||||||
create_file_dialog()
|
|
||||||
.set_title(title)
|
|
||||||
.set_directory(previous_path)
|
|
||||||
.pick_folder()
|
|
||||||
},
|
|
||||||
|
|
||||||
None => create_file_dialog().set_title(title).pick_folder(),
|
|
||||||
};
|
|
||||||
|
|
||||||
match folder_path {
|
|
||||||
Some(path) => {
|
|
||||||
info!("User selected directory: {path:?}");
|
|
||||||
Json(DirectorySelectionResponse {
|
|
||||||
user_cancelled: false,
|
|
||||||
selected_directory: path.to_str().unwrap().to_string(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
None => {
|
|
||||||
info!("User cancelled directory selection.");
|
|
||||||
Json(DirectorySelectionResponse {
|
|
||||||
user_cancelled: true,
|
|
||||||
selected_directory: String::from(""),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
|
||||||
pub struct PreviousDirectory {
|
|
||||||
path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
|
||||||
pub struct FileTypeFilter {
|
|
||||||
filter_name: String,
|
|
||||||
filter_extensions: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
|
||||||
pub struct SelectFileOptions {
|
|
||||||
title: String,
|
|
||||||
previous_file: Option<PreviousFile>,
|
|
||||||
filter: Option<FileTypeFilter>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
|
||||||
pub struct SaveFileOptions {
|
|
||||||
title: String,
|
|
||||||
name_file: Option<PreviousFile>,
|
|
||||||
filter: Option<FileTypeFilter>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct DirectorySelectionResponse {
|
|
||||||
user_cancelled: bool,
|
|
||||||
selected_directory: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Let the user select a file.
|
|
||||||
#[post("/select/file", data = "<payload>")]
|
|
||||||
pub fn select_file(
|
|
||||||
_token: APIToken,
|
|
||||||
payload: Json<SelectFileOptions>,
|
|
||||||
) -> Json<FileSelectionResponse> {
|
|
||||||
// Create a new file dialog builder:
|
|
||||||
let file_dialog = create_file_dialog();
|
|
||||||
|
|
||||||
// Set the title of the file dialog:
|
|
||||||
let file_dialog = file_dialog.set_title(&payload.title);
|
|
||||||
|
|
||||||
// Set the file type filter if provided:
|
|
||||||
let file_dialog = apply_filter(file_dialog, &payload.filter);
|
|
||||||
|
|
||||||
// Set the previous file path if provided:
|
|
||||||
let file_dialog = match &payload.previous_file {
|
|
||||||
Some(previous) => {
|
|
||||||
let previous_path = previous.file_path.as_str();
|
|
||||||
file_dialog.set_directory(previous_path)
|
|
||||||
},
|
|
||||||
|
|
||||||
None => file_dialog,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show the file dialog and get the selected file path:
|
|
||||||
let file_path = file_dialog.pick_file();
|
|
||||||
match file_path {
|
|
||||||
Some(path) => {
|
|
||||||
info!("User selected file: {path:?}");
|
|
||||||
Json(FileSelectionResponse {
|
|
||||||
user_cancelled: false,
|
|
||||||
selected_file_path: path.to_str().unwrap().to_string(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
None => {
|
|
||||||
info!("User cancelled file selection.");
|
|
||||||
Json(FileSelectionResponse {
|
|
||||||
user_cancelled: true,
|
|
||||||
selected_file_path: String::from(""),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Let the user select some files.
|
|
||||||
#[post("/select/files", data = "<payload>")]
|
|
||||||
pub fn select_files(
|
|
||||||
_token: APIToken,
|
|
||||||
payload: Json<SelectFileOptions>,
|
|
||||||
) -> Json<FilesSelectionResponse> {
|
|
||||||
// Create a new file dialog builder:
|
|
||||||
let file_dialog = create_file_dialog();
|
|
||||||
|
|
||||||
// Set the title of the file dialog:
|
|
||||||
let file_dialog = file_dialog.set_title(&payload.title);
|
|
||||||
|
|
||||||
// Set the file type filter if provided:
|
|
||||||
let file_dialog = apply_filter(file_dialog, &payload.filter);
|
|
||||||
|
|
||||||
// Set the previous file path if provided:
|
|
||||||
let file_dialog = match &payload.previous_file {
|
|
||||||
Some(previous) => {
|
|
||||||
let previous_path = previous.file_path.as_str();
|
|
||||||
file_dialog.set_directory(previous_path)
|
|
||||||
},
|
|
||||||
|
|
||||||
None => file_dialog,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show the file dialog and get the selected file path:
|
|
||||||
let file_paths = file_dialog.pick_files();
|
|
||||||
match file_paths {
|
|
||||||
Some(paths) => {
|
|
||||||
info!("User selected {} files.", paths.len());
|
|
||||||
Json(FilesSelectionResponse {
|
|
||||||
user_cancelled: false,
|
|
||||||
selected_file_paths: paths
|
|
||||||
.iter()
|
|
||||||
.map(|p| p.to_str().unwrap().to_string())
|
|
||||||
.collect(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
None => {
|
|
||||||
info!("User cancelled file selection.");
|
|
||||||
Json(FilesSelectionResponse {
|
|
||||||
user_cancelled: true,
|
|
||||||
selected_file_paths: Vec::new(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/save/file", data = "<payload>")]
|
|
||||||
pub fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json<FileSaveResponse> {
|
|
||||||
// Create a new file dialog builder:
|
|
||||||
let file_dialog = create_file_dialog();
|
|
||||||
|
|
||||||
// Set the title of the file dialog:
|
|
||||||
let file_dialog = file_dialog.set_title(&payload.title);
|
|
||||||
|
|
||||||
// Set the file type filter if provided:
|
|
||||||
let file_dialog = apply_filter(file_dialog, &payload.filter);
|
|
||||||
|
|
||||||
// Set the previous file path if provided:
|
|
||||||
let file_dialog = match &payload.name_file {
|
|
||||||
Some(previous) => {
|
|
||||||
let previous_path = previous.file_path.as_str();
|
|
||||||
file_dialog.set_directory(previous_path)
|
|
||||||
},
|
|
||||||
|
|
||||||
None => file_dialog,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Displays the file dialogue box and select the file:
|
|
||||||
let file_path = file_dialog.save_file();
|
|
||||||
match file_path {
|
|
||||||
Some(path) => {
|
|
||||||
info!("User selected file for writing operation: {path:?}");
|
|
||||||
Json(FileSaveResponse {
|
|
||||||
user_cancelled: false,
|
|
||||||
save_file_path: path.to_str().unwrap().to_string(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
None => {
|
|
||||||
info!("User cancelled file selection.");
|
|
||||||
Json(FileSaveResponse {
|
|
||||||
user_cancelled: true,
|
|
||||||
save_file_path: String::from(""),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
|
||||||
pub struct PreviousFile {
|
|
||||||
file_path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a file dialog builder and assigns the main window as parent where supported.
|
|
||||||
fn create_file_dialog() -> FileDialogBuilder {
|
|
||||||
let file_dialog = FileDialogBuilder::new();
|
|
||||||
|
|
||||||
#[cfg(any(windows, target_os = "macos"))]
|
|
||||||
{
|
|
||||||
let main_window_lock = MAIN_WINDOW.lock().unwrap();
|
|
||||||
match main_window_lock.as_ref() {
|
|
||||||
Some(window) => file_dialog.set_parent(window),
|
|
||||||
None => {
|
|
||||||
warn!(Source = "Tauri"; "Cannot assign parent window to file dialog: main window not available.");
|
|
||||||
file_dialog
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(windows, target_os = "macos")))]
|
|
||||||
{
|
|
||||||
file_dialog
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Applies an optional file type filter to a FileDialogBuilder.
|
|
||||||
fn apply_filter(file_dialog: FileDialogBuilder, filter: &Option<FileTypeFilter>) -> FileDialogBuilder {
|
|
||||||
match filter {
|
|
||||||
Some(f) => file_dialog.add_filter(
|
|
||||||
&f.filter_name,
|
|
||||||
&f.filter_extensions.iter().map(|s| s.as_str()).collect::<Vec<&str>>(),
|
|
||||||
),
|
|
||||||
|
|
||||||
None => file_dialog,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct FileSelectionResponse {
|
|
||||||
user_cancelled: bool,
|
|
||||||
selected_file_path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct FilesSelectionResponse {
|
|
||||||
user_cancelled: bool,
|
|
||||||
selected_file_paths: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct FileSaveResponse {
|
|
||||||
user_cancelled: bool,
|
|
||||||
save_file_path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Request payload for registering a global shortcut.
|
/// Request payload for registering a global shortcut.
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
pub struct RegisterShortcutRequest {
|
pub struct RegisterShortcutRequest {
|
||||||
@ -765,37 +591,32 @@ pub struct AppExitResponse {
|
|||||||
/// Internal helper function to register a shortcut with its callback.
|
/// Internal helper function to register a shortcut with its callback.
|
||||||
/// This is used by both `register_shortcut` and `resume_shortcuts` to
|
/// This is used by both `register_shortcut` and `resume_shortcuts` to
|
||||||
/// avoid code duplication.
|
/// avoid code duplication.
|
||||||
fn register_shortcut_with_callback(
|
fn register_shortcut_with_callback<R: tauri::Runtime>(
|
||||||
shortcut_manager: &mut impl GlobalShortcutManager,
|
app_handle: &tauri::AppHandle<R>,
|
||||||
shortcut: &str,
|
shortcut: &str,
|
||||||
shortcut_id: Shortcut,
|
shortcut_id: Shortcut,
|
||||||
event_sender: broadcast::Sender<Event>,
|
event_sender: broadcast::Sender<Event>,
|
||||||
) -> Result<(), tauri::Error> {
|
) -> Result<(), tauri_plugin_global_shortcut::Error> {
|
||||||
//
|
let shortcut_manager = app_handle.global_shortcut();
|
||||||
// Match the shortcut registration to transform the Tauri result into the Rust result:
|
shortcut_manager.on_shortcut(shortcut, move |_app, _shortcut, _event| {
|
||||||
//
|
|
||||||
match shortcut_manager.register(shortcut, move || {
|
|
||||||
info!(Source = "Tauri"; "Global shortcut triggered for '{}'.", shortcut_id);
|
info!(Source = "Tauri"; "Global shortcut triggered for '{}'.", shortcut_id);
|
||||||
let event = Event::new(TauriEventType::GlobalShortcutPressed, vec![shortcut_id.to_string()]);
|
let event = Event::new(TauriEventType::GlobalShortcutPressed, vec![shortcut_id.to_string()]);
|
||||||
let sender = event_sender.clone();
|
let sender = event_sender.clone();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
match sender.send(event) {
|
if let Err(error) = sender.send(event) {
|
||||||
Ok(_) => {}
|
error!(Source = "Tauri"; "Failed to send global shortcut event: {error}");
|
||||||
Err(error) => error!(Source = "Tauri"; "Failed to send global shortcut event: {error}"),
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}) {
|
})
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Requests a controlled shutdown of the entire desktop application.
|
/// Requests a controlled shutdown of the entire desktop application.
|
||||||
#[post("/app/exit")]
|
#[post("/app/exit")]
|
||||||
pub fn exit_app(_token: APIToken) -> Json<AppExitResponse> {
|
pub fn exit_app(_token: APIToken) -> Json<AppExitResponse> {
|
||||||
|
let app_handle = {
|
||||||
let main_window_lock = MAIN_WINDOW.lock().unwrap();
|
let main_window_lock = MAIN_WINDOW.lock().unwrap();
|
||||||
let main_window = match main_window_lock.as_ref() {
|
match main_window_lock.as_ref() {
|
||||||
Some(window) => window,
|
Some(window) => window.app_handle().clone(),
|
||||||
None => {
|
None => {
|
||||||
error!(Source = "Tauri"; "Cannot exit app: main window not available.");
|
error!(Source = "Tauri"; "Cannot exit app: main window not available.");
|
||||||
return Json(AppExitResponse {
|
return Json(AppExitResponse {
|
||||||
@ -803,9 +624,9 @@ pub fn exit_app(_token: APIToken) -> Json<AppExitResponse> {
|
|||||||
error_message: "Main window not available".to_string(),
|
error_message: "Main window not available".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let app_handle = main_window.app_handle();
|
|
||||||
info!(Source = "Tauri"; "Controlled app exit was requested by the UI.");
|
info!(Source = "Tauri"; "Controlled app exit was requested by the UI.");
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
time::sleep(Duration::from_millis(50)).await;
|
time::sleep(Duration::from_millis(50)).await;
|
||||||
@ -848,7 +669,8 @@ pub fn register_shortcut(_token: APIToken, payload: Json<RegisterShortcutRequest
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut shortcut_manager = main_window.app_handle().global_shortcut_manager();
|
let app_handle = main_window.app_handle();
|
||||||
|
let shortcut_manager = app_handle.global_shortcut();
|
||||||
let mut registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap();
|
let mut registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap();
|
||||||
|
|
||||||
// Unregister the old shortcut if one exists for this name:
|
// Unregister the old shortcut if one exists for this name:
|
||||||
@ -887,7 +709,7 @@ pub fn register_shortcut(_token: APIToken, payload: Json<RegisterShortcutRequest
|
|||||||
drop(event_broadcast_lock);
|
drop(event_broadcast_lock);
|
||||||
|
|
||||||
// Register the new shortcut:
|
// Register the new shortcut:
|
||||||
match register_shortcut_with_callback(&mut shortcut_manager, &new_shortcut, id, event_sender) {
|
match register_shortcut_with_callback(app_handle, &new_shortcut, id, event_sender) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
info!(Source = "Tauri"; "Global shortcut '{new_shortcut}' registered successfully for '{}'.", id);
|
info!(Source = "Tauri"; "Global shortcut '{new_shortcut}' registered successfully for '{}'.", id);
|
||||||
registered_shortcuts.insert(id, new_shortcut);
|
registered_shortcuts.insert(id, new_shortcut);
|
||||||
@ -997,7 +819,8 @@ pub fn suspend_shortcuts(_token: APIToken) -> Json<ShortcutResponse> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut shortcut_manager = main_window.app_handle().global_shortcut_manager();
|
let app_handle = main_window.app_handle();
|
||||||
|
let shortcut_manager = app_handle.global_shortcut();
|
||||||
let registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap();
|
let registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap();
|
||||||
|
|
||||||
// Unregister all shortcuts from the OS (but keep them in our map):
|
// Unregister all shortcuts from the OS (but keep them in our map):
|
||||||
@ -1033,7 +856,7 @@ pub fn resume_shortcuts(_token: APIToken) -> Json<ShortcutResponse> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut shortcut_manager = main_window.app_handle().global_shortcut_manager();
|
let app_handle = main_window.app_handle();
|
||||||
let registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap();
|
let registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap();
|
||||||
|
|
||||||
// Get the event broadcast sender for the shortcut callbacks:
|
// Get the event broadcast sender for the shortcut callbacks:
|
||||||
@ -1058,7 +881,7 @@ pub fn resume_shortcuts(_token: APIToken) -> Json<ShortcutResponse> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
match register_shortcut_with_callback(&mut shortcut_manager, shortcut, *shortcut_id, event_sender.clone()) {
|
match register_shortcut_with_callback(&app_handle, shortcut, *shortcut_id, event_sender.clone()) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
info!(Source = "Tauri"; "Re-registered shortcut '{shortcut}' for '{}'.", shortcut_id);
|
info!(Source = "Tauri"; "Re-registered shortcut '{shortcut}' for '{}'.", shortcut_id);
|
||||||
success_count += 1;
|
success_count += 1;
|
||||||
@ -1119,15 +942,31 @@ fn validate_shortcut_syntax(shortcut: &str) -> bool {
|
|||||||
has_key
|
has_key
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_pdfium_path(path_resolver: PathResolver) {
|
fn set_pdfium_path<R: tauri::Runtime>(path_resolver: &PathResolver<R>) {
|
||||||
let pdfium_relative_source_path = String::from("resources/libraries/");
|
let resource_dir = match path_resolver.resource_dir() {
|
||||||
let pdfium_source_path = path_resolver.resolve_resource(pdfium_relative_source_path);
|
Ok(path) => path,
|
||||||
if pdfium_source_path.is_none() {
|
Err(error) => {
|
||||||
error!(Source = "Bootloader Tauri"; "Failed to set the PDFium library path.");
|
error!(Source = "Bootloader Tauri"; "Failed to resolve resource dir: {error}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let pdfium_source_path = pdfium_source_path.unwrap();
|
let candidate_paths = [
|
||||||
let pdfium_source_path = pdfium_source_path.to_str().unwrap().to_string();
|
resource_dir.join("resources").join("libraries"),
|
||||||
*PDFIUM_LIB_PATH.lock().unwrap() = Some(pdfium_source_path.clone());
|
resource_dir.join("libraries"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let pdfium_source_path = candidate_paths
|
||||||
|
.iter()
|
||||||
|
.find(|path| path.exists())
|
||||||
|
.map(|path| path.to_string_lossy().to_string());
|
||||||
|
|
||||||
|
match pdfium_source_path {
|
||||||
|
Some(path) => {
|
||||||
|
*PDFIUM_LIB_PATH.lock().unwrap() = Some(path);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
error!(Source = "Bootloader Tauri"; "Failed to set the PDFium library path.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,9 @@ use base64::prelude::BASE64_STANDARD;
|
|||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use rocket::get;
|
use rocket::get;
|
||||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
|
||||||
use tauri::Url;
|
use tauri::Url;
|
||||||
|
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||||
|
use tauri_plugin_shell::ShellExt;
|
||||||
use crate::api_token::APIToken;
|
use crate::api_token::APIToken;
|
||||||
use crate::runtime_api_token::API_TOKEN;
|
use crate::runtime_api_token::API_TOKEN;
|
||||||
use crate::app_window::change_location_to;
|
use crate::app_window::change_location_to;
|
||||||
@ -130,14 +131,14 @@ pub fn create_startup_env_file() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Starts the .NET server in a separate process.
|
/// Starts the .NET server in a separate process.
|
||||||
pub fn start_dotnet_server() {
|
pub fn start_dotnet_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>) {
|
||||||
|
|
||||||
// Get the secret password & salt and convert it to a base64 string:
|
// Get the secret password & salt and convert it to a base64 string:
|
||||||
let secret_password = BASE64_STANDARD.encode(ENCRYPTION.secret_password);
|
let secret_password = BASE64_STANDARD.encode(ENCRYPTION.secret_password);
|
||||||
let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt);
|
let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt);
|
||||||
let api_port = *API_SERVER_PORT;
|
let api_port = *API_SERVER_PORT;
|
||||||
|
|
||||||
let dotnet_server_environment = HashMap::from_iter([
|
let dotnet_server_environment: HashMap<String, String> = HashMap::from_iter([
|
||||||
(String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password),
|
(String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password),
|
||||||
(String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt),
|
(String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt),
|
||||||
(String::from("AI_STUDIO_CERTIFICATE_FINGERPRINT"), CERTIFICATE_FINGERPRINT.get().unwrap().to_string()),
|
(String::from("AI_STUDIO_CERTIFICATE_FINGERPRINT"), CERTIFICATE_FINGERPRINT.get().unwrap().to_string()),
|
||||||
@ -148,7 +149,9 @@ pub fn start_dotnet_server() {
|
|||||||
info!("Try to start the .NET server...");
|
info!("Try to start the .NET server...");
|
||||||
let server_spawn_clone = DOTNET_SERVER.clone();
|
let server_spawn_clone = DOTNET_SERVER.clone();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let (mut rx, child) = Command::new_sidecar("mindworkAIStudioServer")
|
let shell = app_handle.shell();
|
||||||
|
let (mut rx, child) = shell
|
||||||
|
.sidecar("mindworkAIStudioServer")
|
||||||
.expect("Failed to create sidecar")
|
.expect("Failed to create sidecar")
|
||||||
.envs(dotnet_server_environment)
|
.envs(dotnet_server_environment)
|
||||||
.spawn()
|
.spawn()
|
||||||
@ -163,12 +166,15 @@ pub fn start_dotnet_server() {
|
|||||||
// Log the output of the .NET server:
|
// Log the output of the .NET server:
|
||||||
// NOTE: Log events are sent via structured HTTP API calls.
|
// NOTE: Log events are sent via structured HTTP API calls.
|
||||||
// This loop serves for fundamental output (e.g., startup errors).
|
// This loop serves for fundamental output (e.g., startup errors).
|
||||||
while let Some(CommandEvent::Stdout(line)) = rx.recv().await {
|
while let Some(event) = rx.recv().await {
|
||||||
let line = sanitize_stdout_line(line.trim_end());
|
if let CommandEvent::Stdout(line) = event {
|
||||||
|
let line_utf8 = String::from_utf8_lossy(&line).to_string();
|
||||||
|
let line = sanitize_stdout_line(line_utf8.trim_end());
|
||||||
if !line.trim().is_empty() {
|
if !line.trim().is_empty() {
|
||||||
info!(Source = ".NET Server (stdout)"; "{line}");
|
info!(Source = ".NET Server (stdout)"; "{line}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
298
runtime/src/file_actions.rs
Normal file
298
runtime/src/file_actions.rs
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
use log::{error, info};
|
||||||
|
use rocket::post;
|
||||||
|
use rocket::serde::{Deserialize, Serialize};
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use tauri_plugin_dialog::{DialogExt, FileDialogBuilder};
|
||||||
|
use crate::api_token::APIToken;
|
||||||
|
use crate::app_window::MAIN_WINDOW;
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct PreviousDirectory {
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct FileTypeFilter {
|
||||||
|
filter_name: String,
|
||||||
|
filter_extensions: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct SelectFileOptions {
|
||||||
|
title: String,
|
||||||
|
previous_file: Option<PreviousFile>,
|
||||||
|
filter: Option<FileTypeFilter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct SaveFileOptions {
|
||||||
|
title: String,
|
||||||
|
name_file: Option<PreviousFile>,
|
||||||
|
filter: Option<FileTypeFilter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct DirectorySelectionResponse {
|
||||||
|
user_cancelled: bool,
|
||||||
|
selected_directory: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct FileSelectionResponse {
|
||||||
|
user_cancelled: bool,
|
||||||
|
selected_file_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct FilesSelectionResponse {
|
||||||
|
user_cancelled: bool,
|
||||||
|
selected_file_paths: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct FileSaveResponse {
|
||||||
|
user_cancelled: bool,
|
||||||
|
save_file_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct PreviousFile {
|
||||||
|
file_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Let the user select a directory.
|
||||||
|
#[post("/select/directory?<title>", data = "<previous_directory>")]
|
||||||
|
pub fn select_directory(
|
||||||
|
_token: APIToken,
|
||||||
|
title: &str,
|
||||||
|
previous_directory: Option<Json<PreviousDirectory>>,
|
||||||
|
) -> Json<DirectorySelectionResponse> {
|
||||||
|
let main_window_lock = MAIN_WINDOW.lock().unwrap();
|
||||||
|
let main_window = match main_window_lock.as_ref() {
|
||||||
|
Some(window) => window,
|
||||||
|
None => {
|
||||||
|
error!(Source = "Tauri"; "Cannot open directory dialog: main window not available.");
|
||||||
|
return Json(DirectorySelectionResponse {
|
||||||
|
user_cancelled: true,
|
||||||
|
selected_directory: String::from(""),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut dialog = main_window.dialog().file().set_parent(main_window).set_title(title);
|
||||||
|
if let Some(previous) = previous_directory {
|
||||||
|
dialog = dialog.set_directory(previous.path.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(main_window_lock);
|
||||||
|
|
||||||
|
let folder_path = dialog.blocking_pick_folder();
|
||||||
|
match folder_path {
|
||||||
|
Some(path) => {
|
||||||
|
match path.into_path() {
|
||||||
|
Ok(pb) => {
|
||||||
|
info!("User selected directory: {pb:?}");
|
||||||
|
Json(DirectorySelectionResponse {
|
||||||
|
user_cancelled: false,
|
||||||
|
selected_directory: pb.to_string_lossy().to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(Source = "Tauri"; "Failed to convert directory path: {e}");
|
||||||
|
Json(DirectorySelectionResponse {
|
||||||
|
user_cancelled: true,
|
||||||
|
selected_directory: String::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
None => {
|
||||||
|
info!("User cancelled directory selection.");
|
||||||
|
Json(DirectorySelectionResponse {
|
||||||
|
user_cancelled: true,
|
||||||
|
selected_directory: String::from(""),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Let the user select a file.
|
||||||
|
#[post("/select/file", data = "<payload>")]
|
||||||
|
pub fn select_file(
|
||||||
|
_token: APIToken,
|
||||||
|
payload: Json<SelectFileOptions>,
|
||||||
|
) -> Json<FileSelectionResponse> {
|
||||||
|
// Create a new file dialog builder:
|
||||||
|
let file_dialog = MAIN_WINDOW
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.map(|w| w.dialog().file().set_parent(w).set_title(&payload.title));
|
||||||
|
|
||||||
|
let Some(mut file_dialog) = file_dialog else {
|
||||||
|
error!(Source = "Tauri"; "Cannot open file dialog: main window not available.");
|
||||||
|
return Json(FileSelectionResponse {
|
||||||
|
user_cancelled: true,
|
||||||
|
selected_file_path: String::from(""),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set the file type filter if provided:
|
||||||
|
file_dialog = apply_filter(file_dialog, &payload.filter);
|
||||||
|
|
||||||
|
// Set the previous file path if provided:
|
||||||
|
if let Some(previous) = &payload.previous_file {
|
||||||
|
let previous_path = previous.file_path.as_str();
|
||||||
|
file_dialog = file_dialog.set_directory(previous_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the file dialog and get the selected file path:
|
||||||
|
let file_path = file_dialog.blocking_pick_file();
|
||||||
|
match file_path {
|
||||||
|
Some(path) => match path.into_path() {
|
||||||
|
Ok(pb) => {
|
||||||
|
info!("User selected file: {pb:?}");
|
||||||
|
Json(FileSelectionResponse {
|
||||||
|
user_cancelled: false,
|
||||||
|
selected_file_path: pb.to_string_lossy().to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(Source = "Tauri"; "Failed to convert file path: {e}");
|
||||||
|
Json(FileSelectionResponse {
|
||||||
|
user_cancelled: true,
|
||||||
|
selected_file_path: String::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
None => {
|
||||||
|
info!("User cancelled file selection.");
|
||||||
|
Json(FileSelectionResponse {
|
||||||
|
user_cancelled: true,
|
||||||
|
selected_file_path: String::from(""),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Let the user select some files.
|
||||||
|
#[post("/select/files", data = "<payload>")]
|
||||||
|
pub fn select_files(
|
||||||
|
_token: APIToken,
|
||||||
|
payload: Json<SelectFileOptions>,
|
||||||
|
) -> Json<FilesSelectionResponse> {
|
||||||
|
// Create a new file dialog builder:
|
||||||
|
let file_dialog = MAIN_WINDOW
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.map(|w| w.dialog().file().set_parent(w).set_title(&payload.title));
|
||||||
|
|
||||||
|
let Some(mut file_dialog) = file_dialog else {
|
||||||
|
error!(Source = "Tauri"; "Cannot open file dialog: main window not available.");
|
||||||
|
return Json(FilesSelectionResponse {
|
||||||
|
user_cancelled: true,
|
||||||
|
selected_file_paths: Vec::new(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set the file type filter if provided:
|
||||||
|
file_dialog = apply_filter(file_dialog, &payload.filter);
|
||||||
|
|
||||||
|
// Set the previous file path if provided:
|
||||||
|
if let Some(previous) = &payload.previous_file {
|
||||||
|
let previous_path = previous.file_path.as_str();
|
||||||
|
file_dialog = file_dialog.set_directory(previous_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the file dialog and get the selected file path:
|
||||||
|
let file_paths = file_dialog.blocking_pick_files();
|
||||||
|
match file_paths {
|
||||||
|
Some(paths) => {
|
||||||
|
let converted: Vec<String> = paths.into_iter().filter_map(|p| p.into_path().ok()).map(|pb| pb.to_string_lossy().to_string()).collect();
|
||||||
|
info!("User selected {} files.", converted.len());
|
||||||
|
Json(FilesSelectionResponse {
|
||||||
|
user_cancelled: false,
|
||||||
|
selected_file_paths: converted,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
info!("User cancelled file selection.");
|
||||||
|
Json(FilesSelectionResponse {
|
||||||
|
user_cancelled: true,
|
||||||
|
selected_file_paths: Vec::new(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/save/file", data = "<payload>")]
|
||||||
|
pub fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json<FileSaveResponse> {
|
||||||
|
// Create a new file dialog builder:
|
||||||
|
let file_dialog = MAIN_WINDOW
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.map(|w| w.dialog().file().set_parent(w).set_title(&payload.title));
|
||||||
|
|
||||||
|
let Some(mut file_dialog) = file_dialog else {
|
||||||
|
error!(Source = "Tauri"; "Cannot open save dialog: main window not available.");
|
||||||
|
return Json(FileSaveResponse {
|
||||||
|
user_cancelled: true,
|
||||||
|
save_file_path: String::from(""),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set the file type filter if provided:
|
||||||
|
file_dialog = apply_filter(file_dialog, &payload.filter);
|
||||||
|
|
||||||
|
// Set the previous file path if provided:
|
||||||
|
if let Some(previous) = &payload.name_file {
|
||||||
|
let previous_path = previous.file_path.as_str();
|
||||||
|
file_dialog = file_dialog.set_directory(previous_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Displays the file dialogue box and select the file:
|
||||||
|
let file_path = file_dialog.blocking_save_file();
|
||||||
|
match file_path {
|
||||||
|
Some(path) => match path.into_path() {
|
||||||
|
Ok(pb) => {
|
||||||
|
info!("User selected file for writing operation: {pb:?}");
|
||||||
|
Json(FileSaveResponse {
|
||||||
|
user_cancelled: false,
|
||||||
|
save_file_path: pb.to_string_lossy().to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(Source = "Tauri"; "Failed to convert save file path: {e}");
|
||||||
|
Json(FileSaveResponse {
|
||||||
|
user_cancelled: true,
|
||||||
|
save_file_path: String::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
None => {
|
||||||
|
info!("User cancelled file selection.");
|
||||||
|
Json(FileSaveResponse {
|
||||||
|
user_cancelled: true,
|
||||||
|
save_file_path: String::from(""),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies an optional file type filter to a FileDialogBuilder.
|
||||||
|
fn apply_filter<R: tauri::Runtime>(file_dialog: FileDialogBuilder<R>, filter: &Option<FileTypeFilter>) -> FileDialogBuilder<R> {
|
||||||
|
match filter {
|
||||||
|
Some(f) => file_dialog.add_filter(
|
||||||
|
&f.filter_name,
|
||||||
|
&f.filter_extensions.iter().map(|s| s.as_str()).collect::<Vec<&str>>(),
|
||||||
|
),
|
||||||
|
|
||||||
|
None => file_dialog,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,3 +18,4 @@ pub mod certificate_factory;
|
|||||||
pub mod runtime_api_token;
|
pub mod runtime_api_token;
|
||||||
pub mod stale_process_cleanup;
|
pub mod stale_process_cleanup;
|
||||||
mod sidecar_types;
|
mod sidecar_types;
|
||||||
|
mod file_actions;
|
||||||
@ -10,15 +10,17 @@ use once_cell::sync::Lazy;
|
|||||||
use rocket::get;
|
use rocket::get;
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use rocket::serde::Serialize;
|
use rocket::serde::Serialize;
|
||||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
|
||||||
use crate::api_token::{APIToken};
|
use crate::api_token::{APIToken};
|
||||||
use crate::environment::{is_dev, DATA_DIRECTORY};
|
use crate::environment::{is_dev, DATA_DIRECTORY};
|
||||||
use crate::certificate_factory::generate_certificate;
|
use crate::certificate_factory::generate_certificate;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tauri::PathResolver;
|
use tauri::Manager;
|
||||||
|
use tauri::path::BaseDirectory;
|
||||||
use tempfile::{TempDir, Builder};
|
use tempfile::{TempDir, Builder};
|
||||||
use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process};
|
use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process};
|
||||||
use crate::sidecar_types::SidecarType;
|
use crate::sidecar_types::SidecarType;
|
||||||
|
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||||
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
// Qdrant server process started in a separate process and can communicate
|
// Qdrant server process started in a separate process and can communicate
|
||||||
// via HTTP or gRPC with the .NET server and the runtime process
|
// via HTTP or gRPC with the .NET server and the runtime process
|
||||||
@ -98,7 +100,7 @@ pub fn qdrant_port(_token: APIToken) -> Json<ProvideQdrantInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Starts the Qdrant server in a separate process.
|
/// Starts the Qdrant server in a separate process.
|
||||||
pub fn start_qdrant_server(path_resolver: PathResolver){
|
pub fn start_qdrant_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>){
|
||||||
let path = qdrant_base_path();
|
let path = qdrant_base_path();
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
if let Err(e) = fs::create_dir_all(&path){
|
if let Err(e) = fs::create_dir_all(&path){
|
||||||
@ -121,7 +123,7 @@ pub fn start_qdrant_server(path_resolver: PathResolver){
|
|||||||
let snapshot_path = path.join("snapshots").to_string_lossy().to_string();
|
let snapshot_path = path.join("snapshots").to_string_lossy().to_string();
|
||||||
let init_path = path.join(".qdrant-initialized").to_string_lossy().to_string();
|
let init_path = path.join(".qdrant-initialized").to_string_lossy().to_string();
|
||||||
|
|
||||||
let qdrant_server_environment = HashMap::from_iter([
|
let qdrant_server_environment: HashMap<String, String> = HashMap::from_iter([
|
||||||
(String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()),
|
(String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()),
|
||||||
(String::from("QDRANT__SERVICE__GRPC_PORT"), QDRANT_SERVER_PORT_GRPC.to_string()),
|
(String::from("QDRANT__SERVICE__GRPC_PORT"), QDRANT_SERVER_PORT_GRPC.to_string()),
|
||||||
(String::from("QDRANT_INIT_FILE_PATH"), init_path),
|
(String::from("QDRANT_INIT_FILE_PATH"), init_path),
|
||||||
@ -135,9 +137,9 @@ pub fn start_qdrant_server(path_resolver: PathResolver){
|
|||||||
|
|
||||||
let server_spawn_clone = QDRANT_SERVER.clone();
|
let server_spawn_clone = QDRANT_SERVER.clone();
|
||||||
let qdrant_relative_source_path = "resources/databases/qdrant/config.yaml";
|
let qdrant_relative_source_path = "resources/databases/qdrant/config.yaml";
|
||||||
let qdrant_source_path = match path_resolver.resolve_resource(qdrant_relative_source_path) {
|
let qdrant_source_path = match app_handle.path().resolve(qdrant_relative_source_path, BaseDirectory::Resource) {
|
||||||
Some(path) => path,
|
Ok(path) => path,
|
||||||
None => {
|
Err(_) => {
|
||||||
let reason = format!("The Qdrant config resource '{qdrant_relative_source_path}' could not be resolved.");
|
let reason = format!("The Qdrant config resource '{qdrant_relative_source_path}' could not be resolved.");
|
||||||
error!(Source = "Qdrant"; "{reason} Starting the app without Qdrant.");
|
error!(Source = "Qdrant"; "{reason} Starting the app without Qdrant.");
|
||||||
set_qdrant_unavailable(reason);
|
set_qdrant_unavailable(reason);
|
||||||
@ -147,7 +149,9 @@ pub fn start_qdrant_server(path_resolver: PathResolver){
|
|||||||
|
|
||||||
let qdrant_source_path_display = qdrant_source_path.to_string_lossy().to_string();
|
let qdrant_source_path_display = qdrant_source_path.to_string_lossy().to_string();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let sidecar = match Command::new_sidecar("qdrant") {
|
let shell = app_handle.shell();
|
||||||
|
|
||||||
|
let sidecar = match shell.sidecar("qdrant") {
|
||||||
Ok(sidecar) => sidecar,
|
Ok(sidecar) => sidecar,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let reason = format!("Failed to create sidecar for Qdrant: {e}");
|
let reason = format!("Failed to create sidecar for Qdrant: {e}");
|
||||||
@ -183,7 +187,8 @@ pub fn start_qdrant_server(path_resolver: PathResolver){
|
|||||||
while let Some(event) = rx.recv().await {
|
while let Some(event) = rx.recv().await {
|
||||||
match event {
|
match event {
|
||||||
CommandEvent::Stdout(line) => {
|
CommandEvent::Stdout(line) => {
|
||||||
let line = line.trim_end();
|
let line_utf8 = String::from_utf8_lossy(&line).to_string();
|
||||||
|
let line = line_utf8.trim_end();
|
||||||
if line.contains("INFO") || line.contains("info") {
|
if line.contains("INFO") || line.contains("info") {
|
||||||
info!(Source = "Qdrant Server"; "{line}");
|
info!(Source = "Qdrant Server"; "{line}");
|
||||||
} else if line.contains("WARN") || line.contains("warning") {
|
} else if line.contains("WARN") || line.contains("warning") {
|
||||||
@ -196,7 +201,8 @@ pub fn start_qdrant_server(path_resolver: PathResolver){
|
|||||||
},
|
},
|
||||||
|
|
||||||
CommandEvent::Stderr(line) => {
|
CommandEvent::Stderr(line) => {
|
||||||
error!(Source = "Qdrant Server (stderr)"; "{line}");
|
let line_utf8 = String::from_utf8_lossy(&line).to_string();
|
||||||
|
error!(Source = "Qdrant Server (stderr)"; "{line_utf8}");
|
||||||
},
|
},
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@ -72,11 +72,11 @@ pub fn start_runtime_api() {
|
|||||||
crate::app_window::get_event_stream,
|
crate::app_window::get_event_stream,
|
||||||
crate::app_window::check_for_update,
|
crate::app_window::check_for_update,
|
||||||
crate::app_window::install_update,
|
crate::app_window::install_update,
|
||||||
crate::app_window::select_directory,
|
|
||||||
crate::app_window::select_file,
|
|
||||||
crate::app_window::select_files,
|
|
||||||
crate::app_window::save_file,
|
|
||||||
crate::app_window::exit_app,
|
crate::app_window::exit_app,
|
||||||
|
crate::file_actions::select_directory,
|
||||||
|
crate::file_actions::select_file,
|
||||||
|
crate::file_actions::select_files,
|
||||||
|
crate::file_actions::save_file,
|
||||||
crate::secret::get_secret,
|
crate::secret::get_secret,
|
||||||
crate::secret::store_secret,
|
crate::secret::store_secret,
|
||||||
crate::secret::delete_secret,
|
crate::secret::delete_secret,
|
||||||
|
|||||||
@ -1,44 +1,52 @@
|
|||||||
{
|
{
|
||||||
"build": {
|
|
||||||
"devPath": "ui/",
|
|
||||||
"distDir": "ui/",
|
|
||||||
"withGlobalTauri": false
|
|
||||||
},
|
|
||||||
"package": {
|
|
||||||
"productName": "MindWork AI Studio",
|
"productName": "MindWork AI Studio",
|
||||||
"version": "26.5.1"
|
"mainBinaryName": "MindWork AI Studio",
|
||||||
|
"version": "26.5.2",
|
||||||
|
"identifier": "com.github.mindwork-ai.ai-studio",
|
||||||
|
|
||||||
|
"build": {
|
||||||
|
"frontendDist": "ui/"
|
||||||
},
|
},
|
||||||
"tauri": {
|
|
||||||
"allowlist": {
|
"bundle": {
|
||||||
"all": false,
|
"active": true,
|
||||||
"shell": {
|
"targets": "all",
|
||||||
"sidecar": true,
|
"icon": [
|
||||||
"all": false,
|
"icons/32x32.png",
|
||||||
"open": true,
|
"icons/128x128.png",
|
||||||
"scope": [
|
"icons/128x128@2x.png",
|
||||||
{
|
"icons/icon.icns",
|
||||||
"name": "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer",
|
"icons/icon.ico"
|
||||||
"sidecar": true,
|
],
|
||||||
"args": true
|
"externalBin": [
|
||||||
|
"../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer",
|
||||||
|
"target/databases/qdrant/qdrant"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
"resources/databases/qdrant/config.yaml",
|
||||||
|
"resources/libraries/*"
|
||||||
|
],
|
||||||
|
"macOS": {
|
||||||
|
"exceptionDomain": "localhost"
|
||||||
},
|
},
|
||||||
{
|
"createUpdaterArtifacts": "v1Compatible"
|
||||||
"name": "target/databases/qdrant/qdrant",
|
|
||||||
"sidecar": true,
|
|
||||||
"args": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"http" : {
|
|
||||||
"all": true,
|
"plugins": {
|
||||||
"request": true,
|
"updater": {
|
||||||
"scope": [
|
"windows": {
|
||||||
"http://localhost"
|
"installMode": "passive"
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"fs": {
|
"endpoints": [
|
||||||
"scope": ["$RESOURCE/resources/*"]
|
"https://github.com/MindWorkAI/AI-Studio/releases/download/v26.5.3/latest.json"
|
||||||
|
],
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDM3MzE4MTM4RTNDMkM0NEQKUldSTnhNTGpPSUV4TjFkczFxRFJOZWgydzFQN1dmaFlKbXhJS1YyR1RKS1RnR09jYUpMaGsrWXYK"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"app": {
|
||||||
|
"withGlobalTauri": false,
|
||||||
|
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
@ -46,51 +54,13 @@
|
|||||||
"title": "MindWork AI Studio",
|
"title": "MindWork AI Studio",
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080,
|
"height": 1080,
|
||||||
"fileDropEnabled": true
|
"dragDropEnabled": true,
|
||||||
|
"useHttpsScheme": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null,
|
"csp": null
|
||||||
"dangerousRemoteDomainIpcAccess": [
|
|
||||||
{
|
|
||||||
"domain": "localhost",
|
|
||||||
"windows": ["main"],
|
|
||||||
"enableTauriAPI": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"bundle": {
|
|
||||||
"active": true,
|
|
||||||
"targets": "all",
|
|
||||||
"identifier": "com.github.mindwork-ai.ai-studio",
|
|
||||||
"externalBin": [
|
|
||||||
"../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer",
|
|
||||||
"target/databases/qdrant/qdrant"
|
|
||||||
],
|
|
||||||
"resources": [
|
|
||||||
"resources/**"
|
|
||||||
],
|
|
||||||
"macOS": {
|
|
||||||
"exceptionDomain": "localhost"
|
|
||||||
},
|
|
||||||
"icon": [
|
|
||||||
"icons/32x32.png",
|
|
||||||
"icons/128x128.png",
|
|
||||||
"icons/128x128@2x.png",
|
|
||||||
"icons/icon.icns",
|
|
||||||
"icons/icon.ico"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"updater": {
|
|
||||||
"active": true,
|
|
||||||
"endpoints": [
|
|
||||||
"https://github.com/MindWorkAI/AI-Studio/releases/download/v26.5.2/latest.json"
|
|
||||||
],
|
|
||||||
"dialog": false,
|
|
||||||
"windows": {
|
|
||||||
"installMode": "passive"
|
|
||||||
},
|
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDM3MzE4MTM4RTNDMkM0NEQKUldSTnhNTGpPSUV4TjFkczFxRFJOZWgydzFQN1dmaFlKbXhJS1YyR1RKS1RnR09jYUpMaGsrWXYK"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user