~nora/qt-sandbox

bbf1171e6746b83f10041e8f7c6d496efeedf099 — nora 1 year, 8 months ago 0c64017
Add converter application
A converter/app.spec => converter/app.spec +45 -0
@@ 0,0 1,45 @@
# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


a = Analysis(
    ['main.py'],
    pathex=[],
    binaries=[],
    datas=[('resources', 'resources/')],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name='Converter',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon=['resources/icon.ico'],
)

A converter/main.py => converter/main.py +14 -0
@@ 0,0 1,14 @@
from pathlib import Path
from PySide6.QtWidgets import QApplication

from package.main_window import MainWindow

SOURCE_FILE = Path(__file__).resolve()
SOURCE_DIR = SOURCE_FILE.parent
DATA_DIR = SOURCE_DIR / "resources"

if __name__ == "__main__":
    app = QApplication()
    win = MainWindow(DATA_DIR)
    win.show()
    app.exec()

A converter/package/api/image.py => converter/package/api/image.py +26 -0
@@ 0,0 1,26 @@
from pathlib import Path

from PIL import Image


class CustomImage:
    def __init__(self, path, folder="reduced"):
        self.path = Path(path)
        self.reduced_path = self.path.parent / folder / self.path.name
        self.image = Image.open(self.path)
        self.width, self.height = self.image.size

    def reduce(self, size=0.5, quality=75):
        new_width = round(self.width * size)
        new_height = round(self.height * size)
        self.image.resize((new_width, new_height), Image.Resampling.LANCZOS)

        if not self.reduced_path.parent.exists():
            self.reduced_path.parent.mkdir()
        self.image.save(self.reduced_path, quality=quality)
        return self.reduced_path.exists()


if __name__ == "__main__":
    i = CustomImage("../../resources/img1.jpg")
    i.reduce(size=1, quality=50)

A converter/package/main_window.py => converter/package/main_window.py +163 -0
@@ 0,0 1,163 @@
import time
from pathlib import Path
from PySide6 import QtWidgets, QtGui, QtCore

from package.api.image import CustomImage


class Worker(QtCore.QObject):
    image_converted = QtCore.Signal(object, bool)
    finished = QtCore.Signal()

    def __init__(self, images, quality, size, folder):
        super().__init__()
        self.images = images
        self.quality = quality
        self.size = size
        self.folder = folder
        self.run = True

    def convert_images(self):
        for item in self.images:
            if self.run and not item.processe:
                image = CustomImage(path=item.text(), folder=self.folder)
                success = image.reduce(size=self.size, quality=self.quality)
                time.sleep(2)
                self.image_converted.emit(item, success)

        self.finished.emit()


