Compare commits
17 Commits
3ee3942c01
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b695d7b7f | ||
|
|
2b3d9eb07f | ||
|
|
d0941fd7ab | ||
|
|
efdc63e1ab | ||
|
|
4c3b6925e5 | ||
|
|
09f85c5902 | ||
|
|
a797f4ccf1 | ||
|
|
2108568f50 | ||
|
|
4e4fd9cdc9 | ||
|
|
a8a03321f2 | ||
|
|
490044d9a7 | ||
|
|
a3ae246580 | ||
|
|
0730c160f6 | ||
|
|
ed815fb459 | ||
|
|
05da3a8295 | ||
|
|
f40f154f30 | ||
|
|
4e34c5e614 |
54
.github/workflows/develop.yml
vendored
54
.github/workflows/develop.yml
vendored
@@ -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
|
||||||
|
|||||||
62
.github/workflows/release.yml
vendored
62
.github/workflows/release.yml
vendored
@@ -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
22
AGENTS.md
Normal 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.
|
||||||
10
BUILDING.md
10
BUILDING.md
@@ -7,6 +7,16 @@ $ pip install -r requirements.txt
|
|||||||
$ pip install pyinstaller
|
$ pip install pyinstaller
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Alternative: System packages (Debian/Ubuntu)**
|
||||||
|
|
||||||
|
If you prefer to use system packages instead of pip, install the required PyQt6 packages:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ apt install python3-pyqt6 python3-pyqt6.qtwebsockets python3-pyqt6.qtmultimedia
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: This may require specific Python versions and may not include the latest features.
|
||||||
|
|
||||||
Currently it's only possible to create installer packages from the pyinstaller output. For any target platform, first create the executable with pyinstaller:
|
Currently it's only possible to create installer packages from the pyinstaller output. For any target platform, first create the executable with pyinstaller:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
|||||||
@@ -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
0
build-linux.sh
Normal file → Executable file
13
build.sh
Executable file
13
build.sh
Executable 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/"
|
||||||
@@ -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.12.so', '.'), ('/lib/x86_64-linux-gnu/libpython3.12.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=[],
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
def main():
|
def main():
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if "--version" in sys.argv:
|
if "--version" in sys.argv:
|
||||||
from gotify_tray.__version__ import __version__
|
from gotify_tray.__version__ import __version__
|
||||||
|
|
||||||
print(__version__)
|
print(__version__)
|
||||||
else:
|
else:
|
||||||
|
# Check for display before importing GUI modules
|
||||||
|
if not os.environ.get("DISPLAY"):
|
||||||
|
print(
|
||||||
|
"Error: No display environment detected. This application requires a graphical desktop environment to run.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
from gotify_tray.gui import start_gui
|
from gotify_tray.gui import start_gui
|
||||||
|
|
||||||
start_gui()
|
start_gui()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -174,15 +228,24 @@ class MainApplication(QtWidgets.QApplication):
|
|||||||
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,10 +254,14 @@ 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):
|
||||||
@@ -202,10 +269,14 @@ class MainApplication(QtWidgets.QApplication):
|
|||||||
# -> 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(
|
||||||
@@ -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,22 @@ 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)
|
if hasattr(self.styleHints(), "colorSchemeChanged"):
|
||||||
|
self.styleHints().colorSchemeChanged.connect(
|
||||||
|
self.theme_change_requested_callback
|
||||||
|
)
|
||||||
|
|
||||||
self.messages_model.rowsInserted.connect(self.main_window.display_message_widgets)
|
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 +483,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()
|
||||||
|
|||||||
@@ -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.10.0
|
||||||
#
|
#
|
||||||
# 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__":
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.10.0
|
||||||
#
|
#
|
||||||
# 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.
|
||||||
|
|||||||
@@ -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.10.0
|
||||||
#
|
#
|
||||||
# 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.
|
||||||
|
|||||||
@@ -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.10.0
|
||||||
#
|
#
|
||||||
# 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"))
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
BIN
gotify_tray/gui/images/notification.wav
Normal file
BIN
gotify_tray/gui/images/notification.wav
Normal file
Binary file not shown.
@@ -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
|
||||||
@@ -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 or "").lower()
|
||||||
|
if self.text_filter not in title and self.text_filter not in msg:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|||||||
@@ -5,4 +5,9 @@ from .ApplicationModel import (
|
|||||||
ApplicationProxyModel,
|
ApplicationProxyModel,
|
||||||
ApplicationItemDataRole,
|
ApplicationItemDataRole,
|
||||||
)
|
)
|
||||||
from .MessagesModel import MessagesModelItem, MessagesModel, MessageItemDataRole
|
from .MessagesModel import (
|
||||||
|
MessagesModelItem,
|
||||||
|
MessagesModel,
|
||||||
|
MessagesProxyModel,
|
||||||
|
MessageItemDataRole,
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,14 +3,21 @@ from gotify_tray.utils import get_abs_path
|
|||||||
|
|
||||||
|
|
||||||
themes = {
|
themes = {
|
||||||
QtCore.Qt.ColorScheme.Dark: "dark",
|
2: "dark", # Dark
|
||||||
QtCore.Qt.ColorScheme.Light: "light",
|
1: "light", # Light
|
||||||
QtCore.Qt.ColorScheme.Unknown: "light",
|
0: "light", # Unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def set_theme(app: QtWidgets.QApplication):
|
def set_theme(app: QtWidgets.QApplication):
|
||||||
theme = themes.get(app.styleHints().colorScheme(), "light")
|
if hasattr(app.styleHints(), "colorScheme"):
|
||||||
|
color_scheme = app.styleHints().colorScheme()
|
||||||
|
theme = themes.get(
|
||||||
|
color_scheme.value if hasattr(color_scheme, "value") else color_scheme,
|
||||||
|
"light",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
theme = "light" # Default to light theme if colorScheme not available
|
||||||
|
|
||||||
stylesheet = ""
|
stylesheet = ""
|
||||||
with open(get_abs_path(f"gotify_tray/gui/themes/base.qss"), "r") as f:
|
with open(get_abs_path(f"gotify_tray/gui/themes/base.qss"), "r") as f:
|
||||||
@@ -23,5 +30,12 @@ def set_theme(app: QtWidgets.QApplication):
|
|||||||
|
|
||||||
def get_theme_file(file: str) -> str:
|
def get_theme_file(file: str) -> str:
|
||||||
app = QtCore.QCoreApplication.instance()
|
app = QtCore.QCoreApplication.instance()
|
||||||
theme = themes.get(app.styleHints().colorScheme(), "light")
|
if hasattr(app.styleHints(), "colorScheme"):
|
||||||
|
color_scheme = app.styleHints().colorScheme()
|
||||||
|
theme = themes.get(
|
||||||
|
color_scheme.value if hasattr(color_scheme, "value") else color_scheme,
|
||||||
|
"light",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
theme = "light" # Default to light theme if colorScheme not available
|
||||||
return get_abs_path(f"gotify_tray/gui/themes/{theme}/{file}")
|
return get_abs_path(f"gotify_tray/gui/themes/{theme}/{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)
|
||||||
|
|||||||
145
gotify_tray/gui/widgets/PersistentNotification.py
Normal file
145
gotify_tray/gui/widgets/PersistentNotification.py
Normal 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)
|
||||||
@@ -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
8
pyrightconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"reportOptionalMemberAccess": false,
|
||||||
|
"reportAttributeAccessIssue": false,
|
||||||
|
"reportIncompatibleMethodOverride": false,
|
||||||
|
"reportArgumentType": false,
|
||||||
|
"reportAssignmentType": false,
|
||||||
|
"reportReturnType": false
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
requests==2.32.3
|
requests
|
||||||
pyqt6==6.7.1
|
pyqt6>=6.7.1
|
||||||
|
pyqt6-qt6
|
||||||
|
|||||||
15
run.sh
Executable file
15
run.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check if virtual environment exists
|
||||||
|
if [ ! -d "venv" ]; then
|
||||||
|
echo "Creating virtual environment..."
|
||||||
|
python3 -m venv venv
|
||||||
|
echo "Installing dependencies..."
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
else
|
||||||
|
source venv/bin/activate
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
python -m gotify_tray
|
||||||
5
setup.py
5
setup.py
@@ -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'
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.5.2
|
0.5.3
|
||||||
Reference in New Issue
Block a user