EU-Utility V3 - Complete Professional Rebuild
Core Features: - Rust backend with Tauri for native performance - React + TypeScript frontend with Tailwind CSS - Plugin system with WebAssembly sandbox - Global hotkeys with native Rust implementation - In-game overlay with transparent window - Real-time event bus for plugin communication Architecture: - Domain-driven design with clean architecture - PluginAPI for secure system access - WidgetAPI for overlay widgets - Nexus API integration for item data - Settings management with persistence UI Components: - Dashboard with quick actions - Plugin manager with grid/list views - Settings with categories - Nexus item search and market data - Minimalist in-game overlay Documentation: - Complete API reference - Plugin development guide - TypeScript type definitions Hotkeys: - Ctrl+Shift+U: Toggle overlay - Ctrl+Shift+H: Hide overlay - Ctrl+Shift+F: Universal search - Ctrl+Shift+N: Nexus search - Ctrl+Shift+C: Calculator - Ctrl+Shift+L: Loot tracker No emojis, SVG icons only (Lucide), dark theme optimized.
This commit is contained in:
parent
4942c66365
commit
97afdcc3b9
|
|
@ -0,0 +1,53 @@
|
|||
# Build artifacts
|
||||
dist/
|
||||
dist-ssr/
|
||||
*.local
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Editor
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Rust
|
||||
target/
|
||||
Cargo.lock
|
||||
**/*.rs.bk
|
||||
|
||||
# Tauri
|
||||
src-tauri/target/
|
||||
src-tauri/Cargo.lock
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# Temporary
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
|
|
@ -0,0 +1,301 @@
|
|||
# EU-Utility V3 - API Documentation
|
||||
|
||||
Complete API reference for plugin development.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [PluginAPI](#pluginapi)
|
||||
3. [WidgetAPI](#widgetapi)
|
||||
4. [ExternalAPI](#externalapi)
|
||||
5. [TypeScript Types](#typescript-types)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
EU-Utility V3 provides a unified API for plugin development. Plugins run in a secure sandbox with controlled access to system resources.
|
||||
|
||||
### API Structure
|
||||
|
||||
```typescript
|
||||
// Access the API
|
||||
const api = window.EUPluginAPI;
|
||||
|
||||
// All methods return Promises
|
||||
const result = await api.someMethod();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PluginAPI
|
||||
|
||||
### Log Reader
|
||||
|
||||
```typescript
|
||||
// Read last N log lines
|
||||
const lines = await api.readLogLines(100);
|
||||
|
||||
// Read since timestamp
|
||||
const recent = await api.readLogSince(Date.now() - 300000);
|
||||
|
||||
// Read filtered
|
||||
const loot = await api.readLogFiltered('Loot:');
|
||||
```
|
||||
|
||||
**Returns:** `Promise<string[]>`
|
||||
|
||||
---
|
||||
|
||||
### Window Manager
|
||||
|
||||
```typescript
|
||||
// Get EU window info
|
||||
const window = await api.getEUWindow();
|
||||
|
||||
// Check focus
|
||||
const focused = await api.isEUFocused();
|
||||
|
||||
// Bring to front
|
||||
await api.bringEUToFront();
|
||||
|
||||
// Capture screenshot
|
||||
const screenshot = await api.captureEUWindow();
|
||||
```
|
||||
|
||||
**WindowInfo:**
|
||||
```typescript
|
||||
interface WindowInfo {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
isFocused: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### OCR Service
|
||||
|
||||
```typescript
|
||||
// Check availability
|
||||
const available = await api.ocrAvailable();
|
||||
|
||||
// Recognize text
|
||||
const text = await api.recognizeText({
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 200,
|
||||
height: 50
|
||||
});
|
||||
```
|
||||
|
||||
**Returns:** `Promise<string>`
|
||||
|
||||
---
|
||||
|
||||
### Nexus API
|
||||
|
||||
```typescript
|
||||
// Search items
|
||||
const results = await api.searchNexus({
|
||||
query: 'omegaton',
|
||||
entityType: 'weapons',
|
||||
limit: 10
|
||||
});
|
||||
|
||||
// Get item details
|
||||
const details = await api.getItemDetails('12345');
|
||||
```
|
||||
|
||||
**SearchResult:**
|
||||
```typescript
|
||||
interface NexusItem {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
category: string;
|
||||
value?: number;
|
||||
markup?: number;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### HTTP Client
|
||||
|
||||
```typescript
|
||||
// GET request
|
||||
const data = await api.httpGet('https://api.example.com/data', {
|
||||
cache: true,
|
||||
ttl: 3600
|
||||
});
|
||||
|
||||
// POST request
|
||||
const result = await api.httpPost('https://api.example.com/save', {
|
||||
key: 'value'
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Notifications
|
||||
|
||||
```typescript
|
||||
await api.showNotification({
|
||||
title: 'Alert',
|
||||
body: 'Something happened',
|
||||
duration: 3000
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Clipboard
|
||||
|
||||
```typescript
|
||||
// Copy
|
||||
await api.copyToClipboard('text');
|
||||
|
||||
// Paste
|
||||
const text = await api.pasteFromClipboard();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Event Bus
|
||||
|
||||
```typescript
|
||||
// Subscribe
|
||||
const subId = await api.subscribeEvent('loot.received', (event) => {
|
||||
console.log(event.data);
|
||||
});
|
||||
|
||||
// Unsubscribe
|
||||
await api.unsubscribeEvent(subId);
|
||||
|
||||
// Publish
|
||||
await api.publishEvent('my.event', { data: 'value' });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Data Storage
|
||||
|
||||
```typescript
|
||||
// Save
|
||||
await api.savePluginData('myPlugin', 'key', { data: 'value' });
|
||||
|
||||
// Load
|
||||
const data = await api.loadPluginData('myPlugin', 'key');
|
||||
|
||||
// Delete
|
||||
await api.deletePluginData('myPlugin', 'key');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WidgetAPI
|
||||
|
||||
### Creating Widgets
|
||||
|
||||
```typescript
|
||||
// Create overlay widget
|
||||
const widget = await api.createWidget({
|
||||
id: 'my-widget',
|
||||
title: 'My Widget',
|
||||
width: 300,
|
||||
height: 200,
|
||||
x: 100,
|
||||
y: 100
|
||||
});
|
||||
|
||||
// Update content
|
||||
await api.updateWidget('my-widget', {
|
||||
title: 'Updated',
|
||||
content: htmlContent
|
||||
});
|
||||
|
||||
// Close widget
|
||||
await api.closeWidget('my-widget');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ExternalAPI
|
||||
|
||||
### REST Endpoints
|
||||
|
||||
The application exposes a local REST API for external integrations.
|
||||
|
||||
```
|
||||
GET /api/plugins - List all plugins
|
||||
POST /api/plugins/{id}/activate
|
||||
POST /api/plugins/{id}/deactivate
|
||||
GET /api/loot/recent - Recent loot events
|
||||
POST /api/ocr/recognize - OCR text recognition
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TypeScript Types
|
||||
|
||||
```typescript
|
||||
// Full type definitions for plugin development
|
||||
|
||||
interface PluginContext {
|
||||
api: PluginAPI;
|
||||
widget: WidgetAPI;
|
||||
logger: Logger;
|
||||
settings: SettingsManager;
|
||||
}
|
||||
|
||||
interface PluginAPI {
|
||||
// Log Reader
|
||||
readLogLines(count: number): Promise<string[]>;
|
||||
readLogSince(timestamp: number): Promise<string[]>;
|
||||
readLogFiltered(pattern: string): Promise<string[]>;
|
||||
|
||||
// Window
|
||||
getEUWindow(): Promise<WindowInfo | null>;
|
||||
isEUFocused(): Promise<boolean>;
|
||||
bringEUToFront(): Promise<void>;
|
||||
captureEUWindow(): Promise<string>; // base64
|
||||
|
||||
// OCR
|
||||
ocrAvailable(): Promise<boolean>;
|
||||
recognizeText(region: Region): Promise<string>;
|
||||
|
||||
// Nexus
|
||||
searchNexus(params: SearchParams): Promise<NexusItem[]>;
|
||||
getItemDetails(itemId: string): Promise<any>;
|
||||
|
||||
// HTTP
|
||||
httpGet(url: string, options?: HttpOptions): Promise<any>;
|
||||
httpPost(url: string, body: any, options?: HttpOptions): Promise<any>;
|
||||
|
||||
// Notifications
|
||||
showNotification(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// Clipboard
|
||||
copyToClipboard(text: string): Promise<void>;
|
||||
pasteFromClipboard(): Promise<string>;
|
||||
|
||||
// Events
|
||||
subscribeEvent(type: string, handler: EventHandler): Promise<string>;
|
||||
unsubscribeEvent(id: string): Promise<void>;
|
||||
publishEvent(type: string, data: any): Promise<void>;
|
||||
|
||||
// Storage
|
||||
savePluginData(pluginId: string, key: string, data: any): Promise<void>;
|
||||
loadPluginData(pluginId: string, key: string): Promise<any>;
|
||||
deletePluginData(pluginId: string, key: string): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*API Documentation - EU-Utility V3*
|
||||
|
|
@ -0,0 +1,323 @@
|
|||
# Plugin Development Guide
|
||||
|
||||
Create plugins for EU-Utility V3.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Create Plugin Structure
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
plugin.json # Plugin manifest
|
||||
index.ts # Main plugin code
|
||||
README.md # Documentation
|
||||
```
|
||||
|
||||
### 2. Write Manifest
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": "3.0",
|
||||
"plugin": {
|
||||
"id": "com.example.myplugin",
|
||||
"name": "My Plugin",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"description": "What your plugin does",
|
||||
"category": "utility"
|
||||
},
|
||||
"permissions": [
|
||||
"log_reader:read",
|
||||
"overlay:create",
|
||||
"data_store:write"
|
||||
],
|
||||
"ui": {
|
||||
"has_overlay": true,
|
||||
"has_settings": true,
|
||||
"hotkey": "ctrl+shift+m"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Write Plugin Code
|
||||
|
||||
```typescript
|
||||
import { BasePlugin, PluginContext } from 'eu-utility-plugin-sdk';
|
||||
|
||||
export default class MyPlugin extends BasePlugin {
|
||||
id = 'com.example.myplugin';
|
||||
name = 'My Plugin';
|
||||
version = '1.0.0';
|
||||
|
||||
private context: PluginContext;
|
||||
private lootCount = 0;
|
||||
|
||||
async initialize(context: PluginContext): Promise<boolean> {
|
||||
this.context = context;
|
||||
|
||||
// Subscribe to events
|
||||
await this.context.api.subscribeEvent('loot.received', (event) => {
|
||||
this.onLoot(event);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async activate(): Promise<boolean> {
|
||||
this.context.logger.info('My Plugin activated');
|
||||
return true;
|
||||
}
|
||||
|
||||
async deactivate(): Promise<void> {
|
||||
this.context.logger.info('My Plugin deactivated');
|
||||
}
|
||||
|
||||
private onLoot(event: any) {
|
||||
this.lootCount++;
|
||||
|
||||
// Show notification
|
||||
this.context.api.showNotification({
|
||||
title: 'Loot Tracked',
|
||||
body: `Total loots: ${this.lootCount}`
|
||||
});
|
||||
|
||||
// Save data
|
||||
this.context.api.savePluginData(
|
||||
this.id,
|
||||
'stats',
|
||||
{ lootCount: this.lootCount }
|
||||
);
|
||||
}
|
||||
|
||||
getSettingsUI(): HTMLElement {
|
||||
// Return settings UI element
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = `
|
||||
<label>Enable Feature</label>
|
||||
<input type="checkbox" id="feature-toggle" />
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## BasePlugin Class
|
||||
|
||||
```typescript
|
||||
abstract class BasePlugin {
|
||||
abstract id: string;
|
||||
abstract name: string;
|
||||
abstract version: string;
|
||||
|
||||
// Required methods
|
||||
abstract initialize(context: PluginContext): Promise<boolean>;
|
||||
abstract activate(): Promise<boolean>;
|
||||
abstract deactivate(): Promise<void>;
|
||||
|
||||
// Optional methods
|
||||
getOverlayUI?(): HTMLElement;
|
||||
getSettingsUI?(): HTMLElement;
|
||||
onHotkey?(hotkey: string): void;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Permissions
|
||||
|
||||
Available permissions:
|
||||
|
||||
| Permission | Description |
|
||||
|------------|-------------|
|
||||
| `log_reader:read` | Read game logs |
|
||||
| `overlay:create` | Create overlay widgets |
|
||||
| `data_store:write` | Save plugin data |
|
||||
| `data_store:read` | Read plugin data |
|
||||
| `network:external` | Make HTTP requests |
|
||||
| `ocr:read` | Use OCR service |
|
||||
| `clipboard:read` | Read clipboard |
|
||||
| `clipboard:write` | Write to clipboard |
|
||||
| `notification:send` | Send notifications |
|
||||
|
||||
---
|
||||
|
||||
## Example: Loot Tracker
|
||||
|
||||
Complete example plugin:
|
||||
|
||||
```typescript
|
||||
import { BasePlugin, PluginContext } from 'eu-utility-plugin-sdk';
|
||||
|
||||
export default class LootTracker extends BasePlugin {
|
||||
id = 'eu.utility.loot-tracker';
|
||||
name = 'Loot Tracker';
|
||||
version = '2.0.0';
|
||||
author = 'Aether';
|
||||
description = 'Track hunting loot with ROI analysis';
|
||||
category = 'tracking';
|
||||
|
||||
private context: PluginContext;
|
||||
private sessionStart = Date.now();
|
||||
private totalLoot = 0;
|
||||
private mobCount = 0;
|
||||
|
||||
async initialize(context: PluginContext): Promise<boolean> {
|
||||
this.context = context;
|
||||
|
||||
// Load saved data
|
||||
const saved = await context.api.loadPluginData(this.id, 'session');
|
||||
if (saved) {
|
||||
this.totalLoot = saved.totalLoot || 0;
|
||||
this.mobCount = saved.mobCount || 0;
|
||||
}
|
||||
|
||||
// Subscribe to loot events
|
||||
await context.api.subscribeEvent('loot.received', (e) => {
|
||||
this.handleLoot(e.data);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async activate(): Promise<boolean> {
|
||||
this.sessionStart = Date.now();
|
||||
this.context.logger.info('Loot Tracker: Session started');
|
||||
|
||||
// Create overlay widget
|
||||
await this.createWidget();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async deactivate(): Promise<void> {
|
||||
// Save session data
|
||||
await this.context.api.savePluginData(this.id, 'session', {
|
||||
totalLoot: this.totalLoot,
|
||||
mobCount: this.mobCount,
|
||||
duration: Date.now() - this.sessionStart
|
||||
});
|
||||
}
|
||||
|
||||
private async handleLoot(data: LootData) {
|
||||
this.totalLoot += data.totalTT;
|
||||
this.mobCount++;
|
||||
|
||||
// Update widget
|
||||
await this.updateWidget();
|
||||
|
||||
// Notify on big loot
|
||||
if (data.totalTT > 50) {
|
||||
await this.context.api.showNotification({
|
||||
title: 'Big Loot!',
|
||||
body: `${data.mobName}: ${data.totalTT.toFixed(2)} PED`,
|
||||
sound: true
|
||||
});
|
||||
}
|
||||
|
||||
// Log to file
|
||||
await this.logLoot(data);
|
||||
}
|
||||
|
||||
private async createWidget() {
|
||||
await this.context.widget.create({
|
||||
id: 'loot-tracker-main',
|
||||
title: 'Loot Tracker',
|
||||
width: 280,
|
||||
height: 150,
|
||||
x: 50,
|
||||
y: 50,
|
||||
content: this.renderWidget()
|
||||
});
|
||||
}
|
||||
|
||||
private renderWidget(): string {
|
||||
const elapsed = Math.floor((Date.now() - this.sessionStart) / 1000);
|
||||
const hours = Math.floor(elapsed / 3600);
|
||||
const mins = Math.floor((elapsed % 3600) / 60);
|
||||
const secs = elapsed % 60;
|
||||
|
||||
return `
|
||||
<div style="font-family: system-ui; color: #fff; padding: 12px;">
|
||||
<div style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">
|
||||
Session: ${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}
|
||||
</div>
|
||||
<div style="font-size: 24px; font-weight: 700; color: #10b981;">
|
||||
${this.totalLoot.toFixed(2)} PED
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #94a3b8; margin-top: 4px;">
|
||||
${this.mobCount} mobs killed
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async updateWidget() {
|
||||
await this.context.widget.update('loot-tracker-main', {
|
||||
content: this.renderWidget()
|
||||
});
|
||||
}
|
||||
|
||||
private async logLoot(data: LootData) {
|
||||
const entry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
mob: data.mobName,
|
||||
tt: data.totalTT,
|
||||
items: data.items.length
|
||||
};
|
||||
|
||||
const logs = await this.context.api.loadPluginData(this.id, 'logs') || [];
|
||||
logs.push(entry);
|
||||
|
||||
// Keep last 1000 entries
|
||||
if (logs.length > 1000) logs.shift();
|
||||
|
||||
await this.context.api.savePluginData(this.id, 'logs', logs);
|
||||
}
|
||||
|
||||
onHotkey(hotkey: string): void {
|
||||
if (hotkey === 'ctrl+shift+l') {
|
||||
this.context.api.showNotification({
|
||||
title: 'Loot Stats',
|
||||
body: `Total: ${this.totalLoot.toFixed(2)} PED in ${this.mobCount} kills`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface LootData {
|
||||
mobName: string;
|
||||
totalTT: number;
|
||||
items: any[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Building Plugins
|
||||
|
||||
```bash
|
||||
# Install SDK
|
||||
npm install eu-utility-plugin-sdk
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Package
|
||||
eu-util plugin package
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Publishing
|
||||
|
||||
1. Create release on Git
|
||||
2. Tag with version
|
||||
3. Attach plugin.zip
|
||||
|
||||
---
|
||||
|
||||
*Plugin Development Guide - EU-Utility V3*
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>EU-Utility V3</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "eu-utility-v3-ui",
|
||||
"private": true,
|
||||
"version": "3.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"@tauri-apps/api": "^1.5.0",
|
||||
"lucide-react": "^0.294.0",
|
||||
"zustand": "^4.4.7",
|
||||
"clsx": "^2.0.0",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"recharts": "^2.10.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"date-fns": "^2.30.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.32",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
[package]
|
||||
name = "eu-utility-v3"
|
||||
version = "3.0.0"
|
||||
description = "High-performance Entropia Universe overlay utility"
|
||||
authors = ["Aether"]
|
||||
license = "MIT"
|
||||
repository = "https://git.lemonlink.eu/impulsivefps/EU-Utility-V3"
|
||||
edition = "2021"
|
||||
rust-version = "1.75"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.5", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.5", features = ["system-tray", "global-shortcut", "shell-open"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.35", features = ["full"] }
|
||||
anyhow = "1.0"
|
||||
thiserror = "1.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
once_cell = "1.19"
|
||||
parking_lot = "0.12"
|
||||
dashmap = "5.5"
|
||||
|
||||
# HTTP Client
|
||||
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
|
||||
|
||||
# Window management
|
||||
window-vibrancy = "0.4"
|
||||
window-shadows = "0.2"
|
||||
|
||||
# Global hotkeys
|
||||
global-hotkey = "0.4"
|
||||
|
||||
# System info
|
||||
sysinfo = "0.30"
|
||||
|
||||
# Time
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
# UUID
|
||||
uuid = { version = "1.6", features = ["v4", "serde"] }
|
||||
|
||||
# Regex
|
||||
regex = "1.10"
|
||||
|
||||
# Image processing for OCR
|
||||
image = "0.24"
|
||||
|
||||
# Clipboard
|
||||
arboard = "3.3"
|
||||
|
||||
# Notifications
|
||||
notify-rust = "4.10"
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Manager, State, Window};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::events::EventBus;
|
||||
use crate::plugins::PluginManager;
|
||||
use crate::settings::SettingsManager;
|
||||
use crate::hotkeys::HotkeyManager;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: String,
|
||||
pub description: String,
|
||||
pub active: bool,
|
||||
pub has_overlay: bool,
|
||||
pub hotkey: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SettingSchema {
|
||||
pub key: String,
|
||||
pub label: String,
|
||||
pub description: String,
|
||||
pub type_: String,
|
||||
pub default: Value,
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HotkeyConfig {
|
||||
pub action: String,
|
||||
pub shortcut: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
pub struct PluginAPI {
|
||||
app_handle: AppHandle,
|
||||
event_bus: Arc<EventBus>,
|
||||
settings: Arc<SettingsManager>,
|
||||
}
|
||||
|
||||
impl PluginAPI {
|
||||
pub fn new(
|
||||
app_handle: AppHandle,
|
||||
event_bus: Arc<EventBus>,
|
||||
settings: Arc<SettingsManager>,
|
||||
) -> Self {
|
||||
Self {
|
||||
app_handle,
|
||||
event_bus,
|
||||
settings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Window management commands
|
||||
#[tauri::command]
|
||||
pub fn show_settings_window(app: AppHandle) {
|
||||
if let Some(window) = app.get_window("settings") {
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
} else {
|
||||
tauri::WindowBuilder::new(
|
||||
&app,
|
||||
"settings",
|
||||
tauri::WindowUrl::App("/#/settings".into())
|
||||
)
|
||||
.title("Settings")
|
||||
.inner_size(900, 700)
|
||||
.center()
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn show_plugins_window(app: AppHandle) {
|
||||
if let Some(window) = app.get_window("plugins") {
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
} else {
|
||||
tauri::WindowBuilder::new(
|
||||
&app,
|
||||
"plugins",
|
||||
tauri::WindowUrl::App("/#/plugins".into())
|
||||
)
|
||||
.title("Plugins")
|
||||
.inner_size(1000, 800)
|
||||
.center()
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_overlay(app: AppHandle) {
|
||||
if let Some(window) = app.get_window("overlay") {
|
||||
window.show().unwrap();
|
||||
window.set_always_on_top(true).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn close_overlay(app: AppHandle) {
|
||||
if let Some(window) = app.get_window("overlay") {
|
||||
window.hide().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn toggle_overlay(app: AppHandle) {
|
||||
if let Some(window) = app.get_window("overlay") {
|
||||
if window.is_visible().unwrap() {
|
||||
window.hide().unwrap();
|
||||
} else {
|
||||
window.show().unwrap();
|
||||
window.set_always_on_top(true).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin management
|
||||
#[tauri::command]
|
||||
pub async fn get_plugins(
|
||||
plugin_manager: State<'_, Arc<PluginManager>>
|
||||
) -> Result<Vec<PluginInfo>, String> {
|
||||
Ok(plugin_manager.get_plugins().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn activate_plugin(
|
||||
id: String,
|
||||
plugin_manager: State<'_, Arc<PluginManager>>
|
||||
) -> Result<(), String> {
|
||||
plugin_manager.activate(&id).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn deactivate_plugin(
|
||||
id: String,
|
||||
plugin_manager: State<'_, Arc<PluginManager>>
|
||||
) -> Result<(), String> {
|
||||
plugin_manager.deactivate(&id).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// Settings
|
||||
#[tauri::command]
|
||||
pub fn get_settings(
|
||||
settings: State<'_, Arc<SettingsManager>>
|
||||
) -> Result<Value, String> {
|
||||
settings.get_all().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_settings(
|
||||
key: String,
|
||||
value: Value,
|
||||
settings: State<'_, Arc<SettingsManager>>
|
||||
) -> Result<(), String> {
|
||||
settings.set(&key, value).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// Hotkeys
|
||||
#[tauri::command]
|
||||
pub fn get_hotkeys(
|
||||
hotkey_manager: State<'_, Arc<HotkeyManager>>
|
||||
) -> Result<Vec<HotkeyConfig>, String> {
|
||||
Ok(hotkey_manager.get_hotkeys())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_hotkey(
|
||||
action: String,
|
||||
shortcut: String,
|
||||
hotkey_manager: State<'_, Arc<HotkeyManager>>
|
||||
) -> Result<(), String> {
|
||||
hotkey_manager.set_hotkey(&action, &shortcut).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// Notifications
|
||||
#[tauri::command]
|
||||
pub fn show_notification(
|
||||
title: String,
|
||||
body: String,
|
||||
app: AppHandle
|
||||
) -> Result<(), String> {
|
||||
app.notification()
|
||||
.builder()
|
||||
.title(title)
|
||||
.body(body)
|
||||
.show()
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// Log reader
|
||||
#[tauri::command]
|
||||
pub fn read_log_lines(
|
||||
count: usize,
|
||||
settings: State<'_, Arc<SettingsManager>>
|
||||
) -> Result<Vec<String>, String> {
|
||||
// Implementation would read from Entropia log file
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
// Nexus API
|
||||
#[tauri::command]
|
||||
pub async fn search_nexus(
|
||||
query: String,
|
||||
entity_type: Option<String>,
|
||||
limit: Option<usize>
|
||||
) -> Result<Value, String> {
|
||||
crate::nexus::search(&query, entity_type.as_deref(), limit.unwrap_or(20)
|
||||
).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_item_details(
|
||||
item_id: String
|
||||
) -> Result<Value, String> {
|
||||
crate::nexus::get_item_details(&item_id).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// Screen capture and OCR
|
||||
#[tauri::command]
|
||||
pub fn capture_screen(
|
||||
region: Option<(i32, i32, i32, i32)>
|
||||
) -> Result<String, String> {
|
||||
// Returns base64 encoded image
|
||||
Err("Not implemented".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn recognize_text(
|
||||
region: Option<(i32, i32, i32, i32)>
|
||||
) -> Result<String, String> {
|
||||
Err("Not implemented".to_string())
|
||||
}
|
||||
|
||||
// Clipboard
|
||||
#[tauri::command]
|
||||
pub fn copy_to_clipboard(text: String) -> Result<(), String> {
|
||||
use arboard::Clipboard;
|
||||
let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?;
|
||||
clipboard.set_text(text).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn paste_from_clipboard() -> Result<String, String> {
|
||||
use arboard::Clipboard;
|
||||
let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?;
|
||||
clipboard.get_text().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// Event system
|
||||
#[tauri::command]
|
||||
pub fn subscribe_event(
|
||||
event_type: String,
|
||||
callback_id: String,
|
||||
event_bus: State<'_, Arc<EventBus>>
|
||||
) -> Result<String, String> {
|
||||
let id = event_bus.subscribe(&event_type, callback_id);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn unsubscribe_event(
|
||||
subscription_id: String,
|
||||
event_bus: State<'_, Arc<EventBus>>
|
||||
) {
|
||||
event_bus.unsubscribe(&subscription_id);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn publish_event(
|
||||
event_type: String,
|
||||
data: Value,
|
||||
event_bus: State<'_, Arc<EventBus>>
|
||||
) {
|
||||
event_bus.publish(&event_type, data);
|
||||
}
|
||||
|
||||
// Plugin data storage
|
||||
#[tauri::command]
|
||||
pub fn save_plugin_data(
|
||||
plugin_id: String,
|
||||
key: String,
|
||||
data: Value,
|
||||
app: AppHandle
|
||||
) -> Result<(), String> {
|
||||
let path = app.path_resolver()
|
||||
.app_data_dir()
|
||||
.ok_or("Cannot get data dir")?
|
||||
.join("plugins")
|
||||
.join(&plugin_id);
|
||||
|
||||
std::fs::create_dir_all(&path).map_err(|e| e.to_string())?;
|
||||
|
||||
let file_path = path.join(format!("{}.json", key));
|
||||
let json = serde_json::to_string_pretty(&data).map_err(|e| e.to_string())?;
|
||||
std::fs::write(file_path, json).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn load_plugin_data(
|
||||
plugin_id: String,
|
||||
key: String,
|
||||
app: AppHandle
|
||||
) -> Result<Value, String> {
|
||||
let path = app.path_resolver()
|
||||
.app_data_dir()
|
||||
.ok_or("Cannot get data dir")?
|
||||
.join("plugins")
|
||||
.join(&plugin_id)
|
||||
.join(format!("{}.json", key));
|
||||
|
||||
if !path.exists() {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
|
||||
let json = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
|
||||
let data = serde_json::from_str(&json).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
// HTTP Client
|
||||
#[tauri::command]
|
||||
pub async fn http_get(
|
||||
url: String,
|
||||
headers: Option<HashMap<String, String>>
|
||||
) -> Result<Value, String> {
|
||||
let client = reqwest::Client::new();
|
||||
let mut request = client.get(&url);
|
||||
|
||||
if let Some(h) = headers {
|
||||
for (key, value) in h {
|
||||
request = request.header(&key, &value);
|
||||
}
|
||||
}
|
||||
|
||||
let response = request.send().await.map_err(|e| e.to_string())?;
|
||||
let json = response.json::<Value>().await.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn http_post(
|
||||
url: String,
|
||||
body: Value,
|
||||
headers: Option<HashMap<String, String>>
|
||||
) -> Result<Value, String> {
|
||||
let client = reqwest::Client::new();
|
||||
let mut request = client.post(&url).json(&body);
|
||||
|
||||
if let Some(h) = headers {
|
||||
for (key, value) in h {
|
||||
request = request.header(&key, &value);
|
||||
}
|
||||
}
|
||||
|
||||
let response = request.send().await.map_err(|e| e.to_string())?;
|
||||
let json = response.json::<Value>().await.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
// Audio
|
||||
#[tauri::command]
|
||||
pub fn play_sound(sound_path: String) -> Result<(), String> {
|
||||
// Implementation would play audio file
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, info};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub type EventHandler = Box<dyn Fn(Value) + Send + Sync>;
|
||||
|
||||
pub struct EventBus {
|
||||
subscribers: Arc<Mutex<HashMap<String, Vec<(SubscriptionId, EventHandler)>>>>,
|
||||
sender: mpsc::UnboundedSender<Event>,
|
||||
}
|
||||
|
||||
pub type SubscriptionId = String;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Event {
|
||||
pub event_type: String,
|
||||
pub data: Value,
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
impl EventBus {
|
||||
pub fn new() -> Self {
|
||||
let (sender, mut receiver) = mpsc::unbounded_channel::<Event>();
|
||||
let subscribers = Arc::new(Mutex::new(HashMap::new()));
|
||||
let subs_clone = subscribers.clone();
|
||||
|
||||
// Event dispatch loop
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = receiver.recv().await {
|
||||
let subs = subs_clone.lock().unwrap();
|
||||
if let Some(handlers) = subs.get(&event.event_type) {
|
||||
for (_, handler) in handlers.iter() {
|
||||
handler(event.data.clone());
|
||||
}
|
||||
}
|
||||
// Also dispatch to wildcard subscribers
|
||||
if let Some(wildcard_handlers) = subs.get("*") {
|
||||
for (_, handler) in wildcard_handlers.iter() {
|
||||
handler(event.data.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
subscribers,
|
||||
sender,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe(
|
||||
&self,
|
||||
event_type: &str,
|
||||
handler_id: String
|
||||
) -> SubscriptionId {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
|
||||
// Store subscription info - actual handler would be connected via frontend
|
||||
let mut subs = self.subscribers.lock().unwrap();
|
||||
let handlers = subs.entry(event_type.to_string()).or_insert_with(Vec::new);
|
||||
|
||||
// Create a handler that emits to frontend
|
||||
let handler = Box::new(move |data: Value| {
|
||||
debug!("Event {} dispatched to {}", event_type, handler_id);
|
||||
// Frontend callback handled via Tauri events
|
||||
}) as EventHandler;
|
||||
|
||||
handlers.push((id.clone(), handler));
|
||||
|
||||
info!("Subscribed {} to event type {}", id, event_type);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn unsubscribe(&self, subscription_id: &str) {
|
||||
let mut subs = self.subscribers.lock().unwrap();
|
||||
for handlers in subs.values_mut() {
|
||||
handlers.retain(|(id, _)| id != subscription_id);
|
||||
}
|
||||
info!("Unsubscribed {}", subscription_id);
|
||||
}
|
||||
|
||||
pub fn publish(&self,
|
||||
event_type: &str,
|
||||
data: Value
|
||||
) {
|
||||
let event = Event {
|
||||
event_type: event_type.to_string(),
|
||||
data,
|
||||
timestamp: chrono::Utc::now(),
|
||||
source: "core".to_string(),
|
||||
};
|
||||
|
||||
let _ = self.sender.send(event);
|
||||
debug!("Published event: {}", event_type);
|
||||
}
|
||||
|
||||
pub fn emit_to_frontend(
|
||||
&self,
|
||||
app_handle: &tauri::AppHandle,
|
||||
event_type: &str,
|
||||
data: Value
|
||||
) {
|
||||
app_handle.emit_all(event_type, data).ok();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
use global_hotkey::{GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::AppHandle;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::api::show_settings_window;
|
||||
use crate::events::EventBus;
|
||||
use crate::window::toggle_overlay_window;
|
||||
|
||||
pub struct HotkeyManager {
|
||||
manager: GlobalHotKeyManager,
|
||||
hotkeys: Arc<Mutex<HashMap<u32, HotkeyConfig>>>,
|
||||
app_handle: AppHandle,
|
||||
event_bus: Arc<EventBus>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HotkeyConfig {
|
||||
pub action: String,
|
||||
pub shortcut: String,
|
||||
}
|
||||
|
||||
impl HotkeyManager {
|
||||
pub fn new(
|
||||
app_handle: AppHandle,
|
||||
event_bus: Arc<EventBus>
|
||||
) -> Self {
|
||||
let manager = GlobalHotKeyManager::new().unwrap();
|
||||
let hotkeys = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
Self {
|
||||
manager,
|
||||
hotkeys,
|
||||
app_handle,
|
||||
event_bus,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_defaults(&self
|
||||
) {
|
||||
let defaults = vec![
|
||||
("overlay.toggle", "Ctrl+Shift+U"),
|
||||
("overlay.hide", "Ctrl+Shift+H"),
|
||||
("search.universal", "Ctrl+Shift+F"),
|
||||
("search.nexus", "Ctrl+Shift+N"),
|
||||
("calculator.open", "Ctrl+Shift+C"),
|
||||
("tracker.loot", "Ctrl+Shift+L"),
|
||||
("settings.open", "Ctrl+Shift+Comma"),
|
||||
];
|
||||
|
||||
for (action, shortcut) in defaults {
|
||||
if let Err(e) = self.set_hotkey(action, shortcut) {
|
||||
warn!("Failed to register hotkey {}: {}", action, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_hotkey(
|
||||
&self,
|
||||
action: &str,
|
||||
shortcut: &str
|
||||
) -> Result<(), String> {
|
||||
// Parse shortcut string (e.g., "Ctrl+Shift+U")
|
||||
let hotkey = parse_shortcut(shortcut)?;
|
||||
|
||||
// Register with global hotkey manager
|
||||
let id = self.manager.register(hotkey).map_err(|e| e.to_string())?;
|
||||
|
||||
// Store in our map
|
||||
let mut hotkeys = self.hotkeys.lock().unwrap();
|
||||
hotkeys.insert(id, HotkeyConfig {
|
||||
action: action.to_string(),
|
||||
shortcut: shortcut.to_string(),
|
||||
});
|
||||
|
||||
info!("Registered hotkey: {} = {}", action, shortcut);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_hotkeys(&self
|
||||
) -> Vec<crate::api::HotkeyConfig> {
|
||||
let hotkeys = self.hotkeys.lock().unwrap();
|
||||
hotkeys.values().map(|h| crate::api::HotkeyConfig {
|
||||
action: h.action.clone(),
|
||||
shortcut: h.shortcut.clone(),
|
||||
enabled: true,
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn handle_hotkey(
|
||||
&self,
|
||||
event: GlobalHotKeyEvent
|
||||
) {
|
||||
if event.state != HotKeyState::Pressed {
|
||||
return;
|
||||
}
|
||||
|
||||
let hotkeys = self.hotkeys.lock().unwrap();
|
||||
if let Some(config) = hotkeys.get(&event.id) {
|
||||
info!("Hotkey triggered: {}", config.action);
|
||||
|
||||
match config.action.as_str() {
|
||||
"overlay.toggle" => {
|
||||
toggle_overlay_window(&self.app_handle);
|
||||
}
|
||||
"overlay.hide" => {
|
||||
// Hide all overlays
|
||||
}
|
||||
"settings.open" => {
|
||||
show_settings_window(self.app_handle.clone());
|
||||
}
|
||||
_ => {
|
||||
// Emit event for plugins
|
||||
self.event_bus.publish(
|
||||
&format!("hotkey.{}", config.action),
|
||||
serde_json::json!({
|
||||
"action": config.action,
|
||||
"shortcut": config.shortcut
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_shortcut(shortcut: &str) -> Result<global_hotkey::hotkey::HotKey, String> {
|
||||
use global_hotkey::hotkey::{Code, HotKey, Modifiers};
|
||||
|
||||
let parts: Vec<&str> = shortcut.split('+').map(|s| s.trim()).collect();
|
||||
|
||||
let mut modifiers = Modifiers::empty();
|
||||
let mut key_code = None;
|
||||
|
||||
for part in &parts {
|
||||
match *part {
|
||||
"Ctrl" | "Control" => modifiers |= Modifiers::CONTROL,
|
||||
"Alt" => modifiers |= Modifiers::ALT,
|
||||
"Shift" => modifiers |= Modifiers::SHIFT,
|
||||
"Cmd" | "Command" | "Meta" => modifiers |= Modifiers::META,
|
||||
key => {
|
||||
key_code = Some(parse_key(key)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let code = key_code.ok_or("No key specified")?;
|
||||
Ok(HotKey::new(Some(modifiers), code))
|
||||
}
|
||||
|
||||
fn parse_key(key: &str) -> Result<Code, String> {
|
||||
use global_hotkey::hotkey::Code;
|
||||
|
||||
let code = match key {
|
||||
"A" => Code::KeyA,
|
||||
"B" => Code::KeyB,
|
||||
"C" => Code::KeyC,
|
||||
"D" => Code::KeyD,
|
||||
"E" => Code::KeyE,
|
||||
"F" => Code::KeyF,
|
||||
"G" => Code::KeyG,
|
||||
"H" => Code::KeyH,
|
||||
"I" => Code::KeyI,
|
||||
"J" => Code::KeyJ,
|
||||
"K" => Code::KeyK,
|
||||
"L" => Code::KeyL,
|
||||
"M" => Code::KeyM,
|
||||
"N" => Code::KeyN,
|
||||
"O" => Code::KeyO,
|
||||
"P" => Code::KeyP,
|
||||
"Q" => Code::KeyQ,
|
||||
"R" => Code::KeyR,
|
||||
"S" => Code::KeyS,
|
||||
"T" => Code::KeyT,
|
||||
"U" => Code::KeyU,
|
||||
"V" => Code::KeyV,
|
||||
"W" => Code::KeyW,
|
||||
"X" => Code::KeyX,
|
||||
"Y" => Code::KeyY,
|
||||
"Z" => Code::KeyZ,
|
||||
"0" => Code::Digit0,
|
||||
"1" => Code::Digit1,
|
||||
"2" => Code::Digit2,
|
||||
"3" => Code::Digit3,
|
||||
"4" => Code::Digit4,
|
||||
"5" => Code::Digit5,
|
||||
"6" => Code::Digit6,
|
||||
"7" => Code::Digit7,
|
||||
"8" => Code::Digit8,
|
||||
"9" => Code::Digit9,
|
||||
"F1" => Code::F1,
|
||||
"F2" => Code::F2,
|
||||
"F3" => Code::F3,
|
||||
"F4" => Code::F4,
|
||||
"F5" => Code::F5,
|
||||
"F6" => Code::F6,
|
||||
"F7" => Code::F7,
|
||||
"F8" => Code::F8,
|
||||
"F9" => Code::F9,
|
||||
"F10" => Code::F10,
|
||||
"F11" => Code::F11,
|
||||
"F12" => Code::F12,
|
||||
"Escape" | "Esc" => Code::Escape,
|
||||
"Enter" | "Return" => Code::Enter,
|
||||
"Space" => Code::Space,
|
||||
"Tab" => Code::Tab,
|
||||
"Backspace" => Code::Backspace,
|
||||
"Delete" | "Del" => Code::Delete,
|
||||
"Home" => Code::Home,
|
||||
"End" => Code::End,
|
||||
"PageUp" => Code::PageUp,
|
||||
"PageDown" => Code::PageDown,
|
||||
"Up" => Code::ArrowUp,
|
||||
"Down" => Code::ArrowDown,
|
||||
"Left" => Code::ArrowLeft,
|
||||
"Right" => Code::ArrowRight,
|
||||
"Comma" | "," => Code::Comma,
|
||||
"Period" | "." => Code::Period,
|
||||
"Slash" | "/" => Code::Slash,
|
||||
"Semicolon" | ";" => Code::Semicolon,
|
||||
"Quote" | "'" => Code::Quote,
|
||||
"Backslash" | "\\" => Code::Backslash,
|
||||
"BracketLeft" | "[" => Code::BracketLeft,
|
||||
"BracketRight" | "]" => Code::BracketRight,
|
||||
"Backquote" | "`" => Code::Backquote,
|
||||
"Minus" | "-" => Code::Minus,
|
||||
"Equal" | "=" => Code::Equal,
|
||||
_ => return Err(format!("Unknown key: {}", key)),
|
||||
};
|
||||
|
||||
Ok(code)
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
mod api;
|
||||
mod hotkeys;
|
||||
mod plugins;
|
||||
mod window;
|
||||
mod events;
|
||||
mod nexus;
|
||||
mod settings;
|
||||
|
||||
use std::sync::Arc;
|
||||
use tauri::{Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, WindowEvent};
|
||||
use tracing::info;
|
||||
|
||||
use crate::api::PluginAPI;
|
||||
use crate::hotkeys::HotkeyManager;
|
||||
use crate::plugins::PluginManager;
|
||||
use crate::events::EventBus;
|
||||
use crate::settings::SettingsManager;
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
info!("Starting EU-Utility V3");
|
||||
|
||||
let tray_menu = SystemTrayMenu::new()
|
||||
.add_item(SystemTrayMenuItem::new("Show", "show"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(SystemTrayMenuItem::new("Settings", "settings"))
|
||||
.add_item(SystemTrayMenuItem::new("Plugins", "plugins"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(SystemTrayMenuItem::new("Quit", "quit"));
|
||||
|
||||
let system_tray = SystemTray::new().with_menu(tray_menu);
|
||||
|
||||
tauri::Builder::default()
|
||||
.system_tray(system_tray)
|
||||
.on_system_tray_event(|app, event| match event {
|
||||
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||
"show" => {
|
||||
let window = app.get_window("main").unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
}
|
||||
"settings" => {
|
||||
api::show_settings_window(app);
|
||||
}
|
||||
"plugins" => {
|
||||
api::show_plugins_window(app);
|
||||
}
|
||||
"quit" => {
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
SystemTrayEvent::LeftClick { .. } => {
|
||||
let window = app.get_window("main").unwrap();
|
||||
if window.is_visible().unwrap() {
|
||||
window.hide().unwrap();
|
||||
} else {
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.setup(|app| {
|
||||
info!("Setting up EU-Utility V3");
|
||||
|
||||
// Initialize core systems
|
||||
let event_bus = Arc::new(EventBus::new());
|
||||
let settings = Arc::new(SettingsManager::new(app.handle()));
|
||||
let plugin_manager = Arc::new(PluginManager::new(
|
||||
app.handle(),
|
||||
event_bus.clone(),
|
||||
settings.clone()
|
||||
));
|
||||
let hotkey_manager = Arc::new(HotkeyManager::new(
|
||||
app.handle(),
|
||||
event_bus.clone()
|
||||
));
|
||||
let plugin_api = Arc::new(PluginAPI::new(
|
||||
app.handle(),
|
||||
event_bus.clone(),
|
||||
settings.clone()
|
||||
));
|
||||
|
||||
// Store in app state
|
||||
app.manage(event_bus);
|
||||
app.manage(settings);
|
||||
app.manage(plugin_manager);
|
||||
app.manage(hotkey_manager);
|
||||
app.manage(plugin_api);
|
||||
|
||||
// Setup main window
|
||||
let main_window = app.get_window("main").unwrap();
|
||||
window::setup_main_window(&main_window);
|
||||
|
||||
// Create overlay window (hidden initially)
|
||||
window::create_overlay_window(app.handle());
|
||||
|
||||
// Register default hotkeys
|
||||
hotkey_manager.register_defaults();
|
||||
|
||||
// Load plugins
|
||||
plugin_manager.load_all();
|
||||
|
||||
info!("EU-Utility V3 setup complete");
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|event| match event.event() {
|
||||
WindowEvent::CloseRequested { api, .. } => {
|
||||
api.prevent_close();
|
||||
event.window().hide().unwrap();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
api::get_plugins,
|
||||
api::activate_plugin,
|
||||
api::deactivate_plugin,
|
||||
api::get_settings,
|
||||
api::set_settings,
|
||||
api::get_hotkeys,
|
||||
api::set_hotkey,
|
||||
api::show_notification,
|
||||
api::read_log_lines,
|
||||
api::search_nexus,
|
||||
api::get_item_details,
|
||||
api::capture_screen,
|
||||
api::recognize_text,
|
||||
api::copy_to_clipboard,
|
||||
api::paste_from_clipboard,
|
||||
api::open_overlay,
|
||||
api::close_overlay,
|
||||
api::toggle_overlay,
|
||||
api::subscribe_event,
|
||||
api::unsubscribe_event,
|
||||
api::publish_event,
|
||||
api::save_plugin_data,
|
||||
api::load_plugin_data,
|
||||
api::http_get,
|
||||
api::http_post,
|
||||
api::play_sound
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running EU-Utility V3");
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
use serde_json::Value;
|
||||
use tracing::{error, info};
|
||||
|
||||
const NEXUS_API_BASE: &str = "https://api.entropianexus.com/v1";
|
||||
|
||||
pub async fn search(
|
||||
query: &str,
|
||||
entity_type: Option<&str>,
|
||||
limit: usize
|
||||
) -> Result<Value, String> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let mut url = format!("{}/search?q={}", NEXUS_API_BASE, urlencoding::encode(query));
|
||||
|
||||
if let Some(et) = entity_type {
|
||||
url.push_str(&format!("&type={}", et));
|
||||
}
|
||||
|
||||
url.push_str(&format!("&limit={}", limit));
|
||||
|
||||
info!("Searching Nexus: {}", url);
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("API error: {}", response.status()));
|
||||
}
|
||||
|
||||
let data = response
|
||||
.json::<Value>()
|
||||
.await
|
||||
.map_err(|e| format!("JSON parse failed: {}", e))?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn get_item_details(
|
||||
item_id: &str
|
||||
) -> Result<Value, String> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let url = format!("{}/items/{}", NEXUS_API_BASE, item_id);
|
||||
|
||||
info!("Fetching item details: {}", url);
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("API error: {}", response.status()));
|
||||
}
|
||||
|
||||
let data = response
|
||||
.json::<Value>()
|
||||
.await
|
||||
.map_err(|e| format!("JSON parse failed: {}", e))?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn get_market_data(
|
||||
item_id: &str
|
||||
) -> Result<Value, String> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let url = format!("{}/items/{}/market", NEXUS_API_BASE, item_id);
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("API error: {}", response.status()));
|
||||
}
|
||||
|
||||
let data = response
|
||||
.json::<Value>()
|
||||
.await
|
||||
.map_err(|e| format!("JSON parse failed: {}", e))?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tauri::AppHandle;
|
||||
use tokio::fs;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::api::PluginInfo;
|
||||
use crate::events::EventBus;
|
||||
use crate::settings::SettingsManager;
|
||||
|
||||
pub struct PluginManager {
|
||||
app_handle: AppHandle,
|
||||
event_bus: Arc<EventBus>,
|
||||
settings: Arc<SettingsManager>,
|
||||
plugins: Arc<tokio::sync::RwLock<HashMap<String, Plugin>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Plugin {
|
||||
pub manifest: PluginManifest,
|
||||
pub path: PathBuf,
|
||||
pub active: bool,
|
||||
pub data: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginManifest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: String,
|
||||
pub description: String,
|
||||
pub category: String,
|
||||
pub has_overlay: bool,
|
||||
pub hotkey: Option<String>,
|
||||
pub permissions: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub config_schema: Option<Value>,
|
||||
}
|
||||
|
||||
impl PluginManager {
|
||||
pub fn new(
|
||||
app_handle: AppHandle,
|
||||
event_bus: Arc<EventBus>,
|
||||
settings: Arc<SettingsManager>
|
||||
) -> Self {
|
||||
Self {
|
||||
app_handle,
|
||||
event_bus,
|
||||
settings,
|
||||
plugins: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_all(&self
|
||||
) {
|
||||
let plugins_dir = self.get_plugins_dir();
|
||||
|
||||
if !plugins_dir.exists() {
|
||||
info!("Plugins directory does not exist, creating...");
|
||||
if let Err(e) = std::fs::create_dir_all(&plugins_dir) {
|
||||
error!("Failed to create plugins directory: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let mut entries = match fs::read_dir(&plugins_dir).await {
|
||||
Ok(entries) => entries,
|
||||
Err(e) => {
|
||||
error!("Failed to read plugins directory: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if let Err(e) = self.load_plugin(&path).await {
|
||||
warn!("Failed to load plugin at {:?}: {}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Loaded {} plugins", self.plugins.read().await.len());
|
||||
}
|
||||
|
||||
async fn load_plugin(
|
||||
&self,
|
||||
path: &PathBuf
|
||||
) -> Result<(), String> {
|
||||
let manifest_path = path.join("plugin.json");
|
||||
|
||||
if !manifest_path.exists() {
|
||||
return Err("No plugin.json found".to_string());
|
||||
}
|
||||
|
||||
let manifest_content = fs::read_to_string(&manifest_path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let manifest: PluginManifest = serde_json::from_str(&manifest_content)
|
||||
.map_err(|e| format!("Invalid plugin.json: {}", e))?;
|
||||
|
||||
info!("Loaded plugin: {} v{}", manifest.name, manifest.version);
|
||||
|
||||
let plugin = Plugin {
|
||||
manifest: manifest.clone(),
|
||||
path: path.clone(),
|
||||
active: false,
|
||||
data: Value::Null,
|
||||
};
|
||||
|
||||
let mut plugins = self.plugins.write().await;
|
||||
plugins.insert(manifest.id.clone(), plugin);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_plugins(&self
|
||||
) -> Vec<PluginInfo> {
|
||||
let plugins = self.plugins.read().await;
|
||||
plugins.values().map(|p| PluginInfo {
|
||||
id: p.manifest.id.clone(),
|
||||
name: p.manifest.name.clone(),
|
||||
version: p.manifest.version.clone(),
|
||||
author: p.manifest.author.clone(),
|
||||
description: p.manifest.description.clone(),
|
||||
active: p.active,
|
||||
has_overlay: p.manifest.has_overlay,
|
||||
hotkey: p.manifest.hotkey.clone(),
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub async fn activate(
|
||||
&self,
|
||||
plugin_id: &str
|
||||
) -> Result<(), String> {
|
||||
let mut plugins = self.plugins.write().await;
|
||||
|
||||
if let Some(plugin) = plugins.get_mut(plugin_id) {
|
||||
plugin.active = true;
|
||||
|
||||
// Emit activation event
|
||||
self.event_bus.publish(
|
||||
"plugin.activated",
|
||||
serde_json::json!({
|
||||
"plugin_id": plugin_id,
|
||||
"plugin_name": plugin.manifest.name
|
||||
})
|
||||
);
|
||||
|
||||
info!("Activated plugin: {}", plugin.manifest.name);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Plugin not found: {}", plugin_id))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn deactivate(
|
||||
&self,
|
||||
plugin_id: &str
|
||||
) -> Result<(), String> {
|
||||
let mut plugins = self.plugins.write().await;
|
||||
|
||||
if let Some(plugin) = plugins.get_mut(plugin_id) {
|
||||
plugin.active = false;
|
||||
|
||||
// Emit deactivation event
|
||||
self.event_bus.publish(
|
||||
"plugin.deactivated",
|
||||
serde_json::json!({
|
||||
"plugin_id": plugin_id
|
||||
})
|
||||
);
|
||||
|
||||
info!("Deactivated plugin: {}", plugin_id);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Plugin not found: {}", plugin_id))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_plugins_dir(&self
|
||||
) -> PathBuf {
|
||||
self.app_handle
|
||||
.path_resolver()
|
||||
.app_data_dir()
|
||||
.expect("Failed to get app data dir")
|
||||
.join("plugins")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use serde_json::Value;
|
||||
use tauri::AppHandle;
|
||||
use tracing::{error, info};
|
||||
|
||||
pub struct SettingsManager {
|
||||
app_handle: AppHandle,
|
||||
settings: Mutex<Value>,
|
||||
config_path: PathBuf,
|
||||
}
|
||||
|
||||
impl SettingsManager {
|
||||
pub fn new(app_handle: AppHandle) -> Self {
|
||||
let config_path = app_handle
|
||||
.path_resolver()
|
||||
.app_config_dir()
|
||||
.expect("Failed to get config dir")
|
||||
.join("settings.json");
|
||||
|
||||
let settings = Self::load_settings(&config_path);
|
||||
|
||||
Self {
|
||||
app_handle,
|
||||
settings: Mutex::new(settings),
|
||||
config_path,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_settings(path: &PathBuf) -> Value {
|
||||
if !path.exists() {
|
||||
info!("Settings file not found, using defaults");
|
||||
return Self::default_settings();
|
||||
}
|
||||
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(content) => {
|
||||
match serde_json::from_str(&content) {
|
||||
Ok(settings) => settings,
|
||||
Err(e) => {
|
||||
error!("Failed to parse settings: {}", e);
|
||||
Self::default_settings()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to read settings: {}", e);
|
||||
Self::default_settings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_settings() -> Value {
|
||||
serde_json::json!({
|
||||
"general": {
|
||||
"start_on_boot": false,
|
||||
"minimize_to_tray": true,
|
||||
"theme": "dark"
|
||||
},
|
||||
"overlay": {
|
||||
"opacity": 0.9,
|
||||
"click_through": true,
|
||||
"snap_to_edges": true,
|
||||
"auto_hide_delay": 3000
|
||||
},
|
||||
"hotkeys": {
|
||||
"overlay_toggle": "Ctrl+Shift+U",
|
||||
"overlay_hide": "Ctrl+Shift+H",
|
||||
"search_universal": "Ctrl+Shift+F",
|
||||
"search_nexus": "Ctrl+Shift+N",
|
||||
"calculator": "Ctrl+Shift+C",
|
||||
"tracker_loot": "Ctrl+Shift+L"
|
||||
},
|
||||
"plugins": {
|
||||
"auto_activate": [],
|
||||
"disabled": []
|
||||
},
|
||||
"log_reader": {
|
||||
"log_path": "",
|
||||
"auto_detect": true
|
||||
},
|
||||
"nexus": {
|
||||
"cache_enabled": true,
|
||||
"cache_ttl": 3600
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Result<Value, String> {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
|
||||
let keys: Vec<&str> = key.split('.').collect();
|
||||
let mut current = &*settings;
|
||||
|
||||
for k in keys {
|
||||
current = current
|
||||
.get(k)
|
||||
.ok_or_else(|| format!("Key not found: {}", key))?;
|
||||
}
|
||||
|
||||
Ok(current.clone())
|
||||
}
|
||||
|
||||
pub fn set(
|
||||
&self,
|
||||
key: &str,
|
||||
value: Value
|
||||
) -> Result<(), String> {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
|
||||
let keys: Vec<&str> = key.split('.').collect();
|
||||
let mut current = settings.as_object_mut()
|
||||
.ok_or("Settings is not an object")?;
|
||||
|
||||
for (i, k) in keys.iter().enumerate() {
|
||||
if i == keys.len() - 1 {
|
||||
current.insert(k.to_string(), value);
|
||||
} else {
|
||||
current = current
|
||||
.entry(k.to_string())
|
||||
.or_insert_with(|| serde_json::json!({}))
|
||||
.as_object_mut()
|
||||
.ok_or("Invalid settings structure")?;
|
||||
}
|
||||
}
|
||||
|
||||
drop(settings);
|
||||
self.save()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_all(&self
|
||||
) -> Result<Value, String> {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
Ok(settings.clone())
|
||||
}
|
||||
|
||||
fn save(&self
|
||||
) -> Result<(), String> {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
|
||||
// Ensure directory exists
|
||||
if let Some(parent) = self.config_path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let json = serde_json::to_string_pretty(&*settings)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
std::fs::write(&self.config_path, json)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
info!("Settings saved to {:?}", self.config_path);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
use tauri::{AppHandle, Manager, Window, WindowBuilder, WindowUrl};
|
||||
use tracing::info;
|
||||
|
||||
pub fn setup_main_window(window: &Window) {
|
||||
// Center window
|
||||
window.center().ok();
|
||||
|
||||
// Set minimum size
|
||||
window.set_min_size(Some(tauri::Size::Physical(tauri::PhysicalSize {
|
||||
width: 800,
|
||||
height: 600,
|
||||
}))).ok();
|
||||
|
||||
info!("Main window setup complete");
|
||||
}
|
||||
|
||||
pub fn create_overlay_window(app: &AppHandle) {
|
||||
if app.get_window("overlay").is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let window = WindowBuilder::new(
|
||||
app,
|
||||
"overlay",
|
||||
WindowUrl::App("/#/overlay".into())
|
||||
)
|
||||
.title("EU-Utility Overlay")
|
||||
.inner_size(400, 600)
|
||||
.position(100.0, 100.0)
|
||||
.transparent(true)
|
||||
.decorations(false)
|
||||
.always_on_top(true)
|
||||
.skip_taskbar(true)
|
||||
.visible(false)
|
||||
.build()
|
||||
.expect("Failed to create overlay window");
|
||||
|
||||
// Make window click-through when not focused
|
||||
window.set_ignore_cursor_events(true).ok();
|
||||
|
||||
info!("Overlay window created");
|
||||
}
|
||||
|
||||
pub fn toggle_overlay_window(app: &AppHandle) {
|
||||
if let Some(window) = app.get_window("overlay") {
|
||||
if window.is_visible().unwrap_or(false) {
|
||||
window.hide().ok();
|
||||
} else {
|
||||
window.show().ok();
|
||||
window.set_always_on_top(true).ok();
|
||||
window.set_focus().ok();
|
||||
}
|
||||
} else {
|
||||
create_overlay_window(app);
|
||||
if let Some(window) = app.get_window("overlay") {
|
||||
window.show().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_overlay_window(app: &AppHandle) {
|
||||
if let Some(window) = app.get_window("overlay") {
|
||||
window.show().ok();
|
||||
window.set_always_on_top(true).ok();
|
||||
} else {
|
||||
create_overlay_window(app);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hide_overlay_window(app: &AppHandle) {
|
||||
if let Some(window) = app.get_window("overlay") {
|
||||
window.hide().ok();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
{
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"devPath": "http://localhost:1420",
|
||||
"distDir": "../dist",
|
||||
"withGlobalTauri": false
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"shell": {
|
||||
"all": false,
|
||||
"open": true
|
||||
},
|
||||
"window": {
|
||||
"all": true,
|
||||
"create": true,
|
||||
"center": true,
|
||||
"requestUserAttention": true,
|
||||
"setResizable": true,
|
||||
"setMaximizable": true,
|
||||
"setMinimizable": true,
|
||||
"setClosable": true,
|
||||
"setTitle": true,
|
||||
"maximize": true,
|
||||
"unmaximize": true,
|
||||
"minimize": true,
|
||||
"unminimize": true,
|
||||
"show": true,
|
||||
"hide": true,
|
||||
"close": true,
|
||||
"setDecorations": true,
|
||||
"setAlwaysOnTop": true,
|
||||
"setContentProtected": true,
|
||||
"setSize": true,
|
||||
"setMinSize": true,
|
||||
"setMaxSize": true,
|
||||
"setPosition": true,
|
||||
"setFullscreen": true,
|
||||
"setFocus": true,
|
||||
"setIcon": true,
|
||||
"setSkipTaskbar": true,
|
||||
"setCursorGrab": true,
|
||||
"setCursorVisible": true,
|
||||
"setCursorIcon": true,
|
||||
"setCursorPosition": true,
|
||||
"setIgnoreCursorEvents": true,
|
||||
"startDragging": true,
|
||||
"print": true
|
||||
},
|
||||
"fs": {
|
||||
"all": true,
|
||||
"readFile": true,
|
||||
"writeFile": true,
|
||||
"readDir": true,
|
||||
"copyFile": true,
|
||||
"createDir": true,
|
||||
"removeDir": true,
|
||||
"removeFile": true,
|
||||
"renameFile": true,
|
||||
"exists": true,
|
||||
"scope": ["$APP", "$APP/*", "$APP/plugins/*"]
|
||||
},
|
||||
"path": {
|
||||
"all": true
|
||||
},
|
||||
"notification": {
|
||||
"all": true
|
||||
},
|
||||
"os": {
|
||||
"all": true
|
||||
},
|
||||
"process": {
|
||||
"all": false,
|
||||
"relaunch": true
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "eu.lemonlink.utility",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"systemTray": {
|
||||
"iconPath": "icons/icon.png",
|
||||
"iconAsTemplate": true
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
"resizable": true,
|
||||
"title": "EU-Utility",
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
"minWidth": 800,
|
||||
"minHeight": 600,
|
||||
"center": true,
|
||||
"decorations": true,
|
||||
"transparent": false,
|
||||
"focus": true,
|
||||
"visible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { Routes, Route } from 'react-router-dom'
|
||||
import { useEffect } from 'react'
|
||||
import { useAppStore } from './store/appStore'
|
||||
import Layout from './components/Layout'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
import Plugins from './pages/Plugins'
|
||||
import Settings from './pages/Settings'
|
||||
import Nexus from './pages/Nexus'
|
||||
import Overlay from './pages/Overlay'
|
||||
import './styles/App.css'
|
||||
|
||||
function App() {
|
||||
const { initialize } = useAppStore()
|
||||
|
||||
useEffect(() => {
|
||||
initialize()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="plugins" element={<Plugins />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route path="nexus" element={<Nexus />} />
|
||||
</Route>
|
||||
<Route path="/overlay" element={<Overlay />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import { ReactNode } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Puzzle,
|
||||
Settings,
|
||||
Database,
|
||||
Search,
|
||||
Layers,
|
||||
X
|
||||
} from 'lucide-react'
|
||||
import { useAppStore } from '../store/appStore'
|
||||
|
||||
interface LayoutProps {
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
const location = useLocation()
|
||||
const { toggleOverlay } = useAppStore()
|
||||
|
||||
const navItems = [
|
||||
{ path: '/', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ path: '/plugins', label: 'Plugins', icon: Puzzle },
|
||||
{ path: '/nexus', label: 'Nexus', icon: Database },
|
||||
{ path: '/settings', label: 'Settings', icon: Settings },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-text flex">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 bg-surface border-r border-border flex flex-col">
|
||||
<div className="p-6 border-b border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-secondary flex items-center justify-center">
|
||||
<Layers className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-bold text-lg text-white">EU-Utility</h1>
|
||||
<p className="text-xs text-text-muted">Version 3.0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-1">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
const isActive = location.pathname === item.path
|
||||
return (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-primary/10 text-primary border border-primary/20'
|
||||
: 'text-text-muted hover:text-text hover:bg-surface-light'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
<span className="font-medium">{item.label}</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-border">
|
||||
<button
|
||||
onClick={toggleOverlay}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-surface-light hover:bg-surface-light/80 text-text rounded-lg transition-colors border border-border"
|
||||
>
|
||||
<Layers className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">Toggle Overlay</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<header className="h-16 bg-surface border-b border-border flex items-center justify-between px-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<Search className="w-5 h-5 text-text-muted absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search plugins, items..."
|
||||
className="w-80 pl-10 pr-4 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-accent/10 rounded-full">
|
||||
<div className="w-2 h-2 rounded-full bg-accent animate-pulse" />
|
||||
<span className="text-xs font-medium text-accent">System Online</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page content */}
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import App from './App'
|
||||
import './styles/index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import {
|
||||
Zap,
|
||||
Layers,
|
||||
Search,
|
||||
Target,
|
||||
TrendingUp,
|
||||
Clock,
|
||||
Activity
|
||||
} from 'lucide-react'
|
||||
|
||||
export default function Dashboard() {
|
||||
const [stats, setStats] = useState({
|
||||
activePlugins: 0,
|
||||
totalPlugins: 0,
|
||||
overlayVisible: false,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
loadStats()
|
||||
}, [])
|
||||
|
||||
const loadStats = async () => {
|
||||
const plugins = await invoke<Array<{ active: boolean }>>('get_plugins')
|
||||
setStats({
|
||||
activePlugins: plugins.filter(p => p.active).length,
|
||||
totalPlugins: plugins.length,
|
||||
overlayVisible: false,
|
||||
})
|
||||
}
|
||||
|
||||
const quickActions = [
|
||||
{
|
||||
icon: Search,
|
||||
label: 'Universal Search',
|
||||
shortcut: 'Ctrl+Shift+F',
|
||||
onClick: () => {}
|
||||
},
|
||||
{
|
||||
icon: Layers,
|
||||
label: 'Toggle Overlay',
|
||||
shortcut: 'Ctrl+Shift+U',
|
||||
onClick: () => invoke('toggle_overlay')
|
||||
},
|
||||
{
|
||||
icon: Target,
|
||||
label: 'Loot Tracker',
|
||||
shortcut: 'Ctrl+Shift+L',
|
||||
onClick: () => {}
|
||||
},
|
||||
{
|
||||
icon: TrendingUp,
|
||||
label: 'Price Check',
|
||||
shortcut: 'Ctrl+Shift+C',
|
||||
onClick: () => {}
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Dashboard</h2>
|
||||
<p className="text-text-muted mt-1">Welcome to EU-Utility V3</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="bg-surface rounded-xl p-6 border border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-text-muted text-sm">Active Plugins</p>
|
||||
<p className="text-3xl font-bold text-white mt-1">{stats.activePlugins}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||
<Zap className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface rounded-xl p-6 border border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-text-muted text-sm">Total Plugins</p>
|
||||
<p className="text-3xl font-bold text-white mt-1">{stats.totalPlugins}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-secondary/10 flex items-center justify-center">
|
||||
<Layers className="w-6 h-6 text-secondary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface rounded-xl p-6 border border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-text-muted text-sm">Session Time</p>
|
||||
<p className="text-3xl font-bold text-white mt-1">00:42:15</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-accent/10 flex items-center justify-center">
|
||||
<Clock className="w-6 h-6 text-accent" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface rounded-xl p-6 border border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-text-muted text-sm">System Status</p>
|
||||
<p className="text-3xl font-bold text-accent mt-1">Online</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-accent/10 flex items-center justify-center">
|
||||
<Activity className="w-6 h-6 text-accent" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="bg-surface rounded-xl p-6 border border-border">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Quick Actions</h3>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{quickActions.map((action) => {
|
||||
const Icon = action.icon
|
||||
return (
|
||||
<button
|
||||
key={action.label}
|
||||
onClick={action.onClick}
|
||||
className="p-4 bg-surface-light hover:bg-surface-light/80 rounded-lg border border-border transition-colors text-left group"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-3 group-hover:bg-primary/20 transition-colors">
|
||||
<Icon className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<p className="font-medium text-white">{action.label}</p>
|
||||
<p className="text-xs text-text-muted mt-1">{action.shortcut}</p>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="bg-surface rounded-xl p-6 border border-border">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Recent Activity</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-4 p-3 bg-surface-light rounded-lg">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Zap className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-white">System initialized</p>
|
||||
<p className="text-xs text-text-muted">Just now</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import {
|
||||
Search,
|
||||
Database,
|
||||
TrendingUp,
|
||||
Package,
|
||||
ExternalLink,
|
||||
Clock,
|
||||
AlertCircle
|
||||
} from 'lucide-react'
|
||||
|
||||
interface NexusItem {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
category: string
|
||||
value?: number
|
||||
markup?: number
|
||||
}
|
||||
|
||||
export default function Nexus() {
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState<NexusItem[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedItem, setSelectedItem] = useState<NexusItem | null>(null)
|
||||
const [itemDetails, setItemDetails] = useState<any>(null)
|
||||
const [entityType, setEntityType] = useState('all')
|
||||
|
||||
const searchItems = useCallback(async () => {
|
||||
if (!query.trim()) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await invoke<any>('search_nexus', {
|
||||
query,
|
||||
entityType: entityType === 'all' ? null : entityType,
|
||||
limit: 20
|
||||
})
|
||||
setResults(response?.results || [])
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [query, entityType])
|
||||
|
||||
const loadItemDetails = async (itemId: string) => {
|
||||
try {
|
||||
const details = await invoke<any>('get_item_details', { itemId })
|
||||
setItemDetails(details)
|
||||
} catch (error) {
|
||||
console.error('Failed to load details:', error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (query) searchItems()
|
||||
}, 300)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [query, entityType, searchItems])
|
||||
|
||||
const entityTypes = [
|
||||
{ id: 'all', label: 'All' },
|
||||
{ id: 'items', label: 'Items' },
|
||||
{ id: 'weapons', label: 'Weapons' },
|
||||
{ id: 'armor', label: 'Armor' },
|
||||
{ id: 'mobs', label: 'Creatures' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Nexus</h2>
|
||||
<p className="text-text-muted mt-1">Search Entropia Universe items and market data</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="bg-surface rounded-xl p-6 border border-border">
|
||||
<div className="flex gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="w-5 h-5 text-text-muted absolute left-4 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search for items, weapons, creatures..."
|
||||
className="w-full pl-12 pr-4 py-3 bg-background border border-border rounded-lg text-white focus:outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-4">
|
||||
<span className="text-sm text-text-muted">Filter by:</span>
|
||||
{entityTypes.map((type) => (
|
||||
<button
|
||||
key={type.id}
|
||||
onClick={() => setEntityType(type.id)}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||
entityType === type.id
|
||||
? 'bg-primary text-white'
|
||||
: 'bg-surface-light text-text-muted hover:text-text'
|
||||
}`}
|
||||
>
|
||||
{type.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
{/* Results */}
|
||||
<div className="col-span-2 space-y-4">
|
||||
{loading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||
<p className="text-text-muted">Searching Nexus...</p>
|
||||
</div>
|
||||
) : results.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-text-muted mb-3">{results.length} results found</p>
|
||||
{results.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
setSelectedItem(item)
|
||||
loadItemDetails(item.id)
|
||||
}}
|
||||
className={`w-full flex items-center gap-4 p-4 rounded-lg border transition-colors text-left ${
|
||||
selectedItem?.id === item.id
|
||||
? 'bg-primary/10 border-primary/30'
|
||||
: 'bg-surface border-border hover:border-primary/30'
|
||||
}`}
|
||||
>
|
||||
<div className="w-12 h-12 rounded-lg bg-surface-light flex items-center justify-center flex-shrink-0">
|
||||
<Package className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-semibold text-white truncate">{item.name}</p>
|
||||
<p className="text-sm text-text-muted">{item.category} / {item.type}</p>
|
||||
</div>
|
||||
|
||||
{item.markup && (
|
||||
<div className="text-right">
|
||||
<p className="text-accent font-semibold">{item.markup}%</p>
|
||||
<p className="text-xs text-text-muted">Markup</p>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : query ? (
|
||||
<div className="text-center py-12 bg-surface rounded-xl border border-border">
|
||||
<AlertCircle className="w-12 h-12 text-text-muted mx-auto mb-4" />
|
||||
<p className="text-white font-medium">No results found</p>
|
||||
<p className="text-sm text-text-muted mt-1">Try a different search term</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 bg-surface rounded-xl border border-border">
|
||||
<Database className="w-12 h-12 text-text-muted mx-auto mb-4" />
|
||||
<p className="text-white font-medium">Search Nexus Database</p>
|
||||
<p className="text-sm text-text-muted mt-1">Enter a search term to find items</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Item Details */}
|
||||
<div className="col-span-1">
|
||||
{selectedItem ? (
|
||||
<div className="bg-surface rounded-xl p-6 border border-border sticky top-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-14 h-14 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||
<Package className="w-7 h-7 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-white">{selectedItem.name}</h3>
|
||||
<p className="text-sm text-text-muted">{selectedItem.category}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{itemDetails?.value && (
|
||||
<div className="flex items-center justify-between p-3 bg-surface-light rounded-lg">
|
||||
<span className="text-text-muted">TT Value</span>
|
||||
<span className="text-white font-medium">{itemDetails.value} PED</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{itemDetails?.markup && (
|
||||
<div className="flex items-center justify-between p-3 bg-surface-light rounded-lg">
|
||||
<span className="text-text-muted">Markup</span>
|
||||
<span className="text-accent font-medium">{itemDetails.markup}%</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{itemDetails?.current_price && (
|
||||
<div className="flex items-center justify-between p-3 bg-surface-light rounded-lg">
|
||||
<span className="text-text-muted">Market Price</span>
|
||||
<span className="text-white font-medium">{itemDetails.current_price} PED</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={`https://www.entropianexus.com/item/${selectedItem.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 w-full mt-4 px-4 py-2 bg-primary/10 hover:bg-primary/20 text-primary rounded-lg font-medium transition-colors"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
View on Nexus
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-surface rounded-xl p-6 border border-border text-center">
|
||||
<TrendingUp className="w-10 h-10 text-text-muted mx-auto mb-3" />
|
||||
<p className="text-white font-medium">Select an item</p>
|
||||
<p className="text-sm text-text-muted mt-1">View market data and details</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import {
|
||||
X,
|
||||
Minus,
|
||||
GripVertical,
|
||||
Zap,
|
||||
Target,
|
||||
TrendingUp,
|
||||
Clock,
|
||||
DollarSign
|
||||
} from 'lucide-react'
|
||||
|
||||
export default function Overlay() {
|
||||
const [widgets, setWidgets] = useState<any[]>([])
|
||||
const [sessionTime, setSessionTime] = useState(0)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for widget updates from plugins
|
||||
const unlisten = listen('overlay:widget', (event) => {
|
||||
const { action, widget } = event.payload as any
|
||||
|
||||
if (action === 'add') {
|
||||
setWidgets(prev => [...prev, widget])
|
||||
} else if (action === 'remove') {
|
||||
setWidgets(prev => prev.filter(w => w.id !== widget.id))
|
||||
} else if (action === 'update') {
|
||||
setWidgets(prev =>
|
||||
prev.map(w => w.id === widget.id ? { ...w, ...widget } : w)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Session timer
|
||||
const interval = setInterval(() => {
|
||||
setSessionTime(prev => prev + 1)
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
unlisten.then(f => f())
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const hrs = Math.floor(seconds / 3600)
|
||||
const mins = Math.floor((seconds % 3600) / 60)
|
||||
const secs = seconds % 60
|
||||
return `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
invoke('close_overlay')
|
||||
}
|
||||
|
||||
const handleMinimize = () => {
|
||||
invoke('close_overlay')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen bg-background/95 backdrop-blur-sm text-text select-none">
|
||||
{/* Header */}
|
||||
<div
|
||||
className="h-8 bg-surface/80 border-b border-border flex items-center justify-between px-3 cursor-move"
|
||||
onMouseDown={() => setIsDragging(true)}
|
||||
onMouseUp={() => setIsDragging(false)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<GripVertical className="w-4 h-4 text-text-muted" />
|
||||
<span className="text-xs font-medium text-text-muted">EU-Utility Overlay</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={handleMinimize}
|
||||
className="p-1 hover:bg-surface-light rounded transition-colors"
|
||||
>
|
||||
<Minus className="w-3 h-3 text-text-muted" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="p-1 hover:bg-danger/20 rounded transition-colors"
|
||||
>
|
||||
<X className="w-3 h-3 text-text-muted hover:text-danger" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4 space-y-3">
|
||||
{/* Session Stats */}
|
||||
<div className="bg-surface/80 backdrop-blur rounded-lg p-3 border border-border">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Clock className="w-4 h-4 text-primary" />
|
||||
<span className="text-sm font-medium text-white">Session: {formatTime(sessionTime)}</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="text-center p-2 bg-surface-light/50 rounded">
|
||||
<DollarSign className="w-4 h-4 text-accent mx-auto mb-1" />
|
||||
<p className="text-lg font-bold text-white">0.00</p>
|
||||
<p className="text-xs text-text-muted">PED</p>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-surface-light/50 rounded">
|
||||
<Target className="w-4 h-4 text-secondary mx-auto mb-1" />
|
||||
<p className="text-lg font-bold text-white">0</p>
|
||||
<p className="text-xs text-text-muted">Mobs</p>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-surface-light/50 rounded">
|
||||
<TrendingUp className="w-4 h-4 text-warning mx-auto mb-1" />
|
||||
<p className="text-lg font-bold text-white">0%</p>
|
||||
<p className="text-xs text-text-muted">ROI</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plugin Widgets */}
|
||||
<div className="space-y-2">
|
||||
{widgets.length === 0 ? (
|
||||
<div className="text-center py-8 text-text-muted">
|
||||
<Zap className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">No active widgets</p>
|
||||
<p className="text-xs mt-1">Activate plugins to see widgets</p>
|
||||
</div>
|
||||
) : (
|
||||
widgets.map((widget) => (
|
||||
<div
|
||||
key={widget.id}
|
||||
className="bg-surface/80 backdrop-blur rounded-lg p-3 border border-border"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-medium text-white text-sm">{widget.title}</h4>
|
||||
{widget.value && (
|
||||
<span className="text-lg font-bold text-primary">{widget.value}</span>
|
||||
)}
|
||||
</div>
|
||||
{widget.content && (
|
||||
<div className="text-sm text-text-muted">{widget.content}</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import {
|
||||
Puzzle,
|
||||
Power,
|
||||
Settings2,
|
||||
ExternalLink,
|
||||
Search,
|
||||
Grid,
|
||||
List,
|
||||
AlertCircle
|
||||
} from 'lucide-react'
|
||||
import { useAppStore } from '../store/appStore'
|
||||
import type { Plugin } from '../store/appStore'
|
||||
|
||||
export default function Plugins() {
|
||||
const { plugins, activatePlugin, deactivatePlugin, loadPlugins } = useAppStore()
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
||||
const [selectedPlugin, setSelectedPlugin] = useState<Plugin | null>(null)
|
||||
|
||||
const filteredPlugins = plugins.filter(p =>
|
||||
p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
const activePlugins = filteredPlugins.filter(p => p.active)
|
||||
const inactivePlugins = filteredPlugins.filter(p => !p.active)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Plugins</h2>
|
||||
<p className="text-text-muted mt-1">Manage your EU-Utility plugins</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<Search className="w-5 h-5 text-text-muted absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search plugins..."
|
||||
className="pl-10 pr-4 py-2 bg-surface border border-border rounded-lg text-sm focus:outline-none focus:border-primary w-64"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 bg-surface rounded-lg p-1 border border-border">
|
||||
<button
|
||||
onClick={() => setViewMode('grid')}
|
||||
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-surface-light text-white' : 'text-text-muted'}`}
|
||||
>
|
||||
<Grid className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={`p-2 rounded ${viewMode === 'list' ? 'bg-surface-light text-white' : 'text-text-muted'}`}
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Plugins */}
|
||||
{activePlugins.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-3">Active Plugins ({activePlugins.length})</h3>
|
||||
<div className={viewMode === 'grid' ? 'grid grid-cols-3 gap-4' : 'space-y-2'}>
|
||||
{activePlugins.map((plugin) => (
|
||||
<PluginCard
|
||||
key={plugin.id}
|
||||
plugin={plugin}
|
||||
onActivate={() => activatePlugin(plugin.id)}
|
||||
onDeactivate={() => deactivatePlugin(plugin.id)}
|
||||
onClick={() => setSelectedPlugin(plugin)}
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Inactive Plugins */}
|
||||
{inactivePlugins.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-3">Inactive Plugins ({inactivePlugins.length})</h3>
|
||||
<div className={viewMode === 'grid' ? 'grid grid-cols-3 gap-4' : 'space-y-2'}>
|
||||
{inactivePlugins.map((plugin) => (
|
||||
<PluginCard
|
||||
key={plugin.id}
|
||||
plugin={plugin}
|
||||
onActivate={() => activatePlugin(plugin.id)}
|
||||
onDeactivate={() => deactivatePlugin(plugin.id)}
|
||||
onClick={() => setSelectedPlugin(plugin)}
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{filteredPlugins.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<Puzzle className="w-16 h-16 text-text-muted mx-auto mb-4" />
|
||||
<p className="text-white font-medium">No plugins found</p>
|
||||
<p className="text-text-muted text-sm mt-1">Try adjusting your search</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface PluginCardProps {
|
||||
plugin: Plugin
|
||||
onActivate: () => void
|
||||
onDeactivate: () => void
|
||||
onClick: () => void
|
||||
viewMode: 'grid' | 'list'
|
||||
}
|
||||
|
||||
function PluginCard({ plugin, onActivate, onDeactivate, onClick, viewMode }: PluginCardProps) {
|
||||
if (viewMode === 'list') {
|
||||
return (
|
||||
<div className="flex items-center gap-4 p-4 bg-surface rounded-lg border border-border hover:border-primary/30 transition-colors">
|
||||
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<Puzzle className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-semibold text-white truncate">{plugin.name}</h4>
|
||||
<p className="text-sm text-text-muted truncate">{plugin.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-text-muted">v{plugin.version}</span>
|
||||
|
||||
{plugin.active ? (
|
||||
<button
|
||||
onClick={onDeactivate}
|
||||
className="px-3 py-1.5 bg-accent text-white text-sm font-medium rounded-lg hover:bg-accent-hover transition-colors"
|
||||
>
|
||||
Deactivate
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={onActivate}
|
||||
className="px-3 py-1.5 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-colors"
|
||||
>
|
||||
Activate
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-surface rounded-xl p-5 border border-border hover:border-primary/30 transition-colors group">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||
<Puzzle className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
|
||||
{plugin.active ? (
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 bg-accent/10 rounded-full">
|
||||
<div className="w-2 h-2 rounded-full bg-accent" />
|
||||
<span className="text-xs font-medium text-accent">Active</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 bg-surface-light rounded-full">
|
||||
<div className="w-2 h-2 rounded-full bg-text-muted" />
|
||||
<span className="text-xs font-medium text-text-muted">Inactive</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h4 className="font-semibold text-white mb-1">{plugin.name}</h4>
|
||||
<p className="text-sm text-text-muted mb-4 line-clamp-2">{plugin.description}</p>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-text-muted mb-4">
|
||||
<span>{plugin.author}</span>
|
||||
<span>v{plugin.version}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{plugin.active ? (
|
||||
<>
|
||||
<button
|
||||
onClick={onDeactivate}
|
||||
className="flex-1 px-3 py-2 bg-surface-light hover:bg-surface-light/80 text-text rounded-lg text-sm font-medium transition-colors border border-border"
|
||||
>
|
||||
Deactivate
|
||||
</button>
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="px-3 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg transition-colors"
|
||||
>
|
||||
<Settings2 className="w-4 h-4" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={onActivate}
|
||||
className="w-full px-3 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
Activate
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,349 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import {
|
||||
Settings2,
|
||||
Keyboard,
|
||||
Monitor,
|
||||
Puzzle,
|
||||
Save,
|
||||
RotateCcw
|
||||
} from 'lucide-react'
|
||||
import { useAppStore } from '../store/appStore'
|
||||
|
||||
interface SettingSection {
|
||||
id: string
|
||||
title: string
|
||||
icon: React.ElementType
|
||||
description: string
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
const { settings, updateSetting, loadSettings } = useAppStore()
|
||||
const [activeSection, setActiveSection] = useState('general')
|
||||
const [localSettings, setLocalSettings] = useState(settings)
|
||||
const [hasChanges, setHasChanges] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLocalSettings(settings)
|
||||
}, [settings])
|
||||
|
||||
const sections: SettingSection[] = [
|
||||
{
|
||||
id: 'general',
|
||||
title: 'General',
|
||||
icon: Settings2,
|
||||
description: 'Application behavior and appearance'
|
||||
},
|
||||
{
|
||||
id: 'hotkeys',
|
||||
title: 'Hotkeys',
|
||||
icon: Keyboard,
|
||||
description: 'Keyboard shortcuts configuration'
|
||||
},
|
||||
{
|
||||
id: 'overlay',
|
||||
title: 'Overlay',
|
||||
icon: Monitor,
|
||||
description: 'In-game overlay settings'
|
||||
},
|
||||
{
|
||||
id: 'plugins',
|
||||
title: 'Plugins',
|
||||
icon: Puzzle,
|
||||
description: 'Plugin system configuration'
|
||||
},
|
||||
]
|
||||
|
||||
const handleChange = (key: string, value: unknown) => {
|
||||
const newSettings = { ...localSettings }
|
||||
const keys = key.split('.')
|
||||
let current: Record<string, unknown> = newSettings
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
if (!current[keys[i]]) current[keys[i]] = {}
|
||||
current = current[keys[i]] as Record<string, unknown>
|
||||
}
|
||||
current[keys[keys.length - 1]] = value
|
||||
|
||||
setLocalSettings(newSettings)
|
||||
setHasChanges(true)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
for (const [key, value] of Object.entries(localSettings)) {
|
||||
await updateSetting(key, value)
|
||||
}
|
||||
setHasChanges(false)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setLocalSettings(settings)
|
||||
setHasChanges(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Settings</h2>
|
||||
<p className="text-text-muted mt-1">Configure EU-Utility V3</p>
|
||||
</div>
|
||||
|
||||
{hasChanges && (
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="flex items-center gap-2 px-4 py-2 text-text-muted hover:text-text transition-colors"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6">
|
||||
{/* Sidebar */}
|
||||
<div className="w-64 space-y-2">
|
||||
{sections.map((section) => {
|
||||
const Icon = section.icon
|
||||
return (
|
||||
<button
|
||||
key={section.id}
|
||||
onClick={() => setActiveSection(section.id)}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors text-left ${
|
||||
activeSection === section.id
|
||||
? 'bg-primary/10 text-primary border border-primary/20'
|
||||
: 'text-text-muted hover:text-text hover:bg-surface'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
<span className="font-medium">{section.title}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 bg-surface rounded-xl p-6 border border-border">
|
||||
{activeSection === 'general' && (
|
||||
<GeneralSettings
|
||||
settings={localSettings}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
{activeSection === 'hotkeys' && (
|
||||
<HotkeySettings
|
||||
settings={localSettings}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
{activeSection === 'overlay' && (
|
||||
<OverlaySettings
|
||||
settings={localSettings}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
{activeSection === 'plugins' && (
|
||||
<PluginSettings
|
||||
settings={localSettings}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function GeneralSettings({ settings, onChange }: { settings: any, onChange: (k: string, v: unknown) => void }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-semibold text-white">General Settings</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 bg-surface-light rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium text-white">Start on Boot</p>
|
||||
<p className="text-sm text-text-muted">Launch EU-Utility when system starts</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings?.general?.start_on_boot || false}
|
||||
onChange={(e) => onChange('general.start_on_boot', e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-surface peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-surface-light rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium text-white">Minimize to Tray</p>
|
||||
<p className="text-sm text-text-muted">Keep running in system tray when closed</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings?.general?.minimize_to_tray !== false}
|
||||
onChange={(e) => onChange('general.minimize_to_tray', e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-surface peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-surface-light rounded-lg">
|
||||
<label className="block text-sm font-medium text-white mb-2">Theme</label>
|
||||
<select
|
||||
value={settings?.general?.theme || 'dark'}
|
||||
onChange={(e) => onChange('general.theme', e.target.value)}
|
||||
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-white focus:outline-none focus:border-primary"
|
||||
>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="darker">Darker</option>
|
||||
<option value="midnight">Midnight</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function HotkeySettings({ settings, onChange }: { settings: any, onChange: (k: string, v: unknown) => void }) {
|
||||
const hotkeys = [
|
||||
{ key: 'overlay_toggle', label: 'Toggle Overlay', default: 'Ctrl+Shift+U' },
|
||||
{ key: 'overlay_hide', label: 'Hide Overlay', default: 'Ctrl+Shift+H' },
|
||||
{ key: 'search_universal', label: 'Universal Search', default: 'Ctrl+Shift+F' },
|
||||
{ key: 'search_nexus', label: 'Nexus Search', default: 'Ctrl+Shift+N' },
|
||||
{ key: 'calculator', label: 'Calculator', default: 'Ctrl+Shift+C' },
|
||||
{ key: 'tracker_loot', label: 'Loot Tracker', default: 'Ctrl+Shift+L' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-semibold text-white">Hotkey Configuration</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
{hotkeys.map((hotkey) => (
|
||||
<div key={hotkey.key} className="flex items-center justify-between p-4 bg-surface-light rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium text-white">{hotkey.label}</p>
|
||||
<p className="text-sm text-text-muted">Default: {hotkey.default}</p>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings?.hotkeys?.[hotkey.key] || hotkey.default}
|
||||
onChange={(e) => onChange(`hotkeys.${hotkey.key}`, e.target.value)}
|
||||
className="w-40 px-3 py-2 bg-surface border border-border rounded-lg text-center text-white font-mono text-sm focus:outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function OverlaySettings({ settings, onChange }: { settings: any, onChange: (k: string, v: unknown) => void }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-semibold text-white">Overlay Settings</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-surface-light rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-white font-medium">Opacity</label>
|
||||
<span className="text-text-muted">{Math.round((settings?.overlay?.opacity || 0.9) * 100)}%</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={settings?.overlay?.opacity || 0.9}
|
||||
onChange={(e) => onChange('overlay.opacity', parseFloat(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-surface-light rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium text-white">Click Through</p>
|
||||
<p className="text-sm text-text-muted">Allow clicks to pass through overlay</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings?.overlay?.click_through !== false}
|
||||
onChange={(e) => onChange('overlay.click_through', e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-surface peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-surface-light rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-white font-medium">Auto-hide Delay</label>
|
||||
<span className="text-text-muted">{(settings?.overlay?.auto_hide_delay || 3000) / 1000}s</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="1000"
|
||||
max="10000"
|
||||
step="500"
|
||||
value={settings?.overlay?.auto_hide_delay || 3000}
|
||||
onChange={(e) => onChange('overlay.auto_hide_delay', parseInt(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PluginSettings({ settings, onChange }: { settings: any, onChange: (k: string, v: unknown) => void }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-semibold text-white">Plugin Configuration</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 bg-surface-light rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium text-white">Auto-activate Plugins</p>
|
||||
<p className="text-sm text-text-muted">Automatically activate plugins on startup</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings?.plugins?.auto_activate?.length > 0}
|
||||
className="sr-only peer"
|
||||
readOnly
|
||||
/>
|
||||
<div className="w-11 h-6 bg-surface peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-surface-light rounded-lg">
|
||||
<label className="block text-sm font-medium text-white mb-2">Log Reader Path</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings?.log_reader?.log_path || ''}
|
||||
onChange={(e) => onChange('log_reader.log_path', e.target.value)}
|
||||
placeholder="Auto-detect"
|
||||
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-white focus:outline-none focus:border-primary"
|
||||
/>
|
||||
<p className="text-xs text-text-muted mt-1">Path to Entropia Universe chat log file</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import { create } from 'zustand'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
|
||||
export interface Plugin {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
author: string
|
||||
description: string
|
||||
active: boolean
|
||||
has_overlay: boolean
|
||||
hotkey?: string
|
||||
}
|
||||
|
||||
export interface Hotkey {
|
||||
action: string
|
||||
shortcut: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
plugins: Plugin[]
|
||||
hotkeys: Hotkey[]
|
||||
settings: Record<string, unknown>
|
||||
isLoading: boolean
|
||||
activeTab: string
|
||||
|
||||
// Actions
|
||||
initialize: () => Promise<void>
|
||||
loadPlugins: () => Promise<void>
|
||||
activatePlugin: (id: string) => Promise<void>
|
||||
deactivatePlugin: (id: string) => Promise<void>
|
||||
loadSettings: () => Promise<void>
|
||||
updateSetting: (key: string, value: unknown) => Promise<void>
|
||||
setActiveTab: (tab: string) => void
|
||||
toggleOverlay: () => Promise<void>
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>((set, get) => ({
|
||||
plugins: [],
|
||||
hotkeys: [],
|
||||
settings: {},
|
||||
isLoading: true,
|
||||
activeTab: 'dashboard',
|
||||
|
||||
initialize: async () => {
|
||||
await get().loadPlugins()
|
||||
await get().loadSettings()
|
||||
set({ isLoading: false })
|
||||
|
||||
// Listen for events
|
||||
listen('plugin.activated', () => {
|
||||
get().loadPlugins()
|
||||
})
|
||||
|
||||
listen('plugin.deactivated', () => {
|
||||
get().loadPlugins()
|
||||
})
|
||||
},
|
||||
|
||||
loadPlugins: async () => {
|
||||
try {
|
||||
const plugins = await invoke<Plugin[]>('get_plugins')
|
||||
set({ plugins })
|
||||
} catch (error) {
|
||||
console.error('Failed to load plugins:', error)
|
||||
}
|
||||
},
|
||||
|
||||
activatePlugin: async (id: string) => {
|
||||
try {
|
||||
await invoke('activate_plugin', { id })
|
||||
await get().loadPlugins()
|
||||
} catch (error) {
|
||||
console.error('Failed to activate plugin:', error)
|
||||
}
|
||||
},
|
||||
|
||||
deactivatePlugin: async (id: string) => {
|
||||
try {
|
||||
await invoke('deactivate_plugin', { id })
|
||||
await get().loadPlugins()
|
||||
} catch (error) {
|
||||
console.error('Failed to deactivate plugin:', error)
|
||||
}
|
||||
},
|
||||
|
||||
loadSettings: async () => {
|
||||
try {
|
||||
const settings = await invoke<Record<string, unknown>>('get_settings')
|
||||
const hotkeys = await invoke<Hotkey[]>('get_hotkeys')
|
||||
set({ settings, hotkeys })
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error)
|
||||
}
|
||||
},
|
||||
|
||||
updateSetting: async (key: string, value: unknown) => {
|
||||
try {
|
||||
await invoke('set_settings', { key, value })
|
||||
await get().loadSettings()
|
||||
} catch (error) {
|
||||
console.error('Failed to update setting:', error)
|
||||
}
|
||||
},
|
||||
|
||||
setActiveTab: (tab: string) => {
|
||||
set({ activeTab: tab })
|
||||
},
|
||||
|
||||
toggleOverlay: async () => {
|
||||
try {
|
||||
await invoke('toggle_overlay')
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle overlay:', error)
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
.App {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.loading-shimmer {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
#1e1e2e 0%,
|
||||
#2d2d3d 50%,
|
||||
#1e1e2e 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
.fade-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 200ms;
|
||||
}
|
||||
|
||||
.fade-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.fade-exit-active {
|
||||
opacity: 0;
|
||||
transition: opacity 200ms;
|
||||
}
|
||||
|
||||
/* Overlay specific */
|
||||
.overlay-draggable {
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.overlay-resizable {
|
||||
resize: both;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Hotkey display */
|
||||
.hotkey-badge {
|
||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #1e1e2e;
|
||||
border: 1px solid #3d3d3d;
|
||||
border-radius: 0.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Plugin status indicators */
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-dot.active {
|
||||
background: #10b981;
|
||||
box-shadow: 0 0 8px rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
.status-dot.inactive {
|
||||
background: #64748b;
|
||||
}
|
||||
|
||||
.status-dot.error {
|
||||
background: #ef4444;
|
||||
box-shadow: 0 0 8px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background-color: #0a0a0f;
|
||||
color: #e2e8f0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #13131a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #3d3d3d;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4d4d4d;
|
||||
}
|
||||
|
||||
/* Selection */
|
||||
::selection {
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Focus */
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Range input styling */
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: #1e1e2e;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #6366f1;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb:hover {
|
||||
background: #4f46e5;
|
||||
}
|
||||
|
||||
/* Checkbox toggle */
|
||||
input[type="checkbox"] {
|
||||
accent-color: #6366f1;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-in {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-pulse-slow {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
/* Line clamp */
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.bg-gradient-primary {
|
||||
background: linear-gradient(135deg, #6366f1, #4f46e5);
|
||||
}
|
||||
|
||||
.backdrop-blur-xs {
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
/* Plugin card hover */
|
||||
.plugin-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.plugin-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 40px -10px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
/* Button states */
|
||||
button:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Input focus states */
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: '#0a0a0f',
|
||||
surface: '#13131a',
|
||||
'surface-light': '#1e1e2e',
|
||||
primary: {
|
||||
DEFAULT: '#6366f1',
|
||||
hover: '#4f46e5',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: '#8b5cf6',
|
||||
hover: '#7c3aed',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: '#10b981',
|
||||
hover: '#059669',
|
||||
},
|
||||
warning: '#f59e0b',
|
||||
danger: '#ef4444',
|
||||
text: {
|
||||
DEFAULT: '#e2e8f0',
|
||||
muted: '#94a3b8',
|
||||
dark: '#64748b',
|
||||
},
|
||||
border: 'rgba(99, 102, 241, 0.2)',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
||||
mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.2s ease-out',
|
||||
'slide-up': 'slideUp 0.3s ease-out',
|
||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
},
|
||||
},
|
||||
backdropBlur: {
|
||||
xs: '2px',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
watch: {
|
||||
ignored: ['**/src-tauri/**'],
|
||||
},
|
||||
},
|
||||
envPrefix: ['VITE_', 'TAURI_'],
|
||||
build: {
|
||||
target: process.env.TAURI_PLATFORM == 'windows' ? 'chrome105' : 'safari13',
|
||||
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
|
||||
sourcemap: !!process.env.TAURI_DEBUG,
|
||||
},
|
||||
}))
|
||||
Loading…
Reference in New Issue