diff --git a/BlocksScreen/BlocksScreen.py b/BlocksScreen/BlocksScreen.py index ab198a7e..dab6cb59 100644 --- a/BlocksScreen/BlocksScreen.py +++ b/BlocksScreen/BlocksScreen.py @@ -1,6 +1,5 @@ import logging import sys -import typing from logger import CrashHandler, LogManager, install_crash_handler, setup_logging @@ -39,7 +38,7 @@ def notify(self, a0: QtCore.QObject, a1: QtCore.QEvent) -> bool: # type: ignore RESET = "\033[0m" -def show_splash(window: typing.Optional[QtWidgets.QWidget] = None): +def show_splash(window: QtWidgets.QWidget | None = None): """Show splash screen on app initialization""" logo = QtGui.QPixmap("BlocksScreen/BlocksScreen/lib/ui/resources/logoblocks.png") splash = QtWidgets.QSplashScreen(pixmap=logo) diff --git a/BlocksScreen/lib/filament.py b/BlocksScreen/lib/filament.py index cb4c0232..0c3d0b44 100644 --- a/BlocksScreen/lib/filament.py +++ b/BlocksScreen/lib/filament.py @@ -1,6 +1,7 @@ # Class that represents a filament spool -from typing import Optional +from __future__ import annotations + import enum @@ -29,17 +30,18 @@ def __init__( self, name: str, temperature: int, - brand: Optional[str] = None, - spool_type: Optional[SpoolMaterial] = None, - spool_weight: Optional[float] = None, + brand: str | None = None, + spool_type: SpoolMaterial | None = None, + spool_weight: float | None = None, ): if not isinstance(name, str) or not isinstance(temperature, int): raise TypeError("__init__() invalid argument type") self._name: str = name self._temperature: int = temperature - self._weight: Optional[float] = None - self._brand: Optional[str] = brand + self._weight: float | None = None + self._brand: str | None = brand + self._spool_type: Filament.SpoolMaterial | None = None if spool_type is not None and spool_type in self.SpoolMaterial: self._spool_type = spool_type @@ -55,7 +57,7 @@ def temperature(self) -> int: return self._temperature @property - def weight(self) -> Optional[float]: + def weight(self) -> float | None: if self._weight is None: return return self._weight @@ -65,22 +67,19 @@ def weight(self, new_value: float): self._weight = new_value @property - def brand(self) -> Optional[str]: + def brand(self) -> str | None: return self._brand @brand.setter - def brand(self, new_value: str) -> Optional[str]: + def brand(self, new_value: str) -> None: self._brand = new_value @property - def spool_type(self) -> Optional[SpoolMaterial]: + def spool_type(self) -> SpoolMaterial | None: return self._spool_type @spool_type.setter - def spool_type(self, new): - if new not in self.SpoolMaterial: - if isinstance(new, self.SpoolMaterial): - raise ValueError( - "Spool Material type is invalid" - ) # Correct type but invalid option + def spool_type(self, new) -> None: + if new is not None and not isinstance(new, self.SpoolMaterial): + raise ValueError(f"Spool Material type is invalid: {new!r}") self._spool_type = new diff --git a/BlocksScreen/lib/files.py b/BlocksScreen/lib/files.py index 412f0648..87396151 100644 --- a/BlocksScreen/lib/files.py +++ b/BlocksScreen/lib/files.py @@ -8,7 +8,6 @@ from pathlib import Path import events -from events import ReceivedFileData from lib.moonrakerComm import MoonWebSocket from PyQt6 import QtCore, QtGui, QtWidgets @@ -54,7 +53,7 @@ class FileMetadata: filename: str = "" thumbnail_images: list[QtGui.QImage] = field(default_factory=list) - filament_total: typing.Union[dict, str, float] = field(default_factory=dict) + filament_total: dict | str | float = field(default_factory=dict) estimated_time: int = 0 layer_count: int = -1 total_layer: int = -1 @@ -74,8 +73,8 @@ class FileMetadata: slicer_version: str = "Unknown" gcode_start_byte: int = 0 gcode_end_byte: int = 0 - print_start_time: typing.Optional[float] = None - job_id: typing.Optional[str] = None + print_start_time: float | None = None + job_id: str | None = None def to_dict(self) -> dict: """Convert to dictionary for signal emission.""" @@ -255,7 +254,7 @@ def is_loaded(self) -> bool: """Check if initial load is complete.""" return self._initial_load_complete - def get_file_metadata(self, filename: str) -> typing.Optional[FileMetadata]: + def get_file_metadata(self, filename: str) -> FileMetadata | None: """Get cached metadata for a file.""" return self._files_metadata.get(filename.removeprefix("/")) @@ -279,7 +278,7 @@ def initial_load(self) -> None: self._initial_load_complete = False self.request_dir_info[str, bool].emit("", True) - def handle_filelist_changed(self, data: typing.Union[dict, list]) -> None: + def handle_filelist_changed(self, data: dict | list) -> None: """Handle notify_filelist_changed from Moonraker.""" if isinstance(data, dict) and "params" in data: data = data.get("params", []) @@ -492,7 +491,7 @@ def _process_metadata(self, data: dict) -> None: self.fileinfo.emit(metadata.to_dict()) logger.debug(f"Metadata loaded for: {filename}") - def handle_metadata_error(self, error_data: typing.Union[str, dict]) -> None: + def handle_metadata_error(self, error_data: str | dict) -> None: """ Handle metadata request error from Moonraker. @@ -538,7 +537,7 @@ def _preload_usb_contents(self, usb_path: str) -> None: self._usb_preload_queue.append(usb_path) self.ws.api.get_dir_information(usb_path, True) - def get_cached_usb_files(self, usb_path: str) -> typing.Optional[list[dict]]: + def get_cached_usb_files(self, usb_path: str) -> list[dict] | None: """ Get cached files for a USB path if available. @@ -646,7 +645,7 @@ def on_request_fileinfo(self, filename: str) -> None: @QtCore.pyqtSlot(str, bool, name="get_dir_info") def get_dir_information( self, directory: str = "", extended: bool = True - ) -> typing.Optional[list]: + ) -> list | None: """Get directory information.""" self._current_directory = directory @@ -669,8 +668,8 @@ def eventFilter(self, obj: QtCore.QObject, event: QtCore.QEvent) -> bool: def event(self, event: QtCore.QEvent) -> bool: """Handle object-level events.""" - if event.type() == ReceivedFileData.type(): - if isinstance(event, ReceivedFileData): + if event.type() == events.ReceivedFileData.type(): + if isinstance(event, events.ReceivedFileData): self.handle_message_received(event.method, event.data, event.params) return True return super().event(event) diff --git a/BlocksScreen/lib/moonrakerComm.py b/BlocksScreen/lib/moonrakerComm.py index 5f889d9f..74286e4e 100644 --- a/BlocksScreen/lib/moonrakerComm.py +++ b/BlocksScreen/lib/moonrakerComm.py @@ -21,7 +21,7 @@ class OneShotTokenError(Exception): """Raised when unable to get oneshot token to connect to a websocket""" def __init__(self, message="Unable to get oneshot token", errors=None) -> None: - super(OneShotTokenError).__init__(message, errors) + super().__init__(message, errors) self.errors = errors self.message = message @@ -30,10 +30,6 @@ class MoonWebSocket(QtCore.QObject, threading.Thread): """MoonWebSocket class object for creating a websocket connection to Moonraker.""" QUERY_KLIPPY_TIMEOUT: int = 2 - connected = False - connecting = False - callback_table = {} - _reconnect_count = 0 max_retries = 3 timeout = 3 @@ -45,9 +41,19 @@ class MoonWebSocket(QtCore.QObject, threading.Thread): query_server_info_signal = QtCore.pyqtSignal(name="query_server_information") def __init__(self, parent: QtCore.QObject) -> None: + """Initialize the websocket thread, timers, and Moonraker API helper.""" super().__init__(parent) self.daemon = True + self.connected = False + self.connecting = False + self.disconnected = False + self._reconnect_count = 0 + self.callback_table: dict = {} + + self._state_lock = threading.RLock() + self._request_lock = threading.Lock() + self._host = parent.config.get("host", parser=str, default="localhost") self._port = parent.config.get("port", parser=int, default=7125) @@ -55,10 +61,11 @@ def __init__(self, parent: QtCore.QObject) -> None: self._callback = None self._wst = None self._request_id = 0 - self.request_table = {} + self.request_table: dict = {} + self._klippy_retry_count = 0 self._moonRest = MoonRest(host=self._host, port=self._port) self.api: MoonAPI = MoonAPI(self) - self._retry_timer: RepeatedTimer + self._retry_timer: RepeatedTimer | None = None websocket.setdefaulttimeout(self.timeout) self.query_server_info_signal.connect(self.api.api_query_server_info) @@ -69,41 +76,56 @@ def __init__(self, parent: QtCore.QObject) -> None: self.klippy_state_signal.connect(self.api.request_printer_info) logger.info("Websocket object initialized") + @property + def moonRest(self) -> MoonRest: + """Returns the current moonrestAPI object""" + return self._moonRest + @QtCore.pyqtSlot(name="retry_wb_conn") def retry_wb_conn(self): """Retry websocket connection""" - if self.connecting is True and self.connected is False: - return False - self._reconnect_count = 0 + with self._state_lock: + if self.connecting is True and self.connected is False: + return False + self._reconnect_count = 0 self.try_connection() def try_connection(self): """Try connecting to websocket""" - self.connecting = True + with self._state_lock: + self.connecting = True + if self._retry_timer is not None: + self._retry_timer.stopTimer() self._retry_timer = RepeatedTimer(self.timeout, self.reconnect) return self.connect() def reconnect(self): """Reconnect to websocket""" - if self.connected: - return True - - if self._reconnect_count >= self.max_retries: - self._retry_timer.stopTimer() + with self._state_lock: + if self.connected: + return True + over_limit = self._reconnect_count >= self.max_retries + + if over_limit: + if self._retry_timer is not None: + self._retry_timer.stopTimer() unable_to_connect_event = WebSocketError( data="Unable to establish connection to Websocket" ) self.connecting_signal[int].emit(0) - self.connecting = False + with self._state_lock: + self.connecting = False try: instance = QtWidgets.QApplication.instance() if instance is not None: - instance.sendEvent(self.parent(), unable_to_connect_event) + instance.postEvent(self.parent(), unable_to_connect_event) else: raise TypeError("QApplication.instance expected ad non-None value") except Exception as e: logger.error( - f"Error on sending Event {unable_to_connect_event.__class__.__name__} | Error message: {e}" + "Error on sending Event %s | Error message: %s", + unable_to_connect_event.__class__.__name__, + e, ) logger.info( "Maximum number of connection retries reached, Unable to establish connection with Moonraker" @@ -113,17 +135,19 @@ def reconnect(self): def connect(self) -> bool: """Connect to websocket""" - if self.connected: - logger.info("Connection established") - return True - self._reconnect_count += 1 - self.connecting_signal[int].emit(int(self._reconnect_count)) + with self._state_lock: + if self.connected: + logger.info("Connection established") + return True + self._reconnect_count += 1 + _count_snapshot = self._reconnect_count + self.connecting_signal[int].emit(int(_count_snapshot)) logger.debug( - f"Establishing connection to Moonraker...\n Try number {self._reconnect_count}" + "Establishing connection to Moonraker...\n Try number %d", + _count_snapshot, ) - # TODO Handle if i cannot connect to moonraker, request server.info and see if i get a result try: - _oneshot_token = self._moonRest.get_oneshot_token() + _oneshot_token = self.moonRest.get_oneshot_token() if _oneshot_token is None: raise OneShotTokenError("Unable to retrieve oneshot token") except Exception as e: @@ -152,7 +176,7 @@ def connect(self) -> bool: logger.debug(self.ws.url) self._wst.start() except Exception as e: - logger.info(f"Unexpected while starting websocket {self._wst.name}: {e}") + logger.info("Unexpected while starting websocket %s: %s", self._wst.name, e) return False return True @@ -161,17 +185,17 @@ def wb_disconnect(self) -> None: if self._wst is not None and self.ws is not None: self.ws.close() if self._wst.is_alive(): - self._wst.join() + self._wst.join(timeout=self.timeout + 1) logger.info("Websocket closed") def on_error(self, *args) -> None: """Websocket error callback""" # First argument is ws second is error message - # TODO: Handle error messages _error = args[1] if len(args) == 2 else args[0] - logger.info(f"Websocket error, disconnected: {_error}") - self.connected = False - self.disconnected = True + logger.error("Websocket error, disconnected: %s", _error) + with self._state_lock: + self.connected = False + self.disconnected = True def on_close(self, *args) -> None: """Websocket on close callback @@ -184,7 +208,8 @@ def on_close(self, *args) -> None: return _close_status_code = args[1] if len(args) == 3 else None _close_message = args[2] if len(args) == 3 else None - self.connected = False + with self._state_lock: + self.connected = False self.ws.keep_running = False self.connection_lost[str].emit( f"code: {_close_status_code} | message {_close_message}" @@ -220,8 +245,10 @@ def on_open(self, *args) -> None: TypeError: When QApplication.instance `is` None """ _ws = args[0] if len(args) == 1 else None - self.connecting = False - self.connected = True + with self._state_lock: + self.connecting = False + self.connected = True + self._klippy_retry_count = 0 self.evaluate_klippy_status() open_event = WebSocketOpen(data="Connected") try: @@ -231,11 +258,12 @@ def on_open(self, *args) -> None: else: raise TypeError("QApplication.instance expected non None value") except Exception as e: - logger.info(f"Unexpected error opening websocket: {e}") + logger.info("Unexpected error opening websocket: %s", e) self.connected_signal.emit() - self._retry_timer.stopTimer() - logger.info(f"Connection to websocket achieved on {_ws}") + if self._retry_timer is not None: + self._retry_timer.stopTimer() + logger.info("Connection to websocket achieved on %s", _ws) def on_message(self, *args) -> None: """Websocket on message callback @@ -247,24 +275,44 @@ def on_message(self, *args) -> None: args[1] if len(args) == 2 else args[0] ) # First argument is ws second is message - response: dict = json.loads(_message) - if "id" in response and response["id"] in self.request_table: - _entry = self.request_table.pop(response["id"]) + try: + response: dict = json.loads(_message) + except json.JSONDecodeError as e: + logger.error("Failed to decode websocket message: %s", e) + return + if "id" in response: + with self._request_lock: + _entry = self.request_table.pop(response["id"], None) + else: + _entry = None + + if _entry is not None: if "server.info" in _entry[0]: - if response["result"]["klippy_state"] == "ready": + if "error" in response: + return + _result = response.get("result", {}) + _klippy_state = _result.get("klippy_state") + if not _klippy_state: + return + if _klippy_state == "ready": self.query_klippy_status_timer.stopTimer() + self._klippy_retry_count = 0 self.api.update_status() # Request update status immediately after klippy ready DEVDEBT - elif response["result"]["klippy_state"] == "startup": - # request server.info in 2 seconds - if not self.query_klippy_status_timer.running: - self.query_klippy_status_timer.startTimer() - elif response["result"]["klippy_state"] == "disconnected": - if not self.query_klippy_status_timer.running: + elif _klippy_state in ("startup", "disconnected"): + self._klippy_retry_count += 1 + if self._klippy_retry_count >= 30: + self.query_klippy_status_timer.stopTimer() + logger.error( + "Klippy startup sequence timed out after %d retries (state=%s)", + self._klippy_retry_count, + _klippy_state, + ) + elif not self.query_klippy_status_timer.running: self.query_klippy_status_timer.startTimer() self.klippy_connected_signal.emit( - response["result"]["klippy_connected"] + _result.get("klippy_connected", False) ) - self.klippy_state_signal.emit(response["result"]["klippy_state"]) + self.klippy_state_signal.emit(_klippy_state) return else: if "error" in response: @@ -276,13 +324,15 @@ def on_message(self, *args) -> None: else: message_event = WebSocketMessageReceived( method=str(_entry[0]), - data=response["result"], + data=response.get("result", {}), metadata=_entry, ) elif "method" in response: if ( str(response["method"]).lower() == "notify_klippy_disconnected" ): # Checkout for notify_klippy_disconnect + self.klippy_state_signal.emit("disconnected") + self._klippy_retry_count = 0 self.evaluate_klippy_status() message_event = ( @@ -292,6 +342,8 @@ def on_message(self, *args) -> None: metadata=None, ) ) + else: + return try: instance = QtWidgets.QApplication.instance() @@ -300,36 +352,46 @@ def on_message(self, *args) -> None: else: raise TypeError("QApplication.instance expected non None value") except Exception as e: - logger.info(f"Unexpected error while creating websocket message event: {e}") + logger.info( + "Unexpected error while creating websocket message event: %s", e + ) - def send_request(self, method: str, params: dict = {}) -> bool: + def send_request(self, method: str, params: dict | None = None) -> bool: """Send a request over the websocket Args: method (str): Websocket method name - params (dict, optional): parameters for the websocket method. Defaults to {}. + params (dict, optional): parameters for the websocket method. Defaults to None. Returns: bool: Whether the method finished and a request was sent """ - if not self.connected or self.ws is None: + if params is None: + params = {} + with self._state_lock: + _connected = self.connected + _ws = self.ws + if not _connected or _ws is None: return False - self._request_id += 1 - self.request_table[self._request_id] = [method, params] + with self._request_lock: + self._request_id += 1 + _rid = self._request_id + self.request_table[_rid] = [method, params] + packet = { "jsonrpc": "2.0", "method": method, "params": params, - "id": self._request_id, + "id": _rid, } - self.ws.send(json.dumps(packet)) + _ws.send(json.dumps(packet)) return True class MoonAPI(QtCore.QObject): def __init__(self, ws: MoonWebSocket): - super(MoonAPI, self).__init__(ws) + super().__init__(ws) self._ws: MoonWebSocket = ws @QtCore.pyqtSlot(name="api_query_server_info") @@ -446,11 +508,11 @@ def restart_service(self, service): @QtCore.pyqtSlot(name="firmware_restart") def firmware_restart(self): - """Request Klipper firmware restart + """`POST MoonrakerAPI` /printer/firmware_restart + Firmware restart to Klipper - HTTP_REQUEST: POST /printer/firmware_restart - - JSON_RPC_REQUEST: printer.firmware_restart + Returns: + str: Returns an 'ok' from Moonraker """ return self._ws.send_request(method="printer.firmware_restart") @@ -605,7 +667,7 @@ def move_file(self, source_dir: str, dest_dir: str): isinstance(source_dir, str) is False or isinstance(dest_dir, str) is False or source_dir is None - or dest_dir is False + or dest_dir is None ): return False return self._ws.send_request( @@ -619,7 +681,7 @@ def copy_file(self, source_dir: str, dest_dir: str): isinstance(source_dir, str) is False or isinstance(dest_dir, str) is False or source_dir is None - or dest_dir is False + or dest_dir is None ): return False return self._ws.send_request( @@ -732,7 +794,7 @@ def update_status(self, refresh: bool = False) -> bool: @QtCore.pyqtSlot(name="update-refresh") @QtCore.pyqtSlot(str, name="update-refresh") - def refresh_update_status(self, name: str = None) -> bool: + def refresh_update_status(self, name: str | None = None) -> bool: """Refresh packages state""" if isinstance(name, str): return self._ws.send_request( @@ -763,7 +825,9 @@ def update_client(self, client_name: str = "") -> bool: """Issue client update""" if not isinstance(client_name, str) or not client_name: return False - return self._ws.send_request(method="machine.update.client") + return self._ws.send_request( + method="machine.update.client", params={"name": client_name} + ) @QtCore.pyqtSlot(name="update-system") def update_system(self): @@ -787,7 +851,7 @@ def rollback_update(self, name: str): if not isinstance(name, str) or not name: return False return self._ws.send_request( - method="machine,update.rollback", params={"name": name} + method="machine.update.rollback", params={"name": name} ) def history_list(self, limit, start, since, before, order): diff --git a/BlocksScreen/lib/moonrest.py b/BlocksScreen/lib/moonrest.py index 2c663531..079ca577 100644 --- a/BlocksScreen/lib/moonrest.py +++ b/BlocksScreen/lib/moonrest.py @@ -29,7 +29,6 @@ import logging import requests -from requests import Request, Response logger = logging.getLogger(__name__) @@ -38,7 +37,7 @@ class UncallableError(Exception): """Raised when a method is not callable""" def __init__(self, message="Unable to call method", errors=None): - super(UncallableError, self).__init__(message, errors) + super().__init__(message, errors) self.errors = errors self.message = message @@ -128,7 +127,7 @@ def _request( _headers = {"x-api-key": self._api_key} if self._api_key else {} try: if hasattr(requests, request_type): - _request_method: Request = getattr(requests, request_type) + _request_method = getattr(requests, request_type) if not callable(_request_method): raise UncallableError( "Invalid request method", @@ -142,9 +141,9 @@ def _request( headers=_headers, timeout=timeout, ) - if isinstance(response, Response): + if isinstance(response, requests.Response): response.raise_for_status() return response.json() if json_response else response.content except Exception as e: - logger.info(f"Unexpected error while sending HTTP request: {e}") + logger.info("Unexpected error while sending HTTP request: %s", e) diff --git a/BlocksScreen/lib/panels/controlTab.py b/BlocksScreen/lib/panels/controlTab.py index 8df4ab4d..70484c9e 100644 --- a/BlocksScreen/lib/panels/controlTab.py +++ b/BlocksScreen/lib/panels/controlTab.py @@ -1,717 +1,726 @@ -from __future__ import annotations -import re -import typing -from functools import partial -import logging - -from helper_methods import normalize -from lib.moonrakerComm import MoonWebSocket -from lib.panels.widgets.numpadPage import CustomNumpad -from lib.panels.widgets.optionCardWidget import OptionCard -from lib.panels.widgets.popupDialogWidget import Popup -from lib.panels.widgets.printcorePage import SwapPrintcorePage -from lib.panels.widgets.probeHelperPage import ProbeHelper -from lib.panels.widgets.slider_selector_page import SliderPage -from lib.printer import Printer -from lib.ui.controlStackedWidget_ui import Ui_controlStackedWidget -from lib.utils.display_button import DisplayButton -from PyQt6 import QtCore, QtGui, QtWidgets - - -_logger = logging.getLogger(__name__) - - -class ControlTab(QtWidgets.QStackedWidget): - """Printer Control Stacked Widget""" - - request_back_button = QtCore.pyqtSignal(name="request-back-button") - request_change_page = QtCore.pyqtSignal(int, int, name="request-change-page") - request_numpad_signal = QtCore.pyqtSignal( - int, - str, - str, - "PyQt_PyObject", - QtWidgets.QStackedWidget, - name="request-numpad", - ) - run_gcode_signal: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, name="run-gcode" - ) - disable_popups: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - bool, name="disable-popups" - ) - request_numpad: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - [str, int, "PyQt_PyObject"], - [str, int, "PyQt_PyObject", int, int], - name="request-numpad", - ) - request_file_info: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, name="request-file-info" - ) - call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") - toggle_conn_page = QtCore.pyqtSignal(bool, name="call-load-panel") - tune_display_buttons: dict = {} - card_options: dict = {} - - def __init__( - self, - parent: QtWidgets.QWidget, - ws: MoonWebSocket, - printer: Printer, - /, - ) -> None: - super().__init__(parent) - self.panel = Ui_controlStackedWidget() - self.panel.setupUi(self) - - self.popup = Popup(self) - - self.ws: MoonWebSocket = ws - self.printer: Printer = printer - self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.timers = [] - self.ztilt_state = False - self.extruder_info: dict = {} - self.bed_info: dict = {} - self.toolhead_info: dict = {} - self.extrude_length: int = 10 - self.extrude_feedrate: int = 2 - self.extrude_page_message: str = "" - self.move_length: float = 1.0 - self.move_speed: float = 25.0 - self.probe_helper_page = ProbeHelper(self) - self.probe_helper_page.toggle_conn_page.connect(self.toggle_conn_page) - self.probe_helper_page.disable_popups.connect(self.disable_popups) - self.addWidget(self.probe_helper_page) - self.probe_helper_page.call_load_panel.connect(self.call_load_panel) - self.printcores_page = SwapPrintcorePage(self) - self.addWidget(self.printcores_page) - - self.sliderPage = SliderPage(self) - self.addWidget(self.sliderPage) - self.sliderPage.request_back.connect(self.back_button) - - self.probe_helper_page.request_page_view.connect( - partial(self.change_page, self.indexOf(self.probe_helper_page)) - ) - self.probe_helper_page.query_printer_object.connect(self.ws.api.object_query) - self.probe_helper_page.run_gcode_signal.connect(self.ws.api.run_gcode) - self.probe_helper_page.request_back.connect(self.back_button) - self.printer.print_stats_update[str, str].connect( - self.probe_helper_page.on_print_stats_update - ) - self.printer.print_stats_update[str, dict].connect( - self.probe_helper_page.on_print_stats_update - ) - self.printer.print_stats_update[str, float].connect( - self.probe_helper_page.on_print_stats_update - ) - self.printer.available_gcode_cmds.connect( - self.probe_helper_page.on_available_gcode_cmds - ) - self.probe_helper_page.subscribe_config[str, "PyQt_PyObject"].connect( - self.printer.on_subscribe_config - ) - self.probe_helper_page.subscribe_config[list, "PyQt_PyObject"].connect( - self.printer.on_subscribe_config - ) - self.printer.extruder_update.connect(self.probe_helper_page.on_extruder_update) - self.printer.gcode_move_update.connect( - self.probe_helper_page.on_gcode_move_update - ) - self.printer.manual_probe_update.connect( - self.probe_helper_page.on_manual_probe_update - ) - self.printer.printer_config.connect(self.probe_helper_page.on_printer_config) - self.printer.gcode_response.connect( - self.probe_helper_page.handle_gcode_response - ) - self.printer.toolhead_update[str, list].connect(self.on_toolhead_update) - self.printer.extruder_update.connect(self.on_extruder_update) - self.printer.heater_bed_update.connect(self.on_heater_bed_update) - self.panel.cp_motion_btn.clicked.connect( - partial(self.change_page, self.indexOf(self.panel.motion_page)) - ) - self.panel.cp_temperature_btn.clicked.connect( - partial(self.change_page, self.indexOf(self.panel.temperature_page)) - ) - self.panel.cp_fans_btn.clicked.connect( - partial(self.change_page, self.indexOf(self.panel.fans_page)) - ) - self.panel.fans_back_btn.clicked.connect(self.back_button) - self.panel.cp_switch_print_core_btn.clicked.connect(self.show_swapcore) - self.panel.cp_nozzles_calibration_btn.clicked.connect( - partial(self.change_page, self.indexOf(self.probe_helper_page)) - ) - self.panel.motion_extrude_btn.clicked.connect( - partial(self.change_page, self.indexOf(self.panel.extrude_page)) - ) - self.panel.motion_move_axis_btn.clicked.connect( - partial(self.change_page, self.indexOf(self.panel.move_axis_page)) - ) - self.panel.mp_back_btn.clicked.connect(self.back_button) - self.panel.motion_auto_home_btn.clicked.connect( - partial(self.run_gcode_signal.emit, "G28\nM400") - ) - self.panel.motion_disable_steppers_btn.clicked.connect( - partial(self.run_gcode_signal.emit, "M84\nM400") - ) - self.panel.exp_back_btn.clicked.connect(self.back_button) - self.panel.extrude_select_length_10_btn.toggled.connect( - partial( - self.handle_toggle_extrude_length, - caller=self.panel.extrude_select_length_10_btn, - value=10, - ) - ) - self.panel.extrude_select_length_50_btn.toggled.connect( - partial( - self.handle_toggle_extrude_length, - caller=self.panel.extrude_select_length_50_btn, - value=50, - ) - ) - self.panel.extrude_select_length_100_btn.toggled.connect( - partial( - self.handle_toggle_extrude_length, - caller=self.panel.extrude_select_length_100_btn, - value=100, - ) - ) - self.panel.extrude_select_feedrate_2_btn.toggled.connect( - partial( - self.handle_toggle_extrude_feedrate, - caller=self.panel.extrude_select_feedrate_2_btn, - value=2, - ) - ) - self.panel.extrude_select_feedrate_5_btn.toggled.connect( - partial( - self.handle_toggle_extrude_feedrate, - caller=self.panel.extrude_select_feedrate_5_btn, - value=5, - ) - ) - self.panel.extrude_select_feedrate_10_btn.toggled.connect( - partial( - self.handle_toggle_extrude_feedrate, - caller=self.panel.extrude_select_feedrate_10_btn, - value=10, - ) - ) - self.panel.mva_select_length_1_btn.toggled.connect( - partial(self.handle_select_move_length, value=1.0) - ) - self.panel.mva_select_length_10_btn.toggled.connect( - partial(self.handle_select_move_length, value=10.0) - ) - self.panel.mva_select_length_100_btn.toggled.connect( - partial(self.handle_select_move_length, value=100.0) - ) - self.panel.mva_select_speed_25_btn.toggled.connect( - partial(self.handle_select_move_speed, value=25.0) - ) - self.panel.mva_select_speed_50_btn.toggled.connect( - partial(self.handle_select_move_speed, value=50.0) - ) - self.panel.mva_select_speed_100_btn.toggled.connect( - partial(self.handle_select_move_speed, value=100.0) - ) - self.panel.exp_extrude_btn.clicked.connect( - partial(self.handle_extrusion, True) - ) # True for extrusion - self.panel.exp_unextrude_btn.clicked.connect( - partial(self.handle_extrusion, False) - ) # False for retraction - # Move Axis - self.panel.mva_back_btn.clicked.connect(self.back_button) - self.panel.mva_home_x_btn.clicked.connect( - partial(self.run_gcode_signal.emit, "G28 X\nM400") - ) - self.panel.mva_home_y_btn.clicked.connect( - partial(self.run_gcode_signal.emit, "G28 Y\nM400") - ) - self.panel.mva_home_z_btn.clicked.connect( - partial(self.run_gcode_signal.emit, "G28 Z\nM400") - ) - self.panel.mva_home_all_btn.clicked.connect( - partial(self.run_gcode_signal.emit, "G28\nM400") - ) - self.panel.mva_up_btn.clicked.connect(partial(self.handle_move_axis, "Y")) - self.panel.mva_down_btn.clicked.connect(partial(self.handle_move_axis, "Y-")) - self.panel.mva_right_btn.clicked.connect(partial(self.handle_move_axis, "X")) - self.panel.mva_left_btn.clicked.connect(partial(self.handle_move_axis, "X-")) - self.panel.mva_z_up.clicked.connect( - partial(self.handle_move_axis, "Z-") # Move nozzle closer to bed - ) - self.panel.mva_z_down.clicked.connect( - partial(self.handle_move_axis, "Z") # Move nozzle away from bed - ) - self.panel.temp_back_button.clicked.connect(self.back_button) - self.panel.printer_settings_back_btn.clicked.connect(self.back_button) - self.run_gcode_signal.connect(self.ws.api.run_gcode) - # @ object temperature change clicked - self.numpadPage = CustomNumpad(self) - self.numpadPage.request_back.connect(self.request_back_button) - self.addWidget(self.numpadPage) - - self.request_numpad[str, int, "PyQt_PyObject", int, int].connect( - self.on_numpad_request - ) - - self.panel.cooldown_btn.clicked.connect( - lambda: self.run_gcode_signal.emit( - "SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=0\n\ - SET_HEATER_TEMPERATURE HEATER=extruder TARGET=0" - ) - ) - - self.path = { - "fan_cage": QtGui.QPixmap(":/fan_related/media/btn_icons/fan_cage.svg"), - "blower": QtGui.QPixmap(":/fan_related/media/btn_icons/blower.svg"), - "fan": QtGui.QPixmap(":/fan_related/media/btn_icons/fan.svg"), - } - - self.panel.cp_z_tilt_btn.clicked.connect(lambda: self.handle_ztilt()) - - self.printcores_page.pc_accept.clicked.connect(self.handle_swapcore) - - self.ws.klippy_state_signal.connect(self.on_klippy_status) - self.ws.klippy_state_signal.connect(self.probe_helper_page.on_klippy_status) - self.printer.on_printcore_update.connect(self.handle_printcoreupdate) - self.printer.gcode_response.connect(self._handle_gcode_response) - self.printer.z_tilt_update.connect(self._handle_z_tilt_object_update) - # self.panel.cp_printer_settings_btn.hide() - self.panel.temperature_cooldown_btn.hide() - self.panel.cooldown_btn.hide() - self.panel.cp_switch_print_core_btn.hide() - - self.printer.fan_update[str, str, float].connect(self.on_fan_object_update) - self.printer.fan_update[str, str, int].connect(self.on_fan_object_update) - - self.printer.printer_config.connect(self.on_printer_config) - - def _handle_z_tilt_object_update(self, value, state): - if state: - self.call_load_panel.emit(False, "") - - @QtCore.pyqtSlot(str, str, float, name="on_fan_update") - @QtCore.pyqtSlot(str, str, int, name="on_fan_update") - def on_fan_object_update( - self, name: str, field: str, new_value: int | float - ) -> None: - """Slot that receives updates from fan objects. - - Args: - name (str): Fan object name - field (str): Field name - new_value (int | float): New value for the field - """ - if "speed" not in field: - return - - fields = name.split() - first_field = fields[0] - second_field = fields[1] if len(fields) > 1 else None - name = second_field.replace("_", " ") if second_field else name - - fan_card = self.tune_display_buttons.get(name) - if fan_card is None and first_field in ( - "fan", - "fan_generic", - ): - icon = self.path.get("fan") - if second_field: - second_field = second_field.lower() - pattern_blower = r"(?:^|_)(?:blower|auxiliary)(?:_|$)" - pattern_exhaust = r"(?:^|_)exhaust(?:_|$)" - if re.search(pattern_blower, second_field): - icon = self.path.get("blower") - elif re.search(pattern_exhaust, second_field): - icon = self.path.get("fan_cage") - - card = OptionCard(self, name, str(name), icon) # type: ignore - card.setObjectName(str(name)) - - # Add card to layout and record reference - self.card_options[name] = card - self.panel.fans_content_layout.addWidget(card) - - # If the card doesn't have expected UI properties, discard it - if not hasattr(card, "continue_clicked"): - del card - self.card_options.pop(name, None) - return - - card.setMode(True) - card.secondtext.setText(f"{new_value}%") - card.continue_clicked.connect( - lambda: self.on_slidePage_request( - str(name), - card.secondtext.text().replace("%", ""), - self.on_slider_change, - 0, - 100, - ) - ) - - self.tune_display_buttons[name] = card - self.update() - fan_card = card - - if fan_card: - value_percent = new_value * 100 if new_value <= 1 else new_value - fan_card.secondtext.setText(f"{value_percent:.0f}%") - - @QtCore.pyqtSlot(str, int, "PyQt_PyObject", name="on_slidePage_request") - @QtCore.pyqtSlot(str, int, "PyQt_PyObject", int, int, name="on_slidePage_request") - def on_slidePage_request( - self, - name: str, - current_value: int, - callback, - min_value: int = 0, - max_value: int = 100, - ) -> None: - self.sliderPage.value_selected.connect(callback) - self.sliderPage.set_name(name) - self.sliderPage.set_slider_position(int(current_value)) - self.sliderPage.set_slider_minimum(min_value) - self.sliderPage.set_slider_maximum(max_value) - self.change_page(self.indexOf(self.sliderPage)) - - @QtCore.pyqtSlot(str, int, name="on_slider_change") - def on_slider_change(self, name: str, new_value: int) -> None: - if "speed" in name.lower(): - self.speed_factor_override = new_value / 100 - self.run_gcode_signal.emit(f"M220 S{new_value}") - if name.lower() == "fan": - self.run_gcode_signal.emit( - f"M106 S{int(round((normalize(float(new_value / 100), 0.0, 1.0, 0, 255))))}" - ) # [0, 255] Range - else: - name = name.replace(" ", "_") - self.run_gcode_signal.emit( - f'SET_FAN_SPEED FAN="{name}" SPEED={float(new_value / 100.00)}' - ) # [0.0, 1.0] Range - - def create_display_button(self, name: str) -> DisplayButton: - """Create and return a DisplayButton - - Args: - name (str): Name for the display button - - Returns: - DisplayButton: The created DisplayButton object - """ - display_button = DisplayButton() - display_button.setObjectName(str(name + "_display")) - display_button.setMinimumSize(QtCore.QSize(150, 50)) - display_button.setMaximumSize(QtCore.QSize(150, 80)) - font = QtGui.QFont() - font.setPointSize(16) - display_button.setFont(font) - return display_button - - def handle_printcoreupdate(self, value: dict): - if value["swapping"] == "idle": - return - - if value["swapping"] == "in_pos": - self.call_load_panel.emit(False, "") - self.printcores_page.show() - self.disable_popups.emit(True) - self.printcores_page.setText( - "Please Insert Print Core \n \n Afterwards click continue" - ) - if value["swapping"] == "unloading": - self.call_load_panel.emit(True, "Unloading print core") - if value["swapping"] == "cleaning": - self.call_load_panel.emit(True, "Cleaning print core") - - def _handle_gcode_response(self, messages: list): - """Handle gcode response for Z-tilt adjustment""" - pattern = r"Retries:\s*(\d+)/(\d+).*?range:\s*([\d.]+)\s*tolerance:\s*([\d.]+)" - - for msg_list in messages: - if not msg_list: - continue - - if ( - "Retries:" in msg_list - and "range:" in msg_list - and "tolerance:" in msg_list - ): - match = re.search(pattern, msg_list) - - if match: - retries_done = int(match.group(1)) - retries_total = int(match.group(2)) - probed_range = float(match.group(3)) - tolerance = float(match.group(4)) - if retries_done == retries_total: - self.call_load_panel.emit(False, "") - return - self.call_load_panel.emit( - True, - f"Retries: {retries_done}/{retries_total} | Range: {probed_range:.6f} | Tolerance: {tolerance:.6f}", - ) - - @QtCore.pyqtSlot(dict, name="printer_config") - def on_printer_config(self, config: dict) -> None: - """Slot that receives the full printer configuration, - - Additionally, this method configures the signal connections - between controllable heaters and numpad calls - """ - try: - self.panel.extruder_temp_display.clicked.disconnect() - self.panel.bed_temp_display.clicked.disconnect() - except Exception: - _logger.debug("Signals were not connected") - extruder = config.get("extruder", None) or {} - bed = config.get("heater_bed", None) or {} - e_min_temp = extruder.get("min_temp", 0) - e_max_temp = extruder.get("max_temp", 300) - b_max_temp = bed.get("max_temp", 100) - b_min_temp = bed.get("min_temp", 0) - # Configure numpads - self.panel.extruder_temp_display.clicked.connect( - lambda: self.request_numpad[str, int, "PyQt_PyObject", int, int].emit( - "Extruder Temperature", - int(round(float(self.panel.extruder_temp_display.secondary_text))), - self.on_numpad_change, - int(e_min_temp), - int(e_max_temp), - ) - ) - self.panel.bed_temp_display.clicked.connect( - lambda: self.request_numpad[str, int, "PyQt_PyObject", int, int].emit( - "Bed Temperature", - int(round(float(self.panel.bed_temp_display.secondary_text))), - self.on_numpad_change, - int(b_min_temp), - int(b_max_temp), - ) - ) - - def handle_ztilt(self): - """Handle Z-Tilt Adjustment""" - self.call_load_panel.emit(True, "Please wait, performing Z-axis calibration.") - self.run_gcode_signal.emit("G28\nM400") - self.run_gcode_signal.emit("Z_TILT_ADJUST") - - @QtCore.pyqtSlot(str, name="on-klippy-status") - def on_klippy_status(self, state: str): - """Handles incoming klippy status changes""" - if state.lower() == "ready": - self.printcores_page.hide() - self.disable_popups.emit(False) - return - if state.lower() == "startup": - self.printcores_page.setText("Almost done \n be patient") - return - - def show_swapcore(self): - """Show swap printcore""" - self.run_gcode_signal.emit("CHANGE_PRINTCORES") - self.call_load_panel.emit(True, "Preparing to swap print core") - - def handle_swapcore(self): - """Handle swap printcore routine finish""" - self.printcores_page.setText("Executing \n Firmware Restart") - self.run_gcode_signal.emit("FIRMWARE_RESTART") - - @QtCore.pyqtSlot(str, int, "PyQt_PyObject", name="on-numpad-request") - @QtCore.pyqtSlot(str, int, "PyQt_PyObject", int, int, name="on-numpad-request") - def on_numpad_request( - self, - name: str, - current_value: int, - callback, - min_value: int = 0, - max_value: int = 100, - ) -> None: - """Handles numpad widget request""" - self.numpadPage.value_selected.connect(callback) - self.numpadPage.set_name(name) - self.numpadPage.set_value(current_value) - self.numpadPage.set_min_value(min_value) - self.numpadPage.set_max_value(max_value) - self.change_page(self.indexOf(self.numpadPage)) - - @QtCore.pyqtSlot(str, int, name="on-numpad-change") - def on_numpad_change(self, name: str, new_value: int) -> None: - """Handles inputted numpad values""" - if "bed" in name.lower(): - name = "heater_bed" - elif "extruder" in name.lower(): - name = "extruder" - self.run_gcode_signal.emit( - f"SET_HEATER_TEMPERATURE HEATER={name} TARGET={new_value}" - ) - - def change_page(self, index): - """Handles changing page""" - self.request_change_page.emit(2, index) - - def back_button(self): - """Handle back button click""" - self.request_back_button.emit() - - def register_timed_callback(self, time: int, callback: callable) -> None: - """Registers timed callback and starts the timeout""" - _timer = QtCore.QTimer() - _timer.setSingleShot(True) - _timer.timeout.connect(callback) - _timer.start(int(time)) - self.timers.append(_timer) - - @QtCore.pyqtSlot(bool, "PyQt_PyObject", int, name="select-extrude-feedrate") - def handle_toggle_extrude_feedrate(self, checked: bool, caller, value: int) -> None: - """Slot to change the extruder feedrate, mainly used for toggle buttons - - Args: - checked (bool): Button checked state - caller (PyQtObject): The button that called this slot - value (int): New value for the extruder feedrate - """ - if value == self.extrude_feedrate: - return - self.extrude_feedrate = value - - @QtCore.pyqtSlot(bool, "PyQt_PyObject", int, name="select-extrude-length") - def handle_toggle_extrude_length(self, checked: bool, caller, value: int) -> None: - """Slot that changes the extrude length, mainly used for toggle buttons - - Args: - checked (bool): Button checked state - caller (PyQtObject): The button that called this slot - value (int): New value for the extrude length - """ - if self.extrude_length == value: - return - self.extrude_length = value - - @QtCore.pyqtSlot(bool, float, name="handle-select-move-speed") - def handle_select_move_speed(self, checked: bool, value: float) -> None: - """Slot that changes the move speed of manual move commands, mainly used - for toggle buttons - - Args: - checked (bool): Button checked state - value (float): New move speed value - """ - if self.move_speed == value: - return - self.move_speed = value - - @QtCore.pyqtSlot(bool, float, name="handle-select-move-length") - def handle_select_move_length(self, checked: bool, value: float) -> None: - """Slot that changes the move length of manual move commands, - mainly used for toggle buttons - - - Args: - checked (bool): Button checked state - value (float): New length value - """ - if self.move_length == value: - return - self.move_length = value - - @QtCore.pyqtSlot(str, name="handle-extrusion") - def handle_extrusion(self, extrude: bool) -> None: - """Slot that requests an extrusion/unextrusion move - - Args: - extrude (bool): If True extrudes otherwise unextrudes. - """ - can_extrude = bool(self.printer.heaters_object["extruder"]["can_extrude"]) - if not can_extrude: - self.extrude_page_message = "Temperature too cold to extrude" - self.panel.exp_info_label.setText(self.extrude_page_message) - return - if extrude: - self.run_gcode_signal.emit( - f"M83\nG1 E{self.extrude_length} F{self.extrude_feedrate * 60}\nM82\nM400" - ) - self.extrude_page_message = "Extruding" - self.panel.exp_info_label.setText(self.extrude_page_message) - else: - self.run_gcode_signal.emit( - f"M83\nG1 E-{self.extrude_length} F{self.extrude_feedrate * 60}\nM82\nM400" - ) - self.extrude_page_message = "Retracting" - self.panel.exp_info_label.setText(self.extrude_page_message) - # This block of code schedules a method to be called in x amount of milliseconds - _sch_time_s = float( - self.extrude_length / self.extrude_feedrate - ) # calculate the amount of time it'll take for the operation - self.extrude_page_message = "Ready" - self.register_timed_callback( - int(_sch_time_s + 2.0) * 1000, # In milliseconds - lambda: self.panel.exp_info_label.setText(self.extrude_page_message), - ) - - @QtCore.pyqtSlot(str, name="handle-move-axis") - def handle_move_axis(self, axis: str) -> None: - """Slot that requests manual move command - - Args: - axis (str): String that contains one of the following axis ` - ['X', - 'X-' - ,'Y' - ,'Y-' - ,'Z' - ,'Z-']`. [^1] - - --- - - [^1]: The **-** symbol indicates the negative direction for that axis - - """ - if axis not in ["X", "X-", "Y", "Y-", "Z", "Z-"]: - return - self.run_gcode_signal.emit( - f"G91\nG0 {axis}{float(self.move_length)} F{float(self.move_speed * 60)}\nG90\nM400" - ) - - @QtCore.pyqtSlot(str, list, name="on-toolhead-update") - def on_toolhead_update(self, field: str, values: list) -> None: - """Handles updated from toolhead printer object""" - if field == "position": - self.panel.mva_x_value_label.setText(f"{values[0]:.2f}") - self.panel.mva_y_value_label.setText(f"{values[1]:.2f}") - self.panel.mva_z_value_label.setText(f"{values[2]:.3f}") - - if values[0] == "252,50" and values[1] == "250" and values[2] == "50": - self.call_load_panel.emit(False, "") - self.toolhead_info.update({f"{field}": values}) - - @QtCore.pyqtSlot(str, str, float, name="on-extruder-update") - def on_extruder_update( - self, extruder_name: str, field: str, new_value: float - ) -> None: - """Handles updates from extruder printer object""" - if extruder_name == "extruder" and field == "temperature": - self.panel.extruder_temp_display.setText(f"{new_value:.1f}") - if extruder_name == "extruder" and field == "target": - self.panel.extruder_temp_display.secondary_text = f"{new_value:.1f}" - self.extruder_info.update({f"{extruder_name}": {f"{field}": new_value}}) - - @QtCore.pyqtSlot(str, str, float, name="on-heater-bed-update") - def on_heater_bed_update(self, name: str, field: str, new_value: float) -> None: - """Handles updated from heater_bed printer object""" - if field == "temperature": - self.panel.bed_temp_display.setText(f"{new_value:.1f}") - if field == "target": - self.panel.bed_temp_display.secondary_text = f"{new_value:.1f}" - self.bed_info.update({f"{name}": {f"{field}": new_value}}) - - def paintEvent(self, a0: QtGui.QPaintEvent) -> None: - """Handles ControlTab Widget painting""" - if self.panel.extrude_page.isVisible(): - self.panel.exp_info_label.setText(self.extrude_page_message) - return super().paintEvent(a0) +from __future__ import annotations +import re +import typing +from functools import partial +import logging + +from lib.moonrakerComm import MoonWebSocket +from lib.panels.widgets.numpadPage import CustomNumpad +from lib.panels.widgets.optionCardWidget import OptionCard +from lib.panels.widgets.popupDialogWidget import Popup +from lib.panels.widgets.printcorePage import SwapPrintcorePage +from lib.panels.widgets.probeHelperPage import ProbeHelper +from lib.panels.widgets.slider_selector_page import SliderPage +from lib.printer import Printer +from lib.ui.controlStackedWidget_ui import Ui_controlStackedWidget +from lib.utils.display_button import DisplayButton +from PyQt6 import QtCore, QtGui, QtWidgets + +logger = logging.getLogger(__name__) + + +_logger = logging.getLogger(__name__) + + +class ControlTab(QtWidgets.QStackedWidget): + """Printer Control Stacked Widget""" + + request_back_button = QtCore.pyqtSignal(name="request-back-button") + request_change_page = QtCore.pyqtSignal(int, int, name="request-change-page") + request_numpad_signal = QtCore.pyqtSignal( + int, + str, + str, + "PyQt_PyObject", + QtWidgets.QStackedWidget, + name="request-numpad", + ) + run_gcode_signal: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="run-gcode" + ) + disable_popups: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + bool, name="disable-popups" + ) + request_numpad: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + [str, int, "PyQt_PyObject"], + [str, int, "PyQt_PyObject", int, int], + name="request-numpad", + ) + request_file_info: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="request-file-info" + ) + call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") + toggle_conn_page = QtCore.pyqtSignal(bool, name="call-load-panel") + tune_display_buttons: dict = {} + card_options: dict = {} + + def __init__( + self, + parent: QtWidgets.QWidget, + ws: MoonWebSocket, + printer: Printer, + /, + ) -> None: + super().__init__(parent) + self.panel = Ui_controlStackedWidget() + self.panel.setupUi(self) + + self.popup = Popup(self) + + self.ws: MoonWebSocket = ws + self.printer: Printer = printer + self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.timers = [] + self.ztilt_state = False + self.extruder_info: dict = {} + self.bed_info: dict = {} + self.toolhead_info: dict = {} + self.extrude_length: int = 10 + self.extrude_feedrate: int = 2 + self.extrude_page_message: str = "" + self.move_length: float = 1.0 + self.move_speed: float = 25.0 + self.probe_helper_page = ProbeHelper(self) + self.probe_helper_page.toggle_conn_page.connect(self.toggle_conn_page) + self.probe_helper_page.disable_popups.connect(self.disable_popups) + self.addWidget(self.probe_helper_page) + self.probe_helper_page.call_load_panel.connect(self.call_load_panel) + self.printcores_page = SwapPrintcorePage(self) + self.addWidget(self.printcores_page) + + self.sliderPage = SliderPage(self) + self.addWidget(self.sliderPage) + self.sliderPage.request_back.connect(self.back_button) + + self.probe_helper_page.request_page_view.connect( + partial(self.change_page, self.indexOf(self.probe_helper_page)) + ) + self.probe_helper_page.query_printer_object.connect(self.ws.api.object_query) + self.probe_helper_page.run_gcode_signal.connect(self.ws.api.run_gcode) + self.probe_helper_page.request_back.connect(self.back_button) + self.printer.print_stats_update[str, str].connect( + self.probe_helper_page.on_print_stats_update + ) + self.printer.print_stats_update[str, dict].connect( + self.probe_helper_page.on_print_stats_update + ) + self.printer.print_stats_update[str, float].connect( + self.probe_helper_page.on_print_stats_update + ) + self.printer.available_gcode_cmds.connect( + self.probe_helper_page.on_available_gcode_cmds + ) + self.probe_helper_page.subscribe_config[str, "PyQt_PyObject"].connect( + self.printer.on_subscribe_config + ) + self.probe_helper_page.subscribe_config[list, "PyQt_PyObject"].connect( + self.printer.on_subscribe_config + ) + self.printer.extruder_update.connect(self.probe_helper_page.on_extruder_update) + self.printer.gcode_move_update.connect( + self.probe_helper_page.on_gcode_move_update + ) + self.printer.manual_probe_update.connect( + self.probe_helper_page.on_manual_probe_update + ) + self.printer.printer_config.connect(self.probe_helper_page.on_printer_config) + self.printer.gcode_response.connect( + self.probe_helper_page.handle_gcode_response + ) + self.printer.toolhead_update[str, list].connect(self.on_toolhead_update) + self.printer.extruder_update.connect(self.on_extruder_update) + self.printer.heater_bed_update.connect(self.on_heater_bed_update) + self.panel.cp_motion_btn.clicked.connect( + partial(self.change_page, self.indexOf(self.panel.motion_page)) + ) + self.panel.cp_temperature_btn.clicked.connect( + partial(self.change_page, self.indexOf(self.panel.temperature_page)) + ) + self.panel.cp_fans_btn.clicked.connect( + partial(self.change_page, self.indexOf(self.panel.fans_page)) + ) + self.panel.fans_back_btn.clicked.connect(self.back_button) + self.panel.cp_switch_print_core_btn.clicked.connect(self.show_swapcore) + self.panel.cp_nozzles_calibration_btn.clicked.connect( + partial(self.change_page, self.indexOf(self.probe_helper_page)) + ) + self.panel.motion_extrude_btn.clicked.connect( + partial(self.change_page, self.indexOf(self.panel.extrude_page)) + ) + self.panel.motion_move_axis_btn.clicked.connect( + partial(self.change_page, self.indexOf(self.panel.move_axis_page)) + ) + self.panel.mp_back_btn.clicked.connect(self.back_button) + self.panel.motion_auto_home_btn.clicked.connect( + partial(self.run_gcode_signal.emit, "G28\nM400") + ) + self.panel.motion_disable_steppers_btn.clicked.connect( + partial(self.run_gcode_signal.emit, "M84\nM400") + ) + self.panel.exp_back_btn.clicked.connect(self.back_button) + self.panel.extrude_select_length_10_btn.toggled.connect( + partial( + self.handle_toggle_extrude_length, + caller=self.panel.extrude_select_length_10_btn, + value=10, + ) + ) + self.panel.extrude_select_length_50_btn.toggled.connect( + partial( + self.handle_toggle_extrude_length, + caller=self.panel.extrude_select_length_50_btn, + value=50, + ) + ) + self.panel.extrude_select_length_100_btn.toggled.connect( + partial( + self.handle_toggle_extrude_length, + caller=self.panel.extrude_select_length_100_btn, + value=100, + ) + ) + self.panel.extrude_select_feedrate_2_btn.toggled.connect( + partial( + self.handle_toggle_extrude_feedrate, + caller=self.panel.extrude_select_feedrate_2_btn, + value=2, + ) + ) + self.panel.extrude_select_feedrate_5_btn.toggled.connect( + partial( + self.handle_toggle_extrude_feedrate, + caller=self.panel.extrude_select_feedrate_5_btn, + value=5, + ) + ) + self.panel.extrude_select_feedrate_10_btn.toggled.connect( + partial( + self.handle_toggle_extrude_feedrate, + caller=self.panel.extrude_select_feedrate_10_btn, + value=10, + ) + ) + self.panel.mva_select_length_1_btn.toggled.connect( + partial(self.handle_select_move_length, value=1.0) + ) + self.panel.mva_select_length_10_btn.toggled.connect( + partial(self.handle_select_move_length, value=10.0) + ) + self.panel.mva_select_length_100_btn.toggled.connect( + partial(self.handle_select_move_length, value=100.0) + ) + self.panel.mva_select_speed_25_btn.toggled.connect( + partial(self.handle_select_move_speed, value=25.0) + ) + self.panel.mva_select_speed_50_btn.toggled.connect( + partial(self.handle_select_move_speed, value=50.0) + ) + self.panel.mva_select_speed_100_btn.toggled.connect( + partial(self.handle_select_move_speed, value=100.0) + ) + self.panel.exp_extrude_btn.clicked.connect( + partial(self.handle_extrusion, True) + ) # True for extrusion + self.panel.exp_unextrude_btn.clicked.connect( + partial(self.handle_extrusion, False) + ) # False for retraction + # Move Axis + self.panel.mva_back_btn.clicked.connect(self.back_button) + self.panel.mva_home_x_btn.clicked.connect( + partial(self.run_gcode_signal.emit, "G28 X\nM400") + ) + self.panel.mva_home_y_btn.clicked.connect( + partial(self.run_gcode_signal.emit, "G28 Y\nM400") + ) + self.panel.mva_home_z_btn.clicked.connect( + partial(self.run_gcode_signal.emit, "G28 Z\nM400") + ) + self.panel.mva_home_all_btn.clicked.connect( + partial(self.run_gcode_signal.emit, "G28\nM400") + ) + self.panel.mva_up_btn.clicked.connect(partial(self.handle_move_axis, "Y")) + self.panel.mva_down_btn.clicked.connect(partial(self.handle_move_axis, "Y-")) + self.panel.mva_right_btn.clicked.connect(partial(self.handle_move_axis, "X")) + self.panel.mva_left_btn.clicked.connect(partial(self.handle_move_axis, "X-")) + self.panel.mva_z_up.clicked.connect( + partial(self.handle_move_axis, "Z-") # Move nozzle closer to bed + ) + self.panel.mva_z_down.clicked.connect( + partial(self.handle_move_axis, "Z") # Move nozzle away from bed + ) + self.panel.temp_back_button.clicked.connect(self.back_button) + self.panel.printer_settings_back_btn.clicked.connect(self.back_button) + self.run_gcode_signal.connect(self.ws.api.run_gcode) + # @ object temperature change clicked + self.numpadPage = CustomNumpad(self) + self.numpadPage.request_back.connect(self.request_back_button) + self.addWidget(self.numpadPage) + + self.request_numpad[str, int, "PyQt_PyObject", int, int].connect( + self.on_numpad_request + ) + + self.panel.cooldown_btn.clicked.connect( + lambda: self.run_gcode_signal.emit( + "SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=0\n\ + SET_HEATER_TEMPERATURE HEATER=extruder TARGET=0" + ) + ) + + self.path = { + "fan_cage": QtGui.QPixmap(":/fan_related/media/btn_icons/fan_cage.svg"), + "blower": QtGui.QPixmap(":/fan_related/media/btn_icons/blower.svg"), + "fan": QtGui.QPixmap(":/fan_related/media/btn_icons/fan.svg"), + } + + self.panel.cp_z_tilt_btn.clicked.connect(lambda: self.handle_ztilt()) + + self.printcores_page.pc_accept.clicked.connect(self.handle_swapcore) + + self.ws.klippy_state_signal.connect(self.on_klippy_status) + self.ws.klippy_state_signal.connect(self.probe_helper_page.on_klippy_status) + self.printer.on_printcore_update.connect(self.handle_printcoreupdate) + self.printer.gcode_response.connect(self._handle_gcode_response) + self.printer.z_tilt_update.connect(self._handle_z_tilt_object_update) + # self.panel.cp_printer_settings_btn.hide() + self.panel.temperature_cooldown_btn.hide() + self.panel.cooldown_btn.hide() + self.panel.cp_switch_print_core_btn.hide() + + self.printer.fan_update[str, str, float].connect(self.on_fan_object_update) + self.printer.fan_update[str, str, int].connect(self.on_fan_object_update) + + self.printer.printer_config.connect(self.on_printer_config) + + def _handle_z_tilt_object_update(self, value, state): + if state: + self.call_load_panel.emit(False, "") + + @QtCore.pyqtSlot(str, str, float, name="on_fan_update") + @QtCore.pyqtSlot(str, str, int, name="on_fan_update") + def on_fan_object_update( + self, name: str, field: str, new_value: int | float + ) -> None: + """Slot that receives updates from fan objects. + + Args: + name (str): Fan object name + field (str): Field name + new_value (int | float): New value for the field + """ + if "speed" not in field: + return + + fields = name.split() + first_field = fields[0] + second_field = fields[1] if len(fields) > 1 else None + name = second_field.replace("_", " ") if second_field else name + + fan_card = self.tune_display_buttons.get(name) + if fan_card is None and first_field in ( + "fan", + "fan_generic", + ): + icon = self.path.get("fan") + if second_field: + second_field = second_field.lower() + pattern_blower = r"(?:^|_)(?:blower|auxiliary)(?:_|$)" + pattern_exhaust = r"(?:^|_)exhaust(?:_|$)" + if re.search(pattern_blower, second_field): + icon = self.path.get("blower") + elif re.search(pattern_exhaust, second_field): + icon = self.path.get("fan_cage") + + card = OptionCard(self, name, str(name), icon) # type: ignore + card.setObjectName(str(name)) + + # Add card to layout and record reference + self.card_options[name] = card + self.panel.fans_content_layout.addWidget(card) + + # If the card doesn't have expected UI properties, discard it + if not hasattr(card, "continue_clicked"): + del card + self.card_options.pop(name, None) + return + + card.setMode(True) + card.secondtext.setText(f"{new_value}%") + card.continue_clicked.connect( + lambda: self.on_slidePage_request( + str(name), + card.secondtext.text().replace("%", ""), + self.on_slider_change, + 0, + 100, + ) + ) + + self.tune_display_buttons[name] = card + self.update() + fan_card = card + + if fan_card: + value_percent = new_value * 100 if new_value <= 1 else new_value + fan_card.secondtext.setText(f"{value_percent:.0f}%") + + @QtCore.pyqtSlot(str, int, "PyQt_PyObject", name="on_slidePage_request") + @QtCore.pyqtSlot(str, int, "PyQt_PyObject", int, int, name="on_slidePage_request") + def on_slidePage_request( + self, + name: str, + current_value: int, + callback, + min_value: int = 0, + max_value: int = 100, + ) -> None: + """Configure and navigate to the slider page with the given name, value, and callback.""" + try: + self.sliderPage.value_selected.disconnect() + except (RuntimeError, TypeError): + pass # no connections yet + self.sliderPage.value_selected.connect(callback) + self.sliderPage.set_name(name) + self.sliderPage.set_slider_position(int(current_value)) + self.sliderPage.set_slider_minimum(min_value) + self.sliderPage.set_slider_maximum(max_value) + self.change_page(self.indexOf(self.sliderPage)) + + @QtCore.pyqtSlot(str, int, name="on_slider_change") + def on_slider_change(self, name: str, new_value: int) -> None: + """Handle slider value change for fan controls. + + In controlTab, only fan_generic cards invoke this slot — the "speed" + and "fan" branches are kept for parity with tunePage but should never + match here. + """ + gcode_name = name.replace(" ", "_") + self.run_gcode_signal.emit( + f"SET_FAN_SPEED FAN={gcode_name} SPEED={float(new_value / 100.00)}" + ) # [0.0, 1.0] Range + + def create_display_button(self, name: str) -> DisplayButton: + """Create and return a DisplayButton + + Args: + name (str): Name for the display button + + Returns: + DisplayButton: The created DisplayButton object + """ + display_button = DisplayButton() + display_button.setObjectName(str(name + "_display")) + display_button.setMinimumSize(QtCore.QSize(150, 50)) + display_button.setMaximumSize(QtCore.QSize(150, 80)) + font = QtGui.QFont() + font.setPointSize(16) + display_button.setFont(font) + return display_button + + def handle_printcoreupdate(self, value: dict): + _swapping = value.get("swapping") + if _swapping is None or _swapping == "idle": + return + + if _swapping == "in_pos": + self.call_load_panel.emit(False, "") + self.printcores_page.show() + self.disable_popups.emit(True) + self.printcores_page.setText( + "Please Insert Print Core \n \n Afterwards click continue" + ) + if _swapping == "unloading": + self.call_load_panel.emit(True, "Unloading print core") + if _swapping == "cleaning": + self.call_load_panel.emit(True, "Cleaning print core") + + def _handle_gcode_response(self, messages: list): + """Handle gcode response for Z-tilt adjustment""" + pattern = r"Retries:\s*(\d+)/(\d+).*?range:\s*([\d.]+)\s*tolerance:\s*([\d.]+)" + + for msg_list in messages: + if not msg_list: + continue + + if ( + "Retries:" in msg_list + and "range:" in msg_list + and "tolerance:" in msg_list + ): + match = re.search(pattern, msg_list) + + if match: + retries_done = int(match.group(1)) + retries_total = int(match.group(2)) + probed_range = float(match.group(3)) + tolerance = float(match.group(4)) + if retries_done == retries_total: + self.call_load_panel.emit(False, "") + return + self.call_load_panel.emit( + True, + f"Retries: {retries_done}/{retries_total} | Range: {probed_range:.6f} | Tolerance: {tolerance:.6f}", + ) + + @QtCore.pyqtSlot(dict, name="printer_config") + def on_printer_config(self, config: dict) -> None: + """Slot that receives the full printer configuration, + + Additionally, this method configures the signal connections + between controllable heaters and numpad calls + """ + try: + self.panel.extruder_temp_display.clicked.disconnect() + self.panel.bed_temp_display.clicked.disconnect() + except Exception: + _logger.debug("Signals were not connected") + extruder = config.get("extruder", None) or {} + bed = config.get("heater_bed", None) or {} + e_min_temp = extruder.get("min_temp", 0) + e_max_temp = extruder.get("max_temp", 300) + b_max_temp = bed.get("max_temp", 100) + b_min_temp = bed.get("min_temp", 0) + # Configure numpads + self.panel.extruder_temp_display.clicked.connect( + lambda: self.request_numpad[str, int, "PyQt_PyObject", int, int].emit( + "Extruder Temperature", + int(round(float(self.panel.extruder_temp_display.secondary_text))), + self.on_numpad_change, + int(e_min_temp), + int(e_max_temp), + ) + ) + self.panel.bed_temp_display.clicked.connect( + lambda: self.request_numpad[str, int, "PyQt_PyObject", int, int].emit( + "Bed Temperature", + int(round(float(self.panel.bed_temp_display.secondary_text))), + self.on_numpad_change, + int(b_min_temp), + int(b_max_temp), + ) + ) + + def handle_ztilt(self): + """Handle Z-Tilt Adjustment""" + self.call_load_panel.emit(True, "Please wait, performing Z-axis calibration.") + self.run_gcode_signal.emit("G28\nM400") + self.run_gcode_signal.emit("Z_TILT_ADJUST") + + @QtCore.pyqtSlot(str, name="on-klippy-status") + def on_klippy_status(self, state: str): + """Handles incoming klippy status changes""" + if state.lower() == "ready": + self.printcores_page.hide() + self.disable_popups.emit(False) + return + if state.lower() == "startup": + self.printcores_page.setText("Almost done \n be patient") + return + + def show_swapcore(self): + """Show swap printcore""" + self.run_gcode_signal.emit("CHANGE_PRINTCORES") + self.call_load_panel.emit(True, "Preparing to swap print core") + + def handle_swapcore(self): + """Handle swap printcore routine finish""" + self.printcores_page.setText("Executing \n Firmware Restart") + self.run_gcode_signal.emit("FIRMWARE_RESTART") + + @QtCore.pyqtSlot(str, int, "PyQt_PyObject", name="on-numpad-request") + @QtCore.pyqtSlot(str, int, "PyQt_PyObject", int, int, name="on-numpad-request") + def on_numpad_request( + self, + name: str, + current_value: int, + callback, + min_value: int = 0, + max_value: int = 100, + ) -> None: + """Handles numpad widget request""" + try: + self.numpadPage.value_selected.disconnect() + except (RuntimeError, TypeError): + pass # no connections yet + self.numpadPage.value_selected.connect(callback) + self.numpadPage.set_name(name) + self.numpadPage.set_value(current_value) + self.numpadPage.set_min_value(min_value) + self.numpadPage.set_max_value(max_value) + self.change_page(self.indexOf(self.numpadPage)) + + @QtCore.pyqtSlot(str, int, name="on-numpad-change") + def on_numpad_change(self, name: str, new_value: int) -> None: + """Handles inputted numpad values""" + if "bed" in name.lower(): + name = "heater_bed" + elif "extruder" in name.lower(): + name = "extruder" + self.run_gcode_signal.emit( + f"SET_HEATER_TEMPERATURE HEATER={name} TARGET={new_value}" + ) + + def change_page(self, index): + """Handles changing page""" + self.request_change_page.emit(2, index) + + def back_button(self): + """Handle back button click""" + self.request_back_button.emit() + + def register_timed_callback(self, time: int, callback: callable) -> None: + """Registers timed callback and starts the timeout""" + _timer = QtCore.QTimer() + _timer.setSingleShot(True) + _timer.timeout.connect(callback) + _timer.start(int(time)) + self.timers.append(_timer) + + @QtCore.pyqtSlot(bool, "PyQt_PyObject", int, name="select-extrude-feedrate") + def handle_toggle_extrude_feedrate(self, checked: bool, caller, value: int) -> None: + """Slot to change the extruder feedrate, mainly used for toggle buttons + + Args: + checked (bool): Button checked state + caller (PyQtObject): The button that called this slot + value (int): New value for the extruder feedrate + """ + if value == self.extrude_feedrate: + return + self.extrude_feedrate = value + + @QtCore.pyqtSlot(bool, "PyQt_PyObject", int, name="select-extrude-length") + def handle_toggle_extrude_length(self, checked: bool, caller, value: int) -> None: + """Slot that changes the extrude length, mainly used for toggle buttons + + Args: + checked (bool): Button checked state + caller (PyQtObject): The button that called this slot + value (int): New value for the extrude length + """ + if self.extrude_length == value: + return + self.extrude_length = value + + @QtCore.pyqtSlot(bool, float, name="handle-select-move-speed") + def handle_select_move_speed(self, checked: bool, value: float) -> None: + """Slot that changes the move speed of manual move commands, mainly used + for toggle buttons + + Args: + checked (bool): Button checked state + value (float): New move speed value + """ + if self.move_speed == value: + return + self.move_speed = value + + @QtCore.pyqtSlot(bool, float, name="handle-select-move-length") + def handle_select_move_length(self, checked: bool, value: float) -> None: + """Slot that changes the move length of manual move commands, + mainly used for toggle buttons + + + Args: + checked (bool): Button checked state + value (float): New length value + """ + if self.move_length == value: + return + self.move_length = value + + @QtCore.pyqtSlot(str, name="handle-extrusion") + def handle_extrusion(self, extrude: bool) -> None: + """Slot that requests an extrusion/unextrusion move + + Args: + extrude (bool): If True extrudes otherwise unextrudes. + """ + can_extrude = bool(self.printer.heaters_object["extruder"]["can_extrude"]) + if not can_extrude: + self.extrude_page_message = "Temperature too cold to extrude" + self.panel.exp_info_label.setText(self.extrude_page_message) + return + if extrude: + self.run_gcode_signal.emit( + f"M83\nG1 E{self.extrude_length} F{self.extrude_feedrate * 60}\nM82\nM400" + ) + self.extrude_page_message = "Extruding" + self.panel.exp_info_label.setText(self.extrude_page_message) + else: + self.run_gcode_signal.emit( + f"M83\nG1 E-{self.extrude_length} F{self.extrude_feedrate * 60}\nM82\nM400" + ) + self.extrude_page_message = "Retracting" + self.panel.exp_info_label.setText(self.extrude_page_message) + # This block of code schedules a method to be called in x amount of milliseconds + _sch_time_s = float( + self.extrude_length / self.extrude_feedrate + ) # calculate the amount of time it'll take for the operation + self.extrude_page_message = "Ready" + self.register_timed_callback( + int(_sch_time_s + 2.0) * 1000, # In milliseconds + lambda: self.panel.exp_info_label.setText(self.extrude_page_message), + ) + + @QtCore.pyqtSlot(str, name="handle-move-axis") + def handle_move_axis(self, axis: str) -> None: + """Slot that requests manual move command + + Args: + axis (str): String that contains one of the following axis ` + ['X', + 'X-' + ,'Y' + ,'Y-' + ,'Z' + ,'Z-']`. [^1] + + --- + + [^1]: The **-** symbol indicates the negative direction for that axis + + """ + if axis not in ["X", "X-", "Y", "Y-", "Z", "Z-"]: + return + self.run_gcode_signal.emit( + f"G91\nG0 {axis}{float(self.move_length)} F{float(self.move_speed * 60)}\nG90\nM400" + ) + + @QtCore.pyqtSlot(str, list, name="on-toolhead-update") + def on_toolhead_update(self, field: str, values: list) -> None: + """Handles updated from toolhead printer object""" + if field == "position": + self.panel.mva_x_value_label.setText(f"{values[0]:.2f}") + self.panel.mva_y_value_label.setText(f"{values[1]:.2f}") + self.panel.mva_z_value_label.setText(f"{values[2]:.3f}") + + if values[0] == "252,50" and values[1] == "250" and values[2] == "50": + self.call_load_panel.emit(False, "") + self.toolhead_info.update({f"{field}": values}) + + @QtCore.pyqtSlot(str, str, float, name="on-extruder-update") + def on_extruder_update( + self, extruder_name: str, field: str, new_value: float + ) -> None: + """Handles updates from extruder printer object""" + if extruder_name == "extruder" and field == "temperature": + self.panel.extruder_temp_display.setText(f"{new_value:.1f}") + if extruder_name == "extruder" and field == "target": + self.panel.extruder_temp_display.secondary_text = f"{new_value:.1f}" + self.extruder_info.update({f"{extruder_name}": {f"{field}": new_value}}) + + @QtCore.pyqtSlot(str, str, float, name="on-heater-bed-update") + def on_heater_bed_update(self, name: str, field: str, new_value: float) -> None: + """Handles updated from heater_bed printer object""" + if field == "temperature": + self.panel.bed_temp_display.setText(f"{new_value:.1f}") + if field == "target": + self.panel.bed_temp_display.secondary_text = f"{new_value:.1f}" + self.bed_info.update({f"{name}": {f"{field}": new_value}}) + + def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + """Handles ControlTab Widget painting""" + if self.panel.extrude_page.isVisible(): + self.panel.exp_info_label.setText(self.extrude_page_message) + return super().paintEvent(a0) diff --git a/BlocksScreen/lib/panels/filamentTab.py b/BlocksScreen/lib/panels/filamentTab.py index 04fc5ef4..f5849f7b 100644 --- a/BlocksScreen/lib/panels/filamentTab.py +++ b/BlocksScreen/lib/panels/filamentTab.py @@ -1,12 +1,11 @@ import enum +import typing from functools import partial - -from lib.printer import Printer from lib.filament import Filament -from lib.ui.filamentStackedWidget_ui import Ui_filamentStackedWidget - from lib.panels.widgets.popupDialogWidget import Popup +from lib.printer import Printer +from lib.ui.filamentStackedWidget_ui import Ui_filamentStackedWidget from PyQt6 import QtCore, QtGui, QtWidgets @@ -18,6 +17,9 @@ class FilamentTab(QtWidgets.QStackedWidget): request_toolhead_count = QtCore.pyqtSignal(int, name="toolhead_number_received") run_gcode = QtCore.pyqtSignal(str, name="run_gcode") call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") + filament_type_changed: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="filament-type-changed" + ) class FilamentTypes(enum.Enum): PLA = Filament(name="PLA", temperature=220) @@ -44,6 +46,7 @@ def __init__(self, parent: QtWidgets.QWidget, printer: Printer, ws, /) -> None: self.has_load_unload_objects = None self._filament_state = self.FilamentStates.UNKNOWN self._sensor_states = {} + self._loaded_filament_name: str = "" self.filament_type: Filament | None = None self.panel.filament_page_load_btn.clicked.connect( partial(self.change_page, self.indexOf(self.panel.load_page)) @@ -52,22 +55,22 @@ def __init__(self, parent: QtWidgets.QWidget, printer: Printer, ws, /) -> None: self.panel.load_custom_btn.hide() self.panel.load_header_back_button.clicked.connect(self.back_button) self.panel.load_pla_btn.clicked.connect( - partial(self.load_filament, toolhead=0, temp=220) + partial(self.load_filament, toolhead=0, temp=220, name="PLA") ) self.panel.load_petg_btn.clicked.connect( - partial(self.load_filament, toolhead=0, temp=240) + partial(self.load_filament, toolhead=0, temp=240, name="PETG") ) self.panel.load_abs_btn.clicked.connect( - partial(self.load_filament, toolhead=0, temp=250) + partial(self.load_filament, toolhead=0, temp=250, name="ABS") ) self.panel.load_hips_btn.clicked.connect( - partial(self.load_filament, toolhead=0, temp=250) + partial(self.load_filament, toolhead=0, temp=250, name="HIPS") ) self.panel.load_nylon_btn.clicked.connect( - partial(self.load_filament, toolhead=0, temp=270) + partial(self.load_filament, toolhead=0, temp=270, name="Nylon") ) self.panel.load_tpu_btn.clicked.connect( - partial(self.load_filament, toolhead=0, temp=230) + partial(self.load_filament, toolhead=0, temp=230, name="TPU") ) self.panel.filament_page_unload_btn.clicked.connect( lambda: self.unload_filament(toolhead=0, temp=250) @@ -122,32 +125,32 @@ def on_extruder_update( self, extruder_name: str, field: str, new_value: float ) -> None: """Handle extruder update""" - if not self.isVisible: + if not self.isVisible(): return if not self.loadignore or not self.unloadignore: if self.target_temp != 0: if self.current_temp == self.target_temp: - if self.isVisible: + if self.isVisible(): self.call_load_panel.emit( True, "Extruder heated up \n Please wait" ) return if field == "temperature": self.current_temp = round(new_value, 0) - if self.isVisible: + if self.isVisible(): self.call_load_panel.emit( True, f"Heating up ({new_value}/{self.target_temp}) \n Please wait", ) if field == "target": self.target_temp = round(new_value, 0) - if self.isVisible: + if self.isVisible(): self.call_load_panel.emit(True, "Heating up \n Please wait") @QtCore.pyqtSlot(bool, name="on_load_filament") def on_load_filament(self, status: bool): """Handle load filament object updated""" - if not self.isVisible: + if not self.isVisible(): return if self.loadignore: return @@ -158,12 +161,14 @@ def on_load_filament(self, status: bool): self.target_temp = 0 self.call_load_panel.emit(False, "") self._filament_state = self.FilamentStates.LOADED + if self._loaded_filament_name: + self.filament_type_changed.emit(self._loaded_filament_name) self.handle_filament_state() @QtCore.pyqtSlot(bool, name="on_unload_filament") def on_unload_filament(self, status: bool): """Handle unload filament object updated""" - if not self.isVisible: + if not self.isVisible(): return if self.unloadignore: return @@ -174,12 +179,19 @@ def on_unload_filament(self, status: bool): self.call_load_panel.emit(False, "") self.target_temp = 0 self._filament_state = self.FilamentStates.UNLOADED + self._loaded_filament_name = "" self.handle_filament_state() - @QtCore.pyqtSlot(int, int, name="load_filament") - def load_filament(self, toolhead: int = 0, temp: int = 220) -> None: - """Handle load filament buttons clicked""" - if not self.isVisible: + @QtCore.pyqtSlot(int, int, str, name="load_filament") + def load_filament(self, toolhead: int = 0, temp: int = 220, name: str = "") -> None: + """Handle load filament buttons clicked. + + Args: + toolhead: Toolhead index. + temp: Target temperature for loading. + name: Filament type name (e.g. "PLA", "PETG"). + """ + if not self.isVisible(): return if self._filament_state == self.FilamentStates.UNKNOWN: @@ -194,6 +206,7 @@ def load_filament(self, toolhead: int = 0, temp: int = 220) -> None: message="Filament is already loaded.", ) return + self._loaded_filament_name = name self.loadignore = False self.call_load_panel.emit(True, "Loading Filament") self.run_gcode.emit(f"LOAD_FILAMENT TOOLHEAD=load_toolhead TEMPERATURE={temp}") @@ -201,7 +214,7 @@ def load_filament(self, toolhead: int = 0, temp: int = 220) -> None: @QtCore.pyqtSlot(str, int, name="unload_filament") def unload_filament(self, toolhead: int = 0, temp: int = 220) -> None: """Handle unload filament button clicked""" - if not self.isVisible: + if not self.isVisible(): return if self._filament_state == self.FilamentStates.UNKNOWN: @@ -260,15 +273,15 @@ def find_routine_objects(self): _available_objects = self.printer.available_objects.copy() - if "load_filament" in _available_objects.keys(): + if "load_filament" in _available_objects: self.has_load_unload_objects = True return True - if "unload_filament" in _available_objects.keys(): + if "unload_filament" in _available_objects: self.has_load_unload_objects = True return True - if "gcode_macro LOAD_FILAMENT" in _available_objects.keys(): + if "gcode_macro LOAD_FILAMENT" in _available_objects: return True - if "gcode_macro UNLOAD_FILAMENT" in _available_objects.keys(): + if "gcode_macro UNLOAD_FILAMENT" in _available_objects: return True return False diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 12fbc628..baca9b52 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -106,7 +106,7 @@ class MainWindow(QtWidgets.QMainWindow): def __init__(self): """Set up UI, instantiate subsystems, and wire all inter-component signals.""" - super(MainWindow, self).__init__() + super().__init__() self.config: BlocksScreenConfig = get_configparser() self.ui = Ui_MainWindow() self.ui.setupUi(self) @@ -125,6 +125,7 @@ def __init__(self): self.mc = MachineControl(self) self.file_data = Files(self, self.ws) self.index_stack = deque(maxlen=4) + self._printing_active = False self.printer = Printer(self, self.ws) self.conn_window = ConnectionPage(self, self.ws) self.update_page = UpdatePage(self) @@ -154,6 +155,7 @@ def __init__(self): self.printPanel.request_change_page.connect(slot=self.global_change_page) self.filamentPanel.request_back.connect(slot=self.global_back) self.filamentPanel.request_change_page.connect(slot=self.global_change_page) + self.filamentPanel.filament_type_changed.connect(self.set_header_filament_type) self.controlPanel.request_back_button.connect(slot=self.global_back) self.controlPanel.request_change_page.connect(slot=self.global_change_page) self.utilitiesPanel.request_back.connect(slot=self.global_back) @@ -253,8 +255,9 @@ def __init__(self): ) self.loadscreen.add_widget(self.loadwidget) self.controlPanel.toggle_conn_page.connect(self.conn_window.set_toggle) - self.cancelpage = CancelPage(self, ws=self.ws) + self.cancelpage = CancelPage(self) self.cancelpage.request_file_info.connect(self.file_data.on_request_fileinfo) + self.cancelpage.reprint_start.connect(self.ws.api.start_print) self.cancelpage.run_gcode.connect(self.ws.api.run_gcode) self.printer.print_stats_update[str, str].connect( self.cancelpage.on_print_stats_update @@ -333,9 +336,13 @@ def show_update_page(self, fullscreen: bool): @QtCore.pyqtSlot(name="on-cancel-print") def on_cancel_print(self): """Slot for cancel print signal""" + self._printing_active = False self.enable_tab_bar() - self.ui.extruder_temp_display.clicked.disconnect() - self.ui.bed_temp_display.clicked.disconnect() + try: + self.ui.extruder_temp_display.clicked.disconnect() + self.ui.bed_temp_display.clicked.disconnect() + except TypeError: + pass self.ui.filament_type_icon.setDisabled(False) self.ui.nozzle_size_icon.setDisabled(False) self.ui.extruder_temp_display.clicked.connect( @@ -378,16 +385,16 @@ def enable_tab_bar(self) -> bool: self.ui.header_main_layout.setEnabled(True) return all( [ - not self.ui.main_content_widget.isTabEnabled( + self.ui.main_content_widget.isTabEnabled( self.ui.main_content_widget.indexOf(self.ui.filamentTab) ), - not self.ui.main_content_widget.isTabEnabled( + self.ui.main_content_widget.isTabEnabled( self.ui.main_content_widget.indexOf(self.ui.controlTab) ), - not self.ui.main_content_widget.isTabEnabled( + self.ui.main_content_widget.isTabEnabled( self.ui.main_content_widget.indexOf(self.ui.utilitiesTab) ), - not self.ui.header_main_layout.isEnabled(), + self.ui.header_main_layout.isEnabled(), ] ) @@ -513,8 +520,18 @@ def global_change_page(self, tab_index: int, panel_index: int) -> None: _logger.debug("User is already on the requested page") return self.index_stack.append(current_page) + # Temporarily enable the target tab so setCurrentIndex works, + # then re-disable the tab bar if a print is active. + was_enabled = self.ui.main_content_widget.isTabEnabled(tab_index) + if not was_enabled: + self.ui.main_content_widget.setTabEnabled(tab_index, True) self.ui.main_content_widget.setCurrentIndex(tab_index) self.set_current_panel_index(panel_index) + if self._printing_active: + self.disable_tab_bar() + # Keep the target tab enabled — Qt auto-switches away from + # a disabled current tab, which undoes the navigation. + self.ui.main_content_widget.setTabEnabled(tab_index, True) _logger.debug( f"Requested page change -> Tab index : {requested_page[0]} | panel index : {requested_page[1]}", ) @@ -525,9 +542,16 @@ def global_back(self) -> None: if not bool(self.index_stack): _logger.debug("Index stack is empty, cannot go back any further") return - self.ui.main_content_widget.setCurrentIndex(self.index_stack[-1][0]) - self.set_current_panel_index(self.index_stack[-1][1]) - self.index_stack.pop() # Remove the last position. + tab_index, panel_index = self.index_stack[-1] + was_enabled = self.ui.main_content_widget.isTabEnabled(tab_index) + if not was_enabled: + self.ui.main_content_widget.setTabEnabled(tab_index, True) + self.ui.main_content_widget.setCurrentIndex(tab_index) + self.set_current_panel_index(panel_index) + if self._printing_active: + self.disable_tab_bar() + self.ui.main_content_widget.setTabEnabled(tab_index, True) + self.index_stack.pop() _logger.debug("Successfully went back a page.") @QtCore.pyqtSlot(name="bo-start-websocket-connection") @@ -560,7 +584,7 @@ def messageReceivedEvent(self, event: events.WebSocketMessageReceived) -> None: return api_reference = _method.split(".") if "klippy" in _method: - api_reference = "notify_klippy" + api_reference = ["notify_klippy"] method_handle = f"_handle_{api_reference[0]}_message" if hasattr(self, method_handle): obj = getattr(self, method_handle) @@ -576,10 +600,8 @@ def _handle_server_message(self, method, data, metadata) -> None: QtWidgets.QApplication.postEvent(self.file_data, file_data_event) except Exception as e: _logger.error( - ( - "Error posting event for file related information", - "received from websocket | error message received: %s", - ), + "Error posting event for file related information " + "received from websocket | error message received: %s", str(e), ) @@ -589,20 +611,18 @@ def _handle_machine_message(self, method, data, metadata) -> None: if "ok" in data: return if "update" in method: - if ("status" or "refresh") in method: + if "status" in method or "refresh" in method: self.on_update_message.emit(dict(data)) @api_handler def _handle_notify_update_response_message(self, method, data, metadata) -> None: """Handle update response messages""" - self.on_update_message.emit( - dict(dict(data.get("params", {})[0])) - ) # Also necessary, notify klippy can also signal update complete + self.on_update_message.emit(dict(dict(data.get("params", [{}])[0]))) @api_handler def _handle_notify_update_refreshed_message(self, method, data, metadata) -> None: """Handle update refreshed messages""" - self.on_update_message.emit(dict(data.get("params", {})[0])) + self.on_update_message.emit(dict(data.get("params", [{}])[0])) @api_handler def _handle_printer_message(self, method, data, metadata) -> None: @@ -621,15 +641,19 @@ def _handle_printer_message(self, method, data, metadata) -> None: self.printer_state_signal.emit("canceled") if "objects" in method: if "list" in method: - _object_list: list = data["objects"] + _object_list: list = data.get("objects", []) self.query_object_list[list].emit(_object_list) if "subscribe" in method: - _objects_response_list = [data["status"], data["eventtime"]] + _objects_response_list = [ + data.get("status", {}), + data.get("eventtime", 0), + ] self.printer_object_report_signal[list].emit(_objects_response_list) if "query" in method: - if isinstance(data["status"], dict): - _object_report = [data["status"]] - _object_report_keys = data["status"].items() + _query_status = data.get("status") + if isinstance(_query_status, dict): + _object_report = [_query_status] + _object_report_keys = _query_status.items() _object_report_list_dict: list = [] for _, key in enumerate(_object_report_keys): _helper_dict: dict = {key[0]: key[1]} @@ -690,7 +714,9 @@ def _handle_notify_service_state_changed_message( if self._popup_toggle: return service_entry: dict = entry[0] - service_name, service_info = service_entry.popitem() + if not service_entry: + return + service_name, service_info = next(iter(service_entry.items())) self.show_notifications.emit( "mainwindow", str( @@ -703,12 +729,15 @@ def _handle_notify_service_state_changed_message( @api_handler def _handle_notify_gcode_response_message(self, method, data, metadata) -> None: """Handle websocket gcode responses messages""" - _gcode_response = data.get("params") + _gcode_response = data.get("params", []) self.gcode_response[list].emit(_gcode_response) if _gcode_response: if self._popup_toggle: return - _gcode_msg_type, _message = str(_gcode_response[0]).split(" ", maxsplit=1) + _parts = str(_gcode_response[0]).split(" ", maxsplit=1) + if len(_parts) < 2: + return + _gcode_msg_type, _message = _parts popupWhitelist = ["filament runout", "no filament"] if _message.lower() not in popupWhitelist or _gcode_msg_type != "!!": return @@ -753,22 +782,27 @@ def _handle_notify_cpu_throttled_message(self, method, data, metadata) -> None: "Currently Throttled": 1 << 2, "Temperature Limit Active": 1 << 3, } - _bits = data.get("bits", None) - if not _bits: + _params = data.get("params", [{}]) + _bits = _params[0].get("bits") if _params else None + if _bits is None: self.show_notifications.emit( "mainWindow", "Cpu throttled unknown reason", 2, False ) return + if _bits == 0: + return _active_flags = [name for name, mask in flags.items() if _bits & mask] self.show_notifications.emit("mainwindow", str(_active_flags), 2, False) except Exception: - logging.debug("Error emitting notification for cpu throttled notification.") + _logger.debug("Error emitting notification for cpu throttled notification.") return @api_handler def _handle_notify_status_update_message(self, method, data, metadata) -> None: """Handle websocket printer objects status update messages""" - _object_report = data["params"] + _object_report = data.get("params") + if not _object_report: + return self.printer_object_report_signal[list].emit(_object_report) @QtCore.pyqtSlot(str, str, float, name="on-extruder-update") @@ -825,9 +859,13 @@ def event(self, event: QtCore.QEvent) -> bool: return True return False if event.type() == events.PrintStart.type(): + self._printing_active = True self.disable_tab_bar() - self.ui.extruder_temp_display.clicked.disconnect() - self.ui.bed_temp_display.clicked.disconnect() + try: + self.ui.extruder_temp_display.clicked.disconnect() + self.ui.bed_temp_display.clicked.disconnect() + except TypeError: + pass self.ui.filament_type_icon.setDisabled(True) self.ui.nozzle_size_icon.setDisabled(True) self.ui.extruder_temp_display.clicked.connect( @@ -851,9 +889,13 @@ def event(self, event: QtCore.QEvent) -> bool: ): if event.type() == events.PrintCancelled.type(): self.handle_cancel_print() + self._printing_active = False self.enable_tab_bar() - self.ui.extruder_temp_display.clicked.disconnect() - self.ui.bed_temp_display.clicked.disconnect() + try: + self.ui.extruder_temp_display.clicked.disconnect() + self.ui.bed_temp_display.clicked.disconnect() + except TypeError: + pass self.ui.filament_type_icon.setDisabled(False) self.ui.nozzle_size_icon.setDisabled(False) self.ui.extruder_temp_display.clicked.connect( @@ -874,4 +916,4 @@ def event(self, event: QtCore.QEvent) -> bool: def sizeHint(self) -> QtCore.QSize: """Sets default size for the widget""" self.adjustSize() - return super().sizeHint(QtCore.QSize(800, 480)) + return QtCore.QSize(800, 480) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index f9b17a52..b39a1bd5 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -35,7 +35,6 @@ from lib.utils.icon_button import IconButton from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem from PyQt6 import QtCore, QtGui, QtWidgets -from PyQt6.QtCore import QTimer, pyqtSlot logger = logging.getLogger(__name__) @@ -255,7 +254,7 @@ def _prefill_ip_from_os(self) -> None: except OSError: continue - @pyqtSlot() + @QtCore.pyqtSlot() def _on_reconnect_complete(self) -> None: """Navigate back to the main panel after a static-IP or DHCP-reset operation.""" logger.debug("reconnect_complete received — navigating to main_network_page") @@ -263,7 +262,7 @@ def _on_reconnect_complete(self) -> None: def _init_timers(self) -> None: """Initialize timers.""" - self._load_timer = QTimer(self) + self._load_timer = QtCore.QTimer(self) self._load_timer.setSingleShot(True) self._load_timer.timeout.connect(self._handle_load_timeout) @@ -277,7 +276,7 @@ def _init_model_view(self) -> None: self._entry_delegate.item_selected.connect(self._on_ssid_item_clicked) self._configure_list_view_palette() - @pyqtSlot(NetworkState) + @QtCore.pyqtSlot(NetworkState) def _on_network_state_changed(self, state: NetworkState) -> None: """React to a NetworkState update: sync toggles, populate header and connection info.""" logger.debug( @@ -438,7 +437,7 @@ def _on_network_state_changed(self, state: NetworkState) -> None: self._emit_status_icon(state) self._sync_active_network_list_icon(state) - @pyqtSlot(list) + @QtCore.pyqtSlot(list) def _on_scan_complete(self, networks: list[NetworkInfo]) -> None: """Receive scan results, filter/sort them, and rebuild the SSID list view. @@ -457,9 +456,11 @@ def _on_scan_complete(self, networks: list[NetworkInfo]) -> None: # Stamp the connected AP as ACTIVE so the list is correct on first # render even when the scan ran before the connection fully settled. filtered = [ - replace(net, network_status=NetworkStatus.ACTIVE) - if net.ssid == current_ssid - else net + ( + replace(net, network_status=NetworkStatus.ACTIVE) + if net.ssid == current_ssid + else net + ) for net in filtered ] active = next((n for n in filtered if n.ssid == current_ssid), None) @@ -478,12 +479,12 @@ def _on_scan_complete(self, networks: list[NetworkInfo]) -> None: state = self._nm.current_state self._emit_status_icon(state) - @pyqtSlot(list) + @QtCore.pyqtSlot(list) def _on_saved_networks_loaded(self, networks: list[SavedNetwork]) -> None: """Receive saved-network data and update the priority spinbox for the active SSID.""" logger.debug("Loaded %d saved networks", len(networks)) - @pyqtSlot(ConnectionResult) + @QtCore.pyqtSlot(ConnectionResult) def _on_operation_complete(self, result: ConnectionResult) -> None: """Handle network operation completion.""" logger.debug("Operation: success=%s, msg=%s", result.success, result.message) @@ -570,7 +571,7 @@ def _on_operation_complete(self, result: ConnectionResult) -> None: result.message, ) ssid = self._target_ssid - QTimer.singleShot( + QtCore.QTimer.singleShot( 2000, lambda _ssid=ssid: self._nm.connect_network(_ssid) ) return # Keep loading visible; state machine handles completion @@ -578,7 +579,7 @@ def _on_operation_complete(self, result: ConnectionResult) -> None: self._clear_loading() self._show_error_popup(result.message) - @pyqtSlot(str, str) + @QtCore.pyqtSlot(str, str) def _on_network_error(self, operation: str, message: str) -> None: """Log network errors and surface critical failures in the info box.""" logger.error("Network error [%s]: %s", operation, message) @@ -658,13 +659,15 @@ def _sync_active_network_list_icon(self, state: NetworkState) -> None: # Update the cached entry with the authoritative signal and status updated = [ - replace( - net, - signal_strength=self._active_signal, - network_status=NetworkStatus.ACTIVE, + ( + replace( + net, + signal_strength=self._active_signal, + network_status=NetworkStatus.ACTIVE, + ) + if net.ssid == state.current_ssid + else net ) - if net.ssid == state.current_ssid - else net for net in self._cached_scan_networks ] @@ -1063,7 +1066,9 @@ def _handle_wifi_toggle(self, is_on: bool) -> None: # Non-blocking: disable hotspot then connect self._nm.toggle_hotspot(False) _ssid_to_connect = self._target_ssid - QTimer.singleShot(500, lambda: self._nm.connect_network(_ssid_to_connect)) + QtCore.QTimer.singleShot( + 500, lambda: self._nm.connect_network(_ssid_to_connect) + ) def _handle_hotspot_toggle(self, is_on: bool) -> None: """Enable or disable the hotspot, enforcing the ethernet/Wi-Fi mutual-exclusion rule.""" diff --git a/BlocksScreen/lib/panels/printTab.py b/BlocksScreen/lib/panels/printTab.py index 3f53c91a..bc1ce03b 100644 --- a/BlocksScreen/lib/panels/printTab.py +++ b/BlocksScreen/lib/panels/printTab.py @@ -146,6 +146,7 @@ def __init__( self.file_data.usb_files_loaded.connect( self.filesPage_widget.on_usb_files_loaded ) + self.file_data.fileinfo.connect(self.confirmPage_widget.on_fileinfo) self.jobStatusPage_widget = JobStatusWidget(self) self.addWidget(self.jobStatusPage_widget) self.confirmPage_widget.on_accept.connect( @@ -292,6 +293,10 @@ def on_numpad_request( max_value: int = 100, ) -> None: """Handle numpad request""" + try: + self.numpadPage.value_selected.disconnect() + except (RuntimeError, TypeError): + pass self.numpadPage.value_selected.connect(callback) self.numpadPage.set_name(name) self.numpadPage.set_value(current_value) @@ -311,6 +316,10 @@ def on_slidePage_request( max_value: int = 100, ) -> None: """Handle slider page request""" + try: + self.sliderPage.value_selected.disconnect() + except (RuntimeError, TypeError): + pass self.sliderPage.value_selected.connect(callback) self.sliderPage.set_name(name) self.sliderPage.set_slider_position(int(current_value)) diff --git a/BlocksScreen/lib/panels/utilitiesTab.py b/BlocksScreen/lib/panels/utilitiesTab.py index 6cff5f27..d2dc1679 100644 --- a/BlocksScreen/lib/panels/utilitiesTab.py +++ b/BlocksScreen/lib/panels/utilitiesTab.py @@ -1,9 +1,14 @@ +import logging +import re import typing from dataclasses import dataclass from enum import Enum, auto from functools import partial from lib.moonrakerComm import MoonWebSocket +from lib.panels.widgets.basePopup import BasePopup +from lib.panels.widgets.inputshaperPage import InputShaperPage +from lib.panels.widgets.optionCardWidget import OptionCard from lib.panels.widgets.troubleshootPage import TroubleshootPage from lib.printer import Printer from lib.ui.utilitiesStackedWidget_ui import Ui_utilitiesStackedWidget @@ -11,11 +16,7 @@ from lib.utils.toggleAnimatedButton import ToggleAnimatedButton from PyQt6 import QtCore, QtGui, QtWidgets -from lib.panels.widgets.optionCardWidget import OptionCard -from lib.panels.widgets.inputshaperPage import InputShaperPage -from lib.panels.widgets.basePopup import BasePopup - -import re +logger = logging.getLogger(__name__) @dataclass @@ -113,8 +114,8 @@ def __init__( self.x_inputshaper: dict = {} self.stepper_limits: dict = {} - self.current_object: typing.Optional[str] = None - self.current_process: typing.Optional[Process] = None + self.current_object: str | None = None + self.current_process: Process | None = None self.axis_in: str = "x" self.amount: int = 1 self.tb: bool = False @@ -250,8 +251,9 @@ def handle_gcode_response(self, data: list[str]) -> None: """ if not isinstance(data, list) or len(data) != 1 or not isinstance(data[0], str): - print( - f"WARNING: Invalid input format. Expected a list with one string. Received: {data}" + logger.warning( + "handle_gcode_response: invalid input format. Expected list[str], received: %r", + data, ) return @@ -317,7 +319,7 @@ def handle_gcode_response(self, data: list[str]) -> None: self.is_page.set_type_dictionary(self.is_types) first_key = next(iter(reordered.keys()), None) - for key in reordered.keys(): + for key in reordered: if key == first_key: self.is_page.add_type_entry(key, "Recommended type") else: @@ -365,7 +367,7 @@ def on_object_list(self, object_list: list) -> None: @QtCore.pyqtSlot(dict, name="on_object_config") @QtCore.pyqtSlot(list, name="on_object_config") - def on_object_config(self, config: typing.Union[dict, list]) -> None: + def on_object_config(self, config: dict | list) -> None: """Handle receiving printer object configurations""" if not config: return @@ -381,12 +383,12 @@ def on_object_config(self, config: typing.Union[dict, list]) -> None: pos_max = value.get("position_max") if pos_min is not None or pos_max is not None: self.stepper_limits[key] = { - "min": float(pos_min) - if pos_min is not None - else -float("inf"), - "max": float(pos_max) - if pos_max is not None - else float("inf"), + "min": ( + float(pos_min) if pos_min is not None else -float("inf") + ), + "max": ( + float(pos_max) if pos_max is not None else float("inf") + ), } def on_printer_config_received(self, config: dict) -> None: diff --git a/BlocksScreen/lib/panels/widgets/bannerPopup.py b/BlocksScreen/lib/panels/widgets/bannerPopup.py index 7db54547..99077539 100644 --- a/BlocksScreen/lib/panels/widgets/bannerPopup.py +++ b/BlocksScreen/lib/panels/widgets/bannerPopup.py @@ -80,6 +80,8 @@ def _calculate_target_geometry(self) -> QtCore.QRect: if isinstance(widget, QtWidgets.QMainWindow): main_window = widget break + if main_window is None: + return QtCore.QRect() parent_rect = main_window.geometry() width = int(parent_rect.width() * 0.35) height = 80 diff --git a/BlocksScreen/lib/panels/widgets/basePopup.py b/BlocksScreen/lib/panels/widgets/basePopup.py index a9a4d188..16c42c85 100644 --- a/BlocksScreen/lib/panels/widgets/basePopup.py +++ b/BlocksScreen/lib/panels/widgets/basePopup.py @@ -1,5 +1,3 @@ -import typing - from PyQt6 import QtCore, QtGui, QtWidgets @@ -47,13 +45,11 @@ def __init__( self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True) self.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) else: - self.setStyleSheet( - """ + self.setStyleSheet(""" #MyParent { background-image: url(:/background/media/1st_background.png); } - """ - ) + """) def _update_button_style(self) -> None: """Applies the current color variables and adds the central border to the stylesheets.""" @@ -61,26 +57,21 @@ def _update_button_style(self) -> None: return if not self.floating: - self.confirm_button.setStyleSheet( - f""" + self.confirm_button.setStyleSheet(f""" background-color: {self.confirm_bk_color}; color: {self.confirm_ft_color}; border: none; padding: 10px; - """ - ) + """) - self.cancel_button.setStyleSheet( - f""" + self.cancel_button.setStyleSheet(f""" background-color: {self.cancel_bk_color}; color: {self.cancel_ft_color}; border: none; padding: 10px; - """ - ) + """) else: - self.confirm_button.setStyleSheet( - f""" + self.confirm_button.setStyleSheet(f""" background-color: {self.confirm_bk_color}; color: {self.confirm_ft_color}; border-top: none; @@ -89,11 +80,9 @@ def _update_button_style(self) -> None: border-right: 1px solid #80807e; border-bottom-left-radius: 16px; padding: 10px; - """ - ) + """) - self.cancel_button.setStyleSheet( - f""" + self.cancel_button.setStyleSheet(f""" background-color: {self.cancel_bk_color}; color: {self.cancel_ft_color}; border-left: 1px solid #80807e;; @@ -101,8 +90,7 @@ def _update_button_style(self) -> None: border-right: 2px solid #80807e; border-bottom-right-radius: 16px; padding: 10px; - """ - ) + """) def set_message(self, message: str) -> None: self.label.setText(message) @@ -151,7 +139,7 @@ def add_widget(self, widget: QtWidgets.QWidget) -> None: layout.insertWidget(index, widget) widget.show() - def _get_mainWindow_widget(self) -> typing.Optional[QtWidgets.QMainWindow]: + def _get_mainWindow_widget(self) -> QtWidgets.QMainWindow | None: """Get the main application window""" app_instance = QtWidgets.QApplication.instance() if not app_instance: diff --git a/BlocksScreen/lib/panels/widgets/cancelPage.py b/BlocksScreen/lib/panels/widgets/cancelPage.py index e16fb2e6..832c2f29 100644 --- a/BlocksScreen/lib/panels/widgets/cancelPage.py +++ b/BlocksScreen/lib/panels/widgets/cancelPage.py @@ -1,17 +1,16 @@ +import logging +import typing + from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.blocks_label import BlocksLabel from PyQt6 import QtCore, QtGui, QtWidgets -import typing -from lib.moonrakerComm import MoonWebSocket +logger = logging.getLogger(__name__) class CancelPage(QtWidgets.QWidget): - """Update GUI Page, - retrieves from moonraker available clients and adds functionality - for updating or recovering them - """ + """Displayed when a print is cancelled; offers reprint or ignore.""" request_file_info: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( str, name="request_file_info" @@ -23,16 +22,13 @@ class CancelPage(QtWidgets.QWidget): str, name="run_gcode" ) - def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket) -> None: + def __init__(self, parent: QtWidgets.QWidget) -> None: super().__init__(parent) - self.ws: MoonWebSocket = ws self._setupUI() self.filename = "" - - self.reprint_start.connect(self.ws.api.start_print) + self._thumbnail_scan_done: bool = False self.confirm_button.clicked.connect(lambda: self._handle_accept()) - self.refuse_button.clicked.connect(lambda: self._handle_refuse()) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_StyledBackground, True) @@ -52,8 +48,10 @@ def _handle_refuse(self): def on_print_stats_update(self, field: str, value: dict | float | str) -> None: if isinstance(value, str): if "filename" in field: + if value != self.filename: + self._thumbnail_scan_done = False self.filename = value - if self.isVisible: + if self.isVisible(): self.set_file_name(value) def show(self): @@ -91,22 +89,25 @@ def set_pixmap(self, pixmap: QtGui.QPixmap) -> None: def set_file_name(self, file_name: str) -> None: self.cf_file_name.setText(file_name) - def _show_screen_thumbnail(self, dict): - try: - thumbnails = dict["thumbnail_images"] + def _show_screen_thumbnail(self, metadata: dict | None) -> None: + """Display the largest thumbnail from file metadata. - last_thumb = QtGui.QPixmap.fromImage(thumbnails[-1]) - - if last_thumb.isNull(): - last_thumb = QtGui.QPixmap( - "BlocksScreen/lib/ui/resources/media/logoblocks400x300.png" - ) - except Exception as e: - print(e) - last_thumb = QtGui.QPixmap( - "BlocksScreen/lib/ui/resources/media/logoblocks400x300.png" - ) - self.set_pixmap(last_thumb) + ``thumbnail_images`` values are pre-loaded ``QImage`` + objects produced by ``Files._process_metadata``. + """ + fallback = QtGui.QPixmap( + "BlocksScreen/lib/ui/resources/media/logoblocks400x300.png" + ) + thumbnails = metadata.get("thumbnail_images", []) if metadata else [] + if not thumbnails: + self.set_pixmap(fallback) + return + + last_thumb = thumbnails[-1] + if isinstance(last_thumb, QtGui.QImage) and not last_thumb.isNull(): + self.set_pixmap(QtGui.QPixmap.fromImage(last_thumb)) + else: + self.set_pixmap(fallback) def _setupUI(self) -> None: """Setup widget ui""" @@ -119,11 +120,9 @@ def _setupUI(self) -> None: sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.setSizePolicy(sizePolicy) self.setObjectName("cancelPage") - self.setStyleSheet( - """#cancelPage { + self.setStyleSheet("""#cancelPage { background-image: url(:/background/media/1st_background.png); - }""" - ) + }""") self.setMinimumSize(QtCore.QSize(800, 480)) self.setMaximumSize(QtCore.QSize(800, 480)) self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) diff --git a/BlocksScreen/lib/panels/widgets/confirmPage.py b/BlocksScreen/lib/panels/widgets/confirmPage.py index 0f35ba39..d759f0ea 100644 --- a/BlocksScreen/lib/panels/widgets/confirmPage.py +++ b/BlocksScreen/lib/panels/widgets/confirmPage.py @@ -1,3 +1,4 @@ +import logging import os import typing @@ -8,8 +9,12 @@ from lib.utils.icon_button import IconButton from PyQt6 import QtCore, QtGui, QtWidgets +logger = logging.getLogger(__name__) + class ConfirmWidget(QtWidgets.QWidget): + """Widget displayed when a user selects a file to print.""" + on_accept: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( str, name="on_accept" ) @@ -26,7 +31,6 @@ def __init__(self, parent) -> None: self.setMouseTracking(True) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) self.thumbnail: QtGui.QImage = self._blocksthumbnail - self._thumbnails: typing.List = [] self.directory = "gcodes" self.filename = "" self.confirm_button.clicked.connect( @@ -39,32 +43,34 @@ def __init__(self, parent) -> None: lambda: self.on_delete.emit(self.filename, self.directory) ) - @QtCore.pyqtSlot(str, dict, name="on_show_widget") - def on_show_widget(self, text: str, filedata: dict | None = None) -> None: - """Handle widget show""" - if not filedata: - return + @QtCore.pyqtSlot(str, object, name="on_show_widget") + def on_show_widget(self, text: str, metadata: dict | None = None) -> None: + """Handle widget show.""" directory = os.path.dirname(text) filename = os.path.basename(text) self.directory = directory self.filename = filename self.cf_file_name.setText(self.filename) - self._thumbnails = filedata.get("thumbnail_images", []) - if self._thumbnails: - _biggest_thumbnail = self._thumbnails[-1] # Show last which is biggest - self.thumbnail = QtGui.QImage(_biggest_thumbnail) - else: + if metadata is None: self.thumbnail = self._blocksthumbnail - _total_filament = filedata.get("filament_weight_total") - _estimated_time = filedata.get("estimated_time") - if isinstance(_estimated_time, str): - seconds = 0 - else: - seconds = _estimated_time + self.cf_info_tf.setText("Total Filament: loading...") + self.cf_info_tr.setText("Slicer time: loading...") + self.update() + return + self._update_metadata_labels(metadata) + self.update() + + def _update_metadata_labels(self, metadata: dict) -> None: + """Update thumbnail and text labels from metadata.""" + self._apply_thumbnail(metadata) + raw_weight = metadata.get("filament_weight_total", 0) + _total_filament: float | str = raw_weight if raw_weight > 0 else 0 + seconds = metadata.get("estimated_time", 0) + seconds = seconds if seconds > 0 else 0 days, hours, minutes, _ = helper_methods.estimate_print_time(seconds) if seconds <= 0: - time_str = "??" + time_str = "Unknown" elif seconds < 60: time_str = "less than 1 minute" else: @@ -83,9 +89,39 @@ def on_show_widget(self, text: str, filedata: dict | None = None) -> None: _total_filament = str("%.2f" % _total_filament) + "g" filament_label = f"Total Filament: {_total_filament}" time_label = f"Slicer time: {time_str}" - self.cf_info_tf.setText(f"{filament_label}") - self.cf_info_tr.setText(f"{time_label}") - self.repaint() + self.cf_info_tf.setText(filament_label) + self.cf_info_tr.setText(time_label) + + def _apply_thumbnail(self, metadata: dict) -> None: + """Set self.thumbnail from metadata, falling back to the logo.""" + thumbnails = metadata.get("thumbnail_images", []) + if thumbnails: + last = thumbnails[-1] + if isinstance(last, QtGui.QImage) and not last.isNull(): + self.thumbnail = last + return + self.thumbnail = self._blocksthumbnail + + @QtCore.pyqtSlot(dict, name="on_fileinfo") + def on_fileinfo(self, metadata: dict) -> None: + """Update thumbnail and metadata labels when new data arrives.""" + if not metadata or not self.filename: + return + incoming = metadata.get("filename", "") + current = ( + f"{self.directory}/{self.filename}" if self.directory else self.filename + ) + # Also accept bare-filename match for USB files: Moonraker may strip the + # USB directory prefix from the returned filename. + is_usb_bare_match = ( + incoming == self.filename + and self.directory.startswith("USB-") + and incoming == os.path.basename(incoming) + ) + if incoming != current and not is_usb_bare_match: + return + self._update_metadata_labels(metadata) + self.update() def estimate_print_time(self, seconds: int) -> list: """Convert time in seconds format to days, hours, minutes, seconds. @@ -142,8 +178,8 @@ def paintEvent(self, event: QtGui.QPaintEvent) -> None: def showEvent(self, a0: QtGui.QShowEvent) -> None: """Re-implemented method, Handle widget show event""" - if not self.thumbnail: - self.cf_thumbnail.close() + if self.thumbnail.isNull(): + self.cf_thumbnail.hide() return super().showEvent(a0) def _setupUI(self) -> None: @@ -252,7 +288,6 @@ def _setupUI(self) -> None: "icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") ) self.confirm_button.setText("Print") - # 2. Align buttons to the right self.cf_confirm_layout.addWidget( self.confirm_button, 0, QtCore.Qt.AlignmentFlag.AlignCenter ) @@ -266,7 +301,6 @@ def _setupUI(self) -> None: "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/garbage-icon.svg") ) self.delete_file_button.setText("Delete") - # 2. Align buttons to the right self.cf_confirm_layout.addWidget( self.delete_file_button, 0, QtCore.Qt.AlignmentFlag.AlignCenter ) diff --git a/BlocksScreen/lib/panels/widgets/connectionPage.py b/BlocksScreen/lib/panels/widgets/connectionPage.py index 3cd1fc21..745b8195 100644 --- a/BlocksScreen/lib/panels/widgets/connectionPage.py +++ b/BlocksScreen/lib/panels/widgets/connectionPage.py @@ -57,7 +57,7 @@ def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket, /): self.restart_klipper_clicked.emit ) self.ws.connection_lost.connect(slot=self.show) - self.ws.klippy_connected_signal.connect(self.on_klippy_connected) + self.ws.klippy_connected_signal.connect(self.on_klippy_connection) self.ws.klippy_state_signal.connect(self.on_klippy_state) @QtCore.pyqtSlot(bool, name="toggle_connection_page") @@ -82,7 +82,7 @@ def showEvent(self, a0: QtCore.QEvent | None): self.call_cancel_panel.emit(False) return super().showEvent(a0) - @QtCore.pyqtSlot(bool, name="on_klippy_connected") + @QtCore.pyqtSlot(bool, name="on_klippy_connection") def on_klippy_connection(self, connected: bool): """Handle klippy connection state""" self.dot_timer.stop() @@ -143,15 +143,13 @@ def text_update(self, text: int | str | None = None): if self.state == "shutdown" and self.message is not None: return False self.dot_timer.stop() - logger.debug(f"[ConnectionWindowPanel] text_update: {text}") + logger.debug("[ConnectionWindowPanel] text_update: %r", text) if text == "wb lost": self.panel.connectionTextBox.setText("Moonraker connection lost") if text is None: - self.panel.connectionTextBox.setText( - """ + self.panel.connectionTextBox.setText(""" Not connected to Moonraker Websocket - """ - ) + """) return True if isinstance(text, str): self.panel.connectionTextBox.setText( @@ -208,7 +206,7 @@ def eventFilter(self, object: QtCore.QObject, event: QtCore.QEvent) -> bool: elif event.type() == KlippyShutdown.type(): self.dot_timer.stop() if not self.isVisible(): - self.panel.connectionTextBox.setText(f"{self.message}") + self.panel.connectionTextBox.setText(self.message or "") self.show() return True diff --git a/BlocksScreen/lib/panels/widgets/fansPage.py b/BlocksScreen/lib/panels/widgets/fansPage.py index 925c0230..c31a8600 100644 --- a/BlocksScreen/lib/panels/widgets/fansPage.py +++ b/BlocksScreen/lib/panels/widgets/fansPage.py @@ -1,15 +1,14 @@ from PyQt6 import QtCore, QtWidgets -import typing class FansPage(QtWidgets.QWidget): def __init__( self, - parent: typing.Optional["QtWidgets.QWidget"], - flags: typing.Optional["QtCore.Qt.WindowType"], + parent: QtWidgets.QWidget | None, + flags: QtCore.Qt.WindowType | None, ) -> None: if parent is not None and flags is not None: - super(FansPage, self).__init__(parent, flags) + super().__init__(parent, flags) else: - super(FansPage, self).__init__() + super().__init__() diff --git a/BlocksScreen/lib/panels/widgets/filesPage.py b/BlocksScreen/lib/panels/widgets/filesPage.py index 969399ac..d731eb9c 100644 --- a/BlocksScreen/lib/panels/widgets/filesPage.py +++ b/BlocksScreen/lib/panels/widgets/filesPage.py @@ -1,6 +1,5 @@ import json import logging -import typing import helper_methods from lib.utils.blocks_Scrollbar import CustomScrollBar @@ -34,13 +33,13 @@ class FilesPage(QtWidgets.QWidget): ICON_PATHS = { "back_folder": ":/ui/media/btn_icons/back_folder.svg", "folder": ":/ui/media/btn_icons/folderIcon.svg", - "right_arrow": ":/arrow_icons/media/btn_icons/right_arrow.svg", + "right_arrow": ":/arrow_icons/media/btn_icons/arrow_right.svg", "usb": ":/ui/media/btn_icons/usb_icon.svg", "back": ":/ui/media/btn_icons/back.svg", "refresh": ":/ui/media/btn_icons/refresh.svg", } - def __init__(self, parent: typing.Optional[QtWidgets.QWidget] = None) -> None: + def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: super().__init__(parent) self._file_list: list[dict] = [] @@ -263,7 +262,7 @@ def _find_file_insert_position(self, modified_time: float) -> int: return insert_pos - def _find_file_key_by_display_name(self, display_name: str) -> typing.Optional[str]: + def _find_file_key_by_display_name(self, display_name: str) -> str | None: """Find the file key in _files_data by its display name.""" for key in self._files_data: if self._get_display_name(key) == display_name: @@ -691,7 +690,7 @@ def _add_file_to_list(self, file_item: dict) -> None: if item: self._model.add_item(item) - def _create_file_list_item(self, filedata: dict) -> typing.Optional[ListItem]: + def _create_file_list_item(self, filedata: dict) -> ListItem | None: """Create a ListItem from file metadata.""" filename = filedata.get("filename", "") if not filename: diff --git a/BlocksScreen/lib/panels/widgets/inputshaperPage.py b/BlocksScreen/lib/panels/widgets/inputshaperPage.py index ead82cfb..dd890fb5 100644 --- a/BlocksScreen/lib/panels/widgets/inputshaperPage.py +++ b/BlocksScreen/lib/panels/widgets/inputshaperPage.py @@ -1,11 +1,11 @@ +import typing + from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.icon_button import IconButton from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem from PyQt6 import QtCore, QtGui, QtWidgets -import typing - class InputShaperPage(QtWidgets.QWidget): """Update GUI Page, @@ -24,6 +24,7 @@ def __init__(self, parent=None) -> None: else: super().__init__() self._setupUI() + self.currentItem: ListItem | None = None self.selected_item: ListItem | None = None self.ongoing_update: bool = False self.type_dict: dict = {} @@ -96,21 +97,26 @@ def on_item_clicked(self, item: ListItem) -> None: if not current_info: return - self.vib_label.setText(str("%.0f" % current_info.get("vibration", "N/A")) + "%") + _vib = current_info.get("vibration") + self.vib_label.setText(f"{float(_vib):.0f}%" if _vib is not None else "N/A%") + _accel = current_info.get("max_accel") self.sug_accel_label.setText( - str("%.0f" % current_info.get("max_accel", "N/A")) + "mm/s²" + f"{float(_accel):.0f}mm/s²" if _accel is not None else "N/Amm/s²" ) self.action_btn.show() def handle_ism_confirm(self) -> None: + """Apply the selected input shaper type to the printer and save the config.""" + if self.currentItem is None: + return current_info = self.type_dict.get(self.currentItem.text, {}) frequency = current_info.get("frequency", "N/A") - if self.type_dict["Axis"] == "x": + if self.type_dict.get("Axis") == "x": self.run_gcode_signal.emit( f"SET_INPUT_SHAPER SHAPER_TYPE_X={self.currentItem.text} SHAPER_FREQ_X={frequency}" ) - elif self.type_dict["Axis"] == "y": + elif self.type_dict.get("Axis") == "y": self.run_gcode_signal.emit( f"SET_INPUT_SHAPER SHAPER_TYPE_Y={self.currentItem.text} SHAPER_FREQ_Y={frequency}" ) @@ -138,7 +144,8 @@ def _setupUI(self) -> None: font_id = QtGui.QFontDatabase.addApplicationFont( ":/font/media/fonts for text/Momcake-Bold.ttf" ) - font_family = QtGui.QFontDatabase.applicationFontFamilies(font_id)[0] + _families = QtGui.QFontDatabase.applicationFontFamilies(font_id) + font_family = _families[0] if _families else "" sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding, diff --git a/BlocksScreen/lib/panels/widgets/notificationPage.py b/BlocksScreen/lib/panels/widgets/notificationPage.py index a14b6cbc..6bf1fd00 100644 --- a/BlocksScreen/lib/panels/widgets/notificationPage.py +++ b/BlocksScreen/lib/panels/widgets/notificationPage.py @@ -1,15 +1,12 @@ -from lib.utils.blocks_frame import BlocksCustomFrame -from lib.utils.blocks_button import BlocksCustomButton -from lib.utils.icon_button import IconButton -from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem -from PyQt6 import QtCore, QtGui, QtWidgets import typing - from collections import deque -from typing import Deque - from lib.panels.widgets.popupDialogWidget import Popup +from lib.utils.blocks_button import BlocksCustomButton +from lib.utils.blocks_frame import BlocksCustomFrame +from lib.utils.icon_button import IconButton +from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem +from PyQt6 import QtCore, QtGui, QtWidgets class NotificationPage(QtWidgets.QWidget): @@ -28,7 +25,7 @@ def __init__(self, parent=None) -> None: else: super().__init__() self._setupUI() - self.cli_tracking: Deque = deque() + self.cli_tracking: deque = deque() self.selected_item: ListItem | None = None self.ongoing_update: bool = False self.popup = Popup(self) @@ -78,6 +75,8 @@ def reset_view_model(self) -> None: def build_model_list(self) -> None: """Builds the model list (`self.model`) containing updatable clients""" + if not self.cli_tracking: + return self.update_buttons_list_widget.blockSignals(True) message, origin, priority = self.cli_tracking.popleft() match priority: @@ -194,11 +193,9 @@ def _setupUI(self) -> None: font.setPointSize(20) self.setSizePolicy(sizePolicy) self.setObjectName("updatePage") - self.setStyleSheet( - """#updatePage { + self.setStyleSheet("""#updatePage { background-image: url(:/background/media/1st_background.png); - }""" - ) + }""") self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) self.update_page_content_layout = QtWidgets.QVBoxLayout() self.setMinimumSize(800, 480) @@ -347,10 +344,10 @@ def _setupUI(self) -> None: self.time_title.setFont(font) self.time_title.setStyleSheet("color:#FFFFFF") - self.time_title.setFont(font) + self.type_label.setFont(font) self.type_label.setStyleSheet("color:#FFFFFF") - self.time_title.setFont(font) + self.time_label.setFont(font) self.time_label.setStyleSheet("color:#FFFFFF") self.info_frame.setLayout(self.info_box_layout) diff --git a/BlocksScreen/lib/panels/widgets/numpadPage.py b/BlocksScreen/lib/panels/widgets/numpadPage.py index b904645c..40e222cc 100644 --- a/BlocksScreen/lib/panels/widgets/numpadPage.py +++ b/BlocksScreen/lib/panels/widgets/numpadPage.py @@ -1,7 +1,6 @@ -from lib.utils.icon_button import IconButton from lib.utils.blocks_label import BlocksLabel +from lib.utils.icon_button import IconButton from lib.utils.numpad_button import NumpadButton - from PyQt6 import QtCore, QtGui, QtWidgets diff --git a/BlocksScreen/lib/panels/widgets/optionCardWidget.py b/BlocksScreen/lib/panels/widgets/optionCardWidget.py index 6fbfe20d..e331049d 100644 --- a/BlocksScreen/lib/panels/widgets/optionCardWidget.py +++ b/BlocksScreen/lib/panels/widgets/optionCardWidget.py @@ -1,7 +1,7 @@ import typing -from PyQt6 import QtCore, QtGui, QtWidgets from lib.utils.icon_button import IconButton +from PyQt6 import QtCore, QtGui, QtWidgets class OptionCard(QtWidgets.QAbstractButton): diff --git a/BlocksScreen/lib/panels/widgets/popupDialogWidget.py b/BlocksScreen/lib/panels/widgets/popupDialogWidget.py index d565bd3a..b702e840 100644 --- a/BlocksScreen/lib/panels/widgets/popupDialogWidget.py +++ b/BlocksScreen/lib/panels/widgets/popupDialogWidget.py @@ -1,6 +1,5 @@ import enum from collections import deque -from typing import Deque from lib.utils.icon_button import IconButton from PyQt6 import QtCore, QtGui, QtWidgets @@ -26,9 +25,9 @@ def __init__(self, parent) -> None: super().__init__(parent) self.timeout_timer = QtCore.QTimer(self) self.timeout_timer.setSingleShot(True) - self.messages: Deque = deque() + self.messages: deque = deque() self.isShown = False - self.persistent_notifications: Deque = deque() + self.persistent_notifications: deque = deque() self.message_type: Popup.MessageType = Popup.MessageType.INFO self.default_background_color = QtGui.QColor(164, 164, 164) self.info_icon = QtGui.QPixmap(":ui/media/btn_icons/info.svg") @@ -81,7 +80,8 @@ def _calculate_target_geometry(self) -> QtCore.QRect: if isinstance(widget, QtWidgets.QMainWindow): main_window = widget break - + if main_window is None: + return QtCore.QRect() parent_rect = main_window.geometry() width = int(parent_rect.width() * 0.85) diff --git a/BlocksScreen/lib/panels/widgets/printcorePage.py b/BlocksScreen/lib/panels/widgets/printcorePage.py index c2683cd1..b5942e54 100644 --- a/BlocksScreen/lib/panels/widgets/printcorePage.py +++ b/BlocksScreen/lib/panels/widgets/printcorePage.py @@ -34,6 +34,8 @@ def _geometry_calc(self) -> None: for widget in app_instance.allWidgets(): if isinstance(widget, QtWidgets.QMainWindow): main_window = widget + if main_window is None: + return x = main_window.geometry().x() y = main_window.geometry().y() width = main_window.width() diff --git a/BlocksScreen/lib/panels/widgets/probeHelperPage.py b/BlocksScreen/lib/panels/widgets/probeHelperPage.py index 219adb6a..902f9a4c 100644 --- a/BlocksScreen/lib/panels/widgets/probeHelperPage.py +++ b/BlocksScreen/lib/panels/widgets/probeHelperPage.py @@ -42,7 +42,6 @@ class ProbeHelper(QtWidgets.QWidget): ) distances = ["0.01", ".025", "0.1", "0.5", "1"] - _calibration_commands: list = [] helper_start: bool = False helper_initialize: bool = False _zhop_height: float = float(distances[0]) @@ -52,6 +51,7 @@ class ProbeHelper(QtWidgets.QWidget): z_offset_calibration_speed: int = 100 def __init__(self, parent: QtWidgets.QWidget) -> None: + """Initialize the probe helper widget and connect internal signals.""" super().__init__(parent) self.setObjectName("probe_offset_page") @@ -92,6 +92,7 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: self.target_temp = 0 self.current_temp = 0 self._eddy_calibration_state = False + self._calibration_commands: list = [] @QtCore.pyqtSlot(str, dict, name="on_print_stats_update") @QtCore.pyqtSlot(str, float, name="on_print_stats_update") @@ -281,8 +282,6 @@ def on_object_config(self, config: dict | list) -> None: if not _config: return if _config.get("home_xy_position"): - if not _config.get("home_xy_position"): - return self.z_offset_safe_xy = tuple( map( lambda value: float(value), @@ -333,6 +332,7 @@ def on_printer_config(self, config: dict) -> None: @QtCore.pyqtSlot(dict, name="on_available_gcode_cmds") def on_available_gcode_cmds(self, gcode_cmds: dict) -> None: """Setup available probe calibration commands""" + self._calibration_commands.clear() _available_commands = gcode_cmds.keys() if "PROBE_CALIBRATE" in _available_commands: self._calibration_commands.append("PROBE_CALIBRATE") @@ -367,16 +367,11 @@ def _build_calibration_command(self, tool: str) -> str: return "Z_ENDSTOP_CALIBRATE" elif "eddy" in tool: if self._verify_gcode("PROBE_EDDY_CURRENT_CALIBRATE"): - _name = tool.split(" ")[1] - # if not _name: - # return "" - # return ( - # f"PROBE_EDDY_CURRENT_CALIBRATE CHIP={tool.split(' ')[1]}" - # ) - return ( - f"PROBE_EDDY_CURRENT_CALIBRATE CHIP={tool.split(' ')[1]}" - * bool(_name) - ) + ("" * ~bool(_name)) + _parts = tool.split(" ", 1) + if len(_parts) < 2: + return "" + _name = _parts[1] + return f"PROBE_EDDY_CURRENT_CALIBRATE CHIP={_name}" if _name else "" elif "probe" in tool or "bltouch" in tool: if self._verify_gcode("PROBE_CALIBRATE"): @@ -403,7 +398,7 @@ def handle_zhopHeight_change(self, new_value: float) -> None: self._zhop_height = new_value @QtCore.pyqtSlot("PyQt_PyObject", name="handle_start_tool") - def handle_start_tool(self, sender: typing.Type[OptionCard]) -> None: + def handle_start_tool(self, sender: OptionCard) -> None: """Handle probe tool helper start by sending the correct gcode command according to the clicked option card. This is achieved by @@ -414,7 +409,7 @@ def handle_start_tool(self, sender: typing.Type[OptionCard]) -> None: sender. Args: - sender (typing.Type[OptionCard]): The clicked OptionCard object + sender (OptionCard): The clicked OptionCard instance """ if not sender: return @@ -429,19 +424,21 @@ def handle_start_tool(self, sender: typing.Type[OptionCard]) -> None: lambda: self.query_printer_object.emit({"manual_probe": None}) ) _timer.start(int(300)) - # self.query_printer_object.emit({"manual_probe": None}) - _cmd = self._build_calibration_command(sender.name) # type:ignore + _cmd = self._build_calibration_command(sender.name) # type: ignore if not _cmd: return self.disable_popups.emit(True) self.run_gcode_signal.emit("G28\nM400") - if "eddy" in sender.name: # type:ignore + if "eddy" in sender.name: # type: ignore self.call_load_panel.emit(True, "Preparing Eddy Current Calibration...") self.toggle_conn_page.emit(False) self._move_to_pos(self.z_offset_safe_xy[0], self.z_offset_safe_xy[1], 100) + _name_parts = sender.name.split(" ", 1) # type: ignore + if len(_name_parts) < 2: + return self.run_gcode_signal.emit( - f"LDC_CALIBRATE_DRIVE_CURRENT CHIP={sender.name.split(' ')[1]}" # type:ignore + f"LDC_CALIBRATE_DRIVE_CURRENT CHIP={_name_parts[1]}" ) self.run_gcode_signal.emit("M400\nSAVE_CONFIG") @@ -467,19 +464,19 @@ def on_extruder_update( return if self.target_temp != 0: if self.current_temp == self.target_temp: - if self.isVisible: + if self.isVisible(): self.call_load_panel.emit(True, "Extruder heated up \n Please wait") return if field == "temperature": self.current_temp = round(new_value, 0) - if self.isVisible: + if self.isVisible(): self.call_load_panel.emit( True, f"Heating up ({new_value}/{self.target_temp}) \n Please wait", ) if field == "target": self.target_temp = round(new_value, 0) - if self.isVisible: + if self.isVisible(): self.call_load_panel.emit(True, "Cleaning the nozzle \n Please wait") @QtCore.pyqtSlot(name="handle_accept") @@ -523,32 +520,32 @@ def on_gcode_move_update(self, name: str, value: list) -> None: @QtCore.pyqtSlot(dict, name="on_manual_probe_update") def on_manual_probe_update(self, update: dict) -> None: - """Handle manual probe update""" + """Handle manual probe update. + + Only process ``is_active`` state transitions when the key is + actually present in the update dict. Klipper sends partial + updates (e.g. only position data after TESTZ) and defaulting + ``is_active`` to False on those would reset the entire UI. + """ if not update: return - # if update.get("z_position_lower"): - # f"{update.get('z_position_lower'):.4f} mm" - - is_active = update.get("is_active", None) - if update.get("z_position_upper"): - self.old_offset_info.setText(f"{update.get('z_position_upper'):.4f} mm") - if update.get("z_position"): - self.current_offset_info.setText(f"{update.get('z_position'):.4f} mm") + if "is_active" in update: + is_active = update["is_active"] + if is_active and not self.isVisible(): + self.request_page_view.emit() + self.helper_initialize = False + self.helper_start = is_active + self._toggle_tool_buttons(is_active) + if is_active: + self._hide_option_cards() + else: + self._show_option_cards() - if not is_active: - return - if not self.isVisible(): - self.request_page_view.emit() - # Shared state updates - self.helper_initialize = False - self.helper_start = is_active - # UI updates - self._toggle_tool_buttons(is_active) - if is_active: - self._hide_option_cards() - else: - self._show_option_cards() + if update.get("z_position_upper") is not None: + self.old_offset_info.setText(f"{update['z_position_upper']:.4f} mm") + if update.get("z_position") is not None: + self.current_offset_info.setText(f"{update['z_position']:.4f} mm") @QtCore.pyqtSlot(list, name="handle_gcode_response") def handle_gcode_response(self, data: list) -> None: @@ -558,6 +555,8 @@ def handle_gcode_response(self, data: list) -> None: data (list): A list containing the gcode that originated the response and the response """ + if not data: + return if self.isVisible(): if data[0].startswith("!!"): # An error occurred if "already in a manual z probe" in data[0].strip("!! ").lower(): diff --git a/BlocksScreen/lib/panels/widgets/sensorWidget.py b/BlocksScreen/lib/panels/widgets/sensorWidget.py index e0ed9955..23fc46dc 100644 --- a/BlocksScreen/lib/panels/widgets/sensorWidget.py +++ b/BlocksScreen/lib/panels/widgets/sensorWidget.py @@ -37,10 +37,11 @@ class SensorState(enum.IntEnum): def __init__(self, parent, sensor_name: str): super().__init__(parent) - self.name = str(sensor_name).split(" ")[1] + _parts = str(sensor_name).split(" ", 1) + self.name = _parts[1] if len(_parts) > 1 else _parts[0] self.sensor_type: SensorWidget.SensorType = ( self.SensorType.SWITCH - if "switch" in str(sensor_name).split(" ")[0].lower() + if "switch" in _parts[0].lower() else self.SensorType.MOTION ) @@ -96,13 +97,13 @@ def text(self, new_text) -> None: self._text_label.setText(f"{new_text}") self._text = new_text - @QtCore.pyqtSlot(FilamentState, name="change_fil_sensor_state") - def change_fil_sensor_state(self, state: FilamentState): - """Invert the filament state in response to a Klipper update""" + def set_filament_state(self, state: FilamentState) -> None: + """Set the filament state directly from a Klipper update.""" if not isinstance(state, SensorWidget.FilamentState): return - self.filament_state = SensorWidget.FilamentState(not state.value) - self.update() + if self.filament_state != state: + self.filament_state = state + self.update() def toggle_button_state(self, state: ToggleAnimatedButton.State) -> None: """Called when the Klipper firmware reports an update to the filament sensor state""" diff --git a/BlocksScreen/lib/panels/widgets/sensorsPanel.py b/BlocksScreen/lib/panels/widgets/sensorsPanel.py index df63cfb5..77fe5865 100644 --- a/BlocksScreen/lib/panels/widgets/sensorsPanel.py +++ b/BlocksScreen/lib/panels/widgets/sensorsPanel.py @@ -19,7 +19,7 @@ class SensorsWindow(QtWidgets.QWidget): ) def __init__(self, parent): - super(SensorsWindow, self).__init__(parent) + super().__init__(parent) self.model = EntryListModel() self.entry_delegate = EntryDelegate() self.sensor_tracking_widget = {} @@ -32,11 +32,15 @@ def __init__(self, parent): self.fs_back_button.clicked.connect(self.request_back) def reset_view_model(self) -> None: - """Clears items from ListView - (Resets `QAbstractListModel` by clearing entries) - """ + """Clears items from ListView and removes existing sensor widgets.""" self.model.clear() self.entry_delegate.clear() + for widget in self.sensor_tracking_widget.values(): + self.info_box_layout.removeWidget(widget) + widget.deleteLater() + self.sensor_tracking_widget.clear() + self.sensor_list.clear() + self.current_widget = None @QtCore.pyqtSlot(dict, name="handle_available_fil_sensors") def handle_available_fil_sensors(self, sensors: dict) -> None: @@ -46,7 +50,7 @@ def handle_available_fil_sensors(self, sensors: dict) -> None: self.reset_view_model() filtered_sensors = [ sensor - for sensor in sensors.keys() + for sensor in sensors if sensor.startswith( ("filament_switch_sensor", "filament_motion_sensor", "cutter_sensor") ) @@ -63,14 +67,20 @@ def handle_available_fil_sensors(self, sensors: dict) -> None: def handle_fil_state_change( self, sensor_name: str, parameter: str, value: bool ) -> None: - """Handle Klipper signals for filament sensor changes""" + """Handle Klipper signals for filament sensor changes.""" _item = self.sensor_tracking_widget.get(sensor_name) - if _item: - if parameter == "filament_detected": - state = SensorWidget.FilamentState(not value) - _item.change_fil_sensor_state(state) - elif parameter == "enabled": - _item.toggle_button_state(SensorWidget.SensorState(value)) + if not _item: + return + if parameter == "filament_detected": + # filament_detected=True means filament IS present + state = ( + SensorWidget.FilamentState.PRESENT + if value + else SensorWidget.FilamentState.MISSING + ) + _item.set_filament_state(state) + elif parameter == "enabled": + _item.toggle_button_state(SensorWidget.SensorState(value)) def showEvent(self, event: QtGui.QShowEvent | None) -> None: """Re-add clients to update list""" @@ -108,7 +118,8 @@ def create_sensor_widget(self, name: str) -> SensorWidget: else: _item_widget.show() self.current_widget = _item_widget - name_id = str(name).split(" ")[1] + _parts = str(name).split(" ", 1) + name_id = _parts[1] if len(_parts) > 1 else _parts[0] item = ListItem( text=name_id, right_text="", @@ -133,7 +144,8 @@ def _setupUi(self) -> None: font_id = QtGui.QFontDatabase.addApplicationFont( ":/font/media/fonts for text/Momcake-Bold.ttf" ) - font_family = QtGui.QFontDatabase.applicationFontFamilies(font_id)[0] + _families = QtGui.QFontDatabase.applicationFontFamilies(font_id) + font_family = _families[0] if _families else "" sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding, diff --git a/BlocksScreen/lib/panels/widgets/troubleshootPage.py b/BlocksScreen/lib/panels/widgets/troubleshootPage.py index 0c327ac7..8673f045 100644 --- a/BlocksScreen/lib/panels/widgets/troubleshootPage.py +++ b/BlocksScreen/lib/panels/widgets/troubleshootPage.py @@ -1,6 +1,5 @@ -from PyQt6 import QtCore, QtGui, QtWidgets - from lib.utils.icon_button import IconButton +from PyQt6 import QtCore, QtGui, QtWidgets class TroubleshootPage(QtWidgets.QDialog): @@ -9,14 +8,12 @@ def __init__( parent: QtWidgets.QWidget, ) -> None: super().__init__(parent) - self.setStyleSheet( - """ + self.setStyleSheet(""" #troubleshoot_page { background-image: url(:/background/media/1st_background.png); border: none; } - """ - ) + """) self.setWindowFlags( QtCore.Qt.WindowType.Popup | QtCore.Qt.WindowType.FramelessWindowHint ) diff --git a/BlocksScreen/lib/panels/widgets/tunePage.py b/BlocksScreen/lib/panels/widgets/tunePage.py index 1b68c2f7..2394184c 100644 --- a/BlocksScreen/lib/panels/widgets/tunePage.py +++ b/BlocksScreen/lib/panels/widgets/tunePage.py @@ -2,7 +2,6 @@ import typing import logging -from helper_methods import normalize from lib.utils.blocks_button import BlocksCustomButton from lib.utils.display_button import DisplayButton from lib.utils.icon_button import IconButton @@ -53,7 +52,7 @@ def __init__(self, parent) -> None: lambda: self.request_sliderPage[str, int, "PyQt_PyObject", int, int].emit( "Speed", int(self.speed_factor_override * 100), - self.on_slider_change, + self._on_speed_slider_change, 10, 300, ) @@ -99,30 +98,26 @@ def on_printer_config(self, config: dict) -> None: @QtCore.pyqtSlot(str, int, name="on_numpad_change") def on_numpad_change(self, name: str, new_value: int) -> None: - """Handle numpad value inserted""" + """Handle numpad value inserted.""" if "bed" in name.lower(): name = "heater_bed" elif "extruder" in name.lower(): name = "extruder" self.run_gcode.emit(f"SET_HEATER_TEMPERATURE HEATER={name} TARGET={new_value}") + @QtCore.pyqtSlot(str, int) + def _on_speed_slider_change(self, _name: str, new_value: int) -> None: + """Handle print speed slider change.""" + self.speed_factor_override = new_value / 100 + self.run_gcode.emit(f"M220 S{new_value}") + @QtCore.pyqtSlot(str, int, name="on_slider_change") def on_slider_change(self, name: str, new_value: int) -> None: - """Handle slider page value inserted""" - if "speed" in name.lower(): - self.speed_factor_override = new_value / 100 - self.run_gcode.emit(f"M220 S{new_value}") - - if "fan" in name.lower(): - if name.lower() == "fan": - self.run_gcode.emit( - f"M106 S{int(round((normalize(float(new_value / 100), 0.0, 1.0, 0, 255))))}" - ) # [0, 255] Range - else: - name = name.replace(" ", "_") - self.run_gcode.emit( - f"SET_FAN_SPEED FAN={name} SPEED={float(new_value / 100.00)}" - ) # [0.0, 1.0] Range + """Handle fan_generic slider value change.""" + gcode_name = name.replace(" ", "_") + self.run_gcode.emit( + f"SET_FAN_SPEED FAN={gcode_name} SPEED={float(new_value / 100.00)}" + ) # [0.0, 1.0] Range @QtCore.pyqtSlot(str, str, float, name="on_fan_update") @QtCore.pyqtSlot(str, str, int, name="on_fan_update") @@ -137,6 +132,8 @@ def on_fan_object_update( new_value (int | float): New value for field name """ fields = name.split() + if not fields: + return first_field = fields[0] second_field = fields[1] if len(fields) > 1 else None name = second_field.replace("_", " ") if second_field else name diff --git a/BlocksScreen/lib/panels/widgets/updatePage.py b/BlocksScreen/lib/panels/widgets/updatePage.py index b91e41d3..15e72b00 100644 --- a/BlocksScreen/lib/panels/widgets/updatePage.py +++ b/BlocksScreen/lib/panels/widgets/updatePage.py @@ -101,7 +101,7 @@ def on_request_reload(self, service: str | None = None) -> None: """Handles reload button click, requests update status refresh""" self.show_loading(True) if service: - self.request_refresh_update.emit([service]) + self.request_refresh_update[str].emit(service) else: self.request_refresh_update.emit() @@ -197,14 +197,16 @@ def on_item_clicked(self, item: ListItem) -> None: if not _remote_version: self.remote_version_title.hide() self.remote_version_tracking.hide() - self.remote_version_title.show() - self.remote_version_tracking.show() - self.remote_version_title.setText("Remote Version: ") - self.remote_version_tracking.setText(_remote_version) + else: + self.remote_version_title.show() + self.remote_version_tracking.show() + self.remote_version_title.setText("Remote Version: ") + self.remote_version_tracking.setText(_remote_version) _curr_version = cli_data.get("version", None) if not _curr_version: # There is no version information something is seriously wrong here self.action_btn.setText("Recover") + return self.version_title.show() self.version_tracking_info.show() self.version_tracking_info.setText(_curr_version) @@ -295,7 +297,8 @@ def _setupUI(self) -> None: font_id = QtGui.QFontDatabase.addApplicationFont( ":/font/media/fonts for text/Momcake-Bold.ttf" ) - font_family = QtGui.QFontDatabase.applicationFontFamilies(font_id)[0] + _families = QtGui.QFontDatabase.applicationFontFamilies(font_id) + font_family = _families[0] if _families else "" sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding, @@ -304,11 +307,9 @@ def _setupUI(self) -> None: sizePolicy.setVerticalStretch(1) self.setSizePolicy(sizePolicy) self.setObjectName("updatePage") - self.setStyleSheet( - """#updatePage { + self.setStyleSheet("""#updatePage { background-image: url(:/background/media/1st_background.png); - }""" - ) + }""") self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) self.update_page_content_layout = QtWidgets.QVBoxLayout() self.update_page_content_layout.setContentsMargins(15, 15, 15, 15) diff --git a/BlocksScreen/lib/printer.py b/BlocksScreen/lib/printer.py index c6c76fbc..fc820730 100644 --- a/BlocksScreen/lib/printer.py +++ b/BlocksScreen/lib/printer.py @@ -105,7 +105,7 @@ class Printer(QtCore.QObject): current_loaded_file_metadata: str = "" def __init__(self, parent: QtCore.QObject, ws: MoonWebSocket, /) -> None: - super(Printer, self).__init__(parent) + super().__init__(parent) self.ws = ws self.active_extruder_name: str = "" @@ -138,6 +138,18 @@ def clear_printer_objs(self) -> None: self.printer_busy = False self.current_loaded_file = "" self.current_loaded_file_metadata = "" + _heater_attributes: dict = { + "current_temperature": 0.0, + "target_temperature": 0.0, + "can_extrude": False, + } + self.heaters_object = { + "extruder": _heater_attributes.copy(), + "bed": _heater_attributes.copy(), + } + self.active_extruder_name = "" + self.available_filament_sensors = {} + self.has_chamber = False @QtCore.pyqtSlot(str, name="on_klippy_status") def on_klippy_status(self, state: str): @@ -224,7 +236,7 @@ def get_config(self, section_name: str) -> dict: return _config[0].get(section_name, {}) def search_config_list( - self, search_list: list[str], _objects: typing.Optional[list] = None + self, search_list: list[str], _objects: list | None = None ) -> list: """ Search a list of printer objects recursively @@ -295,7 +307,7 @@ def _gcode_response(self, report: list) -> None: self.gcode_response.emit(report) def _webhook_printcore_updated(self, value: dict): - self.on_printcore_update[dict].emit(value) + self.on_printcore_update.emit(value) def _webhooks_object_updated(self, value: dict, name: str = "webhooks") -> None: """Sends an event type according to the received state @@ -305,7 +317,7 @@ def _webhooks_object_updated(self, value: dict, name: str = "webhooks") -> None: value (dict): _description_ name (str, optional): _description_. Defaults to "". """ - if "state" in value.keys() and "state_message" in value.keys(): + if "state" in value and "state_message" in value: self.webhooks_update.emit(value["state"], value["state_message"]) logger.debug("Webhooks message received") _state: str = value["state"] @@ -318,71 +330,71 @@ def _webhooks_object_updated(self, value: dict, name: str = "webhooks") -> None: event = _event_callback(value["state"], value["state_message"]) instance = QtWidgets.QApplication.instance() if instance is not None and isinstance(event, QtCore.QEvent): - instance.sendEvent(self.parent(), event) + instance.postEvent(self.parent(), event) else: raise TypeError("QApplication.instance is None type.") except Exception as e: logger.debug( - "Unable to send internal Klippy %s notification : %e", + "Unable to send internal Klippy %s notification : %s", _state_call, e, ) def _gcode_move_object_updated(self, value: dict, name: str = "gcode_move") -> None: - if "speed_factor" in value.keys(): + if "speed_factor" in value: self.gcode_move_update[str, float].emit( "speed_factor", value["speed_factor"] ) - if "speed" in value.keys(): + if "speed" in value: self.gcode_move_update[str, float].emit("speed", value["speed"]) - if "extrude_factor" in value.keys(): + if "extrude_factor" in value: self.gcode_move_update[str, float].emit( "extruder_factor", value["extrude_factor"] ) - if "absolute_coordinates" in value.keys(): + if "absolute_coordinates" in value: self.gcode_move_update[str, bool].emit( "absolute_coordinates", value["absolute_coordinates"] ) - if "absolute_extrude" in value.keys(): + if "absolute_extrude" in value: self.gcode_move_update[str, bool].emit( "absolute_extrude", value["absolute_extrude"] ) - if "homing_origin" in value.keys(): + if "homing_origin" in value: self.gcode_move_update[str, list].emit( "homing_origin", value["homing_origin"] ) - if "position" in value.keys(): + if "position" in value: self.gcode_move_update[str, list].emit("position", value["position"]) - if "gcode_position" in value.keys(): + if "gcode_position" in value: self.gcode_move_update[str, list].emit( "gcode_position", value["gcode_position"] ) def _toolhead_object_updated(self, values: dict, name: str = "toolhead") -> None: - if "homed_axes" in values.keys(): + if "homed_axes" in values: self.toolhead_update[str, str].emit("homed_axes", values["homed_axes"]) - if "print_time" in values.keys(): + if "print_time" in values: self.toolhead_update[str, float].emit("print_time", values["print_time"]) - if "estimated_print_time" in values.keys(): + if "estimated_print_time" in values: self.toolhead_update[str, float].emit( "estimated_print_time", values["estimated_print_time"] ) - if "extruder" in values.keys(): + if "extruder" in values: self.toolhead_update[str, str].emit("extruder", values["extruder"]) self.active_extruder_name = values["extruder"] - if "position" in values.keys(): + if "position" in values: self.toolhead_update[str, list].emit("position", values["position"]) - if "max_velocity" in values.keys(): + if "max_velocity" in values: self.toolhead_update[str, float].emit( "max_velocity", values["max_velocity"] ) - if "max_accel" in values.keys(): + if "max_accel" in values: self.toolhead_update[str, float].emit("max_accel", values["max_accel"]) - if "max_accel_to_decel" in values.keys(): + if "max_accel_to_decel" in values: self.toolhead_update[str, float].emit( "max_accel_to_decel", values["max_accel_to_decel"] ) - if "square_corner_velocity" in values.keys(): + if "square_corner_velocity" in values: self.toolhead_update[str, float].emit( "square_corner_velocity", values["square_corner_velocity"] ) @@ -390,76 +402,79 @@ def _toolhead_object_updated(self, values: dict, name: str = "toolhead") -> None def _extruder_object_updated( self, value: dict, extruder_name: str = "extruder" ) -> None: - if "temperature" in value.keys(): + """Handle extruder object updates and emit corresponding signals.""" + if extruder_name not in self.heaters_object: + self.heaters_object[extruder_name] = {} + if "temperature" in value: self.extruder_update.emit( extruder_name, "temperature", value["temperature"] ) self.heaters_object[f"{extruder_name}"]["actual_temperature"] = value[ "temperature" ] - if "target" in value.keys(): + if "target" in value: self.extruder_update.emit(extruder_name, "target", value["target"]) self.heaters_object[f"{extruder_name}"]["target_temperature"] = value[ "target" ] - if "can_extrude" in value.keys(): + if "can_extrude" in value: self.heaters_object[f"{extruder_name}"]["can_extrude"] = value[ "can_extrude" ] - if "power" in value.keys(): + if "power" in value: self.extruder_update.emit(extruder_name, "power", value["power"]) - if "pressure_advance" in value.keys(): + if "pressure_advance" in value: self.extruder_update.emit( extruder_name, "pressure_advance", value["pressure_advance"] ) - if "smooth_time" in value.keys(): + if "smooth_time" in value: self.extruder_update.emit( extruder_name, "smooth_time", value["smooth_time"] ) - if "can_extrude" in value.keys(): + if "can_extrude" in value: pass def _heater_bed_object_updated( self, value: dict, heater_name: str = "heater_bed" ) -> None: - if "temperature" in value.keys(): + if "temperature" in value: self.heater_bed_update.emit( heater_name, "temperature", value["temperature"] ) self.heaters_object["bed"]["actual_temperature"] = value["temperature"] - if "target" in value.keys(): + if "target" in value: self.heater_bed_update.emit(heater_name, "target", value["target"]) self.heaters_object["bed"]["target_temperature"] = value["target"] - if "power" in value.keys(): + if "power" in value: self.heater_bed_update.emit(heater_name, "power", value["power"]) def _chamber_object_updated(self, value: dict, heater_name: str = "chamber"): self.has_chamber = True def _fan_object_updated(self, value: dict, fan_name: str = "fan") -> None: - if "speed" in value.keys(): + if "speed" in value: self.fan_update[str, str, float].emit("fan", "speed", value["speed"]) - if "rpm" in value.keys(): + if "rpm" in value: self.fan_update[str, str, int].emit("fan", "rpm", value["rpm"]) def _fan_generic_object_updated(self, value: dict, fan_name: str = "") -> None: _names = ["fan_generic", fan_name] object_name = " ".join(_names) - if "speed" in value.keys(): + if "speed" in value: self.fan_update[str, str, float].emit( object_name, "speed", value.get("speed") ) - if "rpm" in value.keys(): + if "rpm" in value: self.fan_update[str, str, int].emit(object_name, "rpm", value.get("rpm")) def _controller_fan_object_updated(self, value: dict, fan_name: str = "") -> None: _names = ["controller_fan", fan_name] object_name = " ".join(_names) - if "speed" in value.keys(): + if "speed" in value: self.fan_update[str, str, float].emit( object_name, "speed", value.get("speed") ) - elif "rpm" in value.keys(): + elif "rpm" in value: self.fan_update[str, str, int].emit(object_name, "rpm", value.get("rpm")) def _heater_fan_object_updated(self, value: dict, fan_name: str = "") -> None: @@ -469,20 +484,20 @@ def _heater_fan_object_updated(self, value: dict, fan_name: str = "") -> None: # object_name = " ".join(_names) def _z_tilt_object_updated(self, value: dict, name: str = "") -> None: - if value["applied"]: + if value.get("applied"): self.z_tilt_update[str, bool].emit("applied", value["applied"]) def _idle_timeout_object_updated( self, value: dict, name: str = "idle_timeout" ) -> None: - if "state" in value.keys(): + if "state" in value: self.idle_timeout_update[str, str].emit("state", value["state"]) if "printing" in value["state"]: self.printer_busy = True elif self.printing_state != "printing" and value["state"] != "printing": # It's also busy if the printer is printing or paused self.printer_busy = False - if "printing_time" in value.keys(): + if "printing_time" in value: self.idle_timeout_update[str, float].emit( "printing_time", value["printing_time"] ) @@ -490,11 +505,11 @@ def _idle_timeout_object_updated( def _virtual_sdcard_object_updated( self, values: dict, name: str = "virtual_sdcard" ) -> None: - if "progress" in values.keys(): + if "progress" in values: self.virtual_sdcard_update[str, float].emit("progress", values["progress"]) - if "is_active" in values.keys(): + if "is_active" in values: self.virtual_sdcard_update[str, bool].emit("is_active", values["is_active"]) - if "file_position" in values.keys(): + if "file_position" in values: self.virtual_sdcard_update[str, float].emit( "file_position", float(values["file_position"]) ) @@ -508,6 +523,8 @@ def send_print_event(self, event: str): Raises: TypeError: Thrown when QApplication is None """ + if not event: + return _print_state_upper = event[0].upper() _print_state_call = f"{_print_state_upper}{event[1:]}" if hasattr(events, f"Print{_print_state_call}"): @@ -516,15 +533,16 @@ def send_print_event(self, event: str): _print_state_call, f"Print{_print_state_call}", ) - _event_callback: QtCore.QEvent = getattr( - events, f"Print{_print_state_call}" - ) + _event_callback = getattr(events, f"Print{_print_state_call}") if callable(_event_callback): try: instance = QtWidgets.QApplication.instance() - if instance: - instance.postEvent(self.window(), _event_callback) - else: + # Printer is a QObject, not QWidget — use parent() + # to reach the MainWindow (which has the event handler). + target = self.parent() + if instance and target: + instance.postEvent(target, _event_callback()) + elif not instance: raise TypeError("QApplication.instance expected non None value") except Exception as e: logger.info( @@ -534,24 +552,24 @@ def send_print_event(self, event: str): def _print_stats_object_updated( self, values: dict, name: str = "print_stats" ) -> None: - if "filename" in values.keys(): + if "filename" in values: self.print_stats_update[str, str].emit("filename", values["filename"]) self.print_file_loaded = True - if "total_duration" in values.keys(): + if "total_duration" in values: self.print_stats_update[str, float].emit( "total_duration", values["total_duration"] ) - if "print_duration" in values.keys(): + if "print_duration" in values: self.print_stats_update[str, float].emit( "print_duration", values["print_duration"] ) - if "filament_used" in values.keys(): + if "filament_used" in values: self.print_stats_update[str, float].emit( "filament_used", values["filament_used"] ) - if "state" in values.keys(): + if "state" in values: self.print_stats_update[str, str].emit("state", values["state"]) - self.printing_state = values.get("state", None) + self.printing_state = values.get("state") or "" if not self.printing_state: return self.send_print_event(self.printing_state) @@ -562,33 +580,33 @@ def _print_stats_object_updated( self.print_file_loaded = True if values["state"] == "printing" or values["state"] == "pause": self.printing = True - if "message" in values.keys(): + if "message" in values: self.print_stats_update[str, str].emit("message", values["message"]) - if "info" in values.keys(): + if "info" in values: self.print_stats_update[str, dict].emit("info", values["info"]) def _display_status_object_updated( self, values: dict, name: str = "display_status" ) -> None: - if "message" in values.keys(): + if "message" in values: self.display_update[str, str].emit("message", values["message"]) - if "progress" in values.keys(): + if "progress" in values: self.display_update[str, float].emit("progress", values["progress"]) def _temperature_sensor_object_updated( self, values: dict, temperature_sensor_name: str ) -> None: - if "temperature" in values.keys(): + if "temperature" in values: self.temperature_sensor_update.emit( temperature_sensor_name, "temperature", values["temperature"] ) - if "measured_min_temp" in values.keys(): + if "measured_min_temp" in values: self.temperature_sensor_update.emit( temperature_sensor_name, "measured_min_temp", values["measured_min_temp"], ) - if "measured_max_temp" in values.keys(): + if "measured_max_temp" in values: self.temperature_sensor_update.emit( temperature_sensor_name, "measured_max_temp", @@ -600,19 +618,19 @@ def _temperature_fan_object_updated( ) -> None: _names = ["temperature_fan", temperature_fan_name] object_name = " ".join(_names) - if "speed" in values.keys(): + if "speed" in values: self.temperature_fan_update.emit( object_name, "speed", values["speed"], ) - if "temperature" in values.keys(): + if "temperature" in values: self.temperature_fan_update.emit( object_name, "temperature", values["temperature"], ) - if "target" in values.keys(): + if "target" in values: self.temperature_fan_update.emit( object_name, "target", @@ -622,14 +640,14 @@ def _temperature_fan_object_updated( def _filament_switch_sensor_object_updated( self, values: dict, filament_switch_name: str ) -> None: - if "filament_detected" in values.keys(): + if "filament_detected" in values: self.filament_switch_sensor_update.emit( filament_switch_name, "filament_detected", values["filament_detected"], ) self.available_filament_sensors.update({f"{filament_switch_name}": values}) - if "enabled" in values.keys(): + if "enabled" in values: self.filament_switch_sensor_update.emit( filament_switch_name, "enabled", values["enabled"] ) @@ -638,7 +656,7 @@ def _filament_switch_sensor_object_updated( def _filament_motion_sensor_object_updated( self, values: dict, filament_motion_name: str ) -> None: - if "filament_detected" in values.keys(): + if "filament_detected" in values: self.filament_motion_sensor_update.emit( filament_motion_name, "filament_detected", @@ -648,18 +666,18 @@ def _filament_motion_sensor_object_updated( {f"{filament_motion_name}": values["filament_detected"]} ) - if "enabled" in values.keys(): + if "enabled" in values: self.filament_motion_sensor_update.emit( filament_motion_name, "enabled", values["enabled"] ) self.available_filament_sensors.update({f"{filament_motion_name}": values}) def _cutter_sensor_object_updated(self, values: dict, cutter_name: str) -> None: - if "filament_detected" in values.keys(): + if "filament_detected" in values: self.filament_switch_sensor_update.emit( cutter_name, "filament_detected", values["filament_detected"] ) - if "enabled" in values.keys(): + if "enabled" in values: self.filament_switch_sensor_update.emit( cutter_name, "enabled", values["enabled"] ) @@ -667,7 +685,7 @@ def _cutter_sensor_object_updated(self, values: dict, cutter_name: str) -> None: self.available_filament_sensors.update({f"{cutter_name}": values}) def _output_pin_object_updated(self, values: dict, output_pin_name: str) -> None: - if "value" in values.keys(): + if "value" in values: self.output_pin_update.emit(output_pin_name, "value", values["value"]) def _bed_mesh_object_updated(self, values: dict, name: str = "bed_mesh") -> None: @@ -684,17 +702,17 @@ def _configfile_object_updated( self, values: dict, name: str = "configfile" ) -> None: self.configfile.update(values) - if "config" in values.keys(): + if "config" in values: self.printer_config.emit(values["config"]) - if "settings" in values.keys(): + if "settings" in values: # TODO ... - if "save_config_pending" in values.keys(): + if "save_config_pending" in values: self.save_config_pending.emit() - if "save_config_pending_items" in values.keys(): + if "save_config_pending_items" in values: # TODO ... - if "warnings" in values.keys(): + if "warnings" in values: # TODO ... @@ -734,9 +752,9 @@ def _temperature_probe_object_updated(self, values: dict, name: str) -> None: # TODO: testing needed here idk if does work def _unload_filament_object_updated(self, values: dict, name: str) -> None: - if "state" in values.keys(): + if "state" in values: self.unload_filament_update[bool].emit(values["state"]) def _load_filament_object_updated(self, values: dict, name: str) -> None: - if "state" in values.keys(): + if "state" in values: self.load_filament_update[bool].emit(values["state"]) diff --git a/BlocksScreen/lib/qrcode_gen.py b/BlocksScreen/lib/qrcode_gen.py index 1840f0ae..1caab81a 100644 --- a/BlocksScreen/lib/qrcode_gen.py +++ b/BlocksScreen/lib/qrcode_gen.py @@ -1,7 +1,6 @@ import qrcode - -from PyQt6.QtGui import QImage, QColor, QPainter from PyQt6.QtCore import Qt +from PyQt6.QtGui import QColor, QImage, QPainter BLOCKS_URL = "https://blockstec.com" RF50_MANUAL_PAGE = "https://blockstec.com/RF50" diff --git a/BlocksScreen/lib/ui/resources/icon_resources_rc.py b/BlocksScreen/lib/ui/resources/icon_resources_rc.py index 45c93d8f..c22059f4 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources_rc.py +++ b/BlocksScreen/lib/ui/resources/icon_resources_rc.py @@ -2,7 +2,7 @@ # Resource object code # -# Created by: The Resource Compiler for PyQt5 (Qt v5.15.14) +# Created by: The Resource Compiler for PyQt5 (Qt v5.15.15) # # WARNING! All changes made in this file will be lost! @@ -11519,151 +11519,165 @@ \x37\x30\x2e\x30\x31\x20\x33\x34\x30\x2e\x35\x37\x20\x32\x32\x34\ \x2e\x38\x31\x20\x33\x38\x35\x2e\x37\x37\x22\x2f\x3e\x3c\x2f\x73\ \x76\x67\x3e\ -\x00\x00\x08\xde\ +\x00\x00\x09\xb9\ \x3c\ -\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x31\x22\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\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ -\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ -\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ -\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x65\x30\x65\x30\x64\x66\x3b\x7d\ -\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x31\x22\x20\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x33\x34\x2e\x38\ -\x38\x71\x2d\x38\x39\x2e\x39\x31\x2c\x30\x2d\x31\x37\x39\x2e\x38\ -\x34\x2c\x30\x61\x35\x35\x2c\x35\x35\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x35\x35\x2e\x31\x2d\x35\x35\x2e\x30\x36\x71\x2e\x31\x34\x2d\x31\ -\x37\x39\x2e\x34\x39\x2c\x30\x2d\x33\x35\x39\x61\x35\x35\x2e\x37\ -\x34\x2c\x35\x35\x2e\x37\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x35\ -\x2e\x37\x39\x2d\x35\x35\x2e\x37\x38\x71\x31\x37\x38\x2c\x2e\x31\ -\x34\x2c\x33\x35\x35\x2e\x39\x33\x2c\x30\x61\x35\x38\x2e\x31\x32\ -\x2c\x35\x38\x2e\x31\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x38\x2e\ -\x31\x36\x2c\x35\x38\x2e\x31\x37\x71\x2d\x2e\x31\x33\x2c\x31\x37\ -\x36\x2e\x39\x34\x2c\x30\x2c\x33\x35\x33\x2e\x38\x39\x61\x35\x37\ -\x2e\x37\x33\x2c\x35\x37\x2e\x37\x33\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x35\x37\x2e\x38\x33\x2c\x35\x37\x2e\x37\x37\x51\x33\x38\x38\x2e\ -\x35\x35\x2c\x35\x33\x34\x2e\x38\x32\x2c\x33\x30\x30\x2c\x35\x33\ -\x34\x2e\x38\x38\x5a\x4d\x34\x39\x37\x2e\x33\x35\x2c\x33\x30\x30\ -\x2e\x33\x37\x63\x2e\x30\x36\x2d\x31\x30\x39\x2e\x38\x38\x2d\x38\ -\x38\x2e\x38\x31\x2d\x31\x39\x39\x2e\x36\x34\x2d\x31\x39\x37\x2e\ -\x37\x37\x2d\x31\x39\x39\x2e\x37\x36\x43\x31\x38\x35\x2e\x37\x35\ -\x2c\x31\x30\x30\x2e\x34\x38\x2c\x31\x30\x31\x2e\x37\x34\x2c\x31\ -\x39\x31\x2e\x37\x35\x2c\x39\x39\x2c\x32\x39\x34\x2e\x34\x31\x63\ -\x2d\x32\x2e\x38\x39\x2c\x31\x30\x39\x2e\x37\x34\x2c\x38\x34\x2e\ -\x30\x37\x2c\x32\x30\x33\x2c\x31\x39\x35\x2e\x38\x39\x2c\x32\x30\ -\x34\x2e\x37\x31\x43\x34\x30\x39\x2c\x35\x30\x30\x2e\x38\x33\x2c\ -\x34\x39\x37\x2e\x35\x34\x2c\x34\x30\x38\x2e\x36\x35\x2c\x34\x39\ -\x37\x2e\x33\x35\x2c\x33\x30\x30\x2e\x33\x37\x5a\x6d\x30\x2c\x31\ -\x37\x37\x2e\x38\x38\x61\x32\x30\x2e\x34\x37\x2c\x32\x30\x2e\x34\ -\x37\x2c\x30\x2c\x30\x2c\x30\x2d\x32\x30\x2e\x37\x32\x2d\x32\x30\ -\x2e\x37\x37\x2c\x32\x30\x2e\x38\x37\x2c\x32\x30\x2e\x38\x37\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x32\x31\x2e\x31\x38\x2c\x32\x30\x2e\x34\ -\x36\x63\x2d\x2e\x32\x33\x2c\x31\x31\x2e\x34\x34\x2c\x39\x2e\x36\ -\x36\x2c\x32\x31\x2e\x33\x34\x2c\x32\x31\x2e\x32\x34\x2c\x32\x31\ -\x2e\x32\x36\x41\x32\x30\x2e\x38\x36\x2c\x32\x30\x2e\x38\x36\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x34\x39\x37\x2e\x33\x34\x2c\x34\x37\x38\ -\x2e\x32\x35\x5a\x4d\x39\x39\x2e\x32\x33\x2c\x31\x32\x31\x2e\x33\ -\x33\x63\x2d\x2e\x30\x36\x2c\x31\x32\x2c\x38\x2e\x35\x31\x2c\x32\ -\x30\x2e\x38\x39\x2c\x32\x30\x2e\x32\x35\x2c\x32\x31\x2e\x31\x61\ -\x32\x30\x2e\x36\x37\x2c\x32\x30\x2e\x36\x37\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x32\x31\x2e\x33\x33\x2d\x32\x30\x2e\x38\x38\x41\x32\x31\ -\x2e\x31\x37\x2c\x32\x31\x2e\x31\x37\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x31\x32\x30\x2c\x31\x30\x30\x2e\x36\x31\x2c\x32\x30\x2e\x36\x34\ -\x2c\x32\x30\x2e\x36\x34\x2c\x30\x2c\x30\x2c\x30\x2c\x39\x39\x2e\ -\x32\x33\x2c\x31\x32\x31\x2e\x33\x33\x5a\x6d\x33\x39\x38\x2e\x31\ -\x31\x2e\x32\x33\x61\x32\x30\x2e\x39\x32\x2c\x32\x30\x2e\x39\x32\ -\x2c\x30\x2c\x30\x2c\x30\x2d\x32\x30\x2e\x37\x34\x2d\x32\x31\x2c\ -\x32\x31\x2e\x33\x38\x2c\x32\x31\x2e\x33\x38\x2c\x30\x2c\x30\x2c\ -\x30\x2d\x32\x31\x2e\x31\x38\x2c\x32\x30\x2e\x37\x33\x63\x2d\x2e\ -\x32\x31\x2c\x31\x31\x2e\x35\x32\x2c\x39\x2e\x35\x31\x2c\x32\x31\ -\x2e\x31\x38\x2c\x32\x31\x2e\x32\x36\x2c\x32\x31\x2e\x31\x33\x41\ -\x32\x30\x2e\x36\x38\x2c\x32\x30\x2e\x36\x38\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x34\x39\x37\x2e\x33\x34\x2c\x31\x32\x31\x2e\x35\x36\x5a\ -\x4d\x31\x32\x30\x2c\x34\x35\x37\x2e\x34\x38\x63\x2d\x31\x31\x2e\ -\x38\x38\x2d\x2e\x30\x36\x2d\x32\x30\x2e\x37\x32\x2c\x38\x2e\x36\ -\x36\x2d\x32\x30\x2e\x37\x38\x2c\x32\x30\x2e\x35\x31\x2d\x2e\x30\ -\x36\x2c\x31\x32\x2e\x31\x33\x2c\x39\x2c\x32\x31\x2e\x33\x33\x2c\ -\x32\x30\x2e\x38\x39\x2c\x32\x31\x2e\x32\x31\x61\x32\x31\x2c\x32\ -\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x30\x2e\x36\x39\x2d\x32\x31\ -\x41\x32\x30\x2e\x36\x31\x2c\x32\x30\x2e\x36\x31\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x31\x32\x30\x2c\x34\x35\x37\x2e\x34\x38\x5a\x22\x2f\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x30\x36\x2e\x37\x33\x2c\ -\x32\x31\x33\x2e\x36\x32\x63\x2d\x32\x31\x2e\x35\x38\x2d\x32\x2e\ -\x31\x38\x2d\x34\x31\x2e\x34\x35\x2e\x38\x33\x2d\x36\x30\x2e\x33\ -\x32\x2c\x39\x2e\x36\x37\x61\x38\x34\x2e\x31\x37\x2c\x38\x34\x2e\ -\x31\x37\x2c\x30\x2c\x30\x2c\x30\x2d\x33\x37\x2e\x39\x2c\x33\x34\ -\x2e\x32\x32\x63\x2d\x32\x2e\x39\x31\x2c\x35\x2d\x32\x2e\x39\x35\ -\x2c\x38\x2e\x33\x36\x2c\x31\x2e\x36\x32\x2c\x31\x32\x2e\x36\x2c\ -\x39\x2e\x37\x33\x2c\x39\x2e\x30\x35\x2c\x39\x2e\x33\x38\x2c\x39\ -\x2e\x30\x39\x2c\x32\x31\x2e\x33\x31\x2c\x34\x2e\x34\x31\x2c\x33\ -\x36\x2e\x35\x34\x2d\x31\x34\x2e\x33\x36\x2c\x36\x38\x2e\x32\x38\ -\x2d\x34\x2e\x31\x38\x2c\x39\x38\x2e\x33\x32\x2c\x31\x38\x2e\x33\ -\x32\x2c\x31\x30\x2e\x38\x38\x2c\x38\x2e\x31\x36\x2c\x31\x30\x2e\ -\x32\x32\x2c\x31\x37\x2e\x35\x31\x2c\x38\x2e\x36\x31\x2c\x32\x38\ -\x2e\x35\x31\x51\x34\x33\x30\x2e\x35\x34\x2c\x33\x37\x35\x2c\x33\ -\x38\x39\x2e\x32\x33\x2c\x34\x30\x39\x2e\x39\x61\x38\x2e\x30\x38\ -\x2c\x38\x2e\x30\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2c\x31\x2e\ -\x30\x37\x63\x2d\x2e\x31\x38\x2e\x30\x38\x2d\x2e\x34\x39\x2d\x2e\ -\x31\x33\x2d\x31\x2e\x31\x32\x2d\x2e\x33\x32\x2c\x32\x2e\x31\x37\ -\x2d\x32\x30\x2e\x36\x39\x2e\x31\x32\x2d\x34\x31\x2d\x38\x2e\x35\ -\x36\x2d\x36\x30\x2e\x31\x39\x2d\x37\x2e\x36\x36\x2d\x31\x36\x2e\ -\x39\x34\x2d\x31\x39\x2e\x33\x33\x2d\x33\x30\x2e\x33\x36\x2d\x33\ -\x35\x2e\x36\x31\x2d\x33\x39\x2e\x36\x36\x2d\x33\x2e\x36\x36\x2d\ -\x32\x2e\x30\x39\x2d\x35\x2e\x37\x39\x2d\x31\x2e\x36\x32\x2d\x39\ -\x2e\x33\x2c\x31\x2e\x32\x32\x2d\x39\x2e\x31\x36\x2c\x37\x2e\x34\ -\x31\x2d\x31\x30\x2e\x31\x35\x2c\x31\x33\x2e\x38\x31\x2d\x35\x2e\ -\x34\x2c\x32\x35\x2e\x36\x32\x2c\x31\x33\x2e\x34\x37\x2c\x33\x33\ -\x2e\x34\x37\x2c\x32\x2e\x32\x33\x2c\x36\x33\x2e\x34\x34\x2d\x31\ -\x38\x2c\x39\x31\x2e\x31\x34\x2d\x37\x2e\x37\x2c\x31\x30\x2e\x35\ -\x35\x2d\x31\x36\x2e\x32\x36\x2c\x31\x34\x2e\x38\x32\x2d\x33\x30\ -\x2e\x31\x31\x2c\x31\x32\x2e\x33\x34\x2d\x33\x33\x2e\x39\x31\x2d\ -\x36\x2e\x30\x39\x2d\x36\x32\x2e\x31\x36\x2d\x32\x31\x2e\x33\x32\ -\x2d\x38\x35\x2d\x34\x36\x2e\x39\x61\x31\x37\x2e\x37\x38\x2c\x31\ -\x37\x2e\x37\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x31\x2d\ -\x32\x2e\x38\x34\x2c\x31\x31\x37\x2e\x36\x39\x2c\x31\x31\x37\x2e\ -\x36\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x33\x2e\x36\x36\x2d\x33\ -\x2e\x36\x35\x63\x32\x33\x2e\x33\x2d\x36\x2e\x34\x36\x2c\x34\x32\ -\x2d\x31\x39\x2e\x32\x2c\x35\x34\x2e\x36\x36\x2d\x34\x30\x2e\x32\ -\x39\x2c\x33\x2e\x31\x35\x2d\x35\x2e\x32\x32\x2c\x32\x2e\x36\x37\ -\x2d\x38\x2e\x35\x37\x2d\x31\x2e\x35\x36\x2d\x31\x32\x2e\x37\x31\ -\x2d\x39\x2e\x35\x39\x2d\x39\x2e\x34\x32\x2d\x39\x2e\x33\x37\x2d\ -\x39\x2e\x34\x33\x2d\x32\x32\x2d\x34\x2e\x33\x37\x2d\x33\x34\x2e\ -\x37\x33\x2c\x31\x33\x2e\x38\x39\x2d\x36\x35\x2e\x32\x2c\x34\x2e\ -\x32\x31\x2d\x39\x34\x2e\x35\x35\x2d\x31\x35\x2e\x39\x35\x2d\x31\ -\x33\x2e\x35\x2d\x39\x2e\x32\x36\x2d\x31\x33\x2e\x36\x2d\x32\x30\ -\x2e\x34\x38\x2d\x31\x31\x2e\x32\x31\x2d\x33\x34\x2e\x32\x35\x2c\ -\x35\x2e\x39\x34\x2d\x33\x34\x2e\x32\x2c\x32\x31\x2e\x39\x31\x2d\ -\x36\x32\x2e\x34\x35\x2c\x34\x38\x2e\x32\x33\x2d\x38\x35\x2c\x2e\ -\x37\x2d\x2e\x35\x39\x2c\x31\x2e\x35\x2d\x31\x2e\x30\x35\x2c\x33\ -\x2d\x32\x2e\x31\x31\x2e\x38\x31\x2c\x31\x33\x2d\x2e\x37\x34\x2c\ -\x32\x35\x2e\x31\x31\x2c\x31\x2e\x36\x35\x2c\x33\x37\x2e\x30\x37\ -\x2c\x35\x2e\x33\x34\x2c\x32\x36\x2e\x38\x32\x2c\x31\x38\x2c\x34\ -\x38\x2e\x35\x36\x2c\x34\x31\x2e\x38\x33\x2c\x36\x33\x2e\x31\x36\ -\x2c\x34\x2e\x34\x2c\x32\x2e\x37\x2c\x37\x2e\x32\x35\x2c\x33\x2c\ -\x31\x31\x2e\x31\x37\x2d\x31\x2e\x31\x35\x2c\x39\x2e\x37\x39\x2d\ -\x31\x30\x2e\x32\x36\x2c\x39\x2e\x37\x2d\x39\x2e\x39\x2c\x34\x2e\ -\x38\x2d\x32\x33\x2e\x33\x34\x2d\x31\x32\x2e\x33\x32\x2d\x33\x33\ -\x2e\x37\x39\x2d\x33\x2e\x31\x37\x2d\x36\x33\x2e\x36\x36\x2c\x31\ -\x37\x2d\x39\x31\x2e\x36\x31\x2c\x38\x2e\x30\x38\x2d\x31\x31\x2e\ -\x32\x31\x2c\x31\x36\x2e\x38\x32\x2d\x31\x36\x2e\x33\x39\x2c\x33\ -\x31\x2e\x38\x36\x2d\x31\x33\x2e\x35\x31\x2c\x33\x33\x2e\x35\x33\ -\x2c\x36\x2e\x34\x31\x2c\x36\x31\x2e\x35\x2c\x32\x31\x2e\x35\x33\ -\x2c\x38\x34\x2e\x32\x36\x2c\x34\x36\x2e\x37\x43\x34\x30\x35\x2e\ -\x39\x32\x2c\x32\x31\x30\x2e\x39\x33\x2c\x34\x30\x36\x2c\x32\x31\ -\x31\x2e\x37\x37\x2c\x34\x30\x36\x2e\x37\x33\x2c\x32\x31\x33\x2e\ -\x36\x32\x5a\x4d\x32\x39\x39\x2e\x36\x33\x2c\x33\x31\x39\x2e\x37\ -\x39\x63\x33\x2e\x37\x39\x2e\x31\x31\x2c\x31\x37\x2e\x32\x35\x2d\ -\x31\x33\x2e\x31\x36\x2c\x31\x37\x2e\x34\x37\x2d\x31\x37\x2e\x32\ -\x31\x73\x2d\x31\x32\x2e\x36\x35\x2d\x31\x37\x2e\x32\x39\x2d\x31\ -\x37\x2d\x31\x37\x2e\x35\x32\x63\x2d\x33\x2e\x37\x31\x2d\x2e\x32\ -\x2d\x31\x37\x2e\x34\x33\x2c\x31\x33\x2e\x31\x37\x2d\x31\x37\x2e\ -\x35\x35\x2c\x31\x37\x2e\x31\x53\x32\x39\x35\x2e\x35\x32\x2c\x33\ -\x31\x39\x2e\x36\x37\x2c\x32\x39\x39\x2e\x36\x33\x2c\x33\x31\x39\ -\x2e\x37\x39\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x05\xdd\ +\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\x3f\x3e\x0a\x3c\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\ +\x61\x79\x65\x72\x5f\x31\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\ +\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\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\x20\x76\ +\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x36\x30\x30\x20\ +\x36\x30\x30\x22\x3e\x0a\x20\x20\x3c\x64\x65\x66\x73\x3e\x0a\x20\ +\x20\x20\x20\x3c\x73\x74\x79\x6c\x65\x3e\x0a\x20\x20\x20\x20\x20\ +\x20\x2e\x63\x6c\x73\x2d\x31\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x66\x69\x6c\x6c\x3a\x20\x23\x65\x30\x65\x30\x64\x66\x3b\ +\x0a\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x73\ +\x74\x79\x6c\x65\x3e\x0a\x20\x20\x3c\x2f\x64\x65\x66\x73\x3e\x0a\ +\x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ +\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x30\ +\x35\x2e\x35\x34\x63\x2d\x35\x32\x2e\x34\x36\x2c\x30\x2d\x31\x30\ +\x34\x2e\x39\x31\x2d\x2e\x30\x32\x2d\x31\x35\x37\x2e\x33\x37\x2e\ +\x30\x34\x2d\x32\x36\x2e\x36\x34\x2e\x30\x33\x2d\x34\x38\x2e\x32\ +\x34\x2d\x32\x31\x2e\x35\x34\x2d\x34\x38\x2e\x32\x32\x2d\x34\x38\ +\x2e\x31\x38\x2e\x30\x38\x2d\x31\x30\x34\x2e\x37\x32\x2e\x30\x38\ +\x2d\x32\x30\x39\x2e\x34\x34\x2c\x30\x2d\x33\x31\x34\x2e\x31\x35\ +\x2d\x2e\x30\x32\x2d\x32\x36\x2e\x39\x37\x2c\x32\x31\x2e\x38\x34\ +\x2d\x34\x38\x2e\x38\x33\x2c\x34\x38\x2e\x38\x31\x2d\x34\x38\x2e\ +\x38\x31\x2c\x31\x30\x33\x2e\x38\x32\x2e\x30\x38\x2c\x32\x30\x37\ +\x2e\x36\x35\x2e\x30\x38\x2c\x33\x31\x31\x2e\x34\x37\x2c\x30\x2c\ +\x32\x38\x2e\x31\x32\x2d\x2e\x30\x32\x2c\x35\x30\x2e\x39\x32\x2c\ +\x32\x32\x2e\x37\x37\x2c\x35\x30\x2e\x39\x2c\x35\x30\x2e\x39\x2d\ +\x2e\x30\x37\x2c\x31\x30\x33\x2e\x32\x33\x2d\x2e\x30\x37\x2c\x32\ +\x30\x36\x2e\x34\x36\x2c\x30\x2c\x33\x30\x39\x2e\x36\x38\x2e\x30\ +\x32\x2c\x32\x37\x2e\x39\x36\x2d\x32\x32\x2e\x36\x35\x2c\x35\x30\ +\x2e\x35\x39\x2d\x35\x30\x2e\x36\x2c\x35\x30\x2e\x35\x36\x2d\x35\ +\x31\x2e\x36\x36\x2d\x2e\x30\x36\x2d\x31\x30\x33\x2e\x33\x32\x2d\ +\x2e\x30\x33\x2d\x31\x35\x34\x2e\x39\x39\x2d\x2e\x30\x33\x5a\x4d\ +\x34\x37\x32\x2e\x37\x2c\x33\x30\x30\x2e\x33\x33\x63\x2e\x30\x35\ +\x2d\x39\x36\x2e\x31\x36\x2d\x37\x37\x2e\x37\x31\x2d\x31\x37\x34\ +\x2e\x37\x31\x2d\x31\x37\x33\x2e\x30\x36\x2d\x31\x37\x34\x2e\x38\ +\x31\x2d\x39\x39\x2e\x36\x32\x2d\x2e\x31\x31\x2d\x31\x37\x33\x2e\ +\x31\x33\x2c\x37\x39\x2e\x37\x36\x2d\x31\x37\x35\x2e\x35\x2c\x31\ +\x36\x39\x2e\x35\x39\x2d\x32\x2e\x35\x33\x2c\x39\x36\x2e\x30\x33\ +\x2c\x37\x33\x2e\x35\x37\x2c\x31\x37\x37\x2e\x36\x37\x2c\x31\x37\ +\x31\x2e\x34\x32\x2c\x31\x37\x39\x2e\x31\x34\x2c\x39\x39\x2e\x38\ +\x36\x2c\x31\x2e\x35\x2c\x31\x37\x37\x2e\x33\x31\x2d\x37\x39\x2e\ +\x31\x36\x2c\x31\x37\x37\x2e\x31\x35\x2d\x31\x37\x33\x2e\x39\x32\ +\x5a\x4d\x34\x37\x32\x2e\x36\x39\x2c\x34\x35\x35\x2e\x39\x38\x63\ +\x30\x2d\x31\x30\x2e\x32\x33\x2d\x37\x2e\x38\x37\x2d\x31\x38\x2e\ +\x31\x31\x2d\x31\x38\x2e\x31\x34\x2d\x31\x38\x2e\x31\x37\x2d\x31\ +\x30\x2e\x31\x35\x2d\x2e\x30\x36\x2d\x31\x38\x2e\x33\x33\x2c\x37\ +\x2e\x38\x35\x2d\x31\x38\x2e\x35\x33\x2c\x31\x37\x2e\x39\x2d\x2e\ +\x32\x2c\x31\x30\x2c\x38\x2e\x34\x36\x2c\x31\x38\x2e\x36\x37\x2c\ +\x31\x38\x2e\x35\x39\x2c\x31\x38\x2e\x36\x2c\x31\x30\x2e\x30\x32\ +\x2d\x2e\x30\x37\x2c\x31\x38\x2e\x30\x38\x2d\x38\x2e\x32\x34\x2c\ +\x31\x38\x2e\x30\x38\x2d\x31\x38\x2e\x33\x33\x5a\x4d\x31\x32\x34\ +\x2e\x33\x31\x2c\x31\x34\x33\x2e\x36\x35\x63\x2d\x2e\x30\x35\x2c\ +\x31\x30\x2e\x34\x38\x2c\x37\x2e\x34\x34\x2c\x31\x38\x2e\x32\x38\ +\x2c\x31\x37\x2e\x37\x32\x2c\x31\x38\x2e\x34\x36\x2c\x31\x30\x2e\ +\x34\x33\x2e\x31\x38\x2c\x31\x38\x2e\x36\x36\x2d\x37\x2e\x38\x37\ +\x2c\x31\x38\x2e\x36\x36\x2d\x31\x38\x2e\x32\x37\x2c\x30\x2d\x39\ +\x2e\x39\x31\x2d\x38\x2e\x32\x38\x2d\x31\x38\x2e\x32\x35\x2d\x31\ +\x38\x2e\x32\x2d\x31\x38\x2e\x33\x32\x2d\x31\x30\x2e\x30\x34\x2d\ +\x2e\x30\x38\x2d\x31\x38\x2e\x31\x33\x2c\x38\x2d\x31\x38\x2e\x31\ +\x38\x2c\x31\x38\x2e\x31\x34\x5a\x4d\x34\x37\x32\x2e\x36\x39\x2c\ +\x31\x34\x33\x2e\x38\x35\x63\x2d\x2e\x30\x32\x2d\x31\x30\x2e\x30\ +\x39\x2d\x38\x2e\x31\x35\x2d\x31\x38\x2e\x33\x31\x2d\x31\x38\x2e\ +\x31\x35\x2d\x31\x38\x2e\x33\x34\x2d\x39\x2e\x39\x33\x2d\x2e\x30\ +\x33\x2d\x31\x38\x2e\x33\x35\x2c\x38\x2e\x32\x32\x2d\x31\x38\x2e\ +\x35\x33\x2c\x31\x38\x2e\x31\x34\x2d\x2e\x31\x38\x2c\x31\x30\x2e\ +\x30\x38\x2c\x38\x2e\x33\x32\x2c\x31\x38\x2e\x35\x34\x2c\x31\x38\ +\x2e\x36\x31\x2c\x31\x38\x2e\x34\x39\x2c\x31\x30\x2e\x30\x39\x2d\ +\x2e\x30\x34\x2c\x31\x38\x2e\x31\x2d\x38\x2e\x31\x34\x2c\x31\x38\ +\x2e\x30\x38\x2d\x31\x38\x2e\x33\x5a\x4d\x31\x34\x32\x2e\x35\x2c\ +\x34\x33\x37\x2e\x38\x31\x63\x2d\x31\x30\x2e\x34\x2d\x2e\x30\x36\ +\x2d\x31\x38\x2e\x31\x33\x2c\x37\x2e\x35\x37\x2d\x31\x38\x2e\x31\ +\x39\x2c\x31\x37\x2e\x39\x34\x2d\x2e\x30\x35\x2c\x31\x30\x2e\x36\ +\x32\x2c\x37\x2e\x38\x38\x2c\x31\x38\x2e\x36\x37\x2c\x31\x38\x2e\ +\x32\x38\x2c\x31\x38\x2e\x35\x36\x2c\x31\x30\x2d\x2e\x31\x2c\x31\ +\x38\x2e\x31\x32\x2d\x38\x2e\x33\x34\x2c\x31\x38\x2e\x31\x2d\x31\ +\x38\x2e\x33\x37\x2d\x2e\x30\x31\x2d\x31\x30\x2e\x31\x34\x2d\x37\ +\x2e\x39\x38\x2d\x31\x38\x2e\x30\x37\x2d\x31\x38\x2e\x32\x2d\x31\ +\x38\x2e\x31\x33\x5a\x22\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\ +\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\ +\x3d\x22\x4d\x33\x39\x33\x2e\x34\x2c\x32\x32\x34\x2e\x34\x31\x63\ +\x2d\x31\x38\x2e\x38\x38\x2d\x31\x2e\x39\x31\x2d\x33\x36\x2e\x32\ +\x37\x2e\x37\x33\x2d\x35\x32\x2e\x37\x38\x2c\x38\x2e\x34\x36\x2d\ +\x31\x34\x2e\x30\x39\x2c\x36\x2e\x36\x2d\x32\x35\x2e\x32\x33\x2c\ +\x31\x36\x2e\x34\x34\x2d\x33\x33\x2e\x31\x37\x2c\x32\x39\x2e\x39\ +\x35\x2d\x32\x2e\x35\x35\x2c\x34\x2e\x33\x34\x2d\x32\x2e\x35\x38\ +\x2c\x37\x2e\x33\x31\x2c\x31\x2e\x34\x31\x2c\x31\x31\x2e\x30\x32\ +\x2c\x38\x2e\x35\x32\x2c\x37\x2e\x39\x32\x2c\x38\x2e\x32\x31\x2c\ +\x37\x2e\x39\x36\x2c\x31\x38\x2e\x36\x35\x2c\x33\x2e\x38\x36\x2c\ +\x33\x31\x2e\x39\x38\x2d\x31\x32\x2e\x35\x37\x2c\x35\x39\x2e\x37\ +\x36\x2d\x33\x2e\x36\x36\x2c\x38\x36\x2e\x30\x34\x2c\x31\x36\x2e\ +\x30\x34\x2c\x39\x2e\x35\x33\x2c\x37\x2e\x31\x34\x2c\x38\x2e\x39\ +\x34\x2c\x31\x35\x2e\x33\x32\x2c\x37\x2e\x35\x34\x2c\x32\x34\x2e\ +\x39\x35\x2d\x34\x2e\x35\x37\x2c\x33\x31\x2e\x33\x2d\x31\x38\x2e\ +\x39\x2c\x35\x37\x2e\x30\x38\x2d\x34\x33\x2e\x30\x31\x2c\x37\x37\ +\x2e\x34\x39\x2d\x2e\x34\x39\x2e\x34\x31\x2d\x31\x2e\x31\x32\x2e\ +\x36\x38\x2d\x31\x2e\x37\x31\x2e\x39\x34\x2d\x2e\x31\x36\x2e\x30\ +\x37\x2d\x2e\x34\x32\x2d\x2e\x31\x31\x2d\x2e\x39\x38\x2d\x2e\x32\ +\x39\x2c\x31\x2e\x39\x2d\x31\x38\x2e\x31\x2e\x31\x2d\x33\x35\x2e\ +\x38\x35\x2d\x37\x2e\x35\x2d\x35\x32\x2e\x36\x37\x2d\x36\x2e\x37\ +\x2d\x31\x34\x2e\x38\x33\x2d\x31\x36\x2e\x39\x31\x2d\x32\x36\x2e\ +\x35\x37\x2d\x33\x31\x2e\x31\x36\x2d\x33\x34\x2e\x37\x31\x2d\x33\ +\x2e\x32\x2d\x31\x2e\x38\x33\x2d\x35\x2e\x30\x37\x2d\x31\x2e\x34\ +\x32\x2d\x38\x2e\x31\x33\x2c\x31\x2e\x30\x37\x2d\x38\x2e\x30\x32\ +\x2c\x36\x2e\x34\x39\x2d\x38\x2e\x38\x38\x2c\x31\x32\x2e\x30\x39\ +\x2d\x34\x2e\x37\x33\x2c\x32\x32\x2e\x34\x32\x2c\x31\x31\x2e\x37\ +\x38\x2c\x32\x39\x2e\x32\x39\x2c\x31\x2e\x39\x35\x2c\x35\x35\x2e\ +\x35\x32\x2d\x31\x35\x2e\x37\x35\x2c\x37\x39\x2e\x37\x36\x2d\x36\ +\x2e\x37\x34\x2c\x39\x2e\x32\x33\x2d\x31\x34\x2e\x32\x33\x2c\x31\ +\x32\x2e\x39\x37\x2d\x32\x36\x2e\x33\x35\x2c\x31\x30\x2e\x37\x39\ +\x2d\x32\x39\x2e\x36\x38\x2d\x35\x2e\x33\x33\x2d\x35\x34\x2e\x33\ +\x39\x2d\x31\x38\x2e\x36\x35\x2d\x37\x34\x2e\x33\x39\x2d\x34\x31\ +\x2e\x30\x34\x2d\x2e\x34\x32\x2d\x2e\x34\x37\x2d\x2e\x36\x31\x2d\ +\x31\x2e\x31\x34\x2d\x31\x2e\x33\x32\x2d\x32\x2e\x34\x39\x2c\x31\ +\x33\x2e\x32\x38\x2c\x31\x2e\x33\x32\x2c\x32\x35\x2e\x38\x39\x2e\ +\x32\x33\x2c\x33\x38\x2e\x32\x2d\x33\x2e\x31\x39\x2c\x32\x30\x2e\ +\x33\x39\x2d\x35\x2e\x36\x36\x2c\x33\x36\x2e\x37\x32\x2d\x31\x36\ +\x2e\x38\x31\x2c\x34\x37\x2e\x38\x33\x2d\x33\x35\x2e\x32\x36\x2c\ +\x32\x2e\x37\x35\x2d\x34\x2e\x35\x37\x2c\x32\x2e\x33\x33\x2d\x37\ +\x2e\x35\x2d\x31\x2e\x33\x36\x2d\x31\x31\x2e\x31\x32\x2d\x38\x2e\ +\x34\x2d\x38\x2e\x32\x34\x2d\x38\x2e\x32\x2d\x38\x2e\x32\x36\x2d\ +\x31\x39\x2e\x32\x38\x2d\x33\x2e\x38\x33\x2d\x33\x30\x2e\x34\x2c\ +\x31\x32\x2e\x31\x36\x2d\x35\x37\x2e\x30\x35\x2c\x33\x2e\x36\x39\ +\x2d\x38\x32\x2e\x37\x34\x2d\x31\x33\x2e\x39\x35\x2d\x31\x31\x2e\ +\x38\x31\x2d\x38\x2e\x31\x31\x2d\x31\x31\x2e\x39\x2d\x31\x37\x2e\ +\x39\x32\x2d\x39\x2e\x38\x31\x2d\x32\x39\x2e\x39\x37\x2c\x35\x2e\ +\x32\x2d\x32\x39\x2e\x39\x33\x2c\x31\x39\x2e\x31\x37\x2d\x35\x34\ +\x2e\x36\x35\x2c\x34\x32\x2e\x32\x31\x2d\x37\x34\x2e\x33\x34\x2e\ +\x36\x31\x2d\x2e\x35\x32\x2c\x31\x2e\x33\x31\x2d\x2e\x39\x32\x2c\ +\x32\x2e\x36\x35\x2d\x31\x2e\x38\x34\x2e\x37\x31\x2c\x31\x31\x2e\ +\x33\x39\x2d\x2e\x36\x35\x2c\x32\x31\x2e\x39\x37\x2c\x31\x2e\x34\ +\x34\x2c\x33\x32\x2e\x34\x33\x2c\x34\x2e\x36\x38\x2c\x32\x33\x2e\ +\x34\x37\x2c\x31\x35\x2e\x37\x38\x2c\x34\x32\x2e\x35\x2c\x33\x36\ +\x2e\x36\x2c\x35\x35\x2e\x32\x37\x2c\x33\x2e\x38\x36\x2c\x32\x2e\ +\x33\x36\x2c\x36\x2e\x33\x35\x2c\x32\x2e\x36\x2c\x39\x2e\x37\x38\ +\x2d\x31\x2e\x30\x31\x2c\x38\x2e\x35\x36\x2d\x38\x2e\x39\x37\x2c\ +\x38\x2e\x34\x38\x2d\x38\x2e\x36\x36\x2c\x34\x2e\x32\x2d\x32\x30\ +\x2e\x34\x33\x2d\x31\x30\x2e\x37\x38\x2d\x32\x39\x2e\x35\x37\x2d\ +\x32\x2e\x37\x37\x2d\x35\x35\x2e\x37\x2c\x31\x34\x2e\x38\x37\x2d\ +\x38\x30\x2e\x31\x37\x2c\x37\x2e\x30\x37\x2d\x39\x2e\x38\x2c\x31\ +\x34\x2e\x37\x32\x2d\x31\x34\x2e\x33\x34\x2c\x32\x37\x2e\x38\x38\ +\x2d\x31\x31\x2e\x38\x32\x2c\x32\x39\x2e\x33\x34\x2c\x35\x2e\x36\ +\x31\x2c\x35\x33\x2e\x38\x32\x2c\x31\x38\x2e\x38\x34\x2c\x37\x33\ +\x2e\x37\x34\x2c\x34\x30\x2e\x38\x37\x2e\x34\x2e\x34\x35\x2e\x34\ +\x39\x2c\x31\x2e\x31\x38\x2c\x31\x2e\x31\x31\x2c\x32\x2e\x38\x5a\ +\x4d\x32\x39\x39\x2e\x36\x38\x2c\x33\x31\x37\x2e\x33\x32\x63\x33\ +\x2e\x33\x32\x2e\x30\x39\x2c\x31\x35\x2e\x31\x2d\x31\x31\x2e\x35\ +\x31\x2c\x31\x35\x2e\x32\x38\x2d\x31\x35\x2e\x30\x36\x2e\x31\x39\ +\x2d\x33\x2e\x35\x35\x2d\x31\x31\x2e\x30\x37\x2d\x31\x35\x2e\x31\ +\x33\x2d\x31\x34\x2e\x38\x39\x2d\x31\x35\x2e\x33\x33\x2d\x33\x2e\ +\x32\x35\x2d\x2e\x31\x37\x2d\x31\x35\x2e\x32\x35\x2c\x31\x31\x2e\ +\x35\x33\x2d\x31\x35\x2e\x33\x35\x2c\x31\x34\x2e\x39\x37\x2d\x2e\ +\x31\x2c\x33\x2e\x35\x2c\x31\x31\x2e\x33\x37\x2c\x31\x35\x2e\x33\ +\x33\x2c\x31\x34\x2e\x39\x36\x2c\x31\x35\x2e\x34\x33\x5a\x22\x2f\ +\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x05\xcf\ \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\ @@ -11680,86 +11694,85 @@ \x0a\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x73\ \x74\x79\x6c\x65\x3e\x0a\x20\x20\x3c\x2f\x64\x65\x66\x73\x3e\x0a\ \x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x38\x30\x2e\x30\x34\ -\x2c\x31\x35\x30\x2e\x34\x37\x63\x2d\x33\x36\x2e\x33\x35\x2d\x33\ -\x2e\x36\x37\x2d\x36\x39\x2e\x38\x34\x2c\x31\x2e\x34\x2d\x31\x30\ -\x31\x2e\x36\x32\x2c\x31\x36\x2e\x32\x39\x2d\x32\x37\x2e\x31\x33\ -\x2c\x31\x32\x2e\x37\x31\x2d\x34\x38\x2e\x35\x38\x2c\x33\x31\x2e\ -\x36\x34\x2d\x36\x33\x2e\x38\x36\x2c\x35\x37\x2e\x36\x36\x2d\x34\ -\x2e\x39\x31\x2c\x38\x2e\x33\x36\x2d\x34\x2e\x39\x36\x2c\x31\x34\ -\x2e\x30\x38\x2c\x32\x2e\x37\x32\x2c\x32\x31\x2e\x32\x32\x2c\x31\ -\x36\x2e\x34\x31\x2c\x31\x35\x2e\x32\x35\x2c\x31\x35\x2e\x38\x31\ -\x2c\x31\x35\x2e\x33\x32\x2c\x33\x35\x2e\x39\x2c\x37\x2e\x34\x33\ -\x2c\x36\x31\x2e\x35\x38\x2d\x32\x34\x2e\x31\x39\x2c\x31\x31\x35\ -\x2e\x30\x35\x2d\x37\x2e\x30\x34\x2c\x31\x36\x35\x2e\x36\x35\x2c\ -\x33\x30\x2e\x38\x38\x2c\x31\x38\x2e\x33\x34\x2c\x31\x33\x2e\x37\ -\x34\x2c\x31\x37\x2e\x32\x32\x2c\x32\x39\x2e\x34\x39\x2c\x31\x34\ -\x2e\x35\x31\x2c\x34\x38\x2e\x30\x33\x2d\x38\x2e\x38\x2c\x36\x30\ -\x2e\x32\x35\x2d\x33\x36\x2e\x34\x2c\x31\x30\x39\x2e\x39\x2d\x38\ -\x32\x2e\x38\x2c\x31\x34\x39\x2e\x31\x38\x2d\x2e\x39\x34\x2e\x38\ -\x2d\x32\x2e\x31\x36\x2c\x31\x2e\x33\x2d\x33\x2e\x33\x2c\x31\x2e\ -\x38\x31\x2d\x2e\x33\x2e\x31\x33\x2d\x2e\x38\x31\x2d\x2e\x32\x32\ -\x2d\x31\x2e\x38\x38\x2d\x2e\x35\x35\x2c\x33\x2e\x36\x35\x2d\x33\ -\x34\x2e\x38\x36\x2e\x31\x39\x2d\x36\x39\x2e\x30\x31\x2d\x31\x34\ -\x2e\x34\x33\x2d\x31\x30\x31\x2e\x34\x2d\x31\x32\x2e\x39\x2d\x32\ -\x38\x2e\x35\x35\x2d\x33\x32\x2e\x35\x36\x2d\x35\x31\x2e\x31\x36\ -\x2d\x35\x39\x2e\x39\x39\x2d\x36\x36\x2e\x38\x33\x2d\x36\x2e\x31\ -\x37\x2d\x33\x2e\x35\x32\x2d\x39\x2e\x37\x35\x2d\x32\x2e\x37\x32\ -\x2d\x31\x35\x2e\x36\x36\x2c\x32\x2e\x30\x35\x2d\x31\x35\x2e\x34\ -\x35\x2c\x31\x32\x2e\x35\x2d\x31\x37\x2e\x31\x2c\x32\x33\x2e\x32\ -\x37\x2d\x39\x2e\x31\x2c\x34\x33\x2e\x31\x37\x2c\x32\x32\x2e\x36\ -\x39\x2c\x35\x36\x2e\x33\x39\x2c\x33\x2e\x37\x35\x2c\x31\x30\x36\ -\x2e\x38\x39\x2d\x33\x30\x2e\x33\x32\x2c\x31\x35\x33\x2e\x35\x36\ -\x2d\x31\x32\x2e\x39\x38\x2c\x31\x37\x2e\x37\x38\x2d\x32\x37\x2e\ -\x34\x2c\x32\x34\x2e\x39\x37\x2d\x35\x30\x2e\x37\x32\x2c\x32\x30\ -\x2e\x37\x38\x2d\x35\x37\x2e\x31\x33\x2d\x31\x30\x2e\x32\x36\x2d\ -\x31\x30\x34\x2e\x37\x32\x2d\x33\x35\x2e\x39\x31\x2d\x31\x34\x33\ -\x2e\x32\x31\x2d\x37\x39\x2e\x30\x31\x2d\x2e\x38\x31\x2d\x2e\x39\ -\x2d\x31\x2e\x31\x38\x2d\x32\x2e\x31\x39\x2d\x32\x2e\x35\x33\x2d\ -\x34\x2e\x37\x39\x2c\x32\x35\x2e\x35\x37\x2c\x32\x2e\x35\x34\x2c\ -\x34\x39\x2e\x38\x35\x2e\x34\x33\x2c\x37\x33\x2e\x35\x35\x2d\x36\ -\x2e\x31\x34\x2c\x33\x39\x2e\x32\x35\x2d\x31\x30\x2e\x38\x39\x2c\ -\x37\x30\x2e\x37\x2d\x33\x32\x2e\x33\x36\x2c\x39\x32\x2e\x30\x39\ -\x2d\x36\x37\x2e\x38\x38\x2c\x35\x2e\x33\x2d\x38\x2e\x38\x2c\x34\ -\x2e\x34\x39\x2d\x31\x34\x2e\x34\x34\x2d\x32\x2e\x36\x32\x2d\x32\ -\x31\x2e\x34\x32\x2d\x31\x36\x2e\x31\x37\x2d\x31\x35\x2e\x38\x37\ -\x2d\x31\x35\x2e\x37\x39\x2d\x31\x35\x2e\x38\x39\x2d\x33\x37\x2e\ -\x31\x32\x2d\x37\x2e\x33\x37\x2d\x35\x38\x2e\x35\x32\x2c\x32\x33\ -\x2e\x34\x2d\x31\x30\x39\x2e\x38\x34\x2c\x37\x2e\x31\x2d\x31\x35\ -\x39\x2e\x33\x2d\x32\x36\x2e\x38\x36\x2d\x32\x32\x2e\x37\x33\x2d\ -\x31\x35\x2e\x36\x31\x2d\x32\x32\x2e\x39\x31\x2d\x33\x34\x2e\x35\ -\x2d\x31\x38\x2e\x38\x38\x2d\x35\x37\x2e\x37\x31\x2c\x31\x30\x2e\ -\x30\x31\x2d\x35\x37\x2e\x36\x32\x2c\x33\x36\x2e\x39\x31\x2d\x31\ -\x30\x35\x2e\x32\x32\x2c\x38\x31\x2e\x32\x37\x2d\x31\x34\x33\x2e\ -\x31\x32\x2c\x31\x2e\x31\x37\x2d\x31\x2c\x32\x2e\x35\x33\x2d\x31\ -\x2e\x37\x37\x2c\x35\x2e\x31\x2d\x33\x2e\x35\x35\x2c\x31\x2e\x33\ -\x36\x2c\x32\x31\x2e\x39\x33\x2d\x31\x2e\x32\x34\x2c\x34\x32\x2e\ -\x33\x2c\x32\x2e\x37\x37\x2c\x36\x32\x2e\x34\x35\x2c\x39\x2e\x30\ -\x31\x2c\x34\x35\x2e\x31\x39\x2c\x33\x30\x2e\x33\x37\x2c\x38\x31\ -\x2e\x38\x32\x2c\x37\x30\x2e\x34\x37\x2c\x31\x30\x36\x2e\x34\x31\ -\x2c\x37\x2e\x34\x32\x2c\x34\x2e\x35\x35\x2c\x31\x32\x2e\x32\x32\ -\x2c\x35\x2c\x31\x38\x2e\x38\x33\x2d\x31\x2e\x39\x34\x2c\x31\x36\ -\x2e\x34\x38\x2d\x31\x37\x2e\x32\x38\x2c\x31\x36\x2e\x33\x33\x2d\ -\x31\x36\x2e\x36\x38\x2c\x38\x2e\x30\x38\x2d\x33\x39\x2e\x33\x33\ -\x2d\x32\x30\x2e\x37\x35\x2d\x35\x36\x2e\x39\x33\x2d\x35\x2e\x33\ -\x33\x2d\x31\x30\x37\x2e\x32\x35\x2c\x32\x38\x2e\x36\x32\x2d\x31\ -\x35\x34\x2e\x33\x34\x2c\x31\x33\x2e\x36\x31\x2d\x31\x38\x2e\x38\ -\x38\x2c\x32\x38\x2e\x33\x34\x2d\x32\x37\x2e\x36\x31\x2c\x35\x33\ -\x2e\x36\x37\x2d\x32\x32\x2e\x37\x36\x2c\x35\x36\x2e\x34\x39\x2c\ -\x31\x30\x2e\x38\x31\x2c\x31\x30\x33\x2e\x36\x32\x2c\x33\x36\x2e\ -\x32\x38\x2c\x31\x34\x31\x2e\x39\x37\x2c\x37\x38\x2e\x36\x39\x2e\ -\x37\x38\x2e\x38\x36\x2e\x39\x33\x2c\x32\x2e\x32\x37\x2c\x32\x2e\ -\x31\x34\x2c\x35\x2e\x33\x39\x5a\x4d\x32\x39\x39\x2e\x36\x2c\x33\ -\x32\x39\x2e\x33\x35\x63\x36\x2e\x33\x39\x2e\x31\x38\x2c\x32\x39\ -\x2e\x30\x37\x2d\x32\x32\x2e\x31\x37\x2c\x32\x39\x2e\x34\x33\x2d\ -\x32\x39\x2c\x2e\x33\x36\x2d\x36\x2e\x38\x33\x2d\x32\x31\x2e\x33\ -\x2d\x32\x39\x2e\x31\x33\x2d\x32\x38\x2e\x36\x37\x2d\x32\x39\x2e\ -\x35\x32\x2d\x36\x2e\x32\x35\x2d\x2e\x33\x33\x2d\x32\x39\x2e\x33\ -\x36\x2c\x32\x32\x2e\x32\x2d\x32\x39\x2e\x35\x36\x2c\x32\x38\x2e\ -\x38\x32\x2d\x2e\x32\x2c\x36\x2e\x37\x33\x2c\x32\x31\x2e\x38\x38\ -\x2c\x32\x39\x2e\x35\x31\x2c\x32\x38\x2e\x38\x2c\x32\x39\x2e\x37\ -\x31\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x0b\x8f\ +\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x32\x39\x2e\x30\x34\ +\x2c\x31\x39\x32\x2e\x38\x33\x63\x2d\x32\x36\x2e\x30\x35\x2d\x32\ +\x2e\x36\x33\x2d\x35\x30\x2e\x30\x35\x2c\x31\x2e\x30\x31\x2d\x37\ +\x32\x2e\x38\x33\x2c\x31\x31\x2e\x36\x38\x2d\x31\x39\x2e\x34\x35\ +\x2c\x39\x2e\x31\x31\x2d\x33\x34\x2e\x38\x32\x2c\x32\x32\x2e\x36\ +\x38\x2d\x34\x35\x2e\x37\x37\x2c\x34\x31\x2e\x33\x32\x2d\x33\x2e\ +\x35\x32\x2c\x35\x2e\x39\x39\x2d\x33\x2e\x35\x36\x2c\x31\x30\x2e\ +\x30\x39\x2c\x31\x2e\x39\x35\x2c\x31\x35\x2e\x32\x31\x2c\x31\x31\ +\x2e\x37\x36\x2c\x31\x30\x2e\x39\x33\x2c\x31\x31\x2e\x33\x33\x2c\ +\x31\x30\x2e\x39\x38\x2c\x32\x35\x2e\x37\x33\x2c\x35\x2e\x33\x32\ +\x2c\x34\x34\x2e\x31\x33\x2d\x31\x37\x2e\x33\x34\x2c\x38\x32\x2e\ +\x34\x36\x2d\x35\x2e\x30\x35\x2c\x31\x31\x38\x2e\x37\x32\x2c\x32\ +\x32\x2e\x31\x33\x2c\x31\x33\x2e\x31\x34\x2c\x39\x2e\x38\x35\x2c\ +\x31\x32\x2e\x33\x34\x2c\x32\x31\x2e\x31\x33\x2c\x31\x30\x2e\x34\ +\x2c\x33\x34\x2e\x34\x32\x2d\x36\x2e\x33\x31\x2c\x34\x33\x2e\x31\ +\x38\x2d\x32\x36\x2e\x30\x39\x2c\x37\x38\x2e\x37\x37\x2d\x35\x39\ +\x2e\x33\x34\x2c\x31\x30\x36\x2e\x39\x32\x2d\x2e\x36\x37\x2e\x35\ +\x37\x2d\x31\x2e\x35\x34\x2e\x39\x33\x2d\x32\x2e\x33\x36\x2c\x31\ +\x2e\x33\x2d\x2e\x32\x31\x2e\x31\x2d\x2e\x35\x38\x2d\x2e\x31\x36\ +\x2d\x31\x2e\x33\x35\x2d\x2e\x34\x2c\x32\x2e\x36\x32\x2d\x32\x34\ +\x2e\x39\x38\x2e\x31\x34\x2d\x34\x39\x2e\x34\x36\x2d\x31\x30\x2e\ +\x33\x34\x2d\x37\x32\x2e\x36\x37\x2d\x39\x2e\x32\x34\x2d\x32\x30\ +\x2e\x34\x36\x2d\x32\x33\x2e\x33\x34\x2d\x33\x36\x2e\x36\x37\x2d\ +\x34\x33\x2d\x34\x37\x2e\x39\x2d\x34\x2e\x34\x32\x2d\x32\x2e\x35\ +\x33\x2d\x36\x2e\x39\x39\x2d\x31\x2e\x39\x35\x2d\x31\x31\x2e\x32\ +\x32\x2c\x31\x2e\x34\x37\x2d\x31\x31\x2e\x30\x37\x2c\x38\x2e\x39\ +\x36\x2d\x31\x32\x2e\x32\x36\x2c\x31\x36\x2e\x36\x38\x2d\x36\x2e\ +\x35\x32\x2c\x33\x30\x2e\x39\x34\x2c\x31\x36\x2e\x32\x36\x2c\x34\ +\x30\x2e\x34\x32\x2c\x32\x2e\x36\x39\x2c\x37\x36\x2e\x36\x31\x2d\ +\x32\x31\x2e\x37\x33\x2c\x31\x31\x30\x2e\x30\x36\x2d\x39\x2e\x33\ +\x2c\x31\x32\x2e\x37\x34\x2d\x31\x39\x2e\x36\x34\x2c\x31\x37\x2e\ +\x39\x2d\x33\x36\x2e\x33\x35\x2c\x31\x34\x2e\x38\x39\x2d\x34\x30\ +\x2e\x39\x35\x2d\x37\x2e\x33\x35\x2d\x37\x35\x2e\x30\x36\x2d\x32\ +\x35\x2e\x37\x34\x2d\x31\x30\x32\x2e\x36\x34\x2d\x35\x36\x2e\x36\ +\x33\x2d\x2e\x35\x38\x2d\x2e\x36\x35\x2d\x2e\x38\x35\x2d\x31\x2e\ +\x35\x37\x2d\x31\x2e\x38\x32\x2d\x33\x2e\x34\x33\x2c\x31\x38\x2e\ +\x33\x32\x2c\x31\x2e\x38\x32\x2c\x33\x35\x2e\x37\x33\x2e\x33\x31\ +\x2c\x35\x32\x2e\x37\x32\x2d\x34\x2e\x34\x2c\x32\x38\x2e\x31\x33\ +\x2d\x37\x2e\x38\x2c\x35\x30\x2e\x36\x37\x2d\x32\x33\x2e\x31\x39\ +\x2c\x36\x36\x2d\x34\x38\x2e\x36\x35\x2c\x33\x2e\x38\x2d\x36\x2e\ +\x33\x31\x2c\x33\x2e\x32\x32\x2d\x31\x30\x2e\x33\x35\x2d\x31\x2e\ +\x38\x38\x2d\x31\x35\x2e\x33\x35\x2d\x31\x31\x2e\x35\x39\x2d\x31\ +\x31\x2e\x33\x37\x2d\x31\x31\x2e\x33\x32\x2d\x31\x31\x2e\x33\x39\ +\x2d\x32\x36\x2e\x36\x31\x2d\x35\x2e\x32\x38\x2d\x34\x31\x2e\x39\ +\x34\x2c\x31\x36\x2e\x37\x37\x2d\x37\x38\x2e\x37\x33\x2c\x35\x2e\ +\x30\x39\x2d\x31\x31\x34\x2e\x31\x37\x2d\x31\x39\x2e\x32\x35\x2d\ +\x31\x36\x2e\x32\x39\x2d\x31\x31\x2e\x31\x39\x2d\x31\x36\x2e\x34\ +\x32\x2d\x32\x34\x2e\x37\x33\x2d\x31\x33\x2e\x35\x33\x2d\x34\x31\ +\x2e\x33\x36\x2c\x37\x2e\x31\x38\x2d\x34\x31\x2e\x32\x39\x2c\x32\ +\x36\x2e\x34\x35\x2d\x37\x35\x2e\x34\x32\x2c\x35\x38\x2e\x32\x34\ +\x2d\x31\x30\x32\x2e\x35\x38\x2e\x38\x34\x2d\x2e\x37\x31\x2c\x31\ +\x2e\x38\x31\x2d\x31\x2e\x32\x37\x2c\x33\x2e\x36\x35\x2d\x32\x2e\ +\x35\x34\x2e\x39\x38\x2c\x31\x35\x2e\x37\x32\x2d\x2e\x38\x39\x2c\ +\x33\x30\x2e\x33\x31\x2c\x31\x2e\x39\x39\x2c\x34\x34\x2e\x37\x36\ +\x2c\x36\x2e\x34\x36\x2c\x33\x32\x2e\x33\x39\x2c\x32\x31\x2e\x37\ +\x37\x2c\x35\x38\x2e\x36\x34\x2c\x35\x30\x2e\x35\x31\x2c\x37\x36\ +\x2e\x32\x36\x2c\x35\x2e\x33\x32\x2c\x33\x2e\x32\x36\x2c\x38\x2e\ +\x37\x36\x2c\x33\x2e\x35\x38\x2c\x31\x33\x2e\x35\x2d\x31\x2e\x33\ +\x39\x2c\x31\x31\x2e\x38\x31\x2d\x31\x32\x2e\x33\x38\x2c\x31\x31\ +\x2e\x37\x31\x2d\x31\x31\x2e\x39\x36\x2c\x35\x2e\x37\x39\x2d\x32\ +\x38\x2e\x31\x39\x2d\x31\x34\x2e\x38\x37\x2d\x34\x30\x2e\x38\x2d\ +\x33\x2e\x38\x32\x2d\x37\x36\x2e\x38\x36\x2c\x32\x30\x2e\x35\x31\ +\x2d\x31\x31\x30\x2e\x36\x32\x2c\x39\x2e\x37\x35\x2d\x31\x33\x2e\ +\x35\x33\x2c\x32\x30\x2e\x33\x31\x2d\x31\x39\x2e\x37\x39\x2c\x33\ +\x38\x2e\x34\x37\x2d\x31\x36\x2e\x33\x31\x2c\x34\x30\x2e\x34\x39\ +\x2c\x37\x2e\x37\x34\x2c\x37\x34\x2e\x32\x36\x2c\x32\x36\x2c\x31\ +\x30\x31\x2e\x37\x35\x2c\x35\x36\x2e\x34\x2e\x35\x36\x2e\x36\x31\ +\x2e\x36\x37\x2c\x31\x2e\x36\x33\x2c\x31\x2e\x35\x33\x2c\x33\x2e\ +\x38\x36\x5a\x4d\x32\x39\x39\x2e\x37\x32\x2c\x33\x32\x31\x2e\x30\ +\x33\x63\x34\x2e\x35\x38\x2e\x31\x33\x2c\x32\x30\x2e\x38\x33\x2d\ +\x31\x35\x2e\x38\x39\x2c\x32\x31\x2e\x30\x39\x2d\x32\x30\x2e\x37\ +\x39\x2e\x32\x36\x2d\x34\x2e\x38\x39\x2d\x31\x35\x2e\x32\x37\x2d\ +\x32\x30\x2e\x38\x38\x2d\x32\x30\x2e\x35\x35\x2d\x32\x31\x2e\x31\ +\x36\x2d\x34\x2e\x34\x38\x2d\x2e\x32\x34\x2d\x32\x31\x2e\x30\x34\ +\x2c\x31\x35\x2e\x39\x31\x2d\x32\x31\x2e\x31\x38\x2c\x32\x30\x2e\ +\x36\x35\x2d\x2e\x31\x34\x2c\x34\x2e\x38\x33\x2c\x31\x35\x2e\x36\ +\x38\x2c\x32\x31\x2e\x31\x35\x2c\x32\x30\x2e\x36\x34\x2c\x32\x31\ +\x2e\x32\x39\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x0b\x76\ \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\ @@ -11776,176 +11789,175 @@ \x0a\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x73\ \x74\x79\x6c\x65\x3e\x0a\x20\x20\x3c\x2f\x64\x65\x66\x73\x3e\x0a\ \x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x35\x37\x2e\x37\x39\ -\x2c\x33\x31\x38\x2e\x30\x33\x63\x2e\x34\x37\x2d\x31\x32\x2e\x30\ -\x31\x2c\x35\x2e\x32\x34\x2d\x31\x37\x2e\x36\x2c\x31\x35\x2e\x30\ -\x31\x2d\x31\x36\x2e\x39\x36\x2c\x31\x30\x2e\x38\x36\x2e\x37\x2c\ -\x31\x33\x2e\x33\x38\x2c\x37\x2e\x38\x35\x2c\x31\x33\x2e\x33\x37\ -\x2c\x31\x37\x2e\x33\x39\x2d\x2e\x31\x31\x2c\x36\x36\x2e\x36\x39\ -\x2d\x2e\x30\x32\x2c\x31\x33\x33\x2e\x33\x38\x2d\x2e\x31\x31\x2c\ -\x32\x30\x30\x2e\x30\x38\x2d\x2e\x30\x31\x2c\x31\x31\x2e\x31\x34\ -\x2d\x35\x2e\x30\x32\x2c\x31\x36\x2e\x37\x2d\x31\x34\x2e\x33\x33\ -\x2c\x31\x36\x2e\x33\x37\x2d\x31\x30\x2e\x30\x36\x2d\x2e\x33\x35\ -\x2d\x31\x34\x2e\x35\x39\x2d\x36\x2e\x35\x38\x2d\x31\x33\x2e\x37\ -\x33\x2d\x31\x35\x2e\x37\x37\x2c\x31\x2e\x30\x36\x2d\x31\x31\x2e\ -\x33\x33\x2d\x33\x2e\x34\x31\x2d\x31\x33\x2e\x37\x36\x2d\x31\x34\ -\x2e\x31\x35\x2d\x31\x33\x2e\x36\x39\x2d\x36\x35\x2e\x31\x36\x2e\ -\x34\x38\x2d\x31\x33\x30\x2e\x33\x33\x2e\x33\x39\x2d\x31\x39\x35\ -\x2e\x35\x2e\x31\x33\x2d\x34\x34\x2e\x30\x39\x2d\x2e\x31\x38\x2d\ -\x38\x32\x2e\x33\x35\x2d\x31\x35\x2e\x37\x36\x2d\x31\x31\x35\x2e\ -\x34\x32\x2d\x34\x34\x2e\x37\x38\x2d\x37\x2e\x31\x2d\x36\x2e\x32\ -\x33\x2d\x31\x34\x2e\x37\x35\x2d\x31\x31\x2e\x39\x35\x2d\x32\x32\ -\x2e\x36\x38\x2d\x31\x37\x2e\x30\x37\x43\x34\x30\x2e\x35\x32\x2c\ -\x33\x39\x38\x2e\x36\x34\x2c\x34\x2e\x33\x34\x2c\x33\x31\x34\x2e\ -\x39\x39\x2c\x31\x39\x2e\x34\x33\x2c\x32\x33\x33\x2e\x31\x37\x2c\ -\x34\x31\x2e\x34\x34\x2c\x31\x31\x33\x2e\x37\x37\x2c\x31\x36\x33\ -\x2e\x35\x38\x2c\x33\x39\x2e\x30\x31\x2c\x32\x38\x30\x2e\x32\x33\ -\x2c\x37\x33\x2e\x35\x34\x63\x38\x32\x2e\x36\x36\x2c\x32\x34\x2e\ -\x34\x37\x2c\x31\x34\x31\x2e\x33\x2c\x39\x36\x2e\x39\x2c\x31\x34\ -\x36\x2e\x31\x31\x2c\x31\x38\x33\x2e\x30\x34\x2c\x31\x2e\x33\x33\ -\x2c\x32\x33\x2e\x38\x39\x2d\x32\x2e\x32\x32\x2c\x34\x38\x2e\x30\ -\x36\x2d\x33\x2e\x35\x36\x2c\x37\x32\x2e\x36\x32\x2c\x39\x2e\x36\ -\x38\x2c\x30\x2c\x32\x31\x2e\x33\x36\x2c\x30\x2c\x33\x34\x2e\x34\ -\x33\x2c\x30\x2c\x2e\x32\x34\x2d\x34\x2e\x34\x37\x2e\x34\x35\x2d\ -\x37\x2e\x38\x31\x2e\x35\x38\x2d\x31\x31\x2e\x31\x36\x5a\x4d\x32\ -\x32\x32\x2e\x39\x39\x2c\x39\x33\x2e\x38\x38\x63\x2d\x39\x37\x2e\ -\x37\x36\x2d\x2e\x34\x35\x2d\x31\x37\x38\x2e\x39\x38\x2c\x38\x30\ -\x2e\x37\x39\x2d\x31\x37\x38\x2e\x30\x33\x2c\x31\x37\x38\x2e\x30\ -\x36\x2e\x39\x35\x2c\x39\x36\x2e\x38\x33\x2c\x38\x30\x2e\x32\x34\ -\x2c\x31\x37\x35\x2e\x37\x34\x2c\x31\x37\x36\x2e\x36\x2c\x31\x37\ -\x35\x2e\x37\x35\x2c\x39\x38\x2c\x30\x2c\x31\x37\x37\x2e\x32\x35\ -\x2d\x37\x39\x2e\x32\x33\x2c\x31\x37\x37\x2e\x32\x36\x2d\x31\x37\ -\x37\x2e\x32\x33\x2c\x30\x2d\x39\x36\x2e\x30\x33\x2d\x37\x39\x2e\ -\x37\x37\x2d\x31\x37\x36\x2e\x31\x34\x2d\x31\x37\x35\x2e\x38\x34\ -\x2d\x31\x37\x36\x2e\x35\x38\x5a\x22\x2f\x3e\x0a\x20\x20\x3c\x70\ -\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\ -\x22\x20\x64\x3d\x22\x4d\x33\x32\x34\x2e\x33\x38\x2c\x31\x38\x35\ -\x2e\x36\x36\x63\x2d\x32\x30\x2e\x36\x39\x2d\x32\x2e\x30\x39\x2d\ -\x33\x39\x2e\x37\x36\x2e\x38\x2d\x35\x37\x2e\x38\x35\x2c\x39\x2e\ -\x32\x37\x2d\x31\x35\x2e\x34\x35\x2c\x37\x2e\x32\x34\x2d\x32\x37\ -\x2e\x36\x35\x2c\x31\x38\x2e\x30\x31\x2d\x33\x36\x2e\x33\x35\x2c\ -\x33\x32\x2e\x38\x32\x2d\x32\x2e\x38\x2c\x34\x2e\x37\x36\x2d\x32\ -\x2e\x38\x33\x2c\x38\x2e\x30\x31\x2c\x31\x2e\x35\x35\x2c\x31\x32\ -\x2e\x30\x38\x2c\x39\x2e\x33\x34\x2c\x38\x2e\x36\x38\x2c\x39\x2c\ -\x38\x2e\x37\x32\x2c\x32\x30\x2e\x34\x34\x2c\x34\x2e\x32\x33\x2c\ -\x33\x35\x2e\x30\x35\x2d\x31\x33\x2e\x37\x37\x2c\x36\x35\x2e\x34\ -\x39\x2d\x34\x2e\x30\x31\x2c\x39\x34\x2e\x33\x2c\x31\x37\x2e\x35\ -\x38\x2c\x31\x30\x2e\x34\x34\x2c\x37\x2e\x38\x32\x2c\x39\x2e\x38\ -\x2c\x31\x36\x2e\x37\x39\x2c\x38\x2e\x32\x36\x2c\x32\x37\x2e\x33\ -\x34\x2d\x35\x2e\x30\x31\x2c\x33\x34\x2e\x33\x2d\x32\x30\x2e\x37\ -\x32\x2c\x36\x32\x2e\x35\x36\x2d\x34\x37\x2e\x31\x34\x2c\x38\x34\ -\x2e\x39\x33\x2d\x2e\x35\x34\x2e\x34\x35\x2d\x31\x2e\x32\x33\x2e\ -\x37\x34\x2d\x31\x2e\x38\x38\x2c\x31\x2e\x30\x33\x2d\x2e\x31\x37\ -\x2e\x30\x38\x2d\x2e\x34\x36\x2d\x2e\x31\x33\x2d\x31\x2e\x30\x37\ -\x2d\x2e\x33\x31\x2c\x32\x2e\x30\x38\x2d\x31\x39\x2e\x38\x34\x2e\ -\x31\x31\x2d\x33\x39\x2e\x32\x39\x2d\x38\x2e\x32\x32\x2d\x35\x37\ -\x2e\x37\x32\x2d\x37\x2e\x33\x34\x2d\x31\x36\x2e\x32\x35\x2d\x31\ -\x38\x2e\x35\x33\x2d\x32\x39\x2e\x31\x32\x2d\x33\x34\x2e\x31\x35\ -\x2d\x33\x38\x2e\x30\x34\x2d\x33\x2e\x35\x31\x2d\x32\x2e\x30\x31\ -\x2d\x35\x2e\x35\x35\x2d\x31\x2e\x35\x35\x2d\x38\x2e\x39\x31\x2c\ -\x31\x2e\x31\x37\x2d\x38\x2e\x37\x39\x2c\x37\x2e\x31\x31\x2d\x39\ -\x2e\x37\x34\x2c\x31\x33\x2e\x32\x35\x2d\x35\x2e\x31\x38\x2c\x32\ -\x34\x2e\x35\x37\x2c\x31\x32\x2e\x39\x32\x2c\x33\x32\x2e\x31\x2c\ -\x32\x2e\x31\x33\x2c\x36\x30\x2e\x38\x35\x2d\x31\x37\x2e\x32\x36\ -\x2c\x38\x37\x2e\x34\x32\x2d\x37\x2e\x33\x39\x2c\x31\x30\x2e\x31\ -\x32\x2d\x31\x35\x2e\x36\x2c\x31\x34\x2e\x32\x31\x2d\x32\x38\x2e\ -\x38\x37\x2c\x31\x31\x2e\x38\x33\x2d\x33\x32\x2e\x35\x32\x2d\x35\ -\x2e\x38\x34\x2d\x35\x39\x2e\x36\x32\x2d\x32\x30\x2e\x34\x35\x2d\ -\x38\x31\x2e\x35\x33\x2d\x34\x34\x2e\x39\x38\x2d\x2e\x34\x36\x2d\ -\x2e\x35\x31\x2d\x2e\x36\x37\x2d\x31\x2e\x32\x35\x2d\x31\x2e\x34\ -\x34\x2d\x32\x2e\x37\x33\x2c\x31\x34\x2e\x35\x35\x2c\x31\x2e\x34\ -\x34\x2c\x32\x38\x2e\x33\x38\x2e\x32\x35\x2c\x34\x31\x2e\x38\x37\ -\x2d\x33\x2e\x35\x2c\x32\x32\x2e\x33\x35\x2d\x36\x2e\x32\x2c\x34\ -\x30\x2e\x32\x35\x2d\x31\x38\x2e\x34\x32\x2c\x35\x32\x2e\x34\x32\ -\x2d\x33\x38\x2e\x36\x34\x2c\x33\x2e\x30\x32\x2d\x35\x2e\x30\x31\ -\x2c\x32\x2e\x35\x36\x2d\x38\x2e\x32\x32\x2d\x31\x2e\x34\x39\x2d\ -\x31\x32\x2e\x31\x39\x2d\x39\x2e\x32\x31\x2d\x39\x2e\x30\x33\x2d\ -\x38\x2e\x39\x39\x2d\x39\x2e\x30\x35\x2d\x32\x31\x2e\x31\x33\x2d\ -\x34\x2e\x31\x39\x2d\x33\x33\x2e\x33\x31\x2c\x31\x33\x2e\x33\x32\ -\x2d\x36\x32\x2e\x35\x33\x2c\x34\x2e\x30\x34\x2d\x39\x30\x2e\x36\ -\x39\x2d\x31\x35\x2e\x32\x39\x2d\x31\x32\x2e\x39\x34\x2d\x38\x2e\ -\x38\x39\x2d\x31\x33\x2e\x30\x34\x2d\x31\x39\x2e\x36\x34\x2d\x31\ -\x30\x2e\x37\x35\x2d\x33\x32\x2e\x38\x35\x2c\x35\x2e\x37\x2d\x33\ -\x32\x2e\x38\x2c\x32\x31\x2e\x30\x31\x2d\x35\x39\x2e\x39\x2c\x34\ -\x36\x2e\x32\x36\x2d\x38\x31\x2e\x34\x38\x2e\x36\x36\x2d\x2e\x35\ -\x37\x2c\x31\x2e\x34\x34\x2d\x31\x2e\x30\x31\x2c\x32\x2e\x39\x2d\ -\x32\x2e\x30\x32\x2e\x37\x38\x2c\x31\x32\x2e\x34\x39\x2d\x2e\x37\ -\x31\x2c\x32\x34\x2e\x30\x38\x2c\x31\x2e\x35\x38\x2c\x33\x35\x2e\ -\x35\x35\x2c\x35\x2e\x31\x33\x2c\x32\x35\x2e\x37\x33\x2c\x31\x37\ -\x2e\x32\x39\x2c\x34\x36\x2e\x35\x38\x2c\x34\x30\x2e\x31\x32\x2c\ -\x36\x30\x2e\x35\x37\x2c\x34\x2e\x32\x33\x2c\x32\x2e\x35\x39\x2c\ -\x36\x2e\x39\x36\x2c\x32\x2e\x38\x34\x2c\x31\x30\x2e\x37\x32\x2d\ -\x31\x2e\x31\x2c\x39\x2e\x33\x38\x2d\x39\x2e\x38\x34\x2c\x39\x2e\ -\x33\x2d\x39\x2e\x35\x2c\x34\x2e\x36\x2d\x32\x32\x2e\x33\x39\x2d\ -\x31\x31\x2e\x38\x31\x2d\x33\x32\x2e\x34\x31\x2d\x33\x2e\x30\x34\ -\x2d\x36\x31\x2e\x30\x35\x2c\x31\x36\x2e\x32\x39\x2d\x38\x37\x2e\ -\x38\x36\x2c\x37\x2e\x37\x35\x2d\x31\x30\x2e\x37\x35\x2c\x31\x36\ -\x2e\x31\x33\x2d\x31\x35\x2e\x37\x31\x2c\x33\x30\x2e\x35\x35\x2d\ -\x31\x32\x2e\x39\x36\x2c\x33\x32\x2e\x31\x36\x2c\x36\x2e\x31\x35\ -\x2c\x35\x38\x2e\x39\x39\x2c\x32\x30\x2e\x36\x35\x2c\x38\x30\x2e\ -\x38\x32\x2c\x34\x34\x2e\x38\x2e\x34\x34\x2e\x34\x39\x2e\x35\x33\ -\x2c\x31\x2e\x32\x39\x2c\x31\x2e\x32\x32\x2c\x33\x2e\x30\x37\x5a\ -\x4d\x32\x32\x31\x2e\x36\x36\x2c\x32\x38\x37\x2e\x34\x39\x63\x33\ -\x2e\x36\x34\x2e\x31\x2c\x31\x36\x2e\x35\x35\x2d\x31\x32\x2e\x36\ -\x32\x2c\x31\x36\x2e\x37\x35\x2d\x31\x36\x2e\x35\x31\x2e\x32\x2d\ -\x33\x2e\x38\x39\x2d\x31\x32\x2e\x31\x33\x2d\x31\x36\x2e\x35\x38\ -\x2d\x31\x36\x2e\x33\x32\x2d\x31\x36\x2e\x38\x2d\x33\x2e\x35\x36\ -\x2d\x2e\x31\x39\x2d\x31\x36\x2e\x37\x31\x2c\x31\x32\x2e\x36\x34\ -\x2d\x31\x36\x2e\x38\x33\x2c\x31\x36\x2e\x34\x2d\x2e\x31\x31\x2c\ -\x33\x2e\x38\x33\x2c\x31\x32\x2e\x34\x36\x2c\x31\x36\x2e\x38\x2c\ -\x31\x36\x2e\x34\x2c\x31\x36\x2e\x39\x31\x5a\x22\x2f\x3e\x0a\x20\ -\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x35\x38\x33\x2e\x39\x36\x2c\ -\x33\x33\x33\x2e\x31\x36\x63\x2d\x33\x2e\x36\x35\x2c\x32\x2e\x31\ -\x35\x2d\x37\x2e\x33\x31\x2c\x34\x2e\x32\x39\x2d\x31\x30\x2e\x39\ -\x35\x2c\x36\x2e\x34\x35\x2d\x31\x35\x2e\x33\x2c\x39\x2e\x31\x2d\ -\x33\x30\x2e\x35\x39\x2c\x31\x38\x2e\x32\x2d\x34\x35\x2e\x38\x38\ -\x2c\x32\x37\x2e\x33\x31\x2d\x2e\x31\x37\x2e\x31\x2d\x2e\x33\x34\ -\x2e\x31\x39\x2d\x2e\x36\x37\x2e\x33\x37\x2c\x30\x2d\x32\x2e\x33\ -\x34\x2d\x2e\x30\x35\x2d\x31\x39\x2e\x35\x35\x2d\x2e\x30\x35\x2d\ -\x31\x39\x2e\x35\x35\x68\x2d\x32\x33\x2e\x32\x36\x76\x2d\x32\x38\ -\x2e\x31\x35\x68\x32\x33\x2e\x32\x36\x73\x2d\x2e\x30\x32\x2d\x32\ -\x30\x2e\x35\x37\x2c\x30\x2d\x32\x30\x2e\x37\x32\x63\x2e\x30\x37\ -\x2c\x30\x2c\x31\x30\x2e\x32\x38\x2c\x35\x2e\x39\x37\x2c\x31\x34\ -\x2e\x39\x39\x2c\x38\x2e\x37\x37\x2c\x31\x34\x2e\x31\x39\x2c\x38\ -\x2e\x34\x34\x2c\x32\x38\x2e\x33\x38\x2c\x31\x36\x2e\x38\x38\x2c\ -\x34\x32\x2e\x35\x37\x2c\x32\x35\x2e\x33\x32\x2c\x30\x2c\x2e\x30\ -\x36\x2c\x30\x2c\x2e\x31\x32\x2c\x30\x2c\x2e\x31\x39\x5a\x22\x2f\ +\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x33\x30\x2e\x35\x35\ +\x2c\x33\x31\x34\x2e\x39\x32\x63\x2e\x33\x39\x2d\x39\x2e\x39\x34\ +\x2c\x34\x2e\x33\x33\x2d\x31\x34\x2e\x35\x36\x2c\x31\x32\x2e\x34\ +\x32\x2d\x31\x34\x2e\x30\x34\x2c\x38\x2e\x39\x39\x2e\x35\x38\x2c\ +\x31\x31\x2e\x30\x37\x2c\x36\x2e\x35\x2c\x31\x31\x2e\x30\x36\x2c\ +\x31\x34\x2e\x33\x38\x2d\x2e\x30\x39\x2c\x35\x35\x2e\x31\x38\x2d\ +\x2e\x30\x32\x2c\x31\x31\x30\x2e\x33\x36\x2d\x2e\x30\x39\x2c\x31\ +\x36\x35\x2e\x35\x33\x2d\x2e\x30\x31\x2c\x39\x2e\x32\x31\x2d\x34\ +\x2e\x31\x35\x2c\x31\x33\x2e\x38\x31\x2d\x31\x31\x2e\x38\x35\x2c\ +\x31\x33\x2e\x35\x35\x2d\x38\x2e\x33\x32\x2d\x2e\x32\x39\x2d\x31\ +\x32\x2e\x30\x37\x2d\x35\x2e\x34\x35\x2d\x31\x31\x2e\x33\x36\x2d\ +\x31\x33\x2e\x30\x34\x2e\x38\x38\x2d\x39\x2e\x33\x38\x2d\x32\x2e\ +\x38\x32\x2d\x31\x31\x2e\x33\x39\x2d\x31\x31\x2e\x37\x31\x2d\x31\ +\x31\x2e\x33\x32\x2d\x35\x33\x2e\x39\x31\x2e\x34\x2d\x31\x30\x37\ +\x2e\x38\x33\x2e\x33\x32\x2d\x31\x36\x31\x2e\x37\x35\x2e\x31\x31\ +\x2d\x33\x36\x2e\x34\x38\x2d\x2e\x31\x34\x2d\x36\x38\x2e\x31\x34\ +\x2d\x31\x33\x2e\x30\x34\x2d\x39\x35\x2e\x35\x2d\x33\x37\x2e\x30\ +\x35\x2d\x35\x2e\x38\x37\x2d\x35\x2e\x31\x35\x2d\x31\x32\x2e\x32\ +\x2d\x39\x2e\x38\x38\x2d\x31\x38\x2e\x37\x36\x2d\x31\x34\x2e\x31\ +\x33\x2d\x35\x37\x2e\x36\x39\x2d\x33\x37\x2e\x33\x2d\x38\x37\x2e\ +\x36\x32\x2d\x31\x30\x36\x2e\x35\x31\x2d\x37\x35\x2e\x31\x34\x2d\ +\x31\x37\x34\x2e\x32\x31\x2c\x31\x38\x2e\x32\x31\x2d\x39\x38\x2e\ +\x37\x38\x2c\x31\x31\x39\x2e\x32\x37\x2d\x31\x36\x30\x2e\x36\x34\ +\x2c\x32\x31\x35\x2e\x37\x38\x2d\x31\x33\x32\x2e\x30\x37\x2c\x36\ +\x38\x2e\x33\x39\x2c\x32\x30\x2e\x32\x34\x2c\x31\x31\x36\x2e\x39\ +\x2c\x38\x30\x2e\x31\x37\x2c\x31\x32\x30\x2e\x38\x38\x2c\x31\x35\ +\x31\x2e\x34\x34\x2c\x31\x2e\x31\x2c\x31\x39\x2e\x37\x37\x2d\x31\ +\x2e\x38\x34\x2c\x33\x39\x2e\x37\x36\x2d\x32\x2e\x39\x35\x2c\x36\ +\x30\x2e\x30\x39\x2c\x38\x2e\x30\x31\x2c\x30\x2c\x31\x37\x2e\x36\ +\x37\x2c\x30\x2c\x32\x38\x2e\x34\x39\x2c\x30\x2c\x2e\x32\x2d\x33\ +\x2e\x36\x39\x2e\x33\x37\x2d\x36\x2e\x34\x36\x2e\x34\x38\x2d\x39\ +\x2e\x32\x34\x5a\x4d\x32\x33\x36\x2e\x32\x38\x2c\x31\x32\x39\x2e\ +\x34\x36\x63\x2d\x38\x30\x2e\x38\x38\x2d\x2e\x33\x37\x2d\x31\x34\ +\x38\x2e\x30\x38\x2c\x36\x36\x2e\x38\x35\x2d\x31\x34\x37\x2e\x32\ +\x39\x2c\x31\x34\x37\x2e\x33\x32\x2e\x37\x39\x2c\x38\x30\x2e\x31\ +\x32\x2c\x36\x36\x2e\x33\x39\x2c\x31\x34\x35\x2e\x34\x2c\x31\x34\ +\x36\x2e\x31\x31\x2c\x31\x34\x35\x2e\x34\x31\x2c\x38\x31\x2e\x30\ +\x38\x2c\x30\x2c\x31\x34\x36\x2e\x36\x35\x2d\x36\x35\x2e\x35\x35\ +\x2c\x31\x34\x36\x2e\x36\x36\x2d\x31\x34\x36\x2e\x36\x33\x2c\x30\ +\x2d\x37\x39\x2e\x34\x35\x2d\x36\x36\x2d\x31\x34\x35\x2e\x37\x33\ +\x2d\x31\x34\x35\x2e\x34\x38\x2d\x31\x34\x36\x2e\x31\x5a\x22\x2f\ \x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x35\x38\x33\x2e\ -\x39\x36\x2c\x34\x39\x36\x2e\x30\x35\x63\x2d\x33\x2e\x36\x35\x2c\ -\x32\x2e\x31\x35\x2d\x37\x2e\x33\x31\x2c\x34\x2e\x32\x39\x2d\x31\ -\x30\x2e\x39\x35\x2c\x36\x2e\x34\x35\x2d\x31\x35\x2e\x33\x2c\x39\ -\x2e\x31\x2d\x33\x30\x2e\x35\x39\x2c\x31\x38\x2e\x32\x2d\x34\x35\ -\x2e\x38\x38\x2c\x32\x37\x2e\x33\x31\x2d\x2e\x31\x37\x2e\x31\x2d\ -\x2e\x33\x34\x2e\x31\x39\x2d\x2e\x36\x37\x2e\x33\x37\x2c\x30\x2d\ -\x32\x2e\x33\x34\x2d\x2e\x30\x35\x2d\x31\x39\x2e\x35\x35\x2d\x2e\ -\x30\x35\x2d\x31\x39\x2e\x35\x35\x68\x2d\x32\x33\x2e\x32\x36\x76\ -\x2d\x32\x38\x2e\x31\x35\x68\x32\x33\x2e\x32\x36\x73\x2d\x2e\x30\ -\x32\x2d\x32\x30\x2e\x35\x37\x2c\x30\x2d\x32\x30\x2e\x37\x32\x63\ -\x2e\x30\x37\x2c\x30\x2c\x31\x30\x2e\x32\x38\x2c\x35\x2e\x39\x37\ -\x2c\x31\x34\x2e\x39\x39\x2c\x38\x2e\x37\x37\x2c\x31\x34\x2e\x31\ -\x39\x2c\x38\x2e\x34\x34\x2c\x32\x38\x2e\x33\x38\x2c\x31\x36\x2e\ -\x38\x38\x2c\x34\x32\x2e\x35\x37\x2c\x32\x35\x2e\x33\x32\x2c\x30\ -\x2c\x2e\x30\x36\x2c\x30\x2c\x2e\x31\x32\x2c\x30\x2c\x2e\x31\x39\ -\x5a\x22\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x35\ -\x38\x33\x2e\x39\x36\x2c\x34\x31\x34\x2e\x36\x31\x63\x2d\x33\x2e\ -\x36\x35\x2c\x32\x2e\x31\x35\x2d\x37\x2e\x33\x31\x2c\x34\x2e\x32\ -\x39\x2d\x31\x30\x2e\x39\x35\x2c\x36\x2e\x34\x35\x2d\x31\x35\x2e\ -\x33\x2c\x39\x2e\x31\x2d\x33\x30\x2e\x35\x39\x2c\x31\x38\x2e\x32\ -\x2d\x34\x35\x2e\x38\x38\x2c\x32\x37\x2e\x33\x31\x2d\x2e\x31\x37\ -\x2e\x31\x2d\x2e\x33\x34\x2e\x31\x39\x2d\x2e\x36\x37\x2e\x33\x37\ -\x2c\x30\x2d\x32\x2e\x33\x34\x2d\x2e\x30\x35\x2d\x31\x39\x2e\x35\ -\x35\x2d\x2e\x30\x35\x2d\x31\x39\x2e\x35\x35\x68\x2d\x32\x33\x2e\ -\x32\x36\x76\x2d\x32\x38\x2e\x31\x35\x68\x32\x33\x2e\x32\x36\x73\ -\x2d\x2e\x30\x32\x2d\x32\x30\x2e\x35\x37\x2c\x30\x2d\x32\x30\x2e\ -\x37\x32\x63\x2e\x30\x37\x2c\x30\x2c\x31\x30\x2e\x32\x38\x2c\x35\ -\x2e\x39\x37\x2c\x31\x34\x2e\x39\x39\x2c\x38\x2e\x37\x37\x2c\x31\ -\x34\x2e\x31\x39\x2c\x38\x2e\x34\x34\x2c\x32\x38\x2e\x33\x38\x2c\ -\x31\x36\x2e\x38\x38\x2c\x34\x32\x2e\x35\x37\x2c\x32\x35\x2e\x33\ -\x32\x2c\x30\x2c\x2e\x30\x36\x2c\x30\x2c\x2e\x31\x32\x2c\x30\x2c\ -\x2e\x31\x39\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ +\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x32\x30\x2e\ +\x31\x37\x2c\x32\x30\x35\x2e\x34\x63\x2d\x31\x37\x2e\x31\x32\x2d\ +\x31\x2e\x37\x33\x2d\x33\x32\x2e\x38\x39\x2e\x36\x36\x2d\x34\x37\ +\x2e\x38\x36\x2c\x37\x2e\x36\x37\x2d\x31\x32\x2e\x37\x38\x2c\x35\ +\x2e\x39\x39\x2d\x32\x32\x2e\x38\x38\x2c\x31\x34\x2e\x39\x2d\x33\ +\x30\x2e\x30\x38\x2c\x32\x37\x2e\x31\x36\x2d\x32\x2e\x33\x31\x2c\ +\x33\x2e\x39\x34\x2d\x32\x2e\x33\x34\x2c\x36\x2e\x36\x33\x2c\x31\ +\x2e\x32\x38\x2c\x39\x2e\x39\x39\x2c\x37\x2e\x37\x33\x2c\x37\x2e\ +\x31\x38\x2c\x37\x2e\x34\x35\x2c\x37\x2e\x32\x32\x2c\x31\x36\x2e\ +\x39\x31\x2c\x33\x2e\x35\x2c\x32\x39\x2d\x31\x31\x2e\x33\x39\x2c\ +\x35\x34\x2e\x31\x39\x2d\x33\x2e\x33\x32\x2c\x37\x38\x2e\x30\x32\ +\x2c\x31\x34\x2e\x35\x34\x2c\x38\x2e\x36\x34\x2c\x36\x2e\x34\x37\ +\x2c\x38\x2e\x31\x31\x2c\x31\x33\x2e\x38\x39\x2c\x36\x2e\x38\x34\ +\x2c\x32\x32\x2e\x36\x32\x2d\x34\x2e\x31\x34\x2c\x32\x38\x2e\x33\ +\x38\x2d\x31\x37\x2e\x31\x34\x2c\x35\x31\x2e\x37\x36\x2d\x33\x39\ +\x2c\x37\x30\x2e\x32\x36\x2d\x2e\x34\x34\x2e\x33\x38\x2d\x31\x2e\ +\x30\x32\x2e\x36\x31\x2d\x31\x2e\x35\x35\x2e\x38\x35\x2d\x2e\x31\ +\x34\x2e\x30\x36\x2d\x2e\x33\x38\x2d\x2e\x31\x2d\x2e\x38\x39\x2d\ +\x2e\x32\x36\x2c\x31\x2e\x37\x32\x2d\x31\x36\x2e\x34\x32\x2e\x30\ +\x39\x2d\x33\x32\x2e\x35\x31\x2d\x36\x2e\x38\x2d\x34\x37\x2e\x37\ +\x36\x2d\x36\x2e\x30\x37\x2d\x31\x33\x2e\x34\x35\x2d\x31\x35\x2e\ +\x33\x33\x2d\x32\x34\x2e\x31\x2d\x32\x38\x2e\x32\x35\x2d\x33\x31\ +\x2e\x34\x37\x2d\x32\x2e\x39\x31\x2d\x31\x2e\x36\x36\x2d\x34\x2e\ +\x35\x39\x2d\x31\x2e\x32\x38\x2d\x37\x2e\x33\x38\x2e\x39\x37\x2d\ +\x37\x2e\x32\x38\x2c\x35\x2e\x38\x39\x2d\x38\x2e\x30\x36\x2c\x31\ +\x30\x2e\x39\x36\x2d\x34\x2e\x32\x39\x2c\x32\x30\x2e\x33\x33\x2c\ +\x31\x30\x2e\x36\x39\x2c\x32\x36\x2e\x35\x36\x2c\x31\x2e\x37\x37\ +\x2c\x35\x30\x2e\x33\x34\x2d\x31\x34\x2e\x32\x38\x2c\x37\x32\x2e\ +\x33\x32\x2d\x36\x2e\x31\x31\x2c\x38\x2e\x33\x37\x2d\x31\x32\x2e\ +\x39\x2c\x31\x31\x2e\x37\x36\x2d\x32\x33\x2e\x38\x39\x2c\x39\x2e\ +\x37\x39\x2d\x32\x36\x2e\x39\x31\x2d\x34\x2e\x38\x33\x2d\x34\x39\ +\x2e\x33\x32\x2d\x31\x36\x2e\x39\x32\x2d\x36\x37\x2e\x34\x35\x2d\ +\x33\x37\x2e\x32\x31\x2d\x2e\x33\x38\x2d\x2e\x34\x32\x2d\x2e\x35\ +\x36\x2d\x31\x2e\x30\x33\x2d\x31\x2e\x31\x39\x2d\x32\x2e\x32\x36\ +\x2c\x31\x32\x2e\x30\x34\x2c\x31\x2e\x31\x39\x2c\x32\x33\x2e\x34\ +\x38\x2e\x32\x2c\x33\x34\x2e\x36\x34\x2d\x32\x2e\x38\x39\x2c\x31\ +\x38\x2e\x34\x39\x2d\x35\x2e\x31\x33\x2c\x33\x33\x2e\x33\x2d\x31\ +\x35\x2e\x32\x34\x2c\x34\x33\x2e\x33\x37\x2d\x33\x31\x2e\x39\x37\ +\x2c\x32\x2e\x35\x2d\x34\x2e\x31\x35\x2c\x32\x2e\x31\x32\x2d\x36\ +\x2e\x38\x2d\x31\x2e\x32\x34\x2d\x31\x30\x2e\x30\x39\x2d\x37\x2e\ +\x36\x32\x2d\x37\x2e\x34\x37\x2d\x37\x2e\x34\x34\x2d\x37\x2e\x34\ +\x39\x2d\x31\x37\x2e\x34\x38\x2d\x33\x2e\x34\x37\x2d\x32\x37\x2e\ +\x35\x36\x2c\x31\x31\x2e\x30\x32\x2d\x35\x31\x2e\x37\x34\x2c\x33\ +\x2e\x33\x34\x2d\x37\x35\x2e\x30\x33\x2d\x31\x32\x2e\x36\x35\x2d\ +\x31\x30\x2e\x37\x31\x2d\x37\x2e\x33\x35\x2d\x31\x30\x2e\x37\x39\ +\x2d\x31\x36\x2e\x32\x35\x2d\x38\x2e\x38\x39\x2d\x32\x37\x2e\x31\ +\x38\x2c\x34\x2e\x37\x32\x2d\x32\x37\x2e\x31\x34\x2c\x31\x37\x2e\ +\x33\x38\x2d\x34\x39\x2e\x35\x36\x2c\x33\x38\x2e\x32\x38\x2d\x36\ +\x37\x2e\x34\x31\x2e\x35\x35\x2d\x2e\x34\x37\x2c\x31\x2e\x31\x39\ +\x2d\x2e\x38\x33\x2c\x32\x2e\x34\x2d\x31\x2e\x36\x37\x2e\x36\x34\ +\x2c\x31\x30\x2e\x33\x33\x2d\x2e\x35\x38\x2c\x31\x39\x2e\x39\x32\ +\x2c\x31\x2e\x33\x31\x2c\x32\x39\x2e\x34\x31\x2c\x34\x2e\x32\x34\ +\x2c\x32\x31\x2e\x32\x39\x2c\x31\x34\x2e\x33\x31\x2c\x33\x38\x2e\ +\x35\x34\x2c\x33\x33\x2e\x31\x39\x2c\x35\x30\x2e\x31\x32\x2c\x33\ +\x2e\x35\x2c\x32\x2e\x31\x34\x2c\x35\x2e\x37\x36\x2c\x32\x2e\x33\ +\x35\x2c\x38\x2e\x38\x37\x2d\x2e\x39\x31\x2c\x37\x2e\x37\x36\x2d\ +\x38\x2e\x31\x34\x2c\x37\x2e\x36\x39\x2d\x37\x2e\x38\x36\x2c\x33\ +\x2e\x38\x2d\x31\x38\x2e\x35\x32\x2d\x39\x2e\x37\x37\x2d\x32\x36\ +\x2e\x38\x31\x2d\x32\x2e\x35\x31\x2d\x35\x30\x2e\x35\x31\x2c\x31\ +\x33\x2e\x34\x38\x2d\x37\x32\x2e\x36\x39\x2c\x36\x2e\x34\x31\x2d\ +\x38\x2e\x38\x39\x2c\x31\x33\x2e\x33\x35\x2d\x31\x33\x2c\x32\x35\ +\x2e\x32\x38\x2d\x31\x30\x2e\x37\x32\x2c\x32\x36\x2e\x36\x31\x2c\ +\x35\x2e\x30\x39\x2c\x34\x38\x2e\x38\x2c\x31\x37\x2e\x30\x39\x2c\ +\x36\x36\x2e\x38\x37\x2c\x33\x37\x2e\x30\x36\x2e\x33\x37\x2e\x34\ +\x2e\x34\x34\x2c\x31\x2e\x30\x37\x2c\x31\x2e\x30\x31\x2c\x32\x2e\ +\x35\x34\x5a\x4d\x32\x33\x35\x2e\x31\x39\x2c\x32\x38\x39\x2e\x36\ +\x35\x63\x33\x2e\x30\x31\x2e\x30\x39\x2c\x31\x33\x2e\x36\x39\x2d\ +\x31\x30\x2e\x34\x34\x2c\x31\x33\x2e\x38\x36\x2d\x31\x33\x2e\x36\ +\x36\x2e\x31\x37\x2d\x33\x2e\x32\x31\x2d\x31\x30\x2e\x30\x33\x2d\ +\x31\x33\x2e\x37\x32\x2d\x31\x33\x2e\x35\x31\x2d\x31\x33\x2e\x39\ +\x2d\x32\x2e\x39\x34\x2d\x2e\x31\x36\x2d\x31\x33\x2e\x38\x33\x2c\ +\x31\x30\x2e\x34\x36\x2d\x31\x33\x2e\x39\x32\x2c\x31\x33\x2e\x35\ +\x37\x2d\x2e\x30\x39\x2c\x33\x2e\x31\x37\x2c\x31\x30\x2e\x33\x31\ +\x2c\x31\x33\x2e\x39\x2c\x31\x33\x2e\x35\x37\x2c\x31\x33\x2e\x39\ +\x39\x5a\x22\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\ +\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\ +\x35\x33\x34\x2e\x39\x34\x2c\x33\x32\x37\x2e\x34\x34\x63\x2d\x33\ +\x2e\x30\x32\x2c\x31\x2e\x37\x38\x2d\x36\x2e\x30\x35\x2c\x33\x2e\ +\x35\x35\x2d\x39\x2e\x30\x36\x2c\x35\x2e\x33\x34\x2d\x31\x32\x2e\ +\x36\x36\x2c\x37\x2e\x35\x33\x2d\x32\x35\x2e\x33\x31\x2c\x31\x35\ +\x2e\x30\x36\x2d\x33\x37\x2e\x39\x36\x2c\x32\x32\x2e\x35\x39\x2d\ +\x2e\x31\x34\x2e\x30\x38\x2d\x2e\x32\x38\x2e\x31\x36\x2d\x2e\x35\ +\x36\x2e\x33\x31\x2c\x30\x2d\x31\x2e\x39\x33\x2d\x2e\x30\x34\x2d\ +\x31\x36\x2e\x31\x37\x2d\x2e\x30\x34\x2d\x31\x36\x2e\x31\x37\x68\ +\x2d\x31\x39\x2e\x32\x35\x76\x2d\x32\x33\x2e\x32\x39\x68\x31\x39\ +\x2e\x32\x35\x73\x2d\x2e\x30\x31\x2d\x31\x37\x2e\x30\x32\x2c\x30\ +\x2d\x31\x37\x2e\x31\x35\x63\x2e\x30\x36\x2c\x30\x2c\x38\x2e\x35\ +\x31\x2c\x34\x2e\x39\x34\x2c\x31\x32\x2e\x34\x2c\x37\x2e\x32\x36\ +\x2c\x31\x31\x2e\x37\x34\x2c\x36\x2e\x39\x38\x2c\x32\x33\x2e\x34\ +\x38\x2c\x31\x33\x2e\x39\x37\x2c\x33\x35\x2e\x32\x32\x2c\x32\x30\ +\x2e\x39\x35\x2c\x30\x2c\x2e\x30\x35\x2c\x30\x2c\x2e\x31\x2c\x30\ +\x2c\x2e\x31\x35\x5a\x22\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\ +\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\ +\x3d\x22\x4d\x35\x33\x34\x2e\x39\x34\x2c\x34\x36\x32\x2e\x32\x31\ +\x63\x2d\x33\x2e\x30\x32\x2c\x31\x2e\x37\x38\x2d\x36\x2e\x30\x35\ +\x2c\x33\x2e\x35\x35\x2d\x39\x2e\x30\x36\x2c\x35\x2e\x33\x34\x2d\ +\x31\x32\x2e\x36\x36\x2c\x37\x2e\x35\x33\x2d\x32\x35\x2e\x33\x31\ +\x2c\x31\x35\x2e\x30\x36\x2d\x33\x37\x2e\x39\x36\x2c\x32\x32\x2e\ +\x35\x39\x2d\x2e\x31\x34\x2e\x30\x38\x2d\x2e\x32\x38\x2e\x31\x36\ +\x2d\x2e\x35\x36\x2e\x33\x31\x2c\x30\x2d\x31\x2e\x39\x33\x2d\x2e\ +\x30\x34\x2d\x31\x36\x2e\x31\x37\x2d\x2e\x30\x34\x2d\x31\x36\x2e\ +\x31\x37\x68\x2d\x31\x39\x2e\x32\x35\x76\x2d\x32\x33\x2e\x32\x39\ +\x68\x31\x39\x2e\x32\x35\x73\x2d\x2e\x30\x31\x2d\x31\x37\x2e\x30\ +\x32\x2c\x30\x2d\x31\x37\x2e\x31\x35\x63\x2e\x30\x36\x2c\x30\x2c\ +\x38\x2e\x35\x31\x2c\x34\x2e\x39\x34\x2c\x31\x32\x2e\x34\x2c\x37\ +\x2e\x32\x36\x2c\x31\x31\x2e\x37\x34\x2c\x36\x2e\x39\x38\x2c\x32\ +\x33\x2e\x34\x38\x2c\x31\x33\x2e\x39\x37\x2c\x33\x35\x2e\x32\x32\ +\x2c\x32\x30\x2e\x39\x35\x2c\x30\x2c\x2e\x30\x35\x2c\x30\x2c\x2e\ +\x31\x2c\x30\x2c\x2e\x31\x35\x5a\x22\x2f\x3e\x0a\x20\x20\x3c\x70\ +\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\ +\x22\x20\x64\x3d\x22\x4d\x35\x33\x34\x2e\x39\x34\x2c\x33\x39\x34\ +\x2e\x38\x32\x63\x2d\x33\x2e\x30\x32\x2c\x31\x2e\x37\x38\x2d\x36\ +\x2e\x30\x35\x2c\x33\x2e\x35\x35\x2d\x39\x2e\x30\x36\x2c\x35\x2e\ +\x33\x34\x2d\x31\x32\x2e\x36\x36\x2c\x37\x2e\x35\x33\x2d\x32\x35\ +\x2e\x33\x31\x2c\x31\x35\x2e\x30\x36\x2d\x33\x37\x2e\x39\x36\x2c\ +\x32\x32\x2e\x35\x39\x2d\x2e\x31\x34\x2e\x30\x38\x2d\x2e\x32\x38\ +\x2e\x31\x36\x2d\x2e\x35\x36\x2e\x33\x31\x2c\x30\x2d\x31\x2e\x39\ +\x33\x2d\x2e\x30\x34\x2d\x31\x36\x2e\x31\x37\x2d\x2e\x30\x34\x2d\ +\x31\x36\x2e\x31\x37\x68\x2d\x31\x39\x2e\x32\x35\x76\x2d\x32\x33\ +\x2e\x32\x39\x68\x31\x39\x2e\x32\x35\x73\x2d\x2e\x30\x31\x2d\x31\ +\x37\x2e\x30\x32\x2c\x30\x2d\x31\x37\x2e\x31\x35\x63\x2e\x30\x36\ +\x2c\x30\x2c\x38\x2e\x35\x31\x2c\x34\x2e\x39\x34\x2c\x31\x32\x2e\ +\x34\x2c\x37\x2e\x32\x36\x2c\x31\x31\x2e\x37\x34\x2c\x36\x2e\x39\ +\x38\x2c\x32\x33\x2e\x34\x38\x2c\x31\x33\x2e\x39\x37\x2c\x33\x35\ +\x2e\x32\x32\x2c\x32\x30\x2e\x39\x35\x2c\x30\x2c\x2e\x30\x35\x2c\ +\x30\x2c\x2e\x31\x2c\x30\x2c\x2e\x31\x35\x5a\x22\x2f\x3e\x0a\x3c\ +\x2f\x73\x76\x67\x3e\ \x00\x00\x04\xf7\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -22506,7 +22518,7 @@ \x31\x2e\x36\x20\x34\x34\x36\x2e\x36\x32\x20\x34\x33\x36\x2e\x32\ \x36\x20\x34\x34\x38\x2e\x37\x33\x20\x34\x33\x38\x2e\x34\x20\x35\ \x32\x32\x2e\x36\x35\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x05\xdd\ +\x00\x00\x05\xcf\ \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\ @@ -22523,86 +22535,85 @@ \x0a\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x73\ \x74\x79\x6c\x65\x3e\x0a\x20\x20\x3c\x2f\x64\x65\x66\x73\x3e\x0a\ \x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x38\x30\x2e\x30\x34\ -\x2c\x31\x35\x30\x2e\x34\x37\x63\x2d\x33\x36\x2e\x33\x35\x2d\x33\ -\x2e\x36\x37\x2d\x36\x39\x2e\x38\x34\x2c\x31\x2e\x34\x2d\x31\x30\ -\x31\x2e\x36\x32\x2c\x31\x36\x2e\x32\x39\x2d\x32\x37\x2e\x31\x33\ -\x2c\x31\x32\x2e\x37\x31\x2d\x34\x38\x2e\x35\x38\x2c\x33\x31\x2e\ -\x36\x34\x2d\x36\x33\x2e\x38\x36\x2c\x35\x37\x2e\x36\x36\x2d\x34\ -\x2e\x39\x31\x2c\x38\x2e\x33\x36\x2d\x34\x2e\x39\x36\x2c\x31\x34\ -\x2e\x30\x38\x2c\x32\x2e\x37\x32\x2c\x32\x31\x2e\x32\x32\x2c\x31\ -\x36\x2e\x34\x31\x2c\x31\x35\x2e\x32\x35\x2c\x31\x35\x2e\x38\x31\ -\x2c\x31\x35\x2e\x33\x32\x2c\x33\x35\x2e\x39\x2c\x37\x2e\x34\x33\ -\x2c\x36\x31\x2e\x35\x38\x2d\x32\x34\x2e\x31\x39\x2c\x31\x31\x35\ -\x2e\x30\x35\x2d\x37\x2e\x30\x34\x2c\x31\x36\x35\x2e\x36\x35\x2c\ -\x33\x30\x2e\x38\x38\x2c\x31\x38\x2e\x33\x34\x2c\x31\x33\x2e\x37\ -\x34\x2c\x31\x37\x2e\x32\x32\x2c\x32\x39\x2e\x34\x39\x2c\x31\x34\ -\x2e\x35\x31\x2c\x34\x38\x2e\x30\x33\x2d\x38\x2e\x38\x2c\x36\x30\ -\x2e\x32\x35\x2d\x33\x36\x2e\x34\x2c\x31\x30\x39\x2e\x39\x2d\x38\ -\x32\x2e\x38\x2c\x31\x34\x39\x2e\x31\x38\x2d\x2e\x39\x34\x2e\x38\ -\x2d\x32\x2e\x31\x36\x2c\x31\x2e\x33\x2d\x33\x2e\x33\x2c\x31\x2e\ -\x38\x31\x2d\x2e\x33\x2e\x31\x33\x2d\x2e\x38\x31\x2d\x2e\x32\x32\ -\x2d\x31\x2e\x38\x38\x2d\x2e\x35\x35\x2c\x33\x2e\x36\x35\x2d\x33\ -\x34\x2e\x38\x36\x2e\x31\x39\x2d\x36\x39\x2e\x30\x31\x2d\x31\x34\ -\x2e\x34\x33\x2d\x31\x30\x31\x2e\x34\x2d\x31\x32\x2e\x39\x2d\x32\ -\x38\x2e\x35\x35\x2d\x33\x32\x2e\x35\x36\x2d\x35\x31\x2e\x31\x36\ -\x2d\x35\x39\x2e\x39\x39\x2d\x36\x36\x2e\x38\x33\x2d\x36\x2e\x31\ -\x37\x2d\x33\x2e\x35\x32\x2d\x39\x2e\x37\x35\x2d\x32\x2e\x37\x32\ -\x2d\x31\x35\x2e\x36\x36\x2c\x32\x2e\x30\x35\x2d\x31\x35\x2e\x34\ -\x35\x2c\x31\x32\x2e\x35\x2d\x31\x37\x2e\x31\x2c\x32\x33\x2e\x32\ -\x37\x2d\x39\x2e\x31\x2c\x34\x33\x2e\x31\x37\x2c\x32\x32\x2e\x36\ -\x39\x2c\x35\x36\x2e\x33\x39\x2c\x33\x2e\x37\x35\x2c\x31\x30\x36\ -\x2e\x38\x39\x2d\x33\x30\x2e\x33\x32\x2c\x31\x35\x33\x2e\x35\x36\ -\x2d\x31\x32\x2e\x39\x38\x2c\x31\x37\x2e\x37\x38\x2d\x32\x37\x2e\ -\x34\x2c\x32\x34\x2e\x39\x37\x2d\x35\x30\x2e\x37\x32\x2c\x32\x30\ -\x2e\x37\x38\x2d\x35\x37\x2e\x31\x33\x2d\x31\x30\x2e\x32\x36\x2d\ -\x31\x30\x34\x2e\x37\x32\x2d\x33\x35\x2e\x39\x31\x2d\x31\x34\x33\ -\x2e\x32\x31\x2d\x37\x39\x2e\x30\x31\x2d\x2e\x38\x31\x2d\x2e\x39\ -\x2d\x31\x2e\x31\x38\x2d\x32\x2e\x31\x39\x2d\x32\x2e\x35\x33\x2d\ -\x34\x2e\x37\x39\x2c\x32\x35\x2e\x35\x37\x2c\x32\x2e\x35\x34\x2c\ -\x34\x39\x2e\x38\x35\x2e\x34\x33\x2c\x37\x33\x2e\x35\x35\x2d\x36\ -\x2e\x31\x34\x2c\x33\x39\x2e\x32\x35\x2d\x31\x30\x2e\x38\x39\x2c\ -\x37\x30\x2e\x37\x2d\x33\x32\x2e\x33\x36\x2c\x39\x32\x2e\x30\x39\ -\x2d\x36\x37\x2e\x38\x38\x2c\x35\x2e\x33\x2d\x38\x2e\x38\x2c\x34\ -\x2e\x34\x39\x2d\x31\x34\x2e\x34\x34\x2d\x32\x2e\x36\x32\x2d\x32\ -\x31\x2e\x34\x32\x2d\x31\x36\x2e\x31\x37\x2d\x31\x35\x2e\x38\x37\ -\x2d\x31\x35\x2e\x37\x39\x2d\x31\x35\x2e\x38\x39\x2d\x33\x37\x2e\ -\x31\x32\x2d\x37\x2e\x33\x37\x2d\x35\x38\x2e\x35\x32\x2c\x32\x33\ -\x2e\x34\x2d\x31\x30\x39\x2e\x38\x34\x2c\x37\x2e\x31\x2d\x31\x35\ -\x39\x2e\x33\x2d\x32\x36\x2e\x38\x36\x2d\x32\x32\x2e\x37\x33\x2d\ -\x31\x35\x2e\x36\x31\x2d\x32\x32\x2e\x39\x31\x2d\x33\x34\x2e\x35\ -\x2d\x31\x38\x2e\x38\x38\x2d\x35\x37\x2e\x37\x31\x2c\x31\x30\x2e\ -\x30\x31\x2d\x35\x37\x2e\x36\x32\x2c\x33\x36\x2e\x39\x31\x2d\x31\ -\x30\x35\x2e\x32\x32\x2c\x38\x31\x2e\x32\x37\x2d\x31\x34\x33\x2e\ -\x31\x32\x2c\x31\x2e\x31\x37\x2d\x31\x2c\x32\x2e\x35\x33\x2d\x31\ -\x2e\x37\x37\x2c\x35\x2e\x31\x2d\x33\x2e\x35\x35\x2c\x31\x2e\x33\ -\x36\x2c\x32\x31\x2e\x39\x33\x2d\x31\x2e\x32\x34\x2c\x34\x32\x2e\ -\x33\x2c\x32\x2e\x37\x37\x2c\x36\x32\x2e\x34\x35\x2c\x39\x2e\x30\ -\x31\x2c\x34\x35\x2e\x31\x39\x2c\x33\x30\x2e\x33\x37\x2c\x38\x31\ -\x2e\x38\x32\x2c\x37\x30\x2e\x34\x37\x2c\x31\x30\x36\x2e\x34\x31\ -\x2c\x37\x2e\x34\x32\x2c\x34\x2e\x35\x35\x2c\x31\x32\x2e\x32\x32\ -\x2c\x35\x2c\x31\x38\x2e\x38\x33\x2d\x31\x2e\x39\x34\x2c\x31\x36\ -\x2e\x34\x38\x2d\x31\x37\x2e\x32\x38\x2c\x31\x36\x2e\x33\x33\x2d\ -\x31\x36\x2e\x36\x38\x2c\x38\x2e\x30\x38\x2d\x33\x39\x2e\x33\x33\ -\x2d\x32\x30\x2e\x37\x35\x2d\x35\x36\x2e\x39\x33\x2d\x35\x2e\x33\ -\x33\x2d\x31\x30\x37\x2e\x32\x35\x2c\x32\x38\x2e\x36\x32\x2d\x31\ -\x35\x34\x2e\x33\x34\x2c\x31\x33\x2e\x36\x31\x2d\x31\x38\x2e\x38\ -\x38\x2c\x32\x38\x2e\x33\x34\x2d\x32\x37\x2e\x36\x31\x2c\x35\x33\ -\x2e\x36\x37\x2d\x32\x32\x2e\x37\x36\x2c\x35\x36\x2e\x34\x39\x2c\ -\x31\x30\x2e\x38\x31\x2c\x31\x30\x33\x2e\x36\x32\x2c\x33\x36\x2e\ -\x32\x38\x2c\x31\x34\x31\x2e\x39\x37\x2c\x37\x38\x2e\x36\x39\x2e\ -\x37\x38\x2e\x38\x36\x2e\x39\x33\x2c\x32\x2e\x32\x37\x2c\x32\x2e\ -\x31\x34\x2c\x35\x2e\x33\x39\x5a\x4d\x32\x39\x39\x2e\x36\x2c\x33\ -\x32\x39\x2e\x33\x35\x63\x36\x2e\x33\x39\x2e\x31\x38\x2c\x32\x39\ -\x2e\x30\x37\x2d\x32\x32\x2e\x31\x37\x2c\x32\x39\x2e\x34\x33\x2d\ -\x32\x39\x2c\x2e\x33\x36\x2d\x36\x2e\x38\x33\x2d\x32\x31\x2e\x33\ -\x2d\x32\x39\x2e\x31\x33\x2d\x32\x38\x2e\x36\x37\x2d\x32\x39\x2e\ -\x35\x32\x2d\x36\x2e\x32\x35\x2d\x2e\x33\x33\x2d\x32\x39\x2e\x33\ -\x36\x2c\x32\x32\x2e\x32\x2d\x32\x39\x2e\x35\x36\x2c\x32\x38\x2e\ -\x38\x32\x2d\x2e\x32\x2c\x36\x2e\x37\x33\x2c\x32\x31\x2e\x38\x38\ -\x2c\x32\x39\x2e\x35\x31\x2c\x32\x38\x2e\x38\x2c\x32\x39\x2e\x37\ -\x31\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x0b\x8f\ +\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x32\x39\x2e\x30\x34\ +\x2c\x31\x39\x32\x2e\x38\x33\x63\x2d\x32\x36\x2e\x30\x35\x2d\x32\ +\x2e\x36\x33\x2d\x35\x30\x2e\x30\x35\x2c\x31\x2e\x30\x31\x2d\x37\ +\x32\x2e\x38\x33\x2c\x31\x31\x2e\x36\x38\x2d\x31\x39\x2e\x34\x35\ +\x2c\x39\x2e\x31\x31\x2d\x33\x34\x2e\x38\x32\x2c\x32\x32\x2e\x36\ +\x38\x2d\x34\x35\x2e\x37\x37\x2c\x34\x31\x2e\x33\x32\x2d\x33\x2e\ +\x35\x32\x2c\x35\x2e\x39\x39\x2d\x33\x2e\x35\x36\x2c\x31\x30\x2e\ +\x30\x39\x2c\x31\x2e\x39\x35\x2c\x31\x35\x2e\x32\x31\x2c\x31\x31\ +\x2e\x37\x36\x2c\x31\x30\x2e\x39\x33\x2c\x31\x31\x2e\x33\x33\x2c\ +\x31\x30\x2e\x39\x38\x2c\x32\x35\x2e\x37\x33\x2c\x35\x2e\x33\x32\ +\x2c\x34\x34\x2e\x31\x33\x2d\x31\x37\x2e\x33\x34\x2c\x38\x32\x2e\ +\x34\x36\x2d\x35\x2e\x30\x35\x2c\x31\x31\x38\x2e\x37\x32\x2c\x32\ +\x32\x2e\x31\x33\x2c\x31\x33\x2e\x31\x34\x2c\x39\x2e\x38\x35\x2c\ +\x31\x32\x2e\x33\x34\x2c\x32\x31\x2e\x31\x33\x2c\x31\x30\x2e\x34\ +\x2c\x33\x34\x2e\x34\x32\x2d\x36\x2e\x33\x31\x2c\x34\x33\x2e\x31\ +\x38\x2d\x32\x36\x2e\x30\x39\x2c\x37\x38\x2e\x37\x37\x2d\x35\x39\ +\x2e\x33\x34\x2c\x31\x30\x36\x2e\x39\x32\x2d\x2e\x36\x37\x2e\x35\ +\x37\x2d\x31\x2e\x35\x34\x2e\x39\x33\x2d\x32\x2e\x33\x36\x2c\x31\ +\x2e\x33\x2d\x2e\x32\x31\x2e\x31\x2d\x2e\x35\x38\x2d\x2e\x31\x36\ +\x2d\x31\x2e\x33\x35\x2d\x2e\x34\x2c\x32\x2e\x36\x32\x2d\x32\x34\ +\x2e\x39\x38\x2e\x31\x34\x2d\x34\x39\x2e\x34\x36\x2d\x31\x30\x2e\ +\x33\x34\x2d\x37\x32\x2e\x36\x37\x2d\x39\x2e\x32\x34\x2d\x32\x30\ +\x2e\x34\x36\x2d\x32\x33\x2e\x33\x34\x2d\x33\x36\x2e\x36\x37\x2d\ +\x34\x33\x2d\x34\x37\x2e\x39\x2d\x34\x2e\x34\x32\x2d\x32\x2e\x35\ +\x33\x2d\x36\x2e\x39\x39\x2d\x31\x2e\x39\x35\x2d\x31\x31\x2e\x32\ +\x32\x2c\x31\x2e\x34\x37\x2d\x31\x31\x2e\x30\x37\x2c\x38\x2e\x39\ +\x36\x2d\x31\x32\x2e\x32\x36\x2c\x31\x36\x2e\x36\x38\x2d\x36\x2e\ +\x35\x32\x2c\x33\x30\x2e\x39\x34\x2c\x31\x36\x2e\x32\x36\x2c\x34\ +\x30\x2e\x34\x32\x2c\x32\x2e\x36\x39\x2c\x37\x36\x2e\x36\x31\x2d\ +\x32\x31\x2e\x37\x33\x2c\x31\x31\x30\x2e\x30\x36\x2d\x39\x2e\x33\ +\x2c\x31\x32\x2e\x37\x34\x2d\x31\x39\x2e\x36\x34\x2c\x31\x37\x2e\ +\x39\x2d\x33\x36\x2e\x33\x35\x2c\x31\x34\x2e\x38\x39\x2d\x34\x30\ +\x2e\x39\x35\x2d\x37\x2e\x33\x35\x2d\x37\x35\x2e\x30\x36\x2d\x32\ +\x35\x2e\x37\x34\x2d\x31\x30\x32\x2e\x36\x34\x2d\x35\x36\x2e\x36\ +\x33\x2d\x2e\x35\x38\x2d\x2e\x36\x35\x2d\x2e\x38\x35\x2d\x31\x2e\ +\x35\x37\x2d\x31\x2e\x38\x32\x2d\x33\x2e\x34\x33\x2c\x31\x38\x2e\ +\x33\x32\x2c\x31\x2e\x38\x32\x2c\x33\x35\x2e\x37\x33\x2e\x33\x31\ +\x2c\x35\x32\x2e\x37\x32\x2d\x34\x2e\x34\x2c\x32\x38\x2e\x31\x33\ +\x2d\x37\x2e\x38\x2c\x35\x30\x2e\x36\x37\x2d\x32\x33\x2e\x31\x39\ +\x2c\x36\x36\x2d\x34\x38\x2e\x36\x35\x2c\x33\x2e\x38\x2d\x36\x2e\ +\x33\x31\x2c\x33\x2e\x32\x32\x2d\x31\x30\x2e\x33\x35\x2d\x31\x2e\ +\x38\x38\x2d\x31\x35\x2e\x33\x35\x2d\x31\x31\x2e\x35\x39\x2d\x31\ +\x31\x2e\x33\x37\x2d\x31\x31\x2e\x33\x32\x2d\x31\x31\x2e\x33\x39\ +\x2d\x32\x36\x2e\x36\x31\x2d\x35\x2e\x32\x38\x2d\x34\x31\x2e\x39\ +\x34\x2c\x31\x36\x2e\x37\x37\x2d\x37\x38\x2e\x37\x33\x2c\x35\x2e\ +\x30\x39\x2d\x31\x31\x34\x2e\x31\x37\x2d\x31\x39\x2e\x32\x35\x2d\ +\x31\x36\x2e\x32\x39\x2d\x31\x31\x2e\x31\x39\x2d\x31\x36\x2e\x34\ +\x32\x2d\x32\x34\x2e\x37\x33\x2d\x31\x33\x2e\x35\x33\x2d\x34\x31\ +\x2e\x33\x36\x2c\x37\x2e\x31\x38\x2d\x34\x31\x2e\x32\x39\x2c\x32\ +\x36\x2e\x34\x35\x2d\x37\x35\x2e\x34\x32\x2c\x35\x38\x2e\x32\x34\ +\x2d\x31\x30\x32\x2e\x35\x38\x2e\x38\x34\x2d\x2e\x37\x31\x2c\x31\ +\x2e\x38\x31\x2d\x31\x2e\x32\x37\x2c\x33\x2e\x36\x35\x2d\x32\x2e\ +\x35\x34\x2e\x39\x38\x2c\x31\x35\x2e\x37\x32\x2d\x2e\x38\x39\x2c\ +\x33\x30\x2e\x33\x31\x2c\x31\x2e\x39\x39\x2c\x34\x34\x2e\x37\x36\ +\x2c\x36\x2e\x34\x36\x2c\x33\x32\x2e\x33\x39\x2c\x32\x31\x2e\x37\ +\x37\x2c\x35\x38\x2e\x36\x34\x2c\x35\x30\x2e\x35\x31\x2c\x37\x36\ +\x2e\x32\x36\x2c\x35\x2e\x33\x32\x2c\x33\x2e\x32\x36\x2c\x38\x2e\ +\x37\x36\x2c\x33\x2e\x35\x38\x2c\x31\x33\x2e\x35\x2d\x31\x2e\x33\ +\x39\x2c\x31\x31\x2e\x38\x31\x2d\x31\x32\x2e\x33\x38\x2c\x31\x31\ +\x2e\x37\x31\x2d\x31\x31\x2e\x39\x36\x2c\x35\x2e\x37\x39\x2d\x32\ +\x38\x2e\x31\x39\x2d\x31\x34\x2e\x38\x37\x2d\x34\x30\x2e\x38\x2d\ +\x33\x2e\x38\x32\x2d\x37\x36\x2e\x38\x36\x2c\x32\x30\x2e\x35\x31\ +\x2d\x31\x31\x30\x2e\x36\x32\x2c\x39\x2e\x37\x35\x2d\x31\x33\x2e\ +\x35\x33\x2c\x32\x30\x2e\x33\x31\x2d\x31\x39\x2e\x37\x39\x2c\x33\ +\x38\x2e\x34\x37\x2d\x31\x36\x2e\x33\x31\x2c\x34\x30\x2e\x34\x39\ +\x2c\x37\x2e\x37\x34\x2c\x37\x34\x2e\x32\x36\x2c\x32\x36\x2c\x31\ +\x30\x31\x2e\x37\x35\x2c\x35\x36\x2e\x34\x2e\x35\x36\x2e\x36\x31\ +\x2e\x36\x37\x2c\x31\x2e\x36\x33\x2c\x31\x2e\x35\x33\x2c\x33\x2e\ +\x38\x36\x5a\x4d\x32\x39\x39\x2e\x37\x32\x2c\x33\x32\x31\x2e\x30\ +\x33\x63\x34\x2e\x35\x38\x2e\x31\x33\x2c\x32\x30\x2e\x38\x33\x2d\ +\x31\x35\x2e\x38\x39\x2c\x32\x31\x2e\x30\x39\x2d\x32\x30\x2e\x37\ +\x39\x2e\x32\x36\x2d\x34\x2e\x38\x39\x2d\x31\x35\x2e\x32\x37\x2d\ +\x32\x30\x2e\x38\x38\x2d\x32\x30\x2e\x35\x35\x2d\x32\x31\x2e\x31\ +\x36\x2d\x34\x2e\x34\x38\x2d\x2e\x32\x34\x2d\x32\x31\x2e\x30\x34\ +\x2c\x31\x35\x2e\x39\x31\x2d\x32\x31\x2e\x31\x38\x2c\x32\x30\x2e\ +\x36\x35\x2d\x2e\x31\x34\x2c\x34\x2e\x38\x33\x2c\x31\x35\x2e\x36\ +\x38\x2c\x32\x31\x2e\x31\x35\x2c\x32\x30\x2e\x36\x34\x2c\x32\x31\ +\x2e\x32\x39\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x0b\x76\ \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\ @@ -22619,176 +22630,175 @@ \x0a\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x73\ \x74\x79\x6c\x65\x3e\x0a\x20\x20\x3c\x2f\x64\x65\x66\x73\x3e\x0a\ \x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x35\x37\x2e\x37\x39\ -\x2c\x33\x31\x38\x2e\x30\x33\x63\x2e\x34\x37\x2d\x31\x32\x2e\x30\ -\x31\x2c\x35\x2e\x32\x34\x2d\x31\x37\x2e\x36\x2c\x31\x35\x2e\x30\ -\x31\x2d\x31\x36\x2e\x39\x36\x2c\x31\x30\x2e\x38\x36\x2e\x37\x2c\ -\x31\x33\x2e\x33\x38\x2c\x37\x2e\x38\x35\x2c\x31\x33\x2e\x33\x37\ -\x2c\x31\x37\x2e\x33\x39\x2d\x2e\x31\x31\x2c\x36\x36\x2e\x36\x39\ -\x2d\x2e\x30\x32\x2c\x31\x33\x33\x2e\x33\x38\x2d\x2e\x31\x31\x2c\ -\x32\x30\x30\x2e\x30\x38\x2d\x2e\x30\x31\x2c\x31\x31\x2e\x31\x34\ -\x2d\x35\x2e\x30\x32\x2c\x31\x36\x2e\x37\x2d\x31\x34\x2e\x33\x33\ -\x2c\x31\x36\x2e\x33\x37\x2d\x31\x30\x2e\x30\x36\x2d\x2e\x33\x35\ -\x2d\x31\x34\x2e\x35\x39\x2d\x36\x2e\x35\x38\x2d\x31\x33\x2e\x37\ -\x33\x2d\x31\x35\x2e\x37\x37\x2c\x31\x2e\x30\x36\x2d\x31\x31\x2e\ -\x33\x33\x2d\x33\x2e\x34\x31\x2d\x31\x33\x2e\x37\x36\x2d\x31\x34\ -\x2e\x31\x35\x2d\x31\x33\x2e\x36\x39\x2d\x36\x35\x2e\x31\x36\x2e\ -\x34\x38\x2d\x31\x33\x30\x2e\x33\x33\x2e\x33\x39\x2d\x31\x39\x35\ -\x2e\x35\x2e\x31\x33\x2d\x34\x34\x2e\x30\x39\x2d\x2e\x31\x38\x2d\ -\x38\x32\x2e\x33\x35\x2d\x31\x35\x2e\x37\x36\x2d\x31\x31\x35\x2e\ -\x34\x32\x2d\x34\x34\x2e\x37\x38\x2d\x37\x2e\x31\x2d\x36\x2e\x32\ -\x33\x2d\x31\x34\x2e\x37\x35\x2d\x31\x31\x2e\x39\x35\x2d\x32\x32\ -\x2e\x36\x38\x2d\x31\x37\x2e\x30\x37\x43\x34\x30\x2e\x35\x32\x2c\ -\x33\x39\x38\x2e\x36\x34\x2c\x34\x2e\x33\x34\x2c\x33\x31\x34\x2e\ -\x39\x39\x2c\x31\x39\x2e\x34\x33\x2c\x32\x33\x33\x2e\x31\x37\x2c\ -\x34\x31\x2e\x34\x34\x2c\x31\x31\x33\x2e\x37\x37\x2c\x31\x36\x33\ -\x2e\x35\x38\x2c\x33\x39\x2e\x30\x31\x2c\x32\x38\x30\x2e\x32\x33\ -\x2c\x37\x33\x2e\x35\x34\x63\x38\x32\x2e\x36\x36\x2c\x32\x34\x2e\ -\x34\x37\x2c\x31\x34\x31\x2e\x33\x2c\x39\x36\x2e\x39\x2c\x31\x34\ -\x36\x2e\x31\x31\x2c\x31\x38\x33\x2e\x30\x34\x2c\x31\x2e\x33\x33\ -\x2c\x32\x33\x2e\x38\x39\x2d\x32\x2e\x32\x32\x2c\x34\x38\x2e\x30\ -\x36\x2d\x33\x2e\x35\x36\x2c\x37\x32\x2e\x36\x32\x2c\x39\x2e\x36\ -\x38\x2c\x30\x2c\x32\x31\x2e\x33\x36\x2c\x30\x2c\x33\x34\x2e\x34\ -\x33\x2c\x30\x2c\x2e\x32\x34\x2d\x34\x2e\x34\x37\x2e\x34\x35\x2d\ -\x37\x2e\x38\x31\x2e\x35\x38\x2d\x31\x31\x2e\x31\x36\x5a\x4d\x32\ -\x32\x32\x2e\x39\x39\x2c\x39\x33\x2e\x38\x38\x63\x2d\x39\x37\x2e\ -\x37\x36\x2d\x2e\x34\x35\x2d\x31\x37\x38\x2e\x39\x38\x2c\x38\x30\ -\x2e\x37\x39\x2d\x31\x37\x38\x2e\x30\x33\x2c\x31\x37\x38\x2e\x30\ -\x36\x2e\x39\x35\x2c\x39\x36\x2e\x38\x33\x2c\x38\x30\x2e\x32\x34\ -\x2c\x31\x37\x35\x2e\x37\x34\x2c\x31\x37\x36\x2e\x36\x2c\x31\x37\ -\x35\x2e\x37\x35\x2c\x39\x38\x2c\x30\x2c\x31\x37\x37\x2e\x32\x35\ -\x2d\x37\x39\x2e\x32\x33\x2c\x31\x37\x37\x2e\x32\x36\x2d\x31\x37\ -\x37\x2e\x32\x33\x2c\x30\x2d\x39\x36\x2e\x30\x33\x2d\x37\x39\x2e\ -\x37\x37\x2d\x31\x37\x36\x2e\x31\x34\x2d\x31\x37\x35\x2e\x38\x34\ -\x2d\x31\x37\x36\x2e\x35\x38\x5a\x22\x2f\x3e\x0a\x20\x20\x3c\x70\ -\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\ -\x22\x20\x64\x3d\x22\x4d\x33\x32\x34\x2e\x33\x38\x2c\x31\x38\x35\ -\x2e\x36\x36\x63\x2d\x32\x30\x2e\x36\x39\x2d\x32\x2e\x30\x39\x2d\ -\x33\x39\x2e\x37\x36\x2e\x38\x2d\x35\x37\x2e\x38\x35\x2c\x39\x2e\ -\x32\x37\x2d\x31\x35\x2e\x34\x35\x2c\x37\x2e\x32\x34\x2d\x32\x37\ -\x2e\x36\x35\x2c\x31\x38\x2e\x30\x31\x2d\x33\x36\x2e\x33\x35\x2c\ -\x33\x32\x2e\x38\x32\x2d\x32\x2e\x38\x2c\x34\x2e\x37\x36\x2d\x32\ -\x2e\x38\x33\x2c\x38\x2e\x30\x31\x2c\x31\x2e\x35\x35\x2c\x31\x32\ -\x2e\x30\x38\x2c\x39\x2e\x33\x34\x2c\x38\x2e\x36\x38\x2c\x39\x2c\ -\x38\x2e\x37\x32\x2c\x32\x30\x2e\x34\x34\x2c\x34\x2e\x32\x33\x2c\ -\x33\x35\x2e\x30\x35\x2d\x31\x33\x2e\x37\x37\x2c\x36\x35\x2e\x34\ -\x39\x2d\x34\x2e\x30\x31\x2c\x39\x34\x2e\x33\x2c\x31\x37\x2e\x35\ -\x38\x2c\x31\x30\x2e\x34\x34\x2c\x37\x2e\x38\x32\x2c\x39\x2e\x38\ -\x2c\x31\x36\x2e\x37\x39\x2c\x38\x2e\x32\x36\x2c\x32\x37\x2e\x33\ -\x34\x2d\x35\x2e\x30\x31\x2c\x33\x34\x2e\x33\x2d\x32\x30\x2e\x37\ -\x32\x2c\x36\x32\x2e\x35\x36\x2d\x34\x37\x2e\x31\x34\x2c\x38\x34\ -\x2e\x39\x33\x2d\x2e\x35\x34\x2e\x34\x35\x2d\x31\x2e\x32\x33\x2e\ -\x37\x34\x2d\x31\x2e\x38\x38\x2c\x31\x2e\x30\x33\x2d\x2e\x31\x37\ -\x2e\x30\x38\x2d\x2e\x34\x36\x2d\x2e\x31\x33\x2d\x31\x2e\x30\x37\ -\x2d\x2e\x33\x31\x2c\x32\x2e\x30\x38\x2d\x31\x39\x2e\x38\x34\x2e\ -\x31\x31\x2d\x33\x39\x2e\x32\x39\x2d\x38\x2e\x32\x32\x2d\x35\x37\ -\x2e\x37\x32\x2d\x37\x2e\x33\x34\x2d\x31\x36\x2e\x32\x35\x2d\x31\ -\x38\x2e\x35\x33\x2d\x32\x39\x2e\x31\x32\x2d\x33\x34\x2e\x31\x35\ -\x2d\x33\x38\x2e\x30\x34\x2d\x33\x2e\x35\x31\x2d\x32\x2e\x30\x31\ -\x2d\x35\x2e\x35\x35\x2d\x31\x2e\x35\x35\x2d\x38\x2e\x39\x31\x2c\ -\x31\x2e\x31\x37\x2d\x38\x2e\x37\x39\x2c\x37\x2e\x31\x31\x2d\x39\ -\x2e\x37\x34\x2c\x31\x33\x2e\x32\x35\x2d\x35\x2e\x31\x38\x2c\x32\ -\x34\x2e\x35\x37\x2c\x31\x32\x2e\x39\x32\x2c\x33\x32\x2e\x31\x2c\ -\x32\x2e\x31\x33\x2c\x36\x30\x2e\x38\x35\x2d\x31\x37\x2e\x32\x36\ -\x2c\x38\x37\x2e\x34\x32\x2d\x37\x2e\x33\x39\x2c\x31\x30\x2e\x31\ -\x32\x2d\x31\x35\x2e\x36\x2c\x31\x34\x2e\x32\x31\x2d\x32\x38\x2e\ -\x38\x37\x2c\x31\x31\x2e\x38\x33\x2d\x33\x32\x2e\x35\x32\x2d\x35\ -\x2e\x38\x34\x2d\x35\x39\x2e\x36\x32\x2d\x32\x30\x2e\x34\x35\x2d\ -\x38\x31\x2e\x35\x33\x2d\x34\x34\x2e\x39\x38\x2d\x2e\x34\x36\x2d\ -\x2e\x35\x31\x2d\x2e\x36\x37\x2d\x31\x2e\x32\x35\x2d\x31\x2e\x34\ -\x34\x2d\x32\x2e\x37\x33\x2c\x31\x34\x2e\x35\x35\x2c\x31\x2e\x34\ -\x34\x2c\x32\x38\x2e\x33\x38\x2e\x32\x35\x2c\x34\x31\x2e\x38\x37\ -\x2d\x33\x2e\x35\x2c\x32\x32\x2e\x33\x35\x2d\x36\x2e\x32\x2c\x34\ -\x30\x2e\x32\x35\x2d\x31\x38\x2e\x34\x32\x2c\x35\x32\x2e\x34\x32\ -\x2d\x33\x38\x2e\x36\x34\x2c\x33\x2e\x30\x32\x2d\x35\x2e\x30\x31\ -\x2c\x32\x2e\x35\x36\x2d\x38\x2e\x32\x32\x2d\x31\x2e\x34\x39\x2d\ -\x31\x32\x2e\x31\x39\x2d\x39\x2e\x32\x31\x2d\x39\x2e\x30\x33\x2d\ -\x38\x2e\x39\x39\x2d\x39\x2e\x30\x35\x2d\x32\x31\x2e\x31\x33\x2d\ -\x34\x2e\x31\x39\x2d\x33\x33\x2e\x33\x31\x2c\x31\x33\x2e\x33\x32\ -\x2d\x36\x32\x2e\x35\x33\x2c\x34\x2e\x30\x34\x2d\x39\x30\x2e\x36\ -\x39\x2d\x31\x35\x2e\x32\x39\x2d\x31\x32\x2e\x39\x34\x2d\x38\x2e\ -\x38\x39\x2d\x31\x33\x2e\x30\x34\x2d\x31\x39\x2e\x36\x34\x2d\x31\ -\x30\x2e\x37\x35\x2d\x33\x32\x2e\x38\x35\x2c\x35\x2e\x37\x2d\x33\ -\x32\x2e\x38\x2c\x32\x31\x2e\x30\x31\x2d\x35\x39\x2e\x39\x2c\x34\ -\x36\x2e\x32\x36\x2d\x38\x31\x2e\x34\x38\x2e\x36\x36\x2d\x2e\x35\ -\x37\x2c\x31\x2e\x34\x34\x2d\x31\x2e\x30\x31\x2c\x32\x2e\x39\x2d\ -\x32\x2e\x30\x32\x2e\x37\x38\x2c\x31\x32\x2e\x34\x39\x2d\x2e\x37\ -\x31\x2c\x32\x34\x2e\x30\x38\x2c\x31\x2e\x35\x38\x2c\x33\x35\x2e\ -\x35\x35\x2c\x35\x2e\x31\x33\x2c\x32\x35\x2e\x37\x33\x2c\x31\x37\ -\x2e\x32\x39\x2c\x34\x36\x2e\x35\x38\x2c\x34\x30\x2e\x31\x32\x2c\ -\x36\x30\x2e\x35\x37\x2c\x34\x2e\x32\x33\x2c\x32\x2e\x35\x39\x2c\ -\x36\x2e\x39\x36\x2c\x32\x2e\x38\x34\x2c\x31\x30\x2e\x37\x32\x2d\ -\x31\x2e\x31\x2c\x39\x2e\x33\x38\x2d\x39\x2e\x38\x34\x2c\x39\x2e\ -\x33\x2d\x39\x2e\x35\x2c\x34\x2e\x36\x2d\x32\x32\x2e\x33\x39\x2d\ -\x31\x31\x2e\x38\x31\x2d\x33\x32\x2e\x34\x31\x2d\x33\x2e\x30\x34\ -\x2d\x36\x31\x2e\x30\x35\x2c\x31\x36\x2e\x32\x39\x2d\x38\x37\x2e\ -\x38\x36\x2c\x37\x2e\x37\x35\x2d\x31\x30\x2e\x37\x35\x2c\x31\x36\ -\x2e\x31\x33\x2d\x31\x35\x2e\x37\x31\x2c\x33\x30\x2e\x35\x35\x2d\ -\x31\x32\x2e\x39\x36\x2c\x33\x32\x2e\x31\x36\x2c\x36\x2e\x31\x35\ -\x2c\x35\x38\x2e\x39\x39\x2c\x32\x30\x2e\x36\x35\x2c\x38\x30\x2e\ -\x38\x32\x2c\x34\x34\x2e\x38\x2e\x34\x34\x2e\x34\x39\x2e\x35\x33\ -\x2c\x31\x2e\x32\x39\x2c\x31\x2e\x32\x32\x2c\x33\x2e\x30\x37\x5a\ -\x4d\x32\x32\x31\x2e\x36\x36\x2c\x32\x38\x37\x2e\x34\x39\x63\x33\ -\x2e\x36\x34\x2e\x31\x2c\x31\x36\x2e\x35\x35\x2d\x31\x32\x2e\x36\ -\x32\x2c\x31\x36\x2e\x37\x35\x2d\x31\x36\x2e\x35\x31\x2e\x32\x2d\ -\x33\x2e\x38\x39\x2d\x31\x32\x2e\x31\x33\x2d\x31\x36\x2e\x35\x38\ -\x2d\x31\x36\x2e\x33\x32\x2d\x31\x36\x2e\x38\x2d\x33\x2e\x35\x36\ -\x2d\x2e\x31\x39\x2d\x31\x36\x2e\x37\x31\x2c\x31\x32\x2e\x36\x34\ -\x2d\x31\x36\x2e\x38\x33\x2c\x31\x36\x2e\x34\x2d\x2e\x31\x31\x2c\ -\x33\x2e\x38\x33\x2c\x31\x32\x2e\x34\x36\x2c\x31\x36\x2e\x38\x2c\ -\x31\x36\x2e\x34\x2c\x31\x36\x2e\x39\x31\x5a\x22\x2f\x3e\x0a\x20\ -\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x35\x38\x33\x2e\x39\x36\x2c\ -\x33\x33\x33\x2e\x31\x36\x63\x2d\x33\x2e\x36\x35\x2c\x32\x2e\x31\ -\x35\x2d\x37\x2e\x33\x31\x2c\x34\x2e\x32\x39\x2d\x31\x30\x2e\x39\ -\x35\x2c\x36\x2e\x34\x35\x2d\x31\x35\x2e\x33\x2c\x39\x2e\x31\x2d\ -\x33\x30\x2e\x35\x39\x2c\x31\x38\x2e\x32\x2d\x34\x35\x2e\x38\x38\ -\x2c\x32\x37\x2e\x33\x31\x2d\x2e\x31\x37\x2e\x31\x2d\x2e\x33\x34\ -\x2e\x31\x39\x2d\x2e\x36\x37\x2e\x33\x37\x2c\x30\x2d\x32\x2e\x33\ -\x34\x2d\x2e\x30\x35\x2d\x31\x39\x2e\x35\x35\x2d\x2e\x30\x35\x2d\ -\x31\x39\x2e\x35\x35\x68\x2d\x32\x33\x2e\x32\x36\x76\x2d\x32\x38\ -\x2e\x31\x35\x68\x32\x33\x2e\x32\x36\x73\x2d\x2e\x30\x32\x2d\x32\ -\x30\x2e\x35\x37\x2c\x30\x2d\x32\x30\x2e\x37\x32\x63\x2e\x30\x37\ -\x2c\x30\x2c\x31\x30\x2e\x32\x38\x2c\x35\x2e\x39\x37\x2c\x31\x34\ -\x2e\x39\x39\x2c\x38\x2e\x37\x37\x2c\x31\x34\x2e\x31\x39\x2c\x38\ -\x2e\x34\x34\x2c\x32\x38\x2e\x33\x38\x2c\x31\x36\x2e\x38\x38\x2c\ -\x34\x32\x2e\x35\x37\x2c\x32\x35\x2e\x33\x32\x2c\x30\x2c\x2e\x30\ -\x36\x2c\x30\x2c\x2e\x31\x32\x2c\x30\x2c\x2e\x31\x39\x5a\x22\x2f\ +\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x33\x30\x2e\x35\x35\ +\x2c\x33\x31\x34\x2e\x39\x32\x63\x2e\x33\x39\x2d\x39\x2e\x39\x34\ +\x2c\x34\x2e\x33\x33\x2d\x31\x34\x2e\x35\x36\x2c\x31\x32\x2e\x34\ +\x32\x2d\x31\x34\x2e\x30\x34\x2c\x38\x2e\x39\x39\x2e\x35\x38\x2c\ +\x31\x31\x2e\x30\x37\x2c\x36\x2e\x35\x2c\x31\x31\x2e\x30\x36\x2c\ +\x31\x34\x2e\x33\x38\x2d\x2e\x30\x39\x2c\x35\x35\x2e\x31\x38\x2d\ +\x2e\x30\x32\x2c\x31\x31\x30\x2e\x33\x36\x2d\x2e\x30\x39\x2c\x31\ +\x36\x35\x2e\x35\x33\x2d\x2e\x30\x31\x2c\x39\x2e\x32\x31\x2d\x34\ +\x2e\x31\x35\x2c\x31\x33\x2e\x38\x31\x2d\x31\x31\x2e\x38\x35\x2c\ +\x31\x33\x2e\x35\x35\x2d\x38\x2e\x33\x32\x2d\x2e\x32\x39\x2d\x31\ +\x32\x2e\x30\x37\x2d\x35\x2e\x34\x35\x2d\x31\x31\x2e\x33\x36\x2d\ +\x31\x33\x2e\x30\x34\x2e\x38\x38\x2d\x39\x2e\x33\x38\x2d\x32\x2e\ +\x38\x32\x2d\x31\x31\x2e\x33\x39\x2d\x31\x31\x2e\x37\x31\x2d\x31\ +\x31\x2e\x33\x32\x2d\x35\x33\x2e\x39\x31\x2e\x34\x2d\x31\x30\x37\ +\x2e\x38\x33\x2e\x33\x32\x2d\x31\x36\x31\x2e\x37\x35\x2e\x31\x31\ +\x2d\x33\x36\x2e\x34\x38\x2d\x2e\x31\x34\x2d\x36\x38\x2e\x31\x34\ +\x2d\x31\x33\x2e\x30\x34\x2d\x39\x35\x2e\x35\x2d\x33\x37\x2e\x30\ +\x35\x2d\x35\x2e\x38\x37\x2d\x35\x2e\x31\x35\x2d\x31\x32\x2e\x32\ +\x2d\x39\x2e\x38\x38\x2d\x31\x38\x2e\x37\x36\x2d\x31\x34\x2e\x31\ +\x33\x2d\x35\x37\x2e\x36\x39\x2d\x33\x37\x2e\x33\x2d\x38\x37\x2e\ +\x36\x32\x2d\x31\x30\x36\x2e\x35\x31\x2d\x37\x35\x2e\x31\x34\x2d\ +\x31\x37\x34\x2e\x32\x31\x2c\x31\x38\x2e\x32\x31\x2d\x39\x38\x2e\ +\x37\x38\x2c\x31\x31\x39\x2e\x32\x37\x2d\x31\x36\x30\x2e\x36\x34\ +\x2c\x32\x31\x35\x2e\x37\x38\x2d\x31\x33\x32\x2e\x30\x37\x2c\x36\ +\x38\x2e\x33\x39\x2c\x32\x30\x2e\x32\x34\x2c\x31\x31\x36\x2e\x39\ +\x2c\x38\x30\x2e\x31\x37\x2c\x31\x32\x30\x2e\x38\x38\x2c\x31\x35\ +\x31\x2e\x34\x34\x2c\x31\x2e\x31\x2c\x31\x39\x2e\x37\x37\x2d\x31\ +\x2e\x38\x34\x2c\x33\x39\x2e\x37\x36\x2d\x32\x2e\x39\x35\x2c\x36\ +\x30\x2e\x30\x39\x2c\x38\x2e\x30\x31\x2c\x30\x2c\x31\x37\x2e\x36\ +\x37\x2c\x30\x2c\x32\x38\x2e\x34\x39\x2c\x30\x2c\x2e\x32\x2d\x33\ +\x2e\x36\x39\x2e\x33\x37\x2d\x36\x2e\x34\x36\x2e\x34\x38\x2d\x39\ +\x2e\x32\x34\x5a\x4d\x32\x33\x36\x2e\x32\x38\x2c\x31\x32\x39\x2e\ +\x34\x36\x63\x2d\x38\x30\x2e\x38\x38\x2d\x2e\x33\x37\x2d\x31\x34\ +\x38\x2e\x30\x38\x2c\x36\x36\x2e\x38\x35\x2d\x31\x34\x37\x2e\x32\ +\x39\x2c\x31\x34\x37\x2e\x33\x32\x2e\x37\x39\x2c\x38\x30\x2e\x31\ +\x32\x2c\x36\x36\x2e\x33\x39\x2c\x31\x34\x35\x2e\x34\x2c\x31\x34\ +\x36\x2e\x31\x31\x2c\x31\x34\x35\x2e\x34\x31\x2c\x38\x31\x2e\x30\ +\x38\x2c\x30\x2c\x31\x34\x36\x2e\x36\x35\x2d\x36\x35\x2e\x35\x35\ +\x2c\x31\x34\x36\x2e\x36\x36\x2d\x31\x34\x36\x2e\x36\x33\x2c\x30\ +\x2d\x37\x39\x2e\x34\x35\x2d\x36\x36\x2d\x31\x34\x35\x2e\x37\x33\ +\x2d\x31\x34\x35\x2e\x34\x38\x2d\x31\x34\x36\x2e\x31\x5a\x22\x2f\ \x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x35\x38\x33\x2e\ -\x39\x36\x2c\x34\x39\x36\x2e\x30\x35\x63\x2d\x33\x2e\x36\x35\x2c\ -\x32\x2e\x31\x35\x2d\x37\x2e\x33\x31\x2c\x34\x2e\x32\x39\x2d\x31\ -\x30\x2e\x39\x35\x2c\x36\x2e\x34\x35\x2d\x31\x35\x2e\x33\x2c\x39\ -\x2e\x31\x2d\x33\x30\x2e\x35\x39\x2c\x31\x38\x2e\x32\x2d\x34\x35\ -\x2e\x38\x38\x2c\x32\x37\x2e\x33\x31\x2d\x2e\x31\x37\x2e\x31\x2d\ -\x2e\x33\x34\x2e\x31\x39\x2d\x2e\x36\x37\x2e\x33\x37\x2c\x30\x2d\ -\x32\x2e\x33\x34\x2d\x2e\x30\x35\x2d\x31\x39\x2e\x35\x35\x2d\x2e\ -\x30\x35\x2d\x31\x39\x2e\x35\x35\x68\x2d\x32\x33\x2e\x32\x36\x76\ -\x2d\x32\x38\x2e\x31\x35\x68\x32\x33\x2e\x32\x36\x73\x2d\x2e\x30\ -\x32\x2d\x32\x30\x2e\x35\x37\x2c\x30\x2d\x32\x30\x2e\x37\x32\x63\ -\x2e\x30\x37\x2c\x30\x2c\x31\x30\x2e\x32\x38\x2c\x35\x2e\x39\x37\ -\x2c\x31\x34\x2e\x39\x39\x2c\x38\x2e\x37\x37\x2c\x31\x34\x2e\x31\ -\x39\x2c\x38\x2e\x34\x34\x2c\x32\x38\x2e\x33\x38\x2c\x31\x36\x2e\ -\x38\x38\x2c\x34\x32\x2e\x35\x37\x2c\x32\x35\x2e\x33\x32\x2c\x30\ -\x2c\x2e\x30\x36\x2c\x30\x2c\x2e\x31\x32\x2c\x30\x2c\x2e\x31\x39\ -\x5a\x22\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x35\ -\x38\x33\x2e\x39\x36\x2c\x34\x31\x34\x2e\x36\x31\x63\x2d\x33\x2e\ -\x36\x35\x2c\x32\x2e\x31\x35\x2d\x37\x2e\x33\x31\x2c\x34\x2e\x32\ -\x39\x2d\x31\x30\x2e\x39\x35\x2c\x36\x2e\x34\x35\x2d\x31\x35\x2e\ -\x33\x2c\x39\x2e\x31\x2d\x33\x30\x2e\x35\x39\x2c\x31\x38\x2e\x32\ -\x2d\x34\x35\x2e\x38\x38\x2c\x32\x37\x2e\x33\x31\x2d\x2e\x31\x37\ -\x2e\x31\x2d\x2e\x33\x34\x2e\x31\x39\x2d\x2e\x36\x37\x2e\x33\x37\ -\x2c\x30\x2d\x32\x2e\x33\x34\x2d\x2e\x30\x35\x2d\x31\x39\x2e\x35\ -\x35\x2d\x2e\x30\x35\x2d\x31\x39\x2e\x35\x35\x68\x2d\x32\x33\x2e\ -\x32\x36\x76\x2d\x32\x38\x2e\x31\x35\x68\x32\x33\x2e\x32\x36\x73\ -\x2d\x2e\x30\x32\x2d\x32\x30\x2e\x35\x37\x2c\x30\x2d\x32\x30\x2e\ -\x37\x32\x63\x2e\x30\x37\x2c\x30\x2c\x31\x30\x2e\x32\x38\x2c\x35\ -\x2e\x39\x37\x2c\x31\x34\x2e\x39\x39\x2c\x38\x2e\x37\x37\x2c\x31\ -\x34\x2e\x31\x39\x2c\x38\x2e\x34\x34\x2c\x32\x38\x2e\x33\x38\x2c\ -\x31\x36\x2e\x38\x38\x2c\x34\x32\x2e\x35\x37\x2c\x32\x35\x2e\x33\ -\x32\x2c\x30\x2c\x2e\x30\x36\x2c\x30\x2c\x2e\x31\x32\x2c\x30\x2c\ -\x2e\x31\x39\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ +\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x32\x30\x2e\ +\x31\x37\x2c\x32\x30\x35\x2e\x34\x63\x2d\x31\x37\x2e\x31\x32\x2d\ +\x31\x2e\x37\x33\x2d\x33\x32\x2e\x38\x39\x2e\x36\x36\x2d\x34\x37\ +\x2e\x38\x36\x2c\x37\x2e\x36\x37\x2d\x31\x32\x2e\x37\x38\x2c\x35\ +\x2e\x39\x39\x2d\x32\x32\x2e\x38\x38\x2c\x31\x34\x2e\x39\x2d\x33\ +\x30\x2e\x30\x38\x2c\x32\x37\x2e\x31\x36\x2d\x32\x2e\x33\x31\x2c\ +\x33\x2e\x39\x34\x2d\x32\x2e\x33\x34\x2c\x36\x2e\x36\x33\x2c\x31\ +\x2e\x32\x38\x2c\x39\x2e\x39\x39\x2c\x37\x2e\x37\x33\x2c\x37\x2e\ +\x31\x38\x2c\x37\x2e\x34\x35\x2c\x37\x2e\x32\x32\x2c\x31\x36\x2e\ +\x39\x31\x2c\x33\x2e\x35\x2c\x32\x39\x2d\x31\x31\x2e\x33\x39\x2c\ +\x35\x34\x2e\x31\x39\x2d\x33\x2e\x33\x32\x2c\x37\x38\x2e\x30\x32\ +\x2c\x31\x34\x2e\x35\x34\x2c\x38\x2e\x36\x34\x2c\x36\x2e\x34\x37\ +\x2c\x38\x2e\x31\x31\x2c\x31\x33\x2e\x38\x39\x2c\x36\x2e\x38\x34\ +\x2c\x32\x32\x2e\x36\x32\x2d\x34\x2e\x31\x34\x2c\x32\x38\x2e\x33\ +\x38\x2d\x31\x37\x2e\x31\x34\x2c\x35\x31\x2e\x37\x36\x2d\x33\x39\ +\x2c\x37\x30\x2e\x32\x36\x2d\x2e\x34\x34\x2e\x33\x38\x2d\x31\x2e\ +\x30\x32\x2e\x36\x31\x2d\x31\x2e\x35\x35\x2e\x38\x35\x2d\x2e\x31\ +\x34\x2e\x30\x36\x2d\x2e\x33\x38\x2d\x2e\x31\x2d\x2e\x38\x39\x2d\ +\x2e\x32\x36\x2c\x31\x2e\x37\x32\x2d\x31\x36\x2e\x34\x32\x2e\x30\ +\x39\x2d\x33\x32\x2e\x35\x31\x2d\x36\x2e\x38\x2d\x34\x37\x2e\x37\ +\x36\x2d\x36\x2e\x30\x37\x2d\x31\x33\x2e\x34\x35\x2d\x31\x35\x2e\ +\x33\x33\x2d\x32\x34\x2e\x31\x2d\x32\x38\x2e\x32\x35\x2d\x33\x31\ +\x2e\x34\x37\x2d\x32\x2e\x39\x31\x2d\x31\x2e\x36\x36\x2d\x34\x2e\ +\x35\x39\x2d\x31\x2e\x32\x38\x2d\x37\x2e\x33\x38\x2e\x39\x37\x2d\ +\x37\x2e\x32\x38\x2c\x35\x2e\x38\x39\x2d\x38\x2e\x30\x36\x2c\x31\ +\x30\x2e\x39\x36\x2d\x34\x2e\x32\x39\x2c\x32\x30\x2e\x33\x33\x2c\ +\x31\x30\x2e\x36\x39\x2c\x32\x36\x2e\x35\x36\x2c\x31\x2e\x37\x37\ +\x2c\x35\x30\x2e\x33\x34\x2d\x31\x34\x2e\x32\x38\x2c\x37\x32\x2e\ +\x33\x32\x2d\x36\x2e\x31\x31\x2c\x38\x2e\x33\x37\x2d\x31\x32\x2e\ +\x39\x2c\x31\x31\x2e\x37\x36\x2d\x32\x33\x2e\x38\x39\x2c\x39\x2e\ +\x37\x39\x2d\x32\x36\x2e\x39\x31\x2d\x34\x2e\x38\x33\x2d\x34\x39\ +\x2e\x33\x32\x2d\x31\x36\x2e\x39\x32\x2d\x36\x37\x2e\x34\x35\x2d\ +\x33\x37\x2e\x32\x31\x2d\x2e\x33\x38\x2d\x2e\x34\x32\x2d\x2e\x35\ +\x36\x2d\x31\x2e\x30\x33\x2d\x31\x2e\x31\x39\x2d\x32\x2e\x32\x36\ +\x2c\x31\x32\x2e\x30\x34\x2c\x31\x2e\x31\x39\x2c\x32\x33\x2e\x34\ +\x38\x2e\x32\x2c\x33\x34\x2e\x36\x34\x2d\x32\x2e\x38\x39\x2c\x31\ +\x38\x2e\x34\x39\x2d\x35\x2e\x31\x33\x2c\x33\x33\x2e\x33\x2d\x31\ +\x35\x2e\x32\x34\x2c\x34\x33\x2e\x33\x37\x2d\x33\x31\x2e\x39\x37\ +\x2c\x32\x2e\x35\x2d\x34\x2e\x31\x35\x2c\x32\x2e\x31\x32\x2d\x36\ +\x2e\x38\x2d\x31\x2e\x32\x34\x2d\x31\x30\x2e\x30\x39\x2d\x37\x2e\ +\x36\x32\x2d\x37\x2e\x34\x37\x2d\x37\x2e\x34\x34\x2d\x37\x2e\x34\ +\x39\x2d\x31\x37\x2e\x34\x38\x2d\x33\x2e\x34\x37\x2d\x32\x37\x2e\ +\x35\x36\x2c\x31\x31\x2e\x30\x32\x2d\x35\x31\x2e\x37\x34\x2c\x33\ +\x2e\x33\x34\x2d\x37\x35\x2e\x30\x33\x2d\x31\x32\x2e\x36\x35\x2d\ +\x31\x30\x2e\x37\x31\x2d\x37\x2e\x33\x35\x2d\x31\x30\x2e\x37\x39\ +\x2d\x31\x36\x2e\x32\x35\x2d\x38\x2e\x38\x39\x2d\x32\x37\x2e\x31\ +\x38\x2c\x34\x2e\x37\x32\x2d\x32\x37\x2e\x31\x34\x2c\x31\x37\x2e\ +\x33\x38\x2d\x34\x39\x2e\x35\x36\x2c\x33\x38\x2e\x32\x38\x2d\x36\ +\x37\x2e\x34\x31\x2e\x35\x35\x2d\x2e\x34\x37\x2c\x31\x2e\x31\x39\ +\x2d\x2e\x38\x33\x2c\x32\x2e\x34\x2d\x31\x2e\x36\x37\x2e\x36\x34\ +\x2c\x31\x30\x2e\x33\x33\x2d\x2e\x35\x38\x2c\x31\x39\x2e\x39\x32\ +\x2c\x31\x2e\x33\x31\x2c\x32\x39\x2e\x34\x31\x2c\x34\x2e\x32\x34\ +\x2c\x32\x31\x2e\x32\x39\x2c\x31\x34\x2e\x33\x31\x2c\x33\x38\x2e\ +\x35\x34\x2c\x33\x33\x2e\x31\x39\x2c\x35\x30\x2e\x31\x32\x2c\x33\ +\x2e\x35\x2c\x32\x2e\x31\x34\x2c\x35\x2e\x37\x36\x2c\x32\x2e\x33\ +\x35\x2c\x38\x2e\x38\x37\x2d\x2e\x39\x31\x2c\x37\x2e\x37\x36\x2d\ +\x38\x2e\x31\x34\x2c\x37\x2e\x36\x39\x2d\x37\x2e\x38\x36\x2c\x33\ +\x2e\x38\x2d\x31\x38\x2e\x35\x32\x2d\x39\x2e\x37\x37\x2d\x32\x36\ +\x2e\x38\x31\x2d\x32\x2e\x35\x31\x2d\x35\x30\x2e\x35\x31\x2c\x31\ +\x33\x2e\x34\x38\x2d\x37\x32\x2e\x36\x39\x2c\x36\x2e\x34\x31\x2d\ +\x38\x2e\x38\x39\x2c\x31\x33\x2e\x33\x35\x2d\x31\x33\x2c\x32\x35\ +\x2e\x32\x38\x2d\x31\x30\x2e\x37\x32\x2c\x32\x36\x2e\x36\x31\x2c\ +\x35\x2e\x30\x39\x2c\x34\x38\x2e\x38\x2c\x31\x37\x2e\x30\x39\x2c\ +\x36\x36\x2e\x38\x37\x2c\x33\x37\x2e\x30\x36\x2e\x33\x37\x2e\x34\ +\x2e\x34\x34\x2c\x31\x2e\x30\x37\x2c\x31\x2e\x30\x31\x2c\x32\x2e\ +\x35\x34\x5a\x4d\x32\x33\x35\x2e\x31\x39\x2c\x32\x38\x39\x2e\x36\ +\x35\x63\x33\x2e\x30\x31\x2e\x30\x39\x2c\x31\x33\x2e\x36\x39\x2d\ +\x31\x30\x2e\x34\x34\x2c\x31\x33\x2e\x38\x36\x2d\x31\x33\x2e\x36\ +\x36\x2e\x31\x37\x2d\x33\x2e\x32\x31\x2d\x31\x30\x2e\x30\x33\x2d\ +\x31\x33\x2e\x37\x32\x2d\x31\x33\x2e\x35\x31\x2d\x31\x33\x2e\x39\ +\x2d\x32\x2e\x39\x34\x2d\x2e\x31\x36\x2d\x31\x33\x2e\x38\x33\x2c\ +\x31\x30\x2e\x34\x36\x2d\x31\x33\x2e\x39\x32\x2c\x31\x33\x2e\x35\ +\x37\x2d\x2e\x30\x39\x2c\x33\x2e\x31\x37\x2c\x31\x30\x2e\x33\x31\ +\x2c\x31\x33\x2e\x39\x2c\x31\x33\x2e\x35\x37\x2c\x31\x33\x2e\x39\ +\x39\x5a\x22\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\ +\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\ +\x35\x33\x34\x2e\x39\x34\x2c\x33\x32\x37\x2e\x34\x34\x63\x2d\x33\ +\x2e\x30\x32\x2c\x31\x2e\x37\x38\x2d\x36\x2e\x30\x35\x2c\x33\x2e\ +\x35\x35\x2d\x39\x2e\x30\x36\x2c\x35\x2e\x33\x34\x2d\x31\x32\x2e\ +\x36\x36\x2c\x37\x2e\x35\x33\x2d\x32\x35\x2e\x33\x31\x2c\x31\x35\ +\x2e\x30\x36\x2d\x33\x37\x2e\x39\x36\x2c\x32\x32\x2e\x35\x39\x2d\ +\x2e\x31\x34\x2e\x30\x38\x2d\x2e\x32\x38\x2e\x31\x36\x2d\x2e\x35\ +\x36\x2e\x33\x31\x2c\x30\x2d\x31\x2e\x39\x33\x2d\x2e\x30\x34\x2d\ +\x31\x36\x2e\x31\x37\x2d\x2e\x30\x34\x2d\x31\x36\x2e\x31\x37\x68\ +\x2d\x31\x39\x2e\x32\x35\x76\x2d\x32\x33\x2e\x32\x39\x68\x31\x39\ +\x2e\x32\x35\x73\x2d\x2e\x30\x31\x2d\x31\x37\x2e\x30\x32\x2c\x30\ +\x2d\x31\x37\x2e\x31\x35\x63\x2e\x30\x36\x2c\x30\x2c\x38\x2e\x35\ +\x31\x2c\x34\x2e\x39\x34\x2c\x31\x32\x2e\x34\x2c\x37\x2e\x32\x36\ +\x2c\x31\x31\x2e\x37\x34\x2c\x36\x2e\x39\x38\x2c\x32\x33\x2e\x34\ +\x38\x2c\x31\x33\x2e\x39\x37\x2c\x33\x35\x2e\x32\x32\x2c\x32\x30\ +\x2e\x39\x35\x2c\x30\x2c\x2e\x30\x35\x2c\x30\x2c\x2e\x31\x2c\x30\ +\x2c\x2e\x31\x35\x5a\x22\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\ +\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\ +\x3d\x22\x4d\x35\x33\x34\x2e\x39\x34\x2c\x34\x36\x32\x2e\x32\x31\ +\x63\x2d\x33\x2e\x30\x32\x2c\x31\x2e\x37\x38\x2d\x36\x2e\x30\x35\ +\x2c\x33\x2e\x35\x35\x2d\x39\x2e\x30\x36\x2c\x35\x2e\x33\x34\x2d\ +\x31\x32\x2e\x36\x36\x2c\x37\x2e\x35\x33\x2d\x32\x35\x2e\x33\x31\ +\x2c\x31\x35\x2e\x30\x36\x2d\x33\x37\x2e\x39\x36\x2c\x32\x32\x2e\ +\x35\x39\x2d\x2e\x31\x34\x2e\x30\x38\x2d\x2e\x32\x38\x2e\x31\x36\ +\x2d\x2e\x35\x36\x2e\x33\x31\x2c\x30\x2d\x31\x2e\x39\x33\x2d\x2e\ +\x30\x34\x2d\x31\x36\x2e\x31\x37\x2d\x2e\x30\x34\x2d\x31\x36\x2e\ +\x31\x37\x68\x2d\x31\x39\x2e\x32\x35\x76\x2d\x32\x33\x2e\x32\x39\ +\x68\x31\x39\x2e\x32\x35\x73\x2d\x2e\x30\x31\x2d\x31\x37\x2e\x30\ +\x32\x2c\x30\x2d\x31\x37\x2e\x31\x35\x63\x2e\x30\x36\x2c\x30\x2c\ +\x38\x2e\x35\x31\x2c\x34\x2e\x39\x34\x2c\x31\x32\x2e\x34\x2c\x37\ +\x2e\x32\x36\x2c\x31\x31\x2e\x37\x34\x2c\x36\x2e\x39\x38\x2c\x32\ +\x33\x2e\x34\x38\x2c\x31\x33\x2e\x39\x37\x2c\x33\x35\x2e\x32\x32\ +\x2c\x32\x30\x2e\x39\x35\x2c\x30\x2c\x2e\x30\x35\x2c\x30\x2c\x2e\ +\x31\x2c\x30\x2c\x2e\x31\x35\x5a\x22\x2f\x3e\x0a\x20\x20\x3c\x70\ +\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\ +\x22\x20\x64\x3d\x22\x4d\x35\x33\x34\x2e\x39\x34\x2c\x33\x39\x34\ +\x2e\x38\x32\x63\x2d\x33\x2e\x30\x32\x2c\x31\x2e\x37\x38\x2d\x36\ +\x2e\x30\x35\x2c\x33\x2e\x35\x35\x2d\x39\x2e\x30\x36\x2c\x35\x2e\ +\x33\x34\x2d\x31\x32\x2e\x36\x36\x2c\x37\x2e\x35\x33\x2d\x32\x35\ +\x2e\x33\x31\x2c\x31\x35\x2e\x30\x36\x2d\x33\x37\x2e\x39\x36\x2c\ +\x32\x32\x2e\x35\x39\x2d\x2e\x31\x34\x2e\x30\x38\x2d\x2e\x32\x38\ +\x2e\x31\x36\x2d\x2e\x35\x36\x2e\x33\x31\x2c\x30\x2d\x31\x2e\x39\ +\x33\x2d\x2e\x30\x34\x2d\x31\x36\x2e\x31\x37\x2d\x2e\x30\x34\x2d\ +\x31\x36\x2e\x31\x37\x68\x2d\x31\x39\x2e\x32\x35\x76\x2d\x32\x33\ +\x2e\x32\x39\x68\x31\x39\x2e\x32\x35\x73\x2d\x2e\x30\x31\x2d\x31\ +\x37\x2e\x30\x32\x2c\x30\x2d\x31\x37\x2e\x31\x35\x63\x2e\x30\x36\ +\x2c\x30\x2c\x38\x2e\x35\x31\x2c\x34\x2e\x39\x34\x2c\x31\x32\x2e\ +\x34\x2c\x37\x2e\x32\x36\x2c\x31\x31\x2e\x37\x34\x2c\x36\x2e\x39\ +\x38\x2c\x32\x33\x2e\x34\x38\x2c\x31\x33\x2e\x39\x37\x2c\x33\x35\ +\x2e\x32\x32\x2c\x32\x30\x2e\x39\x35\x2c\x30\x2c\x2e\x30\x35\x2c\ +\x30\x2c\x2e\x31\x2c\x30\x2c\x2e\x31\x35\x5a\x22\x2f\x3e\x0a\x3c\ +\x2f\x73\x76\x67\x3e\ \x00\x00\x07\xc6\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -27421,135 +27431,135 @@ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4d\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x03\x00\x00\x00\x4e\ \x00\x00\x09\x48\x00\x00\x00\x00\x00\x01\x00\x02\xc8\xc8\ -\x00\x00\x09\x66\x00\x00\x00\x00\x00\x01\x00\x02\xd1\xaa\ -\x00\x00\x09\x7a\x00\x00\x00\x00\x00\x01\x00\x02\xd7\x8b\ +\x00\x00\x09\x66\x00\x00\x00\x00\x00\x01\x00\x02\xd2\x85\ +\x00\x00\x09\x7a\x00\x00\x00\x00\x00\x01\x00\x02\xd8\x58\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x52\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x53\ -\x00\x00\x09\x94\x00\x00\x00\x00\x00\x01\x00\x02\xe3\x1e\ -\x00\x00\x09\xbc\x00\x00\x00\x00\x00\x01\x00\x02\xe8\x19\ -\x00\x00\x09\xf4\x00\x00\x00\x00\x00\x01\x00\x02\xf0\xed\ -\x00\x00\x0a\x2c\x00\x00\x00\x00\x00\x01\x00\x02\xf9\x91\ -\x00\x00\x0a\x5c\x00\x00\x00\x00\x00\x01\x00\x03\x01\x30\ -\x00\x00\x0a\x80\x00\x00\x00\x00\x00\x01\x00\x03\x09\x0c\ -\x00\x00\x0a\xa4\x00\x00\x00\x00\x00\x01\x00\x03\x10\xc2\ -\x00\x00\x0a\xd8\x00\x00\x00\x00\x00\x01\x00\x03\x18\xd0\ -\x00\x00\x0b\x0c\x00\x00\x00\x00\x00\x01\x00\x03\x20\xb2\ -\x00\x00\x0b\x40\x00\x00\x00\x00\x00\x01\x00\x03\x28\x7c\ -\x00\x00\x0b\x7a\x00\x00\x00\x00\x00\x01\x00\x03\x2f\xe3\ -\x00\x00\x0b\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x33\xa0\ -\x00\x00\x0b\xcc\x00\x00\x00\x00\x00\x01\x00\x03\x37\x79\ +\x00\x00\x09\x94\x00\x00\x00\x00\x00\x01\x00\x02\xe3\xd2\ +\x00\x00\x09\xbc\x00\x00\x00\x00\x00\x01\x00\x02\xe8\xcd\ +\x00\x00\x09\xf4\x00\x00\x00\x00\x00\x01\x00\x02\xf1\xa1\ +\x00\x00\x0a\x2c\x00\x00\x00\x00\x00\x01\x00\x02\xfa\x45\ +\x00\x00\x0a\x5c\x00\x00\x00\x00\x00\x01\x00\x03\x01\xe4\ +\x00\x00\x0a\x80\x00\x00\x00\x00\x00\x01\x00\x03\x09\xc0\ +\x00\x00\x0a\xa4\x00\x00\x00\x00\x00\x01\x00\x03\x11\x76\ +\x00\x00\x0a\xd8\x00\x00\x00\x00\x00\x01\x00\x03\x19\x84\ +\x00\x00\x0b\x0c\x00\x00\x00\x00\x00\x01\x00\x03\x21\x66\ +\x00\x00\x0b\x40\x00\x00\x00\x00\x00\x01\x00\x03\x29\x30\ +\x00\x00\x0b\x7a\x00\x00\x00\x00\x00\x01\x00\x03\x30\x97\ +\x00\x00\x0b\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x34\x54\ +\x00\x00\x0b\xcc\x00\x00\x00\x00\x00\x01\x00\x03\x38\x2d\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x61\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x08\x00\x00\x00\x62\ -\x00\x00\x0b\xf2\x00\x00\x00\x00\x00\x01\x00\x03\x3d\x82\ -\x00\x00\x0c\x1e\x00\x00\x00\x00\x00\x01\x00\x03\x60\x06\ -\x00\x00\x0c\x4c\x00\x00\x00\x00\x00\x01\x00\x03\x65\xf3\ -\x00\x00\x0c\x76\x00\x00\x00\x00\x00\x01\x00\x03\x68\x13\ -\x00\x00\x0c\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x70\xab\ -\x00\x00\x0c\xb8\x00\x00\x00\x00\x00\x01\x00\x03\x7f\xf4\ -\x00\x00\x0c\xe4\x00\x00\x00\x00\x00\x01\x00\x03\x86\x72\ -\x00\x00\x0d\x0c\x00\x00\x00\x00\x00\x01\x00\x03\x90\xec\ +\x00\x00\x0b\xf2\x00\x00\x00\x00\x00\x01\x00\x03\x3e\x36\ +\x00\x00\x0c\x1e\x00\x00\x00\x00\x00\x01\x00\x03\x60\xba\ +\x00\x00\x0c\x4c\x00\x00\x00\x00\x00\x01\x00\x03\x66\xa7\ +\x00\x00\x0c\x76\x00\x00\x00\x00\x00\x01\x00\x03\x68\xc7\ +\x00\x00\x0c\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x71\x5f\ +\x00\x00\x0c\xb8\x00\x00\x00\x00\x00\x01\x00\x03\x80\xa8\ +\x00\x00\x0c\xe4\x00\x00\x00\x00\x00\x01\x00\x03\x87\x26\ +\x00\x00\x0d\x0c\x00\x00\x00\x00\x00\x01\x00\x03\x91\xa0\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x6b\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x6c\ -\x00\x00\x0d\x42\x00\x00\x00\x00\x00\x01\x00\x03\x9a\x33\ -\x00\x00\x0d\x70\x00\x00\x00\x00\x00\x01\x00\x03\x9c\xda\ -\x00\x00\x0d\x9e\x00\x01\x00\x00\x00\x01\x00\x03\xa9\x57\ -\x00\x00\x0d\xca\x00\x00\x00\x00\x00\x01\x00\x03\xd6\xd6\ -\x00\x00\x0d\xea\x00\x00\x00\x00\x00\x01\x00\x03\xdb\x8d\ -\x00\x00\x0e\x1c\x00\x01\x00\x00\x00\x01\x00\x04\x34\x86\ -\x00\x00\x0e\x4e\x00\x00\x00\x00\x00\x01\x00\x04\x69\x20\ -\x00\x00\x0e\x68\x00\x00\x00\x00\x00\x01\x00\x04\x6e\x6a\ -\x00\x00\x0e\x82\x00\x00\x00\x00\x00\x01\x00\x04\x73\xf9\ -\x00\x00\x0e\x9c\x00\x00\x00\x00\x00\x01\x00\x04\x79\x62\ -\x00\x00\x0e\xb4\x00\x00\x00\x00\x00\x01\x00\x04\x85\x40\ -\x00\x00\x0e\xd2\x00\x00\x00\x00\x00\x01\x00\x04\x8b\x44\ +\x00\x00\x0d\x42\x00\x00\x00\x00\x00\x01\x00\x03\x9a\xe7\ +\x00\x00\x0d\x70\x00\x00\x00\x00\x00\x01\x00\x03\x9d\x8e\ +\x00\x00\x0d\x9e\x00\x01\x00\x00\x00\x01\x00\x03\xaa\x0b\ +\x00\x00\x0d\xca\x00\x00\x00\x00\x00\x01\x00\x03\xd7\x8a\ +\x00\x00\x0d\xea\x00\x00\x00\x00\x00\x01\x00\x03\xdc\x41\ +\x00\x00\x0e\x1c\x00\x01\x00\x00\x00\x01\x00\x04\x35\x3a\ +\x00\x00\x0e\x4e\x00\x00\x00\x00\x00\x01\x00\x04\x69\xd4\ +\x00\x00\x0e\x68\x00\x00\x00\x00\x00\x01\x00\x04\x6f\x1e\ +\x00\x00\x0e\x82\x00\x00\x00\x00\x00\x01\x00\x04\x74\xad\ +\x00\x00\x0e\x9c\x00\x00\x00\x00\x00\x01\x00\x04\x7a\x16\ +\x00\x00\x0e\xb4\x00\x00\x00\x00\x00\x01\x00\x04\x85\xf4\ +\x00\x00\x0e\xd2\x00\x00\x00\x00\x00\x01\x00\x04\x8b\xf8\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x79\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x7a\ -\x00\x00\x0e\xfa\x00\x00\x00\x00\x00\x01\x00\x04\x90\x79\ -\x00\x00\x0f\x0e\x00\x00\x00\x00\x00\x01\x00\x04\x96\x76\ -\x00\x00\x0f\x20\x00\x00\x00\x00\x00\x01\x00\x04\x97\xfc\ -\x00\x00\x0f\x32\x00\x00\x00\x00\x00\x01\x00\x04\x9d\xf6\ +\x00\x00\x0e\xfa\x00\x00\x00\x00\x00\x01\x00\x04\x91\x2d\ +\x00\x00\x0f\x0e\x00\x00\x00\x00\x00\x01\x00\x04\x97\x2a\ +\x00\x00\x0f\x20\x00\x00\x00\x00\x00\x01\x00\x04\x98\xb0\ +\x00\x00\x0f\x32\x00\x00\x00\x00\x00\x01\x00\x04\x9e\xaa\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7f\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x80\ -\x00\x00\x0f\x46\x00\x00\x00\x00\x00\x01\x00\x04\xa0\x4c\ -\x00\x00\x0f\x72\x00\x00\x00\x00\x00\x01\x00\x04\xa7\x33\ +\x00\x00\x0f\x46\x00\x00\x00\x00\x00\x01\x00\x04\xa1\x00\ +\x00\x00\x0f\x72\x00\x00\x00\x00\x00\x01\x00\x04\xa7\xe7\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x83\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x84\ \x00\x00\x00\x46\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x88\ -\x00\x00\x0f\x96\x00\x01\x00\x00\x00\x01\x00\x04\xb0\x97\ -\x00\x00\x0f\xba\x00\x00\x00\x00\x00\x01\x00\x04\xbb\xe8\ -\x00\x00\x0f\xdc\x00\x00\x00\x00\x00\x01\x00\x04\xc3\x8d\ -\x00\x00\x0f\xf8\x00\x00\x00\x00\x00\x01\x00\x04\xd4\x8d\ -\x00\x00\x10\x18\x00\x00\x00\x00\x00\x01\x00\x04\xda\x26\ -\x00\x00\x10\x38\x00\x00\x00\x00\x00\x01\x00\x04\xe0\x4d\ -\x00\x00\x10\x58\x00\x00\x00\x00\x00\x01\x00\x04\xe4\xa2\ -\x00\x00\x10\x78\x00\x00\x00\x00\x00\x01\x00\x04\xea\x26\ -\x00\x00\x10\x98\x00\x00\x00\x00\x00\x01\x00\x04\xef\xbf\ -\x00\x00\x10\xca\x00\x00\x00\x00\x00\x01\x00\x04\xf4\xc1\ -\x00\x00\x10\xea\x00\x00\x00\x00\x00\x01\x00\x04\xfa\x5a\ -\x00\x00\x11\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x04\xce\ -\x00\x00\x11\x52\x00\x00\x00\x00\x00\x01\x00\x05\x0f\x48\ -\x00\x00\x11\x86\x00\x00\x00\x00\x00\x01\x00\x05\x19\xa7\ -\x00\x00\x11\xba\x00\x00\x00\x00\x00\x01\x00\x05\x24\xf2\ +\x00\x00\x0f\x96\x00\x01\x00\x00\x00\x01\x00\x04\xb1\x4b\ +\x00\x00\x0f\xba\x00\x00\x00\x00\x00\x01\x00\x04\xbc\x9c\ +\x00\x00\x0f\xdc\x00\x00\x00\x00\x00\x01\x00\x04\xc4\x41\ +\x00\x00\x0f\xf8\x00\x00\x00\x00\x00\x01\x00\x04\xd5\x41\ +\x00\x00\x10\x18\x00\x00\x00\x00\x00\x01\x00\x04\xda\xda\ +\x00\x00\x10\x38\x00\x00\x00\x00\x00\x01\x00\x04\xe1\x01\ +\x00\x00\x10\x58\x00\x00\x00\x00\x00\x01\x00\x04\xe5\x56\ +\x00\x00\x10\x78\x00\x00\x00\x00\x00\x01\x00\x04\xea\xda\ +\x00\x00\x10\x98\x00\x00\x00\x00\x00\x01\x00\x04\xf0\x73\ +\x00\x00\x10\xca\x00\x00\x00\x00\x00\x01\x00\x04\xf5\x75\ +\x00\x00\x10\xea\x00\x00\x00\x00\x00\x01\x00\x04\xfb\x0e\ +\x00\x00\x11\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x05\x82\ +\x00\x00\x11\x52\x00\x00\x00\x00\x00\x01\x00\x05\x0f\xfc\ +\x00\x00\x11\x86\x00\x00\x00\x00\x00\x01\x00\x05\x1a\x5b\ +\x00\x00\x11\xba\x00\x00\x00\x00\x00\x01\x00\x05\x25\xa6\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x95\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x96\ -\x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x2f\x66\ -\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x38\xfc\ -\x00\x00\x12\x4e\x00\x00\x00\x00\x00\x01\x00\x05\x44\xd8\ -\x00\x00\x12\x72\x00\x00\x00\x00\x00\x01\x00\x05\x4b\x1c\ -\x00\x00\x12\x9c\x00\x00\x00\x00\x00\x01\x00\x05\x52\xa5\ -\x00\x00\x12\xc8\x00\x00\x00\x00\x00\x01\x00\x05\x59\x03\ -\x00\x00\x12\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x60\xf2\ -\x00\x00\x09\x66\x00\x00\x00\x00\x00\x01\x00\x05\x6e\x95\ -\x00\x00\x09\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x74\x76\ -\x00\x00\x13\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x80\x09\ +\x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x30\x1a\ +\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x39\xb0\ +\x00\x00\x12\x4e\x00\x00\x00\x00\x00\x01\x00\x05\x45\x8c\ +\x00\x00\x12\x72\x00\x00\x00\x00\x00\x01\x00\x05\x4b\xd0\ +\x00\x00\x12\x9c\x00\x00\x00\x00\x00\x01\x00\x05\x53\x59\ +\x00\x00\x12\xc8\x00\x00\x00\x00\x00\x01\x00\x05\x59\xb7\ +\x00\x00\x12\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x61\xa6\ +\x00\x00\x09\x66\x00\x00\x00\x00\x00\x01\x00\x05\x6f\x49\ +\x00\x00\x09\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x75\x1c\ +\x00\x00\x13\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x80\x96\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa1\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa2\ -\x00\x00\x13\x44\x00\x00\x00\x00\x00\x01\x00\x05\x87\xd3\ +\x00\x00\x13\x44\x00\x00\x00\x00\x00\x01\x00\x05\x88\x60\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa4\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x2b\x00\x00\x00\xa5\ -\x00\x00\x13\x64\x00\x00\x00\x00\x00\x01\x00\x05\x8c\x99\ -\x00\x00\x13\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x94\x4d\ -\x00\x00\x13\x92\x00\x00\x00\x00\x00\x01\x00\x05\x96\x72\ -\x00\x00\x13\xca\x00\x00\x00\x00\x00\x01\x00\x05\x97\xf2\ -\x00\x00\x13\xe6\x00\x00\x00\x00\x00\x01\x00\x05\x9f\x9f\ -\x00\x00\x14\x06\x00\x00\x00\x00\x00\x01\x00\x05\xa4\x74\ -\x00\x00\x14\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xa5\x64\ -\x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xa8\x90\ -\x00\x00\x14\x58\x00\x00\x00\x00\x00\x01\x00\x05\xac\xb9\ -\x00\x00\x14\x7e\x00\x00\x00\x00\x00\x01\x00\x05\xb2\xf0\ -\x00\x00\x14\x98\x00\x00\x00\x00\x00\x01\x00\x05\xc7\xfd\ -\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xcc\xed\ -\x00\x00\x14\xd0\x00\x00\x00\x00\x00\x01\x00\x05\xcf\xec\ -\x00\x00\x14\xe6\x00\x00\x00\x00\x00\x01\x00\x05\xd5\xf6\ -\x00\x00\x15\x18\x00\x00\x00\x00\x00\x01\x00\x05\xd9\x45\ -\x00\x00\x15\x30\x00\x00\x00\x00\x00\x01\x00\x05\xdc\x37\ -\x00\x00\x15\x46\x00\x00\x00\x00\x00\x01\x00\x05\xe2\x2e\ -\x00\x00\x15\x5a\x00\x00\x00\x00\x00\x01\x00\x05\xe4\x3f\ -\x00\x00\x15\x72\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x02\ -\x00\x00\x15\x98\x00\x00\x00\x00\x00\x01\x00\x05\xf1\xb6\ -\x00\x00\x15\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xf7\x7e\ -\x00\x00\x15\xe0\x00\x00\x00\x00\x00\x01\x00\x05\xfa\xcc\ -\x00\x00\x16\x02\x00\x00\x00\x00\x00\x01\x00\x05\xfe\x5a\ -\x00\x00\x16\x28\x00\x00\x00\x00\x00\x01\x00\x06\x02\xfb\ -\x00\x00\x16\x3c\x00\x00\x00\x00\x00\x01\x00\x06\x0c\xcd\ -\x00\x00\x16\x68\x00\x00\x00\x00\x00\x01\x00\x06\x12\x17\ -\x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x06\x18\x1e\ -\x00\x00\x16\xa6\x00\x00\x00\x00\x00\x01\x00\x06\x19\x02\ -\x00\x00\x16\xd2\x00\x00\x00\x00\x00\x01\x00\x06\x1b\x4b\ -\x00\x00\x16\xe8\x00\x00\x00\x00\x00\x01\x00\x06\x21\xfb\ -\x00\x00\x17\x04\x00\x00\x00\x00\x00\x01\x00\x06\x25\x3f\ -\x00\x00\x17\x38\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x25\ -\x00\x00\x17\x50\x00\x00\x00\x00\x00\x01\x00\x06\x2d\x51\ -\x00\x00\x17\x6a\x00\x00\x00\x00\x00\x01\x00\x06\x33\x12\ -\x00\x00\x17\x8c\x00\x00\x00\x00\x00\x01\x00\x06\x34\x34\ -\x00\x00\x17\xaa\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x27\ -\x00\x00\x17\xca\x00\x00\x00\x00\x00\x01\x00\x06\x3d\x2b\ -\x00\x00\x17\xec\x00\x00\x00\x00\x00\x01\x00\x06\x3e\x4c\ -\x00\x00\x18\x0c\x00\x00\x00\x00\x00\x01\x00\x06\x41\x20\ -\x00\x00\x18\x3a\x00\x00\x00\x00\x00\x01\x00\x06\x49\x8c\ -\x00\x00\x18\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x51\x4c\ -\x00\x00\x18\x82\x00\x00\x00\x00\x00\x01\x00\x06\x56\x67\ -\x00\x00\x18\xaa\x00\x00\x00\x00\x00\x01\x00\x06\x57\xba\ +\x00\x00\x13\x64\x00\x00\x00\x00\x00\x01\x00\x05\x8d\x26\ +\x00\x00\x13\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x94\xda\ +\x00\x00\x13\x92\x00\x00\x00\x00\x00\x01\x00\x05\x96\xff\ +\x00\x00\x13\xca\x00\x00\x00\x00\x00\x01\x00\x05\x98\x7f\ +\x00\x00\x13\xe6\x00\x00\x00\x00\x00\x01\x00\x05\xa0\x2c\ +\x00\x00\x14\x06\x00\x00\x00\x00\x00\x01\x00\x05\xa5\x01\ +\x00\x00\x14\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xa5\xf1\ +\x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xa9\x1d\ +\x00\x00\x14\x58\x00\x00\x00\x00\x00\x01\x00\x05\xad\x46\ +\x00\x00\x14\x7e\x00\x00\x00\x00\x00\x01\x00\x05\xb3\x7d\ +\x00\x00\x14\x98\x00\x00\x00\x00\x00\x01\x00\x05\xc8\x8a\ +\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xcd\x7a\ +\x00\x00\x14\xd0\x00\x00\x00\x00\x00\x01\x00\x05\xd0\x79\ +\x00\x00\x14\xe6\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x83\ +\x00\x00\x15\x18\x00\x00\x00\x00\x00\x01\x00\x05\xd9\xd2\ +\x00\x00\x15\x30\x00\x00\x00\x00\x00\x01\x00\x05\xdc\xc4\ +\x00\x00\x15\x46\x00\x00\x00\x00\x00\x01\x00\x05\xe2\xbb\ +\x00\x00\x15\x5a\x00\x00\x00\x00\x00\x01\x00\x05\xe4\xcc\ +\x00\x00\x15\x72\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x8f\ +\x00\x00\x15\x98\x00\x00\x00\x00\x00\x01\x00\x05\xf2\x43\ +\x00\x00\x15\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xf8\x0b\ +\x00\x00\x15\xe0\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x59\ +\x00\x00\x16\x02\x00\x00\x00\x00\x00\x01\x00\x05\xfe\xe7\ +\x00\x00\x16\x28\x00\x00\x00\x00\x00\x01\x00\x06\x03\x88\ +\x00\x00\x16\x3c\x00\x00\x00\x00\x00\x01\x00\x06\x0d\x5a\ +\x00\x00\x16\x68\x00\x00\x00\x00\x00\x01\x00\x06\x12\xa4\ +\x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x06\x18\xab\ +\x00\x00\x16\xa6\x00\x00\x00\x00\x00\x01\x00\x06\x19\x8f\ +\x00\x00\x16\xd2\x00\x00\x00\x00\x00\x01\x00\x06\x1b\xd8\ +\x00\x00\x16\xe8\x00\x00\x00\x00\x00\x01\x00\x06\x22\x88\ +\x00\x00\x17\x04\x00\x00\x00\x00\x00\x01\x00\x06\x25\xcc\ +\x00\x00\x17\x38\x00\x00\x00\x00\x00\x01\x00\x06\x2c\xb2\ +\x00\x00\x17\x50\x00\x00\x00\x00\x00\x01\x00\x06\x2d\xde\ +\x00\x00\x17\x6a\x00\x00\x00\x00\x00\x01\x00\x06\x33\x9f\ +\x00\x00\x17\x8c\x00\x00\x00\x00\x00\x01\x00\x06\x34\xc1\ +\x00\x00\x17\xaa\x00\x00\x00\x00\x00\x01\x00\x06\x3a\xb4\ +\x00\x00\x17\xca\x00\x00\x00\x00\x00\x01\x00\x06\x3d\xb8\ +\x00\x00\x17\xec\x00\x00\x00\x00\x00\x01\x00\x06\x3e\xd9\ +\x00\x00\x18\x0c\x00\x00\x00\x00\x00\x01\x00\x06\x41\xad\ +\x00\x00\x18\x3a\x00\x00\x00\x00\x00\x01\x00\x06\x4a\x19\ +\x00\x00\x18\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x51\xd9\ +\x00\x00\x18\x82\x00\x00\x00\x00\x00\x01\x00\x06\x56\xf4\ +\x00\x00\x18\xaa\x00\x00\x00\x00\x00\x01\x00\x06\x58\x47\ " qt_resource_struct_v2 = b"\ @@ -27592,87 +27602,87 @@ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x13\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x01\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x08\x66\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x02\x10\x00\x00\x00\x00\x00\x01\x00\x00\x09\x52\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x02\x38\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x35\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x02\x60\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x18\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x02\x88\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x06\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x02\xbc\x00\x00\x00\x00\x00\x01\x00\x00\x14\x42\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x02\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf7\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ \x00\x00\x03\x12\x00\x00\x00\x00\x00\x01\x00\x00\x22\x41\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ \x00\x00\x03\x32\x00\x00\x00\x00\x00\x01\x00\x00\x26\x90\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x03\x4e\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x6e\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x03\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x30\xca\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x03\x92\x00\x00\x00\x00\x00\x01\x00\x00\x32\xb1\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x21\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x22\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x03\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x3a\x55\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ \x00\x00\x03\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x3c\xcf\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x03\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x3f\x43\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ +\x00\x00\x01\x9c\xe2\x99\x3f\x98\ \x00\x00\x04\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x40\xdd\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ \x00\x00\x04\x38\x00\x00\x00\x00\x00\x01\x00\x00\x43\x59\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x04\x5a\x00\x00\x00\x00\x00\x01\x00\x00\x45\xd5\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ +\x00\x00\x01\x9c\xe2\x99\x3f\x98\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x29\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x2a\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x04\x7e\x00\x00\x00\x00\x00\x01\x00\x00\x47\x6d\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x04\x98\x00\x00\x00\x00\x00\x01\x00\x00\x48\x6e\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x04\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x4d\x86\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ \x00\x00\x04\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x59\x48\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x05\x22\x00\x00\x00\x00\x00\x01\x00\x00\x5b\xb8\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ \x00\x00\x05\x48\x00\x00\x00\x00\x00\x01\x00\x00\x61\x50\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ \x00\x00\x05\x6c\x00\x00\x00\x00\x00\x01\x00\x00\x65\xac\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x05\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x39\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x05\xc2\x00\x00\x00\x00\x00\x01\x00\x00\x6f\x35\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x05\xea\x00\x00\x00\x00\x00\x01\x00\x00\x70\x33\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x35\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x36\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x06\x1c\x00\x01\x00\x00\x00\x01\x00\x00\x7a\x9d\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ \x00\x00\x06\x2e\x00\x00\x00\x00\x00\x01\x00\x02\x3d\xf2\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x39\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3a\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x06\x42\x00\x00\x00\x00\x00\x01\x00\x02\x42\x78\ -\x00\x00\x01\x9b\xc6\xe6\xb1\x6c\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ \x00\x00\x06\x70\x00\x00\x00\x00\x00\x01\x00\x02\x44\xa3\ -\x00\x00\x01\x9b\xc6\xe6\xb1\x6c\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3d\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x46\ @@ -27680,295 +27690,295 @@ \x00\x00\x06\xa0\x00\x02\x00\x00\x00\x07\x00\x00\x00\x3f\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x06\xb2\x00\x00\x00\x00\x00\x01\x00\x02\x46\xa9\ -\x00\x00\x01\x98\x55\x96\x0d\x7b\ +\x00\x00\x01\x9a\x72\xe1\x95\x8f\ \x00\x00\x06\xe6\x00\x00\x00\x00\x00\x01\x00\x02\x51\x0d\ -\x00\x00\x01\x98\x55\x96\x0d\x7b\ +\x00\x00\x01\x9a\x72\xe1\x95\x93\ \x00\x00\x07\x1a\x00\x00\x00\x00\x00\x01\x00\x02\x5b\x36\ -\x00\x00\x01\x98\x55\x96\x0d\x7b\ +\x00\x00\x01\x9a\x72\xe1\x95\x8f\ \x00\x00\x07\x54\x00\x00\x00\x00\x00\x01\x00\x02\x65\x8e\ -\x00\x00\x01\x98\x55\x96\x0d\x7b\ +\x00\x00\x01\x9a\x72\xe1\x95\x93\ \x00\x00\x07\x88\x00\x00\x00\x00\x00\x01\x00\x02\x6f\xa6\ -\x00\x00\x01\x98\x55\x96\x0d\x7b\ +\x00\x00\x01\x9a\x72\xe1\x95\x8f\ \x00\x00\x07\xbe\x00\x00\x00\x00\x00\x01\x00\x02\x79\xcf\ -\x00\x00\x01\x98\x55\x96\x0d\x7b\ +\x00\x00\x01\x9a\x72\xe1\x95\x93\ \x00\x00\x07\xf6\x00\x00\x00\x00\x00\x01\x00\x02\x83\xe5\ -\x00\x00\x01\x98\x55\x96\x0d\x7b\ +\x00\x00\x01\x9a\x72\xe1\x95\x93\ \x00\x00\x08\x2c\x00\x00\x00\x00\x00\x01\x00\x02\x8e\x4b\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x08\x54\x00\x00\x00\x00\x00\x01\x00\x02\x98\x7a\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ \x00\x00\x08\x80\x00\x00\x00\x00\x00\x01\x00\x02\xa2\xc1\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x08\xbc\x00\x00\x00\x00\x00\x01\x00\x02\xa4\xc2\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x08\xe8\x00\x00\x00\x00\x00\x01\x00\x02\xc0\x07\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x09\x14\x00\x00\x00\x00\x00\x01\x00\x02\xc5\x50\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4d\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x03\x00\x00\x00\x4e\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x09\x48\x00\x00\x00\x00\x00\x01\x00\x02\xc8\xc8\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ -\x00\x00\x09\x66\x00\x00\x00\x00\x00\x01\x00\x02\xd1\xaa\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ -\x00\x00\x09\x7a\x00\x00\x00\x00\x00\x01\x00\x02\xd7\x8b\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ +\x00\x00\x01\x9d\x06\xcd\x70\x83\ +\x00\x00\x09\x66\x00\x00\x00\x00\x00\x01\x00\x02\xd2\x85\ +\x00\x00\x01\x9d\x06\xcd\x70\x8f\ +\x00\x00\x09\x7a\x00\x00\x00\x00\x00\x01\x00\x02\xd8\x58\ +\x00\x00\x01\x9d\x06\xcd\x70\xa3\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x52\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x53\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x09\x94\x00\x00\x00\x00\x00\x01\x00\x02\xe3\x1e\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ -\x00\x00\x09\xbc\x00\x00\x00\x00\x00\x01\x00\x02\xe8\x19\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x09\xf4\x00\x00\x00\x00\x00\x01\x00\x02\xf0\xed\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x0a\x2c\x00\x00\x00\x00\x00\x01\x00\x02\xf9\x91\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x0a\x5c\x00\x00\x00\x00\x00\x01\x00\x03\x01\x30\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ -\x00\x00\x0a\x80\x00\x00\x00\x00\x00\x01\x00\x03\x09\x0c\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ -\x00\x00\x0a\xa4\x00\x00\x00\x00\x00\x01\x00\x03\x10\xc2\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x0a\xd8\x00\x00\x00\x00\x00\x01\x00\x03\x18\xd0\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x0b\x0c\x00\x00\x00\x00\x00\x01\x00\x03\x20\xb2\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x0b\x40\x00\x00\x00\x00\x00\x01\x00\x03\x28\x7c\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x0b\x7a\x00\x00\x00\x00\x00\x01\x00\x03\x2f\xe3\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ -\x00\x00\x0b\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x33\xa0\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ -\x00\x00\x0b\xcc\x00\x00\x00\x00\x00\x01\x00\x03\x37\x79\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x09\x94\x00\x00\x00\x00\x00\x01\x00\x02\xe3\xd2\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x09\xbc\x00\x00\x00\x00\x00\x01\x00\x02\xe8\xcd\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x09\xf4\x00\x00\x00\x00\x00\x01\x00\x02\xf1\xa1\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0a\x2c\x00\x00\x00\x00\x00\x01\x00\x02\xfa\x45\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0a\x5c\x00\x00\x00\x00\x00\x01\x00\x03\x01\xe4\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0a\x80\x00\x00\x00\x00\x00\x01\x00\x03\x09\xc0\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0a\xa4\x00\x00\x00\x00\x00\x01\x00\x03\x11\x76\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0a\xd8\x00\x00\x00\x00\x00\x01\x00\x03\x19\x84\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0b\x0c\x00\x00\x00\x00\x00\x01\x00\x03\x21\x66\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0b\x40\x00\x00\x00\x00\x00\x01\x00\x03\x29\x30\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0b\x7a\x00\x00\x00\x00\x00\x01\x00\x03\x30\x97\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0b\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x34\x54\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0b\xcc\x00\x00\x00\x00\x00\x01\x00\x03\x38\x2d\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x61\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x08\x00\x00\x00\x62\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0b\xf2\x00\x00\x00\x00\x00\x01\x00\x03\x3d\x82\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x0c\x1e\x00\x00\x00\x00\x00\x01\x00\x03\x60\x06\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x0c\x4c\x00\x00\x00\x00\x00\x01\x00\x03\x65\xf3\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ -\x00\x00\x0c\x76\x00\x00\x00\x00\x00\x01\x00\x03\x68\x13\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ -\x00\x00\x0c\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x70\xab\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x0c\xb8\x00\x00\x00\x00\x00\x01\x00\x03\x7f\xf4\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x0c\xe4\x00\x00\x00\x00\x00\x01\x00\x03\x86\x72\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x0d\x0c\x00\x00\x00\x00\x00\x01\x00\x03\x90\xec\ -\x00\x00\x01\x9a\x27\x73\xa6\xfc\ +\x00\x00\x0b\xf2\x00\x00\x00\x00\x00\x01\x00\x03\x3e\x36\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0c\x1e\x00\x00\x00\x00\x00\x01\x00\x03\x60\xba\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0c\x4c\x00\x00\x00\x00\x00\x01\x00\x03\x66\xa7\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0c\x76\x00\x00\x00\x00\x00\x01\x00\x03\x68\xc7\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0c\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x71\x5f\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0c\xb8\x00\x00\x00\x00\x00\x01\x00\x03\x80\xa8\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0c\xe4\x00\x00\x00\x00\x00\x01\x00\x03\x87\x26\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x0d\x0c\x00\x00\x00\x00\x00\x01\x00\x03\x91\xa0\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x6b\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x6c\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0d\x42\x00\x00\x00\x00\x00\x01\x00\x03\x9a\x33\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ -\x00\x00\x0d\x70\x00\x00\x00\x00\x00\x01\x00\x03\x9c\xda\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ -\x00\x00\x0d\x9e\x00\x01\x00\x00\x00\x01\x00\x03\xa9\x57\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ -\x00\x00\x0d\xca\x00\x00\x00\x00\x00\x01\x00\x03\xd6\xd6\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ -\x00\x00\x0d\xea\x00\x00\x00\x00\x00\x01\x00\x03\xdb\x8d\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ -\x00\x00\x0e\x1c\x00\x01\x00\x00\x00\x01\x00\x04\x34\x86\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ -\x00\x00\x0e\x4e\x00\x00\x00\x00\x00\x01\x00\x04\x69\x20\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x0e\x68\x00\x00\x00\x00\x00\x01\x00\x04\x6e\x6a\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ -\x00\x00\x0e\x82\x00\x00\x00\x00\x00\x01\x00\x04\x73\xf9\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ -\x00\x00\x0e\x9c\x00\x00\x00\x00\x00\x01\x00\x04\x79\x62\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x0e\xb4\x00\x00\x00\x00\x00\x01\x00\x04\x85\x40\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ -\x00\x00\x0e\xd2\x00\x00\x00\x00\x00\x01\x00\x04\x8b\x44\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x0d\x42\x00\x00\x00\x00\x00\x01\x00\x03\x9a\xe7\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x0d\x70\x00\x00\x00\x00\x00\x01\x00\x03\x9d\x8e\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0d\x9e\x00\x01\x00\x00\x00\x01\x00\x03\xaa\x0b\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0d\xca\x00\x00\x00\x00\x00\x01\x00\x03\xd7\x8a\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x0d\xea\x00\x00\x00\x00\x00\x01\x00\x03\xdc\x41\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x0e\x1c\x00\x01\x00\x00\x00\x01\x00\x04\x35\x3a\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x0e\x4e\x00\x00\x00\x00\x00\x01\x00\x04\x69\xd4\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0e\x68\x00\x00\x00\x00\x00\x01\x00\x04\x6f\x1e\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0e\x82\x00\x00\x00\x00\x00\x01\x00\x04\x74\xad\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0e\x9c\x00\x00\x00\x00\x00\x01\x00\x04\x7a\x16\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0e\xb4\x00\x00\x00\x00\x00\x01\x00\x04\x85\xf4\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0e\xd2\x00\x00\x00\x00\x00\x01\x00\x04\x8b\xf8\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x79\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x7a\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0e\xfa\x00\x00\x00\x00\x00\x01\x00\x04\x90\x79\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x0f\x0e\x00\x00\x00\x00\x00\x01\x00\x04\x96\x76\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x0f\x20\x00\x00\x00\x00\x00\x01\x00\x04\x97\xfc\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x0f\x32\x00\x00\x00\x00\x00\x01\x00\x04\x9d\xf6\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ +\x00\x00\x0e\xfa\x00\x00\x00\x00\x00\x01\x00\x04\x91\x2d\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0f\x0e\x00\x00\x00\x00\x00\x01\x00\x04\x97\x2a\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0f\x20\x00\x00\x00\x00\x00\x01\x00\x04\x98\xb0\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0f\x32\x00\x00\x00\x00\x00\x01\x00\x04\x9e\xaa\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7f\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x80\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0f\x46\x00\x00\x00\x00\x00\x01\x00\x04\xa0\x4c\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ -\x00\x00\x0f\x72\x00\x00\x00\x00\x00\x01\x00\x04\xa7\x33\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x0f\x46\x00\x00\x00\x00\x00\x01\x00\x04\xa1\x00\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0f\x72\x00\x00\x00\x00\x00\x01\x00\x04\xa7\xe7\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x83\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x84\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x46\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x88\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0f\x96\x00\x01\x00\x00\x00\x01\x00\x04\xb0\x97\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x0f\xba\x00\x00\x00\x00\x00\x01\x00\x04\xbb\xe8\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x0f\xdc\x00\x00\x00\x00\x00\x01\x00\x04\xc3\x8d\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x0f\xf8\x00\x00\x00\x00\x00\x01\x00\x04\xd4\x8d\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ -\x00\x00\x10\x18\x00\x00\x00\x00\x00\x01\x00\x04\xda\x26\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ -\x00\x00\x10\x38\x00\x00\x00\x00\x00\x01\x00\x04\xe0\x4d\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ -\x00\x00\x10\x58\x00\x00\x00\x00\x00\x01\x00\x04\xe4\xa2\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ -\x00\x00\x10\x78\x00\x00\x00\x00\x00\x01\x00\x04\xea\x26\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ -\x00\x00\x10\x98\x00\x00\x00\x00\x00\x01\x00\x04\xef\xbf\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ -\x00\x00\x10\xca\x00\x00\x00\x00\x00\x01\x00\x04\xf4\xc1\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ -\x00\x00\x10\xea\x00\x00\x00\x00\x00\x01\x00\x04\xfa\x5a\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ -\x00\x00\x11\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x04\xce\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ -\x00\x00\x11\x52\x00\x00\x00\x00\x00\x01\x00\x05\x0f\x48\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ -\x00\x00\x11\x86\x00\x00\x00\x00\x00\x01\x00\x05\x19\xa7\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ -\x00\x00\x11\xba\x00\x00\x00\x00\x00\x01\x00\x05\x24\xf2\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ +\x00\x00\x0f\x96\x00\x01\x00\x00\x00\x01\x00\x04\xb1\x4b\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x0f\xba\x00\x00\x00\x00\x00\x01\x00\x04\xbc\x9c\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0f\xdc\x00\x00\x00\x00\x00\x01\x00\x04\xc4\x41\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0f\xf8\x00\x00\x00\x00\x00\x01\x00\x04\xd5\x41\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ +\x00\x00\x10\x18\x00\x00\x00\x00\x00\x01\x00\x04\xda\xda\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ +\x00\x00\x10\x38\x00\x00\x00\x00\x00\x01\x00\x04\xe1\x01\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ +\x00\x00\x10\x58\x00\x00\x00\x00\x00\x01\x00\x04\xe5\x56\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ +\x00\x00\x10\x78\x00\x00\x00\x00\x00\x01\x00\x04\xea\xda\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ +\x00\x00\x10\x98\x00\x00\x00\x00\x00\x01\x00\x04\xf0\x73\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ +\x00\x00\x10\xca\x00\x00\x00\x00\x00\x01\x00\x04\xf5\x75\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ +\x00\x00\x10\xea\x00\x00\x00\x00\x00\x01\x00\x04\xfb\x0e\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ +\x00\x00\x11\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x05\x82\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ +\x00\x00\x11\x52\x00\x00\x00\x00\x00\x01\x00\x05\x0f\xfc\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ +\x00\x00\x11\x86\x00\x00\x00\x00\x00\x01\x00\x05\x1a\x5b\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ +\x00\x00\x11\xba\x00\x00\x00\x00\x00\x01\x00\x05\x25\xa6\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x95\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x96\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x2f\x66\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x38\xfc\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x12\x4e\x00\x00\x00\x00\x00\x01\x00\x05\x44\xd8\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x12\x72\x00\x00\x00\x00\x00\x01\x00\x05\x4b\x1c\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x12\x9c\x00\x00\x00\x00\x00\x01\x00\x05\x52\xa5\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x12\xc8\x00\x00\x00\x00\x00\x01\x00\x05\x59\x03\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x12\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x60\xf2\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ -\x00\x00\x09\x66\x00\x00\x00\x00\x00\x01\x00\x05\x6e\x95\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ -\x00\x00\x09\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x74\x76\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ -\x00\x00\x13\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x80\x09\ -\x00\x00\x01\x9a\x27\x73\xa6\xfc\ +\x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x30\x1a\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x39\xb0\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x12\x4e\x00\x00\x00\x00\x00\x01\x00\x05\x45\x8c\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x12\x72\x00\x00\x00\x00\x00\x01\x00\x05\x4b\xd0\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x12\x9c\x00\x00\x00\x00\x00\x01\x00\x05\x53\x59\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x12\xc8\x00\x00\x00\x00\x00\x01\x00\x05\x59\xb7\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x12\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x61\xa6\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x09\x66\x00\x00\x00\x00\x00\x01\x00\x05\x6f\x49\ +\x00\x00\x01\x9d\x06\xcd\x70\x8f\ +\x00\x00\x09\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x75\x1c\ +\x00\x00\x01\x9d\x06\xcd\x70\xa3\ +\x00\x00\x13\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x80\x96\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa1\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa2\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x13\x44\x00\x00\x00\x00\x00\x01\x00\x05\x87\xd3\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x13\x44\x00\x00\x00\x00\x00\x01\x00\x05\x88\x60\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa4\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x2b\x00\x00\x00\xa5\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x13\x64\x00\x00\x00\x00\x00\x01\x00\x05\x8c\x99\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x13\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x94\x4d\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x13\x92\x00\x00\x00\x00\x00\x01\x00\x05\x96\x72\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x13\xca\x00\x00\x00\x00\x00\x01\x00\x05\x97\xf2\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x13\xe6\x00\x00\x00\x00\x00\x01\x00\x05\x9f\x9f\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x14\x06\x00\x00\x00\x00\x00\x01\x00\x05\xa4\x74\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x14\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xa5\x64\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ -\x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xa8\x90\ -\x00\x00\x01\x9b\xc6\xe6\xb1\x6c\ -\x00\x00\x14\x58\x00\x00\x00\x00\x00\x01\x00\x05\xac\xb9\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x14\x7e\x00\x00\x00\x00\x00\x01\x00\x05\xb2\xf0\ -\x00\x00\x01\x99\x96\xf9\x85\x6f\ -\x00\x00\x14\x98\x00\x00\x00\x00\x00\x01\x00\x05\xc7\xfd\ -\x00\x00\x01\x9a\x27\x73\xa6\xf8\ -\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xcc\xed\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ -\x00\x00\x14\xd0\x00\x00\x00\x00\x00\x01\x00\x05\xcf\xec\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x14\xe6\x00\x00\x00\x00\x00\x01\x00\x05\xd5\xf6\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ -\x00\x00\x15\x18\x00\x00\x00\x00\x00\x01\x00\x05\xd9\x45\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ -\x00\x00\x15\x30\x00\x00\x00\x00\x00\x01\x00\x05\xdc\x37\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ -\x00\x00\x15\x46\x00\x00\x00\x00\x00\x01\x00\x05\xe2\x2e\ -\x00\x00\x01\x99\x96\xf9\x85\x6f\ -\x00\x00\x15\x5a\x00\x00\x00\x00\x00\x01\x00\x05\xe4\x3f\ -\x00\x00\x01\x99\x96\xf9\x85\x6f\ -\x00\x00\x15\x72\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x02\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ -\x00\x00\x15\x98\x00\x00\x00\x00\x00\x01\x00\x05\xf1\xb6\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ -\x00\x00\x15\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xf7\x7e\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x15\xe0\x00\x00\x00\x00\x00\x01\x00\x05\xfa\xcc\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x16\x02\x00\x00\x00\x00\x00\x01\x00\x05\xfe\x5a\ -\x00\x00\x01\x9a\x27\x73\xa6\xf8\ -\x00\x00\x16\x28\x00\x00\x00\x00\x00\x01\x00\x06\x02\xfb\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x16\x3c\x00\x00\x00\x00\x00\x01\x00\x06\x0c\xcd\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x16\x68\x00\x00\x00\x00\x00\x01\x00\x06\x12\x17\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x06\x18\x1e\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x16\xa6\x00\x00\x00\x00\x00\x01\x00\x06\x19\x02\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ -\x00\x00\x16\xd2\x00\x00\x00\x00\x00\x01\x00\x06\x1b\x4b\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x16\xe8\x00\x00\x00\x00\x00\x01\x00\x06\x21\xfb\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x17\x04\x00\x00\x00\x00\x00\x01\x00\x06\x25\x3f\ -\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ -\x00\x00\x17\x38\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x25\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x17\x50\x00\x00\x00\x00\x00\x01\x00\x06\x2d\x51\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x17\x6a\x00\x00\x00\x00\x00\x01\x00\x06\x33\x12\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x17\x8c\x00\x00\x00\x00\x00\x01\x00\x06\x34\x34\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x17\xaa\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x27\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x17\xca\x00\x00\x00\x00\x00\x01\x00\x06\x3d\x2b\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x17\xec\x00\x00\x00\x00\x00\x01\x00\x06\x3e\x4c\ -\x00\x00\x01\x98\x55\x96\x0d\x49\ -\x00\x00\x18\x0c\x00\x00\x00\x00\x00\x01\x00\x06\x41\x20\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x18\x3a\x00\x00\x00\x00\x00\x01\x00\x06\x49\x8c\ -\x00\x00\x01\x99\x7d\x04\xc3\x11\ -\x00\x00\x18\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x51\x4c\ -\x00\x00\x01\x98\x55\x96\x0d\x3f\ -\x00\x00\x18\x82\x00\x00\x00\x00\x00\x01\x00\x06\x56\x67\ -\x00\x00\x01\x98\x55\x96\x0d\x35\ -\x00\x00\x18\xaa\x00\x00\x00\x00\x00\x01\x00\x06\x57\xba\ -\x00\x00\x01\x98\x55\x96\x0d\x2b\ +\x00\x00\x13\x64\x00\x00\x00\x00\x00\x01\x00\x05\x8d\x26\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x13\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x94\xda\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x13\x92\x00\x00\x00\x00\x00\x01\x00\x05\x96\xff\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x13\xca\x00\x00\x00\x00\x00\x01\x00\x05\x98\x7f\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x13\xe6\x00\x00\x00\x00\x00\x01\x00\x05\xa0\x2c\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x14\x06\x00\x00\x00\x00\x00\x01\x00\x05\xa5\x01\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x14\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xa5\xf1\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ +\x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xa9\x1d\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ +\x00\x00\x14\x58\x00\x00\x00\x00\x00\x01\x00\x05\xad\x46\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x14\x7e\x00\x00\x00\x00\x00\x01\x00\x05\xb3\x7d\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x14\x98\x00\x00\x00\x00\x00\x01\x00\x05\xc8\x8a\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xcd\x7a\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x14\xd0\x00\x00\x00\x00\x00\x01\x00\x05\xd0\x79\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x14\xe6\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x83\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x15\x18\x00\x00\x00\x00\x00\x01\x00\x05\xd9\xd2\ +\x00\x00\x01\x9c\xe2\x99\x3f\x98\ +\x00\x00\x15\x30\x00\x00\x00\x00\x00\x01\x00\x05\xdc\xc4\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x15\x46\x00\x00\x00\x00\x00\x01\x00\x05\xe2\xbb\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x15\x5a\x00\x00\x00\x00\x00\x01\x00\x05\xe4\xcc\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x15\x72\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x8f\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x15\x98\x00\x00\x00\x00\x00\x01\x00\x05\xf2\x43\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ +\x00\x00\x15\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xf8\x0b\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x15\xe0\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x59\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x16\x02\x00\x00\x00\x00\x00\x01\x00\x05\xfe\xe7\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x16\x28\x00\x00\x00\x00\x00\x01\x00\x06\x03\x88\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x16\x3c\x00\x00\x00\x00\x00\x01\x00\x06\x0d\x5a\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x16\x68\x00\x00\x00\x00\x00\x01\x00\x06\x12\xa4\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x06\x18\xab\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x16\xa6\x00\x00\x00\x00\x00\x01\x00\x06\x19\x8f\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x16\xd2\x00\x00\x00\x00\x00\x01\x00\x06\x1b\xd8\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x16\xe8\x00\x00\x00\x00\x00\x01\x00\x06\x22\x88\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x17\x04\x00\x00\x00\x00\x00\x01\x00\x06\x25\xcc\ +\x00\x00\x01\x9c\xe2\x99\x3f\x9c\ +\x00\x00\x17\x38\x00\x00\x00\x00\x00\x01\x00\x06\x2c\xb2\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x17\x50\x00\x00\x00\x00\x00\x01\x00\x06\x2d\xde\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x17\x6a\x00\x00\x00\x00\x00\x01\x00\x06\x33\x9f\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x17\x8c\x00\x00\x00\x00\x00\x01\x00\x06\x34\xc1\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x17\xaa\x00\x00\x00\x00\x00\x01\x00\x06\x3a\xb4\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x17\xca\x00\x00\x00\x00\x00\x01\x00\x06\x3d\xb8\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x17\xec\x00\x00\x00\x00\x00\x01\x00\x06\x3e\xd9\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x18\x0c\x00\x00\x00\x00\x00\x01\x00\x06\x41\xad\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x18\x3a\x00\x00\x00\x00\x00\x01\x00\x06\x4a\x19\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x18\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x51\xd9\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x18\x82\x00\x00\x00\x00\x00\x01\x00\x06\x56\xf4\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x18\xaa\x00\x00\x00\x00\x00\x01\x00\x06\x58\x47\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ " qt_version = [int(v) for v in QtCore.qVersion().split('.')] diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/blower.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/blower.svg index 027ea01c..eb090a6f 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/blower.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/blower.svg @@ -7,9 +7,9 @@ } - - - - - + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/fan.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/fan.svg index 46bccde3..8384f84e 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/fan.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/fan.svg @@ -7,5 +7,5 @@ } - + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/fan_cage.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/fan_cage.svg index 59a32e1b..5a63dbc9 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/fan_cage.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/fan_cage.svg @@ -1 +1,12 @@ - \ No newline at end of file + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/utils/RepeatedTimer.py b/BlocksScreen/lib/utils/RepeatedTimer.py index 42b42aa0..698c2ce8 100644 --- a/BlocksScreen/lib/utils/RepeatedTimer.py +++ b/BlocksScreen/lib/utils/RepeatedTimer.py @@ -10,6 +10,7 @@ def __init__( *args, **kwargs, ): + """Initialize a repeating timer that invokes callback every timeout seconds.""" super().__init__(daemon=True) self.name = name self._timeout = timeout @@ -17,6 +18,7 @@ def __init__( self._args = args self._kwargs = kwargs + self._lock = threading.Lock() self.running = False self.timeoutEvent = threading.Event() self.stopEvent = threading.Event() @@ -24,36 +26,42 @@ def __init__( self.startTimer() def _run(self): - self.running = False - self.startTimer() - self.stopEvent.wait() + """Invoke the callback and restart the timer loop, unless stopped.""" + with self._lock: + self.running = False + if self.stopEvent.is_set(): + return if callable(self._function): self._function(*self._args, **self._kwargs) + self.startTimer() def startTimer(self): """Start timer""" - if self.running is False: + with self._lock: + if self.running: + return + self.stopEvent.clear() try: - self._timer = threading.Timer(self._timeout, self._run) - self._timer.daemon = True - self._timer.start() - if not self.stopEvent.is_set(): - self.stopEvent.set() + timer = threading.Timer(self._timeout, self._run) + timer.daemon = True + self._timer = timer + self.running = True except Exception as e: + self.running = False raise Exception( f"RepeatedTimer {self.name} error while starting timer, error: {e}" - ) - finally: - self.running = False - self.running = True + ) from e + # Start outside the lock to avoid holding it during thread creation + timer.start() def stopTimer(self): """Stop timer""" - if self._timer is None: - return - if self.running: - self._timer.cancel() - self._timer.join() + with self._lock: + if self._timer is None or not self.running: + return + timer = self._timer self._timer = None - self.stopEvent.clear() self.running = False + self.stopEvent.set() + timer.cancel() + timer.join() diff --git a/BlocksScreen/lib/utils/blocks_button.py b/BlocksScreen/lib/utils/blocks_button.py index 292b5125..ec6b24c7 100644 --- a/BlocksScreen/lib/utils/blocks_button.py +++ b/BlocksScreen/lib/utils/blocks_button.py @@ -1,5 +1,6 @@ -import typing import enum +import typing + from PyQt6 import QtCore, QtGui, QtWidgets @@ -106,7 +107,7 @@ def setProperty(self, name: str, value: typing.Any): self.text_color = QtGui.QColor(value) self.update() - def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): + def paintEvent(self, e: QtGui.QPaintEvent | None): """Re-implemented method, paint widget""" painter = QtGui.QPainter(self) painter.setRenderHint(painter.RenderHint.Antialiasing, True) @@ -116,17 +117,7 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): _style = self.style() if not _style or not _rect: return - # Flat button control opt = QtWidgets.QStyleOptionButton() - draw_frame = ( - not self._is_flat - or self.underMouse() - or opt.state & QtWidgets.QStyle.StateFlag.State_Sunken - ) - if draw_frame: - _style.drawControl( - QtWidgets.QStyle.ControlElement.CE_PushButtonLabel, opt, painter, self - ) _style.drawControl( QtWidgets.QStyle.ControlElement.CE_PushButtonLabel, opt, painter, self ) diff --git a/BlocksScreen/lib/utils/blocks_frame.py b/BlocksScreen/lib/utils/blocks_frame.py index 7de7514e..8bd2d6b8 100644 --- a/BlocksScreen/lib/utils/blocks_frame.py +++ b/BlocksScreen/lib/utils/blocks_frame.py @@ -1,6 +1,7 @@ -from PyQt6 import QtCore, QtGui, QtWidgets import typing +from PyQt6 import QtCore, QtGui, QtWidgets + class BlocksCustomFrame(QtWidgets.QFrame): def __init__(self, parent=None): diff --git a/BlocksScreen/lib/utils/blocks_progressbar.py b/BlocksScreen/lib/utils/blocks_progressbar.py index 414097bb..60950b67 100644 --- a/BlocksScreen/lib/utils/blocks_progressbar.py +++ b/BlocksScreen/lib/utils/blocks_progressbar.py @@ -1,5 +1,6 @@ import typing -from PyQt6 import QtWidgets, QtGui, QtCore + +from PyQt6 import QtCore, QtGui, QtWidgets class CustomProgressBar(QtWidgets.QProgressBar): @@ -30,6 +31,12 @@ def __init__(self, parent=None): self.setMinimumSize(100, 100) self._inner_rect: QtCore.QRectF = QtCore.QRectF() + def reset(self) -> None: + """Reset progress to zero.""" + self.progress_value = 0 + super().reset() + self.update() + def set_padding(self, value) -> None: """Set widget padding""" self._padding = value @@ -93,8 +100,8 @@ def setValue(self, value: float) -> None: Raises: ValueError: If provided value in not between 0.0 and 1.0 """ - if not (0 <= value <= 100): - raise ValueError("Argument `value` expected value between 0.0 and 1.0 ") + if not (0.0 <= value <= 1.0): + raise ValueError("Argument `value` expected value between 0.0 and 1.0") value *= 100 self.progress_value = value self.update() @@ -159,7 +166,7 @@ def _draw_circular_bar( bg_pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) painter.setPen(bg_pen) painter.drawArc(arc_rect, arc_start, arc_span) - if self.progress_value is not None: + if self.progress_value is not None and self.progress_value > 0: gradient = QtGui.QConicalGradient(arc_rect.center(), -90) gradient.setColorAt(0.0, self._bar_color) gradient.setColorAt(1.0, QtGui.QColor(100, 100, 100)) diff --git a/BlocksScreen/lib/utils/blocks_tabwidget.py b/BlocksScreen/lib/utils/blocks_tabwidget.py index 4696d967..cc403bd0 100644 --- a/BlocksScreen/lib/utils/blocks_tabwidget.py +++ b/BlocksScreen/lib/utils/blocks_tabwidget.py @@ -1,4 +1,4 @@ -from PyQt6 import QtWidgets, QtGui, QtCore +from PyQt6 import QtCore, QtGui, QtWidgets class NotificationTabBar(QtWidgets.QTabBar): diff --git a/BlocksScreen/lib/utils/check_button.py b/BlocksScreen/lib/utils/check_button.py index e5b184d5..79c998c1 100644 --- a/BlocksScreen/lib/utils/check_button.py +++ b/BlocksScreen/lib/utils/check_button.py @@ -1,4 +1,3 @@ -import typing from PyQt6 import QtCore, QtGui, QtWidgets @@ -39,7 +38,7 @@ def setText(self, text: str | None) -> None: self.update() return - def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): + def paintEvent(self, e: QtGui.QPaintEvent | None): """Re-implemented method, paint widget, optimized for performance.""" painter = QtGui.QPainter(self) diff --git a/BlocksScreen/lib/utils/icon_button.py b/BlocksScreen/lib/utils/icon_button.py index 3880d285..55d9ebc1 100644 --- a/BlocksScreen/lib/utils/icon_button.py +++ b/BlocksScreen/lib/utils/icon_button.py @@ -1,4 +1,5 @@ import typing + from PyQt6 import QtCore, QtGui, QtWidgets @@ -135,7 +136,7 @@ def setProperty(self, name: str, value: typing.Any) -> bool: elif name == "has_text": self.has_text = value elif name == "name": - self._name = name + self._name = value elif name == "text_color": self.text_color = value return super().setProperty(name, value) diff --git a/BlocksScreen/lib/utils/toggleAnimatedButton.py b/BlocksScreen/lib/utils/toggleAnimatedButton.py index b9555876..371802eb 100644 --- a/BlocksScreen/lib/utils/toggleAnimatedButton.py +++ b/BlocksScreen/lib/utils/toggleAnimatedButton.py @@ -1,5 +1,6 @@ import enum import typing + from PyQt6 import QtCore, QtGui, QtWidgets @@ -33,6 +34,7 @@ def __init__(self, parent) -> None: - self.handle_radius * 2 ) + self.trailPath: QtGui.QPainterPath | None = None self.icon_pixmap: QtGui.QPixmap = QtGui.QPixmap() self._backgroundColor: QtGui.QColor = QtGui.QColor(223, 223, 223) self._handleColor: QtGui.QColor = QtGui.QColor(255, 100, 10) @@ -179,7 +181,7 @@ def setup_animation(self) -> None: def mousePressEvent(self, e: QtGui.QMouseEvent) -> None: """Re-implemented method, handle mouse press events""" - if self.trailPath: + if self.trailPath is not None: if self.trailPath.contains(e.pos().toPointF()) and self.underMouse(): if not self.slide_animation.state == self.slide_animation.State.Running: self._state = ToggleAnimatedButton.State(not self._state.value) @@ -214,7 +216,10 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: rect_norm = _rect.toRectF().normalized() min_x = rect_norm.x() max_x = rect_norm.x() + rect_norm.width() - rect_norm.height() * 0.80 - progress = (self._handle_position - min_x) / (max_x - min_x) + denominator = max_x - min_x + if denominator == 0: + return + progress = (self._handle_position - min_x) / denominator progress = max(0.0, min(1.0, progress)) # Inline color interpolation (no separate functions) @@ -237,6 +242,8 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: self.handleColor = QtGui.QColor(int(r), int(g), int(b), int(a)) + if self.trailPath is None: + return painter.fillPath( self.trailPath, bg_color if self.isEnabled() else self.disable_bg_color, diff --git a/BlocksScreen/screensaver.py b/BlocksScreen/screensaver.py index de02ba02..20cda0cf 100644 --- a/BlocksScreen/screensaver.py +++ b/BlocksScreen/screensaver.py @@ -3,15 +3,22 @@ class ScreenSaver(QtCore.QObject): + """Screensaver that uses X11 DPMS to blank the display after inactivity.""" + timer = QtCore.QTimer() - dpms_off_timeout = helper_methods.get_dpms_timeouts().get("off_timeout") - dpms_suspend_timeout = helper_methods.get_dpms_timeouts().get("suspend_timeout") - dpms_standby_timeout = helper_methods.get_dpms_timeouts().get("standby_timeout") touch_blocked: bool = False + _dpms_available: bool = hasattr(helper_methods, "get_dpms_timeouts") def __init__(self, parent) -> None: super().__init__() + dpms_timeouts = ( + helper_methods.get_dpms_timeouts() if self._dpms_available else {} + ) + self.dpms_off_timeout = dpms_timeouts.get("off_timeout") + self.dpms_suspend_timeout = dpms_timeouts.get("suspend_timeout") + self.dpms_standby_timeout = dpms_timeouts.get("standby_timeout") + self.screensaver_config = parent.config.get_section( "screensaver", fallback=None ) @@ -29,9 +36,11 @@ def __init__(self, parent) -> None: self.timer.start() def eventFilter(self, object, event) -> bool: - """Filter touch events considering DPMS Screen state""" + """Filter touch events considering DPMS screen state.""" + if not self._dpms_available: + return False - if event.type() in ( # Block Touch Filter and Wake Touch Filter + if event.type() in ( QtCore.QEvent.Type.TouchBegin, QtCore.QEvent.Type.TouchUpdate, QtCore.QEvent.Type.TouchEnd, @@ -52,14 +61,15 @@ def eventFilter(self, object, event) -> bool: self.touch_blocked = False helper_methods.set_dpms_mode(helper_methods.DPMSState.ON) self.timer.start() - return True # filter out the event, block touch events on the application + return True else: self.timer.stop() self.timer.start() return False def check_dpms(self) -> None: - """Checks the X11 extension dpms for the status of the screen""" + """Blank the display via DPMS standby.""" self.touch_blocked = True - helper_methods.set_dpms_mode(helper_methods.DPMSState.STANDBY) + if self._dpms_available: + helper_methods.set_dpms_mode(helper_methods.DPMSState.STANDBY) self.timer.stop() diff --git a/tests/network/test_network_ui.py b/tests/network/test_network_ui.py index 466ec14a..a60a330a 100644 --- a/tests/network/test_network_ui.py +++ b/tests/network/test_network_ui.py @@ -693,7 +693,7 @@ def test_transient_mismatch_retries(self, win, qapp): message="not compatible with device", error_code="nm_error", ) - with patch("BlocksScreen.lib.panels.networkWindow.QTimer") as mock_timer: + with patch("BlocksScreen.lib.panels.networkWindow.QtCore.QTimer") as mock_timer: w._on_operation_complete(result) mock_timer.singleShot.assert_called_once() # Loading should still be visible — retry is pending @@ -740,7 +740,7 @@ def test_wifi_on_with_saved_networks_starts_connect(self, win): ) ] nm.saved_networks = saved - with patch("BlocksScreen.lib.panels.networkWindow.QTimer") as mock_timer: + with patch("BlocksScreen.lib.panels.networkWindow.QtCore.QTimer") as mock_timer: w._handle_wifi_toggle(True) mock_timer.singleShot.assert_called() assert w._pending_operation == PendingOperation.WIFI_ON