mirror of
https://github.com/spalencsar/bearwave.git
synced 2026-07-06 22:24:17 +02:00
refactor(qml): extract station list, player, world, and about UI
Phase 2 splits the remaining large Main.qml blocks into StationCard, WorldCategories, WorldCategoryHeader, PlayerBar, and AboutDialog. Main.qml is now ~414 lines (down from ~1240); behavior unchanged.
This commit is contained in:
@@ -7,6 +7,11 @@
|
|||||||
<file>qml/components/HeaderNavigation.qml</file>
|
<file>qml/components/HeaderNavigation.qml</file>
|
||||||
<file>qml/components/SearchToolbar.qml</file>
|
<file>qml/components/SearchToolbar.qml</file>
|
||||||
<file>qml/components/QuickFilters.qml</file>
|
<file>qml/components/QuickFilters.qml</file>
|
||||||
|
<file>qml/components/StationCard.qml</file>
|
||||||
|
<file>qml/components/WorldCategoryHeader.qml</file>
|
||||||
|
<file>qml/components/WorldCategories.qml</file>
|
||||||
|
<file>qml/components/PlayerBar.qml</file>
|
||||||
|
<file>qml/components/AboutDialog.qml</file>
|
||||||
<file>qml/utils/FlagUtils.js</file>
|
<file>qml/utils/FlagUtils.js</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="/">
|
<qresource prefix="/">
|
||||||
|
|||||||
851
src/qml/Main.qml
851
src/qml/Main.qml
@@ -7,7 +7,6 @@ import QtQuick.Layouts 1.15
|
|||||||
|
|
||||||
import theme 1.0
|
import theme 1.0
|
||||||
import components 1.0
|
import components 1.0
|
||||||
import "utils/FlagUtils.js" as FlagUtils
|
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
id: root
|
id: root
|
||||||
@@ -187,30 +186,10 @@ ApplicationWindow {
|
|||||||
anchors.margins: 10
|
anchors.margins: 10
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
RowLayout {
|
WorldCategoryHeader {
|
||||||
id: worldHeader
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
visible: currentPage === "world" && selectedWorldCategory !== ""
|
visible: currentPage === "world" && selectedWorldCategory !== ""
|
||||||
spacing: 12
|
app: root
|
||||||
|
|
||||||
Button {
|
|
||||||
text: qsTr("← Back to Categories")
|
|
||||||
onClicked: {
|
|
||||||
selectedWorldCategory = ""
|
|
||||||
selectedWorldType = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
text: selectedWorldType === "country"
|
|
||||||
? (qsTr("World > Country: ") + selectedWorldCategory)
|
|
||||||
: (qsTr("World > Genre: ") + selectedWorldCategory)
|
|
||||||
color: BearTheme.textMain
|
|
||||||
font.bold: true
|
|
||||||
font.pixelSize: 13
|
|
||||||
Layout.fillWidth: true
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@@ -228,435 +207,20 @@ ApplicationWindow {
|
|||||||
model: activeModel()
|
model: activeModel()
|
||||||
visible: count > 0
|
visible: count > 0
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: StationCard {
|
||||||
id: stationCard
|
app: root
|
||||||
required property int index
|
compactMode: root.compactMode
|
||||||
required property var modelData
|
listWidth: stationScrollView.availableWidth
|
||||||
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;
|
|
||||||
}
|
|
||||||
readonly property bool isPlaying: isCurrent && backend && backend.player && backend.player.playing
|
|
||||||
|
|
||||||
color: stationCard.isCurrent
|
|
||||||
? (cardMouse.containsMouse ? "#1d3350" : "#16283e")
|
|
||||||
: (cardMouse.containsMouse ? BearTheme.cardHover : BearTheme.card)
|
|
||||||
border.color: stationCard.isCurrent ? BearTheme.accent : BearTheme.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 {
|
WorldCategories {
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 10
|
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.preferredWidth: 44
|
|
||||||
Layout.preferredHeight: 44
|
|
||||||
radius: 8
|
|
||||||
color: "#123154"
|
|
||||||
border.color: BearTheme.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 ? BearTheme.accent : BearTheme.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: BearTheme.accent
|
|
||||||
radius: 1
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation { duration: 120 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Rectangle {
|
|
||||||
id: bar2
|
|
||||||
width: 2
|
|
||||||
height: 12
|
|
||||||
color: BearTheme.accent
|
|
||||||
radius: 1
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation { duration: 120 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Rectangle {
|
|
||||||
id: bar3
|
|
||||||
width: 2
|
|
||||||
height: 12
|
|
||||||
color: BearTheme.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: BearTheme.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.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
visible: currentPage === "world" && selectedWorldCategory === ""
|
visible: currentPage === "world" && selectedWorldCategory === ""
|
||||||
clip: true
|
app: root
|
||||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
compactMode: root.compactMode
|
||||||
|
|
||||||
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: BearTheme.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: BearTheme.textMuted
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
visible: countrySearchText === ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: BearTheme.cardBorder
|
|
||||||
opacity: 0.4
|
|
||||||
visible: countrySearchText === ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
text: countrySearchText === "" ? qsTr("Countries") : qsTr("Filtered Countries")
|
|
||||||
font.bold: true
|
|
||||||
font.pixelSize: 13
|
|
||||||
color: BearTheme.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 ? BearTheme.cardHover : BearTheme.card
|
|
||||||
border.color: countryMouse.containsMouse ? BearTheme.accent : BearTheme.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" ? "🌎" : FlagUtils.flagEmoji(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: BearTheme.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: BearTheme.textMuted
|
|
||||||
font.pixelSize: 10
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: 16
|
|
||||||
visible: countrySearchText === ""
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: BearTheme.cardBorder
|
|
||||||
opacity: 0.4
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
text: qsTr("Popular Music Styles")
|
|
||||||
font.bold: true
|
|
||||||
font.pixelSize: 13
|
|
||||||
color: BearTheme.accent
|
|
||||||
}
|
|
||||||
|
|
||||||
Flow {
|
|
||||||
id: tagFlow
|
|
||||||
width: parent.width
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: BearTheme.worldTags
|
|
||||||
delegate: Rectangle {
|
|
||||||
id: tagCard
|
|
||||||
width: compactMode ? (tagFlow.width - 8) / 2 : (tagFlow.width - 16) / 3
|
|
||||||
height: 54
|
|
||||||
radius: 8
|
|
||||||
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
|
|
||||||
|
|
||||||
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: BearTheme.textMain
|
|
||||||
font.bold: true
|
|
||||||
font.pixelSize: 13
|
|
||||||
elide: Text.ElideRight
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,203 +242,9 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
PlayerBar {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 108
|
app: root
|
||||||
radius: 12
|
|
||||||
color: BearTheme.panel
|
|
||||||
border.color: BearTheme.cardBorder
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 10
|
|
||||||
spacing: 12
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.preferredWidth: 88
|
|
||||||
Layout.preferredHeight: 88
|
|
||||||
radius: 8
|
|
||||||
color: "#182637"
|
|
||||||
clip: true
|
|
||||||
border.color: BearTheme.cardBorder
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: coverImage
|
|
||||||
anchors.fill: parent
|
|
||||||
source: {
|
|
||||||
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
|
|
||||||
asynchronous: true
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Label {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: backend && backend.player ? (backend.player.currentStationName || qsTr("No station selected")) : qsTr("No station selected")
|
|
||||||
color: BearTheme.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: BearTheme.accent
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Label {
|
|
||||||
id: codecLabel
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: (backend && backend.currentStation && backend.currentStation.codec) ? backend.currentStation.codec.toUpperCase() : ""
|
|
||||||
color: BearTheme.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: BearTheme.accent
|
|
||||||
border.color: BearTheme.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 {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: backend && backend.player && backend.player.currentNowPlaying.length > 0
|
|
||||||
? (qsTr("Now playing: ") + backend.player.currentNowPlaying)
|
|
||||||
: qsTr("Now playing: No track info")
|
|
||||||
color: BearTheme.textMuted
|
|
||||||
font.pixelSize: 12
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Button {
|
|
||||||
text: "⏮"
|
|
||||||
Layout.preferredWidth: 44
|
|
||||||
enabled: backend ? backend.hasPreviousStation() : false
|
|
||||||
onClicked: {
|
|
||||||
if (backend) {
|
|
||||||
backend.playPreviousStation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
text: backend && backend.player && backend.player.playing ? "⏸" : "▶"
|
|
||||||
Layout.preferredWidth: 44
|
|
||||||
onClicked: {
|
|
||||||
if (backend && backend.player) {
|
|
||||||
backend.player.togglePlayPause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
text: "⏹"
|
|
||||||
Layout.preferredWidth: 44
|
|
||||||
onClicked: {
|
|
||||||
if (backend && backend.player) {
|
|
||||||
backend.player.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
text: "⏭"
|
|
||||||
Layout.preferredWidth: 44
|
|
||||||
enabled: backend ? backend.hasNextStation() : false
|
|
||||||
onClicked: {
|
|
||||||
if (backend) {
|
|
||||||
backend.playNextStation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
id: muteButton
|
|
||||||
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 {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
from: 0
|
|
||||||
to: 1
|
|
||||||
value: backend && backend.player ? backend.player.volume : 0.5
|
|
||||||
onMoved: {
|
|
||||||
if (backend && backend.player) {
|
|
||||||
backend.player.setVolume(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -989,204 +359,9 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog {
|
AboutDialog {
|
||||||
id: aboutDialog
|
id: aboutDialog
|
||||||
modal: true
|
compactMode: root.compactMode
|
||||||
anchors.centerIn: Overlay.overlay
|
|
||||||
width: compactMode ? 360 : 520
|
|
||||||
height: compactMode ? 560 : 680
|
|
||||||
standardButtons: Dialog.Ok
|
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
|
||||||
spacing: 16
|
|
||||||
|
|
||||||
// --- HEADER ---
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
Image {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
Layout.preferredWidth: 72
|
|
||||||
Layout.preferredHeight: 72
|
|
||||||
sourceSize.width: 72
|
|
||||||
sourceSize.height: 72
|
|
||||||
source: "qrc:/assets/app/bearwave.png"
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
smooth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
text: qsTr("BearWave")
|
|
||||||
color: BearTheme.textMain
|
|
||||||
font.bold: true
|
|
||||||
font.pixelSize: 26
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
text: qsTr("Internet Radio Player for KDE")
|
|
||||||
color: BearTheme.textMuted
|
|
||||||
font.pixelSize: 14
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
text: qsTr("Version: %1").arg(Qt.application.version)
|
|
||||||
color: BearTheme.textMuted
|
|
||||||
font.pixelSize: 12
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
text: qsTr("Public beta")
|
|
||||||
color: BearTheme.accent
|
|
||||||
font.pixelSize: 12
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- CREDITS & LINKS ---
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
spacing: 8
|
|
||||||
Layout.topMargin: 8
|
|
||||||
Layout.bottomMargin: 8
|
|
||||||
|
|
||||||
Label {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
text: qsTr("Author: Sebastian Palencsár")
|
|
||||||
color: BearTheme.textMain
|
|
||||||
font.bold: true
|
|
||||||
font.pixelSize: 14
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
spacing: 16
|
|
||||||
|
|
||||||
ToolButton {
|
|
||||||
Layout.preferredWidth: 40
|
|
||||||
Layout.preferredHeight: 40
|
|
||||||
onClicked: Qt.openUrlExternally("https://palencsar.pro")
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.text: qsTr("Open website")
|
|
||||||
contentItem: Item {
|
|
||||||
Image {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 19
|
|
||||||
height: 19
|
|
||||||
source: "qrc:/assets/ui/globe.svg"
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
smooth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
background: Rectangle {
|
|
||||||
radius: 20
|
|
||||||
color: parent.hovered ? "#274261" : "#1f3147"
|
|
||||||
border.color: BearTheme.cardBorder
|
|
||||||
border.width: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolButton {
|
|
||||||
Layout.preferredWidth: 40
|
|
||||||
Layout.preferredHeight: 40
|
|
||||||
onClicked: Qt.openUrlExternally("https://github.com/spalencsar")
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.text: qsTr("Open GitHub")
|
|
||||||
contentItem: Item {
|
|
||||||
Image {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 19
|
|
||||||
height: 19
|
|
||||||
source: "qrc:/assets/ui/github.svg"
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
smooth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
background: Rectangle {
|
|
||||||
radius: 20
|
|
||||||
color: parent.hovered ? "#274261" : "#1f3147"
|
|
||||||
border.color: BearTheme.cardBorder
|
|
||||||
border.width: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolButton {
|
|
||||||
Layout.preferredWidth: 40
|
|
||||||
Layout.preferredHeight: 40
|
|
||||||
onClicked: Qt.openUrlExternally("https://www.linkedin.com/in/spalencsar/")
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.text: qsTr("Open LinkedIn")
|
|
||||||
contentItem: Item {
|
|
||||||
Image {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 19
|
|
||||||
height: 19
|
|
||||||
source: "qrc:/assets/ui/linkedin.svg"
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
smooth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
background: Rectangle {
|
|
||||||
radius: 20
|
|
||||||
color: parent.hovered ? "#274261" : "#1f3147"
|
|
||||||
border.color: BearTheme.cardBorder
|
|
||||||
border.width: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- LICENSE ---
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Label {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: qsTr("GNU GPLv3 License")
|
|
||||||
color: BearTheme.textMain
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
text: qsTr("Copyright (c) 2026")
|
|
||||||
color: BearTheme.textMuted
|
|
||||||
font.pixelSize: 11
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.minimumHeight: 140
|
|
||||||
radius: 8
|
|
||||||
color: "#101a26"
|
|
||||||
border.color: BearTheme.cardBorder
|
|
||||||
|
|
||||||
ScrollView {
|
|
||||||
id: licenseScroll
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 10
|
|
||||||
clip: true
|
|
||||||
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
|
|
||||||
|
|
||||||
Label {
|
|
||||||
width: licenseScroll.availableWidth
|
|
||||||
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 <https://www.gnu.org/licenses/>."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Popup {
|
Popup {
|
||||||
|
|||||||
208
src/qml/components/AboutDialog.qml
Normal file
208
src/qml/components/AboutDialog.qml
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
Dialog {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property bool compactMode
|
||||||
|
|
||||||
|
modal: true
|
||||||
|
anchors.centerIn: Overlay.overlay
|
||||||
|
width: compactMode ? 360 : 520
|
||||||
|
height: compactMode ? 560 : 680
|
||||||
|
standardButtons: Dialog.Ok
|
||||||
|
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Image {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.preferredWidth: 72
|
||||||
|
Layout.preferredHeight: 72
|
||||||
|
sourceSize.width: 72
|
||||||
|
sourceSize.height: 72
|
||||||
|
source: "qrc:/assets/app/bearwave.png"
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: qsTr("BearWave")
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 26
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: qsTr("Internet Radio Player for KDE")
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.pixelSize: 14
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: qsTr("Version: %1").arg(Qt.application.version)
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: qsTr("Public beta")
|
||||||
|
color: BearTheme.accent
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
spacing: 8
|
||||||
|
Layout.topMargin: 8
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: qsTr("Author: Sebastian Palencsár")
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 14
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
ToolButton {
|
||||||
|
Layout.preferredWidth: 40
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
onClicked: Qt.openUrlExternally("https://palencsar.pro")
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Open website")
|
||||||
|
contentItem: Item {
|
||||||
|
Image {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 19
|
||||||
|
height: 19
|
||||||
|
source: "qrc:/assets/ui/globe.svg"
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
background: Rectangle {
|
||||||
|
radius: 20
|
||||||
|
color: parent.hovered ? "#274261" : "#1f3147"
|
||||||
|
border.color: BearTheme.cardBorder
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolButton {
|
||||||
|
Layout.preferredWidth: 40
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
onClicked: Qt.openUrlExternally("https://github.com/spalencsar")
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Open GitHub")
|
||||||
|
contentItem: Item {
|
||||||
|
Image {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 19
|
||||||
|
height: 19
|
||||||
|
source: "qrc:/assets/ui/github.svg"
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
background: Rectangle {
|
||||||
|
radius: 20
|
||||||
|
color: parent.hovered ? "#274261" : "#1f3147"
|
||||||
|
border.color: BearTheme.cardBorder
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolButton {
|
||||||
|
Layout.preferredWidth: 40
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
onClicked: Qt.openUrlExternally("https://www.linkedin.com/in/spalencsar/")
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Open LinkedIn")
|
||||||
|
contentItem: Item {
|
||||||
|
Image {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 19
|
||||||
|
height: 19
|
||||||
|
source: "qrc:/assets/ui/linkedin.svg"
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
background: Rectangle {
|
||||||
|
radius: 20
|
||||||
|
color: parent.hovered ? "#274261" : "#1f3147"
|
||||||
|
border.color: BearTheme.cardBorder
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("GNU GPLv3 License")
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: qsTr("Copyright (c) 2026")
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.pixelSize: 11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.minimumHeight: 140
|
||||||
|
radius: 8
|
||||||
|
color: "#101a26"
|
||||||
|
border.color: BearTheme.cardBorder
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: licenseScroll
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 10
|
||||||
|
clip: true
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
|
||||||
|
|
||||||
|
Label {
|
||||||
|
width: licenseScroll.availableWidth
|
||||||
|
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 <https://www.gnu.org/licenses/>."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
221
src/qml/components/PlayerBar.qml
Normal file
221
src/qml/components/PlayerBar.qml
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var app
|
||||||
|
|
||||||
|
implicitHeight: 108
|
||||||
|
radius: 12
|
||||||
|
color: BearTheme.panel
|
||||||
|
border.color: BearTheme.cardBorder
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 10
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 88
|
||||||
|
Layout.preferredHeight: 88
|
||||||
|
radius: 8
|
||||||
|
color: "#182637"
|
||||||
|
clip: true
|
||||||
|
border.color: BearTheme.cardBorder
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: coverImage
|
||||||
|
anchors.fill: parent
|
||||||
|
source: {
|
||||||
|
if (app.backend && app.backend.player && app.backend.player.currentCoverArtUrl
|
||||||
|
&& app.backend.player.currentCoverArtUrl !== "") {
|
||||||
|
return app.backend.player.currentCoverArtUrl
|
||||||
|
}
|
||||||
|
if (app.backend && app.backend.currentStation && app.backend.currentStation.favicon
|
||||||
|
&& app.backend.currentStation.favicon.startsWith("https://")) {
|
||||||
|
return app.backend.currentStation.favicon
|
||||||
|
}
|
||||||
|
return "qrc:/assets/app/bearwave.png"
|
||||||
|
}
|
||||||
|
fillMode: (app.backend && app.backend.player && app.backend.player.currentCoverArtUrl
|
||||||
|
&& app.backend.player.currentCoverArtUrl !== "")
|
||||||
|
? Image.PreserveAspectCrop : Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
anchors.margins: {
|
||||||
|
if (app.backend && app.backend.player && app.backend.player.currentCoverArtUrl
|
||||||
|
&& app.backend.player.currentCoverArtUrl !== "") {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (app.backend && app.backend.currentStation && app.backend.currentStation.favicon
|
||||||
|
&& app.backend.currentStation.favicon.startsWith("https://")) {
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
return 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: app.backend && app.backend.player
|
||||||
|
? (app.backend.player.currentStationName || qsTr("No station selected"))
|
||||||
|
: qsTr("No station selected")
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.pixelSize: 16
|
||||||
|
font.bold: true
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: app.backend && app.backend.currentStation && app.backend.currentStation.codec
|
||||||
|
&& app.backend.currentStation.codec !== "unknown"
|
||||||
|
&& app.backend.currentStation.codec !== ""
|
||||||
|
height: 18
|
||||||
|
width: codecLabel.implicitWidth + 12
|
||||||
|
radius: 4
|
||||||
|
color: "transparent"
|
||||||
|
border.color: BearTheme.accent
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: codecLabel
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (app.backend && app.backend.currentStation && app.backend.currentStation.codec)
|
||||||
|
? app.backend.currentStation.codec.toUpperCase() : ""
|
||||||
|
color: BearTheme.accent
|
||||||
|
font.pixelSize: 9
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: app.backend && app.backend.currentStation && app.backend.currentStation.bitrate > 0
|
||||||
|
height: 18
|
||||||
|
width: bitrateLabel.implicitWidth + 12
|
||||||
|
radius: 4
|
||||||
|
color: BearTheme.accent
|
||||||
|
border.color: BearTheme.accent
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: bitrateLabel
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (app.backend && app.backend.currentStation)
|
||||||
|
? app.backend.currentStation.bitrate + " kbps" : ""
|
||||||
|
color: "#ffffff"
|
||||||
|
font.pixelSize: 9
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: app.backend && app.backend.player && app.backend.player.currentNowPlaying.length > 0
|
||||||
|
? (qsTr("Now playing: ") + app.backend.player.currentNowPlaying)
|
||||||
|
: qsTr("Now playing: No track info")
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.pixelSize: 12
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "⏮"
|
||||||
|
Layout.preferredWidth: 44
|
||||||
|
enabled: app.backend ? app.backend.hasPreviousStation() : false
|
||||||
|
onClicked: {
|
||||||
|
if (app.backend) {
|
||||||
|
app.backend.playPreviousStation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: app.backend && app.backend.player && app.backend.player.playing ? "⏸" : "▶"
|
||||||
|
Layout.preferredWidth: 44
|
||||||
|
onClicked: {
|
||||||
|
if (app.backend && app.backend.player) {
|
||||||
|
app.backend.player.togglePlayPause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "⏹"
|
||||||
|
Layout.preferredWidth: 44
|
||||||
|
onClicked: {
|
||||||
|
if (app.backend && app.backend.player) {
|
||||||
|
app.backend.player.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "⏭"
|
||||||
|
Layout.preferredWidth: 44
|
||||||
|
enabled: app.backend ? app.backend.hasNextStation() : false
|
||||||
|
onClicked: {
|
||||||
|
if (app.backend) {
|
||||||
|
app.backend.playNextStation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
id: muteButton
|
||||||
|
text: (app.backend && app.backend.player && app.backend.player.volume > 0) ? "🔊" : "🔇"
|
||||||
|
Layout.preferredWidth: 44
|
||||||
|
property real lastVolume: 0.5
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: (app.backend && app.backend.player && app.backend.player.volume > 0)
|
||||||
|
? qsTr("Mute") : qsTr("Unmute")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (app.backend && app.backend.player) {
|
||||||
|
if (app.backend.player.volume > 0) {
|
||||||
|
lastVolume = app.backend.player.volume
|
||||||
|
app.backend.player.setVolume(0)
|
||||||
|
} else {
|
||||||
|
app.backend.player.setVolume(lastVolume > 0 ? lastVolume : 0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
value: app.backend && app.backend.player ? app.backend.player.volume : 0.5
|
||||||
|
onMoved: {
|
||||||
|
if (app.backend && app.backend.player) {
|
||||||
|
app.backend.player.setVolume(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
228
src/qml/components/StationCard.qml
Normal file
228
src/qml/components/StationCard.qml
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property int index
|
||||||
|
required property var modelData
|
||||||
|
required property var app
|
||||||
|
required property bool compactMode
|
||||||
|
required property real listWidth
|
||||||
|
|
||||||
|
height: compactMode ? 72 : 78
|
||||||
|
width: listWidth
|
||||||
|
radius: 10
|
||||||
|
|
||||||
|
readonly property bool isCurrent: {
|
||||||
|
if (!app.backend || !modelData) return false
|
||||||
|
var currentUuid = app.backend.currentStationUuid
|
||||||
|
var currentUrl = app.backend.currentStationUrl
|
||||||
|
var cardUuid = modelData.uuid || ""
|
||||||
|
var cardUrl = modelData.urlResolved || modelData.url || ""
|
||||||
|
if (currentUuid !== "" && cardUuid !== "") {
|
||||||
|
return currentUuid === cardUuid
|
||||||
|
}
|
||||||
|
return currentUrl !== "" && currentUrl === cardUrl
|
||||||
|
}
|
||||||
|
readonly property bool isPlaying: isCurrent && app.backend && app.backend.player && app.backend.player.playing
|
||||||
|
|
||||||
|
color: isCurrent
|
||||||
|
? (cardMouse.containsMouse ? "#1d3350" : "#16283e")
|
||||||
|
: (cardMouse.containsMouse ? BearTheme.cardHover : BearTheme.card)
|
||||||
|
border.color: isCurrent ? BearTheme.accent : BearTheme.cardBorder
|
||||||
|
border.width: isCurrent ? 2 : 1
|
||||||
|
|
||||||
|
function playStation() {
|
||||||
|
if (!app.backend) return
|
||||||
|
if (app.currentPage === "favorites") {
|
||||||
|
app.backend.playFavoriteStation(index)
|
||||||
|
} else if (app.currentPage === "history") {
|
||||||
|
app.backend.playRecentByUuid(modelData.uuid, modelData.urlResolved)
|
||||||
|
} else {
|
||||||
|
app.backend.playStation(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: cardMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
onClicked: {
|
||||||
|
if (!app.backend) return
|
||||||
|
if (root.isCurrent) {
|
||||||
|
app.backend.player.togglePlayPause()
|
||||||
|
} else {
|
||||||
|
playStation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 10
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 44
|
||||||
|
Layout.preferredHeight: 44
|
||||||
|
radius: 8
|
||||||
|
color: "#123154"
|
||||||
|
border.color: BearTheme.accent
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: stationFavicon
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 3
|
||||||
|
source: (root.modelData.favicon && root.modelData.favicon.startsWith("https://"))
|
||||||
|
? root.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: root.modelData.name
|
||||||
|
color: root.isCurrent ? BearTheme.accent : BearTheme.textMain
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: compactMode ? 13 : 14
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: eqAnimation
|
||||||
|
spacing: 2
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
visible: root.isPlaying
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: bar1
|
||||||
|
width: 2
|
||||||
|
height: 12
|
||||||
|
color: BearTheme.accent
|
||||||
|
radius: 1
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation { duration: 120 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
id: bar2
|
||||||
|
width: 2
|
||||||
|
height: 12
|
||||||
|
color: BearTheme.accent
|
||||||
|
radius: 1
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation { duration: 120 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
id: bar3
|
||||||
|
width: 2
|
||||||
|
height: 12
|
||||||
|
color: BearTheme.accent
|
||||||
|
radius: 1
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation { duration: 120 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 150
|
||||||
|
running: root.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: root.modelData.country + " • "
|
||||||
|
+ (root.modelData.codec && root.modelData.codec !== "unknown" && root.modelData.codec !== ""
|
||||||
|
? root.modelData.codec.toUpperCase() + " • " : "")
|
||||||
|
+ (root.modelData.bitrate > 0 ? root.modelData.bitrate + " kbps" : qsTr("Stream"))
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.pixelSize: 11
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
visible: !root.modelData.uuid
|
||||||
|
Layout.preferredWidth: compactMode ? 34 : 40
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
text: "✏"
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Edit Station")
|
||||||
|
onClicked: {
|
||||||
|
app.editDialog.stationObject = root.modelData
|
||||||
|
app.editDialog.setupAndOpen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
Layout.preferredWidth: compactMode ? 34 : 40
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
text: root.modelData.isFavorite ? "★" : "☆"
|
||||||
|
onClicked: {
|
||||||
|
if (!app.backend) return
|
||||||
|
app.backend.toggleFavoriteById(root.modelData.uuid, root.modelData.urlResolved)
|
||||||
|
app.toast(root.modelData.isFavorite ? qsTr("Removed from favorites") : qsTr("Added to favorites"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
Layout.preferredWidth: compactMode ? 34 : 40
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
text: (root.isCurrent && app.backend && app.backend.player && app.backend.player.playing) ? "⏸" : "▶"
|
||||||
|
onClicked: {
|
||||||
|
if (!app.backend) return
|
||||||
|
if (root.isCurrent) {
|
||||||
|
app.backend.player.togglePlayPause()
|
||||||
|
} else {
|
||||||
|
playStation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
225
src/qml/components/WorldCategories.qml
Normal file
225
src/qml/components/WorldCategories.qml
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
// 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
|
||||||
|
import "../utils/FlagUtils.js" as FlagUtils
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var app
|
||||||
|
required property bool compactMode
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: root.availableWidth - 12
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Explore World Stations")
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 16
|
||||||
|
color: BearTheme.textMain
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
placeholderText: qsTr("Filter countries...")
|
||||||
|
font.pixelSize: 11
|
||||||
|
Layout.preferredWidth: compactMode ? 140 : 200
|
||||||
|
text: app.countrySearchText
|
||||||
|
onTextChanged: app.countrySearchText = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Choose a country or music style to find radio stations from all over the world.")
|
||||||
|
font.pixelSize: 11
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: parent.width
|
||||||
|
visible: app.countrySearchText === ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: BearTheme.cardBorder
|
||||||
|
opacity: 0.4
|
||||||
|
visible: app.countrySearchText === ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: app.countrySearchText === "" ? qsTr("Countries") : qsTr("Filtered Countries")
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 13
|
||||||
|
color: BearTheme.accent
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
id: countryFlow
|
||||||
|
width: parent.width
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: app.getFilteredCountries()
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: countryCard
|
||||||
|
required property var modelData
|
||||||
|
width: compactMode ? (countryFlow.width - 8) / 2 : (countryFlow.width - 16) / 3
|
||||||
|
height: 62
|
||||||
|
radius: 8
|
||||||
|
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
|
||||||
|
|
||||||
|
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: {
|
||||||
|
app.selectedWorldCategory = modelData.name
|
||||||
|
app.selectedWorldType = "country"
|
||||||
|
if (modelData.code === "GLOBAL") {
|
||||||
|
app.backend.loadWorldStations()
|
||||||
|
} else {
|
||||||
|
app.backend.loadByCountryCode(modelData.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: modelData.code === "GLOBAL" ? "🌎" : FlagUtils.flagEmoji(modelData.code)
|
||||||
|
font.pixelSize: 26
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - 38
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Label {
|
||||||
|
width: parent.width
|
||||||
|
text: modelData.name
|
||||||
|
color: BearTheme.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: BearTheme.textMuted
|
||||||
|
font.pixelSize: 10
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 16
|
||||||
|
visible: app.countrySearchText === ""
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: BearTheme.cardBorder
|
||||||
|
opacity: 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Popular Music Styles")
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 13
|
||||||
|
color: BearTheme.accent
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
id: tagFlow
|
||||||
|
width: parent.width
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: BearTheme.worldTags
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: tagCard
|
||||||
|
required property var modelData
|
||||||
|
width: compactMode ? (tagFlow.width - 8) / 2 : (tagFlow.width - 16) / 3
|
||||||
|
height: 54
|
||||||
|
radius: 8
|
||||||
|
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
|
||||||
|
|
||||||
|
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: {
|
||||||
|
app.selectedWorldCategory = modelData.name
|
||||||
|
app.selectedWorldType = "tag"
|
||||||
|
app.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
|
||||||
|
text: modelData.name
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 13
|
||||||
|
elide: Text.ElideRight
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/qml/components/WorldCategoryHeader.qml
Normal file
35
src/qml/components/WorldCategoryHeader.qml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var app
|
||||||
|
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: qsTr("← Back to Categories")
|
||||||
|
onClicked: {
|
||||||
|
app.selectedWorldCategory = ""
|
||||||
|
app.selectedWorldType = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: app.selectedWorldType === "country"
|
||||||
|
? (qsTr("World > Country: ") + app.selectedWorldCategory)
|
||||||
|
: (qsTr("World > Genre: ") + app.selectedWorldCategory)
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 13
|
||||||
|
Layout.fillWidth: true
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
HeaderNavigation 1.0 HeaderNavigation.qml
|
HeaderNavigation 1.0 HeaderNavigation.qml
|
||||||
SearchToolbar 1.0 SearchToolbar.qml
|
SearchToolbar 1.0 SearchToolbar.qml
|
||||||
QuickFilters 1.0 QuickFilters.qml
|
QuickFilters 1.0 QuickFilters.qml
|
||||||
|
StationCard 1.0 StationCard.qml
|
||||||
|
WorldCategoryHeader 1.0 WorldCategoryHeader.qml
|
||||||
|
WorldCategories 1.0 WorldCategories.qml
|
||||||
|
PlayerBar 1.0 PlayerBar.qml
|
||||||
|
AboutDialog 1.0 AboutDialog.qml
|
||||||
Reference in New Issue
Block a user