diff --git a/gotify_tray/database/default_settings.py b/gotify_tray/database/default_settings.py index acc6935..0717eaa 100644 --- a/gotify_tray/database/default_settings.py +++ b/gotify_tray/database/default_settings.py @@ -1,7 +1,16 @@ +import os +from pathlib import Path + +from ..__version__ import __title__ + + DEFAULT_SETTINGS = { "message/check_missed/notify": True, "message/last_id": 0, "logging/level": "Disabled", + "settings/export_path": os.path.join( + Path.home(), f"{__title__.replace(' ', '-').lower()}-settings.bytes" + ), "shortcuts/quit": "Ctrl+Q", "tray/notifications/priority": 5, "tray/notifications/duration_ms": 5000, diff --git a/gotify_tray/database/settings.py b/gotify_tray/database/settings.py index a14e474..98eaa48 100644 --- a/gotify_tray/database/settings.py +++ b/gotify_tray/database/settings.py @@ -1,3 +1,4 @@ +import pickle from typing import Any from .default_settings import DEFAULT_SETTINGS @@ -8,6 +9,32 @@ 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) + 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)) + return super().value( + key, defaultValue=defaultValue or DEFAULT_SETTINGS.get(key) + ) + + def export(self, path: str): + data = { + key: self.value(key) + for key in self.allKeys() + if not ( # skip settings that might not translate well between platforms + isinstance(self.value(key), QtCore.QByteArray) + or key == "settings/export_path" + ) + } + + with open(path, "wb") as f: + pickle.dump(data, f) + + def load(self, path: str): + self.clear() + + with open(path, "rb") as f: + data = pickle.load(f) + + for key in data: + self.setValue(key, data[key]) diff --git a/gotify_tray/gui/MainApplication.py b/gotify_tray/gui/MainApplication.py index d2329a1..dffcf8b 100644 --- a/gotify_tray/gui/MainApplication.py +++ b/gotify_tray/gui/MainApplication.py @@ -315,6 +315,7 @@ class MainApplication(QtWidgets.QApplication): def settings_callback(self): settings_dialog = SettingsDialog() + settings_dialog.quit_requested.connect(self.quit) accepted = settings_dialog.exec() if accepted and settings_dialog.settings_changed: diff --git a/gotify_tray/gui/designs/widget_server.py b/gotify_tray/gui/designs/widget_server.py index faab5ea..d58e3b7 100644 --- a/gotify_tray/gui/designs/widget_server.py +++ b/gotify_tray/gui/designs/widget_server.py @@ -15,9 +15,10 @@ class Ui_Dialog(object): Dialog.resize(300, 130) self.gridLayout = QtWidgets.QGridLayout(Dialog) self.gridLayout.setObjectName("gridLayout") - self.pb_test = QtWidgets.QPushButton(Dialog) - self.pb_test.setObjectName("pb_test") - self.gridLayout.addWidget(self.pb_test, 1, 4, 1, 1) + self.label_server_info = QtWidgets.QLabel(Dialog) + self.label_server_info.setText("") + self.label_server_info.setObjectName("label_server_info") + self.gridLayout.addWidget(self.label_server_info, 1, 1, 1, 2) self.formLayout = QtWidgets.QFormLayout() self.formLayout.setObjectName("formLayout") self.label = QtWidgets.QLabel(Dialog) @@ -32,30 +33,35 @@ class Ui_Dialog(object): 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, 1, 1, 4) + self.gridLayout.addLayout(self.formLayout, 0, 1, 1, 5) + self.pb_test = QtWidgets.QPushButton(Dialog) + self.pb_test.setObjectName("pb_test") + self.gridLayout.addWidget(self.pb_test, 1, 5, 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, 4, 1, 1) + self.gridLayout.addWidget(self.buttonBox, 2, 5, 1, 1) spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) self.gridLayout.addItem(spacerItem, 1, 3, 1, 1) - self.label_server_info = QtWidgets.QLabel(Dialog) - self.label_server_info.setText("") - self.label_server_info.setObjectName("label_server_info") - self.gridLayout.addWidget(self.label_server_info, 1, 1, 1, 2) + self.pb_import = QtWidgets.QPushButton(Dialog) + self.pb_import.setMaximumSize(QtCore.QSize(30, 16777215)) + self.pb_import.setObjectName("pb_import") + self.gridLayout.addWidget(self.pb_import, 1, 4, 1, 1) self.retranslateUi(Dialog) - self.buttonBox.accepted.connect(Dialog.accept) - self.buttonBox.rejected.connect(Dialog.reject) + self.buttonBox.accepted.connect(Dialog.accept) # type: ignore + self.buttonBox.rejected.connect(Dialog.reject) # type: ignore QtCore.QMetaObject.connectSlotsByName(Dialog) def retranslateUi(self, Dialog): _translate = QtCore.QCoreApplication.translate Dialog.setWindowTitle(_translate("Dialog", "Dialog")) - self.pb_test.setText(_translate("Dialog", "Test")) self.label.setText(_translate("Dialog", "Server url:")) self.label_2.setText(_translate("Dialog", "Client token:")) + self.pb_test.setText(_translate("Dialog", "Test")) + self.pb_import.setToolTip(_translate("Dialog", "Import settings")) + self.pb_import.setText(_translate("Dialog", "...")) if __name__ == "__main__": diff --git a/gotify_tray/gui/designs/widget_server.ui b/gotify_tray/gui/designs/widget_server.ui index 547e08e..64cf6ec 100644 --- a/gotify_tray/gui/designs/widget_server.ui +++ b/gotify_tray/gui/designs/widget_server.ui @@ -14,14 +14,14 @@ Dialog - - + + - Test + - + @@ -45,7 +45,14 @@ - + + + + Test + + + + Qt::Horizontal @@ -68,10 +75,19 @@ - - + + + + + 30 + 16777215 + + + + Import settings + - + ... diff --git a/gotify_tray/gui/designs/widget_settings.py b/gotify_tray/gui/designs/widget_settings.py index 310a6ab..55f4ecf 100644 --- a/gotify_tray/gui/designs/widget_settings.py +++ b/gotify_tray/gui/designs/widget_settings.py @@ -17,11 +17,7 @@ class Ui_Dialog(object): self.gridLayout.setObjectName("gridLayout") 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.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Apply|QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Ok) self.buttonBox.setObjectName("buttonBox") self.gridLayout.addWidget(self.buttonBox, 1, 0, 1, 1) self.tabWidget = QtWidgets.QTabWidget(Dialog) @@ -34,12 +30,7 @@ class Ui_Dialog(object): self.groupBox_5.setObjectName("groupBox_5") self.gridLayout_4 = QtWidgets.QGridLayout(self.groupBox_5) self.gridLayout_4.setObjectName("gridLayout_4") - spacerItem = QtWidgets.QSpacerItem( - 40, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, - ) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) self.gridLayout_4.addItem(spacerItem, 0, 2, 1, 1) self.label_6 = QtWidgets.QLabel(self.groupBox_5) self.label_6.setObjectName("label_6") @@ -73,12 +64,7 @@ class Ui_Dialog(object): 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) - spacerItem1 = QtWidgets.QSpacerItem( - 40, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, - ) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) self.gridLayout_3.addItem(spacerItem1, 0, 1, 1, 1) self.verticalLayout_4.addWidget(self.groupBox_4) self.groupBox_7 = QtWidgets.QGroupBox(self.tab_general) @@ -95,20 +81,10 @@ class Ui_Dialog(object): self.pb_open_log.setMaximumSize(QtCore.QSize(30, 16777215)) self.pb_open_log.setObjectName("pb_open_log") self.gridLayout_6.addWidget(self.pb_open_log, 0, 2, 1, 1) - spacerItem2 = QtWidgets.QSpacerItem( - 190, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, - ) + spacerItem2 = QtWidgets.QSpacerItem(190, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) self.gridLayout_6.addItem(spacerItem2, 0, 3, 1, 1) self.verticalLayout_4.addWidget(self.groupBox_7) - spacerItem3 = QtWidgets.QSpacerItem( - 20, - 40, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Expanding, - ) + spacerItem3 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) self.verticalLayout_4.addItem(spacerItem3) self.tabWidget.addTab(self.tab_general, "") self.tab_fonts = QtWidgets.QWidget() @@ -134,20 +110,31 @@ class Ui_Dialog(object): self.horizontalLayout.addWidget(self.pb_font_message_content) self.layout_fonts_message.addLayout(self.horizontalLayout) self.verticalLayout_5.addWidget(self.groupBox_2) - spacerItem4 = QtWidgets.QSpacerItem( - 20, - 40, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Expanding, - ) + spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) self.verticalLayout_5.addItem(spacerItem4) self.tabWidget.addTab(self.tab_fonts, "") + self.tab = QtWidgets.QWidget() + self.tab.setObjectName("tab") + self.verticalLayout = QtWidgets.QVBoxLayout(self.tab) + self.verticalLayout.setObjectName("verticalLayout") + self.pb_export = QtWidgets.QPushButton(self.tab) + self.pb_export.setObjectName("pb_export") + self.verticalLayout.addWidget(self.pb_export) + self.pb_import = QtWidgets.QPushButton(self.tab) + self.pb_import.setObjectName("pb_import") + self.verticalLayout.addWidget(self.pb_import) + self.pb_reset = QtWidgets.QPushButton(self.tab) + self.pb_reset.setObjectName("pb_reset") + self.verticalLayout.addWidget(self.pb_reset) + spacerItem5 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) + self.verticalLayout.addItem(spacerItem5) + self.tabWidget.addTab(self.tab, "") self.gridLayout.addWidget(self.tabWidget, 0, 0, 1, 1) self.retranslateUi(Dialog) self.tabWidget.setCurrentIndex(0) - self.buttonBox.accepted.connect(Dialog.accept) - self.buttonBox.rejected.connect(Dialog.reject) + self.buttonBox.accepted.connect(Dialog.accept) # type: ignore + self.buttonBox.rejected.connect(Dialog.reject) # type: ignore QtCore.QMetaObject.connectSlotsByName(Dialog) Dialog.setTabOrder(self.tabWidget, self.spin_priority) Dialog.setTabOrder(self.spin_priority, self.spin_duration) @@ -164,37 +151,29 @@ class Ui_Dialog(object): Dialog.setWindowTitle(_translate("Dialog", "Dialog")) self.groupBox_5.setTitle(_translate("Dialog", "Notifications")) self.label_6.setText(_translate("Dialog", "ms")) - self.label_4.setText( - _translate("Dialog", "Minimum priority to show notifications:") - ) + self.label_4.setText(_translate("Dialog", "Minimum priority to show notifications:")) self.label_5.setText(_translate("Dialog", "Notification duration:")) - self.cb_notify.setText( - _translate( - "Dialog", - "Show a notification for missed messages after reconnecting", - ) - ) + self.cb_notify.setText(_translate("Dialog", "Show a notification for missed messages after reconnecting")) self.groupBox_4.setTitle(_translate("Dialog", "Server info")) self.pb_change_server_info.setText(_translate("Dialog", "Change server info")) self.groupBox_7.setTitle(_translate("Dialog", "Logging")) self.label_7.setText(_translate("Dialog", "Level")) self.pb_open_log.setToolTip(_translate("Dialog", "Open logfile")) self.pb_open_log.setText(_translate("Dialog", "...")) - self.tabWidget.setTabText( - self.tabWidget.indexOf(self.tab_general), _translate("Dialog", "General") - ) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_general), _translate("Dialog", "General")) self.groupBox_2.setTitle(_translate("Dialog", "Message")) self.pb_font_message_title.setText(_translate("Dialog", "Title")) self.pb_font_message_date.setText(_translate("Dialog", "Date")) self.pb_font_message_content.setText(_translate("Dialog", "Message")) - self.tabWidget.setTabText( - self.tabWidget.indexOf(self.tab_fonts), _translate("Dialog", "Fonts") - ) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_fonts), _translate("Dialog", "Fonts")) + self.pb_export.setText(_translate("Dialog", "Export Settings")) + self.pb_import.setText(_translate("Dialog", "Import Settings")) + self.pb_reset.setText(_translate("Dialog", "Reset Settings")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("Dialog", "Advanced")) if __name__ == "__main__": import sys - app = QtWidgets.QApplication(sys.argv) Dialog = QtWidgets.QDialog() ui = Ui_Dialog() diff --git a/gotify_tray/gui/designs/widget_settings.ui b/gotify_tray/gui/designs/widget_settings.ui index cda48fe..b0f8d35 100644 --- a/gotify_tray/gui/designs/widget_settings.ui +++ b/gotify_tray/gui/designs/widget_settings.ui @@ -271,6 +271,47 @@ + + + Advanced + + + + + + Export Settings + + + + + + + Import Settings + + + + + + + Reset Settings + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + diff --git a/gotify_tray/gui/widgets/ServerInfoDialog.py b/gotify_tray/gui/widgets/ServerInfoDialog.py index 3c30ac3..817778e 100644 --- a/gotify_tray/gui/widgets/ServerInfoDialog.py +++ b/gotify_tray/gui/widgets/ServerInfoDialog.py @@ -1,12 +1,18 @@ +import os + +from gotify_tray.database import Settings from gotify_tray.gotify.models import GotifyVersionModel -from gotify_tray.tasks import VerifyServerInfoTask +from gotify_tray.tasks import ImportSettingsTask, VerifyServerInfoTask from PyQt6 import QtWidgets from ..designs.widget_server import Ui_Dialog +settings = Settings("gotify-tray") + + class ServerInfoDialog(QtWidgets.QDialog, Ui_Dialog): - def __init__(self, url: str = "", token: str = ""): + def __init__(self, url: str = "", token: str = "", enable_import: bool = True): super(ServerInfoDialog, self).__init__() self.setupUi(self) self.setWindowTitle("Server info") @@ -16,6 +22,7 @@ class ServerInfoDialog(QtWidgets.QDialog, Ui_Dialog): self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setDisabled( True ) + self.pb_import.setVisible(enable_import) self.link_callbacks() def test_server_info(self): @@ -60,6 +67,22 @@ class ServerInfoDialog(QtWidgets.QDialog, Ui_Dialog): self.pb_test.setStyleSheet("background-color: rgba(255, 0, 0, 100);") self.line_url.setStyleSheet("border: 1px solid red;") + def import_success_callback(self): + self.line_url.setText(settings.value("Server/url", type=str)) + self.line_token.setText(settings.value("Server/client_token")) + + def import_callback(self): + fname = QtWidgets.QFileDialog.getOpenFileName( + self, + "Import Settings", + settings.value("settings/export_path", type=str), + "*", + )[0] + if fname and os.path.exists(fname): + self.import_settings_task = ImportSettingsTask(fname) + self.import_settings_task.success.connect(self.import_success_callback) + self.import_settings_task.start() + def link_callbacks(self): self.pb_test.clicked.connect(self.test_server_info) self.line_url.textChanged.connect( @@ -72,3 +95,4 @@ class ServerInfoDialog(QtWidgets.QDialog, Ui_Dialog): QtWidgets.QDialogButtonBox.StandardButton.Ok ).setDisabled(True) ) + self.pb_import.clicked.connect(self.import_callback) diff --git a/gotify_tray/gui/widgets/SettingsDialog.py b/gotify_tray/gui/widgets/SettingsDialog.py index b1f08c1..0525ede 100644 --- a/gotify_tray/gui/widgets/SettingsDialog.py +++ b/gotify_tray/gui/widgets/SettingsDialog.py @@ -1,4 +1,5 @@ import logging +import os import webbrowser from gotify_tray.database import Settings @@ -6,6 +7,7 @@ from gotify_tray.gotify import GotifyMessageModel from gotify_tray.gui.models import MessagesModelItem from . import MessageWidget from gotify_tray.utils import verify_server +from gotify_tray.tasks import ExportSettingsTask, ImportSettingsTask from PyQt6 import QtCore, QtGui, QtWidgets from ..designs.widget_settings import Ui_Dialog @@ -16,6 +18,8 @@ settings = Settings("gotify-tray") class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): + quit_requested = QtCore.pyqtSignal() + def __init__(self): super(SettingsDialog, self).__init__() self.setupUi(self) @@ -74,7 +78,7 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): self.layout_fonts_message.addWidget(self.message_widget) def change_server_info_callback(self): - self.server_changed = verify_server(force_new=True) + self.server_changed = verify_server(force_new=True, enable_import=False) def settings_changed_callback(self, *args, **kwargs): self.settings_changed = True @@ -93,6 +97,42 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): self.settings_changed_callback() label.setFont(font) + def export_callback(self): + fname = QtWidgets.QFileDialog.getSaveFileName( + self, + "Export Settings", + settings.value("settings/export_path", type=str), + "*", + )[0] + if fname and os.path.exists(os.path.dirname(fname)): + self.export_settings_task = ExportSettingsTask(fname) + self.export_settings_task.start() + settings.setValue("settings/export_path", fname) + + def import_callback(self): + fname = QtWidgets.QFileDialog.getOpenFileName( + self, + "Import Settings", + settings.value("settings/export_path", type=str), + "*", + )[0] + if fname and os.path.exists(fname): + self.import_settings_task = ImportSettingsTask(fname) + self.import_settings_task.start() + + def reset_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() + self.quit_requested.emit() + def link_callbacks(self): self.buttonBox.button( QtWidgets.QDialogButtonBox.StandardButton.Apply @@ -123,6 +163,11 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): lambda: self.change_font_callback("message") ) + # Advanced + self.pb_export.clicked.connect(self.export_callback) + self.pb_import.clicked.connect(self.import_callback) + self.pb_reset.clicked.connect(self.reset_callback) + def apply_settings(self): # Priority settings.setValue("tray/notifications/priority", self.spin_priority.value()) diff --git a/gotify_tray/tasks.py b/gotify_tray/tasks.py index b74c81b..d8a821d 100644 --- a/gotify_tray/tasks.py +++ b/gotify_tray/tasks.py @@ -175,3 +175,27 @@ class ServerConnectionWatchdogTask(BaseTask): logger.debug( "ServerConnectionWatchdogTask: gotify_client is not listening" ) + + +class ExportSettingsTask(BaseTask): + success = pyqtSignal() + + def __init__(self, path: str): + super(ExportSettingsTask, self).__init__() + self.path = path + + def task(self): + settings.export(self.path) + self.success.emit() + + +class ImportSettingsTask(BaseTask): + success = pyqtSignal() + + def __init__(self, path: str): + super(ImportSettingsTask, self).__init__() + self.path = path + + def task(self): + settings.load(self.path) + self.success.emit() diff --git a/gotify_tray/utils.py b/gotify_tray/utils.py index 46935ef..dbcbb30 100644 --- a/gotify_tray/utils.py +++ b/gotify_tray/utils.py @@ -4,7 +4,7 @@ import re from pathlib import Path -def verify_server(force_new: bool = False) -> bool: +def verify_server(force_new: bool = False, enable_import: bool = True) -> bool: from gotify_tray.gui import ServerInfoDialog from gotify_tray.database import Settings @@ -14,7 +14,7 @@ def verify_server(force_new: bool = False) -> bool: token = settings.value("Server/client_token", type=str) if not url or not token or force_new: - dialog = ServerInfoDialog(url, token) + dialog = ServerInfoDialog(url, token, enable_import) if dialog.exec(): settings.setValue("Server/url", dialog.line_url.text()) settings.setValue("Server/client_token", dialog.line_token.text())