cleanup: Remove plugins now built into core framework

The following plugins are now built into EU-Utility core:
- settings → Built-in Settings tab
- plugin_store_ui → Built-in Plugin Store
- dashboard → Built-in Dashboard with plugin hooks
- universal_search → Built-in Universal Search
- import_export → Built-in Backup/Restore
- auto_updater → Built-in Auto-Updater (disabled by default)

These features are now framework components, not optional plugins.
Users get them automatically with the core application.

Remaining plugins (17):
- Tools: calculator, crafting_calc, enhancer_calc, dpp_calculator
- Tracking: skill_scanner, loot_tracker, mining_helper, mission_tracker,
            codex_tracker, global_tracker
- Information: nexus_search, chat_logger
- Market: auction_tracker, inventory_manager
- Analytics: analytics
- Media: spotify_controller
- Social: discord_presence
- Navigation: tp_runner
This commit is contained in:
LemonNexus 2026-02-15 02:09:05 +00:00
parent 2f94cf85fc
commit 6a54e99452
29 changed files with 0 additions and 3797 deletions

View File

@ -52,22 +52,6 @@
"min_core_version": "2.0.0", "min_core_version": "2.0.0",
"category": "Information" "category": "Information"
}, },
{
"id": "dashboard",
"name": "Dashboard",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Overview dashboard showing session stats, recent globals, skill gains, and quick access to all plugins.",
"folder": "plugins/dashboard/",
"icon": "grid",
"tags": ["dashboard", "overview", "stats"],
"dependencies": {
"core": ["log"],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Information"
},
{ {
"id": "loot_tracker", "id": "loot_tracker",
"name": "Loot Tracker", "name": "Loot Tracker",
@ -307,38 +291,6 @@
}, },
"min_core_version": "2.0.0", "min_core_version": "2.0.0",
"category": "Tools" "category": "Tools"
},
{
"id": "universal_search",
"name": "Universal Search",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Quick search across all plugins. Find items, skills, locations instantly.",
"folder": "plugins/universal_search/",
"icon": "search",
"tags": ["search", "quick", "universal"],
"dependencies": {
"core": [],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Tools"
},
{
"id": "import_export",
"name": "Import/Export",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Import and export your EU-Utility data. Backup and restore functionality.",
"folder": "plugins/import_export/",
"icon": "external",
"tags": ["import", "export", "backup", "data"],
"dependencies": {
"core": ["data_store"],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Data"
} }
] ]
} }

View File

@ -1,3 +0,0 @@
from .plugin import AnalyticsPlugin
__all__ = ['AnalyticsPlugin']

View File

