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