示例#1
0
    def show_macro(self, repo: Addon) -> None:
        """loads information of a given macro"""

        if not repo.macro.url:
            # We need to populate the macro information... may as well do it while the user reads the wiki page
            self.worker = GetMacroDetailsWorker(repo)
            self.worker.readme_updated.connect(self.macro_readme_updated)
            self.worker.start()
        else:
            self.macro_readme_updated()
示例#2
0
    def show_macro(self, repo: AddonManagerRepo) -> None:
        """loads information of a given macro"""

        if not self.show_cached_readme(repo):
            self.ui.textBrowserReadMe.setText(
                translate("AddonsInstaller",
                          "Fetching README.md from package repository"))
            self.worker = GetMacroDetailsWorker(repo)
            self.worker.readme_updated.connect(
                lambda desc: self.cache_readme(repo, desc))
            self.worker.readme_updated.connect(
                lambda desc: self.ui.textBrowserReadMe.setText(desc))
            self.worker.start()
示例#3
0
    def show_macro(self, repo: AddonManagerRepo) -> None:
        """loads information of a given macro"""

        if HAS_QTWEBENGINE:
            self.ui.webView.load(QUrl(repo.macro.url))
            self.ui.urlBar.setText(repo.macro.url)
        else:
            readme_data = NetworkManager.AM_NETWORK_MANAGER.blocking_get(
                repo.macro.url)
            text = readme_data.data().decode("utf8")
            self.ui.textBrowserReadMe.setHtml(text)

        # We need to populate the macro information... may as well do it while the user reads the wiki page
        self.worker = GetMacroDetailsWorker(repo)
        self.worker.start()
示例#4
0
    def show_package(self, repo: AddonManagerRepo) -> None:
        """Show the details for a package (a repo with a package.xml metadata file)"""

        if not self.show_cached_readme(repo):
            self.ui.textBrowserReadMe.setText(
                translate("AddonsInstaller",
                          "Fetching README.md from package repository"))
            self.worker = ShowWorker(repo, PackageDetails.cache_path(repo))
            self.worker.readme_updated.connect(
                lambda desc: self.cache_readme(repo, desc))
            self.worker.readme_updated.connect(
                lambda desc: self.ui.textBrowserReadMe.setText(desc))
            self.worker.update_status.connect(self.update_status.emit)
            self.worker.update_status.connect(self.show)
            self.worker.start()
示例#5
0
    def show_workbench(self, repo: AddonManagerRepo) -> None:
        """loads information of a given workbench"""

        if not self.show_cached_readme(repo):
            self.ui.textBrowserReadMe.setText(
                translate("AddonsInstaller",
                          "Fetching README.md from package repository"))
            self.worker = ShowWorker(repo, PackageDetails.cache_path(repo))
            self.worker.readme_updated.connect(
                lambda desc: self.cache_readme(repo, desc))
            self.worker.readme_updated.connect(
                lambda desc: self.ui.textBrowserReadMe.setText(desc))
            self.worker.update_status.connect(self.update_status.emit)
            self.worker.update_status.connect(self.show)
            self.worker.start()
