mirror of
https://github.com/spalencsar/bearwave.git
synced 2026-07-06 22:24:17 +02:00
refactor(qml): extract header, search, and theme from Main.qml
Split the duplicated desktop/compact header UI into reusable QML components (HeaderNavigation, SearchToolbar, QuickFilters), centralize colors and world tags in BearTheme singleton, and move flag emoji logic to FlagUtils.js. Register modules via qrc import path qrc:/qml. Main.qml shrinks from ~1816 to ~1240 lines; behavior unchanged.
This commit is contained in:
205
src/qml/components/HeaderNavigation.qml
Normal file
205
src/qml/components/HeaderNavigation.qml
Normal file
@@ -0,0 +1,205 @@
|
||||
// Copyright (c) 2026 Sebastian Palencsar
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import theme 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var app
|
||||
required property bool compactMode
|
||||
|
||||
implicitWidth: compactMode ? compactNav.implicitWidth : desktopNav.implicitWidth
|
||||
implicitHeight: compactMode ? compactNav.implicitHeight : desktopNav.implicitHeight
|
||||
|
||||
function loadTop() {
|
||||
if (!app.backend) return
|
||||
app.currentPage = "top"
|
||||
app.activeQuickFilter = ""
|
||||
app.backend.loadTopStations()
|
||||
}
|
||||
|
||||
function loadGerman() {
|
||||
if (!app.backend) return
|
||||
app.currentPage = "german"
|
||||
app.activeQuickFilter = ""
|
||||
app.backend.loadGermanStations()
|
||||
}
|
||||
|
||||
function loadDutch() {
|
||||
if (!app.backend) return
|
||||
app.currentPage = "dutch"
|
||||
app.activeQuickFilter = ""
|
||||
app.backend.loadDutchStations()
|
||||
}
|
||||
|
||||
function showFavorites() {
|
||||
app.currentPage = "favorites"
|
||||
app.activeQuickFilter = ""
|
||||
}
|
||||
|
||||
function showHistory() {
|
||||
app.currentPage = "history"
|
||||
app.activeQuickFilter = ""
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: desktopNav
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
visible: !root.compactMode
|
||||
spacing: 8
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: 112
|
||||
Layout.preferredHeight: 40
|
||||
source: "qrc:/assets/app/bearwave_line.png"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
mipmap: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 1
|
||||
Layout.fillHeight: true
|
||||
color: BearTheme.cardBorder
|
||||
opacity: 0.6
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Top")
|
||||
highlighted: app.currentPage === "top"
|
||||
onClicked: loadTop()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("DE")
|
||||
highlighted: app.currentPage === "german"
|
||||
onClicked: loadGerman()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("NL")
|
||||
highlighted: app.currentPage === "dutch"
|
||||
onClicked: loadDutch()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Favorites")
|
||||
highlighted: app.currentPage === "favorites"
|
||||
onClicked: showFavorites()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("History")
|
||||
highlighted: app.currentPage === "history"
|
||||
onClicked: showHistory()
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Button {
|
||||
text: qsTr("Manual +")
|
||||
onClicked: app.addDialog.open()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("About")
|
||||
onClicked: app.aboutDialog.open()
|
||||
}
|
||||
|
||||
Button {
|
||||
visible: app.backend && app.backend.canResumeLastStation
|
||||
text: qsTr("Resume")
|
||||
onClicked: {
|
||||
if (app.backend) {
|
||||
app.backend.resumeLastStation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: compactNav
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
visible: root.compactMode
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: 96
|
||||
Layout.preferredHeight: 34
|
||||
source: "qrc:/assets/app/bearwave_line.png"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
mipmap: true
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Button {
|
||||
text: "+"
|
||||
onClicked: app.addDialog.open()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("About")
|
||||
onClicked: app.aboutDialog.open()
|
||||
}
|
||||
|
||||
Button {
|
||||
visible: app.backend && app.backend.canResumeLastStation
|
||||
text: "↺"
|
||||
onClicked: {
|
||||
if (app.backend) {
|
||||
app.backend.resumeLastStation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Button {
|
||||
text: qsTr("Top")
|
||||
highlighted: app.currentPage === "top"
|
||||
onClicked: loadTop()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("DE")
|
||||
highlighted: app.currentPage === "german"
|
||||
onClicked: loadGerman()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("NL")
|
||||
highlighted: app.currentPage === "dutch"
|
||||
onClicked: loadDutch()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Favorites")
|
||||
highlighted: app.currentPage === "favorites"
|
||||
onClicked: showFavorites()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("History")
|
||||
highlighted: app.currentPage === "history"
|
||||
onClicked: showHistory()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
190
src/qml/components/QuickFilters.qml
Normal file
190
src/qml/components/QuickFilters.qml
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright (c) 2026 Sebastian Palencsar
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import theme 1.0
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property var app
|
||||
required property bool compactMode
|
||||
|
||||
spacing: 8
|
||||
|
||||
function loadTag(tag, page) {
|
||||
if (!app.backend) return
|
||||
app.currentPage = page
|
||||
app.activeQuickFilter = "tag:" + tag
|
||||
app.backend.loadByTag(tag)
|
||||
}
|
||||
|
||||
function loadCountry(code, page) {
|
||||
if (!app.backend) return
|
||||
app.currentPage = page
|
||||
app.activeQuickFilter = "cc:" + code
|
||||
app.backend.loadByCountryCode(code)
|
||||
}
|
||||
|
||||
function openWorld() {
|
||||
if (!app.backend) return
|
||||
app.currentPage = "world"
|
||||
app.activeQuickFilter = "world"
|
||||
app.selectedWorldCategory = ""
|
||||
app.selectedWorldType = ""
|
||||
if (app.backend.countries.length === 0) {
|
||||
app.backend.loadCountries()
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: !root.compactMode
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
Label {
|
||||
text: qsTr("Genre:")
|
||||
color: BearTheme.textMuted
|
||||
font.pixelSize: 11
|
||||
rightPadding: 10
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Rock")
|
||||
highlighted: app.activeQuickFilter === "tag:rock"
|
||||
onClicked: loadTag("rock", "genre-rock")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("News")
|
||||
highlighted: app.activeQuickFilter === "tag:news"
|
||||
onClicked: loadTag("news", "genre-news")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Jazz")
|
||||
highlighted: app.activeQuickFilter === "tag:jazz"
|
||||
onClicked: loadTag("jazz", "genre-jazz")
|
||||
}
|
||||
|
||||
Item { Layout.preferredWidth: 20 }
|
||||
|
||||
Label {
|
||||
text: qsTr("Country:")
|
||||
color: BearTheme.textMuted
|
||||
font.pixelSize: 11
|
||||
rightPadding: 10
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("US")
|
||||
highlighted: app.activeQuickFilter === "cc:US"
|
||||
onClicked: loadCountry("US", "country-us")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("UK")
|
||||
highlighted: app.activeQuickFilter === "cc:GB"
|
||||
onClicked: loadCountry("GB", "country-gb")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("FR")
|
||||
highlighted: app.activeQuickFilter === "cc:FR"
|
||||
onClicked: loadCountry("FR", "country-fr")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("WORLD")
|
||||
highlighted: app.activeQuickFilter === "world"
|
||||
onClicked: openWorld()
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: root.compactMode
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: qsTr("Genre:")
|
||||
color: BearTheme.textMuted
|
||||
font.pixelSize: 11
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 8
|
||||
rightPadding: 16
|
||||
topPadding: 8
|
||||
bottomPadding: 8
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Rock")
|
||||
highlighted: app.activeQuickFilter === "tag:rock"
|
||||
onClicked: loadTag("rock", "genre-rock")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("News")
|
||||
highlighted: app.activeQuickFilter === "tag:news"
|
||||
onClicked: loadTag("news", "genre-news")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Jazz")
|
||||
highlighted: app.activeQuickFilter === "tag:jazz"
|
||||
onClicked: loadTag("jazz", "genre-jazz")
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: qsTr("Country:")
|
||||
color: BearTheme.textMuted
|
||||
font.pixelSize: 11
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 8
|
||||
rightPadding: 16
|
||||
topPadding: 8
|
||||
bottomPadding: 8
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("US")
|
||||
highlighted: app.activeQuickFilter === "cc:US"
|
||||
onClicked: loadCountry("US", "country-us")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("UK")
|
||||
highlighted: app.activeQuickFilter === "cc:GB"
|
||||
onClicked: loadCountry("GB", "country-gb")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("FR")
|
||||
highlighted: app.activeQuickFilter === "cc:FR"
|
||||
onClicked: loadCountry("FR", "country-fr")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("WORLD")
|
||||
highlighted: app.activeQuickFilter === "world"
|
||||
onClicked: openWorld()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
166
src/qml/components/SearchToolbar.qml
Normal file
166
src/qml/components/SearchToolbar.qml
Normal file
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) 2026 Sebastian Palencsar
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import theme 1.0
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property var app
|
||||
required property var searchTimer
|
||||
required property bool compactMode
|
||||
|
||||
property alias searchField: searchField
|
||||
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
visible: !root.compactMode
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
TextField {
|
||||
id: searchField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Search stations (name, genre, country)")
|
||||
onTextChanged: {
|
||||
if (app.backend) {
|
||||
app.backend.filterQuery = text
|
||||
}
|
||||
searchTimer.restart()
|
||||
}
|
||||
onAccepted: {
|
||||
if (text.length < 2 || !app.backend) return
|
||||
app.currentPage = "search"
|
||||
app.backend.searchStations(text)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Search")
|
||||
highlighted: true
|
||||
onClicked: {
|
||||
if (searchField.text.length < 2 || !app.backend) return
|
||||
app.currentPage = "search"
|
||||
app.backend.searchStations(searchField.text)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Sort A-Z")
|
||||
onClicked: {
|
||||
if (app.backend && app.currentPage !== "favorites") {
|
||||
app.backend.sortStations("name")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Sort Bitrate")
|
||||
onClicked: {
|
||||
if (app.backend && app.currentPage !== "favorites") {
|
||||
app.backend.sortStations("bitrate")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Sort Votes")
|
||||
onClicked: {
|
||||
if (app.backend && app.currentPage !== "favorites") {
|
||||
app.backend.sortStations("votes")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: root.compactMode
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
TextField {
|
||||
id: compactSearchField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Search stations (name, genre, country)")
|
||||
text: searchField.text
|
||||
onTextChanged: {
|
||||
if (searchField.text !== text) {
|
||||
searchField.text = text
|
||||
}
|
||||
if (app.backend) {
|
||||
app.backend.filterQuery = text
|
||||
}
|
||||
searchTimer.restart()
|
||||
}
|
||||
onAccepted: {
|
||||
if (text.length < 2 || !app.backend) return
|
||||
app.currentPage = "search"
|
||||
app.backend.searchStations(text)
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Button {
|
||||
text: qsTr("Search")
|
||||
highlighted: true
|
||||
onClicked: {
|
||||
if (compactSearchField.text.length < 2 || !app.backend) return
|
||||
app.currentPage = "search"
|
||||
app.backend.searchStations(compactSearchField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Button {
|
||||
text: "A-Z"
|
||||
onClicked: {
|
||||
if (app.backend && app.currentPage !== "favorites") {
|
||||
app.backend.sortStations("name")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "kb"
|
||||
onClicked: {
|
||||
if (app.backend && app.currentPage !== "favorites") {
|
||||
app.backend.sortStations("bitrate")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "❤"
|
||||
onClicked: {
|
||||
if (app.backend && app.currentPage !== "favorites") {
|
||||
app.backend.sortStations("votes")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Tip: you are not limited to DE/NL. Search worldwide by country, genre, or station name.")
|
||||
color: BearTheme.textMuted
|
||||
font.pixelSize: 11
|
||||
wrapMode: root.compactMode ? Text.WordWrap : Text.NoWrap
|
||||
elide: root.compactMode ? Text.ElideNone : Text.ElideRight
|
||||
}
|
||||
}
|
||||
3
src/qml/components/qmldir
Normal file
3
src/qml/components/qmldir
Normal file
@@ -0,0 +1,3 @@
|
||||
HeaderNavigation 1.0 HeaderNavigation.qml
|
||||
SearchToolbar 1.0 SearchToolbar.qml
|
||||
QuickFilters 1.0 QuickFilters.qml
|
||||
Reference in New Issue
Block a user