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 ## UI Fixes Applied
- **Fixed**: `AttributeError: 'dict' object has no attribute 'lower'` in universal_search plugin - **Removed decorative lines** from header (user complaint about "weird lines")
- Location: `plugins/universal_search/plugin.py` line ~501 - **Added scroll area** to sidebar - prevents expanding out of screen
- Fix: Changed `'click' in item.lower()` to `'click' in item` - **Added drag functionality** - click and drag header to move window
- Committed: `5e08f56` - **Removed ALL emojis** from all plugins (replaced with text only)
## UI Redesign - Spotlight Style ## New Features Implemented
- **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`
## Game Integration Features ### 1. Resizable Window
- Removed `FramelessWindowHint`
- Window now fully resizable with minimum size 600x400
- Shows in taskbar properly
### Floating Icon ### 2. Plugin Index Bug Fix
- Draggable floating button (⚡) for in-game access - **Fixed**: Clicking Spotify opened TP Runner instead
- Positioned at top-left (250, 10) near game UI icons - Root cause: Plugin index tracking was incorrect
- EU-styled: dark blue/gray `rgba(20, 25, 35, 220)` with subtle border - Solution: Track buttons in list, store correct index in UserRole data
- Single-click opens overlay, drag to reposition
- Hover effect with brighter border
- Blue glow effect using QGraphicsDropShadowEffect
- Committed: `b007927`
### Game Reader Plugin (OCR) ### 3. Skill Scanner v2.0 (Complete Rewrite)
- New plugin for reading in-game menus and text **OCR Features:**
- Hotkey: `Ctrl+Shift+R` - Screenshot capture of skills window
- Features: - EasyOCR/Pytesseract fallback
- Screen capture with OCR (EasyOCR or Tesseract) - Parses skill names, ranks, points
- 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`
## User Feedback - Game Screenshots **Log Watching:**
User shared screenshots showing ideal use cases: - Real-time chat.log monitoring
1. **Trade Terminal** - Price checking, category browsing - Automatic skill gain detection
2. **Inventory** - Item management, PED balance (26.02 PED), weight tracking - Patterns: "Aim has improved by 5.2 points", "You gained 10 points in Rifle"
3. **Auction** - Price tracking, markup analysis, bid monitoring - Automatically adds gains to total values
- Real-time gain display in UI
## All Hotkeys ### 4. Profession Scanner (NEW PLUGIN)
| Hotkey | Action | - OCR scan of professions window
|--------|--------| - Tracks profession ranks (Elite, Champion, etc.)
| `Ctrl+Shift+U` | Toggle overlay | - Progress percentage with visual bars
| `Ctrl+Shift+F` | Universal Search | - 21st plugin added to EU-Utility
| `Ctrl+Shift+C` | Calculator |
| `Ctrl+Shift+M` | Spotify |
| `Ctrl+Shift+R` | Game Reader (OCR) |
## Repository ### 5. Customizable Dashboard
- EU-Utility: `https://git.lemonlink.eu/impulsivefps/EU-Utility` - Click "Customize" to select widgets
- Local: `/home/impulsivefps/.openclaw/workspace/projects/EU-Utility/` - Available widgets:
- Run: `python -m core.main` (from projects/EU-Utility/) - 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) ## Commits Made
- Auction Price Tracker - capture listings, track history, price alerts - `5b127cf` - UI fixes (drag, scroll, no emojis)
- Inventory Value Calculator - total TT value, market price comparison - `72c3c13` - Resizable window, OCR scanners, customizable dashboard
- Auto-Capture Mode - detect window type, extract relevant data automatically
- Better OCR region selection for specific windows ## Total Plugins: 21

View File

@ -43,7 +43,8 @@ class PluginManager:
return json.loads(config_path.read_text()) return json.loads(config_path.read_text())
except json.JSONDecodeError: except json.JSONDecodeError:
pass pass
return {"enabled": [], "disabled": [], "settings": {}} # Default: no plugins enabled - all disabled by default
return {"enabled": [], "settings": {}}
def save_config(self) -> None: def save_config(self) -> None:
"""Save plugin configuration.""" """Save plugin configuration."""
@ -51,6 +52,40 @@ class PluginManager:
config_path.parent.mkdir(parents=True, exist_ok=True) config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(json.dumps(self.config, indent=2)) 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]]: def discover_plugins(self) -> List[Type[BasePlugin]]:
"""Discover all available plugin classes with error handling.""" """Discover all available plugin classes with error handling."""
discovered = [] discovered = []
@ -153,18 +188,23 @@ class PluginManager:
return False return False
def load_all_plugins(self) -> None: def load_all_plugins(self) -> None:
"""Discover and load all available plugins.""" """Load only enabled plugins."""
discovered = self.discover_plugins() discovered = self.discover_plugins()
for plugin_class in discovered: for plugin_class in discovered:
plugin_id = f"{plugin_class.__module__}.{plugin_class.__name__}" plugin_id = f"{plugin_class.__module__}.{plugin_class.__name__}"
# Check if disabled # Only load if explicitly enabled
if plugin_id in self.config.get("disabled", []): if self.is_plugin_enabled(plugin_id):
print(f"Skipping disabled plugin: {plugin_class.name}")
continue
self.load_plugin(plugin_class) 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]: def get_plugin(self, plugin_id: str) -> Optional[BasePlugin]:
"""Get a loaded plugin by ID.""" """Get a loaded plugin by ID."""