class MainWindow(QtWidgets.QWidget):
    def __init__(self, app_dir):
        super().__init__()
        self.app_dir = app_dir
        self.setup_ui()

    def setup_ui(self):
        self.create_widgets()
        self.modify_widgets()
        self.create_layouts()
        self.add_widgets_to_layouts()
        self.setup_connections()

    def create_widgets(self):
        self.lbl_quality = QtWidgets.QLabel("Qualité :")
        self.spn_quality = QtWidgets.QSpinBox()
        self.lbl_size = QtWidgets.QLabel("Taille :")
        self.spn_size = QtWidgets.QSpinBox()
        self.lbl_dir = QtWidgets.QLabel("Répertoire :")
        self.le_dir = QtWidgets.QLineEdit()
        self.lw_files = QtWidgets.QListWidget()


        self.btn_convert = QtWidgets.QPushButton("Convertir")
        self.lbl_drop_info = QtWidgets.QLabel("^ Déposer les images")
        self.ico_checked = QtGui.QIcon(str(self.app_dir / "checked.png"))
        self.ico_unchecked = QtGui.QIcon(str(self.app_dir / "unchecked.png"))

    def modify_widgets(self):
        icon_file = self.app_dir / "icon.ico"
        css_file = self.app_dir / "style.css"

        self.setWindowTitle("Converter")
        self.setWindowIcon(QtGui.QIcon(str(icon_file)))
        with open(css_file, "r") as f:
            self.setStyleSheet(f.read())

        self.spn_quality.setAlignment(QtCore.Qt.AlignRight)
        self.spn_size.setAlignment(QtCore.Qt.AlignRight)
        self.le_dir.setAlignment(QtCore.Qt.AlignRight)

        self.spn_quality.setRange(1, 100)
        self.spn_quality.setValue(75)
        self.spn_size.setRange(1, 100)
        self.spn_size.setValue(50)
        self.le_dir.setPlaceholderText("Répertoire de destination…")
        self.le_dir.setText("reduced")

        self.lw_files.setAlternatingRowColors(True)
        self.lw_files.setSelectionMode(QtWidgets.QListWidget.ExtendedSelection)
        self.lbl_drop_info.setVisible(False)

        self.setAcceptDrops(True)

    def create_layouts(self):
        self.main_layout = QtWidgets.QGridLayout(self)

    def add_widgets_to_layouts(self):
        self.main_layout.addWidget(self.lbl_quality, 0, 0, 1, 1)
        self.main_layout.addWidget(self.spn_quality, 0, 1, 1, 1)
        self.main_layout.addWidget(self.lbl_size, 1, 0, 1, 1)
        self.main_layout.addWidget(self.spn_size, 1, 1, 1, 1)
        self.main_layout.addWidget(self.lbl_dir, 2, 0, 1, 1)
        self.main_layout.addWidget(self.le_dir, 2, 1, 1, 1)
        self.main_layout.addWidget(self.lw_files, 3, 0, 1, 2)
        self.main_layout.addWidget(self.lbl_drop_info, 4, 0, 1, 2)
        self.main_layout.addWidget(self.btn_convert, 5, 0, 1, 2)

    def setup_connections(self):
        self.btn_convert.clicked.connect(self.convert_images)
        QtGui.QShortcut(QtGui.QKeySequence("Backspace"), self.lw_files, self.delete_selected_items)

    def convert_images(self):
        quality = self.spn_quality.value()
        size = self.spn_size.value() / 100.0
        folder = self.le_dir.text()

        lw_items = [self.lw_files.item(i) for i in range(self.lw_files.count())]
        img_to_convert = [1 for i in lw_items if not i.processe]
        if not img_to_convert:
            msg = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Warning,
                                        "Aucune image à convertir",
                                        "Toutes les images ont déjà été converties")
            msg.exec()
            return False

        # Launch a task in background with Thread and Worker
        self.thread = QtCore.QThread(self)
        self.worker = Worker(images=lw_items, quality=quality, size=size, folder=folder)
        self.worker.moveToThread(self.thread)
        self.worker.image_converted.connect(self.image_converted)
        self.worker.finished.connect(self.thread.quit)
        self.thread.started.connect(self.worker.convert_images)
        self.thread.start()

        self.prg_dialog = QtWidgets.QProgressDialog("Conversion des images", "Annuler", 1, len(img_to_convert))
        self.prg_dialog.canceled.connect(self.cancel_convert_images)
        self.prg_dialog.show()

    def cancel_convert_images(self):
        self.worker.run = False
        self.thread.quit()

    def image_converted(self, lw_item, success):
        if success:
            lw_item.setIcon(self.ico_checked)
            lw_item.processe = True
            self.prg_dialog.setValue(self.prg_dialog.value() + 1)

    def delete_selected_items(self):
        for lw_item in self.lw_files.selectedItems():
            self.lw_files.takeItem(self.lw_files.row(lw_item))

    def dragEnterEvent(self, event):
        self.lbl_drop_info.setVisible(True)
        event.accept()

    def dragLeaveEvent(self, event):
        self.lbl_drop_info.setVisible(False)

    def dropEvent(self, event):
        event.accept()
        for url in event.mimeData().urls():
            self.add_file(url.toLocalFile())
        self.lbl_drop_info.setVisible(False)

    def add_file(self, path):
        items = [self.lw_files.item(i).text() for i in range(self.lw_files.count())]
        if path not in items:
            lw_item = QtWidgets.QListWidgetItem(path)
            lw_item.setIcon(self.ico_unchecked)
            lw_item.processe = False
            self.lw_files.addItem(lw_item)

A converter/resources/checked.png => converter/resources/checked.png +0 -0
A converter/resources/icon.ico => converter/resources/icon.ico +0 -0
A converter/resources/style.css => converter/resources/style.css +435 -0
@@ 0,0 1,435 @@
QDialog {
	background-color: #232323;
}

QWidget {
	background-color: #232323;
}

QLabel {
    font-size: 16px;
	color: #fafafa;
}

QLabel:disabled {
	color: #5A5A5A;
}

QStatusBar {
	background-color: #F9AA33;
	color: rgb(10, 10, 10);
}

