initial commit

This commit is contained in:
dries.k
2021-08-05 18:56:17 +02:00
commit 1520e4d9e7
56 changed files with 3820 additions and 0 deletions

0
gotify_tray/__init__.py Normal file
View File

View File

@@ -0,0 +1,4 @@
__title__ = "Gotify Tray"
__description__ = "A tray notification application for receiving messages from a Gotify server."
__url__ = "https://github.com/seird/gotify-tray"
__version__ = "0.0.11"

View File

@@ -0,0 +1,4 @@
from .cache import Cache
from .database import Database
from .downloader import Downloader
from .settings import Settings

View File

@@ -0,0 +1,74 @@
import glob
import logging
import os
import time
import requests
from PyQt6 import QtCore
from .database import Database
logger = logging.getLogger("logger")
class Cache(object):
def __init__(self):
self.database = Database("cache")
self.cursor = self.database.cursor()
self.cursor.execute(
"""CREATE TABLE IF NOT EXISTS cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT,
filename TEXT,
cached_on TEXT)
"""
)
# create a directory to store cached files
path = QtCore.QStandardPaths.standardLocations(
QtCore.QStandardPaths.StandardLocation.CacheLocation
)[0]
self.cache_dir = os.path.join(path, "cache")
os.makedirs(self.cache_dir, exist_ok=True)
def clear(self):
self.cursor.execute("DELETE FROM cache")
self.database.commit()
for filename in glob.glob(self.cache_dir + "/*"):
os.remove(filename)
def lookup(self, key: str) -> str:
q = self.cursor.execute(
"SELECT filename, cached_on FROM cache WHERE url=?", (key,)
).fetchone()
if q:
# Cache hit
filename, cached_on = q
return filename
else:
# Cache miss
return ""
def store(
self, key: str, response: requests.Response, add_time: bool = True
) -> str:
# Create a file and store the response contents
filename = str(time.time()).replace(".", "") if add_time else ""
if "Content-Disposition" in response.headers.keys():
filename += response.headers["Content-Disposition"]
else:
filename += response.url.split("/")[-1]
filename = "".join([c for c in filename if c.isalpha() or c.isdigit()]).rstrip()
filename = os.path.join(self.cache_dir, filename)
with open(filename, "wb") as f:
f.write(response.content)
self.cursor.execute(
"INSERT INTO cache (url, filename, cached_on) VALUES(?, ?, datetime('now', 'localtime'))",
(key, filename),
)
self.database.commit()
return filename

View File

@@ -0,0 +1,19 @@
import logging
import os
import sqlite3
from PyQt6 import QtCore
logger = logging.getLogger("logger")
class Database(sqlite3.Connection):
def __init__(self, database: str, *args, **kwargs):
self.dir = QtCore.QStandardPaths.standardLocations(
QtCore.QStandardPaths.StandardLocation.CacheLocation
)[0]
os.makedirs(self.dir, exist_ok=True)
path = os.path.join(self.dir, database + ".db.sqlite3")
super(Database, self).__init__(database=path, *args, **kwargs)
self.row_factory = sqlite3.Row

View File

@@ -0,0 +1,18 @@
DEFAULT_SETTINGS = {
"MainWindow/start_minimized": True,
"MainWindow/theme": "default",
"MainWidget/status_image/size": 28,
"MessageWidget/image/show": True,
"MessageWidget/image/size": 33,
"MessageWidget/font/title": "Noto Sans,17,-1,5,75,0,0,0,0,0,Bold",
"MessageWidget/font/date": "Noto Sans,11,-1,5,50,1,0,0,0,0,Italic",
"MessageWidget/font/content": "Noto Sans,11,-1,5,50,0,0,0,0,0,Regular",
"ApplicationModelItem/icon/show": True,
"ApplicationModelItem/icon/size": 33,
"tray/notifications/enabled": True,
"tray/notifications/priority": 5,
"tray/show": True,
"tray/minimize": True,
"tray/notifications/duration_ms": 5000,
"tray/notifications/icon/show": True,
}

View File

@@ -0,0 +1,78 @@
import logging
import requests
from .cache import Cache
from .settings import Settings
logger = logging.getLogger("logger")
settings = Settings("gotify-tray")
class Downloader(object):
def __init__(self):
self.cache = Cache()
self.session = requests.Session()
self.session.proxies.update(
{
"https": settings.value("proxies/https", type=str),
"http": settings.value("proxies/http", type=str),
}
)
def get(self, url: str) -> requests.Response:
"""
Get the response of an http get request.
Bypasses the cache.
"""
return self.session.get(url)
def get_bytes(self, url: str, cached: bool = True, add_time: bool = True) -> bytes:
"""
Get the content of an http get request, as bytes.
Optionally use the cache.
"""
if cached:
# Retrieve from cache
filename = self.cache.lookup(url)
if filename:
with open(filename, "rb") as f:
return f.read()
try:
response = self.get(url)
except Exception as e:
logger.error(f"get_bytes: downloading {url} failed.: {e}")
return b""
if not response.ok:
return b""
if cached:
# Store in cache
self.cache.store(url, response, add_time=add_time)
return response.content
def get_filename(
self, url: str, retrieve_from_cache: bool = True, add_time: bool = True
) -> str:
"""
Get the content of an http get request, as a filename.
"""
if retrieve_from_cache:
filename = self.cache.lookup(url)
if filename:
return filename
try:
response = self.get(url)
except Exception as e:
logger.error(f"get_filename: downloading {url} failed.: {e}")
return ""
if not response.ok:
return ""
return self.cache.store(url, response, add_time=add_time)

View File

@@ -0,0 +1,13 @@
from typing import Any
from .default_settings import DEFAULT_SETTINGS
from PyQt6 import QtCore
class Settings(QtCore.QSettings):
def value(self, key: str, defaultValue: Any = None, type: Any = None) -> Any:
if type:
return super().value(key, defaultValue=defaultValue or DEFAULT_SETTINGS.get(key), type=type)
else:
return super().value(key, defaultValue=defaultValue or DEFAULT_SETTINGS.get(key))

View File

@@ -0,0 +1,10 @@
from .api import GotifyApplication, GotifyClient
from .models import (
GotifyApplicationModel,
GotifyErrorModel,
GotifyHealthModel,
GotifyMessageModel,
GotifyPagedMessagesModel,
GotifyPagingModel,
GotifyVersionModel,
)

223
gotify_tray/gotify/api.py Normal file
View File

@@ -0,0 +1,223 @@
import logging
from typing import Callable, List, Optional, Union
import requests
from .listener import Listener
from .models import (
GotifyApplicationModel,
GotifyErrorModel,
GotifyHealthModel,
GotifyMessageModel,
GotifyPagedMessagesModel,
GotifyPagingModel,
GotifyVersionModel,
)
logger = logging.getLogger("logger")
class GotifySession(object):
def __init__(self, url: str, token: str):
self.url = url.rstrip("/")
self.session = requests.Session()
self.session.headers.update({"X-Gotify-Key": token})
def _get(self, endpoint: str = "/", **kwargs) -> requests.Response:
return self.session.get(self.url + endpoint, **kwargs)
def _post(self, endpoint: str = "/", **kwargs) -> requests.Response:
return self.session.post(self.url + endpoint, **kwargs)
def _put(self, endpoint: str = "/", **kwargs) -> requests.Response:
return self.session.put(self.url + endpoint, **kwargs)
def _delete(self, endpoint: str = "/", **kwargs) -> requests.Response:
return self.session.delete(self.url + endpoint, **kwargs)
# For sending messages
class GotifyApplication(GotifySession):
def __init__(self, url: str, application_token: str):
super(GotifyApplication, self).__init__(url, application_token)
def push(
self, title: str = "", message: str = "", priority: int = 0, extras: dict = None
) -> Union[GotifyMessageModel, GotifyErrorModel]:
response = self._post(
"/message",
json={
"title": title,
"message": message,
"priority": priority,
"extras": extras,
},
)
return (
GotifyMessageModel(response.json())
if response.ok
else GotifyErrorModel(response)
)
# For everything else
class GotifyClient(GotifySession):
def __init__(self, url: str, client_token: str):
super(GotifyClient, self).__init__(url, client_token)
self.hostname = self.url.lstrip("https://").lstrip("http://")
self.listener = Listener(self.hostname, client_token)
"""
Application
"""
def get_applications(self) -> Union[List[GotifyApplicationModel], GotifyErrorModel]:
response = self._get("/application")
return (
[GotifyApplicationModel(x) for x in response.json()]
if response.ok
else GotifyErrorModel(response)
)
def create_application(
self, name: str, description: str = ""
) -> Union[GotifyApplicationModel, GotifyErrorModel]:
response = self._post(
"/application", json={"name": name, "description": description}
)
return (
GotifyApplicationModel(response.json())
if response.ok
else GotifyErrorModel(response)
)
def update_application(
self, application_id: int, name: str, description: str = ""
) -> Union[GotifyApplicationModel, GotifyErrorModel]:
response = self._put(
f"/application/{application_id}",
json={"name": name, "description": description},
)
return (
GotifyApplicationModel(response.json())
if response.ok
else GotifyErrorModel(response)
)
def delete_application(self, application_id: int) -> bool:
return self._delete(f"/application/{application_id}").ok
def upload_application_image(
self, application_id: int, img_path: str
) -> Optional[GotifyApplicationModel]:
try:
with open(img_path, "rb") as f:
response = self._post(
f"/application/{application_id}/image", files={"file": f}
)
return response.json() if response.ok else None
except FileNotFoundError:
logger.error(
f"GotifyClient.upload_application_image: image '{img_path}' not found."
)
return None
"""
Message
"""
def get_application_messages(
self, application_id: int, limit: int = 100, since: int = None
) -> Union[GotifyPagedMessagesModel, GotifyErrorModel]:
response = self._get(
f"/application/{application_id}/message",
params={"limit": limit, "since": since},
)
if not response.ok:
return GotifyErrorModel(response)
j = response.json()
return GotifyPagedMessagesModel(
messages=[GotifyMessageModel(m) for m in j["messages"]],
paging=GotifyPagingModel(j["paging"]),
)
def delete_application_messages(self, application_id: int) -> bool:
return self._delete(f"/application/{application_id}/message").ok
def get_messages(
self, limit: int = 100, since: int = None
) -> Union[GotifyPagedMessagesModel, GotifyErrorModel]:
response = self._get("/message", params={"limit": limit, "since": since})
if not response.ok:
return GotifyErrorModel(response)
j = response.json()
return GotifyPagedMessagesModel(
messages=[GotifyMessageModel(m) for m in j["messages"]],
paging=GotifyPagingModel(j["paging"]),
)
def delete_messages(self) -> bool:
return self._delete("/message").ok
def delete_message(self, message_id: int) -> bool:
return self._delete(f"/message/{message_id}").ok
def listen(
self,
opened_callback: Callable[[], None] = None,
closed_callback: Callable[[int, str], None] = None,
new_message_callback: Callable[[GotifyMessageModel], None] = None,
error_callback: Callable[[Exception], None] = None,
):
def dummy(*args):
...
self.listener.opened.connect(lambda: self.opened_callback(opened_callback))
self.listener.closed.connect(closed_callback or dummy)
self.listener.new_message.connect(new_message_callback or dummy)
self.listener.error.connect(error_callback or dummy)
self.listener.start()
def opened_callback(self, user_callback: Callable[[], None] = None):
self.listener.reset_wait_time()
if user_callback:
user_callback()
def reconnect(self, increase_wait_time: bool = True):
if increase_wait_time:
self.listener.increase_wait_time()
self.listener.start()
def stop(self, reset_wait: bool = False):
if reset_wait:
self.listener.reset_wait_time()
self.listener.stop()
"""
Health
"""
def health(self) -> Union[GotifyHealthModel, GotifyErrorModel]:
response = self._get("/health")
return (
GotifyHealthModel(response.json())
if response.ok
else GotifyErrorModel(response)
)
"""
Version
"""
def version(self) -> Union[GotifyVersionModel, GotifyErrorModel]:
response = self._get("/version")
return (
GotifyVersionModel(response.json())
if response.ok
else GotifyErrorModel(response)
)

View File

@@ -0,0 +1,67 @@
import json
import time
import websocket
from PyQt6 import QtCore
from .models import GotifyMessageModel, GotifyErrorModel
class Listener(QtCore.QThread):
new_message = QtCore.pyqtSignal(GotifyMessageModel)
error = QtCore.pyqtSignal(Exception)
opened = QtCore.pyqtSignal()
closed = QtCore.pyqtSignal(int, str)
def __init__(self, hostname: str, client_token: str):
super(Listener, self).__init__()
self.hostname = hostname
self.client_token = client_token
self.ws = websocket.WebSocketApp(
f"wss://{self.hostname}/stream?token={self.client_token}",
on_message=self._on_message,
on_error=self._on_error,
on_open=self._on_open,
on_close=self._on_close,
)
self.wait_time = 0
self.running = False
def reset_wait_time(self):
self.wait_time = 0
def increase_wait_time(self):
if self.wait_time == 0:
self.wait_time = 1
else:
self.wait_time = min(self.wait_time * 2, 10 * 60)
def _on_message(self, ws: websocket.WebSocketApp, message: str):
self.new_message.emit(GotifyMessageModel(json.loads(message)))
def _on_error(self, ws: websocket.WebSocketApp, error: Exception):
self.error.emit(error)
def _on_open(self, ws: websocket.WebSocketApp):
self.opened.emit()
def _on_close(
self, ws: websocket.WebSocketApp, close_status_code: int, close_msg: str
):
self.closed.emit(close_status_code, close_msg)
def stop(self):
self.ws.close()
self.running = False
def run(self):
self.running = True
try:
time.sleep(self.wait_time)
self.ws.run_forever()
finally:
self.running = False

View File

@@ -0,0 +1,91 @@
import datetime
import logging
from typing import List, Optional
import requests
logger = logging.getLogger("logger")
try:
local_timezone = datetime.datetime.utcnow().astimezone().tzinfo
except Exception as e:
logger.error(f"gotify.models.local_timezone error: {e}")
local_timezone = None
class AttributeDict(dict):
def __init__(self, *args, **kwargs):
super(AttributeDict, self).__init__(*args, **kwargs)
self.__dict__ = self
class GotifyApplicationModel(AttributeDict):
description: str
id: int
image: str
internal: bool
name: str
token: str
class GotifyPagingModel(AttributeDict):
limit: int
next: Optional[str] = None
since: int
size: int
class GotifyMessageModel(AttributeDict):
appid: int
date: datetime.datetime
extras: Optional[dict] = None
id: int
message: str
priority: Optional[int] = None
title: Optional[str] = None
def __init__(self, d: dict, *args, **kwargs):
d.update(
{
"date": datetime.datetime.fromisoformat(
d["date"].split(".")[0] + ".000000+00:00"
).astimezone(local_timezone)
}
)
super(GotifyMessageModel, self).__init__(d, *args, **kwargs)
class GotifyPagedMessagesModel(AttributeDict):
messages: List[GotifyMessageModel]
paging: GotifyPagingModel
class GotifyHealthModel(AttributeDict):
database: str
health: str
class GotifyVersionModel(AttributeDict):
buildDate: str
commit: str
version: str
class GotifyErrorModel(AttributeDict):
error: str
errorCode: int
errorDescription: str
def __init__(self, response: requests.Response, *args, **kwargs):
try:
j = response.json()
except ValueError:
j = {
"error": "unknown",
"errorCode": response.status_code,
"errorDescription": "",
}
super(GotifyErrorModel, self).__init__(j, *args, **kwargs)

View File

@@ -0,0 +1,41 @@
from typing import Optional, Union
from PyQt6 import QtCore, QtGui
from gotify_tray import gotify
class ApplicationModelItem(QtGui.QStandardItem):
def __init__(
self,
application: gotify.GotifyApplicationModel,
icon: Optional[QtGui.QIcon] = None,
*args,
**kwargs
):
super(ApplicationModelItem, self).__init__(application.name)
self.application = application
if icon:
self.setIcon(icon)
class ApplicationAllMessagesItem(QtGui.QStandardItem):
def __init__(self, *args, **kwargs):
super(ApplicationAllMessagesItem, self).__init__("ALL MESSAGES")
class ApplicationModel(QtGui.QStandardItemModel):
def setItem(self, row: int, column: int, item: Union[ApplicationModelItem, ApplicationAllMessagesItem]) -> None:
super(ApplicationModel, self).setItem(row, column, item)
def itemFromIndex(
self, index: QtCore.QModelIndex
) -> Union[ApplicationModelItem, ApplicationAllMessagesItem]:
return super(ApplicationModel, self).itemFromIndex(index)
def itemFromId(self, appid: int) -> Optional[ApplicationModelItem]:
for row in range(self.rowCount()):
item = self.item(row, 0)
if not isinstance(item, ApplicationModelItem):
continue
if item.application.id == appid:
return item
return None

View File

@@ -0,0 +1,398 @@
import getpass
import logging
import os
import tempfile
from typing import List
from gotify_tray import gotify
from gotify_tray.database import Downloader, Settings
from gotify_tray.tasks import (
DeleteApplicationMessagesTask,
DeleteMessageTask,
DeleteAllMessagesTask,
GetApplicationMessagesTask,
GetApplicationsTask,
GetMessagesTask,
)
from PyQt6 import QtCore, QtGui, QtWidgets
from ..__version__ import __title__
from .ApplicationModel import (
ApplicationAllMessagesItem,
ApplicationModel,
ApplicationModelItem,
)
from .designs.widget_main import Ui_Form as Ui_Main
from .themes import set_theme
from .MessagesModel import MessagesModel, MessagesModelItem
from .MessageWidget import MessageWidget
from .SettingsDialog import SettingsDialog
from .Tray import Tray
settings = Settings("gotify-tray")
logger = logging.getLogger("logger")
downloader = Downloader()
class MainWidget(QtWidgets.QWidget, Ui_Main):
def __init__(
self, application_model: ApplicationModel, messages_model: MessagesModel
):
super(MainWidget, self).__init__()
self.setupUi(self)
self.listView_messages.setModel(messages_model)
self.listView_applications.setModel(application_model)
self.listView_applications.setFixedWidth(180)
icon_size = settings.value("ApplicationModelItem/icon/size", type=int)
self.listView_applications.setIconSize(QtCore.QSize(icon_size, icon_size))
label_size = settings.value("MainWidget/status_image/size", type=int)
self.label_status.setFixedSize(QtCore.QSize(label_size, label_size))
self.label_status_connecting()
def label_status_active(self):
self.label_status.setToolTip("Listening for new messages")
self.label_status.setStyleSheet("QLabel {background-color: green;}")
def label_status_connecting(self):
self.label_status.setToolTip("Connecting...")
self.label_status.setStyleSheet("QLabel {background-color: orange;}")
def label_status_inactive(self):
self.label_status.setToolTip("Listener inactive")
self.label_status.setStyleSheet("QLabel {background-color: grey;}")
def label_status_error(self):
self.label_status.setToolTip("Listener error")
self.label_status.setStyleSheet("QLabel {background-color: red;}")
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, app: QtWidgets.QApplication):
super(MainWindow, self).__init__()
self.app = app
self.shutting_down = False
def init_ui(self):
self.gotify_client = gotify.GotifyClient(
settings.value("Server/url", type=str),
settings.value("Server/client_token", type=str),
)
self.setWindowTitle(__title__)
self.resize(800, 600)
set_theme(self.app, settings.value("MainWindow/theme", type=str))
self.application_model = ApplicationModel()
self.messages_model = MessagesModel()
self.main_widget = MainWidget(self.application_model, self.messages_model)
self.setCentralWidget(self.main_widget)
self.refresh_applications()
self.tray = Tray()
self.tray.show()
self.restore_window_state()
self.gotify_client.listen(
new_message_callback=self.new_message_callback,
opened_callback=self.listener_opened_callback,
closed_callback=self.listener_closed_callback,
)
self.link_callbacks()
self.show()
self.window_state_to_restore = QtCore.Qt.WindowState.WindowNoState
if settings.value("MainWindow/start_minimized", type=bool) and settings.value(
"tray/show", type=bool
):
self.tray_activated_callback(
QtWidgets.QSystemTrayIcon.ActivationReason.Trigger
)
def refresh_applications(self):
self.application_model.clear()
self.messages_model.clear()
self.main_widget.listView_applications.clearSelection()
self.main_widget.listView_applications.setEnabled(False)
self.application_model.setItem(0, 0, ApplicationAllMessagesItem())
def get_applications_callback(
applications: List[gotify.GotifyApplicationModel],
):
for i, application in enumerate(applications):
icon = (
QtGui.QIcon(
downloader.get_filename(
f"{self.gotify_client.url}/{application.image}"
)
)
if settings.value("ApplicationModelItem/icon/show", type=bool)
else None
)
self.application_model.setItem(
i + 1, 0, ApplicationModelItem(application, icon),
)
self.get_applications_task = GetApplicationsTask(self.gotify_client)
self.get_applications_task.success.connect(get_applications_callback)
self.get_applications_task.finished.connect(
self.get_applications_finished_callback
)
self.get_applications_task.start()
def get_applications_finished_callback(self):
self.main_widget.listView_applications.setEnabled(True)
self.main_widget.listView_applications.setCurrentIndex(
self.application_model.index(0, 0)
)
def insert_message(
self,
row: int,
message: gotify.GotifyMessageModel,
application: gotify.GotifyApplicationModel,
):
message_item = MessagesModelItem(message)
self.messages_model.insertRow(row, message_item)
message_widget = MessageWidget(
message_item,
image_path=downloader.get_filename(
f"{self.gotify_client.url}/{application.image}"
)
if settings.value("MessageWidget/image/show", type=bool)
else "",
)
self.main_widget.listView_messages.setIndexWidget(
self.messages_model.indexFromItem(message_item), message_widget
)
message_widget.deletion_requested.connect(
self.message_deletion_requested_callback
)
def listener_opened_callback(self):
self.main_widget.label_status_active()
self.tray.set_icon_ok()
def listener_closed_callback(self, close_status_code: int, close_msg: str):
self.main_widget.label_status_connecting()
self.tray.set_icon_error()
if not self.shutting_down:
self.gotify_client.reconnect()
def application_selection_changed(
self, current: QtCore.QModelIndex, previous: QtCore.QModelIndex
):
if item := self.application_model.itemFromIndex(current):
self.main_widget.label_selected.setText(item.text())
self.messages_model.clear()
if isinstance(item, ApplicationModelItem):
def get_application_messages_callback(
page: gotify.GotifyPagedMessagesModel,
):
for i, message in enumerate(page.messages):
self.insert_message(i, message, item.application)
self.get_application_messages_task = GetApplicationMessagesTask(
item.application.id, self.gotify_client
)
self.get_application_messages_task.success.connect(
get_application_messages_callback
)
self.get_application_messages_task.start()
elif isinstance(item, ApplicationAllMessagesItem):
def get_messages_callback(page: gotify.GotifyPagedMessagesModel):
for i, message in enumerate(page.messages):
if item := self.application_model.itemFromId(message.appid):
self.insert_message(i, message, item.application)
self.get_messages_task = GetMessagesTask(self.gotify_client)
self.get_messages_task.success.connect(get_messages_callback)
self.get_messages_task.start()
def refresh_callback(self):
self.application_model.clear()
self.messages_model.clear()
self.refresh_applications()
if not self.gotify_client.listener.running:
self.gotify_client.listener.reset_wait_time()
else:
self.gotify_client.stop(reset_wait=True)
self.gotify_client.reconnect(increase_wait_time=False)
def delete_all_callback(self):
selection_model = self.main_widget.listView_applications.selectionModel()
if item := self.application_model.itemFromIndex(selection_model.currentIndex()):
self.messages_model.clear()
if isinstance(item, ApplicationModelItem):
self.delete_application_messages_task = DeleteApplicationMessagesTask(
item.application.id, self.gotify_client
)
self.delete_application_messages_task.start()
elif isinstance(item, ApplicationAllMessagesItem):
self.delete_all_messages_task = DeleteAllMessagesTask(
self.gotify_client
)
self.delete_all_messages_task.start()
def new_message_callback(self, message: gotify.GotifyMessageModel):
# Show a notification
application_item = self.application_model.itemFromId(message.appid)
if not self.isActiveWindow() and message.priority >= settings.value(
"tray/notifications/priority", type=int
):
image_url = f"{self.gotify_client.url}/{application_item.application.image}"
self.tray.showMessage(
message.title,
message.message,
QtGui.QIcon(downloader.get_filename(image_url))
if settings.value("tray/notifications/icon/show", type=bool)
else QtWidgets.QSystemTrayIcon.Information,
msecs=settings.value("tray/notifications/duration_ms", type=int),
)
# Add the message to the message_model, if its corresponding application is selected
application_index = (
self.main_widget.listView_applications.selectionModel().currentIndex()
)
if selected_application_item := self.application_model.itemFromIndex(
application_index
):
if isinstance(selected_application_item, ApplicationModelItem):
# A single application is selected
if message.appid == selected_application_item.application.id:
self.insert_message(0, message, application_item.application)
elif isinstance(selected_application_item, ApplicationAllMessagesItem):
# "All messages' is selected
self.insert_message(0, message, application_item.application)
def message_deletion_requested_callback(self, message_item: MessagesModelItem):
self.messages_model.removeRow(message_item.row())
self.delete_message_task = DeleteMessageTask(
message_item.message.id, self.gotify_client
)
self.delete_message_task.start()
def tray_activated_callback(
self, reason: QtWidgets.QSystemTrayIcon.ActivationReason
):
if reason == QtWidgets.QSystemTrayIcon.ActivationReason.Trigger:
if self.windowState() & QtCore.Qt.WindowState.WindowMinimized or self.windowState() == (
QtCore.Qt.WindowState.WindowMinimized
| QtCore.Qt.WindowState.WindowMaximized
):
self.show()
self.setWindowState(
self.window_state_to_restore | QtCore.Qt.WindowState.WindowActive
) # Set the window to its normal state
else:
window_state_temp = self.windowState()
self.setWindowState(QtCore.Qt.WindowState.WindowMinimized)
self.hide()
self.window_state_to_restore = window_state_temp
def message_clicked_callback(self):
self.main_widget.listView_messages.scrollToTop()
self.setWindowState(
self.window_state_to_restore | QtCore.Qt.WindowState.WindowActive
)
self.show()
def settings_callback(self):
settings_dialog = SettingsDialog(self.app)
accepted = settings_dialog.exec()
if accepted and settings_dialog.settings_changed:
settings_dialog.apply_settings()
if settings_dialog.server_changed:
mb = QtWidgets.QMessageBox(
QtWidgets.QMessageBox.Icon.Information,
"Restart",
"Restart to apply server changes",
QtWidgets.QMessageBox.StandardButton.Yes
| QtWidgets.QMessageBox.StandardButton.Cancel,
parent=self,
)
r = mb.exec()
if r == QtWidgets.QMessageBox.StandardButton.Yes:
self.close()
def link_callbacks(self):
self.main_widget.listView_applications.selectionModel().currentChanged.connect(
self.application_selection_changed
)
self.main_widget.pb_refresh.clicked.connect(self.refresh_callback)
self.main_widget.pb_delete_all.clicked.connect(self.delete_all_callback)
self.tray.actionQuit.triggered.connect(self.close)
self.tray.actionSettings.triggered.connect(self.settings_callback)
self.tray.actionToggleWindow.triggered.connect(
lambda: self.tray_activated_callback(
QtWidgets.QSystemTrayIcon.ActivationReason.Trigger
)
)
self.tray.messageClicked.connect(self.message_clicked_callback)
self.tray.activated.connect(self.tray_activated_callback)
def acquire_lock(self) -> bool:
temp_dir = tempfile.gettempdir()
lock_filename = os.path.join(
temp_dir, __title__ + "-" + getpass.getuser() + ".lock"
)
self.lock_file = QtCore.QLockFile(lock_filename)
self.lock_file.setStaleLockTime(0)
return self.lock_file.tryLock()
def restore_window_state(self):
window_geometry = settings.value("MainWindow/geometry", type=QtCore.QByteArray)
window_state = settings.value("MainWindow/state", type=QtCore.QByteArray)
if window_geometry:
self.restoreGeometry(window_geometry)
if window_state:
self.restoreState(window_state)
def save_window_state(self):
settings.setValue("MainWindow/geometry", self.saveGeometry())
settings.setValue("MainWindow/state", self.saveState())
def changeEvent(self, event: QtCore.QEvent) -> None:
if event.type() == QtCore.QEvent.Type.WindowStateChange:
if settings.value("tray/show", type=bool) and settings.value(
"tray/minimize", type=bool
):
if self.windowState() & QtCore.Qt.WindowState.WindowMinimized:
self.window_state_to_restore = (
self.windowState() & ~QtCore.Qt.WindowState.WindowMinimized
)
self.hide()
super(MainWindow, self).changeEvent(event)
def closeEvent(self, e: QtGui.QCloseEvent) -> None:
self.save_window_state()
if settings.value("tray/show", type=bool):
self.tray.hide()
self.lock_file.unlock()
self.shutting_down = True
self.gotify_client.stop()
super(MainWindow, self).closeEvent(e)
self.app.quit()

