# cart_widget.py from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame, QSizePolicy, QCheckBox, QScrollArea ) from PySide6.QtCore import Qt, QTimer, QSize, Signal from PySide6.QtGui import QFont import os from icons import icon_play, icon_pause, icon_eject, icon_delete class CartPlayerWidget(QFrame): """Single active cart player with visible 3-item autoload queue.""" cart_triggered = Signal(str) def __init__(self, engine, parent=None): super().__init__(parent) self.engine = engine self._playing_icon = False self.setFrameStyle(QFrame.Box | QFrame.Raised) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setFixedHeight(160) self._build_ui() self._connect_signals() self.timer = QTimer(self) self.timer.setInterval(100) self.timer.timeout.connect(self._refresh_display) self.timer.start() self.queue_timer = QTimer(self) self.queue_timer.setInterval(500) self.queue_timer.timeout.connect(self._refresh_queue) self.queue_timer.start() def _build_ui(self): self.setStyleSheet(""" QFrame { background-color: #111; border: 2px solid #cc3300; border-radius: 6px; } """) root = QHBoxLayout(self) root.setContentsMargins(10, 8, 10, 8) root.setSpacing(10) # ── Left: Cart player box ────────────────────────────────── player_frame = QFrame() player_frame.setStyleSheet(""" QFrame { background-color: #1a1a1a; border: 1px solid #333; border-radius: 5px; } """) player_frame.setFixedWidth(320) player_layout = QVBoxLayout(player_frame) player_layout.setContentsMargins(10, 8, 10, 8) player_layout.setSpacing(6) header_row = QHBoxLayout() badge = QLabel("1") badge.setFixedSize(26, 26) badge.setAlignment(Qt.AlignCenter) badge.setFont(QFont("Arial", 12, QFont.Bold)) badge.setStyleSheet(""" background-color: #cc3300; color: white; border-radius: 15px; border: none; """) header_row.addWidget(badge) cart_label = QLabel("CART") cart_label.setFont(QFont("Arial", 12, QFont.Bold)) cart_label.setStyleSheet("color: #cc3300; background: transparent; border: none;") header_row.addWidget(cart_label) self.continue_toggle = QCheckBox("CONTINUE") self.continue_toggle.setFont(QFont("Arial", 9, QFont.Bold)) self.continue_toggle.setStyleSheet(""" QCheckBox { color: #888; background: transparent; border: none; spacing: 4px; } QCheckBox::indicator { width: 16px; height: 16px; border: 1px solid #555; border-radius: 3px; background-color: #1a1a1a; } QCheckBox::indicator:checked { background-color: #cc3300; border-color: #ff6644; } QCheckBox::indicator:hover { border-color: #888; } """) self.continue_toggle.setToolTip("Auto-play next track from queue after current finishes") self.continue_toggle.stateChanged.connect(self._on_continue_toggled) header_row.addWidget(self.continue_toggle) header_row.addStretch() player_layout.addLayout(header_row) # LCD display self.lcd_frame = QFrame() self.lcd_frame.setStyleSheet(""" QFrame { background-color: #0a1a0a; border: 1px solid #1a2a1a; border-radius: 3px; } """) lcd_layout = QHBoxLayout(self.lcd_frame) lcd_layout.setContentsMargins(8, 4, 8, 4) lcd_layout.setSpacing(6) lcd_left = QVBoxLayout() lcd_left.setSpacing(2) self.track_label = QLabel("EMPTY") self.track_label.setFont(QFont("Courier New", 9, QFont.Bold)) self.track_label.setStyleSheet("color: #00ff41; background: transparent; border: none;") self.track_label.setMinimumWidth(180) time_row = QHBoxLayout() self.elapsed_label = QLabel("00:00") self.elapsed_label.setFont(QFont("Courier New", 18, QFont.Bold)) self.elapsed_label.setStyleSheet("color: #00ff41; background: transparent; border: none;") self.remaining_label = QLabel("-00:00") self.remaining_label.setFont(QFont("Courier New", 10, QFont.Bold)) self.remaining_label.setStyleSheet("color: #00cc33; background: transparent; border: none;") self.remaining_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) time_row.addWidget(self.elapsed_label) time_row.addStretch() time_row.addWidget(self.remaining_label) self.status_label = QLabel("STOPPED") self.status_label.setFont(QFont("Courier New", 8)) self.status_label.setStyleSheet("color: #008822; background: transparent; border: none;") lcd_left.addWidget(self.track_label) lcd_left.addLayout(time_row) lcd_left.addWidget(self.status_label) lcd_layout.addLayout(lcd_left) btn_col = QVBoxLayout() btn_col.setSpacing(3) self.btn_play = QPushButton() self.btn_play.setFixedSize(56, 36) self.btn_play.setIconSize(QSize(22, 22)) self.btn_play.setIcon(icon_play("#00ff41")) self.btn_play.setStyleSheet(""" QPushButton { background-color: #1a6b3a; border: 1px solid #555; border-radius: 3px; } QPushButton:hover { background-color: #2a7b4a; } QPushButton:pressed { background-color: #0a5a2a; } """) self.btn_eject = QPushButton() self.btn_eject.setFixedSize(56, 28) self.btn_eject.setIconSize(QSize(16, 16)) self.btn_eject.setIcon(icon_eject("#ff4444")) self.btn_eject.setStyleSheet(""" QPushButton { background-color: #8a1a1a; border: 1px solid #555; border-radius: 3px; } QPushButton:hover { background-color: #9a2a2a; } QPushButton:pressed { background-color: #6a0a0a; } """) btn_col.addWidget(self.btn_play) btn_col.addWidget(self.btn_eject) lcd_layout.addLayout(btn_col) player_layout.addWidget(self.lcd_frame) root.addWidget(player_frame) # ── Right: Queue panel ────────────────────────────────────── queue_frame = QFrame() queue_frame.setStyleSheet(""" QFrame { background-color: #111; border: 1px solid #2a2010; border-radius: 5px; } """) queue_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) queue_layout = QVBoxLayout(queue_frame) queue_layout.setContentsMargins(8, 6, 8, 6) queue_layout.setSpacing(2) queue_header = QHBoxLayout() ql = QLabel("AUTOLOAD QUEUE") ql.setFont(QFont("Arial", 9, QFont.Bold)) ql.setStyleSheet("color: #ffaa00; background: transparent; border: none;") queue_header.addWidget(ql) queue_header.addStretch() self.queue_count = QLabel("0/3") self.queue_count.setFont(QFont("Courier New", 9, QFont.Bold)) self.queue_count.setStyleSheet("color: #555; background: transparent; border: none;") queue_header.addWidget(self.queue_count) clear_btn = QPushButton("CLR") clear_btn.setFixedSize(40, 20) clear_btn.setFont(QFont("Arial", 7, QFont.Bold)) clear_btn.setStyleSheet(""" QPushButton { background-color: #3a1a1a; color: #ff6666; border: 1px solid #555; border-radius: 3px; } QPushButton:hover { background-color: #5a2a2a; } """) clear_btn.clicked.connect(self.engine.clear_cart_queue) queue_header.addWidget(clear_btn) queue_layout.addLayout(queue_header) self.queue_items = [] for i in range(3): item = QLabel(f" — ") item.setFont(QFont("Courier New", 9)) item.setStyleSheet("color: #555; background: transparent; border: none; padding: 2px 4px;") item.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) queue_layout.addWidget(item) self.queue_items.append(item) queue_layout.addStretch() root.addWidget(queue_frame) def _connect_signals(self): self.btn_play.clicked.connect(self._toggle_play) self.btn_eject.clicked.connect(self._eject) def _on_continue_toggled(self, state): self.engine.cart_continue = bool(state) def load_track(self, filepath: str): self.engine.load_cart_track("cart1", filepath) name = os.path.basename(filepath) short = name[:32] + "..." if len(name) > 32 else name self.track_label.setText(short) self.status_label.setText("LOADED") self.elapsed_label.setText("00:00") self.remaining_label.setText("-00:00") def toggle(self): deck = self.engine.decks["cart1"] if deck.track is None: return if deck.playing: deck.stop() deck.go_to_start() self._set_ui_stopped() self.status_label.setText("READY") else: deck.play() self._set_ui_playing() self.engine.log_cart_trigger(deck.track.filename) self.cart_triggered.emit(deck.track.filename) def _set_ui_playing(self): self.btn_play.setIcon(icon_pause("#ffffff")) self._playing_icon = True self.btn_play.setStyleSheet(""" QPushButton { background-color: #00aa33; border: 1px solid #00ff41; border-radius: 3px; } QPushButton:hover { background-color: #00cc44; } """) self.status_label.setText("PLAYING") self.elapsed_label.setStyleSheet("color: #00ff41; background: transparent; border: none;") def _set_ui_stopped(self): self.btn_play.setIcon(icon_play("#00ff41")) self._playing_icon = False self.btn_play.setStyleSheet(""" QPushButton { background-color: #1a6b3a; border: 1px solid #555; border-radius: 3px; } QPushButton:hover { background-color: #2a7b4a; } """) self.elapsed_label.setText("00:00") self.remaining_label.setText("-00:00") self.elapsed_label.setStyleSheet("color: #00aa2a; background: transparent; border: none;") def _toggle_play(self): deck = self.engine.decks["cart1"] if deck.track is None: return if deck.playing: deck.pause() self._set_ui_stopped() self.status_label.setText("PAUSED") else: deck.play() self._set_ui_playing() self.engine.log_cart_trigger(deck.track.filename) self.cart_triggered.emit(deck.track.filename) def _eject(self): deck = self.engine.decks["cart1"] deck.stop() self._set_ui_stopped() self.track_label.setText("EMPTY") self.status_label.setText("STOPPED") def _refresh_display(self): deck = self.engine.decks["cart1"] track = deck.track playing = deck.playing if not playing and self._playing_icon: self._set_ui_stopped() self.status_label.setText("STOPPED") if track: self.elapsed_label.setText(deck.get_elapsed()) self.remaining_label.setText(deck.get_remaining()) def _refresh_queue(self): q = self.engine.cart_queue qlen = len(q) self.queue_count.setText(f"{qlen}/3") if qlen == 0: self.queue_count.setStyleSheet("color: #555; background: transparent; border: none;") elif qlen < 3: self.queue_count.setStyleSheet("color: #ffaa00; background: transparent; border: none;") else: self.queue_count.setStyleSheet("color: #ff4444; background: transparent; border: none;") for i in range(3): if i < qlen: name = q[i].filename name = name[:35] + "..." if len(name) > 35 else name self.queue_items[i].setText(f" {i+1}. {name}") self.queue_items[i].setStyleSheet("color: #ffaa00; background: transparent; border: none; padding: 2px 4px;") else: self.queue_items[i].setText(" — ") self.queue_items[i].setStyleSheet("color: #333; background: transparent; border: none; padding: 2px 4px;") if not self.engine.cart_continue and self.continue_toggle.isChecked(): self.continue_toggle.blockSignals(True) self.continue_toggle.setChecked(False) self.continue_toggle.blockSignals(False) def apply_theme(self, theme: dict): self.lcd_frame.setStyleSheet(f""" QFrame {{ background-color: {theme['bg']}; border: 1px solid {theme['border']}; border-radius: 3px; }} """)