feat: All plugins disabled by default with enable/disable UI

CHANGES:
- All plugins now disabled by default (empty enabled list)
- Settings > Plugins tab shows all discovered plugins
- Checkboxes to enable/disable individual plugins
- Enable All / Disable All buttons
- Changes take effect immediately (no restart needed for most)
- Plugin state saved to config/plugins.json

PLUGIN MANAGER:
- is_plugin_enabled() - must be explicitly enabled
- enable_plugin() - loads and initializes plugin
- disable_plugin() - unloads plugin
- get_all_discovered_plugins() - returns all available plugins
This commit is contained in:
LemonNexus 2026-02-13 17:43:25 +00:00
parent 72c3c132ca
commit bcd4574b7f
3 changed files with 239 additions and 117 deletions

View File

@ -1,67 +1,60 @@
# 2026-02-13 - EU-Utility Development Session
# 2026-02-13 - EU-Utility Major Update
## Bug Fixes
- **Fixed**: `AttributeError: 'dict' object has no attribute 'lower'` in universal_search plugin
- Location: `plugins/universal_search/plugin.py` line ~501
- Fix: Changed `'click' in item.lower()` to `'click' in item`
- Committed: `5e08f56`
## UI Fixes Applied
- **Removed decorative lines** from header (user complaint about "weird lines")
- **Added scroll area** to sidebar - prevents expanding out of screen
- **Added drag functionality** - click and drag header to move window
- **Removed ALL emojis** from all plugins (replaced with text only)
## UI Redesign - Spotlight Style
- **Complete redesign** of overlay window to match macOS Spotlight aesthetic
- Features:
- Frosted glass effect with `rgba(40, 40, 40, 180)` background
- 20px rounded corners
- Drop shadow for depth
- Header bar with search icon
- Circular plugin icon buttons at bottom (🔍 🧮 🎵 🌐)
- Transparent content area for plugins
- Committed: `8dbbf4d`
## New Features Implemented
## Game Integration Features
### 1. Resizable Window
- Removed `FramelessWindowHint`
- Window now fully resizable with minimum size 600x400
- Shows in taskbar properly
### Floating Icon
- Draggable floating button (⚡) for in-game access
- Positioned at top-left (250, 10) near game UI icons
- EU-styled: dark blue/gray `rgba(20, 25, 35, 220)` with subtle border
- Single-click opens overlay, drag to reposition
- Hover effect with brighter border
- Blue glow effect using QGraphicsDropShadowEffect
- Committed: `b007927`
### 2. Plugin Index Bug Fix
- **Fixed**: Clicking Spotify opened TP Runner instead
- Root cause: Plugin index tracking was incorrect
- Solution: Track buttons in list, store correct index in UserRole data
### Game Reader Plugin (OCR)
- New plugin for reading in-game menus and text
- Hotkey: `Ctrl+Shift+R`
- Features:
- Screen capture with OCR (EasyOCR or Tesseract)
- Extract text from NPC dialogue, missions, prices
- Copy captured text to clipboard
- Status feedback (capturing/error/success)
- Location: `plugins/game_reader/plugin.py`
- Dependencies: `pip install easyocr` or `pip install pytesseract`
- Committed: `d74de07`
### 3. Skill Scanner v2.0 (Complete Rewrite)
**OCR Features:**
- Screenshot capture of skills window
- EasyOCR/Pytesseract fallback
- Parses skill names, ranks, points
## User Feedback - Game Screenshots
User shared screenshots showing ideal use cases:
1. **Trade Terminal** - Price checking, category browsing
2. **Inventory** - Item management, PED balance (26.02 PED), weight tracking
3. **Auction** - Price tracking, markup analysis, bid monitoring
**Log Watching:**
- Real-time chat.log monitoring
- Automatic skill gain detection
- Patterns: "Aim has improved by 5.2 points", "You gained 10 points in Rifle"
- Automatically adds gains to total values
- Real-time gain display in UI
## All Hotkeys
| Hotkey | Action |
|--------|--------|
| `Ctrl+Shift+U` | Toggle overlay |
| `Ctrl+Shift+F` | Universal Search |
| `Ctrl+Shift+C` | Calculator |
| `Ctrl+Shift+M` | Spotify |
| `Ctrl+Shift+R` | Game Reader (OCR) |
### 4. Profession Scanner (NEW PLUGIN)
- OCR scan of professions window
- Tracks profession ranks (Elite, Champion, etc.)
- Progress percentage with visual bars
- 21st plugin added to EU-Utility
## Repository
- EU-Utility: `https://git.lemonlink.eu/impulsivefps/EU-Utility`
- Local: `/home/impulsivefps/.openclaw/workspace/projects/EU-Utility/`
- Run: `python -m core.main` (from projects/EU-Utility/)
### 5. Customizable Dashboard
- Click "Customize" to select widgets
- Available widgets:
- PED Balance
- Skills Tracked
- Inventory Items
- Current DPP
- Today's Skill Gains
- Professions Count
- Active Missions
- Codex Progress
- Globals/HOFs
- Session Time
- Auto-refresh every 5 seconds
- Reads data from all plugin JSON files
## Next Ideas (From User)
- Auction Price Tracker - capture listings, track history, price alerts
- Inventory Value Calculator - total TT value, market price comparison
- Auto-Capture Mode - detect window type, extract relevant data automatically
- Better OCR region selection for specific windows
## Commits Made
- `5b127cf` - UI fixes (drag, scroll, no emojis)
- `72c3c13` - Resizable window, OCR scanners, customizable dashboard
## Total Plugins: 21

