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