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,38 +1,30 @@
|
||||||
// BotApp Extensions - Injected by Tauri into botui's suite
|
(function () {
|
||||||
// Adds app-only guides and native functionality
|
"use strict";
|
||||||
//
|
|
||||||
// This script runs only in the Tauri app context and extends
|
|
||||||
// the botui suite with desktop-specific features.
|
|
||||||
|
|
||||||
(function() {
|
const APP_GUIDES = [
|
||||||
'use strict';
|
{
|
||||||
|
id: "local-files",
|
||||||
// App-only guides that will be injected into the suite navigation
|
label: "Local Files",
|
||||||
const APP_GUIDES = [
|
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
{
|
|
||||||
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="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"/>
|
<path d="M12 11v6M9 14h6"/>
|
||||||
</svg>`,
|
</svg>`,
|
||||||
hxGet: '/app/guides/local-files.html',
|
hxGet: "/app/guides/local-files.html",
|
||||||
description: 'Access and manage files on your device'
|
description: "Access and manage files on your device",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'native-settings',
|
id: "native-settings",
|
||||||
label: 'App Settings',
|
label: "App Settings",
|
||||||
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
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"/>
|
<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"/>
|
<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>`,
|
</svg>`,
|
||||||
hxGet: '/app/guides/native-settings.html',
|
hxGet: "/app/guides/native-settings.html",
|
||||||
description: 'Configure desktop app settings'
|
description: "Configure desktop app settings",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// CSS for app-only elements
|
const APP_STYLES = `
|
||||||
const APP_STYLES = `
|
|
||||||
.app-grid-separator {
|
.app-grid-separator {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -66,158 +58,127 @@
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/**
|
function injectStyles() {
|
||||||
* Inject app-specific styles
|
const styleEl = document.createElement("style");
|
||||||
*/
|
styleEl.id = "botapp-styles";
|
||||||
function injectStyles() {
|
styleEl.textContent = APP_STYLES;
|
||||||
const styleEl = document.createElement('style');
|
document.head.appendChild(styleEl);
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
if (document.querySelector(".app-grid-separator")) {
|
||||||
* Inject app-only guides into the suite navigation
|
return;
|
||||||
*/
|
}
|
||||||
function injectAppGuides() {
|
|
||||||
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
|
const separator = document.createElement("div");
|
||||||
if (document.querySelector('.app-grid-separator')) {
|
separator.className = "app-grid-separator";
|
||||||
return;
|
separator.innerHTML = "<span>Desktop Features</span>";
|
||||||
}
|
grid.appendChild(separator);
|
||||||
|
|
||||||
// Add separator
|
APP_GUIDES.forEach((guide) => {
|
||||||
const separator = document.createElement('div');
|
const item = document.createElement("a");
|
||||||
separator.className = 'app-grid-separator';
|
item.className = "app-item app-only";
|
||||||
separator.innerHTML = '<span>Desktop Features</span>';
|
item.href = `#${guide.id}`;
|
||||||
grid.appendChild(separator);
|
item.dataset.section = guide.id;
|
||||||
|
item.setAttribute("role", "menuitem");
|
||||||
// Add app-only guides
|
item.setAttribute("aria-label", guide.description || guide.label);
|
||||||
APP_GUIDES.forEach(guide => {
|
item.setAttribute("hx-get", guide.hxGet);
|
||||||
const item = document.createElement('a');
|
item.setAttribute("hx-target", "#main-content");
|
||||||
item.className = 'app-item app-only';
|
item.setAttribute("hx-push-url", "true");
|
||||||
item.href = `#${guide.id}`;
|
item.innerHTML = `
|
||||||
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 = `
|
|
||||||
<div class="app-icon" aria-hidden="true">${guide.icon}</div>
|
<div class="app-icon" aria-hidden="true">${guide.icon}</div>
|
||||||
<span>${guide.label}</span>
|
<span>${guide.label}</span>
|
||||||
`;
|
`;
|
||||||
grid.appendChild(item);
|
grid.appendChild(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-process HTMX on new elements
|
if (window.htmx) {
|
||||||
if (window.htmx) {
|
htmx.process(grid);
|
||||||
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');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { listen } = window.__TAURI__.event;
|
function setupTauriEvents() {
|
||||||
|
if (!window.__TAURI__) {
|
||||||
// Listen for upload progress events
|
console.warn("[BotApp] Tauri API not available");
|
||||||
listen('upload_progress', (event) => {
|
return;
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const { listen } = window.__TAURI__.event;
|
||||||
* Initialize BotApp extensions
|
|
||||||
*/
|
|
||||||
function init() {
|
|
||||||
console.log('[BotApp] Initializing app extensions...');
|
|
||||||
|
|
||||||
// Inject styles
|
listen("upload_progress", (event) => {
|
||||||
injectStyles();
|
const progress = event.payload;
|
||||||
|
const progressEl = document.getElementById("upload-progress");
|
||||||
|
if (progressEl) {
|
||||||
|
progressEl.style.width = `${progress}%`;
|
||||||
|
progressEl.textContent = `${Math.round(progress)}%`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Inject app guides
|
console.log("[BotApp] Tauri event listeners registered");
|
||||||
injectAppGuides();
|
}
|
||||||
|
|
||||||
// Setup Tauri events
|
function init() {
|
||||||
setupTauriEvents();
|
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
|
window.BotApp = {
|
||||||
if (document.readyState === 'loading') {
|
isApp: true,
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
version: "6.1.0",
|
||||||
} else {
|
guides: APP_GUIDES,
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expose BotApp API globally
|
invoke: async function (cmd, args) {
|
||||||
window.BotApp = {
|
if (!window.__TAURI__) {
|
||||||
isApp: true,
|
throw new Error("Tauri API not available");
|
||||||
version: '6.1.0',
|
}
|
||||||
guides: APP_GUIDES,
|
return window.__TAURI__.core.invoke(cmd, args);
|
||||||
|
},
|
||||||
|
|
||||||
// Invoke Tauri commands
|
fs: {
|
||||||
invoke: async function(cmd, args) {
|
listFiles: (path) => window.BotApp.invoke("list_files", { path }),
|
||||||
if (!window.__TAURI__) {
|
uploadFile: (srcPath, destPath) =>
|
||||||
throw new Error('Tauri API not available');
|
window.BotApp.invoke("upload_file", { srcPath, destPath }),
|
||||||
}
|
createFolder: (path, name) =>
|
||||||
return window.__TAURI__.core.invoke(cmd, args);
|
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
|
notify: async function (title, body) {
|
||||||
fs: {
|
if (window.__TAURI__?.notification) {
|
||||||
listFiles: (path) => window.BotApp.invoke('list_files', { path }),
|
await window.__TAURI__.notification.sendNotification({ title, body });
|
||||||
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
|
openFileDialog: async function (options = {}) {
|
||||||
notify: async function(title, body) {
|
if (!window.__TAURI__?.dialog) {
|
||||||
if (window.__TAURI__?.notification) {
|
throw new Error("Dialog API not available");
|
||||||
await window.__TAURI__.notification.sendNotification({ title, body });
|
}
|
||||||
}
|
return window.__TAURI__.dialog.open(options);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
saveFileDialog: async function (options = {}) {
|
||||||
|
if (!window.__TAURI__?.dialog) {
|
||||||
|
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 std::path::{Path, PathBuf};
|
||||||
use tauri::{Emitter, Window};
|
use tauri::{Emitter, Window};
|
||||||
|
|
||||||
/// Represents a file or directory item
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct FileItem {
|
pub struct FileItem {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
@ -16,7 +15,6 @@ pub struct FileItem {
|
||||||
pub size: Option<u64>,
|
pub size: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List files in a directory
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
||||||
let base_path = Path::new(path);
|
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| {
|
files.sort_by(|a, b| {
|
||||||
if a.is_dir && !b.is_dir {
|
if a.is_dir && !b.is_dir {
|
||||||
std::cmp::Ordering::Less
|
std::cmp::Ordering::Less
|
||||||
|
|
@ -62,7 +59,6 @@ pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
||||||
Ok(files)
|
Ok(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upload a file with progress reporting
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn upload_file(
|
pub async fn upload_file(
|
||||||
window: Window,
|
window: Window,
|
||||||
|
|
@ -107,7 +103,6 @@ pub async fn upload_file(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new folder
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn create_folder(path: String, name: String) -> Result<(), String> {
|
pub fn create_folder(path: String, name: String) -> Result<(), String> {
|
||||||
let full_path = Path::new(&path).join(&name);
|
let full_path = Path::new(&path).join(&name);
|
||||||
|
|
@ -120,7 +115,6 @@ pub fn create_folder(path: String, name: String) -> Result<(), String> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a file or folder
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_path(path: String) -> Result<(), String> {
|
pub fn delete_path(path: String) -> Result<(), String> {
|
||||||
let target = Path::new(&path);
|
let target = Path::new(&path);
|
||||||
|
|
@ -138,7 +132,6 @@ pub fn delete_path(path: String) -> Result<(), String> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get home directory
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_home_dir() -> Result<String, String> {
|
pub fn get_home_dir() -> Result<String, String> {
|
||||||
dirs::home_dir()
|
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 drive;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
pub mod tray;
|
pub mod tray;
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,8 @@ use std::process::{Child, Command, Stdio};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use tauri::{Emitter, Window};
|
use tauri::{Emitter, Window};
|
||||||
|
|
||||||
/// Global state for tracking the rclone process
|
|
||||||
static RCLONE_PROCESS: Mutex<Option<Child>> = Mutex::new(None);
|
static RCLONE_PROCESS: Mutex<Option<Child>> = Mutex::new(None);
|
||||||
|
|
||||||
/// Sync status reported to the UI
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SyncStatus {
|
pub struct SyncStatus {
|
||||||
pub status: String,
|
pub status: String,
|
||||||
|
|
@ -26,7 +24,6 @@ pub struct SyncStatus {
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync configuration
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SyncConfig {
|
pub struct SyncConfig {
|
||||||
pub local_path: String,
|
pub local_path: String,
|
||||||
|
|
@ -36,14 +33,10 @@ pub struct SyncConfig {
|
||||||
pub exclude_patterns: Vec<String>,
|
pub exclude_patterns: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync direction/mode
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum SyncMode {
|
pub enum SyncMode {
|
||||||
/// Local changes pushed to remote
|
|
||||||
Push,
|
Push,
|
||||||
/// Remote changes pulled to local
|
|
||||||
Pull,
|
Pull,
|
||||||
/// Bidirectional sync (newest wins)
|
|
||||||
Bisync,
|
Bisync,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,7 +59,6 @@ impl Default for SyncConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get current sync status
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_sync_status() -> SyncStatus {
|
pub fn get_sync_status() -> SyncStatus {
|
||||||
let process_guard = RCLONE_PROCESS.lock().unwrap();
|
let process_guard = RCLONE_PROCESS.lock().unwrap();
|
||||||
|
|
@ -87,12 +79,10 @@ pub fn get_sync_status() -> SyncStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start rclone sync process
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn start_sync(window: Window, config: Option<SyncConfig>) -> Result<SyncStatus, String> {
|
pub async fn start_sync(window: Window, config: Option<SyncConfig>) -> Result<SyncStatus, String> {
|
||||||
let config = config.unwrap_or_default();
|
let config = config.unwrap_or_default();
|
||||||
|
|
||||||
// Check if already running
|
|
||||||
{
|
{
|
||||||
let process_guard = RCLONE_PROCESS.lock().unwrap();
|
let process_guard = RCLONE_PROCESS.lock().unwrap();
|
||||||
if process_guard.is_some() {
|
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);
|
let local_path = PathBuf::from(&config.local_path);
|
||||||
if !local_path.exists() {
|
if !local_path.exists() {
|
||||||
std::fs::create_dir_all(&local_path)
|
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))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build rclone command
|
|
||||||
let mut cmd = Command::new("rclone");
|
let mut cmd = Command::new("rclone");
|
||||||
|
|
||||||
// Set sync mode
|
|
||||||
match config.sync_mode {
|
match config.sync_mode {
|
||||||
SyncMode::Push => {
|
SyncMode::Push => {
|
||||||
cmd.arg("sync");
|
cmd.arg("sync");
|
||||||
|
|
@ -126,22 +113,18 @@ pub async fn start_sync(window: Window, config: Option<SyncConfig>) -> Result<Sy
|
||||||
cmd.arg("bisync");
|
cmd.arg("bisync");
|
||||||
cmd.arg(&config.local_path);
|
cmd.arg(&config.local_path);
|
||||||
cmd.arg(format!("{}:{}", config.remote_name, config.remote_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");
|
||||||
cmd.arg("--progress").arg("--verbose").arg("--checksum"); // Use checksums for accuracy
|
|
||||||
|
|
||||||
// Add exclude patterns
|
|
||||||
for pattern in &config.exclude_patterns {
|
for pattern in &config.exclude_patterns {
|
||||||
cmd.arg("--exclude").arg(pattern);
|
cmd.arg("--exclude").arg(pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure output capture
|
|
||||||
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||||
|
|
||||||
// Spawn the process
|
|
||||||
let child = cmd.spawn().map_err(|e| {
|
let child = cmd.spawn().map_err(|e| {
|
||||||
if e.kind() == std::io::ErrorKind::NotFound {
|
if e.kind() == std::io::ErrorKind::NotFound {
|
||||||
"rclone not found. Please install rclone: https://rclone.org/install/".to_string()
|
"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();
|
let mut process_guard = RCLONE_PROCESS.lock().unwrap();
|
||||||
*process_guard = Some(child);
|
*process_guard = Some(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit started event
|
|
||||||
let _ = window.emit("sync_started", ());
|
let _ = window.emit("sync_started", ());
|
||||||
|
|
||||||
// Spawn a task to monitor the process
|
|
||||||
let window_clone = window.clone();
|
let window_clone = window.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
monitor_sync_process(window_clone);
|
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]
|
#[tauri::command]
|
||||||
pub fn stop_sync() -> Result<SyncStatus, String> {
|
pub fn stop_sync() -> Result<SyncStatus, String> {
|
||||||
let mut process_guard = RCLONE_PROCESS.lock().unwrap();
|
let mut process_guard = RCLONE_PROCESS.lock().unwrap();
|
||||||
|
|
||||||
if let Some(mut child) = process_guard.take() {
|
if let Some(mut child) = process_guard.take() {
|
||||||
// Try graceful termination first
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|
@ -195,10 +173,8 @@ pub fn stop_sync() -> Result<SyncStatus, String> {
|
||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait briefly for graceful shutdown
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||||
|
|
||||||
// Force kill if still running
|
|
||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
|
|
||||||
|
|
@ -216,7 +192,6 @@ pub fn stop_sync() -> Result<SyncStatus, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure rclone remote for S3/MinIO
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn configure_remote(
|
pub fn configure_remote(
|
||||||
remote_name: String,
|
remote_name: String,
|
||||||
|
|
@ -225,7 +200,6 @@ pub fn configure_remote(
|
||||||
secret_key: String,
|
secret_key: String,
|
||||||
bucket: String,
|
bucket: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Use rclone config create command
|
|
||||||
let output = Command::new("rclone")
|
let output = Command::new("rclone")
|
||||||
.args([
|
.args([
|
||||||
"config",
|
"config",
|
||||||
|
|
@ -251,7 +225,6 @@ pub fn configure_remote(
|
||||||
return Err(format!("rclone config failed: {}", stderr));
|
return Err(format!("rclone config failed: {}", stderr));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default bucket path
|
|
||||||
let _ = Command::new("rclone")
|
let _ = Command::new("rclone")
|
||||||
.args(["config", "update", &remote_name, "bucket", &bucket])
|
.args(["config", "update", &remote_name, "bucket", &bucket])
|
||||||
.output();
|
.output();
|
||||||
|
|
@ -259,7 +232,6 @@ pub fn configure_remote(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if rclone is installed
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn check_rclone_installed() -> Result<String, String> {
|
pub fn check_rclone_installed() -> Result<String, String> {
|
||||||
let output = Command::new("rclone")
|
let output = Command::new("rclone")
|
||||||
|
|
@ -282,7 +254,6 @@ pub fn check_rclone_installed() -> Result<String, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List configured rclone remotes
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_remotes() -> Result<Vec<String>, String> {
|
pub fn list_remotes() -> Result<Vec<String>, String> {
|
||||||
let output = Command::new("rclone")
|
let output = Command::new("rclone")
|
||||||
|
|
@ -302,7 +273,6 @@ pub fn list_remotes() -> Result<Vec<String>, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get sync folder path
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_sync_folder() -> String {
|
pub fn get_sync_folder() -> String {
|
||||||
dirs::home_dir()
|
dirs::home_dir()
|
||||||
|
|
@ -310,7 +280,6 @@ pub fn get_sync_folder() -> String {
|
||||||
.unwrap_or_else(|| "~/GeneralBots".to_string())
|
.unwrap_or_else(|| "~/GeneralBots".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set sync folder path
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn set_sync_folder(path: String) -> Result<(), String> {
|
pub fn set_sync_folder(path: String) -> Result<(), String> {
|
||||||
let path = PathBuf::from(&path);
|
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());
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Monitor the sync process and emit events
|
|
||||||
fn monitor_sync_process(window: Window) {
|
fn monitor_sync_process(window: Window) {
|
||||||
loop {
|
loop {
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
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 {
|
if let Some(ref mut child) = *process_guard {
|
||||||
match child.try_wait() {
|
match child.try_wait() {
|
||||||
Ok(Some(status)) => {
|
Ok(Some(status)) => {
|
||||||
// Process finished
|
|
||||||
let success = status.success();
|
let success = status.success();
|
||||||
*process_guard = None;
|
*process_guard = None;
|
||||||
|
|
||||||
|
|
@ -364,7 +329,6 @@ fn monitor_sync_process(window: Window) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
// Still running - emit progress
|
|
||||||
let status = SyncStatus {
|
let status = SyncStatus {
|
||||||
status: "syncing".to_string(),
|
status: "syncing".to_string(),
|
||||||
is_running: true,
|
is_running: true,
|
||||||
|
|
@ -377,7 +341,6 @@ fn monitor_sync_process(window: Window) {
|
||||||
let _ = window.emit("sync_progress", &status);
|
let _ = window.emit("sync_progress", &status);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Error checking status
|
|
||||||
*process_guard = None;
|
*process_guard = None;
|
||||||
|
|
||||||
let status = SyncStatus {
|
let status = SyncStatus {
|
||||||
|
|
@ -395,8 +358,8 @@ fn monitor_sync_process(window: Window) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No process running
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,47 @@
|
||||||
//! System Tray functionality for BotApp
|
|
||||||
//!
|
|
||||||
//! Provides system tray icon and menu for desktop platforms.
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use serde::Serialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
/// Tray manager handles system tray icon and interactions
|
#[derive(Clone)]
|
||||||
pub struct TrayManager {
|
pub struct TrayManager {
|
||||||
hostname: Arc<RwLock<Option<String>>>,
|
hostname: Arc<RwLock<Option<String>>>,
|
||||||
running_mode: RunningMode,
|
running_mode: RunningMode,
|
||||||
|
tray_active: Arc<RwLock<bool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Running mode for the application
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum RunningMode {
|
pub enum RunningMode {
|
||||||
Server,
|
Server,
|
||||||
Desktop,
|
Desktop,
|
||||||
Client,
|
Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TrayEvent {
|
||||||
|
Open,
|
||||||
|
Settings,
|
||||||
|
About,
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
impl TrayManager {
|
impl TrayManager {
|
||||||
/// Create a new TrayManager
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
hostname: Arc::new(RwLock::new(None)),
|
hostname: Arc::new(RwLock::new(None)),
|
||||||
running_mode: RunningMode::Desktop,
|
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<()> {
|
pub async fn start(&self) -> Result<()> {
|
||||||
match self.running_mode {
|
match self.running_mode {
|
||||||
RunningMode::Desktop => {
|
RunningMode::Desktop => {
|
||||||
|
|
@ -39,7 +51,7 @@ impl TrayManager {
|
||||||
log::info!("Running in server mode - tray icon disabled");
|
log::info!("Running in server mode - tray icon disabled");
|
||||||
}
|
}
|
||||||
RunningMode::Client => {
|
RunningMode::Client => {
|
||||||
log::info!("Running in client mode - tray icon minimal");
|
self.start_client_mode().await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -47,12 +59,53 @@ impl TrayManager {
|
||||||
|
|
||||||
async fn start_desktop_mode(&self) -> Result<()> {
|
async fn start_desktop_mode(&self) -> Result<()> {
|
||||||
log::info!("Starting desktop mode tray icon");
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get mode as string
|
|
||||||
pub fn get_mode_string(&self) -> String {
|
pub fn get_mode_string(&self) -> String {
|
||||||
match self.running_mode {
|
match self.running_mode {
|
||||||
RunningMode::Desktop => "Desktop".to_string(),
|
RunningMode::Desktop => "Desktop".to_string(),
|
||||||
|
|
@ -61,17 +114,85 @@ impl TrayManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update tray status
|
|
||||||
pub async fn update_status(&self, status: &str) -> Result<()> {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get hostname
|
|
||||||
pub async fn get_hostname(&self) -> Option<String> {
|
pub async fn get_hostname(&self) -> Option<String> {
|
||||||
let hostname = self.hostname.read().await;
|
let hostname = self.hostname.read().await;
|
||||||
hostname.clone()
|
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 {
|
impl Default for TrayManager {
|
||||||
|
|
@ -80,13 +201,11 @@ impl Default for TrayManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Service status monitor
|
|
||||||
pub struct ServiceMonitor {
|
pub struct ServiceMonitor {
|
||||||
services: Vec<ServiceStatus>,
|
services: Vec<ServiceStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Status of a service
|
#[derive(Debug, Clone, Serialize)]
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ServiceStatus {
|
pub struct ServiceStatus {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub running: bool,
|
pub running: bool,
|
||||||
|
|
@ -95,7 +214,6 @@ pub struct ServiceStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServiceMonitor {
|
impl ServiceMonitor {
|
||||||
/// Create a new service monitor with default services
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
services: vec![
|
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> {
|
pub async fn check_services(&mut self) -> Vec<ServiceStatus> {
|
||||||
for service in &mut self.services {
|
for service in &mut self.services {
|
||||||
service.running = Self::check_service(&service.url).await;
|
service.running = Self::check_service(&service.url).await;
|
||||||
|
|
@ -123,28 +249,41 @@ impl ServiceMonitor {
|
||||||
self.services.clone()
|
self.services.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_service(url: &str) -> bool {
|
pub async fn check_service(url: &str) -> bool {
|
||||||
if url.starts_with("http://") || url.starts_with("https://") {
|
if !url.starts_with("http://") && !url.starts_with("https://") {
|
||||||
match reqwest::Client::builder()
|
return false;
|
||||||
.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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
pub mod desktop;
|
||||||
|
|
|
||||||
198
src/main.rs
198
src/main.rs
|
|
@ -1,29 +1,173 @@
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![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 log::info;
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
mod desktop;
|
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() {
|
fn main() {
|
||||||
env_logger::init();
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
||||||
info!("BotApp starting (Tauri)...");
|
.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()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.manage(tray_manager)
|
||||||
|
.manage(service_monitor)
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
// Drive commands
|
|
||||||
desktop::drive::list_files,
|
desktop::drive::list_files,
|
||||||
desktop::drive::upload_file,
|
desktop::drive::upload_file,
|
||||||
desktop::drive::create_folder,
|
desktop::drive::create_folder,
|
||||||
desktop::drive::delete_path,
|
desktop::drive::delete_path,
|
||||||
desktop::drive::get_home_dir,
|
desktop::drive::get_home_dir,
|
||||||
// Sync commands (rclone-based)
|
|
||||||
desktop::sync::get_sync_status,
|
desktop::sync::get_sync_status,
|
||||||
desktop::sync::start_sync,
|
desktop::sync::start_sync,
|
||||||
desktop::sync::stop_sync,
|
desktop::sync::stop_sync,
|
||||||
|
|
@ -32,11 +176,41 @@ fn main() {
|
||||||
desktop::sync::list_remotes,
|
desktop::sync::list_remotes,
|
||||||
desktop::sync::get_sync_folder,
|
desktop::sync::get_sync_folder,
|
||||||
desktop::sync::set_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| {
|
.setup(|app| {
|
||||||
info!("BotApp setup complete");
|
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(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("Failed to run BotApp");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue