From 0fa8559381dbd876a6172f3c00a47b52795ae9b8 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Thu, 19 Mar 2026 16:09:09 +0000 Subject: [PATCH] bugfix: resolve 40+ crashes, threading races, and Klipper protocol issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Threading & concurrency (moonrakerComm.py): - Move class-level state vars (connected/connecting/_reconnect_count/callback_table) into __init__ to prevent shared state across instances - Add _state_lock (RLock) and _request_lock guarding all reads/writes to those vars - Fix mutable default dict in send_request (params={} → params=None) - Fix unbound super() in OneShotTokenError (__init__ without class arg) - Guard _retry_timer with None before stopTimer/startTimer calls; stop existing timer in try_connection before creating a new one (prevents orphaned threads) - sendEvent → postEvent for cross-thread event delivery - Wrap json.loads in try/except; use .get() on all external response dicts - Fix dest_dir guard (False → None) in copy/move file; fix machine.update.client missing params dict; fix machine.update.rollback method typo (comma → dot) - Add klippy startup/disconnected retry cap (30 attempts) to prevent infinite loop - Add join(timeout=self.timeout+1) on _wst to avoid indefinite block RepeatedTimer: fix inverted stopEvent semantics (set=stop, clear=running) Pi runtime crashes: - cancelPage: fix AttributeError on file_metadata dict access - connectionPage: fix stale slot name - inputshaperPage: fix uninitialized currentItem reference - mainWindow: fix _handle_notify (dict.popitem → next(iter(items()))); fix global_back tab-bar gate; fix logger f-string - filamentTab: fix slot signature mismatch; fix stale _is_printing state; fix bare self.isVisible reference (missing parentheses) - jobStatusPage: use separate event_state var (never reassign lstate); capture file locals before clearing state; print_finish only on complete; layer fallback uses Mainsail ceil formula; layer_fallback flag decoupled from total_layer - sensorsPanel: removeWidget+deleteLater before clearing tracking dict on reconnect to prevent widget stacking - babystepPage: label sync with confirmed Klipper value, button reorder, code deduplication - probeHelperPage: only process is_active when key present in update dict (Klipper sends position-only partial updates after TESTZ) - screensaver: guard DPMS calls with hasattr(_dpms_available) check Python 3.11 typing (20 files): - Optional/Union/List/Dict → native syntax; add from __future__ import annotations - filament.brand setter: return type None; _spool_type initialized in __init__ - files.py: remove duplicate ReceivedFileData import; use events.ReceivedFileData helper_methods: - convert_bytes_to_mb: remove spurious self param; rename bytes → size_bytes - calculate_current_layer: Mainsail formula (ceil((z-first)/layer+1)), clamped - calculate_max_layers: extracted from calculate_current_layer; max(1,...) guard - normalize, check_filepath_permission, check_dir_existence: pathlib + type hints - estimate_print_time: modernized variable names and return type annotation - Remove unused get_file_loc/digest_hash stubs; remove normalize import from controlTab and tunePage controlTab/tunePage slider split: - controlTab.on_slider_change: handles fan_generics only (SET_FAN_SPEED) - tunePage: _on_speed_slider_change for print speed (M220) and on_slider_change for fan_generics — eliminates collision when fan is named speed --- BlocksScreen/BlocksScreen.py | 3 +- BlocksScreen/helper_methods.py | 175 +- BlocksScreen/lib/filament.py | 31 +- BlocksScreen/lib/files.py | 21 +- BlocksScreen/lib/moonrakerComm.py | 201 +- BlocksScreen/lib/moonrest.py | 9 +- BlocksScreen/lib/panels/controlTab.py | 44 +- BlocksScreen/lib/panels/filamentTab.py | 63 +- BlocksScreen/lib/panels/mainWindow.py | 116 +- BlocksScreen/lib/panels/networkWindow.py | 43 +- BlocksScreen/lib/panels/printTab.py | 42 +- BlocksScreen/lib/panels/utilitiesTab.py | 36 +- .../lib/panels/widgets/babystepPage.py | 507 ++-- .../lib/panels/widgets/bannerPopup.py | 2 + BlocksScreen/lib/panels/widgets/basePopup.py | 34 +- BlocksScreen/lib/panels/widgets/cancelPage.py | 61 +- .../lib/panels/widgets/confirmPage.py | 84 +- .../lib/panels/widgets/connectionPage.py | 14 +- BlocksScreen/lib/panels/widgets/fansPage.py | 9 +- BlocksScreen/lib/panels/widgets/filesPage.py | 9 +- .../lib/panels/widgets/inputshaperPage.py | 21 +- .../lib/panels/widgets/jobStatusPage.py | 206 +- .../lib/panels/widgets/notificationPage.py | 27 +- BlocksScreen/lib/panels/widgets/numpadPage.py | 3 +- .../lib/panels/widgets/optionCardWidget.py | 2 +- .../lib/panels/widgets/popupDialogWidget.py | 8 +- .../lib/panels/widgets/printcorePage.py | 2 + .../lib/panels/widgets/probeHelperPage.py | 84 +- .../lib/panels/widgets/sensorWidget.py | 15 +- .../lib/panels/widgets/sensorsPanel.py | 40 +- .../lib/panels/widgets/troubleshootPage.py | 9 +- BlocksScreen/lib/panels/widgets/tunePage.py | 33 +- BlocksScreen/lib/panels/widgets/updatePage.py | 21 +- BlocksScreen/lib/printer.py | 180 +- BlocksScreen/lib/qrcode_gen.py | 3 +- .../lib/ui/resources/icon_resources_rc.py | 2052 +++++++++-------- .../ui/resources/media/btn_icons/blower.svg | 10 +- .../lib/ui/resources/media/btn_icons/fan.svg | 2 +- .../ui/resources/media/btn_icons/fan_cage.svg | 13 +- BlocksScreen/lib/utils/RepeatedTimer.py | 46 +- BlocksScreen/lib/utils/blocks_button.py | 15 +- BlocksScreen/lib/utils/blocks_frame.py | 3 +- BlocksScreen/lib/utils/blocks_progressbar.py | 15 +- BlocksScreen/lib/utils/blocks_tabwidget.py | 2 +- BlocksScreen/lib/utils/check_button.py | 3 +- BlocksScreen/lib/utils/icon_button.py | 3 +- .../lib/utils/toggleAnimatedButton.py | 11 +- BlocksScreen/screensaver.py | 26 +- tests/network/test_network_ui.py | 4 +- 49 files changed, 2258 insertions(+), 2105 deletions(-) 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/helper_methods.py b/BlocksScreen/helper_methods.py index 25b76cac..114ff5f1 100644 --- a/BlocksScreen/helper_methods.py +++ b/BlocksScreen/helper_methods.py @@ -9,10 +9,10 @@ import ctypes import enum import logging +import math import os import pathlib import struct -import typing logger = logging.getLogger(__name__) @@ -21,7 +21,7 @@ libxext = ctypes.CDLL("libXext.so.6") class DPMSState(enum.Enum): - """Available DPMS states""" + """Available DPMS states.""" FAIL = -1 ON = 0 @@ -86,7 +86,7 @@ def set_dpms_mode(mode: DPMSState) -> None: finally: libxext.XCloseDisplay(display) - def get_dpms_timeouts() -> typing.Dict: + def get_dpms_timeouts() -> dict: """Get current DPMS timeouts""" _display_name = ctypes.c_char_p(b":0") libxext.XOpenDisplay.restype = ctypes.c_void_p @@ -118,9 +118,7 @@ def get_dpms_timeouts() -> typing.Dict: "off_seconds": _off_timeout, } - def set_dpms_timeouts( - suspend: int = 0, standby: int = 0, off: int = 0 - ) -> typing.Dict: + def set_dpms_timeouts(suspend: int = 0, standby: int = 0, off: int = 0) -> dict: """Set DPMS timeout""" _display_name = ctypes.c_char_p(b":0") libxext.XOpenDisplay.restype = ctypes.c_void_p @@ -155,11 +153,11 @@ def set_dpms_timeouts( "off_seconds": _off_timeout, } - def get_dpms_info() -> typing.Dict: + def get_dpms_info() -> dict: """Get DPMS information Returns: - typing.Dict: Dpms state + dict: Dpms state """ _dpms_state = DPMSState.FAIL onoff = 0 @@ -227,133 +225,122 @@ def disable_dpms() -> None: logger.exception(f"Unexpected exception occurred {e}") -def convert_bytes_to_mb(self, bytes: int | float) -> float: - """Converts byte size to megabyte size +def convert_bytes_to_mb(size_bytes: int | float) -> float: + """Converts byte size to megabyte size. Args: - bytes (int | float): bytes + size_bytes: Value in bytes. Returns: - mb: float that represents the number of mb + Equivalent value in megabytes. """ _relation = 2 ** (-20) - return bytes * _relation + return size_bytes * _relation def calculate_current_layer( z_position: float, - object_height: float, layer_height: float, first_layer_height: float, + max_layers: int = 0, ) -> int: - """Calculated the current printing layer given the GCODE z position received by the - gcode_move object update. - Also updates the label where the current layer should be displayed + """Calculate current layer from Z position (fallback when Klipper + does not provide ``print_stats.info.current_layer``). + + Formula ported from Mainsail ``getPrintCurrentLayer`` getter: + ``src/store/printer/getters.ts`` in ``mainsail-crew/mainsail``. + + Uses ``ceil((z - first_layer_height) / layer_height + 1)`` + and clamps the result to ``[0, max_layers]``. Returns: - int: Current layer + int: Current layer number (0 when not yet printing). """ - if z_position == 0: - return -1 - if z_position <= first_layer_height: - return 1 + if layer_height <= 0 or first_layer_height < 0: + return 0 - _current_layer = (z_position) / layer_height + layer = math.ceil((z_position - first_layer_height) / layer_height + 1) + if max_layers > 0 and layer > max_layers: + return max_layers + return layer if layer > 0 else 0 - return int(_current_layer) +def calculate_max_layers( + object_height: float, + layer_height: float, + first_layer_height: float, +) -> int: + """Calculate total layers from metadata dimensions (fallback when + Klipper does not provide ``print_stats.info.total_layer``). -def estimate_print_time(seconds: int) -> list: - """Convert time in seconds format to days, hours, minutes, seconds. + Formula ported from Mainsail ``getPrintMaxLayers`` getter: + ``src/store/printer/getters.ts`` in ``mainsail-crew/mainsail``. - Args: - seconds (int): Seconds + Uses ``ceil((object_height - first_layer_height) / layer_height + 1)``. Returns: - list: list that contains the converted information [days, hours, minutes, seconds] + int: Total layer count, or 0 if metadata is insufficient. """ - num_min, seconds = divmod(seconds, 60) - num_hours, minutes = divmod(num_min, 60) - days, hours = divmod(num_hours, 24) - return [days, hours, minutes, seconds] - + if layer_height <= 0 or object_height <= 0: + return 0 + return max(1, math.ceil((object_height - first_layer_height) / layer_height + 1)) -def normalize(value, r_min=0.0, r_max=1.0, t_min=0.0, t_max=100): - """Normalize values between a rage""" - # https://stats.stackexchange.com/questions/281162/scale-a-number-between-a-range - c1 = (value - r_min) / (r_max - r_min) - c2 = (t_max - t_min) + t_min - return c1 * c2 +def estimate_print_time(seconds: int) -> list[int]: + """Convert *seconds* to ``[days, hours, minutes, seconds]``.""" + num_min, secs = divmod(seconds, 60) + num_hours, mins = divmod(num_min, 60) + days, hours = divmod(num_hours, 24) + return [days, hours, mins, secs] -def check_filepath_permission(filepath, access_type: int = os.R_OK) -> bool: - # if not isinstance(filepath, pathlib.Path): - """Checks for file path access - Args: - filepath (str | pathlib.Path): path to file - access_type (int, optional): _description_. Defaults to os.R_OK. +def normalize( + value: float, + r_min: float = 0.0, + r_max: float = 1.0, + t_min: float = 0.0, + t_max: float = 100.0, +) -> float: + """Scale *value* from range [r_min, r_max] into [t_min, t_max].""" + return (value - r_min) / (r_max - r_min) * (t_max - t_min) + t_min - *** - #### **Access type can be:** +def check_filepath_permission( + filepath: str | pathlib.Path, access_type: int = os.R_OK +) -> bool: + """Check whether *filepath* exists and has the requested access. - - F_OK -> Checks file existence on path - - R_OK -> Checks if file is readable - - W_OK -> Checks if file is Writable - - X_OK -> Checks if file can be executed + Args: + filepath: Path to file. + access_type: ``os.F_OK`` (existence), ``os.R_OK`` (read), + ``os.W_OK`` (write), or ``os.X_OK`` (execute). - *** Returns: - bool: _description_ - """ # return False - if not os.path.isfile(filepath): - return False - return os.access(filepath, access_type) + ``True`` if the file exists and satisfies *access_type*. + """ + path = pathlib.Path(filepath) + return path.is_file() and os.access(path, access_type) -def check_dir_existence( - directory: typing.Union[str, pathlib.Path], -) -> bool: - """Check if a directory exists. Returns a true if it exists""" - if isinstance(directory, pathlib.Path): - return bool(directory.is_dir()) - return bool(os.path.isdir(directory)) +def check_dir_existence(directory: str | pathlib.Path) -> bool: + """Return ``True`` if *directory* exists and is a directory.""" + return pathlib.Path(directory).is_dir() def check_file_on_path( - path: typing.Union[typing.LiteralString, pathlib.Path], - filename: typing.Union[typing.LiteralString, pathlib.Path], + path: str | pathlib.Path, + filename: str | pathlib.Path, ) -> bool: - """Check if file exists on path. Returns true if file exists on that specified directory""" - _filepath = os.path.join(path, filename) - return os.path.exists(_filepath) - + """Return ``True`` if *filename* exists under *path*.""" + return (pathlib.Path(path) / filename).exists() -def get_file_loc(filename) -> pathlib.Path: ... +def get_file_name(filename: str | None) -> str: + """Extract the basename from a file path (handles ``/`` and ``\\``). -def get_file_name(filename: typing.Optional[str]) -> str: - # If filename is None or empty, return empty string instead of None + Returns: + The last path component, or ``""`` if *filename* is falsy. + """ if not filename: return "" - # Remove trailing slashes or backslashes - filename = filename.rstrip("/\\") - - # Normalize Windows backslashes to forward slashes - filename = filename.replace("\\", "/") - - parts = filename.split("/") - - # Split and return the last path component - return parts[-1] if filename else "" - - -# def get_hash(data) -> hashlib._Hash: -# hash = hashlib.sha256() -# hash.update(data.encode()) -# hash.digest() -# return hash - - -def digest_hash() -> None: ... + return pathlib.PurePosixPath(filename.replace("\\", "/")).name 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..fdecefd3 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,28 +352,37 @@ 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 + if not _connected or self.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)) return True @@ -329,7 +390,7 @@ def send_request(self, method: str, params: dict = {}) -> bool: 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 +507,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 +666,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 +680,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 +793,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 +824,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 +850,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 be84c8bc..1568c449 100644 --- a/BlocksScreen/lib/panels/controlTab.py +++ b/BlocksScreen/lib/panels/controlTab.py @@ -1,10 +1,10 @@ from __future__ import annotations +import logging import re import typing from functools import partial -from helper_methods import normalize from lib.moonrakerComm import MoonWebSocket from lib.panels.widgets.numpadPage import CustomNumpad from lib.panels.widgets.optionCardWidget import OptionCard @@ -17,6 +17,8 @@ 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""" @@ -386,6 +388,11 @@ def on_slidePage_request( 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)) @@ -395,18 +402,16 @@ def on_slidePage_request( @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 + """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 @@ -427,19 +432,20 @@ def create_display_button(self, name: str) -> DisplayButton: return display_button def handle_printcoreupdate(self, value: dict): - if value["swapping"] == "idle": + _swapping = value.get("swapping") + if _swapping is None or _swapping == "idle": return - if value["swapping"] == "in_pos": + 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 value["swapping"] == "unloading": + if _swapping == "unloading": self.call_load_panel.emit(True, "Unloading print core") - if value["swapping"] == "cleaning": + if _swapping == "cleaning": self.call_load_panel.emit(True, "Cleaning print core") def _handle_gcode_response(self, messages: list): @@ -508,6 +514,10 @@ def on_numpad_request( 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) 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..cbfb0348 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,7 @@ def _handle_notify_service_state_changed_message( if self._popup_toggle: return service_entry: dict = entry[0] - service_name, service_info = service_entry.popitem() + service_name, service_info = next(iter(service_entry.items())) self.show_notifications.emit( "mainwindow", str( @@ -703,12 +727,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 +780,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 +857,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 +887,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 +914,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 65927aae..c4ea5f53 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( @@ -267,7 +268,6 @@ def __init__( self.confirmPage_widget.on_delete.connect(self.delete_file) self.change_page(self.indexOf(self.print_page)) # force set the initial page self.save_config_btn.clicked.connect(self.save_config) - self.BasePopup_z_offset.accepted.connect(self.update_configuration_file) @QtCore.pyqtSlot(str, dict, name="on_print_stats_update") @QtCore.pyqtSlot(str, float, name="on_print_stats_update") @@ -292,6 +292,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 +315,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)) @@ -321,8 +329,12 @@ def on_slidePage_request( @QtCore.pyqtSlot(str, str, name="delete_file") @QtCore.pyqtSlot(str, name="delete_file") def delete_file(self, filename: str, directory: str = "gcodes") -> None: - """Handle Delete file signal, shows confirmation dialog""" + """Handle Delete file signal, shows confirmation dialog.""" self.BasePopup.set_message("Are you sure you want to delete this file?") + try: + self.BasePopup.accepted.disconnect() + except (RuntimeError, TypeError): + pass self.BasePopup.accepted.connect( lambda: self._on_delete_file_confirmed(filename, directory) ) @@ -342,13 +354,21 @@ def save_config(self) -> None: "The machine will restart." ) self.BasePopup_z_offset.cancel_button_text("Later") + try: + self.BasePopup_z_offset.accepted.disconnect(self.update_configuration_file) + except (RuntimeError, TypeError): + pass + self.BasePopup_z_offset.accepted.connect(self.update_configuration_file) self.BasePopup_z_offset.open() - def update_configuration_file(self): - """Runs the `SAVE_CONFIG` gcode""" + def update_configuration_file(self) -> None: + """Run ``Z_OFFSET_APPLY_PROBE`` followed by ``SAVE_CONFIG``.""" + try: + self.BasePopup_z_offset.accepted.disconnect(self.update_configuration_file) + except (RuntimeError, TypeError): + pass self.run_gcode_signal.emit("Z_OFFSET_APPLY_PROBE") self.run_gcode_signal.emit("SAVE_CONFIG") - self.BasePopup_z_offset.disconnect() @QtCore.pyqtSlot(str, list, name="activate_save_button") def activate_save_button(self, name: str, value: list) -> None: @@ -357,15 +377,19 @@ def activate_save_button(self, name: str, value: list) -> None: return if name == "homing_origin": - self._active_z_offset = value[2] - self.save_config_btn.setVisible(value[2] != 0) + if len(value) > 2: + self._active_z_offset = value[2] + self.save_config_btn.setVisible(value[2] != 0) def _on_delete_file_confirmed(self, filename: str, directory: str) -> None: - """Handle confirmed file deletion after user accepted the dialog""" + """Handle confirmed file deletion after user accepted the dialog.""" self.file_data.on_request_delete_file(filename, directory) self.request_back.emit() self.filesPage_widget.reset_dir() - self.BasePopup.disconnect() + try: + self.BasePopup.accepted.disconnect() + except (RuntimeError, TypeError): + pass def setProperty(self, name: str, value: typing.Any) -> bool: """Intercept the set property method 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/babystepPage.py b/BlocksScreen/lib/panels/widgets/babystepPage.py index 273e8f9c..24e04b20 100644 --- a/BlocksScreen/lib/panels/widgets/babystepPage.py +++ b/BlocksScreen/lib/panels/widgets/babystepPage.py @@ -1,3 +1,4 @@ +import logging import typing from lib.utils.blocks_label import BlocksLabel @@ -5,8 +6,20 @@ from lib.utils.icon_button import IconButton from PyQt6 import QtCore, QtGui, QtWidgets +logger = logging.getLogger(__name__) + +# Button definitions: (label, value, object_name, initially_checked) +_OFFSET_STEPS: list[tuple[str, float, str, bool]] = [ + ("0.1 mm", 0.1, "bbp_nozzle_offset_1", True), + ("0.05 mm", 0.05, "bbp_nozzle_offset_05", False), + ("0.025 mm", 0.025, "bbp_nozzle_offset_025", False), + ("0.01 mm", 0.01, "bbp_nozzle_offset_01", False), +] + class BabystepPage(QtWidgets.QWidget): + """Page for adjusting Z offset in small increments during a print.""" + request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="request_back" ) @@ -24,19 +37,18 @@ def __init__(self, parent) -> None: self.setTabletTracking(True) self.setMouseTracking(True) - self.setupUI() + self._baby_stepchange = False + self._z_offset_text: float = 0.0 + self._pending_z_offset: float = 0.0 + + self._setupUI() self.bbp_mvup.clicked.connect(self.on_move_nozzle_close) self.bbp_mvdown.clicked.connect(self.on_move_nozzle_away) self.babystep_back_btn.clicked.connect(self.request_back.emit) - self.bbp_nozzle_offset_01.toggled.connect(self.handle_z_offset_change) - self.bbp_nozzle_offset_025.toggled.connect(self.handle_z_offset_change) - self.bbp_nozzle_offset_05.toggled.connect(self.handle_z_offset_change) - self.bbp_nozzle_offset_1.toggled.connect(self.handle_z_offset_change) - self._baby_stepchange = False @property def baby_stepchange(self): - """Returns if the babystep was changed during print""" + """Returns if the babystep was changed during print.""" return self._baby_stepchange @baby_stepchange.setter @@ -47,84 +59,107 @@ def baby_stepchange(self, value: bool) -> None: @QtCore.pyqtSlot(name="on_move_nozzle_close") def on_move_nozzle_close(self) -> None: - """Move the nozzle closer to the print plate - by the amount set in **` self._z_offset`** - """ - self.run_gcode.emit( - f"SET_GCODE_OFFSET Z_ADJUST=-{self._z_offset} MOVE=1" # Z_ADJUST adds the value to the existing offset - ) + """Move the nozzle closer to the print plate.""" + self.run_gcode.emit(f"SET_GCODE_OFFSET Z_ADJUST=-{self._z_offset} MOVE=1") + self._pending_z_offset -= self._z_offset + self.bbp_z_offset_current_value.setText(f"Z: {self._pending_z_offset:.3f}mm") self._baby_stepchange = True @QtCore.pyqtSlot(name="on_move_nozzle_away") def on_move_nozzle_away(self) -> None: - """Slot for Babystep button to get far from the - bed by **` self._z_offset`** amount - """ - self.run_gcode.emit( - f"SET_GCODE_OFFSET Z_ADJUST=+{self._z_offset} MOVE=1" # Z_ADJUST adds the value to the existing offset - ) + """Move the nozzle away from the print plate.""" + self.run_gcode.emit(f"SET_GCODE_OFFSET Z_ADJUST=+{self._z_offset} MOVE=1") + self._pending_z_offset += self._z_offset + self.bbp_z_offset_current_value.setText(f"Z: {self._pending_z_offset:.3f}mm") self._baby_stepchange = True @QtCore.pyqtSlot(name="handle_z_offset_change") def handle_z_offset_change(self) -> None: - """Helper method for changing the value for Babystep. - - When a button is clicked, and the button has the mm value i the text, - it'll change the internal value **z_offset** to the same has the button - - *** - - Possible values are: 0.01, 0.025, 0.05, 0.1 **mm** - """ + """Update step size from the clicked offset button text.""" _sender: QtCore.QObject | None = self.sender() - if self._z_offset == float(_sender.text()[:-3]): + if _sender is None: + return + if not isinstance(_sender, QtWidgets.QAbstractButton): + return + try: + _value = float(_sender.text()[:-3]) + except ValueError: + logger.warning( + "handle_z_offset_change: could not parse button text %r", + _sender.text(), + ) + return + if self._z_offset == _value: return - self._z_offset = float(_sender.text()[:-3]) + self._z_offset = _value def on_gcode_move_update(self, name: str, value: list) -> None: - """Handle gcode move updates""" + """Handle gcode move updates from Klipper.""" if not value: return - if name == "homing_origin": - self._z_offset_text = value[2] - self.bbp_z_offset_current_value.setText(f"Z: {self._z_offset_text:.3f}mm") - - def setupUI(self): - """Setup babystep page ui""" - self.bbp_offset_value_selector_group = QtWidgets.QButtonGroup(self) - self.bbp_offset_value_selector_group.setExclusive(True) - sizePolicy = QtWidgets.QSizePolicy( + if name == "homing_origin" and len(value) > 2: + confirmed = value[2] + self._z_offset_text = confirmed + self.bbp_z_offset_title_label.setText(f"Z: {confirmed:.3f}mm") + # Always sync pending offset to Klipper's confirmed value + self._pending_z_offset = confirmed + self.bbp_z_offset_current_value.setText(f"Z: {confirmed:.3f}mm") + + def _create_offset_button( + self, + parent: QtWidgets.QWidget, + label: str, + obj_name: str, + checked: bool, + font: QtGui.QFont, + ) -> BlocksCustomCheckButton: + """Create a single offset-step check button.""" + btn = BlocksCustomCheckButton(parent=parent) + btn.setMinimumSize(QtCore.QSize(100, 70)) + btn.setMaximumSize(QtCore.QSize(100, 70)) + btn.setText(label) + btn.setFont(font) + btn.setCheckable(True) + btn.setChecked(checked) + btn.setFlat(True) + btn.setProperty("button_type", "") + btn.setObjectName(obj_name) + btn.toggled.connect(self.handle_z_offset_change) + return btn + + def _setupUI(self) -> None: + """Setup babystep page UI.""" + btn_group = QtWidgets.QButtonGroup(self) + btn_group.setExclusive(True) + + size_policy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding, ) - sizePolicy.setHorizontalStretch(1) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) - self.setSizePolicy(sizePolicy) + size_policy.setHorizontalStretch(1) + size_policy.setVerticalStretch(1) + size_policy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) + self.setSizePolicy(size_policy) self.setMinimumSize(QtCore.QSize(710, 400)) - self.setMaximumSize( - QtCore.QSize(720, 420) - ) # This sets the maximum width of the entire page + self.setMaximumSize(QtCore.QSize(720, 420)) self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - # Main Vertical Layout for the entire page - self.verticalLayout = QtWidgets.QVBoxLayout(self) - self.verticalLayout.setObjectName("verticalLayout") + main_vlayout = QtWidgets.QVBoxLayout(self) + + header = QtWidgets.QHBoxLayout() - # Header Layout - self.bbp_header_layout = QtWidgets.QHBoxLayout() - self.bbp_header_layout.setObjectName("bbp_header_layout") - self.bbp_header_title = QtWidgets.QLabel(parent=self) - sizePolicy.setHeightForWidth( - self.bbp_header_title.sizePolicy().hasHeightForWidth() + header.addItem( + QtWidgets.QSpacerItem( + 60, + 20, + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Minimum, + ) ) - self.bbp_header_title.setSizePolicy(sizePolicy) - self.bbp_header_title.setMinimumSize(QtCore.QSize(200, 60)) - self.bbp_header_title.setMaximumSize(QtCore.QSize(16777215, 60)) - font = QtGui.QFont() - font.setPointSize(22) - self.bbp_header_title.setFont(font) + + title_font = QtGui.QFont() + title_font.setPointSize(22) palette = QtGui.QPalette() palette.setColor( palette.ColorGroup.All, @@ -136,187 +171,68 @@ def setupUI(self): palette.ColorRole.WindowText, QtGui.QColor("#FFFFFF"), ) + + self.bbp_header_title = QtWidgets.QLabel("Babystep", parent=self) + self.bbp_header_title.setSizePolicy(size_policy) + self.bbp_header_title.setMinimumSize(QtCore.QSize(200, 60)) + self.bbp_header_title.setMaximumSize(QtCore.QSize(16777215, 60)) + self.bbp_header_title.setFont(title_font) self.bbp_header_title.setAutoFillBackground(True) self.bbp_header_title.setBackgroundRole(palette.ColorRole.Window) self.bbp_header_title.setPalette(palette) - self.bbp_header_title.setText("Babystep") self.bbp_header_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.bbp_header_title.setObjectName("bbp_header_title") + header.addWidget(self.bbp_header_title, 0, QtCore.Qt.AlignmentFlag.AlignCenter) - spacerItem = QtWidgets.QSpacerItem( - 60, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - self.bbp_header_layout.addItem(spacerItem) - - self.bbp_header_layout.addWidget( - self.bbp_header_title, - 0, - QtCore.Qt.AlignmentFlag.AlignCenter, - ) self.babystep_back_btn = IconButton(parent=self) - sizePolicy.setHeightForWidth( - self.babystep_back_btn.sizePolicy().hasHeightForWidth() - ) - self.babystep_back_btn.setSizePolicy(sizePolicy) + self.babystep_back_btn.setSizePolicy(size_policy) self.babystep_back_btn.setMinimumSize(QtCore.QSize(60, 60)) self.babystep_back_btn.setMaximumSize(QtCore.QSize(60, 60)) - self.babystep_back_btn.setText("") self.babystep_back_btn.setFlat(True) self.babystep_back_btn.setPixmap(QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) - self.babystep_back_btn.setObjectName("babystep_back_btn") - - self.bbp_header_layout.addWidget( + header.addWidget( self.babystep_back_btn, 0, QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter, ) - self.bbp_header_layout.setStretch(0, 1) - self.verticalLayout.addLayout(self.bbp_header_layout) - - self.main_content_horizontal_layout = QtWidgets.QHBoxLayout() - self.main_content_horizontal_layout.setObjectName( - "main_content_horizontal_layout" - ) - - # Offset Steps Buttons Group Box (LEFT side of main_content_horizontal_layout) - self.bbp_offset_steps_buttons_group_box = QtWidgets.QGroupBox(self) - font = QtGui.QFont() - font.setPointSize(14) - self.bbp_offset_steps_buttons_group_box.setFont(font) - self.bbp_offset_steps_buttons_group_box.setFlat(True) - # Add stylesheet to explicitly remove any border from the QGroupBox - self.bbp_offset_steps_buttons_group_box.setStyleSheet( - "QGroupBox { border: none; }" - ) - self.bbp_offset_steps_buttons_group_box.setObjectName( - "bbp_offset_steps_buttons_group_box" - ) - - self.bbp_offset_steps_buttons = QtWidgets.QVBoxLayout( - self.bbp_offset_steps_buttons_group_box - ) - self.bbp_offset_steps_buttons.setContentsMargins(9, 9, 9, 9) - self.bbp_offset_steps_buttons.setObjectName("bbp_offset_steps_buttons") - - # 0.1mm button - self.bbp_nozzle_offset_1 = BlocksCustomCheckButton( - parent=self.bbp_offset_steps_buttons_group_box - ) - self.bbp_nozzle_offset_1.setMinimumSize(QtCore.QSize(100, 70)) - self.bbp_nozzle_offset_1.setMaximumSize(QtCore.QSize(100, 70)) - self.bbp_nozzle_offset_1.setText("0.1 mm") - - font = QtGui.QFont() - font.setPointSize(14) - self.bbp_nozzle_offset_1.setFont(font) - self.bbp_nozzle_offset_1.setCheckable(True) - self.bbp_nozzle_offset_1.setChecked(True) # Set as initially checked - self.bbp_nozzle_offset_1.setFlat(True) - self.bbp_nozzle_offset_1.setProperty("button_type", "") - self.bbp_nozzle_offset_1.setObjectName("bbp_nozzle_offset_1") - self.bbp_offset_value_selector_group.addButton(self.bbp_nozzle_offset_1) - self.bbp_offset_steps_buttons.addWidget( - self.bbp_nozzle_offset_1, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - # 0.05mm button - self.bbp_nozzle_offset_05 = BlocksCustomCheckButton( - parent=self.bbp_offset_steps_buttons_group_box - ) - self.bbp_nozzle_offset_05.setMinimumSize(QtCore.QSize(100, 70)) - self.bbp_nozzle_offset_05.setMaximumSize( - QtCore.QSize(100, 70) - ) # Increased max width by 5 pixels - self.bbp_nozzle_offset_05.setText("0.05 mm") - - font = QtGui.QFont() - font.setPointSize(14) - self.bbp_nozzle_offset_05.setFont(font) - self.bbp_nozzle_offset_05.setCheckable(True) - self.bbp_nozzle_offset_05.setFlat(True) - self.bbp_nozzle_offset_05.setProperty("button_type", "") - self.bbp_nozzle_offset_05.setObjectName("bbp_nozzle_offset_05") - self.bbp_offset_value_selector_group.addButton(self.bbp_nozzle_offset_05) - self.bbp_offset_steps_buttons.addWidget( - self.bbp_nozzle_offset_05, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - # Line separator for 0.1mm - set size policy to expanding horizontally - - # 0.01mm button - self.bbp_nozzle_offset_01 = BlocksCustomCheckButton( - parent=self.bbp_offset_steps_buttons_group_box - ) - self.bbp_nozzle_offset_01.setMinimumSize(QtCore.QSize(100, 70)) - self.bbp_nozzle_offset_01.setMaximumSize( - QtCore.QSize(100, 70) - ) # Increased max width by 5 pixels - self.bbp_nozzle_offset_01.setText("0.01 mm") - - font = QtGui.QFont() - font.setPointSize(14) - self.bbp_nozzle_offset_01.setFont(font) - self.bbp_nozzle_offset_01.setCheckable(True) - self.bbp_nozzle_offset_01.setFlat(True) - self.bbp_nozzle_offset_01.setProperty("button_type", "") - self.bbp_nozzle_offset_01.setObjectName("bbp_nozzle_offset_01") - self.bbp_offset_value_selector_group.addButton(self.bbp_nozzle_offset_01) - self.bbp_offset_steps_buttons.addWidget( - self.bbp_nozzle_offset_01, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - # 0.025mm button - self.bbp_nozzle_offset_025 = BlocksCustomCheckButton( - parent=self.bbp_offset_steps_buttons_group_box - ) - self.bbp_nozzle_offset_025.setMinimumSize(QtCore.QSize(100, 70)) - self.bbp_nozzle_offset_025.setMaximumSize( - QtCore.QSize(100, 70) - ) # Increased max width by 5 pixels - self.bbp_nozzle_offset_025.setText("0.025 mm") - - font = QtGui.QFont() - font.setPointSize(14) - self.bbp_nozzle_offset_025.setFont(font) - self.bbp_nozzle_offset_025.setCheckable(True) - self.bbp_nozzle_offset_025.setFlat(True) - self.bbp_nozzle_offset_025.setProperty("button_type", "") - self.bbp_nozzle_offset_025.setObjectName("bbp_nozzle_offset_025") - self.bbp_offset_value_selector_group.addButton(self.bbp_nozzle_offset_025) - self.bbp_offset_steps_buttons.addWidget( - self.bbp_nozzle_offset_025, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - # Line separator for 0.025mm - set size policy to expanding horizontally - - # Set the layout for the group box - self.bbp_offset_steps_buttons_group_box.setLayout(self.bbp_offset_steps_buttons) - # Add the group box to the main content horizontal layout FIRST for left placement - self.main_content_horizontal_layout.addWidget( - self.bbp_offset_steps_buttons_group_box - ) - - # Graphic and Current Value Frame (This will now be in the MIDDLE) - self.frame_2 = QtWidgets.QFrame(parent=self) - sizePolicy.setHeightForWidth(self.frame_2.sizePolicy().hasHeightForWidth()) - self.frame_2.setSizePolicy(sizePolicy) - self.frame_2.setMinimumSize(QtCore.QSize(350, 160)) - self.frame_2.setMaximumSize(QtCore.QSize(350, 160)) - self.frame_2.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.frame_2.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_2.setObjectName("frame_2") - self.bbp_babystep_graphic = QtWidgets.QLabel(parent=self.frame_2) + header.setStretch(0, 1) + main_vlayout.addLayout(header) + + # --- Main content (3 columns) --- + content = QtWidgets.QHBoxLayout() + + # Column 1: offset step buttons (highest → lowest) + group_box = QtWidgets.QGroupBox(self) + btn_font = QtGui.QFont() + btn_font.setPointSize(14) + group_box.setFont(btn_font) + group_box.setFlat(True) + group_box.setStyleSheet("QGroupBox { border: none; }") + + steps_layout = QtWidgets.QVBoxLayout(group_box) + steps_layout.setContentsMargins(9, 9, 9, 9) + + center = ( + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter + ) + for label, _value, obj_name, checked in _OFFSET_STEPS: + btn = self._create_offset_button( + group_box, label, obj_name, checked, btn_font + ) + setattr(self, obj_name, btn) + btn_group.addButton(btn) + steps_layout.addWidget(btn, 0, center) + + content.addWidget(group_box) + + # Column 2: graphic + Z offset labels + frame = QtWidgets.QFrame(parent=self) + frame.setSizePolicy(size_policy) + frame.setMinimumSize(QtCore.QSize(350, 160)) + frame.setMaximumSize(QtCore.QSize(350, 160)) + frame.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + + self.bbp_babystep_graphic = QtWidgets.QLabel(parent=frame) self.bbp_babystep_graphic.setGeometry(QtCore.QRect(0, 30, 371, 121)) self.bbp_babystep_graphic.setLayoutDirection( QtCore.Qt.LayoutDirection.RightToLeft @@ -326,38 +242,25 @@ def setupUI(self): ) self.bbp_babystep_graphic.setScaledContents(False) self.bbp_babystep_graphic.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.bbp_babystep_graphic.setObjectName("bbp_babystep_graphic") - # === NEW LABEL ADDED HERE === - # This is the title label that appears above the red value box. + grey_font = QtGui.QFont() + grey_font.setPointSize(12) self.bbp_z_offset_title_label = QtWidgets.QLabel(parent=self) - # Position it just above the red box. Red box is at y=70, so y=40 is appropriate. - self.bbp_z_offset_title_label.setGeometry(QtCore.QRect(100, 40, 200, 30)) - font = QtGui.QFont() - font.setPointSize(12) - - self.bbp_z_offset_title_label.setFont(font) - # Set color to white to be visible on the dark background + self.bbp_z_offset_title_label.setFont(grey_font) self.bbp_z_offset_title_label.setStyleSheet( "color: gray; background: transparent;" ) - self.bbp_z_offset_title_label.setObjectName("bbp_z_offset_title_label") - self.bbp_z_offset_title_label.setText("Z: 0.000mm") + self.bbp_z_offset_title_label.setText(f"Z: {self._z_offset_text:.3f}mm") self.bbp_z_offset_title_label.setGeometry(420, 270, 200, 30) - # === END OF NEW LABEL === - - self.bbp_z_offset_current_value = BlocksLabel(parent=self.frame_2) + white_font = QtGui.QFont() + white_font.setPointSize(14) + self.bbp_z_offset_current_value = BlocksLabel(parent=frame) self.bbp_z_offset_current_value.setGeometry(QtCore.QRect(100, 70, 200, 60)) - sizePolicy.setHeightForWidth( - self.bbp_z_offset_current_value.sizePolicy().hasHeightForWidth() - ) - self.bbp_z_offset_current_value.setSizePolicy(sizePolicy) + self.bbp_z_offset_current_value.setSizePolicy(size_policy) self.bbp_z_offset_current_value.setMinimumSize(QtCore.QSize(150, 60)) self.bbp_z_offset_current_value.setMaximumSize(QtCore.QSize(200, 60)) - font = QtGui.QFont() - font.setPointSize(14) - self.bbp_z_offset_current_value.setFont(font) + self.bbp_z_offset_current_value.setFont(white_font) self.bbp_z_offset_current_value.setStyleSheet( "background: transparent; color: white;" ) @@ -368,87 +271,59 @@ def setupUI(self): self.bbp_z_offset_current_value.setAlignment( QtCore.Qt.AlignmentFlag.AlignCenter ) - self.bbp_z_offset_current_value.setObjectName("bbp_z_offset_current_value") - # Add graphic frame AFTER the offset buttons group box - self.main_content_horizontal_layout.addWidget( - self.frame_2, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, + + content.addWidget(frame, 0, center) + + # Spacer before move buttons + content.addItem( + QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Minimum, + ) ) - # Move Buttons Layout (This will now be on the RIGHT) - self.bbp_buttons_layout = QtWidgets.QVBoxLayout() - self.bbp_buttons_layout.setContentsMargins(5, 5, 5, 5) - self.bbp_buttons_layout.setObjectName("bbp_buttons_layout") + # Column 3: move up/down buttons + move_layout = QtWidgets.QVBoxLayout() + move_layout.setContentsMargins(5, 5, 5, 5) + self.bbp_mvup = IconButton(parent=self) - sizePolicy.setHeightForWidth(self.bbp_mvup.sizePolicy().hasHeightForWidth()) - self.bbp_mvup.setSizePolicy(sizePolicy) + self.bbp_mvup.setSizePolicy(size_policy) self.bbp_mvup.setMinimumSize(QtCore.QSize(80, 80)) self.bbp_mvup.setMaximumSize(QtCore.QSize(80, 80)) - self.bbp_mvup.setText("") self.bbp_mvup.setFlat(True) self.bbp_mvup.setPixmap( QtGui.QPixmap(":/baby_step/media/btn_icons/move_nozzle_close.svg") ) - self.bbp_mvup.setObjectName("bbp_away_from_bed") - self.bbp_option_button_group = QtWidgets.QButtonGroup(self) - self.bbp_option_button_group.setObjectName("bbp_option_button_group") - self.bbp_option_button_group.addButton(self.bbp_mvup) - self.bbp_buttons_layout.addWidget( - self.bbp_mvup, 0, QtCore.Qt.AlignmentFlag.AlignRight - ) + move_layout.addWidget(self.bbp_mvup, 0, QtCore.Qt.AlignmentFlag.AlignRight) + self.bbp_mvdown = IconButton(parent=self) - sizePolicy.setHeightForWidth(self.bbp_mvdown.sizePolicy().hasHeightForWidth()) - self.bbp_mvdown.setSizePolicy(sizePolicy) + self.bbp_mvdown.setSizePolicy(size_policy) self.bbp_mvdown.setMinimumSize(QtCore.QSize(80, 80)) self.bbp_mvdown.setMaximumSize(QtCore.QSize(80, 80)) - self.bbp_mvdown.setText("") self.bbp_mvdown.setFlat(True) self.bbp_mvdown.setPixmap( QtGui.QPixmap(":/baby_step/media/btn_icons/move_nozzle_away.svg") ) - self.bbp_mvdown.setObjectName("bbp_close_to_bed") - self.bbp_option_button_group.addButton(self.bbp_mvdown) - self.bbp_buttons_layout.addWidget( - self.bbp_mvdown, 0, QtCore.Qt.AlignmentFlag.AlignRight - ) - spacerItem = QtWidgets.QSpacerItem( - 40, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - self.main_content_horizontal_layout.addItem(spacerItem) + move_layout.addWidget(self.bbp_mvdown, 0, QtCore.Qt.AlignmentFlag.AlignRight) - # Add move buttons layout LAST for right placement - self.main_content_horizontal_layout.addLayout(self.bbp_buttons_layout) + content.addLayout(move_layout) - spacerItem = QtWidgets.QSpacerItem( - 40, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, + # Trailing spacer + content.addItem( + QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Minimum, + ) ) - self.main_content_horizontal_layout.addItem(spacerItem) - - # Set stretch factors for main content horizontal layout - # This will distribute space: offset buttons, graphic frame, move buttons - self.main_content_horizontal_layout.setStretch( - 0, 1 - ) # offset_steps_buttons_group_box - self.main_content_horizontal_layout.setStretch( - 1, 2 - ) # frame_2 (graphic and current value) - self.main_content_horizontal_layout.setStretch( - 2, 0 - ) # bbp_buttons_layout (move buttons) - - # Add the main content horizontal layout to the vertical layout - self.verticalLayout.addLayout(self.main_content_horizontal_layout) - - # Set stretch factors for vertical layout (adjust as needed for overall sizing) - self.verticalLayout.setStretch( - 1, 1 - ) # This stretch applies to main_content_horizontal_layout - - self.setLayout(self.verticalLayout) + + content.setStretch(0, 1) # offset buttons + content.setStretch(1, 2) # graphic frame + content.setStretch(2, 0) # move buttons + + main_vlayout.addLayout(content) + main_vlayout.setStretch(1, 1) + self.setLayout(main_vlayout) 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/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index 67add6b9..198b670e 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -2,7 +2,11 @@ import typing import events -from helper_methods import calculate_current_layer, estimate_print_time +from helper_methods import ( + calculate_current_layer, + calculate_max_layers, + estimate_print_time, +) from lib.panels.widgets.basePopup import BasePopup from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_label import BlocksLabel @@ -57,8 +61,9 @@ class JobStatusWidget(QtWidgets.QWidget): _internal_print_status: str = "" _current_file_name: str = "" - file_metadata: dict = {} + file_metadata: dict | None = None total_layers = "?" + _print_duration: float = 0.0 def __init__(self, parent) -> None: super().__init__(parent) @@ -110,20 +115,19 @@ def eventFilter(self, sender_obj: QtCore.QObject, event: events.QEvent) -> bool: return super().eventFilter(sender_obj, event) def _load_thumbnails(self, *thumbnails) -> None: - """Pre-load available thumbnails for the current print object""" - self.thumbnail_graphics = list( - filter( - lambda thumb: not thumb.isNull(), - [QtGui.QPixmap(thumb) for thumb in thumbnails], - ) - ) + """Pre-load available thumbnails for the current print object.""" + loaded = [] + for thumb in thumbnails: + px = QtGui.QPixmap(thumb) + if not px.isNull(): + loaded.append(px) + self.thumbnail_graphics = loaded if not self.thumbnail_graphics: logger.debug("Unable to load thumbnails, no thumbnails provided") return - self.create_thumbnail_widget() - self.thumbnail_view.installEventFilter(self) - scene = QtWidgets.QGraphicsScene() + self._ensure_thumbnail_widget() _biggest_thumb = self.thumbnail_graphics[-1] + scene = QtWidgets.QGraphicsScene() self.thumbnail_view.setSceneRect( QtCore.QRectF( self.rect().x(), @@ -147,9 +151,6 @@ def _load_thumbnails(self, *thumbnails) -> None: ) self.thumbnail_view.setScene(scene) self.printing_progress_bar.set_inner_pixmap(self.thumbnail_graphics[-1]) - self.printing_progress_bar.thumbnail_clicked.connect( - self.toggle_thumbnail_expansion - ) @QtCore.pyqtSlot(name="handle-cancel") def handleCancel(self) -> None: @@ -157,6 +158,10 @@ def handleCancel(self) -> None: self.cancel_print_dialog.set_message( "Are you sure you \n want to cancel \n the current print job?" ) + try: + self.cancel_print_dialog.accepted.disconnect(self.print_cancel) + except TypeError: + pass self.cancel_print_dialog.accepted.connect(self.print_cancel) self.cancel_print_dialog.open() @@ -165,9 +170,10 @@ def on_print_start(self, file: str) -> None: """Start a print job, show job status page""" self._current_file_name = file self.js_file_name_label.setText(self._current_file_name) - self.layer_display_button.setText("?") + self.layer_display_button.setText("0") self.print_time_display_button.setText("?") self.printing_progress_bar.reset() + self._print_duration = 0.0 self._internal_print_status = "printing" self.request_file_info.emit(file) self.print_start.emit(file) @@ -184,15 +190,18 @@ def on_print_start(self, file: str) -> None: logger.debug("Unexpected error while posting print job start event: %s", e) @QtCore.pyqtSlot(dict, name="on_fileinfo") - def on_fileinfo(self, fileinfo: dict) -> None: - """Handle received file information/metadata""" - if not self.isVisible(): - return - self.total_layers = str(fileinfo.get("layer_count", "---")) - self.layer_display_button.setText("---") + def on_fileinfo(self, metadata: dict) -> None: + """Handle received file information/metadata. + + Loads thumbnail and layer count regardless of visibility so they + are ready when the widget is shown. + """ + layer_count = metadata.get("layer_count", -1) + self.total_layers = str(layer_count) if layer_count >= 0 else "---" + self.layer_display_button.setText("0") self.layer_display_button.secondary_text = str(self.total_layers) - self.file_metadata = fileinfo - self._load_thumbnails(*fileinfo.get("thumbnail_images", [])) + self.file_metadata = metadata + self._load_thumbnails(*metadata.get("thumbnail_images", ())) @QtCore.pyqtSlot(name="pause_resume_print") def pause_resume_print(self) -> None: @@ -212,6 +221,7 @@ def _handle_print_state(self, state: str) -> None: valid_states = {"printing", "paused"} invalid_states = {"cancelled", "complete", "error", "standby"} lstate = state.lower() + event_state = lstate if lstate in valid_states: self._internal_print_status = lstate if lstate == "paused": @@ -219,50 +229,54 @@ def _handle_print_state(self, state: str) -> None: self.pause_printing_btn.setPixmap( QtGui.QPixmap(":/ui/media/btn_icons/play.svg") ) + event_state = "pause" elif lstate == "printing": self.pause_printing_btn.setText("Pause") self.pause_printing_btn.setPixmap( QtGui.QPixmap(":/ui/media/btn_icons/pause.svg") ) + event_state = "start" self.pause_printing_btn.setEnabled(True) self.request_query_print_stats.emit({"print_stats": ["filename"]}) self.call_cancel_panel.emit(False) self.show_request.emit() - lstate = "start" elif lstate in invalid_states: - if lstate != "standby": + if lstate == "complete": self.print_finish.emit() + self.hide_request.emit() + # Capture state before clearing so the event carries the real data. + _event_file = self._current_file_name + _event_meta = self.file_metadata + if lstate in invalid_states: self._internal_print_status = "" self._current_file_name = "" self.total_layers = "?" - self.file_metadata.clear() - self.hide_request.emit() - # if hasattr(self, "thumbnail_view"): - # getattr(self, "thumbnail_view").deleteLater() + self._print_duration = 0.0 + self.file_metadata = None # Send Event on Print state - if hasattr(events, str("Print" + lstate.capitalize())): - event_obj = getattr(events, str("Print" + lstate.capitalize())) - event = event_obj(self._current_file_name, self.file_metadata) + event_class_name = "Print" + event_state.capitalize() + if hasattr(events, event_class_name): + event_obj = getattr(events, event_class_name) + event = event_obj(_event_file, _event_meta) instance = QtWidgets.QApplication.instance() if instance: instance.postEvent(self.window(), event) return logger.error( - "QApplication.instance expected non None value,\ - Unable to post event %s", - str("Print" + lstate.capitalize()), + "QApplication.instance expected non None value," + " Unable to post event %s", + event_class_name, ) @QtCore.pyqtSlot(str, dict, name="on_print_stats_update") @QtCore.pyqtSlot(str, float, name="on_print_stats_update") @QtCore.pyqtSlot(str, str, name="on_print_stats_update") def on_print_stats_update(self, field: str, value: dict | float | str) -> None: - """Processes the information that comes from the printer object "print_stats" - Displays information on the ui accordingly. + """Process updates from the ``print_stats`` printer object. Args: - field (str): The name of the updated field. - value (dict | float | str): The value for the field. + field: The name of the updated field. + value: The value for the field. """ if isinstance(value, str): if "state" in field: @@ -273,29 +287,29 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: self.js_file_name_label.setText(self._current_file_name) if self.isVisible(): self.request_file_info.emit(value) - if not self.file_metadata: - return - if not self.isVisible(): - return + # Layer info must be processed regardless of visibility so + # Klipper's runtime values always override metadata defaults. if isinstance(value, dict): - self.layer_fallback = False - if "total_layer" in value.keys(): - self.total_layers = value["total_layer"] + if "total_layer" in value: if value["total_layer"] is not None: + self.total_layers = value["total_layer"] self.layer_display_button.secondary_text = str(self.total_layers) - else: self.total_layers = "---" - self.layer_fallback = True - if "current_layer" in value.keys(): + if "current_layer" in value: if value["current_layer"] is not None: - _current_layer = value["current_layer"] - self.layer_display_button.setText(f"{int(_current_layer)}") + self.layer_display_button.setText(f"{int(value['current_layer'])}") + self.layer_fallback = False else: self.layer_display_button.setText("---") self.layer_fallback = True - elif isinstance(value, float): + # print_duration is tracked regardless of visibility (gates Z fallback) + if isinstance(value, float) and "print_duration" in field: + self._print_duration = value + if not self.isVisible(): + return + if isinstance(value, float): if "total_duration" in field: _time = estimate_print_time(int(value)) _print_time_string = ( @@ -307,33 +321,49 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: @QtCore.pyqtSlot(str, list, name="on_gcode_move_update") def on_gcode_move_update(self, field: str, value: list) -> None: - """Handle gcode move""" + """Z-position fallback for layer count display. + + Only runs when Klipper does not provide + ``print_stats.info.current_layer`` (``layer_fallback`` is True) + AND ``print_duration > 0``. The ``print_duration`` gate + matches Mainsail's ``getPrintCurrentLayer`` getter which + prevents layer updates during pre-print procedures (heating, + nozzle cleaning). + """ if not self.isVisible(): return - if "gcode_position" in field: - if self._internal_print_status == "printing": - if self.layer_fallback: - object_height = float(self.file_metadata.get("object_height", -1.0)) - layer_height = float(self.file_metadata.get("layer_height", -1.0)) - first_layer_height = float( - self.file_metadata.get("first_layer_height", -1.0) - ) - _current_layer = calculate_current_layer( - z_position=value[2], - object_height=object_height, - layer_height=layer_height, - first_layer_height=first_layer_height, - ) - - total_layer = ( - (object_height) / layer_height if layer_height > 0 else -1 - ) - self.layer_display_button.secondary_text = ( - f"{int(total_layer)}" if total_layer != -1 else "---" - ) - self.layer_display_button.setText( - f"{int(_current_layer)}" if _current_layer != -1 else "---" - ) + if "gcode_position" not in field: + return + if self._internal_print_status != "printing": + return + if not self.layer_fallback: + return + # Mainsail: only calculate layers when print_duration > 0 + if self._print_duration <= 0: + return + if len(value) <= 2: + return + meta = self.file_metadata + if not meta: + return + object_height = float(meta.get("object_height", 0)) + layer_height = float(meta.get("layer_height", 0)) + first_layer_height = float(meta.get("first_layer_height", 0)) + if layer_height <= 0: + return + # Mainsail getPrintMaxLayers fallback + _max_layers = calculate_max_layers( + object_height, layer_height, first_layer_height + ) + if _max_layers > 0: + self.layer_display_button.secondary_text = str(_max_layers) + _current_layer = calculate_current_layer( + z_position=value[2], + layer_height=layer_height, + first_layer_height=first_layer_height, + max_layers=_max_layers, + ) + self.layer_display_button.setText(str(_current_layer)) @QtCore.pyqtSlot(str, float, name="virtual_sdcard_update") @QtCore.pyqtSlot(str, bool, name="virtual_sdcard_update") @@ -488,17 +518,20 @@ def _setupUI(self) -> None: ) self.job_content_layout.addLayout(self.job_stats_display_layout) - def create_thumbnail_widget(self) -> None: - """Create thumbnail graphics view widget""" + def _ensure_thumbnail_widget(self) -> None: + """Create thumbnail graphics view widget (once).""" + if hasattr(self, "thumbnail_view"): + return self.thumbnail_view = QtWidgets.QGraphicsView() self.thumbnail_view.setMinimumSize(QtCore.QSize(48, 48)) self.thumbnail_view.setAttribute( QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True ) + self.thumbnail_view.setStyleSheet( + "QGraphicsView { background: transparent; border: none; }" + ) self.thumbnail_view.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) self.thumbnail_view.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) - self.thumbnail_view.setWindowFlags(QtCore.Qt.WindowType.FramelessWindowHint) - self.thumbnail_view.setObjectName("thumbnail_scene") _thumbnail_palette = QtGui.QPalette() _thumbnail_palette.setColor( QtGui.QPalette.ColorRole.Window, QtGui.QColor(0, 0, 0, 0) @@ -507,9 +540,14 @@ def create_thumbnail_widget(self) -> None: QtGui.QPalette.ColorRole.Base, QtGui.QColor(0, 0, 0, 0) ) self.thumbnail_view.setPalette(_thumbnail_palette) + self.thumbnail_view.setAutoFillBackground(False) _thumbnail_brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) _thumbnail_brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) self.thumbnail_view.setBackgroundBrush(_thumbnail_brush) + # Use a transparent viewport widget to prevent black background on eglfs + viewport = QtWidgets.QWidget() + viewport.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True) + self.thumbnail_view.setViewport(viewport) self.thumbnail_view.setRenderHints( QtGui.QPainter.RenderHint.Antialiasing | QtGui.QPainter.RenderHint.SmoothPixmapTransform @@ -521,4 +559,8 @@ def create_thumbnail_widget(self) -> None: self.thumbnail_view.setObjectName("thumbnail_scene") self.thumbnail_view_layout = QtWidgets.QHBoxLayout(self) self.thumbnail_view_layout.addWidget(self.thumbnail_view) + self.thumbnail_view.installEventFilter(self) + self.printing_progress_bar.thumbnail_clicked.connect( + self.toggle_thumbnail_expansion + ) self.thumbnail_view.hide() 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 fe2fa180..9253858c 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,18 +424,20 @@ 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) + _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") @@ -466,19 +463,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") @@ -522,29 +519,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", False) - if is_active and 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 "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 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 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: @@ -554,6 +554,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 301c5cd1..6c09a4d1 100644 --- a/BlocksScreen/lib/panels/widgets/tunePage.py +++ b/BlocksScreen/lib/panels/widgets/tunePage.py @@ -1,7 +1,6 @@ import re import typing -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 @@ -67,7 +66,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, ) @@ -75,30 +74,26 @@ def __init__(self, parent) -> 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") @@ -113,6 +108,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