Files
gti_radiostudio/cart_widget.py
T

364 lines
13 KiB
Python
Raw Normal View History

2026-04-29 16:21:44 +10:00
# cart_widget.py
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
2026-07-17 20:44:15 +10:00
QPushButton, QFrame, QSizePolicy, QCheckBox, QScrollArea
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
import os
2026-07-17 20:44:15 +10:00
from icons import icon_play, icon_pause, icon_eject, icon_delete
2026-04-29 16:21:44 +10:00
2026-07-17 20:44:15 +10:00
class CartPlayerWidget(QFrame):
"""Single active cart player with visible 3-item autoload queue."""
cart_triggered = Signal(str)
def __init__(self, engine, 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._playing_icon = False
2026-04-29 16:21:44 +10:00
self.setFrameStyle(QFrame.Box | QFrame.Raised)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
2026-07-17 20:44:15 +10:00
self.setFixedHeight(160)
2026-04-29 16:21:44 +10:00
self._build_ui()
self._connect_signals()
self.timer = QTimer(self)
self.timer.setInterval(100)
self.timer.timeout.connect(self._refresh_display)
self.timer.start()
2026-07-17 20:44:15 +10:00
self.queue_timer = QTimer(self)
self.queue_timer.setInterval(500)
self.queue_timer.timeout.connect(self._refresh_queue)
self.queue_timer.start()
2026-04-29 16:21:44 +10:00
def _build_ui(self):
2026-07-17 20:44:15 +10:00
self.setStyleSheet("""
QFrame {
background-color: #111;
border: 2px solid #cc3300;
border-radius: 6px;
}
""")
2026-04-29 16:21:44 +10:00
root = QHBoxLayout(self)
2026-07-17 20:44:15 +10:00
root.setContentsMargins(10, 8, 10, 8)
root.setSpacing(10)
2026-04-29 16:21:44 +10:00
2026-07-17 20:44:15 +10:00
# ── Left: Cart player box ──────────────────────────────────
player_frame = QFrame()
player_frame.setStyleSheet("""
QFrame {
background-color: #1a1a1a;
border: 1px solid #333;
border-radius: 5px;
}
""")
player_frame.setFixedWidth(320)
player_layout = QVBoxLayout(player_frame)
player_layout.setContentsMargins(10, 8, 10, 8)
player_layout.setSpacing(6)
header_row = QHBoxLayout()
badge = QLabel("1")
badge.setFixedSize(26, 26)
2026-04-29 16:21:44 +10:00
badge.setAlignment(Qt.AlignCenter)
2026-07-17 20:44:15 +10:00
badge.setFont(QFont("Arial", 12, QFont.Bold))
2026-04-29 16:21:44 +10:00
badge.setStyleSheet("""
background-color: #cc3300;
color: white;
2026-07-17 20:44:15 +10:00
border-radius: 15px;
2026-04-29 16:21:44 +10:00
border: none;
""")
2026-07-17 20:44:15 +10:00
header_row.addWidget(badge)
cart_label = QLabel("CART")
cart_label.setFont(QFont("Arial", 12, QFont.Bold))
cart_label.setStyleSheet("color: #cc3300; background: transparent; border: none;")
header_row.addWidget(cart_label)
self.continue_toggle = QCheckBox("CONTINUE")
self.continue_toggle.setFont(QFont("Arial", 9, QFont.Bold))
self.continue_toggle.setStyleSheet("""
QCheckBox {
color: #888; background: transparent; border: none; spacing: 4px;
}
QCheckBox::indicator {
width: 16px; height: 16px;
border: 1px solid #555; border-radius: 3px;
background-color: #1a1a1a;
}
QCheckBox::indicator:checked {
background-color: #cc3300; border-color: #ff6644;
}
QCheckBox::indicator:hover { border-color: #888; }
""")
self.continue_toggle.setToolTip("Auto-play next track from queue after current finishes")
self.continue_toggle.stateChanged.connect(self._on_continue_toggled)
header_row.addWidget(self.continue_toggle)
header_row.addStretch()
player_layout.addLayout(header_row)
2026-04-29 16:21:44 +10:00
2026-07-17 20:44:15 +10:00
# LCD display
2026-04-30 00:30:23 +10:00
self.lcd_frame = QFrame()
self.lcd_frame.setStyleSheet("""
2026-04-29 16:21:44 +10:00
QFrame {
background-color: #0a1a0a;
border: 1px solid #1a2a1a;
border-radius: 3px;
}
""")
2026-07-17 20:44:15 +10:00
lcd_layout = QHBoxLayout(self.lcd_frame)
lcd_layout.setContentsMargins(8, 4, 8, 4)
lcd_layout.setSpacing(6)
2026-04-29 16:21:44 +10:00
2026-07-17 20:44:15 +10:00
lcd_left = QVBoxLayout()
lcd_left.setSpacing(2)
2026-04-29 16:21:44 +10:00
self.track_label = QLabel("EMPTY")
2026-07-17 20:44:15 +10:00
self.track_label.setFont(QFont("Courier New", 9, QFont.Bold))
self.track_label.setStyleSheet("color: #00ff41; background: transparent; border: none;")
self.track_label.setMinimumWidth(180)
2026-04-29 16:21:44 +10:00
time_row = QHBoxLayout()
self.elapsed_label = QLabel("00:00")
2026-07-17 20:44:15 +10:00
self.elapsed_label.setFont(QFont("Courier New", 18, QFont.Bold))
self.elapsed_label.setStyleSheet("color: #00ff41; background: transparent; border: none;")
2026-04-29 16:21:44 +10:00
self.remaining_label = QLabel("-00:00")
2026-07-17 20:44:15 +10:00
self.remaining_label.setFont(QFont("Courier New", 10, QFont.Bold))
self.remaining_label.setStyleSheet("color: #00cc33; background: transparent; border: none;")
2026-04-29 16:21:44 +10:00
self.remaining_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
time_row.addWidget(self.elapsed_label)
time_row.addStretch()
time_row.addWidget(self.remaining_label)
self.status_label = QLabel("STOPPED")
self.status_label.setFont(QFont("Courier New", 8))
2026-07-17 20:44:15 +10:00
self.status_label.setStyleSheet("color: #008822; background: transparent; border: none;")
2026-04-29 16:21:44 +10:00
2026-07-17 20:44:15 +10:00
lcd_left.addWidget(self.track_label)
lcd_left.addLayout(time_row)
lcd_left.addWidget(self.status_label)
lcd_layout.addLayout(lcd_left)
2026-04-29 16:21:44 +10:00
btn_col = QVBoxLayout()
2026-05-13 20:53:15 +10:00
btn_col.setSpacing(3)
2026-04-29 16:21:44 +10:00
self.btn_play = QPushButton()
2026-07-17 20:44:15 +10:00
self.btn_play.setFixedSize(56, 36)
self.btn_play.setIconSize(QSize(22, 22))
self.btn_play.setIcon(icon_play("#00ff41"))
2026-04-29 16:21:44 +10:00
self.btn_play.setStyleSheet("""
QPushButton {
background-color: #1a6b3a;
border: 1px solid #555;
border-radius: 3px;
}
QPushButton:hover { background-color: #2a7b4a; }
QPushButton:pressed { background-color: #0a5a2a; }
""")
self.btn_eject = QPushButton()
2026-07-17 20:44:15 +10:00
self.btn_eject.setFixedSize(56, 28)
self.btn_eject.setIconSize(QSize(16, 16))
self.btn_eject.setIcon(icon_eject("#ff4444"))
2026-04-29 16:21:44 +10:00
self.btn_eject.setStyleSheet("""
QPushButton {
background-color: #8a1a1a;
border: 1px solid #555;
border-radius: 3px;
}
QPushButton:hover { background-color: #9a2a2a; }
QPushButton:pressed { background-color: #6a0a0a; }
""")
btn_col.addWidget(self.btn_play)
btn_col.addWidget(self.btn_eject)
2026-07-17 20:44:15 +10:00
lcd_layout.addLayout(btn_col)
2026-04-29 16:21:44 +10:00
2026-07-17 20:44:15 +10:00
player_layout.addWidget(self.lcd_frame)
root.addWidget(player_frame)
2026-04-29 16:21:44 +10:00
2026-07-17 20:44:15 +10:00
# ── Right: Queue panel ──────────────────────────────────────
queue_frame = QFrame()
queue_frame.setStyleSheet("""
QFrame {
background-color: #111;
border: 1px solid #2a2010;
border-radius: 5px;
}
""")
queue_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
queue_layout = QVBoxLayout(queue_frame)
queue_layout.setContentsMargins(8, 6, 8, 6)
queue_layout.setSpacing(2)
queue_header = QHBoxLayout()
ql = QLabel("AUTOLOAD QUEUE")
ql.setFont(QFont("Arial", 9, QFont.Bold))
ql.setStyleSheet("color: #ffaa00; background: transparent; border: none;")
queue_header.addWidget(ql)
queue_header.addStretch()
self.queue_count = QLabel("0/3")
self.queue_count.setFont(QFont("Courier New", 9, QFont.Bold))
self.queue_count.setStyleSheet("color: #555; background: transparent; border: none;")
queue_header.addWidget(self.queue_count)
clear_btn = QPushButton("CLR")
clear_btn.setFixedSize(40, 20)
clear_btn.setFont(QFont("Arial", 7, QFont.Bold))
clear_btn.setStyleSheet("""
QPushButton {
background-color: #3a1a1a;
color: #ff6666;
border: 1px solid #555;
2026-04-30 00:30:23 +10:00
border-radius: 3px;
2026-07-17 20:44:15 +10:00
}
QPushButton:hover { background-color: #5a2a2a; }
2026-04-30 00:30:23 +10:00
""")
2026-07-17 20:44:15 +10:00
clear_btn.clicked.connect(self.engine.clear_cart_queue)
queue_header.addWidget(clear_btn)
queue_layout.addLayout(queue_header)
self.queue_items = []
for i in range(3):
item = QLabel(f" — ")
item.setFont(QFont("Courier New", 9))
item.setStyleSheet("color: #555; background: transparent; border: none; padding: 2px 4px;")
item.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
queue_layout.addWidget(item)
self.queue_items.append(item)
queue_layout.addStretch()
root.addWidget(queue_frame)
2026-04-30 00:30:23 +10:00
2026-04-29 16:21:44 +10:00
def _connect_signals(self):
self.btn_play.clicked.connect(self._toggle_play)
self.btn_eject.clicked.connect(self._eject)
2026-07-17 20:44:15 +10:00
def _on_continue_toggled(self, state):
self.engine.cart_continue = bool(state)
2026-04-29 16:21:44 +10:00
def load_track(self, filepath: str):
2026-07-17 20:44:15 +10:00
self.engine.load_cart_track("cart1", filepath)
name = os.path.basename(filepath)
short = name[:32] + "..." if len(name) > 32 else name
2026-04-29 16:21:44 +10:00
self.track_label.setText(short)
self.status_label.setText("LOADED")
self.elapsed_label.setText("00:00")
self.remaining_label.setText("-00:00")
def toggle(self):
2026-07-17 20:44:15 +10:00
deck = self.engine.decks["cart1"]
2026-04-29 16:21:44 +10:00
if deck.track is None:
return
if deck.playing:
deck.stop()
deck.go_to_start()
self._set_ui_stopped()
self.status_label.setText("READY")
else:
deck.play()
self._set_ui_playing()
2026-07-17 20:44:15 +10:00
self.engine.log_cart_trigger(deck.track.filename)
self.cart_triggered.emit(deck.track.filename)
2026-04-29 16:21:44 +10:00
def _set_ui_playing(self):
self.btn_play.setIcon(icon_pause("#ffffff"))
self._playing_icon = True
2026-04-29 16:21:44 +10:00
self.btn_play.setStyleSheet("""
QPushButton {
2026-07-17 20:44:15 +10:00
background-color: #00aa33; border: 1px solid #00ff41; border-radius: 3px;
2026-04-29 16:21:44 +10:00
}
QPushButton:hover { background-color: #00cc44; }
""")
self.status_label.setText("PLAYING")
2026-07-17 20:44:15 +10:00
self.elapsed_label.setStyleSheet("color: #00ff41; background: transparent; border: none;")
2026-04-29 16:21:44 +10:00
def _set_ui_stopped(self):
self.btn_play.setIcon(icon_play("#00ff41"))
self._playing_icon = False
2026-04-29 16:21:44 +10:00
self.btn_play.setStyleSheet("""
QPushButton {
2026-07-17 20:44:15 +10:00
background-color: #1a6b3a; border: 1px solid #555; border-radius: 3px;
2026-04-29 16:21:44 +10:00
}
QPushButton:hover { background-color: #2a7b4a; }
""")
self.elapsed_label.setText("00:00")
self.remaining_label.setText("-00:00")
2026-07-17 20:44:15 +10:00
self.elapsed_label.setStyleSheet("color: #00aa2a; background: transparent; border: none;")
2026-04-29 16:21:44 +10:00
def _toggle_play(self):
2026-07-17 20:44:15 +10:00
deck = self.engine.decks["cart1"]
2026-04-29 16:21:44 +10:00
if deck.track is None:
return
if deck.playing:
deck.pause()
self._set_ui_stopped()
self.status_label.setText("PAUSED")
else:
deck.play()
self._set_ui_playing()
2026-07-17 20:44:15 +10:00
self.engine.log_cart_trigger(deck.track.filename)
self.cart_triggered.emit(deck.track.filename)
2026-04-29 16:21:44 +10:00
def _eject(self):
2026-07-17 20:44:15 +10:00
deck = self.engine.decks["cart1"]
2026-04-29 16:21:44 +10:00
deck.stop()
self._set_ui_stopped()
self.track_label.setText("EMPTY")
self.status_label.setText("STOPPED")
def _refresh_display(self):
2026-07-17 20:44:15 +10:00
deck = self.engine.decks["cart1"]
track = deck.track
playing = deck.playing
2026-04-29 16:21:44 +10:00
if not playing and self._playing_icon:
2026-04-29 16:21:44 +10:00
self._set_ui_stopped()
self.status_label.setText("STOPPED")
if track:
2026-04-29 16:21:44 +10:00
self.elapsed_label.setText(deck.get_elapsed())
self.remaining_label.setText(deck.get_remaining())
2026-07-17 20:44:15 +10:00
def _refresh_queue(self):
q = self.engine.cart_queue
qlen = len(q)
self.queue_count.setText(f"{qlen}/3")
if qlen == 0:
self.queue_count.setStyleSheet("color: #555; background: transparent; border: none;")
elif qlen < 3:
self.queue_count.setStyleSheet("color: #ffaa00; background: transparent; border: none;")
else:
self.queue_count.setStyleSheet("color: #ff4444; background: transparent; border: none;")
for i in range(3):
if i < qlen:
name = q[i].filename
name = name[:35] + "..." if len(name) > 35 else name
self.queue_items[i].setText(f" {i+1}. {name}")
self.queue_items[i].setStyleSheet("color: #ffaa00; background: transparent; border: none; padding: 2px 4px;")
else:
self.queue_items[i].setText(" — ")
self.queue_items[i].setStyleSheet("color: #333; background: transparent; border: none; padding: 2px 4px;")
if not self.engine.cart_continue and self.continue_toggle.isChecked():
self.continue_toggle.blockSignals(True)
self.continue_toggle.setChecked(False)
self.continue_toggle.blockSignals(False)
2026-04-30 00:30:23 +10:00
def apply_theme(self, theme: dict):
2026-07-17 20:44:15 +10:00
self.lcd_frame.setStyleSheet(f"""
QFrame {{
background-color: {theme['bg']};
border: 1px solid {theme['border']};
border-radius: 3px;
}}
""")