From ffcdc7d51689772b5ab67c978fd2fb1e32bed772 Mon Sep 17 00:00:00 2001 From: gmmcosta15 Date: Wed, 19 Nov 2025 16:49:55 +0000 Subject: [PATCH 01/26] refactor: change network list to listview --- BlocksScreen/lib/panels/networkWindow.py | 497 ++++++++++++----------- 1 file changed, 262 insertions(+), 235 deletions(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 57617efe..0e7a32d1 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -1,6 +1,7 @@ import logging import typing -import subprocess # nosec: B404 +import copy +import subprocess from functools import partial from lib.network import SdbusNetworkManagerAsync @@ -8,7 +9,14 @@ from lib.ui.wifiConnectivityWindow_ui import Ui_wifi_stacked_page from lib.utils.list_button import ListCustomButton from lib.panels.widgets.keyboardPage import CustomQwertyKeyboard +from lib.utils.blocks_button import BlocksCustomButton +from lib.utils.blocks_frame import BlocksCustomFrame +from lib.panels.widgets.loadPage import LoadScreen +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 QVariant +from PyQt6.QtWidgets import QScroller, QScrollerProperties logger = logging.getLogger("logs/BlocksScreen.log") @@ -138,23 +146,41 @@ class NetworkControlWindow(QtWidgets.QStackedWidget): delete_network_signal = QtCore.pyqtSignal(str, name="delete-network") def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: - super(NetworkControlWindow, self).__init__(parent) - self.background: typing.Optional[QtGui.QPixmap] = None + if parent: + super().__init__(parent) + else: + super().__init__() + self.panel = Ui_wifi_stacked_page() self.panel.setupUi(self) + + self._setupUI() + #self.background: typing.Optional[QtGui.QPixmap] = None + self.ongoing_update: bool = False + self.popup = Popup(self) self.sdbus_network = SdbusNetworkManagerAsync() self.start: bool = True - self.saved_network = dict + self.saved_network = {} + + self.load_popup: LoadScreen = LoadScreen(self) + self.repeated_request_status = QtCore.QTimer() + self.repeated_request_status.setInterval(2000) # every 2 seconds self._load_timer = QtCore.QTimer() self._load_timer.setSingleShot(True) self._load_timer.timeout.connect(self._handle_load_timeout) + + #View Models and Controllers + self.model = EntryListModel() + self.model.setParent(self.network_list_widget) + self.entry_delegate = EntryDelegate() + self.network_list_widget.setModel(self.model) + self.network_list_widget.setItemDelegate(self.entry_delegate) + self.entry_delegate.item_selected.connect(self.ssid_item_clicked) + self.panel.network_backButton.clicked.connect(self.reset_view_model) # Network Scan - self.network_list_widget = QtWidgets.QListWidget( - parent=self.panel.network_list_page - ) self.build_network_list() self.network_list_worker = BuildNetworkList() @@ -167,10 +193,7 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: self.sdbus_network.nm_state_change.connect(self.evaluate_network_state) self.panel.wifi_button.clicked.connect( - partial( - self.setCurrentIndex, - self.indexOf(self.panel.network_list_page), - ) + partial(self.setCurrentIndex, self.indexOf(self.panel.network_list_page)) ) self.panel.hotspot_button.clicked.connect( partial(self.setCurrentIndex, self.indexOf(self.panel.hotspot_page)) @@ -322,6 +345,7 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: self.network_list_worker.build() self.request_network_scan.emit() self.hide() + self.info_box_load() self.qwerty = CustomQwertyKeyboard(self) @@ -351,6 +375,54 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: ) ) + def handle_update_end(self) -> None: + """Handles update end signal + (closes loading page, returns to normal operation) + """ + if self.load_popup.isVisible(): + self.load_popup.close() + self.repeated_request_status.stop() + self.request_network_scan.emit() + self.build_model_list() + + def handle_ongoing_update(self) -> None: + """Handled ongoing update signal, + calls loading page (blocks user interaction) + """ + self.load_popup.set_status_message("Updating...") + self.load_popup.show() + self.repeated_request_status.start(2000) + + # View Model Methods + def reset_view_model(self) -> None: + """Clears items from ListView + (Resets `QAbstractListModel` by clearing entries) + """ + self.model.clear() + self.entry_delegate.clear() + + def deleteLater(self) -> None: + """Schedule the object for deletion, resets the list model first""" + self.reset_view_model() + return super().deleteLater() + + def showEvent(self, event: QtGui.QShowEvent | None) -> None: + """Re-add clients to update list""" + self.build_model_list() + return super().showEvent(event) + + def build_model_list(self) -> None: + """Builds the model list (`self.model`) containing updatable clients""" + self.network_list_widget.blockSignals(True) + self.model.clear() + #logger.debug(f"len saved: {len(self.saved_network.items())}") + test:dict = copy.copy(self.saved_network) + if test.items(): + for ssid,(signal,is_saved) in test.items(): + self.add_network_entry(ssid=ssid, signal=signal, is_saved=is_saved) + + self.network_list_widget.blockSignals(False) + def saved_wifi_option_selected(self): """Handle connect/delete network button clicks""" _sender = self.sender() @@ -764,32 +836,39 @@ def add_network(self) -> None: self.panel.add_network_validation_button.setEnabled(True) self.panel.add_network_validation_button.repaint() self.popup.new_message(message_type=Popup.MessageType.ERROR, message=message) +<<<<<<< HEAD @QtCore.pyqtSlot(QtWidgets.QListWidgetItem, name="ssid_item_clicked") def ssid_item_clicked(self, item: QtWidgets.QListWidgetItem) -> None: +======= + + @QtCore.pyqtSlot(ListItem, name="ssid_item_clicked") + def ssid_item_clicked(self, item: ListItem) -> None: +>>>>>>> da41c34 (refactor: change network list to listview) """Handles when a network is clicked on the QListWidget. Args: item (QListWidgetItem): The list entry that was clicked """ - _current_item: QtWidgets.QWidget = ( - self.panel.network_list_widget.itemWidget(item) # type: ignore - ) - if _current_item: - _current_ssid_name = _current_item.findChild(QtWidgets.QLabel).text() - - if ( - _current_ssid_name in self.sdbus_network.get_saved_ssid_names() - ): # Network already saved go to the information page - self.setCurrentIndex(self.indexOf(self.panel.saved_connection_page)) - self.panel.saved_connection_network_name.setText( - str(_current_ssid_name) - ) - else: # Network not saved go to the add network page - self.setCurrentIndex(self.indexOf(self.panel.add_network_page)) - self.panel.add_network_network_label.setText( - str(_current_ssid_name) - ) # Add the network name to the title + if not item: + return + + _current_ssid_name = item.text + #_current_ssid_name = self.saved_network.get(item.text, {}) + self.selected_item = copy.copy(item) + if ( + _current_ssid_name in self.sdbus_network.get_saved_ssid_names() + ): # Network already saved go to the information page + self.setCurrentIndex(self.indexOf(self.panel.saved_connection_page)) + self.panel.saved_connection_network_name.setText( + str(_current_ssid_name) + ) + else: # Network not saved go to the add network page + self.setCurrentIndex(self.indexOf(self.panel.add_network_page)) + self.panel.add_network_network_label.setText( + str(_current_ssid_name) + ) # Add the network name to the title + def update_network( self, @@ -810,29 +889,25 @@ def update_network( self.setCurrentIndex(self.indexOf(self.panel.network_list_page)) @QtCore.pyqtSlot(list, name="finished-network-list-build") +<<<<<<< HEAD def handle_network_list(self, data: typing.List[typing.Tuple]) -> None: """Handle available network list update""" scroll_bar_position = self.network_list_widget.verticalScrollBar().value() +======= + def handle_network_list(self, data: typing.Dict) -> None: +>>>>>>> da41c34 (refactor: change network list to listview) self.network_list_widget.blockSignals(True) - self.network_list_widget.clear() - self.network_list_widget.setSpacing(35) for entry in data: - if entry == "separator": - self.separator_item() + if entry[0] == self.sdbus_network.hotspot_ssid: continue - elif entry == "blank": - self.blank_space_item() + if entry == "blank": continue - if entry[0] == self.sdbus_network.hotspot_ssid: + if entry == "separator": continue - self.network_button_item(*entry) - - max_v = self.network_list_widget.verticalScrollBar().maximum() - if scroll_bar_position > max_v: - self.network_list_widget.verticalScrollBar().setValue(max_v) - else: - self.network_list_widget.verticalScrollBar().setValue(scroll_bar_position) - self.network_list_widget.verticalScrollBar().update() + self.saved_network[entry[0]] = (entry[1], entry[2] == "Saved" or entry[2] == "Active") + self.build_model_list() + self.network_list_widget.blockSignals(False) + self.evaluate_network_state() QtCore.QTimer().singleShot(10000, lambda: self.network_list_worker.build()) @@ -852,21 +927,6 @@ def handle_button_click(self, ssid: str): self.setCurrentIndex(self.indexOf(self.panel.add_network_page)) self.panel.add_network_network_label.setText(str(ssid)) - def event(self, event: QtCore.QEvent) -> bool: - """Receives PyQt eEvents, this method is reimplemented from the QEvent class - - Args: - event (QtCore.QEvent) - - Returns: - bool: Event has been handled or not 1 - """ - if event.type() == QtCore.QEvent.Type.ApplicationActivated: - # Request a networks scan right at the start of the application - self.request_network_scan.emit() - return False - return super().event(event) - def setCurrentIndex(self, index: int): """Re-implementation of the QStackedWidget setCurrentIndex method in order to clear and display text as needed for each panel on the StackedWidget @@ -936,198 +996,165 @@ def show_network_panel( self.updateGeometry() self.update() self.show() - + + def add_network_entry(self, ssid: str, signal: int, is_saved:str) -> None: + """Adds a new item to the list model""" + + wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/no_wifi.svg") + if 70 <= signal <= 100: + wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/3bar_wifi.svg") + elif signal >= 40: + wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/2bar_wifi.svg") + elif 1 < signal < 40: + wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/1bar_wifi.svg") + + item = ListItem( + text=ssid, + left_icon=wifi_pixmap, + right_text=f"Signal - {signal} % | {'Active' if is_saved else 'Protect'} ", + selected=False, + allow_check=False, + _lfontsize=17, + _rfontsize=13, + height=60, + ) + self.model.add_item(item) + + def _setupUI(self) -> None: + + """Sets up the UI components and layout for the network window.""" + + font_id = QtGui.QFontDatabase.addApplicationFont( + ":/font/media/fonts for text/Momcake-Bold.ttf" + ) + font_family = QtGui.QFontDatabase.applicationFontFamilies(font_id)[0] + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + ) + sizePolicy.setHorizontalStretch(1) + #sizePolicy.setVerticalStretch(1) + self.setSizePolicy(sizePolicy) + self.setMinimumSize(QtCore.QSize(800, 500)) + #self.setMaximumSize(QtCore.QSize(800, 500)) + self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.main_content_layout = QtWidgets.QHBoxLayout() + self.main_content_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + + font = QtGui.QFont() + font.setFamily(font_family) + font.setPointSize(24) + + self.header_title = QtWidgets.QLabel(self) + self.header_title.setMinimumSize(QtCore.QSize(100, 60)) + self.header_title.setMaximumSize(QtCore.QSize(16777215, 60)) + self.header_title.setFont(font) + + # Timer for loading screen timeout + self._load_timer = QtCore.QTimer(self) + + #Buttons frame for update buttons + self.network_buttons_frame = BlocksCustomFrame() + #self.network_buttons_frame.setMinimumSize(QtCore.QSize(100, 100)) + #self.network_buttons_frame.setMaximumSize(QtCore.QSize(800, 450)) + + #List widget for update buttons + self.network_list_widget = QtWidgets.QListView(self.network_buttons_frame) + + self.network_buttons_layout = QtWidgets.QVBoxLayout() + self.network_buttons_layout.setContentsMargins(15, 20, 20, 5) + self.network_buttons_layout.addWidget(self.network_list_widget, 0, QtCore.Qt.AlignmentFlag.AlignBottom) + self.network_buttons_frame.setLayout(self.network_buttons_layout) + + self.main_content_layout.addWidget(self.network_buttons_frame, 0) + self.setLayout(self.main_content_layout) + def build_network_list(self) -> None: - """Build available/saved network list""" + """Build available/saved network list with optimized palette setup.""" + def set_brush_for_all_groups(palette, role, color, style=QtCore.Qt.BrushStyle.SolidPattern): + """Helper to set a brush for Active, Inactive, and Disabled states.""" + brush = QtGui.QBrush(QtGui.QColor(*color)) + brush.setStyle(style) + for group in [ + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorGroup.Inactive, + QtGui.QPalette.ColorGroup.Disabled, + ]: + palette.setBrush(group, role, brush) + palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Active, - QtGui.QPalette.ColorRole.Button, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) - palette.setBrush( - QtGui.QPalette.ColorGroup.Active, - QtGui.QPalette.ColorRole.Base, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Active, - QtGui.QPalette.ColorRole.Window, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Active, - QtGui.QPalette.ColorRole.Highlight, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Active, - QtGui.QPalette.ColorRole.Link, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Inactive, - QtGui.QPalette.ColorRole.Button, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) - palette.setBrush( - QtGui.QPalette.ColorGroup.Inactive, - QtGui.QPalette.ColorRole.Base, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Inactive, - QtGui.QPalette.ColorRole.Window, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Inactive, - QtGui.QPalette.ColorRole.Highlight, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Inactive, - QtGui.QPalette.ColorRole.Link, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Disabled, - QtGui.QPalette.ColorRole.Button, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) - palette.setBrush( - QtGui.QPalette.ColorGroup.Disabled, - QtGui.QPalette.ColorRole.Base, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Disabled, - QtGui.QPalette.ColorRole.Window, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Disabled, - QtGui.QPalette.ColorRole.Highlight, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Disabled, - QtGui.QPalette.ColorRole.Link, - brush, - ) + + # Transparent backgrounds + set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Button, (0, 0, 0, 0)) + set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Window, (0, 0, 0, 0)) + + # Base (black, no brush) + set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Base, (0, 0, 0), QtCore.Qt.BrushStyle.NoBrush) + + # Highlight & link + set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Highlight, (0, 120, 215, 0)) + set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Link, (0, 0, 255, 0)) + + # Apply palette self.network_list_widget.setPalette(palette) + + # General QListView setup self.network_list_widget.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) self.network_list_widget.setStyleSheet("background-color:transparent") self.network_list_widget.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) self.network_list_widget.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.network_list_widget.setVerticalScrollBarPolicy( - QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) - self.network_list_widget.setHorizontalScrollBarPolicy( - QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) - self.network_list_widget.setSizeAdjustPolicy( - QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents - ) + self.network_list_widget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.network_list_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.network_list_widget.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustIgnored) self.network_list_widget.setAutoScroll(False) self.network_list_widget.setProperty("showDropIndicator", False) self.network_list_widget.setDefaultDropAction(QtCore.Qt.DropAction.IgnoreAction) self.network_list_widget.setAlternatingRowColors(False) - self.network_list_widget.setSelectionMode( - QtWidgets.QAbstractItemView.SelectionMode.NoSelection - ) - self.network_list_widget.setSelectionBehavior( - QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems + self.network_list_widget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.NoSelection) + self.network_list_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems) + self.network_list_widget.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) + self.network_list_widget.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) + self.network_list_widget.setUniformItemSizes(True) + + #self.network_list_widget.setStyleSheet("QListView { padding-bottom: 8px; }") + + # inside build_network_list + + # ... (your palette / list-view setup) + + # Grab gesture on the viewport + viewport = self.network_list_widget.viewport() + QScroller.grabGesture(viewport, QScroller.ScrollerGestureType.TouchGesture) + QScroller.grabGesture(viewport, QScroller.ScrollerGestureType.LeftMouseButtonGesture) + + scroller = QScroller.scroller(viewport) + props = scroller.scrollerProperties() + + props.setScrollMetric( + QScrollerProperties.ScrollMetric.VerticalOvershootPolicy, + QVariant(QScrollerProperties.OvershootPolicy.OvershootAlwaysOff) ) - self.network_list_widget.setVerticalScrollMode( - QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel + props.setScrollMetric( + QScrollerProperties.ScrollMetric.OvershootDragResistanceFactor, + QVariant(1.0) ) - self.network_list_widget.setHorizontalScrollMode( - QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel + props.setScrollMetric( + QScrollerProperties.ScrollMetric.OvershootDragDistanceFactor, + QVariant(0.0) ) - QtWidgets.QScroller.grabGesture( - self.network_list_widget, - QtWidgets.QScroller.ScrollerGestureType.TouchGesture, + props.setScrollMetric( + QScrollerProperties.ScrollMetric.OvershootScrollDistanceFactor, + QVariant(0.0) ) - QtWidgets.QScroller.grabGesture( - self.network_list_widget, - QtWidgets.QScroller.ScrollerGestureType.LeftMouseButtonGesture, + props.setScrollMetric( + QScrollerProperties.ScrollMetric.OvershootScrollTime, + QVariant(0.0) ) - self.network_list_widget.setObjectName("network_list_widget") - self.panel.nl_content_layout.addWidget(self.network_list_widget) + scroller.setScrollerProperties(props) + + # ... add widget to layout, etc. - def separator_item(self) -> None: - """Add separator item to network list""" - separator_item = QtWidgets.QListWidgetItem() - separator_widget = QtWidgets.QLabel() - separator_widget.setStyleSheet( - "background-color: gray; margin: 1px 1px; min-height: 1px; max-height: 1px;" - ) - separator_item.setSizeHint(QtCore.QSize(0, 2)) # Total vertical space: 2px - self.network_list_widget.addItem(separator_item) - self.network_list_widget.setItemWidget(separator_item, separator_widget) - - def blank_space_item(self) -> None: - """Add blank space item to network list""" - spacer_item = QtWidgets.QListWidgetItem() - spacer_widget = QtWidgets.QWidget() - spacer_widget.setFixedHeight(10) # Adjust height as needed - spacer_item.setSizeHint(spacer_widget.sizeHint()) - self.network_list_widget.addItem(spacer_item) - self.network_list_widget.setItemWidget(spacer_item, spacer_widget) - - def network_button_item(self, ssid, signal, right_text, /) -> None: - """Add a network entry to network list""" - wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/no_wifi.svg") - if 70 <= signal <= 100: - wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/3bar_wifi.svg") - elif signal >= 40: - wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/2bar_wifi.svg") - elif 1 < signal < 40: - wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/1bar_wifi.svg") - button = ListCustomButton(parent=self.network_list_widget) - button.setText(ssid) - button.setRightText(right_text) - button.setPixmap(QtGui.QPixmap(":/arrow_icons/media/btn_icons/right_arrow.svg")) - button.setSecondPixmap(wifi_pixmap) - button.setFixedHeight(80) - button.setLeftFontSize(17) - button.setRightFontSize(12) - - button.clicked.connect(lambda checked, s=ssid: self.handle_button_click(s)) - item = QtWidgets.QListWidgetItem() - item.setSizeHint(button.sizeHint()) - self.network_list_widget.addItem(item) - self.network_list_widget.setItemWidget(item, button) + self.network_list_widget.setObjectName("network_list_widget") + self.panel.nl_content_layout.addWidget(self.network_list_widget) From afe33b2b872289c1610dbe1917650bcb0bb4f101 Mon Sep 17 00:00:00 2001 From: gmmcosta15 Date: Thu, 20 Nov 2025 16:19:05 +0000 Subject: [PATCH 02/26] Refactor: Refac to MVC view with Controller being runnables on a threadpoll --- BlocksScreen/lib/panels/networkWindow.py | 336 ++++++++++-------- .../lib/ui/resources/icon_resources.qrc | 13 +- .../resources/media/btn_icons/0bar_wifi.svg | 1 + .../media/btn_icons/0bar_wifi_protected.svg | 1 + .../resources/media/btn_icons/1bar_wifi.svg | 2 +- .../media/btn_icons/1bar_wifi_protected.svg | 1 + .../resources/media/btn_icons/2bar_wifi.svg | 2 +- .../media/btn_icons/2bar_wifi_protected.svg | 1 + .../resources/media/btn_icons/3bar_wifi.svg | 2 +- .../media/btn_icons/3bar_wifi_protected.svg | 1 + .../resources/media/btn_icons/4bar_wifi.svg | 1 + .../media/btn_icons/4bar_wifi_protected.svg | 1 + 12 files changed, 215 insertions(+), 147 deletions(-) create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/0bar_wifi.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/0bar_wifi_protected.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi_protected.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi_protected.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi_protected.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi_protected.svg diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 0e7a32d1..d6b70c13 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -4,139 +4,208 @@ import subprocess from functools import partial +from PyQt6 import QtCore, QtGui, QtWidgets +from PyQt6.QtCore import QRunnable, QThreadPool, QObject, pyqtSignal, QVariant +from PyQt6.QtWidgets import QScroller, QScrollerProperties + from lib.network import SdbusNetworkManagerAsync from lib.panels.widgets.popupDialogWidget import Popup from lib.ui.wifiConnectivityWindow_ui import Ui_wifi_stacked_page -from lib.utils.list_button import ListCustomButton from lib.panels.widgets.keyboardPage import CustomQwertyKeyboard -from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_frame import BlocksCustomFrame from lib.panels.widgets.loadPage import LoadScreen -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 QVariant -from PyQt6.QtWidgets import QScroller, QScrollerProperties + logger = logging.getLogger("logs/BlocksScreen.log") +<<<<<<< HEAD class BuildNetworkList(QtCore.QThread): """Retrieves information from sdbus interface about scanned networks""" - - scan_result: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - dict, name="scan-results" - ) - finished_network_list_build: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - list, name="finished-network-list-build" - ) - - def __init__(self) -> None: +======= +class NetworkScanRunnable(QRunnable): + """QRunnable task that performs network scanning using SdbusNetworkManagerAsync +>>>>>>> 9285fb7 (Refactor: Refac to MVC view with Controller being runnables on a threadpoll) + + This runnable: + - Triggers a network rescan via SdbusNetworkManagerAsync + - collects SSIDs, signal strenght and saved status + - emits signal with raw scan data and a processed lisgs + + Signals: + - scan_results (dict): Emitted with raw scan results mapping SSID to properties + - finished_network_list_build (list): Emitted with processed list of networks + - error (str): Emitted if an error occurs during scanning + + """ + + class Signals(QObject): + scan_results = pyqtSignal(dict, name="scan-results") + finished_network_list_build = pyqtSignal(list, name="finished-network-list-build") + error = pyqtSignal(str) + + def __init__(self): super().__init__() - self.mutex = QtCore.QMutex() - self.condition = QtCore.QWaitCondition() - self.restart = False - self.mutex.unlock() - self.network_items_list = [] self.nm = SdbusNetworkManagerAsync() - if not self.nm: - logger.error( - "Cannot scan for networks, parent does not have \ - sdbus_network ('SdbusNetworkManagerAsync' instance class)" - ) - return - logger.info("Network Scanner Thread Initiated") + self.signals = NetworkScanRunnable.Signals() - def build(self) -> None: - """Starts QThread""" - with QtCore.QMutexLocker(self.mutex): - if not self.isRunning(): - self.start(QtCore.QThread.Priority.LowPriority) - else: - self.restart = True - self.condition.wakeOne() - - def stop(self): - """Stops QThread execution""" - self.mutex.lock() - self.condition.wakeOne() - self.mutex.unlock() - self.deleteLater() - - def run(self) -> None: - """BuildNetworkList main thread logic""" - logger.debug("Scanning and building network list") - while True: - self.mutex.lock() - self.network_items_list.clear() + def run(self): + try: + logger.debug("NetworkScanRunnable: scanning networks") self.nm.rescan_networks() - saved_ssids = self.nm.get_saved_ssid_names() - saved_networks = self.nm.get_saved_networks() - unsaved_networks = [] - networks = [] - if self.nm.check_wifi_interface(): - available_networks = self.nm.get_available_networks() - if not available_networks: # Skip everything if no networks exist - logger.debug("No available networks after scan") - self.finished_network_list_build.emit(self.network_items_list) - return - for ssid_key in available_networks: - properties = available_networks.get(ssid_key, {}) - signal = int(properties.get("signal_level", 0)) - networks.append( - { - "ssid": ssid_key if ssid_key else "UNKNOWN", - "signal": signal, - "is_saved": bool(ssid_key in saved_ssids), - } - ) - if networks: - saved_networks = sorted( - [n for n in networks if n["is_saved"]], - key=lambda x: -x["signal"], - ) - unsaved_networks = sorted( - [n for n in networks if not n["is_saved"]], - key=lambda x: -x["signal"], - ) - elif saved_networks: - saved_networks = sorted([n for n in saved_networks], key=lambda x: -1) - if saved_networks: - for net in saved_networks: - if "ap" in net.get("mode", ""): - return - ssid = net.get("ssid", "UNKNOWN") - signal = ( - self.nm.get_connection_signal_by_ssid(ssid) - if ssid != "UNKNOWN" - else 0 - ) - if ssid == self.nm.get_current_ssid(): - self.network_items_list.append((ssid, signal, "Active")) - else: - self.network_items_list.append((ssid, signal, "Saved")) - if saved_networks and unsaved_networks: # Separator - self.network_items_list.append("separator") - if unsaved_networks: - for net in unsaved_networks: - ssid = net.get("ssid", "UNKNOWN") - signal = ( - self.nm.get_connection_signal_by_ssid(ssid) - if ssid != "UNKNOWN" - else 0 - ) - self.network_items_list.append((ssid, signal, "Protected")) - # Add a dummy blank space at the end if there are any unsaved networks - if unsaved_networks: - self.network_items_list.append("blank") - - self.finished_network_list_build.emit(self.network_items_list) - if not self.restart: - self.condition.wait(self.mutex) - self.restart = False - self.mutex.unlock() + saved = self.nm.get_saved_ssid_names() + available = self.nm.get_available_networks() if self.nm.check_wifi_interface() else {} + + data_dict: dict[str, dict] = {} + for ssid, props in available.items(): + signal = int(props.get("signal_level", 0)) + data_dict[ssid] = { + "signal_level": signal, + "is_saved": ssid in saved, + } + + # Emit scan_result (same name) + self.signals.scan_results.emit(data_dict) + + # Transform into your “list of tuples + blank / separator” format + items: list[typing.Union[tuple[str,int,str], str]] = [] + saved_nets = [ (ssid, info["signal_level"]) for ssid, info in data_dict.items() if info["is_saved"] ] + unsaved_nets = [ (ssid, info["signal_level"]) for ssid, info in data_dict.items() if not info["is_saved"] ] + saved_nets.sort(key=lambda x: -x[1]) + unsaved_nets.sort(key=lambda x: -x[1]) + + # Build your list with statuses + for ssid, sig in saved_nets: + status = "Active" if ssid == self.nm.get_current_ssid() else "Saved" + items.append((ssid, sig, status)) + + for ssid, sig in unsaved_nets: + items.append((ssid, sig, "Protected")) + + 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)) + +class BuildNetworkList(QtCore.QObject): + """ + Controller class that schedules and manages repeted network scans + + Uses a QThreadPool to un NetworkScanRunnable tasks periodically. with a QTimer to trigger scans. + Prevents overlapping scans by tracking whether a scan is already in progress. + + Args: + poll_interval_ms: (int) Milliseconds between scans (default: 10000) + _timer (QtCore.QTimer): Timer that schedules next scan + _is_scanning (bool): Flag indicating if a scan is currently in progress + + Signals: + scan_results (dict): Emitted with raw scan results mapping SSID to properties + finished_network_list_build (list): Emitted with processed list of networks + error (str): Emitted if an error occurs during scanning + """ + scan_results = pyqtSignal(dict, name="scan-results") + finished_network_list_build = pyqtSignal(list, name="finished-network-list-build") + error = pyqtSignal(str) + + def __init__(self, poll_interval_ms: int = 10000): + super().__init__() + self.threadpool = QThreadPool.globalInstance() + self.poll_interval_ms = poll_interval_ms + self._is_scanning = False + + self._timer = QtCore.QTimer(self) + self._timer.setSingleShot(True) + self._timer.timeout.connect(self._do_scan) + + def start_polling(self): + self._schedule_next_scan() + + def stop_polling(self): + self._timer.stop() + + def build(self): + self._do_scan() + def _schedule_next_scan(self): + self._timer.start(self.poll_interval_ms) + def _on_task_finished(self, items): + self._is_scanning = False + self.finished_network_list_build.emit(items) + self._schedule_next_scan() + + def _on_task_scan_results(self, data_dict): + self.scan_results.emit(data_dict) + + def _on_task_error(self, err): + self._is_scanning = False + self.error.emit(err) + self._schedule_next_scan() + + def _do_scan(self): + if self._is_scanning: + logger.debug("Already scanning, skip scheduling.") + self._schedule_next_scan() + return + + self._is_scanning = True + task = NetworkScanRunnable() + 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) + logger.debug("Submitted scan task to thread pool") + +class WifiIconProvider: + """Simple provider: loads QPixmap for WiFi bars + protection without caching.""" + + def __init__(self): + # Map from (bars, is_protected) to resource path + self.paths = { + ("no", False): ":/network/media/btn_icons/0bar_wifi.svg", + (4, False): ":/network/media/btn_icons/4bar_wifi.svg", + (3, False): ":/network/media/btn_icons/3bar_wifi.svg", + (2, False): ":/network/media/btn_icons/2bar_wifi.svg", + (1, False): ":/network/media/btn_icons/1bar_wifi.svg", + + ("no", True): ":/network/media/btn_icons/0bar_wifi_protected.svg", + (4, True): ":/network/media/btn_icons/4bar_wifi_protected.svg", + (3, True): ":/network/media/btn_icons/3bar_wifi_protected.svg", + (2, True): ":/network/media/btn_icons/2bar_wifi_protected.svg", + (1, True): ":/network/media/btn_icons/1bar_wifi_protected.svg", + } + + + def get_pixmap(self, signal: int, state: str) -> QtGui.QPixmap: + """Return a QPixmap for the given signal (0-100) and state ("Protected" or not).""" + # Normalize signal + if signal <= 0: + bars = "no" + elif signal >= 75: + bars = 4 + elif signal >= 50: + bars = 3 + elif signal >= 25: + bars = 2 + else: + bars = 1 + + is_protected = (state == "Protected") + key = (bars, is_protected) + + path = self.paths.get(key) + if path is None: + logger.warning(f"No icon path for key {key}, falling back to no-signal unprotected") + path = self.paths[("no", False)] + + pm = QtGui.QPixmap(path) + if pm.isNull(): + logger.error(f"Failed to load pixmap from '{path}' for key {key}") + return pm class NetworkControlWindow(QtWidgets.QStackedWidget): """Network Control panel Widget""" @@ -155,7 +224,8 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: self.panel.setupUi(self) self._setupUI() - #self.background: typing.Optional[QtGui.QPixmap] = None + + self._provider = WifiIconProvider() self.ongoing_update: bool = False self.popup = Popup(self) @@ -171,7 +241,7 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: self._load_timer.setSingleShot(True) self._load_timer.timeout.connect(self._handle_load_timeout) - #View Models and Controllers + #View Models and Delegates self.model = EntryListModel() self.model.setParent(self.network_list_widget) self.entry_delegate = EntryDelegate() @@ -187,9 +257,8 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: self.network_list_worker.finished_network_list_build.connect( self.handle_network_list ) - self.panel.rescan_button.clicked.connect( - lambda: QtCore.QTimer.singleShot(100, self.network_list_worker.build) - ) + self.network_list_worker.start_polling() + self.panel.rescan_button.clicked.connect(self.network_list_worker.build) self.sdbus_network.nm_state_change.connect(self.evaluate_network_state) self.panel.wifi_button.clicked.connect( @@ -415,7 +484,7 @@ def build_model_list(self) -> None: """Builds the model list (`self.model`) containing updatable clients""" self.network_list_widget.blockSignals(True) self.model.clear() - #logger.debug(f"len saved: {len(self.saved_network.items())}") + test:dict = copy.copy(self.saved_network) if test.items(): for ssid,(signal,is_saved) in test.items(): @@ -904,7 +973,7 @@ def handle_network_list(self, data: typing.Dict) -> None: continue if entry == "separator": continue - self.saved_network[entry[0]] = (entry[1], entry[2] == "Saved" or entry[2] == "Active") + self.saved_network[entry[0]] = (entry[1], entry[2]) self.build_model_list() self.network_list_widget.blockSignals(False) @@ -1000,23 +1069,17 @@ def show_network_panel( def add_network_entry(self, ssid: str, signal: int, is_saved:str) -> None: """Adds a new item to the list model""" - wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/no_wifi.svg") - if 70 <= signal <= 100: - wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/3bar_wifi.svg") - elif signal >= 40: - wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/2bar_wifi.svg") - elif 1 < signal < 40: - wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/1bar_wifi.svg") + wifi_pixmap = self._provider.get_pixmap(signal=signal, state=is_saved) item = ListItem( text=ssid, left_icon=wifi_pixmap, - right_text=f"Signal - {signal} % | {'Active' if is_saved else 'Protect'} ", + right_text=is_saved, selected=False, allow_check=False, _lfontsize=17, _rfontsize=13, - height=60, + height=70, ) self.model.add_item(item) @@ -1115,14 +1178,8 @@ def set_brush_for_all_groups(palette, role, color, style=QtCore.Qt.BrushStyle.So self.network_list_widget.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) self.network_list_widget.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) self.network_list_widget.setUniformItemSizes(True) - - #self.network_list_widget.setStyleSheet("QListView { padding-bottom: 8px; }") - - # inside build_network_list - - # ... (your palette / list-view setup) - - # Grab gesture on the viewport + self.network_list_widget.setSpacing(3) + viewport = self.network_list_widget.viewport() QScroller.grabGesture(viewport, QScroller.ScrollerGestureType.TouchGesture) QScroller.grabGesture(viewport, QScroller.ScrollerGestureType.LeftMouseButtonGesture) @@ -1153,8 +1210,5 @@ def set_brush_for_all_groups(palette, role, color, style=QtCore.Qt.BrushStyle.So scroller.setScrollerProperties(props) - # ... add widget to layout, etc. - - self.network_list_widget.setObjectName("network_list_widget") self.panel.nl_content_layout.addWidget(self.network_list_widget) diff --git a/BlocksScreen/lib/ui/resources/icon_resources.qrc b/BlocksScreen/lib/ui/resources/icon_resources.qrc index 3022fd4d..7c9abcdd 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources.qrc +++ b/BlocksScreen/lib/ui/resources/icon_resources.qrc @@ -1,11 +1,18 @@ - media/btn_icons/wifi_config.svg - media/btn_icons/wifi_locked.svg - media/btn_icons/wifi_unlocked.svg + 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/wifi_config.svg + media/btn_icons/wifi_locked.svg + media/btn_icons/wifi_unlocked.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/media/btn_icons/0bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/0bar_wifi.svg new file mode 100644 index 00000000..ceaff53d --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/0bar_wifi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/0bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/0bar_wifi_protected.svg new file mode 100644 index 00000000..a10ea388 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/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/1bar_wifi.svg index debc48b2..3258893d 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi_protected.svg new file mode 100644 index 00000000..8793447e --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/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/2bar_wifi.svg index d9ba78c1..203b70bb 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi_protected.svg new file mode 100644 index 00000000..a9f3233b --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/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/3bar_wifi.svg index 7c694b76..8d98855c 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi_protected.svg new file mode 100644 index 00000000..458c1ac5 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi.svg new file mode 100644 index 00000000..9aadd8e7 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi_protected.svg new file mode 100644 index 00000000..639762e7 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file From c1ee388876ae13b368adae597fdd98c3c3f1f3f7 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Wed, 26 Nov 2025 12:09:41 +0000 Subject: [PATCH 03/26] UPD: Regenerated icon_resources_rc --- .../lib/ui/resources/icon_resources_rc.py | 2085 +++++++++++++---- 1 file changed, 1632 insertions(+), 453 deletions(-) diff --git a/BlocksScreen/lib/ui/resources/icon_resources_rc.py b/BlocksScreen/lib/ui/resources/icon_resources_rc.py index 14285978..4d2e5ab4 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources_rc.py +++ b/BlocksScreen/lib/ui/resources/icon_resources_rc.py @@ -19093,7 +19093,7 @@ \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\x02\xa8\ +\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\ @@ -19102,42 +19102,190 @@ \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\x6f\x70\x61\x63\x69\x74\x79\x3a\x30\x2e\x37\x35\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x65\x30\x65\ -\x30\x64\x66\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ -\x65\x66\x73\x3e\x3c\x67\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\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\x38\x36\ -\x2e\x31\x38\x2c\x34\x30\x36\x2e\x32\x33\x63\x30\x2d\x39\x2e\x30\ -\x39\x2c\x33\x2e\x35\x32\x2d\x31\x36\x2e\x34\x31\x2c\x39\x2e\x34\ -\x37\x2d\x32\x32\x2e\x34\x38\x2c\x32\x33\x2e\x34\x38\x2d\x32\x34\ -\x2c\x35\x31\x2e\x30\x37\x2d\x33\x39\x2e\x32\x33\x2c\x38\x33\x2e\ -\x33\x39\x2d\x34\x33\x2e\x37\x38\x2c\x34\x37\x2e\x39\x34\x2d\x36\ -\x2e\x37\x36\x2c\x38\x39\x2e\x35\x2c\x38\x2e\x31\x33\x2c\x31\x32\ -\x34\x2e\x37\x37\x2c\x34\x33\x2e\x33\x31\x2c\x31\x30\x2c\x31\x30\ -\x2c\x31\x32\x2e\x36\x32\x2c\x32\x33\x2e\x36\x35\x2c\x37\x2e\x33\ -\x34\x2c\x33\x35\x2e\x35\x32\x2d\x38\x2e\x33\x32\x2c\x31\x38\x2e\ -\x37\x31\x2d\x33\x30\x2e\x35\x34\x2c\x32\x33\x2e\x31\x2d\x34\x34\ -\x2e\x36\x36\x2c\x38\x2e\x35\x34\x2d\x31\x33\x2e\x32\x2d\x31\x33\ -\x2e\x36\x33\x2d\x32\x38\x2e\x33\x38\x2d\x32\x33\x2e\x32\x39\x2d\ -\x34\x36\x2e\x34\x31\x2d\x32\x37\x2e\x31\x35\x2d\x33\x32\x2e\x37\ -\x34\x2d\x37\x2d\x36\x31\x2e\x34\x35\x2c\x31\x2e\x36\x33\x2d\x38\ -\x35\x2e\x36\x39\x2c\x32\x36\x2e\x33\x38\x2d\x31\x36\x2e\x34\x32\ -\x2c\x31\x36\x2e\x37\x36\x2d\x34\x31\x2e\x36\x39\x2c\x39\x2e\x38\ -\x36\x2d\x34\x37\x2e\x32\x33\x2d\x31\x33\x2e\x30\x37\x41\x36\x37\ -\x2e\x35\x36\x2c\x36\x37\x2e\x35\x36\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x31\x38\x36\x2e\x31\x38\x2c\x34\x30\x36\x2e\x32\x33\x5a\x22\x2f\ +\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\x32\x22\x20\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x33\x39\ -\x2e\x38\x34\x63\x2d\x32\x31\x2e\x36\x37\x2c\x30\x2d\x33\x38\x2e\ -\x35\x37\x2d\x31\x38\x2e\x31\x38\x2d\x33\x38\x2e\x35\x33\x2d\x34\ -\x31\x2e\x34\x39\x2c\x30\x2d\x32\x33\x2c\x31\x36\x2e\x39\x34\x2d\ -\x34\x31\x2e\x32\x32\x2c\x33\x38\x2e\x32\x38\x2d\x34\x31\x2e\x31\ -\x38\x2c\x32\x31\x2e\x39\x33\x2c\x30\x2c\x33\x38\x2e\x37\x35\x2c\ -\x31\x37\x2e\x39\x31\x2c\x33\x38\x2e\x37\x36\x2c\x34\x31\x2e\x32\ -\x53\x33\x32\x31\x2e\x37\x36\x2c\x35\x33\x39\x2e\x38\x33\x2c\x33\ -\x30\x30\x2c\x35\x33\x39\x2e\x38\x34\x5a\x22\x2f\x3e\x3c\x2f\x67\ -\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\x0b\x4d\ \x00\ \x00\x38\xfa\x78\x9c\xed\x9b\x49\x6f\x1d\xc7\x15\x85\xff\x0a\xc1\ @@ -19874,7 +20022,7 @@ \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\ -\x00\x00\x05\x99\ +\x00\x00\x05\x80\ \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\ @@ -19886,86 +20034,176 @@ \x31\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\x39\x33\ -\x2e\x38\x32\x43\x34\x30\x32\x2c\x39\x35\x2e\x36\x31\x2c\x34\x39\ -\x30\x2e\x31\x2c\x31\x33\x33\x2e\x39\x31\x2c\x35\x36\x33\x2e\x39\ -\x2c\x32\x30\x39\x2e\x39\x32\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\x2e\x30\x35\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\x32\x2d\x34\ -\x36\x2e\x36\x31\x2d\x34\x31\x2e\x31\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\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\x33\x2d\x31\x31\ -\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x34\x2d\x38\x2e\x35\x37\x2c\ -\x39\x2e\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\x39\x2d\x39\x2e\ -\x37\x34\x2d\x35\x30\x2e\x36\x31\x61\x33\x38\x30\x2e\x35\x31\x2c\ -\x33\x38\x30\x2e\x35\x31\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\x35\x43\x32\x35\x35\x2e\x30\x37\ -\x2c\x39\x36\x2e\x37\x2c\x32\x37\x37\x2e\x34\x33\x2c\x39\x35\x2e\ -\x33\x31\x2c\x32\x39\x30\x2e\x31\x38\x2c\x39\x33\x2e\x38\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\x34\x36\x39\x2e\x38\ -\x37\x2c\x33\x34\x39\x2e\x30\x35\x63\x2d\x31\x31\x2c\x2e\x31\x38\ -\x2d\x31\x37\x2e\x38\x33\x2d\x33\x2e\x30\x38\x2d\x32\x33\x2e\x35\ -\x32\x2d\x39\x2e\x31\x33\x43\x34\x31\x34\x2c\x33\x30\x35\x2e\x34\ -\x38\x2c\x33\x37\x35\x2e\x32\x32\x2c\x32\x38\x34\x2c\x33\x33\x30\ -\x2e\x31\x2c\x32\x37\x37\x2e\x35\x32\x63\x2d\x36\x37\x2e\x33\x37\ -\x2d\x39\x2e\x36\x37\x2d\x31\x32\x36\x2e\x31\x37\x2c\x31\x30\x2e\ -\x38\x32\x2d\x31\x37\x35\x2e\x33\x39\x2c\x36\x31\x2e\x34\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\x2c\ -\x32\x30\x33\x2e\x39\x31\x2c\x35\x38\x2e\x33\x36\x61\x32\x30\x35\ -\x2c\x32\x30\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x33\x2e\x32\x35\ -\x2c\x32\x32\x2e\x37\x63\x38\x2e\x30\x38\x2c\x39\x2e\x32\x39\x2c\ -\x39\x2e\x35\x2c\x32\x30\x2e\x36\x31\x2c\x34\x2e\x35\x39\x2c\x33\ -\x32\x2e\x33\x33\x43\x34\x38\x37\x2e\x34\x34\x2c\x33\x34\x33\x2e\ -\x30\x35\x2c\x34\x37\x38\x2e\x36\x32\x2c\x33\x34\x38\x2e\x34\x39\ -\x2c\x34\x36\x39\x2e\x38\x37\x2c\x33\x34\x39\x2e\x30\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\x31\x38\x34\x2e\x35\x32\ -\x2c\x34\x30\x34\x2e\x32\x33\x63\x30\x2d\x39\x2e\x32\x32\x2c\x33\ -\x2e\x35\x37\x2d\x31\x36\x2e\x36\x34\x2c\x39\x2e\x36\x31\x2d\x32\ -\x32\x2e\x38\x31\x43\x32\x31\x38\x2c\x33\x35\x37\x2e\x30\x38\x2c\ -\x32\x34\x36\x2c\x33\x34\x31\x2e\x36\x31\x2c\x32\x37\x38\x2e\x37\ -\x37\x2c\x33\x33\x37\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\x33\x2e\x39\x35\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\x37\x2d\x31\x33\x2e\x34\x2d\x31\x33\x2e\x38\x34\x2d\ -\x32\x38\x2e\x38\x2d\x32\x33\x2e\x36\x34\x2d\x34\x37\x2e\x31\x31\ -\x2d\x32\x37\x2e\x35\x35\x2d\x33\x33\x2e\x32\x32\x2d\x37\x2e\x30\ -\x39\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\x38\x2e\x39\x33\x2c\x36\x38\x2e\x39\x33\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x31\x38\x34\x2e\x35\x32\x2c\x34\x30\x34\x2e\ -\x32\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\x39\x2e\x38\x34\x63\x2d\x32\x32\x2c\x30\x2d\x33\ -\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x35\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\x37\x39\x2c\x32\x32\x2e\x32\x35\x2c\x30\x2c\x33\ -\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\x37\x2c\x33\x39\x2e\x33\x34\ -\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\x33\x39\ -\x2e\x38\x33\x2c\x33\x30\x30\x2c\x35\x33\x39\x2e\x38\x34\x5a\x22\ -\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ +\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\x31\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\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\x32\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\x32\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\x09\xc5\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -20125,7 +20363,7 @@ \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\ -\x00\x00\x03\xdf\ +\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\ @@ -20134,61 +20372,258 @@ \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\x33\x38\x62\x33\x34\x61\x3b\x7d\ -\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x31\x22\x20\x64\x3d\x22\x4d\x34\x36\x38\x2e\x35\x31\x2c\x33\x35\ -\x30\x2e\x35\x31\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\x41\x32\ -\x30\x33\x2e\x34\x37\x2c\x32\x30\x33\x2e\x34\x37\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x34\x38\x36\x2e\x31\x39\x2c\x33\x30\x31\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\x35\x30\x2c\x34\x36\x38\x2e\x35\x31\x2c\x33\x35\x30\ -\x2e\x35\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\x35\x2e\x33\x36\x2c\x34\x30\x35\x2e\x32\x37\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\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\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\x35\x2e\x32\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\x33\x30\x30\x2c\x35\x33\x39\ -\x2e\x38\x34\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\x39\x2e\x38\x33\x2c\x33\x30\x30\x2c\x35\x33\ -\x39\x2e\x38\x34\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ +\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\x32\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\x0a\x70\ +\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\x2e\x63\x6c\x73\x2d\x33\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\x31\x38\x2c\x37\x31\x2e\x33\x35\x43\x34\x30\ +\x32\x2c\x37\x33\x2e\x31\x34\x2c\x34\x39\x30\x2e\x31\x2c\x31\x31\ +\x31\x2e\x34\x34\x2c\x35\x36\x33\x2e\x39\x2c\x31\x38\x37\x2e\x34\ +\x35\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\ +\x2e\x30\x35\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\x32\x2d\x34\x36\x2e\x36\x31\x2d\x34\ +\x31\x2e\x31\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\x2d\x38\x31\x2e\ +\x33\x33\x2d\x34\x36\x2e\x37\x34\x2d\x31\x32\x37\x2e\x37\x35\x2d\ +\x35\x34\x2e\x36\x34\x2d\x35\x34\x2d\x39\x2e\x32\x2d\x31\x30\x36\ +\x2e\x39\x32\x2d\x34\x2e\x33\x32\x2d\x31\x35\x38\x2e\x35\x2c\x31\ +\x35\x2e\x32\x32\x2d\x34\x35\x2c\x31\x37\x2d\x38\x34\x2e\x32\x39\ +\x2c\x34\x33\x2e\x39\x33\x2d\x31\x31\x38\x2e\x31\x36\x2c\x37\x39\ +\x2e\x39\x34\x2d\x38\x2e\x35\x37\x2c\x39\x2e\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\ +\x31\x61\x33\x38\x30\x2e\x35\x31\x2c\x33\x38\x30\x2e\x35\x31\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\x35\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x34\x2e\x32\x33\x2c\ +\x32\x37\x37\x2e\x34\x33\x2c\x37\x32\x2e\x38\x35\x2c\x32\x39\x30\ +\x2e\x31\x38\x2c\x37\x31\x2e\x33\x35\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\x31\x37\x2e\x33\x37\x63\ +\x2d\x32\x32\x2c\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\ +\x35\x2d\x33\x39\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\ +\x32\x33\x2e\x33\x37\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\ +\x33\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\ +\x37\x2c\x33\x39\x2e\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\ +\x32\x2e\x31\x2c\x35\x31\x37\x2e\x33\x37\x2c\x33\x30\x30\x2c\x35\ +\x31\x37\x2e\x33\x37\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\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\x30\x2e\x33\x37\x68\x31\ +\x34\x2e\x33\x35\x61\x31\x30\x2e\x36\x34\x2c\x31\x30\x2e\x36\x34\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x32\x2c\x31\x63\x32\x37\ +\x2e\x34\x37\x2c\x34\x2c\x34\x39\x2e\x36\x2c\x32\x36\x2e\x36\x37\ +\x2c\x35\x31\x2e\x36\x38\x2c\x35\x34\x2e\x32\x36\x2c\x31\x2e\x31\ +\x37\x2c\x31\x35\x2e\x35\x33\x2e\x35\x38\x2c\x33\x31\x2e\x32\x2e\ +\x37\x38\x2c\x34\x36\x2e\x38\x2c\x30\x2c\x32\x2e\x31\x32\x2c\x30\ +\x2c\x34\x2e\x32\x35\x2c\x30\x2c\x36\x2e\x38\x36\x68\x31\x31\x2e\ +\x37\x35\x63\x31\x33\x2e\x39\x34\x2c\x30\x2c\x31\x39\x2e\x33\x36\ +\x2c\x35\x2e\x33\x39\x2c\x31\x39\x2e\x33\x36\x2c\x31\x39\x2e\x32\ +\x32\x2c\x30\x2c\x33\x33\x2e\x32\x35\x2d\x2e\x31\x38\x2c\x36\x36\ +\x2e\x35\x2e\x31\x32\x2c\x39\x39\x2e\x37\x35\x2e\x30\x39\x2c\x39\ +\x2e\x36\x38\x2d\x32\x2e\x39\x33\x2c\x31\x36\x2e\x36\x37\x2d\x31\ +\x32\x2e\x31\x36\x2c\x32\x30\x2e\x33\x39\x48\x33\x37\x35\x2e\x31\ +\x32\x63\x2d\x37\x2e\x32\x34\x2d\x33\x2e\x31\x37\x2d\x31\x32\x2d\ +\x38\x2e\x31\x31\x2d\x31\x32\x2d\x31\x36\x2e\x35\x33\x2c\x30\x2d\ +\x33\x35\x2e\x34\x36\x2d\x2e\x30\x38\x2d\x37\x30\x2e\x39\x31\x2c\ +\x30\x2d\x31\x30\x36\x2e\x33\x37\x2c\x30\x2d\x31\x30\x2e\x32\x39\ +\x2c\x36\x2e\x32\x34\x2d\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\x35\ +\x36\x2d\x31\x36\x2e\x34\x34\x2c\x34\x2e\x36\x39\x2d\x2e\x30\x39\ +\x2c\x39\x2e\x33\x39\x2c\x30\x2c\x31\x34\x2e\x35\x31\x2c\x30\x2c\ +\x30\x2d\x31\x35\x2e\x37\x33\x2d\x2e\x30\x38\x2d\x33\x30\x2e\x35\ +\x39\x2c\x30\x2d\x34\x35\x2e\x34\x35\x2e\x31\x39\x2d\x32\x38\x2e\ +\x37\x39\x2c\x31\x37\x2e\x32\x38\x2d\x35\x32\x2e\x33\x36\x2c\x34\ +\x33\x2e\x38\x39\x2d\x36\x30\x2e\x35\x38\x43\x34\x34\x31\x2e\x37\ +\x31\x2c\x32\x38\x32\x2e\x31\x34\x2c\x34\x34\x35\x2e\x34\x31\x2c\ +\x32\x38\x31\x2e\x33\x33\x2c\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\ +\x30\x2e\x33\x37\x5a\x6d\x33\x38\x2e\x33\x31\x2c\x31\x30\x38\x2e\ +\x34\x39\x63\x30\x2d\x31\x36\x2e\x35\x34\x2e\x39\x34\x2d\x33\x32\ +\x2e\x37\x33\x2d\x2e\x32\x35\x2d\x34\x38\x2e\x37\x37\x2d\x31\x2e\ +\x33\x31\x2d\x31\x37\x2e\x38\x32\x2d\x31\x35\x2e\x39\x33\x2d\x32\ +\x39\x2e\x37\x35\x2d\x33\x32\x2e\x37\x37\x2d\x32\x38\x2e\x37\x37\ +\x2d\x31\x36\x2e\x36\x35\x2c\x31\x2d\x32\x39\x2e\x32\x31\x2c\x31\ +\x34\x2e\x37\x33\x2d\x32\x39\x2e\x34\x32\x2c\x33\x32\x2e\x35\x32\ +\x2d\x2e\x31\x36\x2c\x31\x33\x2e\x37\x37\x2c\x30\x2c\x32\x37\x2e\ +\x35\x34\x2c\x30\x2c\x34\x31\x2e\x33\x31\x61\x33\x33\x2e\x31\x39\ +\x2c\x33\x33\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x32\ +\x2c\x33\x2e\x37\x31\x5a\x6d\x2d\x33\x31\x2e\x33\x33\x2c\x34\x36\ +\x2e\x37\x31\x61\x31\x34\x2e\x39\x31\x2c\x31\x34\x2e\x39\x31\x2c\ +\x30\x2c\x30\x2c\x30\x2d\x31\x33\x2e\x36\x38\x2c\x38\x2e\x36\x36\ +\x63\x2d\x32\x2e\x38\x32\x2c\x35\x2e\x35\x37\x2d\x32\x2e\x38\x2c\ +\x31\x31\x2e\x35\x39\x2c\x31\x2e\x36\x2c\x31\x35\x2e\x38\x35\x2c\ +\x34\x2e\x31\x36\x2c\x34\x2c\x34\x2e\x34\x34\x2c\x38\x2e\x33\x35\ +\x2c\x34\x2e\x32\x32\x2c\x31\x33\x2e\x33\x32\x61\x36\x38\x2e\x33\ +\x38\x2c\x36\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\ +\x37\x2e\x37\x31\x63\x2e\x33\x37\x2c\x35\x2e\x33\x33\x2c\x33\x2e\ +\x36\x2c\x38\x2e\x38\x33\x2c\x38\x2c\x38\x2e\x38\x37\x73\x37\x2e\ +\x37\x35\x2d\x33\x2e\x34\x36\x2c\x38\x2e\x30\x37\x2d\x38\x2e\x37\ +\x36\x61\x31\x31\x31\x2e\x34\x34\x2c\x31\x31\x31\x2e\x34\x34\x2c\ +\x30\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x31\x2e\x35\x36\x2c\x39\x2e\ +\x36\x38\x2c\x39\x2e\x36\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x2e\ +\x32\x2d\x38\x2e\x31\x33\x63\x34\x2e\x37\x31\x2d\x34\x2e\x36\x34\ +\x2c\x35\x2e\x36\x31\x2d\x31\x30\x2e\x35\x33\x2c\x33\x2d\x31\x36\ +\x2e\x37\x32\x41\x31\x35\x2e\x34\x38\x2c\x31\x35\x2e\x34\x38\x2c\ +\x30\x2c\x30\x2c\x30\x2c\x34\x35\x36\x2e\x30\x35\x2c\x34\x33\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\x32\x22\x20\x64\x3d\x22\x4d\x33\ +\x37\x39\x2e\x35\x38\x2c\x33\x38\x34\x2e\x33\x31\x63\x33\x2e\x31\ +\x38\x2d\x2e\x30\x36\x2c\x36\x2e\x33\x31\x2d\x2e\x30\x35\x2c\x39\ +\x2e\x36\x31\x2c\x30\x2c\x30\x2d\x33\x2e\x32\x34\x2c\x30\x2d\x36\ +\x2e\x34\x33\x2c\x30\x2d\x39\x2e\x36\x31\x2c\x30\x2d\x31\x30\x2e\ +\x35\x33\x2c\x30\x2d\x32\x30\x2e\x34\x38\x2c\x30\x2d\x33\x30\x2e\ +\x35\x38\x2d\x33\x32\x2e\x31\x34\x2d\x32\x35\x2e\x32\x37\x2d\x36\ +\x38\x2e\x39\x33\x2d\x33\x35\x2e\x34\x32\x2d\x31\x31\x30\x2e\x34\ +\x34\x2d\x32\x39\x2e\x35\x37\x43\x32\x34\x36\x2c\x33\x31\x39\x2e\ +\x31\x34\x2c\x32\x31\x38\x2c\x33\x33\x34\x2e\x36\x31\x2c\x31\x39\ +\x34\x2e\x31\x33\x2c\x33\x35\x39\x63\x2d\x36\x2c\x36\x2e\x31\x37\ +\x2d\x39\x2e\x36\x33\x2c\x31\x33\x2e\x35\x39\x2d\x39\x2e\x36\x31\ +\x2c\x32\x32\x2e\x38\x31\x61\x36\x38\x2e\x39\x33\x2c\x36\x38\x2e\ +\x39\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x33\x38\x63\ +\x35\x2e\x36\x33\x2c\x32\x33\x2e\x32\x38\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\x31\x2c\x35\ +\x33\x2e\x37\x35\x2d\x33\x33\x2e\x38\x36\x2c\x38\x37\x2d\x32\x36\ +\x2e\x37\x37\x2c\x31\x35\x2c\x33\x2e\x32\x2c\x32\x38\x2c\x31\x30\ +\x2e\x33\x35\x2c\x33\x39\x2e\x36\x2c\x32\x30\x2e\x34\x32\x43\x33\ +\x36\x33\x2e\x31\x37\x2c\x33\x38\x38\x2e\x38\x32\x2c\x33\x37\x30\ +\x2e\x30\x37\x2c\x33\x38\x34\x2e\x34\x39\x2c\x33\x37\x39\x2e\x35\ +\x38\x2c\x33\x38\x34\x2e\x33\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\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\ +\x31\x61\x32\x36\x2e\x31\x32\x2c\x32\x36\x2e\x31\x32\x2c\x30\x2c\ +\x30\x2c\x30\x2d\x37\x2e\x37\x39\x2c\x31\x2e\x36\x36\x63\x35\x2e\ +\x35\x39\x2c\x35\x2e\x37\x32\x2c\x31\x32\x2e\x33\x34\x2c\x38\x2e\ +\x37\x38\x2c\x32\x33\x2c\x38\x2e\x36\x31\x61\x32\x34\x2c\x32\x34\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x36\x2e\x32\x35\x2d\x31\x2e\x32\x37\ +\x41\x32\x35\x2e\x36\x38\x2c\x32\x35\x2e\x36\x38\x2c\x30\x2c\x30\ +\x2c\x30\x2c\x34\x35\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\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\x33\x36\x2e\ +\x36\x32\x2c\x32\x37\x38\x2e\x34\x38\x63\x32\x2e\x36\x31\x2d\x2e\ +\x38\x2c\x35\x2e\x32\x33\x2d\x31\x2e\x34\x36\x2c\x37\x2e\x37\x37\ +\x2d\x32\x2e\x30\x39\x2c\x31\x2e\x31\x34\x2d\x2e\x32\x38\x2c\x32\ +\x2e\x32\x38\x2d\x2e\x35\x36\x2c\x33\x2e\x34\x32\x2d\x2e\x38\x36\ +\x6c\x2e\x36\x32\x2d\x2e\x31\x36\x68\x31\x36\x6c\x2e\x39\x33\x2e\ +\x33\x39\x63\x2e\x33\x36\x2e\x31\x35\x2e\x37\x32\x2e\x33\x33\x2c\ +\x31\x2e\x30\x37\x2e\x35\x6c\x2e\x33\x37\x2e\x31\x39\x61\x36\x36\ +\x2e\x31\x38\x2c\x36\x36\x2e\x31\x38\x2c\x30\x2c\x30\x2c\x31\x2c\ +\x32\x36\x2e\x38\x33\x2c\x31\x30\x2e\x33\x38\x2c\x33\x30\x2e\x33\ +\x35\x2c\x33\x30\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\ +\x39\x34\x2d\x31\x30\x2e\x31\x35\x41\x32\x30\x35\x2c\x32\x30\x35\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x36\x34\x2e\x34\x34\x2c\x32\x35\ +\x34\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\x36\x2d\x35\x37\x2e\x30\x39\x2c\x38\x2e\x38\x2d\ +\x31\x30\x35\x2e\x38\x34\x2c\x33\x36\x2d\x31\x34\x36\x2e\x38\x34\ +\x2c\x37\x39\x2e\x32\x38\x2d\x38\x2e\x32\x37\x2c\x38\x2e\x37\x32\ +\x2d\x31\x30\x2e\x37\x39\x2c\x31\x39\x2e\x36\x32\x2d\x36\x2e\x38\ +\x39\x2c\x33\x31\x2e\x35\x32\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\x41\x31\x38\x37\x2e\x39\x32\x2c\x31\x38\ +\x37\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x31\x35\x2c\x32\ +\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\ @@ -20291,6 +20726,696 @@ \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\ +\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\x38\x63\x63\x35\x34\x30\x3b\x7d\ +\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\x33\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\x34\x34\x37\x2e\x31\x37\x2c\x33\x33\x34\x2e\x38\x35\x61\x32\ +\x36\x2e\x31\x32\x2c\x32\x36\x2e\x31\x32\x2c\x30\x2c\x30\x2c\x30\ +\x2d\x37\x2e\x37\x39\x2c\x31\x2e\x36\x36\x63\x35\x2e\x35\x39\x2c\ +\x35\x2e\x37\x32\x2c\x31\x32\x2e\x33\x34\x2c\x38\x2e\x37\x38\x2c\ +\x32\x33\x2c\x38\x2e\x36\x31\x61\x32\x33\x2e\x35\x35\x2c\x32\x33\ +\x2e\x35\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x36\x2e\x32\x35\x2d\x31\ +\x2e\x32\x37\x41\x32\x35\x2e\x37\x31\x2c\x32\x35\x2e\x37\x31\x2c\ +\x30\x2c\x30\x2c\x30\x2c\x34\x34\x37\x2e\x31\x37\x2c\x33\x33\x34\ +\x2e\x38\x35\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\x32\ +\x39\x30\x2e\x31\x38\x2c\x37\x31\x2e\x33\x35\x43\x34\x30\x32\x2c\ +\x37\x33\x2e\x31\x34\x2c\x34\x39\x30\x2e\x31\x2c\x31\x31\x31\x2e\ +\x34\x34\x2c\x35\x36\x33\x2e\x39\x2c\x31\x38\x37\x2e\x34\x35\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\x2e\x30\ +\x35\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\x32\x2d\x34\x36\x2e\x36\x31\x2d\x34\x31\x2e\ +\x31\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\x2d\x38\x31\x2e\x33\x33\ +\x2d\x34\x36\x2e\x37\x34\x2d\x31\x32\x37\x2e\x37\x35\x2d\x35\x34\ +\x2e\x36\x34\x2d\x35\x34\x2d\x39\x2e\x32\x2d\x31\x30\x36\x2e\x39\ +\x32\x2d\x34\x2e\x33\x32\x2d\x31\x35\x38\x2e\x35\x2c\x31\x35\x2e\ +\x32\x32\x2d\x34\x35\x2c\x31\x37\x2d\x38\x34\x2e\x32\x39\x2c\x34\ +\x33\x2e\x39\x33\x2d\x31\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\ +\x34\x2d\x38\x2e\x35\x37\x2c\x39\x2e\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\x31\x61\ +\x33\x38\x30\x2e\x35\x31\x2c\x33\x38\x30\x2e\x35\x31\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\x35\ +\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x34\x2e\x32\x33\x2c\x32\x37\ +\x37\x2e\x34\x33\x2c\x37\x32\x2e\x38\x35\x2c\x32\x39\x30\x2e\x31\ +\x38\x2c\x37\x31\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\x33\x30\x30\x2c\x35\x31\x37\x2e\x33\x37\x63\x2d\x32\ +\x32\x2c\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x35\x2d\ +\x33\x39\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\ +\x2e\x33\x37\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\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\x37\x2c\ +\x33\x39\x2e\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\ +\x31\x2c\x35\x31\x37\x2e\x33\x37\x2c\x33\x30\x30\x2c\x35\x31\x37\ +\x2e\x33\x37\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\x34\ +\x34\x39\x2e\x30\x37\x2c\x32\x38\x30\x2e\x33\x37\x68\x31\x34\x2e\ +\x33\x35\x61\x31\x30\x2e\x36\x34\x2c\x31\x30\x2e\x36\x34\x2c\x30\ +\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x32\x2c\x31\x63\x32\x37\x2e\x34\ +\x37\x2c\x34\x2c\x34\x39\x2e\x36\x2c\x32\x36\x2e\x36\x37\x2c\x35\ +\x31\x2e\x36\x38\x2c\x35\x34\x2e\x32\x36\x2c\x31\x2e\x31\x37\x2c\ +\x31\x35\x2e\x35\x33\x2e\x35\x38\x2c\x33\x31\x2e\x32\x2e\x37\x38\ +\x2c\x34\x36\x2e\x38\x2c\x30\x2c\x32\x2e\x31\x32\x2c\x30\x2c\x34\ +\x2e\x32\x35\x2c\x30\x2c\x36\x2e\x38\x36\x68\x31\x31\x2e\x37\x35\ +\x63\x31\x33\x2e\x39\x34\x2c\x30\x2c\x31\x39\x2e\x33\x36\x2c\x35\ +\x2e\x33\x39\x2c\x31\x39\x2e\x33\x36\x2c\x31\x39\x2e\x32\x32\x2c\ +\x30\x2c\x33\x33\x2e\x32\x35\x2d\x2e\x31\x38\x2c\x36\x36\x2e\x35\ +\x2e\x31\x32\x2c\x39\x39\x2e\x37\x35\x2e\x30\x39\x2c\x39\x2e\x36\ +\x38\x2d\x32\x2e\x39\x33\x2c\x31\x36\x2e\x36\x37\x2d\x31\x32\x2e\ +\x31\x36\x2c\x32\x30\x2e\x33\x39\x48\x33\x37\x35\x2e\x31\x32\x63\ +\x2d\x37\x2e\x32\x34\x2d\x33\x2e\x31\x37\x2d\x31\x32\x2d\x38\x2e\ +\x31\x31\x2d\x31\x32\x2d\x31\x36\x2e\x35\x33\x2c\x30\x2d\x33\x35\ +\x2e\x34\x36\x2d\x2e\x30\x38\x2d\x37\x30\x2e\x39\x31\x2c\x30\x2d\ +\x31\x30\x36\x2e\x33\x37\x2c\x30\x2d\x31\x30\x2e\x32\x39\x2c\x36\ +\x2e\x32\x34\x2d\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\x35\x36\x2d\ +\x31\x36\x2e\x34\x34\x2c\x34\x2e\x36\x39\x2d\x2e\x30\x39\x2c\x39\ +\x2e\x33\x39\x2c\x30\x2c\x31\x34\x2e\x35\x31\x2c\x30\x2c\x30\x2d\ +\x31\x35\x2e\x37\x33\x2d\x2e\x30\x38\x2d\x33\x30\x2e\x35\x39\x2c\ +\x30\x2d\x34\x35\x2e\x34\x35\x2e\x31\x39\x2d\x32\x38\x2e\x37\x39\ +\x2c\x31\x37\x2e\x32\x38\x2d\x35\x32\x2e\x33\x36\x2c\x34\x33\x2e\ +\x38\x39\x2d\x36\x30\x2e\x35\x38\x43\x34\x34\x31\x2e\x37\x31\x2c\ +\x32\x38\x32\x2e\x31\x34\x2c\x34\x34\x35\x2e\x34\x31\x2c\x32\x38\ +\x31\x2e\x33\x33\x2c\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\x30\x2e\ +\x33\x37\x5a\x6d\x33\x38\x2e\x33\x31\x2c\x31\x30\x38\x2e\x34\x39\ +\x63\x30\x2d\x31\x36\x2e\x35\x34\x2e\x39\x34\x2d\x33\x32\x2e\x37\ +\x33\x2d\x2e\x32\x35\x2d\x34\x38\x2e\x37\x37\x2d\x31\x2e\x33\x31\ +\x2d\x31\x37\x2e\x38\x32\x2d\x31\x35\x2e\x39\x33\x2d\x32\x39\x2e\ +\x37\x35\x2d\x33\x32\x2e\x37\x37\x2d\x32\x38\x2e\x37\x37\x2d\x31\ +\x36\x2e\x36\x35\x2c\x31\x2d\x32\x39\x2e\x32\x31\x2c\x31\x34\x2e\ +\x37\x33\x2d\x32\x39\x2e\x34\x32\x2c\x33\x32\x2e\x35\x32\x2d\x2e\ +\x31\x36\x2c\x31\x33\x2e\x37\x37\x2c\x30\x2c\x32\x37\x2e\x35\x34\ +\x2c\x30\x2c\x34\x31\x2e\x33\x31\x61\x33\x33\x2e\x31\x39\x2c\x33\ +\x33\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x32\x2c\x33\ +\x2e\x37\x31\x5a\x6d\x2d\x33\x31\x2e\x33\x33\x2c\x34\x36\x2e\x37\ +\x31\x61\x31\x34\x2e\x39\x31\x2c\x31\x34\x2e\x39\x31\x2c\x30\x2c\ +\x30\x2c\x30\x2d\x31\x33\x2e\x36\x38\x2c\x38\x2e\x36\x36\x63\x2d\ +\x32\x2e\x38\x32\x2c\x35\x2e\x35\x37\x2d\x32\x2e\x38\x2c\x31\x31\ +\x2e\x35\x39\x2c\x31\x2e\x36\x2c\x31\x35\x2e\x38\x35\x2c\x34\x2e\ +\x31\x36\x2c\x34\x2c\x34\x2e\x34\x34\x2c\x38\x2e\x33\x35\x2c\x34\ +\x2e\x32\x32\x2c\x31\x33\x2e\x33\x32\x61\x36\x38\x2e\x33\x38\x2c\ +\x36\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\x37\x2e\ +\x37\x31\x63\x2e\x33\x37\x2c\x35\x2e\x33\x33\x2c\x33\x2e\x36\x2c\ +\x38\x2e\x38\x33\x2c\x38\x2c\x38\x2e\x38\x37\x73\x37\x2e\x37\x35\ +\x2d\x33\x2e\x34\x36\x2c\x38\x2e\x30\x37\x2d\x38\x2e\x37\x36\x61\ +\x31\x31\x31\x2e\x34\x34\x2c\x31\x31\x31\x2e\x34\x34\x2c\x30\x2c\ +\x30\x2c\x30\x2c\x30\x2d\x31\x31\x2e\x35\x36\x2c\x39\x2e\x36\x38\ +\x2c\x39\x2e\x36\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x2e\x32\x2d\ +\x38\x2e\x31\x33\x63\x34\x2e\x37\x31\x2d\x34\x2e\x36\x34\x2c\x35\ +\x2e\x36\x31\x2d\x31\x30\x2e\x35\x33\x2c\x33\x2d\x31\x36\x2e\x37\ +\x32\x41\x31\x35\x2e\x34\x38\x2c\x31\x35\x2e\x34\x38\x2c\x30\x2c\ +\x30\x2c\x30\x2c\x34\x35\x36\x2e\x30\x35\x2c\x34\x33\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\x33\x37\x39\ +\x2e\x35\x38\x2c\x33\x38\x34\x2e\x33\x31\x63\x33\x2e\x31\x38\x2d\ +\x2e\x30\x36\x2c\x36\x2e\x33\x31\x2d\x2e\x30\x35\x2c\x39\x2e\x36\ +\x31\x2c\x30\x2c\x30\x2d\x33\x2e\x32\x34\x2c\x30\x2d\x36\x2e\x34\ +\x33\x2c\x30\x2d\x39\x2e\x36\x31\x2c\x30\x2d\x31\x30\x2e\x35\x33\ +\x2c\x30\x2d\x32\x30\x2e\x34\x38\x2c\x30\x2d\x33\x30\x2e\x35\x38\ +\x2d\x33\x32\x2e\x31\x34\x2d\x32\x35\x2e\x32\x37\x2d\x36\x38\x2e\ +\x39\x33\x2d\x33\x35\x2e\x34\x32\x2d\x31\x31\x30\x2e\x34\x34\x2d\ +\x32\x39\x2e\x35\x37\x43\x32\x34\x36\x2c\x33\x31\x39\x2e\x31\x34\ +\x2c\x32\x31\x38\x2c\x33\x33\x34\x2e\x36\x31\x2c\x31\x39\x34\x2e\ +\x31\x33\x2c\x33\x35\x39\x63\x2d\x36\x2c\x36\x2e\x31\x37\x2d\x39\ +\x2e\x36\x33\x2c\x31\x33\x2e\x35\x39\x2d\x39\x2e\x36\x31\x2c\x32\ +\x32\x2e\x38\x31\x61\x36\x38\x2e\x39\x33\x2c\x36\x38\x2e\x39\x33\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x33\x38\x63\x35\x2e\ +\x36\x33\x2c\x32\x33\x2e\x32\x38\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\x31\x2c\x35\x33\x2e\ +\x37\x35\x2d\x33\x33\x2e\x38\x36\x2c\x38\x37\x2d\x32\x36\x2e\x37\ +\x37\x2c\x31\x35\x2c\x33\x2e\x32\x2c\x32\x38\x2c\x31\x30\x2e\x33\ +\x35\x2c\x33\x39\x2e\x36\x2c\x32\x30\x2e\x34\x32\x43\x33\x36\x33\ +\x2e\x31\x37\x2c\x33\x38\x38\x2e\x38\x32\x2c\x33\x37\x30\x2e\x30\ +\x37\x2c\x33\x38\x34\x2e\x34\x39\x2c\x33\x37\x39\x2e\x35\x38\x2c\ +\x33\x38\x34\x2e\x33\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\x33\x36\x2e\x36\x32\x2c\x32\x37\x38\x2e\x34\x38\x63\ +\x32\x2e\x36\x31\x2d\x2e\x38\x2c\x35\x2e\x32\x33\x2d\x31\x2e\x34\ +\x36\x2c\x37\x2e\x37\x37\x2d\x32\x2e\x30\x39\x2c\x31\x2e\x31\x34\ +\x2d\x2e\x32\x38\x2c\x32\x2e\x32\x38\x2d\x2e\x35\x36\x2c\x33\x2e\ +\x34\x32\x2d\x2e\x38\x36\x6c\x2e\x36\x32\x2d\x2e\x31\x36\x68\x31\ +\x36\x6c\x2e\x39\x33\x2e\x33\x39\x63\x2e\x33\x36\x2e\x31\x35\x2e\ +\x37\x32\x2e\x33\x33\x2c\x31\x2e\x30\x37\x2e\x35\x6c\x2e\x33\x37\ +\x2e\x31\x39\x61\x36\x36\x2e\x31\x38\x2c\x36\x36\x2e\x31\x38\x2c\ +\x30\x2c\x30\x2c\x31\x2c\x32\x36\x2e\x38\x33\x2c\x31\x30\x2e\x33\ +\x38\x2c\x33\x30\x2e\x33\x35\x2c\x33\x30\x2e\x33\x35\x2c\x30\x2c\ +\x30\x2c\x30\x2d\x35\x2e\x39\x34\x2d\x31\x30\x2e\x31\x35\x41\x32\ +\x30\x35\x2c\x32\x30\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x36\x34\ +\x2e\x34\x34\x2c\x32\x35\x34\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\x36\x2d\x35\x37\x2e\x30\ +\x39\x2c\x38\x2e\x38\x2d\x31\x30\x35\x2e\x38\x34\x2c\x33\x36\x2d\ +\x31\x34\x36\x2e\x38\x34\x2c\x37\x39\x2e\x32\x38\x2d\x38\x2e\x32\ +\x37\x2c\x38\x2e\x37\x32\x2d\x31\x30\x2e\x37\x39\x2c\x31\x39\x2e\ +\x36\x32\x2d\x36\x2e\x38\x39\x2c\x33\x31\x2e\x35\x32\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\x41\x31\x38\x37\ +\x2e\x39\x32\x2c\x31\x38\x37\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x31\ +\x2c\x34\x31\x35\x2c\x32\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\x0a\x5b\ +\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\x38\x63\x63\x35\x34\x30\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\ +\x31\x38\x2c\x37\x31\x2e\x33\x35\x43\x34\x30\x32\x2c\x37\x33\x2e\ +\x31\x34\x2c\x34\x39\x30\x2e\x31\x2c\x31\x31\x31\x2e\x34\x34\x2c\ +\x35\x36\x33\x2e\x39\x2c\x31\x38\x37\x2e\x34\x35\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\x2e\x30\x35\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\x32\x2d\x34\x36\x2e\x36\x31\x2d\x34\x31\x2e\x31\x31\x2d\ +\x33\x38\x2e\x34\x2d\x32\x39\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\ +\x2e\x37\x34\x2d\x31\x32\x37\x2e\x37\x35\x2d\x35\x34\x2e\x36\x34\ +\x2d\x35\x34\x2d\x39\x2e\x32\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\ +\x2e\x33\x32\x2d\x31\x35\x38\x2e\x35\x2c\x31\x35\x2e\x32\x32\x2d\ +\x34\x35\x2c\x31\x37\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\ +\x33\x2d\x31\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x34\x2d\x38\ +\x2e\x35\x37\x2c\x39\x2e\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\x31\x61\x33\x38\x30\ +\x2e\x35\x31\x2c\x33\x38\x30\x2e\x35\x31\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\x35\x43\x32\x35\ +\x35\x2e\x30\x37\x2c\x37\x34\x2e\x32\x33\x2c\x32\x37\x37\x2e\x34\ +\x33\x2c\x37\x32\x2e\x38\x35\x2c\x32\x39\x30\x2e\x31\x38\x2c\x37\ +\x31\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\ +\x33\x30\x30\x2c\x35\x31\x37\x2e\x33\x37\x63\x2d\x32\x32\x2c\x30\ +\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x35\x2d\x33\x39\x2e\ +\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\x2e\x33\x37\ +\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\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\x37\x2c\x33\x39\x2e\ +\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\ +\x31\x37\x2e\x33\x37\x2c\x33\x30\x30\x2c\x35\x31\x37\x2e\x33\x37\ +\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\x34\x34\x39\x2e\ +\x30\x37\x2c\x32\x38\x30\x2e\x33\x37\x68\x31\x34\x2e\x33\x35\x61\ +\x31\x30\x2e\x36\x34\x2c\x31\x30\x2e\x36\x34\x2c\x30\x2c\x30\x2c\ +\x30\x2c\x32\x2e\x34\x32\x2c\x31\x63\x32\x37\x2e\x34\x37\x2c\x34\ +\x2c\x34\x39\x2e\x36\x2c\x32\x36\x2e\x36\x37\x2c\x35\x31\x2e\x36\ +\x38\x2c\x35\x34\x2e\x32\x36\x2c\x31\x2e\x31\x37\x2c\x31\x35\x2e\ +\x35\x33\x2e\x35\x38\x2c\x33\x31\x2e\x32\x2e\x37\x38\x2c\x34\x36\ +\x2e\x38\x2c\x30\x2c\x32\x2e\x31\x32\x2c\x30\x2c\x34\x2e\x32\x35\ +\x2c\x30\x2c\x36\x2e\x38\x36\x68\x31\x31\x2e\x37\x35\x63\x31\x33\ +\x2e\x39\x34\x2c\x30\x2c\x31\x39\x2e\x33\x36\x2c\x35\x2e\x33\x39\ +\x2c\x31\x39\x2e\x33\x36\x2c\x31\x39\x2e\x32\x32\x2c\x30\x2c\x33\ +\x33\x2e\x32\x35\x2d\x2e\x31\x38\x2c\x36\x36\x2e\x35\x2e\x31\x32\ +\x2c\x39\x39\x2e\x37\x35\x2e\x30\x39\x2c\x39\x2e\x36\x38\x2d\x32\ +\x2e\x39\x33\x2c\x31\x36\x2e\x36\x37\x2d\x31\x32\x2e\x31\x36\x2c\ +\x32\x30\x2e\x33\x39\x48\x33\x37\x35\x2e\x31\x32\x63\x2d\x37\x2e\ +\x32\x34\x2d\x33\x2e\x31\x37\x2d\x31\x32\x2d\x38\x2e\x31\x31\x2d\ +\x31\x32\x2d\x31\x36\x2e\x35\x33\x2c\x30\x2d\x33\x35\x2e\x34\x36\ +\x2d\x2e\x30\x38\x2d\x37\x30\x2e\x39\x31\x2c\x30\x2d\x31\x30\x36\ +\x2e\x33\x37\x2c\x30\x2d\x31\x30\x2e\x32\x39\x2c\x36\x2e\x32\x34\ +\x2d\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\x35\x36\x2d\x31\x36\x2e\ +\x34\x34\x2c\x34\x2e\x36\x39\x2d\x2e\x30\x39\x2c\x39\x2e\x33\x39\ +\x2c\x30\x2c\x31\x34\x2e\x35\x31\x2c\x30\x2c\x30\x2d\x31\x35\x2e\ +\x37\x33\x2d\x2e\x30\x38\x2d\x33\x30\x2e\x35\x39\x2c\x30\x2d\x34\ +\x35\x2e\x34\x35\x2e\x31\x39\x2d\x32\x38\x2e\x37\x39\x2c\x31\x37\ +\x2e\x32\x38\x2d\x35\x32\x2e\x33\x36\x2c\x34\x33\x2e\x38\x39\x2d\ +\x36\x30\x2e\x35\x38\x43\x34\x34\x31\x2e\x37\x31\x2c\x32\x38\x32\ +\x2e\x31\x34\x2c\x34\x34\x35\x2e\x34\x31\x2c\x32\x38\x31\x2e\x33\ +\x33\x2c\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\x30\x2e\x33\x37\x5a\ +\x6d\x33\x38\x2e\x33\x31\x2c\x31\x30\x38\x2e\x34\x39\x63\x30\x2d\ +\x31\x36\x2e\x35\x34\x2e\x39\x34\x2d\x33\x32\x2e\x37\x33\x2d\x2e\ +\x32\x35\x2d\x34\x38\x2e\x37\x37\x2d\x31\x2e\x33\x31\x2d\x31\x37\ +\x2e\x38\x32\x2d\x31\x35\x2e\x39\x33\x2d\x32\x39\x2e\x37\x35\x2d\ +\x33\x32\x2e\x37\x37\x2d\x32\x38\x2e\x37\x37\x2d\x31\x36\x2e\x36\ +\x35\x2c\x31\x2d\x32\x39\x2e\x32\x31\x2c\x31\x34\x2e\x37\x33\x2d\ +\x32\x39\x2e\x34\x32\x2c\x33\x32\x2e\x35\x32\x2d\x2e\x31\x36\x2c\ +\x31\x33\x2e\x37\x37\x2c\x30\x2c\x32\x37\x2e\x35\x34\x2c\x30\x2c\ +\x34\x31\x2e\x33\x31\x61\x33\x33\x2e\x31\x39\x2c\x33\x33\x2e\x31\ +\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x32\x2c\x33\x2e\x37\x31\ +\x5a\x6d\x2d\x33\x31\x2e\x33\x33\x2c\x34\x36\x2e\x37\x31\x61\x31\ +\x34\x2e\x39\x31\x2c\x31\x34\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\ +\x2d\x31\x33\x2e\x36\x38\x2c\x38\x2e\x36\x36\x63\x2d\x32\x2e\x38\ +\x32\x2c\x35\x2e\x35\x37\x2d\x32\x2e\x38\x2c\x31\x31\x2e\x35\x39\ +\x2c\x31\x2e\x36\x2c\x31\x35\x2e\x38\x35\x2c\x34\x2e\x31\x36\x2c\ +\x34\x2c\x34\x2e\x34\x34\x2c\x38\x2e\x33\x35\x2c\x34\x2e\x32\x32\ +\x2c\x31\x33\x2e\x33\x32\x61\x36\x38\x2e\x33\x38\x2c\x36\x38\x2e\ +\x33\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\x37\x2e\x37\x31\x63\ +\x2e\x33\x37\x2c\x35\x2e\x33\x33\x2c\x33\x2e\x36\x2c\x38\x2e\x38\ +\x33\x2c\x38\x2c\x38\x2e\x38\x37\x73\x37\x2e\x37\x35\x2d\x33\x2e\ +\x34\x36\x2c\x38\x2e\x30\x37\x2d\x38\x2e\x37\x36\x61\x31\x31\x31\ +\x2e\x34\x34\x2c\x31\x31\x31\x2e\x34\x34\x2c\x30\x2c\x30\x2c\x30\ +\x2c\x30\x2d\x31\x31\x2e\x35\x36\x2c\x39\x2e\x36\x38\x2c\x39\x2e\ +\x36\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x2e\x32\x2d\x38\x2e\x31\ +\x33\x63\x34\x2e\x37\x31\x2d\x34\x2e\x36\x34\x2c\x35\x2e\x36\x31\ +\x2d\x31\x30\x2e\x35\x33\x2c\x33\x2d\x31\x36\x2e\x37\x32\x41\x31\ +\x35\x2e\x34\x38\x2c\x31\x35\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x30\ +\x2c\x34\x35\x36\x2e\x30\x35\x2c\x34\x33\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\x33\x37\x39\x2e\x35\x38\ +\x2c\x33\x38\x34\x2e\x33\x31\x63\x33\x2e\x31\x38\x2d\x2e\x30\x36\ +\x2c\x36\x2e\x33\x31\x2d\x2e\x30\x35\x2c\x39\x2e\x36\x31\x2c\x30\ +\x2c\x30\x2d\x33\x2e\x32\x34\x2c\x30\x2d\x36\x2e\x34\x33\x2c\x30\ +\x2d\x39\x2e\x36\x31\x2c\x30\x2d\x31\x30\x2e\x35\x33\x2c\x30\x2d\ +\x32\x30\x2e\x34\x38\x2c\x30\x2d\x33\x30\x2e\x35\x38\x2d\x33\x32\ +\x2e\x31\x34\x2d\x32\x35\x2e\x32\x37\x2d\x36\x38\x2e\x39\x33\x2d\ +\x33\x35\x2e\x34\x32\x2d\x31\x31\x30\x2e\x34\x34\x2d\x32\x39\x2e\ +\x35\x37\x43\x32\x34\x36\x2c\x33\x31\x39\x2e\x31\x34\x2c\x32\x31\ +\x38\x2c\x33\x33\x34\x2e\x36\x31\x2c\x31\x39\x34\x2e\x31\x33\x2c\ +\x33\x35\x39\x63\x2d\x36\x2c\x36\x2e\x31\x37\x2d\x39\x2e\x36\x33\ +\x2c\x31\x33\x2e\x35\x39\x2d\x39\x2e\x36\x31\x2c\x32\x32\x2e\x38\ +\x31\x61\x36\x38\x2e\x39\x33\x2c\x36\x38\x2e\x39\x33\x2c\x30\x2c\ +\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x33\x38\x63\x35\x2e\x36\x33\x2c\ +\x32\x33\x2e\x32\x38\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\x31\x2c\x35\x33\x2e\x37\x35\x2d\ +\x33\x33\x2e\x38\x36\x2c\x38\x37\x2d\x32\x36\x2e\x37\x37\x2c\x31\ +\x35\x2c\x33\x2e\x32\x2c\x32\x38\x2c\x31\x30\x2e\x33\x35\x2c\x33\ +\x39\x2e\x36\x2c\x32\x30\x2e\x34\x32\x43\x33\x36\x33\x2e\x31\x37\ +\x2c\x33\x38\x38\x2e\x38\x32\x2c\x33\x37\x30\x2e\x30\x37\x2c\x33\ +\x38\x34\x2e\x34\x39\x2c\x33\x37\x39\x2e\x35\x38\x2c\x33\x38\x34\ +\x2e\x33\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\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\x31\x61\x32\x36\x2e\ +\x31\x32\x2c\x32\x36\x2e\x31\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x37\ +\x2e\x37\x39\x2c\x31\x2e\x36\x36\x63\x35\x2e\x35\x39\x2c\x35\x2e\ +\x37\x32\x2c\x31\x32\x2e\x33\x34\x2c\x38\x2e\x37\x38\x2c\x32\x33\ +\x2c\x38\x2e\x36\x31\x61\x32\x34\x2c\x32\x34\x2c\x30\x2c\x30\x2c\ +\x30\x2c\x36\x2e\x32\x35\x2d\x31\x2e\x32\x37\x41\x32\x35\x2e\x36\ +\x38\x2c\x32\x35\x2e\x36\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x35\ +\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\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\x33\x36\x2e\x36\x32\x2c\x32\x37\ +\x38\x2e\x34\x38\x63\x32\x2e\x36\x31\x2d\x2e\x38\x2c\x35\x2e\x32\ +\x33\x2d\x31\x2e\x34\x36\x2c\x37\x2e\x37\x37\x2d\x32\x2e\x30\x39\ +\x2c\x31\x2e\x31\x34\x2d\x2e\x32\x38\x2c\x32\x2e\x32\x38\x2d\x2e\ +\x35\x36\x2c\x33\x2e\x34\x32\x2d\x2e\x38\x36\x6c\x2e\x36\x32\x2d\ +\x2e\x31\x36\x68\x31\x36\x6c\x2e\x39\x33\x2e\x33\x39\x63\x2e\x33\ +\x36\x2e\x31\x35\x2e\x37\x32\x2e\x33\x33\x2c\x31\x2e\x30\x37\x2e\ +\x35\x6c\x2e\x33\x37\x2e\x31\x39\x61\x36\x36\x2e\x31\x38\x2c\x36\ +\x36\x2e\x31\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x36\x2e\x38\x33\ +\x2c\x31\x30\x2e\x33\x38\x2c\x33\x30\x2e\x33\x35\x2c\x33\x30\x2e\ +\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\x39\x34\x2d\x31\x30\ +\x2e\x31\x35\x41\x32\x30\x35\x2c\x32\x30\x35\x2c\x30\x2c\x30\x2c\ +\x30\x2c\x34\x36\x34\x2e\x34\x34\x2c\x32\x35\x34\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\x36\ +\x2d\x35\x37\x2e\x30\x39\x2c\x38\x2e\x38\x2d\x31\x30\x35\x2e\x38\ +\x34\x2c\x33\x36\x2d\x31\x34\x36\x2e\x38\x34\x2c\x37\x39\x2e\x32\ +\x38\x2d\x38\x2e\x32\x37\x2c\x38\x2e\x37\x32\x2d\x31\x30\x2e\x37\ +\x39\x2c\x31\x39\x2e\x36\x32\x2d\x36\x2e\x38\x39\x2c\x33\x31\x2e\ +\x35\x32\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\x41\x31\x38\x37\x2e\x39\x32\x2c\x31\x38\x37\x2e\x39\x32\x2c\ +\x30\x2c\x30\x2c\x31\x2c\x34\x31\x35\x2c\x32\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\x0b\x47\ +\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\x2e\x63\x6c\x73\x2d\x33\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\x31\x38\x2c\x38\x32\x2e\x36\x32\x43\x34\x30\ +\x32\x2c\x38\x34\x2e\x34\x2c\x34\x39\x30\x2e\x31\x2c\x31\x32\x32\ +\x2e\x37\x2c\x35\x36\x33\x2e\x39\x2c\x31\x39\x38\x2e\x37\x31\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\x43\ +\x31\x34\x37\x2e\x38\x34\x2c\x31\x38\x30\x2e\x31\x32\x2c\x31\x30\ +\x38\x2e\x35\x35\x2c\x32\x30\x37\x2c\x37\x34\x2e\x36\x38\x2c\x32\ +\x34\x33\x63\x2d\x38\x2e\x35\x37\x2c\x39\x2e\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\x35\x30\x63\x34\x33\x2d\x33\x30\x2e\x35\ +\x37\x2c\x38\x39\x2e\x39\x33\x2d\x35\x31\x2e\x30\x39\x2c\x31\x34\ +\x30\x2e\x37\x37\x2d\x36\x30\x2e\x34\x35\x43\x32\x35\x35\x2e\x30\ +\x37\x2c\x38\x35\x2e\x34\x39\x2c\x32\x37\x37\x2e\x34\x33\x2c\x38\ +\x34\x2e\x31\x31\x2c\x32\x39\x30\x2e\x31\x38\x2c\x38\x32\x2e\x36\ +\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\x33\x30\x30\ +\x2c\x35\x32\x38\x2e\x36\x34\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\x33\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\x37\x2c\x33\x39\x2e\x33\x34\x2c\ +\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\x32\x38\x2e\ +\x36\x33\x2c\x33\x30\x30\x2c\x35\x32\x38\x2e\x36\x34\x5a\x22\x2f\ +\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ +\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x37\x39\x2e\x35\x38\x2c\ +\x33\x39\x35\x2e\x35\x37\x63\x33\x2e\x31\x38\x2d\x2e\x30\x36\x2c\ +\x36\x2e\x33\x31\x2d\x2e\x30\x35\x2c\x39\x2e\x36\x31\x2c\x30\x2c\ +\x30\x2d\x33\x2e\x32\x33\x2c\x30\x2d\x36\x2e\x34\x33\x2c\x30\x2d\ +\x39\x2e\x36\x31\x2c\x30\x2d\x31\x30\x2e\x35\x32\x2c\x30\x2d\x32\ +\x30\x2e\x34\x38\x2c\x30\x2d\x33\x30\x2e\x35\x38\x2d\x33\x32\x2e\ +\x31\x34\x2d\x32\x35\x2e\x32\x37\x2d\x36\x38\x2e\x39\x33\x2d\x33\ +\x35\x2e\x34\x32\x2d\x31\x31\x30\x2e\x34\x34\x2d\x32\x39\x2e\x35\ +\x37\x2d\x33\x32\x2e\x38\x2c\x34\x2e\x36\x32\x2d\x36\x30\x2e\x38\ +\x2c\x32\x30\x2e\x30\x39\x2d\x38\x34\x2e\x36\x34\x2c\x34\x34\x2e\ +\x34\x33\x2d\x36\x2c\x36\x2e\x31\x37\x2d\x39\x2e\x36\x33\x2c\x31\ +\x33\x2e\x36\x2d\x39\x2e\x36\x31\x2c\x32\x32\x2e\x38\x32\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\x37\x2c\x34\ +\x37\x2e\x39\x33\x2c\x31\x33\x2e\x32\x36\x2c\x32\x34\x2e\x36\x31\ +\x2d\x32\x35\x2e\x31\x31\x2c\x35\x33\x2e\x37\x35\x2d\x33\x33\x2e\ +\x38\x36\x2c\x38\x37\x2d\x32\x36\x2e\x37\x36\x2c\x31\x35\x2c\x33\ +\x2e\x32\x2c\x32\x38\x2c\x31\x30\x2e\x33\x35\x2c\x33\x39\x2e\x36\ +\x2c\x32\x30\x2e\x34\x32\x43\x33\x36\x33\x2e\x31\x37\x2c\x34\x30\ +\x30\x2e\x30\x38\x2c\x33\x37\x30\x2e\x30\x37\x2c\x33\x39\x35\x2e\ +\x37\x35\x2c\x33\x37\x39\x2e\x35\x38\x2c\x33\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\x35\x34\x2e\ +\x36\x35\x2c\x33\x32\x37\x2e\x35\x37\x61\x32\x36\x2e\x31\x32\x2c\ +\x32\x36\x2e\x31\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x37\x2e\x37\x39\ +\x2c\x31\x2e\x36\x36\x63\x35\x2e\x35\x39\x2c\x35\x2e\x37\x32\x2c\ +\x31\x32\x2e\x33\x34\x2c\x38\x2e\x37\x39\x2c\x32\x33\x2c\x38\x2e\ +\x36\x31\x61\x32\x33\x2e\x35\x35\x2c\x32\x33\x2e\x35\x35\x2c\x30\ +\x2c\x30\x2c\x30\x2c\x36\x2e\x32\x35\x2d\x31\x2e\x32\x37\x41\x32\ +\x35\x2e\x37\x31\x2c\x32\x35\x2e\x37\x31\x2c\x30\x2c\x30\x2c\x30\ +\x2c\x34\x35\x34\x2e\x36\x35\x2c\x33\x32\x37\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\x33\x36\x2e\x36\x32\ +\x2c\x32\x38\x39\x2e\x37\x34\x63\x32\x2e\x36\x31\x2d\x2e\x38\x2c\ +\x35\x2e\x32\x33\x2d\x31\x2e\x34\x35\x2c\x37\x2e\x37\x37\x2d\x32\ +\x2e\x30\x38\x6c\x33\x2e\x34\x32\x2d\x2e\x38\x37\x2e\x36\x32\x2d\ +\x2e\x31\x36\x68\x31\x36\x6c\x2e\x39\x33\x2e\x33\x39\x63\x2e\x33\ +\x36\x2e\x31\x36\x2e\x37\x32\x2e\x33\x33\x2c\x31\x2e\x30\x37\x2e\ +\x35\x31\x6c\x2e\x33\x37\x2e\x31\x38\x61\x36\x36\x2e\x31\x38\x2c\ +\x36\x36\x2e\x31\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x36\x2e\x38\ +\x33\x2c\x31\x30\x2e\x33\x38\x2c\x33\x30\x2e\x34\x35\x2c\x33\x30\ +\x2e\x34\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\x39\x34\x2d\x31\ +\x30\x2e\x31\x35\x2c\x32\x30\x35\x2e\x38\x33\x2c\x32\x30\x35\x2e\ +\x38\x33\x2c\x30\x2c\x30\x2c\x30\x2d\x32\x33\x2e\x32\x35\x2d\x32\ +\x32\x2e\x37\x63\x2d\x36\x30\x2e\x32\x32\x2d\x35\x30\x2e\x33\x2d\ +\x31\x32\x38\x2e\x33\x31\x2d\x37\x30\x2d\x32\x30\x33\x2e\x39\x31\ +\x2d\x35\x38\x2e\x33\x36\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\x32\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\x41\x31\x38\x38\x2e\ +\x31\x34\x2c\x31\x38\x38\x2e\x31\x34\x2c\x30\x2c\x30\x2c\x31\x2c\ +\x34\x31\x35\x2c\x33\x30\x30\x2e\x39\x34\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\x38\x39\x2e\x37\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\x34\x34\x39\x2e\x30\x37\x2c\x32\x39\x31\ +\x2e\x36\x33\x68\x31\x34\x2e\x33\x35\x61\x31\x30\x2e\x36\x34\x2c\ +\x31\x30\x2e\x36\x34\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x32\ +\x2c\x31\x63\x32\x37\x2e\x34\x37\x2c\x34\x2c\x34\x39\x2e\x36\x2c\ +\x32\x36\x2e\x36\x37\x2c\x35\x31\x2e\x36\x38\x2c\x35\x34\x2e\x32\ +\x36\x2c\x31\x2e\x31\x37\x2c\x31\x35\x2e\x35\x33\x2e\x35\x38\x2c\ +\x33\x31\x2e\x32\x2e\x37\x38\x2c\x34\x36\x2e\x38\x2c\x30\x2c\x32\ +\x2e\x31\x33\x2c\x30\x2c\x34\x2e\x32\x35\x2c\x30\x2c\x36\x2e\x38\ +\x37\x68\x31\x31\x2e\x37\x35\x63\x31\x33\x2e\x39\x34\x2c\x30\x2c\ +\x31\x39\x2e\x33\x36\x2c\x35\x2e\x33\x39\x2c\x31\x39\x2e\x33\x36\ +\x2c\x31\x39\x2e\x32\x32\x2c\x30\x2c\x33\x33\x2e\x32\x35\x2d\x2e\ +\x31\x38\x2c\x36\x36\x2e\x35\x2e\x31\x32\x2c\x39\x39\x2e\x37\x35\ +\x2e\x30\x39\x2c\x39\x2e\x36\x38\x2d\x32\x2e\x39\x33\x2c\x31\x36\ +\x2e\x36\x37\x2d\x31\x32\x2e\x31\x36\x2c\x32\x30\x2e\x33\x39\x48\ +\x33\x37\x35\x2e\x31\x32\x63\x2d\x37\x2e\x32\x34\x2d\x33\x2e\x31\ +\x37\x2d\x31\x32\x2d\x38\x2e\x31\x31\x2d\x31\x32\x2d\x31\x36\x2e\ +\x35\x33\x2c\x30\x2d\x33\x35\x2e\x34\x36\x2d\x2e\x30\x38\x2d\x37\ +\x30\x2e\x39\x31\x2c\x30\x2d\x31\x30\x36\x2e\x33\x37\x2c\x30\x2d\ +\x31\x30\x2e\x32\x39\x2c\x36\x2e\x32\x34\x2d\x31\x36\x2e\x32\x34\ +\x2c\x31\x36\x2e\x35\x36\x2d\x31\x36\x2e\x34\x34\x2c\x34\x2e\x36\ +\x39\x2d\x2e\x30\x39\x2c\x39\x2e\x33\x39\x2c\x30\x2c\x31\x34\x2e\ +\x35\x31\x2c\x30\x2c\x30\x2d\x31\x35\x2e\x37\x33\x2d\x2e\x30\x38\ +\x2d\x33\x30\x2e\x35\x39\x2c\x30\x2d\x34\x35\x2e\x34\x34\x2e\x31\ +\x39\x2d\x32\x38\x2e\x38\x2c\x31\x37\x2e\x32\x38\x2d\x35\x32\x2e\ +\x33\x37\x2c\x34\x33\x2e\x38\x39\x2d\x36\x30\x2e\x35\x39\x43\x34\ +\x34\x31\x2e\x37\x31\x2c\x32\x39\x33\x2e\x34\x31\x2c\x34\x34\x35\ +\x2e\x34\x31\x2c\x32\x39\x32\x2e\x35\x39\x2c\x34\x34\x39\x2e\x30\ +\x37\x2c\x32\x39\x31\x2e\x36\x33\x5a\x6d\x33\x38\x2e\x33\x31\x2c\ +\x31\x30\x38\x2e\x34\x39\x63\x30\x2d\x31\x36\x2e\x35\x34\x2e\x39\ +\x34\x2d\x33\x32\x2e\x37\x33\x2d\x2e\x32\x35\x2d\x34\x38\x2e\x37\ +\x37\x2d\x31\x2e\x33\x31\x2d\x31\x37\x2e\x38\x32\x2d\x31\x35\x2e\ +\x39\x33\x2d\x32\x39\x2e\x37\x35\x2d\x33\x32\x2e\x37\x37\x2d\x32\ +\x38\x2e\x37\x37\x2d\x31\x36\x2e\x36\x35\x2c\x31\x2d\x32\x39\x2e\ +\x32\x31\x2c\x31\x34\x2e\x37\x33\x2d\x32\x39\x2e\x34\x32\x2c\x33\ +\x32\x2e\x35\x32\x2d\x2e\x31\x36\x2c\x31\x33\x2e\x37\x37\x2c\x30\ +\x2c\x32\x37\x2e\x35\x34\x2c\x30\x2c\x34\x31\x2e\x33\x31\x61\x33\ +\x33\x2e\x31\x39\x2c\x33\x33\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x30\ +\x2c\x2e\x34\x32\x2c\x33\x2e\x37\x31\x5a\x6d\x2d\x33\x31\x2e\x33\ +\x33\x2c\x34\x36\x2e\x37\x31\x61\x31\x34\x2e\x39\x32\x2c\x31\x34\ +\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x33\x2e\x36\x38\x2c\ +\x38\x2e\x36\x36\x63\x2d\x32\x2e\x38\x32\x2c\x35\x2e\x35\x37\x2d\ +\x32\x2e\x38\x2c\x31\x31\x2e\x35\x39\x2c\x31\x2e\x36\x2c\x31\x35\ +\x2e\x38\x36\x2c\x34\x2e\x31\x36\x2c\x34\x2c\x34\x2e\x34\x34\x2c\ +\x38\x2e\x33\x34\x2c\x34\x2e\x32\x32\x2c\x31\x33\x2e\x33\x31\x61\ +\x36\x38\x2e\x33\x38\x2c\x36\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\ +\x30\x2c\x30\x2c\x37\x2e\x37\x31\x63\x2e\x33\x37\x2c\x35\x2e\x33\ +\x34\x2c\x33\x2e\x36\x2c\x38\x2e\x38\x33\x2c\x38\x2c\x38\x2e\x38\ +\x37\x73\x37\x2e\x37\x35\x2d\x33\x2e\x34\x36\x2c\x38\x2e\x30\x37\ +\x2d\x38\x2e\x37\x36\x61\x31\x31\x31\x2e\x34\x33\x2c\x31\x31\x31\ +\x2e\x34\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x31\x2e\x35\ +\x36\x2c\x39\x2e\x36\x39\x2c\x39\x2e\x36\x39\x2c\x30\x2c\x30\x2c\ +\x31\x2c\x33\x2e\x32\x2d\x38\x2e\x31\x33\x63\x34\x2e\x37\x31\x2d\ +\x34\x2e\x36\x34\x2c\x35\x2e\x36\x31\x2d\x31\x30\x2e\x35\x33\x2c\ +\x33\x2d\x31\x36\x2e\x37\x32\x41\x31\x35\x2e\x34\x38\x2c\x31\x35\ +\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x35\x36\x2e\x30\x35\ +\x2c\x34\x34\x36\x2e\x38\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\x33\x35\x38\x2e\x32\x34\x2c\x34\x31\x35\x2e\x38\x38\ +\x63\x30\x2d\x31\x32\x2e\x30\x35\x2c\x37\x2e\x37\x2d\x31\x39\x2e\ +\x36\x39\x2c\x32\x30\x2d\x31\x39\x2e\x39\x33\x2c\x33\x2e\x32\x35\ +\x2d\x2e\x30\x36\x2c\x36\x2e\x34\x35\x2c\x30\x2c\x39\x2e\x38\x34\ +\x2c\x30\x68\x31\x2e\x32\x34\x63\x30\x2d\x32\x2e\x35\x32\x2c\x30\ +\x2d\x35\x2c\x30\x2d\x37\x2e\x34\x38\x6c\x2d\x36\x31\x2e\x37\x31\ +\x2d\x38\x35\x2e\x36\x32\x2c\x31\x37\x32\x2e\x32\x31\x2d\x32\x33\ +\x39\x48\x34\x34\x31\x2e\x38\x37\x4c\x32\x39\x38\x2e\x36\x32\x2c\ +\x32\x36\x32\x2e\x36\x33\x2c\x31\x35\x35\x2e\x33\x38\x2c\x36\x33\ +\x2e\x38\x36\x48\x39\x37\x2e\x34\x35\x6c\x31\x37\x32\x2e\x32\x31\ +\x2c\x32\x33\x39\x2d\x31\x37\x32\x2e\x32\x33\x2c\x32\x33\x39\x68\ +\x35\x37\x2e\x39\x35\x4c\x32\x39\x38\x2e\x36\x32\x2c\x33\x34\x33\ +\x6c\x35\x39\x2e\x35\x39\x2c\x38\x32\x2e\x36\x39\x5a\x22\x2f\x3e\ +\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x0a\x70\ +\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\x2e\x63\x6c\x73\x2d\x33\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\x31\x38\x2c\x37\x31\x2e\x33\x35\x43\x34\x30\ +\x32\x2c\x37\x33\x2e\x31\x34\x2c\x34\x39\x30\x2e\x31\x2c\x31\x31\ +\x31\x2e\x34\x34\x2c\x35\x36\x33\x2e\x39\x2c\x31\x38\x37\x2e\x34\ +\x35\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\ +\x2e\x30\x35\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\x32\x2d\x34\x36\x2e\x36\x31\x2d\x34\ +\x31\x2e\x31\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\x2d\x38\x31\x2e\ +\x33\x33\x2d\x34\x36\x2e\x37\x34\x2d\x31\x32\x37\x2e\x37\x35\x2d\ +\x35\x34\x2e\x36\x34\x2d\x35\x34\x2d\x39\x2e\x32\x2d\x31\x30\x36\ +\x2e\x39\x32\x2d\x34\x2e\x33\x32\x2d\x31\x35\x38\x2e\x35\x2c\x31\ +\x35\x2e\x32\x32\x2d\x34\x35\x2c\x31\x37\x2d\x38\x34\x2e\x32\x39\ +\x2c\x34\x33\x2e\x39\x33\x2d\x31\x31\x38\x2e\x31\x36\x2c\x37\x39\ +\x2e\x39\x34\x2d\x38\x2e\x35\x37\x2c\x39\x2e\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\ +\x31\x61\x33\x38\x30\x2e\x35\x31\x2c\x33\x38\x30\x2e\x35\x31\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\x35\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x34\x2e\x32\x33\x2c\ +\x32\x37\x37\x2e\x34\x33\x2c\x37\x32\x2e\x38\x35\x2c\x32\x39\x30\ +\x2e\x31\x38\x2c\x37\x31\x2e\x33\x35\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\x31\x37\x2e\x33\x37\x63\ +\x2d\x32\x32\x2c\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\ +\x35\x2d\x33\x39\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\ +\x32\x33\x2e\x33\x37\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\ +\x33\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\ +\x37\x2c\x33\x39\x2e\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\ +\x32\x2e\x31\x2c\x35\x31\x37\x2e\x33\x37\x2c\x33\x30\x30\x2c\x35\ +\x31\x37\x2e\x33\x37\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\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\x30\x2e\x33\x37\x68\x31\ +\x34\x2e\x33\x35\x61\x31\x30\x2e\x36\x34\x2c\x31\x30\x2e\x36\x34\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x32\x2c\x31\x63\x32\x37\ +\x2e\x34\x37\x2c\x34\x2c\x34\x39\x2e\x36\x2c\x32\x36\x2e\x36\x37\ +\x2c\x35\x31\x2e\x36\x38\x2c\x35\x34\x2e\x32\x36\x2c\x31\x2e\x31\ +\x37\x2c\x31\x35\x2e\x35\x33\x2e\x35\x38\x2c\x33\x31\x2e\x32\x2e\ +\x37\x38\x2c\x34\x36\x2e\x38\x2c\x30\x2c\x32\x2e\x31\x32\x2c\x30\ +\x2c\x34\x2e\x32\x35\x2c\x30\x2c\x36\x2e\x38\x36\x68\x31\x31\x2e\ +\x37\x35\x63\x31\x33\x2e\x39\x34\x2c\x30\x2c\x31\x39\x2e\x33\x36\ +\x2c\x35\x2e\x33\x39\x2c\x31\x39\x2e\x33\x36\x2c\x31\x39\x2e\x32\ +\x32\x2c\x30\x2c\x33\x33\x2e\x32\x35\x2d\x2e\x31\x38\x2c\x36\x36\ +\x2e\x35\x2e\x31\x32\x2c\x39\x39\x2e\x37\x35\x2e\x30\x39\x2c\x39\ +\x2e\x36\x38\x2d\x32\x2e\x39\x33\x2c\x31\x36\x2e\x36\x37\x2d\x31\ +\x32\x2e\x31\x36\x2c\x32\x30\x2e\x33\x39\x48\x33\x37\x35\x2e\x31\ +\x32\x63\x2d\x37\x2e\x32\x34\x2d\x33\x2e\x31\x37\x2d\x31\x32\x2d\ +\x38\x2e\x31\x31\x2d\x31\x32\x2d\x31\x36\x2e\x35\x33\x2c\x30\x2d\ +\x33\x35\x2e\x34\x36\x2d\x2e\x30\x38\x2d\x37\x30\x2e\x39\x31\x2c\ +\x30\x2d\x31\x30\x36\x2e\x33\x37\x2c\x30\x2d\x31\x30\x2e\x32\x39\ +\x2c\x36\x2e\x32\x34\x2d\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\x35\ +\x36\x2d\x31\x36\x2e\x34\x34\x2c\x34\x2e\x36\x39\x2d\x2e\x30\x39\ +\x2c\x39\x2e\x33\x39\x2c\x30\x2c\x31\x34\x2e\x35\x31\x2c\x30\x2c\ +\x30\x2d\x31\x35\x2e\x37\x33\x2d\x2e\x30\x38\x2d\x33\x30\x2e\x35\ +\x39\x2c\x30\x2d\x34\x35\x2e\x34\x35\x2e\x31\x39\x2d\x32\x38\x2e\ +\x37\x39\x2c\x31\x37\x2e\x32\x38\x2d\x35\x32\x2e\x33\x36\x2c\x34\ +\x33\x2e\x38\x39\x2d\x36\x30\x2e\x35\x38\x43\x34\x34\x31\x2e\x37\ +\x31\x2c\x32\x38\x32\x2e\x31\x34\x2c\x34\x34\x35\x2e\x34\x31\x2c\ +\x32\x38\x31\x2e\x33\x33\x2c\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\ +\x30\x2e\x33\x37\x5a\x6d\x33\x38\x2e\x33\x31\x2c\x31\x30\x38\x2e\ +\x34\x39\x63\x30\x2d\x31\x36\x2e\x35\x34\x2e\x39\x34\x2d\x33\x32\ +\x2e\x37\x33\x2d\x2e\x32\x35\x2d\x34\x38\x2e\x37\x37\x2d\x31\x2e\ +\x33\x31\x2d\x31\x37\x2e\x38\x32\x2d\x31\x35\x2e\x39\x33\x2d\x32\ +\x39\x2e\x37\x35\x2d\x33\x32\x2e\x37\x37\x2d\x32\x38\x2e\x37\x37\ +\x2d\x31\x36\x2e\x36\x35\x2c\x31\x2d\x32\x39\x2e\x32\x31\x2c\x31\ +\x34\x2e\x37\x33\x2d\x32\x39\x2e\x34\x32\x2c\x33\x32\x2e\x35\x32\ +\x2d\x2e\x31\x36\x2c\x31\x33\x2e\x37\x37\x2c\x30\x2c\x32\x37\x2e\ +\x35\x34\x2c\x30\x2c\x34\x31\x2e\x33\x31\x61\x33\x33\x2e\x31\x39\ +\x2c\x33\x33\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x32\ +\x2c\x33\x2e\x37\x31\x5a\x6d\x2d\x33\x31\x2e\x33\x33\x2c\x34\x36\ +\x2e\x37\x31\x61\x31\x34\x2e\x39\x31\x2c\x31\x34\x2e\x39\x31\x2c\ +\x30\x2c\x30\x2c\x30\x2d\x31\x33\x2e\x36\x38\x2c\x38\x2e\x36\x36\ +\x63\x2d\x32\x2e\x38\x32\x2c\x35\x2e\x35\x37\x2d\x32\x2e\x38\x2c\ +\x31\x31\x2e\x35\x39\x2c\x31\x2e\x36\x2c\x31\x35\x2e\x38\x35\x2c\ +\x34\x2e\x31\x36\x2c\x34\x2c\x34\x2e\x34\x34\x2c\x38\x2e\x33\x35\ +\x2c\x34\x2e\x32\x32\x2c\x31\x33\x2e\x33\x32\x61\x36\x38\x2e\x33\ +\x38\x2c\x36\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\ +\x37\x2e\x37\x31\x63\x2e\x33\x37\x2c\x35\x2e\x33\x33\x2c\x33\x2e\ +\x36\x2c\x38\x2e\x38\x33\x2c\x38\x2c\x38\x2e\x38\x37\x73\x37\x2e\ +\x37\x35\x2d\x33\x2e\x34\x36\x2c\x38\x2e\x30\x37\x2d\x38\x2e\x37\ +\x36\x61\x31\x31\x31\x2e\x34\x34\x2c\x31\x31\x31\x2e\x34\x34\x2c\ +\x30\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x31\x2e\x35\x36\x2c\x39\x2e\ +\x36\x38\x2c\x39\x2e\x36\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x2e\ +\x32\x2d\x38\x2e\x31\x33\x63\x34\x2e\x37\x31\x2d\x34\x2e\x36\x34\ +\x2c\x35\x2e\x36\x31\x2d\x31\x30\x2e\x35\x33\x2c\x33\x2d\x31\x36\ +\x2e\x37\x32\x41\x31\x35\x2e\x34\x38\x2c\x31\x35\x2e\x34\x38\x2c\ +\x30\x2c\x30\x2c\x30\x2c\x34\x35\x36\x2e\x30\x35\x2c\x34\x33\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\x33\ +\x37\x39\x2e\x35\x38\x2c\x33\x38\x34\x2e\x33\x31\x63\x33\x2e\x31\ +\x38\x2d\x2e\x30\x36\x2c\x36\x2e\x33\x31\x2d\x2e\x30\x35\x2c\x39\ +\x2e\x36\x31\x2c\x30\x2c\x30\x2d\x33\x2e\x32\x34\x2c\x30\x2d\x36\ +\x2e\x34\x33\x2c\x30\x2d\x39\x2e\x36\x31\x2c\x30\x2d\x31\x30\x2e\ +\x35\x33\x2c\x30\x2d\x32\x30\x2e\x34\x38\x2c\x30\x2d\x33\x30\x2e\ +\x35\x38\x2d\x33\x32\x2e\x31\x34\x2d\x32\x35\x2e\x32\x37\x2d\x36\ +\x38\x2e\x39\x33\x2d\x33\x35\x2e\x34\x32\x2d\x31\x31\x30\x2e\x34\ +\x34\x2d\x32\x39\x2e\x35\x37\x43\x32\x34\x36\x2c\x33\x31\x39\x2e\ +\x31\x34\x2c\x32\x31\x38\x2c\x33\x33\x34\x2e\x36\x31\x2c\x31\x39\ +\x34\x2e\x31\x33\x2c\x33\x35\x39\x63\x2d\x36\x2c\x36\x2e\x31\x37\ +\x2d\x39\x2e\x36\x33\x2c\x31\x33\x2e\x35\x39\x2d\x39\x2e\x36\x31\ +\x2c\x32\x32\x2e\x38\x31\x61\x36\x38\x2e\x39\x33\x2c\x36\x38\x2e\ +\x39\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x33\x38\x63\ +\x35\x2e\x36\x33\x2c\x32\x33\x2e\x32\x38\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\x31\x2c\x35\ +\x33\x2e\x37\x35\x2d\x33\x33\x2e\x38\x36\x2c\x38\x37\x2d\x32\x36\ +\x2e\x37\x37\x2c\x31\x35\x2c\x33\x2e\x32\x2c\x32\x38\x2c\x31\x30\ +\x2e\x33\x35\x2c\x33\x39\x2e\x36\x2c\x32\x30\x2e\x34\x32\x43\x33\ +\x36\x33\x2e\x31\x37\x2c\x33\x38\x38\x2e\x38\x32\x2c\x33\x37\x30\ +\x2e\x30\x37\x2c\x33\x38\x34\x2e\x34\x39\x2c\x33\x37\x39\x2e\x35\ +\x38\x2c\x33\x38\x34\x2e\x33\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\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\ +\x31\x61\x32\x36\x2e\x31\x32\x2c\x32\x36\x2e\x31\x32\x2c\x30\x2c\ +\x30\x2c\x30\x2d\x37\x2e\x37\x39\x2c\x31\x2e\x36\x36\x63\x35\x2e\ +\x35\x39\x2c\x35\x2e\x37\x32\x2c\x31\x32\x2e\x33\x34\x2c\x38\x2e\ +\x37\x38\x2c\x32\x33\x2c\x38\x2e\x36\x31\x61\x32\x34\x2c\x32\x34\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x36\x2e\x32\x35\x2d\x31\x2e\x32\x37\ +\x41\x32\x35\x2e\x36\x38\x2c\x32\x35\x2e\x36\x38\x2c\x30\x2c\x30\ +\x2c\x30\x2c\x34\x35\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\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\x33\x36\x2e\ +\x36\x32\x2c\x32\x37\x38\x2e\x34\x38\x63\x32\x2e\x36\x31\x2d\x2e\ +\x38\x2c\x35\x2e\x32\x33\x2d\x31\x2e\x34\x36\x2c\x37\x2e\x37\x37\ +\x2d\x32\x2e\x30\x39\x2c\x31\x2e\x31\x34\x2d\x2e\x32\x38\x2c\x32\ +\x2e\x32\x38\x2d\x2e\x35\x36\x2c\x33\x2e\x34\x32\x2d\x2e\x38\x36\ +\x6c\x2e\x36\x32\x2d\x2e\x31\x36\x68\x31\x36\x6c\x2e\x39\x33\x2e\ +\x33\x39\x63\x2e\x33\x36\x2e\x31\x35\x2e\x37\x32\x2e\x33\x33\x2c\ +\x31\x2e\x30\x37\x2e\x35\x6c\x2e\x33\x37\x2e\x31\x39\x61\x36\x36\ +\x2e\x31\x38\x2c\x36\x36\x2e\x31\x38\x2c\x30\x2c\x30\x2c\x31\x2c\ +\x32\x36\x2e\x38\x33\x2c\x31\x30\x2e\x33\x38\x2c\x33\x30\x2e\x33\ +\x35\x2c\x33\x30\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\ +\x39\x34\x2d\x31\x30\x2e\x31\x35\x41\x32\x30\x35\x2c\x32\x30\x35\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x36\x34\x2e\x34\x34\x2c\x32\x35\ +\x34\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\x36\x2d\x35\x37\x2e\x30\x39\x2c\x38\x2e\x38\x2d\ +\x31\x30\x35\x2e\x38\x34\x2c\x33\x36\x2d\x31\x34\x36\x2e\x38\x34\ +\x2c\x37\x39\x2e\x32\x38\x2d\x38\x2e\x32\x37\x2c\x38\x2e\x37\x32\ +\x2d\x31\x30\x2e\x37\x39\x2c\x31\x39\x2e\x36\x32\x2d\x36\x2e\x38\ +\x39\x2c\x33\x31\x2e\x35\x32\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\x41\x31\x38\x37\x2e\x39\x32\x2c\x31\x38\ +\x37\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x31\x35\x2c\x32\ +\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\x09\x92\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -25625,6 +26750,10 @@ \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\ @@ -25642,6 +26771,10 @@ \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\ +\x0a\xdd\x57\x87\ +\x00\x34\ +\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\ \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\ @@ -25654,10 +26787,35 @@ \x0e\xdd\x57\x87\ \x00\x32\ \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\x17\ +\x0f\x21\x74\x27\ +\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\ +\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\x17\ +\x0f\x31\x74\x27\ +\x00\x34\ +\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\x17\ +\x0f\x51\x74\x27\ +\x00\x30\ +\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\x17\ +\x0f\x59\x74\x27\ +\x00\x31\ +\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\x15\ \x00\x03\x60\x47\ \x00\x74\ @@ -25876,9 +27034,9 @@ qt_resource_struct_v1 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x0f\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x94\ -\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x91\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x85\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9b\ +\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x98\ +\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x8c\ \x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7a\ \x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x76\ \x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x70\ @@ -25998,83 +27156,90 @@ \x00\x00\x0e\x70\x00\x00\x00\x00\x00\x01\x00\x04\x8c\xaa\ \x00\x00\x0e\x9c\x00\x00\x00\x00\x00\x01\x00\x04\x93\x91\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7b\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x09\x00\x00\x00\x7c\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x10\x00\x00\x00\x7c\ \x00\x00\x0e\xc0\x00\x00\x00\x00\x00\x01\x00\x04\x9c\xf5\ -\x00\x00\x0e\xe0\x00\x01\x00\x00\x00\x01\x00\x04\x9f\xa1\ -\x00\x00\x0f\x04\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xf2\ -\x00\x00\x0f\x26\x00\x00\x00\x00\x00\x01\x00\x04\xb2\x97\ -\x00\x00\x0f\x42\x00\x00\x00\x00\x00\x01\x00\x04\xc3\x97\ -\x00\x00\x0f\x66\x00\x00\x00\x00\x00\x01\x00\x04\xcd\x18\ -\x00\x00\x0f\x86\x00\x00\x00\x00\x00\x01\x00\x04\xd2\xb5\ -\x00\x00\x0f\xae\x00\x00\x00\x00\x00\x01\x00\x04\xdc\x7e\ -\x00\x00\x0f\xce\x00\x00\x00\x00\x00\x01\x00\x04\xe0\x61\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x86\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x87\ -\x00\x00\x0f\xea\x00\x00\x00\x00\x00\x01\x00\x04\xe6\x9c\ -\x00\x00\x10\x1a\x00\x00\x00\x00\x00\x01\x00\x04\xf0\x32\ -\x00\x00\x10\x4a\x00\x00\x00\x00\x00\x01\x00\x04\xfc\x0e\ -\x00\x00\x10\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x02\x52\ -\x00\x00\x10\x98\x00\x00\x00\x00\x00\x01\x00\x05\x09\xdb\ -\x00\x00\x10\xc4\x00\x00\x00\x00\x00\x01\x00\x05\x10\x39\ -\x00\x00\x10\xfa\x00\x00\x00\x00\x00\x01\x00\x05\x18\x28\ -\x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x25\xcb\ -\x00\x00\x11\x18\x00\x00\x00\x00\x00\x01\x00\x05\x2b\x00\ -\x00\x00\x11\x32\x00\x00\x00\x00\x00\x01\x00\x05\x34\xd5\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ -\x00\x00\x11\x5a\x00\x00\x00\x00\x00\x01\x00\x05\x3c\x9f\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x95\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x28\x00\x00\x00\x96\ -\x00\x00\x11\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x41\x65\ -\x00\x00\x11\x90\x00\x00\x00\x00\x00\x01\x00\x05\x49\x19\ -\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x4b\x3e\ -\x00\x00\x11\xe0\x00\x00\x00\x00\x00\x01\x00\x05\x4c\xbe\ -\x00\x00\x11\xfc\x00\x00\x00\x00\x00\x01\x00\x05\x54\x6b\ -\x00\x00\x12\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x59\x40\ -\x00\x00\x12\x32\x00\x00\x00\x00\x00\x01\x00\x05\x5a\x30\ -\x00\x00\x12\x48\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x14\ -\x00\x00\x12\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x63\x4b\ -\x00\x00\x12\x88\x00\x00\x00\x00\x00\x01\x00\x05\x78\x58\ -\x00\x00\x12\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x7d\x48\ -\x00\x00\x12\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x80\x47\ -\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x86\x51\ -\x00\x00\x13\x08\x00\x00\x00\x00\x00\x01\x00\x05\x89\xa0\ -\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x93\x21\ -\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\x99\x18\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9b\x29\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\xec\ -\x00\x00\x13\x88\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xa0\ -\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xab\xee\ -\x00\x00\x13\xd4\x00\x00\x00\x00\x00\x01\x00\x05\xaf\x7c\ -\x00\x00\x13\xfa\x00\x00\x00\x00\x00\x01\x00\x05\xb4\x1d\ -\x00\x00\x14\x0e\x00\x00\x00\x00\x00\x01\x00\x05\xbd\xef\ -\x00\x00\x14\x3a\x00\x00\x00\x00\x00\x01\x00\x05\xc3\x39\ -\x00\x00\x14\x62\x00\x00\x00\x00\x00\x01\x00\x05\xc9\x40\ -\x00\x00\x14\x78\x00\x00\x00\x00\x00\x01\x00\x05\xca\x24\ -\x00\x00\x14\xa4\x00\x00\x00\x00\x00\x01\x00\x05\xcc\x6d\ -\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x1d\ -\x00\x00\x14\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x61\ -\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xd7\x8d\ -\x00\x00\x15\x08\x00\x00\x00\x00\x00\x01\x00\x05\xdd\x4e\ -\x00\x00\x15\x2a\x00\x00\x00\x00\x00\x01\x00\x05\xde\x70\ -\x00\x00\x15\x48\x00\x00\x00\x00\x00\x01\x00\x05\xe4\x63\ -\x00\x00\x15\x68\x00\x00\x00\x00\x00\x01\x00\x05\xe7\x67\ -\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x88\ -\x00\x00\x15\xaa\x00\x00\x00\x00\x00\x01\x00\x05\xeb\x5c\ -\x00\x00\x15\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xf3\xc8\ -\x00\x00\x15\xfc\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x88\ -\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x06\x00\xa3\ -\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x01\xf6\ +\x00\x00\x0e\xe0\x00\x00\x00\x00\x00\x01\x00\x04\xa2\x8e\ +\x00\x00\x0f\x00\x00\x01\x00\x00\x00\x01\x00\x04\xa8\xb5\ +\x00\x00\x0f\x24\x00\x00\x00\x00\x00\x01\x00\x04\xb4\x06\ +\x00\x00\x0f\x46\x00\x00\x00\x00\x00\x01\x00\x04\xbb\xab\ +\x00\x00\x0f\x62\x00\x00\x00\x00\x00\x01\x00\x04\xcc\xab\ +\x00\x00\x0f\x86\x00\x00\x00\x00\x00\x01\x00\x04\xd6\x2c\ +\x00\x00\x0f\xa6\x00\x00\x00\x00\x00\x01\x00\x04\xdb\xb0\ +\x00\x00\x0f\xc6\x00\x00\x00\x00\x00\x01\x00\x04\xe1\x49\ +\x00\x00\x0f\xee\x00\x00\x00\x00\x00\x01\x00\x04\xeb\x12\ +\x00\x00\x10\x0e\x00\x00\x00\x00\x00\x01\x00\x04\xf0\xab\ +\x00\x00\x10\x42\x00\x00\x00\x00\x00\x01\x00\x04\xfb\x1f\ +\x00\x00\x10\x5e\x00\x00\x00\x00\x00\x01\x00\x05\x01\x5a\ +\x00\x00\x10\x92\x00\x00\x00\x00\x00\x01\x00\x05\x0b\xd4\ +\x00\x00\x10\xc6\x00\x00\x00\x00\x00\x01\x00\x05\x16\x33\ +\x00\x00\x10\xfa\x00\x00\x00\x00\x00\x01\x00\x05\x21\x7e\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x8d\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x8e\ +\x00\x00\x11\x2e\x00\x00\x00\x00\x00\x01\x00\x05\x2b\xf2\ +\x00\x00\x11\x5e\x00\x00\x00\x00\x00\x01\x00\x05\x35\x88\ +\x00\x00\x11\x8e\x00\x00\x00\x00\x00\x01\x00\x05\x41\x64\ +\x00\x00\x11\xb2\x00\x00\x00\x00\x00\x01\x00\x05\x47\xa8\ +\x00\x00\x11\xdc\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x31\ +\x00\x00\x12\x08\x00\x00\x00\x00\x00\x01\x00\x05\x55\x8f\ +\x00\x00\x12\x3e\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x7e\ +\x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x6b\x21\ +\x00\x00\x12\x5c\x00\x00\x00\x00\x00\x01\x00\x05\x70\x56\ +\x00\x00\x12\x76\x00\x00\x00\x00\x00\x01\x00\x05\x7a\x2b\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x99\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9a\ +\x00\x00\x12\x9e\x00\x00\x00\x00\x00\x01\x00\x05\x81\xf5\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9c\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x28\x00\x00\x00\x9d\ +\x00\x00\x12\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x86\xbb\ +\x00\x00\x12\xd4\x00\x00\x00\x00\x00\x01\x00\x05\x8e\x6f\ +\x00\x00\x12\xec\x00\x00\x00\x00\x00\x01\x00\x05\x90\x94\ +\x00\x00\x13\x24\x00\x00\x00\x00\x00\x01\x00\x05\x92\x14\ +\x00\x00\x13\x40\x00\x00\x00\x00\x00\x01\x00\x05\x99\xc1\ +\x00\x00\x13\x60\x00\x00\x00\x00\x00\x01\x00\x05\x9e\x96\ +\x00\x00\x13\x76\x00\x00\x00\x00\x00\x01\x00\x05\x9f\x86\ +\x00\x00\x13\x8c\x00\x00\x00\x00\x00\x01\x00\x05\xa2\x6a\ +\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xa1\ +\x00\x00\x13\xcc\x00\x00\x00\x00\x00\x01\x00\x05\xbd\xae\ +\x00\x00\x13\xee\x00\x00\x00\x00\x00\x01\x00\x05\xc2\x9e\ +\x00\x00\x14\x04\x00\x00\x00\x00\x00\x01\x00\x05\xc5\x9d\ +\x00\x00\x14\x1a\x00\x00\x00\x00\x00\x01\x00\x05\xcb\xa7\ +\x00\x00\x14\x4c\x00\x00\x00\x00\x00\x01\x00\x05\xce\xf6\ +\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xd8\x77\ +\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xde\x6e\ +\x00\x00\x14\x8e\x00\x00\x00\x00\x00\x01\x00\x05\xe0\x7f\ +\x00\x00\x14\xa6\x00\x00\x00\x00\x00\x01\x00\x05\xe4\x42\ +\x00\x00\x14\xcc\x00\x00\x00\x00\x00\x01\x00\x05\xed\xf6\ +\x00\x00\x14\xf6\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x44\ +\x00\x00\x15\x18\x00\x00\x00\x00\x00\x01\x00\x05\xf4\xd2\ +\x00\x00\x15\x3e\x00\x00\x00\x00\x00\x01\x00\x05\xf9\x73\ +\x00\x00\x15\x52\x00\x00\x00\x00\x00\x01\x00\x06\x03\x45\ +\x00\x00\x15\x7e\x00\x00\x00\x00\x00\x01\x00\x06\x08\x8f\ +\x00\x00\x15\xa6\x00\x00\x00\x00\x00\x01\x00\x06\x0e\x96\ +\x00\x00\x15\xbc\x00\x00\x00\x00\x00\x01\x00\x06\x0f\x7a\ +\x00\x00\x15\xe8\x00\x00\x00\x00\x00\x01\x00\x06\x11\xc3\ +\x00\x00\x15\xfe\x00\x00\x00\x00\x00\x01\x00\x06\x18\x73\ +\x00\x00\x16\x1a\x00\x00\x00\x00\x00\x01\x00\x06\x1b\xb7\ +\x00\x00\x16\x32\x00\x00\x00\x00\x00\x01\x00\x06\x1c\xe3\ +\x00\x00\x16\x4c\x00\x00\x00\x00\x00\x01\x00\x06\x22\xa4\ +\x00\x00\x16\x6e\x00\x00\x00\x00\x00\x01\x00\x06\x23\xc6\ +\x00\x00\x16\x8c\x00\x00\x00\x00\x00\x01\x00\x06\x29\xb9\ +\x00\x00\x16\xac\x00\x00\x00\x00\x00\x01\x00\x06\x2c\xbd\ +\x00\x00\x16\xce\x00\x00\x00\x00\x00\x01\x00\x06\x2d\xde\ +\x00\x00\x16\xee\x00\x00\x00\x00\x00\x01\x00\x06\x30\xb2\ +\x00\x00\x17\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x39\x1e\ +\x00\x00\x17\x40\x00\x00\x00\x00\x00\x01\x00\x06\x40\xde\ +\x00\x00\x17\x64\x00\x00\x00\x00\x00\x01\x00\x06\x45\xf9\ +\x00\x00\x17\x8c\x00\x00\x00\x00\x00\x01\x00\x06\x47\x4c\ " qt_resource_struct_v2 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x0f\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x94\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9b\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x91\ +\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x98\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x85\ +\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x8c\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7a\ \x00\x00\x00\x00\x00\x00\x00\x00\ @@ -26105,75 +27270,75 @@ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x12\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x01\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x08\x66\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x01\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x09\x52\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x02\x20\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x35\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x02\x48\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x18\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x02\x70\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x06\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x02\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x14\x42\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x02\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf7\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x02\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x22\x41\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x03\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x26\x90\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x03\x36\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x6e\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x03\x52\x00\x00\x00\x00\x00\x01\x00\x00\x30\xca\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x03\x7a\x00\x00\x00\x00\x00\x01\x00\x00\x32\xb1\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x04\x00\x00\x00\x21\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x03\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x3a\x55\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x03\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x3c\xcf\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x03\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x3f\x43\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x03\xfe\x00\x00\x00\x00\x00\x01\x00\x00\x41\xbf\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x26\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x27\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x04\x20\x00\x00\x00\x00\x00\x01\x00\x00\x44\x3b\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x04\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x45\x3c\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x04\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x4a\x54\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x04\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x56\x16\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x04\xc4\x00\x00\x00\x00\x00\x01\x00\x00\x58\x86\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x04\xea\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x1e\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x05\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x62\x7a\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x05\x48\x00\x00\x00\x00\x00\x01\x00\x00\x69\x07\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x05\x64\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x03\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x05\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x6d\x01\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x32\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x33\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x05\xbe\x00\x01\x00\x00\x00\x01\x00\x00\x77\x6b\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x05\xd0\x00\x00\x00\x00\x00\x01\x00\x02\x3a\xc0\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x02\x00\x00\x00\x36\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x06\x00\x00\x00\x3f\ @@ -26181,273 +27346,287 @@ \x00\x00\x05\xe4\x00\x02\x00\x00\x00\x07\x00\x00\x00\x38\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x05\xf6\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ -\x00\x00\x01\x98\xe1\xb8\x63\x96\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x49\xaa\ -\x00\x00\x01\x98\xe1\xb8\x63\x96\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x06\x5e\x00\x00\x00\x00\x00\x01\x00\x02\x53\xd3\ -\x00\x00\x01\x98\xe1\xb8\x63\x96\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x06\x98\x00\x00\x00\x00\x00\x01\x00\x02\x5e\x2b\ -\x00\x00\x01\x98\xe1\xb8\x63\x9a\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x06\xcc\x00\x00\x00\x00\x00\x01\x00\x02\x68\x43\ -\x00\x00\x01\x98\xe1\xb8\x63\x96\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x07\x02\x00\x00\x00\x00\x00\x01\x00\x02\x72\x6c\ -\x00\x00\x01\x98\xe1\xb8\x63\x96\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x07\x3a\x00\x00\x00\x00\x00\x01\x00\x02\x7c\x82\ -\x00\x00\x01\x98\xe1\xb8\x63\x96\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x07\x70\x00\x00\x00\x00\x00\x01\x00\x02\x86\xe8\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x07\x98\x00\x00\x00\x00\x00\x01\x00\x02\x91\x17\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x07\xc4\x00\x00\x00\x00\x00\x01\x00\x02\x9b\x5e\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x08\x00\x00\x00\x00\x00\x00\x01\x00\x02\x9d\x5f\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x08\x2c\x00\x00\x00\x00\x00\x01\x00\x02\xb8\xa4\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x08\x58\x00\x00\x00\x00\x00\x01\x00\x02\xbd\xed\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x46\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x47\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x08\x8c\x00\x00\x00\x00\x00\x01\x00\x02\xc1\x65\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x02\xca\x47\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4a\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x4b\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x02\xcf\x7c\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x08\xe6\x00\x00\x00\x00\x00\x01\x00\x02\xd4\x77\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x09\x1e\x00\x00\x00\x00\x00\x01\x00\x02\xdd\x4b\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x09\x56\x00\x00\x00\x00\x00\x01\x00\x02\xe5\xef\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x09\x86\x00\x00\x00\x00\x00\x01\x00\x02\xed\x8e\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x09\xaa\x00\x00\x00\x00\x00\x01\x00\x02\xf5\x6a\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x09\xce\x00\x00\x00\x00\x00\x01\x00\x02\xfd\x20\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0a\x02\x00\x00\x00\x00\x00\x01\x00\x03\x05\x2e\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0a\x36\x00\x00\x00\x00\x00\x01\x00\x03\x0d\x10\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0a\x6a\x00\x00\x00\x00\x00\x01\x00\x03\x14\xda\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0a\xa4\x00\x00\x00\x00\x00\x01\x00\x03\x1c\x41\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0a\xc8\x00\x00\x00\x00\x00\x01\x00\x03\x1f\xfe\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0a\xf6\x00\x00\x00\x00\x00\x01\x00\x03\x23\xd7\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x59\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x08\x00\x00\x00\x5a\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0b\x1c\x00\x00\x00\x00\x00\x01\x00\x03\x29\xe0\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0b\x48\x00\x00\x00\x00\x00\x01\x00\x03\x4c\x64\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0b\x76\x00\x00\x00\x00\x00\x01\x00\x03\x52\x51\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0b\xa0\x00\x00\x00\x00\x00\x01\x00\x03\x54\x71\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0b\xc8\x00\x00\x00\x00\x00\x01\x00\x03\x5d\x09\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0b\xe2\x00\x00\x00\x00\x00\x01\x00\x03\x6c\x52\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0c\x0e\x00\x00\x00\x00\x00\x01\x00\x03\x72\xd0\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0c\x36\x00\x00\x00\x00\x00\x01\x00\x03\x7d\x4a\ -\x00\x00\x01\x99\xe8\x24\xab\x4a\ +\x00\x00\x01\x9a\x3a\xfe\x5d\xa0\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x63\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x64\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0c\x6c\x00\x00\x00\x00\x00\x01\x00\x03\x86\x91\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0c\x9a\x00\x00\x00\x00\x00\x01\x00\x03\x89\x38\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0c\xc8\x00\x01\x00\x00\x00\x01\x00\x03\x95\xb5\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0c\xf4\x00\x00\x00\x00\x00\x01\x00\x03\xc3\x34\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0d\x14\x00\x00\x00\x00\x00\x01\x00\x03\xc7\xeb\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0d\x46\x00\x01\x00\x00\x00\x01\x00\x04\x20\xe4\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0d\x78\x00\x00\x00\x00\x00\x01\x00\x04\x55\x7e\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0d\x92\x00\x00\x00\x00\x00\x01\x00\x04\x5a\xc8\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0d\xac\x00\x00\x00\x00\x00\x01\x00\x04\x60\x57\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0d\xc6\x00\x00\x00\x00\x00\x01\x00\x04\x65\xc0\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0d\xde\x00\x00\x00\x00\x00\x01\x00\x04\x71\x9e\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0d\xfc\x00\x00\x00\x00\x00\x01\x00\x04\x77\xa2\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x71\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x04\x00\x00\x00\x72\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0e\x24\x00\x00\x00\x00\x00\x01\x00\x04\x7c\xd7\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0e\x38\x00\x00\x00\x00\x00\x01\x00\x04\x82\xd4\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0e\x4a\x00\x00\x00\x00\x00\x01\x00\x04\x84\x5a\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0e\x5c\x00\x00\x00\x00\x00\x01\x00\x04\x8a\x54\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x77\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x78\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0e\x70\x00\x00\x00\x00\x00\x01\x00\x04\x8c\xaa\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x0e\x9c\x00\x00\x00\x00\x00\x01\x00\x04\x93\x91\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7b\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x09\x00\x00\x00\x7c\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x10\x00\x00\x00\x7c\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0e\xc0\x00\x00\x00\x00\x00\x01\x00\x04\x9c\xf5\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x0e\xe0\x00\x01\x00\x00\x00\x01\x00\x04\x9f\xa1\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x0f\x04\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xf2\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x0f\x26\x00\x00\x00\x00\x00\x01\x00\x04\xb2\x97\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x0f\x42\x00\x00\x00\x00\x00\x01\x00\x04\xc3\x97\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x0f\x66\x00\x00\x00\x00\x00\x01\x00\x04\xcd\x18\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x0f\x86\x00\x00\x00\x00\x00\x01\x00\x04\xd2\xb5\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x0f\xae\x00\x00\x00\x00\x00\x01\x00\x04\xdc\x7e\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x0f\xce\x00\x00\x00\x00\x00\x01\x00\x04\xe0\x61\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x86\ +\x00\x00\x01\x9a\xc0\x0b\xf8\x0b\ +\x00\x00\x0e\xe0\x00\x00\x00\x00\x00\x01\x00\x04\xa2\x8e\ +\x00\x00\x01\x9a\xc0\x0b\xf8\x07\ +\x00\x00\x0f\x00\x00\x01\x00\x00\x00\x01\x00\x04\xa8\xb5\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x0f\x24\x00\x00\x00\x00\x00\x01\x00\x04\xb4\x06\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x0f\x46\x00\x00\x00\x00\x00\x01\x00\x04\xbb\xab\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x0f\x62\x00\x00\x00\x00\x00\x01\x00\x04\xcc\xab\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x0f\x86\x00\x00\x00\x00\x00\x01\x00\x04\xd6\x2c\ +\x00\x00\x01\x9a\xc0\x0b\xf8\x0b\ +\x00\x00\x0f\xa6\x00\x00\x00\x00\x00\x01\x00\x04\xdb\xb0\ +\x00\x00\x01\x9a\xc0\x0b\xf8\x0b\ +\x00\x00\x0f\xc6\x00\x00\x00\x00\x00\x01\x00\x04\xe1\x49\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x0f\xee\x00\x00\x00\x00\x00\x01\x00\x04\xeb\x12\ +\x00\x00\x01\x9a\xc0\x0b\xf8\x0b\ +\x00\x00\x10\x0e\x00\x00\x00\x00\x00\x01\x00\x04\xf0\xab\ +\x00\x00\x01\x9a\xc0\x0b\xf8\x0b\ +\x00\x00\x10\x42\x00\x00\x00\x00\x00\x01\x00\x04\xfb\x1f\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x10\x5e\x00\x00\x00\x00\x00\x01\x00\x05\x01\x5a\ +\x00\x00\x01\x9a\xc0\x0b\xf8\x0b\ +\x00\x00\x10\x92\x00\x00\x00\x00\x00\x01\x00\x05\x0b\xd4\ +\x00\x00\x01\x9a\xc0\x0b\xf8\x0b\ +\x00\x00\x10\xc6\x00\x00\x00\x00\x00\x01\x00\x05\x16\x33\ +\x00\x00\x01\x9a\xc0\x0b\xf8\x07\ +\x00\x00\x10\xfa\x00\x00\x00\x00\x00\x01\x00\x05\x21\x7e\ +\x00\x00\x01\x9a\xc0\x0b\xf8\x0b\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x8d\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x87\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x8e\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0f\xea\x00\x00\x00\x00\x00\x01\x00\x04\xe6\x9c\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x10\x1a\x00\x00\x00\x00\x00\x01\x00\x04\xf0\x32\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x10\x4a\x00\x00\x00\x00\x00\x01\x00\x04\xfc\x0e\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x10\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x02\x52\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x10\x98\x00\x00\x00\x00\x00\x01\x00\x05\x09\xdb\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x10\xc4\x00\x00\x00\x00\x00\x01\x00\x05\x10\x39\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x10\xfa\x00\x00\x00\x00\x00\x01\x00\x05\x18\x28\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x25\xcb\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x11\x18\x00\x00\x00\x00\x00\x01\x00\x05\x2b\x00\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x11\x32\x00\x00\x00\x00\x00\x01\x00\x05\x34\xd5\ -\x00\x00\x01\x99\xe8\x24\xab\x4a\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ +\x00\x00\x11\x2e\x00\x00\x00\x00\x00\x01\x00\x05\x2b\xf2\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x11\x5e\x00\x00\x00\x00\x00\x01\x00\x05\x35\x88\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x11\x8e\x00\x00\x00\x00\x00\x01\x00\x05\x41\x64\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x11\xb2\x00\x00\x00\x00\x00\x01\x00\x05\x47\xa8\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x11\xdc\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x31\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x12\x08\x00\x00\x00\x00\x00\x01\x00\x05\x55\x8f\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x12\x3e\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x7e\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x6b\x21\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x12\x5c\x00\x00\x00\x00\x00\x01\x00\x05\x70\x56\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x12\x76\x00\x00\x00\x00\x00\x01\x00\x05\x7a\x2b\ +\x00\x00\x01\x9a\x3a\xfe\x5d\xa0\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x99\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9a\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x11\x5a\x00\x00\x00\x00\x00\x01\x00\x05\x3c\x9f\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x95\ +\x00\x00\x12\x9e\x00\x00\x00\x00\x00\x01\x00\x05\x81\xf5\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9c\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x28\x00\x00\x00\x96\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x28\x00\x00\x00\x9d\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x11\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x41\x65\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x11\x90\x00\x00\x00\x00\x00\x01\x00\x05\x49\x19\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x4b\x3e\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x11\xe0\x00\x00\x00\x00\x00\x01\x00\x05\x4c\xbe\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x11\xfc\x00\x00\x00\x00\x00\x01\x00\x05\x54\x6b\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x12\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x59\x40\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x12\x32\x00\x00\x00\x00\x00\x01\x00\x05\x5a\x30\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x12\x48\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x14\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x12\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x63\x4b\ -\x00\x00\x01\x99\x96\xf9\x85\x18\ -\x00\x00\x12\x88\x00\x00\x00\x00\x00\x01\x00\x05\x78\x58\ -\x00\x00\x01\x99\xe8\x24\xab\x4a\ -\x00\x00\x12\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x7d\x48\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x12\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x80\x47\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x86\x51\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x13\x08\x00\x00\x00\x00\x00\x01\x00\x05\x89\xa0\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x93\x21\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\x99\x18\ -\x00\x00\x01\x99\x96\xf9\x85\x18\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9b\x29\ -\x00\x00\x01\x99\x96\xf9\x85\x18\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\xec\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x13\x88\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xa0\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xab\xee\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x13\xd4\x00\x00\x00\x00\x00\x01\x00\x05\xaf\x7c\ -\x00\x00\x01\x99\xed\x4d\xf1\x14\ -\x00\x00\x13\xfa\x00\x00\x00\x00\x00\x01\x00\x05\xb4\x1d\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x14\x0e\x00\x00\x00\x00\x00\x01\x00\x05\xbd\xef\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x14\x3a\x00\x00\x00\x00\x00\x01\x00\x05\xc3\x39\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x14\x62\x00\x00\x00\x00\x00\x01\x00\x05\xc9\x40\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x14\x78\x00\x00\x00\x00\x00\x01\x00\x05\xca\x24\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x14\xa4\x00\x00\x00\x00\x00\x01\x00\x05\xcc\x6d\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x1d\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x14\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x61\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xd7\x8d\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x15\x08\x00\x00\x00\x00\x00\x01\x00\x05\xdd\x4e\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x15\x2a\x00\x00\x00\x00\x00\x01\x00\x05\xde\x70\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x15\x48\x00\x00\x00\x00\x00\x01\x00\x05\xe4\x63\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x15\x68\x00\x00\x00\x00\x00\x01\x00\x05\xe7\x67\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x88\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x15\xaa\x00\x00\x00\x00\x00\x01\x00\x05\xeb\x5c\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x15\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xf3\xc8\ +\x00\x00\x12\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x86\xbb\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x12\xd4\x00\x00\x00\x00\x00\x01\x00\x05\x8e\x6f\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x12\xec\x00\x00\x00\x00\x00\x01\x00\x05\x90\x94\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x13\x24\x00\x00\x00\x00\x00\x01\x00\x05\x92\x14\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x13\x40\x00\x00\x00\x00\x00\x01\x00\x05\x99\xc1\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x13\x60\x00\x00\x00\x00\x00\x01\x00\x05\x9e\x96\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x13\x76\x00\x00\x00\x00\x00\x01\x00\x05\x9f\x86\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x13\x8c\x00\x00\x00\x00\x00\x01\x00\x05\xa2\x6a\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xa1\ +\x00\x00\x01\x9a\x3a\xfe\x5d\xa0\ +\x00\x00\x13\xcc\x00\x00\x00\x00\x00\x01\x00\x05\xbd\xae\ +\x00\x00\x01\x9a\x3a\xfe\x5d\xa0\ +\x00\x00\x13\xee\x00\x00\x00\x00\x00\x01\x00\x05\xc2\x9e\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x14\x04\x00\x00\x00\x00\x00\x01\x00\x05\xc5\x9d\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x14\x1a\x00\x00\x00\x00\x00\x01\x00\x05\xcb\xa7\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x14\x4c\x00\x00\x00\x00\x00\x01\x00\x05\xce\xf6\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xd8\x77\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xde\x6e\ +\x00\x00\x01\x9a\x3a\xfe\x5d\xa0\ +\x00\x00\x14\x8e\x00\x00\x00\x00\x00\x01\x00\x05\xe0\x7f\ +\x00\x00\x01\x9a\x3a\xfe\x5d\xa0\ +\x00\x00\x14\xa6\x00\x00\x00\x00\x00\x01\x00\x05\xe4\x42\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x14\xcc\x00\x00\x00\x00\x00\x01\x00\x05\xed\xf6\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x14\xf6\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x44\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x15\x18\x00\x00\x00\x00\x00\x01\x00\x05\xf4\xd2\ +\x00\x00\x01\x9a\x3a\xfe\x5d\xa0\ +\x00\x00\x15\x3e\x00\x00\x00\x00\x00\x01\x00\x05\xf9\x73\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x15\x52\x00\x00\x00\x00\x00\x01\x00\x06\x03\x45\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x15\x7e\x00\x00\x00\x00\x00\x01\x00\x06\x08\x8f\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x15\xa6\x00\x00\x00\x00\x00\x01\x00\x06\x0e\x96\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x15\xbc\x00\x00\x00\x00\x00\x01\x00\x06\x0f\x7a\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x15\xe8\x00\x00\x00\x00\x00\x01\x00\x06\x11\xc3\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x15\xfe\x00\x00\x00\x00\x00\x01\x00\x06\x18\x73\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x16\x1a\x00\x00\x00\x00\x00\x01\x00\x06\x1b\xb7\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x16\x32\x00\x00\x00\x00\x00\x01\x00\x06\x1c\xe3\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x16\x4c\x00\x00\x00\x00\x00\x01\x00\x06\x22\xa4\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x16\x6e\x00\x00\x00\x00\x00\x01\x00\x06\x23\xc6\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x16\x8c\x00\x00\x00\x00\x00\x01\x00\x06\x29\xb9\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x16\xac\x00\x00\x00\x00\x00\x01\x00\x06\x2c\xbd\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x16\xce\x00\x00\x00\x00\x00\x01\x00\x06\x2d\xde\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x16\xee\x00\x00\x00\x00\x00\x01\x00\x06\x30\xb2\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x17\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x39\x1e\ \x00\x00\x01\x99\x7d\x04\xc2\x80\ -\x00\x00\x15\xfc\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x88\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x06\x00\xa3\ -\x00\x00\x01\x99\x4c\xf0\xd6\xbc\ -\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x01\xf6\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x17\x40\x00\x00\x00\x00\x00\x01\x00\x06\x40\xde\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ +\x00\x00\x17\x64\x00\x00\x00\x00\x00\x01\x00\x06\x45\xf9\ +\x00\x00\x01\x99\x4c\xf0\xd6\x60\ +\x00\x00\x17\x8c\x00\x00\x00\x00\x00\x01\x00\x06\x47\x4c\ +\x00\x00\x01\x98\xe1\xb8\x61\xb0\ " qt_version = [int(v) for v in QtCore.qVersion().split('.')] From 635cfd3d3758ac7184ced62035605137704703b8 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Fri, 28 Nov 2025 15:44:45 +0000 Subject: [PATCH 04/26] networkWindow.py: refactor to include listview wifiConnectivityWindow.ui: change horizontalLayout to a vertical layout with a listview and a vertical scrollbar wifiConnectivityWindow.py: generated file from QtDesigner with some optimizations Signed-off-by: Guilherme Costa --- BlocksScreen/lib/panels/networkWindow.py | 191 +++++------------- BlocksScreen/lib/ui/wifiConnectivityWindow.ui | 65 +++++- .../lib/ui/wifiConnectivityWindow_ui.py | 142 +++++++++---- 3 files changed, 218 insertions(+), 180 deletions(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index d6b70c13..d7803955 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -1,32 +1,24 @@ +import copy import logging -import typing -import copy import subprocess +import typing from functools import partial from PyQt6 import QtCore, QtGui, QtWidgets -from PyQt6.QtCore import QRunnable, QThreadPool, QObject, pyqtSignal, QVariant -from PyQt6.QtWidgets import QScroller, QScrollerProperties +from PyQt6.QtCore import QRunnable, QThreadPool, QObject, pyqtSignal from lib.network import SdbusNetworkManagerAsync from lib.panels.widgets.popupDialogWidget import Popup from lib.ui.wifiConnectivityWindow_ui import Ui_wifi_stacked_page from lib.panels.widgets.keyboardPage import CustomQwertyKeyboard -from lib.utils.blocks_frame import BlocksCustomFrame from lib.panels.widgets.loadPage import LoadScreen from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem logger = logging.getLogger("logs/BlocksScreen.log") -<<<<<<< HEAD - -class BuildNetworkList(QtCore.QThread): - """Retrieves information from sdbus interface about scanned networks""" -======= class NetworkScanRunnable(QRunnable): """QRunnable task that performs network scanning using SdbusNetworkManagerAsync ->>>>>>> 9285fb7 (Refactor: Refac to MVC view with Controller being runnables on a threadpoll) This runnable: - Triggers a network rescan via SdbusNetworkManagerAsync @@ -222,9 +214,6 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: self.panel = Ui_wifi_stacked_page() self.panel.setupUi(self) - - self._setupUI() - self._provider = WifiIconProvider() self.ongoing_update: bool = False @@ -243,16 +232,14 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: #View Models and Delegates self.model = EntryListModel() - self.model.setParent(self.network_list_widget) + self.model.setParent(self.panel.listView) self.entry_delegate = EntryDelegate() - self.network_list_widget.setModel(self.model) - self.network_list_widget.setItemDelegate(self.entry_delegate) + self.panel.listView.setModel(self.model) + self.panel.listView.setItemDelegate(self.entry_delegate) self.entry_delegate.item_selected.connect(self.ssid_item_clicked) - self.panel.network_backButton.clicked.connect(self.reset_view_model) # Network Scan self.build_network_list() - self.network_list_worker = BuildNetworkList() self.network_list_worker.finished_network_list_build.connect( self.handle_network_list @@ -413,8 +400,15 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: self.network_list_worker.build() self.request_network_scan.emit() + self.panel.listView.verticalScrollBar().valueChanged.connect( + self._handle_scrollbar + ) + self.panel.verticalScrollBar.valueChanged.connect(self._handle_scrollbar) + self.panel.verticalScrollBar.valueChanged.connect( + lambda value: self.panel.listView.verticalScrollBar().setValue(value) + ) + self.panel.verticalScrollBar.show() self.hide() - self.info_box_load() self.qwerty = CustomQwertyKeyboard(self) @@ -477,20 +471,28 @@ def deleteLater(self) -> None: def showEvent(self, event: QtGui.QShowEvent | None) -> None: """Re-add clients to update list""" + # Block all touch events so multitouch is ignored + if event.type() in ( + QtCore.QEvent.Type.TouchBegin, + QtCore.QEvent.Type.TouchUpdate, + QtCore.QEvent.Type.TouchEnd + ): + return True # ignore the event entirely + self.build_model_list() return super().showEvent(event) def build_model_list(self) -> None: """Builds the model list (`self.model`) containing updatable clients""" - self.network_list_widget.blockSignals(True) - self.model.clear() - - test:dict = copy.copy(self.saved_network) - if test.items(): - for ssid,(signal,is_saved) in test.items(): + self.panel.listView.blockSignals(True) + self.reset_view_model() + saved_networks:dict = copy.copy(self.saved_network) + if saved_networks.items(): + for ssid,(signal,is_saved) in saved_networks.items(): self.add_network_entry(ssid=ssid, signal=signal, is_saved=is_saved) - - self.network_list_widget.blockSignals(False) + self._setup_scrollbar() + + self.panel.listView.blockSignals(False) def saved_wifi_option_selected(self): """Handle connect/delete network button clicks""" @@ -739,6 +741,7 @@ def evaluate_network_state(self, nm_state: str = "") -> None: self.panel.hotspot_button.setEnabled(True) self.repaint() + if ( wifi_btn.state == wifi_btn.State.OFF and hotspot_btn.state == hotspot_btn.State.OFF @@ -905,15 +908,9 @@ def add_network(self) -> None: self.panel.add_network_validation_button.setEnabled(True) self.panel.add_network_validation_button.repaint() self.popup.new_message(message_type=Popup.MessageType.ERROR, message=message) -<<<<<<< HEAD - - @QtCore.pyqtSlot(QtWidgets.QListWidgetItem, name="ssid_item_clicked") - def ssid_item_clicked(self, item: QtWidgets.QListWidgetItem) -> None: -======= @QtCore.pyqtSlot(ListItem, name="ssid_item_clicked") def ssid_item_clicked(self, item: ListItem) -> None: ->>>>>>> da41c34 (refactor: change network list to listview) """Handles when a network is clicked on the QListWidget. Args: @@ -923,7 +920,6 @@ def ssid_item_clicked(self, item: ListItem) -> None: return _current_ssid_name = item.text - #_current_ssid_name = self.saved_network.get(item.text, {}) self.selected_item = copy.copy(item) if ( _current_ssid_name in self.sdbus_network.get_saved_ssid_names() @@ -964,7 +960,6 @@ def handle_network_list(self, data: typing.List[typing.Tuple]) -> None: scroll_bar_position = self.network_list_widget.verticalScrollBar().value() ======= def handle_network_list(self, data: typing.Dict) -> None: ->>>>>>> da41c34 (refactor: change network list to listview) self.network_list_widget.blockSignals(True) for entry in data: if entry[0] == self.sdbus_network.hotspot_ssid: @@ -975,8 +970,6 @@ def handle_network_list(self, data: typing.Dict) -> None: continue self.saved_network[entry[0]] = (entry[1], entry[2]) self.build_model_list() - self.network_list_widget.blockSignals(False) - self.evaluate_network_state() QtCore.QTimer().singleShot(10000, lambda: self.network_list_worker.build()) @@ -990,7 +983,7 @@ def handle_button_click(self, ssid: str): self.panel.network_activate_btn.show() else: self.panel.network_activate_btn.hide() - self.panel.frame.repaint() + #self.panel.frame.repaint() else: self.setCurrentIndex(self.indexOf(self.panel.add_network_page)) @@ -1070,7 +1063,7 @@ def add_network_entry(self, ssid: str, signal: int, is_saved:str) -> None: """Adds a new item to the list model""" wifi_pixmap = self._provider.get_pixmap(signal=signal, state=is_saved) - + ssid = ssid if ssid is not "" else "UNKOWN" item = ListItem( text=ssid, left_icon=wifi_pixmap, @@ -1078,59 +1071,27 @@ def add_network_entry(self, ssid: str, signal: int, is_saved:str) -> None: selected=False, allow_check=False, _lfontsize=17, - _rfontsize=13, - height=70, + _rfontsize=12, + height=80, ) self.model.add_item(item) + + def _handle_scrollbar(self, value): + # Block signals to avoid recursion + self.panel.verticalScrollBar.blockSignals(True) + self.panel.verticalScrollBar.setValue(value) + self.panel.verticalScrollBar.blockSignals(False) - def _setupUI(self) -> None: - - """Sets up the UI components and layout for the network window.""" - - font_id = QtGui.QFontDatabase.addApplicationFont( - ":/font/media/fonts for text/Momcake-Bold.ttf" + def _setup_scrollbar(self) -> None: + self.panel.verticalScrollBar.setMinimum( + self.panel.listView.verticalScrollBar().minimum() ) - font_family = QtGui.QFontDatabase.applicationFontFamilies(font_id)[0] - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.MinimumExpanding, - QtWidgets.QSizePolicy.Policy.MinimumExpanding, + self.panel.verticalScrollBar.setMaximum( + self.panel.listView.verticalScrollBar().maximum() + ) + self.panel.verticalScrollBar.setPageStep( + self.panel.listView.verticalScrollBar().pageStep() ) - sizePolicy.setHorizontalStretch(1) - #sizePolicy.setVerticalStretch(1) - self.setSizePolicy(sizePolicy) - self.setMinimumSize(QtCore.QSize(800, 500)) - #self.setMaximumSize(QtCore.QSize(800, 500)) - self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.main_content_layout = QtWidgets.QHBoxLayout() - self.main_content_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) - - font = QtGui.QFont() - font.setFamily(font_family) - font.setPointSize(24) - - self.header_title = QtWidgets.QLabel(self) - self.header_title.setMinimumSize(QtCore.QSize(100, 60)) - self.header_title.setMaximumSize(QtCore.QSize(16777215, 60)) - self.header_title.setFont(font) - - # Timer for loading screen timeout - self._load_timer = QtCore.QTimer(self) - - #Buttons frame for update buttons - self.network_buttons_frame = BlocksCustomFrame() - #self.network_buttons_frame.setMinimumSize(QtCore.QSize(100, 100)) - #self.network_buttons_frame.setMaximumSize(QtCore.QSize(800, 450)) - - #List widget for update buttons - self.network_list_widget = QtWidgets.QListView(self.network_buttons_frame) - - self.network_buttons_layout = QtWidgets.QVBoxLayout() - self.network_buttons_layout.setContentsMargins(15, 20, 20, 5) - self.network_buttons_layout.addWidget(self.network_list_widget, 0, QtCore.Qt.AlignmentFlag.AlignBottom) - self.network_buttons_frame.setLayout(self.network_buttons_layout) - - self.main_content_layout.addWidget(self.network_buttons_frame, 0) - self.setLayout(self.main_content_layout) def build_network_list(self) -> None: """Build available/saved network list with optimized palette setup.""" @@ -1159,56 +1120,4 @@ def set_brush_for_all_groups(palette, role, color, style=QtCore.Qt.BrushStyle.So set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Link, (0, 0, 255, 0)) # Apply palette - self.network_list_widget.setPalette(palette) - - # General QListView setup - self.network_list_widget.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.network_list_widget.setStyleSheet("background-color:transparent") - self.network_list_widget.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.network_list_widget.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.network_list_widget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded) - self.network_list_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - self.network_list_widget.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustIgnored) - self.network_list_widget.setAutoScroll(False) - self.network_list_widget.setProperty("showDropIndicator", False) - self.network_list_widget.setDefaultDropAction(QtCore.Qt.DropAction.IgnoreAction) - self.network_list_widget.setAlternatingRowColors(False) - self.network_list_widget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.NoSelection) - self.network_list_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems) - self.network_list_widget.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) - self.network_list_widget.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) - self.network_list_widget.setUniformItemSizes(True) - self.network_list_widget.setSpacing(3) - - viewport = self.network_list_widget.viewport() - QScroller.grabGesture(viewport, QScroller.ScrollerGestureType.TouchGesture) - QScroller.grabGesture(viewport, QScroller.ScrollerGestureType.LeftMouseButtonGesture) - - scroller = QScroller.scroller(viewport) - props = scroller.scrollerProperties() - - props.setScrollMetric( - QScrollerProperties.ScrollMetric.VerticalOvershootPolicy, - QVariant(QScrollerProperties.OvershootPolicy.OvershootAlwaysOff) - ) - props.setScrollMetric( - QScrollerProperties.ScrollMetric.OvershootDragResistanceFactor, - QVariant(1.0) - ) - props.setScrollMetric( - QScrollerProperties.ScrollMetric.OvershootDragDistanceFactor, - QVariant(0.0) - ) - props.setScrollMetric( - QScrollerProperties.ScrollMetric.OvershootScrollDistanceFactor, - QVariant(0.0) - ) - props.setScrollMetric( - QScrollerProperties.ScrollMetric.OvershootScrollTime, - QVariant(0.0) - ) - - scroller.setScrollerProperties(props) - - self.network_list_widget.setObjectName("network_list_widget") - self.panel.nl_content_layout.addWidget(self.network_list_widget) + self.panel.listView.setPalette(palette) \ No newline at end of file diff --git a/BlocksScreen/lib/ui/wifiConnectivityWindow.ui b/BlocksScreen/lib/ui/wifiConnectivityWindow.ui index 289cbe08..f069ea17 100644 --- a/BlocksScreen/lib/ui/wifiConnectivityWindow.ui +++ b/BlocksScreen/lib/ui/wifiConnectivityWindow.ui @@ -6,7 +6,7 @@ 0 0 - 800 + 852 480 @@ -40,7 +40,7 @@ - 2 + 1 @@ -697,7 +697,61 @@ using the buttons on the side. - + + + + + + 1 + 1 + + + + + 0 + 0 + + + + BlankCursor + + + background-color: rgba(255, 255, 255, 0); + + + QFrame::NoFrame + + + QFrame::Plain + + + Qt::ScrollBarAlwaysOff + + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + + + + + + 0 + 0 + + + + Qt::Vertical + + + + @@ -2754,6 +2808,11 @@ Type QLabel
lib.panels.widgets.loadWidget
+ + CustomScrollBar + QScrollBar +
lib.utils.blocks_Scrollbar
+
diff --git a/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py b/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py index fccaf1ae..a5316106 100644 --- a/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py +++ b/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py @@ -1,4 +1,4 @@ -# Form implementation generated from reading ui file '/home/levi/main/BlocksScreen/BlocksScreen/lib/ui/wifiConnectivityWindow.ui' +# Form implementation generated from reading ui file '~/home/main/BlocksScreen/BlocksScreen/lib/ui/wifiConnectivityWindow.ui' # # Created by: PyQt6 UI code generator 6.7.1 # @@ -300,14 +300,83 @@ def setupUi(self, wifi_stacked_page): self.nl_back_button.setObjectName("nl_back_button") self.nl_header_layout.addWidget(self.nl_back_button) self.verticalLayout_9.addLayout(self.nl_header_layout) - self.nl_content_layout = QtWidgets.QVBoxLayout() - self.nl_content_layout.setObjectName("nl_content_layout") - self.verticalLayout_9.addLayout(self.nl_content_layout) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.listView = QtWidgets.QListView(self.network_list_page) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) + sizePolicy.setHorizontalStretch(1) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.listView.sizePolicy().hasHeightForWidth()) + self.listView.setSizePolicy(sizePolicy) + self.listView.setMinimumSize(QtCore.QSize(0, 0)) + self.listView.setStyleSheet("background-color: rgba(255, 255, 255, 0);") + self.listView.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.listView.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) + self.listView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.listView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.listView.setSelectionBehavior( + QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems + ) + self.listView.setHorizontalScrollBarPolicy( # No horizontal scroll + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.listView.setVerticalScrollMode( + QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel + ) + self.listView.setVerticalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + QtWidgets.QScroller.grabGesture( + self.listView, + QtWidgets.QScroller.ScrollerGestureType.TouchGesture, + ) + QtWidgets.QScroller.grabGesture( + self.listView, + QtWidgets.QScroller.ScrollerGestureType.LeftMouseButtonGesture, + ) + self.listView.setEditTriggers( + QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers + ) + + scroller_instance = QtWidgets.QScroller.scroller(self.listView) + scroller_props = scroller_instance.scrollerProperties() + scroller_props.setScrollMetric( + QtWidgets.QScrollerProperties.ScrollMetric.DragVelocitySmoothingFactor, + 0.05, # Lower = more responsive + ) + scroller_props.setScrollMetric( + QtWidgets.QScrollerProperties.ScrollMetric.DecelerationFactor, + 0.4, # higher = less inertia + ) + QtWidgets.QScroller.scroller(self.listView).setScrollerProperties( + scroller_props + ) + self.verticalScrollBar = CustomScrollBar(parent=self.network_list_page) + self.listView.setVerticalScrollBar(self.verticalScrollBar) + self.listView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.horizontalLayout_2.addWidget(self.listView) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.verticalScrollBar.sizePolicy().hasHeightForWidth()) + self.verticalScrollBar.setSizePolicy(sizePolicy) + self.verticalScrollBar.setOrientation(QtCore.Qt.Orientation.Vertical) + self.verticalScrollBar.setObjectName("verticalScrollBar") + self.listView.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) + self.listView.setUniformItemSizes(True) + #self.listView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.listView.setSpacing(5) + self.horizontalLayout_2.addWidget(self.verticalScrollBar) + self.verticalLayout_9.addLayout(self.horizontalLayout_2) wifi_stacked_page.addWidget(self.network_list_page) self.add_network_page = QtWidgets.QWidget() self.add_network_page.setObjectName("add_network_page") self.verticalLayout_10 = QtWidgets.QVBoxLayout(self.add_network_page) self.verticalLayout_10.setObjectName("verticalLayout_10") + self.verticalScrollBar.setAttribute( + QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents, True + ) + self.scroller = QtWidgets.QScroller.scroller(self.listView) self.add_np_header_layout = QtWidgets.QHBoxLayout() self.add_np_header_layout.setObjectName("add_np_header_layout") spacerItem1 = QtWidgets.QSpacerItem(40, 60, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) @@ -1017,70 +1086,71 @@ def setupUi(self, wifi_stacked_page): wifi_stacked_page.addWidget(self.hotspot_page) self.retranslateUi(wifi_stacked_page) - wifi_stacked_page.setCurrentIndex(2) + wifi_stacked_page.setCurrentIndex(1) QtCore.QMetaObject.connectSlotsByName(wifi_stacked_page) def retranslateUi(self, wifi_stacked_page): _translate = QtCore.QCoreApplication.translate wifi_stacked_page.setWindowTitle(_translate("wifi_stacked_page", "StackedWidget")) - self.network_main_title.setText(_translate("wifi_stacked_page", "Networks")) - self.netlist_strength_label.setText(_translate("wifi_stacked_page", "Signal\n" -"Strength")) - self.netlist_strength.setText(_translate("wifi_stacked_page", "TextLabel")) - self.netlist_security_label.setText(_translate("wifi_stacked_page", "Security\n" -"Type")) - self.netlist_security.setText(_translate("wifi_stacked_page", "TextLabel")) - self.mn_info_box.setText(_translate("wifi_stacked_page", "No network connection.\n" + self.network_main_title.setText("Networks") + self.netlist_strength_label.setText( "Signal\n" +"Strength") + self.netlist_strength.setText( "TextLabel") + self.netlist_security_label.setText( "Security\n" +"Type") + self.netlist_security.setText("TextLabel") + 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.")) - self.wifi_button.setText(_translate("wifi_stacked_page", "Wi-Fi")) - self.hotspot_button.setText(_translate("wifi_stacked_page", "Hotspot")) - self.rescan_button.setText(_translate("wifi_stacked_page", "Reload")) +"using the buttons on the side.") + self.wifi_button.setText( "Wi-Fi") + self.hotspot_button.setText( "Hotspot") + self.rescan_button.setText( "Reload") self.rescan_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.network_list_title.setText(_translate("wifi_stacked_page", "Wi-Fi List")) - self.nl_back_button.setText(_translate("wifi_stacked_page", "Back")) + self.network_list_title.setText("Wi-Fi List") + self.nl_back_button.setText("Back") self.nl_back_button.setProperty("class", _translate("wifi_stacked_page", "back_btn")) self.nl_back_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.add_network_network_label.setText(_translate("wifi_stacked_page", "TextLabel")) - self.add_network_page_backButton.setText(_translate("wifi_stacked_page", "Back")) + self.add_network_network_label.setText("TextLabel") + self.add_network_page_backButton.setText("Back") self.add_network_page_backButton.setProperty("class", _translate("wifi_stacked_page", "back_btn")) self.add_network_page_backButton.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.add_network_password_label.setText(_translate("wifi_stacked_page", "Password")) - self.add_network_password_view.setText(_translate("wifi_stacked_page", "View")) + self.add_network_password_label.setText("Password") + self.add_network_password_view.setText("View") self.add_network_password_view.setProperty("class", _translate("wifi_stacked_page", "back_btn")) self.add_network_password_view.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.add_network_validation_button.setText(_translate("wifi_stacked_page", "Activate")) - self.saved_connection_delete_network_button.setText(_translate("wifi_stacked_page", "Delete")) + self.add_network_validation_button.setText("Activate") + self.saved_connection_delete_network_button.setText("Delete") self.saved_connection_delete_network_button.setProperty("class", _translate("wifi_stacked_page", "back_btn")) self.saved_connection_delete_network_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.saved_connection_network_name.setText(_translate("wifi_stacked_page", "SSID")) - self.saved_connection_back_button.setText(_translate("wifi_stacked_page", "Back")) + self.saved_connection_network_name.setText("SSID") + self.saved_connection_back_button.setText("Back") self.saved_connection_back_button.setProperty("class", _translate("wifi_stacked_page", "back_btn")) self.saved_connection_back_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) self.saved_connection_change_password_label_2.setText(_translate("wifi_stacked_page", "Change\n" "Password")) - self.saved_connection_change_password_view.setText(_translate("wifi_stacked_page", "View")) + self.saved_connection_change_password_view.setText("View") self.saved_connection_change_password_view.setProperty("class", _translate("wifi_stacked_page", "back_btn")) self.saved_connection_change_password_view.setProperty("button_type", _translate("wifi_stacked_page", "icon")) self.sabed_connection_signal_strength_label.setText(_translate("wifi_stacked_page", "Signal\n" "Strength")) - self.saved_connection_signal_strength_info_frame.setText(_translate("wifi_stacked_page", "TextLabel")) + self.saved_connection_signal_strength_info_frame.setText("TextLabel") self.saved_connection_security_type_label.setText(_translate("wifi_stacked_page", "Security\n" "Type")) - self.saved_connection_security_type_info_label.setText(_translate("wifi_stacked_page", "TextLabel")) - self.hotspot_header_title.setText(_translate("wifi_stacked_page", "Hotspot")) - self.hotspot_back_button.setText(_translate("wifi_stacked_page", "Back")) + self.saved_connection_security_type_info_label.setText("TextLabel") + self.hotspot_header_title.setText("Hotspot") + self.hotspot_back_button.setText("Back") self.hotspot_back_button.setProperty("class", _translate("wifi_stacked_page", "back_btn")) self.hotspot_back_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.hotspot_info_name_label.setText(_translate("wifi_stacked_page", "Hotspot Name: ")) - self.hotspot_info_password_label.setText(_translate("wifi_stacked_page", "Hotspot Password:")) - self.hotspot_password_view_button.setText(_translate("wifi_stacked_page", "View")) + self.hotspot_info_name_label.setText("Hotspot Name: ") + self.hotspot_info_password_label.setText("Hotspot Password:") + self.hotspot_password_view_button.setText("View") self.hotspot_password_view_button.setProperty("class", _translate("wifi_stacked_page", "back_btn")) self.hotspot_password_view_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.hotspot_change_confirm.setText(_translate("wifi_stacked_page", "Save")) + self.hotspot_change_confirm.setText("Save") from lib.panels.widgets.loadWidget import LoadingOverlayWidget +from lib.utils.blocks_Scrollbar import CustomScrollBar from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.blocks_linedit import BlocksCustomLinEdit From 7a023f28c967eb2f09ad2a035b9604f63df5887c Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Thu, 11 Dec 2025 11:24:23 +0000 Subject: [PATCH 05/26] networkWindow: rebase merge conflits fix and cleanup Signed-off-by: Guilherme Costa --- BlocksScreen/lib/panels/networkWindow.py | 143 +++++++++++++---------- 1 file changed, 81 insertions(+), 62 deletions(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index d7803955..85ba5b58 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -17,24 +17,27 @@ logger = logging.getLogger("logs/BlocksScreen.log") + class NetworkScanRunnable(QRunnable): """QRunnable task that performs network scanning using SdbusNetworkManagerAsync - This runnable: - - Triggers a network rescan via SdbusNetworkManagerAsync - - collects SSIDs, signal strenght and saved status - - emits signal with raw scan data and a processed lisgs - - Signals: - - scan_results (dict): Emitted with raw scan results mapping SSID to properties - - finished_network_list_build (list): Emitted with processed list of networks - - error (str): Emitted if an error occurs during scanning - + This runnable: + - Triggers a network rescan via SdbusNetworkManagerAsync + - collects SSIDs, signal strenght and saved status + - emits signal with raw scan data and a processed lisgs + + Signals: + - scan_results (dict): Emitted with raw scan results mapping SSID to properties + - finished_network_list_build (list): Emitted with processed list of networks + - error (str): Emitted if an error occurs during scanning + """ class Signals(QObject): scan_results = pyqtSignal(dict, name="scan-results") - finished_network_list_build = pyqtSignal(list, name="finished-network-list-build") + finished_network_list_build = pyqtSignal( + list, name="finished-network-list-build" + ) error = pyqtSignal(str) def __init__(self): @@ -47,7 +50,11 @@ def run(self): logger.debug("NetworkScanRunnable: scanning networks") self.nm.rescan_networks() saved = self.nm.get_saved_ssid_names() - available = self.nm.get_available_networks() if self.nm.check_wifi_interface() else {} + available = ( + self.nm.get_available_networks() + if self.nm.check_wifi_interface() + else {} + ) data_dict: dict[str, dict] = {} for ssid, props in available.items(): @@ -61,9 +68,17 @@ def run(self): self.signals.scan_results.emit(data_dict) # Transform into your “list of tuples + blank / separator” format - items: list[typing.Union[tuple[str,int,str], str]] = [] - saved_nets = [ (ssid, info["signal_level"]) for ssid, info in data_dict.items() if info["is_saved"] ] - unsaved_nets = [ (ssid, info["signal_level"]) for ssid, info in data_dict.items() if not info["is_saved"] ] + items: list[typing.Union[tuple[str, int, str], str]] = [] + saved_nets = [ + (ssid, info["signal_level"]) + for ssid, info in data_dict.items() + if info["is_saved"] + ] + unsaved_nets = [ + (ssid, info["signal_level"]) + for ssid, info in data_dict.items() + if not info["is_saved"] + ] saved_nets.sort(key=lambda x: -x[1]) unsaved_nets.sort(key=lambda x: -x[1]) @@ -81,10 +96,11 @@ def run(self): logger.error("Error scanning networks", exc_info=True) self.signals.error.emit(str(e)) + class BuildNetworkList(QtCore.QObject): """ Controller class that schedules and manages repeted network scans - + Uses a QThreadPool to un NetworkScanRunnable tasks periodically. with a QTimer to trigger scans. Prevents overlapping scans by tracking whether a scan is already in progress. @@ -92,12 +108,13 @@ class BuildNetworkList(QtCore.QObject): poll_interval_ms: (int) Milliseconds between scans (default: 10000) _timer (QtCore.QTimer): Timer that schedules next scan _is_scanning (bool): Flag indicating if a scan is currently in progress - + Signals: scan_results (dict): Emitted with raw scan results mapping SSID to properties finished_network_list_build (list): Emitted with processed list of networks error (str): Emitted if an error occurs during scanning """ + scan_results = pyqtSignal(dict, name="scan-results") finished_network_list_build = pyqtSignal(list, name="finished-network-list-build") error = pyqtSignal(str) @@ -152,6 +169,7 @@ def _do_scan(self): self.threadpool.start(task) logger.debug("Submitted scan task to thread pool") + class WifiIconProvider: """Simple provider: loads QPixmap for WiFi bars + protection without caching.""" @@ -163,7 +181,6 @@ def __init__(self): (3, False): ":/network/media/btn_icons/3bar_wifi.svg", (2, False): ":/network/media/btn_icons/2bar_wifi.svg", (1, False): ":/network/media/btn_icons/1bar_wifi.svg", - ("no", True): ":/network/media/btn_icons/0bar_wifi_protected.svg", (4, True): ":/network/media/btn_icons/4bar_wifi_protected.svg", (3, True): ":/network/media/btn_icons/3bar_wifi_protected.svg", @@ -171,7 +188,6 @@ def __init__(self): (1, True): ":/network/media/btn_icons/1bar_wifi_protected.svg", } - def get_pixmap(self, signal: int, state: str) -> QtGui.QPixmap: """Return a QPixmap for the given signal (0-100) and state ("Protected" or not).""" # Normalize signal @@ -186,18 +202,22 @@ def get_pixmap(self, signal: int, state: str) -> QtGui.QPixmap: else: bars = 1 - is_protected = (state == "Protected") + is_protected = state == "Protected" key = (bars, is_protected) path = self.paths.get(key) if path is None: - logger.warning(f"No icon path for key {key}, falling back to no-signal unprotected") + logger.warning( + f"No icon path for key {key}, falling back to no-signal unprotected" + ) path = self.paths[("no", False)] pm = QtGui.QPixmap(path) if pm.isNull(): logger.error(f"Failed to load pixmap from '{path}' for key {key}") return pm + + class NetworkControlWindow(QtWidgets.QStackedWidget): """Network Control panel Widget""" @@ -211,17 +231,17 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: super().__init__(parent) else: super().__init__() - + self.panel = Ui_wifi_stacked_page() self.panel.setupUi(self) self._provider = WifiIconProvider() self.ongoing_update: bool = False - + self.popup = Popup(self) self.sdbus_network = SdbusNetworkManagerAsync() self.start: bool = True self.saved_network = {} - + self.load_popup: LoadScreen = LoadScreen(self) self.repeated_request_status = QtCore.QTimer() self.repeated_request_status.setInterval(2000) # every 2 seconds @@ -229,8 +249,8 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: self._load_timer = QtCore.QTimer() self._load_timer.setSingleShot(True) self._load_timer.timeout.connect(self._handle_load_timeout) - - #View Models and Delegates + + # View Models and Delegates self.model = EntryListModel() self.model.setParent(self.panel.listView) self.entry_delegate = EntryDelegate() @@ -455,8 +475,7 @@ def handle_ongoing_update(self) -> None: self.load_popup.set_status_message("Updating...") self.load_popup.show() self.repeated_request_status.start(2000) - - # View Model Methods + def reset_view_model(self) -> None: """Clears items from ListView (Resets `QAbstractListModel` by clearing entries) @@ -475,10 +494,10 @@ def showEvent(self, event: QtGui.QShowEvent | None) -> None: if event.type() in ( QtCore.QEvent.Type.TouchBegin, QtCore.QEvent.Type.TouchUpdate, - QtCore.QEvent.Type.TouchEnd + QtCore.QEvent.Type.TouchEnd, ): return True # ignore the event entirely - + self.build_model_list() return super().showEvent(event) @@ -486,14 +505,14 @@ def build_model_list(self) -> None: """Builds the model list (`self.model`) containing updatable clients""" self.panel.listView.blockSignals(True) self.reset_view_model() - saved_networks:dict = copy.copy(self.saved_network) + saved_networks: dict = copy.copy(self.saved_network) if saved_networks.items(): - for ssid,(signal,is_saved) in saved_networks.items(): + for ssid, (signal, is_saved) in saved_networks.items(): self.add_network_entry(ssid=ssid, signal=signal, is_saved=is_saved) self._setup_scrollbar() self.panel.listView.blockSignals(False) - + def saved_wifi_option_selected(self): """Handle connect/delete network button clicks""" _sender = self.sender() @@ -741,7 +760,6 @@ def evaluate_network_state(self, nm_state: str = "") -> None: self.panel.hotspot_button.setEnabled(True) self.repaint() - if ( wifi_btn.state == wifi_btn.State.OFF and hotspot_btn.state == hotspot_btn.State.OFF @@ -908,7 +926,7 @@ def add_network(self) -> None: self.panel.add_network_validation_button.setEnabled(True) self.panel.add_network_validation_button.repaint() self.popup.new_message(message_type=Popup.MessageType.ERROR, message=message) - + @QtCore.pyqtSlot(ListItem, name="ssid_item_clicked") def ssid_item_clicked(self, item: ListItem) -> None: """Handles when a network is clicked on the QListWidget. @@ -917,24 +935,21 @@ def ssid_item_clicked(self, item: ListItem) -> None: item (QListWidgetItem): The list entry that was clicked """ if not item: - return - + return + _current_ssid_name = item.text self.selected_item = copy.copy(item) if ( _current_ssid_name in self.sdbus_network.get_saved_ssid_names() ): # Network already saved go to the information page self.setCurrentIndex(self.indexOf(self.panel.saved_connection_page)) - self.panel.saved_connection_network_name.setText( - str(_current_ssid_name) - ) + self.panel.saved_connection_network_name.setText(str(_current_ssid_name)) else: # Network not saved go to the add network page self.setCurrentIndex(self.indexOf(self.panel.add_network_page)) self.panel.add_network_network_label.setText( str(_current_ssid_name) ) # Add the network name to the title - def update_network( self, ssid: str, @@ -954,13 +969,8 @@ def update_network( self.setCurrentIndex(self.indexOf(self.panel.network_list_page)) @QtCore.pyqtSlot(list, name="finished-network-list-build") -<<<<<<< HEAD def handle_network_list(self, data: typing.List[typing.Tuple]) -> None: """Handle available network list update""" - scroll_bar_position = self.network_list_widget.verticalScrollBar().value() -======= - def handle_network_list(self, data: typing.Dict) -> None: - self.network_list_widget.blockSignals(True) for entry in data: if entry[0] == self.sdbus_network.hotspot_ssid: continue @@ -983,7 +993,6 @@ def handle_button_click(self, ssid: str): self.panel.network_activate_btn.show() else: self.panel.network_activate_btn.hide() - #self.panel.frame.repaint() else: self.setCurrentIndex(self.indexOf(self.panel.add_network_page)) @@ -1058,12 +1067,12 @@ def show_network_panel( self.updateGeometry() self.update() self.show() - - def add_network_entry(self, ssid: str, signal: int, is_saved:str) -> None: + + def add_network_entry(self, ssid: str, signal: int, is_saved: str) -> None: """Adds a new item to the list model""" - + wifi_pixmap = self._provider.get_pixmap(signal=signal, state=is_saved) - ssid = ssid if ssid is not "" else "UNKOWN" + ssid = ssid if ssid != "" else "UNKOWN" item = ListItem( text=ssid, left_icon=wifi_pixmap, @@ -1075,13 +1084,13 @@ def add_network_entry(self, ssid: str, signal: int, is_saved:str) -> None: height=80, ) self.model.add_item(item) - + def _handle_scrollbar(self, value): # Block signals to avoid recursion self.panel.verticalScrollBar.blockSignals(True) self.panel.verticalScrollBar.setValue(value) self.panel.verticalScrollBar.blockSignals(False) - + def _setup_scrollbar(self) -> None: self.panel.verticalScrollBar.setMinimum( self.panel.listView.verticalScrollBar().minimum() @@ -1092,10 +1101,13 @@ def _setup_scrollbar(self) -> None: self.panel.verticalScrollBar.setPageStep( self.panel.listView.verticalScrollBar().pageStep() ) - + def build_network_list(self) -> None: """Build available/saved network list with optimized palette setup.""" - def set_brush_for_all_groups(palette, role, color, style=QtCore.Qt.BrushStyle.SolidPattern): + + def set_brush_for_all_groups( + palette, role, color, style=QtCore.Qt.BrushStyle.SolidPattern + ): """Helper to set a brush for Active, Inactive, and Disabled states.""" brush = QtGui.QBrush(QtGui.QColor(*color)) brush.setStyle(style) @@ -1107,17 +1119,24 @@ def set_brush_for_all_groups(palette, role, color, style=QtCore.Qt.BrushStyle.So palette.setBrush(group, role, brush) palette = QtGui.QPalette() - + # Transparent backgrounds set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Button, (0, 0, 0, 0)) set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Window, (0, 0, 0, 0)) - + # Base (black, no brush) - set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Base, (0, 0, 0), QtCore.Qt.BrushStyle.NoBrush) - + set_brush_for_all_groups( + palette, + QtGui.QPalette.ColorRole.Base, + (0, 0, 0), + QtCore.Qt.BrushStyle.NoBrush, + ) + # Highlight & link - set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Highlight, (0, 120, 215, 0)) + set_brush_for_all_groups( + palette, QtGui.QPalette.ColorRole.Highlight, (0, 120, 215, 0) + ) set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Link, (0, 0, 255, 0)) - + # Apply palette - self.panel.listView.setPalette(palette) \ No newline at end of file + self.panel.listView.setPalette(palette) From 01d42a4103b203213b7f76511cba6279cd1cbe09 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Fri, 12 Dec 2025 13:07:36 +0000 Subject: [PATCH 06/26] networkWindow.py: added missing right icon Signed-off-by: Guilherme Costa --- BlocksScreen/lib/panels/networkWindow.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 85ba5b58..303784d4 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -281,6 +281,7 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: self.panel.wifi_button.setPixmap( QtGui.QPixmap(":/network/media/btn_icons/wifi_config.svg") ) + self.right_icon = QtGui.QPixmap(":/arrow_icons/media/btn_icons/right_arrow.svg") self.panel.nl_back_button.clicked.connect( partial( @@ -1077,6 +1078,7 @@ def add_network_entry(self, ssid: str, signal: int, is_saved: str) -> None: text=ssid, left_icon=wifi_pixmap, right_text=is_saved, + right_icon=self.right_icon, selected=False, allow_check=False, _lfontsize=17, @@ -1120,23 +1122,17 @@ def set_brush_for_all_groups( palette = QtGui.QPalette() - # Transparent backgrounds set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Button, (0, 0, 0, 0)) set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Window, (0, 0, 0, 0)) - # Base (black, no brush) set_brush_for_all_groups( palette, QtGui.QPalette.ColorRole.Base, (0, 0, 0), QtCore.Qt.BrushStyle.NoBrush, ) - - # Highlight & link set_brush_for_all_groups( palette, QtGui.QPalette.ColorRole.Highlight, (0, 120, 215, 0) ) set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Link, (0, 0, 255, 0)) - - # Apply palette self.panel.listView.setPalette(palette) From cc61cfd2f1fe227351fe2b2edb61a9cc3025214c Mon Sep 17 00:00:00 2001 From: HugoCLSC Date: Mon, 15 Dec 2025 16:44:40 +0000 Subject: [PATCH 07/26] Fix typo --- BlocksScreen/lib/panels/networkWindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 303784d4..5f227aee 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -1073,7 +1073,7 @@ def add_network_entry(self, ssid: str, signal: int, is_saved: str) -> None: """Adds a new item to the list model""" wifi_pixmap = self._provider.get_pixmap(signal=signal, state=is_saved) - ssid = ssid if ssid != "" else "UNKOWN" + ssid = ssid if ssid != "" else "UNKNOWN" item = ListItem( text=ssid, left_icon=wifi_pixmap, From 44e74d4030752b91810bd5aaad6fa9bb6a3694c7 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Tue, 16 Dec 2025 11:16:00 +0000 Subject: [PATCH 08/26] networkWindow.py: optimize and bugfix on self.paths Signed-off-by: Guilherme Costa --- BlocksScreen/lib/panels/networkWindow.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 5f227aee..0ee8d715 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -176,12 +176,12 @@ class WifiIconProvider: def __init__(self): # Map from (bars, is_protected) to resource path self.paths = { - ("no", False): ":/network/media/btn_icons/0bar_wifi.svg", + (0, False): ":/network/media/btn_icons/0bar_wifi.svg", (4, False): ":/network/media/btn_icons/4bar_wifi.svg", (3, False): ":/network/media/btn_icons/3bar_wifi.svg", (2, False): ":/network/media/btn_icons/2bar_wifi.svg", (1, False): ":/network/media/btn_icons/1bar_wifi.svg", - ("no", True): ":/network/media/btn_icons/0bar_wifi_protected.svg", + (0, True): ":/network/media/btn_icons/0bar_wifi_protected.svg", (4, True): ":/network/media/btn_icons/4bar_wifi_protected.svg", (3, True): ":/network/media/btn_icons/3bar_wifi_protected.svg", (2, True): ":/network/media/btn_icons/2bar_wifi_protected.svg", @@ -191,13 +191,13 @@ def __init__(self): def get_pixmap(self, signal: int, state: str) -> QtGui.QPixmap: """Return a QPixmap for the given signal (0-100) and state ("Protected" or not).""" # Normalize signal - if signal <= 0: - bars = "no" + if signal <= 25: + bars = 0 elif signal >= 75: bars = 4 elif signal >= 50: bars = 3 - elif signal >= 25: + elif signal > 25: bars = 2 else: bars = 1 @@ -207,14 +207,14 @@ def get_pixmap(self, signal: int, state: str) -> QtGui.QPixmap: path = self.paths.get(key) if path is None: - logger.warning( + logger.debug( f"No icon path for key {key}, falling back to no-signal unprotected" ) - path = self.paths[("no", False)] + path = self.paths[(0, False)] pm = QtGui.QPixmap(path) if pm.isNull(): - logger.error(f"Failed to load pixmap from '{path}' for key {key}") + logger.debug(f"Failed to load pixmap from '{path}' for key {key}") return pm From 13f91d7370d3a2f0180aca1c1c99a3b325ae63d0 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Tue, 16 Dec 2025 14:27:18 +0000 Subject: [PATCH 09/26] networkWindow.py: comments cleanup --- BlocksScreen/lib/panels/networkWindow.py | 28 +++++++----------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 0ee8d715..75271327 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -64,10 +64,8 @@ def run(self): "is_saved": ssid in saved, } - # Emit scan_result (same name) self.signals.scan_results.emit(data_dict) - # Transform into your “list of tuples + blank / separator” format items: list[typing.Union[tuple[str, int, str], str]] = [] saved_nets = [ (ssid, info["signal_level"]) @@ -82,7 +80,6 @@ def run(self): saved_nets.sort(key=lambda x: -x[1]) unsaved_nets.sort(key=lambda x: -x[1]) - # Build your list with statuses for ssid, sig in saved_nets: status = "Active" if ssid == self.nm.get_current_ssid() else "Saved" items.append((ssid, sig, status)) @@ -174,7 +171,6 @@ class WifiIconProvider: """Simple provider: loads QPixmap for WiFi bars + protection without caching.""" def __init__(self): - # Map from (bars, is_protected) to resource path self.paths = { (0, False): ":/network/media/btn_icons/0bar_wifi.svg", (4, False): ":/network/media/btn_icons/4bar_wifi.svg", @@ -190,7 +186,6 @@ def __init__(self): def get_pixmap(self, signal: int, state: str) -> QtGui.QPixmap: """Return a QPixmap for the given signal (0-100) and state ("Protected" or not).""" - # Normalize signal if signal <= 25: bars = 0 elif signal >= 75: @@ -244,13 +239,12 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: self.load_popup: LoadScreen = LoadScreen(self) self.repeated_request_status = QtCore.QTimer() - self.repeated_request_status.setInterval(2000) # every 2 seconds + self.repeated_request_status.setInterval(2000) self._load_timer = QtCore.QTimer() self._load_timer.setSingleShot(True) self._load_timer.timeout.connect(self._handle_load_timeout) - # View Models and Delegates self.model = EntryListModel() self.model.setParent(self.panel.listView) self.entry_delegate = EntryDelegate() @@ -258,7 +252,6 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: self.panel.listView.setItemDelegate(self.entry_delegate) self.entry_delegate.item_selected.connect(self.ssid_item_clicked) - # Network Scan self.build_network_list() self.network_list_worker = BuildNetworkList() self.network_list_worker.finished_network_list_build.connect( @@ -491,13 +484,12 @@ def deleteLater(self) -> None: def showEvent(self, event: QtGui.QShowEvent | None) -> None: """Re-add clients to update list""" - # Block all touch events so multitouch is ignored if event.type() in ( QtCore.QEvent.Type.TouchBegin, QtCore.QEvent.Type.TouchUpdate, QtCore.QEvent.Type.TouchEnd, ): - return True # ignore the event entirely + return True self.build_model_list() return super().showEvent(event) @@ -838,7 +830,7 @@ def _expand_infobox(self, toggle: bool = False) -> None: self.panel.line_3.setVisible(not toggle) self.panel.netlist_security.setVisible(not toggle) self.panel.netlist_security_label.setVisible(not toggle) - # Align text + self.panel.mn_info_box.setWordWrap(True) self.panel.mn_info_box.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) @@ -878,7 +870,6 @@ def add_network(self) -> None: - add_network_confirmation(pyqtSignal): Signal with a dict that contains the result of adding a new network to the machine. """ - # Check if a password was inserted self.panel.add_network_validation_button.setEnabled(False) self.panel.add_network_validation_button.repaint() @@ -898,7 +889,6 @@ def add_network(self) -> None: error_msg = result.get("error", "") self.panel.add_network_password_field.clear() if not error_msg: - # Assume it was a success QtCore.QTimer().singleShot(5000, self.network_list_worker.build) QtCore.QTimer().singleShot( 5000, @@ -942,15 +932,14 @@ def ssid_item_clicked(self, item: ListItem) -> None: self.selected_item = copy.copy(item) if ( _current_ssid_name in self.sdbus_network.get_saved_ssid_names() - ): # Network already saved go to the information page + ): self.setCurrentIndex(self.indexOf(self.panel.saved_connection_page)) self.panel.saved_connection_network_name.setText(str(_current_ssid_name)) - else: # Network not saved go to the add network page + else: self.setCurrentIndex(self.indexOf(self.panel.add_network_page)) self.panel.add_network_network_label.setText( str(_current_ssid_name) - ) # Add the network name to the title - + ) def update_network( self, ssid: str, @@ -1009,14 +998,14 @@ def setCurrentIndex(self, index: int): if not self.isVisible(): return _cur = self.currentIndex() - if index == self.indexOf(self.panel.add_network_page): # Add network page 2 + if index == self.indexOf(self.panel.add_network_page): self.panel.add_network_password_field.clear() self.panel.add_network_password_field.setPlaceholderText( "Insert password here, press enter when finished." ) elif index == self.indexOf( self.panel.saved_connection_page - ): # Network information page 3 + ): self.panel.saved_connection_change_password_field.clear() self.panel.saved_connection_change_password_field.setPlaceholderText( "Change network password" @@ -1088,7 +1077,6 @@ def add_network_entry(self, ssid: str, signal: int, is_saved: str) -> None: self.model.add_item(item) def _handle_scrollbar(self, value): - # Block signals to avoid recursion self.panel.verticalScrollBar.blockSignals(True) self.panel.verticalScrollBar.setValue(value) self.panel.verticalScrollBar.blockSignals(False) From a7d4981f379736e165cf5ed2ce6789e927404298 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Wed, 17 Dec 2025 12:29:24 +0000 Subject: [PATCH 10/26] networkWindow.py: fix missing formatting --- BlocksScreen/lib/panels/networkWindow.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 75271327..16d89c91 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -489,7 +489,7 @@ def showEvent(self, event: QtGui.QShowEvent | None) -> None: QtCore.QEvent.Type.TouchUpdate, QtCore.QEvent.Type.TouchEnd, ): - return True + return True self.build_model_list() return super().showEvent(event) @@ -930,16 +930,13 @@ def ssid_item_clicked(self, item: ListItem) -> None: _current_ssid_name = item.text self.selected_item = copy.copy(item) - if ( - _current_ssid_name in self.sdbus_network.get_saved_ssid_names() - ): + if _current_ssid_name in self.sdbus_network.get_saved_ssid_names(): self.setCurrentIndex(self.indexOf(self.panel.saved_connection_page)) self.panel.saved_connection_network_name.setText(str(_current_ssid_name)) - else: + else: self.setCurrentIndex(self.indexOf(self.panel.add_network_page)) - self.panel.add_network_network_label.setText( - str(_current_ssid_name) - ) + self.panel.add_network_network_label.setText(str(_current_ssid_name)) + def update_network( self, ssid: str, @@ -998,14 +995,12 @@ def setCurrentIndex(self, index: int): if not self.isVisible(): return _cur = self.currentIndex() - if index == self.indexOf(self.panel.add_network_page): + if index == self.indexOf(self.panel.add_network_page): self.panel.add_network_password_field.clear() self.panel.add_network_password_field.setPlaceholderText( "Insert password here, press enter when finished." ) - elif index == self.indexOf( - self.panel.saved_connection_page - ): + elif index == self.indexOf(self.panel.saved_connection_page): self.panel.saved_connection_change_password_field.clear() self.panel.saved_connection_change_password_field.setPlaceholderText( "Change network password" From 3f51cfe70c60ed55bd5bf6ad6fadd3e2204c6c0a Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Fri, 2 Jan 2026 15:17:51 +0000 Subject: [PATCH 11/26] Revert accidental changes to requirements.txt --- scripts/requirements.txt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/requirements.txt b/scripts/requirements.txt index ed52a007..9dfdbd57 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1,14 +1,14 @@ altgraph==0.17.4 -certifi==2024.7.4 -charset-normalizer==3.3.2 -idna==3.8 -numpy==2.1.0 -pefile==2023.2.7 -PyQt6==6.7.1 -PyQt6-Qt6==6.7.2 -PyQt6_sip==13.8.0 -requests==2.32.4 -sdbus==0.12.0 +certifi==2025.10.5 +charset-normalizer==3.4.4 +idna==3.11 +numpy==2.3.4 +pefile==2024.8.26 +PyQt6==6.10.0 +PyQt6-Qt6==6.10.0 +PyQt6_sip==13.10.2 +requests>=2.32.5 +sdbus==0.14.1 sdbus-networkmanager==2.0.0 typing==3.7.4.3 websocket-client==1.9.0 From a66c95b59316066e14bdeb10b39dc5a1667c8866 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Fri, 2 Jan 2026 15:58:15 +0000 Subject: [PATCH 12/26] networkwindo.py: between 5 and 25 show only one bar icon --- BlocksScreen/lib/panels/networkWindow.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 16d89c91..ed970ef8 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -4,16 +4,14 @@ import typing from functools import partial -from PyQt6 import QtCore, QtGui, QtWidgets -from PyQt6.QtCore import QRunnable, QThreadPool, QObject, pyqtSignal - from lib.network import SdbusNetworkManagerAsync -from lib.panels.widgets.popupDialogWidget import Popup -from lib.ui.wifiConnectivityWindow_ui import Ui_wifi_stacked_page from lib.panels.widgets.keyboardPage import CustomQwertyKeyboard from lib.panels.widgets.loadPage import LoadScreen +from lib.panels.widgets.popupDialogWidget import Popup +from lib.ui.wifiConnectivityWindow_ui import Ui_wifi_stacked_page 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") @@ -186,7 +184,7 @@ def __init__(self): def get_pixmap(self, signal: int, state: str) -> QtGui.QPixmap: """Return a QPixmap for the given signal (0-100) and state ("Protected" or not).""" - if signal <= 25: + if signal < 5: bars = 0 elif signal >= 75: bars = 4 From c1aa80f25417dfd643a170437236cce0d4fa413a Mon Sep 17 00:00:00 2001 From: Roberto Date: Wed, 14 Jan 2026 12:06:44 +0000 Subject: [PATCH 13/26] bugfix: fixed wrong imports --- BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py b/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py index 1af9f761..e66053a9 100644 --- a/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py +++ b/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py @@ -781,7 +781,7 @@ def setupUi(self, wifi_stacked_page): self.verticalLayout_17.addItem(spacerItem8) self.horizontalLayout_4 = QtWidgets.QHBoxLayout() self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.low_priority_btn = GroupButton(parent=self.frame_12) + 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) @@ -793,7 +793,7 @@ def setupUi(self, wifi_stacked_page): self.priority_btn_group.setObjectName("priority_btn_group") self.priority_btn_group.addButton(self.low_priority_btn) self.horizontalLayout_4.addWidget(self.low_priority_btn) - self.med_priority_btn = GroupButton(parent=self.frame_12) + 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) @@ -804,7 +804,7 @@ def setupUi(self, wifi_stacked_page): self.med_priority_btn.setObjectName("med_priority_btn") self.priority_btn_group.addButton(self.med_priority_btn) self.horizontalLayout_4.addWidget(self.med_priority_btn) - self.high_priority_btn = GroupButton(parent=self.frame_12) + 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) @@ -1227,5 +1227,5 @@ def retranslateUi(self, wifi_stacked_page): from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.blocks_linedit import BlocksCustomLinEdit from lib.utils.blocks_togglebutton import NetworkWidgetbuttons -from lib.utils.group_button import GroupButton +from lib.utils.check_button import BlocksCustomCheckButton from lib.utils.icon_button import IconButton From 393d384743decada8e8e76d64a12458dfa15d0ca Mon Sep 17 00:00:00 2001 From: Roberto Date: Wed, 14 Jan 2026 12:39:19 +0000 Subject: [PATCH 14/26] bugfix: wrong button name --- BlocksScreen/lib/panels/networkWindow.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 125d07ba..c15b4f75 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -874,12 +874,11 @@ def handle_button_click(self, ssid: str): priority = entry.get("priority") if priority == 90: - self.panel.hig_priorrity_btn.setChecked(True) + self.panel.high_priority_btn.setChecked(True) elif priority == 20: - self.panel.low_priorrity_btn.setChecked(True) + self.panel.low_priority_btn.setChecked(True) else: - self.panel.med_priorrity_btn.setChecked(True) - + self.panel.med_priority_btn.setChecked(True) _curr_ssid = self.sdbus_network.get_current_ssid() if _curr_ssid != str(ssid): self.panel.network_activate_btn.setDisabled(False) From 52f64f71fe17ef9b2ed8b79c3652a25a63f7a7f1 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Wed, 14 Jan 2026 17:06:36 +0000 Subject: [PATCH 15/26] resolve merge conflits --- BlocksScreen/lib/panels/networkWindow.py | 10 - .../lib/ui/resources/icon_resources_rc.py | 562 +++++++++--------- 2 files changed, 294 insertions(+), 278 deletions(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 4e78069e..61fcefdb 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -6,7 +6,6 @@ from lib.network import SdbusNetworkManagerAsync from lib.panels.widgets.keyboardPage import CustomQwertyKeyboard -from lib.panels.widgets.loadPage import LoadScreen from lib.panels.widgets.popupDialogWidget import Popup from lib.ui.wifiConnectivityWindow_ui import Ui_wifi_stacked_page from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem @@ -235,7 +234,6 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: self.start: bool = True self.saved_network = {} - self.load_popup: LoadScreen = LoadScreen(self) self.repeated_request_status = QtCore.QTimer() self.repeated_request_status.setInterval(2000) @@ -469,8 +467,6 @@ def handle_update_end(self) -> None: """Handles update end signal (closes loading page, returns to normal operation) """ - if self.load_popup.isVisible(): - self.load_popup.close() self.repeated_request_status.stop() self.request_network_scan.emit() self.build_model_list() @@ -479,8 +475,6 @@ def handle_ongoing_update(self) -> None: """Handled ongoing update signal, calls loading page (blocks user interaction) """ - self.load_popup.set_status_message("Updating...") - self.load_popup.show() self.repeated_request_status.start(2000) def reset_view_model(self) -> None: @@ -980,10 +974,6 @@ def handle_network_list(self, data: typing.List[typing.Tuple]) -> None: for entry in data: if entry[0] == self.sdbus_network.hotspot_ssid: continue - if entry == "blank": - continue - if entry == "separator": - continue self.saved_network[entry[0]] = (entry[1], entry[2]) self.build_model_list() self.evaluate_network_state() diff --git a/BlocksScreen/lib/ui/resources/icon_resources_rc.py b/BlocksScreen/lib/ui/resources/icon_resources_rc.py index 9c88b518..a7aca514 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources_rc.py +++ b/BlocksScreen/lib/ui/resources/icon_resources_rc.py @@ -27214,207 +27214,219 @@ " qt_resource_struct_v1 = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x0f\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x95\ -\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x86\ -\x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7b\ -\x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x77\ -\x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x71\ -\x00\x00\x00\x7e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x63\ -\x00\x00\x00\x90\x00\x02\x00\x00\x00\x01\x00\x00\x00\x59\ -\x00\x00\x00\xa2\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4a\ -\x00\x00\x00\xc0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x45\ -\x00\x00\x00\xdc\x00\x02\x00\x00\x00\x01\x00\x00\x00\x35\ -\x00\x00\x01\x02\x00\x02\x00\x00\x00\x01\x00\x00\x00\x31\ -\x00\x00\x01\x2a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x25\ -\x00\x00\x01\x50\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1f\ -\x00\x00\x01\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x10\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x11\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x12\ -\x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x08\x66\ -\x00\x00\x01\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x09\x52\ -\x00\x00\x02\x20\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x35\ -\x00\x00\x02\x48\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x18\ -\x00\x00\x02\x70\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x06\ -\x00\x00\x02\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x14\x42\ -\x00\x00\x02\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf7\ -\x00\x00\x02\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x22\x41\ -\x00\x00\x03\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x26\x90\ -\x00\x00\x03\x36\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x6e\ -\x00\x00\x03\x52\x00\x00\x00\x00\x00\x01\x00\x00\x30\xca\ -\x00\x00\x03\x7a\x00\x00\x00\x00\x00\x01\x00\x00\x32\xb1\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x04\x00\x00\x00\x21\ -\x00\x00\x03\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x3a\x55\ -\x00\x00\x03\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x3c\xcf\ -\x00\x00\x03\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x3f\x43\ -\x00\x00\x03\xfe\x00\x00\x00\x00\x00\x01\x00\x00\x41\xbf\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x26\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x27\ -\x00\x00\x04\x20\x00\x00\x00\x00\x00\x01\x00\x00\x44\x3b\ -\x00\x00\x04\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x45\x3c\ -\x00\x00\x04\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x4a\x54\ -\x00\x00\x04\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x56\x16\ -\x00\x00\x04\xc4\x00\x00\x00\x00\x00\x01\x00\x00\x58\x86\ -\x00\x00\x04\xea\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x1e\ -\x00\x00\x05\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x62\x7a\ -\x00\x00\x05\x48\x00\x00\x00\x00\x00\x01\x00\x00\x69\x07\ -\x00\x00\x05\x64\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x03\ -\x00\x00\x05\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x6d\x01\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x32\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x33\ -\x00\x00\x05\xbe\x00\x01\x00\x00\x00\x01\x00\x00\x77\x6b\ -\x00\x00\x05\xd0\x00\x00\x00\x00\x00\x01\x00\x02\x3a\xc0\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x02\x00\x00\x00\x36\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x06\x00\x00\x00\x3f\ -\x00\x00\x05\xe4\x00\x02\x00\x00\x00\x07\x00\x00\x00\x38\ -\x00\x00\x05\xf6\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ -\x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x49\xaa\ -\x00\x00\x06\x5e\x00\x00\x00\x00\x00\x01\x00\x02\x53\xd3\ -\x00\x00\x06\x98\x00\x00\x00\x00\x00\x01\x00\x02\x5e\x2b\ -\x00\x00\x06\xcc\x00\x00\x00\x00\x00\x01\x00\x02\x68\x43\ -\x00\x00\x07\x02\x00\x00\x00\x00\x00\x01\x00\x02\x72\x6c\ -\x00\x00\x07\x3a\x00\x00\x00\x00\x00\x01\x00\x02\x7c\x82\ -\x00\x00\x07\x70\x00\x00\x00\x00\x00\x01\x00\x02\x86\xe8\ -\x00\x00\x07\x98\x00\x00\x00\x00\x00\x01\x00\x02\x91\x17\ -\x00\x00\x07\xc4\x00\x00\x00\x00\x00\x01\x00\x02\x9b\x5e\ -\x00\x00\x08\x00\x00\x00\x00\x00\x00\x01\x00\x02\x9d\x5f\ -\x00\x00\x08\x2c\x00\x00\x00\x00\x00\x01\x00\x02\xb8\xa4\ -\x00\x00\x08\x58\x00\x00\x00\x00\x00\x01\x00\x02\xbd\xed\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x46\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x03\x00\x00\x00\x47\ -\x00\x00\x08\x8c\x00\x00\x00\x00\x00\x01\x00\x02\xc1\x65\ -\x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x02\xca\x47\ -\x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x02\xcf\x7c\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4b\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x4c\ -\x00\x00\x08\xd8\x00\x00\x00\x00\x00\x01\x00\x02\xd9\x51\ -\x00\x00\x09\x00\x00\x00\x00\x00\x00\x01\x00\x02\xde\x4c\ -\x00\x00\x09\x38\x00\x00\x00\x00\x00\x01\x00\x02\xe7\x20\ -\x00\x00\x09\x70\x00\x00\x00\x00\x00\x01\x00\x02\xef\xc4\ -\x00\x00\x09\xa0\x00\x00\x00\x00\x00\x01\x00\x02\xf7\x63\ -\x00\x00\x09\xc4\x00\x00\x00\x00\x00\x01\x00\x02\xff\x3f\ -\x00\x00\x09\xe8\x00\x00\x00\x00\x00\x01\x00\x03\x06\xf5\ -\x00\x00\x0a\x1c\x00\x00\x00\x00\x00\x01\x00\x03\x0f\x03\ -\x00\x00\x0a\x50\x00\x00\x00\x00\x00\x01\x00\x03\x16\xe5\ -\x00\x00\x0a\x84\x00\x00\x00\x00\x00\x01\x00\x03\x1e\xaf\ -\x00\x00\x0a\xbe\x00\x00\x00\x00\x00\x01\x00\x03\x26\x16\ -\x00\x00\x0a\xe2\x00\x00\x00\x00\x00\x01\x00\x03\x29\xd3\ -\x00\x00\x0b\x10\x00\x00\x00\x00\x00\x01\x00\x03\x2d\xac\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5a\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x08\x00\x00\x00\x5b\ -\x00\x00\x0b\x36\x00\x00\x00\x00\x00\x01\x00\x03\x33\xb5\ -\x00\x00\x0b\x62\x00\x00\x00\x00\x00\x01\x00\x03\x56\x39\ -\x00\x00\x0b\x90\x00\x00\x00\x00\x00\x01\x00\x03\x5c\x26\ -\x00\x00\x0b\xba\x00\x00\x00\x00\x00\x01\x00\x03\x5e\x46\ -\x00\x00\x0b\xe2\x00\x00\x00\x00\x00\x01\x00\x03\x66\xde\ -\x00\x00\x0b\xfc\x00\x00\x00\x00\x00\x01\x00\x03\x76\x27\ -\x00\x00\x0c\x28\x00\x00\x00\x00\x00\x01\x00\x03\x7c\xa5\ -\x00\x00\x0c\x50\x00\x00\x00\x00\x00\x01\x00\x03\x87\x1f\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x64\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x65\ -\x00\x00\x0c\x86\x00\x00\x00\x00\x00\x01\x00\x03\x90\x66\ -\x00\x00\x0c\xb4\x00\x00\x00\x00\x00\x01\x00\x03\x93\x0d\ -\x00\x00\x0c\xe2\x00\x01\x00\x00\x00\x01\x00\x03\x9f\x8a\ -\x00\x00\x0d\x0e\x00\x00\x00\x00\x00\x01\x00\x03\xcd\x09\ -\x00\x00\x0d\x2e\x00\x00\x00\x00\x00\x01\x00\x03\xd1\xc0\ -\x00\x00\x0d\x60\x00\x01\x00\x00\x00\x01\x00\x04\x2a\xb9\ -\x00\x00\x0d\x92\x00\x00\x00\x00\x00\x01\x00\x04\x5f\x53\ -\x00\x00\x0d\xac\x00\x00\x00\x00\x00\x01\x00\x04\x64\x9d\ -\x00\x00\x0d\xc6\x00\x00\x00\x00\x00\x01\x00\x04\x6a\x2c\ -\x00\x00\x0d\xe0\x00\x00\x00\x00\x00\x01\x00\x04\x6f\x95\ -\x00\x00\x0d\xf8\x00\x00\x00\x00\x00\x01\x00\x04\x7b\x73\ -\x00\x00\x0e\x16\x00\x00\x00\x00\x00\x01\x00\x04\x81\x77\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x72\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x04\x00\x00\x00\x73\ -\x00\x00\x0e\x3e\x00\x00\x00\x00\x00\x01\x00\x04\x86\xac\ -\x00\x00\x0e\x52\x00\x00\x00\x00\x00\x01\x00\x04\x8c\xa9\ -\x00\x00\x0e\x64\x00\x00\x00\x00\x00\x01\x00\x04\x8e\x2f\ -\x00\x00\x0e\x76\x00\x00\x00\x00\x00\x01\x00\x04\x94\x29\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x78\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x79\ -\x00\x00\x0e\x8a\x00\x00\x00\x00\x00\x01\x00\x04\x96\x7f\ -\x00\x00\x0e\xb6\x00\x00\x00\x00\x00\x01\x00\x04\x9d\x66\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7c\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x09\x00\x00\x00\x7d\ -\x00\x00\x0e\xda\x00\x00\x00\x00\x00\x01\x00\x04\xa6\xca\ -\x00\x00\x0e\xfa\x00\x01\x00\x00\x00\x01\x00\x04\xa9\x76\ -\x00\x00\x0f\x1e\x00\x00\x00\x00\x00\x01\x00\x04\xb4\xc7\ -\x00\x00\x0f\x40\x00\x00\x00\x00\x00\x01\x00\x04\xbc\x6c\ -\x00\x00\x0f\x5c\x00\x00\x00\x00\x00\x01\x00\x04\xcd\x6c\ -\x00\x00\x0f\x80\x00\x00\x00\x00\x00\x01\x00\x04\xd6\xed\ -\x00\x00\x0f\xa0\x00\x00\x00\x00\x00\x01\x00\x04\xdc\x8a\ -\x00\x00\x0f\xc8\x00\x00\x00\x00\x00\x01\x00\x04\xe6\x53\ -\x00\x00\x0f\xe8\x00\x00\x00\x00\x00\x01\x00\x04\xea\x36\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x87\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x88\ -\x00\x00\x10\x04\x00\x00\x00\x00\x00\x01\x00\x04\xf0\x71\ -\x00\x00\x10\x34\x00\x00\x00\x00\x00\x01\x00\x04\xfa\x07\ -\x00\x00\x10\x64\x00\x00\x00\x00\x00\x01\x00\x05\x05\xe3\ -\x00\x00\x10\x88\x00\x00\x00\x00\x00\x01\x00\x05\x0c\x27\ -\x00\x00\x10\xb2\x00\x00\x00\x00\x00\x01\x00\x05\x13\xb0\ -\x00\x00\x10\xde\x00\x00\x00\x00\x00\x01\x00\x05\x1a\x0e\ -\x00\x00\x11\x14\x00\x00\x00\x00\x00\x01\x00\x05\x21\xfd\ -\x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x2f\xa0\ -\x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x34\xd5\ -\x00\x00\x11\x32\x00\x00\x00\x00\x00\x01\x00\x05\x3e\xaa\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x01\x00\x00\x00\x94\ -\x00\x00\x11\x5a\x00\x00\x00\x00\x00\x01\x00\x05\x46\x74\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x96\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x28\x00\x00\x00\x97\ -\x00\x00\x11\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x4b\x3a\ -\x00\x00\x11\x90\x00\x00\x00\x00\x00\x01\x00\x05\x52\xee\ -\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x55\x13\ -\x00\x00\x11\xe0\x00\x00\x00\x00\x00\x01\x00\x05\x56\x93\ -\x00\x00\x11\xfc\x00\x00\x00\x00\x00\x01\x00\x05\x5e\x40\ -\x00\x00\x12\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x63\x15\ -\x00\x00\x12\x32\x00\x00\x00\x00\x00\x01\x00\x05\x64\x05\ -\x00\x00\x12\x48\x00\x00\x00\x00\x00\x01\x00\x05\x66\xe9\ -\x00\x00\x12\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x6d\x20\ -\x00\x00\x12\x88\x00\x00\x00\x00\x00\x01\x00\x05\x82\x2d\ -\x00\x00\x12\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x87\x1d\ -\x00\x00\x12\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x8a\x1c\ -\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x90\x26\ -\x00\x00\x13\x08\x00\x00\x00\x00\x00\x01\x00\x05\x93\x75\ -\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x9c\xf6\ -\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\xa2\xed\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\xa4\xfe\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xc1\ -\x00\x00\x13\x88\x00\x00\x00\x00\x00\x01\x00\x05\xb2\x75\ -\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xb5\xc3\ -\x00\x00\x13\xd4\x00\x00\x00\x00\x00\x01\x00\x05\xb9\x51\ -\x00\x00\x13\xfa\x00\x00\x00\x00\x00\x01\x00\x05\xbd\xf2\ -\x00\x00\x14\x0e\x00\x00\x00\x00\x00\x01\x00\x05\xc7\xc4\ -\x00\x00\x14\x3a\x00\x00\x00\x00\x00\x01\x00\x05\xcd\x0e\ -\x00\x00\x14\x62\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x15\ -\x00\x00\x14\x78\x00\x00\x00\x00\x00\x01\x00\x05\xd3\xf9\ -\x00\x00\x14\xa4\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x42\ -\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xdc\xf2\ -\x00\x00\x14\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xe0\x36\ -\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xe1\x62\ -\x00\x00\x15\x08\x00\x00\x00\x00\x00\x01\x00\x05\xe7\x23\ -\x00\x00\x15\x2a\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x45\ -\x00\x00\x15\x48\x00\x00\x00\x00\x00\x01\x00\x05\xee\x38\ -\x00\x00\x15\x68\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x3c\ -\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xf2\x5d\ -\x00\x00\x15\xaa\x00\x00\x00\x00\x00\x01\x00\x05\xf5\x31\ -\x00\x00\x15\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xfd\x9d\ -\x00\x00\x15\xfc\x00\x00\x00\x00\x00\x01\x00\x06\x05\x5d\ -\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x06\x0a\x78\ -\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x0b\xcb\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x10\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa1\ +\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9e\ +\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ +\x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x80\ +\x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7c\ +\x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x76\ +\x00\x00\x00\x7e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x68\ +\x00\x00\x00\x90\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5e\ +\x00\x00\x00\xa2\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4f\ +\x00\x00\x00\xc0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4a\ +\x00\x00\x00\xdc\x00\x02\x00\x00\x00\x01\x00\x00\x00\x3a\ +\x00\x00\x01\x02\x00\x02\x00\x00\x00\x01\x00\x00\x00\x36\ +\x00\x00\x01\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x32\ +\x00\x00\x01\x42\x00\x02\x00\x00\x00\x01\x00\x00\x00\x26\ +\x00\x00\x01\x68\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ +\x00\x00\x01\x84\x00\x02\x00\x00\x00\x01\x00\x00\x00\x11\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x12\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x13\ +\x00\x00\x01\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x08\x66\ +\x00\x00\x02\x10\x00\x00\x00\x00\x00\x01\x00\x00\x09\x52\ +\x00\x00\x02\x38\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x35\ +\x00\x00\x02\x60\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x18\ +\x00\x00\x02\x88\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x06\ +\x00\x00\x02\xbc\x00\x00\x00\x00\x00\x01\x00\x00\x14\x42\ +\x00\x00\x02\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf7\ +\x00\x00\x03\x12\x00\x00\x00\x00\x00\x01\x00\x00\x22\x41\ +\x00\x00\x03\x32\x00\x00\x00\x00\x00\x01\x00\x00\x26\x90\ +\x00\x00\x03\x4e\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x6e\ +\x00\x00\x03\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x30\xca\ +\x00\x00\x03\x92\x00\x00\x00\x00\x00\x01\x00\x00\x32\xb1\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x21\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x22\ +\x00\x00\x03\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x3a\x55\ +\x00\x00\x03\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x3c\xcf\ +\x00\x00\x03\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x3f\x43\ +\x00\x00\x04\x16\x00\x00\x00\x00\x00\x01\x00\x00\x41\xbf\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x27\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x28\ +\x00\x00\x04\x38\x00\x00\x00\x00\x00\x01\x00\x00\x44\x3b\ +\x00\x00\x04\x52\x00\x00\x00\x00\x00\x01\x00\x00\x45\x3c\ +\x00\x00\x04\x82\x00\x00\x00\x00\x00\x01\x00\x00\x4a\x54\ +\x00\x00\x04\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x56\x16\ +\x00\x00\x04\xdc\x00\x00\x00\x00\x00\x01\x00\x00\x58\x86\ +\x00\x00\x05\x02\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x1e\ +\x00\x00\x05\x26\x00\x00\x00\x00\x00\x01\x00\x00\x62\x7a\ +\x00\x00\x05\x60\x00\x00\x00\x00\x00\x01\x00\x00\x69\x07\ +\x00\x00\x05\x7c\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x03\ +\x00\x00\x05\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x6d\x01\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x33\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x34\ +\x00\x00\x05\xd6\x00\x01\x00\x00\x00\x01\x00\x00\x77\x6b\ +\x00\x00\x05\xe8\x00\x00\x00\x00\x00\x01\x00\x02\x3a\xc0\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x37\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x38\ +\x00\x00\x05\xfc\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ +\x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x41\x71\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3b\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x44\ +\x00\x00\x06\x5a\x00\x02\x00\x00\x00\x07\x00\x00\x00\x3d\ +\x00\x00\x06\x6c\x00\x00\x00\x00\x00\x01\x00\x02\x43\x77\ +\x00\x00\x06\xa0\x00\x00\x00\x00\x00\x01\x00\x02\x4d\xdb\ +\x00\x00\x06\xd4\x00\x00\x00\x00\x00\x01\x00\x02\x58\x04\ +\x00\x00\x07\x0e\x00\x00\x00\x00\x00\x01\x00\x02\x62\x5c\ +\x00\x00\x07\x42\x00\x00\x00\x00\x00\x01\x00\x02\x6c\x74\ +\x00\x00\x07\x78\x00\x00\x00\x00\x00\x01\x00\x02\x76\x9d\ +\x00\x00\x07\xb0\x00\x00\x00\x00\x00\x01\x00\x02\x80\xb3\ +\x00\x00\x07\xe6\x00\x00\x00\x00\x00\x01\x00\x02\x8b\x19\ +\x00\x00\x08\x0e\x00\x00\x00\x00\x00\x01\x00\x02\x95\x48\ +\x00\x00\x08\x3a\x00\x00\x00\x00\x00\x01\x00\x02\x9f\x8f\ +\x00\x00\x08\x76\x00\x00\x00\x00\x00\x01\x00\x02\xa1\x90\ +\x00\x00\x08\xa2\x00\x00\x00\x00\x00\x01\x00\x02\xbc\xd5\ +\x00\x00\x08\xce\x00\x00\x00\x00\x00\x01\x00\x02\xc2\x1e\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4b\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x03\x00\x00\x00\x4c\ +\x00\x00\x09\x02\x00\x00\x00\x00\x00\x01\x00\x02\xc5\x96\ +\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x02\xce\x78\ +\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x02\xd3\xad\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x50\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x51\ +\x00\x00\x09\x4e\x00\x00\x00\x00\x00\x01\x00\x02\xdd\x82\ +\x00\x00\x09\x76\x00\x00\x00\x00\x00\x01\x00\x02\xe2\x7d\ +\x00\x00\x09\xae\x00\x00\x00\x00\x00\x01\x00\x02\xeb\x51\ +\x00\x00\x09\xe6\x00\x00\x00\x00\x00\x01\x00\x02\xf3\xf5\ +\x00\x00\x0a\x16\x00\x00\x00\x00\x00\x01\x00\x02\xfb\x94\ +\x00\x00\x0a\x3a\x00\x00\x00\x00\x00\x01\x00\x03\x03\x70\ +\x00\x00\x0a\x5e\x00\x00\x00\x00\x00\x01\x00\x03\x0b\x26\ +\x00\x00\x0a\x92\x00\x00\x00\x00\x00\x01\x00\x03\x13\x34\ +\x00\x00\x0a\xc6\x00\x00\x00\x00\x00\x01\x00\x03\x1b\x16\ +\x00\x00\x0a\xfa\x00\x00\x00\x00\x00\x01\x00\x03\x22\xe0\ +\x00\x00\x0b\x34\x00\x00\x00\x00\x00\x01\x00\x03\x2a\x47\ +\x00\x00\x0b\x58\x00\x00\x00\x00\x00\x01\x00\x03\x2e\x04\ +\x00\x00\x0b\x86\x00\x00\x00\x00\x00\x01\x00\x03\x31\xdd\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5f\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x08\x00\x00\x00\x60\ +\x00\x00\x0b\xac\x00\x00\x00\x00\x00\x01\x00\x03\x37\xe6\ +\x00\x00\x0b\xd8\x00\x00\x00\x00\x00\x01\x00\x03\x5a\x6a\ +\x00\x00\x0c\x06\x00\x00\x00\x00\x00\x01\x00\x03\x60\x57\ +\x00\x00\x0c\x30\x00\x00\x00\x00\x00\x01\x00\x03\x62\x77\ +\x00\x00\x0c\x58\x00\x00\x00\x00\x00\x01\x00\x03\x6b\x0f\ +\x00\x00\x0c\x72\x00\x00\x00\x00\x00\x01\x00\x03\x7a\x58\ +\x00\x00\x0c\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x80\xd6\ +\x00\x00\x0c\xc6\x00\x00\x00\x00\x00\x01\x00\x03\x8b\x50\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x69\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x6a\ +\x00\x00\x0c\xfc\x00\x00\x00\x00\x00\x01\x00\x03\x94\x97\ +\x00\x00\x0d\x2a\x00\x00\x00\x00\x00\x01\x00\x03\x97\x3e\ +\x00\x00\x0d\x58\x00\x01\x00\x00\x00\x01\x00\x03\xa3\xbb\ +\x00\x00\x0d\x84\x00\x00\x00\x00\x00\x01\x00\x03\xd1\x3a\ +\x00\x00\x0d\xa4\x00\x00\x00\x00\x00\x01\x00\x03\xd5\xf1\ +\x00\x00\x0d\xd6\x00\x01\x00\x00\x00\x01\x00\x04\x2e\xea\ +\x00\x00\x0e\x08\x00\x00\x00\x00\x00\x01\x00\x04\x63\x84\ +\x00\x00\x0e\x22\x00\x00\x00\x00\x00\x01\x00\x04\x68\xce\ +\x00\x00\x0e\x3c\x00\x00\x00\x00\x00\x01\x00\x04\x6e\x5d\ +\x00\x00\x0e\x56\x00\x00\x00\x00\x00\x01\x00\x04\x73\xc6\ +\x00\x00\x0e\x6e\x00\x00\x00\x00\x00\x01\x00\x04\x7f\xa4\ +\x00\x00\x0e\x8c\x00\x00\x00\x00\x00\x01\x00\x04\x85\xa8\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x77\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x78\ +\x00\x00\x0e\xb4\x00\x00\x00\x00\x00\x01\x00\x04\x8a\xdd\ +\x00\x00\x0e\xc8\x00\x00\x00\x00\x00\x01\x00\x04\x90\xda\ +\x00\x00\x0e\xda\x00\x00\x00\x00\x00\x01\x00\x04\x92\x60\ +\x00\x00\x0e\xec\x00\x00\x00\x00\x00\x01\x00\x04\x98\x5a\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7d\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x7e\ +\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x01\x00\x04\x9a\xb0\ +\x00\x00\x0f\x2c\x00\x00\x00\x00\x00\x01\x00\x04\xa1\x97\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x81\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x10\x00\x00\x00\x82\ +\x00\x00\x0f\x50\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xfb\ +\x00\x00\x0f\x70\x00\x00\x00\x00\x00\x01\x00\x04\xb0\x94\ +\x00\x00\x0f\x90\x00\x01\x00\x00\x00\x01\x00\x04\xb6\xbb\ +\x00\x00\x0f\xb4\x00\x00\x00\x00\x00\x01\x00\x04\xc2\x0c\ +\x00\x00\x0f\xd6\x00\x00\x00\x00\x00\x01\x00\x04\xc9\xb1\ +\x00\x00\x0f\xf2\x00\x00\x00\x00\x00\x01\x00\x04\xda\xb1\ +\x00\x00\x10\x16\x00\x00\x00\x00\x00\x01\x00\x04\xe4\x32\ +\x00\x00\x10\x36\x00\x00\x00\x00\x00\x01\x00\x04\xe9\xb6\ +\x00\x00\x10\x56\x00\x00\x00\x00\x00\x01\x00\x04\xef\x4f\ +\x00\x00\x10\x7e\x00\x00\x00\x00\x00\x01\x00\x04\xf9\x18\ +\x00\x00\x10\x9e\x00\x00\x00\x00\x00\x01\x00\x04\xfe\xb1\ +\x00\x00\x10\xd2\x00\x00\x00\x00\x00\x01\x00\x05\x09\x25\ +\x00\x00\x10\xee\x00\x00\x00\x00\x00\x01\x00\x05\x0f\x60\ +\x00\x00\x11\x22\x00\x00\x00\x00\x00\x01\x00\x05\x19\xda\ +\x00\x00\x11\x56\x00\x00\x00\x00\x00\x01\x00\x05\x24\x39\ +\x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x2f\x84\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x94\ +\x00\x00\x11\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x39\xf8\ +\x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x43\x8e\ +\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x6a\ +\x00\x00\x12\x42\x00\x00\x00\x00\x00\x01\x00\x05\x55\xae\ +\x00\x00\x12\x6c\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x37\ +\x00\x00\x12\x98\x00\x00\x00\x00\x00\x01\x00\x05\x63\x95\ +\x00\x00\x12\xce\x00\x00\x00\x00\x00\x01\x00\x05\x6b\x84\ +\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x79\x27\ +\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x7e\x5c\ +\x00\x00\x12\xec\x00\x00\x00\x00\x00\x01\x00\x05\x88\x31\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9f\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa0\ +\x00\x00\x13\x14\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xfb\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa2\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x28\x00\x00\x00\xa3\ +\x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x94\xc1\ +\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x75\ +\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\x9a\ +\x00\x00\x13\x9a\x00\x00\x00\x00\x00\x01\x00\x05\xa0\x1a\ +\x00\x00\x13\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xa7\xc7\ +\x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xac\x9c\ +\x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xad\x8c\ +\x00\x00\x14\x02\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xb5\ +\x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb7\xec\ +\x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xcc\xf9\ +\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xd1\xe9\ +\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xd4\xe8\ +\x00\x00\x14\x90\x00\x00\x00\x00\x00\x01\x00\x05\xda\xf2\ +\x00\x00\x14\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xde\x41\ +\x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xe2\x62\ +\x00\x00\x14\xf0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x59\ +\x00\x00\x15\x04\x00\x00\x00\x00\x00\x01\x00\x05\xea\x6a\ +\x00\x00\x15\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xee\x2d\ +\x00\x00\x15\x42\x00\x00\x00\x00\x00\x01\x00\x05\xf7\xe1\ +\x00\x00\x15\x6c\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x2f\ +\x00\x00\x15\x8e\x00\x00\x00\x00\x00\x01\x00\x05\xfe\xbd\ +\x00\x00\x15\xb4\x00\x00\x00\x00\x00\x01\x00\x06\x03\x5e\ +\x00\x00\x15\xc8\x00\x00\x00\x00\x00\x01\x00\x06\x0d\x30\ +\x00\x00\x15\xf4\x00\x00\x00\x00\x00\x01\x00\x06\x12\x7a\ +\x00\x00\x16\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x18\x81\ +\x00\x00\x16\x32\x00\x00\x00\x00\x00\x01\x00\x06\x19\x65\ +\x00\x00\x16\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x1b\xae\ +\x00\x00\x16\x74\x00\x00\x00\x00\x00\x01\x00\x06\x22\x5e\ +\x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x06\x25\xa2\ +\x00\x00\x16\xa8\x00\x00\x00\x00\x00\x01\x00\x06\x26\xce\ +\x00\x00\x16\xc2\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x8f\ +\x00\x00\x16\xe4\x00\x00\x00\x00\x00\x01\x00\x06\x2d\xb1\ +\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\xa4\ +\x00\x00\x17\x22\x00\x00\x00\x00\x00\x01\x00\x06\x36\xa8\ +\x00\x00\x17\x44\x00\x00\x00\x00\x00\x01\x00\x06\x37\xc9\ +\x00\x00\x17\x64\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x9d\ +\x00\x00\x17\x92\x00\x00\x00\x00\x00\x01\x00\x06\x43\x09\ +\x00\x00\x17\xb6\x00\x00\x00\x00\x00\x01\x00\x06\x4a\xc9\ +\x00\x00\x17\xda\x00\x00\x00\x00\x00\x01\x00\x06\x4f\xe4\ +\x00\x00\x18\x02\x00\x00\x00\x00\x00\x01\x00\x06\x51\x37\ " qt_resource_struct_v2 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x10\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x95\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa1\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ +\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9e\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x86\ +\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x80\ \x00\x00\x00\x00\x00\x00\x00\x00\ @@ -27521,9 +27533,9 @@ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x38\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x05\xfc\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ -\x00\x00\x01\x9b\xbc\x28\x2f\x35\ +\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ \x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x41\x71\ -\x00\x00\x01\x9b\xbc\x28\x2f\x35\ +\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3b\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x44\ @@ -27666,139 +27678,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\x98\x00\x02\x00\x00\x00\x09\x00\x00\x00\x7d\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x10\x00\x00\x00\x82\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0e\xda\x00\x00\x00\x00\x00\x01\x00\x04\xa6\xca\ -\x00\x00\x01\x9b\x13\x3e\xbb\x62\ -\x00\x00\x0e\xfa\x00\x01\x00\x00\x00\x01\x00\x04\xa9\x76\ +\x00\x00\x0f\x50\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xfb\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x0f\x70\x00\x00\x00\x00\x00\x01\x00\x04\xb0\x94\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x0f\x90\x00\x01\x00\x00\x00\x01\x00\x04\xb6\xbb\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x0f\x1e\x00\x00\x00\x00\x00\x01\x00\x04\xb4\xc7\ +\x00\x00\x0f\xb4\x00\x00\x00\x00\x00\x01\x00\x04\xc2\x0c\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x0f\x40\x00\x00\x00\x00\x00\x01\x00\x04\xbc\x6c\ +\x00\x00\x0f\xd6\x00\x00\x00\x00\x00\x01\x00\x04\xc9\xb1\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x0f\x5c\x00\x00\x00\x00\x00\x01\x00\x04\xcd\x6c\ +\x00\x00\x0f\xf2\x00\x00\x00\x00\x00\x01\x00\x04\xda\xb1\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x0f\x80\x00\x00\x00\x00\x00\x01\x00\x04\xd6\xed\ -\x00\x00\x01\x9b\x13\x3e\xbb\x62\ -\x00\x00\x0f\xa0\x00\x00\x00\x00\x00\x01\x00\x04\xdc\x8a\ +\x00\x00\x10\x16\x00\x00\x00\x00\x00\x01\x00\x04\xe4\x32\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x10\x36\x00\x00\x00\x00\x00\x01\x00\x04\xe9\xb6\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x10\x56\x00\x00\x00\x00\x00\x01\x00\x04\xef\x4f\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x0f\xc8\x00\x00\x00\x00\x00\x01\x00\x04\xe6\x53\ -\x00\x00\x01\x9b\x13\x3e\xbb\x62\ -\x00\x00\x0f\xe8\x00\x00\x00\x00\x00\x01\x00\x04\xea\x36\ +\x00\x00\x10\x7e\x00\x00\x00\x00\x00\x01\x00\x04\xf9\x18\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x10\x9e\x00\x00\x00\x00\x00\x01\x00\x04\xfe\xb1\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x10\xd2\x00\x00\x00\x00\x00\x01\x00\x05\x09\x25\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x87\ +\x00\x00\x10\xee\x00\x00\x00\x00\x00\x01\x00\x05\x0f\x60\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x11\x22\x00\x00\x00\x00\x00\x01\x00\x05\x19\xda\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x11\x56\x00\x00\x00\x00\x00\x01\x00\x05\x24\x39\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x2f\x84\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x88\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x94\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x10\x04\x00\x00\x00\x00\x00\x01\x00\x04\xf0\x71\ +\x00\x00\x11\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x39\xf8\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x10\x34\x00\x00\x00\x00\x00\x01\x00\x04\xfa\x07\ +\x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x43\x8e\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x10\x64\x00\x00\x00\x00\x00\x01\x00\x05\x05\xe3\ +\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x6a\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x10\x88\x00\x00\x00\x00\x00\x01\x00\x05\x0c\x27\ +\x00\x00\x12\x42\x00\x00\x00\x00\x00\x01\x00\x05\x55\xae\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x10\xb2\x00\x00\x00\x00\x00\x01\x00\x05\x13\xb0\ +\x00\x00\x12\x6c\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x37\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x10\xde\x00\x00\x00\x00\x00\x01\x00\x05\x1a\x0e\ +\x00\x00\x12\x98\x00\x00\x00\x00\x00\x01\x00\x05\x63\x95\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x11\x14\x00\x00\x00\x00\x00\x01\x00\x05\x21\xfd\ +\x00\x00\x12\xce\x00\x00\x00\x00\x00\x01\x00\x05\x6b\x84\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x2f\xa0\ +\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x79\x27\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x34\xd5\ +\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x7e\x5c\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x11\x32\x00\x00\x00\x00\x00\x01\x00\x05\x3e\xaa\ +\x00\x00\x12\xec\x00\x00\x00\x00\x00\x01\x00\x05\x88\x31\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ +\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\x98\x00\x02\x00\x00\x00\x01\x00\x00\x00\x94\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa0\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x11\x5a\x00\x00\x00\x00\x00\x01\x00\x05\x46\x74\ +\x00\x00\x13\x14\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xfb\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x96\ +\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\x98\x00\x02\x00\x00\x00\x28\x00\x00\x00\x97\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x28\x00\x00\x00\xa3\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x11\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x4b\x3a\ +\x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x94\xc1\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x11\x90\x00\x00\x00\x00\x00\x01\x00\x05\x52\xee\ +\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x75\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x55\x13\ +\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\x9a\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x11\xe0\x00\x00\x00\x00\x00\x01\x00\x05\x56\x93\ +\x00\x00\x13\x9a\x00\x00\x00\x00\x00\x01\x00\x05\xa0\x1a\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x11\xfc\x00\x00\x00\x00\x00\x01\x00\x05\x5e\x40\ +\x00\x00\x13\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xa7\xc7\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x12\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x63\x15\ +\x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xac\x9c\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x12\x32\x00\x00\x00\x00\x00\x01\x00\x05\x64\x05\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x12\x48\x00\x00\x00\x00\x00\x01\x00\x05\x66\xe9\ +\x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xad\x8c\ +\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ +\x00\x00\x14\x02\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xb5\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x6d\x20\ +\x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb7\xec\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x12\x88\x00\x00\x00\x00\x00\x01\x00\x05\x82\x2d\ +\x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xcc\xf9\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x12\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x87\x1d\ +\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xd1\xe9\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x12\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x8a\x1c\ +\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xd4\xe8\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x90\x26\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x13\x08\x00\x00\x00\x00\x00\x01\x00\x05\x93\x75\ +\x00\x00\x14\x90\x00\x00\x00\x00\x00\x01\x00\x05\xda\xf2\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x9c\xf6\ +\x00\x00\x14\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xde\x41\ +\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ +\x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xe2\x62\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\xa2\xed\ +\x00\x00\x14\xf0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x59\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\xa4\xfe\ +\x00\x00\x15\x04\x00\x00\x00\x00\x00\x01\x00\x05\xea\x6a\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xc1\ +\x00\x00\x15\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xee\x2d\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x13\x88\x00\x00\x00\x00\x00\x01\x00\x05\xb2\x75\ +\x00\x00\x15\x42\x00\x00\x00\x00\x00\x01\x00\x05\xf7\xe1\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xb5\xc3\ +\x00\x00\x15\x6c\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x2f\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\xd4\x00\x00\x00\x00\x00\x01\x00\x05\xb9\x51\ +\x00\x00\x15\x8e\x00\x00\x00\x00\x00\x01\x00\x05\xfe\xbd\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x13\xfa\x00\x00\x00\x00\x00\x01\x00\x05\xbd\xf2\ +\x00\x00\x15\xb4\x00\x00\x00\x00\x00\x01\x00\x06\x03\x5e\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x0e\x00\x00\x00\x00\x00\x01\x00\x05\xc7\xc4\ +\x00\x00\x15\xc8\x00\x00\x00\x00\x00\x01\x00\x06\x0d\x30\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x14\x3a\x00\x00\x00\x00\x00\x01\x00\x05\xcd\x0e\ +\x00\x00\x15\xf4\x00\x00\x00\x00\x00\x01\x00\x06\x12\x7a\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x62\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x15\ +\x00\x00\x16\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x18\x81\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x78\x00\x00\x00\x00\x00\x01\x00\x05\xd3\xf9\ +\x00\x00\x16\x32\x00\x00\x00\x00\x00\x01\x00\x06\x19\x65\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x14\xa4\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x42\ +\x00\x00\x16\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x1b\xae\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xdc\xf2\ +\x00\x00\x16\x74\x00\x00\x00\x00\x00\x01\x00\x06\x22\x5e\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xe0\x36\ +\x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x06\x25\xa2\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xe1\x62\ +\x00\x00\x16\xa8\x00\x00\x00\x00\x00\x01\x00\x06\x26\xce\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x15\x08\x00\x00\x00\x00\x00\x01\x00\x05\xe7\x23\ +\x00\x00\x16\xc2\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x8f\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x2a\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x45\ +\x00\x00\x16\xe4\x00\x00\x00\x00\x00\x01\x00\x06\x2d\xb1\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x48\x00\x00\x00\x00\x00\x01\x00\x05\xee\x38\ +\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\xa4\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x15\x68\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x3c\ +\x00\x00\x17\x22\x00\x00\x00\x00\x00\x01\x00\x06\x36\xa8\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xf2\x5d\ +\x00\x00\x17\x44\x00\x00\x00\x00\x00\x01\x00\x06\x37\xc9\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\xaa\x00\x00\x00\x00\x00\x01\x00\x05\xf5\x31\ +\x00\x00\x17\x64\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x9d\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x15\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xfd\x9d\ +\x00\x00\x17\x92\x00\x00\x00\x00\x00\x01\x00\x06\x43\x09\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x15\xfc\x00\x00\x00\x00\x00\x01\x00\x06\x05\x5d\ +\x00\x00\x17\xb6\x00\x00\x00\x00\x00\x01\x00\x06\x4a\xc9\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x06\x0a\x78\ +\x00\x00\x17\xda\x00\x00\x00\x00\x00\x01\x00\x06\x4f\xe4\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x0b\xcb\ +\x00\x00\x18\x02\x00\x00\x00\x00\x00\x01\x00\x06\x51\x37\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ " From 97a881e5fe8408a06f324c12d6a5dfde7b428e1e Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Wed, 14 Jan 2026 17:41:36 +0000 Subject: [PATCH 16/26] add missing docstrings --- BlocksScreen/lib/panels/networkWindow.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 61fcefdb..df9e39cc 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -124,26 +124,33 @@ def __init__(self, poll_interval_ms: int = 10000): self._timer.timeout.connect(self._do_scan) def start_polling(self): + """Schedule scan folowwing the `poll_interval_ms`""" self._schedule_next_scan() def stop_polling(self): + """Start Polling for future development""" self._timer.stop() def build(self): + """Method called by rescan button""" self._do_scan() def _schedule_next_scan(self): + """Start next rescan timer""" self._timer.start(self.poll_interval_ms) def _on_task_finished(self, items): + """Update data after scan finish, update saved networks list and schedule next scan""" self._is_scanning = False self.finished_network_list_build.emit(items) self._schedule_next_scan() def _on_task_scan_results(self, data_dict): + """Emit results to main thread""" self.scan_results.emit(data_dict) def _on_task_error(self, err): + """Emit errors to main thread""" self._is_scanning = False self.error.emit(err) self._schedule_next_scan() From 58152951c0c2e07ae19a55402d1004a5e0d3f058 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Wed, 14 Jan 2026 17:44:20 +0000 Subject: [PATCH 17/26] add missing docstrings --- BlocksScreen/lib/panels/networkWindow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index df9e39cc..3c190928 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -156,6 +156,7 @@ def _on_task_error(self, err): self._schedule_next_scan() def _do_scan(self): + """Start wifi scanning threadpool""" if self._is_scanning: logger.debug("Already scanning, skip scheduling.") self._schedule_next_scan() From 21cae0204bef141e28c12729de82d1f9475fd010 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Fri, 16 Jan 2026 18:29:39 +0000 Subject: [PATCH 18/26] separation between saved and unsaved network and update code --- BlocksScreen/lib/panels/networkWindow.py | 223 +++++++++++++---------- BlocksScreen/lib/utils/list_model.py | 9 + 2 files changed, 135 insertions(+), 97 deletions(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 1e2dc0e9..0f72c869 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -8,6 +8,7 @@ from lib.panels.widgets.keyboardPage import CustomQwertyKeyboard from lib.panels.widgets.popupDialogWidget import Popup from lib.ui.wifiConnectivityWindow_ui import Ui_wifi_stacked_page +from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal @@ -79,6 +80,8 @@ def run(self): for ssid, sig in saved_nets: status = "Active" if ssid == self.nm.get_current_ssid() else "Saved" items.append((ssid, sig, status)) + if saved_nets: + items.append(("separator", -10, "separator")) for ssid, sig in unsaved_nets: items.append((ssid, sig, "Protected")) @@ -207,7 +210,7 @@ def get_pixmap(self, signal: int, state: str) -> QtGui.QPixmap: path = self.paths.get(key) if path is None: logger.debug( - f"No icon path for key {key}, falling back to no-signal unprotected" + "No icon path for key %s, falling back to no-signal unprotected", key ) path = self.paths[(0, False)] @@ -241,9 +244,12 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: self.start: bool = True self.saved_network = {} - self.repeated_request_status = QtCore.QTimer() + self.repeated_request_status = QtCore.QTimer(self) self.repeated_request_status.setInterval(2000) + self._delayed_action_timer = QtCore.QTimer(self) + self._delayed_action_timer.setSingleShot(True) + self._load_timer = QtCore.QTimer() self._load_timer.setSingleShot(True) self._load_timer.timeout.connect(self._handle_load_timeout) @@ -287,9 +293,6 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: ) self.panel.network_backButton.clicked.connect(self.hide) - self.panel.rescan_button.clicked.connect( - lambda: self.sdbus_network.rescan_networks() - ) self.request_network_scan.connect(self.rescan_networks) self.panel.add_network_validation_button.clicked.connect(self.add_network) @@ -423,12 +426,8 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: lambda: self.setCurrentIndex(self.indexOf(self.panel.saved_details_page)) ) - self.panel.network_activate_btn.clicked.connect( - lambda: self.saved_wifi_option_selected() - ) - self.panel.network_delete_btn.clicked.connect( - lambda: self.saved_wifi_option_selected() - ) + self.panel.network_activate_btn.clicked.connect(self.saved_wifi_option_selected) + self.panel.network_delete_btn.clicked.connect(self.saved_wifi_option_selected) self.network_list_worker.build() self.request_network_scan.emit() @@ -491,49 +490,63 @@ def reset_view_model(self) -> None: self.model.clear() self.entry_delegate.clear() - def deleteLater(self) -> None: - """Schedule the object for deletion, resets the list model first""" - self.reset_view_model() - return super().deleteLater() + def closeEvent(self, event: QtGui.QShowEvent | None) -> None: + """Clean up resources before closing it""" + if hasattr(self, "_load_timer") and self._load_timer.isActive(): + self._load_timer.stop() - def showEvent(self, event: QtGui.QShowEvent | None) -> None: - """Re-add clients to update list""" - if event.type() in ( - QtCore.QEvent.Type.TouchBegin, - QtCore.QEvent.Type.TouchUpdate, - QtCore.QEvent.Type.TouchEnd, + if ( + hasattr(self, "repeated_request_status") + and self.repeated_request_status.isActive() ): - return True + self.repeated_request_status.stop() + if ( + hasattr(self, "_delayed_action_timer") + and self._delayed_action_timer.isActive() + ): + self._delayed_action_timer.stop() + super().closeEvent(event) + + def showEvent(self, event: QtGui.QShowEvent | None) -> None: + """Re-add clients to update list""" self.build_model_list() + self.evaluate_network_state() return super().showEvent(event) def build_model_list(self) -> None: """Builds the model list (`self.model`) containing updatable clients""" self.panel.listView.blockSignals(True) + saved_networks: dict = copy.deepcopy(self.saved_network) self.reset_view_model() - saved_networks: dict = copy.copy(self.saved_network) - if saved_networks.items(): - for ssid, (signal, is_saved) in saved_networks.items(): - self.add_network_entry(ssid=ssid, signal=signal, is_saved=is_saved) - self._setup_scrollbar() - + networks = list(saved_networks.items()) + for ssid, (signal, is_saved) in networks: + self.add_network_entry(ssid=ssid, signal=signal, is_saved=is_saved) + self._setup_scrollbar() self.panel.listView.blockSignals(False) def saved_wifi_option_selected(self): """Handle connect/delete network button clicks""" _sender = self.sender() - self.panel.wifi_button.toggle_button.state = ( - self.panel.wifi_button.toggle_button.State.ON - ) - self.panel.hotspot_button.toggle_button.state = ( - self.panel.hotspot_button.toggle_button.State.OFF + + wifi_toggle = self.panel.wifi_button.toggle_button + hotspot_toggle = self.panel.hotspot_button.toggle_button + + blockers = ( + QtCore.QSignalBlocker(wifi_toggle), + QtCore.QSignalBlocker(hotspot_toggle), ) + wifi_toggle.state = wifi_toggle.State.ON + hotspot_toggle.state = hotspot_toggle.State.OFF + if _sender == self.panel.network_delete_btn: - self.sdbus_network.delete_network( - self.panel.saved_connection_network_name.text() - ) + ssid_to_delete = self.panel.saved_connection_network_name.text() + self.sdbus_network.delete_network(ssid_to_delete) + if ssid_to_delete in self.saved_network: + del self.saved_network[ssid_to_delete] + self.build_model_list() + self.evaluate_network_state() self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) elif _sender == self.panel.network_activate_btn: @@ -626,9 +639,8 @@ def on_toggle_state(self, new_state) -> None: wifi_btn = self.panel.wifi_button.toggle_button hotspot_btn = self.panel.hotspot_button.toggle_button is_sender_now_on = new_state == sender_button.State.ON - _old_hotspot = None - saved_network = self.sdbus_network.get_saved_networks() + saved_network = self.sdbus_network.get_saved_networks_with_for() if sender_button is wifi_btn: if is_sender_now_on: @@ -652,6 +664,7 @@ def on_toggle_state(self, new_state) -> None: elif sender_button is hotspot_btn: if is_sender_now_on: wifi_btn.state = wifi_btn.State.OFF + _old_hotspot = None for n in saved_network: if "ap" in n.get("mode", ""): @@ -696,6 +709,7 @@ def evaluate_network_state(self, nm_state: str = "") -> None: wifi_btn = self.panel.wifi_button.toggle_button hotspot_btn = self.panel.hotspot_button.toggle_button _nm_state = nm_state + _old_hotspot = None if not _nm_state: _nm_state = self.sdbus_network.check_nm_state() @@ -704,7 +718,7 @@ def evaluate_network_state(self, nm_state: str = "") -> None: if self.start: self.start = False - saved_network = self.sdbus_network.get_saved_networks() + saved_network = self.sdbus_network.get_saved_networks_with_for() for n in saved_network: if "ap" in n.get("mode", ""): _old_hotspot = n @@ -747,25 +761,30 @@ def evaluate_network_state(self, nm_state: str = "") -> None: if wifi_btn.state == wifi_btn.State.ON: ipv4_addr = self.sdbus_network.get_current_ip_addr() current_ssid = self.sdbus_network.get_current_ssid() - if current_ssid == "": - return - sec_type = self.sdbus_network.get_security_type_by_ssid(current_ssid) - signal_strength = self.sdbus_network.get_connection_signal_by_ssid( - current_ssid - ) + sec_type = "--" + signal_strength = "--" + curr_ip_text = "No IP Address" + curr_status = "Disconnected" + if current_ssid != "": + sec_type = self.sdbus_network.get_security_type_by_ssid(current_ssid) + signal_strength = self.sdbus_network.get_connection_signal_by_ssid( + current_ssid + ) + curr_ip_text = f"IP: {ipv4_addr}" + curr_status = "Connected" self.panel.netlist_ssuid.setText(current_ssid) - self.panel.netlist_ip.setText(f"IP: {ipv4_addr or 'No IP Address'}") - self.panel.netlist_security.setText(str(sec_type or "--").upper()) + self.panel.netlist_ip.setText(curr_ip_text) + self.panel.netlist_security.setText(str(sec_type).upper()) self.panel.netlist_strength.setText( str(signal_strength if signal_strength != -1 else "--") ) - self.panel.mn_info_box.setText("Connected") + self.panel.mn_info_box.setText(curr_status) self._expand_infobox(False) self.info_box_load(False) self.panel.wifi_button.setEnabled(True) self.panel.hotspot_button.setEnabled(True) - self.repaint() + self.update() if ( wifi_btn.state == wifi_btn.State.OFF @@ -879,7 +898,7 @@ def add_network(self) -> None: """ self.panel.add_network_validation_button.setEnabled(False) - self.panel.add_network_validation_button.repaint() + self.panel.add_network_validation_button.update() if not self.panel.add_network_password_field.text(): self.popup.new_message( @@ -896,7 +915,7 @@ def add_network(self) -> None: error_msg = result.get("error", "") self.panel.add_network_password_field.clear() if not error_msg: - QtCore.QTimer().singleShot(5000, self.network_list_worker.build) + self._schedule_delayed_action(self.network_list_worker.build, 5000) QtCore.QTimer().singleShot( 5000, lambda: self.sdbus_network.connect_network( @@ -910,7 +929,7 @@ def add_network(self) -> None: self.panel.wifi_button.setEnabled(False) self.panel.hotspot_button.setEnabled(False) - self.panel.add_network_validation_button.repaint() + self.panel.add_network_validation_button.update() return if error_msg == "Invalid password": @@ -922,27 +941,18 @@ def add_network(self) -> None: else: message = "Error while adding network. Please try again" self.panel.add_network_validation_button.setEnabled(True) - self.panel.add_network_validation_button.repaint() + self.panel.add_network_validation_button.update() self.popup.new_message(message_type=Popup.MessageType.ERROR, message=message) - @QtCore.pyqtSlot(ListItem, name="ssid_item_clicked") - def ssid_item_clicked(self, item: ListItem) -> None: - """Handles when a network is clicked on the QListWidget. - - Args: - item (QListWidgetItem): The list entry that was clicked - """ - if not item: - return + def _schedule_delayed_action(self, callback, delay_ms: int) -> None: + """Schedule a reschedule timer""" + try: + self._delayed_action_timer.timeout.disconnect() + except TypeError: + pass - _current_ssid_name = item.text - self.selected_item = copy.copy(item) - if _current_ssid_name in self.sdbus_network.get_saved_ssid_names(): - self.setCurrentIndex(self.indexOf(self.panel.saved_connection_page)) - self.panel.saved_connection_network_name.setText(str(_current_ssid_name)) - else: - self.setCurrentIndex(self.indexOf(self.panel.add_network_page)) - self.panel.add_network_network_label.setText(str(_current_ssid_name)) + self._delayed_action_timer.timeout.connect(callback) + self._delayed_action_timer.start(delay_ms) def update_network( self, @@ -965,23 +975,30 @@ def update_network( self.sdbus_network.update_connection_settings( ssid=ssid, password=password, new_ssid=new_ssid, priority=priority ) - QtCore.QTimer().singleShot(10000, lambda: self.network_list_worker.build()) + self._schedule_delayed_action(self.network_list_worker.build, 10000) self.setCurrentIndex(self.indexOf(self.panel.network_list_page)) @QtCore.pyqtSlot(list, name="finished-network-list-build") def handle_network_list(self, data: typing.List[typing.Tuple]) -> None: """Handle available network list update""" + self.saved_network.clear() for entry in data: if entry[0] == self.sdbus_network.hotspot_ssid: continue self.saved_network[entry[0]] = (entry[1], entry[2]) - self.build_model_list() self.evaluate_network_state() - QtCore.QTimer().singleShot(10000, lambda: self.network_list_worker.build()) + self.build_model_list() + self._schedule_delayed_action(self.network_list_worker.build, 10000) + + @QtCore.pyqtSlot(ListItem, name="ssid_item_clicked") + def ssid_item_clicked(self, item: ListItem) -> None: + """Handles when a network is clicked on the QListWidget. - def handle_button_click(self, ssid: str): - """Handles pressing a network""" - _saved_ssids = self.sdbus_network.get_saved_networks() + Args: + item (QListWidgetItem): The list entry that was clicked + """ + ssid = item.text + _saved_ssids = self.sdbus_network.get_saved_networks_with_for() if any(item["ssid"] == ssid for item in _saved_ssids): self.setCurrentIndex(self.indexOf(self.panel.saved_connection_page)) self.panel.saved_connection_network_name.setText(str(ssid)) @@ -1038,11 +1055,6 @@ def setCurrentIndex(self, index: int): _security_type = self.sdbus_network.get_security_type_by_ssid( ssid=self.panel.saved_connection_network_name.text() ) - if not _security_type: - _security_type = "--" - self.panel.saved_connection_security_type_info_label.setText( - str(_security_type) - ) _signal = self.sdbus_network.get_connection_signal_by_ssid( self.panel.saved_connection_network_name.text() ) @@ -1052,7 +1064,10 @@ def setCurrentIndex(self, index: int): self.panel.saved_connection_signal_strength_info_frame.setText( _signal_string ) - self.update() + self.panel.saved_connection_security_type_info_label.setText( + str(_security_type or "--").upper() + ) + self.repaint() super().setCurrentIndex(index) def setProperty(self, name: str, value: typing.Any) -> bool: @@ -1080,25 +1095,39 @@ def show_network_panel( _parent_size = self.parent().size() # type: ignore self.setGeometry(0, 0, _parent_size.width(), _parent_size.height()) self.updateGeometry() - self.update() + self.repaint() self.show() def add_network_entry(self, ssid: str, signal: int, is_saved: str) -> None: """Adds a new item to the list model""" - - wifi_pixmap = self._provider.get_pixmap(signal=signal, state=is_saved) - ssid = ssid if ssid != "" else "UNKNOWN" - item = ListItem( - text=ssid, - left_icon=wifi_pixmap, - right_text=is_saved, - right_icon=self.right_icon, - selected=False, - allow_check=False, - _lfontsize=17, - _rfontsize=12, - height=80, - ) + if signal != -10: + wifi_pixmap = self._provider.get_pixmap(signal=signal, state=is_saved) + ssid = ssid if ssid != "" else "UNKNOWN" + item = ListItem( + text=ssid, + left_icon=wifi_pixmap, + right_text=is_saved, + right_icon=self.right_icon, + selected=False, + allow_check=False, + _lfontsize=17, + _rfontsize=12, + height=80, + not_clickable=False, + ) + else: + item = ListItem( + text="", + left_icon=None, + right_text="", + right_icon=None, + selected=False, + allow_check=False, + _lfontsize=17, + _rfontsize=12, + height=80, + not_clickable=True, + ) self.model.add_item(item) def _handle_scrollbar(self, value): diff --git a/BlocksScreen/lib/utils/list_model.py b/BlocksScreen/lib/utils/list_model.py index 2f4cbc3a..a622f094 100644 --- a/BlocksScreen/lib/utils/list_model.py +++ b/BlocksScreen/lib/utils/list_model.py @@ -19,6 +19,7 @@ class ListItem: _rfontsize: int = 0 height: int = 60 # Change has needed notificate: bool = False # render red dot + not_clickable: bool = False class EntryListModel(QtCore.QAbstractListModel): @@ -59,6 +60,8 @@ def flags(self, index) -> QtCore.Qt.ItemFlag: """Models item flags, re-implemented method""" item = self.entries[index.row()] flags = QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable + if item.not_clickable: + return QtCore.Qt.ItemFlag.NoItemFlags if item.allow_check: flags |= QtCore.Qt.ItemFlag.ItemIsUserCheckable return flags @@ -152,6 +155,10 @@ def paint( path.addRoundedRect(QtCore.QRectF(rect), radius, radius) # Gradient background (left to right) + if item.not_clickable: + painter.restore() + return + if not item.selected: pressed_color = QtGui.QColor("#1A8FBF") pressed_color.setAlpha(20) @@ -327,6 +334,8 @@ def editorEvent( """Capture view model events, re-implemented method""" item = index.data(QtCore.Qt.ItemDataRole.UserRole) if event.type() == QtCore.QEvent.Type.MouseButtonPress: + if item and item.not_clickable: + return True if item.callback: if callable(item.callback): item.callback() From a904b85bc6865d2b926f4b5a224935704cbe0e08 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Wed, 21 Jan 2026 16:32:24 +0000 Subject: [PATCH 19/26] refactor network window file --- BlocksScreen/lib/panels/networkWindow.py | 1935 +++++++++++----------- 1 file changed, 1007 insertions(+), 928 deletions(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 0f72c869..2b64cf5a 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -1,9 +1,17 @@ -import copy import logging -import subprocess # nosec: B404 -import typing +import socket +import threading from functools import partial - +from typing import ( + Any, + Callable, + Dict, + List, + NamedTuple, + Optional, +) + +import psutil from lib.network import SdbusNetworkManagerAsync from lib.panels.widgets.keyboardPage import CustomQwertyKeyboard from lib.panels.widgets.popupDialogWidget import Popup @@ -15,1166 +23,1237 @@ logger = logging.getLogger("logs/BlocksScreen.log") -class NetworkScanRunnable(QRunnable): - """QRunnable task that performs network scanning using SdbusNetworkManagerAsync +LOAD_TIMEOUT_MS = 30_000 +NETWORK_CONNECT_DELAY_MS = 5_000 +NETWORK_LIST_REFRESH_MS = 10_000 +STATUS_CHECK_INTERVAL_MS = 2_000 +DEFAULT_POLL_INTERVAL_MS = 10_000 + +SIGNAL_EXCELLENT_THRESHOLD = 75 +SIGNAL_GOOD_THRESHOLD = 50 +SIGNAL_FAIR_THRESHOLD = 25 +SIGNAL_MINIMUM_THRESHOLD = 5 + +PRIORITY_HIGH = 90 +PRIORITY_MEDIUM = 50 +PRIORITY_LOW = 20 + +SEPARATOR_SIGNAL_VALUE = -10 +PRIVACY_BIT = 1 + - This runnable: - - Triggers a network rescan via SdbusNetworkManagerAsync - - collects SSIDs, signal strenght and saved status - - emits signal with raw scan data and a processed lisgs +class NetworkInfo(NamedTuple): + signal: int + status: str + is_open: bool = False + is_saved: bool = False - Signals: - - scan_results (dict): Emitted with raw scan results mapping SSID to properties - - finished_network_list_build (list): Emitted with processed list of networks - - error (str): Emitted if an error occurs during scanning - """ +class NetworkScanResult(NamedTuple): + ssid: str + signal: int + status: str + is_open: bool = False + + +class NetworkScanRunnable(QRunnable): class Signals(QObject): scan_results = pyqtSignal(dict, name="scan-results") - finished_network_list_build = pyqtSignal( - list, name="finished-network-list-build" - ) + finished_network_list_build = pyqtSignal(list, name="finished-network-list-build") error = pyqtSignal(str) - def __init__(self): + def __init__(self, nm: SdbusNetworkManagerAsync) -> None: super().__init__() - self.nm = SdbusNetworkManagerAsync() + self._nm = nm self.signals = NetworkScanRunnable.Signals() - def run(self): + def run(self) -> None: try: - logger.debug("NetworkScanRunnable: scanning networks") - self.nm.rescan_networks() - saved = self.nm.get_saved_ssid_names() - available = ( - self.nm.get_available_networks() - if self.nm.check_wifi_interface() - else {} - ) - - data_dict: dict[str, dict] = {} - for ssid, props in available.items(): - signal = int(props.get("signal_level", 0)) - data_dict[ssid] = { - "signal_level": signal, - "is_saved": ssid in saved, - } - + 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: list[typing.Union[tuple[str, int, str], str]] = [] - saved_nets = [ - (ssid, info["signal_level"]) - for ssid, info in data_dict.items() - if info["is_saved"] - ] - unsaved_nets = [ - (ssid, info["signal_level"]) - for ssid, info in data_dict.items() - if not info["is_saved"] - ] - saved_nets.sort(key=lambda x: -x[1]) - unsaved_nets.sort(key=lambda x: -x[1]) - - for ssid, sig in saved_nets: - status = "Active" if ssid == self.nm.get_current_ssid() else "Saved" - items.append((ssid, sig, status)) - if saved_nets: - items.append(("separator", -10, "separator")) - - for ssid, sig in unsaved_nets: - items.append((ssid, sig, "Protected")) - + 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]: + if self._nm.check_wifi_interface(): + return self._nm.get_available_networks() or {} + return {} -class BuildNetworkList(QtCore.QObject): - """ - Controller class that schedules and manages repeted network scans - - Uses a QThreadPool to un NetworkScanRunnable tasks periodically. with a QTimer to trigger scans. - Prevents overlapping scans by tracking whether a scan is already in progress. + def _build_data_dict( + self, + available: Dict[str, Dict], + saved_ssids: List[str] + ) -> Dict[str, Dict]: + 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 + data_dict[ssid] = { + "signal_level": signal, + "is_saved": ssid in saved_ssids, + "is_open": is_open, + } + return data_dict + + def _build_network_list(self, data_dict: Dict[str, Dict]) -> List[tuple]: + current_ssid = self._nm.get_current_ssid() + + saved_nets = [ + (ssid, info["signal_level"], info["is_open"]) + for ssid, info in data_dict.items() + if info["is_saved"] + ] + unsaved_nets = [ + (ssid, info["signal_level"], info["is_open"]) + for ssid, info in data_dict.items() + if not info["is_saved"] + ] + + saved_nets.sort(key=lambda x: -x[1]) + unsaved_nets.sort(key=lambda x: -x[1]) + + items: List[tuple] = [] + + for ssid, signal, is_open in saved_nets: + status = "Active" if ssid == current_ssid else "Saved" + items.append((ssid, signal, status, is_open, True)) + + for ssid, signal, is_open in unsaved_nets: + status = "Open" if is_open else "Protected" + items.append((ssid, signal, status, is_open, False)) + + return items - Args: - poll_interval_ms: (int) Milliseconds between scans (default: 10000) - _timer (QtCore.QTimer): Timer that schedules next scan - _is_scanning (bool): Flag indicating if a scan is currently in progress - Signals: - scan_results (dict): Emitted with raw scan results mapping SSID to properties - finished_network_list_build (list): Emitted with processed list of networks - error (str): Emitted if an error occurs during scanning - """ +class BuildNetworkList(QtCore.QObject): scan_results = pyqtSignal(dict, name="scan-results") finished_network_list_build = pyqtSignal(list, name="finished-network-list-build") error = pyqtSignal(str) - def __init__(self, poll_interval_ms: int = 10000): + def __init__( + self, + nm: SdbusNetworkManagerAsync, + poll_interval_ms: int = DEFAULT_POLL_INTERVAL_MS, + ) -> None: super().__init__() - self.threadpool = QThreadPool.globalInstance() - self.poll_interval_ms = poll_interval_ms + 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): - """Schedule scan folowwing the `poll_interval_ms`""" + def start_polling(self) -> None: self._schedule_next_scan() - def stop_polling(self): - """Start Polling for future development""" + def stop_polling(self) -> None: self._timer.stop() - def build(self): - """Method called by rescan button""" + def build(self) -> None: self._do_scan() - def _schedule_next_scan(self): - """Start next rescan timer""" - self._timer.start(self.poll_interval_ms) + def _schedule_next_scan(self) -> None: + self._timer.start(self._poll_interval_ms) - def _on_task_finished(self, items): - """Update data after scan finish, update saved networks list and schedule next scan""" - self._is_scanning = False + def _on_task_finished(self, items: List) -> None: + 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): - """Emit results to main thread""" + def _on_task_scan_results(self, data_dict: Dict) -> None: self.scan_results.emit(data_dict) - def _on_task_error(self, err): - """Emit errors to main thread""" - self._is_scanning = False + def _on_task_error(self, err: str) -> None: + with self._scan_lock: + self._is_scanning = False self.error.emit(err) self._schedule_next_scan() - def _do_scan(self): - """Start wifi scanning threadpool""" - if self._is_scanning: - logger.debug("Already scanning, skip scheduling.") - self._schedule_next_scan() - return + def _do_scan(self) -> None: + with self._scan_lock: + if self._is_scanning: + return + self._is_scanning = True - self._is_scanning = True - task = NetworkScanRunnable() + 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) - logger.debug("Submitted scan task to thread pool") + self._threadpool.start(task) class WifiIconProvider: - """Simple provider: loads QPixmap for WiFi bars + protection without caching.""" - def __init__(self): - self.paths = { + def __init__(self) -> None: + self._paths = { (0, False): ":/network/media/btn_icons/0bar_wifi.svg", - (4, False): ":/network/media/btn_icons/4bar_wifi.svg", - (3, False): ":/network/media/btn_icons/3bar_wifi.svg", - (2, False): ":/network/media/btn_icons/2bar_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", - (4, True): ":/network/media/btn_icons/4bar_wifi_protected.svg", - (3, True): ":/network/media/btn_icons/3bar_wifi_protected.svg", - (2, True): ":/network/media/btn_icons/2bar_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, state: str) -> QtGui.QPixmap: - """Return a QPixmap for the given signal (0-100) and state ("Protected" or not).""" - if signal < 5: - bars = 0 - elif signal >= 75: - bars = 4 - elif signal >= 50: - bars = 3 - elif signal > 25: - bars = 2 - else: - bars = 1 - - is_protected = state == "Protected" + def get_pixmap(self, signal: int, status: str) -> QtGui.QPixmap: + bars = self._signal_to_bars(signal) + is_protected = status == "Protected" key = (bars, is_protected) - - path = self.paths.get(key) - if path is None: - logger.debug( - "No icon path for key %s, falling back to no-signal unprotected", key - ) - path = self.paths[(0, False)] - - pm = QtGui.QPixmap(path) - if pm.isNull(): - logger.debug(f"Failed to load pixmap from '{path}' for key {key}") - return pm + path = self._paths.get(key, self._paths[(0, False)]) + return QtGui.QPixmap(path) + + @staticmethod + def _signal_to_bars(signal: int) -> int: + 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 class NetworkControlWindow(QtWidgets.QStackedWidget): - """Network Control panel Widget""" - - request_network_scan = QtCore.pyqtSignal(name="scan-network") - new_ip_signal = QtCore.pyqtSignal(str, name="ip-address-change") - get_hotspot_ssid = QtCore.pyqtSignal(str, name="hotspot-ssid-name") - delete_network_signal = QtCore.pyqtSignal(str, name="delete-network") - def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: - if parent: - super().__init__(parent) - else: - super().__init__() - - self.panel = Ui_wifi_stacked_page() - self.panel.setupUi(self) - self._provider = WifiIconProvider() - self.ongoing_update: bool = False - - self.popup = Popup(self) - self.sdbus_network = SdbusNetworkManagerAsync() - self.start: bool = True - self.saved_network = {} + 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") + + def __init__(self, parent: Optional[QtWidgets.QWidget] = None, /) -> None: + super().__init__(parent) if parent else super().__init__() + + self._init_instance_variables() + self._init_ui() + self._init_timers() + self._init_model_view() + self._init_network_worker() + self._setup_navigation_signals() + self._setup_action_signals() + self._setup_toggle_signals() + self._setup_password_visibility_signals() + self._setup_icons() + self._setup_input_fields() + self._setup_keyboard() + self._setup_scrollbar_signals() + + self._network_list_worker.build() + self.request_network_scan.emit() + self.hide() + self._set_loading_state(False) + + def _init_instance_variables(self) -> None: + 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._cached_hotspot_ip = "" + self._current_network_is_open = False + + def _init_ui(self) -> None: + self._panel = Ui_wifi_stacked_page() + self._panel.setupUi(self) + self._popup = Popup(self) + self._sdbus_network = SdbusNetworkManagerAsync() + self._right_arrow_icon = QtGui.QPixmap( + ":/arrow_icons/media/btn_icons/right_arrow.svg" + ) - self.repeated_request_status = QtCore.QTimer(self) - self.repeated_request_status.setInterval(2000) + def _init_timers(self) -> None: + 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._load_timer = QtCore.QTimer(self) self._load_timer.setSingleShot(True) self._load_timer.timeout.connect(self._handle_load_timeout) - self.model = EntryListModel() - self.model.setParent(self.panel.listView) - self.entry_delegate = EntryDelegate() - self.panel.listView.setModel(self.model) - self.panel.listView.setItemDelegate(self.entry_delegate) - self.entry_delegate.item_selected.connect(self.ssid_item_clicked) - - self.build_network_list() - self.network_list_worker = BuildNetworkList() - self.network_list_worker.finished_network_list_build.connect( - self.handle_network_list - ) - self.network_list_worker.start_polling() - self.panel.rescan_button.clicked.connect(self.network_list_worker.build) - - self.sdbus_network.nm_state_change.connect(self.evaluate_network_state) - self.panel.wifi_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.panel.network_list_page)) + def _init_model_view(self) -> None: + self._model = EntryListModel() + self._model.setParent(self._panel.listView) + self._entry_delegate = EntryDelegate() + self._panel.listView.setModel(self._model) + self._panel.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: + self._network_list_worker = BuildNetworkList( + nm=self._sdbus_network, + poll_interval_ms=DEFAULT_POLL_INTERVAL_MS ) - self.panel.hotspot_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.panel.hotspot_page)) + self._network_list_worker.finished_network_list_build.connect( + self._handle_network_list ) + self._network_list_worker.start_polling() + self._panel.rescan_button.clicked.connect(self._network_list_worker.build) - self.panel.hotspot_button.setPixmap( - QtGui.QPixmap(":/network/media/btn_icons/hotspot.svg") + def _setup_navigation_signals(self) -> None: + self._panel.wifi_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self._panel.network_list_page)) ) - self.panel.wifi_button.setPixmap( - QtGui.QPixmap(":/network/media/btn_icons/wifi_config.svg") + self._panel.hotspot_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self._panel.hotspot_page)) ) - self.right_icon = QtGui.QPixmap(":/arrow_icons/media/btn_icons/right_arrow.svg") - - self.panel.nl_back_button.clicked.connect( - partial( - self.setCurrentIndex, - self.indexOf(self.panel.main_network_page), - ) + self._panel.nl_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self._panel.main_network_page)) ) + self._panel.network_backButton.clicked.connect(self.hide) - self.panel.network_backButton.clicked.connect(self.hide) - - self.request_network_scan.connect(self.rescan_networks) - self.panel.add_network_validation_button.clicked.connect(self.add_network) - self.panel.add_network_page_backButton.clicked.connect( - partial( - self.setCurrentIndex, - self.indexOf(self.panel.network_list_page), - ) - ) - self.panel.add_network_password_view.pressed.connect( - partial( - self.panel.add_network_password_field.setEchoMode, - QtWidgets.QLineEdit.EchoMode.Normal, - ) - ) - self.panel.add_network_password_view.released.connect( - partial( - self.panel.add_network_password_field.setEchoMode, - QtWidgets.QLineEdit.EchoMode.Password, - ) + self._panel.add_network_page_backButton.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self._panel.network_list_page)) ) - self.panel.saved_connection_back_button.clicked.connect( - partial( - self.setCurrentIndex, - self.indexOf(self.panel.network_list_page), - ) - ) - self.delete_network_signal.connect(self.delete_network) - self.panel.snd_back.clicked.connect( - lambda: self.update_network( - ssid=self.panel.saved_connection_network_name.text(), - password=self.panel.saved_connection_change_password_field.text(), - new_ssid=None, - ) + self._panel.saved_connection_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self._panel.network_list_page)) ) - self.panel.saved_connection_change_password_view.pressed.connect( - partial( - self.panel.saved_connection_change_password_field.setEchoMode, - QtWidgets.QLineEdit.EchoMode.Normal, - ) + self._panel.snd_back.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self._panel.saved_connection_page)) ) - self.panel.saved_connection_change_password_view.released.connect( - partial( - self.panel.saved_connection_change_password_field.setEchoMode, - QtWidgets.QLineEdit.EchoMode.Password, - ) - ) - self.panel.hotspot_back_button.clicked.connect( - lambda: self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) + self._panel.network_details_btn.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self._panel.saved_details_page)) ) - self.panel.hotspot_password_input_field.setPlaceholderText( - "Defaults to: 123456789" + self._panel.hotspot_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self._panel.main_network_page)) ) - self.panel.hotspot_change_confirm.clicked.connect( - lambda: self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) + self._panel.hotspot_change_confirm.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self._panel.main_network_page)) ) - self.panel.hotspot_password_input_field.setHidden(True) - self.panel.hotspot_password_view_button.pressed.connect( - partial(self.panel.hotspot_password_input_field.setHidden, False) - ) - self.panel.hotspot_password_view_button.released.connect( - partial(self.panel.hotspot_password_input_field.setHidden, True) - ) - self.panel.hotspot_name_input_field.setText( - str(self.sdbus_network.get_hotspot_ssid()) - ) - self.panel.hotspot_password_input_field.setText( - str(self.sdbus_network.hotspot_password) + def _setup_action_signals(self) -> None: + 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._panel.add_network_validation_button.clicked.connect(self._add_network) + + self._panel.snd_back.clicked.connect(self._on_save_network_settings) + self._panel.network_activate_btn.clicked.connect(self._on_saved_wifi_option_selected) + self._panel.network_delete_btn.clicked.connect(self._on_saved_wifi_option_selected) + + def _setup_toggle_signals(self) -> None: + self._panel.wifi_button.toggle_button.stateChange.connect(self._on_toggle_state) + self._panel.hotspot_button.toggle_button.stateChange.connect(self._on_toggle_state) + + def _setup_password_visibility_signals(self) -> None: + self._setup_password_visibility_toggle( + self._panel.add_network_password_view, + self._panel.add_network_password_field, ) - self.panel.wifi_button.toggle_button.stateChange.connect(self.on_toggle_state) - self.panel.hotspot_button.toggle_button.stateChange.connect( - self.on_toggle_state + self._setup_password_visibility_toggle( + self._panel.saved_connection_change_password_view, + self._panel.saved_connection_change_password_field, ) - self.panel.saved_connection_change_password_view.pressed.connect( - lambda: self.panel.saved_connection_change_password_view.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/see.svg") - ) + + self._panel.hotspot_password_input_field.setHidden(True) + self._panel.hotspot_password_view_button.pressed.connect( + partial(self._panel.hotspot_password_input_field.setHidden, False) ) - self.panel.saved_connection_change_password_view.released.connect( - lambda: self.panel.saved_connection_change_password_view.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") - ) + self._panel.hotspot_password_view_button.released.connect( + partial(self._panel.hotspot_password_input_field.setHidden, True) ) - self.panel.add_network_password_view.released.connect( - lambda: self.panel.add_network_password_view.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") - ) + + see_icon = QtGui.QPixmap(":/ui/media/btn_icons/see.svg") + unsee_icon = QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") + self._panel.hotspot_password_view_button.pressed.connect( + lambda: self._panel.hotspot_password_view_button.setPixmap(see_icon) ) - self.panel.add_network_password_view.pressed.connect( - lambda: self.panel.add_network_password_view.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/see.svg") - ) + self._panel.hotspot_password_view_button.released.connect( + lambda: self._panel.hotspot_password_view_button.setPixmap(unsee_icon) ) - self.panel.hotspot_password_view_button.released.connect( - lambda: self.panel.hotspot_password_view_button.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") - ) + + def _setup_password_visibility_toggle( + self, + view_button: QtWidgets.QWidget, + password_field: QtWidgets.QLineEdit + ) -> None: + see_icon = QtGui.QPixmap(":/ui/media/btn_icons/see.svg") + unsee_icon = QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") + + view_button.pressed.connect( + lambda: password_field.setEchoMode(QtWidgets.QLineEdit.EchoMode.Normal) ) - self.panel.hotspot_password_view_button.pressed.connect( - lambda: self.panel.hotspot_password_view_button.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/see.svg") - ) + view_button.released.connect( + lambda: password_field.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password) ) + view_button.pressed.connect(lambda: view_button.setPixmap(see_icon)) + view_button.released.connect(lambda: view_button.setPixmap(unsee_icon)) - self.panel.add_network_password_field.setCursor( - QtCore.Qt.CursorShape.BlankCursor + def _setup_icons(self) -> None: + self._panel.hotspot_button.setPixmap( + QtGui.QPixmap(":/network/media/btn_icons/hotspot.svg") ) - self.panel.hotspot_name_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) - self.panel.hotspot_password_input_field.setCursor( - QtCore.Qt.CursorShape.BlankCursor + self._panel.wifi_button.setPixmap( + QtGui.QPixmap(":/network/media/btn_icons/wifi_config.svg") ) - self.panel.network_delete_btn.setPixmap( + self._panel.network_delete_btn.setPixmap( QtGui.QPixmap(":/ui/media/btn_icons/garbage-icon.svg") ) - - self.panel.network_activate_btn.setPixmap( + self._panel.network_activate_btn.setPixmap( QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") ) - - self.panel.network_details_btn.setPixmap( + self._panel.network_details_btn.setPixmap( QtGui.QPixmap(":/ui/media/btn_icons/printer_settings.svg") ) - self.panel.snd_back.clicked.connect( - lambda: self.setCurrentIndex(self.indexOf(self.panel.saved_connection_page)) - ) - self.panel.network_details_btn.clicked.connect( - lambda: self.setCurrentIndex(self.indexOf(self.panel.saved_details_page)) - ) + def _setup_input_fields(self) -> None: + self._panel.add_network_password_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) + self._panel.hotspot_name_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) + self._panel.hotspot_password_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) - self.panel.network_activate_btn.clicked.connect(self.saved_wifi_option_selected) - self.panel.network_delete_btn.clicked.connect(self.saved_wifi_option_selected) - - self.network_list_worker.build() - self.request_network_scan.emit() - self.panel.listView.verticalScrollBar().valueChanged.connect( - self._handle_scrollbar + self._panel.hotspot_password_input_field.setPlaceholderText("Defaults to: 123456789") + self._panel.hotspot_name_input_field.setText( + str(self._sdbus_network.get_hotspot_ssid()) ) - self.panel.verticalScrollBar.valueChanged.connect(self._handle_scrollbar) - self.panel.verticalScrollBar.valueChanged.connect( - lambda value: self.panel.listView.verticalScrollBar().setValue(value) + self._panel.hotspot_password_input_field.setText( + str(self._sdbus_network.hotspot_password) ) - self.panel.verticalScrollBar.show() - self.hide() - self.info_box_load() - 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) + def _setup_keyboard(self) -> None: + 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) - self.panel.add_network_password_field.clicked.connect( - lambda: self.on_show_keyboard( - self.panel.add_network_page, self.panel.add_network_password_field + self._panel.add_network_password_field.clicked.connect( + lambda: self._on_show_keyboard( + self._panel.add_network_page, + self._panel.add_network_password_field ) ) - self.panel.hotspot_password_input_field.clicked.connect( - lambda: self.on_show_keyboard( - self.panel.hotspot_page, self.panel.hotspot_password_input_field + self._panel.hotspot_password_input_field.clicked.connect( + lambda: self._on_show_keyboard( + self._panel.hotspot_page, + self._panel.hotspot_password_input_field ) ) - self.panel.hotspot_name_input_field.clicked.connect( - lambda: self.on_show_keyboard( - self.panel.hotspot_page, self.panel.hotspot_name_input_field + self._panel.hotspot_name_input_field.clicked.connect( + lambda: self._on_show_keyboard( + self._panel.hotspot_page, + self._panel.hotspot_name_input_field ) ) - self.panel.saved_connection_change_password_field.clicked.connect( - lambda: self.on_show_keyboard( - self.panel.saved_connection_page, - self.panel.saved_connection_change_password_field, + self._panel.saved_connection_change_password_field.clicked.connect( + lambda: self._on_show_keyboard( + self._panel.saved_connection_page, + self._panel.saved_connection_change_password_field, ) ) - def handle_update_end(self) -> None: - """Handles update end signal - (closes loading page, returns to normal operation) - """ - self.repeated_request_status.stop() - self.request_network_scan.emit() - self.build_model_list() - - def handle_ongoing_update(self) -> None: - """Handled ongoing update signal, - calls loading page (blocks user interaction) - """ - self.repeated_request_status.start(2000) - - def reset_view_model(self) -> None: - """Clears items from ListView - (Resets `QAbstractListModel` by clearing entries) - """ - self.model.clear() - self.entry_delegate.clear() - - def closeEvent(self, event: QtGui.QShowEvent | None) -> None: - """Clean up resources before closing it""" - if hasattr(self, "_load_timer") and self._load_timer.isActive(): - self._load_timer.stop() + def _setup_scrollbar_signals(self) -> None: + self._panel.listView.verticalScrollBar().valueChanged.connect( + self._handle_scrollbar_change + ) + self._panel.verticalScrollBar.valueChanged.connect(self._handle_scrollbar_change) + self._panel.verticalScrollBar.valueChanged.connect( + lambda value: self._panel.listView.verticalScrollBar().setValue(value) + ) + self._panel.verticalScrollBar.show() - if ( - hasattr(self, "repeated_request_status") - and self.repeated_request_status.isActive() - ): - self.repeated_request_status.stop() + def _configure_list_view_palette(self) -> None: + palette = QtGui.QPalette() - if ( - hasattr(self, "_delayed_action_timer") - and self._delayed_action_timer.isActive() - ): - self._delayed_action_timer.stop() - super().closeEvent(event) + 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) + + 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) + + highlight = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) + highlight.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(group, QtGui.QPalette.ColorRole.Highlight, highlight) + + link = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) + link.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(group, QtGui.QPalette.ColorRole.Link, link) + + self._panel.listView.setPalette(palette) + + def _show_error_popup(self, message: str, timeout: int = 6000) -> None: + self._popup.new_message( + message_type=Popup.MessageType.ERROR, + message=message, + timeout=timeout, + userInput=False, + ) - def showEvent(self, event: QtGui.QShowEvent | None) -> None: - """Re-add clients to update list""" - self.build_model_list() - self.evaluate_network_state() - return super().showEvent(event) - - def build_model_list(self) -> None: - """Builds the model list (`self.model`) containing updatable clients""" - self.panel.listView.blockSignals(True) - saved_networks: dict = copy.deepcopy(self.saved_network) - self.reset_view_model() - networks = list(saved_networks.items()) - for ssid, (signal, is_saved) in networks: - self.add_network_entry(ssid=ssid, signal=signal, is_saved=is_saved) - self._setup_scrollbar() - self.panel.listView.blockSignals(False) - - def saved_wifi_option_selected(self): - """Handle connect/delete network button clicks""" - _sender = self.sender() - - wifi_toggle = self.panel.wifi_button.toggle_button - hotspot_toggle = self.panel.hotspot_button.toggle_button - - blockers = ( - QtCore.QSignalBlocker(wifi_toggle), - QtCore.QSignalBlocker(hotspot_toggle), + def _show_info_popup(self, message: str, timeout: int = 4000) -> None: + self._popup.new_message( + message_type=Popup.MessageType.INFO, + message=message, + timeout=timeout, + userInput=False, ) - wifi_toggle.state = wifi_toggle.State.ON - hotspot_toggle.state = hotspot_toggle.State.OFF - - if _sender == self.panel.network_delete_btn: - ssid_to_delete = self.panel.saved_connection_network_name.text() - self.sdbus_network.delete_network(ssid_to_delete) - if ssid_to_delete in self.saved_network: - del self.saved_network[ssid_to_delete] - self.build_model_list() - self.evaluate_network_state() - self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) - - elif _sender == self.panel.network_activate_btn: - self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) - self.sdbus_network.connect_network( - self.panel.saved_connection_network_name.text() - ) - self.info_box_load(True) - - def on_show_keyboard(self, panel: QtWidgets.QWidget, field: QtWidgets.QLineEdit): - """Handle keyboard show""" - self.previousPanel = panel - self.currentField = field - self.qwerty.set_value(field.text()) - self.setCurrentIndex(self.indexOf(self.qwerty)) - - def on_qwerty_go_back(self): - """Hide keyboard""" - self.setCurrentIndex(self.indexOf(self.previousPanel)) - - def on_qwerty_value_selected(self, value: str): - """Handle keyboard value input""" - self.setCurrentIndex(self.indexOf(self.previousPanel)) - if hasattr(self, "currentField") and self.currentField: - self.currentField.setText(value) - - def info_box_load(self, toggle: bool = False) -> None: - """ - Shows or hides the loading screen. - Sets a 30-second timeout to handle loading failures. - """ - self._show_loadscreen(toggle) + def _show_warning_popup(self, message: str, timeout: int = 5000) -> None: + self._popup.new_message( + message_type=Popup.MessageType.WARNING, + message=message, + timeout=timeout, + userInput=False, + ) - self.panel.wifi_button.setEnabled(not toggle) - self.panel.hotspot_button.setEnabled(not toggle) + def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: + self._stop_all_timers() + self._network_list_worker.stop_polling() + super().closeEvent(event) - if toggle: + def showEvent(self, event: Optional[QtGui.QShowEvent]) -> None: + if self._networks: + self._build_model_list() + self._evaluate_network_state() + super().showEvent(event) + + def _stop_all_timers(self) -> None: + timers = [ + self._load_timer, + self._status_check_timer, + self._delayed_action_timer, + ] + for timer in timers: + if timer.isActive(): + timer.stop() + + def _on_show_keyboard( + self, + panel: QtWidgets.QWidget, + field: QtWidgets.QLineEdit + ) -> None: + self._previous_panel = panel + self._current_field = field + self._qwerty.set_value(field.text()) + self.setCurrentIndex(self.indexOf(self._qwerty)) + + def _on_qwerty_go_back(self) -> None: + if self._previous_panel: + self.setCurrentIndex(self.indexOf(self._previous_panel)) + + def _on_qwerty_value_selected(self, value: str) -> None: + if self._previous_panel: + self.setCurrentIndex(self.indexOf(self._previous_panel)) + if self._current_field: + self._current_field.setText(value) + + def _set_loading_state(self, loading: bool) -> None: + self._set_info_panel_visibility(show_details=not loading, show_loading=loading) + self._panel.wifi_button.setEnabled(not loading) + self._panel.hotspot_button.setEnabled(not loading) + + if loading: + if self._load_timer.isActive(): + self._load_timer.stop() + self._load_timer.start(LOAD_TIMEOUT_MS) + else: if self._load_timer.isActive(): self._load_timer.stop() - self._load_timer.start(30000) - def _handle_load_timeout(self): - """ - Logic to execute if the loading screen is still visible after 30 seconds.< - """ - wifi_btn = self.panel.wifi_button - hotspot_btn = self.panel.hotspot_button + def _handle_load_timeout(self) -> None: + if not self._panel.loadingwidget.isVisible(): + return - if self.panel.loadingwidget.isVisible(): - if wifi_btn.toggle_button.state == wifi_btn.toggle_button.State.ON: - message = "Wi-Fi Connection Failed.\nThe connection attempt \ntimed out.\n Please check\nyour network stability \nor\n try reconnecting." + wifi_btn = self._panel.wifi_button + hotspot_btn = self._panel.hotspot_button - elif hotspot_btn.toggle_button.state == hotspot_btn.toggle_button.State.ON: - message = "Hotspot Setup Failed.\nThe local network sharing\n timed out.\n\n restart the hotspot." - else: - message = "Loading timed out.\n Please check your connection \n and try again." + message = self._get_timeout_message(wifi_btn, hotspot_btn) - self.panel.mn_info_box.setText(message) - self._show_loadscreen(False) - self._expand_infobox(True) + self._panel.mn_info_box.setText(message) + self._set_info_panel_visibility(show_details=False, show_loading=False) + self._configure_info_box_centered() hotspot_btn.setEnabled(True) wifi_btn.setEnabled(True) - def _show_loadscreen(self, toggle: bool = False): - """Expand LOAD box on the main network panel - - Args: - toggle (bool, optional): show or not (Defaults to False) - """ - self.panel.netlist_ip.setVisible(not toggle) - self.panel.netlist_ssuid.setVisible(not toggle) - self.panel.mn_info_seperator.setVisible(not toggle) - self.panel.line_2.setVisible(not toggle) - self.panel.netlist_strength.setVisible(not toggle) - self.panel.netlist_strength_label.setVisible(not toggle) - - self.panel.line_3.setVisible(not toggle) - self.panel.netlist_security.setVisible(not toggle) - self.panel.netlist_security_label.setVisible(not toggle) - - self.panel.mn_info_box.setVisible(not toggle) + def _get_timeout_message(self, wifi_btn, hotspot_btn) -> str: + if wifi_btn.toggle_button.state == wifi_btn.toggle_button.State.ON: + return ( + "Wi-Fi Connection Failed.\nThe connection attempt timed out." + ) + elif hotspot_btn.toggle_button.state == hotspot_btn.toggle_button.State.ON: + return ( + "Hotspot Setup Failed.\n" + "The local network sharing timed out.\n" + "Please restart the hotspot." + ) + else: + return "Loading timed out.\nPlease check your connection and try again." - self.panel.loadingwidget.setVisible(toggle) + def _set_info_panel_visibility( + self, + show_details: bool, + show_loading: bool = False + ) -> None: + self._panel.netlist_ip.setVisible(show_details) + self._panel.netlist_ssuid.setVisible(show_details) + self._panel.mn_info_seperator.setVisible(show_details) + self._panel.line_2.setVisible(show_details) + self._panel.netlist_strength.setVisible(show_details) + self._panel.netlist_strength_label.setVisible(show_details) + self._panel.line_3.setVisible(show_details) + self._panel.netlist_security.setVisible(show_details) + self._panel.netlist_security_label.setVisible(show_details) + self._panel.mn_info_box.setVisible(not show_loading) + self._panel.loadingwidget.setVisible(show_loading) + + def _configure_info_box_centered(self) -> None: + self._panel.mn_info_box.setWordWrap(True) + self._panel.mn_info_box.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) @QtCore.pyqtSlot(object, name="stateChange") - def on_toggle_state(self, new_state) -> None: - """Handle toggle button changes""" + def _on_toggle_state(self, new_state) -> None: sender_button = self.sender() - wifi_btn = self.panel.wifi_button.toggle_button - hotspot_btn = self.panel.hotspot_button.toggle_button + wifi_btn = self._panel.wifi_button.toggle_button + hotspot_btn = self._panel.hotspot_button.toggle_button is_sender_now_on = new_state == sender_button.State.ON - saved_network = self.sdbus_network.get_saved_networks_with_for() + saved_networks = self._sdbus_network.get_saved_networks_with_for() if sender_button is wifi_btn: - if is_sender_now_on: - hotspot_btn.state = hotspot_btn.State.OFF - self.sdbus_network.toggle_hotspot(False) - if saved_network: - try: - ssid = next( - ( - n["ssid"] - for n in saved_network - if "ap" not in n["mode"] and n["signal"] != 0 - ), - None, - ) - self.sdbus_network.connect_network(str(ssid)) - - except Exception as e: - logger.error(f"error when turning ON wifi on_toggle_state:{e}") - + self._handle_wifi_toggle(is_sender_now_on, hotspot_btn, saved_networks) elif sender_button is hotspot_btn: - if is_sender_now_on: - wifi_btn.state = wifi_btn.State.OFF - _old_hotspot = None - - for n in saved_network: - if "ap" in n.get("mode", ""): - _old_hotspot = n - break - - if ( - _old_hotspot - and _old_hotspot["ssid"] - != self.panel.hotspot_name_input_field.text() - ): - self.sdbus_network.delete_network(_old_hotspot["ssid"]) - - self.sdbus_network.create_hotspot( - self.panel.hotspot_name_input_field.text(), - self.panel.hotspot_password_input_field.text(), - ) - self.sdbus_network.toggle_hotspot(True) - self.sdbus_network.connect_network( - self.panel.hotspot_name_input_field.text() - ) + self._handle_hotspot_toggle(is_sender_now_on, wifi_btn, saved_networks) + + self._set_loading_state(False) - self.info_box_load(False) if ( hotspot_btn.state == hotspot_btn.State.OFF and wifi_btn.state == wifi_btn.State.OFF ): - self.evaluate_network_state() + self._evaluate_network_state() else: - self.info_box_load(True) - - @QtCore.pyqtSlot(str, name="nm-state-changed") - def evaluate_network_state(self, nm_state: str = "") -> None: - """Handles or Reloads network state + self._set_loading_state(True) - Args: - nm_state (str, optional): Handles Network state depending on state - """ - # NM State Constants: UNKNOWN=0, ASLEEP=10, DISCONNECTED=20, DISCONNECTING=30, - # CONNECTING=40, CONNECTED_LOCAL=50, CONNECTED_SITE=60, GLOBAL=70 + def _handle_wifi_toggle( + self, + is_on: bool, + hotspot_btn, + saved_networks: List[Dict] + ) -> None: + if not is_on: + return - wifi_btn = self.panel.wifi_button.toggle_button - hotspot_btn = self.panel.hotspot_button.toggle_button - _nm_state = nm_state - _old_hotspot = None + hotspot_btn.state = hotspot_btn.State.OFF + self._sdbus_network.toggle_hotspot(False) + self._clear_hotspot_ip_cache() - if not _nm_state: - _nm_state = self.sdbus_network.check_nm_state() - if not _nm_state: - return + if not saved_networks: + return - if self.start: - self.start = False - saved_network = self.sdbus_network.get_saved_networks_with_for() - for n in saved_network: - if "ap" in n.get("mode", ""): - _old_hotspot = n - break - if _old_hotspot: - self.panel.hotspot_name_input_field.setText(_old_hotspot["ssid"]) - - connection = self.sdbus_network.check_connectivity() - if connection == "FULL": - self.panel.wifi_button.toggle_button.state = ( - self.panel.wifi_button.toggle_button.State.ON - ) - self.panel.hotspot_button.toggle_button.state = ( - self.panel.hotspot_button.toggle_button.State.OFF - ) - if connection == "LIMITED": - self.panel.wifi_button.toggle_button.state = ( - self.panel.wifi_button.toggle_button.State.OFF - ) - self.panel.hotspot_button.toggle_button.state = ( - self.panel.hotspot_button.toggle_button.State.ON - ) + try: + ssid = next( + ( + n["ssid"] + for n in saved_networks + if "ap" not in n.get("mode", "") and n.get("signal", 0) != 0 + ), + None, + ) + if ssid: + self._sdbus_network.connect_network(str(ssid)) + except Exception as e: + logger.error("Error when turning ON wifi: %s",e) - if not self.sdbus_network.check_wifi_interface(): + def _handle_hotspot_toggle( + self, + is_on: bool, + wifi_btn, + saved_networks: List[Dict] + ) -> None: + if not is_on: + self._clear_hotspot_ip_cache() return - if hotspot_btn.state == hotspot_btn.State.ON: - ipv4_addr = self.get_hotspot_ip_via_shell() + wifi_btn.state = wifi_btn.State.OFF - self.panel.netlist_ssuid.setText(self.panel.hotspot_name_input_field.text()) + old_hotspot = self._find_hotspot_in_saved(saved_networks) - self.panel.netlist_ip.setText(f"IP: {ipv4_addr or 'No IP Address'}") + new_hotspot_name = self._panel.hotspot_name_input_field.text() + if old_hotspot and old_hotspot["ssid"] != new_hotspot_name: + self._sdbus_network.delete_network(old_hotspot["ssid"]) - self.panel.netlist_strength.setText("--") + self._sdbus_network.create_hotspot( + new_hotspot_name, + self._panel.hotspot_password_input_field.text(), + ) + self._sdbus_network.toggle_hotspot(True) + try: + self._sdbus_network.connect_network(new_hotspot_name) + except Exception as e: + logger.error(e) - self.panel.netlist_security.setText("--") + def _find_hotspot_in_saved(self, saved_networks: List[Dict]) -> Optional[Dict]: + return next( + (n for n in saved_networks if "ap" in n.get("mode", "")), + None + ) - self.panel.mn_info_box.setText("Hotspot On") + def _find_wifi_networks_in_saved(self, saved_networks: List[Dict]) -> List[Dict]: + return [ + n for n in saved_networks + if "ap" not in n.get("mode", "") and n.get("signal", 0) != 0 + ] - if wifi_btn.state == wifi_btn.State.ON: - ipv4_addr = self.sdbus_network.get_current_ip_addr() - current_ssid = self.sdbus_network.get_current_ssid() - sec_type = "--" - signal_strength = "--" - curr_ip_text = "No IP Address" - curr_status = "Disconnected" - if current_ssid != "": - sec_type = self.sdbus_network.get_security_type_by_ssid(current_ssid) - signal_strength = self.sdbus_network.get_connection_signal_by_ssid( - current_ssid - ) - curr_ip_text = f"IP: {ipv4_addr}" - curr_status = "Connected" - self.panel.netlist_ssuid.setText(current_ssid) - self.panel.netlist_ip.setText(curr_ip_text) - self.panel.netlist_security.setText(str(sec_type).upper()) - self.panel.netlist_strength.setText( - str(signal_strength if signal_strength != -1 else "--") - ) - self.panel.mn_info_box.setText(curr_status) + @QtCore.pyqtSlot(str, name="nm-state-changed") + def _evaluate_network_state(self, nm_state: str = "") -> None: + wifi_btn = self._panel.wifi_button.toggle_button + hotspot_btn = self._panel.hotspot_button.toggle_button + + state = nm_state or self._sdbus_network.check_nm_state() + if not state: + return + + if self._is_first_run: + self._handle_first_run_state() + self._is_first_run = False + + if not self._sdbus_network.check_wifi_interface(): + return + + if hotspot_btn.state == hotspot_btn.State.ON: + self._update_hotspot_display() + elif wifi_btn.state == wifi_btn.State.ON: + self._update_wifi_display() - self._expand_infobox(False) - self.info_box_load(False) - self.panel.wifi_button.setEnabled(True) - self.panel.hotspot_button.setEnabled(True) - self.update() + self._set_info_panel_visibility(show_details=True) + self._set_loading_state(False) + self._panel.wifi_button.setEnabled(True) + self._panel.hotspot_button.setEnabled(True) if ( wifi_btn.state == wifi_btn.State.OFF and hotspot_btn.state == hotspot_btn.State.OFF ): - self.sdbus_network.disconnect_network() - self._expand_infobox(True) - self.panel.mn_info_box.setText( + self._sdbus_network.disconnect_network() + self._set_info_panel_visibility(show_details=False) + self._configure_info_box_centered() + self._panel.mn_info_box.setText( "Network connection required.\n\nConnect to Wi-Fi\nor\nTurn on Hotspot" ) + self.repaint() - def get_hotspot_ip_via_shell(self): - """ - Executes a shell command to retrieve the IPv4 address for a specified interface. + def _handle_first_run_state(self) -> None: + saved_networks = self._sdbus_network.get_saved_networks_with_for() + + old_hotspot = self._find_hotspot_in_saved(saved_networks) + if old_hotspot: + self._panel.hotspot_name_input_field.setText(old_hotspot["ssid"]) + + connection = self._sdbus_network.check_connectivity() + wifi_btn = self._panel.wifi_button.toggle_button + hotspot_btn = self._panel.hotspot_button.toggle_button + + if connection == "FULL": + wifi_btn.state = wifi_btn.State.ON + hotspot_btn.state = hotspot_btn.State.OFF + elif connection == "LIMITED": + wifi_btn.state = wifi_btn.State.OFF + hotspot_btn.state = hotspot_btn.State.ON + + def _update_hotspot_display(self) -> None: + ipv4_addr = self._get_hotspot_ip_via_psutil() + + self._panel.netlist_ssuid.setText(self._panel.hotspot_name_input_field.text()) + self._panel.netlist_ip.setText(f"IP: {ipv4_addr or 'No IP Address'}") + self._panel.netlist_strength.setText("--") + self._panel.netlist_security.setText("--") + self._panel.mn_info_box.setText("Hotspot On") + + def _update_wifi_display(self) -> None: + current_ssid = self._sdbus_network.get_current_ssid() + + 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) + + self._panel.netlist_ssuid.setText(current_ssid) + self._panel.netlist_ip.setText(f"IP: {ipv4_addr}") + self._panel.netlist_security.setText(str(sec_type or "--").upper()) + self._panel.netlist_strength.setText( + str(signal_strength) if signal_strength != -1 else "--" + ) + self._panel.mn_info_box.setText("Connected") + else: + self._panel.netlist_ssuid.setText("") + self._panel.netlist_ip.setText("No IP Address") + self._panel.netlist_security.setText("--") + self._panel.netlist_strength.setText("--") + self._panel.mn_info_box.setText("Disconnected") - Returns: - The IP address string (e.g., '10.42.0.1') or None if not found. + def _get_hotspot_ip_via_psutil(self, iface: str = "wlan0") -> str: + """ + Get the IPv4 address for the given interface using psutil. + Returns an empty string if no address is found. """ - command = ["ip", "-4", "addr", "show", "wlan0"] + if self._cached_hotspot_ip: + return self._cached_hotspot_ip + try: - result = subprocess.run( # nosec: B603 - command, - capture_output=True, - text=True, - check=True, - timeout=5, - ) - except subprocess.CalledProcessError as e: - logging.error( - "Caught exception (exit code %d) failed to run command: %s \nStderr: %s", - e.returncode, - command, - e.stderr.strip(), - ) - return "" - except FileNotFoundError: - logging.error("Command not found") - return "" - except subprocess.TimeoutExpired as e: - logging.error("Caught exception, failed to run command %s", e) - return "" - - for line in result.stdout.splitlines(): - line = line.strip() - if line.startswith("inet "): - ip_address = line.split()[1].split("/")[0] - return ip_address - logging.error("No IPv4 address found in output for wlan0") + addrs = psutil.net_if_addrs().get(iface, []) + for addr in addrs: + # Only look at IPv4 addresses + if addr.family == socket.AF_INET: + ip = addr.address + self._cached_hotspot_ip = ip + return ip + except Exception: + # If psutil isn't installed or something goes wrong, ignore + pass + return "" - def close(self) -> bool: - """Close class, close network module""" - self.sdbus_network.close() - return super().close() + def _clear_hotspot_ip_cache(self) -> None: + self._cached_hotspot_ip = "" - def _expand_infobox(self, toggle: bool = False) -> None: - """Expand information box on the main network panel + @QtCore.pyqtSlot(str, name="delete-network") + def _delete_network(self, ssid: str) -> None: + 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") - Args: - toggle (bool, optional): Expand or not (Defaults to False) - """ - self.panel.netlist_ip.setVisible(not toggle) - self.panel.netlist_ssuid.setVisible(not toggle) - self.panel.mn_info_seperator.setVisible(not toggle) - self.panel.line_2.setVisible(not toggle) - self.panel.netlist_strength.setVisible(not toggle) - self.panel.netlist_strength_label.setVisible(not toggle) + @QtCore.pyqtSlot(name="rescan-networks") + def _rescan_networks(self) -> None: + self._sdbus_network.rescan_networks() - self.panel.line_3.setVisible(not toggle) - self.panel.netlist_security.setVisible(not toggle) - self.panel.netlist_security_label.setVisible(not toggle) + @QtCore.pyqtSlot(name="add-network") + def _add_network(self) -> None: + self._panel.add_network_validation_button.setEnabled(False) + self._panel.add_network_validation_button.update() - self.panel.mn_info_box.setWordWrap(True) - self.panel.mn_info_box.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + password = self._panel.add_network_password_field.text() + ssid = self._panel.add_network_network_label.text() - @QtCore.pyqtSlot(str, name="delete-network") - def delete_network(self, ssid: str) -> None: - """Delete network""" - self.sdbus_network.delete_network(ssid=ssid) + if not password and not self._current_network_is_open: + self._show_error_popup("Password field cannot be empty.") + self._panel.add_network_validation_button.setEnabled(True) + return - @QtCore.pyqtSlot(name="rescan-networks") - def rescan_networks(self) -> None: - """Rescan for networks""" - self.sdbus_network.rescan_networks() + result = self._sdbus_network.add_wifi_network(ssid=ssid, psk=password) + self._panel.add_network_password_field.clear() + + error_msg = result.get("error", "") if result else "" + + if not error_msg: + self._handle_successful_network_add(ssid) + else: + self._handle_failed_network_add(error_msg) + + def _handle_successful_network_add(self, ssid: str) -> None: + self._schedule_delayed_action( + self._network_list_worker.build, + NETWORK_CONNECT_DELAY_MS + ) + QtCore.QTimer.singleShot( + NETWORK_CONNECT_DELAY_MS, + lambda: self._sdbus_network.connect_network(ssid) + ) + + self._set_loading_state(True) + self.setCurrentIndex(self.indexOf(self._panel.main_network_page)) + self._panel.add_network_validation_button.setEnabled(True) + self._panel.wifi_button.setEnabled(False) + self._panel.hotspot_button.setEnabled(False) + self._panel.add_network_validation_button.update() + + def _handle_failed_network_add(self, error_msg: str) -> None: + 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", + } + + message = error_messages.get( + error_msg, + "Error while adding network. Please try again" + ) + + self._panel.add_network_validation_button.setEnabled(True) + self._panel.add_network_validation_button.update() + self._show_error_popup(message) + + def _on_save_network_settings(self) -> None: + self._update_network( + ssid=self._panel.saved_connection_network_name.text(), + password=self._panel.saved_connection_change_password_field.text(), + new_ssid=None, + ) + + def _update_network( + self, + ssid: str, + password: Optional[str], + new_ssid: Optional[str], + ) -> None: + if not self._sdbus_network.is_known(ssid): + return + + priority = self._get_selected_priority() + + 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.setCurrentIndex(self.indexOf(self._panel.network_list_page)) + + def _get_selected_priority(self) -> int: + checked_btn = self._panel.priority_btn_group.checkedButton() + + if checked_btn == self._panel.high_priority_btn: + return PRIORITY_HIGH + elif checked_btn == self._panel.low_priority_btn: + return PRIORITY_LOW + else: + return PRIORITY_MEDIUM + + def _on_saved_wifi_option_selected(self) -> None: + sender = self.sender() + + wifi_toggle = self._panel.wifi_button.toggle_button + hotspot_toggle = self._panel.hotspot_button.toggle_button + + with QtCore.QSignalBlocker(wifi_toggle), QtCore.QSignalBlocker(hotspot_toggle): + wifi_toggle.state = wifi_toggle.State.ON + hotspot_toggle.state = hotspot_toggle.State.OFF + + ssid = self._panel.saved_connection_network_name.text() + + if sender == self._panel.network_delete_btn: + self._handle_network_delete(ssid) + elif sender == self._panel.network_activate_btn: + self._handle_network_activate(ssid) + + def _handle_network_delete(self, ssid: str) -> None: + try: + self._sdbus_network.delete_network(ssid) + if ssid in self._networks: + network_was_saved = self._networks[ssid].is_saved + del self._networks[ssid] + if network_was_saved: + self._network_list_worker.build() + self.setCurrentIndex(self.indexOf(self._panel.network_list_page)) + self._build_model_list() + 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: + self.setCurrentIndex(self.indexOf(self._panel.main_network_page)) + try: + self._sdbus_network.connect_network(ssid) + self._set_loading_state(True) + except Exception as e: + logger.error("Failed to connect to %s: %s",ssid,e) + self._set_loading_state(False) + self._show_error_popup("Failed to connect to network") @QtCore.pyqtSlot(name="handle-hotspot-back") - def handle_hotspot_back(self) -> None: - """Handle go back a page from hotspot page""" + def _handle_hotspot_back(self) -> None: if ( - self.panel.hotspot_password_input_field.text() - != self.sdbus_network.hotspot_password + self._panel.hotspot_password_input_field.text() + != self._sdbus_network.hotspot_password ): - self.panel.hotspot_password_input_field.setText( - self.sdbus_network.hotspot_password + self._panel.hotspot_password_input_field.setText( + self._sdbus_network.hotspot_password ) if ( - self.panel.hotspot_name_input_field.text() - != self.sdbus_network.hotspot_ssid + self._panel.hotspot_name_input_field.text() + != self._sdbus_network.hotspot_ssid ): - self.panel.hotspot_name_input_field.setText(self.sdbus_network.hotspot_ssid) - - self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) + self._panel.hotspot_name_input_field.setText( + self._sdbus_network.hotspot_ssid + ) - @QtCore.pyqtSlot(name="add_network") - def add_network(self) -> None: - """Slot for adding a new network + self.setCurrentIndex(self.indexOf(self._panel.main_network_page)) - Emitted Signals: - - add_network_confirmation(pyqtSignal): Signal with a dict that contains the result of adding a new network to the machine. + @QtCore.pyqtSlot(list, name="finished-network-list-build") + def _handle_network_list(self, data: List[tuple]) -> None: + self._networks.clear() + hotspot_ssid = self._sdbus_network.hotspot_ssid - """ + for entry in data: + if len(entry) >= 5: + ssid, signal, status, is_open, is_saved = entry + elif len(entry) >= 4: + ssid, signal, status, is_open = entry + is_saved = status in ("Active", "Saved") + else: + ssid, signal, status = entry[0], entry[1], entry[2] + is_open = status == "Open" + is_saved = status in ("Active", "Saved") - self.panel.add_network_validation_button.setEnabled(False) - self.panel.add_network_validation_button.update() + if ssid == hotspot_ssid: + continue - if not self.panel.add_network_password_field.text(): - self.popup.new_message( - message_type=Popup.MessageType.ERROR, - message="Password field cannot be empty.", + self._networks[ssid] = NetworkInfo( + signal=signal, + status=status, + is_open=is_open, + is_saved=is_saved ) - return - _network_psk = self.panel.add_network_password_field.text() - result = self.sdbus_network.add_wifi_network( - ssid=self.panel.add_network_network_label.text(), psk=_network_psk - ) + self._build_model_list() - error_msg = result.get("error", "") - self.panel.add_network_password_field.clear() - if not error_msg: - self._schedule_delayed_action(self.network_list_worker.build, 5000) - QtCore.QTimer().singleShot( - 5000, - lambda: self.sdbus_network.connect_network( - self.panel.add_network_network_label.text() - ), - ) - self.info_box_load(True) - self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) - self.panel.add_network_validation_button.setEnabled(True) + def _build_model_list(self) -> None: + self._panel.listView.blockSignals(True) + self._reset_view_model() - self.panel.wifi_button.setEnabled(False) - self.panel.hotspot_button.setEnabled(False) + saved_networks = [] + unsaved_networks = [] - self.panel.add_network_validation_button.update() - return + for ssid, info in self._networks.items(): + if info.is_saved: + saved_networks.append((ssid, info)) + else: + unsaved_networks.append((ssid, info)) - if error_msg == "Invalid password": - message = "Invalid password. Please try again" - elif error_msg == "Network connection properties error": - message = "Network connection properties error. Please try again" - elif error_msg == "Permission Denied": - message = "Permission Denied. Please try again" - else: - message = "Error while adding network. Please try again" - self.panel.add_network_validation_button.setEnabled(True) - self.panel.add_network_validation_button.update() - self.popup.new_message(message_type=Popup.MessageType.ERROR, message=message) + saved_networks.sort(key=lambda x: -x[1].signal) + unsaved_networks.sort(key=lambda x: -x[1].signal) - def _schedule_delayed_action(self, callback, delay_ms: int) -> None: - """Schedule a reschedule timer""" - try: - self._delayed_action_timer.timeout.disconnect() - except TypeError: - pass + for ssid, info in saved_networks: + self._add_network_entry( + ssid=ssid, + signal=info.signal, + status=info.status, + is_open=info.is_open + ) - self._delayed_action_timer.timeout.connect(callback) - self._delayed_action_timer.start(delay_ms) + if saved_networks and unsaved_networks: + self._add_separator_entry() - def update_network( + for ssid, info in unsaved_networks: + self._add_network_entry( + ssid=ssid, + signal=info.signal, + status=info.status, + is_open=info.is_open + ) + + self._sync_scrollbar() + self._panel.listView.blockSignals(False) + self._panel.listView.update() + + def _reset_view_model(self) -> None: + self._model.clear() + self._entry_delegate.clear() + + def _add_separator_entry(self) -> None: + 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_network_entry( self, ssid: str, - password: typing.Union[str, None], - new_ssid: typing.Union[str, None], + signal: int, + status: str, + is_open: bool = False ) -> None: - """Update network information""" - if not self.sdbus_network.is_known(ssid): + wifi_pixmap = self._icon_provider.get_pixmap(signal=signal, status=status) + display_ssid = ssid if ssid else "UNKNOWN" + 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, + ) + self._model.add_item(item) + + @QtCore.pyqtSlot(ListItem, name="ssid-item-clicked") + def _on_ssid_item_clicked(self, item: ListItem) -> None: + ssid = item.text + if not ssid: return - checked_btn = self.panel.priority_btn_group.checkedButton() - if checked_btn == self.panel.high_priority_btn: - priority = 90 - elif checked_btn == self.panel.low_priority_btn: - priority = 20 + network_info = self._networks.get(ssid) + if network_info is None: + return + + if network_info.is_saved: + saved_networks = self._sdbus_network.get_saved_networks_with_for() + self._show_saved_network_page(ssid, saved_networks) else: - priority = 50 + self._show_add_network_page(ssid, is_open=network_info.is_open) - self.sdbus_network.update_connection_settings( - ssid=ssid, password=password, new_ssid=new_ssid, priority=priority - ) - self._schedule_delayed_action(self.network_list_worker.build, 10000) - self.setCurrentIndex(self.indexOf(self.panel.network_list_page)) + def _show_saved_network_page(self, ssid: str, saved_networks: List[Dict]) -> None: + self._panel.saved_connection_network_name.setText(str(ssid)) + self._panel.snd_name.setText(str(ssid)) - @QtCore.pyqtSlot(list, name="finished-network-list-build") - def handle_network_list(self, data: typing.List[typing.Tuple]) -> None: - """Handle available network list update""" - self.saved_network.clear() - for entry in data: - if entry[0] == self.sdbus_network.hotspot_ssid: - continue - self.saved_network[entry[0]] = (entry[1], entry[2]) - self.evaluate_network_state() - self.build_model_list() - self._schedule_delayed_action(self.network_list_worker.build, 10000) + entry = next((net for net in saved_networks if net["ssid"] == ssid), None) - @QtCore.pyqtSlot(ListItem, name="ssid_item_clicked") - def ssid_item_clicked(self, item: ListItem) -> None: - """Handles when a network is clicked on the QListWidget. + if entry is not None: + self._set_priority_button(entry.get("priority")) - Args: - item (QListWidgetItem): The list entry that was clicked - """ - ssid = item.text - _saved_ssids = self.sdbus_network.get_saved_networks_with_for() - if any(item["ssid"] == ssid for item in _saved_ssids): - self.setCurrentIndex(self.indexOf(self.panel.saved_connection_page)) - self.panel.saved_connection_network_name.setText(str(ssid)) - self.panel.snd_name.setText(str(ssid)) - - # find the entry for this SSID - entry = next((item for item in _saved_ssids if item["ssid"] == ssid), None) - - logger.debug(_saved_ssids) - - if entry is not None: - priority = entry.get("priority") - - if priority == 90: - self.panel.high_priority_btn.setChecked(True) - elif priority == 20: - self.panel.low_priority_btn.setChecked(True) - else: - self.panel.med_priority_btn.setChecked(True) - _curr_ssid = self.sdbus_network.get_current_ssid() - if _curr_ssid != str(ssid): - self.panel.network_activate_btn.setDisabled(False) - self.panel.sn_info.setText("Saved Network") + network_info = self._networks.get(ssid) + if network_info: + signal_text = f"{network_info.signal}%" if network_info.signal >= 0 else "--%" + self._panel.saved_connection_signal_strength_info_frame.setText(signal_text) + + if network_info.is_open: + self._panel.saved_connection_security_type_info_label.setText("OPEN") else: - self.panel.network_activate_btn.setDisabled(True) - self.panel.sn_info.setText("Active Network") + sec_type = self._sdbus_network.get_security_type_by_ssid(ssid) + self._panel.saved_connection_security_type_info_label.setText( + str(sec_type or "WPA").upper() + ) + else: + self._panel.saved_connection_signal_strength_info_frame.setText("--%") + self._panel.saved_connection_security_type_info_label.setText("--") + + current_ssid = self._sdbus_network.get_current_ssid() + if current_ssid != ssid: + self._panel.network_activate_btn.setDisabled(False) + self._panel.sn_info.setText("Saved Network") + else: + self._panel.network_activate_btn.setDisabled(True) + self._panel.sn_info.setText("Active Network") - self.panel.frame.repaint() + self.setCurrentIndex(self.indexOf(self._panel.saved_connection_page)) + self._panel.frame.repaint() + def _set_priority_button(self, priority: Optional[int]) -> None: + if priority == PRIORITY_HIGH: + self._panel.high_priority_btn.setChecked(True) + elif priority == PRIORITY_LOW: + self._panel.low_priority_btn.setChecked(True) else: - self.setCurrentIndex(self.indexOf(self.panel.add_network_page)) - self.panel.add_network_network_label.setText(str(ssid)) + self._panel.med_priority_btn.setChecked(True) - def setCurrentIndex(self, index: int): - """Re-implementation of the QStackedWidget setCurrentIndex method - in order to clear and display text as needed for each panel on the StackedWidget - Args: - index (int): The index we want to change to + def _show_add_network_page(self, ssid: str, is_open: bool = False) -> None: + self._current_network_is_open = is_open + self._panel.add_network_network_label.setText(str(ssid)) + self.setCurrentIndex(self.indexOf(self._panel.add_network_page)) - """ + def _set_add_network_page_password_visibility(self, show_password: bool) -> None: + self._panel.frame_2.setVisible(show_password) + + def _handle_scrollbar_change(self, value: int) -> None: + self._panel.verticalScrollBar.blockSignals(True) + self._panel.verticalScrollBar.setValue(value) + self._panel.verticalScrollBar.blockSignals(False) + + def _sync_scrollbar(self) -> None: + list_scrollbar = self._panel.listView.verticalScrollBar() + self._panel.verticalScrollBar.setMinimum(list_scrollbar.minimum()) + self._panel.verticalScrollBar.setMaximum(list_scrollbar.maximum()) + self._panel.verticalScrollBar.setPageStep(list_scrollbar.pageStep()) + + def _schedule_delayed_action(self, callback: Callable, delay_ms: int) -> None: + 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: + self._network_list_worker.stop_polling() + self._sdbus_network.close() + return super().close() + + def setCurrentIndex(self, index: int) -> None: if not self.isVisible(): return - _cur = self.currentIndex() - if index == self.indexOf(self.panel.add_network_page): - self.panel.add_network_password_field.clear() - self.panel.add_network_password_field.setPlaceholderText( - "Insert password here, press enter when finished." - ) - elif index == self.indexOf(self.panel.saved_connection_page): - self.panel.saved_connection_change_password_field.clear() - self.panel.saved_connection_change_password_field.setPlaceholderText( - "Change network password" - ) - _security_type = self.sdbus_network.get_security_type_by_ssid( - ssid=self.panel.saved_connection_network_name.text() - ) - _signal = self.sdbus_network.get_connection_signal_by_ssid( - self.panel.saved_connection_network_name.text() - ) - if _signal == -1: - _signal = "--" - _signal_string = f"{_signal}%" - self.panel.saved_connection_signal_strength_info_frame.setText( - _signal_string - ) - self.panel.saved_connection_security_type_info_label.setText( - str(_security_type or "--").upper() - ) + + if index == self.indexOf(self._panel.add_network_page): + self._setup_add_network_page() + elif index == self.indexOf(self._panel.saved_connection_page): + self._setup_saved_connection_page() + self.repaint() super().setCurrentIndex(index) - def setProperty(self, name: str, value: typing.Any) -> bool: - """setProperty-> Intercept the set property method + def _setup_add_network_page(self) -> None: + self._panel.add_network_password_field.clear() + + if self._current_network_is_open: + self._panel.frame_2.setVisible(False) + self._panel.add_network_validation_button.setText("Connect") + else: + self._panel.frame_2.setVisible(True) + self._panel.add_network_password_field.setPlaceholderText( + "Insert password here, press enter when finished." + ) + self._panel.add_network_validation_button.setText("Activate") - Args: - name (str): Name of the dynamic property - value (typing.Any): Value for the dynamic property + def _setup_saved_connection_page(self) -> None: + self._panel.saved_connection_change_password_field.clear() + self._panel.saved_connection_change_password_field.setPlaceholderText( + "Change network password" + ) - Returns: - bool: Returns to the super class - """ + def setProperty(self, name: str, value: Any) -> bool: if name == "backgroundPixmap": - self.background = value + self._background = value return super().setProperty(name, value) @QtCore.pyqtSlot(name="call-network-panel") - def show_network_panel( - self, - ) -> None: - """Slot for displaying networkWindow Panel""" + def show_network_panel(self) -> None: if not self.parent(): return - self.setCurrentIndex(self.indexOf(self.panel.network_list_page)) - _parent_size = self.parent().size() # type: ignore - self.setGeometry(0, 0, _parent_size.width(), _parent_size.height()) + + self.setCurrentIndex(self.indexOf(self._panel.network_list_page)) + parent_size = self.parent().size() + self.setGeometry(0, 0, parent_size.width(), parent_size.height()) self.updateGeometry() self.repaint() - self.show() - - def add_network_entry(self, ssid: str, signal: int, is_saved: str) -> None: - """Adds a new item to the list model""" - if signal != -10: - wifi_pixmap = self._provider.get_pixmap(signal=signal, state=is_saved) - ssid = ssid if ssid != "" else "UNKNOWN" - item = ListItem( - text=ssid, - left_icon=wifi_pixmap, - right_text=is_saved, - right_icon=self.right_icon, - selected=False, - allow_check=False, - _lfontsize=17, - _rfontsize=12, - height=80, - not_clickable=False, - ) - else: - item = ListItem( - text="", - left_icon=None, - right_text="", - right_icon=None, - selected=False, - allow_check=False, - _lfontsize=17, - _rfontsize=12, - height=80, - not_clickable=True, - ) - self.model.add_item(item) - - def _handle_scrollbar(self, value): - self.panel.verticalScrollBar.blockSignals(True) - self.panel.verticalScrollBar.setValue(value) - self.panel.verticalScrollBar.blockSignals(False) - - def _setup_scrollbar(self) -> None: - self.panel.verticalScrollBar.setMinimum( - self.panel.listView.verticalScrollBar().minimum() - ) - self.panel.verticalScrollBar.setMaximum( - self.panel.listView.verticalScrollBar().maximum() - ) - self.panel.verticalScrollBar.setPageStep( - self.panel.listView.verticalScrollBar().pageStep() - ) - - def build_network_list(self) -> None: - """Build available/saved network list with optimized palette setup.""" - - def set_brush_for_all_groups( - palette, role, color, style=QtCore.Qt.BrushStyle.SolidPattern - ): - """Helper to set a brush for Active, Inactive, and Disabled states.""" - brush = QtGui.QBrush(QtGui.QColor(*color)) - brush.setStyle(style) - for group in [ - QtGui.QPalette.ColorGroup.Active, - QtGui.QPalette.ColorGroup.Inactive, - QtGui.QPalette.ColorGroup.Disabled, - ]: - palette.setBrush(group, role, brush) - - palette = QtGui.QPalette() - - set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Button, (0, 0, 0, 0)) - set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Window, (0, 0, 0, 0)) - - set_brush_for_all_groups( - palette, - QtGui.QPalette.ColorRole.Base, - (0, 0, 0), - QtCore.Qt.BrushStyle.NoBrush, - ) - set_brush_for_all_groups( - palette, QtGui.QPalette.ColorRole.Highlight, (0, 120, 215, 0) - ) - set_brush_for_all_groups(palette, QtGui.QPalette.ColorRole.Link, (0, 0, 255, 0)) - self.panel.listView.setPalette(palette) + self.show() \ No newline at end of file From 0d523780483ad4b88d9fa5bf47e11cbac45c4886 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Tue, 3 Feb 2026 12:31:46 +0000 Subject: [PATCH 20/26] Add hidden network page, fix scrollbar behaviour at borders remove the need to use wificonnectivitywindow_ui --- BlocksScreen/lib/network.py | 62 + BlocksScreen/lib/panels/networkWindow.py | 2792 +++++++++++++++++--- BlocksScreen/lib/utils/blocks_Scrollbar.py | 38 +- BlocksScreen/lib/utils/blocks_linedit.py | 220 +- 4 files changed, 2637 insertions(+), 475 deletions(-) diff --git a/BlocksScreen/lib/network.py b/BlocksScreen/lib/network.py index 51b87074..7fc63aa4 100644 --- a/BlocksScreen/lib/network.py +++ b/BlocksScreen/lib/network.py @@ -472,6 +472,68 @@ def get_current_ip_addr(self) -> str: 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( diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 2b64cf5a..02170307 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -1,5 +1,4 @@ import logging -import socket import threading from functools import partial from typing import ( @@ -11,11 +10,17 @@ Optional, ) -import psutil from lib.network import SdbusNetworkManagerAsync from lib.panels.widgets.keyboardPage import CustomQwertyKeyboard +from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.panels.widgets.popupDialogWidget import Popup -from lib.ui.wifiConnectivityWindow_ui import Ui_wifi_stacked_page +from lib.utils.blocks_button import BlocksCustomButton +from lib.utils.blocks_frame import BlocksCustomFrame +from lib.utils.blocks_linedit import BlocksCustomLinEdit +from lib.utils.blocks_Scrollbar import CustomScrollBar +from lib.utils.blocks_togglebutton import NetworkWidgetbuttons +from lib.utils.check_button import BlocksCustomCheckButton +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 @@ -41,15 +46,23 @@ 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 @@ -57,18 +70,25 @@ class NetworkScanResult(NamedTuple): 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") + 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() def run(self) -> None: + """Execute the network scan.""" try: self._nm.rescan_networks() saved_ssids = self._nm.get_saved_ssid_names() @@ -82,38 +102,42 @@ def run(self) -> None: 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] + 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 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() saved_nets = [ - (ssid, info["signal_level"], info["is_open"]) + (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"]) + (ssid, info["signal_level"], info["is_open"], info.get("is_hidden", False)) for ssid, info in data_dict.items() if not info["is_saved"] ] @@ -123,18 +147,19 @@ def _build_network_list(self, data_dict: Dict[str, Dict]) -> List[tuple]: items: List[tuple] = [] - for ssid, signal, is_open in saved_nets: + 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)) + items.append((ssid, signal, status, is_open, True, is_hidden)) - for ssid, signal, is_open in unsaved_nets: + 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)) + items.append((ssid, signal, status, is_open, False, is_hidden)) return items class BuildNetworkList(QtCore.QObject): + """Worker class for building network lists with polling support.""" scan_results = pyqtSignal(dict, name="scan-results") finished_network_list_build = pyqtSignal(list, name="finished-network-list-build") @@ -145,6 +170,7 @@ def __init__( nm: SdbusNetworkManagerAsync, poll_interval_ms: int = DEFAULT_POLL_INTERVAL_MS, ) -> None: + """Initialize the network list builder.""" super().__init__() self._nm = nm self._threadpool = QThreadPool.globalInstance() @@ -156,33 +182,41 @@ def __init__( 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 @@ -196,8 +230,10 @@ def _do_scan(self) -> None: 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", @@ -212,6 +248,7 @@ def __init__(self) -> None: } 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) @@ -220,6 +257,7 @@ def get_pixmap(self, signal: int, status: str) -> QtGui.QPixmap: @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: @@ -233,6 +271,7 @@ def _signal_to_bars(signal: int) -> int: class NetworkControlWindow(QtWidgets.QStackedWidget): + """Main network control window widget.""" request_network_scan = pyqtSignal(name="scan-network") new_ip_signal = pyqtSignal(str, name="ip-address-change") @@ -240,10 +279,11 @@ class NetworkControlWindow(QtWidgets.QStackedWidget): delete_network_signal = pyqtSignal(str, name="delete-network") def __init__(self, parent: Optional[QtWidgets.QWidget] = None, /) -> None: + """Initialize the network control window.""" super().__init__(parent) if parent else super().__init__() self._init_instance_variables() - self._init_ui() + self._setupUI() self._init_timers() self._init_model_view() self._init_network_worker() @@ -259,28 +299,1635 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None, /) -> None: self._network_list_worker.build() self.request_network_scan.emit() self.hide() - self._set_loading_state(False) + + # Initialize UI state + self._init_ui_state() + + def _init_ui_state(self) -> None: + """Initialize UI to a clean disconnected state.""" + self.loadingwidget.setVisible(False) + 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" + ) + + 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_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._cached_hotspot_ip = "" 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 + ) + + def _setupUI(self) -> None: + """Setup all UI elements programmatically.""" + 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" + ) - def _init_ui(self) -> None: - self._panel = Ui_wifi_stacked_page() - self._panel.setupUi(self) - self._popup = Popup(self) self._sdbus_network = SdbusNetworkManagerAsync() + self._popup = Popup(self) self._right_arrow_icon = QtGui.QPixmap( ":/arrow_icons/media/btn_icons/right_arrow.svg" ) + # 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.setCurrentIndex(0) + + 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) + + 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) + self.main_network_page.setObjectName("main_network_page") + + main_layout = QtWidgets.QVBoxLayout(self.main_network_page) + main_layout.setObjectName("verticalLayout_14") + + # Header layout + header_layout = QtWidgets.QHBoxLayout() + header_layout.setObjectName("main_network_header_layout") + + 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") + self.network_main_title.setObjectName("network_main_title") + 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.setText("") + self.network_backButton.setFlat(True) + self.network_backButton.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + ) + self.network_backButton.setObjectName("network_backButton") + header_layout.addWidget(self.network_backButton) + + main_layout.addLayout(header_layout) + + # Content layout + content_layout = QtWidgets.QHBoxLayout() + content_layout.setObjectName("main_network_content_layout") + + # 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") + + info_layout = QtWidgets.QVBoxLayout(self.mn_information_layout) + info_layout.setObjectName("verticalLayout_3") + + # 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) + + # 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) + + # 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) + + # Connection info layout + conn_info_layout = QtWidgets.QHBoxLayout() + conn_info_layout.setObjectName("mn_conn_info") + + # Signal strength section + sg_info_layout = QtWidgets.QVBoxLayout() + sg_info_layout.setObjectName("mn_sg_info_layout") + + 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) + + 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) + + 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) + + conn_info_layout.addLayout(sg_info_layout) + + # Security section + sec_info_layout = QtWidgets.QVBoxLayout() + sec_info_layout.setObjectName("mn_sec_info_layout") + + self.netlist_security_label = QtWidgets.QLabel( + parent=self.mn_information_layout + ) + self.netlist_security_label.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + 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) + font = QtGui.QFont() + font.setPointSize(11) + self.netlist_security.setFont(font) + 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() + font.setPointSize(17) + 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." + ) + self.mn_info_box.setObjectName("mn_info_box") + 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( + QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum + ) + 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( + 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.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.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) + + content_layout.addLayout(option_layout) + main_layout.addLayout(content_layout) + + self.addWidget(self.main_network_page) + + 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)) + self.rescan_button.setMaximumSize(QtCore.QSize(60, 60)) + self.rescan_button.setText("Reload") + self.rescan_button.setFlat(True) + self.rescan_button.setProperty( + "icon_pixmap", QtGui.QPixmap(":/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) + 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) + self.nl_back_button.setMinimumSize(QtCore.QSize(60, 60)) + self.nl_back_button.setMaximumSize(QtCore.QSize(60, 60)) + 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") + ) + 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( + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + ) + list_policy.setHorizontalStretch(1) + list_policy.setVerticalStretch(1) + self.listView.setSizePolicy(list_policy) + self.listView.setMinimumSize(QtCore.QSize(0, 0)) + self.listView.setStyleSheet("background-color: rgba(255, 255, 255, 0);") + self.listView.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.listView.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) + self.listView.setVerticalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.listView.setHorizontalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.listView.setSelectionBehavior( + QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems + ) + self.listView.setVerticalScrollMode( + QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel + ) + self.listView.setEditTriggers( + QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers + ) + self.listView.setUniformItemSizes(True) + self.listView.setSpacing(5) + + # Setup touch scrolling + QtWidgets.QScroller.grabGesture( + self.listView, + QtWidgets.QScroller.ScrollerGestureType.TouchGesture, + ) + QtWidgets.QScroller.grabGesture( + self.listView, + QtWidgets.QScroller.ScrollerGestureType.LeftMouseButtonGesture, + ) + + scroller_instance = QtWidgets.QScroller.scroller(self.listView) + scroller_props = scroller_instance.scrollerProperties() + scroller_props.setScrollMetric( + QtWidgets.QScrollerProperties.ScrollMetric.DragVelocitySmoothingFactor, + 0.05, + ) + scroller_props.setScrollMetric( + QtWidgets.QScrollerProperties.ScrollMetric.DecelerationFactor, + 0.4, + ) + QtWidgets.QScroller.scroller(self.listView).setScrollerProperties( + scroller_props + ) + + self.verticalScrollBar = CustomScrollBar(parent=self.network_list_page) + scrollbar_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Minimum + ) + 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 + ) + + self.listView.setVerticalScrollBar(self.verticalScrollBar) + self.listView.setVerticalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded + ) + + list_layout.addWidget(self.listView) + list_layout.addWidget(self.verticalScrollBar) + + main_layout.addLayout(list_layout) + + self.scroller = QtWidgets.QScroller.scroller(self.listView) + + self.addWidget(self.network_list_page) + + 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( + 40, + 60, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + + self.add_network_network_label = QtWidgets.QLabel(parent=self.add_network_page) + label_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Preferred, + ) + self.add_network_network_label.setSizePolicy(label_policy) + self.add_network_network_label.setMinimumSize(QtCore.QSize(0, 60)) + self.add_network_network_label.setMaximumSize(QtCore.QSize(16777215, 60)) + font = QtGui.QFont() + font.setPointSize(20) + self.add_network_network_label.setFont(font) + 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) + self.add_network_page_backButton.setMinimumSize(QtCore.QSize(60, 60)) + self.add_network_page_backButton.setMaximumSize(QtCore.QSize(60, 60)) + 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") + ) + 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( + 20, + 50, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + + # Password frame + self.frame_2 = BlocksCustomFrame(parent=self.add_network_page) + frame_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum + ) + frame_policy.setVerticalStretch(2) + self.frame_2.setSizePolicy(frame_policy) + self.frame_2.setMinimumSize(QtCore.QSize(0, 80)) + 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()) + font = QtGui.QFont() + font.setPointSize(15) + self.add_network_password_label.setFont(font) + self.add_network_password_label.setAlignment( + 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( + parent=frame_layout_widget + ) + self.add_network_password_field.setHidden(True) + self.add_network_password_field.setMinimumSize(QtCore.QSize(500, 60)) + 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) + self.add_network_password_view.setMinimumSize(QtCore.QSize(60, 60)) + self.add_network_password_view.setMaximumSize(QtCore.QSize(60, 60)) + 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") + ) + 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) + + content_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 150, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + + # 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.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 _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 "" + + 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) + + 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( + 60, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + + self.saved_connection_network_name = QtWidgets.QLabel( + parent=self.saved_connection_page + ) + name_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.saved_connection_network_name.setSizePolicy(name_policy) + self.saved_connection_network_name.setMaximumSize(QtCore.QSize(16777215, 60)) + font = QtGui.QFont() + font.setPointSize(20) + self.saved_connection_network_name.setFont(font) + self.saved_connection_network_name.setStyleSheet("color: rgb(255, 255, 255);") + self.saved_connection_network_name.setText("") + self.saved_connection_network_name.setAlignment( + QtCore.Qt.AlignmentFlag.AlignCenter + ) + self.saved_connection_network_name.setObjectName( + "saved_connection_network_name" + ) + header_layout.addWidget(self.saved_connection_network_name) + + self.saved_connection_back_button = IconButton( + parent=self.saved_connection_page + ) + 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") + ) + 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( + 20, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + + # 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( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.frame.setSizePolicy(frame_policy) + 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()) + font = QtGui.QFont() + font.setPointSize(15) + 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( + parent=self.frame + ) + self.saved_connection_signal_strength_info_frame.setMinimumSize( + QtCore.QSize(250, 0) + ) + font = QtGui.QFont() + font.setPointSize(11) + self.saved_connection_signal_strength_info_frame.setFont(font) + self.saved_connection_signal_strength_info_frame.setStyleSheet( + "color: rgb(255, 255, 255);" + ) + 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) + + 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()) + font = QtGui.QFont() + font.setPointSize(15) + 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) + ) + self.saved_connection_change_password_field.setMaximumSize( + QtCore.QSize(500, 16777215) + ) + 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" + ) + password_layout.addWidget( + self.saved_connection_change_password_field, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter, + ) + + 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, + ) + + content_layout.addWidget(self.frame_9) + + # Priority buttons layout + priority_outer_layout = QtWidgets.QHBoxLayout() + priority_outer_layout.setObjectName("horizontalLayout_13") + + priority_inner_layout = QtWidgets.QVBoxLayout() + priority_inner_layout.setObjectName("verticalLayout_13") + + 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") + + frame_inner_layout = QtWidgets.QVBoxLayout(self.frame_12) + frame_inner_layout.setObjectName("verticalLayout_17") + + frame_inner_layout.addItem( + QtWidgets.QSpacerItem( + 10, + 10, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + + # Priority buttons + buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.setObjectName("horizontalLayout_4") + + self.priority_btn_group = QtWidgets.QButtonGroup(self) + self.priority_btn_group.setObjectName("priority_btn_group") + + 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.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") + ) + 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.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) + + priority_inner_layout.addWidget( + self.frame_12, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + + priority_outer_layout.addLayout(priority_inner_layout) + content_layout.addLayout(priority_outer_layout) + main_layout.addLayout(content_layout) + + self.addWidget(self.saved_details_page) + + def _setup_hotspot_page(self) -> None: + """Setup the hotspot configuration page.""" + self.hotspot_page = QtWidgets.QWidget() + self.hotspot_page.setObjectName("hotspot_page") + + main_layout = QtWidgets.QVBoxLayout(self.hotspot_page) + main_layout.setObjectName("verticalLayout_12") + + # 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, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + + # Hotspot name frame + self.frame_6 = BlocksCustomFrame(parent=self.hotspot_page) + frame_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") + + name_layout = QtWidgets.QHBoxLayout(frame_layout_widget) + name_layout.setContentsMargins(0, 0, 0, 0) + name_layout.setObjectName("horizontalLayout_11") + + 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 + ) + + name_layout.addItem( + QtWidgets.QSpacerItem( + 60, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + + content_layout.addWidget(self.frame_6) + + content_layout.addItem( + QtWidgets.QSpacerItem( + 773, + 128, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + + # Hotspot password frame + self.frame_7 = BlocksCustomFrame(parent=self.hotspot_page) + frame_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, 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") + + 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") + + password_layout = QtWidgets.QHBoxLayout(password_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.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( + 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.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, + 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) @@ -292,189 +1939,189 @@ def _init_timers(self) -> None: 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._panel.listView) + self._model.setParent(self.listView) self._entry_delegate = EntryDelegate() - self._panel.listView.setModel(self._model) - self._panel.listView.setItemDelegate(self._entry_delegate) + 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 + 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._panel.rescan_button.clicked.connect(self._network_list_worker.build) + self.rescan_button.clicked.connect(self._network_list_worker.build) def _setup_navigation_signals(self) -> None: - self._panel.wifi_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self._panel.network_list_page)) + """Setup navigation button signals.""" + self.wifi_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) ) - self._panel.hotspot_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self._panel.hotspot_page)) + self.hotspot_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.hotspot_page)) ) - self._panel.nl_back_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self._panel.main_network_page)) + self.nl_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) ) - self._panel.network_backButton.clicked.connect(self.hide) + self.network_backButton.clicked.connect(self.hide) - self._panel.add_network_page_backButton.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self._panel.network_list_page)) + self.add_network_page_backButton.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) ) - self._panel.saved_connection_back_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self._panel.network_list_page)) + self.saved_connection_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) ) - self._panel.snd_back.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self._panel.saved_connection_page)) + self.snd_back.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.saved_connection_page)) ) - self._panel.network_details_btn.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self._panel.saved_details_page)) + self.network_details_btn.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.saved_details_page)) ) - self._panel.hotspot_back_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self._panel.main_network_page)) + self.hotspot_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) ) - self._panel.hotspot_change_confirm.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self._panel.main_network_page)) + self.hotspot_change_confirm.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) ) 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._panel.add_network_validation_button.clicked.connect(self._add_network) + self.add_network_validation_button.clicked.connect(self._add_network) - self._panel.snd_back.clicked.connect(self._on_save_network_settings) - self._panel.network_activate_btn.clicked.connect(self._on_saved_wifi_option_selected) - self._panel.network_delete_btn.clicked.connect(self._on_saved_wifi_option_selected) + 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: - self._panel.wifi_button.toggle_button.stateChange.connect(self._on_toggle_state) - self._panel.hotspot_button.toggle_button.stateChange.connect(self._on_toggle_state) + """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._panel.add_network_password_view, - self._panel.add_network_password_field, + self.add_network_password_view, + self.add_network_password_field, ) self._setup_password_visibility_toggle( - self._panel.saved_connection_change_password_view, - self._panel.saved_connection_change_password_field, - ) - - self._panel.hotspot_password_input_field.setHidden(True) - self._panel.hotspot_password_view_button.pressed.connect( - partial(self._panel.hotspot_password_input_field.setHidden, False) - ) - self._panel.hotspot_password_view_button.released.connect( - partial(self._panel.hotspot_password_input_field.setHidden, True) - ) - - see_icon = QtGui.QPixmap(":/ui/media/btn_icons/see.svg") - unsee_icon = QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") - self._panel.hotspot_password_view_button.pressed.connect( - lambda: self._panel.hotspot_password_view_button.setPixmap(see_icon) + self.saved_connection_change_password_view, + self.saved_connection_change_password_field, ) - self._panel.hotspot_password_view_button.released.connect( - lambda: self._panel.hotspot_password_view_button.setPixmap(unsee_icon) + 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 + 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") - view_button.pressed.connect( - lambda: password_field.setEchoMode(QtWidgets.QLineEdit.EchoMode.Normal) + # Connect toggle signal + view_button.toggled.connect( + lambda checked: password_field.setHidden(not checked) ) - view_button.released.connect( - lambda: password_field.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password) + + # Update icon based on toggle state + view_button.toggled.connect( + lambda checked: view_button.setPixmap( + unsee_icon if not checked else see_icon + ) ) - view_button.pressed.connect(lambda: view_button.setPixmap(see_icon)) - view_button.released.connect(lambda: view_button.setPixmap(unsee_icon)) def _setup_icons(self) -> None: - self._panel.hotspot_button.setPixmap( + """Setup button icons.""" + self.hotspot_button.setPixmap( QtGui.QPixmap(":/network/media/btn_icons/hotspot.svg") ) - self._panel.wifi_button.setPixmap( + self.wifi_button.setPixmap( QtGui.QPixmap(":/network/media/btn_icons/wifi_config.svg") ) - self._panel.network_delete_btn.setPixmap( + self.network_delete_btn.setPixmap( QtGui.QPixmap(":/ui/media/btn_icons/garbage-icon.svg") ) - self._panel.network_activate_btn.setPixmap( + self.network_activate_btn.setPixmap( QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") ) - self._panel.network_details_btn.setPixmap( + self.network_details_btn.setPixmap( QtGui.QPixmap(":/ui/media/btn_icons/printer_settings.svg") ) def _setup_input_fields(self) -> None: - self._panel.add_network_password_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) - self._panel.hotspot_name_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) - self._panel.hotspot_password_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) - - self._panel.hotspot_password_input_field.setPlaceholderText("Defaults to: 123456789") - self._panel.hotspot_name_input_field.setText( - str(self._sdbus_network.get_hotspot_ssid()) + """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._panel.hotspot_password_input_field.setText( - str(self._sdbus_network.hotspot_password) + self.hotspot_password_input_field.setText( + str(self._sdbus_network.hotspot_password or "123456789") ) 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) - self._panel.add_network_password_field.clicked.connect( + self.add_network_password_field.clicked.connect( lambda: self._on_show_keyboard( - self._panel.add_network_page, - self._panel.add_network_password_field + self.add_network_page, self.add_network_password_field ) ) - self._panel.hotspot_password_input_field.clicked.connect( + self.hotspot_password_input_field.clicked.connect( lambda: self._on_show_keyboard( - self._panel.hotspot_page, - self._panel.hotspot_password_input_field + self.hotspot_page, self.hotspot_password_input_field ) ) - self._panel.hotspot_name_input_field.clicked.connect( + self.hotspot_name_input_field.clicked.connect( lambda: self._on_show_keyboard( - self._panel.hotspot_page, - self._panel.hotspot_name_input_field + self.hotspot_page, self.hotspot_name_input_field ) ) - self._panel.saved_connection_change_password_field.clicked.connect( + self.saved_connection_change_password_field.clicked.connect( lambda: self._on_show_keyboard( - self._panel.saved_connection_page, - self._panel.saved_connection_change_password_field, + self.saved_connection_page, + self.saved_connection_change_password_field, ) ) def _setup_scrollbar_signals(self) -> None: - self._panel.listView.verticalScrollBar().valueChanged.connect( + """Setup scrollbar synchronization signals.""" + self.listView.verticalScrollBar().valueChanged.connect( self._handle_scrollbar_change ) - self._panel.verticalScrollBar.valueChanged.connect(self._handle_scrollbar_change) - self._panel.verticalScrollBar.valueChanged.connect( - lambda value: self._panel.listView.verticalScrollBar().setValue(value) + self.verticalScrollBar.valueChanged.connect(self._handle_scrollbar_change) + self.verticalScrollBar.valueChanged.connect( + lambda value: self.listView.verticalScrollBar().setValue(value) ) - self._panel.verticalScrollBar.show() + self.verticalScrollBar.show() def _configure_list_view_palette(self) -> None: + """Configure the list view palette for transparency.""" palette = QtGui.QPalette() for group in [ @@ -499,9 +2146,11 @@ def _configure_list_view_palette(self) -> None: link.setStyle(QtCore.Qt.BrushStyle.SolidPattern) palette.setBrush(group, QtGui.QPalette.ColorRole.Link, link) - self._panel.listView.setPalette(palette) + self.listView.setPalette(palette) 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, @@ -510,6 +2159,8 @@ def _show_error_popup(self, message: str, timeout: int = 6000) -> None: ) 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, @@ -518,6 +2169,8 @@ def _show_info_popup(self, message: str, timeout: int = 4000) -> None: ) 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, @@ -526,17 +2179,20 @@ def _show_warning_popup(self, message: str, timeout: int = 5000) -> None: ) def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: + """Handle close event.""" self._stop_all_timers() self._network_list_worker.stop_polling() super().closeEvent(event) 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) def _stop_all_timers(self) -> None: + """Stop all active timers.""" timers = [ self._load_timer, self._status_check_timer, @@ -547,96 +2203,241 @@ def _stop_all_timers(self) -> None: timer.stop() def _on_show_keyboard( - self, - panel: QtWidgets.QWidget, - field: QtWidgets.QLineEdit + 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)) def _on_qwerty_go_back(self) -> None: + """Handle keyboard back button.""" if self._previous_panel: self.setCurrentIndex(self.indexOf(self._previous_panel)) 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) def _set_loading_state(self, loading: bool) -> None: - self._set_info_panel_visibility(show_details=not loading, show_loading=loading) - self._panel.wifi_button.setEnabled(not loading) - self._panel.hotspot_button.setEnabled(not loading) + """Set loading state - controls loading widget visibility. + + 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) 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 + QtWidgets.QApplication.processEvents() + # Now show loading + self.loadingwidget.setVisible(True) + 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) + if self._load_timer.isActive(): self._load_timer.stop() + if self._status_check_timer.isActive(): + self._status_check_timer.stop() + + 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 + QtWidgets.QApplication.processEvents() + + # 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 _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 + QtWidgets.QApplication.processEvents() + + # 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" + ) + + 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) + + 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 + + connectivity = self._sdbus_network.check_connectivity() + is_connected = connectivity in ("FULL", "LIMITED") + + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_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 + + 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("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 def _handle_load_timeout(self) -> None: - if not self._panel.loadingwidget.isVisible(): + """Handle connection timeout.""" + if not self.loadingwidget.isVisible(): return - wifi_btn = self._panel.wifi_button - hotspot_btn = self._panel.hotspot_button + connectivity = self._sdbus_network.check_connectivity() + is_connected = connectivity in ("FULL", "LIMITED") + + wifi_btn = self.wifi_button + hotspot_btn = self.hotspot_button + + # 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() + + 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 + + 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 - message = self._get_timeout_message(wifi_btn, hotspot_btn) + # Connection actually failed + self._is_connecting = False + self._target_ssid = None + self._set_loading_state(False) - self._panel.mn_info_box.setText(message) - self._set_info_panel_visibility(show_details=False, show_loading=False) + # 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)) hotspot_btn.setEnabled(True) wifi_btn.setEnabled(True) + self._show_error_popup("Connection timed out. Please try again.") + 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 timed out." - ) + 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.\n" - "The local network sharing timed out.\n" - "Please restart the hotspot." - ) + return "Hotspot Setup Failed.\nPlease restart the hotspot." else: - return "Loading timed out.\nPlease check your connection and try again." - - def _set_info_panel_visibility( - self, - show_details: bool, - show_loading: bool = False - ) -> None: - self._panel.netlist_ip.setVisible(show_details) - self._panel.netlist_ssuid.setVisible(show_details) - self._panel.mn_info_seperator.setVisible(show_details) - self._panel.line_2.setVisible(show_details) - self._panel.netlist_strength.setVisible(show_details) - self._panel.netlist_strength_label.setVisible(show_details) - self._panel.line_3.setVisible(show_details) - self._panel.netlist_security.setVisible(show_details) - self._panel.netlist_security_label.setVisible(show_details) - self._panel.mn_info_box.setVisible(not show_loading) - self._panel.loadingwidget.setVisible(show_loading) + return "Loading timed out.\nPlease check your connection\n and try again." def _configure_info_box_centered(self) -> None: - self._panel.mn_info_box.setWordWrap(True) - self._panel.mn_info_box.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + """Configure info box for centered text.""" + self.mn_info_box.setWordWrap(True) + self.mn_info_box.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + 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 @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._panel.wifi_button.toggle_button - hotspot_btn = self._panel.hotspot_button.toggle_button + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button is_sender_now_on = new_state == sender_button.State.ON + # Show loading IMMEDIATELY when turning something on + if is_sender_now_on: + self._set_loading_state(True) + QtWidgets.QApplication.processEvents() + saved_networks = self._sdbus_network.get_saved_networks_with_for() if sender_button is wifi_btn: @@ -644,90 +2445,92 @@ def _on_toggle_state(self, new_state) -> None: elif sender_button is hotspot_btn: self._handle_hotspot_toggle(is_sender_now_on, wifi_btn, saved_networks) - self._set_loading_state(False) - + # Handle both OFF if ( hotspot_btn.state == hotspot_btn.State.OFF and wifi_btn.state == wifi_btn.State.OFF ): - self._evaluate_network_state() - else: - self._set_loading_state(True) + self._set_loading_state(False) + self._show_disconnected_message() def _handle_wifi_toggle( - self, - is_on: bool, - hotspot_btn, - saved_networks: List[Dict] + 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 hotspot_btn.state = hotspot_btn.State.OFF self._sdbus_network.toggle_hotspot(False) - self._clear_hotspot_ip_cache() - if not saved_networks: + # Check if already connected + current_ssid = self._sdbus_network.get_current_ssid() + connectivity = self._sdbus_network.check_connectivity() + + 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 + + # Filter wifi networks (not hotspots) + wifi_networks = [ + n for n in saved_networks if "ap" not in str(n.get("mode", "")) + ] + + if not wifi_networks: + self._set_loading_state(False) + self._show_warning_popup( + "No saved Wi-Fi networks. Please add a network first." + ) + self._show_disconnected_message() return try: - ssid = next( - ( - n["ssid"] - for n in saved_networks - if "ap" not in n.get("mode", "") and n.get("signal", 0) != 0 - ), - None, - ) - if ssid: - self._sdbus_network.connect_network(str(ssid)) + 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) + 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] + self, is_on: bool, wifi_btn, saved_networks: List[Dict] ) -> None: + """Handle hotspot toggle state change.""" if not is_on: - self._clear_hotspot_ip_cache() + self._target_ssid = None return wifi_btn.state = wifi_btn.State.OFF + self._target_ssid = None - old_hotspot = self._find_hotspot_in_saved(saved_networks) + new_hotspot_name = self.hotspot_name_input_field.text() or "PrinterHotspot" + new_hotspot_password = self.hotspot_password_input_field.text() or "123456789" - new_hotspot_name = self._panel.hotspot_name_input_field.text() - if old_hotspot and old_hotspot["ssid"] != new_hotspot_name: - self._sdbus_network.delete_network(old_hotspot["ssid"]) - - self._sdbus_network.create_hotspot( - new_hotspot_name, - self._panel.hotspot_password_input_field.text(), - ) - self._sdbus_network.toggle_hotspot(True) - try: - self._sdbus_network.connect_network(new_hotspot_name) - except Exception as e: - logger.error(e) - - def _find_hotspot_in_saved(self, saved_networks: List[Dict]) -> Optional[Dict]: - return next( - (n for n in saved_networks if "ap" in n.get("mode", "")), - None - ) + # 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) - def _find_wifi_networks_in_saved(self, saved_networks: List[Dict]) -> List[Dict]: - return [ - n for n in saved_networks - if "ap" not in n.get("mode", "") and n.get("signal", 0) != 0 - ] + QtCore.QTimer.singleShot(100, setup_hotspot) @QtCore.pyqtSlot(str, name="nm-state-changed") def _evaluate_network_state(self, nm_state: str = "") -> None: - wifi_btn = self._panel.wifi_button.toggle_button - hotspot_btn = self._panel.hotspot_button.toggle_button + """Evaluate and update network state.""" + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button state = nm_state or self._sdbus_network.check_nm_state() if not state: @@ -736,135 +2539,185 @@ def _evaluate_network_state(self, nm_state: str = "") -> None: if self._is_first_run: self._handle_first_run_state() self._is_first_run = False + return if not self._sdbus_network.check_wifi_interface(): return - if hotspot_btn.state == hotspot_btn.State.ON: - self._update_hotspot_display() - elif wifi_btn.state == wifi_btn.State.ON: - self._update_wifi_display() - - self._set_info_panel_visibility(show_details=True) - self._set_loading_state(False) - self._panel.wifi_button.setEnabled(True) - self._panel.hotspot_button.setEnabled(True) - + # 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._set_info_panel_visibility(show_details=False) - self._configure_info_box_centered() - self._panel.mn_info_box.setText( - "Network connection required.\n\nConnect to Wi-Fi\nor\nTurn on Hotspot" - ) - self.repaint() + self._clear_network_display() + self._set_loading_state(False) + self._show_disconnected_message() + return + + connectivity = self._sdbus_network.check_connectivity() + is_connected = connectivity in ("FULL", "LIMITED") + + # 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 + + # 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() def _handle_first_run_state(self) -> None: + """Handle initial state on first run.""" saved_networks = self._sdbus_network.get_saved_networks_with_for() - old_hotspot = self._find_hotspot_in_saved(saved_networks) + old_hotspot = next( + (n for n in saved_networks if "ap" in str(n.get("mode", ""))), None + ) if old_hotspot: - self._panel.hotspot_name_input_field.setText(old_hotspot["ssid"]) + self.hotspot_name_input_field.setText(old_hotspot["ssid"]) - connection = self._sdbus_network.check_connectivity() - wifi_btn = self._panel.wifi_button.toggle_button - hotspot_btn = self._panel.hotspot_button.toggle_button + 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() - if connection == "FULL": - wifi_btn.state = wifi_btn.State.ON - hotspot_btn.state = hotspot_btn.State.OFF - elif connection == "LIMITED": - wifi_btn.state = wifi_btn.State.OFF - hotspot_btn.state = hotspot_btn.State.ON + self._is_connecting = False + self.loadingwidget.setVisible(False) + + 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: - ipv4_addr = self._get_hotspot_ip_via_psutil() + """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() - self._panel.netlist_ssuid.setText(self._panel.hotspot_name_input_field.text()) - self._panel.netlist_ip.setText(f"IP: {ipv4_addr or 'No IP Address'}") - self._panel.netlist_strength.setText("--") - self._panel.netlist_security.setText("--") - self._panel.mn_info_box.setText("Hotspot On") + 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 def _update_wifi_display(self) -> None: + """Update display for wifi connection.""" current_ssid = self._sdbus_network.get_current_ssid() 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) + signal_strength = self._sdbus_network.get_connection_signal_by_ssid( + current_ssid + ) - self._panel.netlist_ssuid.setText(current_ssid) - self._panel.netlist_ip.setText(f"IP: {ipv4_addr}") - self._panel.netlist_security.setText(str(sec_type or "--").upper()) - self._panel.netlist_strength.setText( - str(signal_strength) if signal_strength != -1 else "--" + 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 "--" ) - self._panel.mn_info_box.setText("Connected") + self._last_displayed_ssid = current_ssid else: - self._panel.netlist_ssuid.setText("") - self._panel.netlist_ip.setText("No IP Address") - self._panel.netlist_security.setText("--") - self._panel.netlist_strength.setText("--") - self._panel.mn_info_box.setText("Disconnected") - - def _get_hotspot_ip_via_psutil(self, iface: str = "wlan0") -> str: - """ - Get the IPv4 address for the given interface using psutil. - Returns an empty string if no address is found. - """ - if self._cached_hotspot_ip: - return self._cached_hotspot_ip - - try: - addrs = psutil.net_if_addrs().get(iface, []) - for addr in addrs: - # Only look at IPv4 addresses - if addr.family == socket.AF_INET: - ip = addr.address - self._cached_hotspot_ip = ip - return ip - except Exception: - # If psutil isn't installed or something goes wrong, ignore - pass - - return "" - - def _clear_hotspot_ip_cache(self) -> None: - self._cached_hotspot_ip = "" + self._clear_network_display() @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) + logger.error("Failed to delete network %s: %s", ssid, e) self._show_error_popup("Failed to delete network") @QtCore.pyqtSlot(name="rescan-networks") def _rescan_networks(self) -> None: + """Trigger network rescan.""" self._sdbus_network.rescan_networks() @QtCore.pyqtSlot(name="add-network") def _add_network(self) -> None: - self._panel.add_network_validation_button.setEnabled(False) - self._panel.add_network_validation_button.update() + """Add a new network.""" + self.add_network_validation_button.setEnabled(False) + self.add_network_validation_button.update() - password = self._panel.add_network_password_field.text() - ssid = self._panel.add_network_network_label.text() + password = self.add_network_password_field.text() + ssid = self.add_network_network_label.text() if not password and not self._current_network_is_open: self._show_error_popup("Password field cannot be empty.") - self._panel.add_network_validation_button.setEnabled(True) + self.add_network_validation_button.setEnabled(True) return result = self._sdbus_network.add_wifi_network(ssid=ssid, psk=password) - self._panel.add_network_password_field.clear() + self.add_network_password_field.clear() + + if result is None: + self._handle_failed_network_add("Failed to add network") + return - error_msg = result.get("error", "") if result else "" + error_msg = result.get("error", "") if isinstance(result, dict) else "" if not error_msg: self._handle_successful_network_add(ssid) @@ -872,23 +2725,39 @@ def _add_network(self) -> None: self._handle_failed_network_add(error_msg) 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)) + + 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 + self._schedule_delayed_action( - self._network_list_worker.build, - NETWORK_CONNECT_DELAY_MS - ) - QtCore.QTimer.singleShot( - NETWORK_CONNECT_DELAY_MS, - lambda: self._sdbus_network.connect_network(ssid) + self._network_list_worker.build, NETWORK_CONNECT_DELAY_MS ) - self._set_loading_state(True) - self.setCurrentIndex(self.indexOf(self._panel.main_network_page)) - self._panel.add_network_validation_button.setEnabled(True) - self._panel.wifi_button.setEnabled(False) - self._panel.hotspot_button.setEnabled(False) - self._panel.add_network_validation_button.update() + 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) + + QtCore.QTimer.singleShot(NETWORK_CONNECT_DELAY_MS, connect_and_refresh) + + self.add_network_validation_button.setEnabled(True) + self.wifi_button.setEnabled(False) + self.hotspot_button.setEnabled(False) + self.add_network_validation_button.update() 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": ( @@ -898,18 +2767,18 @@ def _handle_failed_network_add(self, error_msg: str) -> None: } message = error_messages.get( - error_msg, - "Error while adding network. Please try again" + error_msg, "Error while adding network. Please try again" ) - self._panel.add_network_validation_button.setEnabled(True) - self._panel.add_network_validation_button.update() + self.add_network_validation_button.setEnabled(True) + self.add_network_validation_button.update() self._show_error_popup(message) def _on_save_network_settings(self) -> None: + """Save network settings.""" self._update_network( - ssid=self._panel.saved_connection_network_name.text(), - password=self._panel.saved_connection_change_password_field.text(), + ssid=self.saved_connection_network_name.text(), + password=self.saved_connection_change_password_field.text(), new_ssid=None, ) @@ -919,6 +2788,7 @@ def _update_network( password: Optional[str], new_ssid: Optional[str], ) -> None: + """Update network settings.""" if not self._sdbus_network.is_known(ssid): return @@ -926,103 +2796,96 @@ def _update_network( try: self._sdbus_network.update_connection_settings( - ssid=ssid, - password=password, - new_ssid=new_ssid, - priority=priority + ssid=ssid, password=password, new_ssid=new_ssid, priority=priority ) except Exception as e: - logger.error("Failed to update network settings: %s",e) + logger.error("Failed to update network settings: %s", e) self._show_error_popup("Failed to update network settings") - self.setCurrentIndex(self.indexOf(self._panel.network_list_page)) + self.setCurrentIndex(self.indexOf(self.network_list_page)) def _get_selected_priority(self) -> int: - checked_btn = self._panel.priority_btn_group.checkedButton() + """Get selected priority from radio buttons.""" + checked_btn = self.priority_btn_group.checkedButton() - if checked_btn == self._panel.high_priority_btn: + if checked_btn == self.high_priority_btn: return PRIORITY_HIGH - elif checked_btn == self._panel.low_priority_btn: + elif checked_btn == self.low_priority_btn: return PRIORITY_LOW else: return PRIORITY_MEDIUM def _on_saved_wifi_option_selected(self) -> None: + """Handle saved wifi option selection.""" sender = self.sender() - wifi_toggle = self._panel.wifi_button.toggle_button - hotspot_toggle = self._panel.hotspot_button.toggle_button + wifi_toggle = self.wifi_button.toggle_button + hotspot_toggle = self.hotspot_button.toggle_button with QtCore.QSignalBlocker(wifi_toggle), QtCore.QSignalBlocker(hotspot_toggle): wifi_toggle.state = wifi_toggle.State.ON hotspot_toggle.state = hotspot_toggle.State.OFF - ssid = self._panel.saved_connection_network_name.text() + ssid = self.saved_connection_network_name.text() - if sender == self._panel.network_delete_btn: + if sender == self.network_delete_btn: self._handle_network_delete(ssid) - elif sender == self._panel.network_activate_btn: + elif sender == self.network_activate_btn: self._handle_network_activate(ssid) def _handle_network_delete(self, ssid: str) -> None: + """Handle network deletion.""" try: self._sdbus_network.delete_network(ssid) if ssid in self._networks: - network_was_saved = self._networks[ssid].is_saved del self._networks[ssid] - if network_was_saved: - self._network_list_worker.build() - self.setCurrentIndex(self.indexOf(self._panel.network_list_page)) + 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) + 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: - self.setCurrentIndex(self.indexOf(self._panel.main_network_page)) + """Handle network activation.""" + self._target_ssid = ssid + # Show loading IMMEDIATELY + self._set_loading_state(True) + QtWidgets.QApplication.processEvents() + + self.setCurrentIndex(self.indexOf(self.main_network_page)) + try: self._sdbus_network.connect_network(ssid) - self._set_loading_state(True) except Exception as e: - logger.error("Failed to connect to %s: %s",ssid,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(name="handle-hotspot-back") - def _handle_hotspot_back(self) -> None: - if ( - self._panel.hotspot_password_input_field.text() - != self._sdbus_network.hotspot_password - ): - self._panel.hotspot_password_input_field.setText( - self._sdbus_network.hotspot_password - ) - if ( - self._panel.hotspot_name_input_field.text() - != self._sdbus_network.hotspot_ssid - ): - self._panel.hotspot_name_input_field.setText( - self._sdbus_network.hotspot_ssid - ) - - self.setCurrentIndex(self.indexOf(self._panel.main_network_page)) - @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: - if len(entry) >= 5: + # 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) if ssid == hotspot_ssid: continue @@ -1031,13 +2894,37 @@ def _handle_network_list(self, data: List[tuple]) -> None: signal=signal, status=status, is_open=is_open, - is_saved=is_saved + is_saved=is_saved, + is_hidden=is_hidden, ) self._build_model_list() + # 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 "--" + ) + + 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: - self._panel.listView.blockSignals(True) + """Build the network list model.""" + self.listView.blockSignals(True) self._reset_view_model() saved_networks = [] @@ -1057,7 +2944,8 @@ def _build_model_list(self) -> None: ssid=ssid, signal=info.signal, status=info.status, - is_open=info.is_open + is_open=info.is_open, + is_hidden=info.is_hidden, ) if saved_networks and unsaved_networks: @@ -1068,18 +2956,24 @@ def _build_model_list(self) -> None: ssid=ssid, signal=info.signal, status=info.status, - is_open=info.is_open + is_open=info.is_open, + is_hidden=info.is_hidden, ) + # Add "Connect to Hidden Network" entry at the end + self._add_hidden_network_entry() + self._sync_scrollbar() - self._panel.listView.blockSignals(False) - self._panel.listView.update() + 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, @@ -1094,15 +2988,40 @@ def _add_separator_entry(self) -> None: ) 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._model.add_item(item) + def _add_network_entry( self, ssid: str, signal: int, status: str, - is_open: bool = False + 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) - display_ssid = ssid if ssid else "UNKNOWN" + + # 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 + item = ListItem( text=display_ssid, left_icon=wifi_pixmap, @@ -1113,18 +3032,32 @@ def _add_network_entry( _lfontsize=17, _rfontsize=12, height=80, - not_clickable=False, + not_clickable=False, # All entries are clickable ) 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 - if not ssid: + + # 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 if network_info.is_saved: @@ -1134,69 +3067,106 @@ def _on_ssid_item_clicked(self, item: ListItem) -> None: self._show_add_network_page(ssid, is_open=network_info.is_open) def _show_saved_network_page(self, ssid: str, saved_networks: List[Dict]) -> None: - self._panel.saved_connection_network_name.setText(str(ssid)) - self._panel.snd_name.setText(str(ssid)) - - entry = next((net for net in saved_networks if net["ssid"] == ssid), 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 + + # 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) - if entry is not None: - self._set_priority_button(entry.get("priority")) + 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._panel.saved_connection_signal_strength_info_frame.setText(signal_text) + signal_text = ( + f"{network_info.signal}%" if network_info.signal >= 0 else "--%" + ) + self.saved_connection_signal_strength_info_frame.setText(signal_text) if network_info.is_open: - self._panel.saved_connection_security_type_info_label.setText("OPEN") + self.saved_connection_security_type_info_label.setText("OPEN") else: sec_type = self._sdbus_network.get_security_type_by_ssid(ssid) - self._panel.saved_connection_security_type_info_label.setText( + self.saved_connection_security_type_info_label.setText( str(sec_type or "WPA").upper() ) else: - self._panel.saved_connection_signal_strength_info_frame.setText("--%") - self._panel.saved_connection_security_type_info_label.setText("--") + self.saved_connection_signal_strength_info_frame.setText("--%") + self.saved_connection_security_type_info_label.setText("--") current_ssid = self._sdbus_network.get_current_ssid() if current_ssid != ssid: - self._panel.network_activate_btn.setDisabled(False) - self._panel.sn_info.setText("Saved Network") + self.network_activate_btn.setDisabled(False) + self.sn_info.setText("Saved Network") else: - self._panel.network_activate_btn.setDisabled(True) - self._panel.sn_info.setText("Active Network") + self.network_activate_btn.setDisabled(True) + self.sn_info.setText("Active Network") - self.setCurrentIndex(self.indexOf(self._panel.saved_connection_page)) - self._panel.frame.repaint() + self.setCurrentIndex(self.indexOf(self.saved_connection_page)) + self.frame.repaint() def _set_priority_button(self, priority: Optional[int]) -> None: - if priority == PRIORITY_HIGH: - self._panel.high_priority_btn.setChecked(True) - elif priority == PRIORITY_LOW: - self._panel.low_priority_btn.setChecked(True) - else: - self._panel.med_priority_btn.setChecked(True) + """Set the priority button based on value. + + 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 _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._panel.add_network_network_label.setText(str(ssid)) - self.setCurrentIndex(self.indexOf(self._panel.add_network_page)) - - def _set_add_network_page_password_visibility(self, show_password: bool) -> None: - self._panel.frame_2.setVisible(show_password) + self._current_network_is_hidden = False + self.add_network_network_label.setText(str(ssid)) + self.setCurrentIndex(self.indexOf(self.add_network_page)) def _handle_scrollbar_change(self, value: int) -> None: - self._panel.verticalScrollBar.blockSignals(True) - self._panel.verticalScrollBar.setValue(value) - self._panel.verticalScrollBar.blockSignals(False) + """Handle scrollbar value change.""" + self.verticalScrollBar.blockSignals(True) + self.verticalScrollBar.setValue(value) + self.verticalScrollBar.blockSignals(False) def _sync_scrollbar(self) -> None: - list_scrollbar = self._panel.listView.verticalScrollBar() - self._panel.verticalScrollBar.setMinimum(list_scrollbar.minimum()) - self._panel.verticalScrollBar.setMaximum(list_scrollbar.maximum()) - self._panel.verticalScrollBar.setPageStep(list_scrollbar.pageStep()) + """Synchronize scrollbar with list view.""" + 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: @@ -1206,54 +3176,60 @@ def _schedule_delayed_action(self, callback: Callable, delay_ms: int) -> None: 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(): return - if index == self.indexOf(self._panel.add_network_page): - self._setup_add_network_page() - elif index == self.indexOf(self._panel.saved_connection_page): - self._setup_saved_connection_page() + if index == self.indexOf(self.add_network_page): + self._setup_add_network_page_state() + elif index == self.indexOf(self.saved_connection_page): + self._setup_saved_connection_page_state() self.repaint() super().setCurrentIndex(index) - def _setup_add_network_page(self) -> None: - self._panel.add_network_password_field.clear() + def _setup_add_network_page_state(self) -> None: + """Setup add network page state.""" + self.add_network_password_field.clear() if self._current_network_is_open: - self._panel.frame_2.setVisible(False) - self._panel.add_network_validation_button.setText("Connect") + self.frame_2.setVisible(False) + self.add_network_validation_button.setText("Connect") else: - self._panel.frame_2.setVisible(True) - self._panel.add_network_password_field.setPlaceholderText( + self.frame_2.setVisible(True) + self.add_network_password_field.setPlaceholderText( "Insert password here, press enter when finished." ) - self._panel.add_network_validation_button.setText("Activate") + self.add_network_validation_button.setText("Activate") - def _setup_saved_connection_page(self) -> None: - self._panel.saved_connection_change_password_field.clear() - self._panel.saved_connection_change_password_field.setPlaceholderText( + def _setup_saved_connection_page_state(self) -> None: + """Setup saved connection page state.""" + self.saved_connection_change_password_field.clear() + self.saved_connection_change_password_field.setPlaceholderText( "Change network password" ) def setProperty(self, name: str, value: Any) -> bool: + """Set a property value.""" if name == "backgroundPixmap": self._background = value return super().setProperty(name, value) @QtCore.pyqtSlot(name="call-network-panel") def show_network_panel(self) -> None: + """Show the network panel.""" if not self.parent(): return - self.setCurrentIndex(self.indexOf(self._panel.network_list_page)) + self.setCurrentIndex(self.indexOf(self.network_list_page)) parent_size = self.parent().size() self.setGeometry(0, 0, parent_size.width(), parent_size.height()) self.updateGeometry() self.repaint() - self.show() \ No newline at end of file + self.show() diff --git a/BlocksScreen/lib/utils/blocks_Scrollbar.py b/BlocksScreen/lib/utils/blocks_Scrollbar.py index 38e571e0..df2e436d 100644 --- a/BlocksScreen/lib/utils/blocks_Scrollbar.py +++ b/BlocksScreen/lib/utils/blocks_Scrollbar.py @@ -1,5 +1,5 @@ -from PyQt6 import QtCore, QtGui, QtWidgets import numpy as np +from PyQt6 import QtCore, QtGui, QtWidgets class CustomScrollBar(QtWidgets.QScrollBar): @@ -26,34 +26,16 @@ def paintEvent(self, event): handle_percentage = int((self.value() / max_val) * 100) - if handle_percentage < 15: - base_handle_length = int( - (groove.height() * page_step / (max_val - min_val + page_step)) - + np.interp(handle_percentage, [0, 15], [0, 40]) - ) - handle_pos = 0 - - elif handle_percentage > 85: - base_handle_length = int( - (groove.height() * page_step / (max_val - min_val + page_step)) - + np.interp(handle_percentage, [85, 100], [40, 0]) - ) - handle_pos = int( - (groove.height() - base_handle_length) - * (max_val - min_val) - / (max_val - min_val) - ) - else: - val = np.interp((handle_percentage), [15, 85], [0, 100]) / 100 * max_val + val = np.interp((handle_percentage), [15, 85], [0, 100]) / 100 * max_val - base_handle_length = int( - (groove.height() * page_step / (max_val - min_val + page_step)) + 40 - ) - handle_pos = int( - (groove.height() - base_handle_length) - * (val - min_val) - / (max_val - min_val) - ) + base_handle_length = int( + (groove.height() * page_step / (max_val - min_val + page_step)) + 40 + ) + handle_pos = int( + (groove.height() - base_handle_length) + * (val - min_val) + / (max_val - min_val) + ) handle_rect = QtCore.QRect( groove.x(), diff --git a/BlocksScreen/lib/utils/blocks_linedit.py b/BlocksScreen/lib/utils/blocks_linedit.py index 242e4b0d..b7d5845f 100644 --- a/BlocksScreen/lib/utils/blocks_linedit.py +++ b/BlocksScreen/lib/utils/blocks_linedit.py @@ -1,78 +1,220 @@ import typing + from PyQt6 import QtCore, QtGui, QtWidgets class BlocksCustomLinEdit(QtWidgets.QLineEdit): clicked = QtCore.pyqtSignal() + visibilityChanged = QtCore.pyqtSignal(bool) + + # Layout constants + ICON_SIZE = 48 + ICON_MARGIN = 8 + TEXT_MARGIN = 10 + CORNER_RADIUS = 8 + + def __init__(self, parent: typing.Optional[QtWidgets.QWidget] = None) -> None: + super().__init__(parent) + + # State + self._placeholder_str = "Type here" + self._name = "" + self._secret = False # True = show bullets, False = show text + self._show_toggle = False + self._is_password_visible = False + self._icon_pressed = False + + # Pre-allocated colors (avoid allocation in paint) + self._bg_color = QtGui.QColor(223, 223, 223) + self._bg_pressed_color = QtGui.QColor(200, 200, 200) + self._text_color = QtGui.QColor(0, 0, 0) + self._placeholder_color = QtGui.QColor(130, 130, 130) + self._icon_bg_hover = QtGui.QColor(180, 180, 180, 100) - def __init__( - self, - parent: QtWidgets.QWidget, - ) -> None: - super(BlocksCustomLinEdit, self).__init__(parent) - - self.button_background = None - self.button_ellipse = None - self._text: str = "" - self.placeholder_str = "Type here" - self._name: str = "" - self.text_color: QtGui.QColor = QtGui.QColor(0, 0, 0) - self.secret: bool = False + # Pre-allocated rect for icon (reused in paint) + self._icon_rect = QtCore.QRectF() + + # Touch support self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) + # Cursor + self.setCursor(QtCore.Qt.CursorShape.BlankCursor) + @property - def name(self): - """Widget name""" + def name(self) -> str: + """Widget name property.""" return self._name @name.setter - def name(self, new_name) -> None: - self._name = new_name - self.setObjectName(new_name) + def name(self, value: str) -> None: + self._name = value + self.setObjectName(value) + + def placeholderText(self) -> str: + """Get placeholder text.""" + return self._placeholder_str + + def setPlaceholderText(self, text: str) -> None: + """Set placeholder text displayed when empty.""" + self._placeholder_str = text + self.update() + + def showToggleButton(self) -> bool: + """Check if toggle button is enabled.""" + return self._show_toggle + + def _create_fallback_icons(self) -> None: + """Create simple fallback icons if resource files unavailable.""" + size = 32 - def setText(self, text: str) -> None: - """Set widget text""" - super().setText(text) + # Create "see" icon (simple eye shape) + self._icon_see = QtGui.QPixmap(size, size) + self._icon_see.fill(QtCore.Qt.GlobalColor.transparent) + painter = QtGui.QPainter(self._icon_see) + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) + painter.setPen(QtGui.QPen(QtGui.QColor(80, 80, 80), 2)) + painter.drawEllipse(8, 12, 16, 8) # Eye outline + painter.setBrush(QtGui.QColor(80, 80, 80)) + painter.drawEllipse(13, 13, 6, 6) # Pupil + painter.end() + + # Create "unsee" icon (eye with line through) + self._icon_unsee = QtGui.QPixmap(size, size) + self._icon_unsee.fill(QtCore.Qt.GlobalColor.transparent) + painter = QtGui.QPainter(self._icon_unsee) + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) + painter.setPen(QtGui.QPen(QtGui.QColor(80, 80, 80), 2)) + painter.drawEllipse(8, 12, 16, 8) + painter.drawLine(6, 6, 26, 26) # Strike through + painter.end() def setHidden(self, hidden: bool) -> None: - """Hide widget text""" - self.secret = hidden + """ + Set whether text is hidden (password mode). + + Args: + hidden: True to show bullets, False to show actual text + """ + if self._secret == hidden: + return + + self._secret = hidden + self._is_password_visible = not hidden + self.update() + + def isPasswordVisible(self) -> bool: + """Check if password is currently visible.""" + return self._is_password_visible + + def setPasswordVisible(self, visible: bool) -> None: + """Set password visibility state.""" + if self._is_password_visible == visible: + return + + self._is_password_visible = visible + self._secret = not visible + self.visibilityChanged.emit(visible) self.update() + def togglePasswordVisibility(self) -> None: + """Toggle between visible and hidden password.""" + self.setPasswordVisible(not self._is_password_visible) + + def _calculate_icon_rect(self) -> QtCore.QRectF: + """Calculate the bounding rectangle for the toggle icon.""" + if not self._show_toggle: + return QtCore.QRectF() + + x = self.width() - self.ICON_SIZE - self.ICON_MARGIN + y = (self.height() - self.ICON_SIZE) / 2 + return QtCore.QRectF(x, y, self.ICON_SIZE, self.ICON_SIZE) + + def _get_text_rect(self) -> QtCore.QRect: + """Calculate the rectangle available for text rendering.""" + left_margin = self.TEXT_MARGIN + right_margin = self.TEXT_MARGIN + + if self._show_toggle: + # Reserve space for icon button + right_margin = self.ICON_SIZE + self.ICON_MARGIN * 2 + + return self.rect().adjusted(left_margin, 0, -right_margin, 0) + def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: - """Re-implemented method, handle mouse press events""" + """Handle mouse press - detect icon clicks.""" + if self._show_toggle: + self._icon_rect = self._calculate_icon_rect() + + if self._icon_rect.contains(event.position()): + # Click on toggle icon + self._icon_pressed = True + self.update() + event.accept() + return + + # Click on text area - emit signal for keyboard self.clicked.emit() super().mousePressEvent(event) - def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): - """Re-implemented method, paint widget""" + def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None: + """Handle mouse release - trigger toggle if on icon.""" + if self._icon_pressed: + self._icon_pressed = False + + if self._icon_rect.contains(event.position()): + self.togglePasswordVisibility() + + self.update() + event.accept() + return + + super().mouseReleaseEvent(event) + + def focusInEvent(self, event: QtGui.QFocusEvent) -> None: + """Handle focus in - emit clicked for virtual keyboard.""" + self.clicked.emit() + super().focusInEvent(event) + + def paintEvent(self, event: typing.Optional[QtGui.QPaintEvent]) -> None: + """Custom paint with embedded toggle button.""" painter = QtGui.QPainter(self) - painter.setRenderHint(painter.RenderHint.Antialiasing, True) + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) - # Draw background - bg_color = QtGui.QColor(223, 223, 223) - painter.setBrush(bg_color) + # Background + painter.setBrush(self._bg_color) painter.setPen(QtCore.Qt.PenStyle.NoPen) - painter.drawRoundedRect(self.rect(), 8, 8) + painter.drawRoundedRect(self.rect(), self.CORNER_RADIUS, self.CORNER_RADIUS) + + # Text + self._draw_text(painter) - margin = 5 + painter.end() + + def _draw_text(self, painter: QtGui.QPainter) -> None: + """Draw the text or placeholder.""" + text_rect = self._get_text_rect() display_text = self.text() - if self.secret and display_text: + + # Apply password masking + if self._secret and display_text: display_text = "*" * len(display_text) - if self.text(): - painter.setPen(self.text_color) + if display_text: + painter.setPen(self._text_color) + painter.setFont(self.font()) painter.drawText( - self.rect().adjusted(margin, 0, 0, 0), + text_rect, QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter, display_text, ) else: - painter.setPen(QtGui.QColor(150, 150, 150)) + # Placeholder text + painter.setPen(self._placeholder_color) + painter.setFont(self.font()) painter.drawText( - self.rect().adjusted(margin, 0, 0, 0), + text_rect, QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter, - self.placeholder_str, + self._placeholder_str, ) From c63aa87a5a5df76a7a80895b75db067f26fd40bf Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Tue, 3 Feb 2026 13:43:07 +0000 Subject: [PATCH 21/26] cleanup of unused code --- BlocksScreen/lib/utils/blocks_linedit.py | 85 +----------------------- 1 file changed, 2 insertions(+), 83 deletions(-) diff --git a/BlocksScreen/lib/utils/blocks_linedit.py b/BlocksScreen/lib/utils/blocks_linedit.py index b7d5845f..6851eb01 100644 --- a/BlocksScreen/lib/utils/blocks_linedit.py +++ b/BlocksScreen/lib/utils/blocks_linedit.py @@ -5,11 +5,8 @@ class BlocksCustomLinEdit(QtWidgets.QLineEdit): clicked = QtCore.pyqtSignal() - visibilityChanged = QtCore.pyqtSignal(bool) # Layout constants - ICON_SIZE = 48 - ICON_MARGIN = 8 TEXT_MARGIN = 10 CORNER_RADIUS = 8 @@ -22,17 +19,12 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget] = None) -> None: self._secret = False # True = show bullets, False = show text self._show_toggle = False self._is_password_visible = False - self._icon_pressed = False # Pre-allocated colors (avoid allocation in paint) self._bg_color = QtGui.QColor(223, 223, 223) self._bg_pressed_color = QtGui.QColor(200, 200, 200) self._text_color = QtGui.QColor(0, 0, 0) self._placeholder_color = QtGui.QColor(130, 130, 130) - self._icon_bg_hover = QtGui.QColor(180, 180, 180, 100) - - # Pre-allocated rect for icon (reused in paint) - self._icon_rect = QtCore.QRectF() # Touch support self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) @@ -63,31 +55,6 @@ def showToggleButton(self) -> bool: """Check if toggle button is enabled.""" return self._show_toggle - def _create_fallback_icons(self) -> None: - """Create simple fallback icons if resource files unavailable.""" - size = 32 - - # Create "see" icon (simple eye shape) - self._icon_see = QtGui.QPixmap(size, size) - self._icon_see.fill(QtCore.Qt.GlobalColor.transparent) - painter = QtGui.QPainter(self._icon_see) - painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) - painter.setPen(QtGui.QPen(QtGui.QColor(80, 80, 80), 2)) - painter.drawEllipse(8, 12, 16, 8) # Eye outline - painter.setBrush(QtGui.QColor(80, 80, 80)) - painter.drawEllipse(13, 13, 6, 6) # Pupil - painter.end() - - # Create "unsee" icon (eye with line through) - self._icon_unsee = QtGui.QPixmap(size, size) - self._icon_unsee.fill(QtCore.Qt.GlobalColor.transparent) - painter = QtGui.QPainter(self._icon_unsee) - painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) - painter.setPen(QtGui.QPen(QtGui.QColor(80, 80, 80), 2)) - painter.drawEllipse(8, 12, 16, 8) - painter.drawLine(6, 6, 26, 26) # Strike through - painter.end() - def setHidden(self, hidden: bool) -> None: """ Set whether text is hidden (password mode). @@ -106,68 +73,20 @@ def isPasswordVisible(self) -> bool: """Check if password is currently visible.""" return self._is_password_visible - def setPasswordVisible(self, visible: bool) -> None: - """Set password visibility state.""" - if self._is_password_visible == visible: - return - - self._is_password_visible = visible - self._secret = not visible - self.visibilityChanged.emit(visible) - self.update() - - def togglePasswordVisibility(self) -> None: - """Toggle between visible and hidden password.""" - self.setPasswordVisible(not self._is_password_visible) - - def _calculate_icon_rect(self) -> QtCore.QRectF: - """Calculate the bounding rectangle for the toggle icon.""" - if not self._show_toggle: - return QtCore.QRectF() - - x = self.width() - self.ICON_SIZE - self.ICON_MARGIN - y = (self.height() - self.ICON_SIZE) / 2 - return QtCore.QRectF(x, y, self.ICON_SIZE, self.ICON_SIZE) - def _get_text_rect(self) -> QtCore.QRect: """Calculate the rectangle available for text rendering.""" left_margin = self.TEXT_MARGIN right_margin = self.TEXT_MARGIN - if self._show_toggle: - # Reserve space for icon button - right_margin = self.ICON_SIZE + self.ICON_MARGIN * 2 - return self.rect().adjusted(left_margin, 0, -right_margin, 0) def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: - """Handle mouse press - detect icon clicks.""" - if self._show_toggle: - self._icon_rect = self._calculate_icon_rect() - - if self._icon_rect.contains(event.position()): - # Click on toggle icon - self._icon_pressed = True - self.update() - event.accept() - return - - # Click on text area - emit signal for keyboard + """Handle mouse press """ self.clicked.emit() super().mousePressEvent(event) def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None: - """Handle mouse release - trigger toggle if on icon.""" - if self._icon_pressed: - self._icon_pressed = False - - if self._icon_rect.contains(event.position()): - self.togglePasswordVisibility() - - self.update() - event.accept() - return - + """Handle mouse release""" super().mouseReleaseEvent(event) def focusInEvent(self, event: QtGui.QFocusEvent) -> None: From 88e63aae9db0e6aace14930112c6120e4baa4124 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Tue, 3 Feb 2026 14:23:06 +0000 Subject: [PATCH 22/26] fix code formatation --- BlocksScreen/lib/utils/blocks_linedit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlocksScreen/lib/utils/blocks_linedit.py b/BlocksScreen/lib/utils/blocks_linedit.py index 6851eb01..a4342fe3 100644 --- a/BlocksScreen/lib/utils/blocks_linedit.py +++ b/BlocksScreen/lib/utils/blocks_linedit.py @@ -81,7 +81,7 @@ def _get_text_rect(self) -> QtCore.QRect: return self.rect().adjusted(left_margin, 0, -right_margin, 0) def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: - """Handle mouse press """ + """Handle mouse press""" self.clicked.emit() super().mousePressEvent(event) From e08e9a2f40de50ea29c74643753a0faf1369e839 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Tue, 3 Feb 2026 17:57:30 +0000 Subject: [PATCH 23/26] changed QtWidgets.QApplication.processEvents for repaint --- BlocksScreen/lib/panels/networkWindow.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 02170307..19574cf5 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -2239,7 +2239,7 @@ def _set_loading_state(self, loading: bool) -> None: # This prevents the dual panel visibility bug self._hide_all_info_elements() # Force UI update to ensure elements are hidden - QtWidgets.QApplication.processEvents() + self.repaint() # Now show loading self.loadingwidget.setVisible(True) @@ -2265,7 +2265,7 @@ def _show_network_details(self) -> None: self.loadingwidget.setVisible(False) self.mn_info_box.setVisible(False) # Force UI update - QtWidgets.QApplication.processEvents() + self.repaint() # Then show only the details self.netlist_ip.setVisible(True) @@ -2284,7 +2284,7 @@ def _show_disconnected_message(self) -> None: self.loadingwidget.setVisible(False) self._hide_network_detail_labels() # Force UI update - QtWidgets.QApplication.processEvents() + self.repaint() # Then show info box self._configure_info_box_centered() @@ -2436,7 +2436,7 @@ def _on_toggle_state(self, new_state) -> None: # Show loading IMMEDIATELY when turning something on if is_sender_now_on: self._set_loading_state(True) - QtWidgets.QApplication.processEvents() + self.repaint() saved_networks = self._sdbus_network.get_saved_networks_with_for() @@ -2852,7 +2852,7 @@ def _handle_network_activate(self, ssid: str) -> None: self._target_ssid = ssid # Show loading IMMEDIATELY self._set_loading_state(True) - QtWidgets.QApplication.processEvents() + self.repaint() self.setCurrentIndex(self.indexOf(self.main_network_page)) From bd3189af1c406fa5cd266632d68aa63fc5489f9d Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Wed, 4 Feb 2026 10:15:20 +0000 Subject: [PATCH 24/26] delete unused files --- BlocksScreen/lib/ui/connectionWindow.ui | 938 --------------------- BlocksScreen/lib/ui/connectionWindow_ui.py | 338 -------- 2 files changed, 1276 deletions(-) delete mode 100644 BlocksScreen/lib/ui/connectionWindow.ui delete mode 100644 BlocksScreen/lib/ui/connectionWindow_ui.py diff --git a/BlocksScreen/lib/ui/connectionWindow.ui b/BlocksScreen/lib/ui/connectionWindow.ui deleted file mode 100644 index f2bd4899..00000000 --- a/BlocksScreen/lib/ui/connectionWindow.ui +++ /dev/null @@ -1,938 +0,0 @@ - - - ConnectivityForm - - - Qt::WindowModal - - - - 0 - 0 - 800 - 480 - - - - - 0 - 0 - - - - - 800 - 480 - - - - - 800 - 480 - - - - Form - - - 1.000000000000000 - - - false - - - #ConnectivityForm{ -background-image: url(:/background/media/1st_background.png); -} - - - - - - - - 10 - 380 - 780 - 124 - - - - - 0 - 0 - - - - - 780 - 124 - - - - - 780 - 150 - - - - - 800 - 80 - - - - - - - - - - - - PreferAntialias - - - - true - - - false - - - - - - QFrame::NoFrame - - - QFrame::Plain - - - 0 - - - - 0 - - - 0 - - - 5 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 100 - 80 - - - - - 100 - 80 - - - - - 160 - 80 - - - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - - - false - PreferAntialias - false - - - - BlankCursor - - - true - - - Qt::NoFocus - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - false - - - - - - Restart Klipper - - - - :/system_icons/media/btn_icons/restart_klipper.svg - - - - - 46 - 42 - - - - false - - - false - - - false - - - 0 - - - 0 - - - false - - - true - - - :/system/media/btn_icons/restart_klipper.svg - - - true - - - bottom - - - - 255 - 255 - 255 - - - - - - - - - 0 - 0 - - - - - 100 - 80 - - - - - 100 - 80 - - - - - 80 - 80 - - - - BlankCursor - - - true - - - Qt::NoFocus - - - Qt::NoContextMenu - - - false - - - Reboot - - - - :/system_icons/media/btn_icons/firmware_restart.svg:/system_icons/media/btn_icons/firmware_restart.svg - - - false - - - false - - - true - - - :/system/media/btn_icons/reboot.svg - - - bottom - - - - 255 - 255 - 255 - - - - true - - - - - - - - 0 - 0 - - - - - 100 - 80 - - - - - 100 - 80 - - - - - 160 - 80 - - - - BlankCursor - - - true - - - Qt::NoFocus - - - Qt::NoContextMenu - - - false - - - Firmware Restart - - - - :/system_icons/media/btn_icons/firmware_restart.svg:/system_icons/media/btn_icons/firmware_restart.svg - - - false - - - false - - - true - - - :/system/media/btn_icons/restart_firmware.svg - - - true - - - bottom - - - - 255 - 255 - 255 - - - - - - - - - 0 - 0 - - - - - 100 - 80 - - - - - 100 - 80 - - - - - 80 - 80 - - - - - - - - - - - - 13 - - - - true - - - Qt::ClickFocus - - - false - - - - - - Retry - - - - :/system_icons/media/btn_icons/retry_connection.svg:/system_icons/media/btn_icons/retry_connection.svg - - - - 16 - 16 - - - - false - - - 0 - - - 0 - - - false - - - false - - - true - - - bottom - - - :/system/media/btn_icons/restart_printer.svg - - - - 255 - 255 - 255 - - - - true - - - - - - - - 0 - 0 - - - - - 100 - 80 - - - - - 100 - 80 - - - - - 80 - 80 - - - - BlankCursor - - - true - - - Qt::NoFocus - - - Qt::NoContextMenu - - - false - - - Update page - - - - :/system_icons/media/btn_icons/firmware_restart.svg:/system_icons/media/btn_icons/firmware_restart.svg - - - false - - - false - - - true - - - :/system/media/btn_icons/update-software-icon.svg - - - bottom - - - - 255 - 255 - 255 - - - - true - - - - - - - - 0 - 0 - - - - - 100 - 80 - - - - - 100 - 80 - - - - - 80 - 80 - - - - true - - - Qt::NoFocus - - - Qt::NoContextMenu - - - false - - - Wifi Settings - - - - :/system_icons/media/btn_icons/retry_connection.svg:/system_icons/media/btn_icons/retry_connection.svg - - - false - - - false - - - true - - - system_control_btn - - - :/network/media/btn_icons/wifi_config.svg - - - true - - - bottom - - - - - - - - - 0 - 0 - 800 - 380 - - - - - 0 - 0 - - - - - 800 - 380 - - - - - 800 - 380 - - - - false - - - - - - QFrame::NoFrame - - - QFrame::Raised - - - - - - Qt::Vertical - - - QSizePolicy::Minimum - - - - 775 - 70 - - - - - - - - - 0 - 0 - - - - - 800 - 380 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - Momcake - 17 - 75 - false - true - - - - color:white - - - - - - Qt::AutoText - - - false - - - Qt::AlignCenter - - - true - - - Qt::NoTextInteraction - - - - - - - - - IconButton - QPushButton -
lib.utils.icon_button
-
- - BlocksCustomFrame - QFrame -
lib.utils.blocks_frame
- 1 -
-
- - - - - - - -
diff --git a/BlocksScreen/lib/ui/connectionWindow_ui.py b/BlocksScreen/lib/ui/connectionWindow_ui.py deleted file mode 100644 index 772dc227..00000000 --- a/BlocksScreen/lib/ui/connectionWindow_ui.py +++ /dev/null @@ -1,338 +0,0 @@ -# Form implementation generated from reading ui file '/home/levi/BlocksScreen/BlocksScreen/lib/ui/connectionWindow.ui' -# -# Created by: PyQt6 UI code generator 6.7.1 -# -# WARNING: Any manual changes made to this file will be lost when pyuic6 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt6 import QtCore, QtGui, QtWidgets - - -class Ui_ConnectivityForm(object): - def setupUi(self, ConnectivityForm): - ConnectivityForm.setObjectName("ConnectivityForm") - ConnectivityForm.setWindowModality(QtCore.Qt.WindowModality.WindowModal) - ConnectivityForm.resize(800, 480) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(ConnectivityForm.sizePolicy().hasHeightForWidth()) - ConnectivityForm.setSizePolicy(sizePolicy) - ConnectivityForm.setMinimumSize(QtCore.QSize(800, 480)) - ConnectivityForm.setMaximumSize(QtCore.QSize(800, 480)) - ConnectivityForm.setWindowOpacity(1.0) - ConnectivityForm.setAutoFillBackground(False) - ConnectivityForm.setStyleSheet("#ConnectivityForm{\n" -"background-image: url(:/background/media/1st_background.png);\n" -"}") - ConnectivityForm.setProperty("class", "") - self.cw_buttonFrame = BlocksCustomFrame(parent=ConnectivityForm) - self.cw_buttonFrame.setGeometry(QtCore.QRect(10, 380, 780, 124)) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cw_buttonFrame.sizePolicy().hasHeightForWidth()) - self.cw_buttonFrame.setSizePolicy(sizePolicy) - self.cw_buttonFrame.setMinimumSize(QtCore.QSize(780, 124)) - self.cw_buttonFrame.setMaximumSize(QtCore.QSize(780, 150)) - self.cw_buttonFrame.setBaseSize(QtCore.QSize(800, 80)) - palette = QtGui.QPalette() - self.cw_buttonFrame.setPalette(palette) - font = QtGui.QFont() - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.cw_buttonFrame.setFont(font) - self.cw_buttonFrame.setTabletTracking(True) - self.cw_buttonFrame.setAutoFillBackground(False) - self.cw_buttonFrame.setStyleSheet("") - self.cw_buttonFrame.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.cw_buttonFrame.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) - self.cw_buttonFrame.setLineWidth(0) - self.cw_buttonFrame.setObjectName("cw_buttonFrame") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.cw_buttonFrame) - self.horizontalLayout.setContentsMargins(0, 5, 0, 0) - self.horizontalLayout.setSpacing(0) - self.horizontalLayout.setObjectName("horizontalLayout") - self.RestartKlipperButton = IconButton(parent=self.cw_buttonFrame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.RestartKlipperButton.sizePolicy().hasHeightForWidth()) - self.RestartKlipperButton.setSizePolicy(sizePolicy) - self.RestartKlipperButton.setMinimumSize(QtCore.QSize(100, 80)) - self.RestartKlipperButton.setMaximumSize(QtCore.QSize(100, 80)) - self.RestartKlipperButton.setBaseSize(QtCore.QSize(160, 80)) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.ButtonText, brush) - self.RestartKlipperButton.setPalette(palette) - font = QtGui.QFont() - font.setStrikeOut(False) - font.setKerning(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.RestartKlipperButton.setFont(font) - self.RestartKlipperButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) - self.RestartKlipperButton.setTabletTracking(True) - self.RestartKlipperButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.RestartKlipperButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.RestartKlipperButton.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.RestartKlipperButton.setAutoFillBackground(False) - self.RestartKlipperButton.setStyleSheet("") - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(":/system_icons/media/btn_icons/restart_klipper.svg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On) - self.RestartKlipperButton.setIcon(icon) - self.RestartKlipperButton.setIconSize(QtCore.QSize(46, 42)) - self.RestartKlipperButton.setCheckable(False) - self.RestartKlipperButton.setAutoRepeat(False) - self.RestartKlipperButton.setAutoExclusive(False) - self.RestartKlipperButton.setAutoRepeatDelay(0) - self.RestartKlipperButton.setAutoRepeatInterval(0) - self.RestartKlipperButton.setAutoDefault(False) - self.RestartKlipperButton.setFlat(True) - self.RestartKlipperButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/restart_klipper.svg")) - self.RestartKlipperButton.setProperty("has_text", True) - self.RestartKlipperButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) - self.RestartKlipperButton.setObjectName("RestartKlipperButton") - self.horizontalLayout.addWidget(self.RestartKlipperButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) - self.RebootSystemButton = IconButton(parent=self.cw_buttonFrame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.RebootSystemButton.sizePolicy().hasHeightForWidth()) - self.RebootSystemButton.setSizePolicy(sizePolicy) - self.RebootSystemButton.setMinimumSize(QtCore.QSize(100, 80)) - self.RebootSystemButton.setMaximumSize(QtCore.QSize(100, 80)) - self.RebootSystemButton.setBaseSize(QtCore.QSize(80, 80)) - self.RebootSystemButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) - self.RebootSystemButton.setTabletTracking(True) - self.RebootSystemButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.RebootSystemButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.RebootSystemButton.setAutoFillBackground(False) - icon1 = QtGui.QIcon() - icon1.addPixmap(QtGui.QPixmap(":/system_icons/media/btn_icons/firmware_restart.svg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) - self.RebootSystemButton.setIcon(icon1) - self.RebootSystemButton.setAutoDefault(False) - self.RebootSystemButton.setDefault(False) - self.RebootSystemButton.setFlat(True) - self.RebootSystemButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/reboot.svg")) - self.RebootSystemButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) - self.RebootSystemButton.setProperty("has_text", True) - self.RebootSystemButton.setObjectName("RebootSystemButton") - self.horizontalLayout.addWidget(self.RebootSystemButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) - self.FirmwareRestartButton = IconButton(parent=self.cw_buttonFrame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.FirmwareRestartButton.sizePolicy().hasHeightForWidth()) - self.FirmwareRestartButton.setSizePolicy(sizePolicy) - self.FirmwareRestartButton.setMinimumSize(QtCore.QSize(100, 80)) - self.FirmwareRestartButton.setMaximumSize(QtCore.QSize(100, 80)) - self.FirmwareRestartButton.setBaseSize(QtCore.QSize(160, 80)) - self.FirmwareRestartButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) - self.FirmwareRestartButton.setTabletTracking(True) - self.FirmwareRestartButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.FirmwareRestartButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.FirmwareRestartButton.setAutoFillBackground(False) - self.FirmwareRestartButton.setIcon(icon1) - self.FirmwareRestartButton.setAutoDefault(False) - self.FirmwareRestartButton.setDefault(False) - self.FirmwareRestartButton.setFlat(True) - self.FirmwareRestartButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/restart_firmware.svg")) - self.FirmwareRestartButton.setProperty("has_text", True) - self.FirmwareRestartButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) - self.FirmwareRestartButton.setObjectName("FirmwareRestartButton") - self.horizontalLayout.addWidget(self.FirmwareRestartButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) - self.RetryConnectionButton = IconButton(parent=self.cw_buttonFrame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.RetryConnectionButton.sizePolicy().hasHeightForWidth()) - self.RetryConnectionButton.setSizePolicy(sizePolicy) - self.RetryConnectionButton.setMinimumSize(QtCore.QSize(100, 80)) - self.RetryConnectionButton.setMaximumSize(QtCore.QSize(100, 80)) - self.RetryConnectionButton.setBaseSize(QtCore.QSize(80, 80)) - palette = QtGui.QPalette() - self.RetryConnectionButton.setPalette(palette) - font = QtGui.QFont() - font.setPointSize(13) - self.RetryConnectionButton.setFont(font) - self.RetryConnectionButton.setTabletTracking(True) - self.RetryConnectionButton.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) - self.RetryConnectionButton.setAutoFillBackground(False) - self.RetryConnectionButton.setStyleSheet("") - icon2 = QtGui.QIcon() - icon2.addPixmap(QtGui.QPixmap(":/system_icons/media/btn_icons/retry_connection.svg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) - self.RetryConnectionButton.setIcon(icon2) - self.RetryConnectionButton.setIconSize(QtCore.QSize(16, 16)) - self.RetryConnectionButton.setCheckable(False) - self.RetryConnectionButton.setAutoRepeatDelay(0) - self.RetryConnectionButton.setAutoRepeatInterval(0) - self.RetryConnectionButton.setAutoDefault(False) - self.RetryConnectionButton.setDefault(False) - self.RetryConnectionButton.setFlat(True) - self.RetryConnectionButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/restart_printer.svg")) - self.RetryConnectionButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) - self.RetryConnectionButton.setProperty("has_text", True) - self.RetryConnectionButton.setObjectName("RetryConnectionButton") - self.horizontalLayout.addWidget(self.RetryConnectionButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) - self.updatepageButton = IconButton(parent=self.cw_buttonFrame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.updatepageButton.sizePolicy().hasHeightForWidth()) - self.updatepageButton.setSizePolicy(sizePolicy) - self.updatepageButton.setMinimumSize(QtCore.QSize(100, 80)) - self.updatepageButton.setMaximumSize(QtCore.QSize(100, 80)) - self.updatepageButton.setBaseSize(QtCore.QSize(80, 80)) - self.updatepageButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) - self.updatepageButton.setTabletTracking(True) - self.updatepageButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.updatepageButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.updatepageButton.setAutoFillBackground(False) - self.updatepageButton.setIcon(icon1) - self.updatepageButton.setAutoDefault(False) - self.updatepageButton.setDefault(False) - self.updatepageButton.setFlat(True) - self.updatepageButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/update-software-icon.svg")) - self.updatepageButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) - self.updatepageButton.setProperty("has_text", True) - self.updatepageButton.setObjectName("updatepageButton") - self.horizontalLayout.addWidget(self.updatepageButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) - self.wifi_button = IconButton(parent=self.cw_buttonFrame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.wifi_button.sizePolicy().hasHeightForWidth()) - self.wifi_button.setSizePolicy(sizePolicy) - self.wifi_button.setMinimumSize(QtCore.QSize(100, 80)) - self.wifi_button.setMaximumSize(QtCore.QSize(100, 80)) - self.wifi_button.setBaseSize(QtCore.QSize(80, 80)) - self.wifi_button.setTabletTracking(True) - self.wifi_button.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.wifi_button.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.wifi_button.setAutoFillBackground(False) - self.wifi_button.setIcon(icon2) - self.wifi_button.setAutoDefault(False) - self.wifi_button.setDefault(False) - self.wifi_button.setFlat(True) - self.wifi_button.setProperty("icon_pixmap", QtGui.QPixmap(":/network/media/btn_icons/wifi_config.svg")) - self.wifi_button.setProperty("has_text", True) - self.wifi_button.setObjectName("wifi_button") - self.horizontalLayout.addWidget(self.wifi_button, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) - self.cw_Frame = QtWidgets.QFrame(parent=ConnectivityForm) - self.cw_Frame.setGeometry(QtCore.QRect(0, 0, 800, 380)) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cw_Frame.sizePolicy().hasHeightForWidth()) - self.cw_Frame.setSizePolicy(sizePolicy) - self.cw_Frame.setMinimumSize(QtCore.QSize(800, 380)) - self.cw_Frame.setMaximumSize(QtCore.QSize(800, 380)) - self.cw_Frame.setAutoFillBackground(False) - self.cw_Frame.setStyleSheet("") - self.cw_Frame.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.cw_Frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.cw_Frame.setObjectName("cw_Frame") - self.verticalLayout = QtWidgets.QVBoxLayout(self.cw_Frame) - self.verticalLayout.setObjectName("verticalLayout") - spacerItem = QtWidgets.QSpacerItem(775, 70, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.verticalLayout.addItem(spacerItem) - self.connectionTextBox = QtWidgets.QLabel(parent=self.cw_Frame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.connectionTextBox.sizePolicy().hasHeightForWidth()) - self.connectionTextBox.setSizePolicy(sizePolicy) - self.connectionTextBox.setMaximumSize(QtCore.QSize(800, 380)) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.ButtonText, brush) - self.connectionTextBox.setPalette(palette) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(17) - font.setBold(True) - font.setItalic(False) - font.setWeight(75) - self.connectionTextBox.setFont(font) - self.connectionTextBox.setStyleSheet("color:white") - self.connectionTextBox.setText("") - self.connectionTextBox.setTextFormat(QtCore.Qt.TextFormat.AutoText) - self.connectionTextBox.setScaledContents(False) - self.connectionTextBox.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.connectionTextBox.setWordWrap(True) - self.connectionTextBox.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.NoTextInteraction) - self.connectionTextBox.setObjectName("connectionTextBox") - self.verticalLayout.addWidget(self.connectionTextBox) - - self.retranslateUi(ConnectivityForm) - QtCore.QMetaObject.connectSlotsByName(ConnectivityForm) - - def retranslateUi(self, ConnectivityForm): - _translate = QtCore.QCoreApplication.translate - ConnectivityForm.setWindowTitle(_translate("ConnectivityForm", "Form")) - self.RestartKlipperButton.setText(_translate("ConnectivityForm", "Restart Klipper")) - self.RestartKlipperButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) - self.RebootSystemButton.setText(_translate("ConnectivityForm", "Reboot")) - self.RebootSystemButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) - self.FirmwareRestartButton.setText(_translate("ConnectivityForm", "Firmware Restart")) - self.FirmwareRestartButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) - self.RetryConnectionButton.setText(_translate("ConnectivityForm", "Retry ")) - self.RetryConnectionButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) - self.updatepageButton.setText(_translate("ConnectivityForm", "Update page")) - self.updatepageButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) - self.wifi_button.setText(_translate("ConnectivityForm", "Wifi Settings")) - self.wifi_button.setProperty("class", _translate("ConnectivityForm", "system_control_btn")) - self.wifi_button.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) -from lib.utils.blocks_frame import BlocksCustomFrame -from lib.utils.icon_button import IconButton From 8de3ee9b93b0e9348bc0f7b984c160e24972c9f4 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Wed, 4 Feb 2026 11:08:50 +0000 Subject: [PATCH 25/26] fix formatting issues and logic to parse sensors --- BlocksScreen/lib/network.py | 34 +++++++++++-------- .../lib/panels/widgets/sensorsPanel.py | 16 +++------ 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/BlocksScreen/lib/network.py b/BlocksScreen/lib/network.py index 7fc63aa4..61ea4078 100644 --- a/BlocksScreen/lib/network.py +++ b/BlocksScreen/lib/network.py @@ -354,13 +354,15 @@ def get_wired_interfaces(self) -> typing.List[dbusNm.NetworkDeviceWired]: 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, + 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, ), ), @@ -386,13 +388,15 @@ def get_wireless_interfaces( 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, + 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, ), ), @@ -883,7 +887,7 @@ def get_saved_ssid_names(self) -> typing.List[str]: return [] return list( map( - lambda saved_network: (saved_network.get("ssid", None)), + lambda saved_network: saved_network.get("ssid", None), _saved_networks, ) ) diff --git a/BlocksScreen/lib/panels/widgets/sensorsPanel.py b/BlocksScreen/lib/panels/widgets/sensorsPanel.py index 29eea5e7..285abbab 100644 --- a/BlocksScreen/lib/panels/widgets/sensorsPanel.py +++ b/BlocksScreen/lib/panels/widgets/sensorsPanel.py @@ -1,8 +1,8 @@ import typing +from lib.panels.widgets.sensorWidget import SensorWidget from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.icon_button import IconButton -from lib.panels.widgets.sensorWidget import SensorWidget from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem from PyQt6 import QtCore, QtGui, QtWidgets @@ -44,16 +44,10 @@ def handle_available_fil_sensors(self, sensors: dict) -> None: if not isinstance(sensors, dict): return self.reset_view_model() - filtered_sensors = list( - filter( - lambda printer_obj: str(printer_obj).startswith( - "filament_switch_sensor" - ) - or str(printer_obj).startswith("filament_motion_sensor") - or str(printer_obj).startswith("cutter_sensor"), - sensors.keys(), - ) - ) + filtered_sensors = [ + sensor for sensor in sensors.keys() + if sensor.startswith(("filament_switch_sensor", "filament_motion_sensor", "cutter_sensor")) + ] if filtered_sensors: self.sensor_list = [ self.create_sensor_widget(name=sensor) for sensor in filtered_sensors From 0016c81a1a03dd90a301923d79c45c9483965544 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Wed, 4 Feb 2026 11:21:51 +0000 Subject: [PATCH 26/26] fix code formatation --- BlocksScreen/lib/panels/widgets/sensorsPanel.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/BlocksScreen/lib/panels/widgets/sensorsPanel.py b/BlocksScreen/lib/panels/widgets/sensorsPanel.py index 285abbab..df63cfb5 100644 --- a/BlocksScreen/lib/panels/widgets/sensorsPanel.py +++ b/BlocksScreen/lib/panels/widgets/sensorsPanel.py @@ -45,8 +45,11 @@ def handle_available_fil_sensors(self, sensors: dict) -> None: return self.reset_view_model() filtered_sensors = [ - sensor for sensor in sensors.keys() - if sensor.startswith(("filament_switch_sensor", "filament_motion_sensor", "cutter_sensor")) + sensor + for sensor in sensors.keys() + if sensor.startswith( + ("filament_switch_sensor", "filament_motion_sensor", "cutter_sensor") + ) ] if filtered_sensors: self.sensor_list = [