diff --git a/app/core/alert_manager.py b/app/core/alert_manager.py index 81b65b3..bcd7aad 100644 --- a/app/core/alert_manager.py +++ b/app/core/alert_manager.py @@ -1,4 +1,5 @@ from PyQt6.QtWidgets import QMessageBox +from typing import Optional class AlertManager: @@ -20,6 +21,10 @@ class AlertManager: QMessageBox.critical(parent, error_title, error_text) def show_choice(self, message: str, parent=None) -> bool: + """ + Affiche une boîte de dialogue Oui/Non. + Si detailed_text est fourni, l'ajoute comme texte détaillé (affichable par l'utilisateur). + """ box = QMessageBox(parent) box.setWindowTitle(self.language_manager.get_text("confirmation")) box.setText(message) diff --git a/app/core/update_manager.py b/app/core/update_manager.py index f76d6a2..8778738 100644 --- a/app/core/update_manager.py +++ b/app/core/update_manager.py @@ -9,6 +9,7 @@ from app.ui.widgets.loading_bar import LoadingBar import os import sys import subprocess +from typing import List, Dict class UpdateManager: def __init__(self, settings_manager: SettingsManager, language_manager: LanguageManager, alert_manager: AlertManager) -> None: @@ -20,35 +21,51 @@ class UpdateManager: self.app_os = self.settings_manager.get_config("app_os") self.arch = self.settings_manager.get_config("architecture") - def get_latest_release_with_asset(self) -> dict: - # Récupère la release la plus récente qui contient le bon fichier + def get_releases_with_asset(self) -> List[Dict]: + """ + Retourne la liste des releases (dict) qui contiennent un asset correspondant + à l'OS/architecture attendus. Chaque dict contient: tag_name, download_url, body. + """ + releases_list: List[Dict] = [] try: if "gitea" in self.repo_url: - owner_repo = self.repo_url.split("/")[-2:] - api_url = self.repo_url.replace(owner_repo[0], "api/v1/repos/" + owner_repo[0]) + "/releases" + # Gitea: construire URL API (essai basique) + owner_repo = self.repo_url.rstrip("/").split("/")[-2:] + api_base = self.repo_url.replace("/" + owner_repo[0] + "/" + owner_repo[1], "/api/v1/repos/" + owner_repo[0] + "/" + owner_repo[1]) + api_url = api_base + "/releases" resp = requests.get(api_url) + resp.raise_for_status() releases = resp.json() else: - owner_repo = self.repo_url.split("/")[-2:] + owner_repo = self.repo_url.rstrip("/").split("/")[-2:] api_url = f"https://api.github.com/repos/{owner_repo[0]}/{owner_repo[1]}/releases" resp = requests.get(api_url) + resp.raise_for_status() releases = resp.json() - # Cherche le bon asset dans chaque release - expected_filename = f"{self.app_name}-{self.app_os}-{self.arch}" + + expected_filename_frag = f"{self.app_name}-{self.app_os}-{self.arch}" for release in releases: + tag = release.get("tag_name") or release.get("name") + body = release.get("body", "") or "" for asset in release.get("assets", []): - if expected_filename in asset.get("name", ""): - return { - "tag_name": release.get("tag_name"), - "download_url": asset.get("browser_download_url") - } + name = asset.get("name", "") + if expected_filename_frag in name: + downloads = asset.get("browser_download_url") or asset.get("url") + releases_list.append({ + "tag_name": tag, + "download_url": downloads, + "body": body + }) + break except Exception: - pass - return None + # En cas d'erreur, retourner liste vide (on ne lève pas pour ne pas bloquer l'app) + return [] + return releases_list def check_for_update(self, parent=None) -> bool: current_version = self.settings_manager.get_config("app_version") - release = self.get_latest_release_with_asset() + releases = self.get_releases_with_asset() + release = releases[0] if releases else None if release and version.parse(release["tag_name"]) > version.parse(current_version): choice = self.alert_manager.show_choice( self.language_manager.get_text("update_found").replace("{latest_tag}", release["tag_name"]), diff --git a/app/ui/main_window.py b/app/ui/main_window.py index 6aae7b4..077b1e6 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -1,11 +1,11 @@ -from PyQt6.QtWidgets import QApplication, QMainWindow -from PyQt6.QtGui import QIcon, QResizeEvent, QCloseEvent -from PyQt6.QtCore import QSize, QEvent +from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QLabel, QFrame +from PyQt6.QtGui import QResizeEvent, QCloseEvent +from PyQt6.QtCore import QSize, QEvent, Qt from app.core.main_manager import MainManager, NotificationType from app.ui.widgets.tabs_widget import TabsWidget, MenuDirection, ButtonPosition, BorderSide from app.ui.windows.settings_window import SettingsWindow from app.ui.windows.suggestion_window import SuggestionWindow -import app.utils.paths as paths +import app.utils.paths as paths, shutil from typing import Optional class MainWindow(QMainWindow): @@ -28,6 +28,7 @@ class MainWindow(QMainWindow): self.side_menu: TabsWidget self.settings_window: SettingsWindow self.suggestion_window: SuggestionWindow + self.footer_label: QLabel # Ajout d'un attribut pour le footer app: Optional[QApplication] = QApplication.instance() size: QSize = app.primaryScreen().size() @@ -77,16 +78,35 @@ class MainWindow(QMainWindow): self.current_size.height() ) + # Nettoyage des icônes temporaires générées (supprime tout le dossier temp_icons à la fermeture) + try: + shutil.rmtree(paths.get_user_temp(self.settings_manager.get_config("app_name"))) + except Exception: + pass + def setup_ui(self) -> None: - self.side_menu = TabsWidget(self,MenuDirection.HORIZONTAL,70,None,10,1,BorderSide.TOP) + self.side_menu = TabsWidget(self, MenuDirection.HORIZONTAL, 70, None, 10, 1, BorderSide.TOP) self.settings_window = SettingsWindow(self) - self.side_menu.add_widget(self.settings_window,"",paths.get_asset_svg_path("settings"), position=ButtonPosition.CENTER) + self.side_menu.add_widget(self.settings_window, "", paths.get_asset_svg_path("settings"), position=ButtonPosition.CENTER) self.suggestion_window = SuggestionWindow(self) - self.side_menu.add_widget(self.suggestion_window,"",paths.get_asset_svg_path("suggestion"), position=ButtonPosition.CENTER) + self.side_menu.add_widget(self.suggestion_window, "", paths.get_asset_svg_path("suggestion"), position=ButtonPosition.CENTER) - self.setCentralWidget(self.side_menu) + # Ajout du footer + central_widget = QFrame(self) + layout = QVBoxLayout(central_widget) + layout.addWidget(self.side_menu) + + self.footer_label = QLabel(self.language_manager.get_text("footer_text"), self) + self.footer_label.setAlignment(Qt.AlignmentFlag.AlignRight) + self.footer_label.setStyleSheet("padding: 5px; font-size: 12px; color: gray;") + layout.addWidget(self.footer_label) + + self.setCentralWidget(central_widget) def update_theme(self) -> None: - self.setStyleSheet(self.theme_manager.get_sheet()) \ No newline at end of file + self.setStyleSheet(self.theme_manager.get_sheet()) + + def update_language(self) -> None: + self.footer_label.setText(self.language_manager.get_text("footer_text")) \ No newline at end of file diff --git a/app/ui/widgets/tabs_widget.py b/app/ui/widgets/tabs_widget.py index c2fb5b5..ccc0da1 100644 --- a/app/ui/widgets/tabs_widget.py +++ b/app/ui/widgets/tabs_widget.py @@ -2,6 +2,9 @@ from PyQt6.QtWidgets import QLayout, QWidget, QHBoxLayout, QVBoxLayout, QPushBut from PyQt6.QtGui import QIcon from PyQt6.QtCore import QSize from enum import Enum +from pathlib import Path +import hashlib +import app.utils.paths as paths from app.core.main_manager import MainManager, NotificationType import xml.etree.ElementTree as ET @@ -38,7 +41,7 @@ class TabsWidget(QWidget): self.widgets = [] self.button_positions = [] self.button_size_ratios = [] # Individual ratios for each button - + self._icon_cache = {} # Track alignment zones self.start_buttons = [] self.center_buttons = [] @@ -448,25 +451,81 @@ class TabsWidget(QWidget): button.style().polish(button) def apply_color_to_svg_icon(self, icon_path, color) -> QIcon: + """ + Create or reuse a colored copy of the SVG in the user's data folder (temp_icons) + and return a QIcon that points to it. Original SVG is preserved. - # Define namespace mapping (based on your SVG) - ns = {'ns0': 'http://www.w3.org/2000/svg'} + Caching: deterministic filename based on sha256(icon_path + color) so repeated calls reuse files. + """ + try: + if not icon_path: + return QIcon() - # Parse the SVG file - tree = ET.parse(icon_path) - root = tree.getroot() + # deterministic filename based on hash to avoid duplicates + hasher = hashlib.sha256() + hasher.update(str(Path(icon_path).resolve()).encode('utf-8')) + hasher.update(str(color).encode('utf-8')) + digest = hasher.hexdigest() + new_name = f"{Path(icon_path).stem}_{digest}.svg" - # Change the fill attribute on the root element - root.attrib['fill'] = color - root.attrib['stroke'] = color - # Optionally, change fill for all elements as well - for path in root.findall('.//ns0:path', ns): - path.attrib['fill'] = color - path.attrib['stroke'] = color + app_name = self.main_manager.get_settings_manager().get_config("app_name") + temp_dir = Path(paths.get_user_temp_icons(app_name)) + temp_dir.mkdir(parents=True, exist_ok=True) - # Save the modified SVG - tree.write(icon_path) - return QIcon(icon_path) + tmp_path = temp_dir / new_name + + # If exists in cache and file exists on disk, return immediately + cache_key = (str(Path(icon_path).resolve()), color) + if cache_key in self._icon_cache: + cached_path = Path(self._icon_cache[cache_key]) + if cached_path.exists(): + return QIcon(str(cached_path)) + else: + # stale cache entry -> remove + del self._icon_cache[cache_key] + + # If file already exists (from previous run), reuse it + if tmp_path.exists(): + self._icon_cache[cache_key] = str(tmp_path) + return QIcon(str(tmp_path)) + + # Parse original SVG + tree = ET.parse(icon_path) + root = tree.getroot() + + # Apply fill/stroke to root and to all relevant elements. + for el in root.iter(): + tag = el.tag + if not isinstance(tag, str): + continue + lname = tag.split('}')[-1] # local name + if lname in ("svg", "g", "path", "rect", "circle", "ellipse", "polygon", "line", "polyline"): + if 'fill' in el.attrib: + el.attrib['fill'] = color + if 'stroke' in el.attrib: + el.attrib['stroke'] = color + + style = el.attrib.get('style') + if style: + parts = [p.strip() for p in style.split(';') if p.strip()] + parts = [p for p in parts if not p.startswith('fill:') and not p.startswith('stroke:')] + parts.append(f'fill:{color}') + parts.append(f'stroke:{color}') + el.attrib['style'] = ';'.join(parts) + + # Write to deterministic file + tree.write(tmp_path, encoding='utf-8', xml_declaration=True) + + # Update cache + self._icon_cache[cache_key] = str(tmp_path) + + return QIcon(str(tmp_path)) + except Exception: + # On any error, fallback to original icon (do not crash) + try: + return QIcon(icon_path) + except Exception: + return QIcon() def update_buttons_size(self, size): """Update all buttons to a specific pixel size (overrides ratio-based sizing)""" diff --git a/app/utils/paths.py b/app/utils/paths.py index aba4dbf..991026b 100644 --- a/app/utils/paths.py +++ b/app/utils/paths.py @@ -47,3 +47,11 @@ def get_user_data_dir(app_name: str) -> str: if not path.exists(user_data_path): mkdir(user_data_path) return user_data_path + +def get_user_temp_icons(app_name: str) -> str: + user_data_path = get_user_data_dir(app_name) + return path.join(user_data_path, "temp", "icons") + +def get_user_temp(app_name: str) -> str: + user_data_path = get_user_data_dir(app_name) + return path.join(user_data_path, "temp") \ No newline at end of file diff --git a/data/assets/settings.svg b/data/assets/settings.svg index c67bfb2..b9b2280 100644 --- a/data/assets/settings.svg +++ b/data/assets/settings.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/data/assets/suggestion.svg b/data/assets/suggestion.svg index f383c8a..3db29ec 100644 --- a/data/assets/suggestion.svg +++ b/data/assets/suggestion.svg @@ -1,8 +1,8 @@ - + - + diff --git a/data/lang/en.json b/data/lang/en.json index 2dad60b..f1d7e77 100644 --- a/data/lang/en.json +++ b/data/lang/en.json @@ -23,5 +23,6 @@ "update_downloaded": "Update downloaded to {local_path}", "update_download_error": "Error downloading update", "downloading_update": "Downloading update...", - "update": "Update" + "update": "Update", + "footer_text": "© 2025 Louis Mazin. All rights reserved." } \ No newline at end of file diff --git a/data/lang/fr.json b/data/lang/fr.json index f8dcd5c..971ad07 100644 --- a/data/lang/fr.json +++ b/data/lang/fr.json @@ -23,5 +23,6 @@ "update_downloaded": "Mise à jour téléchargée dans {local_path}", "update_download_error": "Erreur lors du téléchargement de la mise à jour", "downloading_update": "Téléchargement de la mise à jour...", - "update": "Mise à jour" + "update": "Mise à jour", + "footer_text": "© 2025 Louis Mazin. Tous droits réservés." } \ No newline at end of file