improvments

This commit is contained in:
Louis Mazin 2025-12-03 22:09:53 +01:00
parent deb80bc9b2
commit 69891c9d6d
13 changed files with 507 additions and 458 deletions

View File

@ -1,13 +0,0 @@
# Python Configuration
PYTHON_PATH=C:/Path/To/Your/Python/python.exe
# Email configuration for suggestion system
EMAIL_ADDRESS=your_email@gmail.com
EMAIL_PASSWORD=your_app_password
EMAIL_SMTP_SERVER=smtp.gmail.com
EMAIL_SMTP_PORT=587
# Licensing configuration
LICENSE_API_URL=https://your-server.com/api/licenses
PURCHASE_URL=https://your-website.com/buy
LICENSE_API_KEY=your_license_api_key

View File

@ -11,14 +11,11 @@ with config_path.open("r", encoding="utf-8") as f:
config = json.load(f)
# --- Extract values ---
version = config.get("app_version", "0.0.0")
arch = config.get("architecture", "x64")
python_version = config.get("python_version", "3.x")
os_name = config.get("app_os", sys.platform)
app_name = config.get("app_name", "Application")
# --- Construct dynamic name ---
name = f"{app_name}-{os_name}-{arch}-v{version}"
name = f"{app_name}"
# --- Optional icon path ---
icon = getenv("ICON_PATH", "")

12
LICENSE
View File

@ -1,12 +0,0 @@
Attribution License
Copyright (c) 2025 LouisMazin
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
**Attribution Requirement:**
Any use, reproduction, or distribution of this Software, or derivative works thereof, must include a clear and visible attribution to the original author: LouisMazin.
The above copyright notice, this permission notice, and the attribution requirement shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

162
README.md
View File

