diff --git a/juniors_toolbox/gui/application.py b/juniors_toolbox/gui/application.py index 0c8afb5..b26e4d5 100644 --- a/juniors_toolbox/gui/application.py +++ b/juniors_toolbox/gui/application.py @@ -1,17 +1,19 @@ import sys from pathlib import Path from typing import Dict, Iterable, List, Optional, Tuple -from PySide6.QtCore import QPoint, QSize, Slot +from PySide6.QtCore import QPoint, QSize, Slot, QThread from PySide6.QtGui import QResizeEvent, Qt, QFontDatabase from PySide6.QtWidgets import QApplication, QFileDialog, QLabel, QSizePolicy, QStyleFactory, QWidget from juniors_toolbox import __version__ +from juniors_toolbox.gui.dialogs.updatefound import UpdateFoundDialog from juniors_toolbox.gui.settings import ToolboxSettings from juniors_toolbox.gui.tabs import TabWidgetManager from juniors_toolbox.gui.tabs.hierarchyviewer import NameRefHierarchyWidget from juniors_toolbox.gui.tabs.projectviewer import ProjectViewerWidget from juniors_toolbox.gui.tabs.propertyviewer import SelectedPropertiesWidget from juniors_toolbox.gui.templates import ToolboxTemplates +from juniors_toolbox.gui.update import GitUpdateScraper from juniors_toolbox.gui.widgets.dockinterface import A_DockingInterface from juniors_toolbox.gui.windows.mainwindow import MainWindow from juniors_toolbox.scene import SMSScene @@ -19,6 +21,8 @@ from juniors_toolbox.utils.filesystem import get_program_folder, resource_path from juniors_toolbox.gui import ToolboxManager +from github.GitRelease import GitRelease + class JuniorsToolbox(QApplication): """ @@ -78,6 +82,18 @@ def __init__(self): lambda _: self.save_scene() ) # throw away checked flag + # Updates + self.updateThread = QThread() + self.updater = GitUpdateScraper("JoshuaMKW", "Juniors-Toolbox", self) + self.updater.moveToThread(self.updateThread) + self.updateThread.finished.connect(self.updateThread.deleteLater) + self.updateThread.start() + + self.gui.actionCheckUpdate.triggered.connect( + self.updater.run + ) + self.updater.updatesFound.connect(self.show_updates) + # Set up theme toggle fontFolder = resource_path("gui/fonts/") @@ -250,6 +266,11 @@ def closeDockerTab(self, tab: A_DockingInterface): # self.gui.removeDockWidget(tab) self.set_central_status(self.is_docker_empty()) + @Slot(list) + def show_updates(self, updates: Iterable[GitRelease]): + dialog = UpdateFoundDialog(self.updater) + dialog.display_updates(updates) + def __init_tabs(self): areas = [Qt.LeftDockWidgetArea, Qt.RightDockWidgetArea] for i, tab in enumerate(TabWidgetManager.iter_tabs()): diff --git a/juniors_toolbox/gui/dialogs/moveconflict.py b/juniors_toolbox/gui/dialogs/moveconflict.py index 13a9a51..58fd2f2 100644 --- a/juniors_toolbox/gui/dialogs/moveconflict.py +++ b/juniors_toolbox/gui/dialogs/moveconflict.py @@ -1,5 +1,4 @@ from pathlib import Path -from tkinter import W from typing import Optional, Tuple from unittest import skip from PySide6.QtCore import (QAbstractItemModel, QDataStream, QEvent, QIODevice, diff --git a/juniors_toolbox/gui/dialogs/updatefound.py b/juniors_toolbox/gui/dialogs/updatefound.py new file mode 100644 index 0000000..27f823e --- /dev/null +++ b/juniors_toolbox/gui/dialogs/updatefound.py @@ -0,0 +1,91 @@ +from pathlib import Path +from typing import Iterable, Optional, Tuple + +from github.GitRelease import GitRelease +from github.PaginatedList import PaginatedList + +from PySide6.QtCore import (QAbstractItemModel, QDataStream, QEvent, QIODevice, + QLine, QMimeData, QModelIndex, QObject, QPoint, + QSize, Qt, QThread, QTimer, QUrl, Signal, + SignalInstance, Slot) +from PySide6.QtGui import (QAction, QColor, QCursor, QDrag, QDragEnterEvent, + QDragLeaveEvent, QDragMoveEvent, QDropEvent, QIcon, + QImage, QKeyEvent, QMouseEvent, QPaintDevice, + QPainter, QPaintEvent, QPalette, QPixmap, + QUndoCommand, QUndoStack) +from PySide6.QtWidgets import (QBoxLayout, QComboBox, QFormLayout, QFrame, + QGridLayout, QGroupBox, QHBoxLayout, QLabel, + QLayout, QLineEdit, QListView, QListWidget, + QListWidgetItem, QMenu, QMenuBar, QPushButton, + QScrollArea, QSizePolicy, QSpacerItem, + QSplitter, QStyle, QStyleOptionComboBox, + QStylePainter, QTableWidget, QTableWidgetItem, + QToolBar, QTreeWidget, QTreeWidgetItem, QDialog, QDialogButtonBox, QCheckBox, QTextBrowser, + QVBoxLayout, QWidget) +from enum import IntEnum + +from juniors_toolbox import __version__ +from juniors_toolbox.update import ReleaseManager + + +class UpdateFoundDialog(QDialog): + def __init__(self, manager: ReleaseManager, isMulti: bool = False, parent: Optional[QWidget] = None): + super().__init__(parent) + if isMulti: + self.setFixedSize(400, 190) + else: + self.setFixedSize(400, 160) + + self.setWindowFlag(Qt.FramelessWindowHint, True) + self.setWindowFlag(Qt.CustomizeWindowHint, True) + + self.mainLayout = QVBoxLayout() + + self.titleText = QLabel() + titleFont = self.titleText.font() + titleFont.setBold(True) + titleFont.setPointSize(16) + self.titleText.setFont(titleFont) + self.mainLayout.addWidget(self.titleText) + + self.descriptionText = QLabel() + descriptionFont = self.descriptionText.font() + descriptionFont.setPointSize(12) + self.descriptionText.setFont(descriptionFont) + self.mainLayout.addWidget(self.descriptionText) + + self.releasesView = QTextBrowser() + self.releasesView.setAcceptRichText(True) + releasesFont = self.releasesView.font() + releasesFont.setPointSize(12) + self.releasesView.setFont(releasesFont) + + self.buttonsLayout = QHBoxLayout() + + self.rejectButton = QPushButton("Maybe Later") + self.acceptButton = QPushButton("Update") + self.rejectButton.clicked.connect(self.reject) + self.acceptButton.clicked.connect(self.accept) + + self.setLayout(self.mainLayout) + + self.manager = manager + + def display_updates(self, updates: PaginatedList[GitRelease]) -> QDialog.DialogCode: + if updates.totalCount == 0: + return QDialog.Rejected + + newestRelease: GitRelease = updates[0] + + self.setWindowModality(Qt.ApplicationModal) + self.titleText.setText( + f"{__name__} {newestRelease.tag_name} available!" + ) + self.releasesView.setMarkdown( + self.manager.compile_changelog_from(__version__) + ) + + retCode: QDialog.DialogCode = self.exec() # type: ignore + if retCode == QDialog.Accepted: + self.manager.view(newestRelease) + return retCode diff --git a/juniors_toolbox/gui/update.py b/juniors_toolbox/gui/update.py new file mode 100644 index 0000000..24f31b1 --- /dev/null +++ b/juniors_toolbox/gui/update.py @@ -0,0 +1,58 @@ +from distutils.version import LooseVersion +import time +from typing import Iterable, Optional + +from github.GitRelease import GitRelease +from PySide6.QtCore import Signal, Slot, QObject, QRunnable + +from juniors_toolbox import __version__ +from juniors_toolbox.update import ReleaseManager + + +class GitUpdateScraper(QObject, QRunnable, ReleaseManager): + updatesFound = Signal(list) + + def __init__(self, owner: str, repository: str, parent: Optional[QObject] = None): + QObject.__init__(self, parent) + ReleaseManager.__init__(self, owner, repository) + self.setObjectName(f"{self.__class__.__name__}.{owner}.{repository}") + + self.waitTime = 0.0 + self._quitting = False + + def set_wait_time(self, seconds: float): + self.waitTime = seconds + + @Slot() + def run(self): + newReleases = self.check_updates() + if len(newReleases) > 0: + self.updatesFound(newReleases) + + # while not self._quitting: + # newReleases = self.check_updates() + # if len(newReleases) > 0: + # self.updatesFound(newReleases) + + # start = time.time() + # while time.time() - start < self.waitTime: + # if self._quitting: + # break + # time.sleep(1) + + def check_updates(self) -> Iterable[GitRelease]: + successful = self.populate() + if not successful: + return [] + + newestRelease = self.get_newest_release() + if newestRelease is None: + return [] + + if LooseVersion(newestRelease.tag_name.lstrip("v")) <= LooseVersion(__version__.lstrip("v")): + return [] + + return self.get_releases() + + def kill(self) -> None: + self._quitting = True \ No newline at end of file diff --git a/juniors_toolbox/update.py b/juniors_toolbox/update.py new file mode 100644 index 0000000..194025f --- /dev/null +++ b/juniors_toolbox/update.py @@ -0,0 +1,86 @@ +import webbrowser +from distutils.version import LooseVersion +from typing import Iterable, List, Optional + +from github import Github +from github.PaginatedList import PaginatedList +from github.GitRelease import GitRelease + +from juniors_toolbox import __version__ + +class ReleaseManager(): + def __init__(self, owner: str, repository: str): + self._owner = owner + self._repo = repository + self._releases: Optional[PaginatedList[GitRelease]] = None + self.populate() + + @property + def owner(self) -> str: + return self._owner + + @property + def repository(self) -> str: + return self._repo + + @owner.setter + def owner(self, owner: str): + self._owner = owner + + @repository.setter + def repository(self, repo: str): + self._repo = repo + + @property + def releaseLatestURL(self) -> str: + return f"https://github.com/{self._owner}/{self._repo}/releases/latest" + + @property + def releasesURL(self) -> str: + return f"https://github.com/{self._owner}/{self._repo}/releases" + + def get_newest_release(self) -> Optional[GitRelease]: + if self._releases is None or self._releases.totalCount == 0: + return None + return self._releases[0] + + def get_oldest_release(self) -> Optional[GitRelease]: + if self._releases is None or self._releases.totalCount == 0: + return None + return self._releases[-1] + + def get_releases(self) -> Iterable[GitRelease]: + if self._releases is None or self._releases.totalCount == 0: + return [] + return self._releases + + def compile_changelog_from(self, version: str) -> str: + """ Returns a Markdown changelog from the info of future versions """ + seperator = "\n\n---\n\n" + + newReleases: List[GitRelease] = list() + lver = LooseVersion(version.lstrip("v")) + for release in self.get_releases(): + if LooseVersion(release.tag_name.lstrip("v")) <= lver: + break + newReleases.append(release) + + markdown = "" + for release in newReleases: + markdown += release.body.replace("Changelog", + f"Changelog ({release.tag_name})").strip() + seperator + + return markdown.rstrip(seperator).strip() + + def populate(self) -> bool: + g = Github() + repo = g.get_repo(f"{self.owner}/{self.repository}") + self._releases = repo.get_releases() + return True + + @staticmethod + def view(release: GitRelease, browser: Optional[webbrowser.GenericBrowser] = None, asWindow: bool = False): + if browser is None: + webbrowser.open(release.html_url, int(asWindow)) + else: + browser.open(release.html_url, int(asWindow))