QToolBar {
	background-color: #232323;
	border: none;
	padding: 5px;
}

QScrollBar:vertical {
	background-color: transparent;
	width: 20px;
	padding-left: 5px;
}

QScrollBar::handle:vertical {
	background: #718792;
	min-height: 0px;
	border-radius: 7px;
}

QScrollBar::add-line:vertical {
	height: 0px;
	subcontrol-position: bottom;
	subcontrol-origin: margin;
}

QScrollBar::sub-line:vertical {
	height: 0px;
	subcontrol-position: top;
	subcontrol-origin: margin;
}

QScrollBar:horizontal {
	background-color: transparent;
	height: 16px;
	padding-left: 5px;
}

QScrollBar::handle:horizontal {
	background: #47A6E5;
	min-height: 0px;
	border-radius: 7px;
}

QScrollBar::add-line:horizontal {
	height: 0px;
	subcontrol-position: left;
	subcontrol-origin: margin;
}

QScrollBar::sub-line:horizontal {
	height: 0px;
	subcontrol-position: right;
	subcontrol-origin: margin;
}

QAbstractScrollArea {
	background-color: #19232D;
}

QScrollArea {
	background: transparent;
}

#frm_titleBar {
	background-color: #47A6E5;
}

#btn_close {
	border-radius: 0px;
	background-color: #47A6E5;
}

#btn_close::hover {
	background-color: hsl(0, 200, 200);
}

#btn_minimize {
	border-radius: 0px;
	background-color: #47A6E5;
}

#btn_minimize::hover {
	background-color: rgb(15, 108, 198);
}

#btn_maximize {
	border-radius: 0px;
	background-color: #47A6E5;
}

#btn_maximize::hover {
	background-color: rgb(15, 108, 198);
}

#lbl_windowTitle {
	color: rgb(250, 250, 250);
	font-size: 16px;
	font-family: "Open Sans";
}

QInputDialog {
	background: #232323;
}

QMessageBox {
	background: #232323;
	color: #fafafa;
}

*[dashboardButton=true] {
	border: none;
	padding-top: 15px;
	height: 50px;
	color: rgb(200, 200, 200);
}

*[dashboardButton-selected=true] {
	border: none;
	padding-top: 15px;
	height: 50px;
	background-color: #47A6E5;
}

*[dashboardButton=true]:hover {
	background-color: rgb(40, 40, 40);
}

QMenuBar {
	padding-top: 5px;
	color: #fafafa;
}

QPushButton[checkboxBtn=true] {
	background-color: transparent;
	border: none;
	font-size: 14px;
	color: #fafafa;
}

QPushButton[checkboxBtn=true]::hover {
	font-size: 14px;
	border: none;
	color: #fafafa;
}

QPushButton {
    background-color: #47A6E5;
    color: rgb(30, 30, 30);
    border: 0px solid rgb(75, 75, 75);
    border-radius: 6px;
    font-size: 16px;
    padding: 0px 8px 0px 8px;
    min-height: 32px;
}

QPushButton::hover {
    background-color: #64b4e8;
}

QRadioButton {
	color: #fafafa;
}

QRadioButton::indicator::unchecked {
	width: 24px;
	height: 24px;
	border-radius: 14px;
	border: 2px solid;
	border-color: rgb(240, 240, 240);
	background-color: rgb(240, 240, 240);
	margin-right: 3px;
	color: rgb(128, 128, 128);
}

QRadioButton::indicator::checked {
	width: 24px;
	height: 24px;
	border-radius: 14px;
	border: 2px solid;
	border-color: #47A6E5;
	background-color: #47A6E5;
	margin-right: 3px;
	color: rgb(240, 240, 240);
}

QLineEdit,
QLineEdit:hover {
	border-radius: 5px;
	height: 30px;
	border: 1px solid rgb(50, 50, 50);
	padding-bottom: 2px;
	padding-left: 4px;
	font-size: 16px;
	background-color: rgb(30, 30, 30);
	color: #fafafa;
}

QLineEdit:disabled {
	color: #5A5A5A;
}

#completer {
	background-color: #232323;
	color: #fafafa;
	font-size: 16px;
	border: none;
}

QTextEdit {
	color: #fafafa;
	padding: 5px;
	border-radius: 5px;
	margin: 1px;
	font-size: 16px;
	border: 1px solid rgb(50, 50, 50);
	background-color: #232323;
}

QTabWidget::pane {
	border: none;
}

