--- /dev/null
+/Makefile
+/RPMS
+/documentation.list
+/harbour-xinema-remote
+/temp
--- /dev/null
+[Desktop Entry]
+Type=Application
+Name=Xinema remote
+Exec=harbour-xinema-remote
--- /dev/null
+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;
+ }
+}
+
--- /dev/null
+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 = "/";
+ }
+}
+
--- /dev/null
+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");
+ }
+ }
+ }
+}
+
--- /dev/null
+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();
+ }
+ }
+}
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+#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();
+}
--- /dev/null
+#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
--- /dev/null
+#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);
+}
--- /dev/null
+#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
--- /dev/null
+#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();
+}
--- /dev/null
+#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
--- /dev/null
+#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();
+}
--- /dev/null
+#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);
+}
--- /dev/null
+#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
--- /dev/null
+#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);
+}
--- /dev/null
+#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