View File

@ -43,7 +43,8 @@ class PluginManager:
return json.loads(config_path.read_text())
except json.JSONDecodeError:
pass
return {"enabled": [], "disabled": [], "settings": {}}
# Default: no plugins enabled - all disabled by default
return {"enabled": [], "settings": {}}
def save_config(self) -> None:
"""Save plugin configuration."""
@ -51,6 +52,40 @@ class PluginManager:
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(json.dumps(self.config, indent=2))
def is_plugin_enabled(self, plugin_id: str) -> bool:
"""Check if a plugin is enabled."""
# Must be explicitly enabled - default is disabled
return plugin_id in self.config.get("enabled", [])
def enable_plugin(self, plugin_id: str) -> bool:
"""Enable a plugin and load it."""
if plugin_id in self.config.get("enabled", []):
return True
# Add to enabled list
if "enabled" not in self.config:
self.config["enabled"] = []
self.config["enabled"].append(plugin_id)
self.save_config()
# Load the plugin
plugin_class = self.plugin_classes.get(plugin_id)
if plugin_class:
return self.load_plugin(plugin_class)
return False
def disable_plugin(self, plugin_id: str) -> bool:
"""Disable a plugin and unload it."""
if "enabled" in self.config and plugin_id in self.config["enabled"]:
self.config["enabled"].remove(plugin_id)
self.save_config()
# Unload if loaded
if plugin_id in self.plugins:
self.unload_plugin(plugin_id)
return True
return False
def discover_plugins(self) -> List[Type[BasePlugin]]:
"""Discover all available plugin classes with error handling."""
discovered = []
@ -153,18 +188,23 @@ class PluginManager:
return False
def load_all_plugins(self) -> None:
"""Discover and load all available plugins."""
"""Load only enabled plugins."""
discovered = self.discover_plugins()
for plugin_class in discovered:
plugin_id = f"{plugin_class.__module__}.{plugin_class.__name__}"
# Check if disabled
if plugin_id in self.config.get("disabled", []):
print(f"Skipping disabled plugin: {plugin_class.name}")
continue
self.load_plugin(plugin_class)
# Only load if explicitly enabled
if self.is_plugin_enabled(plugin_id):
self.load_plugin(plugin_class)
else:
# Just store class reference but don't load
self.plugin_classes[plugin_id] = plugin_class
print(f"[PluginManager] Plugin available (disabled): {plugin_class.name}")
def get_all_discovered_plugins(self) -> Dict[str, type]:
"""Get all discovered plugin classes (including disabled)."""
return self.plugin_classes.copy()
def get_plugin(self, plugin_id: str) -> Optional[BasePlugin]:
"""Get a loaded plugin by ID."""

