mirror of
https://github.com/spalencsar/bearwave.git
synced 2026-07-06 14:24:14 +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/SearchToolbar.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>
|
||||
</qresource>
|
||||
<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 components 1.0
|
||||
import "utils/FlagUtils.js" as FlagUtils
|
||||
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
@@ -187,30 +186,10 @@ ApplicationWindow {
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
|
||||
RowLayout {
|
||||
id: worldHeader
|
||||
WorldCategoryHeader {
|
||||
Layout.fillWidth: true
|
||||
visible: currentPage === "world" && selectedWorldCategory !== ""
|
||||
spacing: 12
|
||||
|
||||
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
|
||||
}
|
||||
app: root
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
@@ -228,435 +207,20 @@ ApplicationWindow {
|
||||
model: activeModel()
|
||||
visible: count > 0
|
||||
|
||||
delegate: Rectangle {
|
||||
id: stationCard
|
||||
required property int index
|
||||
required property var modelData
|
||||
width: stationScrollView.availableWidth
|
||||
height: compactMode ? 72 : 78
|
||||
radius: 10
|
||||
|
||||
readonly property bool isCurrent: {
|
||||
if (!backend || !modelData) return false;
|
||||
var currentUuid = backend.currentStationUuid;
|
||||
var currentUrl = backend.currentStationUrl;
|
||||
var cardUuid = modelData.uuid || "";
|
||||
var cardUrl = modelData.urlResolved || modelData.url || "";
|
||||
if (currentUuid !== "" && cardUuid !== "") {
|
||||
return currentUuid === cardUuid;
|
||||
}
|
||||
return currentUrl !== "" && currentUrl === cardUrl;
|
||||
}
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate: StationCard {
|
||||
app: root
|
||||
compactMode: root.compactMode
|
||||
listWidth: stationScrollView.availableWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: categoriesScrollView
|
||||
WorldCategories {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: currentPage === "world" && selectedWorldCategory === ""
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
|
||||
Column {
|
||||
width: categoriesScrollView.availableWidth - 12
|
||||
spacing: 16
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
spacing: 10
|
||||
|
||||
Label {
|
||||
text: qsTr("Explore World Stations")
|
||||
font.bold: true
|
||||
font.pixelSize: 16
|
||||
color: 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
app: root
|
||||
compactMode: root.compactMode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,203 +242,9 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
PlayerBar {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 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 (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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
app: root
|
||||
}
|
||||
}
|
||||
|
||||
@@ -989,204 +359,9 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
AboutDialog {
|
||||
id: aboutDialog
|
||||
modal: true
|
||||
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/>."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
compactMode: root.compactMode
|
||||
}
|
||||
|
||||
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
|
||||
SearchToolbar 1.0 SearchToolbar.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