diff --git a/src/main.cpp b/src/main.cpp
index e8c9708..cfbc488 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -49,6 +49,7 @@ int main(int argc, char *argv[])
}
QQmlApplicationEngine engine;
+ engine.addImportPath(QStringLiteral("qrc:/qml"));
RadioBackend backend;
engine.rootContext()->setContextProperty("radioBackend", &backend);
@@ -70,7 +71,7 @@ int main(int argc, char *argv[])
mprisPlayer->publishState();
}
- const QUrl url(QStringLiteral("qrc:/Main.qml"));
+ const QUrl url(QStringLiteral("qrc:/qml/Main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [&app, url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl) {
diff --git a/src/qml.qrc b/src/qml.qrc
index 396f25a..03d5461 100644
--- a/src/qml.qrc
+++ b/src/qml.qrc
@@ -1,10 +1,19 @@
+
+ qml/Main.qml
+ qml/theme/qmldir
+ qml/theme/BearTheme.qml
+ qml/components/qmldir
+ qml/components/HeaderNavigation.qml
+ qml/components/SearchToolbar.qml
+ qml/components/QuickFilters.qml
+ qml/utils/FlagUtils.js
+
- qml/Main.qml
../assets/app/bearwave.png
../assets/app/bearwave_line.png
../assets/ui/globe.svg
../assets/ui/github.svg
../assets/ui/linkedin.svg
-
+
\ No newline at end of file
diff --git a/src/qml/Main.qml b/src/qml/Main.qml
index 361d587..9a5f7f1 100644
--- a/src/qml/Main.qml
+++ b/src/qml/Main.qml
@@ -5,6 +5,10 @@ import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
+import theme 1.0
+import components 1.0
+import "utils/FlagUtils.js" as FlagUtils
+
ApplicationWindow {
id: root
title: qsTr("BearWave")
@@ -27,32 +31,7 @@ ApplicationWindow {
property string selectedWorldCategory: ""
property string selectedWorldType: ""
property string countrySearchText: ""
-
- readonly property color bgA: "#0f141b"
- readonly property color bgB: "#131b25"
- readonly property color panel: "#182433"
- readonly property color card: "#1b2a3d"
- readonly property color cardHover: "#223654"
- readonly property color cardBorder: "#2d4566"
- readonly property color accent: "#2bb0ff"
- readonly property color textMain: "#eaf1fb"
- 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: "πΊ" }
- ]
+ property alias searchField: searchToolbar.searchField
function toast(message) {
toastLabel.text = message
@@ -84,16 +63,6 @@ ApplicationWindow {
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 []
@@ -139,8 +108,8 @@ ApplicationWindow {
Rectangle {
anchors.fill: parent
gradient: Gradient {
- GradientStop { position: 0.0; color: bgA }
- GradientStop { position: 1.0; color: bgB }
+ GradientStop { position: 0.0; color: BearTheme.bgA }
+ GradientStop { position: 1.0; color: BearTheme.bgB }
}
}
@@ -153,8 +122,8 @@ ApplicationWindow {
Layout.fillWidth: true
Layout.preferredHeight: headerContent.implicitHeight + 20
radius: 12
- color: panel
- border.color: cardBorder
+ color: BearTheme.panel
+ border.color: BearTheme.cardBorder
ColumnLayout {
id: headerContent
@@ -162,584 +131,38 @@ ApplicationWindow {
anchors.margins: 10
spacing: 8
- RowLayout {
- visible: !compactMode
+ HeaderNavigation {
Layout.fillWidth: true
- spacing: 8
-
- Image {
- Layout.preferredWidth: 112
- Layout.preferredHeight: 40
- source: "qrc:/assets/app/bearwave_line.png"
- fillMode: Image.PreserveAspectFit
- smooth: true
- mipmap: true
- }
-
- Rectangle {
- Layout.preferredWidth: 1
- Layout.fillHeight: true
- color: cardBorder
- opacity: 0.6
- }
-
- Button {
- text: qsTr("Top")
- highlighted: currentPage === "top"
- onClicked: {
- if (!backend) return
- currentPage = "top"
- activeQuickFilter = ""
- backend.loadTopStations()
- }
- }
-
- Button {
- text: qsTr("DE")
- highlighted: currentPage === "german"
- onClicked: {
- if (!backend) return
- currentPage = "german"
- activeQuickFilter = ""
- backend.loadGermanStations()
- }
- }
-
- Button {
- text: qsTr("NL")
- highlighted: currentPage === "dutch"
- onClicked: {
- if (!backend) return
- currentPage = "dutch"
- activeQuickFilter = ""
- backend.loadDutchStations()
- }
- }
-
- Button {
- text: qsTr("Favorites")
- highlighted: currentPage === "favorites"
- onClicked: {
- currentPage = "favorites"
- activeQuickFilter = ""
- }
- }
-
- Button {
- text: qsTr("History")
- highlighted: currentPage === "history"
- onClicked: {
- currentPage = "history"
- activeQuickFilter = ""
- }
- }
-
- Item { Layout.fillWidth: true }
-
- Button {
- text: compactMode ? "+" : qsTr("Manual +")
- onClicked: addDialog.open()
- }
-
- Button {
- text: qsTr("About")
- onClicked: aboutDialog.open()
- }
-
- Button {
- visible: backend && backend.canResumeLastStation
- text: compactMode ? "βΊ" : qsTr("Resume")
- onClicked: {
- if (backend) {
- backend.resumeLastStation()
- }
- }
- }
- }
-
- ColumnLayout {
- visible: compactMode
- Layout.fillWidth: true
- spacing: 8
-
- RowLayout {
- Layout.fillWidth: true
- spacing: 8
-
- Image {
- Layout.preferredWidth: 96
- Layout.preferredHeight: 34
- source: "qrc:/assets/app/bearwave_line.png"
- fillMode: Image.PreserveAspectFit
- smooth: true
- mipmap: true
- }
-
- Item { Layout.fillWidth: true }
-
- Button {
- text: "+"
- onClicked: addDialog.open()
- }
-
- Button {
- text: qsTr("About")
- onClicked: aboutDialog.open()
- }
-
- Button {
- visible: backend && backend.canResumeLastStation
- text: "βΊ"
- onClicked: {
- if (backend) {
- backend.resumeLastStation()
- }
- }
- }
- }
-
- Flow {
- Layout.fillWidth: true
- width: parent.width
- spacing: 8
-
- Button {
- text: qsTr("Top")
- highlighted: currentPage === "top"
- onClicked: {
- if (!backend) return
- currentPage = "top"
- activeQuickFilter = ""
- backend.loadTopStations()
- }
- }
-
- Button {
- text: qsTr("DE")
- highlighted: currentPage === "german"
- onClicked: {
- if (!backend) return
- currentPage = "german"
- activeQuickFilter = ""
- backend.loadGermanStations()
- }
- }
-
- Button {
- text: qsTr("NL")
- highlighted: currentPage === "dutch"
- onClicked: {
- if (!backend) return
- currentPage = "dutch"
- activeQuickFilter = ""
- backend.loadDutchStations()
- }
- }
-
- Button {
- text: qsTr("Favorites")
- highlighted: currentPage === "favorites"
- onClicked: {
- currentPage = "favorites"
- activeQuickFilter = ""
- }
- }
-
- Button {
- text: qsTr("History")
- highlighted: currentPage === "history"
- onClicked: {
- currentPage = "history"
- activeQuickFilter = ""
- }
- }
- }
+ app: root
+ compactMode: root.compactMode
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
- color: cardBorder
+ color: BearTheme.cardBorder
opacity: 0.6
}
- RowLayout {
- visible: !compactMode
+ SearchToolbar {
+ id: searchToolbar
Layout.fillWidth: true
- spacing: 8
-
- TextField {
- id: searchField
- Layout.fillWidth: true
- placeholderText: qsTr("Search stations (name, genre, country)")
- onTextChanged: {
- if (backend) {
- backend.filterQuery = text
- }
- searchTimer.restart()
- }
- onAccepted: {
- if (text.length < 2 || !backend) return
- currentPage = "search"
- backend.searchStations(text)
- }
- }
-
- Button {
- text: qsTr("Search")
- highlighted: true
- onClicked: {
- if (searchField.text.length < 2 || !backend) return
- currentPage = "search"
- backend.searchStations(searchField.text)
- }
- }
-
- Button {
- text: compactMode ? "A-Z" : qsTr("Sort A-Z")
- onClicked: {
- if (backend && currentPage !== "favorites") {
- backend.sortStations("name")
- }
- }
- }
-
- Button {
- text: compactMode ? "kb" : qsTr("Sort Bitrate")
- onClicked: {
- if (backend && currentPage !== "favorites") {
- backend.sortStations("bitrate")
- }
- }
- }
-
- Button {
- text: compactMode ? "β€" : qsTr("Sort Votes")
- onClicked: {
- if (backend && currentPage !== "favorites") {
- backend.sortStations("votes")
- }
- }
- }
+ app: root
+ compactMode: root.compactMode
+ searchTimer: searchTimer
}
- ColumnLayout {
- visible: compactMode
+ QuickFilters {
Layout.fillWidth: true
- spacing: 8
-
- TextField {
- id: compactSearchField
- Layout.fillWidth: true
- placeholderText: qsTr("Search stations (name, genre, country)")
- text: searchField.text
- onTextChanged: {
- if (searchField.text !== text) {
- searchField.text = text
- }
- if (backend) {
- backend.filterQuery = text
- }
- searchTimer.restart()
- }
- onAccepted: {
- if (text.length < 2 || !backend) return
- currentPage = "search"
- backend.searchStations(text)
- }
- }
-
- RowLayout {
- Layout.fillWidth: true
- spacing: 8
-
- Item { Layout.fillWidth: true }
-
- Button {
- text: qsTr("Search")
- highlighted: true
- onClicked: {
- if (compactSearchField.text.length < 2 || !backend) return
- currentPage = "search"
- backend.searchStations(compactSearchField.text)
- }
- }
- }
-
- Flow {
- Layout.fillWidth: true
- width: parent.width
- spacing: 8
-
- Button {
- text: "A-Z"
- onClicked: {
- if (backend && currentPage !== "favorites") {
- backend.sortStations("name")
- }
- }
- }
-
- Button {
- text: "kb"
- onClicked: {
- if (backend && currentPage !== "favorites") {
- backend.sortStations("bitrate")
- }
- }
- }
-
- Button {
- text: "β€"
- onClicked: {
- if (backend && currentPage !== "favorites") {
- backend.sortStations("votes")
- }
- }
- }
- }
- }
-
- Label {
- Layout.fillWidth: true
- text: qsTr("Tip: you are not limited to DE/NL. Search worldwide by country, genre, or station name.")
- color: textMuted
- font.pixelSize: 11
- wrapMode: compactMode ? Text.WordWrap : Text.NoWrap
- elide: compactMode ? Text.ElideNone : Text.ElideRight
- }
-
- RowLayout {
- visible: !compactMode
- Layout.fillWidth: true
- spacing: 6
-
- Label {
- text: qsTr("Genre:")
- color: textMuted
- font.pixelSize: 11
- rightPadding: 10
- }
-
- Button {
- text: qsTr("Rock")
- highlighted: activeQuickFilter === "tag:rock"
- onClicked: {
- if (!backend) return
- currentPage = "genre-rock"
- activeQuickFilter = "tag:rock"
- backend.loadByTag("rock")
- }
- }
-
- Button {
- text: qsTr("News")
- highlighted: activeQuickFilter === "tag:news"
- onClicked: {
- if (!backend) return
- currentPage = "genre-news"
- activeQuickFilter = "tag:news"
- backend.loadByTag("news")
- }
- }
-
- Button {
- text: qsTr("Jazz")
- highlighted: activeQuickFilter === "tag:jazz"
- onClicked: {
- if (!backend) return
- currentPage = "genre-jazz"
- activeQuickFilter = "tag:jazz"
- backend.loadByTag("jazz")
- }
- }
-
- Item { Layout.preferredWidth: 20 }
-
- Label {
- text: qsTr("Country:")
- color: textMuted
- font.pixelSize: 11
- rightPadding: 10
- }
-
- Button {
- text: qsTr("US")
- highlighted: activeQuickFilter === "cc:US"
- onClicked: {
- if (!backend) return
- currentPage = "country-us"
- activeQuickFilter = "cc:US"
- backend.loadByCountryCode("US")
- }
- }
-
- Button {
- text: qsTr("UK")
- highlighted: activeQuickFilter === "cc:GB"
- onClicked: {
- if (!backend) return
- currentPage = "country-gb"
- activeQuickFilter = "cc:GB"
- backend.loadByCountryCode("GB")
- }
- }
-
- Button {
- text: qsTr("FR")
- highlighted: activeQuickFilter === "cc:FR"
- onClicked: {
- if (!backend) return
- currentPage = "country-fr"
- activeQuickFilter = "cc:FR"
- backend.loadByCountryCode("FR")
- }
- }
-
- Button {
- text: qsTr("WORLD")
- highlighted: activeQuickFilter === "world"
- onClicked: {
- if (!backend) return
- currentPage = "world"
- activeQuickFilter = "world"
- selectedWorldCategory = ""
- selectedWorldType = ""
- if (backend.countries.length === 0) {
- backend.loadCountries()
- }
- }
- }
-
- Item { Layout.fillWidth: true }
- }
-
- ColumnLayout {
- visible: compactMode
- Layout.fillWidth: true
- spacing: 8
-
- Flow {
- Layout.fillWidth: true
- width: parent.width
- spacing: 8
-
- Label {
- text: qsTr("Genre:")
- color: textMuted
- font.pixelSize: 11
- verticalAlignment: Text.AlignVCenter
- leftPadding: 8
- rightPadding: 16
- topPadding: 8
- bottomPadding: 8
- }
-
- Button {
- text: qsTr("Rock")
- highlighted: activeQuickFilter === "tag:rock"
- onClicked: {
- if (!backend) return
- currentPage = "genre-rock"
- activeQuickFilter = "tag:rock"
- backend.loadByTag("rock")
- }
- }
-
- Button {
- text: qsTr("News")
- highlighted: activeQuickFilter === "tag:news"
- onClicked: {
- if (!backend) return
- currentPage = "genre-news"
- activeQuickFilter = "tag:news"
- backend.loadByTag("news")
- }
- }
-
- Button {
- text: qsTr("Jazz")
- highlighted: activeQuickFilter === "tag:jazz"
- onClicked: {
- if (!backend) return
- currentPage = "genre-jazz"
- activeQuickFilter = "tag:jazz"
- backend.loadByTag("jazz")
- }
- }
- }
-
- Flow {
- Layout.fillWidth: true
- width: parent.width
- spacing: 8
-
- Label {
- text: qsTr("Country:")
- color: textMuted
- font.pixelSize: 11
- verticalAlignment: Text.AlignVCenter
- leftPadding: 8
- rightPadding: 16
- topPadding: 8
- bottomPadding: 8
- }
-
- Button {
- text: qsTr("US")
- highlighted: activeQuickFilter === "cc:US"
- onClicked: {
- if (!backend) return
- currentPage = "country-us"
- activeQuickFilter = "cc:US"
- backend.loadByCountryCode("US")
- }
- }
-
- Button {
- text: qsTr("UK")
- highlighted: activeQuickFilter === "cc:GB"
- onClicked: {
- if (!backend) return
- currentPage = "country-gb"
- activeQuickFilter = "cc:GB"
- backend.loadByCountryCode("GB")
- }
- }
-
- Button {
- text: qsTr("FR")
- highlighted: activeQuickFilter === "cc:FR"
- onClicked: {
- if (!backend) return
- currentPage = "country-fr"
- activeQuickFilter = "cc:FR"
- backend.loadByCountryCode("FR")
- }
- }
-
- Button {
- text: qsTr("WORLD")
- highlighted: activeQuickFilter === "world"
- onClicked: {
- if (!backend) return
- currentPage = "world"
- activeQuickFilter = "world"
- selectedWorldCategory = ""
- selectedWorldType = ""
- if (backend.countries.length === 0) {
- backend.loadCountries()
- }
- }
- }
- }
+ app: root
+ compactMode: root.compactMode
}
Label {
Layout.fillWidth: true
visible: backend && backend.lastError.length > 0
text: backend ? (qsTr("Error: ") + backend.lastError) : ""
- color: warn
+ color: BearTheme.warn
elide: Text.ElideRight
font.pixelSize: 12
}
@@ -750,8 +173,8 @@ ApplicationWindow {
Layout.fillWidth: true
Layout.fillHeight: true
radius: 12
- color: panel
- border.color: cardBorder
+ color: BearTheme.panel
+ border.color: BearTheme.cardBorder
clip: true
opacity: contentOpacity
@@ -782,7 +205,7 @@ ApplicationWindow {
text: selectedWorldType === "country"
? (qsTr("World > Country: ") + selectedWorldCategory)
: (qsTr("World > Genre: ") + selectedWorldCategory)
- color: textMain
+ color: BearTheme.textMain
font.bold: true
font.pixelSize: 13
Layout.fillWidth: true
@@ -828,8 +251,8 @@ ApplicationWindow {
color: stationCard.isCurrent
? (cardMouse.containsMouse ? "#1d3350" : "#16283e")
- : (cardMouse.containsMouse ? cardHover : card)
- border.color: stationCard.isCurrent ? accent : cardBorder
+ : (cardMouse.containsMouse ? BearTheme.cardHover : BearTheme.card)
+ border.color: stationCard.isCurrent ? BearTheme.accent : BearTheme.cardBorder
border.width: stationCard.isCurrent ? 2 : 1
MouseArea {
@@ -863,7 +286,7 @@ ApplicationWindow {
Layout.preferredHeight: 44
radius: 8
color: "#123154"
- border.color: accent
+ border.color: BearTheme.accent
Image {
id: stationFavicon
@@ -899,7 +322,7 @@ ApplicationWindow {
Label {
Layout.fillWidth: true
text: stationCard.modelData.name
- color: stationCard.isCurrent ? accent : textMain
+ color: stationCard.isCurrent ? BearTheme.accent : BearTheme.textMain
font.bold: true
font.pixelSize: compactMode ? 13 : 14
elide: Text.ElideRight
@@ -915,7 +338,7 @@ ApplicationWindow {
id: bar1
width: 2
height: 12
- color: accent
+ color: BearTheme.accent
radius: 1
Behavior on height {
NumberAnimation { duration: 120 }
@@ -925,7 +348,7 @@ ApplicationWindow {
id: bar2
width: 2
height: 12
- color: accent
+ color: BearTheme.accent
radius: 1
Behavior on height {
NumberAnimation { duration: 120 }
@@ -935,7 +358,7 @@ ApplicationWindow {
id: bar3
width: 2
height: 12
- color: accent
+ color: BearTheme.accent
radius: 1
Behavior on height {
NumberAnimation { duration: 120 }
@@ -968,7 +391,7 @@ ApplicationWindow {
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
+ color: BearTheme.textMuted
font.pixelSize: 11
elide: Text.ElideRight
}
@@ -1042,7 +465,7 @@ ApplicationWindow {
text: qsTr("Explore World Stations")
font.bold: true
font.pixelSize: 16
- color: textMain
+ color: BearTheme.textMain
Layout.fillWidth: true
}
@@ -1059,7 +482,7 @@ ApplicationWindow {
Label {
text: qsTr("Choose a country or music style to find radio stations from all over the world.")
font.pixelSize: 11
- color: textMuted
+ color: BearTheme.textMuted
wrapMode: Text.WordWrap
width: parent.width
visible: countrySearchText === ""
@@ -1068,7 +491,7 @@ ApplicationWindow {
Rectangle {
width: parent.width
height: 1
- color: cardBorder
+ color: BearTheme.cardBorder
opacity: 0.4
visible: countrySearchText === ""
}
@@ -1077,7 +500,7 @@ ApplicationWindow {
text: countrySearchText === "" ? qsTr("Countries") : qsTr("Filtered Countries")
font.bold: true
font.pixelSize: 13
- color: accent
+ color: BearTheme.accent
}
Flow {
@@ -1092,8 +515,8 @@ ApplicationWindow {
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
+ color: countryMouse.containsMouse ? BearTheme.cardHover : BearTheme.card
+ border.color: countryMouse.containsMouse ? BearTheme.accent : BearTheme.cardBorder
border.width: 1
scale: countryMouse.containsMouse ? 1.02 : 1.0
@@ -1123,7 +546,7 @@ ApplicationWindow {
spacing: 12
Label {
- text: modelData.code === "GLOBAL" ? "π" : getFlagEmoji(modelData.code)
+ text: modelData.code === "GLOBAL" ? "π" : FlagUtils.flagEmoji(modelData.code)
font.pixelSize: 26
anchors.verticalCenter: parent.verticalCenter
}
@@ -1136,7 +559,7 @@ ApplicationWindow {
Label {
width: parent.width
text: modelData.name
- color: textMain
+ color: BearTheme.textMain
font.bold: true
font.pixelSize: 13
elide: Text.ElideRight
@@ -1146,7 +569,7 @@ ApplicationWindow {
width: parent.width
text: modelData.code === "GLOBAL" ? qsTr("Top 200") :
(modelData.count ? modelData.count + " " + qsTr("stations") : modelData.code)
- color: textMuted
+ color: BearTheme.textMuted
font.pixelSize: 10
elide: Text.ElideRight
}
@@ -1164,7 +587,7 @@ ApplicationWindow {
Rectangle {
width: parent.width
height: 1
- color: cardBorder
+ color: BearTheme.cardBorder
opacity: 0.4
}
@@ -1172,7 +595,7 @@ ApplicationWindow {
text: qsTr("Popular Music Styles")
font.bold: true
font.pixelSize: 13
- color: accent
+ color: BearTheme.accent
}
Flow {
@@ -1181,14 +604,14 @@ ApplicationWindow {
spacing: 8
Repeater {
- model: worldTags
+ model: BearTheme.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
+ color: tagMouse.containsMouse ? BearTheme.cardHover : BearTheme.card
+ border.color: tagMouse.containsMouse ? BearTheme.accent : BearTheme.cardBorder
border.width: 1
scale: tagMouse.containsMouse ? 1.02 : 1.0
@@ -1222,7 +645,7 @@ ApplicationWindow {
Label {
width: parent.width - 34 // 22 (icon) + 12 (spacing)
text: modelData.name
- color: textMain
+ color: BearTheme.textMain
font.bold: true
font.pixelSize: 13
elide: Text.ElideRight
@@ -1244,13 +667,13 @@ ApplicationWindow {
Label {
text: currentPage === "history" ? qsTr("No playback history") : qsTr("No stations loaded yet")
- color: textMain
+ color: BearTheme.textMain
font.bold: true
}
Label {
text: currentPage === "history" ? qsTr("Play some stations to build history") : qsTr("Load DE/NL stations or use search")
- color: textMuted
+ color: BearTheme.textMuted
}
}
}
@@ -1259,8 +682,8 @@ ApplicationWindow {
Layout.fillWidth: true
Layout.preferredHeight: 108
radius: 12
- color: panel
- border.color: cardBorder
+ color: BearTheme.panel
+ border.color: BearTheme.cardBorder
RowLayout {
anchors.fill: parent
@@ -1273,7 +696,7 @@ ApplicationWindow {
radius: 8
color: "#182637"
clip: true
- border.color: cardBorder
+ border.color: BearTheme.cardBorder
Image {
id: coverImage
@@ -1314,7 +737,7 @@ ApplicationWindow {
Label {
Layout.fillWidth: true
text: backend && backend.player ? (backend.player.currentStationName || qsTr("No station selected")) : qsTr("No station selected")
- color: textMain
+ color: BearTheme.textMain
font.pixelSize: 16
font.bold: true
elide: Text.ElideRight
@@ -1327,14 +750,14 @@ ApplicationWindow {
width: codecLabel.implicitWidth + 12
radius: 4
color: "transparent"
- border.color: accent
+ border.color: BearTheme.accent
border.width: 1
Label {
id: codecLabel
anchors.centerIn: parent
text: (backend && backend.currentStation && backend.currentStation.codec) ? backend.currentStation.codec.toUpperCase() : ""
- color: accent
+ color: BearTheme.accent
font.pixelSize: 9
font.bold: true
}
@@ -1346,8 +769,8 @@ ApplicationWindow {
height: 18
width: bitrateLabel.implicitWidth + 12
radius: 4
- color: accent
- border.color: accent
+ color: BearTheme.accent
+ border.color: BearTheme.accent
border.width: 1
Label {
@@ -1366,7 +789,7 @@ ApplicationWindow {
text: backend && backend.player && backend.player.currentNowPlaying.length > 0
? (qsTr("Now playing: ") + backend.player.currentNowPlaying)
: qsTr("Now playing: No track info")
- color: textMuted
+ color: BearTheme.textMuted
font.pixelSize: 12
elide: Text.ElideRight
}
@@ -1466,8 +889,8 @@ ApplicationWindow {
width: 210
height: 110
radius: 12
- color: panel
- border.color: cardBorder
+ color: BearTheme.panel
+ border.color: BearTheme.cardBorder
Column {
anchors.centerIn: parent
@@ -1482,7 +905,7 @@ ApplicationWindow {
Label {
text: qsTr("Loading stations...")
- color: textMain
+ color: BearTheme.textMain
anchors.horizontalCenter: parent.horizontalCenter
}
}
@@ -1596,7 +1019,7 @@ ApplicationWindow {
Label {
Layout.alignment: Qt.AlignHCenter
text: qsTr("BearWave")
- color: textMain
+ color: BearTheme.textMain
font.bold: true
font.pixelSize: 26
}
@@ -1604,21 +1027,21 @@ ApplicationWindow {
Label {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Internet Radio Player for KDE")
- color: textMuted
+ color: BearTheme.textMuted
font.pixelSize: 14
}
Label {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Version: %1").arg(Qt.application.version)
- color: textMuted
+ color: BearTheme.textMuted
font.pixelSize: 12
}
Label {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Public beta")
- color: accent
+ color: BearTheme.accent
font.pixelSize: 12
font.bold: true
}
@@ -1634,7 +1057,7 @@ ApplicationWindow {
Label {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Author: Sebastian PalencsΓ‘r")
- color: textMain
+ color: BearTheme.textMain
font.bold: true
font.pixelSize: 14
}
@@ -1662,7 +1085,7 @@ ApplicationWindow {
background: Rectangle {
radius: 20
color: parent.hovered ? "#274261" : "#1f3147"
- border.color: cardBorder
+ border.color: BearTheme.cardBorder
border.width: 1
}
}
@@ -1686,7 +1109,7 @@ ApplicationWindow {
background: Rectangle {
radius: 20
color: parent.hovered ? "#274261" : "#1f3147"
- border.color: cardBorder
+ border.color: BearTheme.cardBorder
border.width: 1
}
}
@@ -1710,7 +1133,7 @@ ApplicationWindow {
background: Rectangle {
radius: 20
color: parent.hovered ? "#274261" : "#1f3147"
- border.color: cardBorder
+ border.color: BearTheme.cardBorder
border.width: 1
}
}
@@ -1728,12 +1151,12 @@ ApplicationWindow {
Label {
Layout.fillWidth: true
text: qsTr("GNU GPLv3 License")
- color: textMain
+ color: BearTheme.textMain
font.bold: true
}
Label {
text: qsTr("Copyright (c) 2026")
- color: textMuted
+ color: BearTheme.textMuted
font.pixelSize: 11
}
}
@@ -1744,7 +1167,7 @@ ApplicationWindow {
Layout.minimumHeight: 140
radius: 8
color: "#101a26"
- border.color: cardBorder
+ border.color: BearTheme.cardBorder
ScrollView {
id: licenseScroll
@@ -1755,7 +1178,7 @@ ApplicationWindow {
Label {
width: licenseScroll.availableWidth
- color: textMain
+ color: BearTheme.textMain
font.pixelSize: 11
wrapMode: Text.WordWrap
text: "This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License along with this program. If not, see ."
@@ -1775,12 +1198,12 @@ ApplicationWindow {
background: Rectangle {
radius: 10
color: "#24364e"
- border.color: accent
+ border.color: BearTheme.accent
}
contentItem: Label {
id: toastLabel
- color: textMain
+ color: BearTheme.textMain
font.pixelSize: 12
}
diff --git a/src/qml/components/HeaderNavigation.qml b/src/qml/components/HeaderNavigation.qml
new file mode 100644
index 0000000..6d2dceb
--- /dev/null
+++ b/src/qml/components/HeaderNavigation.qml
@@ -0,0 +1,205 @@
+// Copyright (c) 2026 Sebastian Palencsar
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import theme 1.0
+
+Item {
+ id: root
+
+ required property var app
+ required property bool compactMode
+
+ implicitWidth: compactMode ? compactNav.implicitWidth : desktopNav.implicitWidth
+ implicitHeight: compactMode ? compactNav.implicitHeight : desktopNav.implicitHeight
+
+ function loadTop() {
+ if (!app.backend) return
+ app.currentPage = "top"
+ app.activeQuickFilter = ""
+ app.backend.loadTopStations()
+ }
+
+ function loadGerman() {
+ if (!app.backend) return
+ app.currentPage = "german"
+ app.activeQuickFilter = ""
+ app.backend.loadGermanStations()
+ }
+
+ function loadDutch() {
+ if (!app.backend) return
+ app.currentPage = "dutch"
+ app.activeQuickFilter = ""
+ app.backend.loadDutchStations()
+ }
+
+ function showFavorites() {
+ app.currentPage = "favorites"
+ app.activeQuickFilter = ""
+ }
+
+ function showHistory() {
+ app.currentPage = "history"
+ app.activeQuickFilter = ""
+ }
+
+ RowLayout {
+ id: desktopNav
+ anchors.left: parent.left
+ anchors.right: parent.right
+ visible: !root.compactMode
+ spacing: 8
+
+ Image {
+ Layout.preferredWidth: 112
+ Layout.preferredHeight: 40
+ source: "qrc:/assets/app/bearwave_line.png"
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ mipmap: true
+ }
+
+ Rectangle {
+ Layout.preferredWidth: 1
+ Layout.fillHeight: true
+ color: BearTheme.cardBorder
+ opacity: 0.6
+ }
+
+ Button {
+ text: qsTr("Top")
+ highlighted: app.currentPage === "top"
+ onClicked: loadTop()
+ }
+
+ Button {
+ text: qsTr("DE")
+ highlighted: app.currentPage === "german"
+ onClicked: loadGerman()
+ }
+
+ Button {
+ text: qsTr("NL")
+ highlighted: app.currentPage === "dutch"
+ onClicked: loadDutch()
+ }
+
+ Button {
+ text: qsTr("Favorites")
+ highlighted: app.currentPage === "favorites"
+ onClicked: showFavorites()
+ }
+
+ Button {
+ text: qsTr("History")
+ highlighted: app.currentPage === "history"
+ onClicked: showHistory()
+ }
+
+ Item { Layout.fillWidth: true }
+
+ Button {
+ text: qsTr("Manual +")
+ onClicked: app.addDialog.open()
+ }
+
+ Button {
+ text: qsTr("About")
+ onClicked: app.aboutDialog.open()
+ }
+
+ Button {
+ visible: app.backend && app.backend.canResumeLastStation
+ text: qsTr("Resume")
+ onClicked: {
+ if (app.backend) {
+ app.backend.resumeLastStation()
+ }
+ }
+ }
+ }
+
+ ColumnLayout {
+ id: compactNav
+ anchors.left: parent.left
+ anchors.right: parent.right
+ visible: root.compactMode
+ spacing: 8
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 8
+
+ Image {
+ Layout.preferredWidth: 96
+ Layout.preferredHeight: 34
+ source: "qrc:/assets/app/bearwave_line.png"
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ mipmap: true
+ }
+
+ Item { Layout.fillWidth: true }
+
+ Button {
+ text: "+"
+ onClicked: app.addDialog.open()
+ }
+
+ Button {
+ text: qsTr("About")
+ onClicked: app.aboutDialog.open()
+ }
+
+ Button {
+ visible: app.backend && app.backend.canResumeLastStation
+ text: "βΊ"
+ onClicked: {
+ if (app.backend) {
+ app.backend.resumeLastStation()
+ }
+ }
+ }
+ }
+
+ Flow {
+ Layout.fillWidth: true
+ width: parent.width
+ spacing: 8
+
+ Button {
+ text: qsTr("Top")
+ highlighted: app.currentPage === "top"
+ onClicked: loadTop()
+ }
+
+ Button {
+ text: qsTr("DE")
+ highlighted: app.currentPage === "german"
+ onClicked: loadGerman()
+ }
+
+ Button {
+ text: qsTr("NL")
+ highlighted: app.currentPage === "dutch"
+ onClicked: loadDutch()
+ }
+
+ Button {
+ text: qsTr("Favorites")
+ highlighted: app.currentPage === "favorites"
+ onClicked: showFavorites()
+ }
+
+ Button {
+ text: qsTr("History")
+ highlighted: app.currentPage === "history"
+ onClicked: showHistory()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/qml/components/QuickFilters.qml b/src/qml/components/QuickFilters.qml
new file mode 100644
index 0000000..8e58392
--- /dev/null
+++ b/src/qml/components/QuickFilters.qml
@@ -0,0 +1,190 @@
+// Copyright (c) 2026 Sebastian Palencsar
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import theme 1.0
+
+ColumnLayout {
+ id: root
+
+ required property var app
+ required property bool compactMode
+
+ spacing: 8
+
+ function loadTag(tag, page) {
+ if (!app.backend) return
+ app.currentPage = page
+ app.activeQuickFilter = "tag:" + tag
+ app.backend.loadByTag(tag)
+ }
+
+ function loadCountry(code, page) {
+ if (!app.backend) return
+ app.currentPage = page
+ app.activeQuickFilter = "cc:" + code
+ app.backend.loadByCountryCode(code)
+ }
+
+ function openWorld() {
+ if (!app.backend) return
+ app.currentPage = "world"
+ app.activeQuickFilter = "world"
+ app.selectedWorldCategory = ""
+ app.selectedWorldType = ""
+ if (app.backend.countries.length === 0) {
+ app.backend.loadCountries()
+ }
+ }
+
+ RowLayout {
+ visible: !root.compactMode
+ Layout.fillWidth: true
+ spacing: 6
+
+ Label {
+ text: qsTr("Genre:")
+ color: BearTheme.textMuted
+ font.pixelSize: 11
+ rightPadding: 10
+ }
+
+ Button {
+ text: qsTr("Rock")
+ highlighted: app.activeQuickFilter === "tag:rock"
+ onClicked: loadTag("rock", "genre-rock")
+ }
+
+ Button {
+ text: qsTr("News")
+ highlighted: app.activeQuickFilter === "tag:news"
+ onClicked: loadTag("news", "genre-news")
+ }
+
+ Button {
+ text: qsTr("Jazz")
+ highlighted: app.activeQuickFilter === "tag:jazz"
+ onClicked: loadTag("jazz", "genre-jazz")
+ }
+
+ Item { Layout.preferredWidth: 20 }
+
+ Label {
+ text: qsTr("Country:")
+ color: BearTheme.textMuted
+ font.pixelSize: 11
+ rightPadding: 10
+ }
+
+ Button {
+ text: qsTr("US")
+ highlighted: app.activeQuickFilter === "cc:US"
+ onClicked: loadCountry("US", "country-us")
+ }
+
+ Button {
+ text: qsTr("UK")
+ highlighted: app.activeQuickFilter === "cc:GB"
+ onClicked: loadCountry("GB", "country-gb")
+ }
+
+ Button {
+ text: qsTr("FR")
+ highlighted: app.activeQuickFilter === "cc:FR"
+ onClicked: loadCountry("FR", "country-fr")
+ }
+
+ Button {
+ text: qsTr("WORLD")
+ highlighted: app.activeQuickFilter === "world"
+ onClicked: openWorld()
+ }
+
+ Item { Layout.fillWidth: true }
+ }
+
+ ColumnLayout {
+ visible: root.compactMode
+ Layout.fillWidth: true
+ spacing: 8
+
+ Flow {
+ Layout.fillWidth: true
+ width: parent.width
+ spacing: 8
+
+ Label {
+ text: qsTr("Genre:")
+ color: BearTheme.textMuted
+ font.pixelSize: 11
+ verticalAlignment: Text.AlignVCenter
+ leftPadding: 8
+ rightPadding: 16
+ topPadding: 8
+ bottomPadding: 8
+ }
+
+ Button {
+ text: qsTr("Rock")
+ highlighted: app.activeQuickFilter === "tag:rock"
+ onClicked: loadTag("rock", "genre-rock")
+ }
+
+ Button {
+ text: qsTr("News")
+ highlighted: app.activeQuickFilter === "tag:news"
+ onClicked: loadTag("news", "genre-news")
+ }
+
+ Button {
+ text: qsTr("Jazz")
+ highlighted: app.activeQuickFilter === "tag:jazz"
+ onClicked: loadTag("jazz", "genre-jazz")
+ }
+ }
+
+ Flow {
+ Layout.fillWidth: true
+ width: parent.width
+ spacing: 8
+
+ Label {
+ text: qsTr("Country:")
+ color: BearTheme.textMuted
+ font.pixelSize: 11
+ verticalAlignment: Text.AlignVCenter
+ leftPadding: 8
+ rightPadding: 16
+ topPadding: 8
+ bottomPadding: 8
+ }
+
+ Button {
+ text: qsTr("US")
+ highlighted: app.activeQuickFilter === "cc:US"
+ onClicked: loadCountry("US", "country-us")
+ }
+
+ Button {
+ text: qsTr("UK")
+ highlighted: app.activeQuickFilter === "cc:GB"
+ onClicked: loadCountry("GB", "country-gb")
+ }
+
+ Button {
+ text: qsTr("FR")
+ highlighted: app.activeQuickFilter === "cc:FR"
+ onClicked: loadCountry("FR", "country-fr")
+ }
+
+ Button {
+ text: qsTr("WORLD")
+ highlighted: app.activeQuickFilter === "world"
+ onClicked: openWorld()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/qml/components/SearchToolbar.qml b/src/qml/components/SearchToolbar.qml
new file mode 100644
index 0000000..c55f1ab
--- /dev/null
+++ b/src/qml/components/SearchToolbar.qml
@@ -0,0 +1,166 @@
+// Copyright (c) 2026 Sebastian Palencsar
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import theme 1.0
+
+ColumnLayout {
+ id: root
+
+ required property var app
+ required property var searchTimer
+ required property bool compactMode
+
+ property alias searchField: searchField
+
+ spacing: 8
+
+ RowLayout {
+ visible: !root.compactMode
+ Layout.fillWidth: true
+ spacing: 8
+
+ TextField {
+ id: searchField
+ Layout.fillWidth: true
+ placeholderText: qsTr("Search stations (name, genre, country)")
+ onTextChanged: {
+ if (app.backend) {
+ app.backend.filterQuery = text
+ }
+ searchTimer.restart()
+ }
+ onAccepted: {
+ if (text.length < 2 || !app.backend) return
+ app.currentPage = "search"
+ app.backend.searchStations(text)
+ }
+ }
+
+ Button {
+ text: qsTr("Search")
+ highlighted: true
+ onClicked: {
+ if (searchField.text.length < 2 || !app.backend) return
+ app.currentPage = "search"
+ app.backend.searchStations(searchField.text)
+ }
+ }
+
+ Button {
+ text: qsTr("Sort A-Z")
+ onClicked: {
+ if (app.backend && app.currentPage !== "favorites") {
+ app.backend.sortStations("name")
+ }
+ }
+ }
+
+ Button {
+ text: qsTr("Sort Bitrate")
+ onClicked: {
+ if (app.backend && app.currentPage !== "favorites") {
+ app.backend.sortStations("bitrate")
+ }
+ }
+ }
+
+ Button {
+ text: qsTr("Sort Votes")
+ onClicked: {
+ if (app.backend && app.currentPage !== "favorites") {
+ app.backend.sortStations("votes")
+ }
+ }
+ }
+ }
+
+ ColumnLayout {
+ visible: root.compactMode
+ Layout.fillWidth: true
+ spacing: 8
+
+ TextField {
+ id: compactSearchField
+ Layout.fillWidth: true
+ placeholderText: qsTr("Search stations (name, genre, country)")
+ text: searchField.text
+ onTextChanged: {
+ if (searchField.text !== text) {
+ searchField.text = text
+ }
+ if (app.backend) {
+ app.backend.filterQuery = text
+ }
+ searchTimer.restart()
+ }
+ onAccepted: {
+ if (text.length < 2 || !app.backend) return
+ app.currentPage = "search"
+ app.backend.searchStations(text)
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 8
+
+ Item { Layout.fillWidth: true }
+
+ Button {
+ text: qsTr("Search")
+ highlighted: true
+ onClicked: {
+ if (compactSearchField.text.length < 2 || !app.backend) return
+ app.currentPage = "search"
+ app.backend.searchStations(compactSearchField.text)
+ }
+ }
+ }
+
+ Flow {
+ Layout.fillWidth: true
+ width: parent.width
+ spacing: 8
+
+ Button {
+ text: "A-Z"
+ onClicked: {
+ if (app.backend && app.currentPage !== "favorites") {
+ app.backend.sortStations("name")
+ }
+ }
+ }
+
+ Button {
+ text: "kb"
+ onClicked: {
+ if (app.backend && app.currentPage !== "favorites") {
+ app.backend.sortStations("bitrate")
+ }
+ }
+ }
+
+ Button {
+ text: "β€"
+ onClicked: {
+ if (app.backend && app.currentPage !== "favorites") {
+ app.backend.sortStations("votes")
+ }
+ }
+ }
+ }
+ }
+
+ Label {
+ Layout.fillWidth: true
+ text: qsTr("Tip: you are not limited to DE/NL. Search worldwide by country, genre, or station name.")
+ color: BearTheme.textMuted
+ font.pixelSize: 11
+ wrapMode: root.compactMode ? Text.WordWrap : Text.NoWrap
+ elide: root.compactMode ? Text.ElideNone : Text.ElideRight
+ }
+}
\ No newline at end of file
diff --git a/src/qml/components/qmldir b/src/qml/components/qmldir
new file mode 100644
index 0000000..905d1c1
--- /dev/null
+++ b/src/qml/components/qmldir
@@ -0,0 +1,3 @@
+HeaderNavigation 1.0 HeaderNavigation.qml
+SearchToolbar 1.0 SearchToolbar.qml
+QuickFilters 1.0 QuickFilters.qml
\ No newline at end of file
diff --git a/src/qml/theme/BearTheme.qml b/src/qml/theme/BearTheme.qml
new file mode 100644
index 0000000..2fe0fb4
--- /dev/null
+++ b/src/qml/theme/BearTheme.qml
@@ -0,0 +1,33 @@
+// Copyright (c) 2026 Sebastian Palencsar
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+pragma Singleton
+import QtQuick 2.15
+
+QtObject {
+ readonly property color bgA: "#0f141b"
+ readonly property color bgB: "#131b25"
+ readonly property color panel: "#182433"
+ readonly property color card: "#1b2a3d"
+ readonly property color cardHover: "#223654"
+ readonly property color cardBorder: "#2d4566"
+ readonly property color accent: "#2bb0ff"
+ readonly property color textMain: "#eaf1fb"
+ 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: "πΊ" }
+ ]
+}
\ No newline at end of file
diff --git a/src/qml/theme/qmldir b/src/qml/theme/qmldir
new file mode 100644
index 0000000..6d86625
--- /dev/null
+++ b/src/qml/theme/qmldir
@@ -0,0 +1 @@
+singleton BearTheme 1.0 BearTheme.qml
\ No newline at end of file
diff --git a/src/qml/utils/FlagUtils.js b/src/qml/utils/FlagUtils.js
new file mode 100644
index 0000000..f5360fa
--- /dev/null
+++ b/src/qml/utils/FlagUtils.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2026 Sebastian Palencsar
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+.pragma library
+
+function flagEmoji(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)
+}
\ No newline at end of file