diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 558cdf6..5d6f1d6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -93,7 +93,7 @@ jobs: run: | make build-macos brew install create-dmg - create-dmg --volname "Gotify Tray" --app-drop-link 0 0 --no-internet-enable "gotify-tray.dmg" "./dist/Gotify-Tray.app" + create-dmg --volname "Gotify Tray" --app-drop-link 0 0 --no-internet-enable "gotify-tray.dmg" "./dist/Gotify Tray.app" - name: Upload artifact uses: actions/upload-artifact@v2 with: diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 3f03028..4f7020d 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -74,7 +74,7 @@ jobs: run: | make build-macos brew install create-dmg - create-dmg --volname "Gotify Tray" --app-drop-link 0 0 --no-internet-enable "gotify-tray.dmg" "./dist/Gotify-Tray.app" + create-dmg --volname "Gotify Tray" --app-drop-link 0 0 --no-internet-enable "gotify-tray.dmg" "./dist/Gotify Tray.app" - name: Upload artifact uses: actions/upload-artifact@v2 with: diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 0000000..0cf9c92 --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,64 @@ +## Get the source and install the requirements: + +```shell +$ git clone https://github.com/seird/gotify-tray.git +$ cd gotify-tray +$ pip install -r requirements.txt +``` + + +### Run from source + +```shell +$ python -m gotify_tray +``` + +### Create a pyinstaller executable + +```shell +$ pip install pyinstaller +$ pyinstaller gotify-tray.spec +``` +An executable is created at `dist/gotify-tray/`. + +### Create a macos .app + +```shell +$ pip install pyinstaller Pillow +$ pyinstaller gotify-tray.spec +``` + +### Inno setup (Windows) + +Create an installer for windows with inno setup from pyinstaller output: + +```shell +$ iscc gotify-tray.iss +``` + +### Create and install a pip package + +- Create the pip package: + ```shell + $ python -m build + ``` + +- Install the pip package: + ```shell + $ pip install dist/gotify_tray-0.1.14-py3-none-any.whl + ``` + +- Launch: + ```shell + $ gotify-tray + ``` + +### Create a deb package + +```shell +$ make build + +# or install + +$ sudo make install +``` \ No newline at end of file diff --git a/Makefile b/Makefile index 6630b98..ed766d7 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ build-macos: clean pip install -r requirements.txt pip install pyinstaller pip install Pillow - pyinstaller gotify-tray-macos.spec + pyinstaller gotify-tray.spec install: build sudo dpkg -i dist/gotify-tray_amd64.deb diff --git a/README.md b/README.md index ea06673..0e3d7ad 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,21 @@ A tray notification application for receiving messages from a [Gotify server](https://github.com/gotify/server). -## Download +## Getting started -[Download the latest release.](https://github.com/seird/gotify-tray/releases/latest) +- [Download the latest release.](https://github.com/seird/gotify-tray/releases/latest) -or, install via pip: -``` -$ pip install gotify-tray -``` +- or, install via pip: + ```shell + $ pip install gotify-tray + ``` + +- or, run from source: + ```shell + $ pip install -r requirements.txt + $ python -m gotify_tray + ``` ## Features @@ -41,77 +47,11 @@ Windows 10 | KDE ![settings](https://raw.githubusercontent.com/seird/gotify-tray/master/images/settings.png) -## Manual Installation +## Build instructions -Get the source and install the requirements: - -``` -$ git clone https://github.com/seird/gotify-tray.git -$ cd gotify-tray -$ pip install -r requirements.txt -``` - - -### Run from source - -``` -$ python -m gotify_tray -``` - -### Create a pyinstaller executable - -``` -$ pip install pyinstaller -$ pyinstaller gotify-tray.spec -``` -An executable is created at `dist/gotify-tray/`. - -### Create a macos .app - -``` -$ pip install pyinstaller Pillow -$ pyinstaller gotify-tray-macos.spec -``` - -### Inno setup (Windows) - -Create an installer for windows with inno setup from pyinstaller output: - -``` -$ iscc gotify-tray.iss -``` - -### Create and install a pip package - -- Create the pip package: - ``` - $ python -m build - ``` - -- Install the pip package: - ``` - $ pip install dist/gotify_tray-0.1.14-py3-none-any.whl - ``` - -- Launch: - ``` - $ gotify-tray - ``` - -### Create a deb package - -``` -$ make build - -# or install - -$ sudo make install -``` +See [BUILDING](BUILDING.md). ## Requirements - python >=3.8 -- PyQt6 -- requests -- websocket-client diff --git a/debian/DEBIAN/control b/debian/DEBIAN/control index aeec92e..c7df3f9 100644 --- a/debian/DEBIAN/control +++ b/debian/DEBIAN/control @@ -2,5 +2,5 @@ Package: gotify-tray Version: 0.1.14 Architecture: amd64 Maintainer: k.dries@protonmail.com -Description: gotify-tray +Description: Gotify Tray A tray notification application for receiving messages from a Gotify server. diff --git a/debian/usr/share/applications/gotifytray.desktop b/debian/usr/share/applications/gotifytray.desktop index 81ad25b..c98aaff 100644 --- a/debian/usr/share/applications/gotifytray.desktop +++ b/debian/usr/share/applications/gotifytray.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Name=gotify-tray +Name=Gotify Tray Comment=A tray notification application for receiving messages from a Gotify server. Path=/usr/lib/gotify-tray Exec=/usr/lib/gotify-tray/gotify-tray diff --git a/gotify-tray-macos.spec b/gotify-tray-macos.spec deleted file mode 100644 index 7821b50..0000000 --- a/gotify-tray-macos.spec +++ /dev/null @@ -1,41 +0,0 @@ -# -*- mode: python -*- - -block_cipher = None - -a = Analysis(['gotify_tray/__main__.py'], - pathex=[os.getcwd()], - binaries=[], - datas=[('gotify_tray/gui/images', 'gotify_tray/gui/images')], - hiddenimports=[], - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False) -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) -exe = EXE(pyz, - a.scripts, - [], - exclude_binaries=True, - name='gotify-tray', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - console=False, - version='version.py', - icon='logo.ico') -coll = COLLECT(exe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, - name='gotify-tray') -app = BUNDLE(coll, - name='Gotify-Tray.app', - icon='logo.ico', - bundle_identifier=None) diff --git a/gotify-tray.spec b/gotify-tray.spec index fd27d06..60b6a66 100644 --- a/gotify-tray.spec +++ b/gotify-tray.spec @@ -1,5 +1,7 @@ # -*- mode: python -*- +import platform + block_cipher = None a = Analysis(['gotify_tray/__main__.py'], @@ -35,3 +37,9 @@ coll = COLLECT(exe, strip=False, upx=True, name='gotify-tray') + +if platform.system() == "Darwin": + app = BUNDLE(coll, + name='Gotify Tray.app', + icon='logo.ico', + bundle_identifier=None) diff --git a/gotify_tray/database/default_settings.py b/gotify_tray/database/default_settings.py index 6ce350d..2c8b65a 100644 --- a/gotify_tray/database/default_settings.py +++ b/gotify_tray/database/default_settings.py @@ -14,9 +14,14 @@ DEFAULT_SETTINGS = { "tray/notifications/priority": 5, "tray/notifications/duration_ms": 5000, "tray/notifications/icon/show": True, + "tray/notifications/click": True, "watchdog/interval/s": 60, "MessageWidget/image/size": 33, "MainWindow/label/size": 25, "MainWindow/button/size": 33, "MainWindow/application/icon/size": 40, + "ImagePopup/enabled": False, + "ImagePopup/extensions": [".jpg", ".jpeg", ".png", ".svg"], + "ImagePopup/w": 400, + "ImagePopup/h": 400, } diff --git a/gotify_tray/gui/MainApplication.py b/gotify_tray/gui/MainApplication.py index a3107c9..d5651b6 100644 --- a/gotify_tray/gui/MainApplication.py +++ b/gotify_tray/gui/MainApplication.py @@ -31,7 +31,7 @@ from .models import ( MessagesModelItem, MessageItemDataRole, ) -from .widgets import MainWindow, SettingsDialog, Tray +from .widgets import ImagePopup, MainWindow, SettingsDialog, Tray settings = Settings("gotify-tray") @@ -309,6 +309,17 @@ class MainApplication(QtWidgets.QApplication): self.messages_model.clear() + def image_popup_callback(self, link: str, pos: QtCore.QPoint): + if filename := self.downloader.get_filename(link): + self.image_popup = ImagePopup(filename, pos, link) + self.image_popup.show() + else: + logger.warning(f"Image {link} is not in the cache") + + def main_window_hidden_callback(self): + if image_popup := getattr(self, "image_popup", None): + image_popup.close() + def refresh_callback(self): # Manual refresh -> also reset the image cache Cache().clear() @@ -335,6 +346,10 @@ class MainApplication(QtWidgets.QApplication): closed_callback=self.listener_closed_callback, ) + def tray_notification_clicked_callback(self): + if settings.value("tray/notifications/click", type=bool): + self.main_window.bring_to_front() + def tray_activated_callback( self, reason: QtWidgets.QSystemTrayIcon.ActivationReason ): @@ -349,7 +364,7 @@ class MainApplication(QtWidgets.QApplication): self.tray.actionSettings.triggered.connect(self.settings_callback) self.tray.actionShowWindow.triggered.connect(self.main_window.bring_to_front) self.tray.actionReconnect.triggered.connect(self.reconnect_callback) - self.tray.messageClicked.connect(self.main_window.bring_to_front) + self.tray.messageClicked.connect(self.tray_notification_clicked_callback) self.tray.activated.connect(self.tray_activated_callback) self.main_window.refresh.connect(self.refresh_callback) @@ -358,6 +373,8 @@ class MainApplication(QtWidgets.QApplication): self.application_selection_changed_callback ) self.main_window.delete_message.connect(self.delete_message_callback) + self.main_window.image_popup.connect(self.image_popup_callback) + self.main_window.hidden.connect(self.main_window_hidden_callback) self.watchdog.closed.connect(lambda: self.listener_closed_callback(None, None)) diff --git a/gotify_tray/gui/designs/widget_settings.py b/gotify_tray/gui/designs/widget_settings.py index 3a0f95c..fe146ac 100644 --- a/gotify_tray/gui/designs/widget_settings.py +++ b/gotify_tray/gui/designs/widget_settings.py @@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") - Dialog.resize(384, 274) + Dialog.resize(384, 285) self.gridLayout = QtWidgets.QGridLayout(Dialog) self.gridLayout.setObjectName("gridLayout") self.buttonBox = QtWidgets.QDialogButtonBox(Dialog) @@ -30,32 +30,35 @@ class Ui_Dialog(object): self.groupBox_notifications.setObjectName("groupBox_notifications") self.gridLayout_4 = QtWidgets.QGridLayout(self.groupBox_notifications) self.gridLayout_4.setObjectName("gridLayout_4") - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.gridLayout_4.addItem(spacerItem, 0, 2, 1, 1) - self.label_notification_duration_ms = QtWidgets.QLabel(self.groupBox_notifications) - self.label_notification_duration_ms.setObjectName("label_notification_duration_ms") - self.gridLayout_4.addWidget(self.label_notification_duration_ms, 1, 2, 1, 1) - self.label_notification_priority = QtWidgets.QLabel(self.groupBox_notifications) - self.label_notification_priority.setObjectName("label_notification_priority") - self.gridLayout_4.addWidget(self.label_notification_priority, 0, 0, 1, 1) + self.cb_notify = QtWidgets.QCheckBox(self.groupBox_notifications) + self.cb_notify.setObjectName("cb_notify") + self.gridLayout_4.addWidget(self.cb_notify, 2, 0, 1, 3) self.label_notification_duration = QtWidgets.QLabel(self.groupBox_notifications) self.label_notification_duration.setObjectName("label_notification_duration") self.gridLayout_4.addWidget(self.label_notification_duration, 1, 0, 1, 1) - self.spin_duration = QtWidgets.QSpinBox(self.groupBox_notifications) - self.spin_duration.setMinimum(500) - self.spin_duration.setMaximum(30000) - self.spin_duration.setSingleStep(100) - self.spin_duration.setObjectName("spin_duration") - self.gridLayout_4.addWidget(self.spin_duration, 1, 1, 1, 1) + self.label_notification_duration_ms = QtWidgets.QLabel(self.groupBox_notifications) + self.label_notification_duration_ms.setObjectName("label_notification_duration_ms") + self.gridLayout_4.addWidget(self.label_notification_duration_ms, 1, 2, 1, 1) self.spin_priority = QtWidgets.QSpinBox(self.groupBox_notifications) self.spin_priority.setMinimum(1) self.spin_priority.setMaximum(10) self.spin_priority.setProperty("value", 5) self.spin_priority.setObjectName("spin_priority") self.gridLayout_4.addWidget(self.spin_priority, 0, 1, 1, 1) - self.cb_notify = QtWidgets.QCheckBox(self.groupBox_notifications) - self.cb_notify.setObjectName("cb_notify") - self.gridLayout_4.addWidget(self.cb_notify, 2, 0, 1, 3) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.gridLayout_4.addItem(spacerItem, 0, 2, 1, 1) + self.label_notification_priority = QtWidgets.QLabel(self.groupBox_notifications) + self.label_notification_priority.setObjectName("label_notification_priority") + self.gridLayout_4.addWidget(self.label_notification_priority, 0, 0, 1, 1) + self.spin_duration = QtWidgets.QSpinBox(self.groupBox_notifications) + self.spin_duration.setMinimum(500) + self.spin_duration.setMaximum(30000) + self.spin_duration.setSingleStep(100) + self.spin_duration.setObjectName("spin_duration") + self.gridLayout_4.addWidget(self.spin_duration, 1, 1, 1, 1) + self.cb_notification_click = QtWidgets.QCheckBox(self.groupBox_notifications) + self.cb_notification_click.setObjectName("cb_notification_click") + self.gridLayout_4.addWidget(self.cb_notification_click, 3, 0, 1, 3) self.verticalLayout_4.addWidget(self.groupBox_notifications) self.groupBox_server_info = QtWidgets.QGroupBox(self.tab_general) self.groupBox_server_info.setObjectName("groupBox_server_info") @@ -102,37 +105,66 @@ class Ui_Dialog(object): self.verticalLayout.setObjectName("verticalLayout") self.groupBox = QtWidgets.QGroupBox(self.tab_advanced) self.groupBox.setObjectName("groupBox") - self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.groupBox) - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.pb_export = QtWidgets.QPushButton(self.groupBox) - self.pb_export.setObjectName("pb_export") - self.verticalLayout_2.addWidget(self.pb_export) - self.pb_import = QtWidgets.QPushButton(self.groupBox) - self.pb_import.setObjectName("pb_import") - self.verticalLayout_2.addWidget(self.pb_import) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.groupBox) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") self.pb_reset = QtWidgets.QPushButton(self.groupBox) self.pb_reset.setObjectName("pb_reset") - self.verticalLayout_2.addWidget(self.pb_reset) + self.horizontalLayout_2.addWidget(self.pb_reset) + self.pb_import = QtWidgets.QPushButton(self.groupBox) + self.pb_import.setObjectName("pb_import") + self.horizontalLayout_2.addWidget(self.pb_import) + self.pb_export = QtWidgets.QPushButton(self.groupBox) + self.pb_export.setObjectName("pb_export") + self.horizontalLayout_2.addWidget(self.pb_export) self.verticalLayout.addWidget(self.groupBox) + self.groupbox_image_popup = QtWidgets.QGroupBox(self.tab_advanced) + self.groupbox_image_popup.setCheckable(True) + self.groupbox_image_popup.setObjectName("groupbox_image_popup") + self.gridLayout_2 = QtWidgets.QGridLayout(self.groupbox_image_popup) + self.gridLayout_2.setObjectName("gridLayout_2") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.label = QtWidgets.QLabel(self.groupbox_image_popup) + self.label.setObjectName("label") + self.horizontalLayout_4.addWidget(self.label) + self.spin_popup_w = QtWidgets.QSpinBox(self.groupbox_image_popup) + self.spin_popup_w.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.spin_popup_w.setMinimum(100) + self.spin_popup_w.setMaximum(10000) + self.spin_popup_w.setObjectName("spin_popup_w") + self.horizontalLayout_4.addWidget(self.spin_popup_w) + self.label_2 = QtWidgets.QLabel(self.groupbox_image_popup) + self.label_2.setObjectName("label_2") + self.horizontalLayout_4.addWidget(self.label_2) + self.spin_popup_h = QtWidgets.QSpinBox(self.groupbox_image_popup) + self.spin_popup_h.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.spin_popup_h.setMinimum(100) + self.spin_popup_h.setMaximum(10000) + self.spin_popup_h.setObjectName("spin_popup_h") + self.horizontalLayout_4.addWidget(self.spin_popup_h) + spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout_4.addItem(spacerItem4) + self.gridLayout_2.addLayout(self.horizontalLayout_4, 0, 0, 1, 1) + self.verticalLayout.addWidget(self.groupbox_image_popup) self.groupBox_logging = QtWidgets.QGroupBox(self.tab_advanced) self.groupBox_logging.setObjectName("groupBox_logging") self.gridLayout_6 = QtWidgets.QGridLayout(self.groupBox_logging) self.gridLayout_6.setObjectName("gridLayout_6") - self.label_logging = QtWidgets.QLabel(self.groupBox_logging) - self.label_logging.setObjectName("label_logging") - self.gridLayout_6.addWidget(self.label_logging, 0, 0, 1, 1) self.combo_logging = QtWidgets.QComboBox(self.groupBox_logging) self.combo_logging.setObjectName("combo_logging") self.gridLayout_6.addWidget(self.combo_logging, 0, 1, 1, 1) + spacerItem5 = QtWidgets.QSpacerItem(190, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.gridLayout_6.addItem(spacerItem5, 0, 3, 1, 1) self.pb_open_log = QtWidgets.QPushButton(self.groupBox_logging) self.pb_open_log.setMaximumSize(QtCore.QSize(30, 16777215)) self.pb_open_log.setObjectName("pb_open_log") self.gridLayout_6.addWidget(self.pb_open_log, 0, 2, 1, 1) - spacerItem4 = QtWidgets.QSpacerItem(190, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.gridLayout_6.addItem(spacerItem4, 0, 3, 1, 1) + self.label_logging = QtWidgets.QLabel(self.groupBox_logging) + self.label_logging.setObjectName("label_logging") + self.gridLayout_6.addWidget(self.label_logging, 0, 0, 1, 1) self.verticalLayout.addWidget(self.groupBox_logging) - spacerItem5 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.verticalLayout.addItem(spacerItem5) + spacerItem6 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) + self.verticalLayout.addItem(spacerItem6) self.tabWidget.addTab(self.tab_advanced, "") self.gridLayout.addWidget(self.tabWidget, 0, 0, 1, 1) @@ -153,10 +185,11 @@ class Ui_Dialog(object): _translate = QtCore.QCoreApplication.translate Dialog.setWindowTitle(_translate("Dialog", "Dialog")) self.groupBox_notifications.setTitle(_translate("Dialog", "Notifications")) + self.cb_notify.setText(_translate("Dialog", "Show a notification for missed messages after reconnecting")) + self.label_notification_duration.setText(_translate("Dialog", "Notification duration:")) self.label_notification_duration_ms.setText(_translate("Dialog", "ms")) self.label_notification_priority.setText(_translate("Dialog", "Minimum priority to show notifications:")) - self.label_notification_duration.setText(_translate("Dialog", "Notification duration:")) - self.cb_notify.setText(_translate("Dialog", "Show a notification for missed messages after reconnecting")) + self.cb_notification_click.setText(_translate("Dialog", "Clicking the notification pop-up opens the main window")) self.groupBox_server_info.setTitle(_translate("Dialog", "Server info")) self.pb_change_server_info.setText(_translate("Dialog", "Change server info")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_general), _translate("Dialog", "General")) @@ -166,13 +199,20 @@ class Ui_Dialog(object): self.pb_font_message_content.setText(_translate("Dialog", "Message")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_fonts), _translate("Dialog", "Fonts")) self.groupBox.setTitle(_translate("Dialog", "Settings")) - self.pb_export.setText(_translate("Dialog", "Export")) - self.pb_import.setText(_translate("Dialog", "Import")) self.pb_reset.setText(_translate("Dialog", "Reset")) + self.pb_import.setText(_translate("Dialog", "Import")) + self.pb_export.setText(_translate("Dialog", "Export")) + self.groupbox_image_popup.setTitle(_translate("Dialog", "Image pop-up for URLs")) + self.label.setToolTip(_translate("Dialog", "Maximum pop-up width")) + self.label.setText(_translate("Dialog", "Width")) + self.spin_popup_w.setToolTip(_translate("Dialog", "Maximum pop-up width")) + self.label_2.setToolTip(_translate("Dialog", "Maximum pop-up height")) + self.label_2.setText(_translate("Dialog", "Height")) + self.spin_popup_h.setToolTip(_translate("Dialog", "Maximum pop-up height")) self.groupBox_logging.setTitle(_translate("Dialog", "Logging")) - self.label_logging.setText(_translate("Dialog", "Level")) self.pb_open_log.setToolTip(_translate("Dialog", "Open logfile")) self.pb_open_log.setText(_translate("Dialog", "...")) + self.label_logging.setText(_translate("Dialog", "Level")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_advanced), _translate("Dialog", "Advanced")) diff --git a/gotify_tray/gui/designs/widget_settings.ui b/gotify_tray/gui/designs/widget_settings.ui index 3f3579b..2b32ffa 100644 --- a/gotify_tray/gui/designs/widget_settings.ui +++ b/gotify_tray/gui/designs/widget_settings.ui @@ -7,7 +7,7 @@ 0 0 384 - 274 + 285 @@ -40,30 +40,10 @@ Notifications - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - + + - ms - - - - - - - Minimum priority to show notifications: + Show a notification for missed messages after reconnecting @@ -74,16 +54,10 @@ - - - - 500 - - - 30000 - - - 100 + + + + ms @@ -100,10 +74,43 @@ - - + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + - Show a notification for missed messages after reconnecting + Minimum priority to show notifications: + + + + + + + 500 + + + 30000 + + + 100 + + + + + + + Clicking the notification pop-up opens the main window @@ -233,11 +240,11 @@ Settings - + - + - Export + Reset @@ -249,31 +256,118 @@ - + - Reset + Export + + + + Image pop-up for URLs + + + true + + + + + + + + Maximum pop-up width + + + Width + + + + + + + Maximum pop-up width + + + Qt::AlignCenter + + + 100 + + + 10000 + + + + + + + Maximum pop-up height + + + Height + + + + + + + Maximum pop-up height + + + Qt::AlignCenter + + + 100 + + + 10000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + Logging - - - - Level - - - + + + + Qt::Horizontal + + + + 190 + 20 + + + + @@ -290,18 +384,12 @@ - - - - Qt::Horizontal + + + + Level - - - 190 - 20 - - - + diff --git a/gotify_tray/gui/widgets/ImagePopup.py b/gotify_tray/gui/widgets/ImagePopup.py new file mode 100644 index 0000000..8850c6d --- /dev/null +++ b/gotify_tray/gui/widgets/ImagePopup.py @@ -0,0 +1,65 @@ +import platform +from PyQt6 import QtCore, QtGui, QtWidgets + +from gotify_tray.database import Settings + + +settings = Settings("gotify-tray") + + +class ImagePopup(QtWidgets.QLabel): + def __init__(self, filename: str, pos: QtCore.QPoint, link: str = None): + """Create and show a pop-up image under the cursor + + Args: + filename (str): The path to the image to display + pos (QtCore.QPoint): The location at which the image should be displayed + link (str, optional): The URL of the image. Defaults to None. + """ + super(ImagePopup, self).__init__() + self.link = link + + self.setWindowFlags(QtCore.Qt.WindowType.Popup) + self.installEventFilter(self) + + # Prevent leaving the pop-up open when moving quickly out of the widget + self.popup_timer = QtCore.QTimer() + self.popup_timer.timeout.connect(self.check_mouse) + + pixmap = QtGui.QPixmap(filename) + W = settings.value("ImagePopup/w", type=int) + H = settings.value("ImagePopup/h", type=int) + if pixmap.height() > H or pixmap.width() > W: + pixmap = pixmap.scaled( + W, + H, + aspectRatioMode=QtCore.Qt.AspectRatioMode.KeepAspectRatio, + transformMode=QtCore.Qt.TransformationMode.SmoothTransformation, + ) + self.setPixmap(pixmap) + + self.move(pos - QtCore.QPoint(15, 15)) + + self.popup_timer.start(500) + + def check_mouse(self): + if not self.underMouse(): + self.close() + + def close(self): + self.popup_timer.stop() + super(ImagePopup, self).close() + + def eventFilter(self, object: QtCore.QObject, event: QtCore.QEvent) -> bool: + if platform.system() != "Darwin" and event.type() == QtCore.QEvent.Type.Leave: + # Close the pop-up on mouse leave + self.close() + elif ( + event.type() == QtCore.QEvent.Type.MouseButtonPress + and event.button() == QtCore.Qt.MouseButton.LeftButton + and self.link + ): + # Open the image URL on left click + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.link)) + + return super().eventFilter(object, event) diff --git a/gotify_tray/gui/widgets/MainWindow.py b/gotify_tray/gui/widgets/MainWindow.py index ad4b974..26ed8b3 100644 --- a/gotify_tray/gui/widgets/MainWindow.py +++ b/gotify_tray/gui/widgets/MainWindow.py @@ -21,6 +21,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): delete_all = QtCore.pyqtSignal(QtGui.QStandardItem) delete_message = QtCore.pyqtSignal(MessagesModelItem) application_selection_changed = QtCore.pyqtSignal(QtGui.QStandardItem) + image_popup = QtCore.pyqtSignal(str, QtCore.QPoint) + hidden = QtCore.pyqtSignal() def __init__( self, application_model: ApplicationModel, messages_model: MessagesModel @@ -72,6 +74,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): font_title.fromString(s) else: font_title.setBold(True) + font_title.setPointSize(font_title.pointSize() + 2) self.label_application.setFont(font_title) # Set tooltips @@ -102,6 +105,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self.messages_model.indexFromItem(message_item), message_widget ) message_widget.deletion_requested.connect(self.delete_message.emit) + message_widget.image_popup.connect(self.image_popup.emit) def currentApplicationIndex(self) -> QtCore.QModelIndex: return self.listView_applications.selectionModel().currentIndex() @@ -172,3 +176,4 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): def closeEvent(self, e: QtGui.QCloseEvent) -> None: self.hide() + self.hidden.emit() diff --git a/gotify_tray/gui/widgets/MessageWidget.py b/gotify_tray/gui/widgets/MessageWidget.py index db1cde6..3aab648 100644 --- a/gotify_tray/gui/widgets/MessageWidget.py +++ b/gotify_tray/gui/widgets/MessageWidget.py @@ -1,3 +1,5 @@ +import os + from PyQt6 import QtCore, QtGui, QtWidgets from ..models.MessagesModel import MessageItemDataRole, MessagesModelItem @@ -11,6 +13,7 @@ settings = Settings("gotify-tray") class MessageWidget(QtWidgets.QWidget, Ui_Form): deletion_requested = QtCore.pyqtSignal(MessagesModelItem) + image_popup = QtCore.pyqtSignal(str, QtCore.QPoint) def __init__(self, message_item: MessagesModelItem, image_path: str = ""): super(MessageWidget, self).__init__() @@ -86,7 +89,17 @@ class MessageWidget(QtWidgets.QWidget, Ui_Form): self.label_date.setFont(font_date) self.label_message.setFont(font_content) + def link_hovered_callback(self, link: str): + if not settings.value("ImagePopup/enabled", type=bool): + return + + qurl = QtCore.QUrl(link) + _, ext = os.path.splitext(qurl.fileName()) + if ext in settings.value("ImagePopup/extensions", type=list): + self.image_popup.emit(link, QtGui.QCursor.pos()) + def link_callbacks(self): self.pb_delete.clicked.connect( lambda: self.deletion_requested.emit(self.message_item) ) + self.label_message.linkHovered.connect(self.link_hovered_callback) diff --git a/gotify_tray/gui/widgets/SettingsDialog.py b/gotify_tray/gui/widgets/SettingsDialog.py index 0c98358..90ee6aa 100644 --- a/gotify_tray/gui/widgets/SettingsDialog.py +++ b/gotify_tray/gui/widgets/SettingsDialog.py @@ -1,13 +1,12 @@ import logging import platform import os -import webbrowser from gotify_tray.database import Settings from gotify_tray.gotify import GotifyMessageModel from gotify_tray.gui.models import MessagesModelItem from . import MessageWidget -from gotify_tray.utils import verify_server +from gotify_tray.utils import verify_server, open_file from gotify_tray.tasks import ExportSettingsTask, ImportSettingsTask from PyQt6 import QtCore, QtGui, QtWidgets @@ -57,6 +56,10 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): settings.value("message/check_missed/notify", type=bool) ) + self.cb_notification_click.setChecked( + settings.value("tray/notifications/click", type=bool) + ) + # Logging self.combo_logging.addItems( [ @@ -83,6 +86,11 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): ) self.layout_fonts_message.addWidget(self.message_widget) + # Advanced + self.groupbox_image_popup.setChecked(settings.value("ImagePopup/enabled", type=bool)) + self.spin_popup_w.setValue(settings.value("ImagePopup/w", type=int)) + self.spin_popup_h.setValue(settings.value("ImagePopup/h", type=int)) + def change_server_info_callback(self): self.server_changed = verify_server(force_new=True, enable_import=False) @@ -150,6 +158,7 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): self.spin_priority.valueChanged.connect(self.settings_changed_callback) self.spin_duration.valueChanged.connect(self.settings_changed_callback) self.cb_notify.stateChanged.connect(self.settings_changed_callback) + self.cb_notification_click.stateChanged.connect(self.settings_changed_callback) # Server info self.pb_change_server_info.clicked.connect(self.change_server_info_callback) @@ -157,7 +166,7 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): # Logging self.combo_logging.currentTextChanged.connect(self.settings_changed_callback) self.pb_open_log.clicked.connect( - lambda: webbrowser.open(logger.root.handlers[0].baseFilename) + lambda: open_file(logger.root.handlers[0].baseFilename) ) # Fonts @@ -175,12 +184,18 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): self.pb_export.clicked.connect(self.export_callback) self.pb_import.clicked.connect(self.import_callback) self.pb_reset.clicked.connect(self.reset_callback) + self.groupbox_image_popup.toggled.connect(self.settings_changed_callback) + self.spin_popup_w.valueChanged.connect(self.settings_changed_callback) + self.spin_popup_h.valueChanged.connect(self.settings_changed_callback) def apply_settings(self): # Priority settings.setValue("tray/notifications/priority", self.spin_priority.value()) settings.setValue("tray/notifications/duration_ms", self.spin_duration.value()) settings.setValue("message/check_missed/notify", self.cb_notify.isChecked()) + settings.setValue( + "tray/notifications/click", self.cb_notification_click.isChecked() + ) # Logging selected_level = self.combo_logging.currentText() @@ -204,6 +219,11 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): self.message_widget.label_message.font().toString(), ) + # Advanced + settings.setValue("ImagePopup/enabled", self.groupbox_image_popup.isChecked()) + settings.setValue("ImagePopup/w", self.spin_popup_w.value()) + settings.setValue("ImagePopup/h", self.spin_popup_h.value()) + self.settings_changed = False self.buttonBox.button( QtWidgets.QDialogButtonBox.StandardButton.Apply diff --git a/gotify_tray/gui/widgets/Tray.py b/gotify_tray/gui/widgets/Tray.py index e901dc4..74ffdbb 100644 --- a/gotify_tray/gui/widgets/Tray.py +++ b/gotify_tray/gui/widgets/Tray.py @@ -23,13 +23,13 @@ class Tray(QtWidgets.QSystemTrayIcon): # Tray menu items menu = QtWidgets.QMenu() - self.actionSettings = QtGui.QAction("Settings", self) - menu.addAction(self.actionSettings) + self.actionShowWindow = QtGui.QAction("Show Window", self) + menu.addAction(self.actionShowWindow) menu.addSeparator() - self.actionShowWindow = QtGui.QAction("Show Window", self) - menu.addAction(self.actionShowWindow) + self.actionSettings = QtGui.QAction("Settings", self) + menu.addAction(self.actionSettings) menu.addSeparator() diff --git a/gotify_tray/gui/widgets/__init__.py b/gotify_tray/gui/widgets/__init__.py index d43f65f..2f9952f 100644 --- a/gotify_tray/gui/widgets/__init__.py +++ b/gotify_tray/gui/widgets/__init__.py @@ -1,3 +1,4 @@ +from .ImagePopup import ImagePopup from .MessageWidget import MessageWidget from .MainWindow import MainWindow from .ServerInfoDialog import ServerInfoDialog diff --git a/gotify_tray/utils.py b/gotify_tray/utils.py index dbcbb30..f1f581a 100644 --- a/gotify_tray/utils.py +++ b/gotify_tray/utils.py @@ -1,5 +1,7 @@ import os +import platform import re +import subprocess from pathlib import Path @@ -46,3 +48,12 @@ def get_abs_path(s) -> str: h = Path(__file__).parent.parent p = Path(s) return os.path.join(h, p).replace("\\", "/") + + +def open_file(filename: str): + if platform.system() == "Linux": + subprocess.call(["xdg-open", filename]) + elif platform.system() == "Windows": + os.startfile(filename) + elif platform.system() == "Darwin": + subprocess.call(["open", filename]) diff --git a/images/main_window.png b/images/main_window.png index c9e9611..9270c66 100644 Binary files a/images/main_window.png and b/images/main_window.png differ diff --git a/images/settings.png b/images/settings.png index c1e44e4..309ceae 100644 Binary files a/images/settings.png and b/images/settings.png differ diff --git a/requirements.txt b/requirements.txt index e36d7dd..9da2385 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ requests==2.28.1 -websocket-client==1.3.3 +websocket-client==1.4.0 pyqt6==6.3.1 python-dateutil==2.8.2