EU-Utility/plugins/universal_search/plugin.py

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()