from __future__ import annotations from PySide6.QtCore import QSize, Qt from PySide6.QtWidgets import QFrame, QLabel, QListWidgetItem, QVBoxLayout from main_window.constants import SINGBOX_STATUS_ROLE from transport_protocol_summary import transport_protocol_summary class SingBoxCardsMixin: def _singbox_client_protocol_summary(self, client) -> str: protocol_txt = transport_protocol_summary(client) if protocol_txt == "n/a": cid = str(getattr(client, "id", "") or "").strip() if ( cid and cid == str(self._singbox_editor_profile_client_id or "").strip() and str(self._singbox_editor_protocol or "").strip() ): protocol_txt = str(self._singbox_editor_protocol).strip().lower() return protocol_txt def _make_singbox_profile_card_widget( self, *, name: str, protocol_txt: str, status: str, latency_txt: str, cid: str, ) -> QFrame: frame = QFrame() frame.setObjectName("singboxProfileCard") lay = QVBoxLayout(frame) lay.setContentsMargins(10, 8, 10, 8) lay.setSpacing(2) lbl_name = QLabel(name) lbl_name.setObjectName("cardName") lbl_name.setAlignment(Qt.AlignHCenter) lay.addWidget(lbl_name) lbl_proto = QLabel(protocol_txt) lbl_proto.setObjectName("cardProto") lbl_proto.setAlignment(Qt.AlignHCenter) lay.addWidget(lbl_proto) lbl_state = QLabel(f"{str(status or '').upper()} ยท {latency_txt}") lbl_state.setObjectName("cardState") lbl_state.setAlignment(Qt.AlignHCenter) lay.addWidget(lbl_state) frame.setToolTip(f"{cid}\n{protocol_txt}\nstatus={status}") return frame def _style_singbox_profile_card_widget( self, card: QFrame, *, active: bool, selected: bool, ) -> None: if active and selected: bg = "#c7f1d5" border = "#208f47" name_color = "#11552e" meta_color = "#1f6f43" elif active: bg = "#eafaf0" border = "#2f9e44" name_color = "#14532d" meta_color = "#1f6f43" elif selected: bg = "#e8f1ff" border = "#2f80ed" name_color = "#1b2f50" meta_color = "#28568a" else: bg = "#f7f7f7" border = "#c9c9c9" name_color = "#202020" meta_color = "#666666" card.setStyleSheet( f""" QFrame#singboxProfileCard {{ border: 1px solid {border}; border-radius: 6px; background: {bg}; }} QLabel#cardName {{ color: {name_color}; font-weight: 600; }} QLabel#cardProto {{ color: {meta_color}; }} QLabel#cardState {{ color: {meta_color}; }} """ ) def _refresh_singbox_profile_card_styles(self) -> None: current_id = self._selected_transport_engine_id() for i in range(self.lst_singbox_profile_cards.count()): item = self.lst_singbox_profile_cards.item(i) cid = str(item.data(Qt.UserRole) or "").strip() status = str(item.data(SINGBOX_STATUS_ROLE) or "").strip().lower() card = self.lst_singbox_profile_cards.itemWidget(item) if not isinstance(card, QFrame): continue self._style_singbox_profile_card_widget( card, active=(status == "up"), selected=bool(current_id and cid == current_id), ) def _render_singbox_profile_cards(self) -> None: current_id = self._selected_transport_engine_id() self.lst_singbox_profile_cards.blockSignals(True) self.lst_singbox_profile_cards.clear() selected_item = None if not self._transport_api_supported: item = QListWidgetItem("Transport API unavailable") item.setFlags(item.flags() & ~Qt.ItemIsEnabled & ~Qt.ItemIsSelectable) self.lst_singbox_profile_cards.addItem(item) self.lst_singbox_profile_cards.blockSignals(False) return if not self._transport_clients: item = QListWidgetItem("No SingBox profiles configured") item.setFlags(item.flags() & ~Qt.ItemIsEnabled & ~Qt.ItemIsSelectable) self.lst_singbox_profile_cards.addItem(item) self.lst_singbox_profile_cards.blockSignals(False) return for c in self._transport_clients: cid = str(getattr(c, "id", "") or "").strip() if not cid: continue name = str(getattr(c, "name", "") or "").strip() or cid status, latency, _last_error, _last_check = self._transport_live_health_for_client(c) latency_txt = f"{latency}ms" if latency > 0 else "no ping" protocol_txt = self._singbox_client_protocol_summary(c) item = QListWidgetItem("") item.setData(Qt.UserRole, cid) item.setData(SINGBOX_STATUS_ROLE, status) item.setSizeHint(QSize(228, 78)) self.lst_singbox_profile_cards.addItem(item) self.lst_singbox_profile_cards.setItemWidget( item, self._make_singbox_profile_card_widget( name=name, protocol_txt=protocol_txt, status=status, latency_txt=latency_txt, cid=cid, ), ) if current_id and cid == current_id: selected_item = item if selected_item is not None: self.lst_singbox_profile_cards.setCurrentItem(selected_item) elif self.lst_singbox_profile_cards.count() > 0: self.lst_singbox_profile_cards.setCurrentRow(0) self.lst_singbox_profile_cards.blockSignals(False) self._refresh_singbox_profile_card_styles() def _sync_singbox_profile_card_selection(self, cid: str) -> None: if self._syncing_singbox_selection: return self._syncing_singbox_selection = True try: self.lst_singbox_profile_cards.blockSignals(True) self.lst_singbox_profile_cards.clearSelection() target = str(cid or "").strip() if target: for i in range(self.lst_singbox_profile_cards.count()): item = self.lst_singbox_profile_cards.item(i) if str(item.data(Qt.UserRole) or "").strip() == target: self.lst_singbox_profile_cards.setCurrentItem(item) break self.lst_singbox_profile_cards.blockSignals(False) finally: self._syncing_singbox_selection = False self._refresh_singbox_profile_card_styles() def _select_transport_engine_by_id(self, cid: str) -> bool: target = str(cid or "").strip() if not target: return False idx = self.cmb_transport_engine.findData(target) if idx < 0: return False if idx != self.cmb_transport_engine.currentIndex(): self.cmb_transport_engine.setCurrentIndex(idx) else: self._sync_singbox_profile_card_selection(target) self._sync_selected_singbox_profile_link(silent=True) self._load_singbox_editor_for_selected(silent=True) self._update_transport_engine_view() return True