diff --git a/src/shortcircuit/app.py b/src/shortcircuit/app.py index 4d820a2..10bf162 100644 --- a/src/shortcircuit/app.py +++ b/src/shortcircuit/app.py @@ -1210,19 +1210,18 @@ def _update_sources_status(self): else: self.status_sources_widget.setStyleSheet("color: #abb2bf;") - @QtCore.Slot(bool, str, int) - def login_handler(self, is_ok, char_name=None, char_id=0): - # Handle cases where PySide might mangle the argument list across threads - if isinstance(char_name, int) and char_id == 0: - char_id = char_name - char_name = "Unknown" - + @QtCore.Slot(dict) + def login_handler(self, result): + is_ok = result.get('is_ok', False) + char_name = result.get('char_name', 'Unknown') + char_id = int(result.get('char_id', 0)) + self.state_eve_connection["connected"] = is_ok self.state_eve_connection["char_name"] = char_name self.state_eve_connection["char_id"] = char_id if is_ok else 0 self.state_eve_connection["error"] = "ESI error" if not is_ok else None - - Logger.info(f"Login handler received: ok={is_ok}, name={char_name}, id={char_id}") + + Logger.info(f"Login handler received dict: ok={is_ok}, name={char_name}, id={char_id}") self._status_eve_connection_update() @QtCore.Slot() @@ -1482,16 +1481,22 @@ def version_check_done(self, latest): def _load_portrait(self, char_id): url = f"https://images.evetech.net/characters/{char_id}/portrait?size=128" + Logger.info(f"Portrait request: char_id={char_id}, url={url}") self.network_manager.get(QtNetwork.QNetworkRequest(QtCore.QUrl(url))) def _on_portrait_loaded(self, reply): + url = reply.url().toString() if reply.error() == QtNetwork.QNetworkReply.NetworkError.NoError: data = reply.readAll() pixmap = QtGui.QPixmap() - pixmap.loadFromData(data) - self.lbl_portrait.setPixmap(pixmap) + ok = pixmap.loadFromData(data) + Logger.info(f"Portrait response: url={url}, data_size={len(data)}, decode_ok={ok}") + if ok: + self.lbl_portrait.setPixmap(pixmap) + else: + Logger.error(f"Portrait image decode failed for {url}") else: - Logger.error(f"Failed to load portrait: {reply.errorString()}") + Logger.error(f"Portrait network error: {reply.errorString()}, url={url}") reply.deleteLater() # event: QCloseEvent diff --git a/src/shortcircuit/model/esi/esi.py b/src/shortcircuit/model/esi/esi.py index b7b605b..48b7fe7 100644 --- a/src/shortcircuit/model/esi/esi.py +++ b/src/shortcircuit/model/esi/esi.py @@ -12,201 +12,201 @@ class ESI: - ''' - ESI - - We are bad boys here. - What should have been done is proxy auth server with code request, storage and all that stuff. - Instead we just follow implicit flow and ask to relogin every time. - From Russia with love. - ''' - - ENDPOINT_ESI_VERIFY = 'https://esi.evetech.net/verify' - ENDPOINT_ESI_LOCATION_FORMAT = 'https://esi.evetech.net/latest/characters/{}/location/' - ENDPOINT_ESI_UNIVERSE_NAMES = 'https://esi.evetech.net/latest/universe/names/' - ENDPOINT_ESI_UI_WAYPOINT = 'https://esi.evetech.net/latest/ui/autopilot/waypoint/' - - ENDPOINT_EVE_AUTH_FORMAT = 'https://login.eveonline.com/v2/oauth/authorize' \ - '?response_type=token&redirect_uri={}&client_id={}&scope={}&state={}' - CLIENT_CALLBACK = 'http://127.0.0.1:7444/callback/' - CLIENT_ID = 'd802bba44b7c4f6cbfa2944b0e5ea83f' - CLIENT_SCOPES = [ - 'esi-location.read_location.v1', - 'esi-ui.write_waypoint.v1', - ] - - def __init__(self, login_callback, logout_callback): - self.login_callback = login_callback - self.logout_callback = logout_callback - self.httpd = None - self.state = None - - self.token = None - self.char_id = None - self.char_name = None - self.sso_timer = None - - def start_server(self): - if not self.httpd: - # Server not running - restart it - Logger.debug('Starting server') - self.httpd = StoppableHTTPServer( - server_address=('127.0.0.1', 7444), - request_handler_class=AuthHandler, - timeout_callback=self.timeout_server, - ) - server_thread = threading.Thread( - target=self.httpd.serve, - args=(self.handle_login, ), - ) - server_thread.daemon = True - server_thread.start() - self.state = str(uuid.uuid4()) - else: - # Server already running - reset timeout counter - self.httpd.tries = 0 - - scopes = ' '.join(ESI.CLIENT_SCOPES) - endpoint_auth = ESI.ENDPOINT_EVE_AUTH_FORMAT.format( - ESI.CLIENT_CALLBACK, ESI.CLIENT_ID, scopes, self.state + """ + ESI + + We are bad boys here. + What should have been done is proxy auth server with code request, storage and all that stuff. + Instead we just follow implicit flow and ask to relogin every time. + From Russia with love. + """ + + ENDPOINT_ESI_VERIFY = "https://esi.evetech.net/verify" + ENDPOINT_ESI_LOCATION_FORMAT = "https://esi.evetech.net/latest/characters/{}/location/" + ENDPOINT_ESI_UNIVERSE_NAMES = "https://esi.evetech.net/latest/universe/names/" + ENDPOINT_ESI_UI_WAYPOINT = "https://esi.evetech.net/latest/ui/autopilot/waypoint/" + + ENDPOINT_EVE_AUTH_FORMAT = ( + "https://login.eveonline.com/v2/oauth/authorize" + "?response_type=token&redirect_uri={}&client_id={}&scope={}&state={}" ) + CLIENT_CALLBACK = "http://127.0.0.1:7444/callback/" + CLIENT_ID = "d802bba44b7c4f6cbfa2944b0e5ea83f" + CLIENT_SCOPES = [ + "esi-location.read_location.v1", + "esi-ui.write_waypoint.v1", + ] + + def __init__(self, login_callback, logout_callback): + self.login_callback = login_callback + self.logout_callback = logout_callback + self.httpd = None + self.state = None - if __import__('sys').platform == 'linux': - import subprocess - import os - env = os.environ.copy() - env.pop("LD_LIBRARY_PATH", None) - try: - subprocess.Popen(["xdg-open", endpoint_auth], env=env) - return True - except OSError: - return webbrowser.open(endpoint_auth) - else: - return webbrowser.open(endpoint_auth) - - def timeout_server(self): - self.httpd = None - - def stop_server(self): - Logger.debug('Stopping server') - if self.httpd: - self.httpd.stop() - self.httpd = None - - def handle_login(self, message): - if not message: - return - - if 'state' in message: - if message['state'][0] != self.state: - Logger.warning('OAUTH state mismatch') - return - - if 'access_token' in message: - self.token = message['access_token'][0] - self.sso_timer = threading.Timer( - int(message['expires_in'][0]), self._logout - ) - self.sso_timer.daemon = True - self.sso_timer.start() - - r = httpx.get(ESI.ENDPOINT_ESI_VERIFY, headers=self._get_headers()) - if r.status_code == httpx.codes.OK: - data = r.json() - self.char_id = data['CharacterID'] - self.char_name = data['CharacterName'] - - self.login_callback(True, self.char_name) - else: self.token = None - self.sso_timer = None self.char_id = None self.char_name = None + self.sso_timer = None - self.login_callback(False, None) - - self.stop_server() - - def _get_headers(self): - return { - 'User-Agent': USER_AGENT, - 'Authorization': 'Bearer {}'.format(self.token), - } - - def get_char_location(self): - if not self.token: - return None - - current_location_name = None - current_location_id = None - - r = httpx.get( - ESI.ENDPOINT_ESI_LOCATION_FORMAT.format(self.char_id), - headers=self._get_headers() - ) - if r.status_code == httpx.codes.OK: - current_location_id = r.json()['solar_system_id'] - - r = httpx.post( - ESI.ENDPOINT_ESI_UNIVERSE_NAMES, json=[str(current_location_id)] - ) - if r.status_code == httpx.codes.OK: - current_location_name = r.json()[0]['name'] - - return current_location_name - - def set_char_destination(self, sys_id): - if not self.token: - return False - - success = False - r = httpx.post( - '{}?add_to_beginning=false&clear_other_waypoints=true&destination_id={}'. - format( - ESI.ENDPOINT_ESI_UI_WAYPOINT, - sys_id, - ), - headers=self._get_headers() - ) - if r.status_code == 204: - success = True - - return success - - def logout(self): - if self.sso_timer: - self.sso_timer.cancel() - self._logout() - - def _logout(self): - self.token = None - self.char_id = None - self.char_name = None - self.logout_callback() + def start_server(self): + if not self.httpd: + # Server not running - restart it + Logger.debug("Starting server") + self.httpd = StoppableHTTPServer( + server_address=("127.0.0.1", 7444), + request_handler_class=AuthHandler, + timeout_callback=self.timeout_server, + ) + server_thread = threading.Thread( + target=self.httpd.serve, + args=(self.handle_login,), + ) + server_thread.daemon = True + server_thread.start() + self.state = str(uuid.uuid4()) + else: + # Server already running - reset timeout counter + self.httpd.tries = 0 + + scopes = " ".join(ESI.CLIENT_SCOPES) + endpoint_auth = ESI.ENDPOINT_EVE_AUTH_FORMAT.format( + ESI.CLIENT_CALLBACK, ESI.CLIENT_ID, scopes, self.state + ) + + if __import__("sys").platform == "linux": + import subprocess + import os + + env = os.environ.copy() + env.pop("LD_LIBRARY_PATH", None) + try: + subprocess.Popen(["xdg-open", endpoint_auth], env=env) + return True + except OSError: + return webbrowser.open(endpoint_auth) + else: + return webbrowser.open(endpoint_auth) + + def timeout_server(self): + self.httpd = None + + def stop_server(self): + Logger.debug("Stopping server") + if self.httpd: + self.httpd.stop() + self.httpd = None + + def handle_login(self, message): + if not message: + return + + if "state" in message: + if message["state"][0] != self.state: + Logger.warning("OAUTH state mismatch") + return + + if "access_token" in message: + self.token = message["access_token"][0] + self.sso_timer = threading.Timer(int(message["expires_in"][0]), self._logout) + self.sso_timer.daemon = True + self.sso_timer.start() + + r = httpx.get(ESI.ENDPOINT_ESI_VERIFY, headers=self._get_headers()) + if r.status_code == httpx.codes.OK: + data = r.json() + # Explicitly ensure these are the correct types + self.char_id = int(data.get("CharacterID", 0)) + self.char_name = str(data.get("CharacterName", "Unknown")) + + self.login_callback( + {"is_ok": True, "char_name": self.char_name, "char_id": self.char_id} + ) + else: + self.token = None + self.sso_timer = None + self.char_id = None + self.char_name = None + + self.login_callback({"is_ok": False, "char_name": None, "char_id": 0}) + + self.stop_server() + + def _get_headers(self): + return { + "User-Agent": USER_AGENT, + "Authorization": "Bearer {}".format(self.token), + } + + def get_char_location(self): + if not self.token: + return None + + current_location_name = None + current_location_id = None + + r = httpx.get( + ESI.ENDPOINT_ESI_LOCATION_FORMAT.format(self.char_id), headers=self._get_headers() + ) + if r.status_code == httpx.codes.OK: + current_location_id = r.json()["solar_system_id"] + + r = httpx.post(ESI.ENDPOINT_ESI_UNIVERSE_NAMES, json=[str(current_location_id)]) + if r.status_code == httpx.codes.OK: + current_location_name = r.json()[0]["name"] + + return current_location_name + + def set_char_destination(self, sys_id): + if not self.token: + return False + + success = False + r = httpx.post( + "{}?add_to_beginning=false&clear_other_waypoints=true&destination_id={}".format( + ESI.ENDPOINT_ESI_UI_WAYPOINT, + sys_id, + ), + headers=self._get_headers(), + ) + if r.status_code == 204: + success = True + + return success + + def logout(self): + if self.sso_timer: + self.sso_timer.cancel() + self._logout() + + def _logout(self): + self.token = None + self.char_id = None + self.char_name = None + self.logout_callback() def login_cb(char_name): - print('Welcome, {}'.format(char_name)) + print("Welcome, {}".format(char_name)) def logout_cb(): - print('Session expired') + print("Session expired") def main(): - import code + import code - implicit = True - client_id = '' - client_secret = '' + implicit = True + client_id = "" + client_secret = "" - esi = ESI(login_cb, logout_cb) - print(esi.start_server()) - gvars = globals().copy() - gvars.update(locals()) - shell = code.InteractiveConsole(gvars) - shell.interact() + esi = ESI(login_cb, logout_cb) + print(esi.start_server()) + gvars = globals().copy() + gvars.update(locals()) + shell = code.InteractiveConsole(gvars) + shell.interact() -if __name__ == '__main__': - main() +if __name__ == "__main__": + main() diff --git a/src/shortcircuit/model/esi_processor.py b/src/shortcircuit/model/esi_processor.py index a084aed..16a848b 100644 --- a/src/shortcircuit/model/esi_processor.py +++ b/src/shortcircuit/model/esi_processor.py @@ -7,50 +7,51 @@ class ESIProcessor(QtCore.QObject): - """ - ESI Middleware - """ - login_response = QtCore.Signal(bool, str) - logout_response = QtCore.Signal() - location_response = QtCore.Signal(str) - destination_response = QtCore.Signal(bool) - - def __init__(self, parent=None): - super().__init__(parent) - self.esi = ESI(self._login_callback, self._logout_callback) - - def login(self): - return self.esi.start_server() - - def logout(self): - self.esi.logout() - - def get_location(self): - server_thread = threading.Thread(target=self._get_location) - server_thread.daemon = True - server_thread.start() - - def _get_location(self): - location = self.esi.get_char_location() - self.location_response.emit(location) - - # TODO properly type this - def set_destination(self, sys_id): - server_thread = threading.Thread( - target=self._set_destination, - args=(sys_id, ), - ) - server_thread.daemon = True - server_thread.start() - - # TODO properly type this - def _set_destination(self, sys_id): - response = self.esi.set_char_destination(sys_id) - self.destination_response.emit(response) - - # TODO properly type this - def _login_callback(self, is_ok, char_name): - self.login_response.emit(is_ok, char_name) - - def _logout_callback(self): - self.logout_response.emit() + """ + ESI Middleware + """ + + login_response = QtCore.Signal(dict) + logout_response = QtCore.Signal() + location_response = QtCore.Signal(str) + destination_response = QtCore.Signal(bool) + + def __init__(self, parent=None): + super().__init__(parent) + self.esi = ESI(self._login_callback, self._logout_callback) + + def login(self): + return self.esi.start_server() + + def logout(self): + self.esi.logout() + + def get_location(self): + server_thread = threading.Thread(target=self._get_location) + server_thread.daemon = True + server_thread.start() + + def _get_location(self): + location = self.esi.get_char_location() + self.location_response.emit(location) + + # TODO properly type this + def set_destination(self, sys_id): + server_thread = threading.Thread( + target=self._set_destination, + args=(sys_id,), + ) + server_thread.daemon = True + server_thread.start() + + # TODO properly type this + def _set_destination(self, sys_id): + response = self.esi.set_char_destination(sys_id) + self.destination_response.emit(response) + + # TODO properly type this + def _login_callback(self, result): + self.login_response.emit(result) + + def _logout_callback(self): + self.logout_response.emit() diff --git a/src/shortcircuit/model/gui_source_toggles.py b/src/shortcircuit/model/gui_source_toggles.py index 95663e5..40a1231 100644 --- a/src/shortcircuit/model/gui_source_toggles.py +++ b/src/shortcircuit/model/gui_source_toggles.py @@ -53,7 +53,11 @@ def refresh_menu(self): for source in sources: # Create a sub-menu for each source - title = f"{source.name} ({source.type.value})" + status_emoji = "🟢" if source.enabled and source.status_ok else "⚪" + if source.enabled and not source.status_ok: + status_emoji = "🔴" + + title = f"{status_emoji} {source.name} ({source.type.value})" if source.last_updated: delta = now - source.last_updated secs = int(delta.total_seconds()) diff --git a/src/shortcircuit/model/utility/gui_sources.py b/src/shortcircuit/model/utility/gui_sources.py index cd1098c..52f7fae 100644 --- a/src/shortcircuit/model/utility/gui_sources.py +++ b/src/shortcircuit/model/utility/gui_sources.py @@ -71,12 +71,20 @@ def __init__(self, source_manager, parent=None): splitter.setSizes([200, 500]) # Bottom buttons - button_box = QtWidgets.QDialogButtonBox( - QtWidgets.QDialogButtonBox.Save | QtWidgets.QDialogButtonBox.Cancel - ) - button_box.button(QtWidgets.QDialogButtonBox.Save).clicked.connect(self._save_only) - button_box.rejected.connect(self.reject) - main_layout.addWidget(button_box) + btn_layout = QtWidgets.QHBoxLayout() + btn_layout.addStretch() + + self.btn_save = QtWidgets.QPushButton("Save") + self.btn_save.setFixedWidth(100) + self.btn_save.clicked.connect(self._save_only) + btn_layout.addWidget(self.btn_save) + + self.btn_close = QtWidgets.QPushButton("Close") + self.btn_close.setFixedWidth(100) + self.btn_close.clicked.connect(self.accept) + btn_layout.addWidget(self.btn_close) + + main_layout.addLayout(btn_layout) # Populate list self._populate_list()