Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
setup_dosoft.iss
settings.json
build_dosoft.cmd
__pychache__
build
dist
main.spec
Dosoft.spec
installer_output
Binary file added __pycache__/app_version.cpython-313.pyc
Binary file not shown.
Binary file modified __pycache__/config_manager.cpython-313.pyc
Binary file not shown.
Binary file modified __pycache__/gui.cpython-313.pyc
Binary file not shown.
Binary file modified __pycache__/i18n_manager.cpython-313.pyc
Binary file not shown.
Binary file modified __pycache__/keyboard_layout_manager.cpython-313.pyc
Binary file not shown.
Binary file modified __pycache__/logic.cpython-313.pyc
Binary file not shown.
Binary file added __pycache__/main.cpython-313.pyc
Binary file not shown.
Binary file modified __pycache__/radial_menu.cpython-313.pyc
Binary file not shown.
2 changes: 1 addition & 1 deletion app_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
41 changes: 40 additions & 1 deletion gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -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"))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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("")

Expand Down
258 changes: 180 additions & 78 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pystray
pygame-ce
Pillow
requests
winsdk
Loading