# deck_widget.py from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame, QSizePolicy, QInputDialog ) from PySide6.QtCore import Qt, QTimer, Signal, QSize from PySide6.QtGui import QFont, QColor import os from icons import ( icon_play, icon_pause, icon_stop, icon_skip_start, icon_skip_end, icon_skip_back, icon_skip_forward, icon_eject ) 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_btn = QPushButton("NO TRACK LOADED") self.track_btn.setFont(QFont("Courier New", 13, QFont.Bold)) self.track_btn.setStyleSheet(""" QPushButton { color: #00ff41; background: transparent; border: none; text-align: left; padding: 0px; } QPushButton:hover { color: #ffffff; } """) self.track_btn.setCursor(Qt.PointingHandCursor) self.track_btn.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_btn) layout.addLayout(time_row) layout.addLayout(status_row) def update_track(self, name: str): if len(name) > 40: name = name[:37] + "..." self.track_btn.setText(name) def track_name(self) -> str: return self.track_btn.text() 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;") def flash_warning(self, active: bool): if active: self.elapsed_label.setStyleSheet("color: #ff4444; background: transparent;") self.remaining_label.setStyleSheet("color: #ff4444; background: transparent;") else: self.elapsed_label.setStyleSheet("color: #00ff41; background: transparent;") self.remaining_label.setStyleSheet("color: #00cc33; background: transparent;") def make_icon_button(icon, color="#3a3a3a", icon_color="white", width=52, height=48): btn = QPushButton() btn.setFixedSize(width, height) btn.setIconSize(QSize(22, 22)) btn.setIcon(icon) btn.setStyleSheet(f""" QPushButton {{ background-color: {color}; border: 1px solid #555; border-radius: 4px; }} QPushButton:hover {{ background-color: #555; }} QPushButton:pressed {{ background-color: #222; border: 1px solid #888; }} """) return btn class DeckWidget(QWidget): track_started = Signal(str) def __init__(self, deck_key: str, label: str, accent_color: str, engine, parent=None, mode='deck'): super().__init__(parent) self.deck_key = deck_key self.engine = engine self.accent_color = accent_color self.current_theme = None self.flash_state = False self._playing_icon = False self.mode = mode self.deck_label = label 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.flash_timer = QTimer(self) self.flash_timer.setInterval(500) self.flash_timer.timeout.connect(self._toggle_flash) self.flash_timer.start() def _build_ui(self): 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) self.header = QLabel(self.deck_label) self.header.setFont(QFont("Courier New", 24, QFont.Bold)) self.header.setStyleSheet(f""" color: {self.accent_color}; background: transparent; border: none; padding: 4px; """) self.header.setAlignment(Qt.AlignCenter) root.addWidget(self.header) self.lcd = LCDDisplay() root.addWidget(self.lcd) transport = QHBoxLayout() transport.setSpacing(6) self.btn_to_start = make_icon_button(icon_skip_start()) self.btn_back_15 = make_icon_button(icon_skip_back(), icon_color="#aaaaaa") self.btn_play = make_icon_button(icon_play(), "#1a6b3a", "#00ff41", 68, 48) self.btn_fwd_15 = make_icon_button(icon_skip_forward(), icon_color="#aaaaaa") self.btn_to_end = make_icon_button(icon_skip_end()) self.btn_stop = make_icon_button(icon_stop(), "#8a1a1a", "#ff4444") 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) if self.mode == 'turn': self.btn_to_start.setVisible(False) self.btn_back_15.setVisible(False) self.btn_fwd_15.setVisible(False) self.btn_to_end.setVisible(False) def apply_theme(self, theme: dict): self.current_theme = theme border_color = theme['border'] bg_color = theme['bg2'] self.setStyleSheet(f""" QWidget {{ background-color: {bg_color}; border: 2px solid {border_color}; border-radius: 6px; }} """) self.lcd.setStyleSheet(f""" QFrame {{ background-color: {theme['bg']}; border: 2px solid {border_color}; border-radius: 4px; }} """) def _toggle_flash(self): self.flash_state = not self.flash_state def _connect_signals(self): self.btn_play.clicked.connect(self._toggle_play) self.btn_stop.clicked.connect(self._stop_eject) self.lcd.track_btn.clicked.connect(self._edit_track_name) if self.mode != 'turn': 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)) def _edit_track_name(self): current = self.lcd.track_btn.text() if current == "NO TRACK LOADED": current = "" name, ok = QInputDialog.getText(self, "Track Name", "Track name:", text=current) if ok: if not name.strip(): name = "NO TRACK LOADED" self.lcd.update_track(name.strip()) if self.mode == 'turn': deck = self.engine.decks[self.deck_key] if self.mode == 'turn' and name.strip() != "NO TRACK LOADED": dur, ok2 = QInputDialog.getText(self, "Duration", "Duration (MM:SS):", text="00:00") if ok2 and ':' in dur: parts = dur.split(':') try: total_sec = int(parts[0]) * 60 + int(parts[1]) deck = self.engine.decks[self.deck_key] deck.manual_duration = float(total_sec) except ValueError: pass 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) def toggle_play(self): self._toggle_play() def cue(self): if self.mode == 'turn': return 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.setIcon(icon_play("#00ff41")) self._playing_icon = False self.btn_play.setStyleSheet(""" QPushButton { background-color: #1a6b3a; 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) def _toggle_play(self): deck = self.engine.decks[self.deck_key] if deck.track is None and self.mode != 'turn': return if deck.playing: if self.mode == 'turn': deck.playing = False else: deck.pause() 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: 4px; } QPushButton:hover { background-color: #2a7b4a; } """) self.lcd.update_status("PAUSED" if self.mode != 'turn' else "STOPPED") self.lcd.set_playing(False) else: if self.mode == 'turn': deck.playing = True else: deck.play() 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: 4px; } QPushButton:hover { background-color: #00cc44; } """) self.lcd.update_status("PLAYING") self.lcd.set_playing(True) if self.mode == 'turn': deck.manual_elapsed = 0.0 name = self.lcd.track_btn.text() if name and name != "NO TRACK LOADED": self.track_started.emit(f"[TURN] {name}") elif deck.track: self.track_started.emit(deck.track.filepath) def _stop_eject(self): deck = self.engine.decks[self.deck_key] if self.mode == 'turn': deck.playing = False deck.manual_elapsed = 0.0 else: deck.stop() 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: 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) def _refresh_display(self): deck = self.engine.decks[self.deck_key] if self.mode == 'turn': if deck.playing: deck.manual_elapsed += 0.1 if deck.manual_duration > 0 and deck.manual_elapsed >= deck.manual_duration: deck.playing = False self.btn_play.setIcon(icon_play("#00ff41")) self._playing_icon = False self.lcd.update_status("STOPPED") self.lcd.set_playing(False) elapsed_s = int(deck.manual_elapsed) elapsed = f"{elapsed_s // 60:02d}:{elapsed_s % 60:02d}" if deck.manual_duration > 0: remaining = max(0, deck.manual_duration - deck.manual_elapsed) remaining_s = int(remaining) remaining_str = f"-{remaining_s // 60:02d}:{remaining_s % 60:02d}" else: remaining_str = "-00:00" self.lcd.update_time(elapsed, remaining_str) return track = deck.track playing = deck.playing position = deck.position queued = deck.queued if not playing and track is not None: if position >= track.num_samples - 1: if queued: deck.load(queued) self.lcd.update_track(os.path.splitext(os.path.basename(queued.filepath))[0]) deck.queued = None self.lcd.update_queue("") self.lcd.update_status("LOADED") if not playing and self._playing_icon: 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: 4px; } QPushButton:hover { background-color: #2a7b4a; } """) self.lcd.update_status("STOPPED") self.lcd.set_playing(False) if track: self.lcd.update_time(deck.get_elapsed(), deck.get_remaining()) position_pct = position / track.num_samples remaining_sec = track.duration - (position / track.sr) in_warning = (remaining_sec < 20) or (position_pct > 0.85) if playing and in_warning: flash_color = '#ff4444' if self.flash_state else self.accent_color self.header.setStyleSheet(f""" color: {flash_color}; background: transparent; border: none; padding: 4px; """) self.lcd.flash_warning(self.flash_state) else: self.header.setStyleSheet(f""" color: {self.accent_color}; background: transparent; border: none; padding: 4px; """) self.lcd.flash_warning(False) if queued: self.lcd.update_queue(os.path.basename(queued.filepath))