From 486f2dad71e6508d4ebd41eb289bbe810a61633a Mon Sep 17 00:00:00 2001 From: anne Date: Fri, 14 Jul 2023 08:07:27 +0200 Subject: [PATCH 01/11] Added most of the theme repo GUI + made the url a dedicated setting + started working on implementation --- menus.py | 11 ++++ pesterchum.py | 6 ++ thememanager.py | 156 ++++++++++++++++++++++++++++++++++++++++++++++++ user_profile.py | 4 ++ 4 files changed, 177 insertions(+) create mode 100644 thememanager.py diff --git a/menus.py b/menus.py index 98c9abe..6904655 100644 --- a/menus.py +++ b/menus.py @@ -12,6 +12,7 @@ except ImportError: import ostools import parsetools +from thememanager import ThemeManagerWidget from generic import RightClickList, RightClickTree, MultiTextDialog from dataobjs import pesterQuirk, PesterProfile, PesterHistory from memos import TimeSlider, TimeInput @@ -1425,6 +1426,13 @@ class PesterOptions(QtWidgets.QDialog): layout_5.addWidget(QtWidgets.QLabel("Minutes before Idle:")) layout_5.addWidget(self.idleBox) + layout_repo_url = QtWidgets.QHBoxLayout() + self.repoUrlBox = QtWidgets.QLineEdit(self) + self.repoUrlBox.setText(self.config.theme_repo_url()) + + layout_repo_url.addWidget(QtWidgets.QLabel("Theme repository db URL:")) + layout_repo_url.addWidget(self.repoUrlBox) + # self.updateBox = QtWidgets.QComboBox(self) # self.updateBox.addItem("Once a Day") # self.updateBox.addItem("Once a Week") @@ -1455,6 +1463,7 @@ class PesterOptions(QtWidgets.QDialog): self.themeBox.setCurrentIndex(i) self.refreshtheme = QtWidgets.QPushButton("Refresh current theme", self) self.refreshtheme.clicked.connect(parent.themeSelectOverride) + self.themeManager = ThemeManagerWidget(self.config) self.ghostchum = QtWidgets.QCheckBox("Pesterdunk Ghostchum!!", self) self.ghostchum.setChecked(self.config.ghostchum()) @@ -1656,6 +1665,7 @@ class PesterOptions(QtWidgets.QDialog): layout_idle = QtWidgets.QVBoxLayout(widget) layout_idle.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) layout_idle.addLayout(layout_5) + layout_idle.addLayout(layout_repo_url) layout_idle.addLayout(layout_6) # if not ostools.isOSXLeopard(): # layout_idle.addWidget(self.mspaCheck) @@ -1668,6 +1678,7 @@ class PesterOptions(QtWidgets.QDialog): layout_theme.addWidget(QtWidgets.QLabel("Pick a Theme:")) layout_theme.addWidget(self.themeBox) layout_theme.addWidget(self.refreshtheme) + layout_theme.addWidget(self.themeManager) layout_theme.addWidget(self.ghostchum) self.pages.addWidget(widget) diff --git a/pesterchum.py b/pesterchum.py index b3d7ca4..6004f2d 100755 --- a/pesterchum.py +++ b/pesterchum.py @@ -3101,6 +3101,11 @@ class PesterWindow(MovingWindow): if idlesetting != curidle: self.config.set("idleTime", idlesetting) self.idler["threshold"] = 60 * idlesetting + # theme repo url + repourlsetting = self.optionmenu.repoUrlBox.text() + if repourlsetting != self.config.theme_repo_url(): + self.config.set('theme_repo_url', repourlsetting) + # theme ghostchumsetting = self.optionmenu.ghostchum.isChecked() curghostchum = self.config.ghostchum() @@ -4194,6 +4199,7 @@ class MainProgram(QtCore.QObject): self.irc.finished.disconnect(self.restartIRC) def showUpdate(self, q): + # ~Lisanne: Doesn't seem to be used anywhere, old update notif mechanism? new_url = q.get() if new_url[0]: self.widget.pcUpdate.emit(new_url[0], new_url[1]) diff --git a/thememanager.py b/thememanager.py new file mode 100644 index 0000000..45c477a --- /dev/null +++ b/thememanager.py @@ -0,0 +1,156 @@ + +import logging +from os import remove + +try: + from PyQt6 import QtCore, QtGui, QtWidgets, QtNetwork + from PyQt6.QtGui import QAction + _flag_selectable = QtCore.Qt.TextInteractionFlag.TextSelectableByMouse + _flag_topalign = QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop +except ImportError: + print("PyQt5 fallback (menus.py)") + from PyQt5 import QtCore, QtGui, QtWidgets, QtNetwork + from PyQt5.QtWidgets import QAction + _flag_selectable = QtCore.Qt.TextSelectableByMouse + _flag_topalign = QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop + +# ~Lisanne +# This file has all the stuff needed to use a theme repository +# - ThemeManagerWidget, a GUI widget that +# - class for fetching & parsing database +# - class for handling installs & uninstalls. it also updates the manifest +# - manifest variable / json which keeps track of installed themes & their version + +def unzip_file(path_zip, path_destination): + pass + +class ThemeManagerWidget(QtWidgets.QWidget): + manifest = {} + database = {} + config = None + promise = None + def __init__(self, config, parent=None): + super().__init__(parent) + self.config = config + self.setupUI() + self.refresh() + + @QtCore.pyqtSlot(QtWidgets.QListWidgetItem) + def themeSelected(self, item): + print("Done!") + print(item) + pass + + @QtCore.pyqtSlot(QtNetwork.QNetworkReply) + def receiveReply(self, reply): + print(reply) + breakpoint + + @QtCore.pyqtSlot() + def refresh(self): + print("Starting refresh @ ",self.config.theme_repo_url()) + request = QtNetwork.QNetworkRequest() + request.setUrl(QtCore.QUrl(self.config.theme_repo_url())) + manager = QtNetwork.QNetworkAccessManager() + promise = manager.get(request) + manager.finished[QtNetwork.QNetworkReply].connect(self.receiveReply) + + + def setupUI(self): + self.layout_main = QtWidgets.QVBoxLayout(self) + self.setLayout(self.layout_main) + + # Search bar + self.line_search = QtWidgets.QLineEdit() + self.line_search.setPlaceholderText("Search for themes") + self.layout_main.addWidget(self.line_search) + + # Main layout + # [list of themes/results] | [selected theme details] + layout_hbox_list_and_details = QtWidgets.QHBoxLayout() + # This is the list of database themes + self.list_results = QtWidgets.QListWidget() + self.list_results.setSizePolicy( + QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + ) + ) + self.list_results.itemActivated.connect(self.themeSelected) + layout_hbox_list_and_details.addWidget(self.list_results) + + + # This is the right side, has the install buttons & all the theme details of the selected item + layout_vbox_details = QtWidgets.QVBoxLayout() + # The theme details are inside a scroll container in case of small window + self.frame_scroll = QtWidgets.QScrollArea() + self.frame_scroll.setSizePolicy( + QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + ) + # The vbox that the detail labels will rest in + layout_vbox_scroll_insides = QtWidgets.QVBoxLayout() + + # here starts the actual detail labels + # Selected theme's name + self.lbl_theme_name = QtWidgets.QLabel("Theme name here") + self.lbl_theme_name.setTextInteractionFlags(_flag_selectable) + self.lbl_theme_name.setStyleSheet("QLabel { font-size: 16px; font-weight:bold;}") + self.lbl_theme_name.setWordWrap(True) + layout_vbox_scroll_insides.addWidget(self.lbl_theme_name) + + # Author name + self.lbl_author_name = QtWidgets.QLabel("Author: ?") + self.lbl_author_name.setTextInteractionFlags(_flag_selectable) + layout_vbox_scroll_insides.addWidget(self.lbl_author_name) + + # description. this needs to be the biggest + self.lbl_description = QtWidgets.QLabel("Description") + self.lbl_description.setTextInteractionFlags(_flag_selectable) + self.lbl_description.setSizePolicy( + QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + ) + ) + self.lbl_description.setAlignment(_flag_topalign) + self.lbl_description.setWordWrap(True) + layout_vbox_scroll_insides.addWidget(self.lbl_description) + + # requires. shows up if a theme has "inherits" set & we dont have it installed + self.lbl_requires = QtWidgets.QLabel("Requires: pibby") + self.lbl_requires.setTextInteractionFlags(_flag_selectable) + layout_vbox_scroll_insides.addWidget(self.lbl_requires) + + # Version number. this will also show the current installed one if there is an update + self.lbl_version = QtWidgets.QLabel("Version: 2 (installed: 0)") + self.lbl_version.setTextInteractionFlags(_flag_selectable) + layout_vbox_scroll_insides.addWidget(self.lbl_version) + + # Last update time + self.lbl_last_update = QtWidgets.QLabel("DD/MM/YYYY HH:MM") + self.lbl_last_update.setTextInteractionFlags(_flag_selectable) + layout_vbox_scroll_insides.addWidget(self.lbl_last_update) + + # Theme details done, so we wont need the scroll after this + self.frame_scroll.setLayout(layout_vbox_scroll_insides) + layout_vbox_details.addWidget(self.frame_scroll) + # Insta//uninstall buttons + # "Uninstall" button. Only visisble when the selected thene is installed + self.btn_uninstall = QtWidgets.QPushButton("Uninstall", self) + self.btn_uninstall.setHidden(True) + layout_vbox_details.addWidget(self.btn_uninstall) + # "Install" button. can also say "Update" if an update is availible + self.btn_install = QtWidgets.QPushButton("Install", self) + layout_vbox_details.addWidget(self.btn_install) + + # Done with details + layout_hbox_list_and_details.addLayout(layout_vbox_details) + self.layout_main.addLayout(layout_hbox_list_and_details) + + self.btn_refresh = QtWidgets.QPushButton("Refresh", self) + self.btn_refresh.clicked.connect(self.refresh) + self.layout_main.addWidget(self.btn_refresh) + diff --git a/user_profile.py b/user_profile.py index 7076069..be563fe 100644 --- a/user_profile.py +++ b/user_profile.py @@ -361,6 +361,9 @@ with a backup from: %s" def irc_compatibility_mode(self): return self.config.get("irc_compatibility_mode", False) + def theme_repo_url(self): + return self.config.get('theme_repo_url', 'https://raw.githubusercontent.com/mocchapi/pesterchum-themes/main/db.json') + def force_prefix(self): return self.config.get("force_prefix", True) @@ -782,6 +785,7 @@ class userProfile: def getTheme(self): return self.theme + def getAutoIdentify(self): return self.autoidentify From e6e8f4c1ef4572747e51d7df192981ce4e4ce745 Mon Sep 17 00:00:00 2001 From: anne Date: Fri, 14 Jul 2023 15:09:56 +0200 Subject: [PATCH 02/11] i forgot to commit for maaany hours. its pretty much done i think :D --- .gitignore | 1 + img/download_done.png | Bin 0 -> 189 bytes img/download_pending.png | Bin 0 -> 189 bytes img/download_update.png | Bin 0 -> 189 bytes menus.py | 40 ++++- ostools.py | 13 +- theme_repo_manager.py | 363 +++++++++++++++++++++++++++++++++++++++ thememanager.py | 156 ----------------- 8 files changed, 405 insertions(+), 168 deletions(-) create mode 100644 img/download_done.png create mode 100644 img/download_pending.png create mode 100644 img/download_update.png create mode 100644 theme_repo_manager.py delete mode 100644 thememanager.py diff --git a/.gitignore b/.gitignore index 726274d..aa44cc3 100644 --- a/.gitignore +++ b/.gitignore @@ -179,3 +179,4 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + diff --git a/img/download_done.png b/img/download_done.png new file mode 100644 index 0000000000000000000000000000000000000000..5cd8bf301ad3c9c7b8ec1cc46c0c4762196c149b GIT binary patch literal 189 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|$~|2iLo9le zQyiFu_Wb&P@#utu-RH$6C;rsAA@Xv+$e~p~7yYp2F;uJBCdll@yP%0tr{$5X?1I(l zEu0U!s$&?YsYy)qRlO!K<9ULa@IMyZCg0x`1+ mr5bWLcoHt3IdI?r1H;u$Q?X_C` literal 0 HcmV?d00001 diff --git a/img/download_pending.png b/img/download_pending.png new file mode 100644 index 0000000000000000000000000000000000000000..6a83e5fb2509ee73faa9ea6755e8ba7cf276ff44 GIT binary patch literal 189 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|$~|2iLo9le zQyiFu_Wb&P@#utu-RH$6C;rsAA@X#CR`OMwr{80F4ApA32{OC!E@)!ZX?Y|oyI^&C z3+IEb>KKM;Y7!HDRj&!mcqTqy-{{a})s1Zm*=@TUBAhRnCQCG&7s#2lQL5prK+LQ{ lsfHX5o`lP14jeeZz>vymDwfq^x*X^P22WQ%mvv4FO#qSzLX!Xh literal 0 HcmV?d00001 diff --git a/img/download_update.png b/img/download_update.png new file mode 100644 index 0000000000000000000000000000000000000000..a1aa84702d4be122dc810772ed6f6e5e0f074756 GIT binary patch literal 189 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|$~|2iLo9le zQyiFu_Wb&P@#utu-RH$6C;rsAA;M|0{^+Wo&bbC{62f-3I~W(U1sr4$bNblFcj2n> zA(n!pwt5WS!aXOJ31xHmd, connect() failed between finished(QNetworkReply*) and _on_reply()`` + # maybe because of the underscore? + # @QtCore.pyqtSlot(QtNetwork.QNetworkReply) + def _on_reply(self, reply): + if reply.error() != QtNetwork.QNetworkReply.NetworkError.NoError: + PchumLog.error("An error occured contacting the repository: %s" % reply.error()) + self.errored.emit("An error occured contacting the repository: %s" % reply.error()) + print([reply.error()]) + return + try: + original_url = reply.request().url().url() + # Check if zipfile or database fetch + if original_url in self.downloads: + theme = self.downloads[original_url] + self._handle_downloaded_zip(bytes(reply.readAll()), theme) + self.downloads.pop(original_url) + self.manifest[theme['name']] = theme + self.save_manifest() + self.manifest_updated.emit(self.manifest) + else: + as_json = bytes(reply.readAll()).decode("utf-8") + self.database = json.loads(as_json) + self.database_entries = {} + if not self.is_database_valid(): + PchumLog.error('Incorrect database format, missing "entries"') + self.errored.emit('Incorrect database format, missing "entries"') + return + self.database_entries = {} + for dbitem in self.database['entries']: + self.database_entries[dbitem['name']] = dbitem + self.database_refreshed.emit(self.database) + except json.decoder.JSONDecodeError as e: + PchumLog.error("Could not decode JSON: %s" % e) + self.errored.emit("Could not decode JSON: %s" % e) + return + + + def _handle_downloaded_zip(self, zip_buffer, theme): + # ~liasnne TODO: i dont know if this is inside a thread or not so + # probably check to make sure it is + # when its not 5 am + directory = os.path.join(getDataDir(), 'themes', theme['name']) + with zipfile.ZipFile(io.BytesIO(zip_buffer)) as z: + if os.path.isdir(directory): + rmtree(directory) + # Deletes old files that have been removed in an update + z.extractall(directory) + + + +class ThemeManagerWidget(QtWidgets.QWidget): + icons = None + config = None + + rebuilt = QtCore.pyqtSignal() + + def __init__(self, config, parent=None): + super().__init__(parent) + self.icons = [QtGui.QIcon("img/download_pending.png"), QtGui.QIcon("img/download_done.png"), QtGui.QIcon("img/download_update.png"), ] + self.config = config + global themeManager + if themeManager == None or not themeManager.is_database_valid(): + themeManager = ThemeManager(config) + self.setupUI() + else: + self.setupUI() + self.rebuild() + themeManager.database_refreshed.connect(self._on_database_refreshed) + themeManager.manifest_updated.connect(self._on_database_refreshed) + + + def setupUI(self): + self.layout_main = QtWidgets.QVBoxLayout(self) + self.setLayout(self.layout_main) + + # Search bar + # TODO: implement searching + # self.line_search = QtWidgets.QLineEdit() + # self.line_search.setPlaceholderText("Search for themes") + # self.layout_main.addWidget(self.line_search) + + # Main layout + # [list of themes/results] | [selected theme details] + layout_hbox_list_and_details = QtWidgets.QHBoxLayout() + # This is the list of database themes + self.list_results = QtWidgets.QListWidget() + self.list_results.setSizePolicy( + QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + ) + self.list_results.itemClicked.connect(self._on_theme_selected) + layout_hbox_list_and_details.addWidget(self.list_results) + + + # This is the right side, has the install buttons & all the theme details of the selected item + layout_vbox_details = QtWidgets.QVBoxLayout() + # The theme details are inside a scroll container in case of small window + self.frame_scroll = QtWidgets.QScrollArea() + self.frame_scroll.setSizePolicy( + QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + ) + # The vbox that the detail labels will rest in + layout_vbox_scroll_insides = QtWidgets.QVBoxLayout() + + # here starts the actual detail labels + # Selected theme's name + self.lbl_theme_name = QtWidgets.QLabel("Click a theme to get started") + self.lbl_theme_name.setTextInteractionFlags(_flag_selectable) + self.lbl_theme_name.setStyleSheet("QLabel { font-size: 16px; font-weight:bold;}") + self.lbl_theme_name.setWordWrap(False) + layout_vbox_scroll_insides.addWidget(self.lbl_theme_name) + + # Author name + self.lbl_author_name = QtWidgets.QLabel("") + self.lbl_author_name.setTextInteractionFlags(_flag_selectable) + layout_vbox_scroll_insides.addWidget(self.lbl_author_name) + + # description. this needs to be the biggest + self.lbl_description = QtWidgets.QLabel("") + self.lbl_description.setTextInteractionFlags(_flag_selectable) + self.lbl_description.setSizePolicy( + QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + ) + ) + self.lbl_description.setAlignment(_flag_topalign) + self.lbl_description.setWordWrap(True) + layout_vbox_scroll_insides.addWidget(self.lbl_description) + + # requires. shows up if a theme has "inherits" set & we dont have it installed + self.lbl_requires = QtWidgets.QLabel("") + self.lbl_requires.setTextInteractionFlags(_flag_selectable) + layout_vbox_scroll_insides.addWidget(self.lbl_requires) + + # Version number. this will also show the current installed one if there is an update + self.lbl_version = QtWidgets.QLabel("") + self.lbl_version.setTextInteractionFlags(_flag_selectable) + layout_vbox_scroll_insides.addWidget(self.lbl_version) + + # Last update time + self.lbl_last_update = QtWidgets.QLabel("") + self.lbl_last_update.setWordWrap(True) + self.lbl_last_update.setTextInteractionFlags(_flag_selectable) + layout_vbox_scroll_insides.addWidget(self.lbl_last_update) + + # Theme details done, so we wont need the scroll after this + self.frame_scroll.setLayout(layout_vbox_scroll_insides) + layout_vbox_details.addWidget(self.frame_scroll) + # Insta//uninstall buttons + # "Uninstall" button. Only visisble when the selected thene is installed + self.btn_uninstall = QtWidgets.QPushButton("Uninstall", self) + self.btn_uninstall.setHidden(True) + self.btn_uninstall.clicked.connect(self._on_uninstall_clicked) + layout_vbox_details.addWidget(self.btn_uninstall) + # "Install" button. can also say "Update" if an update is availible + # Only visible when not installed or if theres an update + self.btn_install = QtWidgets.QPushButton("Install", self) + self.btn_install.clicked.connect(self._on_install_clicked) + self.btn_install.setDisabled(True) + layout_vbox_details.addWidget(self.btn_install) + + # Done with details + layout_hbox_list_and_details.addLayout(layout_vbox_details) + self.layout_main.addLayout(layout_hbox_list_and_details) + + self.btn_refresh = QtWidgets.QPushButton("Refresh", self) + self.btn_refresh.clicked.connect(themeManager.refresh_database) # Conneced to themeManager! + self.layout_main.addWidget(self.btn_refresh) + + self.lbl_error = QtWidgets.QLabel("") + self.lbl_error.setVisible(False) + self.lbl_error.setWordWrap(True) + themeManager.errored.connect(self._on_fetch_error) + self.lbl_error.setTextInteractionFlags(_flag_selectable) + self.lbl_error.setStyleSheet(" QLabel { background-color:black; color:red; font-size: 16px;}") + self.layout_main.addWidget(self.lbl_error) + + + def _on_fetch_error(self, text): + self.lbl_error.setText(text) + self.lbl_error.setVisible(True) + + def _on_uninstall_clicked(self): + theme = themeManager.database['entries'][self.list_results.currentRow()] + themeManager.delete_theme(theme['name']) + + def _on_install_clicked(self): + theme = themeManager.database['entries'][self.list_results.currentRow()] + themeManager.download_theme(theme['name']) + + @QtCore.pyqtSlot(QtWidgets.QListWidgetItem) + def _on_theme_selected(self, item): + index = self.list_results.currentRow() + theme = themeManager.database['entries'][index] + theme_name = theme['name'] + is_installed = themeManager.is_installed(theme_name) + has_update = themeManager.has_update(theme_name) + self.btn_install.setDisabled(False) + self.btn_install.setText( "Update" if has_update else "Install" ) + self.btn_install.setVisible( (is_installed and has_update) or not is_installed ) + self.btn_uninstall.setVisible( themeManager.is_installed(theme_name) ) + + self.lbl_theme_name.setText(theme_name) + self.lbl_author_name.setText("By %s" % theme['author']) + self.lbl_description.setText(theme['description']) + version_text = "Version %s" % theme['version'] + if has_update: + version_text += " (Installed: %s)" % themeManager.manifest[theme_name]['version'] + self.lbl_version.setText( version_text ) + self.lbl_requires.setText( ("Requires %s" % theme['inherits']) if theme['inherits'] else "") + self.lbl_last_update.setText( "Released on: " + datetime.fromtimestamp(theme['updated']).strftime("%d/%m/%Y, %H:%M") ) + + @QtCore.pyqtSlot(dict) + def _on_database_refreshed(self,_): + self.rebuild() + + def rebuild(self): + database = themeManager.database + self.list_results.clear() + self.lbl_error.setText("") + self.lbl_error.setVisible(False) + + if not themeManager.is_database_valid(): + self.lbl_error.setText("") + self.lbl_error.setVisible(True) + + # Repopulate the list + for dbitem in database['entries']: + # Determine the suffix + icon = self.icons[0] + status = '' + if themeManager.is_installed(dbitem['name']): + if themeManager.has_update(dbitem['name']): + status = '~ (update available)' + icon = self.icons[2] + else: + status = '~ (installed)' + icon = self.icons[1] + text = "%s by %s %s" % (dbitem['name'], dbitem['author'], status) + item = QtWidgets.QListWidgetItem(icon, text) + self.list_results.addItem(item) + + self.btn_install.setDisabled(True) + for lbl in [self.lbl_author_name, self.lbl_description, self.lbl_version, self.lbl_requires, self.lbl_last_update]: + lbl.setText("") + self.lbl_theme_name.setText("Click a theme to get started") + self.btn_uninstall.setVisible(False) + self.btn_install.setVisible(True) + self.btn_install.setDisabled(True) + + + self.rebuilt.emit() + PchumLog.debug("Rebuilt emitted") \ No newline at end of file diff --git a/thememanager.py b/thememanager.py deleted file mode 100644 index 45c477a..0000000 --- a/thememanager.py +++ /dev/null @@ -1,156 +0,0 @@ - -import logging -from os import remove - -try: - from PyQt6 import QtCore, QtGui, QtWidgets, QtNetwork - from PyQt6.QtGui import QAction - _flag_selectable = QtCore.Qt.TextInteractionFlag.TextSelectableByMouse - _flag_topalign = QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop -except ImportError: - print("PyQt5 fallback (menus.py)") - from PyQt5 import QtCore, QtGui, QtWidgets, QtNetwork - from PyQt5.QtWidgets import QAction - _flag_selectable = QtCore.Qt.TextSelectableByMouse - _flag_topalign = QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop - -# ~Lisanne -# This file has all the stuff needed to use a theme repository -# - ThemeManagerWidget, a GUI widget that -# - class for fetching & parsing database -# - class for handling installs & uninstalls. it also updates the manifest -# - manifest variable / json which keeps track of installed themes & their version - -def unzip_file(path_zip, path_destination): - pass - -class ThemeManagerWidget(QtWidgets.QWidget): - manifest = {} - database = {} - config = None - promise = None - def __init__(self, config, parent=None): - super().__init__(parent) - self.config = config - self.setupUI() - self.refresh() - - @QtCore.pyqtSlot(QtWidgets.QListWidgetItem) - def themeSelected(self, item): - print("Done!") - print(item) - pass - - @QtCore.pyqtSlot(QtNetwork.QNetworkReply) - def receiveReply(self, reply): - print(reply) - breakpoint - - @QtCore.pyqtSlot() - def refresh(self): - print("Starting refresh @ ",self.config.theme_repo_url()) - request = QtNetwork.QNetworkRequest() - request.setUrl(QtCore.QUrl(self.config.theme_repo_url())) - manager = QtNetwork.QNetworkAccessManager() - promise = manager.get(request) - manager.finished[QtNetwork.QNetworkReply].connect(self.receiveReply) - - - def setupUI(self): - self.layout_main = QtWidgets.QVBoxLayout(self) - self.setLayout(self.layout_main) - - # Search bar - self.line_search = QtWidgets.QLineEdit() - self.line_search.setPlaceholderText("Search for themes") - self.layout_main.addWidget(self.line_search) - - # Main layout - # [list of themes/results] | [selected theme details] - layout_hbox_list_and_details = QtWidgets.QHBoxLayout() - # This is the list of database themes - self.list_results = QtWidgets.QListWidget() - self.list_results.setSizePolicy( - QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.MinimumExpanding, - QtWidgets.QSizePolicy.Policy.MinimumExpanding, - ) - ) - self.list_results.itemActivated.connect(self.themeSelected) - layout_hbox_list_and_details.addWidget(self.list_results) - - - # This is the right side, has the install buttons & all the theme details of the selected item - layout_vbox_details = QtWidgets.QVBoxLayout() - # The theme details are inside a scroll container in case of small window - self.frame_scroll = QtWidgets.QScrollArea() - self.frame_scroll.setSizePolicy( - QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Preferred, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - ) - # The vbox that the detail labels will rest in - layout_vbox_scroll_insides = QtWidgets.QVBoxLayout() - - # here starts the actual detail labels - # Selected theme's name - self.lbl_theme_name = QtWidgets.QLabel("Theme name here") - self.lbl_theme_name.setTextInteractionFlags(_flag_selectable) - self.lbl_theme_name.setStyleSheet("QLabel { font-size: 16px; font-weight:bold;}") - self.lbl_theme_name.setWordWrap(True) - layout_vbox_scroll_insides.addWidget(self.lbl_theme_name) - - # Author name - self.lbl_author_name = QtWidgets.QLabel("Author: ?") - self.lbl_author_name.setTextInteractionFlags(_flag_selectable) - layout_vbox_scroll_insides.addWidget(self.lbl_author_name) - - # description. this needs to be the biggest - self.lbl_description = QtWidgets.QLabel("Description") - self.lbl_description.setTextInteractionFlags(_flag_selectable) - self.lbl_description.setSizePolicy( - QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Preferred, - QtWidgets.QSizePolicy.Policy.MinimumExpanding, - ) - ) - self.lbl_description.setAlignment(_flag_topalign) - self.lbl_description.setWordWrap(True) - layout_vbox_scroll_insides.addWidget(self.lbl_description) - - # requires. shows up if a theme has "inherits" set & we dont have it installed - self.lbl_requires = QtWidgets.QLabel("Requires: pibby") - self.lbl_requires.setTextInteractionFlags(_flag_selectable) - layout_vbox_scroll_insides.addWidget(self.lbl_requires) - - # Version number. this will also show the current installed one if there is an update - self.lbl_version = QtWidgets.QLabel("Version: 2 (installed: 0)") - self.lbl_version.setTextInteractionFlags(_flag_selectable) - layout_vbox_scroll_insides.addWidget(self.lbl_version) - - # Last update time - self.lbl_last_update = QtWidgets.QLabel("DD/MM/YYYY HH:MM") - self.lbl_last_update.setTextInteractionFlags(_flag_selectable) - layout_vbox_scroll_insides.addWidget(self.lbl_last_update) - - # Theme details done, so we wont need the scroll after this - self.frame_scroll.setLayout(layout_vbox_scroll_insides) - layout_vbox_details.addWidget(self.frame_scroll) - # Insta//uninstall buttons - # "Uninstall" button. Only visisble when the selected thene is installed - self.btn_uninstall = QtWidgets.QPushButton("Uninstall", self) - self.btn_uninstall.setHidden(True) - layout_vbox_details.addWidget(self.btn_uninstall) - # "Install" button. can also say "Update" if an update is availible - self.btn_install = QtWidgets.QPushButton("Install", self) - layout_vbox_details.addWidget(self.btn_install) - - # Done with details - layout_hbox_list_and_details.addLayout(layout_vbox_details) - self.layout_main.addLayout(layout_hbox_list_and_details) - - self.btn_refresh = QtWidgets.QPushButton("Refresh", self) - self.btn_refresh.clicked.connect(self.refresh) - self.layout_main.addWidget(self.btn_refresh) - From e51a1490b8822fd627705528b0dcb991d0538532 Mon Sep 17 00:00:00 2001 From: anne Date: Thu, 20 Jul 2023 20:40:29 +0200 Subject: [PATCH 03/11] Made checks for non-qt pesterchum themes --- theme_repo_manager.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/theme_repo_manager.py b/theme_repo_manager.py index ab9bd89..032ad53 100644 --- a/theme_repo_manager.py +++ b/theme_repo_manager.py @@ -109,7 +109,6 @@ class ThemeManager(QtCore.QObject): if reply.error() != QtNetwork.QNetworkReply.NetworkError.NoError: PchumLog.error("An error occured contacting the repository: %s" % reply.error()) self.errored.emit("An error occured contacting the repository: %s" % reply.error()) - print([reply.error()]) return try: original_url = reply.request().url().url() @@ -126,17 +125,32 @@ class ThemeManager(QtCore.QObject): self.database = json.loads(as_json) self.database_entries = {} if not self.is_database_valid(): + self.database = {} PchumLog.error('Incorrect database format, missing "entries"') self.errored.emit('Incorrect database format, missing "entries"') return - self.database_entries = {} + + # Filter out non-QTchum client themes, like for godot + for dbindex in range(len(self.database['entries'])-1, -1, -1): # Iterate over the database in reverse + dbitem = self.database['entries'][dbindex] + if dbitem['client'] != "pesterchum": + PchumLog.debug("Removed database theme %s because it is not compatible with this client" % dbitem['name']) + self.database['entries'].pop(dbindex) + # Make an easy lookup table instead of the array we get from the DB for dbitem in self.database['entries']: self.database_entries[dbitem['name']] = dbitem self.database_refreshed.emit(self.database) except json.decoder.JSONDecodeError as e: - PchumLog.error("Could not decode JSON: %s" % e) - self.errored.emit("Could not decode JSON: %s" % e) + PchumLog.error("Could not decode theme database JSON: %s" % e) + self.errored.emit("Could not decode theme database JSON: %s" % e) return + except KeyError as e: + self.database = {} + self.database_entries = {} + PchumLog.error("Vital key missing from theme database: %s" % e) + self.errored.emit("Vital key missing from theme database: %s" % e) + return + def _handle_downloaded_zip(self, zip_buffer, theme): @@ -216,7 +230,7 @@ class ThemeManagerWidget(QtWidgets.QWidget): self.lbl_theme_name = QtWidgets.QLabel("Click a theme to get started") self.lbl_theme_name.setTextInteractionFlags(_flag_selectable) self.lbl_theme_name.setStyleSheet("QLabel { font-size: 16px; font-weight:bold;}") - self.lbl_theme_name.setWordWrap(False) + self.lbl_theme_name.setWordWrap(True) layout_vbox_scroll_insides.addWidget(self.lbl_theme_name) # Author name From 7ef7fa95ddbf417573b826d6168fcb5f37ae2efa Mon Sep 17 00:00:00 2001 From: anne Date: Thu, 20 Jul 2023 22:31:29 +0200 Subject: [PATCH 04/11] Added automatic depedency install chaining throug install_theme. Also added more comments and logging --- theme_repo_manager.py | 105 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 12 deletions(-) diff --git a/theme_repo_manager.py b/theme_repo_manager.py index 032ad53..9d7c597 100644 --- a/theme_repo_manager.py +++ b/theme_repo_manager.py @@ -63,10 +63,15 @@ class ThemeManager(QtCore.QObject): @QtCore.pyqtSlot() def refresh_database(self): + # Fetches a new copy of the theme database from the given URL + # The initialisation & processing of it is handled in self._on_reply PchumLog.debug("Refreshing theme repo database @ %s" % self.config.theme_repo_url()) promise = self.NAManager.get(QtNetwork.QNetworkRequest(QtCore.QUrl(self.config.theme_repo_url()))) def delete_theme(self, theme_name): + # TODO: check if other installed themes inherit from this to avoid broken themes + # would require some kinda confirmation popup which i havent figure out yet + PchumLog.info("Deleting installed repo theme %s" % theme_name) theme = self.manifest[theme_name] directory = os.path.join(getDataDir(), 'themes', theme['name']) if os.path.isdir(directory): @@ -81,28 +86,98 @@ class ThemeManager(QtCore.QObject): PchumLog.debug("Saved manifes.js to %s" % self.manifest_path) def validate_manifest(self): + # Checks if the themes the manifest claims are installed actually exists + # Removes them from the manifest if they dont all_themes = self.config.availableThemes() for theme_name in self.manifest: if not theme_name in all_themes: - PchumLog.warning("Theme %s from the manifest seems to have been deleted" % theme_name) + PchumLog.warning("Supposedly installed theme %s from the manifest seems to have been deleted, removing from manifest now" % theme_name) + self.manifest.pop(theme_name) def download_theme(self, theme_name): + # Downloads the theme .zip + # The actual installing is handled by _on_reply when the theme is downloaded + # Performs no version checks or dependency handling + # Use install_theme() instead unless you know what you're doing PchumLog.info("Downloading %s" % theme_name) + if not theme_name in self.database_entries: + PchumLog.error("Theme name %s does not exist in the database!" % theme_name) + return + PchumLog.debug("(From %s)" % self.database_entries[theme_name]['download']) promise = self.NAManager.get(QtNetwork.QNetworkRequest(QtCore.QUrl(self.database_entries[theme_name]['download']))) self.downloads[self.database_entries[theme_name]['download']] = self.database_entries[theme_name] + + def install_theme(self, theme_name, force_install=False): + # A higher way to install a theme than download_theme + # Checks if the theme is already installed & if its up to date + # Also recursively handled dependencies, which download_theme does not + # !! note that this does not check if theres a circular dependency !! + # Setting force_install to True will install a given theme, even if it is deemed unnecessary to do so or its inherit dependency cannot be installed + # This gives it the same no-nonsense operation as download_theme, but with the checks in place + PchumLog.info("Installing theme %s" % theme_name) + if force_install: + PchumLog.debug("(force_install is enabled)") + if not theme_name in self.database_entries: + PchumLog.error("Theme %s does not exist in the database!" % theme_name) + self.errored.emit("Theme %s does not exist in the database!" % theme_name) + return + + all_themes = self.config.availableThemes() + theme = self.database_entries[theme_name] + if not self.is_installed(theme_name) and theme_name in all_themes: # Theme exists, but not installed by manager + PchumLog.warning("Theme %s is already installed manually. The manual version will get shadowed by the repository version & will not be usable" % theme_name) + + # Check depedencies + if theme['inherits'] != "": + if self.is_installed(theme['inherits']): + # Inherited theme is installed. A-OK + PchumLog.debug("Theme %s requires theme %s, which is already installed through the repository" % (theme_name, theme['inherits'])) + if theme['inherits'] in all_themes: + # Inherited theme is manually installed. A-OK + PchumLog.debug("Theme %s requires theme %s, which is already installed manually by the user" % (theme_name, theme['inherits'])) + elif theme['inherits'] in self.database_entries: + # The Inherited theme is not installed, but can be. A-OK + PchumLog.info("Theme %s requires theme %s, which will now be installed" % (theme_name, theme['inherits'])) + self.install_theme(theme['inherits']) + else: + # Inherited theme is not installed, and can't be installed automatically. Exits unless force_install is True + if force_install: + PchumLog.error("Theme %s requires theme %s, which is not installed and not in the database. Installing %s anyways, because force_install is True" % (theme_name, theme, theme_name['inherits'])) + else: + PchumLog.error("Theme %s requires theme %s, which is not installed and not in the database. Cancelling install" % (theme_name, theme['inherits'])) + self.errored.emit("Theme %s requires theme %s, which is not installed and not in the database. Cancelling install" % (theme_name, theme['inherits'])) + return + + # Check if there's no need to re-install theme + # This is done after the dependency check in case an inherited theme is missing two levels down + if self.is_installed(theme_name) and not self.has_update(theme_name): # Theme is installed by manager, and is up-to-date + if force_install: + PchumLog.warning("Theme %s is already installed, and no update is available. Installing anyways, because force_install is True" % theme_name) + else: + PchumLog.warning("Theme %s is already installed, and no update is available. Cancelling install" % theme_name) + self.errored.emit("Theme %s is already installed, and no update is available. Cancelling install" % theme_name) + return + + # All is ok. or we're just ignoring the errors through force_install + # No matter. downloading time + self.download_theme(theme_name) def has_update(self, theme_name): + # Has the given theme an update available + # Returns False if the theme is installed manually or when the theme is up to date if self.is_installed(theme_name) and theme_name in self.database_entries: - return self.manifest[theme_name]['version'] != self.database_entries[theme_name]['version'] + return self.manifest[theme_name]['version'] < self.database_entries[theme_name]['version'] return False def is_installed(self, theme_name): + # checks if a theme is installed through the manager + # Note that this will return False if the given name is a theme that the user installed manually! return theme_name in self.manifest def is_database_valid(self): return 'entries' in self.database and isinstance(self.database.get('entries'), list) - # using a slot here raises `ERROR - , connect() failed between finished(QNetworkReply*) and _on_reply()`` + # using a slot decorator here raises `ERROR - , connect() failed between finished(QNetworkReply*) and _on_reply()`` # maybe because of the underscore? # @QtCore.pyqtSlot(QtNetwork.QNetworkReply) def _on_reply(self, reply): @@ -114,13 +189,16 @@ class ThemeManager(QtCore.QObject): original_url = reply.request().url().url() # Check if zipfile or database fetch if original_url in self.downloads: + # This is a theme zip! theme = self.downloads[original_url] - self._handle_downloaded_zip(bytes(reply.readAll()), theme) + self._handle_downloaded_zip(bytes(reply.readAll()), theme['name']) self.downloads.pop(original_url) self.manifest[theme['name']] = theme self.save_manifest() self.manifest_updated.emit(self.manifest) + PchumLog.info("Theme %s is now installed" % theme['name']) else: + # This is a database refresh! as_json = bytes(reply.readAll()).decode("utf-8") self.database = json.loads(as_json) self.database_entries = {} @@ -139,7 +217,9 @@ class ThemeManager(QtCore.QObject): # Make an easy lookup table instead of the array we get from the DB for dbitem in self.database['entries']: self.database_entries[dbitem['name']] = dbitem + PchumLog.info("Database refreshed") self.database_refreshed.emit(self.database) + except json.decoder.JSONDecodeError as e: PchumLog.error("Could not decode theme database JSON: %s" % e) self.errored.emit("Could not decode theme database JSON: %s" % e) @@ -153,11 +233,11 @@ class ThemeManager(QtCore.QObject): - def _handle_downloaded_zip(self, zip_buffer, theme): - # ~liasnne TODO: i dont know if this is inside a thread or not so - # probably check to make sure it is - # when its not 5 am - directory = os.path.join(getDataDir(), 'themes', theme['name']) + def _handle_downloaded_zip(self, zip_buffer, theme_name): + # Unzips the downloaded theme package in-memory to datadir/themes/theme_name + # I dont think this runs in a thread so it may block, but its so fast i dont think it really matters + # But i guess if its a zip bomb itll crash + directory = os.path.join(getDataDir(), 'themes', theme_name) with zipfile.ZipFile(io.BytesIO(zip_buffer)) as z: if os.path.isdir(directory): rmtree(directory) @@ -254,6 +334,7 @@ class ThemeManagerWidget(QtWidgets.QWidget): # requires. shows up if a theme has "inherits" set & we dont have it installed self.lbl_requires = QtWidgets.QLabel("") self.lbl_requires.setTextInteractionFlags(_flag_selectable) + self.lbl_requires.setWordWrap(True) layout_vbox_scroll_insides.addWidget(self.lbl_requires) # Version number. this will also show the current installed one if there is an update @@ -310,7 +391,7 @@ class ThemeManagerWidget(QtWidgets.QWidget): def _on_install_clicked(self): theme = themeManager.database['entries'][self.list_results.currentRow()] - themeManager.download_theme(theme['name']) + themeManager.install_theme(theme['name']) @QtCore.pyqtSlot(QtWidgets.QListWidgetItem) def _on_theme_selected(self, item): @@ -329,9 +410,9 @@ class ThemeManagerWidget(QtWidgets.QWidget): self.lbl_description.setText(theme['description']) version_text = "Version %s" % theme['version'] if has_update: - version_text += " (Installed: %s)" % themeManager.manifest[theme_name]['version'] + version_text += " (installed: %s)" % themeManager.manifest[theme_name]['version'] self.lbl_version.setText( version_text ) - self.lbl_requires.setText( ("Requires %s" % theme['inherits']) if theme['inherits'] else "") + self.lbl_requires.setText( ("Requires %s" % theme['inherits'] + (' (installed)' if theme['inherits'] in self.config.availableThemes() else '')) if theme['inherits'] else "") self.lbl_last_update.setText( "Released on: " + datetime.fromtimestamp(theme['updated']).strftime("%d/%m/%Y, %H:%M") ) @QtCore.pyqtSlot(dict) From 7f15a2d90fb7a9ce9bf613ad3dbcc905e79fd583 Mon Sep 17 00:00:00 2001 From: anne Date: Thu, 20 Jul 2023 22:32:03 +0200 Subject: [PATCH 05/11] ran black reformatter --- theme_repo_manager.py | 295 +++++++++++++++++++++++++++--------------- 1 file changed, 194 insertions(+), 101 deletions(-) diff --git a/theme_repo_manager.py b/theme_repo_manager.py index 9d7c597..2685a5b 100644 --- a/theme_repo_manager.py +++ b/theme_repo_manager.py @@ -1,5 +1,5 @@ import os -import io +import io import json import zipfile import logging @@ -11,14 +11,20 @@ from ostools import getDataDir try: from PyQt6 import QtCore, QtGui, QtWidgets, QtNetwork from PyQt6.QtGui import QAction + _flag_selectable = QtCore.Qt.TextInteractionFlag.TextSelectableByMouse - _flag_topalign = QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop + _flag_topalign = ( + QtCore.Qt.AlignmentFlag.AlignLeading + | QtCore.Qt.AlignmentFlag.AlignLeft + | QtCore.Qt.AlignmentFlag.AlignTop + ) except ImportError: print("PyQt5 fallback (thememanager.py)") from PyQt5 import QtCore, QtGui, QtWidgets, QtNetwork from PyQt5.QtWidgets import QAction + _flag_selectable = QtCore.Qt.TextSelectableByMouse - _flag_topalign = QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop + _flag_topalign = QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop PchumLog = logging.getLogger("pchumLogger") themeManager = None @@ -35,11 +41,11 @@ themeManager = None class ThemeManager(QtCore.QObject): # signals - theme_installed = QtCore.pyqtSignal(str) # theme name - zip_downloaded = QtCore.pyqtSignal(str, str) # theme name, zip location - database_refreshed = QtCore.pyqtSignal(dict) # self.manifest - manifest_updated = QtCore.pyqtSignal(dict) # self.manifest - errored = QtCore.pyqtSignal(str) # error_text + theme_installed = QtCore.pyqtSignal(str) # theme name + zip_downloaded = QtCore.pyqtSignal(str, str) # theme name, zip location + database_refreshed = QtCore.pyqtSignal(dict) # self.manifest + manifest_updated = QtCore.pyqtSignal(dict) # self.manifest + errored = QtCore.pyqtSignal(str) # error_text # variables manifest = {} @@ -50,9 +56,10 @@ class ThemeManager(QtCore.QObject): NAManager = None downloads = {} + def __init__(self, config): super().__init__() - with open(self.manifest_path, 'r') as f: + with open(self.manifest_path, "r") as f: self.manifest = json.load(f) PchumLog.debug("Manifest.js loaded with: %s" % self.manifest) self.config = config @@ -65,23 +72,27 @@ class ThemeManager(QtCore.QObject): def refresh_database(self): # Fetches a new copy of the theme database from the given URL # The initialisation & processing of it is handled in self._on_reply - PchumLog.debug("Refreshing theme repo database @ %s" % self.config.theme_repo_url()) - promise = self.NAManager.get(QtNetwork.QNetworkRequest(QtCore.QUrl(self.config.theme_repo_url()))) + PchumLog.debug( + "Refreshing theme repo database @ %s" % self.config.theme_repo_url() + ) + promise = self.NAManager.get( + QtNetwork.QNetworkRequest(QtCore.QUrl(self.config.theme_repo_url())) + ) def delete_theme(self, theme_name): # TODO: check if other installed themes inherit from this to avoid broken themes # would require some kinda confirmation popup which i havent figure out yet PchumLog.info("Deleting installed repo theme %s" % theme_name) theme = self.manifest[theme_name] - directory = os.path.join(getDataDir(), 'themes', theme['name']) + directory = os.path.join(getDataDir(), "themes", theme["name"]) if os.path.isdir(directory): rmtree(directory) self.manifest.pop(theme_name) self.save_manifest() self.manifest_updated.emit(self.manifest) - + def save_manifest(self): - with open(self.manifest_path, 'w') as f: + with open(self.manifest_path, "w") as f: json.dump(self.manifest, f) PchumLog.debug("Saved manifes.js to %s" % self.manifest_path) @@ -91,7 +102,10 @@ class ThemeManager(QtCore.QObject): all_themes = self.config.availableThemes() for theme_name in self.manifest: if not theme_name in all_themes: - PchumLog.warning("Supposedly installed theme %s from the manifest seems to have been deleted, removing from manifest now" % theme_name) + PchumLog.warning( + "Supposedly installed theme %s from the manifest seems to have been deleted, removing from manifest now" + % theme_name + ) self.manifest.pop(theme_name) def download_theme(self, theme_name): @@ -103,10 +117,16 @@ class ThemeManager(QtCore.QObject): if not theme_name in self.database_entries: PchumLog.error("Theme name %s does not exist in the database!" % theme_name) return - PchumLog.debug("(From %s)" % self.database_entries[theme_name]['download']) - promise = self.NAManager.get(QtNetwork.QNetworkRequest(QtCore.QUrl(self.database_entries[theme_name]['download']))) - self.downloads[self.database_entries[theme_name]['download']] = self.database_entries[theme_name] - + PchumLog.debug("(From %s)" % self.database_entries[theme_name]["download"]) + promise = self.NAManager.get( + QtNetwork.QNetworkRequest( + QtCore.QUrl(self.database_entries[theme_name]["download"]) + ) + ) + self.downloads[ + self.database_entries[theme_name]["download"] + ] = self.database_entries[theme_name] + def install_theme(self, theme_name, force_install=False): # A higher way to install a theme than download_theme # Checks if the theme is already installed & if its up to date @@ -124,38 +144,72 @@ class ThemeManager(QtCore.QObject): all_themes = self.config.availableThemes() theme = self.database_entries[theme_name] - if not self.is_installed(theme_name) and theme_name in all_themes: # Theme exists, but not installed by manager - PchumLog.warning("Theme %s is already installed manually. The manual version will get shadowed by the repository version & will not be usable" % theme_name) + if ( + not self.is_installed(theme_name) and theme_name in all_themes + ): # Theme exists, but not installed by manager + PchumLog.warning( + "Theme %s is already installed manually. The manual version will get shadowed by the repository version & will not be usable" + % theme_name + ) # Check depedencies - if theme['inherits'] != "": - if self.is_installed(theme['inherits']): + if theme["inherits"] != "": + if self.is_installed(theme["inherits"]): # Inherited theme is installed. A-OK - PchumLog.debug("Theme %s requires theme %s, which is already installed through the repository" % (theme_name, theme['inherits'])) - if theme['inherits'] in all_themes: + PchumLog.debug( + "Theme %s requires theme %s, which is already installed through the repository" + % (theme_name, theme["inherits"]) + ) + if theme["inherits"] in all_themes: # Inherited theme is manually installed. A-OK - PchumLog.debug("Theme %s requires theme %s, which is already installed manually by the user" % (theme_name, theme['inherits'])) - elif theme['inherits'] in self.database_entries: + PchumLog.debug( + "Theme %s requires theme %s, which is already installed manually by the user" + % (theme_name, theme["inherits"]) + ) + elif theme["inherits"] in self.database_entries: # The Inherited theme is not installed, but can be. A-OK - PchumLog.info("Theme %s requires theme %s, which will now be installed" % (theme_name, theme['inherits'])) - self.install_theme(theme['inherits']) + PchumLog.info( + "Theme %s requires theme %s, which will now be installed" + % (theme_name, theme["inherits"]) + ) + self.install_theme(theme["inherits"]) else: # Inherited theme is not installed, and can't be installed automatically. Exits unless force_install is True if force_install: - PchumLog.error("Theme %s requires theme %s, which is not installed and not in the database. Installing %s anyways, because force_install is True" % (theme_name, theme, theme_name['inherits'])) + PchumLog.error( + "Theme %s requires theme %s, which is not installed and not in the database. Installing %s anyways, because force_install is True" + % (theme_name, theme, theme_name["inherits"]) + ) else: - PchumLog.error("Theme %s requires theme %s, which is not installed and not in the database. Cancelling install" % (theme_name, theme['inherits'])) - self.errored.emit("Theme %s requires theme %s, which is not installed and not in the database. Cancelling install" % (theme_name, theme['inherits'])) + PchumLog.error( + "Theme %s requires theme %s, which is not installed and not in the database. Cancelling install" + % (theme_name, theme["inherits"]) + ) + self.errored.emit( + "Theme %s requires theme %s, which is not installed and not in the database. Cancelling install" + % (theme_name, theme["inherits"]) + ) return # Check if there's no need to re-install theme # This is done after the dependency check in case an inherited theme is missing two levels down - if self.is_installed(theme_name) and not self.has_update(theme_name): # Theme is installed by manager, and is up-to-date + if self.is_installed(theme_name) and not self.has_update( + theme_name + ): # Theme is installed by manager, and is up-to-date if force_install: - PchumLog.warning("Theme %s is already installed, and no update is available. Installing anyways, because force_install is True" % theme_name) + PchumLog.warning( + "Theme %s is already installed, and no update is available. Installing anyways, because force_install is True" + % theme_name + ) else: - PchumLog.warning("Theme %s is already installed, and no update is available. Cancelling install" % theme_name) - self.errored.emit("Theme %s is already installed, and no update is available. Cancelling install" % theme_name) + PchumLog.warning( + "Theme %s is already installed, and no update is available. Cancelling install" + % theme_name + ) + self.errored.emit( + "Theme %s is already installed, and no update is available. Cancelling install" + % theme_name + ) return # All is ok. or we're just ignoring the errors through force_install @@ -166,24 +220,33 @@ class ThemeManager(QtCore.QObject): # Has the given theme an update available # Returns False if the theme is installed manually or when the theme is up to date if self.is_installed(theme_name) and theme_name in self.database_entries: - return self.manifest[theme_name]['version'] < self.database_entries[theme_name]['version'] + return ( + self.manifest[theme_name]["version"] + < self.database_entries[theme_name]["version"] + ) return False - + def is_installed(self, theme_name): # checks if a theme is installed through the manager # Note that this will return False if the given name is a theme that the user installed manually! return theme_name in self.manifest def is_database_valid(self): - return 'entries' in self.database and isinstance(self.database.get('entries'), list) + return "entries" in self.database and isinstance( + self.database.get("entries"), list + ) # using a slot decorator here raises `ERROR - , connect() failed between finished(QNetworkReply*) and _on_reply()`` # maybe because of the underscore? # @QtCore.pyqtSlot(QtNetwork.QNetworkReply) def _on_reply(self, reply): if reply.error() != QtNetwork.QNetworkReply.NetworkError.NoError: - PchumLog.error("An error occured contacting the repository: %s" % reply.error()) - self.errored.emit("An error occured contacting the repository: %s" % reply.error()) + PchumLog.error( + "An error occured contacting the repository: %s" % reply.error() + ) + self.errored.emit( + "An error occured contacting the repository: %s" % reply.error() + ) return try: original_url = reply.request().url().url() @@ -191,12 +254,12 @@ class ThemeManager(QtCore.QObject): if original_url in self.downloads: # This is a theme zip! theme = self.downloads[original_url] - self._handle_downloaded_zip(bytes(reply.readAll()), theme['name']) + self._handle_downloaded_zip(bytes(reply.readAll()), theme["name"]) self.downloads.pop(original_url) - self.manifest[theme['name']] = theme + self.manifest[theme["name"]] = theme self.save_manifest() self.manifest_updated.emit(self.manifest) - PchumLog.info("Theme %s is now installed" % theme['name']) + PchumLog.info("Theme %s is now installed" % theme["name"]) else: # This is a database refresh! as_json = bytes(reply.readAll()).decode("utf-8") @@ -209,14 +272,19 @@ class ThemeManager(QtCore.QObject): return # Filter out non-QTchum client themes, like for godot - for dbindex in range(len(self.database['entries'])-1, -1, -1): # Iterate over the database in reverse - dbitem = self.database['entries'][dbindex] - if dbitem['client'] != "pesterchum": - PchumLog.debug("Removed database theme %s because it is not compatible with this client" % dbitem['name']) - self.database['entries'].pop(dbindex) + for dbindex in range( + len(self.database["entries"]) - 1, -1, -1 + ): # Iterate over the database in reverse + dbitem = self.database["entries"][dbindex] + if dbitem["client"] != "pesterchum": + PchumLog.debug( + "Removed database theme %s because it is not compatible with this client" + % dbitem["name"] + ) + self.database["entries"].pop(dbindex) # Make an easy lookup table instead of the array we get from the DB - for dbitem in self.database['entries']: - self.database_entries[dbitem['name']] = dbitem + for dbitem in self.database["entries"]: + self.database_entries[dbitem["name"]] = dbitem PchumLog.info("Database refreshed") self.database_refreshed.emit(self.database) @@ -231,13 +299,11 @@ class ThemeManager(QtCore.QObject): self.errored.emit("Vital key missing from theme database: %s" % e) return - - def _handle_downloaded_zip(self, zip_buffer, theme_name): # Unzips the downloaded theme package in-memory to datadir/themes/theme_name # I dont think this runs in a thread so it may block, but its so fast i dont think it really matters # But i guess if its a zip bomb itll crash - directory = os.path.join(getDataDir(), 'themes', theme_name) + directory = os.path.join(getDataDir(), "themes", theme_name) with zipfile.ZipFile(io.BytesIO(zip_buffer)) as z: if os.path.isdir(directory): rmtree(directory) @@ -245,16 +311,19 @@ class ThemeManager(QtCore.QObject): z.extractall(directory) - class ThemeManagerWidget(QtWidgets.QWidget): icons = None config = None rebuilt = QtCore.pyqtSignal() - + def __init__(self, config, parent=None): super().__init__(parent) - self.icons = [QtGui.QIcon("img/download_pending.png"), QtGui.QIcon("img/download_done.png"), QtGui.QIcon("img/download_update.png"), ] + self.icons = [ + QtGui.QIcon("img/download_pending.png"), + QtGui.QIcon("img/download_done.png"), + QtGui.QIcon("img/download_update.png"), + ] self.config = config global themeManager if themeManager == None or not themeManager.is_database_valid(): @@ -266,11 +335,10 @@ class ThemeManagerWidget(QtWidgets.QWidget): themeManager.database_refreshed.connect(self._on_database_refreshed) themeManager.manifest_updated.connect(self._on_database_refreshed) - def setupUI(self): self.layout_main = QtWidgets.QVBoxLayout(self) self.setLayout(self.layout_main) - + # Search bar # TODO: implement searching # self.line_search = QtWidgets.QLineEdit() @@ -291,8 +359,7 @@ class ThemeManagerWidget(QtWidgets.QWidget): self.list_results.itemClicked.connect(self._on_theme_selected) layout_hbox_list_and_details.addWidget(self.list_results) - - # This is the right side, has the install buttons & all the theme details of the selected item + # This is the right side, has the install buttons & all the theme details of the selected item layout_vbox_details = QtWidgets.QVBoxLayout() # The theme details are inside a scroll container in case of small window self.frame_scroll = QtWidgets.QScrollArea() @@ -304,20 +371,22 @@ class ThemeManagerWidget(QtWidgets.QWidget): ) # The vbox that the detail labels will rest in layout_vbox_scroll_insides = QtWidgets.QVBoxLayout() - + # here starts the actual detail labels # Selected theme's name self.lbl_theme_name = QtWidgets.QLabel("Click a theme to get started") self.lbl_theme_name.setTextInteractionFlags(_flag_selectable) - self.lbl_theme_name.setStyleSheet("QLabel { font-size: 16px; font-weight:bold;}") + self.lbl_theme_name.setStyleSheet( + "QLabel { font-size: 16px; font-weight:bold;}" + ) self.lbl_theme_name.setWordWrap(True) layout_vbox_scroll_insides.addWidget(self.lbl_theme_name) - + # Author name self.lbl_author_name = QtWidgets.QLabel("") self.lbl_author_name.setTextInteractionFlags(_flag_selectable) layout_vbox_scroll_insides.addWidget(self.lbl_author_name) - + # description. this needs to be the biggest self.lbl_description = QtWidgets.QLabel("") self.lbl_description.setTextInteractionFlags(_flag_selectable) @@ -330,13 +399,13 @@ class ThemeManagerWidget(QtWidgets.QWidget): self.lbl_description.setAlignment(_flag_topalign) self.lbl_description.setWordWrap(True) layout_vbox_scroll_insides.addWidget(self.lbl_description) - + # requires. shows up if a theme has "inherits" set & we dont have it installed self.lbl_requires = QtWidgets.QLabel("") self.lbl_requires.setTextInteractionFlags(_flag_selectable) self.lbl_requires.setWordWrap(True) layout_vbox_scroll_insides.addWidget(self.lbl_requires) - + # Version number. this will also show the current installed one if there is an update self.lbl_version = QtWidgets.QLabel("") self.lbl_version.setTextInteractionFlags(_flag_selectable) @@ -347,7 +416,7 @@ class ThemeManagerWidget(QtWidgets.QWidget): self.lbl_last_update.setWordWrap(True) self.lbl_last_update.setTextInteractionFlags(_flag_selectable) layout_vbox_scroll_insides.addWidget(self.lbl_last_update) - + # Theme details done, so we wont need the scroll after this self.frame_scroll.setLayout(layout_vbox_scroll_insides) layout_vbox_details.addWidget(self.frame_scroll) @@ -363,13 +432,15 @@ class ThemeManagerWidget(QtWidgets.QWidget): self.btn_install.clicked.connect(self._on_install_clicked) self.btn_install.setDisabled(True) layout_vbox_details.addWidget(self.btn_install) - + # Done with details layout_hbox_list_and_details.addLayout(layout_vbox_details) self.layout_main.addLayout(layout_hbox_list_and_details) self.btn_refresh = QtWidgets.QPushButton("Refresh", self) - self.btn_refresh.clicked.connect(themeManager.refresh_database) # Conneced to themeManager! + self.btn_refresh.clicked.connect( + themeManager.refresh_database + ) # Conneced to themeManager! self.layout_main.addWidget(self.btn_refresh) self.lbl_error = QtWidgets.QLabel("") @@ -377,46 +448,63 @@ class ThemeManagerWidget(QtWidgets.QWidget): self.lbl_error.setWordWrap(True) themeManager.errored.connect(self._on_fetch_error) self.lbl_error.setTextInteractionFlags(_flag_selectable) - self.lbl_error.setStyleSheet(" QLabel { background-color:black; color:red; font-size: 16px;}") + self.lbl_error.setStyleSheet( + " QLabel { background-color:black; color:red; font-size: 16px;}" + ) self.layout_main.addWidget(self.lbl_error) - def _on_fetch_error(self, text): self.lbl_error.setText(text) self.lbl_error.setVisible(True) def _on_uninstall_clicked(self): - theme = themeManager.database['entries'][self.list_results.currentRow()] - themeManager.delete_theme(theme['name']) + theme = themeManager.database["entries"][self.list_results.currentRow()] + themeManager.delete_theme(theme["name"]) def _on_install_clicked(self): - theme = themeManager.database['entries'][self.list_results.currentRow()] - themeManager.install_theme(theme['name']) - + theme = themeManager.database["entries"][self.list_results.currentRow()] + themeManager.install_theme(theme["name"]) + @QtCore.pyqtSlot(QtWidgets.QListWidgetItem) def _on_theme_selected(self, item): index = self.list_results.currentRow() - theme = themeManager.database['entries'][index] - theme_name = theme['name'] + theme = themeManager.database["entries"][index] + theme_name = theme["name"] is_installed = themeManager.is_installed(theme_name) has_update = themeManager.has_update(theme_name) self.btn_install.setDisabled(False) - self.btn_install.setText( "Update" if has_update else "Install" ) - self.btn_install.setVisible( (is_installed and has_update) or not is_installed ) - self.btn_uninstall.setVisible( themeManager.is_installed(theme_name) ) + self.btn_install.setText("Update" if has_update else "Install") + self.btn_install.setVisible((is_installed and has_update) or not is_installed) + self.btn_uninstall.setVisible(themeManager.is_installed(theme_name)) self.lbl_theme_name.setText(theme_name) - self.lbl_author_name.setText("By %s" % theme['author']) - self.lbl_description.setText(theme['description']) - version_text = "Version %s" % theme['version'] + self.lbl_author_name.setText("By %s" % theme["author"]) + self.lbl_description.setText(theme["description"]) + version_text = "Version %s" % theme["version"] if has_update: - version_text += " (installed: %s)" % themeManager.manifest[theme_name]['version'] - self.lbl_version.setText( version_text ) - self.lbl_requires.setText( ("Requires %s" % theme['inherits'] + (' (installed)' if theme['inherits'] in self.config.availableThemes() else '')) if theme['inherits'] else "") - self.lbl_last_update.setText( "Released on: " + datetime.fromtimestamp(theme['updated']).strftime("%d/%m/%Y, %H:%M") ) + version_text += ( + " (installed: %s)" % themeManager.manifest[theme_name]["version"] + ) + self.lbl_version.setText(version_text) + self.lbl_requires.setText( + ( + "Requires %s" % theme["inherits"] + + ( + " (installed)" + if theme["inherits"] in self.config.availableThemes() + else "" + ) + ) + if theme["inherits"] + else "" + ) + self.lbl_last_update.setText( + "Released on: " + + datetime.fromtimestamp(theme["updated"]).strftime("%d/%m/%Y, %H:%M") + ) @QtCore.pyqtSlot(dict) - def _on_database_refreshed(self,_): + def _on_database_refreshed(self, _): self.rebuild() def rebuild(self): @@ -430,29 +518,34 @@ class ThemeManagerWidget(QtWidgets.QWidget): self.lbl_error.setVisible(True) # Repopulate the list - for dbitem in database['entries']: + for dbitem in database["entries"]: # Determine the suffix icon = self.icons[0] - status = '' - if themeManager.is_installed(dbitem['name']): - if themeManager.has_update(dbitem['name']): - status = '~ (update available)' + status = "" + if themeManager.is_installed(dbitem["name"]): + if themeManager.has_update(dbitem["name"]): + status = "~ (update available)" icon = self.icons[2] else: - status = '~ (installed)' + status = "~ (installed)" icon = self.icons[1] - text = "%s by %s %s" % (dbitem['name'], dbitem['author'], status) + text = "%s by %s %s" % (dbitem["name"], dbitem["author"], status) item = QtWidgets.QListWidgetItem(icon, text) self.list_results.addItem(item) - + self.btn_install.setDisabled(True) - for lbl in [self.lbl_author_name, self.lbl_description, self.lbl_version, self.lbl_requires, self.lbl_last_update]: + for lbl in [ + self.lbl_author_name, + self.lbl_description, + self.lbl_version, + self.lbl_requires, + self.lbl_last_update, + ]: lbl.setText("") self.lbl_theme_name.setText("Click a theme to get started") self.btn_uninstall.setVisible(False) self.btn_install.setVisible(True) self.btn_install.setDisabled(True) - self.rebuilt.emit() - PchumLog.debug("Rebuilt emitted") \ No newline at end of file + PchumLog.debug("Rebuilt emitted") From 381631bb3917c75f946aef7c330991683a3f4f4a Mon Sep 17 00:00:00 2001 From: anne Date: Sun, 23 Jul 2023 20:13:03 +0200 Subject: [PATCH 06/11] made the pchumlog calls compliant --- theme_repo_manager.py | 99 +++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/theme_repo_manager.py b/theme_repo_manager.py index 2685a5b..a145ed4 100644 --- a/theme_repo_manager.py +++ b/theme_repo_manager.py @@ -61,7 +61,7 @@ class ThemeManager(QtCore.QObject): super().__init__() with open(self.manifest_path, "r") as f: self.manifest = json.load(f) - PchumLog.debug("Manifest.js loaded with: %s" % self.manifest) + PchumLog.debug("Manifest.js loaded with: %s", self.manifest) self.config = config self.NAManager = QtNetwork.QNetworkAccessManager() self.NAManager.finished[QtNetwork.QNetworkReply].connect(self._on_reply) @@ -73,7 +73,7 @@ class ThemeManager(QtCore.QObject): # Fetches a new copy of the theme database from the given URL # The initialisation & processing of it is handled in self._on_reply PchumLog.debug( - "Refreshing theme repo database @ %s" % self.config.theme_repo_url() + "Refreshing theme repo database @ %s", self.config.theme_repo_url() ) promise = self.NAManager.get( QtNetwork.QNetworkRequest(QtCore.QUrl(self.config.theme_repo_url())) @@ -82,7 +82,7 @@ class ThemeManager(QtCore.QObject): def delete_theme(self, theme_name): # TODO: check if other installed themes inherit from this to avoid broken themes # would require some kinda confirmation popup which i havent figure out yet - PchumLog.info("Deleting installed repo theme %s" % theme_name) + PchumLog.info("Deleting installed repo theme %s", theme_name) theme = self.manifest[theme_name] directory = os.path.join(getDataDir(), "themes", theme["name"]) if os.path.isdir(directory): @@ -94,7 +94,7 @@ class ThemeManager(QtCore.QObject): def save_manifest(self): with open(self.manifest_path, "w") as f: json.dump(self.manifest, f) - PchumLog.debug("Saved manifes.js to %s" % self.manifest_path) + PchumLog.debug("Saved manifes.js to %s", self.manifest_path) def validate_manifest(self): # Checks if the themes the manifest claims are installed actually exists @@ -103,8 +103,8 @@ class ThemeManager(QtCore.QObject): for theme_name in self.manifest: if not theme_name in all_themes: PchumLog.warning( - "Supposedly installed theme %s from the manifest seems to have been deleted, removing from manifest now" - % theme_name + "Supposedly installed theme %s from the manifest seems to have been deleted, removing from manifest now", + theme_name, ) self.manifest.pop(theme_name) @@ -113,11 +113,11 @@ class ThemeManager(QtCore.QObject): # The actual installing is handled by _on_reply when the theme is downloaded # Performs no version checks or dependency handling # Use install_theme() instead unless you know what you're doing - PchumLog.info("Downloading %s" % theme_name) + PchumLog.info("Downloading %s", theme_name) if not theme_name in self.database_entries: - PchumLog.error("Theme name %s does not exist in the database!" % theme_name) + PchumLog.error("Theme name %s does not exist in the database!", theme_name) return - PchumLog.debug("(From %s)" % self.database_entries[theme_name]["download"]) + PchumLog.debug("(From %s)", self.database_entries[theme_name]["download"]) promise = self.NAManager.get( QtNetwork.QNetworkRequest( QtCore.QUrl(self.database_entries[theme_name]["download"]) @@ -134,12 +134,12 @@ class ThemeManager(QtCore.QObject): # !! note that this does not check if theres a circular dependency !! # Setting force_install to True will install a given theme, even if it is deemed unnecessary to do so or its inherit dependency cannot be installed # This gives it the same no-nonsense operation as download_theme, but with the checks in place - PchumLog.info("Installing theme %s" % theme_name) + PchumLog.info("Installing theme %s", theme_name) if force_install: PchumLog.debug("(force_install is enabled)") if not theme_name in self.database_entries: - PchumLog.error("Theme %s does not exist in the database!" % theme_name) - self.errored.emit("Theme %s does not exist in the database!" % theme_name) + PchumLog.error("Theme %s does not exist in the database!", theme_name) + self.errored.emit("Theme %s does not exist in the database!", theme_name) return all_themes = self.config.availableThemes() @@ -148,8 +148,8 @@ class ThemeManager(QtCore.QObject): not self.is_installed(theme_name) and theme_name in all_themes ): # Theme exists, but not installed by manager PchumLog.warning( - "Theme %s is already installed manually. The manual version will get shadowed by the repository version & will not be usable" - % theme_name + "Theme %s is already installed manually. The manual version will get shadowed by the repository version & will not be usable", + theme_name, ) # Check depedencies @@ -157,37 +157,42 @@ class ThemeManager(QtCore.QObject): if self.is_installed(theme["inherits"]): # Inherited theme is installed. A-OK PchumLog.debug( - "Theme %s requires theme %s, which is already installed through the repository" - % (theme_name, theme["inherits"]) + "Theme %s requires theme %s, which is already installed through the repository", + theme_name, + theme["inherits"], ) if theme["inherits"] in all_themes: # Inherited theme is manually installed. A-OK PchumLog.debug( - "Theme %s requires theme %s, which is already installed manually by the user" - % (theme_name, theme["inherits"]) + "Theme %s requires theme %s, which is already installed manually by the user", + theme_name, + theme["inherits"], ) elif theme["inherits"] in self.database_entries: # The Inherited theme is not installed, but can be. A-OK PchumLog.info( - "Theme %s requires theme %s, which will now be installed" - % (theme_name, theme["inherits"]) + "Theme %s requires theme %s, which will now be installed", + theme_name, + theme["inherits"], ) self.install_theme(theme["inherits"]) else: # Inherited theme is not installed, and can't be installed automatically. Exits unless force_install is True if force_install: PchumLog.error( - "Theme %s requires theme %s, which is not installed and not in the database. Installing %s anyways, because force_install is True" - % (theme_name, theme, theme_name["inherits"]) + "Theme %s requires theme %s, which is not installed and not in the database. Installing %s anyways, because force_install is True", + (theme_name, theme, theme_name["inherits"]), ) else: PchumLog.error( - "Theme %s requires theme %s, which is not installed and not in the database. Cancelling install" - % (theme_name, theme["inherits"]) + "Theme %s requires theme %s, which is not installed and not in the database. Cancelling install", + theme_name, + theme["inherits"], ) self.errored.emit( - "Theme %s requires theme %s, which is not installed and not in the database. Cancelling install" - % (theme_name, theme["inherits"]) + "Theme %s requires theme %s, which is not installed and not in the database. Cancelling install", + theme_name, + theme["inherits"], ) return @@ -198,17 +203,17 @@ class ThemeManager(QtCore.QObject): ): # Theme is installed by manager, and is up-to-date if force_install: PchumLog.warning( - "Theme %s is already installed, and no update is available. Installing anyways, because force_install is True" - % theme_name + "Theme %s is already installed, and no update is available. Installing anyways, because force_install is True", + theme_name, ) else: PchumLog.warning( - "Theme %s is already installed, and no update is available. Cancelling install" - % theme_name + "Theme %s is already installed, and no update is available. Cancelling install", + theme_name, ) self.errored.emit( - "Theme %s is already installed, and no update is available. Cancelling install" - % theme_name + "Theme %s is already installed, and no update is available. Cancelling install", + theme_name, ) return @@ -242,10 +247,10 @@ class ThemeManager(QtCore.QObject): def _on_reply(self, reply): if reply.error() != QtNetwork.QNetworkReply.NetworkError.NoError: PchumLog.error( - "An error occured contacting the repository: %s" % reply.error() + "An error occured contacting the repository: %s", reply.error() ) self.errored.emit( - "An error occured contacting the repository: %s" % reply.error() + "An error occured contacting the repository: %s", reply.error() ) return try: @@ -259,7 +264,7 @@ class ThemeManager(QtCore.QObject): self.manifest[theme["name"]] = theme self.save_manifest() self.manifest_updated.emit(self.manifest) - PchumLog.info("Theme %s is now installed" % theme["name"]) + PchumLog.info("Theme %s is now installed", theme["name"]) else: # This is a database refresh! as_json = bytes(reply.readAll()).decode("utf-8") @@ -289,14 +294,14 @@ class ThemeManager(QtCore.QObject): self.database_refreshed.emit(self.database) except json.decoder.JSONDecodeError as e: - PchumLog.error("Could not decode theme database JSON: %s" % e) - self.errored.emit("Could not decode theme database JSON: %s" % e) + PchumLog.error("Could not decode theme database JSON: %s", e) + self.errored.emit("Could not decode theme database JSON: %s", e) return except KeyError as e: self.database = {} self.database_entries = {} - PchumLog.error("Vital key missing from theme database: %s" % e) - self.errored.emit("Vital key missing from theme database: %s" % e) + PchumLog.error("Vital key missing from theme database: %s", e) + self.errored.emit("Vital key missing from theme database: %s", e) return def _handle_downloaded_zip(self, zip_buffer, theme_name): @@ -326,7 +331,7 @@ class ThemeManagerWidget(QtWidgets.QWidget): ] self.config = config global themeManager - if themeManager == None or not themeManager.is_database_valid(): + if themeManager is None or not themeManager.is_database_valid(): themeManager = ThemeManager(config) self.setupUI() else: @@ -478,22 +483,24 @@ class ThemeManagerWidget(QtWidgets.QWidget): self.btn_uninstall.setVisible(themeManager.is_installed(theme_name)) self.lbl_theme_name.setText(theme_name) - self.lbl_author_name.setText("By %s" % theme["author"]) + self.lbl_author_name.setText("By %s", theme["author"]) self.lbl_description.setText(theme["description"]) - version_text = "Version %s" % theme["version"] + version_text = "Version %s", theme["version"] if has_update: version_text += ( - " (installed: %s)" % themeManager.manifest[theme_name]["version"] + " (installed: %s)", + themeManager.manifest[theme_name]["version"], ) self.lbl_version.setText(version_text) self.lbl_requires.setText( ( - "Requires %s" % theme["inherits"] + "Requires %s", + theme["inherits"] + ( " (installed)" if theme["inherits"] in self.config.availableThemes() else "" - ) + ), ) if theme["inherits"] else "" @@ -529,7 +536,7 @@ class ThemeManagerWidget(QtWidgets.QWidget): else: status = "~ (installed)" icon = self.icons[1] - text = "%s by %s %s" % (dbitem["name"], dbitem["author"], status) + text = "%s by %s %s", (dbitem["name"], dbitem["author"], status) item = QtWidgets.QListWidgetItem(icon, text) self.list_results.addItem(item) From ed81fd60d2839a913c703a9ca0db9ecbaea0645f Mon Sep 17 00:00:00 2001 From: anne Date: Sun, 23 Jul 2023 20:16:21 +0200 Subject: [PATCH 07/11] ran black on forgotten files --- menus.py | 6 ++++-- pesterchum.py | 4 ++-- user_profile.py | 6 ++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/menus.py b/menus.py index ea60480..ad6d6d8 100644 --- a/menus.py +++ b/menus.py @@ -1429,7 +1429,7 @@ class PesterOptions(QtWidgets.QDialog): layout_repo_url = QtWidgets.QHBoxLayout() self.repoUrlBox = QtWidgets.QLineEdit(self) self.repoUrlBox.setText(self.config.theme_repo_url()) - + layout_repo_url.addWidget(QtWidgets.QLabel("Theme repository db URL:")) layout_repo_url.addWidget(self.repoUrlBox) @@ -1455,6 +1455,7 @@ class PesterOptions(QtWidgets.QDialog): self.randomscheck.setEnabled(False) self.themeBox = QtWidgets.QComboBox(self) + def reset_themeBox(): avail_themes = self.config.availableThemes() PchumLog.debug("Resetting themeself.themeBox") @@ -1470,6 +1471,7 @@ class PesterOptions(QtWidgets.QDialog): QtWidgets.QSizePolicy.Policy.Minimum, ) ) + reset_themeBox() self.refreshtheme = QtWidgets.QPushButton("Refresh current theme", self) self.refreshtheme.clicked.connect(parent.themeSelectOverride) @@ -1480,7 +1482,7 @@ class PesterOptions(QtWidgets.QDialog): ) ) self.themeManager = ThemeManagerWidget(self.config) - self.themeManager.rebuilt.connect( reset_themeBox ) + self.themeManager.rebuilt.connect(reset_themeBox) # This makes it so that the themeBox gets updated when a theme is installed or removed through the repository self.ghostchum = QtWidgets.QCheckBox("Pesterdunk Ghostchum!!", self) self.ghostchum.setChecked(self.config.ghostchum()) diff --git a/pesterchum.py b/pesterchum.py index 6004f2d..00493ce 100755 --- a/pesterchum.py +++ b/pesterchum.py @@ -3104,8 +3104,8 @@ class PesterWindow(MovingWindow): # theme repo url repourlsetting = self.optionmenu.repoUrlBox.text() if repourlsetting != self.config.theme_repo_url(): - self.config.set('theme_repo_url', repourlsetting) - + self.config.set("theme_repo_url", repourlsetting) + # theme ghostchumsetting = self.optionmenu.ghostchum.isChecked() curghostchum = self.config.ghostchum() diff --git a/user_profile.py b/user_profile.py index be563fe..a56f5d1 100644 --- a/user_profile.py +++ b/user_profile.py @@ -362,7 +362,10 @@ with a backup from: %s" return self.config.get("irc_compatibility_mode", False) def theme_repo_url(self): - return self.config.get('theme_repo_url', 'https://raw.githubusercontent.com/mocchapi/pesterchum-themes/main/db.json') + return self.config.get( + "theme_repo_url", + "https://raw.githubusercontent.com/mocchapi/pesterchum-themes/main/db.json", + ) def force_prefix(self): return self.config.get("force_prefix", True) @@ -785,7 +788,6 @@ class userProfile: def getTheme(self): return self.theme - def getAutoIdentify(self): return self.autoidentify From 95060f9bddb795b6efde4735d366c9895abc3d3d Mon Sep 17 00:00:00 2001 From: anne Date: Sun, 23 Jul 2023 20:22:31 +0200 Subject: [PATCH 08/11] a few logs snuck past me >:( --- theme_repo_manager.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/theme_repo_manager.py b/theme_repo_manager.py index a145ed4..4064661 100644 --- a/theme_repo_manager.py +++ b/theme_repo_manager.py @@ -181,7 +181,9 @@ class ThemeManager(QtCore.QObject): if force_install: PchumLog.error( "Theme %s requires theme %s, which is not installed and not in the database. Installing %s anyways, because force_install is True", - (theme_name, theme, theme_name["inherits"]), + theme_name, + theme, + theme_name["inherits"], ) else: PchumLog.error( @@ -283,8 +285,8 @@ class ThemeManager(QtCore.QObject): dbitem = self.database["entries"][dbindex] if dbitem["client"] != "pesterchum": PchumLog.debug( - "Removed database theme %s because it is not compatible with this client" - % dbitem["name"] + "Removed database theme %s because it is not compatible with this client", + dbitem["name"], ) self.database["entries"].pop(dbindex) # Make an easy lookup table instead of the array we get from the DB From d3ee9d07e0b4177749bd6fc92dbd580ee2b17a0d Mon Sep 17 00:00:00 2001 From: anne Date: Wed, 26 Jul 2023 16:09:48 +0200 Subject: [PATCH 09/11] Fixed some embarassing bugs wrt to %s strings outside logging, & made theme selections persist between list rebuilds --- theme_repo_manager.py | 85 +++++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/theme_repo_manager.py b/theme_repo_manager.py index 4064661..512f4df 100644 --- a/theme_repo_manager.py +++ b/theme_repo_manager.py @@ -139,7 +139,7 @@ class ThemeManager(QtCore.QObject): PchumLog.debug("(force_install is enabled)") if not theme_name in self.database_entries: PchumLog.error("Theme %s does not exist in the database!", theme_name) - self.errored.emit("Theme %s does not exist in the database!", theme_name) + self.errored.emit("Theme %s does not exist in the database!" % theme_name) return all_themes = self.config.availableThemes() @@ -192,9 +192,8 @@ class ThemeManager(QtCore.QObject): theme["inherits"], ) self.errored.emit( - "Theme %s requires theme %s, which is not installed and not in the database. Cancelling install", - theme_name, - theme["inherits"], + "Theme %s requires theme %s, which is not installed and not in the database. Cancelling install" + % (theme_name, theme["inherits"]) ) return @@ -214,8 +213,8 @@ class ThemeManager(QtCore.QObject): theme_name, ) self.errored.emit( - "Theme %s is already installed, and no update is available. Cancelling install", - theme_name, + "Theme %s is already installed, and no update is available. Cancelling install" + % theme_name ) return @@ -252,7 +251,7 @@ class ThemeManager(QtCore.QObject): "An error occured contacting the repository: %s", reply.error() ) self.errored.emit( - "An error occured contacting the repository: %s", reply.error() + "An error occured contacting the repository: %s" % reply.error() ) return try: @@ -297,13 +296,13 @@ class ThemeManager(QtCore.QObject): except json.decoder.JSONDecodeError as e: PchumLog.error("Could not decode theme database JSON: %s", e) - self.errored.emit("Could not decode theme database JSON: %s", e) + self.errored.emit("Could not decode theme database JSON: %s" % e) return except KeyError as e: self.database = {} self.database_entries = {} PchumLog.error("Vital key missing from theme database: %s", e) - self.errored.emit("Vital key missing from theme database: %s", e) + self.errored.emit("Vital key missing from theme database: %s" % e) return def _handle_downloaded_zip(self, zip_buffer, theme_name): @@ -485,38 +484,32 @@ class ThemeManagerWidget(QtWidgets.QWidget): self.btn_uninstall.setVisible(themeManager.is_installed(theme_name)) self.lbl_theme_name.setText(theme_name) - self.lbl_author_name.setText("By %s", theme["author"]) + self.lbl_author_name.setText("By %s" % theme["author"]) self.lbl_description.setText(theme["description"]) - version_text = "Version %s", theme["version"] + version_text = "Version %s" % theme["version"] if has_update: version_text += ( - " (installed: %s)", - themeManager.manifest[theme_name]["version"], + " (installed: %s)" % themeManager.manifest[theme_name]["version"] ) self.lbl_version.setText(version_text) - self.lbl_requires.setText( - ( - "Requires %s", - theme["inherits"] - + ( - " (installed)" - if theme["inherits"] in self.config.availableThemes() - else "" - ), - ) - if theme["inherits"] - else "" - ) - self.lbl_last_update.setText( - "Released on: " - + datetime.fromtimestamp(theme["updated"]).strftime("%d/%m/%Y, %H:%M") + requires_text = "" + if theme["inherits"]: + requires_text += "Requires %s" % theme["inherits"] + if theme["inherits"] in self.config.availableThemes(): + requires_text += " (installed)" + self.lbl_requires.setText((requires_text) if theme["inherits"] else "") + last_update_text = "Released on: " + last_update_text += datetime.fromtimestamp(theme["updated"]).strftime( + "%d/%m/%Y, %H:%M" ) + self.lbl_last_update.setText(last_update_text) @QtCore.pyqtSlot(dict) def _on_database_refreshed(self, _): self.rebuild() def rebuild(self): + prev_selected_index = self.list_results.currentRow() database = themeManager.database self.list_results.clear() self.lbl_error.setText("") @@ -538,23 +531,29 @@ class ThemeManagerWidget(QtWidgets.QWidget): else: status = "~ (installed)" icon = self.icons[1] - text = "%s by %s %s", (dbitem["name"], dbitem["author"], status) + text = "%s by %s %s" % (dbitem["name"], dbitem["author"], status) item = QtWidgets.QListWidgetItem(icon, text) self.list_results.addItem(item) - self.btn_install.setDisabled(True) - for lbl in [ - self.lbl_author_name, - self.lbl_description, - self.lbl_version, - self.lbl_requires, - self.lbl_last_update, - ]: - lbl.setText("") - self.lbl_theme_name.setText("Click a theme to get started") - self.btn_uninstall.setVisible(False) - self.btn_install.setVisible(True) - self.btn_install.setDisabled(True) + if prev_selected_index > -1: + # Re-select last item, if it was selected + self.list_results.setCurrentRow(prev_selected_index) + self._on_theme_selected(self.list_results.currentItem()) + else: + # Return sidebar info panel to defaults if nothing was selected + self.btn_install.setDisabled(True) + for lbl in [ + self.lbl_author_name, + self.lbl_description, + self.lbl_version, + self.lbl_requires, + self.lbl_last_update, + ]: + lbl.setText("") + self.lbl_theme_name.setText("Click a theme to get started") + self.btn_uninstall.setVisible(False) + self.btn_install.setVisible(True) + self.btn_install.setDisabled(True) self.rebuilt.emit() PchumLog.debug("Rebuilt emitted") From ea4ffe2c9330ffbd43ac1c671b9af7e659ab1ce6 Mon Sep 17 00:00:00 2001 From: anne Date: Fri, 1 Sep 2023 23:15:50 +0200 Subject: [PATCH 10/11] Fixed popping inside iteration & fixed themes folder not getting created --- ostools.py | 4 ++-- theme_repo_manager.py | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ostools.py b/ostools.py index fd63f32..83e198f 100644 --- a/ostools.py +++ b/ostools.py @@ -52,14 +52,14 @@ def validateDataDir(): errorlogs = os.path.join(datadir, "errorlogs") backup = os.path.join(datadir, "backup") themes = os.path.join(datadir, "themes") - # ~lisanne Datadir/themes for repository themes + # ~lisanne `datadir/themes` is for repository installed themes # Apparently everything checks this folder for themes already # So hopefully im not plugging into an existng system on accident js_pchum = os.path.join(datadir, "pesterchum.js") js_manifest = os.path.join(datadir, "manifest.js") - dirs = [datadir, profile, quirks, logs, errorlogs, backup] + dirs = [datadir, profile, quirks, logs, themes, errorlogs, backup] for d in dirs: if not os.path.isdir(d) or not os.path.exists(d): os.makedirs(d, exist_ok=True) diff --git a/theme_repo_manager.py b/theme_repo_manager.py index 512f4df..ce57a95 100644 --- a/theme_repo_manager.py +++ b/theme_repo_manager.py @@ -63,6 +63,9 @@ class ThemeManager(QtCore.QObject): self.manifest = json.load(f) PchumLog.debug("Manifest.js loaded with: %s", self.manifest) self.config = config + # TODO: maybe make seperate QNetworkAccessManagers for theme downloads, database fetches, and integrity checkfile + # OR figure out how to connect the signal to tasks instead of the whole object + # That way we dont have to figure out what got downloaded afterwards, and can just have a _on_reply_theme & _on_reply_database or something self.NAManager = QtNetwork.QNetworkAccessManager() self.NAManager.finished[QtNetwork.QNetworkReply].connect(self._on_reply) self.validate_manifest() @@ -99,6 +102,7 @@ class ThemeManager(QtCore.QObject): def validate_manifest(self): # Checks if the themes the manifest claims are installed actually exists # Removes them from the manifest if they dont + to_pop = set() all_themes = self.config.availableThemes() for theme_name in self.manifest: if not theme_name in all_themes: @@ -106,7 +110,11 @@ class ThemeManager(QtCore.QObject): "Supposedly installed theme %s from the manifest seems to have been deleted, removing from manifest now", theme_name, ) - self.manifest.pop(theme_name) + # Cannot be popped while iterating! + to_pop.add(theme_name) + + for theme_name in to_pop: + self.manifest.pop(theme_name) def download_theme(self, theme_name): # Downloads the theme .zip From 7ee7bd8f77a291a3e3e65fc9cfc90bd6ad401b26 Mon Sep 17 00:00:00 2001 From: anne Date: Fri, 1 Sep 2023 23:22:09 +0200 Subject: [PATCH 11/11] fixed the theme info & buttons not updating when changing selection in some circumstances --- theme_repo_manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/theme_repo_manager.py b/theme_repo_manager.py index ce57a95..68cd4c1 100644 --- a/theme_repo_manager.py +++ b/theme_repo_manager.py @@ -370,7 +370,7 @@ class ThemeManagerWidget(QtWidgets.QWidget): QtWidgets.QSizePolicy.Policy.Expanding, ) ) - self.list_results.itemClicked.connect(self._on_theme_selected) + self.list_results.itemSelectionChanged.connect(self._on_theme_selected) layout_hbox_list_and_details.addWidget(self.list_results) # This is the right side, has the install buttons & all the theme details of the selected item @@ -479,8 +479,8 @@ class ThemeManagerWidget(QtWidgets.QWidget): theme = themeManager.database["entries"][self.list_results.currentRow()] themeManager.install_theme(theme["name"]) - @QtCore.pyqtSlot(QtWidgets.QListWidgetItem) - def _on_theme_selected(self, item): + @QtCore.pyqtSlot() + def _on_theme_selected(self): index = self.list_results.currentRow() theme = themeManager.database["entries"][index] theme_name = theme["name"] @@ -546,7 +546,7 @@ class ThemeManagerWidget(QtWidgets.QWidget): if prev_selected_index > -1: # Re-select last item, if it was selected self.list_results.setCurrentRow(prev_selected_index) - self._on_theme_selected(self.list_results.currentItem()) + self._on_theme_selected() else: # Return sidebar info panel to defaults if nothing was selected self.btn_install.setDisabled(True)