This commit is contained in:
Louis Mazin 2026-06-28 17:42:55 +02:00
commit 342500bc79
9 changed files with 400 additions and 19 deletions

View File

@ -1,8 +0,0 @@
PYTHON_PATH=path_to_python
EMAIL_ADDRESS=example@gmail.com
EMAIL_PASSWORD=ztgzegzeg
EMAIL_SMTP_SERVER=smtp.gmail.com
EMAIL_SMTP_PORT=587
LICENSE_API_URL=url
PURCHASE_URL=url
LICENSE_API_KEY=key

View File

@ -0,0 +1,279 @@
from enum import Enum
from PyQt6.QtCore import Qt, QRect, QSize, QPoint, QMargins
from PyQt6.QtWidgets import QLayout
class JustifyMode(Enum):
NONE = 0
SPACE_BETWEEN = 1
SPACE_AROUND = 2
SPACE_EVENLY = 3
CENTER = 4
class LastRowMode(Enum):
LEFT = 0
JUSTIFY = 1
MATCH_PREVIOUS = 2
CENTER = 3
class FlowLayout(QLayout):
def __init__(
self,
parent=None,
justify_mode=JustifyMode.SPACE_BETWEEN,
last_row_mode=LastRowMode.LEFT,
):
super().__init__(parent)
if parent is not None:
self.setContentsMargins(QMargins(0, 0, 0, 0))
self._items = []
self.justify_mode = justify_mode
self.last_row_mode = last_row_mode
# ------------------------------------------------------------------
# QLayout API
# ------------------------------------------------------------------
def addItem(self, item):
self._items.append(item)
def count(self):
return len(self._items)
def itemAt(self, index):
if 0 <= index < len(self._items):
return self._items[index]
return None
def takeAt(self, index):
if 0 <= index < len(self._items):
return self._items.pop(index)
return None
def expandingDirections(self):
return Qt.Orientation(0)
def hasHeightForWidth(self):
return True
def heightForWidth(self, width):
return self._do_layout(QRect(0, 0, width, 0), True)
def setGeometry(self, rect):
super().setGeometry(rect)
self._do_layout(rect, False)
def sizeHint(self):
return self.minimumSize()
def minimumSize(self):
size = QSize()
for item in self._items:
size = size.expandedTo(item.minimumSize())
margins = self.contentsMargins()
size += QSize(
margins.left() + margins.right(),
margins.top() + margins.bottom(),
)
return size
# ------------------------------------------------------------------
# Internal
# ------------------------------------------------------------------
def _build_rows(self, available_width):
rows = []
current_row = []
current_width = 0
base_spacing = max(0, self.spacing())
for item in self._items:
width = item.sizeHint().width()
if not current_row:
current_row.append(item)
current_width = width
continue
projected = current_width + base_spacing + width
if projected > available_width:
rows.append(current_row)
current_row = [item]
current_width = width
else:
current_row.append(item)
current_width = projected
if current_row:
rows.append(current_row)
return rows
def _layout_row(
self,
row,
rect,
y,
test_only,
justify_mode,
spacing_override=None,
):
widths = [item.sizeHint().width() for item in row]
heights = [item.sizeHint().height() for item in row]
total_width = sum(widths)
row_height = max(heights)
count = len(row)
gaps = max(0, count - 1)
available_width = rect.width()
x = rect.x()
used_spacing = self.spacing()
# --------------------------------------------------------------
# Explicit spacing override (MATCH_PREVIOUS)
# --------------------------------------------------------------
if spacing_override is not None:
used_spacing = spacing_override
for item, w, h in zip(row, widths, heights):
if not test_only:
item.setGeometry(QRect(int(round(x)), y, w, h))
x += w + used_spacing
return row_height, used_spacing
# --------------------------------------------------------------
# LEFT / NONE
# --------------------------------------------------------------
if justify_mode == JustifyMode.NONE:
used_spacing = max(0, self.spacing())
for item, w, h in zip(row, widths, heights):
if not test_only:
item.setGeometry(QRect(int(round(x)), y, w, h))
x += w + used_spacing
return row_height, used_spacing
# --------------------------------------------------------------
# CENTER
# --------------------------------------------------------------
if justify_mode == JustifyMode.CENTER:
used_spacing = max(0, self.spacing())
content_width = total_width
if count > 1:
content_width += gaps * used_spacing
x += max(0, (available_width - content_width) / 2)
for item, w, h in zip(row, widths, heights):
if not test_only:
item.setGeometry(QRect(int(round(x)), y, w, h))
x += w + used_spacing
return row_height, used_spacing
# --------------------------------------------------------------
# SPACE_*
# --------------------------------------------------------------
free_space = max(0, available_width - total_width)
if justify_mode == JustifyMode.SPACE_BETWEEN:
if gaps > 0:
used_spacing = free_space / gaps
else:
used_spacing = 0
elif justify_mode == JustifyMode.SPACE_AROUND:
used_spacing = free_space / count if count else 0
x += used_spacing / 2
elif justify_mode == JustifyMode.SPACE_EVENLY:
used_spacing = free_space / (count + 1) if count else 0
x += used_spacing
for item, w, h in zip(row, widths, heights):
if not test_only:
item.setGeometry(QRect(int(round(x)), y, w, h))
x += w + used_spacing
return row_height, used_spacing
def _do_layout(self, rect, test_only):
rows = self._build_rows(rect.width())
y = rect.y()
vertical_spacing = max(0, self.spacing())
previous_spacing = None
for row_index, row in enumerate(rows):
is_last_row = row_index == len(rows) - 1
spacing_override = None
row_mode = self.justify_mode
if is_last_row:
if self.last_row_mode == LastRowMode.LEFT:
row_mode = JustifyMode.NONE
elif self.last_row_mode == LastRowMode.CENTER:
row_mode = JustifyMode.CENTER
elif self.last_row_mode == LastRowMode.JUSTIFY:
row_mode = self.justify_mode
elif (
self.last_row_mode == LastRowMode.MATCH_PREVIOUS
and previous_spacing is not None
):
spacing_override = previous_spacing
row_mode = JustifyMode.NONE
row_height, used_spacing = self._layout_row(
row=row,
rect=rect,
y=y,
test_only=test_only,
justify_mode=row_mode,
spacing_override=spacing_override,
)
previous_spacing = used_spacing
y += row_height + vertical_spacing
if rows:
y -= vertical_spacing
return y - rect.y()

View File

@ -1,4 +1,4 @@
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel, QWidget
from PyQt6.QtGui import QResizeEvent, QCloseEvent
from PyQt6.QtCore import QSize, QEvent
from app.core.main_manager import MainManager, NotificationType
@ -6,6 +6,7 @@ from app.ui.widgets.tabs_widget import TabsWidget, MenuDirection, ButtonPosition
from app.ui.windows.settings_window import SettingsWindow
from app.ui.windows.suggestion_window import SuggestionWindow
from app.ui.windows.activation_window import ActivationWindow
from app.ui.windows.editing_window import EditingWindow
import app.utils.paths as paths, shutil
from typing import Optional
@ -202,6 +203,9 @@ class MainWindow(QMainWindow):
for widget_type in widget_types:
for child in widget.findChildren(widget_type):
if child.accessibleName() == "ignore":
continue # Ignorer les widgets marqués pour être ignorés
widget_id = id(child)
# Récupérer la taille de base
@ -249,6 +253,10 @@ class MainWindow(QMainWindow):
self.side_menu = TabsWidget(self, MenuDirection.HORIZONTAL, 70, None, 10, BorderSide.BOTTOM, TabSide.TOP)
self.editing_window = EditingWindow(self)
self.side_menu.add_widget(self.editing_window, self.language_manager.get_text("tab_editing"), paths.get_asset_svg_path("edit"), position=ButtonPosition.CENTER, text_position=TextPosition.BOTTOM)
self.suggestion_window = SuggestionWindow(self)
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)

View File

@ -0,0 +1,48 @@
from PyQt6.QtWidgets import QWidget, QLabel, QVBoxLayout
from PyQt6.QtGui import QPixmap
class GridElement(QWidget):
def __init__(self, parent: QWidget = None, name: str = None, image: str = "", linked_object: object = None) -> None:
super().__init__(parent)
self.image = QPixmap(image)
self.image_path = image
self.name = name
self.linked_object = linked_object
self.setup_ui()
def setup_ui(self) -> None:
layout = QVBoxLayout(self)
self.image_label = QLabel(self)
self.image_label.setPixmap(self.image)
self.name_label = QLabel(self.name, self)
self.name_label.setAccessibleName("ignore")
self.name_label.setObjectName("text_label")
layout.addWidget(self.image_label)
layout.addWidget(self.name_label)
self.set_image_size(70, 70) # Set default image size
def set_image(self, image: str) -> None:
self.image = QPixmap(image)
self.image_path = image
def set_name(self, name: str) -> None:
self.name = name
def set_linked_object(self, linked_object: object) -> None:
self.linked_object = linked_object
def set_image_size(self, width: int, height: int) -> None:
self.image = self.image.scaled(width, height)
self.image_label.setPixmap(self.image)
def get_image(self) -> QPixmap:
return self.image
def get_image_path(self) -> str:
return self.image_path
def get_name(self) -> str:
return self.name
def get_linked_object(self) -> object:
return self.linked_object

View File

@ -0,0 +1,30 @@
from PyQt6.QtWidgets import QWidget
from PyQt6.QtCore import Qt
from app.ui.layouts.flow_layout import FlowLayout, JustifyMode, LastRowMode
from app.ui.widgets.grid_element import GridElement
class GridWidget(QWidget):
def __init__(self, elements: list[GridElement] = [], parent=None) -> None:
super().__init__(parent)
self.elements = elements
self.setup_ui()
def setup_ui(self) -> None:
layout = FlowLayout(self, justify_mode=JustifyMode.SPACE_BETWEEN, last_row_mode=LastRowMode.MATCH_PREVIOUS)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
if self.elements:
for element in self.elements:
layout.addWidget(element)
else:
for _ in range(5000): # Add 58 placeholder elements
layout.addWidget(GridElement(self, name="Placeholder", image="data/assets/icon.png"))
def add_element(self, element: GridElement) -> None:
layout: FlowLayout = self.layout()
layout.addWidget(element)
def remove_element(self, element: GridElement) -> None:
layout: FlowLayout = self.layout()
layout.removeWidget(element)
element.setParent(None)

View File

@ -0,0 +1,31 @@
from PyQt6.QtWidgets import QScrollArea, QWidget, QHBoxLayout
from PyQt6.QtCore import Qt
from app.ui.widgets.grid_widget import GridWidget
from app.core.main_manager import MainManager, NotificationType
from typing import Optional
class EditingWindow(QWidget):
def __init__(self, parent: Optional[QWidget] = None) -> None:
super().__init__(parent)
self.main_manager: MainManager = MainManager.get_instance()
self.language_manager = self.main_manager.get_language_manager()
self.settings_manager = self.main_manager.get_settings_manager()
self.theme_manager = self.main_manager.get_theme_manager()
self.observer_manager = self.main_manager.get_observer_manager()
self.observer_manager.subscribe(NotificationType.LANGUAGE, self.update_language)
self.setup_ui()
def setup_ui(self) -> None:
layout = QHBoxLayout(self)
scroll_area = QScrollArea(self)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
scroll_area.setWidgetResizable(True)
grid_widget = GridWidget(parent=self)
scroll_area.setWidget(grid_widget)
layout.addWidget(scroll_area)
def update_language(self) -> None:
return

View File

@ -14,14 +14,6 @@ class SettingsWindow(QWidget):
self.observer_manager = self.main_manager.get_observer_manager()
self.observer_manager.subscribe(NotificationType.LANGUAGE, self.update_language)
# Type hints for UI elements
self.language_layout: QHBoxLayout
self.languageLabel: QLabel
self.languageCombo: QComboBox
self.theme_layout: QHBoxLayout
self.themeLabel: QLabel
self.themeCombo: QComboBox
self.setup_ui()
def setup_ui(self) -> None:

View File

@ -1,5 +1,5 @@
{
"app_name": "Application",
"app_name": "Youtube MP3 Downloader",
"app_os": "Windows",
"app_version": "1.0.0",
"architecture": "x64",
@ -7,7 +7,7 @@
"splash_image": "splash",
"main_script": "main.py",
"git_repo": "https://gitea.louismazin.ovh/LouisMazin/PythonApplicationTemplate",
"enable_licensing": true,
"enable_licensing": false,
"features_by_license": {
"basic": [
"support"

1
data/assets/edit.svg Normal file
View File

@ -0,0 +1 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M21.2799 6.40005L11.7399 15.94C10.7899 16.89 7.96987 17.33 7.33987 16.7C6.70987 16.07 7.13987 13.25 8.08987 12.3L17.6399 2.75002C17.8754 2.49308 18.1605 2.28654 18.4781 2.14284C18.7956 1.99914 19.139 1.92124 19.4875 1.9139C19.8359 1.90657 20.1823 1.96991 20.5056 2.10012C20.8289 2.23033 21.1225 2.42473 21.3686 2.67153C21.6147 2.91833 21.8083 3.21243 21.9376 3.53609C22.0669 3.85976 22.1294 4.20626 22.1211 4.55471C22.1128 4.90316 22.0339 5.24635 21.8894 5.5635C21.7448 5.88065 21.5375 6.16524 21.2799 6.40005V6.40005Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M11 4H6C4.93913 4 3.92178 4.42142 3.17163 5.17157C2.42149 5.92172 2 6.93913 2 8V18C2 19.0609 2.42149 20.0783 3.17163 20.8284C3.92178 21.5786 4.93913 22 6 22H17C19.21 22 20 20.2 20 18V13" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB