From 1abfbdd94fa45883f6d742df00508715f79c9954 Mon Sep 17 00:00:00 2001 From: Mikko Rasa Date: Thu, 15 Oct 2015 02:56:51 +0300 Subject: [PATCH] Add a remote control program for Sailfish OS --- remote/.gitignore | 5 ++ remote/harbour-xinema-remote.desktop | 4 + remote/qml/main.qml | 16 ++++ remote/qml/pages/BrowsePage.qml | 106 ++++++++++++++++++++++++++ remote/qml/pages/ConnectPage.qml | 53 +++++++++++++ remote/qml/pages/DirectoryEntry.qml | 45 +++++++++++ remote/remote.pro | 26 +++++++ remote/rpm/harbour-xinema-remote.spec | 24 ++++++ remote/source/browsedirectoryitem.cpp | 60 +++++++++++++++ remote/source/browsedirectoryitem.h | 47 ++++++++++++ remote/source/discovery.cpp | 37 +++++++++ remote/source/discovery.h | 33 ++++++++ remote/source/discoveryitem.cpp | 30 ++++++++ remote/source/discoveryitem.h | 34 +++++++++ remote/source/remote.cpp | 18 +++++ remote/source/xinemacontrol.cpp | 80 +++++++++++++++++++ remote/source/xinemacontrol.h | 39 ++++++++++ remote/source/xinemacontrolitem.cpp | 23 ++++++ remote/source/xinemacontrolitem.h | 29 +++++++ 19 files changed, 709 insertions(+) create mode 100644 remote/.gitignore create mode 100644 remote/harbour-xinema-remote.desktop create mode 100644 remote/qml/main.qml create mode 100644 remote/qml/pages/BrowsePage.qml create mode 100644 remote/qml/pages/ConnectPage.qml create mode 100644 remote/qml/pages/DirectoryEntry.qml create mode 100644 remote/remote.pro create mode 100644 remote/rpm/harbour-xinema-remote.spec create mode 100644 remote/source/browsedirectoryitem.cpp create mode 100644 remote/source/browsedirectoryitem.h create mode 100644 remote/source/discovery.cpp create mode 100644 remote/source/discovery.h create mode 100644 remote/source/discoveryitem.cpp create mode 100644 remote/source/discoveryitem.h create mode 100644 remote/source/remote.cpp create mode 100644 remote/source/xinemacontrol.cpp create mode 100644 remote/source/xinemacontrol.h create mode 100644 remote/source/xinemacontrolitem.cpp create mode 100644 remote/source/xinemacontrolitem.h diff --git a/remote/.gitignore b/remote/.gitignore new file mode 100644 index 0000000..6e489d0 --- /dev/null +++ b/remote/.gitignore @@ -0,0 +1,5 @@ +/Makefile +/RPMS +/documentation.list +/harbour-xinema-remote +/temp diff --git a/remote/harbour-xinema-remote.desktop b/remote/harbour-xinema-remote.desktop new file mode 100644 index 0000000..3b4c0dd --- /dev/null +++ b/remote/harbour-xinema-remote.desktop @@ -0,0 +1,4 @@ +[Desktop Entry] +Type=Application +Name=Xinema remote +Exec=harbour-xinema-remote diff --git a/remote/qml/main.qml b/remote/qml/main.qml new file mode 100644 index 0000000..23bb418 --- /dev/null +++ b/remote/qml/main.qml @@ -0,0 +1,16 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import fi.mikkosoft.xinema 0.1 +import "pages" + +ApplicationWindow +{ + initialPage: Component { ConnectPage { } } + allowedOrientations: Orientation.Portrait + + XinemaControl + { + id: xinemaControl; + } +} + diff --git a/remote/qml/pages/BrowsePage.qml b/remote/qml/pages/BrowsePage.qml new file mode 100644 index 0000000..59ebff3 --- /dev/null +++ b/remote/qml/pages/BrowsePage.qml @@ -0,0 +1,106 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import fi.mikkosoft.xinema 0.1 + +Page +{ + id: page + + SilicaFlickable + { + anchors.fill: parent + contentHeight: column.height + + Column + { + id: column + + width: parent.width + spacing: Theme.paddingLarge + + PageHeader + { + title: qsTr("Browse files") + } + + // This really should be SilicaListView, but I can't figure out an + // easy way to turn the string lists into a suitable ListModel + Column + { + width: parent.width + spacing: Theme.paddingSmall + + DirectoryEntry + { + visible: browseDirectory.directory!="/" + icon: "image://theme/icon-m-back" + text: ".." + onPressed: + { + var newDir = browseDirectory.directory; + var slash = newDir.lastIndexOf("/"); + if(slash>0) + newDir = newDir.substring(0, slash); + else + newDir = "/"; + browseDirectory.directory = newDir; + } + } + + Repeater + { + model: browseDirectory.subdirectories + + DirectoryEntry + { + icon: "image://theme/icon-m-folder" + text: modelData + + onPressed: + { + var newDir = browseDirectory.directory; + if(newDir!="/") + newDir += "/"; + newDir += modelData; + browseDirectory.directory = newDir; + } + } + } + + Repeater + { + model: browseDirectory.files + DirectoryEntry + { + text: modelData + + onPressed: xinemaControl.play_file(browseDirectory.directory+"/"+modelData); + } + } + } + } + } + + BrowseDirectory + { + id: browseDirectory + control: xinemaControl + } + + Connections + { + target: xinemaControl + onConnectedChanged: + { + if(xinemaControl.connected) + browseDirectory.directory = "/"; + } + } + + Component.onCompleted: + { + if(xinemaControl.connected) + browseDirectory.directory = "/"; + } +} + diff --git a/remote/qml/pages/ConnectPage.qml b/remote/qml/pages/ConnectPage.qml new file mode 100644 index 0000000..4117cc7 --- /dev/null +++ b/remote/qml/pages/ConnectPage.qml @@ -0,0 +1,53 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import fi.mikkosoft.xinema 0.1 + +Page +{ + id: page + + Item + { + width: page.width + height: page.height + + PageHeader + { + title: qsTr("Connecting...") + } + + Column + { + width: parent.width + spacing: Theme.paddingSmall + anchors.verticalCenter: parent.verticalCenter + + Label + { + width: parent.width + text: "Searching for Xinema server" + horizontalAlignment: Text.AlignHCenter + } + Label + { + width: parent.width + text: "Please wait" + horizontalAlignment: Text.AlignHCenter + } + } + + Discovery + { + id: discovery + active: true + + onServerNameChanged: + { + discovery.active = false; + xinemaControl.connect(discovery.serverName); + pageStack.replace("BrowsePage.qml"); + } + } + } +} + diff --git a/remote/qml/pages/DirectoryEntry.qml b/remote/qml/pages/DirectoryEntry.qml new file mode 100644 index 0000000..f8e0862 --- /dev/null +++ b/remote/qml/pages/DirectoryEntry.qml @@ -0,0 +1,45 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item +{ + id: entry + + property alias icon: icon.source + property alias text: label.text + property int iconSize: Theme.iconSizeMedium + height: row.height + width: row.width + + signal pressed() + + Row + { + id: row + spacing: Theme.paddingSmall + + Image + { + id: icon + sourceSize.width: iconSize + sourceSize.height: iconSize + width: iconSize + height: iconSize + } + Label + { + id: label + height: icon.height + verticalAlignment: Text.AlignVCenter + } + } + + MouseArea + { + anchors.fill: row + onPressed: + { + entry.pressed(); + } + } +} diff --git a/remote/remote.pro b/remote/remote.pro new file mode 100644 index 0000000..ee41215 --- /dev/null +++ b/remote/remote.pro @@ -0,0 +1,26 @@ +TEMPLATE = app +TARGET = harbour-xinema-remote + +CONFIG += sailfishapp + +SOURCES += source/browsedirectoryitem.cpp \ + source/discovery.cpp \ + source/discoveryitem.cpp \ + source/remote.cpp \ + source/xinemacontrol.cpp \ + source/xinemacontrolitem.cpp + +HEADERS += source/browsedirectoryitem.h \ + source/discovery.h \ + source/discoveryitem.h \ + source/xinemacontrol.h \ + source/xinemacontrolitem.h + +OTHER_FILES += qml/main.qml \ + qml/pages/BrowsePage.qml \ + qml/pages/ConnectPage.qml \ + qml/pages/DirectoryEntry.qml \ + rpm/harbour-xinema-remote.spec + +OBJECTS_DIR = temp +MOC_DIR = temp diff --git a/remote/rpm/harbour-xinema-remote.spec b/remote/rpm/harbour-xinema-remote.spec new file mode 100644 index 0000000..4dabbb4 --- /dev/null +++ b/remote/rpm/harbour-xinema-remote.spec @@ -0,0 +1,24 @@ +Name: harbour-xinema-remote +Summary: Remote control program for Xinema media center +Version: 0.1 +Release: 1 +Group: Qt/Qt +License: GPL + +%description +%{summary}. + +%prep + +%build +make + +%install +rm -rf %{buildroot} +%make_install + +%files +%defattr(-,root,root,-) +/usr/bin/%{name} +/usr/share/%{name} +/usr/share/applications/%{name}.desktop diff --git a/remote/source/browsedirectoryitem.cpp b/remote/source/browsedirectoryitem.cpp new file mode 100644 index 0000000..99328bb --- /dev/null +++ b/remote/source/browsedirectoryitem.cpp @@ -0,0 +1,60 @@ +#include "browsedirectoryitem.h" +#include "xinemacontrol.h" +#include "xinemacontrolitem.h" + +#include + +BrowseDirectoryItem::BrowseDirectoryItem(): + control(0) +{ } + +void BrowseDirectoryItem::set_control(XinemaControlItem *c) +{ + if(control) + disconnect(&control->get_control(), 0, this, 0); + + control = c; + XinemaControl &xc = control->get_control(); + connect(&xc, &XinemaControl::directory_started, this, &BrowseDirectoryItem::directory_started); + connect(&xc, &XinemaControl::file_added, this, &BrowseDirectoryItem::file_added); + connect(&xc, &XinemaControl::subdirectory_added, this, &BrowseDirectoryItem::subdirectory_added); + + emit control_changed(); +} + +void BrowseDirectoryItem::set_directory(const QString &d) +{ + if(d==directory) + return; + + directory = d; + if(control) + control->get_control().list_directory(directory); + emit directory_changed(); +} + +void BrowseDirectoryItem::directory_started(const QString &dir) +{ + if(dir!=directory) + { + directory = dir; + emit directory_changed(); + } + + subdirectories.clear(); + files.clear(); + emit subdirectories_changed(); + emit files_changed(); +} + +void BrowseDirectoryItem::file_added(const QString &name) +{ + files.push_back(name); + emit files_changed(); +} + +void BrowseDirectoryItem::subdirectory_added(const QString &name) +{ + subdirectories.push_back(name); + emit subdirectories_changed(); +} diff --git a/remote/source/browsedirectoryitem.h b/remote/source/browsedirectoryitem.h new file mode 100644 index 0000000..84f8a6c --- /dev/null +++ b/remote/source/browsedirectoryitem.h @@ -0,0 +1,47 @@ +#ifndef BROWSEDIRECTORYITEM_H_ +#define BROWSEDIRECTORYITEM_H_ + +#include + +class XinemaControlItem; + +class BrowseDirectoryItem: public QQuickItem +{ + Q_OBJECT + + Q_PROPERTY(XinemaControlItem *control READ get_control WRITE set_control NOTIFY control_changed) + Q_PROPERTY(QString directory READ get_directory WRITE set_directory NOTIFY directory_changed) + Q_PROPERTY(QStringList subdirectories READ get_subdirectories NOTIFY subdirectories_changed) + Q_PROPERTY(QStringList files READ get_files NOTIFY files_changed) + +private: + XinemaControlItem *control; + QString directory; + QStringList subdirectories; + QStringList files; + +public: + BrowseDirectoryItem(); + + void set_control(XinemaControlItem *); + XinemaControlItem *get_control() const { return control; } + + void set_directory(const QString &); + const QString &get_directory() const { return directory; } + + const QStringList &get_subdirectories() const { return subdirectories; } + const QStringList &get_files() const { return files; } + +signals: + void control_changed(); + void directory_changed(); + void subdirectories_changed(); + void files_changed(); + +private slots: + void directory_started(const QString &); + void file_added(const QString &); + void subdirectory_added(const QString &); +}; + +#endif diff --git a/remote/source/discovery.cpp b/remote/source/discovery.cpp new file mode 100644 index 0000000..b4f0662 --- /dev/null +++ b/remote/source/discovery.cpp @@ -0,0 +1,37 @@ +#include "discovery.h" + +Discovery::Discovery(): + broadcast_addr("ff02::1"), + port(34588) +{ + socket.bind(QHostAddress::AnyIPv6); + connect(&socket, &QIODevice::readyRead, this, &Discovery::datagram_available); + + connect(&timer, &QTimer::timeout, this, &Discovery::send_beacon); + timer.setInterval(5000); +} + +void Discovery::start() +{ + timer.start(); + send_beacon(); +} + +void Discovery::stop() +{ + timer.stop(); +} + +void Discovery::send_beacon() +{ + socket.writeDatagram("xinema", 6, broadcast_addr, port); +} + +void Discovery::datagram_available() +{ + char rbuf[1024]; + QHostAddress peer_addr; + socket.readDatagram(rbuf, sizeof(rbuf), &peer_addr, 0); + + emit server_discovered(peer_addr); +} diff --git a/remote/source/discovery.h b/remote/source/discovery.h new file mode 100644 index 0000000..8f5469c --- /dev/null +++ b/remote/source/discovery.h @@ -0,0 +1,33 @@ +#ifndef DISCOVERY_H_ +#define DISCOVERY_H_ + +#include +#include +#include + +class Discovery: public QObject +{ + Q_OBJECT + +private: + QUdpSocket socket; + QHostAddress broadcast_addr; + unsigned port; + QTimer timer; + QString server_name; + +public: + Discovery(); + + void start(); + void stop(); + +signals: + void server_discovered(const QHostAddress &); + +private slots: + void send_beacon(); + void datagram_available(); +}; + +#endif diff --git a/remote/source/discoveryitem.cpp b/remote/source/discoveryitem.cpp new file mode 100644 index 0000000..894991e --- /dev/null +++ b/remote/source/discoveryitem.cpp @@ -0,0 +1,30 @@ +#include "discoveryitem.h" + +DiscoveryItem::DiscoveryItem(): + active(false) +{ + connect(&discovery, &Discovery::server_discovered, this, &DiscoveryItem::server_discovered); +} + +void DiscoveryItem::set_active(bool a) +{ + if(a==active) + return; + + active = a; + if(active) + discovery.start(); + else + discovery.stop(); + + emit active_changed(); +} + +void DiscoveryItem::server_discovered(const QHostAddress &addr) +{ + if(addr==server_addr) + return; + + server_addr = addr; + emit server_name_changed(); +} diff --git a/remote/source/discoveryitem.h b/remote/source/discoveryitem.h new file mode 100644 index 0000000..a200b82 --- /dev/null +++ b/remote/source/discoveryitem.h @@ -0,0 +1,34 @@ +#ifndef DISCOVERYITEM_H_ +#define DISCOVERYITEM_H_ + +#include +#include "discovery.h" + +class DiscoveryItem: public QQuickItem +{ + Q_OBJECT + + Q_PROPERTY(bool active READ is_active WRITE set_active NOTIFY active_changed) + Q_PROPERTY(QString serverName READ get_server_name NOTIFY server_name_changed) + +private: + Discovery discovery; + bool active; + QHostAddress server_addr; + +public: + DiscoveryItem(); + + void set_active(bool); + bool is_active() const { return active; } + QString get_server_name() const { return server_addr.toString(); } + +signals: + void server_name_changed(); + void active_changed(); + +private slots: + void server_discovered(const QHostAddress &); +}; + +#endif diff --git a/remote/source/remote.cpp b/remote/source/remote.cpp new file mode 100644 index 0000000..e405626 --- /dev/null +++ b/remote/source/remote.cpp @@ -0,0 +1,18 @@ +#include +#include +#include +#include "browsedirectoryitem.h" +#include "discoveryitem.h" +#include "xinemacontrolitem.h" + +int main(int argc, char **argv) +{ + QGuiApplication *app = SailfishApp::application(argc, argv); + qmlRegisterType("fi.mikkosoft.xinema", 0, 1, "BrowseDirectory"); + qmlRegisterType("fi.mikkosoft.xinema", 0, 1, "Discovery"); + qmlRegisterType("fi.mikkosoft.xinema", 0, 1, "XinemaControl"); + QQuickView *view = SailfishApp::createView(); + view->setSource(SailfishApp::pathTo("qml/main.qml")); + view->show(); + return app->exec(); +} diff --git a/remote/source/xinemacontrol.cpp b/remote/source/xinemacontrol.cpp new file mode 100644 index 0000000..76108b9 --- /dev/null +++ b/remote/source/xinemacontrol.cpp @@ -0,0 +1,80 @@ +#include "xinemacontrol.h" + +XinemaControl::XinemaControl() +{ + QObject::connect(&socket, &QAbstractSocket::connected, this, &XinemaControl::connected); + QObject::connect(&socket, &QAbstractSocket::disconnected, this, &XinemaControl::disconnected); + QObject::connect(&socket, &QIODevice::readyRead, this, &XinemaControl::data_available); +} + +void XinemaControl::connect(const QHostAddress &addr) +{ + socket.connectToHost(addr, 34588); +} + +bool XinemaControl::is_connected() const +{ + return socket.state()==QAbstractSocket::ConnectedState; +} + +void XinemaControl::list_directory(const QString &dir) +{ + if(!is_connected()) + return; + + send_request("list_directory "+dir); +} + +void XinemaControl::play_file(const QString &fn) +{ + if(!is_connected()) + return; + + send_request("play_file "+fn); +} + +void XinemaControl::send_request(const QString &req) +{ + socket.write(req.toUtf8()); + socket.write("\n", 1); +} + +void XinemaControl::data_available() +{ + char rbuf[1024]; + int len = socket.read(rbuf, sizeof(rbuf)); + if(len<0) + return; + + buffer.append(rbuf, len); + unsigned start = 0; + while(1) + { + int newline = buffer.indexOf('\n', start); + if(newline<0) + break; + + QString reply = QString::fromUtf8(buffer.mid(start, newline-start)); + process_reply(reply); + + start = newline+1; + } + + buffer.remove(0, start); +} + +void XinemaControl::process_reply(const QString &reply) +{ + int space = reply.indexOf(' '); + QString keyword = reply.mid(0, space); + QString args; + if(space>=0) + args = reply.mid(space+1); + + if(keyword=="directory") + emit directory_started(args); + else if(keyword=="subdir") + emit subdirectory_added(args); + else if(keyword=="file") + emit file_added(args); +} diff --git a/remote/source/xinemacontrol.h b/remote/source/xinemacontrol.h new file mode 100644 index 0000000..5dd5e27 --- /dev/null +++ b/remote/source/xinemacontrol.h @@ -0,0 +1,39 @@ +#ifndef XINEMACONTROL_H_ +#define XINEMACONTROL_H_ + +#include +#include + +class XinemaControl: public QObject +{ + Q_OBJECT + +private: + QTcpSocket socket; + QByteArray buffer; + +public: + XinemaControl(); + + void connect(const QHostAddress &); + bool is_connected() const; + + void list_directory(const QString &); + void play_file(const QString &); + +signals: + void connected(); + void disconnected(); + void directory_started(const QString &); + void file_added(const QString &); + void subdirectory_added(const QString &); + +private: + void send_request(const QString &); + +private slots: + void data_available(); + void process_reply(const QString &); +}; + +#endif diff --git a/remote/source/xinemacontrolitem.cpp b/remote/source/xinemacontrolitem.cpp new file mode 100644 index 0000000..6d6cf55 --- /dev/null +++ b/remote/source/xinemacontrolitem.cpp @@ -0,0 +1,23 @@ +#include +#include "xinemacontrolitem.h" + +XinemaControlItem::XinemaControlItem() +{ + QObject::connect(&control, &XinemaControl::connected, this, &XinemaControlItem::connect_state_changed); + QObject::connect(&control, &XinemaControl::disconnected, this, &XinemaControlItem::connect_state_changed); +} + +void XinemaControlItem::connect(const QString &host) +{ + control.connect(QHostAddress(host)); +} + +bool XinemaControlItem::is_connected() const +{ + return control.is_connected(); +} + +void XinemaControlItem::play_file(const QString &fn) +{ + control.play_file(fn); +} diff --git a/remote/source/xinemacontrolitem.h b/remote/source/xinemacontrolitem.h new file mode 100644 index 0000000..c850173 --- /dev/null +++ b/remote/source/xinemacontrolitem.h @@ -0,0 +1,29 @@ +#ifndef XINEMACONTROLITEM_H_ +#define XINEMACONTROLITEM_H_ + +#include +#include "xinemacontrol.h" + +class XinemaControlItem: public QQuickItem +{ + Q_OBJECT + + Q_PROPERTY(bool connected READ is_connected NOTIFY connect_state_changed) + +private: + XinemaControl control; + +public: + XinemaControlItem(); + + XinemaControl &get_control() { return control; } + Q_INVOKABLE void connect(const QString &); + bool is_connected() const; + + Q_INVOKABLE void play_file(const QString &); + +signals: + void connect_state_changed(); +}; + +#endif -- 2.43.0