From ce2dd90a39d65b7e15115a1c9d5697f06c5eb110 Mon Sep 17 00:00:00 2001 From: Mr Finchum Date: Sun, 23 Mar 2025 12:37:56 +0000 Subject: [PATCH 01/13] feat: added feature to show original image in preview window. --- CHANGELOG.md | 6 + src/OptimaLab35/gui.py | 19 +- src/OptimaLab35/ui/main_window.py | 11 +- src/OptimaLab35/ui/main_window.ui | 19 +- src/OptimaLab35/ui/preview_window.py | 28 ++- src/OptimaLab35/ui/preview_window.ui | 44 +++- src/OptimaLab35/ui/settings_window.py | 194 ++++++++--------- src/OptimaLab35/ui/settings_window.ui | 296 +++++++++++++------------- 8 files changed, 359 insertions(+), 258 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ee1ff1..c60c01e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.1.x +### 1.1.0: New Function in Preview Window +- Added a new feature to the preview window: **Hold a button to temporarily view the original (unedited) image.** This makes it easier to compare changes. +- Minor UI adjustments. + + ## 1.0.x ### 1.0.1: Fixed spelling - Fixes spelling some places diff --git a/src/OptimaLab35/gui.py b/src/OptimaLab35/gui.py index a69a452..cf40df3 100644 --- a/src/OptimaLab35/gui.py +++ b/src/OptimaLab35/gui.py @@ -861,6 +861,8 @@ class PreviewWindow(QMainWindow, Ui_Preview_Window): self.ui.contrast_Slider.valueChanged.connect(self.on_ui_change) self.ui.grayscale_checkBox.stateChanged.connect(self.on_ui_change) self.ui_elements(False) + self.ui.show_OG_Button.pressed.connect(self.show_OG_image) + self.ui.show_OG_Button.released.connect(self.update_preview) def on_ui_change(self): """Triggers update only if live update is enabled.""" @@ -874,11 +876,26 @@ class PreviewWindow(QMainWindow, Ui_Preview_Window): self.update_preview() self.ui_elements(True) - def ui_elements(self, state): + def show_OG_image(self): + """Handles loading and displaying the image in a separate thread.""" + path = self.ui.image_path_lineEdit.text() + worker = ImageProcessorWorker( + path = path, + optima_manager = self.o, + brightness = 0, + contrast = 0, + grayscale = False, + resize = self.ui.scale_Slider.value(), + callback = self.display_image # Callback to update UI + ) + self.threadpool.start(worker) + + def ui_elements(self, state): self.ui.groupBox_2.setEnabled(state) self.ui.groupBox.setEnabled(state) self.ui.groupBox_5.setEnabled(state) + self.ui.show_OG_Button.setEnabled(state) def update_preview(self): """Handles loading and displaying the image in a separate thread.""" diff --git a/src/OptimaLab35/ui/main_window.py b/src/OptimaLab35/ui/main_window.py index 0db0bd8..38191e5 100644 --- a/src/OptimaLab35/ui/main_window.py +++ b/src/OptimaLab35/ui/main_window.py @@ -3,7 +3,7 @@ ################################################################################ ## Form generated from reading UI file 'main_window.ui' ## -## Created by: Qt User Interface Compiler version 6.8.1 +## Created by: Qt User Interface Compiler version 6.8.2 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -116,6 +116,7 @@ class Ui_MainWindow(object): self.png_quality_Slider.setPageStep(1) self.png_quality_Slider.setSliderPosition(6) self.png_quality_Slider.setOrientation(Qt.Orientation.Horizontal) + self.png_quality_Slider.setTickPosition(QSlider.TickPosition.TicksBothSides) self.gridLayout_4.addWidget(self.png_quality_Slider, 4, 2, 1, 1) @@ -147,6 +148,7 @@ class Ui_MainWindow(object): self.jpg_quality_Slider.setMaximum(100) self.jpg_quality_Slider.setSliderPosition(90) self.jpg_quality_Slider.setOrientation(Qt.Orientation.Horizontal) + self.jpg_quality_Slider.setTickPosition(QSlider.TickPosition.TicksBothSides) self.gridLayout_4.addWidget(self.jpg_quality_Slider, 3, 2, 1, 1) @@ -161,6 +163,7 @@ class Ui_MainWindow(object): self.resize_Slider.setMaximum(200) self.resize_Slider.setValue(100) self.resize_Slider.setOrientation(Qt.Orientation.Horizontal) + self.resize_Slider.setTickPosition(QSlider.TickPosition.TicksBothSides) self.gridLayout_4.addWidget(self.resize_Slider, 5, 2, 1, 1) @@ -193,6 +196,7 @@ class Ui_MainWindow(object): self.brightness_horizontalSlider.setMinimum(-100) self.brightness_horizontalSlider.setMaximum(100) self.brightness_horizontalSlider.setOrientation(Qt.Orientation.Horizontal) + self.brightness_horizontalSlider.setTickPosition(QSlider.TickPosition.TicksBothSides) self.gridLayout_3.addWidget(self.brightness_horizontalSlider, 1, 1, 1, 1) @@ -224,6 +228,7 @@ class Ui_MainWindow(object): self.contrast_horizontalSlider.setMinimum(-100) self.contrast_horizontalSlider.setMaximum(100) self.contrast_horizontalSlider.setOrientation(Qt.Orientation.Horizontal) + self.contrast_horizontalSlider.setTickPosition(QSlider.TickPosition.TicksBelow) self.gridLayout_3.addWidget(self.contrast_horizontalSlider, 4, 1, 1, 1) @@ -538,7 +543,7 @@ class Ui_MainWindow(object): self.dateEdit = QDateEdit(self.date_groupBox) self.dateEdit.setObjectName(u"dateEdit") self.dateEdit.setEnabled(False) - self.dateEdit.setDateTime(QDateTime(QDate(2024, 12, 31), QTime(23, 0, 0))) + self.dateEdit.setDateTime(QDateTime(QDate(2024, 12, 31), QTime(22, 0, 0))) self.dateEdit.setMaximumDate(QDate(2038, 12, 31)) self.dateEdit.setMinimumDate(QDate(1970, 1, 1)) self.dateEdit.setCalendarPopup(True) @@ -558,7 +563,7 @@ class Ui_MainWindow(object): MainWindow.setStatusBar(self.statusBar) self.menuBar = QMenuBar(MainWindow) self.menuBar.setObjectName(u"menuBar") - self.menuBar.setGeometry(QRect(0, 0, 450, 22)) + self.menuBar.setGeometry(QRect(0, 0, 450, 19)) self.menuHelp = QMenu(self.menuBar) self.menuHelp.setObjectName(u"menuHelp") self.menuSettings = QMenu(self.menuBar) diff --git a/src/OptimaLab35/ui/main_window.ui b/src/OptimaLab35/ui/main_window.ui index 4da4787..6b87b83 100644 --- a/src/OptimaLab35/ui/main_window.ui +++ b/src/OptimaLab35/ui/main_window.ui @@ -188,6 +188,9 @@ Qt::Orientation::Horizontal + + QSlider::TickPosition::TicksBothSides + @@ -249,6 +252,9 @@ Qt::Orientation::Horizontal + + QSlider::TickPosition::TicksBothSides + @@ -278,6 +284,9 @@ Qt::Orientation::Horizontal + + QSlider::TickPosition::TicksBothSides + @@ -338,6 +347,9 @@ Qt::Orientation::Horizontal + + QSlider::TickPosition::TicksBothSides + @@ -390,6 +402,9 @@ Qt::Orientation::Horizontal + + QSlider::TickPosition::TicksBelow + @@ -881,7 +896,7 @@ - 23 + 22 0 0 2024 @@ -924,7 +939,7 @@ 0 0 450 - 22 + 19 diff --git a/src/OptimaLab35/ui/preview_window.py b/src/OptimaLab35/ui/preview_window.py index daa32c8..5caf68c 100644 --- a/src/OptimaLab35/ui/preview_window.py +++ b/src/OptimaLab35/ui/preview_window.py @@ -3,7 +3,7 @@ ################################################################################ ## Form generated from reading UI file 'preview_window.ui' ## -## Created by: Qt User Interface Compiler version 6.8.1 +## Created by: Qt User Interface Compiler version 6.8.2 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -51,6 +51,7 @@ class Ui_Preview_Window(object): self.verticalLayout_3.setObjectName(u"verticalLayout_3") self.image_path_lineEdit = QLineEdit(self.groupBox_3) self.image_path_lineEdit.setObjectName(u"image_path_lineEdit") + self.image_path_lineEdit.setEnabled(False) self.verticalLayout_3.addWidget(self.image_path_lineEdit) @@ -78,6 +79,7 @@ class Ui_Preview_Window(object): self.brightness_Slider.setMinimum(-100) self.brightness_Slider.setMaximum(100) self.brightness_Slider.setOrientation(Qt.Orientation.Horizontal) + self.brightness_Slider.setTickPosition(QSlider.TickPosition.TicksBothSides) self.verticalLayout.addWidget(self.brightness_Slider) @@ -105,6 +107,7 @@ class Ui_Preview_Window(object): self.contrast_Slider.setMinimum(-100) self.contrast_Slider.setMaximum(100) self.contrast_Slider.setOrientation(Qt.Orientation.Horizontal) + self.contrast_Slider.setTickPosition(QSlider.TickPosition.TicksBothSides) self.verticalLayout_2.addWidget(self.contrast_Slider) @@ -132,6 +135,20 @@ class Ui_Preview_Window(object): self.verticalLayout_4.addItem(self.verticalSpacer) + self.label = QLabel(self.widget) + self.label.setObjectName(u"label") + self.label.setLayoutDirection(Qt.LayoutDirection.LeftToRight) + self.label.setAutoFillBackground(False) + self.label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.label.setWordWrap(True) + + self.verticalLayout_4.addWidget(self.label) + + self.show_OG_Button = QPushButton(self.widget) + self.show_OG_Button.setObjectName(u"show_OG_Button") + + self.verticalLayout_4.addWidget(self.show_OG_Button) + self.groupBox_4 = QGroupBox(self.widget) self.groupBox_4.setObjectName(u"groupBox_4") self.groupBox_4.setMaximumSize(QSize(170, 16777215)) @@ -163,8 +180,9 @@ class Ui_Preview_Window(object): self.scale_Slider.setMinimum(10) self.scale_Slider.setMaximum(100) self.scale_Slider.setPageStep(10) - self.scale_Slider.setValue(25) + self.scale_Slider.setValue(50) self.scale_Slider.setOrientation(Qt.Orientation.Horizontal) + self.scale_Slider.setTickPosition(QSlider.TickPosition.TicksBothSides) self.gridLayout_2.addWidget(self.scale_Slider, 1, 0, 1, 2) @@ -192,7 +210,7 @@ class Ui_Preview_Window(object): Preview_Window.setCentralWidget(self.centralwidget) self.menubar = QMenuBar(Preview_Window) self.menubar.setObjectName(u"menubar") - self.menubar.setGeometry(QRect(0, 0, 875, 26)) + self.menubar.setGeometry(QRect(0, 0, 875, 19)) Preview_Window.setMenuBar(self.menubar) self.retranslateUi(Preview_Window) @@ -230,6 +248,8 @@ class Ui_Preview_Window(object): self.grayscale_checkBox.setToolTip(QCoreApplication.translate("Preview_Window", u"Convert the image to grayscale (black and white).", None)) #endif // QT_CONFIG(tooltip) self.grayscale_checkBox.setText(QCoreApplication.translate("Preview_Window", u"Black and White", None)) + self.label.setText(QCoreApplication.translate("Preview_Window", u"Hold button to show original image", None)) + self.show_OG_Button.setText(QCoreApplication.translate("Preview_Window", u"Show", None)) self.groupBox_4.setTitle(QCoreApplication.translate("Preview_Window", u"Behavior", None)) #if QT_CONFIG(tooltip) self.checkBox.setToolTip(QCoreApplication.translate("Preview_Window", u"Enable to copy adjustments to the main window upon closing", None)) @@ -253,6 +273,6 @@ class Ui_Preview_Window(object): #if QT_CONFIG(tooltip) self.scale_label.setToolTip(QCoreApplication.translate("Preview_Window", u"Sets the resize value for the preview image. A high value may cause the application to freeze.", None)) #endif // QT_CONFIG(tooltip) - self.scale_label.setText(QCoreApplication.translate("Preview_Window", u"25", None)) + self.scale_label.setText(QCoreApplication.translate("Preview_Window", u"50", None)) # retranslateUi diff --git a/src/OptimaLab35/ui/preview_window.ui b/src/OptimaLab35/ui/preview_window.ui index 38a1cc0..74b4cbb 100644 --- a/src/OptimaLab35/ui/preview_window.ui +++ b/src/OptimaLab35/ui/preview_window.ui @@ -63,6 +63,9 @@ + + false + Enter the path to the image file or use the 'Select Image' button to browse. @@ -108,6 +111,9 @@ Qt::Orientation::Horizontal + + QSlider::TickPosition::TicksBothSides + @@ -150,6 +156,9 @@ Qt::Orientation::Horizontal + + QSlider::TickPosition::TicksBothSides + @@ -197,6 +206,32 @@ + + + + Qt::LayoutDirection::LeftToRight + + + false + + + Hold button to show original image + + + Qt::AlignmentFlag::AlignCenter + + + true + + + + + + + Show + + + @@ -268,11 +303,14 @@ 10 - 25 + 50 Qt::Orientation::Horizontal + + QSlider::TickPosition::TicksBothSides + @@ -302,7 +340,7 @@ Sets the resize value for the preview image. A high value may cause the application to freeze. - 25 + 50 @@ -320,7 +358,7 @@ 0 0 875 - 26 + 19 diff --git a/src/OptimaLab35/ui/settings_window.py b/src/OptimaLab35/ui/settings_window.py index 1841a94..e3d7823 100644 --- a/src/OptimaLab35/ui/settings_window.py +++ b/src/OptimaLab35/ui/settings_window.py @@ -3,7 +3,7 @@ ################################################################################ ## Form generated from reading UI file 'settings_window.ui' ## -## Created by: Qt User Interface Compiler version 6.8.1 +## Created by: Qt User Interface Compiler version 6.8.2 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -36,79 +36,6 @@ class Ui_Settings_Window(object): self.tabWidget = QTabWidget(self.centralwidget) self.tabWidget.setObjectName(u"tabWidget") self.tabWidget.setMaximumSize(QSize(500, 500)) - self.tabWidgetPage1 = QWidget() - self.tabWidgetPage1.setObjectName(u"tabWidgetPage1") - self.verticalLayout_2 = QVBoxLayout(self.tabWidgetPage1) - self.verticalLayout_2.setObjectName(u"verticalLayout_2") - self.groupBox_4 = QGroupBox(self.tabWidgetPage1) - self.groupBox_4.setObjectName(u"groupBox_4") - self.horizontalLayout_3 = QHBoxLayout(self.groupBox_4) - self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") - self.label_4 = QLabel(self.groupBox_4) - self.label_4.setObjectName(u"label_4") - self.label_4.setWordWrap(True) - - self.horizontalLayout_3.addWidget(self.label_4) - - self.reset_exif_Button = QPushButton(self.groupBox_4) - self.reset_exif_Button.setObjectName(u"reset_exif_Button") - self.reset_exif_Button.setMinimumSize(QSize(100, 0)) - self.reset_exif_Button.setMaximumSize(QSize(100, 16777215)) - - self.horizontalLayout_3.addWidget(self.reset_exif_Button) - - - self.verticalLayout_2.addWidget(self.groupBox_4) - - self.groupBox_3 = QGroupBox(self.tabWidgetPage1) - self.groupBox_3.setObjectName(u"groupBox_3") - self.gridLayout_3 = QGridLayout(self.groupBox_3) - self.gridLayout_3.setObjectName(u"gridLayout_3") - self.label_3 = QLabel(self.groupBox_3) - self.label_3.setObjectName(u"label_3") - - self.gridLayout_3.addWidget(self.label_3, 0, 0, 1, 3) - - self.enable_theme_checkBox = QCheckBox(self.groupBox_3) - self.enable_theme_checkBox.setObjectName(u"enable_theme_checkBox") - self.enable_theme_checkBox.setChecked(False) - - self.gridLayout_3.addWidget(self.enable_theme_checkBox, 1, 0, 1, 1) - - self.theme_selection_comboBox = QComboBox(self.groupBox_3) - self.theme_selection_comboBox.addItem("") - self.theme_selection_comboBox.addItem("") - self.theme_selection_comboBox.addItem("") - self.theme_selection_comboBox.setObjectName(u"theme_selection_comboBox") - self.theme_selection_comboBox.setEnabled(False) - self.theme_selection_comboBox.setMinimumSize(QSize(100, 0)) - self.theme_selection_comboBox.setMaximumSize(QSize(100, 16777215)) - - self.gridLayout_3.addWidget(self.theme_selection_comboBox, 1, 2, 1, 1) - - self.save_and_close_Button = QPushButton(self.groupBox_3) - self.save_and_close_Button.setObjectName(u"save_and_close_Button") - - self.gridLayout_3.addWidget(self.save_and_close_Button, 3, 0, 1, 1) - - self.horizontalSpacer_2 = QSpacerItem(98, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) - - self.gridLayout_3.addItem(self.horizontalSpacer_2, 1, 1, 1, 1) - - self.install_pkg_Button = QPushButton(self.groupBox_3) - self.install_pkg_Button.setObjectName(u"install_pkg_Button") - - self.gridLayout_3.addWidget(self.install_pkg_Button, 2, 0, 1, 3) - - self.save_and_restart_Button = QPushButton(self.groupBox_3) - self.save_and_restart_Button.setObjectName(u"save_and_restart_Button") - - self.gridLayout_3.addWidget(self.save_and_restart_Button, 3, 1, 1, 2) - - - self.verticalLayout_2.addWidget(self.groupBox_3) - - self.tabWidget.addTab(self.tabWidgetPage1, "") self.tabWidgetPage2 = QWidget() self.tabWidgetPage2.setObjectName(u"tabWidgetPage2") self.gridLayout_2 = QGridLayout(self.tabWidgetPage2) @@ -227,6 +154,79 @@ class Ui_Settings_Window(object): self.gridLayout_2.addWidget(self.widget, 5, 0, 1, 2) self.tabWidget.addTab(self.tabWidgetPage2, "") + self.tabWidgetPage1 = QWidget() + self.tabWidgetPage1.setObjectName(u"tabWidgetPage1") + self.verticalLayout_2 = QVBoxLayout(self.tabWidgetPage1) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.groupBox_4 = QGroupBox(self.tabWidgetPage1) + self.groupBox_4.setObjectName(u"groupBox_4") + self.horizontalLayout_3 = QHBoxLayout(self.groupBox_4) + self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") + self.label_4 = QLabel(self.groupBox_4) + self.label_4.setObjectName(u"label_4") + self.label_4.setWordWrap(True) + + self.horizontalLayout_3.addWidget(self.label_4) + + self.reset_exif_Button = QPushButton(self.groupBox_4) + self.reset_exif_Button.setObjectName(u"reset_exif_Button") + self.reset_exif_Button.setMinimumSize(QSize(100, 0)) + self.reset_exif_Button.setMaximumSize(QSize(100, 16777215)) + + self.horizontalLayout_3.addWidget(self.reset_exif_Button) + + + self.verticalLayout_2.addWidget(self.groupBox_4) + + self.groupBox_3 = QGroupBox(self.tabWidgetPage1) + self.groupBox_3.setObjectName(u"groupBox_3") + self.gridLayout_3 = QGridLayout(self.groupBox_3) + self.gridLayout_3.setObjectName(u"gridLayout_3") + self.label_3 = QLabel(self.groupBox_3) + self.label_3.setObjectName(u"label_3") + + self.gridLayout_3.addWidget(self.label_3, 0, 0, 1, 3) + + self.enable_theme_checkBox = QCheckBox(self.groupBox_3) + self.enable_theme_checkBox.setObjectName(u"enable_theme_checkBox") + self.enable_theme_checkBox.setChecked(False) + + self.gridLayout_3.addWidget(self.enable_theme_checkBox, 1, 0, 1, 1) + + self.theme_selection_comboBox = QComboBox(self.groupBox_3) + self.theme_selection_comboBox.addItem("") + self.theme_selection_comboBox.addItem("") + self.theme_selection_comboBox.addItem("") + self.theme_selection_comboBox.setObjectName(u"theme_selection_comboBox") + self.theme_selection_comboBox.setEnabled(False) + self.theme_selection_comboBox.setMinimumSize(QSize(100, 0)) + self.theme_selection_comboBox.setMaximumSize(QSize(100, 16777215)) + + self.gridLayout_3.addWidget(self.theme_selection_comboBox, 1, 2, 1, 1) + + self.save_and_close_Button = QPushButton(self.groupBox_3) + self.save_and_close_Button.setObjectName(u"save_and_close_Button") + + self.gridLayout_3.addWidget(self.save_and_close_Button, 3, 0, 1, 1) + + self.horizontalSpacer_2 = QSpacerItem(98, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.gridLayout_3.addItem(self.horizontalSpacer_2, 1, 1, 1, 1) + + self.install_pkg_Button = QPushButton(self.groupBox_3) + self.install_pkg_Button.setObjectName(u"install_pkg_Button") + + self.gridLayout_3.addWidget(self.install_pkg_Button, 2, 0, 1, 3) + + self.save_and_restart_Button = QPushButton(self.groupBox_3) + self.save_and_restart_Button.setObjectName(u"save_and_restart_Button") + + self.gridLayout_3.addWidget(self.save_and_restart_Button, 3, 1, 1, 2) + + + self.verticalLayout_2.addWidget(self.groupBox_3) + + self.tabWidget.addTab(self.tabWidgetPage1, "") self.verticalLayout.addWidget(self.tabWidget) @@ -243,29 +243,6 @@ class Ui_Settings_Window(object): def retranslateUi(self, Settings_Window): Settings_Window.setWindowTitle(QCoreApplication.translate("Settings_Window", u"Settings", None)) - self.groupBox_4.setTitle(QCoreApplication.translate("Settings_Window", u"EXIF", None)) - self.label_4.setText(QCoreApplication.translate("Settings_Window", u"Reset selectable EXIF data to default", None)) - self.reset_exif_Button.setText(QCoreApplication.translate("Settings_Window", u"Reset", None)) - self.groupBox_3.setTitle(QCoreApplication.translate("Settings_Window", u"Theme", None)) - self.label_3.setText(QCoreApplication.translate("Settings_Window", u"Change theme from OS to PyQT Dark or Light", None)) -#if QT_CONFIG(tooltip) - self.enable_theme_checkBox.setToolTip(QCoreApplication.translate("Settings_Window", u"Changes will take effect after restarting the application.", None)) -#endif // QT_CONFIG(tooltip) - self.enable_theme_checkBox.setText(QCoreApplication.translate("Settings_Window", u"Custom theme", None)) - self.theme_selection_comboBox.setItemText(0, QCoreApplication.translate("Settings_Window", u"Auto", None)) - self.theme_selection_comboBox.setItemText(1, QCoreApplication.translate("Settings_Window", u"Dark", None)) - self.theme_selection_comboBox.setItemText(2, QCoreApplication.translate("Settings_Window", u"Light", None)) - -#if QT_CONFIG(tooltip) - self.theme_selection_comboBox.setToolTip(QCoreApplication.translate("Settings_Window", u"Changes will take effect after restarting the application.", None)) -#endif // QT_CONFIG(tooltip) -#if QT_CONFIG(tooltip) - self.save_and_close_Button.setToolTip(QCoreApplication.translate("Settings_Window", u"Changes will take effect after restarting the application.", None)) -#endif // QT_CONFIG(tooltip) - self.save_and_close_Button.setText(QCoreApplication.translate("Settings_Window", u"Apply theme", None)) - self.install_pkg_Button.setText(QCoreApplication.translate("Settings_Window", u"Install package for custom theme", None)) - self.save_and_restart_Button.setText(QCoreApplication.translate("Settings_Window", u"Apply and restart", None)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabWidgetPage1), QCoreApplication.translate("Settings_Window", u"Preferences", None)) self.pkg_info_groupBox.setTitle(QCoreApplication.translate("Settings_Window", u"Package information", None)) self.label_optima35_latestversion.setText(QCoreApplication.translate("Settings_Window", u"unknown", None)) self.label_latest_version.setText(QCoreApplication.translate("Settings_Window", u"Latest version", None)) @@ -293,5 +270,28 @@ class Ui_Settings_Window(object): #endif // QT_CONFIG(tooltip) self.restart_checkBox.setText(QCoreApplication.translate("Settings_Window", u"Restart", None)) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabWidgetPage2), QCoreApplication.translate("Settings_Window", u"Updater", None)) + self.groupBox_4.setTitle(QCoreApplication.translate("Settings_Window", u"EXIF", None)) + self.label_4.setText(QCoreApplication.translate("Settings_Window", u"Reset selectable EXIF data to default", None)) + self.reset_exif_Button.setText(QCoreApplication.translate("Settings_Window", u"Reset", None)) + self.groupBox_3.setTitle(QCoreApplication.translate("Settings_Window", u"Theme", None)) + self.label_3.setText(QCoreApplication.translate("Settings_Window", u"Change theme from OS to PyQT Dark or Light", None)) +#if QT_CONFIG(tooltip) + self.enable_theme_checkBox.setToolTip(QCoreApplication.translate("Settings_Window", u"Changes will take effect after restarting the application.", None)) +#endif // QT_CONFIG(tooltip) + self.enable_theme_checkBox.setText(QCoreApplication.translate("Settings_Window", u"Custom theme", None)) + self.theme_selection_comboBox.setItemText(0, QCoreApplication.translate("Settings_Window", u"Auto", None)) + self.theme_selection_comboBox.setItemText(1, QCoreApplication.translate("Settings_Window", u"Dark", None)) + self.theme_selection_comboBox.setItemText(2, QCoreApplication.translate("Settings_Window", u"Light", None)) + +#if QT_CONFIG(tooltip) + self.theme_selection_comboBox.setToolTip(QCoreApplication.translate("Settings_Window", u"Changes will take effect after restarting the application.", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(tooltip) + self.save_and_close_Button.setToolTip(QCoreApplication.translate("Settings_Window", u"Changes will take effect after restarting the application.", None)) +#endif // QT_CONFIG(tooltip) + self.save_and_close_Button.setText(QCoreApplication.translate("Settings_Window", u"Apply theme", None)) + self.install_pkg_Button.setText(QCoreApplication.translate("Settings_Window", u"Install package for custom theme", None)) + self.save_and_restart_Button.setText(QCoreApplication.translate("Settings_Window", u"Apply and restart", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabWidgetPage1), QCoreApplication.translate("Settings_Window", u"Preferences", None)) # retranslateUi diff --git a/src/OptimaLab35/ui/settings_window.ui b/src/OptimaLab35/ui/settings_window.ui index d6b92d6..91bee20 100644 --- a/src/OptimaLab35/ui/settings_window.ui +++ b/src/OptimaLab35/ui/settings_window.ui @@ -47,154 +47,6 @@ 0 - - - Preferences - - - - - - EXIF - - - - - - Reset selectable EXIF data to default - - - true - - - - - - - - 100 - 0 - - - - - 100 - 16777215 - - - - Reset - - - - - - - - - - Theme - - - - - - Change theme from OS to PyQT Dark or Light - - - - - - - Changes will take effect after restarting the application. - - - Custom theme - - - false - - - - - - - false - - - - 100 - 0 - - - - - 100 - 16777215 - - - - Changes will take effect after restarting the application. - - - - Auto - - - - - Dark - - - - - Light - - - - - - - - Changes will take effect after restarting the application. - - - Apply theme - - - - - - - Qt::Orientation::Horizontal - - - - 98 - 20 - - - - - - - - Install package for custom theme - - - - - - - Apply and restart - - - - - - - - Updater @@ -381,6 +233,154 @@ + + + Preferences + + + + + + EXIF + + + + + + Reset selectable EXIF data to default + + + true + + + + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + Reset + + + + + + + + + + Theme + + + + + + Change theme from OS to PyQT Dark or Light + + + + + + + Changes will take effect after restarting the application. + + + Custom theme + + + false + + + + + + + false + + + + 100 + 0 + + + + + 100 + 16777215 + + + + Changes will take effect after restarting the application. + + + + Auto + + + + + Dark + + + + + Light + + + + + + + + Changes will take effect after restarting the application. + + + Apply theme + + + + + + + Qt::Orientation::Horizontal + + + + 98 + 20 + + + + + + + + Install package for custom theme + + + + + + + Apply and restart + + + + + + + + From 17f08bc74fcfd6d51ebcffbe917c8b13c2293a21 Mon Sep 17 00:00:00 2001 From: Mr Finchum Date: Sun, 23 Mar 2025 21:48:46 +0000 Subject: [PATCH 02/13] refactor: Splitting Classes into Separate Files --- CHANGELOG.md | 7 +- TODO.md | 6 - src/OptimaLab35/__main__.py | 28 +- src/OptimaLab35/{gui.py => mainWindow.py} | 520 +--------------------- src/OptimaLab35/previewWindow.py | 167 +++++++ src/OptimaLab35/settingsWindow.py | 357 +++++++++++++++ 6 files changed, 568 insertions(+), 517 deletions(-) delete mode 100644 TODO.md rename src/OptimaLab35/{gui.py => mainWindow.py} (51%) create mode 100644 src/OptimaLab35/previewWindow.py create mode 100644 src/OptimaLab35/settingsWindow.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c60c01e..3ec2081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,16 @@ # Changelog +### 1.2.x: Refactor +#### 1.2.0: Splitting Classes into Separate Files +- Refactored `gui.py`, which previously contained almost all the code, into multiple files. +- Each window's logic is now in its own file, improving code organization. +- Window layouts remain in `.ui` folder, while their logic is now properly separated. + ## 1.1.x ### 1.1.0: New Function in Preview Window - Added a new feature to the preview window: **Hold a button to temporarily view the original (unedited) image.** This makes it easier to compare changes. - Minor UI adjustments. - ## 1.0.x ### 1.0.1: Fixed spelling - Fixes spelling some places diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 468cd9b..0000000 --- a/TODO.md +++ /dev/null @@ -1,6 +0,0 @@ -# TODO - -## Improvments -### Preview window -Improve performence by changing when the images is updated. -Right now, when moving the slider, the image gets updated for every single step increase resulting in lagging diff --git a/src/OptimaLab35/__main__.py b/src/OptimaLab35/__main__.py index 14932be..512c7b3 100644 --- a/src/OptimaLab35/__main__.py +++ b/src/OptimaLab35/__main__.py @@ -1,4 +1,30 @@ -from .gui import main +import sys +from PySide6 import QtWidgets +from .utils.utility import Utilities +from .mainWindow import OptimaLab35 +from .const import ( + CONFIG_BASE_PATH +) + +def main(): + u = Utilities(CONFIG_BASE_PATH) + app_settings = u.load_settings() + app = QtWidgets.QApplication(sys.argv) + + try: + import qdarktheme + app_settings["theme"]["theme_pkg"] = True + except ImportError: + app_settings["theme"]["theme_pkg"] = False + + if app_settings["theme"]["use_custom_theme"] and app_settings["theme"]["theme_pkg"]: + qdarktheme.setup_theme(app_settings["theme"]["mode"].lower()) + + u.save_settings(app_settings) + + window = OptimaLab35() + window.show() + app.exec() if __name__ == "__main__": main() diff --git a/src/OptimaLab35/gui.py b/src/OptimaLab35/mainWindow.py similarity index 51% rename from src/OptimaLab35/gui.py rename to src/OptimaLab35/mainWindow.py index cf40df3..3006465 100644 --- a/src/OptimaLab35/gui.py +++ b/src/OptimaLab35/mainWindow.py @@ -1,23 +1,22 @@ -import sys import os from datetime import datetime - -from .ui import resources_rc -from PyPiUpdater import PyPiUpdater from optima35.core import OptimaManager -from .utils.utility import Utilities -from .ui.main_window import Ui_MainWindow -from .ui.preview_window import Ui_Preview_Window -from .ui.settings_window import Ui_Settings_Window -from .ui.exif_handler_window import ExifEditor -from .ui.simple_dialog import SimpleDialog # Import the SimpleDialog class from OptimaLab35 import __version__ from .const import ( APPLICATION_NAME, CONFIG_BASE_PATH ) +from .ui import resources_rc +from .previewWindow import PreviewWindow +from .settingsWindow import SettingsWindow + +from .utils.utility import Utilities +from .ui.main_window import Ui_MainWindow +from .ui.exif_handler_window import ExifEditor +from .ui.simple_dialog import SimpleDialog # Import the SimpleDialog class + from PySide6 import QtWidgets, QtCore from PySide6.QtCore import ( @@ -26,9 +25,7 @@ from PySide6.QtCore import ( Signal, QObject, QRegularExpression, - Qt, - QTimer, - Slot + Qt ) from PySide6.QtWidgets import ( @@ -38,7 +35,7 @@ from PySide6.QtWidgets import ( QFileDialog ) -from PySide6.QtGui import QPixmap, QRegularExpressionValidator, QIcon +from PySide6.QtGui import QRegularExpressionValidator, QIcon class OptimaLab35(QMainWindow, Ui_MainWindow): def __init__(self): @@ -99,8 +96,6 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): validator = QRegularExpressionValidator(regex) self.ui.lat_lineEdit.setValidator(validator) self.ui.long_lineEdit.setValidator(validator) - #layout.addWidget(self.ui.lat_lineEdit) - #layout.addWidget(self.ui.long_lineEdit) # UI related function, changing parts, open, etc. def open_preview_window(self): @@ -501,441 +496,6 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): QApplication.closeAllWindows() event.accept() -class SettingsWindow(QMainWindow, Ui_Settings_Window): - # Mixture of code by me, code/functions refactored by ChatGPT and code directly from ChatGPT - def __init__(self, optimalab35_localversion, optima35_localversion): - super(SettingsWindow, self).__init__() - self.ui = Ui_Settings_Window() - self.ui.setupUi(self) - self.u = Utilities(os.path.expanduser("~/.config/OptimaLab35")) - self.app_settings = self.u.load_settings() - self.dev_mode = True if optimalab35_localversion == "0.0.1" else False - from PyPiUpdater import PyPiUpdater - # Update log file location - self.update_log_file = os.path.expanduser("~/.config/OptimaLab35/update_log.json") - # Store local versions - self.optimalab35_localversion = optimalab35_localversion - self.optima35_localversion = optima35_localversion - - # Create PyPiUpdater instances - self.ppu_ol35 = PyPiUpdater("OptimaLab35", self.optimalab35_localversion, self.update_log_file) - self.ppu_o35 = PyPiUpdater("optima35", self.optima35_localversion, self.update_log_file) - self.ol35_last_state = self.ppu_ol35.get_last_state() - self.o35_last_state = self.ppu_o35.get_last_state() - - # Track which packages need an update - self.updates_available = {"OptimaLab35": False, "optima35": False} - - self.define_gui_interaction() - - def define_gui_interaction(self): - """Setup UI interactions.""" - # Updater related - self.ui.label_optimalab35_localversion.setText(self.optimalab35_localversion) - self.ui.label_optima35_localversion.setText(self.optima35_localversion) - - self.ui.label_latest_version.setText("Latest version") - self.ui.label_optimalab35_latestversion.setText("...") - self.ui.label_optima35_latestversion.setText("...") - - self.ui.update_and_restart_Button.setEnabled(False) - - # Connect buttons to functions - self.ui.check_for_update_Button.clicked.connect(self.check_for_updates) - self.ui.update_and_restart_Button.clicked.connect(self.update_and_restart) - self.ui.label_last_check.setText(f"Last check: {self.time_to_string(self.ol35_last_state[0])}") - self.ui.dev_widget.setVisible(False) - - # Timer for long press detection - self.timer = QTimer() - self.timer.setSingleShot(True) - self.timer.timeout.connect(self.toggle_dev_ui) - - # Connect button press/release - self.ui.check_for_update_Button.pressed.connect(self.start_long_press) - self.ui.check_for_update_Button.released.connect(self.cancel_long_press) - self.ui.label_5.setText('
  • Changelog
  • ') - self.ui.label_5.setOpenExternalLinks(True) - #settings related - self.load_settings_into_ui() - self.ui.reset_exif_Button.clicked.connect(self.ask_reset_exif) - self.ui.save_and_close_Button.clicked.connect(self.save_and_close) - self.ui.save_and_restart_Button.clicked.connect(self.save_and_restart) - - if os.name == "nt": # Disable restart app when windows. - self.ui.save_and_restart_Button.setVisible(False) - self.ui.restart_checkBox.setChecked(False) - self.ui.restart_checkBox.setVisible(False) - -# setting related - - def load_settings_into_ui(self): - """Loads the settings into the UI elements.""" - settings = self.app_settings - theme_mode = settings["theme"]["mode"] - use_custom_theme = settings["theme"]["use_custom_theme"] - pkg_available = settings["theme"]["theme_pkg"] - - if pkg_available: - index = self.ui.theme_selection_comboBox.findText(theme_mode, QtCore.Qt.MatchFlag.MatchExactly) - if index != -1: - self.ui.theme_selection_comboBox.setCurrentIndex(index) - self.ui.enable_theme_checkBox.setChecked(use_custom_theme) - self.ui.install_pkg_Button.setVisible(False) - self.ui.enable_theme_checkBox.setEnabled(True) - else: - self.ui.enable_theme_checkBox.setEnabled(False) - self.ui.install_pkg_Button.clicked.connect(self.install_theme_pkg) - - - def install_theme_pkg(self): - a = self.ppu_ol35.install_package("PyQtDarkTheme-fork") - self.ui.install_pkg_Button.setEnabled(False) - self.ui.install_pkg_Button.setText("Please wait...") - - msg_box = QMessageBox() - msg_box.setIcon(QMessageBox.Information) - msg_box.setWindowTitle("Message") - msg_box.setText(a[1]) - msg_box.setStandardButtons(QMessageBox.Ok) - msg_box.exec() - if a[0]: - self.app_settings["theme"]["theme_pkg"] = True - self.load_settings_into_ui() - else: - self.ui.install_pkg_Button.setEnabled(True) - self.ui.install_pkg_Button.setText("Try again?") - - def save_settings(self): - self.app_settings["theme"]["mode"] = self.ui.theme_selection_comboBox.currentText() - self.app_settings["theme"]["use_custom_theme"] = self.ui.enable_theme_checkBox.isChecked() - self.u.save_settings(self.app_settings) - - def save_and_close(self): - self.save_settings() - self.close() - - def save_and_restart(self): - msg = QMessageBox() - msg.setIcon(QMessageBox.Icon.Question) - msg.setWindowTitle("Confirm Reset") - msg.setText("Are you sure you want to restart the app?") - msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) - - # Show the message box and wait for the user's response - response = msg.exec() - - # Check response and perform action - if response == QMessageBox.StandardButton.Yes: - self.save_settings() - self.restart_program() - else: - pass # Do nothing if "No" is selected - - def ask_reset_exif(self): - """Shows a dialog to ask the user if they are sure about resetting EXIF options to default.""" - # Create a QMessageBox with a Yes/No question - msg = QMessageBox() - msg.setIcon(QMessageBox.Icon.Question) - msg.setWindowTitle("Confirm Reset") - msg.setText("Are you sure you want to reset the EXIF options to default?") - msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) - - # Show the message box and wait for the user's response - response = msg.exec() - - # Check response and perform action - if response == QMessageBox.StandardButton.Yes: - self.u.default_exif() # Reset EXIF options to default - else: - pass # Do nothing if "No" is selected - -# update related parts - def start_long_press(self): - """Start the timer when button is pressed.""" - # brave AI - self.timer.start(1000) # 1-second long press - - def cancel_long_press(self): - """Cancel long press if released early.""" - # brave AI - self.timer.stop() - - def toggle_dev_ui(self): - """Show or hide the hidden UI when long press is detected.""" - self.ui.dev_widget.setVisible(True) - - self.ui.check_local_Button.clicked.connect(self.local_check_for_updates) - self.ui.update_local_Button.clicked.connect(self.local_update) - - def local_check_for_updates(self): - dist_folder = os.path.expanduser("~/.config/OptimaLab35/dist/") - self.ui.label_optimalab35_latestversion.setText("Checking...") - self.ui.label_optima35_latestversion.setText("Checking...") - - # Check OptimaLab35 update - ol35_pkg_info = self.ppu_ol35.check_update_local(dist_folder) - if ol35_pkg_info[0] is None: - self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1][0:13]) - else: - self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1]) - self.updates_available["OptimaLab35"] = ol35_pkg_info[0] - - # Check optima35 update - o35_pkg_info = self.ppu_o35.check_update_local(dist_folder) - if o35_pkg_info[0] is None: - self.ui.label_optima35_latestversion.setText(o35_pkg_info[1][0:13]) - else: - self.ui.label_optima35_latestversion.setText(o35_pkg_info[1]) - self.updates_available["optima35"] = o35_pkg_info[0] - - def local_update(self): - dist_folder = os.path.expanduser("~/.config/OptimaLab35/dist/") - packages_to_update = [pkg for pkg, update in self.updates_available.items() if update] - - if not packages_to_update: - QMessageBox.information(self, "Update", "No updates available.") - return - - # Confirm update - msg = QMessageBox() - msg.setWindowTitle("Update Available") - message = f"Updating: {', '.join(packages_to_update)}\nUpdate " - - if self.ui.restart_checkBox.isChecked(): - message = message + "and restart app?" - else: - message = message + "app?" - - msg.setText(message) - msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - result = msg.exec() - - if result == QMessageBox.Yes: - update_results = [] # Store results - - for package in packages_to_update: - if package == "OptimaLab35": - pkg_info = self.ppu_ol35.update_from_local(dist_folder) - elif package == "optima35": - pkg_info = self.ppu_o35.update_from_local(dist_folder) - - update_results.append(f"{package}: {'Success' if pkg_info[0] else 'Failed'}\n{pkg_info[1]}") - - # Show summary of updates - # Show update completion message - msg = QMessageBox() - msg.setWindowTitle("Update Complete") - msg.setText("\n\n".join(update_results)) - msg.setStandardButtons(QMessageBox.Ok) - msg.exec() - - # Restart the application after user clicks "OK" - if self.ui.restart_checkBox.isChecked(): - self.restart_program() - - def time_to_string(self, time_time): - try: - dt_obj = datetime.fromtimestamp(time_time) - date_string = dt_obj.strftime("%d %h %H:%M") - return date_string - except TypeError: - return "Missing information" - - def check_for_updates(self): - """Check for updates and update the UI.""" - self.ui.check_for_update_Button.setEnabled(False) - self.ui.label_optimalab35_latestversion.setText("Checking...") - self.ui.label_optima35_latestversion.setText("Checking...") - - # Check OptimaLab35 update - ol35_pkg_info = self.ppu_ol35.check_for_update() - if ol35_pkg_info[0] is None: - self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1][0:13]) - else: - self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1]) - self.updates_available["OptimaLab35"] = ol35_pkg_info[0] - - # Check optima35 update - o35_pkg_info = self.ppu_o35.check_for_update() - if o35_pkg_info[0] is None: - self.ui.label_optima35_latestversion.setText(o35_pkg_info[1][0:13]) - else: - self.ui.label_optima35_latestversion.setText(o35_pkg_info[1]) - self.updates_available["optima35"] = o35_pkg_info[0] - - # Enable update button if any update is available - if any(self.updates_available.values()): - if self.dev_mode: - self.ui.update_and_restart_Button.setEnabled(False) - self.ui.update_and_restart_Button.setText("Update disabled") - else: - self.ui.update_and_restart_Button.setEnabled(True) - - last_date = self.time_to_string(self.ppu_ol35.get_last_state()[0]) - self.ui.label_last_check.setText(f"Last check: {last_date}") - self.ui.label_latest_version.setText("Online version") - self.ui.check_for_update_Button.setEnabled(True) - - def update_and_restart(self): - """Update selected packages and restart the application.""" - packages_to_update = [pkg for pkg, update in self.updates_available.items() if update] - - if not packages_to_update: - QMessageBox.information(self, "Update", "No updates available.") - return - - # Confirm update - msg = QMessageBox() - msg.setWindowTitle("Update Available") - message = f"Updating: {', '.join(packages_to_update)}\nUpdate " - - if self.ui.restart_checkBox.isChecked(): - message = message + "and restart app?" - else: - message = message + "app?" - - msg.setText(message) - msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - result = msg.exec() - - if result == QMessageBox.Yes: - update_results = [] # Store results - - for package in packages_to_update: - if package == "OptimaLab35": - pkg_info = self.ppu_ol35.update_package() - elif package == "optima35": - pkg_info = self.ppu_o35.update_package() - - update_results.append(f"{package}: {'Success' if pkg_info[0] else 'Failed'}\n{pkg_info[1]}") - - # Show summary of updates - # Show update completion message - msg = QMessageBox() - msg.setWindowTitle("Update Complete") - msg.setText("\n\n".join(update_results)) - msg.setStandardButtons(QMessageBox.Ok) - msg.exec() - - # Restart the application after user clicks "OK" - if self.ui.restart_checkBox.isChecked(): - self.restart_program() - - def restart_program(self): - """Restart the Python program after an update.""" - print("Restarting the application...") - # Close all running Qt windows before restarting - app = QApplication.instance() - if app: - app.quit() - - python = sys.executable - os.execl(python, python, *sys.argv) - -class PreviewWindow(QMainWindow, Ui_Preview_Window): - values_selected = Signal(int, int, bool) - # Large ChatGPT with rewrite and bug fixes from me. - - def __init__(self): - super(PreviewWindow, self).__init__() - self.ui = Ui_Preview_Window() - self.ui.setupUi(self) - self.o = OptimaManager() - self.threadpool = QThreadPool() # Thread pool for managing worker threads - - self.ui.QLabel.setAlignment(Qt.AlignCenter) - - # UI interactions - self.ui.load_Button.clicked.connect(self.browse_file) - self.ui.update_Button.clicked.connect(self.update_preview) - self.ui.close_Button.clicked.connect(self.close_window) - - self.ui.reset_brightness_Button.clicked.connect(lambda: self.ui.brightness_spinBox.setValue(0)) - self.ui.reset_contrast_Button.clicked.connect(lambda: self.ui.contrast_spinBox.setValue(0)) - - # Connect UI elements to `on_ui_change` - self.ui.brightness_spinBox.valueChanged.connect(self.on_ui_change) - self.ui.brightness_Slider.valueChanged.connect(self.on_ui_change) - self.ui.contrast_spinBox.valueChanged.connect(self.on_ui_change) - self.ui.contrast_Slider.valueChanged.connect(self.on_ui_change) - self.ui.grayscale_checkBox.stateChanged.connect(self.on_ui_change) - self.ui_elements(False) - self.ui.show_OG_Button.pressed.connect(self.show_OG_image) - self.ui.show_OG_Button.released.connect(self.update_preview) - - def on_ui_change(self): - """Triggers update only if live update is enabled.""" - if self.ui.live_update.isChecked(): - self.update_preview() - - def browse_file(self): - file = QFileDialog.getOpenFileName(self, caption="Select File", filter="Images (*.png *.webp *.jpg *.jpeg)") - if file[0]: - self.ui.image_path_lineEdit.setText(file[0]) - self.update_preview() - self.ui_elements(True) - - def show_OG_image(self): - """Handles loading and displaying the image in a separate thread.""" - path = self.ui.image_path_lineEdit.text() - - worker = ImageProcessorWorker( - path = path, - optima_manager = self.o, - brightness = 0, - contrast = 0, - grayscale = False, - resize = self.ui.scale_Slider.value(), - callback = self.display_image # Callback to update UI - ) - self.threadpool.start(worker) - - def ui_elements(self, state): - self.ui.groupBox_2.setEnabled(state) - self.ui.groupBox.setEnabled(state) - self.ui.groupBox_5.setEnabled(state) - self.ui.show_OG_Button.setEnabled(state) - - def update_preview(self): - """Handles loading and displaying the image in a separate thread.""" - path = self.ui.image_path_lineEdit.text() - - worker = ImageProcessorWorker( - path = path, - optima_manager = self.o, - brightness = int(self.ui.brightness_spinBox.text()), - contrast = int(self.ui.contrast_spinBox.text()), - grayscale = self.ui.grayscale_checkBox.isChecked(), - resize = self.ui.scale_Slider.value(), - callback = self.display_image # Callback to update UI - ) - self.threadpool.start(worker) # Run worker in a thread - - def display_image(self, pixmap): - """Adjusts the image to fit within the QLabel.""" - if pixmap is None: - QMessageBox.warning(self, "Warning", "Error processing image...") - return - - max_size = self.ui.QLabel.size() - scaled_pixmap = pixmap.scaled(max_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) - self.ui.QLabel.setPixmap(scaled_pixmap) - self.ui.QLabel.resize(scaled_pixmap.size()) - - def resizeEvent(self, event): - """Triggered when the preview window is resized.""" - file_path = self.ui.image_path_lineEdit.text() - if os.path.exists(file_path): - self.update_preview() # Re-process and display the image - super().resizeEvent(event) # Keep the default behavior - - def close_window(self): - """Emits signal and closes the window.""" - if self.ui.checkBox.isChecked(): - self.values_selected.emit(self.ui.brightness_spinBox.value(), self.ui.contrast_spinBox.value(), self.ui.grayscale_checkBox.isChecked()) - self.close() - class WorkerSignals(QObject): # ChatGPT progress = Signal(int) @@ -984,61 +544,3 @@ class ImageProcessorRunnable(QRunnable): self.signals.progress.emit(int((i / len(self.image_files)) * 100)) self.signals.finished.emit() - -class ImageProcessorWorker(QRunnable): - """Worker class to load and process the image in a separate thread.""" - # ChatGPT - def __init__(self, path, optima_manager, brightness, contrast, grayscale, resize, callback): - super().__init__() - self.path = path - self.optima_manager = optima_manager - self.brightness = brightness - self.contrast = contrast - self.grayscale = grayscale - self.resize = resize - self.callback = callback # Function to call when processing is done - - @Slot() - def run(self): - """Runs the image processing in a separate thread.""" - if not os.path.isfile(self.path): - self.callback(None) - return - - try: - img = self.optima_manager.process_image_object( - image_input_file = self.path, - watermark = "PREVIEW", - resize = self.resize, - grayscale = self.grayscale, - brightness = self.brightness, - contrast = self.contrast - ) - pixmap = QPixmap.fromImage(img) - self.callback(pixmap) - except Exception as e: - print(f"Error processing image: {e}") - self.callback(None) - -def main(): - u = Utilities(os.path.expanduser(CONFIG_BASE_PATH)) - app_settings = u.load_settings() - app = QtWidgets.QApplication(sys.argv) - - try: - import qdarktheme - app_settings["theme"]["theme_pkg"] = True - except Exception: - app_settings["theme"]["theme_pkg"] = False - - if app_settings["theme"]["use_custom_theme"] and app_settings["theme"]["theme_pkg"]: - qdarktheme.setup_theme(app_settings["theme"]["mode"].lower()) - - u.save_settings(app_settings) - del u - window = OptimaLab35() - window.show() - app.exec() - -if __name__ == "__main__": - main() diff --git a/src/OptimaLab35/previewWindow.py b/src/OptimaLab35/previewWindow.py new file mode 100644 index 0000000..72469be --- /dev/null +++ b/src/OptimaLab35/previewWindow.py @@ -0,0 +1,167 @@ +import os +from optima35.core import OptimaManager + +from OptimaLab35 import __version__ + +from .ui import resources_rc +from .ui.preview_window import Ui_Preview_Window + +from PySide6 import QtWidgets, QtCore + +from PySide6.QtCore import ( + QRunnable, + QThreadPool, + Signal, + QObject, + QRegularExpression, + Qt, + QTimer, + Slot +) + +from PySide6.QtWidgets import ( + QMessageBox, + QApplication, + QMainWindow, + QFileDialog +) + +from PySide6.QtGui import QPixmap, QRegularExpressionValidator, QIcon + +class PreviewWindow(QMainWindow, Ui_Preview_Window): + values_selected = Signal(int, int, bool) + # Large ChatGPT with rewrite and bug fixes from me. + + def __init__(self): + super(PreviewWindow, self).__init__() + self.ui = Ui_Preview_Window() + self.ui.setupUi(self) + self.o = OptimaManager() + self.threadpool = QThreadPool() # Thread pool for managing worker threads + self.setWindowIcon(QIcon(":app-icon.png")) + self.ui.QLabel.setAlignment(Qt.AlignCenter) + + # UI interactions + self.ui.load_Button.clicked.connect(self.browse_file) + self.ui.update_Button.clicked.connect(self.update_preview) + self.ui.close_Button.clicked.connect(self.close_window) + + self.ui.reset_brightness_Button.clicked.connect(lambda: self.ui.brightness_spinBox.setValue(0)) + self.ui.reset_contrast_Button.clicked.connect(lambda: self.ui.contrast_spinBox.setValue(0)) + + # Connect UI elements to `on_ui_change` + self.ui.brightness_spinBox.valueChanged.connect(self.on_ui_change) + self.ui.brightness_Slider.valueChanged.connect(self.on_ui_change) + self.ui.contrast_spinBox.valueChanged.connect(self.on_ui_change) + self.ui.contrast_Slider.valueChanged.connect(self.on_ui_change) + self.ui.grayscale_checkBox.stateChanged.connect(self.on_ui_change) + self.ui_elements(False) + self.ui.show_OG_Button.pressed.connect(self.show_OG_image) + self.ui.show_OG_Button.released.connect(self.update_preview) + + def on_ui_change(self): + """Triggers update only if live update is enabled.""" + if self.ui.live_update.isChecked(): + self.update_preview() + + def browse_file(self): + file = QFileDialog.getOpenFileName(self, caption="Select File", filter="Images (*.png *.webp *.jpg *.jpeg)") + if file[0]: + self.ui.image_path_lineEdit.setText(file[0]) + self.update_preview() + self.ui_elements(True) + + def show_OG_image(self): + """Handles loading and displaying the image in a separate thread.""" + path = self.ui.image_path_lineEdit.text() + + worker = ImageProcessorWorker( + path = path, + optima_manager = self.o, + brightness = 0, + contrast = 0, + grayscale = False, + resize = self.ui.scale_Slider.value(), + callback = self.display_image # Callback to update UI + ) + self.threadpool.start(worker) + + def ui_elements(self, state): + self.ui.groupBox_2.setEnabled(state) + self.ui.groupBox.setEnabled(state) + self.ui.groupBox_5.setEnabled(state) + self.ui.show_OG_Button.setEnabled(state) + + def update_preview(self): + """Handles loading and displaying the image in a separate thread.""" + path = self.ui.image_path_lineEdit.text() + + worker = ImageProcessorWorker( + path = path, + optima_manager = self.o, + brightness = int(self.ui.brightness_spinBox.text()), + contrast = int(self.ui.contrast_spinBox.text()), + grayscale = self.ui.grayscale_checkBox.isChecked(), + resize = self.ui.scale_Slider.value(), + callback = self.display_image # Callback to update UI + ) + self.threadpool.start(worker) # Run worker in a thread + + def display_image(self, pixmap): + """Adjusts the image to fit within the QLabel.""" + if pixmap is None: + QMessageBox.warning(self, "Warning", "Error processing image...") + return + + max_size = self.ui.QLabel.size() + scaled_pixmap = pixmap.scaled(max_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) + self.ui.QLabel.setPixmap(scaled_pixmap) + self.ui.QLabel.resize(scaled_pixmap.size()) + + def resizeEvent(self, event): + """Triggered when the preview window is resized.""" + file_path = self.ui.image_path_lineEdit.text() + if os.path.exists(file_path): + self.update_preview() # Re-process and display the image + super().resizeEvent(event) # Keep the default behavior + + def close_window(self): + """Emits signal and closes the window.""" + if self.ui.checkBox.isChecked(): + self.values_selected.emit(self.ui.brightness_spinBox.value(), self.ui.contrast_spinBox.value(), self.ui.grayscale_checkBox.isChecked()) + self.close() + +class ImageProcessorWorker(QRunnable): + """Worker class to load and process the image in a separate thread.""" + # ChatGPT + def __init__(self, path, optima_manager, brightness, contrast, grayscale, resize, callback): + super().__init__() + self.path = path + self.optima_manager = optima_manager + self.brightness = brightness + self.contrast = contrast + self.grayscale = grayscale + self.resize = resize + self.callback = callback # Function to call when processing is done + + @Slot() + def run(self): + """Runs the image processing in a separate thread.""" + if not os.path.isfile(self.path): + self.callback(None) + return + + try: + img = self.optima_manager.process_image_object( + image_input_file = self.path, + watermark = "PREVIEW", + resize = self.resize, + grayscale = self.grayscale, + brightness = self.brightness, + contrast = self.contrast + ) + pixmap = QPixmap.fromImage(img) + self.callback(pixmap) + except Exception as e: + print(f"Error processing image: {e}") + self.callback(None) diff --git a/src/OptimaLab35/settingsWindow.py b/src/OptimaLab35/settingsWindow.py new file mode 100644 index 0000000..df5d494 --- /dev/null +++ b/src/OptimaLab35/settingsWindow.py @@ -0,0 +1,357 @@ +import sys +import os +from datetime import datetime +from PyPiUpdater import PyPiUpdater + +from OptimaLab35 import __version__ +from .const import ( + CONFIG_BASE_PATH +) + +from .ui import resources_rc +from .utils.utility import Utilities +from .ui.settings_window import Ui_Settings_Window + +from PySide6 import QtWidgets, QtCore + +from PySide6.QtCore import ( + QRegularExpression, + Qt, + QTimer +) + +from PySide6.QtWidgets import ( + QMessageBox, + QApplication, + QMainWindow +) + +from PySide6.QtGui import QIcon + +class SettingsWindow(QMainWindow, Ui_Settings_Window): + # Mixture of code by me, code/functions refactored by ChatGPT and code directly from ChatGPT + def __init__(self, optimalab35_localversion, optima35_localversion): + super(SettingsWindow, self).__init__() + self.ui = Ui_Settings_Window() + self.ui.setupUi(self) + self.u = Utilities(os.path.expanduser(CONFIG_BASE_PATH)) + self.app_settings = self.u.load_settings() + self.dev_mode = True if optimalab35_localversion == "0.0.1" else False + self.setWindowIcon(QIcon(":app-icon.png")) + + # Update log file location + self.update_log_file = os.path.expanduser(f"{CONFIG_BASE_PATH}/update_log.json") + # Store local versions + self.optimalab35_localversion = optimalab35_localversion + self.optima35_localversion = optima35_localversion + # Create PyPiUpdater instances + self.ppu_ol35 = PyPiUpdater("OptimaLab35", self.optimalab35_localversion, self.update_log_file) + self.ppu_o35 = PyPiUpdater("optima35", self.optima35_localversion, self.update_log_file) + self.ol35_last_state = self.ppu_ol35.get_last_state() + self.o35_last_state = self.ppu_o35.get_last_state() + # Track which packages need an update + self.updates_available = {"OptimaLab35": False, "optima35": False} + self.define_gui_interaction() + + def define_gui_interaction(self): + """Setup UI interactions.""" + # Updater related + self.ui.label_optimalab35_localversion.setText(self.optimalab35_localversion) + self.ui.label_optima35_localversion.setText(self.optima35_localversion) + + self.ui.label_latest_version.setText("Latest version") + self.ui.label_optimalab35_latestversion.setText("...") + self.ui.label_optima35_latestversion.setText("...") + + self.ui.update_and_restart_Button.setEnabled(False) + + # Connect buttons to functions + self.ui.check_for_update_Button.clicked.connect(self.check_for_updates) + self.ui.update_and_restart_Button.clicked.connect(self.update_and_restart) + self.ui.label_last_check.setText(f"Last check: {self.time_to_string(self.ol35_last_state[0])}") + self.ui.dev_widget.setVisible(False) + + # Timer for long press detection + self.timer = QTimer() + self.timer.setSingleShot(True) + self.timer.timeout.connect(self.toggle_dev_ui) + + # Connect button press/release + self.ui.check_for_update_Button.pressed.connect(self.start_long_press) + self.ui.check_for_update_Button.released.connect(self.cancel_long_press) + self.ui.label_5.setText('
  • Changelog
  • ') + self.ui.label_5.setOpenExternalLinks(True) + #settings related + self.load_settings_into_ui() + self.ui.reset_exif_Button.clicked.connect(self.ask_reset_exif) + self.ui.save_and_close_Button.clicked.connect(self.save_and_close) + self.ui.save_and_restart_Button.clicked.connect(self.save_and_restart) + + if os.name == "nt": # Disable restart app when windows. + self.ui.save_and_restart_Button.setVisible(False) + self.ui.restart_checkBox.setChecked(False) + self.ui.restart_checkBox.setVisible(False) + +# setting related + def load_settings_into_ui(self): + """Loads the settings into the UI elements.""" + settings = self.app_settings + theme_mode = settings["theme"]["mode"] + use_custom_theme = settings["theme"]["use_custom_theme"] + pkg_available = settings["theme"]["theme_pkg"] + + if pkg_available: + index = self.ui.theme_selection_comboBox.findText(theme_mode, QtCore.Qt.MatchFlag.MatchExactly) + if index != -1: + self.ui.theme_selection_comboBox.setCurrentIndex(index) + self.ui.enable_theme_checkBox.setChecked(use_custom_theme) + self.ui.install_pkg_Button.setVisible(False) + self.ui.enable_theme_checkBox.setEnabled(True) + else: + self.ui.enable_theme_checkBox.setEnabled(False) + self.ui.install_pkg_Button.clicked.connect(self.install_theme_pkg) + + def install_theme_pkg(self): + a = self.ppu_ol35.install_package("PyQtDarkTheme-fork") + self.ui.install_pkg_Button.setEnabled(False) + self.ui.install_pkg_Button.setText("Please wait...") + + msg_box = QMessageBox() + msg_box.setIcon(QMessageBox.Information) + msg_box.setWindowTitle("Message") + msg_box.setText(a[1]) + msg_box.setStandardButtons(QMessageBox.Ok) + msg_box.exec() + if a[0]: + self.app_settings["theme"]["theme_pkg"] = True + self.load_settings_into_ui() + else: + self.ui.install_pkg_Button.setEnabled(True) + self.ui.install_pkg_Button.setText("Try again?") + + def save_settings(self): + self.app_settings["theme"]["mode"] = self.ui.theme_selection_comboBox.currentText() + self.app_settings["theme"]["use_custom_theme"] = self.ui.enable_theme_checkBox.isChecked() + self.u.save_settings(self.app_settings) + + def save_and_close(self): + self.save_settings() + self.close() + + def save_and_restart(self): + msg = QMessageBox() + msg.setIcon(QMessageBox.Icon.Question) + msg.setWindowTitle("Confirm Reset") + msg.setText("Are you sure you want to restart the app?") + msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + + # Show the message box and wait for the user's response + response = msg.exec() + + # Check response and perform action + if response == QMessageBox.StandardButton.Yes: + self.save_settings() + self.restart_program() + else: + pass # Do nothing if "No" is selected + + def ask_reset_exif(self): + """Shows a dialog to ask the user if they are sure about resetting EXIF options to default.""" + # Create a QMessageBox with a Yes/No question + msg = QMessageBox() + msg.setIcon(QMessageBox.Icon.Question) + msg.setWindowTitle("Confirm Reset") + msg.setText("Are you sure you want to reset the EXIF options to default?") + msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + + # Show the message box and wait for the user's response + response = msg.exec() + + # Check response and perform action + if response == QMessageBox.StandardButton.Yes: + self.u.default_exif() # Reset EXIF options to default + else: + pass # Do nothing if "No" is selected + +# update related parts + def start_long_press(self): + """Start the timer when button is pressed.""" + # brave AI + self.timer.start(1000) # 1-second long press + + def cancel_long_press(self): + """Cancel long press if released early.""" + # brave AI + self.timer.stop() + + def toggle_dev_ui(self): + """Show or hide the hidden UI when long press is detected.""" + self.ui.dev_widget.setVisible(True) + + self.ui.check_local_Button.clicked.connect(self.local_check_for_updates) + self.ui.update_local_Button.clicked.connect(self.local_update) + + def local_check_for_updates(self): + dist_folder = os.path.expanduser("~/.config/OptimaLab35/dist/") + self.ui.label_optimalab35_latestversion.setText("Checking...") + self.ui.label_optima35_latestversion.setText("Checking...") + + # Check OptimaLab35 update + ol35_pkg_info = self.ppu_ol35.check_update_local(dist_folder) + if ol35_pkg_info[0] is None: + self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1][0:13]) + else: + self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1]) + self.updates_available["OptimaLab35"] = ol35_pkg_info[0] + + # Check optima35 update + o35_pkg_info = self.ppu_o35.check_update_local(dist_folder) + if o35_pkg_info[0] is None: + self.ui.label_optima35_latestversion.setText(o35_pkg_info[1][0:13]) + else: + self.ui.label_optima35_latestversion.setText(o35_pkg_info[1]) + self.updates_available["optima35"] = o35_pkg_info[0] + + def local_update(self): + dist_folder = os.path.expanduser("~/.config/OptimaLab35/dist/") + packages_to_update = [pkg for pkg, update in self.updates_available.items() if update] + + if not packages_to_update: + QMessageBox.information(self, "Update", "No updates available.") + return + + # Confirm update + msg = QMessageBox() + msg.setWindowTitle("Update Available") + message = f"Updating: {', '.join(packages_to_update)}\nUpdate " + + if self.ui.restart_checkBox.isChecked(): + message = message + "and restart app?" + else: + message = message + "app?" + + msg.setText(message) + msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + result = msg.exec() + + if result == QMessageBox.Yes: + update_results = [] # Store results + + for package in packages_to_update: + if package == "OptimaLab35": + pkg_info = self.ppu_ol35.update_from_local(dist_folder) + elif package == "optima35": + pkg_info = self.ppu_o35.update_from_local(dist_folder) + + update_results.append(f"{package}: {'Success' if pkg_info[0] else 'Failed'}\n{pkg_info[1]}") + + # Show summary of updates + # Show update completion message + msg = QMessageBox() + msg.setWindowTitle("Update Complete") + msg.setText("\n\n".join(update_results)) + msg.setStandardButtons(QMessageBox.Ok) + msg.exec() + + # Restart the application after user clicks "OK" + if self.ui.restart_checkBox.isChecked(): + self.restart_program() + + def time_to_string(self, time_time): + try: + dt_obj = datetime.fromtimestamp(time_time) + date_string = dt_obj.strftime("%d %h %H:%M") + return date_string + except TypeError: + return "Missing information" + + def check_for_updates(self): + """Check for updates and update the UI.""" + self.ui.check_for_update_Button.setEnabled(False) + self.ui.label_optimalab35_latestversion.setText("Checking...") + self.ui.label_optima35_latestversion.setText("Checking...") + + # Check OptimaLab35 update + ol35_pkg_info = self.ppu_ol35.check_for_update() + if ol35_pkg_info[0] is None: + self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1][0:13]) + else: + self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1]) + self.updates_available["OptimaLab35"] = ol35_pkg_info[0] + + # Check optima35 update + o35_pkg_info = self.ppu_o35.check_for_update() + if o35_pkg_info[0] is None: + self.ui.label_optima35_latestversion.setText(o35_pkg_info[1][0:13]) + else: + self.ui.label_optima35_latestversion.setText(o35_pkg_info[1]) + self.updates_available["optima35"] = o35_pkg_info[0] + + # Enable update button if any update is available + if any(self.updates_available.values()): + if self.dev_mode: + self.ui.update_and_restart_Button.setEnabled(False) + self.ui.update_and_restart_Button.setText("Update disabled") + else: + self.ui.update_and_restart_Button.setEnabled(True) + + last_date = self.time_to_string(self.ppu_ol35.get_last_state()[0]) + self.ui.label_last_check.setText(f"Last check: {last_date}") + self.ui.label_latest_version.setText("Online version") + self.ui.check_for_update_Button.setEnabled(True) + + def update_and_restart(self): + """Update selected packages and restart the application.""" + packages_to_update = [pkg for pkg, update in self.updates_available.items() if update] + + if not packages_to_update: + QMessageBox.information(self, "Update", "No updates available.") + return + + # Confirm update + msg = QMessageBox() + msg.setWindowTitle("Update Available") + message = f"Updating: {', '.join(packages_to_update)}\nUpdate " + + if self.ui.restart_checkBox.isChecked(): + message = message + "and restart app?" + else: + message = message + "app?" + + msg.setText(message) + msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + result = msg.exec() + + if result == QMessageBox.Yes: + update_results = [] # Store results + + for package in packages_to_update: + if package == "OptimaLab35": + pkg_info = self.ppu_ol35.update_package() + elif package == "optima35": + pkg_info = self.ppu_o35.update_package() + + update_results.append(f"{package}: {'Success' if pkg_info[0] else 'Failed'}\n{pkg_info[1]}") + + # Show summary of updates + # Show update completion message + msg = QMessageBox() + msg.setWindowTitle("Update Complete") + msg.setText("\n\n".join(update_results)) + msg.setStandardButtons(QMessageBox.Ok) + msg.exec() + + # Restart the application after user clicks "OK" + if self.ui.restart_checkBox.isChecked(): + self.restart_program() + + def restart_program(self): + """Restart the Python program after an update.""" + print("Restarting the application...") + # Close all running Qt windows before restarting + app = QApplication.instance() + if app: + app.quit() + + python = sys.executable + os.execl(python, python, *sys.argv) From 35a9f1af55ce43a00c564c1769f4755044ec17f7 Mon Sep 17 00:00:00 2001 From: "Mr. Finchum" Date: Sun, 23 Mar 2025 23:02:39 +0100 Subject: [PATCH 03/13] patch: patched formation of file. --- CHANGELOG.md | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ec2081..bcde410 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,72 +1,82 @@ # Changelog -### 1.2.x: Refactor -#### 1.2.0: Splitting Classes into Separate Files +--- + +## 1.2.x +### 1.2.0 Refactor: Splitting Classes into Separate Files (25.03.23) - Refactored `gui.py`, which previously contained almost all the code, into multiple files. - Each window's logic is now in its own file, improving code organization. - Window layouts remain in `.ui` folder, while their logic is now properly separated. ## 1.1.x -### 1.1.0: New Function in Preview Window +### 1.1.0 Feature: New Function in Preview Window (25.03.23) - Added a new feature to the preview window: **Hold a button to temporarily view the original (unedited) image.** This makes it easier to compare changes. - Minor UI adjustments. -## 1.0.x +--- + +## 1.0.x (25.03.06) ### 1.0.1: Fixed spelling - Fixes spelling some places ### 1.0.0: Fix version bump - Version was not bumped correctly -## 0.15.1: Final Polish (06.03.25) +--- + +## 0.15.1: Final Polish (25.03.06) - Fixed a bug where the GPS field being empty but selected caused issues. - EXIF insertion is now canceled if any image in the folder does not end with a number. - Minor GUI adjustments for a more polished experience. - Disabled preview adjustment controls until an image is loaded to prevent errors. -## 0.15.0: Preview Image Resizing Update (12.02.25) +## 0.15.0: Preview Image Resizing Update (25.02.12) - Added the ability to change the preview image size dynamically. - Previously, the image was processed and displayed at its original size, causing lag or unresponsiveness when adjusting the slider. - Reducing the processed image size should help improve performance. - Default preview image size is now **25%**, but users can adjust it between **10% and 100%**. +--- + ## 0.14.x -### 0.14.1: Patch changelog (12.02.25) +### 0.14.1: Patch changelog (25.02.12) - Updated the changelog to include missing entries. -### 0.14.0: Code refactor (by Mr. Finch) (11.02.25) +### 0.14.0: Code refactor (by Mr. Finch) (25.02.11) - Introduced constants and optimized the code. -## 0.13.x (11.02.25) +## 0.13.x (25.02.11) ### 0.13.1: Fixed image processing - Fixed a bug that made it impossible to process images. ### 0.13.0: Requirements file (by Mr. Finch) - Added a requirements file. +--- + ## 0.12.x -### 0.12.6: Disabled app restart on Windows (11.02.25) +### 0.12.6: Disabled app restart on Windows (25.02.11) - The app can not restart properly on Windows, so the restart button has been hidden when the OS is `nt`. - Also updated tool tip to indicate that changing theme requeres are restart. -### 0.12.5: Fixed EXIF File Generation Bug (10.02.25) +### 0.12.5: Fixed EXIF File Generation Bug (25.02.10) - Fixed a bug where the application failed to generate a new default EXIF file if none was available. Now, the file is correctly created when missing. -### 0.12.4: Updated README (10.02.25) +### 0.12.4: Updated README (25.02.10) - The README file (project description) now includes updates description and screenshots. -### 0.12.3: UI Adjustments (10.02.25) +### 0.12.3: UI Adjustments (25.02.10) - Minor changes to maintain a unified layout across all windows. - Added option to sync app theme with OS (if supported). - Set auto theme as the default value. -### 0.12.2: Minor UI Improvements for Theme Compatibility (10.02.25) +### 0.12.2: Minor UI Improvements for Theme Compatibility (25.02.10) - Fixed text clipping issues when using the new theme options. -### 0.12.1: Removed Unnecessary Debug Prints (09.02.25) +### 0.12.1: Removed Unnecessary Debug Prints (25.02.09) - Removed leftover debug statements. -### 0.12.0: New Settings Menu & Patches (09.02.25) +### 0.12.0: New Settings Menu & Patches (25.02.09) - **New Settings Window:** - The updater window has been reworked into a settings window. - **Initial settings (first tab) include:** @@ -79,9 +89,10 @@ - Fixed an issue where links in labels (About window) did not open a browser. - Added a changelog link in the About window. - Minor changes to `utility.py` to handle settings. + --- -## 0.11.x (05.02.25) +## 0.11.x (25.02.05) ### 0.11.1: Fixed pipeline - Fixed pipeline publish error @@ -93,7 +104,7 @@ --- -## 0.10.x (04.02.25) +## 0.10.x (25.02.04) ### 0.10.1: Fixed Updater - Fixed an issue where the updater was permanently disabled. From 7adea3084a7a2a31cbb9bc779e92c51b7404f32f Mon Sep 17 00:00:00 2001 From: "Mr. Finchum" Date: Sun, 23 Mar 2025 23:09:05 +0100 Subject: [PATCH 04/13] patch: Patched pyproject file. --- CHANGELOG.md | 7 +++++++ src/pyproject.toml | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcde410..1852db8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ --- ## 1.2.x +### 1.2.2 Patch: Pyproject file (25.03.23) +- Fixed `Development Status` Classifier +- Added <4.0 python version + +### 1.2.1 Patch: Changelog file (25.03.23) +- Patches formation in changelog file. + ### 1.2.0 Refactor: Splitting Classes into Separate Files (25.03.23) - Refactored `gui.py`, which previously contained almost all the code, into multiple files. - Each window's logic is now in its own file, improving code organization. diff --git a/src/pyproject.toml b/src/pyproject.toml index 0c07f29..ec4f716 100644 --- a/src/pyproject.toml +++ b/src/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] authors = [{ name = "Mr Finchum" }] description = "User interface for optima35." readme = "../pip_README.md" -requires-python = ">=3.8" +requires-python = ">=3.8, <4.0" dependencies = [ "optima35>=1.0.0, <2.0.0", "PyPiUpdater>=0.7.2, <1.0.0", @@ -16,7 +16,7 @@ dependencies = [ "PyYAML", ] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Environment :: X11 Applications :: Qt", "Topic :: Multimedia :: Graphics :: Editors", "Programming Language :: Python :: 3", From ef92ffb5b9e69e753cb289d061db2117e9cd6d2b Mon Sep 17 00:00:00 2001 From: "Mr. Finchum" Date: Mon, 24 Mar 2025 15:34:29 +0100 Subject: [PATCH 05/13] feat: image adjustments written to exif. --- CHANGELOG.md | 13 +++++++++---- src/OptimaLab35/mainWindow.py | 6 +++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1852db8..7bdbad5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,27 @@ # Changelog +## 1.3.x +### 1.3.0: Feature – Write Adjustments into EXIF +- Changes to contrast and brightness are now recorded in the EXIF comment section (labeled *Scanner* in the program). +- This allows users to track what adjustments were made to an image. + --- ## 1.2.x -### 1.2.2 Patch: Pyproject file (25.03.23) +### 1.2.2: Patch - Pyproject file (25.03.23) - Fixed `Development Status` Classifier - Added <4.0 python version -### 1.2.1 Patch: Changelog file (25.03.23) +### 1.2.1: Patch - Changelog file (25.03.23) - Patches formation in changelog file. -### 1.2.0 Refactor: Splitting Classes into Separate Files (25.03.23) +### 1.2.0: Refactor - Splitting Classes into Separate Files (25.03.23) - Refactored `gui.py`, which previously contained almost all the code, into multiple files. - Each window's logic is now in its own file, improving code organization. - Window layouts remain in `.ui` folder, while their logic is now properly separated. ## 1.1.x -### 1.1.0 Feature: New Function in Preview Window (25.03.23) +### 1.1.0: Feature - New Function in Preview Window (25.03.23) - Added a new feature to the preview window: **Hold a button to temporarily view the original (unedited) image.** This makes it easier to compare changes. - Minor UI adjustments. diff --git a/src/OptimaLab35/mainWindow.py b/src/OptimaLab35/mainWindow.py index 3006465..fb94e83 100644 --- a/src/OptimaLab35/mainWindow.py +++ b/src/OptimaLab35/mainWindow.py @@ -415,10 +415,14 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): user_data["lens"] = self.ui.lens_comboBox.currentText() user_data["iso"] = self.ui.iso_comboBox.currentText() user_data["image_description"] = self.ui.image_description_comboBox.currentText() - user_data["user_comment"] = self.ui.user_comment_comboBox.currentText() user_data["artist"] = self.ui.artist_comboBox.currentText() user_data["copyright_info"] = self.ui.copyright_info_comboBox.currentText() user_data["software"] = f"{self.name} {self.version} with {self.o.name} {self.o.version}" + if int(self.ui.contrast_spinBox.text()) != 0 or int(self.ui.brightness_spinBox.text()) != 0: + user_data["user_comment"] = f"{self.ui.user_comment_comboBox.currentText()}, contrast: {self.ui.contrast_spinBox.text()}, brightness: {self.ui.brightness_spinBox.text()}" + else: + user_data["user_comment"] = self.ui.user_comment_comboBox.currentText() + return user_data def get_selected_exif(self): From d356937e419d88fe4ce118a8e7f3a9b853593d87 Mon Sep 17 00:00:00 2001 From: Mr Finchum Date: Thu, 27 Mar 2025 11:07:58 +0000 Subject: [PATCH 06/13] fix: fixed insert exif feature --- CHANGELOG.md | 3 +++ src/OptimaLab35/mainWindow.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bdbad5..10fef5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## 1.3.x +### 1.3.1: Fix - Fixed insert exif not working +- Fixed the feature that inserted exif into images without modifying them. + ### 1.3.0: Feature – Write Adjustments into EXIF - Changes to contrast and brightness are now recorded in the EXIF comment section (labeled *Scanner* in the program). - This allows users to track what adjustments were made to an image. diff --git a/src/OptimaLab35/mainWindow.py b/src/OptimaLab35/mainWindow.py index fb94e83..ef8ceef 100644 --- a/src/OptimaLab35/mainWindow.py +++ b/src/OptimaLab35/mainWindow.py @@ -341,7 +341,7 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): def control_ending(self, name_lst): for item in name_lst: try: - int(item[-1]) + int(item[-5]) except ValueError: return False return True @@ -377,6 +377,7 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): return image_list = self.image_list_from_folder(self.settings["input_folder"]) + print(image_list) if not self.control_ending(image_list): QMessageBox.warning(self, "Warning", f"Error: one or more filenames do not end on a number.\nCan not adjust time") self.toggle_buttons(True) From 63422ccd88b60b72b545bad1b1dd9b0fecdd94f8 Mon Sep 17 00:00:00 2001 From: Mr Finchum Date: Sun, 30 Mar 2025 19:29:54 +0200 Subject: [PATCH 07/13] fix: fixed app path --- CHANGELOG.md | 3 +++ src/OptimaLab35/utils/utility.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10fef5e..c498256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## 1.3.x +### 1.3.2 Fix - Fixed problem with app folders +- Fixed a problem that the app folder path was not generated correctly. + ### 1.3.1: Fix - Fixed insert exif not working - Fixed the feature that inserted exif into images without modifying them. diff --git a/src/OptimaLab35/utils/utility.py b/src/OptimaLab35/utils/utility.py index 75726a0..3f71698 100644 --- a/src/OptimaLab35/utils/utility.py +++ b/src/OptimaLab35/utils/utility.py @@ -3,7 +3,7 @@ import os class Utilities: def __init__(self, app_folder_path): - self.folder_path = app_folder_path + self.folder_path = os.path.expanduser(app_folder_path) self._ensure_program_folder_exists() self.exif_path = os.path.expanduser(f"{app_folder_path}/exif.yaml") self.settings_path = os.path.expanduser(f"{app_folder_path}/settings.yaml") From 419dc1eada320b972fc01c2b0671f33266fbc8f8 Mon Sep 17 00:00:00 2001 From: Mr Finchum Date: Tue, 1 Apr 2025 16:30:21 +0000 Subject: [PATCH 08/13] patch: better performance for preview window --- .gitignore | 1 + CHANGELOG.md | 12 +++++++++--- src/OptimaLab35/previewWindow.py | 9 ++++----- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 7a56971..e8383c6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist/ __pycache__/ .flatpak-builder/ flatpak-build-dir/ +*.jpg diff --git a/CHANGELOG.md b/CHANGELOG.md index c498256..77ca8c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,19 @@ # Changelog ## 1.3.x -### 1.3.2 Fix - Fixed problem with app folders +### 1.3.3: Patch – Increased Preview Performance x2 +- Reduced preview stutter: Previously, the image was updated twice when changing brightness or contrast. Now, it updates only once, improving performance by 100%. +- There is still room for improvement. Analysis shows that image processing takes the most time, while displaying in Qt is relatively fast. Reducing the image size impacts performance, so resizing to 50% is a good idea. +- There is an issue where `QRunner` does not always finish in the correct order when brightness or contrast values are changed rapidly. ATM I do not know how to fix this easly. +- The "Preview" watermark now displays the brightness and contrast levels of the shown image. + +### 1.3.2 Fix - Fixed problem with app folders (25.03.30) - Fixed a problem that the app folder path was not generated correctly. -### 1.3.1: Fix - Fixed insert exif not working +### 1.3.1: Fix - Fixed insert exif not working (25.03.27) - Fixed the feature that inserted exif into images without modifying them. -### 1.3.0: Feature – Write Adjustments into EXIF +### 1.3.0: Feature – Write Adjustments into EXIF (25.03.24) - Changes to contrast and brightness are now recorded in the EXIF comment section (labeled *Scanner* in the program). - This allows users to track what adjustments were made to an image. diff --git a/src/OptimaLab35/previewWindow.py b/src/OptimaLab35/previewWindow.py index 72469be..8f0a656 100644 --- a/src/OptimaLab35/previewWindow.py +++ b/src/OptimaLab35/previewWindow.py @@ -50,10 +50,8 @@ class PreviewWindow(QMainWindow, Ui_Preview_Window): self.ui.reset_contrast_Button.clicked.connect(lambda: self.ui.contrast_spinBox.setValue(0)) # Connect UI elements to `on_ui_change` - self.ui.brightness_spinBox.valueChanged.connect(self.on_ui_change) - self.ui.brightness_Slider.valueChanged.connect(self.on_ui_change) - self.ui.contrast_spinBox.valueChanged.connect(self.on_ui_change) - self.ui.contrast_Slider.valueChanged.connect(self.on_ui_change) + self.ui.brightness_spinBox.valueChanged.connect(self.on_ui_change) # brightness slider changes spinbox value, do not need an event for the slider + self.ui.contrast_spinBox.valueChanged.connect(self.on_ui_change) # contrast slider changes spinbox value, do not need an event for the slider self.ui.grayscale_checkBox.stateChanged.connect(self.on_ui_change) self.ui_elements(False) self.ui.show_OG_Button.pressed.connect(self.show_OG_image) @@ -154,7 +152,8 @@ class ImageProcessorWorker(QRunnable): try: img = self.optima_manager.process_image_object( image_input_file = self.path, - watermark = "PREVIEW", + watermark = f"PREVIEW B:{self.brightness} S:{self.contrast}", + font_size = 1, resize = self.resize, grayscale = self.grayscale, brightness = self.brightness, From d4e8a25b1e53febfab8919edf96a9358cde75920 Mon Sep 17 00:00:00 2001 From: Mr Finchum Date: Tue, 1 Apr 2025 18:37:11 +0200 Subject: [PATCH 09/13] fix: fixed misspelling --- CHANGELOG.md | 7 +++++-- src/OptimaLab35/previewWindow.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77ca8c9..eec392a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,16 @@ # Changelog ## 1.3.x -### 1.3.3: Patch – Increased Preview Performance x2 +### 1.3.4: Fix - Spelling (25.04.01) +- Fixed misspelling in preview window. + +### 1.3.3: Patch – Increased Preview Performance x2 (25.04.01) - Reduced preview stutter: Previously, the image was updated twice when changing brightness or contrast. Now, it updates only once, improving performance by 100%. - There is still room for improvement. Analysis shows that image processing takes the most time, while displaying in Qt is relatively fast. Reducing the image size impacts performance, so resizing to 50% is a good idea. - There is an issue where `QRunner` does not always finish in the correct order when brightness or contrast values are changed rapidly. ATM I do not know how to fix this easly. - The "Preview" watermark now displays the brightness and contrast levels of the shown image. -### 1.3.2 Fix - Fixed problem with app folders (25.03.30) +### 1.3.2: Fix - Fixed problem with app folders (25.03.30) - Fixed a problem that the app folder path was not generated correctly. ### 1.3.1: Fix - Fixed insert exif not working (25.03.27) diff --git a/src/OptimaLab35/previewWindow.py b/src/OptimaLab35/previewWindow.py index 8f0a656..82161d2 100644 --- a/src/OptimaLab35/previewWindow.py +++ b/src/OptimaLab35/previewWindow.py @@ -152,7 +152,7 @@ class ImageProcessorWorker(QRunnable): try: img = self.optima_manager.process_image_object( image_input_file = self.path, - watermark = f"PREVIEW B:{self.brightness} S:{self.contrast}", + watermark = f"PREVIEW B:{self.brightness} C:{self.contrast}", font_size = 1, resize = self.resize, grayscale = self.grayscale, From 011249e00295a0a7b3d997f7e7a766df6e3069ef Mon Sep 17 00:00:00 2001 From: Mr Finchum Date: Fri, 11 Apr 2025 13:34:29 +0200 Subject: [PATCH 10/13] ci: switch to woodpecker ci. --- .woodpecker/woodpecker_ci.yml | 92 +++++++++++++++++++++++++++++++++++ README.md | 24 ++++----- pip_README.md | 4 +- 3 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 .woodpecker/woodpecker_ci.yml diff --git a/.woodpecker/woodpecker_ci.yml b/.woodpecker/woodpecker_ci.yml new file mode 100644 index 0000000..6d0cfd5 --- /dev/null +++ b/.woodpecker/woodpecker_ci.yml @@ -0,0 +1,92 @@ +steps: + - name: gitversion + depends_on: [] # nothing start emititly + when: + event: push + branch: main + image: mcr.microsoft.com/dotnet/sdk:9.0 + environment: + CI_TOKEN: + from_secret: CI_TOKEN + commands: + - git remote set-url origin https://CodeByMrFinchum:$CI_TOKEN@code.boxyfoxy.net/$CI_REPO.git + - git fetch --unshallow --tags + - apt-get update && apt-get install -y jq + - dotnet tool install --global GitVersion.Tool --version 5.* + - export PATH="$PATH:/root/.dotnet/tools" + - dotnet-gitversion -output json > version.json + - ls + - cat version.json + - | + echo "GitVersion_SemVer=$(jq -r '.SemVer' version.json)" >> gitversion.env + echo "GitVersion_LegacySemVer=$(jq -r '.LegacySemVer' version.json)" >> gitversion.env + echo "GitVersion_FullSemVer=$(jq -r '.FullSemVer' version.json)" >> gitversion.env + echo "GitVersion_Major=$(jq -r '.Major' version.json)" >> gitversion.env + echo "GitVersion_Minor=$(jq -r '.Minor' version.json)" >> gitversion.env + echo "GitVersion_Patch=$(jq -r '.Patch' version.json)" >> gitversion.env + echo "GitVersion_MajorMinorPatch=$(jq -r '.MajorMinorPatch' version.json)" >> gitversion.env + echo "GitVersion_BuildMetaData=$(jq -r '.BuildMetaData' version.json)" >> gitversion.env + + - name: tagging + depends_on: [gitversion] + when: + event: push + branch: main + image: alpine/git + environment: + CI_TOKEN: + from_secret: CI_TOKEN + commands: + - ls + - cat gitversion.env + - git config --global user.email "ci@noreply.boxyfoxy.net" + - git config --global user.name "CI Bot" + - git remote set-url origin https://CodeByMrFinchum:$${CI_TOKEN}@code.boxyfoxy.net/$${CI_REPO}.git + - . gitversion.env + - git tag $GitVersion_SemVer + - git push origin tag $GitVersion_SemVer + + - name: build + depends_on: [gitversion, tagging] + when: + event: push + branch: main + image: python:3.9.21 + commands: + - ls + - cat gitversion.env + - export $(cat gitversion.env | xargs) + - sed -i "s/0.0.1/$GitVersion_SemVer/" src/OptimaLab35/__init__.py + - cat src/OptimaLab35/__init__.py + - python3 -m pip install build + - python3 -m build --wheel --sdist -s src + + - name: publish_pypi + depends_on: [gitversion, tagging, build] + when: + event: push + branch: main + image: python:3.9.21 + environment: + TWINE_PASSWORD: + from_secret: TWINE_API + TWINE_USERNAME: "__token__" + commands: + - ls + - python3 -m pip install twine + - python3 -m twine upload dist/* + + - name: publish_forgejo + depends_on: [gitversion, tagging, build] + when: + event: push + branch: main + image: python:3.9.21 + environment: + TWINE_PASSWORD: + from_secret: PKG_TOKEN + TWINE_USERNAME: "CodeByMrFinchum" + commands: + - ls + - python3 -m pip install twine + - python3 -m twine upload --repository-url https://code.boxyfoxy.net/api/packages/CodeByMrFinchum/pypi dist/* diff --git a/README.md b/README.md index 4aae1cc..22efff4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # **OptimaLab35** -Last updated: 06 Mar 2025 (v.1.0.0) +Developed on my [forgejo instance](https://code.boxyfoxy.net/CodeByMrFinchum), [GitLab](https://gitlab.com/CodeByMrFinchum) is used as backup. ## **Overview** **OptimaLab35** enhances **OPTIMA35** (**Organizing, Processing, Tweaking Images, and Modifying scanned Analogs from 35mm Film**) by offering a user-friendly graphical interface for efficient image and metadata management. -It serves as a GUI for the [OPTIMA35 library](https://gitlab.com/CodeByMrFinchum/optima35), providing an intuitive way to interact with the core functionalities. +It serves as a GUI for the [optima35 library](https://code.boxyfoxy.net/CodeByMrFinchum/optima35), providing an intuitive way to interact with the core functionalities. --- @@ -55,28 +55,28 @@ The UI is OS-dependent, but a custom theme can be enabled in the settings. **Main tab** -![main](https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/mainwindow_light.png){width=40%} -![main](https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/mainwindow_dark.png){width=40%} +![main](https://code.boxyfoxy.net/CodeByMrFinchum/OptimaLab35/raw/branch/main/media/mainwindow_light.png){width=40%} +![main](https://code.boxyfoxy.net/CodeByMrFinchum/OptimaLab35/raw/branch/main/media/mainwindow_dark.png){width=40%} **Exif tab** -![main](https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/mainwindow_exif_light.png){width=40%} -![main](https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/mainwindow_exif_dark.png){width=40%} +![main](https://code.boxyfoxy.net/CodeByMrFinchum/OptimaLab35/raw/branch/main/media/mainwindow_exif_light.png){width=40%} +![main](https://code.boxyfoxy.net/CodeByMrFinchum/OptimaLab35/raw/branch/main/media/mainwindow_exif_dark.png){width=40%} **Preview window** -![main](https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/previewwindow_light.png){width=40%} -![main](https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/previewwindow_dark.png){width=40%} +![main](https://code.boxyfoxy.net/CodeByMrFinchum/OptimaLab35/raw/branch/main/media/previewwindow_light.png){width=40%} +![main](https://code.boxyfoxy.net/CodeByMrFinchum/OptimaLab35/raw/branch/main/media/previewwindow_dark.png){width=40%} **Settings** -![main](https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/settingswindow_light.png){width=40%} -![main](https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/settingswindow_dark.png){width=40%} +![main](https://code.boxyfoxy.net/CodeByMrFinchum/OptimaLab35/raw/branch/main/media/settingswindow_light.png){width=40%} +![main](https://code.boxyfoxy.net/CodeByMrFinchum/OptimaLab35/raw/branch/main/media/settingswindow_dark.png){width=40%} **Updater** -![main](https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/settingswindow_updater_light.png){width=40%} -![main](https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/settingswindow_updater_dark.png){width=40%} +![main](https://code.boxyfoxy.net/CodeByMrFinchum/OptimaLab35/raw/branch/main/media/settingswindow_updater_light.png){width=40%} +![main](https://code.boxyfoxy.net/CodeByMrFinchum/OptimaLab35/raw/branch/main/media/settingswindow_updater_dark.png){width=40%} --- diff --git a/pip_README.md b/pip_README.md index 7b3f576..576c8bf 100644 --- a/pip_README.md +++ b/pip_README.md @@ -1,4 +1,6 @@ -**[OptimaLab35](https://gitlab.com/CodeByMrFinchum/OptimaLab35)** is a graphical interface for the [OPTIMA35 library](https://gitlab.com/CodeByMrFinchum/optima35), designed for efficient image and metadata management. +**[OptimaLab35](https://code.boxyfoxy.net/CodeByMrFinchum/OptimaLab35)** is a graphical interface for the [optima35 library](https://code.boxyfoxy.net/CodeByMrFinchum/optima35), designed for efficient image and metadata management. + +Developed on my [forgejo instance](https://code.boxyfoxy.net/CodeByMrFinchum), [GitLab](https://gitlab.com/CodeByMrFinchum) is used as backup. ### **Features** - Resize, adjust brightness/contrast, and convert images to grayscale From d93d3de8593aa6688e974613100676d4da694043 Mon Sep 17 00:00:00 2001 From: Mr Finchum Date: Fri, 11 Apr 2025 13:41:24 +0200 Subject: [PATCH 11/13] fix: fixing woodpecker ci (#2) Reviewed-on: https://code.boxyfoxy.net/CodeByMrFinchum/OptimaLab35/pulls/2 Co-authored-by: Mr Finchum Co-committed-by: Mr Finchum --- .gitlab-ci.yml | 69 ---------------------------- .gitlab-ci/git/create_tag.yml | 15 ------ .gitlab-ci/versioning/gitversion.yml | 31 ------------- .woodpecker/woodpecker_ci.yml | 4 +- 4 files changed, 2 insertions(+), 117 deletions(-) delete mode 100644 .gitlab-ci.yml delete mode 100644 .gitlab-ci/git/create_tag.yml delete mode 100644 .gitlab-ci/versioning/gitversion.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index ba2d7d3..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,69 +0,0 @@ ---- -include: - - local: .gitlab-ci/versioning/gitversion.yml - - local: .gitlab-ci/git/create_tag.yml - -stages: - - build - - release - -gitversion: - extends: .versioning:gitversion - stage: .pre - tags: - - gitlab-org-docker - rules: - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch - -build: - stage: build - image: python:3.9.21 - tags: - - gitlab-org-docker - rules: - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch - needs: - - job: gitversion - artifacts: true - script: - - sed -i "s/0.0.1/${GitVersion_MajorMinorPatch}/" src/OptimaLab35/__init__.py - - cat src/OptimaLab35/__init__.py - - python3 -m pip install build - - python3 -m build --wheel --sdist -s src - artifacts: - paths: - - src/dist/* - expire_in: 1 day - -publish: - stage: release - image: python:3.9.21 - tags: - - gitlab-org-docker - rules: - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch - variables: - TWINE_USERNAME: "__token__" - TWINE_PASSWORD: $TWINE_API - needs: - - job: build - artifacts: true - script: - - python3 -m pip install twine - - python3 -m twine upload src/dist/* - -create_tag: - extends: .git:create_tag - stage: release - tags: - - gitlab-org-docker - variables: - VERSION: $GitVersion_SemVer - TOKEN: $GITLAB_TOKEN - needs: - - job: gitversion - artifacts: true - rules: - - if: $CI_COMMIT_TAG - when: never # Do not run this job when a tag is created manually - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch diff --git a/.gitlab-ci/git/create_tag.yml b/.gitlab-ci/git/create_tag.yml deleted file mode 100644 index 2c1afd7..0000000 --- a/.gitlab-ci/git/create_tag.yml +++ /dev/null @@ -1,15 +0,0 @@ ---- - -.git:create_tag: - image: alpine:3.21 - variables: - GIT_STRATEGY: clone - GIT_DEPTH: 0 - GIT_LFS_SKIP_SMUDGE: 1 - VERSION: '' - TOKEN: '' # Token with push privileges - script: - - apk add git - - git remote set-url origin https://oauth2:$TOKEN@$CI_SERVER_HOST/$CI_PROJECT_PATH - - git tag $VERSION - - git push origin tag $VERSION diff --git a/.gitlab-ci/versioning/gitversion.yml b/.gitlab-ci/versioning/gitversion.yml deleted file mode 100644 index dbbc149..0000000 --- a/.gitlab-ci/versioning/gitversion.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -.versioning:gitversion: - image: - name: mcr.microsoft.com/dotnet/sdk:9.0 - variables: - GIT_STRATEGY: clone - GIT_DEPTH: 0 # force a deep/non-shallow fetch need by gitversion - GIT_LFS_SKIP_SMUDGE: 1 - cache: [] # caches and before / after scripts can mess things up - script: - - | - dotnet tool install --global GitVersion.Tool --version 5.* - export PATH="$PATH:/root/.dotnet/tools" - - dotnet-gitversion -output buildserver - - # We could just collect the output file gitversion.properties (with artifacts:report:dotenv: gitversion.properties as it is already in DOTENV format, - # however it contains ~33 variables which unnecessarily consumes many of the 50 max DOTENV variables of the free GitLab version. - # Limits are higher for licensed editions, see https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportsdotenv - grep 'GitVersion_LegacySemVer=' gitversion.properties >> gitversion.env - grep 'GitVersion_SemVer=' gitversion.properties >> gitversion.env - grep 'GitVersion_FullSemVer=' gitversion.properties >> gitversion.env - grep 'GitVersion_Major=' gitversion.properties >> gitversion.env - grep 'GitVersion_Minor=' gitversion.properties >> gitversion.env - grep 'GitVersion_Patch=' gitversion.properties >> gitversion.env - grep 'GitVersion_MajorMinorPatch=' gitversion.properties >> gitversion.env - grep 'GitVersion_BuildMetaData=' gitversion.properties >> gitversion.env - artifacts: - reports: - # propagates variables into the pipeline level - dotenv: gitversion.env diff --git a/.woodpecker/woodpecker_ci.yml b/.woodpecker/woodpecker_ci.yml index 6d0cfd5..ba24276 100644 --- a/.woodpecker/woodpecker_ci.yml +++ b/.woodpecker/woodpecker_ci.yml @@ -74,7 +74,7 @@ steps: commands: - ls - python3 -m pip install twine - - python3 -m twine upload dist/* + - python3 -m twine upload src/dist/* - name: publish_forgejo depends_on: [gitversion, tagging, build] @@ -89,4 +89,4 @@ steps: commands: - ls - python3 -m pip install twine - - python3 -m twine upload --repository-url https://code.boxyfoxy.net/api/packages/CodeByMrFinchum/pypi dist/* + - python3 -m twine upload --repository-url https://code.boxyfoxy.net/api/packages/CodeByMrFinchum/pypi src/dist/* From a89f43ba2e8a409ee8518fc355ebcbe3a717f575 Mon Sep 17 00:00:00 2001 From: Mr Finchum Date: Fri, 11 Apr 2025 15:57:58 +0200 Subject: [PATCH 12/13] fix: fixed linkage and changelog --- CHANGELOG.md | 18 +++++++++++++++--- src/OptimaLab35/mainWindow.py | 2 +- src/OptimaLab35/settingsWindow.py | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eec392a..2996353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,22 @@ # Changelog +## 1.4.x +### 1.4.2: Fix links +- Fixed that changelog was linked to GitLab, not it is to code.boxyfoxy.net +- Fixed Changelog + +### 1.4.1: Fix CI +- Fixed pipline +### 1.4.0: New CI +- Migrated repo from GitLab to my forgejo instance, therefore switching to woodpecker CI + +--- + ## 1.3.x ### 1.3.4: Fix - Spelling (25.04.01) - Fixed misspelling in preview window. -### 1.3.3: Patch – Increased Preview Performance x2 (25.04.01) +### 1.3.3: Patch - Increased Preview Performance x2 (25.04.01) - Reduced preview stutter: Previously, the image was updated twice when changing brightness or contrast. Now, it updates only once, improving performance by 100%. - There is still room for improvement. Analysis shows that image processing takes the most time, while displaying in Qt is relatively fast. Reducing the image size impacts performance, so resizing to 50% is a good idea. - There is an issue where `QRunner` does not always finish in the correct order when brightness or contrast values are changed rapidly. ATM I do not know how to fix this easly. @@ -16,7 +28,7 @@ ### 1.3.1: Fix - Fixed insert exif not working (25.03.27) - Fixed the feature that inserted exif into images without modifying them. -### 1.3.0: Feature – Write Adjustments into EXIF (25.03.24) +### 1.3.0: Feature - Write Adjustments into EXIF (25.03.24) - Changes to contrast and brightness are now recorded in the EXIF comment section (labeled *Scanner* in the program). - This allows users to track what adjustments were made to an image. @@ -169,7 +181,7 @@ - Improved error handling for updater: now displays the specific error message instead of just **"error"** when an issue occurs during update checks. - Ensured all child windows close when the main window is closed. -### 0.8.3: Fix – OptimaLab35 Not Closing After Update +### 0.8.3: Fix - OptimaLab35 Not Closing After Update - Fixed an issue where **OptimaLab35** would not close properly when updating, resulting in an unresponsive instance and multiple running processes. ### 0.8.2: Patch for New PyPiUpdater Version diff --git a/src/OptimaLab35/mainWindow.py b/src/OptimaLab35/mainWindow.py index ef8ceef..f0bc614 100644 --- a/src/OptimaLab35/mainWindow.py +++ b/src/OptimaLab35/mainWindow.py @@ -130,7 +130,7 @@ class OptimaLab35(QMainWindow, Ui_MainWindow):

    For more details, visit:

    diff --git a/src/OptimaLab35/settingsWindow.py b/src/OptimaLab35/settingsWindow.py index df5d494..ffbd263 100644 --- a/src/OptimaLab35/settingsWindow.py +++ b/src/OptimaLab35/settingsWindow.py @@ -79,7 +79,7 @@ class SettingsWindow(QMainWindow, Ui_Settings_Window): # Connect button press/release self.ui.check_for_update_Button.pressed.connect(self.start_long_press) self.ui.check_for_update_Button.released.connect(self.cancel_long_press) - self.ui.label_5.setText('
  • Changelog
  • ') + self.ui.label_5.setText('
  • Changelog
  • ') self.ui.label_5.setOpenExternalLinks(True) #settings related self.load_settings_into_ui() From 894d444f914a3bbf85d901a350343a5dcdccb0ff Mon Sep 17 00:00:00 2001 From: Mr Finchum Date: Fri, 30 May 2025 12:17:12 +0200 Subject: [PATCH 13/13] feat: dateEdit is now, instead of static day. --- CHANGELOG.md | 3 +++ src/OptimaLab35/mainWindow.py | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2996353..a9b2781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # Changelog +## 1.5.x +### 1.5.0: Feature - Time of dateEdit now today +- Changes that instead of the dateEdit elements being always set to a last day of 2024 it is the current day. ## 1.4.x ### 1.4.2: Fix links diff --git a/src/OptimaLab35/mainWindow.py b/src/OptimaLab35/mainWindow.py index f0bc614..c299689 100644 --- a/src/OptimaLab35/mainWindow.py +++ b/src/OptimaLab35/mainWindow.py @@ -25,7 +25,8 @@ from PySide6.QtCore import ( Signal, QObject, QRegularExpression, - Qt + Qt, + QDate ) from PySide6.QtWidgets import ( @@ -96,7 +97,7 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): validator = QRegularExpressionValidator(regex) self.ui.lat_lineEdit.setValidator(validator) self.ui.long_lineEdit.setValidator(validator) - + self.ui.dateEdit.setDate(QDate.currentDate()) # UI related function, changing parts, open, etc. def open_preview_window(self): self.preview_window = PreviewWindow()