View File

@@ -0,0 +1,84 @@
import re
from PyQt6 import QtCore, QtGui, QtWidgets
from .MessagesModel import MessagesModelItem
from .designs.widget_message import Ui_Form
from gotify_tray.database import Settings
settings = Settings("gotify-tray")
def convert_links(text):
_link = re.compile(
r'(?:(https://|http://)|(www\.))(\S+\b/?)([!"#$%&\'()*+,\-./:;<=>?@[\\\]^_`{|}~]*)(\s|$)',
re.I,
)
def replace(match):
groups = match.groups()
protocol = groups[0] or "" # may be None
www_lead = groups[1] or "" # may be None
return '<a href="http://{1}{2}" rel="nofollow">{0}{1}{2}</a>{3}{4}'.format(
protocol, www_lead, *groups[2:]
)
return _link.sub(replace, text)
class MessageWidget(QtWidgets.QWidget, Ui_Form):
deletion_requested = QtCore.pyqtSignal(MessagesModelItem)
def __init__(self, message_item: MessagesModelItem, image_path: str = ""):
super(MessageWidget, self).__init__()
self.setupUi(self)
self.setAutoFillBackground(True)
self.message_item = message_item
message = self.message_item.message
# Fonts
font_title = QtGui.QFont()
font_date = QtGui.QFont()
font_content = QtGui.QFont()
font_title.fromString(settings.value("MessageWidget/font/title", type=str))
font_date.fromString(settings.value("MessageWidget/font/date", type=str))
font_content.fromString(settings.value("MessageWidget/font/content", type=str))
self.label_title.setFont(font_title)
self.label_date.setFont(font_date)
self.text_message.setFont(font_content)
self.label_title.setText(message.title)
self.label_date.setText(message.date.strftime("%Y-%m-%d, %H:%M"))
if markdown := message.get("extras", {}).get("client::display", {}).get("contentType") == "text/markdown":
self.text_message.setTextFormat(QtCore.Qt.TextFormat.MarkdownText)
self.text_message.setText(convert_links(message.message))
if image_path:
image_size = settings.value("MessageWidget/image/size", type=int)
self.label_image.setFixedSize(QtCore.QSize(image_size, image_size))
pixmap = QtGui.QPixmap(image_path).scaled(image_size, image_size, aspectRatioMode=QtCore.Qt.AspectRatioMode.KeepAspectRatioByExpanding)
self.label_image.setPixmap(pixmap)
else:
self.label_image.hide()
# Set MessagesModelItem's size hint based on the size of this widget
self.gridLayout_frame.setContentsMargins(10, 5, 10, 5)
self.gridLayout.setContentsMargins(5, 15, 5, 15)
self.adjustSize()
size_hint = self.message_item.sizeHint()
self.message_item.setSizeHint(
QtCore.QSize(
size_hint.width(),
self.height()
)
)
self.pb_delete.setIcon(QtGui.QIcon("gotify_tray/gui/images/trashcan.svg"))
self.pb_delete.setIconSize(QtCore.QSize(24, 24))
self.link_callbacks()
def link_callbacks(self):
self.pb_delete.clicked.connect(lambda: self.deletion_requested.emit(self.message_item))

View File

@@ -0,0 +1,17 @@
from typing import cast
from PyQt6 import QtCore, QtGui, QtWidgets
from gotify_tray import gotify
class MessagesModelItem(QtGui.QStandardItem):
def __init__(self, message: gotify.GotifyMessageModel, *args, **kwargs):
super(MessagesModelItem, self).__init__()
self.message = message
class MessagesModel(QtGui.QStandardItemModel):
def setItem(self, row: int, column: int, item: MessagesModelItem) -> None:
super(MessagesModel, self).setItem(row, column, item)
def itemFromIndex(self, index: QtCore.QModelIndex) -> MessagesModelItem:
return cast(MessagesModelItem, super(MessagesModel, self).itemFromIndex(index))

View File

@@ -0,0 +1,54 @@
from PyQt6 import QtWidgets
from gotify_tray.tasks import VerifyServerInfoTask
from .designs.widget_server import Ui_Dialog
class ServerInfoDialog(QtWidgets.QDialog, Ui_Dialog):
def __init__(self, url: str = "", token: str = ""):
super(ServerInfoDialog, self).__init__()
self.setupUi(self)
self.setWindowTitle("Server info")
self.line_url.setText(url)
self.line_token.setText(token)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setDisabled(True)
self.link_callbacks()
def test_server_info(self):
self.pb_test.setStyleSheet("")
self.line_url.setStyleSheet("")
self.line_token.setStyleSheet("")
url = self.line_url.text()
client_token = self.line_token.text()
if not url or not client_token:
return
self.pb_test.setDisabled(True)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setDisabled(True)
self.task = VerifyServerInfoTask(url, client_token)
self.task.success.connect(self.server_info_success)
self.task.incorrect_token.connect(self.incorrect_token_callback)
self.task.incorrect_url.connect(self.incorrect_url_callback)
self.task.start()
def server_info_success(self):
self.pb_test.setEnabled(True)
self.pb_test.setStyleSheet("background-color: rgba(0, 255, 0, 100);")
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled(True)
def incorrect_token_callback(self):
self.pb_test.setEnabled(True)
self.pb_test.setStyleSheet("background-color: rgba(255, 0, 0, 100);")
self.line_token.setStyleSheet("border: 1px solid red;")
def incorrect_url_callback(self):
self.pb_test.setEnabled(True)
self.pb_test.setStyleSheet("background-color: rgba(255, 0, 0, 100);")
self.line_url.setStyleSheet("border: 1px solid red;")
def link_callbacks(self):
self.pb_test.clicked.connect(self.test_server_info)
self.line_url.textChanged.connect(lambda: self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setDisabled(True))
self.line_token.textChanged.connect(lambda: self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setDisabled(True))

