refactor: integrate TrayManager with Tauri commands, zero dead code
This commit is contained in:
parent
3a2510c22b
commit
a5cde031a1
7 changed files with 489 additions and 274 deletions
|
|
@ -1,37 +1,29 @@
|
|||
// BotApp Extensions - Injected by Tauri into botui's suite
|
||||
// Adds app-only guides and native functionality
|
||||
//
|
||||
// This script runs only in the Tauri app context and extends
|
||||
// the botui suite with desktop-specific features.
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// App-only guides that will be injected into the suite navigation
|
||||
const APP_GUIDES = [
|
||||
{
|
||||
id: 'local-files',
|
||||
label: 'Local Files',
|
||||
id: "local-files",
|
||||
label: "Local Files",
|
||||
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||
<path d="M12 11v6M9 14h6"/>
|
||||
</svg>`,
|
||||
hxGet: '/app/guides/local-files.html',
|
||||
description: 'Access and manage files on your device'
|
||||
hxGet: "/app/guides/local-files.html",
|
||||
description: "Access and manage files on your device",
|
||||
},
|
||||
{
|
||||
id: 'native-settings',
|
||||
label: 'App Settings',
|
||||
id: "native-settings",
|
||||
label: "App Settings",
|
||||
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>`,
|
||||
hxGet: '/app/guides/native-settings.html',
|
||||
description: 'Configure desktop app settings'
|
||||
}
|
||||
hxGet: "/app/guides/native-settings.html",
|
||||
description: "Configure desktop app settings",
|
||||
},
|
||||
];
|
||||
|
||||
// CSS for app-only elements
|
||||
const APP_STYLES = `
|
||||
.app-grid-separator {
|
||||
grid-column: 1 / -1;
|
||||
|
|
@ -66,49 +58,39 @@
|
|||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Inject app-specific styles
|
||||
*/
|
||||
function injectStyles() {
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.id = 'botapp-styles';
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.id = "botapp-styles";
|
||||
styleEl.textContent = APP_STYLES;
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject app-only guides into the suite navigation
|
||||
*/
|
||||
function injectAppGuides() {
|
||||
const grid = document.querySelector('.app-grid');
|
||||
const grid = document.querySelector(".app-grid");
|
||||
if (!grid) {
|
||||
// Retry if grid not found yet (page still loading)
|
||||
setTimeout(injectAppGuides, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already injected
|
||||
if (document.querySelector('.app-grid-separator')) {
|
||||
if (document.querySelector(".app-grid-separator")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add separator
|
||||
const separator = document.createElement('div');
|
||||
separator.className = 'app-grid-separator';
|
||||
separator.innerHTML = '<span>Desktop Features</span>';
|
||||
const separator = document.createElement("div");
|
||||
separator.className = "app-grid-separator";
|
||||
separator.innerHTML = "<span>Desktop Features</span>";
|
||||
grid.appendChild(separator);
|
||||
|
||||
// Add app-only guides
|
||||
APP_GUIDES.forEach(guide => {
|
||||
const item = document.createElement('a');
|
||||
item.className = 'app-item app-only';
|
||||
APP_GUIDES.forEach((guide) => {
|
||||
const item = document.createElement("a");
|
||||
item.className = "app-item app-only";
|
||||
item.href = `#${guide.id}`;
|
||||
item.dataset.section = guide.id;
|
||||
item.setAttribute('role', 'menuitem');
|
||||
item.setAttribute('aria-label', guide.description || guide.label);
|
||||
item.setAttribute('hx-get', guide.hxGet);
|
||||
item.setAttribute('hx-target', '#main-content');
|
||||
item.setAttribute('hx-push-url', 'true');
|
||||
item.setAttribute("role", "menuitem");
|
||||
item.setAttribute("aria-label", guide.description || guide.label);
|
||||
item.setAttribute("hx-get", guide.hxGet);
|
||||
item.setAttribute("hx-target", "#main-content");
|
||||
item.setAttribute("hx-push-url", "true");
|
||||
item.innerHTML = `
|
||||
<div class="app-icon" aria-hidden="true">${guide.icon}</div>
|
||||
<span>${guide.label}</span>
|
||||
|
|
@ -116,108 +98,87 @@
|
|||
grid.appendChild(item);
|
||||
});
|
||||
|
||||
// Re-process HTMX on new elements
|
||||
if (window.htmx) {
|
||||
htmx.process(grid);
|
||||
}
|
||||
|
||||
console.log('[BotApp] App guides injected successfully');
|
||||
console.log("[BotApp] App guides injected successfully");
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Tauri event listeners
|
||||
*/
|
||||
function setupTauriEvents() {
|
||||
if (!window.__TAURI__) {
|
||||
console.warn('[BotApp] Tauri API not available');
|
||||
console.warn("[BotApp] Tauri API not available");
|
||||
return;
|
||||
}
|
||||
|
||||
const { listen } = window.__TAURI__.event;
|
||||
|
||||
// Listen for upload progress events
|
||||
listen('upload_progress', (event) => {
|
||||
listen("upload_progress", (event) => {
|
||||
const progress = event.payload;
|
||||
const progressEl = document.getElementById('upload-progress');
|
||||
const progressEl = document.getElementById("upload-progress");
|
||||
if (progressEl) {
|
||||
progressEl.style.width = `${progress}%`;
|
||||
progressEl.textContent = `${Math.round(progress)}%`;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[BotApp] Tauri event listeners registered');
|
||||
console.log("[BotApp] Tauri event listeners registered");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize BotApp extensions
|
||||
*/
|
||||
function init() {
|
||||
console.log('[BotApp] Initializing app extensions...');
|
||||
|
||||
// Inject styles
|
||||
console.log("[BotApp] Initializing app extensions...");
|
||||
injectStyles();
|
||||
|
||||
// Inject app guides
|
||||
injectAppGuides();
|
||||
|
||||
// Setup Tauri events
|
||||
setupTauriEvents();
|
||||
|
||||
console.log('[BotApp] App extensions initialized');
|
||||
console.log("[BotApp] App extensions initialized");
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// Expose BotApp API globally
|
||||
window.BotApp = {
|
||||
isApp: true,
|
||||
version: '6.1.0',
|
||||
version: "6.1.0",
|
||||
guides: APP_GUIDES,
|
||||
|
||||
// Invoke Tauri commands
|
||||
invoke: async function(cmd, args) {
|
||||
invoke: async function (cmd, args) {
|
||||
if (!window.__TAURI__) {
|
||||
throw new Error('Tauri API not available');
|
||||
throw new Error("Tauri API not available");
|
||||
}
|
||||
return window.__TAURI__.core.invoke(cmd, args);
|
||||
},
|
||||
|
||||
// File system helpers
|
||||
fs: {
|
||||
listFiles: (path) => window.BotApp.invoke('list_files', { path }),
|
||||
uploadFile: (srcPath, destPath) => window.BotApp.invoke('upload_file', { srcPath, destPath }),
|
||||
createFolder: (path, name) => window.BotApp.invoke('create_folder', { path, name }),
|
||||
deletePath: (path) => window.BotApp.invoke('delete_path', { path }),
|
||||
getHomeDir: () => window.BotApp.invoke('get_home_dir'),
|
||||
listFiles: (path) => window.BotApp.invoke("list_files", { path }),
|
||||
uploadFile: (srcPath, destPath) =>
|
||||
window.BotApp.invoke("upload_file", { srcPath, destPath }),
|
||||
createFolder: (path, name) =>
|
||||
window.BotApp.invoke("create_folder", { path, name }),
|
||||
deletePath: (path) => window.BotApp.invoke("delete_path", { path }),
|
||||
getHomeDir: () => window.BotApp.invoke("get_home_dir"),
|
||||
},
|
||||
|
||||
// Show native notification
|
||||
notify: async function(title, body) {
|
||||
notify: async function (title, body) {
|
||||
if (window.__TAURI__?.notification) {
|
||||
await window.__TAURI__.notification.sendNotification({ title, body });
|
||||
}
|
||||
},
|
||||
|
||||
// Open native file dialog
|
||||
openFileDialog: async function(options = {}) {
|
||||
openFileDialog: async function (options = {}) {
|
||||
if (!window.__TAURI__?.dialog) {
|
||||
throw new Error('Dialog API not available');
|
||||
throw new Error("Dialog API not available");
|
||||
}
|
||||
return window.__TAURI__.dialog.open(options);
|
||||
},
|
||||
|
||||
// Open native save dialog
|
||||
saveFileDialog: async function(options = {}) {
|
||||
saveFileDialog: async function (options = {}) {
|
||||
if (!window.__TAURI__?.dialog) {
|
||||
throw new Error('Dialog API not available');
|
||||
throw new Error("Dialog API not available");
|
||||
}
|
||||
return window.__TAURI__.dialog.save(options);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ use std::fs;
|
|||
use std::path::{Path, PathBuf};
|
||||
use tauri::{Emitter, Window};
|
||||
|
||||
/// Represents a file or directory item
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct FileItem {
|
||||
pub name: String,
|
||||
|
|
@ -16,7 +15,6 @@ pub struct FileItem {
|
|||
pub size: Option<u64>,
|
||||
}
|
||||
|
||||
/// List files in a directory
|
||||
#[tauri::command]
|
||||
pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
||||
let base_path = Path::new(path);
|
||||
|
|
@ -48,7 +46,6 @@ pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
|||
});
|
||||
}
|
||||
|
||||
// Sort: directories first, then alphabetically
|
||||
files.sort_by(|a, b| {
|
||||
if a.is_dir && !b.is_dir {
|
||||
std::cmp::Ordering::Less
|
||||
|
|
@ -62,7 +59,6 @@ pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
|||
Ok(files)
|
||||
}
|
||||
|
||||
/// Upload a file with progress reporting
|
||||
#[tauri::command]
|
||||
pub async fn upload_file(
|
||||
window: Window,
|
||||
|
|
@ -107,7 +103,6 @@ pub async fn upload_file(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a new folder
|
||||
#[tauri::command]
|
||||
pub fn create_folder(path: String, name: String) -> Result<(), String> {
|
||||
let full_path = Path::new(&path).join(&name);
|
||||
|
|
@ -120,7 +115,6 @@ pub fn create_folder(path: String, name: String) -> Result<(), String> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a file or folder
|
||||
#[tauri::command]
|
||||
pub fn delete_path(path: String) -> Result<(), String> {
|
||||
let target = Path::new(&path);
|
||||
|
|
@ -138,7 +132,6 @@ pub fn delete_path(path: String) -> Result<(), String> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Get home directory
|
||||
#[tauri::command]
|
||||
pub fn get_home_dir() -> Result<String, String> {
|
||||
dirs::home_dir()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
//! Desktop-specific functionality for BotApp
|
||||
//!
|
||||
//! This module provides native desktop capabilities:
|
||||
//! - Drive/file management via Tauri
|
||||
//! - System tray integration
|
||||
//! - Rclone-based file synchronization (desktop only)
|
||||
|
||||
pub mod drive;
|
||||
pub mod sync;
|
||||
pub mod tray;
|
||||
|
|
|
|||
|
|
@ -11,10 +11,8 @@ use std::process::{Child, Command, Stdio};
|
|||
use std::sync::Mutex;
|
||||
use tauri::{Emitter, Window};
|
||||
|
||||
/// Global state for tracking the rclone process
|
||||
static RCLONE_PROCESS: Mutex<Option<Child>> = Mutex::new(None);
|
||||
|
||||
/// Sync status reported to the UI
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SyncStatus {
|
||||
pub status: String,
|
||||
|
|
@ -26,7 +24,6 @@ pub struct SyncStatus {
|
|||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Sync configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SyncConfig {
|
||||
pub local_path: String,
|
||||
|
|
@ -36,14 +33,10 @@ pub struct SyncConfig {
|
|||
pub exclude_patterns: Vec<String>,
|
||||
}
|
||||
|
||||
/// Sync direction/mode
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum SyncMode {
|
||||
/// Local changes pushed to remote
|
||||
Push,
|
||||
/// Remote changes pulled to local
|
||||
Pull,
|
||||
/// Bidirectional sync (newest wins)
|
||||
Bisync,
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +59,6 @@ impl Default for SyncConfig {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get current sync status
|
||||
#[tauri::command]
|
||||
pub fn get_sync_status() -> SyncStatus {
|
||||
let process_guard = RCLONE_PROCESS.lock().unwrap();
|
||||
|
|
@ -87,12 +79,10 @@ pub fn get_sync_status() -> SyncStatus {
|
|||
}
|
||||
}
|
||||
|
||||
/// Start rclone sync process
|
||||
#[tauri::command]
|
||||
pub async fn start_sync(window: Window, config: Option<SyncConfig>) -> Result<SyncStatus, String> {
|
||||
let config = config.unwrap_or_default();
|
||||
|
||||
// Check if already running
|
||||
{
|
||||
let process_guard = RCLONE_PROCESS.lock().unwrap();
|
||||
if process_guard.is_some() {
|
||||
|
|
@ -100,17 +90,14 @@ pub async fn start_sync(window: Window, config: Option<SyncConfig>) -> Result<Sy
|
|||
}
|
||||
}
|
||||
|
||||
// Ensure local directory exists
|
||||
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))?;
|
||||
}
|
||||
|
||||
// Build rclone command
|
||||
let mut cmd = Command::new("rclone");
|
||||
|
||||
// Set sync mode
|
||||
match config.sync_mode {
|
||||
SyncMode::Push => {
|
||||
cmd.arg("sync");
|
||||
|
|
@ -126,22 +113,18 @@ pub async fn start_sync(window: Window, config: Option<SyncConfig>) -> Result<Sy
|
|||
cmd.arg("bisync");
|
||||
cmd.arg(&config.local_path);
|
||||
cmd.arg(format!("{}:{}", config.remote_name, config.remote_path));
|
||||
cmd.arg("--resync"); // First run needs resync
|
||||
cmd.arg("--resync");
|
||||
}
|
||||
}
|
||||
|
||||
// Add common options
|
||||
cmd.arg("--progress").arg("--verbose").arg("--checksum"); // Use checksums for accuracy
|
||||
cmd.arg("--progress").arg("--verbose").arg("--checksum");
|
||||
|
||||
// Add exclude patterns
|
||||
for pattern in &config.exclude_patterns {
|
||||
cmd.arg("--exclude").arg(pattern);
|
||||
}
|
||||
|
||||
// Configure output capture
|
||||
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
|
||||
// Spawn the process
|
||||
let child = cmd.spawn().map_err(|e| {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
"rclone not found. Please install rclone: https://rclone.org/install/".to_string()
|
||||
|
|
@ -150,16 +133,13 @@ pub async fn start_sync(window: Window, config: Option<SyncConfig>) -> Result<Sy
|
|||
}
|
||||
})?;
|
||||
|
||||
// Store the process handle
|
||||
{
|
||||
let mut process_guard = RCLONE_PROCESS.lock().unwrap();
|
||||
*process_guard = Some(child);
|
||||
}
|
||||
|
||||
// Emit started event
|
||||
let _ = window.emit("sync_started", ());
|
||||
|
||||
// Spawn a task to monitor the process
|
||||
let window_clone = window.clone();
|
||||
std::thread::spawn(move || {
|
||||
monitor_sync_process(window_clone);
|
||||
|
|
@ -176,13 +156,11 @@ pub async fn start_sync(window: Window, config: Option<SyncConfig>) -> Result<Sy
|
|||
})
|
||||
}
|
||||
|
||||
/// Stop rclone sync process
|
||||
#[tauri::command]
|
||||
pub fn stop_sync() -> Result<SyncStatus, String> {
|
||||
let mut process_guard = RCLONE_PROCESS.lock().unwrap();
|
||||
|
||||
if let Some(mut child) = process_guard.take() {
|
||||
// Try graceful termination first
|
||||
#[cfg(unix)]
|
||||
{
|
||||
unsafe {
|
||||
|
|
@ -195,10 +173,8 @@ pub fn stop_sync() -> Result<SyncStatus, String> {
|
|||
let _ = child.kill();
|
||||
}
|
||||
|
||||
// Wait briefly for graceful shutdown
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
|
||||
// Force kill if still running
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
|
||||
|
|
@ -216,7 +192,6 @@ pub fn stop_sync() -> Result<SyncStatus, String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Configure rclone remote for S3/MinIO
|
||||
#[tauri::command]
|
||||
pub fn configure_remote(
|
||||
remote_name: String,
|
||||
|
|
@ -225,7 +200,6 @@ pub fn configure_remote(
|
|||
secret_key: String,
|
||||
bucket: String,
|
||||
) -> Result<(), String> {
|
||||
// Use rclone config create command
|
||||
let output = Command::new("rclone")
|
||||
.args([
|
||||
"config",
|
||||
|
|
@ -251,7 +225,6 @@ pub fn configure_remote(
|
|||
return Err(format!("rclone config failed: {}", stderr));
|
||||
}
|
||||
|
||||
// Set default bucket path
|
||||
let _ = Command::new("rclone")
|
||||
.args(["config", "update", &remote_name, "bucket", &bucket])
|
||||
.output();
|
||||
|
|
@ -259,7 +232,6 @@ pub fn configure_remote(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if rclone is installed
|
||||
#[tauri::command]
|
||||
pub fn check_rclone_installed() -> Result<String, String> {
|
||||
let output = Command::new("rclone")
|
||||
|
|
@ -282,7 +254,6 @@ pub fn check_rclone_installed() -> Result<String, String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// List configured rclone remotes
|
||||
#[tauri::command]
|
||||
pub fn list_remotes() -> Result<Vec<String>, String> {
|
||||
let output = Command::new("rclone")
|
||||
|
|
@ -302,7 +273,6 @@ pub fn list_remotes() -> Result<Vec<String>, String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get sync folder path
|
||||
#[tauri::command]
|
||||
pub fn get_sync_folder() -> String {
|
||||
dirs::home_dir()
|
||||
|
|
@ -310,7 +280,6 @@ pub fn get_sync_folder() -> String {
|
|||
.unwrap_or_else(|| "~/GeneralBots".to_string())
|
||||
}
|
||||
|
||||
/// Set sync folder path
|
||||
#[tauri::command]
|
||||
pub fn set_sync_folder(path: String) -> Result<(), String> {
|
||||
let path = PathBuf::from(&path);
|
||||
|
|
@ -323,12 +292,9 @@ pub fn set_sync_folder(path: String) -> Result<(), String> {
|
|||
return Err("Path is not a directory".to_string());
|
||||
}
|
||||
|
||||
// Store in app config (would need app handle for persistent storage)
|
||||
// For now, just validate the path
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Monitor the sync process and emit events
|
||||
fn monitor_sync_process(window: Window) {
|
||||
loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
|
|
@ -338,7 +304,6 @@ fn monitor_sync_process(window: Window) {
|
|||
if let Some(ref mut child) = *process_guard {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
// Process finished
|
||||
let success = status.success();
|
||||
*process_guard = None;
|
||||
|
||||
|
|
@ -364,7 +329,6 @@ fn monitor_sync_process(window: Window) {
|
|||
break;
|
||||
}
|
||||
Ok(None) => {
|
||||
// Still running - emit progress
|
||||
let status = SyncStatus {
|
||||
status: "syncing".to_string(),
|
||||
is_running: true,
|
||||
|
|
@ -377,7 +341,6 @@ fn monitor_sync_process(window: Window) {
|
|||
let _ = window.emit("sync_progress", &status);
|
||||
}
|
||||
Err(e) => {
|
||||
// Error checking status
|
||||
*process_guard = None;
|
||||
|
||||
let status = SyncStatus {
|
||||
|
|
@ -395,8 +358,8 @@ fn monitor_sync_process(window: Window) {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
// No process running
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,35 +1,47 @@
|
|||
//! System Tray functionality for BotApp
|
||||
//!
|
||||
//! Provides system tray icon and menu for desktop platforms.
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// Tray manager handles system tray icon and interactions
|
||||
#[derive(Clone)]
|
||||
pub struct TrayManager {
|
||||
hostname: Arc<RwLock<Option<String>>>,
|
||||
running_mode: RunningMode,
|
||||
tray_active: Arc<RwLock<bool>>,
|
||||
}
|
||||
|
||||
/// Running mode for the application
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum RunningMode {
|
||||
Server,
|
||||
Desktop,
|
||||
Client,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TrayEvent {
|
||||
Open,
|
||||
Settings,
|
||||
About,
|
||||
Quit,
|
||||
}
|
||||
|
||||
impl TrayManager {
|
||||
/// Create a new TrayManager
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
hostname: Arc::new(RwLock::new(None)),
|
||||
running_mode: RunningMode::Desktop,
|
||||
tray_active: Arc::new(RwLock::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_mode(mode: RunningMode) -> Self {
|
||||
Self {
|
||||
hostname: Arc::new(RwLock::new(None)),
|
||||
running_mode: mode,
|
||||
tray_active: Arc::new(RwLock::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the tray icon
|
||||
pub async fn start(&self) -> Result<()> {
|
||||
match self.running_mode {
|
||||
RunningMode::Desktop => {
|
||||
|
|
@ -39,7 +51,7 @@ impl TrayManager {
|
|||
log::info!("Running in server mode - tray icon disabled");
|
||||
}
|
||||
RunningMode::Client => {
|
||||
log::info!("Running in client mode - tray icon minimal");
|
||||
self.start_client_mode().await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -47,12 +59,53 @@ impl TrayManager {
|
|||
|
||||
async fn start_desktop_mode(&self) -> Result<()> {
|
||||
log::info!("Starting desktop mode tray icon");
|
||||
// Platform-specific tray implementation would go here
|
||||
// For now, this is a placeholder
|
||||
|
||||
let mut active = self.tray_active.write().await;
|
||||
*active = true;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
self.setup_linux_tray().await?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.setup_windows_tray().await?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
self.setup_macos_tray().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_client_mode(&self) -> Result<()> {
|
||||
log::info!("Starting client mode with minimal tray");
|
||||
let mut active = self.tray_active.write().await;
|
||||
*active = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn setup_linux_tray(&self) -> Result<()> {
|
||||
log::info!("Initializing Linux system tray via DBus/StatusNotifierItem");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn setup_windows_tray(&self) -> Result<()> {
|
||||
log::info!("Initializing Windows system tray via Shell_NotifyIcon");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn setup_macos_tray(&self) -> Result<()> {
|
||||
log::info!("Initializing macOS menu bar via NSStatusItem");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get mode as string
|
||||
pub fn get_mode_string(&self) -> String {
|
||||
match self.running_mode {
|
||||
RunningMode::Desktop => "Desktop".to_string(),
|
||||
|
|
@ -61,17 +114,85 @@ impl TrayManager {
|
|||
}
|
||||
}
|
||||
|
||||
/// Update tray status
|
||||
pub async fn update_status(&self, status: &str) -> Result<()> {
|
||||
log::info!("Tray status update: {}", status);
|
||||
let active = self.tray_active.read().await;
|
||||
if *active {
|
||||
log::info!("Tray status: {}", status);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_tooltip(&self, tooltip: &str) -> Result<()> {
|
||||
let active = self.tray_active.read().await;
|
||||
if *active {
|
||||
log::debug!("Tray tooltip: {}", tooltip);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let _ = std::process::Command::new("notify-send")
|
||||
.arg(title)
|
||||
.arg(body)
|
||||
.spawn();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let script = format!("display notification \"{}\" with title \"{}\"", body, title);
|
||||
let _ = std::process::Command::new("osascript")
|
||||
.arg("-e")
|
||||
.arg(&script)
|
||||
.spawn();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get 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);
|
||||
}
|
||||
|
||||
pub async fn stop(&self) -> Result<()> {
|
||||
let mut active = self.tray_active.write().await;
|
||||
*active = false;
|
||||
log::info!("Tray manager stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn is_active(&self) -> bool {
|
||||
let active = self.tray_active.read().await;
|
||||
*active
|
||||
}
|
||||
|
||||
pub fn handle_event(&self, event: TrayEvent) {
|
||||
match event {
|
||||
TrayEvent::Open => {
|
||||
log::info!("Tray event: Open main window");
|
||||
}
|
||||
TrayEvent::Settings => {
|
||||
log::info!("Tray event: Open settings");
|
||||
}
|
||||
TrayEvent::About => {
|
||||
log::info!("Tray event: Show about dialog");
|
||||
}
|
||||
TrayEvent::Quit => {
|
||||
log::info!("Tray event: Quit application");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TrayManager {
|
||||
|
|
@ -80,13 +201,11 @@ impl Default for TrayManager {
|
|||
}
|
||||
}
|
||||
|
||||
/// Service status monitor
|
||||
pub struct ServiceMonitor {
|
||||
services: Vec<ServiceStatus>,
|
||||
}
|
||||
|
||||
/// Status of a service
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ServiceStatus {
|
||||
pub name: String,
|
||||
pub running: bool,
|
||||
|
|
@ -95,7 +214,6 @@ pub struct ServiceStatus {
|
|||
}
|
||||
|
||||
impl ServiceMonitor {
|
||||
/// Create a new service monitor with default services
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
services: vec![
|
||||
|
|
@ -115,7 +233,15 @@ impl ServiceMonitor {
|
|||
}
|
||||
}
|
||||
|
||||
/// Check all services
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn check_services(&mut self) -> Vec<ServiceStatus> {
|
||||
for service in &mut self.services {
|
||||
service.running = Self::check_service(&service.url).await;
|
||||
|
|
@ -123,28 +249,41 @@ impl ServiceMonitor {
|
|||
self.services.clone()
|
||||
}
|
||||
|
||||
async fn check_service(url: &str) -> bool {
|
||||
if url.starts_with("http://") || url.starts_with("https://") {
|
||||
match reqwest::Client::builder()
|
||||
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()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(std::time::Duration::from_secs(2))
|
||||
.build()
|
||||
{
|
||||
Ok(client) => {
|
||||
match client
|
||||
.get(format!("{}/health", url))
|
||||
.timeout(std::time::Duration::from_secs(2))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => true,
|
||||
Ok(c) => c,
|
||||
Err(_) => 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
|
||||
pub fn get_service(&self, name: &str) -> Option<&ServiceStatus> {
|
||||
self.services.iter().find(|s| s.name == name)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
|
||||
pub fn all_running(&self) -> bool {
|
||||
self.services.iter().all(|s| s.running)
|
||||
}
|
||||
|
||||
pub fn any_running(&self) -> bool {
|
||||
self.services.iter().any(|s| s.running)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1 @@
|
|||
//! BotApp - Tauri wrapper for General Bots
|
||||
//!
|
||||
//! This crate wraps botui (pure web) with Tauri for desktop/mobile:
|
||||
//! - Native file system access
|
||||
//! - System tray
|
||||
//! - Native dialogs
|
||||
//! - App-specific guides
|
||||
|
||||
pub mod desktop;
|
||||
|
|
|
|||
198
src/main.rs
198
src/main.rs
|
|
@ -1,29 +1,173 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
//! BotApp - Tauri Desktop Application for General Bots
|
||||
//!
|
||||
//! This is the entry point for the native desktop application.
|
||||
//! It wraps botui's pure web UI with Tauri for native capabilities.
|
||||
|
||||
use log::info;
|
||||
use tauri::Manager;
|
||||
|
||||
mod desktop;
|
||||
|
||||
use desktop::tray::{RunningMode, ServiceMonitor, TrayEvent, TrayManager};
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_tray_status(tray: tauri::State<'_, TrayManager>) -> Result<bool, String> {
|
||||
Ok(tray.is_active().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn start_tray(tray: tauri::State<'_, TrayManager>) -> Result<(), String> {
|
||||
tray.start().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn stop_tray(tray: tauri::State<'_, TrayManager>) -> Result<(), String> {
|
||||
tray.stop().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn show_notification(
|
||||
tray: tauri::State<'_, TrayManager>,
|
||||
title: String,
|
||||
body: String,
|
||||
) -> Result<(), String> {
|
||||
tray.show_notification(&title, &body)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn update_tray_status(
|
||||
tray: tauri::State<'_, TrayManager>,
|
||||
status: String,
|
||||
) -> Result<(), String> {
|
||||
tray.update_status(&status).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn set_tray_tooltip(
|
||||
tray: tauri::State<'_, TrayManager>,
|
||||
tooltip: String,
|
||||
) -> Result<(), String> {
|
||||
tray.set_tooltip(&tooltip).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_tray_hostname(tray: tauri::State<'_, TrayManager>) -> Result<Option<String>, String> {
|
||||
Ok(tray.get_hostname().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn set_tray_hostname(
|
||||
tray: tauri::State<'_, TrayManager>,
|
||||
hostname: String,
|
||||
) -> Result<(), String> {
|
||||
tray.set_hostname(hostname).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn handle_tray_event(tray: tauri::State<'_, TrayManager>, event: String) -> Result<(), String> {
|
||||
let tray_event = match event.as_str() {
|
||||
"open" => TrayEvent::Open,
|
||||
"settings" => TrayEvent::Settings,
|
||||
"about" => TrayEvent::About,
|
||||
"quit" => TrayEvent::Quit,
|
||||
_ => return Err(format!("Unknown event: {}", event)),
|
||||
};
|
||||
tray.handle_event(tray_event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
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)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn add_service(
|
||||
monitor: tauri::State<'_, tokio::sync::Mutex<ServiceMonitor>>,
|
||||
name: String,
|
||||
port: u16,
|
||||
) -> Result<(), String> {
|
||||
let mut monitor = monitor.lock().await;
|
||||
monitor.add_service(&name, port);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
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())
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_tray_mode(tray: tauri::State<'_, TrayManager>) -> String {
|
||||
tray.get_mode_string()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_running_modes() -> Vec<&'static str> {
|
||||
vec!["Server", "Desktop", "Client"]
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn create_tray_with_mode(mode: String) -> Result<String, String> {
|
||||
let running_mode = match mode.to_lowercase().as_str() {
|
||||
"server" => RunningMode::Server,
|
||||
"desktop" => RunningMode::Desktop,
|
||||
"client" => RunningMode::Client,
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"Invalid mode: {}. Use Server, Desktop, or Client",
|
||||
mode
|
||||
))
|
||||
}
|
||||
};
|
||||
let manager = TrayManager::with_mode(running_mode);
|
||||
Ok(manager.get_mode_string())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
info!("BotApp starting (Tauri)...");
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
||||
.format_timestamp_millis()
|
||||
.init();
|
||||
|
||||
info!("BotApp {} starting...", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
let tray_manager = TrayManager::with_mode(RunningMode::Desktop);
|
||||
let service_monitor = tokio::sync::Mutex::new(ServiceMonitor::new());
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.manage(tray_manager)
|
||||
.manage(service_monitor)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// Drive commands
|
||||
desktop::drive::list_files,
|
||||
desktop::drive::upload_file,
|
||||
desktop::drive::create_folder,
|
||||
desktop::drive::delete_path,
|
||||
desktop::drive::get_home_dir,
|
||||
// Sync commands (rclone-based)
|
||||
desktop::sync::get_sync_status,
|
||||
desktop::sync::start_sync,
|
||||
desktop::sync::stop_sync,
|
||||
|
|
@ -32,11 +176,41 @@ fn main() {
|
|||
desktop::sync::list_remotes,
|
||||
desktop::sync::get_sync_folder,
|
||||
desktop::sync::set_sync_folder,
|
||||
get_tray_status,
|
||||
start_tray,
|
||||
stop_tray,
|
||||
show_notification,
|
||||
update_tray_status,
|
||||
set_tray_tooltip,
|
||||
get_tray_hostname,
|
||||
set_tray_hostname,
|
||||
handle_tray_event,
|
||||
check_services,
|
||||
add_service,
|
||||
get_service,
|
||||
all_services_running,
|
||||
any_service_running,
|
||||
get_tray_mode,
|
||||
get_running_modes,
|
||||
create_tray_with_mode,
|
||||
])
|
||||
.setup(|_app| {
|
||||
info!("BotApp setup complete");
|
||||
.setup(|app| {
|
||||
let tray = app.state::<TrayManager>();
|
||||
let mode = tray.get_mode_string();
|
||||
info!("BotApp setup complete in {} mode", mode);
|
||||
|
||||
let tray_clone = tray.inner().clone();
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
if let Err(e) = tray_clone.start().await {
|
||||
log::error!("Failed to start tray: {}", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
.expect("Failed to run BotApp");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue