diff --git a/.env.example b/.env.example deleted file mode 100644 index 05fd8a2..0000000 --- a/.env.example +++ /dev/null @@ -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 \ No newline at end of file diff --git a/app/ui/layouts/flow_layout.py b/app/ui/layouts/flow_layout.py new file mode 100644 index 0000000..04613bd --- /dev/null +++ b/app/ui/layouts/flow_layout.py @@ -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() + \ No newline at end of file diff --git a/app/ui/main_window.py b/app/ui/main_window.py index cef1341..d85923c 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -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) diff --git a/app/ui/widgets/grid_element.py b/app/ui/widgets/grid_element.py new file mode 100644 index 0000000..83f9ddb --- /dev/null +++ b/app/ui/widgets/grid_element.py @@ -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 \ No newline at end of file diff --git a/app/ui/widgets/grid_widget.py b/app/ui/widgets/grid_widget.py new file mode 100644 index 0000000..ad06fcd --- /dev/null +++ b/app/ui/widgets/grid_widget.py @@ -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) diff --git a/app/ui/windows/editing_window.py b/app/ui/windows/editing_window.py new file mode 100644 index 0000000..873518f --- /dev/null +++ b/app/ui/windows/editing_window.py @@ -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 \ No newline at end of file diff --git a/app/ui/windows/settings_window.py b/app/ui/windows/settings_window.py index 34d4302..217297d 100644 --- a/app/ui/windows/settings_window.py +++ b/app/ui/windows/settings_window.py @@ -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: diff --git a/config.json b/config.json index 95719dc..4676091 100644 --- a/config.json +++ b/config.json @@ -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" diff --git a/data/assets/edit.svg b/data/assets/edit.svg new file mode 100644 index 0000000..c2f870b --- /dev/null +++ b/data/assets/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file