mirror of
https://github.com/spalencsar/bearwave.git
synced 2026-07-06 22:24:17 +02:00
Implement playing station visual feedback, track desktop notifications, and local cover caching
This commit is contained in:
@@ -77,6 +77,7 @@ BearWave intentionally does not aim to be:
|
||||
- resume support for last station and volume
|
||||
- MPRIS integration for Plasma media controls and media keys
|
||||
- system tray integration for background playback
|
||||
- desktop notifications for song/track changes with local cover art caching
|
||||
- embedded About dialog with links and MIT license text
|
||||
|
||||
## Project Status
|
||||
@@ -234,6 +235,7 @@ BearWave stores user state under:
|
||||
- favorites: `~/.config/bearwave/favorites.json`
|
||||
- last station + volume: `~/.config/bearwave/state.json`
|
||||
- API cache: `~/.cache/bearwave/api_cache/`
|
||||
- cover art cache: `~/.cache/bearwave/covers/`
|
||||
|
||||
If these files are removed, app state resets to defaults or performs a fresh API sync.
|
||||
|
||||
@@ -311,6 +313,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for local build and review expectations.
|
||||
- stream playback: `src/bearplayer.cpp`
|
||||
- API layer: `src/radiobrowser.cpp`
|
||||
- MPRIS adapter: `src/mprisadaptor.cpp`
|
||||
- desktop notifications: `src/notificationmanager.cpp`
|
||||
|
||||
For contributor and agent guardrails, see `AGENTS.md`.
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ set(SOURCES
|
||||
coverartfetcher.cpp
|
||||
mprisadaptor.cpp
|
||||
bearwavecontroladaptor.cpp
|
||||
notificationmanager.cpp
|
||||
)
|
||||
|
||||
# Create executable
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "radiobackend.h"
|
||||
#include "mprisadaptor.h"
|
||||
#include "bearwavecontroladaptor.h"
|
||||
#include "notificationmanager.h"
|
||||
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
@@ -35,9 +36,11 @@ int main(int argc, char *argv[])
|
||||
MprisRootAdaptor mprisRoot(&backend, &app);
|
||||
MprisPlayerAdaptor mprisPlayer(&backend);
|
||||
BearWaveControlAdaptor controlAdaptor(&backend);
|
||||
NotificationManager notificationManager(&backend);
|
||||
Q_UNUSED(mprisRoot)
|
||||
Q_UNUSED(mprisPlayer)
|
||||
Q_UNUSED(controlAdaptor)
|
||||
Q_UNUSED(notificationManager)
|
||||
|
||||
QDBusConnection sessionBus = QDBusConnection::sessionBus();
|
||||
sessionBus.registerObject(QStringLiteral("/org/mpris/MediaPlayer2"), &backend, QDBusConnection::ExportAdaptors);
|
||||
|
||||
248
src/notificationmanager.cpp
Normal file
248
src/notificationmanager.cpp
Normal file
@@ -0,0 +1,248 @@
|
||||
#include "notificationmanager.h"
|
||||
#include "radiobackend.h"
|
||||
#include "bearplayer.h"
|
||||
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusReply>
|
||||
#include <QDBusMessage>
|
||||
#include <QDBusConnection>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QCryptographicHash>
|
||||
#include <QStandardPaths>
|
||||
#include <QDebug>
|
||||
|
||||
NotificationManager::NotificationManager(RadioBackend *backend, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_backend(backend)
|
||||
, m_player(backend->player())
|
||||
, m_notificationShown(false)
|
||||
{
|
||||
m_coversDir = QDir::homePath() + QStringLiteral("/.cache/bearwave/covers");
|
||||
QDir().mkpath(m_coversDir);
|
||||
|
||||
m_networkManager = new QNetworkAccessManager(this);
|
||||
|
||||
m_notifyTimer.setSingleShot(true);
|
||||
connect(&m_notifyTimer, &QTimer::timeout, this, &NotificationManager::onNotifyTimeout);
|
||||
|
||||
connect(m_player, &BearPlayer::trackInfoChanged, this, &NotificationManager::onTrackInfoChanged);
|
||||
connect(m_player, &BearPlayer::currentStationChanged, this, &NotificationManager::onTrackInfoChanged);
|
||||
}
|
||||
|
||||
NotificationManager::~NotificationManager()
|
||||
{
|
||||
closeNotification();
|
||||
}
|
||||
|
||||
void NotificationManager::onTrackInfoChanged()
|
||||
{
|
||||
const QString station = m_player->currentStationName();
|
||||
const QString artist = m_player->currentTrackArtist();
|
||||
const QString title = m_player->currentTrackTitle();
|
||||
const QString coverUrl = m_player->currentCoverArtUrl();
|
||||
|
||||
if (artist.isEmpty() && title.isEmpty()) {
|
||||
m_notifyTimer.stop();
|
||||
if (m_currentReply) {
|
||||
m_currentReply->abort();
|
||||
m_currentReply->deleteLater();
|
||||
m_currentReply = nullptr;
|
||||
}
|
||||
closeNotification();
|
||||
m_lastArtist.clear();
|
||||
m_lastTitle.clear();
|
||||
m_lastStation.clear();
|
||||
m_notifiedCoverUrl.clear();
|
||||
m_activeCoverPath.clear();
|
||||
m_notificationShown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
bool trackChanged = (artist != m_lastArtist || title != m_lastTitle || station != m_lastStation);
|
||||
|
||||
if (trackChanged) {
|
||||
m_notifyTimer.stop();
|
||||
if (m_currentReply) {
|
||||
m_currentReply->abort();
|
||||
m_currentReply->deleteLater();
|
||||
m_currentReply = nullptr;
|
||||
}
|
||||
m_lastArtist = artist;
|
||||
m_lastTitle = title;
|
||||
m_lastStation = station;
|
||||
m_notifiedCoverUrl.clear();
|
||||
m_activeCoverPath.clear();
|
||||
m_notificationShown = false;
|
||||
}
|
||||
|
||||
if (m_notificationShown) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (coverUrl.isEmpty()) {
|
||||
if (!m_notifyTimer.isActive()) {
|
||||
m_notifyTimer.start(800);
|
||||
}
|
||||
} else {
|
||||
m_notifiedCoverUrl = coverUrl;
|
||||
const QString hash = QString(QCryptographicHash::hash(coverUrl.toUtf8(), QCryptographicHash::Md5).toHex());
|
||||
const QString filePath = m_coversDir + "/" + hash + ".jpg";
|
||||
|
||||
if (QFile::exists(filePath)) {
|
||||
m_activeCoverPath = filePath;
|
||||
triggerNotification();
|
||||
} else {
|
||||
downloadCover(coverUrl);
|
||||
if (!m_notifyTimer.isActive()) {
|
||||
m_notifyTimer.start(800);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationManager::downloadCover(const QString &url)
|
||||
{
|
||||
if (m_currentReply) {
|
||||
m_currentReply->abort();
|
||||
m_currentReply->deleteLater();
|
||||
m_currentReply = nullptr;
|
||||
}
|
||||
|
||||
m_pendingCoverUrl = url;
|
||||
QNetworkRequest request(url);
|
||||
m_currentReply = m_networkManager->get(request);
|
||||
connect(m_currentReply, &QNetworkReply::finished, this, &NotificationManager::onDownloadFinished);
|
||||
}
|
||||
|
||||
void NotificationManager::onDownloadFinished()
|
||||
{
|
||||
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
|
||||
if (!reply) return;
|
||||
|
||||
if (reply == m_currentReply) {
|
||||
m_currentReply = nullptr;
|
||||
}
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray data = reply->readAll();
|
||||
const QString hash = QString(QCryptographicHash::hash(m_pendingCoverUrl.toUtf8(), QCryptographicHash::Md5).toHex());
|
||||
const QString filePath = m_coversDir + "/" + hash + ".jpg";
|
||||
|
||||
QFile file(filePath);
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
file.write(data);
|
||||
file.close();
|
||||
|
||||
m_activeCoverPath = filePath;
|
||||
triggerNotification();
|
||||
}
|
||||
} else {
|
||||
if (reply->error() != QNetworkReply::OperationCanceledError) {
|
||||
qDebug() << "Cover download failed:" << reply->errorString();
|
||||
}
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
void NotificationManager::onNotifyTimeout()
|
||||
{
|
||||
triggerNotification();
|
||||
}
|
||||
|
||||
void NotificationManager::triggerNotification()
|
||||
{
|
||||
m_notifyTimer.stop();
|
||||
qDebug() << "Triggering notification - station:" << m_lastStation << "artist:" << m_lastArtist << "title:" << m_lastTitle << "cover:" << m_activeCoverPath;
|
||||
showNotification(m_lastStation, m_lastArtist, m_lastTitle, m_activeCoverPath);
|
||||
m_notificationShown = true;
|
||||
}
|
||||
|
||||
void NotificationManager::showNotification(const QString &station, const QString &artist, const QString &title, const QString &coverPath)
|
||||
{
|
||||
QDBusInterface notifyInterface(
|
||||
QStringLiteral("org.freedesktop.Notifications"),
|
||||
QStringLiteral("/org/freedesktop/Notifications"),
|
||||
QStringLiteral("org.freedesktop.Notifications"),
|
||||
QDBusConnection::sessionBus()
|
||||
);
|
||||
|
||||
if (!notifyInterface.isValid()) {
|
||||
qWarning() << "DBus Notification interface is invalid";
|
||||
return;
|
||||
}
|
||||
|
||||
QString summary;
|
||||
QString body;
|
||||
|
||||
if (!title.isEmpty()) {
|
||||
summary = title;
|
||||
if (!artist.isEmpty()) {
|
||||
body = artist;
|
||||
}
|
||||
if (!station.isEmpty()) {
|
||||
if (!body.isEmpty()) body += QStringLiteral("\n");
|
||||
body += tr("Station: %1").arg(station);
|
||||
}
|
||||
} else {
|
||||
if (!station.isEmpty()) {
|
||||
summary = station;
|
||||
} else {
|
||||
summary = QStringLiteral("BearWave");
|
||||
}
|
||||
if (!artist.isEmpty()) {
|
||||
body = artist;
|
||||
}
|
||||
}
|
||||
|
||||
QVariantMap hints;
|
||||
hints.insert(QStringLiteral("desktop-entry"), QStringLiteral("org.kde.bearwave"));
|
||||
if (!coverPath.isEmpty()) {
|
||||
hints.insert(QStringLiteral("image-path"), coverPath);
|
||||
hints.insert(QStringLiteral("image_path"), coverPath);
|
||||
}
|
||||
|
||||
QList<QVariant> args;
|
||||
args << QStringLiteral("BearWave")
|
||||
<< m_lastNotificationId
|
||||
<< QStringLiteral("org.kde.bearwave")
|
||||
<< summary
|
||||
<< body
|
||||
<< QStringList()
|
||||
<< hints
|
||||
<< -1;
|
||||
|
||||
QDBusReply<uint> dbusReply = notifyInterface.callWithArgumentList(
|
||||
QDBus::Block,
|
||||
QStringLiteral("Notify"),
|
||||
args
|
||||
);
|
||||
|
||||
if (dbusReply.isValid()) {
|
||||
m_lastNotificationId = dbusReply.value();
|
||||
} else {
|
||||
qWarning() << "Failed to send notification via DBus:" << dbusReply.error().message();
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationManager::closeNotification()
|
||||
{
|
||||
if (m_lastNotificationId == 0) {
|
||||
return;
|
||||
}
|
||||
qDebug() << "Closing notification with ID:" << m_lastNotificationId;
|
||||
|
||||
QDBusInterface notifyInterface(
|
||||
QStringLiteral("org.freedesktop.Notifications"),
|
||||
QStringLiteral("/org/freedesktop/Notifications"),
|
||||
QStringLiteral("org.freedesktop.Notifications"),
|
||||
QDBusConnection::sessionBus()
|
||||
);
|
||||
|
||||
if (notifyInterface.isValid()) {
|
||||
notifyInterface.call(QStringLiteral("CloseNotification"), m_lastNotificationId);
|
||||
}
|
||||
|
||||
m_lastNotificationId = 0;
|
||||
}
|
||||
50
src/notificationmanager.h
Normal file
50
src/notificationmanager.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef NOTIFICATIONMANAGER_H
|
||||
#define NOTIFICATIONMANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
class RadioBackend;
|
||||
class BearPlayer;
|
||||
|
||||
class NotificationManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit NotificationManager(RadioBackend *backend, QObject *parent = nullptr);
|
||||
~NotificationManager();
|
||||
|
||||
private slots:
|
||||
void onTrackInfoChanged();
|
||||
void onDownloadFinished();
|
||||
void onNotifyTimeout();
|
||||
|
||||
private:
|
||||
void showNotification(const QString &station, const QString &artist, const QString &title, const QString &coverPath);
|
||||
void closeNotification();
|
||||
void downloadCover(const QString &url);
|
||||
void triggerNotification();
|
||||
|
||||
RadioBackend *m_backend = nullptr;
|
||||
BearPlayer *m_player = nullptr;
|
||||
QNetworkAccessManager *m_networkManager = nullptr;
|
||||
QNetworkReply *m_currentReply = nullptr;
|
||||
QTimer m_notifyTimer;
|
||||
|
||||
quint32 m_lastNotificationId = 0;
|
||||
QString m_coversDir;
|
||||
|
||||
QString m_lastArtist;
|
||||
QString m_lastTitle;
|
||||
QString m_lastStation;
|
||||
QString m_notifiedCoverUrl;
|
||||
QString m_pendingCoverUrl;
|
||||
QString m_activeCoverPath;
|
||||
bool m_notificationShown = false;
|
||||
};
|
||||
|
||||
#endif // NOTIFICATIONMANAGER_H
|
||||
129
src/qml/Main.qml
129
src/qml/Main.qml
@@ -90,7 +90,13 @@ ApplicationWindow {
|
||||
if (!backend) {
|
||||
return []
|
||||
}
|
||||
return currentPage === "favorites" ? backend.favoriteStations : backend.stations
|
||||
if (currentPage === "favorites") {
|
||||
return backend.favoriteStations
|
||||
} else if (currentPage === "history") {
|
||||
return backend.recentStations
|
||||
} else {
|
||||
return backend.stations
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentPageChanged: {
|
||||
@@ -201,6 +207,15 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("History")
|
||||
highlighted: currentPage === "history"
|
||||
onClicked: {
|
||||
currentPage = "history"
|
||||
activeQuickFilter = ""
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Button {
|
||||
@@ -311,6 +326,15 @@ ApplicationWindow {
|
||||
activeQuickFilter = ""
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("History")
|
||||
highlighted: currentPage === "history"
|
||||
onClicked: {
|
||||
currentPage = "history"
|
||||
activeQuickFilter = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,8 +737,25 @@ ApplicationWindow {
|
||||
width: stationScrollView.availableWidth
|
||||
height: compactMode ? 72 : 78
|
||||
radius: 10
|
||||
color: cardMouse.containsMouse ? cardHover : card
|
||||
border.color: cardBorder
|
||||
|
||||
readonly property bool isCurrent: {
|
||||
if (!backend || !modelData) return false;
|
||||
var currentUuid = backend.currentStationUuid;
|
||||
var currentUrl = backend.currentStationUrl;
|
||||
var cardUuid = modelData.uuid || "";
|
||||
var cardUrl = modelData.urlResolved || modelData.url || "";
|
||||
if (currentUuid !== "" && cardUuid !== "") {
|
||||
return currentUuid === cardUuid;
|
||||
}
|
||||
return currentUrl !== "" && currentUrl === cardUrl;
|
||||
}
|
||||
readonly property bool isPlaying: isCurrent && backend && backend.player && backend.player.playing
|
||||
|
||||
color: stationCard.isCurrent
|
||||
? (cardMouse.containsMouse ? "#1d3350" : "#16283e")
|
||||
: (cardMouse.containsMouse ? cardHover : card)
|
||||
border.color: stationCard.isCurrent ? accent : cardBorder
|
||||
border.width: stationCard.isCurrent ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
id: cardMouse
|
||||
@@ -723,13 +764,19 @@ ApplicationWindow {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onClicked: {
|
||||
if (!backend) return
|
||||
if (stationCard.isCurrent) {
|
||||
backend.player.togglePlayPause()
|
||||
} else {
|
||||
if (currentPage === "favorites") {
|
||||
backend.playFavoriteStation(index)
|
||||
} else if (currentPage === "history") {
|
||||
backend.playRecentByUuid(modelData.uuid, modelData.urlResolved)
|
||||
} else {
|
||||
backend.playStation(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
@@ -768,15 +815,77 @@ ApplicationWindow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: stationCard.modelData.name
|
||||
color: textMain
|
||||
color: stationCard.isCurrent ? accent : textMain
|
||||
font.bold: true
|
||||
font.pixelSize: compactMode ? 13 : 14
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Row {
|
||||
id: eqAnimation
|
||||
spacing: 2
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: stationCard.isPlaying
|
||||
|
||||
Rectangle {
|
||||
id: bar1
|
||||
width: 2
|
||||
height: 12
|
||||
color: accent
|
||||
radius: 1
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: 120 }
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: bar2
|
||||
width: 2
|
||||
height: 12
|
||||
color: accent
|
||||
radius: 1
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: 120 }
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: bar3
|
||||
width: 2
|
||||
height: 12
|
||||
color: accent
|
||||
radius: 1
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: 120 }
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 150
|
||||
running: stationCard.isPlaying
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
bar1.height = Math.floor(Math.random() * 11) + 3
|
||||
bar2.height = Math.floor(Math.random() * 11) + 3
|
||||
bar3.height = Math.floor(Math.random() * 11) + 3
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
bar1.height = 12
|
||||
bar2.height = 12
|
||||
bar3.height = 12
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: stationCard.modelData.country + " • "
|
||||
@@ -801,11 +910,16 @@ ApplicationWindow {
|
||||
Button {
|
||||
Layout.preferredWidth: compactMode ? 34 : 40
|
||||
Layout.preferredHeight: 40
|
||||
text: "▶"
|
||||
text: (stationCard.isCurrent && backend && backend.player && backend.player.playing) ? "⏸" : "▶"
|
||||
onClicked: {
|
||||
if (!backend) return
|
||||
if (stationCard.isCurrent) {
|
||||
backend.player.togglePlayPause()
|
||||
} else {
|
||||
if (currentPage === "favorites") {
|
||||
backend.playFavoriteStation(index)
|
||||
} else if (currentPage === "history") {
|
||||
backend.playRecentByUuid(modelData.uuid, modelData.urlResolved)
|
||||
} else {
|
||||
backend.playStation(index)
|
||||
}
|
||||
@@ -815,6 +929,7 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
@@ -822,13 +937,13 @@ ApplicationWindow {
|
||||
visible: stationList.count === 0
|
||||
|
||||
Label {
|
||||
text: qsTr("No stations loaded yet")
|
||||
text: currentPage === "history" ? qsTr("No playback history") : qsTr("No stations loaded yet")
|
||||
color: textMain
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Load DE/NL stations or use search")
|
||||
text: currentPage === "history" ? qsTr("Play some stations to build history") : qsTr("Load DE/NL stations or use search")
|
||||
color: textMuted
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,10 @@ void RadioBackend::setupConnections()
|
||||
m_lastStationName = name;
|
||||
emit resumeStateChanged();
|
||||
saveState();
|
||||
} else {
|
||||
m_currentStationUuid.clear();
|
||||
m_currentStationUrl.clear();
|
||||
emit currentStationChanged();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -344,6 +348,28 @@ QVariantList RadioBackend::getRecentStations() const
|
||||
return m_recentStations;
|
||||
}
|
||||
|
||||
QVariantList RadioBackend::recentStations() const
|
||||
{
|
||||
QVariantList updatedList;
|
||||
for (const QVariant &entry : m_recentStations) {
|
||||
QVariantMap station = entry.toMap();
|
||||
const QString uuid = station.value(QStringLiteral("uuid")).toString();
|
||||
const QString urlResolved = station.value(QStringLiteral("urlResolved")).toString();
|
||||
|
||||
bool isFav = false;
|
||||
for (const RadioStation *fav : m_favorites) {
|
||||
if ((!uuid.isEmpty() && fav->uuid() == uuid) ||
|
||||
(uuid.isEmpty() && !urlResolved.isEmpty() && fav->urlResolved() == urlResolved)) {
|
||||
isFav = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
station.insert(QStringLiteral("isFavorite"), isFav);
|
||||
updatedList.append(station);
|
||||
}
|
||||
return updatedList;
|
||||
}
|
||||
|
||||
QVariantList RadioBackend::getFavoriteStations() const
|
||||
{
|
||||
QVariantList result;
|
||||
@@ -363,6 +389,9 @@ bool RadioBackend::playRecentByUuid(const QString &uuid, const QString &urlResol
|
||||
emit resumeStateChanged();
|
||||
recordRecentStation(toVariantMap(station));
|
||||
saveState();
|
||||
m_currentStationUuid = station->uuid();
|
||||
m_currentStationUrl = url;
|
||||
emit currentStationChanged();
|
||||
m_player->playUrl(url, station->name());
|
||||
return true;
|
||||
}
|
||||
@@ -381,6 +410,9 @@ bool RadioBackend::playRecentByUuid(const QString &uuid, const QString &urlResol
|
||||
emit resumeStateChanged();
|
||||
recordRecentStation(station);
|
||||
saveState();
|
||||
m_currentStationUuid = station.value(QStringLiteral("uuid")).toString();
|
||||
m_currentStationUrl = url;
|
||||
emit currentStationChanged();
|
||||
m_player->playUrl(url, m_lastStationName);
|
||||
return true;
|
||||
}
|
||||
@@ -410,6 +442,26 @@ void RadioBackend::resumeLastStation()
|
||||
station.insert(QStringLiteral("urlResolved"), m_lastStationUrl);
|
||||
recordRecentStation(station);
|
||||
saveState();
|
||||
|
||||
QString resolvedUuid;
|
||||
for (const RadioStation *s : m_stations) {
|
||||
if (s->urlResolved() == m_lastStationUrl || s->url() == m_lastStationUrl) {
|
||||
resolvedUuid = s->uuid();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (resolvedUuid.isEmpty()) {
|
||||
for (const RadioStation *s : m_favorites) {
|
||||
if (s->urlResolved() == m_lastStationUrl || s->url() == m_lastStationUrl) {
|
||||
resolvedUuid = s->uuid();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
m_currentStationUuid = resolvedUuid;
|
||||
m_currentStationUrl = m_lastStationUrl;
|
||||
emit currentStationChanged();
|
||||
|
||||
m_player->playUrl(m_lastStationUrl, m_lastStationName.isEmpty() ? tr("Last played") : m_lastStationName);
|
||||
}
|
||||
|
||||
@@ -634,6 +686,9 @@ void RadioBackend::playCurrentSelection()
|
||||
emit resumeStateChanged();
|
||||
recordRecentStation(toVariantMap(station));
|
||||
saveState();
|
||||
m_currentStationUuid = station->uuid();
|
||||
m_currentStationUrl = url;
|
||||
emit currentStationChanged();
|
||||
m_player->playUrl(url, station->name());
|
||||
}
|
||||
|
||||
|
||||
@@ -24,12 +24,18 @@ class RadioBackend : public QObject
|
||||
Q_PROPERTY(bool canResumeLastStation READ canResumeLastStation NOTIFY resumeStateChanged)
|
||||
Q_PROPERTY(QString lastStationName READ lastStationName NOTIFY resumeStateChanged)
|
||||
Q_PROPERTY(QString filterQuery READ filterQuery WRITE setFilterQuery NOTIFY filterQueryChanged)
|
||||
Q_PROPERTY(QVariantList recentStations READ recentStations NOTIFY listsChanged)
|
||||
Q_PROPERTY(QString currentStationUuid READ currentStationUuid NOTIFY currentStationChanged)
|
||||
Q_PROPERTY(QString currentStationUrl READ currentStationUrl NOTIFY currentStationChanged)
|
||||
|
||||
public:
|
||||
explicit RadioBackend(QObject *parent = nullptr);
|
||||
|
||||
QList<QObject*> stations() const;
|
||||
QList<QObject*> favoriteStations() const;
|
||||
QVariantList recentStations() const;
|
||||
QString currentStationUuid() const { return m_currentStationUuid; }
|
||||
QString currentStationUrl() const { return m_currentStationUrl; }
|
||||
BearPlayer* player() const { return m_player; }
|
||||
bool loading() const { return m_loading; }
|
||||
QString lastError() const { return m_lastError; }
|
||||
@@ -70,6 +76,7 @@ signals:
|
||||
void lastErrorChanged();
|
||||
void resumeStateChanged();
|
||||
void filterQueryChanged();
|
||||
void currentStationChanged();
|
||||
|
||||
private slots:
|
||||
void onStationsLoaded(const QList<RadioStation*> &stations);
|
||||
@@ -88,6 +95,8 @@ private:
|
||||
QString m_lastStationUrl;
|
||||
QString m_filterQuery;
|
||||
QVariantList m_recentStations;
|
||||
QString m_currentStationUuid;
|
||||
QString m_currentStationUrl;
|
||||
|
||||
void setupConnections();
|
||||
void loadFavorites();
|
||||
|
||||
@@ -211,6 +211,18 @@
|
||||
<source>Copyright (c) 2026</source>
|
||||
<translation>Copyright (c) 2026</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>History</source>
|
||||
<translation>Verlauf</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>No playback history</source>
|
||||
<translation>Kein Wiedergabeverlauf</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Play some stations to build history</source>
|
||||
<translation>Spiele Sender ab, um einen Verlauf aufzubauen</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>RadioBackend</name>
|
||||
|
||||
Reference in New Issue
Block a user