View File

@@ -0,0 +1,170 @@
from gotify_tray.database import Settings
from gotify_tray.utils import verify_server
from PyQt6 import QtCore, QtGui, QtWidgets
from .designs.widget_settings import Ui_Dialog
from .themes import set_theme
settings = Settings("gotify-tray")
class SettingsDialog(QtWidgets.QDialog, Ui_Dialog):
def __init__(self, app: QtWidgets.QApplication):
super(SettingsDialog, self).__init__()
self.setupUi(self)
self.setWindowTitle("Settings")
self.app = app
self.settings_changed = False
self.changes_applied = False
self.server_changed = False
self.initUI()
self.link_callbacks()
def initUI(self):
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Apply
).setEnabled(False)
# Fonts
self.set_font_labels()
# Theme
self.combo_theme.addItems(["default", "dark"])
self.combo_theme.setCurrentText(settings.value("MainWindow/theme", type=str))
# Icons
self.cb_icons_application.setChecked(
settings.value("ApplicationModelItem/icon/show", type=bool)
)
self.cb_icons_message.setChecked(
settings.value("MessageWidget/image/show", type=bool)
)
self.cb_icons_notification.setChecked(
settings.value("tray/notifications/icon/show", type=bool)
)
# Notifications
self.spin_priority.setValue(
settings.value("tray/notifications/priority", type=int)
)
self.spin_duration.setValue(
settings.value("tray/notifications/duration_ms", type=int)
)
def set_font_labels(self):
self.label_font_message_title.setText(
settings.value("MessageWidget/font/title", type=str)
)
self.label_font_message_date.setText(
settings.value("MessageWidget/font/date", type=str)
)
self.label_font_message_content.setText(
settings.value("MessageWidget/font/content", type=str)
)
def change_font_callback(self, key: str):
font = QtGui.QFont()
font.fromString(settings.value(key, type=str))
font, accepted = QtWidgets.QFontDialog.getFont(font, self, "Select font")
if not accepted:
return
self.settings_changed_callback()
label: QtWidgets.QLabel = getattr(
self, "label_font_message_" + key.split("/")[-1]
)
label.setText(font.toString())
def change_server_info_callback(self):
self.server_changed = verify_server(force_new=True)
def settings_changed_callback(self, *args, **kwargs):
self.settings_changed = True
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Apply
).setEnabled(True)
def reset_settings_callback(self):
response = QtWidgets.QMessageBox.warning(
self,
"Are you sure?",
"Reset all settings?",
QtWidgets.QMessageBox.StandardButton.Ok
| QtWidgets.QMessageBox.StandardButton.Cancel,
defaultButton=QtWidgets.QMessageBox.StandardButton.Cancel,
)
if response == QtWidgets.QMessageBox.StandardButton.Ok:
settings.clear()
def link_callbacks(self):
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Apply
).clicked.connect(self.apply_settings)
# Fonts
self.pb_font_message_title.clicked.connect(
lambda: self.change_font_callback("MessageWidget/font/title")
)
self.pb_font_message_date.clicked.connect(
lambda: self.change_font_callback("MessageWidget/font/date")
)
self.pb_font_message_content.clicked.connect(
lambda: self.change_font_callback("MessageWidget/font/content")
)
# Theme
self.combo_theme.currentTextChanged.connect(self.settings_changed_callback)
# Icons
self.cb_icons_application.stateChanged.connect(self.settings_changed_callback)
self.cb_icons_message.stateChanged.connect(self.settings_changed_callback)
self.cb_icons_notification.stateChanged.connect(self.settings_changed_callback)
# Notifications
self.spin_priority.valueChanged.connect(self.settings_changed_callback)
self.spin_duration.valueChanged.connect(self.settings_changed_callback)
# Server info
self.pb_change_server_info.clicked.connect(self.change_server_info_callback)
def apply_settings(self):
# Fonts
settings.setValue(
"MessageWidget/font/title", self.label_font_message_title.text()
)
settings.setValue(
"MessageWidget/font/date", self.label_font_message_date.text()
)
settings.setValue(
"MessageWidget/font/content", self.label_font_message_content.text()
)
# Theme
settings.setValue("MainWindow/theme", self.combo_theme.currentText())
set_theme(self.app, self.combo_theme.currentText())
# Icons
settings.setValue(
"ApplicationModelItem/icon/show", self.cb_icons_application.isChecked()
)
settings.setValue("MessageWidget/image/show", self.cb_icons_message.isChecked())
settings.setValue(
"tray/notifications/icon/show", self.cb_icons_notification.isChecked()
)
# Priority
settings.setValue("tray/notifications/priority", self.spin_priority.value())
settings.setValue("tray/notifications/duration_ms", self.spin_duration.value())
self.settings_changed = False
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Apply
).setEnabled(False)
self.changes_applied = True

34
gotify_tray/gui/Tray.py Normal file
View File

@@ -0,0 +1,34 @@
from PyQt6 import QtGui, QtWidgets
from gotify_tray.__version__ import __title__
class Tray(QtWidgets.QSystemTrayIcon):
def __init__(self):
super(Tray, self).__init__()
self.set_icon_error()
self.setToolTip(__title__)
# Tray menu items
menu = QtWidgets.QMenu()
self.actionSettings = QtGui.QAction("Settings", self)
menu.addAction(self.actionSettings)
menu.addSeparator()
self.actionToggleWindow = QtGui.QAction("Toggle Window", self)
menu.addAction(self.actionToggleWindow)
menu.addSeparator()
self.actionQuit = QtGui.QAction("Quit", self)
menu.addAction(self.actionQuit)
self.setContextMenu(menu)
def set_icon_ok(self):
self.setIcon(QtGui.QIcon("gotify_tray/gui/images/gotify-small.png"))
def set_icon_error(self):
self.setIcon(QtGui.QIcon("gotify_tray/gui/images/gotify-small-error.png"))

View File

@@ -0,0 +1,2 @@
from .MainWindow import MainWindow
from .ServerInfoDialog import ServerInfoDialog

View File

