summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJavier S. Pedro <maemo@javispedro.com>2013-04-01 15:04:58 +0200
committerJavier S. Pedro <maemo@javispedro.com>2013-04-01 15:04:58 +0200
commit5ef8b38e55c1883224fe1f01f47aba45b7b42666 (patch)
tree67a873c6a7c5263d202793314c3b3a61543fbb40
downloadtapasboard-5ef8b38e55c1883224fe1f01f47aba45b7b42666.tar.gz
tapasboard-5ef8b38e55c1883224fe1f01f47aba45b7b42666.zip
initial import
-rw-r--r--action.cpp18
-rw-r--r--action.h32
-rw-r--r--board.cpp220
-rw-r--r--board.h75
-rw-r--r--boardmanager.cpp18
-rw-r--r--boardmanager.h27
-rw-r--r--boardmodel.cpp141
-rw-r--r--boardmodel.h55
-rw-r--r--fetchboardconfigaction.cpp50
-rw-r--r--fetchboardconfigaction.h23
-rw-r--r--fetchforumsaction.cpp107
-rw-r--r--fetchforumsaction.h29
-rw-r--r--fetchtopicsaction.cpp88
-rw-r--r--fetchtopicsaction.h32
-rw-r--r--forummodel.cpp281
-rw-r--r--forummodel.h72
-rw-r--r--global.h23
-rw-r--r--main.cpp37
-rw-r--r--qml/tapasboard/BoardPage.qml57
-rw-r--r--qml/tapasboard/ForumPage.qml43
-rw-r--r--qml/tapasboard/GroupHeader.qml26
-rw-r--r--qml/tapasboard/MainPage.qml17
-rw-r--r--qml/tapasboard/main.qml30
-rw-r--r--qmlapplicationviewer/qmlapplicationviewer.cpp174
-rw-r--r--qmlapplicationviewer/qmlapplicationviewer.h46
-rw-r--r--qmlapplicationviewer/qmlapplicationviewer.pri148
-rw-r--r--qtc_packaging/debian_harmattan/README6
-rw-r--r--qtc_packaging/debian_harmattan/changelog5
-rw-r--r--qtc_packaging/debian_harmattan/compat1
-rw-r--r--qtc_packaging/debian_harmattan/control15
-rw-r--r--qtc_packaging/debian_harmattan/copyright40
-rw-r--r--qtc_packaging/debian_harmattan/manifest.aegis70
-rwxr-xr-xqtc_packaging/debian_harmattan/rules91
-rw-r--r--tapasboard.desktop11
-rw-r--r--tapasboard.pro72
-rw-r--r--tapasboard.svg93
-rw-r--r--tapasboard64.pngbin0 -> 3400 bytes
-rw-r--r--tapasboard80.pngbin0 -> 4945 bytes
-rw-r--r--tapasboard_harmattan.desktop11
-rw-r--r--xmlrpcinterface.cpp99
-rw-r--r--xmlrpcinterface.h70
-rw-r--r--xmlrpcpendingcall.cpp187
-rw-r--r--xmlrpcpendingcall.h69
-rw-r--r--xmlrpcreply.h47
44 files changed, 2756 insertions, 0 deletions
diff --git a/action.cpp b/action.cpp
new file mode 100644
index 0000000..df52dcc
--- /dev/null
+++ b/action.cpp
@@ -0,0 +1,18 @@
+#include "board.h"
+#include "action.h"
+
+Action::Action(Board *board) :
+ QObject(board), _board(board)
+{
+}
+
+void Action::handleDatabaseError(const char *what, const QSqlError &err)
+{
+ QString message = QString("Database error while %1: %2").arg(what).arg(err.text());
+ emit error(this, message);
+}
+
+void Action::handleDatabaseError(const char *what, const QSqlQuery &query)
+{
+ handleDatabaseError(what, query.lastError());
+}
diff --git a/action.h b/action.h
new file mode 100644
index 0000000..421adea
--- /dev/null
+++ b/action.h
@@ -0,0 +1,32 @@
+#ifndef ACTION_H
+#define ACTION_H
+
+#include <QObject>
+#include <QtSql/QSqlError>
+#include <QtSql/QSqlQuery>
+
+class Board;
+
+class Action : public QObject
+{
+ Q_OBJECT
+public:
+ explicit Action(Board *board);
+
+signals:
+ void finished(Action *self);
+ void error(Action *self, const QString& messsage);
+
+public slots:
+ virtual void execute() = 0;
+
+protected:
+ void handleDatabaseError(const char *what, const QSqlError& err);
+ void handleDatabaseError(const char *what, const QSqlQuery& query);
+
+protected:
+ Board *_board;
+
+};
+
+#endif // ACTION_H
diff --git a/board.cpp b/board.cpp
new file mode 100644
index 0000000..887b3c6
--- /dev/null
+++ b/board.cpp
@@ -0,0 +1,220 @@
+#include <QtCore/QRegExp>
+#include <QtCore/QDateTime>
+#include <QtCore/QDir>
+#include <QtCore/QDebug>
+#include <QtSql/QSqlQuery>
+#include <QtSql/QSqlError>
+
+#include "global.h"
+#include "action.h"
+#include "fetchboardconfigaction.h"
+#include "fetchforumsaction.h"
+#include "xmlrpcinterface.h"
+#include "board.h"
+
+Board::Board(const QString& forumUrl, QObject *parent) :
+ QObject(parent), _url(forumUrl), _slug(createSlug(forumUrl)),
+ _db(QSqlDatabase::addDatabase("QSQLITE", _slug)),
+ _iface(new XmlRpcInterface(QUrl(_url), this))
+{
+ _db.setDatabaseName(QDir::toNativeSeparators(getDbPathFor(_slug)));
+ qDebug() << "Opening database file" << _db.databaseName() << "for" << _url;
+ if (!_db.open()) {
+ qWarning() << "Could not open database file" << _db.databaseName() << ":"
+ << _db.lastError().text();
+ }
+ initializeDb();
+ fetchConfigIfOutdated();
+ fetchForumsIfOutdated();
+}
+
+void Board::enqueueAction(Action *action)
+{
+ connect(action, SIGNAL(finished(Action*)), SLOT(handleActionFinished(Action*)));
+ connect(action, SIGNAL(error(Action*,QString)), SLOT(handleActionError(Action*,QString)));
+
+ _queue.enqueue(action);
+
+ if (_queue.size() == 1) {
+ // There were no actions queued, so start by executing this one.
+ executeActionFromQueue();
+ }
+}
+
+QString Board::getConfig(const QString &key) const
+{
+ QSqlQuery query(_db);
+ query.prepare("SELECT key, value FROM config WHERE key = :key");
+ query.bindValue(":key", key);
+ if (!query.exec()) {
+ qWarning() << "Could not get configuration key:" << key;
+ return QString();
+ }
+ if (query.next()) {
+ return query.value(1).toString();
+ }
+ return QString();
+}
+
+void Board::setConfig(const QString &key, const QString &value)
+{
+ QSqlQuery query(_db);
+ query.prepare("INSERT OR REPLACE INTO config (key, value) VALUES (:key, :value)");
+ query.bindValue(":key", key);
+ query.bindValue(":value", value);
+ if (!query.exec()) {
+ qWarning() << "Could not set configuration key" << key << ":" << query.lastError().text();
+ }
+ notifyConfigChanged();
+}
+
+int Board::rootForumId() const
+{
+ QSqlQuery query(_db);
+ query.exec("SELECT forum_id FROM forums WHERE parent_id = -1");
+ if (query.next()) {
+ return query.value(0).toInt();
+ } else {
+ return -1;
+ }
+}
+
+void Board::notifyConfigChanged()
+{
+ emit configChanged();
+}
+
+void Board::notifyForumsChanged()
+{
+ emit forumsChanged();
+}
+
+void Board::notifyForumTopicsChanged(int forumId, int start, int end)
+{
+ qDebug() << "ForumTopics Changed" << forumId << start << end;
+ emit forumTopicsChanged(forumId, start, end);
+}
+
+QString Board::createSlug(const QString &forumUrl)
+{
+ static const QRegExp regexp("[^a-z0-9]+");
+ QString url = forumUrl.toLower();
+ url.replace(regexp, "_");
+ return url;
+}
+
+QString Board::getDbDir()
+{
+ QString path;
+
+#ifdef Q_OS_LINUX
+ char * xdg_cache_dir = getenv("XDG_CACHE_HOME");
+ if (xdg_cache_dir) {
+ path = QString::fromLocal8Bit(xdg_cache_dir) + "/tapasboard";
+ } else {
+ path = QDir::homePath() + "/.cache/tapasboard";
+ }
+ if (!QDir().mkpath(path)) {
+ qWarning() << "Failed to create directory for databases:" << path;
+ }
+#endif
+
+ return path;
+}
+
+QString Board::getDbPathFor(const QString &slug)
+{
+ return getDbDir() + "/" + slug + ".sqlite";
+}
+
+bool Board::initializeDb()
+{
+ QSqlQuery q(_db);
+ if (!q.exec("CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT)")) {
+ qWarning() << "Could not create config table:" << q.lastError().text();
+ return false;
+ }
+
+ if (!q.exec("CREATE TABLE IF NOT EXISTS forums (forum_id INTEGER PRIMARY KEY, forum_name TEXT, description TEXT, parent_id INT, logo_url TEXT, new_post BOOL, is_protected BOOL, is_subscribed BOOL, can_subscribe BOOL, url TEXT, sub_only BOOL, sort_index INT UNIQUE)")) {
+ qWarning() << "Could not create forums table:" << q.lastError().text();
+ return false;
+ }
+ if (!q.exec("CREATE INDEX IF NOT EXISTS forums_parent ON forums (parent_id)")) {
+ qWarning() << "Could not create forums table:" << q.lastError().text();
+ return false;
+ }
+
+ if (!q.exec("CREATE TABLE IF NOT EXISTS topics (forum_id INTEGER, topic_id INTEGER PRIMARY KEY, topic_title TEXT, topic_author_id INTEGER, topic_author_name TEXT, is_subscribed BOOL, is_closed BOOL, icon_url TEXT, last_reply_time TEXT, new_post BOOL, last_update_time TEXT)")) {
+ qWarning() << "Could not create topics table:" << q.lastError().text();
+ return false;
+ }
+ if (!q.exec("CREATE INDEX IF NOT EXISTS topics_forum ON topics (forum_id)")) {
+ qWarning() << "Could not create topics_forum index:" << q.lastError().text();
+ return false;
+ }
+ if (!q.exec("CREATE INDEX IF NOT EXISTS topics_time ON topics (last_reply_time)")) {
+ qWarning() << "Could not create topics_time index:" << q.lastError().text();
+ return false;
+ }
+
+ return true;
+}
+
+bool Board::removeFromActionQueue(Action *action)
+{
+ if (_queue.isEmpty()) return false;
+ Action *head = _queue.head();
+ if (_queue.removeOne(action)) {
+ if (!_queue.isEmpty() && head != _queue.head()) {
+ // The head action was removed; advance the queue.
+ executeActionFromQueue();
+ }
+ action->deleteLater();
+ return true;
+ }
+ return false;
+}
+
+void Board::executeActionFromQueue()
+{
+ if (!_queue.empty()) {
+ Action *head = _queue.head();
+ head->execute();
+ }
+}
+
+void Board::fetchConfigIfOutdated()
+{
+ if (_iface->isAccessible()) {
+ // Only fetch if network is accessible and data is >48h old.
+ QDateTime last_fetch = QDateTime::fromString(
+ getConfig("last_config_fetch"), Qt::ISODate);
+ if (!last_fetch.isValid() || last_fetch.daysTo(QDateTime::currentDateTimeUtc()) >= BOARD_CONFIG_TTL) {
+ enqueueAction(new FetchBoardConfigAction(this));
+ }
+ }
+
+}
+
+void Board::fetchForumsIfOutdated()
+{
+ if (_iface->isAccessible()) {
+ // Only fetch if network is accessible and data is >48h old.
+ QDateTime last_fetch = QDateTime::fromString(
+ getConfig("last_forums_fetch"), Qt::ISODate);
+ if (!last_fetch.isValid() || last_fetch.daysTo(QDateTime::currentDateTimeUtc()) >= BOARD_LIST_TTL) {
+ enqueueAction(new FetchForumsAction(this));
+ }
+ }
+}
+
+void Board::handleActionFinished(Action *action)
+{
+ removeFromActionQueue(action);
+}
+
+void Board::handleActionError(Action *action, const QString& message)
+{
+ qWarning() << "Action failed:" << message;
+ removeFromActionQueue(action);
+}
diff --git a/board.h b/board.h
new file mode 100644
index 0000000..38e5297
--- /dev/null
+++ b/board.h
@@ -0,0 +1,75 @@
+#ifndef BOARD_H
+#define BOARD_H
+
+#include <QtCore/QObject>
+#include <QtCore/QQueue>
+#include <QtSql/QSqlDatabase>
+
+class Action;
+class XmlRpcInterface;
+
+class Board : public QObject
+{
+ Q_OBJECT
+public:
+ explicit Board(const QString& boardUrl, QObject *parent = 0);
+
+ bool busy() const;
+ void enqueueAction(Action* action);
+
+ QSqlDatabase database();
+ XmlRpcInterface *service();
+
+ QString getConfig(const QString& key) const;
+ void setConfig(const QString& key, const QString &value);
+
+ int rootForumId() const;
+
+ void notifyConfigChanged();
+ void notifyForumsChanged();
+ void notifyForumTopicsChanged(int forumId, int start, int end);
+
+signals:
+ void configChanged();
+ void forumsChanged();
+ void imageChanged(const QString& imageUrl);
+ void forumTopicsChanged(int forumId, int start, int end);
+
+private:
+ static QString createSlug(const QString& forumUrl);
+ static QString getDbDir();
+ static QString getDbPathFor(const QString& slug);
+ bool initializeDb();
+ bool removeFromActionQueue(Action *action);
+ void executeActionFromQueue();
+ void fetchConfigIfOutdated();
+ void fetchForumsIfOutdated();
+
+private slots:
+ void handleActionFinished(Action *action);
+ void handleActionError(Action *action, const QString& message);
+
+private:
+ QString _url;
+ QString _slug;
+ QSqlDatabase _db;
+ XmlRpcInterface *_iface;
+ QQueue<Action*> _queue;
+};
+
+inline bool Board::busy() const
+{
+ return !_queue.empty();
+}
+
+inline QSqlDatabase Board::database()
+{
+ return _db;
+}
+
+inline XmlRpcInterface * Board::service()
+{
+ return _iface;
+}
+
+#endif // BOARD_H
diff --git a/boardmanager.cpp b/boardmanager.cpp
new file mode 100644
index 0000000..e2473a6
--- /dev/null
+++ b/boardmanager.cpp
@@ -0,0 +1,18 @@
+#include "board.h"
+#include "boardmanager.h"
+
+BoardManager::BoardManager(QObject *parent) :
+ QObject(parent)
+{
+}
+
+Board* BoardManager::getBoard(const QString &url)
+{
+ QHash<QString, Board*>::iterator i = _boards.find(url);
+ if (i != _boards.end()) {
+ return i.value();
+ }
+ Board *db = new Board(url, this);
+ _boards.insert(url, db);
+ return db;
+}
diff --git a/boardmanager.h b/boardmanager.h
new file mode 100644
index 0000000..8ec4c24
--- /dev/null
+++ b/boardmanager.h
@@ -0,0 +1,27 @@
+#ifndef BOARDMANAGER_H
+#define BOARDMANAGER_H
+
+#include <QtCore/QObject>
+#include <QtCore/QHash>
+
+class Board;
+
+class BoardManager : public QObject
+{
+ Q_OBJECT
+public:
+ explicit BoardManager(QObject *parent = 0);
+
+
+ Board *getBoard(const QString& url);
+
+signals:
+
+public slots:
+
+private:
+ QHash<QString, Board*> _boards;
+
+};
+
+#endif // BOARDMANAGER_H
diff --git a/boardmodel.cpp b/boardmodel.cpp
new file mode 100644
index 0000000..77778ad
--- /dev/null
+++ b/boardmodel.cpp
@@ -0,0 +1,141 @@
+#include <QtCore/QDebug>
+#include <QtSql/QSqlError>
+
+#include "global.h"
+#include "board.h"
+#include "boardmodel.h"
+
+BoardModel::BoardModel(QObject *parent) :
+ QAbstractListModel(parent),
+ _rootForumId(0)
+{
+ QHash<int, QByteArray> roles = roleNames();
+ roles[NameRole] = QByteArray("title");
+ roles[LogoRole] = QByteArray("logo");
+ roles[DescriptionRole] = QByteArray("subtitle");
+ roles[ForumIdRole] = QByteArray("forumId");
+ roles[ParentIdRole] = QByteArray("parentId");
+ roles[CategoryRole] = QByteArray("category");
+ setRoleNames(roles);
+}
+
+QString BoardModel::boardUrl() const
+{
+ return _boardUrl;
+}
+
+void BoardModel::setBoardUrl(const QString &url)
+{
+ if (_boardUrl != url) {
+ disconnect(this, SLOT(reload()));
+ _boardUrl = url;
+ reload();
+ emit boardUrlChanged();
+ }
+}
+
+int BoardModel::rootForumId() const
+{
+ return _rootForumId;
+}
+
+void BoardModel::setRootForumId(const int id)
+{
+ if (_rootForumId != id) {
+ _rootForumId = id;
+ reload();
+ emit rootForumIdChanged();
+ }
+}
+
+int BoardModel::rowCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : _records;
+}
+
+QVariant BoardModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid()) return QVariant();
+ if (!_query.seek(index.row())) {
+ qWarning() << "Could not seek to" << index.row();
+ return QVariant();
+ }
+
+ switch (role) {
+ case NameRole:
+ return _query.value(1);
+ break;
+ case DescriptionRole:
+ return _query.value(4);
+ break;
+ case ForumIdRole:
+ return _query.value(0);
+ break;
+ case CategoryRole:
+ return _query.value(5);
+ break;
+ }
+
+ return QVariant();
+}
+
+bool BoardModel::canFetchMore(const QModelIndex &parent) const
+{
+ return parent.isValid() || !_query.isValid() ? false : !_eof;
+}
+
+void BoardModel::fetchMore(const QModelIndex &parent)
+{
+ if (parent.isValid()) return;
+ if (_eof) return;
+
+ const int prefetch_count = 20;
+ int new_num_records;
+ if (_query.seek(_records + prefetch_count)) {
+ new_num_records = _query.at() + 1;
+ } else if (_query.previous()) {
+ // We hit the last record and just went back
+ new_num_records = _query.at() + 1;
+ _eof = true;
+ } else {
+ // There are no records or other error
+ new_num_records = 0;
+ _eof = true;
+ }
+
+
+ if (new_num_records <= 0) {
+ return; // No records!
+ } else if (new_num_records > _records) {
+ beginInsertRows(QModelIndex(), _records, new_num_records - 1);
+ _records = new_num_records;
+ endInsertRows();
+ }
+}
+
+void BoardModel::reload()
+{
+ beginResetModel();
+ _eof = false;
+ _records = 0;
+ _query.clear();
+
+ if (!_boardUrl.isEmpty()) {
+ Board *board = board_manager->getBoard(_boardUrl);
+ connect(board, SIGNAL(forumsChanged()), SLOT(reload()));
+ _query = QSqlQuery(board->database());
+ _query.prepare("SELECT f1.forum_id,f1.forum_name,f1.parent_id,f1.logo_url,f1.description,f2.forum_name AS cat_name FROM forums f1 "
+ "LEFT JOIN forums f2 ON f2.forum_id = f1.parent_id "
+ "WHERE f1.sub_only=0 AND (f1.parent_id=:parent_id_1 OR f1.parent_id IN "
+ " (SELECT forum_id from forums WHERE parent_id=:parent_id_2 AND sub_only=1)) "
+ "ORDER by f1.sort_index ASC;");
+ _query.bindValue(0, _rootForumId);
+ _query.bindValue(1, _rootForumId);
+ if (!_query.exec()) {
+ qWarning() << "Coult not select forums: " << _query.lastError().text();
+ }
+ }
+
+ endResetModel();
+ fetchMore();
+}
diff --git a/boardmodel.h b/boardmodel.h
new file mode 100644
index 0000000..974173e
--- /dev/null
+++ b/boardmodel.h
@@ -0,0 +1,55 @@
+#ifndef BOARDMODEL_H
+#define BOARDMODEL_H
+
+#include <QtCore/QAbstractListModel>
+#include <QtSql/QSqlQuery>
+
+class Board;
+
+class BoardModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(QString boardUrl READ boardUrl WRITE setBoardUrl NOTIFY boardUrlChanged)
+ Q_PROPERTY(int rootForumId READ rootForumId WRITE setRootForumId NOTIFY rootForumIdChanged)
+
+public:
+ BoardModel(QObject *parent = 0);
+
+ enum DataRoles {
+ NameRole = Qt::DisplayRole,
+ LogoRole = Qt::DecorationRole,
+ DescriptionRole = Qt::StatusTipRole,
+
+ ForumIdRole = Qt::UserRole,
+ ParentIdRole,
+ CategoryRole
+ };
+
+ QString boardUrl() const;
+ void setBoardUrl(const QString& url);
+
+ int rootForumId() const;
+ void setRootForumId(const int id);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const;
+ QVariant data(const QModelIndex &index, int role) const;
+
+ bool canFetchMore(const QModelIndex &parent = QModelIndex()) const;
+ void fetchMore(const QModelIndex &parent = QModelIndex());
+
+signals:
+ void boardUrlChanged();
+ void rootForumIdChanged();
+
+private slots:
+ void reload();
+
+private:
+ QString _boardUrl;
+ int _rootForumId;
+ mutable QSqlQuery _query;
+ int _records;
+ bool _eof;
+};
+
+#endif // BOARDMODEL_H
diff --git a/fetchboardconfigaction.cpp b/fetchboardconfigaction.cpp
new file mode 100644
index 0000000..4d1cea2
--- /dev/null
+++ b/fetchboardconfigaction.cpp
@@ -0,0 +1,50 @@
+#include <QtCore/QDateTime>
+#include <QtCore/QDebug>
+#include <QtSql/QSqlQuery>
+
+#include "board.h"
+#include "xmlrpcinterface.h"
+#include "xmlrpcreply.h"
+#include "fetchboardconfigaction.h"
+
+FetchBoardConfigAction::FetchBoardConfigAction(Board *board) :
+ Action(board)
+{
+}
+
+void FetchBoardConfigAction::execute()
+{
+ _call = _board->service()->asyncCall("get_config");
+ connect(_call, SIGNAL(finished(XmlRpcPendingCall*)), SLOT(handleFinishedCall()));
+}
+
+void FetchBoardConfigAction::handleFinishedCall()
+{
+ XmlRpcReply<QVariantMap> result(_call);
+ if (result.isValid()) {
+ QVariantMap map = result;
+ QSqlDatabase db = _board->database();
+ db.transaction();
+ QSqlQuery query(db);
+ query.prepare("INSERT OR REPLACE INTO config (key, value) VALUES (:key, :value)");
+ for (QVariantMap::iterator i = map.begin(); i != map.end(); i++) {
+ query.bindValue(":key", i.key());
+ query.bindValue(":value", i.value().toString());
+ if (!query.exec()) {
+ qWarning() << "Failed to set config key:" << i.key();
+ }
+ }
+ query.bindValue(":key", "last_config_fetch");
+ query.bindValue(":value", QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
+ if (!query.exec()) {
+ qWarning() << "Failed to set last config fetch date";
+ }
+ db.commit();
+ _board->notifyConfigChanged();
+ } else {
+ qWarning() << "Could not fetch board configuration";
+ // TODO emit error ...
+ }
+ emit finished(this);
+ _call->deleteLater();
+}
diff --git a/fetchboardconfigaction.h b/fetchboardconfigaction.h
new file mode 100644
index 0000000..05bd65f
--- /dev/null
+++ b/fetchboardconfigaction.h
@@ -0,0 +1,23 @@
+#ifndef FETCHBOARDCONFIGACTION_H
+#define FETCHBOARDCONFIGACTION_H
+
+#include "action.h"
+
+class XmlRpcPendingCall;
+
+class FetchBoardConfigAction : public Action
+{
+ Q_OBJECT
+public:
+ explicit FetchBoardConfigAction(Board *board);
+
+ void execute();
+
+private slots:
+ void handleFinishedCall();
+
+private:
+ XmlRpcPendingCall *_call;
+};
+
+#endif // FETCHBOARDCONFIGACTION_H
diff --git a/fetchforumsaction.cpp b/fetchforumsaction.cpp
new file mode 100644
index 0000000..8b3e4c8
--- /dev/null
+++ b/fetchforumsaction.cpp
@@ -0,0 +1,107 @@
+#include <QtCore/QDateTime>
+#include <QtCore/QDebug>
+#include <QtSql/QSqlDatabase>
+#include <QtSql/QSqlQuery>
+
+#include "board.h"
+#include "xmlrpcinterface.h"
+#include "xmlrpcreply.h"
+#include "fetchforumsaction.h"
+
+FetchForumsAction::FetchForumsAction(Board *board) :
+ Action(board)
+{
+}
+
+void FetchForumsAction::execute()
+{
+ _call = _board->service()->asyncCall("get_forum");
+ connect(_call, SIGNAL(finished(XmlRpcPendingCall*)), SLOT(handleFinishedCall()));
+}
+
+void FetchForumsAction::handleFinishedCall()
+{
+ XmlRpcReply<QVariantList> result(_call);
+ if (result.isValid()) {
+ int order = 0;
+ QList<QVariantMap> list = flattenForumList(result, &order);
+ QSqlDatabase db = _board->database();
+ db.transaction();
+ db.exec("DELETE FROM forums");
+ if (db.lastError().isValid()) {
+ handleDatabaseError("truncating forums table", db.lastError());
+ }
+ QSqlQuery query(db);
+ query.prepare("INSERT INTO forums (forum_id, forum_name, description, parent_id, logo_url, new_post, is_protected, is_subscribed, can_subscribe, url, sub_only, sort_index)"
+ "VALUES (:forum_id, :forum_name, :description, :parent_id, :logo_url, :new_post, :is_protected, :is_subscribed, :can_subscribe, :url, :sub_only, :sort_index)");
+
+ foreach (const QVariant& list_element, list) {
+ QVariantMap map = list_element.toMap();
+ bool ok = false;
+ int forum_id = map["forum_id"].toInt(&ok);
+ if (!ok) {
+ qWarning() << "No forum_id in" << map;
+ continue;
+ }
+ int parent_id = map["parent_id"].toInt(&ok);
+ if (!ok) {
+ qWarning() << "No parent_id in" << map;
+ continue;
+ }
+
+ query.bindValue(":forum_id", forum_id);
+ query.bindValue(":parent_id", parent_id);
+ query.bindValue(":forum_name", unencodeForumText(map["forum_name"]));
+ query.bindValue(":description", unencodeForumText(map["description"]));
+ query.bindValue(":logo_url", map["logo_url"].toString());
+ query.bindValue(":new_post", map["new_post"].toBool() ? 1 : 0);
+ query.bindValue(":is_protected", map["is_protected"].toBool() ? 1 : 0);
+ query.bindValue(":is_subscribed", map["is_subscribed"].toBool() ? 1 : 0);
+ query.bindValue(":can_subscribe", map["can_subscribed"].toBool() ? 1 : 0);
+ query.bindValue(":url", map["url"].toString());
+ query.bindValue(":sub_only", map["sub_only"].toBool() ? 1 : 0);
+ query.bindValue(":sort_index", map["sort_index"]);
+
+ if (!query.exec()) {
+ qWarning() << "Failed to store forum info for:" << forum_id;
+ handleDatabaseError("storing forum info", query);
+ continue;
+ }
+ }
+
+ _board->setConfig("last_forums_fetch",
+ QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
+ db.commit();
+ _board->notifyForumsChanged();
+ } else {
+ qWarning() << "Could not fetch board forums";
+ // TODO emit error ...
+ }
+ emit finished(this);
+ _call->deleteLater();
+}
+
+QList<QVariantMap> FetchForumsAction::flattenForumList(const QVariantList &list, int *order)
+{
+ QList<QVariantMap> flattened;
+ foreach (const QVariant& list_element, list) {
+ QVariantMap map = list_element.toMap();
+ map["sort_index"] = (*order)++;
+ QVariantMap::iterator child_key = map.find("child");
+ if (child_key != map.end()) {
+ // There are children, so flatten them too.
+ QVariantList sublist = child_key.value().toList();
+ map.erase(child_key);
+ flattened << map << flattenForumList(sublist, order);
+ } else {
+ flattened << map;
+ }
+ }
+ return flattened;
+}
+
+QString FetchForumsAction::unencodeForumText(const QVariant &v)
+{
+ QByteArray ba = v.toByteArray();
+ return QString::fromUtf8(ba.constData(), ba.length());
+}
diff --git a/fetchforumsaction.h b/fetchforumsaction.h
new file mode 100644
index 0000000..b8625fa
--- /dev/null
+++ b/fetchforumsaction.h
@@ -0,0 +1,29 @@
+#ifndef FETCHFORUMSACTION_H
+#define FETCHFORUMSACTION_H
+
+#include <QtCore/QVariant>
+#include "action.h"
+
+class XmlRpcPendingCall;
+
+class FetchForumsAction : public Action
+{
+ Q_OBJECT
+public:
+ explicit FetchForumsAction(Board *board);
+
+ void execute();
+
+private slots:
+ void handleFinishedCall();
+
+private:
+ static QList<QVariantMap> flattenForumList(const QVariantList& list, int *order);
+ static QString unencodeForumText(const QVariant& v);
+
+
+private:
+ XmlRpcPendingCall *_call;
+};
+
+#endif // FETCHFORUMSACTION_H
diff --git a/fetchtopicsaction.cpp b/fetchtopicsaction.cpp
new file mode 100644
index 0000000..5661da8
--- /dev/null
+++ b/fetchtopicsaction.cpp
@@ -0,0 +1,88 @@
+#include <QtCore/QDateTime>
+#include <QtCore/QDebug>
+#include <QtSql/QSqlDatabase>
+#include <QtSql/QSqlQuery>
+
+#include "board.h"
+#include "xmlrpcinterface.h"
+#include "xmlrpcreply.h"
+#include "fetchtopicsaction.h"
+
+FetchTopicsAction::FetchTopicsAction(int forumId, int start, int end, Board *board) :
+ Action(board), _forumId(forumId), _start(start), _end(end)
+{
+}
+
+void FetchTopicsAction::execute()
+{
+ _call = _board->service()->asyncCall("get_topic",
+ QString::number(_forumId), _start, _end);
+ connect(_call, SIGNAL(finished(XmlRpcPendingCall*)), SLOT(handleFinishedCall()));
+}
+
+// forum_id INTEGER, topic_id INTEGER PRIMARY KEY, topic_title TEXT, topic_author_id INTEGER, topic_author_name TEXT, is_subscribed BOOL, is_closed BOOL, icon_url TEXT, last_reply_time TEXT, new_post BOOL
+
+void FetchTopicsAction::handleFinishedCall()
+{
+ XmlRpcReply<QVariantMap> result(_call);
+ if (result.isValid()) {
+ QVariantMap map = result;
+ QVariantList topics = map["topics"].toList();
+ QSqlDatabase db = _board->database();
+ db.transaction();
+
+ QSqlQuery query(db);
+ query.prepare("INSERT OR REPLACE INTO topics (forum_id, topic_id, topic_title, topic_author_id, topic_author_name, is_subscribed, is_closed, icon_url, last_reply_time, new_post, last_update_time) "
+ "VALUES (:forum_id, :topic_id, :topic_title, :topic_author_id, :topic_author_name, :is_subscribed, :is_closed, :icon_url, :last_reply_time, :new_post, :last_update_time)");
+
+ foreach (const QVariant& topic_v, topics) {
+ QVariantMap topic = topic_v.toMap();
+ bool ok = false;
+ int forum_id = topic["forum_id"].toInt(&ok);
+ if (!ok) {
+ qWarning() << "No forum_id in" << topic;
+ continue;
+ }
+ int topic_id = topic["topic_id"].toInt(&ok);
+ if (!ok) {
+ qWarning() << "No parent_id in" << topic;
+ continue;
+ }
+
+ query.bindValue(":forum_id", forum_id);
+ query.bindValue(":topic_id", topic_id);
+ query.bindValue(":topic_title", unencodeTopicText(topic["topic_title"]));
+ query.bindValue(":topic_author_id", topic["topic_author_id"].toInt());
+ query.bindValue(":topic_author_name", unencodeTopicText(topic["topic_author_name"]));
+ query.bindValue(":is_subscribed", topic["is_subscribed"].toBool() ? 1 : 0);
+ query.bindValue(":is_closed", topic["is_closed"].toBool() ? 1 : 0);
+ query.bindValue(":icon_url", topic["icon_url"].toString());
+ query.bindValue(":last_reply_time", topic["last_reply_time"].toDateTime());
+ query.bindValue(":new_post", topic["new_post"].toBool() ? 1 : 0);
+ query.bindValue(":last_update_time", QDateTime::currentDateTime());
+
+ if (!query.exec()) {
+ qWarning() << "Failed to store topic info for:" << topic_id;
+ handleDatabaseError("storing topic info", query);
+ continue;
+ }
+ }
+
+ db.commit();
+ if (topics.size() > 0) {
+ _board->notifyForumTopicsChanged(_forumId,
+ _start, _start + topics.size() - 1);
+ }
+ } else {
+ qWarning() << "Could not fetch topics";
+ // TODO emit error ...
+ }
+ emit finished(this);
+ _call->deleteLater();
+}
+
+QString FetchTopicsAction::unencodeTopicText(const QVariant &v)
+{
+ QByteArray ba = v.toByteArray();
+ return QString::fromUtf8(ba.constData(), ba.length());
+}
diff --git a/fetchtopicsaction.h b/fetchtopicsaction.h
new file mode 100644
index 0000000..747ad93
--- /dev/null
+++ b/fetchtopicsaction.h
@@ -0,0 +1,32 @@
+#ifndef FETCHTOPICSACTION_H
+#define FETCHTOPICSACTION_H
+
+#include <QtCore/QVariant>
+#include <QtSql/QSqlQuery>
+#include "action.h"
+
+class XmlRpcPendingCall;
+
+class FetchTopicsAction : public Action
+{
+ Q_OBJECT
+public:
+ explicit FetchTopicsAction(int forumId, int start, int end, Board *board);
+
+ void execute();
+
+private slots:
+ void handleFinishedCall();
+
+private:
+ static QString unencodeTopicText(const QVariant& v);
+
+private:
+ XmlRpcPendingCall *_call;
+ int _forumId;
+ int _start;
+ int _end;
+
+};
+
+#endif // FETCHTOPICSACTION_H
diff --git a/forummodel.cpp b/forummodel.cpp
new file mode 100644
index 0000000..43cb0bd
--- /dev/null
+++ b/forummodel.cpp
@@ -0,0 +1,281 @@
+#include <QtCore/QDebug>
+#include <QtSql/QSqlError>
+
+#include "global.h"
+#include "board.h"
+#include "xmlrpcinterface.h"
+#include "fetchtopicsaction.h"
+#include "forummodel.h"
+
+ForumModel::ForumModel(QObject *parent) :
+ QAbstractListModel(parent), _boardUrl(), _board(0), _forumId(-1)
+{
+ QHash<int, QByteArray> roles = roleNames();
+ roles[TitleRole] = QByteArray("title");
+ roles[IconRole] = QByteArray("icon");
+ //roles[DescriptionRole] = QByteArray("subtitle");
+ //roles[ForumIdRole] = QByteArray("forumId");
+ //roles[ParentIdRole] = QByteArray("parentId");
+ //roles[CategoryRole] = QByteArray("category");
+ setRoleNames(roles);
+}
+
+QString ForumModel::boardUrl() const
+{
+ return _boardUrl;
+}
+
+void ForumModel::setBoardUrl(const QString &url)
+{
+ if (_boardUrl != url) {
+ disconnect(this, SLOT(handleForumTopicsChanged(int,int,int)));
+ clearModel();
+ _board = 0;
+
+ _boardUrl = url;
+ if (!_boardUrl.isEmpty()) {
+ _board = board_manager->getBoard(_boardUrl);
+ connect(_board, SIGNAL(forumTopicsChanged(int,int,int)),
+ SLOT(handleForumTopicsChanged(int,int,int)));
+ if (_forumId >= 0) {
+ update();
+ reload();
+ }
+ }
+ emit boardUrlChanged();
+ }
+}
+
+int ForumModel::forumId() const
+{
+ return _forumId;
+}
+
+void ForumModel::setForumId(const int id)
+{
+ if (_forumId != id) {
+ clearModel();
+
+ _forumId = id;
+
+ if (_forumId >= 0 && _board) {
+ update();
+ reload();
+ }
+ emit forumIdChanged();
+ }
+}
+
+int ForumModel::rowCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : _data.size();
+}
+
+QVariant ForumModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid()) return QVariant();
+ const int row = index.row();
+ if (row >= _data.size()) {
+ qWarning() << "Could not seek to" << row;
+ return QVariant();
+ }
+
+ switch (role) {
+ case TitleRole:
+ return _data[row].title;
+ break;
+ }
+
+ return QVariant();
+}
+
+bool ForumModel::canFetchMore(const QModelIndex &parent) const
+{
+ if (parent.isValid() || !_board) return false; // Invalid state
+ return !_eof;
+}
+
+void ForumModel::fetchMore(const QModelIndex &parent)
+{
+ if (parent.isValid()) return;
+ if (!_board) return;
+ if (_eof) return;
+
+ const int start = _data.size();
+ QList<Topic> topics = loadTopics(start, start + FORUM_PAGE_SIZE - 1);
+ const int new_end = start + _data.size() - 1;
+
+ if (topics.empty()) {
+ // We could not load anything more from DB!
+ _eof = true;
+ } else {
+ beginInsertRows(QModelIndex(), start, new_end);
+ _data.append(topics);
+ _eof = topics.size() < FORUM_PAGE_SIZE; // If short read, we reached EOF.
+ endInsertRows();
+ }
+
+ if (_board->service()->isAccessible()) {
+ if (!_data.empty()) {
+ QDateTime last = oldestPostUpdate(topics);
+ // If the topics we got from DB are too old, refresh online.
+ if (last.secsTo(QDateTime::currentDateTime()) > FORUM_TOPICS_TLL) {
+ qDebug() << "Fetching topics because of old";
+ _board->enqueueAction(new FetchTopicsAction(_forumId,
+ start,
+ new_end,
+ _board));
+ }
+ }
+
+ // Try to fetch more topics if board is online and we reached the end of DB
+ if (_eof) {
+ qDebug() << "Fetching topics because of EOF";
+ _board->enqueueAction(new FetchTopicsAction(_forumId,
+ _data.size(),
+ _data.size() + FORUM_PAGE_SIZE - 1,
+ _board));
+ }
+ }
+}
+
+QDateTime ForumModel::parseDateTime(const QVariant &v)
+{
+ QString s = v.toString();
+ return QDateTime::fromString(s, Qt::ISODate);
+}
+
+QDateTime ForumModel::oldestPostUpdate(const QList<Topic> &topics)
+{
+ if (topics.empty()) return QDateTime::currentDateTime();
+ QDateTime min = topics.first().last_update_time;
+ foreach (const Topic& topic, topics) {
+ if (min < topic.last_update_time) min = topic.last_update_time;
+ }
+ return min;
+}
+
+QDateTime ForumModel::lastTopPostUpdate()
+{
+ if (!_board) return QDateTime();
+ QSqlDatabase db = _board->database();
+ QSqlQuery query(db);
+ query.prepare("SELECT last_update_time FROM topics "
+ "WHERE forum_id = :forum_id "
+ "ORDER BY last_reply_time DESC "
+ "LIMIT 1");
+ query.bindValue(":forum_id", _forumId);
+ if (query.exec()) {
+ if (query.next()) {
+ return parseDateTime(query.value(0));
+ }
+ } else {
+ qWarning() << "Could not fetch posts:" << query.lastError().text();
+ }
+ return QDateTime();
+}
+
+QList<ForumModel::Topic> ForumModel::loadTopics(int start, int end)
+{
+ Q_ASSERT(_board);
+ const int rows = end - start + 1;
+ QList<Topic> topics;
+ QSqlQuery query(_board->database());
+ query.prepare("SELECT topic_id, topic_title, last_reply_time, last_update_time FROM topics "
+ "WHERE forum_id = :forum_id "
+ "ORDER by last_reply_time DESC "
+ "LIMIT :start, :limit");
+ query.bindValue(":forum_id", _forumId);
+ query.bindValue(":start", start);
+ query.bindValue(":limit", rows);
+ if (query.exec()) {
+ topics.reserve(rows);
+ while (query.next()) {
+ Topic topic;
+ topic.topic_id = query.value(0).toInt();
+ topic.title = query.value(1).toString();
+ topic.last_reply_time = parseDateTime(query.value(2));
+ topic.last_update_time = parseDateTime(query.value(3));
+ topics.append(topic);
+ }
+ } else {
+ qWarning() << "Could not load topics:" << query.lastError().text();
+ }
+ return topics;
+}
+
+void ForumModel::clearModel()
+{
+ beginResetModel();
+ _eof = false;
+ _data.clear();
+ endResetModel();
+}
+
+void ForumModel::handleForumTopicsChanged(int forumId, int start, int end)
+{
+ if (forumId == _forumId) {
+ // Yep, our topics list changed.
+ qDebug() << "My topics changed" << start << end;
+ if (end > _data.size()) {
+ // If for any reason we have more topics now, it means we might
+ // no longer be EOF...
+ _eof = false;
+ }
+ if (start > _data.size() + 1) {
+ // We are still not interested into these topics.
+ qDebug() << "Topics too far";
+ return;
+ }
+
+ QList<Topic> topics = loadTopics(start, end);
+ if (topics.size() < end - start + 1) {
+ _eof = true; // Short read
+ end = start + topics.size() - 1;
+ }
+
+ if (end >= _data.size()) {
+ qDebug() << "Call insert rows" << _data.size() << end;
+ beginInsertRows(QModelIndex(), _data.size(), end);
+ _data.reserve(end + 1);
+ for (int i = start; i < _data.size(); i++) {
+ _data[i] = topics[i - start];
+ }
+ for (int i = _data.size(); i <= end; i++) {
+ Q_ASSERT(i >= start);
+ _data.append(topics[i - start]);
+ }
+ endInsertRows();
+ emit dataChanged(createIndex(start, 0), createIndex(_data.size() - 1, 0));
+ } else {
+ qDebug() << "Just refresh the data";
+ for (int i = start; i < end; i++) {
+ _data[i] = topics[i - start];
+ }
+ emit dataChanged(createIndex(start, 0), createIndex(end, 0));
+ }
+ }
+}
+
+void ForumModel::update()
+{
+ if (!_board || _forumId < 0) return;
+ // Start by requesting an update of the first 20 topics
+ if (!_board->service()->isAccessible()) return;
+ QDateTime last = lastTopPostUpdate();
+ if (!last.isValid() ||
+ last.secsTo(QDateTime::currentDateTime()) > FORUM_TOP_TLL) {
+ // Outdated or empty, refresh.
+ _board->enqueueAction(new FetchTopicsAction(_forumId, 0, FORUM_PAGE_SIZE - 1, _board));
+ } else {
+ qDebug() << "Topics not outdated";
+ }
+}
+
+void ForumModel::reload()
+{
+ Q_ASSERT(_data.empty());
+ Q_ASSERT(!_eof);
+ // Fetch an initial bunch of topics
+ fetchMore();
+}
diff --git a/forummodel.h b/forummodel.h
new file mode 100644
index 0000000..8cde5cf
--- /dev/null
+++ b/forummodel.h
@@ -0,0 +1,72 @@
+#ifndef FORUMMODEL_H
+#define FORUMMODEL_H
+
+#include <QtCore/QAbstractListModel>
+#include <QtCore/QDateTime>
+#include <QtSql/QSqlQuery>
+
+class Board;
+
+class ForumModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(QString boardUrl READ boardUrl WRITE setBoardUrl NOTIFY boardUrlChanged)
+ Q_PROPERTY(int forumId READ forumId WRITE setForumId NOTIFY forumIdChanged)
+
+public:
+ ForumModel(QObject *parent = 0);
+
+ enum DataRoles {
+ TitleRole = Qt::DisplayRole,
+ IconRole = Qt::DecorationRole,
+
+ TopicIdRole = Qt::UserRole,
+ TopicTypeRole
+ };
+
+ QString boardUrl() const;
+ void setBoardUrl(const QString& url);
+
+ int forumId() const;
+ void setForumId(const int id);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const;
+ QVariant data(const QModelIndex &index, int role) const;
+
+ bool canFetchMore(const QModelIndex &parent = QModelIndex()) const;
+ void fetchMore(const QModelIndex &parent = QModelIndex());
+
+signals:
+ void boardUrlChanged();
+ void forumIdChanged();
+
+protected:
+ struct Topic {
+ int topic_id;
+ QString title;
+ QDateTime last_reply_time;
+ QDateTime last_update_time;
+ };
+
+private:
+ static QDateTime parseDateTime(const QVariant& v);
+ static QDateTime oldestPostUpdate(const QList<Topic>& topics);
+ QDateTime lastTopPostUpdate();
+ QList<Topic> loadTopics(int start, int end);
+ void clearModel();
+
+private slots:
+ void handleForumTopicsChanged(int forumId, int start, int end);
+ void update();
+ void reload();
+
+private:
+ QString _boardUrl;
+ Board *_board;
+ int _forumId;
+ QList<Topic> _data;
+ bool _eof;
+};
+
+
+#endif // FORUMMODEL_H
diff --git a/global.h b/global.h
new file mode 100644
index 0000000..db9a3e6
--- /dev/null
+++ b/global.h
@@ -0,0 +1,23 @@
+#ifndef GLOBAL_H
+#define GLOBAL_H
+
+#include "boardmanager.h"
+
+/** Time the forum config settings should be considered up to date, in days. */
+#define BOARD_CONFIG_TTL 2
+
+/** Time the list of forums should be considered up to date, in days. */
+#define BOARD_LIST_TTL 2
+
+/** Time we should consider the most recent topics in a forum up to date, in seconds. */
+#define FORUM_TOP_TLL 5 * 60
+
+/** Time we should consider other topics in a forum up to date, in seconds. */
+#define FORUM_TOPICS_TLL 15 * 60
+
+/** Number of topics per "block" in the forum view */
+#define FORUM_PAGE_SIZE 20
+
+extern BoardManager *board_manager;
+
+#endif // GLOBAL_H
diff --git a/main.cpp b/main.cpp
new file mode 100644
index 0000000..f15094d
--- /dev/null
+++ b/main.cpp
@@ -0,0 +1,37 @@
+#include <QtGui/QApplication>
+#include <QtDeclarative/QtDeclarative>
+#include "qmlapplicationviewer.h"
+
+#include "global.h"
+#include "board.h"
+#include "boardmodel.h"
+#include "forummodel.h"
+
+#include "xmlrpcinterface.h"
+#include "xmlrpcreply.h"
+
+BoardManager *board_manager;
+
+Q_DECL_EXPORT int main(int argc, char *argv[])
+{
+ QScopedPointer<QApplication> app(createApplication(argc, argv));
+ QScopedPointer<BoardManager> manager(new BoardManager);
+ QmlApplicationViewer viewer;
+
+ board_manager = manager.data();
+
+ //Board *test = manager->getBoard("http://support.tapatalk.com/mobiquo/mobiquo.php");
+ //XmlRpcInterface *iface = test->service();
+ //XmlRpcPendingCall *call = iface->asyncCall("get_topic", "41");
+ //call->waitForFinished();
+ //Board *test = manager->getBoard("http://localhost:4444/point.php");
+
+ qmlRegisterType<BoardModel>("com.javispedro.tapasboard", 1, 0, "BoardModel");
+ qmlRegisterType<ForumModel>("com.javispedro.tapasboard", 1, 0, "ForumModel");
+
+ viewer.setOrientation(QmlApplicationViewer::ScreenOrientationAuto);
+ viewer.setMainQmlFile(QLatin1String("qml/tapasboard/main.qml"));
+ viewer.showExpanded();
+
+ return app->exec();
+}
diff --git a/qml/tapasboard/BoardPage.qml b/qml/tapasboard/BoardPage.qml
new file mode 100644
index 0000000..7c3f1cd
--- /dev/null
+++ b/qml/tapasboard/BoardPage.qml
@@ -0,0 +1,57 @@
+import QtQuick 1.1
+import com.nokia.meego 1.1
+import com.nokia.extras 1.1
+import com.javispedro.tapasboard 1.0
+
+Page {
+ id: boardPage
+
+ anchors.leftMargin: UiConstants.DefaultMargin
+ anchors.rightMargin: UiConstants.DefaultMargin
+
+ property string boardUrl;
+ property int rootForumId;
+
+ tools: ToolBarLayout {
+ ToolIcon {
+ id: backToolIcon
+ platformIconId: "toolbar-back"
+ anchors.left: parent.left
+ onClicked: pageStack.pop()
+ }
+ }
+
+ ListView {
+ id: forumsView
+ anchors.fill: parent
+ model: BoardModel {
+ boardUrl: boardPage.boardUrl
+ rootForumId: boardPage.rootForumId
+ }
+ section.criteria: ViewSection.FullString
+ section.property: "category"
+ section.delegate: GroupHeader {
+ width: parent.width
+ text: section
+ }
+
+ delegate: ListDelegate {
+ Image {
+ source: "image://theme/icon-m-common-drilldown-arrow" + (theme.inverted ? "-inverse" : "")
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ onClicked: {
+ pageStack.push(Qt.resolvedUrl("ForumPage.qml"), {
+ boardUrl: boardPage.boardUrl,
+ forumId: model.forumId
+ });
+ }
+ }
+ }
+
+ ScrollDecorator {
+ flickableItem: forumsView
+ }
+}
diff --git a/qml/tapasboard/ForumPage.qml b/qml/tapasboard/ForumPage.qml
new file mode 100644
index 0000000..a70fe74
--- /dev/null
+++ b/qml/tapasboard/ForumPage.qml
@@ -0,0 +1,43 @@
+import QtQuick 1.1
+import com.nokia.meego 1.1
+import com.nokia.extras 1.1
+import com.javispedro.tapasboard 1.0
+
+Page {
+ id: forumPage
+
+ anchors.leftMargin: UiConstants.DefaultMargin
+ anchors.rightMargin: UiConstants.DefaultMargin
+
+ property string boardUrl;
+ property int forumId;
+
+ tools: ToolBarLayout {
+ ToolIcon {
+ id: backToolIcon
+ platformIconId: "toolbar-back"
+ anchors.left: parent.left
+ onClicked: pageStack.pop()
+ }
+ }
+
+ ListView {
+ id: topicsView
+ anchors.fill: parent
+ model: ForumModel {
+ boardUrl: forumPage.boardUrl
+ forumId: forumPage.forumId
+ }
+ delegate: ListDelegate {
+ Image {
+ source: "image://theme/icon-m-common-drilldown-arrow" + (theme.inverted ? "-inverse" : "")
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+
+ ScrollDecorator {
+ flickableItem: topicsView
+ }
+}
diff --git a/qml/tapasboard/GroupHeader.qml b/qml/tapasboard/GroupHeader.qml
new file mode 100644
index 0000000..0350ee0
--- /dev/null
+++ b/qml/tapasboard/GroupHeader.qml
@@ -0,0 +1,26 @@
+import QtQuick 1.1
+import com.nokia.meego 1.1
+
+Item {
+ id: header
+ height: 40
+
+ property alias text: headerLabel.text
+
+ Text {
+ id: headerLabel
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.rightMargin: 8
+ anchors.bottomMargin: 2
+ font: UiConstants.GroupHeaderFont
+ color: theme.inverted ? "#4D4D4D" : "#3C3C3C";
+ }
+ Image {
+ anchors.right: headerLabel.left
+ anchors.left: parent.left
+ anchors.verticalCenter: headerLabel.verticalCenter
+ anchors.rightMargin: 24
+ source: "image://theme/meegotouch-groupheader" + (theme.inverted ? "-inverted" : "") + "-background"
+ }
+}
diff --git a/qml/tapasboard/MainPage.qml b/qml/tapasboard/MainPage.qml
new file mode 100644
index 0000000..7337b12
--- /dev/null
+++ b/qml/tapasboard/MainPage.qml
@@ -0,0 +1,17 @@
+import QtQuick 1.1
+import com.nokia.meego 1.0
+
+Page {
+ tools: commonTools
+
+ Button {
+ anchors {
+ centerIn: parent
+ }
+ text: qsTr("Open board")
+ onClicked: pageStack.push(Qt.resolvedUrl("BoardPage.qml"), {
+ boardUrl: "http://support.tapatalk.com/mobiquo/mobiquo.php",
+ rootForumId: 0
+ })
+ }
+}
diff --git a/qml/tapasboard/main.qml b/qml/tapasboard/main.qml
new file mode 100644
index 0000000..bfb4552
--- /dev/null
+++ b/qml/tapasboard/main.qml
@@ -0,0 +1,30 @@
+import QtQuick 1.1
+import com.nokia.meego 1.0
+
+PageStackWindow {
+ id: appWindow
+
+ initialPage: mainPage
+
+ MainPage {
+ id: mainPage
+ }
+
+ ToolBarLayout {
+ id: commonTools
+ visible: true
+ ToolIcon {
+ platformIconId: "toolbar-view-menu"
+ anchors.right: (parent === undefined) ? undefined : parent.right
+ onClicked: (myMenu.status === DialogStatus.Closed) ? myMenu.open() : myMenu.close()
+ }
+ }
+
+ Menu {
+ id: myMenu
+ visualParent: pageStack
+ MenuLayout {
+ MenuItem { text: qsTr("Sample menu item") }
+ }
+ }
+}
diff --git a/qmlapplicationviewer/qmlapplicationviewer.cpp b/qmlapplicationviewer/qmlapplicationviewer.cpp
new file mode 100644
index 0000000..997bbfc
--- /dev/null
+++ b/qmlapplicationviewer/qmlapplicationviewer.cpp
@@ -0,0 +1,174 @@
+// checksum 0xee24 version 0x70013
+/*
+ This file was generated by the Qt Quick Application wizard of Qt Creator.
+ QmlApplicationViewer is a convenience class containing mobile device specific
+ code such as screen orientation handling. Also QML paths and debugging are
+ handled here.
+ It is recommended not to modify this file, since newer versions of Qt Creator
+ may offer an updated version of it.
+*/
+
+#include "qmlapplicationviewer.h"
+
+#include <QtCore/QDir>
+#include <QtCore/QFileInfo>
+#include <QtGui/QApplication>
+#include <QtDeclarative/QDeclarativeComponent>
+#include <QtDeclarative/QDeclarativeEngine>
+#include <QtDeclarative/QDeclarativeContext>
+
+#include <qplatformdefs.h> // MEEGO_EDITION_HARMATTAN
+
+#ifdef HARMATTAN_BOOSTER
+#include <MDeclarativeCache>
+#endif
+
+#if defined(QMLJSDEBUGGER) && QT_VERSION < 0x040800
+
+#include <qt_private/qdeclarativedebughelper_p.h>
+
+#if !defined(NO_JSDEBUGGER)
+#include <jsdebuggeragent.h>
+#endif
+#if !defined(NO_QMLOBSERVER)
+#include <qdeclarativeviewobserver.h>
+#endif
+
+// Enable debugging before any QDeclarativeEngine is created
+struct QmlJsDebuggingEnabler
+{
+ QmlJsDebuggingEnabler()
+ {
+ QDeclarativeDebugHelper::enableDebugging();
+ }
+};
+
+// Execute code in constructor before first QDeclarativeEngine is instantiated
+static QmlJsDebuggingEnabler enableDebuggingHelper;
+
+#endif // QMLJSDEBUGGER
+
+class QmlApplicationViewerPrivate
+{
+ QString mainQmlFile;
+ friend class QmlApplicationViewer;
+ static QString adjustPath(const QString &path);
+};
+
+QString QmlApplicationViewerPrivate::adjustPath(const QString &path)
+{
+#ifdef Q_OS_UNIX
+#ifdef Q_OS_MAC
+ if (!QDir::isAbsolutePath(path))
+ return QString::fromLatin1("%1/../Resources/%2")
+ .arg(QCoreApplication::applicationDirPath(), path);
+#else
+ const QString pathInInstallDir =
+ QString::fromLatin1("%1/../%2").arg(QCoreApplication::applicationDirPath(), path);
+ if (QFileInfo(pathInInstallDir).exists())
+ return pathInInstallDir;
+#endif
+#endif
+ return path;
+}
+
+QmlApplicationViewer::QmlApplicationViewer(QWidget *parent)
+ : QDeclarativeView(parent)
+ , d(new QmlApplicationViewerPrivate())
+{
+ connect(engine(), SIGNAL(quit()), SLOT(close()));
+ setResizeMode(QDeclarativeView::SizeRootObjectToView);
+ // Qt versions prior to 4.8.0 don't have QML/JS debugging services built in
+#if defined(QMLJSDEBUGGER) && QT_VERSION < 0x040800
+#if !defined(NO_JSDEBUGGER)
+ new QmlJSDebugger::JSDebuggerAgent(engine());
+#endif
+#if !defined(NO_QMLOBSERVER)
+ new QmlJSDebugger::QDeclarativeViewObserver(this, this);
+#endif
+#endif
+}
+
+QmlApplicationViewer::~QmlApplicationViewer()
+{
+ delete d;
+}
+
+QmlApplicationViewer *QmlApplicationViewer::create()
+{
+ return new QmlApplicationViewer();
+}
+
+void QmlApplicationViewer::setMainQmlFile(const QString &file)
+{
+ d->mainQmlFile = QmlApplicationViewerPrivate::adjustPath(file);
+ setSource(QUrl::fromLocalFile(d->mainQmlFile));
+}
+
+void QmlApplicationViewer::addImportPath(const QString &path)
+{
+ engine()->addImportPath(QmlApplicationViewerPrivate::adjustPath(path));
+}
+
+void QmlApplicationViewer::setOrientation(ScreenOrientation orientation)
+{
+#if defined(Q_OS_SYMBIAN)
+ // If the version of Qt on the device is < 4.7.2, that attribute won't work
+ if (orientation != ScreenOrientationAuto) {
+ const QStringList v = QString::fromAscii(qVersion()).split(QLatin1Char('.'));
+ if (v.count() == 3 && (v.at(0).toInt() << 16 | v.at(1).toInt() << 8 | v.at(2).toInt()) < 0x040702) {
+ qWarning("Screen orientation locking only supported with Qt 4.7.2 and above");
+ return;
+ }
+ }
+#endif // Q_OS_SYMBIAN
+
+ Qt::WidgetAttribute attribute;
+ switch (orientation) {
+#if QT_VERSION < 0x040702
+ // Qt < 4.7.2 does not yet have the Qt::WA_*Orientation attributes
+ case ScreenOrientationLockPortrait:
+ attribute = static_cast<Qt::WidgetAttribute>(128);
+ break;
+ case ScreenOrientationLockLandscape:
+ attribute = static_cast<Qt::WidgetAttribute>(129);
+ break;
+ default:
+ case ScreenOrientationAuto:
+ attribute = static_cast<Qt::WidgetAttribute>(130);
+ break;
+#else // QT_VERSION < 0x040702
+ case ScreenOrientationLockPortrait:
+ attribute = Qt::WA_LockPortraitOrientation;
+ break;
+ case ScreenOrientationLockLandscape:
+ attribute = Qt::WA_LockLandscapeOrientation;
+ break;
+ default:
+ case ScreenOrientationAuto:
+ attribute = Qt::WA_AutoOrientation;
+ break;
+#endif // QT_VERSION < 0x040702
+ };
+ setAttribute(attribute, true);
+}
+
+void QmlApplicationViewer::showExpanded()
+{
+#if defined(Q_OS_SYMBIAN) || defined(MEEGO_EDITION_HARMATTAN) || defined(Q_WS_SIMULATOR)
+ showFullScreen();
+#elif defined(Q_WS_MAEMO_5)
+ showMaximized();
+#else
+ show();
+#endif
+}
+
+QApplication *createApplication(int &argc, char **argv)
+{
+#ifdef HARMATTAN_BOOSTER
+ return MDeclarativeCache::qApplication(argc, argv);
+#else
+ return new QApplication(argc, argv);
+#endif
+}
diff --git a/qmlapplicationviewer/qmlapplicationviewer.h b/qmlapplicationviewer/qmlapplicationviewer.h
new file mode 100644
index 0000000..b01cc88
--- /dev/null
+++ b/qmlapplicationviewer/qmlapplicationviewer.h
@@ -0,0 +1,46 @@
+// checksum 0x898f version 0x70013
+/*
+ This file was generated by the Qt Quick Application wizard of Qt Creator.
+ QmlApplicationViewer is a convenience class containing mobile device specific
+ code such as screen orientation handling. Also QML paths and debugging are
+ handled here.
+ It is recommended not to modify this file, since newer versions of Qt Creator
+ may offer an updated version of it.
+*/
+
+#ifndef QMLAPPLICATIONVIEWER_H
+#define QMLAPPLICATIONVIEWER_H
+
+#include <QtDeclarative/QDeclarativeView>
+
+class QmlApplicationViewer : public QDeclarativeView
+{
+ Q_OBJECT
+
+public:
+ enum ScreenOrientation {
+ ScreenOrientationLockPortrait,
+ ScreenOrientationLockLandscape,
+ ScreenOrientationAuto
+ };
+
+ explicit QmlApplicationViewer(QWidget *parent = 0);
+ virtual ~QmlApplicationViewer();
+
+ static QmlApplicationViewer *create();
+
+ void setMainQmlFile(const QString &file);
+ void addImportPath(const QString &path);
+
+ // Note that this will only have an effect on Symbian and Fremantle.
+ void setOrientation(ScreenOrientation orientation);
+
+ void showExpanded();
+
+private:
+ class QmlApplicationViewerPrivate *d;
+};
+
+QApplication *createApplication(int &argc, char **argv);
+
+#endif // QMLAPPLICATIONVIEWER_H
diff --git a/qmlapplicationviewer/qmlapplicationviewer.pri b/qmlapplicationviewer/qmlapplicationviewer.pri
new file mode 100644
index 0000000..b6931d0
--- /dev/null
+++ b/qmlapplicationviewer/qmlapplicationviewer.pri
@@ -0,0 +1,148 @@
+# checksum 0x5b42 version 0x70013
+# This file was generated by the Qt Quick Application wizard of Qt Creator.
+# The code below adds the QmlApplicationViewer to the project and handles the
+# activation of QML debugging.
+# It is recommended not to modify this file, since newer versions of Qt Creator
+# may offer an updated version of it.
+
+QT += declarative
+
+SOURCES += $$PWD/qmlapplicationviewer.cpp
+HEADERS += $$PWD/qmlapplicationviewer.h
+INCLUDEPATH += $$PWD
+
+# Include JS debugger library if QMLJSDEBUGGER_PATH is set
+!isEmpty(QMLJSDEBUGGER_PATH) {
+ include($$QMLJSDEBUGGER_PATH/qmljsdebugger-lib.pri)
+} else {
+ DEFINES -= QMLJSDEBUGGER
+}
+
+contains(CONFIG,qdeclarative-boostable):contains(MEEGO_EDITION,harmattan) {
+ DEFINES += HARMATTAN_BOOSTER
+}
+# This file was generated by an application wizard of Qt Creator.
+# The code below handles deployment to Symbian and Maemo, aswell as copying
+# of the application data to shadow build directories on desktop.
+# It is recommended not to modify this file, since newer versions of Qt Creator
+# may offer an updated version of it.
+
+defineTest(qtcAddDeployment) {
+for(deploymentfolder, DEPLOYMENTFOLDERS) {
+ item = item$${deploymentfolder}
+ itemsources = $${item}.sources
+ $$itemsources = $$eval($${deploymentfolder}.source)
+ itempath = $${item}.path
+ $$itempath= $$eval($${deploymentfolder}.target)
+ export($$itemsources)
+ export($$itempath)
+ DEPLOYMENT += $$item
+}
+
+MAINPROFILEPWD = $$PWD
+
+symbian {
+ isEmpty(ICON):exists($${TARGET}.svg):ICON = $${TARGET}.svg
+ isEmpty(TARGET.EPOCHEAPSIZE):TARGET.EPOCHEAPSIZE = 0x20000 0x2000000
+} else:win32 {
+ copyCommand =
+ for(deploymentfolder, DEPLOYMENTFOLDERS) {
+ source = $$MAINPROFILEPWD/$$eval($${deploymentfolder}.source)
+ source = $$replace(source, /, \\)
+ sourcePathSegments = $$split(source, \\)
+ target = $$OUT_PWD/$$eval($${deploymentfolder}.target)/$$last(sourcePathSegments)
+ target = $$replace(target, /, \\)
+ target ~= s,\\\\\\.?\\\\,\\,
+ !isEqual(source,$$target) {
+ !isEmpty(copyCommand):copyCommand += &&
+ isEqual(QMAKE_DIR_SEP, \\) {
+ copyCommand += $(COPY_DIR) \"$$source\" \"$$target\"
+ } else {
+ source = $$replace(source, \\\\, /)
+ target = $$OUT_PWD/$$eval($${deploymentfolder}.target)
+ target = $$replace(target, \\\\, /)
+ copyCommand += test -d \"$$target\" || mkdir -p \"$$target\" && cp -r \"$$source\" \"$$target\"
+ }
+ }
+ }
+ !isEmpty(copyCommand) {
+ copyCommand = @echo Copying application data... && $$copyCommand
+ copydeploymentfolders.commands = $$copyCommand
+ first.depends = $(first) copydeploymentfolders
+ export(first.depends)
+ export(copydeploymentfolders.commands)
+ QMAKE_EXTRA_TARGETS += first copydeploymentfolders
+ }
+} else:unix {
+ maemo5 {
+ desktopfile.files = $${TARGET}.desktop
+ desktopfile.path = /usr/share/applications/hildon
+ icon.files = $${TARGET}64.png
+ icon.path = /usr/share/icons/hicolor/64x64/apps
+ } else:!isEmpty(MEEGO_VERSION_MAJOR) {
+ desktopfile.files = $${TARGET}_harmattan.desktop
+ desktopfile.path = /usr/share/applications
+ icon.files = $${TARGET}80.png
+ icon.path = /usr/share/icons/hicolor/80x80/apps
+ } else { # Assumed to be a Desktop Unix
+ copyCommand =
+ for(deploymentfolder, DEPLOYMENTFOLDERS) {
+ source = $$MAINPROFILEPWD/$$eval($${deploymentfolder}.source)
+ source = $$replace(source, \\\\, /)
+ macx {
+ target = $$OUT_PWD/$${TARGET}.app/Contents/Resources/$$eval($${deploymentfolder}.target)
+ } else {
+ target = $$OUT_PWD/$$eval($${deploymentfolder}.target)
+ }
+ target = $$replace(target, \\\\, /)
+ sourcePathSegments = $$split(source, /)
+ targetFullPath = $$target/$$last(sourcePathSegments)
+ targetFullPath ~= s,/\\.?/,/,
+ !isEqual(source,$$targetFullPath) {
+ !isEmpty(copyCommand):copyCommand += &&
+ copyCommand += $(MKDIR) \"$$target\"
+ copyCommand += && $(COPY_DIR) \"$$source\" \"$$target\"
+ }
+ }
+ !isEmpty(copyCommand) {
+ copyCommand = @echo Copying application data... && $$copyCommand
+ copydeploymentfolders.commands = $$copyCommand
+ first.depends = $(first) copydeploymentfolders
+ export(first.depends)
+ export(copydeploymentfolders.commands)
+ QMAKE_EXTRA_TARGETS += first copydeploymentfolders
+ }
+ }
+ installPrefix = /opt/$${TARGET}
+ for(deploymentfolder, DEPLOYMENTFOLDERS) {
+ item = item$${deploymentfolder}
+ itemfiles = $${item}.files
+ $$itemfiles = $$eval($${deploymentfolder}.source)
+ itempath = $${item}.path
+ $$itempath = $${installPrefix}/$$eval($${deploymentfolder}.target)
+ export($$itemfiles)
+ export($$itempath)
+ INSTALLS += $$item
+ }
+
+ !isEmpty(desktopfile.path) {
+ export(icon.files)
+ export(icon.path)
+ export(desktopfile.files)
+ export(desktopfile.path)
+ INSTALLS += icon desktopfile
+ }
+
+ target.path = $${installPrefix}/bin
+ export(target.path)
+ INSTALLS += target
+}
+
+export (ICON)
+export (INSTALLS)
+export (DEPLOYMENT)
+export (TARGET.EPOCHEAPSIZE)
+export (TARGET.CAPABILITY)
+export (LIBS)
+export (QMAKE_EXTRA_TARGETS)
+}
diff --git a/qtc_packaging/debian_harmattan/README b/qtc_packaging/debian_harmattan/README
new file mode 100644
index 0000000..8743a0c
--- /dev/null
+++ b/qtc_packaging/debian_harmattan/README
@@ -0,0 +1,6 @@
+The Debian Package tapasboard
+----------------------------
+
+Comments regarding the Package
+
+ -- Javier <javier@unknown> Fri, 29 Mar 2013 14:59:58 +0100
diff --git a/qtc_packaging/debian_harmattan/changelog b/qtc_packaging/debian_harmattan/changelog
new file mode 100644
index 0000000..40aef97
--- /dev/null
+++ b/qtc_packaging/debian_harmattan/changelog
@@ -0,0 +1,5 @@
+tapasboard (0.0.1) unstable; urgency=low
+
+ * Initial Release.
+
+ -- Javier <javier@unknown> Fri, 29 Mar 2013 14:59:58 +0100
diff --git a/qtc_packaging/debian_harmattan/compat b/qtc_packaging/debian_harmattan/compat
new file mode 100644
index 0000000..7f8f011
--- /dev/null
+++ b/qtc_packaging/debian_harmattan/compat
@@ -0,0 +1 @@
+7
diff --git a/qtc_packaging/debian_harmattan/control b/qtc_packaging/debian_harmattan/control
new file mode 100644
index 0000000..b727d30
--- /dev/null
+++ b/qtc_packaging/debian_harmattan/control
@@ -0,0 +1,15 @@
+Source: tapasboard
+Section: user/other
+Priority: optional
+Maintainer: Javier <javier@unknown>
+Build-Depends: debhelper (>= 5), libqt4-dev
+Standards-Version: 3.7.3
+Homepage: <insert the upstream URL, if relevant>
+
+Package: tapasboard
+Architecture: any
+Depends: ${shlibs:Depends}, ${misc:Depends}
+Description: <insert up to 60 chars description>
+ <insert long description, indented with spaces>
+XSBC-Maemo-Display-Name: tapasboard
+XB-Maemo-Icon-26: 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
diff --git a/qtc_packaging/debian_harmattan/copyright b/qtc_packaging/debian_harmattan/copyright
new file mode 100644
index 0000000..13f537e
--- /dev/null
+++ b/qtc_packaging/debian_harmattan/copyright
@@ -0,0 +1,40 @@
+This package was debianized by Javier <javier@unknown> on
+Fri, 29 Mar 2013 14:59:58 +0100.
+
+It was downloaded from <url://example.com>
+
+Upstream Author(s):
+
+ <put author's name and email here>
+ <likewise for another author>
+
+Copyright:
+
+ <Copyright (C) YYYY Name OfAuthor>
+ <likewise for another author>
+
+License:
+
+ This package is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This package is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this package; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+On Debian systems, the complete text of the GNU General
+Public License can be found in `/usr/share/common-licenses/GPL'.
+
+The Debian packaging is (C) 2013, Javier <javier@unknown> and
+is licensed under the GPL, see above.
+
+
+# Please also look if there are files or directories which have a
+# different copyright/license attached and list them here.
diff --git a/qtc_packaging/debian_harmattan/manifest.aegis b/qtc_packaging/debian_harmattan/manifest.aegis
new file mode 100644
index 0000000..6e1acdd
--- /dev/null
+++ b/qtc_packaging/debian_harmattan/manifest.aegis
@@ -0,0 +1,70 @@
+AutoGenerateAegisFile
+<!-- Aegis manifest declares the security credentials required by an
+ application to run correctly. By default, a manifest file will be
+ created or updated automatically as a part of build.
+
+ The detection of required credentials is based on static scan of
+ application binaries. In some cases, the scan may not be able to
+ detect the correct set of permissions. If this is the case, you must
+ declare the credentials required by your application in this file.
+
+ To create a manifest file automatically as a part of build (DEFAULT):
+
+ * Make sure this file starts with the string "AutoGenerateAegisFile" (without quotes).
+ * Alternatively, it can also be completely empty.
+
+ To provide a manifest yourself:
+
+ * List the correct credentials for the application in this file.
+ * Some commented-out examples of often required tokens are provided.
+ * Ensure the path to your application binary given in
+ '<for path="/path/to/app" />' is correct.
+ * Please do not request more credentials than what your application
+ actually requires.
+
+ To disable manifest file:
+
+ * Replace this file with a file starting with the string "NoAegisFile" (without quotes).
+ * Final application package will not contain a manifest.
+
+-->
+<aegis>
+ <request policy="add">
+
+ <!-- Make a GSM call, send text messages (SMS). -->
+ <!--
+ <credential name="Cellular" />
+ -->
+
+ <!-- Access Facebook social data. -->
+ <!--
+ <credential name="FacebookSocial" />
+ -->
+
+ <!-- Read access to data stored in tracker. -->
+ <!--
+ <credential name="TrackerReadAccess" />
+ -->
+
+ <!-- Read and write access to data stored in tracker. -->
+ <!--
+ <credential name="TrackerWriteAccess" />
+ -->
+
+ <!-- Read Location information. -->
+ <!--
+ <credential name="Location" />
+ -->
+
+ <!-- Access to Audio, Multimedia and Camera. -->
+ <!--
+ <credential name="GRP::pulse-access" />
+ <credential name="GRP::video" />
+ <credential name="GRP::audio" />
+ -->
+
+ </request>
+
+ <for path="/opt/tapasboard/bin/tapasboard" />
+ <for path="applauncherd-launcher::/usr/bin/applauncherd.bin" id="" />
+</aegis>
diff --git a/qtc_packaging/debian_harmattan/rules b/qtc_packaging/debian_harmattan/rules
new file mode 100755
index 0000000..8031f67
--- /dev/null
+++ b/qtc_packaging/debian_harmattan/rules
@@ -0,0 +1,91 @@
+#!/usr/bin/make -f
+# -*- makefile -*-
+# Sample debian/rules that uses debhelper.
+# This file was originally written by Joey Hess and Craig Small.
+# As a special exception, when this file is copied by dh-make into a
+# dh-make output file, you may use that output file without restriction.
+# This special exception was added by Craig Small in version 0.37 of dh-make.
+
+# Uncomment this to turn on verbose mode.
+#export DH_VERBOSE=1
+
+
+
+
+
+configure: configure-stamp
+configure-stamp:
+ dh_testdir
+ # qmake PREFIX=/usr# Uncomment this line for use without Qt Creator
+
+ touch configure-stamp
+
+
+build: build-stamp
+
+build-stamp: configure-stamp
+ dh_testdir
+
+ # Add here commands to compile the package.
+ # $(MAKE) # Uncomment this line for use without Qt Creator
+ #docbook-to-man debian/tapasboard.sgml > tapasboard.1
+
+ touch $@
+
+clean:
+ dh_testdir
+ dh_testroot
+ rm -f build-stamp configure-stamp
+
+ # Add here commands to clean up after the build process.
+ $(MAKE) clean
+
+ dh_clean
+
+install: build
+ dh_testdir
+ dh_testroot
+ dh_clean -k
+ dh_installdirs
+
+ # Add here commands to install the package into debian/tapasboard.
+ $(MAKE) INSTALL_ROOT="$(CURDIR)"/debian/tapasboard install
+
+
+# Build architecture-independent files here.
+binary-indep: build install
+# We have nothing to do by default.
+
+# Build architecture-dependent files here.
+binary-arch: build install
+ dh_testdir
+ dh_testroot
+ dh_installchangelogs
+ dh_installdocs
+ dh_installexamples
+# dh_install
+# dh_installmenu
+# dh_installdebconf
+# dh_installlogrotate
+# dh_installemacsen
+# dh_installpam
+# dh_installmime
+# dh_python
+# dh_installinit
+# dh_installcron
+# dh_installinfo
+ dh_installman
+ dh_link
+ dh_strip
+ dh_compress
+ dh_fixperms
+# dh_perl
+# dh_makeshlibs
+ dh_installdeb
+ # dh_shlibdeps # Uncomment this line for use without Qt Creator
+ dh_gencontrol
+ dh_md5sums
+ dh_builddeb
+
+binary: binary-indep binary-arch
+.PHONY: build clean binary-indep binary-arch binary install configure
diff --git a/tapasboard.desktop b/tapasboard.desktop
new file mode 100644
index 0000000..54a35d1
--- /dev/null
+++ b/tapasboard.desktop
@@ -0,0 +1,11 @@
+[Desktop Entry]
+Encoding=UTF-8
+Version=1.0
+Type=Application
+Terminal=false
+Name=tapasboard
+Exec=/opt/tapasboard/bin/tapasboard
+Icon=tapasboard64
+X-Window-Icon=
+X-HildonDesk-ShowInToolbar=true
+X-Osso-Type=application/x-executable
diff --git a/tapasboard.pro b/tapasboard.pro
new file mode 100644
index 0000000..a3ab2f5
--- /dev/null
+++ b/tapasboard.pro
@@ -0,0 +1,72 @@
+# Add more folders to ship with the application, here
+folder_01.source = qml/tapasboard
+folder_01.target = qml
+DEPLOYMENTFOLDERS = folder_01
+
+# Additional import path used to resolve QML modules in Creator's code model
+QML_IMPORT_PATH =
+
+symbian:TARGET.UID3 = 0xE33C9CB4
+
+# Smart Installer package's UID
+# This UID is from the protected range and therefore the package will
+# fail to install if self-signed. By default qmake uses the unprotected
+# range value if unprotected UID is defined for the application and
+# 0x2002CCCF value if protected UID is given to the application
+#symbian:DEPLOYMENT.installer_header = 0x2002CCCF
+
+# Allow network access on Symbian
+symbian:TARGET.CAPABILITY += NetworkServices
+
+QT += network sql
+
+# If your application uses the Qt Mobility libraries, uncomment the following
+# lines and add the respective components to the MOBILITY variable.
+# CONFIG += mobility
+# MOBILITY +=
+
+# Speed up launching on MeeGo/Harmattan when using applauncherd daemon
+CONFIG += qdeclarative-boostable
+
+# Add dependency to Symbian components
+# CONFIG += qt-components
+
+# The .cpp file which was generated for your project. Feel free to hack it.
+SOURCES += main.cpp \
+ action.cpp \
+ board.cpp \
+ boardmanager.cpp \
+ fetchboardconfigaction.cpp \
+ xmlrpcinterface.cpp \
+ xmlrpcpendingcall.cpp \
+ fetchforumsaction.cpp \
+ boardmodel.cpp \
+ forummodel.cpp \
+ fetchtopicsaction.cpp
+
+# Please do not modify the following two lines. Required for deployment.
+include(qmlapplicationviewer/qmlapplicationviewer.pri)
+qtcAddDeployment()
+
+OTHER_FILES += \
+ qtc_packaging/debian_harmattan/rules \
+ qtc_packaging/debian_harmattan/README \
+ qtc_packaging/debian_harmattan/manifest.aegis \
+ qtc_packaging/debian_harmattan/copyright \
+ qtc_packaging/debian_harmattan/control \
+ qtc_packaging/debian_harmattan/compat \
+ qtc_packaging/debian_harmattan/changelog
+
+HEADERS += \
+ action.h \
+ board.h \
+ boardmanager.h \
+ fetchboardconfigaction.h \
+ xmlrpcinterface.h \
+ xmlrpcpendingcall.h \
+ xmlrpcreply.h \
+ fetchforumsaction.h \
+ global.h \
+ boardmodel.h \
+ forummodel.h \
+ fetchtopicsaction.h
diff --git a/tapasboard.svg b/tapasboard.svg
new file mode 100644
index 0000000..566acfa
--- /dev/null
+++ b/tapasboard.svg
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ height="44px"
+ version="1.1"
+ viewBox="0 0 44 44"
+ width="44px"
+ x="0px"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="qt.svg">
+ <metadata
+ id="metadata18">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs16">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 22 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="44 : 22 : 1"
+ inkscape:persp3d-origin="22 : 14.666667 : 1"
+ id="perspective2836" />
+ </defs>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1020"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="21.454545"
+ inkscape:cx="49.412871"
+ inkscape:cy="21.894358"
+ inkscape:window-x="-4"
+ inkscape:window-y="-4"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="g3" />
+ <g
+ transform="matrix(0.18308778,0,0,0.18308778,6.6100946,3.2385199)"
+ id="g3">
+ <path
+ d="M 43.09,0.3586 C 40.94,0.0036 38.84,-0.0824 36.81,0.0776 31.968136,0.39505671 27.122677,0.73638425 22.28,1.0696 9.62,2.0816 0,12.4996 0,26.8896 l 0,169.7 14.19,13.2 28.87,-209.42 0.03,-0.011 z"
+ style="fill:#006225"
+ id="path5"
+ sodipodi:nodetypes="cccccccc" />
+ <path
+ d="m 174.4,160 c 0,12.5 -7.75,24.07 -17.57,25.77 L 14.23,209.73 V 25.93 C 14.23,9.21 27.57,-2.27 43.12,0.3 l 131.3,21.52 v 138.2 z"
+ style="fill:#80c342"
+ id="path7" />
+ <path
+ d="m 154.9,80.96 -12.96,-0.598 0,0.278 6.945,0.32 6.016,0 z"
+ style="fill:#006225"
+ id="path11" />
+ <path
+ d="m 144.6,135.6 c 0.66,0.328 1.43,0.476 2.351,0.476 0.161,0 0.329,-0.004 0.497,-0.016 2.55,-0.148 5.32,-0.933 8.343,-2.308 h -6.015 c -1.821,0.832 -3.532,1.457 -5.176,1.848 z"
+ style="fill:#006225"
+ id="path13" />
+ <path
+ id="path17"
+ style="fill:#ffffff"
+ d="m 91.15,132.4 c 2.351,-6.051 3.511,-17.91 3.511,-35.62 0,-15.89 -1.148,-26.82 -3.484,-32.81 -2.336,-6.027 -5.832,-9.281 -10.52,-9.691 -0.359,-0.031 -0.714,-0.051 -1.058,-0.051 -4.34,0 -7.68,2.535 -10.01,7.625 -2.52,5.543 -3.793,17.04 -3.793,34.44 0,16.82 1.238,28.75 3.734,35.75 2.356,6.672 5.879,9.976 10.5,9.976 0.207,0 0.41,-0.008 0.621,-0.019 4.633,-0.293 8.121,-3.496 10.49,-9.602 m 17.98,3.75 c -4.117,9.707 -10.39,16.06 -18.99,19 0.867,4.449 2.176,7.441 3.922,9.019 1.351,1.211 3.433,1.821 6.222,1.821 0.805,0 1.668,-0.055 2.59,-0.157 v 13.12 l -5.961,0.782 c -1.758,0.23 -3.426,0.343 -5.004,0.343 -5.218,0 -9.445,-1.265 -12.62,-3.824 -4.207,-3.379 -7.308,-9.894 -9.297,-19.54 -9.136,-1.945 -16.26,-7.754 -21.19,-17.5 -5.004,-9.902 -7.551,-24.39 -7.551,-43.34 0,-20.43 3.484,-35.51 10.34,-45.07 5.789,-8.07 13.86,-12.04 24.02,-12.04 1.629,0 3.309,0.102 5.043,0.305 11.95,1.375 20.62,7.016 26.26,16.79 5.535,9.562 8.254,23.27 8.254,41.26 0,16.48 -2,29.45 -6.043,39.02 z M 130.4,45.91 l 11.52,1.238 0,20.21 12.96,0.914 0,12.68 -12.96,-0.598 0,46.33 c 0,4.032 0.445,6.625 1.34,7.789 0.8,1.067 2.046,1.594 3.71,1.594 0.161,0 0.329,-0.004 0.497,-0.016 2.55,-0.148 5.32,-0.933 8.343,-2.308 v 11.65 c -5.136,2.258 -10.18,3.598 -15.12,4.02 -0.718,0.055 -1.41,0.086 -2.078,0.086 -4.48,0 -7.906,-1.301 -10.25,-3.934 -2.73,-3.051 -4.09,-7.949 -4.09,-14.67 V 79.535 L 118.046,79.25 V 65.66 l 7.586,0.547 4.773,-20.3 z" />
+ <path
+ d="m 100.3,166 c 0.809,0 1.672,-0.055 2.59,-0.157 H 98.054 C 98.73,165.949 99.507,166 100.3,166 z"
+ style="fill:#006225"
+ id="path19" />
+ <path
+ id="path21"
+ style="fill:#006225"
+ d="m 84.85,63.98 c 2.336,5.997 3.484,16.92 3.484,32.81 0,17.7 -1.16,29.57 -3.512,35.62 -1.894,4.879 -4.527,7.902 -7.863,9.07 0.965,0.368 1.992,0.551 3.078,0.551 0.207,0 0.41,-0.008 0.621,-0.019 4.633,-0.293 8.121,-3.496 10.49,-9.602 2.351,-6.051 3.511,-17.91 3.511,-35.62 0,-15.89 -1.148,-26.82 -3.484,-32.81 -2.336,-6.027 -5.832,-9.281 -10.52,-9.691 -0.359,-0.031 -0.714,-0.051 -1.058,-0.051 -1.09,0 -2.117,0.16 -3.082,0.481 h -0.004 c 3.601,1.121 6.379,4.215 8.336,9.261 z m -2.344,114.3 c -0.113,-0.05 -0.227,-0.105 -0.336,-0.16 -0.012,-0.004 -0.023,-0.012 -0.035,-0.015 -0.102,-0.051 -0.207,-0.106 -0.309,-0.157 -0.019,-0.011 -0.039,-0.019 -0.058,-0.031 -0.09,-0.051 -0.184,-0.098 -0.278,-0.148 -0.027,-0.016 -0.054,-0.036 -0.086,-0.051 -0.082,-0.043 -0.164,-0.09 -0.242,-0.137 -0.039,-0.023 -0.078,-0.047 -0.113,-0.07 -0.07,-0.039 -0.145,-0.082 -0.215,-0.125 -0.047,-0.031 -0.094,-0.059 -0.14,-0.09 -0.059,-0.039 -0.118,-0.074 -0.176,-0.113 -0.059,-0.039 -0.114,-0.075 -0.168,-0.114 -0.051,-0.031 -0.102,-0.066 -0.149,-0.097 -0.066,-0.047 -0.132,-0.094 -0.195,-0.137 -0.039,-0.027 -0.078,-0.055 -0.113,-0.082 -0.078,-0.055 -0.153,-0.113 -0.231,-0.172 -0.023,-0.016 -0.05,-0.035 -0.078,-0.055 -0.098,-0.078 -0.199,-0.156 -0.297,-0.234 -4.207,-3.379 -7.308,-9.894 -9.297,-19.54 -9.136,-1.945 -16.26,-7.754 -21.19,-17.5 -5.004,-9.902 -7.551,-24.39 -7.551,-43.34 0,-20.43 3.484,-35.51 10.34,-45.07 5.789,-8.07 13.86,-12.04 24.02,-12.04 h -6.351 c -10.15,0.008 -18.22,3.977 -24,12.04 -6.855,9.563 -10.34,24.64 -10.34,45.07 0,18.95 2.547,33.44 7.551,43.34 4.934,9.75 12.05,15.56 21.19,17.5 1.989,9.641 5.09,16.16 9.297,19.54 3.176,2.559 7.403,3.824 12.62,3.824 0.098,0 0.199,0 0.297,-0.004 h 5.539 c -3.406,-0.05 -6.383,-0.66 -8.906,-1.828 L 82.498,178.28 z M 128.4,145.6 c -2.73,-3.051 -4.09,-7.949 -4.09,-14.67 V 79.57 l -6.226,-0.285 v -13.59 h -6.016 v 3.035 c 0.871,3.273 1.555,6.82 2.063,10.64 l 4.164,0.192 v 51.36 c 0,6.723 1.367,11.62 4.09,14.67 2.343,2.633 5.765,3.934 10.25,3.934 h 6.015 c -4.48,0 -7.906,-1.301 -10.25,-3.934 z m 2.043,-99.66 -6.016,0 -4.668,19.88 5.911,0.422 4.773,-20.3 z" />
+ </g>
+</svg>
diff --git a/tapasboard64.png b/tapasboard64.png
new file mode 100644
index 0000000..707d5c4
--- /dev/null
+++ b/tapasboard64.png
Binary files differ
diff --git a/tapasboard80.png b/tapasboard80.png
new file mode 100644
index 0000000..6ad8096
--- /dev/null
+++ b/tapasboard80.png
Binary files differ
diff --git a/tapasboard_harmattan.desktop b/tapasboard_harmattan.desktop
new file mode 100644
index 0000000..09f8494
--- /dev/null
+++ b/tapasboard_harmattan.desktop
@@ -0,0 +1,11 @@
+[Desktop Entry]
+Encoding=UTF-8
+Version=1.0
+Type=Application
+Terminal=false
+Name=tapasboard
+Exec=/usr/bin/invoker --type=d -s /opt/tapasboard/bin/tapasboard
+Icon=/usr/share/icons/hicolor/80x80/apps/tapasboard80.png
+X-Window-Icon=
+X-HildonDesk-ShowInToolbar=true
+X-Osso-Type=application/x-executable
diff --git a/xmlrpcinterface.cpp b/xmlrpcinterface.cpp
new file mode 100644
index 0000000..bf61e12
--- /dev/null
+++ b/xmlrpcinterface.cpp
@@ -0,0 +1,99 @@
+#include <QtCore/QDateTime>
+#include <QtCore/QDebug>
+#include <QtNetwork/QNetworkRequest>
+
+#include "xmlrpcinterface.h"
+
+XmlRpcInterface::XmlRpcInterface(const QUrl& endpoint, QObject *parent) :
+ QObject(parent), _endpoint(endpoint),
+ _manager(new QNetworkAccessManager(this))
+{
+}
+
+XmlRpcPendingCall *XmlRpcInterface::asyncCallWithArgumentList(const QString &method,
+ const QList<QVariant> &args)
+{
+ QNetworkRequest request(_endpoint);
+ QByteArray data = encodeCall(method, args);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "text/xml");
+ request.setRawHeader("User-Agent", "Tapasboard/1.0");
+ QNetworkReply *reply = _manager->post(request, data);
+ return new XmlRpcPendingCall(reply, this);
+}
+
+QByteArray XmlRpcInterface::encodeCall(const QString &method, const QList<QVariant> &args)
+{
+ QByteArray buffer;
+ QXmlStreamWriter writer(&buffer);
+ writer.writeStartDocument();
+
+ writer.writeStartElement("methodCall");
+
+ writer.writeTextElement("methodName", method);
+
+ if (!args.isEmpty()) {
+ writer.writeStartElement("params");
+ foreach (const QVariant& arg, args) {
+ writer.writeStartElement("param");
+ encodeValue(&writer, arg);
+ writer.writeEndElement(); // param
+ }
+ writer.writeEndElement(); // params
+ }
+
+ writer.writeEndElement(); // methodCall
+
+ writer.writeEndDocument();
+ return buffer;
+}
+
+void XmlRpcInterface::encodeValue(QXmlStreamWriter *w, const QVariant &value)
+{
+ w->writeStartElement("value");
+ switch (value.type()) {
+ case QVariant::String:
+ w->writeTextElement("string", value.toString());
+ break;
+ case QVariant::Int:
+ w->writeTextElement("int", value.toString());
+ break;
+ case QVariant::Bool:
+ w->writeTextElement("boolean", value.toBool() ? "1" : "0");
+ break;
+ case QVariant::Double:
+ w->writeTextElement("double", value.toString());
+ break;
+ case QVariant::DateTime:
+ w->writeTextElement("dateTime.iso8601", value.toDateTime().toString(Qt::ISODate));
+ break;
+ case QVariant::List:
+ w->writeStartElement("array");
+ w->writeStartElement("data");
+ {
+ QVariantList list = value.toList();
+ foreach (const QVariant& v, list) {
+ encodeValue(w, v);
+ }
+ }
+ w->writeEndElement(); // data
+ w->writeEndElement(); // array
+ break;
+ case QVariant::Map:
+ w->writeStartElement("struct");
+ {
+ QVariantMap map = value.toMap();
+ for (QVariantMap::Iterator i = map.begin(); i != map.end(); i++) {
+ w->writeStartElement("member");
+ w->writeTextElement("name", i.key());
+ encodeValue(w, i.value());
+ w->writeEndElement(); // member
+ }
+ }
+ w->writeEndElement(); // struct
+ break;
+ default:
+ qWarning() << "Unhandled value type:" << value.typeName();
+ break;
+ }
+ w->writeEndElement(); // value
+}
diff --git a/xmlrpcinterface.h b/xmlrpcinterface.h
new file mode 100644
index 0000000..1fce4f1
--- /dev/null
+++ b/xmlrpcinterface.h
@@ -0,0 +1,70 @@
+#ifndef XMLRPCINTERFACE_H
+#define XMLRPCINTERFACE_H
+
+#include <QtCore/QObject>
+#include <QtCore/QVariant>
+#include <QtCore/QUrl>
+#include <QtCore/QXmlStreamWriter>
+#include <QtNetwork/QNetworkAccessManager>
+
+#include "xmlrpcpendingcall.h"
+
+class XmlRpcInterface : public QObject
+{
+ Q_OBJECT
+public:
+ explicit XmlRpcInterface(const QUrl& endpoint, QObject *parent = 0);
+
+ bool isAccessible() const;
+
+ XmlRpcPendingCall *asyncCall(const QString& method,
+ const QVariant &arg1 = QVariant(),
+ const QVariant &arg2 = QVariant(),
+ const QVariant &arg3 = QVariant(),
+ const QVariant &arg4 = QVariant(),
+ const QVariant &arg5 = QVariant(),
+ const QVariant &arg6 = QVariant(),
+ const QVariant &arg7 = QVariant(),
+ const QVariant &arg8 = QVariant());
+
+ XmlRpcPendingCall *asyncCallWithArgumentList(const QString& method,
+ const QList<QVariant>& args);
+
+private:
+ static QByteArray encodeCall(const QString& method, const QList<QVariant>& args);
+ static void encodeValue(QXmlStreamWriter* w, const QVariant& value);
+
+private:
+ QUrl _endpoint;
+ QNetworkAccessManager *_manager;
+
+};
+
+inline bool XmlRpcInterface::isAccessible() const
+{
+ return _manager->networkAccessible() != QNetworkAccessManager::NotAccessible;
+}
+
+inline XmlRpcPendingCall *XmlRpcInterface::asyncCall(const QString &method,
+ const QVariant &arg1,
+ const QVariant &arg2,
+ const QVariant &arg3,
+ const QVariant &arg4,
+ const QVariant &arg5,
+ const QVariant &arg6,
+ const QVariant &arg7,
+ const QVariant &arg8)
+{
+ QList<QVariant> args;
+ if (arg1.isValid()) args << arg1;
+ if (arg2.isValid()) args << arg2;
+ if (arg3.isValid()) args << arg3;
+ if (arg4.isValid()) args << arg4;
+ if (arg5.isValid()) args << arg5;
+ if (arg6.isValid()) args << arg6;
+ if (arg7.isValid()) args << arg7;
+ if (arg8.isValid()) args << arg8;
+ return asyncCallWithArgumentList(method, args);
+}
+
+#endif // XMLRPCINTERFACE_H
diff --git a/xmlrpcpendingcall.cpp b/xmlrpcpendingcall.cpp
new file mode 100644
index 0000000..d94b76d
--- /dev/null
+++ b/xmlrpcpendingcall.cpp
@@ -0,0 +1,187 @@
+#include <QtCore/QDateTime>
+#include <QtCore/QEventLoop>
+#include <QtCore/QDebug>
+
+#include "xmlrpcinterface.h"
+#include "xmlrpcpendingcall.h"
+
+XmlRpcPendingCall::XmlRpcPendingCall(QNetworkReply *reply, XmlRpcInterface *parent)
+ : QObject(parent), _reply(reply), _state(StateWaitingReply)
+{
+ connect(_reply, SIGNAL(finished()), SLOT(handleRequestFinished()));
+ _reply->setParent(this);
+}
+
+void XmlRpcPendingCall::waitForFinished()
+{
+ if (_state == StateWaitingReply) {
+ QEventLoop loop;
+ connect(this, SIGNAL(finished(XmlRpcPendingCall*)), &loop, SLOT(quit()));
+ loop.exec();
+ }
+}
+
+bool XmlRpcPendingCall::decodeMethodResponse(QXmlStreamReader *r)
+{
+ if (r->readNextStartElement()) {
+ if (r->name() == "fault") {
+ _state = StateFaultReceived;
+ if (r->readNextStartElement()) {
+ if (r->name() != "value") return false;
+ _value = decodeValue(r);
+ if (!_value.isValid()) return false;
+ }
+ return true;
+ } else if (r->name() == "params") {
+ _state = StateReplyReceived;
+ if (r->readNextStartElement()) {
+ if (r->name() != "param") return false;
+ if (r->readNextStartElement()) {
+ if (r->name() != "value") return false;
+ _value = decodeValue(r);
+ if (!_value.isValid()) return false;
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ return false;
+}
+
+QVariant XmlRpcPendingCall::decodeValue(QXmlStreamReader *r)
+{
+ Q_ASSERT(r->isStartElement() && r->name() == "value");
+ QVariant value;
+
+ if (r->readNextStartElement()) {
+ if (r->name() == "string") {
+ value = QVariant::fromValue(r->readElementText());
+ } else if (r->name() == "int") {
+ bool ok;
+ value = QVariant::fromValue(r->readElementText().toInt(&ok));
+ if (!ok) value.clear();
+ } else if (r->name() == "boolean") {
+ bool ok;
+ value = QVariant::fromValue<bool>(r->readElementText().toInt(&ok));
+ if (!ok) value.clear();
+ } else if (r->name() == "double") {
+ bool ok;
+ value = QVariant::fromValue(r->readElementText().toDouble(&ok));
+ if (!ok) value.clear();
+ } else if (r->name() == "dateTime.iso8601") {
+ QString text = r->readElementText();
+ QDateTime dateTime = QDateTime::fromString(text, Qt::ISODate);
+ if (!dateTime.isValid()) {
+ // Qt seems not be happy without dashes
+ text.insert(4, '-');
+ text.insert(7, '-');
+ dateTime = QDateTime::fromString(text, Qt::ISODate);
+ if (!dateTime.isValid()) {
+ qWarning() << "Invalid dateTime format" << text;
+ return QVariant();
+ }
+ }
+ value = QVariant::fromValue(dateTime);
+ } else if (r->name() == "base64") {
+ QByteArray data = r->readElementText().toAscii();
+ value = QVariant::fromValue(QByteArray::fromBase64(data));
+ } else if (r->name() == "array") {
+ QList<QVariant> list;
+ if (!r->readNextStartElement() || r->name() != "data") {
+ qWarning() << "Unexpected element inside <array>:" << r->name();
+ return QVariant();
+ }
+ while (r->readNextStartElement()) {
+ if (r->name() != "value") return QVariant();
+ QVariant value = decodeValue(r);
+ if (!value.isValid()) return QVariant();
+ list.append(value);
+ }
+ if (r->readNextStartElement()) {
+ // No other elements
+ qWarning() << "Unexpected element inside <array>:" << r->name();
+ return QVariant();
+ }
+ value = QVariant::fromValue(list);
+ } else if (r->name() == "struct") {
+ QMap<QString, QVariant> map;
+ while (r->readNextStartElement()) {
+ if (r->name() != "member") return QVariant();
+ QString name;
+ if (r->readNextStartElement() && r->name() == "name") {
+ name = r->readElementText();
+ } else {
+ qWarning() << "Malformed struct";
+ return QVariant();
+ }
+ if (r->readNextStartElement() && r->name() == "value") {
+ QVariant value = decodeValue(r);
+ if (!value.isValid()) return QVariant();
+ map.insert(name, value);
+ } else {
+ qWarning() << "Malformed struct";
+ return QVariant();
+ }
+ if (r->readNextStartElement()) {
+ // No other elements
+ qWarning() << "Unexpected element inside <member>" << r->name();
+ return false;
+ }
+ }
+ value = QVariant::fromValue(map);
+ } else {
+ qWarning() << "Unknown value type:" << r->name();
+ }
+ }
+
+ if (r->readNextStartElement()) {
+ // There is more than one element inside this <value>
+ qWarning() << "More than element inside <value>";
+ return QVariant();
+ } else if (r->isEndElement() && r->name() == "value") {
+ // Everything OK
+ return value;
+ } else {
+ qWarning() << "Expected </value> instead of" << r->name();
+ return QVariant();
+ }
+}
+
+void XmlRpcPendingCall::handleRequestFinished()
+{
+ Q_ASSERT(_state == StateWaitingReply);
+
+ QNetworkReply::NetworkError error = _reply->error();
+ if (error == QNetworkReply::NoError) {
+ QByteArray data = _reply->readAll();
+ QXmlStreamReader reader(data);
+ bool parse_ok = false;
+ if (reader.readNextStartElement()) {
+ if (reader.name() == "methodResponse") {
+ parse_ok = decodeMethodResponse(&reader);
+ }
+ }
+ if (parse_ok) {
+ Q_ASSERT(_state == StateReplyReceived || _state == StateFaultReceived);
+ } else {
+ qWarning() << "Parse error!";
+ QVariantMap obj;
+ obj.insert("code", QVariant(reader.error()));
+ obj.insert("message", QVariant(reader.errorString()));
+ _value = obj;
+ _state = StateParseError;
+ }
+ } else {
+ qWarning() << "Network error!" << error;
+ QVariantMap obj;
+ obj.insert("code", QVariant(error));
+ obj.insert("message", QVariant(_reply->errorString()));
+ _value = obj;
+ _state = StateNetworkError;
+ }
+
+ emit finished(this);
+}
diff --git a/xmlrpcpendingcall.h b/xmlrpcpendingcall.h
new file mode 100644
index 0000000..620ddc4
--- /dev/null
+++ b/xmlrpcpendingcall.h
@@ -0,0 +1,69 @@
+#ifndef XMLRPCPENDINGCALL_H
+#define XMLRPCPENDINGCALL_H
+
+#include <QtCore/QObject>
+#include <QtCore/QXmlStreamReader>
+#include <QtNetwork/QNetworkReply>
+
+class XmlRpcInterface;
+
+class XmlRpcPendingCall : public QObject
+{
+ Q_OBJECT
+ friend class XmlRpcInterface;
+protected:
+ explicit XmlRpcPendingCall(QNetworkReply *reply, XmlRpcInterface *parent);
+
+public:
+ bool isError() const;
+ bool isFinished() const;
+ bool isValid() const;
+
+ QVariant value() const;
+
+public slots:
+ void waitForFinished();
+
+signals:
+ void finished(XmlRpcPendingCall *self);
+
+private:
+ bool decodeMethodResponse(QXmlStreamReader* r);
+ static QVariant decodeValue(QXmlStreamReader* r);
+
+private slots:
+ void handleRequestFinished();
+
+private:
+ QNetworkReply *_reply;
+ QVariant _value;
+ enum State {
+ StateWaitingReply,
+ StateNetworkError,
+ StateParseError,
+ StateReplyReceived,
+ StateFaultReceived
+ } _state;
+};
+
+inline bool XmlRpcPendingCall::isError() const
+{
+ return _state != StateWaitingReply && _state != StateReplyReceived;
+}
+
+inline bool XmlRpcPendingCall::isFinished() const
+{
+ return _state != StateWaitingReply;
+}
+
+inline bool XmlRpcPendingCall::isValid() const
+{
+ return _state == StateReplyReceived;
+}
+
+inline QVariant XmlRpcPendingCall::value() const
+{
+ return _value;
+}
+
+#endif // XMLRPCPENDINGCALL_H
diff --git a/xmlrpcreply.h b/xmlrpcreply.h
new file mode 100644
index 0000000..58b2bca
--- /dev/null
+++ b/xmlrpcreply.h
@@ -0,0 +1,47 @@
+#ifndef XMLRPCREPLY_H
+#define XMLRPCREPLY_H
+
+#include "xmlrpcpendingcall.h"
+
+template <typename T>
+class XmlRpcReply
+{
+public:
+ XmlRpcReply(XmlRpcPendingCall *call);
+
+ bool isValid() const;
+
+ operator T () const;
+
+private:
+ bool _valid;
+ T _value;
+};
+
+template <typename T>
+XmlRpcReply<T>::XmlRpcReply(XmlRpcPendingCall *call)
+ : _valid(false)
+{
+ call->waitForFinished();
+ if (call->isValid()) {
+ QVariant v = call->value();
+ if (v.canConvert<T>()) {
+ _value = v.value<T>();
+ _valid = true;
+ }
+ }
+}
+
+template <typename T>
+inline bool XmlRpcReply<T>::isValid() const
+{
+ return _valid;
+}
+
+template <typename T>
+inline XmlRpcReply<T>::operator T () const
+{
+ return _value;
+}
+
+#endif // XMLRPCREPLY_H