diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 466536a8..505ff5d1 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -9,52 +9,106 @@ on:
- main
paths:
- 'constructor-manager/**'
- - 'constructor-manager-cli/**'
+ - 'constructor-manager-api/**'
+ - 'constructor-manager-ui/**'
workflow_dispatch:
jobs:
test:
name: ${{ matrix.platform }} py${{ matrix.python-version }}
runs-on: ${{ matrix.platform }}
+ defaults:
+ run:
+ shell: bash -el {0}
strategy:
+ fail-fast: false
matrix:
platform: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.8', '3.9', '3.10']
-
+ env:
+ DISPLAY: ':99.0'
steps:
+ - name: Install OS dependencies
+ if: contains(matrix.platform, 'ubuntu')
+ run: |
+ sudo apt-get update --fix-missing
+ sudo apt install xvfb
+
+ - uses: tlambert03/setup-qt-libs@v1
+
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: conda-incubator/setup-miniconda@v2
with:
+ activate-environment: "constructor-manager"
python-version: ${{ matrix.python-version }}
+ auto-activate-base: true
+ run-post: false
- - name: Install dependencies constructor-manager-cli
+ - name: Install dependencies on base environment
run: |
- python -m pip install --upgrade pip
- python -m pip install setuptools tox tox-gh-actions
- cd constructor-manager-cli
- pip install -e .
- pip list
+ conda install -n base conda-lock mamba -c conda-forge --quiet
+ conda list
- - name: Test constructor-manager-cli
+ - name: create constructor-manager-environment
run: |
- cd constructor-manager-cli
- python -m tox
- env:
- PLATFORM: ${{ matrix.platform }}
+ conda install -n constructor-manager conda packaging requests pyyaml -c conda-forge --quiet
+ # List installed packages
+ conda list
- - name: Install dependencies constructor-manager
+ - name: Install constructor-manager
run: |
- cd constructor-manager-cli
- pip install -e .
- pip list
- env:
- PLATFORM: ${{ matrix.platform }}
+ # Install constructor manager
+ git clone https://github.com/goanpeca/packaging.git packaging_clone
+ cd packaging_clone
+ git checkout constructor-updater
+ cd constructor-manager
+ pip install -e . --no-deps
- - name: Test constructor-manager
+ # Install test deps
+ conda install -n constructor-manager pytest pytest-cov pytest-qt -c conda-forge --quiet
+
+ # List installed packages
+ conda list
+
+ - name: Install constructor-manager-api
run: |
- cd constructor-manager
- python -m tox
- env:
- PLATFORM: ${{ matrix.platform }}
+ # Install dependencies
+ conda install -n constructor-manager qtpy pyqt -c conda-forge --quiet
+
+ # Install constructor manager
+ cd packaging_clone
+ git checkout constructor-cli
+ cd constructor-manager-api
+ pip install -e . --no-deps
+
+ # List installed packages
+ conda list
+
+ - name: Install constructor-manager-ui
+ run: |
+ # Install constructor manager ui
+ cd constructor-manager-ui
+ pip install -e . --no-deps
+
+ # List installed packages
+ conda list
+
+ - name: List installed packages
+ run: |
+ conda list
+
+ - name: Test constructor-manager-ui (linux)
+ if: contains(matrix.platform, 'ubuntu')
+ run: |
+ # Run Tests
+ cd constructor-manager-ui/src
+ xvfb-run pytest constructor_manager_ui --cov=constructor_manager_ui
+
+ - name: Test constructor-manager-ui (other)
+ if: "!contains(matrix.platform, 'ubuntu')"
+ run: |
+ # Run Tests
+ cd constructor-manager-ui/src
+ pytest constructor_manager_ui --cov=constructor_manager_ui
diff --git a/.github/workflows/tests_ui.yml b/.github/workflows/tests_ui.yml
deleted file mode 100644
index 11743e03..00000000
--- a/.github/workflows/tests_ui.yml
+++ /dev/null
@@ -1,50 +0,0 @@
-name: tests_ui
-
-on:
- push:
- branches:
- - main
- pull_request:
- branches:
- - main
- paths:
- - 'constructor-manager-ui/**'
- workflow_dispatch:
-
-jobs:
- test:
- name: ${{ matrix.platform }} py${{ matrix.python-version }}
- runs-on: ${{ matrix.platform }}
- strategy:
- matrix:
- platform: [ubuntu-latest, windows-latest, macos-latest]
- python-version: ['3.8', '3.9', '3.10']
- env:
- DISPLAY: ':99.0'
- steps:
- - uses: actions/checkout@v3
-
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
- with:
- python-version: ${{ matrix.python-version }}
-
- - name: Install OS dependencies
- if: contains(matrix.platform, 'ubuntu')
- run: |
- sudo apt-get update --fix-missing
- sudo apt-get install -qq pyqt5-dev-tools libxcb-xinerama0 xterm --fix-missing
-
- - name: Install dependencies UI
- run: |
- cd constructor-manager-ui
- python -m pip install --upgrade pip
- python -m pip install setuptools tox tox-gh-actions
- pip list
- # this runs the platform-specific tests declared in tox.ini
- - name: Test with tox
- run: |
- cd constructor-manager-ui
- python -m tox
- env:
- PLATFORM: ${{ matrix.platform }}
diff --git a/constructor-manager-ui/README.md b/constructor-manager-ui/README.md
index f9f6ff0c..b0fe13d7 100644
--- a/constructor-manager-ui/README.md
+++ b/constructor-manager-ui/README.md
@@ -46,3 +46,12 @@ constructor-manager-ui-qrc
```
You need to have pyqt5 installed.
+
+### Example commands
+
+```bash
+constructor-manager-ui napari --current-version 0.4.16 --build-string pyside --plugins-url https://api.napari-hub.org/plugins --channel conda-forge
+constructor-manager-ui napari --build-string pyside --plugins-url https://api.napari-hub.org/plugins --channel conda-forge --channel napari
+constructor-manager-ui napari --build-string pyside --plugins-url https://api.napari-hub.org/plugins --channel conda-forge --channel napari --dev
+constructor-manager-ui napari --build-string pyside --plugins-url https://api.napari-hub.org/plugins --channel conda-forge --channel napari -cv 0.4.17
+```
diff --git a/constructor-manager-ui/setup.cfg b/constructor-manager-ui/setup.cfg
index 42209548..7959ffd1 100644
--- a/constructor-manager-ui/setup.cfg
+++ b/constructor-manager-ui/setup.cfg
@@ -39,7 +39,7 @@ where = src
[options.entry_points]
console_scripts =
- constructor-manager-ui = constructor_manager_ui.cli:run
+ constructor-manager-ui = constructor_manager_ui.main:run
constructor-manager-ui-qrc = constructor_manager_ui.style.utils:generate_resource_file
[options.extras_require]
diff --git a/constructor-manager-ui/src/constructor_manager_ui/__init__.py b/constructor-manager-ui/src/constructor_manager_ui/__init__.py
index 96e31bb9..e69de29b 100644
--- a/constructor-manager-ui/src/constructor_manager_ui/__init__.py
+++ b/constructor-manager-ui/src/constructor_manager_ui/__init__.py
@@ -1 +0,0 @@
-"""Constructor manager."""
diff --git a/constructor-manager-ui/src/constructor_manager_ui/cli.py b/constructor-manager-ui/src/constructor_manager_ui/cli.py
index 1776fb71..437f6e18 100644
--- a/constructor-manager-ui/src/constructor_manager_ui/cli.py
+++ b/constructor-manager-ui/src/constructor_manager_ui/cli.py
@@ -2,15 +2,39 @@
import argparse
-from constructor_manager_ui.main import main
-
-def run():
+def create_parser():
parser = argparse.ArgumentParser()
parser.add_argument("package", type=str)
- args = parser.parse_args()
- main(args.package)
-
-if __name__ == "__main__":
- run()
+ parser.add_argument(
+ "--current-version",
+ "-cv",
+ type=str,
+ default=None,
+ )
+ parser.add_argument(
+ "--build-string",
+ help="increase output verbosity",
+ type=str,
+ default=None,
+ )
+ parser.add_argument(
+ "--plugins-url",
+ "-pu",
+ type=str,
+ default=None,
+ )
+ parser.add_argument(
+ "--channel",
+ "-c",
+ action="append",
+ default=None,
+ )
+ parser.add_argument(
+ "--log",
+ default="WARNING",
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
+ )
+ parser.add_argument("--dev", "-d", action="store_true")
+ return parser
diff --git a/constructor-manager-ui/src/constructor_manager_ui/main.py b/constructor-manager-ui/src/constructor_manager_ui/main.py
index 20d7b7cb..cdb3bad6 100644
--- a/constructor-manager-ui/src/constructor_manager_ui/main.py
+++ b/constructor-manager-ui/src/constructor_manager_ui/main.py
@@ -1,434 +1,46 @@
-"""Constructor manager main interface."""
-
+import logging
import sys
-from typing import Optional
+from typing import Any, Tuple
-from qtpy.QtCore import QSize, Qt, QTimer, Signal
-from qtpy.QtGui import QBrush, QMovie
-from qtpy.QtWidgets import (
- QAbstractItemView,
- QApplication,
- QCheckBox,
- QDialog,
- QFrame,
- QGridLayout,
- QGroupBox,
- QHBoxLayout,
- QHeaderView,
- QLabel,
- QPushButton,
- QStackedWidget,
- QTableWidget,
- QTableWidgetItem,
- QVBoxLayout,
- QWidget,
-)
+from qtpy.QtWidgets import QApplication
-# To get mock data
-from constructor_manager_ui.data import (
- INSTALL_INFORMATION,
- PACKAGES,
- UPDATE_AVAILABLE_VERSION,
-)
+from constructor_manager_ui.style.utils import update_styles
+from constructor_manager_ui.widgets.dialog import InstallationManagerDialog
+from constructor_manager_ui.cli import create_parser
+from constructor_manager_api.utils.settings import load_settings # type: ignore
# To setup image resources for .qss file
from constructor_manager_ui.style import images # noqa
-from constructor_manager_ui.style.utils import update_styles
-
-# Packages table constants
-RELATED_PACKAGES = 0
-ALL_PACKAGES = 1
-
-
-class SpinnerWidget(QWidget):
- def __init__(self, text: str, parent: Optional[QWidget] = None):
- super().__init__(parent=parent)
-
- # Widgets for text and loading gif
- self.text_label = QLabel(text)
- spinner_label = QLabel()
- self.spinner_movie = QMovie(":/images/loading.gif")
- self.spinner_movie.setScaledSize(QSize(18, 18))
- spinner_label.setMovie(self.spinner_movie)
-
- # Set layout for text + loading indicator
- layout = QHBoxLayout()
- layout.setContentsMargins(0, 0, 0, 0)
- layout.addWidget(self.text_label)
- layout.addWidget(spinner_label)
- layout.addStretch(1)
- self.setLayout(layout)
- self.spinner_movie.start()
-
- def set_text(self, text: str):
- self.text_label.setText(text)
-
- def show(self):
- self.spinner_movie.start()
- super().show()
-
- def hide(self):
- self.spinner_movie.stop()
- super().hide()
-
-
-class UpdateWidget(QWidget):
-
- install_version = Signal(str)
- skip_version = Signal(str)
-
- def __init__(self, package_name: str, parent: Optional[QWidget] = None):
- super().__init__(parent=parent)
- self.package_name = package_name
- self.update_available_version = None
-
- # Setup widgets
- self.checking_update_widget = SpinnerWidget(
- "Checking for updates...", parent=self
- )
- self.up_to_date_widget = QWidget(self)
- self._initialize_up_to_date_widget()
- self.update_available_widget = QWidget(self)
- self._initialize_update_available_widget()
-
- # Stack widgets to show one at a time and set layout
- update_widget_layout = QHBoxLayout()
- self.update_widgets = QStackedWidget(self)
- self.update_widgets.addWidget(self.checking_update_widget)
- self.update_widgets.addWidget(self.up_to_date_widget)
- self.update_widgets.addWidget(self.update_available_widget)
- update_widget_layout.addWidget(self.update_widgets)
- self.setLayout(update_widget_layout)
-
- # Start showing checking updates widget
- self.show_checking_updates_message()
-
- def _initialize_up_to_date_widget(self):
- up_to_date_layout = QVBoxLayout()
- update_msg_label = QLabel(f"Your {self.package_name} is up to date.")
- up_to_date_layout.addWidget(update_msg_label)
-
- self.up_to_date_widget.setLayout(up_to_date_layout)
-
- def _initialize_update_available_widget(self):
- new_version_layout = QVBoxLayout()
- update_msg_label_layout = QHBoxLayout()
- update_msg_label = QLabel(
- f"A newer version of {self.package_name} is available!"
- )
- update_msg_label_layout.addSpacing(15)
- update_msg_label_layout.addWidget(update_msg_label)
-
- update_actions_layout = QHBoxLayout()
- new_version_label = QLabel(self.update_available_version)
- skip_version_button = QPushButton("Skip This Version")
- install_version_button = QPushButton("Install This Version")
- install_version_button.setObjectName("install_button")
- update_actions_layout.addSpacing(20)
- update_actions_layout.addWidget(new_version_label)
- update_actions_layout.addSpacing(20)
- update_actions_layout.addWidget(skip_version_button)
- update_actions_layout.addSpacing(20)
- update_actions_layout.addWidget(install_version_button)
- update_actions_layout.addStretch(1)
- new_version_layout.addLayout(update_msg_label_layout)
- new_version_layout.addLayout(update_actions_layout)
-
- self.update_available_widget.setLayout(new_version_layout)
-
- # Connect buttons signals to parent class signals
- skip_version_button.clicked.connect(
- lambda checked: self.skip_version.emit(self.update_available_version)
- )
- install_version_button.clicked.connect(
- lambda checked: self.install_version.emit(self.update_available_version)
- )
-
- def show_checking_updates_message(self):
- self.update_widgets.setCurrentWidget(self.checking_update_widget)
-
- def show_up_to_date_message(self):
- self.update_widgets.setCurrentWidget(self.up_to_date_widget)
-
- def show_update_available_message(self, update_available_version):
- self.update_available_version = update_available_version
- if update_available_version:
- self.update_widgets.setCurrentWidget(self.update_available_widget)
-
-
-class PackagesTable(QTableWidget):
- def __init__(self, packages, visible_packages=RELATED_PACKAGES, parent=None):
- super().__init__(parent=parent)
- self.packages = packages
- self.visible_packages = visible_packages
- self.setup()
-
- def _create_item(self, text: str, related_package: bool):
- item = QTableWidgetItem(text)
- if related_package:
- background_brush = QBrush(Qt.GlobalColor.black)
- else:
- background_brush = QBrush(Qt.GlobalColor.darkGray)
- item.setBackground(background_brush)
- if not related_package:
- foreground_brush = QBrush(Qt.GlobalColor.black)
- item.setForeground(foreground_brush)
- return item
-
- def setup(self):
- # Set columns number and headers
- self.setColumnCount(4)
- self.setHorizontalHeaderLabels(["Name", "Version", "Source", "Build"])
- self.verticalHeader().setVisible(False)
-
- # Set horizontal headers alignment and config
- self.horizontalHeader().setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)
- self.horizontalHeader().setStretchLastSection(True)
- self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
-
- # Hide table items borders
- self.setShowGrid(False)
-
- # Set table selection to row
- self.setSelectionBehavior(QAbstractItemView.SelectRows)
-
- def set_data(self, packages):
- self.packages = packages
-
- # Populate table with data available
- for name, version, source, build, related_package in self.packages:
- self.insertRow(self.rowCount())
- package_row = self.rowCount() - 1
- self.setItem(package_row, 0, self._create_item(name, related_package))
- self.setItem(package_row, 1, self._create_item(version, related_package))
- self.setItem(package_row, 2, self._create_item(source, related_package))
- self.setItem(package_row, 3, self._create_item(build, related_package))
- if self.visible_packages == RELATED_PACKAGES and not related_package:
- self.hideRow(package_row)
-
- def change_visible_packages(self, toggled_option):
- if self.packages:
- self.visible_packages = toggled_option
- if toggled_option == RELATED_PACKAGES:
- for idx, package in enumerate(self.packages):
- name, version, source, build, related_package = package
- if not related_package:
- self.hideRow(idx)
- else:
- for idx, _ in enumerate(self.packages):
- self.showRow(idx)
- else:
- self.visible_packages = toggled_option
-
- def change_detailed_info_visibility(self, state):
- if state > Qt.Unchecked:
- self.showColumn(2)
- self.showColumn(3)
- self.change_visible_packages(ALL_PACKAGES)
- else:
- self.hideColumn(2)
- self.hideColumn(3)
- self.change_visible_packages(RELATED_PACKAGES)
-class InstallationManagerDialog(QDialog):
- def __init__(
- self,
- package_name: str,
- install_information,
- parent: Optional[QWidget] = None,
- ):
- super().__init__(parent=parent)
- self.package_name = package_name
- self.current_version = install_information["current_version"]
- self.snapshot_version = install_information["snapshot_version"]
- self.updates_widget = None
- self.packages_tablewidget = None
- self.setWindowTitle(f"{package_name} installation manager")
- self.setMinimumSize(QSize(500, 500))
- self.setup_layout()
+logger = logging.getLogger(__name__)
- def _create_install_information_group(self):
- install_information_group = QGroupBox("Install information")
- install_information_layout = QVBoxLayout()
- current_version_layout = QHBoxLayout()
- # Current version labels and button
- current_version_label = QLabel(
- f"{self.package_name} {self.current_version['version']}"
- )
- last_modified_version_label = QLabel(
- f"Last modified {self.current_version['last_modified']}"
- )
- current_version_open_button = QPushButton("Open")
- current_version_open_button.setObjectName("open_button")
- current_version_layout.addWidget(current_version_label)
- current_version_layout.addSpacing(10)
- current_version_layout.addWidget(last_modified_version_label)
- current_version_layout.addSpacing(10)
- current_version_layout.addWidget(current_version_open_button)
- current_version_layout.addStretch(1)
- install_information_layout.addLayout(current_version_layout)
+def dedup(items: Tuple[Any, ...]) -> Tuple[Any, ...]:
+ """Deduplicate an list of items."""
+ new_items: Tuple[Any, ...] = ()
+ for item in items:
+ if item not in new_items:
+ new_items += (item,)
- # Line to divide current section and update section
- install_version_line = QFrame()
- install_version_line.setObjectName("separator")
- install_version_line.setFrameShape(QFrame.HLine)
- install_version_line.setFrameShadow(QFrame.Sunken)
- install_version_line.setLineWidth(1)
- install_information_layout.addWidget(install_version_line)
+ return new_items
- # Update information widget
- self.updates_widget = UpdateWidget(self.package_name, parent=self)
- install_information_layout.addWidget(self.updates_widget)
- install_information_group.setLayout(install_information_layout)
- # Signals
- # Open button signal
- current_version_open_button.clicked.connect(self.open_installed)
- # Update widget signals
- self.updates_widget.install_version.connect(self.install_version)
- self.updates_widget.skip_version.connect(self.skip_version)
+def _configure_logging(log_level="WARNING"):
+ """Configure logging."""
+ import constructor_manager_api # type: ignore
- return install_information_group
+ log_level = getattr(logging, log_level.upper())
+ log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+ log_format = "%(levelname)s - %(message)s"
+ logging.basicConfig(format=log_format, level=log_level)
- def _create_packages_group(self):
- packages_group = QGroupBox("Packages")
- packages_layout = QVBoxLayout()
+ # Set logging level for libraries used
+ api_logger = logging.getLogger(constructor_manager_api.__name__)
+ api_logger.setLevel(log_level)
- packages_filter_layout = QHBoxLayout()
- packages_filter_label = QLabel("Show:")
- self.packages_spinner_label = SpinnerWidget("Loading packages...", parent=self)
- show_detailed_view_checkbox = QCheckBox("Detailed view")
- show_detailed_view_checkbox.setChecked(False)
- packages_filter_layout.addWidget(packages_filter_label)
- packages_filter_layout.addWidget(show_detailed_view_checkbox)
- packages_filter_layout.addStretch(1)
- packages_filter_layout.addWidget(self.packages_spinner_label)
-
- self.packages_tablewidget = PackagesTable(None, parent=self)
- packages_layout.addLayout(packages_filter_layout)
- packages_layout.addWidget(self.packages_tablewidget)
- packages_group.setLayout(packages_layout)
-
- show_detailed_view_checkbox.stateChanged.connect(
- self.packages_tablewidget.change_detailed_info_visibility
- )
- self.packages_tablewidget.change_detailed_info_visibility(
- show_detailed_view_checkbox.checkState()
- )
-
- return packages_group
-
- def _create_installation_actions_group(self):
- installation_actions_group = QGroupBox("Installation Actions")
- installation_actions_layout = QGridLayout()
-
- # Revert action
- revert_button = QPushButton("Revert Installation")
- revert_label = QLabel(
- "Rollback installation to the latest snapshot: "
- f"{self.snapshot_version['version']} "
- f"({self.snapshot_version['last_modified']})"
- )
- installation_actions_layout.addWidget(revert_button, 0, 0)
- installation_actions_layout.addWidget(revert_label, 0, 1)
-
- # Reset action
- reset_button = QPushButton("Reset Installation")
- reset_label = QLabel(
- "Reset the installation to clear "
- "preferences, plugins, and other packages"
- )
- installation_actions_layout.addWidget(reset_button, 1, 0)
- installation_actions_layout.addWidget(reset_label, 1, 1)
-
- # Uninstall action
- uninstall_button = QPushButton("Uninstall")
- uninstall_button.setObjectName("uninstall_button")
- uninstall_label = QLabel(
- f"Remove the {self.package_name} Bundled App "
- "and Installation Manager from your computer"
- )
- installation_actions_layout.addWidget(uninstall_button, 2, 0)
- installation_actions_layout.addWidget(uninstall_label, 2, 1)
-
- installation_actions_group.setLayout(installation_actions_layout)
-
- # Signals
- revert_button.clicked.connect(self.revert_installation)
- reset_button.clicked.connect(self.reset_installation)
- uninstall_button.clicked.connect(self.uninstall)
-
- return installation_actions_group
-
- def setup_layout(self):
- main_layout = QVBoxLayout(self)
-
- # Install information
- install_information_group = self._create_install_information_group()
- main_layout.addWidget(install_information_group)
-
- # Packages
- packages_group = self._create_packages_group()
- main_layout.addWidget(packages_group, stretch=1)
-
- # Installation Actions
- installation_actions_group = self._create_installation_actions_group()
- main_layout.addWidget(installation_actions_group)
-
- # Layout
- self.setLayout(main_layout)
-
- def open_installed(self):
- # TODO: To be handled with the backend.
- # Maybe this needs to be a signal
- print(self.current_version)
-
- def show_checking_updates_message(self):
- self.updates_widget.show_checking_updates_message()
-
- def show_up_to_date_message(self):
- self.updates_widget.show_up_to_date_message()
-
- def show_update_available_message(self, update_available_version):
- self.updates_widget.show_update_available_message(update_available_version)
-
- def install_version(self, update_version):
- # TODO: To be handled with the backend.
- # Maybe this needs to be a signal
- print(update_version)
-
- def skip_version(self, skip_version):
- # TODO: To be handled with the backend.
- # Maybe this needs to be a signal
- print(skip_version)
-
- def set_packages(self, packages):
- self.packages_spinner_label.show()
- self.packages = packages
- if self.packages_tablewidget:
- self.packages_tablewidget.set_data(self.packages)
- self.packages_spinner_label.hide()
-
- def revert_installation(self):
- # TODO: To be handled with the backend.
- # Maybe this needs to be a signal
- print("Revert installation")
-
- def reset_installation(self):
- # TODO: To be handled with the backend.
- # Maybe this needs to be a signal
- print("Reset installation")
-
- def uninstall(self):
- # TODO: To be handled with the backend.
- # Maybe this needs to be a signal
- print("Uninstall")
-
-
-def main(package_name: str):
+def run():
"""Run the main interface.
Parameters
@@ -436,23 +48,45 @@ def main(package_name: str):
package_name : str
Name of the package that the installation manager is handling.
"""
+ parser = create_parser()
+ args = parser.parse_args()
+ if "channel" in args:
+ if args.channel:
+ args.channel = dedup(args.channel)
+
+ settings = load_settings(args.package)
+ _configure_logging(settings["log"])
+
+ # TODO: Need to add a lock to avoid multiple instances
app = QApplication([])
update_styles(app)
+ print(args)
+
+ if "current_version" in args:
+ current_version = args.current_version or settings["current_version"]
+
+ if "build_string" in args:
+ build_string = args.build_string or settings["build_string"]
+
+ if "plugins_url" in args:
+ plugins_url = args.plugins_url or settings["plugins_url"]
+
+ if "channel" in args:
+ channels = args.channel or dedup(settings["channels"])
+
+ dev = settings["dev"] if settings["dev"] is not None else args.dev
+ log = settings["log"] if settings["log"] is not None else args.log
+
# Installation manager dialog instance
installation_manager_dlg = InstallationManagerDialog(
- package_name,
- INSTALL_INFORMATION,
+ args.package,
+ current_version=current_version,
+ build_string=build_string,
+ plugins_url=plugins_url,
+ channels=channels,
+ dev=dev,
+ log=log,
)
installation_manager_dlg.show()
-
- # Mock data initialization loading.
- # Change commented lines to check different UI update widget states
- def data_initialization():
- installation_manager_dlg.set_packages(PACKAGES)
- installation_manager_dlg.show_update_available_message(UPDATE_AVAILABLE_VERSION)
- # installation_manager_dlg.show_up_to_date_message()
-
- QTimer.singleShot(5000, data_initialization)
-
sys.exit(app.exec_())
diff --git a/constructor-manager-ui/src/constructor_manager_ui/style/base.qss b/constructor-manager-ui/src/constructor_manager_ui/style/base.qss
index 662c1bbc..de0cfe82 100644
--- a/constructor-manager-ui/src/constructor_manager_ui/style/base.qss
+++ b/constructor-manager-ui/src/constructor_manager_ui/style/base.qss
@@ -336,6 +336,12 @@ QHeaderView::section {
/* ----------------- Buttons -------------------- */
+QPushButton:disabled {
+ background-color: @background-color13;
+ background-color: @background-selection-color01;
+ background-color: #758193;
+}
+
QPushButton#open_button, QPushButton#install_button {
background-color: @background-color08;
}
@@ -364,6 +370,10 @@ QPushButton#uninstall_button:pressed {
background-color: @background-color016;
}
+QPushButton#open_button:disabled, QPushButton#install_button:disabled {
+ background-color: @background-color13;
+}
+
/* ----------------- Separator -------------------- */
QFrame#separator {
diff --git a/constructor-manager-ui/src/constructor_manager_ui/style/images.py b/constructor-manager-ui/src/constructor_manager_ui/style/images.py
index 51883bd1..15631198 100644
--- a/constructor-manager-ui/src/constructor_manager_ui/style/images.py
+++ b/constructor-manager-ui/src/constructor_manager_ui/style/images.py
@@ -9,106 +9,106 @@
from qtpy import QtCore
qt_resource_data = b"\
-\x00\x00\x06\x1c\
+\x00\x00\x06\x15\
\x3c\
\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\
\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\
\x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\
-\x6e\x6f\x22\x3f\x3e\x0a\x3c\x21\x2d\x2d\x20\x47\x65\x6e\x65\x72\
-\x61\x74\x6f\x72\x3a\x20\x41\x64\x6f\x62\x65\x20\x49\x6c\x6c\x75\
-\x73\x74\x72\x61\x74\x6f\x72\x20\x32\x33\x2e\x30\x2e\x36\x2c\x20\
-\x53\x56\x47\x20\x45\x78\x70\x6f\x72\x74\x20\x50\x6c\x75\x67\x2d\
-\x49\x6e\x20\x2e\x20\x53\x56\x47\x20\x56\x65\x72\x73\x69\x6f\x6e\
-\x3a\x20\x36\x2e\x30\x30\x20\x42\x75\x69\x6c\x64\x20\x30\x29\x20\
-\x20\x2d\x2d\x3e\x0a\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x76\x65\
+\x6e\x6f\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x76\x65\
\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\x20\x20\x20\x69\
-\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\x0a\x20\x20\x20\x78\
-\x3d\x22\x30\x70\x78\x22\x0a\x20\x20\x20\x79\x3d\x22\x30\x70\x78\
-\x22\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\
-\x30\x20\x31\x30\x30\x20\x31\x30\x30\x22\x0a\x20\x20\x20\x73\x74\
-\x79\x6c\x65\x3d\x22\x65\x6e\x61\x62\x6c\x65\x2d\x62\x61\x63\x6b\
-\x67\x72\x6f\x75\x6e\x64\x3a\x6e\x65\x77\x20\x30\x20\x30\x20\x31\
-\x30\x30\x20\x31\x30\x30\x3b\x22\x0a\x20\x20\x20\x78\x6d\x6c\x3a\
-\x73\x70\x61\x63\x65\x3d\x22\x70\x72\x65\x73\x65\x72\x76\x65\x22\
-\x0a\x20\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\x6f\x63\
-\x6e\x61\x6d\x65\x3d\x22\x6c\x65\x66\x74\x5f\x61\x72\x72\x6f\x77\
-\x2e\x73\x76\x67\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\
-\x65\x3a\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x32\x2e\x31\
-\x20\x28\x39\x63\x36\x64\x34\x31\x65\x34\x31\x30\x2c\x20\x32\x30\
-\x32\x32\x2d\x30\x37\x2d\x31\x34\x29\x22\x0a\x20\x20\x20\x78\x6d\
-\x6c\x6e\x73\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\
-\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\
-\x65\x2e\x6f\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\
-\x2f\x69\x6e\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x78\x6d\
-\x6c\x6e\x73\x3a\x73\x6f\x64\x69\x70\x6f\x64\x69\x3d\x22\x68\x74\
-\x74\x70\x3a\x2f\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2e\x73\x6f\
-\x75\x72\x63\x65\x66\x6f\x72\x67\x65\x2e\x6e\x65\x74\x2f\x44\x54\
-\x44\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2d\x30\x2e\x64\x74\x64\
-\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\
-\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\
-\x30\x30\x2f\x73\x76\x67\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\
-\x3a\x73\x76\x67\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\
-\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\
-\x22\x3e\x3c\x64\x65\x66\x73\x0a\x20\x20\x20\x69\x64\x3d\x22\x64\
-\x65\x66\x73\x37\x22\x20\x2f\x3e\x3c\x73\x6f\x64\x69\x70\x6f\x64\
-\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x0a\x20\x20\x20\x69\
-\x64\x3d\x22\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x35\x22\x0a\x20\
-\x20\x20\x70\x61\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x66\x66\
-\x66\x66\x66\x66\x22\x0a\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\
-\x6f\x6c\x6f\x72\x3d\x22\x23\x30\x30\x30\x30\x30\x30\x22\x0a\x20\
-\x20\x20\x62\x6f\x72\x64\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\
-\x22\x30\x2e\x32\x35\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\
-\x70\x65\x3a\x73\x68\x6f\x77\x70\x61\x67\x65\x73\x68\x61\x64\x6f\
-\x77\x3d\x22\x32\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\
-\x65\x3a\x70\x61\x67\x65\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\
-\x2e\x30\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\
-\x70\x61\x67\x65\x63\x68\x65\x63\x6b\x65\x72\x62\x6f\x61\x72\x64\
-\x3d\x22\x30\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\
-\x3a\x64\x65\x73\x6b\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x64\x31\x64\
-\x31\x64\x31\x22\x0a\x20\x20\x20\x73\x68\x6f\x77\x67\x72\x69\x64\
-\x3d\x22\x66\x61\x6c\x73\x65\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\
-\x63\x61\x70\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x35\x2e\x32\x35\x22\
-\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\
-\x22\x35\x30\x2e\x30\x39\x35\x32\x33\x38\x22\x0a\x20\x20\x20\x69\
-\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\x22\x35\x30\x2e\x30\
-\x39\x35\x32\x33\x38\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\
-\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\
-\x22\x31\x33\x36\x36\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\
-\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x68\x65\x69\x67\x68\x74\
-\x3d\x22\x37\x30\x35\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\
-\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x78\x3d\x22\x2d\x38\x22\
-\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\
-\x64\x6f\x77\x2d\x79\x3d\x22\x2d\x38\x22\x0a\x20\x20\x20\x69\x6e\
-\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x6d\x61\
-\x78\x69\x6d\x69\x7a\x65\x64\x3d\x22\x31\x22\x0a\x20\x20\x20\x69\
-\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x75\x72\x72\x65\x6e\x74\x2d\
-\x6c\x61\x79\x65\x72\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\x20\
-\x2f\x3e\x0a\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\x0a\x20\x20\x20\x70\
-\x6f\x69\x6e\x74\x73\x3d\x22\x32\x30\x2e\x39\x2c\x35\x30\x20\x37\
-\x39\x2e\x31\x2c\x39\x37\x2e\x34\x20\x37\x39\x2e\x31\x2c\x32\x2e\
-\x36\x20\x22\x0a\x20\x20\x20\x69\x64\x3d\x22\x70\x6f\x6c\x79\x67\
-\x6f\x6e\x32\x22\x20\x2f\x3e\x0a\x3c\x70\x61\x74\x68\x0a\x20\x20\
-\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\x23\x66\x66\
-\x66\x66\x66\x66\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\
-\x68\x3a\x30\x2e\x31\x39\x30\x34\x37\x36\x22\x0a\x20\x20\x20\x64\
-\x3d\x22\x4d\x20\x35\x30\x2e\x30\x39\x35\x32\x33\x38\x2c\x37\x33\
-\x2e\x36\x31\x39\x30\x35\x35\x20\x43\x20\x33\x34\x2e\x32\x32\x33\
-\x38\x31\x2c\x36\x30\x2e\x36\x39\x33\x36\x37\x34\x20\x32\x31\x2e\
-\x32\x35\x38\x30\x31\x39\x2c\x35\x30\x2e\x30\x34\x38\x38\x37\x34\
-\x20\x32\x31\x2e\x32\x38\x32\x33\x37\x2c\x34\x39\x2e\x39\x36\x33\
-\x39\x34\x33\x20\x32\x31\x2e\x33\x30\x36\x37\x32\x31\x2c\x34\x39\
-\x2e\x38\x37\x39\x30\x31\x33\x20\x33\x34\x2e\x32\x39\x32\x34\x33\
-\x36\x2c\x33\x39\x2e\x32\x35\x30\x34\x33\x38\x20\x35\x30\x2e\x31\
-\x33\x39\x35\x31\x32\x2c\x32\x36\x2e\x33\x34\x34\x38\x38\x38\x20\
-\x4c\x20\x37\x38\x2e\x39\x35\x32\x33\x38\x31\x2c\x32\x2e\x38\x38\
-\x30\x32\x35\x33\x32\x20\x37\x39\x2e\x30\x30\x30\x37\x34\x35\x2c\
-\x32\x36\x2e\x35\x30\x33\x30\x35\x33\x20\x63\x20\x30\x2e\x30\x32\
-\x36\x36\x2c\x31\x32\x2e\x39\x39\x32\x35\x34\x31\x20\x30\x2e\x30\
-\x32\x36\x36\x2c\x33\x34\x2e\x31\x39\x36\x34\x32\x37\x20\x30\x2c\
-\x34\x37\x2e\x31\x31\x39\x37\x34\x37\x20\x6c\x20\x2d\x30\x2e\x30\
-\x34\x38\x33\x36\x2c\x32\x33\x2e\x34\x39\x36\x39\x34\x37\x20\x7a\
-\x22\x0a\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x31\x38\x32\
-\x22\x20\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\x0a\
+\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\x0a\x20\x20\x20\x76\
+\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x31\x32\x20\x31\
+\x36\x22\x0a\x20\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\
+\x6f\x63\x6e\x61\x6d\x65\x3d\x22\x63\x68\x65\x63\x6b\x2e\x73\x76\
+\x67\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x76\
+\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x32\x2e\x31\x20\x28\x39\
+\x63\x36\x64\x34\x31\x65\x34\x31\x30\x2c\x20\x32\x30\x32\x32\x2d\
+\x30\x37\x2d\x31\x34\x29\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\
+\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\x74\x70\x3a\
+\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\x6f\
+\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\x2f\x69\x6e\
+\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\
+\x3a\x73\x6f\x64\x69\x70\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\x3a\
+\x2f\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\x63\
+\x65\x66\x6f\x72\x67\x65\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\x73\
+\x6f\x64\x69\x70\x6f\x64\x69\x2d\x30\x2e\x64\x74\x64\x22\x0a\x20\
+\x20\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\
+\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\
+\x73\x76\x67\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x76\
+\x67\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\
+\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0a\
+\x20\x20\x3c\x64\x65\x66\x73\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\
+\x22\x64\x65\x66\x73\x39\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x73\x6f\
+\x64\x69\x70\x6f\x64\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\
+\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x6e\x61\x6d\x65\x64\x76\
+\x69\x65\x77\x37\x22\x0a\x20\x20\x20\x20\x20\x70\x61\x67\x65\x63\
+\x6f\x6c\x6f\x72\x3d\x22\x23\x66\x66\x66\x66\x66\x66\x22\x0a\x20\
+\x20\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\x6f\x6c\x6f\x72\x3d\
+\x22\x23\x30\x30\x30\x30\x30\x30\x22\x0a\x20\x20\x20\x20\x20\x62\
+\x6f\x72\x64\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x2e\
+\x32\x35\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\
+\x65\x3a\x73\x68\x6f\x77\x70\x61\x67\x65\x73\x68\x61\x64\x6f\x77\
+\x3d\x22\x32\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\
+\x70\x65\x3a\x70\x61\x67\x65\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\
+\x30\x2e\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\
+\x70\x65\x3a\x70\x61\x67\x65\x63\x68\x65\x63\x6b\x65\x72\x62\x6f\
+\x61\x72\x64\x3d\x22\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\
+\x73\x63\x61\x70\x65\x3a\x64\x65\x73\x6b\x63\x6f\x6c\x6f\x72\x3d\
+\x22\x23\x64\x31\x64\x31\x64\x31\x22\x0a\x20\x20\x20\x20\x20\x73\
+\x68\x6f\x77\x67\x72\x69\x64\x3d\x22\x66\x61\x6c\x73\x65\x22\x0a\
+\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x7a\x6f\
+\x6f\x6d\x3d\x22\x33\x32\x2e\x38\x31\x32\x35\x22\x0a\x20\x20\x20\
+\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\x22\x36\
+\x2e\x30\x30\x33\x38\x30\x39\x35\x22\x0a\x20\x20\x20\x20\x20\x69\
+\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\x22\x38\x2e\x30\x31\
+\x35\x32\x33\x38\x31\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\
+\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x77\x69\x64\x74\
+\x68\x3d\x22\x31\x33\x36\x36\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\
+\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x68\x65\
+\x69\x67\x68\x74\x3d\x22\x37\x30\x35\x22\x0a\x20\x20\x20\x20\x20\
+\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\
+\x78\x3d\x22\x2d\x38\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\
+\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x79\x3d\x22\x2d\
+\x38\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\
+\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x6d\x61\x78\x69\x6d\x69\x7a\x65\
+\x64\x3d\x22\x31\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\
+\x61\x70\x65\x3a\x63\x75\x72\x72\x65\x6e\x74\x2d\x6c\x61\x79\x65\
+\x72\x3d\x22\x67\x34\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x67\x0a\x20\
+\x20\x20\x20\x20\x69\x64\x3d\x22\x67\x34\x22\x3e\x0a\x20\x20\x20\
+\x20\x3c\x70\x61\x74\x68\x0a\x20\x20\x20\x20\x20\x20\x20\x64\x3d\
+\x22\x4d\x31\x32\x20\x35\x6c\x2d\x38\x20\x38\x2d\x34\x2d\x34\x20\
+\x31\x2e\x35\x2d\x31\x2e\x35\x4c\x34\x20\x31\x30\x6c\x36\x2e\x35\
+\x2d\x36\x2e\x35\x4c\x31\x32\x20\x35\x7a\x22\x0a\x20\x20\x20\x20\
+\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x32\x22\x20\x2f\x3e\
+\x0a\x20\x20\x20\x20\x3c\x70\x61\x74\x68\x0a\x20\x20\x20\x20\x20\
+\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\x23\x66\
+\x66\x66\x66\x66\x66\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\
+\x74\x68\x3a\x30\x2e\x30\x33\x30\x34\x37\x36\x32\x22\x0a\x20\x20\
+\x20\x20\x20\x20\x20\x64\x3d\x22\x4d\x20\x32\x2e\x30\x30\x33\x36\
+\x34\x38\x36\x2c\x31\x30\x2e\x39\x37\x38\x38\x38\x37\x20\x30\x2e\
+\x30\x33\x30\x33\x33\x30\x31\x34\x2c\x39\x2e\x30\x30\x35\x33\x39\
+\x33\x37\x20\x30\x2e\x37\x36\x39\x36\x33\x34\x37\x37\x2c\x38\x2e\
+\x32\x36\x36\x35\x35\x31\x32\x20\x31\x2e\x35\x30\x38\x39\x33\x39\
+\x34\x2c\x37\x2e\x35\x32\x37\x37\x30\x38\x33\x20\x32\x2e\x37\x35\
+\x38\x31\x38\x32\x39\x2c\x38\x2e\x37\x37\x37\x32\x33\x30\x36\x20\
+\x34\x2e\x30\x30\x37\x34\x32\x36\x34\x2c\x31\x30\x2e\x30\x32\x36\
+\x37\x35\x33\x20\x37\x2e\x32\x35\x33\x33\x38\x32\x37\x2c\x36\x2e\
+\x37\x38\x30\x39\x30\x33\x20\x31\x30\x2e\x34\x39\x39\x33\x33\x39\
+\x2c\x33\x2e\x35\x33\x35\x30\x35\x33\x31\x20\x31\x31\x2e\x32\x33\
+\x30\x34\x38\x31\x2c\x34\x2e\x32\x36\x36\x36\x37\x31\x34\x20\x31\
+\x31\x2e\x39\x36\x31\x36\x32\x33\x2c\x34\x2e\x39\x39\x38\x32\x38\
+\x39\x36\x20\x37\x2e\x39\x38\x34\x36\x36\x34\x37\x2c\x38\x2e\x39\
+\x37\x35\x33\x33\x35\x33\x20\x43\x20\x35\x2e\x37\x39\x37\x33\x33\
+\x37\x39\x2c\x31\x31\x2e\x31\x36\x32\x37\x31\x31\x20\x34\x2e\x30\
+\x30\x30\x37\x39\x30\x32\x2c\x31\x32\x2e\x39\x35\x32\x33\x38\x31\
+\x20\x33\x2e\x39\x39\x32\x33\x33\x36\x38\x2c\x31\x32\x2e\x39\x35\
+\x32\x33\x38\x31\x20\x63\x20\x2d\x30\x2e\x30\x30\x38\x34\x35\x2c\
+\x30\x20\x2d\x30\x2e\x39\x30\x33\x33\x36\x33\x31\x2c\x2d\x30\x2e\
+\x38\x38\x38\x30\x37\x32\x20\x2d\x31\x2e\x39\x38\x38\x36\x38\x38\
+\x32\x2c\x2d\x31\x2e\x39\x37\x33\x34\x39\x34\x20\x7a\x22\x0a\x20\
+\x20\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x33\x30\
+\x37\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x2f\x67\x3e\x0a\x3c\x2f\x73\
+\x76\x67\x3e\x0a\
\x00\x05\x84\x90\
\x47\
\x49\x46\x38\x39\x61\x00\x01\x00\x01\xf7\xff\x00\xf9\xfa\xfa\x72\
@@ -22803,106 +22803,6 @@
\x2e\x34\x32\x38\x35\x37\x31\x34\x20\x5a\x22\x0a\x20\x20\x20\x69\
\x64\x3d\x22\x70\x61\x74\x68\x31\x32\x38\x22\x20\x2f\x3e\x3c\x2f\
\x73\x76\x67\x3e\x0a\
-\x00\x00\x06\x15\
-\x3c\
-\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\
-\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\
-\x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\
-\x6e\x6f\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x76\x65\
-\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\x20\x20\x20\x69\
-\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\x0a\x20\x20\x20\x76\
-\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x31\x32\x20\x31\
-\x36\x22\x0a\x20\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\
-\x6f\x63\x6e\x61\x6d\x65\x3d\x22\x63\x68\x65\x63\x6b\x2e\x73\x76\
-\x67\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x76\
-\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x32\x2e\x31\x20\x28\x39\
-\x63\x36\x64\x34\x31\x65\x34\x31\x30\x2c\x20\x32\x30\x32\x32\x2d\
-\x30\x37\x2d\x31\x34\x29\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\
-\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\x74\x70\x3a\
-\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\x6f\
-\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\x2f\x69\x6e\
-\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\
-\x3a\x73\x6f\x64\x69\x70\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\x3a\
-\x2f\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\x63\
-\x65\x66\x6f\x72\x67\x65\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\x73\
-\x6f\x64\x69\x70\x6f\x64\x69\x2d\x30\x2e\x64\x74\x64\x22\x0a\x20\
-\x20\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\
-\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\
-\x73\x76\x67\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x76\
-\x67\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\
-\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0a\
-\x20\x20\x3c\x64\x65\x66\x73\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\
-\x22\x64\x65\x66\x73\x39\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x73\x6f\
-\x64\x69\x70\x6f\x64\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\
-\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x6e\x61\x6d\x65\x64\x76\
-\x69\x65\x77\x37\x22\x0a\x20\x20\x20\x20\x20\x70\x61\x67\x65\x63\
-\x6f\x6c\x6f\x72\x3d\x22\x23\x66\x66\x66\x66\x66\x66\x22\x0a\x20\
-\x20\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\x6f\x6c\x6f\x72\x3d\
-\x22\x23\x30\x30\x30\x30\x30\x30\x22\x0a\x20\x20\x20\x20\x20\x62\
-\x6f\x72\x64\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x2e\
-\x32\x35\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\
-\x65\x3a\x73\x68\x6f\x77\x70\x61\x67\x65\x73\x68\x61\x64\x6f\x77\
-\x3d\x22\x32\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\
-\x70\x65\x3a\x70\x61\x67\x65\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\
-\x30\x2e\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\
-\x70\x65\x3a\x70\x61\x67\x65\x63\x68\x65\x63\x6b\x65\x72\x62\x6f\
-\x61\x72\x64\x3d\x22\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\
-\x73\x63\x61\x70\x65\x3a\x64\x65\x73\x6b\x63\x6f\x6c\x6f\x72\x3d\
-\x22\x23\x64\x31\x64\x31\x64\x31\x22\x0a\x20\x20\x20\x20\x20\x73\
-\x68\x6f\x77\x67\x72\x69\x64\x3d\x22\x66\x61\x6c\x73\x65\x22\x0a\
-\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x7a\x6f\
-\x6f\x6d\x3d\x22\x33\x32\x2e\x38\x31\x32\x35\x22\x0a\x20\x20\x20\
-\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\x22\x36\
-\x2e\x30\x30\x33\x38\x30\x39\x35\x22\x0a\x20\x20\x20\x20\x20\x69\
-\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\x22\x38\x2e\x30\x31\
-\x35\x32\x33\x38\x31\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\
-\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x77\x69\x64\x74\
-\x68\x3d\x22\x31\x33\x36\x36\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\
-\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x68\x65\
-\x69\x67\x68\x74\x3d\x22\x37\x30\x35\x22\x0a\x20\x20\x20\x20\x20\
-\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\
-\x78\x3d\x22\x2d\x38\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\
-\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x79\x3d\x22\x2d\
-\x38\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\
-\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x6d\x61\x78\x69\x6d\x69\x7a\x65\
-\x64\x3d\x22\x31\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\
-\x61\x70\x65\x3a\x63\x75\x72\x72\x65\x6e\x74\x2d\x6c\x61\x79\x65\
-\x72\x3d\x22\x67\x34\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x67\x0a\x20\
-\x20\x20\x20\x20\x69\x64\x3d\x22\x67\x34\x22\x3e\x0a\x20\x20\x20\
-\x20\x3c\x70\x61\x74\x68\x0a\x20\x20\x20\x20\x20\x20\x20\x64\x3d\
-\x22\x4d\x31\x32\x20\x35\x6c\x2d\x38\x20\x38\x2d\x34\x2d\x34\x20\
-\x31\x2e\x35\x2d\x31\x2e\x35\x4c\x34\x20\x31\x30\x6c\x36\x2e\x35\
-\x2d\x36\x2e\x35\x4c\x31\x32\x20\x35\x7a\x22\x0a\x20\x20\x20\x20\
-\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x32\x22\x20\x2f\x3e\
-\x0a\x20\x20\x20\x20\x3c\x70\x61\x74\x68\x0a\x20\x20\x20\x20\x20\
-\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\x23\x66\
-\x66\x66\x66\x66\x66\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\
-\x74\x68\x3a\x30\x2e\x30\x33\x30\x34\x37\x36\x32\x22\x0a\x20\x20\
-\x20\x20\x20\x20\x20\x64\x3d\x22\x4d\x20\x32\x2e\x30\x30\x33\x36\
-\x34\x38\x36\x2c\x31\x30\x2e\x39\x37\x38\x38\x38\x37\x20\x30\x2e\
-\x30\x33\x30\x33\x33\x30\x31\x34\x2c\x39\x2e\x30\x30\x35\x33\x39\
-\x33\x37\x20\x30\x2e\x37\x36\x39\x36\x33\x34\x37\x37\x2c\x38\x2e\
-\x32\x36\x36\x35\x35\x31\x32\x20\x31\x2e\x35\x30\x38\x39\x33\x39\
-\x34\x2c\x37\x2e\x35\x32\x37\x37\x30\x38\x33\x20\x32\x2e\x37\x35\
-\x38\x31\x38\x32\x39\x2c\x38\x2e\x37\x37\x37\x32\x33\x30\x36\x20\
-\x34\x2e\x30\x30\x37\x34\x32\x36\x34\x2c\x31\x30\x2e\x30\x32\x36\
-\x37\x35\x33\x20\x37\x2e\x32\x35\x33\x33\x38\x32\x37\x2c\x36\x2e\
-\x37\x38\x30\x39\x30\x33\x20\x31\x30\x2e\x34\x39\x39\x33\x33\x39\
-\x2c\x33\x2e\x35\x33\x35\x30\x35\x33\x31\x20\x31\x31\x2e\x32\x33\
-\x30\x34\x38\x31\x2c\x34\x2e\x32\x36\x36\x36\x37\x31\x34\x20\x31\
-\x31\x2e\x39\x36\x31\x36\x32\x33\x2c\x34\x2e\x39\x39\x38\x32\x38\
-\x39\x36\x20\x37\x2e\x39\x38\x34\x36\x36\x34\x37\x2c\x38\x2e\x39\
-\x37\x35\x33\x33\x35\x33\x20\x43\x20\x35\x2e\x37\x39\x37\x33\x33\
-\x37\x39\x2c\x31\x31\x2e\x31\x36\x32\x37\x31\x31\x20\x34\x2e\x30\
-\x30\x30\x37\x39\x30\x32\x2c\x31\x32\x2e\x39\x35\x32\x33\x38\x31\
-\x20\x33\x2e\x39\x39\x32\x33\x33\x36\x38\x2c\x31\x32\x2e\x39\x35\
-\x32\x33\x38\x31\x20\x63\x20\x2d\x30\x2e\x30\x30\x38\x34\x35\x2c\
-\x30\x20\x2d\x30\x2e\x39\x30\x33\x33\x36\x33\x31\x2c\x2d\x30\x2e\
-\x38\x38\x38\x30\x37\x32\x20\x2d\x31\x2e\x39\x38\x38\x36\x38\x38\
-\x32\x2c\x2d\x31\x2e\x39\x37\x33\x34\x39\x34\x20\x7a\x22\x0a\x20\
-\x20\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x33\x30\
-\x37\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x2f\x67\x3e\x0a\x3c\x2f\x73\
-\x76\x67\x3e\x0a\
\x00\x00\x06\x28\
\x3c\
\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\
@@ -23004,6 +22904,106 @@
\x20\x32\x2e\x38\x31\x31\x32\x35\x32\x35\x20\x5a\x22\x0a\x20\x20\
\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x31\x38\x32\x22\x20\x2f\x3e\
\x3c\x2f\x73\x76\x67\x3e\x0a\
+\x00\x00\x06\x1c\
+\x3c\
+\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\
+\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\
+\x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\
+\x6e\x6f\x22\x3f\x3e\x0a\x3c\x21\x2d\x2d\x20\x47\x65\x6e\x65\x72\
+\x61\x74\x6f\x72\x3a\x20\x41\x64\x6f\x62\x65\x20\x49\x6c\x6c\x75\
+\x73\x74\x72\x61\x74\x6f\x72\x20\x32\x33\x2e\x30\x2e\x36\x2c\x20\
+\x53\x56\x47\x20\x45\x78\x70\x6f\x72\x74\x20\x50\x6c\x75\x67\x2d\
+\x49\x6e\x20\x2e\x20\x53\x56\x47\x20\x56\x65\x72\x73\x69\x6f\x6e\
+\x3a\x20\x36\x2e\x30\x30\x20\x42\x75\x69\x6c\x64\x20\x30\x29\x20\
+\x20\x2d\x2d\x3e\x0a\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x76\x65\
+\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\x20\x20\x20\x69\
+\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\x0a\x20\x20\x20\x78\
+\x3d\x22\x30\x70\x78\x22\x0a\x20\x20\x20\x79\x3d\x22\x30\x70\x78\
+\x22\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\
+\x30\x20\x31\x30\x30\x20\x31\x30\x30\x22\x0a\x20\x20\x20\x73\x74\
+\x79\x6c\x65\x3d\x22\x65\x6e\x61\x62\x6c\x65\x2d\x62\x61\x63\x6b\
+\x67\x72\x6f\x75\x6e\x64\x3a\x6e\x65\x77\x20\x30\x20\x30\x20\x31\
+\x30\x30\x20\x31\x30\x30\x3b\x22\x0a\x20\x20\x20\x78\x6d\x6c\x3a\
+\x73\x70\x61\x63\x65\x3d\x22\x70\x72\x65\x73\x65\x72\x76\x65\x22\
+\x0a\x20\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\x6f\x63\
+\x6e\x61\x6d\x65\x3d\x22\x6c\x65\x66\x74\x5f\x61\x72\x72\x6f\x77\
+\x2e\x73\x76\x67\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\
+\x65\x3a\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x32\x2e\x31\
+\x20\x28\x39\x63\x36\x64\x34\x31\x65\x34\x31\x30\x2c\x20\x32\x30\
+\x32\x32\x2d\x30\x37\x2d\x31\x34\x29\x22\x0a\x20\x20\x20\x78\x6d\
+\x6c\x6e\x73\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\
+\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\
+\x65\x2e\x6f\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\
+\x2f\x69\x6e\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x78\x6d\
+\x6c\x6e\x73\x3a\x73\x6f\x64\x69\x70\x6f\x64\x69\x3d\x22\x68\x74\
+\x74\x70\x3a\x2f\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2e\x73\x6f\
+\x75\x72\x63\x65\x66\x6f\x72\x67\x65\x2e\x6e\x65\x74\x2f\x44\x54\
+\x44\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2d\x30\x2e\x64\x74\x64\
+\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\
+\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\
+\x30\x30\x2f\x73\x76\x67\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\
+\x3a\x73\x76\x67\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\
+\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\
+\x22\x3e\x3c\x64\x65\x66\x73\x0a\x20\x20\x20\x69\x64\x3d\x22\x64\
+\x65\x66\x73\x37\x22\x20\x2f\x3e\x3c\x73\x6f\x64\x69\x70\x6f\x64\
+\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x0a\x20\x20\x20\x69\
+\x64\x3d\x22\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x35\x22\x0a\x20\
+\x20\x20\x70\x61\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x66\x66\
+\x66\x66\x66\x66\x22\x0a\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\
+\x6f\x6c\x6f\x72\x3d\x22\x23\x30\x30\x30\x30\x30\x30\x22\x0a\x20\
+\x20\x20\x62\x6f\x72\x64\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\
+\x22\x30\x2e\x32\x35\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\
+\x70\x65\x3a\x73\x68\x6f\x77\x70\x61\x67\x65\x73\x68\x61\x64\x6f\
+\x77\x3d\x22\x32\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\
+\x65\x3a\x70\x61\x67\x65\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\
+\x2e\x30\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\
+\x70\x61\x67\x65\x63\x68\x65\x63\x6b\x65\x72\x62\x6f\x61\x72\x64\
+\x3d\x22\x30\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\
+\x3a\x64\x65\x73\x6b\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x64\x31\x64\
+\x31\x64\x31\x22\x0a\x20\x20\x20\x73\x68\x6f\x77\x67\x72\x69\x64\
+\x3d\x22\x66\x61\x6c\x73\x65\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\
+\x63\x61\x70\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x35\x2e\x32\x35\x22\
+\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\
+\x22\x35\x30\x2e\x30\x39\x35\x32\x33\x38\x22\x0a\x20\x20\x20\x69\
+\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\x22\x35\x30\x2e\x30\
+\x39\x35\x32\x33\x38\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\
+\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\
+\x22\x31\x33\x36\x36\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\
+\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x68\x65\x69\x67\x68\x74\
+\x3d\x22\x37\x30\x35\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\
+\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x78\x3d\x22\x2d\x38\x22\
+\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\
+\x64\x6f\x77\x2d\x79\x3d\x22\x2d\x38\x22\x0a\x20\x20\x20\x69\x6e\
+\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x6d\x61\
+\x78\x69\x6d\x69\x7a\x65\x64\x3d\x22\x31\x22\x0a\x20\x20\x20\x69\
+\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x75\x72\x72\x65\x6e\x74\x2d\
+\x6c\x61\x79\x65\x72\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\x20\
+\x2f\x3e\x0a\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\x0a\x20\x20\x20\x70\
+\x6f\x69\x6e\x74\x73\x3d\x22\x32\x30\x2e\x39\x2c\x35\x30\x20\x37\
+\x39\x2e\x31\x2c\x39\x37\x2e\x34\x20\x37\x39\x2e\x31\x2c\x32\x2e\
+\x36\x20\x22\x0a\x20\x20\x20\x69\x64\x3d\x22\x70\x6f\x6c\x79\x67\
+\x6f\x6e\x32\x22\x20\x2f\x3e\x0a\x3c\x70\x61\x74\x68\x0a\x20\x20\
+\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\x23\x66\x66\
+\x66\x66\x66\x66\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\
+\x68\x3a\x30\x2e\x31\x39\x30\x34\x37\x36\x22\x0a\x20\x20\x20\x64\
+\x3d\x22\x4d\x20\x35\x30\x2e\x30\x39\x35\x32\x33\x38\x2c\x37\x33\
+\x2e\x36\x31\x39\x30\x35\x35\x20\x43\x20\x33\x34\x2e\x32\x32\x33\
+\x38\x31\x2c\x36\x30\x2e\x36\x39\x33\x36\x37\x34\x20\x32\x31\x2e\
+\x32\x35\x38\x30\x31\x39\x2c\x35\x30\x2e\x30\x34\x38\x38\x37\x34\
+\x20\x32\x31\x2e\x32\x38\x32\x33\x37\x2c\x34\x39\x2e\x39\x36\x33\
+\x39\x34\x33\x20\x32\x31\x2e\x33\x30\x36\x37\x32\x31\x2c\x34\x39\
+\x2e\x38\x37\x39\x30\x31\x33\x20\x33\x34\x2e\x32\x39\x32\x34\x33\
+\x36\x2c\x33\x39\x2e\x32\x35\x30\x34\x33\x38\x20\x35\x30\x2e\x31\
+\x33\x39\x35\x31\x32\x2c\x32\x36\x2e\x33\x34\x34\x38\x38\x38\x20\
+\x4c\x20\x37\x38\x2e\x39\x35\x32\x33\x38\x31\x2c\x32\x2e\x38\x38\
+\x30\x32\x35\x33\x32\x20\x37\x39\x2e\x30\x30\x30\x37\x34\x35\x2c\
+\x32\x36\x2e\x35\x30\x33\x30\x35\x33\x20\x63\x20\x30\x2e\x30\x32\
+\x36\x36\x2c\x31\x32\x2e\x39\x39\x32\x35\x34\x31\x20\x30\x2e\x30\
+\x32\x36\x36\x2c\x33\x34\x2e\x31\x39\x36\x34\x32\x37\x20\x30\x2c\
+\x34\x37\x2e\x31\x31\x39\x37\x34\x37\x20\x6c\x20\x2d\x30\x2e\x30\
+\x34\x38\x33\x36\x2c\x32\x33\x2e\x34\x39\x36\x39\x34\x37\x20\x7a\
+\x22\x0a\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x31\x38\x32\
+\x22\x20\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\x0a\
\x00\x00\x06\x5a\
\x3c\
\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\
@@ -23218,10 +23218,10 @@
\x07\x03\x7d\xc3\
\x00\x69\
\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\
-\x00\x0e\
-\x0e\xde\xf7\x47\
-\x00\x6c\
-\x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x73\x00\x76\x00\x67\
+\x00\x09\
+\x0b\x9e\x89\x07\
+\x00\x63\
+\x00\x68\x00\x65\x00\x63\x00\x6b\x00\x2e\x00\x73\x00\x76\x00\x67\
\x00\x0b\
\x00\xb0\xe2\x96\
\x00\x6c\
@@ -23230,14 +23230,14 @@
\x05\xc6\xb2\xc7\
\x00\x6d\
\x00\x69\x00\x6e\x00\x75\x00\x73\x00\x2e\x00\x73\x00\x76\x00\x67\
-\x00\x09\
-\x0b\x9e\x89\x07\
-\x00\x63\
-\x00\x68\x00\x65\x00\x63\x00\x6b\x00\x2e\x00\x73\x00\x76\x00\x67\
\x00\x0c\
\x06\xe6\xeb\xe7\
\x00\x75\
\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x73\x00\x76\x00\x67\
+\x00\x0e\
+\x0e\xde\xf7\x47\
+\x00\x6c\
+\x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x73\x00\x76\x00\x67\
\x00\x0f\
\x02\x9f\x08\x07\
\x00\x72\
@@ -23251,13 +23251,13 @@
qt_resource_struct_v1 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x07\x00\x00\x00\x02\
-\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x06\x20\
+\x00\x00\x00\x2a\x00\x00\x00\x00\x00\x01\x00\x00\x06\x19\
\x00\x00\x00\x9e\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x83\
\x00\x00\x00\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xa2\xe1\
-\x00\x00\x00\x50\x00\x00\x00\x00\x00\x01\x00\x05\x8a\xb4\
-\x00\x00\x00\x80\x00\x00\x00\x00\x00\x01\x00\x05\x96\x57\
-\x00\x00\x00\x68\x00\x00\x00\x00\x00\x01\x00\x05\x90\x3e\
+\x00\x00\x00\x46\x00\x00\x00\x00\x00\x01\x00\x05\x8a\xad\
+\x00\x00\x00\x5e\x00\x00\x00\x00\x00\x01\x00\x05\x90\x37\
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
+\x00\x00\x00\x7c\x00\x00\x00\x00\x00\x01\x00\x05\x96\x63\
"
qt_resource_struct_v2 = b"\
@@ -23265,20 +23265,20 @@
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x07\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x06\x20\
+\x00\x00\x00\x2a\x00\x00\x00\x00\x00\x01\x00\x00\x06\x19\
\x00\x00\x01\x84\xbb\xb5\x66\x03\
\x00\x00\x00\x9e\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x83\
\x00\x00\x01\x84\xbb\xb5\x66\x03\
\x00\x00\x00\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xa2\xe1\
\x00\x00\x01\x84\xbb\xb5\x66\x00\
-\x00\x00\x00\x50\x00\x00\x00\x00\x00\x01\x00\x05\x8a\xb4\
+\x00\x00\x00\x46\x00\x00\x00\x00\x00\x01\x00\x05\x8a\xad\
\x00\x00\x01\x84\xbb\xb5\x66\x03\
-\x00\x00\x00\x80\x00\x00\x00\x00\x00\x01\x00\x05\x96\x57\
+\x00\x00\x00\x5e\x00\x00\x00\x00\x00\x01\x00\x05\x90\x37\
\x00\x00\x01\x84\xbb\xb5\x66\x03\
-\x00\x00\x00\x68\x00\x00\x00\x00\x00\x01\x00\x05\x90\x3e\
-\x00\x00\x01\x84\xbb\xb5\x66\x00\
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x84\xbb\xb5\x66\x00\
+\x00\x00\x00\x7c\x00\x00\x00\x00\x00\x01\x00\x05\x96\x63\
+\x00\x00\x01\x84\xbb\xb5\x66\x00\
"
qt_version = [int(v) for v in QtCore.qVersion().split('.')]
diff --git a/constructor-manager-ui/src/constructor_manager_ui/tests/test_constructor_manager_ui.py b/constructor-manager-ui/src/constructor_manager_ui/tests/test_constructor_manager_ui.py
index 7d05cc66..9dfe2415 100644
--- a/constructor-manager-ui/src/constructor_manager_ui/tests/test_constructor_manager_ui.py
+++ b/constructor-manager-ui/src/constructor_manager_ui/tests/test_constructor_manager_ui.py
@@ -1,28 +1,5 @@
"""Tests for the constructor manager UI."""
-import pytest # type: ignore
-from constructor_manager_ui.data import (
- INSTALL_INFORMATION,
- PACKAGE_NAME,
- PACKAGES,
- UPDATE_AVAILABLE_VERSION,
-)
-from constructor_manager_ui.main import InstallationManagerDialog
-
-
-@pytest.fixture
-def installation_manager_dlg(qtbot):
- # Initialize installation manager with mock data
- installation_manager_dlg = InstallationManagerDialog(
- PACKAGE_NAME,
- INSTALL_INFORMATION,
- )
- qtbot.addWidget(installation_manager_dlg)
- yield installation_manager_dlg
-
-
-def test_installation_manager_dialog(installation_manager_dlg):
- installation_manager_dlg.show()
- installation_manager_dlg.set_packages(PACKAGES)
- installation_manager_dlg.show_update_available_message(UPDATE_AVAILABLE_VERSION)
+def test_installation_manager_dialog(qtbot):
+ pass
diff --git a/constructor-manager-ui/src/constructor_manager_ui/widgets/__init__.py b/constructor-manager-ui/src/constructor_manager_ui/widgets/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/constructor-manager-ui/src/constructor_manager_ui/widgets/dialog.py b/constructor-manager-ui/src/constructor_manager_ui/widgets/dialog.py
new file mode 100644
index 00000000..f2393b17
--- /dev/null
+++ b/constructor-manager-ui/src/constructor_manager_ui/widgets/dialog.py
@@ -0,0 +1,465 @@
+"""Constructor manager main dialog."""
+
+from typing import Optional, List
+import logging
+
+from qtpy.QtCore import QProcess, QSize, QTimer, Qt
+from qtpy.QtGui import QCloseEvent
+from qtpy.QtWidgets import (
+ QCheckBox,
+ QDialog,
+ QFrame,
+ QGridLayout,
+ QGroupBox,
+ QHBoxLayout,
+ QLabel,
+ QPushButton,
+ QVBoxLayout,
+ QWidget,
+ QTextEdit,
+ QMessageBox,
+)
+
+from constructor_manager_api import api # type: ignore
+
+# To get mock data
+from constructor_manager_ui.data import PackageData
+from constructor_manager_ui.widgets.spinner import SpinnerWidget
+from constructor_manager_ui.widgets.table import PackagesTable
+from constructor_manager_ui.widgets.update import UpdateWidget
+
+# Packages table constants
+RELATED_PACKAGES = 0
+ALL_PACKAGES = 1
+
+
+logger = logging.getLogger(__name__)
+
+
+class InstallationManagerDialog(QDialog):
+ def __init__(
+ self,
+ package_name: str,
+ current_version: Optional[str] = None,
+ build_string: Optional[str] = None,
+ plugins_url: Optional[str] = None,
+ channels: Optional[List[str]] = None,
+ dev: bool = False,
+ log: str = "WARNING",
+ parent: Optional[QWidget] = None,
+ ):
+ super().__init__(parent=parent)
+ self.package_name = package_name
+ self.current_version = current_version
+ self.plugins_url = plugins_url
+ self.build_string = build_string
+ self.channels = channels
+ self.dev = dev
+ self.log = log
+ self._busy = False
+ self.snapshot_version = None
+ self.updates_widget = None
+ self.packages_tablewidget = None
+ self._worker_version = None
+
+ # Setup
+ self.setWindowTitle(f"{package_name} installation manager")
+ self.setMinimumSize(QSize(500, 750))
+ self.setup_layout()
+ self._refresh()
+
+ def _create_install_information_group(self):
+ install_information_group = QGroupBox("Install information")
+ install_information_layout = QVBoxLayout()
+ current_version_layout = QHBoxLayout()
+
+ # Current version labels and button
+ if self.current_version:
+ text = f"{self.package_name} v{self.current_version}"
+ else:
+ text = self.package_name
+
+ self.current_version_label = QLabel(text)
+ last_modified_version_label = QLabel()
+ # f"Last modified {self.current_version['last_modified']}"
+ # )
+ self.current_version_open_button = QPushButton("Open")
+ self.current_version_open_button.setObjectName("open_button")
+ self.refresh_button = QPushButton("Refresh")
+ current_version_layout.addWidget(self.current_version_label)
+ current_version_layout.addSpacing(10)
+ current_version_layout.addWidget(last_modified_version_label)
+ current_version_layout.addSpacing(10)
+ current_version_layout.addWidget(self.current_version_open_button)
+ current_version_layout.addStretch(1)
+ current_version_layout.addWidget(self.refresh_button)
+ install_information_layout.addLayout(current_version_layout)
+
+ # Line to divide current section and update section
+ install_version_line = QFrame()
+ install_version_line.setObjectName("separator")
+ install_version_line.setFrameShape(QFrame.HLine)
+ install_version_line.setFrameShadow(QFrame.Sunken)
+ install_version_line.setLineWidth(1)
+ install_information_layout.addWidget(install_version_line)
+
+ # Update information widget
+ self.updates_widget = UpdateWidget(self.package_name, parent=self)
+ install_information_layout.addWidget(self.updates_widget)
+ install_information_group.setLayout(install_information_layout)
+
+ # Signals
+ # Open button signal
+ self.current_version_open_button.clicked.connect(self.open_installed)
+
+ # Update widget signals
+ self.updates_widget.install_version.connect(self.install_version)
+ self.updates_widget.skip_version.connect(self.skip_version)
+ self.refresh_button.clicked.connect(self._refresh)
+ self.refresh_button.setVisible(False)
+
+ return install_information_group
+
+ def _create_packages_group(self):
+ packages_group = QGroupBox("Packages")
+ packages_layout = QVBoxLayout()
+
+ packages_filter_layout = QHBoxLayout()
+ packages_filter_label = QLabel("Show:")
+ self.packages_spinner_label = SpinnerWidget("Loading packages...", parent=self)
+ show_detailed_view_checkbox = QCheckBox("Detailed view")
+ show_detailed_view_checkbox.setChecked(False)
+
+ packages_filter_layout.addWidget(packages_filter_label)
+ packages_filter_layout.addWidget(show_detailed_view_checkbox)
+ packages_filter_layout.addStretch(1)
+ packages_filter_layout.addWidget(self.packages_spinner_label)
+
+ self.packages_tablewidget = PackagesTable(None, parent=self)
+ packages_layout.addLayout(packages_filter_layout)
+ packages_layout.addWidget(self.packages_tablewidget)
+ packages_group.setLayout(packages_layout)
+
+ show_detailed_view_checkbox.stateChanged.connect(
+ self.packages_tablewidget.change_detailed_info_visibility
+ )
+ self.packages_tablewidget.change_detailed_info_visibility(
+ show_detailed_view_checkbox.checkState()
+ )
+
+ return packages_group
+
+ def _create_installation_actions_group(self):
+ installation_actions_group = QGroupBox("Installation Actions")
+ installation_actions_layout = QGridLayout()
+
+ self.spinner_installation_actions = SpinnerWidget("", parent=self)
+ self.spinner_installation_actions.setVisible(False)
+ installation_actions_layout.addWidget(QLabel(""), 0, 0)
+ installation_actions_layout.addWidget(
+ self.spinner_installation_actions, 0, 1, alignment=Qt.AlignRight
+ )
+
+ # Restore action
+ self._restore_button = QPushButton("Restore Installation")
+ self.restore_label = QLabel(
+ "Restore installation to the latest snapshot of the current version: "
+ # f"{self.snapshot_version['version']} "
+ # f"({self.snapshot_version['last_modified']})"
+ )
+ installation_actions_layout.addWidget(self._restore_button, 1, 0)
+ installation_actions_layout.addWidget(self.restore_label, 1, 1)
+
+ # Revert action
+ self._revert_button = QPushButton("Revert Installation")
+ self.revert_label = QLabel(
+ "Rollback installation to the latest snapshot of the previous version: "
+ # f"{self.snapshot_version['version']} "
+ # f"({self.snapshot_version['last_modified']})"
+ )
+ installation_actions_layout.addWidget(self._revert_button, 2, 0)
+ installation_actions_layout.addWidget(self.revert_label, 2, 1)
+
+ # Reset action
+ self._reset_button = QPushButton("Reset Installation")
+ self.reset_label = QLabel(
+ "Reset the current installation to clear "
+ "preferences, plugins, and other packages"
+ )
+ installation_actions_layout.addWidget(self._reset_button, 3, 0)
+ installation_actions_layout.addWidget(self.reset_label, 3, 1)
+
+ # Uninstall action
+ self.uninstall_button = QPushButton("Uninstall")
+ self.uninstall_button.setObjectName("uninstall_button")
+ self.uninstall_label = QLabel(
+ f"Remove the {self.package_name} Bundled App "
+ "and Installation Manager from your computer"
+ )
+ self.uninstall_button.setVisible(False)
+ self.uninstall_label.setVisible(False)
+ # TODO: to be enabled later on
+ # installation_actions_layout.addWidget(self.uninstall_button, 3, 0)
+ # installation_actions_layout.addWidget(uninstall_label, 3, 1)
+
+ installation_actions_group.setLayout(installation_actions_layout)
+
+ # Signals
+ self._restore_button.clicked.connect(self.restore_installation)
+ self._revert_button.clicked.connect(self.revert_installation)
+ self._reset_button.clicked.connect(self.reset_installation)
+ self.uninstall_button.clicked.connect(self.uninstall)
+
+ return installation_actions_group
+
+ def _create_feedback_group(self):
+ feedback_group = QGroupBox("Feedback")
+ self._status_data = QTextEdit(self)
+ self._status_error = QTextEdit(self)
+
+ self._status_data.setReadOnly(True)
+ self._status_error.setReadOnly(True)
+
+ feedback_layout = QVBoxLayout()
+ feedback_status_layout = QHBoxLayout()
+ feedback_status_layout.addWidget(self._status_data)
+ feedback_status_layout.addWidget(self._status_error)
+
+ feedback_layout.addLayout(feedback_status_layout)
+ feedback_group.setLayout(feedback_layout)
+ return feedback_group
+
+ def setup_layout(self):
+ main_layout = QVBoxLayout(self)
+
+ # Install information
+ install_information_group = self._create_install_information_group()
+ main_layout.addWidget(install_information_group, stretch=2)
+
+ # Packages
+ packages_group = self._create_packages_group()
+ main_layout.addWidget(packages_group, stretch=2)
+
+ # Installation Actions
+ installation_actions_group = self._create_installation_actions_group()
+ main_layout.addWidget(installation_actions_group, stretch=2)
+
+ # Installation Actions
+ feedback_group = self._create_feedback_group()
+ main_layout.addWidget(feedback_group, stretch=1)
+ print(self.log)
+ feedback_group.setVisible(self.log == "DEBUG")
+
+ # Layout
+ self.setLayout(main_layout)
+
+ self.set_version_actions_enabled(False)
+
+ def _refresh(self):
+ self.set_busy(True)
+ self.refresh_button.setVisible(False)
+ self.packages_spinner_label.show()
+ self._worker_version = api.check_version(self.package_name)
+ self._worker_version.finished.connect(self._update_version)
+ self._worker_version.start()
+ self.show_checking_updates_message()
+
+ def _refresh_after_version(self):
+ self._worker_packages = api.check_packages(
+ self.package_name,
+ version=self.current_version,
+ plugins_url=self.plugins_url,
+ )
+ self._worker_packages.finished.connect(self._update_packages)
+ self._worker_packages.start()
+
+ self._worker_updates = api.check_updates(
+ self.package_name,
+ current_version=self.current_version,
+ build_string=self.build_string,
+ channels=self.channels,
+ dev=self.dev,
+ )
+ self._worker_updates.finished.connect(self._update_widget)
+ self._worker_updates.start()
+
+ self.show_checking_updates_message()
+
+ def _update_version(self, result):
+ self._handle_finished(result)
+ if result.get("exit_code") == 0:
+ data = result["data"]
+ self.current_version = data.get("version", "")
+
+ if self.current_version:
+ text = f"v{self.current_version}"
+ else:
+ text = "(Not installed!)"
+
+ self.current_version_open_button.setEnabled(bool(self.current_version))
+
+ self.current_version_label.setText(f"{self.package_name} {text}")
+ self.current_version_open_button.setVisible(True)
+
+ self._refresh_after_version()
+
+ def _update_packages(self, result):
+ self._handle_finished(result)
+ data = result["data"]
+ package_data = []
+ if isinstance(data, dict):
+ packages = data.get("packages", [])
+ for pkg in packages:
+ package_data.append(
+ PackageData(
+ pkg["name"],
+ pkg["version"],
+ pkg["source"],
+ pkg["build_string"],
+ pkg["is_plugin"],
+ )
+ )
+
+ self.set_packages(package_data)
+ self.set_busy(False)
+ self.refresh_button.setVisible(True)
+
+ def _update_widget(self, result):
+ self._handle_finished(result)
+ if result.get("exit_code") == 0:
+ data = result["data"]
+ if data.get("update"):
+ self.show_update_available_message(data.get("latest_version"))
+ else:
+ self.show_up_to_date_message()
+
+ states = data.get("states", [])
+ if states:
+ print(states)
+
+ # Qt Overrides
+ def closeEvent(self, event: QCloseEvent) -> None:
+ """Override Qt method."""
+ if self._busy:
+ QMessageBox.warning(
+ self, "Busy", "Please wait until the current action finishes!"
+ )
+ event.ignore()
+ return
+
+ # while ConstructorManagerWorker._WORKERS:
+ # worker = ConstructorManagerWorker._WORKERS.pop()
+ # self._terminate_worker(worker)
+
+ return super().closeEvent(event)
+
+ # Helpers
+ def _handle_finished(self, result):
+ self._status_data.append("
")
+ self._status_error.append("
")
+ data = result.get("data", {})
+ error = result.get("error", "")
+ self._status_data.append(str(data))
+ self._status_error.append(str(error))
+
+ def _terminate_worker(self, worker):
+ not_running = QProcess.ProcessState.NotRunning
+ if worker and worker.state() != not_running:
+ self._worker_version.terminate()
+
+ def handle_finished(self, result):
+ self._handle_finished(result)
+ self.spinner_installation_actions.hide()
+ self._refresh()
+
+ def show_checking_updates_message(self):
+ self.updates_widget.show_checking_updates_message()
+
+ def show_up_to_date_message(self):
+ self.updates_widget.show_up_to_date_message()
+
+ def show_update_available_message(self, update_available_version):
+ self.updates_widget.show_update_available_message(update_available_version)
+
+ def set_packages(self, packages):
+ self.packages = packages
+ if self.packages_tablewidget:
+ self.packages_tablewidget.set_data(self.packages)
+ self.packages_spinner_label.hide()
+
+ def set_busy(self, value):
+ self._restore_button.setDisabled(value)
+ self._revert_button.setDisabled(value)
+ self._reset_button.setDisabled(value)
+ self._busy = value
+
+ def set_version_actions_enabled(self, value):
+ self._restore_button.setEnabled(value)
+ self._revert_button.setEnabled(value)
+ self._reset_button.setEnabled(value)
+ self.current_version_open_button.setVisible(value)
+
+ # Actions
+ def open_installed(self):
+ self._open_worker = api.open_application(
+ self.package_name, self.current_version
+ )
+ self._open_worker.start()
+ self.current_version_open_button.setEnabled(False)
+
+ # Disable open button for a bit
+ self._timer_open_button = QTimer()
+ self._timer_open_button.timeout.connect(
+ lambda: self.current_version_open_button.setEnabled(True)
+ )
+ self._timer_open_button.setSingleShot(True)
+ self._timer_open_button.start(10000)
+
+ def install_version(self, update_version):
+ print("Update version")
+ worker = api.update(
+ self.package_name,
+ self.current_version,
+ build_string=self.build_string,
+ channels=self.channels,
+ plugins_url=self.plugins_url,
+ dev=self.dev,
+ delayed=False,
+ )
+ worker.finished.connect(self.handle_finished)
+ worker.start()
+ self.set_busy(True)
+
+ def restore_installation(self):
+ self.spinner_installation_actions.set_text("Restoring installation...")
+ self.spinner_installation_actions.show()
+ worker = api.restore(self.package_name)
+ worker.finished.connect(self.handle_finished)
+ worker.start()
+ self.set_busy(True)
+
+ def revert_installation(self):
+ self.spinner_installation_actions.set_text("Reverting installation...")
+ self.spinner_installation_actions.show()
+ worker = api.revert(self.package_name, self.current_version)
+ worker.finished.connect(self.handle_finished)
+ worker.start()
+ self.set_busy(True)
+
+ def reset_installation(self):
+ self.spinner_installation_actions.set_text("Reseting installation...")
+ self.spinner_installation_actions.show()
+ self._worker = api.reset(
+ package_name=self.package_name,
+ current_version=self.current_version,
+ channels=self.channels,
+ )
+ self._worker.finished.connect(self.handle_finished)
+ self._worker.start()
+ self.set_busy(True)
+
+ def skip_version(self, skip_version):
+ pass
+
+ def uninstall(self):
+ pass
diff --git a/constructor-manager-ui/src/constructor_manager_ui/widgets/spinner.py b/constructor-manager-ui/src/constructor_manager_ui/widgets/spinner.py
new file mode 100644
index 00000000..a7664d72
--- /dev/null
+++ b/constructor-manager-ui/src/constructor_manager_ui/widgets/spinner.py
@@ -0,0 +1,49 @@
+"""Constructor manager main interface."""
+
+from typing import Optional
+import logging
+
+from qtpy.QtCore import QSize
+from qtpy.QtGui import QMovie
+from qtpy.QtWidgets import QHBoxLayout, QLabel, QWidget
+
+
+logger = logging.getLogger(__name__)
+
+
+class SpinnerWidget(QWidget):
+ def __init__(self, text: str, parent: Optional[QWidget] = None):
+ super().__init__(parent=parent)
+
+ # Widgets for text and loading gif
+ self.text_label = QLabel(text)
+ spinner_label = QLabel()
+ self.spinner_movie = QMovie(":/images/loading.gif")
+ self.spinner_movie.setScaledSize(QSize(18, 18))
+ spinner_label.setMovie(self.spinner_movie)
+
+ # Set layout for text + loading indicator
+ layout = QHBoxLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(self.text_label)
+ layout.addWidget(spinner_label)
+ layout.addStretch(1)
+ self.setLayout(layout)
+ self.spinner_movie.start()
+
+ def set_text(self, text: str):
+ self.text_label.setText(text)
+
+ def show(self):
+ try:
+ self.spinner_movie.start()
+ except RuntimeError:
+ pass
+ super().show()
+
+ def hide(self):
+ try:
+ self.spinner_movie.stop()
+ except RuntimeError:
+ pass
+ super().hide()
diff --git a/constructor-manager-ui/src/constructor_manager_ui/widgets/table.py b/constructor-manager-ui/src/constructor_manager_ui/widgets/table.py
new file mode 100644
index 00000000..01eb79be
--- /dev/null
+++ b/constructor-manager-ui/src/constructor_manager_ui/widgets/table.py
@@ -0,0 +1,100 @@
+"""Constructor manager main interface."""
+
+import logging
+
+from qtpy.QtCore import Qt
+from qtpy.QtGui import QBrush
+from qtpy.QtWidgets import (
+ QAbstractItemView,
+ QHeaderView,
+ QTableWidget,
+ QTableWidgetItem,
+)
+
+
+# Packages table constants
+RELATED_PACKAGES = 0
+ALL_PACKAGES = 1
+
+
+logger = logging.getLogger(__name__)
+
+
+class PackagesTable(QTableWidget):
+ def __init__(self, packages, visible_packages=RELATED_PACKAGES, parent=None):
+ super().__init__(parent=parent)
+ self.packages = packages
+ self.visible_packages = visible_packages
+ self.setup()
+
+ def _create_item(self, text: str, related_package: bool):
+ item = QTableWidgetItem(text)
+ if related_package:
+ background_brush = QBrush(Qt.GlobalColor.black)
+ else:
+ background_brush = QBrush(Qt.GlobalColor.darkGray)
+
+ item.setBackground(background_brush)
+ if not related_package:
+ foreground_brush = QBrush(Qt.GlobalColor.black)
+ item.setForeground(foreground_brush)
+
+ item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) # type: ignore
+ return item
+
+ def setup(self):
+ # Set columns number and headers
+ self.setColumnCount(4)
+ self.setHorizontalHeaderLabels(["Name", "Version", "Source", "Build"])
+ self.verticalHeader().setVisible(False)
+
+ # Set horizontal headers alignment and config
+ self.horizontalHeader().setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)
+ self.horizontalHeader().setStretchLastSection(True)
+ self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
+
+ # Hide table items borders
+ self.setShowGrid(False)
+
+ # Set table selection to row
+ self.setSelectionBehavior(QAbstractItemView.SelectRows)
+
+ def set_data(self, packages):
+ self.clearContents()
+ self.setRowCount(0)
+ self.packages = packages
+
+ # Populate table with data available
+ for name, version, source, build, related_package in self.packages:
+ self.insertRow(self.rowCount())
+ package_row = self.rowCount() - 1
+ self.setItem(package_row, 0, self._create_item(name, related_package))
+ self.setItem(package_row, 1, self._create_item(version, related_package))
+ self.setItem(package_row, 2, self._create_item(source, related_package))
+ self.setItem(package_row, 3, self._create_item(build, related_package))
+ if self.visible_packages == RELATED_PACKAGES and not related_package:
+ self.hideRow(package_row)
+
+ def change_visible_packages(self, toggled_option):
+ if self.packages:
+ self.visible_packages = toggled_option
+ if toggled_option == RELATED_PACKAGES:
+ for idx, package in enumerate(self.packages):
+ name, version, source, build, related_package = package
+ if not related_package:
+ self.hideRow(idx)
+ else:
+ for idx, _ in enumerate(self.packages):
+ self.showRow(idx)
+ else:
+ self.visible_packages = toggled_option
+
+ def change_detailed_info_visibility(self, state):
+ if state > Qt.Unchecked:
+ self.showColumn(2)
+ self.showColumn(3)
+ self.change_visible_packages(ALL_PACKAGES)
+ else:
+ self.hideColumn(2)
+ self.hideColumn(3)
+ self.change_visible_packages(RELATED_PACKAGES)
diff --git a/constructor-manager-ui/src/constructor_manager_ui/widgets/update.py b/constructor-manager-ui/src/constructor_manager_ui/widgets/update.py
new file mode 100644
index 00000000..8f7984f7
--- /dev/null
+++ b/constructor-manager-ui/src/constructor_manager_ui/widgets/update.py
@@ -0,0 +1,105 @@
+"""Constructor manager update section widgets."""
+
+from typing import Optional
+import logging
+
+from qtpy.QtCore import Signal
+from qtpy.QtWidgets import (
+ QHBoxLayout,
+ QLabel,
+ QPushButton,
+ QStackedWidget,
+ QVBoxLayout,
+ QWidget,
+)
+
+
+from constructor_manager_ui.widgets.spinner import SpinnerWidget
+
+
+logger = logging.getLogger(__name__)
+
+
+class UpdateWidget(QWidget):
+
+ install_version = Signal(str)
+ skip_version = Signal(str)
+
+ def __init__(self, package_name: str, parent: Optional[QWidget] = None):
+ super().__init__(parent=parent)
+ self.package_name = package_name
+ self.update_available_version = None
+
+ # Setup widgets
+ self.checking_update_widget = SpinnerWidget(
+ "Checking for updates...", parent=self
+ )
+ self.up_to_date_widget = QWidget(self)
+ self._initialize_up_to_date_widget()
+ self.update_available_widget = QWidget(self)
+ self._initialize_update_available_widget()
+
+ # Stack widgets to show one at a time and set layout
+ update_widget_layout = QHBoxLayout()
+ self.update_widgets = QStackedWidget(self)
+ self.update_widgets.addWidget(self.checking_update_widget)
+ self.update_widgets.addWidget(self.up_to_date_widget)
+ self.update_widgets.addWidget(self.update_available_widget)
+ update_widget_layout.addWidget(self.update_widgets)
+ self.setLayout(update_widget_layout)
+
+ # Start showing checking updates widget
+ self.show_checking_updates_message()
+
+ def _initialize_up_to_date_widget(self):
+ up_to_date_layout = QVBoxLayout()
+ update_msg_label = QLabel(f"Your {self.package_name} is up to date.")
+ up_to_date_layout.addWidget(update_msg_label)
+
+ self.up_to_date_widget.setLayout(up_to_date_layout)
+
+ def _initialize_update_available_widget(self):
+ new_version_layout = QVBoxLayout()
+ update_msg_label_layout = QHBoxLayout()
+ update_msg_label = QLabel(
+ f"A newer version of {self.package_name} is available!"
+ )
+ update_msg_label_layout.addSpacing(15)
+ update_msg_label_layout.addWidget(update_msg_label)
+
+ update_actions_layout = QHBoxLayout()
+ new_version_label = QLabel(self.update_available_version)
+ self.skip_version_button = QPushButton("Skip This Version")
+ self.install_version_button = QPushButton("Install This Version")
+ self.install_version_button.setObjectName("install_button")
+ update_actions_layout.addSpacing(20)
+ update_actions_layout.addWidget(new_version_label)
+ update_actions_layout.addSpacing(20)
+ update_actions_layout.addWidget(self.skip_version_button)
+ update_actions_layout.addSpacing(20)
+ update_actions_layout.addWidget(self.install_version_button)
+ update_actions_layout.addStretch(1)
+ new_version_layout.addLayout(update_msg_label_layout)
+ new_version_layout.addLayout(update_actions_layout)
+ self.skip_version_button.setVisible(False)
+
+ self.update_available_widget.setLayout(new_version_layout)
+
+ # Connect buttons signals to parent class signals
+ self.skip_version_button.clicked.connect(
+ lambda checked: self.skip_version.emit(self.update_available_version)
+ )
+ self.install_version_button.clicked.connect(
+ lambda checked: self.install_version.emit(self.update_available_version)
+ )
+
+ def show_checking_updates_message(self):
+ self.update_widgets.setCurrentWidget(self.checking_update_widget)
+
+ def show_up_to_date_message(self):
+ self.update_widgets.setCurrentWidget(self.up_to_date_widget)
+
+ def show_update_available_message(self, update_available_version):
+ self.update_available_version = update_available_version
+ if update_available_version:
+ self.update_widgets.setCurrentWidget(self.update_available_widget)
diff --git a/constructor-manager-ui/tox.ini b/constructor-manager-ui/tox.ini
index 5d20d538..7165316d 100644
--- a/constructor-manager-ui/tox.ini
+++ b/constructor-manager-ui/tox.ini
@@ -25,7 +25,8 @@ platform =
passenv =
CI
GITHUB_ACTIONS
- DISPLAY XAUTHORITY
+ DISPLAY
+ XAUTHORITY
NUMPY_EXPERIMENTAL_ARRAY_FUNCTION
PYVISTA_OFF_SCREEN
extras =