@@ -0,0 +1,95 @@
# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_main.ui'
#
# Created by: PyQt6 UI code generator 6.1.1
#
# 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.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(809, 572)
self.gridLayout_2 = QtWidgets.QGridLayout(Form)
self.gridLayout_2.setObjectName("gridLayout_2")
self.listView_applications = QtWidgets.QListView(Form)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.listView_applications.sizePolicy().hasHeightForWidth())
self.listView_applications.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(13)
self.listView_applications.setFont(font)
self.listView_applications.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
self.listView_applications.setWordWrap(True)
self.listView_applications.setObjectName("listView_applications")
self.gridLayout_2.addWidget(self.listView_applications, 0, 0, 1, 1)
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setObjectName("gridLayout")
self.pb_delete_all = QtWidgets.QPushButton(Form)
self.pb_delete_all.setMinimumSize(QtCore.QSize(0, 32))
font = QtGui.QFont()
font.setPointSize(10)
self.pb_delete_all.setFont(font)
self.pb_delete_all.setObjectName("pb_delete_all")
self.gridLayout.addWidget(self.pb_delete_all, 0, 5, 1, 1)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.gridLayout.addItem(spacerItem, 0, 3, 1, 1)
self.label_selected = QtWidgets.QLabel(Form)
self.label_selected.setMinimumSize(QtCore.QSize(0, 32))
font = QtGui.QFont()
font.setPointSize(15)
font.setBold(True)
self.label_selected.setFont(font)
self.label_selected.setObjectName("label_selected")
self.gridLayout.addWidget(self.label_selected, 0, 2, 1, 1)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.gridLayout.addItem(spacerItem1, 0, 1, 1, 1)
self.pb_refresh = QtWidgets.QPushButton(Form)
self.pb_refresh.setMinimumSize(QtCore.QSize(0, 32))
font = QtGui.QFont()
font.setPointSize(10)
self.pb_refresh.setFont(font)
self.pb_refresh.setObjectName("pb_refresh")
self.gridLayout.addWidget(self.pb_refresh, 0, 4, 1, 1)
self.label_status = QtWidgets.QLabel(Form)
self.label_status.setText("")
self.label_status.setObjectName("label_status")
self.gridLayout.addWidget(self.label_status, 0, 0, 1, 1)
self.listView_messages = QtWidgets.QListView(Form)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.listView_messages.sizePolicy().hasHeightForWidth())
self.listView_messages.setSizePolicy(sizePolicy)
self.listView_messages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel)
self.listView_messages.setObjectName("listView_messages")
self.gridLayout.addWidget(self.listView_messages, 1, 0, 1, 6)
self.gridLayout_2.addLayout(self.gridLayout, 0, 1, 1, 1)
self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form)
Form.setTabOrder(self.pb_refresh, self.pb_delete_all)
Form.setTabOrder(self.pb_delete_all, self.listView_messages)
Form.setTabOrder(self.listView_messages, self.listView_applications)
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
self.pb_delete_all.setText(_translate("Form", "Delete All"))
self.label_selected.setText(_translate("Form", "TextLabel"))
self.pb_refresh.setText(_translate("Form", "Refresh"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Form = QtWidgets.QWidget()
ui = Ui_Form()
ui.setupUi(Form)
Form.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>809</width>
<height>572</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QListView" name="listView_applications">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>13</pointsize>
</font>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="5">
<widget class="QPushButton" name="pb_delete_all">
<property name="minimumSize">
<size>
<width>0</width>
<height>32</height>
</size>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>Delete All</string>
</property>
</widget>
</item>
<item row="0" column="3">
<spacer name="horizontalSpacer">
<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 row="0" column="2">
<widget class="QLabel" name="label_selected">
<property name="minimumSize">
<size>
<width>0</width>
<height>32</height>
</size>
</property>
<property name="font">
<font>
<pointsize>15</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="0" column="1">
<spacer name="horizontalSpacer_2">
<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 row="0" column="4">
<widget class="QPushButton" name="pb_refresh">
<property name="minimumSize">
<size>
<width>0</width>
<height>32</height>
</size>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>Refresh</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_status">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="0" colspan="6">
<widget class="QListView" name="listView_messages">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>pb_refresh</tabstop>
<tabstop>pb_delete_all</tabstop>
<tabstop>listView_messages</tabstop>
<tabstop>listView_applications</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,94 @@
# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_message.ui'
#
# Created by: PyQt6 UI code generator 6.1.1
#
# 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.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(454, 122)
self.gridLayout = QtWidgets.QGridLayout(Form)
self.gridLayout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize)
self.gridLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setObjectName("gridLayout")
self.frame = QtWidgets.QFrame(Form)
self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame.setObjectName("frame")
self.gridLayout_frame = QtWidgets.QGridLayout(self.frame)
self.gridLayout_frame.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize)
self.gridLayout_frame.setContentsMargins(-1, 0, -1, 0)
self.gridLayout_frame.setObjectName("gridLayout_frame")
self.label_title = QtWidgets.QLabel(self.frame)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.label_title.sizePolicy().hasHeightForWidth())
self.label_title.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(17)
font.setBold(False)
font.setWeight(50)
self.label_title.setFont(font)
self.label_title.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.LinksAccessibleByMouse|QtCore.Qt.TextInteractionFlag.TextSelectableByMouse)
self.label_title.setObjectName("label_title")
self.gridLayout_frame.addWidget(self.label_title, 0, 1, 1, 1)
self.text_message = QtWidgets.QLabel(self.frame)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.MinimumExpanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.text_message.sizePolicy().hasHeightForWidth())
self.text_message.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(11)
self.text_message.setFont(font)
self.text_message.setWordWrap(True)
self.text_message.setOpenExternalLinks(True)
self.text_message.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.LinksAccessibleByMouse|QtCore.Qt.TextInteractionFlag.TextSelectableByMouse)
self.text_message.setObjectName("text_message")
self.gridLayout_frame.addWidget(self.text_message, 3, 1, 1, 3)
self.label_date = QtWidgets.QLabel(self.frame)
font = QtGui.QFont()
font.setPointSize(11)
self.label_date.setFont(font)
self.label_date.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.LinksAccessibleByMouse|QtCore.Qt.TextInteractionFlag.TextSelectableByMouse)
self.label_date.setObjectName("label_date")
self.gridLayout_frame.addWidget(self.label_date, 2, 1, 1, 1)
self.pb_delete = QtWidgets.QPushButton(self.frame)
self.pb_delete.setText("")
self.pb_delete.setFlat(True)
self.pb_delete.setObjectName("pb_delete")
self.gridLayout_frame.addWidget(self.pb_delete, 0, 3, 1, 1)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.gridLayout_frame.addItem(spacerItem, 0, 2, 1, 1)
self.label_image = QtWidgets.QLabel(self.frame)
self.label_image.setText("")
self.label_image.setObjectName("label_image")
self.gridLayout_frame.addWidget(self.label_image, 0, 0, 1, 1)
self.gridLayout.addWidget(self.frame, 0, 0, 1, 1)
self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
self.label_title.setText(_translate("Form", "Title"))
self.text_message.setText(_translate("Form", "TextLabel"))
self.label_date.setText(_translate("Form", "Date"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Form = QtWidgets.QWidget()
ui = Ui_Form()
ui.setupUi(Form)
Form.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>454</width>
<height>122</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout_frame">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="1">
<widget class="QLabel" name="label_title">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>17</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Title</string>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="3" column="1" colspan="3">
<widget class="QLabel" name="text_message">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string>TextLabel</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="label_date">
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string>Date</string>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QPushButton" name="pb_delete">
<property name="text">
<string/>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<spacer name="horizontalSpacer">
<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 row="0" column="0">
<widget class="QLabel" name="label_image">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,64 @@
# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_server.ui'
#
# Created by: PyQt6 UI code generator 6.1.1
#
# 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.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(300, 124)
self.gridLayout = QtWidgets.QGridLayout(Dialog)
self.gridLayout.setObjectName("gridLayout")
self.formLayout = QtWidgets.QFormLayout()
self.formLayout.setObjectName("formLayout")
self.label = QtWidgets.QLabel(Dialog)
self.label.setObjectName("label")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label)
self.line_url = QtWidgets.QLineEdit(Dialog)
self.line_url.setObjectName("line_url")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.FieldRole, self.line_url)
self.label_2 = QtWidgets.QLabel(Dialog)
self.label_2.setObjectName("label_2")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_2)
self.line_token = QtWidgets.QLineEdit(Dialog)
self.line_token.setObjectName("line_token")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.FieldRole, self.line_token)
self.gridLayout.addLayout(self.formLayout, 0, 0, 1, 2)
self.pb_test = QtWidgets.QPushButton(Dialog)
self.pb_test.setObjectName("pb_test")
self.gridLayout.addWidget(self.pb_test, 1, 1, 1, 1)
self.buttonBox = QtWidgets.QDialogButtonBox(Dialog)
self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Ok)
self.buttonBox.setObjectName("buttonBox")
self.gridLayout.addWidget(self.buttonBox, 2, 1, 1, 1)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.gridLayout.addItem(spacerItem, 1, 0, 1, 1)
self.retranslateUi(Dialog)
self.buttonBox.accepted.connect(Dialog.accept)
self.buttonBox.rejected.connect(Dialog.reject)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
self.label.setText(_translate("Dialog", "Server url:"))
self.label_2.setText(_translate("Dialog", "Client token:"))
self.pb_test.setText(_translate("Dialog", "Test"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Dialog = QtWidgets.QDialog()
ui = Ui_Dialog()
ui.setupUi(Dialog)
Dialog.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>124</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Server url:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="line_url"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Client token:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="line_token"/>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="pb_test">
<property name="text">
<string>Test</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="1" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,175 @@
# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_settings.ui'
#
# Created by: PyQt6 UI code generator 6.1.1
#
# 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.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(375, 540)
self.verticalLayout = QtWidgets.QVBoxLayout(Dialog)
self.verticalLayout.setObjectName("verticalLayout")
self.groupBox = QtWidgets.QGroupBox(Dialog)
self.groupBox.setObjectName("groupBox")
self.gridLayout = QtWidgets.QGridLayout(self.groupBox)
self.gridLayout.setObjectName("gridLayout")
self.groupBox_2 = QtWidgets.QGroupBox(self.groupBox)
self.groupBox_2.setObjectName("groupBox_2")
self.gridLayout_2 = QtWidgets.QGridLayout(self.groupBox_2)
self.gridLayout_2.setObjectName("gridLayout_2")
self.label_3 = QtWidgets.QLabel(self.groupBox_2)
self.label_3.setObjectName("label_3")
self.gridLayout_2.addWidget(self.label_3, 2, 0, 1, 1)
self.label_2 = QtWidgets.QLabel(self.groupBox_2)
self.label_2.setObjectName("label_2")
self.gridLayout_2.addWidget(self.label_2, 1, 0, 1, 1)
self.pb_font_message_content = QtWidgets.QPushButton(self.groupBox_2)
self.pb_font_message_content.setMaximumSize(QtCore.QSize(30, 16777215))
self.pb_font_message_content.setObjectName("pb_font_message_content")
self.gridLayout_2.addWidget(self.pb_font_message_content, 2, 1, 1, 1)
self.pb_font_message_date = QtWidgets.QPushButton(self.groupBox_2)
self.pb_font_message_date.setMaximumSize(QtCore.QSize(30, 16777215))
self.pb_font_message_date.setObjectName("pb_font_message_date")
self.gridLayout_2.addWidget(self.pb_font_message_date, 1, 1, 1, 1)
self.label = QtWidgets.QLabel(self.groupBox_2)
self.label.setObjectName("label")
self.gridLayout_2.addWidget(self.label, 0, 0, 1, 1)
self.label_font_message_title = QtWidgets.QLabel(self.groupBox_2)
self.label_font_message_title.setObjectName("label_font_message_title")
self.gridLayout_2.addWidget(self.label_font_message_title, 0, 2, 1, 1)
self.pb_font_message_title = QtWidgets.QPushButton(self.groupBox_2)
self.pb_font_message_title.setMaximumSize(QtCore.QSize(30, 16777215))
self.pb_font_message_title.setObjectName("pb_font_message_title")
self.gridLayout_2.addWidget(self.pb_font_message_title, 0, 1, 1, 1)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1)
self.label_font_message_date = QtWidgets.QLabel(self.groupBox_2)
self.label_font_message_date.setObjectName("label_font_message_date")
self.gridLayout_2.addWidget(self.label_font_message_date, 1, 2, 1, 1)
self.label_font_message_content = QtWidgets.QLabel(self.groupBox_2)
self.label_font_message_content.setObjectName("label_font_message_content")
self.gridLayout_2.addWidget(self.label_font_message_content, 2, 2, 1, 1)
self.gridLayout.addWidget(self.groupBox_2, 0, 0, 1, 1)
self.verticalLayout.addWidget(self.groupBox)
self.groupBox_6 = QtWidgets.QGroupBox(Dialog)
self.groupBox_6.setObjectName("groupBox_6")
self.gridLayout_5 = QtWidgets.QGridLayout(self.groupBox_6)
self.gridLayout_5.setObjectName("gridLayout_5")
self.combo_theme = QtWidgets.QComboBox(self.groupBox_6)
self.combo_theme.setObjectName("combo_theme")
self.gridLayout_5.addWidget(self.combo_theme, 0, 0, 1, 1)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.gridLayout_5.addItem(spacerItem1, 0, 1, 1, 1)
self.verticalLayout.addWidget(self.groupBox_6)
self.groupBox_3 = QtWidgets.QGroupBox(Dialog)
self.groupBox_3.setObjectName("groupBox_3")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.groupBox_3)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.cb_icons_application = QtWidgets.QCheckBox(self.groupBox_3)
self.cb_icons_application.setObjectName("cb_icons_application")
self.verticalLayout_2.addWidget(self.cb_icons_application)
self.cb_icons_message = QtWidgets.QCheckBox(self.groupBox_3)
self.cb_icons_message.setObjectName("cb_icons_message")
self.verticalLayout_2.addWidget(self.cb_icons_message)
self.cb_icons_notification = QtWidgets.QCheckBox(self.groupBox_3)
self.cb_icons_notification.setObjectName("cb_icons_notification")
self.verticalLayout_2.addWidget(self.cb_icons_notification)
self.verticalLayout.addWidget(self.groupBox_3)
self.groupBox_5 = QtWidgets.QGroupBox(Dialog)
self.groupBox_5.setObjectName("groupBox_5")
self.gridLayout_4 = QtWidgets.QGridLayout(self.groupBox_5)
self.gridLayout_4.setObjectName("gridLayout_4")
self.spin_priority = QtWidgets.QSpinBox(self.groupBox_5)
self.spin_priority.setMinimum(1)
self.spin_priority.setMaximum(10)
self.spin_priority.setProperty("value", 5)
self.spin_priority.setObjectName("spin_priority")
self.gridLayout_4.addWidget(self.spin_priority, 0, 1, 1, 1)
spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.gridLayout_4.addItem(spacerItem2, 0, 2, 1, 1)
self.label_4 = QtWidgets.QLabel(self.groupBox_5)
self.label_4.setObjectName("label_4")
self.gridLayout_4.addWidget(self.label_4, 0, 0, 1, 1)
self.spin_duration = QtWidgets.QSpinBox(self.groupBox_5)
self.spin_duration.setMinimum(500)
self.spin_duration.setMaximum(30000)
self.spin_duration.setSingleStep(100)
self.spin_duration.setObjectName("spin_duration")
self.gridLayout_4.addWidget(self.spin_duration, 1, 1, 1, 1)
self.label_5 = QtWidgets.QLabel(self.groupBox_5)
self.label_5.setObjectName("label_5")
self.gridLayout_4.addWidget(self.label_5, 1, 0, 1, 1)
self.label_6 = QtWidgets.QLabel(self.groupBox_5)
self.label_6.setObjectName("label_6")
self.gridLayout_4.addWidget(self.label_6, 1, 2, 1, 1)
self.verticalLayout.addWidget(self.groupBox_5)
self.groupBox_4 = QtWidgets.QGroupBox(Dialog)
self.groupBox_4.setObjectName("groupBox_4")
self.gridLayout_3 = QtWidgets.QGridLayout(self.groupBox_4)
self.gridLayout_3.setObjectName("gridLayout_3")
self.pb_change_server_info = QtWidgets.QPushButton(self.groupBox_4)
self.pb_change_server_info.setObjectName("pb_change_server_info")
self.gridLayout_3.addWidget(self.pb_change_server_info, 0, 0, 1, 1)
spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.gridLayout_3.addItem(spacerItem3, 0, 1, 1, 1)
self.verticalLayout.addWidget(self.groupBox_4)
self.buttonBox = QtWidgets.QDialogButtonBox(Dialog)
self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Apply|QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Ok)
self.buttonBox.setObjectName("buttonBox")
self.verticalLayout.addWidget(self.buttonBox)
self.retranslateUi(Dialog)
self.buttonBox.accepted.connect(Dialog.accept)
self.buttonBox.rejected.connect(Dialog.reject)
QtCore.QMetaObject.connectSlotsByName(Dialog)
Dialog.setTabOrder(self.pb_font_message_title, self.pb_font_message_date)
Dialog.setTabOrder(self.pb_font_message_date, self.pb_font_message_content)
Dialog.setTabOrder(self.pb_font_message_content, self.cb_icons_application)
Dialog.setTabOrder(self.cb_icons_application, self.cb_icons_message)
Dialog.setTabOrder(self.cb_icons_message, self.cb_icons_notification)
Dialog.setTabOrder(self.cb_icons_notification, self.spin_priority)
Dialog.setTabOrder(self.spin_priority, self.spin_duration)
Dialog.setTabOrder(self.spin_duration, self.pb_change_server_info)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
self.groupBox.setTitle(_translate("Dialog", "Fonts"))
self.groupBox_2.setTitle(_translate("Dialog", "Message"))
self.label_3.setText(_translate("Dialog", "Content"))
self.label_2.setText(_translate("Dialog", "Date"))
self.pb_font_message_content.setText(_translate("Dialog", "..."))
self.pb_font_message_date.setText(_translate("Dialog", "..."))
self.label.setText(_translate("Dialog", "Title"))
self.label_font_message_title.setText(_translate("Dialog", "TextLabel"))
self.pb_font_message_title.setText(_translate("Dialog", "..."))
self.label_font_message_date.setText(_translate("Dialog", "TextLabel"))
self.label_font_message_content.setText(_translate("Dialog", "TextLabel"))
self.groupBox_6.setTitle(_translate("Dialog", "Theme"))
self.groupBox_3.setTitle(_translate("Dialog", "Icons"))
self.cb_icons_application.setText(_translate("Dialog", "Show application icons"))
self.cb_icons_message.setText(_translate("Dialog", "Show message icons"))
self.cb_icons_notification.setText(_translate("Dialog", "Show notification icons"))
self.groupBox_5.setTitle(_translate("Dialog", "Notifications"))
self.label_4.setText(_translate("Dialog", "Minimum priority to show notifications:"))
self.label_5.setText(_translate("Dialog", "Notification duration"))
self.label_6.setText(_translate("Dialog", "ms"))
self.groupBox_4.setTitle(_translate("Dialog", "Server info"))
self.pb_change_server_info.setText(_translate("Dialog", "Change server info"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Dialog = QtWidgets.QDialog()
ui = Ui_Dialog()
ui.setupUi(Dialog)
Dialog.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,340 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>375</width>
<height>540</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Fonts</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Message</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Content</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Date</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="pb_font_message_content">
<property name="maximumSize">
<size>
<width>30</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="pb_font_message_date">
<property name="maximumSize">
<size>
<width>30</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_font_message_title">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="pb_font_message_title">
<property name="maximumSize">
<size>
<width>30</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="0" column="3">
<spacer name="horizontalSpacer">
<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 row="1" column="2">
<widget class="QLabel" name="label_font_message_date">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel" name="label_font_message_content">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_6">
<property name="title">
<string>Theme</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QComboBox" name="combo_theme"/>
</item>
<item row="0" column="1">
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Icons</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="cb_icons_application">
<property name="text">
<string>Show application icons</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cb_icons_message">
<property name="text">
<string>Show message icons</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cb_icons_notification">
<property name="text">
<string>Show notification icons</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Notifications</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="1">
<widget class="QSpinBox" name="spin_priority">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>10</number>
</property>
<property name="value">
<number>5</number>
</property>
</widget>
</item>
<item row="0" column="2">
<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 row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Minimum priority to show notifications:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="spin_duration">
<property name="minimum">
<number>500</number>
</property>
<property name="maximum">
<number>30000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Notification duration</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="label_6">
<property name="text">
<string>ms</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Server info</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QPushButton" name="pb_change_server_info">
<property name="text">
<string>Change server info</string>
</property>
</widget>
</item>
<item row="0" column="1">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>pb_font_message_title</tabstop>
<tabstop>pb_font_message_date</tabstop>
<tabstop>pb_font_message_content</tabstop>
<tabstop>cb_icons_application</tabstop>
<tabstop>cb_icons_message</tabstop>
<tabstop>cb_icons_notification</tabstop>
<tabstop>spin_priority</tabstop>
<tabstop>spin_duration</tabstop>
<tabstop>pb_change_server_info</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>

After

Width:  |  Height:  |  Size: 155 B

View File

@@ -0,0 +1,12 @@
from PyQt6 import QtWidgets
def set_theme(app: QtWidgets.QApplication, theme: str = "default"):
if theme == "default":
from . import default
app.setPalette(default.palette())
elif theme == "dark":
from . import dark
app.setPalette(dark.palette())

View File

@@ -0,0 +1,19 @@
from PyQt6 import QtCore, QtGui
def palette() -> QtGui.QPalette:
palette = QtGui.QPalette()
palette.setColor(QtGui.QPalette.ColorRole.Window, QtGui.QColor(44, 44, 44))
palette.setColor(QtGui.QPalette.ColorRole.WindowText, QtCore.Qt.GlobalColor.white)
palette.setColor(QtGui.QPalette.ColorRole.Base, QtGui.QColor(52, 52, 52))
palette.setColor(QtGui.QPalette.ColorRole.AlternateBase, QtGui.QColor(44, 44, 44))
palette.setColor(QtGui.QPalette.ColorRole.ToolTipBase, QtCore.Qt.GlobalColor.white)
palette.setColor(QtGui.QPalette.ColorRole.ToolTipText, QtCore.Qt.GlobalColor.white)
palette.setColor(QtGui.QPalette.ColorRole.Text, QtCore.Qt.GlobalColor.white)
palette.setColor(QtGui.QPalette.ColorRole.Button, QtGui.QColor(44, 44, 44))
palette.setColor(QtGui.QPalette.ColorRole.ButtonText, QtCore.Qt.GlobalColor.white)
palette.setColor(QtGui.QPalette.ColorRole.BrightText, QtCore.Qt.GlobalColor.red)
palette.setColor(QtGui.QPalette.ColorRole.Link, QtGui.QColor(198, 99, 255))
palette.setColor(QtGui.QPalette.ColorRole.Highlight, QtGui.QColor(198, 99, 255))
palette.setColor(QtGui.QPalette.ColorRole.HighlightedText, QtCore.Qt.GlobalColor.black)
return palette

View File

@@ -0,0 +1,6 @@
from PyQt6 import QtGui
def palette() -> QtGui.QPalette:
palette = QtGui.QPalette()
return palette

148
gotify_tray/tasks.py Normal file
View File

@@ -0,0 +1,148 @@
import abc
import logging
from PyQt6 import QtCore
from PyQt6.QtCore import pyqtSignal
from . import gotify
logger = logging.getLogger("logger")
class BaseTask(QtCore.QThread):
failed = pyqtSignal()
def __init__(self):
super(BaseTask, self).__init__()
self.running = False
@abc.abstractmethod
def task(self):
...
def run(self):
self.running = True
try:
self.task()
except Exception as e:
logger.error(f"{self.__class__.__name__} failed: {e}")
self.failed.emit()
finally:
self.running = False
class DeleteMessageTask(BaseTask):
deleted = pyqtSignal(bool)
def __init__(self, message_id: int, gotify_client: gotify.GotifyClient):
super(DeleteMessageTask, self).__init__()
self.message_id = message_id
self.gotify_client = gotify_client
def task(self):
success = self.gotify_client.delete_message(self.message_id)
self.deleted.emit(success)
class DeleteApplicationMessagesTask(BaseTask):
deleted = pyqtSignal(bool)
def __init__(self, appid: int, gotify_client: gotify.GotifyClient):
super(DeleteApplicationMessagesTask, self).__init__()
self.appid = appid
self.gotify_client = gotify_client
def task(self):
success = self.gotify_client.delete_application_messages(self.appid)
self.deleted.emit(success)
class DeleteAllMessagesTask(BaseTask):
deleted = pyqtSignal(bool)
def __init__(self, gotify_client: gotify.GotifyClient):
super(DeleteAllMessagesTask, self).__init__()
self.gotify_client = gotify_client
def task(self):
success = self.gotify_client.delete_messages()
self.deleted.emit(success)
class GetApplicationsTask(BaseTask):
success = pyqtSignal(list)
error = pyqtSignal(gotify.GotifyErrorModel)
def __init__(self, gotify_client: gotify.GotifyClient):
super(GetApplicationsTask, self).__init__()
self.gotify_client = gotify_client
def task(self):
result = self.gotify_client.get_applications()
if isinstance(result, gotify.GotifyErrorModel):
self.error.emit(result)
else:
self.success.emit(result)
class GetApplicationMessagesTask(BaseTask):
success = pyqtSignal(gotify.GotifyPagedMessagesModel)
error = pyqtSignal(gotify.GotifyErrorModel)
def __init__(self, appid: int, gotify_client: gotify.GotifyClient):
super(GetApplicationMessagesTask, self).__init__()
self.appid = appid
self.gotify_client = gotify_client
def task(self):
result = self.gotify_client.get_application_messages(self.appid)
if isinstance(result, gotify.GotifyErrorModel):
self.error.emit(result)
else:
self.success.emit(result)
class GetMessagesTask(BaseTask):
success = pyqtSignal(gotify.GotifyPagedMessagesModel)
error = pyqtSignal(gotify.GotifyErrorModel)
def __init__(self, gotify_client: gotify.GotifyClient):
super(GetMessagesTask, self).__init__()
self.gotify_client = gotify_client
def task(self):
result = self.gotify_client.get_messages()
if isinstance(result, gotify.GotifyErrorModel):
self.error.emit(result)
else:
self.success.emit(result)
class VerifyServerInfoTask(BaseTask):
success = pyqtSignal()
incorrect_token = pyqtSignal()
incorrect_url = pyqtSignal()
def __init__(self, url: str, client_token: str):
super(VerifyServerInfoTask, self).__init__()
self.url = url
self.client_token = client_token
def task(self):
try:
gotify_client = gotify.GotifyClient(self.url, self.client_token)
result = gotify_client.get_messages(limit=1)
if isinstance(result, gotify.GotifyPagedMessagesModel):
self.success.emit()
return
elif (
isinstance(result, gotify.GotifyErrorModel)
and result["error"] == "Unauthorized"
):
self.incorrect_token.emit()
return
self.incorrect_url.emit()
except Exception as e:
self.incorrect_url.emit()

19
gotify_tray/utils.py Normal file
View File

@@ -0,0 +1,19 @@
def verify_server(force_new: bool = False) -> bool:
from gotify_tray.gui import ServerInfoDialog
from gotify_tray.database import Settings
settings = Settings("gotify-tray")
url = settings.value("Server/url", type=str)
token = settings.value("Server/client_token", type=str)
if not url or not token or force_new:
dialog = ServerInfoDialog(url, token)
if dialog.exec():
settings.setValue("Server/url", dialog.line_url.text())
settings.setValue("Server/client_token", dialog.line_token.text())
return True
else:
return False
else:
return True