Merge branch 'cleanup' into develop

This commit is contained in:
dries.k
2023-05-22 23:19:59 +02:00
16 changed files with 223 additions and 338 deletions

View File

@@ -18,6 +18,7 @@ DEFAULT_SETTINGS = {
"tray/notifications/click": True,
"tray/icon/unread": False,
"watchdog/interval/s": 60,
"MessageWidget/height/min": 100,
"MessageWidget/image/size": 33,
"MessageWidget/content_image/W_percentage": 1.0,
"MessageWidget/content_image/H_percentage": 0.5,
@@ -29,4 +30,4 @@ DEFAULT_SETTINGS = {
"ImagePopup/extensions": [".jpg", ".jpeg", ".png", ".svg"],
"ImagePopup/w": 400,
"ImagePopup/h": 400,
}
}

View File

@@ -1,5 +1,5 @@
import logging
from typing import Callable, List, Optional, Union
from typing import Callable
import requests
@@ -22,12 +22,12 @@ class GotifySession(object):
self.session = requests.Session()
self.update_auth(url.rstrip("/"), token)
def update_auth(self, url: str = None, token: str = None):
def update_auth(self, url: str | None = None, token: str | None = None):
if url:
self.url = url
if token:
self.token = token
self.session.headers.update({"X-Gotify-Key": token})
self.session.headers.update({"X-Gotify-Key": token})
def _get(self, endpoint: str = "/", **kwargs) -> requests.Response:
return self.session.get(self.url + endpoint, **kwargs)
@@ -50,8 +50,8 @@ class GotifyApplication(GotifySession):
super(GotifyApplication, self).__init__(url, application_token)
def push(
self, title: str = "", message: str = "", priority: int = 0, extras: dict = None
) -> Union[GotifyMessageModel, GotifyErrorModel]:
self, title: str = "", message: str = "", priority: int = 0, extras: dict | None = None
) -> GotifyMessageModel | GotifyErrorModel:
response = self._post(
"/message",
json={
@@ -77,7 +77,7 @@ class GotifyClient(GotifySession):
Application
"""
def get_applications(self) -> Union[List[GotifyApplicationModel], GotifyErrorModel]:
def get_applications(self) -> list[GotifyApplicationModel] | GotifyErrorModel:
response = self._get("/application")
return (
[GotifyApplicationModel(x) for x in response.json()]
@@ -87,7 +87,7 @@ class GotifyClient(GotifySession):
def create_application(
self, name: str, description: str = ""
) -> Union[GotifyApplicationModel, GotifyErrorModel]:
) -> GotifyApplicationModel | GotifyErrorModel:
response = self._post(
"/application", json={"name": name, "description": description}
)
@@ -99,7 +99,7 @@ class GotifyClient(GotifySession):
def update_application(
self, application_id: int, name: str, description: str = ""
) -> Union[GotifyApplicationModel, GotifyErrorModel]:
) -> GotifyApplicationModel | GotifyErrorModel:
response = self._put(
f"/application/{application_id}",
json={"name": name, "description": description},
@@ -115,7 +115,7 @@ class GotifyClient(GotifySession):
def upload_application_image(
self, application_id: int, img_path: str
) -> Optional[Union[GotifyApplicationModel, GotifyErrorModel]]:
) -> GotifyApplicationModel | GotifyErrorModel | None:
try:
with open(img_path, "rb") as f:
response = self._post(
@@ -137,8 +137,8 @@ class GotifyClient(GotifySession):
"""
def get_application_messages(
self, application_id: int, limit: int = 100, since: int = None
) -> Union[GotifyPagedMessagesModel, GotifyErrorModel]:
self, application_id: int, limit: int = 100, since: int | None = None
) -> GotifyPagedMessagesModel | GotifyErrorModel:
response = self._get(
f"/application/{application_id}/message",
params={"limit": limit, "since": since},
@@ -155,8 +155,8 @@ class GotifyClient(GotifySession):
return self._delete(f"/application/{application_id}/message").ok
def get_messages(
self, limit: int = 100, since: int = None
) -> Union[GotifyPagedMessagesModel, GotifyErrorModel]:
self, limit: int = 100, since: int | None = None
) -> GotifyPagedMessagesModel | GotifyErrorModel:
response = self._get("/message", params={"limit": limit, "since": since})
if not response.ok:
return GotifyErrorModel(response)
@@ -174,10 +174,10 @@ class GotifyClient(GotifySession):
def listen(
self,
opened_callback: Callable[[], None] = None,
closed_callback: Callable[[int, str], None] = None,
new_message_callback: Callable[[GotifyMessageModel], None] = None,
error_callback: Callable[[Exception], None] = None,
opened_callback: (Callable[[], None]) | None = None,
closed_callback: Callable[[int, str], None] | None = None,
new_message_callback: Callable[[GotifyMessageModel], None] | None = None,
error_callback: Callable[[Exception], None] | None = None,
):
def dummy(*args):
...
@@ -189,7 +189,7 @@ class GotifyClient(GotifySession):
self.listener.error.connect(error_callback or dummy)
self.listener.start()
def opened_callback(self, user_callback: Callable[[], None] = None):
def opened_callback(self, user_callback: Callable[[], None] | None = None):
self.reset_wait_time()
if user_callback:
user_callback()
@@ -222,7 +222,7 @@ class GotifyClient(GotifySession):
Health
"""
def health(self) -> Union[GotifyHealthModel, GotifyErrorModel]:
def health(self) -> GotifyHealthModel | GotifyErrorModel:
response = self._get("/health")
return (
GotifyHealthModel(response.json())
@@ -234,7 +234,7 @@ class GotifyClient(GotifySession):
Version
"""
def version(self) -> Union[GotifyVersionModel, GotifyErrorModel]:
def version(self) -> GotifyVersionModel | GotifyErrorModel:
response = self._get("/version")
return (
GotifyVersionModel(response.json())

View File

@@ -1,7 +1,6 @@
import datetime
from dateutil.parser import isoparse
import logging
from typing import List, Optional
import requests
@@ -33,7 +32,7 @@ class GotifyApplicationModel(AttributeDict):
class GotifyPagingModel(AttributeDict):
limit: int
next: Optional[str] = None
next: str | None = None
since: int
size: int
@@ -41,11 +40,11 @@ class GotifyPagingModel(AttributeDict):
class GotifyMessageModel(AttributeDict):
appid: int
date: datetime.datetime
extras: Optional[dict] = None
extras: dict | None = None
id: int
message: str
priority: Optional[int] = None
title: Optional[str] = None
priority: int | None = None
title: str | None = None
def __init__(self, d: dict, *args, **kwargs):
d.update(
@@ -55,7 +54,7 @@ class GotifyMessageModel(AttributeDict):
class GotifyPagedMessagesModel(AttributeDict):
messages: List[GotifyMessageModel]
messages: list[GotifyMessageModel]
paging: GotifyPagingModel

View File

@@ -4,7 +4,6 @@ import os
import platform
import sys
import tempfile
from typing import List, Union
from gotify_tray import gotify
from gotify_tray.__version__ import __title__
@@ -18,7 +17,6 @@ from gotify_tray.tasks import (
GetApplicationMessagesTask,
GetMessagesTask,
ProcessMessageTask,
ProcessMessagesTask,
ServerConnectionWatchdogTask,
)
from gotify_tray.gui.themes import set_theme
@@ -51,9 +49,7 @@ def init_logger(logger: logging.Logger):
else:
logging.disable()
logdir = QtCore.QStandardPaths.standardLocations(
QtCore.QStandardPaths.StandardLocation.AppDataLocation
)[0]
logdir = QtCore.QStandardPaths.standardLocations(QtCore.QStandardPaths.StandardLocation.AppDataLocation)[0]
if not os.path.exists(logdir):
os.mkdir(logdir)
logging.basicConfig(
@@ -88,9 +84,9 @@ class MainApplication(QtWidgets.QApplication):
self.first_connect = True
self.gotify_client.listen(
new_message_callback=self.new_message_callback,
opened_callback=self.listener_opened_callback,
closed_callback=self.listener_closed_callback,
new_message_callback=self.new_message_callback,
error_callback=self.listener_error_callback,
)
@@ -108,29 +104,17 @@ class MainApplication(QtWidgets.QApplication):
self.application_model.setItem(0, 0, ApplicationAllMessagesItem())
self.get_applications_task = GetApplicationsTask(self.gotify_client)
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.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],
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),
)
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 update_last_id(self, i: int):
if i > settings.value("message/last", type=int):
@@ -153,9 +137,9 @@ class MainApplication(QtWidgets.QApplication):
for message in page.messages:
if message.id > last_id:
if settings.value("message/check_missed/notify", type=bool):
self.new_message_callback(message)
self.new_message_callback(message, process=False)
else:
self.add_message_to_model(message)
self.add_message_to_model(message, process=False)
ids.append(message.id)
if ids:
@@ -169,9 +153,7 @@ class MainApplication(QtWidgets.QApplication):
self.main_window.set_connecting()
self.tray.set_icon_error()
self.gotify_client.increase_wait_time()
QtCore.QTimer.singleShot(
self.gotify_client.get_wait_time() * 1000, self.gotify_client.reconnect
)
QtCore.QTimer.singleShot(self.gotify_client.get_wait_time() * 1000, self.gotify_client.reconnect)
def listener_error_callback(self, exception: Exception):
self.main_window.set_connecting()
@@ -184,51 +166,8 @@ class MainApplication(QtWidgets.QApplication):
else:
self.gotify_client.stop(reset_wait=True)
def insert_message(
self,
row: int,
message: gotify.GotifyMessageModel,
application: gotify.GotifyApplicationModel,
):
"""Insert a message gotify message into the messages model. Also add the message widget to the listview
Args:
row (int): >=0: insert message at specified position, <0: append message to the end of the model
message (gotify.GotifyMessageModel): message
application (gotify.GotifyApplicationModel): application
"""
self.update_last_id(message.id)
message_item = MessagesModelItem(message)
if row >= 0:
self.messages_model.insertRow(row, message_item)
else:
self.messages_model.appendRow(message_item)
self.main_window.insert_message_widget(
message_item,
self.downloader.get_filename(
f"{self.gotify_client.url}/{application.image}"
),
)
def get_messages_finished_callback(self, page: gotify.GotifyPagedMessagesModel):
"""Process messages before inserting them into the main window
"""
def insert_helper(message: gotify.GotifyMessageModel):
if item := self.application_model.itemFromId(message.appid):
self.insert_message(
-1, 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, item: ApplicationModelItem | ApplicationAllMessagesItem
):
self.messages_model.clear()
@@ -237,62 +176,44 @@ class MainApplication(QtWidgets.QApplication):
item.data(ApplicationItemDataRole.ApplicationRole).id,
self.gotify_client,
)
self.get_application_messages_task.success.connect(
self.get_messages_finished_callback
)
self.get_application_messages_task.message.connect(self.messages_model.append_message)
self.get_application_messages_task.start()
elif isinstance(item, ApplicationAllMessagesItem):
self.get_messages_task = GetMessagesTask(self.gotify_client)
self.get_messages_task.success.connect(self.get_messages_finished_callback)
self.get_messages_task.message.connect(self.messages_model.append_message)
self.get_messages_task.start()
def add_message_to_model(self, message: gotify.GotifyMessageModel):
if application_item := self.application_model.itemFromId(message.appid):
def add_message_to_model(self, message: gotify.GotifyMessageModel, process: bool = True):
if self.application_model.itemFromId(message.appid):
application_index = self.main_window.currentApplicationIndex()
if selected_application_item := self.application_model.itemFromIndex(
application_index
):
if selected_application_item := self.application_model.itemFromIndex(application_index):
def insert_message_helper():
if isinstance(selected_application_item, ApplicationModelItem):
# A single application is selected
# -> Only insert the message if the appid matches the selected appid
if (
message.appid
== selected_application_item.data(
ApplicationItemDataRole.ApplicationRole
).id
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
):
self.messages_model.insert_message(0, message)
elif isinstance(selected_application_item, ApplicationAllMessagesItem):
# "All messages' is selected
self.insert_message(
0,
message,
application_item.data(
ApplicationItemDataRole.ApplicationRole
),
)
self.messages_model.insert_message(0, message)
self.process_message_task = ProcessMessageTask(message)
self.process_message_task.finished.connect(insert_message_helper)
self.process_message_task.start()
if process:
self.process_message_task = ProcessMessageTask(message)
self.process_message_task.finished.connect(insert_message_helper)
self.process_message_task.start()
else:
insert_message_helper()
else:
logger.error(
f"App id {message.appid} could not be found. Refreshing applications."
)
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)
def new_message_callback(self, message: gotify.GotifyMessageModel, process: bool = True):
self.add_message_to_model(message, process=process)
# Change the tray icon to show there are unread notifications
if (
@@ -301,23 +222,19 @@ class MainApplication(QtWidgets.QApplication):
):
self.tray.set_icon_unread()
# Show a notification
# Don't show a notification if it's low priority or the window is active
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):
if application_item := self.application_model.itemFromId(message.appid):
image_url = f"{self.gotify_client.url}/{application_item.data(ApplicationItemDataRole.ApplicationRole).image}"
icon = QtGui.QIcon(self.downloader.get_filename(image_url))
else:
logger.error(
f"MainWindow.new_message_callback: App id {message.appid} could not be found. Refreshing applications."
)
self.refresh_applications()
icon = QtWidgets.QSystemTrayIcon.MessageIcon.Information
# Get the application icon
if (
settings.value("tray/notifications/icon/show", type=bool)
and (application_item := self.application_model.itemFromId(message.appid))
):
icon = application_item.icon()
else:
icon = QtWidgets.QSystemTrayIcon.MessageIcon.Information
@@ -336,7 +253,7 @@ class MainApplication(QtWidgets.QApplication):
self.delete_message_task.start()
def delete_all_messages_callback(
self, item: Union[ApplicationModelItem, ApplicationAllMessagesItem]
self, item: ApplicationModelItem | ApplicationAllMessagesItem
):
if isinstance(item, ApplicationModelItem):
self.delete_application_messages_task = DeleteApplicationMessagesTask(
@@ -375,17 +292,13 @@ class MainApplication(QtWidgets.QApplication):
# Update the message widget icons
for r in range(self.messages_model.rowCount()):
message_widget: MessageWidget = self.main_window.listView_messages.indexWidget(
self.messages_model.index(r, 0)
)
message_widget: MessageWidget = self.main_window.listView_messages.indexWidget(self.messages_model.index(r, 0))
message_widget.set_icons()
def settings_callback(self):
settings_dialog = SettingsDialog(self)
settings_dialog.quit_requested.connect(self.quit)
settings_dialog.theme_change_requested.connect(
self.theme_change_requested_callback
)
settings_dialog.theme_change_requested.connect(self.theme_change_requested_callback)
accepted = settings_dialog.exec()
if accepted and settings_dialog.settings_changed:
@@ -428,9 +341,7 @@ class MainApplication(QtWidgets.QApplication):
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.application_selection_changed.connect(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)
@@ -438,7 +349,9 @@ class MainApplication(QtWidgets.QApplication):
self.styleHints().colorSchemeChanged.connect(lambda _: self.theme_change_requested_callback(settings.value("theme", type=str)))
self.watchdog.closed.connect(lambda: self.listener_closed_callback(None, None))
self.messages_model.rowsInserted.connect(self.main_window.display_message_widgets)
self.watchdog.closed.connect(lambda: self.listener_closed_callback(0, 0))
def init_shortcuts(self):
self.shortcut_quit = QtGui.QShortcut(
@@ -449,9 +362,7 @@ class MainApplication(QtWidgets.QApplication):
def acquire_lock(self) -> bool:
temp_dir = tempfile.gettempdir()
lock_filename = os.path.join(
temp_dir, __title__ + "-" + getpass.getuser() + ".lock"
)
lock_filename = os.path.join(temp_dir, __title__ + "-" + getpass.getuser() + ".lock")
self.lock_file = QtCore.QLockFile(lock_filename)
self.lock_file.setStaleLockTime(0)
return self.lock_file.tryLock()

View File

@@ -1,6 +1,5 @@
import enum
from typing import Optional, Union
from PyQt6 import QtCore, QtGui
from gotify_tray import gotify
from gotify_tray.database import Settings
@@ -18,7 +17,7 @@ class ApplicationModelItem(QtGui.QStandardItem):
def __init__(
self,
application: gotify.GotifyApplicationModel,
icon: Optional[QtGui.QIcon] = None,
icon: QtGui.QIcon | None = None,
*args,
**kwargs,
):
@@ -55,24 +54,22 @@ class ApplicationAllMessagesItem(QtGui.QStandardItem):
class ApplicationModel(QtGui.QStandardItemModel):
def __init__(self):
super(ApplicationModel, self).__init__()
self.setItemPrototype(
ApplicationModelItem(gotify.GotifyApplicationModel({"name": ""}), None)
)
self.setItemPrototype(ApplicationModelItem(gotify.GotifyApplicationModel({"name": ""}), None))
def setItem(
self,
row: int,
column: int,
item: Union[ApplicationModelItem, ApplicationAllMessagesItem],
item: ApplicationModelItem | ApplicationAllMessagesItem,
) -> None:
super(ApplicationModel, self).setItem(row, column, item)
def itemFromIndex(
self, index: QtCore.QModelIndex
) -> Union[ApplicationModelItem, ApplicationAllMessagesItem]:
) -> ApplicationModelItem | ApplicationAllMessagesItem:
return super(ApplicationModel, self).itemFromIndex(index)
def itemFromId(self, appid: int) -> Optional[ApplicationModelItem]:
def itemFromId(self, appid: int) -> ApplicationModelItem | None:
for row in range(self.rowCount()):
item = self.item(row, 0)
if not isinstance(item, ApplicationModelItem):

View File

@@ -3,6 +3,10 @@ import enum
from typing import cast
from PyQt6 import QtCore, QtGui
from gotify_tray import gotify
from gotify_tray.database import Settings
settings = Settings("gotify-tray")
class MessageItemDataRole(enum.IntEnum):
@@ -16,6 +20,20 @@ class MessagesModelItem(QtGui.QStandardItem):
class MessagesModel(QtGui.QStandardItemModel):
def update_last_id(self, i: int):
if i > settings.value("message/last", type=int):
settings.setValue("message/last", i)
def insert_message(self, row: int, message: gotify.GotifyMessageModel):
self.update_last_id(message.id)
message_item = MessagesModelItem(message)
self.insertRow(row, message_item)
def append_message(self, message: gotify.GotifyMessageModel):
self.update_last_id(message.id)
message_item = MessagesModelItem(message)
self.appendRow(message_item)
def setItem(self, row: int, column: int, item: MessagesModelItem) -> None:
super(MessagesModel, self).setItem(row, column, item)

View File

@@ -45,7 +45,7 @@ def set_theme(app: QtWidgets.QApplication, theme: str = "automatic"):
app.setStyleSheet(stylesheet)
def get_theme_file(app: QtWidgets.QApplication, file: str, theme: str = None) -> str:
def get_theme_file(app: QtWidgets.QApplication, file: str, theme: str | None = None) -> str:
theme = settings.value("theme", type=str) if not theme else theme
if not is_valid_theme(theme):

View File

@@ -6,19 +6,27 @@ QPushButton:default:hover, QPushButton:checked:hover {
background: #441b85;
}
QPushButton[state="success"] {
ServerInfoDialog QPushButton[state="success"] {
background-color: #960b7a0b;
color: white;
}
QPushButton[state="failed"] {
ServerInfoDialog QPushButton[state="success"]:!default:hover {
background: #960b7a0b;
}
ServerInfoDialog QPushButton[state="failed"] {
background-color: #8ebb2929;
color: white;
}
QLineEdit[state="success"] {}
ServerInfoDialog QPushButton[state="failed"]:!default:hover {
background: #8ebb2929;
}
QLineEdit[state="failed"] {
ServerInfoDialog QLineEdit[state="success"] {}
ServerInfoDialog QLineEdit[state="failed"] {
border: 1px solid red;
}

View File

@@ -6,19 +6,27 @@ QPushButton:default:hover, QPushButton:checked:hover {
background: #5c24b6;
}
QPushButton[state="success"] {
ServerInfoDialog QPushButton[state="success"] {
background-color: #6400FF00;
color: black;
}
QPushButton[state="failed"] {
ServerInfoDialog QPushButton[state="success"]:!default:hover {
background: #6400FF00;
}
ServerInfoDialog QPushButton[state="failed"] {
background-color: #64FF0000;
color: black;
}
QLineEdit[state="success"] {}
ServerInfoDialog QPushButton[state="failed"]:!default:hover {
background: #64FF0000;
}
QLineEdit[state="failed"] {
ServerInfoDialog QLineEdit[state="success"] {}
ServerInfoDialog QLineEdit[state="failed"] {
border: 1px solid red;
}

View File

@@ -8,7 +8,7 @@ settings = Settings("gotify-tray")
class ImagePopup(QtWidgets.QLabel):
def __init__(self, filename: str, pos: QtCore.QPoint, link: str = None):
def __init__(self, filename: str, pos: QtCore.QPoint, link: str | None = None):
"""Create and show a pop-up image under the cursor
Args:

View File

@@ -11,6 +11,8 @@ from . import MessageWidget
from gotify_tray.__version__ import __title__
from gotify_tray.database import Settings
from gotify_tray.gui.themes import get_theme_file
from gotify_tray.gui.models import MessageItemDataRole
from gotify_tray import gotify
settings = Settings("gotify-tray")
@@ -104,17 +106,20 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def set_error(self):
self.status_widget.set_error()
def insert_message_widget(
self, message_item: MessagesModelItem, image_path: str = ""
):
message_widget = MessageWidget(
self.app, self.listView_messages, 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)
message_widget.image_popup.connect(self.image_popup.emit)
def display_message_widgets(self, parent: QtCore.QModelIndex, first: int, last: int):
for i in range(first, last+1):
if index := self.messages_model.index(i, 0, parent):
message_item = self.messages_model.itemFromIndex(index)
message: gotify.GotifyMessageModel = self.messages_model.data(index, MessageItemDataRole.MessageRole)
application_item = self.application_model.itemFromId(message.appid)
message_widget = MessageWidget(self.app, self.listView_messages, message_item, icon=application_item.icon())
message_widget.deletion_requested.connect(self.delete_message.emit)
message_widget.image_popup.connect(self.image_popup.emit)
self.listView_messages.setIndexWidget(self.messages_model.indexFromItem(message_item), message_widget)
def currentApplicationIndex(self) -> QtCore.QModelIndex:
return self.listView_applications.selectionModel().currentIndex()

View File

@@ -23,11 +23,11 @@ class MessageWidget(QtWidgets.QWidget, Ui_Form):
app: QtWidgets.QApplication,
parent: QtWidgets.QWidget,
message_item: MessagesModelItem,
image_path: str = "",
icon: QtGui.QIcon | None = None,
):
super(MessageWidget, self).__init__()
super(MessageWidget, self).__init__(parent)
self.app = app
self.parent = parent
self._parent = parent
self.setupUi(self)
self.setAutoFillBackground(True)
self.message_item = message_item
@@ -43,10 +43,7 @@ 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 message.get("extras", {}).get("client::display", {}).get("contentType") == "text/markdown":
self.label_message.setTextFormat(QtCore.Qt.TextFormat.MarkdownText)
# If the message is only an image URL, then instead of showing the message,
@@ -59,15 +56,9 @@ class MessageWidget(QtWidgets.QWidget, Ui_Form):
self.label_message.setText(convert_links(message.message))
# Show the application icon
if image_path:
if icon:
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,
transformMode=QtCore.Qt.TransformationMode.SmoothTransformation,
)
pixmap = icon.pixmap(QtCore.QSize(image_size, image_size))
self.label_image.setPixmap(pixmap)
else:
self.label_image.hide()
@@ -77,7 +68,12 @@ class MessageWidget(QtWidgets.QWidget, Ui_Form):
self.gridLayout.setContentsMargins(4, 5, 4, 0)
self.adjustSize()
size_hint = self.message_item.sizeHint()
self.message_item.setSizeHint(QtCore.QSize(size_hint.width(), self.height()))
self.message_item.setSizeHint(
QtCore.QSize(
size_hint.width(),
max(settings.value("MessageWidget/height/min", type=int), self.height())
)
)
self.set_icons()
@@ -113,13 +109,10 @@ class MessageWidget(QtWidgets.QWidget, Ui_Form):
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()
)
W = settings.value("MessageWidget/content_image/W_percentage", type=float)
H = settings.value("MessageWidget/content_image/H_percentage", type=float)
W *= self._parent.width() - self.label_image.width()
H *= self._parent.height()
if pixmap.width() > W or pixmap.height() > H:
pixmap = pixmap.scaled(
@@ -150,7 +143,5 @@ class MessageWidget(QtWidgets.QWidget, Ui_Form):
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.pb_delete.clicked.connect(lambda: self.deletion_requested.emit(self.message_item))
self.label_message.linkHovered.connect(self.link_hovered_callback)

View File

@@ -19,9 +19,7 @@ class ServerInfoDialog(QtWidgets.QDialog, Ui_Dialog):
self.line_url.setPlaceholderText("https://gotify.example.com")
self.line_url.setText(url)
self.line_token.setText(token)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setDisabled(
True
)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setDisabled(True)
self.pb_import.setVisible(enable_import)
self.link_callbacks()
@@ -37,9 +35,7 @@ class ServerInfoDialog(QtWidgets.QDialog, Ui_Dialog):
return
self.pb_test.setDisabled(True)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setDisabled(
True
)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setDisabled(True)
self.task = VerifyServerInfoTask(url, client_token)
self.task.success.connect(self.server_info_success)
@@ -59,9 +55,7 @@ class ServerInfoDialog(QtWidgets.QDialog, Ui_Dialog):
self.update_widget_state(self.pb_test, "success")
self.update_widget_state(self.line_token, "success")
self.update_widget_state(self.line_url, "success")
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled(
True
)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled(True)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setFocus()
def incorrect_token_callback(self, version: GotifyVersionModel):
@@ -80,6 +74,10 @@ class ServerInfoDialog(QtWidgets.QDialog, Ui_Dialog):
self.update_widget_state(self.line_url, "failed")
self.line_url.setFocus()
def input_changed_callback(self):
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setDisabled(True)
self.update_widget_state(self.pb_test, "")
def import_success_callback(self):
self.line_url.setText(settings.value("Server/url", type=str))
self.line_token.setText(settings.value("Server/client_token"))
@@ -95,14 +93,6 @@ class ServerInfoDialog(QtWidgets.QDialog, Ui_Dialog):
def link_callbacks(self):
self.pb_test.clicked.connect(self.test_server_info)
self.line_url.textChanged.connect(
lambda: self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Ok
).setDisabled(True)
)
self.line_token.textChanged.connect(
lambda: self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Ok
).setDisabled(True)
)
self.line_url.textChanged.connect(self.input_changed_callback)
self.line_token.textChanged.connect(self.input_changed_callback)
self.pb_import.clicked.connect(self.import_callback)

View File

@@ -43,42 +43,28 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
self.link_callbacks()
def initUI(self):
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Apply
).setEnabled(False)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).setEnabled(False)
# Notifications
self.spin_priority.setValue(
settings.value("tray/notifications/priority", type=int)
)
self.spin_priority.setValue(settings.value("tray/notifications/priority", type=int))
self.spin_duration.setValue(
settings.value("tray/notifications/duration_ms", type=int)
)
self.spin_duration.setValue(settings.value("tray/notifications/duration_ms", type=int))
if platform.system() == "Windows":
# The notification duration setting is ignored by windows
self.label_notification_duration.hide()
self.spin_duration.hide()
self.label_notification_duration_ms.hide()
self.cb_notify.setChecked(
settings.value("message/check_missed/notify", type=bool)
)
self.cb_notify.setChecked(settings.value("message/check_missed/notify", type=bool))
self.cb_notification_click.setChecked(
settings.value("tray/notifications/click", type=bool)
)
self.cb_notification_click.setChecked(settings.value("tray/notifications/click", type=bool))
self.cb_tray_icon_unread.setChecked(
settings.value("tray/icon/unread", type=bool)
)
self.cb_tray_icon_unread.setChecked(settings.value("tray/icon/unread", type=bool))
# Interface
self.combo_theme.addItems(get_themes())
self.combo_theme.setCurrentText(settings.value("theme", type=str))
self.cb_priority_colors.setChecked(
settings.value("MessageWidget/priority_color", type=bool)
)
self.cb_priority_colors.setChecked(settings.value("MessageWidget/priority_color", type=bool))
# Logging
self.combo_logging.addItems(
@@ -96,9 +82,7 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
self.add_message_widget()
# Advanced
self.groupbox_image_popup.setChecked(
settings.value("ImagePopup/enabled", type=bool)
)
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))
self.label_cache.setText("0 MB")
@@ -118,7 +102,7 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
}
)
),
get_icon("gotify-small"),
QtGui.QIcon(get_icon("gotify-small")),
)
self.layout_fonts_message.addWidget(self.message_widget)
@@ -132,16 +116,12 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
def settings_changed_callback(self, *args, **kwargs):
self.settings_changed = True
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Apply
).setEnabled(True)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).setEnabled(True)
def change_font_callback(self, name: str):
label: QtWidgets.QLabel = getattr(self.message_widget, "label_" + name)
font, accepted = QtWidgets.QFontDialog.getFont(
label.font(), self, f"Select a {name} font"
)
font, accepted = QtWidgets.QFontDialog.getFont(label.font(), self, f"Select a {name} font")
if accepted:
self.settings_changed_callback()
@@ -205,9 +185,7 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
self.label_cache.setText("0 MB")
def link_callbacks(self):
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Apply
).clicked.connect(self.apply_settings)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).clicked.connect(self.apply_settings)
# Notifications
self.spin_priority.valueChanged.connect(self.settings_changed_callback)
@@ -225,22 +203,14 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
# Logging
self.combo_logging.currentTextChanged.connect(self.settings_changed_callback)
self.pb_open_log.clicked.connect(
lambda: open_file(logger.root.handlers[0].baseFilename)
)
self.pb_open_log.clicked.connect(lambda: open_file(logger.root.handlers[0].baseFilename))
# Fonts
self.pb_reset_fonts.clicked.connect(self.reset_fonts_callback)
self.pb_font_message_title.clicked.connect(
lambda: self.change_font_callback("title")
)
self.pb_font_message_date.clicked.connect(
lambda: self.change_font_callback("date")
)
self.pb_font_message_content.clicked.connect(
lambda: self.change_font_callback("message")
)
self.pb_font_message_title.clicked.connect(lambda: self.change_font_callback("title"))
self.pb_font_message_date.clicked.connect(lambda: self.change_font_callback("date"))
self.pb_font_message_content.clicked.connect(lambda: self.change_font_callback("message"))
# Advanced
self.pb_export.clicked.connect(self.export_callback)
@@ -257,9 +227,7 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
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()
)
settings.setValue("tray/notifications/click", self.cb_notification_click.isChecked())
settings.setValue("tray/icon/unread", self.cb_tray_icon_unread.isChecked())
# Interface
@@ -269,9 +237,7 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
settings.setValue("theme", selected_theme)
self.theme_change_requested.emit(selected_theme)
settings.setValue(
"MessageWidget/priority_color", self.cb_priority_colors.isChecked()
)
settings.setValue("MessageWidget/priority_color", self.cb_priority_colors.isChecked())
# Logging
selected_level = self.combo_logging.currentText()
@@ -283,17 +249,9 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
logger.setLevel(selected_level)
# Fonts
settings.setValue(
"MessageWidget/font/title",
self.message_widget.label_title.font().toString(),
)
settings.setValue(
"MessageWidget/font/date", self.message_widget.label_date.font().toString()
)
settings.setValue(
"MessageWidget/font/message",
self.message_widget.label_message.font().toString(),
)
settings.setValue("MessageWidget/font/title", self.message_widget.label_title.font().toString())
settings.setValue("MessageWidget/font/date", self.message_widget.label_date.font().toString())
settings.setValue("MessageWidget/font/message", self.message_widget.label_message.font().toString())
# Advanced
settings.setValue("ImagePopup/enabled", self.groupbox_image_popup.isChecked())

View File

@@ -8,10 +8,10 @@ from functools import reduce
from PyQt6 import QtCore
from PyQt6.QtCore import pyqtSignal
from gotify_tray.database import Cache, Downloader, Settings
from gotify_tray.database import Cache, Settings
from gotify_tray.gotify.api import GotifyClient
from gotify_tray.gotify.models import GotifyVersionModel
from gotify_tray.utils import get_image
from gotify_tray.utils import process_messages
from . import gotify
@@ -97,7 +97,7 @@ class GetApplicationsTask(BaseTask):
class GetApplicationMessagesTask(BaseTask):
success = pyqtSignal(gotify.GotifyPagedMessagesModel)
message = pyqtSignal(gotify.GotifyMessageModel)
error = pyqtSignal(gotify.GotifyErrorModel)
def __init__(self, appid: int, gotify_client: gotify.GotifyClient):
@@ -110,10 +110,16 @@ class GetApplicationMessagesTask(BaseTask):
if isinstance(result, gotify.GotifyErrorModel):
self.error.emit(result)
else:
self.success.emit(result)
for message in process_messages(result.messages):
self.message.emit(message)
# Prevent locking up the UI when there are a lot of messages ready at the same time
# -- side effect: switching application while the previous messages are still being inserted causes mixing of messages
time.sleep(0.001)
class GetMessagesTask(BaseTask):
message = pyqtSignal(gotify.GotifyMessageModel)
success = pyqtSignal(gotify.GotifyPagedMessagesModel)
error = pyqtSignal(gotify.GotifyErrorModel)
@@ -126,9 +132,22 @@ class GetMessagesTask(BaseTask):
if isinstance(result, gotify.GotifyErrorModel):
self.error.emit(result)
else:
for message in process_messages(result.messages):
self.message.emit(message)
time.sleep(0.001)
self.success.emit(result)
class ProcessMessageTask(BaseTask):
def __init__(self, message: gotify.GotifyMessageModel):
super(ProcessMessageTask, self).__init__()
self.message = message
def task(self):
for _ in process_messages([self.message]):
pass
class VerifyServerInfoTask(BaseTask):
success = pyqtSignal(GotifyVersionModel)
incorrect_token = pyqtSignal(GotifyVersionModel)
@@ -176,9 +195,7 @@ class ServerConnectionWatchdogTask(BaseTask):
time.sleep(settings.value("watchdog/interval/s", type=int))
if not self.gotify_client.is_listening():
self.closed.emit()
logger.debug(
"ServerConnectionWatchdogTask: gotify_client is not listening"
)
logger.debug("ServerConnectionWatchdogTask: gotify_client is not listening")
class ExportSettingsTask(BaseTask):
@@ -205,35 +222,6 @@ class ImportSettingsTask(BaseTask):
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(gotify.GotifyMessageModel)
def __init__(self, page: gotify.GotifyPagedMessagesModel):
super(ProcessMessagesTask, self).__init__()
self.page = page
def task(self):
downloader = Downloader()
for message in self.page.messages:
if image_url := get_image(message.message):
downloader.get_filename(image_url)
self.message_processed.emit(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)

View File

@@ -4,7 +4,10 @@ import re
import subprocess
from pathlib import Path
from typing import Optional
from typing import Iterator
from gotify_tray import gotify
from gotify_tray.database import Downloader
def verify_server(force_new: bool = False, enable_import: bool = True) -> bool:
@@ -28,6 +31,14 @@ def verify_server(force_new: bool = False, enable_import: bool = True) -> bool:
return True
def process_messages(messages: list[gotify.GotifyMessageModel]) -> Iterator[gotify.GotifyMessageModel]:
downloader = Downloader()
for message in messages:
if image_url := get_image(message.message):
downloader.get_filename(image_url)
yield message
def convert_links(text):
_link = re.compile(
r'(?:(https://|http://)|(www\.))(\S+\b/?)([!"#$%&\'()*+,\-./:;<=>?@[\\\]^_`{|}~]*)(\s|$)',
@@ -45,7 +56,7 @@ def convert_links(text):
return _link.sub(replace, text)
def get_image(s: str) -> Optional[str]:
def get_image(s: str) -> str | None:
"""If `s` contains only an image URL, this function returns that URL.
This function also extracts a URL in the `![](<url>)` markdown image format.
"""