Add persistent notifications for priority 10 messages
Some checks failed
build / build-pip (push) Failing after 1m13s
build / build-win64 (push) Has been cancelled
build / build-macos (push) Has been cancelled

- 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:
kdusek
2025-11-26 15:10:50 +01:00
parent 4e4fd9cdc9
commit 2108568f50
10 changed files with 673 additions and 125 deletions

View File

@@ -0,0 +1,145 @@
import logging
from PyQt6 import QtCore, QtGui, QtWidgets
logger = logging.getLogger("gotify-tray")
class PersistentNotification(QtWidgets.QWidget):
close_all_requested = QtCore.pyqtSignal()
def __init__(
self,
title: str,
message: str,
icon: QtGui.QIcon | QtWidgets.QSystemTrayIcon.MessageIcon,
y_offset: int = 0,
flash: bool = False,
parent=None,
):
super().__init__(parent)
self.y_offset = y_offset
self.flash = flash
self.flash_state = False
self.original_stylesheet = ""
self.setWindowFlags(
QtCore.Qt.WindowType.Window
| QtCore.Qt.WindowType.FramelessWindowHint
| QtCore.Qt.WindowType.WindowStaysOnTopHint
)
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_OpaquePaintEvent, True)
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_ShowWithoutActivating)
# Layout
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(10)
# Icon
self.icon_label = QtWidgets.QLabel()
if isinstance(icon, QtGui.QIcon):
self.icon_label.setPixmap(icon.pixmap(32, 32))
else:
# For MessageIcon
if icon == QtWidgets.QSystemTrayIcon.MessageIcon.Information:
standard_icon = QtWidgets.QStyle.StandardPixmap.SP_MessageBoxInformation
elif icon == QtWidgets.QSystemTrayIcon.MessageIcon.Warning:
standard_icon = QtWidgets.QStyle.StandardPixmap.SP_MessageBoxWarning
elif icon == QtWidgets.QSystemTrayIcon.MessageIcon.Critical:
standard_icon = QtWidgets.QStyle.StandardPixmap.SP_MessageBoxCritical
else:
standard_icon = QtWidgets.QStyle.StandardPixmap.SP_MessageBoxInformation
system_icon = self.style().standardIcon(standard_icon)
self.icon_label.setPixmap(system_icon.pixmap(32, 32))
layout.addWidget(self.icon_label)
# Text
text_layout = QtWidgets.QVBoxLayout()
self.title_label = QtWidgets.QLabel(title)
self.title_label.setStyleSheet("font-weight: bold;")
text_layout.addWidget(self.title_label)
self.message_label = QtWidgets.QLabel(message)
self.message_label.setWordWrap(True)
text_layout.addWidget(self.message_label)
layout.addLayout(text_layout, 1)
# Close button
self.close_button = QtWidgets.QPushButton("×")
self.close_button.setFixedSize(20, 20)
self.close_button.clicked.connect(self._on_close)
layout.addWidget(self.close_button)
# Style
self.setAutoFillBackground(True)
self.setBackgroundRole(QtGui.QPalette.ColorRole.Window)
palette = self.palette()
palette.setColor(QtGui.QPalette.ColorRole.Window, QtGui.QColor("white"))
palette.setColor(QtGui.QPalette.ColorRole.WindowText, QtGui.QColor("red"))
self.setPalette(palette)
self.setStyleSheet("""
PersistentNotification {
border-radius: 10px;
border: 1px solid rgba(100, 100, 100, 200);
}
QPushButton {
background-color: transparent;
color: white;
border: none;
font-size: 16px;
}
QPushButton:hover {
background-color: rgba(100, 100, 100, 100);
}
""")
if self.flash:
self.flash_timer = QtCore.QTimer(self)
self.flash_timer.timeout.connect(self._toggle_flash)
# Size and position
self.adjustSize()
self.setFixedWidth(300)
self._position()
# Timer for fade out if not clicked, but since persistent, maybe not needed
# But to avoid staying forever if forgotten, perhaps auto-close after long time
# But user wants until clicked, so no timer.
def _position(self):
screen = QtWidgets.QApplication.primaryScreen().availableGeometry()
# Stack from bottom right
self.move(
screen.width() - self.width() - 10,
screen.height() - self.height() - 50 - self.y_offset,
)
def _toggle_flash(self):
self.flash_state = not self.flash_state
palette = self.palette()
if self.flash_state:
palette.setColor(QtGui.QPalette.ColorRole.Window, QtGui.QColor("red"))
palette.setColor(QtGui.QPalette.ColorRole.WindowText, QtGui.QColor("white"))
else:
palette.setColor(QtGui.QPalette.ColorRole.Window, QtGui.QColor("white"))
palette.setColor(QtGui.QPalette.ColorRole.WindowText, QtGui.QColor("red"))
self.setPalette(palette)
self.update()
self.repaint()
def mousePressEvent(self, event):
self._on_close()
def _on_close(self):
if self.flash:
self.flash_timer.stop()
self.close_all_requested.emit()
self.close()
def showEvent(self, event):
super().showEvent(event)
self.raise_()
if self.flash:
self.flash_timer.start(1000)