View File

@ -8,9 +8,9 @@ from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QCheckBox, QLineEdit, QComboBox, QPushButton, QCheckBox, QLineEdit, QComboBox,
QSlider, QTabWidget, QGroupBox, QListWidget, 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 core.settings import get_settings
from plugins.base_plugin import BasePlugin from plugins.base_plugin import BasePlugin
@ -38,7 +38,7 @@ class SettingsPlugin(BasePlugin):
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
# Title # Title
title = QLabel("⚙️ Settings") title = QLabel("Settings")
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
layout.addWidget(title) layout.addWidget(title)
@ -84,7 +84,7 @@ class SettingsPlugin(BasePlugin):
# Save/Reset buttons # Save/Reset buttons
btn_layout = QHBoxLayout() btn_layout = QHBoxLayout()
save_btn = QPushButton("💾 Save Settings") save_btn = QPushButton("Save Settings")
save_btn.setStyleSheet(""" save_btn.setStyleSheet("""
QPushButton { QPushButton {
background-color: #4caf50; background-color: #4caf50;
@ -98,7 +98,7 @@ class SettingsPlugin(BasePlugin):
save_btn.clicked.connect(self._save_settings) save_btn.clicked.connect(self._save_settings)
btn_layout.addWidget(save_btn) btn_layout.addWidget(save_btn)
reset_btn = QPushButton("Reset to Default") reset_btn = QPushButton("Reset to Default")
reset_btn.setStyleSheet(""" reset_btn.setStyleSheet("""
QPushButton { QPushButton {
background-color: rgba(255,255,255,20); background-color: rgba(255,255,255,20);
@ -184,68 +184,116 @@ class SettingsPlugin(BasePlugin):
return tab return tab
def _create_plugins_tab(self): def _create_plugins_tab(self):
"""Create plugins management tab.""" """Create plugins management tab - enable/disable plugins."""
tab = QWidget() tab = QWidget()
layout = QVBoxLayout(tab) layout = QVBoxLayout(tab)
layout.setSpacing(15) layout.setSpacing(15)
# Installed plugins # Info label
plugins_group = QGroupBox("Installed Plugins") info = QLabel("Check plugins to enable them. Uncheck to disable. Changes take effect immediately.")
plugins_group.setStyleSheet(self._group_style()) info.setStyleSheet("color: rgba(255,255,255,150);")
plugins_layout = QVBoxLayout(plugins_group) layout.addWidget(info)
self.plugins_list = QListWidget() # Scroll area for plugin list
self.plugins_list.setStyleSheet(""" scroll = QScrollArea()
QListWidget { scroll.setWidgetResizable(True)
background-color: rgba(30, 35, 45, 200); scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent; border: none;")
scroll_content = QWidget()
plugins_layout = QVBoxLayout(scroll_content)
plugins_layout.setSpacing(8)
plugins_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.plugin_checkboxes = {}
# 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; color: white;
border: 1px solid rgba(100, 110, 130, 80); spacing: 8px;
border-radius: 4px;
} }
QListWidget::item { QCheckBox::indicator {
padding: 8px; width: 18px;
border-bottom: 1px solid rgba(100, 110, 130, 40); height: 18px;
}
QListWidget::item:selected {
background-color: rgba(255, 140, 66, 100);
} }
""") """)
# 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)
# Add sample plugins # Version
plugins = [ version_label = QLabel(f"v{plugin_class.version}")
("Universal Search", True), version_label.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;")
("Calculator", True), row.addWidget(version_label)
("Spotify", True),
("Skill Scanner", True),
("Loot Tracker", True),
("Mining Helper", False),
("Chat Logger", True),
]
for name, enabled in plugins: # Description
item = QListWidgetItem(f"{'' if enabled else ''} {name}") desc_label = QLabel(f"- {plugin_class.description}")
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) desc_label.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;")
item.setCheckState(Qt.CheckState.Checked if enabled else Qt.CheckState.Unchecked) desc_label.setWordWrap(True)
self.plugins_list.addItem(item) row.addWidget(desc_label, 1)
plugins_layout.addWidget(self.plugins_list) row.addStretch()
plugins_layout.addLayout(row)
# Plugin store button # Separator
store_btn = QPushButton("🛒 Open Plugin Store") sep = QFrame()
store_btn.setStyleSheet(""" sep.setFrameShape(QFrame.Shape.HLine)
sep.setStyleSheet("background-color: rgba(100, 110, 130, 40);")
sep.setFixedHeight(1)
plugins_layout.addWidget(sep)
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 { QPushButton {
background-color: #ff8c42; background-color: #4caf50;
color: white; color: white;
padding: 10px; padding: 8px 16px;
border: none; border: none;
border-radius: 4px; 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) disable_all_btn = QPushButton("Disable All")
layout.addStretch() 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 return tab
@ -470,3 +518,44 @@ class SettingsPlugin(BasePlugin):
def _clear_data(self): def _clear_data(self):
"""Clear all data.""" """Clear all data."""
pass 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)