Discord-Presence: zeigt "In den Einstellungen" und das gerade bearbeitete Feld
Solange der Einstellungen-Tab offen ist, zeigt Discord "In den Einstellungen" mit dem Feld, das gerade den Fokus hat (z.B. "Bearbeitet: Discord Rich Presence > Client-ID"). Es werden nur Feldnamen uebermittelt, nie Inhalte (z.B. nicht die Client-ID selbst). - discord_rpc: build_settings_payload + DiscordRPCWorker.submit_settings_activity - settings_tab: Signal activityChanged, verfolgt den Fokus (auch bei Spinner/Combobox, deren Fokus auf einem internen Kind-Widget liegt) - mainwindow: _update_discord_presence waehlt zwischen Einstellungen, Wiedergabe und Leerlauf; Einstellungs-Status folgt der Option "Status anzeigen, wenn gerade nichts laeuft" (show_idle_presence); bei Tab-Wechsel und nach dem Speichern aktualisiert - README: Hinweis zur neuen Anzeige
This commit is contained in:
@@ -8,7 +8,12 @@ Erweiterung) mit:
|
||||
- **Login/Premium** – eigenes, persistentes Profil (`%APPDATA%\Playtube`), einmal bei
|
||||
Google anmelden reicht für beide Dienste.
|
||||
- **Discord Rich Presence** – zeigt Titel, Kanal/Interpret, Fortschrittsbalken und
|
||||
einen Link-Button in deinem Discord-Profil, sobald etwas läuft.
|
||||
einen Link-Button in deinem Discord-Profil, sobald etwas läuft. Ist der
|
||||
**Einstellungen-Tab** offen, steht dort stattdessen „In den Einstellungen“ und welches
|
||||
Feld du gerade bearbeitest (z.B. „Discord Rich Presence › Client-ID“) - nur der
|
||||
Feldname, nie der Inhalt. Das folgt der Option „Status anzeigen, wenn gerade nichts
|
||||
läuft“ (aus = auch kein Einstellungs-Status). Discord übernimmt Änderungen höchstens
|
||||
alle 15 Sekunden, die Anzeige hinkt dem Klicken also etwas hinterher.
|
||||
- **System-Tray** – Fenster schliessen minimiert nur (Musik läuft weiter), Rechtsklick
|
||||
aufs Tray-Icon zum Beenden, Play/Pause/Skip direkt aus dem Menü.
|
||||
- **Eigener Name** – erscheint als "Playtube" im Taskmanager, Fenstertitel, Alt-Tab
|
||||
|
||||
@@ -112,6 +112,24 @@ def build_idle_payload(session_start: int) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
SETTINGS_DETAILS = "In den Einstellungen"
|
||||
SETTINGS_DEFAULT_STATE = "Schaut sich die Einstellungen an"
|
||||
|
||||
|
||||
def build_settings_payload(session_start: int, activity: str | None) -> dict[str, Any]:
|
||||
"""Presence, solange der Einstellungen-Tab offen ist. `activity` beschreibt, was
|
||||
gerade bearbeitet wird (z.B. "Bearbeitet: Discord Rich Presence › Client-ID") - es
|
||||
wird bewusst nur der NAME des Feldes uebergeben, nie sein Inhalt (sonst wuerde
|
||||
z.B. die eingetragene Client-ID oeffentlich im Discord-Profil stehen)."""
|
||||
return {
|
||||
"details": SETTINGS_DETAILS,
|
||||
"state": _truncate(activity, fallback=SETTINGS_DEFAULT_STATE),
|
||||
"large_image": ASSET_DEFAULT,
|
||||
"large_text": APP_NAME,
|
||||
"start": session_start,
|
||||
}
|
||||
|
||||
|
||||
class DiscordRPCWorker(QThread):
|
||||
"""Eigener Thread, der die Verbindung zu Discord haelt und Presence-Updates
|
||||
debounced (max. 1 Update pro `interval` Sekunden) versendet."""
|
||||
@@ -143,6 +161,11 @@ class DiscordRPCWorker(QThread):
|
||||
"""Neuester bekannter Zustand (None = nichts spielt / idle)."""
|
||||
self._replace_queue(info if info is not None else _IDLE_SENTINEL)
|
||||
|
||||
def submit_settings_activity(self, activity: str | None) -> None:
|
||||
"""Nutzer ist im Einstellungen-Tab; `activity` = was gerade bearbeitet wird
|
||||
(None = nur "schaut sich die Einstellungen an")."""
|
||||
self._replace_queue(_SettingsActivity(activity))
|
||||
|
||||
def _replace_queue(self, item) -> None:
|
||||
try:
|
||||
self._queue.get_nowait()
|
||||
@@ -254,6 +277,14 @@ class DiscordRPCWorker(QThread):
|
||||
else:
|
||||
payload = None
|
||||
self._presence.clear()
|
||||
elif isinstance(item, _SettingsActivity):
|
||||
# Wie beim Leerlauf: der naechste echte Track bekommt einen frischen
|
||||
# Zeit-Anker.
|
||||
self._track_key = None
|
||||
self._track_start_ts = None
|
||||
payload = build_settings_payload(self._session_start, item.activity)
|
||||
self._presence.update(**payload)
|
||||
log_line("[send-settings] state=%r" % (payload["state"],))
|
||||
else:
|
||||
start_ts, end_ts = self._track_timestamps(item)
|
||||
payload = build_presence_payload(item, self._session_start, start_ts, end_ts)
|
||||
@@ -287,6 +318,15 @@ class DiscordRPCWorker(QThread):
|
||||
pass
|
||||
|
||||
|
||||
class _SettingsActivity:
|
||||
"""Queue-Eintrag: Nutzer ist in den Einstellungen und bearbeitet `activity`."""
|
||||
|
||||
__slots__ = ("activity",)
|
||||
|
||||
def __init__(self, activity: str | None) -> None:
|
||||
self.activity = activity
|
||||
|
||||
|
||||
class _Sentinel:
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
+30
-8
@@ -62,6 +62,7 @@ class MainWindow(QMainWindow):
|
||||
self._tabs.addTab(self._settings_tab, "Einstellungen")
|
||||
self._tabs.setCurrentIndex(0 if config.get("start_tab") != "music" else 1)
|
||||
self._settings_tab.settingsSaved.connect(self._on_settings_saved)
|
||||
self._settings_tab.activityChanged.connect(lambda _activity: self._update_discord_presence())
|
||||
self._settings_tab.checkUpdatesRequested.connect(self._check_for_updates)
|
||||
self._settings_tab.installUpdateRequested.connect(
|
||||
lambda: self._start_update_install(self._pending_download_url)
|
||||
@@ -180,26 +181,46 @@ class MainWindow(QMainWindow):
|
||||
tab = self._current_browser_tab()
|
||||
self._url_bar.setEnabled(tab is not None)
|
||||
self._url_bar.setText(tab.url().toString() if tab is not None else "")
|
||||
# Tab-Wechsel von/zu den Einstellungen aendert, was Discord anzeigen soll.
|
||||
self._update_discord_presence()
|
||||
|
||||
def _active_media_info(self) -> dict | None:
|
||||
"""Bevorzugt der Tab, der gerade wirklich abspielt, sonst der aktuell sichtbare."""
|
||||
playing_info = next((i for i in self._latest_media.values() if i.get("playing")), None)
|
||||
return playing_info or self._latest_media.get(id(self._current_tab()))
|
||||
|
||||
def _on_media_info(self, info: dict) -> None:
|
||||
sender = self.sender()
|
||||
self._latest_media[id(sender)] = info
|
||||
|
||||
# Update Fenstertitel + Discord: bevorzugt der Tab, der gerade wirklich
|
||||
# abspielt, sonst der aktuell sichtbare Tab.
|
||||
playing_info = next((i for i in self._latest_media.values() if i.get("playing")), None)
|
||||
active_info = playing_info or self._latest_media.get(id(self._current_tab()))
|
||||
|
||||
active_info = self._active_media_info()
|
||||
if active_info and active_info.get("title"):
|
||||
self.setWindowTitle(f"{active_info['title']} – {APP_NAME}")
|
||||
else:
|
||||
self.setWindowTitle(APP_NAME)
|
||||
|
||||
if self._rpc_worker is not None:
|
||||
self._update_discord_presence()
|
||||
|
||||
def _update_discord_presence(self) -> None:
|
||||
"""Entscheidet, was Discord gerade anzeigt: solange der Einstellungen-Tab offen
|
||||
ist, "In den Einstellungen" samt dem gerade bearbeiteten Feld; sonst wie bisher
|
||||
die laufende Wiedergabe bzw. den Leerlauf. Die Einstellungs-Anzeige folgt der
|
||||
Option "Status anzeigen, wenn gerade nichts laeuft" (show_idle_presence) - wer den
|
||||
Leerlauf-Status abgeschaltet hat, bekommt auch keinen Einstellungs-Status."""
|
||||
worker = getattr(self, "_rpc_worker", None)
|
||||
if worker is None:
|
||||
return
|
||||
|
||||
discord_cfg = self._config.get("discord", {})
|
||||
if self._current_tab() is self._settings_tab and discord_cfg.get("show_idle_presence", True):
|
||||
worker.submit_settings_activity(self._settings_tab.current_activity())
|
||||
return
|
||||
|
||||
active_info = self._active_media_info()
|
||||
if active_info and active_info.get("hasVideo"):
|
||||
self._rpc_worker.submit_media_info(active_info)
|
||||
worker.submit_media_info(active_info)
|
||||
else:
|
||||
self._rpc_worker.submit_media_info(None)
|
||||
worker.submit_media_info(None)
|
||||
|
||||
def _start_discord_worker(self) -> None:
|
||||
discord_cfg = self._config.get("discord", {})
|
||||
@@ -219,6 +240,7 @@ class MainWindow(QMainWindow):
|
||||
self._rpc_worker.wait(2000)
|
||||
self._rpc_worker = None
|
||||
self._start_discord_worker()
|
||||
self._update_discord_presence() # frischen Worker sofort mit dem aktuellen Stand versorgen
|
||||
|
||||
self._setup_auto_update()
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any
|
||||
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QFormLayout,
|
||||
@@ -55,6 +56,9 @@ class SettingsTab(QWidget):
|
||||
settingsSaved = Signal(dict)
|
||||
checkUpdatesRequested = Signal()
|
||||
installUpdateRequested = Signal()
|
||||
# Neue Beschreibung, was gerade bearbeitet wird (fuer die Discord-Presence, siehe
|
||||
# MainWindow._update_discord_presence).
|
||||
activityChanged = Signal(str)
|
||||
|
||||
def __init__(self, config: dict[str, Any], parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -155,6 +159,42 @@ class SettingsTab(QWidget):
|
||||
|
||||
outer.addStretch(1)
|
||||
|
||||
# Beschreibung je Eingabefeld fuer die Discord-Presence ("was wird bearbeitet").
|
||||
# Bewusst nur Feld-NAMEN, nie Inhalte (z.B. nicht die Client-ID selbst).
|
||||
self._activity_by_widget: dict[QWidget, str] = {
|
||||
self._discord_enabled: "Bearbeitet: Discord Rich Presence › Aktiviert",
|
||||
self._client_id: "Bearbeitet: Discord Rich Presence › Client-ID",
|
||||
self._discord_interval: "Bearbeitet: Discord Rich Presence › Update-Intervall",
|
||||
self._show_idle: "Bearbeitet: Discord Rich Presence › Status im Leerlauf",
|
||||
self._updates_enabled: "Bearbeitet: Automatische Updates › Aktiviert",
|
||||
self._check_interval: "Bearbeitet: Automatische Updates › Prüfintervall",
|
||||
self._check_updates_btn: "Sucht nach Updates",
|
||||
self._install_update_btn: "Installiert ein Update",
|
||||
self._start_tab: "Bearbeitet: Allgemein › Beim Start öffnen",
|
||||
save_btn: "Speichert die Einstellungen",
|
||||
}
|
||||
self._current_activity: str | None = None
|
||||
app = QApplication.instance()
|
||||
if app is not None:
|
||||
app.focusChanged.connect(self._on_focus_changed)
|
||||
|
||||
def current_activity(self) -> str | None:
|
||||
"""Zuletzt bearbeitetes Feld (None, solange noch kein Feld angeklickt wurde)."""
|
||||
return self._current_activity
|
||||
|
||||
def _on_focus_changed(self, _old: QWidget | None, new: QWidget | None) -> None:
|
||||
# Vom fokussierten Widget nach oben suchen: bei QSpinBox/QComboBox liegt der Fokus
|
||||
# auf einem internen Kind-Widget (z.B. dem Textfeld des Spinners).
|
||||
widget = new
|
||||
while widget is not None and widget is not self:
|
||||
activity = self._activity_by_widget.get(widget)
|
||||
if activity:
|
||||
if activity != self._current_activity:
|
||||
self._current_activity = activity
|
||||
self.activityChanged.emit(activity)
|
||||
return
|
||||
widget = widget.parentWidget()
|
||||
|
||||
def _on_save(self) -> None:
|
||||
self._config.setdefault("discord", {})
|
||||
self._config.setdefault("updates", {})
|
||||
|
||||
Reference in New Issue
Block a user