diff --git a/lib/MellowPlayer/Domain/Properties.hpp b/lib/MellowPlayer/Domain/Properties.hpp index 5a6c360e..2aaccd18 100644 --- a/lib/MellowPlayer/Domain/Properties.hpp +++ b/lib/MellowPlayer/Domain/Properties.hpp @@ -77,7 +77,7 @@ #define CONSTANT_OBJECT_PROPERTY(type, name) \ protected: \ - Q_PROPERTY (type name READ name CONSTANT) \ + Q_PROPERTY (type* name READ name CONSTANT) \ std::shared_ptr name##_; \ public: \ virtual type* name() const { \ diff --git a/lib/MellowPlayer/Infrastructure/Network/NetworkProxy.cpp b/lib/MellowPlayer/Infrastructure/Network/NetworkProxy.cpp new file mode 100644 index 00000000..e6c1d6d3 --- /dev/null +++ b/lib/MellowPlayer/Infrastructure/Network/NetworkProxy.cpp @@ -0,0 +1,57 @@ +#include "NetworkProxy.h" + +using namespace MellowPlayer::Infrastructure; + +NetworkProxy::NetworkProxy(const QVariantMap& rawData): rawData_(rawData) +{ +} + +bool NetworkProxy::isEnabled() const { + return rawData_["enabled"].toBool(); +} + +void NetworkProxy::setEnabled(bool value) { + if (isEnabled() != value) { + rawData_["enabled"] = value; + emit enabledChanged(); + emit changed(); + } +} + +QString NetworkProxy::hostName() const +{ + return rawData_["hostName"].toString(); +} + +void NetworkProxy::setHostName(const QString& value) +{ + if(hostName() != value){ + rawData_["hostName"] = value; + emit hostNameChanged(); + emit changed(); + } +} + +int NetworkProxy::port() const +{ + return rawData_["port"].toInt(); +} + +void NetworkProxy::setPort(int value) +{ + if (port() != value) { + rawData_["port"] = value; + emit portChanged(); + emit changed(); + } +} + +QVariantMap NetworkProxy::rawData() const +{ + return rawData_; +} + +QNetworkProxy NetworkProxy::create() const { + return QNetworkProxy(isEnabled() ? QNetworkProxy::HttpProxy : QNetworkProxy::DefaultProxy, + hostName(), static_cast(port())); +} diff --git a/lib/MellowPlayer/Infrastructure/Network/NetworkProxy.h b/lib/MellowPlayer/Infrastructure/Network/NetworkProxy.h new file mode 100644 index 00000000..ae19e757 --- /dev/null +++ b/lib/MellowPlayer/Infrastructure/Network/NetworkProxy.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +namespace MellowPlayer::Infrastructure +{ + class NetworkProxy: public QObject + { + Q_OBJECT + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(QString hostName READ hostName WRITE setHostName NOTIFY hostNameChanged) + Q_PROPERTY(int port READ port WRITE setPort NOTIFY portChanged) + + public: + NetworkProxy() = default; + NetworkProxy(const QVariantMap& rawData); + + bool isEnabled() const; + void setEnabled(bool value); + + QString hostName() const; + void setHostName(const QString& hostName); + + int port() const; + void setPort(int port); + + QNetworkProxy create() const; + + QVariantMap rawData() const; + + signals: + void changed(); + void enabledChanged(); + void hostNameChanged(); + void portChanged(); + + private: + QVariantMap rawData_; + }; +} diff --git a/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServiceViewModel.cpp b/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServiceViewModel.cpp index b45be2e2..95fafbbf 100644 --- a/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServiceViewModel.cpp +++ b/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServiceViewModel.cpp @@ -4,10 +4,13 @@ #include #include #include +#include #include +using namespace std; using namespace MellowPlayer::Domain; using namespace MellowPlayer::Domain; +using namespace MellowPlayer::Infrastructure; using namespace MellowPlayer::Presentation; #define DEFAULT_ZOOM_FACTOR 7 @@ -17,14 +20,19 @@ StreamingServiceViewModel::StreamingServiceViewModel(StreamingService& streaming IUserScriptFactory& factory, Players& players, QObject* parent) : - QObject(parent), + QObject(parent), + networkProxy_(nullptr), streamingService_(streamingService), settingsStore_(settingsStore), player_(players.get(streamingService.name())), userScriptsViewModel_(streamingService.name(), factory, settingsStore, this), zoomFactor_(settingsStore_.value(zoomFactorSettingsKey(), 7).toInt()) { - + networkProxy_ = make_shared(settingsStore_.value(networkProxySettingsKey()).toMap()); + connect(networkProxy_.get(), &NetworkProxy::changed, [&]() + { + settingsStore.setValue(networkProxySettingsKey(), networkProxy()->rawData()); + }); } QString StreamingServiceViewModel::logo() const @@ -176,3 +184,7 @@ QString StreamingServiceViewModel::notificationsEnabledSettingsKey() const { return streamingService_.name() + "/notificationsEnabled"; } + +QString StreamingServiceViewModel::networkProxySettingsKey() const { + return streamingService_.name() + "/networkProxy"; +} diff --git a/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServiceViewModel.hpp b/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServiceViewModel.hpp index 37a6742e..be05842f 100644 --- a/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServiceViewModel.hpp +++ b/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServiceViewModel.hpp @@ -2,6 +2,7 @@ #include #include +#include #include @@ -14,6 +15,11 @@ namespace MellowPlayer::Domain class Players; } +namespace MellowPlayer::Infrastructure +{ + class NetworkProxy; +} + namespace MellowPlayer::Presentation { class StreamingServiceViewModel : public QObject @@ -32,6 +38,8 @@ namespace MellowPlayer::Presentation Q_PROPERTY(QObject* userScripts READ userScripts CONSTANT) Q_PROPERTY(int zoomFactor READ zoomFactor WRITE setZoomFactor NOTIFY zoomFactorChanged) Q_PROPERTY(bool notificationsEnabled READ notificationsEnabled WRITE setNotificationsEnabled NOTIFY notificationsEnabledChanged) + CONSTANT_OBJECT_PROPERTY(Infrastructure::NetworkProxy, networkProxy); + public: StreamingServiceViewModel(Domain::StreamingService& streamingService, Domain::ISettingsStore& settingsStore, @@ -66,6 +74,8 @@ namespace MellowPlayer::Presentation bool notificationsEnabled() const; void setNotificationsEnabled(bool value); + + public slots: void setUrl(const QString& newUrl); @@ -83,6 +93,7 @@ namespace MellowPlayer::Presentation QString isEnabledSettingsKey() const; QString zoomFactorSettingsKey() const; QString notificationsEnabledSettingsKey() const; + QString networkProxySettingsKey() const; Domain::StreamingService& streamingService_; Domain::ISettingsStore& settingsStore_; diff --git a/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServicesViewModel.cpp b/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServicesViewModel.cpp index 676dce0f..3e354f2d 100644 --- a/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServicesViewModel.cpp +++ b/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServicesViewModel.cpp @@ -8,10 +8,12 @@ #include #include #include +#include #include #include #include +using namespace MellowPlayer; using namespace MellowPlayer::Domain; using namespace MellowPlayer::Domain; using namespace MellowPlayer::Presentation; @@ -229,3 +231,8 @@ StreamingServiceProxyListModel* StreamingServicesViewModel::enabledServices() { return &enabledServices_; } + +void StreamingServicesViewModel::initialize(IQmlApplicationEngine &qmlApplicationEngine) { + qRegisterMetaType("Infrastructure::NetworkProxy*"); + ContextProperty::initialize(qmlApplicationEngine); +} diff --git a/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServicesViewModel.hpp b/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServicesViewModel.hpp index 445960af..5432e071 100644 --- a/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServicesViewModel.hpp +++ b/lib/MellowPlayer/Presentation/ViewModels/StreamingServices/StreamingServicesViewModel.hpp @@ -63,6 +63,8 @@ namespace MellowPlayer::Presentation const QString& authorWebsite, bool allPlatforms, bool linuxPlatform, bool appImagePlatform, bool osxPlatform, bool windowsPlatform); + void initialize(IQmlApplicationEngine &qmlApplicationEngine) override; + public slots: void setCurrentService(QObject* value); void setCurrentIndex(int value); diff --git a/lib/MellowPlayer/Presentation/Views/MellowPlayer/Controls/ItemDelegateSeparator.qml b/lib/MellowPlayer/Presentation/Views/MellowPlayer/Controls/ItemDelegateSeparator.qml new file mode 100644 index 00000000..3479ae25 --- /dev/null +++ b/lib/MellowPlayer/Presentation/Views/MellowPlayer/Controls/ItemDelegateSeparator.qml @@ -0,0 +1,7 @@ +import QtQuick 2.9 +import QtQuick.Layouts 1.3 + +Rectangle { + color: _theme.isDark(_theme.background) ? Qt.lighter(_theme.background) : Qt.darker(_theme.background, 1.1) + height: 1 +} diff --git a/lib/MellowPlayer/Presentation/Views/MellowPlayer/Delegates/SpinBoxDelegate.qml b/lib/MellowPlayer/Presentation/Views/MellowPlayer/Delegates/SpinBoxDelegate.qml new file mode 100644 index 00000000..52e4c275 --- /dev/null +++ b/lib/MellowPlayer/Presentation/Views/MellowPlayer/Delegates/SpinBoxDelegate.qml @@ -0,0 +1,40 @@ +import QtQuick 2.9 +import QtQuick.Layouts 1.3 +import QtQuick.Controls 2.2 + +ItemDelegate { + id: root + + property string label: "" + property int value: 0 + property int from: 0 + property int to: 65535 + + hoverEnabled: true + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 16 + anchors.rightMargin: 16 + spacing: 12 + + Label { + text: root.label + enabled: root.enabled + } + + Item { + Layout.fillWidth: true + } + + SpinBox { + enabled: root.enabled + editable: true + value: root.value + from: root.from + to: root.to + + onValueChanged: root.value = value; + } + } +} diff --git a/lib/MellowPlayer/Presentation/Views/MellowPlayer/Delegates/TextFieldDelegate.qml b/lib/MellowPlayer/Presentation/Views/MellowPlayer/Delegates/TextFieldDelegate.qml new file mode 100644 index 00000000..7749d89a --- /dev/null +++ b/lib/MellowPlayer/Presentation/Views/MellowPlayer/Delegates/TextFieldDelegate.qml @@ -0,0 +1,39 @@ +import QtQuick 2.9 +import QtQuick.Layouts 1.3 +import QtQuick.Controls 2.2 + +ItemDelegate { + id: root + + property string label: "" + property string value: "" + property int textFieldPreferredWidth: 320 + + hoverEnabled: true + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 16 + anchors.rightMargin: 16 + spacing: 12 + + Label { + text: root.label + enabled: root.enabled + } + + Item { + Layout.fillWidth: true + } + + TextField { + enabled: root.enabled + selectByMouse: true + text: root.value + + onTextChanged: root.value = text; + + Layout.preferredWidth: root.textFieldPreferredWidth + } + } +} diff --git a/lib/MellowPlayer/Presentation/Views/MellowPlayer/Dialogs/StreamingServiceSettingsDialog.qml b/lib/MellowPlayer/Presentation/Views/MellowPlayer/Dialogs/StreamingServiceSettingsDialog.qml index 99bf0a96..c9625c7f 100644 --- a/lib/MellowPlayer/Presentation/Views/MellowPlayer/Dialogs/StreamingServiceSettingsDialog.qml +++ b/lib/MellowPlayer/Presentation/Views/MellowPlayer/Dialogs/StreamingServiceSettingsDialog.qml @@ -8,6 +8,7 @@ import QtQuick.Controls 1.2 as QuickControls1 import ".." import "../Controls" +import "../Delegates" Dialog { id: root @@ -66,12 +67,7 @@ Dialog { spacing: 0 clip: true - Rectangle { - color: _theme.isDark(_theme.background) ? Qt.lighter(_theme.background) : Qt.darker(_theme.background, 1.1) - Layout.alignment: Qt.AlignCenter - Layout.preferredHeight: 1 - Layout.fillWidth: true - } + ItemDelegateSeparator { Layout.fillWidth: true } TabBar { id: tabBar @@ -127,11 +123,7 @@ Dialog { Layout.fillWidth: true } - Rectangle { - color: _theme.isDark(_theme.background) ? Qt.lighter(_theme.background) : Qt.darker(_theme.background, 1.1) - Layout.preferredHeight: 1 - Layout.fillWidth: true - } + ItemDelegateSeparator { Layout.fillWidth: true } SwitchDelegate { checked: service.notificationsEnabled @@ -145,38 +137,13 @@ Dialog { Layout.fillWidth: true } - Rectangle { - color: _theme.isDark(_theme.background) ? Qt.lighter(_theme.background) : Qt.darker(_theme.background, 1.1) - Layout.preferredHeight: 1 - Layout.fillWidth: true - } - - ItemDelegate { - hoverEnabled: true + ItemDelegateSeparator { Layout.fillWidth: true } + TextFieldDelegate { Layout.fillWidth: true - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 16 - anchors.rightMargin: 16 - spacing: 12 - - Label { - text: qsTr("URL: ") - } - - Item { - Layout.fillWidth: true - } - - TextField { - selectByMouse: true - text: service.url - - Layout.preferredWidth: 320 - } - } + label: qsTr("URL: ") + value: service.url + onValueChanged: service.url = value } Item { @@ -339,14 +306,9 @@ Dialog { } } - Rectangle { - color: _theme.isDark(_theme.background) ? Qt.lighter(_theme.background) : Qt.darker(_theme.background, 1.1) - height: 1 + ItemDelegateSeparator { + anchors { bottom: parent.bottom; left: parent.left; right: parent.right } visible: model.index != (delegate.ListView.view.count - 1) - - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right } } } @@ -366,37 +328,60 @@ Dialog { } } - ColumnLayout { + Item { anchors.fill: parent - spacing: 20 - Item { - Layout.fillHeight: true - } + Pane { + id: networkProxyPane + + padding: 0 + anchors.centerIn: parent + width: parent.width / 2 + height: parent.height * 0.75 - Label { - text: MaterialIcons.icon_build - font.pixelSize: 64 - font.family: MaterialIcons.family - color: Material.color(Material.Orange) + Material.background: _theme.isDark(_theme.background) ? Qt.lighter(_theme.background, 1.05) : Qt.darker(_theme.background, 1.05) + Material.elevation: 2 - horizontalAlignment: "AlignHCenter" + ColumnLayout { + anchors.fill: parent - Layout.fillWidth: true - } + SwitchDelegate { + id: networkProxySwitch + text: qsTr("Use custom network proxy") + checked: root.service.networkProxy.enabled + onCheckedChanged: root.service.networkProxy.enabled = checked - Label { - text: qsTr("Coming soon!") - font.bold: true - font.pixelSize: 20 + Layout.fillWidth: true + } - horizontalAlignment: "AlignHCenter" + ItemDelegateSeparator { Layout.fillWidth: true } - Layout.fillWidth: true - } + TextFieldDelegate { + enabled: networkProxySwitch.checked + label: qsTr("Host") + value: root.service.networkProxy.hostName - Item { - Layout.fillHeight: true + onValueChanged: root.service.networkProxy.hostName = value + + Layout.fillWidth: true + } + + ItemDelegateSeparator { Layout.fillWidth: true } + + SpinBoxDelegate { + label: qsTr("Port") + value: root.service.networkProxy.port + enabled: networkProxySwitch.checked + + onValueChanged: root.service.networkProxy.port = value + + Layout.fillWidth: true + } + + Item { + Layout.fillHeight: true + } + } } } } @@ -439,10 +424,11 @@ Dialog { property string authorWebsite: "" property bool notificationsEnabled: true property bool isEnabled: true - - property QtObject userScripts: QtObject { - + property QtObject userScripts: QtObject { } + property QtObject networkProxy: QtObject { + property bool enabled: false + property string hostName: "" + property int port: 0 } } } - diff --git a/lib/MellowPlayer/Presentation/presentation.qrc b/lib/MellowPlayer/Presentation/presentation.qrc index 18afcb14..559679fe 100644 --- a/lib/MellowPlayer/Presentation/presentation.qrc +++ b/lib/MellowPlayer/Presentation/presentation.qrc @@ -85,5 +85,8 @@ Views/MellowPlayer/SettingsTranslator.js Resources/mellowplayer.js Views/MellowPlayer/Dialogs/StreamingServiceSettingsDialog.qml + Views/MellowPlayer/Delegates/TextFieldDelegate.qml + Views/MellowPlayer/Controls/ItemDelegateSeparator.qml + Views/MellowPlayer/Delegates/SpinBoxDelegate.qml diff --git a/tests/UnitTests/Infrastructure/Network/NetworkProxyTests.cpp b/tests/UnitTests/Infrastructure/Network/NetworkProxyTests.cpp new file mode 100644 index 00000000..b10c6856 --- /dev/null +++ b/tests/UnitTests/Infrastructure/Network/NetworkProxyTests.cpp @@ -0,0 +1,149 @@ +#include +#include +#include + +using namespace MellowPlayer::Infrastructure; + +SCENARIO("NetworkProxyTests") +{ + GIVEN("An empty network proxy") + { + NetworkProxy networkProxy; + + QSignalSpy changedSpy(&networkProxy, &NetworkProxy::changed); + QSignalSpy enabledChangedSpy(&networkProxy, &NetworkProxy::enabledChanged); + QSignalSpy hostNameChangedSpy(&networkProxy, &NetworkProxy::hostNameChanged); + QSignalSpy portChangedSpy(&networkProxy, &NetworkProxy::portChanged); + + REQUIRE(networkProxy.rawData().count() == 0); + + WHEN("I call isEnabled") + { + THEN("it returns false") + { + REQUIRE(!networkProxy.isEnabled()); + } + + AND_THEN("create return a QNetworkProxy with type set to DefaultProxy") + { + REQUIRE(networkProxy.create().type() == QNetworkProxy::DefaultProxy); + } + + AND_WHEN("I set isEnabled to true") + { + networkProxy.setEnabled(true); + + THEN("isEnabled returns true") + { + REQUIRE(networkProxy.isEnabled()); + } + + AND_THEN("enabledChanged signal is emitted") + { + REQUIRE(enabledChangedSpy.count() == 1); + } + + AND_THEN("changed signal is emitted") + { + REQUIRE(changedSpy.count() == 1); + } + + AND_THEN("create return a QNetworkProxy with type set to HttpProxy") + { + REQUIRE(networkProxy.create().type() == QNetworkProxy::HttpProxy); + } + + AND_THEN("rawData contains one entry") + { + REQUIRE(networkProxy.rawData().count() == 1); + } + } + } + + WHEN("I call hostName") + { + THEN("it returns an empty string") + { + REQUIRE(networkProxy.hostName().isEmpty()); + } + + AND_THEN("create return QNetworkProxy with empty host") + { + REQUIRE(networkProxy.create().hostName().isEmpty()); + } + + AND_WHEN("I set hostName to 192.168.0.1") + { + networkProxy.setHostName("192.168.0.1"); + + THEN("hostName is set to 192.168.0.1") + { + REQUIRE(networkProxy.hostName() == "192.168.0.1"); + } + + AND_THEN("hostNameChanged signal is emitted") + { + REQUIRE(hostNameChangedSpy.count() == 1); + } + + AND_THEN("changed signal is emitted") + { + REQUIRE(changedSpy.count() == 1); + } + + AND_THEN("create return QNetworkProxy with hostName set to 192.168.0.1") + { + REQUIRE(networkProxy.create().hostName() == "192.168.0.1"); + } + + AND_THEN("rawData contains one entry") + { + REQUIRE(networkProxy.rawData().count() == 1); + } + } + } + + WHEN("I call port") + { + THEN("it returns 0") + { + REQUIRE(networkProxy.port() == 0); + } + + AND_THEN("create return QNetworkProxy with port set to 0") + { + REQUIRE(networkProxy.create().port() == 0); + } + + AND_WHEN("I set port to 8080") + { + networkProxy.setPort(8080); + + THEN("port is set to 8080") + { + REQUIRE(networkProxy.port() == 8080); + } + + AND_THEN("portChanged signal is emitted") + { + REQUIRE(portChangedSpy.count() == 1); + } + + AND_THEN("changed signal is emitted") + { + REQUIRE(changedSpy.count() == 1); + } + + AND_THEN("create return QNetworkProxy with port set to 8080") + { + REQUIRE(networkProxy.create().port() == 8080); + } + + AND_THEN("rawData contains one entry") + { + REQUIRE(networkProxy.rawData().count() == 1); + } + } + } + } +} diff --git a/tests/UnitTests/Presentation/ViewModels/StreamingServices/StreamingServiceViewModelTests.cpp b/tests/UnitTests/Presentation/ViewModels/StreamingServices/StreamingServiceViewModelTests.cpp index fef0dfe5..47270766 100644 --- a/tests/UnitTests/Presentation/ViewModels/StreamingServices/StreamingServiceViewModelTests.cpp +++ b/tests/UnitTests/Presentation/ViewModels/StreamingServices/StreamingServiceViewModelTests.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -81,4 +82,10 @@ TEST_CASE("StreamingServiceModelTests", "[UnitTest]") REQUIRE(!viewModel.notificationsEnabled()); REQUIRE(spy.count() == 1); } + + SECTION("proxy settings are saved") + { + viewModel.networkProxy()->setEnabled(true); + REQUIRE(settingsStore.value(service1.name() + "/networkProxy").toMap()["enabled"].toBool()); + } }