Add persistent notifications for priority 10 messages
- Implement custom PersistentNotification widget with flashing background - Add settings for persistent priority 10 notifications and sound control - Modify notification logic to show persistent pop-ups for priority 10 - Allow closing all persistent notifications via tray icon click - Add AGENTS.md with type checking guidelines - Configure pyright to suppress PyQt6 false positives - Update UI in settings dialog for new options - Add notification sound file
This commit is contained in:
@@ -8,6 +8,7 @@ import tempfile
|
||||
from gotify_tray import gotify
|
||||
from gotify_tray.__version__ import __title__
|
||||
from gotify_tray.database import Downloader, Settings
|
||||
from .widgets.PersistentNotification import PersistentNotification
|
||||
from gotify_tray.tasks import (
|
||||
ClearCacheTask,
|
||||
DeleteApplicationMessagesTask,
|
||||
@@ -22,6 +23,7 @@ from gotify_tray.tasks import (
|
||||
from gotify_tray.gui.themes import set_theme
|
||||
from gotify_tray.utils import get_icon, verify_server
|
||||
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||
from PyQt6.QtMultimedia import QSoundEffect
|
||||
|
||||
from ..__version__ import __title__
|
||||
from .models import (
|
||||
@@ -47,7 +49,9 @@ 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(
|
||||
@@ -57,6 +61,28 @@ def init_logger(logger: logging.Logger):
|
||||
|
||||
|
||||
class MainApplication(QtWidgets.QApplication):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Initialize notification sound effect
|
||||
self.notification_sound = QSoundEffect()
|
||||
sound_path = os.path.join(
|
||||
os.path.dirname(__file__), "images", "notification.wav"
|
||||
)
|
||||
self.notification_sound.setSource(QtCore.QUrl.fromLocalFile(sound_path))
|
||||
self.notification_sound.setVolume(0.5) # Set volume (0.0 to 1.0)
|
||||
self.persistent_notifications = []
|
||||
self.next_y_offset = 0
|
||||
|
||||
def close_all_persistent_notifications(self):
|
||||
for notification in self.persistent_notifications:
|
||||
notification.close()
|
||||
self.persistent_notifications.clear()
|
||||
self.next_y_offset = 0
|
||||
|
||||
def _on_tray_activated(self, reason):
|
||||
if reason == QtWidgets.QSystemTrayIcon.ActivationReason.Trigger:
|
||||
self.close_all_persistent_notifications()
|
||||
|
||||
def init_ui(self):
|
||||
self.gotify_client = gotify.GotifyClient(
|
||||
settings.value("Server/url", type=str),
|
||||
@@ -69,7 +95,9 @@ class MainApplication(QtWidgets.QApplication):
|
||||
self.application_model = ApplicationModel()
|
||||
self.application_proxy_model = ApplicationProxyModel(self.application_model)
|
||||
|
||||
self.main_window = MainWindow(self.application_model, self.application_proxy_model, self.messages_model)
|
||||
self.main_window = MainWindow(
|
||||
self.application_model, self.application_proxy_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)
|
||||
|
||||
@@ -77,6 +105,7 @@ class MainApplication(QtWidgets.QApplication):
|
||||
|
||||
self.tray = Tray()
|
||||
self.tray.show()
|
||||
self.tray.activated.connect(self._on_tray_activated)
|
||||
|
||||
self.first_connect = True
|
||||
|
||||
@@ -100,17 +129,30 @@ 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):
|
||||
@@ -170,19 +212,28 @@ class MainApplication(QtWidgets.QApplication):
|
||||
task.message.disconnect()
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
|
||||
for task in aborted_tasks:
|
||||
task.wait()
|
||||
|
||||
def application_selection_changed_callback(self, item: ApplicationModelItem | ApplicationAllMessagesItem):
|
||||
def application_selection_changed_callback(
|
||||
self, item: ApplicationModelItem | ApplicationAllMessagesItem
|
||||
):
|
||||
self.main_window.disable_buttons()
|
||||
self.abort_get_messages_task()
|
||||
self.messages_model.clear()
|
||||
|
||||
if isinstance(item, ApplicationModelItem):
|
||||
self.get_application_messages_task = GetApplicationMessagesTask(item.data(ApplicationItemDataRole.ApplicationRole).id, self.gotify_client)
|
||||
self.get_application_messages_task.message.connect(self.messages_model.append_message)
|
||||
self.get_application_messages_task.finished.connect(self.main_window.enable_buttons)
|
||||
self.get_application_messages_task = GetApplicationMessagesTask(
|
||||
item.data(ApplicationItemDataRole.ApplicationRole).id,
|
||||
self.gotify_client,
|
||||
)
|
||||
self.get_application_messages_task.message.connect(
|
||||
self.messages_model.append_message
|
||||
)
|
||||
self.get_application_messages_task.finished.connect(
|
||||
self.main_window.enable_buttons
|
||||
)
|
||||
self.get_application_messages_task.start()
|
||||
|
||||
elif isinstance(item, ApplicationAllMessagesItem):
|
||||
@@ -191,21 +242,29 @@ class MainApplication(QtWidgets.QApplication):
|
||||
self.get_messages_task.finished.connect(self.main_window.enable_buttons)
|
||||
self.get_messages_task.start()
|
||||
|
||||
def add_message_to_model(self, message: gotify.GotifyMessageModel, process: bool = True):
|
||||
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(self.application_proxy_model.mapToSource(application_index)):
|
||||
if selected_application_item := self.application_model.itemFromIndex(
|
||||
self.application_proxy_model.mapToSource(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.messages_model.insert_message(0, message)
|
||||
elif isinstance(selected_application_item, ApplicationAllMessagesItem):
|
||||
elif isinstance(
|
||||
selected_application_item, ApplicationAllMessagesItem
|
||||
):
|
||||
# "All messages' is selected
|
||||
self.messages_model.insert_message(0, message)
|
||||
|
||||
@@ -216,10 +275,14 @@ class MainApplication(QtWidgets.QApplication):
|
||||
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, process: bool = True):
|
||||
def new_message_callback(
|
||||
self, message: gotify.GotifyMessageModel, process: bool = True
|
||||
):
|
||||
self.add_message_to_model(message, process=process)
|
||||
|
||||
# Don't show a notification if it's low priority or the window is active
|
||||
@@ -237,20 +300,51 @@ class MainApplication(QtWidgets.QApplication):
|
||||
self.tray.set_icon_unread()
|
||||
|
||||
# Get the application icon
|
||||
if (
|
||||
settings.value("tray/notifications/icon/show", type=bool)
|
||||
and (application_item := self.application_model.itemFromId(message.appid))
|
||||
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
|
||||
|
||||
self.tray.showMessage(
|
||||
message.title,
|
||||
message.message,
|
||||
icon,
|
||||
msecs=settings.value("tray/notifications/duration_ms", type=int),
|
||||
)
|
||||
# Show notification
|
||||
if message.priority == 10 and settings.value(
|
||||
"tray/notifications/priority10_persistent", type=bool
|
||||
):
|
||||
# Create persistent notification
|
||||
notification = PersistentNotification(
|
||||
message.title or "",
|
||||
message.message or "",
|
||||
icon,
|
||||
y_offset=self.next_y_offset,
|
||||
flash=True,
|
||||
)
|
||||
notification.close_all_requested.connect(
|
||||
self.close_all_persistent_notifications
|
||||
)
|
||||
self.persistent_notifications.append(notification)
|
||||
notification.show()
|
||||
self.next_y_offset += notification.height() + 10
|
||||
else:
|
||||
# Use system tray notification
|
||||
msecs = settings.value("tray/notifications/duration_ms", type=int)
|
||||
self.tray.showMessage(
|
||||
message.title,
|
||||
message.message,
|
||||
icon,
|
||||
msecs=msecs,
|
||||
)
|
||||
|
||||
# Play notification sound
|
||||
if (
|
||||
not settings.value("tray/notifications/sound_only_priority10", type=bool)
|
||||
or message.priority == 10
|
||||
):
|
||||
if self.notification_sound.isLoaded():
|
||||
self.notification_sound.play()
|
||||
else:
|
||||
# Try to play anyway (QSoundEffect will queue if not loaded yet)
|
||||
self.notification_sound.play()
|
||||
|
||||
def delete_message_callback(self, message_item: MessagesModelItem):
|
||||
self.delete_message_task = DeleteMessageTask(
|
||||
@@ -269,9 +363,9 @@ class MainApplication(QtWidgets.QApplication):
|
||||
)
|
||||
self.delete_application_messages_task.start()
|
||||
elif isinstance(item, ApplicationAllMessagesItem):
|
||||
self.clear_cache_task = ClearCacheTask()
|
||||
self.clear_cache_task = ClearCacheTask()
|
||||
self.clear_cache_task.start()
|
||||
|
||||
|
||||
self.delete_all_messages_task = DeleteAllMessagesTask(self.gotify_client)
|
||||
self.delete_all_messages_task.start()
|
||||
else:
|
||||
@@ -299,7 +393,11 @@ 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):
|
||||
@@ -341,15 +439,21 @@ 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)
|
||||
self.main_window.activated.connect(self.tray.revert_icon)
|
||||
|
||||
self.styleHints().colorSchemeChanged.connect(self.theme_change_requested_callback)
|
||||
|
||||
self.messages_model.rowsInserted.connect(self.main_window.display_message_widgets)
|
||||
self.styleHints().colorSchemeChanged.connect(
|
||||
self.theme_change_requested_callback
|
||||
)
|
||||
|
||||
self.messages_model.rowsInserted.connect(
|
||||
self.main_window.display_message_widgets
|
||||
)
|
||||
|
||||
self.gotify_client.opened.connect(self.listener_opened_callback)
|
||||
self.gotify_client.closed.connect(self.listener_closed_callback)
|
||||
@@ -366,7 +470,9 @@ 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()
|
||||
|
||||
Reference in New Issue
Block a user