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:
Sebastian Palencsar
2026-06-20 14:10:51 +02:00
parent 1c736b97a6
commit 480bd483e8
10 changed files with 703 additions and 656 deletions

View 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
}
}