diff --git a/docs/MAPPER_MODULES.md b/docs/MAPPER_MODULES.md index 1c1be17..6ef2a0f 100644 --- a/docs/MAPPER_MODULES.md +++ b/docs/MAPPER_MODULES.md @@ -127,6 +127,30 @@ evescout = EveScout( ) ``` +### Wanderer + +**Status**: Fully supported with multiple instances + +**Configuration**: +- URL: Wanderer instance URL (self-hosted or community instance) +- Map slug: The map identifier from the map's URL +- API token: Bearer token generated in Wanderer under Map Settings → API +- Name: Instance identifier (e.g., "Alliance Wanderer") + +In the persisted `MapperConfig`, the map slug is stored in the `user` slot +and the API token in the `password` slot (the token is the secret, so it +belongs in the slot the UI masks). + +**Example**: +```python +wanderer = Wanderer( + url="https://wanderer.example.com", + map_slug="my-map", + token="generated-api-token", + name="Alliance Wanderer" +) +``` + ### eve-whmapper **Status**: Not currently supported - no public API available diff --git a/resources/ui/gui_wanderer.ui b/resources/ui/gui_wanderer.ui new file mode 100644 index 0000000..a05ff80 --- /dev/null +++ b/resources/ui/gui_wanderer.ui @@ -0,0 +1,174 @@ + + + WandererDialog + + + + 0 + 0 + 480 + 260 + + + + + 0 + 0 + + + + + 480 + 260 + + + + + Segoe UI + 9 + + + + Wanderer Configuration + + + + :/images/app_icon.png:/images/app_icon.png + + + false + + + + + + Name: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + URL: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + https://your-wanderer-instance + + + + + + + Map slug: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Map identifier from the map URL + + + + + + + API token: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::ImhHiddenText|Qt::ImhNoAutoUppercase|Qt::ImhNoPredictiveText|Qt::ImhSensitiveData + + + QLineEdit::Password + + + + + + + <html><head/><body><p>Generate the token in Wanderer under Map Settings → API. <a href="https://github.com/wanderer-industries/wanderer"><span style=" text-decoration: underline; color:#0000ff;">About Wanderer</span></a></p></body></html> + + + true + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + WandererDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + WandererDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/shortcircuit/app.py b/src/shortcircuit/app.py index 7fa37ca..944df77 100644 --- a/src/shortcircuit/app.py +++ b/src/shortcircuit/app.py @@ -16,6 +16,7 @@ MapperConfig, TYPE_EVESCOUT, TYPE_TRIPWIRE, + TYPE_WANDERER, default_configs, load_configs, migrate_legacy, @@ -28,6 +29,7 @@ from .view.gui_main import Ui_MainWindow from .view.gui_mappers import Ui_MappersDialog from .view.gui_tripwire import Ui_TripwireDialog +from .view.gui_wanderer import Ui_WandererDialog class StateEVEConnection(TypedDict): @@ -44,6 +46,7 @@ class StateMapper(TypedDict): MAPPER_TYPE_LABELS = { TYPE_TRIPWIRE: "Tripwire", TYPE_EVESCOUT: "Eve Scout", + TYPE_WANDERER: "Wanderer", } @@ -74,6 +77,30 @@ def __init__( self.lineEdit_proxy.setText(proxy or "") +class WandererDialog(QtWidgets.QDialog, Ui_WandererDialog): + """ + Per-instance Wanderer edit dialog, opened from MappersDialog. + + Edits one Wanderer MapperConfig: name, instance URL, map slug and API + bearer token (generated in Wanderer under Map Settings -> API). + """ + + def __init__( + self, + name: str, + url: str, + map_slug: str, + token: str, + parent=None, + ): + super().__init__(parent) + self.setupUi(self) + self.lineEdit_name.setText(name or "") + self.lineEdit_url.setText(url or "") + self.lineEdit_map.setText(map_slug or "") + self.lineEdit_token.setText(token or "") + + class MappersDialog(QtWidgets.QDialog, Ui_MappersDialog): """ Multi-mapper management dialog. @@ -180,6 +207,13 @@ def _on_add(self): url="https://tripwire.eve-apps.com", ) edited = self._edit_tripwire(cfg) + elif type_key == TYPE_WANDERER: + cfg = MapperConfig( + type=TYPE_WANDERER, + name=self._unique_name("Wanderer"), + enabled=True, + ) + edited = self._edit_wanderer(cfg) else: cfg = MapperConfig( type=TYPE_EVESCOUT, @@ -203,6 +237,8 @@ def _on_edit(self): edited = self._edit_tripwire(cfg) elif cfg.type == TYPE_EVESCOUT: edited = self._edit_evescout(cfg) + elif cfg.type == TYPE_WANDERER: + edited = self._edit_wanderer(cfg) else: edited = None if edited is not None: @@ -238,6 +274,25 @@ def _edit_tripwire(self, cfg: MapperConfig) -> Optional[MapperConfig]: password=dlg.lineEdit_pass.text(), ) + def _edit_wanderer(self, cfg: MapperConfig) -> Optional[MapperConfig]: + dlg = WandererDialog( + name=cfg.name, + url=cfg.url, + map_slug=cfg.user, + token=cfg.password, + parent=self, + ) + if not dlg.exec(): + return None + return MapperConfig( + type=TYPE_WANDERER, + name=dlg.lineEdit_name.text().strip() or cfg.name, + enabled=cfg.enabled, + url=dlg.lineEdit_url.text().strip(), + user=dlg.lineEdit_map.text().strip(), + password=dlg.lineEdit_token.text().strip(), + ) + def _edit_evescout(self, cfg: MapperConfig) -> Optional[MapperConfig]: name, ok = QtWidgets.QInputDialog.getText( self, diff --git a/src/shortcircuit/model/mapper_config.py b/src/shortcircuit/model/mapper_config.py index 5418e3c..df65c70 100644 --- a/src/shortcircuit/model/mapper_config.py +++ b/src/shortcircuit/model/mapper_config.py @@ -12,6 +12,7 @@ TYPE_TRIPWIRE = "tripwire" TYPE_EVESCOUT = "evescout" +TYPE_WANDERER = "wanderer" @dataclass @@ -23,7 +24,9 @@ class MapperConfig: which MapperSource subclass to instantiate; `name` is the user-facing label that also serves as the key in the `results` dict emitted by NavProcessor. `url`/`user`/`password` are mapper-agnostic config slots — - Eve Scout ignores user/password. + Eve Scout ignores user/password, and Wanderer stores its map slug in + `user` and its API bearer token in `password` (the token is a secret, so + it belongs in the slot the UI masks). """ type: str name: str diff --git a/src/shortcircuit/model/navigation.py b/src/shortcircuit/model/navigation.py index ec0aef3..3c9022f 100644 --- a/src/shortcircuit/model/navigation.py +++ b/src/shortcircuit/model/navigation.py @@ -5,10 +5,11 @@ from .evedb import EveDb, SystemDescription, WormholeMassspan, WormholeSize, WormholeTimespan from .evescout import EveScout from .mapper_base import MapperSource -from .mapper_config import TYPE_EVESCOUT, TYPE_TRIPWIRE, MapperConfig +from .mapper_config import TYPE_EVESCOUT, TYPE_TRIPWIRE, TYPE_WANDERER, MapperConfig from .mapper_registry import MapperRegistry from .solarmap import ConnectionType, SolarMap from .tripwire import Tripwire +from .wanderer import Wanderer if TYPE_CHECKING: from shortcircuit.app import MainWindow @@ -30,9 +31,21 @@ def _build_evescout(cfg: MapperConfig) -> MapperSource: return EveScout(name=cfg.name) +def _build_wanderer(cfg: MapperConfig) -> MapperSource: + # Wanderer keeps its map slug in the `user` slot and the API bearer token + # in the `password` slot; see the MapperConfig docstring. + return Wanderer( + url=cfg.url, + map_slug=cfg.user, + token=cfg.password, + name=cfg.name, + ) + + MAPPER_BUILDERS: Dict[str, Callable[[MapperConfig], MapperSource]] = { TYPE_TRIPWIRE: _build_tripwire, TYPE_EVESCOUT: _build_evescout, + TYPE_WANDERER: _build_wanderer, } diff --git a/src/shortcircuit/model/test_wanderer.py b/src/shortcircuit/model/test_wanderer.py new file mode 100644 index 0000000..9857346 --- /dev/null +++ b/src/shortcircuit/model/test_wanderer.py @@ -0,0 +1,218 @@ +"""Tests for the Wanderer mapper integration + +Covers config validation, URL normalization, the signature-to-connection +translation (K162 inference, life/mass mapping from custom_info, elapsed +time), and the failure modes of the API round trip. +""" + +import unittest +from unittest import mock + +import requests + +from shortcircuit.model import wanderer as wanderer_module +from shortcircuit.model.evedb import WormholeMassspan, WormholeSize, WormholeTimespan +from shortcircuit.model.solarmap import ConnectionType +from shortcircuit.model.wanderer import Wanderer + +THERA_ID = 31000005 +J_SYSTEM_ID = 31002238 + + +def _make_wanderer(**kwargs): + defaults = dict( + url='https://wanderer.example.com', + map_slug='my-map', + token='secret-token', + name='Test Wanderer', + ) + defaults.update(kwargs) + w = Wanderer(**defaults) + # Size lookups go through the SDE; tests pin them so they don't depend + # on database contents. + w.eve_db = mock.Mock() + w.eve_db.get_whsize_by_code.return_value = WormholeSize.UNKNOWN + w.eve_db.get_whsize_by_system.return_value = WormholeSize.LARGE + return w + + +def _response(status_code=200, payload=None): + r = mock.Mock() + r.status_code = status_code + r.json = mock.Mock(return_value=payload if payload is not None else {'data': []}) + return r + + +def _signature(**overrides): + sig = { + 'eve_id': 'ABC-123', + 'group': 'Wormhole', + 'solar_system_id': J_SYSTEM_ID, + 'linked_system_id': THERA_ID, + 'type': 'F353', + 'custom_info': '{"time_status": 1, "mass_status": 1}', + 'updated_at': '2026-11-08T00:00:00Z', + } + sig.update(overrides) + return sig + + +class TestWandererConfig(unittest.TestCase): + + def test_valid_config(self): + self.assertEqual(_make_wanderer().validate_config(), (True, None)) + + def test_missing_fields_are_reported(self): + for kwargs, expected in ( + (dict(url=''), 'URL is required'), + (dict(map_slug=''), 'Map slug is required'), + (dict(token=''), 'API token is required'), + ): + is_valid, error = _make_wanderer(**kwargs).validate_config() + self.assertFalse(is_valid) + self.assertEqual(error, expected) + + def test_url_normalization(self): + w = _make_wanderer(url='wanderer.example.com/') + self.assertEqual(w.url, 'https://wanderer.example.com') + + def test_misconfigured_augment_returns_error_without_network(self): + w = _make_wanderer(token='') + solar_map = mock.Mock() + with mock.patch.object(wanderer_module.requests, 'get') as fake_get: + self.assertEqual(w.augment_map(solar_map), -1) + fake_get.assert_not_called() + solar_map.add_connection.assert_not_called() + + +class TestWandererAugmentMap(unittest.TestCase): + + def _augment(self, signatures, wanderer=None): + w = wanderer or _make_wanderer() + solar_map = mock.Mock() + with mock.patch.object( + wanderer_module.requests, 'get', + return_value=_response(payload={'data': signatures}), + ) as fake_get: + count = w.augment_map(solar_map) + return count, solar_map, fake_get + + def test_linked_wormhole_becomes_connection(self): + count, solar_map, fake_get = self._augment([_signature()]) + + self.assertEqual(count, 1) + url = fake_get.call_args.kwargs['url'] + self.assertEqual(url, 'https://wanderer.example.com/api/maps/my-map/signatures') + headers = fake_get.call_args.kwargs['headers'] + self.assertEqual(headers['Authorization'], 'Bearer secret-token') + + args, kwargs = solar_map.add_connection.call_args + source, dest, con_type, info = args + self.assertEqual(source, J_SYSTEM_ID) + self.assertEqual(dest, THERA_ID) + self.assertEqual(con_type, ConnectionType.WORMHOLE) + sig_source, code_source, sig_dest, code_dest = info[0:4] + self.assertEqual(sig_source, 'ABC-123') + self.assertEqual(code_source, 'F353') + self.assertEqual(sig_dest, '???') + # The scanned side carries the hole type; the far side is a K162. + self.assertEqual(code_dest, 'K162') + self.assertEqual(kwargs['source_name'], 'Test Wanderer') + + def test_unknown_type_does_not_fabricate_k162(self): + count, solar_map, _ = self._augment([_signature(type=None)]) + self.assertEqual(count, 1) + info = solar_map.add_connection.call_args.args[3] + self.assertEqual(info[1], '????') + self.assertEqual(info[3], '????') + + def test_unlinked_and_non_wormhole_signatures_are_skipped(self): + count, solar_map, _ = self._augment([ + _signature(linked_system_id=None), + _signature(group='Data'), + _signature(solar_system_id='not-a-number'), + ]) + self.assertEqual(count, 0) + solar_map.add_connection.assert_not_called() + + def test_eol_and_mass_status_map_to_enums(self): + count, solar_map, _ = self._augment([ + _signature(custom_info='{"time_status": 2, "mass_status": 3}'), + ]) + self.assertEqual(count, 1) + info = solar_map.add_connection.call_args.args[3] + self.assertEqual(info[5], WormholeTimespan.CRITICAL) + self.assertEqual(info[6], WormholeMassspan.CRITICAL) + + def test_custom_info_accepts_dict_and_tolerates_garbage(self): + count, solar_map, _ = self._augment([ + _signature(custom_info={'time_status': 1, 'mass_status': 2}), + _signature(custom_info='not json'), + _signature(custom_info=None), + ]) + self.assertEqual(count, 3) + first = solar_map.add_connection.call_args_list[0].args[3] + self.assertEqual(first[6], WormholeMassspan.DESTAB) + for call in solar_map.add_connection.call_args_list[1:]: + info = call.args[3] + self.assertEqual(info[5], WormholeTimespan.STABLE) + self.assertEqual(info[6], WormholeMassspan.STABLE) + + def test_size_falls_back_to_system_pair_when_code_unknown(self): + count, solar_map, _ = self._augment([_signature()]) + self.assertEqual(count, 1) + info = solar_map.add_connection.call_args.args[3] + self.assertEqual(info[4], WormholeSize.LARGE) + + +class TestWandererFailureModes(unittest.TestCase): + + def _augment_with_response(self, response=None, side_effect=None): + w = _make_wanderer() + solar_map = mock.Mock() + with mock.patch.object( + wanderer_module.requests, 'get', + return_value=response, side_effect=side_effect, + ): + return w.augment_map(solar_map), solar_map + + def test_unauthorized_returns_error(self): + count, solar_map = self._augment_with_response(_response(status_code=401)) + self.assertEqual(count, -1) + solar_map.add_connection.assert_not_called() + + def test_map_not_found_returns_error(self): + count, _ = self._augment_with_response(_response(status_code=404)) + self.assertEqual(count, -1) + + def test_server_error_returns_error(self): + count, _ = self._augment_with_response(_response(status_code=500)) + self.assertEqual(count, -1) + + def test_network_exception_returns_error(self): + count, _ = self._augment_with_response( + side_effect=requests.ConnectionError('offline') + ) + self.assertEqual(count, -1) + + def test_unexpected_response_shape_returns_error(self): + count, _ = self._augment_with_response( + _response(payload=['not', 'a', 'dict']) + ) + self.assertEqual(count, -1) + + +class TestTimeElapsed(unittest.TestCase): + + def test_z_suffix_is_parsed(self): + self.assertGreaterEqual( + Wanderer._time_elapsed('2026-01-01T00:00:00Z'), 0.0 + ) + + def test_missing_or_malformed_returns_zero(self): + self.assertEqual(Wanderer._time_elapsed(None), 0.0) + self.assertEqual(Wanderer._time_elapsed('yesterday-ish'), 0.0) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/shortcircuit/model/wanderer.py b/src/shortcircuit/model/wanderer.py new file mode 100644 index 0000000..7ac6c49 --- /dev/null +++ b/src/shortcircuit/model/wanderer.py @@ -0,0 +1,215 @@ +# wanderer.py + +import json +from datetime import UTC, datetime +from typing import Optional + +import requests +from shortcircuit import USER_AGENT + +from .evedb import EveDb, WormholeMassspan, WormholeSize, WormholeTimespan +from .logger import Logger +from .mapper_base import MapperSource +from .solarmap import ConnectionType, SolarMap + + +class Wanderer(MapperSource): + """ + Wanderer wormhole mapper connections provider. + + Wanderer (https://github.com/wanderer-industries/wanderer) exposes a map's + signatures via a REST API authenticated with a per-map bearer token + (Map Settings -> API -> Generate token). Signatures of group "Wormhole" + that have a linked system form the connections. + """ + TIMEOUT = 10 + + def __init__( + self, + url: str, + map_slug: str, + token: str, + name: str = "Wanderer", + ): + url = (url or '').strip().rstrip('/') + if url and not (url.startswith('http://') or url.startswith('https://')): + url = 'https://' + url + self.url = url + self.map_slug = (map_slug or '').strip() + self.token = (token or '').strip() + self.name = name + self.eve_db = EveDb() + + def augment_map(self, solar_map: SolarMap): + """ + :param solar_map: SolarMap + :return: Number of connections in case of success, -1 in case of failure + """ + signatures = self._fetch_signatures() + if signatures is None: + return -1 + + connections = 0 + for sig in signatures: + if sig.get('group') != 'Wormhole': + continue + + # A connection needs both ends; an unlinked wormhole signature is + # just a scanned sig with no destination yet. + try: + source = int(sig.get('solar_system_id') or 0) + dest = int(sig.get('linked_system_id') or 0) + except (TypeError, ValueError): + continue + if not source or not dest: + continue + + wh_life, wh_mass = self._parse_custom_info(sig.get('custom_info')) + + # Wanderer stores the hole type on the scanned side; the other side + # is a K162 by definition unless the type is unknown. + code_source = sig.get('type') or '????' + code_dest = 'K162' if code_source not in ('????', 'K162') else '????' + + wh_size = self.eve_db.get_whsize_by_code(code_source) + if not WormholeSize.valid(wh_size): + wh_size = self.eve_db.get_whsize_by_system(source, dest) + + sig_source = sig.get('eve_id') or '???' + + solar_map.add_connection( + source, + dest, + ConnectionType.WORMHOLE, + [ + sig_source, + code_source, + '???', + code_dest, + wh_size, + wh_life, + wh_mass, + self._time_elapsed(sig.get('updated_at')), + ], + source_name=self.name, + ) + connections += 1 + + return connections + + def _fetch_signatures(self) -> Optional[list]: + is_valid, error = self.validate_config() + if not is_valid: + Logger.error(f"Wanderer '{self.name}' misconfigured: {error}") + return None + + api_url = f"{self.url}/api/maps/{self.map_slug}/signatures" + headers = { + 'User-Agent': USER_AGENT, + 'Authorization': f"Bearer {self.token}", + 'Accept': 'application/json', + } + try: + result = requests.get( + url=api_url, + headers=headers, + timeout=Wanderer.TIMEOUT, + ) + except requests.exceptions.RequestException as e: + Logger.error(f"Exception raised while trying to get Wanderer map '{self.map_slug}'") + Logger.error(e) + return None + + if result.status_code == 401: + Logger.error(f"Wanderer '{self.name}': unauthorized - check the API token") + return None + if result.status_code == 404: + Logger.error(f"Wanderer '{self.name}': map '{self.map_slug}' not found - check URL and map slug") + return None + if result.status_code != 200: + Logger.error(f"Wanderer '{self.name}': HTTP {result.status_code}") + return None + + try: + payload = result.json() + except ValueError as e: + Logger.error(f"Wanderer '{self.name}': invalid JSON response: {e}") + return None + + data = payload.get('data') if isinstance(payload, dict) else None + if not isinstance(data, list): + Logger.error(f"Wanderer '{self.name}': unexpected response shape") + return None + return data + + @staticmethod + def _parse_custom_info(custom_info): + """ + Wanderer encodes wormhole state in a JSON blob (sometimes pre-decoded): + time_status: 1 = stable, 2 = end of life + mass_status: 1 = stable, 2 = destabilized, 3 = critical + Missing/malformed info is treated as stable, matching Wanderer's own UI + defaults for an unmarked hole. + """ + time_status = 1 + mass_status = 1 + if custom_info: + try: + if isinstance(custom_info, str): + custom_info = json.loads(custom_info) + if isinstance(custom_info, dict): + time_status = int(custom_info.get('time_status') or 1) + mass_status = int(custom_info.get('mass_status') or 1) + except (TypeError, ValueError): + pass + + wh_life = ( + WormholeTimespan.CRITICAL if time_status == 2 else WormholeTimespan.STABLE + ) + if mass_status == 3: + wh_mass = WormholeMassspan.CRITICAL + elif mass_status == 2: + wh_mass = WormholeMassspan.DESTAB + else: + wh_mass = WormholeMassspan.STABLE + return wh_life, wh_mass + + @staticmethod + def _time_elapsed(updated_at: Optional[str]) -> float: + """Hours since the signature was last updated; 0.0 when unknown.""" + if not updated_at: + return 0.0 + try: + if updated_at.endswith('Z'): + updated_at = updated_at[:-1] + '+00:00' + last_modified = datetime.fromisoformat(updated_at) + if last_modified.tzinfo is None: + last_modified = last_modified.replace(tzinfo=UTC) + except ValueError: + return 0.0 + delta = datetime.now(UTC) - last_modified + return round(delta.total_seconds() / 3600.0, 1) + + def get_name(self) -> str: + """ + Get the name of this Wanderer instance. + + Returns: + The name of this mapper source + """ + return self.name + + def validate_config(self) -> tuple[bool, Optional[str]]: + """ + Validate the Wanderer configuration. + + Returns: + Tuple of (is_valid, error_message) + """ + if not self.url: + return False, "URL is required" + if not self.map_slug: + return False, "Map slug is required" + if not self.token: + return False, "API token is required" + return True, None diff --git a/src/shortcircuit/view/gui_wanderer.py b/src/shortcircuit/view/gui_wanderer.py new file mode 100644 index 0000000..8348f37 --- /dev/null +++ b/src/shortcircuit/view/gui_wanderer.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'gui_wanderer.ui' +## +## Created by: Qt User Interface Compiler version 6.9.3 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox, + QGridLayout, QLabel, QLineEdit, QSizePolicy, + QWidget) +from . import resources_rc + +class Ui_WandererDialog(object): + def setupUi(self, WandererDialog): + if not WandererDialog.objectName(): + WandererDialog.setObjectName(u"WandererDialog") + WandererDialog.resize(480, 260) + sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(WandererDialog.sizePolicy().hasHeightForWidth()) + WandererDialog.setSizePolicy(sizePolicy) + WandererDialog.setMinimumSize(QSize(480, 260)) + font = QFont() + font.setFamilies([u"Segoe UI"]) + font.setPointSize(9) + WandererDialog.setFont(font) + icon = QIcon() + icon.addFile(u":/images/app_icon.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + WandererDialog.setWindowIcon(icon) + WandererDialog.setSizeGripEnabled(False) + self.gridLayout = QGridLayout(WandererDialog) + self.gridLayout.setObjectName(u"gridLayout") + self.label_name = QLabel(WandererDialog) + self.label_name.setObjectName(u"label_name") + self.label_name.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) + + self.gridLayout.addWidget(self.label_name, 0, 0, 1, 1) + + self.lineEdit_name = QLineEdit(WandererDialog) + self.lineEdit_name.setObjectName(u"lineEdit_name") + + self.gridLayout.addWidget(self.lineEdit_name, 0, 1, 1, 1) + + self.label_url = QLabel(WandererDialog) + self.label_url.setObjectName(u"label_url") + self.label_url.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) + + self.gridLayout.addWidget(self.label_url, 1, 0, 1, 1) + + self.lineEdit_url = QLineEdit(WandererDialog) + self.lineEdit_url.setObjectName(u"lineEdit_url") + + self.gridLayout.addWidget(self.lineEdit_url, 1, 1, 1, 1) + + self.label_map = QLabel(WandererDialog) + self.label_map.setObjectName(u"label_map") + self.label_map.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) + + self.gridLayout.addWidget(self.label_map, 2, 0, 1, 1) + + self.lineEdit_map = QLineEdit(WandererDialog) + self.lineEdit_map.setObjectName(u"lineEdit_map") + + self.gridLayout.addWidget(self.lineEdit_map, 2, 1, 1, 1) + + self.label_token = QLabel(WandererDialog) + self.label_token.setObjectName(u"label_token") + self.label_token.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) + + self.gridLayout.addWidget(self.label_token, 3, 0, 1, 1) + + self.lineEdit_token = QLineEdit(WandererDialog) + self.lineEdit_token.setObjectName(u"lineEdit_token") + self.lineEdit_token.setInputMethodHints(Qt.ImhHiddenText|Qt.ImhNoAutoUppercase|Qt.ImhNoPredictiveText|Qt.ImhSensitiveData) + self.lineEdit_token.setEchoMode(QLineEdit.Password) + + self.gridLayout.addWidget(self.lineEdit_token, 3, 1, 1, 1) + + self.label_help = QLabel(WandererDialog) + self.label_help.setObjectName(u"label_help") + self.label_help.setWordWrap(True) + self.label_help.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignVCenter) + self.label_help.setOpenExternalLinks(True) + + self.gridLayout.addWidget(self.label_help, 4, 1, 1, 1) + + self.buttonBox = QDialogButtonBox(WandererDialog) + self.buttonBox.setObjectName(u"buttonBox") + self.buttonBox.setOrientation(Qt.Horizontal) + self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok) + + self.gridLayout.addWidget(self.buttonBox, 5, 0, 1, 2) + + + self.retranslateUi(WandererDialog) + self.buttonBox.accepted.connect(WandererDialog.accept) + self.buttonBox.rejected.connect(WandererDialog.reject) + + QMetaObject.connectSlotsByName(WandererDialog) + # setupUi + + def retranslateUi(self, WandererDialog): + WandererDialog.setWindowTitle(QCoreApplication.translate("WandererDialog", u"Wanderer Configuration", None)) + self.label_name.setText(QCoreApplication.translate("WandererDialog", u"Name:", None)) + self.label_url.setText(QCoreApplication.translate("WandererDialog", u"URL:", None)) + self.lineEdit_url.setPlaceholderText(QCoreApplication.translate("WandererDialog", u"https://your-wanderer-instance", None)) + self.label_map.setText(QCoreApplication.translate("WandererDialog", u"Map slug:", None)) + self.lineEdit_map.setPlaceholderText(QCoreApplication.translate("WandererDialog", u"Map identifier from the map URL", None)) + self.label_token.setText(QCoreApplication.translate("WandererDialog", u"API token:", None)) + self.label_help.setText(QCoreApplication.translate("WandererDialog", u"

Generate the token in Wanderer under Map Settings \u2192 API. About Wanderer

", None)) + # retranslateUi +