@ -1,162 +0,0 @@
# 🚀 Python PyQt6 Application Template
Bienvenue ! Ce projet est **bien plus qu'un simple template** : c'est une boîte à outils moderne pour créer rapidement des applications desktop Python élégantes, robustes et évolutives.
Vous voulez coder, personnaliser, traduire, mettre à jour, distribuer ? Tout est déjà prêt !
---
## ✨ Fonctionnalités clés
- **Interface moderne (PyQt6)** : Responsive, stylée, et facile à personnaliser.
- **Thèmes dynamiques** : Passez du clair au sombre en un clic, ou créez le vôtre !
- **Multi-langues** : Français, anglais... et ajoutez-en autant que vous voulez.
- **Paramètres utilisateurs** : Tout est sauvegardé (thème, langue, taille de fenêtre...).
- **Architecture modulaire** : Des managers pour chaque besoin, tout est organisé.
- **Notifications automatiques** : Les widgets s'adaptent instantanément aux changements.
- **Barre donglets flexible** : Ajoutez vos fenêtres où vous voulez, comme vous voulez.
- **Système de suggestion** : Vos utilisateurs peuvent vous écrire directement depuis lapp.
- **Mise à jour automatique** : Téléchargez la dernière version sans effort, avec barre de progression !
- **Build & environnement** : Scripts pour tout automatiser, sur Windows, Linux, macOS.
---
## 🗂️ Structure du projet
```
Template/
├── app/
│ ├── core/ # Managers (thème, langue, update, etc.)
│ ├── ui/
│ │ ├── widgets/ # Onglets, loading bar, etc.
│ │ └── windows/ # Paramètres, suggestion...
│ └── utils/ # Fonctions utilitaires
├── data/
│ ├── assets/ # Icônes, images
│ ├── lang/ # Traductions
│ ├── themes/ # Thèmes
│ └── others/ # Autres
├── tools/ # Scripts build/dev
├── config.json # Config principale
├── requirements.txt # Dépendances
├── BUILD.spec # PyInstaller
└── main.py # Point dentrée
```
---
## ⚡ Démarrage Express
1. **Configurez `config.json`**
(nom, version, OS, architecture, icône, dépôt git...)
2. **Copiez `.env.example` → `.env` et configurez les variables requises**
- Depuis la racine du projet :
- Windows (PowerShell / cmd) : copy .env.example .env
- Linux / macOS : cp .env.example .env
- Au minimum, renseignez dans `.env` :
- PYTHON_PATH : chemin absolu vers votre exécutable Python (utilisé par tools/open.bat)
- les identifiants email si vous comptez utiliser l'envoi de suggestions (email + mot de passe / mot de passe d'application)
- Remarque : l'outil `tools/open.bat` s'appuie sur PYTHON_PATH ; sans cette variable, l'ouverture/initialisation de l'environnement échouera.
3. **Lancez le dev**
- Windows : tools\open.bat (nécessite PYTHON_PATH dans `.env`)
- Linux : tools/open.sh (si présent / exécutable)
- macOS : tools/open.command (si présent / exécutable)
- Exécutez depuis leur fichier parent pour que les chemins relatifs fonctionnent correctement.
4. **Build en un clic**
- Windows : tools\build.bat
- Linux : tools/build.sh
- macOS : tools/build.command
- Ces scripts supposent que `.env` est configuré et que les outils requis (pyinstaller, etc.) sont installés.
---
## 🎨 Thèmes & 🌍 Langues
- **Thèmes** : Ajoutez vos fichiers dans `data/themes/` (JSON).
Changez les couleurs, créez votre ambiance !
- **Langues** : Ajoutez vos fichiers dans `data/lang/` (JSON).
Traduisez tout, cest instantané.
---
## 🧩 Managers & Architecture
- **MainManager** : Le chef dorchestre.
- **SettingsManager** : Les préférences utilisateur.
- **ThemeManager** : Les couleurs et le style.
- **LanguageManager** : Les textes traduits.
- **AlertManager** : Les messages, confirmations, erreurs.
- **UpdateManager** : Les mises à jour automatiques.
- **ObserverManager** : Les notifications internes.
---
## 🔄 Mise à jour automatique
- Vérifie la dernière version sur le dépôt Git à chaque démarrage.
- Propose la mise à jour si disponible.
- Télécharge le bon fichier selon votre OS/architecture.
- Affiche une barre de progression stylée.
- Lance la nouvelle version automatiquement !
---
## 💡 Suggestions & Feedback
- Fenêtre dédiée pour envoyer vos idées ou questions par email.
- Sécurisé via `.env`.
- Gestion des erreurs/succès avec AlertManager.
---
## 🛠️ Ajouter vos fenêtres & widgets
- Créez une classe héritant de `QWidget`.
- Ajoutez-la dans la barre donglets (`TabsWidget`).
- Abonnez-vous aux notifications pour la langue/le thème.
---
## 📦 Dépendances
- **Python 3.10+ recommandé** (compatibilité testée avec 3.10/3.11).
- **PyQt6** : GUI moderne.
- **pyinstaller** : Build dexécutables.
- **python-dotenv** : Variables denvironnement.
- **requests** : Requêtes HTTP (update).
---
## 🔒 Sécurité
- **Ne versionnez jamais `.env`** (déjà dans `.gitignore`).
- Utilisez un mot de passe dapplication pour Gmail.
---
## 📝 Licence
Attribution License — voir le fichier local [`LICENSE`](https://gitea.louismazin.ovh/LouisMazin/PythonApplicationTemplate/src/branch/main/LICENSE) pour le texte complet.
Toute utilisation ou distribution doit inclure une attribution visible à l'auteur : LouisMazin.
---
## 🤝 Contribution
1. Forkez le dépôt
2. Créez une branche
3. Proposez vos modifications via pull request
---
## 🆘 Support
- Ouvrez une issue sur le dépôt
- Précisez votre OS, version Python, logs derreur
---
**Ce template est fait pour vous faire gagner du temps et coder avec plaisir !
Testez-le, améliorez-le, partagez-le 🚀**

View File

@ -40,6 +40,7 @@ class ThemeManager:
color: {self.current_theme.get_color("text_color")};
}}
QLabel {{
background-color: transparent;
color: {self.current_theme.get_color("text_color")};
font-size: 20px;
}}
@ -70,7 +71,7 @@ class ThemeManager:
border-radius: 8px;
padding: 5px;
font-size: 14px;
background-color: {self.current_theme.get_color("background_secondary_color")};
background-color: {self.current_theme.get_color("background_tertiary_color")};
color: {self.current_theme.get_color("text_color")};
}}
QLineEdit {{
@ -78,9 +79,24 @@ class ThemeManager:
border-radius: 8px;
padding: 5px;
font-size: 14px;
background-color: {self.current_theme.get_color("background_secondary_color")};
background-color: {self.current_theme.get_color("background_tertiary_color")};
color: {self.current_theme.get_color("text_color")};
}}
QDateEdit {{
border: 2px solid {self.current_theme.get_color("border_color")};
padding: 5px;
border-radius: 8px;
font-size: 14px;
min-height: 30px;
}}
QDateEdit::drop-down {{
border: none;
background: transparent;
}}
QDateEdit:hover {{
border-color: {self.current_theme.get_color("primary_hover_color")};
}}
QComboBox {{
border: 2px solid {self.current_theme.get_color("border_color")};
padding: 5px;
@ -117,6 +133,42 @@ class ThemeManager:
QComboBox:hover {{
border-color: {self.current_theme.get_color("primary_hover_color")};
}}
#table_combobox {{
border: 1px solid {self.current_theme.get_color("border_color")};
padding: 2px 5px;
border-radius: 4px;
font-size: 12px;
min-height: 20px;
max-height: 28px;
}}
#table_combobox::drop-down {{
border: none;
background: transparent;
}}
#table_combobox::down-arrow {{
image: none;
}}
#table_combobox QAbstractItemView {{
border-radius: 4px;
padding: 0px;
outline: none;
}}
#table_combobox QAbstractItemView::item {{
padding: 8px 10px;
margin: 0px;
min-height: 16px;
border: 1px solid {self.current_theme.get_color("border_color")};
border-radius: 4px;
}}
#table_combobox QAbstractItemView::item:hover {{
background-color: {self.current_theme.get_color("background_tertiary_color")};
color: {self.current_theme.get_color("text_color")};
}}
#table_combobox QAbstractItemView::item:selected {{
color: {self.current_theme.get_color("text_color")};
}}
QSlider::groove:horizontal {{
border: 1px solid {self.current_theme.get_color("primary_color")};
height: 10px;

View File

@ -2,7 +2,7 @@ from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel
from PyQt6.QtGui import QResizeEvent, QCloseEvent
from PyQt6.QtCore import QSize, QEvent
from app.core.main_manager import MainManager, NotificationType
from app.ui.widgets.tabs_widget import TabsWidget, MenuDirection, ButtonPosition, BorderSide, TabSide
from app.ui.widgets.tabs_widget import TabsWidget, MenuDirection, ButtonPosition, BorderSide, TabSide, TextPosition
from app.ui.windows.settings_window import SettingsWindow
from app.ui.windows.suggestion_window import SuggestionWindow
from app.ui.windows.activation_window import ActivationWindow
@ -20,6 +20,7 @@ class MainWindow(QMainWindow):
self.settings_manager = self.main_manager.get_settings_manager()
self.observer_manager = self.main_manager.get_observer_manager()
self.observer_manager.subscribe(NotificationType.THEME, self.update_theme)
self.observer_manager.subscribe(NotificationType.LANGUAGE, self.update_language)
self.is_maximizing: bool = False
# Initialiser les attributs de taille AVANT setup_ui
@ -33,11 +34,13 @@ class MainWindow(QMainWindow):
self.previous_size: QSize = QSize(window_size["width"], window_size["height"])
# Configuration des tailles de police de référence
self.base_width = 600 # Largeur de référence
self.base_height = 450 # Hauteur de référence
self.base_width = 1200 # Largeur de référence (taille par défaut)
self.base_height = 700 # Hauteur de référence (taille par défaut)
self.base_tab_height = 70 # Hauteur de base du tab menu
# Cache pour stocker les font-sizes de base de chaque widget
self._base_font_sizes = {}
self._font_sizes_extracted = False # Flag pour savoir si on a déjà extrait les tailles
# UI elements
self.side_menu: TabsWidget
@ -88,11 +91,11 @@ class MainWindow(QMainWindow):
self.previous_size = self.current_size
self.current_size = self.size()
# Ajuster dynamiquement les font-sizes
# Ajuster dynamiquement les font-sizes avec un ratio
self.adjust_all_font_sizes()
def adjust_all_font_sizes(self):
"""Ajuste dynamiquement les font-sizes de tous les labels dans toutes les tabs"""
"""Ajuste dynamiquement les font-sizes de tous les éléments avec un ratio proportionnel"""
# Calculer le ratio basé sur la largeur ET la hauteur actuelle
current_width = self.width()
current_height = self.height()
@ -101,78 +104,131 @@ class MainWindow(QMainWindow):
width_ratio = current_width / self.base_width
height_ratio = current_height / self.base_height
# Prendre le ratio le plus petit pour éviter que le texte dépasse
ratio = min(width_ratio, height_ratio)
# Utiliser la moyenne des deux ratios pour un scaling plus naturel
# Ou utiliser le minimum pour éviter le débordement
ratio = min(width_ratio,height_ratio) * 1.5
# Limiter le ratio pour éviter des tailles extrêmes
ratio = max(0.5, min(ratio, 2.0)) # Entre 50% et 200%
# Récupérer tous les widgets des tabs
all_widgets = []
if hasattr(self, 'side_menu'):
all_widgets = self.side_menu.widgets
# Parcourir tous les widgets et ajuster leurs labels
# Extraire les tailles de base une seule fois
if not self._font_sizes_extracted:
self._extract_base_font_sizes(all_widgets)
self._font_sizes_extracted = True
# Parcourir tous les widgets et ajuster leurs tailles
for widget in all_widgets:
if widget:
self._adjust_widget_labels(widget, ratio)
self._adjust_widget_font_sizes(widget, ratio)
def _adjust_widget_labels(self, widget, ratio):
"""Ajuste récursivement tous les QLabel, QPushButton, QLineEdit, QTextEdit et QComboBox d'un widget"""
def _extract_base_font_sizes(self, widgets):
"""Extrait les tailles de police de base de tous les widgets une seule fois"""
from PyQt6.QtWidgets import QPushButton, QLineEdit, QTextEdit, QComboBox
# Types de widgets à ajuster
widget_types = [QLabel, QPushButton, QLineEdit, QTextEdit, QComboBox]
font_size_dict = self.extract_base_font_size()
# Extraire les tailles des boutons d'onglets du side menu
if hasattr(self, 'side_menu') and hasattr(self.side_menu, 'buttons'):
for button in self.side_menu.buttons:
if button:
widget_id = id(button)
current_style = button.styleSheet()
base_size = self._extract_font_size_from_style(current_style)
if base_size is None:
base_size = 14 # Taille par défaut
self._base_font_sizes[widget_id] = base_size
for widget in widgets:
if not widget:
continue
for widget_type in widget_types:
for child in widget.findChildren(widget_type):
widget_id = id(child)
# Ignorer les widgets avec un objectName (généralement stylisés spécifiquement)
if child.objectName() != "":
continue
# Extraire la taille de police depuis le stylesheet
current_style = child.styleSheet()
base_size = self._extract_font_size_from_style(current_style)
# Si pas trouvé dans le style, utiliser la taille par défaut
if base_size is None:
base_size = 14 # Taille par défaut
# Stocker la taille de base
self._base_font_sizes[widget_id] = base_size
def _extract_font_size_from_style(self, style: str) -> Optional[int]:
"""Extrait la taille de police depuis un stylesheet"""
import re
# Chercher "font-size: XXpx"
match = re.search(r'font-size:\s*(\d+)px', style)
if match:
return int(match.group(1))
return None
def _adjust_widget_font_sizes(self, widget, ratio):
"""Ajuste les font-sizes de tous les éléments d'un widget avec un ratio proportionnel"""
from PyQt6.QtWidgets import QPushButton, QLineEdit, QTextEdit, QComboBox
import re
# Ajuster les boutons d'onglets du side menu
if hasattr(self, 'side_menu') and hasattr(self.side_menu, 'buttons'):
for button in self.side_menu.buttons:
if button:
widget_id = id(button)
if widget_id in self._base_font_sizes:
base_size = self._base_font_sizes[widget_id]
new_size = max(8, int(base_size * ratio))
current_style = button.styleSheet()
style_without_font = re.sub(r'font-size:\s*\d+px;?', '', current_style)
style_without_font = re.sub(r';+', ';', style_without_font)
style_without_font = style_without_font.strip()
if style_without_font and not style_without_font.endswith(';'):
style_without_font += ';'
new_style = f"{style_without_font} font-size: {new_size}px;"
button.setStyleSheet(new_style)
widget_types = [QLabel, QPushButton, QLineEdit, QTextEdit, QComboBox]
for widget_type in widget_types:
for child in widget.findChildren(widget_type):
# Obtenir l'identifiant unique du widget
widget_id = id(child)
# Si c'est la première fois qu'on voit ce widget, extraire sa font-size de base
# Récupérer la taille de base
if widget_id not in self._base_font_sizes:
base_size = font_size_dict.get(child.__class__.__name__, 14)
self._base_font_sizes[widget_id] = base_size
else:
base_size = self._base_font_sizes[widget_id]
continue # Pas de taille de base, ignorer
# Calculer la nouvelle taille
new_size = int(base_size * ratio)
base_size = self._base_font_sizes[widget_id]
# Calculer la nouvelle taille avec le ratio
new_size = max(8, int(base_size * ratio)) # Minimum 8px
# Appliquer le style (en préservant les autres styles existants)
# Appliquer le style en préservant les autres propriétés
current_style = child.styleSheet()
# Retirer l'ancienne font-size si elle existe
style_parts = [s.strip() for s in current_style.split(';') if s.strip()]
style_parts = [s for s in style_parts if not s.startswith('font-size')]
# Retirer l'ancienne font-size
style_without_font = re.sub(r'font-size:\s*\d+px;?', '', current_style)
# Nettoyer les points-virgules multiples
style_without_font = re.sub(r';+', ';', style_without_font)
style_without_font = style_without_font.strip()
# Ajouter la nouvelle font-size
style_parts.append(f'font-size: {new_size}px')
if style_without_font and not style_without_font.endswith(';'):
style_without_font += ';'
new_style = '; '.join(style_parts)
new_style = f"{style_without_font} font-size: {new_size}px;"
child.setStyleSheet(new_style)
def extract_base_font_size(self) -> dict:
"""Extrait la font-size de base d'un widget depuis son stylesheet"""
base_font_sizes = {}
try:
style = self.theme_manager.get_sheet()
# Chercher "font-size: XXpx" dans le style, puis chercher à quel widget cela correspond
lines = style.splitlines()
component = None
for line in lines:
line = line.strip()
if line.startswith("font-size:"):
size_part = line.split(":")[1].strip().rstrip(";")
if size_part.endswith("px"):
size_value = int(size_part[:-2])
base_font_sizes[component] = size_value
elif line.startswith("Q"):
component = line.split("{")[0].strip()
return base_font_sizes
except Exception:
# En cas d'erreur, retourner une valeur par défaut
return {}
def closeEvent(self, event: QCloseEvent) -> None:
"""Handle application close event"""
@ -191,20 +247,31 @@ class MainWindow(QMainWindow):
def setup_ui(self) -> None:
self.side_menu = TabsWidget(self, MenuDirection.HORIZONTAL, 70, None, 10, 1, BorderSide.BOTTOM, TabSide.TOP)
self.side_menu = TabsWidget(self, MenuDirection.HORIZONTAL, 70, None, 10, BorderSide.BOTTOM, TabSide.TOP)
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, self.language_manager.get_text("tab_suggestions"), paths.get_asset_svg_path("suggestion"), position=ButtonPosition.CENTER, text_position=TextPosition.BOTTOM)
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, self.language_manager.get_text("tab_settings"), paths.get_asset_svg_path("settings"), position=ButtonPosition.CENTER, text_position=TextPosition.BOTTOM)
# Ajouter la tab d'activation uniquement si le système de licence est activé
if self.settings_manager.get_config("enable_licensing"):
self.activation_window = ActivationWindow(self)
self.side_menu.add_widget(self.activation_window, "", paths.get_asset_svg_path("license"), position=ButtonPosition.END)
self.side_menu.add_widget(self.activation_window, self.language_manager.get_text("tab_licensing"), paths.get_asset_svg_path("license"), position=ButtonPosition.END, text_position=TextPosition.BOTTOM)
self.setCentralWidget(self.side_menu)
def get_tab_widget(self):
"""Retourne le widget TabsWidget pour permettre le changement d'onglet"""
return self.side_menu
def update_theme(self) -> None:
self.setStyleSheet(self.theme_manager.get_sheet())
self.setStyleSheet(self.theme_manager.get_sheet())
def update_language(self) -> None:
# Mettre à jour les textes des onglets
self.side_menu.update_button_text(0, self.language_manager.get_text("tab_suggestions"))
self.side_menu.update_button_text(1, self.language_manager.get_text("tab_settings"))
if self.settings_manager.get_config("enable_licensing"):
self.side_menu.update_button_text(2, self.language_manager.get_text("tab_licensing"))

