diff --git a/src/dota_notes/app_dota.py b/src/dota_notes/app_dota.py index db62c9e..c61c2f5 100644 --- a/src/dota_notes/app_dota.py +++ b/src/dota_notes/app_dota.py @@ -2,22 +2,28 @@ from dota2.client import Dota2Client from dota2.msg import EDOTAGCMsg -import logging - -from dota_notes.data.messages import Message, MessageType, MessageServerIdResponse, MessageConSatus, \ - MessageServerIdRequest - -logging.basicConfig(format='[%(asctime)s] %(levelname)s %(name)s: %(message)s', level=logging.DEBUG) +from dota_notes.data.messages import MessageServerIdResponse, MessageConnectionStatus, MessageServerIdRequest, \ + MessageConnect def dota_process(username, password, match_id_in_queue, server_id_out_queue): + """Dota client spawner""" app = DotaApp(username, password, match_id_in_queue, server_id_out_queue) app.run() class DotaApp: - def __init__(self, username, password, message_queue_dota, message_queue_qt): - self.username = username + """Dota client processing jobs requested by the Qt process. + + Args: + user: Steam username + password: Steam password + message_queue_dota: Queue to receive jobs + message_queue_qt: Queue to send job results + """ + + def __init__(self, user, password, message_queue_dota, message_queue_qt): + self.user = user self.password = password self.message_queue_dota = message_queue_dota self.message_queue_qt = message_queue_qt @@ -35,37 +41,37 @@ def __init__(self, username, password, message_queue_dota, message_queue_qt): self.dota.on(EDOTAGCMsg.EMsgGCSpectateFriendGameResponse, self.on_spectate_response) def on_logged_on(self): - self.message_queue_qt.put(Message(MessageType.CLIENTS_STATUS, MessageConSatus("On", "Try"))) + self.message_queue_qt.put(MessageConnectionStatus("On", "Try")) self.dota.launch() def do_dota_stuff(self): - self.message_queue_qt.put(Message(MessageType.CLIENTS_STATUS, MessageConSatus("On", "On"))) + self.message_queue_qt.put(MessageConnectionStatus("On", "On")) self.dota_ready = True def on_disconnect(self): - self.message_queue_qt.put(Message(MessageType.CLIENTS_STATUS, MessageConSatus("Off", "Off"))) + self.message_queue_qt.put(MessageConnectionStatus("Off", "Off")) def on_spectate_response(self, response): - self.message_queue_qt.put(Message(MessageType.SERVER_ID_RESPONSE, - MessageServerIdResponse(response.server_steamid))) + self.message_queue_qt.put(MessageServerIdResponse(response.server_steamid)) def connect(self): - self.message_queue_qt.put(Message(MessageType.CLIENTS_STATUS, MessageConSatus("Try", "Off"))) - self.steam.login(username=self.username, password=self.password) + self.message_queue_qt.put(MessageConnectionStatus("Try", "Off")) + self.steam.login(username=self.user, password=self.password) def run(self): while self.keep_running: if not self.message_queue_dota.empty(): message = self.message_queue_dota.get(block=False) - if message.message_type == MessageType.CLIENTS_CONNECT: + if isinstance(message, MessageConnect): + self.user = message.user + self.password = message.password self.connect() else: self.message_buffer_queue.append(message) if self.dota_ready and len(self.message_buffer_queue) > 0: message = self.message_buffer_queue.pop(0) - if message.message_type == MessageType.SERVER_ID_REQUEST: - message_request: MessageServerIdRequest = message.payload - self.dota.send(EDOTAGCMsg.EMsgGCSpectateFriendGame, {'steam_id': int(message_request.account_id)}) + if isinstance(message, MessageServerIdRequest): + self.dota.send(EDOTAGCMsg.EMsgGCSpectateFriendGame, {'steam_id': int(message.account_id)}) self.steam.sleep(1) def stop(self): diff --git a/src/dota_notes/app_flask.py b/src/dota_notes/app_flask.py index a5e7328..eab8d3f 100644 --- a/src/dota_notes/app_flask.py +++ b/src/dota_notes/app_flask.py @@ -2,17 +2,18 @@ def flask_process(port, match_info_queue): - """Simple Flask application listening to GSI and sends match_ids detected to the Qt Application. - - Args: - port: port to listen to - match_info_queue: Queue to transmit the match information - """ + """Flask app spawner""" flask_app = FlaskApp(port, match_info_queue) flask_app.run() class FlaskApp: + """Simple Flask application listening to GSI and sends info detected to the Qt Application. + + Args: + port: port to listen to + match_info_queue: Queue to transmit the match information + """ def __init__(self, port, match_info_queue): self.app = Flask(__name__) self.port = port diff --git a/src/dota_notes/data/game_state.py b/src/dota_notes/data/game_state.py deleted file mode 100644 index bebb367..0000000 --- a/src/dota_notes/data/game_state.py +++ /dev/null @@ -1,23 +0,0 @@ -from PySide6.QtCore import QObject - -from dota_notes.data.player_state import PlayerState - - -class GameState(QObject): - match_id = 0 - server_id = 0 - players = [] - - def __init__(self): - super().__init__() - self.fresh_players() - - def fresh_players(self): - old_player_state = {} - for player in self.players: - old_player_state[player.steam_id] = player - self.players = [ - PlayerState(), PlayerState(), PlayerState(), PlayerState(), PlayerState(), - PlayerState(), PlayerState(), PlayerState(), PlayerState(), PlayerState(), - ] - return old_player_state diff --git a/src/dota_notes/data/messages.py b/src/dota_notes/data/messages.py index 735a7cb..7a0d892 100644 --- a/src/dota_notes/data/messages.py +++ b/src/dota_notes/data/messages.py @@ -1,25 +1,25 @@ -from typing import Optional -from enum import Enum +class MessageConnect: + """Ask the client to connect with specific credentials + Attributes: + user: steam username + password: steam password + """ + user: str + password: str -class MessageType(Enum): - UNKNOWN = 0 - CLIENTS_STATUS = 1 - CLIENTS_CONNECT = 2 - SERVER_ID_REQUEST = 3 - SERVER_ID_RESPONSE = 4 + def __init__(self, user: str, password: str): + self.user = user + self.password = password -class Message: - message_type: MessageType = MessageType.UNKNOWN - payload: Optional[object] +class MessageConnectionStatus: + """Report the status of the connections of steam/dota clients - def __init__(self, message_type: MessageType, payload: Optional[object] = None): - self.message_type = message_type - self.payload = payload - - -class MessageConSatus: + Attributes: + steam: Steam connection status + dota: Dota connection status + """ steam: str dota: str @@ -29,6 +29,7 @@ def __init__(self, steam: str, dota: str): class MessageServerIdRequest: + """Request the server ID a specific account is playing on""" account_id: str def __init__(self, account_id: str): @@ -36,6 +37,7 @@ def __init__(self, account_id: str): class MessageServerIdResponse: + """Server ID where a specific player is playing on""" server_id: int def __init__(self, server_id: int): diff --git a/src/dota_notes/data/database.py b/src/dota_notes/data/models.py similarity index 61% rename from src/dota_notes/data/database.py rename to src/dota_notes/data/models.py index 3b0bef6..b1c7024 100644 --- a/src/dota_notes/data/database.py +++ b/src/dota_notes/data/models.py @@ -1,33 +1,56 @@ import os import sys -from select import select from typing import Optional from sqlalchemy import create_engine, String from sqlalchemy.orm import Session, DeclarativeBase, mapped_column, Mapped -class Database: +class Database(object): + """Singleton defining database URI and unique ressources. + + Attributes: + _instance: Singleton instance + uri: database location + engine: database connection used for session generation + """ + + _instance = None + + def __new__(cls, *args, **kwargs): + """New overload to create a singleton.""" + if not isinstance(cls._instance, cls): + cls._instance = object.__new__(cls) + return cls._instance + def __init__(self): + """Defines all necessary ressources (URI & engine) and create database if necessary.""" if getattr(sys, 'frozen', False): file_uri = os.path.dirname(sys.executable) elif __file__: file_uri = os.path.dirname(__file__) self.uri = 'sqlite+pysqlite:///{0}/sqlite.db'.format(file_uri) self.engine = create_engine(self.uri, echo=False) - Base.metadata.create_all(self.engine) + BaseEntity.metadata.create_all(self.engine) with Session(self.engine) as session: - if session.get(Setting, "version") is None: - session.add(Setting("version", "1")) + if session.get(SettingEntity, "version") is None: + session.add(SettingEntity("version", "1")) session.commit() -class Base(DeclarativeBase): +class BaseEntity(DeclarativeBase): + """Database model base class""" pass -class Setting(Base): +class SettingEntity(BaseEntity): + """An application setting. + + Attributes: + key: unique string defining a setting + value: value of the setting + """ __tablename__ = 'settings' key: Mapped[str] = mapped_column(primary_key=True) @@ -41,7 +64,23 @@ def __repr__(self) -> str: return f"Setting(key={self.key!r}, value={self.value!r})" -class Player(Base): +class PlayerEntity(BaseEntity): + """Player information + + Attributes: + steam_id: unique identifier + name: last seen name + pro_name: pro name (if fetched from the API) + custom_name: user set name + smurf: user defined smurf indicator + is_racist: flag + is_sexist: flag + is_toxic: flag + is_feeder: flag + gives_up: flag + destroys_items: flag + note: user set note + """ __tablename__ = 'players' steam_id: Mapped[str] = mapped_column(primary_key=True) @@ -74,7 +113,12 @@ def __init__(self, steam_id, name, pro_name=None, custom_name="", smurf="", is_r @staticmethod def make_from_state(player_state): - return Player( + """Create an entity from its state counterpart + + Args: + player_state: state to import information from + """ + return PlayerEntity( str(player_state.steam_id), player_state.name, player_state.pro_name if player_state.pro_name != "" else None, @@ -89,6 +133,12 @@ def make_from_state(player_state): @staticmethod def import_export(from_object, to_object): + """Copy the attributes from state to entity (or reverse) + + Args: + from_object: Object to copy attributes from (can be entity or state) + to_object: Object to copy attributes to (can be entity or state) + """ to_object.pro_name = from_object.pro_name to_object.custom_name = from_object.custom_name to_object.smurf = from_object.smurf @@ -101,5 +151,4 @@ def import_export(from_object, to_object): to_object.note = from_object.note def __repr__(self) -> str: - return f"Player(steam_id{self.steam_id!r}, last_seen_name={self.name!r}, " \ - f"pro_name={self.pro_name!r}, custom_name={self.custom_name!r}, smurf={self.smurf!r})" + return f"Player(steam_id{self.steam_id!r}, last_seen_name={self.name!r}, custom_name={self.custom_name!r})" diff --git a/src/dota_notes/data/player_state.py b/src/dota_notes/data/player_state.py deleted file mode 100644 index 4eb78e6..0000000 --- a/src/dota_notes/data/player_state.py +++ /dev/null @@ -1,16 +0,0 @@ -from PySide6.QtCore import QObject - - -class PlayerState(QObject): - steam_id = 0 - name = "" - pro_name = None - custom_name = "" - smurf = "" - is_racist = False - is_sexist = False - is_toxic = False - is_feeder = False - gives_up = False - destroys_items = False - note = "" diff --git a/src/dota_notes/data/settings.py b/src/dota_notes/data/settings.py index 667d075..d29cf82 100644 --- a/src/dota_notes/data/settings.py +++ b/src/dota_notes/data/settings.py @@ -1,9 +1,24 @@ -from dota_notes.data.database import Setting +from dota_notes.data.models import SettingEntity -class Settings: +class Settings(object): + """Singleton defining settings used by the application. + Attributes: + _instance: Singleton instance + gsi_port: Port used by the http server awaiting request from GSI + gsi_spectate: flag to enable gsi spectate or not + software_mode: Mode the software works into + proxy_url: URL to do requests at when using "Proxy" mode + proxy_api_key: Key to use in "Proxy" mode + steam_user: Steam username in "Client" mode + steam_password: Steam password in "Client" mode + steam_api_key: Steam WEB API key in "Client" mode + """ + + _instance = None gsi_port: int = 58765 + gsi_spectate: bool = False software_mode: str = "Client" proxy_url: str = "https://dota-notes.the-cluster.org" proxy_api_key: str = "" @@ -11,13 +26,42 @@ class Settings: steam_password: str = "" steam_api_key: str = "" - TO_IMPORT_EXPORT = ["software_mode", "proxy_url", "proxy_api_key", "steam_user", "steam_password", "steam_api_key"] + TO_IMPORT_EXPORT = ["gsi_port", "gsi_spectate", "software_mode", "proxy_url", "proxy_api_key", "steam_user", + "steam_password", "steam_api_key"] + + def __new__(cls, *args, **kwargs): + """New overload to create a singleton.""" + if not isinstance(cls._instance, cls): + cls._instance = object.__new__(cls) + return cls._instance def import_from_database(self, session): + """Import this object attributes from the database. + + Args: + session: Database session to use for requests + """ + for at in self.TO_IMPORT_EXPORT: + row = session.get(SettingEntity, at) + if row is not None: + at_type = type(getattr(self, at)) + if at_type is bool: + setattr(self, at, row.value == "True") + else: + setattr(self, at, at_type(row.value)) + else: + session.add(SettingEntity(at, str(getattr(self, at)))) + + def export_to_database(self, session): + """Export this object attributes to the database. + Args: + session: Database session to use for requests + """ for at in self.TO_IMPORT_EXPORT: - row = session.get(Setting, at) + row = session.get(SettingEntity, at) if row is not None: - setattr(self, at, row.value) + at_type = type(getattr(self, at)) + row.value = at_type(getattr(self, at)) else: - session.add(Setting(at, getattr(self, at))) + session.add(SettingEntity(at, str(getattr(self, at)))) diff --git a/src/dota_notes/data/states.py b/src/dota_notes/data/states.py new file mode 100644 index 0000000..a1dd04e --- /dev/null +++ b/src/dota_notes/data/states.py @@ -0,0 +1,49 @@ +from PySide6.QtCore import QObject + + +class GameState(QObject): + """In memory information about a game + + Attributes: + match_id: unique identifier of a match + server_id: unique identifier of the server the match is played on + players: list of all the player in the game + """ + match_id = 0 + server_id = 0 + players = [] + + def __init__(self): + super().__init__() + self.fresh_players() + + def fresh_players(self): + """Clean the list of players. + + Returns + list of players before the cleaning + """ + old_player_state = {} + for player in self.players: + old_player_state[player.steam_id] = player + self.players = [ + PlayerState(), PlayerState(), PlayerState(), PlayerState(), PlayerState(), + PlayerState(), PlayerState(), PlayerState(), PlayerState(), PlayerState(), + ] + return old_player_state + + +class PlayerState(QObject): + """In memory information about a player. Attributes similar to the PlayerEntity""" + steam_id = 0 + name = "" + pro_name = None + custom_name = "" + smurf = "" + is_racist = False + is_sexist = False + is_toxic = False + is_feeder = False + gives_up = False + destroys_items = False + note = "" diff --git a/src/dota_notes/dota_notes.py b/src/dota_notes/dota_notes.py index 2cea66b..2ff0f06 100644 --- a/src/dota_notes/dota_notes.py +++ b/src/dota_notes/dota_notes.py @@ -1,8 +1,8 @@ import multiprocessing import sys -from dota_notes.data.game_state import GameState -from dota_notes.data.database import Database, Setting +from dota_notes.data.states import GameState +from dota_notes.data.models import Database from dota_notes.app_flask import flask_process from dota_notes.app_dota import dota_process from dota_notes.data.settings import Settings @@ -10,12 +10,27 @@ from sqlalchemy.orm import Session +import logging +logging.basicConfig(format='[%(asctime)s] %(levelname)s %(name)s: %(message)s', level=logging.DEBUG) + class DotaNotes: + """Main software class + + Attributes: + settings: settings + database: database connection + state: in-memory data state + message_queue_dota: queue to send message to the dota process + message_queue_qt: queue to send message to the qt process + app_flask: Web server process listening to GSI + app_dota: Dota client process + app_qt: Qt process + """ def __init__(self): - self.state = GameState() - self.database = Database() self.settings = Settings() + self.database = Database() + self.state = GameState() with Session(self.database.engine) as session: self.settings.import_from_database(session) session.commit() @@ -23,26 +38,21 @@ def __init__(self): self.match_information_from_gsi = multiprocessing.Queue() self.message_queue_dota = multiprocessing.Queue() self.message_queue_qt = multiprocessing.Queue() - self.app_qt = QtApp(self) - - def run(self): - # Web Server - app_flask = multiprocessing.Process(target=flask_process, args=(self.settings.gsi_port, - self.match_information_from_gsi,)) - app_flask.start() - # Dota client - app_dota = multiprocessing.Process( + self.app_flask = multiprocessing.Process(target=flask_process, args=(self.settings.gsi_port, + self.match_information_from_gsi,)) + self.app_dota = multiprocessing.Process( target=dota_process, args=(self.settings.steam_user, self.settings.steam_password, self.message_queue_dota, - self.message_queue_qt,) - ) - app_dota.start() + self.message_queue_qt,)) + self.app_qt = QtApp(self) - # Qt App + def run(self): + """Run the software""" + self.app_flask.start() + self.app_dota.start() return_code = self.app_qt.run() - # Clean - app_flask.kill() - app_dota.kill() + self.app_flask.kill() + self.app_dota.kill() sys.exit(return_code) diff --git a/src/dota_notes/helpers.py b/src/dota_notes/helpers.py index b0adf3f..40569a6 100644 --- a/src/dota_notes/helpers.py +++ b/src/dota_notes/helpers.py @@ -14,8 +14,7 @@ def get_game_live_stats(api_key, server_id): s = requests.Session() retries = Retry(total=8, backoff_jitter=1, status_forcelist=[400]) s.mount("https://", HTTPAdapter(max_retries=retries)) - url = "https://api.steampowered.com/IDOTA2MatchStats_570/GetRealtimeStats/v1/?key=" + \ - api_key + "&server_steam_id=" + str(server_id) + url = f"https://api.steampowered.com/IDOTA2MatchStats_570/GetRealtimeStats/v1/?key={api_key}&server_steam_id={str(server_id)}" response = s.get(url) response.raise_for_status() diff --git a/src/dota_notes/ui/app_qt.py b/src/dota_notes/ui/app_qt.py index 3cde085..f71aaf5 100644 --- a/src/dota_notes/ui/app_qt.py +++ b/src/dota_notes/ui/app_qt.py @@ -1,22 +1,24 @@ import sys -from datetime import datetime from PySide6.QtCore import QTimer from PySide6.QtWidgets import QApplication from sqlalchemy.orm import Session -from dota_notes.data.database import Setting, Player -from dota_notes.data.messages import Message, MessageType, MessageServerIdResponse, MessageConSatus, MessageServerIdRequest +from dota_notes.data.models import SettingEntity, PlayerEntity +from dota_notes.data.messages import MessageServerIdResponse, MessageConnectionStatus, MessageServerIdRequest, \ + MessageConnect from dota_notes.helpers import get_game_live_stats from dota_notes.ui.main_window import MainWindow class QtApp: + """Qt application process""" + def __init__(self, dota_notes): self.dota_notes = dota_notes - # Build App + # Build Qt app components self.app = QApplication(sys.argv) self.window = MainWindow() self.lastIndexSelected = 0 @@ -33,19 +35,16 @@ def __init__(self, dota_notes): self.window.buttonPhilaeux.clicked.connect(lambda: self.window.inputSteamId.setText("76561197961298382")) self.window.buttonS4.clicked.connect(lambda: self.window.inputSteamId.setText("76561198001497299")) self.window.buttonSearch.clicked.connect(self.on_search_game) - self.window.buttonDetailsSave.clicked.connect(self.save_player_details) + self.window.buttonDetailsSave.clicked.connect(self.on_save_player_details) for i in range(10): getattr(self.window, f"labelPlayer{i}Name").clicked.connect(self.on_label_click) with Session(self.dota_notes.database.engine) as session: - last_search = session.get(Setting, "last_search") + last_search = session.get(SettingEntity, "last_search") if last_search is not None: self.window.inputSteamId.setText(last_search.value) self.window.show() - def status_message(self, message, timeout=0): - self.window.statusBar().showMessage(datetime.now().strftime("%H:%M:%S - ") + message, timeout) - def run(self): timer = QTimer() timer.timeout.connect(self.process_queues) @@ -57,41 +56,51 @@ def run(self): def process_queues(self): while not self.dota_notes.match_information_from_gsi.empty(): match_info = self.dota_notes.match_information_from_gsi.get(block=False) - if self.dota_notes.state.match_id != match_info["match_id"]: + if self.dota_notes.state.match_id != match_info["match_id"] and self.dota_notes.settings.gsi_spectate: self.dota_notes.state.match_id = match_info["match_id"] self.dota_notes.state.server_id = 0 - self.status_message(f"Detected match {self.dota_notes.state.match_id!s} from GSI.") + self.window.draw_status_message(f"Detected match {self.dota_notes.state.match_id!s} from GSI.") self.update_state_with_gsi(self.dota_notes.state, match_info) - self.draw_match_with_state(self.dota_notes.state) + self.window.draw_match_with_state(self.dota_notes.state) self.draw_details_with_player(0) while not self.dota_notes.message_queue_qt.empty(): - message: Message = self.dota_notes.message_queue_qt.get(block=False) - if message.message_type == MessageType.SERVER_ID_RESPONSE: - server_id_message: MessageServerIdResponse = message.payload - if server_id_message.server_id != 0: - self.dota_notes.state.server_id = server_id_message.server_id + message = self.dota_notes.message_queue_qt.get(block=False) + if isinstance(message, MessageServerIdResponse): + if message.server_id != 0: + self.dota_notes.state.server_id = message.server_id game_json = get_game_live_stats(self.dota_notes.settings.steam_api_key, self.dota_notes.state.server_id) self.update_state_with_json(self.dota_notes.state, game_json) self.update_state_with_database(self.dota_notes.state) - self.status_message("Found game info for player " + self.window.inputSteamId.text() + " using WEBAPI.") - self.draw_match_with_state(self.dota_notes.state) + self.window.draw_status_message("Found game info for player " + self.window.inputSteamId.text() + " using WEBAPI.") + self.window.draw_match_with_state(self.dota_notes.state) self.draw_details_with_player(0) else: - self.status_message("No game found for player " + self.window.inputSteamId.text()) - elif message.message_type == MessageType.CLIENTS_STATUS: - con_status_message: MessageConSatus = message.payload - self.window.draw_connection_status(con_status_message.steam, con_status_message.dota) + self.window.draw_status_message("No game found for player " + self.window.inputSteamId.text()) + elif isinstance(message, MessageConnectionStatus): + self.window.draw_connection_status(message.steam, message.dota) def on_open_settings(self): - self.window.comboBoxSettingsMode.setCurrentText(self.dota_notes.settings.software_mode) - self.window.lineEditSettingsProxyURL.setText(self.dota_notes.settings.proxy_url) - self.window.lineEditSettingsProxyAPIKey.setText(self.dota_notes.settings.proxy_api_key) - self.window.lineEditSettingsSteamUser.setText(self.dota_notes.settings.steam_user) - self.window.lineEditSettingsSteamPassword.setText(self.dota_notes.settings.steam_password) - self.window.lineEditSettingsSteamAPIKey.setText(self.dota_notes.settings.steam_api_key) + settings = self.dota_notes.settings + self.window.comboBoxSettingsMode.setCurrentText(settings.software_mode) + self.window.checkBoxGSI.setChecked(settings.gsi_spectate) + self.window.lineEditSettingsProxyURL.setText(settings.proxy_url) + self.window.lineEditSettingsProxyAPIKey.setText(settings.proxy_api_key) + self.window.lineEditSettingsSteamUser.setText(settings.steam_user) + self.window.lineEditSettingsSteamPassword.setText(settings.steam_password) + self.window.lineEditSettingsSteamAPIKey.setText(settings.steam_api_key) self.window.centralStackedWidget.setCurrentIndex(1) def on_settings_save(self): + settings = self.dota_notes.settings + settings.software_mode = self.window.comboBoxSettingsMode.currentText() + settings.gsi_spectate = self.window.checkBoxGSI.isChecked() + settings.proxy_url = self.window.lineEditSettingsProxyURL.text() + settings.proxy_api_key = self.window.lineEditSettingsProxyAPIKey.text() + settings.steam_user = self.window.lineEditSettingsSteamUser.text() + settings.steam_password = self.window.lineEditSettingsSteamPassword.text() + settings.steam_api_key = self.window.lineEditSettingsSteamAPIKey.text() + with Session(self.dota_notes.database.engine) as session: + settings.export_to_database(session) self.window.centralStackedWidget.setCurrentIndex(0) def on_settings_cancel(self): @@ -99,7 +108,8 @@ def on_settings_cancel(self): def on_connect_client(self): self.window.buttonConnect.setVisible(False) - self.dota_notes.message_queue_dota.put(Message(MessageType.CLIENTS_CONNECT)) + message = MessageConnect(self.dota_notes.settings.steam_user, self.dota_notes.settings.steam_password) + self.dota_notes.message_queue_dota.put(message) def on_label_click(self, label_name, label_text): row = 0 @@ -126,38 +136,15 @@ def draw_details_with_player(self, player_slot): self.window.checkBoxDetailsDestroysItems.setChecked(player_state.destroys_items) self.window.inputDetailsNote.setPlainText(player_state.note) - def draw_match_with_state(self, data): - self.window.labelMatchId.setText(str(data.match_id)) - self.window.labelServerId.setText(str(data.server_id)) - for index, player in enumerate(data.players): - if index > 9: - break - self.draw_match_player(index, player) - - def draw_match_player(self, player_slot, player_data): - getattr(self.window, f"labelPlayer{player_slot}Name").setText(player_data.name) - getattr(self.window, f"labelPlayer{player_slot}ProName").setText( - player_data.pro_name if player_data.pro_name is not None else "") - getattr(self.window, f"labelPlayer{player_slot}CustomName").setText( - player_data.custom_name if player_data.custom_name is not None else "") - getattr(self.window, f"labelPlayer{player_slot}Smurf").setText(player_data.smurf) - getattr(self.window, f"labelPlayer{player_slot}FlagRacist").setVisible(player_data.is_racist) - getattr(self.window, f"labelPlayer{player_slot}FlagSexist").setVisible(player_data.is_sexist) - getattr(self.window, f"labelPlayer{player_slot}FlagToxic").setVisible(player_data.is_toxic) - getattr(self.window, f"labelPlayer{player_slot}FlagFeeder").setVisible(player_data.is_feeder) - getattr(self.window, f"labelPlayer{player_slot}FlagGivesUp").setVisible(player_data.gives_up) - getattr(self.window, f"labelPlayer{player_slot}FlagDestroyer").setVisible(player_data.destroys_items) - def on_search_game(self): if self.window.inputSteamId.text() != "": - self.dota_notes.message_queue_dota.put(Message(MessageType.SERVER_ID_REQUEST, - MessageServerIdRequest(self.window.inputSteamId.text()))) + self.dota_notes.message_queue_dota.put(MessageServerIdRequest(self.window.inputSteamId.text())) with Session(self.dota_notes.database.engine) as session: - last_search = session.query(Setting).filter_by(key="last_search").one_or_none() + last_search = session.query(SettingEntity).filter_by(key="last_search").one_or_none() if last_search is not None: last_search.value = self.window.inputSteamId.text() else: - last_search = Setting("last_search", self.window.inputSteamId.text()) + last_search = SettingEntity("last_search", self.window.inputSteamId.text()) session.add(last_search) session.commit() @@ -193,11 +180,11 @@ def update_state_with_database(self, state): for player in state.players: if player.steam_id == 0: continue - player_db = session.get(Player, str(player.steam_id)) + player_db = session.get(PlayerEntity, str(player.steam_id)) if player_db is not None: - Player.import_export(player_db, player) + PlayerEntity.import_export(player_db, player) - def save_player_details(self): + def on_save_player_details(self): player_state = self.dota_notes.state.players[self.lastIndexSelected] player_state.pro_name = \ self.window.labelDetailsProName.text() if self.window.labelDetailsProName != "" else None @@ -210,13 +197,13 @@ def save_player_details(self): player_state.gives_up = self.window.checkBoxDetailsGivesUp.isChecked() player_state.destroys_items = self.window.checkBoxDetailsDestroysItems.isChecked() player_state.note = self.window.inputDetailsNote.toPlainText() - self.draw_match_player(self.lastIndexSelected, player_state) + self.window.draw_match_player(self.lastIndexSelected, player_state) with Session(self.dota_notes.database.engine) as session: - player_info = session.get(Player, str(player_state.steam_id)) + player_info = session.get(PlayerEntity, str(player_state.steam_id)) if player_info is None: - player_info = Player.make_from_state(player_state) + player_info = PlayerEntity.make_from_state(player_state) session.add(player_info) else: player_info.name = player_state.name - Player.import_export(player_state, player_info) + PlayerEntity.import_export(player_state, player_info) session.commit() diff --git a/src/dota_notes/ui/clickable_label.py b/src/dota_notes/ui/clickable_label.py index 6011c9b..5d845bc 100644 --- a/src/dota_notes/ui/clickable_label.py +++ b/src/dota_notes/ui/clickable_label.py @@ -3,6 +3,7 @@ class ClickableLabel(QLabel): + """Custom QT widget, a Label firing a signal when clicked.""" clicked = Signal(str, str) def __init__(self, *args, **kwargs): diff --git a/src/dota_notes/ui/main_window.py b/src/dota_notes/ui/main_window.py index 35e6d28..c706486 100644 --- a/src/dota_notes/ui/main_window.py +++ b/src/dota_notes/ui/main_window.py @@ -1,13 +1,16 @@ +from datetime import datetime + from PySide6.QtWidgets import QMainWindow from dota_notes.ui.ui_main_window import Ui_MainWindow class MainWindow(QMainWindow, Ui_MainWindow): + """Application main window that is seeded with the generated class from .ui file""" SMURF_CHOICES = ["", "Account Buyer", "Booster", "Main", "Maybe", "Smurf", "Sweaty Smurf"] - FLAGS = ["Racist", "Sexist", "Feeder", "GivesUp", "Destroyer"] - MODE_CHOICES = ["Proxy Server", "Client"] + FLAGS = ["Racist", "Sexist", "Toxic", "Feeder", "GivesUp", "Destroyer"] + MODE_CHOICES = ["Client", "Proxy"] def __init__(self): super().__init__() @@ -18,6 +21,7 @@ def __init__(self): self.comboBoxDetailsSmurf.addItem(smurf_choice) for mode_choice in self.MODE_CHOICES: self.comboBoxSettingsMode.addItem(mode_choice) + self.comboBoxSettingsMode.currentTextChanged.connect(self.on_settings_mode_changed) for i in range(10): for flag in self.FLAGS: @@ -25,7 +29,12 @@ def __init__(self): self.draw_connection_status("Off", "Off") + def draw_status_message(self, message, timeout=0): + """Draw a bottom status message with a date in front""" + self.statusBar().showMessage(datetime.now().strftime("%H:%M:%S - ") + message, timeout) + def draw_connection_status(self, steam: str, dota: str): + """Display the correct label corresponding to the connection status""" possible_status = ["On", "Try", "Off"] for client in ["Steam", "Dota"]: for status in possible_status: @@ -39,3 +48,42 @@ def draw_connection_status(self, steam: str, dota: str): self.buttonConnect.setVisible(True) else: self.buttonConnect.setVisible(False) + + def draw_match_with_state(self, data): + """Draw a match info in the main widget""" + self.labelMatchId.setText(str(data.match_id)) + self.labelServerId.setText(str(data.server_id)) + for index, player in enumerate(data.players): + if index > 9: + break + self.draw_match_player(index, player) + + def draw_match_player(self, player_slot, player_data): + """Draw the player information on the specific slot""" + getattr(self, f"labelPlayer{player_slot}Name").setText(player_data.name) + getattr(self, f"labelPlayer{player_slot}ProName").setText( + player_data.pro_name if player_data.pro_name is not None else "") + getattr(self, f"labelPlayer{player_slot}CustomName").setText( + player_data.custom_name if player_data.custom_name is not None else "") + getattr(self, f"labelPlayer{player_slot}Smurf").setText(player_data.smurf) + getattr(self, f"labelPlayer{player_slot}FlagRacist").setVisible(player_data.is_racist) + getattr(self, f"labelPlayer{player_slot}FlagSexist").setVisible(player_data.is_sexist) + getattr(self, f"labelPlayer{player_slot}FlagToxic").setVisible(player_data.is_toxic) + getattr(self, f"labelPlayer{player_slot}FlagFeeder").setVisible(player_data.is_feeder) + getattr(self, f"labelPlayer{player_slot}FlagGivesUp").setVisible(player_data.gives_up) + getattr(self, f"labelPlayer{player_slot}FlagDestroyer").setVisible(player_data.destroys_items) + + def on_settings_mode_changed(self, new_current_text): + """When the user select a new mode, modify the Settings UI accordingly""" + if new_current_text == "Proxy": + self.lineEditSettingsProxyURL.setEnabled(True) + self.lineEditSettingsProxyAPIKey.setEnabled(True) + self.lineEditSettingsSteamUser.setEnabled(False) + self.lineEditSettingsSteamPassword.setEnabled(False) + self.lineEditSettingsSteamAPIKey.setEnabled(False) + elif new_current_text == "Client": + self.lineEditSettingsProxyURL.setEnabled(False) + self.lineEditSettingsProxyAPIKey.setEnabled(False) + self.lineEditSettingsSteamUser.setEnabled(True) + self.lineEditSettingsSteamPassword.setEnabled(True) + self.lineEditSettingsSteamAPIKey.setEnabled(True) diff --git a/src/dota_notes/ui/ui_main_window.ui b/src/dota_notes/ui/ui_main_window.ui index 87f052a..78269a4 100644 --- a/src/dota_notes/ui/ui_main_window.ui +++ b/src/dota_notes/ui/ui_main_window.ui @@ -3413,7 +3413,7 @@ - + Use GSI to show players when spectating games