mirror of
https://github.com/spalencsar/bearwave.git
synced 2026-07-06 22:24:17 +02:00
Bump version to 1.0.1, implement automatic online search fallback with debounce, cover art notifications, and stream details edit
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
project(bearwave VERSION 1.0.0 LANGUAGES CXX)
|
project(bearwave VERSION 1.0.1 LANGUAGES CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|||||||
4
PKGBUILD
4
PKGBUILD
@@ -1,5 +1,5 @@
|
|||||||
pkgname=bearwave-git
|
pkgname=bearwave-git
|
||||||
pkgver=1.0.0
|
pkgver=1.0.1
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="A KDE-focused desktop internet radio app"
|
pkgdesc="A KDE-focused desktop internet radio app"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
@@ -14,7 +14,7 @@ md5sums=('SKIP')
|
|||||||
|
|
||||||
pkgver() {
|
pkgver() {
|
||||||
cd "${srcdir}/${pkgname%-git}"
|
cd "${srcdir}/${pkgname%-git}"
|
||||||
printf "1.0.0.r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
printf "1.0.1.r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
||||||
}
|
}
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ int main(int argc, char *argv[])
|
|||||||
QApplication::setApplicationName(QStringLiteral("BearWave"));
|
QApplication::setApplicationName(QStringLiteral("BearWave"));
|
||||||
QApplication::setDesktopFileName(QStringLiteral("org.kde.bearwave.desktop"));
|
QApplication::setDesktopFileName(QStringLiteral("org.kde.bearwave.desktop"));
|
||||||
QApplication::setOrganizationName(QStringLiteral("BearWave"));
|
QApplication::setOrganizationName(QStringLiteral("BearWave"));
|
||||||
QApplication::setApplicationVersion(QStringLiteral("1.0.0"));
|
QApplication::setApplicationVersion(QStringLiteral("1.0.1"));
|
||||||
|
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
app.setWindowIcon(QIcon::fromTheme(QStringLiteral("org.kde.bearwave")));
|
app.setWindowIcon(QIcon::fromTheme(QStringLiteral("org.kde.bearwave")));
|
||||||
|
|||||||
@@ -217,8 +217,19 @@ QVariantMap MprisPlayerAdaptor::metadata() const
|
|||||||
if (!artist.isEmpty()) {
|
if (!artist.isEmpty()) {
|
||||||
map.insert(QStringLiteral("xesam:artist"), QStringList{artist});
|
map.insert(QStringLiteral("xesam:artist"), QStringList{artist});
|
||||||
}
|
}
|
||||||
if (!coverUrl.isEmpty()) {
|
QString artUrl = coverUrl;
|
||||||
map.insert(QStringLiteral("mpris:artUrl"), coverUrl);
|
if (artUrl.isEmpty() && m_backend) {
|
||||||
|
QObject *stationObj = m_backend->currentStation();
|
||||||
|
if (stationObj) {
|
||||||
|
QString fav = stationObj->property("favicon").toString();
|
||||||
|
if (fav.startsWith(QLatin1String("https://"))) {
|
||||||
|
artUrl = fav;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!artUrl.isEmpty()) {
|
||||||
|
map.insert(QStringLiteral("mpris:artUrl"), artUrl);
|
||||||
} else {
|
} else {
|
||||||
QString iconPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("icons/hicolor/256x256/apps/org.kde.bearwave.png"));
|
QString iconPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("icons/hicolor/256x256/apps/org.kde.bearwave.png"));
|
||||||
if (!iconPath.isEmpty()) {
|
if (!iconPath.isEmpty()) {
|
||||||
|
|||||||
@@ -80,20 +80,31 @@ void NotificationManager::onTrackInfoChanged()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coverUrl.isEmpty()) {
|
QString targetUrl = coverUrl;
|
||||||
|
if (targetUrl.isEmpty() && m_backend) {
|
||||||
|
QObject *stationObj = m_backend->currentStation();
|
||||||
|
if (stationObj) {
|
||||||
|
targetUrl = stationObj->property("favicon").toString();
|
||||||
|
if (!targetUrl.startsWith(QLatin1String("https://"))) {
|
||||||
|
targetUrl.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetUrl.isEmpty()) {
|
||||||
if (!m_notifyTimer.isActive()) {
|
if (!m_notifyTimer.isActive()) {
|
||||||
m_notifyTimer.start(800);
|
m_notifyTimer.start(800);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m_notifiedCoverUrl = coverUrl;
|
m_notifiedCoverUrl = targetUrl;
|
||||||
const QString hash = QString(QCryptographicHash::hash(coverUrl.toUtf8(), QCryptographicHash::Md5).toHex());
|
const QString hash = QString(QCryptographicHash::hash(targetUrl.toUtf8(), QCryptographicHash::Md5).toHex());
|
||||||
const QString filePath = m_coversDir + "/" + hash + ".jpg";
|
const QString filePath = m_coversDir + "/" + hash + ".jpg";
|
||||||
|
|
||||||
if (QFile::exists(filePath)) {
|
if (QFile::exists(filePath)) {
|
||||||
m_activeCoverPath = filePath;
|
m_activeCoverPath = filePath;
|
||||||
triggerNotification();
|
triggerNotification();
|
||||||
} else {
|
} else {
|
||||||
downloadCover(coverUrl);
|
downloadCover(targetUrl);
|
||||||
if (!m_notifyTimer.isActive()) {
|
if (!m_notifyTimer.isActive()) {
|
||||||
m_notifyTimer.start(800);
|
m_notifyTimer.start(800);
|
||||||
}
|
}
|
||||||
|
|||||||
213
src/qml/Main.qml
213
src/qml/Main.qml
@@ -133,7 +133,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: compactMode ? (headerContent.implicitHeight + 20) : 176
|
Layout.preferredHeight: headerContent.implicitHeight + 20
|
||||||
radius: 12
|
radius: 12
|
||||||
color: panel
|
color: panel
|
||||||
border.color: cardBorder
|
border.color: cardBorder
|
||||||
@@ -338,6 +338,13 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 1
|
||||||
|
color: cardBorder
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
visible: !compactMode
|
visible: !compactMode
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@@ -351,6 +358,7 @@ ApplicationWindow {
|
|||||||
if (backend) {
|
if (backend) {
|
||||||
backend.filterQuery = text
|
backend.filterQuery = text
|
||||||
}
|
}
|
||||||
|
searchTimer.restart()
|
||||||
}
|
}
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (text.length < 2 || !backend) return
|
if (text.length < 2 || !backend) return
|
||||||
@@ -491,6 +499,7 @@ ApplicationWindow {
|
|||||||
text: qsTr("Genre:")
|
text: qsTr("Genre:")
|
||||||
color: textMuted
|
color: textMuted
|
||||||
font.pixelSize: 11
|
font.pixelSize: 11
|
||||||
|
rightPadding: 10
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
@@ -526,12 +535,13 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item { Layout.preferredWidth: 10 }
|
Item { Layout.preferredWidth: 20 }
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
text: qsTr("Country:")
|
text: qsTr("Country:")
|
||||||
color: textMuted
|
color: textMuted
|
||||||
font.pixelSize: 11
|
font.pixelSize: 11
|
||||||
|
rightPadding: 10
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
@@ -596,7 +606,10 @@ ApplicationWindow {
|
|||||||
color: textMuted
|
color: textMuted
|
||||||
font.pixelSize: 11
|
font.pixelSize: 11
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
padding: 8
|
leftPadding: 8
|
||||||
|
rightPadding: 16
|
||||||
|
topPadding: 8
|
||||||
|
bottomPadding: 8
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
@@ -643,7 +656,10 @@ ApplicationWindow {
|
|||||||
color: textMuted
|
color: textMuted
|
||||||
font.pixelSize: 11
|
font.pixelSize: 11
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
padding: 8
|
leftPadding: 8
|
||||||
|
rightPadding: 16
|
||||||
|
topPadding: 8
|
||||||
|
bottomPadding: 8
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
@@ -791,6 +807,7 @@ ApplicationWindow {
|
|||||||
border.color: accent
|
border.color: accent
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
|
id: stationFavicon
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 3
|
anchors.margins: 3
|
||||||
source: (stationCard.modelData.favicon && stationCard.modelData.favicon.startsWith("https://"))
|
source: (stationCard.modelData.favicon && stationCard.modelData.favicon.startsWith("https://"))
|
||||||
@@ -802,12 +819,13 @@ ApplicationWindow {
|
|||||||
visible: source !== "" && status === Image.Ready
|
visible: source !== "" && status === Image.Ready
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Image {
|
||||||
anchors.centerIn: parent
|
anchors.fill: parent
|
||||||
text: qsTr("FM")
|
anchors.margins: 6
|
||||||
color: "#d8ecff"
|
source: "qrc:/assets/app/bearwave.png"
|
||||||
font.bold: true
|
fillMode: Image.PreserveAspectFit
|
||||||
visible: !parent.children[0].visible
|
smooth: true
|
||||||
|
visible: !stationFavicon.visible
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -889,6 +907,7 @@ ApplicationWindow {
|
|||||||
Label {
|
Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: stationCard.modelData.country + " • "
|
text: stationCard.modelData.country + " • "
|
||||||
|
+ (stationCard.modelData.codec && stationCard.modelData.codec !== "unknown" && stationCard.modelData.codec !== "" ? stationCard.modelData.codec.toUpperCase() + " • " : "")
|
||||||
+ (stationCard.modelData.bitrate > 0 ? stationCard.modelData.bitrate + " kbps" : qsTr("Stream"))
|
+ (stationCard.modelData.bitrate > 0 ? stationCard.modelData.bitrate + " kbps" : qsTr("Stream"))
|
||||||
color: textMuted
|
color: textMuted
|
||||||
font.pixelSize: 11
|
font.pixelSize: 11
|
||||||
@@ -896,6 +915,19 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
visible: !stationCard.modelData.uuid
|
||||||
|
Layout.preferredWidth: compactMode ? 34 : 40
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
text: "✏"
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Edit Station")
|
||||||
|
onClicked: {
|
||||||
|
editDialog.stationObject = stationCard.modelData
|
||||||
|
editDialog.setupAndOpen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
Layout.preferredWidth: compactMode ? 34 : 40
|
Layout.preferredWidth: compactMode ? 34 : 40
|
||||||
Layout.preferredHeight: 40
|
Layout.preferredHeight: 40
|
||||||
@@ -972,11 +1004,27 @@ ApplicationWindow {
|
|||||||
Image {
|
Image {
|
||||||
id: coverImage
|
id: coverImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: backend && backend.player && backend.player.currentCoverArtUrl ? backend.player.currentCoverArtUrl : "qrc:/assets/app/bearwave.png"
|
source: {
|
||||||
fillMode: backend && backend.player && backend.player.currentCoverArtUrl ? Image.PreserveAspectCrop : Image.PreserveAspectFit
|
if (backend && backend.player && backend.player.currentCoverArtUrl && backend.player.currentCoverArtUrl !== "") {
|
||||||
|
return backend.player.currentCoverArtUrl;
|
||||||
|
}
|
||||||
|
if (backend && backend.currentStation && backend.currentStation.favicon && backend.currentStation.favicon.startsWith("https://")) {
|
||||||
|
return backend.currentStation.favicon;
|
||||||
|
}
|
||||||
|
return "qrc:/assets/app/bearwave.png";
|
||||||
|
}
|
||||||
|
fillMode: (backend && backend.player && backend.player.currentCoverArtUrl && backend.player.currentCoverArtUrl !== "") ? Image.PreserveAspectCrop : Image.PreserveAspectFit
|
||||||
smooth: true
|
smooth: true
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
anchors.margins: backend && backend.player && backend.player.currentCoverArtUrl ? 0 : 16
|
anchors.margins: {
|
||||||
|
if (backend && backend.player && backend.player.currentCoverArtUrl && backend.player.currentCoverArtUrl !== "") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (backend && backend.currentStation && backend.currentStation.favicon && backend.currentStation.favicon.startsWith("https://")) {
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
return 16;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -985,13 +1033,58 @@ ApplicationWindow {
|
|||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
spacing: 6
|
spacing: 6
|
||||||
|
|
||||||
Label {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: backend && backend.player ? (backend.player.currentStationName || qsTr("No station selected")) : qsTr("No station selected")
|
spacing: 8
|
||||||
color: textMain
|
|
||||||
font.pixelSize: 16
|
Label {
|
||||||
font.bold: true
|
Layout.fillWidth: true
|
||||||
elide: Text.ElideRight
|
text: backend && backend.player ? (backend.player.currentStationName || qsTr("No station selected")) : qsTr("No station selected")
|
||||||
|
color: textMain
|
||||||
|
font.pixelSize: 16
|
||||||
|
font.bold: true
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: codecBadge
|
||||||
|
visible: backend && backend.currentStation && backend.currentStation.codec && backend.currentStation.codec !== "unknown" && backend.currentStation.codec !== ""
|
||||||
|
height: 18
|
||||||
|
width: codecLabel.implicitWidth + 12
|
||||||
|
radius: 4
|
||||||
|
color: "transparent"
|
||||||
|
border.color: accent
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: codecLabel
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (backend && backend.currentStation && backend.currentStation.codec) ? backend.currentStation.codec.toUpperCase() : ""
|
||||||
|
color: accent
|
||||||
|
font.pixelSize: 9
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: bitrateBadge
|
||||||
|
visible: backend && backend.currentStation && backend.currentStation.bitrate > 0
|
||||||
|
height: 18
|
||||||
|
width: bitrateLabel.implicitWidth + 12
|
||||||
|
radius: 4
|
||||||
|
color: accent
|
||||||
|
border.color: accent
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: bitrateLabel
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (backend && backend.currentStation) ? backend.currentStation.bitrate + " kbps" : ""
|
||||||
|
color: "#ffffff"
|
||||||
|
font.pixelSize: 9
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@@ -1050,9 +1143,25 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Button {
|
||||||
text: qsTr("Volume")
|
id: muteButton
|
||||||
color: textMuted
|
text: (backend && backend.player && backend.player.volume > 0) ? "🔊" : "🔇"
|
||||||
|
Layout.preferredWidth: 44
|
||||||
|
property real lastVolume: 0.5
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: (backend && backend.player && backend.player.volume > 0) ? qsTr("Mute") : qsTr("Unmute")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (backend && backend.player) {
|
||||||
|
if (backend.player.volume > 0) {
|
||||||
|
lastVolume = backend.player.volume
|
||||||
|
backend.player.setVolume(0)
|
||||||
|
} else {
|
||||||
|
backend.player.setVolume(lastVolume > 0 ? lastVolume : 0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Slider {
|
Slider {
|
||||||
@@ -1138,6 +1247,51 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Dialog {
|
||||||
|
id: editDialog
|
||||||
|
title: qsTr("Edit station")
|
||||||
|
modal: true
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: compactMode ? 320 : 420
|
||||||
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||||
|
|
||||||
|
property var stationObject: null
|
||||||
|
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
TextField { id: editName; Layout.fillWidth: true; placeholderText: qsTr("Name") }
|
||||||
|
TextField { id: editUrl; Layout.fillWidth: true; placeholderText: qsTr("Stream URL (http/https)") }
|
||||||
|
TextField { id: editCountry; Layout.fillWidth: true; placeholderText: qsTr("Country (optional)") }
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupAndOpen() {
|
||||||
|
if (stationObject) {
|
||||||
|
editName.text = stationObject.name || ""
|
||||||
|
editUrl.text = stationObject.url || ""
|
||||||
|
editCountry.text = (stationObject.country === qsTr("Manual") ? "" : (stationObject.country || ""))
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
if (backend && stationObject) {
|
||||||
|
backend.editManualStation(stationObject, editName.text, editUrl.text, editCountry.text)
|
||||||
|
toast(qsTr("Station updated"))
|
||||||
|
}
|
||||||
|
stationObject = null
|
||||||
|
editName.text = ""
|
||||||
|
editUrl.text = ""
|
||||||
|
editCountry.text = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onRejected: {
|
||||||
|
stationObject = null
|
||||||
|
editName.text = ""
|
||||||
|
editUrl.text = ""
|
||||||
|
editCountry.text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Dialog {
|
Dialog {
|
||||||
id: aboutDialog
|
id: aboutDialog
|
||||||
modal: true
|
modal: true
|
||||||
@@ -1357,6 +1511,21 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: searchTimer
|
||||||
|
interval: 600
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
var text = searchField.text.trim()
|
||||||
|
if (text.length < 2 || !backend) return
|
||||||
|
|
||||||
|
if (currentPage === "search" || stationList.count === 0) {
|
||||||
|
currentPage = "search"
|
||||||
|
backend.searchStations(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: contentFadeRestart
|
id: contentFadeRestart
|
||||||
interval: 120
|
interval: 120
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ void RadioBackend::searchStations(const QString &query)
|
|||||||
if (query.isEmpty()) {
|
if (query.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
qDebug() << "RadioBackend: searchStations called with query:" << query;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
m_radioBrowser->search(query);
|
m_radioBrowser->search(query);
|
||||||
}
|
}
|
||||||
@@ -692,6 +693,74 @@ void RadioBackend::playCurrentSelection()
|
|||||||
m_player->playUrl(url, station->name());
|
m_player->playUrl(url, station->name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QObject* RadioBackend::currentStation() const
|
||||||
|
{
|
||||||
|
for (RadioStation *s : m_favorites) {
|
||||||
|
if (matchesStation(s, m_currentStationUuid, m_currentStationUrl)) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (RadioStation *s : m_stations) {
|
||||||
|
if (matchesStation(s, m_currentStationUuid, m_currentStationUrl)) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RadioBackend::editManualStation(QObject *stationObj, const QString &name, const QString &url, const QString &country)
|
||||||
|
{
|
||||||
|
RadioStation *station = qobject_cast<RadioStation*>(stationObj);
|
||||||
|
if (!station || name.trimmed().isEmpty() || url.trimmed().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString oldUrl = station->url();
|
||||||
|
QString oldUrlResolved = station->urlResolved();
|
||||||
|
|
||||||
|
station->setName(name.trimmed());
|
||||||
|
station->setUrl(url.trimmed());
|
||||||
|
station->setUrlResolved(url.trimmed());
|
||||||
|
station->setCountry(country.trimmed().isEmpty() ? tr("Manual") : country.trimmed());
|
||||||
|
|
||||||
|
// Update in favorites list if present
|
||||||
|
for (RadioStation *fav : m_favorites) {
|
||||||
|
if (fav == station || (fav->uuid().isEmpty() && (fav->url() == oldUrl || fav->urlResolved() == oldUrlResolved))) {
|
||||||
|
fav->setName(station->name());
|
||||||
|
fav->setUrl(station->url());
|
||||||
|
fav->setUrlResolved(station->urlResolved());
|
||||||
|
fav->setCountry(station->country());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save updated favorites
|
||||||
|
saveFavorites();
|
||||||
|
|
||||||
|
// Update in main stations list if present
|
||||||
|
for (RadioStation *s : m_stations) {
|
||||||
|
if (s == station || (s->uuid().isEmpty() && (s->url() == oldUrl || s->urlResolved() == oldUrlResolved))) {
|
||||||
|
s->setName(station->name());
|
||||||
|
s->setUrl(station->url());
|
||||||
|
s->setUrlResolved(station->urlResolved());
|
||||||
|
s->setCountry(station->country());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's the currently playing station, update player stream info
|
||||||
|
if (m_currentStationUrl == oldUrl || m_currentStationUrl == oldUrlResolved) {
|
||||||
|
m_currentStationUrl = station->urlResolved();
|
||||||
|
m_lastStationUrl = station->urlResolved();
|
||||||
|
m_lastStationName = station->name();
|
||||||
|
saveState();
|
||||||
|
emit resumeStateChanged();
|
||||||
|
emit currentStationChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
emit stationsChanged();
|
||||||
|
emit favoritesChanged();
|
||||||
|
emit listsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
QVariantMap RadioBackend::toVariantMap(const RadioStation *station)
|
QVariantMap RadioBackend::toVariantMap(const RadioStation *station)
|
||||||
{
|
{
|
||||||
QVariantMap map;
|
QVariantMap map;
|
||||||
@@ -705,6 +774,7 @@ QVariantMap RadioBackend::toVariantMap(const RadioStation *station)
|
|||||||
map.insert(QStringLiteral("isFavorite"), station->isFavorite());
|
map.insert(QStringLiteral("isFavorite"), station->isFavorite());
|
||||||
map.insert(QStringLiteral("favicon"), station->favicon());
|
map.insert(QStringLiteral("favicon"), station->favicon());
|
||||||
map.insert(QStringLiteral("urlResolved"), station->urlResolved());
|
map.insert(QStringLiteral("urlResolved"), station->urlResolved());
|
||||||
|
map.insert(QStringLiteral("codec"), station->codec());
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class RadioBackend : public QObject
|
|||||||
Q_PROPERTY(QVariantList recentStations READ recentStations NOTIFY listsChanged)
|
Q_PROPERTY(QVariantList recentStations READ recentStations NOTIFY listsChanged)
|
||||||
Q_PROPERTY(QString currentStationUuid READ currentStationUuid NOTIFY currentStationChanged)
|
Q_PROPERTY(QString currentStationUuid READ currentStationUuid NOTIFY currentStationChanged)
|
||||||
Q_PROPERTY(QString currentStationUrl READ currentStationUrl NOTIFY currentStationChanged)
|
Q_PROPERTY(QString currentStationUrl READ currentStationUrl NOTIFY currentStationChanged)
|
||||||
|
Q_PROPERTY(QObject* currentStation READ currentStation NOTIFY currentStationChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit RadioBackend(QObject *parent = nullptr);
|
explicit RadioBackend(QObject *parent = nullptr);
|
||||||
@@ -34,6 +35,7 @@ public:
|
|||||||
QList<QObject*> stations() const;
|
QList<QObject*> stations() const;
|
||||||
QList<QObject*> favoriteStations() const;
|
QList<QObject*> favoriteStations() const;
|
||||||
QVariantList recentStations() const;
|
QVariantList recentStations() const;
|
||||||
|
QObject* currentStation() const;
|
||||||
QString currentStationUuid() const { return m_currentStationUuid; }
|
QString currentStationUuid() const { return m_currentStationUuid; }
|
||||||
QString currentStationUrl() const { return m_currentStationUrl; }
|
QString currentStationUrl() const { return m_currentStationUrl; }
|
||||||
BearPlayer* player() const { return m_player; }
|
BearPlayer* player() const { return m_player; }
|
||||||
@@ -61,6 +63,7 @@ public:
|
|||||||
Q_INVOKABLE void toggleFavorite(int index);
|
Q_INVOKABLE void toggleFavorite(int index);
|
||||||
Q_INVOKABLE void toggleFavoriteById(const QString &uuid, const QString &urlResolved);
|
Q_INVOKABLE void toggleFavoriteById(const QString &uuid, const QString &urlResolved);
|
||||||
Q_INVOKABLE void addManualStation(const QString &name, const QString &url, const QString &country);
|
Q_INVOKABLE void addManualStation(const QString &name, const QString &url, const QString &country);
|
||||||
|
Q_INVOKABLE void editManualStation(QObject *stationObj, const QString &name, const QString &url, const QString &country);
|
||||||
Q_INVOKABLE void sortStations(const QString &mode);
|
Q_INVOKABLE void sortStations(const QString &mode);
|
||||||
Q_INVOKABLE QVariantList getRecentStations() const;
|
Q_INVOKABLE QVariantList getRecentStations() const;
|
||||||
Q_INVOKABLE QVariantList getFavoriteStations() const;
|
Q_INVOKABLE QVariantList getFavoriteStations() const;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
QString apiCacheDir()
|
QString apiCacheDir()
|
||||||
@@ -25,7 +26,15 @@ RadioBrowser::RadioBrowser(QObject *parent)
|
|||||||
|
|
||||||
void RadioBrowser::search(const QString &query)
|
void RadioBrowser::search(const QString &query)
|
||||||
{
|
{
|
||||||
QString endpoint = "/stations/byname/" + QUrl::toPercentEncoding(query);
|
QUrlQuery queryParams;
|
||||||
|
queryParams.addQueryItem("name", query);
|
||||||
|
queryParams.addQueryItem("hidebroken", "true");
|
||||||
|
queryParams.addQueryItem("limit", "50");
|
||||||
|
queryParams.addQueryItem("order", "votes");
|
||||||
|
queryParams.addQueryItem("reverse", "true");
|
||||||
|
|
||||||
|
QString endpoint = "/stations/search?" + queryParams.toString(QUrl::FullyEncoded);
|
||||||
|
qDebug() << "RadioBrowser: Searching with endpoint:" << endpoint;
|
||||||
makeRequest(endpoint);
|
makeRequest(endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user