View File

@ -1,4 +1,4 @@
from PyQt6.QtWidgets import QLayout, QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QStackedWidget, QSizePolicy, QSpacerItem
from PyQt6.QtWidgets import QLayout, QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QStackedWidget, QSizePolicy, QSpacerItem, QLabel
from PyQt6.QtGui import QIcon
from PyQt6.QtCore import QSize, Qt
from enum import Enum
@ -25,6 +25,12 @@ class BorderSide(Enum):
BOTTOM = "bottom" # Bord inférieur
NONE = None
class TextPosition(Enum):
LEFT = 0 # Texte à gauche de l'icône
RIGHT = 1 # Texte à droite de l'icône
TOP = 2 # Texte au-dessus de l'icône
BOTTOM = 3 # Texte en dessous de l'icône
class TabSide(Enum):
LEFT = 0 # Barre à gauche (pour VERTICAL)
RIGHT = 1 # Barre à droite (pour VERTICAL)
@ -32,7 +38,7 @@ class TabSide(Enum):
BOTTOM = 1 # Barre en bas (pour HORIZONTAL)
class TabsWidget(QWidget):
def __init__(self, parent=None, direction=MenuDirection.VERTICAL, menu_width=80, onTabChange=None, spacing=10, button_size_ratio=0.8, border_side=BorderSide.LEFT, tab_side=None):
def __init__(self, parent=None, direction=MenuDirection.VERTICAL, menu_width=80, onTabChange=None, spacing=10, border_side=BorderSide.LEFT, tab_side=None, text_position=TextPosition.BOTTOM):
super().__init__(parent)
self.main_manager = MainManager.get_instance()
self.theme_manager = self.main_manager.get_theme_manager()
@ -40,8 +46,8 @@ class TabsWidget(QWidget):
self.observer_manager.subscribe(NotificationType.THEME, self.set_theme)
self.direction = direction
self.menu_width = menu_width
self.button_size_ratio = button_size_ratio # Default ratio for button size relative to menu width
self.onTabChange = onTabChange
self.text_position = text_position # Position du texte par rapport à l'icône
# Gérer border_side comme une liste ou un seul élément
if isinstance(border_side, list):
@ -60,7 +66,7 @@ class TabsWidget(QWidget):
self.buttons = []
self.widgets = []
self.button_positions = []
self.button_size_ratios = [] # Individual ratios for each button
self.button_text_positions = [] # Individual text positions for each button
self._icon_cache = {}
self._original_icon_paths = []
self._square_buttons = []
@ -184,43 +190,40 @@ class TabsWidget(QWidget):
insert_index = end_start_index + len(self.end_buttons)
self.button_layout.insertWidget(insert_index, widget)
def add_widget(self, widget, button_text, icon_path=None, position=ButtonPosition.END, after_button_index=None, button_size_ratio=None):
def add_widget(self, widget, button_text, icon_path=None, position=ButtonPosition.END, after_button_index=None, text_position=None):
"""Add a widget with its corresponding button at specified position"""
# Create button
if icon_path:
colored_icon = self.apply_color_to_svg_icon(icon_path, self.unselected_icon_color)
button = QPushButton(colored_icon, button_text)
self._original_icon_paths.append(icon_path)
else:
button = QPushButton(button_text)
self._original_icon_paths.append(None)
# Use provided text_position or default to widget's text_position
btn_text_position = text_position if text_position is not None else self.text_position
button.setCheckable(True)
# Create button container with custom layout
button_container = self._create_button_with_layout(button_text, icon_path, btn_text_position)
# Store button size ratio (use provided ratio or default)
ratio = button_size_ratio if button_size_ratio is not None else self.button_size_ratio
self.button_size_ratios.append(ratio)
self._original_icon_paths.append(icon_path)
button_container.setCheckable(True)
self.button_text_positions.append(btn_text_position)
# Make button square with specified ratio
self._style_square_button(button)
self._style_square_button(button_container)
# Configurer le widget pour qu'il soit responsive
widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# Add to collections first
widget_index = len(self.widgets)
self.buttons.append(button)
self.buttons.append(button_container)
self.widgets.append(widget)
self.button_positions.append(position)
# Connect button to switch function
button.clicked.connect(lambda checked, idx=widget_index: self.switch_to_tab(idx))
button_container.clicked.connect(lambda checked, idx=widget_index: self.switch_to_tab(idx))
# Add widget to stacked widget
self.stacked_widget.addWidget(widget)
# Insert button at specified position with proper alignment
self._insert_button_with_alignment(button, position, after_button_index)
self._insert_button_with_alignment(button_container, position, after_button_index)
# Select first tab by default
if len(self.buttons) == 1:
@ -228,6 +231,127 @@ class TabsWidget(QWidget):
return widget_index
def _create_button_with_layout(self, text: str, icon_path: str, text_position: TextPosition) -> QPushButton:
"""Create a button with custom layout for icon and text positioning"""
button = QPushButton()
has_icon = icon_path is not None and icon_path != ""
has_text = text is not None and text.strip() != ""
# Create icon label only if there's an icon
icon_label = None
if has_icon:
icon_label = QLabel()
icon_label.setObjectName("icon_label")
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter)
colored_icon = self.apply_color_to_svg_icon(icon_path, self.unselected_icon_color)
pixmap = colored_icon.pixmap(QSize(32, 32)) # Taille par défaut, sera ajustée
icon_label.setPixmap(pixmap)
# Create text label only if there's text
text_label = None
if has_text:
text_label = QLabel(text)
text_label.setObjectName("text_label")
text_label.setAlignment(Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter)
text_label.setWordWrap(True)
# Create layout based on what we have and text position
if has_icon and has_text:
# Both icon and text
if text_position == TextPosition.LEFT:
layout = QHBoxLayout()
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif text_position == TextPosition.RIGHT:
layout = QHBoxLayout()
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif text_position == TextPosition.TOP:
layout = QVBoxLayout()
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
else: # BOTTOM (default)
layout = QVBoxLayout()
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif has_icon:
# Only icon
layout = QVBoxLayout()
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif has_text:
# Only text
layout = QVBoxLayout()
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
else:
# Neither icon nor text - empty button
layout = QVBoxLayout()
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(2)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
button.setLayout(layout)
# Store references to labels for later updates (can be None)
button.icon_label = icon_label
button.text_label = text_label
return button
def _apply_text_position(self, button: QPushButton, text_position: TextPosition):
"""Apply text position to button by recreating its layout"""
# Get existing labels
if not hasattr(button, 'icon_label') or not hasattr(button, 'text_label'):
return
icon_label = button.icon_label
text_label = button.text_label
has_icon = icon_label is not None
has_text = text_label is not None
# Remove old layout
old_layout = button.layout()
if old_layout:
# Remove widgets from layout
while old_layout.count():
item = old_layout.takeAt(0)
if item.widget():
item.widget().setParent(None)
QWidget().setLayout(old_layout) # Delete old layout
# Create new layout based on what we have and text position
if has_icon and has_text:
if text_position == TextPosition.LEFT:
layout = QHBoxLayout()
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif text_position == TextPosition.RIGHT:
layout = QHBoxLayout()
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif text_position == TextPosition.TOP:
layout = QVBoxLayout()
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
else: # BOTTOM (default)
layout = QVBoxLayout()
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif has_icon:
layout = QVBoxLayout()
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif has_text:
layout = QVBoxLayout()
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
else:
layout = QVBoxLayout()
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(2)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
button.setLayout(layout)
def _style_square_button(self, button):
# Set size policy
button.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
@ -250,10 +374,25 @@ class TabsWidget(QWidget):
if obj in self.buttons:
if event.type() == event.Type.Enter:
# Mouse entered button
obj.setIcon(self.apply_color_to_svg_icon(self._original_icon_paths[self.buttons.index(obj)],self.hover_icon_color))
button_index = self.buttons.index(obj)
if hasattr(obj, 'icon_label') and button_index < len(self._original_icon_paths):
icon_path = self._original_icon_paths[button_index]
if icon_path:
colored_icon = self.apply_color_to_svg_icon(icon_path, self.hover_icon_color)
current_size = obj.icon_label.size()
pixmap = colored_icon.pixmap(current_size)
obj.icon_label.setPixmap(pixmap)
elif event.type() == event.Type.Leave:
# Mouse left button
obj.setIcon(self.apply_color_to_svg_icon(self._original_icon_paths[self.buttons.index(obj)],self.unselected_icon_color if not obj.isChecked() else self.selected_icon_color))
button_index = self.buttons.index(obj)
if hasattr(obj, 'icon_label') and button_index < len(self._original_icon_paths):
icon_path = self._original_icon_paths[button_index]
if icon_path:
color = self.unselected_icon_color if not obj.isChecked() else self.selected_icon_color
colored_icon = self.apply_color_to_svg_icon(icon_path, color)
current_size = obj.icon_label.size()
pixmap = colored_icon.pixmap(current_size)
obj.icon_label.setPixmap(pixmap)
return super().eventFilter(obj, event)
@ -299,32 +438,97 @@ class TabsWidget(QWidget):
"""
def _update_button_size(self, button):
"""Update button size based on menu width and button's individual ratio"""
# Find the button's index to get its specific ratio
button_index = -1
for i, btn in enumerate(self.buttons):
if btn == button:
button_index = i
break
"""Update button size - will be recalculated globally"""
# This method now triggers a global recalculation
self._recalculate_all_buttons_size()
def _recalculate_all_buttons_size(self):
"""Recalculate size for all buttons to ensure uniform sizing"""
if not self.buttons:
return
if button_index == -1:
return # Button not found
max_secondary_size = 0
# Get the specific ratio for this button
ratio = self.button_size_ratios[button_index] if button_index < len(self.button_size_ratios) else self.button_size_ratio
# First pass: calculate the maximum secondary dimension needed
for i, button in enumerate(self.buttons):
has_icon = button.icon_label is not None
has_text = button.text_label is not None
if has_icon and has_text:
text_pos = self.button_text_positions[i] if i < len(self.button_text_positions) else self.text_position
if self.direction == MenuDirection.VERTICAL:
# Vertical menu: calculate needed height
if text_pos in [TextPosition.TOP, TextPosition.BOTTOM]:
# Vertical layout: need square
max_secondary_size = max(max_secondary_size, self.menu_width)
else:
# Horizontal layout: need less height but ensure text fits
if has_text and button.text_label:
text_height = button.text_label.sizeHint().height() + 20
icon_height = int(self.menu_width * 0.4) + 10
needed_height = max(text_height, icon_height, int(self.menu_width * 0.6))
max_secondary_size = max(max_secondary_size, needed_height)
else:
# Horizontal menu: calculate needed width
if text_pos in [TextPosition.LEFT, TextPosition.RIGHT]:
# Horizontal layout: need square
max_secondary_size = max(max_secondary_size, self.menu_width)
else:
# Vertical layout: need less width but ensure text fits
if has_text and button.text_label:
text_width = button.text_label.sizeHint().width() + 20
icon_width = int(self.menu_width * 0.4) + 10
needed_width = max(text_width, icon_width, int(self.menu_width * 0.6))
max_secondary_size = max(max_secondary_size, needed_width)
elif has_icon or has_text:
# Only icon or only text
if has_text and button.text_label:
if self.direction == MenuDirection.VERTICAL:
text_height = button.text_label.sizeHint().height() + 20
max_secondary_size = max(max_secondary_size, text_height, int(self.menu_width * 0.6))
else:
text_width = button.text_label.sizeHint().width() + 20
max_secondary_size = max(max_secondary_size, text_width, int(self.menu_width * 0.6))
else:
max_secondary_size = max(max_secondary_size, int(self.menu_width * 0.6))
else:
# Empty button
max_secondary_size = max(max_secondary_size, int(self.menu_width * 0.4))
button_size = int(self.menu_width * ratio)
button.setFixedSize(QSize(button_size, button_size))
# Set proportional icon size
icon_size = int(button_size * 0.6)
button.setIconSize(QSize(icon_size, icon_size))
# Ensure minimum size
max_secondary_size = max(max_secondary_size, int(self.menu_width * 0.6))
# Second pass: apply uniform size to all buttons
for i, button in enumerate(self.buttons):
has_icon = button.icon_label is not None
if self.direction == MenuDirection.VERTICAL:
# Vertical: width = menu_width, height = max calculated
button_width = self.menu_width
button_height = max_secondary_size
else:
# Horizontal: height = menu_width, width = max calculated
button_height = self.menu_width
button_width = max_secondary_size
button.setFixedSize(QSize(button_width, button_height))
# Update icon size if it exists
if has_icon and i < len(self._original_icon_paths) and self._original_icon_paths[i]:
icon_path = self._original_icon_paths[i]
# Icon size is 40% of the smaller dimension
icon_size = int(min(button_width, button_height) * 0.4)
is_selected = button.isChecked()
color = self.selected_icon_color if is_selected else self.unselected_icon_color
colored_icon = self.apply_color_to_svg_icon(icon_path, color)
pixmap = colored_icon.pixmap(QSize(icon_size, icon_size))
button.icon_label.setPixmap(pixmap)
button.icon_label.setFixedSize(QSize(icon_size, icon_size))
def _update_all_button_sizes(self):
"""Update all button sizes when container is resized"""
if hasattr(self, '_square_buttons'):
for button in self._square_buttons:
if button.parent(): # Check if button still exists
self._update_button_size(button)
if hasattr(self, '_square_buttons') and self._square_buttons:
self._recalculate_all_buttons_size()
def showEvent(self, event):
"""Handle show event to set initial button sizes"""
@ -388,23 +592,8 @@ class TabsWidget(QWidget):
return -1
def set_button_size_ratio(self, ratio, button_index=None):
"""Set the button size ratio globally or for a specific button"""
if button_index is not None:
# Set ratio for specific button
if 0 <= button_index < len(self.button_size_ratios):
self.button_size_ratios[button_index] = ratio
if button_index < len(self.buttons):
self._update_button_size(self.buttons[button_index])
else:
# Set global ratio
self.button_size_ratio = ratio
# Update all buttons that don't have individual ratios
for i in range(len(self.buttons)):
if i >= len(self.button_size_ratios):
self.button_size_ratios.append(ratio)
else:
self.button_size_ratios[i] = ratio
self._update_all_button_sizes()
"""Deprecated: Button sizes now adapt automatically to content"""
pass
def switch_to_tab(self, index):
"""Switch to the specified tab (only for widgets, not simple buttons)"""
@ -419,13 +608,17 @@ class TabsWidget(QWidget):
button.setChecked(is_selected)
# Update icon color based on selection
if hasattr(self, '_original_icon_paths') and i < len(self._original_icon_paths):
if hasattr(button, 'icon_label') and i < len(self._original_icon_paths):
icon_path = self._original_icon_paths[i]
if icon_path:
color = (self.selected_icon_color if is_selected
else self.unselected_icon_color)
colored_icon = self.apply_color_to_svg_icon(icon_path, color)
button.setIcon(colored_icon)
# Get current icon size
current_size = button.icon_label.size()
pixmap = colored_icon.pixmap(current_size)
button.icon_label.setPixmap(pixmap)
# Update button property for styling
button.setProperty("selected", is_selected)
@ -456,19 +649,18 @@ class TabsWidget(QWidget):
border_style = self._get_border_style()
button.setStyleSheet(border_style)
# Get original icon if it exists
original_icon = button.icon()
if not original_icon.isNull():
# Apply color to SVG and create new icon
if hasattr(self, '_original_icon_paths') and i < len(self._original_icon_paths):
icon_path = self._original_icon_paths[i]
if icon_path:
# Choose color based on selection state
color = self.selected_icon_color if is_selected else self.unselected_icon_color
# Update icon color if icon_label exists
if hasattr(button, 'icon_label') and i < len(self._original_icon_paths):
icon_path = self._original_icon_paths[i]
if icon_path:
# Choose color based on selection state
color = self.selected_icon_color if is_selected else self.unselected_icon_color
# Apply color to SVG and create new QIcon
colored_icon = self.apply_color_to_svg_icon(icon_path, color)
button.setIcon(colored_icon)
# Apply color to SVG and create new QIcon
colored_icon = self.apply_color_to_svg_icon(icon_path, color)
current_size = button.icon_label.size()
pixmap = colored_icon.pixmap(current_size)
button.icon_label.setPixmap(pixmap)
# Apply button styling based on selection state
button.setProperty("selected", is_selected)
@ -476,7 +668,16 @@ class TabsWidget(QWidget):
# Force style update
button.style().unpolish(button)
button.style().polish(button)
def update_button_text(self, index, new_text):
"""Update the text of a button at the specified index"""
if 0 <= index < len(self.buttons):
button = self.buttons[index]
if hasattr(button, 'text_label') and button.text_label is not None:
button.text_label.setText(new_text)
# Optionally, update button size after text change
self._update_button_size(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)
@ -555,9 +756,5 @@ class TabsWidget(QWidget):
return QIcon()
def update_buttons_size(self, size):
"""Update all buttons to a specific pixel size (overrides ratio-based sizing)"""
for button in self.buttons:
button.setFixedSize(size, size)
# Update icon size proportionally
icon_size = max(int(size * 0.6), 12)
button.setIconSize(QSize(icon_size, icon_size))
"""Update all buttons size"""
self._update_all_button_sizes()

