diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index 556119c2..28f36966 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -28,6 +28,10 @@ jobs: cache: pip cache-dependency-path: scripts/requirements-dev.txt + - name: Install system dependencies + if: matrix.test-type == 'pytest' + run: sudo apt-get install -y libgl1 libglib2.0-0 libegl1 + - name: Install dependencies run: | echo "Installing dependencies" diff --git a/BlocksScreen.cfg b/BlocksScreen.cfg index b5807fa0..42ab150b 100644 --- a/BlocksScreen.cfg +++ b/BlocksScreen.cfg @@ -3,4 +3,7 @@ host: localhost port: 7125 [screensaver] -timeout: 5000 \ No newline at end of file +timeout: 5000 + +[usb_manager] +gcodes_dir: ~/printer_data/gcodes/ diff --git a/BlocksScreen/BlocksScreen.py b/BlocksScreen/BlocksScreen.py index a7a2098e..ab198a7e 100644 --- a/BlocksScreen/BlocksScreen.py +++ b/BlocksScreen/BlocksScreen.py @@ -2,11 +2,28 @@ import sys import typing -import logger -from lib.panels.mainWindow import MainWindow -from PyQt6 import QtCore, QtGui, QtWidgets +from logger import CrashHandler, LogManager, install_crash_handler, setup_logging + +install_crash_handler() + +from lib.panels.mainWindow import MainWindow # noqa: E402 +from PyQt6 import QtCore, QtGui, QtWidgets # noqa: E402 + + +class BlocksScreenApp(QtWidgets.QApplication): + """QApplication subclass that routes unhandled slot exceptions to CrashHandler.""" + + def notify(self, a0: QtCore.QObject, a1: QtCore.QEvent) -> bool: # type: ignore[override] + try: + return super().notify(a0, a1) + except Exception: + exc_type, exc_value, exc_tb = sys.exc_info() + handler = CrashHandler._instance + if handler is not None and exc_type is not None and exc_value is not None: + handler._exception_hook(exc_type, exc_value, exc_tb) + return False + -_logger = logging.getLogger(name="logs/BlocksScreen.log") QtGui.QGuiApplication.setAttribute( QtCore.Qt.ApplicationAttribute.AA_SynthesizeMouseForUnhandledTouchEvents, True, @@ -22,13 +39,6 @@ RESET = "\033[0m" -def setup_app_loggers(): - """Setup logger""" - _ = logger.create_logger(name="logs/BlocksScreen.log", level=logging.DEBUG) - _logger = logging.getLogger(name="logs/BlocksScreen.log") - _logger.info("============ BlocksScreen Initializing ============") - - def show_splash(window: typing.Optional[QtWidgets.QWidget] = None): """Show splash screen on app initialization""" logo = QtGui.QPixmap("BlocksScreen/BlocksScreen/lib/ui/resources/logoblocks.png") @@ -38,13 +48,28 @@ def show_splash(window: typing.Optional[QtWidgets.QWidget] = None): splash.finish(window) +def on_quit() -> None: + logging.info("Final exit cleanup") + LogManager.shutdown() + + if __name__ == "__main__": - setup_app_loggers() - BlocksScreen = QtWidgets.QApplication([]) + setup_logging( + filename="logs/BlocksScreen.log", + level=logging.DEBUG, + console_output=True, + console_level=logging.DEBUG, + capture_stderr=True, + capture_stdout=False, + ) + _logger = logging.getLogger(__name__) + _logger.info("============ BlocksScreen Initializing ============") + BlocksScreen = BlocksScreenApp([]) BlocksScreen.setApplicationName("BlocksScreen") BlocksScreen.setApplicationDisplayName("BlocksScreen") BlocksScreen.setDesktopFileName("BlocksScreen") main_window = MainWindow() BlocksScreen.processEvents() + BlocksScreen.aboutToQuit.connect(on_quit) main_window.show() sys.exit(BlocksScreen.exec()) diff --git a/BlocksScreen/configfile.py b/BlocksScreen/configfile.py index 981ac4b2..ed5828a0 100644 --- a/BlocksScreen/configfile.py +++ b/BlocksScreen/configfile.py @@ -37,6 +37,8 @@ from helper_methods import check_file_on_path +logger = logging.getLogger(__name__) + HOME_DIR = os.path.expanduser("~/") WORKING_DIR = os.getcwd() DEFAULT_CONFIGFILE_PATH = pathlib.Path(HOME_DIR, "printer_data", "config") @@ -56,11 +58,19 @@ class ConfigError(Exception): """Exception raised when Configfile errors exist""" def __init__(self, msg) -> None: + """Store the error message on both the exception and the ``msg`` attribute.""" super().__init__(msg) self.msg = msg class BlocksScreenConfig: + """Thread-safe wrapper around :class:`configparser.ConfigParser` with raw-text tracking. + + Maintains a ``raw_config`` list that mirrors the on-disk file so that + ``add_section``, ``add_option``, and ``update_option`` can write back + changes without losing comments or formatting. + """ + config = configparser.ConfigParser( allow_no_value=True, ) @@ -70,6 +80,7 @@ class BlocksScreenConfig: def __init__( self, configfile: typing.Union[str, pathlib.Path], section: str ) -> None: + """Initialise with the path to the config file and the default section name.""" self.configfile = pathlib.Path(configfile) self.section = section self.raw_config: typing.List[str] = [] @@ -77,9 +88,11 @@ def __init__( self.file_lock = threading.Lock() # Thread safety for future work def __getitem__(self, key: str) -> BlocksScreenConfig: + """Return a :class:`BlocksScreenConfig` for *key* section (same as ``get_section``).""" return self.get_section(key) def __contains__(self, key): + """Return True if *key* is a section in the underlying ConfigParser.""" return key in self.config def sections(self) -> typing.List[str]: @@ -91,7 +104,7 @@ def get_section( ) -> BlocksScreenConfig: """Get configfile section""" if not self.config.has_section(section): - raise configparser.NoSectionError(f"No section with name: {section}") + return fallback return BlocksScreenConfig(self.configfile, section) def get_options(self) -> list: @@ -193,12 +206,14 @@ def getboolean( ) def _find_section_index(self, section: str) -> int: + """Return the index of the ``[section]`` header line in ``raw_config``.""" try: return self.raw_config.index("[" + section + "]") except ValueError as e: raise configparser.Error(f'Section "{section}" does not exist: {e}') def _find_section_limits(self, section: str) -> typing.Tuple: + """Return ``(start_index, end_index)`` of *section* in ``raw_config``.""" try: section_start = self._find_section_index(section) buffer = self.raw_config[section_start:] @@ -212,6 +227,7 @@ def _find_section_limits(self, section: str) -> typing.Tuple: def _find_option_index( self, section: str, option: str ) -> typing.Union[Sentinel, int, None]: + """Return the index of the *option* line within *section* in ``raw_config``.""" try: start, end = self._find_section_limits(section) section_buffer = self.raw_config[start:][:end] @@ -253,9 +269,9 @@ def add_section(self, section: str) -> None: self.config.add_section(section) self.update_pending = True except configparser.DuplicateSectionError as e: - logging.error(f'Section "{section}" already exists. {e}') + logger.error(f'Section "{section}" already exists. {e}') except configparser.Error as e: - logging.error(f'Unable to add "{section}" section to configuration: {e}') + logger.error(f'Unable to add "{section}" section to configuration: {e}') def add_option( self, @@ -283,12 +299,46 @@ def add_option( self.config.set(section, option, value) self.update_pending = True except configparser.DuplicateOptionError as e: - logging.error(f"Option {option} already present on {section}: {e}") + logger.error(f"Option {option} already present on {section}: {e}") except configparser.Error as e: - logging.error( + logger.error( f'Unable to add "{option}" option to section "{section}": {e} ' ) + def update_option( + self, + section: str, + option: str, + value: typing.Any, + ) -> None: + """Update an existing option's value in both raw tracking and configparser.""" + try: + with self.file_lock: + if not self.config.has_section(section): + self.add_section(section) + + if not self.config.has_option(section, option): + self.add_option(section, option, str(value)) + return + + line_idx = self._find_option_line_index(section, option) + self.raw_config[line_idx] = f"{option}: {value}" + self.config.set(section, option, str(value)) + self.update_pending = True + except Exception as e: + logger.error( + f'Unable to update option "{option}" in section "{section}": {e}' + ) + + def _find_option_line_index(self, section: str, option: str) -> int: + """Find the index of an option line within a specific section.""" + start, end = self._find_section_limits(section) + opt_regex = re.compile(rf"^\s*{re.escape(option)}\s*[:=]") + for i in range(start + 1, end): + if opt_regex.match(self.raw_config[i]): + return i + raise configparser.Error(f'Option "{option}" not found in section "{section}"') + def save_configuration(self) -> None: """Save teh configuration to file""" try: @@ -301,7 +351,7 @@ def save_configuration(self) -> None: self.config.write(sio) sio.close() except Exception as e: - logging.error( + logger.error( f"ERROR: Unable to save new configuration, something went wrong while saving updated configuration. {e}" ) finally: @@ -319,6 +369,14 @@ def load_config(self): raise configparser.Error(f"Error loading configuration file: {e}") def _parse_file(self) -> typing.Tuple[typing.List[str], typing.Dict]: + """Read and normalise the config file into a raw line list and a nested dict. + + Strips comments, normalises ``=`` to ``:`` separators, deduplicates + sections/options, and ensures the buffer ends with an empty line. + + Returns: + A tuple of (raw_lines, dict_representation). + """ buffer = [] dict_buff: typing.Dict = {} curr_sec: typing.Union[Sentinel, str] = Sentinel.MISSING @@ -336,7 +394,7 @@ def _parse_file(self) -> typing.Tuple[typing.List[str], typing.Dict]: if not line: continue # remove leading and trailing white spaces - line = re.sub(r"\s*([:=])\s*", r"\1", line) + line = re.sub(r"\s*([:=])\s*", r"\1 ", line) line = re.sub(r"=", r":", line) # find the beginning of sections section_match = re.compile(r"[^\s]*\[([^]]+)\]") @@ -344,9 +402,10 @@ def _parse_file(self) -> typing.Tuple[typing.List[str], typing.Dict]: if match_sec: sec_name = re.sub(r"[\[*\]]", r"", line) if sec_name not in dict_buff.keys(): - buffer.extend( - [""] - ) # REFACTOR: Just add some line separation between sections + if buffer: + buffer.extend( + [""] + ) # REFACTOR: Just add some line separation between sections dict_buff.update({sec_name: {}}) curr_sec = sec_name else: @@ -386,6 +445,6 @@ def get_configparser() -> BlocksScreenConfig: config_object = BlocksScreenConfig(configfile=configfile, section="server") config_object.load_config() if not config_object.has_section("server"): - logging.error("Error loading configuration file for the application.") + logger.error("Error loading configuration file for the application.") raise ConfigError("Section [server] is missing from configuration") - return BlocksScreenConfig(configfile=configfile, section="server") + return config_object diff --git a/BlocksScreen/devices/storage/__init__.py b/BlocksScreen/devices/storage/__init__.py new file mode 100644 index 00000000..beeb1563 --- /dev/null +++ b/BlocksScreen/devices/storage/__init__.py @@ -0,0 +1,24 @@ +from .usb_controller import USBManager + +__doc__ = """ + +The storage package contains a tool that monitors +pluggable usb devices via python-sdbus library. +While offering an automounting option. +The package is also capable of creating a symlink that +points directly to the mounted usb drive on the gcodes +directory. + + +There is still a lot of functionality missing, that may +be added in the future, but for now it just automounts, +creates symlinks, cleans up broken symlinks on the +gcodes directory. + + +All tools related to storage devices should be contained +in this package directory. +""" +__version__ = "0.0.1" +__all__ = ["USBManager"] +__name__ = "storage" diff --git a/BlocksScreen/devices/storage/device.py b/BlocksScreen/devices/storage/device.py new file mode 100644 index 00000000..c4057913 --- /dev/null +++ b/BlocksScreen/devices/storage/device.py @@ -0,0 +1,104 @@ +from .udisks2_dbus_async import ( + UDisks2BlockAsyncInterface, + UDisks2DriveAsyncInterface, + UDisks2PartitionAsyncInterface, + UDisks2FileSystemAsyncInterface, + UDisks2PartitionTableAsyncInterface, +) + + +class Device: + def __init__( + self, + path: str, + DriveInterface: UDisks2DriveAsyncInterface, + symlink_path: str, + ) -> None: + self.path: str = path + self.symlink_path: str = symlink_path + self.driver_interface: UDisks2DriveAsyncInterface = DriveInterface + self.partitions: dict[str, UDisks2PartitionAsyncInterface] = {} + self.raw_block: dict[str, UDisks2BlockAsyncInterface] = {} + self.logical_blocks: dict[str, UDisks2BlockAsyncInterface] = {} + self.file_systems: dict[str, UDisks2FileSystemAsyncInterface] = {} + self.partition_tables: dict[str, UDisks2PartitionTableAsyncInterface] = {} + self.symlinks: list[str] = [] + + def get_logical_blocks(self) -> dict[str, UDisks2BlockAsyncInterface]: + """The available logical blocks for the device""" + return self.logical_blocks + + def get_driver(self) -> UDisks2DriveAsyncInterface | None: + """Get current device driver""" + if not self.driver_interface: + return None + return self.driver_interface + + def update_file_system( + self, path: str, data: UDisks2FileSystemAsyncInterface + ) -> None: + """Add or update a filesystem for this device + + Args: + path (str): filesystem path + data (UDisks2FileSystemAsyncInterface): The interface + """ + self.file_systems.update({path: data}) + + def update_raw_block(self, path: str, block: UDisks2BlockAsyncInterface) -> None: + """Add or update a raw block for this device + + Args: + path (str): block path + data (UDisks2BlockAsyncInterface): The blocks interface + """ + self.raw_block.update({path: block}) + + def update_logical_blocks( + self, path: str, block: UDisks2BlockAsyncInterface + ) -> None: + """Add or update a logical block for this device + + Args: + path (str): block path + data (UDisks2BlockAsyncInterface): The block interface + """ + self.logical_blocks.update({path: block}) + + def update_part_table( + self, path: str, part: UDisks2PartitionTableAsyncInterface + ) -> None: + """Add or update partition table for this device + + Args: + path (str): Partition table path + part (UDisks2PartitionTableAsyncInterface): The interface + """ + self.partition_tables.update({path: part}) + + def update_partitions( + self, path: str, block: UDisks2PartitionAsyncInterface + ) -> None: + """Add or update partitions for the current device + + Args: + path (str): the partition path + data (UDisks2PartitionAsyncInterface): The partition interface + """ + self.partitions.update({path: block}) + + def kill(self) -> None: + """Delete the device and removes any track of it + + Especially used when devices were removed unsafely + """ + self.delete() + + def delete(self) -> None: + """Cleanup and delete this device""" + del self.driver_interface + self.partitions.clear() + self.raw_block.clear() + self.file_systems.clear() + self.partition_tables.clear() + self.symlinks.clear() diff --git a/BlocksScreen/devices/storage/udisks2.py b/BlocksScreen/devices/storage/udisks2.py new file mode 100644 index 00000000..a766656e --- /dev/null +++ b/BlocksScreen/devices/storage/udisks2.py @@ -0,0 +1,577 @@ +import asyncio +import logging +import os +import pathlib +import shutil +import typing +from collections.abc import Coroutine +import unicodedata + +import sdbus +from PyQt6 import QtCore + +from .device import Device +from .udisks2_dbus_async import ( + Interfaces, + UDisks2AsyncManager, + UDisks2BlockAsyncInterface, + UDisks2DriveAsyncInterface, + UDisks2FileSystemAsyncInterface, + UDisks2PartitionAsyncInterface, + UDisks2PartitionTableAsyncInterface, +) + +UDisks2_service: str = "org.freedesktop.UDisks2" +UDisks2_obj_path: str = "org/freedesktop/UDisks2" +AlreadyMountedException = "org.freedesktop.UDisks2.Error.AlreadyMounted" + +_T = typing.TypeVar(name="_T") + + +def validate_label(label: str, strict: bool = True, max_length: int = 100) -> str: + """ + Comprehensive validation for filesystem labels with security protection. + + Args: + label: Raw input label to validate + strict: If True, returns empty string for any invalid input + max_length: Maximum allowed length in bytes + + Returns: + Sanitized and validated label safe for filesystem use + """ + if not label: + return "" + if not label.strip(): + return "" + if len(label.encode("utf-8")) > max_length: + return "" if strict else label[:max_length] + normalized_label = unicodedata.normalize("NFC", label) + if any(ord(char) < 32 for char in normalized_label): + if strict: + return "" + normalized_label = "".join(char for char in normalized_label if ord(char) >= 32) + + dangerous_chars = { + "\0", + "\x00", + "/", + "\\", + ";", + "|", + "&", + "$", + "`", + "(", + ")", + "{", + "}", + "[", + "]", + "<", + ">", + '"', + "'", + "*", + "?", + "!", + } + clean_label = "".join(c for c in normalized_label if c not in dangerous_chars) + if ( + ".." in clean_label + or clean_label.startswith("/") + or clean_label.startswith("\\") + ): + return ( + "" + if strict + else clean_label.replace("..", "").replace("/", "_").replace("\\", "_") + ) + + final_label = clean_label.strip(" .")[:max_length] + return final_label if final_label else "" + + +def fire_n_forget( + coro: Coroutine[typing.Any, typing.Any, typing.Any], + name: str, + task_stack: set[asyncio.Task[typing.Any]], +) -> asyncio.Task[typing.Any]: + task: asyncio.Task[typing.Any] = asyncio.create_task(coro, name=name) + task_stack.add(task) + """Create a task for a specified coroutine and run it + + Args: + coro (Coroutine): coroutine to create task + name (str): Name for the task + task_stack (set): Task stack that keeps track of currently running tasks + """ + + def cleanup(task: asyncio.Task[_T]) -> None: + """Cleanup task""" + task_stack.discard(task) + try: + task.result() + except asyncio.CancelledError: + task.cancel() + logging.error("Task %s was cancelled", task.get_name()) + except Exception as e: + logging.error( + "Caught exception in %s : %s", task.get_name(), e, exc_info=True + ) + + task.add_done_callback(cleanup) + return task + + +class UDisksDBusAsync(QtCore.QThread): + hardware_detected: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="hardware-detected" + ) + device_added: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, dict, name="device-added" + ) + device_removed: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, str, name="device-removed" + ) # device path + hardware_removed: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="hardware-removed" + ) # device path + device_mounted: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, str, name="device-mounted" + ) # device path, new symlink path + device_unmounted: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="device-unmounted" + ) + + def __init__(self, parent: QtCore.QObject, gcodes_dir: str) -> None: + super().__init__(parent) + self.task_stack: set[asyncio.Task[typing.Any]] = set() + self.gcodes_path: pathlib.Path = pathlib.Path(gcodes_dir) + self.system_bus: sdbus.SdBus = sdbus.sd_bus_open_system() + self._active: bool = False + if not self.system_bus: + self.close() + return + sdbus.set_default_bus(self.system_bus) + self.obj_manager: UDisks2AsyncManager = UDisks2AsyncManager.new_proxy( + service_name="org.freedesktop.UDisks2", + object_path="/org/freedesktop/UDisks2", + bus=self.system_bus, + ) + self.loop: asyncio.AbstractEventLoop | None = None + self.stop_event: asyncio.Event = asyncio.Event() + self.listener_running: bool = False + self.controlled_devs: dict[str, Device] = {} + self._cleanup_broken_symlinks() + self._cleanup_legacy_dir() + + @property + def active(self) -> bool: + return self._active + + def run(self) -> None: + """Start UDisks2 USB monitoring""" + self.stop_event.clear() + try: + self.loop = asyncio.new_event_loop() + self._active = True + asyncio.set_event_loop(self.loop) + self.loop.run_until_complete(self.monitor_dbus()) + except asyncio.CancelledError as err: + logging.error("Caught exception on udisks2 monitor, %s", err) + self.close() + return + + def close(self) -> None: + """Close usb devices monitoring thread and run loop""" + try: + if not self.loop: + return + if self.loop.is_running(): + self.stop_event.set() + self.loop.call_soon_threadsafe(self.loop.stop) + _ = self.wait() + self._active = False + for path in self.controlled_devs.keys(): + dev: Device = self.controlled_devs.pop(path) + dev.delete() + self.terminate() + self.deleteLater() + except asyncio.CancelledError as e: + logging.error( + "Caught exception while trying to close Udisks2 monitor: %s", e + ) + + async def monitor_dbus(self) -> None: + """Schedule coroutines for UDisks2 signals `interfaces_added`, `interfaces_removed` + and `properties_changed`. Creates symlink upon device insertion and cleans up symlink on removal. + + """ + tasks: dict[str, Coroutine[typing.Any, typing.Any, typing.Any]] = { + "add": self._add_interface_listener(), + "rem": self._rem_interface_listener(), + "prop": self._properties_changed_listener(), + } + _ = fire_n_forget( + coro=self.restore_tracked(), + name="Main-Restore-Discovery", + task_stack=self.task_stack, + ) + managed_tasks: list[asyncio.Task[typing.Any]] = [] + for name, coro in tasks.items(): + t = asyncio.create_task(coro, name=name) + self.task_stack.add(t) + t.add_done_callback(lambda _: self.task_stack.discard(t)) + managed_tasks.append(t) + try: + await asyncio.gather(*managed_tasks) + except asyncio.CancelledError as e: + for task in self.task_stack: + _ = task.cancel() + logging.info("UDisks2 Monitor stopped: %s", e) + self._active = False + except sdbus.SdBusBaseError as e: + logging.error("Caught generic fatal Sdbus exception: %s", e, exc_info=True) + self.close() + except Exception as e: + logging.error("Caught exception UDisks2 listeners failed: %s", e) + self._active = False + + async def restore_tracked(self) -> None: + """Get and restore controlled mass storage devices""" + info = await self.obj_manager.get_managed_objects() + for path, interfaces in info.items(): + fire_n_forget( + coro=self._handle_new_device(path, interfaces), + name=f"Restore-Discovery-{path}", + task_stack=self.task_stack, + ) + + async def _add_interface_listener(self) -> None: + """Handle add interface signal from UDisks2 DBus connection + + Adds the new device to internal tracking, can be retrieved with device path + Creates symlink onto specified directory configured on the class + """ + async for path, interfaces in self.obj_manager.interfaces_added: + fire_n_forget( + self._handle_new_device(path, interfaces), + name=f"UDisks-Discovery-{path}", + task_stack=self.task_stack, + ) + + async def _handle_new_device(self, path: str, interfaces) -> None: + """Handle new devices, can be used on `interfaces_added` signal and + when recovering states from `get_managed_objects` + + """ + try: + if Interfaces.Drive.value in interfaces: + ddev: UDisks2DriveAsyncInterface = UDisks2DriveAsyncInterface.new_proxy( + service_name=UDisks2_service, + object_path=path, + bus=self.system_bus, + ) + hwbus: str = await ddev.connection_bus + logging.debug( + "New Hardware device recognized type: %s \n path: %s", + hwbus, + path, + ) + media_removable, ejectable, con_bus = await asyncio.gather( + ddev.media_removable, ddev.ejectable, ddev.connection_bus + ) + if not (media_removable and ejectable and con_bus == "usb"): + # Only handle usb devices and removable storage media + return + device: Device = Device( + path, DriveInterface=ddev, symlink_path=self.gcodes_path.as_posix() + ) + self.controlled_devs.update({path: device}) + self.hardware_detected[str].emit(path) + if Interfaces.Block.value in interfaces: + bdev: UDisks2BlockAsyncInterface = UDisks2BlockAsyncInterface.new_proxy( + service_name=UDisks2_service, + object_path=path, + bus=self.system_bus, + ) + drv_path: str = await bdev.drive + hint_sys, hint_ignore = await asyncio.gather( + bdev.hint_system, bdev.hint_ignore + ) + dev_name = await bdev.hint_name or await bdev.id_label + if hint_sys or hint_ignore: + # Always ignore device if these flags are set + return + if drv_path in self.controlled_devs: + dev: Device = self.controlled_devs[drv_path] + if all( + phase in interfaces + for phase in ( + Interfaces.PartitionTable.value, + Interfaces.Block.value, + ) + ): + devpt: UDisks2PartitionTableAsyncInterface = ( + UDisks2PartitionTableAsyncInterface.new_proxy( + service_name=UDisks2_service, + object_path=path, + bus=self.system_bus, + ) + ) + dev.update_raw_block(path, bdev) + dev.update_part_table(path, devpt) + if Interfaces.Filesystem.value in interfaces: + devfs: UDisks2FileSystemAsyncInterface = ( + UDisks2FileSystemAsyncInterface.new_proxy( + service_name=UDisks2_service, + object_path=path, + bus=self.system_bus, + ) + ) + dev.update_file_system(path, devfs) + self.device_added.emit(path, interfaces) + _label = dev_name or "" + self.mount(dev, _label) + if Interfaces.Partition.value in interfaces: + devpart: UDisks2PartitionAsyncInterface = ( + UDisks2PartitionAsyncInterface.new_proxy( + service_name=UDisks2_service, + object_path=path, + bus=self.system_bus, + ) + ) + dev.update_partitions(path, devpart) + dev.update_logical_blocks(path, bdev) + except sdbus.dbus_exceptions.DbusUnknownMethodError as e: + logging.error( + "Caught exception on device inserted unknown method: %s", + e, + exc_info=True, + ) + except sdbus.dbus_exceptions.DbusUnknownInterfaceError as e: + logging.error( + "Caught exception on device inserted unknown interface: %s", + e, + exc_info=True, + ) + except Exception as e: + logging.error( + "Caught fatal exception during discovery process %s: %s", + path, + e, + exc_info=True, + ) + + async def _properties_changed_listener(self) -> None: + """Handle properties_changed signal from UDisks2 Dbus connection + + Updates tracked objects + """ + async for ( + path, + changed_properties, + invalid_properties, + ) in self.obj_manager.properties_changed: + pass + + async def _rem_interface_listener(self) -> None: + """Handle device removal signals from UDisks2 Dbus connection + + Removes tracked interface and cleans up any left behind data + """ + async for path, interfaces in self.obj_manager.interfaces_removed: + try: + if Interfaces.Drive.value in interfaces: + if path in self.controlled_devs: + device: Device = self.controlled_devs.pop(path) + device.kill() + del device + self.hardware_removed[str].emit(path) + self._cleanup_broken_symlinks() + except sdbus.dbus_exceptions.DbusUnknownMethodError as e: + logging.error( + "Caught exception on device removed unknown method: %s", + e, + exc_info=True, + ) + except sdbus.dbus_exceptions.DbusUnknownInterfaceError as e: + logging.error( + "Caught exception on device removed unknown interface %s", + e, + exc_info=True, + ) + except Exception as e: + logging.error( + "Caught fatal exception on removed device: %s, %s", + path, + e, + exc_info=True, + ) + + def mount(self, device: Device, label: str = ""): + """Mounts the devices mountpoints""" + for path, filesystem in device.file_systems.items(): + _ = fire_n_forget( + coro=self._mount_filesystem(filesystem, label), + name=f"Mount-filesystem-{path}", + task_stack=self.task_stack, + ) + + async def _mount_filesystem( + self, filesystem: UDisks2FileSystemAsyncInterface, label: str = "" + ) -> str: + val_label: str = validate_label(label) + try: + opts: dict[str, tuple[str, typing.Any]] = { + "auto.no_user_interactions": ("b", True), + "fstype": ("s", "auto"), + "as-user": ("s", os.environ.get("USER")), + "options": ("s", "rw,relatime,sync"), + } + mnt_path: str = await filesystem.mount(opts) + return self.add_symlink( + path=mnt_path, label=val_label, dst_path=self.gcodes_path.as_posix() + ) + except sdbus.SdBusUnmappedMessageError as e: + if AlreadyMountedException in e.args[0]: + logging.debug( + "Device filesystem already mounted on %s, verifying gcodes symlink", + str(e.args[1]), + ) + mount_points: list[bytes] = await filesystem.mount_points + if not mount_points: + return "" + mpoint: str = mount_points[0].decode("utf-8").strip("\x00") + if os.path.exists(mpoint): + return "" + return self.add_symlink( + path=mpoint, + dst_path=self.gcodes_path.as_posix(), + label=val_label, + ) + except Exception as e: + logging.error( + "Caught exception while mounting file system %s : %s", + filesystem, + e, + exc_info=True, + ) + return "" + + def add_symlink( + self, + path: str, + dst_path: str, + label: str = "", + _index: int = 0, + _validated: bool = False, + ) -> str: + """Create symlink on `dst_path` + + If there is a symlink created on `dst_path` with the same label, + which points to the same `path` then it will return the `dst_path` + as validation. If `dst_path` does not resolve to the same `path` + then it will cleanup that symlink and create a replacement. + + In case there is no `label` then the created `symlink` on `dst_path` + will default to **USB DRIVE**. If *USB DRIVE* symlink already exists + then it will create a variant of that fallback **USB DRIVE [1-254]** + + Be careful with the provided directories and labels. They must come + clean or else this method won't work. + """ + if not _validated and label: + label = validate_label(label, strict=True) + label = "USB-" + label + fallback: str = "USB DRIVE" if _index == 0 else str(f"USB DRIVE {_index}") + dstb = pathlib.Path(dst_path).joinpath(label if label else fallback) + try: + if not os.path.islink(dstb): + os.symlink(src=path, dst=dstb) + return dstb.as_posix() if os.path.exists(dstb) else "" + if os.path.islink(dstb): + if dstb.resolve().as_posix() == pathlib.Path(path).as_posix(): + return dstb.as_posix() + if not label: + _index += 1 + if _index == 255: + return "" + return self.add_symlink(path, dst_path, label, _index, _validated=True) + if self.rem_symlink(path=dstb.as_posix()): + return self.add_symlink(path, dst_path, label, _validated=True) + except PermissionError: + logging.error( + "Caught fatal exception no permissions, unable to create symlink on specified path" + ) + except OSError as e: + logging.error("Caught fatal exception OSERROR %s", e) + return "" + + def rem_symlink(self, path: str | pathlib.Path) -> bool: + """Remove `ONLY` symlinks located in `path` if it is allowed""" + resolved_path = pathlib.Path(path) + resolved_gcodes_path = pathlib.Path(self.gcodes_path).as_posix() + try: + _ = resolved_path.relative_to(resolved_gcodes_path) + except ValueError: + logging.error("Path transversal attempt in rem_symlink: %s", path) + return False + + if not os.path.islink(resolved_path): + logging.error("Provided path %s is NOT a symlink, refusing to delete", path) + return False + try: + os.remove(resolved_path) + return True + except (PermissionError, OSError): + logging.error("Caught fatal exception failed to remove symlink %s", path) + return False + + def _cleanup_symlinks(self) -> None: + """Cleanup all symlinks on gcodes directory + + This method is private, if used outside of it's intended purpose + devices will lose track of what symlinks are associated with them + + USE WITH CARE + """ + for dir in self.gcodes_path.rglob("*"): + if os.path.islink(dir): + _ = self.rem_symlink(dir.as_posix()) + + def _cleanup_broken_symlinks(self) -> None: + for dir in self.gcodes_path.rglob("*"): + if os.path.islink(dir) and not os.path.exists(dir): + _ = self.rem_symlink(dir) + + def _resolve_symlinks( + self, path: str | pathlib.Path, mount_path: str | pathlib.Path + ) -> bool: + """Checks if `gcodes directory` already has a symlink that + resolves to the same mount directory. + + + This method returns on the first encounter. It does not + evaluate any other symlinks after that. + """ + for dir in self.gcodes_path.rglob("*"): + if os.path.islink(dir): + if ( + pathlib.Path(path).resolve().as_posix() + == pathlib.Path(mount_path).as_posix() + ): + # This `path` resolves to the `mount_path` + return True + return False + + def _cleanup_legacy_dir(self) -> None: + """Removes legacy directory that contained symlinks + for all mounted USB devices on the machine + """ + legacy_dir = self.gcodes_path.joinpath("USB") + if legacy_dir.is_dir() and not ( + legacy_dir.is_symlink() or legacy_dir.is_file() + ): + shutil.rmtree(legacy_dir) diff --git a/BlocksScreen/devices/storage/udisks2_dbus_async.py b/BlocksScreen/devices/storage/udisks2_dbus_async.py new file mode 100644 index 00000000..5f884c02 --- /dev/null +++ b/BlocksScreen/devices/storage/udisks2_dbus_async.py @@ -0,0 +1,568 @@ +# Sdbus Udisks2 Interface classes and manager +# +# Contains interface classes for async and blocking api of sdbus +# +# +# Hugo Costa hugo.santos.costa@gmail.com +import enum +import typing + +import sdbus + + +class Interfaces(enum.Enum): + Filesystem = "org.freedesktop.UDisks2.Filesystem" + Drive = "org.freedesktop.UDisks2.Drive" + Partition = "org.freedesktop.UDisks2.Partition" + Block = "org.freedesktop.UDisks2.Block" + PartitionTable = "org.freedesktop.UDisks2.PartitionTable" + + @classmethod + def has_value(cls, value) -> bool: + return value in (item.value for item in cls) + + +class UDisks2AsyncManager(sdbus.DbusObjectManagerInterfaceAsync): + """Subclassed async dbus object manager""" + + def __init__(self) -> None: + super().__init__() + + +class UDisks2PartitionTableAsyncInterface( + sdbus.DbusInterfaceCommonAsync, interface_name=Interfaces.PartitionTable.value +): + def __init__(self) -> None: + super().__init__() + + @sdbus.dbus_property_async(property_signature="ao") + def partitions(self) -> list[str]: + """Get list of object paths of the `org.freedesktop.Udisks2.Partitions` + + Returns: + list[str]: list of object paths + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def type(self) -> str: + """Get the type of partition table detected + + If blank the partition table was detected but it's unknown + Returns: + str: Known values ['dos', 'gpt', ''] + + """ + raise NotImplementedError + + +class UDisks2PartitionAsyncInterface( + sdbus.DbusInterfaceCommonAsync, interface_name=Interfaces.Partition.value +): + def __init__(self) -> None: + super().__init__() + + @sdbus.dbus_method_async(input_signature="s") + async def set_type(self, type: str) -> None: + """Set new partition type + + Args: + type (str): New partition type + """ + raise NotImplementedError + + @sdbus.dbus_method_async(input_signature="s") + async def set_name(self, name: str) -> None: + """Set partition name + + Args: + name (str): new partition name + """ + raise NotImplementedError + + @sdbus.dbus_method_async(input_signature="a{sv}") + async def delete(self, opts: dict[str, typing.Any]) -> None: + """Deletes the partition + + Args: + options (dict[str, any]) + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="u") + def number(self) -> int: + """Number of the partition on the partition table + + Returns: + number (int): partition number + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def type(self) -> str: + """Partition type + + Returns: + type (str): The partition type. For `dos` partition + tables this string is a hexadecimal code (0x83, 0xfd). + For `gpt` partition tables this is the UUID""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="t") + def flags(self) -> int: + """Flags describing the partition. + --------------- + For `dos` partitions: + - Bit 7 - The partition is marked as bootable + --------------- + For `gpt` partitions : + - Bit 0 - System Partition + - Bit 2 - Legacy BIOS bootable + - Bit 60 - Read-only + - Bit 62 - Hidden + - Bit 63 - Do not automount + + + Returns: + flags (int): current flags + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="t") + def offset(self) -> int: + """Offset of the partition in bytes + + Returns: + Offset (int): partition offset + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="t") + def size(self) -> int: + """Partition size, in bytes + + Returns: + Size (int): partition size + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def name(self) -> str: + """Partition name + Returns: + name (str): partition name + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def uuid(self) -> str: + """Partition UUID + Returns: + uuid (str): partition uuid + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="o") + def table(self) -> str: + """Object path of the `org.freedesktop.Udisks2.PartitionTable` object that + the partition belongs to. + + Returns: + table (str): path + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def is_container(self) -> bool: + """Set to True if the partition itself is a container for other partitions + + For example, for dos partition tables, this applies to so-called extended + partition (partitions of type 0x05, 0x0f or 0x85) containing so-called logical partitions. + + + Returns: + is_container (bool): if it is a container + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def is_contained(self) -> bool: + """Set to True if the partition is contained in another partition + Returns: + is_contained (bool): if it's contained + """ + raise NotImplementedError + + +class UDisks2FileSystemAsyncInterface( + sdbus.DbusInterfaceCommonAsync, interface_name=Interfaces.Filesystem.value +): + def __init__(self) -> None: + super().__init__() + + @sdbus.dbus_method_async(input_signature="a{sv}", result_signature="s") + async def mount(self, opts) -> str: + """Mounts the filesystem + + Args: + options dict[str, tuple[str, any]]: Options to mount the filesystem + + Returns: + path (str): mount path + """ + raise NotImplementedError + + @sdbus.dbus_method_async(input_signature="a{sv}") + async def unmount(self, opts) -> None: + """Unmount a mounted device + + Args: + options dict[str, any]: Known options (in addition to the standart options) include `force` (of type `b`) + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="t") + def size(self) -> int: + """Size of the filesystem. This is the amount + of bytes used on the block device representing an outer + filesystem boundary + + Returns: + size (int): Size of the filesystem + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="ayy") + def mount_points(self) -> list[bytes]: + """An array of filesystem paths for where the + file system on the device is mounted. If the + device is not mounted, this array will be empty + + Returns + mount_points (list[bytes]): Array of filesystem paths + """ + raise NotImplementedError + + +class UDisks2BlockAsyncInterface( + sdbus.DbusInterfaceCommonAsync, interface_name=Interfaces.Block.value +): + def __init__(self) -> None: + super().__init__() + + @sdbus.dbus_property_async(property_signature="s") + def hint_name(self) -> str: + """Hint name, if not blank, the name to + that presents the device + + Returns: + name (str): name of the device""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def hint_system(self) -> bool: + """If the device is considered a system device + True if it is. System devices are devices that + require additional permissions to access + + Returns + hint system (bool): If device is `system device`""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def hint_ignore(self) -> bool: + """If the device should be hidden from users + Returns + ignore (bool): True if the system should be ignored""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="ay") + def device(self) -> list[int] | bytes: + """Special device file for the block device + + Returns: + file path (list[int] | bytes): The file path""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="t") + def device_number(self) -> int: + """Device `dev_t` of the block device""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def id(self) -> str: + """Unique persistent identifier for the device + blank if no such identifier is available + + Returns: + id (str): unique identifier""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def id_label(self) -> str: + """Label for the filesystem or other structured + data on the block device. + If the property is blank there is no label or + it is unknown + + Returns: + label (str): filesystem label for the block""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def id_UUID(self) -> str: + """UUID of the filesystem or other structured + data on the block device. Do not make any + assumptions about the UUID as its format + depends on what kind of data is on the device + + Returns: + uuid (str): uuid""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def id_usage(self) -> str: + """Result of probing for signatures on the block device + Known values include + - filesystem -> Used for mountable filesystems + - crypto -> Used for e.g. LUKS devices + - raid -> Used for e.g. RAID members + - other -> Something else was detected + + ----- + If blank no known signature was detected. It doesn't + necessarily mean the device contains no structured data; + it only means that probing failed + + Returns: + usage (str): usage signature""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def id_type(self) -> str: + """Property that contains more information about + the probing result of the blocks device. It depends + on the IdUsage property + - filesystem -> The mountable file system that was detected (e.g. vfat). + - crypto -> Encrypted data. Known values include crypto_LUKS. + - raid -> RAID or similar. Known values include LVM2_member (for LVM2 components), linux_raid_member (for MD-RAID components.) + - other -> Something else. Known values include swap (for swap space), suspend (data used when resuming from suspend-to-disk). + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="ayy") + def symlinks(self) -> list[bytes]: + """Known symlinks in `/dev` that point to the device + in the file **Device** property. + + Returns: + symlinks (list[bytes]): available symlinks + """ + raise NotImplementedError + + @sdbus.dbus_method_async(input_signature="a{sv}") + async def rescan(self, opts: dict[str, typing.Any]) -> None: + """Request that the kernel and core OS rescans the + contents of the device and update their state to reflect + this + + Args: + options: unused + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="o") + def drive(self) -> str: + """The org.freedesktop.UDisks2.Drive object that the + block device belongs to, or '/' if no such object + exits + + Returns: + drive (str): path + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="a(sa{sv})") + def configuration(self) -> list[typing.Any]: + """The configuration for the device + This is an array of pairs (type, details), where `type` is + a string identifying the configuration source and the + `details` has the actual configuration data. + For entries of type `fstab` known configurations are: + - fsname (type 'ay') - The special device + - dir (type 'ay') - The mount point + - type (type 'ay') - The filesystem type + - opts (type 'ay') - Options + - freq (type 'i') - Dump frequency in days + - passno (type 'i') - Pass number of parallel fsck + For entries of type `crypttab` known configurations are: + - name (type 'ay') - The name to set the device up as + - device (type 'ay') - The special device + - passphrase-path (type 'ay') - Either empty to specify + that no password is set, otherwise a path to a file + containing the encryption password. This may also point + to a special device file in /dev such as /dev/random + - options (type 'ay') - Options + """ + raise NotImplementedError + + +class UDisks2DriveAsyncInterface( + sdbus.DbusInterfaceCommonAsync, interface_name=Interfaces.Drive.value +): + def __init__(self) -> None: + super().__init__() + + @sdbus.dbus_property_async(property_signature="s") + def revision(self) -> str: + """Firmware revision or blank if unknown + Returns: + revision (str): revision or blank + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def WWN(self) -> str: + """The World Wide Name of the drive or blank if unknown + Returns: + wwn (str) : wwn or none + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="a{sv}") + def configuration(self) -> dict[str, typing.Any]: + """Configuration directives applied to the drive when + its connected. + + Returns: + configurations (dict): applied configurations + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def can_power_off(self) -> bool: + """Whether the drive can be safely removed/powered off + + Returns: + can_power_off (bool): whether it can be removed or powered off""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def model(self) -> str: + """Name for the model of the drive + + Returns: + model (str): name of the model, blank if unknown""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def connection_bus(self) -> str: + """Physical connection bus for the drive, as seen + by the user + + Returns: + connection bus (str): physical connection bus ['usb', 'sdio', 'ieee1394'] + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def serial(self) -> str: + """Serial number + + Returns: + serial (str): serial number blank if unknown + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def ejectable(self) -> bool: + """Whether the media can be ejected from the drive of the + drive accepts the `eject` command to switch its state + so that the it displays 'Safe To Remove' + + *This is only a guess* + Returns: + ejectable (bool): can be ejected + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def removable(self) -> bool: + """Hint whether the drive and/or its media is considered + removable by the user. + + *This is only a guess* + + Returns: + removable (bool): whether the drive is considered removable + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="t") + def time_detected(self) -> int: + """The time the drive was first detected + + Returns: + time (int): time it was first detected + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def media_available(self) -> bool: + """This is always True if `MediaChangeDetected` is False + Returns: + media available (bool): True if media change detected is false + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def media_changed_detected(self) -> bool: + """Set to true only if media changes are detected + + Returns: + media change detected (bool): True if media changes are detected""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def media(self) -> str: + """The kind of media + + Returns: + media (str): The kind of media, blank if unknown""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def media_removable(self) -> bool: + """Whether the media can be removed from the drive + + Returns: + media_removable (bool): Whether it can be removed""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def id(self) -> str: + """Unique persistent identifier for the device or + blank if not available + + Returns: + id (str): Identifier e.g “ST32000542AS-6XW00W51”""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def vendor(self) -> str: + """Name for the vendor of the drive or blank if + unknown + + Returns: + vendor (str): Name of the vendor or blank""" + raise NotImplementedError + + @sdbus.dbus_method_async(input_signature="a{sv}") + async def eject(self, opts: dict[str, typing.Any]) -> None: + """Ejects the media from the drive + + Args: + options (dict): currently unused + """ + + raise NotImplementedError diff --git a/BlocksScreen/devices/storage/usb_controller.py b/BlocksScreen/devices/storage/usb_controller.py new file mode 100644 index 00000000..8094e8df --- /dev/null +++ b/BlocksScreen/devices/storage/usb_controller.py @@ -0,0 +1,144 @@ +import logging +import os +import typing +from PyQt6 import QtCore + +from .udisks2 import UDisksDBusAsync +from lib.panels.widgets.bannerPopup import BannerPopup + +ResType: typing.TypeAlias = typing.Literal["always", "none"] + + +class USBManager(QtCore.QObject): + usb_add: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, dict, name="usb-add" + ) + usb_rem: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, str, name="usb-rem" + ) + usb_hardware_detected: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="hardware-detected" + ) + usb_hardware_removed: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="hardware-removed" + ) + usb_monitor_started: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + name="usb-monitor-started" + ) + usb_monitor_finished: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + name="usb-monitor-finished" + ) + usb_mounted: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, str, name="device-mounted" + ) + + usb_unmounted: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="device-unmounted" + ) + + def __init__(self, parent: QtCore.QObject, gcodes_dir: str | None) -> None: + super().__init__(parent) + self.gcodes_dir: str = gcodes_dir or os.path.expanduser("~/printer_data/gcodes") + if not (os.path.isdir(self.gcodes_dir) and os.path.exists(self.gcodes_dir)): + logging.info("Provided gcodes directory does not exist.") + self.udisks: UDisksDBusAsync = UDisksDBusAsync( + parent=self, gcodes_dir=self.gcodes_dir + ) + # self.banner = BannerPopup(self) + self.banner = BannerPopup() + self._restart_type: ResType = "always" + self.udisks.start(self.udisks.Priority.InheritPriority) + self.udisks.hardware_detected.connect(self.handle_new_hardware) + self.udisks.hardware_detected.connect(self.usb_hardware_detected) + self.udisks.hardware_removed.connect(self.handle_rem_hardware) + self.udisks.hardware_removed.connect(self.usb_hardware_removed) + self.udisks.device_added.connect(self.handle_new_device) + self.udisks.device_added.connect(self.usb_add) + self.udisks.device_removed.connect(self.handle_rem_device) + self.udisks.device_removed.connect(self.usb_rem) + self.udisks.device_mounted.connect(self.handle_mounted_device) + self.udisks.device_mounted.connect(self.usb_mounted) + self.udisks.device_unmounted.connect(self.handle_unmounted_device) + self.udisks.device_unmounted.connect(self.usb_unmounted) + self.udisks.started.connect(self.usb_monitor_started) + self.udisks.finished.connect(self.usb_monitor_finished) + self.need_restart: bool = False + self.udisks.finished.connect(self._handle_full_restart) + if self.restart_type == "always": + self.udisks.finished.connect(self._handle_monitor_finished) + + def restart(self) -> None: + """Restart usb monitoring tool""" + if not self.udisks.active: + self.udisks.start(self.udisks.Priority.InheritPriority) + return + self.udisks.close() + self.need_restart = True + + def close(self) -> None: + """Close usb monitoring tool""" + self.udisks.close() + self.deleteLater() + + def _handle_full_restart(self) -> None: + if self.need_restart: + self.udisks.start(self.udisks.Priority.InheritPriority) + self.need_restart = False + + @property + def restart_type(self) -> ResType: + return self._restart_type + + @restart_type.setter + def restart_type(self, type: ResType) -> None: + """Tool restart type, currently there are only two + options available. + + - `always` - restarts the tool every time it stops + - `none` - doesn't restart the tool at all + """ + if type not in ("always", "none"): + logging.info("Unknown restart type %s", (type,)) + if type == "always": + if not self._restart_type == "always": + self.udisks.finished.connect(self._handle_monitor_finished) + else: + try: + self.udisks.finished.disconnect(self._handle_monitor_finished) + except TypeError: + pass + self._restart_type = type + + @QtCore.pyqtSlot(name="monitor-finished") + def _handle_monitor_finished(self) -> None: + # Just restart the monitor for now + self.restart() + + @QtCore.pyqtSlot(str, str, name="device-mounted") + def handle_mounted_device(self, path, symlink) -> None: + """Handle new mounted device""" + pass + + @QtCore.pyqtSlot(str, name="device-unmounted") + def handle_unmounted_device(self, path) -> None: + pass + + @QtCore.pyqtSlot(str, dict, name="device-added") + def handle_new_device(self, path, interface) -> None: + """Handle new device""" + pass + + @QtCore.pyqtSlot(str, name="device-removed") + def handle_rem_device(self, path) -> None: + """Handle device removed""" + pass + + @QtCore.pyqtSlot(str, name="hardware_detected") + def handle_new_hardware(self, path: str) -> None: + """Handle new usb device hardware""" + self.banner.new_message(self.banner.MessageType.CONNECT) + + @QtCore.pyqtSlot(str, name="hardware_removed") + def handle_rem_hardware(self, path: str) -> None: + """Handle usb device hardware removed""" + self.banner.new_message(self.banner.MessageType.DISCONNECT) diff --git a/BlocksScreen/helper_methods.py b/BlocksScreen/helper_methods.py index b4dafc0f..25b76cac 100644 --- a/BlocksScreen/helper_methods.py +++ b/BlocksScreen/helper_methods.py @@ -14,6 +14,8 @@ import struct import typing +logger = logging.getLogger(__name__) + try: ctypes.cdll.LoadLibrary("libXext.so.6") libxext = ctypes.CDLL("libXext.so.6") @@ -220,9 +222,9 @@ def disable_dpms() -> None: set_dpms_mode(DPMSState.OFF) except OSError as e: - logging.exception(f"OSError couldn't load DPMS library: {e}") + logger.exception(f"OSError couldn't load DPMS library: {e}") except Exception as e: - logging.exception(f"Unexpected exception occurred {e}") + logger.exception(f"Unexpected exception occurred {e}") def convert_bytes_to_mb(self, bytes: int | float) -> float: diff --git a/BlocksScreen/lib/files.py b/BlocksScreen/lib/files.py index 0eda561d..412f0648 100644 --- a/BlocksScreen/lib/files.py +++ b/BlocksScreen/lib/files.py @@ -1,180 +1,687 @@ -# -# Gcode File manager -# from __future__ import annotations -import os +import logging import typing +from collections import deque +from dataclasses import dataclass, field +from enum import Enum, auto +from pathlib import Path import events from events import ReceivedFileData from lib.moonrakerComm import MoonWebSocket from PyQt6 import QtCore, QtGui, QtWidgets +logger = logging.getLogger(__name__) + + +class FileAction(Enum): + """Enumeration of possible file actions from Moonraker notifications.""" + + CREATE_FILE = auto() + DELETE_FILE = auto() + MOVE_FILE = auto() + MODIFY_FILE = auto() + CREATE_DIR = auto() + DELETE_DIR = auto() + MOVE_DIR = auto() + ROOT_UPDATE = auto() + UNKNOWN = auto() + + @classmethod + def from_string(cls, action: str) -> "FileAction": + """Convert Moonraker action string to enum.""" + mapping = { + "create_file": cls.CREATE_FILE, + "delete_file": cls.DELETE_FILE, + "move_file": cls.MOVE_FILE, + "modify_file": cls.MODIFY_FILE, + "create_dir": cls.CREATE_DIR, + "delete_dir": cls.DELETE_DIR, + "move_dir": cls.MOVE_DIR, + "root_update": cls.ROOT_UPDATE, + } + return mapping.get(action.lower(), cls.UNKNOWN) + + +@dataclass +class FileMetadata: + """ + Data class for file metadata. + + Thumbnails are stored as QImage objects when available. + """ + + filename: str = "" + thumbnail_images: list[QtGui.QImage] = field(default_factory=list) + filament_total: typing.Union[dict, str, float] = field(default_factory=dict) + estimated_time: int = 0 + layer_count: int = -1 + total_layer: int = -1 + object_height: float = -1.0 + size: int = 0 + modified: float = 0.0 + filament_type: str = "Unknown" + filament_weight_total: float = -1.0 + layer_height: float = -1.0 + first_layer_height: float = -1.0 + first_layer_extruder_temp: float = -1.0 + first_layer_bed_temp: float = -1.0 + chamber_temp: float = -1.0 + filament_name: str = "Unknown" + nozzle_diameter: float = -1.0 + slicer: str = "Unknown" + 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 + + def to_dict(self) -> dict: + """Convert to dictionary for signal emission.""" + return { + "filename": self.filename, + "thumbnail_images": self.thumbnail_images, + "filament_total": self.filament_total, + "estimated_time": self.estimated_time, + "layer_count": self.layer_count, + "total_layer": self.total_layer, + "object_height": self.object_height, + "size": self.size, + "modified": self.modified, + "filament_type": self.filament_type, + "filament_weight_total": self.filament_weight_total, + "layer_height": self.layer_height, + "first_layer_height": self.first_layer_height, + "first_layer_extruder_temp": self.first_layer_extruder_temp, + "first_layer_bed_temp": self.first_layer_bed_temp, + "chamber_temp": self.chamber_temp, + "filament_name": self.filament_name, + "nozzle_diameter": self.nozzle_diameter, + "slicer": self.slicer, + "slicer_version": self.slicer_version, + "gcode_start_byte": self.gcode_start_byte, + "gcode_end_byte": self.gcode_end_byte, + "print_start_time": self.print_start_time, + "job_id": self.job_id, + } + + @classmethod + def from_dict( + cls, data: dict, thumbnail_images: list[QtGui.QImage] + ) -> "FileMetadata": + """ + `Create FileMetadata from Moonraker API response.` + + All data comes directly from Moonraker - no local filesystem access. + """ + filename = data.get("filename", "") + + # Helper to safely get values with fallback + def safe_get(key: str, default: typing.Any) -> typing.Any: + value = data.get(key, default) + if value is None or value == -1.0: + return default + return value + + return cls( + filename=filename, + thumbnail_images=thumbnail_images, + filament_total=safe_get("filament_total", {}), + estimated_time=int(safe_get("estimated_time", 0)), + layer_count=safe_get("layer_count", -1), + total_layer=safe_get("total_layer", -1), + object_height=safe_get("object_height", -1.0), + size=safe_get("size", 0), + modified=safe_get("modified", 0.0), + filament_type=safe_get("filament_type", "Unknown") or "Unknown", + filament_weight_total=safe_get("filament_weight_total", -1.0), + layer_height=safe_get("layer_height", -1.0), + first_layer_height=safe_get("first_layer_height", -1.0), + first_layer_extruder_temp=safe_get("first_layer_extruder_temp", -1.0), + first_layer_bed_temp=safe_get("first_layer_bed_temp", -1.0), + chamber_temp=safe_get("chamber_temp", -1.0), + filament_name=safe_get("filament_name", "Unknown") or "Unknown", + nozzle_diameter=safe_get("nozzle_diameter", -1.0), + slicer=safe_get("slicer", "Unknown") or "Unknown", + slicer_version=safe_get("slicer_version", "Unknown") or "Unknown", + gcode_start_byte=safe_get("gcode_start_byte", 0), + gcode_end_byte=safe_get("gcode_end_byte", 0), + print_start_time=data.get("print_start_time"), + job_id=data.get("job_id"), + ) + class Files(QtCore.QObject): - request_file_list = QtCore.pyqtSignal([], [str], name="api-get-files-list") + """ + Manages gcode files with event-driven updates. + E + Signals emitted: + - on_dirs: Full directory list + - on_file_list: Full file list + - fileinfo: Single file metadata update + - file_added/removed/modified: Incremental updates + - dir_added/removed: Directory updates + - full_refresh_needed: Root changed + """ + + # Signals for API requests + request_file_list = QtCore.pyqtSignal([], [str], name="api_get_files_list") request_dir_info = QtCore.pyqtSignal( - [], [str], [str, bool], name="api-get-dir-info" - ) - request_file_metadata = QtCore.pyqtSignal([str], name="get_file_metadata") - request_files_thumbnails = QtCore.pyqtSignal([str], name="request_files_thumbnail") - request_file_download = QtCore.pyqtSignal([str, str], name="file_download") - on_dirs: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - list, name="on-dirs" - ) - on_file_list: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - list, name="on_file_list" - ) - fileinfo: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - dict, name="fileinfo" + [], [str], [str, bool], name="api_get_dir_info" ) + request_file_metadata = QtCore.pyqtSignal(str, name="get_file_metadata") - def __init__( - self, - parent: QtCore.QObject, - ws: MoonWebSocket, - update_interval: int = 5000, - ) -> None: - super(Files, self).__init__(parent) + # Signals for UI updates + on_dirs = QtCore.pyqtSignal(list, name="on_dirs") + on_file_list = QtCore.pyqtSignal(list, name="on_file_list") + fileinfo = QtCore.pyqtSignal(dict, name="fileinfo") + metadata_error = QtCore.pyqtSignal( + str, name="metadata_error" + ) # filename when metadata fails + + # Signals for incremental updates + file_added = QtCore.pyqtSignal(dict, name="file_added") + file_removed = QtCore.pyqtSignal(str, name="file_removed") + file_modified = QtCore.pyqtSignal(dict, name="file_modified") + dir_added = QtCore.pyqtSignal(dict, name="dir_added") + dir_removed = QtCore.pyqtSignal(str, name="dir_removed") + full_refresh_needed = QtCore.pyqtSignal(name="full_refresh_needed") + + # Signal for preloaded USB files + usb_files_loaded = QtCore.pyqtSignal( + str, list, name="usb_files_loaded" + ) # (usb_path, files) + GCODE_EXTENSION = ".gcode" + GCODE_PATH = "~/printer_data/gcodes" + + def __init__(self, parent: QtCore.QObject, ws: MoonWebSocket) -> None: + super().__init__(parent) self.ws = ws - self.gcode_path = os.path.expanduser("~/printer_data/gcodes") - self.files: list = [] - self.directories: list = [] - self.files_metadata: dict = {} - self.request_file_list.connect(slot=self.ws.api.get_file_list) - self.request_file_list[str].connect(slot=self.ws.api.get_file_list) - self.request_dir_info.connect(slot=self.ws.api.get_dir_information) + + # Internal state + self._files: dict[str, dict] = {} + self._directories: dict[str, dict] = {} + self._files_metadata: dict[str, FileMetadata] = {} + self._current_directory: str = "" + self._initial_load_complete: bool = False + self.gcode_path = Path(self.GCODE_PATH).expanduser() + # USB preloaded files cache: usb_path -> list of files + self._usb_files_cache: dict[str, list[dict]] = {} + # Track pending USB preload requests (ordered FIFO queue) + self._pending_usb_preloads: set[str] = set() + self._usb_preload_queue: deque[str] = deque() + + self._connect_signals() + self._install_event_filter() + + def _connect_signals(self) -> None: + """Connect internal signals to websocket API.""" + self.request_file_list.connect(self.ws.api.get_file_list) + self.request_file_list[str].connect(self.ws.api.get_file_list) + self.request_dir_info.connect(self.ws.api.get_dir_information) self.request_dir_info[str, bool].connect(self.ws.api.get_dir_information) - self.request_dir_info[str].connect(slot=self.ws.api.get_dir_information) - self.request_file_metadata.connect(slot=self.ws.api.get_gcode_metadata) - self.request_files_thumbnails.connect(slot=self.ws.api.get_gcode_thumbnail) - self.request_file_download.connect(slot=self.ws.api.download_file) - QtWidgets.QApplication.instance().installEventFilter(self) # type: ignore + self.request_dir_info[str].connect(self.ws.api.get_dir_information) + self.request_file_metadata.connect(self.ws.api.get_gcode_metadata) + + def _install_event_filter(self) -> None: + """Install event filter on application instance.""" + app = QtWidgets.QApplication.instance() + if app: + app.installEventFilter(self) + + @property + def file_list(self) -> list[dict]: + """Get list of files in current directory.""" + return list(self._files.values()) + + @property + def directories(self) -> list[dict]: + """Get list of directories in current directory.""" + return list(self._directories.values()) @property - def file_list(self): - """Available files list""" - return self.files + def current_directory(self) -> str: + """Get current directory path.""" + return self._current_directory + + @current_directory.setter + def current_directory(self, value: str) -> None: + """Set current directory path.""" + self._current_directory = value + + @property + 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]: + """Get cached metadata for a file.""" + return self._files_metadata.get(filename.removeprefix("/")) + + def get_file_data(self, filename: str) -> dict: + """Get cached file data dict for a file.""" + clean_name = filename.removeprefix("/") + metadata = self._files_metadata.get(clean_name) + if metadata: + return metadata.to_dict() + return {} + + def refresh_directory(self, directory: str = "") -> None: + """Force refresh of a specific directory.""" + logger.debug(f"Refreshing directory: {directory or 'root'}") + self._current_directory = directory + self.request_dir_info[str, bool].emit(directory, True) + + def initial_load(self) -> None: + """Perform initial load of file list.""" + logger.info("Performing initial file list load") + self._initial_load_complete = False + self.request_dir_info[str, bool].emit("", True) + + def handle_filelist_changed(self, data: typing.Union[dict, list]) -> None: + """Handle notify_filelist_changed from Moonraker.""" + if isinstance(data, dict) and "params" in data: + data = data.get("params", []) + + if isinstance(data, list): + if len(data) > 0: + data = data[0] + else: + return + + if not isinstance(data, dict): + return + + action_str = data.get("action", "") + action = FileAction.from_string(action_str) + item = data.get("item", {}) + source_item = data.get("source_item", {}) + + logger.debug(f"File list changed: action={action_str}, item={item}") + + handlers = { + FileAction.CREATE_FILE: self._handle_file_created, + FileAction.DELETE_FILE: self._handle_file_deleted, + FileAction.MODIFY_FILE: self._handle_file_modified, + FileAction.MOVE_FILE: self._handle_file_moved, + FileAction.CREATE_DIR: self._handle_dir_created, + FileAction.DELETE_DIR: self._handle_dir_deleted, + FileAction.MOVE_DIR: self._handle_dir_moved, + FileAction.ROOT_UPDATE: self._handle_root_update, + } + + handler = handlers.get(action) + if handler: + handler(item, source_item) + + def _handle_file_created(self, item: dict, _: dict) -> None: + """Handle new file creation.""" + path = item.get("path", "") + if not path: + return + + if self._is_usb_mount(path): + item["dirname"] = path + self._handle_dir_created(item, {}) + return + + if not path.lower().endswith(self.GCODE_EXTENSION): + return + + self._files[path] = item + self.file_added.emit(item) + + # Request metadata (will update later) + self.request_file_metadata.emit(path.removeprefix("/")) + logger.info(f"File created: {path}") + + def _handle_file_deleted(self, item: dict, _: dict) -> None: + """Handle file deletion.""" + path = item.get("path", "") + if not path: + return + + if self._is_usb_mount(path): + item["dirname"] = path + self._handle_dir_deleted(item, {}) + return + + self._files.pop(path, None) + self._files_metadata.pop(path.removeprefix("/"), None) + + self.file_removed.emit(path) + logger.info(f"File deleted: {path}") + + def _handle_file_modified(self, item: dict, _: dict) -> None: + """Handle file modification.""" + path = item.get("path", "") + if not path or not path.lower().endswith(self.GCODE_EXTENSION): + return + + self._files[path] = item + self._files_metadata.pop(path.removeprefix("/"), None) + + self.request_file_metadata.emit(path.removeprefix("/")) + self.file_modified.emit(item) + logger.info(f"File modified: {path}") + + def _handle_file_moved(self, item: dict, source_item: dict) -> None: + """Handle file move/rename.""" + old_path = source_item.get("path", "") + new_path = item.get("path", "") + + if old_path: + self._handle_file_deleted(source_item, {}) + if new_path: + self._handle_file_created(item, {}) + + def _handle_dir_created(self, item: dict, _: dict) -> None: + """Handle directory creation.""" + path = item.get("path", "") + dirname = item.get("dirname", "") + + if not dirname and path: + dirname = path.rstrip("/").split("/")[-1] + + if not dirname or dirname.startswith("."): + return + + item["dirname"] = dirname + self._directories[dirname] = item + self.dir_added.emit(item) + logger.info(f"Directory created: {dirname}") + + if self._is_usb_mount(dirname): + self._preload_usb_contents(dirname) + + def _handle_dir_deleted(self, item: dict, _: dict) -> None: + """Handle directory deletion.""" + path = item.get("path", "") + dirname = item.get("dirname", "") + + if not dirname and path: + dirname = path.rstrip("/").split("/")[-1] - def handle_message_received(self, method: str, data, params: dict) -> None: - """Handle file related messages received by moonraker""" + if not dirname: + return + + self._directories.pop(dirname, None) + + # Clear USB cache if this was a USB mount + if self._is_usb_mount(dirname): + self._usb_files_cache.pop(dirname, None) + self._pending_usb_preloads.discard(dirname) + if dirname in self._usb_preload_queue: + self._usb_preload_queue.remove(dirname) + logger.info(f"Cleared USB cache for: {dirname}") + + self.dir_removed.emit(dirname) + logger.info(f"Directory deleted: {dirname}") + + def _handle_dir_moved(self, item: dict, source_item: dict) -> None: + """Handle directory move/rename.""" + self._handle_dir_deleted(source_item, {}) + self._handle_dir_created(item, {}) + + def _handle_root_update(self, _: dict, __: dict) -> None: + """Handle root update.""" + logger.info("Root update detected, requesting full refresh") + self.full_refresh_needed.emit() + self.initial_load() + + @staticmethod + def _is_usb_mount(path: str) -> bool: + """Check if a path is a USB mount point.""" + path = path.removeprefix("/") + return "/" not in path and path.startswith("USB-") + + def handle_message_received( + self, method: str, data: typing.Any, params: dict + ) -> None: + """Handle file-related messages received from Moonraker.""" if "server.files.list" in method: - self.files.clear() - self.files = data - [self.request_file_metadata.emit(item["path"]) for item in self.files] + self._process_file_list(data) elif "server.files.metadata" in method: - if data["filename"] in self.files_metadata.keys(): - if not data.get("filename", None): - return - self.files_metadata.update({data["filename"]: data}) - else: - self.files_metadata[data["filename"]] = data + self._process_metadata(data) elif "server.files.get_directory" in method: - self.directories = data.get("dirs", {}) - self.files.clear() - self.files = data.get("files", []) - self.on_file_list[list].emit(self.files) - self.on_dirs[list].emit(self.directories) + self._process_directory_info(data) - @QtCore.pyqtSlot(str, str, name="on_request_delete_file") - def on_request_delete_file(self, filename: str, directory: str = "gcodes") -> None: - """Requests deletion of a file + def _process_file_list(self, data: list) -> None: + """Process full file list response.""" + self._files.clear() + + for item in data: + path = item.get("path", item.get("filename", "")) + if path: + self._files[path] = item + + self._initial_load_complete = True + self.on_file_list.emit(self.file_list) + logger.info(f"Loaded {len(self._files)} files") + # Request metadata only for gcode files (async update) + for path in self._files: + if path.lower().endswith(self.GCODE_EXTENSION): + self.request_file_metadata.emit(path.removeprefix("/")) + + def _process_metadata(self, data: dict) -> None: + """Process file metadata response.""" + filename = data.get("filename") + if not filename: + return + + thumbnails = data.get("thumbnails", []) + base_dir = (self.gcode_path / filename).parent + thumbnail_paths = [ + str(base_dir / t.get("relative_path", "")) + for t in thumbnails + if isinstance(t.get("relative_path", None), str) and t["relative_path"] + ] + + # Load images, filtering out invalid files + thumbnail_images = [] + for path in thumbnail_paths: + image = QtGui.QImage(path) + if not image.isNull(): # skip loading errors + thumbnail_images.append(image) + + metadata = FileMetadata.from_dict(data, thumbnail_images) + self._files_metadata[filename] = metadata + + # Emit updated fileinfo + 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: + """ + Handle metadata request error from Moonraker. + + Parses the filename from the error message and emits metadata_error signal. + Called directly from MainWindow error handler. Args: - filename (str): file to delete - directory (str): root directory where the file is located + error_data: The error message string or dict from Moonraker """ - if not directory: - self.ws.api.delete_file(filename) + if not error_data: return - self.ws.api.delete_file(filename, directory) # Use the root directory 'gcodes' - @QtCore.pyqtSlot(str, name="on_request_fileinfo") - def on_request_fileinfo(self, filename: str) -> None: - """Requests metadata for a file + if isinstance(error_data, dict): + text = error_data.get("message", str(error_data)) + else: + text = str(error_data) + + if "metadata" not in text.lower(): + return + + # Parse filename from error message (format: ) + start = text.find("<") + 1 + end = text.find(">", start) + + if start > 0 and end > start: + filename = text[start:end] + clean_filename = filename.removeprefix("/") + self.metadata_error.emit(clean_filename) + logger.debug(f"Metadata error for: {clean_filename}") + + def _preload_usb_contents(self, usb_path: str) -> None: + """ + Preload USB contents when USB is inserted. + + Requests directory info for the USB mount so files are ready + when user navigates to it. Args: - filename (str): file to get metadata from + usb_path: The USB mount path (e.g., "USB-sda1") """ - _data: dict = { - "thumbnail_images": list, - "filament_total": dict, - "estimated_time": int, - "layer_count": int, - "object_height": float, - "size": int, - "filament_type": str, - "filament_weight_total": float, - "layer_height": float, - "first_layer_height": float, - "first_layer_extruder_temp": float, - "first_layer_bed_temp": float, - "chamber_temp": float, - "filament_name": str, - "nozzle_diameter": float, - "slicer": str, - "filename": str, - } - _file_metadata = self.files_metadata.get(str(filename), {}) - _data.update({"filename": filename}) - _thumbnails = _file_metadata.get("thumbnails", {}) - _thumbnail_paths = list( - map( - lambda thumbnail_path: os.path.join( - os.path.dirname(os.path.join(self.gcode_path, filename)), - thumbnail_path.get("relative_path", "?"), - ), - _thumbnails, - ) - ) - _thumbnail_images = list(map(lambda path: QtGui.QImage(path), _thumbnail_paths)) - _data.update({"thumbnail_images": _thumbnail_images}) - _data.update({"filament_total": _file_metadata.get("filament_total", "?")}) - _data.update({"estimated_time": _file_metadata.get("estimated_time", 0)}) - _data.update({"layer_count": _file_metadata.get("layer_count", -1.0)}) - _data.update({"total_layer": _file_metadata.get("total_layer", -1.0)}) - _data.update({"object_height": _file_metadata.get("object_height", -1.0)}) - _data.update({"nozzle_diameter": _file_metadata.get("nozzle_diameter", -1.0)}) - _data.update({"layer_height": _file_metadata.get("layer_height", -1.0)}) - _data.update( - {"first_layer_height": _file_metadata.get("first_layer_height", -1.0)} - ) - _data.update( - { - "first_layer_extruder_temp": _file_metadata.get( - "first_layer_extruder_temp", -1.0 - ) - } - ) - _data.update( - {"first_layer_bed_temp": _file_metadata.get("first_layer_bed_temp", -1.0)} - ) - _data.update({"chamber_temp": _file_metadata.get("chamber_temp", -1.0)}) - _data.update({"filament_name": _file_metadata.get("filament_name", -1.0)}) - _data.update({"filament_type": _file_metadata.get("filament_type", -1.0)}) - _data.update( - {"filament_weight_total": _file_metadata.get("filament_weight_total", -1.0)} + logger.info(f"Preloading USB contents: {usb_path}") + self._pending_usb_preloads.add(usb_path) + 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]]: + """ + Get cached files for a USB path if available. + + Args: + usb_path: The USB mount path + + Returns: + List of file dicts if cached, None otherwise + """ + return self._usb_files_cache.get(usb_path.removeprefix("/")) + + def _process_usb_directory_info(self, usb_path: str, data: dict) -> None: + """ + Process preloaded USB directory info. + + Caches the files and requests metadata for gcode files. + + Args: + usb_path: The USB mount path + data: Directory info response from Moonraker + """ + files = [] + for file_data in data.get("files", []): + filename = file_data.get("filename", file_data.get("path", "")) + if filename: + files.append(file_data) + + full_path = f"{usb_path}/{filename}" + if filename.lower().endswith(self.GCODE_EXTENSION): + self.request_file_metadata.emit(full_path) + + # Cache the files + self._usb_files_cache[usb_path] = files + self.usb_files_loaded.emit(usb_path, files) + logger.info(f"Preloaded {len(files)} files from USB: {usb_path}") + + def _process_directory_info(self, data: dict) -> None: + """Process directory info response.""" + # Check if this is a USB preload response. + # Match by FIFO queue — Moonraker responds to get_dir_information in order. + matched_usb = None + + if self._usb_preload_queue: + candidate = self._usb_preload_queue.popleft() + if candidate in self._pending_usb_preloads: + matched_usb = candidate + + if matched_usb: + self._pending_usb_preloads.discard(matched_usb) + self._process_usb_directory_info(matched_usb, data) + return + + self._directories.clear() + self._files.clear() + + for dir_data in data.get("dirs", []): + dirname = dir_data.get("dirname", "") + if dirname and not dirname.startswith("."): + self._directories[dirname] = dir_data + + for file_data in data.get("files", []): + filename = file_data.get("filename", file_data.get("path", "")) + if filename: + self._files[filename] = file_data + + self.on_file_list.emit(self.file_list) + self.on_dirs.emit(self.directories) + self._initial_load_complete = True + + logger.info( + f"Directory loaded: {len(self._directories)} dirs, {len(self._files)} files" ) - _data.update({"slicer": _file_metadata.get("slicer", -1.0)}) - self.fileinfo.emit(_data) - - def eventFilter(self, a0: QtCore.QObject, a1: QtCore.QEvent) -> bool: - """Handle Websocket and Klippy events""" - if a1.type() == events.WebSocketOpen.type(): - self.request_file_list.emit() - self.request_dir_info[str, bool].emit("", False) + + # Request metadata only for gcode files (async update) + for filename in self._files: + if filename.lower().endswith(self.GCODE_EXTENSION): + self.request_file_metadata.emit(filename.removeprefix("/")) + + @QtCore.pyqtSlot(str, str, name="on_request_delete_file") + def on_request_delete_file(self, filename: str, directory: str = "gcodes") -> None: + """Request deletion of a file.""" + if not filename: + return + + if directory: + self.ws.api.delete_file(filename, directory) + else: + self.ws.api.delete_file(filename) + + logger.info(f"Requested deletion of: {filename}") + + @QtCore.pyqtSlot(str, name="on_request_fileinfo") + def on_request_fileinfo(self, filename: str) -> None: + """Request and emit metadata for a file.""" + clean_filename = filename.removeprefix("/") + cached = self._files_metadata.get(clean_filename) + + if cached: + self.fileinfo.emit(cached.to_dict()) + else: + self.request_file_metadata.emit(clean_filename) + + @QtCore.pyqtSlot(name="get_dir_info") + @QtCore.pyqtSlot(str, name="get_dir_info") + @QtCore.pyqtSlot(str, bool, name="get_dir_info") + def get_dir_information( + self, directory: str = "", extended: bool = True + ) -> typing.Optional[list]: + """Get directory information.""" + self._current_directory = directory + + if not extended and self._initial_load_complete: + return self.directories + + return self.ws.api.get_dir_information(directory, extended) + + def eventFilter(self, obj: QtCore.QObject, event: QtCore.QEvent) -> bool: + """Handle application-level events.""" + if event.type() == events.WebSocketOpen.type(): + self.initial_load() return False - if a1.type() == events.KlippyDisconnected.type(): - self.files_metadata.clear() - self.files.clear() + + if event.type() == events.KlippyDisconnected.type(): + self._clear_all_data() return False - return super().eventFilter(a0, a1) - def event(self, a0: QtCore.QEvent) -> bool: - """Filter ReceivedFileData event""" - if a0.type() == ReceivedFileData.type(): - if isinstance(a0, ReceivedFileData): - self.handle_message_received(a0.method, a0.data, a0.params) + return super().eventFilter(obj, event) + + def event(self, event: QtCore.QEvent) -> bool: + """Handle object-level events.""" + if event.type() == ReceivedFileData.type(): + if isinstance(event, ReceivedFileData): + self.handle_message_received(event.method, event.data, event.params) return True - return super().event(a0) + return super().event(event) + + def _clear_all_data(self) -> None: + """Clear all cached data.""" + self._files.clear() + self._directories.clear() + self._files_metadata.clear() + self._usb_files_cache.clear() + self._pending_usb_preloads.clear() + self._usb_preload_queue.clear() + self._initial_load_complete = False + logger.info("All file data cleared") diff --git a/BlocksScreen/lib/machine.py b/BlocksScreen/lib/machine.py index e1c4a0ca..69938e79 100644 --- a/BlocksScreen/lib/machine.py +++ b/BlocksScreen/lib/machine.py @@ -8,6 +8,8 @@ from PyQt6 import QtCore +logger = logging.getLogger(__name__) + class MachineControl(QtCore.QObject): service_restart = QtCore.pyqtSignal(str, name="service-restart") @@ -67,10 +69,10 @@ def _run_command(self, command: str): ) return p.stdout.strip() + "\n" + p.stderr.strip() except ValueError as e: - logging.error("Failed to parse command string '%s': '%s'", command, e) + logger.error("Failed to parse command string '%s': '%s'", command, e) raise RuntimeError(f"Invalid command format: {e}") from e except subprocess.CalledProcessError as e: - logging.error( + logger.error( "Caught exception (exit code %d) failed to run command: %s \nStderr: %s", e.returncode, command, @@ -82,4 +84,4 @@ def _run_command(self, command: str): subprocess.TimeoutExpired, FileNotFoundError, ): - logging.error("Caught exception failed to run command %s", command) + logger.error("Caught exception failed to run command %s", command) diff --git a/BlocksScreen/lib/moonrakerComm.py b/BlocksScreen/lib/moonrakerComm.py index ba298ba7..5f889d9f 100644 --- a/BlocksScreen/lib/moonrakerComm.py +++ b/BlocksScreen/lib/moonrakerComm.py @@ -14,7 +14,7 @@ from lib.utils.RepeatedTimer import RepeatedTimer from PyQt6 import QtCore, QtWidgets -_logger = logging.getLogger(name="logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class OneShotTokenError(Exception): @@ -67,7 +67,7 @@ def __init__(self, parent: QtCore.QObject) -> None: ) self.klippy_state_signal.connect(self.api.request_printer_info) - _logger.info("Websocket object initialized") + logger.info("Websocket object initialized") @QtCore.pyqtSlot(name="retry_wb_conn") def retry_wb_conn(self): @@ -102,10 +102,10 @@ def reconnect(self): else: raise TypeError("QApplication.instance expected ad non-None value") except Exception as e: - _logger.error( + logger.error( f"Error on sending Event {unable_to_connect_event.__class__.__name__} | Error message: {e}" ) - _logger.info( + logger.info( "Maximum number of connection retries reached, Unable to establish connection with Moonraker" ) return False @@ -114,11 +114,11 @@ def reconnect(self): def connect(self) -> bool: """Connect to websocket""" if self.connected: - _logger.info("Connection established") + logger.info("Connection established") return True self._reconnect_count += 1 self.connecting_signal[int].emit(int(self._reconnect_count)) - _logger.debug( + logger.debug( f"Establishing connection to Moonraker...\n Try number {self._reconnect_count}" ) # TODO Handle if i cannot connect to moonraker, request server.info and see if i get a result @@ -127,7 +127,7 @@ def connect(self) -> bool: if _oneshot_token is None: raise OneShotTokenError("Unable to retrieve oneshot token") except Exception as e: - _logger.info( + logger.info( f"Unexpected error occurred when trying to acquire oneshot token: {e}" ) return False @@ -148,11 +148,11 @@ def connect(self) -> bool: daemon=True, ) try: - _logger.info("Websocket Start...") - _logger.debug(self.ws.url) + logger.info("Websocket Start...") + 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(f"Unexpected while starting websocket {self._wst.name}: {e}") return False return True @@ -162,14 +162,14 @@ def wb_disconnect(self) -> None: self.ws.close() if self._wst.is_alive(): self._wst.join() - _logger.info("Websocket closed") + 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}") + logger.info(f"Websocket error, disconnected: {_error}") self.connected = False self.disconnected = True @@ -199,11 +199,11 @@ def on_close(self, *args) -> None: else: raise TypeError("QApplication.instance expected non None value") except Exception as e: - _logger.info( + logger.info( f"Unexpected error when sending websocket close_event on disconnection: {e}" ) - _logger.info( + logger.info( f"Websocket closed, code: {_close_status_code}, message: {_close_message}" ) @@ -231,11 +231,11 @@ 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(f"Unexpected error opening websocket: {e}") self.connected_signal.emit() self._retry_timer.stopTimer() - _logger.info(f"Connection to websocket achieved on {_ws}") + logger.info(f"Connection to websocket achieved on {_ws}") def on_message(self, *args) -> None: """Websocket on message callback @@ -300,9 +300,7 @@ 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(f"Unexpected error while creating websocket message event: {e}") def send_request(self, method: str, params: dict = {}) -> bool: """Send a request over the websocket diff --git a/BlocksScreen/lib/moonrest.py b/BlocksScreen/lib/moonrest.py index 1e43552a..2c663531 100644 --- a/BlocksScreen/lib/moonrest.py +++ b/BlocksScreen/lib/moonrest.py @@ -31,6 +31,8 @@ import requests from requests import Request, Response +logger = logging.getLogger(__name__) + class UncallableError(Exception): """Raised when a method is not callable""" @@ -145,4 +147,4 @@ def _request( return response.json() if json_response else response.content except Exception as e: - logging.info(f"Unexpected error while sending HTTP request: {e}") + logger.info(f"Unexpected error while sending HTTP request: {e}") diff --git a/BlocksScreen/lib/network.py b/BlocksScreen/lib/network.py deleted file mode 100644 index 61ea4078..00000000 --- a/BlocksScreen/lib/network.py +++ /dev/null @@ -1,1509 +0,0 @@ -import asyncio -import enum -import logging -import threading -import typing -from uuid import uuid4 - -import sdbus -from PyQt6 import QtCore -from sdbus_async import networkmanager as dbusNm - -logger = logging.getLogger("logs/BlocksScreen.log") - - -class NetworkManagerRescanError(Exception): - """Exception raised when rescanning the network fails.""" - - def __init__(self, error): - super(NetworkManagerRescanError, self).__init__() - self.error = error - - -class SdbusNetworkManagerAsync(QtCore.QObject): - class ConnectionPriority(enum.Enum): - """Connection priorities""" - - HIGH = 90 - MEDIUM = 50 - LOW = 20 - - nm_state_change: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, name="nm-state-changed" - ) - nm_properties_change: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - tuple, name="nm-properties-changed" - ) - - def __init__(self) -> None: - super().__init__() - self._listeners_running: bool = False - self.listener_thread: threading.Thread = threading.Thread( - name="NMonitor.run_forever", - target=self._listener_run_loop, - daemon=False, - ) - self.listener_task_queue: list = [] - self.loop = asyncio.new_event_loop() - self.stop_listener_event = asyncio.Event() - self.stop_listener_event.clear() - self.system_dbus = sdbus.sd_bus_open_system() - if not self.system_dbus: - logger.error("No dbus found, async network monitor exiting") - self.close() - return - sdbus.set_default_bus(self.system_dbus) - self.nm = dbusNm.NetworkManager() - self.listener_thread.start() - if self.listener_thread.is_alive(): - logger.info( - f"Sdbus NetworkManager Monitor Thread {self.listener_thread.name} Running" - ) - self.hotspot_ssid: str = "PrinterHotspot" - self.hotspot_password: str = "123456789" - self.check_connectivity() - self.available_wired_interfaces = self.get_wired_interfaces() - self.available_wireless_interfaces = self.get_wireless_interfaces() - self.old_ssid: str = "" - wireless_interfaces: typing.List[dbusNm.NetworkDeviceWireless] = ( - self.get_wireless_interfaces() - ) - self.primary_wifi_interface: typing.Optional[dbusNm.NetworkDeviceWireless] = ( - wireless_interfaces[0] if wireless_interfaces else None - ) - wired_interfaces: typing.List[dbusNm.NetworkDeviceWired] = ( - self.get_wired_interfaces() - ) - self.primary_wired_interface: typing.Optional[dbusNm.NetworkDeviceWired] = ( - wired_interfaces[0] if wired_interfaces else None - ) - - self.create_hotspot(self.hotspot_ssid, self.hotspot_password) - if self.primary_wifi_interface: - self.rescan_networks() - - def _listener_run_loop(self) -> None: - try: - asyncio.set_event_loop(self.loop) - self.loop.run_until_complete(asyncio.gather(self.listener_monitor())) - except Exception as e: - logging.error(f"Exception on loop coroutine: {e}") - - async def _end_tasks(self) -> None: - for task in self.listener_task_queue: - task.cancel() - results = await asyncio.gather( - *self.listener_task_queue, return_exceptions=True - ) - for result in results: - if isinstance(result, Exception): - logger.error(f"Caught Exception while ending asyncio tasks: {result}") - return - - def close(self) -> None: - future = asyncio.run_coroutine_threadsafe(self._end_tasks(), self.loop) - try: - future.result(timeout=5) - except Exception as e: - logging.info(f"Exception while ending loop tasks: {e}") - self.stop_listener_event.set() - self.loop.call_soon_threadsafe(self.loop.stop) - self.listener_thread.join() - self.loop.close() - - async def listener_monitor(self) -> None: - """Monitor for NetworkManager properties""" - try: - self._listeners_running = True - - self.listener_task_queue.append( - self.loop.create_task(self._nm_state_listener()) - ) - self.listener_task_queue.append( - self.loop.create_task(self._nm_properties_listener()) - ) - results = asyncio.gather(*self.listener_task_queue, return_exceptions=True) - for result in results: - if isinstance(result, Exception): - logger.error( - f"Caught Exception on network manager asyncio loop: {result}" - ) - raise Exception(result) - await self.stop_listener_event.wait() - - except Exception as e: - logging.error(f"Exception on listener monitor produced coroutine: {e}") - - async def _nm_state_listener(self) -> None: - while self._listeners_running: - try: - async for state in self.nm.state_changed: - enum_state = dbusNm.NetworkManagerState(state) - self.nm_state_change.emit(enum_state.name) - except Exception as e: - logging.error(f"Exception on Network Manager state listener: {e}") - - async def _nm_properties_listener(self) -> None: - while self._listeners_running: - try: - logging.debug("Listening for Network Manager state change") - async for properties in self.nm.properties_changed: - self.nm_properties_change.emit(properties) - - except Exception as e: - logging.error(f"Exception on Network Manager state listener: {e}") - - def check_nm_state(self) -> typing.Union[str, None]: - """Check NetworkManager state""" - if not self.nm: - return - future = asyncio.run_coroutine_threadsafe(self.nm.state.get_async(), self.loop) - try: - state_value = future.result(timeout=2) - return str(dbusNm.NetworkManagerState(state_value).name) - except Exception as e: - logging.error(f"Exception while fetching Network Monitor State: {e}") - return None - - def check_connectivity(self) -> str: - """Checks Network Manager Connectivity state - - UNKNOWN = 0 - Network connectivity is unknown, connectivity checks are disabled. - - NONE = 1 - Host is not connected to any network. - - PORTAL = 2 - Internet connection is hijacked by a captive portal gateway. - - LIMITED = 3 - The host is connected to a network, does not appear to be able to reach full internet. - - FULL = 4 - The host is connected to a network, appears to be able to reach fill internet. - - - Returns: - _type_: _description_ - """ - if not self.nm: - return "" - future = asyncio.run_coroutine_threadsafe( - self.nm.check_connectivity(), self.loop - ) - try: - connectivity = future.result(timeout=2) - return dbusNm.NetworkManagerConnectivityState(connectivity).name - except Exception as e: - logging.error( - f"Exception while fetching Network Monitor Connectivity State: {e}" - ) - return "" - - def check_wifi_interface(self) -> bool: - """Check if wifi interface is set - - Returns: - bool: true if it is. False otherwise - """ - return bool(self.primary_wifi_interface) - - def get_available_interfaces(self) -> typing.Union[typing.List[str], None]: - """Gets the names of all available interfaces - - Returns: - typing.List[str]: List of strings with the available names of all interfaces - """ - try: - future = asyncio.run_coroutine_threadsafe(self.nm.get_devices(), self.loop) - devices = future.result(timeout=2) - interfaces = [] - for device in devices: - interface_future = asyncio.run_coroutine_threadsafe( - dbusNm.NetworkDeviceGeneric( - bus=self.system_dbus, device_path=device - ).interface.get_async(), - self.loop, - ) - interface_name = interface_future.result(timeout=2) - interfaces.append(interface_name) - return interfaces - except Exception as e: - logging.error(f"Exception on fetching available interfaces: {e}") - - def wifi_enabled(self) -> bool: - """Returns a boolean if wireless is enabled on the device. - - Returns: - bool: True if device is enabled | False if not - """ - future = asyncio.run_coroutine_threadsafe( - self.nm.wireless_enabled.get_async(), self.loop - ) - return future.result(timeout=2) - - def toggle_wifi(self, toggle: bool): - """toggle_wifi Enable/Disable wifi - - Args: - toggle (bool): - - - True -> Enable wireless - - - False -> Disable wireless - - Raises: - ValueError: Raised when the argument is not of type boolean. - - """ - if not isinstance(toggle, bool): - raise TypeError("Toggle wifi expected boolean") - if self.wifi_enabled() == toggle: - return - asyncio.run_coroutine_threadsafe( - self.nm.wireless_enabled.set_async(toggle), self.loop - ) - - async def _toggle_networking(self, value: bool = True) -> None: - if not self.primary_wifi_interface: - return - if self.primary_wifi_interface == "/": - return - results = asyncio.gather( - self.loop.create_task(self.nm.enable(value)), - return_exceptions=True, - ) - for result in results: - if isinstance(result, Exception): - logger.error(f"Exception Caught when toggling network : {result}") - - def disable_networking(self) -> None: - """Disable networking""" - if not (self.primary_wifi_interface and self.primary_wired_interface): - return - if self.primary_wifi_interface == "/" and self.primary_wired_interface == "/": - return - asyncio.run_coroutine_threadsafe(self._toggle_networking(False), self.loop) - - def activate_networking(self) -> None: - """Activate networking""" - if not (self.primary_wifi_interface and self.primary_wired_interface): - return - if self.primary_wifi_interface == "/" and self.primary_wired_interface == "/": - return - asyncio.run_coroutine_threadsafe(self._toggle_networking(True), self.loop) - - def toggle_hotspot(self, toggle: bool) -> None: - """Activate/Deactivate device hotspot - - Args: - toggle (bool): toggle option, True to activate Hotspot, False otherwise - - Raises: - ValueError: If the toggle argument is not a Boolean. - """ - if not isinstance(toggle, bool): - raise TypeError("Correct type should be a boolean.") - - if not self.nm: - return - try: - old_ssid: typing.Union[str, None] = self.get_current_ssid() - if old_ssid: - self.old_ssid = old_ssid - if toggle: - self.disconnect_network() - self.connect_network(self.hotspot_ssid) - results = asyncio.gather( - self.nm.reload(0x0), return_exceptions=True - ).result() - for result in results: - if isinstance(result, Exception): - raise Exception(result) - - if self.nm.check_connectivity() == ( - dbusNm.NetworkManagerConnectivityState.FULL - | dbusNm.NetworkManagerConnectivityState.LIMITED - ): - logging.debug(f"Hotspot AP {self.hotspot_ssid} up!") - - return - else: - if self.old_ssid: - self.connect_network(self.old_ssid) - return - except Exception as e: - logging.error(f"Caught Exception while toggling hotspot to {toggle}: {e}") - - def hotspot_enabled(self) -> typing.Optional["bool"]: - """Returns a boolean indicating whether the device hotspot is on or not . - - Returns: - bool: True if Hotspot is activated, False otherwise. - """ - return bool(self.hotspot_ssid == self.get_current_ssid()) - - def get_wired_interfaces(self) -> typing.List[dbusNm.NetworkDeviceWired]: - """get_wired_interfaces Get only the names for the available wired (Ethernet) interfaces. - - Returns: - typing.List[str]: List containing the names of all wired(Ethernet) interfaces. - """ - devs_future = asyncio.run_coroutine_threadsafe(self.nm.get_devices(), self.loop) - devices = devs_future.result(timeout=2) - - return list( - map( - lambda path: dbusNm.NetworkDeviceWired(path), - filter( - lambda path: path, - filter( - lambda device: ( - asyncio.run_coroutine_threadsafe( - dbusNm.NetworkDeviceGeneric( - bus=self.system_dbus, device_path=device - ).device_type.get_async(), - self.loop, - ).result(timeout=2) - == dbusNm.enums.DeviceType.ETHERNET - ), - devices, - ), - ), - ) - ) - - def get_wireless_interfaces( - self, - ) -> typing.List[dbusNm.NetworkDeviceWireless]: - """get_wireless_interfaces Get only the names of wireless interfaces. - - Returns: - typing.List[str]: A list containing the names of wireless interfaces. - """ - # Each interface type has a device flag that is exposed in enums.DeviceType. - devs_future = asyncio.run_coroutine_threadsafe(self.nm.get_devices(), self.loop) - devices = devs_future.result(timeout=2) - return list( - map( - lambda path: dbusNm.NetworkDeviceWireless( - bus=self.system_dbus, device_path=path - ), - filter( - lambda path: path, - filter( - lambda device: ( - asyncio.run_coroutine_threadsafe( - dbusNm.NetworkDeviceGeneric( - bus=self.system_dbus, device_path=device - ).device_type.get_async(), - self.loop, - ).result(timeout=3) - == dbusNm.enums.DeviceType.WIFI - ), - devices, - ), - ), - ) - ) - - async def _gather_ssid(self) -> str: - try: - if not self.nm: - return "" - primary_con = await self.nm.primary_connection.get_async() - if primary_con == "/": - logger.debug("No primary connection") - return "" - active_connection = dbusNm.ActiveConnection( - bus=self.system_dbus, connection_path=primary_con - ) - if not active_connection: - logger.debug("Active connection is none my man") - return "" - con = await active_connection.connection.get_async() - con_settings = dbusNm.NetworkConnectionSettings( - bus=self.system_dbus, settings_path=con - ) - settings = await con_settings.get_settings() - return str(settings["802-11-wireless"]["ssid"][1].decode()) - except Exception as e: - logger.error("Caught exception while gathering ssid %s", e) - return "" - - def get_current_ssid(self) -> str: - """Get current ssid - - Returns: - str: ssid address - """ - try: - future = asyncio.run_coroutine_threadsafe(self._gather_ssid(), self.loop) - return future.result(timeout=5) - except Exception as e: - logging.info(f"Unexpected error occurred: {e}") - return "" - - def get_current_ip_addr(self) -> str: - """Get the current connection ip address. - Returns: - str: A string containing the current ip address - """ - try: - primary_con_fut = asyncio.run_coroutine_threadsafe( - self.nm.primary_connection.get_async(), self.loop - ) - primary_con = primary_con_fut.result(timeout=2) - if primary_con == "/": - logging.info("There is no NetworkManager active connection.") - return "" - - _device_ip4_conf_path = dbusNm.ActiveConnection( - bus=self.system_dbus, connection_path=primary_con - ) - ip4_conf_future = asyncio.run_coroutine_threadsafe( - _device_ip4_conf_path.ip4_config.get_async(), self.loop - ) - - if _device_ip4_conf_path == "/": - logging.info( - "NetworkManager reports no IP configuration for the interface" - ) - return "" - ip4_conf = dbusNm.IPv4Config( - bus=self.system_dbus, ip4_path=ip4_conf_future.result(timeout=2) - ) - addr_data_fut = asyncio.run_coroutine_threadsafe( - ip4_conf.address_data.get_async(), self.loop - ) - addr_data = addr_data_fut.result(timeout=2) - return [address_data["address"][1] for address_data in addr_data][0] - except IndexError as e: - logger.error("List out of index %s", e) - except Exception as e: - logger.error("Error getting current IP address: %s", e) - return "" - - def get_device_ip_by_interface(self, interface_name: str = "wlan0") -> str: - """Get IPv4 address for a specific interface via NetworkManager D-Bus. - - This method retrieves the IP address directly from a specific network - interface, useful for getting hotspot IP when it's the active connection - on that interface. - - Args: - interface_name: The network interface name (e.g., "wlan0", "eth0") - - Returns: - str: The IPv4 address or empty string if not found - """ - if not self.nm: - return "" - - try: - devices_future = asyncio.run_coroutine_threadsafe( - self.nm.get_devices(), self.loop - ) - devices = devices_future.result(timeout=2) - - for device_path in devices: - device = dbusNm.NetworkDeviceGeneric( - bus=self.system_dbus, device_path=device_path - ) - - # Check if this is the interface we want - iface_future = asyncio.run_coroutine_threadsafe( - device.interface.get_async(), self.loop - ) - iface = iface_future.result(timeout=2) - - if iface != interface_name: - continue - - # Get IP4Config path - ip4_path_future = asyncio.run_coroutine_threadsafe( - device.ip4_config.get_async(), self.loop - ) - ip4_path = ip4_path_future.result(timeout=2) - - if not ip4_path or ip4_path == "/": - return "" - - # Get address data - ip4_config = dbusNm.IPv4Config(bus=self.system_dbus, ip4_path=ip4_path) - addr_data_future = asyncio.run_coroutine_threadsafe( - ip4_config.address_data.get_async(), self.loop - ) - addr_data = addr_data_future.result(timeout=2) - - if addr_data and len(addr_data) > 0: - return addr_data[0]["address"][1] - - except Exception as e: - logger.error("Failed to get IP for interface %s: %s", interface_name, e) - - return "" - - async def _gather_primary_interface( - self, - ) -> typing.Union[ - dbusNm.NetworkDeviceWired, - dbusNm.NetworkDeviceWireless, - typing.Tuple, - str, - ]: - if not self.nm: - return "" - - primary_connection = await self.nm.primary_connection.get_async() - if not primary_connection: - return "" - if primary_connection == "/": - if self.primary_wifi_interface and self.primary_wifi_interface != "/": - return self.primary_wifi_interface - elif self.primary_wired_interface and self.primary_wired_interface != "/": - return self.primary_wired_interface - else: - "/" - - primary_conn_type = await self.nm.primary_connection_type.get_async() - active_connection = dbusNm.ActiveConnection( - bus=self.system_dbus, connection_path=primary_connection - ) - gateway = await active_connection.devices.get_async() - device_interface = await dbusNm.NetworkDeviceGeneric( - bus=self.system_dbus, device_path=gateway[0] - ).interface.get_async() - return (device_interface, primary_connection, primary_conn_type) - - def get_primary_interface( - self, - ) -> typing.Union[ - dbusNm.NetworkDeviceWired, - dbusNm.NetworkDeviceWireless, - typing.Tuple, - str, - ]: - """Get the primary interface, - If a there is a connection, returns the interface that is being currently used. - - If there is no connection and wifi is available return de wireless interface. - - If there is no wireless interface and no active connection return the first wired interface that is not (lo). - - - Returns: - typing.List: - """ - future = asyncio.run_coroutine_threadsafe( - self._gather_primary_interface(), self.loop - ) - return future.result(timeout=2) - - async def _rescan(self) -> None: - if not self.primary_wifi_interface: - return - if self.primary_wifi_interface == "/": - return - try: - task = self.loop.create_task(self.primary_wifi_interface.request_scan({})) - results = await asyncio.gather(task, return_exceptions=True) - for result in results: - if isinstance(result, Exception): - raise NetworkManagerRescanError(f"Rescan error: {result}") - return - except Exception as e: - logger.error(f"Caught Exception: {e.__class__.__name__}: {e}") - return - - def rescan_networks(self) -> None: - """Scan for available networks.""" - try: - future = asyncio.run_coroutine_threadsafe(self._rescan(), self.loop) - result = future.result(timeout=2) - return result - - except Exception as e: - logger.error(f"Caught Exception while rescanning networks: {e}") - - async def _get_network_info(self, ap: dbusNm.AccessPoint) -> typing.Tuple: - ssid = await ap.ssid.get_async() - sec = await self._get_security_type(ap) - freq = await ap.frequency.get_async() - channel = await ap.frequency.get_async() - signal = await ap.strength.get_async() - mbit = await ap.max_bitrate.get_async() - bssid = await ap.hw_address.get_async() - return ( - ssid.decode(), - { - "security": sec, - "frequency": freq, - "channel": channel, - "signal_level": signal, - "max_bitrate": mbit, - "bssid": bssid, - }, - ) - - async def _gather_networks( - self, aps: typing.List[dbusNm.AccessPoint] - ) -> typing.Union[typing.List[typing.Tuple], None]: - try: - results = await asyncio.gather( - *(self.loop.create_task(self._get_network_info(ap)) for ap in aps), - return_exceptions=False, - ) - return results - except Exception as e: - logger.error( - f"Caught Exception while asynchronously gathering AP information: {e}" - ) - - async def _get_available_networks(self) -> typing.Union[typing.Dict, None]: - if not self.primary_wifi_interface: - return - if self.primary_wifi_interface == "/": - return - await self._rescan() - try: - last_scan = await self.primary_wifi_interface.last_scan.get_async() - if last_scan != -1: - primary_wifi_dev_type = ( - await self.primary_wifi_interface.device_type.get_async() - ) - if primary_wifi_dev_type == dbusNm.enums.DeviceType.WIFI: - aps = await self.primary_wifi_interface.get_all_access_points() - _aps: typing.List[dbusNm.AccessPoint] = list( - map( - lambda ap_path: dbusNm.AccessPoint( - bus=self.system_dbus, point_path=ap_path - ), - aps, - ) - ) - task = self.loop.create_task(self._gather_networks(_aps)) - result = await asyncio.gather(task, return_exceptions=False) - return dict(*result) if result else None # type:ignore - except Exception as e: - logger.error(f"Caught Exception while gathering access points: {e}") - return {} - - def get_available_networks(self) -> typing.Union[typing.Dict, None]: - """Get available networks""" - future = asyncio.run_coroutine_threadsafe( - self._get_available_networks(), self.loop - ) - return future.result(timeout=20) - - async def _get_security_type(self, ap: dbusNm.AccessPoint) -> typing.Tuple: - """Get the security type from a network AccessPoint - - Args: - ap (AccessPoint): The AccessPoint of the network. - - Returns: - typing.Tuple: A Tuple containing all the flags about the WpaSecurityFlags ans AccessPointCapabilities - - `(flags, wpa_flags, rsn_flags)` - - - - Check: For more information about the flags - :py:class:`WpaSecurityFlags` and `AccessPointCapabilities` from :py:module:`python-sdbus-networkmanager.enums` - """ - if not ap: - return - - _rsn_flag_task = self.loop.create_task(ap.rsn_flags.get_async()) - _wpa_flag_task = self.loop.create_task(ap.wpa_flags.get_async()) - _sec_flags_task = self.loop.create_task(ap.flags.get_async()) - - results = await asyncio.gather( - _rsn_flag_task, - _wpa_flag_task, - _sec_flags_task, - return_exceptions=True, - ) - for result in results: - if isinstance(result, Exception): - logger.error(f"Exception caught getting security type: {result}") - return () - _rsn, _wpa, _sec = results - if len(dbusNm.AccessPointCapabilities(_sec)) == 0: - return ("Open", "") - return ( - dbusNm.WpaSecurityFlags(_rsn), - dbusNm.WpaSecurityFlags(_wpa), - dbusNm.AccessPointCapabilities(_sec), - ) - - def get_saved_networks( - self, - ) -> typing.List[typing.Dict] | None: - """get_saved_networks Gets a list with the names and ids of all saved networks on the device. - - Returns: - typing.List[dict] | None: List that contains the names and ids of all saved networks on the device. - - - - I admit that this implementation is way to complicated, I don't even think it's great on memory and time, but i didn't use for loops so mission achieved. - """ - if not self.nm: - return [] - - try: - _connections: typing.List[str] = asyncio.run_coroutine_threadsafe( - dbusNm.NetworkManagerSettings(bus=self.system_dbus).list_connections(), - self.loop, - ).result(timeout=2) - - saved_cons = list( - map( - lambda connection: dbusNm.NetworkConnectionSettings( - bus=self.system_dbus, settings_path=connection - ), - _connections, - ) - ) - - sv_cons_settings_future = asyncio.run_coroutine_threadsafe( - self._get_settings(saved_cons), - self.loop, - ) - settings_list: typing.List[dbusNm.NetworkManagerConnectionProperties] = ( - sv_cons_settings_future.result(timeout=2) - ) - _known_networks_parameters = list( - filter( - lambda network_entry: network_entry is not None, - list( - map( - lambda network_properties: ( - { - "ssid": network_properties["802-11-wireless"][ - "ssid" - ][1].decode(), - "uuid": network_properties["connection"]["uuid"][1], - "signal": 0 - + self.get_connection_signal_by_ssid( - network_properties["802-11-wireless"]["ssid"][ - 1 - ].decode() - ), - "security": network_properties[ - str( - network_properties["802-11-wireless"][ - "security" - ][1] - ) - ]["key-mgmt"][1], - "mode": network_properties["802-11-wireless"][ - "mode" - ], - "priority": network_properties["connection"].get( - "autoconnect-priority", (None, None) - )[1], - } - if network_properties["connection"]["type"][1] - == "802-11-wireless" - else None - ), - settings_list, - ) - ), - ) - ) - return _known_networks_parameters - except Exception as e: - logger.error(f"Caught exception while fetching saved networks: {e}") - return [] - - @staticmethod - async def _get_settings( - saved_connections: typing.List[dbusNm.NetworkConnectionSettings], - ) -> typing.List[dbusNm.NetworkManagerConnectionProperties]: - tasks = [sc.get_settings() for sc in saved_connections] - return await asyncio.gather(*tasks, return_exceptions=False) - - def get_saved_networks_with_for(self) -> typing.List: - """Get a list with the names and ids of all saved networks on the device. - - Returns: - typing.List[dict]: List that contains the names and ids of all saved networks on the device. - - - This implementation is equal to the klipper screen implementation, this one uses for loops and is simpler. - https://github.com/KlipperScreen/KlipperScreen/blob/master/ks_includes/sdbus_nm.py Alfredo Monclues (alfrix) 2024 - """ - if not self.nm: - return [] - try: - saved_networks: list = [] - conn_future = asyncio.run_coroutine_threadsafe( - dbusNm.NetworkManagerSettings(bus=self.system_dbus).list_connections(), - self.loop, - ) - - connections = conn_future.result(timeout=2) - - # logger.debug(f"got connections from request {connections}") - saved_cons = [ - dbusNm.NetworkConnectionSettings(bus=self.system_dbus, settings_path=c) - for c in connections - ] - # logger.error(f"Getting saved networks with for: {conn_future}") - - sv_cons_settings_future = asyncio.run_coroutine_threadsafe( - self._get_settings(saved_cons), - self.loop, - ) - - settings_list = sv_cons_settings_future.result(timeout=2) - - for connection, conn in zip(connections, settings_list): - if conn["connection"]["type"][1] == "802-11-wireless": - saved_networks.append( - { - "ssid": conn["802-11-wireless"]["ssid"][1].decode(), - "uuid": conn["connection"]["uuid"][1], - "security_type": conn[ - str(conn["802-11-wireless"]["security"][1]) - ]["key-mgmt"][1], - "connection_path": connection, - "mode": conn["802-11-wireless"]["mode"], - } - ) - return saved_networks - except Exception as e: - logger.error(f"Caught Exception while fetching saved networks: {e}") - return [] - - def get_saved_ssid_names(self) -> typing.List[str]: - """Get a list with the current saved network ssid names - - Returns: - typing.List[str]: List that contains the names of the saved ssid network names - """ - try: - _saved_networks = self.get_saved_networks_with_for() - if not _saved_networks: - return [] - return list( - map( - lambda saved_network: saved_network.get("ssid", None), - _saved_networks, - ) - ) - except BaseException as e: - logger.error("Caught exception while getting saved SSID names %s", e) - return [] - - def is_known(self, ssid: str) -> bool: - """Whether or not a network is known - - Args: - ssid (str): The networks ssid - - Returns: - bool: True if the network is known otherwise False - """ - # saved_networks = asyncio.new_event_loop().run_until_complete( - # self.get_saved_networks_with_for() - # ) - saved_networks = self.get_saved_networks_with_for() - return any(net.get("ssid", "") == ssid for net in saved_networks) - - async def _add_wifi_network( - self, - ssid: str, - psk: str, - priority: ConnectionPriority = ConnectionPriority.LOW, - ) -> dict: - """Add new wifi connection - - Args: - ssid (str): Network ssid. - psk (str): Network password - priority (ConnectionPriority, optional): Priority of the network connection. Defaults to ConnectionPriority.LOW. - - Raises: - NotImplementedError: Network security type is not implemented - - Returns: - dict: A dictionary containing the result of the operation - """ - if not self.primary_wifi_interface: - logger.debug("[add wifi network] no primary wifi interface ") - return - if self.primary_wifi_interface == "/": - logger.debug("[add wifi network] no primary wifi interface ") - return - try: - _available_networks = await self._get_available_networks() - if not _available_networks: - logger.debug("Networks not available cancelling adding network") - return {"error": "No networks available"} - if self.is_known(ssid): - self.delete_network(ssid) - if ssid in _available_networks.keys(): - target_network = _available_networks.get(ssid, {}) - if not target_network: - return {"error": "Network unavailable"} - target_interface = ( - await self.primary_wifi_interface.interface.get_async() - ) - _properties: dbusNm.NetworkManagerConnectionProperties = { - "connection": { - "id": ("s", str(ssid)), - "uuid": ("s", str(uuid4())), - "type": ("s", "802-11-wireless"), - "interface-name": ( - "s", - target_interface, - ), - "autoconnect": ("b", bool(True)), - "autoconnect-priority": ( - "u", - priority.value, - ), # We need an integer here - }, - "802-11-wireless": { - "mode": ("s", "infrastructure"), - "ssid": ("ay", ssid.encode("utf-8")), - }, - "ipv4": {"method": ("s", "auto")}, - "ipv6": {"method": ("s", "auto")}, - } - if "security" in target_network.keys(): - _security_types = target_network.get("security") - if not _security_types: - return - if not _security_types[0]: - return - if ( - dbusNm.AccessPointCapabilities.NONE != _security_types[-1] - ): # Normally on last index - _properties["802-11-wireless"]["security"] = ( - "s", - "802-11-wireless-security", - ) - if ( - dbusNm.WpaSecurityFlags.P2P_WEP104 - or dbusNm.WpaSecurityFlags.P2P_WEP40 - or dbusNm.WpaSecurityFlags.BROADCAST_WEP104 - or dbusNm.WpaSecurityFlags.BROADCAST_WEP40 - ) in (_security_types[0] or _security_types[1]): - _properties["802-11-wireless-security"] = { - "key-mgmt": ("s", "none"), - "wep-key-type": ("u", 2), - "wep-key0": ("s", psk), - "auth-alg": ("s", "shared"), - } - elif ( - dbusNm.WpaSecurityFlags.P2P_TKIP - or dbusNm.WpaSecurityFlags.BROADCAST_TKIP - ) in (_security_types[0] or _security_types[1]): - raise NotImplementedError( - "Security type P2P_TKIP OR BRADCAST_TKIP not supported" - ) - elif ( - dbusNm.WpaSecurityFlags.P2P_CCMP - or dbusNm.WpaSecurityFlags.BROADCAST_CCMP - ) in (_security_types[0] or _security_types[1]): - # * AES/CCMP WPA2 - _properties["802-11-wireless-security"] = { - "key-mgmt": ("s", "wpa-psk"), - "auth-alg": ("s", "open"), - "psk": ("s", psk), - "pairwise": ("as", ["ccmp"]), - } - elif (dbusNm.WpaSecurityFlags.AUTH_PSK) in ( - _security_types[0] or _security_types[1] - ): - # * AUTH_PSK -> WPA-PSK - _properties["802-11-wireless-security"] = { - "key-mgmt": ("s", "wpa-psk"), - "auth-alg": ("s", "open"), - "psk": ("s", psk), - } - elif dbusNm.WpaSecurityFlags.AUTH_802_1X in ( - _security_types[0] or _security_types[1] - ): - # * 802.1x IEEE standard ieee802.1x - # Notes: - # IEEE 802.1x standard used 8 to 64 passphrase hashed to derive - # the actual key in the form of 64 hexadecimal character. - # - _properties["802-11-wireless-security"] = { - "key-mgmt": ("s", "wpa-eap"), - "wep-key-type": ("u", 2), - "wep-key0": ("s", psk), - "auth-alg": ("s", "shared"), - } - elif (dbusNm.WpaSecurityFlags.AUTH_SAE) in ( - _security_types[0] or _security_types[1] - ): - # * SAE - # Notes: - # The SAE is WPA3 so they use a passphrase of any length for authentication. - # - _properties["802-11-wireless-security"] = { - "key-mgmt": ("s", "sae"), - "auth-alg": ("s", "open"), - "psk": ("s", psk), - } - elif (dbusNm.WpaSecurityFlags.AUTH_OWE) in ( - _security_types[0] or _security_types[1] - ): - # * OWE - _properties["802-11-wireless-security"] = { - "key-mgmt": ("s", "owe"), - "psk": ("s", psk), - } - elif (dbusNm.WpaSecurityFlags.AUTH_OWE_TM) in ( - _security_types[0] or _security_types[1] - ): - # * OWE TM - raise NotImplementedError("AUTH_OWE_TM not supported") - elif (dbusNm.WpaSecurityFlags.AUTH_EAP_SUITE_B) in ( - _security_types[0] or _security_types[1] - ): - # * EAP SUITE B - raise NotImplementedError("EAP SUITE B Auth not supported") - tasks = [ - self.loop.create_task( - dbusNm.NetworkManagerSettings( - bus=self.system_dbus - ).add_connection(_properties) - ), - self.loop.create_task(self.nm.reload(0x0)), - ] - results = await asyncio.gather(*tasks, return_exceptions=True) - for result in results: - if isinstance(result, Exception): - if isinstance( - result, - dbusNm.exceptions.NmConnectionFailedError, - ): - logger.error( - "Exception caught, could not connect to network: %s", - str(result), - ) - return {"error": f"Connection failed to {ssid}"} - if isinstance( - result, - dbusNm.exceptions.NmConnectionPropertyNotFoundError, - ): - logger.error( - "Exception caught, network properties internal error: %s", - str(result), - ) - return {"error": "Network connection properties error"} - if isinstance( - result, - dbusNm.exceptions.NmConnectionInvalidPropertyError, - ): - logger.error( - "Caught exception while adding new wifi connection: Invalid password: %s", - str(result), - ) - return {"error": "Invalid password"} - if isinstance( - result, - dbusNm.exceptions.NmSettingsPermissionDeniedError, - ): - logger.error( - "Caught exception while adding new wifi connection: Permission Denied: %s", - str(result), - ) - return {"error": "Permission Denied"} - return {"state": "success"} - except NotImplementedError: - logger.error("Network security type not implemented") - return {"error": "Network security type not implemented"} - except Exception as e: - logger.error( - "Caught Exception Unable to add network connection : %s", str(e) - ) - return {"error": "Unable to add network"} - - def add_wifi_network( - self, - ssid: str, - psk: str, - priority: ConnectionPriority = ConnectionPriority.MEDIUM, - ) -> dict: - """Add new wifi password `Synchronous` - - Args: - ssid (str): Network ssid - psk (str): Network password - priority (ConnectionPriority, optional): Network priority. Defaults to ConnectionPriority.MEDIUM. - - Returns: - dict: A dictionary containing the result of the operation - """ - future = asyncio.run_coroutine_threadsafe( - self._add_wifi_network(ssid, psk, priority), self.loop - ) - return future.result(timeout=5) - - def disconnect_network(self) -> None: - """Disconnect the active connection""" - if not self.primary_wifi_interface: - return - if self.primary_wifi_interface == "/": - return - asyncio.run_coroutine_threadsafe( - self.primary_wifi_interface.disconnect(), self.loop - ) - - def get_connection_path_by_ssid(self, ssid: str) -> typing.Union[str, None]: - """Given a ssid, get the connection path, if it's saved - - Raises: - ValueError: If the ssid was not of type string. - - Returns: - str: connection path - """ - if not isinstance(ssid, str): - raise ValueError( - f"SSID argument must be a string, inserted type is : {type(ssid)}" - ) - _connection_path = None - _saved_networks = self.get_saved_networks_with_for() - if not _saved_networks: - raise Exception(f"No network with ssid: {ssid}") - if len(_saved_networks) == 0: - raise Exception("There are no saved networks") - for saved_network in _saved_networks: - if saved_network["ssid"].lower() == ssid.lower(): - _connection_path = saved_network["connection_path"] - return _connection_path - - def get_security_type_by_ssid(self, ssid: str) -> typing.Union[str, None]: - """Get the security type for a saved network by its ssid. - - Args: - ssid (str): SSID of a saved network - - Returns: None or str wit the security type - """ - if not self.nm: - return - if not self.is_known(ssid): - return - _security_type: str = "" - _saved_networks = self.get_saved_networks_with_for() - for network in _saved_networks: - if network["ssid"].lower() == ssid.lower(): - _security_type = network["security_type"] - - return _security_type - - def get_connection_signal_by_ssid(self, ssid: str) -> int: - """Get the signal strength for a ssid - - Args: - ssid (str): Ssid we wan't to scan - - Returns: - int: the signal strength for that ssid - """ - if not self.nm: - return 0 - if not self.primary_wifi_interface: - return 0 - if self.primary_wifi_interface == "/": - return 0 - - self.rescan_networks() - - dev_type = asyncio.run_coroutine_threadsafe( - self.primary_wifi_interface.device_type.get_async(), self.loop - ) - - if dev_type.result(timeout=2) == dbusNm.enums.DeviceType.WIFI: - # Get information on scanned networks: - _aps: typing.List[dbusNm.AccessPoint] = list( - map( - lambda ap_path: dbusNm.AccessPoint( - bus=self.system_dbus, point_path=ap_path - ), - asyncio.run_coroutine_threadsafe( - self.primary_wifi_interface.access_points.get_async(), - self.loop, - ).result(timeout=2), - ) - ) - try: - for ap in _aps: - if ( - asyncio.run_coroutine_threadsafe(ap.ssid.get_async(), self.loop) - .result(timeout=2) - .decode("utf-8") - .lower() - == ssid.lower() - ): - return asyncio.run_coroutine_threadsafe( - ap.strength.get_async(), self.loop - ).result(timeout=2) - except Exception: - return 0 - return 0 - - def connect_network(self, ssid: str) -> str: - """Connect to a saved network given an ssid - - Raises: - ValueError: Raised if the ssid argument is not of type string. - Exception: Raised if there was an error while trying to connect. - - Returns: - str: The active connection path, or a Message. - """ - if not isinstance(ssid, str): - raise ValueError( - f"SSID argument must be a string, inserted type is : {type(ssid)}" - ) - _connection_path = self.get_connection_path_by_ssid(ssid) - if not _connection_path: - raise Exception(f"No saved connection path for the SSID: {ssid}") - try: - if self.nm.primary_connection == _connection_path: - raise Exception(f"Network connection already established with {ssid}") - active_path = asyncio.run_coroutine_threadsafe( - self.nm.activate_connection(str(_connection_path)), self.loop - ).result(timeout=2) - return active_path - except Exception as e: - raise Exception( - f"Unknown error while trying to connect to {ssid} network: {e}" - ) - - async def _delete_network(self, settings_path) -> None: - tasks = [] - tasks.append( - self.loop.create_task( - dbusNm.NetworkConnectionSettings( - bus=self.system_dbus, settings_path=str(settings_path) - ).delete() - ) - ) - - tasks.append( - self.loop.create_task( - dbusNm.NetworkManagerSettings(bus=self.system_dbus).reload_connections() - ) - ) - results = await asyncio.gather(*tasks, return_exceptions=True) - for result in results: - if isinstance(result, Exception): - raise Exception(f"Caught Exception while deleting network: {result}") - - def delete_network(self, ssid: str) -> None: - """Deletes a saved network given a ssid - - Args: - ssid (str): The networks ssid to be deleted - - ### `Should be refactored` - Returns: - typing.Dict: Status key with the outcome of the networks deletion. - """ - if not isinstance(ssid, str): - raise TypeError("SSID argument is of type string") - if not self.is_known(ssid): - logging.debug(f"No known network with SSID {ssid}") - return - try: - self.deactivate_connection_by_ssid(ssid) - _path = self.get_connection_path_by_ssid(ssid) - task = self.loop.create_task(self._delete_network(_path)) - future = asyncio.gather(task, return_exceptions=True) - results = future.result() - for result in results: - if isinstance(result, Exception): - raise Exception(result) - except Exception as e: - logging.debug(f"Caught Exception while deleting network {ssid}: {e}") - - def get_hotspot_ssid(self) -> str: - """Get current hotspot ssid""" - return self.hotspot_ssid - - def deactivate_connection(self, connection_path) -> None: - """Deactivate a connection, by connection path""" - if not self.nm: - return - if not self.primary_wifi_interface: - return - if self.primary_wifi_interface == "/": - return - try: - future = asyncio.run_coroutine_threadsafe( - self.nm.active_connections.get_async(), self.loop - ) - active_connections = future.result(timeout=2) - if connection_path in active_connections: - task = self.loop.create_task( - self.nm.deactivate_connection(active_connection=connection_path) - ) - future = asyncio.gather(task) - except Exception as e: - logger.error( - f"Caught exception while deactivating network {connection_path}: {e}" - ) - - def deactivate_connection_by_ssid(self, ssid: str) -> None: - """Deactivate connection by ssid""" - if not self.nm: - return - if not self.primary_wifi_interface: - return - if self.primary_wifi_interface == "/": - return - - try: - _connection_path = self.get_connection_path_by_ssid(ssid) - if not _connection_path: - raise Exception(f"Network saved network with name {ssid}") - self.deactivate_connection(_connection_path) - except Exception as e: - logger.error(f"Exception Caught while deactivating network {ssid}: {e}") - - def create_hotspot( - self, ssid: str = "PrinterHotspot", password: str = "123456789" - ) -> None: - """Create hostpot - - Args: - ssid (str, optional): Hotspot ssid. Defaults to "PrinterHotspot". - password (str, optional): connection password. Defaults to "123456789". - """ - if self.is_known(ssid): - self.delete_network(ssid) - logger.debug("old hotspot deleted") - try: - self.delete_network(ssid) - # psk = hashlib.sha256(password.encode()).hexdigest() - _properties: dbusNm.NetworkManagerConnectionProperties = { - "connection": { - "id": ("s", str(ssid)), - "uuid": ("s", str(uuid4())), - "type": ("s", "802-11-wireless"), # 802-3-ethernet - "interface-name": ("s", "wlan0"), - }, - "802-11-wireless": { - "ssid": ("ay", ssid.encode("utf-8")), - "mode": ("s", "ap"), - "band": ("s", "bg"), - "channel": ("u", 6), - "security": ("s", "802-11-wireless-security"), - }, - "802-11-wireless-security": { - "key-mgmt": ("s", "wpa-psk"), - "psk": ("s", password), - "pmf": ("u", 0), - }, - "ipv4": { - "method": ("s", "shared"), - }, - "ipv6": {"method": ("s", "ignore")}, - } - - tasks = [ - self.loop.create_task( - dbusNm.NetworkManagerSettings(bus=self.system_dbus).add_connection( - _properties - ) - ), - self.loop.create_task(self.nm.reload(0x0)), - ] - - self.loop.run_until_complete( - asyncio.gather(*tasks, return_exceptions=False) - ) - for task in tasks: - self.loop.run_until_complete(task) - - except Exception as e: - logging.error(f"Caught Exception while creating hotspot: {e}") - - def set_network_priority( - self, ssid: str, priority: ConnectionPriority = ConnectionPriority.LOW - ) -> None: - """Set network priority - - Args: - ssid (str): connection ssid - priority (ConnectionPriority, optional): Priority. Defaults to ConnectionPriority.LOW. - """ - if not self.nm: - return - if not self.is_known(ssid): - return - self.update_connection_settings(ssid=ssid, priority=priority.value) - - def update_connection_settings( - self, - ssid: str, - password: typing.Optional["str"] = None, - new_ssid: typing.Optional["str"] = None, - priority: int = 20, - ) -> None: - """Update the settings for a connection with a specified ssid and or a password - - Args: - ssid (str | None): SSID of the network we want to update - password - Returns: - typing.Dict: status dictionary with possible keys "error" and "status" - """ - - if not self.nm: - raise Exception("NetworkManager Missing") - if not self.is_known(str(ssid)): - raise Exception("%s network is not known, cannot update", ssid) - - _connection_path = self.get_connection_path_by_ssid(str(ssid)) - if not _connection_path: - raise Exception("No saved connection with the specified ssid") - try: - con_settings = dbusNm.NetworkConnectionSettings( - bus=self.system_dbus, settings_path=str(_connection_path) - ) - properties = asyncio.run_coroutine_threadsafe( - con_settings.get_settings(), self.loop - ).result(timeout=2) - if new_ssid: - properties["connection"]["id"] = ("s", str(new_ssid)) - properties["802-11-wireless"]["ssid"] = ( - "ay", - new_ssid.encode("utf-8"), - ) - if password: - # pwd = hashlib.sha256(password.encode()).hexdigest() - properties["802-11-wireless-security"]["psk"] = ( - "s", - str(password.encode("utf-8")), - ) - - if priority != 0: - properties["connection"]["autoconnect-priority"] = ( - "u", - priority, - ) - - tasks = [ - self.loop.create_task(con_settings.update(properties)), - self.loop.create_task(self.nm.reload(0x0)), - ] - self.loop.run_until_complete( - asyncio.gather(*tasks, return_exceptions=False) - ) - - if ssid == self.hotspot_ssid and new_ssid: - self.hotspot_ssid = new_ssid - if password != self.hotspot_password and password: - self.hotspot_password = password - except Exception as e: - logger.error("Caught Exception while updating network: %s", e) diff --git a/BlocksScreen/lib/network/__init__.py b/BlocksScreen/lib/network/__init__.py new file mode 100644 index 00000000..9f06e612 --- /dev/null +++ b/BlocksScreen/lib/network/__init__.py @@ -0,0 +1,60 @@ +"""Network Manager Package + +Architecture: + NetworkManager (manager.py) + └── Main thread interface with signals/slots + └── Non-blocking API + └── Caches state for quick access + + NetworkManagerWorker (worker.py) + └── Runs in dedicated Thread + └── Owns asyncio event loop + └── Handles all D-Bus async operations + + Models (models.py) + └── Data classes for type safety + └── Enums for states and types +""" + +from .manager import NetworkManager +from .models import ( + UNSUPPORTED_SECURITY_TYPES, + ConnectionPriority, + ConnectionResult, + ConnectivityState, + HotspotConfig, + HotspotSecurity, + NetworkInfo, + NetworkState, + NetworkStatus, + PendingOperation, + SavedNetwork, + SecurityType, + VlanInfo, + WifiIconKey, + is_connectable_security, + is_hidden_ssid, + signal_to_bars, +) + +__all__ = [ + "NetworkManager", + "ConnectionPriority", + "ConnectionResult", + "ConnectivityState", + "HotspotConfig", + "HotspotSecurity", + "NetworkInfo", + "NetworkState", + "NetworkStatus", + "PendingOperation", + "SavedNetwork", + "SecurityType", + "UNSUPPORTED_SECURITY_TYPES", + "VlanInfo", + "WifiIconKey", + # Utilities + "is_connectable_security", + "is_hidden_ssid", + "signal_to_bars", +] diff --git a/BlocksScreen/lib/network/manager.py b/BlocksScreen/lib/network/manager.py new file mode 100644 index 00000000..1ef6cd82 --- /dev/null +++ b/BlocksScreen/lib/network/manager.py @@ -0,0 +1,368 @@ +# pylint: disable=protected-access + +import asyncio +import logging + +from PyQt6.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot + +from .models import ( + ConnectionPriority, + ConnectionResult, + ConnectivityState, + NetworkInfo, + NetworkState, + SavedNetwork, +) +from .worker import NetworkManagerWorker + +logger = logging.getLogger(__name__) + +_KEEPALIVE_POLL_MS: int = 300_000 # 5 minutes — safety net for missed signals + + +class NetworkManager(QObject): + """Main-thread manager/interface to the NetworkManager D-Bus worker. + + The UI layer should only interact with this class. Internally it owns + a ``NetworkManagerWorker`` that runs all D-Bus coroutines on its + dedicated asyncio thread. + + Coroutines are submitted to ``worker._asyncio_loop`` — the same loop + on which the D-Bus file-descriptor was registered — so signal delivery + and async I/O always occur on the correct selector. + + """ + + state_changed = pyqtSignal(NetworkState) + networks_scanned = pyqtSignal(list) + saved_networks_loaded = pyqtSignal(list) + connection_result = pyqtSignal(ConnectionResult) + connectivity_changed = pyqtSignal(ConnectivityState) + error_occurred = pyqtSignal(str, str) + reconnect_complete = pyqtSignal() + hotspot_config_updated = pyqtSignal(str, str, str) + + def __init__(self, parent: QObject | None = None) -> None: + """Create the worker, wire all signals""" + super().__init__(parent) + + self._cached_state: NetworkState = NetworkState() + self._cached_networks: list[NetworkInfo] = [] + self._cached_saved: list[SavedNetwork] = [] + self._network_info_map: dict[str, NetworkInfo] = {} + self._saved_network_map: dict[str, SavedNetwork] = {} + + self._shutting_down: bool = False + self._worker_ready: bool = False + + self._pending_futures: set["asyncio.Future"] = set() + + self._worker = NetworkManagerWorker() + + self._cached_hotspot_ssid: str = self._worker._hotspot_config.ssid + self._cached_hotspot_password: str = self._worker._hotspot_config.password + self._cached_hotspot_security: str = self._worker._hotspot_config.security + self._worker.state_changed.connect(self._on_state_changed) + self._worker.networks_scanned.connect(self._on_networks_scanned) + self._worker.saved_networks_loaded.connect(self._on_saved_networks_loaded) + self._worker.connection_result.connect(self.connection_result) + self._worker.connectivity_changed.connect(self.connectivity_changed) + self._worker.error_occurred.connect(self.error_occurred) + self._worker.hotspot_info_ready.connect(self._on_hotspot_info_ready) + self._worker.reconnect_complete.connect(self.reconnect_complete) + self._worker.initialized.connect(self._on_worker_initialized) + + # Keepalive timer — safety net for any missed D-Bus signals. + self._keepalive_timer = QTimer(self) + self._keepalive_timer.setInterval(_KEEPALIVE_POLL_MS) + self._keepalive_timer.timeout.connect(self._on_keepalive_tick) + + logger.info("NetworkManager manager created (waiting for worker init)") + + def _schedule(self, coro: "asyncio.Coroutine") -> None: + """Submit *coro* to the worker's asyncio loop from the main thread. + + Stores a strong reference to the returned + Future to prevent Python's GC from destroying the underlying + asyncio.Task while it is still running. + """ + if self._shutting_down: + coro.close() + return + loop = self._worker._asyncio_loop + if loop.is_running(): + future = asyncio.run_coroutine_threadsafe(coro, loop) + self._pending_futures.add(future) + future.add_done_callback(self._pending_futures.discard) + else: + logger.debug( + "Dropping early coroutine — loop not yet running: %s", + coro.__qualname__, + ) + coro.close() + + @pyqtSlot() + def _on_worker_initialized(self) -> None: + """Called once when the worker finishes + D-Bus init and interface detection. + + Starts the keepalive timer *after* _primary_wifi_path and + _primary_wired_path are populated, eliminating the old 2-second + guess-timer that raced with init on slow boots. + """ + if self._shutting_down: + return + self._worker_ready = True + logger.info( + "Worker initialised — starting keepalive (every %d ms)", + _KEEPALIVE_POLL_MS, + ) + self._keepalive_timer.start() + self._schedule(self._worker._async_get_current_state()) + self._schedule(self._worker._async_scan_networks()) + self._schedule(self._worker._async_load_saved_networks()) + + def shutdown(self) -> None: + """Gracefully stop the worker, asyncio loop, and background thread.""" + self._shutting_down = True + self._keepalive_timer.stop() + + loop = self._worker._asyncio_loop + if loop.is_running(): + future = asyncio.run_coroutine_threadsafe( + self._worker._async_shutdown(), loop + ) + try: + future.result(timeout=5.0) + except Exception as exc: + logger.warning("Worker shutdown coroutine raised: %s", exc) + + self._worker._asyncio_thread.join(timeout=3.0) + if self._worker._asyncio_thread.is_alive(): + logger.warning("Asyncio thread did not exit within 3 s") + + self._pending_futures.clear() + + logger.info("NetworkManager manager shutdown complete") + + def close(self) -> None: + """Alias for ``shutdown``""" + self.shutdown() + + @pyqtSlot(NetworkState) + def _on_state_changed(self, state: NetworkState) -> None: + """Cache the new state and re-emit to UI consumers.""" + if self._shutting_down: + return + self._cached_state = state + self.state_changed.emit(state) + + @pyqtSlot(list) + def _on_networks_scanned(self, networks: list) -> None: + """Cache scan results, rebuild SSID lookup map, and re-emit.""" + if self._shutting_down: + return + self._cached_networks = networks + self._network_info_map = {n.ssid: n for n in networks} + self.networks_scanned.emit(networks) + + @pyqtSlot(list) + def _on_saved_networks_loaded(self, networks: list) -> None: + """Cache saved profiles, rebuild lowercase lookup map, and re-emit.""" + if self._shutting_down: + return + self._cached_saved = networks + self._saved_network_map = {n.ssid.lower(): n for n in networks} + self.saved_networks_loaded.emit(networks) + + @pyqtSlot(str, str, str) + def _on_hotspot_info_ready(self, ssid: str, password: str, security: str) -> None: + """Update the main-thread hotspot cache and notify UI via ``hotspot_config_updated``.""" + self._cached_hotspot_ssid = ssid + self._cached_hotspot_password = password + self._cached_hotspot_security = security + self.hotspot_config_updated.emit(ssid, password, security) + + @pyqtSlot() + def _on_keepalive_tick(self) -> None: + """Safety-net refresh — runs every 5 min to catch any missed signals.""" + if self._shutting_down: + return + self._schedule(self._worker._async_get_current_state()) + self._schedule(self._worker._async_check_connectivity()) + self._schedule(self._worker._async_load_saved_networks()) + + def request_state_soon(self, delay_ms: int = 500) -> None: + """Request a state refresh after a short delay.""" + QTimer.singleShot( + delay_ms, + lambda: self._schedule(self._worker._async_get_current_state()), + ) + + def get_current_state(self) -> None: + """Request an immediate state refresh from the worker.""" + self._schedule(self._worker._async_get_current_state()) + + def refresh_state(self) -> None: + """Request a state refresh and a saved-network reload from the worker.""" + self._schedule(self._worker._async_get_current_state()) + self._schedule(self._worker._async_load_saved_networks()) + + def scan_networks(self) -> None: + """Request an immediate Wi-Fi scan from the worker.""" + self._schedule(self._worker._async_scan_networks()) + + def load_saved_networks(self) -> None: + """Request a reload of saved connection profiles from the worker.""" + self._schedule(self._worker._async_load_saved_networks()) + + def check_connectivity(self) -> None: + """Request an NM connectivity check from the worker.""" + self._schedule(self._worker._async_check_connectivity()) + + def add_network( + self, + ssid: str, + password: str = "", # nosec B107 + priority: int = ConnectionPriority.MEDIUM.value, + ) -> None: + """Add a new Wi-Fi profile (and connect immediately) with optional priority.""" + self._schedule(self._worker._async_add_network(ssid, password, priority)) + + def connect_network(self, ssid: str) -> None: + """Connect to an already-saved network by *ssid*.""" + self._schedule(self._worker._async_connect_network(ssid)) + + def disconnect(self) -> None: + """Disconnect the currently active Wi-Fi connection.""" + self._schedule(self._worker._async_disconnect()) + + def delete_network(self, ssid: str) -> None: + """Delete the saved profile for *ssid*.""" + self._schedule(self._worker._async_delete_network(ssid)) + + def update_network( # nosec B107 + self, ssid: str, password: str = "", priority: int = 0 + ) -> None: + """Update the password and/or autoconnect priority for a saved profile.""" + self._schedule(self._worker._async_update_network(ssid, password, priority)) + + def set_wifi_enabled(self, enabled: bool) -> None: + """Enable or disable the Wi-Fi radio.""" + self._schedule(self._worker._async_set_wifi_enabled(enabled)) + + def create_hotspot( + self, + ssid: str = "", + password: str = "", + security: str = "wpa-psk", # nosec B107 + ) -> None: + """Create and immediately activate a hotspot with the given credentials.""" + self._schedule( + self._worker._async_create_and_activate_hotspot(ssid, password, security) + ) + + def toggle_hotspot(self, enable: bool) -> None: + """Deactivate the hotspot (enable=False) or create+activate (enable=True).""" + self._schedule(self._worker._async_toggle_hotspot(enable)) + + def update_hotspot_config( + self, + old_ssid: str, + new_ssid: str, + new_password: str, + security: str = "wpa-psk", + ) -> None: + """Change hotspot name/password/security — cleans up old profiles.""" + self._schedule( + self._worker._async_update_hotspot_config( + old_ssid, new_ssid, new_password, security + ) + ) + + def disconnect_ethernet(self) -> None: + """Deactivate the primary wired interface.""" + self._schedule(self._worker._async_disconnect_ethernet()) + + def connect_ethernet(self) -> None: + """Activate the primary wired interface.""" + self._schedule(self._worker._async_connect_ethernet()) + + def create_vlan_connection( + self, + vlan_id: int, + ip_address: str, + subnet_mask: str, + gateway: str, + dns1: str = "", + dns2: str = "", + ) -> None: + """Create and activate a VLAN connection with + given static IP settings""" + self._schedule( + self._worker._async_create_vlan( + vlan_id, ip_address, subnet_mask, gateway, dns1, dns2 + ) + ) + + def delete_vlan_connection(self, vlan_id: int) -> None: + """Delete all NM profiles for *vlan_id*.""" + self._schedule(self._worker._async_delete_vlan(vlan_id)) + + def update_wifi_static_ip( + self, + ssid: str, + ip_address: str, + subnet_mask: str, + gateway: str, + dns1: str = "", + dns2: str = "", + ) -> None: + """Apply a static IP configuration to a saved Wi-Fi profile.""" + self._schedule( + self._worker._async_update_wifi_static_ip( + ssid, ip_address, subnet_mask, gateway, dns1, dns2 + ) + ) + + def reset_wifi_to_dhcp(self, ssid: str) -> None: + """Reset a saved Wi-Fi profile back to DHCP.""" + self._schedule(self._worker._async_reset_wifi_to_dhcp(ssid)) + + @property + def current_state(self) -> NetworkState: + """Most recently cached ``NetworkState`` snapshot.""" + return self._cached_state + + @property + def current_ssid(self) -> str | None: + """SSID of the currently active Wi-Fi connection, or ``None``.""" + return self._cached_state.current_ssid + + @property + def saved_networks(self) -> list[SavedNetwork]: + """Most recently cached list of saved ``SavedNetwork`` profiles.""" + return self._cached_saved + + @property + def hotspot_ssid(self) -> str: + """Hotspot SSID — read from main-thread cache (thread-safe).""" + return self._cached_hotspot_ssid + + @property + def hotspot_password(self) -> str: + """Hotspot password — read from main-thread cache (thread-safe).""" + return self._cached_hotspot_password + + @property + def hotspot_security(self) -> str: + """Hotspot security type — always 'wpa-psk' (WPA2-PSK, thread-safe).""" + return self._cached_hotspot_security + + def get_network_info(self, ssid: str) -> NetworkInfo | None: + """Return the scanned ``NetworkInfo`` for *ssid*, or ``None``.""" + return self._network_info_map.get(ssid) + + def get_saved_network(self, ssid: str) -> SavedNetwork | None: + """Return the saved ``SavedNetwork`` for *ssid* (case-insensitive).""" + return self._saved_network_map.get(ssid.lower()) diff --git a/BlocksScreen/lib/network/models.py b/BlocksScreen/lib/network/models.py new file mode 100644 index 00000000..6b5f68d5 --- /dev/null +++ b/BlocksScreen/lib/network/models.py @@ -0,0 +1,327 @@ +"""Data models for the NetworkManager subsystem.""" + +import sys +from dataclasses import dataclass +from enum import Enum, IntEnum + + +class SecurityType(str, Enum): + """Wi-Fi security types.""" + + OPEN = "open" + WEP = "wep" + WPA_PSK = "wpa-psk" + WPA2_PSK = "wpa2-psk" + WPA3_SAE = "sae" + WPA_EAP = "wpa-eap" + OWE = "owe" + UNKNOWN = "unknown" + + +# Security types this device cannot connect to. +UNSUPPORTED_SECURITY_TYPES: frozenset[str] = frozenset( + { + SecurityType.WEP.value, + SecurityType.WPA_EAP.value, + SecurityType.OWE.value, + SecurityType.OPEN.value, + } +) + + +def is_connectable_security(security: "SecurityType | str") -> bool: + """Return True if this device can connect to *security* type.""" + return security not in UNSUPPORTED_SECURITY_TYPES + + +class ConnectivityState(IntEnum): + """NetworkManager connectivity states.""" + + UNKNOWN = 0 + NONE = 1 + PORTAL = 2 + LIMITED = 3 + FULL = 4 + + +class ConnectionPriority(IntEnum): + """Autoconnect priority levels for saved connections (higher = \ + preferred).""" + + LOW = 20 + MEDIUM = 50 + HIGH = 90 + HIGHEST = 100 + + +class PendingOperation(IntEnum): + """Identifies which network transition is currently in-flight.""" + + NONE = 0 + WIFI_ON = 1 + WIFI_OFF = 2 + HOTSPOT_ON = 3 + HOTSPOT_OFF = 4 + CONNECT = 5 + ETHERNET_ON = 6 + ETHERNET_OFF = 7 + WIFI_STATIC_IP = 8 # VLAN with DHCP (long-running, up to 45 s) + + +class NetworkStatus(IntEnum): + """State of a Wi-Fi network from the device's perspective. + + Values are ordered so that higher values indicate a "more connected" + state. This lets callers use comparison operators for grouping:: + + is_saved <-> network.network_status >= NetworkStatus.SAVED + is_active <-> network.network_status == NetworkStatus.ACTIVE + + ``is_open`` is **not** encoded here because it is a property of the + network's *security type*, not its connection state. Use + ``NetworkInfo.is_open`` (derived from ``security_type``) instead. + """ + + DISCOVERED = 0 # Seen in scan, not saved — protected security + OPEN = 1 # Seen in scan, not saved — open (no passphrase) + SAVED = 2 # Profile saved on this device + ACTIVE = 3 # Currently connected + HIDDEN = 4 # Hidden-network placeholder + + @property + def label(self) -> str: + """Human-readable status label for UI display.""" + return _STATUS_LABELS[self] + + @staticmethod + def update_status_label(status: "NetworkStatus", label: str) -> None: + """Update the human-readable label for a given network status.""" + _STATUS_LABELS[status] = sys.intern(label) + + +_STATUS_LABELS: dict[NetworkStatus, str] = { + NetworkStatus.DISCOVERED: sys.intern("Protected"), + NetworkStatus.OPEN: sys.intern("Open"), + NetworkStatus.SAVED: sys.intern("Saved"), + NetworkStatus.ACTIVE: sys.intern("Active"), + NetworkStatus.HIDDEN: sys.intern("Hidden"), +} + + +SIGNAL_EXCELLENT_THRESHOLD = 75 +SIGNAL_GOOD_THRESHOLD = 50 +SIGNAL_FAIR_THRESHOLD = 25 +SIGNAL_MINIMUM_THRESHOLD = 5 + + +def signal_to_bars(signal: int) -> int: + """Convert signal strength percentage (0-100) to bar count (0-4).""" + if signal < SIGNAL_MINIMUM_THRESHOLD: + return 0 + if signal >= SIGNAL_EXCELLENT_THRESHOLD: + return 4 + if signal >= SIGNAL_GOOD_THRESHOLD: + return 3 + if signal > SIGNAL_FAIR_THRESHOLD: + return 2 + return 1 + + +class WifiIconKey(IntEnum): + """Lightweight icon key for the header Wi-Fi status icon. + + Encodes signal bars (0-4), protection status, and special states + into a single integer for cheap cross-thread signalling via + pyqtSignal(int). + + Encoding: ethernet = -1, hotspot = 10, wifi = bars * 2 + is_protected + Range: -1, 0..10 + """ + + ETHERNET = -1 + + WIFI_0_OPEN = 0 + WIFI_0_PROTECTED = 1 + WIFI_1_OPEN = 2 + WIFI_1_PROTECTED = 3 + WIFI_2_OPEN = 4 + WIFI_2_PROTECTED = 5 + WIFI_3_OPEN = 6 + WIFI_3_PROTECTED = 7 + WIFI_4_OPEN = 8 + WIFI_4_PROTECTED = 9 + + HOTSPOT = 10 + + @classmethod + def from_bars(cls, bars: int, is_protected: bool) -> "WifiIconKey": + """Encode bar count (0-4) + protection flag into a WifiIconKey.""" + if not 0 <= bars <= 4: + raise ValueError(f"Bars must be 0-4 (got {bars})") + return cls(bars * 2 + int(is_protected)) + + @classmethod + def from_signal(cls, signal_strength: int, is_protected: bool) -> "WifiIconKey": + """Convert raw signal strength + protection to a WifiIconKey.""" + return cls.from_bars(signal_to_bars(signal_strength), is_protected) + + @property + def bars(self) -> int: + """Signal bars (0-4). Raises ValueError for ETHERNET/HOTSPOT.""" + if self is WifiIconKey.ETHERNET or self is WifiIconKey.HOTSPOT: + raise ValueError(f"{self.name} has no bar count") + return self.value // 2 + + @property + def is_protected(self) -> bool: + """Whether the network is protected. + Raises ValueError for ETHERNET/HOTSPOT.""" + if self is WifiIconKey.ETHERNET or self is WifiIconKey.HOTSPOT: + raise ValueError(f"{self.name} has no protection status") + return bool(self.value % 2) + + +@dataclass(frozen=True, slots=True) +class NetworkInfo: + """Represents a single Wi-Fi access point discovered during a scan. + + Connection state is encoded in *network_status* (a single ``int`` + the same width as the four booleans it replaced). Security openness + is derived from *security_type* via the ``is_open`` property. + """ + + ssid: str = "" + signal_strength: int = 0 + network_status: NetworkStatus = NetworkStatus.DISCOVERED + bssid: str = "" + frequency: int = 0 + max_bitrate: int = 0 + security_type: SecurityType | str = SecurityType.UNKNOWN + + @property + def is_open(self) -> bool: + """True when the AP broadcasts no security flags.""" + return self.security_type == SecurityType.OPEN + + @property + def is_saved(self) -> bool: + """True when a profile for this network exists on the device.""" + return self.network_status >= NetworkStatus.SAVED + + @property + def is_active(self) -> bool: + """True when the device is currently connected to this AP.""" + return self.network_status == NetworkStatus.ACTIVE + + @property + def is_hidden(self) -> bool: + """True for hidden-network placeholders.""" + return self.network_status == NetworkStatus.HIDDEN + + @property + def status(self) -> str: + """Human-readable status label (Active > Saved > Open > Protected).""" + return self.network_status.label + + +@dataclass(frozen=True, slots=True) +class SavedNetwork: + """Represents a saved (known) Wi-Fi connection profile.""" + + ssid: str = "" + uuid: str = "" + connection_path: str = "" + security_type: str = "" + mode: str = "infrastructure" + priority: int = ConnectionPriority.MEDIUM.value + signal_strength: int = 0 + timestamp: int = 0 # Unix time of last successful activation + is_dhcp: bool = True # True = auto (DHCP), False = manual (static IP) + + +@dataclass(frozen=True, slots=True) +class ConnectionResult: + """Outcome of a connection/network operation.""" + + success: bool = False + message: str = "" + error_code: str = "" + data: dict[str, object] | None = None + + +@dataclass(frozen=True, slots=True) +class VlanInfo: + """Snapshot of an active VLAN connection.""" + + vlan_id: int = 0 + ip_address: str = "" + interface: str = "" + gateway: str = "" + dns_servers: tuple[str, ...] = () + is_dhcp: bool = False + + +@dataclass(frozen=True, slots=True) +class NetworkState: + """Snapshot of the current network state.""" + + connectivity: ConnectivityState = ConnectivityState.UNKNOWN + current_ssid: str | None = None + current_ip: str = "" + wifi_enabled: bool = False + hotspot_enabled: bool = False + primary_interface: str = "" + signal_strength: int = 0 + security_type: str = "" + ethernet_connected: bool = False + ethernet_carrier: bool = False + active_vlans: tuple[VlanInfo, ...] = () + + +class HotspotSecurity(str, Enum): + """Supported hotspot security protocols. + + The *value* is the internal key passed through manager -> worker; + the NM ``key-mgmt`` and cipher settings are resolved at profile + creation time in ``create_and_activate_hotspot``. + """ + + WPA1 = "wpa1" + WPA2_PSK = "wpa-psk" # WPA2-PSK (CCMP) — default + + @classmethod + def is_valid(cls, value: str) -> bool: + """Return True if *value* matches a known security key.""" + return value in cls._value2member_map_ + + +@dataclass(slots=True) +class HotspotConfig: + """Mutable configuration for the access-point / hotspot.""" + + ssid: str = "PrinterHotspot" + password: str = "123456789" + band: str = "bg" + channel: int = 6 + security: str = HotspotSecurity.WPA2_PSK.value + + +# Patterns that indicate a hidden or invalid SSID +_HIDDEN_INDICATORS = frozenset({"unknown", "hidden", ""}) + + +def is_hidden_ssid(ssid: str | None) -> bool: + """Return True if *ssid* is blank, whitespace, null-bytes, or a + well-known hidden-network placeholder. + + Handles: None, "", " ", "\\x00\\x00", "unknown", "UNKNOWN", + "hidden", "", "". + """ + if not ssid: + return True + stripped = ssid.strip() + if not stripped: + return True + if stripped[0] == "\x00" and all(c == "\x00" for c in stripped): + return True + return stripped.lower() in _HIDDEN_INDICATORS diff --git a/BlocksScreen/lib/network/worker.py b/BlocksScreen/lib/network/worker.py new file mode 100644 index 00000000..a1e971ae --- /dev/null +++ b/BlocksScreen/lib/network/worker.py @@ -0,0 +1,2587 @@ +import asyncio +import fcntl +import ipaddress +import logging +import os +import socket as _socket +import struct +import threading +from uuid import uuid4 + +import sdbus +from configfile import get_configparser +from PyQt6.QtCore import QObject, pyqtSignal +from sdbus_async import networkmanager as dbus_nm + +from .models import ( + ConnectionPriority, + ConnectionResult, + ConnectivityState, + HotspotConfig, + HotspotSecurity, + NetworkInfo, + NetworkState, + NetworkStatus, + SavedNetwork, + SecurityType, + VlanInfo, + is_connectable_security, + is_hidden_ssid, +) + +logger = logging.getLogger(__name__) + +_CAN_RELOAD_CONNECTIONS: bool = os.getuid() == 0 + +# Debounce window for coalescing rapid D-Bus signal bursts (seconds). +_DEBOUNCE_DELAY: float = 0.8 +# Delay before restarting a failed signal listener (seconds). +_LISTENER_RESTART_DELAY: float = 3.0 +# Timeout for _wait_for_connection: must cover 802.11 handshake + DHCP. +_WIFI_CONNECT_TIMEOUT: float = 20.0 + + +class NetworkManagerWorker(QObject): + """Async NetworkManager worker (signal-reactive). + + Owns an asyncio event loop running on a dedicated daemon thread. + All D-Bus operations execute as coroutines on that loop. + + Primary state updates are driven by D-Bus signals, not polling. + """ + + state_changed = pyqtSignal(NetworkState, name="stateChanged") + networks_scanned = pyqtSignal(list, name="networksScanned") + saved_networks_loaded = pyqtSignal(list, name="savedNetworksLoaded") + connection_result = pyqtSignal(ConnectionResult, name="connectionResult") + connectivity_changed = pyqtSignal(ConnectivityState, name="connectivityChanged") + error_occurred = pyqtSignal(str, str, name="errorOccurred") + hotspot_info_ready = pyqtSignal(str, str, str, name="hotspotInfoReady") + reconnect_complete = pyqtSignal(name="reconnectComplete") + + _MAX_DBUS_ERRORS_BEFORE_RECONNECT: int = 3 + + initialized = pyqtSignal(name="workerInitialized") + + def __init__(self) -> None: + """Initialise the worker, creating the asyncio loop and daemon thread. + + Sets up all instance state (interface paths, hotspot config, signal + proxies, debounce handles) and immediately starts the asyncio daemon + thread that opens the system D-Bus and drives all NetworkManager + coroutines. + """ + super().__init__() + self._running: bool = False + self._system_bus: sdbus.SdBus | None = None + + # Path strings only — read-proxies are always created fresh. + self._primary_wifi_path: str = "" + self._primary_wifi_iface: str = "" + self._primary_wired_path: str = "" + self._primary_wired_iface: str = "" + + self._iface_to_device_path: dict[str, str] = {} + + self._hotspot_config = HotspotConfig() + self._load_hotspot_config() + self._saved_cache: list[SavedNetwork] = [] + self._saved_cache_dirty: bool = True + self._is_hotspot_active: bool = False + self._consecutive_dbus_errors: int = 0 + + self._background_tasks: set[asyncio.Task] = set() + + self._signal_nm: dbus_nm.NetworkManager | None = None + self._signal_wifi: dbus_nm.NetworkDeviceWireless | None = None + self._signal_wired: dbus_nm.NetworkDeviceGeneric | None = None + self._signal_settings: dbus_nm.NetworkManagerSettings | None = None + + self._state_debounce_handle: asyncio.TimerHandle | None = None + self._scan_debounce_handle: asyncio.TimerHandle | None = None + + # Tracked for cancellation during shutdown. + self._listener_tasks: list[asyncio.Task] = [] + + # Asyncio loop — created here, driven on the daemon thread. + self.stop_event = asyncio.Event() + self.stop_event.clear() + self._asyncio_loop: asyncio.AbstractEventLoop = asyncio.new_event_loop() + self._asyncio_thread = threading.Thread( + target=self._run_asyncio_loop, + daemon=True, + name="NetworkManagerAsyncLoop", + ) + self._asyncio_thread.start() + + def _run_asyncio_loop(self) -> None: + """Open the system D-Bus and run the asyncio event loop on this thread.""" + asyncio.set_event_loop(self._asyncio_loop) + try: + self._system_bus = sdbus.sd_bus_open_system() + sdbus.set_default_bus(self._system_bus) + self._track_task( + self._asyncio_loop.create_task(self._async_initialize(), name="nm_init") + ) + logger.debug( + "D-Bus opened on asyncio thread '%s'", + threading.current_thread().name, + ) + except Exception as exc: + logger.error("Failed to open system D-Bus: %s", exc) + self._asyncio_loop.run_forever() + + def _track_task(self, task: asyncio.Task) -> None: + """Register a background task so it is cancelled on shutdown.""" + self._background_tasks.add(task) + task.add_done_callback(self._background_tasks.discard) + + async def _async_shutdown(self) -> None: + """Tear down all async state and stop the event loop.""" + self._running = False + + for task in self._listener_tasks: + if not task.done(): + task.cancel() + self._listener_tasks.clear() + + if self._state_debounce_handle: + self._state_debounce_handle.cancel() + self._state_debounce_handle = None + if self._scan_debounce_handle: + self._scan_debounce_handle.cancel() + self._scan_debounce_handle = None + + self._signal_nm = None + self._signal_wifi = None + self._signal_wired = None + self._signal_settings = None + + self._primary_wifi_path = "" + self._primary_wifi_iface = "" + self._primary_wired_path = "" + self._primary_wired_iface = "" + self._iface_to_device_path.clear() + self._saved_cache.clear() + + for task in list(self._background_tasks): + if not task.done(): + task.cancel() + self._background_tasks.clear() + self._system_bus = None + logger.info("NetworkManagerWorker async shutdown complete") + self._asyncio_loop.call_soon_threadsafe(self._asyncio_loop.stop) + + def _nm(self) -> dbus_nm.NetworkManager: + """Return a fresh NetworkManager root D-Bus proxy.""" + return dbus_nm.NetworkManager(bus=self._system_bus) + + def _generic(self, path: str) -> dbus_nm.NetworkDeviceGeneric: + """Return a fresh generic network device D-Bus proxy for the given path.""" + return dbus_nm.NetworkDeviceGeneric(bus=self._system_bus, device_path=path) + + def _wifi(self, path: str | None = None) -> dbus_nm.NetworkDeviceWireless: + """Return a fresh wireless device D-Bus proxy (defaults to primary Wi-Fi path).""" + return dbus_nm.NetworkDeviceWireless( + bus=self._system_bus, + device_path=path or self._primary_wifi_path, + ) + + def _wired(self, path: str | None = None) -> dbus_nm.NetworkDeviceWired: + """Return a fresh wired device D-Bus proxy (defaults to primary wired path).""" + return dbus_nm.NetworkDeviceWired( + bus=self._system_bus, + device_path=path or self._primary_wired_path, + ) + + def _get_wifi_iface_name(self) -> str: + """Return the detected Wi-Fi interface name. + + ``_primary_wifi_iface`` is set atomically with ``_primary_wifi_path`` + in ``_detect_interfaces()``. The dict-lookup and ``"wlan0"`` branches + are defensive fallbacks in case the two somehow diverge. + """ + if self._primary_wifi_iface: + return self._primary_wifi_iface + for iface, path in self._iface_to_device_path.items(): + if path == self._primary_wifi_path: + return iface + return "wlan0" # safe fallback + + def _active_conn(self, path: str) -> dbus_nm.ActiveConnection: + """Return a fresh ActiveConnection D-Bus proxy for the given path.""" + return dbus_nm.ActiveConnection(bus=self._system_bus, connection_path=path) + + def _conn_settings(self, path: str) -> dbus_nm.NetworkConnectionSettings: + """Return a fresh NetworkConnectionSettings D-Bus proxy for the given path.""" + return dbus_nm.NetworkConnectionSettings( + bus=self._system_bus, settings_path=path + ) + + def _nm_settings(self) -> dbus_nm.NetworkManagerSettings: + """Return a fresh NetworkManagerSettings D-Bus proxy.""" + return dbus_nm.NetworkManagerSettings(bus=self._system_bus) + + def _ap(self, path: str) -> dbus_nm.AccessPoint: + """Return a fresh AccessPoint D-Bus proxy for the given path.""" + return dbus_nm.AccessPoint(bus=self._system_bus, point_path=path) + + def _ipv4(self, path: str) -> dbus_nm.IPv4Config: + """Return a fresh IPv4Config D-Bus proxy for the given path.""" + return dbus_nm.IPv4Config(bus=self._system_bus, ip4_path=path) + + def _ensure_signal_proxies(self) -> None: + """Create or recreate persistent proxies for D-Bus signal listening. + + These proxies are NOT used for property reads (to avoid the + sdbus_async caching bug). They exist solely so the ``async for`` + signal iterators stay alive. Must be called on the asyncio thread. + """ + if self._signal_nm is None: + self._signal_nm = dbus_nm.NetworkManager(bus=self._system_bus) + + if self._signal_wifi is None and self._primary_wifi_path: + self._signal_wifi = dbus_nm.NetworkDeviceWireless( + bus=self._system_bus, + device_path=self._primary_wifi_path, + ) + + if self._signal_wired is None and self._primary_wired_path: + self._signal_wired = dbus_nm.NetworkDeviceGeneric( + bus=self._system_bus, + device_path=self._primary_wired_path, + ) + + if self._signal_settings is None: + self._signal_settings = dbus_nm.NetworkManagerSettings( + bus=self._system_bus, + ) + + def get_ip_by_interface(self, interface: str = "wlan0") -> str: + """Return the current IPv4 address for *interface*, blocking up to 5 s.""" + future = asyncio.run_coroutine_threadsafe( + self._get_ip_by_interface(interface), self._asyncio_loop + ) + try: + return future.result(timeout=5.0) + except Exception: + # Timeout or cancellation from the async loop; caller treats "" as unknown. + return "" + + @property + def hotspot_ssid(self) -> str: + """The SSID configured for the hotspot.""" + return self._hotspot_config.ssid + + @property + def hotspot_password(self) -> str: + """The password configured for the hotspot.""" + return self._hotspot_config.password + + async def _async_initialize(self) -> None: + """Bootstrap the worker on the asyncio thread. + + Detects network interfaces, enforces the boot-time ethernet/Wi-Fi + mutual exclusion, activates any saved VLANs if ethernet is present, + triggers an initial Wi-Fi scan, and starts all D-Bus signal listeners. + Emits ``initialized`` when done (even on failure, so the manager can + unblock its caller). + """ + try: + if not self._system_bus: + self.error_occurred.emit("initialize", "No D-Bus connection") + return + + self._running = True + await self._detect_interfaces() + await self._enforce_boot_mutual_exclusion() + + if await self._is_ethernet_connected(): + await self._activate_saved_vlans() + + self.hotspot_info_ready.emit( + self._hotspot_config.ssid, + self._hotspot_config.password, + self._hotspot_config.security, + ) + + if self._primary_wifi_path: + try: + await self._wifi().request_scan({}) + except Exception as exc: + logger.debug("Initial Wi-Fi scan request ignored: %s", exc) + + await self._start_signal_listeners() + + logger.info( + "NetworkManagerWorker initialised on thread '%s' " + "(sdbus_async, signal-reactive)", + threading.current_thread().name, + ) + self.initialized.emit() + except Exception as exc: + logger.exception("Failed to initialise NetworkManagerWorker") + self.error_occurred.emit("initialize", str(exc)) + self.initialized.emit() + + async def _detect_interfaces(self) -> None: + """Enumerate NM devices and record the primary Wi-Fi and Ethernet paths. + + Iterates all NetworkManager devices, maps interface names to D-Bus + object paths, and stores the first WIFI and ETHERNET device found as + the primary interfaces used for all subsequent operations. Emits + ``error_occurred`` if no interfaces at all are found. + """ + try: + devices = await self._nm().get_devices() + for device_path in devices: + device = self._generic(device_path) + device_type = await device.device_type + iface_name = await self._generic(device_path).interface + if iface_name: + self._iface_to_device_path[iface_name] = device_path + + if ( + device_type == dbus_nm.enums.DeviceType.WIFI + and not self._primary_wifi_path + ): + self._primary_wifi_path = device_path + self._primary_wifi_iface = iface_name + elif ( + device_type == dbus_nm.enums.DeviceType.ETHERNET + and not self._primary_wired_path + ): + self._primary_wired_path = device_path + self._primary_wired_iface = iface_name + except Exception as exc: + logger.error("Failed to detect interfaces: %s", exc) + + if not self._primary_wifi_path and not self._primary_wired_path: + # Both absent — likely D-Bus not ready yet or no hardware present. + logger.warning("No network interfaces detected after scan") + self.error_occurred.emit("wifi_unavailable", "No network device found") + elif not self._primary_wifi_path: + # Ethernet-only or Wi-Fi driver still loading — log but don't alarm. + logger.warning("No Wi-Fi interface detected; ethernet-only mode") + + async def _enforce_boot_mutual_exclusion(self) -> None: + """Disable Wi-Fi at boot if ethernet is already connected. + + Prevents the device from simultaneously using both interfaces at + startup. If ethernet is active and the Wi-Fi radio is on, the Wi-Fi + device is disconnected and the radio is disabled, then we wait up to + 8 s for the radio to confirm it is off. Failures are logged but not + propagated — a non-fatal best-effort action at boot. + """ + try: + if not await self._is_ethernet_connected(): + return + if not await self._nm().wireless_enabled: + return + logger.info("Boot: ethernet active + Wi-Fi enabled — disabling Wi-Fi") + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Pre-radio-disable disconnect ignored: %s", exc) + await self._nm().wireless_enabled.set_async(False) + await self._wait_for_wifi_radio(False, timeout=8.0) + self._is_hotspot_active = False + except Exception as exc: + logger.warning("Boot mutual exclusion failed (non-fatal): %s", exc) + + async def _start_signal_listeners(self) -> None: + """Create persistent proxies and spawn all D-Bus signal listeners. + + Each listener runs in its own Task and automatically restarts + after transient errors (with a back-off delay). + """ + self._ensure_signal_proxies() + + listeners = [ + ("nm_state", self._listen_nm_state_changed), + ("wifi_ap_added", self._listen_ap_added), + ("wifi_ap_removed", self._listen_ap_removed), + ("wired_state", self._listen_wired_state_changed), + ("wifi_state", self._listen_wifi_state_changed), + ("settings_conn_added", self._listen_settings_new_connection), + ("settings_conn_removed", self._listen_settings_connection_removed), + ] + + for name, coro_fn in listeners: + task = self._asyncio_loop.create_task( + self._resilient_listener(name, coro_fn), + name=f"listener_{name}", + ) + self._listener_tasks.append(task) + self._track_task(task) + + logger.info("Started %d D-Bus signal listeners", len(self._listener_tasks)) + + async def _resilient_listener( + self, name: str, listener_fn: "asyncio.coroutines" + ) -> None: + """Wrapper that restarts *listener_fn* on failure with back-off.""" + while self._running: + try: + await listener_fn() + except asyncio.CancelledError: + logger.debug("Listener '%s' cancelled", name) + return + except Exception as exc: + if not self._running: + return + logger.warning( + "Listener '%s' failed: %s — restarting in %.1f s", + name, + exc, + _LISTENER_RESTART_DELAY, + ) + # Rebuild signal proxies in case the bus was reset + self._signal_nm = None + self._signal_wifi = None + self._signal_wired = None + self._signal_settings = None + await asyncio.sleep(_LISTENER_RESTART_DELAY) + if self._running: + self._ensure_signal_proxies() + + async def _listen_nm_state_changed(self) -> None: + """React to NetworkManager global state transitions.""" + if not self._signal_nm: + return + logger.debug("NM StateChanged listener started") + async for state_value in self._signal_nm.state_changed: + if not self._running: + return + try: + nm_state = dbus_nm.NetworkManagerState(state_value) + logger.debug( + "NM StateChanged: %s (%d)", + nm_state.name, + state_value, + ) + except ValueError: + logger.debug("NM StateChanged: unknown (%d)", state_value) + + self._schedule_debounced_state_rebuild() + self._schedule_debounced_scan() + + async def _listen_ap_added(self) -> None: + """React to new access points appearing in scan results. + + Triggers a debounced scan rebuild (not a full rescan — NM has + already updated its internal AP list). + """ + if not self._signal_wifi: + return + logger.debug("AP Added listener started on %s", self._primary_wifi_path) + async for ap_path in self._signal_wifi.access_point_added: + if not self._running: + return + logger.debug("AP added: %s", ap_path) + self._schedule_debounced_scan() + + async def _listen_ap_removed(self) -> None: + """React to access points disappearing from scan results.""" + if not self._signal_wifi: + return + logger.debug("AP Removed listener started on %s", self._primary_wifi_path) + async for ap_path in self._signal_wifi.access_point_removed: + if not self._running: + return + logger.debug("AP removed: %s", ap_path) + self._schedule_debounced_scan() + + async def _listen_wired_state_changed(self) -> None: + """React to wired device state transitions (cable plug/unplug). + + The ``state_changed`` signal on the Device interface emits + ``(new_state, old_state, reason)`` with signature ``'uuu'``. + """ + if not self._signal_wired: + return + logger.debug("Wired state listener started on %s", self._primary_wired_path) + async for new_state, old_state, reason in self._signal_wired.state_changed: + if not self._running: + return + logger.debug( + "Wired state: %d -> %d (reason %d)", + old_state, + new_state, + reason, + ) + self._schedule_debounced_state_rebuild() + + async def _listen_wifi_state_changed(self) -> None: + """React to Wi-Fi device state transitions. + + Detects enabled/disabled, connecting, disconnected transitions + instantly — complements the NM global ``state_changed`` signal + which may not fire for all device-level transitions. + """ + if not self._signal_wifi: + return + logger.debug("Wi-Fi state listener started on %s", self._primary_wifi_path) + async for new_state, old_state, reason in self._signal_wifi.state_changed: + if not self._running: + return + logger.debug( + "Wi-Fi state: %d -> %d (reason %d)", + old_state, + new_state, + reason, + ) + self._schedule_debounced_state_rebuild() + + async def _listen_settings_new_connection(self) -> None: + """React to new saved connection profiles being added.""" + if not self._signal_settings: + return + logger.debug("Settings NewConnection listener started") + async for conn_path in self._signal_settings.new_connection: + if not self._running: + return + logger.debug("Settings: new connection %s", conn_path) + self._saved_cache_dirty = True + self._track_task( + self._asyncio_loop.create_task( + self._async_load_saved_networks(), + name="saved_on_new_connection", + ) + ) + + async def _listen_settings_connection_removed(self) -> None: + """React to saved connection profiles being deleted.""" + if not self._signal_settings: + return + logger.debug("Settings ConnectionRemoved listener started") + async for conn_path in self._signal_settings.connection_removed: + if not self._running: + return + logger.debug("Settings: connection removed %s", conn_path) + self._saved_cache_dirty = True + self._track_task( + self._asyncio_loop.create_task( + self._async_load_saved_networks(), + name="saved_on_connection_removed", + ) + ) + + def _schedule_debounced_state_rebuild(self) -> None: + """Schedule a state rebuild after a short debounce window. + + Multiple rapid D-Bus signals (e.g. during a roam or reconnect) + coalesce into a single ``_build_current_state`` call, saving + ~12-15 D-Bus round-trips per coalesced burst. + """ + if self._state_debounce_handle: + self._state_debounce_handle.cancel() + self._state_debounce_handle = self._asyncio_loop.call_later( + _DEBOUNCE_DELAY, self._fire_state_rebuild + ) + + def _fire_state_rebuild(self) -> None: + """Debounce callback — spawns the actual async state rebuild.""" + self._state_debounce_handle = None + if self._running: + self._track_task( + self._asyncio_loop.create_task( + self._async_get_current_state(), + name="debounced_state_rebuild", + ) + ) + + def _schedule_debounced_scan(self) -> None: + """Schedule a scan-results rebuild after a debounce window. + + AP Added/Removed signals can fire in rapid bursts when + entering/leaving a dense area. Coalescing prevents NxN AP + property reads. + """ + if self._scan_debounce_handle: + self._scan_debounce_handle.cancel() + self._scan_debounce_handle = self._asyncio_loop.call_later( + _DEBOUNCE_DELAY, self._fire_scan_rebuild + ) + + def _fire_scan_rebuild(self) -> None: + """Debounce callback — spawns the async scan rebuild.""" + self._scan_debounce_handle = None + if self._running: + self._track_task( + self._asyncio_loop.create_task( + self._async_scan_networks(), + name="debounced_scan_rebuild", + ) + ) + + async def _async_fallback_poll(self) -> None: + """Lightweight fallback for missed signals. + + Called at a long interval (default 60 s) by the manager. + Rebuilds state, connectivity, and saved networks. + """ + if not self._running: + return + await self._async_get_current_state() + await self._async_check_connectivity() + await self._async_load_saved_networks() + + async def _ensure_dbus_connection(self) -> bool: + """Verify the D-Bus connection is healthy, reconnecting if needed. + + Performs a lightweight ``version`` property read as a health check. + Consecutive failures increment ``_consecutive_dbus_errors``; once the + threshold is reached, opens a new system bus, re-detects interfaces, + rebuilds signal proxies, and restarts all listener tasks. Returns + ``True`` if the bus is usable (either always-healthy or successfully + reconnected), ``False`` otherwise. + """ + if not self._running: + return False + try: + _ = await self._nm().version + self._consecutive_dbus_errors = 0 + return True + except Exception as exc: + self._consecutive_dbus_errors += 1 + logger.warning( + "D-Bus health check failed (%d/%d): %s", + self._consecutive_dbus_errors, + self._MAX_DBUS_ERRORS_BEFORE_RECONNECT, + exc, + ) + if self._consecutive_dbus_errors < self._MAX_DBUS_ERRORS_BEFORE_RECONNECT: + return False + logger.warning("Attempting D-Bus reconnection...") + try: + self._system_bus = sdbus.sd_bus_open_system() + sdbus.set_default_bus(self._system_bus) + self._primary_wifi_path = "" + self._primary_wifi_iface = "" + self._primary_wired_path = "" + self._primary_wired_iface = "" + self._iface_to_device_path.clear() + await self._detect_interfaces() + # Rebuild signal proxies on new bus + self._signal_nm = None + self._signal_wifi = None + self._signal_wired = None + self._signal_settings = None + self._ensure_signal_proxies() + # Cancel stale listener tasks bound to old proxies + # and restart them on the new bus connection. + for task in self._listener_tasks: + if not task.done(): + task.cancel() + self._listener_tasks.clear() + await self._start_signal_listeners() + self._consecutive_dbus_errors = 0 + logger.info("D-Bus reconnection succeeded") + if self._primary_wifi_path or self._primary_wired_path: + self.error_occurred.emit( + "device_reconnected", "Network device reconnected" + ) + return True + except Exception as re_err: + logger.error("D-Bus reconnection failed: %s", re_err) + return False + + async def _is_ethernet_connected(self) -> bool: + """Return True if the primary wired device is fully activated (state 100).""" + if not self._primary_wired_path: + return False + try: + return await self._generic(self._primary_wired_path).state == 100 + except Exception as exc: + logger.debug("Error checking ethernet state: %s", exc) + return False + + async def _has_ethernet_carrier(self) -> bool: + """Return True if the primary wired device has a physical link (state >= 30). + + State 30 is DISCONNECTED in NM's device state enum, which still implies + a cable is present. This is a weaker check than ``_is_ethernet_connected`` + and is used to populate ``NetworkState.ethernet_carrier`` for UI feedback. + """ + if not self._primary_wired_path: + return False + try: + return await self._generic(self._primary_wired_path).state >= 30 + except Exception: + # D-Bus read failed; carrier state unknown — treat as no carrier. + return False + + async def _wait_for_wifi_radio(self, desired: bool, timeout: float = 3.0) -> bool: + """Poll NM wireless_enabled until it matches *desired* or *timeout* expires.""" + loop = asyncio.get_running_loop() + deadline = loop.time() + timeout + _logged = False + while loop.time() < deadline: + try: + if await self._nm().wireless_enabled == desired: + return True + except Exception as exc: + if not _logged: + logger.debug("Polling wireless_enabled failed: %s", exc) + _logged = True + await asyncio.sleep(0.25) + return False + + async def _wait_for_wifi_device_ready(self, timeout: float = 8.0) -> bool: + """Poll wlan0 device state until it reaches DISCONNECTED (30) or above.""" + if not self._primary_wifi_path: + return False + loop = asyncio.get_running_loop() + deadline = loop.time() + timeout + _logged = False + while loop.time() < deadline: + try: + if await self._generic(self._primary_wifi_path).state >= 30: + return True + except Exception as exc: + if not _logged: + logger.debug("Polling Wi-Fi device state failed: %s", exc) + _logged = True + await asyncio.sleep(0.25) + return False + + async def _async_get_current_state(self) -> None: + """Rebuild and emit the full NetworkState, enforcing runtime mutual exclusion.""" + try: + if not await self._ensure_dbus_connection(): + self.state_changed.emit(NetworkState()) + return + state = await self._build_current_state() + if ( + state.ethernet_connected + and state.wifi_enabled + and not state.hotspot_enabled + and not self._is_hotspot_active + ): + logger.info( + "Runtime mutual exclusion: ethernet active + " + "Wi-Fi — disabling Wi-Fi" + ) + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Disconnect before Wi-Fi disable ignored: %s", exc) + await self._nm().wireless_enabled.set_async(False) + await asyncio.sleep(0.5) + state = await self._build_current_state() + self.state_changed.emit(state) + except Exception as exc: + logger.error("Failed to get current state: %s", exc) + self.error_occurred.emit("get_current_state", str(exc)) + + @staticmethod + def _get_ip_os_fallback(iface: str) -> str: + """Return the IPv4 address for *iface* via a raw ioctl SIOCGIFADDR call. + + Used as a fallback when the NM D-Bus IPv4Config path returns nothing — + common immediately after DHCP on slower hardware. + """ + if not iface: + return "" + _SIOCGIFADDR = 0x8915 + try: + with _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM) as sock: + ifreq = struct.pack("256s", iface[:15].encode()) + result = fcntl.ioctl(sock.fileno(), _SIOCGIFADDR, ifreq) + return _socket.inet_ntoa(result[20:24]) + except Exception: + # ioctl fails when the interface has no address; caller treats "" as unknown. + return "" + + async def _build_current_state(self) -> NetworkState: + """Read all relevant NM properties and assemble a NetworkState snapshot.""" + if not self._system_bus: + return NetworkState() + try: + connectivity_value = await self._nm().check_connectivity() + connectivity = self._map_connectivity(connectivity_value) + wifi_enabled = bool(await self._nm().wireless_enabled) + current_ssid = await self._get_current_ssid() + + eth_connected = await self._is_ethernet_connected() + if eth_connected: + current_ip = await self._get_ip_by_interface( + self._primary_wired_iface or "eth0" + ) + if not current_ip: + current_ip = self._get_ip_os_fallback( + self._primary_wired_iface or "eth0" + ) + current_ssid = "" + elif current_ssid: + current_ip = await self._get_ip_by_interface("wlan0") + if not current_ip: + current_ip = await self._get_current_ip() + else: + current_ip = "" + + if not current_ip and connectivity in ( + ConnectivityState.FULL, + ConnectivityState.LIMITED, + ): + for _iface in ( + self._primary_wired_iface or "eth0", + "wlan0", + ): + _fallback = self._get_ip_os_fallback(_iface) + if _fallback: + current_ip = _fallback + if _iface != "wlan0": + eth_connected = True + logger.debug("OS fallback IP for '%s': %s", _iface, _fallback) + break + + signal = 0 + sec_type = "" + if current_ssid: + signal_map = await self._build_signal_map() + signal = signal_map.get(current_ssid.lower(), 0) + saved = await self._get_saved_network_cached(current_ssid) + sec_type = saved.security_type if saved else "" + + hotspot_enabled = current_ssid == self._hotspot_config.ssid + + if not hotspot_enabled and self._is_hotspot_active and not current_ssid: + hotspot_enabled = True + current_ssid = self._hotspot_config.ssid + logger.debug( + "Hotspot SSID not found via D-Bus, using config: '%s'", + current_ssid, + ) + + if hotspot_enabled: + sec_type = self._hotspot_config.security + if not current_ip: + current_ip = await self._get_ip_by_interface("wlan0") + + return NetworkState( + connectivity=connectivity, + current_ssid=current_ssid, + current_ip=current_ip, + wifi_enabled=wifi_enabled, + hotspot_enabled=hotspot_enabled, + signal_strength=signal, + security_type=sec_type, + ethernet_connected=eth_connected, + ethernet_carrier=await self._has_ethernet_carrier(), + active_vlans=await self._get_active_vlans(), + ) + except Exception as exc: + logger.error("Error building current state: %s", exc) + return NetworkState() + + @staticmethod + def _map_connectivity(value: int) -> ConnectivityState: + """Map a raw NM connectivity integer to a ConnectivityState enum member.""" + try: + return ConnectivityState(value) + except ValueError: + return ConnectivityState.UNKNOWN + + async def _async_check_connectivity(self) -> None: + """Query NM connectivity and emit connectivity_changed.""" + try: + if not self._system_bus: + self.connectivity_changed.emit(ConnectivityState.UNKNOWN) + return + self.connectivity_changed.emit( + self._map_connectivity(await self._nm().check_connectivity()) + ) + except Exception as exc: + logger.error("Failed to check connectivity: %s", exc) + self.connectivity_changed.emit(ConnectivityState.UNKNOWN) + + async def _get_current_ssid(self) -> str: + """Return the SSID of the currently active Wi-Fi connection, or empty string.""" + try: + primary_con = await self._nm().primary_connection + if primary_con and primary_con != "/": + ssid = await self._ssid_from_active_connection(primary_con) + if ssid: + return ssid + return await self._get_ssid_from_any_active() + except Exception as exc: + logger.debug("Error getting current SSID: %s", exc) + return "" + + async def _ssid_from_active_connection(self, active_path: str) -> str: + """Extract the Wi-Fi SSID from an active connection object path, or return ''.""" + try: + conn_path = await self._active_conn(active_path).connection + if not conn_path or conn_path == "/": + return "" + settings = await self._conn_settings(conn_path).get_settings() + if "802-11-wireless" in settings: + ssid = settings["802-11-wireless"]["ssid"][1].decode() + return ssid + except Exception as exc: + logger.debug( + "Error reading active connection %s: %s", + active_path, + exc, + ) + return "" + + async def _get_ssid_from_any_active(self) -> str: + """Scan all active NM connections and return the first Wi-Fi SSID found.""" + try: + active_paths = await self._nm().active_connections + for active_path in active_paths: + ssid = await self._ssid_from_active_connection(active_path) + if ssid: + return ssid + except Exception as exc: + logger.debug("Error scanning active connections: %s", exc) + return "" + + async def _get_current_ip(self) -> str: + """Return the IPv4 address from the primary NM connection's IP4Config.""" + try: + primary_con = await self._nm().primary_connection + if primary_con == "/": + return "" + ip4_path = await self._active_conn(primary_con).ip4_config + if ip4_path == "/": + return "" + addr_data = await self._ipv4(ip4_path).address_data + if addr_data: + return addr_data[0]["address"][1] + return "" + except Exception as exc: + logger.debug("Error getting current IP: %s", exc) + return "" + + async def _get_ip_by_interface(self, interface: str = "wlan0") -> str: + """Return the IPv4 address assigned to *interface* via NM's IP4Config D-Bus object.""" + try: + device_path = self._iface_to_device_path.get(interface) + if not device_path: + devices = await self._nm().get_devices() + for dp in devices: + if await self._generic(dp).interface == interface: + device_path = dp + self._iface_to_device_path[interface] = dp + break + if not device_path: + return "" + ip4_path = await self._generic(device_path).ip4_config + if not ip4_path or ip4_path == "/": + return "" + addr_data = await self._ipv4(ip4_path).address_data + if addr_data: + return addr_data[0]["address"][1] + return "" + except Exception as exc: + logger.error("Failed to get IP for %s: %s", interface, exc) + return "" + + async def _async_scan_networks(self) -> None: + """Request an NM rescan, parse visible APs, and emit networks_scanned.""" + try: + if not self._primary_wifi_path: + self.networks_scanned.emit([]) + return + if not await self._ensure_dbus_connection(): + self.networks_scanned.emit([]) + return + + if not await self._nm().wireless_enabled: + self.networks_scanned.emit([]) + return + + try: + await self._wifi().request_scan({}) + except Exception as exc: + logger.debug( + "Scan request ignored (already scanning or radio off): %s", exc + ) + + if await self._wifi().last_scan == -1: + self.networks_scanned.emit([]) + return + + ap_paths = await self._wifi().get_all_access_points() + current_ssid = await self._get_current_ssid() + saved_ssids = set(await self._get_saved_ssid_names_cached()) + + networks: list[NetworkInfo] = [] + seen_ssids: set[str] = set() + + for ap_path in ap_paths: + try: + info = await self._parse_ap(ap_path, current_ssid, saved_ssids) + if ( + info + and info.ssid not in seen_ssids + and not is_hidden_ssid(info.ssid) + and (info.signal_strength > 0 or info.is_active) + ): + networks.append(info) + seen_ssids.add(info.ssid) + except Exception as exc: + logger.debug("Failed to parse AP %s: %s", ap_path, exc) + + networks.sort(key=lambda n: (-n.network_status, -n.signal_strength)) + self.networks_scanned.emit(networks) + + except Exception as exc: + logger.error("Failed to scan networks: %s", exc) + self.error_occurred.emit("scan_networks", str(exc)) + self.networks_scanned.emit([]) + + async def _get_all_ap_properties(self, ap_path: str) -> dict[str, object]: + """Fetch all D-Bus properties for an AccessPoint in one round-trip.""" + try: + return await self._ap(ap_path).properties_get_all_dict( + on_unknown_member="ignore" + ) + except Exception as exc: + logger.debug("GetAll failed for AP %s: %s", ap_path, exc) + return {} + + async def _build_signal_map(self) -> dict[str, int]: + """Return a mapping of lowercase SSID to best-seen signal strength (0-100).""" + signal_map: dict[str, int] = {} + if not self._primary_wifi_path: + return signal_map + try: + ap_paths = await self._wifi().access_points + for ap_path in ap_paths: + try: + props = await self._get_all_ap_properties(ap_path) + ssid = self._decode_ssid(props.get("ssid", b"")) + if ssid: + strength = int(props.get("strength", 0)) + key = ssid.lower() + if strength > signal_map.get(key, 0): + signal_map[key] = strength + except Exception as exc: + logger.debug("Skipping AP in signal map: %s", exc) + continue + except Exception as exc: + logger.debug("Error building signal map: %s", exc) + return signal_map + + async def _parse_ap( + self, ap_path: str, current_ssid: str, saved_ssids: set + ) -> NetworkInfo | None: + """Parse an AccessPoint D-Bus object into a NetworkInfo, or None if unusable.""" + props = await self._get_all_ap_properties(ap_path) + if not props: + return None + + ssid = self._decode_ssid(props.get("ssid", b"")) + if not ssid or is_hidden_ssid(ssid): + return None + + flags = int(props.get("flags", 0)) + wpa_flags = int(props.get("wpa_flags", 0)) + rsn_flags = int(props.get("rsn_flags", 0)) + is_open = (flags & 1) == 0 + + security = self._determine_security_type(flags, wpa_flags, rsn_flags) + if not is_connectable_security(security): + return None + + is_active = ssid == current_ssid + is_saved = ssid in saved_ssids + if is_active: + net_status = NetworkStatus.ACTIVE + elif is_saved: + net_status = NetworkStatus.SAVED + elif is_open: + net_status = NetworkStatus.OPEN + else: + net_status = NetworkStatus.DISCOVERED + + return NetworkInfo( + ssid=ssid, + signal_strength=int(props.get("strength", 0)), + network_status=net_status, + bssid=str(props.get("hw_address", "")), + frequency=int(props.get("frequency", 0)), + max_bitrate=int(props.get("max_bitrate", 0)), + security_type=security, + ) + + @staticmethod + def _decode_ssid(raw: object) -> str: + """Decode a raw SSID byte string to a UTF-8 str, replacing invalid bytes.""" + if isinstance(raw, bytes): + return raw.decode("utf-8", errors="replace") + return str(raw) if raw else "" + + @staticmethod + def _determine_security_type( + flags: int, wpa_flags: int, rsn_flags: int + ) -> SecurityType: + """Determine the Wi-Fi SecurityType from AP capability flags.""" + if (flags & 1) == 0: + return SecurityType.OPEN + if rsn_flags: + if rsn_flags & 0x400: + return SecurityType.WPA3_SAE + if rsn_flags & 0x200: + return SecurityType.WPA_EAP + return SecurityType.WPA2_PSK + if wpa_flags: + if wpa_flags & 0x200: + return SecurityType.WPA_EAP + return SecurityType.WPA_PSK + return SecurityType.WEP + + def _invalidate_saved_cache(self) -> None: + """Mark the saved-networks cache as dirty so it is rebuilt on next access.""" + self._saved_cache_dirty = True + + async def _get_saved_ssid_names_cached(self) -> list[str]: + """Return SSID names for all saved Wi-Fi profiles, refreshing cache if dirty.""" + if self._saved_cache_dirty: + self._saved_cache = await self._get_saved_networks_impl() + self._saved_cache_dirty = False + return [n.ssid for n in self._saved_cache] + + async def _get_saved_network_cached(self, ssid: str) -> SavedNetwork | None: + """Return the SavedNetwork for *ssid* from cache (case-insensitive), or None.""" + if self._saved_cache_dirty: + self._saved_cache = await self._get_saved_networks_impl() + self._saved_cache_dirty = False + ssid_lower = ssid.lower() + for n in self._saved_cache: + if n.ssid.lower() == ssid_lower: + return n + return None + + async def _async_load_saved_networks(self) -> None: + """Reload all saved Wi-Fi profiles and emit saved_networks_loaded.""" + try: + networks = await self._get_saved_networks_impl() + self._saved_cache = networks + self._saved_cache_dirty = False + self.saved_networks_loaded.emit(networks) + except Exception as exc: + logger.error("Failed to load saved networks: %s", exc) + self.error_occurred.emit("load_saved_networks", str(exc)) + self.saved_networks_loaded.emit([]) + + async def _get_saved_networks_impl(self) -> list[SavedNetwork]: + """Enumerate NM connection profiles and return infrastructure Wi-Fi ones.""" + if not self._system_bus: + return [] + try: + connections = await self._nm_settings().list_connections() + signal_map = await self._build_signal_map() + saved: list[SavedNetwork] = [] + + for conn_path in connections: + try: + settings = await self._conn_settings(conn_path).get_settings() + if settings["connection"]["type"][1] != "802-11-wireless": + continue + + wireless = settings["802-11-wireless"] + ssid = wireless["ssid"][1].decode() + uuid = settings["connection"]["uuid"][1] + mode = str(wireless.get("mode", (None, "infrastructure"))[1]) + + security_key = str(wireless.get("security", (None, ""))[1]) + sec_type = "" + if security_key and security_key in settings: + sec_type = settings[security_key].get("key-mgmt", (None, ""))[1] + + priority = settings["connection"].get( + "autoconnect-priority", + (None, ConnectionPriority.MEDIUM.value), + )[1] + timestamp = settings["connection"].get("timestamp", (None, 0))[1] + signal = signal_map.get(ssid.lower(), 0) + ipv4_method = settings.get("ipv4", {}).get( + "method", (None, "auto") + )[1] + is_dhcp = ipv4_method != "manual" + + saved.append( + SavedNetwork( + ssid=ssid, + uuid=uuid, + connection_path=conn_path, + security_type=sec_type, + mode=mode, + priority=priority or ConnectionPriority.MEDIUM.value, + signal_strength=signal, + timestamp=int(timestamp or 0), + is_dhcp=is_dhcp, + ) + ) + except Exception as exc: + logger.debug("Failed to parse connection: %s", exc) + + return saved + except Exception as exc: + logger.error("Error getting saved networks: %s", exc) + return [] + + async def _is_known(self, ssid: str) -> bool: + """Return True if a saved profile for *ssid* exists in the cache.""" + return await self._get_saved_network_cached(ssid) is not None + + async def _get_connection_path(self, ssid: str) -> str | None: + """Return the D-Bus connection path for a saved *ssid* profile, or None.""" + saved = await self._get_saved_network_cached(ssid) + return saved.connection_path if saved else None + + async def _async_add_network(self, ssid: str, password: str, priority: int) -> None: + """Add and activate a new Wi-Fi profile, emitting connection_result when done.""" + try: + result = await self._add_network_impl(ssid, password, priority) + self._invalidate_saved_cache() + self.connection_result.emit(result) + except Exception as exc: + logger.error("Failed to add network: %s", exc) + self.connection_result.emit( + ConnectionResult( + success=False, + message=str(exc), + error_code="add_failed", + ) + ) + + async def _add_network_impl( + self, ssid: str, password: str, priority: int + ) -> ConnectionResult: + """Scan for the SSID, build a connection profile, add it to NM, and activate it. + + Deletes any pre-existing profile for the same SSID before adding. + Returns a failed ConnectionResult if the SSID is not visible, the + security type is unsupported, or the 20-second activation wait times out. + """ + if not self._primary_wifi_path or not self._system_bus: + return ConnectionResult(False, "No Wi-Fi interface", "no_interface") + + if await self._is_known(ssid): + await self._delete_network_impl(ssid) + self._invalidate_saved_cache() + + try: + await self._wifi().request_scan({}) + except Exception as exc: + logger.debug("Pre-connect scan request ignored: %s", exc) + + ap_paths = await self._wifi().get_all_access_points() + target_ap_path: str | None = None + target_ap_props: dict[str, object] = {} + for ap_path in ap_paths: + props = await self._get_all_ap_properties(ap_path) + if self._decode_ssid(props.get("ssid", b"")) == ssid: + target_ap_path = ap_path + target_ap_props = props + break + + if not target_ap_path: + return ConnectionResult(False, f"Network '{ssid}' not found", "not_found") + + interface = await self._wifi().interface + conn_props = self._build_connection_properties( + ssid, password, interface, priority, target_ap_props + ) + if not conn_props: + return ConnectionResult( + False, + "Unsupported security type", + "unsupported_security", + ) + + try: + nm_settings = self._nm_settings() + conn_path = await nm_settings.add_connection(conn_props) + except Exception as exc: + err_str = str(exc).lower() + if "psk" in err_str and ("invalid" in err_str or "property" in err_str): + return ConnectionResult( + False, + "Wrong password, try again.", + "invalid_password", + ) + return ConnectionResult(False, str(exc), "add_failed") + + if _CAN_RELOAD_CONNECTIONS: + try: + await self._nm_settings().reload_connections() + except Exception as reload_err: + logger.debug("reload_connections non-fatal: %s", reload_err) + + try: + await self._nm().activate_connection(conn_path) + if not await self._wait_for_connection(ssid, timeout=_WIFI_CONNECT_TIMEOUT): + await self._delete_network_impl(ssid) + self._invalidate_saved_cache() + return ConnectionResult( + False, + f"Authentication failed for '{ssid}'.\n" + "The saved profile has been removed.\n" + "Please check the password and try again.", + "auth_failed", + ) + return ConnectionResult(True, f"Network '{ssid}' added and connecting") + except Exception as act_err: + logger.warning("Activate after add failed: %s", act_err) + return ConnectionResult(True, f"Network '{ssid}' added (activate manually)") + + def _build_connection_properties( + self, + ssid: str, + password: str, + interface: str, + priority: int, + ap_props: dict[str, object], + ) -> dict[str, object] | None: + """Build NM connection property dict for *ssid* from its AP capability flags. + + Returns None if the security type is unsupported (e.g. WPA-EAP). + Handles OPEN, WPA-PSK, WPA2-PSK, and WPA3-SAE (including SAE-transition). + """ + flags = int(ap_props.get("flags", 0)) + wpa_flags = int(ap_props.get("wpa_flags", 0)) + rsn_flags = int(ap_props.get("rsn_flags", 0)) + + props: dict[str, object] = { + "connection": { + "id": ("s", ssid), + "uuid": ("s", str(uuid4())), + "type": ("s", "802-11-wireless"), + "interface-name": ("s", interface), + "autoconnect": ("b", True), + "autoconnect-priority": ("i", priority), + }, + "802-11-wireless": { + "mode": ("s", "infrastructure"), + "ssid": ("ay", ssid.encode("utf-8")), + }, + "ipv4": { + "method": ("s", "auto"), + "route-metric": ("i", 200), + }, + "ipv6": {"method": ("s", "auto")}, + } + + if (flags & 1) == 0: + return props + + props["802-11-wireless"]["security"] = ( + "s", + "802-11-wireless-security", + ) + security = self._determine_security_type(flags, wpa_flags, rsn_flags) + + if not is_connectable_security(security): + logger.warning( + "Rejecting connection to '%s': unsupported security %s", + ssid, + security.value, + ) + return None + + if security == SecurityType.WPA3_SAE: + has_psk = bool((rsn_flags & 0x100) or wpa_flags) + if has_psk: + logger.debug( + "SAE transition for '%s' — using wpa-psk + PMF optional", + ssid, + ) + props["802-11-wireless-security"] = { + "key-mgmt": ("s", "wpa-psk"), + "auth-alg": ("s", "open"), + "psk": ("s", password), + "pmf": ("u", 2), # OPTIONAL — required for SAE-transition APs + } + else: + logger.debug("Pure SAE detected for '%s'", ssid) + props["802-11-wireless-security"] = { + "key-mgmt": ("s", "sae"), + "auth-alg": ("s", "open"), + "psk": ("s", password), + "pmf": ("u", 3), # REQUIRED — mandatory for pure WPA3-SAE + } + elif security in ( + SecurityType.WPA2_PSK, + SecurityType.WPA_PSK, + ): + props["802-11-wireless-security"] = { + "key-mgmt": ("s", "wpa-psk"), + "auth-alg": ("s", "open"), + "psk": ("s", password), + } + else: + logger.warning( + "Unsupported security type '%s' for '%s'", + security.value, + ssid, + ) + return None + + return props + + async def _async_connect_network(self, ssid: str) -> None: + """Activate an existing saved Wi-Fi profile and emit connection_result.""" + try: + self._is_hotspot_active = False + result = await self._connect_network_impl(ssid) + self.connection_result.emit(result) + self.state_changed.emit(await self._build_current_state()) + except Exception as exc: + logger.error("Failed to connect: %s", exc) + self.connection_result.emit( + ConnectionResult( + success=False, + message=str(exc), + error_code="connect_failed", + ) + ) + + async def _wait_for_connection( + self, ssid: str, timeout: float = _WIFI_CONNECT_TIMEOUT + ) -> bool: + """Poll until *ssid* is active and has an IP, or until *timeout* expires. + + Starts with a 1.5 s initial delay to let NM begin the association. + Returns False early if the SSID disappears for 3 consecutive polls. + """ + loop = asyncio.get_running_loop() + deadline = loop.time() + timeout + await asyncio.sleep(1.5) + consecutive_empty = 0 + while loop.time() < deadline: + try: + current = await self._get_current_ssid() + if current and current.lower() == ssid.lower(): + ip = await self._get_current_ip() + if ip: + return True + consecutive_empty = 0 + else: + consecutive_empty += 1 + if consecutive_empty >= 3: + return False + except Exception as exc: + logger.debug("Connection wait poll failed: %s", exc) + await asyncio.sleep(0.5) + return False + + async def _connect_network_impl(self, ssid: str) -> ConnectionResult: + """Enable Wi-Fi if needed, locate the saved profile, and activate it.""" + if not self._system_bus: + return ConnectionResult(False, "NetworkManager unavailable", "no_nm") + + if not await self._nm().wireless_enabled: + await self._nm().wireless_enabled.set_async(True) + if not await self._wait_for_wifi_radio(True, timeout=8.0): + return ConnectionResult( + False, + "Wi-Fi radio failed to turn on.\nPlease try again.", + "radio_failed", + ) + await self._wait_for_wifi_device_ready(timeout=8.0) + + conn_path = await self._get_connection_path(ssid) + if not conn_path: + conn_path = await self._find_connection_path_direct(ssid) + if not conn_path: + return ConnectionResult(False, f"Network '{ssid}' not saved", "not_found") + + try: + await self._nm().activate_connection(conn_path) + if not await self._wait_for_connection(ssid, timeout=_WIFI_CONNECT_TIMEOUT): + return ConnectionResult( + False, + f"Could not connect to '{ssid}'.\n" + "Please check signal strength and try again.", + "connect_timeout", + ) + return ConnectionResult(True, f"Connected to '{ssid}'") + except Exception as exc: + return ConnectionResult(False, str(exc), "connect_failed") + + async def _find_connection_path_direct(self, ssid: str) -> str | None: + """Search NM settings for an infrastructure profile matching *ssid* directly.""" + try: + connections = await self._nm_settings().list_connections() + for conn_path in connections: + try: + settings = await self._conn_settings(conn_path).get_settings() + if settings["connection"]["type"][1] != "802-11-wireless": + continue + conn_ssid = settings["802-11-wireless"]["ssid"][1].decode() + if conn_ssid.lower() == ssid.lower(): + self._invalidate_saved_cache() + return conn_path + except Exception as exc: + logger.debug("Skipping connection in path lookup: %s", exc) + continue + except Exception as exc: + logger.debug("Direct connection path lookup failed: %s", exc) + return None + + async def _async_disconnect(self) -> None: + """Disconnect the primary Wi-Fi device and emit connection_result.""" + try: + if self._primary_wifi_path: + await self._wifi().disconnect() + self.connection_result.emit(ConnectionResult(True, "Disconnected")) + except Exception as exc: + logger.error("Disconnect failed: %s", exc) + self.connection_result.emit( + ConnectionResult( + success=False, + message=str(exc), + error_code="disconnect_failed", + ) + ) + + async def _async_delete_network(self, ssid: str) -> None: + """Delete the saved profile for *ssid* and emit connection_result.""" + try: + result = await self._delete_network_impl(ssid) + self._invalidate_saved_cache() + self.connection_result.emit(result) + if result.success: + self.state_changed.emit(await self._build_current_state()) + except Exception as exc: + logger.error("Delete failed: %s", exc) + self.connection_result.emit( + ConnectionResult( + success=False, + message=str(exc), + error_code="delete_failed", + ) + ) + + async def _delete_network_impl(self, ssid: str) -> ConnectionResult: + """Delete the NM connection profile for *ssid* and disconnect if it is active.""" + conn_path = await self._get_connection_path(ssid) + if not conn_path: + return ConnectionResult(False, f"Network '{ssid}' not found", "not_found") + try: + await self._conn_settings(conn_path).delete() + + if _CAN_RELOAD_CONNECTIONS: + try: + await self._nm_settings().reload_connections() + except Exception as reload_err: + logger.debug("reload_connections non-fatal: %s", reload_err) + + current_ssid = await self._get_current_ssid() + if current_ssid and current_ssid.lower() == ssid.lower(): + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Disconnect after network delete ignored: %s", exc) + + return ConnectionResult(True, f"Network '{ssid}' deleted") + except Exception as exc: + return ConnectionResult(False, str(exc), "delete_failed") + + async def _async_update_network( + self, + ssid: str, + password: str = "", + priority: int = 0, + ) -> None: + """Update password and/or priority for a saved profile and emit connection_result.""" + try: + result = await self._update_network_impl( + ssid, + password or None, + priority if priority != 0 else None, + ) + self._invalidate_saved_cache() + self.connection_result.emit(result) + except Exception as exc: + logger.error("Update failed: %s", exc) + self.connection_result.emit( + ConnectionResult( + success=False, + message=str(exc), + error_code="update_failed", + ) + ) + + async def _update_network_impl( + self, + ssid: str, + password: str | None, + priority: int | None, + ) -> ConnectionResult: + """Merge updated password/priority into the existing NM connection settings.""" + conn_path = await self._get_connection_path(ssid) + if not conn_path: + return ConnectionResult(False, f"Network '{ssid}' not found", "not_found") + try: + cs = self._conn_settings(conn_path) + props = await cs.get_settings() + await self._merge_wifi_secrets(cs, props) + + if password and "802-11-wireless-security" in props: + props["802-11-wireless-security"]["psk"] = ( + "s", + password, + ) + + if priority is not None: + props["connection"]["autoconnect-priority"] = ( + "i", + priority, + ) + logger.debug("Setting priority for '%s' to %d", ssid, priority) + + await cs.update(props) + logger.debug("Network '%s' update() succeeded", ssid) + return ConnectionResult(True, f"Network '{ssid}' updated") + except Exception as exc: + logger.error("Update failed for '%s': %s", ssid, exc) + err_str = str(exc).lower() + if "psk" in err_str and ("invalid" in err_str or "property" in err_str): + return ConnectionResult( + False, + "Wrong password, try again.", + "invalid_password", + ) + return ConnectionResult(False, str(exc), "update_failed") + + async def _async_set_wifi_enabled(self, enabled: bool) -> None: + """Enable or disable the Wi-Fi radio, handling ethernet mutual exclusion.""" + try: + if not self._system_bus: + return + if not enabled: + self._is_hotspot_active = False + + if enabled and await self._is_ethernet_connected(): + await self._async_disconnect_ethernet() + + current = await self._nm().wireless_enabled + if current != enabled: + if not enabled: + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug( + "Disconnect before Wi-Fi toggle ignored: %s", exc + ) + await asyncio.sleep(0.5) + + await self._nm().wireless_enabled.set_async(enabled) + + if not await self._wait_for_wifi_radio(enabled, timeout=8.0): + logger.warning( + "Wi-Fi radio did not reach %s within 8 s", + "enabled" if enabled else "disabled", + ) + + self.connection_result.emit( + ConnectionResult( + True, + f"Wi-Fi {'enabled' if enabled else 'disabled'}", + ) + ) + self.state_changed.emit(await self._build_current_state()) + except Exception as exc: + logger.error("Failed to toggle Wi-Fi: %s", exc) + self.error_occurred.emit("set_wifi_enabled", str(exc)) + + async def _async_disconnect_ethernet(self) -> None: + """Deactivate all VLANs, disconnect ethernet, and wait up to 4 s for teardown.""" + if not self._primary_wired_path: + return + try: + await self._deactivate_all_vlans() + await self._wired().disconnect() + loop = asyncio.get_running_loop() + deadline = loop.time() + 4.0 + while loop.time() < deadline: + await asyncio.sleep(0.5) + if not await self._is_ethernet_connected(): + break + logger.info("Ethernet disconnected") + except Exception as exc: + logger.error("Failed to disconnect ethernet: %s", exc) + + async def _async_connect_ethernet(self) -> None: + """Disable Wi-Fi/hotspot, activate the wired device, and restore saved VLANs.""" + if not self._primary_wired_path: + self.error_occurred.emit("connect_ethernet", "No wired device found") + return + try: + if self._is_hotspot_active: + await self._async_toggle_hotspot(False) + + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Pre-VLAN disconnect ignored: %s", exc) + await asyncio.sleep(0.5) + + if await self._nm().wireless_enabled: + await self._nm().wireless_enabled.set_async(False) + await self._wait_for_wifi_radio(False, timeout=8.0) + + await self._nm().activate_connection("/", self._primary_wired_path, "/") + await asyncio.sleep(1.5) + + await self._activate_saved_vlans() + logger.info("Ethernet connection activated") + self.connection_result.emit(ConnectionResult(True, "Ethernet connected")) + self.state_changed.emit(await self._build_current_state()) + except Exception as exc: + logger.error("Failed to connect ethernet: %s", exc) + self.error_occurred.emit("connect_ethernet", str(exc)) + self.state_changed.emit(await self._build_current_state()) + + async def _async_create_vlan( + self, + vlan_id: int, + ip_address: str, + subnet_mask: str, + gateway: str, + dns1: str, + dns2: str, + ) -> None: + """Create and activate a VLAN connection with a static IP on the primary wired interface. + + Emits connection_result and state_changed when done. + """ + if not self._primary_wired_path: + self.error_occurred.emit("create_vlan", "No wired device") + return + try: + if self._is_hotspot_active: + await self._async_toggle_hotspot(False) + + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Pre-VLAN disconnect ignored: %s", exc) + await asyncio.sleep(0.5) + + if await self._nm().wireless_enabled: + await self._nm().wireless_enabled.set_async(False) + await self._wait_for_wifi_radio(False, timeout=8.0) + + if not await self._is_ethernet_connected(): + await self._nm().activate_connection("/", self._primary_wired_path, "/") + await asyncio.sleep(1.5) + + iface = self._primary_wired_iface or "eth0" + + try: + existing_conns = await self._nm_settings().list_connections() + for existing_path in existing_conns: + try: + s = await self._conn_settings(existing_path).get_settings() + if ( + s.get("connection", {}).get("type", (None, ""))[1] == "vlan" + and s.get("vlan", {}).get("id", (None, -1))[1] == vlan_id + and s.get("vlan", {}).get("parent", (None, ""))[1] == iface + ): + self.connection_result.emit( + ConnectionResult( + False, + f"VLAN {vlan_id} already exists on " + f"{iface}.\nRemove it first before " + "creating a new one.", + "duplicate_vlan", + ) + ) + return + except Exception as exc: + logger.debug( + "Skipping connection in duplicate VLAN check: %s", exc + ) + continue + except Exception as dup_err: + logger.debug( + "Duplicate VLAN check failed (non-fatal): %s", + dup_err, + ) + + vlan_conn_id = f"VLAN {vlan_id}" + + if await self._deactivate_connection_by_id(vlan_conn_id): + await asyncio.sleep(1.0) + + await self._delete_all_connections_by_id(vlan_conn_id) + await asyncio.sleep(0.5) + + prefix = self._mask_to_prefix(subnet_mask) + ip_uint = self._ip_to_nm_uint32(ip_address) + gw_uint = self._ip_to_nm_uint32(gateway) if gateway else 0 + dns_list: list[int] = [] + if dns1: + dns_list.append(self._ip_to_nm_uint32(dns1)) + if dns2: + dns_list.append(self._ip_to_nm_uint32(dns2)) + + conn_props: dict[str, object] = { + "connection": { + "id": ("s", vlan_conn_id), + "uuid": ("s", str(uuid4())), + "type": ("s", "vlan"), + "autoconnect": ("b", False), + }, + "vlan": { + "id": ("u", vlan_id), + "parent": ("s", iface), + }, + "ipv4": { + "method": ("s", "manual"), + "addresses": ( + "aau", + [[ip_uint, prefix, gw_uint]], + ), + "gateway": ("s", gateway or ""), + "dns": ("au", dns_list), + "route-metric": ("i", 500), + }, + "ipv6": {"method": ("s", "ignore")}, + } + + conn_path = await self._nm_settings().add_connection(conn_props) + await self._nm().activate_connection(conn_path, "/", "/") + self.state_changed.emit(await self._build_current_state()) + await asyncio.sleep(1.5) + + self.connection_result.emit( + ConnectionResult(True, f"VLAN {vlan_id} connected") + ) + self.state_changed.emit(await self._build_current_state()) + except Exception as exc: + logger.error("Failed to create VLAN %d: %s", vlan_id, exc) + self.error_occurred.emit("create_vlan", str(exc)) + self.state_changed.emit(await self._build_current_state()) + + async def _async_delete_vlan(self, vlan_id: int) -> None: + """Delete all NM connection profiles for *vlan_id* and emit connection_result.""" + try: + deleted = await self._delete_all_connections_by_id(f"VLAN {vlan_id}") + logger.info( + "Deleted %d VLAN profile(s) for VLAN %d", + deleted, + vlan_id, + ) + self.connection_result.emit( + ConnectionResult(True, f"VLAN {vlan_id} removed") + ) + self.state_changed.emit(await self._build_current_state()) + except Exception as exc: + logger.error("Failed to delete VLAN %d: %s", vlan_id, exc) + self.error_occurred.emit("delete_vlan", str(exc)) + + async def _get_active_vlans(self) -> tuple[VlanInfo, ...]: + """Return a tuple of VlanInfo for all currently active VLAN connections.""" + vlans: list[VlanInfo] = [] + try: + active_paths = await self._nm().active_connections + for active_path in active_paths: + try: + ac = self._active_conn(active_path) + conn_path = await ac.connection + settings = await self._conn_settings(conn_path).get_settings() + conn_type = settings.get("connection", {}).get("type", (None, ""))[ + 1 + ] + if conn_type != "vlan": + continue + + vlan_id = settings.get("vlan", {}).get("id", (None, 0))[1] + iface = settings.get("connection", {}).get( + "interface-name", (None, "") + )[1] + if not iface: + parent = settings.get("vlan", {}).get("parent", (None, "eth0"))[ + 1 + ] + iface = f"{parent}.{vlan_id}" + + ipv4_method = settings.get("ipv4", {}).get( + "method", (None, "auto") + )[1] + is_dhcp = ipv4_method != "manual" + + dns_data = settings.get("ipv4", {}).get("dns-data", (None, []))[1] + dns_servers: tuple[str, ...] = () + if dns_data: + dns_servers = tuple(str(d) for d in dns_data) + else: + dns_raw = settings.get("ipv4", {}).get("dns", (None, []))[1] + if dns_raw: + dns_servers = tuple( + self._nm_uint32_to_ip(d) for d in dns_raw + ) + + ip_addr = "" + gateway = "" + try: + ip4_path = await self._active_conn(active_path).ip4_config + if ip4_path and ip4_path != "/": + ip4_cfg = self._ipv4(ip4_path) + addr_data = await ip4_cfg.address_data + if addr_data: + ip_addr = str(addr_data[0]["address"][1]) + gw = await ip4_cfg.gateway + if gw: + gateway = str(gw) + except Exception as exc: + logger.debug( + "D-Bus IP read for VLAN failed, falling back to OS: %s", exc + ) + if iface: + ip_addr = await self._get_ip_by_interface(iface) + + if not ip_addr and iface: + ip_addr = self._get_ip_os_fallback(iface) + + vlans.append( + VlanInfo( + vlan_id=int(vlan_id), + ip_address=ip_addr, + interface=iface, + gateway=gateway, + dns_servers=dns_servers, + is_dhcp=is_dhcp, + ) + ) + except Exception as exc: + logger.debug("Skipping connection in active VLAN list: %s", exc) + continue + except Exception as exc: + logger.debug("Error getting active VLANs: %s", exc) + return tuple(vlans) + + async def _deactivate_all_vlans(self) -> None: + """Deactivate all active VLAN connections via the NM D-Bus interface.""" + try: + active_paths = list(await self._nm().active_connections) + for active_path in active_paths: + try: + conn_path = await self._active_conn(active_path).connection + settings = await self._conn_settings(conn_path).get_settings() + conn_type = settings.get("connection", {}).get("type", (None, ""))[ + 1 + ] + if conn_type != "vlan": + continue + conn_id = settings.get("connection", {}).get("id", (None, ""))[1] + await self._nm().deactivate_connection(active_path) + logger.debug("Deactivated VLAN '%s'", conn_id) + except Exception as exc: + logger.debug("Skipping VLAN during deactivation: %s", exc) + continue + await asyncio.sleep(0.5) + except Exception as exc: + logger.debug("Error deactivating VLANs: %s", exc) + + async def _activate_saved_vlans(self) -> None: + """Activate all saved VLAN connection profiles found in NM settings.""" + try: + nm_settings = self._nm_settings() + connections = await nm_settings.connections + for conn_path in connections: + try: + settings = await self._conn_settings(conn_path).get_settings() + conn_type = settings.get("connection", {}).get("type", (None, ""))[ + 1 + ] + if conn_type != "vlan": + continue + conn_id = settings.get("connection", {}).get("id", (None, ""))[1] + await self._nm().activate_connection(conn_path, "/", "/") + logger.debug("Activated saved VLAN '%s'", conn_id) + await asyncio.sleep(1.0) + except Exception as exc: + logger.debug("Failed to activate VLAN: %s", exc) + except Exception as exc: + logger.debug("Error activating saved VLANs: %s", exc) + + async def _reconnect_wifi_profile(self, ssid: str) -> None: + """Disconnect, then re-activate the saved Wi-Fi profile for *ssid*. + + Waits up to 10 s for an IP address before returning. Used after + updating a connection's static IP or DHCP settings so the new + configuration is applied immediately. + """ + logger.debug( + "Reconnecting Wi-Fi profile '%s' to apply new settings", + ssid, + ) + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as disc_err: + logger.debug("Disconnect before reconnect: %s", disc_err) + await asyncio.sleep(1.5) + + fresh_path = await self._get_connection_path(ssid) + if not fresh_path: + fresh_path = await self._find_connection_path_direct(ssid) + if not fresh_path: + logger.warning( + "Reconnect skipped: could not find saved profile for '%s'", + ssid, + ) + return + + try: + await self._nm().activate_connection(fresh_path) + except Exception as act_err: + logger.warning( + "Reconnect activate failed for '%s': %s", + ssid, + act_err, + ) + return + + loop = asyncio.get_running_loop() + deadline = loop.time() + 10.0 + while loop.time() < deadline: + await asyncio.sleep(1.0) + found_ip: str = "" + try: + current = await self._get_current_ssid() + if current and current.lower() == ssid.lower(): + found_ip = await self._get_current_ip() or "" + if not found_ip: + found_ip = self._get_ip_os_fallback("wlan0") or "" + except Exception as exc: + logger.debug( + "IP address lookup during connection wait ignored: %s", exc + ) + + if found_ip: + logger.info( + "Reconnect complete for '%s': IP=%s", + ssid, + found_ip, + ) + try: + self._invalidate_saved_cache() + self.saved_networks_loaded.emit( + await self._get_saved_networks_impl() + ) + except Exception as cache_err: + logger.debug( + "Cache refresh after reconnect failed: %s", + cache_err, + ) + return + + logger.warning("Reconnect for '%s': IP not assigned within 10 s", ssid) + + async def _async_update_wifi_static_ip( + self, + ssid: str, + ip_address: str, + subnet_mask: str, + gateway: str, + dns1: str, + dns2: str, + ) -> None: + """Apply a static IPv4 configuration to a saved Wi-Fi profile and reconnect.""" + conn_path = await self._get_connection_path(ssid) + if not conn_path: + self.error_occurred.emit("wifi_static_ip", f"'{ssid}' not found") + return + try: + cs = self._conn_settings(conn_path) + props = await cs.get_settings() + await self._merge_wifi_secrets(cs, props) + + prefix = self._mask_to_prefix(subnet_mask) + ip_uint = self._ip_to_nm_uint32(ip_address) + gw_uint = self._ip_to_nm_uint32(gateway) if gateway else 0 + dns_list: list[int] = [] + if dns1: + dns_list.append(self._ip_to_nm_uint32(dns1)) + if dns2: + dns_list.append(self._ip_to_nm_uint32(dns2)) + + props["ipv4"] = { + "method": ("s", "manual"), + "addresses": ( + "aau", + [[ip_uint, prefix, gw_uint]], + ), + "gateway": ("s", gateway or ""), + "dns": ("au", dns_list), + } + props["ipv6"] = {"method": ("s", "disabled")} + await cs.update(props) + self._invalidate_saved_cache() + logger.info( + "Static IP set for '%s': %s/%d gw %s (IPv6 disabled)", + ssid, + ip_address, + prefix, + gateway, + ) + + await self._reconnect_wifi_profile(ssid) + + self.connection_result.emit( + ConnectionResult(True, f"Static IP set for '{ssid}'") + ) + self.state_changed.emit(await self._build_current_state()) + self.reconnect_complete.emit() + except Exception as exc: + logger.error("Failed to set static IP for '%s': %s", ssid, exc) + self.error_occurred.emit("wifi_static_ip", str(exc)) + + async def _async_reset_wifi_to_dhcp(self, ssid: str) -> None: + """Reset a saved Wi-Fi profile's IPv4 settings to DHCP and reconnect.""" + conn_path = await self._get_connection_path(ssid) + if not conn_path: + self.error_occurred.emit("wifi_dhcp", f"'{ssid}' not found") + return + try: + cs = self._conn_settings(conn_path) + props = await cs.get_settings() + await self._merge_wifi_secrets(cs, props) + props["ipv4"] = {"method": ("s", "auto")} + await cs.update(props) + self._invalidate_saved_cache() + logger.info("Reset '%s' to DHCP", ssid) + + await self._reconnect_wifi_profile(ssid) + + self.connection_result.emit(ConnectionResult(True, f"'{ssid}' set to DHCP")) + self.state_changed.emit(await self._build_current_state()) + self.reconnect_complete.emit() + except Exception as exc: + logger.error("Failed to reset '%s' to DHCP: %s", ssid, exc) + self.error_occurred.emit("wifi_dhcp", str(exc)) + + def _load_hotspot_config(self) -> None: + """Populate _hotspot_config from the config file. + + Writes defaults if missing. + """ + try: + cfg = get_configparser() + if not cfg.has_section("hotspot"): + cfg.add_section("hotspot") + + hotspot = cfg.get_section("hotspot") + + if hotspot.has_option("ssid"): + self._hotspot_config.ssid = hotspot.get("ssid", str, "PrinterHotspot") + else: + cfg.add_option("hotspot", "ssid", "PrinterHotspot") + + if hotspot.has_option("password"): + self._hotspot_config.password = hotspot.get( + "password", str, "123456789" + ) + else: + cfg.add_option("hotspot", "password", "123456789") + + cfg.save_configuration() + except Exception as exc: + logger.warning("Could not load hotspot config, using defaults: %s", exc) + + def _save_hotspot_config(self) -> None: + """Persist current _hotspot_config ssid/password to the config file.""" + try: + cfg = get_configparser() + cfg.update_option("hotspot", "ssid", self._hotspot_config.ssid) + cfg.update_option("hotspot", "password", self._hotspot_config.password) + cfg.save_configuration() + except Exception as exc: + logger.warning("Could not save hotspot config: %s", exc) + + async def _async_create_and_activate_hotspot( + self, + ssid: str, + password: str, + security: str = "wpa-psk", + ) -> None: + """Create a new WPA2-PSK AP-mode profile and activate it as a hotspot. + + Removes all stale AP-mode and same-name profiles before adding the new + one. Disconnects ethernet if active so the Wi-Fi radio is available. + """ + try: + config_ssid = ssid or "PrinterHotspot" + config_pwd = password or "123456789" + config_sec = ( + security + if HotspotSecurity.is_valid(security) + else HotspotSecurity.WPA2_PSK.value + ) + self._hotspot_config.ssid = config_ssid + self._hotspot_config.password = config_pwd + self._hotspot_config.security = config_sec + self._save_hotspot_config() + + if not await self._nm().wireless_enabled: + await self._nm().wireless_enabled.set_async(True) + await self._wait_for_wifi_radio(True, timeout=8.0) + + if not await self._wait_for_wifi_device_ready(timeout=8.0): + logger.warning( + "wlan0 did not reach DISCONNECTED within 8 s; " + "proceeding with hotspot activation anyway" + ) + + ethernet_was_active = await self._is_ethernet_connected() + if ethernet_was_active: + try: + await self._async_disconnect_ethernet() + except Exception as exc: + logger.debug("Pre-hotspot ethernet disconnect ignored: %s", exc) + # Brief pause to let eth0 finish deactivating before NM + # processes the hotspot activation request. + await asyncio.sleep(1.0) + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Pre-hotspot Wi-Fi disconnect ignored: %s", exc) + + await self._delete_all_ap_mode_connections() + # Also delete by connection id in case a non-AP profile shares the + # hotspot name (e.g. a leftover infrastructure profile named the + # same as the SSID). _delete_all_ap_mode_connections already caught + # all AP-mode profiles, so this second list_connections call is a + # narrow safety net. + await self._delete_connections_by_id(config_ssid) + + conn_props: dict[str, object] = { + "connection": { + "id": ("s", config_ssid), + "uuid": ("s", str(uuid4())), + "type": ("s", "802-11-wireless"), + "interface-name": ("s", self._get_wifi_iface_name()), + "autoconnect": ("b", False), + }, + "802-11-wireless": { + "ssid": ("ay", config_ssid.encode("utf-8")), + "mode": ("s", "ap"), + "band": ("s", self._hotspot_config.band), + "channel": ( + "u", + self._hotspot_config.channel, + ), + "security": ( + "s", + "802-11-wireless-security", + ), + }, + "ipv4": {"method": ("s", "shared")}, + "ipv6": {"method": ("s", "ignore")}, + } + + conn_props["802-11-wireless-security"] = { + "key-mgmt": ("s", "wpa-psk"), + "psk": ("s", config_pwd), + "pmf": ("u", 0), + } + # AP mode is always WPA2-PSK; WPA3-SAE in AP mode requires driver + # support not guaranteed on the target hardware. + config_sec = HotspotSecurity.WPA2_PSK.value + self._hotspot_config.security = config_sec + + conn_path = await self._nm_settings().add_connection(conn_props) + logger.debug( + "Hotspot profile created at %s (security=%s)", + conn_path, + config_sec, + ) + + await self._nm().activate_connection( + conn_path, self._primary_wifi_path, "/" + ) + self._is_hotspot_active = True + self._invalidate_saved_cache() + + self.hotspot_info_ready.emit(config_ssid, config_pwd, config_sec) + self.connection_result.emit( + ConnectionResult(True, f"Hotspot '{config_ssid}' activated") + ) + + await asyncio.sleep(1.5) + self.state_changed.emit(await self._build_current_state()) + + except Exception as exc: + logger.error("Hotspot create+activate failed: %s", exc) + self._is_hotspot_active = False + self.connection_result.emit( + ConnectionResult(False, str(exc), "hotspot_failed") + ) + + async def _async_update_hotspot_config( + self, + old_ssid: str, + new_ssid: str, + new_password: str, + security: str = "wpa-psk", + ) -> None: + """Update hotspot SSID/password and re-activate if the hotspot was running.""" + try: + was_active = self._is_hotspot_active + + if was_active and self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Pre-hotspot-update disconnect ignored: %s", exc) + self._is_hotspot_active = False + + await self._delete_all_ap_mode_connections() + deleted_old = await self._delete_connections_by_id(old_ssid) + logger.debug( + "Cleaned up %d old hotspot profiles for '%s'", + deleted_old, + old_ssid, + ) + + if new_ssid.lower() != old_ssid.lower(): + await self._delete_connections_by_id(new_ssid) + + validated_sec = ( + security + if HotspotSecurity.is_valid(security) + else HotspotSecurity.WPA2_PSK.value + ) + self._hotspot_config.ssid = new_ssid + self._hotspot_config.password = new_password + self._hotspot_config.security = validated_sec + self._save_hotspot_config() + + self.hotspot_info_ready.emit( + new_ssid, + new_password, + self._hotspot_config.security, + ) + + if was_active: + await self._async_create_and_activate_hotspot( + new_ssid, + new_password, + self._hotspot_config.security, + ) + else: + self.connection_result.emit( + ConnectionResult( + True, + f"Hotspot config updated to '{new_ssid}'", + ) + ) + except Exception as exc: + logger.error("Hotspot config update failed: %s", exc) + self.connection_result.emit( + ConnectionResult(False, str(exc), "hotspot_config_failed") + ) + + async def _async_toggle_hotspot(self, enable: bool) -> None: + """Enable or disable the hotspot, cleaning up profiles and Wi-Fi radio state.""" + try: + if enable: + await self._async_create_and_activate_hotspot( + self._hotspot_config.ssid, + self._hotspot_config.password, + ) + return + + was_hotspot_active = self._is_hotspot_active + self._is_hotspot_active = False + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Hotspot-off disconnect ignored: %s", exc) + + deleted = await self._delete_connections_by_id(self._hotspot_config.ssid) + logger.debug("Hotspot OFF: cleaned up %d profile(s)", deleted) + + if was_hotspot_active and await self._nm().wireless_enabled: + await self._nm().wireless_enabled.set_async(False) + + self.connection_result.emit(ConnectionResult(True, "Hotspot disabled")) + self.state_changed.emit(await self._build_current_state()) + except Exception as exc: + logger.error("Failed to toggle hotspot: %s", exc) + self._is_hotspot_active = False + self.connection_result.emit( + ConnectionResult( + success=False, + message=str(exc), + error_code="hotspot_toggle_failed", + ) + ) + + async def _merge_wifi_secrets( + self, + conn_settings: dbus_nm.NetworkConnectionSettings, + props: dict, + ) -> None: + """Fetch Wi-Fi secrets from NM and merge them into *props* in place. + + Required before calling update() so that the PSK is re-included; + NM redacts secrets from get_settings() responses. + """ + try: + secrets = await conn_settings.get_secrets("802-11-wireless-security") + sec_key = "802-11-wireless-security" + if sec_key in secrets: + props.setdefault(sec_key, {}).update(secrets[sec_key]) + except Exception as exc: + logger.debug("Could not fetch Wi-Fi secrets (NM may redact): %s", exc) + + async def _deactivate_connection_by_id(self, conn_id: str) -> bool: + """Deactivate the first active connection whose profile id matches *conn_id*.""" + try: + active_paths = await self._nm().active_connections + for active_path in active_paths: + try: + conn_path = await self._active_conn(active_path).connection + settings = await self._conn_settings(conn_path).get_settings() + cid = settings.get("connection", {}).get("id", (None, ""))[1] + if cid == conn_id: + await self._nm().deactivate_connection(active_path) + logger.debug( + "Deactivated active connection '%s'", + conn_id, + ) + return True + except Exception as exc: + logger.debug( + "Skipping connection during deactivation lookup: %s", exc + ) + except Exception as exc: + logger.debug("Error deactivating '%s': %s", conn_id, exc) + return False + + async def _delete_all_connections_by_id(self, conn_id: str) -> int: + """Delete every NM connection profile whose id exactly matches *conn_id*.""" + deleted = 0 + try: + connections = await self._nm_settings().list_connections() + for conn_path in connections: + try: + cs = self._conn_settings(conn_path) + settings = await cs.get_settings() + cid = settings.get("connection", {}).get("id", (None, ""))[1] + if cid == conn_id: + await cs.delete() + deleted += 1 + except Exception as exc: + logger.debug( + "Skipping connection in cleanup for '%s': %s", conn_id, exc + ) + except Exception as exc: + logger.error("Cleanup for '%s' failed: %s", conn_id, exc) + return deleted + + async def _delete_all_ap_mode_connections(self) -> int: + """Delete all saved Wi-Fi connections in AP mode. + + Called before creating a new hotspot to remove stale profiles from + previous hotspot sessions, regardless of their SSID. Without this, + old AP-mode profiles accumulate in NetworkManager and NM may + auto-activate them on the next boot. + """ + deleted = 0 + try: + connections = await self._nm_settings().list_connections() + for conn_path in connections: + try: + cs = self._conn_settings(conn_path) + settings = await cs.get_settings() + conn_type = settings.get("connection", {}).get("type", (None, ""))[ + 1 + ] + if conn_type != "802-11-wireless": + continue + mode = settings.get("802-11-wireless", {}).get("mode", (None, ""))[ + 1 + ] + if mode == "ap": + conn_id = settings.get("connection", {}).get("id", (None, ""))[ + 1 + ] + await cs.delete() + deleted += 1 + logger.debug( + "Removed stale AP profile '%s' at %s", conn_id, conn_path + ) + except Exception as exc: + logger.debug("Skipping connection in AP profile cleanup: %s", exc) + except Exception as exc: + logger.error("Failed to remove stale AP profiles: %s", exc) + if deleted: + self._invalidate_saved_cache() + return deleted + + async def _delete_connections_by_id(self, ssid: str) -> int: + """Delete every NM connection profile whose id matches *ssid* (case-insensitive).""" + deleted = 0 + try: + connections = await self._nm_settings().list_connections() + for conn_path in connections: + try: + cs = self._conn_settings(conn_path) + settings = await cs.get_settings() + conn_id = settings.get("connection", {}).get("id", (None, ""))[1] + if conn_id.lower() == ssid.lower(): + await cs.delete() + deleted += 1 + logger.debug( + "Deleted stale profile '%s' at %s", + conn_id, + conn_path, + ) + except Exception as exc: + logger.debug( + "Skip connection %s during cleanup: %s", + conn_path, + exc, + ) + except Exception as exc: + logger.error( + "Failed to enumerate connections for cleanup: %s", + exc, + ) + if deleted: + self._invalidate_saved_cache() + return deleted + + @staticmethod + def _ip_to_nm_uint32(ip_str: str) -> int: + """Convert a dotted-decimal IPv4 string to a native-endian uint32 for NM.""" + return struct.unpack("=I", ipaddress.IPv4Address(ip_str).packed)[0] + + @staticmethod + def _nm_uint32_to_ip(uint_ip: int) -> str: + """Convert a native-endian uint32 from NM back to a dotted-decimal IPv4 string.""" + return str(ipaddress.IPv4Address(struct.pack("=I", uint_ip))) + + @staticmethod + def _mask_to_prefix(mask_str: str) -> int: + """Convert a subnet mask or CIDR prefix string to an integer prefix length.""" + stripped = mask_str.strip() + if stripped.isdigit(): + prefix = int(stripped) + if 0 <= prefix <= 32: + return prefix + raise ValueError(f"CIDR prefix out of range: {prefix}") + return bin(int(ipaddress.IPv4Address(stripped))).count("1") diff --git a/BlocksScreen/lib/panels/controlTab.py b/BlocksScreen/lib/panels/controlTab.py index 0f77cd24..28e48c2c 100644 --- a/BlocksScreen/lib/panels/controlTab.py +++ b/BlocksScreen/lib/panels/controlTab.py @@ -46,6 +46,7 @@ class ControlTab(QtWidgets.QStackedWidget): str, name="request-file-info" ) call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") + toggle_conn_page = QtCore.pyqtSignal(bool, name="call-load-panel") tune_display_buttons: dict = {} card_options: dict = {} @@ -66,6 +67,7 @@ def __init__( self.printer: Printer = printer self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) self.timers = [] + self.ztilt_state = False self.extruder_info: dict = {} self.bed_info: dict = {} self.toolhead_info: dict = {} @@ -75,6 +77,8 @@ def __init__( self.move_length: float = 1.0 self.move_speed: float = 25.0 self.probe_helper_page = ProbeHelper(self) + self.probe_helper_page.toggle_conn_page.connect(self.toggle_conn_page) + self.probe_helper_page.disable_popups.connect(self.disable_popups) self.addWidget(self.probe_helper_page) self.probe_helper_page.call_load_panel.connect(self.call_load_panel) self.printcores_page = SwapPrintcorePage(self) @@ -90,6 +94,15 @@ def __init__( self.probe_helper_page.query_printer_object.connect(self.ws.api.object_query) self.probe_helper_page.run_gcode_signal.connect(self.ws.api.run_gcode) self.probe_helper_page.request_back.connect(self.back_button) + self.printer.print_stats_update[str, str].connect( + self.probe_helper_page.on_print_stats_update + ) + self.printer.print_stats_update[str, dict].connect( + self.probe_helper_page.on_print_stats_update + ) + self.printer.print_stats_update[str, float].connect( + self.probe_helper_page.on_print_stats_update + ) self.printer.available_gcode_cmds.connect( self.probe_helper_page.on_available_gcode_cmds ) @@ -460,6 +473,7 @@ def _handle_gcode_response(self, messages: list): def handle_ztilt(self): """Handle Z-Tilt Adjustment""" self.call_load_panel.emit(True, "Please wait, performing Z-axis calibration.") + self.run_gcode_signal.emit("G28\nM400") self.run_gcode_signal.emit("Z_TILT_ADJUST") @QtCore.pyqtSlot(str, name="on-klippy-status") diff --git a/BlocksScreen/lib/panels/filamentTab.py b/BlocksScreen/lib/panels/filamentTab.py index 4b368bd8..04fc5ef4 100644 --- a/BlocksScreen/lib/panels/filamentTab.py +++ b/BlocksScreen/lib/panels/filamentTab.py @@ -147,14 +147,14 @@ def on_extruder_update( @QtCore.pyqtSlot(bool, name="on_load_filament") def on_load_filament(self, status: bool): """Handle load filament object updated""" - if self.loadignore: - self.loadignore = False - return if not self.isVisible: return + if self.loadignore: + return if status: self.call_load_panel.emit(True, "Loading Filament") else: + self.loadignore = True self.target_temp = 0 self.call_load_panel.emit(False, "") self._filament_state = self.FilamentStates.LOADED @@ -163,14 +163,14 @@ def on_load_filament(self, status: bool): @QtCore.pyqtSlot(bool, name="on_unload_filament") def on_unload_filament(self, status: bool): """Handle unload filament object updated""" - if self.unloadignore: - self.unloadignore = False - return if not self.isVisible: return + if self.unloadignore: + return if status: self.call_load_panel.emit(True, "Unloading Filament") else: + self.unloadignore = True self.call_load_panel.emit(False, "") self.target_temp = 0 self._filament_state = self.FilamentStates.UNLOADED @@ -218,7 +218,7 @@ def unload_filament(self, toolhead: int = 0, temp: int = 220) -> None: return self.find_routine_objects() - self.unload_filament = False + self.unloadignore = False self.call_load_panel.emit(True, "Unloading Filament") self.run_gcode.emit(f"UNLOAD_FILAMENT TEMPERATURE={temp}") diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 32355803..77d17e86 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -4,23 +4,24 @@ import events from configfile import BlocksScreenConfig, get_configparser +from devices.storage import USBManager from lib.files import Files from lib.machine import MachineControl from lib.moonrakerComm import MoonWebSocket +from lib.network import WifiIconKey from lib.panels.controlTab import ControlTab from lib.panels.filamentTab import FilamentTab -from lib.panels.networkWindow import NetworkControlWindow +from lib.panels.networkWindow import NetworkControlWindow, PixmapCache from lib.panels.printTab import PrintTab from lib.panels.utilitiesTab import UtilitiesTab +from lib.panels.widgets.basePopup import BasePopup +from lib.panels.widgets.cancelPage import CancelPage from lib.panels.widgets.connectionPage import ConnectionPage -from lib.panels.widgets.popupDialogWidget import Popup +from lib.panels.widgets.loadWidget import LoadingOverlayWidget +from lib.panels.widgets.notificationPage import NotificationPage +from lib.panels.widgets.updatePage import UpdatePage from lib.printer import Printer from lib.ui.mainWindow_ui import Ui_MainWindow # With header -from lib.panels.widgets.updatePage import UpdatePage -from lib.panels.widgets.basePopup import BasePopup -from lib.panels.widgets.loadWidget import LoadingOverlayWidget - -# from lib.ui.mainWindow_v2_ui import Ui_MainWindow # No header from lib.ui.resources.background_resources_rc import * from lib.ui.resources.font_rc import * from lib.ui.resources.graphic_resources_rc import * @@ -31,7 +32,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets from screensaver import ScreenSaver -_logger = logging.getLogger(name="logs/BlocksScreen.log") +_logger = logging.getLogger(__name__) def api_handler(func): @@ -49,6 +50,34 @@ def wrapper(*args, **kwargs): return wrapper +class HeaderWifiIconProvider: + """Resolves WifiIconKey integer values to cached QPixmaps for the header bar.""" + + _WIFI_PATHS: dict[tuple[int, bool], str] = { + ( + b, + p, + ): f":/network/media/btn_icons/network/{b}bar_wifi{'_protected' if p else ''}.svg" + for b in range(5) + for p in (False, True) + } + _ETHERNET_PATH = ":/network/media/btn_icons/network/ethernet_connected.svg" + _HOTSPOT_PATH = ":/network/media/btn_icons/hotspot.svg" + + @classmethod + def get_pixmap(cls, icon_key: int) -> QtGui.QPixmap: + """Resolve an icon key to a QPixmap (cached via PixmapCache).""" + key = WifiIconKey(icon_key) + if key is WifiIconKey.ETHERNET: + return PixmapCache.get(cls._ETHERNET_PATH) + if key is WifiIconKey.HOTSPOT: + return PixmapCache.get(cls._HOTSPOT_PATH) + path = cls._WIFI_PATHS.get( + (key.bars, key.is_protected), cls._WIFI_PATHS[(0, False)] + ) + return PixmapCache.get(path) + + class MainWindow(QtWidgets.QMainWindow): """GUI MainWindow, handles most of the app logic""" @@ -61,13 +90,22 @@ class MainWindow(QtWidgets.QMainWindow): gcode_response = QtCore.pyqtSignal(list, name="gcode_response") handle_error_response = QtCore.pyqtSignal(list, name="handle_error_response") call_network_panel = QtCore.pyqtSignal(name="call-network-panel") + call_notification_panel = QtCore.pyqtSignal(name="call-notification-panel") call_update_panel = QtCore.pyqtSignal(name="call-update-panel") on_update_message: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( dict, name="on-update-message" ) + run_gcode_signal: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="run_gcode" + ) + show_notifications: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, str, int, bool, name="show-notifications" + ) + call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") def __init__(self): + """Set up UI, instantiate subsystems, and wire all inter-component signals.""" super(MainWindow, self).__init__() self.config: BlocksScreenConfig = get_configparser() self.ui = Ui_MainWindow() @@ -75,21 +113,28 @@ def __init__(self): self.screensaver = ScreenSaver(self) self._popup_toggle: bool = False self.ui.main_content_widget.setCurrentIndex(0) - self.popup = Popup(self) + + usb_config = self.config.get_section("usb_manager", fallback=None) + gdir = None + if usb_config: + gdir = usb_config.get("gcodes_dir", default=None) + + self.usb_manager: USBManager = USBManager(parent=self, gcodes_dir=gdir) self.ws = MoonWebSocket(self) + self.notiPage = NotificationPage(self) self.mc = MachineControl(self) self.file_data = Files(self, self.ws) self.index_stack = deque(maxlen=4) self.printer = Printer(self, self.ws) self.conn_window = ConnectionPage(self, self.ws) - self.up = UpdatePage(self) - self.up.hide() + self.update_page = UpdatePage(self) + self.update_page.hide() + self.conn_window.call_cancel_panel.connect(self.handle_cancel_print) self.installEventFilter(self.conn_window) self.printPanel = PrintTab( self.ui.printTab, self.file_data, self.ws, self.printer ) QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.CursorShape.BlankCursor) - self.filamentPanel = FilamentTab(self.ui.filamentTab, self.printer, self.ws) self.controlPanel = ControlTab(self.ui.controlTab, self.ws, self.printer) self.utilitiesPanel = UtilitiesTab(self.ui.utilitiesTab, self.ws, self.printer) @@ -104,6 +149,8 @@ def __init__(self): self.printPanel.request_back.connect(slot=self.global_back) self.printPanel.on_cancel_print.connect(slot=self.on_cancel_print) + self.show_notifications.connect(self.notiPage.new_notication) + 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) @@ -112,6 +159,7 @@ def __init__(self): self.utilitiesPanel.request_back.connect(slot=self.global_back) self.utilitiesPanel.request_change_page.connect(slot=self.global_change_page) self.utilitiesPanel.update_available.connect(self.on_update_available) + self.ui.notification_btn.clicked.connect(self.notiPage.show_notification_panel) self.ui.extruder_temp_display.clicked.connect( lambda: self.global_change_page( self.ui.main_content_widget.indexOf(self.ui.controlTab), @@ -152,29 +200,42 @@ def __init__(self): self.query_object_list.connect(self.utilitiesPanel.on_object_list) self.printer.extruder_update.connect(self.on_extruder_update) self.printer.heater_bed_update.connect(self.on_heater_bed_update) + self.run_gcode_signal.connect(self.ws.api.run_gcode) + self.ui.main_content_widget.currentChanged.connect(slot=self.reset_tab_indexes) self.call_network_panel.connect(self.networkPanel.show_network_panel) + self.call_notification_panel.connect(self.notiPage.show_notification_panel) + self.networkPanel.update_wifi_icon.connect(self.change_wifi_icon) self.conn_window.wifi_button_clicked.connect(self.call_network_panel.emit) + self.conn_window.notification_btn_clicked.connect( + self.call_notification_panel.emit + ) self.ui.wifi_button.clicked.connect(self.call_network_panel.emit) self.handle_error_response.connect( self.controlPanel.probe_helper_page.handle_error_response ) self.controlPanel.disable_popups.connect(self.popup_toggle) - self.on_update_message.connect(self.up.handle_update_message) - self.up.request_full_update.connect(self.ws.api.full_update) - self.up.request_recover_repo[str].connect(self.ws.api.recover_corrupt_repo) - self.up.request_recover_repo[str, bool].connect( + self.on_update_message.connect(self.update_page.handle_update_message) + self.update_page.request_full_update.connect(self.ws.api.full_update) + self.update_page.request_recover_repo[str].connect( self.ws.api.recover_corrupt_repo ) - self.up.request_refresh_update.connect(self.ws.api.refresh_update_status) - self.up.request_refresh_update[str].connect(self.ws.api.refresh_update_status) - self.up.request_rollback_update.connect(self.ws.api.rollback_update) - self.up.request_update_client.connect(self.ws.api.update_client) - self.up.request_update_klipper.connect(self.ws.api.update_klipper) - self.up.request_update_moonraker.connect(self.ws.api.update_moonraker) - self.up.request_update_status.connect(self.ws.api.update_status) - self.up.request_update_system.connect(self.ws.api.update_system) - self.up.update_back_btn.clicked.connect(self.up.hide) + self.update_page.request_recover_repo[str, bool].connect( + self.ws.api.recover_corrupt_repo + ) + self.update_page.request_refresh_update.connect( + self.ws.api.refresh_update_status + ) + self.update_page.request_refresh_update[str].connect( + self.ws.api.refresh_update_status + ) + self.update_page.request_rollback_update.connect(self.ws.api.rollback_update) + self.update_page.request_update_client.connect(self.ws.api.update_client) + self.update_page.request_update_klipper.connect(self.ws.api.update_klipper) + self.update_page.request_update_moonraker.connect(self.ws.api.update_moonraker) + self.update_page.request_update_status.connect(self.ws.api.update_status) + self.update_page.request_update_system.connect(self.ws.api.update_system) + self.update_page.update_back_btn.clicked.connect(self.update_page.hide) self.utilitiesPanel.show_update_page.connect(self.show_update_page) self.conn_window.update_button_clicked.connect(self.show_update_page) self.ui.extruder_temp_display.display_format = "upper_downer" @@ -191,13 +252,42 @@ def __init__(self): self, LoadingOverlayWidget.AnimationGIF.DEFAULT ) 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.request_file_info.connect(self.file_data.on_request_fileinfo) + self.cancelpage.run_gcode.connect(self.ws.api.run_gcode) + self.printer.print_stats_update[str, str].connect( + self.cancelpage.on_print_stats_update + ) + self.printer.print_stats_update[str, dict].connect( + self.cancelpage.on_print_stats_update + ) + self.printer.print_stats_update[str, float].connect( + self.cancelpage.on_print_stats_update + ) + self.file_data.fileinfo.connect(self.cancelpage._show_screen_thumbnail) + self.printPanel.call_cancel_panel.connect(self.handle_cancel_print) + if self.config.has_section("server"): - # @ Start websocket connection with moonraker self.bo_ws_startup.emit() self.reset_tab_indexes() + @QtCore.pyqtSlot(bool, name="show-cancel-page") + def handle_cancel_print(self, show: bool = True): + """Slot for displaying update Panel""" + if not show: + self.cancelpage.hide() + return + + self.cancelpage.setGeometry(0, 0, self.width(), self.height()) + self.cancelpage.raise_() + self.cancelpage.updateGeometry() + self.cancelpage.repaint() + self.cancelpage.show() + @QtCore.pyqtSlot(bool, str, name="show-load-page") def show_LoadScreen(self, show: bool = True, msg: str = ""): + """Show or hide the loading overlay, guarded by the calling panel's visibility.""" _sender = self.sender() if _sender == self.filamentPanel: @@ -223,22 +313,22 @@ def show_LoadScreen(self, show: bool = True, msg: str = ""): def show_update_page(self, fullscreen: bool): """Slot for displaying update Panel""" if not fullscreen: - self.up.setParent(self.ui.main_content_widget) + self.update_page.setParent(self.ui.main_content_widget) current_index = self.ui.main_content_widget.currentIndex() tab_rect = self.ui.main_content_widget.tabBar().tabRect(current_index) width = tab_rect.width() - _parent_size = self.up.parent().size() - self.up.setGeometry( + _parent_size = self.update_page.parent().size() + self.update_page.setGeometry( width, 0, _parent_size.width() - width, _parent_size.height() ) else: - self.up.setParent(self) - self.up.setGeometry(0, 0, self.width(), self.height()) + self.update_page.setParent(self) + self.update_page.setGeometry(0, 0, self.width(), self.height()) - self.up.raise_() - self.up.updateGeometry() - self.up.repaint() - self.up.show() + self.update_page.raise_() + self.update_page.updateGeometry() + self.update_page.repaint() + self.update_page.show() @QtCore.pyqtSlot(name="on-cancel-print") def on_cancel_print(self): @@ -347,7 +437,7 @@ def reset_tab_indexes(self): Used to grantee all tabs reset to their first page once the user leaves the tab """ - self.up.hide() + self.update_page.hide() self.printPanel.setCurrentIndex(0) self.filamentPanel.setCurrentIndex(0) self.controlPanel.setCurrentIndex(0) @@ -387,6 +477,15 @@ def set_current_panel_index(self, panel_index: int) -> None: case 3: self.utilitiesPanel.setCurrentIndex(panel_index) + @QtCore.pyqtSlot(int) + def change_wifi_icon(self, icon_key: int) -> None: + """Change the icon of the netowrk by a key enum match + + Args: + icon_key (int): WifiIconKey mapping for the current network state + """ + self.ui.wifi_button.setPixmap(HeaderWifiIconProvider.get_pixmap(icon_key)) + @QtCore.pyqtSlot(int, int, name="request-change-page") def global_change_page(self, tab_index: int, panel_index: int) -> None: """Changes panels pages globally @@ -470,6 +569,7 @@ def messageReceivedEvent(self, event: events.WebSocketMessageReceived) -> None: @api_handler def _handle_server_message(self, method, data, metadata) -> None: + """Route file-related WebSocket messages to the Files subsystem.""" if "file" in method: file_data_event = events.ReceivedFileData(data, method, metadata) try: @@ -485,8 +585,8 @@ def _handle_server_message(self, method, data, metadata) -> None: @api_handler def _handle_machine_message(self, method, data, metadata) -> None: + """Route machine-state WebSocket messages to the update signal.""" if "ok" in data: - # Here capture if 'ok' if a request for an update was successful return if "update" in method: if ("status" or "refresh") in method: @@ -578,7 +678,7 @@ def _handle_notify_klippy_message(self, method, data, metadata) -> None: @api_handler def _handle_notify_filelist_changed_message(self, method, data, metadata) -> None: """Handle websocket file list messages""" - ... + self.file_data.handle_filelist_changed(data) @api_handler def _handle_notify_service_state_changed_message( @@ -591,9 +691,13 @@ def _handle_notify_service_state_changed_message( return service_entry: dict = entry[0] service_name, service_info = service_entry.popitem() - self.popup.new_message( - message_type=Popup.MessageType.INFO, - message=f"{service_name} service changed state to \n{service_info.get('sub_state')}", + self.show_notifications.emit( + "mainwindow", + str( + f"{service_name} service changed state to \n{service_info.get('sub_state')}" + ), + 1, + False, ) @api_handler @@ -608,43 +712,41 @@ def _handle_notify_gcode_response_message(self, method, data, metadata) -> None: popupWhitelist = ["filament runout", "no filament"] if _message.lower() not in popupWhitelist or _gcode_msg_type != "!!": return - - self.popup.new_message( - message_type=Popup.MessageType.ERROR, - message=str(_message), - userInput=True, - ) + self.show_notifications.emit("mainwindow", _message, 3, True) @api_handler def _handle_error_message(self, method, data, metadata) -> None: - """Handle error messages""" + """Handle error messages from Moonraker API.""" self.handle_error_response[list].emit([data, metadata]) - if "metadata" in data.get("message", "").lower(): - # Quick fix, don't care about no metadata errors - return if self._popup_toggle: return - text = data - if isinstance(data, dict): - if "message" in data: - text = f"{data['message']}" - else: - text = data - self.popup.new_message( - message_type=Popup.MessageType.ERROR, - message=str(text), - userInput=True, - ) + + text = data.get("message", str(data)) if isinstance(data, dict) else str(data) + lower_text = text.lower() + + # Metadata errors - silent, handled by files_manager + if "metadata" in lower_text: + self.file_data.handle_metadata_error(text) + return + + # File not found - silent + if "file" in lower_text and "does not exist" in lower_text: + return + + # Directory not found - navigate back + show popup + if "does not exist" in lower_text: + self.printPanel.filesPage_widget.on_directory_error() + + # Show popup for all other errors (including directory errors) + self.show_notifications.emit("mainwindow", str(text), 3, True) + _logger.error(text) @api_handler def _handle_notify_cpu_throttled_message(self, method, data, metadata) -> None: """Handle websocket cpu throttled messages""" if self._popup_toggle: return - self.popup.new_message( - message_type=Popup.MessageType.WARNING, - message=f"CPU THROTTLED: {data} | {metadata}", - ) + self.show_notifications.emit("mainwindow", data, 2, False) @api_handler def _handle_notify_status_update_message(self, method, data, metadata) -> None: @@ -685,18 +787,14 @@ def set_header_nozzle_diameter(self, diam: str): self.ui.nozzle_size_icon.setText(f"{diam}mm") self.ui.nozzle_size_icon.update() - def closeEvent(self, a0: typing.Optional[QtGui.QCloseEvent]) -> None: + def closeEvent(self, a0: QtGui.QCloseEvent | None) -> None: """Handles GUI closing""" - _loggers = [ - logging.getLogger(name) for name in logging.root.manager.loggerDict - ] # Get available logger handlers - for logger in _loggers: # noqa: F402 - if hasattr(logger, "cancel"): - _callback = getattr(logger, "cancel") - if callable(_callback): - _callback() + try: + self.networkPanel.close() + self.usb_manager.close() + except Exception as e: + _logger.warning("Error shutting down: %s", e) self.ws.wb_disconnect() - self.close() if a0 is None: return QtWidgets.QMainWindow.closeEvent(self, a0) @@ -734,6 +832,8 @@ def event(self, event: QtCore.QEvent) -> bool: events.PrintComplete.type(), events.PrintCancelled.type(), ): + if event.type() == events.PrintCancelled.type(): + self.handle_cancel_print() self.enable_tab_bar() self.ui.extruder_temp_display.clicked.disconnect() self.ui.bed_temp_display.clicked.disconnect() diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 19574cf5..f9b17a52 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -1,21 +1,33 @@ +import fcntl +import ipaddress as _ipaddress import logging -import threading +import socket as _socket +import struct +from dataclasses import replace from functools import partial -from typing import ( - Any, - Callable, - Dict, - List, - NamedTuple, - Optional, -) -from lib.network import SdbusNetworkManagerAsync +from lib.network import ( + ConnectionPriority, + ConnectionResult, + ConnectivityState, + NetworkInfo, + NetworkManager, + NetworkState, + NetworkStatus, + PendingOperation, + SavedNetwork, + WifiIconKey, + is_connectable_security, + is_hidden_ssid, + signal_to_bars, +) from lib.panels.widgets.keyboardPage import CustomQwertyKeyboard from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.panels.widgets.popupDialogWidget import Popup +from lib.qrcode_gen import generate_wifi_qrcode from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_frame import BlocksCustomFrame +from lib.utils.blocks_label import BlocksLabel from lib.utils.blocks_linedit import BlocksCustomLinEdit from lib.utils.blocks_Scrollbar import CustomScrollBar from lib.utils.blocks_togglebutton import NetworkWidgetbuttons @@ -23,270 +35,131 @@ 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 QObject, QRunnable, QThreadPool, pyqtSignal - -logger = logging.getLogger("logs/BlocksScreen.log") +from PyQt6.QtCore import QTimer, pyqtSlot +logger = logging.getLogger(__name__) LOAD_TIMEOUT_MS = 30_000 -NETWORK_CONNECT_DELAY_MS = 5_000 -NETWORK_LIST_REFRESH_MS = 10_000 STATUS_CHECK_INTERVAL_MS = 2_000 -DEFAULT_POLL_INTERVAL_MS = 10_000 - -SIGNAL_EXCELLENT_THRESHOLD = 75 -SIGNAL_GOOD_THRESHOLD = 50 -SIGNAL_FAIR_THRESHOLD = 25 -SIGNAL_MINIMUM_THRESHOLD = 5 - -PRIORITY_HIGH = 90 -PRIORITY_MEDIUM = 50 -PRIORITY_LOW = 20 - -SEPARATOR_SIGNAL_VALUE = -10 -PRIVACY_BIT = 1 - -# SSIDs that indicate hidden networks -HIDDEN_NETWORK_INDICATORS = ("", "UNKNOWN", "", None) - - -class NetworkInfo(NamedTuple): - """Information about a network.""" - - signal: int - status: str - is_open: bool = False - is_saved: bool = False - is_hidden: bool = False # Added flag for hidden networks - - -class NetworkScanResult(NamedTuple): - """Result of a network scan.""" - - ssid: str - signal: int - status: str - is_open: bool = False - - -class NetworkScanRunnable(QRunnable): - """Runnable for scanning networks in background thread.""" - - class Signals(QObject): - """Signals for network scan results.""" - scan_results = pyqtSignal(dict, name="scan-results") - finished_network_list_build = pyqtSignal( - list, name="finished-network-list-build" - ) - error = pyqtSignal(str) - def __init__(self, nm: SdbusNetworkManagerAsync) -> None: - """Initialize the network scan runnable.""" - super().__init__() - self._nm = nm - self.signals = NetworkScanRunnable.Signals() +class PixmapCache: + """Process-wide cache for QPixmaps loaded from Qt resource paths. - def run(self) -> None: - """Execute the network scan.""" - try: - self._nm.rescan_networks() - saved_ssids = self._nm.get_saved_ssid_names() - available = self._get_available_networks() - data_dict = self._build_data_dict(available, saved_ssids) - self.signals.scan_results.emit(data_dict) - items = self._build_network_list(data_dict) - self.signals.finished_network_list_build.emit(items) - except Exception as e: - logger.error("Error scanning networks", exc_info=True) - self.signals.error.emit(str(e)) - - def _get_available_networks(self) -> Dict[str, Dict]: - """Get available networks from NetworkManager.""" - if self._nm.check_wifi_interface(): - return self._nm.get_available_networks() or {} - return {} - - def _build_data_dict( - self, available: Dict[str, Dict], saved_ssids: List[str] - ) -> Dict[str, Dict]: - """Build data dictionary from available networks.""" - data_dict: Dict[str, Dict] = {} - for ssid, props in available.items(): - signal = int(props.get("signal_level", 0)) - sec_tuple = props.get("security", (0, 0, 0)) - caps_value = sec_tuple[2] if len(sec_tuple) > 2 else 0 - is_open = (caps_value & PRIVACY_BIT) == 0 - # Check if this is a hidden network - is_hidden = ssid in HIDDEN_NETWORK_INDICATORS or not ssid.strip() - data_dict[ssid] = { - "signal_level": signal, - "is_saved": ssid in saved_ssids, - "is_open": is_open, - "is_hidden": is_hidden, - } - return data_dict + Every SVG is decoded exactly once. Qt's implicit sharing means the + same QPixmap can be safely referenced by any number of widgets. + Must only be called after QApplication is created. + """ - def _build_network_list(self, data_dict: Dict[str, Dict]) -> List[tuple]: - """Build sorted network list for display.""" - current_ssid = self._nm.get_current_ssid() + _cache: dict[str, QtGui.QPixmap] = {} - saved_nets = [ - (ssid, info["signal_level"], info["is_open"], info.get("is_hidden", False)) - for ssid, info in data_dict.items() - if info["is_saved"] - ] - unsaved_nets = [ - (ssid, info["signal_level"], info["is_open"], info.get("is_hidden", False)) - for ssid, info in data_dict.items() - if not info["is_saved"] - ] + @classmethod + def get(cls, path: str) -> QtGui.QPixmap: + """Return the cached QPixmap for *path*, loading it on first access.""" + if path not in cls._cache: + cls._cache[path] = QtGui.QPixmap(path) + return cls._cache[path] - saved_nets.sort(key=lambda x: -x[1]) - unsaved_nets.sort(key=lambda x: -x[1]) + @classmethod + def preload(cls, paths: list[str]) -> None: + """Batch-load a list of paths (called once during init).""" + for path in paths: + cls.get(path) - items: List[tuple] = [] - for ssid, signal, is_open, is_hidden in saved_nets: - status = "Active" if ssid == current_ssid else "Saved" - items.append((ssid, signal, status, is_open, True, is_hidden)) +class WifiIconProvider: + """Maps (signal_strength, is_protected) -> cached QPixmap via PixmapCache.""" - for ssid, signal, is_open, is_hidden in unsaved_nets: - status = "Open" if is_open else "Protected" - items.append((ssid, signal, status, is_open, False, is_hidden)) + _PATHS: dict[tuple[int, bool], str] = { + ( + b, + p, + ): f":/network/media/btn_icons/network/{b}bar_wifi{'_protected' if p else ''}.svg" + for b in range(5) + for p in (False, True) + } - return items + @classmethod + def get_pixmap(cls, signal: int, is_protected: bool = False) -> QtGui.QPixmap: + """Get pixmap for given signal strength and protection status.""" + bars = signal_to_bars(signal) + path = cls._PATHS.get((bars, is_protected), cls._PATHS[(0, False)]) + return PixmapCache.get(path) -class BuildNetworkList(QtCore.QObject): - """Worker class for building network lists with polling support.""" +class IPAddressLineEdit(BlocksCustomLinEdit): + """Line-edit restricted to valid IPv4 addresses.""" - scan_results = pyqtSignal(dict, name="scan-results") - finished_network_list_build = pyqtSignal(list, name="finished-network-list-build") - error = pyqtSignal(str) + _VALID_STYLE = "" + _INVALID_STYLE = "border: 2px solid red; border-radius: 8px;" def __init__( self, - nm: SdbusNetworkManagerAsync, - poll_interval_ms: int = DEFAULT_POLL_INTERVAL_MS, + parent: QtWidgets.QWidget | None = None, + *, + placeholder: str = "0.0.0.0", # nosec B104 — UI placeholder text, not a socket bind ) -> None: - """Initialize the network list builder.""" - super().__init__() - self._nm = nm - self._threadpool = QThreadPool.globalInstance() - self._poll_interval_ms = poll_interval_ms - self._is_scanning = False - self._scan_lock = threading.Lock() - self._timer = QtCore.QTimer(self) - self._timer.setSingleShot(True) - self._timer.timeout.connect(self._do_scan) - - def start_polling(self) -> None: - """Start periodic network scanning.""" - self._schedule_next_scan() - - def stop_polling(self) -> None: - """Stop periodic network scanning.""" - self._timer.stop() - - def build(self) -> None: - """Trigger immediate network scan.""" - self._do_scan() - - def _schedule_next_scan(self) -> None: - """Schedule the next network scan.""" - self._timer.start(self._poll_interval_ms) - - def _on_task_finished(self, items: List) -> None: - """Handle scan completion.""" - with self._scan_lock: - self._is_scanning = False - self.finished_network_list_build.emit(items) - self._schedule_next_scan() - - def _on_task_scan_results(self, data_dict: Dict) -> None: - """Handle scan results.""" - self.scan_results.emit(data_dict) - - def _on_task_error(self, err: str) -> None: - """Handle scan error.""" - with self._scan_lock: - self._is_scanning = False - self.error.emit(err) - self._schedule_next_scan() - - def _do_scan(self) -> None: - """Execute network scan in background thread.""" - with self._scan_lock: - if self._is_scanning: - return - self._is_scanning = True - - task = NetworkScanRunnable(self._nm) - task.signals.finished_network_list_build.connect(self._on_task_finished) - task.signals.scan_results.connect(self._on_task_scan_results) - task.signals.error.connect(self._on_task_error) - self._threadpool.start(task) - + """Initialise the IP-address input field with regex validation and optional placeholder.""" + super().__init__(parent) + self.setPlaceholderText(placeholder) + ip_re = QtCore.QRegularExpression(r"^[\d.]*$") + self.setValidator(QtGui.QRegularExpressionValidator(ip_re, self)) + self.textChanged.connect(self._on_text_changed) + + def is_valid(self) -> bool: + """Return ``True`` when the current text is a valid dotted-quad IPv4 address.""" + try: + _ipaddress.IPv4Address(self.text().strip()) + return True + except ValueError: + return False + + def is_valid_mask(self) -> bool: + """Return ``True`` when the current text is a valid subnet mask or CIDR prefix.""" + txt = self.text().strip() + if txt.isdigit(): + n = int(txt) + if 0 <= n <= 32: + return True + return False -class WifiIconProvider: - """Provider for Wi-Fi signal strength icons.""" - - def __init__(self) -> None: - """Initialize icon paths.""" - self._paths = { - (0, False): ":/network/media/btn_icons/0bar_wifi.svg", - (1, False): ":/network/media/btn_icons/1bar_wifi.svg", - (2, False): ":/network/media/btn_icons/2bar_wifi.svg", - (3, False): ":/network/media/btn_icons/3bar_wifi.svg", - (4, False): ":/network/media/btn_icons/4bar_wifi.svg", - (0, True): ":/network/media/btn_icons/0bar_wifi_protected.svg", - (1, True): ":/network/media/btn_icons/1bar_wifi_protected.svg", - (2, True): ":/network/media/btn_icons/2bar_wifi_protected.svg", - (3, True): ":/network/media/btn_icons/3bar_wifi_protected.svg", - (4, True): ":/network/media/btn_icons/4bar_wifi_protected.svg", - } - - def get_pixmap(self, signal: int, status: str) -> QtGui.QPixmap: - """Get pixmap for given signal strength and status.""" - bars = self._signal_to_bars(signal) - is_protected = status == "Protected" - key = (bars, is_protected) - path = self._paths.get(key, self._paths[(0, False)]) - return QtGui.QPixmap(path) + try: + _ipaddress.IPv4Network(f"0.0.0.0/{txt}", strict=False) + return True + except ValueError: + return False - @staticmethod - def _signal_to_bars(signal: int) -> int: - """Convert signal strength to bar count.""" - if signal < SIGNAL_MINIMUM_THRESHOLD: - return 0 - elif signal >= SIGNAL_EXCELLENT_THRESHOLD: - return 4 - elif signal >= SIGNAL_GOOD_THRESHOLD: - return 3 - elif signal > SIGNAL_FAIR_THRESHOLD: - return 2 - else: - return 1 + def _on_text_changed(self, text: str) -> None: + """Update the field border colour in real-time as the user types.""" + if not text: + self.setStyleSheet(self._VALID_STYLE) + return + try: + _ipaddress.IPv4Address(text.strip()) + self.setStyleSheet(self._VALID_STYLE) + except ValueError: + self.setStyleSheet(self._INVALID_STYLE) + self.update() class NetworkControlWindow(QtWidgets.QStackedWidget): - """Main network control window widget.""" + """Stacked-widget UI for all network control pages (Wi-Fi, Ethernet, VLAN, Hotspot). + + Owns a :class:`~BlocksScreen.lib.network.facade.NetworkManager` instance and + mediates between the UI pages and the async D-Bus worker. + """ - request_network_scan = pyqtSignal(name="scan-network") - new_ip_signal = pyqtSignal(str, name="ip-address-change") - get_hotspot_ssid = pyqtSignal(str, name="hotspot-ssid-name") - delete_network_signal = pyqtSignal(str, name="delete-network") + update_wifi_icon = QtCore.pyqtSignal(int, name="update-wifi-icon") - def __init__(self, parent: Optional[QtWidgets.QWidget] = None, /) -> None: - """Initialize the network control window.""" + def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: + """Construct the stacked-widget UI, wire all signals/slots, and request initial state.""" super().__init__(parent) if parent else super().__init__() self._init_instance_variables() self._setupUI() self._init_timers() self._init_model_view() - self._init_network_worker() + self._init_network_manager() self._setup_navigation_signals() self._setup_action_signals() self._setup_toggle_signals() @@ -296,271 +169,1853 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None, /) -> None: self._setup_keyboard() self._setup_scrollbar_signals() - self._network_list_worker.build() - self.request_network_scan.emit() + self._init_ui_state() self.hide() - # Initialize UI state - self._init_ui_state() + def _init_instance_variables(self) -> None: + """Initialize instance variables.""" + self._is_first_run = True + self._previous_panel: QtWidgets.QWidget | None = None + self._current_field: QtWidgets.QLineEdit | None = None + self._current_network_is_open = False + self._current_network_is_hidden = False + self._is_connecting = False + self._target_ssid: str | None = None + self._was_ethernet_connected: bool = False + self._initial_priority: ConnectionPriority = ConnectionPriority.MEDIUM + self._pending_operation: PendingOperation = PendingOperation.NONE + self._pending_expected_ip: str = ( + "" # IP to wait for before clearing WIFI_STATIC_IP loading + ) + self._cached_scan_networks: list[NetworkInfo] = [] + self._last_active_signal_bars: int = -1 + self._active_signal: int = 0 + # Key = SSID, value = (signal_bars, status_label, ListItem). + self._item_cache: dict[str, tuple[int, str, ListItem]] = {} + # Singleton items reused across reconcile calls (zero allocation). + self._separator_item: ListItem | None = None + self._hidden_network_item: ListItem | None = None def _init_ui_state(self) -> None: - """Initialize UI to a clean disconnected state.""" + """Initialize UI to clean disconnected state.""" self.loadingwidget.setVisible(False) + self._pending_operation = PendingOperation.NONE self._hide_all_info_elements() self._configure_info_box_centered() self.mn_info_box.setVisible(True) self.mn_info_box.setText( - "Network connection required.\n\nConnect to Wi-Fi\nor\nTurn on Hotspot" + "There no active\ninternet connection.\nConnect via Ethernet, Wi-Fi,\nor enable a mobile hotspot\n for online features.\nPrinting functions will\nstill work offline." ) - def _hide_all_info_elements(self) -> None: - """Hide ALL elements in the info panel (details, loading, info box).""" - # Hide network details - self.netlist_ip.setVisible(False) - self.netlist_ssuid.setVisible(False) - self.mn_info_seperator.setVisible(False) - self.line_2.setVisible(False) - self.netlist_strength.setVisible(False) - self.netlist_strength_label.setVisible(False) - self.line_3.setVisible(False) - self.netlist_security.setVisible(False) - self.netlist_security_label.setVisible(False) - # Hide loading - self.loadingwidget.setVisible(False) - # Hide info box - self.mn_info_box.setVisible(False) + def _init_network_manager(self) -> None: + """Initialize network manager and connect signals.""" + self._nm = NetworkManager(self) - def _init_instance_variables(self) -> None: - """Initialize all instance variables.""" - self._icon_provider = WifiIconProvider() - self._ongoing_update = False - self._is_first_run = True - self._networks: Dict[str, NetworkInfo] = {} - self._previous_panel: Optional[QtWidgets.QWidget] = None - self._current_field: Optional[QtWidgets.QLineEdit] = None - self._current_network_is_open = False - self._current_network_is_hidden = False - self._is_connecting = False - self._target_ssid: Optional[str] = None - self._last_displayed_ssid: Optional[str] = None - self._current_network_ssid: Optional[str] = ( - None # Track current network for priority - ) + self._nm.state_changed.connect(self._on_network_state_changed) - def _setupUI(self) -> None: - """Setup all UI elements programmatically.""" - self.setObjectName("wifi_stacked_page") - self.resize(800, 480) + self._nm.saved_networks_loaded.connect(self._on_saved_networks_loaded) - size_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum - ) - size_policy.setHorizontalStretch(0) - size_policy.setVerticalStretch(0) - size_policy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) - self.setSizePolicy(size_policy) - self.setMinimumSize(QtCore.QSize(0, 400)) - self.setMaximumSize(QtCore.QSize(16777215, 575)) - self.setStyleSheet( - "#wifi_stacked_page{\n" - " background-image: url(:/background/media/1st_background.png);\n" - "}\n" - ) + self._nm.connection_result.connect(self._on_operation_complete) - self._sdbus_network = SdbusNetworkManagerAsync() - self._popup = Popup(self) - self._right_arrow_icon = QtGui.QPixmap( - ":/arrow_icons/media/btn_icons/right_arrow.svg" - ) + self._nm.error_occurred.connect(self._on_network_error) - # Create all pages - self._setup_main_network_page() - self._setup_network_list_page() - self._setup_add_network_page() - self._setup_saved_connection_page() - self._setup_saved_details_page() - self._setup_hotspot_page() - self._setup_hidden_network_page() + self.rescan_button.clicked.connect(self._nm.scan_networks) - self.setCurrentIndex(0) + self.hotspot_name_input_field.setText(self._nm.hotspot_ssid) + self.hotspot_password_input_field.setText(self._nm.hotspot_password) - def _create_white_palette(self) -> QtGui.QPalette: - """Create a palette with white text.""" - palette = QtGui.QPalette() - white_brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - white_brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - grey_brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - grey_brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + self._nm.networks_scanned.connect(self._on_scan_complete) - for group in [ - QtGui.QPalette.ColorGroup.Active, - QtGui.QPalette.ColorGroup.Inactive, - ]: - palette.setBrush(group, QtGui.QPalette.ColorRole.WindowText, white_brush) - palette.setBrush(group, QtGui.QPalette.ColorRole.Text, white_brush) + self._nm.reconnect_complete.connect(self._on_reconnect_complete) - palette.setBrush( - QtGui.QPalette.ColorGroup.Disabled, - QtGui.QPalette.ColorRole.WindowText, - grey_brush, - ) - palette.setBrush( - QtGui.QPalette.ColorGroup.Disabled, - QtGui.QPalette.ColorRole.Text, - grey_brush, - ) + self._nm.hotspot_config_updated.connect(self._on_hotspot_config_updated) - return palette + self._prefill_ip_from_os() - def _setup_main_network_page(self) -> None: - """Setup the main network page.""" - self.main_network_page = QtWidgets.QWidget() - size_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.main_network_page.setSizePolicy(size_policy) - self.main_network_page.setObjectName("main_network_page") + def _prefill_ip_from_os(self) -> None: + """Read the current IP via SIOCGIFADDR ioctl and show it immediately. - main_layout = QtWidgets.QVBoxLayout(self.main_network_page) - main_layout.setObjectName("verticalLayout_14") + Bypasses NetworkManager D-Bus entirely — runs on the main thread, + costs a single syscall, and completes in microseconds. Called once + during init so the user never sees "IP: --" if a connection was + already active before the UI launched. + """ + _SIOCGIFADDR = 0x8915 + for iface in ("eth0", "wlan0"): + try: + with _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM) as sock: + ifreq = struct.pack("256s", iface[:15].encode()) + result = fcntl.ioctl(sock.fileno(), _SIOCGIFADDR, ifreq) + ip = _socket.inet_ntoa(result[20:24]) + if ip and not ip.startswith("0."): + self.netlist_ip.setText(f"IP: {ip}") + self.netlist_ip.setVisible(True) + logger.debug("Startup IP prefill from OS (%s): %s", iface, ip) + return + except OSError: + continue - # Header layout - header_layout = QtWidgets.QHBoxLayout() - header_layout.setObjectName("main_network_header_layout") + @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") + self.setCurrentIndex(self.indexOf(self.main_network_page)) - header_layout.addItem( - QtWidgets.QSpacerItem( - 60, - 60, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) + def _init_timers(self) -> None: + """Initialize timers.""" + self._load_timer = QTimer(self) + self._load_timer.setSingleShot(True) + self._load_timer.timeout.connect(self._handle_load_timeout) - self.network_main_title = QtWidgets.QLabel(parent=self.main_network_page) - title_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum - ) - self.network_main_title.setSizePolicy(title_policy) - self.network_main_title.setMinimumSize(QtCore.QSize(300, 0)) - self.network_main_title.setMaximumSize(QtCore.QSize(16777215, 60)) - font = QtGui.QFont() - font.setPointSize(20) - self.network_main_title.setFont(font) - self.network_main_title.setStyleSheet("color:white") - self.network_main_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.network_main_title.setText("Networks") - self.network_main_title.setObjectName("network_main_title") - header_layout.addWidget(self.network_main_title) + def _init_model_view(self) -> None: + """Initialize list model and view.""" + self._model = EntryListModel() + self._model.setParent(self.listView) + self._entry_delegate = EntryDelegate() + self.listView.setModel(self._model) + self.listView.setItemDelegate(self._entry_delegate) + self._entry_delegate.item_selected.connect(self._on_ssid_item_clicked) + self._configure_list_view_palette() - self.network_backButton = IconButton(parent=self.main_network_page) - self.network_backButton.setMinimumSize(QtCore.QSize(60, 60)) - self.network_backButton.setMaximumSize(QtCore.QSize(60, 60)) - self.network_backButton.setText("") - self.network_backButton.setFlat(True) - self.network_backButton.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + @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( + "Network state: %s, SSID: %s, IP: %s, eth: %s", + state.connectivity.name, + state.current_ssid, + state.current_ip, + state.ethernet_connected, ) - self.network_backButton.setObjectName("network_backButton") - header_layout.addWidget(self.network_backButton) - main_layout.addLayout(header_layout) + if ( + state.current_ssid + and state.signal_strength > 0 + and not state.hotspot_enabled + ): + self._active_signal = state.signal_strength + elif not state.current_ssid or state.hotspot_enabled: + self._active_signal = 0 - # Content layout - content_layout = QtWidgets.QHBoxLayout() - content_layout.setObjectName("main_network_content_layout") + if self._is_first_run: + self._handle_first_run(state) + self._emit_status_icon(state) + self._is_first_run = False + self._was_ethernet_connected = state.ethernet_connected + return - # Information frame - self.mn_information_layout = BlocksCustomFrame(parent=self.main_network_page) - info_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.mn_information_layout.setSizePolicy(info_policy) - self.mn_information_layout.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.mn_information_layout.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.mn_information_layout.setObjectName("mn_information_layout") + # Cable just plugged in while Wi-Fi is active -> disable Wi-Fi + if ( + state.ethernet_connected + and not self._was_ethernet_connected + and state.wifi_enabled + and not self._is_connecting + ): + logger.info("Ethernet connected — turning off Wi-Fi") + self._was_ethernet_connected = True + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.OFF + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = hotspot_btn.State.OFF + self._nm.set_wifi_enabled(False) + self._sync_ethernet_panel(state) + self._emit_status_icon(state) + return - info_layout = QtWidgets.QVBoxLayout(self.mn_information_layout) - info_layout.setObjectName("verticalLayout_3") + self._was_ethernet_connected = state.ethernet_connected + + # Ethernet panel visibility is pure hardware state (carrier + + # connection) and must update even while a loading operation is + # in-flight. + self._sync_ethernet_panel(state) + + # Sync toggle states (skipped when _is_connecting) + self._sync_toggle_states(state) + + if self._is_connecting: + # OFF operations: complete when radio off + no connection + if self._pending_operation in ( + PendingOperation.WIFI_OFF, + PendingOperation.HOTSPOT_OFF, + ): + if ( + not state.wifi_enabled + and not state.hotspot_enabled + and not state.current_ssid + ): + self._clear_loading() + self._display_disconnected_state() + self._emit_status_icon(state) + return + # Also catch partial-off (wifi still disabling, no ssid) + if not state.current_ssid and not state.hotspot_enabled: + self._clear_loading() + self._display_disconnected_state() + self._emit_status_icon(state) + return + # Still transitioning — keep loading visible + return - # SSID label - self.netlist_ssuid = QtWidgets.QLabel(parent=self.mn_information_layout) - ssid_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum - ) - self.netlist_ssuid.setSizePolicy(ssid_policy) - font = QtGui.QFont() - font.setPointSize(17) - self.netlist_ssuid.setFont(font) - self.netlist_ssuid.setStyleSheet("color: rgb(255, 255, 255);") - self.netlist_ssuid.setText("") - self.netlist_ssuid.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_ssuid.setObjectName("netlist_ssuid") - info_layout.addWidget(self.netlist_ssuid) + # Hotspot ON: complete when hotspot_enabled + SSID + IP + if self._pending_operation == PendingOperation.HOTSPOT_ON: + if state.hotspot_enabled and state.current_ssid and state.current_ip: + self._clear_loading() + self._display_connected_state(state) + self._emit_status_icon(state) + return + # Still waiting for hotspot to fully come up + return - # Separator - self.mn_info_seperator = QtWidgets.QFrame(parent=self.mn_information_layout) - self.mn_info_seperator.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.mn_info_seperator.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.mn_info_seperator.setObjectName("mn_info_seperator") - info_layout.addWidget(self.mn_info_seperator) + if self._pending_operation in ( + PendingOperation.WIFI_ON, + PendingOperation.CONNECT, + ): + if self._target_ssid and state.current_ssid == self._target_ssid: + if state.current_ip and state.connectivity in ( + ConnectivityState.FULL, + ConnectivityState.LIMITED, + ): + self._clear_loading() + self._display_connected_state(state) + self._emit_status_icon(state) + return + return - # IP label - self.netlist_ip = QtWidgets.QLabel(parent=self.mn_information_layout) - font = QtGui.QFont() - font.setPointSize(15) - self.netlist_ip.setFont(font) - self.netlist_ip.setStyleSheet("color: rgb(255, 255, 255);") - self.netlist_ip.setText("") - self.netlist_ip.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_ip.setObjectName("netlist_ip") - info_layout.addWidget(self.netlist_ip) + if self._pending_operation == PendingOperation.ETHERNET_ON: + if state.ethernet_connected: + self._clear_loading() + self._sync_ethernet_panel(state) + self._display_connected_state(state) + self._emit_status_icon(state) + return + return - # Connection info layout - conn_info_layout = QtWidgets.QHBoxLayout() - conn_info_layout.setObjectName("mn_conn_info") + if self._pending_operation == PendingOperation.ETHERNET_OFF: + if not state.ethernet_connected: + self._clear_loading() + self._sync_ethernet_panel(state) + self._display_disconnected_state() + self._emit_status_icon(state) + return + return - # Signal strength section - sg_info_layout = QtWidgets.QVBoxLayout() - sg_info_layout.setObjectName("mn_sg_info_layout") + # Wi-Fi static IP / DHCP reset: complete when we have the right IP. + if self._pending_operation == PendingOperation.WIFI_STATIC_IP: + ip = state.current_ip or "" + expected = self._pending_expected_ip + ip_matches = ip and (not expected or ip == expected) + if ip_matches: + self._pending_expected_ip = "" + self._clear_loading() + self._display_connected_state(state) + self._emit_status_icon(state) + return + # IP not yet correct — keep loading visible + return - self.netlist_strength_label = QtWidgets.QLabel( - parent=self.mn_information_layout - ) - self.netlist_strength_label.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(15) - self.netlist_strength_label.setFont(font) - self.netlist_strength_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_strength_label.setText("Signal\nStrength") - self.netlist_strength_label.setObjectName("netlist_strength_label") - sg_info_layout.addWidget(self.netlist_strength_label) + return - self.line_2 = QtWidgets.QFrame(parent=self.mn_information_layout) - self.line_2.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.line_2.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line_2.setObjectName("line_2") - sg_info_layout.addWidget(self.line_2) + # Normal (not connecting) display updates. + if state.ethernet_connected: + self._display_connected_state(state) + elif ( + state.current_ssid + and state.current_ip + and state.connectivity + in ( + ConnectivityState.FULL, + ConnectivityState.LIMITED, + ) + ): + self._display_connected_state(state) + elif state.wifi_enabled or state.hotspot_enabled: + self._display_wifi_on_no_connection() + else: + self._display_disconnected_state() - self.netlist_strength = QtWidgets.QLabel(parent=self.mn_information_layout) - font = QtGui.QFont() - font.setPointSize(11) - self.netlist_strength.setFont(font) - self.netlist_strength.setStyleSheet("color: rgb(255, 255, 255);") - self.netlist_strength.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_strength.setText("") - self.netlist_strength.setObjectName("netlist_strength") - sg_info_layout.addWidget(self.netlist_strength) + self._emit_status_icon(state) + self._sync_active_network_list_icon(state) - conn_info_layout.addLayout(sg_info_layout) + @pyqtSlot(list) + def _on_scan_complete(self, networks: list[NetworkInfo]) -> None: + """Receive scan results, filter/sort them, and rebuild the SSID list view. - # Security section - sec_info_layout = QtWidgets.QVBoxLayout() - sec_info_layout.setObjectName("mn_sec_info_layout") + Filters out the own hotspot SSID and networks with unsupported security + types before populating the list view. + """ + hotspot_ssid = self._nm.hotspot_ssid + filtered = [ + n + for n in networks + if n.ssid != hotspot_ssid and is_connectable_security(n.security_type) + ] - self.netlist_security_label = QtWidgets.QLabel( + current_ssid = self._nm.current_ssid + if current_ssid: + # 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 + for net in filtered + ] + active = next((n for n in filtered if n.ssid == current_ssid), None) + if active: + self._active_signal = active.signal_strength + self._last_active_signal_bars = signal_to_bars(self._active_signal) + + # Cache for signal-bar-change rebuilds + self._cached_scan_networks = filtered + + self._build_network_list_from_scan(filtered) + + # Update panel text + header icon (both read _active_signal) + if current_ssid: + self.netlist_strength.setText(f"{self._active_signal}%") + state = self._nm.current_state + self._emit_status_icon(state) + + @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) + def _on_operation_complete(self, result: ConnectionResult) -> None: + """Handle network operation completion.""" + logger.debug("Operation: success=%s, msg=%s", result.success, result.message) + + if result.success: + msg_lower = result.message.lower() + if "deleted" in msg_lower: + ssid_deleted = ( + self._target_ssid + ) # capture before _clear_loading wipes it + self._show_info_popup(result.message) + self._clear_loading() + self._display_wifi_on_no_connection() + self.setCurrentIndex(self.indexOf(self.main_network_page)) + if ssid_deleted: + self._patch_cached_network_status( + ssid_deleted, NetworkStatus.DISCOVERED + ) + elif "hotspot" in msg_lower and "activated" in msg_lower: + self._show_hotspot_qr( + self._nm.hotspot_ssid, + self._nm.hotspot_password, + self._nm.hotspot_security, + ) + elif "hotspot disabled" in msg_lower: + self.qrcode_img.clearPixmap() + self.qrcode_img.setText("Hotspot not active") + elif "wi-fi disabled" in msg_lower: + pass + elif "config updated" in msg_lower: + self._show_info_popup(result.message) + elif any( + skip in msg_lower + for skip in ( + "added", + "connecting", + "disconnected", + "wi-fi enabled", + ) + ): + if ( + ("added" in msg_lower or "connecting" in msg_lower) + and self._target_ssid + and not self._current_network_is_hidden + ): + # Hidden networks are not in the scan cache; the next scan + # will surface them once NM reports them as saved/active. + self._patch_cached_network_status( + self._target_ssid, NetworkStatus.SAVED + ) + elif self._pending_operation == PendingOperation.WIFI_STATIC_IP: + # Loading cleared by state machine (IP appears) or reconnect_complete. + # No popup — the updated IP in the header is the confirmation. + pass + else: + self._show_info_popup(result.message) + else: + msg_lower = result.message.lower() + + # Duplicate VLAN: clear loading and show the reason. + if result.error_code == "duplicate_vlan": + self._clear_loading() + self._show_error_popup(result.message) + return + + # When switching from ethernet to wifi, NM may report a + # device-mismatch error because the wired profile hasn't + # fully deactivated yet. Retry the connection instead of + # showing a confusing popup to the user. + is_transient_mismatch = ( + "not compatible with device" in msg_lower + or "mismatching interface" in msg_lower + or "not available because profile" in msg_lower + ) + if ( + is_transient_mismatch + and self._pending_operation + in (PendingOperation.WIFI_ON, PendingOperation.CONNECT) + and self._target_ssid + ): + logger.debug( + "Transient NM device-mismatch during wifi activation " + "— retrying in 2 s: %s", + result.message, + ) + ssid = self._target_ssid + QTimer.singleShot( + 2000, lambda _ssid=ssid: self._nm.connect_network(_ssid) + ) + return # Keep loading visible; state machine handles completion + + self._clear_loading() + self._show_error_popup(result.message) + + @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) + + if operation == "wifi_unavailable": + self.wifi_button.setEnabled(False) + self._show_error_popup( + "Wi-Fi interface unavailable. Please check hardware." + ) + return + + if operation == "device_reconnected": + self.wifi_button.setEnabled(True) + self._nm.refresh_state() + return + + self._clear_loading() + self._show_error_popup(f"Error: {message}") + + def _emit_status_icon(self, state: NetworkState) -> None: + """Emit the correct header icon key based on current state. + + Ethernet -> ETHERNET, Hotspot -> HOTSPOT, + Wi-Fi connected -> signal-strength key, otherwise -> 0-bar. + + Uses self._active_signal (the single source of truth) so the + header icon always matches the list icon and panel percentage. + """ + if state.ethernet_connected: + self.update_wifi_icon.emit(WifiIconKey.ETHERNET) + elif state.hotspot_enabled: + self.update_wifi_icon.emit(WifiIconKey.HOTSPOT) + elif state.current_ssid and state.connectivity in ( + ConnectivityState.FULL, + ConnectivityState.LIMITED, + ): + self.update_wifi_icon.emit( + WifiIconKey.from_signal(self._active_signal, False) + ) + else: + # Disconnected / no connection — 0-bar unprotected + self.update_wifi_icon.emit(WifiIconKey.from_bars(0, False)) + + def _sync_active_network_list_icon(self, state: NetworkState) -> None: + """Rebuild the wifi list when the active network's signal bars or status changes. + + Between scans, state polling may report a different signal strength + for the connected AP. Also corrects the status label from SAVED to + ACTIVE when the connection establishes after the last scan ran. + Invalidates the item cache for that SSID so the next reconcile picks + up the new icon/label, without touching other items. + + Uses self._active_signal as the single source of truth. + """ + if not self._cached_scan_networks or not state.current_ssid: + self._last_active_signal_bars = -1 + return + + new_bars = signal_to_bars(self._active_signal) + + # Also check whether the cached status already reflects ACTIVE. + # If not, we must rebuild even when bars haven't changed (e.g. the + # scan ran before the connection was fully established and marked the + # network SAVED instead of ACTIVE). + cached_active = next( + (n for n in self._cached_scan_networks if n.ssid == state.current_ssid), + None, + ) + status_needs_update = cached_active is not None and not cached_active.is_active + + if new_bars == self._last_active_signal_bars and not status_needs_update: + return # No visual change — skip the rebuild + + # Invalidate cache for the active SSID so _get_or_create_item + # creates a fresh ListItem with the updated signal icon and status. + self._item_cache.pop(state.current_ssid, None) + + # Update the cached entry with the authoritative signal and status + updated = [ + replace( + net, + signal_strength=self._active_signal, + network_status=NetworkStatus.ACTIVE, + ) + if net.ssid == state.current_ssid + else net + for net in self._cached_scan_networks + ] + + self._cached_scan_networks = updated + self._last_active_signal_bars = new_bars + self._build_network_list_from_scan(updated) + + def _handle_first_run(self, state: NetworkState) -> None: + """Run first-time UI setup once an initial state arrives (hide loading screen, etc.).""" + self.loadingwidget.setVisible(False) + self._is_connecting = False + self._pending_operation = PendingOperation.NONE + + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + + wifi_on = False + hotspot_on = False + + if state.ethernet_connected: + if state.wifi_enabled: + self._nm.set_wifi_enabled(False) + self._display_connected_state(state) + elif state.connectivity == ConnectivityState.FULL and state.current_ssid: + wifi_on = True + self._display_connected_state(state) + elif state.connectivity == ConnectivityState.LIMITED: + hotspot_on = True + self._display_connected_state(state) + self._show_hotspot_qr( + self._nm.hotspot_ssid, + self._nm.hotspot_password, + self._nm.hotspot_security, + ) + elif state.wifi_enabled: + wifi_on = True + self._display_wifi_on_no_connection() + else: + self._display_disconnected_state() + + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.ON if wifi_on else wifi_btn.State.OFF + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = ( + hotspot_btn.State.ON if hotspot_on else hotspot_btn.State.OFF + ) + + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + self.ethernet_button.setEnabled(True) + self._sync_ethernet_panel(state) + + def _sync_toggle_states(self, state: NetworkState) -> None: + """Synchronise Wi-Fi and hotspot toggle buttons to the current NetworkState + without loops.""" + if self._is_connecting: + return + + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + + wifi_on = False + hotspot_on = False + + if state.ethernet_connected: + pass + elif state.hotspot_enabled: + hotspot_on = True + elif state.wifi_enabled: + wifi_on = True + + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.ON if wifi_on else wifi_btn.State.OFF + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = ( + hotspot_btn.State.ON if hotspot_on else hotspot_btn.State.OFF + ) + + def _sync_ethernet_panel(self, state: NetworkState) -> None: + """Show/hide the ethernet panel and sync its toggle state. + + Visibility is driven by ``ethernet_carrier`` (cable physically + plugged in), while the toggle position reflects the active + connection state (``ethernet_connected``). + """ + eth_btn = self.ethernet_button.toggle_button + + with QtCore.QSignalBlocker(eth_btn): + eth_btn.state = ( + eth_btn.State.ON if state.ethernet_connected else eth_btn.State.OFF + ) + + # Panel visible as long as the cable is physically present + self.ethernet_button.setVisible(state.ethernet_carrier) + + def _display_connected_state(self, state: NetworkState) -> None: + """Display connected network information. + + Ethernet always takes display priority — if ``ethernet_connected`` + is True we show "Ethernet" even if a Wi-Fi SSID is still lingering + (e.g. during the brief overlap before NM finishes disabling wifi). + """ + self._hide_all_info_elements() + + is_ethernet = state.ethernet_connected + + self.netlist_ssuid.setText( + "Ethernet" if is_ethernet else (state.current_ssid or "") + ) + self.netlist_ssuid.setVisible(True) + + if state.current_ip: + self.netlist_ip.setText(f"IP: {state.current_ip}") + else: + self.netlist_ip.setText("IP: --") + self.netlist_ip.setVisible(True) + + # Show interface combo when ethernet is connected AND VLANs exist + if is_ethernet and state.active_vlans: + self.netlist_vlans_combo.blockSignals(True) + self.netlist_vlans_combo.clear() + self.netlist_vlans_combo.addItem( + f"Ethernet — {state.current_ip or '--'}", + state.current_ip or "", + ) + for v in state.active_vlans: + if v.is_dhcp: + ip_label = v.ip_address or "DHCP" + else: + ip_label = v.ip_address or "--" + self.netlist_vlans_combo.addItem( + f"VLAN {v.vlan_id} — {ip_label}", + v.ip_address or "", + ) + self.netlist_vlans_combo.setCurrentIndex(0) + self.netlist_vlans_combo.blockSignals(False) + self.netlist_vlans_combo.setVisible(True) + else: + self.netlist_vlans_combo.setVisible(False) + + self.mn_info_seperator.setVisible(True) + + if not is_ethernet and not state.hotspot_enabled: + signal_text = f"{self._active_signal}%" if self._active_signal > 0 else "--" + self.netlist_strength.setText(signal_text) + self.netlist_strength.setVisible(True) + self.netlist_strength_label.setVisible(True) + self.line_2.setVisible(True) + + sec_text = state.security_type.upper() if state.security_type else "OPEN" + self.netlist_security.setText(sec_text) + self.netlist_security.setVisible(True) + self.netlist_security_label.setVisible(True) + self.line_3.setVisible(True) + + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + self.ethernet_button.setEnabled(True) + + self.update() + + def _display_disconnected_state(self) -> None: + """Display disconnected state — both toggles OFF.""" + self._hide_all_info_elements() + + self.mn_info_box.setVisible(True) + self.mn_info_box.setText( + "There no active\ninternet connection.\nConnect via Ethernet, Wi-Fi,\nor enable a mobile hotspot\n for online features.\nPrinting functions will\nstill work offline." + ) + + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + self.ethernet_button.setEnabled(True) + + self.update() + + def _display_wifi_on_no_connection(self) -> None: + """Display info panel when Wi-Fi is on but not connected. + + Uses the same layout as the connected state but shows + 'No network connected' and empty fields. + """ + self._hide_all_info_elements() + + self.netlist_ssuid.setText("No network connected") + self.netlist_ssuid.setVisible(True) + + self.netlist_ip.setText("IP: --") + self.netlist_ip.setVisible(True) + + self.mn_info_seperator.setVisible(True) + + self.netlist_strength.setText("--") + self.netlist_strength.setVisible(True) + self.netlist_strength_label.setVisible(True) + self.line_2.setVisible(True) + + self.netlist_security.setText("--") + self.netlist_security.setVisible(True) + self.netlist_security_label.setVisible(True) + self.line_3.setVisible(True) + + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + self.ethernet_button.setEnabled(True) + + self.update() + + def _hide_all_info_elements(self) -> None: + """Hide all info panel elements.""" + self.netlist_ip.setVisible(False) + self.netlist_ssuid.setVisible(False) + self.netlist_vlans_combo.setVisible(False) + self.mn_info_seperator.setVisible(False) + self.line_2.setVisible(False) + self.netlist_strength.setVisible(False) + self.netlist_strength_label.setVisible(False) + self.line_3.setVisible(False) + self.netlist_security.setVisible(False) + self.netlist_security_label.setVisible(False) + self.loadingwidget.setVisible(False) + self.mn_info_box.setVisible(False) + + def _set_loading_state( + self, loading: bool, timeout_ms: int = LOAD_TIMEOUT_MS + ) -> None: + """Set loading state with visible feedback text.""" + self.wifi_button.setEnabled(not loading) + self.hotspot_button.setEnabled(not loading) + self.ethernet_button.setEnabled(not loading) + + if loading: + self._is_connecting = True + self._hide_all_info_elements() + self.loadingwidget.setVisible(True) + + if self._load_timer.isActive(): + self._load_timer.stop() + self._load_timer.start(timeout_ms) + else: + self._is_connecting = False + self._target_ssid = None + self._pending_operation = PendingOperation.NONE + self.loadingwidget.setVisible(False) + + if self._load_timer.isActive(): + self._load_timer.stop() + self.update() + + def _clear_loading(self) -> None: + """Hide the loading widget and re-enable the full UI.""" + self._set_loading_state(False) + + def _handle_load_timeout(self) -> None: + """Hide the loading widget if it is still visible after the timeout fires.""" + if not self.loadingwidget.isVisible(): + return + + state = self._nm.current_state + if ( + self._pending_operation == PendingOperation.HOTSPOT_ON + and state.hotspot_enabled + and state.current_ssid + ): + self._clear_loading() + self._display_connected_state(state) + return + if ( + self._pending_operation + in (PendingOperation.WIFI_ON, PendingOperation.CONNECT) + and self._target_ssid + ): + if state.current_ssid == self._target_ssid and state.current_ip: + self._clear_loading() + self._display_connected_state(state) + return + if ( + self._pending_operation == PendingOperation.ETHERNET_ON + and state.ethernet_connected + ): + self._clear_loading() + self._sync_ethernet_panel(state) + self._display_connected_state(state) + return + + # Static IP / DHCP reset — if a state with an IP has arrived, accept it. + if self._pending_operation == PendingOperation.WIFI_STATIC_IP: + if state.current_ip: + self._clear_loading() + self._display_connected_state(state) + return + # No IP yet after timeout — clear loading and show whatever state we have. + self._clear_loading() + if state.current_ssid: + self._display_connected_state(state) + else: + self._display_disconnected_state() + return + + self._clear_loading() + self._hide_all_info_elements() + self._configure_info_box_centered() + self.mn_info_box.setVisible(True) + + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + eth_btn = self.ethernet_button.toggle_button + + if self._pending_operation == PendingOperation.ETHERNET_ON: + self.mn_info_box.setText( + "Ethernet Connection Failed.\nCheck that the cable\nis plugged in." + ) + with QtCore.QSignalBlocker(eth_btn): + eth_btn.state = eth_btn.State.OFF + elif wifi_btn.state == wifi_btn.State.ON: + self.mn_info_box.setText( + "Wi-Fi Connection Failed.\nThe connection attempt\ntimed out." + ) + elif hotspot_btn.state == hotspot_btn.State.ON: + self.mn_info_box.setText( + "Hotspot Setup Failed.\nPlease restart the hotspot." + ) + else: + self.mn_info_box.setText( + "Loading timed out.\nPlease check your connection\n and \ntry again." + ) + + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + self.ethernet_button.setEnabled(True) + self._show_error_popup("Connection timed out. Please try again.") + + def _configure_info_box_centered(self) -> None: + """Centre-align the info box text and enable word-wrap.""" + self.mn_info_box.setWordWrap(True) + self.mn_info_box.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + @QtCore.pyqtSlot(object, name="stateChange") + def _on_toggle_state(self, new_state) -> None: + """Route a toggle-button state change to the correct handler (Wi-Fi or hotspot).""" + sender_button = self.sender() + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + eth_btn = self.ethernet_button.toggle_button + is_on = new_state == sender_button.State.ON + + if sender_button is wifi_btn: + self._handle_wifi_toggle(is_on) + elif sender_button is hotspot_btn: + self._handle_hotspot_toggle(is_on) + elif sender_button is eth_btn: + self._handle_ethernet_toggle(is_on) + + # Both OFF state is now handled by _on_network_state_changed + # when the worker emits the disconnected state. + + def _handle_wifi_toggle(self, is_on: bool) -> None: + """Enable or disable Wi-Fi, enforcing the ethernet/hotspot mutual-exclusion rule.""" + if not is_on: + self._target_ssid = None + self._pending_operation = PendingOperation.WIFI_OFF + self._set_loading_state(True) + self._nm.set_wifi_enabled(False) + return + + hotspot_btn = self.hotspot_button.toggle_button + eth_btn = self.ethernet_button.toggle_button + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = hotspot_btn.State.OFF + with QtCore.QSignalBlocker(eth_btn): + eth_btn.state = eth_btn.State.OFF + + self._nm.set_wifi_enabled(True) + + # NOTE: set_wifi_enabled is dispatched to the worker — cached state + # is STALE here (may still show ethernet). Always proceed to the + # saved-network connection path. + + saved = self._nm.saved_networks + wifi_networks = [n for n in saved if "ap" not in n.mode] + + if not wifi_networks: + self._show_warning_popup("No saved Wi-Fi networks. Please add one first.") + self._display_wifi_on_no_connection() + return + + # Sort by priority descending (highest priority first), + # then by timestamp as tiebreaker — this gives "reconnect to + # highest-priority saved network" behaviour. + wifi_networks.sort(key=lambda n: (n.priority, n.timestamp), reverse=True) + + self._target_ssid = wifi_networks[0].ssid + self._pending_operation = PendingOperation.WIFI_ON + self._set_loading_state(True) + + # 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)) + + def _handle_hotspot_toggle(self, is_on: bool) -> None: + """Enable or disable the hotspot, enforcing the ethernet/Wi-Fi mutual-exclusion rule.""" + if not is_on: + self._target_ssid = None + self._pending_operation = PendingOperation.HOTSPOT_OFF + self._set_loading_state(True) + self._nm.toggle_hotspot(False) + return + + wifi_btn = self.wifi_button.toggle_button + eth_btn = self.ethernet_button.toggle_button + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.OFF + with QtCore.QSignalBlocker(eth_btn): + eth_btn.state = eth_btn.State.OFF + + self._target_ssid = None + self._pending_operation = PendingOperation.HOTSPOT_ON + self._set_loading_state(True) + + hotspot_name = self.hotspot_name_input_field.text() or "" + hotspot_pass = self.hotspot_password_input_field.text() or "" + hotspot_sec = "wpa-psk" + + # Single atomic call: disconnect + delete stale + create + activate + self._nm.create_hotspot(hotspot_name, hotspot_pass, hotspot_sec) + + def _handle_ethernet_toggle(self, is_on: bool) -> None: + """Handle ethernet toggle with mutual exclusion.""" + if is_on: + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.OFF + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = hotspot_btn.State.OFF + + self._target_ssid = None + self._pending_operation = PendingOperation.ETHERNET_ON + self._set_loading_state(True) + self._nm.connect_ethernet() + return + + self._target_ssid = None + self._pending_operation = PendingOperation.ETHERNET_OFF + self._set_loading_state(True) + self._nm.disconnect_ethernet() + + @QtCore.pyqtSlot(str, str, str) + def _on_hotspot_config_updated( + self, + ssid: str, + password: str, + security: str, # pylint: disable=unused-argument + ) -> None: + """Refresh hotspot UI fields when worker reports updated config.""" + self.hotspot_name_input_field.setText(ssid) + self.hotspot_password_input_field.setText(password) + + def _on_hotspot_config_save(self) -> None: + """Save hotspot configuration changes. + + Reads new name/password from the UI fields, asks the worker to + delete old profiles and create a new one. If the hotspot was + active, it will be re-activated with the new config (with a + loading screen shown). + """ + new_name = self.hotspot_name_input_field.text().strip() + new_password = self.hotspot_password_input_field.text().strip() + + if not new_name: + self._show_error_popup("Hotspot name cannot be empty.") + return + + if len(new_password) < 8: + self._show_error_popup("Hotspot password must be at least 8 characters.") + return + + old_ssid = self._nm.hotspot_ssid + + self.setCurrentIndex(self.indexOf(self.main_network_page)) + + # If hotspot is currently active, show loading for the reconnect + hotspot_btn = self.hotspot_button.toggle_button + if hotspot_btn.state == hotspot_btn.State.ON: + self._target_ssid = None + self._pending_operation = PendingOperation.HOTSPOT_ON + self._set_loading_state(True) + + new_security = "wpa-psk" + self._nm.update_hotspot_config(old_ssid, new_name, new_password, new_security) + + @QtCore.pyqtSlot() + def _on_hotspot_activate(self) -> None: + """Validate UI fields and immediately create + activate the hotspot.""" + new_name = self.hotspot_name_input_field.text().strip() + new_password = self.hotspot_password_input_field.text().strip() + + if not new_name: + self._show_error_popup("Hotspot name cannot be empty.") + return + + if len(new_password) < 8: + self._show_error_popup("Hotspot password must be at least 8 characters.") + return + + # Mutual exclusion: turn off Wi-Fi and Ethernet + wifi_btn = self.wifi_button.toggle_button + eth_btn = self.ethernet_button.toggle_button + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.OFF + with QtCore.QSignalBlocker(eth_btn): + eth_btn.state = eth_btn.State.OFF + + hotspot_btn = self.hotspot_button.toggle_button + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = hotspot_btn.State.ON + + self._target_ssid = None + self._pending_operation = PendingOperation.HOTSPOT_ON + self.setCurrentIndex(self.indexOf(self.main_network_page)) + self._set_loading_state(True) + self._nm.create_hotspot(new_name, new_password, "wpa-psk") + + def _show_hotspot_qr(self, ssid: str, password: str, security: str) -> None: + """Generate and display a WiFi QR code on the hotspot page.""" + try: + img = generate_wifi_qrcode(ssid, password, security) + pixmap = QtGui.QPixmap.fromImage(img) + self.qrcode_img.setText("") + self.qrcode_img.setPixmap(pixmap) + except Exception as exc: # pylint: disable=broad-except + logger.debug("QR code generation failed: %s", exc) + self.qrcode_img.clearPixmap() + self.qrcode_img.setText("QR error") + + def _on_ethernet_button_clicked(self) -> None: + """Navigate to the ethernet/VLAN settings page when the ethernet button is clicked.""" + if ( + self.ethernet_button.toggle_button.state + == self.ethernet_button.toggle_button.State.OFF + ): + self._show_warning_popup("Turn on Ethernet first.") + return + self.setCurrentIndex(self.indexOf(self.vlan_page)) + + def _on_vlan_apply(self) -> None: + """Validate VLAN fields and call ``create_vlan_connection`` on the facade.""" + vlan_id = self.vlan_id_spinbox.value() + ip_addr = self.vlan_ip_field.text().strip() + mask = self.vlan_mask_field.text().strip() + gateway = self.vlan_gateway_field.text().strip() + dns1 = self.vlan_dns1_field.text().strip() + dns2 = self.vlan_dns2_field.text().strip() + + if not ip_addr: + self._show_error_popup("IP address is required.") + return + if not self.vlan_ip_field.is_valid(): + self._show_error_popup("Invalid IP address.") + return + if not self.vlan_mask_field.is_valid_mask(): + self._show_error_popup("Invalid subnet mask.") + return + if gateway and not self.vlan_gateway_field.is_valid(): + self._show_error_popup("Invalid gateway address.") + return + if dns1 and not self.vlan_dns1_field.is_valid(): + self._show_error_popup("Invalid primary DNS.") + return + if dns2 and not self.vlan_dns2_field.is_valid(): + self._show_error_popup("Invalid secondary DNS.") + return + + self.setCurrentIndex(self.indexOf(self.main_network_page)) + self._pending_operation = PendingOperation.ETHERNET_ON + self._set_loading_state(True) + self._nm.create_vlan_connection( + vlan_id, + ip_addr, + mask, + gateway, + dns1, + dns2, + ) + self._nm.request_state_soon(delay_ms=3000) + + def _on_vlan_delete(self) -> None: + """Read the VLAN ID from the spinbox and request deletion via the facade.""" + vlan_id = self.vlan_id_spinbox.value() + self._nm.delete_vlan_connection(vlan_id) + self._show_warning_popup(f"VLAN {vlan_id} profile removed.") + + def _on_interface_combo_changed(self, index: int) -> None: + """Swap the displayed IP when the user selects a different interface.""" + ip = self.netlist_vlans_combo.itemData(index) + if ip is not None: + self.netlist_ip.setText(f"IP: {ip}" if ip else "IP: --") + + def _on_wifi_static_ip_clicked(self) -> None: + """Navigate from saved details page to WiFi static IP page.""" + ssid = self.snd_name.text() + self.wifi_sip_title.setText(ssid) + self.wifi_sip_ip_field.clear() + self.wifi_sip_mask_field.clear() + self.wifi_sip_gateway_field.clear() + self.wifi_sip_dns1_field.clear() + self.wifi_sip_dns2_field.clear() + + # Enable "Reset to DHCP" only when the profile is currently using a + # static IP — if it is already DHCP there is nothing to reset. + saved = self._nm.get_saved_network(ssid) + is_dhcp = saved.is_dhcp if saved else True + self.wifi_sip_dhcp_button.setEnabled(not is_dhcp) + self.wifi_sip_dhcp_button.setToolTip( + "Already using DHCP" if is_dhcp else "Reset this network to DHCP" + ) + + self.setCurrentIndex(self.indexOf(self.wifi_static_ip_page)) + + def _on_wifi_static_ip_apply(self) -> None: + """Validate static-IP fields and apply them to the current Wi-Fi connection. + + Mirrors the VLAN-creation UX: navigate to the main panel immediately, + show the loading overlay, and clear it silently once ``reconnect_complete`` + fires (no popup — the updated IP appears in the panel header instead). + """ + ssid = self.wifi_sip_title.text() + ip_addr = self.wifi_sip_ip_field.text().strip() + mask = self.wifi_sip_mask_field.text().strip() + gateway = self.wifi_sip_gateway_field.text().strip() + dns1 = self.wifi_sip_dns1_field.text().strip() + dns2 = self.wifi_sip_dns2_field.text().strip() + + if not self.wifi_sip_ip_field.is_valid(): + self._show_error_popup("Invalid IP address.") + return + if not self.wifi_sip_mask_field.is_valid_mask(): + self._show_error_popup("Invalid subnet mask.") + return + if gateway and not self.wifi_sip_gateway_field.is_valid(): + self._show_error_popup("Invalid gateway address.") + return + if dns1 and not self.wifi_sip_dns1_field.is_valid(): + self._show_error_popup("Invalid primary DNS.") + return + if dns2 and not self.wifi_sip_dns2_field.is_valid(): + self._show_error_popup("Invalid secondary DNS.") + return + + self.setCurrentIndex(self.indexOf(self.main_network_page)) + self._pending_operation = PendingOperation.WIFI_STATIC_IP + self._pending_expected_ip: str = ip_addr # hold loading until this IP appears + self._active_signal = 0 # reset so signal shows "--" during reconnect + self._set_loading_state(True) + self._nm.update_wifi_static_ip(ssid, ip_addr, mask, gateway, dns1, dns2) + self._nm.request_state_soon(delay_ms=3000) + + def _on_wifi_reset_dhcp(self) -> None: + """Reset the current Wi-Fi connection back to DHCP via the facade. + + Same loading-screen pattern as static IP — no popup on success. + """ + ssid = self.wifi_sip_title.text() + self.setCurrentIndex(self.indexOf(self.main_network_page)) + self._pending_operation = PendingOperation.WIFI_STATIC_IP + self._pending_expected_ip: str = "" # any IP confirms DHCP success + self._active_signal = 0 # reset so signal shows "--" during reconnect + self._set_loading_state(True) + self._nm.reset_wifi_to_dhcp(ssid) + self._nm.request_state_soon(delay_ms=3000) + + def _build_network_list_from_scan(self, networks: list[NetworkInfo]) -> None: + """Build/update network list from scan results. + + Uses the model's built-in reconcile() with an item cache so that + ListItems are only allocated for networks whose visual state + actually changed (different signal bars or status label). + Unchanged items are reused from the cache — zero allocation. + """ + self.listView.blockSignals(True) + + desired_items: list[ListItem] = [] + + saved = [n for n in networks if n.is_saved] + unsaved = [n for n in networks if not n.is_saved] + + for net in saved: + item = self._get_or_create_item(net) + if item is not None: + desired_items.append(item) + + if saved and unsaved: + desired_items.append(self._get_separator_item()) + + for net in unsaved: + item = self._get_or_create_item(net) + if item is not None: + desired_items.append(item) + + desired_items.append(self._get_hidden_network_item()) + + self._model.reconcile(desired_items, self._item_key) + self._entry_delegate.prev_index = 0 + self._sync_scrollbar() + + # Evict cache entries for SSIDs no longer in scan results + live_ssids = {n.ssid for n in networks} + stale = [k for k in self._item_cache if k not in live_ssids] + for k in stale: + del self._item_cache[k] + + self.listView.blockSignals(False) + self.listView.update() + + def _patch_cached_network_status(self, ssid: str, status: NetworkStatus) -> None: + """Optimistically update one entry in the scan cache and rebuild the list. + + Called immediately after add/delete so the list reflects the change + without waiting for the next scan cycle. + """ + self._cached_scan_networks = [ + replace(n, network_status=status) if n.ssid == ssid else n + for n in self._cached_scan_networks + ] + self._item_cache.pop(ssid, None) + self._build_network_list_from_scan(self._cached_scan_networks) + + def _get_or_create_item(self, network: NetworkInfo) -> ListItem | None: + """Return a cached ListItem if the network's visual state is + unchanged, otherwise create a new one and update the cache. + + Visual state = (signal_bars, status_label). When both match + the cached entry, the existing ListItem is returned as-is — + no QPixmap lookup, no allocation. + """ + if network.is_hidden or is_hidden_ssid(network.ssid): + return None + if not is_connectable_security(network.security_type): + return None + + bars = signal_to_bars(network.signal_strength) + status = network.status + ssid = network.ssid + + cached = self._item_cache.get(ssid) + if cached is not None: + cached_bars, cached_status, cached_item = cached + if cached_bars == bars and cached_status == status: + return cached_item + + item = self._make_network_item(network) + if item is not None: + self._item_cache[ssid] = (bars, status, item) + return item + + def _get_separator_item(self) -> ListItem: + """Return the singleton separator item (created once, reused forever).""" + if self._separator_item is None: + self._separator_item = self._make_separator_item() + return self._separator_item + + def _get_hidden_network_item(self) -> ListItem: + """Return the singleton 'Connect to Hidden Network' item.""" + if self._hidden_network_item is None: + self._hidden_network_item = self._make_hidden_network_item() + return self._hidden_network_item + + @staticmethod + def _item_key(item: ListItem) -> str: + """Unique key for a list item (SSID, or sentinel for special rows).""" + if item.not_clickable and not item.text: + return "__separator__" + return item.text + + def _make_network_item(self, network: NetworkInfo) -> ListItem | None: + """Create a ListItem for a scanned network, or None if hidden/unsupported.""" + if network.is_hidden or is_hidden_ssid(network.ssid): + return None + if not is_connectable_security(network.security_type): + return None + + wifi_pixmap = WifiIconProvider.get_pixmap( + network.signal_strength, not network.is_open + ) + + return ListItem( + text=network.ssid, + left_icon=wifi_pixmap, + right_text=network.status, + right_icon=self._right_arrow_icon, + selected=False, + allow_check=False, + _lfontsize=17, + _rfontsize=12, + height=80, + not_clickable=False, + ) + + @staticmethod + def _make_separator_item() -> ListItem: + """Create a non-clickable separator item.""" + return ListItem( + text="", + left_icon=None, + right_text="", + right_icon=None, + selected=False, + allow_check=False, + _lfontsize=17, + _rfontsize=12, + height=20, + not_clickable=True, + ) + + def _make_hidden_network_item(self) -> ListItem: + """Create the 'Connect to Hidden Network' entry.""" + return ListItem( + text="Connect to Hidden Network...", + left_icon=self._hiden_network_icon, + right_text="", + right_icon=self._right_arrow_icon, + selected=False, + allow_check=False, + _lfontsize=17, + _rfontsize=12, + height=80, + not_clickable=False, + ) + + @QtCore.pyqtSlot(ListItem, name="ssid-item-clicked") + def _on_ssid_item_clicked(self, item: ListItem) -> None: + """Handle a tap on an SSID list item: show the save or connect page as appropriate.""" + ssid = item.text + + if is_hidden_ssid(ssid) or ssid == "Connect to Hidden Network...": + self.setCurrentIndex(self.indexOf(self.hidden_network_page)) + return + + network = self._nm.get_network_info(ssid) + if not network: + return + + # Reject unsupported security types (defence-in-depth) + if not is_connectable_security(network.security_type): + self._show_error_popup( + f"'{ssid}' uses unsupported security " + f"({network.security_type}).\n" + "Only WPA/WPA2 networks are supported." + ) + return + + if network.is_saved: + self._show_saved_network_page(network) + else: + self._show_add_network_page(network) + + def _show_saved_network_page(self, network: NetworkInfo) -> None: + """Populate and navigate to the saved-network detail page for *network*.""" + ssid = network.ssid + + self.saved_connection_network_name.setText(ssid) + self.snd_name.setText(ssid) + + self.saved_connection_change_password_field.clear() + self.saved_connection_change_password_field.setPlaceholderText( + "Enter new password" + ) + self.saved_connection_change_password_field.setHidden(True) + if self.saved_connection_change_password_view.isChecked(): + self.saved_connection_change_password_view.setChecked(False) + + saved = self._nm.get_saved_network(ssid) + + if saved: + self._set_priority_button(saved.priority) + # Track initial values for change detection + self._initial_priority = self._get_selected_priority() + else: + self._initial_priority = ConnectionPriority.MEDIUM + + # Signal strength — for the active network, use the unified + # _active_signal so the details page matches the main panel + # and header icon exactly. + is_active = ssid == self._nm.current_ssid + if is_active and self._active_signal > 0: + signal_value = self._active_signal + else: + signal_value = network.signal_strength + + signal_text = f"{signal_value}%" if signal_value >= 0 else "--%" + + self.saved_connection_signal_strength_info_frame.setText(signal_text) + + if network.is_open: + self.saved_connection_security_type_info_label.setText("OPEN") + else: + sec_type = saved.security_type if saved else "WPA" + self.saved_connection_security_type_info_label.setText(sec_type.upper()) + + self.network_activate_btn.setDisabled(is_active) + self.sn_info.setText("Active Network" if is_active else "Saved Network") + + self.setCurrentIndex(self.indexOf(self.saved_connection_page)) + self.frame.update() + + def _show_add_network_page(self, network: NetworkInfo) -> None: + """Populate and navigate to the add-network page for *network*.""" + self._current_network_is_open = network.is_open + self._current_network_is_hidden = False + + self.add_network_network_label.setText(network.ssid) + self.add_network_password_field.clear() + + self.frame_2.setVisible(not network.is_open) + self.add_network_validation_button.setText( + "Connect" if network.is_open else "Activate" + ) + + self.setCurrentIndex(self.indexOf(self.add_network_page)) + + def _set_priority_button(self, priority: int | None) -> None: + """Set priority button based on value.""" + if priority is not None and priority >= ConnectionPriority.HIGH.value: + target = self.high_priority_btn + elif priority is not None and priority <= ConnectionPriority.LOW.value: + target = self.low_priority_btn + else: + target = self.med_priority_btn + + logger.debug( + "Setting priority button: priority=%r -> %s", priority, target.text() + ) + + target.setChecked(True) + + self.high_priority_btn.update() + self.med_priority_btn.update() + self.low_priority_btn.update() + + def _get_selected_priority(self) -> ConnectionPriority: + """Return the ``ConnectionPriority`` matching the currently selected radio button.""" + checked = self.priority_btn_group.checkedButton() + logger.debug( + "Priority selection: checked=%s, h=%s m=%s l=%s", + checked.text() if checked else "None", + self.high_priority_btn.isChecked(), + self.med_priority_btn.isChecked(), + self.low_priority_btn.isChecked(), + ) + + if checked is self.high_priority_btn: + return ConnectionPriority.HIGH + elif checked is self.low_priority_btn: + return ConnectionPriority.LOW + + if self.high_priority_btn.isChecked(): + return ConnectionPriority.HIGH + if self.low_priority_btn.isChecked(): + return ConnectionPriority.LOW + return ConnectionPriority.MEDIUM + + @QtCore.pyqtSlot(name="add-network") + def _add_network(self) -> None: + """Add network - non-blocking.""" + self.add_network_validation_button.setEnabled(False) + + ssid = self.add_network_network_label.text() + password = self.add_network_password_field.text() + + if not password and not self._current_network_is_open: + self._show_error_popup("Password field cannot be empty.") + self.add_network_validation_button.setEnabled(True) + return + + self._target_ssid = ssid + self._pending_operation = PendingOperation.CONNECT + self._set_loading_state(True) + + self.add_network_password_field.clear() + self.setCurrentIndex(self.indexOf(self.main_network_page)) + + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.ON + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = hotspot_btn.State.OFF + + self._nm.add_network(ssid, password) + + self.add_network_validation_button.setEnabled(True) + + def _on_activate_network(self) -> None: + """Activate the network shown on the saved-connection page.""" + ssid = self.saved_connection_network_name.text() + + self._target_ssid = ssid + self._pending_operation = PendingOperation.CONNECT + self._set_loading_state(True) + + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.ON + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = hotspot_btn.State.OFF + + self.setCurrentIndex(self.indexOf(self.main_network_page)) + self._nm.connect_network(ssid) + + def _on_delete_network(self) -> None: + """Delete the profile shown on the saved-connection page and navigate back.""" + ssid = self.saved_connection_network_name.text() + self._target_ssid = ssid + self._nm.delete_network(ssid) + + def _on_save_network_details(self) -> None: + """Save network settings changes (password / priority). + + Only performs an update if the user actually changed something. + Shows a confirmation popup on success. + """ + ssid = self.saved_connection_network_name.text() + password = self.saved_connection_change_password_field.text() + priority = self._get_selected_priority() + + password_changed = bool(password) + priority_changed = priority != self._initial_priority + + if not password_changed and not priority_changed: + self._show_info_popup("No changes to save.") + return + + self._nm.update_network( + ssid, + password=password or "", + priority=priority.value, + ) + + self._nm.load_saved_networks() + + # Update tracked baseline so a second press won't re-save + self._initial_priority = priority + + self.saved_connection_change_password_field.clear() + + def _on_hidden_network_connect(self) -> None: + """Connect to hidden network - non-blocking.""" + ssid = self.hidden_network_ssid_field.text().strip() + password = self.hidden_network_password_field.text() + + if not ssid: + self._show_error_popup("Please enter a network name.") + return + + self._current_network_is_hidden = True + self._current_network_is_open = not password + self._target_ssid = ssid + self._pending_operation = PendingOperation.CONNECT + self._set_loading_state(True) + + self.hidden_network_ssid_field.clear() + self.hidden_network_password_field.clear() + + self.setCurrentIndex(self.indexOf(self.main_network_page)) + + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.ON + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = hotspot_btn.State.OFF + + self._nm.add_network(ssid, password) + + def _show_error_popup(self, message: str, timeout: int = 6000) -> None: + """Display *message* in an error-styled info box with an auto-dismiss *timeout* ms.""" + self._popup.raise_() + self._popup.new_message( + message_type=Popup.MessageType.ERROR, + message=message, + timeout=timeout, + userInput=False, + ) + + def _show_info_popup(self, message: str, timeout: int = 4000) -> None: + """Display *message* in a neutral info box with an auto-dismiss *timeout* ms.""" + self._popup.raise_() + self._popup.new_message( + message_type=Popup.MessageType.INFO, + message=message, + timeout=timeout, + userInput=False, + ) + + def _show_warning_popup(self, message: str, timeout: int = 5000) -> None: + """Display *message* in a warning-styled info box with an auto-dismiss *timeout* ms.""" + self._popup.raise_() + self._popup.new_message( + message_type=Popup.MessageType.WARNING, + message=message, + timeout=timeout, + userInput=False, + ) + + def close(self) -> bool: + """Close and cleanup.""" + self._nm.close() + return super().close() + + def closeEvent(self, event: QtGui.QCloseEvent | None) -> None: + """Handle close event.""" + if self._load_timer.isActive(): + self._load_timer.stop() + super().closeEvent(event) + + def showEvent(self, event: QtGui.QShowEvent | None) -> None: + """Handle show event.""" + self._nm.refresh_state() + super().showEvent(event) + + def _setupUI(self) -> None: + """Build and lay out the entire stacked-widget UI tree.""" + self.setObjectName("wifi_stacked_page") + self.resize(800, 480) + + size_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum + ) + size_policy.setHorizontalStretch(0) + size_policy.setVerticalStretch(0) + size_policy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) + self.setSizePolicy(size_policy) + self.setMinimumSize(QtCore.QSize(0, 400)) + self.setMaximumSize(QtCore.QSize(16777215, 575)) + self.setStyleSheet( + "#wifi_stacked_page{\n" + " background-image: url(:/background/media/1st_background.png);\n" + "}\n" + ) + + self._popup = Popup(self) + self._right_arrow_icon = PixmapCache.get( + ":/arrow_icons/media/btn_icons/right_arrow.svg" + ) + self._hiden_network_icon = PixmapCache.get( + ":/network/media/btn_icons/network/0bar_wifi_protected.svg" + ) + + self._setup_main_network_page() + self._setup_network_list_page() + self._setup_add_network_page() + self._setup_saved_connection_page() + self._setup_saved_details_page() + self._setup_hotspot_page() + self._setup_hidden_network_page() + self._setup_vlan_page() + self._setup_wifi_static_ip_page() + + self.setCurrentIndex(0) + + def _create_white_palette(self) -> QtGui.QPalette: + """Return a QPalette with all roles set to white (flat widget backgrounds).""" + palette = QtGui.QPalette() + white_brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + white_brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + grey_brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) + grey_brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + + for group in [ + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorGroup.Inactive, + ]: + palette.setBrush(group, QtGui.QPalette.ColorRole.WindowText, white_brush) + palette.setBrush(group, QtGui.QPalette.ColorRole.Text, white_brush) + + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.WindowText, + grey_brush, + ) + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.Text, + grey_brush, + ) + + return palette + + def _setup_main_network_page(self) -> None: + """Setup the main network page.""" + self.main_network_page = QtWidgets.QWidget() + size_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.main_network_page.setSizePolicy(size_policy) + + main_layout = QtWidgets.QVBoxLayout(self.main_network_page) + + header_layout = QtWidgets.QHBoxLayout() + + header_layout.addItem( + QtWidgets.QSpacerItem( + 60, + 60, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + + self.network_main_title = QtWidgets.QLabel(parent=self.main_network_page) + title_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum + ) + self.network_main_title.setSizePolicy(title_policy) + self.network_main_title.setMinimumSize(QtCore.QSize(300, 0)) + self.network_main_title.setMaximumSize(QtCore.QSize(16777215, 60)) + font = QtGui.QFont() + font.setPointSize(20) + self.network_main_title.setFont(font) + self.network_main_title.setStyleSheet("color:white") + self.network_main_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.network_main_title.setText("Networks") + + header_layout.addWidget(self.network_main_title) + + self.network_backButton = IconButton(parent=self.main_network_page) + self.network_backButton.setMinimumSize(QtCore.QSize(60, 60)) + self.network_backButton.setMaximumSize(QtCore.QSize(60, 60)) + self.network_backButton.setFlat(True) + self.network_backButton.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") + ) + + header_layout.addWidget(self.network_backButton) + + main_layout.addLayout(header_layout) + + content_layout = QtWidgets.QHBoxLayout() + + self.mn_information_layout = BlocksCustomFrame(parent=self.main_network_page) + info_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.mn_information_layout.setSizePolicy(info_policy) + self.mn_information_layout.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.mn_information_layout.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + + info_layout = QtWidgets.QVBoxLayout(self.mn_information_layout) + + self.netlist_ssuid = QtWidgets.QLabel(parent=self.mn_information_layout) + ssid_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum + ) + self.netlist_ssuid.setSizePolicy(ssid_policy) + font = QtGui.QFont() + font.setPointSize(17) + self.netlist_ssuid.setFont(font) + self.netlist_ssuid.setStyleSheet("color: rgb(255, 255, 255);") + self.netlist_ssuid.setText("") + self.netlist_ssuid.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + info_layout.addWidget(self.netlist_ssuid) + + self.mn_info_seperator = QtWidgets.QFrame(parent=self.mn_information_layout) + self.mn_info_seperator.setFrameShape(QtWidgets.QFrame.Shape.HLine) + self.mn_info_seperator.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + + info_layout.addWidget(self.mn_info_seperator) + + self.netlist_ip = QtWidgets.QLabel(parent=self.mn_information_layout) + font = QtGui.QFont() + font.setPointSize(15) + self.netlist_ip.setFont(font) + self.netlist_ip.setStyleSheet("color: rgb(255, 255, 255);") + self.netlist_ip.setText("") + self.netlist_ip.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + info_layout.addWidget(self.netlist_ip) + + self.netlist_vlans_combo = QtWidgets.QComboBox( + parent=self.mn_information_layout + ) + font = QtGui.QFont() + font.setPointSize(11) + self.netlist_vlans_combo.setFont(font) + self.netlist_vlans_combo.setMinimumSize(QtCore.QSize(240, 50)) + self.netlist_vlans_combo.setMaximumSize(QtCore.QSize(250, 50)) + self.netlist_vlans_combo.setStyleSheet(""" + QComboBox { + background-color: rgba(26, 143, 191, 0.05); + color: rgba(255, 255, 255, 200); + border: 1px solid rgba(255, 255, 255, 80); + border-radius: 8px; + } + QComboBox QAbstractItemView { + background-color: rgb(40, 40, 40); + color: white; + selection-background-color: rgba(26, 143, 191, 0.6); + } + """) + + self.netlist_vlans_combo.setVisible(False) + self.netlist_vlans_combo.currentIndexChanged.connect( + self._on_interface_combo_changed + ) + + info_layout.addWidget( + self.netlist_vlans_combo, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) + + conn_info_layout = QtWidgets.QHBoxLayout() + + sg_info_layout = QtWidgets.QVBoxLayout() + + self.netlist_strength_label = QtWidgets.QLabel( + parent=self.mn_information_layout + ) + self.netlist_strength_label.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + self.netlist_strength_label.setFont(font) + self.netlist_strength_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_strength_label.setText("Signal\nStrength") + + sg_info_layout.addWidget(self.netlist_strength_label) + + self.line_2 = QtWidgets.QFrame(parent=self.mn_information_layout) + self.line_2.setFrameShape(QtWidgets.QFrame.Shape.HLine) + self.line_2.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + + sg_info_layout.addWidget(self.line_2) + + self.netlist_strength = QtWidgets.QLabel(parent=self.mn_information_layout) + font = QtGui.QFont() + font.setPointSize(11) + self.netlist_strength.setFont(font) + self.netlist_strength.setStyleSheet("color: rgb(255, 255, 255);") + self.netlist_strength.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_strength.setText("") + + sg_info_layout.addWidget(self.netlist_strength) + + conn_info_layout.addLayout(sg_info_layout) + + sec_info_layout = QtWidgets.QVBoxLayout() + + self.netlist_security_label = QtWidgets.QLabel( parent=self.mn_information_layout ) self.netlist_security_label.setPalette(self._create_white_palette()) @@ -569,13 +2024,13 @@ def _setup_main_network_page(self) -> None: self.netlist_security_label.setFont(font) self.netlist_security_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.netlist_security_label.setText("Security\nType") - self.netlist_security_label.setObjectName("netlist_security_label") + sec_info_layout.addWidget(self.netlist_security_label) self.line_3 = QtWidgets.QFrame(parent=self.mn_information_layout) self.line_3.setFrameShape(QtWidgets.QFrame.Shape.HLine) self.line_3.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line_3.setObjectName("line_3") + sec_info_layout.addWidget(self.line_3) self.netlist_security = QtWidgets.QLabel(parent=self.mn_information_layout) @@ -585,13 +2040,12 @@ def _setup_main_network_page(self) -> None: self.netlist_security.setStyleSheet("color: rgb(255, 255, 255);") self.netlist_security.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.netlist_security.setText("") - self.netlist_security.setObjectName("netlist_security") + sec_info_layout.addWidget(self.netlist_security) conn_info_layout.addLayout(sec_info_layout) info_layout.addLayout(conn_info_layout) - # Info box self.mn_info_box = QtWidgets.QLabel(parent=self.mn_information_layout) self.mn_info_box.setEnabled(False) font = QtGui.QFont() @@ -599,17 +2053,17 @@ def _setup_main_network_page(self) -> None: self.mn_info_box.setFont(font) self.mn_info_box.setStyleSheet("color: white") self.mn_info_box.setTextFormat(QtCore.Qt.TextFormat.PlainText) - self.mn_info_box.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.mn_info_box.setText( - "No network connection.\n\n" - "Try connecting to Wi-Fi \n" - "or turn on the hotspot\n" - "using the buttons on the side." + "There no active\ninternet connection.\nConnect via Ethernet, Wi-Fi,\nor enable a mobile hotspot\n for online features.\nPrinting functions will\nstill work offline." + ) + + self.mn_info_box.setSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Expanding, ) - self.mn_info_box.setObjectName("mn_info_box") + self.mn_info_box.setWordWrap(True) info_layout.addWidget(self.mn_info_box) - # Loading widget self.loadingwidget = LoadingOverlayWidget(parent=self.mn_information_layout) self.loadingwidget.setEnabled(True) loading_policy = QtWidgets.QSizePolicy( @@ -617,39 +2071,42 @@ def _setup_main_network_page(self) -> None: ) self.loadingwidget.setSizePolicy(loading_policy) self.loadingwidget.setText("") - self.loadingwidget.setObjectName("loadingwidget") + info_layout.addWidget(self.loadingwidget) content_layout.addWidget(self.mn_information_layout) - # Option buttons layout option_layout = QtWidgets.QVBoxLayout() - option_layout.setObjectName("mn_option_button_layout") - self.wifi_button = NetworkWidgetbuttons(parent=self.main_network_page) - wifi_policy = QtWidgets.QSizePolicy( + panel_policy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding, ) - self.wifi_button.setSizePolicy(wifi_policy) - self.wifi_button.setMaximumSize(QtCore.QSize(400, 9999)) font = QtGui.QFont() font.setPointSize(20) + + self.wifi_button = NetworkWidgetbuttons(parent=self.main_network_page) + self.wifi_button.setSizePolicy(panel_policy) + self.wifi_button.setMaximumSize(QtCore.QSize(400, 9999)) self.wifi_button.setFont(font) self.wifi_button.setText("Wi-Fi") - self.wifi_button.setObjectName("wifi_button") option_layout.addWidget(self.wifi_button) self.hotspot_button = NetworkWidgetbuttons(parent=self.main_network_page) - self.hotspot_button.setSizePolicy(wifi_policy) + self.hotspot_button.setSizePolicy(panel_policy) self.hotspot_button.setMaximumSize(QtCore.QSize(400, 9999)) - font = QtGui.QFont() - font.setPointSize(20) self.hotspot_button.setFont(font) self.hotspot_button.setText("Hotspot") - self.hotspot_button.setObjectName("hotspot_button") option_layout.addWidget(self.hotspot_button) + self.ethernet_button = NetworkWidgetbuttons(parent=self.main_network_page) + self.ethernet_button.setSizePolicy(panel_policy) + self.ethernet_button.setMaximumSize(QtCore.QSize(400, 9999)) + self.ethernet_button.setFont(font) + self.ethernet_button.setText("Ethernet") + self.ethernet_button.setVisible(False) + option_layout.addWidget(self.ethernet_button) + content_layout.addLayout(option_layout) main_layout.addLayout(content_layout) @@ -658,14 +2115,10 @@ def _setup_main_network_page(self) -> None: def _setup_network_list_page(self) -> None: """Setup the network list page.""" self.network_list_page = QtWidgets.QWidget() - self.network_list_page.setObjectName("network_list_page") main_layout = QtWidgets.QVBoxLayout(self.network_list_page) - main_layout.setObjectName("verticalLayout_9") - # Header layout header_layout = QtWidgets.QHBoxLayout() - header_layout.setObjectName("nl_header_layout") self.rescan_button = IconButton(parent=self.network_list_page) self.rescan_button.setMinimumSize(QtCore.QSize(60, 60)) @@ -673,21 +2126,21 @@ def _setup_network_list_page(self) -> None: self.rescan_button.setText("Reload") self.rescan_button.setFlat(True) self.rescan_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/refresh.svg") + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/refresh.svg") ) self.rescan_button.setProperty("button_type", "icon") - self.rescan_button.setObjectName("rescan_button") + header_layout.addWidget(self.rescan_button) self.network_list_title = QtWidgets.QLabel(parent=self.network_list_page) self.network_list_title.setMaximumSize(QtCore.QSize(16777215, 60)) self.network_list_title.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(20) - self.network_list_title.setFont(font) + title_font = QtGui.QFont() + title_font.setPointSize(20) + self.network_list_title.setFont(title_font) self.network_list_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.network_list_title.setText("Wi-Fi List") - self.network_list_title.setObjectName("network_list_title") + header_layout.addWidget(self.network_list_title) self.nl_back_button = IconButton(parent=self.network_list_page) @@ -696,18 +2149,16 @@ def _setup_network_list_page(self) -> None: self.nl_back_button.setText("Back") self.nl_back_button.setFlat(True) self.nl_back_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") ) self.nl_back_button.setProperty("class", "back_btn") self.nl_back_button.setProperty("button_type", "icon") - self.nl_back_button.setObjectName("nl_back_button") + header_layout.addWidget(self.nl_back_button) main_layout.addLayout(header_layout) - # List view layout list_layout = QtWidgets.QHBoxLayout() - list_layout.setObjectName("horizontalLayout_2") self.listView = QtWidgets.QListView(self.network_list_page) list_policy = QtWidgets.QSizePolicy( @@ -739,7 +2190,6 @@ def _setup_network_list_page(self) -> None: self.listView.setUniformItemSizes(True) self.listView.setSpacing(5) - # Setup touch scrolling QtWidgets.QScroller.grabGesture( self.listView, QtWidgets.QScroller.ScrollerGestureType.TouchGesture, @@ -769,7 +2219,7 @@ def _setup_network_list_page(self) -> None: ) self.verticalScrollBar.setSizePolicy(scrollbar_policy) self.verticalScrollBar.setOrientation(QtCore.Qt.Orientation.Vertical) - self.verticalScrollBar.setObjectName("verticalScrollBar") + self.verticalScrollBar.setAttribute( QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents, True ) @@ -791,14 +2241,10 @@ def _setup_network_list_page(self) -> None: def _setup_add_network_page(self) -> None: """Setup the add network page.""" self.add_network_page = QtWidgets.QWidget() - self.add_network_page.setObjectName("add_network_page") main_layout = QtWidgets.QVBoxLayout(self.add_network_page) - main_layout.setObjectName("verticalLayout_10") - # Header layout header_layout = QtWidgets.QHBoxLayout() - header_layout.setObjectName("add_np_header_layout") header_layout.addItem( QtWidgets.QSpacerItem( @@ -823,7 +2269,7 @@ def _setup_add_network_page(self) -> None: self.add_network_network_label.setStyleSheet("color:white") self.add_network_network_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.add_network_network_label.setText("TextLabel") - self.add_network_network_label.setObjectName("add_network_network_label") + header_layout.addWidget(self.add_network_network_label) self.add_network_page_backButton = IconButton(parent=self.add_network_page) @@ -832,21 +2278,19 @@ def _setup_add_network_page(self) -> None: self.add_network_page_backButton.setText("Back") self.add_network_page_backButton.setFlat(True) self.add_network_page_backButton.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") ) self.add_network_page_backButton.setProperty("class", "back_btn") self.add_network_page_backButton.setProperty("button_type", "icon") - self.add_network_page_backButton.setObjectName("add_network_page_backButton") + header_layout.addWidget(self.add_network_page_backButton) main_layout.addLayout(header_layout) - # Content layout content_layout = QtWidgets.QVBoxLayout() content_layout.setSizeConstraint( QtWidgets.QLayout.SizeConstraint.SetMinimumSize ) - content_layout.setObjectName("add_np_content_layout") content_layout.addItem( QtWidgets.QSpacerItem( @@ -857,7 +2301,6 @@ def _setup_add_network_page(self) -> None: ) ) - # Password frame self.frame_2 = BlocksCustomFrame(parent=self.add_network_page) frame_policy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum @@ -868,18 +2311,15 @@ def _setup_add_network_page(self) -> None: self.frame_2.setMaximumSize(QtCore.QSize(16777215, 90)) self.frame_2.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) self.frame_2.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_2.setObjectName("frame_2") frame_layout_widget = QtWidgets.QWidget(parent=self.frame_2) frame_layout_widget.setGeometry(QtCore.QRect(10, 10, 761, 82)) - frame_layout_widget.setObjectName("layoutWidget_2") password_layout = QtWidgets.QHBoxLayout(frame_layout_widget) password_layout.setSizeConstraint( QtWidgets.QLayout.SizeConstraint.SetMaximumSize ) password_layout.setContentsMargins(0, 0, 0, 0) - password_layout.setObjectName("horizontalLayout_5") self.add_network_password_label = QtWidgets.QLabel(parent=frame_layout_widget) self.add_network_password_label.setPalette(self._create_white_palette()) @@ -890,7 +2330,7 @@ def _setup_add_network_page(self) -> None: QtCore.Qt.AlignmentFlag.AlignCenter ) self.add_network_password_label.setText("Password") - self.add_network_password_label.setObjectName("add_network_password_label") + password_layout.addWidget(self.add_network_password_label) self.add_network_password_field = BlocksCustomLinEdit( @@ -901,7 +2341,7 @@ def _setup_add_network_page(self) -> None: font = QtGui.QFont() font.setPointSize(12) self.add_network_password_field.setFont(font) - self.add_network_password_field.setObjectName("add_network_password_field") + password_layout.addWidget(self.add_network_password_field) self.add_network_password_view = IconButton(parent=frame_layout_widget) @@ -910,11 +2350,11 @@ def _setup_add_network_page(self) -> None: self.add_network_password_view.setText("View") self.add_network_password_view.setFlat(True) self.add_network_password_view.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/unsee.svg") ) self.add_network_password_view.setProperty("class", "back_btn") self.add_network_password_view.setProperty("button_type", "icon") - self.add_network_password_view.setObjectName("add_network_password_view") + password_layout.addWidget(self.add_network_password_view) content_layout.addWidget(self.frame_2) @@ -928,263 +2368,55 @@ def _setup_add_network_page(self) -> None: ) ) - # Validation button layout button_layout = QtWidgets.QHBoxLayout() button_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize) - button_layout.setObjectName("horizontalLayout_6") self.add_network_validation_button = BlocksCustomButton( parent=self.add_network_page ) - self.add_network_validation_button.setEnabled(True) - btn_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.MinimumExpanding, - QtWidgets.QSizePolicy.Policy.MinimumExpanding, - ) - btn_policy.setHorizontalStretch(1) - btn_policy.setVerticalStretch(1) - self.add_network_validation_button.setSizePolicy(btn_policy) - self.add_network_validation_button.setMinimumSize(QtCore.QSize(250, 80)) - self.add_network_validation_button.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(15) - self.add_network_validation_button.setFont(font) - self.add_network_validation_button.setIconSize(QtCore.QSize(16, 16)) - self.add_network_validation_button.setCheckable(False) - self.add_network_validation_button.setChecked(False) - self.add_network_validation_button.setFlat(True) - self.add_network_validation_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") - ) - self.add_network_validation_button.setText("Activate") - self.add_network_validation_button.setObjectName( - "add_network_validation_button" - ) - button_layout.addWidget( - self.add_network_validation_button, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignTop, - ) - - content_layout.addLayout(button_layout) - main_layout.addLayout(content_layout) - - self.addWidget(self.add_network_page) - - def _setup_hidden_network_page(self) -> None: - """Setup the hidden network page for connecting to networks with hidden SSID.""" - self.hidden_network_page = QtWidgets.QWidget() - self.hidden_network_page.setObjectName("hidden_network_page") - - main_layout = QtWidgets.QVBoxLayout(self.hidden_network_page) - main_layout.setObjectName("hidden_network_layout") - - # Header layout - header_layout = QtWidgets.QHBoxLayout() - header_layout.addItem( - QtWidgets.QSpacerItem( - 40, - 60, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) - - self.hidden_network_title = QtWidgets.QLabel(parent=self.hidden_network_page) - self.hidden_network_title.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(20) - self.hidden_network_title.setFont(font) - self.hidden_network_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.hidden_network_title.setText("Hidden Network") - header_layout.addWidget(self.hidden_network_title) - - self.hidden_network_back_button = IconButton(parent=self.hidden_network_page) - self.hidden_network_back_button.setMinimumSize(QtCore.QSize(60, 60)) - self.hidden_network_back_button.setMaximumSize(QtCore.QSize(60, 60)) - self.hidden_network_back_button.setFlat(True) - self.hidden_network_back_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") - ) - self.hidden_network_back_button.setProperty("button_type", "icon") - header_layout.addWidget(self.hidden_network_back_button) - - main_layout.addLayout(header_layout) - - # Content - content_layout = QtWidgets.QVBoxLayout() - content_layout.addItem( - QtWidgets.QSpacerItem( - 20, - 30, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) - - # SSID Frame - ssid_frame = BlocksCustomFrame(parent=self.hidden_network_page) - ssid_frame.setMinimumSize(QtCore.QSize(0, 80)) - ssid_frame.setMaximumSize(QtCore.QSize(16777215, 90)) - ssid_frame_layout = QtWidgets.QHBoxLayout(ssid_frame) - - ssid_label = QtWidgets.QLabel("Network\nName", parent=ssid_frame) - ssid_label.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(15) - ssid_label.setFont(font) - ssid_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - ssid_frame_layout.addWidget(ssid_label) - - self.hidden_network_ssid_field = BlocksCustomLinEdit(parent=ssid_frame) - self.hidden_network_ssid_field.setMinimumSize(QtCore.QSize(500, 60)) - font = QtGui.QFont() - font.setPointSize(12) - self.hidden_network_ssid_field.setFont(font) - self.hidden_network_ssid_field.setPlaceholderText("Enter network name") - ssid_frame_layout.addWidget(self.hidden_network_ssid_field) - - content_layout.addWidget(ssid_frame) - - content_layout.addItem( - QtWidgets.QSpacerItem( - 20, - 20, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) - - # Password Frame - password_frame = BlocksCustomFrame(parent=self.hidden_network_page) - password_frame.setMinimumSize(QtCore.QSize(0, 80)) - password_frame.setMaximumSize(QtCore.QSize(16777215, 90)) - password_frame_layout = QtWidgets.QHBoxLayout(password_frame) - - password_label = QtWidgets.QLabel("Password", parent=password_frame) - password_label.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(15) - password_label.setFont(font) - password_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - password_frame_layout.addWidget(password_label) - - self.hidden_network_password_field = BlocksCustomLinEdit(parent=password_frame) - self.hidden_network_password_field.setHidden(True) - self.hidden_network_password_field.setMinimumSize(QtCore.QSize(500, 60)) - font = QtGui.QFont() - font.setPointSize(12) - self.hidden_network_password_field.setFont(font) - self.hidden_network_password_field.setPlaceholderText( - "Enter password (leave empty for open networks)" - ) - self.hidden_network_password_field.setEchoMode( - QtWidgets.QLineEdit.EchoMode.Password - ) - password_frame_layout.addWidget(self.hidden_network_password_field) - - self.hidden_network_password_view = IconButton(parent=password_frame) - self.hidden_network_password_view.setMinimumSize(QtCore.QSize(60, 60)) - self.hidden_network_password_view.setMaximumSize(QtCore.QSize(60, 60)) - self.hidden_network_password_view.setFlat(True) - self.hidden_network_password_view.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") - ) - self.hidden_network_password_view.setProperty("button_type", "icon") - password_frame_layout.addWidget(self.hidden_network_password_view) - - content_layout.addWidget(password_frame) - - content_layout.addItem( - QtWidgets.QSpacerItem( - 20, - 50, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) - - # Connect button - self.hidden_network_connect_button = BlocksCustomButton( - parent=self.hidden_network_page - ) - self.hidden_network_connect_button.setMinimumSize(QtCore.QSize(250, 80)) - self.hidden_network_connect_button.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setPointSize(15) - self.hidden_network_connect_button.setFont(font) - self.hidden_network_connect_button.setFlat(True) - self.hidden_network_connect_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") - ) - self.hidden_network_connect_button.setText("Connect") - content_layout.addWidget( - self.hidden_network_connect_button, 0, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - - main_layout.addLayout(content_layout) - self.addWidget(self.hidden_network_page) - - # Connect signals - self.hidden_network_back_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) - ) - self.hidden_network_connect_button.clicked.connect( - self._on_hidden_network_connect + self.add_network_validation_button.setEnabled(True) + btn_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + QtWidgets.QSizePolicy.Policy.MinimumExpanding, ) - self.hidden_network_ssid_field.clicked.connect( - lambda: self._on_show_keyboard( - self.hidden_network_page, self.hidden_network_ssid_field - ) + btn_policy.setHorizontalStretch(1) + btn_policy.setVerticalStretch(1) + self.add_network_validation_button.setSizePolicy(btn_policy) + self.add_network_validation_button.setMinimumSize(QtCore.QSize(250, 80)) + self.add_network_validation_button.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setFamily("Momcake") + font.setPointSize(15) + self.add_network_validation_button.setFont(font) + self.add_network_validation_button.setIconSize(QtCore.QSize(16, 16)) + self.add_network_validation_button.setCheckable(False) + self.add_network_validation_button.setChecked(False) + self.add_network_validation_button.setFlat(True) + self.add_network_validation_button.setProperty( + "icon_pixmap", PixmapCache.get(":/dialog/media/btn_icons/yes.svg") ) - self.hidden_network_password_field.clicked.connect( - lambda: self._on_show_keyboard( - self.hidden_network_page, self.hidden_network_password_field - ) + self.add_network_validation_button.setText("Activate") + self.add_network_validation_button.setObjectName( + "add_network_validation_button" ) - self._setup_password_visibility_toggle( - self.hidden_network_password_view, self.hidden_network_password_field + button_layout.addWidget( + self.add_network_validation_button, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignTop, ) - def _on_hidden_network_connect(self) -> None: - """Handle connection to hidden network.""" - ssid = self.hidden_network_ssid_field.text().strip() - password = self.hidden_network_password_field.text() - - if not ssid: - self._show_error_popup("Please enter a network name.") - return - - self._current_network_is_hidden = True - self._current_network_is_open = not password - - result = self._sdbus_network.add_wifi_network(ssid=ssid, psk=password) - - if result is None: - self._handle_failed_network_add("Failed to add network") - return - - error_msg = result.get("error", "") if isinstance(result, dict) else "" + content_layout.addLayout(button_layout) + main_layout.addLayout(content_layout) - if not error_msg: - self.hidden_network_ssid_field.clear() - self.hidden_network_password_field.clear() - self._handle_successful_network_add(ssid) - else: - self._handle_failed_network_add(error_msg) + self.addWidget(self.add_network_page) def _setup_saved_connection_page(self) -> None: """Setup the saved connection page.""" self.saved_connection_page = QtWidgets.QWidget() - self.saved_connection_page.setObjectName("saved_connection_page") main_layout = QtWidgets.QVBoxLayout(self.saved_connection_page) - main_layout.setObjectName("verticalLayout_11") - # Header layout header_layout = QtWidgets.QHBoxLayout() - header_layout.setObjectName("horizontalLayout_7") header_layout.addItem( QtWidgets.QSpacerItem( @@ -1222,23 +2454,20 @@ def _setup_saved_connection_page(self) -> None: ) self.saved_connection_back_button.setMinimumSize(QtCore.QSize(60, 60)) self.saved_connection_back_button.setMaximumSize(QtCore.QSize(60, 60)) - self.saved_connection_back_button.setText("Back") self.saved_connection_back_button.setFlat(True) self.saved_connection_back_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") ) self.saved_connection_back_button.setProperty("class", "back_btn") self.saved_connection_back_button.setProperty("button_type", "icon") - self.saved_connection_back_button.setObjectName("saved_connection_back_button") + header_layout.addWidget( self.saved_connection_back_button, 0, QtCore.Qt.AlignmentFlag.AlignRight ) main_layout.addLayout(header_layout) - # Content layout content_layout = QtWidgets.QVBoxLayout() - content_layout.setObjectName("verticalLayout_5") content_layout.addItem( QtWidgets.QSpacerItem( @@ -1249,13 +2478,9 @@ def _setup_saved_connection_page(self) -> None: ) ) - # Main content horizontal layout main_content_layout = QtWidgets.QHBoxLayout() - main_content_layout.setObjectName("horizontalLayout_9") - # Info frame layout info_layout = QtWidgets.QVBoxLayout() - info_layout.setObjectName("verticalLayout_2") self.frame = BlocksCustomFrame(parent=self.saved_connection_page) frame_policy = QtWidgets.QSizePolicy( @@ -1266,14 +2491,10 @@ def _setup_saved_connection_page(self) -> None: self.frame.setMaximumSize(QtCore.QSize(400, 16777215)) self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame.setObjectName("frame") frame_inner_layout = QtWidgets.QVBoxLayout(self.frame) - frame_inner_layout.setObjectName("verticalLayout_6") - # Signal strength row signal_layout = QtWidgets.QHBoxLayout() - signal_layout.setObjectName("horizontalLayout") self.netlist_strength_label_2 = QtWidgets.QLabel(parent=self.frame) self.netlist_strength_label_2.setPalette(self._create_white_palette()) @@ -1282,7 +2503,7 @@ def _setup_saved_connection_page(self) -> None: self.netlist_strength_label_2.setFont(font) self.netlist_strength_label_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.netlist_strength_label_2.setText("Signal\nStrength") - self.netlist_strength_label_2.setObjectName("netlist_strength_label_2") + signal_layout.addWidget(self.netlist_strength_label_2) self.saved_connection_signal_strength_info_frame = QtWidgets.QLabel( @@ -1300,10 +2521,6 @@ def _setup_saved_connection_page(self) -> None: self.saved_connection_signal_strength_info_frame.setAlignment( QtCore.Qt.AlignmentFlag.AlignCenter ) - self.saved_connection_signal_strength_info_frame.setText("TextLabel") - self.saved_connection_signal_strength_info_frame.setObjectName( - "saved_connection_signal_strength_info_frame" - ) signal_layout.addWidget(self.saved_connection_signal_strength_info_frame) frame_inner_layout.addLayout(signal_layout) @@ -1311,12 +2528,10 @@ def _setup_saved_connection_page(self) -> None: self.line_4 = QtWidgets.QFrame(parent=self.frame) self.line_4.setFrameShape(QtWidgets.QFrame.Shape.HLine) self.line_4.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line_4.setObjectName("line_4") + frame_inner_layout.addWidget(self.line_4) - # Security type row security_layout = QtWidgets.QHBoxLayout() - security_layout.setObjectName("horizontalLayout_2") self.netlist_security_label_2 = QtWidgets.QLabel(parent=self.frame) self.netlist_security_label_2.setPalette(self._create_white_palette()) @@ -1325,1862 +2540,1258 @@ def _setup_saved_connection_page(self) -> None: self.netlist_security_label_2.setFont(font) self.netlist_security_label_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.netlist_security_label_2.setText("Security\nType") - self.netlist_security_label_2.setObjectName("netlist_security_label_2") - security_layout.addWidget(self.netlist_security_label_2) - - self.saved_connection_security_type_info_label = QtWidgets.QLabel( - parent=self.frame - ) - self.saved_connection_security_type_info_label.setMinimumSize( - QtCore.QSize(250, 0) - ) - font = QtGui.QFont() - font.setPointSize(11) - self.saved_connection_security_type_info_label.setFont(font) - self.saved_connection_security_type_info_label.setStyleSheet( - "color: rgb(255, 255, 255);" - ) - self.saved_connection_security_type_info_label.setAlignment( - QtCore.Qt.AlignmentFlag.AlignCenter - ) - self.saved_connection_security_type_info_label.setText("TextLabel") - self.saved_connection_security_type_info_label.setObjectName( - "saved_connection_security_type_info_label" - ) - security_layout.addWidget(self.saved_connection_security_type_info_label) - - frame_inner_layout.addLayout(security_layout) - - self.line_5 = QtWidgets.QFrame(parent=self.frame) - self.line_5.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.line_5.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line_5.setObjectName("line_5") - frame_inner_layout.addWidget(self.line_5) - - # Status row - status_layout = QtWidgets.QHBoxLayout() - status_layout.setObjectName("horizontalLayout_8") - - self.netlist_security_label_4 = QtWidgets.QLabel(parent=self.frame) - self.netlist_security_label_4.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(15) - self.netlist_security_label_4.setFont(font) - self.netlist_security_label_4.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_security_label_4.setText("Status") - self.netlist_security_label_4.setObjectName("netlist_security_label_4") - status_layout.addWidget(self.netlist_security_label_4) - - self.sn_info = QtWidgets.QLabel(parent=self.frame) - self.sn_info.setMinimumSize(QtCore.QSize(250, 0)) - font = QtGui.QFont() - font.setPointSize(11) - self.sn_info.setFont(font) - self.sn_info.setStyleSheet("color: rgb(255, 255, 255);") - self.sn_info.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.sn_info.setText("TextLabel") - self.sn_info.setObjectName("sn_info") - status_layout.addWidget(self.sn_info) - - frame_inner_layout.addLayout(status_layout) - info_layout.addWidget(self.frame) - main_content_layout.addLayout(info_layout) - - # Action buttons frame - self.frame_8 = BlocksCustomFrame(parent=self.saved_connection_page) - self.frame_8.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_8.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_8.setObjectName("frame_8") - - buttons_layout = QtWidgets.QVBoxLayout(self.frame_8) - buttons_layout.setObjectName("verticalLayout_4") - - self.network_activate_btn = BlocksCustomButton(parent=self.frame_8) - self.network_activate_btn.setMinimumSize(QtCore.QSize(250, 80)) - self.network_activate_btn.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setPointSize(15) - self.network_activate_btn.setFont(font) - self.network_activate_btn.setFlat(True) - self.network_activate_btn.setText("Connect") - self.network_activate_btn.setObjectName("network_activate_btn") - buttons_layout.addWidget( - self.network_activate_btn, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - self.network_details_btn = BlocksCustomButton(parent=self.frame_8) - self.network_details_btn.setMinimumSize(QtCore.QSize(250, 80)) - self.network_details_btn.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setPointSize(15) - self.network_details_btn.setFont(font) - self.network_details_btn.setFlat(True) - self.network_details_btn.setText("Details") - self.network_details_btn.setObjectName("network_details_btn") - buttons_layout.addWidget( - self.network_details_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - - self.network_delete_btn = BlocksCustomButton(parent=self.frame_8) - self.network_delete_btn.setMinimumSize(QtCore.QSize(250, 80)) - self.network_delete_btn.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setPointSize(15) - self.network_delete_btn.setFont(font) - self.network_delete_btn.setFlat(True) - self.network_delete_btn.setText("Forget") - self.network_delete_btn.setObjectName("network_delete_btn") - buttons_layout.addWidget( - self.network_delete_btn, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - main_content_layout.addWidget(self.frame_8) - content_layout.addLayout(main_content_layout) - main_layout.addLayout(content_layout) - - self.addWidget(self.saved_connection_page) - - def _setup_saved_details_page(self) -> None: - """Setup the saved network details page.""" - self.saved_details_page = QtWidgets.QWidget() - self.saved_details_page.setObjectName("saved_details_page") - - main_layout = QtWidgets.QVBoxLayout(self.saved_details_page) - main_layout.setObjectName("verticalLayout_19") - - # Header layout - header_layout = QtWidgets.QHBoxLayout() - header_layout.setObjectName("horizontalLayout_14") - - header_layout.addItem( - QtWidgets.QSpacerItem( - 60, - 60, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) - - self.snd_name = QtWidgets.QLabel(parent=self.saved_details_page) - name_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.snd_name.setSizePolicy(name_policy) - self.snd_name.setMaximumSize(QtCore.QSize(16777215, 60)) - self.snd_name.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(20) - self.snd_name.setFont(font) - self.snd_name.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.snd_name.setText("SSID") - self.snd_name.setObjectName("snd_name") - header_layout.addWidget(self.snd_name) - - self.snd_back = IconButton(parent=self.saved_details_page) - self.snd_back.setMinimumSize(QtCore.QSize(60, 60)) - self.snd_back.setMaximumSize(QtCore.QSize(60, 60)) - self.snd_back.setText("Back") - self.snd_back.setFlat(True) - self.snd_back.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") - ) - self.snd_back.setProperty("class", "back_btn") - self.snd_back.setProperty("button_type", "icon") - self.snd_back.setObjectName("snd_back") - header_layout.addWidget(self.snd_back) - - main_layout.addLayout(header_layout) - - # Content layout - content_layout = QtWidgets.QVBoxLayout() - content_layout.setObjectName("verticalLayout_8") - - content_layout.addItem( - QtWidgets.QSpacerItem( - 20, - 20, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) - - # Password change frame - self.frame_9 = BlocksCustomFrame(parent=self.saved_details_page) - frame_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum - ) - self.frame_9.setSizePolicy(frame_policy) - self.frame_9.setMinimumSize(QtCore.QSize(0, 70)) - self.frame_9.setMaximumSize(QtCore.QSize(16777215, 70)) - self.frame_9.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_9.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_9.setObjectName("frame_9") - - frame_layout_widget = QtWidgets.QWidget(parent=self.frame_9) - frame_layout_widget.setGeometry(QtCore.QRect(0, 0, 776, 62)) - frame_layout_widget.setObjectName("layoutWidget_8") - - password_layout = QtWidgets.QHBoxLayout(frame_layout_widget) - password_layout.setContentsMargins(0, 0, 0, 0) - password_layout.setObjectName("horizontalLayout_10") - - self.saved_connection_change_password_label_3 = QtWidgets.QLabel( - parent=frame_layout_widget - ) - self.saved_connection_change_password_label_3.setPalette( - self._create_white_palette() - ) - font = QtGui.QFont() - font.setPointSize(15) - self.saved_connection_change_password_label_3.setFont(font) - self.saved_connection_change_password_label_3.setAlignment( - QtCore.Qt.AlignmentFlag.AlignCenter - ) - self.saved_connection_change_password_label_3.setText("Change\nPassword") - self.saved_connection_change_password_label_3.setObjectName( - "saved_connection_change_password_label_3" - ) - password_layout.addWidget( - self.saved_connection_change_password_label_3, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - self.saved_connection_change_password_field = BlocksCustomLinEdit( - parent=frame_layout_widget - ) - self.saved_connection_change_password_field.setHidden(True) - self.saved_connection_change_password_field.setMinimumSize( - QtCore.QSize(500, 60) + security_layout.addWidget(self.netlist_security_label_2) + + self.saved_connection_security_type_info_label = QtWidgets.QLabel( + parent=self.frame ) - self.saved_connection_change_password_field.setMaximumSize( - QtCore.QSize(500, 16777215) + self.saved_connection_security_type_info_label.setMinimumSize( + QtCore.QSize(250, 0) ) font = QtGui.QFont() - font.setPointSize(12) - self.saved_connection_change_password_field.setFont(font) - self.saved_connection_change_password_field.setObjectName( - "saved_connection_change_password_field" + font.setPointSize(11) + self.saved_connection_security_type_info_label.setFont(font) + self.saved_connection_security_type_info_label.setStyleSheet( + "color: rgb(255, 255, 255);" ) - password_layout.addWidget( - self.saved_connection_change_password_field, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter, + self.saved_connection_security_type_info_label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignCenter ) + security_layout.addWidget(self.saved_connection_security_type_info_label) - self.saved_connection_change_password_view = IconButton( - parent=frame_layout_widget - ) - self.saved_connection_change_password_view.setMinimumSize(QtCore.QSize(60, 60)) - self.saved_connection_change_password_view.setMaximumSize(QtCore.QSize(60, 60)) - self.saved_connection_change_password_view.setText("View") - self.saved_connection_change_password_view.setFlat(True) - self.saved_connection_change_password_view.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") - ) - self.saved_connection_change_password_view.setProperty("class", "back_btn") - self.saved_connection_change_password_view.setProperty("button_type", "icon") - self.saved_connection_change_password_view.setObjectName( - "saved_connection_change_password_view" - ) - password_layout.addWidget( - self.saved_connection_change_password_view, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) + frame_inner_layout.addLayout(security_layout) - content_layout.addWidget(self.frame_9) + self.line_5 = QtWidgets.QFrame(parent=self.frame) + self.line_5.setFrameShape(QtWidgets.QFrame.Shape.HLine) + self.line_5.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - # Priority buttons layout - priority_outer_layout = QtWidgets.QHBoxLayout() - priority_outer_layout.setObjectName("horizontalLayout_13") + frame_inner_layout.addWidget(self.line_5) - priority_inner_layout = QtWidgets.QVBoxLayout() - priority_inner_layout.setObjectName("verticalLayout_13") + status_layout = QtWidgets.QHBoxLayout() - self.frame_12 = BlocksCustomFrame(parent=self.saved_details_page) - frame_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.frame_12.setSizePolicy(frame_policy) - self.frame_12.setMinimumSize(QtCore.QSize(400, 160)) - self.frame_12.setMaximumSize(QtCore.QSize(400, 99999)) - self.frame_12.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_12.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_12.setProperty("text", "Network priority") - self.frame_12.setObjectName("frame_12") + self.netlist_security_label_4 = QtWidgets.QLabel(parent=self.frame) + self.netlist_security_label_4.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + self.netlist_security_label_4.setFont(font) + self.netlist_security_label_4.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_security_label_4.setText("Status") - frame_inner_layout = QtWidgets.QVBoxLayout(self.frame_12) - frame_inner_layout.setObjectName("verticalLayout_17") + status_layout.addWidget(self.netlist_security_label_4) - frame_inner_layout.addItem( - QtWidgets.QSpacerItem( - 10, - 10, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) + self.sn_info = QtWidgets.QLabel(parent=self.frame) + self.sn_info.setMinimumSize(QtCore.QSize(250, 0)) + font = QtGui.QFont() + font.setPointSize(11) + self.sn_info.setFont(font) + self.sn_info.setStyleSheet("color: rgb(255, 255, 255);") + self.sn_info.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.sn_info.setText("TextLabel") - # Priority buttons - buttons_layout = QtWidgets.QHBoxLayout() - buttons_layout.setObjectName("horizontalLayout_4") + status_layout.addWidget(self.sn_info) - self.priority_btn_group = QtWidgets.QButtonGroup(self) - self.priority_btn_group.setObjectName("priority_btn_group") + frame_inner_layout.addLayout(status_layout) + info_layout.addWidget(self.frame) + main_content_layout.addLayout(info_layout) - self.low_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) - self.low_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) - self.low_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) - self.low_priority_btn.setCheckable(True) - self.low_priority_btn.setAutoExclusive(True) - self.low_priority_btn.setFlat(True) - self.low_priority_btn.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg") - ) - self.low_priority_btn.setText("Low") - self.low_priority_btn.setProperty("class", "back_btn") - self.low_priority_btn.setProperty("button_type", "icon") - self.low_priority_btn.setObjectName("low_priority_btn") - self.priority_btn_group.addButton(self.low_priority_btn) - buttons_layout.addWidget(self.low_priority_btn) + self.frame_8 = BlocksCustomFrame(parent=self.saved_connection_page) + self.frame_8.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_8.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.med_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) - self.med_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) - self.med_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) - self.med_priority_btn.setCheckable(True) - self.med_priority_btn.setChecked(False) # Don't set default checked - self.med_priority_btn.setAutoExclusive(True) - self.med_priority_btn.setFlat(True) - self.med_priority_btn.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg") + buttons_layout = QtWidgets.QVBoxLayout(self.frame_8) + + self.network_activate_btn = BlocksCustomButton(parent=self.frame_8) + self.network_activate_btn.setMinimumSize(QtCore.QSize(250, 80)) + self.network_activate_btn.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(15) + self.network_activate_btn.setFont(font) + self.network_activate_btn.setFlat(True) + self.network_activate_btn.setText("Connect") + + buttons_layout.addWidget( + self.network_activate_btn, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) - self.med_priority_btn.setText("Medium") - self.med_priority_btn.setProperty("class", "back_btn") - self.med_priority_btn.setProperty("button_type", "icon") - self.med_priority_btn.setObjectName("med_priority_btn") - self.priority_btn_group.addButton(self.med_priority_btn) - buttons_layout.addWidget(self.med_priority_btn) - self.high_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) - self.high_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) - self.high_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) - self.high_priority_btn.setCheckable(True) - self.high_priority_btn.setChecked(False) - self.high_priority_btn.setAutoExclusive(True) - self.high_priority_btn.setFlat(True) - self.high_priority_btn.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg") + self.network_details_btn = BlocksCustomButton(parent=self.frame_8) + self.network_details_btn.setMinimumSize(QtCore.QSize(250, 80)) + self.network_details_btn.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(15) + self.network_details_btn.setFont(font) + self.network_details_btn.setFlat(True) + self.network_details_btn.setText("Details") + + buttons_layout.addWidget( + self.network_details_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter ) - self.high_priority_btn.setText("High") - self.high_priority_btn.setProperty("class", "back_btn") - self.high_priority_btn.setProperty("button_type", "icon") - self.high_priority_btn.setObjectName("high_priority_btn") - self.priority_btn_group.addButton(self.high_priority_btn) - buttons_layout.addWidget(self.high_priority_btn) - frame_inner_layout.addLayout(buttons_layout) + self.network_delete_btn = BlocksCustomButton(parent=self.frame_8) + self.network_delete_btn.setMinimumSize(QtCore.QSize(250, 80)) + self.network_delete_btn.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(15) + self.network_delete_btn.setFont(font) + self.network_delete_btn.setFlat(True) + self.network_delete_btn.setText("Forget") - priority_inner_layout.addWidget( - self.frame_12, + buttons_layout.addWidget( + self.network_delete_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) - priority_outer_layout.addLayout(priority_inner_layout) - content_layout.addLayout(priority_outer_layout) + main_content_layout.addWidget(self.frame_8) + content_layout.addLayout(main_content_layout) main_layout.addLayout(content_layout) - self.addWidget(self.saved_details_page) + self.addWidget(self.saved_connection_page) - def _setup_hotspot_page(self) -> None: - """Setup the hotspot configuration page.""" - self.hotspot_page = QtWidgets.QWidget() - self.hotspot_page.setObjectName("hotspot_page") + def _setup_saved_details_page(self) -> None: + """Setup the saved network details page.""" + self.saved_details_page = QtWidgets.QWidget() - main_layout = QtWidgets.QVBoxLayout(self.hotspot_page) - main_layout.setObjectName("verticalLayout_12") + main_layout = QtWidgets.QVBoxLayout(self.saved_details_page) - # Header layout header_layout = QtWidgets.QHBoxLayout() - header_layout.setObjectName("hospot_page_header_layout") header_layout.addItem( QtWidgets.QSpacerItem( - 40, - 20, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) - - self.hotspot_header_title = QtWidgets.QLabel(parent=self.hotspot_page) - self.hotspot_header_title.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(20) - self.hotspot_header_title.setFont(font) - self.hotspot_header_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.hotspot_header_title.setText("Hotspot") - self.hotspot_header_title.setObjectName("hotspot_header_title") - header_layout.addWidget(self.hotspot_header_title) - - self.hotspot_back_button = IconButton(parent=self.hotspot_page) - self.hotspot_back_button.setMinimumSize(QtCore.QSize(60, 60)) - self.hotspot_back_button.setMaximumSize(QtCore.QSize(60, 60)) - self.hotspot_back_button.setText("Back") - self.hotspot_back_button.setFlat(True) - self.hotspot_back_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") - ) - self.hotspot_back_button.setProperty("class", "back_btn") - self.hotspot_back_button.setProperty("button_type", "icon") - self.hotspot_back_button.setObjectName("hotspot_back_button") - header_layout.addWidget(self.hotspot_back_button) - - main_layout.addLayout(header_layout) - - # Content layout - content_layout = QtWidgets.QVBoxLayout() - content_layout.setContentsMargins(-1, 5, -1, 5) - content_layout.setObjectName("hotspot_page_content_layout") - - content_layout.addItem( - QtWidgets.QSpacerItem( - 20, - 50, + 60, + 60, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum, ) ) - # Hotspot name frame - self.frame_6 = BlocksCustomFrame(parent=self.hotspot_page) - frame_policy = QtWidgets.QSizePolicy( + self.snd_name = QtWidgets.QLabel(parent=self.saved_details_page) + name_policy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding, ) - self.frame_6.setSizePolicy(frame_policy) - self.frame_6.setMinimumSize(QtCore.QSize(70, 80)) - self.frame_6.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_6.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_6.setObjectName("frame_6") - - frame_layout_widget = QtWidgets.QWidget(parent=self.frame_6) - frame_layout_widget.setGeometry(QtCore.QRect(0, 10, 776, 61)) - frame_layout_widget.setObjectName("layoutWidget_6") + self.snd_name.setSizePolicy(name_policy) + self.snd_name.setMaximumSize(QtCore.QSize(16777215, 60)) + self.snd_name.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(20) + self.snd_name.setFont(font) + self.snd_name.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.snd_name.setText("SSID") - name_layout = QtWidgets.QHBoxLayout(frame_layout_widget) - name_layout.setContentsMargins(0, 0, 0, 0) - name_layout.setObjectName("horizontalLayout_11") + header_layout.addWidget(self.snd_name) - self.hotspot_info_name_label = QtWidgets.QLabel(parent=frame_layout_widget) - label_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Maximum - ) - self.hotspot_info_name_label.setSizePolicy(label_policy) - self.hotspot_info_name_label.setMaximumSize(QtCore.QSize(150, 16777215)) - self.hotspot_info_name_label.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(10) - self.hotspot_info_name_label.setFont(font) - self.hotspot_info_name_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.hotspot_info_name_label.setText("Hotspot Name: ") - self.hotspot_info_name_label.setObjectName("hotspot_info_name_label") - name_layout.addWidget(self.hotspot_info_name_label) - - self.hotspot_name_input_field = BlocksCustomLinEdit(parent=frame_layout_widget) - field_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.MinimumExpanding, - ) - self.hotspot_name_input_field.setSizePolicy(field_policy) - self.hotspot_name_input_field.setMinimumSize(QtCore.QSize(500, 40)) - self.hotspot_name_input_field.setMaximumSize(QtCore.QSize(500, 60)) - font = QtGui.QFont() - font.setPointSize(12) - self.hotspot_name_input_field.setFont(font) - # Name should be visible, not masked - self.hotspot_name_input_field.setEchoMode(QtWidgets.QLineEdit.EchoMode.Normal) - self.hotspot_name_input_field.setObjectName("hotspot_name_input_field") - name_layout.addWidget( - self.hotspot_name_input_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + self.snd_back = IconButton(parent=self.saved_details_page) + self.snd_back.setMinimumSize(QtCore.QSize(60, 60)) + self.snd_back.setMaximumSize(QtCore.QSize(60, 60)) + self.snd_back.setText("Back") + self.snd_back.setFlat(True) + self.snd_back.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") ) + self.snd_back.setProperty("class", "back_btn") + self.snd_back.setProperty("button_type", "icon") - name_layout.addItem( - QtWidgets.QSpacerItem( - 60, - 20, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) + header_layout.addWidget(self.snd_back) - content_layout.addWidget(self.frame_6) + main_layout.addLayout(header_layout) + + content_layout = QtWidgets.QVBoxLayout() content_layout.addItem( QtWidgets.QSpacerItem( - 773, - 128, + 20, + 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum, ) ) - # Hotspot password frame - self.frame_7 = BlocksCustomFrame(parent=self.hotspot_page) + self.frame_9 = BlocksCustomFrame(parent=self.saved_details_page) frame_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum + QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum ) - self.frame_7.setSizePolicy(frame_policy) - self.frame_7.setMinimumSize(QtCore.QSize(0, 80)) - self.frame_7.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_7.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_7.setObjectName("frame_7") + self.frame_9.setSizePolicy(frame_policy) + self.frame_9.setMinimumSize(QtCore.QSize(0, 70)) + self.frame_9.setMaximumSize(QtCore.QSize(16777215, 70)) + self.frame_9.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_9.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - password_layout_widget = QtWidgets.QWidget(parent=self.frame_7) - password_layout_widget.setGeometry(QtCore.QRect(0, 10, 776, 62)) - password_layout_widget.setObjectName("layoutWidget_7") + frame_layout_widget = QtWidgets.QWidget(parent=self.frame_9) + frame_layout_widget.setGeometry(QtCore.QRect(0, 0, 776, 62)) - password_layout = QtWidgets.QHBoxLayout(password_layout_widget) + password_layout = QtWidgets.QHBoxLayout(frame_layout_widget) password_layout.setContentsMargins(0, 0, 0, 0) - password_layout.setObjectName("horizontalLayout_12") - self.hotspot_info_password_label = QtWidgets.QLabel( - parent=password_layout_widget + self.saved_connection_change_password_label_3 = QtWidgets.QLabel( + parent=frame_layout_widget + ) + self.saved_connection_change_password_label_3.setPalette( + self._create_white_palette() ) - self.hotspot_info_password_label.setSizePolicy(label_policy) - self.hotspot_info_password_label.setMaximumSize(QtCore.QSize(150, 16777215)) - self.hotspot_info_password_label.setPalette(self._create_white_palette()) font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(10) - self.hotspot_info_password_label.setFont(font) - self.hotspot_info_password_label.setAlignment( + font.setPointSize(15) + self.saved_connection_change_password_label_3.setFont(font) + self.saved_connection_change_password_label_3.setAlignment( QtCore.Qt.AlignmentFlag.AlignCenter ) - self.hotspot_info_password_label.setText("Hotspot Password:") - self.hotspot_info_password_label.setObjectName("hotspot_info_password_label") - password_layout.addWidget(self.hotspot_info_password_label) - - self.hotspot_password_input_field = BlocksCustomLinEdit( - parent=password_layout_widget - ) - self.hotspot_password_input_field.setHidden(True) - self.hotspot_password_input_field.setSizePolicy(field_policy) - self.hotspot_password_input_field.setMinimumSize(QtCore.QSize(500, 40)) - self.hotspot_password_input_field.setMaximumSize(QtCore.QSize(500, 60)) - font = QtGui.QFont() - font.setPointSize(12) - self.hotspot_password_input_field.setFont(font) - self.hotspot_password_input_field.setEchoMode( - QtWidgets.QLineEdit.EchoMode.Password + self.saved_connection_change_password_label_3.setText("Change\nPassword") + self.saved_connection_change_password_label_3.setObjectName( + "saved_connection_change_password_label_3" ) - self.hotspot_password_input_field.setObjectName("hotspot_password_input_field") password_layout.addWidget( - self.hotspot_password_input_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - - self.hotspot_password_view_button = IconButton(parent=password_layout_widget) - self.hotspot_password_view_button.setMinimumSize(QtCore.QSize(60, 60)) - self.hotspot_password_view_button.setMaximumSize(QtCore.QSize(60, 60)) - self.hotspot_password_view_button.setText("View") - self.hotspot_password_view_button.setFlat(True) - self.hotspot_password_view_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") - ) - self.hotspot_password_view_button.setProperty("class", "back_btn") - self.hotspot_password_view_button.setProperty("button_type", "icon") - self.hotspot_password_view_button.setObjectName("hotspot_password_view_button") - password_layout.addWidget(self.hotspot_password_view_button) - - content_layout.addWidget(self.frame_7) - - # Save button - self.hotspot_change_confirm = BlocksCustomButton(parent=self.hotspot_page) - self.hotspot_change_confirm.setMinimumSize(QtCore.QSize(200, 80)) - self.hotspot_change_confirm.setMaximumSize(QtCore.QSize(250, 100)) - font = QtGui.QFont() - font.setPointSize(18) - font.setBold(True) - font.setWeight(75) - self.hotspot_change_confirm.setFont(font) - self.hotspot_change_confirm.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/save.svg") - ) - self.hotspot_change_confirm.setText("Save") - self.hotspot_change_confirm.setObjectName("hotspot_change_confirm") - content_layout.addWidget( - self.hotspot_change_confirm, + self.saved_connection_change_password_label_3, 0, QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) - main_layout.addLayout(content_layout) - - self.addWidget(self.hotspot_page) - - def _init_timers(self) -> None: - """Initialize all timers.""" - self._status_check_timer = QtCore.QTimer(self) - self._status_check_timer.setInterval(STATUS_CHECK_INTERVAL_MS) - - self._delayed_action_timer = QtCore.QTimer(self) - self._delayed_action_timer.setSingleShot(True) - - self._load_timer = QtCore.QTimer(self) - self._load_timer.setSingleShot(True) - self._load_timer.timeout.connect(self._handle_load_timeout) - - def _init_model_view(self) -> None: - """Initialize the model and view for network list.""" - self._model = EntryListModel() - self._model.setParent(self.listView) - self._entry_delegate = EntryDelegate() - self.listView.setModel(self._model) - self.listView.setItemDelegate(self._entry_delegate) - self._entry_delegate.item_selected.connect(self._on_ssid_item_clicked) - self._configure_list_view_palette() - - def _init_network_worker(self) -> None: - """Initialize the network list worker.""" - self._network_list_worker = BuildNetworkList( - nm=self._sdbus_network, poll_interval_ms=DEFAULT_POLL_INTERVAL_MS - ) - self._network_list_worker.finished_network_list_build.connect( - self._handle_network_list - ) - self._network_list_worker.start_polling() - self.rescan_button.clicked.connect(self._network_list_worker.build) - - def _setup_navigation_signals(self) -> None: - """Setup navigation button signals.""" - self.wifi_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) - ) - self.hotspot_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.hotspot_page)) - ) - self.nl_back_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) - ) - self.network_backButton.clicked.connect(self.hide) - - self.add_network_page_backButton.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) - ) - - self.saved_connection_back_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) - ) - self.snd_back.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.saved_connection_page)) - ) - self.network_details_btn.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.saved_details_page)) + self.saved_connection_change_password_field = BlocksCustomLinEdit( + parent=frame_layout_widget ) - - self.hotspot_back_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) + self.saved_connection_change_password_field.setHidden(True) + self.saved_connection_change_password_field.setMinimumSize( + QtCore.QSize(500, 60) ) - self.hotspot_change_confirm.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) + self.saved_connection_change_password_field.setMaximumSize( + QtCore.QSize(500, 16777215) ) - - def _setup_action_signals(self) -> None: - """Setup action button signals.""" - self._sdbus_network.nm_state_change.connect(self._evaluate_network_state) - self.request_network_scan.connect(self._rescan_networks) - self.delete_network_signal.connect(self._delete_network) - - self.add_network_validation_button.clicked.connect(self._add_network) - - self.snd_back.clicked.connect(self._on_save_network_settings) - self.network_activate_btn.clicked.connect(self._on_saved_wifi_option_selected) - self.network_delete_btn.clicked.connect(self._on_saved_wifi_option_selected) - - self._status_check_timer.timeout.connect(self._check_connection_status) - - def _setup_toggle_signals(self) -> None: - """Setup toggle button signals.""" - self.wifi_button.toggle_button.stateChange.connect(self._on_toggle_state) - self.hotspot_button.toggle_button.stateChange.connect(self._on_toggle_state) - - def _setup_password_visibility_signals(self) -> None: - """Setup password visibility toggle signals.""" - self._setup_password_visibility_toggle( - self.add_network_password_view, - self.add_network_password_field, + font = QtGui.QFont() + font.setPointSize(12) + self.saved_connection_change_password_field.setFont(font) + self.saved_connection_change_password_field.setObjectName( + "saved_connection_change_password_field" ) - self._setup_password_visibility_toggle( - self.saved_connection_change_password_view, + password_layout.addWidget( self.saved_connection_change_password_field, - ) - self._setup_password_visibility_toggle( - self.hotspot_password_view_button, - self.hotspot_password_input_field, - ) - - def _setup_password_visibility_toggle( - self, view_button: QtWidgets.QWidget, password_field: QtWidgets.QLineEdit - ) -> None: - """Setup password visibility toggle for a button/field pair.""" - view_button.setCheckable(True) - - see_icon = QtGui.QPixmap(":/ui/media/btn_icons/see.svg") - unsee_icon = QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") - - # Connect toggle signal - view_button.toggled.connect( - lambda checked: password_field.setHidden(not checked) - ) - - # Update icon based on toggle state - view_button.toggled.connect( - lambda checked: view_button.setPixmap( - unsee_icon if not checked else see_icon - ) + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter, ) - def _setup_icons(self) -> None: - """Setup button icons.""" - self.hotspot_button.setPixmap( - QtGui.QPixmap(":/network/media/btn_icons/hotspot.svg") - ) - self.wifi_button.setPixmap( - QtGui.QPixmap(":/network/media/btn_icons/wifi_config.svg") + self.saved_connection_change_password_view = IconButton( + parent=frame_layout_widget ) - self.network_delete_btn.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/garbage-icon.svg") + self.saved_connection_change_password_view.setMinimumSize(QtCore.QSize(60, 60)) + self.saved_connection_change_password_view.setMaximumSize(QtCore.QSize(60, 60)) + self.saved_connection_change_password_view.setText("View") + self.saved_connection_change_password_view.setFlat(True) + self.saved_connection_change_password_view.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/unsee.svg") ) - self.network_activate_btn.setPixmap( - QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") + self.saved_connection_change_password_view.setProperty("class", "back_btn") + self.saved_connection_change_password_view.setProperty("button_type", "icon") + self.saved_connection_change_password_view.setObjectName( + "saved_connection_change_password_view" ) - self.network_details_btn.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/printer_settings.svg") + password_layout.addWidget( + self.saved_connection_change_password_view, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) - def _setup_input_fields(self) -> None: - """Setup input field properties.""" - self.add_network_password_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) - self.hotspot_name_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) - self.hotspot_password_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) - - self.hotspot_password_input_field.setPlaceholderText("Defaults to: 123456789") - self.hotspot_name_input_field.setText( - str(self._sdbus_network.get_hotspot_ssid() or "PrinterHotspot") - ) - self.hotspot_password_input_field.setText( - str(self._sdbus_network.hotspot_password or "123456789") - ) + content_layout.addWidget(self.frame_9) - def _setup_keyboard(self) -> None: - """Setup the on-screen keyboard.""" - self._qwerty = CustomQwertyKeyboard(self) - self.addWidget(self._qwerty) - self._qwerty.value_selected.connect(self._on_qwerty_value_selected) - self._qwerty.request_back.connect(self._on_qwerty_go_back) + priority_outer_layout = QtWidgets.QHBoxLayout() - self.add_network_password_field.clicked.connect( - lambda: self._on_show_keyboard( - self.add_network_page, self.add_network_password_field - ) - ) - self.hotspot_password_input_field.clicked.connect( - lambda: self._on_show_keyboard( - self.hotspot_page, self.hotspot_password_input_field - ) - ) - self.hotspot_name_input_field.clicked.connect( - lambda: self._on_show_keyboard( - self.hotspot_page, self.hotspot_name_input_field - ) - ) - self.saved_connection_change_password_field.clicked.connect( - lambda: self._on_show_keyboard( - self.saved_connection_page, - self.saved_connection_change_password_field, - ) - ) + priority_inner_layout = QtWidgets.QVBoxLayout() - def _setup_scrollbar_signals(self) -> None: - """Setup scrollbar synchronization signals.""" - self.listView.verticalScrollBar().valueChanged.connect( - self._handle_scrollbar_change - ) - self.verticalScrollBar.valueChanged.connect(self._handle_scrollbar_change) - self.verticalScrollBar.valueChanged.connect( - lambda value: self.listView.verticalScrollBar().setValue(value) + self.frame_12 = BlocksCustomFrame(parent=self.saved_details_page) + frame_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, ) - self.verticalScrollBar.show() - - def _configure_list_view_palette(self) -> None: - """Configure the list view palette for transparency.""" - palette = QtGui.QPalette() - - for group in [ - QtGui.QPalette.ColorGroup.Active, - QtGui.QPalette.ColorGroup.Inactive, - QtGui.QPalette.ColorGroup.Disabled, - ]: - transparent = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - transparent.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(group, QtGui.QPalette.ColorRole.Button, transparent) - palette.setBrush(group, QtGui.QPalette.ColorRole.Window, transparent) + self.frame_12.setSizePolicy(frame_policy) + self.frame_12.setMinimumSize(QtCore.QSize(400, 160)) + self.frame_12.setMaximumSize(QtCore.QSize(400, 99999)) + self.frame_12.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_12.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.frame_12.setProperty("text", "Network priority") - no_brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - no_brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) - palette.setBrush(group, QtGui.QPalette.ColorRole.Base, no_brush) + frame_inner_layout = QtWidgets.QVBoxLayout(self.frame_12) - highlight = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) - highlight.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(group, QtGui.QPalette.ColorRole.Highlight, highlight) + frame_inner_layout.addItem( + QtWidgets.QSpacerItem( + 10, + 10, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) - link = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) - link.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(group, QtGui.QPalette.ColorRole.Link, link) + buttons_layout = QtWidgets.QHBoxLayout() - self.listView.setPalette(palette) + self.priority_btn_group = QtWidgets.QButtonGroup(self) - def _show_error_popup(self, message: str, timeout: int = 6000) -> None: - """Show an error popup message.""" - self._popup.raise_() - self._popup.new_message( - message_type=Popup.MessageType.ERROR, - message=message, - timeout=timeout, - userInput=False, + self.low_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) + self.low_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) + self.low_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) + self.low_priority_btn.setCheckable(True) + self.low_priority_btn.setFlat(True) + self.low_priority_btn.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/indf_svg.svg") ) + self.low_priority_btn.setText("Low") + self.low_priority_btn.setProperty("class", "back_btn") + self.low_priority_btn.setProperty("button_type", "icon") - def _show_info_popup(self, message: str, timeout: int = 4000) -> None: - """Show an info popup message.""" - self._popup.raise_() - self._popup.new_message( - message_type=Popup.MessageType.INFO, - message=message, - timeout=timeout, - userInput=False, - ) + self.priority_btn_group.addButton(self.low_priority_btn) + buttons_layout.addWidget(self.low_priority_btn) - def _show_warning_popup(self, message: str, timeout: int = 5000) -> None: - """Show a warning popup message.""" - self._popup.raise_() - self._popup.new_message( - message_type=Popup.MessageType.WARNING, - message=message, - timeout=timeout, - userInput=False, + self.med_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) + self.med_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) + self.med_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) + self.med_priority_btn.setCheckable(True) + self.med_priority_btn.setChecked(False) # Don't set default checked + self.med_priority_btn.setFlat(True) + self.med_priority_btn.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/indf_svg.svg") ) + self.med_priority_btn.setText("Medium") + self.med_priority_btn.setProperty("class", "back_btn") + self.med_priority_btn.setProperty("button_type", "icon") - def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: - """Handle close event.""" - self._stop_all_timers() - self._network_list_worker.stop_polling() - super().closeEvent(event) + self.priority_btn_group.addButton(self.med_priority_btn) + buttons_layout.addWidget(self.med_priority_btn) - def showEvent(self, event: Optional[QtGui.QShowEvent]) -> None: - """Handle show event.""" - if self._networks: - self._build_model_list() - self._evaluate_network_state() - super().showEvent(event) + self.high_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) + self.high_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) + self.high_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) + self.high_priority_btn.setCheckable(True) + self.high_priority_btn.setChecked(False) + self.high_priority_btn.setFlat(True) + self.high_priority_btn.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/indf_svg.svg") + ) + self.high_priority_btn.setText("High") + self.high_priority_btn.setProperty("class", "back_btn") + self.high_priority_btn.setProperty("button_type", "icon") - def _stop_all_timers(self) -> None: - """Stop all active timers.""" - timers = [ - self._load_timer, - self._status_check_timer, - self._delayed_action_timer, - ] - for timer in timers: - if timer.isActive(): - timer.stop() + self.priority_btn_group.addButton(self.high_priority_btn) + buttons_layout.addWidget(self.high_priority_btn) - def _on_show_keyboard( - self, panel: QtWidgets.QWidget, field: QtWidgets.QLineEdit - ) -> None: - """Show the on-screen keyboard for a field.""" - self._previous_panel = panel - self._current_field = field - self._qwerty.set_value(field.text()) - self.setCurrentIndex(self.indexOf(self._qwerty)) + frame_inner_layout.addLayout(buttons_layout) - def _on_qwerty_go_back(self) -> None: - """Handle keyboard back button.""" - if self._previous_panel: - self.setCurrentIndex(self.indexOf(self._previous_panel)) + priority_inner_layout.addWidget( + self.frame_12, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) - def _on_qwerty_value_selected(self, value: str) -> None: - """Handle keyboard value selection.""" - if self._previous_panel: - self.setCurrentIndex(self.indexOf(self._previous_panel)) - if self._current_field: - self._current_field.setText(value) + priority_outer_layout.addLayout(priority_inner_layout) + content_layout.addLayout(priority_outer_layout) - def _set_loading_state(self, loading: bool) -> None: - """Set loading state - controls loading widget visibility. + bottom_btn_layout = QtWidgets.QHBoxLayout() + bottom_btn_layout.setSpacing(20) - This method ensures mutual exclusivity between - loading widget, network details, and info box. - """ - self.wifi_button.setEnabled(not loading) - self.hotspot_button.setEnabled(not loading) + self.saved_details_save_btn = BlocksCustomButton(parent=self.saved_details_page) + self.saved_details_save_btn.setMinimumSize(QtCore.QSize(200, 80)) + self.saved_details_save_btn.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(16) + self.saved_details_save_btn.setFont(font) + self.saved_details_save_btn.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/save.svg") + ) + self.saved_details_save_btn.setText("Save") + bottom_btn_layout.addWidget( + self.saved_details_save_btn, + 0, + QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) - if loading: - self._is_connecting = True - # - # Hide ALL other elements first before showing loading - # This prevents the dual panel visibility bug - self._hide_all_info_elements() - # Force UI update to ensure elements are hidden - self.repaint() - # Now show loading - self.loadingwidget.setVisible(True) + self.wifi_static_ip_btn = BlocksCustomButton(parent=self.saved_details_page) + self.wifi_static_ip_btn.setMinimumSize(QtCore.QSize(200, 80)) + self.wifi_static_ip_btn.setMaximumSize(QtCore.QSize(250, 80)) + self.wifi_static_ip_btn.setFont(font) + self.wifi_static_ip_btn.setFlat(True) + self.wifi_static_ip_btn.setText("Static\nIP") + self.wifi_static_ip_btn.setProperty( + "icon_pixmap", + PixmapCache.get(":/network/media/btn_icons/network/static_ip.svg"), + ) + bottom_btn_layout.addWidget( + self.wifi_static_ip_btn, + 0, + QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) - if self._load_timer.isActive(): - self._load_timer.stop() - self._load_timer.start(LOAD_TIMEOUT_MS) - if not self._status_check_timer.isActive(): - self._status_check_timer.start() - else: - self._is_connecting = False - self._target_ssid = None - # Just hide loading - caller decides what to show next - self.loadingwidget.setVisible(False) + content_layout.addLayout(bottom_btn_layout) - if self._load_timer.isActive(): - self._load_timer.stop() - if self._status_check_timer.isActive(): - self._status_check_timer.stop() + main_layout.addLayout(content_layout) - def _show_network_details(self) -> None: - """Show network details panel - HIDES everything else first.""" - # Hide everything else first to prevent dual panel - self.loadingwidget.setVisible(False) - self.mn_info_box.setVisible(False) - # Force UI update - self.repaint() + self.addWidget(self.saved_details_page) - # Then show only the details - self.netlist_ip.setVisible(True) - self.netlist_ssuid.setVisible(True) - self.mn_info_seperator.setVisible(True) - self.line_2.setVisible(True) - self.netlist_strength.setVisible(True) - self.netlist_strength_label.setVisible(True) - self.line_3.setVisible(True) - self.netlist_security.setVisible(True) - self.netlist_security_label.setVisible(True) + def _setup_hotspot_page(self) -> None: + """Setup the hotspot configuration page.""" + self.hotspot_page = QtWidgets.QWidget() - def _show_disconnected_message(self) -> None: - """Show the disconnected state message - HIDES everything else first.""" - # Hide everything else first to prevent dual panel - self.loadingwidget.setVisible(False) - self._hide_network_detail_labels() - # Force UI update - self.repaint() + main_layout = QtWidgets.QVBoxLayout(self.hotspot_page) - # Then show info box - self._configure_info_box_centered() - self.mn_info_box.setVisible(True) - self.mn_info_box.setText( - "Network connection required.\n\nConnect to Wi-Fi\nor\nTurn on Hotspot" + header_layout = QtWidgets.QHBoxLayout() + + header_layout.addItem( + QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) ) + title_font = QtGui.QFont() + title_font.setPointSize(20) - def _hide_network_detail_labels(self) -> None: - """Hide only the network detail labels (not loading or info box).""" - self.netlist_ip.setVisible(False) - self.netlist_ssuid.setVisible(False) - self.mn_info_seperator.setVisible(False) - self.line_2.setVisible(False) - self.netlist_strength.setVisible(False) - self.netlist_strength_label.setVisible(False) - self.line_3.setVisible(False) - self.netlist_security.setVisible(False) - self.netlist_security_label.setVisible(False) + self.hotspot_header_title = QtWidgets.QLabel(parent=self.hotspot_page) + self.hotspot_header_title.setPalette(self._create_white_palette()) + self.hotspot_header_title.setFont(title_font) + self.hotspot_header_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.hotspot_header_title.setText("Hotspot") - def _check_connection_status(self) -> None: - """Backup periodic check to detect successful connections.""" - if not self.loadingwidget.isVisible(): - if self._status_check_timer.isActive(): - self._status_check_timer.stop() - return + header_layout.addWidget(self.hotspot_header_title) - connectivity = self._sdbus_network.check_connectivity() - is_connected = connectivity in ("FULL", "LIMITED") + self.hotspot_back_button = IconButton(parent=self.hotspot_page) + self.hotspot_back_button.setMinimumSize(QtCore.QSize(60, 60)) + self.hotspot_back_button.setMaximumSize(QtCore.QSize(60, 60)) + self.hotspot_back_button.setFlat(True) + self.hotspot_back_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") + ) + self.hotspot_back_button.setProperty("class", "back_btn") + self.hotspot_back_button.setProperty("button_type", "icon") - wifi_btn = self.wifi_button.toggle_button - hotspot_btn = self.hotspot_button.toggle_button + header_layout.addWidget(self.hotspot_back_button) - if hotspot_btn.state == hotspot_btn.State.ON: - hotspot_ip = self._sdbus_network.get_device_ip_by_interface("wlan0") - if hotspot_ip: - logger.debug("Hotspot connection detected via status check") - # Stop loading first, then show details - self._set_loading_state(False) - self._update_hotspot_display() - self._show_network_details() - return + main_layout.addLayout(header_layout) - if wifi_btn.state == wifi_btn.State.ON: - current_ssid = self._sdbus_network.get_current_ssid() + self.hotspot_header_title.setMaximumSize(QtCore.QSize(16777215, 60)) - if self._target_ssid: - if current_ssid == self._target_ssid and is_connected: - logger.debug("Target Wi-Fi connection detected: %s", current_ssid) - # Stop loading first, then show details - self._set_loading_state(False) - self._update_wifi_display() - self._show_network_details() - return - else: - if current_ssid and is_connected: - logger.debug("Wi-Fi connection detected: %s", current_ssid) - # Stop loading first, then show details - self._set_loading_state(False) - self._update_wifi_display() - self._show_network_details() - return + content_layout = QtWidgets.QHBoxLayout() + content_layout.setContentsMargins(-1, 5, -1, 5) - def _handle_load_timeout(self) -> None: - """Handle connection timeout.""" - if not self.loadingwidget.isVisible(): - return + # Left side: QR code frame + self.frame_4 = QtWidgets.QFrame(parent=self.hotspot_page) + frame_4_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.frame_4.setSizePolicy(frame_4_policy) + qr_frame_font = QtGui.QFont() + qr_frame_font.setPointSize(15) + self.frame_4.setFont(qr_frame_font) + self.frame_4.setStyleSheet("color: white;") - connectivity = self._sdbus_network.check_connectivity() - is_connected = connectivity in ("FULL", "LIMITED") + frame_4_layout = QtWidgets.QHBoxLayout(self.frame_4) - wifi_btn = self.wifi_button - hotspot_btn = self.hotspot_button + self.qrcode_img = BlocksLabel(parent=self.frame_4) + self.qrcode_img.setMinimumSize(QtCore.QSize(325, 325)) + self.qrcode_img.setMaximumSize(QtCore.QSize(325, 325)) + qrcode_font = QtGui.QFont() + qrcode_font.setPointSize(15) + self.qrcode_img.setFont(qrcode_font) + self.qrcode_img.setText("Hotspot not active") - # Final check if connection succeeded - if wifi_btn.toggle_button.state == wifi_btn.toggle_button.State.ON: - current_ssid = self._sdbus_network.get_current_ssid() + frame_4_layout.addWidget(self.qrcode_img) - if self._target_ssid: - if current_ssid == self._target_ssid and is_connected: - logger.debug("Target connection succeeded on timeout check") - self._set_loading_state(False) - self._update_wifi_display() - self._show_network_details() - return - else: - if current_ssid and is_connected: - logger.debug("Connection succeeded on timeout check") - self._set_loading_state(False) - self._update_wifi_display() - self._show_network_details() - return + content_layout.addWidget(self.frame_4) - elif hotspot_btn.toggle_button.state == hotspot_btn.toggle_button.State.ON: - hotspot_ip = self._sdbus_network.get_device_ip_by_interface("wlan0") - if hotspot_ip: - logger.debug("Hotspot succeeded on timeout check") - self._set_loading_state(False) - self._update_hotspot_display() - self._show_network_details() - return + # Right side: form fields frame + self.frame_3 = QtWidgets.QFrame(parent=self.hotspot_page) + self.frame_3.setMaximumWidth(350) - # Connection actually failed - self._is_connecting = False - self._target_ssid = None - self._set_loading_state(False) + frame_3_layout = QtWidgets.QVBoxLayout(self.frame_3) - # Show error message - self._hide_all_info_elements() - self._configure_info_box_centered() - self.mn_info_box.setVisible(True) - self.mn_info_box.setText(self._get_timeout_message(wifi_btn, hotspot_btn)) + label_font = QtGui.QFont() + label_font.setPointSize(15) + label_font.setFamily("Momcake") + field_font = QtGui.QFont() + field_font.setPointSize(12) - hotspot_btn.setEnabled(True) - wifi_btn.setEnabled(True) + self.hotspot_info_name_label = QtWidgets.QLabel(parent=self.frame_3) + name_label_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Maximum, + ) + self.hotspot_info_name_label.setSizePolicy(name_label_policy) + self.hotspot_info_name_label.setMinimumSize(QtCore.QSize(173, 0)) + self.hotspot_info_name_label.setPalette(self._create_white_palette()) + self.hotspot_info_name_label.setFont(label_font) + self.hotspot_info_name_label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignCenter | QtCore.Qt.AlignmentFlag.AlignBottom + ) + self.hotspot_info_name_label.setText("Hotspot Name") - self._show_error_popup("Connection timed out. Please try again.") + frame_3_layout.addWidget(self.hotspot_info_name_label) - def _get_timeout_message(self, wifi_btn, hotspot_btn) -> str: - """Get appropriate timeout message based on state.""" - if wifi_btn.toggle_button.state == wifi_btn.toggle_button.State.ON: - return "Wi-Fi Connection Failed.\nThe connection attempt\n timed out." - elif hotspot_btn.toggle_button.state == hotspot_btn.toggle_button.State.ON: - return "Hotspot Setup Failed.\nPlease restart the hotspot." - else: - return "Loading timed out.\nPlease check your connection\n and try again." + self.hotspot_name_input_field = BlocksCustomLinEdit(parent=self.frame_3) + self.hotspot_name_input_field.setMinimumSize(QtCore.QSize(300, 40)) + self.hotspot_name_input_field.setMaximumSize(QtCore.QSize(300, 60)) + self.hotspot_name_input_field.setFont(field_font) + self.hotspot_name_input_field.setEchoMode(QtWidgets.QLineEdit.EchoMode.Normal) - def _configure_info_box_centered(self) -> None: - """Configure info box for centered text.""" - self.mn_info_box.setWordWrap(True) - self.mn_info_box.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + frame_3_layout.addWidget( + self.hotspot_name_input_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) - def _clear_network_display(self) -> None: - """Clear all network display labels.""" - self.netlist_ssuid.setText("") - self.netlist_ip.setText("") - self.netlist_strength.setText("") - self.netlist_security.setText("") - self._last_displayed_ssid = None + self.hotspot_info_password_label = QtWidgets.QLabel(parent=self.frame_3) + self.hotspot_info_password_label.setSizePolicy(name_label_policy) + self.hotspot_info_password_label.setMinimumSize(QtCore.QSize(173, 0)) + self.hotspot_info_password_label.setPalette(self._create_white_palette()) + self.hotspot_info_password_label.setFont(label_font) + self.hotspot_info_password_label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignCenter | QtCore.Qt.AlignmentFlag.AlignBottom + ) + self.hotspot_info_password_label.setText("Hotspot Password") - @QtCore.pyqtSlot(object, name="stateChange") - def _on_toggle_state(self, new_state) -> None: - """Handle toggle button state change.""" - sender_button = self.sender() - wifi_btn = self.wifi_button.toggle_button - hotspot_btn = self.hotspot_button.toggle_button - is_sender_now_on = new_state == sender_button.State.ON + frame_3_layout.addWidget(self.hotspot_info_password_label) - # Show loading IMMEDIATELY when turning something on - if is_sender_now_on: - self._set_loading_state(True) - self.repaint() + self.hotspot_password_input_field = BlocksCustomLinEdit(parent=self.frame_3) + self.hotspot_password_input_field.setMinimumSize(QtCore.QSize(300, 40)) + self.hotspot_password_input_field.setMaximumSize(QtCore.QSize(300, 60)) + self.hotspot_password_input_field.setFont(field_font) + self.hotspot_password_input_field.setEchoMode( + QtWidgets.QLineEdit.EchoMode.Password + ) - saved_networks = self._sdbus_network.get_saved_networks_with_for() + frame_3_layout.addWidget( + self.hotspot_password_input_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) - if sender_button is wifi_btn: - self._handle_wifi_toggle(is_sender_now_on, hotspot_btn, saved_networks) - elif sender_button is hotspot_btn: - self._handle_hotspot_toggle(is_sender_now_on, wifi_btn, saved_networks) + frame_3_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 40, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) - # Handle both OFF - if ( - hotspot_btn.state == hotspot_btn.State.OFF - and wifi_btn.state == wifi_btn.State.OFF - ): - self._set_loading_state(False) - self._show_disconnected_message() + self.hotspot_change_confirm = BlocksCustomButton(parent=self.frame_3) + self.hotspot_change_confirm.setMinimumSize(QtCore.QSize(250, 80)) + self.hotspot_change_confirm.setMaximumSize(QtCore.QSize(250, 80)) + confirm_font = QtGui.QFont() + confirm_font.setPointSize(18) + confirm_font.setBold(True) + confirm_font.setWeight(75) + self.hotspot_change_confirm.setFont(confirm_font) + self.hotspot_change_confirm.setProperty( + "icon_pixmap", PixmapCache.get(":/dialog/media/btn_icons/yes.svg") + ) + self.hotspot_change_confirm.setText("Activate") - def _handle_wifi_toggle( - self, is_on: bool, hotspot_btn, saved_networks: List[Dict] - ) -> None: - """Handle Wi-Fi toggle state change.""" - if not is_on: - self._target_ssid = None - return + frame_3_layout.addWidget( + self.hotspot_change_confirm, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) + + content_layout.addWidget(self.frame_3) - hotspot_btn.state = hotspot_btn.State.OFF - self._sdbus_network.toggle_hotspot(False) + main_layout.addLayout(content_layout) - # Check if already connected - current_ssid = self._sdbus_network.get_current_ssid() - connectivity = self._sdbus_network.check_connectivity() + self.addWidget(self.hotspot_page) - if current_ssid and connectivity == "FULL": - # Already connected - show immediately - self._target_ssid = current_ssid - self._set_loading_state(False) - self._update_wifi_display() - self._show_network_details() - return + def _setup_hidden_network_page(self) -> None: + """Setup the hidden network page for connecting to networks with hidden SSID.""" + self.hidden_network_page = QtWidgets.QWidget() - # Filter wifi networks (not hotspots) - wifi_networks = [ - n for n in saved_networks if "ap" not in str(n.get("mode", "")) - ] + main_layout = QtWidgets.QVBoxLayout(self.hidden_network_page) - if not wifi_networks: - self._set_loading_state(False) - self._show_warning_popup( - "No saved Wi-Fi networks. Please add a network first." + header_layout = QtWidgets.QHBoxLayout() + header_layout.addItem( + QtWidgets.QSpacerItem( + 40, + 60, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) - self._show_disconnected_message() - return + ) - try: - ssid = wifi_networks[0]["ssid"] - self._target_ssid = ssid - self._sdbus_network.connect_network(str(ssid)) - except Exception as e: - logger.error("Error when turning ON wifi: %s", e) - self._set_loading_state(False) - self._show_error_popup("Failed to connect to Wi-Fi") - - def _handle_hotspot_toggle( - self, is_on: bool, wifi_btn, saved_networks: List[Dict] - ) -> None: - """Handle hotspot toggle state change.""" - if not is_on: - self._target_ssid = None - return + self.hidden_network_title = QtWidgets.QLabel(parent=self.hidden_network_page) + self.hidden_network_title.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(20) + self.hidden_network_title.setFont(font) + self.hidden_network_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.hidden_network_title.setText("Hidden Network") + header_layout.addWidget(self.hidden_network_title) - wifi_btn.state = wifi_btn.State.OFF - self._target_ssid = None + self.hidden_network_back_button = IconButton(parent=self.hidden_network_page) + self.hidden_network_back_button.setMinimumSize(QtCore.QSize(60, 60)) + self.hidden_network_back_button.setMaximumSize(QtCore.QSize(60, 60)) + self.hidden_network_back_button.setFlat(True) + self.hidden_network_back_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") + ) + self.hidden_network_back_button.setProperty("button_type", "icon") + header_layout.addWidget(self.hidden_network_back_button) - new_hotspot_name = self.hotspot_name_input_field.text() or "PrinterHotspot" - new_hotspot_password = self.hotspot_password_input_field.text() or "123456789" + main_layout.addLayout(header_layout) - # Use QTimer to defer async operations - def setup_hotspot(): - try: - self._sdbus_network.create_hotspot( - new_hotspot_name, new_hotspot_password - ) - self._sdbus_network.toggle_hotspot(True) - except Exception as e: - logger.error("Error creating/activating hotspot: %s", e) - self._show_error_popup("Failed to start hotspot") - self._set_loading_state(False) + content_layout = QtWidgets.QVBoxLayout() + content_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 30, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) - QtCore.QTimer.singleShot(100, setup_hotspot) + ssid_frame = BlocksCustomFrame(parent=self.hidden_network_page) + ssid_frame.setMinimumSize(QtCore.QSize(0, 80)) + ssid_frame.setMaximumSize(QtCore.QSize(16777215, 90)) + ssid_frame_layout = QtWidgets.QHBoxLayout(ssid_frame) - @QtCore.pyqtSlot(str, name="nm-state-changed") - def _evaluate_network_state(self, nm_state: str = "") -> None: - """Evaluate and update network state.""" - wifi_btn = self.wifi_button.toggle_button - hotspot_btn = self.hotspot_button.toggle_button + ssid_label = QtWidgets.QLabel("Network\nName", parent=ssid_frame) + ssid_label.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + ssid_label.setFont(font) + ssid_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + ssid_frame_layout.addWidget(ssid_label) - state = nm_state or self._sdbus_network.check_nm_state() - if not state: - return + self.hidden_network_ssid_field = BlocksCustomLinEdit(parent=ssid_frame) + self.hidden_network_ssid_field.setMinimumSize(QtCore.QSize(500, 60)) + font = QtGui.QFont() + font.setPointSize(12) + self.hidden_network_ssid_field.setFont(font) + self.hidden_network_ssid_field.setPlaceholderText("Enter network name") + ssid_frame_layout.addWidget(self.hidden_network_ssid_field) - if self._is_first_run: - self._handle_first_run_state() - self._is_first_run = False - return + content_layout.addWidget(ssid_frame) - if not self._sdbus_network.check_wifi_interface(): - return + content_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) - # Handle both OFF first - if ( - wifi_btn.state == wifi_btn.State.OFF - and hotspot_btn.state == hotspot_btn.State.OFF - ): - self._sdbus_network.disconnect_network() - self._clear_network_display() - self._set_loading_state(False) - self._show_disconnected_message() - return + password_frame = BlocksCustomFrame(parent=self.hidden_network_page) + password_frame.setMinimumSize(QtCore.QSize(0, 80)) + password_frame.setMaximumSize(QtCore.QSize(16777215, 90)) + password_frame_layout = QtWidgets.QHBoxLayout(password_frame) - connectivity = self._sdbus_network.check_connectivity() - is_connected = connectivity in ("FULL", "LIMITED") + password_label = QtWidgets.QLabel("Password", parent=password_frame) + password_label.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + password_label.setFont(font) + password_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + password_frame_layout.addWidget(password_label) - # Handle hotspot - if hotspot_btn.state == hotspot_btn.State.ON: - hotspot_ip = self._sdbus_network.get_device_ip_by_interface("wlan0") - if hotspot_ip or is_connected: - # Stop loading first, then update display, then show details - self._set_loading_state(False) - self._update_hotspot_display() - self._show_network_details() - self.wifi_button.setEnabled(True) - self.hotspot_button.setEnabled(True) - return + self.hidden_network_password_field = BlocksCustomLinEdit(parent=password_frame) + self.hidden_network_password_field.setHidden(True) + self.hidden_network_password_field.setMinimumSize(QtCore.QSize(500, 60)) + font = QtGui.QFont() + font.setPointSize(12) + self.hidden_network_password_field.setFont(font) + self.hidden_network_password_field.setPlaceholderText( + "Enter password (leave empty for open networks)" + ) + self.hidden_network_password_field.setEchoMode( + QtWidgets.QLineEdit.EchoMode.Password + ) + password_frame_layout.addWidget(self.hidden_network_password_field) - # Handle wifi - if wifi_btn.state == wifi_btn.State.ON: - current_ssid = self._sdbus_network.get_current_ssid() - - if self._target_ssid: - if current_ssid == self._target_ssid and is_connected: - logger.debug("Connected to target: %s", current_ssid) - # Stop loading first, then update display, then show details - self._set_loading_state(False) - self._update_wifi_display() - self._show_network_details() - self.wifi_button.setEnabled(True) - self.hotspot_button.setEnabled(True) - else: - if current_ssid and is_connected: - # Stop loading first, then update display, then show details - self._set_loading_state(False) - self._update_wifi_display() - self._show_network_details() - self.wifi_button.setEnabled(True) - self.hotspot_button.setEnabled(True) - self.update() + self.hidden_network_password_view = IconButton(parent=password_frame) + self.hidden_network_password_view.setMinimumSize(QtCore.QSize(60, 60)) + self.hidden_network_password_view.setMaximumSize(QtCore.QSize(60, 60)) + self.hidden_network_password_view.setFlat(True) + self.hidden_network_password_view.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/unsee.svg") + ) + self.hidden_network_password_view.setProperty("button_type", "icon") + password_frame_layout.addWidget(self.hidden_network_password_view) - def _handle_first_run_state(self) -> None: - """Handle initial state on first run.""" - saved_networks = self._sdbus_network.get_saved_networks_with_for() + content_layout.addWidget(password_frame) - old_hotspot = next( - (n for n in saved_networks if "ap" in str(n.get("mode", ""))), None + content_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 50, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) ) - if old_hotspot: - self.hotspot_name_input_field.setText(old_hotspot["ssid"]) - connectivity = self._sdbus_network.check_connectivity() - wifi_btn = self.wifi_button.toggle_button - hotspot_btn = self.hotspot_button.toggle_button - current_ssid = self._sdbus_network.get_current_ssid() + self.hidden_network_connect_button = BlocksCustomButton( + parent=self.hidden_network_page + ) + self.hidden_network_connect_button.setMinimumSize(QtCore.QSize(250, 80)) + self.hidden_network_connect_button.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(15) + self.hidden_network_connect_button.setFont(font) + self.hidden_network_connect_button.setFlat(True) + self.hidden_network_connect_button.setProperty( + "icon_pixmap", PixmapCache.get(":/dialog/media/btn_icons/yes.svg") + ) + self.hidden_network_connect_button.setText("Connect") + content_layout.addWidget( + self.hidden_network_connect_button, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) - self._is_connecting = False - self.loadingwidget.setVisible(False) + main_layout.addLayout(content_layout) + self.addWidget(self.hidden_network_page) - with QtCore.QSignalBlocker(wifi_btn), QtCore.QSignalBlocker(hotspot_btn): - if connectivity == "FULL" and current_ssid: - wifi_btn.state = wifi_btn.State.ON - hotspot_btn.state = hotspot_btn.State.OFF - self._update_wifi_display() - self._show_network_details() - self.wifi_button.setEnabled(True) - self.hotspot_button.setEnabled(True) - elif connectivity == "LIMITED": - wifi_btn.state = wifi_btn.State.OFF - hotspot_btn.state = hotspot_btn.State.ON - self._update_hotspot_display() - self._show_network_details() - self.wifi_button.setEnabled(True) - self.hotspot_button.setEnabled(True) - else: - wifi_btn.state = wifi_btn.State.OFF - hotspot_btn.state = hotspot_btn.State.OFF - self._clear_network_display() - self._show_disconnected_message() - self.wifi_button.setEnabled(True) - self.hotspot_button.setEnabled(True) - - def _update_hotspot_display(self) -> None: - """Update display for hotspot mode.""" - ipv4_addr = self._sdbus_network.get_device_ip_by_interface("wlan0") - if not ipv4_addr: - ipv4_addr = self._sdbus_network.get_current_ip_addr() - - hotspot_name = self.hotspot_name_input_field.text() - if not hotspot_name: - hotspot_name = self._sdbus_network.hotspot_ssid or "Hotspot" - self.hotspot_name_input_field.setText(hotspot_name) - - self.netlist_ssuid.setText(hotspot_name) - # Handle empty IP properly - if ipv4_addr and ipv4_addr.strip(): - self.netlist_ip.setText(f"IP: {ipv4_addr}") - else: - self.netlist_ip.setText("IP: Obtaining...") - self.netlist_strength.setText("--") - self.netlist_security.setText("WPA2") - self._last_displayed_ssid = hotspot_name + self.hidden_network_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) + ) + self.hidden_network_connect_button.clicked.connect( + self._on_hidden_network_connect + ) + self.hidden_network_ssid_field.clicked.connect( + lambda: self._on_show_keyboard( + self.hidden_network_page, self.hidden_network_ssid_field + ) + ) + self.hidden_network_password_field.clicked.connect( + lambda: self._on_show_keyboard( + self.hidden_network_page, self.hidden_network_password_field + ) + ) + self._setup_password_visibility_toggle( + self.hidden_network_password_view, self.hidden_network_password_field + ) - def _update_wifi_display(self) -> None: - """Update display for wifi connection.""" - current_ssid = self._sdbus_network.get_current_ssid() + def _setup_vlan_page(self) -> None: + """Construct the VLAN settings page widgets and add it to the stacked widget.""" + self.vlan_page = QtWidgets.QWidget() + main_layout = QtWidgets.QVBoxLayout(self.vlan_page) - if current_ssid: - ipv4_addr = self._sdbus_network.get_current_ip_addr() - sec_type = self._sdbus_network.get_security_type_by_ssid(current_ssid) - signal_strength = self._sdbus_network.get_connection_signal_by_ssid( - current_ssid + header_layout = QtWidgets.QHBoxLayout() + header_layout.addItem( + QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) + ) + vlan_title = QtWidgets.QLabel("VLAN Configuration", parent=self.vlan_page) + vlan_title.setPalette(self._create_white_palette()) + title_font = QtGui.QFont() + title_font.setPointSize(20) + vlan_title.setFont(title_font) + vlan_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + header_layout.addWidget(vlan_title) + + self.vlan_back_button = IconButton(parent=self.vlan_page) + self.vlan_back_button.setMinimumSize(QtCore.QSize(60, 60)) + self.vlan_back_button.setMaximumSize(QtCore.QSize(60, 60)) + self.vlan_back_button.setFlat(True) + self.vlan_back_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") + ) + self.vlan_back_button.setProperty("button_type", "icon") + header_layout.addWidget(self.vlan_back_button) + main_layout.addLayout(header_layout) - self.netlist_ssuid.setText(current_ssid) - # Handle empty IP properly - if ipv4_addr and ipv4_addr.strip(): - self.netlist_ip.setText(f"IP: {ipv4_addr}") - else: - self.netlist_ip.setText("IP: Obtaining...") - self.netlist_security.setText(str(sec_type or "OPEN").upper()) - self.netlist_strength.setText( - f"{signal_strength}%" - if signal_strength and signal_strength != -1 - else "--" + content_layout = QtWidgets.QVBoxLayout() + content_layout.setContentsMargins(-1, 5, -1, 5) + + label_font = QtGui.QFont() + label_font.setPointSize(13) + label_font.setBold(True) + field_font = QtGui.QFont() + field_font.setPointSize(12) + field_min = QtCore.QSize(360, 45) + field_max = QtCore.QSize(500, 55) + + def _make_row(label_text, field): + """Build a labelled row widget containing *field* for the VLAN settings form.""" + frame = BlocksCustomFrame(parent=self.vlan_page) + frame.setMinimumSize(QtCore.QSize(0, 50)) + frame.setMaximumSize(QtCore.QSize(16777215, 50)) + row = QtWidgets.QHBoxLayout(frame) + row.setContentsMargins(10, 2, 10, 2) + label = QtWidgets.QLabel(label_text, parent=frame) + label.setPalette(self._create_white_palette()) + label.setFont(label_font) + label.setMinimumWidth(120) + label.setMaximumWidth(160) + label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignVCenter ) - self._last_displayed_ssid = current_ssid - else: - self._clear_network_display() + row.addWidget(label) + field.setFont(field_font) + field.setMinimumSize(field_min) + field.setMaximumSize(field_max) + row.addWidget(field) + return frame + + self.vlan_id_spinbox = QtWidgets.QSpinBox(parent=self.vlan_page) + self.vlan_id_spinbox.setRange(1, 4094) + self.vlan_id_spinbox.setValue(1) + self.vlan_id_spinbox.lineEdit().setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.vlan_id_spinbox.lineEdit().setReadOnly(True) + # Prevent text selection when stepping — deselect after each value change + self.vlan_id_spinbox.valueChanged.connect( + lambda: self.vlan_id_spinbox.lineEdit().deselect() + ) + self.vlan_id_spinbox.setStyleSheet(""" + QSpinBox { + color: white; + background: rgba(13,99,128,54); + border: 1px solid rgba(255,255,255,60); + border-radius: 8px; + padding: 4px 8px; + nohighlights; + } + QSpinBox::up-button { + width: 55px; + height: 22px; + } + QSpinBox::down-button { + width: 55px; + height: 22px; + } + """) + content_layout.addWidget(_make_row("VLAN ID", self.vlan_id_spinbox)) - @QtCore.pyqtSlot(str, name="delete-network") - def _delete_network(self, ssid: str) -> None: - """Delete a network.""" - try: - self._sdbus_network.delete_network(ssid=ssid) - except Exception as e: - logger.error("Failed to delete network %s: %s", ssid, e) - self._show_error_popup("Failed to delete network") + self.vlan_ip_field = IPAddressLineEdit( + parent=self.vlan_page, placeholder="192.168.1.100" + ) + content_layout.addWidget(_make_row("IP Address", self.vlan_ip_field)) - @QtCore.pyqtSlot(name="rescan-networks") - def _rescan_networks(self) -> None: - """Trigger network rescan.""" - self._sdbus_network.rescan_networks() + self.vlan_mask_field = IPAddressLineEdit( + parent=self.vlan_page, placeholder="255.255.255.0 or 24" + ) + content_layout.addWidget(_make_row("Subnet Mask", self.vlan_mask_field)) - @QtCore.pyqtSlot(name="add-network") - def _add_network(self) -> None: - """Add a new network.""" - self.add_network_validation_button.setEnabled(False) - self.add_network_validation_button.update() + self.vlan_gateway_field = IPAddressLineEdit( + parent=self.vlan_page, placeholder="192.168.1.1" + ) + content_layout.addWidget(_make_row("Gateway", self.vlan_gateway_field)) - password = self.add_network_password_field.text() - ssid = self.add_network_network_label.text() + self.vlan_dns1_field = IPAddressLineEdit( + parent=self.vlan_page, placeholder="8.8.8.8" + ) + content_layout.addWidget(_make_row("DNS 1", self.vlan_dns1_field)) - if not password and not self._current_network_is_open: - self._show_error_popup("Password field cannot be empty.") - self.add_network_validation_button.setEnabled(True) - return + self.vlan_dns2_field = IPAddressLineEdit( + parent=self.vlan_page, placeholder="8.8.4.4 (optional)" + ) + content_layout.addWidget(_make_row("DNS 2", self.vlan_dns2_field)) - result = self._sdbus_network.add_wifi_network(ssid=ssid, psk=password) - self.add_network_password_field.clear() + btn_layout = QtWidgets.QHBoxLayout() + btn_layout.addItem( + QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + btn_font = QtGui.QFont() + btn_font.setPointSize(16) + btn_font.setBold(True) - if result is None: - self._handle_failed_network_add("Failed to add network") - return + self.vlan_apply_button = BlocksCustomButton(parent=self.vlan_page) + self.vlan_apply_button.setMinimumSize(QtCore.QSize(180, 60)) + self.vlan_apply_button.setMaximumSize(QtCore.QSize(220, 60)) + self.vlan_apply_button.setFont(btn_font) + self.vlan_apply_button.setText("Apply") + self.vlan_apply_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/save.svg") + ) + btn_layout.addWidget( + self.vlan_apply_button, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) - error_msg = result.get("error", "") if isinstance(result, dict) else "" + self.vlan_delete_button = BlocksCustomButton(parent=self.vlan_page) + self.vlan_delete_button.setMinimumSize(QtCore.QSize(180, 60)) + self.vlan_delete_button.setMaximumSize(QtCore.QSize(220, 60)) + self.vlan_delete_button.setFont(btn_font) + self.vlan_delete_button.setText("Delete") + self.vlan_delete_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/garbage-icon.svg") + ) + btn_layout.addWidget( + self.vlan_delete_button, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) - if not error_msg: - self._handle_successful_network_add(ssid) - else: - self._handle_failed_network_add(error_msg) + content_layout.addLayout(btn_layout) + main_layout.addLayout(content_layout) + self.addWidget(self.vlan_page) - def _handle_successful_network_add(self, ssid: str) -> None: - """Handle successful network addition.""" - self._target_ssid = ssid - self._set_loading_state(True) - self.setCurrentIndex(self.indexOf(self.main_network_page)) + def _setup_wifi_static_ip_page(self) -> None: + """Construct the Wi-Fi static-IP settings page widgets and add it to the stacked widget.""" + self.wifi_static_ip_page = QtWidgets.QWidget() + main_layout = QtWidgets.QVBoxLayout(self.wifi_static_ip_page) - wifi_btn = self.wifi_button.toggle_button - hotspot_btn = self.hotspot_button.toggle_button - with QtCore.QSignalBlocker(wifi_btn), QtCore.QSignalBlocker(hotspot_btn): - wifi_btn.state = wifi_btn.State.ON - hotspot_btn.state = hotspot_btn.State.OFF + header_layout = QtWidgets.QHBoxLayout() + header_layout.addItem( + QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + self.wifi_sip_title = QtWidgets.QLabel( + "Static IP", parent=self.wifi_static_ip_page + ) + self.wifi_sip_title.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(20) + self.wifi_sip_title.setFont(font) + self.wifi_sip_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + header_layout.addWidget(self.wifi_sip_title) + + self.wifi_sip_back_button = IconButton(parent=self.wifi_static_ip_page) + self.wifi_sip_back_button.setMinimumSize(QtCore.QSize(60, 60)) + self.wifi_sip_back_button.setMaximumSize(QtCore.QSize(60, 60)) + self.wifi_sip_back_button.setFlat(True) + self.wifi_sip_back_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") + ) + self.wifi_sip_back_button.setProperty("button_type", "icon") + header_layout.addWidget(self.wifi_sip_back_button) + main_layout.addLayout(header_layout) + + content_layout = QtWidgets.QVBoxLayout() + content_layout.setContentsMargins(-1, 5, -1, 5) + + label_font = QtGui.QFont() + label_font.setPointSize(13) + label_font.setBold(True) + field_font = QtGui.QFont() + field_font.setPointSize(12) + field_min = QtCore.QSize(360, 45) + field_max = QtCore.QSize(500, 55) + + def _make_row(label_text, field): + """Build a labelled row widget containing *field* for the static-IP settings form.""" + frame = BlocksCustomFrame(parent=self.wifi_static_ip_page) + frame.setMinimumSize(QtCore.QSize(0, 50)) + frame.setMaximumSize(QtCore.QSize(16777215, 50)) + row = QtWidgets.QHBoxLayout(frame) + row.setContentsMargins(10, 2, 10, 2) + label = QtWidgets.QLabel(label_text, parent=frame) + label.setPalette(self._create_white_palette()) + label.setFont(label_font) + label.setMinimumWidth(120) + label.setMaximumWidth(160) + label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignVCenter + ) + row.addWidget(label) + field.setFont(field_font) + field.setMinimumSize(field_min) + field.setMaximumSize(field_max) + row.addWidget(field) + return frame + + self.wifi_sip_ip_field = IPAddressLineEdit( + parent=self.wifi_static_ip_page, placeholder="192.168.1.100" + ) + content_layout.addWidget(_make_row("IP Address", self.wifi_sip_ip_field)) + + self.wifi_sip_mask_field = IPAddressLineEdit( + parent=self.wifi_static_ip_page, placeholder="255.255.255.0 or 24" + ) + content_layout.addWidget(_make_row("Subnet Mask", self.wifi_sip_mask_field)) - self._schedule_delayed_action( - self._network_list_worker.build, NETWORK_CONNECT_DELAY_MS + self.wifi_sip_gateway_field = IPAddressLineEdit( + parent=self.wifi_static_ip_page, placeholder="192.168.1.1" ) + content_layout.addWidget(_make_row("Gateway", self.wifi_sip_gateway_field)) - def connect_and_refresh(): - try: - self._sdbus_network.connect_network(ssid) - except Exception as e: - logger.error("Failed to connect to %s: %s", ssid, e) - self._show_error_popup(f"Failed to connect to {ssid}") - self._set_loading_state(False) + self.wifi_sip_dns1_field = IPAddressLineEdit( + parent=self.wifi_static_ip_page, placeholder="8.8.8.8" + ) + content_layout.addWidget(_make_row("DNS 1", self.wifi_sip_dns1_field)) - QtCore.QTimer.singleShot(NETWORK_CONNECT_DELAY_MS, connect_and_refresh) + self.wifi_sip_dns2_field = IPAddressLineEdit( + parent=self.wifi_static_ip_page, placeholder="8.8.4.4 (optional)" + ) + content_layout.addWidget(_make_row("DNS 2", self.wifi_sip_dns2_field)) - self.add_network_validation_button.setEnabled(True) - self.wifi_button.setEnabled(False) - self.hotspot_button.setEnabled(False) - self.add_network_validation_button.update() + btn_layout = QtWidgets.QHBoxLayout() + btn_layout.setSpacing(10) + btn_font = QtGui.QFont() + btn_font.setPointSize(16) + btn_font.setBold(True) - def _handle_failed_network_add(self, error_msg: str) -> None: - """Handle failed network addition.""" - logging.error(error_msg) - error_messages = { - "Invalid password": "Invalid password. Please try again", - "Network connection properties error": ( - "Network connection properties error. Please try again" - ), - "Permission Denied": "Permission Denied. Please try again", - } + self.wifi_sip_apply_button = BlocksCustomButton(parent=self.wifi_static_ip_page) + self.wifi_sip_apply_button.setMinimumSize(QtCore.QSize(180, 80)) + self.wifi_sip_apply_button.setMaximumSize(QtCore.QSize(220, 80)) + self.wifi_sip_apply_button.setFont(btn_font) + self.wifi_sip_apply_button.setText("Apply") + self.wifi_sip_apply_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/save.svg") + ) + btn_layout.addWidget( + self.wifi_sip_apply_button, 0, QtCore.Qt.AlignmentFlag.AlignVCenter + ) - message = error_messages.get( - error_msg, "Error while adding network. Please try again" + self.wifi_sip_dhcp_button = BlocksCustomButton(parent=self.wifi_static_ip_page) + self.wifi_sip_dhcp_button.setMinimumSize(QtCore.QSize(180, 80)) + self.wifi_sip_dhcp_button.setMaximumSize(QtCore.QSize(220, 80)) + self.wifi_sip_dhcp_button.setFont(btn_font) + self.wifi_sip_dhcp_button.setText("Reset\nDHCP") + self.wifi_sip_dhcp_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/garbage-icon.svg") + ) + btn_layout.addWidget( + self.wifi_sip_dhcp_button, + 0, + QtCore.Qt.AlignmentFlag.AlignVCenter, ) - self.add_network_validation_button.setEnabled(True) - self.add_network_validation_button.update() - self._show_error_popup(message) + content_layout.addLayout(btn_layout) + main_layout.addLayout(content_layout) + self.addWidget(self.wifi_static_ip_page) - def _on_save_network_settings(self) -> None: - """Save network settings.""" - self._update_network( - ssid=self.saved_connection_network_name.text(), - password=self.saved_connection_change_password_field.text(), - new_ssid=None, + def _setup_navigation_signals(self) -> None: + """Connect all navigation-button clicked signals to their target page indexes.""" + self.wifi_button.clicked.connect(self._on_wifi_button_clicked) + self.hotspot_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.hotspot_page)) + ) + self.ethernet_button.clicked.connect(self._on_ethernet_button_clicked) + self.nl_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) ) + self.network_backButton.clicked.connect(self.hide) - def _update_network( - self, - ssid: str, - password: Optional[str], - new_ssid: Optional[str], - ) -> None: - """Update network settings.""" - if not self._sdbus_network.is_known(ssid): - return + self.add_network_page_backButton.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) + ) + self.saved_connection_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) + ) + self.network_details_btn.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.saved_details_page)) + ) + self.hotspot_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) + ) + self.hotspot_change_confirm.clicked.connect(self._on_hotspot_activate) - priority = self._get_selected_priority() + self.vlan_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) + ) + self.vlan_apply_button.clicked.connect(self._on_vlan_apply) + self.vlan_delete_button.clicked.connect(self._on_vlan_delete) - try: - self._sdbus_network.update_connection_settings( - ssid=ssid, password=password, new_ssid=new_ssid, priority=priority - ) - except Exception as e: - logger.error("Failed to update network settings: %s", e) - self._show_error_popup("Failed to update network settings") + self.wifi_static_ip_btn.clicked.connect(self._on_wifi_static_ip_clicked) + self.wifi_sip_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.saved_details_page)) + ) + self.wifi_sip_apply_button.clicked.connect(self._on_wifi_static_ip_apply) + self.wifi_sip_dhcp_button.clicked.connect(self._on_wifi_reset_dhcp) + def _on_wifi_button_clicked(self) -> None: + """Navigate to the Wi-Fi scan page, starting or stopping scan polling as needed.""" + if ( + self.wifi_button.toggle_button.state + == self.wifi_button.toggle_button.State.OFF + ): + self._show_warning_popup("Turn on Wi-Fi first.") + return self.setCurrentIndex(self.indexOf(self.network_list_page)) - def _get_selected_priority(self) -> int: - """Get selected priority from radio buttons.""" - checked_btn = self.priority_btn_group.checkedButton() - - if checked_btn == self.high_priority_btn: - return PRIORITY_HIGH - elif checked_btn == self.low_priority_btn: - return PRIORITY_LOW - else: - return PRIORITY_MEDIUM + def _setup_action_signals(self) -> None: + """Setup action signals.""" + self.add_network_validation_button.clicked.connect(self._add_network) + self.snd_back.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.saved_connection_page)) + ) + self.saved_details_save_btn.clicked.connect(self._on_save_network_details) + self.network_activate_btn.clicked.connect(self._on_activate_network) + self.network_delete_btn.clicked.connect(self._on_delete_network) - def _on_saved_wifi_option_selected(self) -> None: - """Handle saved wifi option selection.""" - sender = self.sender() + def _setup_toggle_signals(self) -> None: + """Setup toggle button signals.""" + self.wifi_button.toggle_button.stateChange.connect(self._on_toggle_state) + self.hotspot_button.toggle_button.stateChange.connect(self._on_toggle_state) + self.ethernet_button.toggle_button.stateChange.connect(self._on_toggle_state) - wifi_toggle = self.wifi_button.toggle_button - hotspot_toggle = self.hotspot_button.toggle_button + def _setup_password_visibility_signals(self) -> None: + """Setup password visibility toggle signals.""" + self._setup_password_visibility_toggle( + self.add_network_password_view, + self.add_network_password_field, + ) + self._setup_password_visibility_toggle( + self.saved_connection_change_password_view, + self.saved_connection_change_password_field, + ) - with QtCore.QSignalBlocker(wifi_toggle), QtCore.QSignalBlocker(hotspot_toggle): - wifi_toggle.state = wifi_toggle.State.ON - hotspot_toggle.state = hotspot_toggle.State.OFF + def _setup_password_visibility_toggle( + self, view_button: QtWidgets.QWidget, password_field: QtWidgets.QLineEdit + ) -> None: + """Setup password visibility toggle for a button/field pair.""" + view_button.setCheckable(True) - ssid = self.saved_connection_network_name.text() + see_icon = PixmapCache.get(":/ui/media/btn_icons/see.svg") + unsee_icon = PixmapCache.get(":/ui/media/btn_icons/unsee.svg") - if sender == self.network_delete_btn: - self._handle_network_delete(ssid) - elif sender == self.network_activate_btn: - self._handle_network_activate(ssid) + view_button.toggled.connect( + lambda checked: password_field.setHidden(not checked) + ) - def _handle_network_delete(self, ssid: str) -> None: - """Handle network deletion.""" - try: - self._sdbus_network.delete_network(ssid) - if ssid in self._networks: - del self._networks[ssid] - self.setCurrentIndex(self.indexOf(self.network_list_page)) - self._build_model_list() - self._network_list_worker.build() - self._show_info_popup(f"Network '{ssid}' deleted") - except Exception as e: - logger.error("Failed to delete network %s: %s", ssid, e) - self._show_error_popup("Failed to delete network") - - def _handle_network_activate(self, ssid: str) -> None: - """Handle network activation.""" - self._target_ssid = ssid - # Show loading IMMEDIATELY - self._set_loading_state(True) - self.repaint() + view_button.toggled.connect( + lambda checked: view_button.setPixmap( + unsee_icon if not checked else see_icon + ) + ) - self.setCurrentIndex(self.indexOf(self.main_network_page)) + def _setup_icons(self) -> None: + """Setup button icons.""" + self.hotspot_button.setPixmap( + PixmapCache.get(":/network/media/btn_icons/hotspot.svg") + ) + self.wifi_button.setPixmap( + PixmapCache.get(":/network/media/btn_icons/wifi_config.svg") + ) + self.ethernet_button.setPixmap( + PixmapCache.get(":/network/media/btn_icons/network/ethernet_connected.svg"), + ) + self.network_delete_btn.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/garbage-icon.svg") + ) + self.network_activate_btn.setProperty( + "icon_pixmap", PixmapCache.get(":/dialog/media/btn_icons/yes.svg") + ) + self.network_details_btn.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/printer_settings.svg") + ) - try: - self._sdbus_network.connect_network(ssid) - except Exception as e: - logger.error("Failed to connect to %s: %s", ssid, e) - self._set_loading_state(False) - self._show_disconnected_message() - self._show_error_popup("Failed to connect to network") - - @QtCore.pyqtSlot(list, name="finished-network-list-build") - def _handle_network_list(self, data: List[tuple]) -> None: - """Handle network list build completion.""" - self._networks.clear() - hotspot_ssid = self._sdbus_network.hotspot_ssid - - for entry in data: - # Handle different tuple lengths - if len(entry) >= 6: - ssid, signal, status, is_open, is_saved, is_hidden = entry - elif len(entry) >= 5: - ssid, signal, status, is_open, is_saved = entry - is_hidden = self._is_hidden_ssid(ssid) - elif len(entry) >= 4: - ssid, signal, status, is_open = entry - is_saved = status in ("Active", "Saved") - is_hidden = self._is_hidden_ssid(ssid) - else: - ssid, signal, status = entry[0], entry[1], entry[2] - is_open = status == "Open" - is_saved = status in ("Active", "Saved") - is_hidden = self._is_hidden_ssid(ssid) + def _setup_input_fields(self) -> None: + """Setup input field properties.""" + self.add_network_password_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) + self.hotspot_name_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) + self.hotspot_password_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) - if ssid == hotspot_ssid: - continue + self.hotspot_password_input_field.setPlaceholderText("Defaults to: 123456789") + self.hotspot_name_input_field.setText(str(self._nm.hotspot_ssid)) - self._networks[ssid] = NetworkInfo( - signal=signal, - status=status, - is_open=is_open, - is_saved=is_saved, - is_hidden=is_hidden, - ) + self.hotspot_password_input_field.setText(str(self._nm.hotspot_password)) - self._build_model_list() + def _setup_keyboard(self) -> None: + """Setup the on-screen keyboard.""" + self._qwerty = CustomQwertyKeyboard(self) + self.addWidget(self._qwerty) + self._qwerty.value_selected.connect(self._on_qwerty_value_selected) + self._qwerty.request_back.connect(self._on_qwerty_go_back) - # Update main panel if connected - if self._last_displayed_ssid and self._last_displayed_ssid in self._networks: - network_info = self._networks[self._last_displayed_ssid] - self.netlist_strength.setText( - f"{network_info.signal}%" if network_info.signal != -1 else "--" + self.add_network_password_field.clicked.connect( + lambda: self._on_show_keyboard( + self.add_network_page, self.add_network_password_field ) - - def _is_hidden_ssid(self, ssid: str) -> bool: - """Check if an SSID indicates a hidden network.""" - if ssid is None: - return True - ssid_stripped = ssid.strip() - ssid_lower = ssid_stripped.lower() - # Check for empty, unknown, or hidden indicators - return ( - ssid_stripped == "" - or ssid_lower == "unknown" - or ssid_lower == "" - or ssid_lower == "hidden" - or not ssid_stripped - ) - - def _build_model_list(self) -> None: - """Build the network list model.""" - self.listView.blockSignals(True) - self._reset_view_model() - - saved_networks = [] - unsaved_networks = [] - - for ssid, info in self._networks.items(): - if info.is_saved: - saved_networks.append((ssid, info)) - else: - unsaved_networks.append((ssid, info)) - - saved_networks.sort(key=lambda x: -x[1].signal) - unsaved_networks.sort(key=lambda x: -x[1].signal) - - for ssid, info in saved_networks: - self._add_network_entry( - ssid=ssid, - signal=info.signal, - status=info.status, - is_open=info.is_open, - is_hidden=info.is_hidden, + ) + self.hotspot_password_input_field.clicked.connect( + lambda: self._on_show_keyboard( + self.hotspot_page, self.hotspot_password_input_field ) - - if saved_networks and unsaved_networks: - self._add_separator_entry() - - for ssid, info in unsaved_networks: - self._add_network_entry( - ssid=ssid, - signal=info.signal, - status=info.status, - is_open=info.is_open, - is_hidden=info.is_hidden, + ) + self.hotspot_name_input_field.clicked.connect( + lambda: self._on_show_keyboard( + self.hotspot_page, self.hotspot_name_input_field ) - - # Add "Connect to Hidden Network" entry at the end - self._add_hidden_network_entry() - - self._sync_scrollbar() - self.listView.blockSignals(False) - self.listView.update() - - def _reset_view_model(self) -> None: - """Reset the view model.""" - self._model.clear() - self._entry_delegate.clear() - - def _add_separator_entry(self) -> None: - """Add a separator entry to the list.""" - item = ListItem( - text="", - left_icon=None, - right_text="", - right_icon=None, - selected=False, - allow_check=False, - _lfontsize=17, - _rfontsize=12, - height=20, - not_clickable=True, ) - self._model.add_item(item) - - def _add_hidden_network_entry(self) -> None: - """Add a 'Connect to Hidden Network' entry at the end of the list.""" - wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/0bar_wifi_protected.svg") - item = ListItem( - text="Connect to Hidden Network...", - left_icon=wifi_pixmap, - right_text="", - right_icon=self._right_arrow_icon, - selected=False, - allow_check=False, - _lfontsize=17, - _rfontsize=12, - height=80, - not_clickable=False, + self.saved_connection_change_password_field.clicked.connect( + lambda: self._on_show_keyboard( + self.saved_details_page, + self.saved_connection_change_password_field, + ) ) - self._model.add_item(item) - - def _add_network_entry( - self, - ssid: str, - signal: int, - status: str, - is_open: bool = False, - is_hidden: bool = False, - ) -> None: - """Add a network entry to the list.""" - wifi_pixmap = self._icon_provider.get_pixmap(signal=signal, status=status) - # Skipping hidden networks - # Check both the is_hidden flag AND the ssid content - if is_hidden or self._is_hidden_ssid(ssid): - return - display_ssid = ssid + for field, page in [ + (self.vlan_ip_field, self.vlan_page), + (self.vlan_mask_field, self.vlan_page), + (self.vlan_gateway_field, self.vlan_page), + (self.vlan_dns1_field, self.vlan_page), + (self.vlan_dns2_field, self.vlan_page), + (self.wifi_sip_ip_field, self.wifi_static_ip_page), + (self.wifi_sip_mask_field, self.wifi_static_ip_page), + (self.wifi_sip_gateway_field, self.wifi_static_ip_page), + (self.wifi_sip_dns1_field, self.wifi_static_ip_page), + (self.wifi_sip_dns2_field, self.wifi_static_ip_page), + ]: + field.clicked.connect( + lambda _=False, f=field, p=page: self._on_show_keyboard(p, f) + ) - item = ListItem( - text=display_ssid, - left_icon=wifi_pixmap, - right_text=status, - right_icon=self._right_arrow_icon, - selected=False, - allow_check=False, - _lfontsize=17, - _rfontsize=12, - height=80, - not_clickable=False, # All entries are clickable + def _setup_scrollbar_signals(self) -> None: + """Setup scrollbar synchronization signals.""" + self.listView.verticalScrollBar().valueChanged.connect( + self._handle_scrollbar_change ) - self._model.add_item(item) - - @QtCore.pyqtSlot(ListItem, name="ssid-item-clicked") - def _on_ssid_item_clicked(self, item: ListItem) -> None: - """Handle network item click.""" - ssid = item.text - - # Handle hidden network entries - check for various hidden indicators - if ( - self._is_hidden_ssid(ssid) - or ssid == "Hidden Network" - or ssid == "Connect to Hidden Network..." - ): - self.setCurrentIndex(self.indexOf(self.hidden_network_page)) - return - - network_info = self._networks.get(ssid) - if network_info is None: - # Also check if it might be a hidden network in the _networks dict - # Hidden networks might have empty or UNKNOWN as key - for key, info in self._networks.items(): - if info.is_hidden: - self.setCurrentIndex(self.indexOf(self.hidden_network_page)) - return - return + self.verticalScrollBar.valueChanged.connect(self._handle_scrollbar_change) + self.verticalScrollBar.valueChanged.connect( + lambda value: self.listView.verticalScrollBar().setValue(value) + ) + self.verticalScrollBar.show() - if network_info.is_saved: - saved_networks = self._sdbus_network.get_saved_networks_with_for() - self._show_saved_network_page(ssid, saved_networks) - else: - self._show_add_network_page(ssid, is_open=network_info.is_open) + def _configure_list_view_palette(self) -> None: + """Configure the list view palette for transparency.""" + palette = QtGui.QPalette() - def _show_saved_network_page(self, ssid: str, saved_networks: List[Dict]) -> None: - """Show the saved network page.""" - self.saved_connection_network_name.setText(str(ssid)) - self.snd_name.setText(str(ssid)) - self._current_network_ssid = ssid # Track for priority lookup + for group in [ + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorGroup.Inactive, + QtGui.QPalette.ColorGroup.Disabled, + ]: + transparent = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + transparent.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(group, QtGui.QPalette.ColorRole.Button, transparent) + palette.setBrush(group, QtGui.QPalette.ColorRole.Window, transparent) - # Fetch priority from get_saved_networks() which includes priority - # get_saved_networks_with_for() does NOT include priority field - priority = None - try: - full_saved_networks = self._sdbus_network.get_saved_networks() - if full_saved_networks: - for net in full_saved_networks: - if net.get("ssid") == ssid: - priority = net.get("priority") - logger.debug("Found priority %s for network %s", priority, ssid) - break - except Exception as e: - logger.error("Failed to get priority for %s: %s", ssid, e) - - self._set_priority_button(priority) - - network_info = self._networks.get(ssid) - if network_info: - signal_text = ( - f"{network_info.signal}%" if network_info.signal >= 0 else "--%" - ) - self.saved_connection_signal_strength_info_frame.setText(signal_text) + no_brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + no_brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) + palette.setBrush(group, QtGui.QPalette.ColorRole.Base, no_brush) - if network_info.is_open: - self.saved_connection_security_type_info_label.setText("OPEN") - else: - sec_type = self._sdbus_network.get_security_type_by_ssid(ssid) - self.saved_connection_security_type_info_label.setText( - str(sec_type or "WPA").upper() - ) - else: - self.saved_connection_signal_strength_info_frame.setText("--%") - self.saved_connection_security_type_info_label.setText("--") + highlight = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) + highlight.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(group, QtGui.QPalette.ColorRole.Highlight, highlight) - current_ssid = self._sdbus_network.get_current_ssid() - if current_ssid != ssid: - self.network_activate_btn.setDisabled(False) - self.sn_info.setText("Saved Network") - else: - self.network_activate_btn.setDisabled(True) - self.sn_info.setText("Active Network") + link = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) + link.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(group, QtGui.QPalette.ColorRole.Link, link) - self.setCurrentIndex(self.indexOf(self.saved_connection_page)) - self.frame.repaint() + self.listView.setPalette(palette) - def _set_priority_button(self, priority: Optional[int]) -> None: - """Set the priority button based on value. + def _on_show_keyboard( + self, panel: QtWidgets.QWidget, field: QtWidgets.QLineEdit + ) -> None: + """Show the QWERTY keyboard panel, saving the originating panel and input field.""" + self._previous_panel = panel + self._current_field = field + self._qwerty.set_value(field.text()) + self.setCurrentIndex(self.indexOf(self._qwerty)) - Block signals while setting to prevent unwanted triggers. - """ - # Block signals to prevent any side effects - with ( - QtCore.QSignalBlocker(self.high_priority_btn), - QtCore.QSignalBlocker(self.med_priority_btn), - QtCore.QSignalBlocker(self.low_priority_btn), - ): - # Uncheck all first - self.high_priority_btn.setChecked(False) - self.med_priority_btn.setChecked(False) - self.low_priority_btn.setChecked(False) - - # Then check the correct one - if priority is not None: - if priority >= PRIORITY_HIGH: - self.high_priority_btn.setChecked(True) - elif priority <= PRIORITY_LOW: - self.low_priority_btn.setChecked(True) - else: - self.med_priority_btn.setChecked(True) - else: - # Default to medium if no priority set - self.med_priority_btn.setChecked(True) + def _on_qwerty_go_back(self) -> None: + """Hide the keyboard and return to the previously active panel.""" + if self._previous_panel: + self.setCurrentIndex(self.indexOf(self._previous_panel)) - def _show_add_network_page(self, ssid: str, is_open: bool = False) -> None: - """Show the add network page.""" - self._current_network_is_open = is_open - self._current_network_is_hidden = False - self.add_network_network_label.setText(str(ssid)) - self.setCurrentIndex(self.indexOf(self.add_network_page)) + def _on_qwerty_value_selected(self, value: str) -> None: + """Apply the keyboard-selected *value* to the previously focused input field.""" + if self._previous_panel: + self.setCurrentIndex(self.indexOf(self._previous_panel)) + if self._current_field: + self._current_field.setText(value) def _handle_scrollbar_change(self, value: int) -> None: - """Handle scrollbar value change.""" + """Synchronise the custom scrollbar thumb to the list-view scroll position.""" self.verticalScrollBar.blockSignals(True) self.verticalScrollBar.setValue(value) self.verticalScrollBar.blockSignals(False) def _sync_scrollbar(self) -> None: - """Synchronize scrollbar with list view.""" + """Push the current list-view scroll position into the custom scrollbar.""" list_scrollbar = self.listView.verticalScrollBar() self.verticalScrollBar.setMinimum(list_scrollbar.minimum()) self.verticalScrollBar.setMaximum(list_scrollbar.maximum()) self.verticalScrollBar.setPageStep(list_scrollbar.pageStep()) - def _schedule_delayed_action(self, callback: Callable, delay_ms: int) -> None: - """Schedule a delayed action.""" - try: - self._delayed_action_timer.timeout.disconnect() - except TypeError: - pass - - self._delayed_action_timer.timeout.connect(callback) - self._delayed_action_timer.start(delay_ms) - - def close(self) -> bool: - """Close the window.""" - self._network_list_worker.stop_polling() - self._sdbus_network.close() - return super().close() - def setCurrentIndex(self, index: int) -> None: """Set the current page index.""" if not self.isVisible(): @@ -3191,7 +3802,7 @@ def setCurrentIndex(self, index: int) -> None: elif index == self.indexOf(self.saved_connection_page): self._setup_saved_connection_page_state() - self.repaint() + self.update() super().setCurrentIndex(index) def _setup_add_network_page_state(self) -> None: @@ -3215,9 +3826,9 @@ def _setup_saved_connection_page_state(self) -> None: "Change network password" ) - def setProperty(self, name: str, value: Any) -> bool: + def setProperty(self, name: str, value: object) -> bool: """Set a property value.""" - if name == "backgroundPixmap": + if name == "wifi_button_pixmap": self._background = value return super().setProperty(name, value) @@ -3233,3 +3844,4 @@ def show_network_panel(self) -> None: self.updateGeometry() self.repaint() self.show() + self._nm.scan_networks() diff --git a/BlocksScreen/lib/panels/printTab.py b/BlocksScreen/lib/panels/printTab.py index 7431938e..65927aae 100644 --- a/BlocksScreen/lib/panels/printTab.py +++ b/BlocksScreen/lib/panels/printTab.py @@ -20,7 +20,7 @@ from lib.utils.display_button import DisplayButton from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger(name="logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class PrintTab(QtWidgets.QStackedWidget): @@ -64,6 +64,7 @@ class PrintTab(QtWidgets.QStackedWidget): ) call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") + call_cancel_panel = QtCore.pyqtSignal(bool, name="call-load-panel") _z_offset: float = 0.0 _active_z_offset: float = 0.0 _finish_print_handled: bool = False @@ -124,11 +125,27 @@ def __init__( self.file_data.request_file_list ) self.file_data.on_dirs.connect(self.filesPage_widget.on_directories) + self.filesPage_widget.request_dir_info[str].connect( self.file_data.request_dir_info[str] ) + self.filesPage_widget.request_scan_metadata.connect( + self.ws.api.scan_gcode_metadata + ) + self.file_data.metadata_error.connect(self.filesPage_widget.on_metadata_error) self.filesPage_widget.request_dir_info.connect(self.file_data.request_dir_info) self.file_data.on_file_list.connect(self.filesPage_widget.on_file_list) + self.file_data.file_added.connect(self.filesPage_widget.on_file_added) + self.file_data.file_removed.connect(self.filesPage_widget.on_file_removed) + self.file_data.file_modified.connect(self.filesPage_widget.on_file_modified) + self.file_data.dir_added.connect(self.filesPage_widget.on_dir_added) + self.file_data.dir_removed.connect(self.filesPage_widget.on_dir_removed) + self.file_data.full_refresh_needed.connect( + self.filesPage_widget.on_full_refresh_needed + ) + self.file_data.usb_files_loaded.connect( + self.filesPage_widget.on_usb_files_loaded + ) self.jobStatusPage_widget = JobStatusWidget(self) self.addWidget(self.jobStatusPage_widget) self.confirmPage_widget.on_accept.connect( @@ -137,6 +154,7 @@ def __init__( self.jobStatusPage_widget.show_request.connect( lambda: self.change_page(self.indexOf(self.jobStatusPage_widget)) ) + self.jobStatusPage_widget.call_cancel_panel.connect(self.call_cancel_panel) self.jobStatusPage_widget.hide_request.connect( lambda: self.change_page(self.indexOf(self.print_page)) ) diff --git a/BlocksScreen/lib/panels/widgets/bannerPopup.py b/BlocksScreen/lib/panels/widgets/bannerPopup.py new file mode 100644 index 00000000..7db54547 --- /dev/null +++ b/BlocksScreen/lib/panels/widgets/bannerPopup.py @@ -0,0 +1,222 @@ +import enum +from collections import deque + +from lib.utils.icon_button import IconButton +from PyQt6 import QtCore, QtGui, QtWidgets + + +class BannerPopup(QtWidgets.QWidget): + class MessageType(enum.Enum): + """Popup Message type (level)""" + + CONNECT = enum.auto() + DISCONNECT = enum.auto() + CORRUPTED = enum.auto() + UNKNOWN = enum.auto() + + def __init__(self, parent=None) -> None: + if parent: + super().__init__(parent) + else: + super().__init__() + self.timeout_timer = QtCore.QTimer(self) + self.timeout_timer.setSingleShot(True) + self.messages: deque = deque() + self.isShown = False + self.default_background_color = QtGui.QColor(164, 164, 164) + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True) + self.setMouseTracking(True) + self.setWindowFlags( + QtCore.Qt.WindowType.FramelessWindowHint + | QtCore.Qt.WindowType.Tool + | QtCore.Qt.WindowType.X11BypassWindowManagerHint + ) + self._setupUI() + self.slide_in_animation = QtCore.QPropertyAnimation(self, b"geometry") + self.slide_in_animation.setDuration(1000) + self.slide_in_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutCubic) + self.slide_out_animation = QtCore.QPropertyAnimation(self, b"geometry") + self.slide_out_animation.setDuration(200) + self.slide_out_animation.setEasingCurve(QtCore.QEasingCurve.Type.InCubic) + self.oneshot = QtCore.QTimer(self) + self.oneshot.setInterval(5000) + self.oneshot.setSingleShot(True) + self.oneshot.timeout.connect(self._add_popup) + self.timeout_timer.setInterval(4000) + self.slide_out_animation.finished.connect(self.on_slide_out_finished) + self.slide_in_animation.finished.connect(self.on_slide_in_finished) + self.timeout_timer.timeout.connect(lambda: self.slide_out_animation.start()) + self.actionbtn.clicked.connect(self.slide_out_animation.start) + + def event(self, a0): + if a0.type() in (QtCore.QEvent.Type.MouseButtonPress,): + if self.rect().contains(a0.position().toPoint()): + self.timeout_timer.stop() + self.slide_out_animation.setStartValue( + self.slide_in_animation.currentValue() + ) + self.slide_in_animation.stop() + self.slide_out_animation.start() + + return super().event(a0) + + def on_slide_in_finished(self): + """Handle slide in animation finished""" + self.timeout_timer.start() + + def on_slide_out_finished(self): + """Handle slide out animation finished""" + self.hide() + self.isShown = False + self.timeout_timer.stop() + self._add_popup() + + def _calculate_target_geometry(self) -> QtCore.QRect: + """Calculate on end posisition rect for popup""" + app_instance = QtWidgets.QApplication.instance() + main_window = app_instance.activeWindow() if app_instance else None + if main_window is None and app_instance: + for widget in app_instance.allWidgets(): + if isinstance(widget, QtWidgets.QMainWindow): + main_window = widget + break + parent_rect = main_window.geometry() + width = int(parent_rect.width() * 0.35) + height = 80 + x = parent_rect.x() + parent_rect.width() - width + 50 + y = parent_rect.y() + 30 + return QtCore.QRect(x, y, width, height) + + def updateMask(self) -> None: + """Update widget mask properties""" + path = QtGui.QPainterPath() + path.addRoundedRect(self.rect().toRectF(), 50, 70) + region = QtGui.QRegion(path.toFillPolygon(QtGui.QTransform()).toPolygon()) + self.setMask(region) + + def mousePressEvent(self, a0: QtGui.QMouseEvent | None) -> None: + """Re-implemented method, handle mouse press events""" + return + + def new_message( + self, + message_type: MessageType = MessageType.CONNECT, + ): + """Create new popup message + + Args: + message_type (MessageType, optional): Message Level, See `MessageType` Types. Defaults to MessageType.CONNECT . + Returns: + _type_: _description_ + """ + if len(self.messages) == 4: + return + + self.messages.append( + { + "type": message_type, + } + ) + return self._add_popup() + + def _add_popup(self) -> None: + """Add popup to queue""" + if self.isShown: + if self.oneshot.isActive(): + return + self.oneshot.start() + return + if ( + self.messages + and self.slide_in_animation.state() + == QtCore.QPropertyAnimation.State.Stopped + and self.slide_out_animation.state() + == QtCore.QPropertyAnimation.State.Stopped + ): + message_entry = self.messages.popleft() + message_type = message_entry.get("type") + + message = "Unknown Event" + icon = ":ui/media/btn_icons/info.svg" + + # TODO: missing usb icons + match message_type: + case BannerPopup.MessageType.CONNECT: + message = "Usb Connected" + # icon = "" + case BannerPopup.MessageType.DISCONNECT: + message = "Usb Disconnected" + # icon = "" + case BannerPopup.MessageType.CORRUPTED: + message = "Usb Corrupted" + icon = ":/ui/media/btn_icons/troubleshoot.svg" + end_rect = self._calculate_target_geometry() + start_rect = end_rect.translated(end_rect.width() * 2, 0) + + self.icon_label.setPixmap(QtGui.QPixmap(icon)) + + self.slide_in_animation.setStartValue(start_rect) + self.slide_in_animation.setEndValue(end_rect) + self.slide_out_animation.setStartValue(end_rect) + self.slide_out_animation.setEndValue(start_rect) + self.setGeometry(end_rect) + self.text_label.setText(message) + self.show() + + def showEvent(self, a0: QtGui.QShowEvent | None) -> None: + """Re-implementation, widget show""" + self.slide_in_animation.start() + self.isShown = True + super().showEvent(a0) + + def resizeEvent(self, a0: QtGui.QResizeEvent) -> None: + """Re-implementation, handle resize event""" + self.updateMask() + super().resizeEvent(a0) + + def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" + painter = QtGui.QPainter(self) + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) + + _base_color = self.default_background_color + + center_point = QtCore.QPointF(self.rect().center()) + gradient = QtGui.QRadialGradient(center_point, self.rect().width() / 2.0) + + gradient.setColorAt(0, _base_color.darker(160)) + gradient.setColorAt(1.0, _base_color.darker(200)) + + painter.setBrush(gradient) + painter.setPen(QtCore.Qt.PenStyle.NoPen) + painter.drawRoundedRect(self.rect(), 50, 70) + + def _setupUI(self) -> None: + self.horizontal_layout = QtWidgets.QHBoxLayout(self) + self.horizontal_layout.setContentsMargins(5, 5, 5, 5) + + self.icon_label = QtWidgets.QLabel(self) + self.icon_label.setFixedSize(QtCore.QSize(60, 60)) + self.icon_label.setMaximumSize(QtCore.QSize(60, 60)) + self.icon_label.setScaledContents(True) + + self.text_label = QtWidgets.QLabel(self) + self.text_label.setStyleSheet("background: transparent; color:white") + self.text_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.text_label.setWordWrap(True) + font = self.text_label.font() + font.setPixelSize(18) + font.setFamily("sans-serif") + palette = self.text_label.palette() + palette.setColor( + QtGui.QPalette.ColorRole.WindowText, QtCore.Qt.GlobalColor.white + ) + self.text_label.setPalette(palette) + self.text_label.setFont(font) + + self.actionbtn = IconButton(self) + self.actionbtn.setMaximumSize(QtCore.QSize(60, 60)) + + self.horizontal_layout.addWidget(self.icon_label) + self.horizontal_layout.addWidget(self.text_label) + self.horizontal_layout.addWidget(self.actionbtn) diff --git a/BlocksScreen/lib/panels/widgets/cancelPage.py b/BlocksScreen/lib/panels/widgets/cancelPage.py new file mode 100644 index 00000000..e16fb2e6 --- /dev/null +++ b/BlocksScreen/lib/panels/widgets/cancelPage.py @@ -0,0 +1,267 @@ +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 + + +class CancelPage(QtWidgets.QWidget): + """Update GUI Page, + retrieves from moonraker available clients and adds functionality + for updating or recovering them + """ + + request_file_info: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="request_file_info" + ) + reprint_start: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="reprint_start" + ) + run_gcode: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="run_gcode" + ) + + def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket) -> None: + super().__init__(parent) + self.ws: MoonWebSocket = ws + self._setupUI() + self.filename = "" + + self.reprint_start.connect(self.ws.api.start_print) + + 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) + + def _handle_accept(self): + self.run_gcode.emit("SDCARD_RESET_FILE") + self.reprint_start.emit(self.filename) + self.close() + + def _handle_refuse(self): + self.close() + self.run_gcode.emit("SDCARD_RESET_FILE") + + @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: + if isinstance(value, str): + if "filename" in field: + self.filename = value + if self.isVisible: + self.set_file_name(value) + + def show(self): + self.request_file_info.emit(self.filename) + + super().show() + + def set_pixmap(self, pixmap: QtGui.QPixmap) -> None: + if not hasattr(self, "_scene"): + self._scene = QtWidgets.QGraphicsScene(self) + self.cf_thumbnail.setScene(self._scene) + + # Scene rectangle (available display area) + graphics_rect = self.cf_thumbnail.rect().toRectF() + + # Scale pixmap preserving aspect ratio + pixmap = pixmap.scaled( + graphics_rect.size().toSize(), + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, + ) + + adjusted_x = (graphics_rect.width() - pixmap.width()) / 2.0 + adjusted_y = (graphics_rect.height() - pixmap.height()) / 2.0 + + if not hasattr(self, "_pixmap_item"): + self._pixmap_item = QtWidgets.QGraphicsPixmapItem(pixmap) + self._scene.addItem(self._pixmap_item) + else: + self._pixmap_item.setPixmap(pixmap) + + self._pixmap_item.setPos(adjusted_x, adjusted_y) + self._scene.setSceneRect(graphics_rect) + + 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"] + + 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) + + def _setupUI(self) -> None: + """Setup widget ui""" + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + ) + sizePolicy.setHorizontalStretch(1) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) + self.setSizePolicy(sizePolicy) + self.setObjectName("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) + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self) + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.cf_header_title = QtWidgets.QHBoxLayout() + self.cf_header_title.setObjectName("cf_header_title") + + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.cf_file_name = BlocksLabel(parent=self) + self.cf_file_name.setMinimumSize(QtCore.QSize(0, 60)) + self.cf_file_name.setMaximumSize(QtCore.QSize(16777215, 60)) + font = QtGui.QFont() + font.setFamily("Momcake") + font.setPointSize(24) + self.cf_file_name.setFont(font) + self.cf_file_name.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) + self.cf_file_name.setSizePolicy(sizePolicy) + self.cf_file_name.setStyleSheet("background: transparent; color: white;") + self.cf_file_name.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.cf_file_name.setObjectName("cf_file_name") + self.cf_header_title.addWidget(self.cf_file_name) + + self.verticalLayout_4.addLayout(self.cf_header_title) + self.cf_content_vertical_layout = QtWidgets.QHBoxLayout() + self.cf_content_vertical_layout.setObjectName("cf_content_vertical_layout") + self.cf_content_horizontal_layout = QtWidgets.QVBoxLayout() + self.cf_content_horizontal_layout.setObjectName("cf_content_horizontal_layout") + self.info_frame = BlocksCustomFrame(parent=self) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.info_frame.setSizePolicy(sizePolicy) + + self.info_frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + + self.info_layout = QtWidgets.QVBoxLayout(self.info_frame) + + self.cf_info_tf = QtWidgets.QLabel(parent=self.info_frame) + self.cf_info_tf.setText("Print job was\ncancelled") + font = QtGui.QFont() + font.setFamily("Momcake") + font.setPointSize(20) + + self.cf_info_tf.setFont(font) + self.cf_info_tf.setStyleSheet("background: transparent; color: white;") + + self.cf_info_tf.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.info_layout.addWidget(self.cf_info_tf) + + self.cf_info_tr = QtWidgets.QLabel(parent=self.info_frame) + font = QtGui.QFont() + font.setPointSize(15) + self.cf_info_tr.setFont(font) + self.cf_info_tr.setStyleSheet("background: transparent; color: white;") + self.cf_info_tr.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.cf_info_tr.setText("Do you want to reprint?") + self.info_layout.addWidget(self.cf_info_tr) + + self.cf_confirm_layout = QtWidgets.QVBoxLayout() + self.cf_confirm_layout.setSpacing(15) + + self.confirm_button = BlocksCustomButton(parent=self.info_frame) + self.confirm_button.setMinimumSize(QtCore.QSize(250, 70)) + self.confirm_button.setMaximumSize(QtCore.QSize(250, 70)) + font = QtGui.QFont("Momcake", 18) + self.confirm_button.setFont(font) + self.confirm_button.setFlat(True) + self.confirm_button.setProperty( + "icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") + ) + self.confirm_button.setText("Reprint") + # 2. Align buttons to the right + self.cf_confirm_layout.addWidget( + self.confirm_button, 0, QtCore.Qt.AlignmentFlag.AlignCenter + ) + + self.refuse_button = BlocksCustomButton(parent=self.info_frame) + self.refuse_button.setMinimumSize(QtCore.QSize(250, 70)) + self.refuse_button.setMaximumSize(QtCore.QSize(250, 70)) + self.refuse_button.setFont(font) + self.refuse_button.setFlat(True) + self.refuse_button.setProperty( + "icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/no.svg") + ) + self.refuse_button.setText("Ignore") + # 2. Align buttons to the right + self.cf_confirm_layout.addWidget( + self.refuse_button, 0, QtCore.Qt.AlignmentFlag.AlignCenter + ) + + self.info_layout.addLayout(self.cf_confirm_layout) + + self.cf_content_horizontal_layout.addWidget(self.info_frame) + + self.cf_content_vertical_layout.addLayout(self.cf_content_horizontal_layout) + self.cf_thumbnail = QtWidgets.QGraphicsView(self) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + sizePolicy.setHorizontalStretch(1) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.cf_thumbnail.sizePolicy().hasHeightForWidth()) + self.cf_thumbnail.setSizePolicy(sizePolicy) + self.cf_thumbnail.setMinimumSize(QtCore.QSize(400, 300)) + self.cf_thumbnail.setMaximumSize(QtCore.QSize(400, 300)) + self.cf_thumbnail.setStyleSheet( + "QGraphicsView{\nbackground-color: transparent;\n}" + ) + self.cf_thumbnail.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.cf_thumbnail.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) + self.cf_thumbnail.setVerticalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.cf_thumbnail.setHorizontalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.cf_thumbnail.setSizeAdjustPolicy( + QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustIgnored + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) + self.cf_thumbnail.setBackgroundBrush(brush) + self.cf_thumbnail.setRenderHints( + QtGui.QPainter.RenderHint.Antialiasing + | QtGui.QPainter.RenderHint.SmoothPixmapTransform + | QtGui.QPainter.RenderHint.TextAntialiasing + ) + self.cf_thumbnail.setViewportUpdateMode( + QtWidgets.QGraphicsView.ViewportUpdateMode.SmartViewportUpdate + ) + self.cf_thumbnail.setObjectName("cf_thumbnail") + self.cf_content_vertical_layout.addWidget( + self.cf_thumbnail, 0, QtCore.Qt.AlignmentFlag.AlignCenter + ) + self.verticalLayout_4.addLayout(self.cf_content_vertical_layout) diff --git a/BlocksScreen/lib/panels/widgets/confirmPage.py b/BlocksScreen/lib/panels/widgets/confirmPage.py index 12432449..0f35ba39 100644 --- a/BlocksScreen/lib/panels/widgets/confirmPage.py +++ b/BlocksScreen/lib/panels/widgets/confirmPage.py @@ -25,7 +25,7 @@ def __init__(self, parent) -> None: self._setupUI() self.setMouseTracking(True) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) - self.thumbnail: QtGui.QImage = QtGui.QImage() + self.thumbnail: QtGui.QImage = self._blocksthumbnail self._thumbnails: typing.List = [] self.directory = "gcodes" self.filename = "" @@ -42,17 +42,19 @@ def __init__(self, parent) -> None: @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 directory = os.path.dirname(text) filename = os.path.basename(text) self.directory = directory self.filename = filename self.cf_file_name.setText(self.filename) - if not filedata: - return 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: + self.thumbnail = self._blocksthumbnail _total_filament = filedata.get("filament_weight_total") _estimated_time = filedata.get("estimated_time") if isinstance(_estimated_time, str): @@ -114,12 +116,6 @@ def paintEvent(self, event: QtGui.QPaintEvent) -> None: self._scene = QtWidgets.QGraphicsScene(self) self.cf_thumbnail.setScene(self._scene) - # Pick thumbnail or fallback logo - if self.thumbnail.isNull(): - self.thumbnail = QtGui.QImage( - "BlocksScreen/lib/ui/resources/media/logoblocks400x300.png" - ) - # Scene rectangle (available display area) graphics_rect = self.cf_thumbnail.rect().toRectF() @@ -323,3 +319,6 @@ def _setupUI(self) -> None: QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter, ) self.verticalLayout_4.addLayout(self.cf_content_vertical_layout) + self._blocksthumbnail = QtGui.QImage( + "BlocksScreen/lib/ui/resources/media/logoblocks400x300.png" + ) diff --git a/BlocksScreen/lib/panels/widgets/connectionPage.py b/BlocksScreen/lib/panels/widgets/connectionPage.py index 9403d290..3cd1fc21 100644 --- a/BlocksScreen/lib/panels/widgets/connectionPage.py +++ b/BlocksScreen/lib/panels/widgets/connectionPage.py @@ -5,6 +5,8 @@ from lib.ui.connectionWindow_ui import Ui_ConnectivityForm from PyQt6 import QtCore, QtWidgets +logger = logging.getLogger(__name__) + class ConnectionPage(QtWidgets.QFrame): text_updated = QtCore.pyqtSignal(int, name="connection_text_updated") @@ -14,7 +16,9 @@ class ConnectionPage(QtWidgets.QFrame): restart_klipper_clicked = QtCore.pyqtSignal(name="restart_klipper_clicked") firmware_restart_clicked = QtCore.pyqtSignal(name="firmware_restart_clicked") update_button_clicked = QtCore.pyqtSignal(bool, name="show-update-page") + notification_btn_clicked = QtCore.pyqtSignal(name="notification_btn_clicked") call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") + call_cancel_panel = QtCore.pyqtSignal(bool, name="call-load-panel") def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket, /): super().__init__(parent) @@ -33,6 +37,7 @@ def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket, /): self.state = "shutdown" self.dot_count = 0 self.message = None + self.conn_toggle: bool = True self.dot_timer = QtCore.QTimer(self) self.dot_timer.setInterval(1000) self.dot_timer.timeout.connect(self._add_dot) @@ -43,6 +48,7 @@ def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket, /): self.retry_connection_clicked.emit ) self.panel.wifi_button.clicked.connect(self.wifi_button_clicked.emit) + self.panel.notification_btn.clicked.connect(self.notification_btn_clicked.emit) self.panel.FirmwareRestartButton.clicked.connect( self.firmware_restart_clicked.emit ) @@ -54,6 +60,11 @@ def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket, /): self.ws.klippy_connected_signal.connect(self.on_klippy_connected) self.ws.klippy_state_signal.connect(self.on_klippy_state) + @QtCore.pyqtSlot(bool, name="toggle_connection_page") + def set_toggle(self, toggle: bool): + """Toggle connection page showing or not""" + self.conn_toggle = toggle + def show_panel(self, reason: str | None = None): """Show widget""" self.show() @@ -65,9 +76,11 @@ def show_panel(self, reason: str | None = None): def showEvent(self, a0: QtCore.QEvent | None): """Handle show event""" - self.ws.api.refresh_update_status() - self.call_load_panel.emit(False, "") - return super().showEvent(a0) + if self.conn_toggle: + self.ws.api.refresh_update_status() + self.call_load_panel.emit(False, "") + self.call_cancel_panel.emit(False) + return super().showEvent(a0) @QtCore.pyqtSlot(bool, name="on_klippy_connected") def on_klippy_connection(self, connected: bool): @@ -130,7 +143,7 @@ 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() - logging.debug(f"[ConnectionWindowPanel] text_update: {text}") + logger.debug(f"[ConnectionWindowPanel] text_update: {text}") if text == "wb lost": self.panel.connectionTextBox.setText("Moonraker connection lost") if text is None: diff --git a/BlocksScreen/lib/panels/widgets/filesPage.py b/BlocksScreen/lib/panels/widgets/filesPage.py index f8fa490f..969399ac 100644 --- a/BlocksScreen/lib/panels/widgets/filesPage.py +++ b/BlocksScreen/lib/panels/widgets/filesPage.py @@ -1,411 +1,1121 @@ +import json import logging -import os import typing import helper_methods from lib.utils.blocks_Scrollbar import CustomScrollBar from lib.utils.icon_button import IconButton -from PyQt6 import QtCore, QtGui, QtWidgets - from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem +from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger("logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class FilesPage(QtWidgets.QWidget): - request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - name="request-back" - ) - file_selected: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, dict, name="file-selected" - ) - request_file_info: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, name="request-file-info" + # Signals + request_back = QtCore.pyqtSignal(name="request_back") + file_selected = QtCore.pyqtSignal(str, dict, name="file_selected") + request_file_info = QtCore.pyqtSignal(str, name="request_file_info") + request_dir_info = QtCore.pyqtSignal( + [], [str], [str, bool], name="api_get_dir_info" ) - request_dir_info: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - [], [str], [str, bool], name="api-get-dir-info" - ) - request_file_list: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - [], [str], name="api-get-files-list" - ) - request_file_metadata: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, name="api-get-gcode-metadata" - ) - file_list: list = [] - files_data: dict = {} - directories: list = [] - - def __init__(self, parent) -> None: - super().__init__() - self.model = EntryListModel() - self.entry_delegate = EntryDelegate() - self._setupUI() + request_file_list = QtCore.pyqtSignal([], [str], name="api_get_files_list") + request_file_metadata = QtCore.pyqtSignal(str, name="api_get_gcode_metadata") + request_scan_metadata = QtCore.pyqtSignal(str, name="api_scan_gcode_metadata") + + # Constants + GCODE_EXTENSION = ".gcode" + USB_PREFIX = "USB-" + ITEM_HEIGHT = 80 + LEFT_FONT_SIZE = 17 + RIGHT_FONT_SIZE = 12 + + # Icon paths + 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", + "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: + super().__init__(parent) + + self._file_list: list[dict] = [] + self._files_data: dict[str, dict] = {} # filename -> metadata dict + self._directories: list[dict] = [] + self._curr_dir: str = "" + self._pending_action: bool = False + self._pending_metadata_requests: set[str] = set() # Track pending requests + self._metadata_retry_count: dict[ + str, int + ] = {} # Track retry count per file (max 3) + self._icons: dict[str, QtGui.QPixmap] = {} + + self._model = EntryListModel() + self._entry_delegate = EntryDelegate() + + self._model.rowsInserted.connect(self._delayed_scrollbar_update) + self._model.rowsRemoved.connect(self._delayed_scrollbar_update) + self._model.modelReset.connect(self._delayed_scrollbar_update) + + self._setup_ui() + self._load_icons() + self._connect_signals() + self.setMouseTracking(True) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) - self.curr_dir: str = "" - self.ReloadButton.clicked.connect( - lambda: self.request_dir_info[str].emit(self.curr_dir) - ) - self.listWidget.verticalScrollBar().valueChanged.connect(self._handle_scrollbar) - self.scrollbar.valueChanged.connect(self._handle_scrollbar) - self.scrollbar.valueChanged.connect( - lambda value: self.listWidget.verticalScrollBar().setValue(value) - ) - self.back_btn.clicked.connect(self.reset_dir) - self.entry_delegate.item_selected.connect(self._on_item_selected) - self._refresh_one_and_half_sec_timer = QtCore.QTimer() - self._refresh_one_and_half_sec_timer.timeout.connect( - lambda: self.request_dir_info[str].emit(self.curr_dir) - ) - self._refresh_one_and_half_sec_timer.start(1500) + @property + def current_directory(self) -> str: + """Get current directory path.""" + return self._curr_dir - @QtCore.pyqtSlot(ListItem, name="on-item-selected") - def _on_item_selected(self, item: ListItem) -> None: - """Slot called when a list item is selected in the UI. - This method is connected to the `item_selected` signal of the entry delegate. - It handles the selection of a `ListItem` and process it accoding it with its type + @current_directory.setter + def current_directory(self, value: str) -> None: + """Set current directory path.""" + self._curr_dir = value + + def reload_gcodes_folder(self) -> None: + """Request reload of the gcodes folder from root.""" + logger.debug("Reloading gcodes folder") + self.request_dir_info[str].emit("") + + def clear_files_data(self) -> None: + """Clear all cached file data.""" + self._files_data.clear() + self._pending_metadata_requests.clear() + self._metadata_retry_count.clear() + + def retry_metadata_request(self, file_path: str) -> bool: + """ + Request metadata with a maximum of 3 retries per file. Args: - item : ListItem The item that was selected by the user. + file_path: Path to the file + Returns: + True if request was made, False if max retries reached """ - if not item.left_icon: - filename = self.curr_dir + "/" + item.text + ".gcode" - self._fileItemClicked(filename) - else: - if item.text == "Go Back": - go_back_path = os.path.dirname(self.curr_dir) - if go_back_path == "/": - go_back_path = "" - self._on_goback_dir(go_back_path) - else: - self._dirItemClicked("/" + item.text) + clean_path = file_path.removeprefix("/") - @QtCore.pyqtSlot(name="reset-dir") - def reset_dir(self) -> None: - """Reset current directory""" - self.curr_dir = "" - self.request_dir_info[str].emit(self.curr_dir) + if not clean_path.lower().endswith(self.GCODE_EXTENSION): + return False - def showEvent(self, a0: QtGui.QShowEvent) -> None: - """Re-implemented method, handle widget show event""" - self._build_file_list() - return super().showEvent(a0) + current_count = self._metadata_retry_count.get(clean_path, 0) - @QtCore.pyqtSlot(list, name="on-file-list") + if current_count > 3: + # Maximum 3 force scan per file + logger.debug(f"Metadata retry limit reached for: {clean_path}") + return False + + self._metadata_retry_count[clean_path] = current_count + 1 + + if current_count == 0: + # First attempt: regular metadata request + self.request_file_metadata.emit(clean_path) + logger.debug(f"Metadata request (attempt 1) for: {clean_path}") + else: + # Subsequent attempts: force scan + self.request_scan_metadata.emit(clean_path) + logger.debug( + f"Metadata scan (attempt {current_count + 1}) for: {clean_path}" + ) + + return True + + @QtCore.pyqtSlot(list, name="on_file_list") def on_file_list(self, file_list: list) -> None: - """Handle receiving files list from websocket""" - self.files_data.clear() - self.file_list = file_list + """Handle receiving full files list.""" + self._file_list = file_list.copy() if file_list else [] + logger.debug(f"Received file list with {len(self._file_list)} files") - @QtCore.pyqtSlot(list, name="on-dirs") + @QtCore.pyqtSlot(list, name="on_dirs") def on_directories(self, directories_data: list) -> None: - """Handle receiving available directories from websocket""" - self.directories = directories_data + """Handle receiving full directories list.""" + self._directories = directories_data.copy() if directories_data else [] + logger.debug(f"Received {len(self._directories)} directories") + if self.isVisible(): self._build_file_list() - @QtCore.pyqtSlot(dict, name="on-fileinfo") + @QtCore.pyqtSlot(dict, name="on_fileinfo") def on_fileinfo(self, filedata: dict) -> None: - """Method called per file to contruct file entry to the list""" + """ + Handle receiving file metadata. + + Updates existing file entry in the list with better info (time, filament). + """ if not filedata or not self.isVisible(): return + filename = filedata.get("filename", "") + if not filename or not filename.lower().endswith(self.GCODE_EXTENSION): + return + + # Cache the file data + self._files_data[filename] = filedata + + # Remove from pending requests and reset retry count (success) + self._pending_metadata_requests.discard(filename) + self._metadata_retry_count.pop(filename, None) + + # Check if this file should be displayed in current view + file_dir = self._get_parent_directory(filename) + current = self._curr_dir.removeprefix("/") + + # Both empty = root directory, otherwise must match exactly + if file_dir != current: + return + + display_name = self._get_display_name(filename) + item = self._create_file_list_item(filedata) + if not item: + return + + self._model.remove_item_by_text(display_name) + + # Find correct position (sorted by modification time, newest first) + insert_position = self._find_file_insert_position(filedata.get("modified", 0)) + self._model.insert_item(insert_position, item) + + logger.debug(f"Updated file in list: {display_name}") + + @QtCore.pyqtSlot(str, name="on_metadata_error") + def on_metadata_error(self, filename: str) -> None: + """ + Handle metadata request failure. + + Triggers retry with scan_gcode_metadata if under retry limit. + Called when metadata request fails. + """ if not filename: return - self.files_data.update({f"{filename}": filedata}) - estimated_time = filedata.get("estimated_time", 0) - seconds = int(estimated_time) if isinstance(estimated_time, (int, float)) else 0 - filament_type = ( - filedata.get("filament_type", "Unknown filament") - if filedata.get("filament_type", "Unknown filament") != -1.0 - else "Unknown filament" - ) - time_str = "" - days, hours, minutes, _ = helper_methods.estimate_print_time(seconds) - if seconds <= 0: - time_str = "??" - elif seconds < 60: - time_str = "less than 1 minute" - else: - if days > 0: - time_str = f"{days}d {hours}h {minutes}m" - elif hours > 0: - time_str = f"{hours}h {minutes}m" - else: - time_str = f"{minutes}m" - name = helper_methods.get_file_name(filename) + clean_filename = filename.removeprefix("/") + + if clean_filename not in self._pending_metadata_requests: + return + + # Try again (will use force scan to the max of 3 times) + if not self.retry_metadata_request(clean_filename): + # Max retries reached, remove from pending + self._pending_metadata_requests.discard(clean_filename) + logger.debug(f"Giving up on metadata for: {clean_filename}") + + @QtCore.pyqtSlot(str, list, name="on_usb_files_loaded") + def on_usb_files_loaded(self, usb_path: str, files: list) -> None: + """ + Handle preloaded USB files. + + Called when USB files are preloaded in background. + If we're currently viewing this USB, update the display. + + Args: + usb_path: The USB mount path + files: List of file dicts from the USB + """ + current = self._curr_dir.removeprefix("/") + + # If we're currently in this USB folder, update the file list + if current == usb_path: + self._file_list = files.copy() + if self.isVisible(): + self._build_file_list() + logger.debug(f"Updated view with preloaded USB files: {usb_path}") + + def _find_file_insert_position(self, modified_time: float) -> int: + """ + Find the correct position to insert a new file. + + Files should be: + 1. After all directories + 2. Sorted by modification time (newest first) + + Returns: + The index at which to insert the new file. + """ + insert_pos = 0 + + for i in range(self._model.rowCount()): + index = self._model.index(i) + item = self._model.data(index, QtCore.Qt.ItemDataRole.UserRole) + + if not item: + continue + + # Skip directories (items with left_icon) + if item.left_icon: + insert_pos = i + 1 + continue + + # This is a file - check its modification time + file_key = self._find_file_key_by_display_name(item.text) + if file_key: + file_data = self._files_data.get(file_key, {}) + file_modified = file_data.get("modified", 0) + + # Files are sorted newest first, so insert before older files + if modified_time > file_modified: + return i + + insert_pos = i + 1 + + return insert_pos + + def _find_file_key_by_display_name(self, display_name: str) -> typing.Optional[str]: + """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: + return key + return None + + @QtCore.pyqtSlot(dict, name="on_file_added") + def on_file_added(self, file_data: dict) -> None: + """ + Handle a single file being added. + + Called when Moonraker sends notify_filelist_changed with create_file action. + Adds file to list immediately, metadata updates later. + """ + path = file_data.get("path", file_data.get("filename", "")) + if not path: + return + + # Normalize paths + path = path.removeprefix("/") + file_dir = self._get_parent_directory(path) + current = self._curr_dir.removeprefix("/") + + # Check if file belongs to current directory + if file_dir != current: + logger.debug( + f"File '{path}' (dir: '{file_dir}') not in current directory ('{current}'), skipping" + ) + return + + # Only update UI if visible + if not self.isVisible(): + logger.debug("Widget not visible, will refresh on show") + return + + display_name = self._get_display_name(path) + + if not self._model_contains_item(display_name): + # Create basic item with unknown info + modified = file_data.get("modified", 0) + + item = ListItem( + text=display_name, + right_text="Unknown Filament - Unknown time", + right_icon=self._icons.get("right_arrow"), + left_icon=None, + callback=None, + selected=False, + allow_check=False, + _lfontsize=self.LEFT_FONT_SIZE, + _rfontsize=self.RIGHT_FONT_SIZE, + height=self.ITEM_HEIGHT, + notificate=False, + ) + + # Find correct position + insert_position = self._find_file_insert_position(modified) + self._model.insert_item(insert_position, item) + self._hide_placeholder() + logger.debug(f"Added new file to list: {display_name}") + + # Request metadata for gcode files using retry mechanism + if path.lower().endswith(self.GCODE_EXTENSION): + if path not in self._pending_metadata_requests: + self._pending_metadata_requests.add(path) + self.retry_metadata_request(path) + logger.debug(f"Requested metadata for new file: {path}") + + @QtCore.pyqtSlot(str, name="on_file_removed") + def on_file_removed(self, filepath: str) -> None: + """ + Handle a file being removed. + + Called when Moonraker sends notify_filelist_changed with delete_file action. + """ + filepath = filepath.removeprefix("/") + file_dir = self._get_parent_directory(filepath) + current = self._curr_dir.removeprefix("/") + + # Always clean up cache + self._files_data.pop(filepath, None) + self._pending_metadata_requests.discard(filepath) + self._metadata_retry_count.pop(filepath, None) + + # Only update UI if visible and in current directory + if not self.isVisible(): + return + + if file_dir != current: + logger.debug( + f"Deleted file '{filepath}' not in current directory ('{current}'), skipping UI update" + ) + return + + filename = self._get_basename(filepath) + display_name = self._get_display_name(filename) + + # Remove from model + removed = self._model.remove_item_by_text(display_name) + + if removed: + self._check_empty_state() + logger.debug(f"File removed from view: {filepath}") + + @QtCore.pyqtSlot(dict, name="on_file_modified") + def on_file_modified(self, file_data: dict) -> None: + """ + Handle a file being modified. + + Called when Moonraker sends notify_filelist_changed with modify_file action. + """ + path = file_data.get("path", file_data.get("filename", "")) + if path: + # Remove old entry and request fresh metadata + self.on_file_removed(path) + self.on_file_added(file_data) + + @QtCore.pyqtSlot(dict, name="on_dir_added") + def on_dir_added(self, dir_data: dict) -> None: + """ + Handle a directory being added. + + Called when Moonraker sends notify_filelist_changed with create_dir action. + Inserts the directory in the correct sorted position (alphabetically, after Go Back). + """ + # Extract dirname from path or dirname field + path = dir_data.get("path", "") + dirname = dir_data.get("dirname", "") + + if not dirname and path: + dirname = self._get_basename(path) + + if not dirname or dirname.startswith("."): + return + + path = path.removeprefix("/") + parent_dir = self._get_parent_directory(path) if path else "" + current = self._curr_dir.removeprefix("/") + + if parent_dir != current: + logger.debug( + f"Directory '{dirname}' (parent: '{parent_dir}') not in current directory ('{current}'), skipping" + ) + return + + if not self.isVisible(): + logger.debug( + f"Widget not visible, skipping UI update for added dir: {dirname}" + ) + return + + # Check if already exists + if self._model_contains_item(dirname): + return + + # Ensure dirname is in dir_data + dir_data["dirname"] = dirname + + # Find the correct sorted position for this directory + insert_position = self._find_directory_insert_position(dirname) + + # Create the list item + icon = self._icons.get("folder") + if self._is_usb_directory(self._curr_dir, dirname): + icon = self._icons.get("usb") + item = ListItem( - text=name[:-6], - right_text=f"{filament_type} - {time_str}", - right_icon=self.path.get("right_arrow"), - left_icon=None, - callback=None, + text=str(dirname), + left_icon=icon, + right_text="", + right_icon=None, selected=False, + callback=None, allow_check=False, - _lfontsize=17, - _rfontsize=12, - height=80, - notificate=False, + _lfontsize=self.LEFT_FONT_SIZE, + _rfontsize=self.RIGHT_FONT_SIZE, + height=self.ITEM_HEIGHT, ) - self.model.add_item(item) + # Insert at the correct position + self._model.insert_item(insert_position, item) - @QtCore.pyqtSlot(str, name="file-item-clicked") - def _fileItemClicked(self, filename: str) -> None: - """Slot for List Item clicked + self._hide_placeholder() + logger.debug( + f"Directory added to view at position {insert_position}: {dirname}" + ) - Args: - filename (str): Clicked item path + def _find_directory_insert_position(self, new_dirname: str) -> int: """ - self.file_selected.emit( - str(filename.removeprefix("/")), - self.files_data.get(filename.removeprefix("/")), + Find the correct position to insert a new directory. + + Directories should be: + 1. After "Go Back" (if present) + 2. Before all files + 3. Sorted alphabetically among other directories + + Returns: + The index at which to insert the new directory. + """ + new_dirname_lower = new_dirname.lower() + insert_pos = 0 + + for i in range(self._model.rowCount()): + index = self._model.index(i) + item = self._model.data(index, QtCore.Qt.ItemDataRole.UserRole) + + if not item: + continue + + # Skip "Go Back" - always stays at top + if item.text == "Go Back": + insert_pos = i + 1 + continue + + # If this item has a left_icon, it's a directory + if item.left_icon: + # Compare alphabetically + if item.text.lower() > new_dirname_lower: + # Found a directory that should come after the new one + return i + else: + # This directory comes before, keep looking + insert_pos = i + 1 + else: + # Hit a file - insert before it (directories come before files) + return i + + # Insert at the end of directories (or end of list if no files) + return insert_pos + + @QtCore.pyqtSlot(str, name="on_dir_removed") + def on_dir_removed(self, dirname_or_path: str) -> None: + """ + Handle a directory being removed. + + Called when Moonraker sends notify_filelist_changed with delete_dir action. + Also handles USB mount removal (which Moonraker reports as delete_file). + """ + dirname_or_path = dirname_or_path.removeprefix("/") + dirname = ( + self._get_basename(dirname_or_path) + if "/" in dirname_or_path + else dirname_or_path ) - def _dirItemClicked(self, directory: str) -> None: - """Method that changes the current view in the list""" - self.curr_dir = self.curr_dir + directory - self.request_dir_info[str].emit(self.curr_dir) + if not dirname: + return + + # Check if user is currently inside the removed directory (e.g., USB removed) + current = self._curr_dir.removeprefix("/") + if current == dirname or current.startswith(dirname + "/"): + logger.warning( + f"Current directory '{current}' was removed, returning to root" + ) + self.on_directory_error() + return + + # Skip UI update if not visible + if not self.isVisible(): + return + + removed = self._model.remove_item_by_text(dirname) + + if removed: + self._check_empty_state() + logger.debug(f"Directory removed from view: {dirname}") + + @QtCore.pyqtSlot(name="on_full_refresh_needed") + def on_full_refresh_needed(self) -> None: + """ + Handle full refresh request. + + Called when Moonraker sends root_update or when major changes occur. + """ + logger.info("Full refresh requested") + self._curr_dir = "" + self.request_dir_info[str].emit(self._curr_dir) + + @QtCore.pyqtSlot(name="on_directory_error") + def on_directory_error(self) -> None: + """ + Handle Directory Error. + + Immediately navigates back to root gcodes folder. + Call this from MainWindow when detecting USB removal or directory errors. + """ + logger.info("Directory Error - returning to root directory") + + # Reset to root directory + self._curr_dir = "" + + # Clear any pending actions + self._pending_action = False + self._pending_metadata_requests.clear() + + # Request fresh data for root directory + self.request_dir_info[str].emit("") + + @QtCore.pyqtSlot(ListItem, name="on_item_selected") + def _on_item_selected(self, item: ListItem) -> None: + """Handle list item selection.""" + if not item.left_icon: + # File selected (files don't have left icon) + filename = self._build_filepath(item.text + self.GCODE_EXTENSION) + self._on_file_item_clicked(filename) + elif item.text == "Go Back": + # Go back selected + go_back_path = self._get_parent_directory(self._curr_dir) + if go_back_path == "/": + go_back_path = "" + self._on_go_back_dir(go_back_path) + else: + # Directory selected + self._on_dir_item_clicked("/" + item.text) + + @QtCore.pyqtSlot(name="reset_dir") + def reset_dir(self) -> None: + """Reset to root directory.""" + self._curr_dir = "" + self.request_dir_info[str].emit(self._curr_dir) + + def showEvent(self, event: QtGui.QShowEvent) -> None: + """Handle widget becoming visible.""" + # Request fresh data when becoming visible + self.request_dir_info[str].emit(self._curr_dir) + super().showEvent(event) + + def hideEvent(self, event: QtGui.QHideEvent) -> None: + """Handle widget being hidden.""" + # Clear pending requests when hidden + self._pending_metadata_requests.clear() + super().hideEvent(event) def _build_file_list(self) -> None: - """Inserts the currently available gcode files on the QListWidget""" - self.listWidget.blockSignals(True) - self.model.clear() - self.entry_delegate.clear() - if ( - not self.file_list - and not self.directories - and os.path.islink(self.curr_dir) - ): - self._add_placeholder() + """Build the complete file list display.""" + self._list_widget.blockSignals(True) + self._model.clear() + self._entry_delegate.clear() + self._pending_action = False + self._pending_metadata_requests.clear() + self._metadata_retry_count.clear() + + # Determine if we're in root directory + is_root = not self._curr_dir or self._curr_dir == "/" + + # Check for empty state in root directory + if not self._file_list and not self._directories and is_root: + self._show_placeholder() + self._list_widget.blockSignals(False) return - if self.directories or self.curr_dir != "": - if self.curr_dir != "" and self.curr_dir != "/": - self._add_back_folder_entry() - for dir_data in self.directories: - if dir_data.get("dirname").startswith("."): - continue + # We have content (or we're in a subdirectory), hide placeholder + self._hide_placeholder() + + # Add back button if not in root + if not is_root: + self._add_back_folder_entry() + + # Add directories (sorted alphabetically) + sorted_dirs = sorted( + self._directories, key=lambda x: x.get("dirname", "").lower() + ) + for dir_data in sorted_dirs: + dirname = dir_data.get("dirname", "") + if dirname and not dirname.startswith("."): self._add_directory_list_item(dir_data) - sorted_list = sorted(self.file_list, key=lambda x: x["modified"], reverse=True) - for item in sorted_list: - self._add_file_list_item(item) - self._setup_scrollbar() - self.listWidget.blockSignals(False) - self.listWidget.update() + # Add files immediately (sorted by modification time, newest first) + sorted_files = sorted( + self._file_list, key=lambda x: x.get("modified", 0), reverse=True + ) + for file_item in sorted_files: + filename = file_item.get("filename", file_item.get("path", "")) + if not filename: + continue + + # Add file to list immediately with basic info + self._add_file_to_list(file_item) + + # Request metadata for gcode files (will update display later) + if filename.lower().endswith(self.GCODE_EXTENSION): + self._request_file_info(file_item) + + self._list_widget.blockSignals(False) + self._list_widget.update() + + def _delayed_scrollbar_update(self) -> None: + """Update scrollbar after model changes.""" + QtCore.QTimer.singleShot(10, self._setup_scrollbar) + + def _add_file_to_list(self, file_item: dict) -> None: + """Add a file entry to the list with basic info.""" + filename = file_item.get("filename", file_item.get("path", "")) + if not filename or not filename.lower().endswith(self.GCODE_EXTENSION): + return + + # Get display name + display_name = self._get_display_name(filename) + + # Check if already in list + if self._model_contains_item(display_name): + return + + # Use cached metadata if available, otherwise show unknown + full_path = self._build_filepath(filename) + cached = self._files_data.get(full_path) + + if cached: + item = self._create_file_list_item(cached) + else: + item = ListItem( + text=display_name, + right_text="Unknown Filament - Unknown time", + right_icon=self._icons.get("right_arrow"), + left_icon=None, + callback=None, + selected=False, + allow_check=False, + _lfontsize=self.LEFT_FONT_SIZE, + _rfontsize=self.RIGHT_FONT_SIZE, + height=self.ITEM_HEIGHT, + notificate=False, + ) + + if item: + self._model.add_item(item) + + def _create_file_list_item(self, filedata: dict) -> typing.Optional[ListItem]: + """Create a ListItem from file metadata.""" + filename = filedata.get("filename", "") + if not filename: + return None + + # Format estimated time + estimated_time = filedata.get("estimated_time", 0) + seconds = int(estimated_time) if isinstance(estimated_time, (int, float)) else 0 + time_str = self._format_print_time(seconds) + + # Get filament type + filament_type = filedata.get("filament_type") + if isinstance(filament_type, str): + text = filament_type.strip() + if text.startswith("[") and text.endswith("]"): + try: + types = json.loads(text) + except json.JSONDecodeError: + types = [text] + else: + types = [text] + else: + types = filament_type or [] + + if not isinstance(types, list): + types = [types] + + filament_type = ",".join(dict.fromkeys(types)) + + if not filament_type or filament_type == -1.0 or filament_type == "Unknown": + filament_type = "Unknown Filament" + + display_name = self._get_display_name(filename) + + return ListItem( + text=display_name, + right_text=f"{filament_type} - {time_str}", + right_icon=self._icons.get("right_arrow"), + left_icon=None, # Files have no left icon + callback=None, + selected=False, + allow_check=False, + _lfontsize=self.LEFT_FONT_SIZE, + _rfontsize=self.RIGHT_FONT_SIZE, + height=self.ITEM_HEIGHT, + notificate=False, + ) def _add_directory_list_item(self, dir_data: dict) -> None: - """Method that adds directories to the list""" + """Add a directory entry to the list.""" dir_name = dir_data.get("dirname", "") if not dir_name: return + + # Choose appropriate icon + icon = self._icons.get("folder") + if self._is_usb_directory(self._curr_dir, dir_name): + icon = self._icons.get("usb") + item = ListItem( text=str(dir_name), - left_icon=self.path.get("folderIcon"), + left_icon=icon, right_text="", + right_icon=None, selected=False, callback=None, allow_check=False, - _lfontsize=17, - _rfontsize=12, - height=80, + _lfontsize=self.LEFT_FONT_SIZE, + _rfontsize=self.RIGHT_FONT_SIZE, + height=self.ITEM_HEIGHT, ) - self.model.add_item(item) + self._model.add_item(item) def _add_back_folder_entry(self) -> None: - """Method to insert in the list the "Go back" item""" - go_back_path = os.path.dirname(self.curr_dir) - if go_back_path == "/": - go_back_path = "" - + """Add the 'Go Back' navigation entry.""" item = ListItem( text="Go Back", right_text="", right_icon=None, - left_icon=self.path.get("back_folder"), + left_icon=self._icons.get("back_folder"), callback=None, selected=False, allow_check=False, - _lfontsize=17, - _rfontsize=12, - height=80, + _lfontsize=self.LEFT_FONT_SIZE, + _rfontsize=self.RIGHT_FONT_SIZE, + height=self.ITEM_HEIGHT, notificate=False, ) - self.model.add_item(item) + self._model.add_item(item) - @QtCore.pyqtSlot(str, str, name="on-goback-dir") - def _on_goback_dir(self, directory) -> None: - """Go back behaviour""" - self.request_dir_info[str].emit(directory) - self.curr_dir = directory - - def _add_file_list_item(self, file_data_item) -> None: - """Request file information and metadata to create filelist""" + def _request_file_info(self, file_data_item: dict) -> None: + """Request metadata for a file item using retry mechanism.""" if not file_data_item: return - name = ( - file_data_item["path"] - if "path" in file_data_item.keys() - else file_data_item["filename"] - ) - if not name.endswith(".gcode"): + name = file_data_item.get("path", file_data_item.get("filename", "")) + if not name or not name.lower().endswith(self.GCODE_EXTENSION): + return + + # Build full path + file_path = self._build_filepath(name) + + # Track pending request + self._pending_metadata_requests.add(file_path) + + # Use retry mechanism (first attempt uses get_gcode_metadata) + self.retry_metadata_request(file_path) + + def _on_file_item_clicked(self, filename: str) -> None: + """Handle file item click.""" + clean_filename = filename.removeprefix("/") + file_data = self._files_data.get(clean_filename, {}) + self.file_selected.emit(clean_filename, file_data) + + def _on_dir_item_clicked(self, directory: str) -> None: + """Handle directory item click.""" + if self._pending_action: return - file_path = ( - name if not self.curr_dir else str(self.curr_dir + "/" + name) - ).removeprefix("/") - self.request_file_metadata.emit(file_path.removeprefix("/")) - self.request_file_info.emit(file_path.removeprefix("/")) + self._curr_dir = self._curr_dir + directory + self.request_dir_info[str].emit(self._curr_dir) + self._pending_action = True + + def _on_go_back_dir(self, directory: str) -> None: + """Handle go back navigation.""" + self.request_dir_info[str].emit(directory) + self._curr_dir = directory + + def _show_placeholder(self) -> None: + """Show the 'No Files found' placeholder.""" + self._scrollbar.hide() + self._list_widget.hide() + self._label.show() + + def _hide_placeholder(self) -> None: + """Hide the placeholder and show the list.""" + self._label.hide() + self._list_widget.show() + + def _check_empty_state(self) -> None: + """Check if list is empty and show placeholder if needed.""" + is_root = not self._curr_dir or self._curr_dir == "/" - def _add_placeholder(self) -> None: - """Shows placeholder when no items exist""" - self.scrollbar.hide() - self.listWidget.hide() - self.label.show() + if self._model.rowCount() == 0 and is_root: + self._show_placeholder() + elif self._model.rowCount() == 0 and not is_root: + # In subdirectory with no files - just show "Go Back" + self._add_back_folder_entry() - def _handle_scrollbar(self, value): - """Updates scrollbar value""" - self.scrollbar.blockSignals(True) - self.scrollbar.setValue(value) - self.scrollbar.blockSignals(False) + def _model_contains_item(self, text: str) -> bool: + """Check if model contains an item with the given text.""" + for i in range(self._model.rowCount()): + index = self._model.index(i) + item = self._model.data(index, QtCore.Qt.ItemDataRole.UserRole) + if item and item.text == text: + return True + return False + + def _handle_scrollbar_value_changed(self, value: int) -> None: + """Sync scrollbar with list widget.""" + self._scrollbar.blockSignals(True) + self._scrollbar.setValue(value) + self._scrollbar.blockSignals(False) def _setup_scrollbar(self) -> None: - """Syncs the scrollbar with the list size""" - self.scrollbar.setMinimum(self.listWidget.verticalScrollBar().minimum()) - self.scrollbar.setMaximum(self.listWidget.verticalScrollBar().maximum()) - self.scrollbar.setPageStep(self.listWidget.verticalScrollBar().pageStep()) - self.scrollbar.show() - - def _setupUI(self): - sizePolicy = QtWidgets.QSizePolicy( + """Configure scrollbar to match list size.""" + list_scrollbar = self._list_widget.verticalScrollBar() + self._scrollbar.setMinimum(list_scrollbar.minimum()) + self._scrollbar.setMaximum(list_scrollbar.maximum()) + self._scrollbar.setPageStep(list_scrollbar.pageStep()) + + if list_scrollbar.maximum() > 0: + self._scrollbar.show() + else: + self._scrollbar.hide() + + @staticmethod + def _get_basename(path: str) -> str: + """ + Get the basename of a path without using os.path. + Works with paths from Moonraker (forward slashes only). + """ + if not path: + return "" + # Remove trailing slashes and get last component + path = path.rstrip("/") + if "/" in path: + return path.rsplit("/", 1)[-1] + return path + + @staticmethod + def _get_parent_directory(path: str) -> str: + """ + Get the parent directory of a path without using os.path. + Works with paths from Moonraker (forward slashes only). + """ + if not path: + return "" + path = path.removeprefix("/").rstrip("/") + if "/" in path: + return path.rsplit("/", 1)[0] + return "" + + def _build_filepath(self, filename: str) -> str: + """Build full file path from current directory and filename.""" + filename = filename.removeprefix("/") + if self._curr_dir: + curr = self._curr_dir.removeprefix("/") + return f"{curr}/{filename}" + return filename + + @staticmethod + def _is_usb_directory(parent_dir: str, directory_name: str) -> bool: + """Check if directory is a USB mount in the root.""" + return parent_dir == "" and directory_name.startswith("USB-") + + @staticmethod + def _format_print_time(seconds: int) -> str: + """Format print time in human-readable form.""" + if seconds <= 0: + return "Unknown time" + if seconds < 60: + return f"{seconds}s" + + days, hours, minutes, _ = helper_methods.estimate_print_time(seconds) + + if days > 0: + return f"{days}d {hours}h {minutes}m" + elif hours > 0: + return f"{hours}h {minutes}m" + else: + return f"{minutes}m" + + def _get_display_name(self, filename: str) -> str: + """Get display name from filename (without path and extension).""" + basename = self._get_basename(filename) + name = helper_methods.get_file_name(basename) + + # Remove .gcode extension + if name.lower().endswith(self.GCODE_EXTENSION): + name = name[:-6] + + return name + + def _load_icons(self) -> None: + """Load all icons into cache.""" + self._icons = { + "back_folder": QtGui.QPixmap(self.ICON_PATHS["back_folder"]), + "folder": QtGui.QPixmap(self.ICON_PATHS["folder"]), + "right_arrow": QtGui.QPixmap(self.ICON_PATHS["right_arrow"]), + "usb": QtGui.QPixmap(self.ICON_PATHS["usb"]), + } + + def _connect_signals(self) -> None: + """Connect internal signals.""" + # Button connections + self._reload_button.clicked.connect( + lambda: self.request_dir_info[str].emit(self._curr_dir) + ) + self.back_btn.clicked.connect(self.reset_dir) + + # List widget connections + self._list_widget.verticalScrollBar().valueChanged.connect( + self._handle_scrollbar_value_changed + ) + self._scrollbar.valueChanged.connect(self._handle_scrollbar_value_changed) + self._scrollbar.valueChanged.connect( + lambda value: self._list_widget.verticalScrollBar().setValue(value) + ) + + # Delegate connections + self._entry_delegate.item_selected.connect(self._on_item_selected) + + def _setup_ui(self) -> None: + """Set up the widget UI.""" + # Size policy + 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) + self.setSizePolicy(size_policy) self.setMinimumSize(QtCore.QSize(710, 400)) + + # Font font = QtGui.QFont() font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) self.setFont(font) + + # Layout direction and style self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) self.setAutoFillBackground(False) - self.setStyleSheet("#file_page{\n background-color: transparent;\n}") - self.verticalLayout_5 = QtWidgets.QVBoxLayout(self) - self.verticalLayout_5.setObjectName("verticalLayout_5") - self.fp_header_layout = QtWidgets.QHBoxLayout() - self.fp_header_layout.setObjectName("fp_header_layout") + self.setStyleSheet("#file_page { background-color: transparent; }") + + # Main layout + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setObjectName("main_layout") + + # Header layout + header_layout = self._create_header_layout() + main_layout.addLayout(header_layout) + + # Separator line + line = QtWidgets.QFrame(parent=self) + line.setFrameShape(QtWidgets.QFrame.Shape.HLine) + line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + main_layout.addWidget(line) + + # Content layout + content_layout = self._create_content_layout() + main_layout.addLayout(content_layout) + + def _create_header_layout(self) -> QtWidgets.QHBoxLayout: + """Create the header with back and reload buttons.""" + layout = QtWidgets.QHBoxLayout() + layout.setObjectName("header_layout") + + # Back button self.back_btn = IconButton(parent=self) self.back_btn.setMinimumSize(QtCore.QSize(60, 60)) self.back_btn.setMaximumSize(QtCore.QSize(60, 60)) self.back_btn.setFlat(True) - self.back_btn.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") - ) + self.back_btn.setProperty("icon_pixmap", QtGui.QPixmap(self.ICON_PATHS["back"])) self.back_btn.setObjectName("back_btn") - self.fp_header_layout.addWidget( - self.back_btn, 0, QtCore.Qt.AlignmentFlag.AlignLeft - ) - self.ReloadButton = IconButton(parent=self) - self.ReloadButton.setMinimumSize(QtCore.QSize(60, 60)) - self.ReloadButton.setMaximumSize(QtCore.QSize(60, 60)) - self.ReloadButton.setFlat(True) - self.ReloadButton.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/refresh.svg") + layout.addWidget(self.back_btn, 0, QtCore.Qt.AlignmentFlag.AlignLeft) + + # Reload button + self._reload_button = IconButton(parent=self) + self._reload_button.setMinimumSize(QtCore.QSize(60, 60)) + self._reload_button.setMaximumSize(QtCore.QSize(60, 60)) + self._reload_button.setFlat(True) + self._reload_button.setProperty( + "icon_pixmap", QtGui.QPixmap(self.ICON_PATHS["refresh"]) ) - self.ReloadButton.setObjectName("ReloadButton") - self.fp_header_layout.addWidget( - self.ReloadButton, 0, QtCore.Qt.AlignmentFlag.AlignRight + self._reload_button.setObjectName("reload_button") + layout.addWidget(self._reload_button, 0, QtCore.Qt.AlignmentFlag.AlignRight) + + return layout + + def _create_content_layout(self) -> QtWidgets.QHBoxLayout: + """Create the content area with list and scrollbar.""" + layout = QtWidgets.QHBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setObjectName("content_layout") + + # Placeholder label + font = QtGui.QFont() + font.setPointSize(25) + self._label = QtWidgets.QLabel("No Files found") + self._label.setFont(font) + self._label.setStyleSheet("color: gray;") + self._label.hide() + + # List widget + self._list_widget = self._create_list_widget() + + # Scrollbar + self._scrollbar = CustomScrollBar() + self._scrollbar.show() + + # Add widgets to layout + layout.addWidget( + self._label, + alignment=( + QtCore.Qt.AlignmentFlag.AlignHCenter + | QtCore.Qt.AlignmentFlag.AlignVCenter + ), ) - self.verticalLayout_5.addLayout(self.fp_header_layout) - self.line = QtWidgets.QFrame(parent=self) - self.line.setMinimumSize(QtCore.QSize(0, 0)) - self.line.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line.setObjectName("line") - self.verticalLayout_5.addWidget(self.line) - self.fp_content_layout = QtWidgets.QHBoxLayout() - self.fp_content_layout.setContentsMargins(0, 0, 0, 0) - self.fp_content_layout.setObjectName("fp_content_layout") - self.listWidget = QtWidgets.QListView(parent=self) - self.listWidget.setModel(self.model) - self.listWidget.setItemDelegate(self.entry_delegate) - self.listWidget.setSpacing(5) - self.listWidget.setProperty("showDropIndicator", False) - self.listWidget.setProperty("selectionMode", "NoSelection") - self.listWidget.setStyleSheet("background: transparent;") - self.listWidget.setDefaultDropAction(QtCore.Qt.DropAction.IgnoreAction) - self.listWidget.setUniformItemSizes(True) - self.listWidget.setObjectName("listWidget") - self.listWidget.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.listWidget.setSelectionBehavior( + layout.addWidget(self._list_widget) + layout.addWidget(self._scrollbar) + + return layout + + def _create_list_widget(self) -> QtWidgets.QListView: + """Create and configure the list view widget.""" + list_widget = QtWidgets.QListView(parent=self) + list_widget.setModel(self._model) + list_widget.setItemDelegate(self._entry_delegate) + list_widget.setSpacing(5) + list_widget.setProperty("showDropIndicator", False) + list_widget.setProperty("selectionMode", "NoSelection") + list_widget.setStyleSheet("background: transparent;") + list_widget.setDefaultDropAction(QtCore.Qt.DropAction.IgnoreAction) + list_widget.setUniformItemSizes(True) + list_widget.setObjectName("list_widget") + list_widget.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + list_widget.setSelectionBehavior( QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems ) - self.listWidget.setHorizontalScrollBarPolicy( + list_widget.setHorizontalScrollBarPolicy( QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff ) - self.listWidget.setVerticalScrollMode( + list_widget.setVerticalScrollMode( QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel ) - self.listWidget.setVerticalScrollBarPolicy( + list_widget.setVerticalScrollBarPolicy( QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff ) + list_widget.setEditTriggers( + QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers + ) + + # Enable touch gestures QtWidgets.QScroller.grabGesture( - self.listWidget, + list_widget, QtWidgets.QScroller.ScrollerGestureType.TouchGesture, ) QtWidgets.QScroller.grabGesture( - self.listWidget, + list_widget, QtWidgets.QScroller.ScrollerGestureType.LeftMouseButtonGesture, ) - self.listWidget.setEditTriggers( - QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers - ) - scroller_instance = QtWidgets.QScroller.scroller(self.listWidget) - scroller_props = scroller_instance.scrollerProperties() - scroller_props.setScrollMetric( + # Configure scroller properties + scroller = QtWidgets.QScroller.scroller(list_widget) + props = scroller.scrollerProperties() + props.setScrollMetric( QtWidgets.QScrollerProperties.ScrollMetric.DragVelocitySmoothingFactor, 0.05, ) - scroller_props.setScrollMetric( + props.setScrollMetric( QtWidgets.QScrollerProperties.ScrollMetric.DecelerationFactor, 0.4, ) - QtWidgets.QScroller.scroller(self.listWidget).setScrollerProperties( - scroller_props - ) - - font = QtGui.QFont() - font.setPointSize(25) - self.label = QtWidgets.QLabel("No Files found") - self.label.setFont(font) - self.label.setStyleSheet("color: gray;") - self.label.setMinimumSize( - QtCore.QSize(self.listWidget.width(), self.listWidget.height()) - ) + scroller.setScrollerProperties(props) - self.scrollbar = CustomScrollBar() - - self.fp_content_layout.addWidget( - self.label, - alignment=QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - self.fp_content_layout.addWidget(self.listWidget) - self.fp_content_layout.addWidget(self.scrollbar) - self.verticalLayout_5.addLayout(self.fp_content_layout) - self.scrollbar.show() - self.label.hide() - - self.path = { - "back_folder": QtGui.QPixmap(":/ui/media/btn_icons/back_folder.svg"), - "folderIcon": QtGui.QPixmap(":/ui/media/btn_icons/folderIcon.svg"), - "right_arrow": QtGui.QPixmap( - ":/arrow_icons/media/btn_icons/right_arrow.svg" - ), - } + return list_widget diff --git a/BlocksScreen/lib/panels/widgets/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index bd5bbb8c..67add6b9 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -10,7 +10,7 @@ from lib.utils.display_button import DisplayButton from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger("logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class JobStatusWidget(QtWidgets.QWidget): @@ -53,6 +53,7 @@ class JobStatusWidget(QtWidgets.QWidget): request_file_info: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( str, name="request_file_info" ) + call_cancel_panel = QtCore.pyqtSignal(bool, name="call-load-panel") _internal_print_status: str = "" _current_file_name: str = "" @@ -225,18 +226,19 @@ def _handle_print_state(self, state: str) -> None: ) 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": self.print_finish.emit() - self._current_file_name = "" 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() + # if hasattr(self, "thumbnail_view"): + # getattr(self, "thumbnail_view").deleteLater() # Send Event on Print state if hasattr(events, str("Print" + lstate.capitalize())): event_obj = getattr(events, str("Print" + lstate.capitalize())) diff --git a/BlocksScreen/lib/panels/widgets/keyboardPage.py b/BlocksScreen/lib/panels/widgets/keyboardPage.py index 4f1d13d8..88ba9f2c 100644 --- a/BlocksScreen/lib/panels/widgets/keyboardPage.py +++ b/BlocksScreen/lib/panels/widgets/keyboardPage.py @@ -1,1166 +1,345 @@ -from lib.utils.icon_button import IconButton +import typing +from lib.utils.icon_button import IconButton from PyQt6 import QtCore, QtGui, QtWidgets +_LOWERCASE = list("qwertyuiopasdfghjklzxcvbnm") +_UPPERCASE = list("QWERTYUIOPASDFGHJKLZXCVBNM") +_NUMBERS = [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + "@", + "#", + "$", + '"', + "&&", + "*", + "-", + "+", + "=", + "(", + ")", + "!", + ":", + ";", + "'", + "?", +] +_SYMBOLS = [ + "~", + "`", + "|", + "%", + "^", + "°", + "_", + "{", + "}", + "[", + "]", + "<", + ">", + "/", + "\\", + ",", + ".", + "-", + "+", + "=", + "!", + "?", + ":", + ";", + "'", + "#", +] + +_NUM_KEYS = 26 + + +def _make_key_font(size: int = 29) -> QtGui.QFont: + font = QtGui.QFont() + font.setFamily("Modern") + font.setPointSize(size) + return font + class CustomQwertyKeyboard(QtWidgets.QWidget): - """A custom keyboard for inserting integer values""" + """Custom on-screen QWERTY keyboard for touch input.""" - value_selected = QtCore.pyqtSignal(str, name="value_selected") - request_back = QtCore.pyqtSignal(name="request_back") + value_selected: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="value_selected" + ) + request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + name="request_back" + ) - def __init__( - self, - parent, - ) -> None: + def __init__(self, parent: QtWidgets.QWidget) -> None: super().__init__(parent) - self._setupUi() self.current_value: str = "" - self.symbolsrun = False - self.setCursor( - QtCore.Qt.CursorShape.BlankCursor - ) # Disable cursor on touch keyboard + self.symbolsrun: bool = False + self._key_buttons: list[QtWidgets.QPushButton] = [] - self.K_q.clicked.connect(lambda: self.value_inserted(str(self.K_q.text()))) - self.K_w.clicked.connect(lambda: self.value_inserted(str(self.K_w.text()))) - self.K_e.clicked.connect(lambda: self.value_inserted(str(self.K_e.text()))) - self.K_r.clicked.connect(lambda: self.value_inserted(str(self.K_r.text()))) - self.K_t.clicked.connect(lambda: self.value_inserted(str(self.K_t.text()))) - self.K_y.clicked.connect(lambda: self.value_inserted(str(self.K_y.text()))) - self.K_u.clicked.connect(lambda: self.value_inserted(str(self.K_u.text()))) - self.K_i.clicked.connect(lambda: self.value_inserted(str(self.K_i.text()))) - self.K_o.clicked.connect(lambda: self.value_inserted(str(self.K_o.text()))) - self.K_p.clicked.connect(lambda: self.value_inserted(str(self.K_p.text()))) - self.K_a.clicked.connect(lambda: self.value_inserted(str(self.K_a.text()))) - self.K_s.clicked.connect(lambda: self.value_inserted(str(self.K_s.text()))) - self.K_d.clicked.connect(lambda: self.value_inserted(str(self.K_d.text()))) - self.K_f.clicked.connect(lambda: self.value_inserted(str(self.K_f.text()))) - self.K_g.clicked.connect(lambda: self.value_inserted(str(self.K_g.text()))) - self.K_h.clicked.connect(lambda: self.value_inserted(str(self.K_h.text()))) - self.K_j.clicked.connect(lambda: self.value_inserted(str(self.K_j.text()))) - self.K_k.clicked.connect(lambda: self.value_inserted(str(self.K_k.text()))) - self.K_l.clicked.connect(lambda: self.value_inserted(str(self.K_l.text()))) - self.K_z.clicked.connect(lambda: self.value_inserted(str(self.K_z.text()))) - self.K_x.clicked.connect(lambda: self.value_inserted(str(self.K_x.text()))) - self.K_c.clicked.connect(lambda: self.value_inserted(str(self.K_c.text()))) - self.K_v.clicked.connect(lambda: self.value_inserted(str(self.K_v.text()))) - self.K_b.clicked.connect(lambda: self.value_inserted(str(self.K_b.text()))) - self.K_n.clicked.connect(lambda: self.value_inserted(str(self.K_n.text()))) - self.K_m.clicked.connect(lambda: self.value_inserted(str(self.K_m.text()))) + self._setup_ui() + self.setCursor(QtCore.Qt.CursorShape.BlankCursor) + for btn in self._key_buttons: + btn.clicked.connect(lambda _, b=btn: self.value_inserted(b.text())) + + self.K_dot.clicked.connect(lambda: self.value_inserted(".")) self.K_space.clicked.connect(lambda: self.value_inserted(" ")) self.k_Enter.clicked.connect(lambda: self.value_inserted("enter")) self.k_delete.clicked.connect(lambda: self.value_inserted("clear")) - - self.inserted_value.setText("") - self.K_keychange.clicked.connect(self.handle_keyboard_layout) self.K_shift.clicked.connect(self.handle_keyboard_layout) + self.numpad_back_btn.clicked.connect(self.request_back.emit) - self.numpad_back_btn.clicked.connect(lambda: self.request_back.emit()) + self.inserted_value.setText("") - self.setStyleSheet(""" - QPushButton { - background-color: #dfdfdf; - border-radius: 10px; - padding: 6px; - font-size: 25px; - } - QPushButton:pressed { - background-color: lightgrey; - color: black; - } - QPushButton:checked { - background-color: #212120; - color: white; - } - """) + self.setStyleSheet( + "QPushButton {" + " background-color: #dfdfdf;" + " border-radius: 10px;" + " padding: 6px;" + " font-size: 25px;" + "}" + "QPushButton:pressed {" + " background-color: lightgrey;" + " color: black;" + "}" + "QPushButton:checked {" + " background-color: #212120;" + " color: white;" + "}" + ) self.handle_keyboard_layout() - def handle_keyboard_layout(self): - """Verifies if shift is toggled, changes layout accordingly""" + def handle_keyboard_layout(self) -> None: + """Update key labels based on current shift/keychange state.""" shift = self.K_shift.isChecked() keychange = self.K_keychange.isChecked() - # references to all key buttons - keys = [ - self.K_q, - self.K_w, - self.K_e, - self.K_r, - self.K_t, - self.K_y, - self.K_u, - self.K_i, - self.K_o, - self.K_p, - self.K_a, - self.K_s, - self.K_d, - self.K_f, - self.K_g, - self.K_h, - self.K_j, - self.K_k, - self.K_l, - self.K_z, - self.K_x, - self.K_c, - self.K_v, - self.K_b, - self.K_n, - self.K_m, - ] - - # ------------------------------- - # Keyboard Layouts - # ------------------------------- - lowercase = list("qwertyuiopasdfghjklzxcvbnm") - uppercase = list("QWERTYUIOPASDFGHJKLZXCVBNM") - numbers = [ - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "0", - "@", - "#", - "$", - "%", - "&&", - "*", - "-", - "+", - "=", - "(", - ")", - "!", - ":", - ";", - "'", - "?", - ] - symbols = [ - "~", - "`", - "|", - "•", - "√", - "π", - "÷", - "×", - "¶", - "∆", - "€", - "£", - "¥", - "₩", - "^", - "°", - "_", - "{", - "}", - "[", - "]", - "<", - ">", - "/", - "\\", - ",", - ".", - ] - - # ------------------------------- - # Logic - # ------------------------------- - if not keychange and not shift: - layout = lowercase - + layout = _LOWERCASE elif not keychange and shift: if self.symbolsrun: - layout = lowercase + layout = _LOWERCASE self.K_shift.setChecked(False) self.symbolsrun = False else: - layout = uppercase + layout = _UPPERCASE elif keychange and not shift: - layout = numbers + layout = _NUMBERS elif keychange and shift: - layout = symbols + layout = _SYMBOLS self.symbolsrun = True else: - layout = lowercase + layout = _LOWERCASE - # update all button texts - for btn, txt in zip(keys, layout): + for btn, txt in zip(self._key_buttons, layout): btn.setText(txt) - # update shift button text for clarity - if keychange: - self.K_shift.setText("#+=") - else: - self.K_shift.setText("Shift") + self.K_shift.setText("#+=") if keychange else self.K_shift.setText("⇧") def value_inserted(self, value: str) -> None: - """Handle value insertion on the keyboard - - Args: - value (int | str): value - """ - + """Handle key press: append char, delete, or submit on enter.""" if value == "&&": value = "&" - if "enter" in value: + if value == "enter": self.value_selected.emit(self.current_value) self.current_value = "" self.inserted_value.setText("") return - elif "clear" in value: + if value == "clear": if len(self.current_value) > 1: self.current_value = self.current_value[:-1] - else: self.current_value = "" - else: - self.current_value += str(value) + self.current_value += value - self.inserted_value.setText(str(self.current_value)) + self.inserted_value.setText(self.current_value) def set_value(self, value: str) -> None: - """Set keyboard value""" + """Pre-fill keyboard input with an existing value.""" self.current_value = value self.inserted_value.setText(value) - def _setupUi(self): + def _create_key_button( + self, + parent: QtWidgets.QWidget, + name: str, + *, + min_w: int = 75, + min_h: int = 50, + max_h: int = 50, + h_policy: QtWidgets.QSizePolicy.Policy = ( + QtWidgets.QSizePolicy.Policy.Expanding + ), + v_policy: QtWidgets.QSizePolicy.Policy = (QtWidgets.QSizePolicy.Policy.Fixed), + ) -> QtWidgets.QPushButton: + """Create a styled key button with consistent appearance.""" + btn = QtWidgets.QPushButton(parent=parent) + policy = QtWidgets.QSizePolicy(h_policy, v_policy) + btn.setSizePolicy(policy) + btn.setMinimumSize(QtCore.QSize(min_w, min_h)) + btn.setMaximumSize(QtCore.QSize(16777215, max_h)) + btn.setFont(_make_key_font()) + btn.setTabletTracking(False) + btn.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) + btn.setFlat(True) + btn.setObjectName(name) + return btn + + def _setup_ui(self) -> None: self.setObjectName("self") - self.setEnabled(True) self.resize(800, 480) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Preferred, - QtWidgets.QSizePolicy.Policy.Preferred, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) - self.setSizePolicy(sizePolicy) self.setMaximumSize(QtCore.QSize(800, 480)) self.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.ArrowCursor)) self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.layoutWidget_2 = QtWidgets.QWidget(parent=self) - self.layoutWidget_2.setGeometry(QtCore.QRect(10, 150, 781, 52)) - self.layoutWidget_2.setObjectName("layoutWidget_2") - self.gridLayout_2 = QtWidgets.QGridLayout(self.layoutWidget_2) - self.gridLayout_2.setSizeConstraint( - QtWidgets.QLayout.SizeConstraint.SetMinimumSize - ) - self.gridLayout_2.setContentsMargins(0, 0, 0, 0) - self.gridLayout_2.setHorizontalSpacing(2) - self.gridLayout_2.setVerticalSpacing(5) - self.gridLayout_2.setObjectName("gridLayout_2") - self.K_p = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_p.sizePolicy().hasHeightForWidth()) - self.K_p.setSizePolicy(sizePolicy) - self.K_p.setMinimumSize(QtCore.QSize(75, 50)) - self.K_p.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_p.setFont(font) - self.K_p.setTabletTracking(False) - self.K_p.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_p.setFlat(True) - self.K_p.setObjectName("K_p") - self.gridLayout_2.addWidget( - self.K_p, 0, 9, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_i = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_i.sizePolicy().hasHeightForWidth()) - self.K_i.setSizePolicy(sizePolicy) - self.K_i.setMinimumSize(QtCore.QSize(75, 50)) - self.K_i.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_i.setFont(font) - self.K_i.setTabletTracking(False) - self.K_i.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_i.setFlat(True) - self.K_i.setObjectName("K_i") - self.gridLayout_2.addWidget( - self.K_i, 0, 7, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_y = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_y.sizePolicy().hasHeightForWidth()) - self.K_y.setSizePolicy(sizePolicy) - self.K_y.setMinimumSize(QtCore.QSize(75, 50)) - self.K_y.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_y.setFont(font) - self.K_y.setTabletTracking(False) - self.K_y.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_y.setFlat(True) - self.K_y.setObjectName("K_y") - self.gridLayout_2.addWidget( - self.K_y, 0, 5, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_t = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_t.sizePolicy().hasHeightForWidth()) - self.K_t.setSizePolicy(sizePolicy) - self.K_t.setMinimumSize(QtCore.QSize(75, 50)) - self.K_t.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_t.setFont(font) - self.K_t.setTabletTracking(False) - self.K_t.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_t.setFlat(True) - self.K_t.setObjectName("K_t") - self.gridLayout_2.addWidget( - self.K_t, 0, 4, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_r = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_r.sizePolicy().hasHeightForWidth()) - self.K_r.setSizePolicy(sizePolicy) - self.K_r.setMinimumSize(QtCore.QSize(75, 50)) - self.K_r.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_r.setFont(font) - self.K_r.setTabletTracking(False) - self.K_r.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_r.setFlat(True) - self.K_r.setObjectName("K_r") - self.gridLayout_2.addWidget( - self.K_r, 0, 3, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_o = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_o.sizePolicy().hasHeightForWidth()) - self.K_o.setSizePolicy(sizePolicy) - self.K_o.setMinimumSize(QtCore.QSize(75, 50)) - self.K_o.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_o.setFont(font) - self.K_o.setTabletTracking(False) - self.K_o.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_o.setFlat(True) - self.K_o.setObjectName("K_o") - self.gridLayout_2.addWidget( - self.K_o, 0, 8, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_u = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_u.sizePolicy().hasHeightForWidth()) - self.K_u.setSizePolicy(sizePolicy) - self.K_u.setMinimumSize(QtCore.QSize(75, 50)) - self.K_u.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_u.setFont(font) - self.K_u.setTabletTracking(False) - self.K_u.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_u.setFlat(True) - self.K_u.setObjectName("K_u") - self.gridLayout_2.addWidget( - self.K_u, 0, 6, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_w = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_w.sizePolicy().hasHeightForWidth()) - self.K_w.setSizePolicy(sizePolicy) - self.K_w.setMinimumSize(QtCore.QSize(75, 50)) - self.K_w.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_w.setFont(font) - self.K_w.setTabletTracking(False) - self.K_w.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_w.setFlat(True) - self.K_w.setObjectName("K_w") - self.gridLayout_2.addWidget( - self.K_w, - 0, - 1, - 1, - 1, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - self.K_e = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_e.sizePolicy().hasHeightForWidth()) - self.K_e.setSizePolicy(sizePolicy) - self.K_e.setMinimumSize(QtCore.QSize(75, 50)) - self.K_e.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_e.setFont(font) - self.K_e.setTabletTracking(False) - self.K_e.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_e.setFlat(True) - self.K_e.setObjectName("K_e") - self.gridLayout_2.addWidget( - self.K_e, 0, 2, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_q = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_q.sizePolicy().hasHeightForWidth()) - self.K_q.setSizePolicy(sizePolicy) - self.K_q.setMinimumSize(QtCore.QSize(75, 50)) - self.K_q.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_q.setFont(font) - self.K_q.setTabletTracking(False) - self.K_q.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_q.setFlat(True) - self.K_q.setObjectName("K_q") - self.gridLayout_2.addWidget( - self.K_q, - 0, - 0, - 1, - 1, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - self.layoutWidget = QtWidgets.QWidget(parent=self) - self.layoutWidget.setGeometry(QtCore.QRect(50, 220, 701, 52)) - self.layoutWidget.setMinimumSize(QtCore.QSize(64, 0)) - self.layoutWidget.setObjectName("layoutWidget") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.layoutWidget) - self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout_2.setSpacing(2) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.K_a = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_a.sizePolicy().hasHeightForWidth()) - self.K_a.setSizePolicy(sizePolicy) - self.K_a.setMinimumSize(QtCore.QSize(75, 50)) - self.K_a.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_a.setFont(font) - self.K_a.setTabletTracking(False) - self.K_a.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_a.setFlat(True) - self.K_a.setObjectName("K_a") - self.horizontalLayout_2.addWidget(self.K_a) - self.K_s = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_s.sizePolicy().hasHeightForWidth()) - self.K_s.setSizePolicy(sizePolicy) - self.K_s.setMinimumSize(QtCore.QSize(75, 50)) - self.K_s.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_s.setFont(font) - self.K_s.setTabletTracking(False) - self.K_s.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_s.setFlat(True) - self.K_s.setObjectName("K_s") - self.horizontalLayout_2.addWidget(self.K_s) - self.K_d = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_d.sizePolicy().hasHeightForWidth()) - self.K_d.setSizePolicy(sizePolicy) - self.K_d.setMinimumSize(QtCore.QSize(75, 50)) - self.K_d.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_d.setFont(font) - self.K_d.setTabletTracking(False) - self.K_d.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_d.setFlat(True) - self.K_d.setObjectName("K_d") - self.horizontalLayout_2.addWidget(self.K_d) - self.K_f = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_f.sizePolicy().hasHeightForWidth()) - self.K_f.setSizePolicy(sizePolicy) - self.K_f.setMinimumSize(QtCore.QSize(75, 50)) - self.K_f.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_f.setFont(font) - self.K_f.setTabletTracking(False) - self.K_f.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_f.setFlat(True) - self.K_f.setObjectName("K_f") - self.horizontalLayout_2.addWidget(self.K_f) - self.K_g = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_g.sizePolicy().hasHeightForWidth()) - self.K_g.setSizePolicy(sizePolicy) - self.K_g.setMinimumSize(QtCore.QSize(75, 50)) - self.K_g.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_g.setFont(font) - self.K_g.setTabletTracking(False) - self.K_g.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_g.setFlat(True) - self.K_g.setObjectName("K_g") - self.horizontalLayout_2.addWidget(self.K_g) - self.K_h = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_h.sizePolicy().hasHeightForWidth()) - self.K_h.setSizePolicy(sizePolicy) - self.K_h.setMinimumSize(QtCore.QSize(75, 50)) - self.K_h.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_h.setFont(font) - self.K_h.setTabletTracking(False) - self.K_h.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_h.setFlat(True) - self.K_h.setObjectName("K_h") - self.horizontalLayout_2.addWidget(self.K_h) - self.K_j = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_j.sizePolicy().hasHeightForWidth()) - self.K_j.setSizePolicy(sizePolicy) - self.K_j.setMinimumSize(QtCore.QSize(75, 50)) - self.K_j.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_j.setFont(font) - self.K_j.setTabletTracking(False) - self.K_j.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_j.setFlat(True) - self.K_j.setObjectName("K_j") - self.horizontalLayout_2.addWidget(self.K_j) - self.K_k = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_k.sizePolicy().hasHeightForWidth()) - self.K_k.setSizePolicy(sizePolicy) - self.K_k.setMinimumSize(QtCore.QSize(75, 50)) - self.K_k.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_k.setFont(font) - self.K_k.setTabletTracking(False) - self.K_k.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_k.setFlat(True) - self.K_k.setObjectName("K_k") - self.horizontalLayout_2.addWidget(self.K_k) - self.K_l = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_l.sizePolicy().hasHeightForWidth()) - self.K_l.setSizePolicy(sizePolicy) - self.K_l.setMinimumSize(QtCore.QSize(75, 50)) - self.K_l.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_l.setFont(font) - self.K_l.setTabletTracking(False) - self.K_l.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_l.setFlat(True) - self.K_l.setObjectName("K_l") - self.horizontalLayout_2.addWidget(self.K_l) - self.layoutWidget_3 = QtWidgets.QWidget(parent=self) - self.layoutWidget_3.setGeometry(QtCore.QRect(100, 290, 591, 52)) - self.layoutWidget_3.setObjectName("layoutWidget_3") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.layoutWidget_3) - self.horizontalLayout.setSizeConstraint( - QtWidgets.QLayout.SizeConstraint.SetMinimumSize - ) - self.horizontalLayout.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout.setSpacing(2) - self.horizontalLayout.setObjectName("horizontalLayout") - self.K_z = QtWidgets.QPushButton(parent=self.layoutWidget_3) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_z.sizePolicy().hasHeightForWidth()) - self.K_z.setSizePolicy(sizePolicy) - self.K_z.setMinimumSize(QtCore.QSize(60, 50)) - self.K_z.setMaximumSize(QtCore.QSize(16777215, 60)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_z.setFont(font) - self.K_z.setTabletTracking(False) - self.K_z.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_z.setFlat(True) - self.K_z.setObjectName("K_z") - self.horizontalLayout.addWidget(self.K_z) - self.K_x = QtWidgets.QPushButton(parent=self.layoutWidget_3) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_x.sizePolicy().hasHeightForWidth()) - self.K_x.setSizePolicy(sizePolicy) - self.K_x.setMinimumSize(QtCore.QSize(60, 50)) - self.K_x.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_x.setFont(font) - self.K_x.setTabletTracking(False) - self.K_x.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_x.setFlat(True) - self.K_x.setObjectName("K_x") - self.horizontalLayout.addWidget(self.K_x) - self.K_c = QtWidgets.QPushButton(parent=self.layoutWidget_3) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_c.sizePolicy().hasHeightForWidth()) - self.K_c.setSizePolicy(sizePolicy) - self.K_c.setMinimumSize(QtCore.QSize(60, 50)) - self.K_c.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_c.setFont(font) - self.K_c.setTabletTracking(False) - self.K_c.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_c.setFlat(True) - self.K_c.setObjectName("K_c") - self.horizontalLayout.addWidget(self.K_c) - self.K_v = QtWidgets.QPushButton(parent=self.layoutWidget_3) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_v.sizePolicy().hasHeightForWidth()) - self.K_v.setSizePolicy(sizePolicy) - self.K_v.setMinimumSize(QtCore.QSize(60, 50)) - self.K_v.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_v.setFont(font) - self.K_v.setTabletTracking(False) - self.K_v.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_v.setFlat(True) - self.K_v.setObjectName("K_v") - self.horizontalLayout.addWidget(self.K_v) - self.K_b = QtWidgets.QPushButton(parent=self.layoutWidget_3) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_b.sizePolicy().hasHeightForWidth()) - self.K_b.setSizePolicy(sizePolicy) - self.K_b.setMinimumSize(QtCore.QSize(60, 50)) - self.K_b.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_b.setFont(font) - self.K_b.setTabletTracking(False) - self.K_b.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_b.setFlat(True) - self.K_b.setObjectName("K_b") - self.horizontalLayout.addWidget(self.K_b) - self.K_n = QtWidgets.QPushButton(parent=self.layoutWidget_3) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_n.sizePolicy().hasHeightForWidth()) - self.K_n.setSizePolicy(sizePolicy) - self.K_n.setMinimumSize(QtCore.QSize(60, 50)) - self.K_n.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_n.setFont(font) - self.K_n.setTabletTracking(False) - self.K_n.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_n.setFlat(True) - self.K_n.setObjectName("K_n") - self.horizontalLayout.addWidget(self.K_n) - self.K_m = QtWidgets.QPushButton(parent=self.layoutWidget_3) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_m.sizePolicy().hasHeightForWidth()) - self.K_m.setSizePolicy(sizePolicy) - self.K_m.setMinimumSize(QtCore.QSize(60, 50)) - self.K_m.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_m.setFont(font) - self.K_m.setTabletTracking(False) - self.K_m.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_m.setFlat(True) - self.K_m.setObjectName("K_m") - self.horizontalLayout.addWidget(self.K_m) + + # Row 1: qwertyuiop (grid layout) + row1_widget = QtWidgets.QWidget(parent=self) + row1_widget.setGeometry(QtCore.QRect(10, 150, 781, 52)) + row1_layout = QtWidgets.QGridLayout(row1_widget) + row1_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize) + row1_layout.setContentsMargins(0, 0, 0, 0) + row1_layout.setHorizontalSpacing(2) + row1_layout.setVerticalSpacing(5) + + row1_keys = "qwertyuiop" + for col, letter in enumerate(row1_keys): + btn = self._create_key_button( + row1_widget, + f"K_{letter}", + v_policy=QtWidgets.QSizePolicy.Policy.Expanding, + ) + row1_layout.addWidget( + btn, 0, col, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter + ) + self._key_buttons.append(btn) + + # Row 2: asdfghjkl (hbox layout) + row2_widget = QtWidgets.QWidget(parent=self) + row2_widget.setGeometry(QtCore.QRect(50, 220, 701, 52)) + row2_widget.setMinimumSize(QtCore.QSize(64, 0)) + row2_layout = QtWidgets.QHBoxLayout(row2_widget) + row2_layout.setContentsMargins(0, 0, 0, 0) + row2_layout.setSpacing(2) + + for letter in "asdfghjkl": + btn = self._create_key_button(row2_widget, f"K_{letter}") + row2_layout.addWidget(btn) + self._key_buttons.append(btn) + + # Row 3: zxcvbnm (hbox layout) + row3_widget = QtWidgets.QWidget(parent=self) + row3_widget.setGeometry(QtCore.QRect(100, 290, 591, 52)) + row3_layout = QtWidgets.QHBoxLayout(row3_widget) + row3_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize) + row3_layout.setContentsMargins(0, 0, 0, 0) + row3_layout.setSpacing(2) + + for letter in "zxcvbnm": + btn = self._create_key_button(row3_widget, f"K_{letter}", min_w=60) + row3_layout.addWidget(btn) + self._key_buttons.append(btn) + + # Shift button (left of row 3) + self.K_shift = QtWidgets.QPushButton(parent=self) + self.K_shift.setGeometry(QtCore.QRect(10, 280, 81, 51)) + self.K_shift.setCheckable(True) + self.K_shift.setText("Shift") + self.K_shift.setObjectName("K_shift") + + # Delete button (right of row 3) + self.k_delete = QtWidgets.QPushButton(parent=self) + self.k_delete.setGeometry(QtCore.QRect(700, 280, 81, 51)) + self.k_delete.setText("\u232b") + self.k_delete.setObjectName("k_delete") + + # Bottom row: [123] [.] [ space ] [enter] + self.K_keychange = QtWidgets.QPushButton(parent=self) + self.K_keychange.setGeometry(QtCore.QRect(20, 350, 93, 60)) + self.K_keychange.setCheckable(True) + self.K_keychange.setText("123") + self.K_keychange.setObjectName("K_keychange") + + self.K_dot = QtWidgets.QPushButton(parent=self) + self.K_dot.setGeometry(QtCore.QRect(120, 362, 55, 60)) + self.K_dot.setFont(_make_key_font()) + self.K_dot.setText(".") + self.K_dot.setFlat(True) + self.K_dot.setObjectName("K_dot") + self.K_space = QtWidgets.QPushButton(parent=self) - self.K_space.setGeometry(QtCore.QRect(120, 362, 551, 60)) - sizePolicy = QtWidgets.QSizePolicy( + self.K_space.setGeometry(QtCore.QRect(177, 362, 494, 60)) + policy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding, ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_space.sizePolicy().hasHeightForWidth()) - self.K_space.setSizePolicy(sizePolicy) + self.K_space.setSizePolicy(policy) self.K_space.setMinimumSize(QtCore.QSize(0, 60)) self.K_space.setMaximumSize(QtCore.QSize(16777215, 60)) self.K_space.setObjectName("K_space") + self.k_Enter = QtWidgets.QPushButton(parent=self) self.k_Enter.setGeometry(QtCore.QRect(680, 350, 93, 60)) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.k_Enter.sizePolicy().hasHeightForWidth()) - self.k_Enter.setSizePolicy(sizePolicy) - self.k_Enter.setMinimumSize(QtCore.QSize(0, 0)) - self.k_Enter.setMaximumSize(QtCore.QSize(16777215, 16777215)) + self.k_Enter.setText("\u23ce") self.k_Enter.setAutoRepeat(False) self.k_Enter.setObjectName("k_Enter") - self.k_delete = QtWidgets.QPushButton(parent=self) - self.k_delete.setGeometry(QtCore.QRect(700, 280, 81, 51)) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.k_delete.sizePolicy().hasHeightForWidth()) - self.k_delete.setSizePolicy(sizePolicy) - self.k_delete.setMinimumSize(QtCore.QSize(0, 0)) - self.k_delete.setMaximumSize(QtCore.QSize(16777215, 16777215)) - self.k_delete.setObjectName("k_delete") - self.K_keychange = QtWidgets.QPushButton(parent=self) - self.K_keychange.setGeometry(QtCore.QRect(20, 350, 93, 60)) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_keychange.sizePolicy().hasHeightForWidth()) - self.K_keychange.setSizePolicy(sizePolicy) - self.K_keychange.setMinimumSize(QtCore.QSize(0, 0)) - self.K_keychange.setMaximumSize(QtCore.QSize(16777215, 16777215)) - self.K_keychange.setCheckable(True) - self.K_keychange.setObjectName("K_keychange") - self.K_shift = QtWidgets.QPushButton(parent=self) - self.K_shift.setGeometry(QtCore.QRect(10, 280, 81, 51)) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_shift.sizePolicy().hasHeightForWidth()) - self.K_shift.setSizePolicy(sizePolicy) - self.K_shift.setMinimumSize(QtCore.QSize(0, 0)) - self.K_shift.setMaximumSize(QtCore.QSize(16777215, 16777215)) - self.K_shift.setCheckable(True) - self.K_shift.setObjectName("K_shift") + + # Back button (top-right) self.numpad_back_btn = IconButton(parent=self) - self.numpad_back_btn.setEnabled(True) self.numpad_back_btn.setGeometry(QtCore.QRect(720, 20, 60, 60)) - sizePolicy = QtWidgets.QSizePolicy( + policy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding, ) - sizePolicy.setHorizontalStretch(1) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth( - self.numpad_back_btn.sizePolicy().hasHeightForWidth() - ) - self.numpad_back_btn.setSizePolicy(sizePolicy) + policy.setHorizontalStretch(1) + policy.setVerticalStretch(1) + self.numpad_back_btn.setSizePolicy(policy) self.numpad_back_btn.setMinimumSize(QtCore.QSize(60, 60)) self.numpad_back_btn.setMaximumSize(QtCore.QSize(60, 60)) - self.numpad_back_btn.setStyleSheet("") - self.numpad_back_btn.setText("") self.numpad_back_btn.setIconSize(QtCore.QSize(16, 16)) - self.numpad_back_btn.setCheckable(False) - self.numpad_back_btn.setChecked(False) self.numpad_back_btn.setFlat(True) self.numpad_back_btn.setProperty( "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") ) + self.numpad_back_btn.setProperty("button_type", "icon") self.numpad_back_btn.setObjectName("numpad_back_btn") - self.layoutWidget1 = QtWidgets.QWidget(parent=self) - self.layoutWidget1.setGeometry(QtCore.QRect(10, 90, 781, 48)) - self.layoutWidget1.setObjectName("layoutWidget1") - self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.layoutWidget1) - self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.inserted_value = QtWidgets.QLabel(parent=self.layoutWidget1) + + # Input display area + input_widget = QtWidgets.QWidget(parent=self) + input_widget.setGeometry(QtCore.QRect(10, 90, 781, 48)) + input_layout = QtWidgets.QVBoxLayout(input_widget) + input_layout.setContentsMargins(0, 0, 0, 0) + + self.inserted_value = QtWidgets.QLabel(parent=input_widget) self.inserted_value.setMinimumSize(QtCore.QSize(500, 0)) self.inserted_value.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(18) - self.inserted_value.setFont(font) - self.inserted_value.setAutoFillBackground(False) - self.inserted_value.setStyleSheet( - "color: white;\n " - ) + self.inserted_value.setFont(_make_key_font(18)) + self.inserted_value.setStyleSheet("color: white;") self.inserted_value.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) self.inserted_value.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) self.inserted_value.setLineWidth(0) - self.inserted_value.setText("") - self.inserted_value.setScaledContents(False) self.inserted_value.setAlignment( QtCore.Qt.AlignmentFlag.AlignBottom | QtCore.Qt.AlignmentFlag.AlignHCenter ) - self.inserted_value.setIndent(0) - self.inserted_value.setTextInteractionFlags( - QtCore.Qt.TextInteractionFlag.LinksAccessibleByMouse - ) self.inserted_value.setObjectName("inserted_value") - self.verticalLayout_2.addWidget(self.inserted_value) - self.line = QtWidgets.QFrame(parent=self.layoutWidget1) - self.line.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line.setObjectName("line") - self.verticalLayout_2.addWidget(self.line) - - self.retranslateUi() - QtCore.QMetaObject.connectSlotsByName(self) + input_layout.addWidget(self.inserted_value) - def retranslateUi(self): - _translate = QtCore.QCoreApplication.translate - self.setWindowTitle(_translate("self", "Form")) - self.K_p.setText(_translate("self", "P")) - self.K_p.setProperty("position", _translate("self", "left")) - self.K_i.setText(_translate("self", "I")) - self.K_i.setProperty("position", _translate("self", "left")) - self.K_y.setText(_translate("self", "Y")) - self.K_y.setProperty("position", _translate("self", "left")) - self.K_t.setText(_translate("self", "T")) - self.K_t.setProperty("position", _translate("self", "left")) - self.K_r.setText(_translate("self", "R")) - self.K_r.setProperty("position", _translate("self", "left")) - self.K_o.setText(_translate("self", "O")) - self.K_o.setProperty("position", _translate("self", "left")) - self.K_u.setText(_translate("self", "U")) - self.K_u.setProperty("position", _translate("self", "left")) - self.K_w.setText(_translate("self", "W")) - self.K_w.setProperty("position", _translate("self", "left")) - self.K_e.setText(_translate("self", "E")) - self.K_e.setProperty("position", _translate("self", "left")) - self.K_q.setText(_translate("self", "Q")) - self.K_q.setProperty("position", _translate("self", "left")) - self.K_a.setText(_translate("self", "A")) - self.K_a.setProperty("position", _translate("self", "left")) - self.K_s.setText(_translate("self", "S")) - self.K_s.setProperty("position", _translate("self", "left")) - self.K_d.setText(_translate("self", "D")) - self.K_d.setProperty("position", _translate("self", "left")) - self.K_f.setText(_translate("self", "F")) - self.K_f.setProperty("position", _translate("self", "left")) - self.K_g.setText(_translate("self", "G")) - self.K_g.setProperty("position", _translate("self", "left")) - self.K_h.setText(_translate("self", "H")) - self.K_h.setProperty("position", _translate("self", "left")) - self.K_j.setText(_translate("self", "J")) - self.K_j.setProperty("position", _translate("self", "left")) - self.K_k.setText(_translate("self", "K")) - self.K_k.setProperty("position", _translate("self", "left")) - self.K_l.setText(_translate("self", "L")) - self.K_l.setProperty("position", _translate("self", "left")) - self.K_z.setText(_translate("self", "Z")) - self.K_z.setProperty("position", _translate("self", "left")) - self.K_x.setText(_translate("self", "X")) - self.K_x.setProperty("position", _translate("self", "left")) - self.K_c.setText(_translate("self", "C")) - self.K_c.setProperty("position", _translate("self", "left")) - self.K_v.setText(_translate("self", "V")) - self.K_v.setProperty("position", _translate("self", "left")) - self.K_b.setText(_translate("self", "B")) - self.K_b.setProperty("position", _translate("self", "left")) - self.K_n.setText(_translate("self", "N")) - self.K_n.setProperty("position", _translate("self", "left")) - self.K_m.setText(_translate("self", "M")) - self.K_m.setProperty("position", _translate("self", "left")) - self.K_space.setText(_translate("self", "Spacing")) - self.k_Enter.setText(_translate("self", "⏎")) - self.k_delete.setText(_translate("self", "⌫")) - self.K_keychange.setText(_translate("self", "123")) - self.K_shift.setText(_translate("self", "⇧")) - self.numpad_back_btn.setProperty("button_type", _translate("self", "icon")) + line = QtWidgets.QFrame(parent=input_widget) + line.setFrameShape(QtWidgets.QFrame.Shape.HLine) + line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + input_layout.addWidget(line) diff --git a/BlocksScreen/lib/panels/widgets/notificationPage.py b/BlocksScreen/lib/panels/widgets/notificationPage.py new file mode 100644 index 00000000..a14b6cbc --- /dev/null +++ b/BlocksScreen/lib/panels/widgets/notificationPage.py @@ -0,0 +1,414 @@ +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 + + +class NotificationPage(QtWidgets.QWidget): + """Update GUI Page, + retrieves from moonraker available clients and adds functionality + for updating or recovering them + """ + + on_update_message: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + dict, name="on-update-message" + ) + + def __init__(self, parent=None) -> None: + if parent: + super().__init__(parent) + else: + super().__init__() + self._setupUI() + self.cli_tracking: Deque = deque() + self.selected_item: ListItem | None = None + self.ongoing_update: bool = False + self.popup = Popup(self) + + self.model = EntryListModel() + self.model.setParent(self.update_buttons_list_widget) + self.entry_delegate = EntryDelegate() + self.update_buttons_list_widget.setModel(self.model) + self.update_buttons_list_widget.setItemDelegate(self.entry_delegate) + self.entry_delegate.item_selected.connect(self.on_item_clicked) + + self.update_back_btn.clicked.connect(self.hide) + self.delete_btn.clicked.connect(self.delete_selected_item) + self.delete_all_btn.clicked.connect(self.reset_view_model) + + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_StyledBackground, True) + self.hide() + + @QtCore.pyqtSlot(name="call-notification-panel") + def show_notification_panel( + self, + ) -> None: + """Slot for displaying notification Panel""" + if not self.parent(): + return + _parent_size = self.parent().size() # type: ignore + self.setGeometry(0, 0, _parent_size.width(), _parent_size.height()) + self.updateGeometry() + self.update() + self.show() + self.raise_() + + def delete_selected_item(self) -> None: + """Deletes currently selected item from the list view""" + if self.selected_item is None: + return + self.model.remove_item(self.selected_item) + self.delete_btn.setEnabled(False) + self.selected_item = None + + def reset_view_model(self) -> None: + """Clears items from ListView + (Resets `QAbstractListModel` by clearing entries) + """ + self.model.clear() + self.entry_delegate.clear() + + def build_model_list(self) -> None: + """Builds the model list (`self.model`) containing updatable clients""" + self.update_buttons_list_widget.blockSignals(True) + message, origin, priority = self.cli_tracking.popleft() + match priority: + case 1: + self._add_notif_entry( + message, "#1A8FBF", QtGui.QPixmap(":/ui/media/btn_icons/info.svg") + ) + case 2: + self._add_notif_entry( + message, + "#E7E147", + QtGui.QPixmap(":/ui/media/btn_icons/troubleshoot.svg"), + ) + case 3: + self._add_notif_entry( + message, "#CA4949", QtGui.QPixmap(":/ui/media/btn_icons/error.svg") + ) + case _: + self._add_notif_entry( + message, "#a4a4a4", QtGui.QPixmap(":/ui/media/btn_icons/info.svg") + ) + + self.model.setData(self.model.index(0), True, EntryListModel.EnableRole) + self.update_buttons_list_widget.blockSignals(False) + + @QtCore.pyqtSlot(ListItem, name="on-item-clicked") + def on_item_clicked(self, item: ListItem) -> None: + """Setup information for the currently clicked list item on the info box. + Keeps track of the list item + """ + self.delete_btn.setEnabled(True) + + match item.color: + case "#1A8FBF": + self.type_label.setText("Info") + case "#E7E147": + self.type_label.setText("Warning") + case "#CA4949": + self.type_label.setText("Error") + case _: + self.type_label.setText("Unknown") + + self.time_label.setText(item._cache.get(-1, "N/A")) + self.selected_item = item + + @QtCore.pyqtSlot(str, str, int, bool, name="new-notication") + def new_notication( + self, + origin: str | None = None, + message: str = "", + priority: int = 0, + popup: bool = False, + ): + """ + :param message: sets notification message + :type message: str + :param priority: sets notification priority from 0 to 3 + :type priority: int + :param popup: sets if notification should appear as popup + :type popup: bool + """ + self.cli_tracking.append((message, origin, priority)) + self.model.delete_duplicates() + + if popup: + ui = False + match priority: + case 3: + type = Popup.MessageType.ERROR + ui = True + case 2: + type = Popup.MessageType.WARNING + case 1: + type = Popup.MessageType.INFO + case _: + type = Popup.MessageType.UNKNOWN + + self.popup.new_message(message_type=type, message=message, userInput=ui) + + self.build_model_list() + + def _add_notif_entry( + self, + message: str, + color: str = "#dfdfdf", + right_icon: QtGui.QPixmap | None = None, + ) -> None: + """Adds a new item to the list model""" + item = ListItem( + text=message, + left_icon=right_icon, + selected=False, + _lfontsize=17, + _rfontsize=12, + color=color, + height=80, + allow_expand=True, + notificate=False, + color_left_icon=True, + ) + time = QtCore.QDateTime.currentDateTime().toString("hh:mm:ss") + item._cache[-1] = time + self.model.add_item(item) + + def _setupUI(self) -> None: + """Setup UI for updatePage""" + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + ) + sizePolicy.setHorizontalStretch(1) + sizePolicy.setVerticalStretch(1) + font = QtGui.QFont() + font.setPointSize(20) + self.setSizePolicy(sizePolicy) + self.setObjectName("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) + self.update_page_content_layout.setContentsMargins(15, 15, 15, 15) + + self.header_content_layout = QtWidgets.QHBoxLayout() + self.header_content_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + self.spacer = QtWidgets.QSpacerItem( + 60, + 60, + QtWidgets.QSizePolicy.Policy.Fixed, + QtWidgets.QSizePolicy.Policy.Fixed, + ) + self.header_content_layout.addItem(self.spacer) + + self.header_title = QtWidgets.QLabel(self) + self.header_title.setMinimumSize(QtCore.QSize(100, 60)) + self.header_title.setMaximumSize(QtCore.QSize(16777215, 60)) + palette = self.header_title.palette() + palette.setColor(palette.ColorRole.WindowText, QtGui.QColor("#FFFFFF")) + self.header_title.setFont(font) + font.setPointSize(15) + self.header_title.setPalette(palette) + self.header_title.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) + self.header_title.setObjectName("header-title") + self.header_title.setText("Notification") + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.header_title.setSizePolicy(sizePolicy) + self.header_content_layout.addWidget( + self.header_title, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + ) + self.update_back_btn = IconButton(self) + self.update_back_btn.setMinimumSize(QtCore.QSize(60, 60)) + self.update_back_btn.setMaximumSize(QtCore.QSize(60, 60)) + self.update_back_btn.setFlat(True) + self.update_back_btn.setPixmap(QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) + self.header_content_layout.addWidget( + self.update_back_btn + ) # alignment=QtCore.Qt.AlignmentFlag.AlignCenter) + self.update_page_content_layout.addLayout(self.header_content_layout, 0) + + self.main_content_layout = QtWidgets.QHBoxLayout() + self.main_content_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.update_buttons_frame = BlocksCustomFrame(self) + + self.update_buttons_frame.setMinimumSize(QtCore.QSize(500, 380)) + self.update_buttons_frame.setMaximumSize(QtCore.QSize(560, 500)) + + self.update_buttons_list_widget = QtWidgets.QListView(self.update_buttons_frame) + self.update_buttons_list_widget.setMouseTracking(True) + self.update_buttons_list_widget.setTabletTracking(True) + + self.update_buttons_list_widget.setPalette(palette) + self.update_buttons_list_widget.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.update_buttons_list_widget.setStyleSheet("background-color:transparent") + self.update_buttons_list_widget.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + self.update_buttons_list_widget.setMinimumSize(self.update_buttons_frame.size()) + self.update_buttons_list_widget.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.update_buttons_list_widget.setVerticalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.update_buttons_list_widget.setHorizontalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.update_buttons_list_widget.setSizeAdjustPolicy( + QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents + ) + self.update_buttons_list_widget.setAutoScroll(False) + self.update_buttons_list_widget.setProperty("showDropIndicator", False) + self.update_buttons_list_widget.setDefaultDropAction( + QtCore.Qt.DropAction.IgnoreAction + ) + self.update_buttons_list_widget.setAlternatingRowColors(False) + self.update_buttons_list_widget.setSelectionMode( + QtWidgets.QAbstractItemView.SelectionMode.NoSelection + ) + self.update_buttons_list_widget.setSelectionBehavior( + QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems + ) + self.update_buttons_list_widget.setVerticalScrollMode( + QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel + ) + self.update_buttons_list_widget.setHorizontalScrollMode( + QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel + ) + QtWidgets.QScroller.grabGesture( + self.update_buttons_list_widget, + QtWidgets.QScroller.ScrollerGestureType.TouchGesture, + ) + QtWidgets.QScroller.grabGesture( + self.update_buttons_list_widget, + QtWidgets.QScroller.ScrollerGestureType.LeftMouseButtonGesture, + ) + self.update_buttons_layout = QtWidgets.QVBoxLayout() + self.update_buttons_layout.setContentsMargins(0, 0, 0, 0) + self.update_buttons_layout.addWidget(self.update_buttons_list_widget, 0) + self.update_buttons_frame.setLayout(self.update_buttons_layout) + + self.main_content_layout.addWidget(self.update_buttons_frame) + + self.vlayout = QtWidgets.QVBoxLayout() + self.vlayout.setContentsMargins(5, 5, 5, 5) + + self.info_frame = BlocksCustomFrame() + self.info_frame.setMinimumSize(QtCore.QSize(200, 150)) + + self.spacer_item = QtWidgets.QSpacerItem( + 20, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + + self.info_box_layout = QtWidgets.QGridLayout(self.info_frame) + self.info_box_layout.setContentsMargins(0, 0, 0, 0) + + self.info_box_layout.addItem(self.spacer_item, 0, 0) + + self.type_title = QtWidgets.QLabel(self.info_frame) + self.type_title.setText("Type:") + self.type_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.info_box_layout.addWidget(self.type_title, 1, 0) + + self.type_label = QtWidgets.QLabel(self.info_frame) + self.type_label.setText("N/A") + self.type_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.info_box_layout.addWidget(self.type_label, 1, 1) + + self.time_title = QtWidgets.QLabel(self.info_frame) + self.time_title.setText("Time:") + self.time_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.info_box_layout.addWidget(self.time_title, 2, 0) + + self.time_label = QtWidgets.QLabel(self.info_frame) + self.time_label.setText("N/A") + self.time_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.info_box_layout.addWidget(self.time_label, 2, 1) + + self.type_title.setFont(font) + self.type_title.setStyleSheet("color:#FFFFFF") + + self.time_title.setFont(font) + self.time_title.setStyleSheet("color:#FFFFFF") + + self.time_title.setFont(font) + self.type_label.setStyleSheet("color:#FFFFFF") + + self.time_title.setFont(font) + self.time_label.setStyleSheet("color:#FFFFFF") + + self.info_frame.setLayout(self.info_box_layout) + + self.buttons_frame = BlocksCustomFrame() + self.buttons_frame.setMinimumSize(QtCore.QSize(200, 200)) + self.buttons_frame.setMaximumSize(QtCore.QSize(300, 200)) + + self.button_box_layout = QtWidgets.QVBoxLayout() + self.button_box_layout.setContentsMargins(10, 10, 10, 10) + self.buttons_frame.setLayout(self.button_box_layout) + + self.button_box = QtWidgets.QVBoxLayout() + self.button_box.setContentsMargins(0, 0, 0, 0) + self.button_box.addSpacing(-1) + + self.button_box.addItem(self.spacer_item) + + self.delete_btn = BlocksCustomButton() + self.delete_btn.setMinimumSize(QtCore.QSize(200, 60)) + self.delete_btn.setMaximumSize(QtCore.QSize(300, 60)) + font.setPointSize(15) + + self.delete_btn.setFont(font) + self.delete_btn.setPalette(palette) + self.delete_btn.setSizePolicy(sizePolicy) + self.delete_btn.setText("Delete") + self.delete_btn.setEnabled(False) + self.delete_btn.setPixmap( + QtGui.QPixmap(":/ui/media/btn_icons/garbage-icon.svg") + ) + self.button_box.addWidget( + self.delete_btn, 0, QtCore.Qt.AlignmentFlag.AlignCenter + ) + + self.delete_all_btn = BlocksCustomButton() + self.delete_all_btn.setMinimumSize(QtCore.QSize(200, 60)) + self.delete_all_btn.setMaximumSize(QtCore.QSize(300, 60)) + font.setPointSize(15) + self.delete_all_btn.setFont(font) + self.delete_all_btn.setPalette(palette) + self.delete_all_btn.setSizePolicy(sizePolicy) + self.delete_all_btn.setText("Delete all") + self.delete_all_btn.setPixmap( + QtGui.QPixmap(":/ui/media/btn_icons/garbage-icon.svg") + ) + self.button_box.addWidget( + self.delete_all_btn, 0, QtCore.Qt.AlignmentFlag.AlignCenter + ) + + self.button_box_layout.addLayout( + self.button_box, + 0, + ) + + self.vlayout.addWidget(self.info_frame) + self.vlayout.addWidget(self.buttons_frame) + + self.main_content_layout.addLayout(self.vlayout) + self.update_page_content_layout.addLayout(self.main_content_layout, 1) + self.setLayout(self.update_page_content_layout) diff --git a/BlocksScreen/lib/panels/widgets/numpadPage.py b/BlocksScreen/lib/panels/widgets/numpadPage.py index 9674084d..b904645c 100644 --- a/BlocksScreen/lib/panels/widgets/numpadPage.py +++ b/BlocksScreen/lib/panels/widgets/numpadPage.py @@ -39,6 +39,10 @@ def __init__( self.numpad_back_btn.clicked.connect(self.back_button) self.start_glow_animation.connect(self.inserted_value.start_glow_animation) + def showEvent(self, a0: QtGui.QShowEvent | None) -> None: + self.firsttime = True + return super().showEvent(a0) + def value_inserted(self, value: str) -> None: """Handle number insertion on the numpad diff --git a/BlocksScreen/lib/panels/widgets/probeHelperPage.py b/BlocksScreen/lib/panels/widgets/probeHelperPage.py index 6ce968d3..fe2fa180 100644 --- a/BlocksScreen/lib/panels/widgets/probeHelperPage.py +++ b/BlocksScreen/lib/panels/widgets/probeHelperPage.py @@ -35,11 +35,17 @@ class ProbeHelper(QtWidgets.QWidget): ) call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") + toggle_conn_page = QtCore.pyqtSignal(bool, name="toggles-conn-panel") + + disable_popups: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + bool, name="disable-popups" + ) + 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[4]) + _zhop_height: float = float(distances[0]) card_options: dict = {} z_offset_method_type: str = "" z_offset_config_method: tuple = () @@ -85,6 +91,30 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: self.block_list = False self.target_temp = 0 self.current_temp = 0 + self._eddy_calibration_state = False + + @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: + """Handle print stats object update""" + if isinstance(value, str): + if "state" in field: + if value in ("standby"): + if self._eddy_calibration_state: + self.run_gcode_signal.emit("G28\nM400") + self._move_to_pos( + self.z_offset_safe_xy[0], self.z_offset_safe_xy[1], 100 + ) + self.call_load_panel.emit(True, "Almost done...\nPlease wait") + self.run_gcode_signal.emit(self._eddy_command) + + self.request_page_view.emit() + + self.disable_popups.emit(False) + self.toggle_conn_page.emit(True) + + self._eddy_calibration_state = False def on_klippy_status(self, state: str): """Handle Klippy status event change""" @@ -202,29 +232,28 @@ def on_object_config(self, config: dict | list) -> None: # BUG: If i don't add if not self.probe_config i'll just receive the configuration a bunch of times if isinstance(config, list): - ... - # if self.block_list: - # return - # else: - # self.block_list = True - - # _keys = [] - # if not isinstance(config, list): - # return - - # list(map(lambda item: _keys.extend(item.keys()), config)) - - # probe, *_ = config[0].items() - # self.z_offset_method_type = probe[0] # The one found first - # self.z_offset_method_config = ( - # probe[1], - # "PROBE_CALIBRATE", - # "Z_OFFSET_APPLY_PROBE", - # ) - # self.init_probe_config() - # if not _keys: - # return - # self._configure_option_cards(_keys) + if self.block_list: + return + else: + self.block_list = True + + _keys = [] + if not isinstance(config, list): + return + + list(map(lambda item: _keys.extend(item.keys()), config)) + + probe, *_ = config[0].items() + self.z_offset_method_type = probe[0] # The one found first + self.z_offset_method_config = ( + probe[1], + "PROBE_CALIBRATE", + "Z_OFFSET_APPLY_PROBE", + ) + self._init_probe_config() + if not _keys: + return + self._configure_option_cards(_keys) elif isinstance(config, dict): if config.get("stepper_z"): @@ -393,10 +422,6 @@ def handle_start_tool(self, sender: typing.Type[OptionCard]) -> None: for i in self.card_options.values(): i.setDisabled(True) - self.call_load_panel.emit(True, "Homing Axes...") - if self.z_offset_safe_xy: - self.run_gcode_signal.emit("G28\nM400") - self._move_to_pos(self.z_offset_safe_xy[0], self.z_offset_safe_xy[1], 100) self.helper_initialize = True _timer = QtCore.QTimer() _timer.setSingleShot(True) @@ -408,6 +433,26 @@ def handle_start_tool(self, sender: typing.Type[OptionCard]) -> None: _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 + self.call_load_panel.emit(True, "Preparing Eddy Current Calibration...") + self.toggle_conn_page.emit(False) + self.run_gcode_signal.emit( + f"LDC_CALIBRATE_DRIVE_CURRENT CHIP={sender.name.split(' ')[1]}" # type:ignore + ) + self.run_gcode_signal.emit("M400\nSAVE_CONFIG") + + self._eddy_command = _cmd + self._eddy_calibration_state = True + return + else: + if self.z_offset_safe_xy: + self.call_load_panel.emit(True, "Homing Axes...") + self._move_to_pos( + self.z_offset_safe_xy[0], self.z_offset_safe_xy[1], 100 + ) self.run_gcode_signal.emit(_cmd) @QtCore.pyqtSlot(str, str, float, name="on_extruder_update") @@ -417,6 +462,8 @@ def on_extruder_update( """Handle extruder update""" if not self.helper_initialize: return + if self._eddy_calibration_state: + return if self.target_temp != 0: if self.current_temp == self.target_temp: if self.isVisible: @@ -481,14 +528,18 @@ def on_manual_probe_update(self, update: dict) -> None: # if update.get("z_position_lower"): # f"{update.get('z_position_lower'):.4f} mm" - if update.get("is_active"): - if not self.isVisible(): - self.request_page_view.emit() - - self.helper_initialize = False - self.helper_start = True + 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() - self._toggle_tool_buttons(True) + else: + self._show_option_cards() if update.get("z_position_upper"): self.old_offset_info.setText(f"{update.get('z_position_upper'):.4f} mm") diff --git a/BlocksScreen/lib/printer.py b/BlocksScreen/lib/printer.py index 5889c19d..c6c76fbc 100644 --- a/BlocksScreen/lib/printer.py +++ b/BlocksScreen/lib/printer.py @@ -7,7 +7,7 @@ from lib.moonrakerComm import MoonWebSocket from PyQt6 import QtCore, QtWidgets -logger = logging.getLogger(name="logs/BlocksScreen.logs") +logger = logging.getLogger(__name__) class Printer(QtCore.QObject): @@ -511,7 +511,7 @@ def send_print_event(self, event: str): _print_state_upper = event[0].upper() _print_state_call = f"{_print_state_upper}{event[1:]}" if hasattr(events, f"Print{_print_state_call}"): - logging.debug( + logger.debug( "Print Event Caught, print is %s, calling event %s", _print_state_call, f"Print{_print_state_call}", diff --git a/BlocksScreen/lib/qrcode_gen.py b/BlocksScreen/lib/qrcode_gen.py index 1901cef1..1840f0ae 100644 --- a/BlocksScreen/lib/qrcode_gen.py +++ b/BlocksScreen/lib/qrcode_gen.py @@ -1,14 +1,17 @@ import qrcode -from PIL import ImageQt + +from PyQt6.QtGui import QImage, QColor, QPainter +from PyQt6.QtCore import Qt BLOCKS_URL = "https://blockstec.com" RF50_MANUAL_PAGE = "https://blockstec.com/RF50" RF50_PRODUCT_PAGE = "https://blockstec.com/rf-50" RF50_DATASHEET_PAGE = "https://www.blockstec.com/assets/downloads/rf50_datasheet.pdf" -RF50_DATASHEET_PAGE = "https://blockstec.com/assets/files/rf50_user_manual.pdf" +RF50_USER_MANUAL_PAGE = "https://blockstec.com/assets/files/rf50_user_manual.pdf" -def make_qrcode(data) -> ImageQt.ImageQt: +def make_qrcode(data: str) -> QImage: + """Generate a QR code image from *data* and return it as a QImage.""" qr = qrcode.QRCode( version=1, error_correction=qrcode.ERROR_CORRECT_L, @@ -17,16 +20,46 @@ def make_qrcode(data) -> ImageQt.ImageQt: ) qr.add_data(data) qr.make(fit=True) - img = qr.make_image(fill_color="black", back_color="white") - pil_image = img.get_image() - pil_image.show() - return pil_image.toqimage() + + matrix = qr.get_matrix() + box_size = 10 + size = len(matrix) * box_size + + image = QImage(size, size, QImage.Format.Format_RGB32) + image.fill(QColor("white")) + + painter = QPainter(image) + painter.setPen(Qt.PenStyle.NoPen) + painter.setBrush(QColor("black")) + + for y, row in enumerate(matrix): + for x, cell in enumerate(row): + if cell: + painter.drawRect(x * box_size, y * box_size, box_size, box_size) + + painter.end() + return image + + +_NM_TO_WIFI_QR_AUTH: dict[str, str] = { + "wpa-psk": "WPA", + "wpa2-psk": "WPA", + "sae": "WPA", + "wep": "WEP", + "open": "nopass", + "nopass": "nopass", + "owe": "nopass", +} def generate_wifi_qrcode( ssid: str, password: str, auth_type: str, hidden: bool = False -) -> ImageQt.ImageQt: - wifi_data = ( - f"WIFI:T:{auth_type};S:{ssid};P:{password};{'H:true;' if hidden else ''};" - ) +) -> QImage: + """Build a Wi-Fi QR code for the given SSID/password/auth combination. + + *auth_type* is a NetworkManager key-mgmt value (e.g. ``"wpa-psk"``, + ``"sae"``). Unknown values default to WPA. + """ + qr_auth = _NM_TO_WIFI_QR_AUTH.get(auth_type.lower(), "WPA") + wifi_data = f"WIFI:T:{qr_auth};S:{ssid};P:{password};H:{str(hidden).lower()};;" return make_qrcode(wifi_data) diff --git a/BlocksScreen/lib/ui/connectionWindow.ui b/BlocksScreen/lib/ui/connectionWindow.ui index f2bd4899..e495a380 100644 --- a/BlocksScreen/lib/ui/connectionWindow.ui +++ b/BlocksScreen/lib/ui/connectionWindow.ui @@ -574,6 +574,105 @@ background-image: url(:/background/media/1st_background.png); + + + + + 0 + 0 + + + + + 100 + 80 + + + + + 100 + 80 + + + + + 80 + 80 + + + + + + + + + + + + 8 + + + + true + + + Qt::ClickFocus + + + false + + + + + + Notifications + + + + :/system_icons/media/btn_icons/retry_connection.svg:/system_icons/media/btn_icons/retry_connection.svg + + + + 16 + 16 + + + + false + + + 0 + + + 0 + + + false + + + false + + + true + + + bottom + + + :/ui/media/btn_icons/notification.svg + + + + 255 + 255 + 255 + + + + true + + + diff --git a/BlocksScreen/lib/ui/connectionWindow_ui.py b/BlocksScreen/lib/ui/connectionWindow_ui.py index 772dc227..ccb61ae3 100644 --- a/BlocksScreen/lib/ui/connectionWindow_ui.py +++ b/BlocksScreen/lib/ui/connectionWindow_ui.py @@ -200,6 +200,37 @@ def setupUi(self, ConnectivityForm): self.RetryConnectionButton.setProperty("has_text", True) self.RetryConnectionButton.setObjectName("RetryConnectionButton") self.horizontalLayout.addWidget(self.RetryConnectionButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) + self.notification_btn = IconButton(parent=self.cw_buttonFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.notification_btn.sizePolicy().hasHeightForWidth()) + self.notification_btn.setSizePolicy(sizePolicy) + self.notification_btn.setMinimumSize(QtCore.QSize(100, 80)) + self.notification_btn.setMaximumSize(QtCore.QSize(100, 80)) + self.notification_btn.setBaseSize(QtCore.QSize(80, 80)) + palette = QtGui.QPalette() + self.notification_btn.setPalette(palette) + font = QtGui.QFont() + font.setPointSize(8) + self.notification_btn.setFont(font) + self.notification_btn.setTabletTracking(True) + self.notification_btn.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) + self.notification_btn.setAutoFillBackground(False) + self.notification_btn.setStyleSheet("") + self.notification_btn.setIcon(icon2) + self.notification_btn.setIconSize(QtCore.QSize(16, 16)) + self.notification_btn.setCheckable(False) + self.notification_btn.setAutoRepeatDelay(0) + self.notification_btn.setAutoRepeatInterval(0) + self.notification_btn.setAutoDefault(False) + self.notification_btn.setDefault(False) + self.notification_btn.setFlat(True) + self.notification_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/notification.svg")) + self.notification_btn.setProperty("text_color", QtGui.QColor(255, 255, 255)) + self.notification_btn.setProperty("has_text", True) + self.notification_btn.setObjectName("notification_btn") + self.horizontalLayout.addWidget(self.notification_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) self.updatepageButton = IconButton(parent=self.cw_buttonFrame) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -329,6 +360,8 @@ def retranslateUi(self, ConnectivityForm): self.FirmwareRestartButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) self.RetryConnectionButton.setText(_translate("ConnectivityForm", "Retry ")) self.RetryConnectionButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) + self.notification_btn.setText(_translate("ConnectivityForm", "Notifications")) + self.notification_btn.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) self.updatepageButton.setText(_translate("ConnectivityForm", "Update page")) self.updatepageButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) self.wifi_button.setText(_translate("ConnectivityForm", "Wifi Settings")) diff --git a/BlocksScreen/lib/ui/mainWindow.ui b/BlocksScreen/lib/ui/mainWindow.ui index 0cb579cc..a18f2fd5 100644 --- a/BlocksScreen/lib/ui/mainWindow.ui +++ b/BlocksScreen/lib/ui/mainWindow.ui @@ -235,7 +235,7 @@ QTabBar::tab{ QTabWidget::Rounded - 2 + 0 @@ -512,7 +512,7 @@ QPushButton:pressed{ false - + 0 @@ -531,64 +531,6 @@ QPushButton:pressed{ 0 - - - - - 1 - 1 - - - - - 60 - 60 - - - - - 90 - 90 - - - - - 1 - 1 - - - - - 60 - 60 - - - - QFrame::StyledPanel - - - QFrame::Plain - - - 0 - - - Qt::ScrollBarAlwaysOff - - - Qt::ScrollBarAlwaysOff - - - QAbstractScrollArea::AdjustToContents - - - QPainter::Antialiasing|QPainter::SmoothPixmapTransform - - - QGraphicsView::SmartViewportUpdate - - - @@ -1278,6 +1220,46 @@ QPushButton:pressed{ nozzle_size_icon + + + + + 1 + 1 + + + + + 60 + 60 + + + + + 60 + 60 + + + + + + + + 60 + 60 + + + + true + + + :/ui/media/btn_icons/notification.svg + + + icon_text + + + @@ -1348,13 +1330,12 @@ QPushButton:pressed{ NotificationQTabWidget QTabWidget -
lib.utils.tabwidget_test
+
lib.utils.blocks_tabwidget
1
- diff --git a/BlocksScreen/lib/ui/mainWindow_ui.py b/BlocksScreen/lib/ui/mainWindow_ui.py index ec3f2166..3f309922 100644 --- a/BlocksScreen/lib/ui/mainWindow_ui.py +++ b/BlocksScreen/lib/ui/mainWindow_ui.py @@ -1,4 +1,4 @@ -# Form implementation generated from reading ui file 'BlocksScreen/lib/ui/mainWindow.ui' +# Form implementation generated from reading ui file '/home/levi/BlocksScreen/BlocksScreen/lib/ui/mainWindow.ui' # # Created by: PyQt6 UI code generator 6.7.1 # @@ -226,26 +226,6 @@ def setupUi(self, MainWindow): self.header_main_layout.setContentsMargins(0, 0, 0, 0) self.header_main_layout.setSpacing(0) self.header_main_layout.setObjectName("header_main_layout") - self.header_image_logo = QtWidgets.QGraphicsView(parent=self.main_header_layout) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) - sizePolicy.setHorizontalStretch(1) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.header_image_logo.sizePolicy().hasHeightForWidth()) - self.header_image_logo.setSizePolicy(sizePolicy) - self.header_image_logo.setMinimumSize(QtCore.QSize(60, 60)) - self.header_image_logo.setMaximumSize(QtCore.QSize(90, 90)) - self.header_image_logo.setSizeIncrement(QtCore.QSize(1, 1)) - self.header_image_logo.setBaseSize(QtCore.QSize(60, 60)) - self.header_image_logo.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.header_image_logo.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) - self.header_image_logo.setLineWidth(0) - self.header_image_logo.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - self.header_image_logo.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - self.header_image_logo.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents) - self.header_image_logo.setRenderHints(QtGui.QPainter.RenderHint.Antialiasing|QtGui.QPainter.RenderHint.SmoothPixmapTransform) - self.header_image_logo.setViewportUpdateMode(QtWidgets.QGraphicsView.ViewportUpdateMode.SmartViewportUpdate) - self.header_image_logo.setObjectName("header_image_logo") - self.header_main_layout.addWidget(self.header_image_logo, 0, QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop) self.header_display_layout = QtWidgets.QFrame(parent=self.main_header_layout) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) sizePolicy.setHorizontalStretch(1) @@ -481,6 +461,20 @@ def setupUi(self, MainWindow): self.bed_temp_display.raise_() self.nozzle_size_icon.raise_() self.header_main_layout.addWidget(self.header_display_layout, 0, QtCore.Qt.AlignmentFlag.AlignVCenter) + self.notification_btn = IconButton(parent=self.main_header_layout) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(1) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.notification_btn.sizePolicy().hasHeightForWidth()) + self.notification_btn.setSizePolicy(sizePolicy) + self.notification_btn.setMinimumSize(QtCore.QSize(60, 60)) + self.notification_btn.setMaximumSize(QtCore.QSize(60, 60)) + self.notification_btn.setText("") + self.notification_btn.setIconSize(QtCore.QSize(60, 60)) + self.notification_btn.setFlat(True) + self.notification_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/notification.svg")) + self.notification_btn.setObjectName("notification_btn") + self.header_main_layout.addWidget(self.notification_btn) self.wifi_button = IconButton(parent=self.main_header_layout) self.wifi_button.setEnabled(True) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) @@ -499,11 +493,11 @@ def setupUi(self, MainWindow): self.wifi_button.setProperty("icon_pixmap", QtGui.QPixmap(":/network/media/btn_icons/3bar_wifi.svg")) self.wifi_button.setObjectName("wifi_button") self.header_main_layout.addWidget(self.wifi_button, 0, QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTop) - self.header_main_layout.setStretch(1, 2) + self.header_main_layout.setStretch(0, 2) MainWindow.setCentralWidget(self.main_widget) self.retranslateUi(MainWindow) - self.main_content_widget.setCurrentIndex(2) + self.main_content_widget.setCurrentIndex(0) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): @@ -516,7 +510,8 @@ def retranslateUi(self, MainWindow): self.nozzle_size_icon.setProperty("button_type", _translate("MainWindow", "icon_text")) self.bed_temp_display.setProperty("name", _translate("MainWindow", "bed_temperature_display")) self.bed_temp_display.setProperty("button_type", _translate("MainWindow", "secondary_display")) + self.notification_btn.setProperty("button_type", _translate("MainWindow", "icon_text")) self.wifi_button.setProperty("button_type", _translate("MainWindow", "icon")) +from lib.utils.blocks_tabwidget import NotificationQTabWidget from lib.utils.display_button import DisplayButton from lib.utils.icon_button import IconButton -from lib.utils.blocks_tabwidget import NotificationQTabWidget diff --git a/BlocksScreen/lib/ui/resources/icon_resources.qrc b/BlocksScreen/lib/ui/resources/icon_resources.qrc index 5239a1fd..4acd3718 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources.qrc +++ b/BlocksScreen/lib/ui/resources/icon_resources.qrc @@ -1,20 +1,19 @@ - media/btn_icons/0bar_wifi.svg - media/btn_icons/0bar_wifi_protected.svg - media/btn_icons/1bar_wifi.svg - media/btn_icons/1bar_wifi_protected.svg - media/btn_icons/2bar_wifi.svg - media/btn_icons/2bar_wifi_protected.svg - media/btn_icons/3bar_wifi.svg - media/btn_icons/3bar_wifi_protected.svg - media/btn_icons/4bar_wifi.svg - media/btn_icons/4bar_wifi_protected.svg + media/btn_icons/network/static_ip.svg media/btn_icons/wifi_config.svg - media/btn_icons/wifi_locked.svg - media/btn_icons/wifi_unlocked.svg + media/btn_icons/network/0bar_wifi.svg + media/btn_icons/network/0bar_wifi_protected.svg + media/btn_icons/network/1bar_wifi.svg + media/btn_icons/network/1bar_wifi_protected.svg + media/btn_icons/network/2bar_wifi.svg + media/btn_icons/network/2bar_wifi_protected.svg + media/btn_icons/network/3bar_wifi.svg + media/btn_icons/network/3bar_wifi_protected.svg + media/btn_icons/network/4bar_wifi.svg + media/btn_icons/network/4bar_wifi_protected.svg + media/btn_icons/network/ethernet_connected.svg media/btn_icons/hotspot.svg - media/btn_icons/no_wifi.svg media/btn_icons/retry_wifi.svg @@ -91,6 +90,9 @@ media/btn_icons/unload_filament.svg + media/btn_icons/notification.svg + media/btn_icons/notification_active.svg + media/btn_icons/usb_icon.svg media/btn_icons/garbage-icon.svg media/btn_icons/back.svg media/btn_icons/refresh.svg @@ -175,6 +177,8 @@ media/btn_icons/input_shaper_manual_Y.svg + media/btn_icons/arrow_down.svg + media/btn_icons/arrow_right.svg media/btn_icons/left_arrow.svg media/btn_icons/right_arrow.svg media/btn_icons/down_arrow.svg diff --git a/BlocksScreen/lib/ui/resources/icon_resources_rc.py b/BlocksScreen/lib/ui/resources/icon_resources_rc.py index a7aca514..45c93d8f 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources_rc.py +++ b/BlocksScreen/lib/ui/resources/icon_resources_rc.py @@ -1054,6 +1054,34 @@ \x30\x39\x2e\x31\x35\x2c\x34\x37\x39\x2e\x37\x32\x2c\x33\x30\x34\ \x2e\x34\x35\x2c\x34\x38\x33\x2e\x35\x33\x2c\x33\x30\x30\x2c\x34\ \x38\x37\x2e\x35\x36\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x01\x96\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\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\x6e\x6f\x6e\x65\x3b\x0a\x20\x20\ +\x20\x20\x20\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x3a\x20\x23\x65\ +\x30\x65\x30\x64\x66\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x73\ +\x74\x72\x6f\x6b\x65\x2d\x6d\x69\x74\x65\x72\x6c\x69\x6d\x69\x74\ +\x3a\x20\x31\x30\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x73\x74\ +\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\x3a\x20\x34\x33\x70\x78\ +\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\x39\x38\x2e\x32\ +\x37\x2c\x32\x33\x38\x2e\x35\x6c\x2d\x39\x35\x2e\x36\x38\x2c\x31\ +\x32\x34\x2e\x38\x36\x63\x2d\x2e\x36\x31\x2e\x36\x31\x2d\x31\x2e\ +\x36\x2e\x35\x39\x2d\x32\x2e\x32\x32\x2d\x2e\x30\x33\x6c\x2d\x39\ +\x38\x2e\x36\x34\x2d\x31\x32\x37\x2e\x31\x34\x22\x2f\x3e\x0a\x3c\ +\x2f\x73\x76\x67\x3e\ \x00\x00\x02\x78\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -1138,6 +1166,34 @@ \x35\x2c\x31\x31\x36\x2e\x34\x37\x2c\x32\x39\x35\x2e\x35\x35\x2c\ \x31\x31\x32\x2e\x34\x34\x2c\x33\x30\x30\x2e\x30\x35\x5a\x22\x2f\ \x3e\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x01\x94\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\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\x6e\x6f\x6e\x65\x3b\x0a\x20\x20\ +\x20\x20\x20\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x3a\x20\x23\x65\ +\x30\x65\x30\x64\x66\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x73\ +\x74\x72\x6f\x6b\x65\x2d\x6d\x69\x74\x65\x72\x6c\x69\x6d\x69\x74\ +\x3a\x20\x31\x30\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x73\x74\ +\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\x3a\x20\x34\x33\x70\x78\ +\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\x32\x33\x38\x2e\x35\ +\x2c\x32\x30\x31\x2e\x37\x33\x6c\x31\x32\x34\x2e\x38\x36\x2c\x39\ +\x35\x2e\x36\x38\x63\x2e\x36\x31\x2e\x36\x31\x2e\x35\x39\x2c\x31\ +\x2e\x36\x2d\x2e\x30\x33\x2c\x32\x2e\x32\x32\x6c\x2d\x31\x32\x37\ +\x2e\x31\x34\x2c\x39\x38\x2e\x36\x34\x22\x2f\x3e\x0a\x3c\x2f\x73\ +\x76\x67\x3e\ \x00\x00\x00\xfd\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -11607,252 +11663,289 @@ \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\x31\ +\x00\x00\x05\xdd\ \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\x34\x30\x36\x2e\x38\x36\x2c\x32\x31\ -\x31\x2e\x32\x35\x63\x2d\x32\x31\x2e\x35\x37\x2d\x32\x2e\x31\x38\ -\x2d\x34\x31\x2e\x34\x35\x2e\x38\x33\x2d\x36\x30\x2e\x33\x31\x2c\ -\x39\x2e\x36\x37\x61\x38\x34\x2e\x31\x35\x2c\x38\x34\x2e\x31\x35\ -\x2c\x30\x2c\x30\x2c\x30\x2d\x33\x37\x2e\x39\x31\x2c\x33\x34\x2e\ -\x32\x32\x63\x2d\x32\x2e\x39\x31\x2c\x35\x2d\x32\x2e\x39\x34\x2c\ -\x38\x2e\x33\x36\x2c\x31\x2e\x36\x32\x2c\x31\x32\x2e\x36\x2c\x39\ -\x2e\x37\x34\x2c\x39\x2e\x30\x35\x2c\x39\x2e\x33\x39\x2c\x39\x2e\ -\x30\x39\x2c\x32\x31\x2e\x33\x31\x2c\x34\x2e\x34\x31\x2c\x33\x36\ -\x2e\x35\x35\x2d\x31\x34\x2e\x33\x36\x2c\x36\x38\x2e\x32\x39\x2d\ -\x34\x2e\x31\x38\x2c\x39\x38\x2e\x33\x32\x2c\x31\x38\x2e\x33\x32\ -\x2c\x31\x30\x2e\x38\x39\x2c\x38\x2e\x31\x36\x2c\x31\x30\x2e\x32\ -\x32\x2c\x31\x37\x2e\x35\x2c\x38\x2e\x36\x32\x2c\x32\x38\x2e\x35\ -\x31\x71\x2d\x37\x2e\x38\x35\x2c\x35\x33\x2e\x36\x34\x2d\x34\x39\ -\x2e\x31\x35\x2c\x38\x38\x2e\x35\x35\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\x38\x2d\x2e\x31\x33\ -\x2d\x31\x2e\x31\x31\x2d\x2e\x33\x32\x2c\x32\x2e\x31\x36\x2d\x32\ -\x30\x2e\x36\x39\x2e\x31\x31\x2d\x34\x31\x2d\x38\x2e\x35\x37\x2d\ -\x36\x30\x2e\x31\x39\x2d\x37\x2e\x36\x35\x2d\x31\x36\x2e\x39\x34\ -\x2d\x31\x39\x2e\x33\x32\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\x32\ -\x39\x2c\x31\x2e\x32\x32\x2d\x39\x2e\x31\x37\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\x36\x2c\x33\x33\x2e\ -\x34\x37\x2c\x32\x2e\x32\x32\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\x2c\x31\x32\x2e\x33\x34\x2d\x33\x33\x2e\x39\x32\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\x36\x33\x2c\x31\x37\x2e\ -\x36\x33\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x2d\x32\x2e\x38\ -\x34\x2c\x31\x31\x37\x2e\x39\x32\x2c\x31\x31\x37\x2e\x39\x32\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\x35\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\x35\x2d\x31\x32\x2e\x37\x31\x2d\x39\x2e\ -\x36\x2d\x39\x2e\x34\x32\x2d\x39\x2e\x33\x38\x2d\x39\x2e\x34\x34\ -\x2d\x32\x32\x2d\x34\x2e\x33\x37\x43\x32\x33\x32\x2e\x38\x2c\x33\ -\x34\x31\x2e\x38\x38\x2c\x32\x30\x32\x2e\x33\x34\x2c\x33\x33\x32\ -\x2e\x32\x2c\x31\x37\x33\x2c\x33\x31\x32\x63\x2d\x31\x33\x2e\x34\ -\x39\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\x35\x2d\x33\x34\x2e\x32\x2c\x32\x31\x2e\x39\x31\x2d\x36\x32\ -\x2e\x34\x36\x2c\x34\x38\x2e\x32\x34\x2d\x38\x35\x2c\x2e\x36\x39\ -\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\x33\x2c\x32\ -\x35\x2e\x31\x31\x2c\x31\x2e\x36\x35\x2c\x33\x37\x2e\x30\x37\x2c\ -\x35\x2e\x33\x35\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\x31\x2c\x32\x2e\x37\x2c\x37\x2e\x32\x36\x2c\x33\x2c\ -\x31\x31\x2e\x31\x38\x2d\x31\x2e\x31\x35\x2c\x39\x2e\x37\x38\x2d\ -\x31\x30\x2e\x32\x36\x2c\x39\x2e\x36\x39\x2d\x39\x2e\x39\x2c\x34\ -\x2e\x37\x39\x2d\x32\x33\x2e\x33\x35\x2d\x31\x32\x2e\x33\x31\x2d\ -\x33\x33\x2e\x37\x38\x2d\x33\x2e\x31\x36\x2d\x36\x33\x2e\x36\x35\ -\x2c\x31\x37\x2d\x39\x31\x2e\x36\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\x36\ -\x2e\x30\x35\x2c\x32\x30\x38\x2e\x35\x36\x2c\x34\x30\x36\x2e\x31\ -\x35\x2c\x32\x30\x39\x2e\x34\x2c\x34\x30\x36\x2e\x38\x36\x2c\x32\ -\x31\x31\x2e\x32\x35\x5a\x4d\x32\x39\x39\x2e\x37\x36\x2c\x33\x31\ -\x37\x2e\x34\x32\x63\x33\x2e\x38\x2e\x31\x31\x2c\x31\x37\x2e\x32\ -\x36\x2d\x31\x33\x2e\x31\x36\x2c\x31\x37\x2e\x34\x37\x2d\x31\x37\ -\x2e\x32\x31\x73\x2d\x31\x32\x2e\x36\x34\x2d\x31\x37\x2e\x32\x39\ -\x2d\x31\x37\x2d\x31\x37\x2e\x35\x33\x63\x2d\x33\x2e\x37\x31\x2d\ -\x2e\x31\x39\x2d\x31\x37\x2e\x34\x33\x2c\x31\x33\x2e\x31\x38\x2d\ -\x31\x37\x2e\x35\x34\x2c\x31\x37\x2e\x31\x31\x53\x32\x39\x35\x2e\ -\x36\x36\x2c\x33\x31\x37\x2e\x33\x2c\x32\x39\x39\x2e\x37\x36\x2c\ -\x33\x31\x37\x2e\x34\x32\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\ -\x00\x00\x09\xd1\ +\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\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\ \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\x34\x35\x36\x2e\x31\x37\x2c\x33\x31\ -\x37\x2e\x38\x35\x63\x2e\x34\x36\x2d\x31\x31\x2e\x38\x39\x2c\x35\ -\x2e\x31\x38\x2d\x31\x37\x2e\x34\x32\x2c\x31\x34\x2e\x38\x36\x2d\ -\x31\x36\x2e\x37\x39\x2c\x31\x30\x2e\x37\x35\x2e\x36\x39\x2c\x31\ -\x33\x2e\x32\x34\x2c\x37\x2e\x37\x37\x2c\x31\x33\x2e\x32\x32\x2c\ -\x31\x37\x2e\x32\x2d\x2e\x31\x31\x2c\x36\x36\x2c\x30\x2c\x31\x33\ -\x32\x2d\x2e\x31\x2c\x31\x39\x38\x2c\x30\x2c\x31\x31\x2d\x35\x2c\ -\x31\x36\x2e\x35\x32\x2d\x31\x34\x2e\x31\x38\x2c\x31\x36\x2e\x32\ -\x2d\x31\x30\x2d\x2e\x33\x34\x2d\x31\x34\x2e\x34\x34\x2d\x36\x2e\ -\x35\x31\x2d\x31\x33\x2e\x35\x39\x2d\x31\x35\x2e\x36\x2c\x31\x2e\ -\x30\x35\x2d\x31\x31\x2e\x32\x32\x2d\x33\x2e\x33\x38\x2d\x31\x33\ -\x2e\x36\x32\x2d\x31\x34\x2d\x31\x33\x2e\x35\x34\x2d\x36\x34\x2e\ -\x34\x39\x2e\x34\x37\x2d\x31\x32\x39\x2c\x2e\x33\x38\x2d\x31\x39\ -\x33\x2e\x34\x39\x2e\x31\x32\x2d\x34\x33\x2e\x36\x33\x2d\x2e\x31\ -\x37\x2d\x38\x31\x2e\x35\x2d\x31\x35\x2e\x36\x2d\x31\x31\x34\x2e\ -\x32\x33\x2d\x34\x34\x2e\x33\x32\x61\x31\x39\x32\x2e\x31\x38\x2c\ -\x31\x39\x32\x2e\x31\x38\x2c\x30\x2c\x30\x2c\x30\x2d\x32\x32\x2e\ -\x34\x34\x2d\x31\x36\x2e\x38\x39\x41\x32\x30\x34\x2e\x38\x32\x2c\ -\x32\x30\x34\x2e\x38\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x32\x2e\ -\x33\x32\x2c\x32\x33\x33\x2e\x38\x36\x43\x34\x34\x2e\x31\x31\x2c\ -\x31\x31\x35\x2e\x36\x39\x2c\x31\x36\x35\x2c\x34\x31\x2e\x37\x2c\ -\x32\x38\x30\x2e\x34\x34\x2c\x37\x35\x2e\x38\x37\x2c\x33\x36\x32\ -\x2e\x32\x35\x2c\x31\x30\x30\x2e\x30\x38\x2c\x34\x32\x30\x2e\x32\ -\x38\x2c\x31\x37\x31\x2e\x37\x37\x2c\x34\x32\x35\x2c\x32\x35\x37\ -\x63\x31\x2e\x33\x32\x2c\x32\x33\x2e\x36\x35\x2d\x32\x2e\x32\x2c\ -\x34\x37\x2e\x35\x36\x2d\x33\x2e\x35\x33\x2c\x37\x31\x2e\x38\x38\ -\x68\x33\x34\x2e\x30\x38\x43\x34\x35\x35\x2e\x38\x32\x2c\x33\x32\ -\x34\x2e\x34\x38\x2c\x34\x35\x36\x2c\x33\x32\x31\x2e\x31\x36\x2c\ -\x34\x35\x36\x2e\x31\x37\x2c\x33\x31\x37\x2e\x38\x35\x5a\x4d\x32\ -\x32\x33\x2e\x37\x38\x2c\x39\x36\x43\x31\x32\x37\x2c\x39\x35\x2e\ -\x35\x36\x2c\x34\x36\x2e\x36\x34\x2c\x31\x37\x36\x2c\x34\x37\x2e\ -\x35\x39\x2c\x32\x37\x32\x2e\x32\x32\x63\x2e\x39\x34\x2c\x39\x35\ -\x2e\x38\x34\x2c\x37\x39\x2e\x34\x31\x2c\x31\x37\x33\x2e\x39\x34\ -\x2c\x31\x37\x34\x2e\x37\x38\x2c\x31\x37\x33\x2e\x39\x34\x61\x31\ -\x37\x35\x2e\x32\x38\x2c\x31\x37\x35\x2e\x32\x38\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x31\x37\x35\x2e\x34\x34\x2d\x31\x37\x35\x2e\x34\x43\ -\x33\x39\x37\x2e\x38\x31\x2c\x31\x37\x35\x2e\x37\x32\x2c\x33\x31\ -\x38\x2e\x38\x36\x2c\x39\x36\x2e\x34\x34\x2c\x32\x32\x33\x2e\x37\ -\x38\x2c\x39\x36\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\ -\x33\x33\x32\x2e\x32\x35\x2c\x31\x37\x39\x2e\x39\x34\x63\x2d\x32\ -\x30\x2e\x34\x39\x2d\x32\x2e\x30\x37\x2d\x33\x39\x2e\x33\x35\x2e\ -\x37\x39\x2d\x35\x37\x2e\x32\x36\x2c\x39\x2e\x31\x38\x61\x38\x30\ -\x2c\x38\x30\x2c\x30\x2c\x30\x2c\x30\x2d\x33\x36\x2c\x33\x32\x2e\ -\x34\x38\x63\x2d\x32\x2e\x37\x36\x2c\x34\x2e\x37\x31\x2d\x32\x2e\ -\x37\x39\x2c\x37\x2e\x39\x33\x2c\x31\x2e\x35\x34\x2c\x31\x32\x2c\ -\x39\x2e\x32\x34\x2c\x38\x2e\x35\x39\x2c\x38\x2e\x39\x31\x2c\x38\ -\x2e\x36\x33\x2c\x32\x30\x2e\x32\x33\x2c\x34\x2e\x31\x38\x2c\x33\ -\x34\x2e\x36\x39\x2d\x31\x33\x2e\x36\x33\x2c\x36\x34\x2e\x38\x32\ -\x2d\x34\x2c\x39\x33\x2e\x33\x32\x2c\x31\x37\x2e\x34\x2c\x31\x30\ -\x2e\x33\x34\x2c\x37\x2e\x37\x34\x2c\x39\x2e\x37\x31\x2c\x31\x36\ -\x2e\x36\x31\x2c\x38\x2e\x31\x38\x2c\x32\x37\x2e\x30\x36\x71\x2d\ -\x37\x2e\x34\x34\x2c\x35\x30\x2e\x39\x33\x2d\x34\x36\x2e\x36\x35\ -\x2c\x38\x34\x2e\x30\x35\x61\x37\x2e\x36\x31\x2c\x37\x2e\x36\x31\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x38\x36\x2c\x31\x63\x2d\x2e\ -\x31\x37\x2e\x30\x38\x2d\x2e\x34\x36\x2d\x2e\x31\x32\x2d\x31\x2e\ -\x30\x36\x2d\x2e\x33\x31\x2c\x32\x2e\x30\x36\x2d\x31\x39\x2e\x36\ -\x34\x2e\x31\x31\x2d\x33\x38\x2e\x38\x38\x2d\x38\x2e\x31\x33\x2d\ -\x35\x37\x2e\x31\x33\x2d\x37\x2e\x32\x36\x2d\x31\x36\x2e\x30\x38\ -\x2d\x31\x38\x2e\x33\x34\x2d\x32\x38\x2e\x38\x32\x2d\x33\x33\x2e\ -\x38\x2d\x33\x37\x2e\x36\x35\x2d\x33\x2e\x34\x37\x2d\x32\x2d\x35\ -\x2e\x34\x39\x2d\x31\x2e\x35\x33\x2d\x38\x2e\x38\x32\x2c\x31\x2e\ -\x31\x36\x2d\x38\x2e\x37\x2c\x37\x2d\x39\x2e\x36\x33\x2c\x31\x33\ -\x2e\x31\x31\x2d\x35\x2e\x31\x33\x2c\x32\x34\x2e\x33\x32\x2c\x31\ -\x32\x2e\x37\x39\x2c\x33\x31\x2e\x37\x37\x2c\x32\x2e\x31\x32\x2c\ -\x36\x30\x2e\x32\x32\x2d\x31\x37\x2e\x30\x38\x2c\x38\x36\x2e\x35\ -\x31\x2d\x37\x2e\x33\x31\x2c\x31\x30\x2d\x31\x35\x2e\x34\x33\x2c\ -\x31\x34\x2e\x30\x37\x2d\x32\x38\x2e\x35\x37\x2c\x31\x31\x2e\x37\ -\x31\x2d\x33\x32\x2e\x31\x39\x2d\x35\x2e\x37\x38\x2d\x35\x39\x2d\ -\x32\x30\x2e\x32\x33\x2d\x38\x30\x2e\x36\x39\x2d\x34\x34\x2e\x35\ -\x31\x61\x31\x37\x2e\x32\x2c\x31\x37\x2e\x32\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x31\x2e\x34\x33\x2d\x32\x2e\x37\x2c\x31\x31\x31\x2e\x38\ -\x2c\x31\x31\x31\x2e\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x31\x2e\ -\x34\x34\x2d\x33\x2e\x34\x36\x63\x32\x32\x2e\x31\x32\x2d\x36\x2e\ -\x31\x34\x2c\x33\x39\x2e\x38\x33\x2d\x31\x38\x2e\x32\x33\x2c\x35\ -\x31\x2e\x38\x38\x2d\x33\x38\x2e\x32\x35\x2c\x33\x2d\x35\x2c\x32\ -\x2e\x35\x34\x2d\x38\x2e\x31\x33\x2d\x31\x2e\x34\x37\x2d\x31\x32\ -\x2e\x30\x36\x2d\x39\x2e\x31\x31\x2d\x38\x2e\x39\x34\x2d\x38\x2e\ -\x39\x2d\x39\x2d\x32\x30\x2e\x39\x32\x2d\x34\x2e\x31\x35\x2d\x33\ -\x33\x2c\x31\x33\x2e\x31\x38\x2d\x36\x31\x2e\x38\x38\x2c\x34\x2d\ -\x38\x39\x2e\x37\x35\x2d\x31\x35\x2e\x31\x34\x2d\x31\x32\x2e\x38\ -\x31\x2d\x38\x2e\x37\x39\x2d\x31\x32\x2e\x39\x31\x2d\x31\x39\x2e\ -\x34\x34\x2d\x31\x30\x2e\x36\x34\x2d\x33\x32\x2e\x35\x31\x2c\x35\ -\x2e\x36\x34\x2d\x33\x32\x2e\x34\x36\x2c\x32\x30\x2e\x38\x2d\x35\ -\x39\x2e\x32\x38\x2c\x34\x35\x2e\x37\x39\x2d\x38\x30\x2e\x36\x34\ -\x2e\x36\x36\x2d\x2e\x35\x36\x2c\x31\x2e\x34\x32\x2d\x31\x2c\x32\ -\x2e\x38\x37\x2d\x32\x2c\x2e\x37\x37\x2c\x31\x32\x2e\x33\x36\x2d\ -\x2e\x37\x2c\x32\x33\x2e\x38\x34\x2c\x31\x2e\x35\x36\x2c\x33\x35\ -\x2e\x31\x39\x2c\x35\x2e\x30\x38\x2c\x32\x35\x2e\x34\x36\x2c\x31\ -\x37\x2e\x31\x32\x2c\x34\x36\x2e\x30\x39\x2c\x33\x39\x2e\x37\x31\ -\x2c\x35\x39\x2e\x39\x35\x2c\x34\x2e\x31\x38\x2c\x32\x2e\x35\x36\ -\x2c\x36\x2e\x38\x38\x2c\x32\x2e\x38\x31\x2c\x31\x30\x2e\x36\x31\ -\x2d\x31\x2e\x30\x39\x2c\x39\x2e\x32\x38\x2d\x39\x2e\x37\x34\x2c\ -\x39\x2e\x32\x2d\x39\x2e\x34\x2c\x34\x2e\x35\x35\x2d\x32\x32\x2e\ -\x31\x36\x2d\x31\x31\x2e\x36\x39\x2d\x33\x32\x2e\x30\x37\x2d\x33\ -\x2d\x36\x30\x2e\x34\x32\x2c\x31\x36\x2e\x31\x33\x2d\x38\x37\x2c\ -\x37\x2e\x36\x36\x2d\x31\x30\x2e\x36\x33\x2c\x31\x36\x2d\x31\x35\ -\x2e\x35\x35\x2c\x33\x30\x2e\x32\x34\x2d\x31\x32\x2e\x38\x32\x2c\ -\x33\x31\x2e\x38\x32\x2c\x36\x2e\x30\x39\x2c\x35\x38\x2e\x33\x38\ -\x2c\x32\x30\x2e\x34\x34\x2c\x38\x30\x2c\x34\x34\x2e\x33\x33\x43\ -\x33\x33\x31\x2e\x34\x38\x2c\x31\x37\x37\x2e\x33\x39\x2c\x33\x33\ -\x31\x2e\x35\x37\x2c\x31\x37\x38\x2e\x31\x38\x2c\x33\x33\x32\x2e\ -\x32\x35\x2c\x31\x37\x39\x2e\x39\x34\x5a\x4d\x32\x33\x30\x2e\x35\ -\x39\x2c\x32\x38\x30\x2e\x37\x32\x63\x33\x2e\x35\x39\x2e\x31\x2c\ -\x31\x36\x2e\x33\x37\x2d\x31\x32\x2e\x34\x39\x2c\x31\x36\x2e\x35\ -\x37\x2d\x31\x36\x2e\x33\x34\x53\x32\x33\x35\x2e\x31\x36\x2c\x32\ -\x34\x38\x2c\x32\x33\x31\x2c\x32\x34\x37\x2e\x37\x35\x63\x2d\x33\ -\x2e\x35\x32\x2d\x2e\x31\x39\x2d\x31\x36\x2e\x35\x34\x2c\x31\x32\ -\x2e\x35\x2d\x31\x36\x2e\x36\x35\x2c\x31\x36\x2e\x32\x33\x53\x32\ -\x32\x36\x2e\x36\x39\x2c\x32\x38\x30\x2e\x36\x31\x2c\x32\x33\x30\ -\x2e\x35\x39\x2c\x32\x38\x30\x2e\x37\x32\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\x35\x38\x31\x2c\x33\x33\x32\x2e\x38\x32\ -\x63\x2d\x33\x2e\x36\x31\x2c\x32\x2e\x31\x33\x2d\x37\x2e\x32\x33\ -\x2c\x34\x2e\x32\x35\x2d\x31\x30\x2e\x38\x33\x2c\x36\x2e\x33\x39\ -\x6c\x2d\x34\x35\x2e\x34\x31\x2c\x32\x37\x63\x2d\x2e\x31\x36\x2e\ -\x31\x2d\x2e\x33\x33\x2e\x31\x39\x2d\x2e\x36\x36\x2e\x33\x37\x2c\ -\x30\x2d\x32\x2e\x33\x31\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\x34\ -\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\x34\x68\x2d\x32\x33\x56\x33\ -\x31\x39\x2e\x34\x68\x32\x33\x73\x30\x2d\x32\x30\x2e\x33\x36\x2c\ -\x30\x2d\x32\x30\x2e\x35\x31\x63\x2e\x30\x37\x2c\x30\x2c\x31\x30\ -\x2e\x31\x38\x2c\x35\x2e\x39\x31\x2c\x31\x34\x2e\x38\x34\x2c\x38\ -\x2e\x36\x38\x4c\x35\x38\x31\x2c\x33\x33\x32\x2e\x36\x34\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\x35\x38\x31\x2c\x34\x39\ -\x34\x63\x2d\x33\x2e\x36\x31\x2c\x32\x2e\x31\x33\x2d\x37\x2e\x32\ -\x33\x2c\x34\x2e\x32\x35\x2d\x31\x30\x2e\x38\x33\x2c\x36\x2e\x33\ -\x39\x6c\x2d\x34\x35\x2e\x34\x31\x2c\x32\x37\x63\x2d\x2e\x31\x36\ -\x2e\x31\x2d\x2e\x33\x33\x2e\x31\x39\x2d\x2e\x36\x36\x2e\x33\x37\ -\x2c\x30\x2d\x32\x2e\x33\x31\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\ -\x34\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\x34\x68\x2d\x32\x33\x56\ -\x34\x38\x30\x2e\x36\x31\x68\x32\x33\x73\x30\x2d\x32\x30\x2e\x33\ -\x36\x2c\x30\x2d\x32\x30\x2e\x35\x31\x2c\x31\x30\x2e\x31\x38\x2c\ -\x35\x2e\x39\x31\x2c\x31\x34\x2e\x38\x34\x2c\x38\x2e\x36\x39\x4c\ -\x35\x38\x31\x2c\x34\x39\x33\x2e\x38\x35\x5a\x22\x2f\x3e\x3c\x70\ +\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\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\x35\x38\x31\x2c\x34\x31\x33\x2e\x34\x33\ -\x63\x2d\x33\x2e\x36\x31\x2c\x32\x2e\x31\x33\x2d\x37\x2e\x32\x33\ -\x2c\x34\x2e\x32\x34\x2d\x31\x30\x2e\x38\x33\x2c\x36\x2e\x33\x38\ -\x6c\x2d\x34\x35\x2e\x34\x31\x2c\x32\x37\x63\x2d\x2e\x31\x36\x2e\ -\x31\x2d\x2e\x33\x33\x2e\x31\x38\x2d\x2e\x36\x36\x2e\x33\x37\x2c\ -\x30\x2d\x32\x2e\x33\x32\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\x35\ -\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\x35\x68\x2d\x32\x33\x56\x34\ -\x30\x30\x68\x32\x33\x73\x30\x2d\x32\x30\x2e\x33\x36\x2c\x30\x2d\ -\x32\x30\x2e\x35\x31\x2c\x31\x30\x2e\x31\x38\x2c\x35\x2e\x39\x31\ -\x2c\x31\x34\x2e\x38\x34\x2c\x38\x2e\x36\x38\x4c\x35\x38\x31\x2c\ -\x34\x31\x33\x2e\x32\x34\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\ +\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\ +\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\ \x00\x00\x04\xf7\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -19325,199 +19418,6 @@ \x22\x32\x34\x36\x2e\x32\x38\x22\x20\x77\x69\x64\x74\x68\x3d\x22\ \x35\x32\x35\x2e\x39\x31\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\ \x31\x30\x37\x2e\x34\x35\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x05\x95\ -\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\x64\x30\x64\x32\x64\x33\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\ -\x35\x34\x30\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\x32\x39\x30\x2e\ -\x31\x38\x2c\x37\x37\x43\x34\x30\x32\x2c\x37\x38\x2e\x37\x37\x2c\ -\x34\x39\x30\x2e\x31\x2c\x31\x31\x37\x2e\x30\x37\x2c\x35\x36\x33\ -\x2e\x39\x2c\x31\x39\x33\x2e\x30\x39\x63\x31\x34\x2e\x38\x2c\x31\ -\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\x2e\x39\x33\ -\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2d\x31\x31\x2e\x31\x32\x2c\x31\ -\x33\x2e\x31\x37\x2d\x32\x38\x2e\x36\x32\x2c\x31\x33\x2d\x34\x31\ -\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\x30\x38\x2d\x31\x34\x2e\ -\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\x2e\x37\x31\x2d\x34\x36\ -\x2e\x36\x31\x2d\x34\x31\x2e\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\ -\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\x37\x35\x2d\x31\x32\x37\ -\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\x35\x34\x2d\x39\x2e\x31\ -\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\x2e\x33\x31\x2d\x31\x35\ -\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x2d\x34\x35\x2c\x31\x37\x2e\ -\x30\x35\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\x33\x2d\x31\ -\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x33\x2d\x38\x2e\x35\x37\ -\x2c\x39\x2e\x31\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\x32\x2e\x33\ -\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\x30\x39\x2d\ -\x37\x2e\x35\x37\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\x30\x39\x2d\ -\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x31\x61\x33\x38\x30\x2c\x33\ -\x38\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x37\x2e\x36\x32\x2d\x35\ -\x30\x2e\x33\x38\x63\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\x38\x39\ -\x2e\x39\x33\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\x37\x2d\ -\x36\x30\x2e\x34\x36\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x39\x2e\ -\x38\x37\x2c\x32\x37\x37\x2e\x34\x33\x2c\x37\x38\x2e\x34\x38\x2c\ -\x32\x39\x30\x2e\x31\x38\x2c\x37\x37\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\x36\x39\x2e\x38\x37\x2c\x33\x33\x32\x2e\ -\x32\x31\x63\x2d\x31\x31\x2c\x2e\x31\x38\x2d\x31\x37\x2e\x38\x33\ -\x2d\x33\x2e\x30\x37\x2d\x32\x33\x2e\x35\x32\x2d\x39\x2e\x31\x32\ -\x43\x34\x31\x34\x2c\x32\x38\x38\x2e\x36\x35\x2c\x33\x37\x35\x2e\ -\x32\x32\x2c\x32\x36\x37\x2e\x31\x36\x2c\x33\x33\x30\x2e\x31\x2c\ -\x32\x36\x30\x2e\x36\x38\x63\x2d\x36\x37\x2e\x33\x37\x2d\x39\x2e\ -\x36\x37\x2d\x31\x32\x36\x2e\x31\x37\x2c\x31\x30\x2e\x38\x33\x2d\ -\x31\x37\x35\x2e\x33\x39\x2c\x36\x31\x2e\x34\x31\x2d\x31\x36\x2e\ -\x33\x35\x2c\x31\x36\x2e\x38\x2d\x34\x30\x2e\x36\x37\x2c\x31\x32\ -\x2d\x34\x37\x2e\x39\x31\x2d\x31\x30\x2d\x33\x2e\x39\x2d\x31\x31\ -\x2e\x39\x2d\x31\x2e\x33\x38\x2d\x32\x32\x2e\x38\x2c\x36\x2e\x38\ -\x39\x2d\x33\x31\x2e\x35\x32\x2c\x34\x31\x2d\x34\x33\x2e\x32\x34\ -\x2c\x38\x39\x2e\x37\x35\x2d\x37\x30\x2e\x34\x38\x2c\x31\x34\x36\ -\x2e\x38\x34\x2d\x37\x39\x2e\x32\x38\x2c\x37\x35\x2e\x36\x2d\x31\ -\x31\x2e\x36\x36\x2c\x31\x34\x33\x2e\x36\x39\x2c\x38\x2e\x30\x35\ -\x2c\x32\x30\x33\x2e\x39\x31\x2c\x35\x38\x2e\x33\x36\x61\x32\x30\ -\x35\x2e\x37\x34\x2c\x32\x30\x35\x2e\x37\x34\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x32\x33\x2e\x32\x35\x2c\x32\x32\x2e\x36\x39\x63\x38\x2e\ -\x30\x38\x2c\x39\x2e\x33\x2c\x39\x2e\x35\x2c\x32\x30\x2e\x36\x32\ -\x2c\x34\x2e\x35\x39\x2c\x33\x32\x2e\x33\x34\x53\x34\x37\x38\x2e\ -\x36\x32\x2c\x33\x33\x31\x2e\x36\x36\x2c\x34\x36\x39\x2e\x38\x37\ -\x2c\x33\x33\x32\x2e\x32\x31\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\x31\x38\x34\x2e\x35\x32\x2c\x33\x38\x37\x2e\x34\x63\ -\x30\x2d\x39\x2e\x32\x32\x2c\x33\x2e\x35\x37\x2d\x31\x36\x2e\x36\ -\x35\x2c\x39\x2e\x36\x31\x2d\x32\x32\x2e\x38\x31\x43\x32\x31\x38\ -\x2c\x33\x34\x30\x2e\x32\x34\x2c\x32\x34\x36\x2c\x33\x32\x34\x2e\ -\x37\x38\x2c\x32\x37\x38\x2e\x37\x37\x2c\x33\x32\x30\x2e\x31\x35\ -\x63\x34\x38\x2e\x36\x36\x2d\x36\x2e\x38\x36\x2c\x39\x30\x2e\x38\ -\x33\x2c\x38\x2e\x32\x35\x2c\x31\x32\x36\x2e\x36\x33\x2c\x34\x34\ -\x2c\x31\x30\x2e\x31\x38\x2c\x31\x30\x2e\x31\x35\x2c\x31\x32\x2e\ -\x38\x31\x2c\x32\x34\x2c\x37\x2e\x34\x35\x2c\x33\x36\x2e\x30\x35\ -\x2d\x38\x2e\x34\x34\x2c\x31\x39\x2d\x33\x31\x2c\x32\x33\x2e\x34\ -\x35\x2d\x34\x35\x2e\x33\x32\x2c\x38\x2e\x36\x36\x2d\x31\x33\x2e\ -\x34\x2d\x31\x33\x2e\x38\x33\x2d\x32\x38\x2e\x38\x2d\x32\x33\x2e\ -\x36\x33\x2d\x34\x37\x2e\x31\x31\x2d\x32\x37\x2e\x35\x34\x2d\x33\ -\x33\x2e\x32\x32\x2d\x37\x2e\x31\x2d\x36\x32\x2e\x33\x36\x2c\x31\ -\x2e\x36\x35\x2d\x38\x37\x2c\x32\x36\x2e\x37\x37\x2d\x31\x36\x2e\ -\x36\x36\x2c\x31\x37\x2d\x34\x32\x2e\x33\x2c\x31\x30\x2d\x34\x37\ -\x2e\x39\x33\x2d\x31\x33\x2e\x32\x37\x41\x36\x39\x2e\x32\x38\x2c\ -\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x38\x34\x2e\ -\x35\x32\x2c\x33\x38\x37\x2e\x34\x5a\x22\x2f\x3e\x3c\x70\x61\x74\ -\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\ -\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x32\x33\x63\x2d\x32\x32\x2c\ -\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x36\x2d\x33\x39\ -\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\x2e\x33\ -\x38\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\x33\x38\ -\x2e\x38\x36\x2d\x34\x31\x2e\x38\x2c\x32\x32\x2e\x32\x35\x2c\x30\ -\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\x38\x2c\x33\x39\x2e\ -\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\ -\x32\x33\x2c\x33\x30\x30\x2c\x35\x32\x33\x5a\x22\x2f\x3e\x3c\x2f\ -\x73\x76\x67\x3e\ -\x00\x00\x06\x23\ -\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\x64\x30\x64\x32\x64\x33\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x35\x65\x36\ -\x30\x36\x31\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\x32\x39\x30\x2e\ -\x32\x36\x2c\x39\x34\x2e\x37\x63\x31\x31\x30\x2e\x39\x34\x2c\x31\ -\x2e\x37\x37\x2c\x31\x39\x38\x2e\x33\x39\x2c\x33\x39\x2e\x37\x37\ -\x2c\x32\x37\x31\x2e\x36\x32\x2c\x31\x31\x35\x2e\x32\x31\x2c\x31\ -\x34\x2e\x36\x39\x2c\x31\x35\x2e\x31\x33\x2c\x31\x31\x2e\x37\x32\ -\x2c\x33\x33\x2e\x36\x37\x2c\x33\x2e\x32\x34\x2c\x34\x33\x2e\x37\ -\x2d\x31\x31\x2c\x31\x33\x2e\x30\x37\x2d\x32\x38\x2e\x34\x2c\x31\ -\x32\x2e\x38\x39\x2d\x34\x31\x2e\x31\x35\x2e\x38\x33\x2d\x31\x35\ -\x2d\x31\x34\x2e\x31\x36\x2d\x33\x30\x2d\x32\x38\x2e\x35\x2d\x34\ -\x36\x2e\x32\x35\x2d\x34\x30\x2e\x37\x39\x2d\x33\x38\x2e\x31\x31\ -\x2d\x32\x38\x2e\x37\x38\x2d\x38\x30\x2e\x37\x31\x2d\x34\x36\x2e\ -\x33\x39\x2d\x31\x32\x36\x2e\x37\x38\x2d\x35\x34\x2e\x32\x33\x2d\ -\x35\x33\x2e\x36\x2d\x39\x2e\x31\x32\x2d\x31\x30\x36\x2e\x30\x39\ -\x2d\x34\x2e\x32\x38\x2d\x31\x35\x37\x2e\x32\x38\x2c\x31\x35\x2e\ -\x31\x31\x43\x31\x34\x39\x2c\x31\x39\x31\x2e\x34\x35\x2c\x31\x31\ -\x30\x2c\x32\x31\x38\x2e\x31\x32\x2c\x37\x36\x2e\x34\x2c\x32\x35\ -\x33\x2e\x38\x35\x63\x2d\x38\x2e\x35\x2c\x39\x2d\x31\x38\x2e\x35\ -\x2c\x31\x32\x2e\x32\x33\x2d\x33\x30\x2c\x37\x2e\x39\x31\x2d\x31\ -\x39\x2e\x39\x33\x2d\x37\x2e\x35\x2d\x32\x35\x2d\x33\x33\x2e\x38\ -\x32\x2d\x39\x2e\x36\x36\x2d\x35\x30\x2e\x32\x31\x61\x33\x37\x37\ -\x2e\x33\x2c\x33\x37\x37\x2e\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x35\ -\x37\x2e\x31\x38\x2d\x35\x30\x63\x34\x32\x2e\x37\x2d\x33\x30\x2e\ -\x33\x35\x2c\x38\x39\x2e\x32\x34\x2d\x35\x30\x2e\x37\x31\x2c\x31\ -\x33\x39\x2e\x36\x39\x2d\x36\x30\x43\x32\x35\x35\x2e\x34\x31\x2c\ -\x39\x37\x2e\x35\x35\x2c\x32\x37\x37\x2e\x36\x2c\x39\x36\x2e\x31\ -\x38\x2c\x32\x39\x30\x2e\x32\x36\x2c\x39\x34\x2e\x37\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\x36\x38\x2e\x35\x31\x2c\ -\x33\x34\x38\x63\x2d\x31\x30\x2e\x39\x31\x2e\x31\x38\x2d\x31\x37\ -\x2e\x36\x39\x2d\x33\x2e\x30\x35\x2d\x32\x33\x2e\x33\x34\x2d\x39\ -\x2e\x30\x36\x2d\x33\x32\x2e\x31\x32\x2d\x33\x34\x2e\x31\x38\x2d\ -\x37\x30\x2e\x35\x39\x2d\x35\x35\x2e\x35\x2d\x31\x31\x35\x2e\x33\ -\x36\x2d\x36\x31\x2e\x39\x33\x2d\x36\x36\x2e\x38\x35\x2d\x39\x2e\ -\x35\x39\x2d\x31\x32\x35\x2e\x32\x2c\x31\x30\x2e\x37\x35\x2d\x31\ -\x37\x34\x2c\x36\x30\x2e\x39\x34\x2d\x31\x36\x2e\x32\x32\x2c\x31\ -\x36\x2e\x36\x37\x2d\x34\x30\x2e\x33\x36\x2c\x31\x31\x2e\x39\x31\ -\x2d\x34\x37\x2e\x35\x34\x2d\x31\x30\x2d\x33\x2e\x38\x38\x2d\x31\ -\x31\x2e\x38\x2d\x31\x2e\x33\x37\x2d\x32\x32\x2e\x36\x32\x2c\x36\ -\x2e\x38\x34\x2d\x33\x31\x2e\x32\x37\x2c\x34\x30\x2e\x36\x38\x2d\ -\x34\x32\x2e\x39\x31\x2c\x38\x39\x2e\x30\x36\x2d\x36\x39\x2e\x39\ -\x34\x2c\x31\x34\x35\x2e\x37\x31\x2d\x37\x38\x2e\x36\x38\x2c\x37\ -\x35\x2d\x31\x31\x2e\x35\x37\x2c\x31\x34\x32\x2e\x35\x39\x2c\x38\ -\x2c\x32\x30\x32\x2e\x33\x35\x2c\x35\x37\x2e\x39\x32\x61\x32\x30\ -\x33\x2e\x34\x37\x2c\x32\x30\x33\x2e\x34\x37\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x32\x33\x2e\x30\x37\x2c\x32\x32\x2e\x35\x32\x63\x38\x2c\ -\x39\x2e\x32\x32\x2c\x39\x2e\x34\x33\x2c\x32\x30\x2e\x34\x36\x2c\ -\x34\x2e\x35\x36\x2c\x33\x32\x2e\x30\x38\x53\x34\x37\x37\x2e\x31\ -\x39\x2c\x33\x34\x37\x2e\x34\x31\x2c\x34\x36\x38\x2e\x35\x31\x2c\ -\x33\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\x31\ -\x38\x35\x2e\x33\x36\x2c\x34\x30\x32\x2e\x37\x33\x63\x30\x2d\x39\ -\x2e\x31\x35\x2c\x33\x2e\x35\x35\x2d\x31\x36\x2e\x35\x32\x2c\x39\ -\x2e\x35\x34\x2d\x32\x32\x2e\x36\x34\x2c\x32\x33\x2e\x36\x36\x2d\ -\x32\x34\x2e\x31\x35\x2c\x35\x31\x2e\x34\x34\x2d\x33\x39\x2e\x35\ -\x2c\x38\x34\x2d\x34\x34\x2e\x30\x39\x2c\x34\x38\x2e\x32\x38\x2d\ -\x36\x2e\x38\x31\x2c\x39\x30\x2e\x31\x34\x2c\x38\x2e\x31\x39\x2c\ -\x31\x32\x35\x2e\x36\x37\x2c\x34\x33\x2e\x36\x32\x2c\x31\x30\x2e\ -\x30\x39\x2c\x31\x30\x2e\x30\x37\x2c\x31\x32\x2e\x37\x2c\x32\x33\ -\x2e\x38\x32\x2c\x37\x2e\x33\x39\x2c\x33\x35\x2e\x37\x37\x2d\x38\ -\x2e\x33\x39\x2c\x31\x38\x2e\x38\x35\x2d\x33\x30\x2e\x37\x37\x2c\ -\x32\x33\x2e\x32\x37\x2d\x34\x35\x2c\x38\x2e\x36\x2d\x31\x33\x2e\ -\x33\x2d\x31\x33\x2e\x37\x33\x2d\x32\x38\x2e\x35\x38\x2d\x32\x33\ -\x2e\x34\x35\x2d\x34\x36\x2e\x37\x35\x2d\x32\x37\x2e\x33\x33\x2d\ -\x33\x33\x2d\x37\x2e\x30\x35\x2d\x36\x31\x2e\x38\x38\x2c\x31\x2e\ -\x36\x34\x2d\x38\x36\x2e\x33\x2c\x32\x36\x2e\x35\x36\x2d\x31\x36\ -\x2e\x35\x34\x2c\x31\x36\x2e\x38\x38\x2d\x34\x32\x2c\x39\x2e\x39\ -\x33\x2d\x34\x37\x2e\x35\x37\x2d\x31\x33\x2e\x31\x37\x41\x37\x30\ -\x2e\x34\x31\x2c\x37\x30\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x31\x38\x35\x2e\x33\x36\x2c\x34\x30\x32\x2e\x37\x33\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\x33\x30\x30\x2c\x35\x33\x37\ -\x2e\x33\x63\x2d\x32\x31\x2e\x38\x33\x2c\x30\x2d\x33\x38\x2e\x38\ -\x34\x2d\x31\x38\x2e\x33\x31\x2d\x33\x38\x2e\x38\x31\x2d\x34\x31\ -\x2e\x37\x39\x2c\x30\x2d\x32\x33\x2e\x31\x39\x2c\x31\x37\x2e\x30\ -\x36\x2d\x34\x31\x2e\x35\x31\x2c\x33\x38\x2e\x35\x36\x2d\x34\x31\ -\x2e\x34\x37\x2c\x32\x32\x2e\x30\x39\x2c\x30\x2c\x33\x39\x2c\x31\ -\x38\x2c\x33\x39\x2c\x34\x31\x2e\x34\x39\x53\x33\x32\x31\x2e\x39\ -\x31\x2c\x35\x33\x37\x2e\x32\x39\x2c\x33\x30\x30\x2c\x35\x33\x37\ -\x2e\x33\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x31\x35\ -\x36\x2e\x37\x36\x2c\x36\x31\x2c\x33\x30\x30\x2c\x32\x35\x39\x2e\ -\x38\x2c\x34\x34\x33\x2e\x32\x35\x2c\x36\x31\x68\x35\x37\x2e\x39\ -\x33\x4c\x33\x32\x39\x2c\x33\x30\x30\x2c\x35\x30\x31\x2e\x31\x39\ -\x2c\x35\x33\x39\x48\x34\x34\x33\x2e\x32\x35\x4c\x33\x30\x30\x2c\ -\x33\x34\x30\x2e\x31\x39\x2c\x31\x35\x36\x2e\x37\x36\x2c\x35\x33\ -\x39\x48\x39\x38\x2e\x38\x31\x4c\x32\x37\x31\x2c\x33\x30\x30\x2c\ -\x39\x38\x2e\x38\x33\x2c\x36\x31\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\ -\x67\x3e\ \x00\x00\x0b\x4d\ \x00\ \x00\x38\xfa\x78\x9c\xed\x9b\x49\x6f\x1d\xc7\x15\x85\xff\x0a\xc1\ @@ -20100,7 +20000,7 @@ \x32\x36\x39\x2e\x39\x2c\x33\x38\x35\x2e\x34\x37\x2c\x32\x37\x34\ \x2e\x34\x34\x2c\x33\x38\x35\x2e\x37\x34\x2c\x32\x37\x37\x2e\x31\ \x34\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x09\x7d\ +\x00\x00\x05\x95\ \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\ @@ -20109,151 +20009,262 @@ \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\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x65\x65\x32\ -\x66\x32\x36\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ +\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ +\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\ +\x35\x34\x30\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\x32\x39\x30\x2e\ -\x31\x38\x2c\x37\x36\x2e\x35\x33\x43\x34\x30\x32\x2c\x37\x38\x2e\ -\x33\x31\x2c\x34\x39\x30\x2e\x31\x2c\x31\x31\x36\x2e\x36\x31\x2c\ -\x35\x36\x33\x2e\x39\x2c\x31\x39\x32\x2e\x36\x33\x63\x31\x34\x2e\ -\x38\x2c\x31\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\ -\x2e\x39\x33\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2d\x31\x31\x2e\x31\ -\x32\x2c\x31\x33\x2e\x31\x36\x2d\x32\x38\x2e\x36\x32\x2c\x31\x33\ -\x2d\x34\x31\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\x30\x38\x2d\ -\x31\x34\x2e\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\x2e\x37\x31\ -\x2d\x34\x36\x2e\x36\x31\x2d\x34\x31\x2e\x31\x2d\x33\x38\x2e\x34\ -\x2d\x32\x39\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\x37\x35\x2d\ -\x31\x32\x37\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\x35\x34\x2d\ -\x39\x2e\x31\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\x2e\x33\x32\ -\x2d\x31\x35\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x2d\x34\x35\x2c\ -\x31\x37\x2e\x30\x35\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\ -\x33\x2d\x31\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x33\x2d\x38\ -\x2e\x35\x37\x2c\x39\x2e\x31\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\ -\x32\x2e\x33\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\ -\x30\x39\x2d\x37\x2e\x35\x36\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\ -\x30\x38\x2d\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x41\x33\x38\x30\ -\x2c\x33\x38\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x39\x32\x2e\x33\x37\ -\x2c\x31\x34\x33\x2e\x39\x63\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\ -\x38\x39\x2e\x39\x33\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\ -\x37\x2d\x36\x30\x2e\x34\x36\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\ -\x39\x2e\x34\x31\x2c\x32\x37\x37\x2e\x34\x33\x2c\x37\x38\x2c\x32\ -\x39\x30\x2e\x31\x38\x2c\x37\x36\x2e\x35\x33\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\x33\x30\x30\x2c\x35\x32\x32\x2e\x35\ -\x35\x63\x2d\x32\x32\x2c\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\ -\x2e\x34\x36\x2d\x33\x39\x2e\x31\x31\x2d\x34\x32\x2e\x31\x32\x2c\ -\x30\x2d\x32\x33\x2e\x33\x37\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\ -\x2e\x38\x32\x2c\x33\x38\x2e\x38\x36\x2d\x34\x31\x2e\x37\x39\x2c\ -\x32\x32\x2e\x32\x35\x2c\x30\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\ -\x2e\x31\x38\x2c\x33\x39\x2e\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\ -\x33\x32\x32\x2e\x31\x2c\x35\x32\x32\x2e\x35\x34\x2c\x33\x30\x30\ -\x2c\x35\x32\x32\x2e\x35\x35\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ +\x31\x38\x2c\x37\x37\x43\x34\x30\x32\x2c\x37\x38\x2e\x37\x37\x2c\ +\x34\x39\x30\x2e\x31\x2c\x31\x31\x37\x2e\x30\x37\x2c\x35\x36\x33\ +\x2e\x39\x2c\x31\x39\x33\x2e\x30\x39\x63\x31\x34\x2e\x38\x2c\x31\ +\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\x2e\x39\x33\ +\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2d\x31\x31\x2e\x31\x32\x2c\x31\ +\x33\x2e\x31\x37\x2d\x32\x38\x2e\x36\x32\x2c\x31\x33\x2d\x34\x31\ +\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\x30\x38\x2d\x31\x34\x2e\ +\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\x2e\x37\x31\x2d\x34\x36\ +\x2e\x36\x31\x2d\x34\x31\x2e\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\ +\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\x37\x35\x2d\x31\x32\x37\ +\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\x35\x34\x2d\x39\x2e\x31\ +\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\x2e\x33\x31\x2d\x31\x35\ +\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x2d\x34\x35\x2c\x31\x37\x2e\ +\x30\x35\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\x33\x2d\x31\ +\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x33\x2d\x38\x2e\x35\x37\ +\x2c\x39\x2e\x31\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\x32\x2e\x33\ +\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\x30\x39\x2d\ +\x37\x2e\x35\x37\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\x30\x39\x2d\ +\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x31\x61\x33\x38\x30\x2c\x33\ +\x38\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x37\x2e\x36\x32\x2d\x35\ +\x30\x2e\x33\x38\x63\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\x38\x39\ +\x2e\x39\x33\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\x37\x2d\ +\x36\x30\x2e\x34\x36\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x39\x2e\ +\x38\x37\x2c\x32\x37\x37\x2e\x34\x33\x2c\x37\x38\x2e\x34\x38\x2c\ +\x32\x39\x30\x2e\x31\x38\x2c\x37\x37\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\x36\x39\x2e\x38\x37\x2c\x33\x33\x32\x2e\ +\x32\x31\x63\x2d\x31\x31\x2c\x2e\x31\x38\x2d\x31\x37\x2e\x38\x33\ +\x2d\x33\x2e\x30\x37\x2d\x32\x33\x2e\x35\x32\x2d\x39\x2e\x31\x32\ +\x43\x34\x31\x34\x2c\x32\x38\x38\x2e\x36\x35\x2c\x33\x37\x35\x2e\ +\x32\x32\x2c\x32\x36\x37\x2e\x31\x36\x2c\x33\x33\x30\x2e\x31\x2c\ +\x32\x36\x30\x2e\x36\x38\x63\x2d\x36\x37\x2e\x33\x37\x2d\x39\x2e\ +\x36\x37\x2d\x31\x32\x36\x2e\x31\x37\x2c\x31\x30\x2e\x38\x33\x2d\ +\x31\x37\x35\x2e\x33\x39\x2c\x36\x31\x2e\x34\x31\x2d\x31\x36\x2e\ +\x33\x35\x2c\x31\x36\x2e\x38\x2d\x34\x30\x2e\x36\x37\x2c\x31\x32\ +\x2d\x34\x37\x2e\x39\x31\x2d\x31\x30\x2d\x33\x2e\x39\x2d\x31\x31\ +\x2e\x39\x2d\x31\x2e\x33\x38\x2d\x32\x32\x2e\x38\x2c\x36\x2e\x38\ +\x39\x2d\x33\x31\x2e\x35\x32\x2c\x34\x31\x2d\x34\x33\x2e\x32\x34\ +\x2c\x38\x39\x2e\x37\x35\x2d\x37\x30\x2e\x34\x38\x2c\x31\x34\x36\ +\x2e\x38\x34\x2d\x37\x39\x2e\x32\x38\x2c\x37\x35\x2e\x36\x2d\x31\ +\x31\x2e\x36\x36\x2c\x31\x34\x33\x2e\x36\x39\x2c\x38\x2e\x30\x35\ +\x2c\x32\x30\x33\x2e\x39\x31\x2c\x35\x38\x2e\x33\x36\x61\x32\x30\ +\x35\x2e\x37\x34\x2c\x32\x30\x35\x2e\x37\x34\x2c\x30\x2c\x30\x2c\ +\x31\x2c\x32\x33\x2e\x32\x35\x2c\x32\x32\x2e\x36\x39\x63\x38\x2e\ +\x30\x38\x2c\x39\x2e\x33\x2c\x39\x2e\x35\x2c\x32\x30\x2e\x36\x32\ +\x2c\x34\x2e\x35\x39\x2c\x33\x32\x2e\x33\x34\x53\x34\x37\x38\x2e\ +\x36\x32\x2c\x33\x33\x31\x2e\x36\x36\x2c\x34\x36\x39\x2e\x38\x37\ +\x2c\x33\x33\x32\x2e\x32\x31\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\x35\x36\x2e\x37\x32\x2c\x33\x31\x32\x2e\x37\x61\ -\x33\x32\x2e\x35\x33\x2c\x33\x32\x2e\x35\x33\x2c\x30\x2c\x30\x2c\ -\x30\x2d\x31\x36\x2e\x34\x31\x2c\x33\x2e\x37\x32\x71\x33\x2e\x30\ -\x36\x2c\x33\x2c\x36\x2c\x36\x2e\x32\x31\x63\x35\x2e\x36\x39\x2c\ -\x36\x2e\x30\x35\x2c\x31\x32\x2e\x35\x33\x2c\x39\x2e\x33\x2c\x32\ -\x33\x2e\x35\x32\x2c\x39\x2e\x31\x32\x61\x32\x33\x2e\x39\x2c\x32\ -\x33\x2e\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x33\x2e\x35\x2d\x35\ -\x2e\x33\x31\x41\x33\x35\x2e\x35\x33\x2c\x33\x35\x2e\x35\x33\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x34\x35\x36\x2e\x37\x32\x2c\x33\x31\x32\ -\x2e\x37\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\x34\ -\x36\x2e\x32\x36\x2c\x32\x36\x36\x2e\x33\x34\x61\x37\x36\x2e\x32\ -\x33\x2c\x37\x36\x2e\x32\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x37\ -\x2e\x39\x31\x2c\x31\x2e\x34\x37\x71\x2d\x34\x2e\x37\x36\x2d\x34\ -\x2e\x35\x2d\x39\x2e\x37\x33\x2d\x38\x2e\x36\x35\x63\x2d\x36\x30\ -\x2e\x32\x32\x2d\x35\x30\x2e\x33\x31\x2d\x31\x32\x38\x2e\x33\x31\ -\x2d\x37\x30\x2d\x32\x30\x33\x2e\x39\x31\x2d\x35\x38\x2e\x33\x37\ -\x2d\x35\x37\x2e\x30\x39\x2c\x38\x2e\x38\x31\x2d\x31\x30\x35\x2e\ -\x38\x34\x2c\x33\x36\x2e\x30\x35\x2d\x31\x34\x36\x2e\x38\x34\x2c\ -\x37\x39\x2e\x32\x38\x2d\x38\x2e\x32\x37\x2c\x38\x2e\x37\x33\x2d\ -\x31\x30\x2e\x37\x39\x2c\x31\x39\x2e\x36\x33\x2d\x36\x2e\x38\x39\ -\x2c\x33\x31\x2e\x35\x33\x2c\x37\x2e\x32\x34\x2c\x32\x32\x2c\x33\ -\x31\x2e\x35\x36\x2c\x32\x36\x2e\x38\x33\x2c\x34\x37\x2e\x39\x31\ -\x2c\x31\x30\x2c\x34\x39\x2e\x32\x32\x2d\x35\x30\x2e\x35\x37\x2c\ -\x31\x30\x38\x2d\x37\x31\x2e\x30\x37\x2c\x31\x37\x35\x2e\x33\x39\ -\x2d\x36\x31\x2e\x34\x61\x31\x38\x37\x2c\x31\x38\x37\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x37\x32\x2e\x36\x36\x2c\x32\x36\x2e\x33\x39\x41\ -\x38\x30\x2e\x36\x2c\x38\x30\x2e\x36\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x34\x34\x36\x2e\x32\x36\x2c\x32\x36\x36\x2e\x33\x34\x5a\x22\x2f\ +\x3d\x22\x4d\x31\x38\x34\x2e\x35\x32\x2c\x33\x38\x37\x2e\x34\x63\ +\x30\x2d\x39\x2e\x32\x32\x2c\x33\x2e\x35\x37\x2d\x31\x36\x2e\x36\ +\x35\x2c\x39\x2e\x36\x31\x2d\x32\x32\x2e\x38\x31\x43\x32\x31\x38\ +\x2c\x33\x34\x30\x2e\x32\x34\x2c\x32\x34\x36\x2c\x33\x32\x34\x2e\ +\x37\x38\x2c\x32\x37\x38\x2e\x37\x37\x2c\x33\x32\x30\x2e\x31\x35\ +\x63\x34\x38\x2e\x36\x36\x2d\x36\x2e\x38\x36\x2c\x39\x30\x2e\x38\ +\x33\x2c\x38\x2e\x32\x35\x2c\x31\x32\x36\x2e\x36\x33\x2c\x34\x34\ +\x2c\x31\x30\x2e\x31\x38\x2c\x31\x30\x2e\x31\x35\x2c\x31\x32\x2e\ +\x38\x31\x2c\x32\x34\x2c\x37\x2e\x34\x35\x2c\x33\x36\x2e\x30\x35\ +\x2d\x38\x2e\x34\x34\x2c\x31\x39\x2d\x33\x31\x2c\x32\x33\x2e\x34\ +\x35\x2d\x34\x35\x2e\x33\x32\x2c\x38\x2e\x36\x36\x2d\x31\x33\x2e\ +\x34\x2d\x31\x33\x2e\x38\x33\x2d\x32\x38\x2e\x38\x2d\x32\x33\x2e\ +\x36\x33\x2d\x34\x37\x2e\x31\x31\x2d\x32\x37\x2e\x35\x34\x2d\x33\ +\x33\x2e\x32\x32\x2d\x37\x2e\x31\x2d\x36\x32\x2e\x33\x36\x2c\x31\ +\x2e\x36\x35\x2d\x38\x37\x2c\x32\x36\x2e\x37\x37\x2d\x31\x36\x2e\ +\x36\x36\x2c\x31\x37\x2d\x34\x32\x2e\x33\x2c\x31\x30\x2d\x34\x37\ +\x2e\x39\x33\x2d\x31\x33\x2e\x32\x37\x41\x36\x39\x2e\x32\x38\x2c\ +\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x38\x34\x2e\ +\x35\x32\x2c\x33\x38\x37\x2e\x34\x5a\x22\x2f\x3e\x3c\x70\x61\x74\ +\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\ +\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x32\x33\x63\x2d\x32\x32\x2c\ +\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x36\x2d\x33\x39\ +\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\x2e\x33\ +\x38\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\x33\x38\ +\x2e\x38\x36\x2d\x34\x31\x2e\x38\x2c\x32\x32\x2e\x32\x35\x2c\x30\ +\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\x38\x2c\x33\x39\x2e\ +\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\ +\x32\x33\x2c\x33\x30\x30\x2c\x35\x32\x33\x5a\x22\x2f\x3e\x3c\x2f\ +\x73\x76\x67\x3e\ +\x00\x00\x06\x23\ +\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\x64\x30\x64\x32\x64\x33\x3b\x7d\ +\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x35\x65\x36\ +\x30\x36\x31\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\x32\x39\x30\x2e\ +\x32\x36\x2c\x39\x34\x2e\x37\x63\x31\x31\x30\x2e\x39\x34\x2c\x31\ +\x2e\x37\x37\x2c\x31\x39\x38\x2e\x33\x39\x2c\x33\x39\x2e\x37\x37\ +\x2c\x32\x37\x31\x2e\x36\x32\x2c\x31\x31\x35\x2e\x32\x31\x2c\x31\ +\x34\x2e\x36\x39\x2c\x31\x35\x2e\x31\x33\x2c\x31\x31\x2e\x37\x32\ +\x2c\x33\x33\x2e\x36\x37\x2c\x33\x2e\x32\x34\x2c\x34\x33\x2e\x37\ +\x2d\x31\x31\x2c\x31\x33\x2e\x30\x37\x2d\x32\x38\x2e\x34\x2c\x31\ +\x32\x2e\x38\x39\x2d\x34\x31\x2e\x31\x35\x2e\x38\x33\x2d\x31\x35\ +\x2d\x31\x34\x2e\x31\x36\x2d\x33\x30\x2d\x32\x38\x2e\x35\x2d\x34\ +\x36\x2e\x32\x35\x2d\x34\x30\x2e\x37\x39\x2d\x33\x38\x2e\x31\x31\ +\x2d\x32\x38\x2e\x37\x38\x2d\x38\x30\x2e\x37\x31\x2d\x34\x36\x2e\ +\x33\x39\x2d\x31\x32\x36\x2e\x37\x38\x2d\x35\x34\x2e\x32\x33\x2d\ +\x35\x33\x2e\x36\x2d\x39\x2e\x31\x32\x2d\x31\x30\x36\x2e\x30\x39\ +\x2d\x34\x2e\x32\x38\x2d\x31\x35\x37\x2e\x32\x38\x2c\x31\x35\x2e\ +\x31\x31\x43\x31\x34\x39\x2c\x31\x39\x31\x2e\x34\x35\x2c\x31\x31\ +\x30\x2c\x32\x31\x38\x2e\x31\x32\x2c\x37\x36\x2e\x34\x2c\x32\x35\ +\x33\x2e\x38\x35\x63\x2d\x38\x2e\x35\x2c\x39\x2d\x31\x38\x2e\x35\ +\x2c\x31\x32\x2e\x32\x33\x2d\x33\x30\x2c\x37\x2e\x39\x31\x2d\x31\ +\x39\x2e\x39\x33\x2d\x37\x2e\x35\x2d\x32\x35\x2d\x33\x33\x2e\x38\ +\x32\x2d\x39\x2e\x36\x36\x2d\x35\x30\x2e\x32\x31\x61\x33\x37\x37\ +\x2e\x33\x2c\x33\x37\x37\x2e\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x35\ +\x37\x2e\x31\x38\x2d\x35\x30\x63\x34\x32\x2e\x37\x2d\x33\x30\x2e\ +\x33\x35\x2c\x38\x39\x2e\x32\x34\x2d\x35\x30\x2e\x37\x31\x2c\x31\ +\x33\x39\x2e\x36\x39\x2d\x36\x30\x43\x32\x35\x35\x2e\x34\x31\x2c\ +\x39\x37\x2e\x35\x35\x2c\x32\x37\x37\x2e\x36\x2c\x39\x36\x2e\x31\ +\x38\x2c\x32\x39\x30\x2e\x32\x36\x2c\x39\x34\x2e\x37\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\x35\x35\x35\x2e\x33\x32\x2c\ -\x33\x39\x39\x2e\x37\x63\x30\x2d\x38\x2e\x37\x32\x2d\x34\x2e\x33\ -\x34\x2d\x31\x33\x2e\x32\x31\x2d\x31\x32\x2e\x38\x31\x2d\x31\x33\ -\x2e\x34\x32\x2d\x34\x2e\x35\x39\x2d\x2e\x31\x31\x2d\x39\x2e\x31\ -\x39\x2c\x30\x2d\x31\x34\x2e\x31\x31\x2c\x30\x2c\x30\x2d\x35\x2e\ -\x31\x35\x2e\x31\x39\x2d\x39\x2e\x33\x39\x2c\x30\x2d\x31\x33\x2e\ -\x36\x31\x2d\x2e\x37\x35\x2d\x31\x34\x2e\x31\x37\x2e\x32\x34\x2d\ -\x32\x38\x2e\x37\x39\x2d\x32\x2e\x38\x33\x2d\x34\x32\x2e\x34\x2d\ -\x38\x2e\x32\x31\x2d\x33\x36\x2e\x32\x39\x2d\x34\x33\x2d\x36\x30\ -\x2e\x31\x39\x2d\x37\x38\x2e\x31\x34\x2d\x35\x35\x2e\x34\x38\x2d\ -\x33\x36\x2e\x37\x32\x2c\x34\x2e\x39\x32\x2d\x36\x33\x2e\x36\x33\ -\x2c\x33\x36\x2e\x37\x34\x2d\x36\x33\x2e\x35\x31\x2c\x37\x35\x2e\ -\x30\x36\x2c\x30\x2c\x31\x31\x2e\x38\x38\x2c\x30\x2c\x32\x33\x2e\ -\x37\x35\x2c\x30\x2c\x33\x36\x2e\x34\x31\x68\x2d\x37\x2e\x34\x33\ -\x63\x2d\x32\x2e\x34\x35\x2c\x30\x2d\x34\x2e\x39\x2d\x2e\x30\x35\ -\x2d\x37\x2e\x33\x34\x2c\x30\x2d\x38\x2e\x35\x33\x2e\x32\x38\x2d\ -\x31\x32\x2e\x31\x32\x2c\x34\x2e\x31\x36\x2d\x31\x32\x2e\x31\x32\ -\x2c\x31\x33\x2e\x31\x31\x71\x30\x2c\x35\x34\x2e\x38\x37\x2c\x30\ -\x2c\x31\x30\x39\x2e\x37\x35\x63\x30\x2c\x31\x30\x2e\x34\x34\x2c\ -\x33\x2e\x35\x39\x2c\x31\x34\x2e\x33\x31\x2c\x31\x33\x2e\x35\x32\ -\x2c\x31\x34\x2e\x33\x32\x71\x38\x35\x2e\x36\x33\x2c\x30\x2c\x31\ -\x37\x31\x2e\x32\x37\x2c\x30\x63\x39\x2e\x32\x34\x2c\x30\x2c\x31\ -\x33\x2e\x35\x33\x2d\x34\x2e\x34\x33\x2c\x31\x33\x2e\x35\x33\x2d\ -\x31\x34\x51\x35\x35\x35\x2e\x34\x2c\x34\x35\x34\x2e\x35\x38\x2c\ -\x35\x35\x35\x2e\x33\x32\x2c\x33\x39\x39\x2e\x37\x5a\x6d\x2d\x35\ -\x35\x2e\x39\x34\x2d\x31\x33\x2e\x38\x31\x48\x34\x31\x33\x2e\x32\ -\x63\x30\x2d\x31\x35\x2d\x31\x2e\x33\x33\x2d\x32\x39\x2e\x37\x39\ -\x2e\x33\x31\x2d\x34\x34\x2e\x31\x39\x2c\x32\x2e\x35\x32\x2d\x32\ -\x32\x2e\x30\x38\x2c\x32\x32\x2e\x36\x31\x2d\x33\x38\x2e\x33\x38\ -\x2c\x34\x33\x2e\x35\x37\x2d\x33\x37\x2e\x34\x39\x61\x34\x33\x2e\ -\x39\x34\x2c\x34\x33\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x34\ -\x32\x2e\x31\x36\x2c\x34\x31\x2e\x35\x33\x43\x35\x30\x30\x2c\x33\ -\x35\x39\x2c\x34\x39\x39\x2e\x33\x38\x2c\x33\x37\x32\x2e\x34\x31\ -\x2c\x34\x39\x39\x2e\x33\x38\x2c\x33\x38\x35\x2e\x38\x39\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\x33\x36\x38\x2e\x38\x34\ -\x2c\x33\x37\x37\x2e\x37\x38\x63\x31\x2e\x38\x38\x2d\x2e\x30\x36\ -\x2c\x33\x2e\x37\x31\x2c\x30\x2c\x35\x2e\x34\x37\x2c\x30\x68\x31\ -\x2e\x30\x39\x76\x2d\x33\x2e\x34\x39\x63\x30\x2d\x38\x2e\x32\x39\ -\x2c\x30\x2d\x31\x36\x2e\x33\x34\x2c\x30\x2d\x32\x34\x2e\x33\x39\ -\x61\x38\x39\x2e\x37\x37\x2c\x38\x39\x2e\x37\x37\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x2e\x35\x35\x2d\x31\x30\x63\x2d\x32\x38\x2e\x38\x39\ -\x2d\x31\x38\x2e\x33\x31\x2d\x36\x31\x2e\x32\x36\x2d\x32\x35\x2e\ -\x32\x33\x2d\x39\x37\x2e\x31\x37\x2d\x32\x30\x2e\x31\x37\x2d\x33\ -\x32\x2e\x38\x2c\x34\x2e\x36\x33\x2d\x36\x30\x2e\x38\x2c\x32\x30\ -\x2e\x30\x39\x2d\x38\x34\x2e\x36\x34\x2c\x34\x34\x2e\x34\x34\x2d\ -\x36\x2c\x36\x2e\x31\x36\x2d\x39\x2e\x36\x33\x2c\x31\x33\x2e\x35\ -\x39\x2d\x39\x2e\x36\x31\x2c\x32\x32\x2e\x38\x31\x61\x36\x39\x2e\ -\x32\x38\x2c\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x37\x2e\x33\x38\x63\x35\x2e\x36\x33\x2c\x32\x33\x2e\x32\x37\ -\x2c\x33\x31\x2e\x32\x37\x2c\x33\x30\x2e\x32\x38\x2c\x34\x37\x2e\ -\x39\x33\x2c\x31\x33\x2e\x32\x37\x2c\x32\x34\x2e\x36\x31\x2d\x32\ -\x35\x2e\x31\x32\x2c\x35\x33\x2e\x37\x35\x2d\x33\x33\x2e\x38\x37\ -\x2c\x38\x37\x2d\x32\x36\x2e\x37\x37\x41\x38\x33\x2e\x37\x35\x2c\ -\x38\x33\x2e\x37\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x34\x39\x2e\ -\x31\x35\x2c\x33\x39\x33\x43\x33\x35\x31\x2e\x31\x37\x2c\x33\x38\ -\x33\x2e\x34\x37\x2c\x33\x35\x38\x2c\x33\x37\x38\x2e\x31\x33\x2c\ -\x33\x36\x38\x2e\x38\x34\x2c\x33\x37\x37\x2e\x37\x38\x5a\x22\x2f\ -\x3e\x3c\x72\x65\x63\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x32\x22\x20\x78\x3d\x22\x34\x34\x37\x2e\x36\x39\x22\x20\ -\x79\x3d\x22\x33\x38\x36\x2e\x31\x33\x22\x20\x77\x69\x64\x74\x68\ -\x3d\x22\x31\x36\x2e\x39\x34\x22\x20\x68\x65\x69\x67\x68\x74\x3d\ -\x22\x31\x32\x37\x2e\x36\x31\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\ -\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x34\x35\ -\x31\x2e\x37\x35\x20\x2d\x31\x39\x30\x2e\x37\x37\x29\x20\x72\x6f\ -\x74\x61\x74\x65\x28\x34\x35\x29\x22\x2f\x3e\x3c\x72\x65\x63\x74\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x78\ -\x3d\x22\x34\x34\x37\x2e\x36\x39\x22\x20\x79\x3d\x22\x33\x38\x36\ -\x2e\x31\x33\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x31\x36\x2e\x39\ -\x34\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x31\x32\x37\x2e\x36\ -\x31\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\ -\x61\x6e\x73\x6c\x61\x74\x65\x28\x31\x30\x39\x36\x2e\x38\x36\x20\ -\x34\x34\x35\x2e\x35\x33\x29\x20\x72\x6f\x74\x61\x74\x65\x28\x31\ -\x33\x35\x29\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x36\x38\x2e\x35\x31\x2c\ +\x33\x34\x38\x63\x2d\x31\x30\x2e\x39\x31\x2e\x31\x38\x2d\x31\x37\ +\x2e\x36\x39\x2d\x33\x2e\x30\x35\x2d\x32\x33\x2e\x33\x34\x2d\x39\ +\x2e\x30\x36\x2d\x33\x32\x2e\x31\x32\x2d\x33\x34\x2e\x31\x38\x2d\ +\x37\x30\x2e\x35\x39\x2d\x35\x35\x2e\x35\x2d\x31\x31\x35\x2e\x33\ +\x36\x2d\x36\x31\x2e\x39\x33\x2d\x36\x36\x2e\x38\x35\x2d\x39\x2e\ +\x35\x39\x2d\x31\x32\x35\x2e\x32\x2c\x31\x30\x2e\x37\x35\x2d\x31\ +\x37\x34\x2c\x36\x30\x2e\x39\x34\x2d\x31\x36\x2e\x32\x32\x2c\x31\ +\x36\x2e\x36\x37\x2d\x34\x30\x2e\x33\x36\x2c\x31\x31\x2e\x39\x31\ +\x2d\x34\x37\x2e\x35\x34\x2d\x31\x30\x2d\x33\x2e\x38\x38\x2d\x31\ +\x31\x2e\x38\x2d\x31\x2e\x33\x37\x2d\x32\x32\x2e\x36\x32\x2c\x36\ +\x2e\x38\x34\x2d\x33\x31\x2e\x32\x37\x2c\x34\x30\x2e\x36\x38\x2d\ +\x34\x32\x2e\x39\x31\x2c\x38\x39\x2e\x30\x36\x2d\x36\x39\x2e\x39\ +\x34\x2c\x31\x34\x35\x2e\x37\x31\x2d\x37\x38\x2e\x36\x38\x2c\x37\ +\x35\x2d\x31\x31\x2e\x35\x37\x2c\x31\x34\x32\x2e\x35\x39\x2c\x38\ +\x2c\x32\x30\x32\x2e\x33\x35\x2c\x35\x37\x2e\x39\x32\x61\x32\x30\ +\x33\x2e\x34\x37\x2c\x32\x30\x33\x2e\x34\x37\x2c\x30\x2c\x30\x2c\ +\x31\x2c\x32\x33\x2e\x30\x37\x2c\x32\x32\x2e\x35\x32\x63\x38\x2c\ +\x39\x2e\x32\x32\x2c\x39\x2e\x34\x33\x2c\x32\x30\x2e\x34\x36\x2c\ +\x34\x2e\x35\x36\x2c\x33\x32\x2e\x30\x38\x53\x34\x37\x37\x2e\x31\ +\x39\x2c\x33\x34\x37\x2e\x34\x31\x2c\x34\x36\x38\x2e\x35\x31\x2c\ +\x33\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\x31\ +\x38\x35\x2e\x33\x36\x2c\x34\x30\x32\x2e\x37\x33\x63\x30\x2d\x39\ +\x2e\x31\x35\x2c\x33\x2e\x35\x35\x2d\x31\x36\x2e\x35\x32\x2c\x39\ +\x2e\x35\x34\x2d\x32\x32\x2e\x36\x34\x2c\x32\x33\x2e\x36\x36\x2d\ +\x32\x34\x2e\x31\x35\x2c\x35\x31\x2e\x34\x34\x2d\x33\x39\x2e\x35\ +\x2c\x38\x34\x2d\x34\x34\x2e\x30\x39\x2c\x34\x38\x2e\x32\x38\x2d\ +\x36\x2e\x38\x31\x2c\x39\x30\x2e\x31\x34\x2c\x38\x2e\x31\x39\x2c\ +\x31\x32\x35\x2e\x36\x37\x2c\x34\x33\x2e\x36\x32\x2c\x31\x30\x2e\ +\x30\x39\x2c\x31\x30\x2e\x30\x37\x2c\x31\x32\x2e\x37\x2c\x32\x33\ +\x2e\x38\x32\x2c\x37\x2e\x33\x39\x2c\x33\x35\x2e\x37\x37\x2d\x38\ +\x2e\x33\x39\x2c\x31\x38\x2e\x38\x35\x2d\x33\x30\x2e\x37\x37\x2c\ +\x32\x33\x2e\x32\x37\x2d\x34\x35\x2c\x38\x2e\x36\x2d\x31\x33\x2e\ +\x33\x2d\x31\x33\x2e\x37\x33\x2d\x32\x38\x2e\x35\x38\x2d\x32\x33\ +\x2e\x34\x35\x2d\x34\x36\x2e\x37\x35\x2d\x32\x37\x2e\x33\x33\x2d\ +\x33\x33\x2d\x37\x2e\x30\x35\x2d\x36\x31\x2e\x38\x38\x2c\x31\x2e\ +\x36\x34\x2d\x38\x36\x2e\x33\x2c\x32\x36\x2e\x35\x36\x2d\x31\x36\ +\x2e\x35\x34\x2c\x31\x36\x2e\x38\x38\x2d\x34\x32\x2c\x39\x2e\x39\ +\x33\x2d\x34\x37\x2e\x35\x37\x2d\x31\x33\x2e\x31\x37\x41\x37\x30\ +\x2e\x34\x31\x2c\x37\x30\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\ +\x31\x38\x35\x2e\x33\x36\x2c\x34\x30\x32\x2e\x37\x33\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\x33\x30\x30\x2c\x35\x33\x37\ +\x2e\x33\x63\x2d\x32\x31\x2e\x38\x33\x2c\x30\x2d\x33\x38\x2e\x38\ +\x34\x2d\x31\x38\x2e\x33\x31\x2d\x33\x38\x2e\x38\x31\x2d\x34\x31\ +\x2e\x37\x39\x2c\x30\x2d\x32\x33\x2e\x31\x39\x2c\x31\x37\x2e\x30\ +\x36\x2d\x34\x31\x2e\x35\x31\x2c\x33\x38\x2e\x35\x36\x2d\x34\x31\ +\x2e\x34\x37\x2c\x32\x32\x2e\x30\x39\x2c\x30\x2c\x33\x39\x2c\x31\ +\x38\x2c\x33\x39\x2c\x34\x31\x2e\x34\x39\x53\x33\x32\x31\x2e\x39\ +\x31\x2c\x35\x33\x37\x2e\x32\x39\x2c\x33\x30\x30\x2c\x35\x33\x37\ +\x2e\x33\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ +\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x31\x35\ +\x36\x2e\x37\x36\x2c\x36\x31\x2c\x33\x30\x30\x2c\x32\x35\x39\x2e\ +\x38\x2c\x34\x34\x33\x2e\x32\x35\x2c\x36\x31\x68\x35\x37\x2e\x39\ +\x33\x4c\x33\x32\x39\x2c\x33\x30\x30\x2c\x35\x30\x31\x2e\x31\x39\ +\x2c\x35\x33\x39\x48\x34\x34\x33\x2e\x32\x35\x4c\x33\x30\x30\x2c\ +\x33\x34\x30\x2e\x31\x39\x2c\x31\x35\x36\x2e\x37\x36\x2c\x35\x33\ +\x39\x48\x39\x38\x2e\x38\x31\x4c\x32\x37\x31\x2c\x33\x30\x30\x2c\ +\x39\x38\x2e\x38\x33\x2c\x36\x31\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\ +\x67\x3e\ +\x00\x00\x04\x51\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\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\x37\x37\x2e\x38\x35\ +\x2c\x31\x39\x38\x2e\x36\x37\x63\x30\x2d\x38\x2e\x33\x31\x2d\x32\ +\x2e\x35\x39\x2d\x31\x34\x2e\x37\x37\x2d\x37\x2e\x37\x36\x2d\x31\ +\x39\x2e\x33\x35\x2d\x35\x2e\x31\x38\x2d\x34\x2e\x35\x38\x2d\x31\ +\x33\x2e\x30\x33\x2d\x36\x2e\x38\x37\x2d\x32\x33\x2e\x35\x35\x2d\ +\x36\x2e\x38\x37\x68\x2d\x32\x38\x76\x35\x32\x2e\x31\x39\x68\x32\ +\x38\x63\x31\x30\x2e\x35\x32\x2c\x30\x2c\x31\x38\x2e\x33\x37\x2d\ +\x32\x2e\x32\x39\x2c\x32\x33\x2e\x35\x35\x2d\x36\x2e\x38\x37\x2c\ +\x35\x2e\x31\x37\x2d\x34\x2e\x35\x38\x2c\x37\x2e\x37\x36\x2d\x31\ +\x30\x2e\x39\x34\x2c\x37\x2e\x37\x36\x2d\x31\x39\x2e\x30\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\x33\x35\ +\x31\x2e\x34\x36\x2c\x34\x38\x31\x2e\x37\x39\x76\x2d\x36\x2e\x37\ +\x37\x63\x30\x2d\x38\x2e\x39\x38\x2d\x37\x2e\x32\x38\x2d\x31\x36\ +\x2e\x32\x35\x2d\x31\x36\x2e\x32\x35\x2d\x31\x36\x2e\x32\x35\x68\ +\x2d\x31\x33\x2e\x35\x34\x76\x2d\x35\x34\x2e\x38\x34\x68\x31\x30\ +\x32\x2e\x39\x31\x63\x33\x32\x2e\x31\x36\x2c\x30\x2c\x35\x38\x2e\ +\x32\x33\x2d\x32\x36\x2e\x30\x37\x2c\x35\x38\x2e\x32\x33\x2d\x35\ +\x38\x2e\x32\x33\x56\x39\x36\x2e\x35\x34\x63\x30\x2d\x33\x32\x2e\ +\x31\x36\x2d\x32\x36\x2e\x30\x37\x2d\x35\x38\x2e\x32\x33\x2d\x35\ +\x38\x2e\x32\x33\x2d\x35\x38\x2e\x32\x33\x68\x2d\x32\x34\x39\x2e\ +\x31\x36\x63\x2d\x33\x32\x2e\x31\x36\x2c\x30\x2d\x35\x38\x2e\x32\ +\x33\x2c\x32\x36\x2e\x30\x37\x2d\x35\x38\x2e\x32\x33\x2c\x35\x38\ +\x2e\x32\x33\x76\x32\x34\x39\x2e\x31\x36\x63\x30\x2c\x33\x32\x2e\ +\x31\x36\x2c\x32\x36\x2e\x30\x37\x2c\x35\x38\x2e\x32\x33\x2c\x35\ +\x38\x2e\x32\x33\x2c\x35\x38\x2e\x32\x33\x68\x31\x30\x32\x2e\x39\ +\x31\x76\x35\x34\x2e\x38\x34\x68\x2d\x31\x33\x2e\x35\x34\x63\x2d\ +\x38\x2e\x39\x37\x2c\x30\x2d\x31\x36\x2e\x32\x35\x2c\x37\x2e\x32\ +\x37\x2d\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\x32\x35\x76\x36\x2e\ +\x37\x37\x48\x38\x38\x2e\x37\x36\x76\x35\x36\x2e\x38\x37\x68\x31\ +\x35\x39\x2e\x37\x39\x76\x36\x2e\x37\x37\x63\x30\x2c\x38\x2e\x39\ +\x38\x2c\x37\x2e\x32\x38\x2c\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\ +\x32\x35\x2c\x31\x36\x2e\x32\x35\x68\x37\x30\x2e\x34\x31\x63\x38\ +\x2e\x39\x37\x2c\x30\x2c\x31\x36\x2e\x32\x35\x2d\x37\x2e\x32\x37\ +\x2c\x31\x36\x2e\x32\x35\x2d\x31\x36\x2e\x32\x35\x76\x2d\x36\x2e\ +\x37\x37\x68\x31\x35\x39\x2e\x37\x39\x76\x2d\x35\x36\x2e\x38\x37\ +\x68\x2d\x31\x35\x39\x2e\x37\x39\x5a\x4d\x32\x33\x32\x2e\x32\x34\ +\x2c\x33\x31\x30\x2e\x39\x33\x68\x2d\x35\x30\x2e\x34\x31\x76\x2d\ +\x31\x37\x38\x2e\x32\x68\x35\x30\x2e\x34\x31\x76\x31\x37\x38\x2e\ +\x32\x5a\x4d\x33\x31\x38\x2e\x35\x34\x2c\x33\x31\x30\x2e\x39\x33\ +\x68\x2d\x35\x30\x2e\x34\x31\x76\x2d\x31\x37\x38\x2e\x32\x68\x38\ +\x31\x2e\x34\x36\x63\x31\x36\x2e\x31\x32\x2c\x30\x2c\x33\x30\x2e\ +\x31\x32\x2c\x32\x2e\x36\x37\x2c\x34\x32\x2c\x38\x2e\x30\x32\x2c\ +\x31\x31\x2e\x38\x38\x2c\x35\x2e\x33\x35\x2c\x32\x31\x2e\x30\x34\ +\x2c\x31\x32\x2e\x39\x34\x2c\x32\x37\x2e\x35\x2c\x32\x32\x2e\x37\ +\x38\x2c\x36\x2e\x34\x35\x2c\x39\x2e\x38\x35\x2c\x39\x2e\x36\x37\ +\x2c\x32\x31\x2e\x35\x36\x2c\x39\x2e\x36\x37\x2c\x33\x35\x2e\x31\ +\x33\x73\x2d\x33\x2e\x32\x33\x2c\x32\x35\x2e\x30\x34\x2d\x39\x2e\ +\x36\x37\x2c\x33\x34\x2e\x38\x38\x63\x2d\x36\x2e\x34\x35\x2c\x39\ +\x2e\x38\x35\x2d\x31\x35\x2e\x36\x32\x2c\x31\x37\x2e\x34\x34\x2d\ +\x32\x37\x2e\x35\x2c\x32\x32\x2e\x37\x38\x2d\x31\x31\x2e\x38\x38\ +\x2c\x35\x2e\x33\x35\x2d\x32\x35\x2e\x38\x38\x2c\x38\x2e\x30\x32\ +\x2d\x34\x32\x2c\x38\x2e\x30\x32\x68\x2d\x33\x31\x2e\x30\x36\x76\ +\x34\x36\x2e\x35\x39\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ +\ \x00\x00\x05\x80\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -20436,165 +20447,88 @@ \x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\ \x32\x33\x2c\x33\x30\x30\x2c\x35\x32\x33\x5a\x22\x2f\x3e\x3c\x2f\ \x73\x76\x67\x3e\ -\x00\x00\x09\xc5\ +\x00\x00\x04\xfe\ \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\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x35\x66\x62\ -\x62\x34\x36\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\x32\x39\x30\x2e\ -\x31\x38\x2c\x37\x36\x2e\x35\x33\x43\x34\x30\x32\x2c\x37\x38\x2e\ -\x33\x31\x2c\x34\x39\x30\x2e\x31\x2c\x31\x31\x36\x2e\x36\x31\x2c\ -\x35\x36\x33\x2e\x39\x2c\x31\x39\x32\x2e\x36\x33\x63\x31\x34\x2e\ -\x38\x2c\x31\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\ -\x2e\x39\x33\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2d\x31\x31\x2e\x31\ -\x32\x2c\x31\x33\x2e\x31\x36\x2d\x32\x38\x2e\x36\x32\x2c\x31\x33\ -\x2d\x34\x31\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\x30\x38\x2d\ -\x31\x34\x2e\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\x2e\x37\x31\ -\x2d\x34\x36\x2e\x36\x31\x2d\x34\x31\x2e\x31\x2d\x33\x38\x2e\x34\ -\x2d\x32\x39\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\x37\x35\x2d\ -\x31\x32\x37\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\x35\x34\x2d\ -\x39\x2e\x31\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\x2e\x33\x32\ -\x2d\x31\x35\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x2d\x34\x35\x2c\ -\x31\x37\x2e\x30\x35\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\ -\x33\x2d\x31\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x33\x2d\x38\ -\x2e\x35\x37\x2c\x39\x2e\x31\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\ -\x32\x2e\x33\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\ -\x30\x39\x2d\x37\x2e\x35\x36\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\ -\x30\x38\x2d\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x41\x33\x38\x30\ -\x2c\x33\x38\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x39\x32\x2e\x33\x37\ -\x2c\x31\x34\x33\x2e\x39\x63\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\ -\x38\x39\x2e\x39\x33\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\ -\x37\x2d\x36\x30\x2e\x34\x36\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\ -\x39\x2e\x34\x31\x2c\x32\x37\x37\x2e\x34\x33\x2c\x37\x38\x2c\x32\ -\x39\x30\x2e\x31\x38\x2c\x37\x36\x2e\x35\x33\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\x33\x30\x30\x2c\x35\x32\x32\x2e\x35\ -\x35\x63\x2d\x32\x32\x2c\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\ -\x2e\x34\x36\x2d\x33\x39\x2e\x31\x31\x2d\x34\x32\x2e\x31\x32\x2c\ -\x30\x2d\x32\x33\x2e\x33\x37\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\ -\x2e\x38\x32\x2c\x33\x38\x2e\x38\x36\x2d\x34\x31\x2e\x37\x39\x2c\ -\x32\x32\x2e\x32\x35\x2c\x30\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\ -\x2e\x31\x38\x2c\x33\x39\x2e\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\ -\x33\x32\x32\x2e\x31\x2c\x35\x32\x32\x2e\x35\x34\x2c\x33\x30\x30\ -\x2c\x35\x32\x32\x2e\x35\x35\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\x35\x36\x2e\x37\x32\x2c\x33\x31\x32\x2e\x37\x61\ -\x33\x32\x2e\x35\x33\x2c\x33\x32\x2e\x35\x33\x2c\x30\x2c\x30\x2c\ -\x30\x2d\x31\x36\x2e\x34\x31\x2c\x33\x2e\x37\x32\x71\x33\x2e\x30\ -\x36\x2c\x33\x2c\x36\x2c\x36\x2e\x32\x31\x63\x35\x2e\x36\x39\x2c\ -\x36\x2e\x30\x35\x2c\x31\x32\x2e\x35\x33\x2c\x39\x2e\x33\x2c\x32\ -\x33\x2e\x35\x32\x2c\x39\x2e\x31\x32\x61\x32\x33\x2e\x39\x2c\x32\ -\x33\x2e\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x33\x2e\x35\x2d\x35\ -\x2e\x33\x31\x41\x33\x35\x2e\x35\x33\x2c\x33\x35\x2e\x35\x33\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x34\x35\x36\x2e\x37\x32\x2c\x33\x31\x32\ -\x2e\x37\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\x34\ -\x36\x2e\x32\x36\x2c\x32\x36\x36\x2e\x33\x34\x61\x37\x36\x2e\x32\ -\x33\x2c\x37\x36\x2e\x32\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x37\ -\x2e\x39\x31\x2c\x31\x2e\x34\x37\x71\x2d\x34\x2e\x37\x36\x2d\x34\ -\x2e\x35\x2d\x39\x2e\x37\x33\x2d\x38\x2e\x36\x35\x63\x2d\x36\x30\ -\x2e\x32\x32\x2d\x35\x30\x2e\x33\x31\x2d\x31\x32\x38\x2e\x33\x31\ -\x2d\x37\x30\x2d\x32\x30\x33\x2e\x39\x31\x2d\x35\x38\x2e\x33\x37\ -\x2d\x35\x37\x2e\x30\x39\x2c\x38\x2e\x38\x31\x2d\x31\x30\x35\x2e\ -\x38\x34\x2c\x33\x36\x2e\x30\x35\x2d\x31\x34\x36\x2e\x38\x34\x2c\ -\x37\x39\x2e\x32\x38\x2d\x38\x2e\x32\x37\x2c\x38\x2e\x37\x33\x2d\ -\x31\x30\x2e\x37\x39\x2c\x31\x39\x2e\x36\x33\x2d\x36\x2e\x38\x39\ -\x2c\x33\x31\x2e\x35\x33\x2c\x37\x2e\x32\x34\x2c\x32\x32\x2c\x33\ -\x31\x2e\x35\x36\x2c\x32\x36\x2e\x38\x33\x2c\x34\x37\x2e\x39\x31\ -\x2c\x31\x30\x2c\x34\x39\x2e\x32\x32\x2d\x35\x30\x2e\x35\x37\x2c\ -\x31\x30\x38\x2d\x37\x31\x2e\x30\x37\x2c\x31\x37\x35\x2e\x33\x39\ -\x2d\x36\x31\x2e\x34\x61\x31\x38\x37\x2c\x31\x38\x37\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x37\x32\x2e\x36\x36\x2c\x32\x36\x2e\x33\x39\x41\ -\x38\x30\x2e\x36\x2c\x38\x30\x2e\x36\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x34\x34\x36\x2e\x32\x36\x2c\x32\x36\x36\x2e\x33\x34\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\x33\x36\x38\x2e\x38\x34\x2c\ -\x33\x37\x37\x2e\x37\x38\x63\x31\x2e\x38\x38\x2d\x2e\x30\x36\x2c\ -\x33\x2e\x37\x31\x2c\x30\x2c\x35\x2e\x34\x37\x2c\x30\x68\x31\x2e\ -\x30\x39\x76\x2d\x33\x2e\x34\x39\x63\x30\x2d\x38\x2e\x32\x39\x2c\ -\x30\x2d\x31\x36\x2e\x33\x34\x2c\x30\x2d\x32\x34\x2e\x33\x39\x61\ -\x38\x39\x2e\x37\x37\x2c\x38\x39\x2e\x37\x37\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x2e\x35\x35\x2d\x31\x30\x63\x2d\x32\x38\x2e\x38\x39\x2d\ -\x31\x38\x2e\x33\x31\x2d\x36\x31\x2e\x32\x36\x2d\x32\x35\x2e\x32\ -\x33\x2d\x39\x37\x2e\x31\x37\x2d\x32\x30\x2e\x31\x37\x2d\x33\x32\ -\x2e\x38\x2c\x34\x2e\x36\x33\x2d\x36\x30\x2e\x38\x2c\x32\x30\x2e\ -\x30\x39\x2d\x38\x34\x2e\x36\x34\x2c\x34\x34\x2e\x34\x34\x2d\x36\ -\x2c\x36\x2e\x31\x36\x2d\x39\x2e\x36\x33\x2c\x31\x33\x2e\x35\x39\ -\x2d\x39\x2e\x36\x31\x2c\x32\x32\x2e\x38\x31\x61\x36\x39\x2e\x32\ -\x38\x2c\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x37\x2e\x33\x38\x63\x35\x2e\x36\x33\x2c\x32\x33\x2e\x32\x37\x2c\ -\x33\x31\x2e\x32\x37\x2c\x33\x30\x2e\x32\x38\x2c\x34\x37\x2e\x39\ -\x33\x2c\x31\x33\x2e\x32\x37\x2c\x32\x34\x2e\x36\x31\x2d\x32\x35\ -\x2e\x31\x32\x2c\x35\x33\x2e\x37\x35\x2d\x33\x33\x2e\x38\x37\x2c\ -\x38\x37\x2d\x32\x36\x2e\x37\x37\x41\x38\x33\x2e\x37\x35\x2c\x38\ -\x33\x2e\x37\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x34\x39\x2e\x31\ -\x35\x2c\x33\x39\x33\x43\x33\x35\x31\x2e\x31\x37\x2c\x33\x38\x33\ -\x2e\x34\x37\x2c\x33\x35\x38\x2c\x33\x37\x38\x2e\x31\x33\x2c\x33\ -\x36\x38\x2e\x38\x34\x2c\x33\x37\x37\x2e\x37\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\x35\x34\x32\x2e\x35\x31\x2c\x33\ -\x38\x36\x2e\x32\x38\x63\x2d\x34\x2e\x35\x39\x2d\x2e\x31\x31\x2d\ -\x39\x2e\x31\x39\x2c\x30\x2d\x31\x34\x2e\x31\x31\x2c\x30\x76\x2d\ -\x2e\x33\x37\x48\x34\x31\x33\x2e\x32\x63\x30\x2d\x31\x35\x2d\x31\ -\x2e\x33\x33\x2d\x32\x39\x2e\x37\x39\x2e\x33\x31\x2d\x34\x34\x2e\ -\x31\x39\x2c\x32\x2e\x35\x32\x2d\x32\x32\x2e\x30\x38\x2c\x32\x32\ -\x2e\x36\x31\x2d\x33\x38\x2e\x33\x38\x2c\x34\x33\x2e\x35\x37\x2d\ -\x33\x37\x2e\x34\x39\x61\x34\x34\x2c\x34\x34\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x33\x34\x2e\x37\x35\x2c\x31\x39\x2e\x34\x31\x6c\x32\x31\ -\x2e\x32\x34\x2d\x32\x30\x2e\x34\x34\x63\x2d\x31\x35\x2e\x31\x34\ -\x2d\x32\x30\x2e\x33\x2d\x34\x30\x2e\x33\x33\x2d\x33\x31\x2e\x38\ -\x31\x2d\x36\x35\x2e\x36\x37\x2d\x32\x38\x2e\x34\x31\x2d\x33\x36\ -\x2e\x37\x32\x2c\x34\x2e\x39\x32\x2d\x36\x33\x2e\x36\x33\x2c\x33\ -\x36\x2e\x37\x34\x2d\x36\x33\x2e\x35\x31\x2c\x37\x35\x2e\x30\x36\ -\x2c\x30\x2c\x31\x31\x2e\x38\x38\x2c\x30\x2c\x32\x33\x2e\x37\x35\ -\x2c\x30\x2c\x33\x36\x2e\x34\x31\x68\x2d\x37\x2e\x34\x33\x63\x2d\ -\x32\x2e\x34\x35\x2c\x30\x2d\x34\x2e\x39\x2d\x2e\x30\x35\x2d\x37\ -\x2e\x33\x34\x2c\x30\x2d\x38\x2e\x35\x33\x2e\x32\x38\x2d\x31\x32\ -\x2e\x31\x32\x2c\x34\x2e\x31\x36\x2d\x31\x32\x2e\x31\x32\x2c\x31\ -\x33\x2e\x31\x31\x71\x30\x2c\x35\x34\x2e\x38\x37\x2c\x30\x2c\x31\ -\x30\x39\x2e\x37\x35\x63\x30\x2c\x31\x30\x2e\x34\x34\x2c\x33\x2e\ -\x35\x39\x2c\x31\x34\x2e\x33\x31\x2c\x31\x33\x2e\x35\x32\x2c\x31\ -\x34\x2e\x33\x32\x71\x38\x35\x2e\x36\x33\x2c\x30\x2c\x31\x37\x31\ -\x2e\x32\x37\x2c\x30\x63\x39\x2e\x32\x34\x2c\x30\x2c\x31\x33\x2e\ -\x35\x33\x2d\x34\x2e\x34\x33\x2c\x31\x33\x2e\x35\x33\x2d\x31\x34\ -\x71\x2e\x30\x36\x2d\x35\x34\x2e\x38\x37\x2c\x30\x2d\x31\x30\x39\ -\x2e\x37\x35\x43\x35\x35\x35\x2e\x33\x31\x2c\x33\x39\x31\x2c\x35\ -\x35\x31\x2c\x33\x38\x36\x2e\x34\x39\x2c\x35\x34\x32\x2e\x35\x31\ -\x2c\x33\x38\x36\x2e\x32\x38\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\ -\x3d\x22\x4d\x35\x32\x38\x2e\x32\x39\x2c\x34\x31\x36\x2e\x36\x38\ -\x63\x2d\x2e\x31\x37\x2e\x32\x31\x2d\x2e\x32\x39\x2e\x34\x2d\x2e\ -\x34\x34\x2e\x35\x36\x6c\x2d\x2e\x37\x36\x2e\x37\x37\x71\x2d\x34\ -\x34\x2e\x36\x39\x2c\x34\x34\x2e\x36\x37\x2d\x38\x39\x2e\x33\x34\ -\x2c\x38\x39\x2e\x33\x36\x63\x2d\x31\x2c\x31\x2d\x31\x2e\x35\x34\ -\x2c\x31\x2e\x32\x37\x2d\x32\x2e\x37\x32\x2e\x30\x37\x71\x2d\x31\ -\x39\x2e\x33\x36\x2d\x31\x39\x2e\x35\x33\x2d\x33\x38\x2e\x38\x39\ -\x2d\x33\x38\x2e\x39\x31\x63\x2d\x2e\x38\x39\x2d\x2e\x38\x38\x2d\ -\x31\x2e\x30\x38\x2d\x31\x2e\x33\x2d\x2e\x30\x35\x2d\x32\x2e\x32\ -\x39\x2c\x33\x2e\x35\x35\x2d\x33\x2e\x33\x38\x2c\x37\x2d\x36\x2e\ -\x38\x36\x2c\x31\x30\x2e\x34\x2d\x31\x30\x2e\x34\x2c\x31\x2e\x31\ -\x31\x2d\x31\x2e\x31\x36\x2c\x31\x2e\x37\x35\x2d\x31\x2e\x31\x35\ -\x2c\x32\x2e\x38\x37\x2c\x30\x2c\x38\x2e\x35\x38\x2c\x38\x2e\x36\ -\x38\x2c\x31\x37\x2e\x32\x34\x2c\x31\x37\x2e\x32\x38\x2c\x32\x35\ -\x2e\x38\x33\x2c\x32\x35\x2e\x39\x35\x2e\x39\x33\x2e\x39\x34\x2c\ -\x31\x2e\x33\x39\x2c\x31\x2e\x30\x36\x2c\x32\x2e\x34\x32\x2c\x30\ -\x71\x33\x38\x2e\x32\x2d\x33\x38\x2e\x33\x32\x2c\x37\x36\x2e\x34\ -\x38\x2d\x37\x36\x2e\x35\x38\x63\x31\x2e\x31\x33\x2d\x31\x2e\x31\ -\x33\x2c\x31\x2e\x37\x33\x2d\x31\x2e\x32\x35\x2c\x32\x2e\x38\x38\ -\x2c\x30\x2c\x33\x2e\x31\x39\x2c\x33\x2e\x33\x37\x2c\x36\x2e\x35\ -\x33\x2c\x36\x2e\x35\x39\x2c\x39\x2e\x38\x31\x2c\x39\x2e\x38\x37\ -\x43\x35\x32\x37\x2e\x32\x38\x2c\x34\x31\x35\x2e\x35\x38\x2c\x35\ -\x32\x37\x2e\x37\x35\x2c\x34\x31\x36\x2e\x31\x31\x2c\x35\x32\x38\ -\x2e\x32\x39\x2c\x34\x31\x36\x2e\x36\x38\x5a\x22\x2f\x3e\x3c\x2f\ -\x73\x76\x67\x3e\ +\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\x39\x38\x2e\x38\x33\ +\x2c\x32\x31\x36\x2e\x38\x31\x68\x2d\x31\x39\x37\x2e\x36\x36\x63\ +\x2d\x31\x36\x2e\x36\x39\x2c\x30\x2d\x32\x30\x2e\x35\x35\x2c\x33\ +\x35\x2e\x36\x35\x2d\x31\x38\x2e\x30\x37\x2c\x35\x32\x2e\x31\x36\ +\x6c\x32\x31\x2e\x35\x31\x2c\x31\x32\x32\x2e\x32\x35\x63\x32\x2e\ +\x30\x31\x2c\x31\x33\x2e\x33\x35\x2c\x31\x33\x2e\x34\x38\x2c\x32\ +\x33\x2e\x32\x33\x2c\x32\x36\x2e\x39\x39\x2c\x32\x33\x2e\x32\x33\ +\x68\x31\x2e\x36\x32\x63\x2d\x35\x2e\x38\x37\x2c\x30\x2d\x31\x30\ +\x2e\x36\x32\x2c\x34\x2e\x37\x36\x2d\x31\x30\x2e\x36\x32\x2c\x31\ +\x30\x2e\x36\x32\x73\x34\x2e\x37\x36\x2c\x31\x30\x2e\x36\x32\x2c\ +\x31\x30\x2e\x36\x32\x2c\x31\x30\x2e\x36\x32\x63\x2d\x35\x2e\x38\ +\x37\x2c\x30\x2d\x31\x30\x2e\x36\x32\x2c\x34\x2e\x37\x36\x2d\x31\ +\x30\x2e\x36\x32\x2c\x31\x30\x2e\x36\x32\x73\x34\x2e\x37\x36\x2c\ +\x31\x30\x2e\x36\x32\x2c\x31\x30\x2e\x36\x32\x2c\x31\x30\x2e\x36\ +\x32\x63\x2d\x35\x2e\x38\x37\x2c\x30\x2d\x31\x30\x2e\x36\x32\x2c\ +\x34\x2e\x37\x36\x2d\x31\x30\x2e\x36\x32\x2c\x31\x30\x2e\x36\x32\ +\x73\x34\x2e\x37\x36\x2c\x31\x30\x2e\x36\x32\x2c\x31\x30\x2e\x36\ +\x32\x2c\x31\x30\x2e\x36\x32\x68\x33\x34\x2e\x39\x31\x76\x38\x33\ +\x2e\x34\x39\x68\x36\x33\x2e\x37\x35\x76\x2d\x38\x33\x2e\x34\x39\ +\x68\x33\x34\x2e\x39\x31\x63\x35\x2e\x38\x37\x2c\x30\x2c\x31\x30\ +\x2e\x36\x32\x2d\x34\x2e\x37\x36\x2c\x31\x30\x2e\x36\x32\x2d\x31\ +\x30\x2e\x36\x32\x73\x2d\x34\x2e\x37\x36\x2d\x31\x30\x2e\x36\x32\ +\x2d\x31\x30\x2e\x36\x32\x2d\x31\x30\x2e\x36\x32\x63\x35\x2e\x38\ +\x37\x2c\x30\x2c\x31\x30\x2e\x36\x32\x2d\x34\x2e\x37\x36\x2c\x31\ +\x30\x2e\x36\x32\x2d\x31\x30\x2e\x36\x32\x73\x2d\x34\x2e\x37\x36\ +\x2d\x31\x30\x2e\x36\x32\x2d\x31\x30\x2e\x36\x32\x2d\x31\x30\x2e\ +\x36\x32\x63\x35\x2e\x38\x37\x2c\x30\x2c\x31\x30\x2e\x36\x32\x2d\ +\x34\x2e\x37\x36\x2c\x31\x30\x2e\x36\x32\x2d\x31\x30\x2e\x36\x32\ +\x73\x2d\x34\x2e\x37\x36\x2d\x31\x30\x2e\x36\x32\x2d\x31\x30\x2e\ +\x36\x32\x2d\x31\x30\x2e\x36\x32\x68\x31\x2e\x36\x32\x63\x31\x33\ +\x2e\x35\x31\x2c\x30\x2c\x32\x34\x2e\x39\x38\x2d\x39\x2e\x38\x38\ +\x2c\x32\x36\x2e\x39\x39\x2d\x32\x33\x2e\x32\x33\x6c\x32\x31\x2e\ +\x35\x31\x2d\x31\x32\x32\x2e\x32\x35\x63\x32\x2e\x34\x38\x2d\x31\ +\x36\x2e\x35\x2d\x31\x2e\x33\x38\x2d\x35\x32\x2e\x31\x36\x2d\x31\ +\x38\x2e\x30\x37\x2d\x35\x32\x2e\x31\x36\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\x35\x37\x2e\x32\x2c\x35\ +\x38\x2e\x30\x35\x68\x2d\x34\x2e\x38\x31\x6c\x2d\x31\x2e\x31\x33\ +\x2d\x33\x2e\x38\x63\x30\x2d\x38\x2e\x38\x2d\x37\x2e\x31\x34\x2d\ +\x31\x35\x2e\x39\x34\x2d\x31\x35\x2e\x39\x34\x2d\x31\x35\x2e\x39\ +\x34\x68\x2d\x37\x30\x2e\x36\x34\x63\x2d\x38\x2e\x38\x2c\x30\x2d\ +\x31\x35\x2e\x39\x34\x2c\x37\x2e\x31\x33\x2d\x31\x35\x2e\x39\x34\ +\x2c\x31\x35\x2e\x39\x34\x6c\x2d\x31\x2e\x31\x33\x2c\x33\x2e\x38\ +\x68\x2d\x34\x2e\x38\x31\x63\x2d\x32\x32\x2e\x32\x36\x2c\x30\x2d\ +\x34\x30\x2e\x33\x31\x2c\x31\x38\x2e\x30\x35\x2d\x34\x30\x2e\x33\ +\x31\x2c\x34\x30\x2e\x33\x31\x76\x31\x30\x37\x2e\x34\x34\x68\x31\ +\x39\x35\x2e\x30\x33\x76\x2d\x31\x30\x37\x2e\x34\x34\x63\x30\x2d\ +\x32\x32\x2e\x32\x36\x2d\x31\x38\x2e\x30\x35\x2d\x34\x30\x2e\x33\ +\x31\x2d\x34\x30\x2e\x33\x31\x2d\x34\x30\x2e\x33\x31\x5a\x4d\x32\ +\x34\x32\x2e\x32\x2c\x31\x31\x34\x2e\x37\x31\x68\x2d\x31\x36\x2e\ +\x31\x39\x76\x2d\x34\x30\x2e\x35\x32\x68\x31\x36\x2e\x31\x39\x76\ +\x34\x30\x2e\x35\x32\x5a\x4d\x32\x36\x38\x2e\x35\x36\x2c\x31\x31\ +\x34\x2e\x37\x31\x68\x2d\x31\x36\x2e\x31\x39\x76\x2d\x34\x30\x2e\ +\x35\x32\x68\x31\x36\x2e\x31\x39\x76\x34\x30\x2e\x35\x32\x5a\x4d\ +\x32\x39\x34\x2e\x39\x32\x2c\x31\x31\x34\x2e\x37\x31\x68\x2d\x31\ +\x36\x2e\x31\x39\x76\x2d\x34\x30\x2e\x35\x32\x68\x31\x36\x2e\x31\ +\x39\x76\x34\x30\x2e\x35\x32\x5a\x4d\x33\x32\x31\x2e\x32\x37\x2c\ +\x31\x31\x34\x2e\x37\x31\x68\x2d\x31\x36\x2e\x31\x39\x76\x2d\x34\ +\x30\x2e\x35\x32\x68\x31\x36\x2e\x31\x39\x76\x34\x30\x2e\x35\x32\ +\x5a\x4d\x33\x34\x37\x2e\x36\x33\x2c\x31\x31\x34\x2e\x37\x31\x68\ +\x2d\x31\x36\x2e\x31\x39\x76\x2d\x34\x30\x2e\x35\x32\x68\x31\x36\ +\x2e\x31\x39\x76\x34\x30\x2e\x35\x32\x5a\x4d\x33\x37\x33\x2e\x39\ +\x39\x2c\x31\x31\x34\x2e\x37\x31\x68\x2d\x31\x36\x2e\x31\x39\x76\ +\x2d\x34\x30\x2e\x35\x32\x68\x31\x36\x2e\x31\x39\x76\x34\x30\x2e\ +\x35\x32\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ \x00\x00\x05\x95\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -20856,108 +20790,6 @@ \x38\x39\x2e\x36\x38\x2c\x36\x37\x2e\x34\x39\x2c\x36\x37\x2e\x34\ \x39\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x33\x36\x2e\x36\x32\x2c\x32\ \x37\x38\x2e\x34\x38\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x06\x37\ -\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\x64\x30\x64\x32\x64\x33\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x39\x32\x39\ -\x34\x39\x37\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\x32\x39\x30\x2e\ -\x32\x36\x2c\x39\x35\x2e\x35\x37\x63\x31\x31\x30\x2e\x39\x34\x2c\ -\x31\x2e\x37\x37\x2c\x31\x39\x38\x2e\x33\x39\x2c\x33\x39\x2e\x37\ -\x38\x2c\x32\x37\x31\x2e\x36\x32\x2c\x31\x31\x35\x2e\x32\x31\x2c\ -\x31\x34\x2e\x36\x39\x2c\x31\x35\x2e\x31\x34\x2c\x31\x31\x2e\x37\ -\x32\x2c\x33\x33\x2e\x36\x37\x2c\x33\x2e\x32\x34\x2c\x34\x33\x2e\ -\x37\x31\x2d\x31\x31\x2c\x31\x33\x2e\x30\x36\x2d\x32\x38\x2e\x34\ -\x2c\x31\x32\x2e\x38\x39\x2d\x34\x31\x2e\x31\x35\x2e\x38\x33\x2d\ -\x31\x35\x2d\x31\x34\x2e\x31\x36\x2d\x33\x30\x2d\x32\x38\x2e\x35\ -\x2d\x34\x36\x2e\x32\x35\x2d\x34\x30\x2e\x37\x39\x43\x34\x33\x39\ -\x2e\x36\x31\x2c\x31\x38\x35\x2e\x37\x34\x2c\x33\x39\x37\x2c\x31\ -\x36\x38\x2e\x31\x34\x2c\x33\x35\x30\x2e\x39\x34\x2c\x31\x36\x30\ -\x2e\x33\x63\x2d\x35\x33\x2e\x36\x2d\x39\x2e\x31\x33\x2d\x31\x30\ -\x36\x2e\x30\x39\x2d\x34\x2e\x32\x39\x2d\x31\x35\x37\x2e\x32\x38\ -\x2c\x31\x35\x2e\x31\x31\x43\x31\x34\x39\x2c\x31\x39\x32\x2e\x33\ -\x33\x2c\x31\x31\x30\x2c\x32\x31\x39\x2c\x37\x36\x2e\x34\x2c\x32\ -\x35\x34\x2e\x37\x33\x63\x2d\x38\x2e\x35\x2c\x39\x2d\x31\x38\x2e\ -\x35\x2c\x31\x32\x2e\x32\x33\x2d\x33\x30\x2c\x37\x2e\x39\x31\x2d\ -\x31\x39\x2e\x39\x33\x2d\x37\x2e\x35\x2d\x32\x35\x2d\x33\x33\x2e\ -\x38\x32\x2d\x39\x2e\x36\x36\x2d\x35\x30\x2e\x32\x32\x61\x33\x37\ -\x37\x2e\x32\x34\x2c\x33\x37\x37\x2e\x32\x34\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x35\x37\x2e\x31\x38\x2d\x35\x30\x63\x34\x32\x2e\x37\x2d\ -\x33\x30\x2e\x33\x35\x2c\x38\x39\x2e\x32\x34\x2d\x35\x30\x2e\x37\ -\x31\x2c\x31\x33\x39\x2e\x36\x39\x2d\x36\x30\x43\x32\x35\x35\x2e\ -\x34\x31\x2c\x39\x38\x2e\x34\x33\x2c\x32\x37\x37\x2e\x36\x2c\x39\ -\x37\x2e\x30\x35\x2c\x32\x39\x30\x2e\x32\x36\x2c\x39\x35\x2e\x35\ -\x37\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\x36\x38\ -\x2e\x35\x31\x2c\x33\x34\x38\x2e\x38\x34\x63\x2d\x31\x30\x2e\x39\ -\x31\x2e\x31\x38\x2d\x31\x37\x2e\x36\x39\x2d\x33\x2d\x32\x33\x2e\ -\x33\x34\x2d\x39\x2d\x33\x32\x2e\x31\x32\x2d\x33\x34\x2e\x31\x38\ -\x2d\x37\x30\x2e\x35\x39\x2d\x35\x35\x2e\x35\x2d\x31\x31\x35\x2e\ -\x33\x36\x2d\x36\x31\x2e\x39\x33\x2d\x36\x36\x2e\x38\x35\x2d\x39\ -\x2e\x35\x39\x2d\x31\x32\x35\x2e\x32\x2c\x31\x30\x2e\x37\x35\x2d\ -\x31\x37\x34\x2c\x36\x30\x2e\x39\x33\x2d\x31\x36\x2e\x32\x32\x2c\ -\x31\x36\x2e\x36\x38\x2d\x34\x30\x2e\x33\x36\x2c\x31\x31\x2e\x39\ -\x32\x2d\x34\x37\x2e\x35\x34\x2d\x31\x30\x2d\x33\x2e\x38\x38\x2d\ -\x31\x31\x2e\x38\x2d\x31\x2e\x33\x37\x2d\x32\x32\x2e\x36\x32\x2c\ -\x36\x2e\x38\x34\x2d\x33\x31\x2e\x32\x38\x2c\x34\x30\x2e\x36\x38\ -\x2d\x34\x32\x2e\x39\x2c\x38\x39\x2e\x30\x36\x2d\x36\x39\x2e\x39\ -\x33\x2c\x31\x34\x35\x2e\x37\x31\x2d\x37\x38\x2e\x36\x37\x2c\x37\ -\x35\x2d\x31\x31\x2e\x35\x37\x2c\x31\x34\x32\x2e\x35\x39\x2c\x38\ -\x2c\x32\x30\x32\x2e\x33\x35\x2c\x35\x37\x2e\x39\x31\x61\x32\x30\ -\x32\x2e\x37\x31\x2c\x32\x30\x32\x2e\x37\x31\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x32\x33\x2e\x30\x37\x2c\x32\x32\x2e\x35\x33\x63\x38\x2c\ -\x39\x2e\x32\x32\x2c\x39\x2e\x34\x33\x2c\x32\x30\x2e\x34\x35\x2c\ -\x34\x2e\x35\x36\x2c\x33\x32\x2e\x30\x38\x53\x34\x37\x37\x2e\x31\ -\x39\x2c\x33\x34\x38\x2e\x32\x39\x2c\x34\x36\x38\x2e\x35\x31\x2c\ -\x33\x34\x38\x2e\x38\x34\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\x31\x38\x35\x2e\x33\x36\x2c\x34\x30\x33\x2e\x36\x31\x63\ -\x30\x2d\x39\x2e\x31\x35\x2c\x33\x2e\x35\x35\x2d\x31\x36\x2e\x35\ -\x32\x2c\x39\x2e\x35\x34\x2d\x32\x32\x2e\x36\x34\x2c\x32\x33\x2e\ -\x36\x36\x2d\x32\x34\x2e\x31\x36\x2c\x35\x31\x2e\x34\x34\x2d\x33\ -\x39\x2e\x35\x2c\x38\x34\x2d\x34\x34\x2e\x30\x39\x2c\x34\x38\x2e\ -\x32\x38\x2d\x36\x2e\x38\x31\x2c\x39\x30\x2e\x31\x34\x2c\x38\x2e\ -\x31\x38\x2c\x31\x32\x35\x2e\x36\x37\x2c\x34\x33\x2e\x36\x32\x2c\ -\x31\x30\x2e\x30\x39\x2c\x31\x30\x2e\x30\x37\x2c\x31\x32\x2e\x37\ -\x2c\x32\x33\x2e\x38\x32\x2c\x37\x2e\x33\x39\x2c\x33\x35\x2e\x37\ -\x37\x2d\x38\x2e\x33\x39\x2c\x31\x38\x2e\x38\x34\x2d\x33\x30\x2e\ -\x37\x37\x2c\x32\x33\x2e\x32\x37\x2d\x34\x35\x2c\x38\x2e\x36\x2d\ -\x31\x33\x2e\x33\x2d\x31\x33\x2e\x37\x33\x2d\x32\x38\x2e\x35\x38\ -\x2d\x32\x33\x2e\x34\x35\x2d\x34\x36\x2e\x37\x35\x2d\x32\x37\x2e\ -\x33\x34\x2d\x33\x33\x2d\x37\x2d\x36\x31\x2e\x38\x38\x2c\x31\x2e\ -\x36\x34\x2d\x38\x36\x2e\x33\x2c\x32\x36\x2e\x35\x37\x2d\x31\x36\ -\x2e\x35\x34\x2c\x31\x36\x2e\x38\x38\x2d\x34\x32\x2c\x39\x2e\x39\ -\x32\x2d\x34\x37\x2e\x35\x37\x2d\x31\x33\x2e\x31\x37\x41\x37\x30\ -\x2e\x37\x38\x2c\x37\x30\x2e\x37\x38\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x31\x38\x35\x2e\x33\x36\x2c\x34\x30\x33\x2e\x36\x31\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\x33\x30\x30\x2c\x35\x33\x38\ -\x2e\x31\x38\x63\x2d\x32\x31\x2e\x38\x33\x2c\x30\x2d\x33\x38\x2e\ -\x38\x34\x2d\x31\x38\x2e\x33\x31\x2d\x33\x38\x2e\x38\x31\x2d\x34\ -\x31\x2e\x37\x39\x2c\x30\x2d\x32\x33\x2e\x32\x2c\x31\x37\x2e\x30\ -\x36\x2d\x34\x31\x2e\x35\x31\x2c\x33\x38\x2e\x35\x36\x2d\x34\x31\ -\x2e\x34\x37\x2c\x32\x32\x2e\x30\x39\x2c\x30\x2c\x33\x39\x2c\x31\ -\x38\x2c\x33\x39\x2c\x34\x31\x2e\x34\x39\x53\x33\x32\x31\x2e\x39\ -\x31\x2c\x35\x33\x38\x2e\x31\x37\x2c\x33\x30\x30\x2c\x35\x33\x38\ -\x2e\x31\x38\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x31\ -\x35\x36\x2e\x37\x36\x2c\x36\x31\x2e\x39\x31\x2c\x33\x30\x30\x2c\ -\x32\x36\x30\x2e\x36\x38\x2c\x34\x34\x33\x2e\x32\x35\x2c\x36\x31\ -\x2e\x39\x31\x68\x35\x37\x2e\x39\x33\x4c\x33\x32\x39\x2c\x33\x30\ -\x30\x2e\x38\x38\x6c\x31\x37\x32\x2e\x32\x32\x2c\x32\x33\x39\x48\ -\x34\x34\x33\x2e\x32\x35\x4c\x33\x30\x30\x2c\x33\x34\x31\x2e\x30\ -\x37\x2c\x31\x35\x36\x2e\x37\x36\x2c\x35\x33\x39\x2e\x38\x34\x48\ -\x39\x38\x2e\x38\x31\x4c\x32\x37\x31\x2c\x33\x30\x30\x2e\x38\x38\ -\x2c\x39\x38\x2e\x38\x33\x2c\x36\x31\x2e\x39\x31\x5a\x22\x2f\x3e\ -\x3c\x2f\x73\x76\x67\x3e\ \x00\x00\x0a\x76\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -22674,252 +22506,289 @@ \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\x31\ +\x00\x00\x05\xdd\ \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\x34\x30\x36\x2e\x38\x36\x2c\x32\x31\ -\x31\x2e\x32\x35\x63\x2d\x32\x31\x2e\x35\x37\x2d\x32\x2e\x31\x38\ -\x2d\x34\x31\x2e\x34\x35\x2e\x38\x33\x2d\x36\x30\x2e\x33\x31\x2c\ -\x39\x2e\x36\x37\x61\x38\x34\x2e\x31\x35\x2c\x38\x34\x2e\x31\x35\ -\x2c\x30\x2c\x30\x2c\x30\x2d\x33\x37\x2e\x39\x31\x2c\x33\x34\x2e\ -\x32\x32\x63\x2d\x32\x2e\x39\x31\x2c\x35\x2d\x32\x2e\x39\x34\x2c\ -\x38\x2e\x33\x36\x2c\x31\x2e\x36\x32\x2c\x31\x32\x2e\x36\x2c\x39\ -\x2e\x37\x34\x2c\x39\x2e\x30\x35\x2c\x39\x2e\x33\x39\x2c\x39\x2e\ -\x30\x39\x2c\x32\x31\x2e\x33\x31\x2c\x34\x2e\x34\x31\x2c\x33\x36\ -\x2e\x35\x35\x2d\x31\x34\x2e\x33\x36\x2c\x36\x38\x2e\x32\x39\x2d\ -\x34\x2e\x31\x38\x2c\x39\x38\x2e\x33\x32\x2c\x31\x38\x2e\x33\x32\ -\x2c\x31\x30\x2e\x38\x39\x2c\x38\x2e\x31\x36\x2c\x31\x30\x2e\x32\ -\x32\x2c\x31\x37\x2e\x35\x2c\x38\x2e\x36\x32\x2c\x32\x38\x2e\x35\ -\x31\x71\x2d\x37\x2e\x38\x35\x2c\x35\x33\x2e\x36\x34\x2d\x34\x39\ -\x2e\x31\x35\x2c\x38\x38\x2e\x35\x35\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\x38\x2d\x2e\x31\x33\ -\x2d\x31\x2e\x31\x31\x2d\x2e\x33\x32\x2c\x32\x2e\x31\x36\x2d\x32\ -\x30\x2e\x36\x39\x2e\x31\x31\x2d\x34\x31\x2d\x38\x2e\x35\x37\x2d\ -\x36\x30\x2e\x31\x39\x2d\x37\x2e\x36\x35\x2d\x31\x36\x2e\x39\x34\ -\x2d\x31\x39\x2e\x33\x32\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\x32\ -\x39\x2c\x31\x2e\x32\x32\x2d\x39\x2e\x31\x37\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\x36\x2c\x33\x33\x2e\ -\x34\x37\x2c\x32\x2e\x32\x32\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\x2c\x31\x32\x2e\x33\x34\x2d\x33\x33\x2e\x39\x32\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\x36\x33\x2c\x31\x37\x2e\ -\x36\x33\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x2d\x32\x2e\x38\ -\x34\x2c\x31\x31\x37\x2e\x39\x32\x2c\x31\x31\x37\x2e\x39\x32\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\x35\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\x35\x2d\x31\x32\x2e\x37\x31\x2d\x39\x2e\ -\x36\x2d\x39\x2e\x34\x32\x2d\x39\x2e\x33\x38\x2d\x39\x2e\x34\x34\ -\x2d\x32\x32\x2d\x34\x2e\x33\x37\x43\x32\x33\x32\x2e\x38\x2c\x33\ -\x34\x31\x2e\x38\x38\x2c\x32\x30\x32\x2e\x33\x34\x2c\x33\x33\x32\ -\x2e\x32\x2c\x31\x37\x33\x2c\x33\x31\x32\x63\x2d\x31\x33\x2e\x34\ -\x39\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\x35\x2d\x33\x34\x2e\x32\x2c\x32\x31\x2e\x39\x31\x2d\x36\x32\ -\x2e\x34\x36\x2c\x34\x38\x2e\x32\x34\x2d\x38\x35\x2c\x2e\x36\x39\ -\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\x33\x2c\x32\ -\x35\x2e\x31\x31\x2c\x31\x2e\x36\x35\x2c\x33\x37\x2e\x30\x37\x2c\ -\x35\x2e\x33\x35\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\x31\x2c\x32\x2e\x37\x2c\x37\x2e\x32\x36\x2c\x33\x2c\ -\x31\x31\x2e\x31\x38\x2d\x31\x2e\x31\x35\x2c\x39\x2e\x37\x38\x2d\ -\x31\x30\x2e\x32\x36\x2c\x39\x2e\x36\x39\x2d\x39\x2e\x39\x2c\x34\ -\x2e\x37\x39\x2d\x32\x33\x2e\x33\x35\x2d\x31\x32\x2e\x33\x31\x2d\ -\x33\x33\x2e\x37\x38\x2d\x33\x2e\x31\x36\x2d\x36\x33\x2e\x36\x35\ -\x2c\x31\x37\x2d\x39\x31\x2e\x36\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\x36\ -\x2e\x30\x35\x2c\x32\x30\x38\x2e\x35\x36\x2c\x34\x30\x36\x2e\x31\ -\x35\x2c\x32\x30\x39\x2e\x34\x2c\x34\x30\x36\x2e\x38\x36\x2c\x32\ -\x31\x31\x2e\x32\x35\x5a\x4d\x32\x39\x39\x2e\x37\x36\x2c\x33\x31\ -\x37\x2e\x34\x32\x63\x33\x2e\x38\x2e\x31\x31\x2c\x31\x37\x2e\x32\ -\x36\x2d\x31\x33\x2e\x31\x36\x2c\x31\x37\x2e\x34\x37\x2d\x31\x37\ -\x2e\x32\x31\x73\x2d\x31\x32\x2e\x36\x34\x2d\x31\x37\x2e\x32\x39\ -\x2d\x31\x37\x2d\x31\x37\x2e\x35\x33\x63\x2d\x33\x2e\x37\x31\x2d\ -\x2e\x31\x39\x2d\x31\x37\x2e\x34\x33\x2c\x31\x33\x2e\x31\x38\x2d\ -\x31\x37\x2e\x35\x34\x2c\x31\x37\x2e\x31\x31\x53\x32\x39\x35\x2e\ -\x36\x36\x2c\x33\x31\x37\x2e\x33\x2c\x32\x39\x39\x2e\x37\x36\x2c\ -\x33\x31\x37\x2e\x34\x32\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\ -\x00\x00\x09\xd1\ +\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\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\ \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\x34\x35\x36\x2e\x31\x37\x2c\x33\x31\ -\x37\x2e\x38\x35\x63\x2e\x34\x36\x2d\x31\x31\x2e\x38\x39\x2c\x35\ -\x2e\x31\x38\x2d\x31\x37\x2e\x34\x32\x2c\x31\x34\x2e\x38\x36\x2d\ -\x31\x36\x2e\x37\x39\x2c\x31\x30\x2e\x37\x35\x2e\x36\x39\x2c\x31\ -\x33\x2e\x32\x34\x2c\x37\x2e\x37\x37\x2c\x31\x33\x2e\x32\x32\x2c\ -\x31\x37\x2e\x32\x2d\x2e\x31\x31\x2c\x36\x36\x2c\x30\x2c\x31\x33\ -\x32\x2d\x2e\x31\x2c\x31\x39\x38\x2c\x30\x2c\x31\x31\x2d\x35\x2c\ -\x31\x36\x2e\x35\x32\x2d\x31\x34\x2e\x31\x38\x2c\x31\x36\x2e\x32\ -\x2d\x31\x30\x2d\x2e\x33\x34\x2d\x31\x34\x2e\x34\x34\x2d\x36\x2e\ -\x35\x31\x2d\x31\x33\x2e\x35\x39\x2d\x31\x35\x2e\x36\x2c\x31\x2e\ -\x30\x35\x2d\x31\x31\x2e\x32\x32\x2d\x33\x2e\x33\x38\x2d\x31\x33\ -\x2e\x36\x32\x2d\x31\x34\x2d\x31\x33\x2e\x35\x34\x2d\x36\x34\x2e\ -\x34\x39\x2e\x34\x37\x2d\x31\x32\x39\x2c\x2e\x33\x38\x2d\x31\x39\ -\x33\x2e\x34\x39\x2e\x31\x32\x2d\x34\x33\x2e\x36\x33\x2d\x2e\x31\ -\x37\x2d\x38\x31\x2e\x35\x2d\x31\x35\x2e\x36\x2d\x31\x31\x34\x2e\ -\x32\x33\x2d\x34\x34\x2e\x33\x32\x61\x31\x39\x32\x2e\x31\x38\x2c\ -\x31\x39\x32\x2e\x31\x38\x2c\x30\x2c\x30\x2c\x30\x2d\x32\x32\x2e\ -\x34\x34\x2d\x31\x36\x2e\x38\x39\x41\x32\x30\x34\x2e\x38\x32\x2c\ -\x32\x30\x34\x2e\x38\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x32\x2e\ -\x33\x32\x2c\x32\x33\x33\x2e\x38\x36\x43\x34\x34\x2e\x31\x31\x2c\ -\x31\x31\x35\x2e\x36\x39\x2c\x31\x36\x35\x2c\x34\x31\x2e\x37\x2c\ -\x32\x38\x30\x2e\x34\x34\x2c\x37\x35\x2e\x38\x37\x2c\x33\x36\x32\ -\x2e\x32\x35\x2c\x31\x30\x30\x2e\x30\x38\x2c\x34\x32\x30\x2e\x32\ -\x38\x2c\x31\x37\x31\x2e\x37\x37\x2c\x34\x32\x35\x2c\x32\x35\x37\ -\x63\x31\x2e\x33\x32\x2c\x32\x33\x2e\x36\x35\x2d\x32\x2e\x32\x2c\ -\x34\x37\x2e\x35\x36\x2d\x33\x2e\x35\x33\x2c\x37\x31\x2e\x38\x38\ -\x68\x33\x34\x2e\x30\x38\x43\x34\x35\x35\x2e\x38\x32\x2c\x33\x32\ -\x34\x2e\x34\x38\x2c\x34\x35\x36\x2c\x33\x32\x31\x2e\x31\x36\x2c\ -\x34\x35\x36\x2e\x31\x37\x2c\x33\x31\x37\x2e\x38\x35\x5a\x4d\x32\ -\x32\x33\x2e\x37\x38\x2c\x39\x36\x43\x31\x32\x37\x2c\x39\x35\x2e\ -\x35\x36\x2c\x34\x36\x2e\x36\x34\x2c\x31\x37\x36\x2c\x34\x37\x2e\ -\x35\x39\x2c\x32\x37\x32\x2e\x32\x32\x63\x2e\x39\x34\x2c\x39\x35\ -\x2e\x38\x34\x2c\x37\x39\x2e\x34\x31\x2c\x31\x37\x33\x2e\x39\x34\ -\x2c\x31\x37\x34\x2e\x37\x38\x2c\x31\x37\x33\x2e\x39\x34\x61\x31\ -\x37\x35\x2e\x32\x38\x2c\x31\x37\x35\x2e\x32\x38\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x31\x37\x35\x2e\x34\x34\x2d\x31\x37\x35\x2e\x34\x43\ -\x33\x39\x37\x2e\x38\x31\x2c\x31\x37\x35\x2e\x37\x32\x2c\x33\x31\ -\x38\x2e\x38\x36\x2c\x39\x36\x2e\x34\x34\x2c\x32\x32\x33\x2e\x37\ -\x38\x2c\x39\x36\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\ -\x33\x33\x32\x2e\x32\x35\x2c\x31\x37\x39\x2e\x39\x34\x63\x2d\x32\ -\x30\x2e\x34\x39\x2d\x32\x2e\x30\x37\x2d\x33\x39\x2e\x33\x35\x2e\ -\x37\x39\x2d\x35\x37\x2e\x32\x36\x2c\x39\x2e\x31\x38\x61\x38\x30\ -\x2c\x38\x30\x2c\x30\x2c\x30\x2c\x30\x2d\x33\x36\x2c\x33\x32\x2e\ -\x34\x38\x63\x2d\x32\x2e\x37\x36\x2c\x34\x2e\x37\x31\x2d\x32\x2e\ -\x37\x39\x2c\x37\x2e\x39\x33\x2c\x31\x2e\x35\x34\x2c\x31\x32\x2c\ -\x39\x2e\x32\x34\x2c\x38\x2e\x35\x39\x2c\x38\x2e\x39\x31\x2c\x38\ -\x2e\x36\x33\x2c\x32\x30\x2e\x32\x33\x2c\x34\x2e\x31\x38\x2c\x33\ -\x34\x2e\x36\x39\x2d\x31\x33\x2e\x36\x33\x2c\x36\x34\x2e\x38\x32\ -\x2d\x34\x2c\x39\x33\x2e\x33\x32\x2c\x31\x37\x2e\x34\x2c\x31\x30\ -\x2e\x33\x34\x2c\x37\x2e\x37\x34\x2c\x39\x2e\x37\x31\x2c\x31\x36\ -\x2e\x36\x31\x2c\x38\x2e\x31\x38\x2c\x32\x37\x2e\x30\x36\x71\x2d\ -\x37\x2e\x34\x34\x2c\x35\x30\x2e\x39\x33\x2d\x34\x36\x2e\x36\x35\ -\x2c\x38\x34\x2e\x30\x35\x61\x37\x2e\x36\x31\x2c\x37\x2e\x36\x31\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x38\x36\x2c\x31\x63\x2d\x2e\ -\x31\x37\x2e\x30\x38\x2d\x2e\x34\x36\x2d\x2e\x31\x32\x2d\x31\x2e\ -\x30\x36\x2d\x2e\x33\x31\x2c\x32\x2e\x30\x36\x2d\x31\x39\x2e\x36\ -\x34\x2e\x31\x31\x2d\x33\x38\x2e\x38\x38\x2d\x38\x2e\x31\x33\x2d\ -\x35\x37\x2e\x31\x33\x2d\x37\x2e\x32\x36\x2d\x31\x36\x2e\x30\x38\ -\x2d\x31\x38\x2e\x33\x34\x2d\x32\x38\x2e\x38\x32\x2d\x33\x33\x2e\ -\x38\x2d\x33\x37\x2e\x36\x35\x2d\x33\x2e\x34\x37\x2d\x32\x2d\x35\ -\x2e\x34\x39\x2d\x31\x2e\x35\x33\x2d\x38\x2e\x38\x32\x2c\x31\x2e\ -\x31\x36\x2d\x38\x2e\x37\x2c\x37\x2d\x39\x2e\x36\x33\x2c\x31\x33\ -\x2e\x31\x31\x2d\x35\x2e\x31\x33\x2c\x32\x34\x2e\x33\x32\x2c\x31\ -\x32\x2e\x37\x39\x2c\x33\x31\x2e\x37\x37\x2c\x32\x2e\x31\x32\x2c\ -\x36\x30\x2e\x32\x32\x2d\x31\x37\x2e\x30\x38\x2c\x38\x36\x2e\x35\ -\x31\x2d\x37\x2e\x33\x31\x2c\x31\x30\x2d\x31\x35\x2e\x34\x33\x2c\ -\x31\x34\x2e\x30\x37\x2d\x32\x38\x2e\x35\x37\x2c\x31\x31\x2e\x37\ -\x31\x2d\x33\x32\x2e\x31\x39\x2d\x35\x2e\x37\x38\x2d\x35\x39\x2d\ -\x32\x30\x2e\x32\x33\x2d\x38\x30\x2e\x36\x39\x2d\x34\x34\x2e\x35\ -\x31\x61\x31\x37\x2e\x32\x2c\x31\x37\x2e\x32\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x31\x2e\x34\x33\x2d\x32\x2e\x37\x2c\x31\x31\x31\x2e\x38\ -\x2c\x31\x31\x31\x2e\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x31\x2e\ -\x34\x34\x2d\x33\x2e\x34\x36\x63\x32\x32\x2e\x31\x32\x2d\x36\x2e\ -\x31\x34\x2c\x33\x39\x2e\x38\x33\x2d\x31\x38\x2e\x32\x33\x2c\x35\ -\x31\x2e\x38\x38\x2d\x33\x38\x2e\x32\x35\x2c\x33\x2d\x35\x2c\x32\ -\x2e\x35\x34\x2d\x38\x2e\x31\x33\x2d\x31\x2e\x34\x37\x2d\x31\x32\ -\x2e\x30\x36\x2d\x39\x2e\x31\x31\x2d\x38\x2e\x39\x34\x2d\x38\x2e\ -\x39\x2d\x39\x2d\x32\x30\x2e\x39\x32\x2d\x34\x2e\x31\x35\x2d\x33\ -\x33\x2c\x31\x33\x2e\x31\x38\x2d\x36\x31\x2e\x38\x38\x2c\x34\x2d\ -\x38\x39\x2e\x37\x35\x2d\x31\x35\x2e\x31\x34\x2d\x31\x32\x2e\x38\ -\x31\x2d\x38\x2e\x37\x39\x2d\x31\x32\x2e\x39\x31\x2d\x31\x39\x2e\ -\x34\x34\x2d\x31\x30\x2e\x36\x34\x2d\x33\x32\x2e\x35\x31\x2c\x35\ -\x2e\x36\x34\x2d\x33\x32\x2e\x34\x36\x2c\x32\x30\x2e\x38\x2d\x35\ -\x39\x2e\x32\x38\x2c\x34\x35\x2e\x37\x39\x2d\x38\x30\x2e\x36\x34\ -\x2e\x36\x36\x2d\x2e\x35\x36\x2c\x31\x2e\x34\x32\x2d\x31\x2c\x32\ -\x2e\x38\x37\x2d\x32\x2c\x2e\x37\x37\x2c\x31\x32\x2e\x33\x36\x2d\ -\x2e\x37\x2c\x32\x33\x2e\x38\x34\x2c\x31\x2e\x35\x36\x2c\x33\x35\ -\x2e\x31\x39\x2c\x35\x2e\x30\x38\x2c\x32\x35\x2e\x34\x36\x2c\x31\ -\x37\x2e\x31\x32\x2c\x34\x36\x2e\x30\x39\x2c\x33\x39\x2e\x37\x31\ -\x2c\x35\x39\x2e\x39\x35\x2c\x34\x2e\x31\x38\x2c\x32\x2e\x35\x36\ -\x2c\x36\x2e\x38\x38\x2c\x32\x2e\x38\x31\x2c\x31\x30\x2e\x36\x31\ -\x2d\x31\x2e\x30\x39\x2c\x39\x2e\x32\x38\x2d\x39\x2e\x37\x34\x2c\ -\x39\x2e\x32\x2d\x39\x2e\x34\x2c\x34\x2e\x35\x35\x2d\x32\x32\x2e\ -\x31\x36\x2d\x31\x31\x2e\x36\x39\x2d\x33\x32\x2e\x30\x37\x2d\x33\ -\x2d\x36\x30\x2e\x34\x32\x2c\x31\x36\x2e\x31\x33\x2d\x38\x37\x2c\ -\x37\x2e\x36\x36\x2d\x31\x30\x2e\x36\x33\x2c\x31\x36\x2d\x31\x35\ -\x2e\x35\x35\x2c\x33\x30\x2e\x32\x34\x2d\x31\x32\x2e\x38\x32\x2c\ -\x33\x31\x2e\x38\x32\x2c\x36\x2e\x30\x39\x2c\x35\x38\x2e\x33\x38\ -\x2c\x32\x30\x2e\x34\x34\x2c\x38\x30\x2c\x34\x34\x2e\x33\x33\x43\ -\x33\x33\x31\x2e\x34\x38\x2c\x31\x37\x37\x2e\x33\x39\x2c\x33\x33\ -\x31\x2e\x35\x37\x2c\x31\x37\x38\x2e\x31\x38\x2c\x33\x33\x32\x2e\ -\x32\x35\x2c\x31\x37\x39\x2e\x39\x34\x5a\x4d\x32\x33\x30\x2e\x35\ -\x39\x2c\x32\x38\x30\x2e\x37\x32\x63\x33\x2e\x35\x39\x2e\x31\x2c\ -\x31\x36\x2e\x33\x37\x2d\x31\x32\x2e\x34\x39\x2c\x31\x36\x2e\x35\ -\x37\x2d\x31\x36\x2e\x33\x34\x53\x32\x33\x35\x2e\x31\x36\x2c\x32\ -\x34\x38\x2c\x32\x33\x31\x2c\x32\x34\x37\x2e\x37\x35\x63\x2d\x33\ -\x2e\x35\x32\x2d\x2e\x31\x39\x2d\x31\x36\x2e\x35\x34\x2c\x31\x32\ -\x2e\x35\x2d\x31\x36\x2e\x36\x35\x2c\x31\x36\x2e\x32\x33\x53\x32\ -\x32\x36\x2e\x36\x39\x2c\x32\x38\x30\x2e\x36\x31\x2c\x32\x33\x30\ -\x2e\x35\x39\x2c\x32\x38\x30\x2e\x37\x32\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\x35\x38\x31\x2c\x33\x33\x32\x2e\x38\x32\ -\x63\x2d\x33\x2e\x36\x31\x2c\x32\x2e\x31\x33\x2d\x37\x2e\x32\x33\ -\x2c\x34\x2e\x32\x35\x2d\x31\x30\x2e\x38\x33\x2c\x36\x2e\x33\x39\ -\x6c\x2d\x34\x35\x2e\x34\x31\x2c\x32\x37\x63\x2d\x2e\x31\x36\x2e\ -\x31\x2d\x2e\x33\x33\x2e\x31\x39\x2d\x2e\x36\x36\x2e\x33\x37\x2c\ -\x30\x2d\x32\x2e\x33\x31\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\x34\ -\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\x34\x68\x2d\x32\x33\x56\x33\ -\x31\x39\x2e\x34\x68\x32\x33\x73\x30\x2d\x32\x30\x2e\x33\x36\x2c\ -\x30\x2d\x32\x30\x2e\x35\x31\x63\x2e\x30\x37\x2c\x30\x2c\x31\x30\ -\x2e\x31\x38\x2c\x35\x2e\x39\x31\x2c\x31\x34\x2e\x38\x34\x2c\x38\ -\x2e\x36\x38\x4c\x35\x38\x31\x2c\x33\x33\x32\x2e\x36\x34\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\x35\x38\x31\x2c\x34\x39\ -\x34\x63\x2d\x33\x2e\x36\x31\x2c\x32\x2e\x31\x33\x2d\x37\x2e\x32\ -\x33\x2c\x34\x2e\x32\x35\x2d\x31\x30\x2e\x38\x33\x2c\x36\x2e\x33\ -\x39\x6c\x2d\x34\x35\x2e\x34\x31\x2c\x32\x37\x63\x2d\x2e\x31\x36\ -\x2e\x31\x2d\x2e\x33\x33\x2e\x31\x39\x2d\x2e\x36\x36\x2e\x33\x37\ -\x2c\x30\x2d\x32\x2e\x33\x31\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\ -\x34\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\x34\x68\x2d\x32\x33\x56\ -\x34\x38\x30\x2e\x36\x31\x68\x32\x33\x73\x30\x2d\x32\x30\x2e\x33\ -\x36\x2c\x30\x2d\x32\x30\x2e\x35\x31\x2c\x31\x30\x2e\x31\x38\x2c\ -\x35\x2e\x39\x31\x2c\x31\x34\x2e\x38\x34\x2c\x38\x2e\x36\x39\x4c\ -\x35\x38\x31\x2c\x34\x39\x33\x2e\x38\x35\x5a\x22\x2f\x3e\x3c\x70\ +\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\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\x35\x38\x31\x2c\x34\x31\x33\x2e\x34\x33\ -\x63\x2d\x33\x2e\x36\x31\x2c\x32\x2e\x31\x33\x2d\x37\x2e\x32\x33\ -\x2c\x34\x2e\x32\x34\x2d\x31\x30\x2e\x38\x33\x2c\x36\x2e\x33\x38\ -\x6c\x2d\x34\x35\x2e\x34\x31\x2c\x32\x37\x63\x2d\x2e\x31\x36\x2e\ -\x31\x2d\x2e\x33\x33\x2e\x31\x38\x2d\x2e\x36\x36\x2e\x33\x37\x2c\ -\x30\x2d\x32\x2e\x33\x32\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\x35\ -\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\x35\x68\x2d\x32\x33\x56\x34\ -\x30\x30\x68\x32\x33\x73\x30\x2d\x32\x30\x2e\x33\x36\x2c\x30\x2d\ -\x32\x30\x2e\x35\x31\x2c\x31\x30\x2e\x31\x38\x2c\x35\x2e\x39\x31\ -\x2c\x31\x34\x2e\x38\x34\x2c\x38\x2e\x36\x38\x4c\x35\x38\x31\x2c\ -\x34\x31\x33\x2e\x32\x34\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\ +\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\ +\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\ \x00\x00\x07\xc6\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -23536,6 +23405,59 @@ \x20\x34\x38\x31\x2e\x32\x32\x20\x35\x31\x30\x2e\x37\x37\x20\x32\ \x38\x36\x2e\x30\x36\x20\x38\x39\x2e\x32\x33\x20\x31\x31\x34\x2e\ \x34\x31\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x03\x28\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\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\x34\x38\x34\x2e\x34\x37\ +\x2c\x34\x34\x37\x2e\x31\x6c\x2d\x33\x36\x36\x2e\x38\x31\x2e\x30\ +\x34\x63\x2d\x35\x2e\x37\x34\x2c\x30\x2d\x31\x32\x2e\x39\x38\x2d\ +\x35\x2e\x31\x32\x2d\x31\x35\x2e\x31\x39\x2d\x38\x2e\x37\x39\x2d\ +\x32\x2e\x38\x33\x2d\x34\x2e\x36\x38\x2d\x31\x2e\x39\x39\x2d\x31\ +\x34\x2e\x39\x39\x2c\x31\x2e\x37\x38\x2d\x31\x39\x2e\x31\x34\x6c\ +\x34\x32\x2e\x33\x39\x2d\x34\x36\x2e\x35\x38\x63\x38\x2e\x30\x33\ +\x2d\x38\x2e\x38\x32\x2c\x31\x34\x2e\x38\x36\x2d\x31\x39\x2e\x37\ +\x39\x2c\x31\x34\x2e\x39\x33\x2d\x33\x32\x2e\x33\x31\x6c\x2e\x35\ +\x2d\x38\x33\x2e\x36\x37\x63\x2e\x33\x38\x2d\x36\x33\x2e\x34\x39\ +\x2c\x35\x30\x2e\x32\x35\x2d\x31\x31\x35\x2e\x31\x2c\x31\x31\x32\ +\x2e\x31\x2d\x31\x32\x37\x2e\x31\x31\x6c\x2e\x39\x35\x2d\x33\x31\ +\x2e\x30\x34\x63\x2e\x34\x32\x2d\x31\x33\x2e\x37\x36\x2c\x31\x31\ +\x2e\x35\x39\x2d\x32\x33\x2e\x31\x38\x2c\x32\x33\x2e\x34\x31\x2d\ +\x32\x33\x2e\x38\x33\x2c\x31\x33\x2e\x31\x38\x2d\x2e\x37\x33\x2c\ +\x32\x36\x2e\x30\x31\x2c\x39\x2e\x32\x32\x2c\x32\x36\x2e\x33\x35\ +\x2c\x32\x33\x2e\x38\x33\x6c\x2e\x37\x32\x2c\x33\x30\x2e\x39\x33\ +\x63\x36\x34\x2e\x35\x33\x2c\x31\x33\x2e\x33\x32\x2c\x31\x31\x32\ +\x2e\x32\x33\x2c\x36\x38\x2e\x34\x31\x2c\x31\x31\x32\x2e\x35\x37\ +\x2c\x31\x33\x35\x2e\x30\x31\x6c\x2e\x33\x39\x2c\x37\x35\x2e\x39\ +\x34\x63\x2e\x30\x35\x2c\x31\x30\x2e\x35\x38\x2c\x34\x2e\x37\x33\ +\x2c\x32\x31\x2e\x31\x31\x2c\x31\x31\x2e\x37\x37\x2c\x32\x38\x2e\ +\x38\x38\x6c\x34\x36\x2e\x33\x33\x2c\x35\x31\x2e\x31\x33\x63\x32\ +\x2e\x36\x36\x2c\x32\x2e\x39\x33\x2c\x33\x2e\x32\x32\x2c\x31\x32\ +\x2e\x38\x36\x2c\x31\x2e\x37\x39\x2c\x31\x36\x2e\x35\x34\x2d\x31\ +\x2e\x34\x33\x2c\x33\x2e\x36\x39\x2d\x37\x2e\x35\x33\x2c\x31\x30\ +\x2e\x31\x37\x2d\x31\x33\x2e\x39\x38\x2c\x31\x30\x2e\x31\x37\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\x36\ +\x32\x2e\x36\x35\x2c\x34\x36\x32\x2e\x39\x33\x63\x32\x2e\x35\x35\ +\x2c\x33\x33\x2e\x38\x35\x2d\x32\x37\x2e\x38\x34\x2c\x36\x31\x2e\ +\x31\x31\x2d\x35\x39\x2e\x36\x32\x2c\x36\x32\x2e\x33\x38\x2d\x33\ +\x35\x2e\x37\x36\x2c\x31\x2e\x34\x34\x2d\x36\x36\x2e\x34\x34\x2d\ +\x32\x35\x2e\x32\x34\x2d\x36\x36\x2e\x33\x35\x2d\x36\x32\x2e\x33\ +\x31\x6c\x31\x32\x35\x2e\x39\x37\x2d\x2e\x30\x37\x5a\x22\x2f\x3e\ +\x0a\x3c\x2f\x73\x76\x67\x3e\ \x00\x00\x04\x25\ \x3c\ \x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ @@ -24331,7 +24253,7 @@ \x35\x33\x32\x2e\x39\x36\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\ \x34\x31\x38\x2e\x33\x36\x22\x20\x72\x78\x3d\x22\x32\x39\x2e\x31\ \x37\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x04\x1d\ +\x00\x00\x02\xee\ \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\ @@ -24348,57 +24270,38 @@ \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\x32\x36\x32\x2e\x30\x35\ -\x2c\x33\x35\x39\x2e\x35\x31\x63\x32\x34\x2e\x39\x32\x2d\x2e\x32\ -\x32\x2c\x34\x39\x2e\x38\x34\x2d\x2e\x33\x39\x2c\x37\x34\x2e\x37\ -\x35\x2e\x30\x38\x2c\x36\x2e\x37\x34\x2e\x31\x33\x2c\x37\x2e\x36\ -\x37\x2d\x32\x2e\x31\x32\x2c\x37\x2e\x36\x32\x2d\x38\x2e\x31\x2d\ -\x2e\x32\x37\x2d\x33\x39\x2e\x31\x35\x2d\x2e\x31\x34\x2d\x37\x38\ -\x2e\x33\x2d\x2e\x31\x34\x2d\x31\x31\x37\x2e\x34\x35\x73\x2d\x2e\ -\x31\x35\x2d\x37\x38\x2e\x33\x2e\x31\x35\x2d\x31\x31\x37\x2e\x34\ -\x35\x63\x2e\x30\x35\x2d\x36\x2e\x31\x33\x2d\x31\x2e\x37\x31\x2d\ -\x37\x2e\x35\x33\x2d\x37\x2e\x36\x2d\x37\x2e\x34\x36\x2d\x32\x34\ -\x2e\x39\x32\x2e\x33\x33\x2d\x34\x39\x2e\x38\x34\x2e\x32\x36\x2d\ -\x37\x34\x2e\x37\x36\x2e\x30\x34\x2d\x34\x2e\x38\x32\x2d\x2e\x30\ -\x34\x2d\x36\x2e\x34\x37\x2c\x31\x2e\x30\x35\x2d\x36\x2e\x34\x36\ -\x2c\x36\x2e\x32\x34\x2e\x31\x37\x2c\x37\x39\x2e\x33\x32\x2e\x31\ -\x37\x2c\x31\x35\x38\x2e\x36\x34\x2c\x30\x2c\x32\x33\x37\x2e\x39\ -\x36\x2d\x2e\x30\x31\x2c\x35\x2e\x32\x32\x2c\x31\x2e\x36\x36\x2c\ -\x36\x2e\x31\x39\x2c\x36\x2e\x34\x33\x2c\x36\x2e\x31\x34\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\x32\x35\x2e\ -\x37\x36\x2c\x33\x30\x35\x2e\x36\x32\x63\x30\x2c\x31\x35\x31\x2e\ -\x37\x34\x2c\x31\x32\x31\x2e\x37\x31\x2c\x32\x37\x34\x2e\x32\x39\ -\x2c\x32\x37\x32\x2e\x35\x37\x2c\x32\x37\x34\x2e\x34\x36\x2c\x31\ -\x35\x33\x2e\x30\x33\x2e\x31\x36\x2c\x32\x37\x35\x2e\x38\x36\x2d\ -\x31\x32\x31\x2e\x39\x2c\x32\x37\x35\x2e\x39\x36\x2d\x32\x37\x34\ -\x2e\x32\x34\x2e\x31\x2d\x31\x35\x30\x2e\x32\x36\x2d\x31\x32\x32\ -\x2e\x34\x2d\x32\x37\x32\x2e\x35\x36\x2d\x32\x37\x31\x2e\x30\x32\ -\x2d\x32\x37\x34\x2e\x36\x33\x43\x31\x35\x30\x2e\x33\x35\x2c\x32\ -\x39\x2e\x30\x38\x2c\x32\x32\x2e\x39\x35\x2c\x31\x35\x37\x2e\x32\ -\x2c\x32\x35\x2e\x37\x36\x2c\x33\x30\x35\x2e\x36\x32\x5a\x4d\x33\ -\x30\x32\x2e\x36\x32\x2c\x38\x35\x2e\x38\x35\x63\x31\x31\x39\x2e\ -\x30\x33\x2c\x31\x2e\x36\x36\x2c\x32\x31\x37\x2e\x31\x33\x2c\x39\ -\x39\x2e\x36\x31\x2c\x32\x31\x37\x2e\x30\x36\x2c\x32\x31\x39\x2e\ -\x39\x35\x2d\x2e\x30\x38\x2c\x31\x32\x32\x2d\x39\x38\x2e\x34\x35\ -\x2c\x32\x31\x39\x2e\x37\x36\x2d\x32\x32\x31\x2e\x30\x31\x2c\x32\ -\x31\x39\x2e\x36\x33\x2d\x31\x32\x30\x2e\x38\x32\x2d\x2e\x31\x33\ -\x2d\x32\x31\x38\x2e\x33\x2d\x39\x38\x2e\x32\x38\x2d\x32\x31\x38\ -\x2e\x33\x2d\x32\x31\x39\x2e\x38\x31\x2d\x32\x2e\x32\x35\x2d\x31\ -\x31\x38\x2e\x38\x37\x2c\x39\x39\x2e\x37\x38\x2d\x32\x32\x31\x2e\ -\x34\x37\x2c\x32\x32\x32\x2e\x32\x35\x2d\x32\x31\x39\x2e\x37\x37\ -\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\x32\ -\x39\x38\x2e\x38\x37\x2c\x34\x39\x31\x2e\x30\x33\x63\x32\x35\x2e\ -\x33\x31\x2e\x33\x35\x2c\x34\x37\x2e\x33\x33\x2d\x31\x35\x2e\x31\ -\x32\x2c\x35\x33\x2e\x31\x33\x2d\x33\x37\x2e\x33\x32\x2c\x36\x2e\ -\x39\x38\x2d\x32\x36\x2e\x37\x2d\x38\x2e\x34\x36\x2d\x35\x33\x2e\ -\x32\x2d\x33\x35\x2e\x35\x35\x2d\x36\x31\x2e\x30\x31\x2d\x32\x39\ -\x2e\x32\x33\x2d\x38\x2e\x34\x32\x2d\x36\x30\x2e\x32\x35\x2c\x36\ -\x2e\x38\x31\x2d\x36\x38\x2e\x35\x2c\x33\x33\x2e\x36\x34\x2d\x39\ -\x2e\x39\x38\x2c\x33\x32\x2e\x34\x36\x2c\x31\x34\x2e\x39\x39\x2c\ -\x36\x34\x2e\x31\x39\x2c\x35\x30\x2e\x39\x31\x2c\x36\x34\x2e\x36\ -\x38\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ +\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x35\x36\x31\x2e\x36\x35\ +\x2c\x31\x39\x31\x2e\x35\x37\x4c\x34\x30\x38\x2e\x34\x2c\x33\x38\ +\x2e\x33\x32\x68\x2d\x32\x31\x36\x2e\x38\x32\x53\x33\x38\x2e\x33\ +\x2c\x31\x39\x31\x2e\x36\x2c\x33\x38\x2e\x33\x2c\x31\x39\x31\x2e\ +\x36\x76\x32\x31\x36\x2e\x38\x6c\x31\x35\x33\x2e\x32\x38\x2c\x31\ +\x35\x33\x2e\x32\x38\x68\x32\x31\x36\x2e\x37\x39\x73\x31\x34\x34\ +\x2e\x36\x32\x2d\x31\x34\x34\x2e\x35\x37\x2c\x31\x34\x34\x2e\x36\ +\x32\x2d\x31\x34\x34\x2e\x35\x37\x63\x35\x2e\x31\x34\x2d\x35\x2e\ +\x30\x38\x2c\x38\x2e\x31\x33\x2d\x31\x31\x2e\x36\x31\x2c\x38\x2e\ +\x36\x33\x2d\x31\x38\x2e\x38\x39\x6c\x2e\x30\x37\x2d\x2e\x39\x38\ +\x2d\x2e\x30\x34\x2d\x32\x30\x35\x2e\x36\x38\x5a\x4d\x32\x31\x34\ +\x2e\x38\x34\x2c\x35\x30\x35\x2e\x35\x35\x6c\x2d\x31\x32\x30\x2e\ +\x34\x2d\x31\x32\x30\x2e\x34\x76\x2d\x31\x37\x30\x2e\x33\x73\x31\ +\x32\x30\x2e\x34\x2d\x31\x32\x30\x2e\x34\x2c\x31\x32\x30\x2e\x34\ +\x2d\x31\x32\x30\x2e\x34\x68\x31\x37\x30\x2e\x33\x31\x73\x31\x32\ +\x30\x2e\x33\x37\x2c\x31\x32\x30\x2e\x33\x37\x2c\x31\x32\x30\x2e\ +\x33\x37\x2c\x31\x32\x30\x2e\x33\x37\x6c\x2e\x30\x34\x2c\x31\x37\ +\x30\x2e\x33\x33\x2d\x31\x32\x30\x2e\x34\x33\x2c\x31\x32\x30\x2e\ +\x33\x38\x68\x2d\x31\x37\x30\x2e\x32\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\x32\x35\x35\x2e\x31\x32\x2c\ +\x34\x32\x38\x2e\x38\x63\x30\x2d\x32\x33\x2e\x38\x34\x2c\x31\x38\ +\x2e\x37\x2d\x34\x31\x2e\x36\x31\x2c\x34\x34\x2e\x38\x38\x2d\x34\ +\x31\x2e\x36\x31\x73\x34\x34\x2e\x38\x38\x2c\x31\x37\x2e\x37\x37\ +\x2c\x34\x34\x2e\x38\x38\x2c\x34\x31\x2e\x36\x31\x2d\x31\x38\x2e\ +\x37\x2c\x34\x32\x2e\x35\x35\x2d\x34\x34\x2e\x38\x38\x2c\x34\x32\ +\x2e\x35\x35\x2d\x34\x34\x2e\x38\x38\x2d\x31\x39\x2e\x31\x37\x2d\ +\x34\x34\x2e\x38\x38\x2d\x34\x32\x2e\x35\x35\x5a\x4d\x32\x34\x38\ +\x2e\x39\x31\x2c\x31\x34\x30\x2e\x33\x32\x68\x31\x30\x32\x2e\x31\ +\x38\x6c\x2d\x32\x32\x2e\x31\x2c\x32\x31\x33\x2e\x36\x37\x68\x2d\ +\x35\x37\x2e\x39\x38\x6c\x2d\x32\x32\x2e\x31\x2d\x32\x31\x33\x2e\ +\x36\x37\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ \x00\x00\x05\xf3\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -24751,6 +24654,101 @@ \x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x33\x36\x39\x2e\ \x33\x36\x20\x37\x30\x35\x2e\x33\x38\x29\x20\x72\x6f\x74\x61\x74\ \x65\x28\x2d\x39\x30\x29\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x05\xc4\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\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\x72\x65\x63\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ +\x6c\x73\x2d\x31\x22\x20\x78\x3d\x22\x34\x35\x36\x2e\x37\x22\x20\ +\x79\x3d\x22\x32\x35\x35\x2e\x39\x31\x22\x20\x77\x69\x64\x74\x68\ +\x3d\x22\x32\x31\x2e\x30\x31\x22\x20\x68\x65\x69\x67\x68\x74\x3d\ +\x22\x33\x30\x2e\x37\x35\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\ +\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x2d\x2e\x38\ +\x20\x31\x2e\x33\x39\x29\x20\x72\x6f\x74\x61\x74\x65\x28\x2d\x2e\ +\x31\x37\x29\x22\x2f\x3e\x0a\x20\x20\x3c\x72\x65\x63\x74\x20\x63\ +\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x78\x3d\x22\ +\x34\x35\x36\x2e\x37\x34\x22\x20\x79\x3d\x22\x33\x31\x32\x2e\x39\ +\x39\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x31\x2e\x30\x36\x22\ +\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x33\x30\x2e\x37\x33\x22\x20\ +\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ +\x6c\x61\x74\x65\x28\x2d\x2e\x36\x39\x20\x2e\x39\x38\x29\x20\x72\ +\x6f\x74\x61\x74\x65\x28\x2d\x2e\x31\x32\x29\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\x34\x38\x2e\x31\x32\x2c\ +\x33\x38\x35\x2e\x38\x37\x6c\x2d\x2e\x31\x39\x2d\x31\x37\x32\x2e\ +\x33\x2d\x31\x31\x34\x2e\x38\x31\x2e\x31\x63\x2e\x35\x36\x2d\x32\ +\x33\x2e\x39\x31\x2d\x31\x36\x2e\x33\x2d\x34\x32\x2e\x30\x33\x2d\ +\x34\x30\x2e\x30\x34\x2d\x34\x31\x2e\x38\x34\x6c\x2d\x33\x30\x34\ +\x2e\x34\x31\x2e\x34\x37\x63\x2d\x32\x33\x2e\x30\x31\x2e\x30\x33\ +\x2d\x33\x36\x2e\x38\x35\x2c\x32\x30\x2e\x34\x32\x2d\x33\x36\x2e\ +\x37\x39\x2c\x34\x31\x2e\x32\x32\x6c\x2e\x34\x38\x2c\x31\x37\x37\ +\x2e\x39\x37\x68\x30\x63\x2e\x30\x36\x2c\x32\x31\x2e\x38\x35\x2c\ +\x31\x39\x2e\x30\x33\x2c\x33\x36\x2e\x37\x31\x2c\x33\x39\x2e\x31\ +\x39\x2c\x33\x36\x2e\x36\x39\x6c\x33\x30\x33\x2e\x31\x36\x2d\x2e\ +\x33\x31\x63\x32\x33\x2e\x31\x2d\x2e\x35\x33\x2c\x33\x39\x2e\x32\ +\x32\x2d\x31\x38\x2e\x35\x37\x2c\x33\x38\x2e\x35\x38\x2d\x34\x31\ +\x2e\x38\x36\x6c\x31\x31\x34\x2e\x38\x34\x2d\x2e\x31\x34\x5a\x4d\ +\x31\x30\x39\x2e\x30\x38\x2c\x33\x34\x31\x2e\x33\x31\x63\x2e\x30\ +\x31\x2c\x38\x2e\x34\x35\x2d\x34\x2e\x39\x37\x2c\x31\x34\x2e\x31\ +\x38\x2d\x31\x32\x2e\x30\x38\x2c\x31\x34\x2e\x36\x31\x2d\x36\x2e\ +\x39\x35\x2e\x34\x32\x2d\x31\x33\x2e\x38\x32\x2d\x34\x2e\x38\x39\ +\x2d\x31\x33\x2e\x38\x34\x2d\x31\x32\x2e\x36\x32\x6c\x2d\x2e\x31\ +\x34\x2d\x38\x35\x2e\x30\x31\x63\x2d\x2e\x30\x31\x2d\x37\x2e\x37\ +\x38\x2c\x35\x2e\x31\x37\x2d\x31\x33\x2e\x35\x36\x2c\x31\x32\x2e\ +\x35\x35\x2d\x31\x33\x2e\x39\x33\x2c\x37\x2e\x35\x31\x2d\x2e\x33\ +\x38\x2c\x31\x33\x2e\x34\x33\x2c\x35\x2e\x36\x39\x2c\x31\x33\x2e\ +\x34\x33\x2c\x31\x33\x2e\x38\x33\x6c\x2e\x30\x38\x2c\x38\x33\x2e\ +\x31\x31\x68\x2d\x2e\x30\x31\x5a\x4d\x33\x36\x31\x2e\x38\x34\x2c\ +\x33\x31\x38\x2e\x34\x34\x6c\x2d\x2e\x33\x34\x2d\x31\x33\x2e\x32\ +\x2d\x31\x31\x32\x2e\x34\x36\x2e\x35\x2c\x32\x34\x2e\x36\x36\x2c\ +\x32\x35\x2e\x35\x33\x2c\x34\x32\x2e\x32\x2d\x2e\x31\x39\x2e\x33\ +\x33\x2d\x31\x31\x2e\x35\x35\x2c\x33\x32\x2e\x39\x2d\x2e\x32\x2e\ +\x32\x32\x2c\x33\x33\x2e\x30\x36\x2d\x33\x32\x2e\x39\x33\x2e\x32\ +\x38\x2d\x2e\x33\x38\x2d\x31\x31\x2e\x35\x33\x2d\x34\x33\x2e\x30\ +\x34\x2e\x32\x32\x63\x2d\x32\x2e\x30\x31\x2e\x30\x31\x2d\x35\x2e\ +\x34\x35\x2d\x31\x2e\x38\x37\x2d\x36\x2e\x39\x37\x2d\x33\x2e\x34\ +\x32\x6c\x2d\x33\x31\x2e\x33\x39\x2d\x33\x32\x2e\x31\x33\x2d\x34\ +\x34\x2e\x35\x39\x2e\x31\x37\x63\x2d\x33\x2e\x30\x39\x2c\x31\x33\ +\x2e\x31\x2d\x31\x34\x2e\x36\x31\x2c\x32\x30\x2e\x38\x34\x2d\x32\ +\x36\x2e\x38\x33\x2c\x31\x39\x2e\x35\x37\x2d\x31\x32\x2e\x35\x32\ +\x2d\x31\x2e\x33\x2d\x32\x31\x2e\x38\x2d\x31\x31\x2e\x38\x33\x2d\ +\x32\x32\x2e\x30\x36\x2d\x32\x33\x2e\x39\x31\x2d\x2e\x32\x37\x2d\ +\x31\x32\x2e\x37\x38\x2c\x39\x2e\x31\x35\x2d\x32\x33\x2e\x34\x33\ +\x2c\x32\x31\x2e\x33\x2d\x32\x34\x2e\x39\x39\x2c\x31\x32\x2e\x36\ +\x36\x2d\x31\x2e\x36\x32\x2c\x32\x34\x2e\x31\x34\x2c\x36\x2c\x32\ +\x37\x2e\x35\x32\x2c\x31\x39\x2e\x31\x38\x6c\x32\x31\x2e\x31\x34\ +\x2e\x30\x38\x2c\x33\x31\x2e\x37\x36\x2d\x33\x33\x2e\x38\x37\x63\ +\x31\x2e\x35\x35\x2d\x31\x2e\x36\x35\x2c\x35\x2e\x30\x32\x2d\x32\ +\x2e\x38\x32\x2c\x37\x2e\x31\x39\x2d\x32\x2e\x38\x32\x6c\x32\x39\ +\x2e\x31\x32\x2d\x2e\x30\x35\x63\x33\x2e\x32\x39\x2d\x37\x2e\x38\ +\x39\x2c\x31\x30\x2d\x31\x32\x2e\x37\x2c\x31\x38\x2e\x33\x2d\x31\ +\x31\x2e\x37\x2c\x37\x2e\x36\x35\x2e\x39\x32\x2c\x31\x34\x2e\x30\ +\x39\x2c\x37\x2e\x32\x36\x2c\x31\x34\x2e\x36\x33\x2c\x31\x35\x2e\ +\x34\x2e\x35\x33\x2c\x37\x2e\x39\x31\x2d\x34\x2e\x36\x31\x2c\x31\ +\x35\x2e\x34\x35\x2d\x31\x32\x2e\x32\x2c\x31\x37\x2e\x33\x34\x2d\ +\x38\x2e\x30\x36\x2c\x32\x2e\x30\x31\x2d\x31\x36\x2e\x36\x38\x2d\ +\x2e\x38\x33\x2d\x32\x30\x2e\x32\x2d\x31\x30\x2e\x38\x35\x6c\x2d\ +\x32\x39\x2e\x38\x35\x2d\x2e\x30\x39\x2d\x32\x34\x2e\x31\x32\x2c\ +\x32\x36\x2e\x34\x39\x2c\x31\x33\x35\x2e\x32\x36\x2d\x2e\x34\x39\ +\x2e\x37\x38\x2d\x31\x33\x2e\x31\x33\x2c\x33\x31\x2e\x34\x33\x2c\ +\x31\x38\x2e\x30\x32\x2d\x33\x31\x2e\x34\x31\x2c\x31\x38\x2e\x33\ +\x31\x5a\x4d\x34\x33\x32\x2e\x38\x33\x2c\x32\x33\x32\x2e\x34\x37\ +\x6c\x39\x36\x2e\x33\x38\x2d\x2e\x31\x36\x2e\x32\x32\x2c\x31\x33\ +\x34\x2e\x37\x36\x2d\x39\x36\x2e\x33\x38\x2e\x31\x36\x2d\x2e\x32\ +\x32\x2d\x31\x33\x34\x2e\x37\x36\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\ +\x76\x67\x3e\ \x00\x00\x03\x4a\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -25504,6 +25502,119 @@ \x34\x43\x34\x35\x39\x2c\x31\x34\x38\x2e\x31\x32\x2c\x34\x34\x34\ \x2e\x32\x33\x2c\x31\x35\x34\x2c\x34\x34\x32\x2e\x35\x36\x2c\x31\ \x36\x36\x2e\x31\x32\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x06\xe2\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\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\x34\x38\x39\x2e\x35\x39\ +\x2c\x34\x35\x31\x2e\x39\x39\x6c\x2d\x33\x36\x32\x2e\x38\x37\x2e\ +\x30\x32\x63\x2d\x35\x2e\x34\x38\x2c\x30\x2d\x31\x32\x2e\x37\x35\ +\x2d\x36\x2e\x33\x37\x2d\x31\x34\x2e\x33\x31\x2d\x31\x30\x2e\x33\ +\x35\x2d\x32\x2e\x30\x33\x2d\x35\x2e\x31\x37\x2e\x32\x2d\x31\x34\ +\x2e\x33\x37\x2c\x34\x2e\x32\x32\x2d\x31\x38\x2e\x38\x6c\x34\x34\ +\x2e\x36\x31\x2d\x34\x39\x2e\x31\x33\x63\x35\x2e\x33\x31\x2d\x35\ +\x2e\x38\x35\x2c\x31\x30\x2e\x35\x34\x2d\x31\x36\x2e\x39\x39\x2c\ +\x31\x30\x2e\x35\x32\x2d\x32\x34\x2e\x39\x36\x6c\x2d\x2e\x32\x2d\ +\x37\x34\x2e\x31\x38\x63\x2d\x2e\x31\x38\x2d\x36\x37\x2e\x33\x39\ +\x2c\x34\x35\x2e\x34\x2d\x31\x32\x34\x2e\x30\x39\x2c\x31\x31\x32\ +\x2e\x30\x32\x2d\x31\x33\x36\x2e\x32\x38\x6c\x2e\x31\x2d\x32\x38\ +\x2e\x32\x34\x63\x2e\x30\x35\x2d\x31\x34\x2e\x35\x38\x2c\x31\x30\ +\x2e\x31\x38\x2d\x32\x35\x2e\x35\x31\x2c\x32\x33\x2e\x38\x35\x2d\ +\x32\x36\x2e\x31\x32\x2c\x31\x32\x2e\x35\x34\x2d\x2e\x35\x36\x2c\ +\x32\x35\x2e\x31\x39\x2c\x39\x2e\x31\x38\x2c\x32\x35\x2e\x36\x31\ +\x2c\x32\x33\x2e\x35\x37\x6c\x2e\x38\x38\x2c\x33\x30\x2e\x36\x33\ +\x63\x36\x33\x2e\x38\x37\x2c\x31\x33\x2e\x33\x2c\x31\x31\x30\x2e\ +\x37\x39\x2c\x36\x37\x2e\x39\x34\x2c\x31\x31\x31\x2e\x31\x2c\x31\ +\x33\x33\x2e\x38\x31\x6c\x2e\x33\x34\x2c\x37\x34\x2e\x34\x33\x63\ +\x2e\x30\x34\x2c\x38\x2e\x34\x34\x2c\x33\x2e\x37\x2c\x31\x39\x2e\ +\x36\x2c\x39\x2e\x33\x2c\x32\x35\x2e\x38\x35\x6c\x34\x37\x2e\x35\ +\x35\x2c\x35\x33\x2e\x31\x32\x63\x32\x2e\x39\x33\x2c\x33\x2e\x32\ +\x37\x2c\x33\x2e\x38\x34\x2c\x31\x32\x2e\x38\x2c\x32\x2e\x34\x36\ +\x2c\x31\x36\x2e\x34\x36\x2d\x31\x2e\x35\x33\x2c\x34\x2e\x30\x34\ +\x2d\x37\x2e\x35\x33\x2c\x31\x30\x2e\x31\x37\x2d\x31\x35\x2e\x31\ +\x38\x2c\x31\x30\x2e\x31\x37\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\x34\x33\x39\x2e\x31\x39\x2c\x34\x36\x37\ +\x2e\x38\x38\x63\x2d\x2e\x39\x38\x2c\x33\x36\x2e\x35\x2d\x32\x39\ +\x2e\x35\x37\x2c\x36\x30\x2e\x37\x34\x2d\x36\x31\x2e\x39\x34\x2c\ +\x36\x31\x2e\x33\x36\x2d\x33\x33\x2e\x39\x31\x2e\x36\x36\x2d\x36\ +\x33\x2e\x39\x36\x2d\x32\x35\x2e\x39\x31\x2d\x36\x33\x2e\x34\x35\ +\x2d\x36\x31\x2e\x36\x35\x6c\x31\x32\x35\x2e\x34\x2e\x32\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\x31\x34\ +\x38\x2e\x35\x31\x2c\x32\x32\x37\x2e\x37\x33\x63\x2d\x31\x2e\x32\ +\x36\x2c\x35\x2e\x36\x34\x2d\x37\x2e\x35\x32\x2c\x39\x2e\x33\x33\ +\x2d\x31\x31\x2e\x35\x2c\x38\x2e\x34\x38\x2d\x33\x2e\x35\x31\x2d\ +\x2e\x37\x36\x2d\x37\x2e\x34\x36\x2d\x37\x2e\x30\x36\x2d\x36\x2e\ +\x34\x33\x2d\x31\x31\x2e\x38\x38\x2c\x31\x31\x2e\x31\x37\x2d\x35\ +\x32\x2e\x32\x36\x2c\x34\x37\x2e\x36\x33\x2d\x39\x33\x2e\x39\x31\ +\x2c\x39\x37\x2e\x35\x33\x2d\x31\x31\x33\x2e\x32\x31\x2c\x33\x2e\ +\x38\x32\x2d\x31\x2e\x34\x38\x2c\x31\x30\x2e\x36\x35\x2c\x33\x2e\ +\x31\x34\x2c\x31\x31\x2e\x33\x32\x2c\x36\x2e\x33\x33\x2e\x37\x34\ +\x2c\x33\x2e\x35\x34\x2d\x32\x2e\x32\x35\x2c\x39\x2e\x37\x32\x2d\ +\x36\x2e\x32\x34\x2c\x31\x31\x2e\x33\x33\x2d\x34\x33\x2e\x31\x38\ +\x2c\x31\x37\x2e\x33\x39\x2d\x37\x34\x2e\x32\x38\x2c\x35\x32\x2e\ +\x35\x32\x2d\x38\x34\x2e\x36\x38\x2c\x39\x38\x2e\x39\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\x34\x36\x37\ +\x2e\x37\x2c\x32\x32\x38\x2e\x36\x35\x63\x2d\x39\x2e\x39\x36\x2d\ +\x34\x36\x2e\x34\x34\x2d\x34\x30\x2e\x39\x37\x2d\x38\x32\x2e\x35\ +\x38\x2d\x38\x35\x2e\x30\x35\x2d\x31\x30\x30\x2e\x31\x35\x2d\x33\ +\x2e\x38\x2d\x31\x2e\x35\x32\x2d\x37\x2e\x30\x34\x2d\x37\x2e\x38\ +\x2d\x35\x2e\x39\x35\x2d\x31\x30\x2e\x38\x34\x2c\x31\x2e\x35\x36\ +\x2d\x34\x2e\x33\x34\x2c\x38\x2e\x31\x2d\x38\x2e\x32\x37\x2c\x31\ +\x33\x2e\x30\x34\x2d\x36\x2e\x32\x36\x2c\x34\x38\x2e\x39\x31\x2c\ +\x31\x39\x2e\x38\x39\x2c\x38\x34\x2e\x36\x33\x2c\x36\x30\x2e\x38\ +\x37\x2c\x39\x35\x2e\x37\x38\x2c\x31\x31\x32\x2e\x37\x36\x2e\x39\ +\x37\x2c\x34\x2e\x35\x33\x2d\x33\x2e\x33\x35\x2c\x31\x30\x2e\x35\ +\x39\x2d\x37\x2e\x30\x39\x2c\x31\x31\x2e\x37\x31\x2d\x33\x2e\x32\ +\x34\x2e\x39\x36\x2d\x39\x2e\x38\x33\x2d\x32\x2e\x39\x38\x2d\x31\ +\x30\x2e\x37\x34\x2d\x37\x2e\x32\x32\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\x31\x30\x32\x2e\x34\x32\x2c\x32\ +\x32\x37\x2e\x31\x38\x63\x2d\x31\x2e\x36\x39\x2c\x37\x2e\x35\x35\ +\x2d\x31\x30\x2e\x30\x37\x2c\x31\x32\x2e\x34\x39\x2d\x31\x35\x2e\ +\x33\x38\x2c\x31\x31\x2e\x33\x34\x2d\x34\x2e\x37\x2d\x31\x2e\x30\ +\x32\x2d\x39\x2e\x39\x38\x2d\x39\x2e\x34\x35\x2d\x38\x2e\x36\x2d\ +\x31\x35\x2e\x39\x2c\x31\x34\x2e\x39\x35\x2d\x36\x39\x2e\x39\x34\ +\x2c\x36\x33\x2e\x37\x35\x2d\x31\x32\x35\x2e\x36\x38\x2c\x31\x33\ +\x30\x2e\x35\x32\x2d\x31\x35\x31\x2e\x35\x2c\x35\x2e\x31\x31\x2d\ +\x31\x2e\x39\x38\x2c\x31\x34\x2e\x32\x35\x2c\x34\x2e\x32\x31\x2c\ +\x31\x35\x2e\x31\x35\x2c\x38\x2e\x34\x37\x2c\x31\x2c\x34\x2e\x37\ +\x34\x2d\x33\x2e\x30\x31\x2c\x31\x33\x2e\x30\x31\x2d\x38\x2e\x33\ +\x35\x2c\x31\x35\x2e\x31\x36\x2d\x35\x37\x2e\x37\x39\x2c\x32\x33\ +\x2e\x32\x37\x2d\x39\x39\x2e\x34\x31\x2c\x37\x30\x2e\x32\x38\x2d\ +\x31\x31\x33\x2e\x33\x33\x2c\x31\x33\x32\x2e\x34\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\x35\x31\x31\x2e\ +\x37\x36\x2c\x32\x32\x38\x2e\x33\x38\x63\x2d\x31\x33\x2e\x33\x33\ +\x2d\x36\x32\x2e\x31\x34\x2d\x35\x34\x2e\x38\x32\x2d\x31\x31\x30\ +\x2e\x35\x31\x2d\x31\x31\x33\x2e\x38\x32\x2d\x31\x33\x34\x2e\x30\ +\x32\x2d\x35\x2e\x30\x39\x2d\x32\x2e\x30\x33\x2d\x39\x2e\x34\x33\ +\x2d\x31\x30\x2e\x34\x33\x2d\x37\x2e\x39\x36\x2d\x31\x34\x2e\x35\ +\x2c\x32\x2e\x30\x39\x2d\x35\x2e\x38\x31\x2c\x31\x30\x2e\x38\x34\ +\x2d\x31\x31\x2e\x30\x37\x2c\x31\x37\x2e\x34\x36\x2d\x38\x2e\x33\ +\x38\x2c\x36\x35\x2e\x34\x35\x2c\x32\x36\x2e\x36\x32\x2c\x31\x31\ +\x33\x2e\x32\x36\x2c\x38\x31\x2e\x34\x36\x2c\x31\x32\x38\x2e\x31\ +\x38\x2c\x31\x35\x30\x2e\x39\x2c\x31\x2e\x33\x2c\x36\x2e\x30\x36\ +\x2d\x34\x2e\x34\x38\x2c\x31\x34\x2e\x31\x38\x2d\x39\x2e\x34\x39\ +\x2c\x31\x35\x2e\x36\x36\x2d\x34\x2e\x33\x34\x2c\x31\x2e\x32\x39\ +\x2d\x31\x33\x2e\x31\x35\x2d\x33\x2e\x39\x39\x2d\x31\x34\x2e\x33\ +\x37\x2d\x39\x2e\x36\x36\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\ +\x3e\ \x00\x00\x01\x28\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -26599,6 +26710,10 @@ \x04\xa2\xf1\x27\ \x00\x64\ \x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0e\ +\x06\x0c\xeb\x87\ +\x00\x61\ +\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x6f\x00\x77\x00\x6e\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0c\ \x06\xe6\xeb\xe7\ \x00\x75\ @@ -26607,6 +26722,10 @@ \x0e\xde\xf7\x47\ \x00\x6c\ \x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0f\ +\x0f\x2c\x29\x47\ +\x00\x61\ +\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x72\x00\x69\x00\x67\x00\x68\x00\x74\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0a\ \x01\x2a\xf4\x87\ \x00\x6e\ @@ -26931,14 +27050,6 @@ \x0c\xa5\x75\xc7\ \x00\x6c\ \x00\x6f\x00\x67\x00\x6f\x00\x5f\x00\x42\x00\x4c\x00\x4f\x00\x43\x00\x4b\x00\x53\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0d\ -\x00\xdd\x57\xa7\ -\x00\x31\ -\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0d\ -\x02\xdd\x57\xa7\ -\x00\x30\ -\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0f\ \x05\x15\x19\xa7\ \x00\x77\ @@ -26951,10 +27062,18 @@ \x07\xb9\x8d\x87\ \x00\x68\ \x00\x6f\x00\x74\x00\x73\x00\x70\x00\x6f\x00\x74\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0f\ -\x08\x5a\xc7\xe7\ -\x00\x77\ -\x00\x69\x00\x66\x00\x69\x00\x5f\x00\x6c\x00\x6f\x00\x63\x00\x6b\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0d\ +\x00\xdd\x57\xa7\ +\x00\x31\ +\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0d\ +\x02\xdd\x57\xa7\ +\x00\x30\ +\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0d\ +\x03\x52\x04\x07\ +\x00\x73\ +\x00\x74\x00\x61\x00\x74\x00\x69\x00\x63\x00\x5f\x00\x69\x00\x70\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0d\ \x0a\xdd\x57\x87\ \x00\x34\ @@ -26963,11 +27082,11 @@ \x0c\xdd\x57\x87\ \x00\x33\ \x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x11\ -\x0d\x4a\xc3\xc7\ -\x00\x77\ -\x00\x69\x00\x66\x00\x69\x00\x5f\x00\x75\x00\x6e\x00\x6c\x00\x6f\x00\x63\x00\x6b\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ -\ +\x00\x16\ +\x0d\x98\xb3\x87\ +\x00\x65\ +\x00\x74\x00\x68\x00\x65\x00\x72\x00\x6e\x00\x65\x00\x74\x00\x5f\x00\x63\x00\x6f\x00\x6e\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x65\ +\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0d\ \x0e\xdd\x57\x87\ \x00\x32\ @@ -26977,10 +27096,6 @@ \x00\x32\ \x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x5f\x00\x70\x00\x72\x00\x6f\x00\x74\x00\x65\x00\x63\x00\x74\ \x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0b\ -\x0f\x22\xf7\x67\ -\x00\x6e\ -\x00\x6f\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x17\ \x0f\x29\x74\x27\ \x00\x33\ @@ -27068,6 +27183,10 @@ \x02\x8c\x54\x27\ \x00\x70\ \x00\x6c\x00\x61\x00\x79\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x10\ +\x03\xe3\x31\x27\ +\x00\x6e\ +\x00\x6f\x00\x74\x00\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x08\ \x04\xd2\x54\xc7\ \x00\x69\ @@ -27117,6 +27236,10 @@ \x09\xcb\x31\x47\ \x00\x4c\ \x00\x43\x00\x44\x00\x5f\x00\x73\x00\x65\x00\x74\x00\x74\x00\x69\x00\x6e\x00\x67\x00\x73\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0c\ +\x09\xf2\x51\x27\ +\x00\x75\ +\x00\x73\x00\x62\x00\x5f\x00\x69\x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x12\ \x0a\x4f\x00\xc7\ \x00\x73\ @@ -27161,6 +27284,11 @@ \x0c\x6a\x21\xc7\ \x00\x72\ \x00\x65\x00\x66\x00\x72\x00\x65\x00\x73\x00\x68\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x17\ +\x0c\x83\xd3\x47\ +\x00\x6e\ +\x00\x6f\x00\x74\x00\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x5f\x00\x61\x00\x63\x00\x74\x00\x69\ +\x00\x76\x00\x65\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x09\ \x0c\x98\xb7\xc7\ \x00\x70\ @@ -27215,20 +27343,20 @@ qt_resource_struct_v1 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x10\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa1\ -\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9e\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ -\x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x80\ -\x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7c\ -\x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x76\ -\x00\x00\x00\x7e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x68\ -\x00\x00\x00\x90\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5e\ -\x00\x00\x00\xa2\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4f\ -\x00\x00\x00\xc0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4a\ -\x00\x00\x00\xdc\x00\x02\x00\x00\x00\x01\x00\x00\x00\x3a\ -\x00\x00\x01\x02\x00\x02\x00\x00\x00\x01\x00\x00\x00\x36\ -\x00\x00\x01\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x32\ -\x00\x00\x01\x42\x00\x02\x00\x00\x00\x01\x00\x00\x00\x26\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa3\ +\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa0\ +\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x94\ +\x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x82\ +\x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7e\ +\x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x78\ +\x00\x00\x00\x7e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x6a\ +\x00\x00\x00\x90\x00\x02\x00\x00\x00\x01\x00\x00\x00\x60\ +\x00\x00\x00\xa2\x00\x02\x00\x00\x00\x01\x00\x00\x00\x51\ +\x00\x00\x00\xc0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4c\ +\x00\x00\x00\xdc\x00\x02\x00\x00\x00\x01\x00\x00\x00\x3c\ +\x00\x00\x01\x02\x00\x02\x00\x00\x00\x01\x00\x00\x00\x38\ +\x00\x00\x01\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x34\ +\x00\x00\x01\x42\x00\x02\x00\x00\x00\x01\x00\x00\x00\x28\ \x00\x00\x01\x68\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ \x00\x00\x01\x84\x00\x02\x00\x00\x00\x01\x00\x00\x00\x11\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x12\ @@ -27247,208 +27375,213 @@ \x00\x00\x03\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x30\xca\ \x00\x00\x03\x92\x00\x00\x00\x00\x00\x01\x00\x00\x32\xb1\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x21\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x22\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x22\ \x00\x00\x03\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x3a\x55\ \x00\x00\x03\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x3c\xcf\ \x00\x00\x03\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x3f\x43\ -\x00\x00\x04\x16\x00\x00\x00\x00\x00\x01\x00\x00\x41\xbf\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x27\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x28\ -\x00\x00\x04\x38\x00\x00\x00\x00\x00\x01\x00\x00\x44\x3b\ -\x00\x00\x04\x52\x00\x00\x00\x00\x00\x01\x00\x00\x45\x3c\ -\x00\x00\x04\x82\x00\x00\x00\x00\x00\x01\x00\x00\x4a\x54\ -\x00\x00\x04\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x56\x16\ -\x00\x00\x04\xdc\x00\x00\x00\x00\x00\x01\x00\x00\x58\x86\ -\x00\x00\x05\x02\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x1e\ -\x00\x00\x05\x26\x00\x00\x00\x00\x00\x01\x00\x00\x62\x7a\ -\x00\x00\x05\x60\x00\x00\x00\x00\x00\x01\x00\x00\x69\x07\ -\x00\x00\x05\x7c\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x03\ -\x00\x00\x05\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x6d\x01\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x33\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x34\ -\x00\x00\x05\xd6\x00\x01\x00\x00\x00\x01\x00\x00\x77\x6b\ -\x00\x00\x05\xe8\x00\x00\x00\x00\x00\x01\x00\x02\x3a\xc0\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x37\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x38\ -\x00\x00\x05\xfc\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ -\x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x41\x71\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3b\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x44\ -\x00\x00\x06\x5a\x00\x02\x00\x00\x00\x07\x00\x00\x00\x3d\ -\x00\x00\x06\x6c\x00\x00\x00\x00\x00\x01\x00\x02\x43\x77\ -\x00\x00\x06\xa0\x00\x00\x00\x00\x00\x01\x00\x02\x4d\xdb\ -\x00\x00\x06\xd4\x00\x00\x00\x00\x00\x01\x00\x02\x58\x04\ -\x00\x00\x07\x0e\x00\x00\x00\x00\x00\x01\x00\x02\x62\x5c\ -\x00\x00\x07\x42\x00\x00\x00\x00\x00\x01\x00\x02\x6c\x74\ -\x00\x00\x07\x78\x00\x00\x00\x00\x00\x01\x00\x02\x76\x9d\ -\x00\x00\x07\xb0\x00\x00\x00\x00\x00\x01\x00\x02\x80\xb3\ -\x00\x00\x07\xe6\x00\x00\x00\x00\x00\x01\x00\x02\x8b\x19\ -\x00\x00\x08\x0e\x00\x00\x00\x00\x00\x01\x00\x02\x95\x48\ -\x00\x00\x08\x3a\x00\x00\x00\x00\x00\x01\x00\x02\x9f\x8f\ -\x00\x00\x08\x76\x00\x00\x00\x00\x00\x01\x00\x02\xa1\x90\ -\x00\x00\x08\xa2\x00\x00\x00\x00\x00\x01\x00\x02\xbc\xd5\ -\x00\x00\x08\xce\x00\x00\x00\x00\x00\x01\x00\x02\xc2\x1e\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4b\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x03\x00\x00\x00\x4c\ -\x00\x00\x09\x02\x00\x00\x00\x00\x00\x01\x00\x02\xc5\x96\ -\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x02\xce\x78\ -\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x02\xd3\xad\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x50\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x51\ -\x00\x00\x09\x4e\x00\x00\x00\x00\x00\x01\x00\x02\xdd\x82\ -\x00\x00\x09\x76\x00\x00\x00\x00\x00\x01\x00\x02\xe2\x7d\ -\x00\x00\x09\xae\x00\x00\x00\x00\x00\x01\x00\x02\xeb\x51\ -\x00\x00\x09\xe6\x00\x00\x00\x00\x00\x01\x00\x02\xf3\xf5\ -\x00\x00\x0a\x16\x00\x00\x00\x00\x00\x01\x00\x02\xfb\x94\ -\x00\x00\x0a\x3a\x00\x00\x00\x00\x00\x01\x00\x03\x03\x70\ -\x00\x00\x0a\x5e\x00\x00\x00\x00\x00\x01\x00\x03\x0b\x26\ -\x00\x00\x0a\x92\x00\x00\x00\x00\x00\x01\x00\x03\x13\x34\ -\x00\x00\x0a\xc6\x00\x00\x00\x00\x00\x01\x00\x03\x1b\x16\ -\x00\x00\x0a\xfa\x00\x00\x00\x00\x00\x01\x00\x03\x22\xe0\ -\x00\x00\x0b\x34\x00\x00\x00\x00\x00\x01\x00\x03\x2a\x47\ -\x00\x00\x0b\x58\x00\x00\x00\x00\x00\x01\x00\x03\x2e\x04\ -\x00\x00\x0b\x86\x00\x00\x00\x00\x00\x01\x00\x03\x31\xdd\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5f\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x08\x00\x00\x00\x60\ -\x00\x00\x0b\xac\x00\x00\x00\x00\x00\x01\x00\x03\x37\xe6\ -\x00\x00\x0b\xd8\x00\x00\x00\x00\x00\x01\x00\x03\x5a\x6a\ -\x00\x00\x0c\x06\x00\x00\x00\x00\x00\x01\x00\x03\x60\x57\ -\x00\x00\x0c\x30\x00\x00\x00\x00\x00\x01\x00\x03\x62\x77\ -\x00\x00\x0c\x58\x00\x00\x00\x00\x00\x01\x00\x03\x6b\x0f\ -\x00\x00\x0c\x72\x00\x00\x00\x00\x00\x01\x00\x03\x7a\x58\ -\x00\x00\x0c\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x80\xd6\ -\x00\x00\x0c\xc6\x00\x00\x00\x00\x00\x01\x00\x03\x8b\x50\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x69\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x6a\ -\x00\x00\x0c\xfc\x00\x00\x00\x00\x00\x01\x00\x03\x94\x97\ -\x00\x00\x0d\x2a\x00\x00\x00\x00\x00\x01\x00\x03\x97\x3e\ -\x00\x00\x0d\x58\x00\x01\x00\x00\x00\x01\x00\x03\xa3\xbb\ -\x00\x00\x0d\x84\x00\x00\x00\x00\x00\x01\x00\x03\xd1\x3a\ -\x00\x00\x0d\xa4\x00\x00\x00\x00\x00\x01\x00\x03\xd5\xf1\ -\x00\x00\x0d\xd6\x00\x01\x00\x00\x00\x01\x00\x04\x2e\xea\ -\x00\x00\x0e\x08\x00\x00\x00\x00\x00\x01\x00\x04\x63\x84\ -\x00\x00\x0e\x22\x00\x00\x00\x00\x00\x01\x00\x04\x68\xce\ -\x00\x00\x0e\x3c\x00\x00\x00\x00\x00\x01\x00\x04\x6e\x5d\ -\x00\x00\x0e\x56\x00\x00\x00\x00\x00\x01\x00\x04\x73\xc6\ -\x00\x00\x0e\x6e\x00\x00\x00\x00\x00\x01\x00\x04\x7f\xa4\ -\x00\x00\x0e\x8c\x00\x00\x00\x00\x00\x01\x00\x04\x85\xa8\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x77\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x78\ -\x00\x00\x0e\xb4\x00\x00\x00\x00\x00\x01\x00\x04\x8a\xdd\ -\x00\x00\x0e\xc8\x00\x00\x00\x00\x00\x01\x00\x04\x90\xda\ -\x00\x00\x0e\xda\x00\x00\x00\x00\x00\x01\x00\x04\x92\x60\ -\x00\x00\x0e\xec\x00\x00\x00\x00\x00\x01\x00\x04\x98\x5a\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7d\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x7e\ -\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x01\x00\x04\x9a\xb0\ -\x00\x00\x0f\x2c\x00\x00\x00\x00\x00\x01\x00\x04\xa1\x97\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x81\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x10\x00\x00\x00\x82\ -\x00\x00\x0f\x50\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xfb\ -\x00\x00\x0f\x70\x00\x00\x00\x00\x00\x01\x00\x04\xb0\x94\ -\x00\x00\x0f\x90\x00\x01\x00\x00\x00\x01\x00\x04\xb6\xbb\ -\x00\x00\x0f\xb4\x00\x00\x00\x00\x00\x01\x00\x04\xc2\x0c\ -\x00\x00\x0f\xd6\x00\x00\x00\x00\x00\x01\x00\x04\xc9\xb1\ -\x00\x00\x0f\xf2\x00\x00\x00\x00\x00\x01\x00\x04\xda\xb1\ -\x00\x00\x10\x16\x00\x00\x00\x00\x00\x01\x00\x04\xe4\x32\ -\x00\x00\x10\x36\x00\x00\x00\x00\x00\x01\x00\x04\xe9\xb6\ -\x00\x00\x10\x56\x00\x00\x00\x00\x00\x01\x00\x04\xef\x4f\ -\x00\x00\x10\x7e\x00\x00\x00\x00\x00\x01\x00\x04\xf9\x18\ -\x00\x00\x10\x9e\x00\x00\x00\x00\x00\x01\x00\x04\xfe\xb1\ -\x00\x00\x10\xd2\x00\x00\x00\x00\x00\x01\x00\x05\x09\x25\ -\x00\x00\x10\xee\x00\x00\x00\x00\x00\x01\x00\x05\x0f\x60\ -\x00\x00\x11\x22\x00\x00\x00\x00\x00\x01\x00\x05\x19\xda\ -\x00\x00\x11\x56\x00\x00\x00\x00\x00\x01\x00\x05\x24\x39\ -\x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x2f\x84\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x94\ -\x00\x00\x11\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x39\xf8\ -\x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x43\x8e\ -\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x6a\ -\x00\x00\x12\x42\x00\x00\x00\x00\x00\x01\x00\x05\x55\xae\ -\x00\x00\x12\x6c\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x37\ -\x00\x00\x12\x98\x00\x00\x00\x00\x00\x01\x00\x05\x63\x95\ -\x00\x00\x12\xce\x00\x00\x00\x00\x00\x01\x00\x05\x6b\x84\ -\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x79\x27\ -\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x7e\x5c\ -\x00\x00\x12\xec\x00\x00\x00\x00\x00\x01\x00\x05\x88\x31\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9f\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa0\ -\x00\x00\x13\x14\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xfb\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa2\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x28\x00\x00\x00\xa3\ -\x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x94\xc1\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x75\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\x9a\ -\x00\x00\x13\x9a\x00\x00\x00\x00\x00\x01\x00\x05\xa0\x1a\ -\x00\x00\x13\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xa7\xc7\ -\x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xac\x9c\ -\x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xad\x8c\ -\x00\x00\x14\x02\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xb5\ -\x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb7\xec\ -\x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xcc\xf9\ -\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xd1\xe9\ -\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xd4\xe8\ -\x00\x00\x14\x90\x00\x00\x00\x00\x00\x01\x00\x05\xda\xf2\ -\x00\x00\x14\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xde\x41\ -\x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xe2\x62\ -\x00\x00\x14\xf0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x59\ -\x00\x00\x15\x04\x00\x00\x00\x00\x00\x01\x00\x05\xea\x6a\ -\x00\x00\x15\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xee\x2d\ -\x00\x00\x15\x42\x00\x00\x00\x00\x00\x01\x00\x05\xf7\xe1\ -\x00\x00\x15\x6c\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x2f\ -\x00\x00\x15\x8e\x00\x00\x00\x00\x00\x01\x00\x05\xfe\xbd\ -\x00\x00\x15\xb4\x00\x00\x00\x00\x00\x01\x00\x06\x03\x5e\ -\x00\x00\x15\xc8\x00\x00\x00\x00\x00\x01\x00\x06\x0d\x30\ -\x00\x00\x15\xf4\x00\x00\x00\x00\x00\x01\x00\x06\x12\x7a\ -\x00\x00\x16\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x18\x81\ -\x00\x00\x16\x32\x00\x00\x00\x00\x00\x01\x00\x06\x19\x65\ -\x00\x00\x16\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x1b\xae\ -\x00\x00\x16\x74\x00\x00\x00\x00\x00\x01\x00\x06\x22\x5e\ -\x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x06\x25\xa2\ -\x00\x00\x16\xa8\x00\x00\x00\x00\x00\x01\x00\x06\x26\xce\ -\x00\x00\x16\xc2\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x8f\ -\x00\x00\x16\xe4\x00\x00\x00\x00\x00\x01\x00\x06\x2d\xb1\ -\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\xa4\ -\x00\x00\x17\x22\x00\x00\x00\x00\x00\x01\x00\x06\x36\xa8\ -\x00\x00\x17\x44\x00\x00\x00\x00\x00\x01\x00\x06\x37\xc9\ -\x00\x00\x17\x64\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x9d\ -\x00\x00\x17\x92\x00\x00\x00\x00\x00\x01\x00\x06\x43\x09\ -\x00\x00\x17\xb6\x00\x00\x00\x00\x00\x01\x00\x06\x4a\xc9\ -\x00\x00\x17\xda\x00\x00\x00\x00\x00\x01\x00\x06\x4f\xe4\ -\x00\x00\x18\x02\x00\x00\x00\x00\x00\x01\x00\x06\x51\x37\ +\x00\x00\x04\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x40\xdd\ +\x00\x00\x04\x38\x00\x00\x00\x00\x00\x01\x00\x00\x43\x59\ +\x00\x00\x04\x5a\x00\x00\x00\x00\x00\x01\x00\x00\x45\xd5\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x29\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x2a\ +\x00\x00\x04\x7e\x00\x00\x00\x00\x00\x01\x00\x00\x47\x6d\ +\x00\x00\x04\x98\x00\x00\x00\x00\x00\x01\x00\x00\x48\x6e\ +\x00\x00\x04\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x4d\x86\ +\x00\x00\x04\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x59\x48\ +\x00\x00\x05\x22\x00\x00\x00\x00\x00\x01\x00\x00\x5b\xb8\ +\x00\x00\x05\x48\x00\x00\x00\x00\x00\x01\x00\x00\x61\x50\ +\x00\x00\x05\x6c\x00\x00\x00\x00\x00\x01\x00\x00\x65\xac\ +\x00\x00\x05\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x39\ +\x00\x00\x05\xc2\x00\x00\x00\x00\x00\x01\x00\x00\x6f\x35\ +\x00\x00\x05\xea\x00\x00\x00\x00\x00\x01\x00\x00\x70\x33\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x35\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x36\ +\x00\x00\x06\x1c\x00\x01\x00\x00\x00\x01\x00\x00\x7a\x9d\ +\x00\x00\x06\x2e\x00\x00\x00\x00\x00\x01\x00\x02\x3d\xf2\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x39\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3a\ +\x00\x00\x06\x42\x00\x00\x00\x00\x00\x01\x00\x02\x42\x78\ +\x00\x00\x06\x70\x00\x00\x00\x00\x00\x01\x00\x02\x44\xa3\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3d\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x46\ +\x00\x00\x06\xa0\x00\x02\x00\x00\x00\x07\x00\x00\x00\x3f\ +\x00\x00\x06\xb2\x00\x00\x00\x00\x00\x01\x00\x02\x46\xa9\ +\x00\x00\x06\xe6\x00\x00\x00\x00\x00\x01\x00\x02\x51\x0d\ +\x00\x00\x07\x1a\x00\x00\x00\x00\x00\x01\x00\x02\x5b\x36\ +\x00\x00\x07\x54\x00\x00\x00\x00\x00\x01\x00\x02\x65\x8e\ +\x00\x00\x07\x88\x00\x00\x00\x00\x00\x01\x00\x02\x6f\xa6\ +\x00\x00\x07\xbe\x00\x00\x00\x00\x00\x01\x00\x02\x79\xcf\ +\x00\x00\x07\xf6\x00\x00\x00\x00\x00\x01\x00\x02\x83\xe5\ +\x00\x00\x08\x2c\x00\x00\x00\x00\x00\x01\x00\x02\x8e\x4b\ +\x00\x00\x08\x54\x00\x00\x00\x00\x00\x01\x00\x02\x98\x7a\ +\x00\x00\x08\x80\x00\x00\x00\x00\x00\x01\x00\x02\xa2\xc1\ +\x00\x00\x08\xbc\x00\x00\x00\x00\x00\x01\x00\x02\xa4\xc2\ +\x00\x00\x08\xe8\x00\x00\x00\x00\x00\x01\x00\x02\xc0\x07\ +\x00\x00\x09\x14\x00\x00\x00\x00\x00\x01\x00\x02\xc5\x50\ +\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\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\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\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\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\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\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\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\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\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\ " qt_resource_struct_v2 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x10\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa1\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa3\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9e\ +\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa0\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ +\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x94\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x80\ +\x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x82\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7c\ +\x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7e\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x76\ +\x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x78\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x7e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x68\ +\x00\x00\x00\x7e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x6a\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x90\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5e\ +\x00\x00\x00\x90\x00\x02\x00\x00\x00\x01\x00\x00\x00\x60\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\xa2\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4f\ +\x00\x00\x00\xa2\x00\x02\x00\x00\x00\x01\x00\x00\x00\x51\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\xc0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4a\ +\x00\x00\x00\xc0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4c\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\xdc\x00\x02\x00\x00\x00\x01\x00\x00\x00\x3a\ +\x00\x00\x00\xdc\x00\x02\x00\x00\x00\x01\x00\x00\x00\x3c\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x02\x00\x02\x00\x00\x00\x01\x00\x00\x00\x36\ +\x00\x00\x01\x02\x00\x02\x00\x00\x00\x01\x00\x00\x00\x38\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x32\ +\x00\x00\x01\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x34\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x42\x00\x02\x00\x00\x00\x01\x00\x00\x00\x26\ +\x00\x00\x01\x42\x00\x02\x00\x00\x00\x01\x00\x00\x00\x28\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x68\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ \x00\x00\x00\x00\x00\x00\x00\x00\ @@ -27459,373 +27592,383 @@ \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\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x01\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x08\x66\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x02\x10\x00\x00\x00\x00\x00\x01\x00\x00\x09\x52\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x02\x38\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x35\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x02\x60\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x18\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x02\x88\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x06\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x02\xbc\x00\x00\x00\x00\x00\x01\x00\x00\x14\x42\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x02\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf7\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x03\x12\x00\x00\x00\x00\x00\x01\x00\x00\x22\x41\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x03\x32\x00\x00\x00\x00\x00\x01\x00\x00\x26\x90\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x03\x4e\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x6e\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x03\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x30\xca\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x03\x92\x00\x00\x00\x00\x00\x01\x00\x00\x32\xb1\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \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\x04\x00\x00\x00\x22\ +\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\x9a\x72\xe1\x94\x57\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x03\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x3c\xcf\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x03\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x3f\x43\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x04\x16\x00\x00\x00\x00\x00\x01\x00\x00\x41\xbf\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x27\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ +\x00\x00\x04\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x40\xdd\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ +\x00\x00\x04\x38\x00\x00\x00\x00\x00\x01\x00\x00\x43\x59\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\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\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\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x28\ +\x00\x00\x04\x7e\x00\x00\x00\x00\x00\x01\x00\x00\x47\x6d\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x04\x98\x00\x00\x00\x00\x00\x01\x00\x00\x48\x6e\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x04\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x4d\x86\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ +\x00\x00\x04\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x59\x48\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x05\x22\x00\x00\x00\x00\x00\x01\x00\x00\x5b\xb8\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ +\x00\x00\x05\x48\x00\x00\x00\x00\x00\x01\x00\x00\x61\x50\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ +\x00\x00\x05\x6c\x00\x00\x00\x00\x00\x01\x00\x00\x65\xac\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x05\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x39\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x05\xc2\x00\x00\x00\x00\x00\x01\x00\x00\x6f\x35\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\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\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x35\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x04\x38\x00\x00\x00\x00\x00\x01\x00\x00\x44\x3b\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x04\x52\x00\x00\x00\x00\x00\x01\x00\x00\x45\x3c\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x04\x82\x00\x00\x00\x00\x00\x01\x00\x00\x4a\x54\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x04\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x56\x16\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x04\xdc\x00\x00\x00\x00\x00\x01\x00\x00\x58\x86\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x05\x02\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x1e\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x05\x26\x00\x00\x00\x00\x00\x01\x00\x00\x62\x7a\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x05\x60\x00\x00\x00\x00\x00\x01\x00\x00\x69\x07\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x05\x7c\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x03\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x05\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x6d\x01\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x33\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x36\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x34\ +\x00\x00\x06\x1c\x00\x01\x00\x00\x00\x01\x00\x00\x7a\x9d\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\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\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x39\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x05\xd6\x00\x01\x00\x00\x00\x01\x00\x00\x77\x6b\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x05\xe8\x00\x00\x00\x00\x00\x01\x00\x02\x3a\xc0\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x37\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3a\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x38\ +\x00\x00\x06\x42\x00\x00\x00\x00\x00\x01\x00\x02\x42\x78\ +\x00\x00\x01\x9b\xc6\xe6\xb1\x6c\ +\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\xa0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3d\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x05\xfc\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ -\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ -\x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x41\x71\ -\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3b\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x46\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x44\ +\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\x5a\x00\x02\x00\x00\x00\x07\x00\x00\x00\x3d\ +\x00\x00\x06\xb2\x00\x00\x00\x00\x00\x01\x00\x02\x46\xa9\ +\x00\x00\x01\x98\x55\x96\x0d\x7b\ +\x00\x00\x06\xe6\x00\x00\x00\x00\x00\x01\x00\x02\x51\x0d\ +\x00\x00\x01\x98\x55\x96\x0d\x7b\ +\x00\x00\x07\x1a\x00\x00\x00\x00\x00\x01\x00\x02\x5b\x36\ +\x00\x00\x01\x98\x55\x96\x0d\x7b\ +\x00\x00\x07\x54\x00\x00\x00\x00\x00\x01\x00\x02\x65\x8e\ +\x00\x00\x01\x98\x55\x96\x0d\x7b\ +\x00\x00\x07\x88\x00\x00\x00\x00\x00\x01\x00\x02\x6f\xa6\ +\x00\x00\x01\x98\x55\x96\x0d\x7b\ +\x00\x00\x07\xbe\x00\x00\x00\x00\x00\x01\x00\x02\x79\xcf\ +\x00\x00\x01\x98\x55\x96\x0d\x7b\ +\x00\x00\x07\xf6\x00\x00\x00\x00\x00\x01\x00\x02\x83\xe5\ +\x00\x00\x01\x98\x55\x96\x0d\x7b\ +\x00\x00\x08\x2c\x00\x00\x00\x00\x00\x01\x00\x02\x8e\x4b\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ +\x00\x00\x08\x54\x00\x00\x00\x00\x00\x01\x00\x02\x98\x7a\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ +\x00\x00\x08\x80\x00\x00\x00\x00\x00\x01\x00\x02\xa2\xc1\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x08\xbc\x00\x00\x00\x00\x00\x01\x00\x02\xa4\xc2\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ +\x00\x00\x08\xe8\x00\x00\x00\x00\x00\x01\x00\x02\xc0\x07\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ +\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\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4d\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x06\x6c\x00\x00\x00\x00\x00\x01\x00\x02\x43\x77\ -\x00\x00\x01\x9a\x72\xe1\x95\x8f\ -\x00\x00\x06\xa0\x00\x00\x00\x00\x00\x01\x00\x02\x4d\xdb\ -\x00\x00\x01\x9a\x72\xe1\x95\x93\ -\x00\x00\x06\xd4\x00\x00\x00\x00\x00\x01\x00\x02\x58\x04\ -\x00\x00\x01\x9a\x72\xe1\x95\x8f\ -\x00\x00\x07\x0e\x00\x00\x00\x00\x00\x01\x00\x02\x62\x5c\ -\x00\x00\x01\x9a\x72\xe1\x95\x93\ -\x00\x00\x07\x42\x00\x00\x00\x00\x00\x01\x00\x02\x6c\x74\ -\x00\x00\x01\x9a\x72\xe1\x95\x8f\ -\x00\x00\x07\x78\x00\x00\x00\x00\x00\x01\x00\x02\x76\x9d\ -\x00\x00\x01\x9a\x72\xe1\x95\x93\ -\x00\x00\x07\xb0\x00\x00\x00\x00\x00\x01\x00\x02\x80\xb3\ -\x00\x00\x01\x9a\x72\xe1\x95\x93\ -\x00\x00\x07\xe6\x00\x00\x00\x00\x00\x01\x00\x02\x8b\x19\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x08\x0e\x00\x00\x00\x00\x00\x01\x00\x02\x95\x48\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x08\x3a\x00\x00\x00\x00\x00\x01\x00\x02\x9f\x8f\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x08\x76\x00\x00\x00\x00\x00\x01\x00\x02\xa1\x90\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x08\xa2\x00\x00\x00\x00\x00\x01\x00\x02\xbc\xd5\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x08\xce\x00\x00\x00\x00\x00\x01\x00\x02\xc2\x1e\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4b\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x03\x00\x00\x00\x4e\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x03\x00\x00\x00\x4c\ +\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\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x52\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x09\x02\x00\x00\x00\x00\x00\x01\x00\x02\xc5\x96\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x02\xce\x78\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x02\xd3\xad\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x50\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x53\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x51\ +\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\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x61\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x09\x4e\x00\x00\x00\x00\x00\x01\x00\x02\xdd\x82\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x09\x76\x00\x00\x00\x00\x00\x01\x00\x02\xe2\x7d\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x09\xae\x00\x00\x00\x00\x00\x01\x00\x02\xeb\x51\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x09\xe6\x00\x00\x00\x00\x00\x01\x00\x02\xf3\xf5\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x0a\x16\x00\x00\x00\x00\x00\x01\x00\x02\xfb\x94\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x0a\x3a\x00\x00\x00\x00\x00\x01\x00\x03\x03\x70\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x0a\x5e\x00\x00\x00\x00\x00\x01\x00\x03\x0b\x26\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x0a\x92\x00\x00\x00\x00\x00\x01\x00\x03\x13\x34\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x0a\xc6\x00\x00\x00\x00\x00\x01\x00\x03\x1b\x16\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x0a\xfa\x00\x00\x00\x00\x00\x01\x00\x03\x22\xe0\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x0b\x34\x00\x00\x00\x00\x00\x01\x00\x03\x2a\x47\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x0b\x58\x00\x00\x00\x00\x00\x01\x00\x03\x2e\x04\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x0b\x86\x00\x00\x00\x00\x00\x01\x00\x03\x31\xdd\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5f\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x08\x00\x00\x00\x62\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x08\x00\x00\x00\x60\ +\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\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x6b\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0b\xac\x00\x00\x00\x00\x00\x01\x00\x03\x37\xe6\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x0b\xd8\x00\x00\x00\x00\x00\x01\x00\x03\x5a\x6a\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x0c\x06\x00\x00\x00\x00\x00\x01\x00\x03\x60\x57\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x0c\x30\x00\x00\x00\x00\x00\x01\x00\x03\x62\x77\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x0c\x58\x00\x00\x00\x00\x00\x01\x00\x03\x6b\x0f\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x0c\x72\x00\x00\x00\x00\x00\x01\x00\x03\x7a\x58\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x0c\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x80\xd6\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x0c\xc6\x00\x00\x00\x00\x00\x01\x00\x03\x8b\x50\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x69\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x6c\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x6a\ +\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\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x79\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0c\xfc\x00\x00\x00\x00\x00\x01\x00\x03\x94\x97\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x0d\x2a\x00\x00\x00\x00\x00\x01\x00\x03\x97\x3e\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x0d\x58\x00\x01\x00\x00\x00\x01\x00\x03\xa3\xbb\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x0d\x84\x00\x00\x00\x00\x00\x01\x00\x03\xd1\x3a\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x0d\xa4\x00\x00\x00\x00\x00\x01\x00\x03\xd5\xf1\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x0d\xd6\x00\x01\x00\x00\x00\x01\x00\x04\x2e\xea\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x0e\x08\x00\x00\x00\x00\x00\x01\x00\x04\x63\x84\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x0e\x22\x00\x00\x00\x00\x00\x01\x00\x04\x68\xce\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x0e\x3c\x00\x00\x00\x00\x00\x01\x00\x04\x6e\x5d\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x0e\x56\x00\x00\x00\x00\x00\x01\x00\x04\x73\xc6\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x0e\x6e\x00\x00\x00\x00\x00\x01\x00\x04\x7f\xa4\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x0e\x8c\x00\x00\x00\x00\x00\x01\x00\x04\x85\xa8\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x77\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x7a\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x78\ +\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\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7f\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0e\xb4\x00\x00\x00\x00\x00\x01\x00\x04\x8a\xdd\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x0e\xc8\x00\x00\x00\x00\x00\x01\x00\x04\x90\xda\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x0e\xda\x00\x00\x00\x00\x00\x01\x00\x04\x92\x60\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x0e\xec\x00\x00\x00\x00\x00\x01\x00\x04\x98\x5a\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7d\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x80\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x7e\ +\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\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x83\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x01\x00\x04\x9a\xb0\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x0f\x2c\x00\x00\x00\x00\x00\x01\x00\x04\xa1\x97\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x81\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x84\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x10\x00\x00\x00\x82\ +\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\x50\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xfb\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x0f\x70\x00\x00\x00\x00\x00\x01\x00\x04\xb0\x94\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x0f\x90\x00\x01\x00\x00\x00\x01\x00\x04\xb6\xbb\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x0f\xb4\x00\x00\x00\x00\x00\x01\x00\x04\xc2\x0c\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x0f\xd6\x00\x00\x00\x00\x00\x01\x00\x04\xc9\xb1\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x0f\xf2\x00\x00\x00\x00\x00\x01\x00\x04\xda\xb1\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x10\x16\x00\x00\x00\x00\x00\x01\x00\x04\xe4\x32\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x10\x36\x00\x00\x00\x00\x00\x01\x00\x04\xe9\xb6\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x10\x56\x00\x00\x00\x00\x00\x01\x00\x04\xef\x4f\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x10\x7e\x00\x00\x00\x00\x00\x01\x00\x04\xf9\x18\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x10\x9e\x00\x00\x00\x00\x00\x01\x00\x04\xfe\xb1\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x10\xd2\x00\x00\x00\x00\x00\x01\x00\x05\x09\x25\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x10\xee\x00\x00\x00\x00\x00\x01\x00\x05\x0f\x60\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x11\x22\x00\x00\x00\x00\x00\x01\x00\x05\x19\xda\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x11\x56\x00\x00\x00\x00\x00\x01\x00\x05\x24\x39\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x2f\x84\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ +\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\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\x94\ +\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\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x39\xf8\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x43\x8e\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x6a\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\x42\x00\x00\x00\x00\x00\x01\x00\x05\x55\xae\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\x6c\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x37\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x12\x98\x00\x00\x00\x00\x00\x01\x00\x05\x63\x95\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x12\xce\x00\x00\x00\x00\x00\x01\x00\x05\x6b\x84\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x79\x27\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x7e\x5c\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x12\xec\x00\x00\x00\x00\x00\x01\x00\x05\x88\x31\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9f\ +\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\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\xa0\ +\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\x14\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xfb\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x01\xa0\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\x01\x98\x55\x96\x0d\x35\ +\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\x28\x00\x00\x00\xa3\ +\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\x34\x00\x00\x00\x00\x00\x01\x00\x05\x94\xc1\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x75\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\x9a\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x13\x9a\x00\x00\x00\x00\x00\x01\x00\x05\xa0\x1a\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xa7\xc7\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xac\x9c\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xad\x8c\ -\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ -\x00\x00\x14\x02\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xb5\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb7\xec\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xcc\xf9\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xd1\xe9\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xd4\xe8\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x90\x00\x00\x00\x00\x00\x01\x00\x05\xda\xf2\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x14\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xde\x41\ -\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ -\x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xe2\x62\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x14\xf0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x59\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x04\x00\x00\x00\x00\x00\x01\x00\x05\xea\x6a\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x15\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xee\x2d\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x15\x42\x00\x00\x00\x00\x00\x01\x00\x05\xf7\xe1\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x6c\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x2f\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x8e\x00\x00\x00\x00\x00\x01\x00\x05\xfe\xbd\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x15\xb4\x00\x00\x00\x00\x00\x01\x00\x06\x03\x5e\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\xc8\x00\x00\x00\x00\x00\x01\x00\x06\x0d\x30\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x15\xf4\x00\x00\x00\x00\x00\x01\x00\x06\x12\x7a\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x18\x81\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\x32\x00\x00\x00\x00\x00\x01\x00\x06\x19\x65\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x16\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x1b\xae\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x16\x74\x00\x00\x00\x00\x00\x01\x00\x06\x22\x5e\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x06\x25\xa2\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\xa8\x00\x00\x00\x00\x00\x01\x00\x06\x26\xce\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\xc2\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x8f\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\xe4\x00\x00\x00\x00\x00\x01\x00\x06\x2d\xb1\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\xa4\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x17\x22\x00\x00\x00\x00\x00\x01\x00\x06\x36\xa8\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x44\x00\x00\x00\x00\x00\x01\x00\x06\x37\xc9\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x64\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x9d\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x17\x92\x00\x00\x00\x00\x00\x01\x00\x06\x43\x09\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x17\xb6\x00\x00\x00\x00\x00\x01\x00\x06\x4a\xc9\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x17\xda\x00\x00\x00\x00\x00\x01\x00\x06\x4f\xe4\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x18\x02\x00\x00\x00\x00\x00\x01\x00\x06\x51\x37\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\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\ " qt_version = [int(v) for v in QtCore.qVersion().split('.')] diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/arrow_down.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/arrow_down.svg new file mode 100644 index 00000000..1c23e554 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/arrow_down.svg @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/arrow_right.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/arrow_right.svg new file mode 100644 index 00000000..8abacdf2 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/arrow_right.svg @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/blower.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/blower.svg index 6de49748..027ea01c 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/blower.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/blower.svg @@ -1 +1,15 @@ - \ No newline at end of file + + + + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/error.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/error.svg index 0bb1f19f..27d84d05 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/error.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/error.svg @@ -7,7 +7,6 @@ } - - - + + \ 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 9c0023ab..46bccde3 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/fan.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/fan.svg @@ -1 +1,11 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/0bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/0bar_wifi.svg new file mode 100644 index 00000000..ceaff53d --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/0bar_wifi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/0bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/0bar_wifi_protected.svg new file mode 100644 index 00000000..a10ea388 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/0bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/1bar_wifi.svg similarity index 100% rename from BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi.svg rename to BlocksScreen/lib/ui/resources/media/btn_icons/network/1bar_wifi.svg diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/1bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/1bar_wifi_protected.svg new file mode 100644 index 00000000..8793447e --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/1bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/2bar_wifi.svg similarity index 100% rename from BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi.svg rename to BlocksScreen/lib/ui/resources/media/btn_icons/network/2bar_wifi.svg diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/2bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/2bar_wifi_protected.svg new file mode 100644 index 00000000..a9f3233b --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/2bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/3bar_wifi.svg similarity index 100% rename from BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi.svg rename to BlocksScreen/lib/ui/resources/media/btn_icons/network/3bar_wifi.svg diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/3bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/3bar_wifi_protected.svg new file mode 100644 index 00000000..458c1ac5 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/3bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/4bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/4bar_wifi.svg new file mode 100644 index 00000000..9aadd8e7 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/4bar_wifi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/4bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/4bar_wifi_protected.svg new file mode 100644 index 00000000..639762e7 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/4bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/ethernet_connected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/ethernet_connected.svg new file mode 100644 index 00000000..8f727437 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/ethernet_connected.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/static_ip.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/static_ip.svg new file mode 100644 index 00000000..92c07b79 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/static_ip.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/notification.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/notification.svg new file mode 100644 index 00000000..ac306fb6 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/notification.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/notification_active.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/notification_active.svg new file mode 100644 index 00000000..4c3915e2 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/notification_active.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/usb_icon.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/usb_icon.svg new file mode 100644 index 00000000..3ea01599 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/usb_icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/topbar/internet_cable.svg b/BlocksScreen/lib/ui/resources/media/topbar/internet_cable.svg deleted file mode 100644 index 0fd24fd8..00000000 --- a/BlocksScreen/lib/ui/resources/media/topbar/internet_cable.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/top_bar_resources.qrc b/BlocksScreen/lib/ui/resources/top_bar_resources.qrc index a8ff7789..06dc4e50 100644 --- a/BlocksScreen/lib/ui/resources/top_bar_resources.qrc +++ b/BlocksScreen/lib/ui/resources/top_bar_resources.qrc @@ -6,7 +6,6 @@ media/topbar/custom_filament_topbar.svg media/topbar/high_temp_printcore.svg media/topbar/hips_filament_topbar.svg - media/topbar/internet_cable.svg media/topbar/not_avaible_printcore.svg media/topbar/not_available_filament_topbar.svg media/topbar/nozzle_temp_topbar.svg @@ -15,11 +14,6 @@ media/topbar/nylon_filament_topbar.svg media/topbar/petg_filament_topbar.svg media/topbar/pla_filament_topbar.svg - media/topbar/signal_good_signal.svg - media/topbar/signal_no_signal.svg - media/topbar/signal_very_good_signal.svg - media/topbar/signal_veryweak_signal.svg - media/topbar/signal_weak_signal.svg media/topbar/standard_temp_printcore.svg media/topbar/tpu_filament_topbar.svg diff --git a/BlocksScreen/lib/ui/resources/top_bar_resources_rc.py b/BlocksScreen/lib/ui/resources/top_bar_resources_rc.py index 24e73622..47047530 100644 --- a/BlocksScreen/lib/ui/resources/top_bar_resources_rc.py +++ b/BlocksScreen/lib/ui/resources/top_bar_resources_rc.py @@ -1,2712 +1,2254 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created by: The Resource Compiler for PyQt6 (Qt v5.15.14) -# +# Resource object code (Python 3) +# Created by: object code +# Created by: The Resource Compiler for Qt version 6.8.2 # WARNING! All changes made in this file will be lost! from PyQt6 import QtCore qt_resource_data = b"\ -\x00\x00\x01\x47\ -\x3c\ -\x73\x76\x67\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\x34\x2e\x36\x39\x20\x35\x2e\x30\x34\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\x66\x66\x66\x3b\x7d\x3c\x2f\ -\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x67\x20\ -\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\x61\x74\ -\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x32\x22\ -\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x2d\ -\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\ -\x79\x65\x72\x20\x31\x22\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\x32\ -\x2e\x33\x35\x2c\x35\x41\x32\x2e\x34\x32\x2c\x32\x2e\x34\x32\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x30\x2c\x32\x2e\x35\x31\x2c\x32\x2e\x34\ -\x31\x2c\x32\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2e\x33\ -\x33\x2c\x30\x2c\x32\x2e\x33\x39\x2c\x32\x2e\x33\x39\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x34\x2e\x36\x39\x2c\x32\x2e\x35\x31\x2c\x32\x2e\ -\x34\x31\x2c\x32\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2e\ -\x33\x35\x2c\x35\x5a\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\ -\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x08\x73\ -\x3c\ -\x73\x76\x67\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\x38\x38\x2e\x33\x32\x20\x33\x34\x2e\x32\x34\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\x66\x30\x35\x61\x32\ -\x38\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\ -\x66\x36\x39\x32\x31\x65\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x66\ -\x69\x6c\x6c\x3a\x23\x65\x63\x31\x63\x32\x34\x3b\x7d\x3c\x2f\x73\ -\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x67\x20\x69\ -\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\x61\x74\x61\ -\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x32\x22\x3e\ -\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x2d\x32\ -\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\ -\x65\x72\x20\x31\x22\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\x37\x38\ -\x2e\x31\x32\x2c\x32\x30\x2e\x35\x36\x68\x2d\x37\x2e\x38\x61\x32\ -\x2e\x33\x38\x2c\x32\x2e\x33\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x32\ -\x2e\x33\x39\x2d\x32\x2e\x33\x38\x71\x30\x2d\x37\x2e\x38\x2c\x30\ -\x2d\x31\x35\x2e\x35\x38\x41\x32\x2e\x34\x32\x2c\x32\x2e\x34\x32\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x30\x2e\x33\x35\x2e\x31\x38\x48\ -\x38\x35\x2e\x37\x39\x41\x32\x2e\x35\x32\x2c\x32\x2e\x35\x32\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x38\x38\x2e\x33\x32\x2c\x32\x2e\x37\x71\ -\x30\x2c\x37\x2e\x36\x38\x2c\x30\x2c\x31\x35\x2e\x33\x36\x61\x32\ -\x2e\x35\x2c\x32\x2e\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x35\ -\x31\x2c\x32\x2e\x35\x5a\x6d\x38\x2e\x35\x37\x2d\x31\x30\x2e\x31\ -\x37\x41\x38\x2e\x36\x34\x2c\x38\x2e\x36\x34\x2c\x30\x2c\x31\x2c\ -\x30\x2c\x37\x37\x2e\x39\x2c\x31\x39\x2c\x38\x2e\x36\x36\x2c\x38\ -\x2e\x36\x36\x2c\x30\x2c\x30\x2c\x30\x2c\x38\x36\x2e\x36\x39\x2c\ -\x31\x30\x2e\x33\x39\x5a\x6d\x30\x2c\x37\x2e\x37\x32\x61\x2e\x39\ -\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x38\x32\x2c\ -\x30\x2c\x2e\x39\x32\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\ -\x39\x32\x2e\x39\x32\x41\x2e\x39\x2e\x39\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x38\x36\x2e\x36\x39\x2c\x31\x38\x2e\x31\x31\x5a\x4d\x36\x39\ -\x2e\x34\x31\x2c\x32\x2e\x36\x32\x61\x2e\x38\x38\x2e\x38\x38\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x38\x2e\x39\x32\x2e\x39\x31\x2e\ -\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x39\x33\x2d\x2e\x39\x31\ -\x2e\x39\x32\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2d\ -\x2e\x39\x31\x41\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x36\x39\x2e\x34\x31\x2c\x32\x2e\x36\x32\x5a\x6d\x31\x37\x2e\ -\x32\x38\x2c\x30\x61\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\ -\x30\x2d\x2e\x39\x2d\x2e\x39\x31\x2e\x39\x31\x2e\x39\x31\x2c\x30\ -\x2c\x31\x2c\x30\x2c\x30\x2c\x31\x2e\x38\x32\x41\x2e\x39\x2e\x39\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x38\x36\x2e\x36\x39\x2c\x32\x2e\x36\ -\x33\x5a\x4d\x37\x30\x2e\x33\x32\x2c\x31\x37\x2e\x32\x61\x2e\x39\ -\x31\x2e\x39\x31\x2c\x30\x2c\x31\x2c\x30\x2c\x30\x2c\x31\x2e\x38\ -\x31\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2d\ -\x31\x2e\x38\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ -\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\ -\x38\x34\x2e\x37\x37\x2c\x39\x2e\x38\x38\x61\x35\x2e\x31\x36\x2c\ -\x35\x2e\x31\x36\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\x35\x33\x2d\ -\x2e\x37\x38\x6c\x2d\x2e\x33\x33\x2d\x2e\x31\x36\x63\x2d\x31\x2e\ -\x36\x36\x2d\x31\x2e\x36\x36\x2c\x33\x2e\x32\x35\x2d\x33\x2e\x36\ -\x2c\x34\x2e\x36\x38\x2d\x33\x2e\x31\x31\x61\x36\x2e\x39\x32\x2c\ -\x36\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x34\x2e\x33\x37\x2d\ -\x32\x2e\x35\x35\x43\x37\x36\x2e\x38\x31\x2c\x33\x2e\x31\x37\x2c\ -\x37\x36\x2c\x36\x2e\x37\x37\x2c\x37\x36\x2e\x37\x33\x2c\x38\x2e\ -\x36\x35\x63\x2e\x32\x35\x2e\x36\x39\x2e\x32\x35\x2e\x36\x37\x2d\ -\x2e\x32\x35\x2c\x31\x2e\x32\x61\x2e\x33\x38\x2e\x33\x38\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x35\x37\x2e\x30\x35\x63\x2d\x31\x2e\x38\ -\x33\x2d\x31\x2e\x30\x37\x2d\x32\x2e\x33\x33\x2d\x33\x2e\x31\x31\ -\x2d\x32\x2e\x32\x32\x2d\x35\x2e\x31\x32\x2d\x31\x2e\x35\x32\x2c\ -\x31\x2e\x32\x32\x2d\x33\x2e\x37\x38\x2c\x34\x2e\x34\x39\x2d\x32\ -\x2c\x36\x2e\x32\x61\x34\x2e\x38\x37\x2c\x34\x2e\x38\x37\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x34\x2e\x38\x33\x2e\x38\x32\x63\x2e\x36\x35\ -\x2d\x2e\x32\x36\x2e\x36\x34\x2d\x2e\x32\x36\x2c\x31\x2e\x31\x33\ -\x2e\x32\x32\x2e\x34\x34\x2c\x32\x2d\x33\x2e\x32\x35\x2c\x33\x2e\ -\x31\x36\x2d\x34\x2e\x39\x34\x2c\x32\x2e\x38\x39\x2c\x31\x2e\x32\ -\x2c\x31\x2e\x34\x37\x2c\x34\x2e\x33\x31\x2c\x33\x2e\x36\x35\x2c\ -\x36\x2c\x31\x2e\x39\x32\x61\x34\x2e\x36\x32\x2c\x34\x2e\x36\x32\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x39\x32\x2d\x34\x2e\x36\x36\x63\ -\x2d\x2e\x34\x2d\x33\x2e\x38\x36\x2c\x33\x2e\x38\x39\x2c\x31\x2c\ -\x33\x2c\x33\x2e\x37\x33\x43\x38\x34\x2e\x30\x36\x2c\x31\x35\x2c\ -\x38\x36\x2e\x31\x38\x2c\x31\x31\x2e\x35\x31\x2c\x38\x34\x2e\x37\ -\x37\x2c\x39\x2e\x38\x38\x5a\x22\x2f\x3e\x3c\x72\x65\x63\x74\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x78\x3d\ -\x22\x36\x39\x2e\x34\x39\x22\x20\x79\x3d\x22\x32\x31\x2e\x35\x34\ -\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x31\x37\x2e\x32\x37\x22\x20\ -\x68\x65\x69\x67\x68\x74\x3d\x22\x33\x2e\x32\x38\x22\x20\x72\x78\ -\x3d\x22\x30\x2e\x33\x38\x22\x2f\x3e\x3c\x72\x65\x63\x74\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x78\x3d\x22\ -\x37\x31\x2e\x30\x32\x22\x20\x79\x3d\x22\x32\x33\x2e\x36\x32\x22\ -\x20\x77\x69\x64\x74\x68\x3d\x22\x31\x34\x2e\x32\x32\x22\x20\x68\ -\x65\x69\x67\x68\x74\x3d\x22\x33\x2e\x34\x32\x22\x20\x72\x78\x3d\ -\x22\x30\x2e\x33\x38\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\ -\x37\x35\x2e\x30\x37\x2c\x32\x39\x2e\x34\x37\x56\x32\x38\x2e\x32\ -\x39\x63\x30\x2d\x2e\x31\x35\x2c\x30\x2d\x2e\x32\x35\x2e\x32\x33\ -\x2d\x2e\x32\x32\x68\x32\x2e\x32\x32\x63\x2e\x31\x39\x2c\x30\x2c\ -\x2e\x32\x36\x2e\x30\x37\x2e\x32\x36\x2e\x32\x35\x76\x32\x2e\x33\ -\x61\x2e\x33\x35\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x32\ -\x2e\x33\x31\x2c\x32\x2e\x36\x31\x2c\x32\x2e\x36\x31\x2c\x30\x2c\ -\x30\x2c\x31\x2d\x32\x2e\x33\x31\x2c\x30\x2c\x2e\x33\x32\x2e\x33\ -\x32\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x32\x2d\x2e\x33\x34\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\x37\x35\x2e\x35\x2c\x33\ -\x31\x2e\x38\x37\x41\x33\x2e\x38\x31\x2c\x33\x2e\x38\x31\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x37\x38\x2c\x33\x31\x2e\x35\x36\x61\x2e\x34\ -\x38\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x32\x39\x2c\x30\ -\x2c\x33\x2e\x37\x38\x2c\x33\x2e\x37\x38\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x32\x2e\x34\x35\x2e\x33\x34\x63\x2d\x2e\x32\x2e\x32\x31\x2d\ -\x2e\x34\x33\x2e\x34\x32\x2d\x2e\x36\x32\x2e\x36\x2d\x2e\x34\x39\ -\x2e\x34\x36\x2d\x31\x2c\x2e\x39\x32\x2d\x31\x2e\x34\x33\x2c\x31\ -\x2e\x33\x37\x61\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x31\ -\x2d\x31\x2c\x2e\x30\x39\x43\x37\x36\x2e\x39\x33\x2c\x33\x33\x2e\ -\x32\x36\x2c\x37\x36\x2e\x32\x34\x2c\x33\x32\x2e\x35\x37\x2c\x37\ -\x35\x2e\x35\x2c\x33\x31\x2e\x38\x37\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\x37\x38\x2e\x34\x38\x2c\x32\x39\x2e\x34\x37\ -\x56\x32\x38\x2e\x32\x39\x63\x30\x2d\x2e\x31\x35\x2c\x30\x2d\x2e\ -\x32\x35\x2e\x32\x33\x2d\x2e\x32\x32\x68\x32\x2e\x32\x31\x63\x2e\ -\x32\x2c\x30\x2c\x2e\x32\x36\x2e\x30\x37\x2e\x32\x36\x2e\x32\x35\ -\x76\x32\x2e\x33\x61\x2e\x33\x34\x2e\x33\x34\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x2e\x31\x39\x2e\x33\x31\x2c\x32\x2e\x36\x33\x2c\x32\x2e\ -\x36\x33\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x33\x32\x2c\x30\x2c\ -\x2e\x33\x31\x2e\x33\x31\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x31\x39\ -\x2d\x2e\x33\x34\x5a\x22\x2f\x3e\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x70\ -\x6f\x69\x6e\x74\x73\x3d\x22\x32\x32\x2e\x31\x32\x20\x31\x34\x2e\ -\x30\x38\x20\x34\x2e\x33\x35\x20\x31\x34\x2e\x30\x38\x20\x34\x2e\ -\x33\x35\x20\x30\x20\x30\x20\x30\x20\x30\x20\x33\x34\x2e\x32\x34\ -\x20\x34\x2e\x33\x35\x20\x33\x34\x2e\x32\x34\x20\x34\x2e\x33\x35\ -\x20\x31\x38\x2e\x34\x32\x20\x32\x32\x2e\x31\x32\x20\x31\x38\x2e\ -\x34\x32\x20\x32\x32\x2e\x31\x32\x20\x33\x34\x2e\x32\x34\x20\x32\ -\x36\x2e\x34\x36\x20\x33\x34\x2e\x32\x34\x20\x32\x36\x2e\x34\x36\ -\x20\x30\x20\x32\x32\x2e\x31\x32\x20\x30\x20\x32\x32\x2e\x31\x32\ -\x20\x31\x34\x2e\x30\x38\x22\x2f\x3e\x3c\x70\x6f\x6c\x79\x67\x6f\ -\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\ -\x70\x6f\x69\x6e\x74\x73\x3d\x22\x33\x30\x2e\x33\x33\x20\x30\x20\ -\x33\x30\x2e\x33\x33\x20\x34\x2e\x33\x35\x20\x34\x32\x2e\x33\x39\ -\x20\x34\x2e\x33\x35\x20\x34\x32\x2e\x33\x39\x20\x33\x34\x2e\x32\ -\x34\x20\x34\x36\x2e\x37\x34\x20\x33\x34\x2e\x32\x34\x20\x34\x36\ -\x2e\x37\x34\x20\x34\x2e\x33\x35\x20\x35\x38\x2e\x38\x20\x34\x2e\ -\x33\x35\x20\x35\x38\x2e\x38\x20\x30\x20\x33\x30\x2e\x33\x33\x20\ -\x30\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\ -\x67\x3e\ -\x00\x00\x09\xfe\ -\x3c\ -\x73\x76\x67\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\x38\x32\x2e\x32\x33\x20\x33\x36\x2e\x31\x37\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\x38\x62\x63\x35\x33\ -\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\ -\x30\x30\x39\x31\x34\x37\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x66\ -\x69\x6c\x6c\x3a\x23\x30\x30\x61\x35\x35\x31\x3b\x7d\x3c\x2f\x73\ -\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x67\x20\x69\ -\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\x61\x74\x61\ -\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x32\x22\x3e\ -\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x2d\x32\ -\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\ -\x65\x72\x20\x31\x22\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\x37\x32\ -\x2c\x32\x30\x2e\x35\x38\x68\x2d\x37\x2e\x38\x61\x32\x2e\x33\x39\ -\x2c\x32\x2e\x33\x39\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x33\x39\ -\x2d\x32\x2e\x33\x39\x56\x32\x2e\x36\x32\x41\x32\x2e\x34\x32\x2c\ -\x32\x2e\x34\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x36\x34\x2e\x32\x37\ -\x2e\x32\x48\x37\x39\x2e\x37\x31\x61\x32\x2e\x35\x32\x2c\x32\x2e\ -\x35\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2e\x35\x32\x2c\x32\x2e\ -\x35\x32\x56\x31\x38\x2e\x30\x37\x61\x32\x2e\x35\x31\x2c\x32\x2e\ -\x35\x31\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x35\x31\x2c\x32\x2e\ -\x35\x31\x5a\x4d\x38\x30\x2e\x36\x2c\x31\x30\x2e\x34\x41\x38\x2e\ -\x36\x34\x2c\x38\x2e\x36\x34\x2c\x30\x2c\x31\x2c\x30\x2c\x37\x31\ -\x2e\x38\x32\x2c\x31\x39\x2c\x38\x2e\x36\x34\x2c\x38\x2e\x36\x34\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x38\x30\x2e\x36\x2c\x31\x30\x2e\x34\ -\x5a\x6d\x30\x2c\x37\x2e\x37\x32\x61\x2e\x38\x38\x2e\x38\x38\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2d\x2e\x39\x2e\x39\x31\x2e\x39\ -\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x32\x2e\x38\x39\x2e\x39\ -\x33\x2e\x39\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x39\x33\x2e\x39\ -\x32\x41\x2e\x39\x2e\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x38\x30\x2e\ -\x36\x2c\x31\x38\x2e\x31\x32\x5a\x4d\x36\x33\x2e\x33\x33\x2c\x32\ -\x2e\x36\x34\x61\x2e\x38\x38\x2e\x38\x38\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x2e\x38\x38\x2e\x39\x31\x2e\x38\x39\x2e\x38\x39\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x2e\x39\x32\x2d\x2e\x39\x2e\x39\x31\x2e\x39\x31\ -\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2d\x2e\x39\x31\x41\x2e\x38\ -\x38\x2e\x38\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x36\x33\x2e\x33\x33\ -\x2c\x32\x2e\x36\x34\x5a\x6d\x31\x37\x2e\x32\x37\x2c\x30\x61\x2e\ -\x39\x2e\x39\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2d\x2e\x39\x31\ -\x2e\x39\x33\x2e\x39\x33\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x32\ -\x2e\x39\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\ -\x39\x33\x2e\x39\x31\x41\x2e\x38\x39\x2e\x38\x39\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x38\x30\x2e\x36\x2c\x32\x2e\x36\x35\x5a\x4d\x36\x34\ -\x2e\x32\x33\x2c\x31\x37\x2e\x32\x32\x61\x2e\x38\x38\x2e\x38\x38\ -\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2e\x38\x39\x2e\x38\x39\x2e\ -\x38\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x39\x31\x2e\x39\x32\x2e\ -\x39\x2e\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x39\x2d\x2e\x39\ -\x31\x41\x2e\x38\x39\x2e\x38\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x36\ -\x34\x2e\x32\x33\x2c\x31\x37\x2e\x32\x32\x5a\x22\x2f\x3e\x3c\x70\ -\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\ -\x22\x20\x64\x3d\x22\x4d\x37\x38\x2e\x36\x38\x2c\x39\x2e\x39\x61\ -\x35\x2e\x31\x34\x2c\x35\x2e\x31\x34\x2c\x30\x2c\x30\x2c\x30\x2d\ -\x35\x2e\x35\x32\x2d\x2e\x37\x39\x4c\x37\x32\x2e\x38\x32\x2c\x39\ -\x63\x2d\x31\x2e\x36\x35\x2d\x31\x2e\x36\x37\x2c\x33\x2e\x32\x36\ -\x2d\x33\x2e\x36\x2c\x34\x2e\x36\x39\x2d\x33\x2e\x31\x31\x41\x36\ -\x2e\x39\x2c\x36\x2e\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x37\x33\x2e\ -\x31\x34\x2c\x33\x2e\x33\x63\x2d\x32\x2e\x34\x31\x2d\x2e\x31\x31\ -\x2d\x33\x2e\x32\x32\x2c\x33\x2e\x34\x38\x2d\x32\x2e\x35\x2c\x35\ -\x2e\x33\x37\x2e\x32\x35\x2e\x36\x39\x2e\x32\x36\x2e\x36\x37\x2d\ -\x2e\x32\x34\x2c\x31\x2e\x31\x39\x61\x2e\x33\x38\x2e\x33\x38\x2c\ -\x30\x2c\x30\x2c\x31\x2d\x2e\x35\x37\x2e\x30\x36\x43\x36\x38\x2c\ -\x38\x2e\x38\x34\x2c\x36\x37\x2e\x35\x2c\x36\x2e\x38\x31\x2c\x36\ -\x37\x2e\x36\x2c\x34\x2e\x38\x63\x2d\x31\x2e\x35\x31\x2c\x31\x2e\ -\x32\x31\x2d\x33\x2e\x37\x38\x2c\x34\x2e\x34\x38\x2d\x32\x2c\x36\ -\x2e\x32\x61\x34\x2e\x38\x37\x2c\x34\x2e\x38\x37\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x34\x2e\x38\x33\x2e\x38\x31\x63\x2e\x36\x35\x2d\x2e\ -\x32\x36\x2e\x36\x33\x2d\x2e\x32\x36\x2c\x31\x2e\x31\x32\x2e\x32\ -\x33\x2e\x34\x35\x2c\x32\x2d\x33\x2e\x32\x35\x2c\x33\x2e\x31\x35\ -\x2d\x34\x2e\x39\x34\x2c\x32\x2e\x38\x39\x2c\x31\x2e\x32\x2c\x31\ -\x2e\x34\x36\x2c\x34\x2e\x33\x31\x2c\x33\x2e\x36\x34\x2c\x36\x2c\ -\x31\x2e\x39\x31\x61\x34\x2e\x36\x34\x2c\x34\x2e\x36\x34\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x2e\x39\x32\x2d\x34\x2e\x36\x36\x63\x2d\x2e\ -\x34\x2d\x33\x2e\x38\x36\x2c\x33\x2e\x38\x39\x2c\x31\x2c\x33\x2c\ -\x33\x2e\x37\x33\x43\x37\x38\x2c\x31\x35\x2c\x38\x30\x2e\x31\x2c\ -\x31\x31\x2e\x35\x33\x2c\x37\x38\x2e\x36\x38\x2c\x39\x2e\x39\x5a\ -\x22\x2f\x3e\x3c\x72\x65\x63\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x32\x22\x20\x78\x3d\x22\x36\x33\x2e\x34\x22\x20\ -\x79\x3d\x22\x32\x31\x2e\x35\x36\x22\x20\x77\x69\x64\x74\x68\x3d\ -\x22\x31\x37\x2e\x32\x37\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\ -\x33\x2e\x32\x38\x22\x20\x72\x78\x3d\x22\x30\x2e\x33\x38\x22\x2f\ -\x3e\x3c\x72\x65\x63\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x32\x22\x20\x78\x3d\x22\x36\x34\x2e\x39\x33\x22\x20\x79\ -\x3d\x22\x32\x33\x2e\x36\x33\x22\x20\x77\x69\x64\x74\x68\x3d\x22\ -\x31\x34\x2e\x32\x32\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x33\ -\x2e\x34\x32\x22\x20\x72\x78\x3d\x22\x30\x2e\x33\x38\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\x36\x39\x2c\x32\x39\x2e\x34\x39\ -\x56\x32\x38\x2e\x33\x63\x30\x2d\x2e\x31\x35\x2c\x30\x2d\x2e\x32\ -\x34\x2e\x32\x33\x2d\x2e\x32\x31\x68\x32\x2e\x32\x31\x63\x2e\x32\ -\x2c\x30\x2c\x2e\x32\x36\x2e\x30\x36\x2e\x32\x36\x2e\x32\x34\x76\ -\x32\x2e\x33\x31\x61\x2e\x33\x32\x2e\x33\x32\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x2e\x31\x39\x2e\x33\x2c\x32\x2e\x35\x36\x2c\x32\x2e\x35\ -\x36\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x33\x32\x2c\x30\x2c\x2e\ -\x32\x39\x2e\x32\x39\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x31\x39\x2d\ -\x2e\x33\x33\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\x36\ -\x39\x2e\x34\x31\x2c\x33\x31\x2e\x38\x38\x61\x33\x2e\x38\x33\x2c\ -\x33\x2e\x38\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x38\x2d\ -\x2e\x33\x2e\x34\x38\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\ -\x32\x39\x2c\x30\x2c\x33\x2e\x36\x39\x2c\x33\x2e\x36\x39\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x34\x2e\x33\x33\x2c\x37\x2e\x31\ -\x2c\x37\x2e\x31\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x36\x32\x2e\x36\ -\x31\x63\x2d\x2e\x34\x38\x2e\x34\x36\x2d\x31\x2c\x2e\x39\x31\x2d\ -\x31\x2e\x34\x33\x2c\x31\x2e\x33\x37\x61\x2e\x38\x39\x2e\x38\x39\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x39\x34\x2e\x30\x39\x43\x37\x30\ -\x2e\x38\x34\x2c\x33\x33\x2e\x32\x38\x2c\x37\x30\x2e\x31\x36\x2c\ -\x33\x32\x2e\x35\x38\x2c\x36\x39\x2e\x34\x31\x2c\x33\x31\x2e\x38\ -\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\x37\x32\x2e\ -\x33\x39\x2c\x32\x39\x2e\x34\x39\x56\x32\x38\x2e\x33\x63\x30\x2d\ -\x2e\x31\x35\x2c\x30\x2d\x2e\x32\x34\x2e\x32\x33\x2d\x2e\x32\x31\ -\x68\x32\x2e\x32\x32\x63\x2e\x31\x39\x2c\x30\x2c\x2e\x32\x36\x2e\ -\x30\x36\x2e\x32\x36\x2e\x32\x34\x76\x32\x2e\x33\x31\x61\x2e\x33\ -\x33\x2e\x33\x33\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x32\x2e\x33\x2c\ -\x32\x2e\x35\x34\x2c\x32\x2e\x35\x34\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x32\x2e\x33\x31\x2c\x30\x2c\x2e\x33\x2e\x33\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x2e\x32\x2d\x2e\x33\x33\x5a\x22\x2f\x3e\x3c\x70\x61\x74\ -\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\ -\x64\x3d\x22\x4d\x31\x39\x2e\x33\x38\x2c\x31\x38\x2e\x34\x38\x61\ -\x31\x37\x2e\x38\x39\x2c\x31\x37\x2e\x38\x39\x2c\x30\x2c\x30\x2c\ -\x30\x2d\x36\x2e\x37\x39\x2d\x33\x2e\x33\x33\x6c\x2d\x2e\x34\x2d\ -\x2e\x31\x32\x63\x2d\x33\x2d\x2e\x39\x2d\x36\x2e\x37\x38\x2d\x32\ -\x2d\x36\x2e\x37\x38\x2d\x35\x2e\x37\x39\x2c\x30\x2d\x33\x2e\x31\ -\x33\x2c\x33\x2d\x34\x2e\x37\x36\x2c\x36\x2d\x34\x2e\x37\x36\x61\ -\x31\x33\x2e\x34\x35\x2c\x31\x33\x2e\x34\x35\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x36\x2e\x38\x39\x2c\x32\x2e\x30\x39\x6c\x2e\x35\x32\x2e\ -\x32\x39\x2c\x32\x2e\x32\x36\x2d\x33\x2e\x37\x34\x2d\x2e\x35\x34\ -\x2d\x2e\x33\x32\x41\x31\x36\x2e\x37\x35\x2c\x31\x36\x2e\x37\x35\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x31\x2e\x34\x35\x2c\x30\x2c\x31\ -\x31\x2e\x36\x31\x2c\x31\x31\x2e\x36\x31\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x33\x2e\x39\x31\x2c\x32\x2e\x36\x34\x2c\x38\x2e\x36\x32\x2c\ -\x38\x2e\x36\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x37\x34\x2c\x39\ -\x2e\x32\x34\x63\x30\x2c\x34\x2e\x38\x35\x2c\x33\x2e\x33\x32\x2c\ -\x38\x2e\x32\x39\x2c\x39\x2e\x35\x39\x2c\x31\x30\x61\x31\x37\x2c\ -\x31\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x2e\x33\x32\x2c\x32\x2e\ -\x32\x33\x2c\x35\x2e\x31\x33\x2c\x35\x2e\x31\x33\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x32\x2e\x35\x35\x2c\x34\x2e\x31\x33\x63\x30\x2c\x33\ -\x2e\x33\x39\x2d\x33\x2c\x35\x2e\x39\x35\x2d\x37\x2e\x31\x2c\x35\ -\x2e\x39\x35\x68\x30\x61\x31\x36\x2e\x31\x31\x2c\x31\x36\x2e\x31\ -\x31\x2c\x30\x2c\x30\x2c\x31\x2d\x38\x2e\x32\x32\x2d\x32\x2e\x36\ -\x31\x6c\x2d\x2e\x35\x34\x2d\x2e\x33\x35\x4c\x30\x2c\x33\x32\x2e\ -\x34\x38\x6c\x2e\x34\x39\x2e\x33\x32\x41\x31\x39\x2e\x35\x37\x2c\ -\x31\x39\x2e\x35\x37\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x31\x2e\x32\ -\x2c\x33\x36\x2e\x31\x37\x61\x31\x31\x2e\x37\x37\x2c\x31\x31\x2e\ -\x37\x37\x2c\x30\x2c\x30\x2c\x30\x2c\x38\x2e\x36\x32\x2d\x33\x2e\ -\x33\x32\x2c\x31\x30\x2e\x34\x31\x2c\x31\x30\x2e\x34\x31\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x33\x2e\x30\x35\x2d\x37\x2e\x33\x31\x76\x30\ -\x41\x39\x2e\x34\x37\x2c\x39\x2e\x34\x37\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x31\x39\x2e\x33\x38\x2c\x31\x38\x2e\x34\x38\x5a\x22\x2f\x3e\ -\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x33\x22\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\x32\ -\x34\x2e\x32\x20\x30\x2e\x38\x31\x20\x32\x34\x2e\x32\x20\x35\x2e\ -\x32\x20\x33\x36\x2e\x33\x37\x20\x35\x2e\x32\x20\x33\x36\x2e\x33\ -\x37\x20\x33\x35\x2e\x33\x36\x20\x34\x30\x2e\x37\x35\x20\x33\x35\ -\x2e\x33\x36\x20\x34\x30\x2e\x37\x35\x20\x35\x2e\x32\x20\x35\x32\ -\x2e\x39\x32\x20\x35\x2e\x32\x20\x35\x32\x2e\x39\x32\x20\x30\x2e\ -\x38\x31\x20\x32\x34\x2e\x32\x20\x30\x2e\x38\x31\x22\x2f\x3e\x3c\ -\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x05\x0e\ -\x3c\ -\x73\x76\x67\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\x33\x32\x2e\x39\x32\x20\x32\x38\x2e\x39\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\x64\x30\x64\x32\x64\x33\ -\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x39\ -\x32\x39\x34\x39\x37\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\ -\x2f\x64\x65\x66\x73\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\ -\x65\x72\x5f\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\ -\x22\x4c\x61\x79\x65\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\ -\x22\x4c\x61\x79\x65\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\ -\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\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\x31\x35\x2e\x38\x37\x2c\x32\x41\ -\x32\x32\x2e\x31\x37\x2c\x32\x32\x2e\x31\x37\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x33\x32\x2e\x33\x2c\x39\x61\x32\x2c\x32\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x2e\x31\x39\x2c\x32\x2e\x36\x35\x2c\x31\x2e\x36\x35\ -\x2c\x31\x2e\x36\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x34\x39\ -\x2c\x30\x2c\x33\x32\x2e\x34\x32\x2c\x33\x32\x2e\x34\x32\x2c\x30\ -\x2c\x30\x2c\x30\x2d\x32\x2e\x37\x39\x2d\x32\x2e\x34\x37\x41\x31\ -\x37\x2e\x33\x35\x2c\x31\x37\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x31\x39\x2e\x35\x34\x2c\x36\x2c\x31\x38\x2c\x31\x38\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x31\x30\x2c\x36\x2e\x38\x36\x61\x31\x38\x2e\ -\x38\x38\x2c\x31\x38\x2e\x38\x38\x2c\x30\x2c\x30\x2c\x30\x2d\x37\ -\x2e\x30\x39\x2c\x34\x2e\x38\x2c\x31\x2e\x35\x37\x2c\x31\x2e\x35\ -\x37\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x38\x31\x2e\x34\x38\x2c\ -\x31\x2e\x38\x36\x2c\x31\x2e\x38\x36\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x2e\x35\x39\x2d\x33\x41\x32\x32\x2e\x35\x2c\x32\x32\x2e\x35\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x34\x2c\x36\x2e\x30\x38\x61\x32\x31\x2c\ -\x32\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x38\x2e\x34\x35\x2d\x33\x2e\ -\x36\x33\x43\x31\x33\x2e\x37\x36\x2c\x32\x2e\x32\x31\x2c\x31\x35\ -\x2e\x31\x2c\x32\x2e\x31\x33\x2c\x31\x35\x2e\x38\x37\x2c\x32\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\x32\x36\x2e\x36\x35\ -\x2c\x31\x37\x2e\x33\x35\x61\x31\x2e\x37\x2c\x31\x2e\x37\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x31\x2e\x34\x31\x2d\x2e\x35\x35\x2c\x31\x31\ -\x2e\x38\x37\x2c\x31\x31\x2e\x38\x37\x2c\x30\x2c\x30\x2c\x30\x2d\ -\x31\x37\x2e\x35\x2d\x2e\x30\x36\x2c\x31\x2e\x36\x35\x2c\x31\x2e\ -\x36\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x38\x38\x2d\x2e\x36\ -\x2c\x31\x2e\x38\x2c\x31\x2e\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\ -\x34\x32\x2d\x31\x2e\x38\x39\x2c\x31\x35\x2e\x31\x33\x2c\x31\x35\ -\x2e\x31\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x38\x2e\x38\x31\x2d\x34\ -\x2e\x37\x36\x41\x31\x34\x2e\x38\x34\x2c\x31\x34\x2e\x38\x34\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x32\x36\x2e\x33\x32\x2c\x31\x33\x61\x31\ -\x32\x2e\x39\x34\x2c\x31\x32\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x31\x2e\x34\x2c\x31\x2e\x33\x37\x41\x31\x2e\x37\x36\x2c\x31\ -\x2e\x37\x36\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x38\x2c\x31\x36\x2e\ -\x33\x2c\x31\x2e\x35\x37\x2c\x31\x2e\x35\x37\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x32\x36\x2e\x36\x35\x2c\x31\x37\x2e\x33\x35\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\x39\x2e\x35\x33\x2c\x32\x30\ -\x2e\x36\x36\x61\x31\x2e\x39\x2c\x31\x2e\x39\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x2e\x35\x37\x2d\x31\x2e\x33\x37\x2c\x38\x2e\x37\x2c\x38\ -\x2e\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x2e\x30\x38\x2d\x32\x2e\ -\x36\x36\x2c\x38\x2e\x35\x38\x2c\x38\x2e\x35\x38\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x37\x2e\x36\x2c\x32\x2e\x36\x34\x2c\x31\x2e\x39\x31\ -\x2c\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x35\x2c\ -\x32\x2e\x31\x36\x2c\x31\x2e\x36\x33\x2c\x31\x2e\x36\x33\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x32\x2e\x37\x32\x2e\x35\x32\x2c\x35\x2e\x34\ -\x37\x2c\x35\x2e\x34\x37\x2c\x30\x2c\x30\x2c\x30\x2d\x32\x2e\x38\ -\x33\x2d\x31\x2e\x36\x35\x2c\x35\x2e\x33\x33\x2c\x35\x2e\x33\x33\ -\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\x32\x32\x2c\x31\x2e\x36\x2c\ -\x31\x2e\x36\x35\x2c\x31\x2e\x36\x35\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x32\x2e\x38\x37\x2d\x2e\x37\x39\x41\x33\x2e\x34\x37\x2c\x33\x2e\ -\x34\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x39\x2e\x35\x33\x2c\x32\x30\ -\x2e\x36\x36\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\x31\ -\x36\x2e\x34\x36\x2c\x32\x38\x2e\x38\x61\x32\x2e\x35\x32\x2c\x32\ -\x2e\x35\x32\x2c\x30\x2c\x31\x2c\x31\x2c\x32\x2e\x33\x35\x2d\x32\ -\x2e\x35\x33\x41\x32\x2e\x34\x32\x2c\x32\x2e\x34\x32\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x31\x36\x2e\x34\x36\x2c\x32\x38\x2e\x38\x5a\x22\ -\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x37\x2e\x38\x2c\x30\x6c\ -\x38\x2e\x36\x36\x2c\x31\x32\x4c\x32\x35\x2e\x31\x32\x2c\x30\x68\ -\x33\x2e\x35\x31\x4c\x31\x38\x2e\x32\x31\x2c\x31\x34\x2e\x34\x35\ -\x2c\x32\x38\x2e\x36\x33\x2c\x32\x38\x2e\x39\x48\x32\x35\x2e\x31\ -\x32\x6c\x2d\x38\x2e\x36\x36\x2d\x31\x32\x4c\x37\x2e\x38\x2c\x32\ -\x38\x2e\x39\x48\x34\x2e\x32\x39\x4c\x31\x34\x2e\x37\x31\x2c\x31\ -\x34\x2e\x34\x35\x2c\x34\x2e\x32\x39\x2c\x30\x5a\x22\x2f\x3e\x3c\ -\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x07\xa4\ -\x3c\ -\x73\x76\x67\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\x34\x30\x2e\x38\x36\x20\x34\x30\x2e\x38\x36\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\x66\x66\x66\x3b\x7d\ -\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\ -\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\ -\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\ -\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\ -\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\ -\x4c\x61\x79\x65\x72\x20\x31\x22\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\x37\x2e\x36\x36\x2c\x30\x48\x33\x2e\x32\x41\x33\x2e\x32\ -\x2c\x33\x2e\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\x33\x2e\x32\ -\x56\x33\x37\x2e\x36\x36\x61\x33\x2e\x32\x2c\x33\x2e\x32\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x33\x2e\x32\x2c\x33\x2e\x32\x48\x33\x37\x2e\ -\x36\x36\x61\x33\x2e\x32\x31\x2c\x33\x2e\x32\x31\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x33\x2e\x32\x2d\x33\x2e\x32\x56\x33\x2e\x32\x41\x33\ -\x2e\x32\x2c\x33\x2e\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x33\x37\x2e\ -\x36\x36\x2c\x30\x5a\x6d\x2d\x2e\x33\x33\x2c\x34\x2e\x36\x33\x76\ -\x33\x30\x2e\x31\x61\x2e\x38\x31\x2e\x38\x31\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x2e\x38\x31\x2e\x38\x31\x48\x34\x2e\x33\x33\x61\x2e\x38\ -\x31\x2e\x38\x31\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x38\x31\x2d\x2e\ -\x38\x31\x56\x34\x2e\x36\x33\x61\x2e\x38\x31\x2e\x38\x31\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x2e\x38\x31\x2d\x2e\x38\x31\x48\x33\x36\x2e\ -\x35\x32\x41\x2e\x38\x31\x2e\x38\x31\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x33\x37\x2e\x33\x33\x2c\x34\x2e\x36\x33\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\x31\x31\x2c\x31\x32\x2e\x33\x35\x6c\x2e\ -\x36\x39\x2c\x31\x2e\x31\x31\x63\x31\x2c\x31\x2e\x35\x36\x2c\x31\ -\x2e\x39\x33\x2c\x33\x2e\x31\x32\x2c\x32\x2e\x39\x2c\x34\x2e\x36\ -\x37\x6c\x30\x2c\x2e\x30\x37\x48\x31\x32\x63\x30\x2c\x2e\x30\x38\ -\x2e\x30\x38\x2e\x31\x33\x2e\x31\x31\x2e\x31\x39\x61\x33\x2e\x33\ -\x31\x2c\x33\x2e\x33\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x38\ -\x2c\x31\x2e\x39\x2c\x33\x2e\x37\x37\x2c\x33\x2e\x37\x37\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x34\x37\x2c\x31\x2e\x35\x37\x2c\x31\x33\ -\x2e\x38\x36\x2c\x31\x33\x2e\x38\x36\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x31\x2c\x31\x2e\x34\x34\x2c\x35\x2e\x38\x34\x2c\x35\x2e\x38\x34\ -\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x35\x39\x2c\x31\x2c\x31\x2e\x38\ -\x39\x2c\x31\x2e\x38\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x33\x35\ -\x2c\x32\x2e\x30\x37\x2c\x33\x2e\x37\x31\x2c\x33\x2e\x37\x31\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x37\x38\x63\x2e\x30\x37\x2e\ -\x30\x35\x2e\x30\x37\x2e\x30\x38\x2c\x30\x2c\x2e\x31\x34\x6c\x2d\ -\x2e\x37\x38\x2c\x31\x2e\x32\x32\x4c\x31\x31\x2c\x32\x38\x2e\x34\ -\x34\x61\x35\x2e\x35\x2c\x35\x2e\x35\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x31\x2e\x35\x34\x2d\x31\x2e\x33\x39\x2c\x33\x2e\x34\x34\x2c\x33\ -\x2e\x34\x34\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x37\x2d\x32\x2c\x33\ -\x2e\x34\x39\x2c\x33\x2e\x34\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\ -\x33\x36\x2d\x31\x2e\x35\x39\x2c\x38\x2e\x37\x33\x2c\x38\x2e\x37\ -\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x39\x2d\x31\x2e\x34\x32\x2c\ -\x37\x2e\x32\x2c\x37\x2e\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x37\ -\x34\x2d\x31\x2e\x31\x39\x2c\x31\x2e\x38\x35\x2c\x31\x2e\x38\x35\ -\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x30\x36\x2d\x31\x2e\x37\x37\x2c\ -\x32\x2e\x37\x33\x2c\x32\x2e\x37\x33\x2c\x30\x2c\x30\x2c\x30\x2d\ -\x2e\x37\x38\x2d\x2e\x38\x34\x2e\x33\x34\x2e\x33\x34\x2c\x30\x2c\ -\x30\x2c\x30\x2d\x2e\x31\x39\x2d\x2e\x30\x37\x48\x37\x2e\x33\x39\ -\x73\x2e\x36\x33\x2d\x31\x2e\x30\x35\x2e\x39\x33\x2d\x31\x2e\x35\ -\x33\x4c\x31\x31\x2c\x31\x32\x2e\x33\x35\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\x32\x30\x2e\x34\x34\x2c\x31\x32\x2e\x33\ -\x35\x6c\x2e\x36\x38\x2c\x31\x2e\x31\x31\x4c\x32\x34\x2c\x31\x38\ -\x2e\x31\x33\x6c\x30\x2c\x2e\x30\x37\x48\x32\x31\x2e\x33\x36\x6c\ -\x2e\x31\x31\x2e\x31\x39\x61\x33\x2e\x33\x32\x2c\x33\x2e\x33\x32\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x39\x2c\x31\x2e\x39\x2c\x33\ -\x2e\x37\x37\x2c\x33\x2e\x37\x37\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\ -\x34\x37\x2c\x31\x2e\x35\x37\x2c\x31\x35\x2e\x36\x36\x2c\x31\x35\ -\x2e\x36\x36\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2c\x31\x2e\x34\x34\ -\x2c\x34\x2e\x37\x38\x2c\x34\x2e\x37\x38\x2c\x30\x2c\x30\x2c\x30\ -\x2d\x2e\x35\x39\x2c\x31\x2c\x31\x2e\x38\x37\x2c\x31\x2e\x38\x37\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x33\x34\x2c\x32\x2e\x30\x37\x2c\ -\x33\x2e\x38\x37\x2c\x33\x2e\x38\x37\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x2e\x37\x38\x63\x2e\x30\x38\x2e\x30\x35\x2e\x30\x37\x2e\ -\x30\x38\x2c\x30\x2c\x2e\x31\x34\x2d\x2e\x32\x35\x2e\x33\x37\x2d\ -\x2e\x37\x38\x2c\x31\x2e\x32\x32\x2d\x2e\x37\x38\x2c\x31\x2e\x32\ -\x32\x6c\x2d\x2e\x31\x33\x2d\x2e\x30\x37\x61\x35\x2e\x36\x34\x2c\ -\x35\x2e\x36\x34\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x34\x2d\ -\x31\x2e\x33\x39\x2c\x33\x2e\x33\x36\x2c\x33\x2e\x33\x36\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x36\x39\x2d\x32\x2c\x33\x2e\x34\x39\x2c\ -\x33\x2e\x34\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x33\x35\x2d\x31\ -\x2e\x35\x39\x2c\x31\x30\x2e\x33\x34\x2c\x31\x30\x2e\x33\x34\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x2e\x39\x2d\x31\x2e\x34\x32\x2c\x36\x2e\ -\x35\x39\x2c\x36\x2e\x35\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x37\ -\x34\x2d\x31\x2e\x31\x39\x2c\x31\x2e\x38\x38\x2c\x31\x2e\x38\x38\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x37\x37\x2c\x32\x2e\ -\x37\x36\x2c\x32\x2e\x37\x36\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x37\ -\x39\x2d\x2e\x38\x34\x2e\x33\x34\x2e\x33\x34\x2c\x30\x2c\x30\x2c\ -\x30\x2d\x2e\x31\x39\x2d\x2e\x30\x37\x48\x31\x36\x2e\x37\x39\x73\ -\x2e\x36\x34\x2d\x31\x2e\x30\x35\x2e\x39\x33\x2d\x31\x2e\x35\x33\ -\x6c\x32\x2e\x37\x2d\x34\x2e\x33\x33\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\x32\x39\x2e\x38\x34\x2c\x31\x32\x2e\x33\x35\ -\x6c\x2e\x36\x39\x2c\x31\x2e\x31\x31\x63\x31\x2c\x31\x2e\x35\x36\ -\x2c\x31\x2e\x39\x33\x2c\x33\x2e\x31\x32\x2c\x32\x2e\x39\x2c\x34\ -\x2e\x36\x37\x6c\x30\x2c\x2e\x30\x37\x68\x2d\x32\x2e\x37\x61\x31\ -\x2e\x36\x2c\x31\x2e\x36\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x31\x31\ -\x2e\x31\x39\x2c\x33\x2e\x33\x31\x2c\x33\x2e\x33\x31\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x2e\x34\x38\x2c\x31\x2e\x39\x2c\x33\x2e\x37\x37\ -\x2c\x33\x2e\x37\x37\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x34\x37\x2c\ -\x31\x2e\x35\x37\x2c\x31\x33\x2e\x38\x36\x2c\x31\x33\x2e\x38\x36\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2c\x31\x2e\x34\x34\x2c\x35\x2e\ -\x38\x34\x2c\x35\x2e\x38\x34\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x35\ -\x39\x2c\x31\x2c\x31\x2e\x38\x39\x2c\x31\x2e\x38\x39\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x2e\x33\x35\x2c\x32\x2e\x30\x37\x2c\x33\x2e\x37\ -\x31\x2c\x33\x2e\x37\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\ -\x37\x38\x63\x2e\x30\x37\x2e\x30\x35\x2e\x30\x37\x2e\x30\x38\x2c\ -\x30\x2c\x2e\x31\x34\x6c\x2d\x2e\x37\x38\x2c\x31\x2e\x32\x32\x2d\ -\x2e\x31\x32\x2d\x2e\x30\x37\x61\x35\x2e\x35\x2c\x35\x2e\x35\x2c\ -\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x34\x2d\x31\x2e\x33\x39\x2c\ -\x33\x2e\x34\x34\x2c\x33\x2e\x34\x34\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x2e\x37\x2d\x32\x2c\x33\x2e\x34\x39\x2c\x33\x2e\x34\x39\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x2e\x33\x36\x2d\x31\x2e\x35\x39\x2c\x38\x2e\ -\x37\x33\x2c\x38\x2e\x37\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x39\ -\x2d\x31\x2e\x34\x32\x2c\x37\x2e\x32\x2c\x37\x2e\x32\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x2e\x37\x34\x2d\x31\x2e\x31\x39\x2c\x31\x2e\x38\ -\x35\x2c\x31\x2e\x38\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x30\x36\ -\x2d\x31\x2e\x37\x37\x2c\x32\x2e\x37\x33\x2c\x32\x2e\x37\x33\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x2e\x37\x38\x2d\x2e\x38\x34\x2e\x33\x35\ -\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x32\x2d\x2e\x30\x37\ -\x48\x32\x36\x2e\x31\x39\x73\x2e\x36\x34\x2d\x31\x2e\x30\x35\x2e\ -\x39\x34\x2d\x31\x2e\x35\x33\x6c\x32\x2e\x36\x39\x2d\x34\x2e\x33\ -\x33\x5a\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\ -\x76\x67\x3e\ -\x00\x00\x07\x3b\ -\x3c\ -\x73\x76\x67\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\x34\x30\x2e\x35\x20\x33\x33\x2e\x35\x32\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\x66\x66\x66\x3b\x7d\x3c\ -\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x67\ -\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\x61\ -\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x32\ -\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\ -\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\ -\x61\x79\x65\x72\x20\x31\x22\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\x32\x2e\x36\x33\x2c\x33\x33\x2e\x35\x32\x48\x2e\x39\x34\x61\ -\x2e\x39\x34\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x36\x38\ -\x2d\x31\x2e\x35\x38\x6c\x36\x2e\x39\x32\x2d\x37\x2e\x33\x35\x61\ -\x2e\x39\x32\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x36\x38\ -\x2d\x2e\x32\x39\x68\x33\x31\x2e\x37\x61\x2e\x39\x34\x2e\x39\x34\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x36\x38\x2c\x31\x2e\x35\x38\x6c\ -\x2d\x36\x2e\x39\x32\x2c\x37\x2e\x33\x35\x41\x31\x2c\x31\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x33\x32\x2e\x36\x33\x2c\x33\x33\x2e\x35\x32\ -\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\x31\x30\x2e\x33\ -\x32\x2c\x30\x63\x2e\x33\x2e\x34\x39\x2e\x36\x2c\x31\x2c\x2e\x39\ -\x31\x2c\x31\x2e\x34\x37\x6c\x33\x2e\x38\x33\x2c\x36\x2e\x31\x36\ -\x2c\x30\x2c\x2e\x30\x39\x48\x31\x31\x2e\x35\x35\x6c\x2e\x31\x34\ -\x2e\x32\x34\x61\x34\x2e\x33\x37\x2c\x34\x2e\x33\x37\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x2e\x36\x34\x2c\x32\x2e\x35\x31\x2c\x34\x2e\x39\ -\x34\x2c\x34\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x36\x32\ -\x2c\x32\x2e\x30\x36\x2c\x31\x39\x2c\x31\x39\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x31\x2e\x32\x37\x2c\x31\x2e\x39\x2c\x38\x2e\x33\x33\x2c\ -\x38\x2e\x33\x33\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x37\x38\x2c\x31\ -\x2e\x33\x32\x2c\x32\x2e\x35\x32\x2c\x32\x2e\x35\x32\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x2e\x34\x36\x2c\x32\x2e\x37\x34\x2c\x35\x2e\x34\ -\x32\x2c\x35\x2e\x34\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2e\x33\ -\x31\x2c\x31\x63\x2e\x31\x2e\x30\x35\x2e\x30\x38\x2e\x30\x39\x2c\ -\x30\x2c\x2e\x31\x38\x6c\x2d\x31\x2c\x31\x2e\x36\x2d\x2e\x31\x36\ -\x2d\x2e\x30\x38\x61\x37\x2e\x35\x36\x2c\x37\x2e\x35\x36\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x32\x2d\x31\x2e\x38\x34\x2c\x34\x2e\x34\x33\ -\x2c\x34\x2e\x34\x33\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x39\x32\x2d\ -\x32\x2e\x35\x39\x2c\x34\x2e\x35\x38\x2c\x34\x2e\x35\x38\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x2e\x34\x37\x2d\x32\x2e\x31\x41\x31\x32\x2e\ -\x31\x33\x2c\x31\x32\x2e\x31\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x39\ -\x2c\x31\x32\x2e\x38\x31\x61\x38\x2e\x39\x32\x2c\x38\x2e\x39\x32\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x36\x2c\x32\x2e\ -\x35\x2c\x32\x2e\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x30\x37\x2d\ -\x32\x2e\x33\x33\x2c\x33\x2e\x36\x38\x2c\x33\x2e\x36\x38\x2c\x30\ -\x2c\x30\x2c\x30\x2d\x31\x2d\x31\x2e\x31\x31\x2e\x34\x2e\x34\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x2e\x32\x35\x2d\x2e\x30\x39\x48\x35\x2e\ -\x35\x32\x73\x2e\x38\x33\x2d\x31\x2e\x33\x38\x2c\x31\x2e\x32\x33\ -\x2d\x32\x43\x37\x2e\x39\x33\x2c\x33\x2e\x38\x31\x2c\x39\x2e\x31\ -\x31\x2c\x31\x2e\x39\x2c\x31\x30\x2e\x33\x2c\x30\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\x32\x32\x2e\x37\x32\x2c\x30\x6c\ -\x2e\x39\x2c\x31\x2e\x34\x37\x71\x31\x2e\x39\x32\x2c\x33\x2e\x30\ -\x38\x2c\x33\x2e\x38\x33\x2c\x36\x2e\x31\x36\x61\x2e\x32\x34\x2e\ -\x32\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x30\x35\x2e\x30\x39\x48\ -\x32\x33\x2e\x39\x34\x63\x2e\x30\x36\x2e\x30\x39\x2e\x31\x2e\x31\ -\x37\x2e\x31\x34\x2e\x32\x34\x61\x34\x2e\x32\x39\x2c\x34\x2e\x32\ -\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x36\x34\x2c\x32\x2e\x35\x31\ -\x2c\x34\x2e\x38\x2c\x34\x2e\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\ -\x36\x32\x2c\x32\x2e\x30\x36\x2c\x31\x37\x2e\x33\x37\x2c\x31\x37\ -\x2e\x33\x37\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x32\x36\x2c\x31\ -\x2e\x39\x2c\x37\x2e\x36\x33\x2c\x37\x2e\x36\x33\x2c\x30\x2c\x30\ -\x2c\x30\x2d\x2e\x37\x38\x2c\x31\x2e\x33\x32\x2c\x32\x2e\x35\x2c\ -\x32\x2e\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x36\x2c\x32\x2e\ -\x37\x34\x2c\x35\x2e\x33\x37\x2c\x35\x2e\x33\x37\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x31\x2e\x33\x2c\x31\x63\x2e\x31\x2e\x30\x35\x2e\x30\ -\x39\x2e\x30\x39\x2c\x30\x2c\x2e\x31\x38\x2d\x2e\x33\x33\x2e\x34\ -\x39\x2d\x31\x2c\x31\x2e\x36\x2d\x31\x2c\x31\x2e\x36\x6c\x2d\x2e\ -\x31\x36\x2d\x2e\x30\x38\x61\x37\x2e\x37\x31\x2c\x37\x2e\x37\x31\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2d\x31\x2e\x38\x34\x2c\x34\x2e\ -\x35\x2c\x34\x2e\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x39\x32\x2d\ -\x32\x2e\x35\x39\x2c\x34\x2e\x35\x38\x2c\x34\x2e\x35\x38\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x2e\x34\x37\x2d\x32\x2e\x31\x2c\x31\x33\x2c\ -\x31\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x31\x39\x2d\x31\x2e\ -\x38\x38\x2c\x38\x2e\x39\x32\x2c\x38\x2e\x39\x32\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x36\x2c\x32\x2e\x34\x37\x2c\x32\ -\x2e\x34\x37\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x30\x38\x2d\x32\x2e\ -\x33\x33\x2c\x33\x2e\x36\x35\x2c\x33\x2e\x36\x35\x2c\x30\x2c\x30\ -\x2c\x30\x2d\x31\x2d\x31\x2e\x31\x31\x41\x2e\x34\x31\x2e\x34\x31\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x31\x2c\x37\x2e\x37\x32\x48\x31\ -\x37\x2e\x39\x31\x73\x2e\x38\x34\x2d\x31\x2e\x33\x38\x2c\x31\x2e\ -\x32\x33\x2d\x32\x43\x32\x30\x2e\x33\x33\x2c\x33\x2e\x38\x31\x2c\ -\x32\x31\x2e\x35\x31\x2c\x31\x2e\x39\x2c\x32\x32\x2e\x36\x39\x2c\ -\x30\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\x33\x35\x2e\ -\x31\x32\x2c\x30\x2c\x33\x36\x2c\x31\x2e\x34\x37\x6c\x33\x2e\x38\ -\x33\x2c\x36\x2e\x31\x36\x2c\x30\x2c\x2e\x30\x39\x48\x33\x36\x2e\ -\x33\x34\x63\x2e\x30\x36\x2e\x30\x39\x2e\x31\x2e\x31\x37\x2e\x31\ -\x34\x2e\x32\x34\x61\x34\x2e\x33\x37\x2c\x34\x2e\x33\x37\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x2e\x36\x34\x2c\x32\x2e\x35\x31\x2c\x34\x2e\ -\x38\x2c\x34\x2e\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x36\x32\x2c\ -\x32\x2e\x30\x36\x2c\x31\x39\x2c\x31\x39\x2c\x30\x2c\x30\x2c\x31\ -\x2d\x31\x2e\x32\x36\x2c\x31\x2e\x39\x2c\x37\x2c\x37\x2c\x30\x2c\ -\x30\x2c\x30\x2d\x2e\x37\x38\x2c\x31\x2e\x33\x32\x2c\x32\x2e\x35\ -\x2c\x32\x2e\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x35\x2c\x32\ -\x2e\x37\x34\x2c\x35\x2e\x34\x32\x2c\x35\x2e\x34\x32\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x31\x2e\x33\x31\x2c\x31\x63\x2e\x31\x2e\x30\x35\ -\x2e\x30\x39\x2e\x30\x39\x2c\x30\x2c\x2e\x31\x38\x6c\x2d\x31\x2c\ -\x31\x2e\x36\x2d\x2e\x31\x36\x2d\x2e\x30\x38\x61\x37\x2e\x35\x36\ -\x2c\x37\x2e\x35\x36\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2d\x31\x2e\ -\x38\x34\x2c\x34\x2e\x34\x33\x2c\x34\x2e\x34\x33\x2c\x30\x2c\x30\ -\x2c\x31\x2d\x2e\x39\x32\x2d\x32\x2e\x35\x39\x2c\x34\x2e\x35\x38\ -\x2c\x34\x2e\x35\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x37\x2d\ -\x32\x2e\x31\x2c\x31\x32\x2e\x39\x33\x2c\x31\x32\x2e\x39\x33\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x31\x38\x2d\x31\x2e\x38\x38\x2c\ -\x38\x2e\x33\x36\x2c\x38\x2e\x33\x36\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x31\x2e\x35\x36\x2c\x32\x2e\x34\x34\x2c\x32\x2e\x34\x34\ -\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x30\x37\x2d\x32\x2e\x33\x33\x2c\ -\x33\x2e\x35\x36\x2c\x33\x2e\x35\x36\x2c\x30\x2c\x30\x2c\x30\x2d\ -\x31\x2d\x31\x2e\x31\x31\x2e\x33\x38\x2e\x33\x38\x2c\x30\x2c\x30\ -\x2c\x30\x2d\x2e\x32\x35\x2d\x2e\x30\x39\x48\x33\x30\x2e\x33\x31\ -\x73\x2e\x38\x34\x2d\x31\x2e\x33\x38\x2c\x31\x2e\x32\x33\x2d\x32\ -\x4c\x33\x35\x2e\x30\x39\x2c\x30\x5a\x22\x2f\x3e\x3c\x2f\x67\x3e\ -\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x0a\x60\ -\x3c\ -\x73\x76\x67\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\x31\x37\x36\x2e\x30\x32\x20\x35\x38\x2e\x34\x31\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\ -\x36\x35\x2e\x37\x38\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\ -\x69\x6c\x79\x3a\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\ -\x2c\x20\x4d\x6f\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\ -\x65\x69\x67\x68\x74\x3a\x32\x30\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x31\x2c\x2e\x63\x6c\x73\x2d\x34\x7b\x66\x69\x6c\x6c\x3a\x23\x66\ -\x66\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x6c\x65\x74\x74\x65\ -\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x32\x65\ -\x6d\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x6c\x65\x74\x74\x65\x72\ -\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x35\x65\x6d\ -\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\ -\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\ -\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\ -\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x74\x65\x78\x74\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x74\ -\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\ -\x61\x74\x65\x28\x34\x37\x2e\x39\x31\x20\x34\x39\x2e\x39\x33\x29\ -\x22\x3e\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x32\x22\x3e\x61\x3c\x2f\x74\x73\x70\x61\x6e\x3e\ -\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x33\x22\x20\x78\x3d\x22\x33\x32\x2e\x36\x39\x22\x20\x79\ -\x3d\x22\x30\x22\x3e\x62\x3c\x2f\x74\x73\x70\x61\x6e\x3e\x3c\x74\ -\x73\x70\x61\x6e\x20\x78\x3d\x22\x35\x38\x2e\x34\x31\x22\x20\x79\ -\x3d\x22\x30\x22\x3e\x73\x3c\x2f\x74\x73\x70\x61\x6e\x3e\x3c\x2f\ -\x74\x65\x78\x74\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x32\x34\x2e\ -\x37\x38\x2c\x31\x38\x2e\x31\x34\x63\x32\x2e\x35\x36\x2c\x31\x2e\ -\x33\x35\x2c\x33\x2e\x37\x31\x2c\x36\x2e\x34\x34\x2c\x34\x2e\x31\ -\x38\x2c\x39\x2e\x30\x36\x2c\x31\x2e\x32\x2c\x36\x2e\x35\x36\x2c\ -\x31\x2e\x31\x34\x2c\x31\x39\x2e\x31\x2d\x33\x2e\x30\x39\x2c\x32\ -\x34\x2e\x35\x31\x2d\x2e\x38\x31\x2c\x31\x2d\x31\x2e\x38\x32\x2c\ -\x31\x2e\x32\x32\x2d\x32\x2e\x37\x34\x2e\x32\x33\x61\x39\x2e\x31\ -\x31\x2c\x39\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x38\ -\x35\x2d\x33\x2e\x31\x31\x63\x2d\x32\x2e\x38\x33\x2d\x37\x2e\x36\ -\x2d\x32\x2e\x38\x38\x2d\x31\x39\x2e\x32\x39\x2c\x30\x2d\x32\x36\ -\x2e\x39\x2e\x35\x38\x2d\x31\x2e\x34\x32\x2c\x31\x2e\x33\x31\x2d\ -\x33\x2c\x32\x2e\x37\x35\x2d\x33\x2e\x37\x39\x5a\x4d\x32\x33\x2e\ -\x35\x33\x2c\x34\x39\x2e\x37\x32\x61\x2e\x38\x38\x2e\x38\x38\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x36\x2e\x36\x2e\x39\x31\x2e\x39\ -\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x39\x2d\x2e\x36\x32\x63\ -\x33\x2e\x30\x39\x2d\x36\x2e\x35\x34\x2c\x33\x2e\x31\x33\x2d\x31\ -\x36\x2e\x35\x35\x2c\x31\x2e\x36\x39\x2d\x32\x33\x2e\x35\x37\x41\ -\x32\x30\x2e\x32\x37\x2c\x32\x30\x2e\x32\x37\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x32\x35\x2e\x32\x37\x2c\x32\x31\x61\x2e\x39\x2e\x39\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x2e\x38\x35\x2d\x2e\x36\x2e\x39\x32\x2e\ -\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2e\x36\x31\x2c\x31\ -\x34\x2e\x39\x34\x2c\x31\x34\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x30\ -\x2d\x31\x2e\x33\x2c\x33\x2e\x34\x38\x43\x32\x30\x2e\x35\x32\x2c\ -\x33\x31\x2e\x36\x34\x2c\x31\x39\x2e\x34\x31\x2c\x34\x30\x2e\x34\ -\x31\x2c\x32\x33\x2e\x35\x33\x2c\x34\x39\x2e\x37\x32\x5a\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\ -\x22\x4d\x35\x2e\x36\x34\x2c\x31\x38\x2e\x31\x34\x61\x35\x2e\x31\ -\x33\x2c\x35\x2e\x31\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2c\x32\ -\x2e\x32\x37\x63\x2d\x2e\x37\x32\x2c\x30\x2d\x31\x2e\x33\x39\x2c\ -\x30\x2d\x32\x2e\x30\x36\x2c\x30\x73\x2d\x31\x2c\x2e\x31\x2d\x31\ -\x2e\x32\x39\x2e\x37\x34\x43\x31\x2c\x32\x38\x2e\x34\x38\x2c\x31\ -\x2c\x34\x31\x2e\x31\x39\x2c\x33\x2e\x39\x32\x2c\x34\x38\x2e\x37\ -\x63\x2e\x31\x35\x2e\x33\x36\x2e\x33\x33\x2e\x37\x31\x2e\x34\x39\ -\x2c\x31\x2e\x30\x36\x61\x31\x2c\x31\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x2e\x35\x37\x48\x37\x2e\x36\x37\x41\x35\x2e\x30\x37\x2c\ -\x35\x2e\x30\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x2e\x39\x32\x2c\ -\x35\x32\x2e\x34\x63\x2d\x31\x2e\x32\x34\x2e\x37\x34\x2d\x32\x2e\ -\x34\x2d\x2e\x38\x38\x2d\x32\x2e\x39\x33\x2d\x31\x2e\x38\x31\x43\ -\x2d\x2e\x38\x32\x2c\x34\x33\x2e\x35\x33\x2d\x2e\x37\x34\x2c\x33\ -\x30\x2e\x32\x38\x2c\x31\x2e\x38\x33\x2c\x32\x32\x2e\x37\x34\x63\ -\x2e\x35\x39\x2d\x31\x2e\x36\x34\x2c\x31\x2e\x34\x2d\x33\x2e\x36\ -\x39\x2c\x33\x2e\x30\x35\x2d\x34\x2e\x36\x5a\x22\x20\x74\x72\x61\ -\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\ -\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x33\ -\x2e\x35\x32\x2c\x33\x35\x2e\x37\x37\x63\x30\x2d\x35\x2c\x2e\x33\ -\x37\x2d\x39\x2e\x32\x33\x2c\x32\x2e\x31\x37\x2d\x31\x33\x2e\x36\ -\x31\x61\x2e\x34\x32\x2e\x34\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\ -\x34\x38\x2d\x2e\x32\x39\x63\x2e\x36\x2c\x30\x2c\x31\x2e\x32\x2c\ -\x30\x2c\x31\x2e\x38\x2c\x30\x2c\x2e\x32\x39\x2c\x30\x2c\x2e\x33\ -\x35\x2c\x30\x2c\x2e\x32\x35\x2e\x33\x61\x32\x37\x2e\x33\x35\x2c\ -\x32\x37\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x33\x37\ -\x2c\x35\x63\x2d\x31\x2e\x31\x37\x2c\x36\x2e\x39\x2d\x31\x2e\x30\ -\x38\x2c\x31\x34\x2e\x37\x35\x2c\x31\x2e\x33\x37\x2c\x32\x31\x2e\ -\x33\x38\x2e\x31\x2e\x32\x34\x2e\x30\x39\x2e\x33\x34\x2d\x2e\x32\ -\x35\x2e\x33\x33\x2d\x2e\x36\x2c\x30\x2d\x31\x2e\x32\x31\x2c\x30\ -\x2d\x31\x2e\x38\x31\x2c\x30\x61\x2e\x34\x33\x2e\x34\x33\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x34\x37\x2d\x2e\x33\x43\x34\x2c\x34\x34\ -\x2e\x36\x38\x2c\x33\x2e\x35\x32\x2c\x33\x39\x2e\x36\x38\x2c\x33\ -\x2e\x35\x32\x2c\x33\x35\x2e\x37\x37\x5a\x22\x20\x74\x72\x61\x6e\ -\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\ -\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x31\x38\ -\x2e\x33\x38\x2c\x32\x31\x2e\x38\x37\x63\x2e\x32\x38\x2e\x30\x35\ -\x2e\x38\x31\x2d\x2e\x31\x36\x2c\x31\x2c\x2e\x30\x39\x73\x2d\x2e\ -\x31\x33\x2e\x36\x31\x2d\x2e\x32\x32\x2e\x39\x32\x61\x34\x35\x2e\ -\x36\x39\x2c\x34\x35\x2e\x36\x39\x2c\x30\x2c\x30\x2c\x30\x2d\x31\ -\x2e\x35\x32\x2c\x31\x37\x2e\x32\x39\x2c\x33\x39\x2e\x36\x31\x2c\ -\x33\x39\x2e\x36\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2e\x37\x32\ -\x2c\x38\x2e\x33\x31\x63\x2e\x31\x31\x2e\x33\x32\x2e\x30\x35\x2e\ -\x34\x2d\x2e\x33\x33\x2e\x33\x38\x73\x2d\x31\x2c\x30\x2d\x31\x2e\ -\x35\x33\x2c\x30\x61\x2e\x34\x39\x2e\x34\x39\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x2e\x35\x34\x2d\x2e\x33\x33\x2c\x32\x33\x2e\x38\x35\x2c\ -\x32\x33\x2e\x38\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x31\ -\x2d\x35\x2e\x33\x63\x2d\x31\x2e\x30\x39\x2d\x36\x2e\x35\x38\x2d\ -\x31\x2d\x31\x34\x2e\x37\x34\x2c\x31\x2e\x34\x39\x2d\x32\x31\x61\ -\x2e\x35\x2e\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x36\x2d\x2e\x33\ -\x37\x41\x37\x2e\x34\x31\x2c\x37\x2e\x34\x31\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x31\x38\x2e\x33\x38\x2c\x32\x31\x2e\x38\x37\x5a\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\ -\x22\x4d\x37\x2e\x38\x37\x2c\x33\x36\x2e\x35\x34\x61\x31\x31\x2e\ -\x32\x36\x2c\x31\x31\x2e\x32\x36\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\ -\x30\x36\x2d\x32\x2e\x33\x33\x63\x2e\x32\x37\x2d\x34\x2e\x30\x38\ -\x2e\x35\x37\x2d\x38\x2e\x32\x38\x2c\x32\x2e\x32\x35\x2d\x31\x32\ -\x2e\x31\x31\x61\x2e\x33\x36\x2e\x33\x36\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x2e\x33\x38\x2d\x2e\x32\x33\x63\x31\x2e\x33\x39\x2d\x2e\x30\ -\x38\x2c\x31\x2e\x33\x38\x2d\x2e\x30\x38\x2c\x31\x2c\x31\x43\x39\ -\x2c\x33\x30\x2e\x34\x35\x2c\x38\x2e\x39\x33\x2c\x34\x31\x2c\x31\ -\x31\x2e\x36\x38\x2c\x34\x38\x2e\x35\x63\x2e\x31\x32\x2e\x33\x31\ -\x2e\x30\x35\x2e\x33\x37\x2d\x2e\x33\x2e\x33\x36\x2d\x31\x2e\x32\ -\x35\x2c\x30\x2d\x31\x2e\x32\x36\x2c\x30\x2d\x31\x2e\x36\x34\x2d\ -\x31\x41\x33\x36\x2e\x31\x34\x2c\x33\x36\x2e\x31\x34\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x37\x2e\x38\x37\x2c\x33\x36\x2e\x35\x34\x5a\x22\ -\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\ -\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\ -\x3d\x22\x4d\x31\x31\x2e\x33\x35\x2c\x33\x35\x2e\x36\x36\x63\x30\ -\x2d\x34\x2e\x38\x36\x2e\x33\x38\x2d\x39\x2e\x31\x37\x2c\x32\x2e\ -\x31\x37\x2d\x31\x33\x2e\x34\x39\x61\x2e\x34\x2e\x34\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x2e\x34\x37\x2d\x2e\x33\x63\x2e\x34\x2c\x30\x2c\ -\x31\x2d\x2e\x31\x35\x2c\x31\x2e\x31\x38\x2e\x30\x37\x73\x2d\x2e\ -\x31\x37\x2e\x36\x33\x2d\x2e\x32\x38\x2c\x31\x63\x2d\x32\x2e\x34\ -\x31\x2c\x37\x2e\x36\x31\x2d\x32\x2e\x35\x31\x2c\x31\x38\x2c\x2e\ -\x32\x37\x2c\x32\x35\x2e\x35\x36\x2e\x31\x34\x2e\x33\x36\x2c\x30\ -\x2c\x2e\x34\x2d\x2e\x33\x35\x2e\x33\x39\x2d\x31\x2e\x31\x37\x2c\ -\x30\x2d\x31\x2e\x31\x39\x2c\x30\x2d\x31\x2e\x35\x37\x2d\x31\x41\ -\x33\x35\x2e\x37\x34\x2c\x33\x35\x2e\x37\x34\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x31\x31\x2e\x33\x35\x2c\x33\x35\x2e\x36\x36\x5a\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\ -\x22\x4d\x31\x39\x2e\x31\x32\x2c\x32\x30\x2e\x37\x37\x63\x2e\x39\ -\x32\x2c\x30\x2c\x2e\x39\x32\x2c\x30\x2c\x31\x2e\x31\x33\x2d\x2e\ -\x37\x38\x2e\x38\x36\x2d\x32\x2e\x38\x36\x2d\x2e\x32\x35\x2d\x35\ -\x2e\x34\x35\x2d\x33\x2e\x35\x2d\x35\x2e\x38\x37\x2d\x31\x2e\x35\ -\x31\x2d\x2e\x32\x33\x2d\x31\x2e\x35\x2d\x2e\x32\x31\x2d\x31\x2e\ -\x34\x38\x2c\x31\x2e\x31\x2c\x30\x2c\x2e\x32\x35\x2e\x30\x39\x2e\ -\x33\x33\x2e\x33\x37\x2e\x33\x33\x61\x35\x2c\x35\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x31\x2e\x34\x39\x2e\x32\x33\x2c\x32\x2e\x31\x31\x2c\ -\x32\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x35\x38\x2c\ -\x31\x2e\x37\x37\x2c\x35\x2e\x37\x34\x2c\x35\x2e\x37\x34\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x33\x38\x2c\x32\x2e\x37\x38\x63\x2d\x2e\ -\x31\x35\x2e\x34\x34\x2d\x2e\x31\x35\x2e\x34\x34\x2e\x34\x31\x2e\ -\x34\x34\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\ -\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\ -\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x34\x22\x20\x63\x78\x3d\x22\x32\x34\x2e\x34\x32\x22\ -\x20\x63\x79\x3d\x22\x33\x35\x2e\x35\x34\x22\x20\x72\x78\x3d\x22\ -\x31\x2e\x30\x31\x22\x20\x72\x79\x3d\x22\x35\x2e\x39\x39\x22\x2f\ -\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x0a\x25\ -\x3c\ -\x73\x76\x67\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\x31\x37\x36\x2e\x30\x32\x20\x35\x38\x2e\x34\x31\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\ -\x36\x35\x2e\x37\x38\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\ -\x69\x6c\x79\x3a\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\ -\x2c\x20\x4d\x6f\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\ -\x65\x69\x67\x68\x74\x3a\x32\x30\x30\x3b\x6c\x65\x74\x74\x65\x72\ -\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x31\x65\x6d\ -\x3b\x7d\x2e\x63\x6c\x73\x2d\x31\x2c\x2e\x63\x6c\x73\x2d\x33\x7b\ -\x66\x69\x6c\x6c\x3a\x23\x66\x66\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x32\x7b\x6c\x65\x74\x74\x65\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\ -\x3a\x2d\x30\x2e\x30\x33\x65\x6d\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\ -\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x67\x20\x69\x64\x3d\x22\ -\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\ -\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x32\x22\x3e\x3c\x67\x20\ -\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x2d\x32\x22\x20\x64\ -\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\ -\x31\x22\x3e\x3c\x74\x65\x78\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x31\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\ -\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x34\x37\x2e\x39\ -\x31\x20\x34\x39\x2e\x39\x33\x29\x22\x3e\x70\x6c\x3c\x74\x73\x70\ -\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\ -\x20\x78\x3d\x22\x35\x35\x2e\x31\x39\x22\x20\x79\x3d\x22\x30\x22\ -\x3e\x61\x3c\x2f\x74\x73\x70\x61\x6e\x3e\x3c\x2f\x74\x65\x78\x74\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x32\x34\x2e\x37\x38\x2c\x31\ -\x38\x2e\x31\x32\x63\x32\x2e\x35\x36\x2c\x31\x2e\x33\x34\x2c\x33\ -\x2e\x37\x31\x2c\x36\x2e\x34\x34\x2c\x34\x2e\x31\x38\x2c\x39\x2e\ -\x30\x36\x2c\x31\x2e\x32\x2c\x36\x2e\x35\x36\x2c\x31\x2e\x31\x34\ -\x2c\x31\x39\x2e\x31\x2d\x33\x2e\x30\x39\x2c\x32\x34\x2e\x35\x31\ -\x2d\x2e\x38\x31\x2c\x31\x2d\x31\x2e\x38\x32\x2c\x31\x2e\x32\x31\ -\x2d\x32\x2e\x37\x34\x2e\x32\x32\x61\x39\x2c\x39\x2c\x30\x2c\x30\ -\x2c\x31\x2d\x31\x2e\x38\x35\x2d\x33\x2e\x31\x63\x2d\x32\x2e\x38\ -\x33\x2d\x37\x2e\x36\x31\x2d\x32\x2e\x38\x38\x2d\x31\x39\x2e\x33\ -\x2c\x30\x2d\x32\x36\x2e\x39\x31\x2e\x35\x38\x2d\x31\x2e\x34\x31\ -\x2c\x31\x2e\x33\x31\x2d\x33\x2c\x32\x2e\x37\x35\x2d\x33\x2e\x37\ -\x38\x5a\x4d\x32\x33\x2e\x35\x33\x2c\x34\x39\x2e\x36\x39\x61\x2e\ -\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x36\x2e\ -\x36\x31\x2e\x39\x34\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\ -\x38\x39\x2d\x2e\x36\x32\x63\x33\x2e\x30\x39\x2d\x36\x2e\x35\x34\ -\x2c\x33\x2e\x31\x33\x2d\x31\x36\x2e\x35\x35\x2c\x31\x2e\x36\x39\ -\x2d\x32\x33\x2e\x35\x37\x41\x32\x30\x2e\x32\x37\x2c\x32\x30\x2e\ -\x32\x37\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x35\x2e\x32\x37\x2c\x32\ -\x31\x61\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\ -\x38\x35\x2d\x2e\x36\x31\x2e\x39\x34\x2e\x39\x34\x2c\x30\x2c\x30\ -\x2c\x30\x2d\x2e\x39\x2e\x36\x32\x2c\x31\x34\x2e\x39\x34\x2c\x31\ -\x34\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x33\x2c\x33\ -\x2e\x34\x38\x43\x32\x30\x2e\x35\x32\x2c\x33\x31\x2e\x36\x32\x2c\ -\x31\x39\x2e\x34\x31\x2c\x34\x30\x2e\x33\x39\x2c\x32\x33\x2e\x35\ -\x33\x2c\x34\x39\x2e\x36\x39\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\ -\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\ -\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x35\x2e\x36\x34\ -\x2c\x31\x38\x2e\x31\x32\x61\x35\x2e\x31\x36\x2c\x35\x2e\x31\x36\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2c\x32\x2e\x32\x36\x48\x35\x2e\ -\x36\x31\x63\x2d\x2e\x38\x2c\x30\x2d\x31\x2c\x2e\x31\x31\x2d\x31\ -\x2e\x32\x39\x2e\x37\x34\x43\x31\x2c\x32\x38\x2e\x34\x35\x2c\x31\ -\x2c\x34\x31\x2e\x31\x37\x2c\x33\x2e\x39\x32\x2c\x34\x38\x2e\x36\ -\x38\x63\x2e\x31\x35\x2e\x33\x36\x2e\x33\x33\x2e\x37\x2e\x34\x39\ -\x2c\x31\x2e\x30\x36\x61\x31\x2c\x31\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x2e\x35\x37\x63\x2e\x37\x37\x2c\x30\x2c\x31\x2e\x34\x31\ -\x2c\x30\x2c\x32\x2e\x32\x32\x2c\x30\x61\x35\x2c\x35\x2c\x30\x2c\ -\x30\x2c\x31\x2d\x31\x2e\x37\x35\x2c\x32\x2e\x30\x37\x63\x2d\x31\ -\x2e\x32\x34\x2e\x37\x34\x2d\x32\x2e\x34\x2d\x2e\x38\x37\x2d\x32\ -\x2e\x39\x33\x2d\x31\x2e\x38\x43\x2d\x2e\x38\x32\x2c\x34\x33\x2e\ -\x35\x2d\x2e\x37\x34\x2c\x33\x30\x2e\x32\x35\x2c\x31\x2e\x38\x33\ -\x2c\x32\x32\x2e\x37\x32\x63\x2e\x35\x39\x2d\x31\x2e\x36\x35\x2c\ -\x31\x2e\x34\x2d\x33\x2e\x36\x39\x2c\x33\x2e\x30\x35\x2d\x34\x2e\ -\x36\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\ -\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\ -\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\ -\x22\x20\x64\x3d\x22\x4d\x33\x2e\x35\x32\x2c\x33\x35\x2e\x37\x35\ -\x63\x30\x2d\x35\x2c\x2e\x33\x37\x2d\x39\x2e\x32\x34\x2c\x32\x2e\ -\x31\x37\x2d\x31\x33\x2e\x36\x32\x61\x2e\x34\x34\x2e\x34\x34\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x38\x2d\x2e\x32\x39\x63\x2e\x36\ -\x2c\x30\x2c\x31\x2e\x32\x2c\x30\x2c\x31\x2e\x38\x2c\x30\x2c\x2e\ -\x32\x39\x2c\x30\x2c\x2e\x33\x35\x2c\x30\x2c\x2e\x32\x35\x2e\x32\ -\x39\x61\x32\x37\x2e\x34\x35\x2c\x32\x37\x2e\x34\x35\x2c\x30\x2c\ -\x30\x2c\x30\x2d\x31\x2e\x33\x37\x2c\x35\x43\x35\x2e\x36\x38\x2c\ -\x33\x34\x2c\x35\x2e\x37\x37\x2c\x34\x31\x2e\x38\x39\x2c\x38\x2e\ -\x32\x32\x2c\x34\x38\x2e\x35\x32\x63\x2e\x31\x2e\x32\x34\x2e\x30\ -\x39\x2e\x33\x33\x2d\x2e\x32\x35\x2e\x33\x32\x2d\x2e\x36\x2c\x30\ -\x2d\x31\x2e\x32\x31\x2c\x30\x2d\x31\x2e\x38\x31\x2c\x30\x61\x2e\ -\x34\x32\x2e\x34\x32\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x34\x37\x2d\ -\x2e\x32\x39\x43\x34\x2c\x34\x34\x2e\x36\x36\x2c\x33\x2e\x35\x32\ -\x2c\x33\x39\x2e\x36\x35\x2c\x33\x2e\x35\x32\x2c\x33\x35\x2e\x37\ -\x35\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\ -\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\ -\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\ -\x22\x20\x64\x3d\x22\x4d\x31\x38\x2e\x33\x38\x2c\x32\x31\x2e\x38\ -\x35\x63\x2e\x32\x38\x2c\x30\x2c\x2e\x38\x31\x2d\x2e\x31\x36\x2c\ -\x31\x2c\x2e\x30\x39\x73\x2d\x2e\x31\x33\x2e\x36\x2d\x2e\x32\x32\ -\x2e\x39\x32\x61\x34\x35\x2e\x36\x39\x2c\x34\x35\x2e\x36\x39\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x35\x32\x2c\x31\x37\x2e\x32\x39\ -\x2c\x33\x39\x2e\x38\x37\x2c\x33\x39\x2e\x38\x37\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x31\x2e\x37\x32\x2c\x38\x2e\x33\x31\x63\x2e\x31\x31\ -\x2e\x33\x32\x2e\x30\x35\x2e\x34\x2d\x2e\x33\x33\x2e\x33\x38\x73\ -\x2d\x31\x2c\x30\x2d\x31\x2e\x35\x33\x2c\x30\x61\x2e\x34\x37\x2e\ -\x34\x37\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x35\x34\x2d\x2e\x33\x33\ -\x2c\x32\x33\x2e\x36\x32\x2c\x32\x33\x2e\x36\x32\x2c\x30\x2c\x30\ -\x2c\x31\x2d\x31\x2e\x35\x31\x2d\x35\x2e\x32\x39\x63\x2d\x31\x2e\ -\x30\x39\x2d\x36\x2e\x35\x39\x2d\x31\x2d\x31\x34\x2e\x37\x34\x2c\ -\x31\x2e\x34\x39\x2d\x32\x31\x61\x2e\x35\x2e\x35\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x2e\x36\x2d\x2e\x33\x38\x41\x37\x2e\x31\x33\x2c\x37\ -\x2e\x31\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x38\x2e\x33\x38\x2c\ -\x32\x31\x2e\x38\x35\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\ -\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\ -\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x37\x2e\x38\x37\x2c\x33\ -\x36\x2e\x35\x32\x61\x31\x31\x2e\x32\x36\x2c\x31\x31\x2e\x32\x36\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x30\x36\x2d\x32\x2e\x33\x33\x63\ -\x2e\x32\x37\x2d\x34\x2e\x30\x38\x2e\x35\x37\x2d\x38\x2e\x32\x39\ -\x2c\x32\x2e\x32\x35\x2d\x31\x32\x2e\x31\x31\x61\x2e\x33\x38\x2e\ -\x33\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x33\x38\x2d\x2e\x32\x34\ -\x63\x31\x2e\x33\x39\x2d\x2e\x30\x37\x2c\x31\x2e\x33\x38\x2d\x2e\ -\x30\x38\x2c\x31\x2c\x31\x2e\x30\x35\x43\x39\x2c\x33\x30\x2e\x34\ -\x33\x2c\x38\x2e\x39\x33\x2c\x34\x31\x2c\x31\x31\x2e\x36\x38\x2c\ -\x34\x38\x2e\x34\x38\x63\x2e\x31\x32\x2e\x33\x31\x2e\x30\x35\x2e\ -\x33\x36\x2d\x2e\x33\x2e\x33\x36\x2d\x31\x2e\x32\x35\x2c\x30\x2d\ -\x31\x2e\x32\x36\x2c\x30\x2d\x31\x2e\x36\x34\x2d\x31\x41\x33\x36\ -\x2e\x31\x39\x2c\x33\x36\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x37\x2e\x38\x37\x2c\x33\x36\x2e\x35\x32\x5a\x22\x20\x74\x72\x61\ -\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\ -\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x31\ -\x31\x2e\x33\x35\x2c\x33\x35\x2e\x36\x34\x63\x30\x2d\x34\x2e\x38\ -\x36\x2e\x33\x38\x2d\x39\x2e\x31\x37\x2c\x32\x2e\x31\x37\x2d\x31\ -\x33\x2e\x35\x61\x2e\x34\x31\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x2e\x34\x37\x2d\x2e\x32\x39\x63\x2e\x34\x2c\x30\x2c\x31\x2d\ -\x2e\x31\x35\x2c\x31\x2e\x31\x38\x2e\x30\x36\x73\x2d\x2e\x31\x37\ -\x2e\x36\x34\x2d\x2e\x32\x38\x2c\x31\x63\x2d\x32\x2e\x34\x31\x2c\ -\x37\x2e\x36\x31\x2d\x32\x2e\x35\x31\x2c\x31\x38\x2c\x2e\x32\x37\ -\x2c\x32\x35\x2e\x35\x37\x2e\x31\x34\x2e\x33\x36\x2c\x30\x2c\x2e\ -\x33\x39\x2d\x2e\x33\x35\x2e\x33\x39\x2d\x31\x2e\x31\x37\x2c\x30\ -\x2d\x31\x2e\x31\x39\x2c\x30\x2d\x31\x2e\x35\x37\x2d\x31\x41\x33\ -\x35\x2e\x37\x37\x2c\x33\x35\x2e\x37\x37\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x31\x31\x2e\x33\x35\x2c\x33\x35\x2e\x36\x34\x5a\x22\x20\x74\ -\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\ -\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\ -\x4d\x31\x39\x2e\x31\x32\x2c\x32\x30\x2e\x37\x35\x63\x2e\x39\x32\ -\x2c\x30\x2c\x2e\x39\x32\x2c\x30\x2c\x31\x2e\x31\x33\x2d\x2e\x37\ -\x38\x2e\x38\x36\x2d\x32\x2e\x38\x37\x2d\x2e\x32\x35\x2d\x35\x2e\ -\x34\x36\x2d\x33\x2e\x35\x2d\x35\x2e\x38\x37\x2d\x31\x2e\x35\x31\ -\x2d\x2e\x32\x33\x2d\x31\x2e\x35\x2d\x2e\x32\x31\x2d\x31\x2e\x34\ -\x38\x2c\x31\x2e\x31\x2c\x30\x2c\x2e\x32\x35\x2e\x30\x39\x2e\x33\ -\x33\x2e\x33\x37\x2e\x33\x33\x61\x35\x2e\x33\x32\x2c\x35\x2e\x33\ -\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x34\x39\x2e\x32\x32\x2c\ -\x32\x2e\x31\x32\x2c\x32\x2e\x31\x32\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x31\x2e\x35\x38\x2c\x31\x2e\x37\x38\x2c\x35\x2e\x37\x38\x2c\x35\ -\x2e\x37\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x33\x38\x2c\x32\x2e\ -\x37\x38\x63\x2d\x2e\x31\x35\x2e\x34\x34\x2d\x2e\x31\x35\x2e\x34\ -\x34\x2e\x34\x31\x2e\x34\x34\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\ -\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\ -\x29\x22\x2f\x3e\x3c\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x63\x78\x3d\x22\x32\ -\x34\x2e\x34\x32\x22\x20\x63\x79\x3d\x22\x33\x35\x2e\x35\x32\x22\ -\x20\x72\x78\x3d\x22\x31\x2e\x30\x31\x22\x20\x72\x79\x3d\x22\x35\ -\x2e\x39\x39\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\ -\x73\x76\x67\x3e\ -\x00\x00\x0a\x54\ -\x3c\ -\x73\x76\x67\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\x32\x35\x32\x2e\x31\x33\x20\x35\x38\x2e\x34\x31\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\ -\x36\x35\x2e\x37\x38\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\ -\x69\x6c\x79\x3a\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\ -\x2c\x20\x4d\x6f\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\ -\x65\x69\x67\x68\x74\x3a\x32\x30\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x31\x2c\x2e\x63\x6c\x73\x2d\x34\x7b\x66\x69\x6c\x6c\x3a\x23\x66\ -\x66\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x6c\x65\x74\x74\x65\ -\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x35\x65\ -\x6d\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x6c\x65\x74\x74\x65\x72\ -\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x34\x65\x6d\ -\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\ -\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\ -\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\ -\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x74\x65\x78\x74\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x74\ -\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\ -\x61\x74\x65\x28\x34\x37\x2e\x39\x31\x20\x34\x39\x2e\x39\x33\x29\ -\x22\x3e\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x32\x22\x3e\x63\x3c\x2f\x74\x73\x70\x61\x6e\x3e\ -\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x33\x22\x20\x78\x3d\x22\x33\x31\x2e\x39\x22\x20\x79\x3d\ -\x22\x30\x22\x3e\x6f\x73\x3c\x2f\x74\x73\x70\x61\x6e\x3e\x3c\x74\ -\x73\x70\x61\x6e\x20\x78\x3d\x22\x39\x34\x2e\x37\x39\x22\x20\x79\ -\x3d\x22\x30\x22\x3e\x74\x75\x6d\x3c\x2f\x74\x73\x70\x61\x6e\x3e\ -\x3c\x2f\x74\x65\x78\x74\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x32\ -\x34\x2e\x37\x38\x2c\x31\x38\x2e\x38\x34\x63\x32\x2e\x35\x36\x2c\ -\x31\x2e\x33\x35\x2c\x33\x2e\x37\x31\x2c\x36\x2e\x34\x34\x2c\x34\ -\x2e\x31\x38\x2c\x39\x2e\x30\x37\x2c\x31\x2e\x32\x2c\x36\x2e\x35\ -\x36\x2c\x31\x2e\x31\x34\x2c\x31\x39\x2e\x31\x2d\x33\x2e\x30\x39\ -\x2c\x32\x34\x2e\x35\x31\x2d\x2e\x38\x31\x2c\x31\x2d\x31\x2e\x38\ -\x32\x2c\x31\x2e\x32\x31\x2d\x32\x2e\x37\x34\x2e\x32\x32\x61\x39\ -\x2e\x31\x31\x2c\x39\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2d\x31\ -\x2e\x38\x35\x2d\x33\x2e\x31\x31\x63\x2d\x32\x2e\x38\x33\x2d\x37\ -\x2e\x36\x2d\x32\x2e\x38\x38\x2d\x31\x39\x2e\x32\x39\x2c\x30\x2d\ -\x32\x36\x2e\x39\x41\x37\x2e\x30\x36\x2c\x37\x2e\x30\x36\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x32\x34\x2c\x31\x38\x2e\x38\x34\x5a\x4d\x32\ -\x33\x2e\x35\x33\x2c\x35\x30\x2e\x34\x32\x61\x2e\x38\x38\x2e\x38\ -\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x36\x2e\x36\x2e\x39\x31\ -\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x39\x2d\x2e\x36\ -\x32\x63\x33\x2e\x30\x39\x2d\x36\x2e\x35\x34\x2c\x33\x2e\x31\x33\ -\x2d\x31\x36\x2e\x35\x35\x2c\x31\x2e\x36\x39\x2d\x32\x33\x2e\x35\ -\x37\x61\x32\x30\x2e\x32\x37\x2c\x32\x30\x2e\x32\x37\x2c\x30\x2c\ -\x30\x2c\x30\x2d\x31\x2e\x37\x2d\x35\x2e\x31\x32\x2e\x39\x31\x2e\ -\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x38\x35\x2d\x2e\x36\x2e\ -\x39\x32\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2e\x36\ -\x31\x2c\x31\x35\x2c\x31\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\ -\x33\x2c\x33\x2e\x34\x39\x43\x32\x30\x2e\x35\x32\x2c\x33\x32\x2e\ -\x33\x34\x2c\x31\x39\x2e\x34\x31\x2c\x34\x31\x2e\x31\x31\x2c\x32\ -\x33\x2e\x35\x33\x2c\x35\x30\x2e\x34\x32\x5a\x22\x20\x74\x72\x61\ -\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\ -\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x35\ -\x2e\x36\x34\x2c\x31\x38\x2e\x38\x34\x61\x35\x2e\x31\x39\x2c\x35\ -\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2c\x32\x2e\x32\x37\ -\x48\x35\x2e\x36\x31\x63\x2d\x2e\x38\x2c\x30\x2d\x31\x2c\x2e\x31\ -\x31\x2d\x31\x2e\x32\x39\x2e\x37\x34\x43\x31\x2c\x32\x39\x2e\x31\ -\x38\x2c\x31\x2c\x34\x31\x2e\x39\x2c\x33\x2e\x39\x32\x2c\x34\x39\ -\x2e\x34\x63\x2e\x31\x35\x2e\x33\x36\x2e\x33\x33\x2e\x37\x31\x2e\ -\x34\x39\x2c\x31\x2e\x30\x36\x61\x31\x2c\x31\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x2e\x35\x37\x48\x37\x2e\x36\x37\x41\x35\x2c\x35\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x2e\x39\x32\x2c\x35\x33\x2e\x31\ -\x63\x2d\x31\x2e\x32\x34\x2e\x37\x34\x2d\x32\x2e\x34\x2d\x2e\x38\ -\x37\x2d\x32\x2e\x39\x33\x2d\x31\x2e\x38\x43\x2d\x2e\x38\x32\x2c\ -\x34\x34\x2e\x32\x33\x2d\x2e\x37\x34\x2c\x33\x31\x2c\x31\x2e\x38\ -\x33\x2c\x32\x33\x2e\x34\x34\x63\x2e\x35\x39\x2d\x31\x2e\x36\x34\ -\x2c\x31\x2e\x34\x2d\x33\x2e\x36\x38\x2c\x33\x2e\x30\x35\x2d\x34\ -\x2e\x36\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\ -\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x34\x22\x20\x64\x3d\x22\x4d\x33\x2e\x35\x32\x2c\x33\x36\x2e\x34\ -\x37\x63\x30\x2d\x35\x2c\x2e\x33\x37\x2d\x39\x2e\x32\x33\x2c\x32\ -\x2e\x31\x37\x2d\x31\x33\x2e\x36\x31\x61\x2e\x34\x33\x2e\x34\x33\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x38\x2d\x2e\x32\x39\x71\x2e\ -\x39\x2c\x30\x2c\x31\x2e\x38\x2c\x30\x63\x2e\x32\x39\x2c\x30\x2c\ -\x2e\x33\x35\x2e\x30\x35\x2e\x32\x35\x2e\x33\x61\x32\x37\x2e\x33\ -\x35\x2c\x32\x37\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\ -\x33\x37\x2c\x35\x63\x2d\x31\x2e\x31\x37\x2c\x36\x2e\x39\x2d\x31\ -\x2e\x30\x38\x2c\x31\x34\x2e\x37\x36\x2c\x31\x2e\x33\x37\x2c\x32\ -\x31\x2e\x33\x38\x2e\x31\x2e\x32\x35\x2e\x30\x39\x2e\x33\x34\x2d\ -\x2e\x32\x35\x2e\x33\x33\x2d\x2e\x36\x2c\x30\x2d\x31\x2e\x32\x31\ -\x2c\x30\x2d\x31\x2e\x38\x31\x2c\x30\x61\x2e\x34\x32\x2e\x34\x32\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x34\x37\x2d\x2e\x33\x43\x34\x2c\ -\x34\x35\x2e\x33\x39\x2c\x33\x2e\x35\x32\x2c\x34\x30\x2e\x33\x38\ -\x2c\x33\x2e\x35\x32\x2c\x33\x36\x2e\x34\x37\x5a\x22\x20\x74\x72\ -\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\ -\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ -\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\ -\x31\x38\x2e\x33\x38\x2c\x32\x32\x2e\x35\x38\x63\x2e\x32\x38\x2c\ -\x30\x2c\x2e\x38\x31\x2d\x2e\x31\x36\x2c\x31\x2c\x2e\x30\x38\x73\ -\x2d\x2e\x31\x33\x2e\x36\x31\x2d\x2e\x32\x32\x2e\x39\x32\x61\x34\ -\x35\x2e\x36\x39\x2c\x34\x35\x2e\x36\x39\x2c\x30\x2c\x30\x2c\x30\ -\x2d\x31\x2e\x35\x32\x2c\x31\x37\x2e\x32\x39\x2c\x33\x39\x2e\x36\ -\x31\x2c\x33\x39\x2e\x36\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2e\ -\x37\x32\x2c\x38\x2e\x33\x31\x63\x2e\x31\x31\x2e\x33\x33\x2e\x30\ -\x35\x2e\x34\x31\x2d\x2e\x33\x33\x2e\x33\x39\x61\x31\x33\x2c\x31\ -\x33\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x35\x33\x2c\x30\x2c\x2e\ -\x34\x38\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x35\x34\x2d\ -\x2e\x33\x33\x2c\x32\x33\x2e\x38\x35\x2c\x32\x33\x2e\x38\x35\x2c\ -\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x31\x2d\x35\x2e\x33\x63\x2d\ -\x31\x2e\x30\x39\x2d\x36\x2e\x35\x38\x2d\x31\x2d\x31\x34\x2e\x37\ -\x34\x2c\x31\x2e\x34\x39\x2d\x32\x31\x61\x2e\x35\x2e\x35\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x2e\x36\x2d\x2e\x33\x37\x41\x37\x2e\x31\x33\ -\x2c\x37\x2e\x31\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x38\x2e\x33\ -\x38\x2c\x32\x32\x2e\x35\x38\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\ -\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\ -\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x37\x2e\x38\x37\ -\x2c\x33\x37\x2e\x32\x35\x61\x31\x31\x2e\x32\x38\x2c\x31\x31\x2e\ -\x32\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x30\x36\x2d\x32\x2e\x33\ -\x33\x63\x2e\x32\x37\x2d\x34\x2e\x30\x38\x2e\x35\x37\x2d\x38\x2e\ -\x32\x39\x2c\x32\x2e\x32\x35\x2d\x31\x32\x2e\x31\x32\x61\x2e\x33\ -\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x33\x38\x2d\x2e\ -\x32\x33\x63\x31\x2e\x33\x39\x2d\x2e\x30\x37\x2c\x31\x2e\x33\x38\ -\x2d\x2e\x30\x38\x2c\x31\x2c\x31\x43\x39\x2c\x33\x31\x2e\x31\x35\ -\x2c\x38\x2e\x39\x33\x2c\x34\x31\x2e\x37\x33\x2c\x31\x31\x2e\x36\ -\x38\x2c\x34\x39\x2e\x32\x63\x2e\x31\x32\x2e\x33\x31\x2e\x30\x35\ -\x2e\x33\x37\x2d\x2e\x33\x2e\x33\x37\x2d\x31\x2e\x32\x35\x2c\x30\ -\x2d\x31\x2e\x32\x36\x2c\x30\x2d\x31\x2e\x36\x34\x2d\x31\x41\x33\ -\x36\x2e\x30\x38\x2c\x33\x36\x2e\x30\x38\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x37\x2e\x38\x37\x2c\x33\x37\x2e\x32\x35\x5a\x22\x20\x74\x72\ -\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\ -\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ -\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\ -\x31\x31\x2e\x33\x35\x2c\x33\x36\x2e\x33\x37\x63\x30\x2d\x34\x2e\ -\x38\x36\x2e\x33\x38\x2d\x39\x2e\x31\x38\x2c\x32\x2e\x31\x37\x2d\ -\x31\x33\x2e\x35\x61\x2e\x34\x2e\x34\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x2e\x34\x37\x2d\x2e\x33\x63\x2e\x34\x2c\x30\x2c\x31\x2d\x2e\x31\ -\x35\x2c\x31\x2e\x31\x38\x2e\x30\x37\x73\x2d\x2e\x31\x37\x2e\x36\ -\x34\x2d\x2e\x32\x38\x2c\x31\x63\x2d\x32\x2e\x34\x31\x2c\x37\x2e\ -\x36\x31\x2d\x32\x2e\x35\x31\x2c\x31\x38\x2c\x2e\x32\x37\x2c\x32\ -\x35\x2e\x35\x37\x2e\x31\x34\x2e\x33\x36\x2c\x30\x2c\x2e\x33\x39\ -\x2d\x2e\x33\x35\x2e\x33\x39\x2d\x31\x2e\x31\x37\x2c\x30\x2d\x31\ -\x2e\x31\x39\x2c\x30\x2d\x31\x2e\x35\x37\x2d\x31\x41\x33\x35\x2e\ -\x37\x32\x2c\x33\x35\x2e\x37\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x31\ -\x31\x2e\x33\x35\x2c\x33\x36\x2e\x33\x37\x5a\x22\x20\x74\x72\x61\ -\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\ -\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x31\ -\x39\x2e\x31\x32\x2c\x32\x31\x2e\x34\x37\x63\x2e\x39\x32\x2c\x30\ -\x2c\x2e\x39\x32\x2c\x30\x2c\x31\x2e\x31\x33\x2d\x2e\x37\x38\x2e\ -\x38\x36\x2d\x32\x2e\x38\x36\x2d\x2e\x32\x35\x2d\x35\x2e\x34\x35\ -\x2d\x33\x2e\x35\x2d\x35\x2e\x38\x36\x2d\x31\x2e\x35\x31\x2d\x2e\ -\x32\x33\x2d\x31\x2e\x35\x2d\x2e\x32\x31\x2d\x31\x2e\x34\x38\x2c\ -\x31\x2e\x30\x39\x2c\x30\x2c\x2e\x32\x35\x2e\x30\x39\x2e\x33\x33\ -\x2e\x33\x37\x2e\x33\x34\x61\x34\x2e\x36\x38\x2c\x34\x2e\x36\x38\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x34\x39\x2e\x32\x32\x2c\x32\ -\x2e\x31\x31\x2c\x32\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x31\ -\x2e\x35\x38\x2c\x31\x2e\x37\x38\x41\x35\x2e\x37\x38\x2c\x35\x2e\ -\x37\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x38\x2e\x33\x33\x2c\x32\ -\x31\x63\x2d\x2e\x31\x35\x2e\x34\x33\x2d\x2e\x31\x35\x2e\x34\x33\ -\x2e\x34\x31\x2e\x34\x33\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\ -\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\ -\x22\x2f\x3e\x3c\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x63\x78\x3d\x22\x32\x34\ -\x2e\x34\x32\x22\x20\x63\x79\x3d\x22\x33\x36\x2e\x32\x35\x22\x20\ -\x72\x78\x3d\x22\x31\x2e\x30\x31\x22\x20\x72\x79\x3d\x22\x35\x2e\ -\x39\x39\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\ -\x76\x67\x3e\ +\x00\x00\x0aT\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 252.13 58.41\ +\x22>c\ +ostum\ +<\ +path class=\x22cls-\ +4\x22 d=\x22M3.52,36.4\ +7c0-5,.37-9.23,2\ +.17-13.61a.43.43\ +,0,0,1,.48-.29q.\ +9,0,1.8,0c.29,0,\ +.35.05.25.3a27.3\ +5,27.35,0,0,0-1.\ +37,5c-1.17,6.9-1\ +.08,14.76,1.37,2\ +1.38.1.25.09.34-\ +.25.33-.6,0-1.21\ +,0-1.81,0a.42.42\ +,0,0,1-.47-.3C4,\ +45.39,3.52,40.38\ +,3.52,36.47Z\x22 tr\ +ansform=\x22transla\ +te(0)\x22/>\ +\x00\x00\x0ab\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 196.37 58.41\ +\x22>p\ +e\ +tg\ +\x00\x00\x0a%\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 176.02 58.41\ +\x22>hips<\ +path class=\x22cls-\ +3\x22 d=\x22M5.64,18.1\ +4a5.13,5.13,0,0,\ +1,2,2.27c-.72,0-\ +1.39,0-2.06,0s-1\ +,.1-1.29.74C1,28\ +.48,1,41.19,3.92\ +,48.7c.15.36.33.\ +71.49,1.06a1,1,0\ +,0,0,1,.57H7.67A\ +5.07,5.07,0,0,1,\ +5.92,52.4c-1.24.\ +74-2.4-.88-2.93-\ +1.81C-.82,43.53-\ +.74,30.28,1.83,2\ +2.74c.59-1.64,1.\ +4-3.69,3.05-4.6Z\ +\x22 transform=\x22tra\ +nslate(0)\x22/><\ +path class=\x22cls-\ +3\x22 d=\x22M7.87,36.5\ +4a11.26,11.26,0,\ +0,1-.06-2.33c.27\ +-4.08.57-8.28,2.\ +25-12.11a.36.36,\ +0,0,1,.38-.23c1.\ +39-.08,1.38-.08,\ +1,1C9,30.45,8.93\ +,41,11.68,48.5c.\ +12.31.05.37-.3.3\ +6-1.25,0-1.26,0-\ +1.64-1A36.14,36.\ +14,0,0,1,7.87,36\ +.54Z\x22 transform=\ +\x22translate(0)\x22/>\ +<\ +path class=\x22cls-\ +3\x22 d=\x22M19.12,20.\ +77c.92,0,.92,0,1\ +.13-.78.86-2.86-\ +.25-5.45-3.5-5.8\ +7-1.51-.23-1.5-.\ +21-1.48,1.1,0,.2\ +5.09.33.37.33a5,\ +5,0,0,1,1.49.23,\ +2.11,2.11,0,0,1,\ +1.58,1.77,5.74,5\ +.74,0,0,1-.38,2.\ +78c-.15.44-.15.4\ +4.41.44Z\x22 transf\ +orm=\x22translate(0\ +)\x22/>\ \x00\x00\x0a\x14\ -\x3c\ -\x73\x76\x67\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\x31\x37\x36\x2e\x30\x32\x20\x35\x38\x2e\x34\x31\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\ -\x36\x35\x2e\x37\x38\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\ -\x69\x6c\x79\x3a\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\ -\x2c\x20\x4d\x6f\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\ -\x65\x69\x67\x68\x74\x3a\x32\x30\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x31\x2c\x2e\x63\x6c\x73\x2d\x33\x7b\x66\x69\x6c\x6c\x3a\x23\x66\ -\x66\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x6c\x65\x74\x74\x65\ -\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x31\x65\ -\x6d\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\ -\x73\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\ -\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\ -\x65\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\ -\x65\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\ -\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x74\x65\x78\ -\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x34\x37\x2e\x39\x31\x20\x34\x39\x2e\x39\x33\ -\x29\x22\x3e\x74\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x78\x3d\x22\x33\x33\x2e\x33\ -\x35\x22\x20\x79\x3d\x22\x30\x22\x3e\x70\x3c\x2f\x74\x73\x70\x61\ -\x6e\x3e\x3c\x74\x73\x70\x61\x6e\x20\x78\x3d\x22\x36\x31\x2e\x33\ -\x37\x22\x20\x79\x3d\x22\x30\x22\x3e\x75\x3c\x2f\x74\x73\x70\x61\ -\x6e\x3e\x3c\x2f\x74\x65\x78\x74\x3e\x3c\x70\x61\x74\x68\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\ -\x4d\x32\x34\x2e\x37\x38\x2c\x31\x38\x2e\x38\x34\x63\x32\x2e\x35\ -\x36\x2c\x31\x2e\x33\x35\x2c\x33\x2e\x37\x31\x2c\x36\x2e\x34\x34\ -\x2c\x34\x2e\x31\x38\x2c\x39\x2e\x30\x37\x2c\x31\x2e\x32\x2c\x36\ -\x2e\x35\x36\x2c\x31\x2e\x31\x34\x2c\x31\x39\x2e\x31\x2d\x33\x2e\ -\x30\x39\x2c\x32\x34\x2e\x35\x31\x2d\x2e\x38\x31\x2c\x31\x2d\x31\ -\x2e\x38\x32\x2c\x31\x2e\x32\x31\x2d\x32\x2e\x37\x34\x2e\x32\x32\ -\x61\x39\x2e\x31\x31\x2c\x39\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\ -\x2d\x31\x2e\x38\x35\x2d\x33\x2e\x31\x31\x63\x2d\x32\x2e\x38\x33\ -\x2d\x37\x2e\x36\x2d\x32\x2e\x38\x38\x2d\x31\x39\x2e\x32\x39\x2c\ -\x30\x2d\x32\x36\x2e\x39\x41\x37\x2e\x30\x36\x2c\x37\x2e\x30\x36\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x34\x2c\x31\x38\x2e\x38\x34\x5a\ -\x4d\x32\x33\x2e\x35\x33\x2c\x35\x30\x2e\x34\x32\x61\x2e\x38\x38\ -\x2e\x38\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x36\x2e\x36\x2e\ -\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x39\x2d\ -\x2e\x36\x32\x63\x33\x2e\x30\x39\x2d\x36\x2e\x35\x34\x2c\x33\x2e\ -\x31\x33\x2d\x31\x36\x2e\x35\x35\x2c\x31\x2e\x36\x39\x2d\x32\x33\ -\x2e\x35\x37\x61\x32\x30\x2e\x32\x37\x2c\x32\x30\x2e\x32\x37\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x37\x2d\x35\x2e\x31\x32\x2e\x39\ -\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x38\x35\x2d\x2e\ -\x36\x2e\x39\x32\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\ -\x2e\x36\x31\x2c\x31\x35\x2c\x31\x35\x2c\x30\x2c\x30\x2c\x30\x2d\ -\x31\x2e\x33\x2c\x33\x2e\x34\x39\x43\x32\x30\x2e\x35\x32\x2c\x33\ -\x32\x2e\x33\x34\x2c\x31\x39\x2e\x34\x31\x2c\x34\x31\x2e\x31\x31\ -\x2c\x32\x33\x2e\x35\x33\x2c\x35\x30\x2e\x34\x32\x5a\x22\x20\x74\ -\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\ -\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\ -\x4d\x35\x2e\x36\x34\x2c\x31\x38\x2e\x38\x34\x61\x35\x2e\x31\x39\ -\x2c\x35\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2c\x32\x2e\ -\x32\x37\x48\x35\x2e\x36\x31\x63\x2d\x2e\x38\x2c\x30\x2d\x31\x2c\ -\x2e\x31\x31\x2d\x31\x2e\x32\x39\x2e\x37\x34\x43\x31\x2c\x32\x39\ -\x2e\x31\x38\x2c\x31\x2c\x34\x31\x2e\x39\x2c\x33\x2e\x39\x32\x2c\ -\x34\x39\x2e\x34\x63\x2e\x31\x35\x2e\x33\x36\x2e\x33\x33\x2e\x37\ -\x31\x2e\x34\x39\x2c\x31\x2e\x30\x36\x61\x31\x2c\x31\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x2e\x35\x37\x48\x37\x2e\x36\x37\x41\x35\ -\x2c\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x2e\x39\x32\x2c\x35\x33\ -\x2e\x31\x63\x2d\x31\x2e\x32\x34\x2e\x37\x34\x2d\x32\x2e\x34\x2d\ -\x2e\x38\x37\x2d\x32\x2e\x39\x33\x2d\x31\x2e\x38\x43\x2d\x2e\x38\ -\x32\x2c\x34\x34\x2e\x32\x33\x2d\x2e\x37\x34\x2c\x33\x31\x2c\x31\ -\x2e\x38\x33\x2c\x32\x33\x2e\x34\x34\x63\x2e\x35\x39\x2d\x31\x2e\ -\x36\x34\x2c\x31\x2e\x34\x2d\x33\x2e\x36\x38\x2c\x33\x2e\x30\x35\ -\x2d\x34\x2e\x36\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\ -\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x33\x2e\x35\x32\x2c\x33\x36\ -\x2e\x34\x37\x63\x30\x2d\x35\x2c\x2e\x33\x37\x2d\x39\x2e\x32\x33\ -\x2c\x32\x2e\x31\x37\x2d\x31\x33\x2e\x36\x31\x61\x2e\x34\x33\x2e\ -\x34\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x38\x2d\x2e\x32\x39\ -\x71\x2e\x39\x2c\x30\x2c\x31\x2e\x38\x2c\x30\x63\x2e\x32\x39\x2c\ -\x30\x2c\x2e\x33\x35\x2e\x30\x35\x2e\x32\x35\x2e\x33\x61\x32\x37\ -\x2e\x33\x35\x2c\x32\x37\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\ -\x31\x2e\x33\x37\x2c\x35\x63\x2d\x31\x2e\x31\x37\x2c\x36\x2e\x39\ -\x2d\x31\x2e\x30\x38\x2c\x31\x34\x2e\x37\x36\x2c\x31\x2e\x33\x37\ -\x2c\x32\x31\x2e\x33\x38\x2e\x31\x2e\x32\x35\x2e\x30\x39\x2e\x33\ -\x34\x2d\x2e\x32\x35\x2e\x33\x33\x2d\x2e\x36\x2c\x30\x2d\x31\x2e\ -\x32\x31\x2c\x30\x2d\x31\x2e\x38\x31\x2c\x30\x61\x2e\x34\x32\x2e\ -\x34\x32\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x34\x37\x2d\x2e\x33\x43\ -\x34\x2c\x34\x35\x2e\x33\x39\x2c\x33\x2e\x35\x32\x2c\x34\x30\x2e\ -\x33\x38\x2c\x33\x2e\x35\x32\x2c\x33\x36\x2e\x34\x37\x5a\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\ -\x22\x4d\x31\x38\x2e\x33\x38\x2c\x32\x32\x2e\x35\x38\x63\x2e\x32\ -\x38\x2c\x30\x2c\x2e\x38\x31\x2d\x2e\x31\x36\x2c\x31\x2c\x2e\x30\ -\x38\x73\x2d\x2e\x31\x33\x2e\x36\x31\x2d\x2e\x32\x32\x2e\x39\x32\ -\x61\x34\x35\x2e\x37\x32\x2c\x34\x35\x2e\x37\x32\x2c\x30\x2c\x30\ -\x2c\x30\x2d\x31\x2e\x35\x32\x2c\x31\x37\x2e\x33\x2c\x33\x39\x2e\ -\x36\x35\x2c\x33\x39\x2e\x36\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x31\ -\x2e\x37\x32\x2c\x38\x2e\x33\x63\x2e\x31\x31\x2e\x33\x33\x2e\x30\ -\x35\x2e\x34\x31\x2d\x2e\x33\x33\x2e\x33\x39\x61\x31\x33\x2c\x31\ -\x33\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x35\x33\x2c\x30\x2c\x2e\ -\x34\x38\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x35\x34\x2d\ -\x2e\x33\x33\x2c\x32\x33\x2e\x38\x35\x2c\x32\x33\x2e\x38\x35\x2c\ -\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x31\x2d\x35\x2e\x33\x63\x2d\ -\x31\x2e\x30\x39\x2d\x36\x2e\x35\x38\x2d\x31\x2d\x31\x34\x2e\x37\ -\x34\x2c\x31\x2e\x34\x39\x2d\x32\x31\x61\x2e\x35\x2e\x35\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x2e\x36\x2d\x2e\x33\x37\x41\x37\x2e\x31\x33\ -\x2c\x37\x2e\x31\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x38\x2e\x33\ -\x38\x2c\x32\x32\x2e\x35\x38\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\ -\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\ -\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x37\x2e\x38\x37\ -\x2c\x33\x37\x2e\x32\x35\x61\x31\x31\x2e\x32\x38\x2c\x31\x31\x2e\ -\x32\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x30\x36\x2d\x32\x2e\x33\ -\x33\x63\x2e\x32\x37\x2d\x34\x2e\x30\x38\x2e\x35\x37\x2d\x38\x2e\ -\x32\x39\x2c\x32\x2e\x32\x35\x2d\x31\x32\x2e\x31\x32\x61\x2e\x33\ -\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x33\x38\x2d\x2e\ -\x32\x33\x63\x31\x2e\x33\x39\x2d\x2e\x30\x37\x2c\x31\x2e\x33\x38\ -\x2d\x2e\x30\x38\x2c\x31\x2c\x31\x43\x39\x2c\x33\x31\x2e\x31\x35\ -\x2c\x38\x2e\x39\x33\x2c\x34\x31\x2e\x37\x33\x2c\x31\x31\x2e\x36\ -\x38\x2c\x34\x39\x2e\x32\x63\x2e\x31\x32\x2e\x33\x31\x2e\x30\x35\ -\x2e\x33\x37\x2d\x2e\x33\x2e\x33\x37\x2d\x31\x2e\x32\x35\x2c\x30\ -\x2d\x31\x2e\x32\x36\x2c\x30\x2d\x31\x2e\x36\x34\x2d\x31\x41\x33\ -\x36\x2e\x30\x38\x2c\x33\x36\x2e\x30\x38\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x37\x2e\x38\x37\x2c\x33\x37\x2e\x32\x35\x5a\x22\x20\x74\x72\ -\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\ -\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ -\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\ -\x31\x31\x2e\x33\x35\x2c\x33\x36\x2e\x33\x37\x63\x30\x2d\x34\x2e\ -\x38\x36\x2e\x33\x38\x2d\x39\x2e\x31\x38\x2c\x32\x2e\x31\x37\x2d\ -\x31\x33\x2e\x35\x61\x2e\x34\x2e\x34\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x2e\x34\x37\x2d\x2e\x33\x63\x2e\x34\x2c\x30\x2c\x31\x2d\x2e\x31\ -\x35\x2c\x31\x2e\x31\x38\x2e\x30\x37\x73\x2d\x2e\x31\x37\x2e\x36\ -\x34\x2d\x2e\x32\x38\x2c\x31\x63\x2d\x32\x2e\x34\x31\x2c\x37\x2e\ -\x36\x31\x2d\x32\x2e\x35\x31\x2c\x31\x38\x2c\x2e\x32\x37\x2c\x32\ -\x35\x2e\x35\x37\x2e\x31\x34\x2e\x33\x36\x2c\x30\x2c\x2e\x33\x39\ -\x2d\x2e\x33\x35\x2e\x33\x39\x2d\x31\x2e\x31\x37\x2c\x30\x2d\x31\ -\x2e\x31\x39\x2c\x30\x2d\x31\x2e\x35\x37\x2d\x31\x41\x33\x35\x2e\ -\x37\x32\x2c\x33\x35\x2e\x37\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x31\ -\x31\x2e\x33\x35\x2c\x33\x36\x2e\x33\x37\x5a\x22\x20\x74\x72\x61\ -\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\ -\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x31\ -\x39\x2e\x31\x32\x2c\x32\x31\x2e\x34\x37\x63\x2e\x39\x32\x2c\x30\ -\x2c\x2e\x39\x32\x2c\x30\x2c\x31\x2e\x31\x33\x2d\x2e\x37\x38\x2e\ -\x38\x36\x2d\x32\x2e\x38\x36\x2d\x2e\x32\x35\x2d\x35\x2e\x34\x35\ -\x2d\x33\x2e\x35\x2d\x35\x2e\x38\x36\x2d\x31\x2e\x35\x31\x2d\x2e\ -\x32\x33\x2d\x31\x2e\x35\x2d\x2e\x32\x31\x2d\x31\x2e\x34\x38\x2c\ -\x31\x2e\x30\x39\x2c\x30\x2c\x2e\x32\x35\x2e\x30\x39\x2e\x33\x33\ -\x2e\x33\x37\x2e\x33\x34\x61\x34\x2e\x36\x38\x2c\x34\x2e\x36\x38\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x34\x39\x2e\x32\x32\x2c\x32\ -\x2e\x31\x31\x2c\x32\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x31\ -\x2e\x35\x38\x2c\x31\x2e\x37\x38\x41\x35\x2e\x37\x38\x2c\x35\x2e\ -\x37\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x38\x2e\x33\x33\x2c\x32\ -\x31\x63\x2d\x2e\x31\x35\x2e\x34\x33\x2d\x2e\x31\x35\x2e\x34\x33\ -\x2e\x34\x31\x2e\x34\x33\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\ -\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\ -\x22\x2f\x3e\x3c\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x63\x78\x3d\x22\x32\x34\ -\x2e\x34\x32\x22\x20\x63\x79\x3d\x22\x33\x36\x2e\x32\x35\x22\x20\ -\x72\x78\x3d\x22\x31\x2e\x30\x31\x22\x20\x72\x79\x3d\x22\x35\x2e\ -\x39\x39\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\ -\x76\x67\x3e\ -\x00\x00\x01\x21\ -\x3c\ -\x73\x76\x67\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\x33\x38\x2e\x32\x32\x20\x33\x34\x2e\x35\x37\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\x66\x66\x66\x3b\x7d\ -\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\ -\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\ -\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\ -\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\ -\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\ -\x4c\x61\x79\x65\x72\x20\x31\x22\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\x30\x2c\x30\x56\x32\x31\x2e\x36\x6c\x31\x39\x2e\x31\x31\x2c\ -\x31\x33\x2c\x31\x39\x2e\x31\x31\x2d\x31\x33\x56\x30\x5a\x4d\x33\ -\x35\x2e\x32\x2c\x31\x39\x2e\x34\x34\x6c\x2d\x39\x2e\x35\x35\x2c\ -\x36\x2e\x34\x39\x56\x38\x2e\x36\x34\x48\x33\x35\x2e\x32\x5a\x22\ -\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\x67\x3e\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 176.02 58.41\ +\x22>tpu\ +\x00\x00\x07\xa4\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 40.86 40.86\x22\ +><\ +g id=\x22Layer_2\x22 d\ +ata-name=\x22Layer \ +2\x22>\ +\x00\x00\x08s\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 88.32 34.24\x22\ +>a\ +bs<\ +ellipse class=\x22c\ +ls-4\x22 cx=\x2224.42\x22\ + cy=\x2235.54\x22 rx=\x22\ +1.01\x22 ry=\x225.99\x22/\ +>\ +\x00\x00\x01a\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 79.53 48.16\x22\ +>\ \ \x00\x00\x0a\x9a\ -\x3c\ -\x73\x76\x67\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\x31\x38\x34\x2e\x31\x36\x20\x35\x37\x2e\x37\x36\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x2c\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\ -\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x32\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\x36\x35\x2e\x30\ -\x35\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\x69\x6c\x79\x3a\ -\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\x2c\x20\x4d\x6f\ -\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\x65\x69\x67\x68\ -\x74\x3a\x32\x30\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x6c\x65\ -\x74\x74\x65\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\ -\x30\x32\x65\x6d\x3b\x7d\x2e\x63\x6c\x73\x2d\x34\x7b\x6c\x65\x74\ -\x74\x65\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\ -\x33\x65\x6d\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ -\x65\x66\x73\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\ -\x5f\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\ -\x61\x79\x65\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\ -\x61\x79\x65\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\ -\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\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\x2e\x37\x35\x2c\x32\x32\x2e\x33\x35\ -\x63\x31\x2e\x35\x35\x2d\x32\x2e\x39\x31\x2c\x37\x2e\x34\x2d\x34\ -\x2e\x32\x31\x2c\x31\x30\x2e\x34\x32\x2d\x34\x2e\x37\x35\x2c\x37\ -\x2e\x35\x35\x2d\x31\x2e\x33\x36\x2c\x32\x32\x2d\x31\x2e\x33\x2c\ -\x32\x38\x2e\x31\x39\x2c\x33\x2e\x35\x31\x2c\x31\x2e\x31\x33\x2e\ -\x39\x32\x2c\x31\x2e\x34\x2c\x32\x2e\x30\x37\x2e\x32\x36\x2c\x33\ -\x2e\x31\x32\x41\x31\x30\x2e\x33\x36\x2c\x31\x30\x2e\x33\x36\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x34\x30\x2c\x32\x36\x2e\x33\x33\x63\x2d\ -\x38\x2e\x37\x34\x2c\x33\x2e\x32\x32\x2d\x32\x32\x2e\x31\x38\x2c\ -\x33\x2e\x32\x38\x2d\x33\x30\x2e\x39\x34\x2c\x30\x2d\x31\x2e\x36\ -\x33\x2d\x2e\x36\x35\x2d\x33\x2e\x34\x35\x2d\x31\x2e\x34\x39\x2d\ -\x34\x2e\x33\x35\x2d\x33\x2e\x31\x33\x5a\x6d\x33\x36\x2e\x33\x31\ -\x2c\x31\x2e\x34\x33\x61\x31\x2c\x31\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x2e\x37\x2d\x31\x2c\x31\x2e\x30\x38\x2c\x31\x2e\x30\x38\x2c\x30\ -\x2c\x30\x2c\x30\x2d\x2e\x37\x31\x2d\x31\x63\x2d\x37\x2e\x35\x33\ -\x2d\x33\x2e\x35\x31\x2d\x31\x39\x2d\x33\x2e\x35\x36\x2d\x32\x37\ -\x2e\x31\x31\x2d\x31\x2e\x39\x32\x61\x32\x33\x2e\x36\x34\x2c\x32\ -\x33\x2e\x36\x34\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\x38\x39\x2c\ -\x31\x2e\x39\x33\x2c\x31\x2c\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\ -\x36\x39\x2c\x31\x2c\x31\x2e\x30\x36\x2c\x31\x2e\x30\x36\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x2e\x37\x2c\x31\x2c\x31\x37\x2e\x39\x34\x2c\ -\x31\x37\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x2c\x31\x2e\ -\x34\x38\x43\x32\x30\x2e\x32\x37\x2c\x32\x37\x2e\x32\x2c\x33\x30\ -\x2e\x33\x36\x2c\x32\x38\x2e\x34\x36\x2c\x34\x31\x2e\x30\x36\x2c\ -\x32\x33\x2e\x37\x38\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\ -\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\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\x2e\x37\x35\x2c\x34\ -\x34\x2e\x31\x32\x61\x35\x2e\x38\x38\x2c\x35\x2e\x38\x38\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x32\x2e\x36\x2d\x32\x2e\x33\x31\x76\x32\x2e\ -\x33\x35\x63\x30\x2c\x2e\x39\x2e\x31\x33\x2c\x31\x2e\x31\x32\x2e\ -\x38\x35\x2c\x31\x2e\x34\x36\x2c\x38\x2e\x34\x33\x2c\x33\x2e\x38\ -\x2c\x32\x33\x2e\x30\x36\x2c\x33\x2e\x37\x38\x2c\x33\x31\x2e\x36\ -\x39\x2e\x34\x35\x2e\x34\x32\x2d\x2e\x31\x36\x2e\x38\x32\x2d\x2e\ -\x33\x36\x2c\x31\x2e\x32\x32\x2d\x2e\x35\x35\x61\x31\x2e\x31\x34\ -\x2c\x31\x2e\x31\x34\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x36\x36\x2d\ -\x31\x2e\x31\x38\x63\x30\x2d\x2e\x38\x38\x2c\x30\x2d\x31\x2e\x36\ -\x2c\x30\x2d\x32\x2e\x35\x33\x61\x35\x2e\x38\x2c\x35\x2e\x38\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x32\x2e\x33\x39\x2c\x32\x63\x2e\x38\x35\ -\x2c\x31\x2e\x34\x31\x2d\x31\x2c\x32\x2e\x37\x33\x2d\x32\x2e\x30\ -\x38\x2c\x33\x2e\x33\x32\x2d\x38\x2e\x31\x33\x2c\x34\x2e\x33\x34\ -\x2d\x32\x33\x2e\x33\x36\x2c\x34\x2e\x32\x34\x2d\x33\x32\x2c\x31\ -\x2e\x33\x32\x43\x38\x2e\x31\x35\x2c\x34\x37\x2e\x37\x39\x2c\x35\ -\x2e\x38\x2c\x34\x36\x2e\x38\x36\x2c\x34\x2e\x37\x35\x2c\x34\x35\ -\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\ -\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\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\x32\x35\x2c\x34\x36\x2e\x35\x33\x63\x2d\x35\ -\x2e\x37\x36\x2d\x2e\x30\x35\x2d\x31\x30\x2e\x36\x31\x2d\x2e\x34\ -\x32\x2d\x31\x35\x2e\x36\x35\x2d\x32\x2e\x34\x37\x41\x2e\x34\x37\ -\x2e\x34\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x39\x2c\x34\x33\x2e\x35\ -\x32\x63\x30\x2d\x2e\x36\x38\x2c\x30\x2d\x31\x2e\x33\x37\x2c\x30\ -\x2d\x32\x2e\x30\x35\x2c\x30\x2d\x2e\x33\x33\x2e\x30\x35\x2d\x2e\ -\x34\x2e\x33\x34\x2d\x2e\x32\x39\x61\x33\x30\x2e\x34\x39\x2c\x33\ -\x30\x2e\x34\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x35\x2e\x37\x34\x2c\ -\x31\x2e\x35\x36\x63\x37\x2e\x39\x33\x2c\x31\x2e\x33\x33\x2c\x31\ -\x37\x2c\x31\x2e\x32\x33\x2c\x32\x34\x2e\x35\x39\x2d\x31\x2e\x35\ -\x36\x2e\x32\x38\x2d\x2e\x31\x2e\x33\x39\x2d\x2e\x31\x2e\x33\x38\ -\x2e\x32\x39\x2c\x30\x2c\x2e\x36\x39\x2c\x30\x2c\x31\x2e\x33\x37\ -\x2c\x30\x2c\x32\x2e\x30\x36\x61\x2e\x34\x37\x2e\x34\x37\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x33\x35\x2e\x35\x33\x43\x33\x35\x2e\x32\ -\x38\x2c\x34\x36\x2c\x32\x39\x2e\x35\x32\x2c\x34\x36\x2e\x35\x33\ -\x2c\x32\x35\x2c\x34\x36\x2e\x35\x33\x5a\x22\x20\x74\x72\x61\x6e\ -\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\ -\x28\x30\x29\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\x39\x2c\ -\x32\x39\x2e\x36\x34\x63\x2e\x30\x35\x2d\x2e\x33\x33\x2d\x2e\x31\ -\x38\x2d\x2e\x39\x33\x2e\x31\x2d\x31\x2e\x31\x36\x73\x2e\x37\x2e\ -\x31\x34\x2c\x31\x2e\x30\x36\x2e\x32\x34\x61\x35\x32\x2e\x37\x38\ -\x2c\x35\x32\x2e\x37\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x39\x2e\ -\x38\x39\x2c\x31\x2e\x37\x33\x2c\x34\x35\x2e\x36\x35\x2c\x34\x35\ -\x2e\x36\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x39\x2e\x35\x35\x2d\x32\ -\x63\x2e\x33\x37\x2d\x2e\x31\x32\x2e\x34\x36\x2d\x2e\x30\x36\x2e\ -\x34\x34\x2e\x33\x38\x61\x31\x36\x2e\x36\x32\x2c\x31\x36\x2e\x36\ -\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2e\x37\x33\x2e\x35\ -\x35\x2e\x35\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x33\x38\x2e\x36\ -\x32\x2c\x32\x37\x2e\x34\x37\x2c\x32\x37\x2e\x34\x37\x2c\x30\x2c\ -\x30\x2c\x31\x2d\x36\x2e\x31\x2c\x31\x2e\x37\x32\x43\x32\x36\x2c\ -\x33\x34\x2e\x31\x38\x2c\x31\x36\x2e\x36\x36\x2c\x33\x34\x2e\x30\ -\x39\x2c\x39\x2e\x34\x36\x2c\x33\x31\x2e\x32\x34\x41\x2e\x35\x35\ -\x2e\x35\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x39\x2c\x33\x30\x2e\x35\ -\x36\x43\x39\x2e\x30\x37\x2c\x33\x30\x2e\x33\x2c\x39\x2c\x33\x30\ -\x2c\x39\x2c\x32\x39\x2e\x36\x34\x5a\x22\x20\x74\x72\x61\x6e\x73\ -\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\ -\x30\x29\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\x32\x35\x2e\ -\x39\x31\x2c\x34\x31\x2e\x35\x38\x61\x31\x32\x2e\x35\x34\x2c\x31\ -\x32\x2e\x35\x34\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x36\x38\x2e\ -\x30\x37\x63\x2d\x34\x2e\x36\x39\x2d\x2e\x33\x2d\x39\x2e\x35\x33\ -\x2d\x2e\x36\x35\x2d\x31\x33\x2e\x39\x33\x2d\x32\x2e\x35\x36\x41\ -\x2e\x34\x31\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x39\x2c\x33\ -\x38\x2e\x36\x36\x63\x2d\x2e\x30\x39\x2d\x31\x2e\x35\x38\x2d\x2e\ -\x31\x2d\x31\x2e\x35\x37\x2c\x31\x2e\x31\x39\x2d\x31\x2e\x31\x32\ -\x2c\x38\x2e\x36\x37\x2c\x32\x2e\x38\x31\x2c\x32\x30\x2e\x38\x34\ -\x2c\x32\x2e\x38\x34\x2c\x32\x39\x2e\x34\x34\x2d\x2e\x32\x39\x2e\ -\x33\x35\x2d\x2e\x31\x34\x2e\x34\x31\x2d\x2e\x30\x36\x2e\x34\x31\ -\x2e\x33\x35\x2c\x30\x2c\x31\x2e\x34\x31\x2c\x30\x2c\x31\x2e\x34\ -\x32\x2d\x31\x2e\x31\x36\x2c\x31\x2e\x38\x36\x41\x34\x31\x2e\x38\ -\x36\x2c\x34\x31\x2e\x38\x36\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x35\ -\x2e\x39\x31\x2c\x34\x31\x2e\x35\x38\x5a\x22\x20\x74\x72\x61\x6e\ -\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\ -\x28\x30\x29\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\x32\x34\ -\x2e\x39\x2c\x33\x37\x2e\x36\x33\x63\x2d\x35\x2e\x35\x39\x2c\x30\ -\x2d\x31\x30\x2e\x35\x35\x2d\x2e\x34\x34\x2d\x31\x35\x2e\x35\x32\ -\x2d\x32\x2e\x34\x37\x2d\x2e\x32\x34\x2d\x2e\x31\x31\x2d\x2e\x33\ -\x37\x2d\x2e\x32\x32\x2d\x2e\x33\x34\x2d\x2e\x35\x33\x2c\x30\x2d\ -\x2e\x34\x37\x2d\x2e\x31\x37\x2d\x31\x2e\x31\x32\x2e\x30\x38\x2d\ -\x31\x2e\x33\x35\x73\x2e\x37\x33\x2e\x32\x2c\x31\x2e\x31\x31\x2e\ -\x33\x32\x43\x31\x39\x2c\x33\x36\x2e\x33\x34\x2c\x33\x31\x2c\x33\ -\x36\x2e\x34\x36\x2c\x33\x39\x2e\x36\x33\x2c\x33\x33\x2e\x32\x39\ -\x63\x2e\x34\x32\x2d\x2e\x31\x36\x2e\x34\x36\x2c\x30\x2c\x2e\x34\ -\x35\x2e\x34\x2c\x30\x2c\x31\x2e\x33\x34\x2c\x30\x2c\x31\x2e\x33\ -\x35\x2d\x31\x2e\x31\x32\x2c\x31\x2e\x37\x38\x41\x34\x31\x2e\x35\ -\x31\x2c\x34\x31\x2e\x35\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x34\ -\x2e\x39\x2c\x33\x37\x2e\x36\x33\x5a\x22\x20\x74\x72\x61\x6e\x73\ -\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\ -\x30\x29\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\x37\x2e\x37\ -\x37\x2c\x32\x38\x2e\x37\x39\x63\x30\x2d\x31\x2c\x30\x2d\x31\x2e\ -\x30\x35\x2d\x2e\x38\x39\x2d\x31\x2e\x32\x39\x2d\x33\x2e\x33\x2d\ -\x31\x2d\x36\x2e\x32\x38\x2e\x32\x39\x2d\x36\x2e\x37\x35\x2c\x34\ -\x2d\x2e\x32\x37\x2c\x31\x2e\x37\x31\x2d\x2e\x32\x34\x2c\x31\x2e\ -\x37\x31\x2c\x31\x2e\x32\x36\x2c\x31\x2e\x36\x38\x2e\x32\x39\x2c\ -\x30\x2c\x2e\x33\x38\x2d\x2e\x31\x2e\x33\x38\x2d\x2e\x34\x32\x41\ -\x35\x2e\x36\x33\x2c\x35\x2e\x36\x33\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x32\x2c\x33\x31\x2e\x30\x36\x61\x32\x2e\x34\x2c\x32\x2e\x34\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x32\x2d\x31\x2e\x38\x2c\x36\x2e\x37\x33\ -\x2c\x36\x2e\x37\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x2e\x32\x2e\ -\x34\x32\x63\x2e\x35\x2e\x31\x38\x2e\x35\x2e\x31\x38\x2e\x35\x2d\ -\x2e\x34\x35\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\ -\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\ -\x3c\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x31\x22\x20\x63\x78\x3d\x22\x32\x34\x2e\x37\x36\ -\x22\x20\x63\x79\x3d\x22\x32\x32\x2e\x37\x37\x22\x20\x72\x78\x3d\ -\x22\x36\x2e\x38\x39\x22\x20\x72\x79\x3d\x22\x31\x2e\x31\x35\x22\ -\x2f\x3e\x3c\x74\x65\x78\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x32\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\ -\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x35\x36\x2e\x30\x34\ -\x20\x34\x39\x2e\x33\x37\x29\x20\x73\x63\x61\x6c\x65\x28\x31\x2e\ -\x30\x31\x20\x31\x29\x22\x3e\x6e\x3c\x74\x73\x70\x61\x6e\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x78\x3d\x22\ -\x33\x32\x2e\x39\x31\x22\x20\x79\x3d\x22\x30\x22\x3e\x2f\x3c\x2f\ -\x74\x73\x70\x61\x6e\x3e\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x78\x3d\x22\x35\x30\ -\x2e\x34\x38\x22\x20\x79\x3d\x22\x30\x22\x3e\x61\x3c\x2f\x74\x73\ -\x70\x61\x6e\x3e\x3c\x2f\x74\x65\x78\x74\x3e\x3c\x2f\x67\x3e\x3c\ -\x2f\x67\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x02\x53\ -\x3c\ -\x73\x76\x67\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\x31\x33\x2e\x38\x36\x20\x31\x32\x2e\x32\x37\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\x66\x66\x66\x3b\x7d\ -\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\ -\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\ -\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\ -\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\ -\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\ -\x4c\x61\x79\x65\x72\x20\x31\x22\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\x30\x2c\x34\x2e\x31\x33\x41\x31\x2e\x38\x37\x2c\x31\x2e\x38\ -\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x35\x38\x2c\x32\x2e\x37\x36\ -\x2c\x38\x2e\x36\x34\x2c\x38\x2e\x36\x34\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x35\x2e\x36\x36\x2e\x31\x61\x38\x2e\x35\x37\x2c\x38\x2e\x35\ -\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x35\x39\x2c\x32\x2e\x36\ -\x33\x41\x31\x2e\x39\x31\x2c\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x31\x33\x2e\x37\x2c\x34\x2e\x39\x2c\x31\x2e\x36\x33\x2c\ -\x31\x2e\x36\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x31\x2c\x35\x2e\ -\x34\x32\x2c\x35\x2e\x35\x35\x2c\x35\x2e\x35\x35\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x38\x2e\x31\x35\x2c\x33\x2e\x37\x36\x2c\x35\x2e\x33\ -\x34\x2c\x35\x2e\x33\x34\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x2e\x39\ -\x34\x2c\x35\x2e\x33\x37\x61\x31\x2e\x36\x36\x2c\x31\x2e\x36\x36\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x38\x38\x2d\x2e\x38\x41\x33\ -\x2e\x31\x31\x2c\x33\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x30\ -\x2c\x34\x2e\x31\x33\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\x36\x2e\x39\x33\x2c\x31\x32\x2e\x32\x37\x41\x32\x2e\x34\x2c\ -\x32\x2e\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x2e\x35\x39\x2c\x39\ -\x2e\x37\x34\x2c\x32\x2e\x34\x31\x2c\x32\x2e\x34\x31\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x36\x2e\x39\x32\x2c\x37\x2e\x32\x33\x2c\x32\x2e\ -\x34\x31\x2c\x32\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x39\x2e\ -\x32\x38\x2c\x39\x2e\x37\x34\x2c\x32\x2e\x34\x2c\x32\x2e\x34\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x36\x2e\x39\x33\x2c\x31\x32\x2e\x32\x37\ -\x5a\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\ -\x67\x3e\ -\x00\x00\x0a\x25\ -\x3c\ -\x73\x76\x67\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\x31\x37\x36\x2e\x30\x32\x20\x35\x38\x2e\x34\x31\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\ -\x36\x35\x2e\x37\x38\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\ -\x69\x6c\x79\x3a\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\ -\x2c\x20\x4d\x6f\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\ -\x65\x69\x67\x68\x74\x3a\x32\x30\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x31\x2c\x2e\x63\x6c\x73\x2d\x33\x7b\x66\x69\x6c\x6c\x3a\x23\x66\ -\x66\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x6c\x65\x74\x74\x65\ -\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x33\x65\ -\x6d\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\ -\x73\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\ -\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\ -\x65\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\ -\x65\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\ -\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x74\x65\x78\ -\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x34\x37\x2e\x39\x31\x20\x34\x39\x2e\x39\x33\ -\x29\x22\x3e\x68\x69\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x78\x3d\x22\x33\x39\x2e\ -\x37\x33\x22\x20\x79\x3d\x22\x30\x22\x3e\x70\x3c\x2f\x74\x73\x70\ -\x61\x6e\x3e\x3c\x74\x73\x70\x61\x6e\x20\x78\x3d\x22\x36\x36\x2e\ -\x34\x33\x22\x20\x79\x3d\x22\x30\x22\x3e\x73\x3c\x2f\x74\x73\x70\ -\x61\x6e\x3e\x3c\x2f\x74\x65\x78\x74\x3e\x3c\x70\x61\x74\x68\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\ -\x22\x4d\x32\x34\x2e\x37\x38\x2c\x31\x38\x2e\x31\x34\x63\x32\x2e\ -\x35\x36\x2c\x31\x2e\x33\x35\x2c\x33\x2e\x37\x31\x2c\x36\x2e\x34\ -\x34\x2c\x34\x2e\x31\x38\x2c\x39\x2e\x30\x36\x2c\x31\x2e\x32\x2c\ -\x36\x2e\x35\x36\x2c\x31\x2e\x31\x34\x2c\x31\x39\x2e\x31\x2d\x33\ -\x2e\x30\x39\x2c\x32\x34\x2e\x35\x31\x2d\x2e\x38\x31\x2c\x31\x2d\ -\x31\x2e\x38\x32\x2c\x31\x2e\x32\x32\x2d\x32\x2e\x37\x34\x2e\x32\ -\x33\x61\x39\x2e\x31\x31\x2c\x39\x2e\x31\x31\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x31\x2e\x38\x35\x2d\x33\x2e\x31\x31\x63\x2d\x32\x2e\x38\ -\x33\x2d\x37\x2e\x36\x2d\x32\x2e\x38\x38\x2d\x31\x39\x2e\x32\x39\ -\x2c\x30\x2d\x32\x36\x2e\x39\x2e\x35\x38\x2d\x31\x2e\x34\x32\x2c\ -\x31\x2e\x33\x31\x2d\x33\x2c\x32\x2e\x37\x35\x2d\x33\x2e\x37\x39\ -\x5a\x4d\x32\x33\x2e\x35\x33\x2c\x34\x39\x2e\x37\x32\x61\x2e\x38\ -\x38\x2e\x38\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x36\x2e\x36\ -\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x39\ -\x2d\x2e\x36\x32\x63\x33\x2e\x30\x39\x2d\x36\x2e\x35\x34\x2c\x33\ -\x2e\x31\x33\x2d\x31\x36\x2e\x35\x35\x2c\x31\x2e\x36\x39\x2d\x32\ -\x33\x2e\x35\x37\x41\x32\x30\x2e\x32\x37\x2c\x32\x30\x2e\x32\x37\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x35\x2e\x32\x37\x2c\x32\x31\x61\ -\x2e\x39\x2e\x39\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x38\x35\x2d\x2e\ -\x36\x2e\x39\x32\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\ -\x2e\x36\x31\x2c\x31\x34\x2e\x39\x34\x2c\x31\x34\x2e\x39\x34\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x33\x2c\x33\x2e\x34\x38\x43\x32\ -\x30\x2e\x35\x32\x2c\x33\x31\x2e\x36\x34\x2c\x31\x39\x2e\x34\x31\ -\x2c\x34\x30\x2e\x34\x31\x2c\x32\x33\x2e\x35\x33\x2c\x34\x39\x2e\ -\x37\x32\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\ -\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x33\x22\x20\x64\x3d\x22\x4d\x35\x2e\x36\x34\x2c\x31\x38\x2e\x31\ -\x34\x61\x35\x2e\x31\x33\x2c\x35\x2e\x31\x33\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x32\x2c\x32\x2e\x32\x37\x63\x2d\x2e\x37\x32\x2c\x30\x2d\ -\x31\x2e\x33\x39\x2c\x30\x2d\x32\x2e\x30\x36\x2c\x30\x73\x2d\x31\ -\x2c\x2e\x31\x2d\x31\x2e\x32\x39\x2e\x37\x34\x43\x31\x2c\x32\x38\ -\x2e\x34\x38\x2c\x31\x2c\x34\x31\x2e\x31\x39\x2c\x33\x2e\x39\x32\ -\x2c\x34\x38\x2e\x37\x63\x2e\x31\x35\x2e\x33\x36\x2e\x33\x33\x2e\ -\x37\x31\x2e\x34\x39\x2c\x31\x2e\x30\x36\x61\x31\x2c\x31\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x35\x37\x48\x37\x2e\x36\x37\x41\ -\x35\x2e\x30\x37\x2c\x35\x2e\x30\x37\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x35\x2e\x39\x32\x2c\x35\x32\x2e\x34\x63\x2d\x31\x2e\x32\x34\x2e\ -\x37\x34\x2d\x32\x2e\x34\x2d\x2e\x38\x38\x2d\x32\x2e\x39\x33\x2d\ -\x31\x2e\x38\x31\x43\x2d\x2e\x38\x32\x2c\x34\x33\x2e\x35\x33\x2d\ -\x2e\x37\x34\x2c\x33\x30\x2e\x32\x38\x2c\x31\x2e\x38\x33\x2c\x32\ -\x32\x2e\x37\x34\x63\x2e\x35\x39\x2d\x31\x2e\x36\x34\x2c\x31\x2e\ -\x34\x2d\x33\x2e\x36\x39\x2c\x33\x2e\x30\x35\x2d\x34\x2e\x36\x5a\ -\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\ -\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\ -\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\ -\x64\x3d\x22\x4d\x33\x2e\x35\x32\x2c\x33\x35\x2e\x37\x37\x63\x30\ -\x2d\x35\x2c\x2e\x33\x37\x2d\x39\x2e\x32\x33\x2c\x32\x2e\x31\x37\ -\x2d\x31\x33\x2e\x36\x31\x61\x2e\x34\x32\x2e\x34\x32\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x2e\x34\x38\x2d\x2e\x32\x39\x63\x2e\x36\x2c\x30\ -\x2c\x31\x2e\x32\x2c\x30\x2c\x31\x2e\x38\x2c\x30\x2c\x2e\x32\x39\ -\x2c\x30\x2c\x2e\x33\x35\x2c\x30\x2c\x2e\x32\x35\x2e\x33\x61\x32\ -\x37\x2e\x33\x35\x2c\x32\x37\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\ -\x2d\x31\x2e\x33\x37\x2c\x35\x63\x2d\x31\x2e\x31\x37\x2c\x36\x2e\ -\x39\x2d\x31\x2e\x30\x38\x2c\x31\x34\x2e\x37\x35\x2c\x31\x2e\x33\ -\x37\x2c\x32\x31\x2e\x33\x38\x2e\x31\x2e\x32\x34\x2e\x30\x39\x2e\ -\x33\x34\x2d\x2e\x32\x35\x2e\x33\x33\x2d\x2e\x36\x2c\x30\x2d\x31\ -\x2e\x32\x31\x2c\x30\x2d\x31\x2e\x38\x31\x2c\x30\x61\x2e\x34\x33\ -\x2e\x34\x33\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x34\x37\x2d\x2e\x33\ -\x43\x34\x2c\x34\x34\x2e\x36\x38\x2c\x33\x2e\x35\x32\x2c\x33\x39\ -\x2e\x36\x38\x2c\x33\x2e\x35\x32\x2c\x33\x35\x2e\x37\x37\x5a\x22\ -\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\ -\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\ -\x3d\x22\x4d\x31\x38\x2e\x33\x38\x2c\x32\x31\x2e\x38\x37\x63\x2e\ -\x32\x38\x2e\x30\x35\x2e\x38\x31\x2d\x2e\x31\x36\x2c\x31\x2c\x2e\ -\x30\x39\x73\x2d\x2e\x31\x33\x2e\x36\x31\x2d\x2e\x32\x32\x2e\x39\ -\x32\x61\x34\x35\x2e\x36\x39\x2c\x34\x35\x2e\x36\x39\x2c\x30\x2c\ -\x30\x2c\x30\x2d\x31\x2e\x35\x32\x2c\x31\x37\x2e\x32\x39\x2c\x33\ -\x39\x2e\x36\x31\x2c\x33\x39\x2e\x36\x31\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x31\x2e\x37\x32\x2c\x38\x2e\x33\x31\x63\x2e\x31\x31\x2e\x33\ -\x33\x2e\x30\x35\x2e\x34\x2d\x2e\x33\x33\x2e\x33\x38\x73\x2d\x31\ -\x2c\x30\x2d\x31\x2e\x35\x33\x2c\x30\x61\x2e\x34\x39\x2e\x34\x39\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x35\x34\x2d\x2e\x33\x33\x2c\x32\ -\x33\x2e\x38\x35\x2c\x32\x33\x2e\x38\x35\x2c\x30\x2c\x30\x2c\x31\ -\x2d\x31\x2e\x35\x31\x2d\x35\x2e\x33\x63\x2d\x31\x2e\x30\x39\x2d\ -\x36\x2e\x35\x38\x2d\x31\x2d\x31\x34\x2e\x37\x34\x2c\x31\x2e\x34\ -\x39\x2d\x32\x31\x61\x2e\x35\x2e\x35\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x2e\x36\x2d\x2e\x33\x37\x41\x37\x2e\x34\x31\x2c\x37\x2e\x34\x31\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x38\x2e\x33\x38\x2c\x32\x31\x2e\ -\x38\x37\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\ -\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x33\x22\x20\x64\x3d\x22\x4d\x37\x2e\x38\x37\x2c\x33\x36\x2e\x35\ -\x34\x61\x31\x31\x2e\x32\x36\x2c\x31\x31\x2e\x32\x36\x2c\x30\x2c\ -\x30\x2c\x31\x2d\x2e\x30\x36\x2d\x32\x2e\x33\x33\x63\x2e\x32\x37\ -\x2d\x34\x2e\x30\x38\x2e\x35\x37\x2d\x38\x2e\x32\x38\x2c\x32\x2e\ -\x32\x35\x2d\x31\x32\x2e\x31\x31\x61\x2e\x33\x36\x2e\x33\x36\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x2e\x33\x38\x2d\x2e\x32\x33\x63\x31\x2e\ -\x33\x39\x2d\x2e\x30\x38\x2c\x31\x2e\x33\x38\x2d\x2e\x30\x38\x2c\ -\x31\x2c\x31\x43\x39\x2c\x33\x30\x2e\x34\x35\x2c\x38\x2e\x39\x33\ -\x2c\x34\x31\x2c\x31\x31\x2e\x36\x38\x2c\x34\x38\x2e\x35\x63\x2e\ -\x31\x32\x2e\x33\x31\x2e\x30\x35\x2e\x33\x37\x2d\x2e\x33\x2e\x33\ -\x36\x2d\x31\x2e\x32\x35\x2c\x30\x2d\x31\x2e\x32\x36\x2c\x30\x2d\ -\x31\x2e\x36\x34\x2d\x31\x41\x33\x36\x2e\x31\x34\x2c\x33\x36\x2e\ -\x31\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x38\x37\x2c\x33\x36\ -\x2e\x35\x34\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\ -\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\ -\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\ -\x2d\x33\x22\x20\x64\x3d\x22\x4d\x31\x31\x2e\x33\x35\x2c\x33\x35\ -\x2e\x36\x36\x63\x30\x2d\x34\x2e\x38\x36\x2e\x33\x38\x2d\x39\x2e\ -\x31\x37\x2c\x32\x2e\x31\x37\x2d\x31\x33\x2e\x34\x39\x61\x2e\x34\ -\x2e\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x37\x2d\x2e\x33\x63\ -\x2e\x34\x2c\x30\x2c\x31\x2d\x2e\x31\x35\x2c\x31\x2e\x31\x38\x2e\ -\x30\x37\x73\x2d\x2e\x31\x37\x2e\x36\x33\x2d\x2e\x32\x38\x2c\x31\ -\x63\x2d\x32\x2e\x34\x31\x2c\x37\x2e\x36\x31\x2d\x32\x2e\x35\x31\ -\x2c\x31\x38\x2c\x2e\x32\x37\x2c\x32\x35\x2e\x35\x36\x2e\x31\x34\ -\x2e\x33\x36\x2c\x30\x2c\x2e\x34\x2d\x2e\x33\x35\x2e\x33\x39\x2d\ -\x31\x2e\x31\x37\x2c\x30\x2d\x31\x2e\x31\x39\x2c\x30\x2d\x31\x2e\ -\x35\x37\x2d\x31\x41\x33\x35\x2e\x37\x34\x2c\x33\x35\x2e\x37\x34\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x31\x2e\x33\x35\x2c\x33\x35\x2e\ -\x36\x36\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\ -\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x33\x22\x20\x64\x3d\x22\x4d\x31\x39\x2e\x31\x32\x2c\x32\x30\x2e\ -\x37\x37\x63\x2e\x39\x32\x2c\x30\x2c\x2e\x39\x32\x2c\x30\x2c\x31\ -\x2e\x31\x33\x2d\x2e\x37\x38\x2e\x38\x36\x2d\x32\x2e\x38\x36\x2d\ -\x2e\x32\x35\x2d\x35\x2e\x34\x35\x2d\x33\x2e\x35\x2d\x35\x2e\x38\ -\x37\x2d\x31\x2e\x35\x31\x2d\x2e\x32\x33\x2d\x31\x2e\x35\x2d\x2e\ -\x32\x31\x2d\x31\x2e\x34\x38\x2c\x31\x2e\x31\x2c\x30\x2c\x2e\x32\ -\x35\x2e\x30\x39\x2e\x33\x33\x2e\x33\x37\x2e\x33\x33\x61\x35\x2c\ -\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x34\x39\x2e\x32\x33\x2c\ -\x32\x2e\x31\x31\x2c\x32\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x31\x2e\x35\x38\x2c\x31\x2e\x37\x37\x2c\x35\x2e\x37\x34\x2c\x35\ -\x2e\x37\x34\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x33\x38\x2c\x32\x2e\ -\x37\x38\x63\x2d\x2e\x31\x35\x2e\x34\x34\x2d\x2e\x31\x35\x2e\x34\ -\x34\x2e\x34\x31\x2e\x34\x34\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\ -\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\ -\x29\x22\x2f\x3e\x3c\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x63\x78\x3d\x22\x32\ -\x34\x2e\x34\x32\x22\x20\x63\x79\x3d\x22\x33\x35\x2e\x35\x34\x22\ -\x20\x72\x78\x3d\x22\x31\x2e\x30\x31\x22\x20\x72\x79\x3d\x22\x35\ -\x2e\x39\x39\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\ -\x73\x76\x67\x3e\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 184.16 57.76\ +\x22>\ +n/a<\ +/g>\ \x00\x00\x0a\x12\ -\x3c\ -\x73\x76\x67\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\x31\x37\x36\x2e\x30\x32\x20\x35\x38\x2e\x34\x31\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\ -\x36\x35\x2e\x37\x38\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\ -\x69\x6c\x79\x3a\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\ -\x2c\x20\x4d\x6f\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\ -\x65\x69\x67\x68\x74\x3a\x32\x30\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x31\x2c\x2e\x63\x6c\x73\x2d\x33\x7b\x66\x69\x6c\x6c\x3a\x23\x66\ -\x66\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x6c\x65\x74\x74\x65\ -\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x31\x65\ -\x6d\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\ -\x73\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\ -\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\ -\x65\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\ -\x65\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\ -\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x74\x65\x78\ -\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x34\x37\x2e\x39\x31\x20\x34\x39\x2e\x39\x33\ -\x29\x22\x3e\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x32\x22\x3e\x6e\x79\x3c\x2f\x74\x73\x70\x61\ -\x6e\x3e\x3c\x74\x73\x70\x61\x6e\x20\x78\x3d\x22\x36\x34\x2e\x34\ -\x22\x20\x79\x3d\x22\x30\x22\x3e\x6c\x3c\x2f\x74\x73\x70\x61\x6e\ -\x3e\x3c\x2f\x74\x65\x78\x74\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ -\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\ -\x32\x34\x2e\x37\x38\x2c\x31\x38\x2e\x31\x32\x63\x32\x2e\x35\x36\ -\x2c\x31\x2e\x33\x34\x2c\x33\x2e\x37\x31\x2c\x36\x2e\x34\x34\x2c\ -\x34\x2e\x31\x38\x2c\x39\x2e\x30\x36\x2c\x31\x2e\x32\x2c\x36\x2e\ -\x35\x36\x2c\x31\x2e\x31\x34\x2c\x31\x39\x2e\x31\x2d\x33\x2e\x30\ -\x39\x2c\x32\x34\x2e\x35\x31\x2d\x2e\x38\x31\x2c\x31\x2d\x31\x2e\ -\x38\x32\x2c\x31\x2e\x32\x31\x2d\x32\x2e\x37\x34\x2e\x32\x32\x61\ -\x39\x2c\x39\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x38\x35\x2d\x33\ -\x2e\x31\x63\x2d\x32\x2e\x38\x33\x2d\x37\x2e\x36\x31\x2d\x32\x2e\ -\x38\x38\x2d\x31\x39\x2e\x33\x2c\x30\x2d\x32\x36\x2e\x39\x31\x2e\ -\x35\x38\x2d\x31\x2e\x34\x31\x2c\x31\x2e\x33\x31\x2d\x33\x2c\x32\ -\x2e\x37\x35\x2d\x33\x2e\x37\x38\x5a\x4d\x32\x33\x2e\x35\x33\x2c\ -\x34\x39\x2e\x36\x39\x61\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x2e\x38\x36\x2e\x36\x31\x2e\x39\x34\x2e\x39\x34\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x39\x2d\x2e\x36\x32\x63\x33\x2e\ -\x30\x39\x2d\x36\x2e\x35\x34\x2c\x33\x2e\x31\x33\x2d\x31\x36\x2e\ -\x35\x35\x2c\x31\x2e\x36\x39\x2d\x32\x33\x2e\x35\x37\x41\x32\x30\ -\x2e\x32\x37\x2c\x32\x30\x2e\x32\x37\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x32\x35\x2e\x32\x37\x2c\x32\x31\x61\x2e\x38\x38\x2e\x38\x38\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x2e\x38\x35\x2d\x2e\x36\x2e\x39\x32\x2e\ -\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2e\x36\x31\x2c\x31\ -\x34\x2e\x39\x34\x2c\x31\x34\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x30\ -\x2d\x31\x2e\x33\x2c\x33\x2e\x34\x38\x43\x32\x30\x2e\x35\x32\x2c\ -\x33\x31\x2e\x36\x32\x2c\x31\x39\x2e\x34\x31\x2c\x34\x30\x2e\x33\ -\x39\x2c\x32\x33\x2e\x35\x33\x2c\x34\x39\x2e\x36\x39\x5a\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\ -\x22\x4d\x35\x2e\x36\x34\x2c\x31\x38\x2e\x31\x32\x61\x35\x2e\x31\ -\x36\x2c\x35\x2e\x31\x36\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2c\x32\ -\x2e\x32\x36\x48\x35\x2e\x36\x31\x63\x2d\x2e\x38\x2c\x30\x2d\x31\ -\x2c\x2e\x31\x31\x2d\x31\x2e\x32\x39\x2e\x37\x34\x43\x31\x2c\x32\ -\x38\x2e\x34\x35\x2c\x31\x2c\x34\x31\x2e\x31\x37\x2c\x33\x2e\x39\ -\x32\x2c\x34\x38\x2e\x36\x38\x63\x2e\x31\x35\x2e\x33\x36\x2e\x33\ -\x33\x2e\x37\x2e\x34\x39\x2c\x31\x2e\x30\x36\x61\x31\x2c\x31\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x35\x37\x48\x37\x2e\x36\x37\ -\x61\x35\x2c\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x37\x35\x2c\ -\x32\x2e\x30\x37\x63\x2d\x31\x2e\x32\x34\x2e\x37\x35\x2d\x32\x2e\ -\x34\x2d\x2e\x38\x37\x2d\x32\x2e\x39\x33\x2d\x31\x2e\x38\x43\x2d\ -\x2e\x38\x32\x2c\x34\x33\x2e\x35\x2d\x2e\x37\x34\x2c\x33\x30\x2e\ -\x32\x35\x2c\x31\x2e\x38\x33\x2c\x32\x32\x2e\x37\x32\x63\x2e\x35\ -\x39\x2d\x31\x2e\x36\x35\x2c\x31\x2e\x34\x2d\x33\x2e\x36\x39\x2c\ -\x33\x2e\x30\x35\x2d\x34\x2e\x36\x5a\x22\x20\x74\x72\x61\x6e\x73\ -\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\ -\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x33\x2e\x35\ -\x32\x2c\x33\x35\x2e\x37\x35\x63\x30\x2d\x35\x2c\x2e\x33\x37\x2d\ -\x39\x2e\x32\x34\x2c\x32\x2e\x31\x37\x2d\x31\x33\x2e\x36\x32\x61\ -\x2e\x34\x34\x2e\x34\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x38\ -\x2d\x2e\x32\x39\x63\x2e\x36\x2c\x30\x2c\x31\x2e\x32\x2c\x30\x2c\ -\x31\x2e\x38\x2c\x30\x2c\x2e\x32\x39\x2c\x30\x2c\x2e\x33\x35\x2c\ -\x30\x2c\x2e\x32\x35\x2e\x32\x39\x61\x32\x37\x2e\x34\x35\x2c\x32\ -\x37\x2e\x34\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x33\x37\x2c\ -\x35\x43\x35\x2e\x36\x38\x2c\x33\x34\x2c\x35\x2e\x37\x37\x2c\x34\ -\x31\x2e\x38\x39\x2c\x38\x2e\x32\x32\x2c\x34\x38\x2e\x35\x32\x63\ -\x2e\x31\x2e\x32\x34\x2e\x30\x39\x2e\x33\x34\x2d\x2e\x32\x35\x2e\ -\x33\x32\x2d\x2e\x36\x2c\x30\x2d\x31\x2e\x32\x31\x2c\x30\x2d\x31\ -\x2e\x38\x31\x2c\x30\x61\x2e\x34\x32\x2e\x34\x32\x2c\x30\x2c\x30\ -\x2c\x31\x2d\x2e\x34\x37\x2d\x2e\x32\x39\x43\x34\x2c\x34\x34\x2e\ -\x36\x36\x2c\x33\x2e\x35\x32\x2c\x33\x39\x2e\x36\x35\x2c\x33\x2e\ -\x35\x32\x2c\x33\x35\x2e\x37\x35\x5a\x22\x20\x74\x72\x61\x6e\x73\ -\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\ -\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x31\x38\x2e\ -\x33\x38\x2c\x32\x31\x2e\x38\x35\x63\x2e\x32\x38\x2c\x30\x2c\x2e\ -\x38\x31\x2d\x2e\x31\x36\x2c\x31\x2c\x2e\x30\x39\x73\x2d\x2e\x31\ -\x33\x2e\x36\x2d\x2e\x32\x32\x2e\x39\x32\x61\x34\x35\x2e\x36\x39\ -\x2c\x34\x35\x2e\x36\x39\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x35\ -\x32\x2c\x31\x37\x2e\x32\x39\x2c\x33\x39\x2e\x38\x37\x2c\x33\x39\ -\x2e\x38\x37\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2e\x37\x32\x2c\x38\ -\x2e\x33\x31\x63\x2e\x31\x31\x2e\x33\x32\x2e\x30\x35\x2e\x34\x2d\ -\x2e\x33\x33\x2e\x33\x38\x73\x2d\x31\x2c\x30\x2d\x31\x2e\x35\x33\ -\x2c\x30\x61\x2e\x34\x37\x2e\x34\x37\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x2e\x35\x34\x2d\x2e\x33\x33\x2c\x32\x33\x2e\x36\x32\x2c\x32\x33\ -\x2e\x36\x32\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x31\x2d\x35\ -\x2e\x32\x39\x63\x2d\x31\x2e\x30\x39\x2d\x36\x2e\x35\x39\x2d\x31\ -\x2d\x31\x34\x2e\x37\x34\x2c\x31\x2e\x34\x39\x2d\x32\x31\x61\x2e\ -\x34\x38\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x36\x2d\x2e\ -\x33\x37\x43\x31\x37\x2e\x38\x2c\x32\x31\x2e\x38\x37\x2c\x31\x38\ -\x2c\x32\x31\x2e\x38\x35\x2c\x31\x38\x2e\x33\x38\x2c\x32\x31\x2e\ -\x38\x35\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\ -\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x33\x22\x20\x64\x3d\x22\x4d\x37\x2e\x38\x37\x2c\x33\x36\x2e\x35\ -\x32\x61\x31\x31\x2e\x32\x36\x2c\x31\x31\x2e\x32\x36\x2c\x30\x2c\ -\x30\x2c\x31\x2d\x2e\x30\x36\x2d\x32\x2e\x33\x33\x63\x2e\x32\x37\ -\x2d\x34\x2e\x30\x38\x2e\x35\x37\x2d\x38\x2e\x32\x39\x2c\x32\x2e\ -\x32\x35\x2d\x31\x32\x2e\x31\x31\x61\x2e\x33\x38\x2e\x33\x38\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x2e\x33\x38\x2d\x2e\x32\x34\x63\x31\x2e\ -\x33\x39\x2d\x2e\x30\x37\x2c\x31\x2e\x33\x38\x2d\x2e\x30\x38\x2c\ -\x31\x2c\x31\x2e\x30\x35\x43\x39\x2c\x33\x30\x2e\x34\x33\x2c\x38\ -\x2e\x39\x33\x2c\x34\x31\x2c\x31\x31\x2e\x36\x38\x2c\x34\x38\x2e\ -\x34\x38\x63\x2e\x31\x32\x2e\x33\x31\x2e\x30\x35\x2e\x33\x36\x2d\ -\x2e\x33\x2e\x33\x36\x2d\x31\x2e\x32\x35\x2c\x30\x2d\x31\x2e\x32\ -\x36\x2c\x30\x2d\x31\x2e\x36\x34\x2d\x31\x41\x33\x36\x2e\x31\x39\ -\x2c\x33\x36\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x38\ -\x37\x2c\x33\x36\x2e\x35\x32\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\ -\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\ -\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x31\x31\x2e\x33\ -\x35\x2c\x33\x35\x2e\x36\x34\x63\x30\x2d\x34\x2e\x38\x36\x2e\x33\ -\x38\x2d\x39\x2e\x31\x37\x2c\x32\x2e\x31\x37\x2d\x31\x33\x2e\x35\ -\x61\x2e\x34\x31\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\ -\x37\x2d\x2e\x32\x39\x63\x2e\x34\x2c\x30\x2c\x31\x2d\x2e\x31\x35\ -\x2c\x31\x2e\x31\x38\x2e\x30\x36\x73\x2d\x2e\x31\x37\x2e\x36\x34\ -\x2d\x2e\x32\x38\x2c\x31\x63\x2d\x32\x2e\x34\x31\x2c\x37\x2e\x36\ -\x31\x2d\x32\x2e\x35\x31\x2c\x31\x38\x2c\x2e\x32\x37\x2c\x32\x35\ -\x2e\x35\x37\x2e\x31\x34\x2e\x33\x36\x2c\x30\x2c\x2e\x33\x39\x2d\ -\x2e\x33\x35\x2e\x33\x39\x2d\x31\x2e\x31\x37\x2c\x30\x2d\x31\x2e\ -\x31\x39\x2c\x30\x2d\x31\x2e\x35\x37\x2d\x31\x41\x33\x35\x2e\x37\ -\x37\x2c\x33\x35\x2e\x37\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x31\ -\x2e\x33\x35\x2c\x33\x35\x2e\x36\x34\x5a\x22\x20\x74\x72\x61\x6e\ -\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\ -\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x31\x39\ -\x2e\x31\x32\x2c\x32\x30\x2e\x37\x35\x63\x2e\x39\x32\x2c\x30\x2c\ -\x2e\x39\x32\x2c\x30\x2c\x31\x2e\x31\x33\x2d\x2e\x37\x38\x2e\x38\ -\x36\x2d\x32\x2e\x38\x37\x2d\x2e\x32\x35\x2d\x35\x2e\x34\x36\x2d\ -\x33\x2e\x35\x2d\x35\x2e\x38\x37\x2d\x31\x2e\x35\x31\x2d\x2e\x32\ -\x33\x2d\x31\x2e\x35\x2d\x2e\x32\x31\x2d\x31\x2e\x34\x38\x2c\x31\ -\x2e\x31\x2c\x30\x2c\x2e\x32\x35\x2e\x30\x39\x2e\x33\x33\x2e\x33\ -\x37\x2e\x33\x33\x61\x35\x2e\x33\x32\x2c\x35\x2e\x33\x32\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x31\x2e\x34\x39\x2e\x32\x32\x2c\x32\x2e\x31\ -\x32\x2c\x32\x2e\x31\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x35\ -\x38\x2c\x31\x2e\x37\x38\x2c\x35\x2e\x37\x38\x2c\x35\x2e\x37\x38\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x33\x38\x2c\x32\x2e\x37\x38\x63\ -\x2d\x2e\x31\x35\x2e\x34\x34\x2d\x2e\x31\x35\x2e\x34\x34\x2e\x34\ -\x31\x2e\x34\x34\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\ -\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\ -\x3e\x3c\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x33\x22\x20\x63\x78\x3d\x22\x32\x34\x2e\x34\ -\x32\x22\x20\x63\x79\x3d\x22\x33\x35\x2e\x35\x32\x22\x20\x72\x78\ -\x3d\x22\x31\x2e\x30\x31\x22\x20\x72\x79\x3d\x22\x35\x2e\x39\x39\ -\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\x67\ -\x3e\ -\x00\x00\x0a\x62\ -\x3c\ -\x73\x76\x67\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\x31\x39\x36\x2e\x33\x37\x20\x35\x38\x2e\x34\x31\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\ -\x36\x35\x2e\x37\x38\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\ -\x69\x6c\x79\x3a\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\ -\x2c\x20\x4d\x6f\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\ -\x65\x69\x67\x68\x74\x3a\x32\x30\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x31\x2c\x2e\x63\x6c\x73\x2d\x34\x7b\x66\x69\x6c\x6c\x3a\x23\x66\ -\x66\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x6c\x65\x74\x74\x65\ -\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x31\x65\ -\x6d\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x6c\x65\x74\x74\x65\x72\ -\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x35\x65\x6d\ -\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\ -\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\ -\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\ -\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x74\x65\x78\x74\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x74\ -\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\ -\x61\x74\x65\x28\x34\x37\x2e\x39\x31\x20\x34\x39\x2e\x39\x33\x29\ -\x22\x3e\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x32\x22\x3e\x70\x3c\x2f\x74\x73\x70\x61\x6e\x3e\ -\x3c\x74\x73\x70\x61\x6e\x20\x78\x3d\x22\x32\x38\x2e\x30\x32\x22\ -\x20\x79\x3d\x22\x30\x22\x3e\x65\x3c\x2f\x74\x73\x70\x61\x6e\x3e\ -\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x33\x22\x20\x78\x3d\x22\x35\x34\x2e\x38\x36\x22\x20\x79\ -\x3d\x22\x30\x22\x3e\x74\x3c\x2f\x74\x73\x70\x61\x6e\x3e\x3c\x74\ -\x73\x70\x61\x6e\x20\x78\x3d\x22\x38\x34\x2e\x39\x32\x22\x20\x79\ -\x3d\x22\x30\x22\x3e\x67\x3c\x2f\x74\x73\x70\x61\x6e\x3e\x3c\x2f\ -\x74\x65\x78\x74\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x32\x34\x2e\ -\x37\x38\x2c\x31\x38\x2e\x31\x31\x63\x32\x2e\x35\x36\x2c\x31\x2e\ -\x33\x35\x2c\x33\x2e\x37\x31\x2c\x36\x2e\x34\x34\x2c\x34\x2e\x31\ -\x38\x2c\x39\x2e\x30\x37\x2c\x31\x2e\x32\x2c\x36\x2e\x35\x36\x2c\ -\x31\x2e\x31\x34\x2c\x31\x39\x2e\x31\x2d\x33\x2e\x30\x39\x2c\x32\ -\x34\x2e\x35\x31\x2d\x2e\x38\x31\x2c\x31\x2d\x31\x2e\x38\x32\x2c\ -\x31\x2e\x32\x31\x2d\x32\x2e\x37\x34\x2e\x32\x32\x61\x39\x2c\x39\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x38\x35\x2d\x33\x2e\x31\x31\ -\x63\x2d\x32\x2e\x38\x33\x2d\x37\x2e\x36\x2d\x32\x2e\x38\x38\x2d\ -\x31\x39\x2e\x32\x39\x2c\x30\x2d\x32\x36\x2e\x39\x41\x37\x2e\x30\ -\x36\x2c\x37\x2e\x30\x36\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x34\x2c\ -\x31\x38\x2e\x31\x31\x5a\x4d\x32\x33\x2e\x35\x33\x2c\x34\x39\x2e\ -\x36\x39\x61\x2e\x38\x38\x2e\x38\x38\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x2e\x38\x36\x2e\x36\x2e\x39\x32\x2e\x39\x32\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x2e\x38\x39\x2d\x2e\x36\x31\x63\x33\x2e\x30\x39\x2d\x36\ -\x2e\x35\x35\x2c\x33\x2e\x31\x33\x2d\x31\x36\x2e\x35\x36\x2c\x31\ -\x2e\x36\x39\x2d\x32\x33\x2e\x35\x37\x41\x32\x30\x2e\x32\x31\x2c\ -\x32\x30\x2e\x32\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x35\x2e\x32\ -\x37\x2c\x32\x31\x61\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\ -\x30\x2d\x2e\x38\x35\x2d\x2e\x36\x2c\x31\x2c\x31\x2c\x30\x2c\x30\ -\x2c\x30\x2d\x2e\x39\x2e\x36\x31\x2c\x31\x35\x2c\x31\x35\x2c\x30\ -\x2c\x30\x2c\x30\x2d\x31\x2e\x33\x2c\x33\x2e\x34\x39\x43\x32\x30\ -\x2e\x35\x32\x2c\x33\x31\x2e\x36\x31\x2c\x31\x39\x2e\x34\x31\x2c\ -\x34\x30\x2e\x33\x39\x2c\x32\x33\x2e\x35\x33\x2c\x34\x39\x2e\x36\ -\x39\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\ -\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\ -\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\ -\x22\x20\x64\x3d\x22\x4d\x35\x2e\x36\x34\x2c\x31\x38\x2e\x31\x31\ -\x61\x35\x2e\x32\x34\x2c\x35\x2e\x32\x34\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x32\x2c\x32\x2e\x32\x37\x48\x35\x2e\x36\x31\x63\x2d\x2e\x38\ -\x2c\x30\x2d\x31\x2c\x2e\x31\x31\x2d\x31\x2e\x32\x39\x2e\x37\x34\ -\x43\x31\x2c\x32\x38\x2e\x34\x35\x2c\x31\x2c\x34\x31\x2e\x31\x37\ -\x2c\x33\x2e\x39\x32\x2c\x34\x38\x2e\x36\x37\x63\x2e\x31\x35\x2e\ -\x33\x36\x2e\x33\x33\x2e\x37\x31\x2e\x34\x39\x2c\x31\x2e\x30\x36\ -\x61\x31\x2c\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x35\x37\ -\x48\x37\x2e\x36\x37\x61\x35\x2c\x35\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x31\x2e\x37\x35\x2c\x32\x2e\x30\x37\x63\x2d\x31\x2e\x32\x34\x2e\ -\x37\x34\x2d\x32\x2e\x34\x2d\x2e\x38\x37\x2d\x32\x2e\x39\x33\x2d\ -\x31\x2e\x38\x43\x2d\x2e\x38\x32\x2c\x34\x33\x2e\x35\x2d\x2e\x37\ -\x34\x2c\x33\x30\x2e\x32\x35\x2c\x31\x2e\x38\x33\x2c\x32\x32\x2e\ -\x37\x32\x63\x2e\x35\x39\x2d\x31\x2e\x36\x35\x2c\x31\x2e\x34\x2d\ -\x33\x2e\x36\x39\x2c\x33\x2e\x30\x35\x2d\x34\x2e\x36\x31\x5a\x22\ -\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\ -\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\ -\x3d\x22\x4d\x33\x2e\x35\x32\x2c\x33\x35\x2e\x37\x34\x63\x30\x2d\ -\x35\x2c\x2e\x33\x37\x2d\x39\x2e\x32\x33\x2c\x32\x2e\x31\x37\x2d\ -\x31\x33\x2e\x36\x31\x61\x2e\x34\x33\x2e\x34\x33\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x2e\x34\x38\x2d\x2e\x32\x39\x71\x2e\x39\x2c\x30\x2c\ -\x31\x2e\x38\x2c\x30\x63\x2e\x32\x39\x2c\x30\x2c\x2e\x33\x35\x2e\ -\x30\x35\x2e\x32\x35\x2e\x33\x61\x32\x37\x2e\x33\x35\x2c\x32\x37\ -\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x33\x37\x2c\x35\ -\x43\x35\x2e\x36\x38\x2c\x33\x34\x2c\x35\x2e\x37\x37\x2c\x34\x31\ -\x2e\x38\x39\x2c\x38\x2e\x32\x32\x2c\x34\x38\x2e\x35\x32\x63\x2e\ -\x31\x2e\x32\x34\x2e\x30\x39\x2e\x33\x33\x2d\x2e\x32\x35\x2e\x33\ -\x32\x2d\x2e\x36\x2c\x30\x2d\x31\x2e\x32\x31\x2c\x30\x2d\x31\x2e\ -\x38\x31\x2c\x30\x61\x2e\x34\x32\x2e\x34\x32\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x2e\x34\x37\x2d\x2e\x33\x43\x34\x2c\x34\x34\x2e\x36\x36\ -\x2c\x33\x2e\x35\x32\x2c\x33\x39\x2e\x36\x35\x2c\x33\x2e\x35\x32\ -\x2c\x33\x35\x2e\x37\x34\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\ -\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\ -\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x31\x38\x2e\x33\x38\ -\x2c\x32\x31\x2e\x38\x35\x63\x2e\x32\x38\x2c\x30\x2c\x2e\x38\x31\ -\x2d\x2e\x31\x36\x2c\x31\x2c\x2e\x30\x38\x73\x2d\x2e\x31\x33\x2e\ -\x36\x31\x2d\x2e\x32\x32\x2e\x39\x32\x61\x34\x35\x2e\x37\x32\x2c\ -\x34\x35\x2e\x37\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x35\x32\ -\x2c\x31\x37\x2e\x33\x2c\x33\x39\x2e\x36\x35\x2c\x33\x39\x2e\x36\ -\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2e\x37\x32\x2c\x38\x2e\x33\ -\x63\x2e\x31\x31\x2e\x33\x33\x2e\x30\x35\x2e\x34\x31\x2d\x2e\x33\ -\x33\x2e\x33\x39\x61\x31\x33\x2c\x31\x33\x2c\x30\x2c\x30\x2c\x30\ -\x2d\x31\x2e\x35\x33\x2c\x30\x2c\x2e\x34\x38\x2e\x34\x38\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x35\x34\x2d\x2e\x33\x33\x2c\x32\x33\x2e\ -\x37\x2c\x32\x33\x2e\x37\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\ -\x31\x2d\x35\x2e\x33\x63\x2d\x31\x2e\x30\x39\x2d\x36\x2e\x35\x38\ -\x2d\x31\x2d\x31\x34\x2e\x37\x34\x2c\x31\x2e\x34\x39\x2d\x32\x31\ -\x61\x2e\x35\x2e\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x36\x2d\x2e\ -\x33\x37\x41\x37\x2e\x31\x33\x2c\x37\x2e\x31\x33\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x31\x38\x2e\x33\x38\x2c\x32\x31\x2e\x38\x35\x5a\x22\ -\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\ -\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\ -\x3d\x22\x4d\x37\x2e\x38\x37\x2c\x33\x36\x2e\x35\x32\x61\x31\x31\ -\x2e\x32\x38\x2c\x31\x31\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x2e\x30\x36\x2d\x32\x2e\x33\x33\x63\x2e\x32\x37\x2d\x34\x2e\x30\ -\x38\x2e\x35\x37\x2d\x38\x2e\x32\x39\x2c\x32\x2e\x32\x35\x2d\x31\ -\x32\x2e\x31\x31\x61\x2e\x33\x37\x2e\x33\x37\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x2e\x33\x38\x2d\x2e\x32\x34\x63\x31\x2e\x33\x39\x2d\x2e\ -\x30\x37\x2c\x31\x2e\x33\x38\x2d\x2e\x30\x38\x2c\x31\x2c\x31\x43\ -\x39\x2c\x33\x30\x2e\x34\x32\x2c\x38\x2e\x39\x33\x2c\x34\x31\x2c\ -\x31\x31\x2e\x36\x38\x2c\x34\x38\x2e\x34\x38\x63\x2e\x31\x32\x2e\ -\x33\x2e\x30\x35\x2e\x33\x36\x2d\x2e\x33\x2e\x33\x36\x2d\x31\x2e\ -\x32\x35\x2c\x30\x2d\x31\x2e\x32\x36\x2c\x30\x2d\x31\x2e\x36\x34\ -\x2d\x31\x41\x33\x36\x2e\x30\x38\x2c\x33\x36\x2e\x30\x38\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x37\x2e\x38\x37\x2c\x33\x36\x2e\x35\x32\x5a\ -\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\ -\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\ -\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\ -\x64\x3d\x22\x4d\x31\x31\x2e\x33\x35\x2c\x33\x35\x2e\x36\x34\x63\ -\x30\x2d\x34\x2e\x38\x36\x2e\x33\x38\x2d\x39\x2e\x31\x38\x2c\x32\ -\x2e\x31\x37\x2d\x31\x33\x2e\x35\x61\x2e\x34\x2e\x34\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x2e\x34\x37\x2d\x2e\x33\x63\x2e\x34\x2c\x30\x2c\ -\x31\x2d\x2e\x31\x35\x2c\x31\x2e\x31\x38\x2e\x30\x37\x73\x2d\x2e\ -\x31\x37\x2e\x36\x34\x2d\x2e\x32\x38\x2c\x31\x63\x2d\x32\x2e\x34\ -\x31\x2c\x37\x2e\x36\x31\x2d\x32\x2e\x35\x31\x2c\x31\x38\x2c\x2e\ -\x32\x37\x2c\x32\x35\x2e\x35\x37\x2e\x31\x34\x2e\x33\x36\x2c\x30\ -\x2c\x2e\x33\x39\x2d\x2e\x33\x35\x2e\x33\x39\x2d\x31\x2e\x31\x37\ -\x2c\x30\x2d\x31\x2e\x31\x39\x2c\x30\x2d\x31\x2e\x35\x37\x2d\x31\ -\x41\x33\x35\x2e\x36\x38\x2c\x33\x35\x2e\x36\x38\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x31\x31\x2e\x33\x35\x2c\x33\x35\x2e\x36\x34\x5a\x22\ -\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\ -\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\ -\x3d\x22\x4d\x31\x39\x2e\x31\x32\x2c\x32\x30\x2e\x37\x34\x63\x2e\ -\x39\x32\x2c\x30\x2c\x2e\x39\x32\x2c\x30\x2c\x31\x2e\x31\x33\x2d\ -\x2e\x37\x38\x2e\x38\x36\x2d\x32\x2e\x38\x36\x2d\x2e\x32\x35\x2d\ -\x35\x2e\x34\x35\x2d\x33\x2e\x35\x2d\x35\x2e\x38\x36\x2d\x31\x2e\ -\x35\x31\x2d\x2e\x32\x33\x2d\x31\x2e\x35\x2d\x2e\x32\x31\x2d\x31\ -\x2e\x34\x38\x2c\x31\x2e\x30\x39\x2c\x30\x2c\x2e\x32\x35\x2e\x30\ -\x39\x2e\x33\x33\x2e\x33\x37\x2e\x33\x34\x61\x35\x2c\x35\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x31\x2e\x34\x39\x2e\x32\x32\x2c\x32\x2e\x31\ -\x31\x2c\x32\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x35\ -\x38\x2c\x31\x2e\x37\x38\x2c\x35\x2e\x37\x38\x2c\x35\x2e\x37\x38\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x33\x38\x2c\x32\x2e\x37\x38\x63\ -\x2d\x2e\x31\x35\x2e\x34\x33\x2d\x2e\x31\x35\x2e\x34\x33\x2e\x34\ -\x31\x2e\x34\x33\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\ -\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\ -\x3e\x3c\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x34\x22\x20\x63\x78\x3d\x22\x32\x34\x2e\x34\ -\x32\x22\x20\x63\x79\x3d\x22\x33\x35\x2e\x35\x32\x22\x20\x72\x78\ -\x3d\x22\x31\x2e\x30\x31\x22\x20\x72\x79\x3d\x22\x35\x2e\x39\x39\ -\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\x67\ -\x3e\ -\x00\x00\x05\xf3\ -\x3c\ -\x73\x76\x67\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\x31\x31\x2e\x32\x34\x20\x32\x36\x2e\x38\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\x66\x66\x66\x3b\x7d\x3c\ -\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x67\ -\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\x61\ -\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x32\ -\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\ -\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\ -\x61\x79\x65\x72\x20\x31\x22\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\ -\x38\x2e\x36\x34\x2c\x31\x39\x76\x31\x2e\x31\x32\x63\x30\x2c\x2e\ -\x34\x38\x2d\x2e\x31\x33\x2e\x36\x2d\x2e\x36\x31\x2e\x36\x31\x48\ -\x37\x2e\x37\x38\x56\x32\x36\x2e\x32\x63\x30\x2c\x2e\x34\x36\x2d\ -\x2e\x31\x34\x2e\x36\x2d\x2e\x35\x39\x2e\x36\x48\x34\x63\x2d\x2e\ -\x34\x33\x2c\x30\x2d\x2e\x35\x37\x2d\x2e\x31\x35\x2d\x2e\x35\x37\ -\x2d\x2e\x35\x37\x56\x32\x30\x2e\x37\x36\x48\x33\x2e\x32\x31\x63\ -\x2d\x2e\x35\x2c\x30\x2d\x2e\x36\x32\x2d\x2e\x31\x33\x2d\x2e\x36\ -\x32\x2d\x2e\x36\x33\x56\x31\x39\x48\x32\x2e\x32\x32\x61\x2e\x34\ -\x34\x2e\x34\x34\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x34\x39\x2d\x2e\ -\x35\x31\x63\x30\x2d\x31\x2c\x30\x2d\x32\x2c\x30\x2d\x33\x2e\x30\ -\x35\x61\x31\x2c\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x31\x2d\x2e\ -\x34\x4c\x2e\x31\x34\x2c\x31\x32\x2e\x34\x35\x41\x31\x2e\x31\x34\ -\x2c\x31\x2e\x31\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x30\x2c\x31\x31\ -\x2e\x39\x34\x51\x30\x2c\x36\x2e\x36\x36\x2c\x30\x2c\x31\x2e\x33\ -\x38\x41\x31\x2e\x32\x39\x2c\x31\x2e\x32\x39\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x31\x2e\x33\x39\x2c\x30\x48\x39\x2e\x38\x34\x61\x31\x2e\ -\x33\x31\x2c\x31\x2e\x33\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\ -\x34\x2c\x31\x2e\x34\x71\x30\x2c\x35\x2e\x32\x38\x2c\x30\x2c\x31\ -\x30\x2e\x35\x34\x61\x31\x2e\x31\x32\x2c\x31\x2e\x31\x32\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x31\x35\x2e\x35\x33\x63\x2d\x2e\x34\x37\ -\x2e\x38\x36\x2d\x31\x2c\x31\x2e\x37\x2d\x31\x2e\x34\x35\x2c\x32\ -\x2e\x35\x36\x61\x31\x2c\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x31\ -\x32\x2e\x34\x35\x63\x30\x2c\x31\x2c\x30\x2c\x31\x2e\x39\x33\x2c\ -\x30\x2c\x32\x2e\x38\x39\x2c\x30\x2c\x2e\x35\x33\x2d\x2e\x31\x32\ -\x2e\x36\x34\x2d\x2e\x36\x33\x2e\x36\x35\x5a\x4d\x2e\x38\x36\x2c\ -\x37\x2e\x37\x39\x63\x30\x2c\x31\x2e\x33\x38\x2c\x30\x2c\x32\x2e\ -\x37\x33\x2c\x30\x2c\x34\x2e\x30\x38\x61\x2e\x36\x31\x2e\x36\x31\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x30\x39\x2e\x32\x38\x63\x2e\x34\ -\x35\x2e\x37\x39\x2e\x39\x2c\x31\x2e\x35\x37\x2c\x31\x2e\x33\x34\ -\x2c\x32\x2e\x33\x36\x61\x2e\x33\x32\x2e\x33\x32\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x2e\x33\x32\x2e\x31\x39\x68\x36\x61\x2e\x33\x32\x2e\ -\x33\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x33\x32\x2d\x2e\x31\x38\ -\x63\x2e\x34\x34\x2d\x2e\x37\x39\x2e\x39\x2d\x31\x2e\x35\x38\x2c\ -\x31\x2e\x33\x34\x2d\x32\x2e\x33\x37\x61\x2e\x38\x33\x2e\x38\x33\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x31\x2d\x2e\x33\x38\x76\x2d\x34\ -\x68\x2d\x33\x76\x33\x41\x31\x2e\x33\x2c\x31\x2e\x33\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x36\x2c\x31\x32\x2e\x31\x48\x35\x2e\x32\x36\x61\ -\x31\x2e\x32\x39\x2c\x31\x2e\x32\x39\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x31\x2e\x33\x37\x2d\x31\x2e\x33\x37\x56\x37\x2e\x37\x39\x5a\x4d\ -\x31\x2e\x37\x33\x2e\x38\x39\x43\x31\x2e\x31\x31\x2e\x38\x31\x2e\ -\x38\x35\x2e\x38\x38\x2e\x38\x36\x2c\x31\x2e\x36\x32\x63\x30\x2c\ -\x31\x2e\x36\x37\x2c\x30\x2c\x33\x2e\x33\x33\x2c\x30\x2c\x35\x76\ -\x2e\x33\x68\x33\x41\x2e\x33\x33\x2e\x33\x33\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x34\x2c\x36\x2e\x37\x34\x61\x31\x2e\x33\x32\x2c\x31\x2e\ -\x33\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x31\x35\x2d\x2e\x36\ -\x39\x68\x2e\x38\x36\x61\x31\x2e\x32\x34\x2c\x31\x2e\x32\x34\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x31\x32\x2e\x36\x38\x2e\x33\x32\ -\x2e\x33\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x33\x34\x2e\x31\x39\ -\x68\x32\x2e\x38\x36\x56\x31\x2e\x32\x39\x41\x2e\x34\x2e\x34\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x31\x30\x2c\x2e\x38\x38\x61\x32\x2e\x37\ -\x37\x2c\x32\x2e\x37\x37\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x35\x2c\ -\x30\x76\x32\x2e\x39\x41\x31\x2e\x33\x31\x2c\x31\x2e\x33\x31\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x38\x2e\x31\x2c\x35\x2e\x31\x39\x48\x33\ -\x2e\x31\x36\x41\x31\x2e\x33\x31\x2c\x31\x2e\x33\x31\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x31\x2e\x37\x33\x2c\x33\x2e\x37\x36\x5a\x6d\x2e\ -\x38\x36\x2c\x30\x56\x33\x2e\x37\x38\x63\x30\x2c\x2e\x33\x39\x2e\ -\x31\x36\x2e\x35\x34\x2e\x35\x34\x2e\x35\x34\x68\x35\x63\x2e\x34\ -\x2c\x30\x2c\x2e\x35\x34\x2d\x2e\x31\x35\x2e\x35\x34\x2d\x2e\x35\ -\x36\x56\x31\x2e\x30\x39\x63\x30\x2d\x2e\x30\x37\x2c\x30\x2d\x2e\ -\x31\x34\x2c\x30\x2d\x2e\x32\x32\x5a\x4d\x34\x2e\x33\x34\x2c\x32\ -\x30\x2e\x37\x36\x76\x35\x2e\x31\x35\x48\x36\x2e\x39\x56\x32\x30\ -\x2e\x37\x36\x5a\x4d\x34\x2e\x37\x35\x2c\x39\x2e\x30\x35\x63\x30\ -\x2c\x2e\x35\x37\x2c\x30\x2c\x31\x2e\x31\x33\x2c\x30\x2c\x31\x2e\ -\x37\x61\x2e\x34\x34\x2e\x34\x34\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\ -\x34\x38\x2e\x34\x39\x48\x36\x61\x2e\x34\x33\x2e\x34\x33\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x2e\x34\x36\x2d\x2e\x34\x36\x56\x37\x2e\x33\ -\x37\x41\x2e\x34\x32\x2e\x34\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x36\ -\x2c\x36\x2e\x39\x32\x48\x35\x2e\x32\x37\x63\x2d\x2e\x33\x36\x2c\ -\x30\x2d\x2e\x35\x31\x2e\x31\x36\x2d\x2e\x35\x32\x2e\x35\x31\x5a\ -\x6d\x33\x2e\x38\x38\x2c\x36\x2e\x35\x33\x68\x2d\x36\x76\x32\x2e\ -\x35\x35\x68\x36\x5a\x4d\x33\x2e\x34\x37\x2c\x31\x39\x76\x2e\x38\ -\x33\x48\x37\x2e\x37\x36\x56\x31\x39\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\x2e\x33\x31\x2c\x31\x2e\x37\x35\x56\x33\ -\x2e\x34\x34\x48\x33\x2e\x34\x37\x56\x31\x2e\x37\x35\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\x35\x2e\x32\x31\x2c\x31\x2e\ -\x37\x34\x48\x36\x76\x31\x2e\x37\x48\x35\x2e\x32\x31\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\x37\x2e\x37\x36\x2c\x33\x2e\ -\x34\x35\x48\x36\x2e\x39\x33\x56\x31\x2e\x37\x34\x68\x2e\x38\x33\ -\x5a\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\ -\x67\x3e\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 176.02 58.41\ +\x22>nyl<\ +path class=\x22cls-\ +3\x22 d=\x22M7.87,36.5\ +2a11.26,11.26,0,\ +0,1-.06-2.33c.27\ +-4.08.57-8.29,2.\ +25-12.11a.38.38,\ +0,0,1,.38-.24c1.\ +39-.07,1.38-.08,\ +1,1.05C9,30.43,8\ +.93,41,11.68,48.\ +48c.12.31.05.36-\ +.3.36-1.25,0-1.2\ +6,0-1.64-1A36.19\ +,36.19,0,0,1,7.8\ +7,36.52Z\x22 transf\ +orm=\x22translate(0\ +)\x22/>\ +\x00\x00\x07;\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 40.5 33.52\x22>\ +\ +\x00\x00\x01!\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 38.22 34.57\x22\ +><\ +g id=\x22Layer_2\x22 d\ +ata-name=\x22Layer \ +2\x22>\ \ +\x00\x00\x09\xfe\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 82.23 36.17\x22\ +>