- Remove vestigial lufs_meter_widget.py and debug_audio.py - Delete unused WaveformWidget/WaveformMonitor classes, dead stubs, duplicate code - Fix turn-mode deck crashes (deck3/deck4 missing from engine.decks, uninitialized manual_elapsed/duration) - Replace raw print() with logging in main.py - Add master_volume MIDI learn signal + settings popup entry - Bump version to 0.5
709 lines
24 KiB
Python
709 lines
24 KiB
Python
# 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, QSize
|
|
from PySide6.QtGui import QFont, QColor, QAction
|
|
import json
|
|
import os
|
|
|
|
from icons import icon_save, icon_load
|
|
|
|
|
|
# ─────────────────────────────────────────
|
|
# 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"))
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────
|
|
# 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):
|
|
PLAYLIST_JSON_FILTER = "JSON Files (*.json)"
|
|
|
|
def __init__(self, engine, deck_widgets: dict,
|
|
cart_slots: list, parent=None):
|
|
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)
|
|
|
|
# Toolbar
|
|
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)
|
|
|
|
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)
|
|
|
|
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) |