View File

@ -29,7 +29,7 @@ class SplashScreen(QWidget):
# Configuration de la fenêtre
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setFixedSize(800, 600)
self.setFixedSize(2000, 2000)
# Layout principal
layout = QVBoxLayout(self)

View File

@ -60,5 +60,8 @@
"cancel": "Cancel",
"activation_required": "Activation is required to continue.",
"compare_versions": "Compare Versions",
"no_license": "No License"
"no_license": "No License",
"tab_suggestions": "Suggestions",
"tab_settings": "Settings",
"tab_licensing": "Licensing"
}

View File

@ -61,5 +61,8 @@
"cancel": "Annuler",
"activation_required": "L'activation est requise pour continuer.",
"compare_versions": "Comparer les versions",
"no_license": "Pas de licence"
"no_license": "Pas de licence",
"tab_suggestions": "Suggestions",
"tab_settings": "Paramètres",
"tab_licensing": "Licence"
}

View File

@ -4,6 +4,7 @@ setlocal enabledelayedexpansion
REM === PATH SETUP ===
set PARENT_DIR=%~dp0..
set CONFIG_FILE=%PARENT_DIR%\config.json
set ICON_FILE=%PARENT_DIR%\data\assets\icon.png
set ENV_FILE=%PARENT_DIR%\.env
REM Check if .env file exists
@ -24,8 +25,10 @@ REM === Extract python path from .env file ===
for /f "usebackq tokens=2 delims==" %%i in (`findstr "PYTHON_PATH" "%ENV_FILE%"`) do set SYSTEM_PYTHON=%%i
set VENV_PATH=%PARENT_DIR%\WINenv_%ARCHITECTURE%
set EXE_NAME=%APP_NAME%-Windows-%ARCHITECTURE%
set EXE_NAME=%APP_NAME%.exe
set PYTHON_IN_VENV=%VENV_PATH%\Scripts\python.exe
set BUILD_DIR=%PARENT_DIR%\build
set ZIP_FILE=%BUILD_DIR%\%APP_NAME%.zip
REM === Verify Python existence ===
if not exist "%SYSTEM_PYTHON%" (
@ -45,12 +48,44 @@ if not exist "%VENV_PATH%\Scripts\activate.bat" (
REM === Run PyInstaller ===
"%PYTHON_IN_VENV%" -m PyInstaller ^
--distpath "%PARENT_DIR%\build" ^
--workpath "%PARENT_DIR%\build\dist" ^
--distpath "%BUILD_DIR%" ^
--workpath "%BUILD_DIR%\dist" ^
--clean ^
"%PARENT_DIR%\BUILD.spec"
REM === Clean build cache ===
rmdir /s /q "%PARENT_DIR%\build\dist"
rmdir /s /q "%BUILD_DIR%\dist"
REM === Create ZIP ===
echo [INFO] Creating ZIP archive...
set TEMP_ZIP_DIR=%BUILD_DIR%\temp_zip
REM Remove old temp dir if exists
if exist "%TEMP_ZIP_DIR%" rmdir /s /q "%TEMP_ZIP_DIR%"
mkdir "%TEMP_ZIP_DIR%"
REM Copy compiled app - tout le contenu du build sauf les ZIP existants
move /Y "%BUILD_DIR%\%EXE_NAME%" "%TEMP_ZIP_DIR%\"
REM Copy config.json
copy "%CONFIG_FILE%" "%TEMP_ZIP_DIR%\" /Y
REM Copy icon.png
copy "%ICON_FILE%" "%TEMP_ZIP_DIR%\" /Y
REM Copy data/lang
xcopy /E /I /Y "%PARENT_DIR%\data\lang" "%TEMP_ZIP_DIR%\lang"
REM Remove old ZIP if exists
if exist "%ZIP_FILE%" del "%ZIP_FILE%"
REM Create ZIP
powershell -NoProfile -Command "Add-Type -AssemblyName 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::CreateFromDirectory('%TEMP_ZIP_DIR%', '%ZIP_FILE%')"
REM Remove temp folder
rmdir /s /q "%TEMP_ZIP_DIR%"
echo [INFO] ZIP created at: %ZIP_FILE%
endlocal

View File

@ -1,59 +0,0 @@
#!/bin/bash
set -e
# === PATH SETUP ===
PARENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CONFIG_FILE="$PARENT_DIR/config.json"
ENV_FILE="$PARENT_DIR/.env"
# --- Check .env file ---
if [[ ! -f "$ENV_FILE" ]]; then
echo "[ERROR] .env file not found. Please copy .env.example to .env and configure it."
exit 1
fi
# --- Check jq availability ---
if ! command -v jq &>/dev/null; then
echo "[ERROR] 'jq' is required. Install it with: brew install jq"
exit 1
fi
# --- Extract values from config.json ---
ICON_PATH=$(jq -r '.icon_path' "$CONFIG_FILE")
APP_NAME=$(jq -r '.app_name' "$CONFIG_FILE")
ARCHITECTURE=$(jq -r '.architecture' "$CONFIG_FILE")
# --- Extract PYTHON_PATH from .env ---
SYSTEM_PYTHON=$(grep -E "^PYTHON_PATH=" "$ENV_FILE" | cut -d '=' -f2)
VENV_PATH="$PARENT_DIR/MACenv_$ARCHITECTURE"
EXE_NAME="${APP_NAME}-MacOS-${ARCHITECTURE}"
PYTHON_IN_VENV="$VENV_PATH/bin/python"
# --- Verify Python existence ---
if [[ ! -x "$SYSTEM_PYTHON" ]]; then
echo "[ERROR] Python not found at: $SYSTEM_PYTHON"
exit 1
fi
# --- Check if virtual environment exists ---
if [[ ! -f "$VENV_PATH/bin/activate" ]]; then
echo "[INFO] Virtual environment not found. Creating..."
"$SYSTEM_PYTHON" -m venv "$VENV_PATH"
"$PYTHON_IN_VENV" -m pip install --upgrade pip
"$PYTHON_IN_VENV" -m pip install -r "$PARENT_DIR/requirements.txt"
else
echo "[INFO] Virtual environment found."
fi
# --- Run PyInstaller ---
"$PYTHON_IN_VENV" -m PyInstaller \
--distpath "$PARENT_DIR/build" \
--workpath "$PARENT_DIR/build/dist" \
--clean \
"$PARENT_DIR/BUILD.spec"
# --- Clean build cache ---
rm -rf "$PARENT_DIR/build/dist"
echo "[INFO] Build complete: $EXE_NAME"

View File

@ -1,59 +0,0 @@
#!/bin/bash
set -e
# Root paths
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CONFIG_FILE="$ROOT_DIR/config.json"
REQUIREMENTS="$ROOT_DIR/requirements.txt"
ENV_FILE="$ROOT_DIR/.env"
# Check if .env exists
if [[ ! -f "$ENV_FILE" ]]; then
echo "[ERROR] .env file not found. Please copy .env.example to .env and configure it."
exit 1
fi
# Extract architecture from config.json (requires jq)
if ! command -v jq &>/dev/null; then
echo "[ERROR] 'jq' is required. Install it with: brew install jq"
exit 1
fi
ARCHITECTURE=$(jq -r '.architecture' "$CONFIG_FILE")
# Extract python path from .env
PYTHON_EXEC=$(grep -E "^PYTHON_PATH=" "$ENV_FILE" | cut -d '=' -f2)
# Construct venv path
ENV_NAME="MACenv_$ARCHITECTURE"
ENV_PATH="$ROOT_DIR/$ENV_NAME"
# Check python executable
if [[ ! -x "$PYTHON_EXEC" ]]; then
echo "[ERROR] Python not found at: $PYTHON_EXEC"
echo "Please check your .env or installation path."
exit 1
fi
# Show info
echo "[INFO] Configuration:"
echo " Python: $PYTHON_EXEC"
echo " Env: $ENV_NAME"
# Create virtual env if missing
if [[ ! -d "$ENV_PATH/bin" ]]; then
echo "[INFO] Virtual environment not found, creating..."
"$PYTHON_EXEC" -m venv "$ENV_PATH"
echo "[INFO] Installing dependencies..."
"$ENV_PATH/bin/python" -m pip install --upgrade pip
"$ENV_PATH/bin/pip" install -r "$REQUIREMENTS"
else
echo "[INFO] Virtual environment found."
fi
# Activate and launch VS Code
echo "[INFO] Launching VS Code..."
# shellcheck source=/dev/null
source "$ENV_PATH/bin/activate"
open -a "Visual Studio Code" "$ROOT_DIR"
exit 0