601 lines
20 KiB
Python
601 lines
20 KiB
Python
"""
|
|
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()
|