示例#6
0
class PackageDetails(QWidget):

    back = Signal()
    install = Signal(AddonManagerRepo)
    uninstall = Signal(AddonManagerRepo)
    update = Signal(AddonManagerRepo)
    execute = Signal(AddonManagerRepo)
    update_status = Signal(AddonManagerRepo)
    check_for_update = Signal(AddonManagerRepo)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_PackageDetails()
        self.ui.setupUi(self)

        self.worker = None
        self.repo = None
        self.status_update_thread = None

        self.ui.buttonBack.clicked.connect(self.back.emit)
        self.ui.buttonExecute.clicked.connect(
            lambda: self.execute.emit(self.repo))
        self.ui.buttonInstall.clicked.connect(
            lambda: self.install.emit(self.repo))
        self.ui.buttonUninstall.clicked.connect(
            lambda: self.uninstall.emit(self.repo))
        self.ui.buttonUpdate.clicked.connect(
            lambda: self.update.emit(self.repo))
        self.ui.buttonCheckForUpdate.clicked.connect(
            lambda: self.check_for_update.emit(self.repo))
        self.ui.buttonChangeBranch.clicked.connect(self.change_branch_clicked)
        if HAS_QTWEBENGINE:
            self.ui.webView.loadStarted.connect(self.load_started)
            self.ui.webView.loadProgress.connect(self.load_progress)
            self.ui.webView.loadFinished.connect(self.load_finished)

            loading_html_file = os.path.join(os.path.dirname(__file__),
                                             "loading.html")
            with open(loading_html_file, "r", errors="ignore") as f:
                html = f.read()
                self.ui.loadingLabel.setHtml(html)
                self.ui.loadingLabel.show()
                self.ui.webView.hide()

    def show_repo(self, repo: AddonManagerRepo, reload: bool = False) -> None:

        # If this is the same repo we were already showing, we do not have to do the
        # expensive refetch unless reload is true
        if self.repo != repo or reload:
            self.repo = repo

            if HAS_QTWEBENGINE:
                self.ui.loadingLabel.show()
                self.ui.slowLoadLabel.hide()
                self.ui.webView.setHtml("<html><body>Loading...</body></html>")
                self.ui.webView.hide()
                self.ui.progressBar.show()
                self.timeout = QTimer.singleShot(
                    6000, self.long_load_running)  # Six seconds
            else:
                self.ui.missingWebViewLabel.setStyleSheet(
                    "color:" + utils.warning_color_string())

            if self.worker is not None:
                if not self.worker.isFinished():
                    self.worker.requestInterruption()
                    self.worker.wait()

            if repo.repo_type == AddonManagerRepo.RepoType.MACRO:
                self.show_macro(repo)
                self.ui.buttonExecute.show()
            elif repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH:
                self.show_workbench(repo)
                self.ui.buttonExecute.hide()
            elif repo.repo_type == AddonManagerRepo.RepoType.PACKAGE:
                self.show_package(repo)
                self.ui.buttonExecute.hide()

        if self.status_update_thread is not None:
            if not self.status_update_thread.isFinished():
                self.status_update_thread.requestInterruption()
                self.status_update_thread.wait()

        if repo.status() == AddonManagerRepo.UpdateStatus.UNCHECKED:
            self.status_update_thread = QThread()
            self.status_update_worker = CheckSingleUpdateWorker(repo, self)
            self.status_update_worker.moveToThread(self.status_update_thread)
            self.status_update_thread.finished.connect(
                self.status_update_worker.deleteLater)
            self.check_for_update.connect(self.status_update_worker.do_work)
            self.status_update_worker.update_status.connect(
                self.display_repo_status)
            self.status_update_thread.start()
            self.check_for_update.emit(self.repo)

        self.display_repo_status(self.repo.update_status)

    def display_repo_status(self, status):
        repo = self.repo
        self.set_change_branch_button_state()
        if status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED:

            version = repo.installed_version
            date = ""
            installed_version_string = "<h3>"
            if repo.updated_timestamp:
                date = (QDateTime.fromTime_t(
                    repo.updated_timestamp).date().toString(
                        Qt.SystemLocaleShortDate))
            if version and date:
                installed_version_string += (
                    translate("AddonsInstaller",
                              "Version {version} installed on {date}").format(
                                  version=version, date=date) + ". ")
            elif version:
                installed_version_string += (translate(
                    "AddonsInstaller", "Version {version} installed") +
                                             ". ").format(version=version)
            elif date:
                installed_version_string += (
                    translate("AddonsInstaller", "Installed on {date}") +
                    ". ").format(date=date)
            else:
                installed_version_string += (
                    translate("AddonsInstaller", "Installed") + ". ")

            if status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
                if repo.metadata:
                    installed_version_string += ("<b>" + translate(
                        "AddonsInstaller",
                        "On branch {}, update available to version",
                    ).format(repo.branch) + " ")
                    installed_version_string += repo.metadata.Version
                    installed_version_string += ".</b>"
                elif repo.macro and repo.macro.version:
                    installed_version_string += ("<b>" + translate(
                        "AddonsInstaller", "Update available to version") +
                                                 " ")
                    installed_version_string += repo.macro.version
                    installed_version_string += ".</b>"
                else:
                    installed_version_string += ("<b>" + translate(
                        "AddonsInstaller",
                        "An update is available",
                    ) + ".</b>")
            elif status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
                detached_head = False
                branch = repo.branch
                if have_git and repo.repo_type != AddonManagerRepo.RepoType.MACRO:
                    basedir = FreeCAD.getUserAppDataDir()
                    moddir = os.path.join(basedir, "Mod", repo.name)
                    if os.path.exists(os.path.join(moddir, ".git")):
                        gitrepo = git.Repo(moddir)
                        branch = gitrepo.head.ref.name
                        detached_head = gitrepo.head.is_detached

                if detached_head:
                    installed_version_string += (translate(
                        "AddonsInstaller",
                        "Git tag '{}' checked out, no updates possible",
                    ).format(branch) + ".")
                else:
                    installed_version_string += (translate(
                        "AddonsInstaller",
                        "This is the latest version available for branch {}",
                    ).format(branch) + ".")
            elif status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
                installed_version_string += (
                    translate("AddonsInstaller",
                              "Updated, please restart FreeCAD to use") + ".")
            elif status == AddonManagerRepo.UpdateStatus.UNCHECKED:

                pref = FreeCAD.ParamGet(
                    "User parameter:BaseApp/Preferences/Addons")
                autocheck = pref.GetBool("AutoCheck", False)
                if autocheck:
                    installed_version_string += (translate(
                        "AddonsInstaller", "Update check in progress") + ".")
                else:
                    installed_version_string += (
                        translate("AddonsInstaller",
                                  "Automatic update checks disabled") + ".")

            installed_version_string += "</h3>"
            self.ui.labelPackageDetails.setText(installed_version_string)
            if repo.status() == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
                self.ui.labelPackageDetails.setStyleSheet(
                    "color:" + utils.attention_color_string())
            else:
                self.ui.labelPackageDetails.setStyleSheet(
                    "color:" + utils.bright_color_string())
            self.ui.labelPackageDetails.show()

            if repo.macro is not None:
                moddir = FreeCAD.getUserMacroDir(True)
            else:
                basedir = FreeCAD.getUserAppDataDir()
                moddir = os.path.join(basedir, "Mod", repo.name)
            installationLocationString = (
                translate("AddonsInstaller", "Installation location") + ": " +
                moddir)

            self.ui.labelInstallationLocation.setText(
                installationLocationString)
            self.ui.labelInstallationLocation.show()
        else:
            self.ui.labelPackageDetails.hide()
            self.ui.labelInstallationLocation.hide()

        if status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
            self.ui.buttonInstall.show()
            self.ui.buttonUninstall.hide()
            self.ui.buttonUpdate.hide()
            self.ui.buttonCheckForUpdate.hide()
        elif status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
            self.ui.buttonInstall.hide()
            self.ui.buttonUninstall.show()
            self.ui.buttonUpdate.hide()
            self.ui.buttonCheckForUpdate.hide()
        elif status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
            self.ui.buttonInstall.hide()
            self.ui.buttonUninstall.show()
            self.ui.buttonUpdate.show()
            self.ui.buttonCheckForUpdate.hide()
        elif status == AddonManagerRepo.UpdateStatus.UNCHECKED:
            self.ui.buttonInstall.hide()
            self.ui.buttonUninstall.show()
            self.ui.buttonUpdate.hide()
            self.ui.buttonCheckForUpdate.show()
        elif status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
            self.ui.buttonInstall.hide()
            self.ui.buttonUninstall.show()
            self.ui.buttonUpdate.hide()
            self.ui.buttonCheckForUpdate.hide()

        required_version = self.requires_newer_freecad()
        if repo.obsolete:
            self.ui.labelWarningInfo.show()
            self.ui.labelWarningInfo.setText("<h1>" + translate(
                "AddonsInstaller", "WARNING: This addon is obsolete") +
                                             "</h1>")
            self.ui.labelWarningInfo.setStyleSheet(
                "color:" + utils.warning_color_string())
        elif repo.python2:
            self.ui.labelWarningInfo.show()
            self.ui.labelWarningInfo.setText("<h1>" + translate(
                "AddonsInstaller", "WARNING: This addon is Python 2 Only") +
                                             "</h1>")
            self.ui.labelWarningInfo.setStyleSheet(
                "color:" + utils.warning_color_string())
        elif required_version:
            self.ui.labelWarningInfo.show()
            self.ui.labelWarningInfo.setText("<h1>" + translate(
                "AddonsInstaller", "WARNING: This addon requires FreeCAD ") +
                                             required_version + "</h1>")
            self.ui.labelWarningInfo.setStyleSheet(
                "color:" + utils.warning_color_string())

        else:
            self.ui.labelWarningInfo.hide()

    def requires_newer_freecad(self) -> Optional[str]:
        # If it's not installed, check to see if it's for a newer version of FreeCAD
        if (self.repo.status() == AddonManagerRepo.UpdateStatus.NOT_INSTALLED
                and self.repo.metadata):
            # Only hide if ALL content items require a newer version, otherwise
            # it's possible that this package actually provides versions of itself
            # for newer and older versions

            first_supported_version = (
                self.repo.metadata.getFirstSupportedFreeCADVersion())
            if first_supported_version is not None:
                required_version = first_supported_version.split(".")
                fc_major = int(FreeCAD.Version()[0])
                fc_minor = int(FreeCAD.Version()[1])

                if int(required_version[0]) > fc_major:
                    return first_supported_version
                elif int(required_version[0]
                         ) == fc_major and len(required_version) > 1:
                    if int(required_version[1]) > fc_minor:
                        return first_supported_version
        return None

    def set_change_branch_button_state(self):
        """The change branch button is only available for installed Addons that have a .git directory
        and in runs where the GitPython import is available."""

        self.ui.buttonChangeBranch.hide()

        pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
        show_switcher = pref.GetBool("ShowBranchSwitcher", False)
        if not show_switcher:
            return

        # Is this repo installed? If not, return.
        if self.repo.status() == AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
            return

        # Is it a Macro? If so, return:
        if self.repo.repo_type == AddonManagerRepo.RepoType.MACRO:
            return

        # Can we actually switch branches? If not, return.
        if not have_git:
            return

        # Is there a .git subdirectory? If not, return.
        basedir = FreeCAD.getUserAppDataDir()
        path_to_git = os.path.join(basedir, "Mod", self.repo.name, ".git")
        if not os.path.isdir(path_to_git):
            return

        # If all four above checks passed, then it's possible for us to switch
        # branches, if there are any besides the one we are on: show the button
        self.ui.buttonChangeBranch.show()

    def show_workbench(self, repo: AddonManagerRepo) -> None:
        """loads information of a given workbench"""
        url = utils.get_readme_html_url(repo)
        if HAS_QTWEBENGINE:
            self.ui.webView.load(QUrl(url))
            self.ui.urlBar.setText(url)
        else:
            readme_data = NetworkManager.AM_NETWORK_MANAGER.blocking_get(url)
            text = readme_data.data().decode("utf8")
            self.ui.textBrowserReadMe.setHtml(text)

    def show_package(self, repo: AddonManagerRepo) -> None:
        """Show the details for a package (a repo with a package.xml metadata file)"""

        readme_url = None
        if repo.metadata:
            urls = repo.metadata.Urls
            for url in urls:
                if url["type"] == "readme":
                    readme_url = url["location"]
                    break
        if not readme_url:
            readme_url = utils.get_readme_html_url(repo)
        if HAS_QTWEBENGINE:
            self.ui.webView.load(QUrl(readme_url))
            self.ui.urlBar.setText(readme_url)
        else:
            readme_data = NetworkManager.AM_NETWORK_MANAGER.blocking_get(
                readme_url)
            text = readme_data.data().decode("utf8")
            self.ui.textBrowserReadMe.setHtml(text)

    def show_macro(self, repo: AddonManagerRepo) -> None:
        """loads information of a given macro"""

        if HAS_QTWEBENGINE:
            self.ui.webView.load(QUrl(repo.macro.url))
            self.ui.urlBar.setText(repo.macro.url)
        else:
            readme_data = NetworkManager.AM_NETWORK_MANAGER.blocking_get(
                repo.macro.url)
            text = readme_data.data().decode("utf8")
            self.ui.textBrowserReadMe.setHtml(text)

        # We need to populate the macro information... may as well do it while the user reads the wiki page
        self.worker = GetMacroDetailsWorker(repo)
        self.worker.start()

    def run_javascript(self):
        """Modify the page for a README to optimize for viewing in a smaller window"""

        s = """
( function() {
    const url = new URL (window.location);
    const body = document.getElementsByTagName("body")[0];
    if (url.hostname === "github.com") {
        const articles = document.getElementsByTagName("article");
        if (articles.length > 0) {
            const article = articles[0];
            body.appendChild (article);
            body.style.padding = "1em";
            let sibling = article.previousSibling;
            while (sibling) {
                sibling.remove();
                sibling = article.previousSibling;
            }
        }
    } else if (url.hostname === "gitlab.com" || 
               url.hostname === "framagit.org" || 
               url.hostname === "salsa.debian.org") {
        // These all use the GitLab page display...
        const articles = document.getElementsByTagName("article");
        if (articles.length > 0) {
            const article = articles[0];
            body.appendChild (article);
            body.style.padding = "1em";
            let sibling = article.previousSibling;
            while (sibling) {
                sibling.remove();
                sibling = article.previousSibling;
            }
        }
    } else if (url.hostname === "wiki.freecad.org" || 
               url.hostname === "wiki.freecadweb.org") {
        const first_heading = document.getElementById('firstHeading');
        const body_content = document.getElementById('bodyContent');
        const new_node = document.createElement("div");
        new_node.appendChild(first_heading);
        new_node.appendChild(body_content);
        body.appendChild(new_node);
        let sibling = new_node.previousSibling;
        while (sibling) {
            sibling.remove();
            sibling = new_node.previousSibling;
        }
    }
}
) ()
"""
        self.ui.webView.page().runJavaScript(s)

    def load_started(self):
        self.ui.progressBar.show()
        self.ui.progressBar.setValue(0)

    def load_progress(self, progress: int):
        self.ui.progressBar.setValue(progress)

    def load_finished(self, load_succeeded: bool):
        self.ui.loadingLabel.hide()
        self.ui.slowLoadLabel.hide()
        self.ui.webView.show()
        self.ui.progressBar.hide()
        url = self.ui.webView.url()
        if (hasattr(self, "timeout") and hasattr(self.timeout, "isActive")
                and self.timeout.isActive()):
            self.timeout.stop()
        if load_succeeded:
            # It says it succeeded, but it might have only succeeded in loading a "Page not found" page!
            title = self.ui.webView.title()
            path_components = url.path().split("/")
            expected_content = path_components[-1]
            if url.host() == "github.com" and expected_content not in title:
                self.show_error_for(url)
            elif title == "":
                self.show_error_for(url)
            else:
                self.run_javascript()
        else:
            self.show_error_for(url)

    def long_load_running(self):
        if hasattr(self.ui, "webView") and self.ui.webView.isHidden():
            self.ui.slowLoadLabel.show()
            self.ui.loadingLabel.hide()
            self.ui.webView.show()

    def show_error_for(self, url: QUrl) -> None:
        m = translate("AddonsInstaller",
                      "Could not load README data from URL {}").format(
                          url.toString())
        html = f"<html><body><p>{m}</p></body></html>"
        self.ui.webView.setHtml(html)

    def change_branch_clicked(self) -> None:
        basedir = FreeCAD.getUserAppDataDir()
        path_to_repo = os.path.join(basedir, "Mod", self.repo.name)
        change_branch_dialog = ChangeBranchDialog(path_to_repo, self)
        change_branch_dialog.branch_changed.connect(self.branch_changed)
        change_branch_dialog.exec()

    def branch_changed(self, name: str) -> None:
        QMessageBox.information(
            self,
            translate("AddonsInstaller", "Success"),
            translate(
                "AddonsInstaller",
                "Branch change succeeded, please restart to use the new version.",
            ),
        )
        # See if this branch has a package.xml file:
        basedir = FreeCAD.getUserAppDataDir()
        path_to_metadata = os.path.join(basedir, "Mod", self.repo.name,
                                        "package.xml")
        if os.path.isfile(path_to_metadata):
            self.repo.load_metadata_file(path_to_metadata)
            self.repo.installed_version = self.repo.metadata.Version
        else:
            self.repo.repo_type = AddonManagerRepo.RepoType.WORKBENCH
            self.repo.metadata = None
            self.repo.installed_version = None
        self.repo.updated_timestamp = QDateTime.currentDateTime(
        ).toSecsSinceEpoch()
        self.repo.branch = name
        self.repo.set_status(AddonManagerRepo.UpdateStatus.PENDING_RESTART)

        installed_version_string = "<h3>"
        installed_version_string += translate(
            "AddonsInstaller",
            "Changed to git ref '{}' -- please restart to use Addon.").format(
                name)
        installed_version_string += "</h3>"
        self.ui.labelPackageDetails.setText(installed_version_string)
        self.ui.labelPackageDetails.setStyleSheet(
            "color:" + utils.attention_color_string())
        self.update_status.emit(self.repo)
