├── websockets9 ├── libQt5WebSockets.so.5 ├── libQt5WebSockets.so.5.3 ├── libQt5WebSockets.so.5.3.3 └── harbour │ └── lgremote │ └── webos │ └── websockets │ ├── qmldir │ └── plugins.qmltypes ├── images ├── cover3.png ├── volume.png ├── volume-muted.png ├── icon-arrow-down.png ├── icon-arrow-left.png ├── icon-arrow-up.png ├── icon-cover-play.png ├── icon-m-arrows.png ├── icon-media-next.png ├── icon-media-play.png ├── icon-media-stop.png ├── icon-arrow-right.png ├── icon-cover-pause.png ├── icon-media-pause.png ├── icon-media-rewind.png ├── icon-media-previous.png └── icon-media-fastforward.png ├── harbour-lgremote-webos.png ├── README.md ├── harbour-lgremote-webos.desktop ├── LICENSE ├── .gitignore ├── qml ├── pages │ ├── ImageButton.qml │ ├── InputPanel.qml │ ├── ControlButton.qml │ ├── PointerSocket.qml │ ├── ColoredImage.qml │ ├── TextPanel.qml │ ├── SmoothPanel.qml │ ├── ApplicationsPanel.qml │ ├── ExtraPanel.qml │ ├── AboutPage.qml │ ├── ChannelsPanel.qml │ ├── ActionsPanel.qml │ ├── TouchpadPanel.qml │ ├── DiscoverPage.qml │ ├── MainSocket.qml │ └── MainPage.qml ├── main.qml └── cover │ └── CoverPage.qml ├── src ├── networkobserver.h ├── main.cpp ├── settings.cpp ├── settings.h └── networkobserver.cpp ├── harbour-lgremote-webos.pro └── rpm └── harbour-lgremote-webos.spec /websockets9/libQt5WebSockets.so.5: -------------------------------------------------------------------------------- 1 | libQt5WebSockets.so.5.3.3 -------------------------------------------------------------------------------- /websockets9/libQt5WebSockets.so.5.3: -------------------------------------------------------------------------------- 1 | libQt5WebSockets.so.5.3.3 -------------------------------------------------------------------------------- /images/cover3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/cover3.png -------------------------------------------------------------------------------- /images/volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/volume.png -------------------------------------------------------------------------------- /images/volume-muted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/volume-muted.png -------------------------------------------------------------------------------- /harbour-lgremote-webos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/harbour-lgremote-webos.png -------------------------------------------------------------------------------- /images/icon-arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/icon-arrow-down.png -------------------------------------------------------------------------------- /images/icon-arrow-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/icon-arrow-left.png -------------------------------------------------------------------------------- /images/icon-arrow-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/icon-arrow-up.png -------------------------------------------------------------------------------- /images/icon-cover-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/icon-cover-play.png -------------------------------------------------------------------------------- /images/icon-m-arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/icon-m-arrows.png -------------------------------------------------------------------------------- /images/icon-media-next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/icon-media-next.png -------------------------------------------------------------------------------- /images/icon-media-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/icon-media-play.png -------------------------------------------------------------------------------- /images/icon-media-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/icon-media-stop.png -------------------------------------------------------------------------------- /images/icon-arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/icon-arrow-right.png -------------------------------------------------------------------------------- /images/icon-cover-pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/icon-cover-pause.png -------------------------------------------------------------------------------- /images/icon-media-pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/icon-media-pause.png -------------------------------------------------------------------------------- /images/icon-media-rewind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/icon-media-rewind.png -------------------------------------------------------------------------------- /images/icon-media-previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/icon-media-previous.png -------------------------------------------------------------------------------- /images/icon-media-fastforward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/images/icon-media-fastforward.png -------------------------------------------------------------------------------- /websockets9/libQt5WebSockets.so.5.3.3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CODeRUS/harbour-lgremote-webos/HEAD/websockets9/libQt5WebSockets.so.5.3.3 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | harbour-lgremote-webos 2 | ====================== 3 | 4 | Simple webOS TV remote control application for SailfishOS 5 | 6 | https://openrepos.net/content/coderus/webos-tv-remote -------------------------------------------------------------------------------- /harbour-lgremote-webos.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | X-Nemo-Application-Type=silica-qt5 4 | Icon=harbour-lgremote-webos 5 | Exec=harbour-lgremote-webos 6 | Name=webOS TV Remote 7 | 8 | -------------------------------------------------------------------------------- /websockets9/harbour/lgremote/webos/websockets/qmldir: -------------------------------------------------------------------------------- 1 | module harbour.lgremote.webos.websockets 2 | plugin declarative_qmlwebsockets 3 | classname QtWebSocketsDeclarativeModule 4 | typeinfo plugins.qmltypes 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2014 Andrey Kozhevnikov 2 | This work is free. You can redistribute it and/or modify it under the 3 | terms of the Do What The Fuck You Want To Public License, Version 2, 4 | as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | 3 | *.slo 4 | *.lo 5 | *.o 6 | *.a 7 | *.la 8 | *.lai 9 | *.so 10 | *.dll 11 | *.dylib 12 | 13 | # Qt-es 14 | 15 | /.qmake.cache 16 | /.qmake.stash 17 | *.pro.user 18 | *.pro.user.* 19 | *.moc 20 | moc_*.cpp 21 | qrc_*.cpp 22 | ui_*.h 23 | Makefile* 24 | *-build-* 25 | 26 | # QtCreator 27 | 28 | *.autosave 29 | 30 | #QtCtreator Qml 31 | *.qmlproject.user 32 | *.qmlproject.user.* 33 | -------------------------------------------------------------------------------- /qml/pages/ImageButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | MouseArea { 5 | property alias icon: image 6 | property bool down: pressed && containsMouse 7 | property bool highlighted: down 8 | property bool _showPress: highlighted || pressTimer.running 9 | 10 | onPressedChanged: { 11 | if (pressed) { 12 | pressTimer.start() 13 | } 14 | } 15 | onCanceled: pressTimer.stop() 16 | 17 | width: Theme.itemSizeSmall; height: Theme.itemSizeSmall 18 | 19 | ColoredImage { 20 | id: image 21 | anchors.centerIn: parent 22 | opacity: parent.enabled ? 1.0 : 0.4 23 | 24 | highlighted: _showPress 25 | } 26 | 27 | Timer { 28 | id: pressTimer 29 | interval: 50 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/networkobserver.h: -------------------------------------------------------------------------------- 1 | #ifndef NETWORKOBSERVER_H 2 | #define NETWORKOBSERVER_H 3 | 4 | #define QT_DEBUG_TM_NETWORK_OBSERVER 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | class NetworkObserver : public QObject 13 | { 14 | Q_OBJECT 15 | public: 16 | explicit NetworkObserver(QObject *parent = 0); 17 | virtual ~NetworkObserver(); 18 | 19 | public Q_SLOTS: 20 | void startSearch(); 21 | void stopSearch(); 22 | void searchIp(const QString &ip); 23 | 24 | Q_SIGNALS: 25 | void discovered(const QVariantMap &result); 26 | void timeout(); 27 | 28 | private: 29 | QNetworkAccessManager *nam; 30 | QTimer *timeoutTimer; 31 | QStringList discoveredDevices; 32 | 33 | protected: 34 | void handleMessage( const QByteArray& message ); 35 | 36 | protected Q_SLOTS: 37 | void onUdpSocketReadyRead(); 38 | void checkDeviceInfo(const QString &server); 39 | }; 40 | 41 | #endif // NETWORKOBSERVER_H 42 | -------------------------------------------------------------------------------- /qml/pages/InputPanel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import Sailfish.Silica 1.0 3 | 4 | SmoothPanel { 5 | id: panel 6 | property int maxMargin 7 | property MainSocket socket 8 | property var inputList: [] 9 | onInputListChanged: { 10 | inputModel.clear() 11 | for (var i = 0; i < inputList.length; i++) { 12 | inputModel.append(inputList[i]) 13 | } 14 | } 15 | topMargin: height - content.height 16 | 17 | Column { 18 | id: content 19 | width: parent.width 20 | 21 | Repeater { 22 | width: parent.width 23 | delegate: Component { 24 | ControlButton { 25 | width: parent.width 26 | height: Theme.itemSizeSmall 27 | title: model.label 28 | onClicked: { 29 | socket.sendSwitchInput(model.id) 30 | panel.active = false 31 | } 32 | } 33 | } 34 | model: ListModel { 35 | id: inputModel 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "networkobserver.h" 8 | #include "settings.h" 9 | 10 | int main(int argc, char *argv[]) 11 | { 12 | QScopedPointer app(SailfishApp::application(argc, argv)); 13 | app->setApplicationDisplayName("LG Remote"); 14 | app->setApplicationName("LG Remote"); 15 | app->setApplicationVersion(QString(APP_VERSION)); 16 | app->setOrganizationName("harbour-lgremote-webos"); 17 | 18 | QScopedPointer view(SailfishApp::createView()); 19 | view->setTitle("LG Remote"); 20 | view->engine()->addImportPath("/usr/share/harbour-lgremote-webos/import"); 21 | 22 | QScopedPointer network(new NetworkObserver(app.data())); 23 | view->rootContext()->setContextProperty("network", network.data()); 24 | 25 | QScopedPointer settings(new Settings(app.data())); 26 | view->rootContext()->setContextProperty("settings", settings.data()); 27 | 28 | view->setSource(SailfishApp::pathTo("qml/main.qml")); 29 | view->showFullScreen(); 30 | 31 | return app->exec(); 32 | } 33 | 34 | -------------------------------------------------------------------------------- /harbour-lgremote-webos.pro: -------------------------------------------------------------------------------- 1 | TARGET = harbour-lgremote-webos 2 | 3 | QT += network xml xmlpatterns websockets 4 | CONFIG += sailfishapp 5 | 6 | SOURCES += \ 7 | src/main.cpp \ 8 | src/networkobserver.cpp \ 9 | src/settings.cpp 10 | 11 | HEADERS += \ 12 | src/networkobserver.h \ 13 | src/settings.h 14 | 15 | DEFINES += APP_VERSION=\\\"$$VERSION\\\" 16 | 17 | images.files = images 18 | images.path = /usr/share/harbour-lgremote-webos 19 | 20 | INSTALLS += images 21 | 22 | OTHER_FILES += \ 23 | qml/cover/CoverPage.qml \ 24 | rpm/harbour-lgremote-webos.spec \ 25 | harbour-lgremote-webos.desktop \ 26 | harbour-lgremote-webos.png \ 27 | qml/main.qml \ 28 | qml/pages/DiscoverPage.qml \ 29 | qml/pages/MainPage.qml \ 30 | qml/pages/AboutPage.qml \ 31 | qml/pages/TouchpadPanel.qml \ 32 | qml/pages/ImageButton.qml \ 33 | qml/pages/SmoothPanel.qml \ 34 | qml/pages/ChannelsPanel.qml \ 35 | qml/pages/ControlButton.qml \ 36 | qml/pages/ActionsPanel.qml \ 37 | qml/pages/ColoredImage.qml \ 38 | qml/pages/ApplicationsPanel.qml \ 39 | qml/pages/ExtraPanel.qml \ 40 | qml/pages/TextPanel.qml \ 41 | qml/pages/InputPanel.qml \ 42 | qml/pages/PointerSocket.qml \ 43 | qml/pages/MainSocket.qml 44 | -------------------------------------------------------------------------------- /qml/pages/ControlButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import Sailfish.Silica 1.0 3 | 4 | MouseArea { 5 | id: root 6 | 7 | width: 100 8 | height: 80 9 | 10 | property bool down: pressed && containsMouse 11 | 12 | property alias title: label.text 13 | property alias titleSize: label.font.pixelSize 14 | property alias bold: label.font.bold 15 | property alias icon: image.imageSource 16 | property alias color: background.color 17 | property int borderWidth: root.down ? 0 : 2 18 | 19 | Rectangle { 20 | id: background 21 | anchors.fill: root 22 | border.width: root.borderWidth 23 | border.color: Theme.highlightDimmerColor 24 | color: Theme.rgba(Theme.highlightBackgroundColor, root.down ? 1.0 : Theme.highlightBackgroundOpacity) 25 | } 26 | 27 | Label { 28 | id: label 29 | anchors.centerIn: root 30 | color: root.down ? Theme.highlightColor : Theme.primaryColor 31 | } 32 | 33 | ColoredImage { 34 | id: image 35 | anchors.centerIn: root 36 | highlighted: root.down 37 | highlightColor: Theme.highlightColor 38 | height: Math.min(root.height, root.width) / 5 * 3 39 | //width: (sourceSize.width * height) / sourceSize.height 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /qml/pages/PointerSocket.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | ;import QtWebSockets 1.1 3 | 4 | WebSocket { 5 | id: pointerSocket 6 | active: false 7 | property bool connected: status == WebSocket.Open 8 | onTextMessageReceived: { 9 | // 10 | } 11 | onErrorStringChanged: { 12 | console.log("pointerSocket: " + errorString) 13 | } 14 | 15 | function sendLogMessage(message) { 16 | pointerSocket.sendTextMessage(message) 17 | } 18 | 19 | function sendMove(dx, dy) { 20 | if (pointerSocket.status == WebSocket.Open) { 21 | pointerSocket.sendLogMessage('type:move\ndx:' + dx + '\ndy:' + dy + '\ndown:0\n\n') 22 | } 23 | } 24 | 25 | function sendScroll(dy) { 26 | if (pointerSocket.status == WebSocket.Open) { 27 | pointerSocket.sendLogMessage('type:scroll\ndx:0\ndy:' + dy + '\ndown:0\n\n') 28 | } 29 | } 30 | 31 | function sendClick() { 32 | if (pointerSocket.status == WebSocket.Open) { 33 | pointerSocket.sendLogMessage('type:click\n\n') 34 | } 35 | } 36 | 37 | function sendInput(btype, bname) { 38 | if (pointerSocket.status == WebSocket.Open) { 39 | console.log("send " + btype + ": " + bname) 40 | pointerSocket.sendLogMessage('type:' + btype + '\nname:' + bname + '\n\n') 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /qml/pages/ColoredImage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import Sailfish.Silica 1.0 3 | 4 | Image { 5 | id: image 6 | 7 | property bool themeImage: imageSource.toString().indexOf("image://theme/") == 0 8 | 9 | property bool highlighted 10 | property color highlightColor: Theme.highlightColor 11 | property url _highlightSource: highlighted ? (imageSource.toString() + "?" + highlightColor) : imageSource 12 | property url imageSource 13 | 14 | source: imageSource != "" ? _highlightSource : "" 15 | 16 | fillMode: Image.PreserveAspectFit 17 | cache: true 18 | smooth: true 19 | 20 | layer.effect: ShaderEffect { 21 | id: shaderItem 22 | property color color: image.highlightColor 23 | 24 | fragmentShader: " 25 | varying mediump vec2 qt_TexCoord0; 26 | uniform highp float qt_Opacity; 27 | uniform lowp sampler2D imageSource; 28 | uniform highp vec4 color; 29 | void main() { 30 | highp vec4 pixelColor = texture2D(imageSource, qt_TexCoord0); 31 | gl_FragColor = vec4(mix(pixelColor.rgb/max(pixelColor.a, 0.00390625), color.rgb/max(color.a, 0.00390625), color.a) * pixelColor.a, pixelColor.a) * qt_Opacity; 32 | } 33 | " 34 | } 35 | layer.enabled: !themeImage && image.highlighted 36 | layer.samplerName: "imageSource" 37 | } 38 | -------------------------------------------------------------------------------- /websockets9/harbour/lgremote/webos/websockets/plugins.qmltypes: -------------------------------------------------------------------------------- 1 | import QtQuick.tooling 1.1 2 | 3 | // This file describes the plugin-supplied types contained in the library. 4 | // It is used for QML tooling purposes only. 5 | // 6 | // This file was auto-generated by: 7 | // 'qmlplugindump -notrelocatable harbour.lgremote.webos.websockets 1.0' 8 | 9 | Module { 10 | Component { 11 | name: "QQmlWebSocket" 12 | prototype: "QObject" 13 | exports: ["harbour.lgremote.webos.websockets/WebSocket 1.0"] 14 | exportMetaObjectRevisions: [0] 15 | Enum { 16 | name: "Status" 17 | values: { 18 | "Connecting": 0, 19 | "Open": 1, 20 | "Closing": 2, 21 | "Closed": 3, 22 | "Error": 4 23 | } 24 | } 25 | Property { name: "url"; type: "QUrl" } 26 | Property { name: "status"; type: "Status"; isReadonly: true } 27 | Property { name: "errorString"; type: "string"; isReadonly: true } 28 | Property { name: "active"; type: "bool" } 29 | Signal { 30 | name: "textMessageReceived" 31 | Parameter { name: "message"; type: "string" } 32 | } 33 | Signal { 34 | name: "statusChanged" 35 | Parameter { name: "status"; type: "Status" } 36 | } 37 | Signal { 38 | name: "activeChanged" 39 | Parameter { name: "isActive"; type: "bool" } 40 | } 41 | Method { 42 | name: "sendTextMessage" 43 | Parameter { name: "message"; type: "string" } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /qml/pages/TextPanel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import Sailfish.Silica 1.0 3 | 4 | SmoothPanel { 5 | id: panel 6 | property int maxMargin 7 | topMargin: height - fieldClipping.height 8 | property alias textField: field 9 | 10 | signal inputComplete(string text) 11 | signal acceptableInput 12 | 13 | onActiveChanged: { 14 | if (active) { 15 | field.forceActiveFocus() 16 | } 17 | } 18 | 19 | function startShowAnimation() { 20 | panel.opacity = 1.0 21 | panel.y = panel.topMargin 22 | } 23 | 24 | function startStopAnimation() { 25 | panel.opacity = 0.0 26 | panel.y = Screen.height 27 | } 28 | 29 | Item { 30 | id: fieldClipping 31 | width: parent.width 32 | height: Theme.itemSizeSmall 33 | clip: true 34 | 35 | Rectangle { 36 | anchors.fill: parent 37 | border.width: 2 38 | border.color: Theme.highlightDimmerColor 39 | color: Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) 40 | } 41 | 42 | TextField { 43 | id: field 44 | width: parent.width 45 | textTopMargin: Theme.paddingLarge 46 | EnterKey.enabled: text.length > 0 47 | EnterKey.iconSource: "image://theme/icon-m-enter-next" 48 | EnterKey.onClicked: { 49 | inputComplete(text) 50 | panel.active = false 51 | } 52 | background: Item {} 53 | onAcceptableInputChanged: if (field.acceptableInput) panel.acceptableInput() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/settings.cpp: -------------------------------------------------------------------------------- 1 | #include "settings.h" 2 | 3 | Settings::Settings(QObject *parent) : 4 | QObject(parent) 5 | { 6 | nam = new QNetworkAccessManager(this); 7 | 8 | QSettings settings; 9 | settings.sync(); 10 | QString code = settings.value("code", "demo").toString(); 11 | checkActivation(code); 12 | } 13 | 14 | QString Settings::bannerPath() const 15 | { 16 | return _bannerPath; 17 | } 18 | 19 | void Settings::checkActivation(const QString &code) 20 | { 21 | QSettings settings; 22 | settings.setValue("code", code); 23 | settings.sync(); 24 | 25 | QString url(QByteArray::fromBase64("aHR0cHM6Ly9jb2RlcnVzLm9wZW5yZXBvcy5uZXQvd2hpdGVzb2Z0L2FjdGl2YXRpb24vJTE=")); 26 | QObject::connect(nam->get(QNetworkRequest(QUrl(url.arg(code)))), SIGNAL(finished()), this, SLOT(onActivationReply())); 27 | } 28 | 29 | void Settings::onActivationReply() 30 | { 31 | QNetworkReply *reply = qobject_cast(sender()); 32 | if (reply) { 33 | _bannerPath = QString::fromUtf8(reply->readAll()); 34 | Q_EMIT bannerPathChanged(); 35 | } 36 | } 37 | 38 | QString Settings::getAuthMessage(const QString &ip) const 39 | { 40 | QSettings settings; 41 | settings.sync(); 42 | QString settingsKey = ip; 43 | QString authKey = settings.value(settingsKey.replace(".", "_"), QString()).toString(); 44 | QString msg = QString(AUTH_TEMPLATE).arg("0").arg(authKey); 45 | return msg; 46 | } 47 | 48 | void Settings::setAuthKey(const QString &ip, const QString &key) 49 | { 50 | QSettings settings; 51 | QString settingsKey = ip; 52 | settings.setValue(settingsKey.replace(".", "_"), key); 53 | settings.sync(); 54 | } 55 | -------------------------------------------------------------------------------- /rpm/harbour-lgremote-webos.spec: -------------------------------------------------------------------------------- 1 | # 2 | # Do NOT Edit the Auto-generated Part! 3 | # Generated by: spectacle version 0.27 4 | # 5 | 6 | Name: harbour-lgremote-webos 7 | 8 | %{!?qtc_qmake5:%define qtc_qmake5 %qmake5} 9 | %{!?qtc_make:%define qtc_make make} 10 | 11 | Summary: LG Remote for webOS Smart TV 12 | Version: 0.1.7 13 | Release: 1 14 | Group: Qt/Qt 15 | License: WTFPL 16 | URL: https://github.com/CODeRUS/harbour-lgremote-webos 17 | Source0: %{name}-%{version}.tar.bz2 18 | Requires: sailfishsilica-qt5 >= 0.10.9 19 | Requires: qt5-qtdeclarative-import-websockets 20 | BuildRequires: pkgconfig(sailfishapp) >= 1.0.2 21 | BuildRequires: pkgconfig(Qt5Core) 22 | BuildRequires: pkgconfig(Qt5Qml) 23 | BuildRequires: pkgconfig(Qt5Quick) 24 | BuildRequires: desktop-file-utils 25 | BuildRequires: qt5-qtdeclarative-import-websockets 26 | 27 | %description 28 | LG Remote for webOS Smart TV 29 | 30 | %prep 31 | %setup -q -n %{name}-%{version} 32 | 33 | # >> setup 34 | # << setup 35 | 36 | %build 37 | # >> build pre 38 | # << build pre 39 | 40 | %qtc_qmake5 \ 41 | VERSION=%{version} 42 | 43 | %qtc_make %{?_smp_mflags} 44 | 45 | # >> build post 46 | # << build post 47 | 48 | %install 49 | rm -rf %{buildroot} 50 | # >> install pre 51 | # << install pre 52 | %qmake5_install 53 | 54 | # >> install post 55 | # << install post 56 | 57 | desktop-file-install --delete-original \ 58 | --dir %{buildroot}%{_datadir}/applications \ 59 | %{buildroot}%{_datadir}/applications/*.desktop 60 | 61 | %files 62 | %defattr(-,root,root,-) 63 | %{_bindir} 64 | %{_datadir}/%{name} 65 | %{_datadir}/applications/%{name}.desktop 66 | %{_datadir}/icons/hicolor/86x86/apps/%{name}.png 67 | # >> files 68 | # << files 69 | -------------------------------------------------------------------------------- /qml/pages/SmoothPanel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import Sailfish.Silica 1.0 3 | 4 | Rectangle { 5 | id: panel 6 | property int topMargin: 200 7 | property bool active: false 8 | onActiveChanged: { 9 | if (active) { 10 | startShowAnimation() 11 | } 12 | else { 13 | startStopAnimation() 14 | } 15 | } 16 | default property alias _content: mover.children 17 | 18 | function startShowAnimation() { 19 | showAnimation.start() 20 | } 21 | 22 | function startStopAnimation() { 23 | hideAnimation.start() 24 | } 25 | 26 | anchors.fill: parent 27 | opacity: 0.0 28 | enabled: opacity > 0.0 29 | 30 | color: "#80000000" 31 | 32 | SequentialAnimation { 33 | id: showAnimation 34 | NumberAnimation { 35 | target: panel 36 | property: "opacity" 37 | from: 0.0 38 | to: 1.0 39 | duration: 200 40 | } 41 | NumberAnimation { 42 | target: mover 43 | property: "y" 44 | from: Screen.height 45 | to: panel.topMargin 46 | duration: 100 47 | } 48 | } 49 | 50 | SequentialAnimation { 51 | id: hideAnimation 52 | NumberAnimation { 53 | target: panel 54 | property: "opacity" 55 | from: 1.0 56 | to: 0.0 57 | duration: 200 58 | } 59 | NumberAnimation { 60 | target: mover 61 | property: "y" 62 | from: panel.topMargin 63 | to: Screen.height 64 | duration: 1 65 | } 66 | } 67 | 68 | MouseArea { 69 | anchors.fill: panel 70 | enabled: panel.active 71 | onClicked: panel.active = false 72 | } 73 | 74 | MouseArea { 75 | id: mover 76 | width: panel.width 77 | height: panel.height - panel.topMargin 78 | y: Screen.height 79 | clip: true 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /qml/pages/ApplicationsPanel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import Sailfish.Silica 1.0 3 | 4 | SmoothPanel { 5 | id: panel 6 | property MainSocket socket 7 | property var appList: [] 8 | onAppListChanged: { 9 | appsModel.clear() 10 | for (var i = 0; i < appList.length; i++) { 11 | appsModel.append(appList[i]) 12 | } 13 | } 14 | 15 | Rectangle { 16 | id: background 17 | anchors.fill: gridView 18 | border.width: 2 19 | border.color: Theme.highlightDimmerColor 20 | color: Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) 21 | } 22 | 23 | SilicaGridView { 24 | id: gridView 25 | anchors.fill: parent 26 | cellWidth: width / 4 27 | cellHeight: width / 4 28 | clip: true 29 | model: ListModel { 30 | id: appsModel 31 | } 32 | 33 | delegate: Component { 34 | BackgroundItem { 35 | id: item 36 | height: GridView.view.cellHeight 37 | width: GridView.view.cellWidth 38 | 39 | Column { 40 | anchors { 41 | top: parent.top 42 | left: parent.left 43 | right: parent.right 44 | margins: Theme.paddingSmall 45 | } 46 | Image { 47 | anchors.horizontalCenter: parent.horizontalCenter 48 | width: height 49 | height: item.height - label.height - Theme.paddingMedium 50 | source: model.icon 51 | smooth: true 52 | cache: true 53 | asynchronous: true 54 | } 55 | Label { 56 | id: label 57 | width: parent.width 58 | text: model.title 59 | horizontalAlignment: Text.AlignHCenter 60 | wrapMode: Text.NoWrap 61 | elide: Text.ElideRight 62 | font.pixelSize: Theme.fontSizeTiny 63 | } 64 | } 65 | 66 | onClicked: { 67 | socket.launchApp(model.id) 68 | panel.active = false 69 | } 70 | } 71 | } 72 | 73 | VerticalScrollDecorator {} 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /qml/pages/ExtraPanel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import Sailfish.Silica 1.0 3 | 4 | SmoothPanel { 5 | id: panel 6 | property int maxMargin 7 | topMargin: height - content.height 8 | 9 | signal toastMessage 10 | signal openBrowser 11 | signal openYoutube 12 | signal turnOff 13 | signal switchInput 14 | signal touchAcceleration 15 | 16 | Column { 17 | id: content 18 | width: parent.width 19 | 20 | ControlButton { 21 | width: parent.width 22 | height: Theme.itemSizeSmall 23 | title: qsTr("Send toast message") 24 | onClicked: { 25 | panel.active = false 26 | panel.toastMessage() 27 | } 28 | } 29 | 30 | ControlButton { 31 | width: parent.width 32 | height: Theme.itemSizeSmall 33 | title: qsTr("Open link in browser") 34 | onClicked: { 35 | panel.active = false 36 | panel.openYoutube() 37 | } 38 | } 39 | 40 | ControlButton { 41 | width: parent.width 42 | height: Theme.itemSizeSmall 43 | title: qsTr("Open Youtube video") 44 | onClicked: { 45 | panel.active = false 46 | panel.openBrowser() 47 | } 48 | } 49 | 50 | ControlButton { 51 | width: parent.width 52 | height: Theme.itemSizeSmall 53 | title: qsTr("Change touchpad acceleration") 54 | onClicked: { 55 | panel.active = false 56 | panel.touchAcceleration() 57 | } 58 | } 59 | 60 | ControlButton { 61 | width: parent.width 62 | height: Theme.itemSizeSmall 63 | title: qsTr("Switch input") 64 | onClicked: { 65 | panel.active = false 66 | panel.switchInput() 67 | } 68 | } 69 | 70 | ControlButton { 71 | width: parent.width 72 | height: Theme.itemSizeSmall 73 | title: qsTr("Turn off TV") 74 | onClicked: { 75 | panel.active = false 76 | panel.turnOff() 77 | } 78 | } 79 | 80 | ControlButton { 81 | width: parent.width 82 | height: Theme.itemSizeSmall 83 | title: qsTr("About") 84 | onClicked: { 85 | panel.active = false 86 | pageStack.push(Qt.resolvedUrl("AboutPage.qml")) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/settings.h: -------------------------------------------------------------------------------- 1 | #ifndef SETTINGS_H 2 | #define SETTINGS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define AUTH_TEMPLATE "{\"type\":\"register\",\"id\":\"register_%1\",\"payload\":{\"forcePairing\":false,\"pairingType\":\"PIN\",\"client-key\":\"%2\",\"manifest\":{\"manifestVersion\":1,\"appVersion\":\"1.1\",\"signed\":{\"created\":\"20140509\",\"appId\":\"com.lge.test\",\"vendorId\":\"com.lge\",\"localizedAppNames\":{\"\":\"LG Remote App\",\"ko-KR\":\"리모컨 앱\",\"zxx-XX\":\"ЛГ Rэмotэ AПП\"},\"localizedVendorNames\":{\"\":\"LG Electronics\"},\"permissions\":[\"TEST_SECURE\",\"CONTROL_INPUT_TEXT\",\"CONTROL_MOUSE_AND_KEYBOARD\",\"READ_INSTALLED_APPS\",\"READ_LGE_SDX\",\"READ_NOTIFICATIONS\",\"SEARCH\",\"WRITE_SETTINGS\",\"WRITE_NOTIFICATION_ALERT\",\"CONTROL_POWER\",\"READ_CURRENT_CHANNEL\",\"READ_RUNNING_APPS\",\"READ_UPDATE_INFO\",\"UPDATE_FROM_REMOTE_APP\",\"READ_LGE_TV_INPUT_EVENTS\",\"READ_TV_CURRENT_TIME\"],\"serial\":\"2f930e2d2cfe083771f68e4fe7bb07\"},\"permissions\":[\"LAUNCH\",\"LAUNCH_WEBAPP\",\"APP_TO_APP\",\"CLOSE\",\"TEST_OPEN\",\"TEST_PROTECTED\",\"CONTROL_AUDIO\",\"CONTROL_DISPLAY\",\"CONTROL_INPUT_JOYSTICK\",\"CONTROL_INPUT_MEDIA_RECORDING\",\"CONTROL_INPUT_MEDIA_PLAYBACK\",\"CONTROL_INPUT_TV\",\"CONTROL_POWER\",\"READ_APP_STATUS\",\"READ_CURRENT_CHANNEL\",\"READ_INPUT_DEVICE_LIST\",\"READ_NETWORK_STATE\",\"READ_RUNNING_APPS\",\"READ_TV_CHANNEL_LIST\",\"WRITE_NOTIFICATION_TOAST\",\"READ_POWER_STATE\",\"READ_COUNTRY_INFO\"],\"signatures\":[{\"signatureVersion\":1,\"signature\":\"eyJhbGdvcml0aG0iOiJSU0EtU0hBMjU2Iiwia2V5SWQiOiJ0ZXN0LXNpZ25pbmctY2VydCIsInNpZ25hdHVyZVZlcnNpb24iOjF9.hrVRgjCwXVvE2OOSpDZ58hR+59aFNwYDyjQgKk3auukd7pcegmE2CzPCa0bJ0ZsRAcKkCTJrWo5iDzNhMBWRyaMOv5zWSrthlf7G128qvIlpMT0YNY+n/FaOHE73uLrS/g7swl3/qH/BGFG2Hu4RlL48eb3lLKqTt2xKHdCs6Cd4RMfJPYnzgvI4BNrFUKsjkcu+WD4OO2A27Pq1n50cMchmcaXadJhGrOqH5YmHdOCj5NSHzJYrsW0HPlpuAx/ECMeIZYDh6RMqaFM2DXzdKX9NmmyqzJ3o/0lkk/N97gfVRLW5hA29yeAwaCViZNCP8iC9aO0q9fQojoa7NQnAtw==\"}]}}}" 11 | 12 | class Settings : public QObject 13 | { 14 | Q_OBJECT 15 | public: 16 | explicit Settings(QObject *parent = 0); 17 | 18 | Q_PROPERTY(QString bannerPath READ bannerPath NOTIFY bannerPathChanged) 19 | Q_INVOKABLE void checkActivation(const QString &code); 20 | 21 | private: 22 | QString bannerPath() const; 23 | 24 | QString _bannerPath; 25 | QNetworkAccessManager *nam; 26 | 27 | private slots: 28 | void onActivationReply(); 29 | 30 | signals: 31 | void bannerPathChanged(); 32 | 33 | public slots: 34 | QString getAuthMessage(const QString &ip) const; 35 | void setAuthKey(const QString &ip, const QString &key); 36 | 37 | }; 38 | 39 | #endif // SETTINGS_H 40 | -------------------------------------------------------------------------------- /qml/pages/AboutPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import Sailfish.Silica 1.0 3 | 4 | Page { 5 | id: page 6 | 7 | SilicaFlickable { 8 | anchors.fill: page 9 | contentHeight: content.height 10 | 11 | Column { 12 | id: content 13 | anchors { 14 | left: parent.left 15 | right: parent.right 16 | margins: Theme.paddingLarge 17 | } 18 | spacing: Theme.paddingLarge 19 | 20 | PageHeader { 21 | title: qsTr("About") 22 | width: page.width 23 | anchors.horizontalCenter: parent.horizontalCenter 24 | } 25 | 26 | Label { 27 | text: qsTr("webOS TV Remote control\nonly for webOS Smart TV") 28 | width: parent.width 29 | horizontalAlignment: Text.AlignHCenter 30 | wrapMode: Text.Wrap 31 | } 32 | 33 | Label { 34 | text: qsTr("version %1").arg(Qt.application.version) 35 | width: parent.width 36 | horizontalAlignment: Text.AlignHCenter 37 | wrapMode: Text.Wrap 38 | } 39 | 40 | Label { 41 | text: qsTr("by coderus in 0x7DF") 42 | width: parent.width 43 | horizontalAlignment: Text.AlignHCenter 44 | wrapMode: Text.Wrap 45 | } 46 | 47 | Image { 48 | anchors.horizontalCenter: parent.horizontalCenter 49 | source: settings.bannerPath 50 | asynchronous: true 51 | cache: true 52 | } 53 | 54 | Label { 55 | text: qsTr("Send your donations via") 56 | font.pixelSize: Theme.fontSizeMedium 57 | width: parent.width 58 | horizontalAlignment: Text.AlignHCenter 59 | wrapMode: Text.WordWrap 60 | } 61 | 62 | Button { 63 | text: "PayPal EUR" 64 | width: 300 65 | anchors.horizontalCenter: parent.horizontalCenter 66 | onClicked: { 67 | Qt.openUrlExternally("https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=ovi.coderus%40gmail%2ecom&lg=en&lc=US&item_name=Donation%20for%20coderus%20webOS%20TV%20Remote&no_note=0¤cy_code=EUR&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHostedGuest") 68 | } 69 | } 70 | 71 | Button { 72 | text: qsTr("Activate product") 73 | anchors.horizontalCenter: parent.horizontalCenter 74 | onClicked: { 75 | codeField.visible = true 76 | codeField.forceActiveFocus() 77 | } 78 | } 79 | 80 | TextField { 81 | id: codeField 82 | width: parent.width 83 | placeholderText: qsTr("Enter your PayPal e-mail") 84 | label: qsTr("PayPal e-mail") 85 | EnterKey.iconSource: "image://theme/icon-m-enter-next" 86 | EnterKey.onClicked: { 87 | settings.checkActivation(text) 88 | page.forceActiveFocus() 89 | codeField.visible = false 90 | } 91 | visible: false 92 | } 93 | } 94 | 95 | VerticalScrollDecorator {} 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /qml/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import Sailfish.Silica 1.0 3 | import "pages" 4 | ;import QtWebSockets 1.1 5 | ;import Sailfish.Media 1.0 6 | ;import org.nemomobile.policy 1.0 7 | ;import org.nemomobile.configuration 1.0 8 | 9 | ApplicationWindow 10 | { 11 | id: appWindow 12 | 13 | property bool soundMuted: false 14 | property int soundVolume: 0 15 | 16 | property string currentApplication 17 | property string currentApplicationId 18 | 19 | property string currentChannelNumber 20 | property string currentChannelName 21 | 22 | property bool coverActionActive: false 23 | 24 | property string coverIconLeft: "../../images/icon-cover-pause.png" 25 | property string coverIconRight: "../../images/icon-cover-play.png" 26 | 27 | function coverLeftClicked() { 28 | pauseAction() 29 | } 30 | 31 | function coverRightClicked() { 32 | playAction() 33 | } 34 | 35 | function getSocketStatus(status) { 36 | switch (status) { 37 | case WebSocket.Connecting: return "Connecting..." 38 | case WebSocket.Open: return "Connected" 39 | case WebSocket.Closing: return "Disconnecting..." 40 | case WebSocket.Closed: return "Disconnected" 41 | case WebSocket.Error: return "Connection Error" 42 | } 43 | } 44 | 45 | signal playAction 46 | signal pauseAction 47 | signal volumeUpAction 48 | signal volumeDownAction 49 | 50 | initialPage: Component { DiscoverPage { } } 51 | cover: Qt.resolvedUrl("cover/CoverPage.qml") 52 | 53 | property var configuration: ConfigurationValue { 54 | key: "/apps/harbour-lgremote-webos/touchpadAcceleration" 55 | defaultValue: 1.0 56 | } 57 | 58 | MediaKey { 59 | enabled: keysResource.acquired 60 | key: Qt.Key_VolumeUp 61 | onPressed: { 62 | volumeUpAction() 63 | upTimer.interval = 400 64 | upTimer.start() 65 | } 66 | onReleased: upTimer.stop() 67 | Timer { 68 | id: upTimer 69 | repeat: true 70 | onTriggered: { 71 | interval = 60 72 | volumeUpAction() 73 | } 74 | } 75 | } 76 | MediaKey { 77 | enabled: keysResource.acquired 78 | key: Qt.Key_VolumeDown 79 | onPressed: { 80 | volumeDownAction() 81 | downTimer.interval = 400 82 | downTimer.start() 83 | } 84 | onReleased: downTimer.stop() 85 | Timer { 86 | id: downTimer 87 | repeat: true 88 | onTriggered: { 89 | interval = 60 90 | volumeDownAction() 91 | } 92 | } 93 | } 94 | Permissions { 95 | id: permissions 96 | enabled: appWindow.applicationActive && appWindow.coverActionActive 97 | applicationClass: "player" 98 | Resource { 99 | id: keysResource 100 | type: Resource.ScaleButton 101 | optional: true 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /qml/cover/CoverPage.qml: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2013 Jolla Ltd. 3 | Contact: Thomas Perl 4 | All rights reserved. 5 | 6 | You may use this file under the terms of BSD license as follows: 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | * Neither the name of the Jolla Ltd nor the 16 | names of its contributors may be used to endorse or promote products 17 | derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | import QtQuick 2.0 32 | import Sailfish.Silica 1.0 33 | 34 | CoverBackground { 35 | id: coverItem 36 | Image { 37 | id: bgimg 38 | source: "../../images/cover3.png" 39 | anchors.horizontalCenter: parent.horizontalCenter 40 | width: parent.width 41 | height: sourceSize.height * width / sourceSize.width 42 | } 43 | 44 | Column { 45 | anchors { 46 | top: parent.top 47 | left: parent.left 48 | right: parent.right 49 | margins: Theme.paddingLarge 50 | } 51 | 52 | spacing: Theme.paddingLarge 53 | 54 | Label { 55 | width: parent.width 56 | wrapMode: Text.Wrap 57 | horizontalAlignment: Text.AlignHCenter 58 | text: coverActionActive ? currentApplication : qsTr("webOS TV Remote") 59 | } 60 | 61 | Label { 62 | width: parent.width 63 | wrapMode: Text.Wrap 64 | horizontalAlignment: Text.AlignHCenter 65 | visible: currentApplicationId == "com.webos.app.livetv" 66 | text: currentChannelNumber + " " + currentChannelName 67 | } 68 | } 69 | 70 | 71 | Row { 72 | height: mute.height 73 | anchors.centerIn: parent 74 | spacing: Theme.paddingMedium 75 | visible: coverActionActive 76 | 77 | Image { 78 | id: mute 79 | source: "../../images/" + (soundMuted ? "volume-muted" : "volume") + ".png" 80 | } 81 | 82 | Label { 83 | text: soundVolume 84 | anchors.verticalCenter: parent.verticalCenter 85 | } 86 | } 87 | 88 | CoverActionList { 89 | enabled: coverActionActive 90 | 91 | CoverAction { 92 | iconSource: coverIconLeft 93 | onTriggered: coverLeftClicked() 94 | } 95 | 96 | CoverAction { 97 | iconSource: coverIconRight 98 | onTriggered: coverRightClicked() 99 | } 100 | } 101 | } 102 | 103 | 104 | -------------------------------------------------------------------------------- /qml/pages/ChannelsPanel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import Sailfish.Silica 1.0 3 | 4 | SmoothPanel { 5 | id: panel 6 | property MainSocket socket 7 | property var channelsList: [] 8 | property string currentChannelId 9 | onChannelsListChanged: { 10 | channelsFilterModel.update() 11 | } 12 | topMargin: 100 13 | 14 | Rectangle { 15 | id: background 16 | anchors.fill: listView 17 | border.width: 2 18 | border.color: Theme.highlightDimmerColor 19 | color: Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) 20 | } 21 | 22 | SilicaListView { 23 | id: listView 24 | anchors.fill: parent 25 | property string searchPattern 26 | onSearchPatternChanged: { 27 | channelsFilterModel.update() 28 | } 29 | clip: true 30 | currentIndex: -1 31 | header: SearchField { 32 | width: parent.width 33 | placeholderText: qsTr("Channels search") 34 | 35 | onTextChanged: { 36 | listView.searchPattern = text 37 | } 38 | } 39 | delegate: Component { 40 | id: channelsDelegate 41 | BackgroundItem { 42 | id: item 43 | height: Theme.itemSizeSmall 44 | width: ListView.view.width 45 | 46 | Item { 47 | anchors { 48 | fill: parent 49 | leftMargin: Theme.paddingLarge 50 | rightMargin: Theme.paddingLarge 51 | } 52 | 53 | Label { 54 | id: numLabel 55 | anchors { 56 | left: parent.left 57 | verticalCenter: parent.verticalCenter 58 | } 59 | text: Theme.highlightText(model.channelNumber, listView.searchPattern, Theme.highlightColor) 60 | font.bold: currentChannelId == model.channelId 61 | color: item.highlighted ? Theme.highlightColor : Theme.primaryColor 62 | } 63 | 64 | Label { 65 | id: nameLabel 66 | anchors { 67 | left: numLabel.right 68 | leftMargin: Theme.paddingLarge 69 | verticalCenter: parent.verticalCenter 70 | right: parent.right 71 | } 72 | text: Theme.highlightText(model.channelName, listView.searchPattern, Theme.highlightColor) 73 | wrapMode: Text.NoWrap 74 | truncationMode: TruncationMode.Fade 75 | font.bold: currentChannelId == model.channelId 76 | color: item.highlighted ? Theme.highlightColor : Theme.primaryColor 77 | } 78 | } 79 | 80 | onClicked: { 81 | socket.sendOpenChannel(model.channelId) 82 | } 83 | } 84 | } 85 | model: ListModel { 86 | id: channelsFilterModel 87 | 88 | function update() { 89 | clear() 90 | if (channelsList == undefined) { 91 | return 92 | } 93 | 94 | for (var i = 0; i < channelsList.length; i++) { 95 | if (listView.searchPattern == "" 96 | || channelsList[i].channelName.indexOf(listView.searchPattern) >= 0 97 | || channelsList[i].channelNumber.indexOf(listView.searchPattern) >= 0) { 98 | append(channelsList[i]) 99 | } 100 | } 101 | } 102 | } 103 | 104 | ViewPlaceholder { 105 | text: qsTr("You have no channels") 106 | } 107 | 108 | VerticalScrollDecorator {} 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /qml/pages/ActionsPanel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import Sailfish.Silica 1.0 3 | 4 | SmoothPanel { 5 | id: panel 6 | property PointerSocket socket 7 | topMargin: height - content.height 8 | property bool status3D: false 9 | 10 | Column { 11 | id: content 12 | 13 | width: parent.width 14 | 15 | Row { 16 | ControlButton { 17 | width: panel.width / 4 18 | icon: "../../images/icon-media-rewind.png" 19 | onClicked: socket.sendInput("button", "REWIND") 20 | } 21 | ControlButton { 22 | width: panel.width / 4 23 | icon: "../../images/icon-media-previous.png" 24 | onClicked: socket.sendInput("button", "GOTOPREV") 25 | } 26 | ControlButton { 27 | width: panel.width / 4 28 | icon: "../../images/icon-media-next.png" 29 | onClicked: socket.sendInput("button", "GOTONEXT") 30 | } 31 | ControlButton { 32 | width: panel.width / 4 33 | icon: "../../images/icon-media-fastforward.png" 34 | onClicked: socket.sendInput("button", "FASTFORWARD") 35 | } 36 | } 37 | 38 | Row { 39 | ControlButton { 40 | width: panel.width / 4 41 | icon: "../../images/icon-media-stop.png" 42 | onClicked: socket.sendInput("button", "STOP") 43 | } 44 | ControlButton { 45 | width: panel.width / 2 46 | icon: "../../images/icon-media-play.png" 47 | onClicked: socket.sendInput("button", "PLAY") 48 | } 49 | ControlButton { 50 | width: panel.width / 4 51 | icon: "../../images/icon-media-pause.png" 52 | onClicked: socket.sendInput("button", "PAUSE") 53 | } 54 | } 55 | 56 | Row { 57 | ControlButton { 58 | width: panel.width / 4 59 | color: Theme.rgba("red", down ? Theme.highlightBackgroundOpacity : 1.0) 60 | onClicked: socket.sendInput("button", "RED") 61 | } 62 | ControlButton { 63 | width: panel.width / 4 64 | color: Theme.rgba("green", down ? Theme.highlightBackgroundOpacity : 1.0) 65 | onClicked: socket.sendInput("button", "GREEN") 66 | } 67 | ControlButton { 68 | width: panel.width / 4 69 | color: Theme.rgba("orange", down ? Theme.highlightBackgroundOpacity : 1.0) 70 | onClicked: socket.sendInput("button", "YELLOW") 71 | } 72 | ControlButton { 73 | width: panel.width / 4 74 | color: Theme.rgba("blue", down ? Theme.highlightBackgroundOpacity : 1.0) 75 | onClicked: socket.sendInput("button", "BLUE") 76 | } 77 | } 78 | 79 | Row { 80 | ControlButton { 81 | width: panel.width / 3 82 | title: qsTr("HOME") 83 | onClicked: socket.sendInput("button", "HOME") 84 | } 85 | ControlButton { 86 | width: panel.width / 3 87 | icon: "../../images/icon-arrow-up.png" 88 | onClicked: socket.sendInput("button", "UP") 89 | } 90 | ControlButton { 91 | width: panel.width / 3 92 | title: qsTr("3D") 93 | bold: status3D 94 | onClicked: socket.sendInput("button", "3D_MODE") 95 | } 96 | } 97 | 98 | Row { 99 | ControlButton { 100 | width: panel.width / 3 101 | icon: "../../images/icon-arrow-left.png" 102 | onClicked: socket.sendInput("button", "LEFT") 103 | } 104 | ControlButton { 105 | width: panel.width / 3 106 | title: qsTr("OK") 107 | onClicked: socket.sendInput("button", "ENTER") 108 | } 109 | ControlButton { 110 | width: panel.width / 3 111 | icon: "../../images/icon-arrow-right.png" 112 | onClicked: socket.sendInput("button", "RIGHT") 113 | } 114 | } 115 | 116 | Row { 117 | ControlButton { 118 | width: panel.width / 3 119 | title: qsTr("EXIT") 120 | onClicked: socket.sendInput("button", "EXIT") 121 | } 122 | ControlButton { 123 | width: panel.width / 3 124 | icon: "../../images/icon-arrow-down.png" 125 | onClicked: socket.sendInput("button", "DOWN") 126 | } 127 | ControlButton { 128 | width: panel.width / 3 129 | title: qsTr("BACK") 130 | onClicked: socket.sendInput("button", "BACK") 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /qml/pages/TouchpadPanel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import Sailfish.Silica 1.0 3 | 4 | SmoothPanel { 5 | id: panel 6 | property PointerSocket socket 7 | topMargin: 100 8 | 9 | Column { 10 | width: parent.width 11 | 12 | Row { 13 | id: controls 14 | ControlButton { 15 | width: 80 16 | height: 80 17 | icon: "image://theme/icon-m-rotate-left" 18 | onClicked: socket.sendInput("button", "BACK") 19 | } 20 | 21 | ControlButton { 22 | width: 80 23 | height: 80 24 | icon: "image://theme/icon-m-close" 25 | onClicked: socket.sendInput("button", "EXIT") 26 | } 27 | 28 | ControlButton { 29 | width: panel.width - 320 30 | height: 80 31 | icon: "image://theme/icon-m-home" 32 | onClicked: socket.sendInput("button", "HOME") 33 | } 34 | 35 | ControlButton { 36 | width: 80 37 | height: 80 38 | icon: "image://theme/icon-m-page-up" 39 | onClicked: socket.sendScroll(1) 40 | onPressed: scrollDownTimer.start() 41 | onReleased: scrollDownTimer.stop() 42 | 43 | Timer { 44 | id: scrollDownTimer 45 | repeat: true 46 | interval: 600 47 | onTriggered: socket.sendScroll(1) 48 | } 49 | } 50 | 51 | ControlButton { 52 | width: 80 53 | height: 80 54 | icon: "image://theme/icon-m-page-down" 55 | onClicked: socket.sendScroll(-1) 56 | onPressed: scrolUpTimer.start() 57 | onReleased: scrolUpTimer.stop() 58 | 59 | Timer { 60 | id: scrolUpTimer 61 | repeat: true 62 | interval: 600 63 | onTriggered: socket.sendScroll(-1) 64 | } 65 | } 66 | } 67 | 68 | MultiPointTouchArea { 69 | id: mTouchArea 70 | width: parent.width 71 | height: panel.height - panel.topMargin - controls.height 72 | enabled: socket.connected 73 | maximumTouchPoints: 2 74 | minimumTouchPoints: 1 75 | touchPoints: [ 76 | TouchPoint { id: point1 }, 77 | TouchPoint { id: point2 } 78 | ] 79 | property double pressTime 80 | property real lastDelta 81 | onPressed: { 82 | if (point1.pressed && !point2.pressed) { 83 | var date = new Date() 84 | pressTime = date.getTime() 85 | } 86 | } 87 | onReleased: { 88 | if (!point1.pressed && !point2.pressed) { 89 | var date = new Date() 90 | var releaseTime = date.getTime() 91 | if (releaseTime - pressTime < 600 92 | && (point1.x - point1.startX < 2.0) 93 | && (point1.y - point1.startY < 2.0)) { 94 | socket.sendClick() 95 | } 96 | } 97 | } 98 | 99 | onUpdated: { 100 | if (point2.pressed) { 101 | var stop = (point1.y + point2.y) / 2 102 | var previous = (point1.previousY + point2.previousY) / 2 103 | var delta = stop - previous 104 | lastDelta -= delta 105 | if (Math.abs(lastDelta) > 10) { 106 | socket.sendScroll(Math.round(lastDelta / 10)) 107 | lastDelta = 0.0 108 | } 109 | } 110 | else { 111 | lastDelta = 0.0 112 | if (point1.pressed) { 113 | var accel = appWindow.configuration.value 114 | var dx = (point1.x - point1.previousX) * accel 115 | var dy = (point1.y - point1.previousY) * accel 116 | socket.sendMove(dx, dy) 117 | } 118 | else { 119 | 120 | } 121 | } 122 | } 123 | 124 | Rectangle { 125 | id: background 126 | anchors.fill: parent 127 | border.width: 2 128 | border.color: Theme.highlightDimmerColor 129 | color: Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) 130 | } 131 | 132 | Label { 133 | anchors { 134 | left: parent.left 135 | right: parent.right 136 | bottom: parent.bottom 137 | margins: Theme.paddingLarge 138 | } 139 | wrapMode: Text.Wrap 140 | horizontalAlignment: Text.AlignHCenter 141 | text: qsTr("Drag one finger to move cursor, two fingers to scroll, tap to click") 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /qml/pages/DiscoverPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import Sailfish.Silica 1.0 3 | 4 | 5 | Page { 6 | id: page 7 | 8 | property bool searching: false 9 | 10 | function discover() { 11 | discoveredTargets.clear() 12 | searching = true 13 | network.startSearch(); 14 | timeoutTimer.restart() 15 | } 16 | 17 | Component.onCompleted: { 18 | discover() 19 | } 20 | 21 | Connections { 22 | target: network 23 | onDiscovered: { 24 | discoveredTargets.append({"name": result.name, 25 | "ip": result.ip, 26 | "modelNumber": result.modelNumber, 27 | "manufacturer": result.manufacturer}) 28 | } 29 | onTimeout: { 30 | searching = false 31 | } 32 | } 33 | 34 | ListModel { 35 | id: discoveredTargets 36 | } 37 | 38 | Component { 39 | id: discoveredDelegate 40 | BackgroundItem { 41 | id: innerItem 42 | height: Theme.itemSizeLarge 43 | 44 | Column { 45 | anchors { 46 | left: parent.left 47 | right: parent.right 48 | margins: Theme.paddingLarge 49 | verticalCenter: parent.verticalCenter 50 | } 51 | 52 | Row { 53 | spacing: Theme.paddingLarge 54 | 55 | Label { 56 | color: innerItem.down ? Theme.highlightColor : Theme.primaryColor 57 | text: model.name 58 | } 59 | 60 | Label { 61 | anchors.verticalCenter: parent.verticalCenter 62 | color: innerItem.down ? Theme.secondaryHighlightColor : Theme.secondaryColor 63 | text: model.modelNumber 64 | font.pixelSize: Theme.fontSizeSmall 65 | } 66 | } 67 | 68 | Label { 69 | color: innerItem.down ? Theme.secondaryHighlightColor : Theme.secondaryColor 70 | text: model.manufacturer 71 | font.pixelSize: Theme.fontSizeSmall 72 | } 73 | 74 | Label { 75 | color: innerItem.down ? Theme.secondaryHighlightColor : Theme.secondaryColor 76 | text: model.ip 77 | font.pixelSize: Theme.fontSizeSmall 78 | } 79 | } 80 | 81 | onClicked: { 82 | console.log("Selected ip: " + model.ip) 83 | pageStack.replace(Qt.resolvedUrl("MainPage.qml"), {"model": model}) 84 | } 85 | } 86 | } 87 | 88 | SilicaFlickable { 89 | anchors.fill: parent 90 | 91 | PullDownMenu { 92 | MenuItem { 93 | text: qsTr("About") 94 | onClicked: pageStack.push(Qt.resolvedUrl("AboutPage.qml")) 95 | } 96 | 97 | MenuItem { 98 | text: qsTr("Discover by ip") 99 | onClicked: { 100 | ipField.visible = true 101 | ipField.forceActiveFocus() 102 | } 103 | } 104 | 105 | MenuItem { 106 | text: qsTr("Refresh") 107 | onClicked: discover() 108 | } 109 | } 110 | 111 | contentHeight: column.height 112 | 113 | Column { 114 | id: column 115 | 116 | width: page.width 117 | spacing: Theme.paddingLarge 118 | PageHeader { 119 | title: qsTr("Discover webOS TV") 120 | } 121 | 122 | TextField { 123 | id: ipField 124 | width: parent.width 125 | placeholderText: "192.168.1.123" 126 | label: qsTr("IP address") 127 | EnterKey.iconSource: "image://theme/icon-m-enter-next" 128 | EnterKey.onClicked: { 129 | network.searchIp(text) 130 | page.forceActiveFocus() 131 | ipField.visible = false 132 | } 133 | visible: false 134 | } 135 | 136 | Repeater { 137 | id: repeater 138 | width: parent.width 139 | model: discoveredTargets 140 | delegate: discoveredDelegate 141 | } 142 | } 143 | 144 | VerticalScrollDecorator {} 145 | } 146 | 147 | Label { 148 | anchors { 149 | left: parent.left 150 | right: parent.right 151 | bottom: parent.bottom 152 | margins: Theme.paddingLarge 153 | } 154 | wrapMode: Text.Wrap 155 | horizontalAlignment: Text.AlignHCenter 156 | text: qsTr("Taking too long? Check if you have compatible webOS Smart TV, it's powered and switched on") 157 | visible: discoveredTargets.count == 0 && !timeoutTimer.running 158 | } 159 | 160 | Timer { 161 | id: timeoutTimer 162 | interval: 10000 163 | repeat: false 164 | running: true 165 | } 166 | 167 | BusyIndicator { 168 | id: busyIndicator 169 | anchors.centerIn: parent 170 | size: BusyIndicatorSize.Large 171 | running: visible 172 | visible: discoveredTargets.count == 0 && searching 173 | } 174 | 175 | Label { 176 | anchors.top: busyIndicator.bottom 177 | visible: busyIndicator.running 178 | text: qsTr("Searching devices...") 179 | anchors.horizontalCenter: busyIndicator.horizontalCenter 180 | } 181 | } 182 | 183 | 184 | -------------------------------------------------------------------------------- /src/networkobserver.cpp: -------------------------------------------------------------------------------- 1 | #include "networkobserver.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | // C 9 | #include 10 | #include 11 | #include 12 | #include 13 | #ifndef Q_WS_WIN 14 | #include 15 | #include 16 | #endif 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #define SSDP_BROADCAST_ADDRESS "239.255.255.250" 24 | #define SSDP_PORT_NUMBER 1900 25 | #define SSDP_PORT "1900" 26 | #define WEBOSSCREEN "urn:lge-com:service:webos-second-screen:1" 27 | 28 | static const int SSDPPortNumber = SSDP_PORT_NUMBER; 29 | static const char SSDPBroadCastAddress[] = SSDP_BROADCAST_ADDRESS; 30 | 31 | NetworkObserver::NetworkObserver(QObject *parent) : 32 | QObject(parent) 33 | { 34 | nam = new QNetworkAccessManager(this); 35 | timeoutTimer = new QTimer(this); 36 | timeoutTimer->setSingleShot(false); 37 | timeoutTimer->setInterval(30000); 38 | QObject::connect(timeoutTimer, SIGNAL(timeout()), this, SIGNAL(timeout())); 39 | } 40 | 41 | NetworkObserver::~NetworkObserver() 42 | { 43 | } 44 | 45 | void NetworkObserver::startSearch() 46 | { 47 | if (timeoutTimer->isActive()) { 48 | timeoutTimer->stop(); 49 | } 50 | discoveredDevices.clear(); 51 | 52 | // send a HTTP M-SEARCH message to 239.255.255.250:1900 53 | const char mSearchMessage[] = 54 | "M-SEARCH * HTTP/1.1\r\n" 55 | "HOST: "SSDP_BROADCAST_ADDRESS":"SSDP_PORT"\r\n" 56 | "ST: "WEBOSSCREEN"\r\n" 57 | "MAN: \"ssdp:discover\"\r\n" 58 | "MX: 30\r\n" // max number of seconds to wait for response 59 | "\r\n"; 60 | const int mSearchMessageLength = sizeof(mSearchMessage) / sizeof(mSearchMessage[0]); 61 | 62 | foreach (QNetworkInterface iface, QNetworkInterface::allInterfaces()) { 63 | foreach (QNetworkAddressEntry addr, iface.addressEntries()) { 64 | QUdpSocket *socket = new QUdpSocket(this); 65 | QObject::connect(socket, SIGNAL(readyRead()), SLOT(onUdpSocketReadyRead())); 66 | QObject::connect(this, SIGNAL(timeout()), socket, SLOT(deleteLater())); 67 | if (addr.ip().protocol() == QUdpSocket::IPv4Protocol && socket->bind(addr.ip(), SSDPPortNumber + 1 ,QUdpSocket::ShareAddress)) { 68 | //qDebug() << addr.ip().toString(); 69 | socket->joinMulticastGroup(QHostAddress(SSDPBroadCastAddress)); 70 | socket->writeDatagram( mSearchMessage, mSearchMessageLength, QHostAddress(SSDPBroadCastAddress), SSDPPortNumber ); 71 | } 72 | else { 73 | socket->deleteLater(); 74 | } 75 | } 76 | } 77 | 78 | timeoutTimer->start(); 79 | } 80 | 81 | void NetworkObserver::stopSearch() 82 | { 83 | if (timeoutTimer->isActive()) { 84 | timeoutTimer->stop(); 85 | } 86 | Q_EMIT timeout(); 87 | } 88 | 89 | void NetworkObserver::searchIp(const QString &ip) 90 | { 91 | QString location("http://%1:1939/"); 92 | checkDeviceInfo(location.arg(ip)); 93 | } 94 | 95 | void NetworkObserver::handleMessage(const QByteArray &message) 96 | { 97 | const QStringList lines = QString::fromUtf8( message ).split( "\r\n" ); 98 | 99 | // first read first line and see if contains a HTTP 200 OK message or 100 | // "HTTP/1.1 200 OK" 101 | // "NOTIFY * HTTP/1.1" 102 | const QString firstLine = lines.first(); 103 | if( ! firstLine.contains("HTTP") 104 | || (! firstLine.contains("NOTIFY") 105 | && ! firstLine.contains("200 OK")) ) 106 | return; 107 | 108 | // read all lines and try to find the location field 109 | foreach( const QString& line, lines ) 110 | { 111 | const int separatorIndex = line.indexOf( ':' ); 112 | const QString key = line.left( separatorIndex ).toUpper(); 113 | const QString value = line.mid( separatorIndex+1 ).trimmed(); 114 | 115 | if( key == QLatin1String("LOCATION") ) 116 | { 117 | checkDeviceInfo(value); 118 | } 119 | } 120 | } 121 | 122 | void NetworkObserver::onUdpSocketReadyRead() 123 | { 124 | QUdpSocket *socket = qobject_cast(sender()); 125 | if (socket) { 126 | const int pendingDatagramSize = socket->pendingDatagramSize(); 127 | 128 | QByteArray message(pendingDatagramSize, 0); 129 | const int bytesRead = socket->readDatagram( message.data(), pendingDatagramSize ); 130 | if( bytesRead == -1 ) 131 | return; 132 | 133 | handleMessage(message); 134 | } 135 | } 136 | 137 | void NetworkObserver::checkDeviceInfo(const QString &server) 138 | { 139 | if (discoveredDevices.contains(server)) { 140 | return; 141 | } 142 | else { 143 | discoveredDevices.append(server); 144 | } 145 | 146 | QXmlQuery query; 147 | query.setNetworkAccessManager(nam); 148 | query.setFocus(QUrl(server)); 149 | 150 | QString queryTemplate("declare default element namespace \"urn:schemas-upnp-org:device-1-0\"; /root/device/%1/string()"); 151 | 152 | query.setQuery(queryTemplate.arg("friendlyName")); 153 | QString friendlyName; 154 | query.evaluateTo(&friendlyName); 155 | friendlyName = friendlyName.trimmed(); 156 | 157 | query.setQuery(queryTemplate.arg("manufacturer")); 158 | QString manufacturer; 159 | query.evaluateTo(&manufacturer); 160 | manufacturer = manufacturer.trimmed(); 161 | 162 | query.setQuery(queryTemplate.arg("modelNumber")); 163 | QString modelNumber; 164 | query.evaluateTo(&modelNumber); 165 | modelNumber = modelNumber.trimmed(); 166 | 167 | qDebug() << server << friendlyName << manufacturer << modelNumber; 168 | QVariantMap result; 169 | result["ip"] = QUrl(server).host(); 170 | result["name"] = friendlyName; 171 | result["manufacturer"] = manufacturer; 172 | result["modelNumber"] = modelNumber; 173 | Q_EMIT discovered(result); 174 | } 175 | -------------------------------------------------------------------------------- /qml/pages/MainSocket.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | ;import QtWebSockets 1.1 3 | 4 | WebSocket { 5 | id: mainSocket 6 | active: false 7 | 8 | signal pointerSocketReceived(string url) 9 | signal pairingReceived 10 | signal keyboardInput(bool haveFocus) 11 | 12 | signal connected 13 | signal disconnected 14 | 15 | property var applications 16 | property var appsList: [] 17 | property var externalinput 18 | property var inputList: [] 19 | 20 | property var channelsTv: [] 21 | 22 | property bool muting: false 23 | onMutingChanged: soundMuted = muting 24 | property int volume: 0 25 | onVolumeChanged: soundVolume = volume 26 | 27 | property bool status3D: false 28 | 29 | property string currentAppId 30 | onCurrentAppIdChanged: { 31 | currentApplicationId = currentAppId 32 | currentApplication = applications[currentAppId] == undefined ? externalinput[currentAppId].label : applications[currentAppId].title 33 | } 34 | 35 | property string channelNumber 36 | onChannelNumberChanged: currentChannelNumber = channelNumber 37 | 38 | property string channelName 39 | onChannelNameChanged: currentChannelName = channelName 40 | 41 | property string channelId 42 | 43 | 44 | onStatusChanged: { 45 | if (status == WebSocket.Open) { 46 | connected() 47 | } 48 | else if (status == WebSocket.Closed) { 49 | msgId = 0 50 | disconnected() 51 | } 52 | } 53 | 54 | onTextMessageReceived: { 55 | var msg = JSON.parse(message) 56 | 57 | //messagesModel.append({name: message, socket: "m", direction: "<"}) 58 | console.log("<:m " + message) 59 | 60 | if (msg.type == "registered") { 61 | mainSocket.registered = true 62 | mainSocket.pairing = false 63 | var authKey = msg.payload["client-key"] 64 | settings.setAuthKey(deviceIp, authKey) 65 | 66 | //socket.sendCommand("muting_", "subscribe", "ssap://audio/getMute") 67 | mainSocket.sendCommand("volume_", "subscribe", "ssap://audio/getVolume") 68 | //mainSocket.sendCommand("status_", "subscribe", "ssap://audio/getStatus") 69 | 70 | //mainSocket.sendCommand("http_header_", "request", "ssap://com.webos.service.sdx/getHttpHeaderForServiceRequest") 71 | mainSocket.sendCommand("sw_info_", "request", "ssap://com.webos.service.update/getCurrentSWInformation") 72 | 73 | mainSocket.sendCommand("services_", "request", "ssap://api/getServiceList") 74 | 75 | //mainSocket.sendCommand("apps_", "subscribe", "ssap://com.webos.applicationManager/listApps") 76 | mainSocket.sendCommand("launcher_", "subscribe", "ssap://com.webos.applicationManager/listLaunchPoints") 77 | mainSocket.sendCommand("keyboard_", "subscribe", "ssap://com.webos.service.ime/registerRemoteKeyboard") 78 | mainSocket.sendCommand("events_", "subscribe", "ssap://com.webos.service.tv.keymanager/listInterestingEvents", {"subscribe": true}) 79 | mainSocket.sendCommand("foreground_app_", "subscribe", "ssap://com.webos.applicationManager/getForegroundAppInfo") 80 | mainSocket.sendCommand("channels_", "subscribe", "ssap://tv/getChannelList") 81 | mainSocket.sendCommand("channel_", "subscribe", "ssap://tv/getCurrentChannel") 82 | mainSocket.sendCommand("input_", "subscribe", "ssap://tv/getExternalInputList") 83 | mainSocket.sendCommand("3dstatus_", "subscribe", "ssap://com.webos.service.tv.display/get3DStatus") 84 | 85 | mainSocket.sendCommand("get_pointer_", "request", "ssap://com.webos.service.networkinput/getPointerInputSocket") 86 | } 87 | else if (msg.type == "response") { 88 | if (msg.id.indexOf("register_") == 0) { 89 | if (msg.payload.pairingType == "PIN") { 90 | mainSocket.pairing = true 91 | pairingReceived() 92 | } 93 | } 94 | else if (msg.id.indexOf("get_pointer_") == 0) { 95 | console.log("pointer data received: " + msg.payload.returnValue) 96 | if (msg.payload.returnValue) { 97 | console.log("pointer socket: " + msg.payload.socketPath) 98 | pointerSocketReceived(msg.payload.socketPath) 99 | } 100 | } 101 | else if (msg.id.indexOf("muting_") == 0) { 102 | mainSocket.muting = msg.payload.mute 103 | } 104 | else if (msg.id.indexOf("volume_") == 0) { 105 | mainSocket.volume = msg.payload.volume 106 | mainSocket.muting = msg.payload.muted 107 | //volSlider.value = msg.payload.volume 108 | } 109 | else if (msg.id.indexOf("sw_info_") == 0) { 110 | //infoText.text = JSON.stringify(msg.payload) 111 | } 112 | else if (msg.id.indexOf("apps_") == 0) { 113 | /*var appList = msg.payload.apps 114 | var apps = {} 115 | for (var i = 0; i < appList.length; i++) { 116 | if (appList[i].id != undefined) { 117 | apps[appList[i].id] = appList[i] 118 | } 119 | } 120 | applications = apps 121 | if (currentAppId.length > 0) { 122 | currentApplication = applications[currentAppId].title 123 | }*/ 124 | } 125 | else if (msg.id.indexOf("launcher_") == 0) { 126 | var launchPoints = msg.payload.launchPoints 127 | appsList = launchPoints 128 | var apps = {} 129 | for (var i = 0; i < appsList.length; i++) { 130 | if (appsList[i].id != undefined) { 131 | apps[appsList[i].id] = appsList[i] 132 | } 133 | } 134 | applications = apps 135 | if (currentAppId.length > 0) { 136 | currentApplication = applications[currentAppId].title 137 | } 138 | } 139 | else if (msg.id.indexOf("foreground_app_") == 0) { 140 | currentAppId = msg.payload.appId 141 | //var sessionId = Qt.btoa(currentAppId + ":undefined") 142 | //mainSocket.sendCommand("", "request", "ssap://system.launcher/getAppState", {"id": currentAppId, "sessionId": sessionId}) 143 | } 144 | else if (msg.id.indexOf("keyboard_") == 0) { 145 | if (msg.payload.focusChanged || msg.payload.subscribed) { 146 | keyboardFocus = msg.payload.currentWidget.focus 147 | } 148 | } 149 | else if (msg.id.indexOf("channels_") == 0) { 150 | channelsTv = msg.payload.channelList 151 | } 152 | else if (msg.id.indexOf("channel_") == 0) { 153 | channelName = msg.payload.channelName 154 | channelNumber = msg.payload.channelNumber 155 | channelId = msg.payload.channelId 156 | } 157 | else if (msg.id.indexOf("input_") == 0) { 158 | inputList = msg.payload.devices 159 | var devices = {} 160 | for (var i = 0; i < inputList.length; i++) { 161 | if (inputList[i].id != undefined) { 162 | devices[inputList[i].appId] = inputList[i] 163 | } 164 | } 165 | externalinput = devices 166 | if (currentAppId.length > 0 && devices[currentAppId] != undefined) { 167 | currentApplication = devices[currentAppId].label 168 | } 169 | } 170 | else if (msg.id.indexOf("3dstatus_") == 0) { 171 | status3D = msg.payload.status3D.status 172 | } 173 | else { 174 | return 175 | } 176 | } 177 | } 178 | 179 | onErrorStringChanged: { 180 | console.log("mainSocket: " + errorString) 181 | } 182 | 183 | property bool pairing: false 184 | property bool registered: false 185 | property bool ready: registered && status == WebSocket.Open 186 | 187 | property bool keyboardFocus: false 188 | 189 | property int msgId: 0 190 | 191 | function getMsgId() { 192 | msgId = msgId + 1 193 | return msgId 194 | } 195 | 196 | function sendLogMessage(message) { 197 | if (mainSocket.status == WebSocket.Open) { 198 | //messagesModel.append({name: message, socket: "m", direction: ">"}) 199 | console.log(">:m " + message) 200 | mainSocket.sendTextMessage(message) 201 | } 202 | } 203 | 204 | function sendCommand(prefix, msgtype, uri, payload) { 205 | var msg = {} 206 | msg["id"] = prefix + mainSocket.getMsgId() 207 | msg["type"] = msgtype 208 | msg["uri"] = uri 209 | if (payload) { 210 | msg["payload"] = payload 211 | } 212 | 213 | mainSocket.sendLogMessage(JSON.stringify(msg)) 214 | } 215 | 216 | function sendPin(pincode) { 217 | mainSocket.sendCommand("pin_", "request", "ssap://pairing/setPin", {"pin": pincode}) 218 | } 219 | 220 | function sendEnter() { 221 | mainSocket.sendCommand("", "request", "ssap://com.webos.service.ime/sendEnterKey") 222 | } 223 | 224 | function launcher(appId) { 225 | mainSocket.sendCommand("", "request", "ssap://system.launcher/launch", {"id": appId}) 226 | } 227 | 228 | function closeApp(appId) { 229 | mainSocket.sendCommand("", "request", "ssap://system.launcher/close", {"id": appId, "sessionId": Qt.btoa(appId + ":undefined")}) 230 | } 231 | 232 | function launchApp(appId) { 233 | mainSocket.sendCommand("", "request", "ssap://com.webos.applicationManager/launch", {"id": appId}) 234 | } 235 | 236 | function launchWithPayload(payload) { 237 | mainSocket.sendCommand("", "request", "ssap://com.webos.applicationManager/launch", payload) 238 | } 239 | 240 | function browserUrl(url) { 241 | launchWithPayload({"id": "com.webos.app.browser", "target": url}) 242 | } 243 | 244 | function openYoutube(video) { 245 | launchWithPayload({"id": "youtube.leanback.v4", "params": { "contentTarget": ("http://www.youtube.com/tv?v=" + video) }}) 246 | } 247 | 248 | function sendPause() { 249 | mainSocket.sendCommand("pause_", "request", "ssap://media.controls/pause") 250 | } 251 | 252 | function sendPlay() { 253 | mainSocket.sendCommand("play_", "request", "ssap://media.controls/play") 254 | } 255 | 256 | function sendStop() { 257 | mainSocket.sendCommand("stop_", "request", "ssap://media.controls/stop") 258 | } 259 | 260 | function sendVolumeUp() { 261 | mainSocket.sendCommand("volumeup_", "request", "ssap://audio/volumeUp") 262 | } 263 | 264 | function sendVolumeDown() { 265 | mainSocket.sendCommand("volumedown_", "request", "ssap://audio/volumeDown") 266 | } 267 | 268 | function toggleMute() { 269 | mainSocket.sendCommand("", "request", "ssap://audio/setMute", {"mute": !mainSocket.muting}) 270 | } 271 | 272 | function sendBackspace(count) { 273 | mainSocket.sendCommand("", "request", "ssap://com.webos.service.ime/deleteCharacters", {"count": count == undefined ? 1 : count}) 274 | } 275 | 276 | function sendText(text, replace) { 277 | mainSocket.sendCommand("", "request", "ssap://com.webos.service.ime/insertText", {"text": text, "replace": replace == true}) 278 | } 279 | 280 | function sendTurnOff() { 281 | mainSocket.sendCommand("", "request", "ssap://system/turnOff") 282 | } 283 | 284 | function sendShowToast(text) { 285 | mainSocket.sendCommand("", "request", "ssap://system.notifications/createToast", {"message": text}) 286 | } 287 | 288 | function sendOpenChannel(channelId) { 289 | mainSocket.sendCommand("", "request", "ssap://tv/openChannel", {"channelId": channelId}) 290 | } 291 | 292 | function sendSwitchInput(inputId) { 293 | mainSocket.sendCommand("", "request", "ssap://tv/switchInput", {"inputId": inputId}) 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /qml/pages/MainPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import Sailfish.Silica 1.0 3 | 4 | Page { 5 | id: mainPage 6 | allowedOrientations: Orientation.Portrait 7 | 8 | property var model 9 | onModelChanged: { 10 | if (model != undefined) { 11 | deviceIp = model.ip 12 | deviceName = model.name 13 | modelNumber = model.modelNumber 14 | manufacturer = model.manufacturer 15 | var ws = "ws://" + deviceIp + ":3000" 16 | console.log("connecting to: " + ws) 17 | mainSocket.url = ws 18 | } 19 | } 20 | 21 | function openBrowser(text) { 22 | var isYoutube = text.indexOf("youtube.com/watch?v=") != -1 23 | if (isYoutube) { 24 | var videoId = text.indexOf("watch?v=") 25 | if (videoId != -1) { 26 | var res = text.substring(videoId + 8) 27 | mainSocket.openYoutube(res) 28 | } 29 | else { 30 | mainSocket.browserUrl(text) 31 | } 32 | } 33 | else { 34 | mainSocket.browserUrl(text) 35 | } 36 | } 37 | 38 | Component.onCompleted: { 39 | mainSocket.active = true 40 | network.stopSearch() 41 | 42 | var dbusAdaptor = Qt.createQmlObject(Qt.atob("aW1wb3J0IFF0UXVpY2sgMi4xOyBpbXBvcnQgb3JnLm5lbW9tb2JpbGUuZGJ1cyAxLjA7IERCdXNBZGFwdG9yIHtzZXJ2aWNlOiAiaGFyYm91ci5jb2RlcnVzLmxncmVtb3RlIjsgcGF0aDogIi9oYXJib3VyL2NvZGVydXMvbGdyZW1vdGUiOyBpZmFjZTogImhhcmJvdXIuY29kZXJ1cy5sZ3JlbW90ZSI7IHhtbDogUXQuYXRvYigiUEdsdWRHVnlabUZqWlNCdVlXMWxQU0pvWVhKaWIzVnlMbU52WkdWeWRYTXViR2R5WlcxdmRHVWlQanh0WlhSb2IyUWdibUZ0WlQwaWIzQmxia3hwYm1zaVBqeGhibTV2ZEdGMGFXOXVJRzVoYldVOUltOXlaeTVtY21WbFpHVnphM1J2Y0M1RVFuVnpMazFsZEdodlpDNU9iMUpsY0d4NUlpQjJZV3gxWlQwaWRISjFaU0l2UGp4aGNtY2daR2x5WldOMGFXOXVQU0pwYmlJZ2RIbHdaVDBpY3lJdlBqd3ZiV1YwYUc5a1Bqd3ZhVzUwWlhKbVlXTmxQZz09Iik7IHNpZ25hbCBvcGVuTGluayhzdHJpbmcgbGluayl9"), mainPage) 43 | dbusAdaptor.openLink.connect(mainPage.openBrowser) 44 | } 45 | 46 | property string deviceIp 47 | property string deviceName 48 | property string modelNumber 49 | property string manufacturer 50 | 51 | property string pointerAddress 52 | 53 | Connections { 54 | target: appWindow 55 | onPauseAction: { 56 | //pointerSocket.sendInput("button", "PAUSE") 57 | mainSocket.sendPause() 58 | } 59 | onPlayAction: { 60 | //pointerSocket.sendInput("button", "PLAY") 61 | mainSocket.sendPlay() 62 | } 63 | onVolumeUpAction: { 64 | mainSocket.sendVolumeUp() 65 | //pointerSocket.sendInput("button", "VOLUMEUP") 66 | } 67 | onVolumeDownAction: { 68 | mainSocket.sendVolumeDown() 69 | //pointerSocket.sendInput("button", "VOLUMEDOWN") 70 | } 71 | } 72 | 73 | PointerSocket { 74 | id: pointerSocket 75 | url: mainPage.pointerAddress 76 | } 77 | 78 | MainSocket { 79 | id: mainSocket 80 | 81 | onConnected: { 82 | var msg = settings.getAuthMessage(deviceIp) 83 | mainSocket.sendLogMessage(msg) 84 | } 85 | 86 | onDisconnected: { 87 | pageStack.replace(Qt.resolvedUrl("DiscoverPage.qml")) 88 | } 89 | 90 | onReadyChanged: coverActionActive = ready 91 | 92 | onPointerSocketReceived: { 93 | pointerAddress = url 94 | } 95 | 96 | onPairingReceived: { 97 | pin.active = true 98 | } 99 | 100 | onKeyboardFocusChanged: { 101 | toggleKeyboard(keyboardFocus) 102 | } 103 | } 104 | 105 | function toggleKeyboard(haveFocus) { 106 | console.log("toggleKeyboard: " + haveFocus) 107 | if (haveFocus) { 108 | invisibleInput.forceActiveFocus() 109 | } 110 | else { 111 | mainPage.forceActiveFocus() 112 | } 113 | } 114 | 115 | TextEdit { 116 | id: invisibleInput 117 | width: 0 118 | height: 0 119 | inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase 120 | onTextChanged: { 121 | if (text.length == 1) { 122 | mainSocket.sendText(text) 123 | } 124 | text = "" 125 | } 126 | Keys.onPressed: { 127 | if (event.key == Qt.Key_Backspace) { 128 | mainSocket.sendBackspace() 129 | event.accepted = true; 130 | } 131 | else if (event.key == Qt.Key_Return) { 132 | mainSocket.sendEnter() 133 | event.accepted = true; 134 | } 135 | } 136 | } 137 | 138 | Column { 139 | id: content 140 | anchors { 141 | top: parent.top 142 | left: parent.left 143 | right: parent.right 144 | leftMargin: Theme.paddingLarge 145 | rightMargin: Theme.paddingLarge 146 | } 147 | property int smallItemSize: (width - (Theme.itemSizeExtraLarge * 2)) / 4 148 | 149 | spacing: Theme.paddingMedium 150 | 151 | Label { 152 | width: parent.width 153 | text: currentApplication 154 | wrapMode: Text.Wrap 155 | horizontalAlignment: Text.AlignHCenter 156 | } 157 | 158 | Label { 159 | width: parent.width 160 | wrapMode: Text.Wrap 161 | horizontalAlignment: Text.AlignHCenter 162 | visible: currentApplicationId == "com.webos.app.livetv" 163 | text: mainSocket.channelName 164 | } 165 | 166 | Row { 167 | height: Theme.itemSizeExtraLarge 168 | 169 | ControlButton { 170 | height: parent.height 171 | width: content.smallItemSize 172 | title: "-" 173 | titleSize: height / 2 174 | color: down ? Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) : "transparent" 175 | borderWidth: 0 176 | onPressed: { 177 | mainSocket.sendVolumeDown() 178 | downTimer.interval = 400 179 | downTimer.start() 180 | } 181 | onReleased: downTimer.stop() 182 | Timer { 183 | id: downTimer 184 | repeat: true 185 | onTriggered: { 186 | interval = 60 187 | mainSocket.sendVolumeDown() 188 | } 189 | } 190 | } 191 | 192 | ProgressCircleBase { 193 | progressColor: Theme.highlightColor 194 | backgroundColor: Theme.highlightDimmerColor 195 | 196 | width: Theme.itemSizeExtraLarge 197 | height: Theme.itemSizeExtraLarge 198 | value: mainSocket.volume / 100 199 | 200 | Column { 201 | anchors.centerIn: parent 202 | 203 | Label { 204 | anchors.horizontalCenter: parent.horizontalCenter 205 | text: mainSocket.volume 206 | } 207 | 208 | Image { 209 | anchors.horizontalCenter: parent.horizontalCenter 210 | source: "../../images/" + (mainSocket.muting ? "volume-muted" : "volume") + ".png" 211 | } 212 | } 213 | 214 | MouseArea { 215 | anchors.fill: parent 216 | onClicked: mainSocket.toggleMute() 217 | } 218 | } 219 | 220 | ControlButton { 221 | height: parent.height 222 | width: content.smallItemSize 223 | title: "+" 224 | titleSize: height / 2 225 | color: down ? Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) : "transparent" 226 | borderWidth: 0 227 | onPressed: { 228 | mainSocket.sendVolumeUp() 229 | upTimer.interval = 400 230 | upTimer.start() 231 | } 232 | onReleased: upTimer.stop() 233 | Timer { 234 | id: upTimer 235 | repeat: true 236 | onTriggered: { 237 | interval = 60 238 | mainSocket.sendVolumeUp() 239 | } 240 | } 241 | } 242 | 243 | ControlButton { 244 | height: parent.height 245 | width: content.smallItemSize 246 | title: "-" 247 | titleSize: height / 2 248 | color: down ? Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) : "transparent" 249 | borderWidth: 0 250 | onClicked: pointerSocket.sendInput("button", "CHANNELUP") 251 | } 252 | 253 | Item { 254 | height: parent.height 255 | width: height 256 | 257 | Label { 258 | anchors.centerIn: parent 259 | text: qsTr("CH: %1").arg(mainSocket.channelNumber) 260 | } 261 | } 262 | 263 | ControlButton { 264 | height: parent.height 265 | width: content.smallItemSize 266 | title: "+" 267 | titleSize: height / 2 268 | color: down ? Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) : "transparent" 269 | borderWidth: 0 270 | onClicked: pointerSocket.sendInput("button", "CHANNELDOWN") 271 | } 272 | } 273 | 274 | Row { 275 | anchors.horizontalCenter: parent.horizontalCenter 276 | spacing: Theme.paddingSmall 277 | 278 | IconButton { 279 | icon.source: "image://theme/icon-m-mouse" 280 | onClicked: touchpad.active = true 281 | } 282 | 283 | IconButton { 284 | icon.source: "image://theme/icon-m-keyboard" 285 | onClicked: toggleKeyboard(true) 286 | } 287 | 288 | IconButton { 289 | icon.source: "image://theme/icon-m-events" 290 | onClicked: channels.active = true 291 | } 292 | 293 | ImageButton { 294 | icon.source: "../../images/icon-m-arrows.png" 295 | onClicked: actions.active = true 296 | } 297 | 298 | IconButton { 299 | icon.source: "image://theme/icon-m-levels" 300 | onClicked: apps.active = true 301 | } 302 | 303 | IconButton { 304 | icon.source: "image://theme/icon-m-favorite" 305 | onClicked: extra.active = true 306 | } 307 | } 308 | } 309 | 310 | TouchpadPanel { 311 | id: touchpad 312 | socket: pointerSocket 313 | topMargin: content.height + content.anchors.topMargin 314 | } 315 | 316 | ChannelsPanel { 317 | id: channels 318 | socket: mainSocket 319 | channelsList: mainSocket.channelsTv 320 | currentChannelId: mainSocket.channelId 321 | topMargin: content.height + content.anchors.topMargin 322 | } 323 | 324 | ActionsPanel { 325 | id: actions 326 | socket: pointerSocket 327 | status3D: mainSocket.status3D 328 | } 329 | 330 | ApplicationsPanel { 331 | id: apps 332 | socket: mainSocket 333 | appList: mainSocket.appsList 334 | topMargin: content.height + content.anchors.topMargin 335 | } 336 | 337 | ExtraPanel { 338 | id: extra 339 | maxMargin: content.height + content.anchors.topMargin 340 | 341 | onToastMessage: { 342 | toast.active = true 343 | } 344 | 345 | onOpenBrowser: { 346 | browser.active = true 347 | } 348 | 349 | onOpenYoutube: { 350 | youtube.active = true 351 | } 352 | 353 | onTurnOff: { 354 | mainSocket.sendTurnOff() 355 | } 356 | 357 | onSwitchInput: { 358 | inputs.active = true 359 | } 360 | 361 | onTouchAcceleration: { 362 | acceleration.textField.text = appWindow.configuration.value 363 | acceleration.textField.text = acceleration.textField.text.replace(".", ",") 364 | acceleration.active = true 365 | } 366 | } 367 | 368 | TextPanel { 369 | id: toast 370 | maxMargin: content.height + content.anchors.topMargin 371 | textField.placeholderText: qsTr("Enter message...") 372 | 373 | onInputComplete: mainSocket.sendShowToast(text) 374 | } 375 | 376 | TextPanel { 377 | id: browser 378 | maxMargin: content.height + content.anchors.topMargin 379 | textField.placeholderText: qsTr("Enter url...") 380 | 381 | onInputComplete: { 382 | openBrowser(text) 383 | } 384 | } 385 | 386 | TextPanel { 387 | id: youtube 388 | maxMargin: content.height + content.anchors.topMargin 389 | textField.placeholderText: qsTr("Enter url or video id...") 390 | 391 | onInputComplete: { 392 | var videoId = text.indexOf("watch?v=") 393 | if (videoId == -1) { 394 | mainSocket.openYoutube(text) 395 | } 396 | else { 397 | var res = str.substring(videoId + 8) 398 | mainSocket.openYoutube(res) 399 | } 400 | } 401 | } 402 | 403 | TextPanel { 404 | id: pin 405 | maxMargin: content.height + content.anchors.topMargin 406 | textField.placeholderText: qsTr("Enter pin...") 407 | textField.inputMethodHints: Qt.ImhDigitsOnly 408 | textField.validator: RegExpValidator { regExp: /^[0-9]{3}$/; } 409 | onAcceptableInput: { 410 | if (pin.textField.acceptableInput) { 411 | mainSocket.sendPin(pin.textField.text) 412 | pin.active = false 413 | } 414 | } 415 | 416 | onInputComplete: mainSocket.sendPin(pin.textField.text) 417 | } 418 | 419 | InputPanel { 420 | id: inputs 421 | inputList: mainSocket.inputList 422 | maxMargin: content.height + content.anchors.topMargin 423 | socket: mainSocket 424 | } 425 | 426 | TextPanel { 427 | id: acceleration 428 | maxMargin: content.height + content.anchors.topMargin 429 | textField.placeholderText: qsTr("Touchpad acceleration: 1-4") 430 | textField.inputMethodHints: Qt.ImhDigitsOnly 431 | 432 | onInputComplete: { 433 | appWindow.configuration.value = parseFloat(text.replace(",", ".")) 434 | } 435 | } 436 | 437 | MouseArea { 438 | id: dimmer 439 | anchors.fill: parent 440 | enabled: invisibleInput.focus 441 | 442 | Rectangle { 443 | anchors.fill: parent 444 | color: "#80000000" 445 | visible: parent.enabled 446 | } 447 | 448 | onClicked: { 449 | toggleKeyboard(false) 450 | } 451 | } 452 | } 453 | 454 | 455 | 456 | 457 | 458 | --------------------------------------------------------------------------------