Compare commits

..

16 Commits

Author SHA1 Message Date
kdusek
2b3d9eb07f Add search feature for Gotify alerts
Some checks failed
build / build-win64 (push) Waiting to run
build / build-macos (push) Waiting to run
build / build-pip (push) Failing after 12s
2025-12-06 03:10:41 +01:00
kdusek
d0941fd7ab Update gotify-tray.spec
Some checks failed
build / build-win64 (push) Waiting to run
build / build-macos (push) Waiting to run
build / build-pip (push) Failing after 13s
2025-12-06 03:06:03 +01:00
kdusek
efdc63e1ab Remove subject filtering, keep only priority buttons
Some checks failed
build / build-pip (push) Failing after 12s
build / build-win64 (push) Has been cancelled
build / build-macos (push) Has been cancelled
- Remove subject filter menu and related code
- Simplify filtering to priority groups only
- Keep Remove Filters button for priority reset
- Clean up unused code and UI elements
2025-12-01 18:03:54 +01:00
kdusek
4c3b6925e5 Add message filtering by priority and subject, with CRITICAL always visible
- Implement MessagesProxyModel for client-side filtering
- Add priority filter buttons (LOW 0-3, NORMAL 4-8, HIGH 9, CRITICAL 10 always shown)
- Add subject filter menu with checkable actions for message titles
- Add Remove Filters button to reset all filters
- Restore priority 10 persistent notification setting in options
- Fix settings dialog errors and update UI layouts
- Ensure CRITICAL priority messages cannot be filtered out but can toggle persistent pop-ups
2025-12-01 18:02:05 +01:00
kdusek
09f85c5902 Remove invalid pyqt6-qt6-multimedia dependency and add build.sh script for easy compilation
Some checks failed
build / build-pip (push) Failing after 26s
build / build-win64 (push) Has been cancelled
build / build-macos (push) Has been cancelled
2025-11-27 10:01:39 +01:00
kdusek
a797f4ccf1 Update documentation
Some checks failed
build / build-pip (push) Failing after 8s
build / build-win64 (push) Has been cancelled
build / build-macos (push) Has been cancelled
- Add repository details to AGENTS.md for customized repo
- Add customizations section to README.md describing new features:
  - Persistent notifications for priority 10 with flashing
  - Sound control for priority 10 only
  - Stacking multiple notifications
  - Tray icon click to close all
2025-11-26 15:17:43 +01:00
kdusek
2108568f50 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
2025-11-26 15:10:50 +01:00
seird
4e4fd9cdc9 Merge pull request #45 from hydrargyrum/develop
mainwindow: add standard shortcut for "refresh" button
2025-04-05 19:05:26 +02:00
Hg
a8a03321f2 mainwindow: add standard shortcut for "refresh" button 2025-04-05 09:07:48 +02:00
dries.k
490044d9a7 update widgets_settings design 2025-03-27 18:32:58 +01:00
seird
a3ae246580 Merge pull request #44 from hydrargyrum/patch-1
widget_settings.ui: allow minimum priority of 0
2025-03-27 18:27:10 +01:00
hydrargyrum
0730c160f6 widget_settings.ui: allow minimum priority of 0
it's a valid priority for gotify

closes #43
2025-03-27 10:45:21 +01:00
dries.k
ed815fb459 fix typo in setup.py 2024-10-29 17:15:07 +01:00
dries.k
05da3a8295 v0.5.3 2024-10-29 17:10:20 +01:00
dries.k
f40f154f30 remove linux builds 2024-10-29 16:58:20 +01:00
dries.k
4e34c5e614 update github actions 2024-10-29 16:52:18 +01:00
27 changed files with 877 additions and 226 deletions

View File

@@ -12,11 +12,11 @@ jobs:
build-win64: build-win64:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: '3.10.8' python-version: '3.13'
- name: Upgrade pip and enable wheel support - name: Upgrade pip and enable wheel support
run: python -m pip install --upgrade pip setuptools wheel run: python -m pip install --upgrade pip setuptools wheel
- name: Install Requirements - name: Install Requirements
@@ -30,49 +30,19 @@ jobs:
mv inno-output\gotify-tray-installer.exe gotify-tray-installer-win.exe mv inno-output\gotify-tray-installer.exe gotify-tray-installer-win.exe
shell: cmd shell: cmd
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: gotify-tray-installer-win.exe name: gotify-tray-installer-win.exe
path: gotify-tray-installer-win.exe path: gotify-tray-installer-win.exe
build-ubuntu:
strategy:
matrix:
tag: [jammy]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10.8'
- name: Upgrade pip and enable wheel support
run: python -m pip install --upgrade pip setuptools wheel
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
- name: Build
run: |
pip install -r requirements.txt
pip install pyinstaller
gem install fpm
chmod +x build-linux.sh
./build-linux.sh deb
mv "dist/gotify-tray_$(cat version.txt)_amd64.deb" "gotify-tray_$(cat version.txt)_amd64_${{ matrix.tag }}.deb"
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
path: "gotify-tray_*_amd64_${{ matrix.tag }}.deb"
build-macos: build-macos:
runs-on: macos-12 runs-on: macos-12
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: '3.10.8' python-version: '3.13'
- name: Upgrade pip and enable wheel support - name: Upgrade pip and enable wheel support
run: python -m pip install --upgrade pip setuptools wheel run: python -m pip install --upgrade pip setuptools wheel
- name: Build - name: Build
@@ -81,7 +51,7 @@ jobs:
brew install create-dmg brew install create-dmg
create-dmg --volname "Gotify Tray" --app-drop-link 0 0 --no-internet-enable "gotify-tray.dmg" "./dist/Gotify Tray.app" create-dmg --volname "Gotify Tray" --app-drop-link 0 0 --no-internet-enable "gotify-tray.dmg" "./dist/Gotify Tray.app"
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: gotify-tray.dmg name: gotify-tray.dmg
path: gotify-tray.dmg path: gotify-tray.dmg
@@ -89,11 +59,11 @@ jobs:
build-pip: build-pip:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: '3.10.8' python-version: '3.13'
- name: Upgrade pip and enable wheel support - name: Upgrade pip and enable wheel support
run: python -m pip install --upgrade pip setuptools wheel run: python -m pip install --upgrade pip setuptools wheel
- name: install requirements - name: install requirements
@@ -103,6 +73,6 @@ jobs:
- name: create pip package - name: create pip package
run: python -m build run: python -m build
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
path: dist/gotify_tray-*.whl path: dist/gotify_tray-*.whl

View File

@@ -10,11 +10,11 @@ jobs:
build-win64: build-win64:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: '3.10.8' python-version: '3.13'
- name: Upgrade pip and enable wheel support - name: Upgrade pip and enable wheel support
run: python -m pip install --upgrade pip setuptools wheel run: python -m pip install --upgrade pip setuptools wheel
- name: Install Requirements - name: Install Requirements
@@ -28,49 +28,19 @@ jobs:
mv inno-output\gotify-tray-installer.exe gotify-tray-installer-win.exe mv inno-output\gotify-tray-installer.exe gotify-tray-installer-win.exe
shell: cmd shell: cmd
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: gotify-tray-installer-win.exe name: gotify-tray-installer-win.exe
path: gotify-tray-installer-win.exe path: gotify-tray-installer-win.exe
build-debian:
strategy:
matrix:
tag: [bullseye, bookworm]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10.8'
- name: Upgrade pip and enable wheel support
run: python -m pip install --upgrade pip setuptools wheel
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
- name: Build
run: |
pip install -r requirements.txt
pip install pyinstaller
gem install fpm
chmod +x build-linux.sh
./build-linux.sh deb
mv "dist/gotify-tray_$(cat version.txt)_amd64.deb" "gotify-tray_$(cat version.txt)_amd64_${{ matrix.tag }}.deb"
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: gotify-tray_${{github.ref_name}}_amd64_${{ matrix.tag }}.deb
path: gotify-tray_${{github.ref_name}}_amd64_${{ matrix.tag }}.deb
build-macos: build-macos:
runs-on: macos-12 runs-on: macos-12
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: '3.10.8' python-version: '3.13'
- name: Upgrade pip and enable wheel support - name: Upgrade pip and enable wheel support
run: python -m pip install --upgrade pip setuptools wheel run: python -m pip install --upgrade pip setuptools wheel
- name: Build - name: Build
@@ -79,7 +49,7 @@ jobs:
brew install create-dmg brew install create-dmg
create-dmg --volname "Gotify Tray" --app-drop-link 0 0 --no-internet-enable "gotify-tray.dmg" "./dist/Gotify Tray.app" create-dmg --volname "Gotify Tray" --app-drop-link 0 0 --no-internet-enable "gotify-tray.dmg" "./dist/Gotify Tray.app"
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: gotify-tray.dmg name: gotify-tray.dmg
path: gotify-tray.dmg path: gotify-tray.dmg
@@ -92,11 +62,11 @@ jobs:
permissions: permissions:
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: '3.10.8' python-version: '3.13'
- name: Upgrade pip and enable wheel support - name: Upgrade pip and enable wheel support
run: python -m pip install --upgrade pip setuptools wheel run: python -m pip install --upgrade pip setuptools wheel
- name: install requirements - name: install requirements
@@ -108,17 +78,17 @@ jobs:
- name: upload to pypi - name: upload to pypi
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: gotify_tray-${{github.ref_name}}-py3-none-any.whl name: gotify_tray-${{github.ref_name}}-py3-none-any.whl
path: dist/gotify_tray-${{github.ref_name}}-py3-none-any.whl path: dist/gotify_tray-${{github.ref_name}}-py3-none-any.whl
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build-win64, build-debian, build-macos, pypi] needs: [build-win64, build-macos, pypi]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
- name: Release - name: Release
uses: marvinpinto/action-automatic-releases@latest uses: marvinpinto/action-automatic-releases@latest
with: with:
@@ -127,6 +97,4 @@ jobs:
files: | files: |
gotify-tray-installer-win.exe gotify-tray-installer-win.exe
gotify-tray.dmg gotify-tray.dmg
gotify-tray_${{github.ref_name}}_amd64_bullseye.deb
gotify-tray_${{github.ref_name}}_amd64_bookworm.deb
gotify_tray-${{github.ref_name}}-py3-none-any.whl gotify_tray-${{github.ref_name}}-py3-none-any.whl

22
AGENTS.md Normal file
View File

@@ -0,0 +1,22 @@
# Agent Guidelines
## Repository
This project uses a customized repository at `http://192.168.88.97:3000/kadu/gotify-tray-customized.git`. Clone from this repository only for the latest changes and customizations.
## Type Checking
Run `pyright .` in the project root to perform static type checking with Pyright. Address any critical errors before committing changes.
Note: Pyright may report many Qt-related type issues that are false positives due to PyQt6 stubs limitations. Focus on logical errors rather than Qt API mismatches.
To ignore PyQt6-related errors, create a `pyrightconfig.json` in the project root with:
```json
{
"reportOptionalMemberAccess": false,
"reportAttributeAccessIssue": false,
"reportIncompatibleMethodOverride": false,
"reportArgumentType": false,
"reportAssignmentType": false,
"reportReturnType": false
}
```
This suppresses common PyQt6 false positives while still catching real issues.

View File

