diff --git a/projects/EU-Utility/plugins/universal_search/__init__.py b/projects/EU-Utility/plugins/universal_search/__init__.py new file mode 100644 index 0000000..44d5b53 --- /dev/null +++ b/projects/EU-Utility/plugins/universal_search/__init__.py @@ -0,0 +1,7 @@ +""" +Universal Search Plugin for EU-Utility +""" + +from .plugin import UniversalSearchPlugin + +__all__ = ["UniversalSearchPlugin"] diff --git a/projects/EU-Utility/plugins/universal_search/plugin.py b/projects/EU-Utility/plugins/universal_search/plugin.py new file mode 100644 index 0000000..887290b --- /dev/null +++ b/projects/EU-Utility/plugins/universal_search/plugin.py @@ -0,0 +1,590 @@ +""" +EU-Utility - Universal Search Plugin + +Search across all Entropia Nexus entities - items, mobs, locations, blueprints, skills, etc. +""" + +import urllib.request +import urllib.parse +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): + """Search for entities of a specific type.""" + try: + endpoint = cls.ENDPOINTS.get(entity_type, "/items") + + # Build URL with query + params = urllib.parse.urlencode({ + 'q': query, + 'limit': limit, + 'fuzzy': 'true' + }) + url = f"{cls.BASE_URL}{endpoint}?{params}" + + req = urllib.request.Request( + url, + headers={ + 'Accept': 'application/json', + 'User-Agent': 'EU-Utility/1.0' + } + ) + + with urllib.request.urlopen(req, timeout=15) as response: + data = json.loads(response.read().decode('utf-8')) + 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): + """Universal search across all entity types.""" + try: + params = urllib.parse.urlencode({ + 'query': query, + 'limit': limit, + 'fuzzy': 'true' + }) + url = f"{cls.BASE_URL}/search?{params}" + + req = urllib.request.Request( + url, + headers={ + 'Accept': 'application/json', + 'User-Agent': 'EU-Utility/1.0' + } + ) + + with urllib.request.urlopen(req, timeout=15) as response: + data = json.loads(response.read().decode('utf-8')) + 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): + super().__init__() + self.query = query + self.entity_type = entity_type + self.universal = universal + + def run(self): + """Perform API search.""" + try: + if self.universal: + results = NexusEntityAPI.universal_search(self.query) + else: + results = NexusEntityAPI.search_entities(self.entity_type, self.query) + + 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 + 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 + self.search_thread = UniversalSearchThread(query, entity_type, universal) + 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 or 'damage' in item: + return "Mob" + elif 'x' in item and 'y' in item: + return "Location" + elif 'qr' in item or 'click' in item.lower(): + 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()