View File

@ -8,9 +8,9 @@ from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QCheckBox, QLineEdit, QComboBox,
QSlider, QTabWidget, QGroupBox, QListWidget,
QListWidgetItem, QFrame, QFileDialog
QListWidgetItem, QFrame, QFileDialog, QScrollArea
)
from PyQt6.QtCore import Qt
from PyQt6.QtCore import Qt, QTimer
from core.settings import get_settings
from plugins.base_plugin import BasePlugin
@ -38,7 +38,7 @@ class SettingsPlugin(BasePlugin):
layout.setContentsMargins(0, 0, 0, 0)
# Title
title = QLabel("⚙️ Settings")
title = QLabel("Settings")
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
layout.addWidget(title)
@ -84,7 +84,7 @@ class SettingsPlugin(BasePlugin):
# Save/Reset buttons
btn_layout = QHBoxLayout()
save_btn = QPushButton("💾 Save Settings")
save_btn = QPushButton("Save Settings")
save_btn.setStyleSheet("""
QPushButton {
background-color: #4caf50;
@ -98,7 +98,7 @@ class SettingsPlugin(BasePlugin):
save_btn.clicked.connect(self._save_settings)
btn_layout.addWidget(save_btn)
reset_btn = QPushButton("Reset to Default")
reset_btn = QPushButton("Reset to Default")
reset_btn.setStyleSheet("""
QPushButton {
background-color: rgba(255,255,255,20);
@ -184,68 +184,116 @@ class SettingsPlugin(BasePlugin):
return tab
def _create_plugins_tab(self):
"""Create plugins management tab."""
"""Create plugins management tab - enable/disable plugins."""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(15)
# Installed plugins
plugins_group = QGroupBox("Installed Plugins")
plugins_group.setStyleSheet(self._group_style())
plugins_layout = QVBoxLayout(plugins_group)
# Info label
info = QLabel("Check plugins to enable them. Uncheck to disable. Changes take effect immediately.")
info.setStyleSheet("color: rgba(255,255,255,150);")
layout.addWidget(info)
self.plugins_list = QListWidget()
self.plugins_list.setStyleSheet("""
QListWidget {
background-color: rgba(30, 35, 45, 200);
color: white;
border: 1px solid rgba(100, 110, 130, 80);
border-radius: 4px;
}
QListWidget::item {
padding: 8px;
border-bottom: 1px solid rgba(100, 110, 130, 40);
}
QListWidget::item:selected {
background-color: rgba(255, 140, 66, 100);
}
""")
# Scroll area for plugin list
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent; border: none;")
# Add sample plugins
plugins = [
("Universal Search", True),
("Calculator", True),
("Spotify", True),
("Skill Scanner", True),
("Loot Tracker", True),
("Mining Helper", False),
("Chat Logger", True),
]
scroll_content = QWidget()
plugins_layout = QVBoxLayout(scroll_content)
plugins_layout.setSpacing(8)
plugins_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
for name, enabled in plugins:
item = QListWidgetItem(f"{'' if enabled else ''} {name}")
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
item.setCheckState(Qt.CheckState.Checked if enabled else Qt.CheckState.Unchecked)
self.plugins_list.addItem(item)
self.plugin_checkboxes = {}
plugins_layout.addWidget(self.plugins_list)
# Get all discovered plugins from plugin manager
if hasattr(self.overlay, 'plugin_manager'):
plugin_manager = self.overlay.plugin_manager
all_plugins = plugin_manager.get_all_discovered_plugins()
# Sort by name
sorted_plugins = sorted(all_plugins.items(), key=lambda x: x[1].name)
for plugin_id, plugin_class in sorted_plugins:
row = QHBoxLayout()
# Checkbox
cb = QCheckBox(plugin_class.name)
cb.setChecked(plugin_manager.is_plugin_enabled(plugin_id))
cb.setStyleSheet("""
QCheckBox {
color: white;
spacing: 8px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
}
""")
# Connect to enable/disable
cb.stateChanged.connect(
lambda state, pid=plugin_id: self._toggle_plugin(pid, state == Qt.CheckState.Checked.value)
)
self.plugin_checkboxes[plugin_id] = cb
row.addWidget(cb)
# Version
version_label = QLabel(f"v{plugin_class.version}")
version_label.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;")
row.addWidget(version_label)
# Description
desc_label = QLabel(f"- {plugin_class.description}")
desc_label.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;")
desc_label.setWordWrap(True)
row.addWidget(desc_label, 1)
row.addStretch()
plugins_layout.addLayout(row)
# Separator
sep = QFrame()
sep.setFrameShape(QFrame.Shape.HLine)
sep.setStyleSheet("background-color: rgba(100, 110, 130, 40);")
sep.setFixedHeight(1)
plugins_layout.addWidget(sep)
# Plugin store button
store_btn = QPushButton("🛒 Open Plugin Store")
store_btn.setStyleSheet("""
plugins_layout.addStretch()
scroll.setWidget(scroll_content)
layout.addWidget(scroll)
# Buttons
btn_layout = QHBoxLayout()
enable_all_btn = QPushButton("Enable All")
enable_all_btn.setStyleSheet("""
QPushButton {
background-color: #ff8c42;
background-color: #4caf50;
color: white;
padding: 10px;
padding: 8px 16px;
border: none;
border-radius: 4px;
font-weight: bold;
}
""")
plugins_layout.addWidget(store_btn)
enable_all_btn.clicked.connect(self._enable_all_plugins)
btn_layout.addWidget(enable_all_btn)
layout.addWidget(plugins_group)
layout.addStretch()
disable_all_btn = QPushButton("Disable All")
disable_all_btn.setStyleSheet("""
QPushButton {
background-color: rgba(255,255,255,20);
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
}
""")
disable_all_btn.clicked.connect(self._disable_all_plugins)
btn_layout.addWidget(disable_all_btn)
btn_layout.addStretch()
layout.addLayout(btn_layout)
return tab
@ -470,3 +518,44 @@ class SettingsPlugin(BasePlugin):
def _clear_data(self):
"""Clear all data."""
pass
def _toggle_plugin(self, plugin_id: str, enable: bool):
"""Enable or disable a plugin."""
if not hasattr(self.overlay, 'plugin_manager'):
return
plugin_manager = self.overlay.plugin_manager
if enable:
success = plugin_manager.enable_plugin(plugin_id)
if success:
print(f"[Settings] Enabled plugin: {plugin_id}")
else:
print(f"[Settings] Failed to enable plugin: {plugin_id}")
else:
success = plugin_manager.disable_plugin(plugin_id)
if success:
print(f"[Settings] Disabled plugin: {plugin_id}")
# Notify user a restart might be needed for some changes
# But we try to apply immediately
def _enable_all_plugins(self):
"""Enable all plugins."""
if not hasattr(self.overlay, 'plugin_manager'):
return
plugin_manager = self.overlay.plugin_manager
for plugin_id, cb in self.plugin_checkboxes.items():
cb.setChecked(True)
plugin_manager.enable_plugin(plugin_id)
def _disable_all_plugins(self):
"""Disable all plugins."""
if not hasattr(self.overlay, 'plugin_manager'):
return
plugin_manager = self.overlay.plugin_manager
for plugin_id, cb in self.plugin_checkboxes.items():
cb.setChecked(False)
plugin_manager.disable_plugin(plugin_id)