Update botapp
This commit is contained in:
parent
a5cde031a1
commit
9f641d3504
8 changed files with 477 additions and 178 deletions
24
Cargo.toml
24
Cargo.toml
|
|
@ -4,6 +4,9 @@ version = "6.1.0"
|
|||
edition = "2021"
|
||||
description = "General Bots App - Tauri wrapper for desktop/mobile"
|
||||
license = "AGPL-3.0"
|
||||
repository = "https://github.com/GeneralBots/BotServer"
|
||||
keywords = ["bot", "ai", "chatbot", "tauri", "desktop"]
|
||||
categories = ["gui", "network-programming"]
|
||||
|
||||
[dependencies]
|
||||
# Core from botlib
|
||||
|
|
@ -42,3 +45,24 @@ desktop-tray = ["dep:ksni", "dep:trayicon"]
|
|||
|
||||
[build-dependencies]
|
||||
tauri-build = "2"
|
||||
|
||||
[lints.rust]
|
||||
unused_imports = "warn"
|
||||
unused_variables = "warn"
|
||||
unused_mut = "warn"
|
||||
unsafe_code = "deny"
|
||||
missing_debug_implementations = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
all = "warn"
|
||||
pedantic = "warn"
|
||||
nursery = "warn"
|
||||
cargo = "warn"
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
||||
panic = "warn"
|
||||
todo = "warn"
|
||||
# Disabled: Tauri commands require owned types (Window) that cannot be passed by reference
|
||||
needless_pass_by_value = "allow"
|
||||
# Disabled: transitive dependencies we cannot control
|
||||
multiple_crate_versions = "allow"
|
||||
|
|
|
|||
116
PROMPT.md
116
PROMPT.md
|
|
@ -5,6 +5,107 @@
|
|||
|
||||
---
|
||||
|
||||
## ZERO TOLERANCE POLICY
|
||||
|
||||
**This project has the strictest code quality requirements possible.**
|
||||
|
||||
**EVERY SINGLE WARNING MUST BE FIXED. NO EXCEPTIONS.**
|
||||
|
||||
---
|
||||
|
||||
## ABSOLUTE PROHIBITIONS
|
||||
|
||||
```
|
||||
❌ NEVER use #![allow()] or #[allow()] in source code to silence warnings
|
||||
❌ NEVER use _ prefix for unused variables - DELETE the variable or USE it
|
||||
❌ NEVER use .unwrap() - use ? or proper error handling
|
||||
❌ NEVER use .expect() - use ? or proper error handling
|
||||
❌ NEVER use panic!() or unreachable!() - handle all cases
|
||||
❌ NEVER use todo!() or unimplemented!() - write real code
|
||||
❌ NEVER leave unused imports - DELETE them
|
||||
❌ NEVER leave dead code - DELETE it or IMPLEMENT it
|
||||
❌ NEVER use approximate constants (3.14159) - use std::f64::consts::PI
|
||||
❌ NEVER silence clippy in code - FIX THE CODE or configure in Cargo.toml
|
||||
❌ NEVER add comments explaining what code does - code must be self-documenting
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CARGO.TOML LINT EXCEPTIONS
|
||||
|
||||
When a clippy lint has **technical false positives** that cannot be fixed in code,
|
||||
disable it in `Cargo.toml` with a comment explaining why:
|
||||
|
||||
```toml
|
||||
[lints.clippy]
|
||||
# Disabled: has false positives for functions with mut self, heap types (Vec, String)
|
||||
missing_const_for_fn = "allow"
|
||||
# Disabled: Tauri commands require owned types (Window) that cannot be passed by reference
|
||||
needless_pass_by_value = "allow"
|
||||
# Disabled: transitive dependencies we cannot control
|
||||
multiple_crate_versions = "allow"
|
||||
```
|
||||
|
||||
**Approved exceptions:**
|
||||
- `missing_const_for_fn` - false positives for `mut self`, heap types
|
||||
- `needless_pass_by_value` - Tauri/framework requirements
|
||||
- `multiple_crate_versions` - transitive dependencies
|
||||
- `future_not_send` - when async traits require non-Send futures
|
||||
|
||||
---
|
||||
|
||||
## MANDATORY CODE PATTERNS
|
||||
|
||||
### Error Handling - Use `?` Operator
|
||||
|
||||
```rust
|
||||
// ❌ WRONG
|
||||
let value = something.unwrap();
|
||||
let value = something.expect("msg");
|
||||
|
||||
// ✅ CORRECT
|
||||
let value = something?;
|
||||
let value = something.ok_or_else(|| Error::NotFound)?;
|
||||
```
|
||||
|
||||
### Self Usage in Impl Blocks
|
||||
|
||||
```rust
|
||||
// ❌ WRONG
|
||||
impl MyStruct {
|
||||
fn new() -> MyStruct { MyStruct { } }
|
||||
}
|
||||
|
||||
// ✅ CORRECT
|
||||
impl MyStruct {
|
||||
fn new() -> Self { Self { } }
|
||||
}
|
||||
```
|
||||
|
||||
### Format Strings - Inline Variables
|
||||
|
||||
```rust
|
||||
// ❌ WRONG
|
||||
format!("Hello {}", name)
|
||||
|
||||
// ✅ CORRECT
|
||||
format!("Hello {name}")
|
||||
```
|
||||
|
||||
### Derive Eq with PartialEq
|
||||
|
||||
```rust
|
||||
// ❌ WRONG
|
||||
#[derive(PartialEq)]
|
||||
struct MyStruct { }
|
||||
|
||||
// ✅ CORRECT
|
||||
#[derive(PartialEq, Eq)]
|
||||
struct MyStruct { }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Weekly Maintenance - EVERY MONDAY
|
||||
|
||||
### Package Review Checklist
|
||||
|
|
@ -387,11 +488,20 @@ cargo test
|
|||
|
||||
---
|
||||
|
||||
## Rules
|
||||
## Remember
|
||||
|
||||
- **ZERO WARNINGS** - Every clippy warning must be fixed
|
||||
- **NO ALLOW IN CODE** - Never use #[allow()] in source files
|
||||
- **CARGO.TOML EXCEPTIONS OK** - Disable lints with false positives in Cargo.toml with comment
|
||||
- **NO DEAD CODE** - Delete unused code, never prefix with _
|
||||
- **NO UNWRAP/EXPECT** - Use ? operator or proper error handling
|
||||
- **INLINE FORMAT ARGS** - format!("{name}") not format!("{}", name)
|
||||
- **USE SELF** - In impl blocks, use Self not the type name
|
||||
- **DERIVE EQ** - Always derive Eq with PartialEq
|
||||
- **USE DIAGNOSTICS** - Use IDE diagnostics tool, never call cargo clippy directly
|
||||
- **Desktop-only features** - Shared logic in botserver
|
||||
- **Tauri APIs** - No direct fs access from JS
|
||||
- **Platform abstractions** - Use cfg for platform code
|
||||
- **Security** - Minimal allowlist in tauri.conf.json
|
||||
- **Zero warnings** - Clean compilation required
|
||||
- **No cargo audit** - Exempt per project requirements
|
||||
- **Version**: Always 6.1.0 - do not change without approval
|
||||
- **Session Continuation**: When running out of context, create detailed summary: (1) what was done, (2) what remains, (3) specific files and line numbers, (4) exact next steps.
|
||||
|
|
|
|||
4
build.rs
4
build.rs
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
tauri_build::build();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::{Emitter, Window};
|
||||
|
||||
|
|
@ -15,6 +17,11 @@ pub struct FileItem {
|
|||
pub size: Option<u64>,
|
||||
}
|
||||
|
||||
/// Lists files in the specified directory.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the path does not exist or cannot be read.
|
||||
#[tauri::command]
|
||||
pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
||||
let base_path = Path::new(path);
|
||||
|
|
@ -35,8 +42,8 @@ pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
|||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
let size = metadata.as_ref().map(|m| m.len());
|
||||
let is_dir = metadata.map(|m| m.is_dir()).unwrap_or(false);
|
||||
let size = metadata.as_ref().map(std::fs::Metadata::len);
|
||||
let is_dir = metadata.is_some_and(|m| m.is_dir());
|
||||
|
||||
files.push(FileItem {
|
||||
name,
|
||||
|
|
@ -59,17 +66,15 @@ pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
|||
Ok(files)
|
||||
}
|
||||
|
||||
/// Uploads a file from source to destination with progress events.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the source file cannot be read or the destination cannot be written.
|
||||
#[tauri::command]
|
||||
pub async fn upload_file(
|
||||
window: Window,
|
||||
src_path: String,
|
||||
dest_path: String,
|
||||
) -> Result<(), String> {
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
let src = PathBuf::from(&src_path);
|
||||
let dest_dir = PathBuf::from(&dest_path);
|
||||
pub fn upload_file(window: Window, src_path: &str, dest_path: &str) -> Result<(), String> {
|
||||
let src = PathBuf::from(src_path);
|
||||
let dest_dir = PathBuf::from(dest_path);
|
||||
let dest = dest_dir.join(src.file_name().ok_or("Invalid source file")?);
|
||||
|
||||
if !dest_dir.exists() {
|
||||
|
|
@ -81,7 +86,7 @@ pub async fn upload_file(
|
|||
let file_size = source_file.metadata().map_err(|e| e.to_string())?.len();
|
||||
|
||||
let mut buffer = [0; 8192];
|
||||
let mut total_read = 0u64;
|
||||
let mut total_read: u64 = 0;
|
||||
|
||||
loop {
|
||||
let bytes_read = source_file.read(&mut buffer).map_err(|e| e.to_string())?;
|
||||
|
|
@ -93,7 +98,12 @@ pub async fn upload_file(
|
|||
.map_err(|e| e.to_string())?;
|
||||
|
||||
total_read += bytes_read as u64;
|
||||
let progress = (total_read as f64 / file_size as f64) * 100.0;
|
||||
|
||||
let progress = if file_size > 0 {
|
||||
(total_read * 100) / file_size
|
||||
} else {
|
||||
100
|
||||
};
|
||||
|
||||
window
|
||||
.emit("upload_progress", progress)
|
||||
|
|
@ -103,9 +113,14 @@ pub async fn upload_file(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a new folder at the specified path.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the folder already exists or cannot be created.
|
||||
#[tauri::command]
|
||||
pub fn create_folder(path: String, name: String) -> Result<(), String> {
|
||||
let full_path = Path::new(&path).join(&name);
|
||||
pub fn create_folder(path: &str, name: &str) -> Result<(), String> {
|
||||
let full_path = Path::new(path).join(name);
|
||||
|
||||
if full_path.exists() {
|
||||
return Err("Folder already exists".into());
|
||||
|
|
@ -115,9 +130,14 @@ pub fn create_folder(path: String, name: String) -> Result<(), String> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes a file or directory at the specified path.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the path does not exist or cannot be deleted.
|
||||
#[tauri::command]
|
||||
pub fn delete_path(path: String) -> Result<(), String> {
|
||||
let target = Path::new(&path);
|
||||
pub fn delete_path(path: &str) -> Result<(), String> {
|
||||
let target = Path::new(path);
|
||||
|
||||
if !target.exists() {
|
||||
return Err("Path does not exist".into());
|
||||
|
|
@ -132,6 +152,11 @@ pub fn delete_path(path: String) -> Result<(), String> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the user's home directory path.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the home directory cannot be determined.
|
||||
#[tauri::command]
|
||||
pub fn get_home_dir() -> Result<String, String> {
|
||||
dirs::home_dir()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
//! Rclone Sync Module for Desktop File Synchronization
|
||||
//!
|
||||
//! Provides bidirectional sync between local filesystem and remote S3 storage
|
||||
//! using rclone as the underlying sync engine.
|
||||
//!
|
||||
//! Desktop-only feature: This runs rclone as a subprocess on the user's machine.
|
||||
//! Sync module for cloud storage synchronization using rclone.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -42,10 +37,12 @@ pub enum SyncMode {
|
|||
|
||||
impl Default for SyncConfig {
|
||||
fn default() -> Self {
|
||||
let local_path = dirs::home_dir().map_or_else(
|
||||
|| "~/GeneralBots".to_string(),
|
||||
|p| p.join("GeneralBots").to_string_lossy().to_string(),
|
||||
);
|
||||
Self {
|
||||
local_path: dirs::home_dir()
|
||||
.map(|p| p.join("GeneralBots").to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "~/GeneralBots".to_string()),
|
||||
local_path,
|
||||
remote_name: "gbdrive".to_string(),
|
||||
remote_path: "/".to_string(),
|
||||
sync_mode: SyncMode::Bisync,
|
||||
|
|
@ -59,10 +56,15 @@ impl Default for SyncConfig {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the current sync status.
|
||||
#[tauri::command]
|
||||
#[must_use]
|
||||
pub fn get_sync_status() -> SyncStatus {
|
||||
let process_guard = RCLONE_PROCESS.lock().unwrap();
|
||||
let process_guard = RCLONE_PROCESS
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let is_running = process_guard.is_some();
|
||||
drop(process_guard);
|
||||
|
||||
SyncStatus {
|
||||
status: if is_running {
|
||||
|
|
@ -79,12 +81,19 @@ pub fn get_sync_status() -> SyncStatus {
|
|||
}
|
||||
}
|
||||
|
||||
/// Starts the sync process with the given configuration.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if sync is already running, directory creation fails, or rclone fails to start.
|
||||
#[tauri::command]
|
||||
pub async fn start_sync(window: Window, config: Option<SyncConfig>) -> Result<SyncStatus, String> {
|
||||
pub fn start_sync(window: Window, config: Option<SyncConfig>) -> Result<SyncStatus, String> {
|
||||
let config = config.unwrap_or_default();
|
||||
|
||||
{
|
||||
let process_guard = RCLONE_PROCESS.lock().unwrap();
|
||||
let process_guard = RCLONE_PROCESS
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if process_guard.is_some() {
|
||||
return Err("Sync already running".to_string());
|
||||
}
|
||||
|
|
@ -93,7 +102,7 @@ pub async fn start_sync(window: Window, config: Option<SyncConfig>) -> Result<Sy
|
|||
let local_path = PathBuf::from(&config.local_path);
|
||||
if !local_path.exists() {
|
||||
std::fs::create_dir_all(&local_path)
|
||||
.map_err(|e| format!("Failed to create local directory: {}", e))?;
|
||||
.map_err(|e| format!("Failed to create local directory: {e}"))?;
|
||||
}
|
||||
|
||||
let mut cmd = Command::new("rclone");
|
||||
|
|
@ -129,20 +138,21 @@ pub async fn start_sync(window: Window, config: Option<SyncConfig>) -> Result<Sy
|
|||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
"rclone not found. Please install rclone: https://rclone.org/install/".to_string()
|
||||
} else {
|
||||
format!("Failed to start rclone: {}", e)
|
||||
format!("Failed to start rclone: {e}")
|
||||
}
|
||||
})?;
|
||||
|
||||
{
|
||||
let mut process_guard = RCLONE_PROCESS.lock().unwrap();
|
||||
let mut process_guard = RCLONE_PROCESS
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
*process_guard = Some(child);
|
||||
}
|
||||
|
||||
let _ = window.emit("sync_started", ());
|
||||
|
||||
let window_clone = window.clone();
|
||||
std::thread::spawn(move || {
|
||||
monitor_sync_process(window_clone);
|
||||
monitor_sync_process(&window);
|
||||
});
|
||||
|
||||
Ok(SyncStatus {
|
||||
|
|
@ -156,82 +166,87 @@ pub async fn start_sync(window: Window, config: Option<SyncConfig>) -> Result<Sy
|
|||
})
|
||||
}
|
||||
|
||||
/// Stops the currently running sync process.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if no sync process is running.
|
||||
#[tauri::command]
|
||||
pub fn stop_sync() -> Result<SyncStatus, String> {
|
||||
let mut process_guard = RCLONE_PROCESS.lock().unwrap();
|
||||
let mut process_guard = RCLONE_PROCESS
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
|
||||
if let Some(mut child) = process_guard.take() {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
unsafe {
|
||||
libc::kill(child.id() as i32, libc::SIGTERM);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
process_guard
|
||||
.take()
|
||||
.ok_or_else(|| "No sync process running".to_string())
|
||||
.map(|mut child| {
|
||||
let _ = child.kill();
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
let _ = child.wait();
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
|
||||
Ok(SyncStatus {
|
||||
status: "stopped".to_string(),
|
||||
is_running: false,
|
||||
last_sync: Some(chrono::Utc::now().to_rfc3339()),
|
||||
files_synced: 0,
|
||||
bytes_transferred: 0,
|
||||
current_file: None,
|
||||
error: None,
|
||||
SyncStatus {
|
||||
status: "stopped".to_string(),
|
||||
is_running: false,
|
||||
last_sync: Some(chrono::Utc::now().to_rfc3339()),
|
||||
files_synced: 0,
|
||||
bytes_transferred: 0,
|
||||
current_file: None,
|
||||
error: None,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Err("No sync process running".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures an rclone remote for S3-compatible storage.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if rclone configuration fails.
|
||||
#[tauri::command]
|
||||
pub fn configure_remote(
|
||||
remote_name: String,
|
||||
endpoint: String,
|
||||
access_key: String,
|
||||
secret_key: String,
|
||||
bucket: String,
|
||||
remote_name: &str,
|
||||
endpoint: &str,
|
||||
access_key: &str,
|
||||
secret_key: &str,
|
||||
bucket: &str,
|
||||
) -> Result<(), String> {
|
||||
let output = Command::new("rclone")
|
||||
.args([
|
||||
"config",
|
||||
"create",
|
||||
&remote_name,
|
||||
remote_name,
|
||||
"s3",
|
||||
"provider",
|
||||
"Minio",
|
||||
"endpoint",
|
||||
&endpoint,
|
||||
endpoint,
|
||||
"access_key_id",
|
||||
&access_key,
|
||||
access_key,
|
||||
"secret_access_key",
|
||||
&secret_key,
|
||||
secret_key,
|
||||
"acl",
|
||||
"private",
|
||||
])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to configure rclone: {}", e))?;
|
||||
.map_err(|e| format!("Failed to configure rclone: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("rclone config failed: {}", stderr));
|
||||
return Err(format!("rclone config failed: {stderr}"));
|
||||
}
|
||||
|
||||
let _ = Command::new("rclone")
|
||||
.args(["config", "update", &remote_name, "bucket", &bucket])
|
||||
.args(["config", "update", remote_name, "bucket", bucket])
|
||||
.output();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if rclone is installed and returns its version.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if rclone is not installed or the check fails.
|
||||
#[tauri::command]
|
||||
pub fn check_rclone_installed() -> Result<String, String> {
|
||||
let output = Command::new("rclone")
|
||||
|
|
@ -241,7 +256,7 @@ pub fn check_rclone_installed() -> Result<String, String> {
|
|||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
"rclone not installed".to_string()
|
||||
} else {
|
||||
format!("Error checking rclone: {}", e)
|
||||
format!("Error checking rclone: {e}")
|
||||
}
|
||||
})?;
|
||||
|
||||
|
|
@ -254,12 +269,17 @@ pub fn check_rclone_installed() -> Result<String, String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Lists all configured rclone remotes.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if rclone fails to list remotes.
|
||||
#[tauri::command]
|
||||
pub fn list_remotes() -> Result<Vec<String>, String> {
|
||||
let output = Command::new("rclone")
|
||||
.args(["listremotes"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to list remotes: {}", e))?;
|
||||
.map_err(|e| format!("Failed to list remotes: {e}"))?;
|
||||
|
||||
if output.status.success() {
|
||||
let remotes = String::from_utf8_lossy(&output.stdout);
|
||||
|
|
@ -273,19 +293,27 @@ pub fn list_remotes() -> Result<Vec<String>, String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the default sync folder path.
|
||||
#[tauri::command]
|
||||
#[must_use]
|
||||
pub fn get_sync_folder() -> String {
|
||||
dirs::home_dir()
|
||||
.map(|p| p.join("GeneralBots").to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "~/GeneralBots".to_string())
|
||||
dirs::home_dir().map_or_else(
|
||||
|| "~/GeneralBots".to_string(),
|
||||
|p| p.join("GeneralBots").to_string_lossy().to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets the sync folder path, creating it if necessary.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the directory cannot be created or the path is not a directory.
|
||||
#[tauri::command]
|
||||
pub fn set_sync_folder(path: String) -> Result<(), String> {
|
||||
let path = PathBuf::from(&path);
|
||||
pub fn set_sync_folder(path: &str) -> Result<(), String> {
|
||||
let path = PathBuf::from(path);
|
||||
|
||||
if !path.exists() {
|
||||
std::fs::create_dir_all(&path).map_err(|e| format!("Failed to create directory: {}", e))?;
|
||||
std::fs::create_dir_all(&path).map_err(|e| format!("Failed to create directory: {e}"))?;
|
||||
}
|
||||
|
||||
if !path.is_dir() {
|
||||
|
|
@ -295,17 +323,20 @@ pub fn set_sync_folder(path: String) -> Result<(), String> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn monitor_sync_process(window: Window) {
|
||||
fn monitor_sync_process(window: &Window) {
|
||||
loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
|
||||
let mut process_guard = RCLONE_PROCESS.lock().unwrap();
|
||||
let mut process_guard = RCLONE_PROCESS
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
|
||||
if let Some(ref mut child) = *process_guard {
|
||||
let status_opt = if let Some(ref mut child) = *process_guard {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
let success = status.success();
|
||||
Ok(Some(exit_status)) => {
|
||||
let success = exit_status.success();
|
||||
*process_guard = None;
|
||||
drop(process_guard);
|
||||
|
||||
let status = SyncStatus {
|
||||
status: if success {
|
||||
|
|
@ -321,15 +352,20 @@ fn monitor_sync_process(window: Window) {
|
|||
error: if success {
|
||||
None
|
||||
} else {
|
||||
Some(format!("Exit code: {:?}", status.code()))
|
||||
Some(format!("Exit code: {:?}", exit_status.code()))
|
||||
},
|
||||
};
|
||||
|
||||
let _ = window.emit("sync_completed", &status);
|
||||
break;
|
||||
if success {
|
||||
let _ = window.emit("sync_completed", &status);
|
||||
} else {
|
||||
let _ = window.emit("sync_error", &status);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Ok(None) => {
|
||||
let status = SyncStatus {
|
||||
drop(process_guard);
|
||||
Some(SyncStatus {
|
||||
status: "syncing".to_string(),
|
||||
is_running: true,
|
||||
last_sync: None,
|
||||
|
|
@ -337,11 +373,11 @@ fn monitor_sync_process(window: Window) {
|
|||
bytes_transferred: 0,
|
||||
current_file: None,
|
||||
error: None,
|
||||
};
|
||||
let _ = window.emit("sync_progress", &status);
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
*process_guard = None;
|
||||
drop(process_guard);
|
||||
|
||||
let status = SyncStatus {
|
||||
status: "error".to_string(),
|
||||
|
|
@ -350,16 +386,19 @@ fn monitor_sync_process(window: Window) {
|
|||
files_synced: 0,
|
||||
bytes_transferred: 0,
|
||||
current_file: None,
|
||||
error: Some(format!("Process error: {}", e)),
|
||||
error: Some(format!("Process error: {e}")),
|
||||
};
|
||||
|
||||
let _ = window.emit("sync_error", &status);
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
drop(process_guard);
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(status) = status_opt {
|
||||
let _ = window.emit("sync_progress", &status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,47 @@
|
|||
//! Tray manager for desktop application.
|
||||
//!
|
||||
//! Provides system tray functionality for different operating modes.
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Clone)]
|
||||
/// Manages the system tray icon and its interactions.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TrayManager {
|
||||
hostname: Arc<RwLock<Option<String>>>,
|
||||
running_mode: RunningMode,
|
||||
tray_active: Arc<RwLock<bool>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
/// The running mode of the application.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RunningMode {
|
||||
/// Full server mode with all services.
|
||||
Server,
|
||||
/// Desktop mode with UI and local services.
|
||||
Desktop,
|
||||
/// Client mode connecting to remote server.
|
||||
Client,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Events that can be triggered from the tray menu.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum TrayEvent {
|
||||
/// Open the main application window.
|
||||
Open,
|
||||
/// Open settings dialog.
|
||||
Settings,
|
||||
/// Show about dialog.
|
||||
About,
|
||||
/// Quit the application.
|
||||
Quit,
|
||||
}
|
||||
|
||||
impl TrayManager {
|
||||
/// Creates a new tray manager with default settings.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
hostname: Arc::new(RwLock::new(None)),
|
||||
|
|
@ -34,6 +50,8 @@ impl TrayManager {
|
|||
}
|
||||
}
|
||||
|
||||
/// Creates a new tray manager with the specified running mode.
|
||||
#[must_use]
|
||||
pub fn with_mode(mode: RunningMode) -> Self {
|
||||
Self {
|
||||
hostname: Arc::new(RwLock::new(None)),
|
||||
|
|
@ -42,6 +60,11 @@ impl TrayManager {
|
|||
}
|
||||
}
|
||||
|
||||
/// Starts the tray manager based on the running mode.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the tray initialization fails.
|
||||
pub async fn start(&self) -> Result<()> {
|
||||
match self.running_mode {
|
||||
RunningMode::Desktop => {
|
||||
|
|
@ -51,7 +74,7 @@ impl TrayManager {
|
|||
log::info!("Running in server mode - tray icon disabled");
|
||||
}
|
||||
RunningMode::Client => {
|
||||
self.start_client_mode().await?;
|
||||
self.start_client_mode().await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -59,53 +82,55 @@ impl TrayManager {
|
|||
|
||||
async fn start_desktop_mode(&self) -> Result<()> {
|
||||
log::info!("Starting desktop mode tray icon");
|
||||
|
||||
let mut active = self.tray_active.write().await;
|
||||
*active = true;
|
||||
drop(active);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
self.setup_linux_tray().await?;
|
||||
}
|
||||
self.setup_linux_tray();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.setup_windows_tray().await?;
|
||||
}
|
||||
self.setup_windows_tray();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
self.setup_macos_tray().await?;
|
||||
}
|
||||
self.setup_macos_tray();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_client_mode(&self) -> Result<()> {
|
||||
async fn start_client_mode(&self) {
|
||||
log::info!("Starting client mode with minimal tray");
|
||||
let mut active = self.tray_active.write().await;
|
||||
*active = true;
|
||||
Ok(())
|
||||
drop(active);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn setup_linux_tray(&self) -> Result<()> {
|
||||
log::info!("Initializing Linux system tray via DBus/StatusNotifierItem");
|
||||
Ok(())
|
||||
fn setup_linux_tray(&self) {
|
||||
log::info!(
|
||||
"Initializing Linux system tray via DBus/StatusNotifierItem for mode: {:?}",
|
||||
self.running_mode
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn setup_windows_tray(&self) -> Result<()> {
|
||||
log::info!("Initializing Windows system tray via Shell_NotifyIcon");
|
||||
Ok(())
|
||||
fn setup_windows_tray(&self) {
|
||||
log::info!(
|
||||
"Initializing Windows system tray via Shell_NotifyIcon for mode: {:?}",
|
||||
self.running_mode
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn setup_macos_tray(&self) -> Result<()> {
|
||||
log::info!("Initializing macOS menu bar via NSStatusItem");
|
||||
Ok(())
|
||||
fn setup_macos_tray(&self) {
|
||||
log::info!(
|
||||
"Initializing macOS menu bar via NSStatusItem for mode: {:?}",
|
||||
self.running_mode
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a string representation of the current running mode.
|
||||
#[must_use]
|
||||
pub fn get_mode_string(&self) -> String {
|
||||
match self.running_mode {
|
||||
RunningMode::Desktop => "Desktop".to_string(),
|
||||
|
|
@ -114,26 +139,50 @@ impl TrayManager {
|
|||
}
|
||||
}
|
||||
|
||||
/// Updates the tray status message.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the status update fails.
|
||||
pub async fn update_status(&self, status: &str) -> Result<()> {
|
||||
let active = self.tray_active.read().await;
|
||||
if *active {
|
||||
log::info!("Tray status: {}", status);
|
||||
let is_active = *active;
|
||||
drop(active);
|
||||
|
||||
if is_active {
|
||||
log::info!("Tray status: {status}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the tray tooltip text.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if setting the tooltip fails.
|
||||
pub async fn set_tooltip(&self, tooltip: &str) -> Result<()> {
|
||||
let active = self.tray_active.read().await;
|
||||
if *active {
|
||||
log::debug!("Tray tooltip: {}", tooltip);
|
||||
let is_active = *active;
|
||||
drop(active);
|
||||
|
||||
if is_active {
|
||||
log::debug!("Tray tooltip: {tooltip}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shows a desktop notification.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if showing the notification fails.
|
||||
pub async fn show_notification(&self, title: &str, body: &str) -> Result<()> {
|
||||
let active = self.tray_active.read().await;
|
||||
if *active {
|
||||
log::info!("Notification: {} - {}", title, body);
|
||||
let is_active = *active;
|
||||
drop(active);
|
||||
|
||||
if is_active {
|
||||
log::info!("Notification: {title} - {body}");
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
|
|
@ -145,7 +194,7 @@ impl TrayManager {
|
|||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let script = format!("display notification \"{}\" with title \"{}\"", body, title);
|
||||
let script = format!("display notification \"{body}\" with title \"{title}\"");
|
||||
let _ = std::process::Command::new("osascript")
|
||||
.arg("-e")
|
||||
.arg(&script)
|
||||
|
|
@ -155,41 +204,53 @@ impl TrayManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the current hostname.
|
||||
pub async fn get_hostname(&self) -> Option<String> {
|
||||
let hostname = self.hostname.read().await;
|
||||
hostname.clone()
|
||||
}
|
||||
|
||||
pub async fn set_hostname(&self, hostname: String) {
|
||||
let mut h = self.hostname.write().await;
|
||||
*h = Some(hostname);
|
||||
/// Sets the hostname.
|
||||
pub async fn set_hostname(&self, new_hostname: String) {
|
||||
let mut hostname = self.hostname.write().await;
|
||||
*hostname = Some(new_hostname);
|
||||
}
|
||||
|
||||
pub async fn stop(&self) -> Result<()> {
|
||||
/// Stops the tray manager.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if stopping fails.
|
||||
pub async fn stop(&self) {
|
||||
let mut active = self.tray_active.write().await;
|
||||
*active = false;
|
||||
drop(active);
|
||||
log::info!("Tray manager stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns whether the tray is currently active.
|
||||
pub async fn is_active(&self) -> bool {
|
||||
let active = self.tray_active.read().await;
|
||||
*active
|
||||
let result = *active;
|
||||
drop(active);
|
||||
result
|
||||
}
|
||||
|
||||
/// Handles a tray event and performs the appropriate action.
|
||||
pub fn handle_event(&self, event: TrayEvent) {
|
||||
let mode = self.get_mode_string();
|
||||
match event {
|
||||
TrayEvent::Open => {
|
||||
log::info!("Tray event: Open main window");
|
||||
log::info!("Tray event: Open main window (mode: {mode})");
|
||||
}
|
||||
TrayEvent::Settings => {
|
||||
log::info!("Tray event: Open settings");
|
||||
log::info!("Tray event: Open settings (mode: {mode})");
|
||||
}
|
||||
TrayEvent::About => {
|
||||
log::info!("Tray event: Show about dialog");
|
||||
log::info!("Tray event: Show about dialog (mode: {mode})");
|
||||
}
|
||||
TrayEvent::Quit => {
|
||||
log::info!("Tray event: Quit application");
|
||||
log::info!("Tray event: Quit application (mode: {mode})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -201,19 +262,28 @@ impl Default for TrayManager {
|
|||
}
|
||||
}
|
||||
|
||||
/// Monitors the status of services.
|
||||
#[derive(Debug)]
|
||||
pub struct ServiceMonitor {
|
||||
services: Vec<ServiceStatus>,
|
||||
}
|
||||
|
||||
/// Status of a monitored service.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ServiceStatus {
|
||||
/// Service name.
|
||||
pub name: String,
|
||||
/// Whether the service is running.
|
||||
pub running: bool,
|
||||
/// Service port number.
|
||||
pub port: u16,
|
||||
/// Service URL.
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl ServiceMonitor {
|
||||
/// Creates a new service monitor with default services.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
services: vec![
|
||||
|
|
@ -233,15 +303,17 @@ impl ServiceMonitor {
|
|||
}
|
||||
}
|
||||
|
||||
/// Adds a service to monitor.
|
||||
pub fn add_service(&mut self, name: &str, port: u16) {
|
||||
self.services.push(ServiceStatus {
|
||||
name: name.to_string(),
|
||||
running: false,
|
||||
port,
|
||||
url: format!("http://localhost:{}", port),
|
||||
url: format!("http://localhost:{port}"),
|
||||
});
|
||||
}
|
||||
|
||||
/// Checks all services and returns their current status.
|
||||
pub async fn check_services(&mut self) -> Vec<ServiceStatus> {
|
||||
for service in &mut self.services {
|
||||
service.running = Self::check_service(&service.url).await;
|
||||
|
|
@ -249,39 +321,43 @@ impl ServiceMonitor {
|
|||
self.services.clone()
|
||||
}
|
||||
|
||||
/// Checks if a service is running at the given URL.
|
||||
pub async fn check_service(url: &str) -> bool {
|
||||
if !url.starts_with("http://") && !url.starts_with("https://") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let client = match reqwest::Client::builder()
|
||||
let Ok(client) = reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(std::time::Duration::from_secs(2))
|
||||
.build()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(_) => return false,
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let health_url = format!("{}/health", url.trim_end_matches('/'));
|
||||
|
||||
match client.get(&health_url).send().await {
|
||||
Ok(response) => response.status().is_success(),
|
||||
Err(_) => match client.get(url).send().await {
|
||||
Ok(response) => response.status().is_success(),
|
||||
Err(_) => false,
|
||||
},
|
||||
}
|
||||
client
|
||||
.get(&health_url)
|
||||
.send()
|
||||
.await
|
||||
.is_ok_and(|response| response.status().is_success())
|
||||
}
|
||||
|
||||
/// Gets a service by name.
|
||||
#[must_use]
|
||||
pub fn get_service(&self, name: &str) -> Option<&ServiceStatus> {
|
||||
self.services.iter().find(|s| s.name == name)
|
||||
}
|
||||
|
||||
/// Returns whether all services are running.
|
||||
#[must_use]
|
||||
pub fn all_running(&self) -> bool {
|
||||
self.services.iter().all(|s| s.running)
|
||||
}
|
||||
|
||||
/// Returns whether any service is running.
|
||||
#[must_use]
|
||||
pub fn any_running(&self) -> bool {
|
||||
self.services.iter().any(|s| s.running)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,5 @@
|
|||
//! `BotApp` - Tauri desktop application for General Bots
|
||||
//!
|
||||
//! This crate wraps the web UI with native desktop features.
|
||||
|
||||
pub mod desktop;
|
||||
|
|
|
|||
61
src/main.rs
61
src/main.rs
|
|
@ -19,7 +19,8 @@ async fn start_tray(tray: tauri::State<'_, TrayManager>) -> Result<(), String> {
|
|||
|
||||
#[tauri::command]
|
||||
async fn stop_tray(tray: tauri::State<'_, TrayManager>) -> Result<(), String> {
|
||||
tray.stop().await.map_err(|e| e.to_string())
|
||||
tray.stop().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -70,7 +71,7 @@ fn handle_tray_event(tray: tauri::State<'_, TrayManager>, event: String) -> Resu
|
|||
"settings" => TrayEvent::Settings,
|
||||
"about" => TrayEvent::About,
|
||||
"quit" => TrayEvent::Quit,
|
||||
_ => return Err(format!("Unknown event: {}", event)),
|
||||
_ => return Err(format!("Unknown event: {event}")),
|
||||
};
|
||||
tray.handle_event(tray_event);
|
||||
Ok(())
|
||||
|
|
@ -80,8 +81,10 @@ fn handle_tray_event(tray: tauri::State<'_, TrayManager>, event: String) -> Resu
|
|||
async fn check_services(
|
||||
monitor: tauri::State<'_, tokio::sync::Mutex<ServiceMonitor>>,
|
||||
) -> Result<Vec<desktop::tray::ServiceStatus>, String> {
|
||||
let mut monitor = monitor.lock().await;
|
||||
Ok(monitor.check_services().await)
|
||||
let mut guard = monitor.lock().await;
|
||||
let result = guard.check_services().await;
|
||||
drop(guard);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -90,8 +93,9 @@ async fn add_service(
|
|||
name: String,
|
||||
port: u16,
|
||||
) -> Result<(), String> {
|
||||
let mut monitor = monitor.lock().await;
|
||||
monitor.add_service(&name, port);
|
||||
let mut guard = monitor.lock().await;
|
||||
guard.add_service(&name, port);
|
||||
drop(guard);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -100,24 +104,30 @@ async fn get_service(
|
|||
monitor: tauri::State<'_, tokio::sync::Mutex<ServiceMonitor>>,
|
||||
name: String,
|
||||
) -> Result<Option<desktop::tray::ServiceStatus>, String> {
|
||||
let monitor = monitor.lock().await;
|
||||
Ok(monitor.get_service(&name).cloned())
|
||||
let guard = monitor.lock().await;
|
||||
let result = guard.get_service(&name).cloned();
|
||||
drop(guard);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn all_services_running(
|
||||
monitor: tauri::State<'_, tokio::sync::Mutex<ServiceMonitor>>,
|
||||
) -> Result<bool, String> {
|
||||
let monitor = monitor.lock().await;
|
||||
Ok(monitor.all_running())
|
||||
let guard = monitor.lock().await;
|
||||
let result = guard.all_running();
|
||||
drop(guard);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn any_service_running(
|
||||
monitor: tauri::State<'_, tokio::sync::Mutex<ServiceMonitor>>,
|
||||
) -> Result<bool, String> {
|
||||
let monitor = monitor.lock().await;
|
||||
Ok(monitor.any_running())
|
||||
let guard = monitor.lock().await;
|
||||
let result = guard.any_running();
|
||||
drop(guard);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -138,8 +148,7 @@ fn create_tray_with_mode(mode: String) -> Result<String, String> {
|
|||
"client" => RunningMode::Client,
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"Invalid mode: {}. Use Server, Desktop, or Client",
|
||||
mode
|
||||
"Invalid mode: {mode}. Use Server, Desktop, or Client"
|
||||
))
|
||||
}
|
||||
};
|
||||
|
|
@ -152,12 +161,13 @@ fn main() {
|
|||
.format_timestamp_millis()
|
||||
.init();
|
||||
|
||||
info!("BotApp {} starting...", env!("CARGO_PKG_VERSION"));
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
info!("BotApp {version} starting...");
|
||||
|
||||
let tray_manager = TrayManager::with_mode(RunningMode::Desktop);
|
||||
let service_monitor = tokio::sync::Mutex::new(ServiceMonitor::new());
|
||||
|
||||
tauri::Builder::default()
|
||||
let builder_result = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.manage(tray_manager)
|
||||
|
|
@ -197,20 +207,29 @@ fn main() {
|
|||
.setup(|app| {
|
||||
let tray = app.state::<TrayManager>();
|
||||
let mode = tray.get_mode_string();
|
||||
info!("BotApp setup complete in {} mode", mode);
|
||||
info!("BotApp setup complete in {mode} mode");
|
||||
|
||||
let tray_clone = tray.inner().clone();
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let rt = match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
log::error!("Failed to create runtime: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
rt.block_on(async {
|
||||
if let Err(e) = tray_clone.start().await {
|
||||
log::error!("Failed to start tray: {}", e);
|
||||
log::error!("Failed to start tray: {e}");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("Failed to run BotApp");
|
||||
.run(tauri::generate_context!());
|
||||
|
||||
if let Err(e) = builder_result {
|
||||
log::error!("Failed to run BotApp: {e}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue