diff --git a/gotify_tray/database/cache.py b/gotify_tray/database/cache.py
index e7ebcd5..ac62dac 100644
--- a/gotify_tray/database/cache.py
+++ b/gotify_tray/database/cache.py
@@ -32,6 +32,9 @@ class Cache(object):
self.cache_dir = os.path.join(path, "cache")
os.makedirs(self.cache_dir, exist_ok=True)
+ def directory(self) -> str:
+ return self.cache_dir
+
def clear(self):
self.cursor.execute("DELETE FROM cache")
self.database.commit()
diff --git a/gotify_tray/database/default_settings.py b/gotify_tray/database/default_settings.py
index fa03269..500ffd8 100644
--- a/gotify_tray/database/default_settings.py
+++ b/gotify_tray/database/default_settings.py
@@ -19,6 +19,8 @@ DEFAULT_SETTINGS = {
"tray/icon/unread": False,
"watchdog/interval/s": 60,
"MessageWidget/image/size": 33,
+ "MessageWidget/content_image/W_percentage": 1.0,
+ "MessageWidget/content_image/H_percentage": 0.5,
"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 0bf2d91..19eba73 100644
--- a/gotify_tray/gui/MainApplication.py
+++ b/gotify_tray/gui/MainApplication.py
@@ -16,6 +16,8 @@ from gotify_tray.tasks import (
GetApplicationsTask,
GetApplicationMessagesTask,
GetMessagesTask,
+ ProcessMessageTask,
+ ProcessMessagesTask,
ServerConnectionWatchdogTask,
)
from gotify_tray.gui.themes import set_theme
@@ -74,6 +76,8 @@ class MainApplication(QtWidgets.QApplication):
self.application_model = ApplicationModel()
self.main_window = MainWindow(self.application_model, self.messages_model)
+ self.main_window.show() # The initial .show() is necessary to get the correct sizes when adding MessageWigets
+ QtCore.QTimer.singleShot(0, self.main_window.hide)
self.refresh_applications()
@@ -195,43 +199,39 @@ class MainApplication(QtWidgets.QApplication):
),
)
+ def get_messages_finished_callback(self, page: gotify.GotifyPagedMessagesModel):
+ """Process messages before inserting them into the main window
+ """
+
+ def insert_helper(row: int, message: gotify.GotifyMessageModel):
+ if item := self.application_model.itemFromId(message.appid):
+ self.insert_message(
+ row, message, item.data(ApplicationItemDataRole.ApplicationRole),
+ )
+ self.processEvents()
+
+ self.process_messages_task = ProcessMessagesTask(page)
+ self.process_messages_task.message_processed.connect(insert_helper)
+ self.process_messages_task.start()
+
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_messages_finished_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.success.connect(self.get_messages_finished_callback)
self.get_messages_task.start()
def add_message_to_model(self, message: gotify.GotifyMessageModel):
@@ -240,14 +240,27 @@ class MainApplication(QtWidgets.QApplication):
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
+
+ def insert_message_helper():
+ 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,
@@ -255,13 +268,15 @@ class MainApplication(QtWidgets.QApplication):
ApplicationItemDataRole.ApplicationRole
),
)
- elif isinstance(selected_application_item, ApplicationAllMessagesItem):
- # "All messages' is selected
- self.insert_message(
- 0,
- message,
- application_item.data(ApplicationItemDataRole.ApplicationRole),
- )
+
+ self.process_message_task = ProcessMessageTask(message)
+ self.process_message_task.finished.connect(insert_message_helper)
+ self.process_message_task.start()
+ else:
+ logger.error(
+ f"App id {message.appid} could not be found. Refreshing applications."
+ )
+ self.refresh_applications()
def new_message_callback(self, message: gotify.GotifyMessageModel):
self.add_message_to_model(message)
@@ -335,18 +350,13 @@ class MainApplication(QtWidgets.QApplication):
if image_popup := getattr(self, "image_popup", None):
image_popup.close()
- def refresh_callback(self):
- # Manual refresh -> also reset the image cache
- Cache().clear()
- self.refresh_applications()
-
def theme_change_requested_callback(self, theme: str):
# Set the theme
set_theme(self, theme)
# Update the main window icons
self.main_window.set_icons()
-
+
# Update the message widget icons
for r in range(self.messages_model.rowCount()):
message_widget: MessageWidget = self.main_window.listView_messages.indexWidget(
@@ -400,7 +410,7 @@ class MainApplication(QtWidgets.QApplication):
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)
+ 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
diff --git a/gotify_tray/gui/designs/widget_settings.py b/gotify_tray/gui/designs/widget_settings.py
index 31df766..97e7ac8 100644
--- a/gotify_tray/gui/designs/widget_settings.py
+++ b/gotify_tray/gui/designs/widget_settings.py
@@ -171,6 +171,22 @@ class Ui_Dialog(object):
self.horizontalLayout_4.addItem(spacerItem5)
self.gridLayout_2.addLayout(self.horizontalLayout_4, 0, 0, 1, 1)
self.verticalLayout.addWidget(self.groupbox_image_popup)
+ self.groupBox_cache = QtWidgets.QGroupBox(self.tab_advanced)
+ self.groupBox_cache.setObjectName("groupBox_cache")
+ self.horizontalLayout_6 = QtWidgets.QHBoxLayout(self.groupBox_cache)
+ self.horizontalLayout_6.setObjectName("horizontalLayout_6")
+ self.pb_clear_cache = QtWidgets.QPushButton(self.groupBox_cache)
+ self.pb_clear_cache.setObjectName("pb_clear_cache")
+ self.horizontalLayout_6.addWidget(self.pb_clear_cache)
+ self.pb_open_cache_dir = QtWidgets.QPushButton(self.groupBox_cache)
+ self.pb_open_cache_dir.setObjectName("pb_open_cache_dir")
+ self.horizontalLayout_6.addWidget(self.pb_open_cache_dir)
+ self.label_cache = QtWidgets.QLabel(self.groupBox_cache)
+ self.label_cache.setObjectName("label_cache")
+ self.horizontalLayout_6.addWidget(self.label_cache)
+ spacerItem6 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
+ self.horizontalLayout_6.addItem(spacerItem6)
+ self.verticalLayout.addWidget(self.groupBox_cache)
self.groupBox_logging = QtWidgets.QGroupBox(self.tab_advanced)
self.groupBox_logging.setObjectName("groupBox_logging")
self.gridLayout_6 = QtWidgets.QGridLayout(self.groupBox_logging)
@@ -178,8 +194,8 @@ class Ui_Dialog(object):
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)
- spacerItem6 = QtWidgets.QSpacerItem(190, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
- self.gridLayout_6.addItem(spacerItem6, 0, 3, 1, 1)
+ spacerItem7 = QtWidgets.QSpacerItem(190, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
+ self.gridLayout_6.addItem(spacerItem7, 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")
@@ -188,8 +204,8 @@ class Ui_Dialog(object):
self.label_logging.setObjectName("label_logging")
self.gridLayout_6.addWidget(self.label_logging, 0, 0, 1, 1)
self.verticalLayout.addWidget(self.groupBox_logging)
- spacerItem7 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
- self.verticalLayout.addItem(spacerItem7)
+ spacerItem8 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
+ self.verticalLayout.addItem(spacerItem8)
self.tabWidget.addTab(self.tab_advanced, "")
self.gridLayout.addWidget(self.tabWidget, 0, 0, 1, 1)
@@ -238,6 +254,10 @@ class Ui_Dialog(object):
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_cache.setTitle(_translate("Dialog", "Cache"))
+ self.pb_clear_cache.setText(_translate("Dialog", "Clear"))
+ self.pb_open_cache_dir.setText(_translate("Dialog", "Open"))
+ self.label_cache.setText(_translate("Dialog", "TextLabel"))
self.groupBox_logging.setTitle(_translate("Dialog", "Logging"))
self.pb_open_log.setToolTip(_translate("Dialog", "Open logfile"))
self.pb_open_log.setText(_translate("Dialog", "..."))
diff --git a/gotify_tray/gui/designs/widget_settings.ui b/gotify_tray/gui/designs/widget_settings.ui
index 5660f81..fc75ed8 100644
--- a/gotify_tray/gui/designs/widget_settings.ui
+++ b/gotify_tray/gui/designs/widget_settings.ui
@@ -400,6 +400,49 @@
+ -
+
+
+ Cache
+
+
+
-
+
+
+ Clear
+
+
+
+ -
+
+
+ Open
+
+
+
+ -
+
+
+ TextLabel
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
-
diff --git a/gotify_tray/gui/widgets/MainWindow.py b/gotify_tray/gui/widgets/MainWindow.py
index 78c7c92..a4c095b 100644
--- a/gotify_tray/gui/widgets/MainWindow.py
+++ b/gotify_tray/gui/widgets/MainWindow.py
@@ -30,7 +30,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
):
super(MainWindow, self).__init__()
self.setupUi(self)
-
+
self.installEventFilter(self)
self.setWindowTitle(__title__)
@@ -85,7 +85,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
size = settings.value("MainWindow/application/icon/size", type=int)
self.listView_applications.setIconSize(QtCore.QSize(size, size))
-
+
# Refresh the status widget
self.status_widget.refresh()
@@ -104,7 +104,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def insert_message_widget(
self, message_item: MessagesModelItem, image_path: str = ""
):
- message_widget = MessageWidget(message_item, image_path=image_path)
+ message_widget = MessageWidget(
+ self.listView_messages, message_item, image_path=image_path
+ )
self.listView_messages.setIndexWidget(
self.messages_model.indexFromItem(message_item), message_widget
)
diff --git a/gotify_tray/gui/widgets/MessageWidget.py b/gotify_tray/gui/widgets/MessageWidget.py
index 339963d..8916f28 100644
--- a/gotify_tray/gui/widgets/MessageWidget.py
+++ b/gotify_tray/gui/widgets/MessageWidget.py
@@ -4,9 +4,11 @@ from PyQt6 import QtCore, QtGui, QtWidgets
from ..models.MessagesModel import MessageItemDataRole, MessagesModelItem
from ..designs.widget_message import Ui_Form
+from gotify_tray.database import Downloader
from gotify_tray.database import Settings
-from gotify_tray.utils import convert_links
+from gotify_tray.utils import convert_links, get_image
from gotify_tray.gui.themes import get_theme_file
+from gotify_tray.gotify.models import GotifyMessageModel
settings = Settings("gotify-tray")
@@ -16,13 +18,18 @@ 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 = ""):
+ def __init__(
+ self,
+ parent: QtWidgets.QWidget,
+ message_item: MessagesModelItem,
+ image_path: str = "",
+ ):
super(MessageWidget, self).__init__()
+ self.parent = parent
self.setupUi(self)
self.setAutoFillBackground(True)
-
self.message_item = message_item
- message = message_item.data(MessageItemDataRole.MessageRole)
+ message: GotifyMessageModel = message_item.data(MessageItemDataRole.MessageRole)
# Fonts
self.set_fonts()
@@ -31,14 +38,20 @@ class MessageWidget(QtWidgets.QWidget, Ui_Form):
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":
+ if markdown := (
+ message.get("extras", {}).get("client::display", {}).get("contentType")
+ == "text/markdown"
+ ):
self.label_message.setTextFormat(QtCore.Qt.TextFormat.MarkdownText)
- self.label_message.setText(convert_links(message.message))
+ # If the message is only an image URL, then instead of showing the message,
+ # download the image and show it in the message label
+ if image_url := get_image(message.message):
+ downloader = Downloader()
+ filename = downloader.get_filename(image_url)
+ self.set_message_image(filename)
+ else:
+ self.label_message.setText(convert_links(message.message))
# Show the application icon
if image_path:
@@ -91,6 +104,27 @@ class MessageWidget(QtWidgets.QWidget, Ui_Form):
self.pb_delete.setIcon(QtGui.QIcon(get_theme_file("trashcan.svg")))
self.pb_delete.setIconSize(QtCore.QSize(24, 24))
+ def set_message_image(self, filename: str):
+ pixmap = QtGui.QPixmap(filename)
+
+ # Make sure the image fits within the listView
+ W = settings.value("MessageWidget/content_image/W_percentage", type=float) * (
+ self.parent.width() - self.label_image.width()
+ )
+ H = (
+ settings.value("MessageWidget/content_image/H_percentage", type=float)
+ * self.parent.height()
+ )
+
+ if pixmap.width() > W or pixmap.height() > H:
+ pixmap = pixmap.scaled(
+ QtCore.QSize(int(W), int(H)),
+ aspectRatioMode=QtCore.Qt.AspectRatioMode.KeepAspectRatio,
+ transformMode=QtCore.Qt.TransformationMode.SmoothTransformation,
+ )
+
+ self.label_message.setPixmap(pixmap)
+
def link_hovered_callback(self, link: str):
if not settings.value("ImagePopup/enabled", type=bool):
return
diff --git a/gotify_tray/gui/widgets/SettingsDialog.py b/gotify_tray/gui/widgets/SettingsDialog.py
index ab66dab..c37171f 100644
--- a/gotify_tray/gui/widgets/SettingsDialog.py
+++ b/gotify_tray/gui/widgets/SettingsDialog.py
@@ -2,12 +2,12 @@ import logging
import platform
import os
-from gotify_tray.database import Settings
+from gotify_tray.database import Cache, Settings
from gotify_tray.gotify import GotifyMessageModel
from gotify_tray.gui.models import MessagesModelItem
from . import MessageWidget
from gotify_tray.utils import get_icon, verify_server, open_file
-from gotify_tray.tasks import ExportSettingsTask, ImportSettingsTask
+from gotify_tray.tasks import ExportSettingsTask, ImportSettingsTask, CacheSizeTask, ClearCacheTask
from gotify_tray.gui.themes import get_themes
from PyQt6 import QtCore, QtGui, QtWidgets
@@ -91,9 +91,12 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
)
self.spin_popup_w.setValue(settings.value("ImagePopup/w", type=int))
self.spin_popup_h.setValue(settings.value("ImagePopup/h", type=int))
+ self.label_cache.setText("0 MB")
+ self.compute_cache_size()
def add_message_widget(self):
self.message_widget = MessageWidget(
+ self,
MessagesModelItem(
GotifyMessageModel(
{
@@ -107,6 +110,11 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
)
self.layout_fonts_message.addWidget(self.message_widget)
+ def compute_cache_size(self):
+ self.cache_size_task = CacheSizeTask()
+ self.cache_size_task.size.connect(lambda size: self.label_cache.setText(f"{round(size/1e6, 1)} MB"))
+ self.cache_size_task.start()
+
def change_server_info_callback(self):
self.server_changed = verify_server(force_new=True, enable_import=False)
@@ -179,6 +187,11 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
settings.clear()
self.quit_requested.emit()
+ def clear_cache_callback(self):
+ self.clear_cache_task = ClearCacheTask()
+ self.clear_cache_task.start()
+ self.label_cache.setText("0 MB")
+
def link_callbacks(self):
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Apply
@@ -223,6 +236,8 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
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)
+ self.pb_clear_cache.clicked.connect(self.clear_cache_callback)
+ self.pb_open_cache_dir.clicked.connect(lambda: open_file(Cache().directory()))
def apply_settings(self):
# Priority
diff --git a/gotify_tray/tasks.py b/gotify_tray/tasks.py
index d8a821d..d9e0233 100644
--- a/gotify_tray/tasks.py
+++ b/gotify_tray/tasks.py
@@ -1,13 +1,17 @@
import abc
+import glob
import logging
import time
+import os
+from functools import reduce
from PyQt6 import QtCore
from PyQt6.QtCore import pyqtSignal
-from gotify_tray.database import Settings
+from gotify_tray.database import Cache, Downloader, Settings
from gotify_tray.gotify.api import GotifyClient
from gotify_tray.gotify.models import GotifyVersionModel
+from gotify_tray.utils import get_image
from . import gotify
@@ -199,3 +203,48 @@ class ImportSettingsTask(BaseTask):
def task(self):
settings.load(self.path)
self.success.emit()
+
+
+class ProcessMessageTask(BaseTask):
+ def __init__(self, message: gotify.GotifyMessageModel):
+ super(ProcessMessageTask, self).__init__()
+ self.message = message
+
+ def task(self):
+ if image_url := get_image(self.message.message):
+ downloader = Downloader()
+ downloader.get_filename(image_url)
+
+
+class ProcessMessagesTask(BaseTask):
+ message_processed = pyqtSignal(int, gotify.GotifyMessageModel)
+
+ def __init__(self, page: gotify.GotifyPagedMessagesModel):
+ super(ProcessMessagesTask, self).__init__()
+ self.page = page
+
+ def task(self):
+ downloader = Downloader()
+ for i, message in enumerate(self.page.messages):
+ if image_url := get_image(message.message):
+ downloader.get_filename(image_url)
+ self.message_processed.emit(i, message)
+
+ # Prevent locking up the UI when there are a lot of messages with images ready at the same time
+ time.sleep(0.001)
+
+
+class CacheSizeTask(BaseTask):
+ size = pyqtSignal(int)
+
+ def task(self):
+ cache_dir = Cache().directory()
+ if os.path.exists(cache_dir):
+ cache_size_bytes = reduce(lambda x, f: x + os.path.getsize(f), glob.glob(os.path.join(cache_dir, "*")), 0)
+ self.size.emit(cache_size_bytes)
+
+class ClearCacheTask(BaseTask):
+ def task(self):
+ cache = Cache()
+ cache.clear()
+
\ No newline at end of file
diff --git a/gotify_tray/utils.py b/gotify_tray/utils.py
index d36c3cf..b087872 100644
--- a/gotify_tray/utils.py
+++ b/gotify_tray/utils.py
@@ -4,6 +4,7 @@ import re
import subprocess
from pathlib import Path
+from typing import Optional
def verify_server(force_new: bool = False, enable_import: bool = True) -> bool:
@@ -44,6 +45,25 @@ def convert_links(text):
return _link.sub(replace, text)
+def get_image(s: str) -> Optional[str]:
+ """If `s` contains only an image URL, this function returns that URL.
+ This function also extracts a URL in the `![]()` markdown image format.
+ """
+ s = s.strip()
+
+ # Return True if 's' is a url and has an image extension
+ RE = r'(?:(https://|http://)|(www\.))(\S+\b/?)([!"#$%&\'()*+,\-./:;<=>?@[\\\]^_`{|}~]*).(jpg|jpeg|png|bmp|gif)(\s|$)'
+ if re.compile(RE, re.I).fullmatch(s) is not None:
+ return s
+
+ # Return True if 's' has the markdown image format
+ RE = r'!\[[^\]]*\]\((.*?)\s*("(?:.*[^"])")?\s*\)'
+ if re.compile(RE, re.I).fullmatch(s) is not None:
+ return re.compile(RE, re.I).findall(s)[0][0]
+
+ return None
+
+
def get_abs_path(s) -> str:
h = Path(__file__).parent.parent
p = Path(s)