from __future__ import annotations from PySide6.QtCore import Qt, QTimer, QSettings, QEvent, Signal from PySide6.QtGui import QFont from PySide6.QtWidgets import ( QWidget, QLabel, QVBoxLayout, QHBoxLayout, QPushButton, QListWidget, QListWidgetItem, QScroller, QFrame, ) import build_info from services.bluetooth_service import BluetoothService, BluetoothDevice from ui.confirm_dialog import ConfirmDialog class BluetoothDeviceCard(QFrame): """Карточка Bluetooth устройства.""" connect_clicked = Signal(str) # mac disconnect_clicked = Signal(str) # mac remove_clicked = Signal(str) # mac def __init__(self, device: BluetoothDevice, connected: bool, parent=None): super().__init__(parent) self._device = device self._connected = connected self.setObjectName("BluetoothDeviceCard") self.setFrameShape(QFrame.StyledPanel) self.setCursor(Qt.PointingHandCursor) root = QHBoxLayout(self) root.setContentsMargins(14, 10, 14, 10) root.setSpacing(12) # Индикатор статуса (цветная точка) self.status_indicator = QLabel("●") self.status_indicator.setObjectName("BluetoothStatusIndicator") self.status_indicator.setFont(QFont("", 18)) self.status_indicator.setAlignment(Qt.AlignCenter) self.status_indicator.setFixedSize(24, 24) # Информация об устройстве info_col = QVBoxLayout() info_col.setContentsMargins(0, 0, 0, 0) info_col.setSpacing(2) self.name_label = QLabel(device.name if device.name else "Неизвестное устройство") self.name_label.setObjectName("BluetoothDeviceName") self.name_label.setFont(QFont("", 15, 600)) self.mac_label = QLabel(device.mac) self.mac_label.setObjectName("BluetoothDeviceMac") self.mac_label.setFont(QFont("", 11)) self.status_label = QLabel("Подключено" if connected else "Не подключено") self.status_label.setObjectName("BluetoothDeviceStatus") self.status_label.setFont(QFont("", 12)) info_col.addWidget(self.name_label) info_col.addWidget(self.mac_label) info_col.addWidget(self.status_label) # Кнопка удаления (крестик) self.remove_btn = QPushButton("✕") self.remove_btn.setObjectName("BluetoothRemoveBtn") self.remove_btn.setToolTip("Удалить устройство") self.remove_btn.setFixedSize(40, 40) self.remove_btn.setCursor(Qt.PointingHandCursor) self.remove_btn.clicked.connect(self._on_remove) root.addWidget(self.status_indicator) root.addLayout(info_col, 1) root.addWidget(self.remove_btn) self._update_status_indicator() def _update_status_indicator(self): """Обновить цвет индикатора.""" if self._connected: self.status_indicator.setStyleSheet("color: #4ade80;") # зелёный self.status_label.setStyleSheet("color: #4ade80;") self.status_label.setText("Подключено") else: self.status_indicator.setStyleSheet("color: #9ca3af;") # серый self.status_label.setStyleSheet("color: #9ca3af;") self.status_label.setText("Не подключено") def update_connected(self, connected: bool): """Обновить статус подключения.""" self._connected = connected self._update_status_indicator() def _on_remove(self): """Обработчик нажатия кнопки удаления.""" self.remove_clicked.emit(self._device.mac) def mousePressEvent(self, event): """Обработка клика по карточке.""" if self._connected: self.disconnect_clicked.emit(self._device.mac) else: self.connect_clicked.emit(self._device.mac) super().mousePressEvent(event) class BluetoothScreen(QWidget): def __init__(self, on_back): super().__init__() self._on_back = on_back self._settings = QSettings("car_ui", "ui") self._bt_service = BluetoothService(self) self._discoverable_timer = QTimer(self) self._discoverable_timer.timeout.connect(self._refresh_discoverable) self._list_refresh_timer = QTimer(self) self._list_refresh_timer.timeout.connect(self.refresh_list) root = QVBoxLayout(self) root.setContentsMargins(0, 0, 0, 0) root.setSpacing(12) self.status = QLabel("Статус: —") self.status.setObjectName("BluetoothStatus") self.status.setFont(QFont("", 12)) self.list = QListWidget() self.list.setObjectName("BluetoothList") self.list.setSpacing(8) self.list.setSelectionMode(QListWidget.NoSelection) # убираем выделение, т.к. карточки кликабельны QScroller.scroller(self.list.viewport()).grabGesture( self.list.viewport(), QScroller.LeftMouseButtonGesture, ) actions = QHBoxLayout() actions.setContentsMargins(0, 0, 0, 0) actions.setSpacing(12) # Кнопка скрыта, видимость включается автоматически при входе в меню self.btn_visible = QPushButton("Сделать видимым") self.btn_visible.setObjectName("BluetoothActionBtn") self.btn_visible.setMinimumHeight(56) self.btn_visible.clicked.connect(self._make_visible) self.btn_visible.setVisible(False) self.btn_refresh = QPushButton("Обновить список") self.btn_refresh.setObjectName("BluetoothActionBtn") self.btn_refresh.setMinimumHeight(56) self.btn_refresh.clicked.connect(self.refresh_list) self.btn_refresh.setVisible(False) # скрываем, обновляемся автоматически # Кнопки подключить/отключить скрыты — управление через клик по карточке self.btn_connect = QPushButton("Подключить") self.btn_connect.setObjectName("BluetoothActionBtnPrimary") self.btn_connect.setMinimumHeight(56) self.btn_connect.clicked.connect(self._connect_last_selected) self.btn_connect.setVisible(False) self.btn_disconnect = QPushButton("Отключить") self.btn_disconnect.setObjectName("BluetoothActionBtn") self.btn_disconnect.setMinimumHeight(56) self.btn_disconnect.clicked.connect(self._disconnect_last_selected) self.btn_disconnect.setVisible(False) actions.addWidget(self.btn_visible, 1) actions.addWidget(self.btn_refresh, 1) actions.addWidget(self.btn_connect, 1) actions.addWidget(self.btn_disconnect, 1) root.addWidget(self.status) root.addWidget(self.list, 1) root.addLayout(actions) self.refresh_list() QTimer.singleShot(200, self._auto_connect_last) def refresh_list(self): """Обновить список сопряженных устройств.""" devices = self._bt_service.get_paired_devices() self.list.clear() self._cards: dict[str, BluetoothDeviceCard] = {} for dev in devices: connected = self._bt_service.is_connected(dev.mac) card = BluetoothDeviceCard(dev, connected, self) card.connect_clicked.connect(self._connect_device) card.disconnect_clicked.connect(self._disconnect_device) card.remove_clicked.connect(self._remove_device) item = QListWidgetItem() item.setSizeHint(card.sizeHint()) self.list.addItem(item) self.list.setItemWidget(item, card) self._cards[dev.mac] = card self._update_status() def _auto_connect_last(self): """Автоподключение к последнему устройству.""" last_mac = self._settings.value("bluetooth/last_mac", "") if not last_mac: return self._connect_device(last_mac) def _connect_last_selected(self): """Подключить последнее выбранное устройство.""" if self._cards: last_mac = self._settings.value("bluetooth/last_mac", "") if last_mac and last_mac in self._cards: self._connect_device(last_mac) def _disconnect_last_selected(self): """Отключить последнее выбранное устройство.""" if self._cards: last_mac = self._settings.value("bluetooth/last_mac", "") if last_mac and last_mac in self._cards: self._disconnect_device(last_mac) def _connect_device(self, mac: str): """Подключиться к устройству по MAC.""" if mac not in self._cards: return success = self._bt_service.connect_device(mac) if not success: self.status.setText(f"Статус: ошибка ({self._bt_service.last_error})") self._settings.setValue("bluetooth/last_mac", mac) QTimer.singleShot(300, lambda: self._update_card_status(mac)) def _disconnect_device(self, mac: str): """Отключить устройство по MAC.""" if mac not in self._cards: return success = self._bt_service.disconnect_device(mac) if not success: self.status.setText(f"Статус: ошибка ({self._bt_service.last_error})") else: self.status.setText(f"Статус: отключено от {mac}") QTimer.singleShot(300, lambda: self._update_card_status(mac)) def _remove_device(self, mac: str): """Удалить устройство из списка сопряженных.""" device = self._cards.get(mac) if not device: return name = device._device.name if device._device.name else mac dialog = ConfirmDialog( "Подтверждение", f"Удалить устройство \"{name}\" из списка сопряженных?", "Удалить", ok_object_name="ConfirmOkDanger", ) if dialog.exec() == ConfirmDialog.Accepted: success = self._bt_service.remove_device(mac) if success: self.status.setText(f"Статус: устройство {mac} удалено") QTimer.singleShot(300, self.refresh_list) else: self.status.setText(f"Статус: ошибка удаления ({self._bt_service.last_error})") def _update_card_status(self, mac: str): """Обновить статус на карточке устройства.""" if mac in self._cards: connected = self._bt_service.is_connected(mac) self._cards[mac].update_connected(connected) self._update_status() def _make_visible(self): """Сделать устройство видимым для сопряжения.""" success = self._bt_service.make_discoverable() if self._bt_service.last_error == "power_off": self.status.setText("Статус: питание BT выключено (проверьте rfkill)") elif self._bt_service.last_error == "max_devices": self.status.setText( f"Статус: достигнуто макс. число устройств ({build_info.BLUETOOTH_MAX_PAIRED_DEVICES})" ) elif not success: self.status.setText(f"Статус: ошибка ({self._bt_service.last_error})") else: self.status.setText("Статус: видим для сопряжения (10 сек)") self._discoverable_timer.start(9000) # 9 секунд def _refresh_discoverable(self): """Продлить режим сопряжения.""" self._bt_service.make_discoverable() self._discoverable_timer.start(9000) # 9 секунд def _update_status(self): """Обновить текстовый статус.""" last_mac = self._settings.value("bluetooth/last_mac", "") if not last_mac or last_mac not in self._cards: self.status.setText("Статус: —") return self.status.setText(self._bt_service.get_status_text(last_mac)) def showEvent(self, event): """Экран показан — запускаем режим сопряжения и обновление списка.""" super().showEvent(event) self._refresh_discoverable() # Обновляем список каждые 2 секунды для отслеживания новых устройств self._list_refresh_timer.start(2000) def hideEvent(self, event): """Экран скрыт — останавливаем таймеры.""" super().hideEvent(event) self._discoverable_timer.stop() self._list_refresh_timer.stop()