From 0bea6ea14f9d113cddb3bfb4b9b41805f3645fc2 Mon Sep 17 00:00:00 2001 From: "dries.k" Date: Tue, 8 Feb 2022 22:12:52 +0100 Subject: [PATCH] a better main window --- gotify_tray/database/default_settings.py | 9 + gotify_tray/gui/MainApplication.py | 205 ++++++++++++++++-- gotify_tray/gui/__init__.py | 2 +- gotify_tray/gui/designs/widget_main.py | 77 +++++++ gotify_tray/gui/designs/widget_main.ui | 118 ++++++++++ gotify_tray/gui/designs/widget_message.py | 94 ++++++++ gotify_tray/gui/designs/widget_message.ui | 152 +++++++++++++ gotify_tray/gui/designs/widget_server.py | 6 +- gotify_tray/gui/designs/widget_settings.py | 6 +- gotify_tray/gui/images/refresh.svg | 45 ++++ gotify_tray/gui/images/status_active.svg | 49 +++++ gotify_tray/gui/images/status_connecting.svg | 49 +++++ gotify_tray/gui/images/status_error.svg | 49 +++++ gotify_tray/gui/images/status_inactive.svg | 49 +++++ gotify_tray/gui/images/trashcan.svg | 1 + .../gui/{ => models}/ApplicationModel.py | 28 ++- gotify_tray/gui/models/MessagesModel.py | 23 ++ gotify_tray/gui/models/__init__.py | 7 + gotify_tray/gui/widgets/MainWindow.py | 154 +++++++++++++ gotify_tray/gui/widgets/MessageWidget.py | 71 ++++++ .../gui/{ => widgets}/ServerInfoDialog.py | 2 +- .../gui/{ => widgets}/SettingsDialog.py | 2 +- gotify_tray/gui/widgets/StatusWidget.py | 34 +++ gotify_tray/gui/{ => widgets}/Tray.py | 5 + gotify_tray/gui/widgets/__init__.py | 6 + gotify_tray/tasks.py | 59 +++++ gotify_tray/utils.py | 29 +++ 27 files changed, 1294 insertions(+), 37 deletions(-) create mode 100644 gotify_tray/gui/designs/widget_main.py create mode 100644 gotify_tray/gui/designs/widget_main.ui create mode 100644 gotify_tray/gui/designs/widget_message.py create mode 100644 gotify_tray/gui/designs/widget_message.ui create mode 100644 gotify_tray/gui/images/refresh.svg create mode 100644 gotify_tray/gui/images/status_active.svg create mode 100644 gotify_tray/gui/images/status_connecting.svg create mode 100644 gotify_tray/gui/images/status_error.svg create mode 100644 gotify_tray/gui/images/status_inactive.svg create mode 100644 gotify_tray/gui/images/trashcan.svg rename gotify_tray/gui/{ => models}/ApplicationModel.py (67%) create mode 100644 gotify_tray/gui/models/MessagesModel.py create mode 100644 gotify_tray/gui/models/__init__.py create mode 100644 gotify_tray/gui/widgets/MainWindow.py create mode 100644 gotify_tray/gui/widgets/MessageWidget.py rename gotify_tray/gui/{ => widgets}/ServerInfoDialog.py (98%) rename gotify_tray/gui/{ => widgets}/SettingsDialog.py (98%) create mode 100644 gotify_tray/gui/widgets/StatusWidget.py rename gotify_tray/gui/{ => widgets}/Tray.py (89%) create mode 100644 gotify_tray/gui/widgets/__init__.py diff --git a/gotify_tray/database/default_settings.py b/gotify_tray/database/default_settings.py index 6644428..e527ad3 100644 --- a/gotify_tray/database/default_settings.py +++ b/gotify_tray/database/default_settings.py @@ -4,4 +4,13 @@ DEFAULT_SETTINGS = { "tray/notifications/duration_ms": 5000, "tray/notifications/icon/show": True, "watchdog/interval/s": 60, + "MessageWidget/image/size": 33, + "MessageWidget/font/title": "Noto Sans,17,-1,5,75,0,0,0,0,0,Bold", + "MessageWidget/font/date": "Noto Sans,11,-1,5,50,1,0,0,0,0,Italic", + "MessageWidget/font/content": "Noto Sans,11,-1,5,50,0,0,0,0,0,Regular", + "ApplicationItem/font": "Noto Sans,10,-1,5,50,0,0,0,0,0,Regular", + "MainWindow/font/application": "Noto Sans,13,-1,5,75,0,0,0,0,0,Bold", + "MainWindow/label/size": 25, + "MainWindow/button/size": 33, + "MainWindow/application/icon/size": 40, } diff --git a/gotify_tray/gui/MainApplication.py b/gotify_tray/gui/MainApplication.py index 4741841..ff7d7a7 100644 --- a/gotify_tray/gui/MainApplication.py +++ b/gotify_tray/gui/MainApplication.py @@ -3,23 +3,35 @@ import logging import os import sys import tempfile -from typing import List +from typing import List, Union from gotify_tray import gotify from gotify_tray.__version__ import __title__ from gotify_tray.database import Downloader, Settings -from gotify_tray.tasks import GetApplicationsTask, ServerConnectionWatchdogTask +from gotify_tray.tasks import ( + DeleteApplicationMessagesTask, + DeleteAllMessagesTask, + DeleteMessageTask, + GetApplicationsTask, + GetApplicationMessagesTask, + GetMessagesTask, + ServerConnectionWatchdogTask, +) from gotify_tray.utils import verify_server from PyQt6 import QtCore, QtGui, QtWidgets from ..__version__ import __title__ -from .ApplicationModel import ( +from .models import ( + ApplicationAllMessagesItem, ApplicationItemDataRole, ApplicationModel, ApplicationModelItem, + MessagesModel, + MessagesModelItem, + MessageItemDataRole, ) -from .SettingsDialog import SettingsDialog -from .Tray import Tray +from .widgets import MainWindow, SettingsDialog, Tray + settings = Settings("gotify-tray") logger = logging.getLogger("gotify-tray") @@ -58,7 +70,11 @@ class MainApplication(QtWidgets.QApplication): self.downloader = Downloader() + self.messages_model = MessagesModel() self.application_model = ApplicationModel() + + self.main_window = MainWindow(self.application_model, self.messages_model) + self.refresh_applications() self.tray = Tray() @@ -77,44 +93,144 @@ class MainApplication(QtWidgets.QApplication): self.watchdog.start() def refresh_applications(self): + self.messages_model.clear() self.application_model.clear() - def get_applications_callback( - applications: List[gotify.GotifyApplicationModel], - ): - for i, application in enumerate(applications): - icon = QtGui.QIcon( - self.downloader.get_filename( - f"{self.gotify_client.url}/{application.image}" - ) - ) - self.application_model.setItem( - i, 0, ApplicationModelItem(application, icon), - ) + self.application_model.setItem(0, 0, ApplicationAllMessagesItem()) self.get_applications_task = GetApplicationsTask(self.gotify_client) - self.get_applications_task.success.connect(get_applications_callback) + self.get_applications_task.success.connect( + self.get_applications_success_callback + ) + self.get_applications_task.started.connect( + self.main_window.disable_applications + ) + self.get_applications_task.finished.connect( + self.main_window.enable_applications + ) self.get_applications_task.start() + def get_applications_success_callback( + self, applications: List[gotify.GotifyApplicationModel], + ): + for i, application in enumerate(applications): + icon = QtGui.QIcon( + self.downloader.get_filename( + f"{self.gotify_client.url}/{application.image}" + ) + ) + self.application_model.setItem( + i + 1, 0, ApplicationModelItem(application, icon), + ) + def listener_opened_callback(self): + self.main_window.set_active() self.tray.set_icon_ok() - # self.tray.actionReconnect.setEnabled(True) def listener_closed_callback(self, close_status_code: int, close_msg: str): + self.main_window.set_connecting() self.tray.set_icon_error() if not self.shutting_down: self.gotify_client.reconnect() - def refresh_callback(self): - # self.tray.actionReconnect.setDisabled(True) + def reconnect_callback(self): if not self.gotify_client.is_listening(): self.gotify_client.listener.reset_wait_time() else: self.gotify_client.stop(reset_wait=True) self.gotify_client.reconnect(increase_wait_time=False) + def insert_message( + self, + row: int, + message: gotify.GotifyMessageModel, + application: gotify.GotifyApplicationModel, + ): + message_item = MessagesModelItem(message) + self.messages_model.insertRow(row, message_item) + self.main_window.insert_message_widget( + message_item, + self.downloader.get_filename( + f"{self.gotify_client.url}/{application.image}" + ), + ) + + def application_selection_changed_callback( + self, item: Union[ApplicationModelItem, ApplicationAllMessagesItem] + ): + self.messages_model.clear() + + if isinstance(item, ApplicationModelItem): + + def get_application_messages_callback( + page: gotify.GotifyPagedMessagesModel, + ): + for i, message in enumerate(page.messages): + self.insert_message( + i, message, item.data(ApplicationItemDataRole.ApplicationRole), + ) + + self.get_application_messages_task = GetApplicationMessagesTask( + item.data(ApplicationItemDataRole.ApplicationRole).id, + self.gotify_client, + ) + self.get_application_messages_task.success.connect( + get_application_messages_callback + ) + self.get_application_messages_task.start() + + elif isinstance(item, ApplicationAllMessagesItem): + + def get_messages_callback(page: gotify.GotifyPagedMessagesModel): + for i, message in enumerate(page.messages): + if item := self.application_model.itemFromId(message.appid): + self.insert_message( + i, + message, + item.data(ApplicationItemDataRole.ApplicationRole), + ) + + self.get_messages_task = GetMessagesTask(self.gotify_client) + self.get_messages_task.success.connect(get_messages_callback) + self.get_messages_task.start() + + def add_message_to_model(self, message: gotify.GotifyMessageModel): + if application_item := self.application_model.itemFromId(message.appid): + application_index = self.main_window.currentApplicationIndex() + if selected_application_item := self.application_model.itemFromIndex( + application_index + ): + if isinstance(selected_application_item, ApplicationModelItem): + # A single application is selected + if ( + message.appid + == selected_application_item.data( + ApplicationItemDataRole.ApplicationRole + ).id + ): + self.insert_message( + 0, + message, + application_item.data( + ApplicationItemDataRole.ApplicationRole + ), + ) + elif isinstance(selected_application_item, ApplicationAllMessagesItem): + # "All messages' is selected + self.insert_message( + 0, + message, + application_item.data(ApplicationItemDataRole.ApplicationRole), + ) + def new_message_callback(self, message: gotify.GotifyMessageModel): - if message.priority < settings.value("tray/notifications/priority", type=int): + self.add_message_to_model(message) + + # Show a notification + if ( + message.priority < settings.value("tray/notifications/priority", type=int) + or self.main_window.isActiveWindow() + ): return if settings.value("tray/notifications/icon/show", type=bool): @@ -130,7 +246,6 @@ class MainApplication(QtWidgets.QApplication): else: icon = QtWidgets.QSystemTrayIcon.MessageIcon.Information - # Show a notification self.tray.showMessage( message.title, message.message, @@ -138,6 +253,30 @@ class MainApplication(QtWidgets.QApplication): msecs=settings.value("tray/notifications/duration_ms", type=int), ) + def delete_message_callback(self, message_item: MessagesModelItem): + self.delete_message_task = DeleteMessageTask( + message_item.data(MessageItemDataRole.MessageRole).id, self.gotify_client + ) + self.messages_model.removeRow(message_item.row()) + self.delete_message_task.start() + + def delete_all_messages_callback( + self, item: Union[ApplicationModelItem, ApplicationAllMessagesItem] + ): + if isinstance(item, ApplicationModelItem): + self.delete_application_messages_task = DeleteApplicationMessagesTask( + item.data(ApplicationItemDataRole.ApplicationRole).id, + self.gotify_client, + ) + self.delete_application_messages_task.start() + elif isinstance(item, ApplicationAllMessagesItem): + self.delete_all_messages_task = DeleteAllMessagesTask(self.gotify_client) + self.delete_all_messages_task.start() + else: + return + + self.messages_model.clear() + def settings_callback(self): settings_dialog = SettingsDialog() accepted = settings_dialog.exec() @@ -158,10 +297,26 @@ class MainApplication(QtWidgets.QApplication): if r == QtWidgets.QMessageBox.StandardButton.Yes: self.quit() + def tray_activated_callback( + self, reason: QtWidgets.QSystemTrayIcon.ActivationReason + ): + if reason == QtWidgets.QSystemTrayIcon.ActivationReason.Trigger: + self.main_window.bring_to_front() + def link_callbacks(self): self.tray.actionQuit.triggered.connect(self.quit) self.tray.actionSettings.triggered.connect(self.settings_callback) - self.tray.actionReconnect.triggered.connect(self.refresh_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.activated.connect(self.tray_activated_callback) + + self.main_window.refresh.connect(self.refresh_applications) + self.main_window.delete_all.connect(self.delete_all_messages_callback) + self.main_window.application_selection_changed.connect( + self.application_selection_changed_callback + ) + self.main_window.delete_message.connect(self.delete_message_callback) self.watchdog.closed.connect(lambda: self.listener_closed_callback(None, None)) @@ -175,6 +330,8 @@ class MainApplication(QtWidgets.QApplication): return self.lock_file.tryLock() def quit(self) -> None: + self.main_window.store_state() + self.tray.hide() self.lock_file.unlock() diff --git a/gotify_tray/gui/__init__.py b/gotify_tray/gui/__init__.py index 73e901d..a92bfaa 100644 --- a/gotify_tray/gui/__init__.py +++ b/gotify_tray/gui/__init__.py @@ -1,2 +1,2 @@ from .MainApplication import start_gui -from .ServerInfoDialog import ServerInfoDialog +from .widgets import ServerInfoDialog diff --git a/gotify_tray/gui/designs/widget_main.py b/gotify_tray/gui/designs/widget_main.py new file mode 100644 index 0000000..ef54132 --- /dev/null +++ b/gotify_tray/gui/designs/widget_main.py @@ -0,0 +1,77 @@ +# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_main.ui' +# +# Created by: PyQt6 UI code generator 6.1.0 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(809, 647) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.gridLayout = QtWidgets.QGridLayout(self.centralwidget) + self.gridLayout.setContentsMargins(4, 4, 4, 4) + self.gridLayout.setObjectName("gridLayout") + self.splitter = QtWidgets.QSplitter(self.centralwidget) + self.splitter.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.splitter.setObjectName("splitter") + self.listView_applications = QtWidgets.QListView(self.splitter) + self.listView_applications.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) + self.listView_applications.setObjectName("listView_applications") + self.verticalLayoutWidget = QtWidgets.QWidget(self.splitter) + self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.verticalLayoutWidget) + self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout.addItem(spacerItem) + self.label_application = QtWidgets.QLabel(self.verticalLayoutWidget) + self.label_application.setObjectName("label_application") + self.horizontalLayout.addWidget(self.label_application) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout.addItem(spacerItem1) + self.pb_refresh = QtWidgets.QPushButton(self.verticalLayoutWidget) + self.pb_refresh.setText("") + self.pb_refresh.setObjectName("pb_refresh") + self.horizontalLayout.addWidget(self.pb_refresh) + self.pb_delete_all = QtWidgets.QPushButton(self.verticalLayoutWidget) + self.pb_delete_all.setText("") + self.pb_delete_all.setObjectName("pb_delete_all") + self.horizontalLayout.addWidget(self.pb_delete_all) + self.verticalLayout_2.addLayout(self.horizontalLayout) + self.listView_messages = QtWidgets.QListView(self.verticalLayoutWidget) + self.listView_messages.setAutoScroll(True) + self.listView_messages.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) + self.listView_messages.setObjectName("listView_messages") + self.verticalLayout_2.addWidget(self.listView_messages) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + MainWindow.setTabOrder(self.listView_applications, self.listView_messages) + MainWindow.setTabOrder(self.listView_messages, self.pb_refresh) + MainWindow.setTabOrder(self.pb_refresh, self.pb_delete_all) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "Form")) + self.label_application.setText(_translate("MainWindow", "Application")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + MainWindow = QtWidgets.QMainWindow() + ui = Ui_MainWindow() + ui.setupUi(MainWindow) + MainWindow.show() + sys.exit(app.exec()) diff --git a/gotify_tray/gui/designs/widget_main.ui b/gotify_tray/gui/designs/widget_main.ui new file mode 100644 index 0000000..6718d61 --- /dev/null +++ b/gotify_tray/gui/designs/widget_main.ui @@ -0,0 +1,118 @@ + + + MainWindow + + + + 0 + 0 + 809 + 647 + + + + Form + + + + + 4 + + + 4 + + + 4 + + + 4 + + + + + Qt::Horizontal + + + + QAbstractItemView::NoEditTriggers + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Application + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + + + + + + true + + + QAbstractItemView::NoEditTriggers + + + + + + + + + + + + listView_applications + listView_messages + pb_refresh + pb_delete_all + + + + diff --git a/gotify_tray/gui/designs/widget_message.py b/gotify_tray/gui/designs/widget_message.py new file mode 100644 index 0000000..2d8cf7e --- /dev/null +++ b/gotify_tray/gui/designs/widget_message.py @@ -0,0 +1,94 @@ +# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_message.ui' +# +# Created by: PyQt6 UI code generator 6.1.0 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(454, 122) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.frame = QtWidgets.QFrame(Form) + self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.frame.setObjectName("frame") + self.gridLayout_frame = QtWidgets.QGridLayout(self.frame) + self.gridLayout_frame.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize) + self.gridLayout_frame.setContentsMargins(-1, 0, -1, 0) + self.gridLayout_frame.setObjectName("gridLayout_frame") + self.label_title = QtWidgets.QLabel(self.frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_title.sizePolicy().hasHeightForWidth()) + self.label_title.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setPointSize(17) + font.setBold(False) + font.setWeight(50) + self.label_title.setFont(font) + self.label_title.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.LinksAccessibleByMouse|QtCore.Qt.TextInteractionFlag.TextSelectableByMouse) + self.label_title.setObjectName("label_title") + self.gridLayout_frame.addWidget(self.label_title, 0, 1, 1, 1) + self.text_message = QtWidgets.QLabel(self.frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.MinimumExpanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.text_message.sizePolicy().hasHeightForWidth()) + self.text_message.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setPointSize(11) + self.text_message.setFont(font) + self.text_message.setWordWrap(True) + self.text_message.setOpenExternalLinks(True) + self.text_message.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.LinksAccessibleByMouse|QtCore.Qt.TextInteractionFlag.TextSelectableByMouse) + self.text_message.setObjectName("text_message") + self.gridLayout_frame.addWidget(self.text_message, 3, 1, 1, 3) + self.label_date = QtWidgets.QLabel(self.frame) + font = QtGui.QFont() + font.setPointSize(11) + self.label_date.setFont(font) + self.label_date.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.LinksAccessibleByMouse|QtCore.Qt.TextInteractionFlag.TextSelectableByMouse) + self.label_date.setObjectName("label_date") + self.gridLayout_frame.addWidget(self.label_date, 2, 1, 1, 1) + self.pb_delete = QtWidgets.QPushButton(self.frame) + self.pb_delete.setText("") + self.pb_delete.setFlat(True) + self.pb_delete.setObjectName("pb_delete") + self.gridLayout_frame.addWidget(self.pb_delete, 0, 3, 1, 1) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.gridLayout_frame.addItem(spacerItem, 0, 2, 1, 1) + self.label_image = QtWidgets.QLabel(self.frame) + self.label_image.setText("") + self.label_image.setObjectName("label_image") + self.gridLayout_frame.addWidget(self.label_image, 0, 0, 1, 1) + self.gridLayout.addWidget(self.frame, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + self.label_title.setText(_translate("Form", "Title")) + self.text_message.setText(_translate("Form", "TextLabel")) + self.label_date.setText(_translate("Form", "Date")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + Form = QtWidgets.QWidget() + ui = Ui_Form() + ui.setupUi(Form) + Form.show() + sys.exit(app.exec()) diff --git a/gotify_tray/gui/designs/widget_message.ui b/gotify_tray/gui/designs/widget_message.ui new file mode 100644 index 0000000..718406f --- /dev/null +++ b/gotify_tray/gui/designs/widget_message.ui @@ -0,0 +1,152 @@ + + + Form + + + + 0 + 0 + 454 + 122 + + + + Form + + + + QLayout::SetMinimumSize + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + QLayout::SetMinimumSize + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 17 + 50 + false + + + + Title + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + + 11 + + + + TextLabel + + + true + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + 11 + + + + Date + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + diff --git a/gotify_tray/gui/designs/widget_server.py b/gotify_tray/gui/designs/widget_server.py index b2cba3f..faab5ea 100644 --- a/gotify_tray/gui/designs/widget_server.py +++ b/gotify_tray/gui/designs/widget_server.py @@ -1,6 +1,6 @@ # Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_server.ui' # -# Created by: PyQt6 UI code generator 6.2.1 +# Created by: PyQt6 UI code generator 6.1.0 # # WARNING: Any manual changes made to this file will be lost when pyuic6 is # run again. Do not edit this file unless you know what you are doing. @@ -46,8 +46,8 @@ class Ui_Dialog(object): self.gridLayout.addWidget(self.label_server_info, 1, 1, 1, 2) self.retranslateUi(Dialog) - self.buttonBox.accepted.connect(Dialog.accept) # type: ignore - self.buttonBox.rejected.connect(Dialog.reject) # type: ignore + self.buttonBox.accepted.connect(Dialog.accept) + self.buttonBox.rejected.connect(Dialog.reject) QtCore.QMetaObject.connectSlotsByName(Dialog) def retranslateUi(self, Dialog): diff --git a/gotify_tray/gui/designs/widget_settings.py b/gotify_tray/gui/designs/widget_settings.py index c32e056..caad204 100644 --- a/gotify_tray/gui/designs/widget_settings.py +++ b/gotify_tray/gui/designs/widget_settings.py @@ -1,6 +1,6 @@ # Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_settings.ui' # -# Created by: PyQt6 UI code generator 6.2.1 +# Created by: PyQt6 UI code generator 6.1.0 # # WARNING: Any manual changes made to this file will be lost when pyuic6 is # run again. Do not edit this file unless you know what you are doing. @@ -80,8 +80,8 @@ class Ui_Dialog(object): self.verticalLayout.addWidget(self.buttonBox) self.retranslateUi(Dialog) - self.buttonBox.accepted.connect(Dialog.accept) # type: ignore - self.buttonBox.rejected.connect(Dialog.reject) # type: ignore + self.buttonBox.accepted.connect(Dialog.accept) + self.buttonBox.rejected.connect(Dialog.reject) QtCore.QMetaObject.connectSlotsByName(Dialog) Dialog.setTabOrder(self.cb_icons_notification, self.spin_priority) Dialog.setTabOrder(self.spin_priority, self.spin_duration) diff --git a/gotify_tray/gui/images/refresh.svg b/gotify_tray/gui/images/refresh.svg new file mode 100644 index 0000000..3257974 --- /dev/null +++ b/gotify_tray/gui/images/refresh.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gotify_tray/gui/images/status_active.svg b/gotify_tray/gui/images/status_active.svg new file mode 100644 index 0000000..7af0e8e --- /dev/null +++ b/gotify_tray/gui/images/status_active.svg @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/gotify_tray/gui/images/status_connecting.svg b/gotify_tray/gui/images/status_connecting.svg new file mode 100644 index 0000000..8b1a364 --- /dev/null +++ b/gotify_tray/gui/images/status_connecting.svg @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/gotify_tray/gui/images/status_error.svg b/gotify_tray/gui/images/status_error.svg new file mode 100644 index 0000000..2f89ca1 --- /dev/null +++ b/gotify_tray/gui/images/status_error.svg @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/gotify_tray/gui/images/status_inactive.svg b/gotify_tray/gui/images/status_inactive.svg new file mode 100644 index 0000000..2d0eba9 --- /dev/null +++ b/gotify_tray/gui/images/status_inactive.svg @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/gotify_tray/gui/images/trashcan.svg b/gotify_tray/gui/images/trashcan.svg new file mode 100644 index 0000000..94b811f --- /dev/null +++ b/gotify_tray/gui/images/trashcan.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gotify_tray/gui/ApplicationModel.py b/gotify_tray/gui/models/ApplicationModel.py similarity index 67% rename from gotify_tray/gui/ApplicationModel.py rename to gotify_tray/gui/models/ApplicationModel.py index e6435aa..716fe38 100644 --- a/gotify_tray/gui/ApplicationModel.py +++ b/gotify_tray/gui/models/ApplicationModel.py @@ -1,6 +1,6 @@ import enum -from typing import Optional +from typing import Optional, Union from PyQt6 import QtCore, QtGui from gotify_tray import gotify from gotify_tray.database import Settings @@ -20,12 +20,15 @@ class ApplicationModelItem(QtGui.QStandardItem): application: gotify.GotifyApplicationModel, icon: Optional[QtGui.QIcon] = None, *args, - **kwargs + **kwargs, ): super(ApplicationModelItem, self).__init__(application.name) self.setDropEnabled(False) self.setData(application, ApplicationItemDataRole.ApplicationRole) self.setData(icon, ApplicationItemDataRole.IconRole) + font = QtGui.QFont() + font.fromString(settings.value("ApplicationItem/font", type=str)) + self.setFont(font) if icon: self.setIcon(icon) @@ -36,6 +39,16 @@ class ApplicationModelItem(QtGui.QStandardItem): ) +class ApplicationAllMessagesItem(QtGui.QStandardItem): + def __init__(self, *args, **kwargs): + super(ApplicationAllMessagesItem, self).__init__("ALL MESSAGES") + self.setDropEnabled(False) + self.setDragEnabled(False) + font = QtGui.QFont() + font.fromString(settings.value("ApplicationItem/font", type=str)) + self.setFont(font) + + class ApplicationModel(QtGui.QStandardItemModel): def __init__(self): super(ApplicationModel, self).__init__() @@ -43,10 +56,17 @@ class ApplicationModel(QtGui.QStandardItemModel): ApplicationModelItem(gotify.GotifyApplicationModel({"name": ""}), None) ) - def setItem(self, row: int, column: int, item: ApplicationModelItem,) -> None: + def setItem( + self, + row: int, + column: int, + item: Union[ApplicationModelItem, ApplicationAllMessagesItem], + ) -> None: super(ApplicationModel, self).setItem(row, column, item) - def itemFromIndex(self, index: QtCore.QModelIndex) -> ApplicationModelItem: + def itemFromIndex( + self, index: QtCore.QModelIndex + ) -> Union[ApplicationModelItem, ApplicationAllMessagesItem]: return super(ApplicationModel, self).itemFromIndex(index) def itemFromId(self, appid: int) -> Optional[ApplicationModelItem]: diff --git a/gotify_tray/gui/models/MessagesModel.py b/gotify_tray/gui/models/MessagesModel.py new file mode 100644 index 0000000..b74d895 --- /dev/null +++ b/gotify_tray/gui/models/MessagesModel.py @@ -0,0 +1,23 @@ +import enum + +from typing import cast +from PyQt6 import QtCore, QtGui +from gotify_tray import gotify + + +class MessageItemDataRole(enum.IntEnum): + MessageRole = QtCore.Qt.ItemDataRole.UserRole + 1 + + +class MessagesModelItem(QtGui.QStandardItem): + def __init__(self, message: gotify.GotifyMessageModel, *args, **kwargs): + super(MessagesModelItem, self).__init__() + self.setData(message, MessageItemDataRole.MessageRole) + + +class MessagesModel(QtGui.QStandardItemModel): + def setItem(self, row: int, column: int, item: MessagesModelItem) -> None: + super(MessagesModel, self).setItem(row, column, item) + + def itemFromIndex(self, index: QtCore.QModelIndex) -> MessagesModelItem: + return cast(MessagesModelItem, super(MessagesModel, self).itemFromIndex(index)) diff --git a/gotify_tray/gui/models/__init__.py b/gotify_tray/gui/models/__init__.py new file mode 100644 index 0000000..e298d93 --- /dev/null +++ b/gotify_tray/gui/models/__init__.py @@ -0,0 +1,7 @@ +from .ApplicationModel import ( + ApplicationAllMessagesItem, + ApplicationModelItem, + ApplicationModel, + ApplicationItemDataRole, +) +from .MessagesModel import MessagesModelItem, MessagesModel, MessageItemDataRole diff --git a/gotify_tray/gui/widgets/MainWindow.py b/gotify_tray/gui/widgets/MainWindow.py new file mode 100644 index 0000000..6f2fe13 --- /dev/null +++ b/gotify_tray/gui/widgets/MainWindow.py @@ -0,0 +1,154 @@ +from PyQt6 import QtCore, QtGui, QtWidgets +from ..designs.widget_main import Ui_MainWindow +from .StatusWidget import StatusWidget +from ..models import ( + ApplicationModel, + MessagesModel, + MessagesModelItem, +) +from . import MessageWidget + +from gotify_tray.__version__ import __title__ +from gotify_tray.database import Settings +from gotify_tray.utils import get_abs_path + + +settings = Settings("gotify-tray") + + +class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): + refresh = QtCore.pyqtSignal() + delete_all = QtCore.pyqtSignal(QtGui.QStandardItem) + delete_message = QtCore.pyqtSignal(MessagesModelItem) + application_selection_changed = QtCore.pyqtSignal(QtGui.QStandardItem) + + def __init__( + self, application_model: ApplicationModel, messages_model: MessagesModel + ): + super(MainWindow, self).__init__() + self.setupUi(self) + + self.setWindowTitle(__title__) + + self.application_model = application_model + self.messages_model = messages_model + + self.listView_applications.setModel(application_model) + self.listView_messages.setModel(messages_model) + + # Do not expand the applications listview when resizing + self.splitter.setStretchFactor(0, 0) + self.splitter.setStretchFactor(1, 1) + + self.status_widget = StatusWidget() + self.horizontalLayout.insertWidget(0, self.status_widget) + + # Set button icons + self.pb_refresh.setIcon( + QtGui.QIcon(get_abs_path(f"gotify_tray/gui/images/refresh.svg")) + ) + self.pb_delete_all.setIcon( + QtGui.QIcon(get_abs_path(f"gotify_tray/gui/images/trashcan.svg")) + ) + + # Resize the labels and icons + size = settings.value("MainWindow/label/size", type=int) + self.status_widget.setFixedSize(QtCore.QSize(size, size)) + + size = settings.value("MainWindow/button/size", type=int) + self.pb_refresh.setFixedSize(QtCore.QSize(size, size)) + self.pb_delete_all.setFixedSize(QtCore.QSize(size, size)) + self.pb_refresh.setIconSize(QtCore.QSize(0.7 * size, 0.7 * size)) + self.pb_delete_all.setIconSize(QtCore.QSize(0.9 * size, 0.9 * size)) + + size = settings.value("MainWindow/application/icon/size", type=int) + self.listView_applications.setIconSize(QtCore.QSize(size, size)) + + font_title = QtGui.QFont() + font_title.fromString(settings.value("MainWindow/font/application", type=str)) + self.label_application.setFont(font_title) + + # Set tooltips + self.pb_refresh.setToolTip("Refresh") + self.pb_delete_all.setToolTip("Delete all messages") + + self.restore_state() + + self.link_callbacks() + + def set_active(self): + self.status_widget.set_active() + + def set_connecting(self): + self.status_widget.set_connecting() + + def set_inactive(self): + self.status_widget.set_inactive() + + def set_error(self): + self.status_widget.set_error() + + def insert_message_widget( + self, message_item: MessagesModelItem, image_path: str = "" + ): + message_widget = MessageWidget(message_item, image_path=image_path) + self.listView_messages.setIndexWidget( + self.messages_model.indexFromItem(message_item), message_widget + ) + message_widget.deletion_requested.connect(self.delete_message.emit) + + def currentApplicationIndex(self) -> QtCore.QModelIndex: + return self.listView_applications.selectionModel().currentIndex() + + def application_selection_changed_callback( + self, current: QtCore.QModelIndex, previous: QtCore.QModelIndex + ): + if item := self.application_model.itemFromIndex(current): + self.label_application.setText(item.text()) + self.application_selection_changed.emit(item) + + def delete_all_callback(self): + index = self.currentApplicationIndex() + if item := self.application_model.itemFromIndex(index): + self.delete_all.emit(item) + + def disable_applications(self): + self.listView_applications.clearSelection() + self.listView_applications.setDisabled(True) + + def enable_applications(self): + self.listView_applications.setEnabled(True) + self.listView_applications.setCurrentIndex(self.application_model.index(0, 0)) + + def bring_to_front(self): + self.ensurePolished() + self.setWindowState( + self.windowState() & ~QtCore.Qt.WindowState.WindowMinimized + | QtCore.Qt.WindowState.WindowActive + ) + self.show() + self.activateWindow() + + def link_callbacks(self): + self.pb_refresh.clicked.connect(self.refresh.emit) + self.pb_delete_all.clicked.connect(self.delete_all_callback) + + self.listView_applications.selectionModel().currentChanged.connect( + self.application_selection_changed_callback + ) + + def store_state(self): + settings.setValue("MainWindow/geometry", self.saveGeometry()) + settings.setValue("MainWindow/state", self.saveState()) + settings.setValue("MainWindow/splitter", self.splitter.saveState()) + + def restore_state(self): + if geometry := settings.value("MainWindow/geometry", type=QtCore.QByteArray): + self.restoreGeometry(geometry) + if state := settings.value("MainWindow/state", type=QtCore.QByteArray): + self.restoreState(state) + if splitter := settings.value("MainWindow/splitter", type=QtCore.QByteArray): + self.splitter.restoreState(splitter) + + def closeEvent(self, e: QtGui.QCloseEvent) -> None: + self.hide() diff --git a/gotify_tray/gui/widgets/MessageWidget.py b/gotify_tray/gui/widgets/MessageWidget.py new file mode 100644 index 0000000..d8400eb --- /dev/null +++ b/gotify_tray/gui/widgets/MessageWidget.py @@ -0,0 +1,71 @@ +from PyQt6 import QtCore, QtGui, QtWidgets + +from ..models.MessagesModel import MessageItemDataRole, MessagesModelItem +from ..designs.widget_message import Ui_Form +from gotify_tray.database import Settings +from gotify_tray.utils import convert_links + + +settings = Settings("gotify-tray") + + +class MessageWidget(QtWidgets.QWidget, Ui_Form): + deletion_requested = QtCore.pyqtSignal(MessagesModelItem) + + def __init__(self, message_item: MessagesModelItem, image_path: str = ""): + super(MessageWidget, self).__init__() + self.setupUi(self) + self.setAutoFillBackground(True) + + self.message_item = message_item + message = message_item.data(MessageItemDataRole.MessageRole) + + # Fonts + font_title = QtGui.QFont() + font_date = QtGui.QFont() + font_content = QtGui.QFont() + + font_title.fromString(settings.value("MessageWidget/font/title", type=str)) + font_date.fromString(settings.value("MessageWidget/font/date", type=str)) + font_content.fromString(settings.value("MessageWidget/font/content", type=str)) + + self.label_title.setFont(font_title) + self.label_date.setFont(font_date) + self.text_message.setFont(font_content) + + # Display message contents + self.label_title.setText(message.title) + self.label_date.setText(message.date.strftime("%Y-%m-%d, %H:%M")) + + if markdown := message.get("extras", {}).get("client::display", {}).get("contentType") == "text/markdown": + self.text_message.setTextFormat(QtCore.Qt.TextFormat.MarkdownText) + self.text_message.setText(convert_links(message.message)) + + # Show the application icon + if image_path: + image_size = settings.value("MessageWidget/image/size", type=int) + self.label_image.setFixedSize(QtCore.QSize(image_size, image_size)) + pixmap = QtGui.QPixmap(image_path).scaled(image_size, image_size, aspectRatioMode=QtCore.Qt.AspectRatioMode.KeepAspectRatioByExpanding) + self.label_image.setPixmap(pixmap) + else: + self.label_image.hide() + + # Set MessagesModelItem's size hint based on the size of this widget + self.gridLayout_frame.setContentsMargins(10, 5, 10, 5) + self.gridLayout.setContentsMargins(5, 15, 5, 15) + self.adjustSize() + size_hint = self.message_item.sizeHint() + self.message_item.setSizeHint( + QtCore.QSize( + size_hint.width(), + self.height() + ) + ) + + self.pb_delete.setIcon(QtGui.QIcon("gotify_tray/gui/images/trashcan.svg")) + self.pb_delete.setIconSize(QtCore.QSize(24, 24)) + + self.link_callbacks() + + def link_callbacks(self): + self.pb_delete.clicked.connect(lambda: self.deletion_requested.emit(self.message_item)) \ No newline at end of file diff --git a/gotify_tray/gui/ServerInfoDialog.py b/gotify_tray/gui/widgets/ServerInfoDialog.py similarity index 98% rename from gotify_tray/gui/ServerInfoDialog.py rename to gotify_tray/gui/widgets/ServerInfoDialog.py index 6871d67..3c30ac3 100644 --- a/gotify_tray/gui/ServerInfoDialog.py +++ b/gotify_tray/gui/widgets/ServerInfoDialog.py @@ -2,7 +2,7 @@ from gotify_tray.gotify.models import GotifyVersionModel from gotify_tray.tasks import VerifyServerInfoTask from PyQt6 import QtWidgets -from .designs.widget_server import Ui_Dialog +from ..designs.widget_server import Ui_Dialog class ServerInfoDialog(QtWidgets.QDialog, Ui_Dialog): diff --git a/gotify_tray/gui/SettingsDialog.py b/gotify_tray/gui/widgets/SettingsDialog.py similarity index 98% rename from gotify_tray/gui/SettingsDialog.py rename to gotify_tray/gui/widgets/SettingsDialog.py index 17e41ff..72b5150 100644 --- a/gotify_tray/gui/SettingsDialog.py +++ b/gotify_tray/gui/widgets/SettingsDialog.py @@ -5,7 +5,7 @@ from gotify_tray.database import Settings from gotify_tray.utils import verify_server from PyQt6 import QtCore, QtGui, QtWidgets -from .designs.widget_settings import Ui_Dialog +from ..designs.widget_settings import Ui_Dialog logger = logging.getLogger("gotify-tray") diff --git a/gotify_tray/gui/widgets/StatusWidget.py b/gotify_tray/gui/widgets/StatusWidget.py new file mode 100644 index 0000000..25ff5ae --- /dev/null +++ b/gotify_tray/gui/widgets/StatusWidget.py @@ -0,0 +1,34 @@ +from PyQt6 import QtCore, QtGui, QtWidgets + +from gotify_tray.database import Settings +from gotify_tray.utils import get_abs_path + + +settings = Settings("gotify-tray") + + +class StatusWidget(QtWidgets.QLabel): + def __init__(self): + super(StatusWidget, self).__init__() + self.setFixedSize(QtCore.QSize(20, 20)) + self.setScaledContents(True) + self.set_connecting() + + def set_status(self, image: str): + self.setPixmap(QtGui.QPixmap(get_abs_path(f"gotify_tray/gui/images/{image}"))) + + def set_active(self): + self.setToolTip("Listening for new messages") + self.set_status("status_active.svg") + + def set_connecting(self): + self.setToolTip("Connecting...") + self.set_status("status_connecting.svg") + + def set_inactive(self): + self.setToolTip("Listener inactive") + self.set_status("status_inactive.svg") + + def set_error(self): + self.setToolTip("Listener error") + self.set_status("status_error.svg") diff --git a/gotify_tray/gui/Tray.py b/gotify_tray/gui/widgets/Tray.py similarity index 89% rename from gotify_tray/gui/Tray.py rename to gotify_tray/gui/widgets/Tray.py index 4030166..54ca1a0 100644 --- a/gotify_tray/gui/Tray.py +++ b/gotify_tray/gui/widgets/Tray.py @@ -27,6 +27,11 @@ class Tray(QtWidgets.QSystemTrayIcon): menu.addSeparator() + self.actionShowWindow = QtGui.QAction("Show Window", self) + menu.addAction(self.actionShowWindow) + + menu.addSeparator() + self.actionReconnect = QtGui.QAction("Reconnect", self) menu.addAction(self.actionReconnect) diff --git a/gotify_tray/gui/widgets/__init__.py b/gotify_tray/gui/widgets/__init__.py new file mode 100644 index 0000000..d43f65f --- /dev/null +++ b/gotify_tray/gui/widgets/__init__.py @@ -0,0 +1,6 @@ +from .MessageWidget import MessageWidget +from .MainWindow import MainWindow +from .ServerInfoDialog import ServerInfoDialog +from .SettingsDialog import SettingsDialog +from .StatusWidget import StatusWidget +from .Tray import Tray diff --git a/gotify_tray/tasks.py b/gotify_tray/tasks.py index 6143fd6..b74c81b 100644 --- a/gotify_tray/tasks.py +++ b/gotify_tray/tasks.py @@ -38,6 +38,32 @@ class BaseTask(QtCore.QThread): self.running = False +class DeleteMessageTask(BaseTask): + deleted = pyqtSignal(bool) + + def __init__(self, message_id: int, gotify_client: gotify.GotifyClient): + super(DeleteMessageTask, self).__init__() + self.message_id = message_id + self.gotify_client = gotify_client + + def task(self): + success = self.gotify_client.delete_message(self.message_id) + self.deleted.emit(success) + + +class DeleteApplicationMessagesTask(BaseTask): + deleted = pyqtSignal(bool) + + def __init__(self, appid: int, gotify_client: gotify.GotifyClient): + super(DeleteApplicationMessagesTask, self).__init__() + self.appid = appid + self.gotify_client = gotify_client + + def task(self): + success = self.gotify_client.delete_application_messages(self.appid) + self.deleted.emit(success) + + class DeleteAllMessagesTask(BaseTask): deleted = pyqtSignal(bool) @@ -66,6 +92,39 @@ class GetApplicationsTask(BaseTask): self.success.emit(result) +class GetApplicationMessagesTask(BaseTask): + success = pyqtSignal(gotify.GotifyPagedMessagesModel) + error = pyqtSignal(gotify.GotifyErrorModel) + + def __init__(self, appid: int, gotify_client: gotify.GotifyClient): + super(GetApplicationMessagesTask, self).__init__() + self.appid = appid + self.gotify_client = gotify_client + + def task(self): + result = self.gotify_client.get_application_messages(self.appid) + if isinstance(result, gotify.GotifyErrorModel): + self.error.emit(result) + else: + self.success.emit(result) + + +class GetMessagesTask(BaseTask): + success = pyqtSignal(gotify.GotifyPagedMessagesModel) + error = pyqtSignal(gotify.GotifyErrorModel) + + def __init__(self, gotify_client: gotify.GotifyClient): + super(GetMessagesTask, self).__init__() + self.gotify_client = gotify_client + + def task(self): + result = self.gotify_client.get_messages() + if isinstance(result, gotify.GotifyErrorModel): + self.error.emit(result) + else: + self.success.emit(result) + + class VerifyServerInfoTask(BaseTask): success = pyqtSignal(GotifyVersionModel) incorrect_token = pyqtSignal(GotifyVersionModel) diff --git a/gotify_tray/utils.py b/gotify_tray/utils.py index 12bbbba..46935ef 100644 --- a/gotify_tray/utils.py +++ b/gotify_tray/utils.py @@ -1,3 +1,9 @@ +import os +import re + +from pathlib import Path + + def verify_server(force_new: bool = False) -> bool: from gotify_tray.gui import ServerInfoDialog from gotify_tray.database import Settings @@ -17,3 +23,26 @@ def verify_server(force_new: bool = False) -> bool: return False else: return True + + +def convert_links(text): + _link = re.compile( + r'(?:(https://|http://)|(www\.))(\S+\b/?)([!"#$%&\'()*+,\-./:;<=>?@[\\\]^_`{|}~]*)(\s|$)', + re.I, + ) + + def replace(match): + groups = match.groups() + protocol = groups[0] or "" # may be None + www_lead = groups[1] or "" # may be None + return '{0}{1}{2}{3}{4}'.format( + protocol, www_lead, *groups[2:] + ) + + return _link.sub(replace, text) + + +def get_abs_path(s) -> str: + h = Path(__file__).parent.parent + p = Path(s) + return os.path.join(h, p).replace("\\", "/")