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")