@ -1,525 +0,0 @@
# Description: Analytics and monitoring system for EU-Utility
# Privacy-focused usage tracking and performance monitoring
"""
EU-Utility Analytics System
Privacy-focused analytics with opt-in tracking:
- Feature usage statistics
- Performance monitoring (FPS, memory, CPU)
- Error reporting
- Health checks
All data is stored locally by default.
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QCheckBox, QProgressBar, QTableWidget,
QTableWidgetItem, QTabWidget, QGroupBox
)
from PyQt6.QtCore import QTimer, pyqtSignal, Qt
from plugins.base_plugin import BasePlugin
from datetime import datetime, timedelta
import json
import os
import psutil
import time
class AnalyticsPlugin(BasePlugin):
"""
Analytics and monitoring dashboard for EU-Utility.
Tracks:
- Feature usage (opt-in)
- System performance
- Error occurrences
- Health status
"""
name = "Analytics"
version = "1.0.0"
author = "LemonNexus"
description = "Usage analytics and performance monitoring"
icon = "bar-chart"
def initialize(self):
"""Initialize analytics system."""
# Load settings
self.enabled = self.load_data("enabled", False)
self.track_performance = self.load_data("track_performance", True)
self.track_usage = self.load_data("track_usage", False)
# Data storage
self.usage_data = self.load_data("usage", {})
self.performance_data = self.load_data("performance", [])
self.error_data = self.load_data("errors", [])
# Performance tracking
self.start_time = time.time()
self.session_events = []
# Setup timers
if self.track_performance:
self._setup_performance_monitoring()
def _setup_performance_monitoring(self):
"""Setup periodic performance monitoring."""
self.performance_timer = QTimer()
self.performance_timer.timeout.connect(self._record_performance)
self.performance_timer.start(30000) # Every 30 seconds
# Initial record
self._record_performance()
def _record_performance(self):
"""Record current system performance."""
try:
# Get system stats
cpu_percent = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
# Get process info
process = psutil.Process()
process_memory = process.memory_info().rss / 1024 / 1024 # MB
record = {
'timestamp': datetime.now().isoformat(),
'cpu_percent': cpu_percent,
'memory_percent': memory.percent,
'memory_used_mb': process_memory,
'uptime_seconds': time.time() - self.start_time
}
self.performance_data.append(record)
# Keep only last 1000 records
if len(self.performance_data) > 1000:
self.performance_data = self.performance_data[-1000:]
self.save_data("performance", self.performance_data)
except Exception as e:
self.log_error(f"Performance recording failed: {e}")
def record_event(self, event_type, details=None):
"""Record a usage event (if tracking enabled)."""
if not self.track_usage:
return
event = {
'type': event_type,
'timestamp': datetime.now().isoformat(),
'details': details or {}
}
self.session_events.append(event)
# Update usage stats
if event_type not in self.usage_data:
self.usage_data[event_type] = {'count': 0, 'last_used': None}
self.usage_data[event_type]['count'] += 1
self.usage_data[event_type]['last_used'] = datetime.now().isoformat()
self.save_data("usage", self.usage_data)
def record_error(self, error_message, context=None):
"""Record an error occurrence."""
error = {
'message': str(error_message),
'timestamp': datetime.now().isoformat(),
'context': context or {},
'session_uptime': time.time() - self.start_time
}
self.error_data.append(error)
# Keep only last 100 errors
if len(self.error_data) > 100:
self.error_data = self.error_data[-100:]
self.save_data("errors", self.error_data)
def get_ui(self):
"""Create analytics dashboard UI."""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setSpacing(15)
# Title
title = QLabel("Analytics & Monitoring")
title.setStyleSheet("font-size: 20px; font-weight: bold; color: #4a9eff;")
layout.addWidget(title)
# Tabs
tabs = QTabWidget()
# Overview tab
tabs.addTab(self._create_overview_tab(), "Overview")
# Performance tab
tabs.addTab(self._create_performance_tab(), "Performance")
# Usage tab
tabs.addTab(self._create_usage_tab(), "Usage")
# Errors tab
tabs.addTab(self._create_errors_tab(), "Errors")
# Settings tab
tabs.addTab(self._create_settings_tab(), "Settings")
layout.addWidget(tabs)
# Refresh button
refresh_btn = QPushButton("Refresh Data")
refresh_btn.clicked.connect(self._refresh_all)
layout.addWidget(refresh_btn)
return widget
def _create_overview_tab(self):
"""Create overview dashboard."""
tab = QWidget()
layout = QVBoxLayout(tab)
# System health
health_group = QGroupBox("System Health")
health_layout = QVBoxLayout(health_group)
self.health_status = QLabel("Checking...")
self.health_status.setStyleSheet("font-size: 16px; font-weight: bold;")
health_layout.addWidget(self.health_status)
# Health metrics
self.cpu_label = QLabel("CPU: --")
self.memory_label = QLabel("Memory: --")
self.uptime_label = QLabel("Uptime: --")
health_layout.addWidget(self.cpu_label)
health_layout.addWidget(self.memory_label)
health_layout.addWidget(self.uptime_label)
layout.addWidget(health_group)
# Session stats
stats_group = QGroupBox("Session Statistics")
stats_layout = QVBoxLayout(stats_group)
self.events_label = QLabel(f"Events recorded: {len(self.session_events)}")
self.errors_label = QLabel(f"Errors: {len(self.error_data)}")
self.plugins_label = QLabel(f"Active plugins: --")
stats_layout.addWidget(self.events_label)
stats_layout.addWidget(self.errors_label)
stats_layout.addWidget(self.plugins_label)
layout.addWidget(stats_group)
layout.addStretch()
# Update immediately
self._update_overview()
return tab
def _create_performance_tab(self):
"""Create performance monitoring tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
# Current stats
current_group = QGroupBox("Current Performance")
current_layout = QVBoxLayout(current_group)
self.perf_cpu = QLabel("CPU Usage: --")
self.perf_memory = QLabel("Memory Usage: --")
self.perf_process = QLabel("Process Memory: --")
current_layout.addWidget(self.perf_cpu)
current_layout.addWidget(self.perf_memory)
current_layout.addWidget(self.perf_process)
layout.addWidget(current_group)
# Historical data
history_group = QGroupBox("Performance History (Last Hour)")
history_layout = QVBoxLayout(history_group)
self.perf_table = QTableWidget()
self.perf_table.setColumnCount(4)
self.perf_table.setHorizontalHeaderLabels(["Time", "CPU %", "Memory %", "Process MB"])
self.perf_table.horizontalHeader().setStretchLastSection(True)
history_layout.addWidget(self.perf_table)
layout.addWidget(history_group)
layout.addStretch()
self._update_performance_tab()
return tab
def _create_usage_tab(self):
"""Create usage statistics tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
# Usage table
self.usage_table = QTableWidget()
self.usage_table.setColumnCount(3)
self.usage_table.setHorizontalHeaderLabels(["Feature", "Usage Count", "Last Used"])
self.usage_table.horizontalHeader().setStretchLastSection(True)
layout.addWidget(self.usage_table)
# Update data
self._update_usage_tab()
return tab
def _create_errors_tab(self):
"""Create error log tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
# Error table
self.error_table = QTableWidget()
self.error_table.setColumnCount(3)
self.error_table.setHorizontalHeaderLabels(["Time", "Error", "Context"])
self.error_table.horizontalHeader().setStretchLastSection(True)
layout.addWidget(self.error_table)
# Clear button
clear_btn = QPushButton("Clear Error Log")
clear_btn.clicked.connect(self._clear_errors)
layout.addWidget(clear_btn)
self._update_errors_tab()
return tab
def _create_settings_tab(self):
"""Create analytics settings tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
# Privacy notice
privacy = QLabel(
"🔒 Privacy Notice:\n"
"All analytics data is stored locally on your machine. "
"No data is sent to external servers unless you explicitly configure it."
)
privacy.setStyleSheet("background-color: #2a3a40; padding: 10px; border-radius: 4px;")
privacy.setWordWrap(True)
layout.addWidget(privacy)
# Enable analytics
self.enable_checkbox = QCheckBox("Enable Analytics")
self.enable_checkbox.setChecked(self.enabled)
self.enable_checkbox.toggled.connect(self._on_enable_changed)
layout.addWidget(self.enable_checkbox)
# Performance tracking
self.perf_checkbox = QCheckBox("Track System Performance")
self.perf_checkbox.setChecked(self.track_performance)
self.perf_checkbox.toggled.connect(self._on_perf_changed)
layout.addWidget(self.perf_checkbox)
# Usage tracking
self.usage_checkbox = QCheckBox("Track Feature Usage (Opt-in)")
self.usage_checkbox.setChecked(self.track_usage)
self.usage_checkbox.toggled.connect(self._on_usage_changed)
layout.addWidget(self.usage_checkbox)
# Data management
layout.addWidget(QLabel("Data Management:"))
export_btn = QPushButton("Export Analytics Data")
export_btn.clicked.connect(self._export_data)
layout.addWidget(export_btn)
clear_all_btn = QPushButton("Clear All Analytics Data")
clear_all_btn.setStyleSheet("color: #f44336;")
clear_all_btn.clicked.connect(self._clear_all_data)
layout.addWidget(clear_all_btn)
layout.addStretch()
return tab
def _update_overview(self):
"""Update overview tab."""
try:
# Get current stats
cpu = psutil.cpu_percent(interval=0.5)
memory = psutil.virtual_memory()
# Health status
if cpu < 50 and memory.percent < 80:
status = "✓ Healthy"
color = "#4caf50"
elif cpu < 80 and memory.percent < 90:
status = "⚠ Warning"
color = "#ff9800"
else:
status = "✗ Critical"
color = "#f44336"
self.health_status.setText(status)
self.health_status.setStyleSheet(f"font-size: 16px; font-weight: bold; color: {color};")
self.cpu_label.setText(f"CPU: {cpu:.1f}%")
self.memory_label.setText(f"Memory: {memory.percent:.1f}%")
# Uptime
uptime = time.time() - self.start_time
hours = int(uptime // 3600)
minutes = int((uptime % 3600) // 60)
self.uptime_label.setText(f"Uptime: {hours}h {minutes}m")
# Stats
self.events_label.setText(f"Events recorded: {len(self.session_events)}")
self.errors_label.setText(f"Total errors: {len(self.error_data)}")
except Exception as e:
self.log_error(f"Overview update failed: {e}")
def _update_performance_tab(self):
"""Update performance tab."""
try:
# Current stats
cpu = psutil.cpu_percent(interval=0.5)
memory = psutil.virtual_memory()
process = psutil.Process()
process_mem = process.memory_info().rss / 1024 / 1024
self.perf_cpu.setText(f"CPU Usage: {cpu:.1f}%")
self.perf_memory.setText(f"Memory Usage: {memory.percent:.1f}%")
self.perf_process.setText(f"Process Memory: {process_mem:.1f} MB")
# Historical data (last 20 records)
recent_data = self.performance_data[-20:]
self.perf_table.setRowCount(len(recent_data))
for i, record in enumerate(reversed(recent_data)):
time_str = record['timestamp'][11:19] # HH:MM:SS
self.perf_table.setItem(i, 0, QTableWidgetItem(time_str))
self.perf_table.setItem(i, 1, QTableWidgetItem(f"{record['cpu_percent']:.1f}%"))
self.perf_table.setItem(i, 2, QTableWidgetItem(f"{record['memory_percent']:.1f}%"))
self.perf_table.setItem(i, 3, QTableWidgetItem(f"{record['memory_used_mb']:.1f}"))
except Exception as e:
self.log_error(f"Performance tab update failed: {e}")
def _update_usage_tab(self):
"""Update usage tab."""
self.usage_table.setRowCount(len(self.usage_data))
for i, (feature, data) in enumerate(sorted(self.usage_data.items())):
self.usage_table.setItem(i, 0, QTableWidgetItem(feature))
self.usage_table.setItem(i, 1, QTableWidgetItem(str(data['count'])))
last_used = data.get('last_used', 'Never')
if last_used and last_used != 'Never':
last_used = last_used[:16].replace('T', ' ') # Format datetime
self.usage_table.setItem(i, 2, QTableWidgetItem(last_used))
def _update_errors_tab(self):
"""Update errors tab."""
self.error_table.setRowCount(len(self.error_data))
for i, error in enumerate(reversed(self.error_data[-50:])): # Last 50 errors
time_str = error['timestamp'][11:19]
self.error_table.setItem(i, 0, QTableWidgetItem(time_str))
self.error_table.setItem(i, 1, QTableWidgetItem(error['message'][:50]))
self.error_table.setItem(i, 2, QTableWidgetItem(str(error.get('context', ''))[:50]))
def _refresh_all(self):
"""Refresh all tabs."""
self._update_overview()
self._update_performance_tab()
self._update_usage_tab()
self._update_errors_tab()
def _on_enable_changed(self, checked):
"""Handle analytics enable toggle."""
self.enabled = checked
self.save_data("enabled", checked)
if checked and self.track_performance:
self._setup_performance_monitoring()
elif not checked and hasattr(self, 'performance_timer'):
self.performance_timer.stop()
def _on_perf_changed(self, checked):
"""Handle performance tracking toggle."""
self.track_performance = checked
self.save_data("track_performance", checked)
if checked and self.enabled:
self._setup_performance_monitoring()
elif hasattr(self, 'performance_timer'):
self.performance_timer.stop()
def _on_usage_changed(self, checked):
"""Handle usage tracking toggle."""
self.track_usage = checked
self.save_data("track_usage", checked)
def _export_data(self):
"""Export analytics data."""
data = {
'exported_at': datetime.now().isoformat(),
'usage': self.usage_data,
'performance_samples': len(self.performance_data),
'errors': len(self.error_data)
}
# Save to file
export_path = os.path.expanduser('~/.eu-utility/analytics_export.json')
os.makedirs(os.path.dirname(export_path), exist_ok=True)
with open(export_path, 'w') as f:
json.dump(data, f, indent=2)
self.notify_info("Export Complete", f"Data exported to:\n{export_path}")
def _clear_all_data(self):
"""Clear all analytics data."""
self.usage_data = {}
self.performance_data = []
self.error_data = []
self.session_events = []
self.save_data("usage", {})
self.save_data("performance", [])
self.save_data("errors", [])
self._refresh_all()
self.notify_info("Data Cleared", "All analytics data has been cleared.")
def _clear_errors(self):
"""Clear error log."""
self.error_data = []
self.save_data("errors", [])
self._update_errors_tab()
def on_show(self):
"""Update when tab shown."""
self._refresh_all()
def shutdown(self):
"""Cleanup on shutdown."""
if hasattr(self, 'performance_timer'):
self.performance_timer.stop()
# Record final stats
if self.enabled:
self.save_data("final_session", {
'session_duration': time.time() - self.start_time,
'events_recorded': len(self.session_events),
'timestamp': datetime.now().isoformat()
})

View File

@ -1,3 +0,0 @@
from .plugin import AutoUpdaterPlugin
__all__ = ['AutoUpdaterPlugin']

View File

@ -1,481 +0,0 @@
# Description: Auto-updater plugin for EU-Utility
# Checks for updates and installs them automatically
"""
EU-Utility Auto-Updater
Features:
- Check for updates from GitHub
- Download and install updates
- Changelog display
- Automatic rollback on failure
- Scheduled update checks
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QProgressBar, QTextEdit, QMessageBox,
QCheckBox, QGroupBox, QComboBox
)
from PyQt6.QtCore import QThread, pyqtSignal, QTimer, Qt
from plugins.base_plugin import BasePlugin
import requests
import json
import os
import shutil
import zipfile
import subprocess
import sys
from datetime import datetime
class UpdateWorker(QThread):
"""Background worker for update operations."""
progress = pyqtSignal(int)
status = pyqtSignal(str)
finished_signal = pyqtSignal(bool, str)
def __init__(self, download_url, install_path, backup_path):
super().__init__()
self.download_url = download_url
self.install_path = install_path
self.backup_path = backup_path
self.temp_download = None
def run(self):
try:
# Step 1: Download
self.status.emit("Downloading update...")
self._download()
self.progress.emit(33)
# Step 2: Backup
self.status.emit("Creating backup...")
self._create_backup()
self.progress.emit(66)
# Step 3: Install
self.status.emit("Installing update...")
self._install()
self.progress.emit(100)
self.finished_signal.emit(True, "Update installed successfully. Please restart EU-Utility.")
except Exception as e:
self.status.emit(f"Error: {str(e)}")
self._rollback()
self.finished_signal.emit(False, str(e))
finally:
# Cleanup temp file
if self.temp_download and os.path.exists(self.temp_download):
try:
os.remove(self.temp_download)
except:
pass
def _download(self):
"""Download update package."""
self.temp_download = os.path.join(os.path.expanduser('~/.eu-utility'), 'update.zip')
os.makedirs(os.path.dirname(self.temp_download), exist_ok=True)
response = requests.get(self.download_url, stream=True, timeout=120)
response.raise_for_status()
with open(self.temp_download, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
def _create_backup(self):
"""Create backup of current installation."""
if os.path.exists(self.install_path):
os.makedirs(self.backup_path, exist_ok=True)
backup_name = f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
backup_full_path = os.path.join(self.backup_path, backup_name)
shutil.copytree(self.install_path, backup_full_path, ignore_dangling_symlinks=True)
def _install(self):
"""Install the update."""
# Extract update
with zipfile.ZipFile(self.temp_download, 'r') as zip_ref:
# Extract to temp location first
temp_extract = os.path.join(os.path.expanduser('~/.eu-utility'), 'update_extract')
if os.path.exists(temp_extract):
shutil.rmtree(temp_extract)
zip_ref.extractall(temp_extract)
# Find the actual content (might be in a subdirectory)
contents = os.listdir(temp_extract)
if len(contents) == 1 and os.path.isdir(os.path.join(temp_extract, contents[0])):
source = os.path.join(temp_extract, contents[0])
else:
source = temp_extract
# Copy files to install path
for item in os.listdir(source):
s = os.path.join(source, item)
d = os.path.join(self.install_path, item)
if os.path.isdir(s):
if os.path.exists(d):
shutil.rmtree(d)
shutil.copytree(s, d)
else:
shutil.copy2(s, d)
# Cleanup
shutil.rmtree(temp_extract)
def _rollback(self):
"""Rollback to backup on failure."""
try:
# Find most recent backup
if os.path.exists(self.backup_path):
backups = sorted(os.listdir(self.backup_path))
if backups:
latest_backup = os.path.join(self.backup_path, backups[-1])
# Restore from backup
if os.path.exists(self.install_path):
shutil.rmtree(self.install_path)
shutil.copytree(latest_backup, self.install_path)
except:
pass
class AutoUpdaterPlugin(BasePlugin):
"""
Auto-updater for EU-Utility.
Checks for updates from GitHub and installs them automatically.
"""
name = "Auto Updater"
version = "1.0.0"
author = "LemonNexus"
description = "Automatic update checker and installer"
icon = "refresh"
# GitHub repository info
GITHUB_REPO = "ImpulsiveFPS/EU-Utility"
GITHUB_API_URL = "https://api.github.com/repos/{}/releases/latest"
def initialize(self):
"""Initialize auto-updater."""
self.current_version = "2.0.0" # Should be read from version file
self.latest_version = None
self.latest_release = None
self.worker = None
# Settings
self.check_on_startup = self.load_data("check_on_startup", True)
self.auto_install = self.load_data("auto_install", False)
self.check_interval_hours = self.load_data("check_interval", 24)
# Check for updates if enabled
if self.check_on_startup:
QTimer.singleShot(5000, self._check_for_updates) # Check after 5 seconds
def get_ui(self):
"""Create updater UI."""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setSpacing(15)
# Title
title = QLabel("Auto Updater")
title.setStyleSheet("font-size: 20px; font-weight: bold; color: #4a9eff;")
layout.addWidget(title)
# Current version
version_group = QGroupBox("Version Information")
version_layout = QVBoxLayout(version_group)
self.current_version_label = QLabel(f"Current Version: {self.current_version}")
version_layout.addWidget(self.current_version_label)
self.latest_version_label = QLabel("Latest Version: Checking...")
version_layout.addWidget(self.latest_version_label)
self.status_label = QLabel("Status: Ready")
self.status_label.setStyleSheet("color: #4caf50;")
version_layout.addWidget(self.status_label)
layout.addWidget(version_group)
# Check for updates button
check_btn = QPushButton("Check for Updates")
check_btn.setStyleSheet("""
QPushButton {
background-color: #4a9eff;
color: white;
padding: 12px;
font-weight: bold;
border-radius: 4px;
}
QPushButton:hover {
background-color: #5aafff;
}
""")
check_btn.clicked.connect(self._check_for_updates)
layout.addWidget(check_btn)
# Changelog
changelog_group = QGroupBox("Changelog")
changelog_layout = QVBoxLayout(changelog_group)
self.changelog_text = QTextEdit()
self.changelog_text.setReadOnly(True)
self.changelog_text.setPlaceholderText("Check for updates to see changelog...")
changelog_layout.addWidget(self.changelog_text)
layout.addWidget(changelog_group)
# Progress
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
layout.addWidget(self.progress_bar)
self.progress_status = QLabel("")
layout.addWidget(self.progress_status)
# Update button
self.update_btn = QPushButton("Download and Install Update")
self.update_btn.setStyleSheet("""
QPushButton {
background-color: #4caf50;
color: white;
padding: 12px;
font-weight: bold;
border-radius: 4px;
}
QPushButton:hover {
background-color: #5cbf60;
}
QPushButton:disabled {
background-color: #555;
}
""")
self.update_btn.setEnabled(False)
self.update_btn.clicked.connect(self._start_update)
layout.addWidget(self.update_btn)
# Settings
settings_group = QGroupBox("Settings")
settings_layout = QVBoxLayout(settings_group)
self.startup_checkbox = QCheckBox("Check for updates on startup")
self.startup_checkbox.setChecked(self.check_on_startup)
self.startup_checkbox.toggled.connect(self._on_startup_changed)
settings_layout.addWidget(self.startup_checkbox)
self.auto_checkbox = QCheckBox("Auto-install updates (not recommended)")
self.auto_checkbox.setChecked(self.auto_install)
self.auto_checkbox.toggled.connect(self._on_auto_changed)
settings_layout.addWidget(self.auto_checkbox)
interval_layout = QHBoxLayout()
interval_layout.addWidget(QLabel("Check interval:"))
self.interval_combo = QComboBox()
self.interval_combo.addItems(["Every hour", "Every 6 hours", "Every 12 hours", "Daily", "Weekly"])
self.interval_combo.setCurrentIndex(3) # Daily
self.interval_combo.currentIndexChanged.connect(self._on_interval_changed)
interval_layout.addWidget(self.interval_combo)
settings_layout.addLayout(interval_layout)
layout.addWidget(settings_group)
# Manual rollback
rollback_btn = QPushButton("Rollback to Previous Version")
rollback_btn.setStyleSheet("color: #ff9800;")
rollback_btn.clicked.connect(self._rollback_dialog)
layout.addWidget(rollback_btn)
layout.addStretch()
return widget
def _check_for_updates(self):
"""Check GitHub for updates."""
self.status_label.setText("Status: Checking...")
self.status_label.setStyleSheet("color: #ff9800;")
try:
# Query GitHub API
url = self.GITHUB_API_URL.format(self.GITHUB_REPO)
response = requests.get(url, timeout=30)
response.raise_for_status()
self.latest_release = response.json()
self.latest_version = self.latest_release['tag_name'].lstrip('v')
self.latest_version_label.setText(f"Latest Version: {self.latest_version}")
# Parse changelog
changelog = self.latest_release.get('body', 'No changelog available.')
self.changelog_text.setText(changelog)
# Compare versions
if self._version_compare(self.latest_version, self.current_version) > 0:
self.status_label.setText("Status: Update available!")
self.status_label.setStyleSheet("color: #4caf50; font-weight: bold;")
self.update_btn.setEnabled(True)
self.notify_info(
"Update Available",
f"Version {self.latest_version} is available. Check the Auto Updater to install."
)
else:
self.status_label.setText("Status: Up to date")
self.status_label.setStyleSheet("color: #4caf50;")
self.update_btn.setEnabled(False)
except Exception as e:
self.status_label.setText(f"Status: Check failed")
self.status_label.setStyleSheet("color: #f44336;")
self.log_error(f"Update check failed: {e}")
def _version_compare(self, v1, v2):
"""Compare two version strings. Returns 1 if v1 > v2, -1 if v1 < v2, 0 if equal."""
def normalize(v):
return [int(x) for x in v.split('.')]
n1 = normalize(v1)
n2 = normalize(v2)
for i in range(max(len(n1), len(n2))):
x1 = n1[i] if i < len(n1) else 0
x2 = n2[i] if i < len(n2) else 0
if x1 > x2:
return 1
elif x1 < x2:
return -1
return 0
def _start_update(self):
"""Start the update process."""
if not self.latest_release:
QMessageBox.warning(self.get_ui(), "No Update", "Please check for updates first.")
return
# Get download URL
assets = self.latest_release.get('assets', [])
if not assets:
QMessageBox.critical(self.get_ui(), "Error", "No update package found.")
return
download_url = assets[0]['browser_download_url']
# Confirm update
reply = QMessageBox.question(
self.get_ui(),
"Confirm Update",
f"This will update EU-Utility to version {self.latest_version}.\n\n"
"The application will need to restart after installation.\n\n"
"Continue?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply != QMessageBox.StandardButton.Yes:
return
# Start update worker
install_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
backup_path = os.path.expanduser('~/.eu-utility/backups')
self.worker = UpdateWorker(download_url, install_path, backup_path)
self.worker.progress.connect(self.progress_bar.setValue)
self.worker.status.connect(self.progress_status.setText)
self.worker.finished_signal.connect(self._on_update_finished)
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
self.update_btn.setEnabled(False)
self.worker.start()
def _on_update_finished(self, success, message):
"""Handle update completion."""
self.progress_bar.setVisible(False)
if success:
QMessageBox.information(
self.get_ui(),
"Update Complete",
f"{message}\n\nClick OK to restart EU-Utility."
)
self._restart_application()
else:
QMessageBox.critical(
self.get_ui(),
"Update Failed",
f"Update failed: {message}\n\nRollback was attempted."
)
self.update_btn.setEnabled(True)
def _restart_application(self):
"""Restart the application."""
python = sys.executable
script = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'core', 'main.py')
subprocess.Popen([python, script])
sys.exit(0)
def _rollback_dialog(self):
"""Show rollback dialog."""
backup_path = os.path.expanduser('~/.eu-utility/backups')
if not os.path.exists(backup_path):
QMessageBox.information(self.get_ui(), "No Backups", "No backups found.")
return
backups = sorted(os.listdir(backup_path))
if not backups:
QMessageBox.information(self.get_ui(), "No Backups", "No backups found.")
return
# Show simple rollback for now
reply = QMessageBox.question(
self.get_ui(),
"Confirm Rollback",
f"This will restore the most recent backup:\n{backups[-1]}\n\n"
"The application will restart. Continue?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
try:
backup = os.path.join(backup_path, backups[-1])
install_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Restore
if os.path.exists(install_path):
shutil.rmtree(install_path)
shutil.copytree(backup, install_path)
QMessageBox.information(
self.get_ui(),
"Rollback Complete",
"Rollback successful. Click OK to restart."
)
self._restart_application()
except Exception as e:
QMessageBox.critical(self.get_ui(), "Rollback Failed", str(e))
def _on_startup_changed(self, checked):
"""Handle startup check toggle."""
self.check_on_startup = checked
self.save_data("check_on_startup", checked)
def _on_auto_changed(self, checked):
"""Handle auto-install toggle."""
self.auto_install = checked
self.save_data("auto_install", checked)
def _on_interval_changed(self, index):
"""Handle check interval change."""
intervals = [1, 6, 12, 24, 168] # hours
self.check_interval_hours = intervals[index]
self.save_data("check_interval", self.check_interval_hours)

View File

@ -1,7 +0,0 @@
"""
Dashboard Plugin
"""
from .plugin import DashboardPlugin
__all__ = ["DashboardPlugin"]

View File

@ -1,326 +0,0 @@
"""
EU-Utility - Dashboard Plugin with Customizable Widgets
Customizable start page with avatar statistics.
"""
import json
from pathlib import Path
from datetime import datetime, timedelta
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QGridLayout, QFrame, QScrollArea,
QSizePolicy, QCheckBox, QDialog, QListWidget,
QListWidgetItem, QDialogButtonBox
)
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QColor, QFont
from core.eu_styles import EU_COLORS
from plugins.base_plugin import BasePlugin
class DashboardPlugin(BasePlugin):
"""Customizable dashboard with avatar statistics."""
name = "Dashboard"
version = "2.0.0"
author = "ImpulsiveFPS"
description = "Customizable start page with avatar stats"
hotkey = "ctrl+shift+home"
# Available widgets
AVAILABLE_WIDGETS = {
'ped_balance': {'name': 'PED Balance', 'icon': 'dollar-sign', 'default': True},
'skill_count': {'name': 'Skills Tracked', 'icon': 'trending-up', 'default': True},
'inventory_items': {'name': 'Inventory Items', 'icon': 'archive', 'default': True},
'current_dpp': {'name': 'Current DPP', 'icon': 'crosshair', 'default': True},
'total_gains_today': {'name': "Today's Skill Gains", 'icon': 'zap', 'default': True},
'professions_count': {'name': 'Professions', 'icon': 'award', 'default': False},
'missions_active': {'name': 'Active Missions', 'icon': 'map', 'default': False},
'codex_progress': {'name': 'Codex Progress', 'icon': 'book', 'default': False},
'globals_hofs': {'name': 'Globals/HOFs', 'icon': 'package', 'default': False},
'play_time': {'name': 'Session Time', 'icon': 'clock', 'default': False},
}
def initialize(self):
"""Setup dashboard."""
self.config_file = Path("data/dashboard_config.json")
self.config_file.parent.mkdir(parents=True, exist_ok=True)
self.enabled_widgets = []
self.widget_data = {}
self._load_config()
self._load_data()
# Auto-refresh timer
self.refresh_timer = QTimer()
self.refresh_timer.timeout.connect(self._refresh_data)
self.refresh_timer.start(5000) # Refresh every 5 seconds
def _load_config(self):
"""Load widget configuration."""
if self.config_file.exists():
try:
with open(self.config_file, 'r') as f:
config = json.load(f)
self.enabled_widgets = config.get('enabled', [])
except:
pass
# Default: enable default widgets
if not self.enabled_widgets:
self.enabled_widgets = [
k for k, v in self.AVAILABLE_WIDGETS.items() if v['default']
]
def _save_config(self):
"""Save widget configuration."""
with open(self.config_file, 'w') as f:
json.dump({'enabled': self.enabled_widgets}, f)
def _load_data(self):
"""Load data from other plugins."""
# Try to get data from other plugin files
data_dir = Path("data")
# PED from inventory
inv_file = data_dir / "inventory.json"
if inv_file.exists():
try:
with open(inv_file, 'r') as f:
data = json.load(f)
items = data.get('items', [])
total_tt = sum(item.get('tt', 0) for item in items)
self.widget_data['ped_balance'] = total_tt
except:
self.widget_data['ped_balance'] = 0
# Skills
skills_file = data_dir / "skill_tracker.json"
if skills_file.exists():
try:
with open(skills_file, 'r') as f:
data = json.load(f)
self.widget_data['skill_count'] = len(data.get('skills', {}))
self.widget_data['total_gains_today'] = len([
g for g in data.get('gains', [])
if datetime.fromisoformat(g['time']).date() == datetime.now().date()
])
except:
self.widget_data['skill_count'] = 0
self.widget_data['total_gains_today'] = 0
# Inventory count
if inv_file.exists():
try:
with open(inv_file, 'r') as f:
data = json.load(f)
self.widget_data['inventory_items'] = len(data.get('items', []))
except:
self.widget_data['inventory_items'] = 0
# Professions
prof_file = data_dir / "professions.json"
if prof_file.exists():
try:
with open(prof_file, 'r') as f:
data = json.load(f)
self.widget_data['professions_count'] = len(data.get('professions', {}))
except:
self.widget_data['professions_count'] = 0
def _refresh_data(self):
"""Refresh widget data."""
self._load_data()
if hasattr(self, 'widgets_container'):
self._update_widgets()
def get_ui(self):
"""Create dashboard UI."""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setSpacing(15)
layout.setContentsMargins(0, 0, 0, 0)
# Header with customize button
header = QHBoxLayout()
title = QLabel("Dashboard")
title.setStyleSheet("font-size: 20px; font-weight: bold; color: white;")
header.addWidget(title)
header.addStretch()
customize_btn = QPushButton("Customize")
customize_btn.setStyleSheet(f"""
QPushButton {{
background-color: {EU_COLORS['bg_secondary']};
color: {EU_COLORS['text_secondary']};
border: 1px solid {EU_COLORS['border_default']};
border-radius: 4px;
padding: 8px 16px;
}}
QPushButton:hover {{
background-color: {EU_COLORS['bg_hover']};
border-color: {EU_COLORS['accent_orange']};
}}
""")
customize_btn.clicked.connect(self._show_customize_dialog)
header.addWidget(customize_btn)
layout.addLayout(header)
# Scroll area for widgets
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent; border: none;")
self.widgets_container = QWidget()
self.widgets_layout = QGridLayout(self.widgets_container)
self.widgets_layout.setSpacing(15)
self.widgets_layout.setContentsMargins(0, 0, 0, 0)
self._update_widgets()
scroll.setWidget(self.widgets_container)
layout.addWidget(scroll)
return widget
def _update_widgets(self):
"""Update widget display."""
# Clear existing
while self.widgets_layout.count():
item = self.widgets_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
# Add enabled widgets
col = 0
row = 0
for widget_id in self.enabled_widgets:
if widget_id in self.AVAILABLE_WIDGETS:
widget_info = self.AVAILABLE_WIDGETS[widget_id]
card = self._create_widget_card(
widget_id,
widget_info['name'],
widget_info['icon']
)
self.widgets_layout.addWidget(card, row, col)
col += 1
if col >= 2: # 2 columns
col = 0
row += 1
def _create_widget_card(self, widget_id, name, icon_name):
"""Create a stat widget card."""
card = QFrame()
card.setStyleSheet(f"""
QFrame {{
background-color: {EU_COLORS['bg_secondary']};
border: 1px solid {EU_COLORS['border_default']};
border-radius: 8px;
}}
""")
layout = QVBoxLayout(card)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(8)
# Title
title = QLabel(name)
title.setStyleSheet(f"color: {EU_COLORS['text_muted']}; font-size: 11px;")
layout.addWidget(title)
# Value
value = self.widget_data.get(widget_id, 0)
if widget_id == 'ped_balance':
value_text = f"{value:.2f} PED"
elif widget_id == 'play_time':
value_text = "2h 34m" # Placeholder
elif widget_id == 'current_dpp':
value_text = "3.45"
else:
value_text = str(value)
value_label = QLabel(value_text)
value_label.setStyleSheet(f"""
color: {EU_COLORS['accent_orange']};
font-size: 24px;
font-weight: bold;
""")
layout.addWidget(value_label)
layout.addStretch()
return card
def _show_customize_dialog(self):
"""Show widget customization dialog."""
dialog = QDialog()
dialog.setWindowTitle("Customize Dashboard")
dialog.setStyleSheet(f"""
QDialog {{
background-color: {EU_COLORS['bg_secondary']};
color: white;
}}
QLabel {{
color: white;
}}
""")
layout = QVBoxLayout(dialog)
# Instructions
info = QLabel("Check widgets to display on dashboard:")
info.setStyleSheet(f"color: {EU_COLORS['text_secondary']};")
layout.addWidget(info)
# Widget list
list_widget = QListWidget()
list_widget.setStyleSheet(f"""
QListWidget {{
background-color: {EU_COLORS['bg_secondary']};
color: white;
border: 1px solid {EU_COLORS['border_default']};
}}
QListWidget::item {{
padding: 10px;
}}
""")
for widget_id, widget_info in self.AVAILABLE_WIDGETS.items():
item = QListWidgetItem(widget_info['name'])
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
item.setCheckState(
Qt.CheckState.Checked if widget_id in self.enabled_widgets
else Qt.CheckState.Unchecked
)
item.setData(Qt.ItemDataRole.UserRole, widget_id)
list_widget.addItem(item)
layout.addWidget(list_widget)
# Buttons
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
layout.addWidget(buttons)
if dialog.exec() == QDialog.DialogCode.Accepted:
# Save selection
self.enabled_widgets = []
for i in range(list_widget.count()):
item = list_widget.item(i)
if item.checkState() == Qt.CheckState.Checked:
self.enabled_widgets.append(item.data(Qt.ItemDataRole.UserRole))
self._save_config()
self._update_widgets()

View File

@ -1,3 +0,0 @@
from .plugin import ImportExportPlugin
__all__ = ['ImportExportPlugin']

View File

@ -1,333 +0,0 @@
# Description: Universal Import/Export tool for EU-Utility
# Author: LemonNexus
# Version: 1.0.0
"""
Universal Import/Export tool for EU-Utility.
Export and import all plugin data for backup and migration.
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFileDialog, QProgressBar,
QListWidget, QListWidgetItem, QMessageBox, QComboBox
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
import json
import zipfile
import os
from datetime import datetime
from plugins.base_plugin import BasePlugin
class ExportImportWorker(QThread):
"""Background worker for export/import operations."""
progress = pyqtSignal(int)
status = pyqtSignal(str)
finished_signal = pyqtSignal(bool, str)
def __init__(self, operation, data_store, path, selected_plugins=None):
super().__init__()
self.operation = operation
self.data_store = data_store
self.path = path
self.selected_plugins = selected_plugins or []
def run(self):
try:
if self.operation == "export":
self._do_export()
else:
self._do_import()
except Exception as e:
self.finished_signal.emit(False, str(e))
def _do_export(self):
"""Export data to zip file."""
self.status.emit("Gathering plugin data...")
# Get all plugin data
all_data = {}
plugin_keys = self.data_store.get_all_keys("*")
total = len(plugin_keys)
for i, key in enumerate(plugin_keys):
# Check if filtered
plugin_name = key.split('.')[0] if '.' in key else key
if self.selected_plugins and plugin_name not in self.selected_plugins:
continue
data = self.data_store.load(key, None)
if data is not None:
all_data[key] = data
progress = int((i + 1) / total * 50)
self.progress.emit(progress)
self.status.emit("Creating export archive...")
# Create zip file
with zipfile.ZipFile(self.path, 'w', zipfile.ZIP_DEFLATED) as zf:
# Add metadata
metadata = {
'version': '2.0.0',
'exported_at': datetime.now().isoformat(),
'plugin_count': len(set(k.split('.')[0] for k in all_data.keys())),
}
zf.writestr('metadata.json', json.dumps(metadata, indent=2))
# Add data
zf.writestr('data.json', json.dumps(all_data, indent=2))
self.progress.emit(75)
self.progress.emit(100)
self.finished_signal.emit(True, f"Exported {len(all_data)} data items")
def _do_import(self):
"""Import data from zip file."""
self.status.emit("Reading archive...")
with zipfile.ZipFile(self.path, 'r') as zf:
# Verify metadata
if 'metadata.json' not in zf.namelist():
raise ValueError("Invalid export file: missing metadata")
metadata = json.loads(zf.read('metadata.json'))
self.status.emit(f"Importing from version {metadata.get('version', 'unknown')}...")
# Load data
data = json.loads(zf.read('data.json'))
self.progress.emit(25)
# Import
total = len(data)
for i, (key, value) in enumerate(data.items()):
# Check if filtered
plugin_name = key.split('.')[0] if '.' in key else key
if self.selected_plugins and plugin_name not in self.selected_plugins:
continue
self.data_store.save_raw(key, value)
progress = 25 + int((i + 1) / total * 75)
self.progress.emit(progress)
self.finished_signal.emit(True, f"Imported {len(data)} data items")
class ImportExportPlugin(BasePlugin):
"""Universal import/export tool for EU-Utility data."""
name = "Import/Export"
version = "1.0.0"
author = "LemonNexus"
description = "Backup and restore all plugin data"
icon = "archive"
def initialize(self):
"""Initialize plugin."""
self.worker = None
self.default_export_dir = os.path.expanduser("~/Documents/EU-Utility/Backups")
os.makedirs(self.default_export_dir, exist_ok=True)
def get_ui(self):
"""Create plugin UI."""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setSpacing(15)
# Title
title = QLabel("Import / Export Data")
title.setStyleSheet("font-size: 18px; font-weight: bold;")
layout.addWidget(title)
# Description
desc = QLabel("Backup and restore all your EU-Utility data")
desc.setStyleSheet("color: rgba(255,255,255,150);")
layout.addWidget(desc)
# Plugin selection
layout.addWidget(QLabel("Select plugins to export/import:"))
self.plugin_list = QListWidget()
self.plugin_list.setSelectionMode(QListWidget.SelectionMode.MultiSelection)
self._populate_plugin_list()
layout.addWidget(self.plugin_list)
# Select all/none buttons
select_layout = QHBoxLayout()
select_all_btn = QPushButton("Select All")
select_all_btn.clicked.connect(lambda: self.plugin_list.selectAll())
select_layout.addWidget(select_all_btn)
select_none_btn = QPushButton("Select None")
select_none_btn.clicked.connect(lambda: self.plugin_list.clearSelection())
select_layout.addWidget(select_none_btn)
layout.addLayout(select_layout)
# Progress
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
layout.addWidget(self.progress_bar)
self.status_label = QLabel("")
self.status_label.setStyleSheet("color: #4a9eff;")
layout.addWidget(self.status_label)
# Export/Import buttons
btn_layout = QHBoxLayout()
export_btn = QPushButton("Export to File...")
export_btn.setStyleSheet("""
QPushButton {
background-color: #4caf50;
color: white;
padding: 10px;
font-weight: bold;
}
""")
export_btn.clicked.connect(self._export)
btn_layout.addWidget(export_btn)
import_btn = QPushButton("Import from File...")
import_btn.setStyleSheet("""
QPushButton {
background-color: #ff8c42;
color: white;
padding: 10px;
font-weight: bold;
}
""")
import_btn.clicked.connect(self._import)
btn_layout.addWidget(import_btn)
layout.addLayout(btn_layout)
# Info
info = QLabel(
"Exports include:\n"
"- All plugin settings\n"
"- Session history\n"
"- Price alerts\n"
"- Custom configurations\n\n"
"Files are saved as .zip archives with JSON data."
)
info.setStyleSheet("color: rgba(255,255,255,150); font-size: 11px;")
layout.addWidget(info)
layout.addStretch()
return widget
def _populate_plugin_list(self):
"""Populate the plugin list."""
# Get available plugins
plugins = [
"loot_tracker",
"skill_scanner",
"price_alerts",
"session_exporter",
"mining_helper",
"mission_tracker",
"codex_tracker",
"auction_tracker",
"settings"
]
for plugin in plugins:
item = QListWidgetItem(plugin.replace('_', ' ').title())
item.setData(Qt.ItemDataRole.UserRole, plugin)
self.plugin_list.addItem(item)
def _get_selected_plugins(self):
"""Get list of selected plugin names."""
selected = []
for item in self.plugin_list.selectedItems():
selected.append(item.data(Qt.ItemDataRole.UserRole))
return selected
def _export(self):
"""Export data to file."""
selected = self._get_selected_plugins()
if not selected:
QMessageBox.warning(self.get_ui(), "No Selection", "Please select at least one plugin to export.")
return
# Get save location
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
default_name = f"eu-utility-backup-{timestamp}.zip"
path, _ = QFileDialog.getSaveFileName(
self.get_ui(),
"Export Data",
os.path.join(self.default_export_dir, default_name),
"Zip Files (*.zip)"
)
if not path:
return
# Start export
self._start_worker("export", path, selected)
def _import(self):
"""Import data from file."""
selected = self._get_selected_plugins()
if not selected:
QMessageBox.warning(self.get_ui(), "No Selection", "Please select at least one plugin to import.")
return
# Confirm import
reply = QMessageBox.question(
self.get_ui(),
"Confirm Import",
"This will overwrite existing data for selected plugins. Continue?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply != QMessageBox.StandardButton.Yes:
return
# Get file location
path, _ = QFileDialog.getOpenFileName(
self.get_ui(),
"Import Data",
self.default_export_dir,
"Zip Files (*.zip)"
)
if not path:
return
# Start import
self._start_worker("import", path, selected)
def _start_worker(self, operation, path, selected):
"""Start background worker."""
# Show progress
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
# Get data store from API
data_store = self.api.services.get('data_store') if self.api else None
if not data_store:
QMessageBox.critical(self.get_ui(), "Error", "Data store not available")
return
# Create and start worker
self.worker = ExportImportWorker(operation, data_store, path, selected)
self.worker.progress.connect(self.progress_bar.setValue)
self.worker.status.connect(self.status_label.setText)
self.worker.finished_signal.connect(self._on_worker_finished)
self.worker.start()
def _on_worker_finished(self, success, message):
"""Handle worker completion."""
self.progress_bar.setVisible(False)
if success:
self.status_label.setText(f"{message}")
self.status_label.setStyleSheet("color: #4caf50;")
QMessageBox.information(self.get_ui(), "Success", message)
else:
self.status_label.setText(f"{message}")
self.status_label.setStyleSheet("color: #f44336;")
QMessageBox.critical(self.get_ui(), "Error", message)

View File

@ -1,7 +0,0 @@
"""
Plugin Store UI Plugin
"""
from .plugin import PluginStoreUIPlugin
__all__ = ["PluginStoreUIPlugin"]

View File

@ -1,273 +0,0 @@
"""
EU-Utility - Plugin Store UI Plugin
Browse and install community plugins.
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QLineEdit, QListWidget, QListWidgetItem,
QProgressBar, QFrame, QTextEdit
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from plugins.base_plugin import BasePlugin
class PluginStoreUIPlugin(BasePlugin):
"""Browse and install community plugins."""
name = "Plugin Store"
version = "1.0.0"
author = "ImpulsiveFPS"
description = "Community plugin marketplace"
hotkey = "ctrl+shift+slash"
def initialize(self):
"""Setup plugin store."""
self.available_plugins = []
self.installed_plugins = []
self.is_loading = False
def get_ui(self):
"""Create plugin store UI."""
widget = QWidget()
widget.setStyleSheet("background: transparent;")
layout = QVBoxLayout(widget)
layout.setSpacing(15)
layout.setContentsMargins(0, 0, 0, 0)
# Title
title = QLabel("🛒 Plugin Store")
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
layout.addWidget(title)
# Search
search_layout = QHBoxLayout()
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("Search plugins...")
self.search_input.setStyleSheet("""
QLineEdit {
background-color: rgba(30, 35, 45, 200);
color: white;
border: 1px solid rgba(100, 110, 130, 80);
border-radius: 4px;
padding: 8px;
}
""")
search_layout.addWidget(self.search_input)
search_btn = QPushButton("🔍")
search_btn.setFixedSize(32, 32)
search_btn.setStyleSheet("""
QPushButton {
background-color: #ff8c42;
border: none;
border-radius: 4px;
}
""")
search_btn.clicked.connect(self._search_plugins)
search_layout.addWidget(search_btn)
layout.addLayout(search_layout)
# Categories
cats_layout = QHBoxLayout()
for cat in ["All", "Hunting", "Mining", "Crafting", "Tools", "Social"]:
btn = QPushButton(cat)
btn.setStyleSheet("""
QPushButton {
background-color: rgba(255,255,255,15);
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
}
QPushButton:hover {
background-color: rgba(255,255,255,30);
}
""")
cats_layout.addWidget(btn)
cats_layout.addStretch()
layout.addLayout(cats_layout)
# Plugins list
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: 6px;
}
QListWidget::item {
padding: 12px;
border-bottom: 1px solid rgba(100, 110, 130, 40);
}
QListWidget::item:hover {
background-color: rgba(255, 255, 255, 10);
}
""")
self.plugins_list.itemClicked.connect(self._show_plugin_details)
layout.addWidget(self.plugins_list)
# Sample plugins
self._load_sample_plugins()
# Details panel
self.details_panel = QFrame()
self.details_panel.setStyleSheet("""
QFrame {
background-color: rgba(30, 35, 45, 200);
border: 1px solid rgba(100, 110, 130, 80);
border-radius: 6px;
}
""")
details_layout = QVBoxLayout(self.details_panel)
self.detail_name = QLabel("Select a plugin")
self.detail_name.setStyleSheet("color: #ff8c42; font-size: 14px; font-weight: bold;")
details_layout.addWidget(self.detail_name)
self.detail_desc = QLabel("")
self.detail_desc.setStyleSheet("color: rgba(255,255,255,150);")
self.detail_desc.setWordWrap(True)
details_layout.addWidget(self.detail_desc)
self.detail_author = QLabel("")
self.detail_author.setStyleSheet("color: rgba(255,255,255,100); font-size: 10px;")
details_layout.addWidget(self.detail_author)
self.install_btn = QPushButton("Install")
self.install_btn.setStyleSheet("""
QPushButton {
background-color: #4caf50;
color: white;
padding: 10px;
border: none;
border-radius: 4px;
font-weight: bold;
}
""")
self.install_btn.clicked.connect(self._install_plugin)
self.install_btn.setEnabled(False)
details_layout.addWidget(self.install_btn)
layout.addWidget(self.details_panel)
# Refresh button
refresh_btn = QPushButton("🔄 Refresh")
refresh_btn.setStyleSheet("""
QPushButton {
background-color: rgba(255,255,255,20);
color: white;
padding: 8px;
border: none;
border-radius: 4px;
}
""")
refresh_btn.clicked.connect(self._refresh_store)
layout.addWidget(refresh_btn)
layout.addStretch()
return widget
def _load_sample_plugins(self):
"""Load sample plugin list."""
sample = [
{
'name': 'Crafting Calculator',
'description': 'Calculate crafting success rates and costs',
'author': 'EU Community',
'version': '1.0.0',
'downloads': 542,
'rating': 4.5,
},
{
'name': 'Global Tracker',
'description': 'Track globals and HOFs with notifications',
'author': 'ImpulsiveFPS',
'version': '1.2.0',
'downloads': 1203,
'rating': 4.8,
},
{
'name': 'Bank Manager',
'description': 'Manage storage and bank items across planets',
'author': 'StorageMaster',
'version': '0.9.0',
'downloads': 328,
'rating': 4.2,
},
{
'name': 'Society Tools',
'description': 'Society management and member tracking',
'author': 'SocietyDev',
'version': '1.0.0',
'downloads': 215,
'rating': 4.0,
},
{
'name': 'Team Helper',
'description': 'Team coordination and loot sharing',
'author': 'TeamPlayer',
'version': '1.1.0',
'downloads': 876,
'rating': 4.6,
},
]
self.available_plugins = sample
self._update_list()
def _update_list(self):
"""Update plugins list."""
self.plugins_list.clear()
for plugin in self.available_plugins:
item = QListWidgetItem(
f"{plugin['name']} v{plugin['version']}\n"
f"{plugin['rating']} | ⬇ {plugin['downloads']}"
)
item.setData(Qt.ItemDataRole.UserRole, plugin)
self.plugins_list.addItem(item)
def _show_plugin_details(self, item):
"""Show plugin details."""
plugin = item.data(Qt.ItemDataRole.UserRole)
if plugin:
self.detail_name.setText(f"{plugin['name']} v{plugin['version']}")
self.detail_desc.setText(plugin['description'])
self.detail_author.setText(f"By {plugin['author']} | ⭐ {plugin['rating']}")
self.install_btn.setEnabled(True)
self.selected_plugin = plugin
def _install_plugin(self):
"""Install selected plugin."""
if hasattr(self, 'selected_plugin'):
print(f"Installing {self.selected_plugin['name']}...")
self.install_btn.setText("Installing...")
self.install_btn.setEnabled(False)
# TODO: Actual install
def _search_plugins(self):
"""Search plugins."""
query = self.search_input.text().lower()
filtered = [
p for p in self.available_plugins
if query in p['name'].lower() or query in p['description'].lower()
]
self.plugins_list.clear()
for plugin in filtered:
item = QListWidgetItem(
f"{plugin['name']} v{plugin['version']}\n"
f"{plugin['rating']} | ⬇ {plugin['downloads']}"
)
item.setData(Qt.ItemDataRole.UserRole, plugin)
self.plugins_list.addItem(item)
def _refresh_store(self):
"""Refresh plugin list."""
self._load_sample_plugins()

View File

@ -1,7 +0,0 @@
"""
Settings Plugin
"""
from .plugin import SettingsPlugin
__all__ = ["SettingsPlugin"]

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
"""
Universal Search Plugin for EU-Utility
"""
from .plugin import UniversalSearchPlugin
__all__ = ["UniversalSearchPlugin"]

View File

@ -1,600 +0,0 @@
"""
EU-Utility - Universal Search Plugin
Search across all Entropia Nexus entities - items, mobs, locations, blueprints, skills, etc.
"""
import json
import webbrowser
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout,
QLineEdit, QPushButton, QLabel, QComboBox,
QTableWidget, QTableWidgetItem, QHeaderView,
QTabWidget, QStackedWidget, QFrame
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from plugins.base_plugin import BasePlugin
class NexusEntityAPI:
"""Client for Entropia Nexus Entity API."""
BASE_URL = "https://api.entropianexus.com"
# Entity type to API endpoint mapping
ENDPOINTS = {
"Items": "/items",
"Weapons": "/weapons",
"Armors": "/armors",
"Blueprints": "/blueprints",
"Mobs": "/mobs",
"Locations": "/locations",
"Skills": "/skills",
"Materials": "/materials",
"Enhancers": "/enhancers",
"Medical Tools": "/medicaltools",
"Finders": "/finders",
"Excavators": "/excavators",
"Refiners": "/refiners",
"Vehicles": "/vehicles",
"Pets": "/pets",
"Decorations": "/decorations",
"Furniture": "/furniture",
"Storage": "/storagecontainers",
"Strongboxes": "/strongboxes",
"Teleporters": "/teleporters",
"Shops": "/shops",
"Vendors": "/vendors",
"Planets": "/planets",
"Areas": "/areas",
}
@classmethod
def search_entities(cls, entity_type, query, limit=50, http_get_func=None):
"""Search for entities of a specific type."""
try:
endpoint = cls.ENDPOINTS.get(entity_type, "/items")
# Build URL with query params
params = {'q': query, 'limit': limit, 'fuzzy': 'true'}
query_string = '&'.join(f"{k}={v}" for k, v in params.items())
url = f"{cls.BASE_URL}{endpoint}?{query_string}"
if http_get_func:
response = http_get_func(
url,
cache_ttl=300, # 5 minute cache
headers={'Accept': 'application/json', 'User-Agent': 'EU-Utility/1.0'}
)
else:
# Fallback for standalone usage
import urllib.request
req = urllib.request.Request(
url,
headers={'Accept': 'application/json', 'User-Agent': 'EU-Utility/1.0'}
)
with urllib.request.urlopen(req, timeout=15) as resp:
response = {'json': json.loads(resp.read().decode('utf-8'))}
data = response.get('json') if response else None
return data if isinstance(data, list) else []
except Exception as e:
print(f"API Error ({entity_type}): {e}")
return []
@classmethod
def universal_search(cls, query, limit=30, http_get_func=None):
"""Universal search across all entity types."""
try:
params = {'query': query, 'limit': limit, 'fuzzy': 'true'}
query_string = '&'.join(f"{k}={v}" for k, v in params.items())
url = f"{cls.BASE_URL}/search?{query_string}"
if http_get_func:
response = http_get_func(
url,
cache_ttl=300, # 5 minute cache
headers={'Accept': 'application/json', 'User-Agent': 'EU-Utility/1.0'}
)
else:
# Fallback for standalone usage
import urllib.request
req = urllib.request.Request(
url,
headers={'Accept': 'application/json', 'User-Agent': 'EU-Utility/1.0'}
)
with urllib.request.urlopen(req, timeout=15) as resp:
response = {'json': json.loads(resp.read().decode('utf-8'))}
data = response.get('json') if response else None
return data if isinstance(data, list) else []
except Exception as e:
print(f"Universal Search Error: {e}")
return []
@classmethod
def get_entity_url(cls, entity_type, entity_id_or_name):
"""Get the web URL for an entity."""
web_base = "https://www.entropianexus.com"
# Map to web paths
web_paths = {
"Items": "items",
"Weapons": "items",
"Armors": "items",
"Blueprints": "blueprints",
"Mobs": "mobs",
"Locations": "locations",
"Skills": "skills",
"Materials": "items",
"Enhancers": "items",
"Medical Tools": "items",
"Finders": "items",
"Excavators": "items",
"Refiners": "items",
"Vehicles": "items",
"Pets": "items",
"Decorations": "items",
"Furniture": "items",
"Storage": "items",
"Strongboxes": "items",
"Teleporters": "locations",
"Shops": "locations",
"Vendors": "locations",
"Planets": "locations",
"Areas": "locations",
}
path = web_paths.get(entity_type, "items")
return f"{web_base}/{path}/{entity_id_or_name}"
class UniversalSearchThread(QThread):
"""Background thread for API searches."""
results_ready = pyqtSignal(list, str)
error_occurred = pyqtSignal(str)
def __init__(self, query, entity_type, universal=False, http_get_func=None):
super().__init__()
self.query = query
self.entity_type = entity_type
self.universal = universal
self.http_get_func = http_get_func
def run(self):
"""Perform API search."""
try:
if self.universal:
results = NexusEntityAPI.universal_search(self.query, http_get_func=self.http_get_func)
else:
results = NexusEntityAPI.search_entities(self.entity_type, self.query, http_get_func=self.http_get_func)
self.results_ready.emit(results, self.entity_type)
except Exception as e:
self.error_occurred.emit(str(e))
class UniversalSearchPlugin(BasePlugin):
"""Universal search across all Nexus entities."""
name = "Universal Search"
version = "2.0.0"
author = "ImpulsiveFPS"
description = "Search items, mobs, locations, blueprints, skills, and more"
hotkey = "ctrl+shift+f" # F for Find
def initialize(self):
"""Setup the plugin."""
self.search_thread = None
self.current_results = []
self.current_entity_type = "Universal"
def get_ui(self):
"""Create plugin UI."""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setSpacing(10)
# Title - NO EMOJI
title = QLabel("Universal Search")
title.setStyleSheet("color: #4a9eff; font-size: 18px; font-weight: bold;")
layout.addWidget(title)
# Search mode selector
mode_layout = QHBoxLayout()
mode_layout.addWidget(QLabel("Mode:"))
self.search_mode = QComboBox()
self.search_mode.addItem("Universal (All Types)", "Universal")
self.search_mode.addItem("──────────────────", "separator")
# Add all entity types
entity_types = [
"Items",
"Weapons",
"Armors",
"Blueprints",
"Mobs",
"Locations",
"Skills",
"Materials",
"Enhancers",
"Medical Tools",
"Finders",
"Excavators",
"Refiners",
"Vehicles",
"Pets",
"Decorations",
"Furniture",
"Storage",
"Strongboxes",
"Teleporters",
"Shops",
"Vendors",
"Planets",
"Areas",
]
for etype in entity_types:
self.search_mode.addItem(f" {etype}", etype)
self.search_mode.setStyleSheet("""
QComboBox {
background-color: #444;
color: white;
padding: 8px;
border-radius: 4px;
min-width: 200px;
}
QComboBox::drop-down {
border: none;
}
""")
self.search_mode.currentIndexChanged.connect(self._on_mode_changed)
mode_layout.addWidget(self.search_mode)
mode_layout.addStretch()
layout.addLayout(mode_layout)
# Search bar
search_layout = QHBoxLayout()
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("Search for anything... (e.g., 'ArMatrix', 'Argonaut', 'Calypso')")
self.search_input.setStyleSheet("""
QLineEdit {
background-color: #333;
color: white;
padding: 10px;
border: 2px solid #555;
border-radius: 4px;
font-size: 14px;
}
QLineEdit:focus {
border-color: #4a9eff;
}
""")
self.search_input.returnPressed.connect(self._do_search)
search_layout.addWidget(self.search_input, 1)
search_btn = QPushButton("Search")
search_btn.setStyleSheet("""
QPushButton {
background-color: #4a9eff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
font-weight: bold;
font-size: 13px;
}
QPushButton:hover {
background-color: #5aafff;
}
""")
search_btn.clicked.connect(self._do_search)
search_layout.addWidget(search_btn)
layout.addLayout(search_layout)
# Status
self.status_label = QLabel("Ready to search")
self.status_label.setStyleSheet("color: #666; font-size: 11px;")
layout.addWidget(self.status_label)
# Results table
self.results_table = QTableWidget()
self.results_table.setColumnCount(4)
self.results_table.setHorizontalHeaderLabels(["Name", "Type", "Details", "ID"])
self.results_table.horizontalHeader().setStretchLastSection(False)
self.results_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
self.results_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
self.results_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
self.results_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed)
self.results_table.setColumnWidth(1, 120)
self.results_table.setColumnWidth(3, 60)
self.results_table.verticalHeader().setVisible(False)
self.results_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
self.results_table.setStyleSheet("""
QTableWidget {
background-color: #2a2a2a;
color: white;
border: 1px solid #444;
border-radius: 4px;
gridline-color: #333;
}
QTableWidget::item {
padding: 10px;
border-bottom: 1px solid #333;
}
QTableWidget::item:selected {
background-color: #4a9eff;
}
QTableWidget::item:hover {
background-color: #3a3a3a;
}
QHeaderView::section {
background-color: #333;
color: #aaa;
padding: 10px;
border: none;
font-weight: bold;
}
""")
self.results_table.cellDoubleClicked.connect(self._on_item_double_clicked)
self.results_table.setMaximumHeight(350)
self.results_table.setMinimumHeight(200)
layout.addWidget(self.results_table)
# Action buttons
action_layout = QHBoxLayout()
self.open_btn = QPushButton("Open Selected")
self.open_btn.setEnabled(False)
self.open_btn.setStyleSheet("""
QPushButton {
background-color: #4a9eff;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #5aafff;
}
QPushButton:disabled {
background-color: #444;
color: #666;
}
""")
self.open_btn.clicked.connect(self._open_selected)
action_layout.addWidget(self.open_btn)
action_layout.addStretch()
# Quick category buttons
quick_label = QLabel("Quick:")
quick_label.setStyleSheet("color: #666;")
action_layout.addWidget(quick_label)
for category in ["Items", "Mobs", "Blueprints", "Locations"]:
btn = QPushButton(category)
btn.setStyleSheet("""
QPushButton {
background-color: transparent;
color: #4a9eff;
border: 1px solid #4a9eff;
padding: 5px 10px;
border-radius: 3px;
}
QPushButton:hover {
background-color: #4a9eff;
color: white;
}
""")
btn.clicked.connect(lambda checked, c=category: self._quick_search(c))
action_layout.addWidget(btn)
layout.addLayout(action_layout)
# Tips
tips = QLabel("Tip: Double-click result to open on Nexus website")
tips.setStyleSheet("color: #555; font-size: 10px;")
layout.addWidget(tips)
layout.addStretch()
return widget
def _on_mode_changed(self):
"""Handle search mode change."""
data = self.search_mode.currentData()
if data == "separator":
# Reset to previous valid selection
self.search_mode.setCurrentIndex(0)
def _do_search(self):
"""Perform search."""
query = self.search_input.text().strip()
if len(query) < 2:
self.status_label.setText("Enter at least 2 characters")
return
entity_type = self.search_mode.currentData()
if entity_type == "separator":
entity_type = "Universal"
self.current_entity_type = entity_type
universal = (entity_type == "Universal")
# Clear previous results
self.results_table.setRowCount(0)
self.current_results = []
self.open_btn.setEnabled(False)
self.status_label.setText(f"Searching for '{query}'...")
# Start search thread with http_get function
self.search_thread = UniversalSearchThread(
query, entity_type, universal,
http_get_func=self.http_get
)
self.search_thread.results_ready.connect(self._on_results)
self.search_thread.error_occurred.connect(self._on_error)
self.search_thread.start()
def _quick_search(self, category):
"""Quick search for a specific category."""
# Set the category
index = self.search_mode.findData(category)
if index >= 0:
self.search_mode.setCurrentIndex(index)
# If there's text in the search box, search immediately
if self.search_input.text().strip():
self._do_search()
else:
self.search_input.setFocus()
self.status_label.setText(f"Selected: {category} - Enter search term")
def _on_results(self, results, entity_type):
"""Handle search results."""
self.current_results = results
if not results:
self.status_label.setText("No results found")
return
# Populate table
self.results_table.setRowCount(len(results))
for row, item in enumerate(results):
# Extract data based on available fields
name = item.get('name', item.get('Name', 'Unknown'))
item_id = str(item.get('id', item.get('Id', '')))
# Determine type
if 'type' in item:
item_type = item['type']
elif entity_type != "Universal":
item_type = entity_type
else:
# Try to infer from other fields
item_type = self._infer_type(item)
# Build details string
details = self._build_details(item, item_type)
# Set table items
self.results_table.setItem(row, 0, QTableWidgetItem(name))
self.results_table.setItem(row, 1, QTableWidgetItem(item_type))
self.results_table.setItem(row, 2, QTableWidgetItem(details))
self.results_table.setItem(row, 3, QTableWidgetItem(item_id))
self.open_btn.setEnabled(True)
self.status_label.setText(f"Found {len(results)} results")
def _infer_type(self, item):
"""Infer entity type from item fields."""
if 'damage' in item or 'range' in item:
return "Weapon"
elif 'protection' in item or 'durability' in item:
return "Armor"
elif 'hitpoints' in item:
return "Mob"
elif 'x' in item and 'y' in item:
return "Location"
elif 'qr' in item or 'click' in item:
return "Blueprint"
elif 'category' in item:
return item['category']
else:
return "Item"
def _build_details(self, item, item_type):
"""Build details string based on item type."""
details = []
if item_type in ["Weapon", "Weapons"]:
if 'damage' in item:
details.append(f"Dmg: {item['damage']}")
if 'range' in item:
details.append(f"Range: {item['range']}m")
if 'attacks' in item:
details.append(f"{item['attacks']} attacks")
elif item_type in ["Armor", "Armors"]:
if 'protection' in item:
details.append(f"Prot: {item['protection']}")
if 'durability' in item:
details.append(f"Dur: {item['durability']}")
elif item_type in ["Mob", "Mobs"]:
if 'hitpoints' in item:
details.append(f"HP: {item['hitpoints']}")
if 'damage' in item:
details.append(f"Dmg: {item['damage']}")
if 'threat' in item:
details.append(f"Threat: {item['threat']}")
elif item_type in ["Blueprint", "Blueprints"]:
if 'qr' in item:
details.append(f"QR: {item['qr']}")
if 'click' in item:
details.append(f"Clicks: {item['click']}")
elif item_type in ["Location", "Locations", "Teleporter", "Shop"]:
if 'planet' in item:
details.append(item['planet'])
if 'x' in item and 'y' in item:
details.append(f"[{item['x']}, {item['y']}]")
elif item_type in ["Skill", "Skills"]:
if 'category' in item:
details.append(item['category'])
# Add any other interesting fields
if 'level' in item:
details.append(f"Lvl: {item['level']}")
if 'weight' in item:
details.append(f"{item['weight']}kg")
return " | ".join(details) if details else ""
def _on_error(self, error):
"""Handle search error."""
self.status_label.setText(f"Error: {error}")
def _on_item_double_clicked(self, row, column):
"""Handle item double-click."""
self._open_result(row)
def _open_selected(self):
"""Open selected result."""
selected = self.results_table.selectedItems()
if selected:
row = selected[0].row()
self._open_result(row)
def _open_result(self, row):
"""Open result in browser."""
if row < len(self.current_results):
item = self.current_results[row]
entity_id = item.get('id', item.get('Id', ''))
entity_name = item.get('name', item.get('Name', ''))
# Use name for URL if available, otherwise ID
url_param = entity_name if entity_name else str(entity_id)
url = NexusEntityAPI.get_entity_url(self.current_entity_type, url_param)
webbrowser.open(url)
def on_hotkey(self):
"""Focus search when hotkey pressed."""
if hasattr(self, 'search_input'):
self.search_input.setFocus()
self.search_input.selectAll()