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:
parent
2f94cf85fc
commit
6a54e99452
|
|
@ -52,22 +52,6 @@
|
|||
"min_core_version": "2.0.0",
|
||||
"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",
|
||||
"name": "Loot Tracker",
|
||||
|
|
@ -307,38 +291,6 @@
|
|||
},
|
||||
"min_core_version": "2.0.0",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
from .plugin import AnalyticsPlugin
|
||||
|
||||
__all__ = ['AnalyticsPlugin']
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
})
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from .plugin import AutoUpdaterPlugin
|
||||
|
||||
__all__ = ['AutoUpdaterPlugin']
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
"""
|
||||
Dashboard Plugin
|
||||
"""
|
||||
|
||||
from .plugin import DashboardPlugin
|
||||
|
||||
__all__ = ["DashboardPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from .plugin import ImportExportPlugin
|
||||
|
||||
__all__ = ['ImportExportPlugin']
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
"""
|
||||
Plugin Store UI Plugin
|
||||
"""
|
||||
|
||||
from .plugin import PluginStoreUIPlugin
|
||||
|
||||
__all__ = ["PluginStoreUIPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
"""
|
||||
Settings Plugin
|
||||
"""
|
||||
|
||||
from .plugin import SettingsPlugin
|
||||
|
||||
__all__ = ["SettingsPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +0,0 @@
|
|||
"""
|
||||
Universal Search Plugin for EU-Utility
|
||||
"""
|
||||
|
||||
from .plugin import UniversalSearchPlugin
|
||||
|
||||
__all__ = ["UniversalSearchPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
Loading…
Reference in New Issue