mirror of
https://github.com/spalencsar/bearwave.git
synced 2026-07-06 14:24:14 +02:00
feat(ui): Mac-inspired layout with sidebar, details panel, and About page
Replace the modal About dialog with an embedded About page and introduce a three-pane desktop layout: persistent sidebar navigation, flat station list, right-side details panel, and compact player bar. Load the GPLv3 license text from C++ and expose it to QML as context data. Normalize i18n source strings to English, complete German and Russian translations (133 strings each), and centralize page navigation helpers in Main.qml. Simplify compact HeaderNavigation and extend .gitignore for local build artifacts.
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -4,7 +4,11 @@ build.md
|
|||||||
|
|
||||||
## Build directories
|
## Build directories
|
||||||
build/
|
build/
|
||||||
|
build-about/
|
||||||
build-ci/
|
build-ci/
|
||||||
|
build-fix/
|
||||||
|
build-maclayout/
|
||||||
|
build-test/
|
||||||
build-arch-ci/
|
build-arch-ci/
|
||||||
build-dir/
|
build-dir/
|
||||||
cmake-build-*/
|
cmake-build-*/
|
||||||
@@ -36,6 +40,11 @@ test-*.json
|
|||||||
pkg/
|
pkg/
|
||||||
src/bearwave/
|
src/bearwave/
|
||||||
bearwave/
|
bearwave/
|
||||||
|
flatpak_repo/
|
||||||
|
.flatpak-builder/
|
||||||
|
.flatpak-builder-manifest.json
|
||||||
|
scratch/
|
||||||
|
spalencsar-README.md
|
||||||
*.pkg.tar.zst
|
*.pkg.tar.zst
|
||||||
*.pkg.tar.zst.sig
|
*.pkg.tar.zst.sig
|
||||||
*.pkg.tar.xz
|
*.pkg.tar.xz
|
||||||
|
|||||||
@@ -10,14 +10,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Add Russian language support and translation (thanks to [@aaly11](https://github.com/aaly11)).
|
- Add Russian language support and translation (thanks to [@aaly11](https://github.com/aaly11)).
|
||||||
|
- Add a Mac-inspired desktop layout with persistent sidebar navigation, flat station rows, right-side station details, and a compact now-playing bar.
|
||||||
|
- Add an embedded About page with full GPLv3 license text loaded from the bundled `LICENSE` resource and a third-party technologies section.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Flatpak repository signing key replaced with `Bearwave App <dev@bearwave.app>` (`5BAA384577671E45`).
|
- Flatpak repository signing key replaced with `Bearwave App <dev@bearwave.app>` (`5BAA384577671E45`).
|
||||||
|
- Replace the modal About dialog with an in-app About page reachable from sidebar and compact navigation.
|
||||||
|
- Refresh the QML visual theme toward a neutral dark palette and align the main layout more closely with the macOS BearWave structure.
|
||||||
|
- Use the BearWave line logo asset in the sidebar and remove duplicate branding from the top search toolbar.
|
||||||
|
- Make the top search/filter area and bottom player bar more responsive to narrower desktop window sizes.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fix German translation contexts broken after the QML refactor (thanks to [@aaly11](https://github.com/aaly11)).
|
- Fix German translation contexts broken after the QML refactor (thanks to [@aaly11](https://github.com/aaly11)).
|
||||||
|
- Fix the embedded GPL text failing to load in QML by loading the resource in C++ and exposing it to QML as context data.
|
||||||
|
- Fix the About license view formatting so the text uses the available width without an oversized empty box.
|
||||||
|
- Fix right-side station details and top toolbar clipping in the Mac-style layout.
|
||||||
|
|
||||||
## Version notes (1.0.3 → 1.0.5)
|
## Version notes (1.0.3 → 1.0.5)
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,14 @@ If you changed QML:
|
|||||||
qmllint src/qml/Main.qml src/qml/components/*.qml src/qml/theme/BearTheme.qml
|
qmllint src/qml/Main.qml src/qml/components/*.qml src/qml/theme/BearTheme.qml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For UI layout work that should be tested separately from the default build tree:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -S . -B build-maclayout -DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build build-maclayout -j"$(nproc)"
|
||||||
|
./build-maclayout/src/bearwave
|
||||||
|
```
|
||||||
|
|
||||||
Run unit tests after backend changes:
|
Run unit tests after backend changes:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -51,6 +59,7 @@ ctest --test-dir build --output-on-failure
|
|||||||
- Keep network operations asynchronous.
|
- Keep network operations asynchronous.
|
||||||
- Preserve MPRIS behavior and user state compatibility.
|
- Preserve MPRIS behavior and user state compatibility.
|
||||||
- When changing resources, register them in `src/qml.qrc`.
|
- When changing resources, register them in `src/qml.qrc`.
|
||||||
|
- When changing the About page, keep the full GPL text resource visible and keep third-party technology notes accurate.
|
||||||
|
|
||||||
## Manual Checks
|
## Manual Checks
|
||||||
|
|
||||||
@@ -63,6 +72,7 @@ Please smoke-test the area you changed when possible:
|
|||||||
- favorites persist across restart
|
- favorites persist across restart
|
||||||
- resume restores last station and volume
|
- resume restores last station and volume
|
||||||
- tray and MPRIS behavior still work
|
- tray and MPRIS behavior still work
|
||||||
|
- About page opens from sidebar/compact navigation, social links work, and the GPL license text is scrollable
|
||||||
|
|
||||||
## Pull Requests
|
## Pull Requests
|
||||||
|
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -20,7 +20,7 @@ Why not 1.0.4 on GitHub? [1.0.4](CHANGELOG.md#104---2026-06-20) exists as a **gi
|
|||||||
| Main window | Station browser |
|
| Main window | Station browser |
|
||||||
| ----------------------------------------- | -------------------------------------------- |
|
| ----------------------------------------- | -------------------------------------------- |
|
||||||
|  |  |
|
|  |  |
|
||||||
|  |  |
|
|  |  |
|
||||||
|  | |
|
|  | |
|
||||||
|
|
||||||
Screenshots: KDE Plasma on Linux.
|
Screenshots: KDE Plasma on Linux.
|
||||||
@@ -163,7 +163,8 @@ BearWave intentionally does not aim to be:
|
|||||||
- MPRIS integration for Plasma media controls and media keys
|
- MPRIS integration for Plasma media controls and media keys
|
||||||
- system tray integration for background playback
|
- system tray integration for background playback
|
||||||
- desktop notifications for song/track changes with local cover art caching
|
- desktop notifications for song/track changes with local cover art caching
|
||||||
- embedded About dialog with links and GNU GPLv3 license text
|
- Mac-inspired three-pane desktop layout with sidebar navigation, station list, details panel, and compact now-playing bar
|
||||||
|
- embedded About page with author links, full GNU GPLv3 license text, and third-party technology notes
|
||||||
|
|
||||||
## Project Status
|
## Project Status
|
||||||
|
|
||||||
@@ -216,14 +217,14 @@ Add distributions here only after explicit testing, not by assumption alone.
|
|||||||
|
|
||||||
### Language Support
|
### Language Support
|
||||||
|
|
||||||
BearWave currently supports English and German.
|
BearWave currently supports English, German, and Russian.
|
||||||
|
|
||||||
- the application uses the system locale to select its UI language
|
- the application uses the system locale to select its UI language
|
||||||
- English is the base UI language
|
- English is the base UI language
|
||||||
- German is provided as a bundled translation
|
- German and Russian are provided as bundled translations
|
||||||
- README, repository metadata, and development-facing material remain in English
|
- README, repository metadata, and development-facing material remain in English
|
||||||
|
|
||||||
If the system language is German, BearWave appears in German. Otherwise it falls back to English.
|
If the system language matches a bundled translation, BearWave uses that language. Otherwise it falls back to English.
|
||||||
|
|
||||||
## Installation Status
|
## Installation Status
|
||||||
|
|
||||||
@@ -290,6 +291,16 @@ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
|||||||
cmake --build build -j"$(nproc)"
|
cmake --build build -j"$(nproc)"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### UI layout development build
|
||||||
|
|
||||||
|
For layout work that should not disturb the normal `build/` directory, a separate build tree can be used:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -S . -B build-maclayout -DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build build-maclayout -j"$(nproc)"
|
||||||
|
./build-maclayout/src/bearwave
|
||||||
|
```
|
||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
|
|
||||||
From the build directory:
|
From the build directory:
|
||||||
@@ -425,13 +436,14 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for local build and review expectations.
|
|||||||
## Development Notes
|
## Development Notes
|
||||||
|
|
||||||
- main UI shell: `src/qml/Main.qml`
|
- main UI shell: `src/qml/Main.qml`
|
||||||
- QML components: `src/qml/components/` (navigation, search, station cards, player bar, dialogs)
|
- QML components: `src/qml/components/` (navigation, search, station rows, detail panel, About page, player bar, dialogs)
|
||||||
- theme singleton: `src/qml/theme/BearTheme.qml`
|
- theme singleton: `src/qml/theme/BearTheme.qml`
|
||||||
- backend orchestration: `src/radiobackend.cpp`
|
- backend orchestration: `src/radiobackend.cpp`
|
||||||
- stream playback: `src/bearplayer.cpp`
|
- stream playback: `src/bearplayer.cpp`
|
||||||
- API layer: `src/radiobrowser.cpp`
|
- API layer: `src/radiobrowser.cpp`
|
||||||
- MPRIS adapter: `src/mprisadaptor.cpp`
|
- MPRIS adapter: `src/mprisadaptor.cpp`
|
||||||
- desktop notifications: `src/notificationmanager.cpp`
|
- desktop notifications: `src/notificationmanager.cpp`
|
||||||
|
- embedded GPL license resource: `qrc:/assets/legal/gpl-3.0.txt` from `LICENSE`
|
||||||
- unit tests: `tests/`
|
- unit tests: `tests/`
|
||||||
|
|
||||||
See [CHANGELOG.md](CHANGELOG.md) for release history. For contributor and agent guardrails, see `AGENTS.md`.
|
See [CHANGELOG.md](CHANGELOG.md) for release history. For contributor and agent guardrails, see `AGENTS.md`.
|
||||||
|
|||||||
BIN
screenshots/screen6.png
Normal file
BIN
screenshots/screen6.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
@@ -6,6 +6,7 @@
|
|||||||
#include <QDBusConnectionInterface>
|
#include <QDBusConnectionInterface>
|
||||||
#include <QDBusMessage>
|
#include <QDBusMessage>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QFile>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QLocale>
|
#include <QLocale>
|
||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
@@ -56,9 +57,17 @@ int main(int argc, char *argv[])
|
|||||||
engine.addImportPath(QStringLiteral("qrc:/qml"));
|
engine.addImportPath(QStringLiteral("qrc:/qml"));
|
||||||
|
|
||||||
RadioBackend backend;
|
RadioBackend backend;
|
||||||
|
QFile licenseFile(QStringLiteral(":/assets/legal/gpl-3.0.txt"));
|
||||||
|
QString licenseText;
|
||||||
|
if (licenseFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
licenseText = QString::fromUtf8(licenseFile.readAll());
|
||||||
|
} else {
|
||||||
|
qWarning() << "Failed to load embedded GPLv3 license text";
|
||||||
|
}
|
||||||
engine.rootContext()->setContextProperty("radioBackend", &backend);
|
engine.rootContext()->setContextProperty("radioBackend", &backend);
|
||||||
engine.rootContext()->setContextProperty("bearwaveVersion", QStringLiteral(BEARWAVE_VERSION));
|
engine.rootContext()->setContextProperty("bearwaveVersion", QStringLiteral(BEARWAVE_VERSION));
|
||||||
engine.rootContext()->setContextProperty("bearwaveBuildId", QStringLiteral(BEARWAVE_GIT_HASH));
|
engine.rootContext()->setContextProperty("bearwaveBuildId", QStringLiteral(BEARWAVE_GIT_HASH));
|
||||||
|
engine.rootContext()->setContextProperty("bearwaveLicenseText", licenseText);
|
||||||
|
|
||||||
auto *mprisRoot = new MprisRootAdaptor(&backend, &app);
|
auto *mprisRoot = new MprisRootAdaptor(&backend, &app);
|
||||||
auto *mprisPlayer = new MprisPlayerAdaptor(&backend);
|
auto *mprisPlayer = new MprisPlayerAdaptor(&backend);
|
||||||
|
|||||||
@@ -11,12 +11,15 @@
|
|||||||
<file>qml/components/WorldCategoryHeader.qml</file>
|
<file>qml/components/WorldCategoryHeader.qml</file>
|
||||||
<file>qml/components/WorldCategories.qml</file>
|
<file>qml/components/WorldCategories.qml</file>
|
||||||
<file>qml/components/PlayerBar.qml</file>
|
<file>qml/components/PlayerBar.qml</file>
|
||||||
<file>qml/components/AboutDialog.qml</file>
|
<file>qml/components/AboutPage.qml</file>
|
||||||
<file>qml/components/AddStationDialog.qml</file>
|
<file>qml/components/AddStationDialog.qml</file>
|
||||||
<file>qml/components/EditStationDialog.qml</file>
|
<file>qml/components/EditStationDialog.qml</file>
|
||||||
<file>qml/components/LoadingOverlay.qml</file>
|
<file>qml/components/LoadingOverlay.qml</file>
|
||||||
<file>qml/components/ToastPopup.qml</file>
|
<file>qml/components/ToastPopup.qml</file>
|
||||||
<file>qml/components/StationListPanel.qml</file>
|
<file>qml/components/StationListPanel.qml</file>
|
||||||
|
<file>qml/components/SidebarNavigation.qml</file>
|
||||||
|
<file>qml/components/SidebarButton.qml</file>
|
||||||
|
<file>qml/components/StationDetailsPanel.qml</file>
|
||||||
<file>qml/utils/FlagUtils.js</file>
|
<file>qml/utils/FlagUtils.js</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="/">
|
<qresource prefix="/">
|
||||||
@@ -25,5 +28,6 @@
|
|||||||
<file alias="assets/ui/globe.svg">../assets/ui/globe.svg</file>
|
<file alias="assets/ui/globe.svg">../assets/ui/globe.svg</file>
|
||||||
<file alias="assets/ui/github.svg">../assets/ui/github.svg</file>
|
<file alias="assets/ui/github.svg">../assets/ui/github.svg</file>
|
||||||
<file alias="assets/ui/linkedin.svg">../assets/ui/linkedin.svg</file>
|
<file alias="assets/ui/linkedin.svg">../assets/ui/linkedin.svg</file>
|
||||||
|
<file alias="assets/legal/gpl-3.0.txt">../LICENSE</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
228
src/qml/Main.qml
228
src/qml/Main.qml
@@ -11,9 +11,9 @@ import components 1.0
|
|||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
id: root
|
id: root
|
||||||
title: qsTr("BearWave")
|
title: qsTr("BearWave")
|
||||||
width: 980
|
width: 1400
|
||||||
height: 700
|
height: 720
|
||||||
minimumWidth: 520
|
minimumWidth: 900
|
||||||
minimumHeight: 460
|
minimumHeight: 460
|
||||||
visible: true
|
visible: true
|
||||||
|
|
||||||
@@ -26,7 +26,9 @@ ApplicationWindow {
|
|||||||
property var backend: (typeof radioBackend !== "undefined" ? radioBackend : null)
|
property var backend: (typeof radioBackend !== "undefined" ? radioBackend : null)
|
||||||
readonly property string appVersion: (typeof bearwaveVersion !== "undefined" ? ("" + bearwaveVersion) : Qt.application.version)
|
readonly property string appVersion: (typeof bearwaveVersion !== "undefined" ? ("" + bearwaveVersion) : Qt.application.version)
|
||||||
readonly property string appBuildId: (typeof bearwaveBuildId !== "undefined" ? ("" + bearwaveBuildId) : "?")
|
readonly property string appBuildId: (typeof bearwaveBuildId !== "undefined" ? ("" + bearwaveBuildId) : "?")
|
||||||
property bool compactMode: width < 780
|
readonly property string appLicenseText: (typeof bearwaveLicenseText !== "undefined" ? ("" + bearwaveLicenseText) : "")
|
||||||
|
property bool compactMode: width < 900
|
||||||
|
property var selectedStation: null
|
||||||
property real contentOpacity: 1.0
|
property real contentOpacity: 1.0
|
||||||
property string activeQuickFilter: ""
|
property string activeQuickFilter: ""
|
||||||
property string selectedWorldCategory: ""
|
property string selectedWorldCategory: ""
|
||||||
@@ -36,12 +38,54 @@ ApplicationWindow {
|
|||||||
property alias stationList: stationPanel.stationList
|
property alias stationList: stationPanel.stationList
|
||||||
property alias addDialog: addDialogPane
|
property alias addDialog: addDialogPane
|
||||||
property alias editDialog: editDialogPane
|
property alias editDialog: editDialogPane
|
||||||
property alias aboutDialog: aboutDialogPane
|
|
||||||
|
|
||||||
function toast(message) {
|
function toast(message) {
|
||||||
toastPopup.show(message)
|
toastPopup.show(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function navigateToTop() {
|
||||||
|
if (!backend) return
|
||||||
|
currentPage = "top"
|
||||||
|
activeQuickFilter = ""
|
||||||
|
backend.loadTopStations()
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToGerman() {
|
||||||
|
if (!backend) return
|
||||||
|
currentPage = "german"
|
||||||
|
activeQuickFilter = ""
|
||||||
|
backend.loadGermanStations()
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToDutch() {
|
||||||
|
if (!backend) return
|
||||||
|
currentPage = "dutch"
|
||||||
|
activeQuickFilter = ""
|
||||||
|
backend.loadDutchStations()
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToWorld() {
|
||||||
|
currentPage = "world"
|
||||||
|
activeQuickFilter = ""
|
||||||
|
selectedWorldCategory = ""
|
||||||
|
selectedWorldType = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToFavorites() {
|
||||||
|
currentPage = "favorites"
|
||||||
|
activeQuickFilter = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToHistory() {
|
||||||
|
currentPage = "history"
|
||||||
|
activeQuickFilter = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToAbout() {
|
||||||
|
currentPage = "about"
|
||||||
|
activeQuickFilter = ""
|
||||||
|
}
|
||||||
|
|
||||||
function resetSearchFilter() {
|
function resetSearchFilter() {
|
||||||
searchTimer.stop()
|
searchTimer.stop()
|
||||||
if (searchField.text !== "") {
|
if (searchField.text !== "") {
|
||||||
@@ -85,6 +129,9 @@ ApplicationWindow {
|
|||||||
onCurrentPageChanged: {
|
onCurrentPageChanged: {
|
||||||
contentOpacity = 0.85
|
contentOpacity = 0.85
|
||||||
contentFadeRestart.restart()
|
contentFadeRestart.restart()
|
||||||
|
if (currentPage !== "about") {
|
||||||
|
selectedStation = null
|
||||||
|
}
|
||||||
if (currentPage !== "search") {
|
if (currentPage !== "search") {
|
||||||
resetSearchFilter()
|
resetSearchFilter()
|
||||||
}
|
}
|
||||||
@@ -111,34 +158,61 @@ ApplicationWindow {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
gradient: Gradient {
|
color: BearTheme.bgA
|
||||||
GradientStop { position: 0.0; color: BearTheme.bgA }
|
|
||||||
GradientStop { position: 1.0; color: BearTheme.bgB }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
SidebarNavigation {
|
||||||
anchors.fill: parent
|
id: sidebarNav
|
||||||
anchors.margins: 12
|
visible: !root.compactMode
|
||||||
spacing: 10
|
width: root.compactMode ? 0 : 200
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: 0
|
||||||
|
app: root
|
||||||
|
compactMode: root.compactMode
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: workspace
|
||||||
|
anchors.left: root.compactMode ? parent.left : sidebarNav.right
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: 0
|
||||||
|
anchors.leftMargin: root.compactMode ? 0 : 0
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
id: headerPanel
|
||||||
Layout.preferredHeight: headerContent.implicitHeight + 20
|
height: currentPage === "about" && !root.compactMode ? 1 : headerContent.implicitHeight + 18
|
||||||
radius: 12
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
radius: 0
|
||||||
color: BearTheme.panel
|
color: BearTheme.panel
|
||||||
border.color: BearTheme.cardBorder
|
border.color: "transparent"
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: 1
|
||||||
|
color: BearTheme.cardBorder
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: headerContent
|
id: headerContent
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 10
|
anchors.margins: 9
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
visible: !(currentPage === "about" && !root.compactMode)
|
||||||
|
|
||||||
HeaderNavigation {
|
HeaderNavigation {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
app: root
|
app: root
|
||||||
compactMode: root.compactMode
|
compactMode: root.compactMode
|
||||||
|
visible: root.compactMode
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -146,6 +220,7 @@ ApplicationWindow {
|
|||||||
Layout.preferredHeight: 1
|
Layout.preferredHeight: 1
|
||||||
color: BearTheme.cardBorder
|
color: BearTheme.cardBorder
|
||||||
opacity: 0.6
|
opacity: 0.6
|
||||||
|
visible: root.compactMode
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchToolbar {
|
SearchToolbar {
|
||||||
@@ -154,12 +229,14 @@ ApplicationWindow {
|
|||||||
app: root
|
app: root
|
||||||
compactMode: root.compactMode
|
compactMode: root.compactMode
|
||||||
searchTimer: searchTimer
|
searchTimer: searchTimer
|
||||||
|
visible: currentPage !== "about"
|
||||||
}
|
}
|
||||||
|
|
||||||
QuickFilters {
|
QuickFilters {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
app: root
|
app: root
|
||||||
compactMode: root.compactMode
|
compactMode: root.compactMode
|
||||||
|
visible: currentPage !== "about"
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@@ -173,12 +250,23 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
PlayerBar {
|
||||||
Layout.fillWidth: true
|
id: playerPanel
|
||||||
Layout.fillHeight: true
|
height: implicitHeight
|
||||||
radius: 12
|
anchors.left: parent.left
|
||||||
color: BearTheme.panel
|
anchors.right: parent.right
|
||||||
border.color: BearTheme.cardBorder
|
anchors.bottom: parent.bottom
|
||||||
|
app: root
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: contentArea
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: headerPanel.bottom
|
||||||
|
anchors.bottom: playerPanel.top
|
||||||
|
anchors.topMargin: 0
|
||||||
|
anchors.bottomMargin: 0
|
||||||
clip: true
|
clip: true
|
||||||
opacity: contentOpacity
|
opacity: contentOpacity
|
||||||
|
|
||||||
@@ -186,37 +274,74 @@ ApplicationWindow {
|
|||||||
NumberAnimation { duration: 180; easing.type: Easing.OutCubic }
|
NumberAnimation { duration: 180; easing.type: Easing.OutCubic }
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
Rectangle {
|
||||||
anchors.fill: parent
|
id: stationPanelFrame
|
||||||
anchors.margins: 10
|
visible: currentPage !== "about"
|
||||||
spacing: 10
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.right: root.compactMode ? parent.right : stationDetails.left
|
||||||
|
anchors.rightMargin: root.compactMode ? 0 : 0
|
||||||
|
radius: 0
|
||||||
|
color: BearTheme.panel
|
||||||
|
border.color: "transparent"
|
||||||
|
clip: true
|
||||||
|
|
||||||
WorldCategoryHeader {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
anchors.right: parent.right
|
||||||
visible: currentPage === "world" && selectedWorldCategory !== ""
|
anchors.top: parent.top
|
||||||
app: root
|
anchors.bottom: parent.bottom
|
||||||
|
width: 1
|
||||||
|
color: BearTheme.cardBorder
|
||||||
|
visible: !root.compactMode
|
||||||
}
|
}
|
||||||
|
|
||||||
StationListPanel {
|
ColumnLayout {
|
||||||
id: stationPanel
|
anchors.fill: parent
|
||||||
Layout.fillWidth: true
|
anchors.margins: 10
|
||||||
Layout.fillHeight: true
|
spacing: 0
|
||||||
app: root
|
|
||||||
}
|
|
||||||
|
|
||||||
WorldCategories {
|
WorldCategoryHeader {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
visible: currentPage === "world" && selectedWorldCategory !== ""
|
||||||
visible: currentPage === "world" && selectedWorldCategory === ""
|
app: root
|
||||||
app: root
|
}
|
||||||
compactMode: root.compactMode
|
|
||||||
|
StationListPanel {
|
||||||
|
id: stationPanel
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
app: root
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldCategories {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
visible: currentPage === "world" && selectedWorldCategory === ""
|
||||||
|
app: root
|
||||||
|
compactMode: root.compactMode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
PlayerBar {
|
StationDetailsPanel {
|
||||||
Layout.fillWidth: true
|
id: stationDetails
|
||||||
app: root
|
visible: !root.compactMode && currentPage !== "about"
|
||||||
|
width: 350
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
app: root
|
||||||
|
}
|
||||||
|
|
||||||
|
AboutPage {
|
||||||
|
visible: currentPage === "about"
|
||||||
|
anchors.fill: parent
|
||||||
|
compactMode: root.compactMode
|
||||||
|
appVersion: root.appVersion
|
||||||
|
buildId: root.appBuildId
|
||||||
|
licenseText: root.appLicenseText
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,13 +367,6 @@ ApplicationWindow {
|
|||||||
compactMode: root.compactMode
|
compactMode: root.compactMode
|
||||||
}
|
}
|
||||||
|
|
||||||
AboutDialog {
|
|
||||||
id: aboutDialogPane
|
|
||||||
compactMode: root.compactMode
|
|
||||||
appVersion: root.appVersion
|
|
||||||
buildId: root.appBuildId
|
|
||||||
}
|
|
||||||
|
|
||||||
ToastPopup {
|
ToastPopup {
|
||||||
id: toastPopup
|
id: toastPopup
|
||||||
window: root
|
window: root
|
||||||
|
|||||||
@@ -1,212 +0,0 @@
|
|||||||
// 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
|
|
||||||
property string appVersion: Qt.application.version
|
|
||||||
property string buildId: "?"
|
|
||||||
|
|
||||||
readonly property string versionLine: {
|
|
||||||
var version = root.appVersion
|
|
||||||
if (version === undefined || version === null || version === "")
|
|
||||||
version = Qt.application.version
|
|
||||||
var build = root.buildId
|
|
||||||
if (build === undefined || build === null || build === "")
|
|
||||||
build = "?"
|
|
||||||
return qsTr("Version") + " " + String(version) + " · " + qsTr("build") + " " + String(build)
|
|
||||||
}
|
|
||||||
|
|
||||||
modal: true
|
|
||||||
anchors.centerIn: parent
|
|
||||||
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: root.versionLine
|
|
||||||
color: BearTheme.textMuted
|
|
||||||
font.pixelSize: 12
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.AsNeeded
|
|
||||||
|
|
||||||
Label {
|
|
||||||
width: Math.max(licenseScroll.width, 1)
|
|
||||||
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/>."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
347
src/qml/components/AboutPage.qml
Normal file
347
src/qml/components/AboutPage.qml
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
// 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 bool compactMode
|
||||||
|
property string appVersion: Qt.application.version
|
||||||
|
property string buildId: "?"
|
||||||
|
property string licenseText: ""
|
||||||
|
|
||||||
|
readonly property string versionLine: {
|
||||||
|
var version = root.appVersion
|
||||||
|
if (version === undefined || version === null || version === "")
|
||||||
|
version = Qt.application.version
|
||||||
|
var build = root.buildId
|
||||||
|
if (build === undefined || build === null || build === "")
|
||||||
|
build = "?"
|
||||||
|
return qsTr("Version") + " " + String(version) + " · " + qsTr("Build") + " " + String(build)
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var thirdPartyItems: [
|
||||||
|
{
|
||||||
|
name: qsTr("Qt 6"),
|
||||||
|
usage: qsTr("Application framework and UI runtime: Core, DBus, Network, Quick, Quick Controls 2, Widgets, Multimedia, and LinguistTools."),
|
||||||
|
license: qsTr("Subject to the upstream licenses of the installed Qt distribution.")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: qsTr("Qt Multimedia backend"),
|
||||||
|
usage: qsTr("Audio playback runs through Qt Multimedia and the backend available on the system, such as GStreamer or FFmpeg."),
|
||||||
|
license: qsTr("Backend components are provided by the operating system or Qt build and retain their own licenses.")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: qsTr("Radio Browser API"),
|
||||||
|
usage: qsTr("Public station directory API for station data and stream discovery. BearWave does not bundle any Radio Browser server code."),
|
||||||
|
license: qsTr("External service or API; subject to the upstream project and service terms.")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: qsTr("freedesktop.org integrations"),
|
||||||
|
usage: qsTr("D-Bus, MPRIS, desktop notifications, desktop entry, and AppStream metadata."),
|
||||||
|
license: qsTr("Uses standards and interfaces for desktop integration; no third-party implementation is bundled.")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: pageScroll
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
width: Math.max(pageScroll.availableWidth, 1)
|
||||||
|
spacing: root.compactMode ? 18 : 22
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: root.compactMode ? 18 : 34
|
||||||
|
Layout.leftMargin: root.compactMode ? 18 : 46
|
||||||
|
Layout.rightMargin: root.compactMode ? 18 : 46
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
Image {
|
||||||
|
Layout.preferredWidth: root.compactMode ? 58 : 72
|
||||||
|
Layout.preferredHeight: root.compactMode ? 58 : 72
|
||||||
|
sourceSize.width: root.compactMode ? 58 : 72
|
||||||
|
sourceSize.height: root.compactMode ? 58 : 72
|
||||||
|
source: "qrc:/assets/app/bearwave.png"
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
Image {
|
||||||
|
Layout.preferredWidth: root.compactMode ? 170 : 210
|
||||||
|
Layout.preferredHeight: root.compactMode ? 40 : 48
|
||||||
|
source: "qrc:/assets/app/bearwave_line.png"
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
horizontalAlignment: Image.AlignLeft
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Internet radio player for KDE")
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.pixelSize: root.compactMode ? 14 : 16
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: root.versionLine
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.pixelSize: 12
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: root.compactMode ? 18 : 46
|
||||||
|
Layout.rightMargin: root.compactMode ? 18 : 46
|
||||||
|
Layout.preferredHeight: 1
|
||||||
|
color: BearTheme.cardBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: root.compactMode ? 18 : 46
|
||||||
|
Layout.rightMargin: root.compactMode ? 18 : 46
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Author")
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 11
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 14
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Sebastian Palencsár")
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 15
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolButton {
|
||||||
|
Layout.preferredWidth: 36
|
||||||
|
Layout.preferredHeight: 30
|
||||||
|
onClicked: Qt.openUrlExternally("https://palencsar.pro")
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Open website")
|
||||||
|
contentItem: Item {
|
||||||
|
Image {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 18
|
||||||
|
height: 18
|
||||||
|
source: "qrc:/assets/ui/globe.svg"
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolButton {
|
||||||
|
Layout.preferredWidth: 36
|
||||||
|
Layout.preferredHeight: 30
|
||||||
|
onClicked: Qt.openUrlExternally("https://github.com/spalencsar")
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Open GitHub")
|
||||||
|
contentItem: Item {
|
||||||
|
Image {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 18
|
||||||
|
height: 18
|
||||||
|
source: "qrc:/assets/ui/github.svg"
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolButton {
|
||||||
|
Layout.preferredWidth: 36
|
||||||
|
Layout.preferredHeight: 30
|
||||||
|
onClicked: Qt.openUrlExternally("https://www.linkedin.com/in/spalencsar/")
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Open LinkedIn")
|
||||||
|
contentItem: Item {
|
||||||
|
Image {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 18
|
||||||
|
height: 18
|
||||||
|
source: "qrc:/assets/ui/linkedin.svg"
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: root.compactMode ? 18 : 46
|
||||||
|
Layout.rightMargin: root.compactMode ? 18 : 46
|
||||||
|
Layout.preferredHeight: 1
|
||||||
|
color: BearTheme.cardBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: root.compactMode ? 18 : 46
|
||||||
|
Layout.rightMargin: root.compactMode ? 18 : 46
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Technologies and third-party components")
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 14
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("BearWave uses the following frameworks, services, and desktop standards. These components are not relicensed by BearWave.")
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.pixelSize: 12
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.thirdPartyItems
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: thirdPartyRow.implicitHeight + 16
|
||||||
|
radius: 6
|
||||||
|
color: "#24252b"
|
||||||
|
border.color: BearTheme.cardBorder
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: thirdPartyRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
spacing: 14
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.preferredWidth: root.compactMode ? 120 : 180
|
||||||
|
text: modelData.name
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 13
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: modelData.usage
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.pixelSize: 12
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: modelData.license
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.pixelSize: 11
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: root.compactMode ? 18 : 46
|
||||||
|
Layout.rightMargin: root.compactMode ? 18 : 46
|
||||||
|
Layout.bottomMargin: root.compactMode ? 18 : 34
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("GNU GPLv3 License")
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 14
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Copyright (c) 2026")
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.pixelSize: 11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: root.compactMode ? 240 : 280
|
||||||
|
radius: 6
|
||||||
|
color: "#202127"
|
||||||
|
border.color: BearTheme.cardBorder
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: licenseScroll
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 10
|
||||||
|
clip: true
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
|
|
||||||
|
TextArea {
|
||||||
|
id: licenseTextArea
|
||||||
|
width: Math.max(licenseScroll.availableWidth, 1)
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
textFormat: TextEdit.PlainText
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
text: root.licenseText.length > 0 ? root.licenseText : qsTr("License text could not be loaded.")
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.family: "monospace"
|
||||||
|
font.pixelSize: 12
|
||||||
|
background: Rectangle {
|
||||||
|
color: "transparent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,121 +13,13 @@ Item {
|
|||||||
required property var app
|
required property var app
|
||||||
required property bool compactMode
|
required property bool compactMode
|
||||||
|
|
||||||
implicitWidth: compactMode ? compactNav.implicitWidth : desktopNav.implicitWidth
|
implicitWidth: compactNav.implicitWidth
|
||||||
implicitHeight: compactMode ? compactNav.implicitHeight : desktopNav.implicitHeight
|
implicitHeight: compactNav.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 {
|
ColumnLayout {
|
||||||
id: compactNav
|
id: compactNav
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
visible: root.compactMode
|
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
@@ -152,7 +44,8 @@ Item {
|
|||||||
|
|
||||||
Button {
|
Button {
|
||||||
text: qsTr("About")
|
text: qsTr("About")
|
||||||
onClicked: app.aboutDialog.open()
|
highlighted: app.currentPage === "about"
|
||||||
|
onClicked: app.navigateToAbout()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
@@ -174,31 +67,31 @@ Item {
|
|||||||
Button {
|
Button {
|
||||||
text: qsTr("Top")
|
text: qsTr("Top")
|
||||||
highlighted: app.currentPage === "top"
|
highlighted: app.currentPage === "top"
|
||||||
onClicked: loadTop()
|
onClicked: app.navigateToTop()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
text: qsTr("DE")
|
text: qsTr("DE")
|
||||||
highlighted: app.currentPage === "german"
|
highlighted: app.currentPage === "german"
|
||||||
onClicked: loadGerman()
|
onClicked: app.navigateToGerman()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
text: qsTr("NL")
|
text: qsTr("NL")
|
||||||
highlighted: app.currentPage === "dutch"
|
highlighted: app.currentPage === "dutch"
|
||||||
onClicked: loadDutch()
|
onClicked: app.navigateToDutch()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
text: qsTr("Favorites")
|
text: qsTr("Favorites")
|
||||||
highlighted: app.currentPage === "favorites"
|
highlighted: app.currentPage === "favorites"
|
||||||
onClicked: showFavorites()
|
onClicked: app.navigateToFavorites()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
text: qsTr("History")
|
text: qsTr("History")
|
||||||
highlighted: app.currentPage === "history"
|
highlighted: app.currentPage === "history"
|
||||||
onClicked: showHistory()
|
onClicked: app.navigateToHistory()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,23 +12,34 @@ Rectangle {
|
|||||||
|
|
||||||
required property var app
|
required property var app
|
||||||
|
|
||||||
implicitHeight: 108
|
implicitHeight: 76
|
||||||
radius: 12
|
radius: 0
|
||||||
color: BearTheme.panel
|
color: BearTheme.panel
|
||||||
border.color: BearTheme.cardBorder
|
border.color: "transparent"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
height: 1
|
||||||
|
color: BearTheme.cardBorder
|
||||||
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 10
|
anchors.leftMargin: 12
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.topMargin: 9
|
||||||
|
anchors.bottomMargin: 9
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.preferredWidth: 88
|
Layout.preferredWidth: 56
|
||||||
Layout.preferredHeight: 88
|
Layout.preferredHeight: 56
|
||||||
radius: 8
|
radius: 6
|
||||||
color: "#182637"
|
color: "#34353b"
|
||||||
clip: true
|
clip: true
|
||||||
border.color: BearTheme.cardBorder
|
border.color: "transparent"
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: coverImage
|
id: coverImage
|
||||||
@@ -58,164 +69,122 @@ Rectangle {
|
|||||||
&& app.backend.currentStation.favicon.startsWith("https://")) {
|
&& app.backend.currentStation.favicon.startsWith("https://")) {
|
||||||
return 8
|
return 8
|
||||||
}
|
}
|
||||||
return 16
|
return 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.alignment: Qt.AlignVCenter
|
||||||
spacing: 6
|
spacing: 3
|
||||||
|
|
||||||
RowLayout {
|
Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 8
|
text: app.backend && app.backend.player
|
||||||
|
? (app.backend.player.currentStationName || qsTr("No station selected"))
|
||||||
Label {
|
: qsTr("No station selected")
|
||||||
Layout.fillWidth: true
|
color: BearTheme.textMain
|
||||||
text: app.backend && app.backend.player
|
font.pixelSize: 14
|
||||||
? (app.backend.player.currentStationName || qsTr("No station selected"))
|
font.bold: true
|
||||||
: qsTr("No station selected")
|
elide: Text.ElideRight
|
||||||
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 {
|
Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: app.backend && app.backend.player && app.backend.player.currentNowPlaying.length > 0
|
text: app.backend && app.backend.player && app.backend.player.currentNowPlaying.length > 0
|
||||||
? (qsTr("Now playing: ") + app.backend.player.currentNowPlaying)
|
? (qsTr("Now playing: ") + app.backend.player.currentNowPlaying)
|
||||||
: qsTr("Now playing: No track info")
|
: qsTr("Live radio")
|
||||||
color: BearTheme.textMuted
|
color: BearTheme.textMuted
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.alignment: Qt.AlignVCenter
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
text: "⏮"
|
text: "⏮"
|
||||||
Layout.preferredWidth: 44
|
Layout.preferredWidth: 42
|
||||||
enabled: app.backend ? app.backend.hasPreviousStation() : false
|
Layout.preferredHeight: 30
|
||||||
onClicked: {
|
enabled: app.backend ? app.backend.hasPreviousStation() : false
|
||||||
if (app.backend) {
|
onClicked: {
|
||||||
app.backend.playPreviousStation()
|
if (app.backend) {
|
||||||
|
app.backend.playPreviousStation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: app.backend && app.backend.player && app.backend.player.playing ? "⏸" : "▶"
|
||||||
|
Layout.preferredWidth: 42
|
||||||
|
Layout.preferredHeight: 30
|
||||||
|
onClicked: {
|
||||||
|
if (app.backend && app.backend.player) {
|
||||||
|
app.backend.player.togglePlayPause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "⏹"
|
||||||
|
Layout.preferredWidth: 42
|
||||||
|
Layout.preferredHeight: 30
|
||||||
|
onClicked: {
|
||||||
|
if (app.backend && app.backend.player) {
|
||||||
|
app.backend.player.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "⏭"
|
||||||
|
Layout.preferredWidth: 42
|
||||||
|
Layout.preferredHeight: 30
|
||||||
|
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: 36
|
||||||
|
Layout.preferredHeight: 30
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Button {
|
Slider {
|
||||||
text: app.backend && app.backend.player && app.backend.player.playing ? "⏸" : "▶"
|
Layout.preferredWidth: app.width < 1180 ? 115 : 160
|
||||||
Layout.preferredWidth: 44
|
from: 0
|
||||||
onClicked: {
|
to: 1
|
||||||
if (app.backend && app.backend.player) {
|
value: app.backend && app.backend.player ? app.backend.player.volume : 0.5
|
||||||
app.backend.player.togglePlayPause()
|
onMoved: {
|
||||||
}
|
if (app.backend && app.backend.player) {
|
||||||
}
|
app.backend.player.setVolume(value)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ ColumnLayout {
|
|||||||
app.backend.loadByTag(tag)
|
app.backend.loadByTag(tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load country by country code
|
||||||
function loadCountry(code, page) {
|
function loadCountry(code, page) {
|
||||||
if (!app.backend) return
|
if (!app.backend) return
|
||||||
app.currentPage = page
|
app.currentPage = page
|
||||||
@@ -40,72 +41,124 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
Flow {
|
||||||
visible: !root.compactMode
|
visible: !root.compactMode
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 6
|
Layout.preferredHeight: childrenRect.height
|
||||||
|
width: parent.width
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
// Left: Schnellzugriff filters
|
||||||
Label {
|
Label {
|
||||||
text: qsTr("Genre:")
|
text: qsTr("Quick access")
|
||||||
color: BearTheme.textMuted
|
color: BearTheme.textMuted
|
||||||
|
font.bold: true
|
||||||
font.pixelSize: 11
|
font.pixelSize: 11
|
||||||
rightPadding: 10
|
width: 86
|
||||||
|
height: 30
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
|
width: 72
|
||||||
|
height: 30
|
||||||
text: qsTr("Rock")
|
text: qsTr("Rock")
|
||||||
highlighted: app.activeQuickFilter === "tag:rock"
|
highlighted: app.activeQuickFilter === "tag:rock"
|
||||||
onClicked: loadTag("rock", "genre-rock")
|
onClicked: loadTag("rock", "genre-rock")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
|
width: 72
|
||||||
|
height: 30
|
||||||
text: qsTr("News")
|
text: qsTr("News")
|
||||||
highlighted: app.activeQuickFilter === "tag:news"
|
highlighted: app.activeQuickFilter === "tag:news"
|
||||||
onClicked: loadTag("news", "genre-news")
|
onClicked: loadTag("news", "genre-news")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
|
width: 72
|
||||||
|
height: 30
|
||||||
text: qsTr("Jazz")
|
text: qsTr("Jazz")
|
||||||
highlighted: app.activeQuickFilter === "tag:jazz"
|
highlighted: app.activeQuickFilter === "tag:jazz"
|
||||||
onClicked: loadTag("jazz", "genre-jazz")
|
onClicked: loadTag("jazz", "genre-jazz")
|
||||||
}
|
}
|
||||||
|
|
||||||
Item { Layout.preferredWidth: 20 }
|
Rectangle {
|
||||||
|
width: 1
|
||||||
Label {
|
height: 18
|
||||||
text: qsTr("Country:")
|
color: BearTheme.cardBorder
|
||||||
color: BearTheme.textMuted
|
opacity: 0.6
|
||||||
font.pixelSize: 11
|
|
||||||
rightPadding: 10
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
|
width: 62
|
||||||
|
height: 30
|
||||||
text: qsTr("US")
|
text: qsTr("US")
|
||||||
highlighted: app.activeQuickFilter === "cc:US"
|
highlighted: app.activeQuickFilter === "cc:US"
|
||||||
onClicked: loadCountry("US", "country-us")
|
onClicked: loadCountry("US", "country-us")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
text: qsTr("UK")
|
width: 62
|
||||||
|
height: 30
|
||||||
|
text: qsTr("GB")
|
||||||
highlighted: app.activeQuickFilter === "cc:GB"
|
highlighted: app.activeQuickFilter === "cc:GB"
|
||||||
onClicked: loadCountry("GB", "country-gb")
|
onClicked: loadCountry("GB", "country-gb")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
|
width: 62
|
||||||
|
height: 30
|
||||||
text: qsTr("FR")
|
text: qsTr("FR")
|
||||||
highlighted: app.activeQuickFilter === "cc:FR"
|
highlighted: app.activeQuickFilter === "cc:FR"
|
||||||
onClicked: loadCountry("FR", "country-fr")
|
onClicked: loadCountry("FR", "country-fr")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Label {
|
||||||
text: qsTr("WORLD")
|
text: qsTr("Sort")
|
||||||
highlighted: app.activeQuickFilter === "world"
|
color: BearTheme.textMuted
|
||||||
onClicked: openWorld()
|
font.bold: true
|
||||||
|
font.pixelSize: 11
|
||||||
|
width: 76
|
||||||
|
height: 30
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
Button {
|
||||||
|
width: 78
|
||||||
|
height: 30
|
||||||
|
text: qsTr("Name")
|
||||||
|
onClicked: {
|
||||||
|
if (app.backend && app.currentPage !== "favorites") {
|
||||||
|
app.backend.sortStations("name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
width: 78
|
||||||
|
height: 30
|
||||||
|
text: qsTr("Bitrate")
|
||||||
|
onClicked: {
|
||||||
|
if (app.backend && app.currentPage !== "favorites") {
|
||||||
|
app.backend.sortStations("bitrate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
width: 78
|
||||||
|
height: 30
|
||||||
|
text: qsTr("Votes")
|
||||||
|
onClicked: {
|
||||||
|
if (app.backend && app.currentPage !== "favorites") {
|
||||||
|
app.backend.sortStations("votes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compact Mode (Mobile/Narrow) layout
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
visible: root.compactMode
|
visible: root.compactMode
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@@ -187,4 +240,4 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ ColumnLayout {
|
|||||||
RowLayout {
|
RowLayout {
|
||||||
visible: !root.compactMode
|
visible: !root.compactMode
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 8
|
spacing: 10
|
||||||
|
|
||||||
TextField {
|
TextField {
|
||||||
id: searchField
|
id: searchField
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
placeholderText: qsTr("Search stations (name, genre, country)")
|
Layout.minimumWidth: 180
|
||||||
|
placeholderText: qsTr("Search stations (name, genre, country)...")
|
||||||
onTextChanged: {
|
onTextChanged: {
|
||||||
if (app.backend) {
|
if (app.backend) {
|
||||||
app.backend.filterQuery = text
|
app.backend.filterQuery = text
|
||||||
@@ -40,44 +41,35 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
// Right Search and Clear buttons
|
||||||
text: qsTr("Search")
|
RowLayout {
|
||||||
highlighted: true
|
spacing: 6
|
||||||
onClicked: {
|
|
||||||
if (searchField.text.length < 2 || !app.backend) return
|
|
||||||
app.currentPage = "search"
|
|
||||||
app.backend.searchStations(searchField.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
text: qsTr("Sort A-Z")
|
text: root.app.width < 1180 ? "🔍" : "🔍 " + qsTr("Search")
|
||||||
onClicked: {
|
highlighted: true
|
||||||
if (app.backend && app.currentPage !== "favorites") {
|
Layout.preferredWidth: root.app.width < 1180 ? 44 : 88
|
||||||
app.backend.sortStations("name")
|
onClicked: {
|
||||||
|
if (searchField.text.length < 2 || !app.backend) return
|
||||||
|
app.currentPage = "search"
|
||||||
|
app.backend.searchStations(searchField.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
text: qsTr("Sort Bitrate")
|
text: root.app.width < 1180 ? "✕" : "✕ " + qsTr("Clear")
|
||||||
onClicked: {
|
Layout.preferredWidth: root.app.width < 1180 ? 44 : 82
|
||||||
if (app.backend && app.currentPage !== "favorites") {
|
onClicked: {
|
||||||
app.backend.sortStations("bitrate")
|
searchField.text = ""
|
||||||
}
|
if (app.backend) {
|
||||||
}
|
app.backend.filterQuery = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
|
||||||
text: qsTr("Sort Votes")
|
|
||||||
onClicked: {
|
|
||||||
if (app.backend && app.currentPage !== "favorites") {
|
|
||||||
app.backend.sortStations("votes")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compact Mode (Mobile/Narrow) Search Layout
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
visible: root.compactMode
|
visible: root.compactMode
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@@ -108,59 +100,27 @@ ColumnLayout {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
text: qsTr("Search")
|
text: "🔍 " + qsTr("Search")
|
||||||
highlighted: true
|
highlighted: true
|
||||||
|
Layout.fillWidth: true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (compactSearchField.text.length < 2 || !app.backend) return
|
if (searchField.text.length < 2 || !app.backend) return
|
||||||
app.currentPage = "search"
|
app.currentPage = "search"
|
||||||
app.backend.searchStations(compactSearchField.text)
|
app.backend.searchStations(searchField.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 {
|
Button {
|
||||||
text: "kb"
|
text: "✕ " + qsTr("Clear")
|
||||||
|
Layout.fillWidth: true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (app.backend && app.currentPage !== "favorites") {
|
searchField.text = ""
|
||||||
app.backend.sortStations("bitrate")
|
if (app.backend) {
|
||||||
}
|
app.backend.filterQuery = ""
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
59
src/qml/components/SidebarButton.qml
Normal file
59
src/qml/components/SidebarButton.qml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// 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: btn
|
||||||
|
|
||||||
|
property string text: ""
|
||||||
|
property string iconText: ""
|
||||||
|
property bool isActive: false
|
||||||
|
signal clicked()
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 32
|
||||||
|
radius: 6
|
||||||
|
|
||||||
|
color: isActive
|
||||||
|
? "#4b4b4f"
|
||||||
|
: (mouseArea.containsMouse ? "#36373d" : "transparent")
|
||||||
|
|
||||||
|
border.color: "transparent"
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: btn.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 9
|
||||||
|
anchors.rightMargin: 9
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: btn.iconText
|
||||||
|
font.pixelSize: 14
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: btn.text
|
||||||
|
color: btn.isActive ? BearTheme.textMain : (mouseArea.containsMouse ? BearTheme.textMain : BearTheme.textMuted)
|
||||||
|
font.bold: btn.isActive
|
||||||
|
font.pixelSize: 12
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
153
src/qml/components/SidebarNavigation.qml
Normal file
153
src/qml/components/SidebarNavigation.qml
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// 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
|
||||||
|
required property bool compactMode
|
||||||
|
|
||||||
|
color: BearTheme.sidebar
|
||||||
|
border.color: "transparent"
|
||||||
|
border.width: 0
|
||||||
|
radius: 0
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 20
|
||||||
|
spacing: 14
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 10
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
|
||||||
|
Image {
|
||||||
|
Layout.preferredWidth: 34
|
||||||
|
Layout.preferredHeight: 34
|
||||||
|
source: "qrc:/assets/app/bearwave.png"
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 28
|
||||||
|
source: "qrc:/assets/app/bearwave_line.png"
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
horizontalAlignment: Image.AlignLeft
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Stations")
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 10
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
SidebarButton {
|
||||||
|
text: qsTr("Top Global")
|
||||||
|
iconText: "🌍"
|
||||||
|
isActive: app.currentPage === "top"
|
||||||
|
onClicked: app.navigateToTop()
|
||||||
|
}
|
||||||
|
|
||||||
|
SidebarButton {
|
||||||
|
text: qsTr("Germany")
|
||||||
|
iconText: "🇩🇪"
|
||||||
|
isActive: app.currentPage === "german"
|
||||||
|
onClicked: app.navigateToGerman()
|
||||||
|
}
|
||||||
|
|
||||||
|
SidebarButton {
|
||||||
|
text: qsTr("Netherlands")
|
||||||
|
iconText: "🇳🇱"
|
||||||
|
isActive: app.currentPage === "dutch"
|
||||||
|
onClicked: app.navigateToDutch()
|
||||||
|
}
|
||||||
|
|
||||||
|
SidebarButton {
|
||||||
|
text: qsTr("Worldwide")
|
||||||
|
iconText: "🌐"
|
||||||
|
isActive: app.currentPage === "world"
|
||||||
|
onClicked: app.navigateToWorld()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Library")
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 10
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
SidebarButton {
|
||||||
|
text: qsTr("Favorites")
|
||||||
|
iconText: "★"
|
||||||
|
isActive: app.currentPage === "favorites"
|
||||||
|
onClicked: app.navigateToFavorites()
|
||||||
|
}
|
||||||
|
|
||||||
|
SidebarButton {
|
||||||
|
text: qsTr("History")
|
||||||
|
iconText: "🕒"
|
||||||
|
isActive: app.currentPage === "history"
|
||||||
|
onClicked: app.navigateToHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillHeight: true }
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
SidebarButton {
|
||||||
|
text: qsTr("Add Station")
|
||||||
|
iconText: "➕"
|
||||||
|
isActive: false
|
||||||
|
onClicked: app.addDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
SidebarButton {
|
||||||
|
text: qsTr("About BearWave")
|
||||||
|
iconText: "ℹ️"
|
||||||
|
isActive: app.currentPage === "about"
|
||||||
|
onClicked: app.navigateToAbout()
|
||||||
|
}
|
||||||
|
|
||||||
|
SidebarButton {
|
||||||
|
visible: app.backend && app.backend.canResumeLastStation
|
||||||
|
text: qsTr("Resume")
|
||||||
|
iconText: "↺"
|
||||||
|
isActive: false
|
||||||
|
onClicked: {
|
||||||
|
if (app.backend) {
|
||||||
|
app.backend.resumeLastStation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,9 +16,9 @@ Rectangle {
|
|||||||
required property bool compactMode
|
required property bool compactMode
|
||||||
required property real listWidth
|
required property real listWidth
|
||||||
|
|
||||||
height: compactMode ? 72 : 78
|
height: 52
|
||||||
width: listWidth
|
width: listWidth
|
||||||
radius: 10
|
radius: 0
|
||||||
|
|
||||||
readonly property bool isCurrent: {
|
readonly property bool isCurrent: {
|
||||||
if (!app.backend || !modelData) return false
|
if (!app.backend || !modelData) return false
|
||||||
@@ -34,10 +34,24 @@ Rectangle {
|
|||||||
readonly property bool isPlaying: isCurrent && app.backend && app.backend.player && app.backend.player.playing
|
readonly property bool isPlaying: isCurrent && app.backend && app.backend.player && app.backend.player.playing
|
||||||
|
|
||||||
color: isCurrent
|
color: isCurrent
|
||||||
? (cardMouse.containsMouse ? "#1d3350" : "#16283e")
|
? "#4a4a4d"
|
||||||
: (cardMouse.containsMouse ? BearTheme.cardHover : BearTheme.card)
|
: (cardMouse.containsMouse ? "#2d2e34" : "transparent")
|
||||||
border.color: isCurrent ? BearTheme.accent : BearTheme.cardBorder
|
|
||||||
border.width: isCurrent ? 2 : 1
|
border.width: 0
|
||||||
|
|
||||||
|
readonly property string metadataText: {
|
||||||
|
if (!modelData) return ""
|
||||||
|
var parts = []
|
||||||
|
if (modelData.country) parts.push(modelData.country)
|
||||||
|
if (modelData.codec && modelData.codec !== "unknown" && modelData.codec !== "") parts.push(modelData.codec.toUpperCase())
|
||||||
|
if (modelData.bitrate > 0) parts.push(modelData.bitrate + " kbps")
|
||||||
|
if (modelData.votes > 0) {
|
||||||
|
parts.push(modelData.votes.toLocaleString(Qt.locale(), "f", 0) + " " + qsTr("votes"))
|
||||||
|
} else {
|
||||||
|
parts.push(qsTr("Stream"))
|
||||||
|
}
|
||||||
|
return parts.join(" • ")
|
||||||
|
}
|
||||||
|
|
||||||
function playStation() {
|
function playStation() {
|
||||||
if (!app.backend) return
|
if (!app.backend) return
|
||||||
@@ -56,6 +70,7 @@ Rectangle {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
app.selectedStation = root.modelData
|
||||||
if (!app.backend) return
|
if (!app.backend) return
|
||||||
if (root.isCurrent) {
|
if (root.isCurrent) {
|
||||||
app.backend.player.togglePlayPause()
|
app.backend.player.togglePlayPause()
|
||||||
@@ -65,22 +80,33 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: 1
|
||||||
|
color: BearTheme.cardBorder
|
||||||
|
opacity: 0.65
|
||||||
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 10
|
anchors.leftMargin: 8
|
||||||
|
anchors.rightMargin: 8
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
|
// Favicon Rect
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.preferredWidth: 44
|
Layout.preferredWidth: 34
|
||||||
Layout.preferredHeight: 44
|
Layout.preferredHeight: 34
|
||||||
radius: 8
|
radius: 5
|
||||||
color: "#123154"
|
color: "#34353b"
|
||||||
border.color: BearTheme.accent
|
clip: true
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: stationFavicon
|
id: stationFavicon
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 3
|
anchors.margins: 2
|
||||||
source: (root.modelData.favicon && root.modelData.favicon.startsWith("https://"))
|
source: (root.modelData.favicon && root.modelData.favicon.startsWith("https://"))
|
||||||
? root.modelData.favicon
|
? root.modelData.favicon
|
||||||
: ""
|
: ""
|
||||||
@@ -100,9 +126,11 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Info Column
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 2
|
spacing: 1
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@@ -113,10 +141,11 @@ Rectangle {
|
|||||||
text: root.modelData.name
|
text: root.modelData.name
|
||||||
color: root.isCurrent ? BearTheme.accent : BearTheme.textMain
|
color: root.isCurrent ? BearTheme.accent : BearTheme.textMain
|
||||||
font.bold: true
|
font.bold: true
|
||||||
font.pixelSize: compactMode ? 13 : 14
|
font.pixelSize: 13
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tiny EQ Indicator
|
||||||
Row {
|
Row {
|
||||||
id: eqAnimation
|
id: eqAnimation
|
||||||
spacing: 2
|
spacing: 2
|
||||||
@@ -126,32 +155,26 @@ Rectangle {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: bar1
|
id: bar1
|
||||||
width: 2
|
width: 2
|
||||||
height: 12
|
height: 10
|
||||||
color: BearTheme.accent
|
color: BearTheme.playingAccent
|
||||||
radius: 1
|
radius: 1
|
||||||
Behavior on height {
|
Behavior on height { NumberAnimation { duration: 120 } }
|
||||||
NumberAnimation { duration: 120 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: bar2
|
id: bar2
|
||||||
width: 2
|
width: 2
|
||||||
height: 12
|
height: 10
|
||||||
color: BearTheme.accent
|
color: BearTheme.playingAccent
|
||||||
radius: 1
|
radius: 1
|
||||||
Behavior on height {
|
Behavior on height { NumberAnimation { duration: 120 } }
|
||||||
NumberAnimation { duration: 120 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: bar3
|
id: bar3
|
||||||
width: 2
|
width: 2
|
||||||
height: 12
|
height: 10
|
||||||
color: BearTheme.accent
|
color: BearTheme.playingAccent
|
||||||
radius: 1
|
radius: 1
|
||||||
Behavior on height {
|
Behavior on height { NumberAnimation { duration: 120 } }
|
||||||
NumberAnimation { duration: 120 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
@@ -159,17 +182,17 @@ Rectangle {
|
|||||||
running: root.isPlaying
|
running: root.isPlaying
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
bar1.height = Math.floor(Math.random() * 11) + 3
|
bar1.height = Math.floor(Math.random() * 9) + 2
|
||||||
bar2.height = Math.floor(Math.random() * 11) + 3
|
bar2.height = Math.floor(Math.random() * 9) + 2
|
||||||
bar3.height = Math.floor(Math.random() * 11) + 3
|
bar3.height = Math.floor(Math.random() * 9) + 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
bar1.height = 12
|
bar1.height = 10
|
||||||
bar2.height = 12
|
bar2.height = 10
|
||||||
bar3.height = 12
|
bar3.height = 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,21 +200,20 @@ Rectangle {
|
|||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: root.modelData.country + " • "
|
text: root.metadataText
|
||||||
+ (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
|
color: BearTheme.textMuted
|
||||||
font.pixelSize: 11
|
font.pixelSize: 11
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edit Button (Custom User Stations Only)
|
||||||
Button {
|
Button {
|
||||||
visible: !root.modelData.uuid
|
visible: !root.modelData.uuid
|
||||||
Layout.preferredWidth: compactMode ? 34 : 40
|
Layout.preferredWidth: 32
|
||||||
Layout.preferredHeight: 40
|
Layout.preferredHeight: 32
|
||||||
text: "✏"
|
text: "✏"
|
||||||
|
flat: true
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: qsTr("Edit Station")
|
ToolTip.text: qsTr("Edit Station")
|
||||||
onClicked: {
|
onClicked: {
|
||||||
@@ -200,10 +222,13 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Favorite Button
|
||||||
Button {
|
Button {
|
||||||
Layout.preferredWidth: compactMode ? 34 : 40
|
Layout.preferredWidth: 32
|
||||||
Layout.preferredHeight: 40
|
Layout.preferredHeight: 32
|
||||||
text: root.modelData.isFavorite ? "★" : "☆"
|
text: root.modelData.isFavorite ? "★" : "☆"
|
||||||
|
flat: true
|
||||||
|
font.pixelSize: 14
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!app.backend) return
|
if (!app.backend) return
|
||||||
app.backend.toggleFavoriteById(root.modelData.uuid, root.modelData.urlResolved)
|
app.backend.toggleFavoriteById(root.modelData.uuid, root.modelData.urlResolved)
|
||||||
@@ -211,10 +236,13 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Play/Pause Button
|
||||||
Button {
|
Button {
|
||||||
Layout.preferredWidth: compactMode ? 34 : 40
|
Layout.preferredWidth: 32
|
||||||
Layout.preferredHeight: 40
|
Layout.preferredHeight: 32
|
||||||
text: (root.isCurrent && app.backend && app.backend.player && app.backend.player.playing) ? "⏸" : "▶"
|
text: (root.isCurrent && app.backend && app.backend.player && app.backend.player.playing) ? "⏸" : "▶"
|
||||||
|
flat: true
|
||||||
|
font.pixelSize: 13
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!app.backend) return
|
if (!app.backend) return
|
||||||
if (root.isCurrent) {
|
if (root.isCurrent) {
|
||||||
@@ -225,4 +253,4 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
359
src/qml/components/StationDetailsPanel.qml
Normal file
359
src/qml/components/StationDetailsPanel.qml
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
color: BearTheme.panel
|
||||||
|
border.color: "transparent"
|
||||||
|
border.width: 0
|
||||||
|
radius: 0
|
||||||
|
|
||||||
|
readonly property var activeStation: app.selectedStation || (app.backend ? app.backend.currentStation : null)
|
||||||
|
readonly property bool isCurrentPlaying: {
|
||||||
|
if (!activeStation || !app.backend) return false
|
||||||
|
var currentUuid = app.backend.currentStationUuid
|
||||||
|
var currentUrl = app.backend.currentStationUrl
|
||||||
|
var activeUuid = activeStation.uuid || ""
|
||||||
|
var activeUrl = activeStation.urlResolved || activeStation.url || ""
|
||||||
|
if (currentUuid !== "" && activeUuid !== "") {
|
||||||
|
return currentUuid === activeUuid
|
||||||
|
}
|
||||||
|
return currentUrl !== "" && currentUrl === activeUrl
|
||||||
|
}
|
||||||
|
readonly property bool isPlaying: isCurrentPlaying && app.backend && app.backend.player && app.backend.player.playing
|
||||||
|
|
||||||
|
// Helper text edit to facilitate clipboard copying
|
||||||
|
TextEdit {
|
||||||
|
id: clipboardHelper
|
||||||
|
visible: false
|
||||||
|
selectByMouse: true
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
clipboardHelper.text = text
|
||||||
|
clipboardHelper.selectAll()
|
||||||
|
clipboardHelper.copy()
|
||||||
|
app.toast(qsTr("Stream URL copied!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 16
|
||||||
|
spacing: 10
|
||||||
|
visible: root.activeStation !== null
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: detailsScroll
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
clip: true
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
width: detailsScroll.availableWidth
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
// Cover Art / Favicon
|
||||||
|
Rectangle {
|
||||||
|
Layout.alignment: Qt.AlignLeft
|
||||||
|
Layout.preferredWidth: 72
|
||||||
|
Layout.preferredHeight: 72
|
||||||
|
radius: 8
|
||||||
|
color: "#34353b"
|
||||||
|
border.color: "transparent"
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: detailFavicon
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: {
|
||||||
|
if (app.backend && app.backend.player && app.backend.player.currentCoverArtUrl && root.isCurrentPlaying) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return activeStation && activeStation.favicon && activeStation.favicon.startsWith("https://") ? 8 : 16
|
||||||
|
}
|
||||||
|
source: {
|
||||||
|
if (!root.activeStation) return ""
|
||||||
|
if (root.isCurrentPlaying && app.backend && app.backend.player && app.backend.player.currentCoverArtUrl && app.backend.player.currentCoverArtUrl !== "") {
|
||||||
|
return app.backend.player.currentCoverArtUrl
|
||||||
|
}
|
||||||
|
return (root.activeStation.favicon && root.activeStation.favicon.startsWith("https://")) ? root.activeStation.favicon : ""
|
||||||
|
}
|
||||||
|
fillMode: (root.isCurrentPlaying && app.backend && app.backend.player && app.backend.player.currentCoverArtUrl && app.backend.player.currentCoverArtUrl !== "") ? Image.PreserveAspectCrop : Image.PreserveAspectFit
|
||||||
|
asynchronous: true
|
||||||
|
cache: true
|
||||||
|
visible: source !== "" && status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 16
|
||||||
|
source: "qrc:/assets/app/bearwave.png"
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
|
visible: !detailFavicon.visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Station Name & Country
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 4
|
||||||
|
Layout.alignment: Qt.AlignLeft
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: root.activeStation ? root.activeStation.name : ""
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 16
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: root.activeStation ? root.activeStation.country : ""
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.pixelSize: 13
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: root.isPlaying ? "⏸ " + qsTr("Pause") : "▶ " + qsTr("Play")
|
||||||
|
highlighted: true
|
||||||
|
width: 86
|
||||||
|
height: 28
|
||||||
|
font.pixelSize: 11
|
||||||
|
onClicked: {
|
||||||
|
if (!app.backend || !root.activeStation) return
|
||||||
|
if (root.isCurrentPlaying) {
|
||||||
|
app.backend.player.togglePlayPause()
|
||||||
|
} else {
|
||||||
|
// Find index or play by url
|
||||||
|
app.backend.playRecentByUuid(root.activeStation.uuid, root.activeStation.urlResolved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: root.activeStation && root.activeStation.isFavorite ? "★ " + qsTr("Favorite") : "☆ " + qsTr("Add")
|
||||||
|
width: 104
|
||||||
|
height: 28
|
||||||
|
font.pixelSize: 11
|
||||||
|
onClicked: {
|
||||||
|
if (!app.backend || !root.activeStation) return
|
||||||
|
app.backend.toggleFavoriteById(root.activeStation.uuid, root.activeStation.urlResolved)
|
||||||
|
app.toast(root.activeStation.isFavorite ? qsTr("Removed from favorites") : qsTr("Added to favorites"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "📁 " + qsTr("Groups")
|
||||||
|
width: 92
|
||||||
|
height: 28
|
||||||
|
font.pixelSize: 11
|
||||||
|
onClicked: app.toast(qsTr("Groups coming soon"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 1
|
||||||
|
color: BearTheme.cardBorder
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
|
|
||||||
|
// Technical Specifications
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Details")
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
GridLayout {
|
||||||
|
columns: 2
|
||||||
|
columnSpacing: 16
|
||||||
|
rowSpacing: 8
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Codec:")
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: (root.activeStation && root.activeStation.codec && root.activeStation.codec !== "unknown") ? root.activeStation.codec.toUpperCase() : qsTr("Unknown")
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Bitrate:")
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: (root.activeStation && root.activeStation.bitrate > 0) ? root.activeStation.bitrate + " kbps" : qsTr("Unknown")
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Votes:")
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: root.activeStation ? "" + root.activeStation.votes : "0"
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 1
|
||||||
|
color: BearTheme.cardBorder
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags / Genres
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Tags")
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: (root.activeStation && root.activeStation.tags) ? root.activeStation.tags.split(",") : []
|
||||||
|
delegate: Rectangle {
|
||||||
|
visible: modelData.trim().length > 0
|
||||||
|
height: tagText.implicitHeight + 8
|
||||||
|
width: tagText.implicitWidth + 12
|
||||||
|
radius: 4
|
||||||
|
color: BearTheme.card
|
||||||
|
border.color: BearTheme.cardBorder
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: tagText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData.trim()
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.pixelSize: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 1
|
||||||
|
color: BearTheme.cardBorder
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links / URLs
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Links")
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "🔗 " + qsTr("Open homepage")
|
||||||
|
Layout.fillWidth: true
|
||||||
|
enabled: root.activeStation && root.activeStation.homepage && root.activeStation.homepage.startsWith("http")
|
||||||
|
onClicked: {
|
||||||
|
if (root.activeStation && root.activeStation.homepage) {
|
||||||
|
Qt.openUrlExternally(root.activeStation.homepage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "📋 " + qsTr("Copy stream URL")
|
||||||
|
Layout.fillWidth: true
|
||||||
|
enabled: root.activeStation && (root.activeStation.urlResolved || root.activeStation.url)
|
||||||
|
onClicked: {
|
||||||
|
if (root.activeStation) {
|
||||||
|
var url = root.activeStation.urlResolved || root.activeStation.url
|
||||||
|
root.copyToClipboard(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty state if no station is active
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 12
|
||||||
|
visible: root.activeStation === null
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: "📻"
|
||||||
|
font.pixelSize: 48
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("No station selected")
|
||||||
|
color: BearTheme.textMain
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 14
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Select a station from the list\nto view details.")
|
||||||
|
color: BearTheme.textMuted
|
||||||
|
font.pixelSize: 12
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ Item {
|
|||||||
ListView {
|
ListView {
|
||||||
id: stationList
|
id: stationList
|
||||||
width: stationScrollView.availableWidth
|
width: stationScrollView.availableWidth
|
||||||
spacing: 8
|
spacing: 0
|
||||||
model: app.activeModel()
|
model: app.activeModel()
|
||||||
visible: count > 0
|
visible: count > 0
|
||||||
|
|
||||||
@@ -56,4 +56,4 @@ Item {
|
|||||||
color: BearTheme.textMuted
|
color: BearTheme.textMuted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ StationCard 1.0 StationCard.qml
|
|||||||
WorldCategoryHeader 1.0 WorldCategoryHeader.qml
|
WorldCategoryHeader 1.0 WorldCategoryHeader.qml
|
||||||
WorldCategories 1.0 WorldCategories.qml
|
WorldCategories 1.0 WorldCategories.qml
|
||||||
PlayerBar 1.0 PlayerBar.qml
|
PlayerBar 1.0 PlayerBar.qml
|
||||||
AboutDialog 1.0 AboutDialog.qml
|
AboutPage 1.0 AboutPage.qml
|
||||||
AddStationDialog 1.0 AddStationDialog.qml
|
AddStationDialog 1.0 AddStationDialog.qml
|
||||||
EditStationDialog 1.0 EditStationDialog.qml
|
EditStationDialog 1.0 EditStationDialog.qml
|
||||||
LoadingOverlay 1.0 LoadingOverlay.qml
|
LoadingOverlay 1.0 LoadingOverlay.qml
|
||||||
ToastPopup 1.0 ToastPopup.qml
|
ToastPopup 1.0 ToastPopup.qml
|
||||||
StationListPanel 1.0 StationListPanel.qml
|
StationListPanel 1.0 StationListPanel.qml
|
||||||
|
SidebarNavigation 1.0 SidebarNavigation.qml
|
||||||
|
SidebarButton 1.0 SidebarButton.qml
|
||||||
|
StationDetailsPanel 1.0 StationDetailsPanel.qml
|
||||||
|
|||||||
@@ -5,15 +5,17 @@ pragma Singleton
|
|||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
readonly property color bgA: "#0f141b"
|
readonly property color bgA: "#1d1d22"
|
||||||
readonly property color bgB: "#131b25"
|
readonly property color bgB: "#202027"
|
||||||
readonly property color panel: "#182433"
|
readonly property color panel: "#22232a"
|
||||||
readonly property color card: "#1b2a3d"
|
readonly property color sidebar: "#2b2c33"
|
||||||
readonly property color cardHover: "#223654"
|
readonly property color card: "#2a2b31"
|
||||||
readonly property color cardBorder: "#2d4566"
|
readonly property color cardHover: "#33343b"
|
||||||
readonly property color accent: "#2bb0ff"
|
readonly property color cardBorder: "#3a3b43"
|
||||||
readonly property color textMain: "#eaf1fb"
|
readonly property color accent: "#d7d7df"
|
||||||
readonly property color textMuted: "#9eb1c9"
|
readonly property color playingAccent: "#0a84ff"
|
||||||
|
readonly property color textMain: "#f0f0f4"
|
||||||
|
readonly property color textMuted: "#a3a4ad"
|
||||||
readonly property color warn: "#ff8b8b"
|
readonly property color warn: "#ff8b8b"
|
||||||
|
|
||||||
readonly property var worldTags: [
|
readonly property var worldTags: [
|
||||||
@@ -30,4 +32,4 @@ QtObject {
|
|||||||
{ name: qsTr("Ambient"), tag: "ambient", icon: "🌌" },
|
{ name: qsTr("Ambient"), tag: "ambient", icon: "🌌" },
|
||||||
{ name: qsTr("Blues / Soul"), tag: "blues", icon: "🎺" }
|
{ name: qsTr("Blues / Soul"), tag: "blues", icon: "🎺" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ void QrcResourcesTest::qmlModulesAreEmbedded()
|
|||||||
QVERIFY(QFile::exists(QStringLiteral(":/qml/theme/qmldir")));
|
QVERIFY(QFile::exists(QStringLiteral(":/qml/theme/qmldir")));
|
||||||
QVERIFY(QFile::exists(QStringLiteral(":/qml/components/qmldir")));
|
QVERIFY(QFile::exists(QStringLiteral(":/qml/components/qmldir")));
|
||||||
QVERIFY(QFile::exists(QStringLiteral(":/assets/app/bearwave.png")));
|
QVERIFY(QFile::exists(QStringLiteral(":/assets/app/bearwave.png")));
|
||||||
|
QVERIFY(QFile::exists(QStringLiteral(":/assets/legal/gpl-3.0.txt")));
|
||||||
}
|
}
|
||||||
|
|
||||||
QTEST_APPLESS_MAIN(QrcResourcesTest)
|
QTEST_APPLESS_MAIN(QrcResourcesTest)
|
||||||
#include "qrc_resources_test.moc"
|
#include "qrc_resources_test.moc"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user