]> git.tdb.fi Git - xinema.git/commitdiff
Add a remote control program for Sailfish OS
authorMikko Rasa <tdb@tdb.fi>
Wed, 14 Oct 2015 23:56:51 +0000 (02:56 +0300)
committerMikko Rasa <tdb@tdb.fi>
Wed, 14 Oct 2015 23:56:51 +0000 (02:56 +0300)
19 files changed:
remote/.gitignore [new file with mode: 0644]
remote/harbour-xinema-remote.desktop [new file with mode: 0644]
remote/qml/main.qml [new file with mode: 0644]
remote/qml/pages/BrowsePage.qml [new file with mode: 0644]
remote/qml/pages/ConnectPage.qml [new file with mode: 0644]
remote/qml/pages/DirectoryEntry.qml [new file with mode: 0644]
remote/remote.pro [new file with mode: 0644]
remote/rpm/harbour-xinema-remote.spec [new file with mode: 0644]
remote/source/browsedirectoryitem.cpp [new file with mode: 0644]
remote/source/browsedirectoryitem.h [new file with mode: 0644]
remote/source/discovery.cpp [new file with mode: 0644]
remote/source/discovery.h [new file with mode: 0644]
remote/source/discoveryitem.cpp [new file with mode: 0644]
remote/source/discoveryitem.h [new file with mode: 0644]
remote/source/remote.cpp [new file with mode: 0644]
remote/source/xinemacontrol.cpp [new file with mode: 0644]
remote/source/xinemacontrol.h [new file with mode: 0644]
remote/source/xinemacontrolitem.cpp [new file with mode: 0644]
remote/source/xinemacontrolitem.h [new file with mode: 0644]

diff --git a/remote/.gitignore b/remote/.gitignore
new file mode 100644 (file)
index 0000000..6e489d0
--- /dev/null
@@ -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 (file)
index 0000000..3b4c0dd
--- /dev/null
@@ -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 (file)
index 0000000..23bb418
--- /dev/null
@@ -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 (file)
index 0000000..59ebff3
--- /dev/null
@@ -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 (file)
index 0000000..4117cc7
--- /dev/null
@@ -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 (file)
index 0000000..f8e0862
--- /dev/null
@@ -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 (file)
index 0000000..ee41215
--- /dev/null
@@ -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 (file)
index 0000000..4dabbb4
--- /dev/null
@@ -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 (file)
index 0000000..99328bb
--- /dev/null
@@ -0,0 +1,60 @@
+#include "browsedirectoryitem.h"
+#include "xinemacontrol.h"
+#include "xinemacontrolitem.h"
+
+#include <QDebug>
+
+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 (file)
index 0000000..84f8a6c
--- /dev/null
@@ -0,0 +1,47 @@
+#ifndef BROWSEDIRECTORYITEM_H_
+#define BROWSEDIRECTORYITEM_H_
+
+#include <QQuickItem>
+
+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 (file)
index 0000000..b4f0662
--- /dev/null
@@ -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 (file)
index 0000000..8f5469c
--- /dev/null
@@ -0,0 +1,33 @@
+#ifndef DISCOVERY_H_
+#define DISCOVERY_H_
+
+#include <QObject>
+#include <QTimer>
+#include <QUdpSocket>
+
+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 (file)
index 0000000..894991e
--- /dev/null
@@ -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 (file)
index 0000000..a200b82
--- /dev/null
@@ -0,0 +1,34 @@
+#ifndef DISCOVERYITEM_H_
+#define DISCOVERYITEM_H_
+
+#include <QQuickItem>
+#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 (file)
index 0000000..e405626
--- /dev/null
@@ -0,0 +1,18 @@
+#include <QGuiApplication>
+#include <QQuickView>
+#include <sailfishapp.h>
+#include "browsedirectoryitem.h"
+#include "discoveryitem.h"
+#include "xinemacontrolitem.h"
+
+int main(int argc, char **argv)
+{
+       QGuiApplication *app = SailfishApp::application(argc, argv);
+       qmlRegisterType<BrowseDirectoryItem>("fi.mikkosoft.xinema", 0, 1, "BrowseDirectory");
+       qmlRegisterType<DiscoveryItem>("fi.mikkosoft.xinema", 0, 1, "Discovery");
+       qmlRegisterType<XinemaControlItem>("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 (file)
index 0000000..76108b9
--- /dev/null
@@ -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 (file)
index 0000000..5dd5e27
--- /dev/null
@@ -0,0 +1,39 @@
+#ifndef XINEMACONTROL_H_
+#define XINEMACONTROL_H_
+
+#include <QObject>
+#include <QTcpSocket>
+
+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 (file)
index 0000000..6d6cf55
--- /dev/null
@@ -0,0 +1,23 @@
+#include <QHostAddress>
+#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 (file)
index 0000000..c850173
--- /dev/null
@@ -0,0 +1,29 @@
+#ifndef XINEMACONTROLITEM_H_
+#define XINEMACONTROLITEM_H_
+
+#include <QQuickItem>
+#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