diff --git a/screens/media.py b/screens/media.py index 7563448..7b8b762 100644 --- a/screens/media.py +++ b/screens/media.py @@ -1,3 +1,5 @@ +import subprocess + from PySide6.QtWidgets import ( QWidget, QLabel, @@ -7,7 +9,7 @@ from PySide6.QtWidgets import ( QSlider, QSizePolicy, ) -from PySide6.QtCore import Qt, QSize +from PySide6.QtCore import Qt, QSize, QTimer from PySide6.QtGui import QFont @@ -26,21 +28,21 @@ class MediaScreen(QWidget): info_col.setContentsMargins(0, 0, 0, 0) info_col.setSpacing(6) - source = QLabel("Источник: Bluetooth") - source.setObjectName("MediaSource") - source.setFont(QFont("", 14, 600)) + self.source = QLabel("Источник: Bluetooth") + self.source.setObjectName("MediaSource") + self.source.setFont(QFont("", 14, 600)) - title = QLabel("Название трека") - title.setObjectName("MediaTitle") - title.setFont(QFont("", 22, 700)) + self.title = QLabel("Название трека") + self.title.setObjectName("MediaTitle") + self.title.setFont(QFont("", 22, 700)) - artist = QLabel("Исполнитель") - artist.setObjectName("MediaArtist") - artist.setFont(QFont("", 16, 600)) + self.artist = QLabel("Исполнитель") + self.artist.setObjectName("MediaArtist") + self.artist.setFont(QFont("", 16, 600)) - info_col.addWidget(source) - info_col.addWidget(title) - info_col.addWidget(artist) + info_col.addWidget(self.source) + info_col.addWidget(self.title) + info_col.addWidget(self.artist) info_col.addStretch(1) cover = QLabel("COVER") @@ -55,10 +57,28 @@ class MediaScreen(QWidget): controls.setContentsMargins(0, 0, 0, 0) controls.setSpacing(12) + time_row = QHBoxLayout() + time_row.setContentsMargins(0, 0, 0, 0) + time_row.setSpacing(10) + + self.time_pos = QLabel("0:00") + self.time_pos.setObjectName("MediaTimePos") + self.time_pos.setFont(QFont("", 12, 600)) + + self.time_total = QLabel("0:00") + self.time_total.setObjectName("MediaTimeTotal") + self.time_total.setFont(QFont("", 12, 600)) + self.time_total.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + + time_row.addWidget(self.time_pos) + time_row.addStretch(1) + time_row.addWidget(self.time_total) + progress = QSlider(Qt.Horizontal) progress.setObjectName("MediaProgress") progress.setRange(0, 100) progress.setValue(35) + self.progress = progress transport = QHBoxLayout() transport.setContentsMargins(0, 0, 0, 0) @@ -67,14 +87,17 @@ class MediaScreen(QWidget): btn_prev = QPushButton("⏮") btn_prev.setObjectName("MediaTransportBtn") btn_prev.setFixedSize(QSize(72, 72)) + btn_prev.clicked.connect(self._prev) btn_play = QPushButton("▶") btn_play.setObjectName("MediaTransportBtnPrimary") btn_play.setFixedSize(QSize(96, 72)) + btn_play.clicked.connect(self._toggle_play) btn_next = QPushButton("⏭") btn_next.setObjectName("MediaTransportBtn") btn_next.setFixedSize(QSize(72, 72)) + btn_next.clicked.connect(self._next) transport.addStretch(1) transport.addWidget(btn_prev) @@ -99,6 +122,7 @@ class MediaScreen(QWidget): volume_row.addWidget(volume_lbl) volume_row.addWidget(volume, 1) + controls.addLayout(time_row) controls.addWidget(progress) controls.addLayout(transport) controls.addLayout(volume_row) @@ -116,3 +140,93 @@ class MediaScreen(QWidget): root.addLayout(header) root.addLayout(controls) root.addLayout(soft_keys) + + self._poll_timer = QTimer(self) + self._poll_timer.timeout.connect(self._refresh_metadata) + self._poll_timer.start(2000) + self._refresh_metadata() + + def _toggle_play(self): + status = self._player_status() + if status == "playing": + self._run_btctl(["menu player", "pause"]) + else: + self._run_btctl(["menu player", "play"]) + QTimer.singleShot(300, self._refresh_metadata) + + def _prev(self): + self._run_btctl(["menu player", "previous"]) + QTimer.singleShot(300, self._refresh_metadata) + + def _next(self): + self._run_btctl(["menu player", "next"]) + QTimer.singleShot(300, self._refresh_metadata) + + def _player_status(self) -> str | None: + out = self._run_btctl(["menu player", "show"]) + for line in out.splitlines(): + if "Status:" in line: + return line.split("Status:", 1)[1].strip() + return None + + def _refresh_metadata(self): + out = self._run_btctl(["menu player", "show"]) + title = None + artist = None + source = None + position = None + duration = None + for line in out.splitlines(): + if "Name:" in line: + source = line.split("Name:", 1)[1].strip() + if "Track.Title:" in line: + title = line.split("Track.Title:", 1)[1].strip() + if "Track.Artist:" in line: + artist = line.split("Track.Artist:", 1)[1].strip() + if "Position:" in line: + position = self._parse_hex_value(line) + if "Track.Duration:" in line: + duration = self._parse_hex_value(line) + if title: + self.title.setText(title) + if artist: + self.artist.setText(artist) + if source: + self.source.setText(f"Источник: {source}") + if duration is not None and duration > 0: + self.progress.setRange(0, duration) + self.time_total.setText(self._format_time(duration)) + if position is not None: + self.progress.setValue(position) + self.time_pos.setText(self._format_time(position)) + + def _run_btctl(self, commands: list[str]) -> str: + script = "\n".join(commands + ["back", "quit"]) + "\n" + try: + result = subprocess.run( + ["bluetoothctl"], + input=script, + capture_output=True, + text=True, + timeout=3, + check=False, + ) + except (subprocess.SubprocessError, OSError): + return "" + return result.stdout.strip() + + def _parse_hex_value(self, line: str) -> int | None: + start = line.find("0x") + if start == -1: + return None + hex_part = line[start:].split()[0] + try: + return int(hex_part, 16) + except ValueError: + return None + + def _format_time(self, ms: int) -> str: + total_seconds = max(ms, 0) // 1000 + minutes = total_seconds // 60 + seconds = total_seconds % 60 + return f"{minutes}:{seconds:02d}" diff --git a/themes/day.py b/themes/day.py index 749f2f4..42a7d24 100644 --- a/themes/day.py +++ b/themes/day.py @@ -72,6 +72,7 @@ QWidget { background: #F4F6F8; color: #111827; } #MediaProgress::handle:horizontal { width: 18px; margin: -6px 0; background: #111827; border-radius: 9px; } #MediaVolume::groove:horizontal { height: 8px; background: #E5E7EB; border-radius: 4px; } #MediaVolume::handle:horizontal { width: 18px; margin: -6px 0; background: #111827; border-radius: 9px; } +#MediaTimePos, #MediaTimeTotal { color: rgba(107,114,128,0.95); } #MediaTransportBtn { background: #FFFFFF; border-radius: 14px; diff --git a/themes/night.py b/themes/night.py index 898c9a3..c6cd67c 100644 --- a/themes/night.py +++ b/themes/night.py @@ -67,6 +67,7 @@ QWidget { background: #0B0E11; color: #E6EAF0; } #MediaProgress::handle:horizontal { width: 18px; margin: -6px 0; background: #E6EAF0; border-radius: 9px; } #MediaVolume::groove:horizontal { height: 8px; background: #1B2330; border-radius: 4px; } #MediaVolume::handle:horizontal { width: 18px; margin: -6px 0; background: #E6EAF0; border-radius: 9px; } +#MediaTimePos, #MediaTimeTotal { color: rgba(138,147,166,0.95); } #MediaTransportBtn { background: #141A22; border-radius: 14px;