@@ -30,6 +30,13 @@ A tray notification application for receiving messages from a [Gotify server](ht
- Go through a history of all previously received messages. - Go through a history of all previously received messages.
- Receive missed messages after losing network connection. - Receive missed messages after losing network connection.
## Customizations
- **Persistent Notifications for Priority 10**: Messages with priority 10 display as persistent pop-up windows that stay on screen until clicked, with a flashing background for attention.
- **Sound Notification Control**: Option to play notification sound only for priority 10 messages.
- **Multiple Persistent Notifications**: Multiple priority 10 messages stack vertically and can be closed all at once by clicking any one or the tray icon.
- **Enhanced UI Settings**: Added configurable options for persistent notifications and sound behavior in the settings dialog.
## Images ## Images

0
build-linux.sh Normal file → Executable file
View File

13
build.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
# Clean previous builds
rm -rf dist build
# Install requirements
pip install -r requirements.txt
pip install pyinstaller
# Build
pyinstaller gotify-tray.spec
echo "Build complete. Executable in dist/gotify-tray/"

View File

@@ -8,7 +8,7 @@ logo = "gotify_tray/gui/images/logo.ico" if platform.system() != "Darwin" else "
a = Analysis(['gotify_tray/__main__.py'], a = Analysis(['gotify_tray/__main__.py'],
pathex=[os.getcwd()], pathex=[os.getcwd()],
binaries=[], binaries=[('/lib/x86_64-linux-gnu/libpython3.10.so', '.'), ('/lib/x86_64-linux-gnu/libpython3.10.so.1', '.')],
datas=[('gotify_tray/gui/images', 'gotify_tray/gui/images'), ('gotify_tray/gui/themes', 'gotify_tray/gui/themes')], datas=[('gotify_tray/gui/images', 'gotify_tray/gui/images'), ('gotify_tray/gui/themes', 'gotify_tray/gui/themes')],
hiddenimports=[], hiddenimports=[],
hookspath=[], hookspath=[],

View File

@@ -3,4 +3,4 @@ __description__ = (
"A tray notification application for receiving messages from a Gotify server." "A tray notification application for receiving messages from a Gotify server."
) )
__url__ = "https://github.com/seird/gotify-tray" __url__ = "https://github.com/seird/gotify-tray"
__version__ = "0.5.2" __version__ = "0.5.3"

View File

@@ -16,6 +16,8 @@ DEFAULT_SETTINGS = {
"tray/notifications/duration_ms": 5000, "tray/notifications/duration_ms": 5000,
"tray/notifications/icon/show": True, "tray/notifications/icon/show": True,
"tray/notifications/click": True, "tray/notifications/click": True,
"tray/notifications/priority10_persistent": True,
"tray/notifications/sound_only_priority10": True,
"tray/icon/unread": False, "tray/icon/unread": False,
"watchdog/enabled": True, "watchdog/enabled": True,
"watchdog/interval/s": 60, "watchdog/interval/s": 60,

View File

@@ -8,6 +8,7 @@ import tempfile
from gotify_tray import gotify from gotify_tray import gotify
from gotify_tray.__version__ import __title__ from gotify_tray.__version__ import __title__
from gotify_tray.database import Downloader, Settings from gotify_tray.database import Downloader, Settings
from .widgets.PersistentNotification import PersistentNotification
from gotify_tray.tasks import ( from gotify_tray.tasks import (
ClearCacheTask, ClearCacheTask,
DeleteApplicationMessagesTask, DeleteApplicationMessagesTask,
@@ -22,6 +23,7 @@ from gotify_tray.tasks import (
from gotify_tray.gui.themes import set_theme from gotify_tray.gui.themes import set_theme
from gotify_tray.utils import get_icon, verify_server from gotify_tray.utils import get_icon, verify_server
from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtMultimedia import QSoundEffect
from ..__version__ import __title__ from ..__version__ import __title__
from .models import ( from .models import (
@@ -32,6 +34,7 @@ from .models import (
ApplicationProxyModel, ApplicationProxyModel,
MessagesModel, MessagesModel,
MessagesModelItem, MessagesModelItem,
MessagesProxyModel,
MessageItemDataRole, MessageItemDataRole,
) )
from .widgets import ImagePopup, MainWindow, MessageWidget, SettingsDialog, Tray from .widgets import ImagePopup, MainWindow, MessageWidget, SettingsDialog, Tray
@@ -47,7 +50,9 @@ def init_logger(logger: logging.Logger):
else: else:
logging.disable() 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): if not os.path.exists(logdir):
os.mkdir(logdir) os.mkdir(logdir)
logging.basicConfig( logging.basicConfig(
@@ -57,6 +62,28 @@ def init_logger(logger: logging.Logger):
class MainApplication(QtWidgets.QApplication): 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): def init_ui(self):
self.gotify_client = gotify.GotifyClient( self.gotify_client = gotify.GotifyClient(
settings.value("Server/url", type=str), settings.value("Server/url", type=str),
@@ -66,10 +93,17 @@ class MainApplication(QtWidgets.QApplication):
self.downloader = Downloader() self.downloader = Downloader()
self.messages_model = MessagesModel() self.messages_model = MessagesModel()
self.messages_proxy_model = MessagesProxyModel()
self.messages_proxy_model.setSourceModel(self.messages_model)
self.application_model = ApplicationModel() self.application_model = ApplicationModel()
self.application_proxy_model = ApplicationProxyModel(self.application_model) 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.messages_proxy_model,
)
self.main_window.show() # The initial .show() is necessary to get the correct sizes when adding MessageWigets 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) QtCore.QTimer.singleShot(0, self.main_window.hide)
@@ -77,12 +111,16 @@ class MainApplication(QtWidgets.QApplication):
self.tray = Tray() self.tray = Tray()
self.tray.show() self.tray.show()
self.tray.activated.connect(self._on_tray_activated)
self.first_connect = True self.first_connect = True
self.watchdog = ServerConnectionWatchdogTask(self.gotify_client) self.watchdog = ServerConnectionWatchdogTask(self.gotify_client)
self.link_callbacks() self.link_callbacks()
self.main_window.priority_filter_changed.connect(
self.on_priority_filter_changed
)
self.init_shortcuts() self.init_shortcuts()
self.gotify_client.listen() self.gotify_client.listen()
@@ -100,17 +138,30 @@ class MainApplication(QtWidgets.QApplication):
self.application_model.setItem(0, 0, ApplicationAllMessagesItem()) self.application_model.setItem(0, 0, ApplicationAllMessagesItem())
self.get_applications_task = GetApplicationsTask(self.gotify_client) self.get_applications_task = GetApplicationsTask(self.gotify_client)
self.get_applications_task.success.connect(self.get_applications_success_callback) self.get_applications_task.success.connect(
self.get_applications_task.started.connect(self.main_window.disable_applications) self.get_applications_success_callback
self.get_applications_task.finished.connect(self.main_window.enable_applications) )
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() self.get_applications_task.start()
def get_applications_success_callback( def get_applications_success_callback(
self, applications: list[gotify.GotifyApplicationModel], self,
applications: list[gotify.GotifyApplicationModel],
): ):
for i, application in enumerate(applications): for i, application in enumerate(applications):
icon = QtGui.QIcon(self.downloader.get_filename(f"{self.gotify_client.url}/{application.image}")) icon = QtGui.QIcon(
self.application_model.setItem(i + 1, 0, ApplicationModelItem(application, icon)) 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): def update_last_id(self, i: int):
if i > settings.value("message/last", type=int): if i > settings.value("message/last", type=int):
@@ -157,6 +208,9 @@ class MainApplication(QtWidgets.QApplication):
else: else:
self.gotify_client.stop() self.gotify_client.stop()
def on_priority_filter_changed(self, priorities: set[int]):
self.messages_proxy_model.set_allowed_priorities(priorities)
def abort_get_messages_task(self): def abort_get_messages_task(self):
""" """
Abort any tasks that will result in new messages getting appended to messages_model Abort any tasks that will result in new messages getting appended to messages_model
@@ -170,19 +224,28 @@ class MainApplication(QtWidgets.QApplication):
task.message.disconnect() task.message.disconnect()
except TypeError: except TypeError:
pass pass
for task in aborted_tasks: for task in aborted_tasks:
task.wait() 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.main_window.disable_buttons()
self.abort_get_messages_task() self.abort_get_messages_task()
self.messages_model.clear() self.messages_model.clear()
if isinstance(item, ApplicationModelItem): if isinstance(item, ApplicationModelItem):
self.get_application_messages_task = GetApplicationMessagesTask(item.data(ApplicationItemDataRole.ApplicationRole).id, self.gotify_client) self.get_application_messages_task = GetApplicationMessagesTask(
self.get_application_messages_task.message.connect(self.messages_model.append_message) item.data(ApplicationItemDataRole.ApplicationRole).id,
self.get_application_messages_task.finished.connect(self.main_window.enable_buttons) 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() self.get_application_messages_task.start()
elif isinstance(item, ApplicationAllMessagesItem): elif isinstance(item, ApplicationAllMessagesItem):
@@ -191,21 +254,29 @@ class MainApplication(QtWidgets.QApplication):
self.get_messages_task.finished.connect(self.main_window.enable_buttons) self.get_messages_task.finished.connect(self.main_window.enable_buttons)
self.get_messages_task.start() 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): if self.application_model.itemFromId(message.appid):
application_index = self.main_window.currentApplicationIndex() 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(): def insert_message_helper():
if isinstance(selected_application_item, ApplicationModelItem): if isinstance(selected_application_item, ApplicationModelItem):
# A single application is selected # A single application is selected
# -> Only insert the message if the appid matches the selected appid # -> Only insert the message if the appid matches the selected appid
if ( if (
message.appid message.appid
== selected_application_item.data(ApplicationItemDataRole.ApplicationRole).id == selected_application_item.data(
ApplicationItemDataRole.ApplicationRole
).id
): ):
self.messages_model.insert_message(0, message) self.messages_model.insert_message(0, message)
elif isinstance(selected_application_item, ApplicationAllMessagesItem): elif isinstance(
selected_application_item, ApplicationAllMessagesItem
):
# "All messages' is selected # "All messages' is selected
self.messages_model.insert_message(0, message) self.messages_model.insert_message(0, message)
@@ -216,10 +287,14 @@ class MainApplication(QtWidgets.QApplication):
else: else:
insert_message_helper() insert_message_helper()
else: 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() 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) self.add_message_to_model(message, process=process)
# Don't show a notification if it's low priority or the window is active # Don't show a notification if it's low priority or the window is active
@@ -237,20 +312,51 @@ class MainApplication(QtWidgets.QApplication):
self.tray.set_icon_unread() self.tray.set_icon_unread()
# Get the application icon # Get the application icon
if ( if settings.value("tray/notifications/icon/show", type=bool) and (
settings.value("tray/notifications/icon/show", type=bool) application_item := self.application_model.itemFromId(message.appid)
and (application_item := self.application_model.itemFromId(message.appid))
): ):
icon = application_item.icon() icon = application_item.icon()
else: else:
icon = QtWidgets.QSystemTrayIcon.MessageIcon.Information icon = QtWidgets.QSystemTrayIcon.MessageIcon.Information
self.tray.showMessage( # Show notification
message.title, if message.priority == 10 and settings.value(
message.message, "tray/notifications/priority10_persistent", type=bool
icon, ):
msecs=settings.value("tray/notifications/duration_ms", type=int), # 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): def delete_message_callback(self, message_item: MessagesModelItem):
self.delete_message_task = DeleteMessageTask( self.delete_message_task = DeleteMessageTask(
@@ -269,9 +375,9 @@ class MainApplication(QtWidgets.QApplication):
) )
self.delete_application_messages_task.start() self.delete_application_messages_task.start()
elif isinstance(item, ApplicationAllMessagesItem): elif isinstance(item, ApplicationAllMessagesItem):
self.clear_cache_task = ClearCacheTask() self.clear_cache_task = ClearCacheTask()
self.clear_cache_task.start() self.clear_cache_task.start()
self.delete_all_messages_task = DeleteAllMessagesTask(self.gotify_client) self.delete_all_messages_task = DeleteAllMessagesTask(self.gotify_client)
self.delete_all_messages_task.start() self.delete_all_messages_task.start()
else: else:
@@ -299,7 +405,11 @@ class MainApplication(QtWidgets.QApplication):
# Update the message widget icons # Update the message widget icons
for r in range(self.messages_model.rowCount()): 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() message_widget.set_icons()
def settings_callback(self): def settings_callback(self):
@@ -341,15 +451,21 @@ class MainApplication(QtWidgets.QApplication):
self.main_window.refresh.connect(self.refresh_applications) self.main_window.refresh.connect(self.refresh_applications)
self.main_window.delete_all.connect(self.delete_all_messages_callback) 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.delete_message.connect(self.delete_message_callback)
self.main_window.image_popup.connect(self.image_popup_callback) self.main_window.image_popup.connect(self.image_popup_callback)
self.main_window.hidden.connect(self.main_window_hidden_callback) self.main_window.hidden.connect(self.main_window_hidden_callback)
self.main_window.activated.connect(self.tray.revert_icon) 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.opened.connect(self.listener_opened_callback)
self.gotify_client.closed.connect(self.listener_closed_callback) self.gotify_client.closed.connect(self.listener_closed_callback)
@@ -366,7 +482,9 @@ class MainApplication(QtWidgets.QApplication):
def acquire_lock(self) -> bool: def acquire_lock(self) -> bool:
temp_dir = tempfile.gettempdir() 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 = QtCore.QLockFile(lock_filename)
self.lock_file.setStaleLockTime(0) self.lock_file.setStaleLockTime(0)
return self.lock_file.tryLock() return self.lock_file.tryLock()

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_main.ui' # Form implementation generated from reading ui file 'gotify_tray/gui/designs/widget_main.ui'
# #
# Created by: PyQt6 UI code generator 6.5.0 # Created by: PyQt6 UI code generator 6.9.1
# #
# WARNING: Any manual changes made to this file will be lost when pyuic6 is # 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. # run again. Do not edit this file unless you know what you are doing.
@@ -47,6 +47,40 @@ class Ui_MainWindow(object):
self.pb_delete_all.setObjectName("pb_delete_all") self.pb_delete_all.setObjectName("pb_delete_all")
self.horizontalLayout.addWidget(self.pb_delete_all) self.horizontalLayout.addWidget(self.pb_delete_all)
self.verticalLayout_2.addLayout(self.horizontalLayout) self.verticalLayout_2.addLayout(self.horizontalLayout)
self.filtersLayout = QtWidgets.QHBoxLayout()
self.filtersLayout.setObjectName("filtersLayout")
self.label_priority = QtWidgets.QLabel(parent=self.verticalLayoutWidget)
self.label_priority.setObjectName("label_priority")
self.filtersLayout.addWidget(self.label_priority)
self.pb_low = QtWidgets.QPushButton(parent=self.verticalLayoutWidget)
self.pb_low.setCheckable(True)
self.pb_low.setChecked(True)
self.pb_low.setObjectName("pb_low")
self.filtersLayout.addWidget(self.pb_low)
self.pb_normal = QtWidgets.QPushButton(parent=self.verticalLayoutWidget)
self.pb_normal.setCheckable(True)
self.pb_normal.setChecked(True)
self.pb_normal.setObjectName("pb_normal")
self.filtersLayout.addWidget(self.pb_normal)
self.pb_high = QtWidgets.QPushButton(parent=self.verticalLayoutWidget)
self.pb_high.setCheckable(True)
self.pb_high.setChecked(True)
self.pb_high.setObjectName("pb_high")
self.filtersLayout.addWidget(self.pb_high)
self.pb_critical = QtWidgets.QPushButton(parent=self.verticalLayoutWidget)
self.pb_critical.setCheckable(True)
self.pb_critical.setChecked(True)
self.pb_critical.setObjectName("pb_critical")
self.filtersLayout.addWidget(self.pb_critical)
self.le_search = QtWidgets.QLineEdit(parent=self.verticalLayoutWidget)
self.le_search.setObjectName("le_search")
self.filtersLayout.addWidget(self.le_search)
spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.filtersLayout.addItem(spacerItem2)
self.pb_remove_filters = QtWidgets.QPushButton(parent=self.verticalLayoutWidget)
self.pb_remove_filters.setObjectName("pb_remove_filters")
self.filtersLayout.addWidget(self.pb_remove_filters)
self.verticalLayout_2.addLayout(self.filtersLayout)
self.listView_messages = QtWidgets.QListView(parent=self.verticalLayoutWidget) self.listView_messages = QtWidgets.QListView(parent=self.verticalLayoutWidget)
self.listView_messages.setAutoScroll(True) self.listView_messages.setAutoScroll(True)
self.listView_messages.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) self.listView_messages.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
@@ -66,6 +100,13 @@ class Ui_MainWindow(object):
_translate = QtCore.QCoreApplication.translate _translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Form")) MainWindow.setWindowTitle(_translate("MainWindow", "Form"))
self.label_application.setText(_translate("MainWindow", "Application")) self.label_application.setText(_translate("MainWindow", "Application"))
self.label_priority.setText(_translate("MainWindow", "Priority:"))
self.pb_low.setText(_translate("MainWindow", "LOW"))
self.pb_normal.setText(_translate("MainWindow", "NORMAL"))
self.pb_high.setText(_translate("MainWindow", "HIGH"))
self.pb_critical.setText(_translate("MainWindow", "CRITICAL"))
self.le_search.setPlaceholderText(_translate("MainWindow", "Search messages..."))
self.pb_remove_filters.setText(_translate("MainWindow", "Remove Filters"))
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -88,10 +88,100 @@
</property> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</item> </item>
<item> <item>
<widget class="QListView" name="listView_messages"> <layout class="QHBoxLayout" name="filtersLayout">
<item>
<widget class="QLabel" name="label_priority">
<property name="text">
<string>Priority:</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pb_low">
<property name="text">
<string>LOW</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pb_normal">
<property name="text">
<string>NORMAL</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pb_high">
<property name="text">
<string>HIGH</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pb_critical">
<property name="text">
<string>CRITICAL</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="le_search">
<property name="placeholderText">
<string>Search messages...</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="pb_remove_filters">
<property name="text">
<string>Remove Filters</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListView" name="listView_messages">
<property name="autoScroll"> <property name="autoScroll">
<bool>true</bool> <bool>true</bool>
</property> </property>

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_message.ui' # Form implementation generated from reading ui file 'gotify_tray/gui/designs/widget_message.ui'
# #
# Created by: PyQt6 UI code generator 6.5.0 # Created by: PyQt6 UI code generator 6.9.1
# #
# WARNING: Any manual changes made to this file will be lost when pyuic6 is # 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. # run again. Do not edit this file unless you know what you are doing.

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_server.ui' # Form implementation generated from reading ui file 'gotify_tray/gui/designs/widget_server.ui'
# #
# Created by: PyQt6 UI code generator 6.5.0 # Created by: PyQt6 UI code generator 6.9.1
# #
# WARNING: Any manual changes made to this file will be lost when pyuic6 is # 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. # run again. Do not edit this file unless you know what you are doing.

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file 'gotify_tray/gui/designs/widget_settings.ui' # Form implementation generated from reading ui file 'gotify_tray/gui/designs/widget_settings.ui'
# #
# Created by: PyQt6 UI code generator 6.4.2 # Created by: PyQt6 UI code generator 6.9.1
# #
# WARNING: Any manual changes made to this file will be lost when pyuic6 is # 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. # run again. Do not edit this file unless you know what you are doing.
@@ -51,7 +51,7 @@ class Ui_Dialog(object):
self.cb_notify.setObjectName("cb_notify") self.cb_notify.setObjectName("cb_notify")
self.gridLayout_4.addWidget(self.cb_notify, 2, 0, 1, 3) self.gridLayout_4.addWidget(self.cb_notify, 2, 0, 1, 3)
self.spin_priority = QtWidgets.QSpinBox(parent=self.groupBox_notifications) self.spin_priority = QtWidgets.QSpinBox(parent=self.groupBox_notifications)
self.spin_priority.setMinimum(1) self.spin_priority.setMinimum(0)
self.spin_priority.setMaximum(10) self.spin_priority.setMaximum(10)
self.spin_priority.setProperty("value", 5) self.spin_priority.setProperty("value", 5)
self.spin_priority.setObjectName("spin_priority") self.spin_priority.setObjectName("spin_priority")
@@ -62,6 +62,9 @@ class Ui_Dialog(object):
self.cb_tray_icon_unread = QtWidgets.QCheckBox(parent=self.groupBox_notifications) self.cb_tray_icon_unread = QtWidgets.QCheckBox(parent=self.groupBox_notifications)
self.cb_tray_icon_unread.setObjectName("cb_tray_icon_unread") self.cb_tray_icon_unread.setObjectName("cb_tray_icon_unread")
self.gridLayout_4.addWidget(self.cb_tray_icon_unread, 4, 0, 1, 3) self.gridLayout_4.addWidget(self.cb_tray_icon_unread, 4, 0, 1, 3)
self.cb_priority10_persistent = QtWidgets.QCheckBox(parent=self.groupBox_notifications)
self.cb_priority10_persistent.setObjectName("cb_priority10_persistent")
self.gridLayout_4.addWidget(self.cb_priority10_persistent, 5, 0, 1, 3)
self.verticalLayout_4.addWidget(self.groupBox_notifications) self.verticalLayout_4.addWidget(self.groupBox_notifications)
self.groupBox_2 = QtWidgets.QGroupBox(parent=self.tab_general) self.groupBox_2 = QtWidgets.QGroupBox(parent=self.tab_general)
self.groupBox_2.setObjectName("groupBox_2") self.groupBox_2.setObjectName("groupBox_2")
@@ -290,6 +293,7 @@ class Ui_Dialog(object):
self.cb_notify.setText(_translate("Dialog", "Show a notification for missed messages after reconnecting")) self.cb_notify.setText(_translate("Dialog", "Show a notification for missed messages after reconnecting"))
self.label_notification_priority.setText(_translate("Dialog", "Minimum priority to show notifications:")) self.label_notification_priority.setText(_translate("Dialog", "Minimum priority to show notifications:"))
self.cb_tray_icon_unread.setText(_translate("Dialog", "Change the tray icon color when there are unread notifications")) self.cb_tray_icon_unread.setText(_translate("Dialog", "Change the tray icon color when there are unread notifications"))
self.cb_priority10_persistent.setText(_translate("Dialog", "Show persistent notifications for priority 10 messages"))
self.groupBox_2.setTitle(_translate("Dialog", "Interface")) self.groupBox_2.setTitle(_translate("Dialog", "Interface"))
self.cb_priority_colors.setToolTip(_translate("Dialog", "4..7 -> medium\n" self.cb_priority_colors.setToolTip(_translate("Dialog", "4..7 -> medium\n"
"8..10 -> high")) "8..10 -> high"))

View File

@@ -97,7 +97,7 @@
<item row="0" column="1"> <item row="0" column="1">
<widget class="QSpinBox" name="spin_priority"> <widget class="QSpinBox" name="spin_priority">
<property name="minimum"> <property name="minimum">
<number>1</number> <number>0</number>
</property> </property>
<property name="maximum"> <property name="maximum">
<number>10</number> <number>10</number>
@@ -114,13 +114,20 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0" colspan="3"> <item row="4" column="0" colspan="3">
<widget class="QCheckBox" name="cb_tray_icon_unread"> <widget class="QCheckBox" name="cb_tray_icon_unread">
<property name="text"> <property name="text">
<string>Change the tray icon color when there are unread notifications</string> <string>Change the tray icon color when there are unread notifications</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0" colspan="3">
<widget class="QCheckBox" name="cb_priority10_persistent">
<property name="text">
<string>Show persistent notifications for priority 10 messages</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

Binary file not shown.

View File

@@ -1,5 +1,4 @@
import enum import enum
from typing import cast from typing import cast
from PyQt6 import QtCore, QtGui from PyQt6 import QtCore, QtGui
from gotify_tray import gotify from gotify_tray import gotify
@@ -28,7 +27,7 @@ class MessagesModel(QtGui.QStandardItemModel):
self.update_last_id(message.id) self.update_last_id(message.id)
message_item = MessagesModelItem(message) message_item = MessagesModelItem(message)
self.insertRow(row, message_item) self.insertRow(row, message_item)
def append_message(self, message: gotify.GotifyMessageModel): def append_message(self, message: gotify.GotifyMessageModel):
self.update_last_id(message.id) self.update_last_id(message.id)
message_item = MessagesModelItem(message) message_item = MessagesModelItem(message)
@@ -39,3 +38,34 @@ class MessagesModel(QtGui.QStandardItemModel):
def itemFromIndex(self, index: QtCore.QModelIndex) -> MessagesModelItem: def itemFromIndex(self, index: QtCore.QModelIndex) -> MessagesModelItem:
return cast(MessagesModelItem, super(MessagesModel, self).itemFromIndex(index)) return cast(MessagesModelItem, super(MessagesModel, self).itemFromIndex(index))
class MessagesProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self.allowed_priorities = set(range(11)) # 0-10
self.text_filter = ""
def set_allowed_priorities(self, priorities: set[int]):
self.allowed_priorities = priorities
self.invalidateFilter()
def set_text_filter(self, text: str):
self.text_filter = text.lower()
self.invalidateFilter()
def filterAcceptsRow(
self, source_row: int, source_parent: QtCore.QModelIndex
) -> bool:
index = self.sourceModel().index(source_row, 0, source_parent)
item = self.sourceModel().itemFromIndex(index)
message = item.data(MessageItemDataRole.MessageRole)
priority = message.priority if message.priority is not None else 0
if self.allowed_priorities and priority not in self.allowed_priorities:
return False
if self.text_filter:
title = (message.title or "").lower()
msg = message.message.lower()
if self.text_filter not in title and self.text_filter not in msg:
return False
return True

View File

@@ -5,4 +5,9 @@ from .ApplicationModel import (
ApplicationProxyModel, ApplicationProxyModel,
ApplicationItemDataRole, ApplicationItemDataRole,
) )
from .MessagesModel import MessagesModelItem, MessagesModel, MessageItemDataRole from .MessagesModel import (
MessagesModelItem,
MessagesModel,
MessagesProxyModel,
MessageItemDataRole,
)

View File

@@ -5,6 +5,7 @@ from ..models import (
ApplicationModel, ApplicationModel,
MessagesModel, MessagesModel,
MessagesModelItem, MessagesModelItem,
MessagesProxyModel,
) )
from . import MessageWidget from . import MessageWidget
@@ -26,8 +27,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
image_popup = QtCore.pyqtSignal(str, QtCore.QPoint) image_popup = QtCore.pyqtSignal(str, QtCore.QPoint)
hidden = QtCore.pyqtSignal() hidden = QtCore.pyqtSignal()
activated = QtCore.pyqtSignal() activated = QtCore.pyqtSignal()
priority_filter_changed = QtCore.pyqtSignal(set)
def __init__(self, application_model: ApplicationModel, application_proxy_model: QtCore.QSortFilterProxyModel, messages_model: MessagesModel): def __init__(
self,
application_model: ApplicationModel,
application_proxy_model: QtCore.QSortFilterProxyModel,
messages_model: MessagesModel,
messages_proxy_model: MessagesProxyModel,
):
super(MainWindow, self).__init__() super(MainWindow, self).__init__()
self.setupUi(self) self.setupUi(self)
@@ -38,9 +46,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.application_model = application_model self.application_model = application_model
self.application_proxy_model = application_proxy_model self.application_proxy_model = application_proxy_model
self.messages_model = messages_model self.messages_model = messages_model
self.messages_proxy_model = messages_proxy_model
self.listView_applications.setModel(application_proxy_model) self.listView_applications.setModel(application_proxy_model)
self.listView_messages.setModel(messages_model) self.listView_messages.setModel(messages_proxy_model)
self.messages_proxy_model.rowsInserted.connect(self.display_message_widgets)
self.messages_proxy_model.layoutChanged.connect(self.redisplay_message_widgets)
# Do not expand the applications listview when resizing # Do not expand the applications listview when resizing
self.splitter.setStretchFactor(0, 0) self.splitter.setStretchFactor(0, 0)
@@ -69,6 +81,33 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.link_callbacks() self.link_callbacks()
# Setup filters
self.pb_low.toggled.connect(self.on_priority_button_toggled)
self.pb_normal.toggled.connect(self.on_priority_button_toggled)
self.pb_high.toggled.connect(self.on_priority_button_toggled)
# Critical is always shown, no toggle
# Set styles for priority buttons
button_style = "QPushButton { background-color: grey; border: 1px solid black; } QPushButton:checked { background-color: green; }"
self.pb_low.setStyleSheet(button_style)
self.pb_normal.setStyleSheet(button_style)
self.pb_high.setStyleSheet(button_style)
# Critical always green
self.pb_critical.setStyleSheet(
"QPushButton { background-color: green; border: 1px solid black; }"
)
self.pb_critical.setChecked(True)
self.pb_critical.setCheckable(False)
self.pb_remove_filters.clicked.connect(self.on_remove_filters_clicked)
self.le_search.returnPressed.connect(self.on_search_return_pressed)
# set refresh shortcut (usually ctrl-r)
# unfortunately this cannot be done with designer
self.pb_refresh.setShortcut(
QtGui.QKeySequence(QtGui.QKeySequence.StandardKey.Refresh)
)
def set_icons(self): def set_icons(self):
# Set button icons # Set button icons
self.pb_refresh.setIcon(QtGui.QIcon(get_theme_file("refresh.svg"))) self.pb_refresh.setIcon(QtGui.QIcon(get_theme_file("refresh.svg")))
@@ -102,20 +141,36 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def set_error(self): def set_error(self):
self.status_widget.set_error() self.status_widget.set_error()
def display_message_widgets(self, parent: QtCore.QModelIndex, first: int, last: int): def display_message_widgets(
for i in range(first, last+1): self, parent: QtCore.QModelIndex, first: int, last: int
if index := self.messages_model.index(i, 0, parent): ):
message_item = self.messages_model.itemFromIndex(index) for i in range(first, last + 1):
if proxy_index := self.messages_proxy_model.index(i, 0, parent):
message: gotify.GotifyMessageModel = self.messages_model.data(index, MessageItemDataRole.MessageRole) source_index = self.messages_proxy_model.mapToSource(proxy_index)
message_item = self.messages_model.itemFromIndex(source_index)
message: gotify.GotifyMessageModel = message_item.data(
MessageItemDataRole.MessageRole
)
application_item = self.application_model.itemFromId(message.appid) application_item = self.application_model.itemFromId(message.appid)
message_widget = MessageWidget(self.listView_messages, message_item, icon=application_item.icon()) message_widget = MessageWidget(
self.listView_messages, message_item, icon=application_item.icon()
)
message_widget.deletion_requested.connect(self.delete_message.emit) message_widget.deletion_requested.connect(self.delete_message.emit)
message_widget.image_popup.connect(self.image_popup.emit) message_widget.image_popup.connect(self.image_popup.emit)
self.listView_messages.setIndexWidget(index, message_widget) self.listView_messages.setIndexWidget(proxy_index, message_widget)
def redisplay_message_widgets(self):
# Clear existing widgets
for row in range(self.messages_proxy_model.rowCount()):
index = self.messages_proxy_model.index(row, 0)
self.listView_messages.setIndexWidget(index, None)
# Redisplay for current visible rows
self.display_message_widgets(
QtCore.QModelIndex(), 0, self.messages_proxy_model.rowCount() - 1
)
def currentApplicationIndex(self) -> QtCore.QModelIndex: def currentApplicationIndex(self) -> QtCore.QModelIndex:
return self.listView_applications.selectionModel().currentIndex() return self.listView_applications.selectionModel().currentIndex()
@@ -123,13 +178,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def application_selection_changed_callback( def application_selection_changed_callback(
self, current: QtCore.QModelIndex, previous: QtCore.QModelIndex self, current: QtCore.QModelIndex, previous: QtCore.QModelIndex
): ):
if item := self.application_model.itemFromIndex(self.application_proxy_model.mapToSource(current)): if item := self.application_model.itemFromIndex(
self.application_proxy_model.mapToSource(current)
):
self.label_application.setText(item.text()) self.label_application.setText(item.text())
self.application_selection_changed.emit(item) self.application_selection_changed.emit(item)
def delete_all_callback(self): def delete_all_callback(self):
if ( if (
self.messages_model.rowCount() == 0 self.messages_proxy_model.rowCount() == 0
or QtWidgets.QMessageBox.warning( or QtWidgets.QMessageBox.warning(
self, self,
"Are you sure?", "Are you sure?",
@@ -143,7 +200,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
return return
index = self.currentApplicationIndex() index = self.currentApplicationIndex()
if item := self.application_model.itemFromIndex(self.application_proxy_model.mapToSource(index)): if item := self.application_model.itemFromIndex(
self.application_proxy_model.mapToSource(index)
):
self.delete_all.emit(item) self.delete_all.emit(item)
def disable_applications(self): def disable_applications(self):
@@ -152,7 +211,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def enable_applications(self): def enable_applications(self):
self.listView_applications.setEnabled(True) self.listView_applications.setEnabled(True)
self.listView_applications.setCurrentIndex(self.application_proxy_model.index(0, 0)) self.listView_applications.setCurrentIndex(
self.application_proxy_model.index(0, 0)
)
def disable_buttons(self): def disable_buttons(self):
self.pb_delete_all.setDisabled(True) self.pb_delete_all.setDisabled(True)
@@ -177,7 +238,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.pb_refresh.clicked.connect(self.refresh.emit) self.pb_refresh.clicked.connect(self.refresh.emit)
self.pb_delete_all.clicked.connect(self.delete_all_callback) self.pb_delete_all.clicked.connect(self.delete_all_callback)
self.listView_applications.selectionModel().currentChanged.connect(self.application_selection_changed_callback) self.listView_applications.selectionModel().currentChanged.connect(
self.application_selection_changed_callback
)
def store_state(self): def store_state(self):
settings.setValue("MainWindow/geometry", self.saveGeometry()) settings.setValue("MainWindow/geometry", self.saveGeometry())
@@ -202,3 +265,27 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.activated.emit() self.activated.emit()
return super().eventFilter(object, event) return super().eventFilter(object, event)
def on_priority_button_toggled(self, checked):
priorities = {10} # Critical always included
if self.pb_low.isChecked():
priorities.update({0, 1, 2, 3})
if self.pb_normal.isChecked():
priorities.update({4, 5, 6, 7, 8})
if self.pb_high.isChecked():
priorities.add(9)
self.priority_filter_changed.emit(priorities)
def on_remove_filters_clicked(self):
# Reset priority buttons
self.pb_low.setChecked(True)
self.pb_normal.setChecked(True)
self.pb_high.setChecked(True)
# Critical is always on
# Clear search
self.le_search.clear()
self.messages_proxy_model.set_text_filter("")
def on_search_return_pressed(self):
text = self.le_search.text()
self.messages_proxy_model.set_text_filter(text)

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)

View File

@@ -31,7 +31,7 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
super(SettingsDialog, self).__init__() super(SettingsDialog, self).__init__()
self.setupUi(self) self.setupUi(self)
self.setWindowTitle("Settings") self.setWindowTitle("Settings")
self.settings_changed = False self.settings_changed = False
self.changes_applied = False self.changes_applied = False
self.server_changed = False self.server_changed = False
@@ -41,29 +41,51 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
self.link_callbacks() self.link_callbacks()
def initUI(self): def initUI(self):
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).setEnabled(False) self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Apply
).setEnabled(False)
# Notifications # 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": if platform.system() == "Windows":
# The notification duration setting is ignored by windows # The notification duration setting is ignored by windows
self.label_notification_duration.hide() self.label_notification_duration.hide()
self.spin_duration.hide() self.spin_duration.hide()
self.label_notification_duration_ms.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)
)
self.cb_priority10_persistent.setChecked(
settings.value("tray/notifications/priority10_persistent", type=bool)
)
# Interface # Interface
self.cb_priority_colors.setChecked(settings.value("MessageWidget/priority_color", type=bool)) self.cb_priority_colors.setChecked(
self.cb_image_urls.setChecked(settings.value("MessageWidget/image_urls", type=bool)) settings.value("MessageWidget/priority_color", type=bool)
)
self.cb_image_urls.setChecked(
settings.value("MessageWidget/image_urls", type=bool)
)
self.cb_locale.setChecked(settings.value("locale", type=bool)) self.cb_locale.setChecked(settings.value("locale", type=bool))
self.cb_sort_applications.setChecked(settings.value("ApplicationModel/sort", type=bool)) self.cb_sort_applications.setChecked(
settings.value("ApplicationModel/sort", type=bool)
)
# Logging # Logging
self.combo_logging.addItems( self.combo_logging.addItems(
@@ -81,18 +103,22 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
self.add_message_widget() self.add_message_widget()
# Advanced # 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_w.setValue(settings.value("ImagePopup/w", type=int))
self.spin_popup_h.setValue(settings.value("ImagePopup/h", type=int)) self.spin_popup_h.setValue(settings.value("ImagePopup/h", type=int))
self.label_cache.setText("0 MB") self.label_cache.setText("0 MB")
self.compute_cache_size() self.compute_cache_size()
self.groupbox_watchdog.setChecked(settings.value("watchdog/enabled", type=bool)) self.groupbox_watchdog.setChecked(settings.value("watchdog/enabled", type=bool))
self.spin_watchdog_interval.setValue(settings.value("watchdog/interval/s", type=int)) self.spin_watchdog_interval.setValue(
settings.value("watchdog/interval/s", type=int)
)
self.label_app_version.setText(__version__) self.label_app_version.setText(__version__)
self.label_qt_version.setText(QtCore.QT_VERSION_STR) self.label_qt_version.setText(QtCore.QT_VERSION_STR)
self.label_app_icon.setPixmap(QtGui.QIcon(get_image("logo.ico")).pixmap(22,22)) self.label_app_icon.setPixmap(QtGui.QIcon(get_image("logo.ico")).pixmap(22, 22))
self.label_qt_icon.setPixmap(QtGui.QIcon(get_image("qt.png")).pixmap(22,22)) self.label_qt_icon.setPixmap(QtGui.QIcon(get_image("qt.png")).pixmap(22, 22))
def add_message_widget(self): def add_message_widget(self):
self.message_widget = MessageWidget( self.message_widget = MessageWidget(
@@ -113,18 +139,18 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
def compute_cache_size(self): def compute_cache_size(self):
self.cache_size_task = CacheSizeTask() 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.size.connect(
lambda size: self.label_cache.setText(f"{round(size / 1e6, 1)} MB")
)
self.cache_size_task.start() self.cache_size_task.start()
def set_value(self, key: str, value: Any, widget: QtWidgets.QWidget): def set_value(self, key: str, value: Any, widget: QtWidgets.QWidget):
"""Set a Settings value, only if the widget's value_changed attribute has been set """Set a Settings value, only if the widget's value_changed attribute has been set"""
"""
if hasattr(widget, "value_changed"): if hasattr(widget, "value_changed"):
settings.setValue(key, value) settings.setValue(key, value)
def connect_signal(self, signal: QtCore.pyqtBoundSignal, widget: QtWidgets.QWidget): def connect_signal(self, signal: QtCore.pyqtBoundSignal, widget: QtWidgets.QWidget):
"""Connect to a signal and set the value_changed attribute for a widget on trigger """Connect to a signal and set the value_changed attribute for a widget on trigger"""
"""
signal.connect(lambda *args: self.setting_changed_callback(widget)) signal.connect(lambda *args: self.setting_changed_callback(widget))
def change_server_info_callback(self): def change_server_info_callback(self):
@@ -132,13 +158,17 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
def setting_changed_callback(self, widget: QtWidgets.QWidget): def setting_changed_callback(self, widget: QtWidgets.QWidget):
self.settings_changed = True self.settings_changed = True
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).setEnabled(True) self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Apply
).setEnabled(True)
setattr(widget, "value_changed", True) setattr(widget, "value_changed", True)
def change_font_callback(self, name: str): def change_font_callback(self, name: str):
label: QtWidgets.QLabel = getattr(self.message_widget, "label_" + name) 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: if accepted:
self.setting_changed_callback(label) self.setting_changed_callback(label)
@@ -146,7 +176,10 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
def export_callback(self): def export_callback(self):
fname = QtWidgets.QFileDialog.getSaveFileName( fname = QtWidgets.QFileDialog.getSaveFileName(
self, "Export Settings", settings.value("export/path", type=str), "*", self,
"Export Settings",
settings.value("export/path", type=str),
"*",
)[0] )[0]
if fname and os.path.exists(os.path.dirname(fname)): if fname and os.path.exists(os.path.dirname(fname)):
self.export_settings_task = ExportSettingsTask(fname) self.export_settings_task = ExportSettingsTask(fname)
@@ -162,7 +195,10 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
def import_callback(self): def import_callback(self):
fname = QtWidgets.QFileDialog.getOpenFileName( fname = QtWidgets.QFileDialog.getOpenFileName(
self, "Import Settings", settings.value("export/path", type=str), "*", self,
"Import Settings",
settings.value("export/path", type=str),
"*",
)[0] )[0]
if fname and os.path.exists(fname): if fname and os.path.exists(fname):
self.import_settings_task = ImportSettingsTask(fname) self.import_settings_task = ImportSettingsTask(fname)
@@ -202,60 +238,130 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
self.label_cache.setText("0 MB") self.label_cache.setText("0 MB")
def link_callbacks(self): 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 # Notifications
self.connect_signal(self.spin_priority.valueChanged, self.spin_priority) self.connect_signal(self.spin_priority.valueChanged, self.spin_priority)
self.connect_signal(self.spin_duration.valueChanged, self.spin_duration) self.connect_signal(self.spin_duration.valueChanged, self.spin_duration)
self.connect_signal(self.cb_notify.stateChanged, self.cb_notify) self.connect_signal(self.cb_notify.stateChanged, self.cb_notify)
self.connect_signal(self.cb_notification_click.stateChanged, self.cb_notification_click) self.connect_signal(
self.connect_signal(self.cb_tray_icon_unread.stateChanged, self.cb_tray_icon_unread) self.cb_notification_click.stateChanged, self.cb_notification_click
)
self.connect_signal(
self.cb_tray_icon_unread.stateChanged, self.cb_tray_icon_unread
)
self.connect_signal(
self.cb_priority10_persistent.stateChanged, self.cb_priority10_persistent
)
# Interface # Interface
self.connect_signal(self.cb_priority_colors.stateChanged, self.cb_priority_colors) self.connect_signal(
self.cb_priority_colors.stateChanged, self.cb_priority_colors
)
self.connect_signal(self.cb_image_urls.stateChanged, self.cb_image_urls) self.connect_signal(self.cb_image_urls.stateChanged, self.cb_image_urls)
self.connect_signal(self.cb_locale.stateChanged, self.cb_locale) self.connect_signal(self.cb_locale.stateChanged, self.cb_locale)
self.connect_signal(self.cb_sort_applications.stateChanged, self.cb_sort_applications) self.connect_signal(
self.cb_sort_applications.stateChanged, self.cb_sort_applications
)
# Server info # Server info
self.pb_change_server_info.clicked.connect(self.change_server_info_callback) self.pb_change_server_info.clicked.connect(self.change_server_info_callback)
# Logging # Logging
self.connect_signal(self.combo_logging.currentTextChanged, self.combo_logging) self.connect_signal(self.combo_logging.currentTextChanged, self.combo_logging)
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 # Fonts
self.pb_reset_fonts.clicked.connect(self.reset_fonts_callback) 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_title.clicked.connect(
self.pb_font_message_date.clicked.connect(lambda: self.change_font_callback("date")) lambda: self.change_font_callback("title")
self.pb_font_message_content.clicked.connect(lambda: self.change_font_callback("message")) )
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 # Advanced
self.pb_export.clicked.connect(self.export_callback) self.pb_export.clicked.connect(self.export_callback)
self.pb_import.clicked.connect(self.import_callback) self.pb_import.clicked.connect(self.import_callback)
self.pb_reset.clicked.connect(self.reset_callback) self.pb_reset.clicked.connect(self.reset_callback)
self.connect_signal(self.groupbox_image_popup.toggled, self.groupbox_image_popup) self.connect_signal(
self.groupbox_image_popup.toggled, self.groupbox_image_popup
)
self.connect_signal(self.spin_popup_w.valueChanged, self.spin_popup_w) self.connect_signal(self.spin_popup_w.valueChanged, self.spin_popup_w)
self.connect_signal(self.spin_popup_h.valueChanged, self.spin_popup_h) self.connect_signal(self.spin_popup_h.valueChanged, self.spin_popup_h)
self.pb_clear_cache.clicked.connect(self.clear_cache_callback) self.pb_clear_cache.clicked.connect(self.clear_cache_callback)
self.pb_open_cache_dir.clicked.connect(lambda: open_file(Cache().directory())) self.pb_open_cache_dir.clicked.connect(lambda: open_file(Cache().directory()))
self.connect_signal(self.groupbox_watchdog.toggled, self.groupbox_watchdog) self.connect_signal(self.groupbox_watchdog.toggled, self.groupbox_watchdog)
self.connect_signal(self.spin_watchdog_interval.valueChanged, self.spin_watchdog_interval) self.connect_signal(
self.spin_watchdog_interval.valueChanged, self.spin_watchdog_interval
)
def apply_settings(self): def apply_settings(self):
# Priority # Priority
self.set_value("tray/notifications/priority", self.spin_priority.value(), self.spin_priority) self.set_value(
self.set_value("tray/notifications/duration_ms", self.spin_duration.value(), self.spin_duration) "tray/notifications/priority",
self.set_value("message/check_missed/notify", self.cb_notify.isChecked(), self.cb_notify) self.spin_priority.value(),
self.set_value("tray/notifications/click", self.cb_notification_click.isChecked(), self.cb_notification_click) self.spin_priority,
self.set_value("tray/icon/unread", self.cb_tray_icon_unread.isChecked(), self.cb_tray_icon_unread) )
self.set_value(
"tray/notifications/duration_ms",
self.spin_duration.value(),
self.spin_duration,
)
self.set_value(
"message/check_missed/notify", self.cb_notify.isChecked(), self.cb_notify
)
self.set_value(
"tray/notifications/click",
self.cb_notification_click.isChecked(),
self.cb_notification_click,
)
self.set_value(
"tray/icon/unread",
self.cb_tray_icon_unread.isChecked(),
self.cb_tray_icon_unread,
)
self.set_value(
"tray/notifications/priority10_persistent",
self.cb_priority10_persistent.isChecked(),
self.cb_priority10_persistent,
)
# Interface # Interface
self.set_value("MessageWidget/priority_color", self.cb_priority_colors.isChecked(), self.cb_priority_colors) self.set_value(
self.set_value("MessageWidget/image_urls", self.cb_image_urls.isChecked(), self.cb_image_urls) "tray/notifications/priority10_persistent",
True, # Always persistent for priority 10
None,
)
self.set_value(
"tray/notifications/sound_only_priority10",
False, # Not sound only
None,
)
self.set_value(
"MessageWidget/priority_color",
self.cb_priority_colors.isChecked(),
self.cb_priority_colors,
)
self.set_value(
"MessageWidget/image_urls",
self.cb_image_urls.isChecked(),
self.cb_image_urls,
)
self.set_value("locale", self.cb_locale.isChecked(), self.cb_locale) self.set_value("locale", self.cb_locale.isChecked(), self.cb_locale)
self.set_value("ApplicationModel/sort", self.cb_sort_applications.isChecked(), self.cb_sort_applications) self.set_value(
"ApplicationModel/sort",
self.cb_sort_applications.isChecked(),
self.cb_sort_applications,
)
# Logging # Logging
selected_level = self.combo_logging.currentText() selected_level = self.combo_logging.currentText()
@@ -267,18 +373,44 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
logger.setLevel(selected_level) logger.setLevel(selected_level)
# Fonts # Fonts
self.set_value("MessageWidget/font/title", self.message_widget.label_title.font().toString(), self.message_widget.label_title) self.set_value(
self.set_value("MessageWidget/font/date", self.message_widget.label_date.font().toString(), self.message_widget.label_date) "MessageWidget/font/title",
self.set_value("MessageWidget/font/message", self.message_widget.label_message.font().toString(), self.message_widget.label_message) self.message_widget.label_title.font().toString(),
self.message_widget.label_title,
)
self.set_value(
"MessageWidget/font/date",
self.message_widget.label_date.font().toString(),
self.message_widget.label_date,
)
self.set_value(
"MessageWidget/font/message",
self.message_widget.label_message.font().toString(),
self.message_widget.label_message,
)
# Advanced # Advanced
self.set_value("ImagePopup/enabled", self.groupbox_image_popup.isChecked(), self.groupbox_image_popup) self.set_value(
"ImagePopup/enabled",
self.groupbox_image_popup.isChecked(),
self.groupbox_image_popup,
)
self.set_value("ImagePopup/w", self.spin_popup_w.value(), self.spin_popup_w) self.set_value("ImagePopup/w", self.spin_popup_w.value(), self.spin_popup_w)
self.set_value("ImagePopup/h", self.spin_popup_h.value(), self.spin_popup_h) self.set_value("ImagePopup/h", self.spin_popup_h.value(), self.spin_popup_h)
self.set_value("watchdog/enabled", self.groupbox_watchdog.isChecked(), self.groupbox_watchdog) self.set_value(
self.set_value("watchdog/interval/s", self.spin_watchdog_interval.value(), self.spin_watchdog_interval) "watchdog/enabled",
self.groupbox_watchdog.isChecked(),
self.groupbox_watchdog,
)
self.set_value(
"watchdog/interval/s",
self.spin_watchdog_interval.value(),
self.spin_watchdog_interval,
)
self.settings_changed = False self.settings_changed = False
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).setEnabled(False) self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Apply
).setEnabled(False)
self.changes_applied = True self.changes_applied = True

8
pyrightconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"reportOptionalMemberAccess": false,
"reportAttributeAccessIssue": false,
"reportIncompatibleMethodOverride": false,
"reportArgumentType": false,
"reportAssignmentType": false,
"reportReturnType": false
}

View File

@@ -1,2 +1,3 @@
requests==2.32.3 requests
pyqt6==6.7.1 pyqt6>=6.7.1
pyqt6-qt6

View File

@@ -15,7 +15,7 @@ with open("version.txt", "r") as f:
# What packages are required for this module to be executed? # What packages are required for this module to be executed?
REQUIRED = [ REQUIRED = [
'requests==2.32.3', 'pyqt6==6.7.1' 'requests', 'pyqt6>=6.7.1'
] ]
# What packages are optional? # What packages are optional?
@@ -86,6 +86,7 @@ setup(
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12' 'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13'
] ]
) )

View File

@@ -6,8 +6,8 @@ VSVersionInfo(
ffi=FixedFileInfo( ffi=FixedFileInfo(
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
# Set not needed items to zero 0. # Set not needed items to zero 0.
filevers=(0, 5, 2, 0), filevers=(0, 5, 3, 0),
prodvers=(0, 5, 2, 0), prodvers=(0, 5, 3, 0),
# Contains a bitmask that specifies the valid bits 'flags'r # Contains a bitmask that specifies the valid bits 'flags'r
mask=0x3F, mask=0x3F,
# Contains a bitmask that specifies the Boolean attributes of the file. # Contains a bitmask that specifies the Boolean attributes of the file.
@@ -34,12 +34,12 @@ VSVersionInfo(
StringStruct(u"Comments", u"Gotify Tray"), StringStruct(u"Comments", u"Gotify Tray"),
StringStruct(u"CompanyName", u""), StringStruct(u"CompanyName", u""),
StringStruct(u"FileDescription", u"Gotifiy Tray"), StringStruct(u"FileDescription", u"Gotifiy Tray"),
StringStruct(u"FileVersion", u"0.5.2"), StringStruct(u"FileVersion", u"0.5.3"),
StringStruct(u"InternalName", u"gotify-tray"), StringStruct(u"InternalName", u"gotify-tray"),
StringStruct(u"LegalCopyright", u""), StringStruct(u"LegalCopyright", u""),
StringStruct(u"OriginalFilename", u"gotify-tray.exe"), StringStruct(u"OriginalFilename", u"gotify-tray.exe"),
StringStruct(u"ProductName", u"Gotify Tray"), StringStruct(u"ProductName", u"Gotify Tray"),
StringStruct(u"ProductVersion", u"0.5.2"), StringStruct(u"ProductVersion", u"0.5.3"),
], ],
) )
] ]

View File

@@ -1 +1 @@
0.5.2 0.5.3