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/configfile.py b/BlocksScreen/configfile.py index 981ac4b2..9536fffd 100644 --- a/BlocksScreen/configfile.py +++ b/BlocksScreen/configfile.py @@ -56,11 +56,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 +78,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 +86,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]: @@ -193,12 +204,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 +225,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] @@ -289,6 +303,40 @@ def add_option( 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: + logging.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: @@ -319,6 +367,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 +392,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 +400,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: @@ -388,4 +445,4 @@ def get_configparser() -> BlocksScreenConfig: if not config_object.has_section("server"): logging.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/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..b743d875 --- /dev/null +++ b/BlocksScreen/lib/network/models.py @@ -0,0 +1,328 @@ +"""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 # static IP or resetting to DHCP on a Wi-Fi profile + VLAN_DHCP = 9 # 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..227b7e9b --- /dev/null +++ b/BlocksScreen/lib/network/worker.py @@ -0,0 +1,2755 @@ +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._deleted_vlan_ids: set[int] = 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() + self._deleted_vlan_ids.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 on the primary wired interface. + + If *ip_address* is empty the VLAN uses DHCP and waits up to 45 s for a + lease; otherwise a static configuration is applied. 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) + + use_dhcp = not ip_address + + 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), + }, + "ipv6": {"method": ("s", "ignore")}, + } + + if use_dhcp: + conn_props["ipv4"] = { + "method": ("s", "auto"), + "route-metric": ("i", 500), + } + else: + 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["ipv4"] = { + "method": ("s", "manual"), + "addresses": ( + "aau", + [[ip_uint, prefix, gw_uint]], + ), + "gateway": ("s", gateway or ""), + "dns": ("au", dns_list), + "route-metric": ("i", 500), + } + + conn_path = await self._nm_settings().add_connection(conn_props) + + if use_dhcp: + vlan_iface = f"{iface}.{vlan_id}" + ok, _msg = await self._async_activate_vlan_with_timeout( + conn_path, vlan_id, vlan_iface, timeout=45.0 + ) + if not ok: + if vlan_id in self._deleted_vlan_ids: + self._deleted_vlan_ids.discard(vlan_id) + logger.info( + "VLAN %d was manually deleted during DHCP " + "activation — skipping cleanup", + vlan_id, + ) + self.connection_result.emit( + ConnectionResult( + False, + f"VLAN {vlan_id} was removed during DHCP activation.", + "vlan_dhcp_timeout", + ) + ) + return + await self._delete_all_connections_by_id(vlan_conn_id) + logger.info( + "Deleted VLAN %d profile after DHCP failure", + vlan_id, + ) + self.connection_result.emit( + ConnectionResult( + False, + "There isn't a DHCP VLAN server.\n" + "Use a static IP address for this VLAN.", + "vlan_dhcp_timeout", + ) + ) + self.state_changed.emit(await self._build_current_state()) + return + else: + 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: + self._deleted_vlan_ids.add(vlan_id) + 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 _async_activate_vlan_with_timeout( + self, + conn_path: str, + vlan_id: int, + iface: str, + timeout: float = 45.0, + ) -> tuple[bool, str]: + """Activate a VLAN and wait for DHCP via D-Bus ``state_changed`` signal. + + Subscribes to ``ActiveConnection.state_changed`` (signature ``'uu'``) + which fires ``(ConnectionState, ConnectionStateReason)`` on every NM + transition. This replaces the old poll-and-sleep loop, cutting + latency from ~2 s per poll to near-instant and eliminating + unnecessary D-Bus round-trips on resource-constrained Pi hardware. + + .. note:: The old code used ``_NM_ACTIVATED = 4`` which was + actually ``DEACTIVATED`` — DHCP success was never detected + via state polling. This version uses the correct enum values. + """ + try: + active_path = await self._nm().activate_connection(conn_path, "/", "/") + except Exception as exc: + return ( + False, + f"VLAN {vlan_id}: activation request failed — {exc}", + ) + + # Fresh proxy for signal subscription lifetime. + ac = dbus_nm.ActiveConnection(bus=self._system_bus, connection_path=active_path) + + try: + async with asyncio.timeout(timeout): + # state_changed signature 'uu' -> (state: int, reason: int) + async for ac_state, ac_reason in ac.state_changed: + if not self._running: + return False, "Worker shutting down" + + try: + state_name = dbus_nm.ConnectionState(ac_state).name + except ValueError: + state_name = str(ac_state) + try: + reason_name = dbus_nm.ConnectionStateReason(ac_reason).name + except ValueError: + reason_name = str(ac_reason) + + logger.debug( + "VLAN %d AC: state=%s reason=%s", + vlan_id, + state_name, + reason_name, + ) + + if ac_state == dbus_nm.ConnectionState.ACTIVATED: + logger.info( + "VLAN %d DHCP activated on %s", + vlan_id, + iface, + ) + return True, "" + + if ac_state in ( + dbus_nm.ConnectionState.DEACTIVATING, + dbus_nm.ConnectionState.DEACTIVATED, + ): + logger.warning( + "VLAN %d DHCP failed: %s/%s on %s", + vlan_id, + state_name, + reason_name, + iface, + ) + return ( + False, + f"VLAN {vlan_id}: no DHCP server " + f"responded on {iface}.\n" + "Use a static IP or connect a " + "DHCP server to this segment.", + ) + + except TimeoutError: + logger.warning( + "VLAN %d DHCP timed out after %.0f s — " + "deactivating to stop NM retry loop", + vlan_id, + timeout, + ) + try: + await self._nm().deactivate_connection(active_path) + except Exception as exc: + logger.debug("VLAN deactivation after DHCP timeout ignored: %s", exc) + return ( + False, + f"VLAN {vlan_id}: DHCP timed out after " + f"{int(timeout)} s.\nNo DHCP server responded " + "on this network segment.\n" + "Use a static IP address instead.", + ) + + except Exception as exc: + err_str = str(exc) + if "does not exist" in err_str or "No such" in err_str: + logger.debug( + "VLAN %d active connection gone (%s) — signal iterator ended", + vlan_id, + err_str, + ) + return ( + False, + f"VLAN {vlan_id}: connection was removed externally.", + ) + raise + + # Signal iterator ended without a terminal state (shouldn't + # happen, but defensive). + return False, f"VLAN {vlan_id}: unexpected end of state stream." + + 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/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 7b0578fe..322c483e 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -7,9 +7,10 @@ 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 @@ -20,8 +21,6 @@ from lib.panels.widgets.updatePage import UpdatePage from lib.printer import Printer from lib.ui.mainWindow_ui import Ui_MainWindow # With header - -# 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 * @@ -50,6 +49,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""" @@ -72,6 +99,7 @@ class MainWindow(QtWidgets.QMainWindow): 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() @@ -162,6 +190,7 @@ def __init__(self): self.ui.main_content_widget.currentChanged.connect(slot=self.reset_tab_indexes) self.call_network_panel.connect(self.networkPanel.show_network_panel) + self.networkPanel.update_wifi_icon.connect(self.change_wifi_icon) self.conn_window.wifi_button_clicked.connect(self.call_network_panel.emit) self.ui.wifi_button.clicked.connect(self.call_network_panel.emit) self.handle_error_response.connect( @@ -216,7 +245,6 @@ def __init__(self): 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() @@ -235,6 +263,7 @@ def handle_cancel_print(self, show: bool = True): @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: @@ -424,6 +453,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 @@ -507,6 +545,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: @@ -522,8 +561,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: @@ -734,6 +773,11 @@ def set_header_nozzle_diameter(self, diam: str): def closeEvent(self, a0: typing.Optional[QtGui.QCloseEvent]) -> None: """Handles GUI closing""" + try: + self.networkPanel.close() + except Exception as e: + _logger.warning("Network panel shutdown error: %s", e) + _loggers = [ logging.getLogger(name) for name in logging.root.manager.loggerDict ] # Get available logger handlers diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 19574cf5..0d0a6df5 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,132 @@ 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 +VLAN_DHCP_TIMEOUT_MS = 50_000 # Generous: worker has 45 s, UI needs headroom 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,272 +170,1887 @@ 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") + # VLAN DHCP: keep loading visible. + if self._pending_operation == PendingOperation.VLAN_DHCP: + # Update display behind the loading overlay so state is + # current when loading is eventually cleared. + self._sync_ethernet_panel(state) + 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) + # 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.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) + return - 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) + # 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() - conn_info_layout.addLayout(sg_info_layout) + self._emit_status_icon(state) + self._sync_active_network_list_icon(state) - # Security section - sec_info_layout = QtWidgets.QVBoxLayout() - sec_info_layout.setObjectName("mn_sec_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. - self.netlist_security_label = QtWidgets.QLabel( - parent=self.mn_information_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) + ] + + 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 + elif self._pending_operation == PendingOperation.VLAN_DHCP: + # Worker confirmed VLAN DHCP success — clear loading and + # refresh the display to show the new VLAN interface. + self._clear_loading() + state = self._nm.current_state + self._display_connected_state(state) + self._emit_status_icon(state) + self._show_info_popup(result.message) + else: + self._show_info_popup(result.message) + else: + msg_lower = result.message.lower() + + # DHCP VLAN / Wi-Fi static-IP errors: clear loading and show the + # reason without the generic error prefix. + if result.error_code in ("vlan_dhcp_timeout", "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 + + # VLAN DHCP — the 50 s UI timer expired before the worker's 45 s + # D-Bus signal timeout. Clear loading and show a specific message. + if self._pending_operation == PendingOperation.VLAN_DHCP: + self._clear_loading() + self._display_connected_state(state) + self._show_error_popup( + "VLAN DHCP timed out.\n" + "No DHCP server responded.\n" + "Use a static IP for this VLAN." + ) + 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() + + # When IP is empty -> DHCP mode (no validation needed) + use_dhcp = not ip_addr + + if not use_dhcp: + 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)) + if use_dhcp: + self._pending_operation = PendingOperation.VLAN_DHCP + self._set_loading_state(True, timeout_ms=VLAN_DHCP_TIMEOUT_MS) + else: + self._pending_operation = PendingOperation.ETHERNET_ON + self._set_loading_state(True) + self._nm.create_vlan_connection( + vlan_id, + ip_addr, # empty -> DHCP + mask if not use_dhcp else "", + gateway if not use_dhcp else "", + dns1 if not use_dhcp else "", + dns2 if not use_dhcp else "", + ) + 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()) font = QtGui.QFont() @@ -569,13 +2058,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 +2074,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 +2087,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 +2105,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 +2149,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 +2160,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 +2183,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 +2224,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 +2253,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 +2275,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 +2303,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 +2312,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 +2335,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 +2345,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 +2364,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 +2375,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 +2384,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 +2402,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 +2488,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 +2512,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 +2525,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 +2537,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 +2555,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 +2562,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 +2574,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 (empty = DHCP)" + ) + 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 +3836,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 +3860,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 +3878,4 @@ def show_network_panel(self) -> None: self.updateGeometry() self.repaint() self.show() + self._nm.scan_networks() diff --git a/BlocksScreen/lib/qrcode_gen.py b/BlocksScreen/lib/qrcode_gen.py index 1901cef1..160ec6fd 100644 --- a/BlocksScreen/lib/qrcode_gen.py +++ b/BlocksScreen/lib/qrcode_gen.py @@ -5,10 +5,11 @@ 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: + """Generate a QR code image from *data* and return it as a Qt-compatible image.""" qr = qrcode.QRCode( version=1, error_correction=qrcode.ERROR_CORRECT_L, @@ -19,14 +20,28 @@ def make_qrcode(data) -> ImageQt.ImageQt: 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() + return ImageQt.toqimage(pil_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 ''};" - ) + """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/resources/icon_resources.qrc b/BlocksScreen/lib/ui/resources/icon_resources.qrc index f9d1f0a9..8f66472a 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 diff --git a/BlocksScreen/lib/ui/resources/icon_resources_rc.py b/BlocksScreen/lib/ui/resources/icon_resources_rc.py index 3bbc3133..4e2ad197 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources_rc.py +++ b/BlocksScreen/lib/ui/resources/icon_resources_rc.py @@ -2,7 +2,7 @@ # Resource object code # -# Created by: The Resource Compiler for PyQt5 (Qt v5.15.15) +# Created by: The Resource Compiler for PyQt6 (Qt v5.15.15) # # WARNING! All changes made in this file will be lost! @@ -19325,199 +19325,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 +19907,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 +19916,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 +20354,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 +20697,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\ @@ -27026,14 +26765,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\ @@ -27046,10 +26777,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\ @@ -27058,11 +26797,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\ @@ -27072,10 +26811,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\ @@ -27442,81 +27177,80 @@ \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\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x82\ +\x00\x00\x00\x46\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x86\ +\x00\x00\x0f\x50\x00\x01\x00\x00\x00\x01\x00\x04\xaa\xfb\ +\x00\x00\x0f\x74\x00\x00\x00\x00\x00\x01\x00\x04\xb6\x4c\ +\x00\x00\x0f\x96\x00\x00\x00\x00\x00\x01\x00\x04\xbd\xf1\ +\x00\x00\x0f\xb2\x00\x00\x00\x00\x00\x01\x00\x04\xce\xf1\ +\x00\x00\x0f\xd2\x00\x00\x00\x00\x00\x01\x00\x04\xd4\x8a\ \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\x10\x12\x00\x00\x00\x00\x00\x01\x00\x04\xdf\x06\ +\x00\x00\x10\x32\x00\x00\x00\x00\x00\x01\x00\x04\xe4\x8a\ +\x00\x00\x10\x52\x00\x00\x00\x00\x00\x01\x00\x04\xea\x23\ +\x00\x00\x10\x84\x00\x00\x00\x00\x00\x01\x00\x04\xef\x25\ +\x00\x00\x10\xa4\x00\x00\x00\x00\x00\x01\x00\x04\xf4\xbe\ +\x00\x00\x10\xd8\x00\x00\x00\x00\x00\x01\x00\x04\xff\x32\ +\x00\x00\x11\x0c\x00\x00\x00\x00\x00\x01\x00\x05\x09\xac\ +\x00\x00\x11\x40\x00\x00\x00\x00\x00\x01\x00\x05\x14\x0b\ +\x00\x00\x11\x74\x00\x00\x00\x00\x00\x01\x00\x05\x1f\x56\ \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\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x29\xca\ +\x00\x00\x11\xd8\x00\x00\x00\x00\x00\x01\x00\x05\x33\x60\ +\x00\x00\x12\x08\x00\x00\x00\x00\x00\x01\x00\x05\x3f\x3c\ +\x00\x00\x12\x2c\x00\x00\x00\x00\x00\x01\x00\x05\x45\x80\ +\x00\x00\x12\x56\x00\x00\x00\x00\x00\x01\x00\x05\x4d\x09\ +\x00\x00\x12\x82\x00\x00\x00\x00\x00\x01\x00\x05\x53\x67\ +\x00\x00\x12\xb8\x00\x00\x00\x00\x00\x01\x00\x05\x5b\x56\ +\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x68\xf9\ +\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x6e\x2e\ +\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x78\x03\ \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\x12\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x7f\xcd\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa2\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x29\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\x60\x00\x00\x00\x00\x00\x01\x00\x05\xfd\xa9\ -\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x06\x00\xf7\ -\x00\x00\x15\xac\x00\x00\x00\x00\x00\x01\x00\x06\x04\x85\ -\x00\x00\x15\xd2\x00\x00\x00\x00\x00\x01\x00\x06\x09\x26\ -\x00\x00\x15\xe6\x00\x00\x00\x00\x00\x01\x00\x06\x12\xf8\ -\x00\x00\x16\x12\x00\x00\x00\x00\x00\x01\x00\x06\x18\x42\ -\x00\x00\x16\x3a\x00\x00\x00\x00\x00\x01\x00\x06\x1e\x49\ -\x00\x00\x16\x50\x00\x00\x00\x00\x00\x01\x00\x06\x1f\x2d\ -\x00\x00\x16\x7c\x00\x00\x00\x00\x00\x01\x00\x06\x21\x76\ -\x00\x00\x16\x92\x00\x00\x00\x00\x00\x01\x00\x06\x28\x26\ -\x00\x00\x16\xae\x00\x00\x00\x00\x00\x01\x00\x06\x2b\x6a\ -\x00\x00\x16\xc6\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x96\ -\x00\x00\x16\xe0\x00\x00\x00\x00\x00\x01\x00\x06\x32\x57\ -\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\x79\ -\x00\x00\x17\x20\x00\x00\x00\x00\x00\x01\x00\x06\x39\x6c\ -\x00\x00\x17\x40\x00\x00\x00\x00\x00\x01\x00\x06\x3c\x70\ -\x00\x00\x17\x62\x00\x00\x00\x00\x00\x01\x00\x06\x3d\x91\ -\x00\x00\x17\x82\x00\x00\x00\x00\x00\x01\x00\x06\x40\x65\ -\x00\x00\x17\xb0\x00\x00\x00\x00\x00\x01\x00\x06\x48\xd1\ -\x00\x00\x17\xd4\x00\x00\x00\x00\x00\x01\x00\x06\x50\x91\ -\x00\x00\x17\xf8\x00\x00\x00\x00\x00\x01\x00\x06\x55\xac\ -\x00\x00\x18\x20\x00\x00\x00\x00\x00\x01\x00\x06\x56\xff\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x28\x00\x00\x00\xa3\ +\x00\x00\x13\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x84\x93\ +\x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x8c\x47\ +\x00\x00\x13\x4c\x00\x00\x00\x00\x00\x01\x00\x05\x8e\x6c\ +\x00\x00\x13\x84\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xec\ +\x00\x00\x13\xa0\x00\x00\x00\x00\x00\x01\x00\x05\x97\x99\ +\x00\x00\x13\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x6e\ +\x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x9d\x5e\ +\x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xa1\x87\ +\x00\x00\x14\x12\x00\x00\x00\x00\x00\x01\x00\x05\xa7\xbe\ +\x00\x00\x14\x2c\x00\x00\x00\x00\x00\x01\x00\x05\xbc\xcb\ +\x00\x00\x14\x4e\x00\x00\x00\x00\x00\x01\x00\x05\xc1\xbb\ +\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xc4\xba\ +\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xca\xc4\ +\x00\x00\x14\xac\x00\x00\x00\x00\x00\x01\x00\x05\xce\x13\ +\x00\x00\x14\xc4\x00\x00\x00\x00\x00\x01\x00\x05\xd2\x34\ +\x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xd8\x2b\ +\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xda\x3c\ +\x00\x00\x15\x06\x00\x00\x00\x00\x00\x01\x00\x05\xdd\xff\ +\x00\x00\x15\x2c\x00\x00\x00\x00\x00\x01\x00\x05\xe7\xb3\ +\x00\x00\x15\x56\x00\x00\x00\x00\x00\x01\x00\x05\xeb\x01\ +\x00\x00\x15\x78\x00\x00\x00\x00\x00\x01\x00\x05\xee\x8f\ +\x00\x00\x15\x9e\x00\x00\x00\x00\x00\x01\x00\x05\xf3\x30\ +\x00\x00\x15\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xfd\x02\ +\x00\x00\x15\xde\x00\x00\x00\x00\x00\x01\x00\x06\x02\x4c\ +\x00\x00\x16\x06\x00\x00\x00\x00\x00\x01\x00\x06\x08\x53\ +\x00\x00\x16\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x09\x37\ +\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x0b\x80\ +\x00\x00\x16\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x12\x30\ +\x00\x00\x16\x7a\x00\x00\x00\x00\x00\x01\x00\x06\x15\x74\ +\x00\x00\x16\x92\x00\x00\x00\x00\x00\x01\x00\x06\x16\xa0\ +\x00\x00\x16\xac\x00\x00\x00\x00\x00\x01\x00\x06\x1c\x61\ +\x00\x00\x16\xce\x00\x00\x00\x00\x00\x01\x00\x06\x1d\x83\ +\x00\x00\x16\xec\x00\x00\x00\x00\x00\x01\x00\x06\x23\x76\ +\x00\x00\x17\x0c\x00\x00\x00\x00\x00\x01\x00\x06\x26\x7a\ +\x00\x00\x17\x2e\x00\x00\x00\x00\x00\x01\x00\x06\x27\x9b\ +\x00\x00\x17\x4e\x00\x00\x00\x00\x00\x01\x00\x06\x2a\x6f\ +\x00\x00\x17\x7c\x00\x00\x00\x00\x00\x01\x00\x06\x32\xdb\ +\x00\x00\x17\xa0\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x9b\ +\x00\x00\x17\xc4\x00\x00\x00\x00\x00\x01\x00\x06\x3f\xb6\ +\x00\x00\x17\xec\x00\x00\x00\x00\x00\x01\x00\x06\x41\x09\ " qt_resource_struct_v2 = b"\ @@ -27778,155 +27512,153 @@ \x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x81\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x10\x00\x00\x00\x82\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x82\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x46\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x86\ \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\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x0f\x70\x00\x00\x00\x00\x00\x01\x00\x04\xb0\x94\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x0f\x90\x00\x01\x00\x00\x00\x01\x00\x04\xb6\xbb\ +\x00\x00\x0f\x50\x00\x01\x00\x00\x00\x01\x00\x04\xaa\xfb\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x0f\xb4\x00\x00\x00\x00\x00\x01\x00\x04\xc2\x0c\ +\x00\x00\x0f\x74\x00\x00\x00\x00\x00\x01\x00\x04\xb6\x4c\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x0f\xd6\x00\x00\x00\x00\x00\x01\x00\x04\xc9\xb1\ +\x00\x00\x0f\x96\x00\x00\x00\x00\x00\x01\x00\x04\xbd\xf1\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0f\xb2\x00\x00\x00\x00\x00\x01\x00\x04\xce\xf1\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x0f\xd2\x00\x00\x00\x00\x00\x01\x00\x04\xd4\x8a\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ \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\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x10\x36\x00\x00\x00\x00\x00\x01\x00\x04\xe9\xb6\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ -\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\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x10\x9e\x00\x00\x00\x00\x00\x01\x00\x04\xfe\xb1\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ -\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\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x11\x22\x00\x00\x00\x00\x00\x01\x00\x05\x19\xda\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x11\x56\x00\x00\x00\x00\x00\x01\x00\x05\x24\x39\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x2f\x84\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x10\x12\x00\x00\x00\x00\x00\x01\x00\x04\xdf\x06\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x10\x32\x00\x00\x00\x00\x00\x01\x00\x04\xe4\x8a\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x10\x52\x00\x00\x00\x00\x00\x01\x00\x04\xea\x23\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x10\x84\x00\x00\x00\x00\x00\x01\x00\x04\xef\x25\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x10\xa4\x00\x00\x00\x00\x00\x01\x00\x04\xf4\xbe\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x10\xd8\x00\x00\x00\x00\x00\x01\x00\x04\xff\x32\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x11\x0c\x00\x00\x00\x00\x00\x01\x00\x05\x09\xac\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x11\x40\x00\x00\x00\x00\x00\x01\x00\x05\x14\x0b\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x11\x74\x00\x00\x00\x00\x00\x01\x00\x05\x1f\x56\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x94\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x11\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x39\xf8\ +\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x29\xca\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x43\x8e\ +\x00\x00\x11\xd8\x00\x00\x00\x00\x00\x01\x00\x05\x33\x60\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x6a\ +\x00\x00\x12\x08\x00\x00\x00\x00\x00\x01\x00\x05\x3f\x3c\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\x42\x00\x00\x00\x00\x00\x01\x00\x05\x55\xae\ +\x00\x00\x12\x2c\x00\x00\x00\x00\x00\x01\x00\x05\x45\x80\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\x6c\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x37\ +\x00\x00\x12\x56\x00\x00\x00\x00\x00\x01\x00\x05\x4d\x09\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x12\x98\x00\x00\x00\x00\x00\x01\x00\x05\x63\x95\ +\x00\x00\x12\x82\x00\x00\x00\x00\x00\x01\x00\x05\x53\x67\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x12\xce\x00\x00\x00\x00\x00\x01\x00\x05\x6b\x84\ +\x00\x00\x12\xb8\x00\x00\x00\x00\x00\x01\x00\x05\x5b\x56\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x79\x27\ +\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x68\xf9\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x7e\x5c\ +\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x6e\x2e\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x12\xec\x00\x00\x00\x00\x00\x01\x00\x05\x88\x31\ +\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x78\x03\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9f\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa0\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x13\x14\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xfb\ +\x00\x00\x12\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x7f\xcd\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa2\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x29\x00\x00\x00\xa3\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x94\xc1\ +\x00\x00\x13\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x84\x93\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x75\ +\x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x8c\x47\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\x9a\ +\x00\x00\x13\x4c\x00\x00\x00\x00\x00\x01\x00\x05\x8e\x6c\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x13\x9a\x00\x00\x00\x00\x00\x01\x00\x05\xa0\x1a\ +\x00\x00\x13\x84\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xec\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xa7\xc7\ +\x00\x00\x13\xa0\x00\x00\x00\x00\x00\x01\x00\x05\x97\x99\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xac\x9c\ +\x00\x00\x13\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x6e\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xad\x8c\ +\x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x9d\x5e\ \x00\x00\x01\x9c\x48\x63\x91\xdd\ -\x00\x00\x14\x02\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xb5\ +\x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xa1\x87\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb7\xec\ +\x00\x00\x14\x12\x00\x00\x00\x00\x00\x01\x00\x05\xa7\xbe\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xcc\xf9\ +\x00\x00\x14\x2c\x00\x00\x00\x00\x00\x01\x00\x05\xbc\xcb\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xd1\xe9\ +\x00\x00\x14\x4e\x00\x00\x00\x00\x00\x01\x00\x05\xc1\xbb\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xd4\xe8\ +\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xc4\xba\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x90\x00\x00\x00\x00\x00\x01\x00\x05\xda\xf2\ +\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xca\xc4\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x14\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xde\x41\ +\x00\x00\x14\xac\x00\x00\x00\x00\x00\x01\x00\x05\xce\x13\ \x00\x00\x01\x9c\x48\x63\x91\xdd\ -\x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xe2\x62\ +\x00\x00\x14\xc4\x00\x00\x00\x00\x00\x01\x00\x05\xd2\x34\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x14\xf0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x59\ +\x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xd8\x2b\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x04\x00\x00\x00\x00\x00\x01\x00\x05\xea\x6a\ +\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xda\x3c\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x15\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xee\x2d\ +\x00\x00\x15\x06\x00\x00\x00\x00\x00\x01\x00\x05\xdd\xff\ \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\x9c\xb3\x48\x8b\xb6\ -\x00\x00\x15\x60\x00\x00\x00\x00\x00\x01\x00\x05\xfd\xa9\ +\x00\x00\x15\x2c\x00\x00\x00\x00\x00\x01\x00\x05\xe7\xb3\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x06\x00\xf7\ +\x00\x00\x15\x56\x00\x00\x00\x00\x00\x01\x00\x05\xeb\x01\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\xac\x00\x00\x00\x00\x00\x01\x00\x06\x04\x85\ +\x00\x00\x15\x78\x00\x00\x00\x00\x00\x01\x00\x05\xee\x8f\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x15\xd2\x00\x00\x00\x00\x00\x01\x00\x06\x09\x26\ +\x00\x00\x15\x9e\x00\x00\x00\x00\x00\x01\x00\x05\xf3\x30\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\xe6\x00\x00\x00\x00\x00\x01\x00\x06\x12\xf8\ +\x00\x00\x15\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xfd\x02\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\x12\x00\x00\x00\x00\x00\x01\x00\x06\x18\x42\ +\x00\x00\x15\xde\x00\x00\x00\x00\x00\x01\x00\x06\x02\x4c\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\x3a\x00\x00\x00\x00\x00\x01\x00\x06\x1e\x49\ +\x00\x00\x16\x06\x00\x00\x00\x00\x00\x01\x00\x06\x08\x53\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\x50\x00\x00\x00\x00\x00\x01\x00\x06\x1f\x2d\ +\x00\x00\x16\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x09\x37\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x16\x7c\x00\x00\x00\x00\x00\x01\x00\x06\x21\x76\ +\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x0b\x80\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x16\x92\x00\x00\x00\x00\x00\x01\x00\x06\x28\x26\ +\x00\x00\x16\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x12\x30\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\xae\x00\x00\x00\x00\x00\x01\x00\x06\x2b\x6a\ +\x00\x00\x16\x7a\x00\x00\x00\x00\x00\x01\x00\x06\x15\x74\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\xc6\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x96\ +\x00\x00\x16\x92\x00\x00\x00\x00\x00\x01\x00\x06\x16\xa0\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\xe0\x00\x00\x00\x00\x00\x01\x00\x06\x32\x57\ +\x00\x00\x16\xac\x00\x00\x00\x00\x00\x01\x00\x06\x1c\x61\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\x79\ +\x00\x00\x16\xce\x00\x00\x00\x00\x00\x01\x00\x06\x1d\x83\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x20\x00\x00\x00\x00\x00\x01\x00\x06\x39\x6c\ +\x00\x00\x16\xec\x00\x00\x00\x00\x00\x01\x00\x06\x23\x76\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x17\x40\x00\x00\x00\x00\x00\x01\x00\x06\x3c\x70\ +\x00\x00\x17\x0c\x00\x00\x00\x00\x00\x01\x00\x06\x26\x7a\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x62\x00\x00\x00\x00\x00\x01\x00\x06\x3d\x91\ +\x00\x00\x17\x2e\x00\x00\x00\x00\x00\x01\x00\x06\x27\x9b\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x82\x00\x00\x00\x00\x00\x01\x00\x06\x40\x65\ +\x00\x00\x17\x4e\x00\x00\x00\x00\x00\x01\x00\x06\x2a\x6f\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x17\xb0\x00\x00\x00\x00\x00\x01\x00\x06\x48\xd1\ +\x00\x00\x17\x7c\x00\x00\x00\x00\x00\x01\x00\x06\x32\xdb\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x17\xd4\x00\x00\x00\x00\x00\x01\x00\x06\x50\x91\ +\x00\x00\x17\xa0\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x9b\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x17\xf8\x00\x00\x00\x00\x00\x01\x00\x06\x55\xac\ +\x00\x00\x17\xc4\x00\x00\x00\x00\x00\x01\x00\x06\x3f\xb6\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x18\x20\x00\x00\x00\x00\x00\x01\x00\x06\x56\xff\ +\x00\x00\x17\xec\x00\x00\x00\x00\x00\x01\x00\x06\x41\x09\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ " @@ -27944,4 +27676,4 @@ def qInitResources(): def qCleanupResources(): QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) -qInitResources() +qInitResources() \ 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/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\ +>