diff --git a/.gitignore b/.gitignore index 42fd0f6..b614919 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ -setup_dosoft.iss settings.json -build_dosoft.cmd __pychache__ build dist -main.spec +Dosoft.spec +installer_output diff --git a/__pycache__/app_version.cpython-313.pyc b/__pycache__/app_version.cpython-313.pyc new file mode 100644 index 0000000..0e878dd Binary files /dev/null and b/__pycache__/app_version.cpython-313.pyc differ diff --git a/__pycache__/config_manager.cpython-313.pyc b/__pycache__/config_manager.cpython-313.pyc index db63755..b77ce5d 100644 Binary files a/__pycache__/config_manager.cpython-313.pyc and b/__pycache__/config_manager.cpython-313.pyc differ diff --git a/__pycache__/gui.cpython-313.pyc b/__pycache__/gui.cpython-313.pyc index a0938ec..a0de136 100644 Binary files a/__pycache__/gui.cpython-313.pyc and b/__pycache__/gui.cpython-313.pyc differ diff --git a/__pycache__/i18n_manager.cpython-313.pyc b/__pycache__/i18n_manager.cpython-313.pyc index ae310a2..4e74c26 100644 Binary files a/__pycache__/i18n_manager.cpython-313.pyc and b/__pycache__/i18n_manager.cpython-313.pyc differ diff --git a/__pycache__/keyboard_layout_manager.cpython-313.pyc b/__pycache__/keyboard_layout_manager.cpython-313.pyc index a0ef19d..a8fa35d 100644 Binary files a/__pycache__/keyboard_layout_manager.cpython-313.pyc and b/__pycache__/keyboard_layout_manager.cpython-313.pyc differ diff --git a/__pycache__/logic.cpython-313.pyc b/__pycache__/logic.cpython-313.pyc index 15788bc..55679fd 100644 Binary files a/__pycache__/logic.cpython-313.pyc and b/__pycache__/logic.cpython-313.pyc differ diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..8b80f23 Binary files /dev/null and b/__pycache__/main.cpython-313.pyc differ diff --git a/__pycache__/radial_menu.cpython-313.pyc b/__pycache__/radial_menu.cpython-313.pyc index e58ec7b..73d18a1 100644 Binary files a/__pycache__/radial_menu.cpython-313.pyc and b/__pycache__/radial_menu.cpython-313.pyc differ diff --git a/app_version.py b/app_version.py index b347489..0f25aee 100644 --- a/app_version.py +++ b/app_version.py @@ -2,7 +2,7 @@ import os import sys -APP_VERSION = "1.2.0" +APP_VERSION = "1.2.2" def _candidate_paths(filename: str) -> list[str]: diff --git a/gui.py b/gui.py index b8be23f..a5c57b9 100644 --- a/gui.py +++ b/gui.py @@ -71,6 +71,26 @@ def __init__(self, parent_gui): self.var_keyboard_layout = ctk.StringVar(value=self.app.config.data.get("keyboard_layout", "azerty_fr")) ctk.CTkOptionMenu(frame_keyboard, values=["azerty_fr", "qwerty_us"], variable=self.var_keyboard_layout, command=lambda _: self.save_settings()).pack(side="right", padx=8, pady=8) + # --- BLOC MOTEUR NOTIFICATIONS (DEBUG) --- + frame_debug = ctk.CTkFrame(self.scroll_container) + frame_debug.pack(fill="x", padx=10, pady=5) + + self.lbl_debug = ctk.CTkLabel(frame_debug, text="Moteur Auto-Focus (Si bug)") + self.lbl_debug.pack(side="left", padx=8, pady=8) + + # On définit v2 par défaut + self.var_notif_api = ctk.StringVar(value=self.app.config.data.get("notif_api_mode", "v2")) + + # Le sélecteur avec les 3 versions + combo_debug = ctk.CTkOptionMenu( + frame_debug, + values=["v1", "v2", "v3"], + variable=self.var_notif_api, + command=self.on_notif_api_change + ) + combo_debug.pack(side="right", padx=8, pady=8) + self.parent.bind_i18n_tooltip(combo_debug, "tooltip_api_mode", "Changez de version si l'autofocus cesse de fonctionner.\nNécessite un redémarrage.") + self.btn_close = ctk.CTkButton(self.scroll_container, text=self.app.i18n.t("settings_close", "Fermer"), fg_color="#7f8c8d", command=self.destroy) self.btn_close.pack(pady=(20, 10)) @@ -85,9 +105,25 @@ def save_settings(self): self.app.config.save() self.parent.apply_translations() self.apply_translations() + self.app.config.data["notif_api_mode"] = self.var_notif_api.get() if previous_language != self.var_language.get(): self.title(self.app.i18n.t("settings_title", "⚙️ Paramètres")) + + def on_notif_api_change(self, choice): + self.app.config.data["notif_api_mode"] = choice + self.app.config.save() + + rep = messagebox.askyesno( + "Redémarrage requis", + "Pour appliquer le changement de moteur d'Autofocus, DOSOFT doit être redémarré", + parent=self + ) + if rep: + # On utilise ta fonction de fermeture native qui nettoie tout correctement + self.app.quit_app() + + def apply_translations(self): self.title(self.app.i18n.t("settings_title", "⚙️ Paramètres")) @@ -144,6 +180,7 @@ def __init__(self, app_controller): self.header_f.pack(fill="x", padx=15, pady=(15, 5)) self.lbl_app_title = ctk.CTkLabel(self.header_f, text=APP_TITLE, font=ctk.CTkFont(size=20, weight="bold")) + self.lbl_app_title.pack(side="left") self.btn_settings = ctk.CTkButton(self.header_f, text=self.app.i18n.t("header_settings", "⚙️ Paramètres"), fg_color="#34495e", hover_color="#2c3e50", width=120, command=self.open_settings) @@ -252,6 +289,8 @@ def __init__(self, app_controller): def apply_translations(self): none_label = self.app.i18n.t("none", "Aucun") + self.root.title(self.app.i18n.t("app_title", "DOSOFT v1.1.1")) + self.lbl_app_title.configure(text=self.app.i18n.t("app_title", "DOSOFT v1.1.1")) self.root.title(APP_TITLE) self.lbl_app_title.configure(text=APP_TITLE) self.btn_settings.configure(text=self.app.i18n.t("header_settings", "⚙️ Paramètres")) @@ -293,7 +332,7 @@ def launch_tutorial(self): self.app.config.data["tutorial_done"] = True self.app.config.save() - rep = messagebox.askyesno(self.app.i18n.t("dialog_tutorial_title", "Tutoriel Vidéo"),self.app.i18n.t("dialog_tutorial_text", "Voulez-vous ouvrir la vidéo de présentation sur YouTube dans votre navigateur web ?"),self.app.i18n.t("button_yes", "Oui"),self.app.i18n.t("button_no", "Non")) + rep = messagebox.askyesno(self.app.i18n.t("dialog_tutorial_title", "Tutoriel Vidéo"), self.app.i18n.t("dialog_tutorial_text", "Voulez-vous ouvrir la vidéo de présentation sur YouTube dans votre navigateur web ?")) if rep: webbrowser.open("") diff --git a/main.py b/main.py index a031374..cc7eb07 100644 --- a/main.py +++ b/main.py @@ -425,99 +425,204 @@ def quit_app(self): os.system(f"taskkill /F /PID {my_pid} /T") def start_notification_listener(self): - if not WINSDK_AVAILABLE: return + if not WINSDK_AVAILABLE: + return def run_async_loop(): try: import pythoncom pythoncom.CoInitializeEx(0, pythoncom.COINIT_MULTITHREADED) - except: pass - - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.run_until_complete(self.poll_notifications()) + except Exception: + pass + + mode = self.config.data.get("notif_api_mode", "v2") + if mode == "v1": + engine = self.poll_notifications + elif mode == "v3": + engine = self._poll_notifications_v3 + else: + engine = self._poll_notifications_v2 + + while True: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + loop.run_until_complete(engine()) + except Exception: + pass + finally: + try: + loop.close() + except Exception: + pass + time.sleep(3) threading.Thread(target=run_async_loop, daemon=True).start() async def poll_notifications(self): - # On demande l'accès une seule fois (évite le bug des déconnexions sur certains PC) - try: - listener = UserNotificationListener.current - access = await listener.request_access_async() - if access != 1: - return - except Exception: - return - - seen_ids = set() - first_pass = True + while True: + try: + listener = UserNotificationListener.current + access = await listener.request_access_async() + if access != 1: + await asyncio.sleep(5) + continue + + seen_ids = set() + first_pass = True + + while True: + try: + is_retro = self.config.data.get("game_version", "Unity") == "Rétro" + is_autofocus_on = self.config.data.get("auto_focus_retro", False) + notifs = await listener.get_notifications_async(NotificationKinds.TOAST) + current_ids = set() + + for n in notifs: + current_ids.add(n.id) + if n.id not in seen_ids: + seen_ids.add(n.id) + if not first_pass and is_retro and is_autofocus_on: + try: + binding = n.notification.visual.bindings[0] + texts = [t.text for t in binding.get_text_elements()] + for ligne in texts: + if " - Dofus Retro" in ligne: + pseudo = ligne.split(" - ")[0].strip() + cycle_list = self.logic.get_cycle_list() + for index, acc in enumerate(cycle_list): + if acc['name'] == pseudo: + self.gui.root.after(0, self.logic.focus_window, acc['hwnd']) + self.current_idx = index + break + break + except Exception: + pass + + seen_ids.intersection_update(current_ids) + first_pass = False + except Exception: + pass + await asyncio.sleep(0.5) + except Exception: + pass + await asyncio.sleep(3) - # Fonction asynchrone pour effacer les alertes Dofus sans bloquer le logiciel - async def remove_notif_delayed(notif_id): - await asyncio.sleep(1.0) + # ========================================== + # MOTEUR V2 : AVEC NETTOYAGE NOTIFICATIONS + # ========================================== + async def _poll_notifications_v2(self): + while True: try: - listener.remove_notification(notif_id) + listener = UserNotificationListener.current + access = await listener.request_access_async() + if access != 1: + await asyncio.sleep(5) + continue + + seen_ids = set() + first_pass = True + + async def remove_notif_delayed(notif_id): + await asyncio.sleep(1.0) + try: + listener.remove_notification(notif_id) + except Exception: + pass + + while True: + try: + is_retro = self.config.data.get("game_version", "Unity") == "Rétro" + is_autofocus_on = self.config.data.get("auto_focus_retro", False) + notifs = await listener.get_notifications_async(NotificationKinds.TOAST) + current_ids = set() + + for n in notifs: + current_ids.add(n.id) + if n.id not in seen_ids: + seen_ids.add(n.id) + if not first_pass and is_retro and is_autofocus_on: + try: + binding = n.notification.visual.bindings[0] + texts = [t.text for t in binding.get_text_elements()] + is_dofus_notif = False + for ligne in texts: + if " - Dofus Retro" in ligne: + is_dofus_notif = True + pseudo = ligne.split(" - ")[0].strip() + cycle_list = self.logic.get_cycle_list() + for index, acc in enumerate(cycle_list): + if acc['name'] == pseudo: + self.gui.root.after(0, self.logic.focus_window, acc['hwnd']) + self.current_idx = index + break + break + if is_dofus_notif: + asyncio.create_task(remove_notif_delayed(n.id)) + except Exception: + pass + + seen_ids.intersection_update(current_ids) + first_pass = False + except Exception: + pass + await asyncio.sleep(0.5) except Exception: pass + await asyncio.sleep(3) - # Une seule boucle while True = stabilité maximale + # ========================================== + # MOTEUR V3 : POLLING RAPIDE SANS NETTOYAGE + # ========================================== + async def _poll_notifications_v3(self): while True: try: - is_retro = self.config.data.get("game_version", "Unity") == "Rétro" - is_autofocus_on = self.config.data.get("auto_focus_retro", False) - - notifs = await listener.get_notifications_async(NotificationKinds.TOAST) - current_ids = set() - - for n in notifs: - current_ids.add(n.id) - - if n.id not in seen_ids: - seen_ids.add(n.id) - - try: - binding = n.notification.visual.bindings[0] - texts = [t.text for t in binding.get_text_elements()] - - is_dofus_notif = False - - for ligne in texts: - if " - Dofus Retro" in ligne: - is_dofus_notif = True - - # Auto-focus si ce n'est pas le 1er scan et que l'option est active - if not first_pass and is_retro and is_autofocus_on: - pseudo = ligne.split(" - ")[0].strip() - cycle_list = self.logic.get_cycle_list() - for index, acc in enumerate(cycle_list): - if acc['name'] == pseudo: - self.gui.root.after(0, self.logic.focus_window, acc['hwnd']) - self.current_idx = index + listener = UserNotificationListener.current + access = await listener.request_access_async() + if access != 1: + await asyncio.sleep(5) + continue + + seen_ids = set() + first_pass = True + + while True: + is_retro = self.config.data.get("game_version", "Unity") == "Rétro" + is_autofocus_on = self.config.data.get("auto_focus_retro", False) + try: + notifs = await listener.get_notifications_async(NotificationKinds.TOAST) + current_ids = {n.id for n in notifs} + + for n in notifs: + if n.id not in seen_ids: + seen_ids.add(n.id) + if not first_pass and is_retro and is_autofocus_on: + try: + binding = n.notification.visual.bindings[0] + for t in binding.get_text_elements(): + if " - Dofus Retro" in t.text: + pseudo = t.text.split(" - ")[0].strip() + for index, acc in enumerate(self.logic.get_cycle_list()): + if acc['name'] == pseudo: + self.gui.root.after(0, self.logic.focus_window, acc['hwnd']) + self.current_idx = index + break break - break - - # Nettoyage automatique : on clear SEULEMENT les notifs Dofus Rétro - # (Même au first_pass, ça vide l'historique Windows pour éviter le crash) - if is_dofus_notif: - asyncio.create_task(remove_notif_delayed(n.id)) - - except Exception: pass - - seen_ids.intersection_update(current_ids) - first_pass = False - - except Exception: - # Si Windows sature un quart de seconde, on ignore et on continue + except Exception: + pass + + seen_ids.intersection_update(current_ids) + except Exception: + pass + + first_pass = False + await asyncio.sleep(0.3) + except Exception: pass - - # Focus ultra-réactif (0.5s) - await asyncio.sleep(0.5) + await asyncio.sleep(3) -# --- SYSTÈME DE VÉRIFICATION DE VERSION --- -<<<<<<< fix/crash-notifrétro -CURRENT_VERSION = "1.2.1" -======= ->>>>>>> dev + +CURRENT_VERSION = "1.2.2" VERSION_URL = "https://raw.githubusercontent.com/LuframeCode/Dosoft/main/version.json" def check_version(i18n=None): @@ -570,11 +675,8 @@ def handle_multiple_instances(): root.attributes("-topmost", True) rep = messagebox.askyesno(i18n.t("header_instace_off", "Instance détectée"),i18n.t("popup_conflict_instance_text","Une instance de DOSOFT est déjà en cours d'exécution !\n\nVoulez-vous fermer l'ancienne instance pour ouvrir celle-ci ?")) if rep: -<<<<<<< fix/crash-notifrétro - hwnd = win32gui.FindWindow(None, "DOSOFT v1.2.1") -======= + hwnd = win32gui.FindWindow(None, APP_TITLE) ->>>>>>> dev if hwnd: _, pid = win32process.GetWindowThreadProcessId(hwnd) try: diff --git a/requirements.txt b/requirements.txt index 377df26..2be0143 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pystray pygame-ce Pillow requests +winsdk \ No newline at end of file diff --git a/setup.iss b/setup.iss index ef0c06c..8075b2f 100644 --- a/setup.iss +++ b/setup.iss @@ -1,13 +1,13 @@ ; ============================================================ ; DOSOFT - Script Inno Setup -; Prerequis : Inno Setup 6+ (https://jrsoftware.org/isinfo.php) -; A lancer APRES la compilation Nuitka (build_dosoft.cmd) +; Prerequis : Inno Setup 6+ ; ============================================================ #define AppName "Dosoft" #define AppPublisher "Dosoft" #define AppExeName "Dosoft.exe" #define SourceDir "dist" +#define SourceDir "dist" #define VersionFile "version.json" #define VersionRaw FileRead(VersionFile) #define VersionKey "\"version\": \"" @@ -55,50 +55,49 @@ UninstallDisplayIcon={app}\{#AppExeName} UninstallDisplayName={#AppName} VersionInfoVersion={#AppVersion} VersionInfoCompany={#AppPublisher} -VersionInfoDescription=Gestionnaire multi-compte Dofus - Dosoft +VersionInfoDescription=Gestionnaire multi-compte Dofus - {#AppName} [Languages] Name: "french"; MessagesFile: "compiler:Languages\French.isl" [Tasks] -Name: "desktopicon"; Description: "Créer un raccourci sur le Bureau"; GroupDescription: "Raccourcis :" +Name: "desktopicon"; Description: "Créer un raccourci sur le Bureau"; GroupDescription: "Raccourcis :" Name: "startmenuicon"; Description: "Créer un raccourci dans le Menu Démarrer"; GroupDescription: "Raccourcis :" [Files] -; --- Executable principal (compilé par Nuitka/PyInstaller) --- +; --- Executable principal --- Source: "{#SourceDir}\{#AppExeName}"; DestDir: "{app}"; Flags: ignoreversion ; --- Ressources --- Source: "logo.ico"; DestDir: "{app}"; Flags: ignoreversion -Source: "skin\*"; DestDir: "{app}\skin"; Flags: ignoreversion recursesubdirs createallsubdirs -Source: "sounds\*"; DestDir: "{app}\sounds"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "skin\*"; DestDir: "{app}\skin"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "sounds\*"; DestDir: "{app}\sounds"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] -; --- Raccourci Bureau --- +; --- Raccourcis --- Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; IconFilename: "{app}\logo.ico"; Tasks: desktopicon +Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; IconFilename: "{app}\logo.ico"; Tasks: startmenuicon -; --- Raccourci Menu Démarrer --- -Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; IconFilename: "{app}\logo.ico"; Tasks: startmenuicon -Name: "{group}\Désinstaller {#AppName}"; Filename: "{uninstallexe}" +; CORRECTION ICI : On ajoute "Tasks: startmenuicon" à la fin de la ligne +Name: "{group}\Désinstaller {#AppName}"; Filename: "{uninstallexe}"; Tasks: startmenuicon [Run] ; --- Lancement après installation --- Filename: "{app}\{#AppExeName}"; Description: "Lancer {#AppName} maintenant"; Flags: nowait postinstall skipifsilent runascurrentuser -; [UninstallDelete] -; Type: files; Name: "{app}\settings.json" -; Type: filesandordirs; Name: "{app}" - [Code] function GetUninstallString(): String; var sUnInstPath: String; sUnInstallString: String; begin - sUnInstPath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\{#SetupSetting("AppId")}_is1'); + // Chemin de désinstallation écrit en dur pour éviter les bugs d'accolades avec ExpandConstant + sUnInstPath := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}_is1'; sUnInstallString := ''; + if not RegQueryStringValue(HKLM, sUnInstPath, 'UninstallString', sUnInstallString) then RegQueryStringValue(HKCU, sUnInstPath, 'UninstallString', sUnInstallString); + Result := sUnInstallString; end; @@ -135,22 +134,23 @@ begin BackupPath := ExpandConstant('{tmp}\settings_backup.json'); if (CurStep = ssInstall) then begin - // 1. On sauvegarde le fichier existant AVANT de désinstaller l'ancienne version + // 1. Sauvegarde du settings.json de l'ancienne version if FileExists(SettingsPath) then begin FileCopy(SettingsPath, BackupPath, False); end; + // Désinstallation silencieuse if (IsUpgrade()) then UnInstallOldVersion(); end; if (CurStep = ssPostInstall) then begin - // 2. On restaure le fichier dans le dossier tout neuf + // 2. Restauration du settings.json if FileExists(BackupPath) then begin FileCopy(BackupPath, SettingsPath, False); end; - // Après installation : exclusion Windows Defender pour éviter faux positifs + // Exclusion Windows Defender ExePath := ExpandConstant('{app}\{#AppExeName}'); Exec('powershell.exe', '-ExecutionPolicy Bypass -Command "Add-MpPreference -ExclusionPath ''' + ExePath + '''"', diff --git a/version.json b/version.json index b2dc7bd..cd8e271 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "version": "1.2.1", - "message": "Veuillez mettre à jour le depuis le DISCORD." +"version": "1.2.2", +"message": "Veuillez mettre à jour le depuis le DISCORD." }