a better main window
This commit is contained in:
@@ -4,4 +4,13 @@ DEFAULT_SETTINGS = {
|
||||
"tray/notifications/duration_ms": 5000,
|
||||
"tray/notifications/icon/show": True,
|
||||
"watchdog/interval/s": 60,
|
||||
"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",
|
||||
"ApplicationItem/font": "Noto Sans,10,-1,5,50,0,0,0,0,0,Regular",
|
||||
"MainWindow/font/application": "Noto Sans,13,-1,5,75,0,0,0,0,0,Bold",
|
||||
"MainWindow/label/size": 25,
|
||||
"MainWindow/button/size": 33,
|
||||
"MainWindow/application/icon/size": 40,
|
||||
}
|
||||
|
||||
@@ -3,23 +3,35 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import List
|
||||
from typing import List, Union
|
||||
|
||||
from gotify_tray import gotify
|
||||
from gotify_tray.__version__ import __title__
|
||||
from gotify_tray.database import Downloader, Settings
|
||||
from gotify_tray.tasks import GetApplicationsTask, ServerConnectionWatchdogTask
|
||||
from gotify_tray.tasks import (
|
||||
DeleteApplicationMessagesTask,
|
||||
DeleteAllMessagesTask,
|
||||
DeleteMessageTask,
|
||||
GetApplicationsTask,
|
||||
GetApplicationMessagesTask,
|
||||
GetMessagesTask,
|
||||
ServerConnectionWatchdogTask,
|
||||
)
|
||||
from gotify_tray.utils import verify_server
|
||||
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from ..__version__ import __title__
|
||||
from .ApplicationModel import (
|
||||
from .models import (
|
||||
ApplicationAllMessagesItem,
|
||||
ApplicationItemDataRole,
|
||||
ApplicationModel,
|
||||
ApplicationModelItem,
|
||||
MessagesModel,
|
||||
MessagesModelItem,
|
||||
MessageItemDataRole,
|
||||
)
|
||||
from .SettingsDialog import SettingsDialog
|
||||
from .Tray import Tray
|
||||
from .widgets import MainWindow, SettingsDialog, Tray
|
||||
|
||||
|
||||
settings = Settings("gotify-tray")
|
||||
logger = logging.getLogger("gotify-tray")
|
||||
@@ -58,7 +70,11 @@ class MainApplication(QtWidgets.QApplication):
|
||||
|
||||
self.downloader = Downloader()
|
||||
|
||||
self.messages_model = MessagesModel()
|
||||
self.application_model = ApplicationModel()
|
||||
|
||||
self.main_window = MainWindow(self.application_model, self.messages_model)
|
||||
|
||||
self.refresh_applications()
|
||||
|
||||
self.tray = Tray()
|
||||
@@ -77,44 +93,144 @@ class MainApplication(QtWidgets.QApplication):
|
||||
self.watchdog.start()
|
||||
|
||||
def refresh_applications(self):
|
||||
self.messages_model.clear()
|
||||
self.application_model.clear()
|
||||
|
||||
def get_applications_callback(
|
||||
applications: List[gotify.GotifyApplicationModel],
|
||||
):
|
||||
for i, application in enumerate(applications):
|
||||
icon = QtGui.QIcon(
|
||||
self.downloader.get_filename(
|
||||
f"{self.gotify_client.url}/{application.image}"
|
||||
)
|
||||
)
|
||||
self.application_model.setItem(
|
||||
i, 0, ApplicationModelItem(application, icon),
|
||||
)
|
||||
self.application_model.setItem(0, 0, ApplicationAllMessagesItem())
|
||||
|
||||
self.get_applications_task = GetApplicationsTask(self.gotify_client)
|
||||
self.get_applications_task.success.connect(get_applications_callback)
|
||||
self.get_applications_task.success.connect(
|
||||
self.get_applications_success_callback
|
||||
)
|
||||
self.get_applications_task.started.connect(
|
||||
self.main_window.disable_applications
|
||||
)
|
||||
self.get_applications_task.finished.connect(
|
||||
self.main_window.enable_applications
|
||||
)
|
||||
self.get_applications_task.start()
|
||||
|
||||
def get_applications_success_callback(
|
||||
self, applications: List[gotify.GotifyApplicationModel],
|
||||
):
|
||||
for i, application in enumerate(applications):
|
||||
icon = QtGui.QIcon(
|
||||
self.downloader.get_filename(
|
||||
f"{self.gotify_client.url}/{application.image}"
|
||||
)
|
||||
)
|
||||
self.application_model.setItem(
|
||||
i + 1, 0, ApplicationModelItem(application, icon),
|
||||
)
|
||||
|
||||
def listener_opened_callback(self):
|
||||
self.main_window.set_active()
|
||||
self.tray.set_icon_ok()
|
||||
# self.tray.actionReconnect.setEnabled(True)
|
||||
|
||||
def listener_closed_callback(self, close_status_code: int, close_msg: str):
|
||||
self.main_window.set_connecting()
|
||||
self.tray.set_icon_error()
|
||||
if not self.shutting_down:
|
||||
self.gotify_client.reconnect()
|
||||
|
||||
def refresh_callback(self):
|
||||
# self.tray.actionReconnect.setDisabled(True)
|
||||
def reconnect_callback(self):
|
||||
if not self.gotify_client.is_listening():
|
||||
self.gotify_client.listener.reset_wait_time()
|
||||
else:
|
||||
self.gotify_client.stop(reset_wait=True)
|
||||
self.gotify_client.reconnect(increase_wait_time=False)
|
||||
|
||||
def insert_message(
|
||||
self,
|
||||
row: int,
|
||||
message: gotify.GotifyMessageModel,
|
||||
application: gotify.GotifyApplicationModel,
|
||||
):
|
||||
message_item = MessagesModelItem(message)
|
||||
self.messages_model.insertRow(row, message_item)
|
||||
self.main_window.insert_message_widget(
|
||||
message_item,
|
||||
self.downloader.get_filename(
|
||||
f"{self.gotify_client.url}/{application.image}"
|
||||
),
|
||||
)
|
||||
|
||||
def application_selection_changed_callback(
|
||||
self, item: Union[ApplicationModelItem, ApplicationAllMessagesItem]
|
||||
):
|
||||
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.data(ApplicationItemDataRole.ApplicationRole),
|
||||
)
|
||||
|
||||
self.get_application_messages_task = GetApplicationMessagesTask(
|
||||
item.data(ApplicationItemDataRole.ApplicationRole).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.data(ApplicationItemDataRole.ApplicationRole),
|
||||
)
|
||||
|
||||
self.get_messages_task = GetMessagesTask(self.gotify_client)
|
||||
self.get_messages_task.success.connect(get_messages_callback)
|
||||
self.get_messages_task.start()
|
||||
|
||||
def add_message_to_model(self, message: gotify.GotifyMessageModel):
|
||||
if application_item := self.application_model.itemFromId(message.appid):
|
||||
application_index = self.main_window.currentApplicationIndex()
|
||||
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.data(
|
||||
ApplicationItemDataRole.ApplicationRole
|
||||
).id
|
||||
):
|
||||
self.insert_message(
|
||||
0,
|
||||
message,
|
||||
application_item.data(
|
||||
ApplicationItemDataRole.ApplicationRole
|
||||
),
|
||||
)
|
||||
elif isinstance(selected_application_item, ApplicationAllMessagesItem):
|
||||
# "All messages' is selected
|
||||
self.insert_message(
|
||||
0,
|
||||
message,
|
||||
application_item.data(ApplicationItemDataRole.ApplicationRole),
|
||||
)
|
||||
|
||||
def new_message_callback(self, message: gotify.GotifyMessageModel):
|
||||
if message.priority < settings.value("tray/notifications/priority", type=int):
|
||||
self.add_message_to_model(message)
|
||||
|
||||
# Show a notification
|
||||
if (
|
||||
message.priority < settings.value("tray/notifications/priority", type=int)
|
||||
or self.main_window.isActiveWindow()
|
||||
):
|
||||
return
|
||||
|
||||
if settings.value("tray/notifications/icon/show", type=bool):
|
||||
@@ -130,7 +246,6 @@ class MainApplication(QtWidgets.QApplication):
|
||||
else:
|
||||
icon = QtWidgets.QSystemTrayIcon.MessageIcon.Information
|
||||
|
||||
# Show a notification
|
||||
self.tray.showMessage(
|
||||
message.title,
|
||||
message.message,
|
||||
@@ -138,6 +253,30 @@ class MainApplication(QtWidgets.QApplication):
|
||||
msecs=settings.value("tray/notifications/duration_ms", type=int),
|
||||
)
|
||||
|
||||
def delete_message_callback(self, message_item: MessagesModelItem):
|
||||
self.delete_message_task = DeleteMessageTask(
|
||||
message_item.data(MessageItemDataRole.MessageRole).id, self.gotify_client
|
||||
)
|
||||
self.messages_model.removeRow(message_item.row())
|
||||
self.delete_message_task.start()
|
||||
|
||||
def delete_all_messages_callback(
|
||||
self, item: Union[ApplicationModelItem, ApplicationAllMessagesItem]
|
||||
):
|
||||
if isinstance(item, ApplicationModelItem):
|
||||
self.delete_application_messages_task = DeleteApplicationMessagesTask(
|
||||
item.data(ApplicationItemDataRole.ApplicationRole).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()
|
||||
else:
|
||||
return
|
||||
|
||||
self.messages_model.clear()
|
||||
|
||||
def settings_callback(self):
|
||||
settings_dialog = SettingsDialog()
|
||||
accepted = settings_dialog.exec()
|
||||
@@ -158,10 +297,26 @@ class MainApplication(QtWidgets.QApplication):
|
||||
if r == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
self.quit()
|
||||
|
||||
def tray_activated_callback(
|
||||
self, reason: QtWidgets.QSystemTrayIcon.ActivationReason
|
||||
):
|
||||
if reason == QtWidgets.QSystemTrayIcon.ActivationReason.Trigger:
|
||||
self.main_window.bring_to_front()
|
||||
|
||||
def link_callbacks(self):
|
||||
self.tray.actionQuit.triggered.connect(self.quit)
|
||||
self.tray.actionSettings.triggered.connect(self.settings_callback)
|
||||
self.tray.actionReconnect.triggered.connect(self.refresh_callback)
|
||||
self.tray.actionShowWindow.triggered.connect(self.main_window.bring_to_front)
|
||||
self.tray.actionReconnect.triggered.connect(self.reconnect_callback)
|
||||
self.tray.messageClicked.connect(self.main_window.bring_to_front)
|
||||
self.tray.activated.connect(self.tray_activated_callback)
|
||||
|
||||
self.main_window.refresh.connect(self.refresh_applications)
|
||||
self.main_window.delete_all.connect(self.delete_all_messages_callback)
|
||||
self.main_window.application_selection_changed.connect(
|
||||
self.application_selection_changed_callback
|
||||
)
|
||||
self.main_window.delete_message.connect(self.delete_message_callback)
|
||||
|
||||
self.watchdog.closed.connect(lambda: self.listener_closed_callback(None, None))
|
||||
|
||||
@@ -175,6 +330,8 @@ class MainApplication(QtWidgets.QApplication):
|
||||
return self.lock_file.tryLock()
|
||||
|
||||
def quit(self) -> None:
|
||||
self.main_window.store_state()
|
||||
|
||||
self.tray.hide()
|
||||
|
||||
self.lock_file.unlock()
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
from .MainApplication import start_gui
|
||||
from .ServerInfoDialog import ServerInfoDialog
|
||||
from .widgets import ServerInfoDialog
|
||||
|
||||
77
gotify_tray/gui/designs/widget_main.py
Normal file
77
gotify_tray/gui/designs/widget_main.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_main.ui'
|
||||
#
|
||||
# Created by: PyQt6 UI code generator 6.1.0
|
||||
#
|
||||
# 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_MainWindow(object):
|
||||
def setupUi(self, MainWindow):
|
||||
MainWindow.setObjectName("MainWindow")
|
||||
MainWindow.resize(809, 647)
|
||||
self.centralwidget = QtWidgets.QWidget(MainWindow)
|
||||
self.centralwidget.setObjectName("centralwidget")
|
||||
self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
|
||||
self.gridLayout.setContentsMargins(4, 4, 4, 4)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.splitter = QtWidgets.QSplitter(self.centralwidget)
|
||||
self.splitter.setOrientation(QtCore.Qt.Orientation.Horizontal)
|
||||
self.splitter.setObjectName("splitter")
|
||||
self.listView_applications = QtWidgets.QListView(self.splitter)
|
||||
self.listView_applications.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
self.listView_applications.setObjectName("listView_applications")
|
||||
self.verticalLayoutWidget = QtWidgets.QWidget(self.splitter)
|
||||
self.verticalLayoutWidget.setObjectName("verticalLayoutWidget")
|
||||
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.verticalLayoutWidget)
|
||||
self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
|
||||
self.verticalLayout_2.setObjectName("verticalLayout_2")
|
||||
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
|
||||
self.horizontalLayout.addItem(spacerItem)
|
||||
self.label_application = QtWidgets.QLabel(self.verticalLayoutWidget)
|
||||
self.label_application.setObjectName("label_application")
|
||||
self.horizontalLayout.addWidget(self.label_application)
|
||||
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
|
||||
self.horizontalLayout.addItem(spacerItem1)
|
||||
self.pb_refresh = QtWidgets.QPushButton(self.verticalLayoutWidget)
|
||||
self.pb_refresh.setText("")
|
||||
self.pb_refresh.setObjectName("pb_refresh")
|
||||
self.horizontalLayout.addWidget(self.pb_refresh)
|
||||
self.pb_delete_all = QtWidgets.QPushButton(self.verticalLayoutWidget)
|
||||
self.pb_delete_all.setText("")
|
||||
self.pb_delete_all.setObjectName("pb_delete_all")
|
||||
self.horizontalLayout.addWidget(self.pb_delete_all)
|
||||
self.verticalLayout_2.addLayout(self.horizontalLayout)
|
||||
self.listView_messages = QtWidgets.QListView(self.verticalLayoutWidget)
|
||||
self.listView_messages.setAutoScroll(True)
|
||||
self.listView_messages.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
self.listView_messages.setObjectName("listView_messages")
|
||||
self.verticalLayout_2.addWidget(self.listView_messages)
|
||||
self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1)
|
||||
MainWindow.setCentralWidget(self.centralwidget)
|
||||
|
||||
self.retranslateUi(MainWindow)
|
||||
QtCore.QMetaObject.connectSlotsByName(MainWindow)
|
||||
MainWindow.setTabOrder(self.listView_applications, self.listView_messages)
|
||||
MainWindow.setTabOrder(self.listView_messages, self.pb_refresh)
|
||||
MainWindow.setTabOrder(self.pb_refresh, self.pb_delete_all)
|
||||
|
||||
def retranslateUi(self, MainWindow):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
MainWindow.setWindowTitle(_translate("MainWindow", "Form"))
|
||||
self.label_application.setText(_translate("MainWindow", "Application"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
MainWindow = QtWidgets.QMainWindow()
|
||||
ui = Ui_MainWindow()
|
||||
ui.setupUi(MainWindow)
|
||||
MainWindow.show()
|
||||
sys.exit(app.exec())
|
||||
118
gotify_tray/gui/designs/widget_main.ui
Normal file
118
gotify_tray/gui/designs/widget_main.ui
Normal file
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>809</width>
|
||||
<height>647</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="leftMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QListView" name="listView_applications">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QWidget" name="verticalLayoutWidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<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>
|
||||
<widget class="QLabel" name="label_application">
|
||||
<property name="text">
|
||||
<string>Application</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<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>
|
||||
<widget class="QPushButton" name="pb_refresh">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pb_delete_all">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListView" name="listView_messages">
|
||||
<property name="autoScroll">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>listView_applications</tabstop>
|
||||
<tabstop>listView_messages</tabstop>
|
||||
<tabstop>pb_refresh</tabstop>
|
||||
<tabstop>pb_delete_all</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
94
gotify_tray/gui/designs/widget_message.py
Normal file
94
gotify_tray/gui/designs/widget_message.py
Normal 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.0
|
||||
#
|
||||
# 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())
|
||||
152
gotify_tray/gui/designs/widget_message.ui
Normal file
152
gotify_tray/gui/designs/widget_message.ui
Normal 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>
|
||||
@@ -1,6 +1,6 @@
|
||||
# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_server.ui'
|
||||
#
|
||||
# Created by: PyQt6 UI code generator 6.2.1
|
||||
# Created by: PyQt6 UI code generator 6.1.0
|
||||
#
|
||||
# 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.
|
||||
@@ -46,8 +46,8 @@ class Ui_Dialog(object):
|
||||
self.gridLayout.addWidget(self.label_server_info, 1, 1, 1, 2)
|
||||
|
||||
self.retranslateUi(Dialog)
|
||||
self.buttonBox.accepted.connect(Dialog.accept) # type: ignore
|
||||
self.buttonBox.rejected.connect(Dialog.reject) # type: ignore
|
||||
self.buttonBox.accepted.connect(Dialog.accept)
|
||||
self.buttonBox.rejected.connect(Dialog.reject)
|
||||
QtCore.QMetaObject.connectSlotsByName(Dialog)
|
||||
|
||||
def retranslateUi(self, Dialog):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_settings.ui'
|
||||
#
|
||||
# Created by: PyQt6 UI code generator 6.2.1
|
||||
# Created by: PyQt6 UI code generator 6.1.0
|
||||
#
|
||||
# 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.
|
||||
@@ -80,8 +80,8 @@ class Ui_Dialog(object):
|
||||
self.verticalLayout.addWidget(self.buttonBox)
|
||||
|
||||
self.retranslateUi(Dialog)
|
||||
self.buttonBox.accepted.connect(Dialog.accept) # type: ignore
|
||||
self.buttonBox.rejected.connect(Dialog.reject) # type: ignore
|
||||
self.buttonBox.accepted.connect(Dialog.accept)
|
||||
self.buttonBox.rejected.connect(Dialog.reject)
|
||||
QtCore.QMetaObject.connectSlotsByName(Dialog)
|
||||
Dialog.setTabOrder(self.cb_icons_notification, self.spin_priority)
|
||||
Dialog.setTabOrder(self.spin_priority, self.spin_duration)
|
||||
|
||||
45
gotify_tray/gui/images/refresh.svg
Normal file
45
gotify_tray/gui/images/refresh.svg
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 492.883 492.883" style="enable-background:new 0 0 492.883 492.883;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M122.941,374.241c-20.1-18.1-34.6-39.8-44.1-63.1c-25.2-61.8-13.4-135.3,35.8-186l45.4,45.4c2.5,2.5,7,0.7,7.6-3
|
||||
l24.8-162.3c0.4-2.7-1.9-5-4.6-4.6l-162.4,24.8c-3.7,0.6-5.5,5.1-3,7.6l45.5,45.5c-75.1,76.8-87.9,192-38.6,282
|
||||
c14.8,27.1,35.3,51.9,61.4,72.7c44.4,35.3,99,52.2,153.2,51.1l10.2-66.7C207.441,421.641,159.441,407.241,122.941,374.241z"/>
|
||||
<path d="M424.941,414.341c75.1-76.8,87.9-192,38.6-282c-14.8-27.1-35.3-51.9-61.4-72.7c-44.4-35.3-99-52.2-153.2-51.1l-10.2,66.7
|
||||
c46.6-4,94.7,10.4,131.2,43.4c20.1,18.1,34.6,39.8,44.1,63.1c25.2,61.8,13.4,135.3-35.8,186l-45.4-45.4c-2.5-2.5-7-0.7-7.6,3
|
||||
l-24.8,162.3c-0.4,2.7,1.9,5,4.6,4.6l162.4-24.8c3.7-0.6,5.4-5.1,3-7.6L424.941,414.341z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
49
gotify_tray/gui/images/status_active.svg
Normal file
49
gotify_tray/gui/images/status_active.svg
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="29.348576mm"
|
||||
height="29.348576mm"
|
||||
viewBox="0 0 29.348576 29.348576"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
sodipodi:docname="status_ok.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.77771465"
|
||||
inkscape:cx="188.37243"
|
||||
inkscape:cy="505.96964"
|
||||
inkscape:window-width="1284"
|
||||
inkscape:window-height="1082"
|
||||
inkscape:window-x="1622"
|
||||
inkscape:window-y="248"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-24.619528,-14.753546)">
|
||||
<circle
|
||||
style="fill:#00db00;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path846"
|
||||
cx="39.293816"
|
||||
cy="29.427834"
|
||||
r="14.674288" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
49
gotify_tray/gui/images/status_connecting.svg
Normal file
49
gotify_tray/gui/images/status_connecting.svg
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="29.348576mm"
|
||||
height="29.348576mm"
|
||||
viewBox="0 0 29.348576 29.348576"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
sodipodi:docname="status_connecting.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.77771465"
|
||||
inkscape:cx="188.37243"
|
||||
inkscape:cy="505.96964"
|
||||
inkscape:window-width="1284"
|
||||
inkscape:window-height="1082"
|
||||
inkscape:window-x="1852"
|
||||
inkscape:window-y="271"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-24.619528,-14.753546)">
|
||||
<circle
|
||||
style="fill:#ffb200;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path846"
|
||||
cx="39.293816"
|
||||
cy="29.427834"
|
||||
r="14.674288" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
49
gotify_tray/gui/images/status_error.svg
Normal file
49
gotify_tray/gui/images/status_error.svg
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="29.348576mm"
|
||||
height="29.348576mm"
|
||||
viewBox="0 0 29.348576 29.348576"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
sodipodi:docname="status_error.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.77771465"
|
||||
inkscape:cx="188.37243"
|
||||
inkscape:cy="505.96964"
|
||||
inkscape:window-width="1284"
|
||||
inkscape:window-height="1082"
|
||||
inkscape:window-x="2013"
|
||||
inkscape:window-y="263"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-24.619528,-14.753546)">
|
||||
<circle
|
||||
style="fill:#ff3232;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path846"
|
||||
cx="39.293816"
|
||||
cy="29.427834"
|
||||
r="14.674288" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
49
gotify_tray/gui/images/status_inactive.svg
Normal file
49
gotify_tray/gui/images/status_inactive.svg
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="29.348576mm"
|
||||
height="29.348576mm"
|
||||
viewBox="0 0 29.348576 29.348576"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
sodipodi:docname="status_inactive.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.77771465"
|
||||
inkscape:cx="188.37243"
|
||||
inkscape:cy="505.96964"
|
||||
inkscape:window-width="1284"
|
||||
inkscape:window-height="1082"
|
||||
inkscape:window-x="1852"
|
||||
inkscape:window-y="271"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-24.619528,-14.753546)">
|
||||
<circle
|
||||
style="fill:#b4b4b4;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path846"
|
||||
cx="39.293816"
|
||||
cy="29.427834"
|
||||
r="14.674288" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
gotify_tray/gui/images/trashcan.svg
Normal file
1
gotify_tray/gui/images/trashcan.svg
Normal 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 |
@@ -1,6 +1,6 @@
|
||||
import enum
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
from PyQt6 import QtCore, QtGui
|
||||
from gotify_tray import gotify
|
||||
from gotify_tray.database import Settings
|
||||
@@ -20,12 +20,15 @@ class ApplicationModelItem(QtGui.QStandardItem):
|
||||
application: gotify.GotifyApplicationModel,
|
||||
icon: Optional[QtGui.QIcon] = None,
|
||||
*args,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
):
|
||||
super(ApplicationModelItem, self).__init__(application.name)
|
||||
self.setDropEnabled(False)
|
||||
self.setData(application, ApplicationItemDataRole.ApplicationRole)
|
||||
self.setData(icon, ApplicationItemDataRole.IconRole)
|
||||
font = QtGui.QFont()
|
||||
font.fromString(settings.value("ApplicationItem/font", type=str))
|
||||
self.setFont(font)
|
||||
if icon:
|
||||
self.setIcon(icon)
|
||||
|
||||
@@ -36,6 +39,16 @@ class ApplicationModelItem(QtGui.QStandardItem):
|
||||
)
|
||||
|
||||
|
||||
class ApplicationAllMessagesItem(QtGui.QStandardItem):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ApplicationAllMessagesItem, self).__init__("ALL MESSAGES")
|
||||
self.setDropEnabled(False)
|
||||
self.setDragEnabled(False)
|
||||
font = QtGui.QFont()
|
||||
font.fromString(settings.value("ApplicationItem/font", type=str))
|
||||
self.setFont(font)
|
||||
|
||||
|
||||
class ApplicationModel(QtGui.QStandardItemModel):
|
||||
def __init__(self):
|
||||
super(ApplicationModel, self).__init__()
|
||||
@@ -43,10 +56,17 @@ class ApplicationModel(QtGui.QStandardItemModel):
|
||||
ApplicationModelItem(gotify.GotifyApplicationModel({"name": ""}), None)
|
||||
)
|
||||
|
||||
def setItem(self, row: int, column: int, item: ApplicationModelItem,) -> None:
|
||||
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) -> ApplicationModelItem:
|
||||
def itemFromIndex(
|
||||
self, index: QtCore.QModelIndex
|
||||
) -> Union[ApplicationModelItem, ApplicationAllMessagesItem]:
|
||||
return super(ApplicationModel, self).itemFromIndex(index)
|
||||
|
||||
def itemFromId(self, appid: int) -> Optional[ApplicationModelItem]:
|
||||
23
gotify_tray/gui/models/MessagesModel.py
Normal file
23
gotify_tray/gui/models/MessagesModel.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import enum
|
||||
|
||||
from typing import cast
|
||||
from PyQt6 import QtCore, QtGui
|
||||
from gotify_tray import gotify
|
||||
|
||||
|
||||
class MessageItemDataRole(enum.IntEnum):
|
||||
MessageRole = QtCore.Qt.ItemDataRole.UserRole + 1
|
||||
|
||||
|
||||
class MessagesModelItem(QtGui.QStandardItem):
|
||||
def __init__(self, message: gotify.GotifyMessageModel, *args, **kwargs):
|
||||
super(MessagesModelItem, self).__init__()
|
||||
self.setData(message, MessageItemDataRole.MessageRole)
|
||||
|
||||
|
||||
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))
|
||||
7
gotify_tray/gui/models/__init__.py
Normal file
7
gotify_tray/gui/models/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .ApplicationModel import (
|
||||
ApplicationAllMessagesItem,
|
||||
ApplicationModelItem,
|
||||
ApplicationModel,
|
||||
ApplicationItemDataRole,
|
||||
)
|
||||
from .MessagesModel import MessagesModelItem, MessagesModel, MessageItemDataRole
|
||||
154
gotify_tray/gui/widgets/MainWindow.py
Normal file
154
gotify_tray/gui/widgets/MainWindow.py
Normal file
@@ -0,0 +1,154 @@
|
||||
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||
from ..designs.widget_main import Ui_MainWindow
|
||||
from .StatusWidget import StatusWidget
|
||||
from ..models import (
|
||||
ApplicationModel,
|
||||
MessagesModel,
|
||||
MessagesModelItem,
|
||||
)
|
||||
from . import MessageWidget
|
||||
|
||||
from gotify_tray.__version__ import __title__
|
||||
from gotify_tray.database import Settings
|
||||
from gotify_tray.utils import get_abs_path
|
||||
|
||||
|
||||
settings = Settings("gotify-tray")
|
||||
|
||||
|
||||
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
refresh = QtCore.pyqtSignal()
|
||||
delete_all = QtCore.pyqtSignal(QtGui.QStandardItem)
|
||||
delete_message = QtCore.pyqtSignal(MessagesModelItem)
|
||||
application_selection_changed = QtCore.pyqtSignal(QtGui.QStandardItem)
|
||||
|
||||
def __init__(
|
||||
self, application_model: ApplicationModel, messages_model: MessagesModel
|
||||
):
|
||||
super(MainWindow, self).__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
self.setWindowTitle(__title__)
|
||||
|
||||
self.application_model = application_model
|
||||
self.messages_model = messages_model
|
||||
|
||||
self.listView_applications.setModel(application_model)
|
||||
self.listView_messages.setModel(messages_model)
|
||||
|
||||
# Do not expand the applications listview when resizing
|
||||
self.splitter.setStretchFactor(0, 0)
|
||||
self.splitter.setStretchFactor(1, 1)
|
||||
|
||||
self.status_widget = StatusWidget()
|
||||
self.horizontalLayout.insertWidget(0, self.status_widget)
|
||||
|
||||
# Set button icons
|
||||
self.pb_refresh.setIcon(
|
||||
QtGui.QIcon(get_abs_path(f"gotify_tray/gui/images/refresh.svg"))
|
||||
)
|
||||
self.pb_delete_all.setIcon(
|
||||
QtGui.QIcon(get_abs_path(f"gotify_tray/gui/images/trashcan.svg"))
|
||||
)
|
||||
|
||||
# Resize the labels and icons
|
||||
size = settings.value("MainWindow/label/size", type=int)
|
||||
self.status_widget.setFixedSize(QtCore.QSize(size, size))
|
||||
|
||||
size = settings.value("MainWindow/button/size", type=int)
|
||||
self.pb_refresh.setFixedSize(QtCore.QSize(size, size))
|
||||
self.pb_delete_all.setFixedSize(QtCore.QSize(size, size))
|
||||
self.pb_refresh.setIconSize(QtCore.QSize(0.7 * size, 0.7 * size))
|
||||
self.pb_delete_all.setIconSize(QtCore.QSize(0.9 * size, 0.9 * size))
|
||||
|
||||
size = settings.value("MainWindow/application/icon/size", type=int)
|
||||
self.listView_applications.setIconSize(QtCore.QSize(size, size))
|
||||
|
||||
font_title = QtGui.QFont()
|
||||
font_title.fromString(settings.value("MainWindow/font/application", type=str))
|
||||
self.label_application.setFont(font_title)
|
||||
|
||||
# Set tooltips
|
||||
self.pb_refresh.setToolTip("Refresh")
|
||||
self.pb_delete_all.setToolTip("Delete all messages")
|
||||
|
||||
self.restore_state()
|
||||
|
||||
self.link_callbacks()
|
||||
|
||||
def set_active(self):
|
||||
self.status_widget.set_active()
|
||||
|
||||
def set_connecting(self):
|
||||
self.status_widget.set_connecting()
|
||||
|
||||
def set_inactive(self):
|
||||
self.status_widget.set_inactive()
|
||||
|
||||
def set_error(self):
|
||||
self.status_widget.set_error()
|
||||
|
||||
def insert_message_widget(
|
||||
self, message_item: MessagesModelItem, image_path: str = ""
|
||||
):
|
||||
message_widget = MessageWidget(message_item, image_path=image_path)
|
||||
self.listView_messages.setIndexWidget(
|
||||
self.messages_model.indexFromItem(message_item), message_widget
|
||||
)
|
||||
message_widget.deletion_requested.connect(self.delete_message.emit)
|
||||
|
||||
def currentApplicationIndex(self) -> QtCore.QModelIndex:
|
||||
return self.listView_applications.selectionModel().currentIndex()
|
||||
|
||||
def application_selection_changed_callback(
|
||||
self, current: QtCore.QModelIndex, previous: QtCore.QModelIndex
|
||||
):
|
||||
if item := self.application_model.itemFromIndex(current):
|
||||
self.label_application.setText(item.text())
|
||||
self.application_selection_changed.emit(item)
|
||||
|
||||
def delete_all_callback(self):
|
||||
index = self.currentApplicationIndex()
|
||||
if item := self.application_model.itemFromIndex(index):
|
||||
self.delete_all.emit(item)
|
||||
|
||||
def disable_applications(self):
|
||||
self.listView_applications.clearSelection()
|
||||
self.listView_applications.setDisabled(True)
|
||||
|
||||
def enable_applications(self):
|
||||
self.listView_applications.setEnabled(True)
|
||||
self.listView_applications.setCurrentIndex(self.application_model.index(0, 0))
|
||||
|
||||
def bring_to_front(self):
|
||||
self.ensurePolished()
|
||||
self.setWindowState(
|
||||
self.windowState() & ~QtCore.Qt.WindowState.WindowMinimized
|
||||
| QtCore.Qt.WindowState.WindowActive
|
||||
)
|
||||
self.show()
|
||||
self.activateWindow()
|
||||
|
||||
def link_callbacks(self):
|
||||
self.pb_refresh.clicked.connect(self.refresh.emit)
|
||||
self.pb_delete_all.clicked.connect(self.delete_all_callback)
|
||||
|
||||
self.listView_applications.selectionModel().currentChanged.connect(
|
||||
self.application_selection_changed_callback
|
||||
)
|
||||
|
||||
def store_state(self):
|
||||
settings.setValue("MainWindow/geometry", self.saveGeometry())
|
||||
settings.setValue("MainWindow/state", self.saveState())
|
||||
settings.setValue("MainWindow/splitter", self.splitter.saveState())
|
||||
|
||||
def restore_state(self):
|
||||
if geometry := settings.value("MainWindow/geometry", type=QtCore.QByteArray):
|
||||
self.restoreGeometry(geometry)
|
||||
if state := settings.value("MainWindow/state", type=QtCore.QByteArray):
|
||||
self.restoreState(state)
|
||||
if splitter := settings.value("MainWindow/splitter", type=QtCore.QByteArray):
|
||||
self.splitter.restoreState(splitter)
|
||||
|
||||
def closeEvent(self, e: QtGui.QCloseEvent) -> None:
|
||||
self.hide()
|
||||
71
gotify_tray/gui/widgets/MessageWidget.py
Normal file
71
gotify_tray/gui/widgets/MessageWidget.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from ..models.MessagesModel import MessageItemDataRole, MessagesModelItem
|
||||
from ..designs.widget_message import Ui_Form
|
||||
from gotify_tray.database import Settings
|
||||
from gotify_tray.utils import convert_links
|
||||
|
||||
|
||||
settings = Settings("gotify-tray")
|
||||
|
||||
|
||||
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 = message_item.data(MessageItemDataRole.MessageRole)
|
||||
|
||||
# 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)
|
||||
|
||||
# Display message contents
|
||||
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))
|
||||
|
||||
# Show the application icon
|
||||
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))
|
||||
@@ -2,7 +2,7 @@ from gotify_tray.gotify.models import GotifyVersionModel
|
||||
from gotify_tray.tasks import VerifyServerInfoTask
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
from .designs.widget_server import Ui_Dialog
|
||||
from ..designs.widget_server import Ui_Dialog
|
||||
|
||||
|
||||
class ServerInfoDialog(QtWidgets.QDialog, Ui_Dialog):
|
||||
@@ -5,7 +5,7 @@ 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 ..designs.widget_settings import Ui_Dialog
|
||||
|
||||
|
||||
logger = logging.getLogger("gotify-tray")
|
||||
34
gotify_tray/gui/widgets/StatusWidget.py
Normal file
34
gotify_tray/gui/widgets/StatusWidget.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from gotify_tray.database import Settings
|
||||
from gotify_tray.utils import get_abs_path
|
||||
|
||||
|
||||
settings = Settings("gotify-tray")
|
||||
|
||||
|
||||
class StatusWidget(QtWidgets.QLabel):
|
||||
def __init__(self):
|
||||
super(StatusWidget, self).__init__()
|
||||
self.setFixedSize(QtCore.QSize(20, 20))
|
||||
self.setScaledContents(True)
|
||||
self.set_connecting()
|
||||
|
||||
def set_status(self, image: str):
|
||||
self.setPixmap(QtGui.QPixmap(get_abs_path(f"gotify_tray/gui/images/{image}")))
|
||||
|
||||
def set_active(self):
|
||||
self.setToolTip("Listening for new messages")
|
||||
self.set_status("status_active.svg")
|
||||
|
||||
def set_connecting(self):
|
||||
self.setToolTip("Connecting...")
|
||||
self.set_status("status_connecting.svg")
|
||||
|
||||
def set_inactive(self):
|
||||
self.setToolTip("Listener inactive")
|
||||
self.set_status("status_inactive.svg")
|
||||
|
||||
def set_error(self):
|
||||
self.setToolTip("Listener error")
|
||||
self.set_status("status_error.svg")
|
||||
@@ -27,6 +27,11 @@ class Tray(QtWidgets.QSystemTrayIcon):
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
self.actionShowWindow = QtGui.QAction("Show Window", self)
|
||||
menu.addAction(self.actionShowWindow)
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
self.actionReconnect = QtGui.QAction("Reconnect", self)
|
||||
menu.addAction(self.actionReconnect)
|
||||
|
||||
6
gotify_tray/gui/widgets/__init__.py
Normal file
6
gotify_tray/gui/widgets/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .MessageWidget import MessageWidget
|
||||
from .MainWindow import MainWindow
|
||||
from .ServerInfoDialog import ServerInfoDialog
|
||||
from .SettingsDialog import SettingsDialog
|
||||
from .StatusWidget import StatusWidget
|
||||
from .Tray import Tray
|
||||
@@ -38,6 +38,32 @@ class BaseTask(QtCore.QThread):
|
||||
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)
|
||||
|
||||
@@ -66,6 +92,39 @@ class GetApplicationsTask(BaseTask):
|
||||
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(GotifyVersionModel)
|
||||
incorrect_token = pyqtSignal(GotifyVersionModel)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def verify_server(force_new: bool = False) -> bool:
|
||||
from gotify_tray.gui import ServerInfoDialog
|
||||
from gotify_tray.database import Settings
|
||||
@@ -17,3 +23,26 @@ def verify_server(force_new: bool = False) -> bool:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def get_abs_path(s) -> str:
|
||||
h = Path(__file__).parent.parent
|
||||
p = Path(s)
|
||||
return os.path.join(h, p).replace("\\", "/")
|
||||
|
||||
Reference in New Issue
Block a user