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
|
- **Login/Premium** – eigenes, persistentes Profil (`%APPDATA%\Playtube`), einmal bei
|
||||||
Google anmelden reicht für beide Dienste.
|
Google anmelden reicht für beide Dienste.
|
||||||
- **Discord Rich Presence** – zeigt Titel, Kanal/Interpret, Fortschrittsbalken und
|
- **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
|
- **System-Tray** – Fenster schliessen minimiert nur (Musik läuft weiter), Rechtsklick
|
||||||
aufs Tray-Icon zum Beenden, Play/Pause/Skip direkt aus dem Menü.
|
aufs Tray-Icon zum Beenden, Play/Pause/Skip direkt aus dem Menü.
|
||||||
- **Eigener Name** – erscheint als "Playtube" im Taskmanager, Fenstertitel, Alt-Tab
|
- **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):
|
class DiscordRPCWorker(QThread):
|
||||||
"""Eigener Thread, der die Verbindung zu Discord haelt und Presence-Updates
|
"""Eigener Thread, der die Verbindung zu Discord haelt und Presence-Updates
|
||||||
debounced (max. 1 Update pro `interval` Sekunden) versendet."""
|
debounced (max. 1 Update pro `interval` Sekunden) versendet."""
|
||||||
@@ -143,6 +161,11 @@ class DiscordRPCWorker(QThread):
|
|||||||
"""Neuester bekannter Zustand (None = nichts spielt / idle)."""
|
"""Neuester bekannter Zustand (None = nichts spielt / idle)."""
|
||||||
self._replace_queue(info if info is not None else _IDLE_SENTINEL)
|
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:
|
def _replace_queue(self, item) -> None:
|
||||||
try:
|
try:
|
||||||
self._queue.get_nowait()
|
self._queue.get_nowait()
|
||||||
@@ -254,6 +277,14 @@ class DiscordRPCWorker(QThread):
|
|||||||
else:
|
else:
|
||||||
payload = None
|
payload = None
|
||||||
self._presence.clear()
|
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:
|
else:
|
||||||
start_ts, end_ts = self._track_timestamps(item)
|
start_ts, end_ts = self._track_timestamps(item)
|
||||||
payload = build_presence_payload(item, self._session_start, start_ts, end_ts)
|
payload = build_presence_payload(item, self._session_start, start_ts, end_ts)
|
||||||
@@ -287,6 +318,15 @@ class DiscordRPCWorker(QThread):
|
|||||||
pass
|
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:
|
class _Sentinel:
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
|
|||||||
+32
-10
@@ -62,6 +62,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._tabs.addTab(self._settings_tab, "Einstellungen")
|
self._tabs.addTab(self._settings_tab, "Einstellungen")
|
||||||
self._tabs.setCurrentIndex(0 if config.get("start_tab") != "music" else 1)
|
self._tabs.setCurrentIndex(0 if config.get("start_tab") != "music" else 1)
|
||||||
self._settings_tab.settingsSaved.connect(self._on_settings_saved)
|
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.checkUpdatesRequested.connect(self._check_for_updates)
|
||||||
self._settings_tab.installUpdateRequested.connect(
|
self._settings_tab.installUpdateRequested.connect(
|
||||||
lambda: self._start_update_install(self._pending_download_url)
|
lambda: self._start_update_install(self._pending_download_url)
|
||||||
@@ -180,26 +181,46 @@ class MainWindow(QMainWindow):
|
|||||||
tab = self._current_browser_tab()
|
tab = self._current_browser_tab()
|
||||||
self._url_bar.setEnabled(tab is not None)
|
self._url_bar.setEnabled(tab is not None)
|
||||||
self._url_bar.setText(tab.url().toString() if tab is not None else "")
|
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:
|
def _on_media_info(self, info: dict) -> None:
|
||||||
sender = self.sender()
|
sender = self.sender()
|
||||||
self._latest_media[id(sender)] = info
|
self._latest_media[id(sender)] = info
|
||||||
|
|
||||||
# Update Fenstertitel + Discord: bevorzugt der Tab, der gerade wirklich
|
active_info = self._active_media_info()
|
||||||
# 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()))
|
|
||||||
|
|
||||||
if active_info and active_info.get("title"):
|
if active_info and active_info.get("title"):
|
||||||
self.setWindowTitle(f"{active_info['title']} – {APP_NAME}")
|
self.setWindowTitle(f"{active_info['title']} – {APP_NAME}")
|
||||||
else:
|
else:
|
||||||
self.setWindowTitle(APP_NAME)
|
self.setWindowTitle(APP_NAME)
|
||||||
|
|
||||||
if self._rpc_worker is not None:
|
self._update_discord_presence()
|
||||||
if active_info and active_info.get("hasVideo"):
|
|
||||||
self._rpc_worker.submit_media_info(active_info)
|
def _update_discord_presence(self) -> None:
|
||||||
else:
|
"""Entscheidet, was Discord gerade anzeigt: solange der Einstellungen-Tab offen
|
||||||
self._rpc_worker.submit_media_info(None)
|
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"):
|
||||||
|
worker.submit_media_info(active_info)
|
||||||
|
else:
|
||||||
|
worker.submit_media_info(None)
|
||||||
|
|
||||||
def _start_discord_worker(self) -> None:
|
def _start_discord_worker(self) -> None:
|
||||||
discord_cfg = self._config.get("discord", {})
|
discord_cfg = self._config.get("discord", {})
|
||||||
@@ -219,6 +240,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._rpc_worker.wait(2000)
|
self._rpc_worker.wait(2000)
|
||||||
self._rpc_worker = None
|
self._rpc_worker = None
|
||||||
self._start_discord_worker()
|
self._start_discord_worker()
|
||||||
|
self._update_discord_presence() # frischen Worker sofort mit dem aktuellen Stand versorgen
|
||||||
|
|
||||||
self._setup_auto_update()
|
self._setup_auto_update()
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from typing import Any
|
|||||||
|
|
||||||
from PySide6.QtCore import Signal
|
from PySide6.QtCore import Signal
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
QFormLayout,
|
QFormLayout,
|
||||||
@@ -55,6 +56,9 @@ class SettingsTab(QWidget):
|
|||||||
settingsSaved = Signal(dict)
|
settingsSaved = Signal(dict)
|
||||||
checkUpdatesRequested = Signal()
|
checkUpdatesRequested = Signal()
|
||||||
installUpdateRequested = 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):
|
def __init__(self, config: dict[str, Any], parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -155,6 +159,42 @@ class SettingsTab(QWidget):
|
|||||||
|
|
||||||
outer.addStretch(1)
|
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:
|
def _on_save(self) -> None:
|
||||||
self._config.setdefault("discord", {})
|
self._config.setdefault("discord", {})
|
||||||
self._config.setdefault("updates", {})
|
self._config.setdefault("updates", {})
|
||||||
|
|||||||
Reference in New Issue
Block a user