# deck_widget.py from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame, QSizePolicy ) from PySide6.QtCore import Qt, QTimer, Signal from PySide6.QtGui import QFont, QColor import os # ───────────────────────────────────────── # LCD DISPLAY WIDGET # ───────────────────────────────────────── class LCDDisplay(QFrame): def __init__(self, parent=None): super().__init__(parent) self.setFrameStyle(QFrame.Box | QFrame.Sunken) self.setStyleSheet(""" QFrame { background-color: #0a1a0a; border: 2px solid #1a2a1a; border-radius: 4px; } """) layout = QVBoxLayout(self) layout.setContentsMargins(10, 8, 10, 8) layout.setSpacing(4) self.track_label = QLabel("NO TRACK LOADED") self.track_label.setFont(QFont("Courier New", 13, QFont.Bold)) self.track_label.setStyleSheet( "color: #00ff41; background: transparent;" ) self.track_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.track_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) time_row = QHBoxLayout() time_row.setSpacing(4) self.elapsed_label = QLabel("00:00") self.elapsed_label.setFont(QFont("Courier New", 32, QFont.Bold)) self.elapsed_label.setStyleSheet( "color: #00ff41; background: transparent;" ) self.elapsed_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.remaining_label = QLabel("-00:00") self.remaining_label.setFont(QFont("Courier New", 18, QFont.Bold)) self.remaining_label.setStyleSheet( "color: #00cc33; background: transparent;" ) self.remaining_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) time_row.addWidget(self.elapsed_label) time_row.addStretch() time_row.addWidget(self.remaining_label) status_row = QHBoxLayout() self.status_label = QLabel("STOPPED") self.status_label.setFont(QFont("Courier New", 11, QFont.Bold)) self.status_label.setStyleSheet( "color: #008822; background: transparent;" ) self.queue_label = QLabel("QUEUE: EMPTY") self.queue_label.setFont(QFont("Courier New", 10)) self.queue_label.setStyleSheet( "color: #008822; background: transparent;" ) self.queue_label.setAlignment(Qt.AlignRight) status_row.addWidget(self.status_label) status_row.addStretch() status_row.addWidget(self.queue_label) layout.addWidget(self.track_label) layout.addLayout(time_row) layout.addLayout(status_row) def update_track(self, name: str): if len(name) > 40: name = name[:37] + "..." self.track_label.setText(name) def update_time(self, elapsed: str, remaining: str): self.elapsed_label.setText(elapsed) self.remaining_label.setText(remaining) def update_status(self, status: str): self.status_label.setText(status) def update_queue(self, name: str): if name: short = name[:25] + "..." if len(name) > 25 else name self.queue_label.setText(f"QUEUE: {short}") else: self.queue_label.setText("QUEUE: EMPTY") def set_playing(self, playing: bool): colour = "#00ff41" if playing else "#00aa2a" self.elapsed_label.setStyleSheet( f"color: {colour}; background: transparent;" ) # ───────────────────────────────────────── # TRANSPORT BUTTON HELPER # ───────────────────────────────────────── def make_transport_button(text: str, color: str = "#3a3a3a", text_color: str = "white", width: int = 52, height: int = 48) -> QPushButton: btn = QPushButton(text) btn.setFixedSize(width, height) btn.setFont(QFont("Arial", 11, QFont.Bold)) btn.setStyleSheet(f""" QPushButton {{ background-color: {color}; color: {text_color}; border: 1px solid #555; border-radius: 4px; }} QPushButton:hover {{ background-color: #555; }} QPushButton:pressed {{ background-color: #222; border: 1px solid #888; }} """) return btn # ───────────────────────────────────────── # DECK WIDGET # ───────────────────────────────────────── class DeckWidget(QWidget): track_started = Signal(str) # emits filepath when playback begins def __init__(self, deck_key: str, label: str, accent_color: str, engine, parent=None): super().__init__(parent) self.deck_key = deck_key self.engine = engine self.accent_color = accent_color self._build_ui(label) self._connect_signals() self.timer = QTimer(self) self.timer.setInterval(100) self.timer.timeout.connect(self._refresh_display) self.timer.start() def _build_ui(self, label: str): self.setStyleSheet(f""" QWidget {{ background-color: #1c1c1c; border: 2px solid {self.accent_color}; border-radius: 6px; }} """) root = QVBoxLayout(self) root.setContentsMargins(10, 10, 10, 10) root.setSpacing(8) header = QLabel(label) header.setFont(QFont("Arial", 13, QFont.Bold)) header.setStyleSheet(f""" color: {self.accent_color}; background: transparent; border: none; padding: 2px; """) header.setAlignment(Qt.AlignCenter) root.addWidget(header) self.lcd = LCDDisplay() root.addWidget(self.lcd) transport = QHBoxLayout() transport.setSpacing(6) self.btn_to_start = make_transport_button("⏮", "#3a3a3a", "white", 52, 48) self.btn_back_15 = make_transport_button("-15", "#3a3a3a", "#aaaaaa", 52, 48) self.btn_play = make_transport_button("▶", "#1a6b3a", "#00ff41", 68, 48) self.btn_fwd_15 = make_transport_button("+15", "#3a3a3a", "#aaaaaa", 52, 48) self.btn_to_end = make_transport_button("⏭", "#3a3a3a", "white", 52, 48) self.btn_stop = make_transport_button("■", "#8a1a1a", "#ff4444", 52, 48) transport.addWidget(self.btn_to_start) transport.addWidget(self.btn_back_15) transport.addWidget(self.btn_play) transport.addWidget(self.btn_fwd_15) transport.addWidget(self.btn_to_end) transport.addStretch() transport.addWidget(self.btn_stop) root.addLayout(transport) def _connect_signals(self): self.btn_play.clicked.connect(self._toggle_play) self.btn_stop.clicked.connect(self._stop_eject) self.btn_to_start.clicked.connect(self._go_to_start) self.btn_to_end.clicked.connect(self._go_to_end) self.btn_back_15.clicked.connect(lambda: self._scrub(-15)) self.btn_fwd_15.clicked.connect(lambda: self._scrub(15)) # ── PUBLIC — called by playlist ─────────────────────────────────────── def set_track_name(self, name: str): self.lcd.update_track(name) self.lcd.update_status("LOADED") self.lcd.update_time("00:00", "-00:00") def set_queue_name(self, name: str): self.lcd.update_queue(name) # ── PUBLIC MIDI METHODS ─────────────────────────────────────────────── def toggle_play(self): self._toggle_play() def cue(self): deck = self.engine.decks[self.deck_key] if deck.track is None: return if deck.playing: deck.pause() deck.go_to_start() self.btn_play.setText("▶") self.btn_play.setStyleSheet(""" QPushButton { background-color: #1a6b3a; color: #00ff41; border: 1px solid #555; border-radius: 4px; } QPushButton:hover { background-color: #2a7b4a; } """) self.lcd.update_status("CUE") self.lcd.set_playing(False) def scrub(self, delta: float): self._scrub(delta) # ── ACTIONS ─────────────────────────────────────────────────────────── def _toggle_play(self): deck = self.engine.decks[self.deck_key] if deck.track is None: return if deck.playing: deck.pause() self.btn_play.setText("▶") self.btn_play.setStyleSheet(""" QPushButton { background-color: #1a6b3a; color: #00ff41; border: 1px solid #555; border-radius: 4px; } QPushButton:hover { background-color: #2a7b4a; } """) self.lcd.update_status("PAUSED") self.lcd.set_playing(False) else: deck.play() self.btn_play.setText("⏸") self.btn_play.setStyleSheet(""" QPushButton { background-color: #00aa33; color: white; border: 1px solid #00ff41; border-radius: 4px; } QPushButton:hover { background-color: #00cc44; } """) self.lcd.update_status("PLAYING") self.lcd.set_playing(True) if deck.track: self.track_started.emit(deck.track.filepath) def _stop_eject(self): deck = self.engine.decks[self.deck_key] deck.stop() self.btn_play.setText("▶") self.btn_play.setStyleSheet(""" QPushButton { background-color: #1a6b3a; color: #00ff41; border: 1px solid #555; border-radius: 4px; } QPushButton:hover { background-color: #2a7b4a; } """) self.lcd.update_track("NO TRACK LOADED") self.lcd.update_time("00:00", "-00:00") self.lcd.update_status("STOPPED") self.lcd.set_playing(False) def _go_to_start(self): self.engine.decks[self.deck_key].go_to_start() def _go_to_end(self): self.engine.decks[self.deck_key].go_to_end() def _scrub(self, seconds: float): self.engine.decks[self.deck_key].skip_seconds(seconds) # ── DISPLAY REFRESH ─────────────────────────────────────────────────── def _refresh_display(self): deck = self.engine.decks[self.deck_key] if not deck.playing and self.btn_play.text() == "⏸": self.btn_play.setText("▶") self.btn_play.setStyleSheet(""" QPushButton { background-color: #1a6b3a; color: #00ff41; border: 1px solid #555; border-radius: 4px; } QPushButton:hover { background-color: #2a7b4a; } """) self.lcd.update_status("STOPPED") self.lcd.set_playing(False) if deck.track: self.lcd.update_time( deck.get_elapsed(), deck.get_remaining() ) if deck.queued: self.lcd.update_queue( os.path.basename(deck.queued.filepath) )