QTabBar::tab:selected {
	background-color: #47A6E5;
	color: rgb(10, 10, 10);
}

QTabBar::tab {
	background-color: #232323;
	color: #F9AA33;
}

QListView {
	border-radius: 4px;
	border: 1px solid rgb(37, 37, 37);
	background-color: rgb(30, 30, 30);
	alternate-background-color: rgb(31, 31, 31);
	font-size: 14px;
}

QListView::item {
	border: 0px;
	color: #fafafa;
}

QListView::item:selected {
	border: 0px;
	background-color: #718792;
}

QDockWidget::title {
	background: #232323;
}

QTreeView {
	border-radius: 4px;
	background-color: rgb(30, 30, 30);
	alternate-background-color: rgb(29, 29, 29);
	color: #fafafa;
}

QTreeView::item:selected {
	background-color: #718792;
}

QTableView {
	border: none;
	background-color: #232323;
}

QTableView::item {
	color: #fafafa;
	border: none;
	padding: 2px;
}

QHeaderView {
	background-color: #232323;
}

QHeaderView::section {
	border: 0px solid rgb(100, 100, 100);
	background-color: #232323;
	color: #fafafa;
}

QSlider::groove:horizontal {
	border: 0px solid transparent;
	height: 8px;
	margin: 2px 0;
	background-color: rgb(200, 200, 200);
	border-radius: 4px;
}

QSlider::sub-page:horizontal {
	background: rgb(165, 203, 239);
	border-radius: 4px;
}

QSlider::handle:horizontal {
	background-color: #47A6E5;
	height: 18px;
	width: 18px;
	border: 0px solid transparent;
	margin: -5px 0;
	border-radius: 9px;
}

QSplitter::handle {
	background-color: #232323;
}

QProgressBar {
	border: none;
	background-color: rgb(165, 203, 239);
	height: 10px;
	font-size: 2px;
}

QProgressBar::chunk {
	background-color: rgb(209, 41, 41);
}

QComboBox {
	height: 35px;
	border: 1px solid rgb(50, 50, 50);
	border-radius: 3px;
	padding-left: 10px;
	color: #fafafa;
}

QComboBox QAbstractItemView {
	color: #fafafa;
	border: 1px solid rgb(75, 75, 75);
}

QComboBox::drop-down {
	width: 15px;
}

QGroupBox {
	background-color: #232323;
	color: rgb(10, 10, 10);
}

QGraphicsView {
	background-color: rgb(30, 30, 30);
}

QPushButton[checkboxBtn=false] {
	background-color: transparent;
	color: #fafafa;
	font-size: 15px;
	border: none;
	border-radius: 3px;
	height: 28px;
	font-weight: bold;
}

QPushButton[checkboxBtn=false]::hover {
	background-color: #47A6E5;
	border-radius: 3px;
}

QPushButton[checkboxBtn=false]::pressed {
	background-color: transparent;
	color: #fafafa;
}

QMenu {
	background-color: #232323;
	color: #fafafa;
	border: 1px solid rgb(70, 70, 70);
}

QMenu::item {
	padding: 6px 5px 6px 25px;
	margin-left: 5px;
	x margin-right: 5px;
	border: 1px solid transparent;
	color: rgb(10, 10, 10);
	background-color: #232323;
}

QMenu::item:selected {
	background-color: #47A6E5;
}

QSpinBox {
	border-radius: 5px;
	height: 30px;
	border: 1px solid rgb(50, 50, 50);
	padding-bottom: 2px;
	font-size: 16px;
	background-color: rgb(30, 30, 30);
	color: rgb(180, 180, 180);
}


QStatusBar {
	background-color: rgb(30, 30, 30);
}

QCalendarWidget QWidget {
	alternate-background-color: rgb(40, 40, 40);
	color: rgb(250, 250, 250);
}

QCalendarWidget QAbstractItemView:enabled {
	color: rgb(120, 120, 120);
	background-color: rgb(240, 240, 240);
    selection-background-color: rgb(40, 40, 40);
    selection-color: #47A6E5;
}
\ No newline at end of file

A converter/resources/unchecked.png => converter/resources/unchecked.png +0 -0
M requirements.txt => requirements.txt +1 -0
@@ 1,4 1,5 @@
altgraph==0.17.3
Pillow==9.3.0
pyinstaller==5.5
pyinstaller-hooks-contrib==2022.10
PySide6==6.4.0