示例#7
0
class PackageDetails(QWidget):

    back = Signal()
    install = Signal(AddonManagerRepo)
    uninstall = Signal(AddonManagerRepo)
    update = Signal(AddonManagerRepo)
    execute = Signal(AddonManagerRepo)
    update_status = Signal(AddonManagerRepo)
    check_for_update = Signal(AddonManagerRepo)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_PackageDetails()
        self.ui.setupUi(self)

        self.worker = None
        self.repo = None

        self.ui.buttonBack.clicked.connect(self.back.emit)
        self.ui.buttonRefresh.clicked.connect(self.refresh)
        self.ui.buttonExecute.clicked.connect(
            lambda: self.execute.emit(self.repo))
        self.ui.buttonInstall.clicked.connect(
            lambda: self.install.emit(self.repo))
        self.ui.buttonUninstall.clicked.connect(
            lambda: self.uninstall.emit(self.repo))
        self.ui.buttonUpdate.clicked.connect(
            lambda: self.update.emit(self.repo))
        self.ui.buttonCheckForUpdate.clicked.connect(
            lambda: self.check_for_update.emit(self.repo))

    def show_repo(self, repo: AddonManagerRepo, reload: bool = False) -> None:

        self.repo = repo

        if self.worker is not None:
            if not self.worker.isFinished():
                self.worker.requestInterruption()
                self.worker.wait()

        # Always load bare macros from scratch, we need to grab their code, which isn't cached
        force_reload = reload
        if repo.repo_type == AddonManagerRepo.RepoType.MACRO:
            force_reload = True

        self.check_and_clean_cache(force_reload)

        if repo.repo_type == AddonManagerRepo.RepoType.MACRO:
            self.show_macro(repo)
            self.ui.buttonExecute.show()
        elif repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH:
            self.show_workbench(repo)
            self.ui.buttonExecute.hide()
        elif repo.repo_type == AddonManagerRepo.RepoType.PACKAGE:
            self.show_package(repo)
            self.ui.buttonExecute.hide()

        if repo.update_status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED:

            version = repo.installed_version
            date = ""
            installed_version_string = "<h3>"
            if repo.updated_timestamp:
                date = (QDateTime.fromTime_t(
                    repo.updated_timestamp).date().toString(
                        Qt.SystemLocaleShortDate))
            if version and date:
                installed_version_string += (
                    translate("AddonsInstaller",
                              f"Version {version} installed on {date}") + ". ")
            elif version:
                installed_version_string += (translate(
                    "AddonsInstaller", f"Version {version} installed") + ". ")
            elif date:
                installed_version_string += (
                    translate("AddonsInstaller", f"Installed on {date}") +
                    ". ")
            else:
                installed_version_string += (
                    translate("AddonsInstaller", "Installed") + ". ")

            if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
                if repo.metadata:
                    installed_version_string += ("<b>" + translate(
                        "AddonsInstaller", "Update available to version") +
                                                 " ")
                    installed_version_string += repo.metadata.Version
                    installed_version_string += ".</b>"
                elif repo.macro and repo.macro.version:
                    installed_version_string += ("<b>" + translate(
                        "AddonsInstaller", "Update available to version") +
                                                 " ")
                    installed_version_string += repo.macro.version
                    installed_version_string += ".</b>"
                else:
                    installed_version_string += ("<b>" + translate(
                        "AddonsInstaller",
                        "An update is available",
                    ) + ".</b>")
            elif (repo.update_status ==
                  AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE):
                installed_version_string += (
                    translate("AddonsInstaller",
                              "This is the latest version available") + ".")
            elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
                installed_version_string += (
                    translate("AddonsInstaller",
                              "Updated, please restart FreeCAD to use") + ".")
            elif repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:

                pref = FreeCAD.ParamGet(
                    "User parameter:BaseApp/Preferences/Addons")
                autocheck = pref.GetBool("AutoCheck", False)
                if autocheck:
                    installed_version_string += (translate(
                        "AddonsInstaller", "Update check in progress") + ".")
                else:
                    installed_version_string += (
                        translate("AddonsInstaller",
                                  "Automatic update checks disabled") + ".")

            installed_version_string += "</h3>"
            self.ui.labelPackageDetails.setText(installed_version_string)
            if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
                self.ui.labelPackageDetails.setStyleSheet(
                    "color:" + utils.attention_color_string())
            else:
                self.ui.labelPackageDetails.setStyleSheet(
                    "color:" + utils.bright_color_string())
            self.ui.labelPackageDetails.show()

            if repo.macro is not None:
                moddir = FreeCAD.getUserMacroDir(True)
            else:
                basedir = FreeCAD.getUserAppDataDir()
                moddir = os.path.join(basedir, "Mod", repo.name)
            installationLocationString = (
                translate("AddonsInstaller", "Installation location") + ": " +
                moddir)

            self.ui.labelInstallationLocation.setText(
                installationLocationString)
            self.ui.labelInstallationLocation.show()
        else:
            self.ui.labelPackageDetails.hide()
            self.ui.labelInstallationLocation.hide()

        if repo.update_status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
            self.ui.buttonInstall.show()
            self.ui.buttonUninstall.hide()
            self.ui.buttonUpdate.hide()
            self.ui.buttonCheckForUpdate.hide()
        elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
            self.ui.buttonInstall.hide()
            self.ui.buttonUninstall.show()
            self.ui.buttonUpdate.hide()
            self.ui.buttonCheckForUpdate.hide()
        elif repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
            self.ui.buttonInstall.hide()
            self.ui.buttonUninstall.show()
            self.ui.buttonUpdate.show()
            self.ui.buttonCheckForUpdate.hide()
        elif repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
            self.ui.buttonInstall.hide()
            self.ui.buttonUninstall.show()
            self.ui.buttonUpdate.hide()
            self.ui.buttonCheckForUpdate.show()
        elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
            self.ui.buttonInstall.hide()
            self.ui.buttonUninstall.show()
            self.ui.buttonUpdate.hide()
            self.ui.buttonCheckForUpdate.hide()

        if repo.obsolete:
            self.ui.labelWarningInfo.show()
            self.ui.labelWarningInfo.setText("<h1>" + translate(
                "AddonsInstaller", "WARNING: This addon is obsolete") +
                                             "</h1>")
            self.ui.labelWarningInfo.setStyleSheet(
                "color:" + utils.warning_color_string())
        elif repo.python2:
            self.ui.labelWarningInfo.show()
            self.ui.labelWarningInfo.setText("<h1>" + translate(
                "AddonsInstaller", "WARNING: This addon is Python 2 Only") +
                                             "</h1>")
            self.ui.labelWarningInfo.setStyleSheet(
                "color:" + utils.warning_color_string())
        else:
            self.ui.labelWarningInfo.hide()

    @classmethod
    def cache_path(self, repo: AddonManagerRepo) -> str:
        cache_path = FreeCAD.getUserCachePath()
        full_path = os.path.join(cache_path, "AddonManager", repo.name)
        return full_path

    def check_and_clean_cache(self, force: bool = False) -> None:
        cache_path = PackageDetails.cache_path(self.repo)
        readme_cache_file = os.path.join(cache_path, "README.html")
        readme_images_path = os.path.join(cache_path, "Images")
        download_interrupted_sentinel = os.path.join(readme_images_path,
                                                     "download_in_progress")
        download_interrupted = os.path.isfile(download_interrupted_sentinel)
        if os.path.isfile(readme_cache_file):
            pref = FreeCAD.ParamGet(
                "User parameter:BaseApp/Preferences/Addons")
            days_between_updates = pref.GetInt("DaysBetweenUpdates", 2 ^ 32)
            timestamp = os.path.getmtime(readme_cache_file)
            last_cache_update = date.fromtimestamp(timestamp)
            delta_update = timedelta(days=days_between_updates)
            if (date.today() >= last_cache_update + delta_update
                    or download_interrupted or force):
                if force:
                    FreeCAD.Console.PrintLog(
                        f"Forced README cache update for {self.repo.name}\n")
                elif download_interrupted:
                    FreeCAD.Console.PrintLog(
                        f"Restarting interrupted README download for {self.repo.name}\n"
                    )
                else:
                    FreeCAD.Console.PrintLog(
                        f"Cache expired, downloading README for {self.repo.name} again\n"
                    )
                os.remove(readme_cache_file)
                if os.path.isdir(readme_images_path):
                    shutil.rmtree(readme_images_path)

    def refresh(self):
        self.check_and_clean_cache(force=True)
        self.show_repo(self.repo)

    def show_cached_readme(self, repo: AddonManagerRepo) -> bool:
        """Attempts to show a cached readme, returns true if there was a cache, or false if not"""

        cache_path = PackageDetails.cache_path(repo)
        readme_cache_file = os.path.join(cache_path, "README.html")
        if os.path.isfile(readme_cache_file):
            with open(readme_cache_file, "rb") as f:
                data = f.read()
                self.ui.textBrowserReadMe.setText(data.decode())
                return True
        return False

    def show_workbench(self, repo: AddonManagerRepo) -> None:
        """loads information of a given workbench"""

        if not self.show_cached_readme(repo):
            self.ui.textBrowserReadMe.setText(
                translate("AddonsInstaller",
                          "Fetching README.md from package repository"))
            self.worker = ShowWorker(repo, PackageDetails.cache_path(repo))
            self.worker.readme_updated.connect(
                lambda desc: self.cache_readme(repo, desc))
            self.worker.readme_updated.connect(
                lambda desc: self.ui.textBrowserReadMe.setText(desc))
            self.worker.update_status.connect(self.update_status.emit)
            self.worker.update_status.connect(self.show)
            self.worker.start()

    def show_package(self, repo: AddonManagerRepo) -> None:
        """Show the details for a package (a repo with a package.xml metadata file)"""

        if not self.show_cached_readme(repo):
            self.ui.textBrowserReadMe.setText(
                translate("AddonsInstaller",
                          "Fetching README.md from package repository"))
            self.worker = ShowWorker(repo, PackageDetails.cache_path(repo))
            self.worker.readme_updated.connect(
                lambda desc: self.cache_readme(repo, desc))
            self.worker.readme_updated.connect(
                lambda desc: self.ui.textBrowserReadMe.setText(desc))
            self.worker.update_status.connect(self.update_status.emit)
            self.worker.update_status.connect(self.show)
            self.worker.start()

    def show_macro(self, repo: AddonManagerRepo) -> None:
        """loads information of a given macro"""

        if not self.show_cached_readme(repo):
            self.ui.textBrowserReadMe.setText(
                translate("AddonsInstaller",
                          "Fetching README.md from package repository"))
            self.worker = GetMacroDetailsWorker(repo)
            self.worker.readme_updated.connect(
                lambda desc: self.cache_readme(repo, desc))
            self.worker.readme_updated.connect(
                lambda desc: self.ui.textBrowserReadMe.setText(desc))
            self.worker.start()

    def cache_readme(self, repo: AddonManagerRepo, readme: str) -> None:
        cache_path = PackageDetails.cache_path(repo)
        readme_cache_file = os.path.join(cache_path, "README.html")
        os.makedirs(cache_path, exist_ok=True)
        with open(readme_cache_file, "wb") as f:
            f.write(readme.encode())