diff --git a/Addon.py b/Addon.py index 035a65a0..9a01741d 100644 --- a/Addon.py +++ b/Addon.py @@ -47,6 +47,7 @@ from addonmanager_metadata import ( Metadata, MetadataReader, + License, UrlType, Version, DependencyType, @@ -220,7 +221,7 @@ def __init__( self.python_min_version = Version(from_list=[3, 0, 0]) self._icon_file = None - self._cached_license: str = "" + self._cached_license: str | List[License | str] = "" self._cached_update_date = None def __eq__(self, other): diff --git a/Widgets/addonmanager_widget_package_details_view.py b/Widgets/addonmanager_widget_package_details_view.py index b45eb6d7..5eebc520 100644 --- a/Widgets/addonmanager_widget_package_details_view.py +++ b/Widgets/addonmanager_widget_package_details_view.py @@ -22,11 +22,12 @@ # *************************************************************************** from dataclasses import dataclass +from typing import Optional, Dict from enum import Enum, auto import os -from typing import Optional from addonmanager_freecad_interface import translate +from addonmanager_readme_controller import TabView from PySideWrapper import QtCore, QtWidgets @@ -71,6 +72,7 @@ class PackageDetailsView(QtWidgets.QWidget): def __init__(self, parent: QtWidgets.QWidget = None): super().__init__(parent) self.button_bar = None + self.license_browser = None self.readme_browser = None self.message_label = None self.location_label = None @@ -83,12 +85,26 @@ def __init__(self, parent: QtWidgets.QWidget = None): self.installed_branch = None self.installed_timestamp = None self.can_disable = True + self.tabs: Dict[TabView, int] = {} self._setup_ui() def _setup_ui(self): self.vertical_layout = QtWidgets.QVBoxLayout(self) self.button_bar = WidgetAddonButtons(self) + + self.license_browser = WidgetReadmeBrowser(self) self.readme_browser = WidgetReadmeBrowser(self) + + self.tabs_widget = QtWidgets.QTabWidget(self) + + self.tabs[TabView.Readme] = self.tabs_widget.addTab( + self.readme_browser, translate("AddonsInstaller", "Overview") + ) + + self.tabs[TabView.License] = self.tabs_widget.addTab( + self.license_browser, translate("AddonsInstaller", "License") + ) + self.message_label = QtWidgets.QLabel(self) self.location_label = QtWidgets.QLabel(self) self.url_label = QtWidgets.QLabel(self) @@ -98,7 +114,7 @@ def _setup_ui(self): self.vertical_layout.addWidget(self.message_label) self.vertical_layout.addWidget(self.location_label) self.vertical_layout.addWidget(self.url_label) - self.vertical_layout.addWidget(self.readme_browser) + self.vertical_layout.addWidget(self.tabs_widget) self.button_bar.hide() # Start with no bar def set_location(self, location: Optional[str]): diff --git a/addonmanager_package_details_controller.py b/addonmanager_package_details_controller.py index 0a9eda08..be8e8c4b 100644 --- a/addonmanager_package_details_controller.py +++ b/addonmanager_package_details_controller.py @@ -33,11 +33,15 @@ get_branch_from_metadata, get_repo_url_from_metadata, ) +from Widgets.addonmanager_widget_package_details_view import ( + PackageDetailsView, + UpdateInformation, + WarningFlags, +) +from addonmanager_readme_controller import ReadmeController, TabView from addonmanager_workers_startup import CheckSingleUpdateWorker from addonmanager_git import GitManager, NoGitFound from Addon import Addon -from addonmanager_readme_controller import ReadmeController -from Widgets.addonmanager_widget_package_details_view import UpdateInformation, WarningFlags translate = fci.translate @@ -52,10 +56,13 @@ class PackageDetailsController(QtCore.QObject): execute = QtCore.Signal(Addon) update_status = QtCore.Signal(Addon) - def __init__(self, widget=None): + def __init__(self, widget: PackageDetailsView): super().__init__() self.ui = widget + + self.license_controller = ReadmeController(self.ui.license_browser) self.readme_controller = ReadmeController(self.ui.readme_browser) + self.worker = None self.addon = None self.update_check_thread = None @@ -79,9 +86,21 @@ def __init__(self, widget=None): def show_addon(self, addon: Addon) -> None: """The main entry point for this class shows the package details and related buttons for the provided repo.""" + self.addon = addon - self.readme_controller.set_addon(addon) + + has_license = bool(self.addon.license) + + self.ui.tabs_widget.setTabVisible(self.ui.tabs[TabView.License], has_license) + + if has_license: + self.license_controller.set_addon(addon, TabView.License) + + self.readme_controller.set_addon(addon, TabView.Readme) + self.ui.tabs_widget.setCurrentIndex(TabView.Readme) + self.original_disabled_state = self.addon.is_disabled() + if addon is not None: self.ui.button_bar.show() if addon.repo_type == Addon.Kind.MACRO: diff --git a/addonmanager_readme_controller.py b/addonmanager_readme_controller.py index 36bc2726..e32d76e3 100644 --- a/addonmanager_readme_controller.py +++ b/addonmanager_readme_controller.py @@ -27,11 +27,13 @@ import addonmanager_utilities as utils import addonmanager_freecad_interface as fci -from enum import IntEnum -from typing import Optional +from typing import TypedDict, Optional, Dict +from enum import IntEnum, auto import NetworkManager -from addonmanager_metadata import UrlType +from Widgets.addonmanager_widget_readme_browser import WidgetReadmeBrowser +from addonmanager_metadata import UrlType, License +from PySideWrapper import QtWidgets translate = fci.translate @@ -44,17 +46,31 @@ class ReadmeDataType(IntEnum): Html = 2 +class TabView(IntEnum): + License = auto() + Readme = 0 + + +class LicenseRequest(TypedDict): + license: License + text: QtWidgets.QTextEdit + + class ReadmeController(QtCore.QObject): """A class that can provide README data from an Addon, possibly loading external resources such as images""" - def __init__(self, widget): + def __init__(self, widget: WidgetReadmeBrowser): super().__init__() NetworkManager.InitializeNetworkManager() NetworkManager.AM_NETWORK_MANAGER.completed.connect(self._download_completed) self.readme_request_index = 0 + self.resource_requests = {} self.resource_failures = [] + + self.license_requests: Dict[int, LicenseRequest] = {} + self.url = "" self.readme_data = None self.readme_data_type = None @@ -64,16 +80,16 @@ def __init__(self, widget): self.widget.load_resource.connect(self.loadResource) self.widget.follow_link.connect(self.follow_link) - def set_addon(self, repo: Addon): + def set_addon(self, addon: Addon, view: TabView): """Set which Addon's information is displayed""" - self.addon = repo + self.addon = addon self.stop = False self.readme_data = None if self.addon.repo_type == Addon.Kind.MACRO: self._create_wiki_display() else: - self._create_non_wiki_display() + self._create_non_wiki_display(view) def _download_completed(self, index: int, code: int, data: QtCore.QByteArray) -> None: """Callback for handling a completed README file download.""" @@ -103,7 +119,29 @@ def _download_completed(self, index: int, code: int, data: QtCore.QByteArray) -> else: self.widget.setText(self.readme_data) else: - self.set_addon(self.addon) # Trigger a reload of the page now with resources + self.set_addon( + self.addon, TabView.Readme + ) # Trigger a reload of the page now with resources + + elif index in self.license_requests: + + entry = self.license_requests.get(index) + + if not entry: + return + + text: None | str = None + + if code == 200: + text = data.data().decode("utf-8") + else: + fci.Console.PrintLog( + f'Failed to fetch license. Name : "{ entry["license"].name }" , File : "{ entry["license"].file }"' + ) + + text = text or entry["license"].name + + entry["text"].setText(text) def _process_package_download(self, data: str): self.readme_data = data @@ -114,6 +152,16 @@ def _process_resource_download(self, resource_name: str, resource_data: bytes): image = QtGui.QImage.fromData(resource_data) self.widget.set_resource(resource_name, image) + def loadLicense(self, license: License, text: QtWidgets.QTextEdit): + + url = utils.construct_git_url(self.addon, license.file) + + manager = NetworkManager.AM_NETWORK_MANAGER + + index = manager.submit_unmonitored_get(url) + + self.license_requests[index] = {"license": license, "text": text} + def loadResource(self, full_url: str): if full_url not in self.resource_failures: index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(full_url) @@ -182,47 +230,96 @@ def _create_wiki_display(self): self.readme_data_type = ReadmeDataType.Markdown self.widget.setMarkdown(markdown) - def _create_non_wiki_display(self): - self.url = utils.get_readme_url(self.addon) - if self.addon.metadata and self.addon.metadata.url: - for url in self.addon.metadata.url: - if url.type == UrlType.readme: - if self.url != url.location: - fci.Console.PrintLog("README url does not match expected location\n") - fci.Console.PrintLog(f"Expected: {self.url}\n") - fci.Console.PrintLog(f"package.xml contents: {url.location}\n") - fci.Console.PrintLog( - "Note to addon devs: package.xml now expects a" - " url to the raw MD data, now that Qt can render" - " it without having it transformed to HTML.\n" - ) - self.url = url.location - if "/blob/" in self.url: - fci.Console.PrintLog("Attempting to replace 'blob' with 'raw'...\n") - self.url = self.url.replace("/blob/", "/raw/") - elif "/src/" in self.url and "codeberg" in self.url: - fci.Console.PrintLog( - "Attempting to replace 'src' with 'raw' in codeberg URL..." - ) - self.url = self.url.replace("/src/", "/raw/") - - self.widget.setUrl(self.url) - - self.widget.setText( - translate("AddonsInstaller", "Loading page for {} from {}...").format( - self.addon.display_name, self.url - ) - ) + def _create_non_wiki_display(self, view: TabView): - if self.url[0] == "/": - if self.url.lower().endswith(".md"): - self.readme_data_type = ReadmeDataType.Markdown - elif self.url.lower().endswith(".html"): - self.readme_data_type = ReadmeDataType.Html + if view == TabView.License: - with open(self.url, "r") as fd: - self._process_package_download("".join(fd.readlines())) - else: - self.readme_request_index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get( - self.url + addon = self.addon + + if not addon: + return + + licenses = addon.license + + layout = self.widget.layout() + + if not layout: + + layout = QtWidgets.QVBoxLayout() + + self.widget.setLayout(layout) + + while layout.count(): + + item = layout.takeAt(0) + widget = item.widget() + widget.setParent(None) + + if type(licenses) is list: + + for license in licenses: + + text = QtWidgets.QTextEdit() + + text.setText(translate("AddonsInstaller", "Loading license..")) + + layout.addWidget(text) + + if type(license) is License: + self.loadLicense(license, text) + elif type(license) is str: + text.setText(license) + + elif type(licenses) is str: + + browser = QtWidgets.QTextBrowser() + browser.setText(licenses) + + layout.addWidget(browser) + + elif view == TabView.Readme: + + self.url = utils.get_readme_url(self.addon) + + if self.addon.metadata and self.addon.metadata.url: + for url in self.addon.metadata.url: + if url.type == UrlType.readme: + if self.url != url.location: + fci.Console.PrintLog("README url does not match expected location\n") + fci.Console.PrintLog(f"Expected: {self.url}\n") + fci.Console.PrintLog(f"package.xml contents: {url.location}\n") + fci.Console.PrintLog( + "Note to addon devs: package.xml now expects a" + " url to the raw MD data, now that Qt can render" + " it without having it transformed to HTML.\n" + ) + self.url = url.location + if "/blob/" in self.url: + fci.Console.PrintLog("Attempting to replace 'blob' with 'raw'...\n") + self.url = self.url.replace("/blob/", "/raw/") + elif "/src/" in self.url and "codeberg" in self.url: + fci.Console.PrintLog( + "Attempting to replace 'src' with 'raw' in codeberg URL..." + ) + self.url = self.url.replace("/src/", "/raw/") + + self.widget.setUrl(self.url) + + self.widget.setText( + translate("AddonsInstaller", "Loading page for {} from {}...").format( + self.addon.display_name, self.url + ) ) + + if self.url[0] == "/": + if self.url.lower().endswith(".md"): + self.readme_data_type = ReadmeDataType.Markdown + elif self.url.lower().endswith(".html"): + self.readme_data_type = ReadmeDataType.Html + + with open(self.url, "r") as fd: + self._process_package_download("".join(fd.readlines())) + else: + self.readme_request_index = ( + NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(self.url) + )