# playlist_widget.py from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFileDialog, QFrame, QSizePolicy, QListWidget, QListWidgetItem, QAbstractItemView, QMenu, QSplitter ) from PySide6.QtCore import Qt, QTimer from PySide6.QtGui import QFont, QColor, QAction import os # ───────────────────────────────────────── # PLAYLIST ITEM # ───────────────────────────────────────── class PlaylistItem(QListWidgetItem): def __init__(self, filepath: str): self.filepath = filepath self.filename = os.path.basename(filepath) display = os.path.splitext(self.filename)[0] super().__init__(display) self.setFont(QFont("Courier New", 10, QFont.Bold)) self.setForeground(QColor("#cccccc")) self.setToolTip(filepath) # ───────────────────────────────────────── # HOUR DISPLAY + CLOCK # ───────────────────────────────────────── class HourDisplay(QFrame): def __init__(self, parent=None): super().__init__(parent) self.setFrameStyle(QFrame.Box | QFrame.Sunken) self.setFixedHeight(44) self.setStyleSheet(""" QFrame { background-color: #0a0a1a; border: 1px solid #333; border-radius: 3px; } """) layout = QHBoxLayout(self) layout.setContentsMargins(8, 2, 8, 2) self.hour_label = QLabel("HOUR --:--") self.hour_label.setFont(QFont("Courier New", 12, QFont.Bold)) self.hour_label.setStyleSheet( "color: #4488ff; background: transparent; border: none;" ) self.hour_label.setAlignment(Qt.AlignCenter) layout.addWidget(self.hour_label) self.clock_label = QLabel("00:00:00") self.clock_label.setFont(QFont("Courier New", 12, QFont.Bold)) self.clock_label.setStyleSheet( "color: #aaaaaa; background: transparent; border: none;" ) self.clock_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) layout.addWidget(self.clock_label) self.clock_timer = QTimer(self) self.clock_timer.setInterval(1000) self.clock_timer.timeout.connect(self._update_clock) self.clock_timer.start() self._update_clock() def _update_clock(self): from datetime import datetime now = datetime.now() self.clock_label.setText(now.strftime("%H:%M:%S")) self.hour_label.setText(f"HOUR {now.strftime('%H:00')}") # ───────────────────────────────────────── # DECK SELECTOR # ───────────────────────────────────────── class DeckSelector(QFrame): def __init__(self, parent=None): super().__init__(parent) self.setFixedHeight(36) self.setStyleSheet(""" QFrame { background-color: #1a1a1a; border: 1px solid #333; border-radius: 3px; } """) layout = QHBoxLayout(self) layout.setContentsMargins(6, 2, 6, 2) layout.setSpacing(6) label = QLabel("SEND TO:") label.setFont(QFont("Arial", 9, QFont.Bold)) label.setStyleSheet("color: #888; background: transparent; border: none;") layout.addWidget(label) self.deck1_btn = QPushButton("DECK 1") self.deck1_btn.setCheckable(True) self.deck1_btn.setChecked(True) self.deck1_btn.setFixedHeight(26) self.deck1_btn.setFont(QFont("Arial", 9, QFont.Bold)) self._style_btn(self.deck1_btn, "#00897b") self.deck2_btn = QPushButton("DECK 2") self.deck2_btn.setCheckable(True) self.deck2_btn.setFixedHeight(26) self.deck2_btn.setFont(QFont("Arial", 9, QFont.Bold)) self._style_btn(self.deck2_btn, "#f9ca24") self.deck1_btn.clicked.connect(lambda: self._select("deck1")) self.deck2_btn.clicked.connect(lambda: self._select("deck2")) layout.addWidget(self.deck1_btn) layout.addWidget(self.deck2_btn) layout.addStretch() self._selected = "deck1" def _style_btn(self, btn: QPushButton, accent: str): btn.setStyleSheet(f""" QPushButton {{ background-color: #2a2a2a; color: #888; border: 1px solid #444; border-radius: 3px; padding: 0 8px; }} QPushButton:checked {{ background-color: #1a1a1a; color: {accent}; border: 1px solid {accent}; }} QPushButton:hover {{ background-color: #333; }} """) def _select(self, deck: str): self._selected = deck self.deck1_btn.setChecked(deck == "deck1") self.deck2_btn.setChecked(deck == "deck2") @property def selected_deck(self) -> str: return self._selected # ───────────────────────────────────────── # BASE PLAYLIST PANEL # ───────────────────────────────────────── class BasePlaylistPanel(QWidget): AUDIO_FILTER = ( "Audio Files " "(*.wav *.flac *.mp3 *.m4a *.mp4 *.ogg *.opus *.aac *.aiff)" ) AUDIO_EXTENSIONS = { '.wav', '.flac', '.mp3', '.m4a', '.mp4', '.ogg', '.opus', '.aac', '.aiff' } def __init__(self, parent=None): super().__init__(parent) def _make_list(self) -> QListWidget: lst = QListWidget() lst.setStyleSheet(""" QListWidget { background-color: #0d0d0d; color: #cccccc; border: 1px solid #333; border-radius: 3px; outline: none; } QListWidget::item { padding: 5px 6px; border-bottom: 1px solid #1a1a1a; } QListWidget::item:selected { background-color: #1a3a5a; color: #ffffff; } QListWidget::item:hover { background-color: #1e1e1e; } QScrollBar:vertical { background: #1a1a1a; width: 8px; border-radius: 4px; } QScrollBar::handle:vertical { background: #444; border-radius: 4px; } """) lst.setSelectionMode(QAbstractItemView.SingleSelection) lst.setDragDropMode(QAbstractItemView.InternalMove) lst.setContextMenuPolicy(Qt.CustomContextMenu) return lst def _make_btn(self, text: str, color: str, height: int = 28) -> QPushButton: btn = QPushButton(text) btn.setFixedHeight(height) btn.setFont(QFont("Arial", 9, QFont.Bold)) btn.setStyleSheet(f""" QPushButton {{ background-color: {color}; color: #cccccc; border: 1px solid #444; border-radius: 3px; }} QPushButton:hover {{ background-color: #555; }} QPushButton:pressed {{ background-color: #222; }} """) return btn def _add_files_to(self, lst: QListWidget): paths, _ = QFileDialog.getOpenFileNames( self, "Add Files", os.path.expanduser("~"), self.AUDIO_FILTER ) for path in paths: self._add_item_to(lst, path) self._update_count(lst) def _add_folder_to(self, lst: QListWidget): folder = QFileDialog.getExistingDirectory( self, "Add Folder", os.path.expanduser("~") ) if not folder: return for root_dir, dirs, files in os.walk(folder): dirs.sort() for filename in sorted(files): ext = os.path.splitext(filename)[1].lower() if ext in self.AUDIO_EXTENSIONS: self._add_item_to(lst, os.path.join(root_dir, filename)) self._update_count(lst) def _add_item_to(self, lst: QListWidget, filepath: str): for i in range(lst.count()): item = lst.item(i) if isinstance(item, PlaylistItem) and item.filepath == filepath: return lst.addItem(PlaylistItem(filepath)) def _update_count(self, lst: QListWidget, label: QLabel = None): n = lst.count() if label: label.setText(f"{n} track{'s' if n != 1 else ''}") def _highlight(self, item: PlaylistItem, active: bool = False, queued: bool = False): if active: item.setForeground(QColor("#00ff41")) elif queued: item.setForeground(QColor("#f9ca24")) else: item.setForeground(QColor("#cccccc")) # ───────────────────────────────────────── # TRACK PLAYLIST PANEL # ───────────────────────────────────────── class TrackPlaylistPanel(BasePlaylistPanel): def __init__(self, engine, deck_widgets: dict, parent=None): super().__init__(parent) self.engine = engine self.deck_widgets = deck_widgets self._build_ui() def _build_ui(self): self.setStyleSheet(""" QWidget { background-color: #111111; border: 2px solid #333333; border-radius: 6px; } """) root = QVBoxLayout(self) root.setContentsMargins(8, 8, 8, 8) root.setSpacing(6) header = QLabel("PLAYLIST") header.setFont(QFont("Arial", 12, QFont.Bold)) header.setStyleSheet( "color: #aaaaaa; background: transparent; border: none; padding: 2px;" ) header.setAlignment(Qt.AlignCenter) root.addWidget(header) self.hour_display = HourDisplay() root.addWidget(self.hour_display) self.deck_selector = DeckSelector() root.addWidget(self.deck_selector) self.playlist = self._make_list() self.playlist.itemDoubleClicked.connect(self._send_to_deck) self.playlist.customContextMenuRequested.connect(self._context_menu) root.addWidget(self.playlist) row1 = QHBoxLayout() row1.setSpacing(4) self.btn_add_files = self._make_btn("ADD FILES", "#2a5a8a") self.btn_add_folder = self._make_btn("ADD FOLDER", "#2a4a6a") row1.addWidget(self.btn_add_files) row1.addWidget(self.btn_add_folder) root.addLayout(row1) row2 = QHBoxLayout() row2.setSpacing(4) self.btn_send_deck = self._make_btn("→ LOAD DECK", "#1a5a3a") self.btn_send_queue = self._make_btn("→ QUEUE", "#1a3a2a") row2.addWidget(self.btn_send_deck) row2.addWidget(self.btn_send_queue) root.addLayout(row2) row3 = QHBoxLayout() row3.setSpacing(4) self.btn_remove = self._make_btn("REMOVE", "#5a1a1a") self.btn_clear = self._make_btn("CLEAR ALL", "#3a1a1a") row3.addWidget(self.btn_remove) row3.addWidget(self.btn_clear) root.addLayout(row3) self.count_label = QLabel("0 tracks") self.count_label.setFont(QFont("Courier New", 9)) self.count_label.setStyleSheet( "color: #555; background: transparent; border: none;" ) self.count_label.setAlignment(Qt.AlignCenter) root.addWidget(self.count_label) self.btn_add_files.clicked.connect( lambda: self._add_files_to(self.playlist) ) self.btn_add_folder.clicked.connect( lambda: self._add_folder_to(self.playlist) ) self.btn_send_deck.clicked.connect(self._send_to_deck) self.btn_send_queue.clicked.connect(self._send_to_queue) self.btn_remove.clicked.connect(self._remove_selected) self.btn_clear.clicked.connect(self._clear_all) def _send_to_deck(self, item=None): if item is None: item = self.playlist.currentItem() if not isinstance(item, PlaylistItem): return deck_key = self.deck_selector.selected_deck self.engine.load_track(deck_key, item.filepath) name = os.path.splitext(item.filename)[0] if deck_key in self.deck_widgets: self.deck_widgets[deck_key].set_track_name(name) self._highlight(item, active=True) def _send_to_queue(self): item = self.playlist.currentItem() if not isinstance(item, PlaylistItem): return deck_key = self.deck_selector.selected_deck self.engine.load_queue(deck_key, item.filepath) name = os.path.splitext(item.filename)[0] if deck_key in self.deck_widgets: self.deck_widgets[deck_key].set_queue_name(name) self._highlight(item, queued=True) def _remove_selected(self): row = self.playlist.currentRow() if row >= 0: self.playlist.takeItem(row) self._update_count(self.playlist, self.count_label) def _clear_all(self): self.playlist.clear() self._update_count(self.playlist, self.count_label) def _context_menu(self, position): item = self.playlist.itemAt(position) if not isinstance(item, PlaylistItem): return menu = QMenu(self) menu.setStyleSheet(""" QMenu { background-color: #1e1e1e; color: #cccccc; border: 1px solid #444; } QMenu::item:selected { background-color: #1a3a5a; } """) def load(deck_key): self.engine.load_track(deck_key, item.filepath) name = os.path.splitext(item.filename)[0] if deck_key in self.deck_widgets: self.deck_widgets[deck_key].set_track_name(name) self._highlight(item, active=True) def queue(deck_key): self.engine.load_queue(deck_key, item.filepath) name = os.path.splitext(item.filename)[0] if deck_key in self.deck_widgets: self.deck_widgets[deck_key].set_queue_name(name) self._highlight(item, queued=True) a1 = QAction("Load → Deck 1", self) a2 = QAction("Load → Deck 2", self) a3 = QAction("Queue → Deck 1", self) a4 = QAction("Queue → Deck 2", self) ar = QAction("Remove", self) a1.triggered.connect(lambda: load("deck1")) a2.triggered.connect(lambda: load("deck2")) a3.triggered.connect(lambda: queue("deck1")) a4.triggered.connect(lambda: queue("deck2")) ar.triggered.connect( lambda: self.playlist.takeItem(self.playlist.row(item)) ) menu.addAction(a1) menu.addAction(a2) menu.addSeparator() menu.addAction(a3) menu.addAction(a4) menu.addSeparator() menu.addAction(ar) menu.exec(self.playlist.viewport().mapToGlobal(position)) self._update_count(self.playlist, self.count_label) # ───────────────────────────────────────── # CART PLAYLIST PANEL # ───────────────────────────────────────── class CartPlaylistPanel(BasePlaylistPanel): def __init__(self, engine, cart_slots: list, parent=None): super().__init__(parent) self.engine = engine self.cart_slots = cart_slots self._build_ui() def _build_ui(self): self.setStyleSheet(""" QWidget { background-color: #111111; border: 2px solid #cc3300; border-radius: 6px; } """) root = QVBoxLayout(self) root.setContentsMargins(8, 8, 8, 8) root.setSpacing(6) header = QLabel("CART PLAYLIST") header.setFont(QFont("Arial", 12, QFont.Bold)) header.setStyleSheet( "color: #cc3300; background: transparent; border: none; padding: 2px;" ) header.setAlignment(Qt.AlignCenter) root.addWidget(header) self.playlist = self._make_list() self.playlist.itemDoubleClicked.connect(self._send_to_next_free_cart) self.playlist.customContextMenuRequested.connect(self._context_menu) root.addWidget(self.playlist) row1 = QHBoxLayout() row1.setSpacing(4) self.btn_add_files = self._make_btn("ADD FILES", "#5a2a00") self.btn_add_folder = self._make_btn("ADD FOLDER", "#4a2000") row1.addWidget(self.btn_add_files) row1.addWidget(self.btn_add_folder) root.addLayout(row1) row2 = QHBoxLayout() row2.setSpacing(4) self.btn_remove = self._make_btn("REMOVE", "#5a1a1a") self.btn_clear = self._make_btn("CLEAR ALL", "#3a1a1a") row2.addWidget(self.btn_remove) row2.addWidget(self.btn_clear) root.addLayout(row2) self.count_label = QLabel("0 tracks") self.count_label.setFont(QFont("Courier New", 9)) self.count_label.setStyleSheet( "color: #555; background: transparent; border: none;" ) self.count_label.setAlignment(Qt.AlignCenter) root.addWidget(self.count_label) self.btn_add_files.clicked.connect( lambda: self._add_files_to(self.playlist) ) self.btn_add_folder.clicked.connect( lambda: self._add_folder_to(self.playlist) ) self.btn_remove.clicked.connect(self._remove_selected) self.btn_clear.clicked.connect(self._clear_all) def _send_to_cart(self, item: PlaylistItem, cart_index: int): if not isinstance(item, PlaylistItem): return slot = self.cart_slots[cart_index] slot.load_track(item.filepath) self._highlight(item, active=True) def _send_to_next_free_cart(self, item=None): if item is None: item = self.playlist.currentItem() if not isinstance(item, PlaylistItem): return for i in range(4): cart_key = f"cart{i + 1}" if self.engine.decks[cart_key].track is None: self._send_to_cart(item, i) return self._send_to_cart(item, 0) def _remove_selected(self): row = self.playlist.currentRow() if row >= 0: self.playlist.takeItem(row) self._update_count(self.playlist, self.count_label) def _clear_all(self): self.playlist.clear() self._update_count(self.playlist, self.count_label) def _context_menu(self, position): item = self.playlist.itemAt(position) if not isinstance(item, PlaylistItem): return menu = QMenu(self) menu.setStyleSheet(""" QMenu { background-color: #1e1e1e; color: #cccccc; border: 1px solid #444; } QMenu::item:selected { background-color: #1a3a5a; } """) for i in range(4): action = QAction(f"Load → Cart {i + 1}", self) action.triggered.connect( lambda checked, idx=i: self._send_to_cart(item, idx) ) menu.addAction(action) menu.addSeparator() ar = QAction("Remove", self) ar.triggered.connect( lambda: self.playlist.takeItem(self.playlist.row(item)) ) menu.addAction(ar) menu.exec(self.playlist.viewport().mapToGlobal(position)) self._update_count(self.playlist, self.count_label) # ───────────────────────────────────────── # COMBINED PLAYLIST WIDGET # ───────────────────────────────────────── class PlaylistWidget(QWidget): def __init__(self, engine, deck_widgets: dict, cart_slots: list, parent=None): super().__init__(parent) self.engine = engine # Expandable - no fixed width self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.setMinimumWidth(200) root = QVBoxLayout(self) root.setContentsMargins(0, 0, 0, 0) root.setSpacing(0) splitter = QSplitter(Qt.Vertical) splitter.setStyleSheet(""" QSplitter::handle { background-color: #333; height: 4px; } """) self.track_panel = TrackPlaylistPanel(engine, deck_widgets) self.cart_panel = CartPlaylistPanel(engine, cart_slots) splitter.addWidget(self.track_panel) splitter.addWidget(self.cart_panel) splitter.setSizes([600, 400]) root.addWidget(splitter)