Files
gti_radiostudio/playlist_widget.py
T

885 lines
31 KiB
Python
Raw Normal View History

2026-04-29 16:21:44 +10:00
# playlist_widget.py
2026-07-17 20:44:15 +10:00
import json
import os
from datetime import datetime, timedelta
2026-04-29 16:21:44 +10:00
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFileDialog, QFrame, QSizePolicy,
QListWidget, QListWidgetItem, QAbstractItemView,
2026-07-17 20:44:15 +10:00
QMenu, QSplitter, QTextEdit, QLineEdit
2026-04-29 16:21:44 +10:00
)
2026-07-17 20:44:15 +10:00
from PySide6.QtCore import Qt, QTimer, QSize, Signal
2026-04-29 16:21:44 +10:00
from PySide6.QtGui import QFont, QColor, QAction
from icons import icon_save, icon_load
2026-04-29 16:21:44 +10:00
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)
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')}")
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
2026-07-17 20:44:15 +10:00
# ─────────────────────────────────────────
# SHOW LOG PANEL
# ─────────────────────────────────────────
2026-04-29 16:21:44 +10:00
# ─────────────────────────────────────────
# 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"))
def get_filepaths(self, lst: QListWidget) -> list:
paths = []
for i in range(lst.count()):
item = lst.item(i)
if isinstance(item, PlaylistItem):
paths.append(item.filepath)
return paths
2026-07-17 20:44:15 +10:00
# ─────────────────────────────────────────
# PREVIEW BAR
# ─────────────────────────────────────────
class PreviewBar(QFrame):
def __init__(self, engine, parent=None):
super().__init__(parent)
self.engine = engine
self.setFixedHeight(40)
self.setStyleSheet("""
QFrame {
background-color: #0a0a12;
border: 1px solid #222;
border-radius: 3px;
}
""")
layout = QHBoxLayout(self)
layout.setContentsMargins(6, 2, 6, 2)
layout.setSpacing(6)
self.track_label = QLabel("NO TRACK")
self.track_label.setFont(QFont("Courier New", 9, QFont.Bold))
self.track_label.setStyleSheet("color: #4488ff; background: transparent; border: none;")
self.track_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
layout.addWidget(self.track_label, stretch=1)
self.time_label = QLabel("--:--")
self.time_label.setFont(QFont("Courier New", 12, QFont.Bold))
self.time_label.setStyleSheet("color: #4488ff; background: transparent; border: none;")
layout.addWidget(self.time_label)
self.btn_play = QPushButton("\u25B6")
self.btn_play.setFixedSize(30, 26)
self.btn_play.setFont(QFont("Arial", 10, QFont.Bold))
self.btn_play.setStyleSheet("""
QPushButton {
background-color: #1a3a5a; color: #4488ff;
border: 1px solid #4488ff; border-radius: 3px;
}
QPushButton:hover { background-color: #2a4a6a; }
QPushButton:checked { background-color: #1a5a3a; color: #00ff41; border-color: #00ff41; }
""")
self.btn_play.setCheckable(True)
layout.addWidget(self.btn_play)
self.btn_forward = QPushButton("+10s")
self.btn_forward.setFixedSize(40, 26)
self.btn_forward.setFont(QFont("Arial", 8, QFont.Bold))
self.btn_forward.setStyleSheet("""
QPushButton {
background-color: #2a1a0a; color: #aa6622;
border: 1px solid #553311; border-radius: 3px;
}
QPushButton:hover { background-color: #3a2a1a; }
""")
layout.addWidget(self.btn_forward)
self.btn_play.clicked.connect(self._toggle_preview)
self.btn_forward.clicked.connect(self._skip_forward)
self.timer = QTimer(self)
self.timer.setInterval(100)
self.timer.timeout.connect(self._refresh)
self._playing = False
self._pending_play = False
def set_playlist(self, playlist):
self._playlist = playlist
def _selected_item(self):
item = self._playlist.currentItem()
if isinstance(item, PlaylistItem):
return item
return None
def _toggle_preview(self):
item = self._selected_item()
if item is None:
self.btn_play.setChecked(False)
return
deck = self.engine.decks['preview']
if self._playing:
deck.stop()
self._playing = False
self._pending_play = False
self.btn_play.setChecked(False)
self.track_label.setText("NO TRACK")
self.time_label.setText("--:--")
else:
self.engine.load_track('preview', item.filepath)
self.track_label.setText(item.text()[:30])
self.time_label.setText("00:00")
self._pending_play = True
def _skip_forward(self):
if not self._playing:
return
self.engine.decks['preview'].skip_seconds(10)
def _refresh(self):
deck = self.engine.decks['preview']
with deck.lock:
playing = deck.playing
track = deck.track
pos = deck.position
if self._pending_play and track is not None:
self._pending_play = False
deck.play()
self._playing = True
if playing and track:
s = pos / track.sr
self.time_label.setText(f"{int(s//60):02d}:{int(s%60):02d}")
if pos >= track.num_samples - 1:
self._playing = False
self.btn_play.setChecked(False)
self.track_label.setText("NO TRACK")
self.time_label.setText("--:--")
elif not playing and self._playing:
self._playing = False
self.btn_play.setChecked(False)
2026-04-29 16:21:44 +10:00
# ─────────────────────────────────────────
# 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)
2026-07-17 20:44:15 +10:00
self.preview_bar = PreviewBar(self.engine)
root.addWidget(self.preview_bar)
2026-04-29 16:21:44 +10:00
self.hour_display = HourDisplay()
root.addWidget(self.hour_display)
self.deck_selector = DeckSelector()
root.addWidget(self.deck_selector)
2026-07-17 20:44:15 +10:00
self.search_box = QLineEdit()
self.search_box.setPlaceholderText("search...")
self.search_box.setFont(QFont("Courier New", 9))
self.search_box.setStyleSheet("""
QLineEdit {
background-color: #0d0d0d;
color: #00ff41;
border: 1px solid #333;
border-radius: 3px;
padding: 3px 6px;
}
QLineEdit:focus { border-color: #00ff41; }
""")
self.search_box.textChanged.connect(self._filter_list)
root.addWidget(self.search_box)
2026-04-29 16:21:44 +10:00
self.playlist = self._make_list()
self.playlist.itemDoubleClicked.connect(self._send_to_deck)
self.playlist.customContextMenuRequested.connect(self._context_menu)
2026-07-17 20:44:15 +10:00
self.preview_bar.set_playlist(self.playlist)
2026-04-29 16:21:44 +10:00
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)
2026-07-17 20:44:15 +10:00
def _filter_list(self):
query = self.search_box.text().lower()
for i in range(self.playlist.count()):
item = self.playlist.item(i)
if isinstance(item, PlaylistItem):
item.setHidden(bool(query and query not in item.filename.lower()))
2026-04-29 16:21:44 +10:00
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):
2026-07-17 20:44:15 +10:00
def __init__(self, engine, cart_player, parent=None):
2026-04-29 16:21:44 +10:00
super().__init__(parent)
2026-07-17 20:44:15 +10:00
self.engine = engine
self.cart_player = cart_player
2026-04-29 16:21:44 +10:00
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)
2026-07-17 20:44:15 +10:00
self.search_box = QLineEdit()
self.search_box.setPlaceholderText("search...")
self.search_box.setFont(QFont("Courier New", 9))
self.search_box.setStyleSheet("""
QLineEdit {
background-color: #0d0d0d;
color: #ffaa00;
border: 1px solid #333;
border-radius: 3px;
padding: 3px 6px;
}
QLineEdit:focus { border-color: #ffaa00; }
""")
self.search_box.textChanged.connect(self._filter_list)
root.addWidget(self.search_box)
2026-04-29 16:21:44 +10:00
self.playlist = self._make_list()
2026-07-17 20:44:15 +10:00
self.playlist.itemDoubleClicked.connect(self._send_to_cart)
2026-04-29 16:21:44 +10:00
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)
2026-07-17 20:44:15 +10:00
self.btn_send_cart = self._make_btn("→ LOAD CART", "#3a5a1a")
self.btn_send_queue = self._make_btn("→ QUEUE", "#4a3a00")
row2.addWidget(self.btn_send_cart)
row2.addWidget(self.btn_send_queue)
root.addLayout(row2)
row3 = QHBoxLayout()
row3.setSpacing(4)
2026-04-29 16:21:44 +10:00
self.btn_remove = self._make_btn("REMOVE", "#5a1a1a")
self.btn_clear = self._make_btn("CLEAR ALL", "#3a1a1a")
2026-07-17 20:44:15 +10:00
row3.addWidget(self.btn_remove)
row3.addWidget(self.btn_clear)
root.addLayout(row3)
2026-04-29 16:21:44 +10:00
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)
)
2026-07-17 20:44:15 +10:00
self.btn_send_cart.clicked.connect(self._send_to_cart)
self.btn_send_queue.clicked.connect(self._send_to_cart_queue)
2026-04-29 16:21:44 +10:00
self.btn_remove.clicked.connect(self._remove_selected)
self.btn_clear.clicked.connect(self._clear_all)
2026-07-17 20:44:15 +10:00
def _filter_list(self):
query = self.search_box.text().lower()
for i in range(self.playlist.count()):
item = self.playlist.item(i)
if isinstance(item, PlaylistItem):
item.setHidden(bool(query and query not in item.filename.lower()))
def _send_to_cart(self, item=None):
if item is None:
item = self.playlist.currentItem()
2026-04-29 16:21:44 +10:00
if not isinstance(item, PlaylistItem):
return
2026-07-17 20:44:15 +10:00
self.cart_player.load_track(item.filepath)
2026-04-29 16:21:44 +10:00
self._highlight(item, active=True)
2026-07-17 20:44:15 +10:00
def _send_to_cart_queue(self, item=None):
2026-04-29 16:21:44 +10:00
if item is None:
item = self.playlist.currentItem()
if not isinstance(item, PlaylistItem):
return
2026-07-17 20:44:15 +10:00
self.engine.add_to_cart_queue(item.filepath)
self._highlight(item, queued=True)
2026-04-29 16:21:44 +10:00
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; }
""")
2026-07-17 20:44:15 +10:00
a1 = QAction("Load → Cart", self)
a1.triggered.connect(lambda: self._send_to_cart(item))
menu.addAction(a1)
a2 = QAction("Queue → Cart", self)
a2.triggered.connect(lambda: self._send_to_cart_queue(item))
menu.addAction(a2)
2026-04-29 16:21:44 +10:00
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):
PLAYLIST_JSON_FILTER = "JSON Files (*.json)"
2026-04-29 16:21:44 +10:00
def __init__(self, engine, deck_widgets: dict,
2026-07-17 20:44:15 +10:00
cart_player, parent=None):
2026-04-29 16:21:44 +10:00
super().__init__(parent)
self.engine = engine
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.setMinimumWidth(200)
root = QVBoxLayout(self)
root.setContentsMargins(0, 0, 0, 0)
root.setSpacing(4)
bar = QHBoxLayout()
bar.setSpacing(4)
self._save_btn = QPushButton("SAVE")
self._save_btn.setFixedHeight(28)
self._save_btn.setIconSize(QSize(16, 16))
self._save_btn.setIcon(icon_save("#aaaaaa"))
self._save_btn.setFont(QFont("Arial", 9, QFont.Bold))
self._save_btn.setStyleSheet("""
QPushButton {
background-color: #2a4a2a;
color: #aaaaaa;
border: 1px solid #444;
border-radius: 3px;
padding: 0 10px;
}
QPushButton:hover { background-color: #3a5a3a; }
""")
self._save_btn.clicked.connect(self._save_playlist)
self._load_btn = QPushButton("LOAD")
self._load_btn.setFixedHeight(28)
self._load_btn.setIconSize(QSize(16, 16))
self._load_btn.setIcon(icon_load("#aaaaaa"))
self._load_btn.setFont(QFont("Arial", 9, QFont.Bold))
self._load_btn.setStyleSheet("""
QPushButton {
background-color: #3a2a2a;
color: #aaaaaa;
border: 1px solid #444;
border-radius: 3px;
padding: 0 10px;
}
QPushButton:hover { background-color: #4a3a3a; }
""")
self._load_btn.clicked.connect(self._load_playlist)
bar.addWidget(self._save_btn)
bar.addWidget(self._load_btn)
bar.addStretch()
root.addLayout(bar)
2026-04-29 16:21:44 +10:00
splitter = QSplitter(Qt.Vertical)
splitter.setStyleSheet("""
QSplitter::handle {
background-color: #333;
height: 4px;
}
""")
self.track_panel = TrackPlaylistPanel(engine, deck_widgets)
2026-07-17 20:44:15 +10:00
self.cart_panel = CartPlaylistPanel(engine, cart_player)
2026-04-29 16:21:44 +10:00
splitter.addWidget(self.track_panel)
splitter.addWidget(self.cart_panel)
2026-07-17 20:44:15 +10:00
splitter.setSizes([400, 400])
2026-04-29 16:21:44 +10:00
root.addWidget(splitter)
def _save_playlist(self):
path, _ = QFileDialog.getSaveFileName(
self, "Save Playlist",
os.path.expanduser("~/playlist.json"),
self.PLAYLIST_JSON_FILTER
)
if not path:
return
data = {
"version": 1,
"tracks": self.track_panel.get_filepaths(self.track_panel.playlist),
"cart_tracks": self.cart_panel.get_filepaths(self.cart_panel.playlist),
}
with open(path, 'w') as f:
json.dump(data, f, indent=2)
def _load_playlist(self):
path, _ = QFileDialog.getOpenFileName(
self, "Load Playlist",
os.path.expanduser("~"),
self.PLAYLIST_JSON_FILTER
)
if not path:
return
try:
with open(path) as f:
data = json.load(f)
except Exception:
return
tracks = data.get("tracks", [])
cart = data.get("cart_tracks", [])
self.track_panel.playlist.clear()
for fp in tracks:
if os.path.exists(fp):
self.track_panel._add_item_to(self.track_panel.playlist, fp)
self.track_panel._update_count(self.track_panel.playlist,
self.track_panel.count_label)
self.cart_panel.playlist.clear()
for fp in cart:
if os.path.exists(fp):
self.cart_panel._add_item_to(self.cart_panel.playlist, fp)
self.cart_panel._update_count(self.cart_panel.playlist,
self.cart_panel.count_label)