From 61486008f644acfb6edc4d41283a7740cbc6cab5 Mon Sep 17 00:00:00 2001 From: Nils Walther Date: Sat, 3 Jan 2026 23:27:14 +0100 Subject: [PATCH] init --- .gitignore | 2 + README.md | 0 build.nix | 25 ++++++ flake.lock | 27 ++++++ flake.nix | 39 +++++++++ qt-navidrome-client/CMakeLists.txt | 46 ++++++++++ qt-navidrome-client/CMakePresets.json | 31 +++++++ qt-navidrome-client/qml/Main.qml | 86 ++++++++++++++++++ qt-navidrome-client/src/main.cpp | 24 +++++ .../src/navidrome/NavidromeApi.cpp | 87 +++++++++++++++++++ .../src/navidrome/NavidromeApi.h | 45 ++++++++++ 11 files changed, 412 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.nix create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 qt-navidrome-client/CMakeLists.txt create mode 100644 qt-navidrome-client/CMakePresets.json create mode 100644 qt-navidrome-client/qml/Main.qml create mode 100644 qt-navidrome-client/src/main.cpp create mode 100644 qt-navidrome-client/src/navidrome/NavidromeApi.cpp create mode 100644 qt-navidrome-client/src/navidrome/NavidromeApi.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a7810a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +qt-navidrome-client/.qtcreator +qt-navidrome-client/build diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/build.nix b/build.nix new file mode 100644 index 0000000..0f506e4 --- /dev/null +++ b/build.nix @@ -0,0 +1,25 @@ +{ lib, stdenv, cmake, ninja, pkg-config, qt6 }: + +stdenv.mkDerivation { + pname = "qt-example"; + version = "1.0"; + + src = ./qt-example; + + nativeBuildInputs = [ + cmake + ninja + pkg-config + qt6.wrapQtAppsHook + ]; + + buildInputs = [ + qt6.qtbase + qt6.qtdeclarative + qt6.qtwayland + ]; + + cmakeFlags = [ + "-DCMAKE_BUILD_TYPE=Release" + ]; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..ee81d0a --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1767379071, + "narHash": "sha256-EgE0pxsrW9jp9YFMkHL9JMXxcqi/OoumPJYwf+Okucw=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "fb7944c166a3b630f177938e478f0378e64ce108", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..6e331c9 --- /dev/null +++ b/flake.nix @@ -0,0 +1,39 @@ +{ + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: + let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + + pkg = pkgs.callPackage ./build.nix {}; + in { + packages.${system}.default = pkg; + + apps.${system}.default = { + type = "app"; + program = "${pkg}/bin/appuntitled"; + }; + + devShells.${system}.default = pkgs.mkShell { + buildInputs = with pkgs; [ + cmake + gdb + qt6.qtbase + qt6.qtdeclarative + qt6.qtwayland + qtcreator + + qt6.wrapQtAppsHook + makeWrapper + bashInteractive + ]; + shellHook = '' + export QT_QPA_PLATFORM=wayland + bashdir=$(mktemp -d) + makeWrapper "$(type -p bash)" "$bashdir/bash" "''${qtWrapperArgs[@]}" + exec "$bashdir/bash" + ''; + }; + }; +} diff --git a/qt-navidrome-client/CMakeLists.txt b/qt-navidrome-client/CMakeLists.txt new file mode 100644 index 0000000..a7ffe66 --- /dev/null +++ b/qt-navidrome-client/CMakeLists.txt @@ -0,0 +1,46 @@ +cmake_minimum_required(VERSION 3.16) + +project(navidrome_client VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +find_package(Qt6 REQUIRED COMPONENTS Quick Network) + +qt_standard_project_setup(REQUIRES 6.8) + +qt_add_executable(appnavidrome_client + src/main.cpp + src/navidrome/NavidromeApi.cpp + src/navidrome/NavidromeApi.h +) + +qt_add_qml_module(appnavidrome_client + URI Navidrome + VERSION 1.0 + QML_FILES + qml/Main.qml +) + +set_target_properties(appnavidrome_client PROPERTIES + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + MACOSX_BUNDLE TRUE + WIN32_EXECUTABLE TRUE +) + +target_include_directories(appnavidrome_client + PRIVATE src +) + +target_link_libraries(appnavidrome_client + PRIVATE Qt6::Quick Qt6::Network +) + +include(GNUInstallDirs) +install(TARGETS appnavidrome_client + BUNDLE DESTINATION . + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +) diff --git a/qt-navidrome-client/CMakePresets.json b/qt-navidrome-client/CMakePresets.json new file mode 100644 index 0000000..30f42f4 --- /dev/null +++ b/qt-navidrome-client/CMakePresets.json @@ -0,0 +1,31 @@ +{ + "version": 3, + "cmakeMinimumRequired": { + "major": 3, + "minor": 20, + "patch": 0 + }, + "configurePresets": [ + { + "name": "dev-debug", + "displayName": "Debug (Nix Umgebung)", + "description": "Debug Build mit Compiler aus der Flake", + "generator": "Unix Makefiles", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_PREFIX_PATH": "$env{CMAKE_PREFIX_PATH}" + }, + "environment": { + "QT_QPA_PLATFORM": "wayland;xcb" + } + } + ], + "buildPresets": [ + { + "name": "build-debug", + "configurePreset": "dev-debug" + } + ] +} diff --git a/qt-navidrome-client/qml/Main.qml b/qt-navidrome-client/qml/Main.qml new file mode 100644 index 0000000..9d2fb63 --- /dev/null +++ b/qt-navidrome-client/qml/Main.qml @@ -0,0 +1,86 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +ApplicationWindow { + id: win + width: 420 + height: 320 + visible: true + title: "Navidrome Login" + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 10 + + Label { + text: "Server" + font.bold: true + } + + TextField { + id: urlField + Layout.fillWidth: true + placeholderText: "https://navidrome.timvandenboom.eth64.de" + text: "https://navidrome.timvandenboom.eth64.de" + } + + Label { + text: "Username" + font.bold: true + } + + TextField { + id: userField + Layout.fillWidth: true + placeholderText: "username" + } + + Label { + text: "Password" + font.bold: true + } + + TextField { + id: passField + Layout.fillWidth: true + placeholderText: "password" + echoMode: TextInput.Password + } + + RowLayout { + Layout.fillWidth: true + spacing: 10 + + Button { + text: navidromeApi.busy ? "Connecting..." : "Login (Ping)" + enabled: !navidromeApi.busy + Layout.fillWidth: true + + onClicked: { + navidromeApi.ping(urlField.text, userField.text, passField.text) + } + } + } + + Rectangle { + Layout.fillWidth: true + height: 1 + opacity: 0.2 + color: "white" + } + + Label { + Layout.fillWidth: true + wrapMode: Text.WordWrap + + text: { + if (navidromeApi.busy) return "Verbinde…" + if (navidromeApi.lastError.length > 0) return "❌ " + navidromeApi.lastError + if (navidromeApi.lastStatus.length > 0) return "✅ status: " + navidromeApi.lastStatus + return "Noch nicht verbunden." + } + } + } +} diff --git a/qt-navidrome-client/src/main.cpp b/qt-navidrome-client/src/main.cpp new file mode 100644 index 0000000..e3b695f --- /dev/null +++ b/qt-navidrome-client/src/main.cpp @@ -0,0 +1,24 @@ +#include +#include +#include + +#include "navidrome/NavidromeApi.h" + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + + NavidromeApi api; + engine.rootContext()->setContextProperty("navidromeApi", &api); + + QObject::connect( + &engine, &QQmlApplicationEngine::objectCreationFailed, + &app, []() { QCoreApplication::exit(1); }, + Qt::QueuedConnection + ); + + engine.loadFromModule("Navidrome", "Main"); + return app.exec(); +} diff --git a/qt-navidrome-client/src/navidrome/NavidromeApi.cpp b/qt-navidrome-client/src/navidrome/NavidromeApi.cpp new file mode 100644 index 0000000..e50fd78 --- /dev/null +++ b/qt-navidrome-client/src/navidrome/NavidromeApi.cpp @@ -0,0 +1,87 @@ +#include "NavidromeApi.h" + +#include +#include +#include +#include + +NavidromeApi::NavidromeApi(QObject *parent) +: QObject(parent) +{} + +void NavidromeApi::setBusy(bool v) +{ + if (m_busy == v) return; + m_busy = v; + emit busyChanged(); +} + +void NavidromeApi::setLastError(const QString &e) +{ + if (m_lastError == e) return; + m_lastError = e; + emit lastErrorChanged(); +} + +void NavidromeApi::setLastStatus(const QString &s) +{ + if (m_lastStatus == s) return; + m_lastStatus = s; + emit lastStatusChanged(); +} + +void NavidromeApi::ping(const QString &baseUrl, + const QString &user, + const QString &password) +{ + setLastError({}); + setLastStatus({}); + + QUrl url(baseUrl.trimmed()); + if (!url.isValid() || url.scheme().isEmpty()) { + setLastError("Ungültige URL (z.B. http://localhost:4533)"); + emit pingFailed(lastError()); + return; + } + + // /rest/ping.view anhängen + url.setPath("/rest/ping.view"); + + QUrlQuery q; + q.addQueryItem("u", user); + q.addQueryItem("p", password); // später Token-Auth (t/s) + q.addQueryItem("v", "1.16.1"); + q.addQueryItem("c", "qt-navidrome"); + q.addQueryItem("f", "json"); + url.setQuery(q); + + setBusy(true); + + auto *reply = m_net.get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + setBusy(false); + + if (reply->error() != QNetworkReply::NoError) { + setLastError(reply->errorString()); + emit pingFailed(lastError()); + reply->deleteLater(); + return; + } + + // Optional: JSON prüfen, ob status == "ok" + const QByteArray body = reply->readAll(); + const auto doc = QJsonDocument::fromJson(body); + if (doc.isObject()) { + const auto root = doc.object(); + const auto ssr = root.value("subsonic-response").toObject(); + const QString status = ssr.value("status").toString(); + if (!status.isEmpty()) setLastStatus(status); + } + + if (lastStatus().isEmpty()) + setLastStatus("ok"); + + emit pingOk(); + reply->deleteLater(); + }); +} diff --git a/qt-navidrome-client/src/navidrome/NavidromeApi.h b/qt-navidrome-client/src/navidrome/NavidromeApi.h new file mode 100644 index 0000000..a8bdcb8 --- /dev/null +++ b/qt-navidrome-client/src/navidrome/NavidromeApi.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +class NavidromeApi : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) + Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged) + Q_PROPERTY(QString lastStatus READ lastStatus NOTIFY lastStatusChanged) + +public: + explicit NavidromeApi(QObject *parent = nullptr); + + // QML Entry: baseUrl als String, damit TextField direkt passt + Q_INVOKABLE void ping(const QString &baseUrl, + const QString &user, + const QString &password); + + bool busy() const { return m_busy; } + QString lastError() const { return m_lastError; } + QString lastStatus() const { return m_lastStatus; } + +signals: + void pingOk(); + void pingFailed(const QString &error); + + void busyChanged(); + void lastErrorChanged(); + void lastStatusChanged(); + +private: + QNetworkAccessManager m_net; + + bool m_busy = false; + QString m_lastError; + QString m_lastStatus; + + void setBusy(bool v); + void setLastError(const QString &e); + void setLastStatus(const QString &s); +};