diff --git a/js/app-extensions.js b/js/app-extensions.js index cbf6a8f..298c2ab 100644 --- a/js/app-extensions.js +++ b/js/app-extensions.js @@ -1,38 +1,30 @@ -// 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', - icon: ` + const APP_GUIDES = [ + { + id: "local-files", + label: "Local Files", + icon: ` `, - hxGet: '/app/guides/local-files.html', - description: 'Access and manage files on your device' - }, - { - id: 'native-settings', - label: 'App Settings', - icon: ` + hxGet: "/app/guides/local-files.html", + description: "Access and manage files on your device", + }, + { + id: "native-settings", + label: "App Settings", + icon: ` `, - 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 = ` + const APP_STYLES = ` .app-grid-separator { grid-column: 1 / -1; display: flex; @@ -66,158 +58,127 @@ } `; - /** - * Inject app-specific styles - */ - function injectStyles() { - const styleEl = document.createElement('style'); - styleEl.id = 'botapp-styles'; - styleEl.textContent = APP_STYLES; - document.head.appendChild(styleEl); + function injectStyles() { + const styleEl = document.createElement("style"); + styleEl.id = "botapp-styles"; + styleEl.textContent = APP_STYLES; + document.head.appendChild(styleEl); + } + + function injectAppGuides() { + const grid = document.querySelector(".app-grid"); + if (!grid) { + setTimeout(injectAppGuides, 100); + return; } - /** - * Inject app-only guides into the suite navigation - */ - function injectAppGuides() { - const grid = document.querySelector('.app-grid'); - if (!grid) { - // Retry if grid not found yet (page still loading) - setTimeout(injectAppGuides, 100); - return; - } + if (document.querySelector(".app-grid-separator")) { + return; + } - // Check if already injected - if (document.querySelector('.app-grid-separator')) { - return; - } + const separator = document.createElement("div"); + separator.className = "app-grid-separator"; + separator.innerHTML = "Desktop Features"; + grid.appendChild(separator); - // Add separator - const separator = document.createElement('div'); - separator.className = 'app-grid-separator'; - separator.innerHTML = 'Desktop Features'; - grid.appendChild(separator); - - // Add app-only guides - 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.innerHTML = ` + 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.innerHTML = ` ${guide.label} `; - grid.appendChild(item); - }); + grid.appendChild(item); + }); - // Re-process HTMX on new elements - if (window.htmx) { - htmx.process(grid); - } - - console.log('[BotApp] App guides injected successfully'); + if (window.htmx) { + htmx.process(grid); } - /** - * Setup Tauri event listeners - */ - function setupTauriEvents() { - if (!window.__TAURI__) { - console.warn('[BotApp] Tauri API not available'); - return; - } + console.log("[BotApp] App guides injected successfully"); + } - const { listen } = window.__TAURI__.event; - - // Listen for upload progress events - listen('upload_progress', (event) => { - const progress = event.payload; - const progressEl = document.getElementById('upload-progress'); - if (progressEl) { - progressEl.style.width = `${progress}%`; - progressEl.textContent = `${Math.round(progress)}%`; - } - }); - - console.log('[BotApp] Tauri event listeners registered'); + function setupTauriEvents() { + if (!window.__TAURI__) { + console.warn("[BotApp] Tauri API not available"); + return; } - /** - * Initialize BotApp extensions - */ - function init() { - console.log('[BotApp] Initializing app extensions...'); + const { listen } = window.__TAURI__.event; - // Inject styles - injectStyles(); + listen("upload_progress", (event) => { + const progress = event.payload; + const progressEl = document.getElementById("upload-progress"); + if (progressEl) { + progressEl.style.width = `${progress}%`; + progressEl.textContent = `${Math.round(progress)}%`; + } + }); - // Inject app guides - injectAppGuides(); + console.log("[BotApp] Tauri event listeners registered"); + } - // Setup Tauri events - setupTauriEvents(); + function init() { + console.log("[BotApp] Initializing app extensions..."); + injectStyles(); + injectAppGuides(); + setupTauriEvents(); + console.log("[BotApp] App extensions initialized"); + } - console.log('[BotApp] App extensions initialized'); - } + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } - // Initialize when DOM is ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); - } else { - init(); - } + window.BotApp = { + isApp: true, + version: "6.1.0", + guides: APP_GUIDES, - // Expose BotApp API globally - window.BotApp = { - isApp: true, - version: '6.1.0', - guides: APP_GUIDES, + invoke: async function (cmd, args) { + if (!window.__TAURI__) { + throw new Error("Tauri API not available"); + } + return window.__TAURI__.core.invoke(cmd, args); + }, - // Invoke Tauri commands - invoke: async function(cmd, args) { - if (!window.__TAURI__) { - throw new Error('Tauri API not available'); - } - return window.__TAURI__.core.invoke(cmd, args); - }, + 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"), + }, - // 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'), - }, + notify: async function (title, body) { + if (window.__TAURI__?.notification) { + await window.__TAURI__.notification.sendNotification({ title, body }); + } + }, - // Show native notification - notify: async function(title, body) { - if (window.__TAURI__?.notification) { - await window.__TAURI__.notification.sendNotification({ title, body }); - } - }, - - // Open native file dialog - openFileDialog: async function(options = {}) { - if (!window.__TAURI__?.dialog) { - throw new Error('Dialog API not available'); - } - return window.__TAURI__.dialog.open(options); - }, - - // Open native save dialog - saveFileDialog: async function(options = {}) { - if (!window.__TAURI__?.dialog) { - throw new Error('Dialog API not available'); - } - return window.__TAURI__.dialog.save(options); - } - }; + openFileDialog: async function (options = {}) { + if (!window.__TAURI__?.dialog) { + throw new Error("Dialog API not available"); + } + return window.__TAURI__.dialog.open(options); + }, + saveFileDialog: async function (options = {}) { + if (!window.__TAURI__?.dialog) { + throw new Error("Dialog API not available"); + } + return window.__TAURI__.dialog.save(options); + }, + }; })(); diff --git a/src/desktop/drive.rs b/src/desktop/drive.rs index aa1dee6..8b227cd 100644 --- a/src/desktop/drive.rs +++ b/src/desktop/drive.rs @@ -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, } -/// List files in a directory #[tauri::command] pub fn list_files(path: &str) -> Result, String> { let base_path = Path::new(path); @@ -48,7 +46,6 @@ pub fn list_files(path: &str) -> Result, 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, 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 { dirs::home_dir() diff --git a/src/desktop/mod.rs b/src/desktop/mod.rs index a0f76c7..b161c02 100644 --- a/src/desktop/mod.rs +++ b/src/desktop/mod.rs @@ -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; diff --git a/src/desktop/sync.rs b/src/desktop/sync.rs index dd56780..1c05e89 100644 --- a/src/desktop/sync.rs +++ b/src/desktop/sync.rs @@ -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> = 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, } -/// 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, } -/// 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) -> Result { 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) -> Result { cmd.arg("sync"); @@ -126,22 +113,18 @@ pub async fn start_sync(window: Window, config: Option) -> Result) -> Result) -> Result Result { 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 { 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 { } } -/// 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 { let output = Command::new("rclone") @@ -282,7 +254,6 @@ pub fn check_rclone_installed() -> Result { } } -/// List configured rclone remotes #[tauri::command] pub fn list_remotes() -> Result, String> { let output = Command::new("rclone") @@ -302,7 +273,6 @@ pub fn list_remotes() -> Result, 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; } } } + diff --git a/src/desktop/tray.rs b/src/desktop/tray.rs index 8410617..4e6b32e 100644 --- a/src/desktop/tray.rs +++ b/src/desktop/tray.rs @@ -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>>, running_mode: RunningMode, + tray_active: Arc>, } -/// 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 { 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, } -/// 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 { 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() - .danger_accept_invalid_certs(true) - .build() - { - Ok(client) => { - match client - .get(format!("{}/health", url)) - .timeout(std::time::Duration::from_secs(2)) - .send() - .await - { - Ok(_) => true, - Err(_) => false, - } - } - Err(_) => false, - } - } else { - false + 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(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, + }, + } + } + + pub fn get_service(&self, name: &str) -> Option<&ServiceStatus> { + self.services.iter().find(|s| s.name == name) + } + + 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) } } diff --git a/src/lib.rs b/src/lib.rs index 78eda69..6cf24c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/main.rs b/src/main.rs index b842fe4..99060c7 100644 --- a/src/main.rs +++ b/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 { + 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, 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>, +) -> Result, String> { + let mut monitor = monitor.lock().await; + Ok(monitor.check_services().await) +} + +#[tauri::command] +async fn add_service( + monitor: tauri::State<'_, tokio::sync::Mutex>, + 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>, + name: String, +) -> Result, 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>, +) -> Result { + let monitor = monitor.lock().await; + Ok(monitor.all_running()) +} + +#[tauri::command] +async fn any_service_running( + monitor: tauri::State<'_, tokio::sync::Mutex>, +) -> Result { + 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 { + 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::(); + 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"); }