diff --git a/.gitignore b/.gitignore index f5ed8fd..2a4c6ad 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,11 @@ build.md ## Build directories build/ +build-about/ build-ci/ +build-fix/ +build-maclayout/ +build-test/ build-arch-ci/ build-dir/ cmake-build-*/ @@ -36,6 +40,11 @@ test-*.json pkg/ src/bearwave/ bearwave/ +flatpak_repo/ +.flatpak-builder/ +.flatpak-builder-manifest.json +scratch/ +spalencsar-README.md *.pkg.tar.zst *.pkg.tar.zst.sig *.pkg.tar.xz diff --git a/CHANGELOG.md b/CHANGELOG.md index 098f752..e8e08c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,14 +10,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - 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 - Flatpak repository signing key replaced with `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 - 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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 92fa3b7..59f23b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,6 +37,14 @@ If you changed 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: ```bash @@ -51,6 +59,7 @@ ctest --test-dir build --output-on-failure - Keep network operations asynchronous. - Preserve MPRIS behavior and user state compatibility. - 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 @@ -63,6 +72,7 @@ Please smoke-test the area you changed when possible: - favorites persist across restart - resume restores last station and volume - 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 diff --git a/README.md b/README.md index 63ef8a2..e19b2c3 100644 --- a/README.md +++ b/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](screenshots/screen01.png) | ![Station Browser](screenshots/screen02.png) | -| ![About Dialog](screenshots/screen03.png) | ![Additional View](screenshots/screen04.png) | +| ![About Page](screenshots/screen03.png) | ![Additional View](screenshots/screen04.png) | | ![World View](screenshots/screen05.png) | | 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 - system tray integration for background playback - 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 @@ -216,14 +217,14 @@ Add distributions here only after explicit testing, not by assumption alone. ### 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 - 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 -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 @@ -290,6 +291,16 @@ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release 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 From the build directory: @@ -425,13 +436,14 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for local build and review expectations. ## Development Notes - 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` - backend orchestration: `src/radiobackend.cpp` - stream playback: `src/bearplayer.cpp` - API layer: `src/radiobrowser.cpp` - MPRIS adapter: `src/mprisadaptor.cpp` - desktop notifications: `src/notificationmanager.cpp` +- embedded GPL license resource: `qrc:/assets/legal/gpl-3.0.txt` from `LICENSE` - unit tests: `tests/` See [CHANGELOG.md](CHANGELOG.md) for release history. For contributor and agent guardrails, see `AGENTS.md`. diff --git a/screenshots/screen6.png b/screenshots/screen6.png new file mode 100644 index 0000000..4be2877 Binary files /dev/null and b/screenshots/screen6.png differ diff --git a/src/main.cpp b/src/main.cpp index ca7f9ac..b29a70a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -56,9 +57,17 @@ int main(int argc, char *argv[]) engine.addImportPath(QStringLiteral("qrc:/qml")); 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("bearwaveVersion", QStringLiteral(BEARWAVE_VERSION)); engine.rootContext()->setContextProperty("bearwaveBuildId", QStringLiteral(BEARWAVE_GIT_HASH)); + engine.rootContext()->setContextProperty("bearwaveLicenseText", licenseText); auto *mprisRoot = new MprisRootAdaptor(&backend, &app); auto *mprisPlayer = new MprisPlayerAdaptor(&backend); diff --git a/src/qml.qrc b/src/qml.qrc index f9a0eb5..95fcfdb 100644 --- a/src/qml.qrc +++ b/src/qml.qrc @@ -11,12 +11,15 @@ qml/components/WorldCategoryHeader.qml qml/components/WorldCategories.qml qml/components/PlayerBar.qml - qml/components/AboutDialog.qml + qml/components/AboutPage.qml qml/components/AddStationDialog.qml qml/components/EditStationDialog.qml qml/components/LoadingOverlay.qml qml/components/ToastPopup.qml qml/components/StationListPanel.qml + qml/components/SidebarNavigation.qml + qml/components/SidebarButton.qml + qml/components/StationDetailsPanel.qml qml/utils/FlagUtils.js @@ -25,5 +28,6 @@ ../assets/ui/globe.svg ../assets/ui/github.svg ../assets/ui/linkedin.svg + ../LICENSE - \ No newline at end of file + diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 0b1b127..531edef 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -11,9 +11,9 @@ import components 1.0 ApplicationWindow { id: root title: qsTr("BearWave") - width: 980 - height: 700 - minimumWidth: 520 + width: 1400 + height: 720 + minimumWidth: 900 minimumHeight: 460 visible: true @@ -26,7 +26,9 @@ ApplicationWindow { property var backend: (typeof radioBackend !== "undefined" ? radioBackend : null) readonly property string appVersion: (typeof bearwaveVersion !== "undefined" ? ("" + bearwaveVersion) : Qt.application.version) 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 string activeQuickFilter: "" property string selectedWorldCategory: "" @@ -36,12 +38,54 @@ ApplicationWindow { property alias stationList: stationPanel.stationList property alias addDialog: addDialogPane property alias editDialog: editDialogPane - property alias aboutDialog: aboutDialogPane function toast(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() { searchTimer.stop() if (searchField.text !== "") { @@ -85,6 +129,9 @@ ApplicationWindow { onCurrentPageChanged: { contentOpacity = 0.85 contentFadeRestart.restart() + if (currentPage !== "about") { + selectedStation = null + } if (currentPage !== "search") { resetSearchFilter() } @@ -111,34 +158,61 @@ ApplicationWindow { Rectangle { anchors.fill: parent - gradient: Gradient { - GradientStop { position: 0.0; color: BearTheme.bgA } - GradientStop { position: 1.0; color: BearTheme.bgB } - } + color: BearTheme.bgA } - ColumnLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 10 + SidebarNavigation { + id: sidebarNav + visible: !root.compactMode + 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 { - Layout.fillWidth: true - Layout.preferredHeight: headerContent.implicitHeight + 20 - radius: 12 + id: headerPanel + height: currentPage === "about" && !root.compactMode ? 1 : headerContent.implicitHeight + 18 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + radius: 0 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 { id: headerContent anchors.fill: parent - anchors.margins: 10 + anchors.margins: 9 spacing: 8 + visible: !(currentPage === "about" && !root.compactMode) HeaderNavigation { Layout.fillWidth: true app: root compactMode: root.compactMode + visible: root.compactMode } Rectangle { @@ -146,6 +220,7 @@ ApplicationWindow { Layout.preferredHeight: 1 color: BearTheme.cardBorder opacity: 0.6 + visible: root.compactMode } SearchToolbar { @@ -154,12 +229,14 @@ ApplicationWindow { app: root compactMode: root.compactMode searchTimer: searchTimer + visible: currentPage !== "about" } QuickFilters { Layout.fillWidth: true app: root compactMode: root.compactMode + visible: currentPage !== "about" } Label { @@ -173,12 +250,23 @@ ApplicationWindow { } } - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - radius: 12 - color: BearTheme.panel - border.color: BearTheme.cardBorder + PlayerBar { + id: playerPanel + height: implicitHeight + anchors.left: parent.left + anchors.right: parent.right + 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 opacity: contentOpacity @@ -186,37 +274,74 @@ ApplicationWindow { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } } - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - spacing: 10 + Rectangle { + id: stationPanelFrame + visible: currentPage !== "about" + 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 { - Layout.fillWidth: true - visible: currentPage === "world" && selectedWorldCategory !== "" - app: root + Rectangle { + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + width: 1 + color: BearTheme.cardBorder + visible: !root.compactMode } - StationListPanel { - id: stationPanel - Layout.fillWidth: true - Layout.fillHeight: true - app: root - } + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 0 - WorldCategories { - Layout.fillWidth: true - Layout.fillHeight: true - visible: currentPage === "world" && selectedWorldCategory === "" - app: root - compactMode: root.compactMode + WorldCategoryHeader { + Layout.fillWidth: true + visible: currentPage === "world" && selectedWorldCategory !== "" + app: root + } + + 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 { - Layout.fillWidth: true - app: root + StationDetailsPanel { + id: stationDetails + 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 } - AboutDialog { - id: aboutDialogPane - compactMode: root.compactMode - appVersion: root.appVersion - buildId: root.appBuildId - } - ToastPopup { id: toastPopup window: root diff --git a/src/qml/components/AboutDialog.qml b/src/qml/components/AboutDialog.qml deleted file mode 100644 index 94f3363..0000000 --- a/src/qml/components/AboutDialog.qml +++ /dev/null @@ -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 ." - } - } - } - } - } -} \ No newline at end of file diff --git a/src/qml/components/AboutPage.qml b/src/qml/components/AboutPage.qml new file mode 100644 index 0000000..1fc214c --- /dev/null +++ b/src/qml/components/AboutPage.qml @@ -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" + } + } + } + } + } + } + } +} diff --git a/src/qml/components/HeaderNavigation.qml b/src/qml/components/HeaderNavigation.qml index 6d2dceb..750b64e 100644 --- a/src/qml/components/HeaderNavigation.qml +++ b/src/qml/components/HeaderNavigation.qml @@ -13,121 +13,13 @@ Item { required property var app required property bool compactMode - implicitWidth: compactMode ? compactNav.implicitWidth : desktopNav.implicitWidth - implicitHeight: compactMode ? compactNav.implicitHeight : desktopNav.implicitHeight - - function loadTop() { - if (!app.backend) return - app.currentPage = "top" - app.activeQuickFilter = "" - app.backend.loadTopStations() - } - - function loadGerman() { - if (!app.backend) return - app.currentPage = "german" - app.activeQuickFilter = "" - app.backend.loadGermanStations() - } - - function loadDutch() { - if (!app.backend) return - app.currentPage = "dutch" - app.activeQuickFilter = "" - app.backend.loadDutchStations() - } - - function showFavorites() { - app.currentPage = "favorites" - app.activeQuickFilter = "" - } - - function showHistory() { - app.currentPage = "history" - app.activeQuickFilter = "" - } - - RowLayout { - id: desktopNav - anchors.left: parent.left - anchors.right: parent.right - visible: !root.compactMode - spacing: 8 - - Image { - Layout.preferredWidth: 112 - Layout.preferredHeight: 40 - source: "qrc:/assets/app/bearwave_line.png" - fillMode: Image.PreserveAspectFit - smooth: true - mipmap: true - } - - Rectangle { - Layout.preferredWidth: 1 - Layout.fillHeight: true - color: BearTheme.cardBorder - opacity: 0.6 - } - - Button { - text: qsTr("Top") - highlighted: app.currentPage === "top" - onClicked: loadTop() - } - - Button { - text: qsTr("DE") - highlighted: app.currentPage === "german" - onClicked: loadGerman() - } - - Button { - text: qsTr("NL") - highlighted: app.currentPage === "dutch" - onClicked: loadDutch() - } - - Button { - text: qsTr("Favorites") - highlighted: app.currentPage === "favorites" - onClicked: showFavorites() - } - - Button { - text: qsTr("History") - highlighted: app.currentPage === "history" - onClicked: showHistory() - } - - Item { Layout.fillWidth: true } - - Button { - text: qsTr("Manual +") - onClicked: app.addDialog.open() - } - - Button { - text: qsTr("About") - onClicked: app.aboutDialog.open() - } - - Button { - visible: app.backend && app.backend.canResumeLastStation - text: qsTr("Resume") - onClicked: { - if (app.backend) { - app.backend.resumeLastStation() - } - } - } - } + implicitWidth: compactNav.implicitWidth + implicitHeight: compactNav.implicitHeight ColumnLayout { id: compactNav anchors.left: parent.left anchors.right: parent.right - visible: root.compactMode spacing: 8 RowLayout { @@ -152,7 +44,8 @@ Item { Button { text: qsTr("About") - onClicked: app.aboutDialog.open() + highlighted: app.currentPage === "about" + onClicked: app.navigateToAbout() } Button { @@ -174,31 +67,31 @@ Item { Button { text: qsTr("Top") highlighted: app.currentPage === "top" - onClicked: loadTop() + onClicked: app.navigateToTop() } Button { text: qsTr("DE") highlighted: app.currentPage === "german" - onClicked: loadGerman() + onClicked: app.navigateToGerman() } Button { text: qsTr("NL") highlighted: app.currentPage === "dutch" - onClicked: loadDutch() + onClicked: app.navigateToDutch() } Button { text: qsTr("Favorites") highlighted: app.currentPage === "favorites" - onClicked: showFavorites() + onClicked: app.navigateToFavorites() } Button { text: qsTr("History") highlighted: app.currentPage === "history" - onClicked: showHistory() + onClicked: app.navigateToHistory() } } } diff --git a/src/qml/components/PlayerBar.qml b/src/qml/components/PlayerBar.qml index 6137ddc..120eb44 100644 --- a/src/qml/components/PlayerBar.qml +++ b/src/qml/components/PlayerBar.qml @@ -12,23 +12,34 @@ Rectangle { required property var app - implicitHeight: 108 - radius: 12 + implicitHeight: 76 + radius: 0 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 { anchors.fill: parent - anchors.margins: 10 + anchors.leftMargin: 12 + anchors.rightMargin: 12 + anchors.topMargin: 9 + anchors.bottomMargin: 9 spacing: 12 Rectangle { - Layout.preferredWidth: 88 - Layout.preferredHeight: 88 - radius: 8 - color: "#182637" + Layout.preferredWidth: 56 + Layout.preferredHeight: 56 + radius: 6 + color: "#34353b" clip: true - border.color: BearTheme.cardBorder + border.color: "transparent" Image { id: coverImage @@ -58,164 +69,122 @@ Rectangle { && app.backend.currentStation.favicon.startsWith("https://")) { return 8 } - return 16 + return 10 } } } ColumnLayout { Layout.fillWidth: true - Layout.fillHeight: true - spacing: 6 + Layout.alignment: Qt.AlignVCenter + spacing: 3 - RowLayout { + Label { Layout.fillWidth: true - spacing: 8 - - Label { - Layout.fillWidth: true - text: app.backend && app.backend.player - ? (app.backend.player.currentStationName || qsTr("No station selected")) - : qsTr("No station selected") - color: BearTheme.textMain - font.pixelSize: 16 - font.bold: true - elide: Text.ElideRight - } - - Rectangle { - visible: app.backend && app.backend.currentStation && app.backend.currentStation.codec - && app.backend.currentStation.codec !== "unknown" - && app.backend.currentStation.codec !== "" - height: 18 - width: codecLabel.implicitWidth + 12 - radius: 4 - color: "transparent" - border.color: BearTheme.accent - border.width: 1 - - Label { - id: codecLabel - anchors.centerIn: parent - text: (app.backend && app.backend.currentStation && app.backend.currentStation.codec) - ? app.backend.currentStation.codec.toUpperCase() : "" - color: BearTheme.accent - font.pixelSize: 9 - font.bold: true - } - } - - Rectangle { - visible: app.backend && app.backend.currentStation && app.backend.currentStation.bitrate > 0 - height: 18 - width: bitrateLabel.implicitWidth + 12 - radius: 4 - color: BearTheme.accent - border.color: BearTheme.accent - border.width: 1 - - Label { - id: bitrateLabel - anchors.centerIn: parent - text: (app.backend && app.backend.currentStation) - ? app.backend.currentStation.bitrate + " kbps" : "" - color: "#ffffff" - font.pixelSize: 9 - font.bold: true - } - } + text: app.backend && app.backend.player + ? (app.backend.player.currentStationName || qsTr("No station selected")) + : qsTr("No station selected") + color: BearTheme.textMain + font.pixelSize: 14 + font.bold: true + elide: Text.ElideRight } Label { Layout.fillWidth: true text: app.backend && app.backend.player && app.backend.player.currentNowPlaying.length > 0 ? (qsTr("Now playing: ") + app.backend.player.currentNowPlaying) - : qsTr("Now playing: No track info") + : qsTr("Live radio") color: BearTheme.textMuted font.pixelSize: 12 elide: Text.ElideRight } + } - RowLayout { - Layout.fillWidth: true - spacing: 8 + RowLayout { + Layout.alignment: Qt.AlignVCenter + spacing: 8 - Button { - text: "⏮" - Layout.preferredWidth: 44 - enabled: app.backend ? app.backend.hasPreviousStation() : false - onClicked: { - if (app.backend) { - app.backend.playPreviousStation() + Button { + text: "⏮" + Layout.preferredWidth: 42 + Layout.preferredHeight: 30 + enabled: app.backend ? app.backend.hasPreviousStation() : false + onClicked: { + if (app.backend) { + app.backend.playPreviousStation() + } + } + } + + Button { + text: app.backend && app.backend.player && app.backend.player.playing ? "⏸" : "▶" + Layout.preferredWidth: 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 { - text: app.backend && app.backend.player && app.backend.player.playing ? "⏸" : "▶" - Layout.preferredWidth: 44 - onClicked: { - if (app.backend && app.backend.player) { - app.backend.player.togglePlayPause() - } - } - } - - Button { - text: "⏹" - Layout.preferredWidth: 44 - onClicked: { - if (app.backend && app.backend.player) { - app.backend.player.stop() - } - } - } - - Button { - text: "⏭" - Layout.preferredWidth: 44 - enabled: app.backend ? app.backend.hasNextStation() : false - onClicked: { - if (app.backend) { - app.backend.playNextStation() - } - } - } - - Button { - id: muteButton - text: (app.backend && app.backend.player && app.backend.player.volume > 0) ? "🔊" : "🔇" - Layout.preferredWidth: 44 - property real lastVolume: 0.5 - - ToolTip.visible: hovered - ToolTip.text: (app.backend && app.backend.player && app.backend.player.volume > 0) - ? qsTr("Mute") : qsTr("Unmute") - - onClicked: { - if (app.backend && app.backend.player) { - if (app.backend.player.volume > 0) { - lastVolume = app.backend.player.volume - app.backend.player.setVolume(0) - } else { - app.backend.player.setVolume(lastVolume > 0 ? lastVolume : 0.5) - } - } - } - } - - Slider { - Layout.fillWidth: true - from: 0 - to: 1 - value: app.backend && app.backend.player ? app.backend.player.volume : 0.5 - onMoved: { - if (app.backend && app.backend.player) { - app.backend.player.setVolume(value) - } + Slider { + Layout.preferredWidth: app.width < 1180 ? 115 : 160 + 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) } } } } } -} \ No newline at end of file +} diff --git a/src/qml/components/QuickFilters.qml b/src/qml/components/QuickFilters.qml index 8e58392..5d797c7 100644 --- a/src/qml/components/QuickFilters.qml +++ b/src/qml/components/QuickFilters.qml @@ -22,6 +22,7 @@ ColumnLayout { app.backend.loadByTag(tag) } + // Load country by country code function loadCountry(code, page) { if (!app.backend) return app.currentPage = page @@ -40,72 +41,124 @@ ColumnLayout { } } - RowLayout { + Flow { visible: !root.compactMode Layout.fillWidth: true - spacing: 6 + Layout.preferredHeight: childrenRect.height + width: parent.width + spacing: 8 + // Left: Schnellzugriff filters Label { - text: qsTr("Genre:") + text: qsTr("Quick access") color: BearTheme.textMuted + font.bold: true font.pixelSize: 11 - rightPadding: 10 + width: 86 + height: 30 + verticalAlignment: Text.AlignVCenter } Button { + width: 72 + height: 30 text: qsTr("Rock") highlighted: app.activeQuickFilter === "tag:rock" onClicked: loadTag("rock", "genre-rock") } Button { + width: 72 + height: 30 text: qsTr("News") highlighted: app.activeQuickFilter === "tag:news" onClicked: loadTag("news", "genre-news") } Button { + width: 72 + height: 30 text: qsTr("Jazz") highlighted: app.activeQuickFilter === "tag:jazz" onClicked: loadTag("jazz", "genre-jazz") } - Item { Layout.preferredWidth: 20 } - - Label { - text: qsTr("Country:") - color: BearTheme.textMuted - font.pixelSize: 11 - rightPadding: 10 + Rectangle { + width: 1 + height: 18 + color: BearTheme.cardBorder + opacity: 0.6 } Button { + width: 62 + height: 30 text: qsTr("US") highlighted: app.activeQuickFilter === "cc:US" onClicked: loadCountry("US", "country-us") } Button { - text: qsTr("UK") + width: 62 + height: 30 + text: qsTr("GB") highlighted: app.activeQuickFilter === "cc:GB" onClicked: loadCountry("GB", "country-gb") } Button { + width: 62 + height: 30 text: qsTr("FR") highlighted: app.activeQuickFilter === "cc:FR" onClicked: loadCountry("FR", "country-fr") } - Button { - text: qsTr("WORLD") - highlighted: app.activeQuickFilter === "world" - onClicked: openWorld() + Label { + text: qsTr("Sort") + color: BearTheme.textMuted + 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 { visible: root.compactMode Layout.fillWidth: true @@ -187,4 +240,4 @@ ColumnLayout { } } } -} \ No newline at end of file +} diff --git a/src/qml/components/SearchToolbar.qml b/src/qml/components/SearchToolbar.qml index c55f1ab..c06b33c 100644 --- a/src/qml/components/SearchToolbar.qml +++ b/src/qml/components/SearchToolbar.qml @@ -21,12 +21,13 @@ ColumnLayout { RowLayout { visible: !root.compactMode Layout.fillWidth: true - spacing: 8 + spacing: 10 TextField { id: searchField Layout.fillWidth: true - placeholderText: qsTr("Search stations (name, genre, country)") + Layout.minimumWidth: 180 + placeholderText: qsTr("Search stations (name, genre, country)...") onTextChanged: { if (app.backend) { app.backend.filterQuery = text @@ -40,44 +41,35 @@ ColumnLayout { } } - Button { - text: qsTr("Search") - highlighted: true - onClicked: { - if (searchField.text.length < 2 || !app.backend) return - app.currentPage = "search" - app.backend.searchStations(searchField.text) - } - } + // Right Search and Clear buttons + RowLayout { + spacing: 6 - Button { - text: qsTr("Sort A-Z") - onClicked: { - if (app.backend && app.currentPage !== "favorites") { - app.backend.sortStations("name") + Button { + text: root.app.width < 1180 ? "🔍" : "🔍 " + qsTr("Search") + highlighted: true + Layout.preferredWidth: root.app.width < 1180 ? 44 : 88 + onClicked: { + if (searchField.text.length < 2 || !app.backend) return + app.currentPage = "search" + app.backend.searchStations(searchField.text) } } - } - Button { - text: qsTr("Sort Bitrate") - onClicked: { - if (app.backend && app.currentPage !== "favorites") { - app.backend.sortStations("bitrate") - } - } - } - - Button { - text: qsTr("Sort Votes") - onClicked: { - if (app.backend && app.currentPage !== "favorites") { - app.backend.sortStations("votes") + Button { + text: root.app.width < 1180 ? "✕" : "✕ " + qsTr("Clear") + Layout.preferredWidth: root.app.width < 1180 ? 44 : 82 + onClicked: { + searchField.text = "" + if (app.backend) { + app.backend.filterQuery = "" + } } } } } + // Compact Mode (Mobile/Narrow) Search Layout ColumnLayout { visible: root.compactMode Layout.fillWidth: true @@ -108,59 +100,27 @@ ColumnLayout { Layout.fillWidth: true spacing: 8 - Item { Layout.fillWidth: true } - Button { - text: qsTr("Search") + text: "🔍 " + qsTr("Search") highlighted: true + Layout.fillWidth: true onClicked: { - if (compactSearchField.text.length < 2 || !app.backend) return + if (searchField.text.length < 2 || !app.backend) return app.currentPage = "search" - app.backend.searchStations(compactSearchField.text) - } - } - } - - Flow { - Layout.fillWidth: true - width: parent.width - spacing: 8 - - Button { - text: "A-Z" - onClicked: { - if (app.backend && app.currentPage !== "favorites") { - app.backend.sortStations("name") - } + app.backend.searchStations(searchField.text) } } Button { - text: "kb" + text: "✕ " + qsTr("Clear") + Layout.fillWidth: true onClicked: { - if (app.backend && app.currentPage !== "favorites") { - app.backend.sortStations("bitrate") - } - } - } - - Button { - text: "❤" - onClicked: { - if (app.backend && app.currentPage !== "favorites") { - app.backend.sortStations("votes") + searchField.text = "" + if (app.backend) { + app.backend.filterQuery = "" } } } } } - - 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 - } -} \ No newline at end of file +} diff --git a/src/qml/components/SidebarButton.qml b/src/qml/components/SidebarButton.qml new file mode 100644 index 0000000..2b62072 --- /dev/null +++ b/src/qml/components/SidebarButton.qml @@ -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 + } + } +} diff --git a/src/qml/components/SidebarNavigation.qml b/src/qml/components/SidebarNavigation.qml new file mode 100644 index 0000000..a99a90c --- /dev/null +++ b/src/qml/components/SidebarNavigation.qml @@ -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() + } + } + } + } + } +} \ No newline at end of file diff --git a/src/qml/components/StationCard.qml b/src/qml/components/StationCard.qml index 1a3c27c..32030d8 100644 --- a/src/qml/components/StationCard.qml +++ b/src/qml/components/StationCard.qml @@ -16,9 +16,9 @@ Rectangle { required property bool compactMode required property real listWidth - height: compactMode ? 72 : 78 + height: 52 width: listWidth - radius: 10 + radius: 0 readonly property bool isCurrent: { 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 color: isCurrent - ? (cardMouse.containsMouse ? "#1d3350" : "#16283e") - : (cardMouse.containsMouse ? BearTheme.cardHover : BearTheme.card) - border.color: isCurrent ? BearTheme.accent : BearTheme.cardBorder - border.width: isCurrent ? 2 : 1 + ? "#4a4a4d" + : (cardMouse.containsMouse ? "#2d2e34" : "transparent") + + 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() { if (!app.backend) return @@ -56,6 +70,7 @@ Rectangle { hoverEnabled: true acceptedButtons: Qt.LeftButton onClicked: { + app.selectedStation = root.modelData if (!app.backend) return if (root.isCurrent) { 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 { anchors.fill: parent - anchors.margins: 10 + anchors.leftMargin: 8 + anchors.rightMargin: 8 spacing: 10 + // Favicon Rect Rectangle { - Layout.preferredWidth: 44 - Layout.preferredHeight: 44 - radius: 8 - color: "#123154" - border.color: BearTheme.accent + Layout.preferredWidth: 34 + Layout.preferredHeight: 34 + radius: 5 + color: "#34353b" + clip: true Image { id: stationFavicon anchors.fill: parent - anchors.margins: 3 + anchors.margins: 2 source: (root.modelData.favicon && root.modelData.favicon.startsWith("https://")) ? root.modelData.favicon : "" @@ -100,9 +126,11 @@ Rectangle { } } + // Info Column ColumnLayout { Layout.fillWidth: true - spacing: 2 + spacing: 1 + Layout.alignment: Qt.AlignVCenter RowLayout { Layout.fillWidth: true @@ -113,10 +141,11 @@ Rectangle { text: root.modelData.name color: root.isCurrent ? BearTheme.accent : BearTheme.textMain font.bold: true - font.pixelSize: compactMode ? 13 : 14 + font.pixelSize: 13 elide: Text.ElideRight } + // Tiny EQ Indicator Row { id: eqAnimation spacing: 2 @@ -126,32 +155,26 @@ Rectangle { Rectangle { id: bar1 width: 2 - height: 12 - color: BearTheme.accent + height: 10 + color: BearTheme.playingAccent radius: 1 - Behavior on height { - NumberAnimation { duration: 120 } - } + Behavior on height { NumberAnimation { duration: 120 } } } Rectangle { id: bar2 width: 2 - height: 12 - color: BearTheme.accent + height: 10 + color: BearTheme.playingAccent radius: 1 - Behavior on height { - NumberAnimation { duration: 120 } - } + Behavior on height { NumberAnimation { duration: 120 } } } Rectangle { id: bar3 width: 2 - height: 12 - color: BearTheme.accent + height: 10 + color: BearTheme.playingAccent radius: 1 - Behavior on height { - NumberAnimation { duration: 120 } - } + Behavior on height { NumberAnimation { duration: 120 } } } Timer { @@ -159,17 +182,17 @@ Rectangle { running: root.isPlaying repeat: true onTriggered: { - bar1.height = Math.floor(Math.random() * 11) + 3 - bar2.height = Math.floor(Math.random() * 11) + 3 - bar3.height = Math.floor(Math.random() * 11) + 3 + bar1.height = Math.floor(Math.random() * 9) + 2 + bar2.height = Math.floor(Math.random() * 9) + 2 + bar3.height = Math.floor(Math.random() * 9) + 2 } } onVisibleChanged: { if (!visible) { - bar1.height = 12 - bar2.height = 12 - bar3.height = 12 + bar1.height = 10 + bar2.height = 10 + bar3.height = 10 } } } @@ -177,21 +200,20 @@ Rectangle { Label { Layout.fillWidth: true - text: root.modelData.country + " • " - + (root.modelData.codec && root.modelData.codec !== "unknown" && root.modelData.codec !== "" - ? root.modelData.codec.toUpperCase() + " • " : "") - + (root.modelData.bitrate > 0 ? root.modelData.bitrate + " kbps" : qsTr("Stream")) + text: root.metadataText color: BearTheme.textMuted font.pixelSize: 11 elide: Text.ElideRight } } + // Edit Button (Custom User Stations Only) Button { visible: !root.modelData.uuid - Layout.preferredWidth: compactMode ? 34 : 40 - Layout.preferredHeight: 40 + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 text: "✏" + flat: true ToolTip.visible: hovered ToolTip.text: qsTr("Edit Station") onClicked: { @@ -200,10 +222,13 @@ Rectangle { } } + // Favorite Button Button { - Layout.preferredWidth: compactMode ? 34 : 40 - Layout.preferredHeight: 40 + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 text: root.modelData.isFavorite ? "★" : "☆" + flat: true + font.pixelSize: 14 onClicked: { if (!app.backend) return app.backend.toggleFavoriteById(root.modelData.uuid, root.modelData.urlResolved) @@ -211,10 +236,13 @@ Rectangle { } } + // Play/Pause Button Button { - Layout.preferredWidth: compactMode ? 34 : 40 - Layout.preferredHeight: 40 + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 text: (root.isCurrent && app.backend && app.backend.player && app.backend.player.playing) ? "⏸" : "▶" + flat: true + font.pixelSize: 13 onClicked: { if (!app.backend) return if (root.isCurrent) { @@ -225,4 +253,4 @@ Rectangle { } } } -} \ No newline at end of file +} diff --git a/src/qml/components/StationDetailsPanel.qml b/src/qml/components/StationDetailsPanel.qml new file mode 100644 index 0000000..0148b9e --- /dev/null +++ b/src/qml/components/StationDetailsPanel.qml @@ -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 + } + } +} diff --git a/src/qml/components/StationListPanel.qml b/src/qml/components/StationListPanel.qml index 5ae17d1..5a41d31 100644 --- a/src/qml/components/StationListPanel.qml +++ b/src/qml/components/StationListPanel.qml @@ -26,7 +26,7 @@ Item { ListView { id: stationList width: stationScrollView.availableWidth - spacing: 8 + spacing: 0 model: app.activeModel() visible: count > 0 @@ -56,4 +56,4 @@ Item { color: BearTheme.textMuted } } -} \ No newline at end of file +} diff --git a/src/qml/components/qmldir b/src/qml/components/qmldir index 70e0b82..90e1946 100644 --- a/src/qml/components/qmldir +++ b/src/qml/components/qmldir @@ -5,9 +5,12 @@ StationCard 1.0 StationCard.qml WorldCategoryHeader 1.0 WorldCategoryHeader.qml WorldCategories 1.0 WorldCategories.qml PlayerBar 1.0 PlayerBar.qml -AboutDialog 1.0 AboutDialog.qml +AboutPage 1.0 AboutPage.qml AddStationDialog 1.0 AddStationDialog.qml EditStationDialog 1.0 EditStationDialog.qml LoadingOverlay 1.0 LoadingOverlay.qml ToastPopup 1.0 ToastPopup.qml -StationListPanel 1.0 StationListPanel.qml \ No newline at end of file +StationListPanel 1.0 StationListPanel.qml +SidebarNavigation 1.0 SidebarNavigation.qml +SidebarButton 1.0 SidebarButton.qml +StationDetailsPanel 1.0 StationDetailsPanel.qml diff --git a/src/qml/theme/BearTheme.qml b/src/qml/theme/BearTheme.qml index 2fe0fb4..63ac6ea 100644 --- a/src/qml/theme/BearTheme.qml +++ b/src/qml/theme/BearTheme.qml @@ -5,15 +5,17 @@ pragma Singleton import QtQuick 2.15 QtObject { - readonly property color bgA: "#0f141b" - readonly property color bgB: "#131b25" - readonly property color panel: "#182433" - readonly property color card: "#1b2a3d" - readonly property color cardHover: "#223654" - readonly property color cardBorder: "#2d4566" - readonly property color accent: "#2bb0ff" - readonly property color textMain: "#eaf1fb" - readonly property color textMuted: "#9eb1c9" + readonly property color bgA: "#1d1d22" + readonly property color bgB: "#202027" + readonly property color panel: "#22232a" + readonly property color sidebar: "#2b2c33" + readonly property color card: "#2a2b31" + readonly property color cardHover: "#33343b" + readonly property color cardBorder: "#3a3b43" + readonly property color accent: "#d7d7df" + readonly property color playingAccent: "#0a84ff" + readonly property color textMain: "#f0f0f4" + readonly property color textMuted: "#a3a4ad" readonly property color warn: "#ff8b8b" readonly property var worldTags: [ @@ -30,4 +32,4 @@ QtObject { { name: qsTr("Ambient"), tag: "ambient", icon: "🌌" }, { name: qsTr("Blues / Soul"), tag: "blues", icon: "🎺" } ] -} \ No newline at end of file +} diff --git a/tests/qrc_resources_test.cpp b/tests/qrc_resources_test.cpp index e37a33d..2c07fe3 100644 --- a/tests/qrc_resources_test.cpp +++ b/tests/qrc_resources_test.cpp @@ -24,7 +24,8 @@ void QrcResourcesTest::qmlModulesAreEmbedded() QVERIFY(QFile::exists(QStringLiteral(":/qml/theme/qmldir"))); QVERIFY(QFile::exists(QStringLiteral(":/qml/components/qmldir"))); QVERIFY(QFile::exists(QStringLiteral(":/assets/app/bearwave.png"))); + QVERIFY(QFile::exists(QStringLiteral(":/assets/legal/gpl-3.0.txt"))); } QTEST_APPLESS_MAIN(QrcResourcesTest) -#include "qrc_resources_test.moc" \ No newline at end of file +#include "qrc_resources_test.moc" diff --git a/translations/bearwave_de.ts b/translations/bearwave_de.ts index 82dbec3..bb21ef3 100644 --- a/translations/bearwave_de.ts +++ b/translations/bearwave_de.ts @@ -1,555 +1,875 @@ - - + - - AboutDialog - - - Version - - - - - build - - - - - BearWave - BearWave - - - - Internet Radio Player for KDE - Internet Radio Player für KDE - - - - Author: Sebastian Palencsár - Autor: Sebastian Palencsár - - - - Open website - Website öffnen - - - - Open GitHub - GitHub öffnen - - - - Open LinkedIn - LinkedIn öffnen - - - - GNU GPLv3 License - GNU GPLv3-Lizenz - - - - Copyright (c) 2026 - Copyright (c) 2026 - - - - AddStationDialog - - - Add station manually - Sender manuell hinzufügen - - - - Name - Name - - - - Stream URL (http/https) - Stream URL (http/https) - - - - Country (optional) - Land (optional) - - - - Station added - Sender hinzugefügt - - - - BearTheme - - - Pop - - - - - Rock - Rock - - - - Electronic - - - - - Classical - - - - - Jazz - Jazz - - - - Metal - - - - - Hip Hop - - - - - Chillout - - - - - News / Talk - - - - - Soundtracks - - - - - Ambient - - - - - Blues / Soul - - - - - EditStationDialog - - - Edit station - - - - - Name - Name - - - - Stream URL (http/https) - Stream URL (http/https) - - - - Country (optional) - Land (optional) - - - - Manual - Manuell - - - - Station updated - - - - - HeaderNavigation - - - - Top - Top - - - - - DE - DE - - - - - NL - NL - - - - - Favorites - Favoriten - - - - - History - Verlauf - - - - Manual + - Manuell + - - - - - About - About - - - - Resume - Fortsetzen - - - - LoadingOverlay - - - Loading stations... - Lade Sender... - - - - Main - - - BearWave - BearWave - - - - Error: - Fehler: - - - - Top Global - Weltweite Top-Sender - - - - NotificationManager - - - Station: %1 - - - - - PlayerBar - - - - No station selected - Kein Sender ausgewählt - - - - Now playing: - Jetzt läuft: - - - - Now playing: No track info - Jetzt läuft: Keine Titelinfo - - - - Mute - - - - - Unmute - - - - - QuickFilters - - - - Genre: - Genre: - - - - - Rock - Rock - - - - - News - News - - - - - Jazz - Jazz - - - - - Country: - Land: - - - - - US - US - - - - - UK - UK - - - - - FR - FR - - - - - WORLD - WELT - - - - RadioBackend - - - - Stream URL must use http:// or https:// - - - - - - Manual - Manuell - - - - - - Last played - Zuletzt gehört - - - - RadioBrowser - - - Network error - Netzwerkfehler - - - - SearchToolbar - - - - Search stations (name, genre, country) - Sender suchen (Name, Genre, Land) - - - - - Search - Suche - - - - Sort A-Z - Sort A-Z - - - - Sort Bitrate - Sort Bitrate - - - - Sort Votes - Sort Votes - - - - Tip: you are not limited to DE/NL. Search worldwide by country, genre, or station name. - Tipp: Du bist nicht auf DE/NL begrenzt. Suche weltweit nach Ländern, Genres oder Sendernamen. - - - - StationCard - - - Stream - Stream - - - - Edit Station - - - - - Removed from favorites - Aus Favoriten entfernt - - - - Added to favorites - Zu Favoriten hinzugefügt - - - - StationListPanel - - - No playback history - Kein Wiedergabeverlauf - - - - No stations loaded yet - Noch keine Sender geladen - - - - Play some stations to build history - Spiele Sender ab, um einen Verlauf aufzubauen - - - - Load DE/NL stations or use search - Lade DE/NL oder nutze die Suche - - - - SystemTrayManager - - - BearWave - BearWave - - - - Pause - Pause - - - - Play - Wiedergabe - - - - Hide - Verstecken - - - - Show - Zeigen - - - - Quit - Beenden - - - - WorldCategories - - - Explore World Stations - Weltweite Sender entdecken - - - - Filter countries... - Länder filtern... - - - - Choose a country or music style to find radio stations from all over the world. - Wähle ein Land oder eine Musikrichtung, um Radiosender aus der ganzen Welt zu finden. - - - - Countries - Länder - - - - Filtered Countries - Gefilterte Länder - - - - Top 200 - - - - - stations - Sender - - - - Popular Music Styles - Beliebte Musikrichtungen - - - - WorldCategoryHeader - - - ← Back to Categories - ← Zurück zu den Kategorien - - - - World > Country: - Welt > Land: - - - - World > Genre: - Welt > Genre: - - - + + AboutDialog + + BearWave + BearWave + + + Internet Radio Player for KDE + Internet Radio Player für KDE + + + Author: Sebastian Palencsár + Autor: Sebastian Palencsár + + + Open website + Website öffnen + + + Open GitHub + GitHub öffnen + + + Open LinkedIn + LinkedIn öffnen + + + GNU GPLv3 License + GNU GPLv3-Lizenz + + + Copyright (c) 2026 + Copyright (c) 2026 + + + + AboutPage + + + Version + Version + + + + Build + Build + + + + Qt 6 + Qt 6 + + + + Application framework and UI runtime: Core, DBus, Network, Quick, Quick Controls 2, Widgets, Multimedia, and LinguistTools. + Anwendungsframework und UI-Laufzeit: Core, DBus, Network, Quick, Quick Controls 2, Widgets, Multimedia und LinguistTools. + + + + Subject to the upstream licenses of the installed Qt distribution. + Es gelten die Upstream-Lizenzen der installierten Qt-Distribution. + + + + Qt Multimedia backend + Qt-Multimedia-Backend + + + + Audio playback runs through Qt Multimedia and the backend available on the system, such as GStreamer or FFmpeg. + Die Audiowiedergabe läuft über Qt Multimedia und das auf dem System verfügbare Backend, zum Beispiel GStreamer oder FFmpeg. + + + + Backend components are provided by the operating system or Qt build and retain their own licenses. + Backend-Komponenten werden vom Betriebssystem oder Qt-Build bereitgestellt und behalten ihre eigenen Lizenzen. + + + + Radio Browser API + Radio Browser API + + + + Public station directory API for station data and stream discovery. BearWave does not bundle any Radio Browser server code. + Öffentliche Senderverzeichnis-API für Senderdaten und Stream-Erkennung. BearWave bündelt keinen Radio-Browser-Servercode. + + + + External service or API; subject to the upstream project and service terms. + Externer Dienst bzw. externe API; es gelten die Bedingungen des Upstream-Projekts und Dienstes. + + + + freedesktop.org integrations + freedesktop.org-Integrationen + + + + D-Bus, MPRIS, desktop notifications, desktop entry, and AppStream metadata. + D-Bus, MPRIS, Desktop-Benachrichtigungen, Desktop-Datei und AppStream-Metadaten. + + + + Uses standards and interfaces for desktop integration; no third-party implementation is bundled. + Genutzte Standards und Schnittstellen für Desktop-Integration; es wird keine fremde Implementierung gebündelt. + + + + Internet radio player for KDE + Internetradio-Player für KDE + + + + Author + Autor + + + + Sebastian Palencsár + Sebastian Palencsár + + + + Open website + Website öffnen + + + + Open GitHub + GitHub öffnen + + + + Open LinkedIn + LinkedIn öffnen + + + + Technologies and third-party components + Technologien und Drittkomponenten + + + + BearWave uses the following frameworks, services, and desktop standards. These components are not relicensed by BearWave. + BearWave nutzt die folgenden Frameworks, Dienste und Desktop-Standards. Diese Komponenten werden nicht durch BearWave neu lizenziert. + + + + GNU GPLv3 License + GNU GPLv3-Lizenz + + + + Copyright (c) 2026 + Copyright (c) 2026 + + + + License text could not be loaded. + Lizenztext konnte nicht geladen werden. + + + + AddStationDialog + + + Add station manually + Sender manuell hinzufügen + + + + Name + Name + + + + Stream URL (http/https) + Stream URL (http/https) + + + + Country (optional) + Land (optional) + + + + Station added + Sender hinzugefügt + + + + BearTheme + + + Pop + Pop + + + + Rock + Rock + + + + Electronic + Elektronisch + + + + Classical + Klassik + + + + Jazz + Jazz + + + + Metal + Metal + + + + Hip Hop + Hip Hop + + + + Chillout + Chillout + + + + News / Talk + News / Talk + + + + Soundtracks + Soundtracks + + + + Ambient + Ambient + + + + Blues / Soul + Blues / Soul + + + + EditStationDialog + + + Edit station + Sender bearbeiten + + + + Name + Name + + + + Stream URL (http/https) + Stream URL (http/https) + + + + Country (optional) + Land (optional) + + + + Manual + Manuell + + + + Station updated + Sender aktualisiert + + + + HeaderNavigation + + + Top + Top + + + + DE + DE + + + + NL + NL + + + + Favorites + Favoriten + + + + History + Verlauf + + + Manual + + Manuell + + + + + About + About + + + Resume + Fortsetzen + + + + LoadingOverlay + + + Loading stations... + Lade Sender... + + + + Main + + + BearWave + BearWave + + + + Error: + Fehler: + + + + Top Global + Weltweite Top-Sender + + + + PlayerBar + + + + No station selected + Kein Sender ausgewählt + + + + Now playing: + Jetzt läuft: + + + + Live radio + Live-Radio + + + Now playing: No track info + Jetzt läuft: Keine Titelinfo + + + + Mute + Stummschalten + + + + Unmute + Ton einschalten + + + + QuickFilters + + + Genre: + Genre: + + + + + Rock + Rock + + + + + News + News + + + + + Jazz + Jazz + + + + GB + GB + + + + Name + Name + + + + Bitrate + Bitrate + + + + Country: + Land: + + + + + US + US + + + + Quick access + Schnellzugriff + + + + Sort + Sortierung + + + + Votes + Stimmen + + + + UK + UK + + + + + FR + FR + + + + WORLD + WELT + + + + RadioBackend + + Manual + Manuell + + + Last played + Zuletzt gehört + + + + RadioBrowser + + Network error + Netzwerkfehler + + + + SearchToolbar + + + Search stations (name, genre, country)... + Sender suchen (Name, Genre, Land)... + + + + + Clear + Löschen + + + + Search stations (name, genre, country) + Sender suchen (Name, Genre, Land) + + + + + Search + Suche + + + Sort A-Z + Sort A-Z + + + Sort Bitrate + Sort Bitrate + + + Sort Votes + Sort Votes + + + Tip: you are not limited to DE/NL. Search worldwide by country, genre, or station name. + Tipp: Du bist nicht auf DE/NL begrenzt. Suche weltweit nach Ländern, Genres oder Sendernamen. + + + + SidebarNavigation + + + Stations + Sender + + + + Top Global + Weltweite Top-Sender + + + + Germany + Deutschland + + + + Netherlands + Niederlande + + + + Library + Mediathek + + + + Favorites + Favoriten + + + + History + Verlauf + + + + Worldwide + Weltweit + + + Search + Suche + + + STATIONS + SENDER + + + LIBRARY + MEDIATHEK + + + + Add Station + Sender hinzufügen + + + + About BearWave + Über BearWave + + + + Resume + Fortsetzen + + + + StationCard + + + votes + Stimmen + + + + Stream + Stream + + + + Edit Station + Sender bearbeiten + + + + Removed from favorites + Aus Favoriten entfernt + + + + Added to favorites + Zu Favoriten hinzugefügt + + + + StationDetailsPanel + + + Stream URL copied! + Stream-URL kopiert! + + + + Details + Details + + + Pause + Pause + + + Play + Abspielen + + + Favorite + Favorit + + + Add + Hinzufügen + + + + Pause + Pause + + + + Play + Abspielen + + + + Favorite + Favorit + + + + Add + Hinzufügen + + + + Removed from favorites + Aus Favoriten entfernt + + + + Added to favorites + Zu Favoriten hinzugefügt + + + + Groups + Gruppen + + + + Groups coming soon + Gruppen-Feature in Kürze verfügbar! + + + + Tags + Tags + + + + Links + Links + + + TECHNICAL DETAILS + TECHNISCHE DETAILS + + + + Codec: + Codec: + + + + + Unknown + Unbekannt + + + + Bitrate: + Bitrate: + + + + Votes: + Stimmen (Votes): + + + GENRES / TAGS + GENRES / TAGS + + + LINKS & ACTIONS + LINKS & AKTIONEN + + + + Open homepage + Homepage öffnen + + + + Copy stream URL + Stream-URL kopieren + + + + No station selected + Kein Sender ausgewählt + + + + Select a station from the list +to view details. + Wähle einen Sender aus der Liste, +um Details anzuzeigen. + + + + StationListPanel + + + No playback history + Kein Wiedergabeverlauf + + + + No stations loaded yet + Noch keine Sender geladen + + + + Play some stations to build history + Spiele Sender ab, um einen Verlauf aufzubauen + + + + Load DE/NL stations or use search + Lade DE/NL oder nutze die Suche + + + + SystemTrayManager + + BearWave + BearWave + + + Pause + Pause + + + Play + Wiedergabe + + + Hide + Verstecken + + + Show + Zeigen + + + Quit + Beenden + + + + WorldCategories + + + Explore World Stations + Weltweite Sender entdecken + + + + Filter countries... + Länder filtern... + + + + Choose a country or music style to find radio stations from all over the world. + Wähle ein Land oder eine Musikrichtung, um Radiosender aus der ganzen Welt zu finden. + + + + Countries + Länder + + + + Filtered Countries + Gefilterte Länder + + + + Top 200 + Top 200 + + + + stations + Sender + + + + Popular Music Styles + Beliebte Musikrichtungen + + + + WorldCategoryHeader + + + ← Back to Categories + ← Zurück zu den Kategorien + + + + World > Country: + Welt > Land: + + + + World > Genre: + Welt > Genre: + + + \ No newline at end of file diff --git a/translations/bearwave_ru.ts b/translations/bearwave_ru.ts index 3c71201..e13d5a7 100644 --- a/translations/bearwave_ru.ts +++ b/translations/bearwave_ru.ts @@ -1,555 +1,858 @@ - - + - - AboutDialog - - - Version - Версия - - - - build - сборка - - - - BearWave - BearWave - - - - Internet Radio Player for KDE - Интернет радиоплеер для KDE - - - - Author: Sebastian Palencsár - Автор: Sebastian Palencsár - - - - Open website - Перейти на веб-сайт - - - - Open GitHub - Перейти на GitHub - - - - Open LinkedIn - Перейти на LinkedIn - - - - GNU GPLv3 License - Лицензия GNU GPLv3 - - - - Copyright (c) 2026 - Copyright (c) 2026 - - - - AddStationDialog - - - Add station manually - Добавить станцию вручную - - - - Name - Название - - - - Stream URL (http/https) - Адрес канала (http/https) - - - - Country (optional) - Страна (необязательно) - - - - Station added - Станция добавлена - - - - BearTheme - - - Pop - Поп - - - - Rock - Рок - - - - Electronic - Электронная - - - - Classical - Классическая - - - - Jazz - Джаз - - - - Metal - Метал - - - - Hip Hop - Хип-хоп - - - - Chillout - Успокаивающая - - - - News / Talk - Новости / Разговоры - - - - Soundtracks - Саундтреки - - - - Ambient - Атмосфера - - - - Blues / Soul - Блюз / Соул - - - - EditStationDialog - - - Edit station - Редактировать станцию - - - - Name - Название - - - - Stream URL (http/https) - Адрес канала (http/https) - - - - Country (optional) - Страна (необязательно) - - - - Manual - Справка - - - - Station updated - Обновление станции - - - - HeaderNavigation - - - - Top - Топ - - - - - DE - DE - - - - - NL - NL - - - - - Favorites - Избранные - - - - - History - История - - - - Manual + - Вручную + - - - - - About - О BearWave - - - - Resume - Продолжить - - - - LoadingOverlay - - - Loading stations... - Загрузка станций... - - - - Main - - - BearWave - BearWave - - - - Error: - Ошибка: - - - - Top Global - Всемирный топ - - - - NotificationManager - - - Station: %1 - Станция: %1 - - - - PlayerBar - - - - No station selected - Нет выбранной станции - - - - Now playing: - Сейчас играет: - - - - Now playing: No track info - Сейчас играет: нет информации о треке - - - - Mute - Без звука - - - - Unmute - Со звуком - - - - QuickFilters - - - - Genre: - Жанр: - - - - - Rock - Рок - - - - - News - Новости - - - - - Jazz - Джаз - - - - - Country: - Страна: - - - - - US - US - - - - - UK - UK - - - - - FR - FR - - - - - WORLD - Мир - - - - RadioBackend - - - - Stream URL must use http:// or https:// - URL-адрес потока должен использовать http:// или https:// - - - - - Manual - Справка - - - - - - Last played - Последнее - - - - RadioBrowser - - - Network error - Ошибка сети - - - - SearchToolbar - - - - Search stations (name, genre, country) - Поиск станций (название, жанр, страна) - - - - - Search - Поиск - - - - Sort A-Z - Сорт. А-Я - - - - Sort Bitrate - Сорт. битрейт - - - - Sort Votes - Сорт. голоса - - - - Tip: you are not limited to DE/NL. Search worldwide by country, genre, or station name. - Совет: вы не ограничены DE/NL. Ищите по всему миру, по стране, жанру или названию станции. - - - - StationCard - - - Stream - Поток - - - - Edit Station - Редактировать станцию - - - - Removed from favorites - Убрать из избранного - - - - Added to favorites - Добавить в избранное - - - - StationListPanel - - - No playback history - Нет истории воспроизведения - - - - No stations loaded yet - Станции пока не загружены - - - - Play some stations to build history - Прослушайте несколько станций, чтобы создать историю - - - - Load DE/NL stations or use search - Загрузите станции DE/NL или воспользуйтесь поиском - - - - SystemTrayManager - - - BearWave - BearWave - - - - Pause - Пауза - - - - Play - Слушать - - - - Hide - Скрыть - - - - Show - Показать - - - - Quit - Выйти - - - - WorldCategories - - - Explore World Stations - Сканировать мировые станции - - - - Filter countries... - Фильтровать страны... - - - - Choose a country or music style to find radio stations from all over the world. - Выберите страну или музыкальный стиль, чтобы найти радиостанции со всего мира. - - - - Countries - Страны - - - - Filtered Countries - Выбранные страны - - - - Top 200 - Топ 200 - - - - stations - станции - - - - Popular Music Styles - Популярные музыкальные стили - - - - WorldCategoryHeader - - - ← Back to Categories - ← Вернуться к категориям - - - - World > Country: - Мир > Страна: - - - - World > Genre: - Мир > Жанр: - - - + + AboutDialog + + Version + Версия + + + build + сборка + + + BearWave + BearWave + + + Internet Radio Player for KDE + Интернет радиоплеер для KDE + + + Author: Sebastian Palencsár + Автор: Sebastian Palencsár + + + Open website + Перейти на веб-сайт + + + Open GitHub + Перейти на GitHub + + + Open LinkedIn + Перейти на LinkedIn + + + GNU GPLv3 License + Лицензия GNU GPLv3 + + + Copyright (c) 2026 + Copyright (c) 2026 + + + + AboutPage + + + Version + Версия + + + + Build + Сборка + + + + Qt 6 + Qt 6 + + + + Application framework and UI runtime: Core, DBus, Network, Quick, Quick Controls 2, Widgets, Multimedia, and LinguistTools. + Прикладной фреймворк и среда выполнения UI: Core, DBus, Network, Quick, Quick Controls 2, Widgets, Multimedia и LinguistTools. + + + + Subject to the upstream licenses of the installed Qt distribution. + Действуют лицензии upstream-версии установленного дистрибутива Qt. + + + + Qt Multimedia backend + Мультимедийный бэкенд Qt + + + + Audio playback runs through Qt Multimedia and the backend available on the system, such as GStreamer or FFmpeg. + Воспроизведение аудио выполняется через Qt Multimedia и доступный в системе бэкенд, например GStreamer или FFmpeg. + + + + Backend components are provided by the operating system or Qt build and retain their own licenses. + Компоненты бэкенда предоставляются операционной системой или сборкой Qt и сохраняют свои собственные лицензии. + + + + Radio Browser API + Radio Browser API + + + + Public station directory API for station data and stream discovery. BearWave does not bundle any Radio Browser server code. + Публичный API каталога станций для данных о станциях и обнаружения потоков. BearWave не включает серверный код Radio Browser. + + + + External service or API; subject to the upstream project and service terms. + Внешний сервис или API; действуют условия upstream-проекта и сервиса. + + + + freedesktop.org integrations + Интеграции freedesktop.org + + + + D-Bus, MPRIS, desktop notifications, desktop entry, and AppStream metadata. + D-Bus, MPRIS, уведомления рабочего стола, desktop-файл и метаданные AppStream. + + + + Uses standards and interfaces for desktop integration; no third-party implementation is bundled. + Используются стандарты и интерфейсы для интеграции с рабочим столом; сторонние реализации не включаются. + + + + Internet radio player for KDE + Интернет-радиоплеер для KDE + + + + Author + Автор + + + + Sebastian Palencsár + Себастьян Паленчар + + + + Open website + Перейти на веб-сайт + + + + Open GitHub + Перейти на GitHub + + + + Open LinkedIn + Перейти на LinkedIn + + + + Technologies and third-party components + Технологии и сторонние компоненты + + + + BearWave uses the following frameworks, services, and desktop standards. These components are not relicensed by BearWave. + BearWave использует следующие фреймворки, сервисы и стандарты рабочего стола. Эти компоненты не перелицензируются BearWave. + + + + GNU GPLv3 License + Лицензия GNU GPLv3 + + + + Copyright (c) 2026 + Copyright (c) 2026 + + + + License text could not be loaded. + Не удалось загрузить текст лицензии. + + + + AddStationDialog + + + Add station manually + Добавить станцию вручную + + + + Name + Название + + + + Stream URL (http/https) + Адрес канала (http/https) + + + + Country (optional) + Страна (необязательно) + + + + Station added + Станция добавлена + + + + BearTheme + + + Pop + Поп + + + + Rock + Рок + + + + Electronic + Электронная + + + + Classical + Классическая + + + + Jazz + Джаз + + + + Metal + Метал + + + + Hip Hop + Хип-хоп + + + + Chillout + Успокаивающая + + + + News / Talk + Новости / Разговоры + + + + Soundtracks + Саундтреки + + + + Ambient + Атмосфера + + + + Blues / Soul + Блюз / Соул + + + + EditStationDialog + + + Edit station + Редактировать станцию + + + + Name + Название + + + + Stream URL (http/https) + Адрес канала (http/https) + + + + Country (optional) + Страна (необязательно) + + + + Manual + Справка + + + + Station updated + Обновление станции + + + + HeaderNavigation + + + Top + Топ + + + + DE + DE + + + + NL + NL + + + + Favorites + Избранные + + + + History + История + + + Manual + + Вручную + + + + + About + О BearWave + + + Resume + Продолжить + + + + LoadingOverlay + + + Loading stations... + Загрузка станций... + + + + Main + + + BearWave + BearWave + + + + Error: + Ошибка: + + + + Top Global + Всемирный топ + + + + NotificationManager + + Station: %1 + Станция: %1 + + + + PlayerBar + + + + No station selected + Нет выбранной станции + + + + Now playing: + Сейчас играет: + + + + Live radio + Прямой эфир + + + Now playing: No track info + Сейчас играет: нет информации о треке + + + + Mute + Без звука + + + + Unmute + Со звуком + + + + QuickFilters + + + Genre: + Жанр: + + + + + Rock + Рок + + + + + News + Новости + + + + + Jazz + Джаз + + + + GB + GB + + + + Name + Название + + + + Bitrate + Битрейт + + + + Country: + Страна: + + + + + US + US + + + + Quick access + Быстрый доступ + + + + Sort + Сортировка + + + + Votes + Голоса + + + + UK + UK + + + + + FR + FR + + + + WORLD + Мир + + + + RadioBackend + + Stream URL must use http:// or https:// + URL-адрес потока должен использовать http:// или https:// + + + Manual + Справка + + + Last played + Последнее + + + + RadioBrowser + + Network error + Ошибка сети + + + + SearchToolbar + + + Search stations (name, genre, country)... + Поиск станций (название, жанр, страна)... + + + + + Clear + Очистить + + + + Search stations (name, genre, country) + Поиск станций (название, жанр, страна) + + + + + Search + Поиск + + + Sort A-Z + Сорт. А-Я + + + Sort Bitrate + Сорт. битрейт + + + Sort Votes + Сорт. голоса + + + Tip: you are not limited to DE/NL. Search worldwide by country, genre, or station name. + Совет: вы не ограничены DE/NL. Ищите по всему миру, по стране, жанру или названию станции. + + + + SidebarNavigation + + + Stations + Станции + + + + Top Global + Всемирный топ + + + + Germany + Германия + + + + Netherlands + Нидерланды + + + + Library + Библиотека + + + + Favorites + Избранные + + + + History + История + + + + Worldwide + Весь мир + + + Search + Поиск + + + + Add Station + Добавить станцию + + + + About BearWave + О BearWave + + + + Resume + Продолжить + + + + StationCard + + + votes + голосов + + + + Stream + Поток + + + + Edit Station + Редактировать станцию + + + + Removed from favorites + Удалено из избранного + + + + Added to favorites + Добавлено в избранное + + + + StationDetailsPanel + + + Stream URL copied! + URL потока скопирован! + + + + Details + Подробности + + + + Pause + Пауза + + + + Play + Слушать + + + + Favorite + Избранное + + + + Add + Добавить + + + + Removed from favorites + Удалено из избранного + + + + Added to favorites + Добавлено в избранное + + + + Groups + Группы + + + + Groups coming soon + Группы скоро появятся + + + + Tags + Теги + + + + Links + Ссылки + + + + Codec: + Кодек: + + + + + Unknown + Неизвестно + + + + Bitrate: + Битрейт: + + + + Votes: + Голоса: + + + + Open homepage + Открыть сайт + + + + Copy stream URL + Копировать URL потока + + + + No station selected + Нет выбранной станции + + + + Select a station from the list +to view details. + Выберите станцию из списка, +чтобы просмотреть подробности. + + + + StationListPanel + + + No playback history + Нет истории воспроизведения + + + + No stations loaded yet + Станции пока не загружены + + + + Play some stations to build history + Прослушайте несколько станций, чтобы создать историю + + + + Load DE/NL stations or use search + Загрузите станции DE/NL или воспользуйтесь поиском + + + + SystemTrayManager + + BearWave + BearWave + + + Pause + Пауза + + + Play + Слушать + + + Hide + Скрыть + + + Show + Показать + + + Quit + Выйти + + + + WorldCategories + + + Explore World Stations + Сканировать мировые станции + + + + Filter countries... + Фильтровать страны... + + + + Choose a country or music style to find radio stations from all over the world. + Выберите страну или музыкальный стиль, чтобы найти радиостанции со всего мира. + + + + Countries + Страны + + + + Filtered Countries + Выбранные страны + + + + Top 200 + Топ 200 + + + + stations + станции + + + + Popular Music Styles + Популярные музыкальные стили + + + + WorldCategoryHeader + + + ← Back to Categories + ← Вернуться к категориям + + + + World > Country: + Мир > Страна: + + + + World > Genre: + Мир > Жанр: + + + \ No newline at end of file