Initial botapp - Tauri wrapper for General Bots
This crate wraps botui (pure web UI) with Tauri for desktop/mobile: - Native file system access via Tauri commands - System tray integration (prepared) - App-specific guides injected at runtime - Desktop settings and configuration Architecture: - botui: Pure web UI (no Tauri deps) - botapp: Tauri wrapper that loads botui's suite Files: - src/desktop/drive.rs: File system commands - src/desktop/tray.rs: System tray (prepared) - js/app-extensions.js: Injects app guides into suite - ui/app-guides/: App-only HTML content
This commit is contained in:
commit
64e11506a2
20 changed files with 13327 additions and 0 deletions
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Build artifacts
|
||||
/target
|
||||
/dist
|
||||
|
||||
# Tauri generated
|
||||
/src-tauri/target
|
||||
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Lock files (keep Cargo.lock for apps)
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Debug
|
||||
debug/
|
||||
*.pdb
|
||||
|
||||
# Test coverage
|
||||
coverage/
|
||||
6725
Cargo.lock
generated
Normal file
6725
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
42
Cargo.toml
Normal file
42
Cargo.toml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
[package]
|
||||
name = "botapp"
|
||||
version = "6.1.0"
|
||||
edition = "2021"
|
||||
description = "General Bots App - Tauri wrapper for desktop/mobile"
|
||||
license = "AGPL-3.0"
|
||||
|
||||
[dependencies]
|
||||
# Core from botlib
|
||||
botlib = { path = "../botlib", features = ["http-client"] }
|
||||
|
||||
# Tauri
|
||||
tauri = { version = "2", features = ["unstable"] }
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-opener = "2"
|
||||
|
||||
# Common
|
||||
anyhow = "1.0"
|
||||
dirs = "6.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.41", features = ["full"] }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
|
||||
# Platform-specific tray
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
ksni = { version = "0.2", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
trayicon = { version = "0.2", optional = true }
|
||||
image = "0.25"
|
||||
thiserror = "2.0"
|
||||
|
||||
[features]
|
||||
default = ["desktop"]
|
||||
desktop = []
|
||||
desktop-tray = ["dep:ksni", "dep:trayicon"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = "2"
|
||||
3
LICENSE
Normal file
3
LICENSE
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
AGPL-3.0
|
||||
|
||||
See https://www.gnu.org/licenses/agpl-3.0.html
|
||||
161
README.md
Normal file
161
README.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# BotApp - General Bots Desktop Application
|
||||
|
||||
BotApp is the Tauri-based desktop wrapper for General Bots, providing native desktop and mobile capabilities on top of the pure web UI from [botui](https://github.com/GeneralBots/botui).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
botui (pure web) botapp (Tauri wrapper)
|
||||
┌─────────────────┐ ┌─────────────────────────┐
|
||||
│ suite/ │◄─────│ Loads botui's UI │
|
||||
│ minimal/ │ │ + injects app-only │
|
||||
│ shared/ │ │ features via JS │
|
||||
│ │ │ │
|
||||
│ No Tauri deps │ │ Tauri + native APIs │
|
||||
└─────────────────┘ └─────────────────────────┘
|
||||
```
|
||||
|
||||
### Why Two Projects?
|
||||
|
||||
- **botui**: Pure web UI with zero native dependencies. Works in any browser.
|
||||
- **botapp**: Wraps botui with Tauri for desktop/mobile native features.
|
||||
|
||||
This separation allows:
|
||||
- Same UI code for web, desktop, and mobile
|
||||
- Clean dependency management (web users don't need Tauri)
|
||||
- App-specific features only in the native app
|
||||
|
||||
## Features
|
||||
|
||||
BotApp adds these native capabilities to botui:
|
||||
|
||||
- **Local File Access**: Browse and manage files on your device
|
||||
- **System Tray**: Minimize to tray, background operation
|
||||
- **Native Dialogs**: File open/save dialogs
|
||||
- **Desktop Notifications**: Native OS notifications
|
||||
- **App Settings**: Desktop-specific configuration
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
botapp/
|
||||
├── Cargo.toml # Rust dependencies (includes Tauri)
|
||||
├── build.rs # Tauri build script
|
||||
├── tauri.conf.json # Tauri configuration
|
||||
├── src/
|
||||
│ ├── main.rs # Tauri entry point
|
||||
│ ├── lib.rs # Library exports
|
||||
│ └── desktop/
|
||||
│ ├── mod.rs # Desktop module
|
||||
│ ├── drive.rs # File system commands
|
||||
│ └── tray.rs # System tray functionality
|
||||
├── ui/
|
||||
│ └── app-guides/ # App-only HTML content
|
||||
│ ├── local-files.html
|
||||
│ └── native-settings.html
|
||||
└── js/
|
||||
└── app-extensions.js # Injected into botui's suite
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Rust 1.70+
|
||||
- Node.js 18+ (for Tauri CLI)
|
||||
- Tauri CLI: `cargo install tauri-cli`
|
||||
|
||||
### Platform-specific
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
sudo apt install libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
- Visual Studio Build Tools with C++ workload
|
||||
|
||||
## Development
|
||||
|
||||
1. Clone both repositories:
|
||||
```bash
|
||||
git clone https://github.com/GeneralBots/botui.git
|
||||
git clone https://github.com/GeneralBots/botapp.git
|
||||
```
|
||||
|
||||
2. Start botui's web server (required for dev):
|
||||
```bash
|
||||
cd botui
|
||||
cargo run
|
||||
```
|
||||
|
||||
3. Run botapp in development mode:
|
||||
```bash
|
||||
cd botapp
|
||||
cargo tauri dev
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### Debug Build
|
||||
```bash
|
||||
cargo tauri build --debug
|
||||
```
|
||||
|
||||
### Release Build
|
||||
```bash
|
||||
cargo tauri build
|
||||
```
|
||||
|
||||
Binaries will be in `target/release/bundle/`.
|
||||
|
||||
## How App Extensions Work
|
||||
|
||||
BotApp injects `js/app-extensions.js` into botui's suite at runtime. This script:
|
||||
|
||||
1. Detects Tauri environment (`window.__TAURI__`)
|
||||
2. Injects app-only navigation items into the suite's `.app-grid`
|
||||
3. Exposes `window.BotApp` API for native features
|
||||
|
||||
Example usage in suite:
|
||||
```javascript
|
||||
if (window.BotApp?.isApp) {
|
||||
// Running in desktop app
|
||||
const files = await BotApp.fs.listFiles('/home/user');
|
||||
await BotApp.notify('Title', 'Native notification!');
|
||||
}
|
||||
```
|
||||
|
||||
## Tauri Commands
|
||||
|
||||
Available Tauri commands (invokable from JS):
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `list_files` | List directory contents |
|
||||
| `upload_file` | Copy file with progress |
|
||||
| `create_folder` | Create new directory |
|
||||
| `delete_path` | Delete file or folder |
|
||||
| `get_home_dir` | Get user's home directory |
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `tauri.conf.json` to customize:
|
||||
|
||||
- `productName`: Application name
|
||||
- `identifier`: Unique app identifier
|
||||
- `build.devUrl`: URL for development (default: `http://localhost:3000`)
|
||||
- `build.frontendDist`: Path to botui's UI (default: `../botui/ui/suite`)
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0 - See [LICENSE](LICENSE) for details.
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [botui](https://github.com/GeneralBots/botui) - Pure web UI
|
||||
- [botserver](https://github.com/GeneralBots/botserver) - Backend server
|
||||
- [botlib](https://github.com/GeneralBots/botlib) - Shared Rust library
|
||||
3
build.rs
Normal file
3
build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
1
gen/schemas/acl-manifests.json
Normal file
1
gen/schemas/acl-manifests.json
Normal file
File diff suppressed because one or more lines are too long
1
gen/schemas/capabilities.json
Normal file
1
gen/schemas/capabilities.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
2543
gen/schemas/desktop-schema.json
Normal file
2543
gen/schemas/desktop-schema.json
Normal file
File diff suppressed because it is too large
Load diff
2543
gen/schemas/linux-schema.json
Normal file
2543
gen/schemas/linux-schema.json
Normal file
File diff suppressed because it is too large
Load diff
BIN
icons/icon.png
Normal file
BIN
icons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
223
js/app-extensions.js
Normal file
223
js/app-extensions.js
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
// 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';
|
||||
|
||||
// App-only guides that will be injected into the suite navigation
|
||||
const APP_GUIDES = [
|
||||
{
|
||||
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'
|
||||
},
|
||||
{
|
||||
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'
|
||||
}
|
||||
];
|
||||
|
||||
// CSS for app-only elements
|
||||
const APP_STYLES = `
|
||||
.app-grid-separator {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0;
|
||||
margin-top: 0.5rem;
|
||||
border-top: 1px solid var(--border-color, #e0e0e0);
|
||||
}
|
||||
|
||||
.app-grid-separator span {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary, #666);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.app-item.app-only {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.app-item.app-only::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--accent-color, #4a90d9);
|
||||
border-radius: 50%;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Inject app-specific styles
|
||||
*/
|
||||
function injectStyles() {
|
||||
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');
|
||||
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')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add separator
|
||||
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';
|
||||
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 = `
|
||||
<div class="app-icon" aria-hidden="true">${guide.icon}</div>
|
||||
<span>${guide.label}</span>
|
||||
`;
|
||||
grid.appendChild(item);
|
||||
});
|
||||
|
||||
// Re-process HTMX on new elements
|
||||
if (window.htmx) {
|
||||
htmx.process(grid);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize BotApp extensions
|
||||
*/
|
||||
function init() {
|
||||
console.log('[BotApp] Initializing app extensions...');
|
||||
|
||||
// Inject styles
|
||||
injectStyles();
|
||||
|
||||
// Inject app guides
|
||||
injectAppGuides();
|
||||
|
||||
// Setup Tauri events
|
||||
setupTauriEvents();
|
||||
|
||||
console.log('[BotApp] App extensions initialized');
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// Expose BotApp API globally
|
||||
window.BotApp = {
|
||||
isApp: true,
|
||||
version: '6.1.0',
|
||||
guides: APP_GUIDES,
|
||||
|
||||
// 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);
|
||||
},
|
||||
|
||||
// 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'),
|
||||
},
|
||||
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
147
src/desktop/drive.rs
Normal file
147
src/desktop/drive.rs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
//! Drive/File System commands for Tauri
|
||||
//!
|
||||
//! Provides native file system access for the desktop app.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
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,
|
||||
pub path: String,
|
||||
pub is_dir: bool,
|
||||
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);
|
||||
let mut files = Vec::new();
|
||||
|
||||
if !base_path.exists() {
|
||||
return Err("Path does not exist".into());
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(base_path).map_err(|e| e.to_string())? {
|
||||
let entry = entry.map_err(|e| e.to_string())?;
|
||||
let path = entry.path();
|
||||
let metadata = entry.metadata().ok();
|
||||
|
||||
let name = path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
let size = metadata.as_ref().map(|m| m.len());
|
||||
let is_dir = metadata.map(|m| m.is_dir()).unwrap_or(false);
|
||||
|
||||
files.push(FileItem {
|
||||
name,
|
||||
path: path.to_str().unwrap_or("").to_string(),
|
||||
is_dir,
|
||||
size,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort: directories first, then alphabetically
|
||||
files.sort_by(|a, b| {
|
||||
if a.is_dir && !b.is_dir {
|
||||
std::cmp::Ordering::Less
|
||||
} else if !a.is_dir && b.is_dir {
|
||||
std::cmp::Ordering::Greater
|
||||
} else {
|
||||
a.name.to_lowercase().cmp(&b.name.to_lowercase())
|
||||
}
|
||||
});
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// Upload a file with progress reporting
|
||||
#[tauri::command]
|
||||
pub async fn upload_file(
|
||||
window: Window,
|
||||
src_path: String,
|
||||
dest_path: String,
|
||||
) -> Result<(), String> {
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
let src = PathBuf::from(&src_path);
|
||||
let dest_dir = PathBuf::from(&dest_path);
|
||||
let dest = dest_dir.join(src.file_name().ok_or("Invalid source file")?);
|
||||
|
||||
if !dest_dir.exists() {
|
||||
fs::create_dir_all(&dest_dir).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let mut source_file = File::open(&src).map_err(|e| e.to_string())?;
|
||||
let mut dest_file = File::create(&dest).map_err(|e| e.to_string())?;
|
||||
let file_size = source_file.metadata().map_err(|e| e.to_string())?.len();
|
||||
|
||||
let mut buffer = [0; 8192];
|
||||
let mut total_read = 0u64;
|
||||
|
||||
loop {
|
||||
let bytes_read = source_file.read(&mut buffer).map_err(|e| e.to_string())?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
dest_file
|
||||
.write_all(&buffer[..bytes_read])
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
total_read += bytes_read as u64;
|
||||
let progress = (total_read as f64 / file_size as f64) * 100.0;
|
||||
|
||||
window
|
||||
.emit("upload_progress", progress)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if full_path.exists() {
|
||||
return Err("Folder already exists".into());
|
||||
}
|
||||
|
||||
fs::create_dir(&full_path).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a file or folder
|
||||
#[tauri::command]
|
||||
pub fn delete_path(path: String) -> Result<(), String> {
|
||||
let target = Path::new(&path);
|
||||
|
||||
if !target.exists() {
|
||||
return Err("Path does not exist".into());
|
||||
}
|
||||
|
||||
if target.is_dir() {
|
||||
fs::remove_dir_all(target).map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
fs::remove_file(target).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get home directory
|
||||
#[tauri::command]
|
||||
pub fn get_home_dir() -> Result<String, String> {
|
||||
dirs::home_dir()
|
||||
.and_then(|p| p.to_str().map(String::from))
|
||||
.ok_or_else(|| "Could not determine home directory".into())
|
||||
}
|
||||
8
src/desktop/mod.rs
Normal file
8
src/desktop/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
//! Desktop-specific functionality for BotApp
|
||||
//!
|
||||
//! This module provides native desktop capabilities:
|
||||
//! - Drive/file management via Tauri
|
||||
//! - System tray integration
|
||||
|
||||
pub mod drive;
|
||||
pub mod tray;
|
||||
156
src/desktop/tray.rs
Normal file
156
src/desktop/tray.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
//! System Tray functionality for BotApp
|
||||
//!
|
||||
//! Provides system tray icon and menu for desktop platforms.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// Tray manager handles system tray icon and interactions
|
||||
pub struct TrayManager {
|
||||
hostname: Arc<RwLock<Option<String>>>,
|
||||
running_mode: RunningMode,
|
||||
}
|
||||
|
||||
/// Running mode for the application
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RunningMode {
|
||||
Server,
|
||||
Desktop,
|
||||
Client,
|
||||
}
|
||||
|
||||
impl TrayManager {
|
||||
/// Create a new TrayManager
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
hostname: Arc::new(RwLock::new(None)),
|
||||
running_mode: RunningMode::Desktop,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the tray icon
|
||||
pub async fn start(&self) -> Result<()> {
|
||||
match self.running_mode {
|
||||
RunningMode::Desktop => {
|
||||
self.start_desktop_mode().await?;
|
||||
}
|
||||
RunningMode::Server => {
|
||||
log::info!("Running in server mode - tray icon disabled");
|
||||
}
|
||||
RunningMode::Client => {
|
||||
log::info!("Running in client mode - tray icon minimal");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get mode as string
|
||||
#[allow(dead_code)]
|
||||
fn get_mode_string(&self) -> String {
|
||||
match self.running_mode {
|
||||
RunningMode::Desktop => "Desktop".to_string(),
|
||||
RunningMode::Server => "Server".to_string(),
|
||||
RunningMode::Client => "Client".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update tray status
|
||||
pub async fn update_status(&self, status: &str) -> Result<()> {
|
||||
log::info!("Tray status update: {}", status);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get hostname
|
||||
pub async fn get_hostname(&self) -> Option<String> {
|
||||
let hostname = self.hostname.read().await;
|
||||
hostname.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TrayManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Service status monitor
|
||||
pub struct ServiceMonitor {
|
||||
services: Vec<ServiceStatus>,
|
||||
}
|
||||
|
||||
/// Status of a service
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServiceStatus {
|
||||
pub name: String,
|
||||
pub running: bool,
|
||||
pub port: u16,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl ServiceMonitor {
|
||||
/// Create a new service monitor with default services
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
services: vec![
|
||||
ServiceStatus {
|
||||
name: "API".to_string(),
|
||||
running: false,
|
||||
port: 8080,
|
||||
url: "http://localhost:8080".to_string(),
|
||||
},
|
||||
ServiceStatus {
|
||||
name: "UI".to_string(),
|
||||
running: false,
|
||||
port: 3000,
|
||||
url: "http://localhost:3000".to_string(),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Check all services
|
||||
pub async fn check_services(&mut self) -> Vec<ServiceStatus> {
|
||||
for service in &mut self.services {
|
||||
service.running = Self::check_service(&service.url).await;
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ServiceMonitor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
9
src/lib.rs
Normal file
9
src/lib.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
//! 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;
|
||||
32
src/main.rs
Normal file
32
src/main.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#![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;
|
||||
|
||||
mod desktop;
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
info!("BotApp starting (Tauri)...");
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
desktop::drive::list_files,
|
||||
desktop::drive::upload_file,
|
||||
desktop::drive::create_folder,
|
||||
desktop::drive::delete_path,
|
||||
desktop::drive::get_home_dir,
|
||||
])
|
||||
.setup(|_app| {
|
||||
info!("BotApp setup complete");
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
31
tauri.conf.json
Normal file
31
tauri.conf.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "General Bots",
|
||||
"version": "6.1.0",
|
||||
"identifier": "br.com.pragmatismo.botapp",
|
||||
"build": {
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": "",
|
||||
"devUrl": "http://localhost:3000",
|
||||
"frontendDist": "../botui/ui/suite"
|
||||
},
|
||||
"app": {
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "General Bots",
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": []
|
||||
}
|
||||
}
|
||||
326
ui/app-guides/local-files.html
Normal file
326
ui/app-guides/local-files.html
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Local Files - General Bots</title>
|
||||
<style>
|
||||
.local-files {
|
||||
padding: 1.5rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.guide-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.guide-header h1 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--fg, #333);
|
||||
}
|
||||
|
||||
.guide-header p {
|
||||
color: var(--fg-muted, #666);
|
||||
}
|
||||
|
||||
.path-bar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--bg-secondary, #f5f5f5);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.path-bar input {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border, #ddd);
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
background: var(--bg, #fff);
|
||||
color: var(--fg, #333);
|
||||
}
|
||||
|
||||
.path-bar button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--primary, #0066cc);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.path-bar button:hover {
|
||||
background: var(--primary-hover, #0055aa);
|
||||
}
|
||||
|
||||
.file-list {
|
||||
border: 1px solid var(--border, #ddd);
|
||||
border-radius: 8px;
|
||||
min-height: 300px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg, #fff);
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-light, #eee);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.file-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background: var(--bg-hover, #f8f8f8);
|
||||
}
|
||||
|
||||
.file-item.folder {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-item .icon {
|
||||
font-size: 1.25rem;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-item .name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-item .size {
|
||||
color: var(--fg-muted, #888);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.file-actions button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-secondary, #f0f0f0);
|
||||
color: var(--fg, #333);
|
||||
border: 1px solid var(--border, #ddd);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-actions button:hover {
|
||||
background: var(--bg-hover, #e8e8e8);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
color: var(--fg-muted, #888);
|
||||
}
|
||||
|
||||
.empty-state .icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
color: var(--fg-muted, #888);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-guide local-files">
|
||||
<header class="guide-header">
|
||||
<h1>📁 Local Files</h1>
|
||||
<p>Access and manage files on your device using native file system access.</p>
|
||||
</header>
|
||||
|
||||
<section class="file-browser">
|
||||
<div class="path-bar">
|
||||
<button onclick="goUp()" title="Go up one level">⬆️</button>
|
||||
<input type="text" id="currentPath" placeholder="Enter path..." onkeydown="if(event.key==='Enter')loadFiles()">
|
||||
<button onclick="loadFiles()">Browse</button>
|
||||
<button onclick="goHome()">🏠 Home</button>
|
||||
</div>
|
||||
|
||||
<div class="file-list" id="fileList">
|
||||
<div class="empty-state">
|
||||
<span class="icon">📂</span>
|
||||
<p>Click "Home" or enter a path to browse files</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-actions">
|
||||
<button onclick="createFolder()">📁 New Folder</button>
|
||||
<button onclick="refreshFiles()">🔄 Refresh</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Check if running in Tauri
|
||||
const isTauri = window.__TAURI__ !== undefined;
|
||||
|
||||
async function invoke(cmd, args) {
|
||||
if (isTauri && window.__TAURI__.core) {
|
||||
return await window.__TAURI__.core.invoke(cmd, args);
|
||||
} else {
|
||||
console.warn('Not running in Tauri - file operations unavailable');
|
||||
throw new Error('Native file access requires the desktop app');
|
||||
}
|
||||
}
|
||||
|
||||
async function goHome() {
|
||||
try {
|
||||
const homePath = await invoke('get_home_dir', {});
|
||||
document.getElementById('currentPath').value = homePath;
|
||||
await loadFiles();
|
||||
} catch (e) {
|
||||
showError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function goUp() {
|
||||
const pathInput = document.getElementById('currentPath');
|
||||
const currentPath = pathInput.value;
|
||||
if (!currentPath) return;
|
||||
|
||||
const parts = currentPath.split(/[/\\]/);
|
||||
parts.pop();
|
||||
if (parts.length > 0) {
|
||||
pathInput.value = parts.join('/') || '/';
|
||||
await loadFiles();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFiles() {
|
||||
const pathInput = document.getElementById('currentPath');
|
||||
const path = pathInput.value || '/';
|
||||
const listEl = document.getElementById('fileList');
|
||||
|
||||
listEl.innerHTML = '<div class="loading">Loading...</div>';
|
||||
|
||||
try {
|
||||
const files = await invoke('list_files', { path });
|
||||
|
||||
if (files.length === 0) {
|
||||
listEl.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<span class="icon">📭</span>
|
||||
<p>This folder is empty</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = files.map(f => `
|
||||
<div class="file-item ${f.is_dir ? 'folder' : 'file'}"
|
||||
onclick="${f.is_dir ? `navigateTo('${f.path.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}')` : ''}">
|
||||
<span class="icon">${f.is_dir ? '📁' : getFileIcon(f.name)}</span>
|
||||
<span class="name">${escapeHtml(f.name)}</span>
|
||||
<span class="size">${f.is_dir ? '' : formatSize(f.size)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
listEl.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<span class="icon">⚠️</span>
|
||||
<p>${escapeHtml(e.message || 'Failed to load files')}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTo(path) {
|
||||
document.getElementById('currentPath').value = path;
|
||||
loadFiles();
|
||||
}
|
||||
|
||||
async function createFolder() {
|
||||
const path = document.getElementById('currentPath').value;
|
||||
if (!path) {
|
||||
showError('Please navigate to a directory first');
|
||||
return;
|
||||
}
|
||||
|
||||
const name = prompt('Enter folder name:');
|
||||
if (name) {
|
||||
try {
|
||||
await invoke('create_folder', { path, name });
|
||||
await loadFiles();
|
||||
} catch (e) {
|
||||
showError(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refreshFiles() {
|
||||
loadFiles();
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes === null || bytes === undefined) return '';
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
||||
return (bytes / 1024 / 1024 / 1024).toFixed(1) + ' GB';
|
||||
}
|
||||
|
||||
function getFileIcon(filename) {
|
||||
const ext = filename.split('.').pop().toLowerCase();
|
||||
const icons = {
|
||||
'pdf': '📕',
|
||||
'doc': '📘', 'docx': '📘',
|
||||
'xls': '📗', 'xlsx': '📗',
|
||||
'ppt': '📙', 'pptx': '📙',
|
||||
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', 'svg': '🖼️',
|
||||
'mp3': '🎵', 'wav': '🎵', 'flac': '🎵',
|
||||
'mp4': '🎬', 'mkv': '🎬', 'avi': '🎬',
|
||||
'zip': '📦', 'rar': '📦', 'tar': '📦', 'gz': '📦',
|
||||
'js': '📜', 'ts': '📜', 'py': '📜', 'rs': '📜',
|
||||
'html': '🌐', 'css': '🎨',
|
||||
'json': '📋', 'xml': '📋', 'yaml': '📋', 'yml': '📋',
|
||||
'txt': '📄', 'md': '📄',
|
||||
};
|
||||
return icons[ext] || '📄';
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
alert('Error: ' + message);
|
||||
}
|
||||
|
||||
// Auto-load home directory on init if in Tauri
|
||||
if (isTauri) {
|
||||
goHome();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
332
ui/app-guides/native-settings.html
Normal file
332
ui/app-guides/native-settings.html
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>App Settings - General Bots</title>
|
||||
<style>
|
||||
.app-guide {
|
||||
padding: 2rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.guide-header {
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border-color, #e0e0e0);
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.guide-header h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.guide-header p {
|
||||
margin: 0;
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.settings-section h2 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-primary, #333);
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: var(--bg-secondary, #f5f5f5);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
.setting-control {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
background: var(--bg-tertiary, #ccc);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.toggle-switch.active {
|
||||
background: var(--accent-color, #4CAF50);
|
||||
}
|
||||
|
||||
.toggle-switch::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.toggle-switch.active::after {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
select, input[type="text"], input[type="number"] {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border-color, #ddd);
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent-color, #2196F3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-tertiary, #e0e0e0);
|
||||
color: var(--text-primary, #333);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border-color, #e0e0e0);
|
||||
}
|
||||
|
||||
.version-info {
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-secondary, #f5f5f5);
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-guide native-settings">
|
||||
<header class="guide-header">
|
||||
<h1>App Settings</h1>
|
||||
<p>Configure native desktop application preferences</p>
|
||||
</header>
|
||||
|
||||
<section class="settings-section">
|
||||
<h2>General</h2>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Start on login</div>
|
||||
<div class="setting-description">Automatically start General Bots when you log in</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<div class="toggle-switch" id="startOnLogin" onclick="toggleSetting(this)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Minimize to tray</div>
|
||||
<div class="setting-description">Keep running in system tray when window is closed</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<div class="toggle-switch active" id="minimizeToTray" onclick="toggleSetting(this)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Default download location</div>
|
||||
<div class="setting-description">Where to save downloaded files</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<button class="btn btn-secondary" onclick="selectDownloadPath()">Browse...</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h2>Server Connection</h2>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Server URL</div>
|
||||
<div class="setting-description">BotServer API endpoint</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<input type="text" id="serverUrl" value="http://localhost:8080" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Auto-reconnect</div>
|
||||
<div class="setting-description">Automatically reconnect when connection is lost</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<div class="toggle-switch active" id="autoReconnect" onclick="toggleSetting(this)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h2>Notifications</h2>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Desktop notifications</div>
|
||||
<div class="setting-description">Show native desktop notifications</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<div class="toggle-switch active" id="desktopNotifications" onclick="toggleSetting(this)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Sound alerts</div>
|
||||
<div class="setting-description">Play sound when receiving messages</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<div class="toggle-switch" id="soundAlerts" onclick="toggleSetting(this)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" onclick="saveSettings()">Save Settings</button>
|
||||
<button class="btn btn-secondary" onclick="resetSettings()">Reset to Defaults</button>
|
||||
</div>
|
||||
|
||||
<div class="version-info">
|
||||
<strong>General Bots App</strong><br>
|
||||
Version: <span id="appVersion">6.1.0</span><br>
|
||||
Platform: <span id="appPlatform">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Toggle switch functionality
|
||||
function toggleSetting(element) {
|
||||
element.classList.toggle('active');
|
||||
}
|
||||
|
||||
// Select download path via Tauri dialog
|
||||
async function selectDownloadPath() {
|
||||
if (window.__TAURI__) {
|
||||
try {
|
||||
const { open } = window.__TAURI__.dialog;
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: 'Select Download Location'
|
||||
});
|
||||
if (selected) {
|
||||
console.log('Selected path:', selected);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to open dialog:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save settings
|
||||
async function saveSettings() {
|
||||
const settings = {
|
||||
startOnLogin: document.getElementById('startOnLogin').classList.contains('active'),
|
||||
minimizeToTray: document.getElementById('minimizeToTray').classList.contains('active'),
|
||||
serverUrl: document.getElementById('serverUrl').value,
|
||||
autoReconnect: document.getElementById('autoReconnect').classList.contains('active'),
|
||||
desktopNotifications: document.getElementById('desktopNotifications').classList.contains('active'),
|
||||
soundAlerts: document.getElementById('soundAlerts').classList.contains('active')
|
||||
};
|
||||
|
||||
// Save to localStorage for now
|
||||
localStorage.setItem('botapp_settings', JSON.stringify(settings));
|
||||
|
||||
// Show confirmation
|
||||
if (window.BotServerApp) {
|
||||
window.BotServerApp.showNotification('Settings saved successfully', 'success');
|
||||
} else {
|
||||
alert('Settings saved!');
|
||||
}
|
||||
}
|
||||
|
||||
// Reset settings to defaults
|
||||
function resetSettings() {
|
||||
document.getElementById('startOnLogin').classList.remove('active');
|
||||
document.getElementById('minimizeToTray').classList.add('active');
|
||||
document.getElementById('serverUrl').value = 'http://localhost:8080';
|
||||
document.getElementById('autoReconnect').classList.add('active');
|
||||
document.getElementById('desktopNotifications').classList.add('active');
|
||||
document.getElementById('soundAlerts').classList.remove('active');
|
||||
}
|
||||
|
||||
// Load settings on page load
|
||||
function loadSettings() {
|
||||
const saved = localStorage.getItem('botapp_settings');
|
||||
if (saved) {
|
||||
const settings = JSON.parse(saved);
|
||||
|
||||
if (settings.startOnLogin) document.getElementById('startOnLogin').classList.add('active');
|
||||
if (!settings.minimizeToTray) document.getElementById('minimizeToTray').classList.remove('active');
|
||||
if (settings.serverUrl) document.getElementById('serverUrl').value = settings.serverUrl;
|
||||
if (!settings.autoReconnect) document.getElementById('autoReconnect').classList.remove('active');
|
||||
if (!settings.desktopNotifications) document.getElementById('desktopNotifications').classList.remove('active');
|
||||
if (settings.soundAlerts) document.getElementById('soundAlerts').classList.add('active');
|
||||
}
|
||||
|
||||
// Set platform info
|
||||
if (window.__TAURI__) {
|
||||
document.getElementById('appPlatform').textContent = navigator.platform;
|
||||
} else {
|
||||
document.getElementById('appPlatform').textContent = 'Web (not in app)';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
loadSettings();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Reference in a new issue