mirror of
https://github.com/spalencsar/bearwave.git
synced 2026-07-06 22:24:17 +02:00
Release 1.0.3: World Categories dashboard and localization
This commit is contained in:
12
CHANGELOG.md
12
CHANGELOG.md
@@ -5,7 +5,17 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
## [1.0.3] - 2026-05-30
|
||||
|
||||
### Added
|
||||
|
||||
- Dynamic World Categories dashboard showing country flags and popular genre tags.
|
||||
- Dynamic country name localization mapping for German translation support.
|
||||
- Local JSON caching for countries retrieved from the Radio Browser API.
|
||||
|
||||
### Changed
|
||||
|
||||
- Optimized Categories view layout to prevent text clipping (dynamic grid columns and scroll margins).
|
||||
|
||||
## [1.0.2] - 2026-05-28
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
project(bearwave VERSION 1.0.2 LANGUAGES CXX)
|
||||
project(bearwave VERSION 1.0.3 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
@@ -16,6 +16,7 @@ BearWave is designed for fast station browsing, simple playback controls, favori
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  | |
|
||||
|
||||
Screenshots: KDE Plasma on Linux.
|
||||
|
||||
@@ -71,7 +72,8 @@ BearWave intentionally does not aim to be:
|
||||
## Features
|
||||
|
||||
- internet radio via the Radio Browser API with local JSON caching
|
||||
- station pages for Top, Germany, Netherlands, and quick world/genre filters
|
||||
- station pages for Top, Germany, Netherlands, and a dynamic World Categories dashboard
|
||||
- interactive World View to search/browse stations by country flags and popular genre tags
|
||||
- local search and filtering by name, genre, and country
|
||||
- sorting by name, bitrate, and votes
|
||||
- favorites with persistent local storage
|
||||
|
||||
@@ -59,6 +59,16 @@
|
||||
</provides>
|
||||
|
||||
<releases>
|
||||
<release version="1.0.3" date="2026-05-30">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Added dynamic World Categories dashboard showing country flags and popular genre tags</li>
|
||||
<li>Added country name localization support for German systems</li>
|
||||
<li>Added local API caching for countries retrieved from Radio Browser</li>
|
||||
<li>Optimized Categories view layout to prevent text clipping</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.0.2" date="2026-05-28">
|
||||
<description>
|
||||
<ul>
|
||||
|
||||
BIN
screenshots/screen05.png
Normal file
BIN
screenshots/screen05.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
@@ -22,7 +22,7 @@ int main(int argc, char *argv[])
|
||||
QApplication::setApplicationName(QStringLiteral("BearWave"));
|
||||
QApplication::setDesktopFileName(QStringLiteral("de.nerdbear.bearwave"));
|
||||
QApplication::setOrganizationName(QStringLiteral("BearWave"));
|
||||
QApplication::setApplicationVersion(QStringLiteral("1.0.2"));
|
||||
QApplication::setApplicationVersion(QStringLiteral("1.0.3"));
|
||||
|
||||
QApplication app(argc, argv);
|
||||
|
||||
|
||||
718
src/qml/Main.qml
718
src/qml/Main.qml
@@ -81,6 +81,9 @@ ApplicationWindow {
|
||||
property bool compactMode: width < 780
|
||||
property real contentOpacity: 1.0
|
||||
property string activeQuickFilter: ""
|
||||
property string selectedWorldCategory: ""
|
||||
property string selectedWorldType: ""
|
||||
property string countrySearchText: ""
|
||||
|
||||
readonly property color bgA: "#0f141b"
|
||||
readonly property color bgB: "#131b25"
|
||||
@@ -93,11 +96,52 @@ ApplicationWindow {
|
||||
readonly property color textMuted: "#9eb1c9"
|
||||
readonly property color warn: "#ff8b8b"
|
||||
|
||||
readonly property var worldTags: [
|
||||
{ name: qsTr("Pop"), tag: "pop", icon: "🎵" },
|
||||
{ name: qsTr("Rock"), tag: "rock", icon: "🎸" },
|
||||
{ name: qsTr("Electronic"), tag: "electronic", icon: "🎹" },
|
||||
{ name: qsTr("Classical"), tag: "classical", icon: "🎻" },
|
||||
{ name: qsTr("Jazz"), tag: "jazz", icon: "🎷" },
|
||||
{ name: qsTr("Metal"), tag: "metal", icon: "🤘" },
|
||||
{ name: qsTr("Hip Hop"), tag: "hiphop", icon: "🎤" },
|
||||
{ name: qsTr("Chillout"), tag: "chillout", icon: "🌴" },
|
||||
{ name: qsTr("News / Talk"), tag: "news", icon: "📻" },
|
||||
{ name: qsTr("Soundtracks"), tag: "soundtrack", icon: "🎬" },
|
||||
{ name: qsTr("Ambient"), tag: "ambient", icon: "🌌" },
|
||||
{ name: qsTr("Blues / Soul"), tag: "blues", icon: "🎺" }
|
||||
]
|
||||
|
||||
function toast(message) {
|
||||
toastLabel.text = message
|
||||
toastPopup.open()
|
||||
}
|
||||
|
||||
function getFilteredCountries() {
|
||||
if (!backend || !backend.countries) return [];
|
||||
var query = countrySearchText.toLowerCase().trim();
|
||||
var list = [];
|
||||
if (query === "") {
|
||||
list.push({ name: qsTr("Top Global"), code: "GLOBAL" });
|
||||
}
|
||||
for (var i = 0; i < backend.countries.length; i++) {
|
||||
var c = backend.countries[i];
|
||||
if (query === "" || c.name.toLowerCase().indexOf(query) !== -1 || c.code.toLowerCase().indexOf(query) !== -1) {
|
||||
list.push(c);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function getFlagEmoji(countryCode) {
|
||||
if (!countryCode || countryCode.length !== 2) return "🌎";
|
||||
var codeUpper = countryCode.toUpperCase();
|
||||
var codePoints = [];
|
||||
for (var i = 0; i < codeUpper.length; i++) {
|
||||
codePoints.push(127397 + codeUpper.charCodeAt(i));
|
||||
}
|
||||
return String.fromCodePoint.apply(null, codePoints);
|
||||
}
|
||||
|
||||
function activeModel() {
|
||||
if (!backend) {
|
||||
return []
|
||||
@@ -106,6 +150,8 @@ ApplicationWindow {
|
||||
return backend.favoriteStations
|
||||
} else if (currentPage === "history") {
|
||||
return backend.recentStations
|
||||
} else if (currentPage === "world" && selectedWorldCategory === "") {
|
||||
return []
|
||||
} else {
|
||||
return backend.stations
|
||||
}
|
||||
@@ -114,6 +160,11 @@ ApplicationWindow {
|
||||
onCurrentPageChanged: {
|
||||
contentOpacity = 0.85
|
||||
contentFadeRestart.restart()
|
||||
if (currentPage !== "world") {
|
||||
selectedWorldCategory = ""
|
||||
selectedWorldType = ""
|
||||
countrySearchText = ""
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
@@ -596,7 +647,11 @@ ApplicationWindow {
|
||||
if (!backend) return
|
||||
currentPage = "world"
|
||||
activeQuickFilter = "world"
|
||||
backend.loadWorldStations()
|
||||
selectedWorldCategory = ""
|
||||
selectedWorldType = ""
|
||||
if (backend.countries.length === 0) {
|
||||
backend.loadCountries()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,7 +769,11 @@ ApplicationWindow {
|
||||
if (!backend) return
|
||||
currentPage = "world"
|
||||
activeQuickFilter = "world"
|
||||
backend.loadWorldStations()
|
||||
selectedWorldCategory = ""
|
||||
selectedWorldType = ""
|
||||
if (backend.countries.length === 0) {
|
||||
backend.loadCountries()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -744,217 +803,84 @@ ApplicationWindow {
|
||||
NumberAnimation { duration: 180; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: stationScrollView
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
|
||||
spacing: 10
|
||||
|
||||
ListView {
|
||||
id: stationList
|
||||
width: stationScrollView.availableWidth
|
||||
spacing: 8
|
||||
model: activeModel()
|
||||
visible: count > 0
|
||||
RowLayout {
|
||||
id: worldHeader
|
||||
Layout.fillWidth: true
|
||||
visible: currentPage === "world" && selectedWorldCategory !== ""
|
||||
spacing: 12
|
||||
|
||||
delegate: Rectangle {
|
||||
id: stationCard
|
||||
required property int index
|
||||
required property var modelData
|
||||
Button {
|
||||
text: qsTr("← Back to Categories")
|
||||
onClicked: {
|
||||
selectedWorldCategory = ""
|
||||
selectedWorldType = ""
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: selectedWorldType === "country"
|
||||
? (qsTr("World > Country: ") + selectedWorldCategory)
|
||||
: (qsTr("World > Genre: ") + selectedWorldCategory)
|
||||
color: textMain
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: stationScrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: !(currentPage === "world" && selectedWorldCategory === "")
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
|
||||
|
||||
ListView {
|
||||
id: stationList
|
||||
width: stationScrollView.availableWidth
|
||||
height: compactMode ? 72 : 78
|
||||
radius: 10
|
||||
spacing: 8
|
||||
model: activeModel()
|
||||
visible: count > 0
|
||||
|
||||
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;
|
||||
delegate: Rectangle {
|
||||
id: stationCard
|
||||
required property int index
|
||||
required property var modelData
|
||||
width: stationScrollView.availableWidth
|
||||
height: compactMode ? 72 : 78
|
||||
radius: 10
|
||||
|
||||
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;
|
||||
}
|
||||
return currentUrl !== "" && currentUrl === cardUrl;
|
||||
}
|
||||
readonly property bool isPlaying: isCurrent && backend && backend.player && backend.player.playing
|
||||
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
|
||||
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
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
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
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 44
|
||||
Layout.preferredHeight: 44
|
||||
radius: 8
|
||||
color: "#123154"
|
||||
border.color: accent
|
||||
|
||||
Image {
|
||||
id: stationFavicon
|
||||
anchors.fill: parent
|
||||
anchors.margins: 3
|
||||
source: (stationCard.modelData.favicon && stationCard.modelData.favicon.startsWith("https://"))
|
||||
? stationCard.modelData.favicon
|
||||
: ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
cache: true
|
||||
visible: source !== "" && status === Image.Ready
|
||||
}
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
source: "qrc:/assets/app/bearwave.png"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
visible: !stationFavicon.visible
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: stationCard.modelData.name
|
||||
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 + " • "
|
||||
+ (stationCard.modelData.codec && stationCard.modelData.codec !== "unknown" && stationCard.modelData.codec !== "" ? stationCard.modelData.codec.toUpperCase() + " • " : "")
|
||||
+ (stationCard.modelData.bitrate > 0 ? stationCard.modelData.bitrate + " kbps" : qsTr("Stream"))
|
||||
color: textMuted
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Layout.preferredWidth: compactMode ? 34 : 40
|
||||
Layout.preferredHeight: 40
|
||||
text: stationCard.modelData.isFavorite ? "★" : "☆"
|
||||
onClicked: {
|
||||
if (!backend) return
|
||||
backend.toggleFavoriteById(stationCard.modelData.uuid, stationCard.modelData.urlResolved)
|
||||
toast(stationCard.modelData.isFavorite ? qsTr("Removed from favorites") : qsTr("Added to favorites"))
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.preferredWidth: compactMode ? 34 : 40
|
||||
Layout.preferredHeight: 40
|
||||
text: (stationCard.isCurrent && backend && backend.player && backend.player.playing) ? "⏸" : "▶"
|
||||
MouseArea {
|
||||
id: cardMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onClicked: {
|
||||
if (!backend) return
|
||||
if (stationCard.isCurrent) {
|
||||
@@ -970,15 +896,395 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 44
|
||||
Layout.preferredHeight: 44
|
||||
radius: 8
|
||||
color: "#123154"
|
||||
border.color: accent
|
||||
|
||||
Image {
|
||||
id: stationFavicon
|
||||
anchors.fill: parent
|
||||
anchors.margins: 3
|
||||
source: (stationCard.modelData.favicon && stationCard.modelData.favicon.startsWith("https://"))
|
||||
? stationCard.modelData.favicon
|
||||
: ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
cache: true
|
||||
visible: source !== "" && status === Image.Ready
|
||||
}
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
source: "qrc:/assets/app/bearwave.png"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
visible: !stationFavicon.visible
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: stationCard.modelData.name
|
||||
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 + " • "
|
||||
+ (stationCard.modelData.codec && stationCard.modelData.codec !== "unknown" && stationCard.modelData.codec !== "" ? stationCard.modelData.codec.toUpperCase() + " • " : "")
|
||||
+ (stationCard.modelData.bitrate > 0 ? stationCard.modelData.bitrate + " kbps" : qsTr("Stream"))
|
||||
color: textMuted
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Layout.preferredWidth: compactMode ? 34 : 40
|
||||
Layout.preferredHeight: 40
|
||||
text: stationCard.modelData.isFavorite ? "★" : "☆"
|
||||
onClicked: {
|
||||
if (!backend) return
|
||||
backend.toggleFavoriteById(stationCard.modelData.uuid, stationCard.modelData.urlResolved)
|
||||
toast(stationCard.modelData.isFavorite ? qsTr("Removed from favorites") : qsTr("Added to favorites"))
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.preferredWidth: compactMode ? 34 : 40
|
||||
Layout.preferredHeight: 40
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: categoriesScrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: currentPage === "world" && selectedWorldCategory === ""
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
|
||||
Column {
|
||||
width: categoriesScrollView.availableWidth - 12
|
||||
spacing: 16
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
spacing: 10
|
||||
|
||||
Label {
|
||||
text: qsTr("Explore World Stations")
|
||||
font.bold: true
|
||||
font.pixelSize: 16
|
||||
color: textMain
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: countryFilterInput
|
||||
placeholderText: qsTr("Filter countries...")
|
||||
font.pixelSize: 11
|
||||
Layout.preferredWidth: compactMode ? 140 : 200
|
||||
text: countrySearchText
|
||||
onTextChanged: countrySearchText = text
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Choose a country or music style to find radio stations from all over the world.")
|
||||
font.pixelSize: 11
|
||||
color: textMuted
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
visible: countrySearchText === ""
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: cardBorder
|
||||
opacity: 0.4
|
||||
visible: countrySearchText === ""
|
||||
}
|
||||
|
||||
Label {
|
||||
text: countrySearchText === "" ? qsTr("Countries") : qsTr("Filtered Countries")
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
color: accent
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: countryFlow
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Repeater {
|
||||
model: getFilteredCountries()
|
||||
delegate: Rectangle {
|
||||
id: countryCard
|
||||
width: compactMode ? (countryFlow.width - 8) / 2 : (countryFlow.width - 16) / 3
|
||||
height: 62
|
||||
radius: 8
|
||||
color: countryMouse.containsMouse ? cardHover : card
|
||||
border.color: countryMouse.containsMouse ? accent : cardBorder
|
||||
border.width: 1
|
||||
scale: countryMouse.containsMouse ? 1.02 : 1.0
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 100 } }
|
||||
Behavior on scale { NumberAnimation { duration: 100; easing.type: Easing.OutQuad } }
|
||||
|
||||
MouseArea {
|
||||
id: countryMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
selectedWorldCategory = modelData.name
|
||||
selectedWorldType = "country"
|
||||
if (modelData.code === "GLOBAL") {
|
||||
backend.loadWorldStations()
|
||||
} else {
|
||||
backend.loadByCountryCode(modelData.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
spacing: 12
|
||||
|
||||
Label {
|
||||
text: modelData.code === "GLOBAL" ? "🌎" : getFlagEmoji(modelData.code)
|
||||
font.pixelSize: 26
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - 38 // 26 (emoji) + 12 (spacing)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: modelData.name
|
||||
color: textMain
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: modelData.code === "GLOBAL" ? qsTr("Top 200") :
|
||||
(modelData.count ? modelData.count + " " + qsTr("stations") : modelData.code)
|
||||
color: textMuted
|
||||
font.pixelSize: 10
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 16
|
||||
visible: countrySearchText === ""
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: cardBorder
|
||||
opacity: 0.4
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Popular Music Styles")
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
color: accent
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: tagFlow
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Repeater {
|
||||
model: worldTags
|
||||
delegate: Rectangle {
|
||||
id: tagCard
|
||||
width: compactMode ? (tagFlow.width - 8) / 2 : (tagFlow.width - 16) / 3
|
||||
height: 54
|
||||
radius: 8
|
||||
color: tagMouse.containsMouse ? cardHover : card
|
||||
border.color: tagMouse.containsMouse ? accent : cardBorder
|
||||
border.width: 1
|
||||
scale: tagMouse.containsMouse ? 1.02 : 1.0
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 100 } }
|
||||
Behavior on scale { NumberAnimation { duration: 100; easing.type: Easing.OutQuad } }
|
||||
|
||||
MouseArea {
|
||||
id: tagMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
selectedWorldCategory = modelData.name
|
||||
selectedWorldType = "tag"
|
||||
backend.loadByTag(modelData.tag)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
spacing: 12
|
||||
|
||||
Label {
|
||||
text: modelData.icon
|
||||
font.pixelSize: 22
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width - 34 // 22 (icon) + 12 (spacing)
|
||||
text: modelData.name
|
||||
color: textMain
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
visible: stationList.count === 0
|
||||
visible: stationList.count === 0 && !(currentPage === "world" && selectedWorldCategory === "")
|
||||
|
||||
Label {
|
||||
text: currentPage === "history" ? qsTr("No playback history") : qsTr("No stations loaded yet")
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QStandardPaths>
|
||||
#include <QLocale>
|
||||
|
||||
namespace {
|
||||
constexpr int kRecentLimit = 20;
|
||||
@@ -53,6 +54,8 @@ void RadioBackend::setupConnections()
|
||||
{
|
||||
connect(m_radioBrowser, &RadioBrowser::stationsLoaded,
|
||||
this, &RadioBackend::onStationsLoaded);
|
||||
connect(m_radioBrowser, &RadioBrowser::countriesLoaded,
|
||||
this, &RadioBackend::onCountriesLoaded);
|
||||
connect(m_radioBrowser, &RadioBrowser::error,
|
||||
this, [this](const QString &error) {
|
||||
qWarning() << "RadioBrowser error:" << error;
|
||||
@@ -128,6 +131,30 @@ void RadioBackend::loadWorldStations()
|
||||
m_radioBrowser->getWorldStations(200);
|
||||
}
|
||||
|
||||
void RadioBackend::loadCountries()
|
||||
{
|
||||
setLoading(true);
|
||||
m_radioBrowser->getCountries();
|
||||
}
|
||||
|
||||
void RadioBackend::onCountriesLoaded(const QVariantList &countries)
|
||||
{
|
||||
m_countries.clear();
|
||||
for (const QVariant &item : countries) {
|
||||
QVariantMap map = item.toMap();
|
||||
QString code = map["code"].toString();
|
||||
QString englishName = map["name"].toString();
|
||||
map["name"] = localizeCountry(code, englishName);
|
||||
m_countries.append(map);
|
||||
}
|
||||
|
||||
std::sort(m_countries.begin(), m_countries.end(), [](const QVariant &a, const QVariant &b) {
|
||||
return QString::localeAwareCompare(a.toMap()["name"].toString(), b.toMap()["name"].toString()) < 0;
|
||||
});
|
||||
emit countriesChanged();
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
void RadioBackend::loadByTag(const QString &tag)
|
||||
{
|
||||
if (tag.trimmed().isEmpty()) {
|
||||
@@ -794,3 +821,100 @@ bool RadioBackend::matchesStation(const RadioStation *station, const QString &uu
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString RadioBackend::localizeCountry(const QString &code, const QString &englishName)
|
||||
{
|
||||
QLocale locale;
|
||||
if (locale.language() != QLocale::German) {
|
||||
return englishName;
|
||||
}
|
||||
|
||||
static const QHash<QString, QString> germanCountries = {
|
||||
{"AD", "Andorra"}, {"AE", "Vereinigte Arabische Emirate"}, {"AF", "Afghanistan"},
|
||||
{"AG", "Antigua und Barbuda"}, {"AI", "Anguilla"}, {"AL", "Albanien"},
|
||||
{"AM", "Armenien"}, {"AO", "Angola"}, {"AQ", "Antarktis"}, {"AR", "Argentinien"},
|
||||
{"AS", "Amerikanisch-Samoa"}, {"AT", "Österreich"}, {"AU", "Australien"},
|
||||
{"AW", "Aruba"}, {"AX", "Åland-Inseln"}, {"AZ", "Aserbaidschan"},
|
||||
{"BA", "Bosnien und Herzegowina"}, {"BB", "Barbados"}, {"BD", "Bangladesch"},
|
||||
{"BE", "Belgien"}, {"BF", "Burkina Faso"}, {"BG", "Bulgarien"},
|
||||
{"BH", "Bahrain"}, {"BI", "Burundi"}, {"BJ", "Benin"},
|
||||
{"BL", "St. Bartholomäus"}, {"BM", "Bermuda"}, {"BN", "Brunei"},
|
||||
{"BO", "Bolivien"}, {"BQ", "Bonaire"}, {"BR", "Brasilien"},
|
||||
{"BS", "Bahamas"}, {"BT", "Bhutan"}, {"BV", "Bouvetinsel"},
|
||||
{"BW", "Botswana"}, {"BY", "Belarus"}, {"BZ", "Belize"},
|
||||
{"CA", "Kanada"}, {"CC", "Kokosinseln"}, {"CD", "Kongo-Kinshasa"},
|
||||
{"CF", "Zentralafrikanische Republik"}, {"CG", "Kongo-Brazzaville"}, {"CH", "Schweiz"},
|
||||
{"CI", "Elfenbeinküste"}, {"CK", "Cookinseln"}, {"CL", "Chile"},
|
||||
{"CM", "Kamerun"}, {"CN", "China"}, {"CO", "Kolumbien"},
|
||||
{"CR", "Costa Rica"}, {"CU", "Kuba"}, {"CV", "Cabo Verde"},
|
||||
{"CW", "Curaçao"}, {"CX", "Weihnachtsinsel"}, {"CY", "Zypern"},
|
||||
{"CZ", "Tschechien"}, {"DE", "Deutschland"}, {"DJ", "Dschibuti"},
|
||||
{"DK", "Dänemark"}, {"DM", "Dominica"}, {"DO", "Dominikanische Republik"},
|
||||
{"DZ", "Algerien"}, {"EC", "Ecuador"}, {"EE", "Estland"},
|
||||
{"EG", "Ägypten"}, {"EH", "Westsahara"}, {"ER", "Eritrea"},
|
||||
{"ES", "Spanien"}, {"ET", "Äthiopien"}, {"FI", "Finnland"},
|
||||
{"FJ", "Fidschi"}, {"FK", "Falklandinseln"}, {"FM", "Mikronesien"},
|
||||
{"FO", "Färöer"}, {"FR", "Frankreich"}, {"GA", "Gabun"},
|
||||
{"GB", "Großbritannien"}, {"GD", "Grenada"}, {"GE", "Georgien"},
|
||||
{"GF", "Französisch-Guayana"}, {"GG", "Guernsey"}, {"GH", "Ghana"},
|
||||
{"GI", "Gibraltar"}, {"GL", "Grönland"}, {"GM", "Gambia"},
|
||||
{"GN", "Guinea"}, {"GP", "Guadeloupe"}, {"GQ", "Äquatorialguinea"},
|
||||
{"GR", "Griechenland"}, {"GS", "Südgeorgien"}, {"GT", "Guatemala"},
|
||||
{"GU", "Guam"}, {"GW", "Guinea-Bissau"}, {"GY", "Guyana"},
|
||||
{"HK", "Hongkong"}, {"HM", "Heard und McDonaldinseln"}, {"HN", "Honduras"},
|
||||
{"HR", "Kroatien"}, {"HT", "Haiti"}, {"HU", "Ungarn"},
|
||||
{"ID", "Indonesien"}, {"IE", "Irland"}, {"IL", "Israel"},
|
||||
{"IM", "Isle of Man"}, {"IN", "Indien"}, {"IO", "Britisches Territorium im Indischen Ozean"},
|
||||
{"IQ", "Irak"}, {"IR", "Iran"}, {"IS", "Island"},
|
||||
{"IT", "Italien"}, {"JE", "Jersey"}, {"JM", "Jamaika"},
|
||||
{"JO", "Jordanien"}, {"JP", "Japan"}, {"KE", "Kenia"},
|
||||
{"KG", "Kirgisistan"}, {"KH", "Kambodscha"}, {"KI", "Kiribati"},
|
||||
{"KM", "Komoren"}, {"KN", "St. Kitts und Nevis"}, {"KP", "Nordkorea"},
|
||||
{"KR", "Südkorea"}, {"KW", "Kuwait"}, {"KY", "Kaimaninseln"},
|
||||
{"KZ", "Kasachstan"}, {"LA", "Laos"}, {"LB", "Libanon"},
|
||||
{"LC", "St. Lucia"}, {"LI", "Liechtenstein"}, {"LK", "Sri Lanka"},
|
||||
{"LR", "Liberia"}, {"LS", "Lesotho"}, {"LT", "Litauen"},
|
||||
{"LU", "Luxemburg"}, {"LV", "Lettland"}, {"LY", "Libyen"},
|
||||
{"MA", "Marokko"}, {"MC", "Monaco"}, {"MD", "Moldawien"},
|
||||
{"ME", "Montenegro"}, {"MF", "St. Martin"}, {"MG", "Madagaskar"},
|
||||
{"MH", "Marshallinseln"}, {"MK", "Nordmazedonien"}, {"ML", "Mali"},
|
||||
{"MM", "Myanmar"}, {"MN", "Mongolei"}, {"MO", "Macau"},
|
||||
{"MP", "Nördliche Marianen"}, {"MQ", "Martinique"}, {"MR", "Mauretanien"},
|
||||
{"MS", "Montserrat"}, {"MT", "Malta"}, {"MU", "Mauritius"},
|
||||
{"MV", "Malediven"}, {"MW", "Malawi"}, {"MX", "Mexiko"},
|
||||
{"MY", "Malaysia"}, {"MZ", "Mosambik"}, {"NA", "Namibia"},
|
||||
{"NC", "Neukaledonien"}, {"NE", "Niger"}, {"NF", "Norfolkinsel"},
|
||||
{"NG", "Nigeria"}, {"NI", "Nicaragua"}, {"NL", "Niederlande"},
|
||||
{"NO", "Norwegen"}, {"NP", "Nepal"}, {"NR", "Nauru"},
|
||||
{"NU", "Niue"}, {"NZ", "Neuseeland"}, {"OM", "Oman"},
|
||||
{"PA", "Panama"}, {"PE", "Peru"}, {"PF", "Französisch-Polynesien"},
|
||||
{"PG", "Papua-Neuguinea"}, {"PH", "Philippinen"}, {"PK", "Pakistan"},
|
||||
{"PL", "Polen"}, {"PM", "St. Pierre und Miquelon"}, {"PN", "Pitcairninseln"},
|
||||
{"PR", "Puerto Rico"}, {"PS", "Palästina"}, {"PT", "Portugal"},
|
||||
{"PW", "Palau"}, {"PY", "Paraguay"}, {"QA", "Katar"},
|
||||
{"RE", "Réunion"}, {"RO", "Rumänien"}, {"RS", "Serbien"},
|
||||
{"RU", "Russland"}, {"RW", "Ruanda"}, {"SA", "Saudi-Arabien"},
|
||||
{"SB", "Salomonen"}, {"SC", "Seychellen"}, {"SD", "Sudan"},
|
||||
{"SE", "Schweden"}, {"SG", "Singapur"}, {"SH", "St. Helena"},
|
||||
{"SI", "Slowenien"}, {"SJ", "Svalbard und Jan Mayen"}, {"SK", "Slowakei"},
|
||||
{"SL", "Sierra Leone"}, {"SM", "San Marino"}, {"SN", "Senegal"},
|
||||
{"SO", "Somalia"}, {"SR", "Suriname"}, {"SS", "Südsudan"},
|
||||
{"ST", "São Tomé und Príncipe"}, {"SV", "El Salvador"}, {"SX", "Sint Maarten"},
|
||||
{"SY", "Syrien"}, {"SZ", "Eswatini"}, {"TC", "Turks- und Caicosinseln"},
|
||||
{"TD", "Tschad"}, {"TF", "Französische Süd- und Antarktisgebiete"}, {"TG", "Togo"},
|
||||
{"TH", "Thailand"}, {"TJ", "Tadschikistan"}, {"TK", "Tokelau"},
|
||||
{"TL", "Osttimor"}, {"TM", "Turkmenistan"}, {"TN", "Tunesien"},
|
||||
{"TO", "Tonga"}, {"TR", "Türkei"}, {"TT", "Trinidad und Tobago"},
|
||||
{"TV", "Tuvalu"}, {"TW", "Taiwan"}, {"TZ", "Tansania"},
|
||||
{"UA", "Ukraine"}, {"UG", "Uganda"}, {"UM", "Amerikanische Überseeinseln"},
|
||||
{"US", "Vereinigte Staaten"}, {"UY", "Uruguay"}, {"UZ", "Usbekistan"},
|
||||
{"VA", "Vatikanstadt"}, {"VC", "St. Vincent und die Grenadinen"}, {"VE", "Venezuela"},
|
||||
{"VG", "Britische Jungferninseln"}, {"VI", "Amerikanische Jungferninseln"}, {"VN", "Vietnam"},
|
||||
{"VU", "Vanuatu"}, {"WF", "Wallis und Futuna"}, {"WS", "Samoa"},
|
||||
{"YE", "Jemen"}, {"YT", "Mayotte"}, {"ZA", "Südafrika"},
|
||||
{"ZM", "Sambia"}, {"ZW", "Simbabwe"}
|
||||
};
|
||||
|
||||
QString result = germanCountries.value(code.toUpper());
|
||||
return result.isEmpty() ? englishName : result;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class RadioBackend : public QObject
|
||||
Q_PROPERTY(QString currentStationUuid READ currentStationUuid NOTIFY currentStationChanged)
|
||||
Q_PROPERTY(QString currentStationUrl READ currentStationUrl NOTIFY currentStationChanged)
|
||||
Q_PROPERTY(QObject* currentStation READ currentStation NOTIFY currentStationChanged)
|
||||
Q_PROPERTY(QVariantList countries READ countries NOTIFY countriesChanged)
|
||||
|
||||
public:
|
||||
explicit RadioBackend(QObject *parent = nullptr);
|
||||
@@ -54,6 +55,7 @@ public:
|
||||
Q_INVOKABLE void loadDutchStations();
|
||||
Q_INVOKABLE void loadTopStations();
|
||||
Q_INVOKABLE void loadWorldStations();
|
||||
Q_INVOKABLE void loadCountries();
|
||||
Q_INVOKABLE void loadByTag(const QString &tag);
|
||||
Q_INVOKABLE void loadByCountryCode(const QString &countryCode);
|
||||
Q_INVOKABLE void searchStations(const QString &query);
|
||||
@@ -74,6 +76,8 @@ public:
|
||||
Q_INVOKABLE bool playFavoriteByUuid(const QString &uuid, const QString &urlResolved = QString());
|
||||
Q_INVOKABLE void resumeLastStation();
|
||||
|
||||
QVariantList countries() const { return m_countries; }
|
||||
|
||||
signals:
|
||||
void stationsChanged();
|
||||
void favoritesChanged();
|
||||
@@ -84,9 +88,11 @@ signals:
|
||||
void filterQueryChanged();
|
||||
void currentStationChanged();
|
||||
void raiseRequested();
|
||||
void countriesChanged();
|
||||
|
||||
private slots:
|
||||
void onStationsLoaded(const QList<RadioStation*> &stations);
|
||||
void onCountriesLoaded(const QVariantList &countries);
|
||||
|
||||
private:
|
||||
RadioBrowser *m_radioBrowser = nullptr;
|
||||
@@ -104,6 +110,7 @@ private:
|
||||
QVariantList m_recentStations;
|
||||
QString m_currentStationUuid;
|
||||
QString m_currentStationUrl;
|
||||
QVariantList m_countries;
|
||||
|
||||
void setupConnections();
|
||||
void loadFavorites();
|
||||
@@ -119,6 +126,7 @@ private:
|
||||
void playCurrentSelection();
|
||||
static QVariantMap toVariantMap(const RadioStation *station);
|
||||
static bool matchesStation(const RadioStation *station, const QString &uuid, const QString &urlResolved);
|
||||
static QString localizeCountry(const QString &code, const QString &englishName);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -77,6 +77,11 @@ void RadioBrowser::getDutchStations()
|
||||
makeRequest(endpoint);
|
||||
}
|
||||
|
||||
void RadioBrowser::getCountries()
|
||||
{
|
||||
makeRequest("/countries");
|
||||
}
|
||||
|
||||
void RadioBrowser::makeRequest(const QString &endpoint)
|
||||
{
|
||||
const QString cacheDir = apiCacheDir();
|
||||
@@ -87,9 +92,16 @@ void RadioBrowser::makeRequest(const QString &endpoint)
|
||||
QFile cacheFile(cachePath);
|
||||
if (cacheFile.exists() && cacheFile.open(QIODevice::ReadOnly)) {
|
||||
QByteArray cachedData = cacheFile.readAll();
|
||||
QList<RadioStation*> stations = parseJsonResponse(cachedData);
|
||||
if (!stations.isEmpty()) {
|
||||
emit stationsLoaded(stations);
|
||||
if (endpoint == "/countries") {
|
||||
QVariantList list = parseCountriesJson(cachedData);
|
||||
if (!list.isEmpty()) {
|
||||
emit countriesLoaded(list);
|
||||
}
|
||||
} else {
|
||||
QList<RadioStation*> stations = parseJsonResponse(cachedData);
|
||||
if (!stations.isEmpty()) {
|
||||
emit stationsLoaded(stations);
|
||||
}
|
||||
}
|
||||
cacheFile.close();
|
||||
}
|
||||
@@ -122,19 +134,61 @@ void RadioBrowser::onReplyFinished()
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
QList<RadioStation*> stations = parseJsonResponse(data);
|
||||
|
||||
QString cachePath = reply->property("cachePath").toString();
|
||||
if (!cachePath.isEmpty() && !stations.isEmpty()) {
|
||||
QFile file(cachePath);
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
file.write(data);
|
||||
file.close();
|
||||
|
||||
if (reply->url().path().endsWith("/countries")) {
|
||||
QVariantList list = parseCountriesJson(data);
|
||||
if (!cachePath.isEmpty() && !list.isEmpty()) {
|
||||
QFile file(cachePath);
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
file.write(data);
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
emit countriesLoaded(list);
|
||||
} else {
|
||||
QList<RadioStation*> stations = parseJsonResponse(data);
|
||||
if (!cachePath.isEmpty() && !stations.isEmpty()) {
|
||||
QFile file(cachePath);
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
file.write(data);
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
emit stationsLoaded(stations);
|
||||
}
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
QVariantList RadioBrowser::parseCountriesJson(const QByteArray &jsonData)
|
||||
{
|
||||
QVariantList countries;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonData);
|
||||
if (!doc.isArray()) {
|
||||
return countries;
|
||||
}
|
||||
|
||||
QJsonArray array = doc.array();
|
||||
for (const QJsonValue &value : array) {
|
||||
if (value.isObject()) {
|
||||
QJsonObject obj = value.toObject();
|
||||
QString name = obj.value("name").toString();
|
||||
QString code = obj.value("iso_3166_1").toString();
|
||||
int count = obj.value("stationcount").toInt();
|
||||
|
||||
if (name.isEmpty() || code.isEmpty() || count <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QVariantMap map;
|
||||
map["name"] = name;
|
||||
map["code"] = code;
|
||||
map["count"] = count;
|
||||
countries.append(map);
|
||||
}
|
||||
}
|
||||
|
||||
emit stationsLoaded(stations);
|
||||
reply->deleteLater();
|
||||
return countries;
|
||||
}
|
||||
|
||||
QList<RadioStation*> RadioBrowser::parseJsonResponse(const QByteArray &jsonData)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QList>
|
||||
#include <QVariantList>
|
||||
|
||||
#include "radiostation.h"
|
||||
|
||||
@@ -25,9 +26,11 @@ public:
|
||||
void getWorldStations(int count = 200);
|
||||
void getGermanStations();
|
||||
void getDutchStations();
|
||||
void getCountries();
|
||||
|
||||
signals:
|
||||
void stationsLoaded(const QList<RadioStation*> &stations);
|
||||
void countriesLoaded(const QVariantList &countries);
|
||||
void error(const QString &message);
|
||||
|
||||
private slots:
|
||||
@@ -39,6 +42,7 @@ private:
|
||||
|
||||
void makeRequest(const QString &endpoint);
|
||||
QList<RadioStation*> parseJsonResponse(const QByteArray &jsonData);
|
||||
QVariantList parseCountriesJson(const QByteArray &jsonData);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -223,6 +223,50 @@
|
||||
<source>Play some stations to build history</source>
|
||||
<translation>Spiele Sender ab, um einen Verlauf aufzubauen</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>← Back to Categories</source>
|
||||
<translation>← Zurück zu den Kategorien</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>World > Country: </source>
|
||||
<translation>Welt > Land: </translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>World > Genre: </source>
|
||||
<translation>Welt > Genre: </translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Explore World Stations</source>
|
||||
<translation>Weltweite Sender entdecken</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Filter countries...</source>
|
||||
<translation>Länder filtern...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Choose a country or music style to find radio stations from all over the world.</source>
|
||||
<translation>Wähle ein Land oder eine Musikrichtung, um Radiosender aus der ganzen Welt zu finden.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Countries</source>
|
||||
<translation>Länder</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Filtered Countries</source>
|
||||
<translation>Gefilterte Länder</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Popular Music Styles</source>
|
||||
<translation>Beliebte Musikrichtungen</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Top Global</source>
|
||||
<translation>Weltweite Top-Sender</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>stations</source>
|
||||
<translation>Sender</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>RadioBackend</name>
|
||||
|
||||
Reference in New Issue
Block a user