From e6e8f4c1ef4572747e51d7df192981ce4e4ce745 Mon Sep 17 00:00:00 2001 From: anne Date: Fri, 14 Jul 2023 15:09:56 +0200 Subject: [PATCH] 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) -