From 5ef8b38e55c1883224fe1f01f47aba45b7b42666 Mon Sep 17 00:00:00 2001 From: "Javier S. Pedro" Date: Mon, 1 Apr 2013 15:04:58 +0200 Subject: initial import --- action.cpp | 18 ++ action.h | 32 +++ board.cpp | 220 ++++++++++++++++++++ board.h | 75 +++++++ boardmanager.cpp | 18 ++ boardmanager.h | 27 +++ boardmodel.cpp | 141 +++++++++++++ boardmodel.h | 55 +++++ fetchboardconfigaction.cpp | 50 +++++ fetchboardconfigaction.h | 23 +++ fetchforumsaction.cpp | 107 ++++++++++ fetchforumsaction.h | 29 +++ fetchtopicsaction.cpp | 88 ++++++++ fetchtopicsaction.h | 32 +++ forummodel.cpp | 281 ++++++++++++++++++++++++++ forummodel.h | 72 +++++++ global.h | 23 +++ main.cpp | 37 ++++ qml/tapasboard/BoardPage.qml | 57 ++++++ qml/tapasboard/ForumPage.qml | 43 ++++ qml/tapasboard/GroupHeader.qml | 26 +++ qml/tapasboard/MainPage.qml | 17 ++ qml/tapasboard/main.qml | 30 +++ qmlapplicationviewer/qmlapplicationviewer.cpp | 174 ++++++++++++++++ qmlapplicationviewer/qmlapplicationviewer.h | 46 +++++ qmlapplicationviewer/qmlapplicationviewer.pri | 148 ++++++++++++++ qtc_packaging/debian_harmattan/README | 6 + qtc_packaging/debian_harmattan/changelog | 5 + qtc_packaging/debian_harmattan/compat | 1 + qtc_packaging/debian_harmattan/control | 15 ++ qtc_packaging/debian_harmattan/copyright | 40 ++++ qtc_packaging/debian_harmattan/manifest.aegis | 70 +++++++ qtc_packaging/debian_harmattan/rules | 91 +++++++++ tapasboard.desktop | 11 + tapasboard.pro | 72 +++++++ tapasboard.svg | 93 +++++++++ tapasboard64.png | Bin 0 -> 3400 bytes tapasboard80.png | Bin 0 -> 4945 bytes tapasboard_harmattan.desktop | 11 + xmlrpcinterface.cpp | 99 +++++++++ xmlrpcinterface.h | 70 +++++++ xmlrpcpendingcall.cpp | 187 +++++++++++++++++ xmlrpcpendingcall.h | 69 +++++++ xmlrpcreply.h | 47 +++++ 44 files changed, 2756 insertions(+) create mode 100644 action.cpp create mode 100644 action.h create mode 100644 board.cpp create mode 100644 board.h create mode 100644 boardmanager.cpp create mode 100644 boardmanager.h create mode 100644 boardmodel.cpp create mode 100644 boardmodel.h create mode 100644 fetchboardconfigaction.cpp create mode 100644 fetchboardconfigaction.h create mode 100644 fetchforumsaction.cpp create mode 100644 fetchforumsaction.h create mode 100644 fetchtopicsaction.cpp create mode 100644 fetchtopicsaction.h create mode 100644 forummodel.cpp create mode 100644 forummodel.h create mode 100644 global.h create mode 100644 main.cpp create mode 100644 qml/tapasboard/BoardPage.qml create mode 100644 qml/tapasboard/ForumPage.qml create mode 100644 qml/tapasboard/GroupHeader.qml create mode 100644 qml/tapasboard/MainPage.qml create mode 100644 qml/tapasboard/main.qml create mode 100644 qmlapplicationviewer/qmlapplicationviewer.cpp create mode 100644 qmlapplicationviewer/qmlapplicationviewer.h create mode 100644 qmlapplicationviewer/qmlapplicationviewer.pri create mode 100644 qtc_packaging/debian_harmattan/README create mode 100644 qtc_packaging/debian_harmattan/changelog create mode 100644 qtc_packaging/debian_harmattan/compat create mode 100644 qtc_packaging/debian_harmattan/control create mode 100644 qtc_packaging/debian_harmattan/copyright create mode 100644 qtc_packaging/debian_harmattan/manifest.aegis create mode 100755 qtc_packaging/debian_harmattan/rules create mode 100644 tapasboard.desktop create mode 100644 tapasboard.pro create mode 100644 tapasboard.svg create mode 100644 tapasboard64.png create mode 100644 tapasboard80.png create mode 100644 tapasboard_harmattan.desktop create mode 100644 xmlrpcinterface.cpp create mode 100644 xmlrpcinterface.h create mode 100644 xmlrpcpendingcall.cpp create mode 100644 xmlrpcpendingcall.h create mode 100644 xmlrpcreply.h 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 +#include +#include + +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 +#include +#include +#include +#include +#include + +#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 +#include +#include + +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 _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::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 +#include + +class Board; + +class BoardManager : public QObject +{ + Q_OBJECT +public: + explicit BoardManager(QObject *parent = 0); + + + Board *getBoard(const QString& url); + +signals: + +public slots: + +private: + QHash _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 +#include + +#include "global.h" +#include "board.h" +#include "boardmodel.h" + +BoardModel::BoardModel(QObject *parent) : + QAbstractListModel(parent), + _rootForumId(0) +{ + QHash 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 +#include + +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 +#include +#include + +#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 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 +#include +#include +#include + +#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 result(_call); + if (result.isValid()) { + int order = 0; + QList 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 FetchForumsAction::flattenForumList(const QVariantList &list, int *order) +{ + QList 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 +#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 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 +#include +#include +#include + +#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 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 +#include +#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 +#include + +#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 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 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 &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::loadTopics(int start, int end) +{ + Q_ASSERT(_board); + const int rows = end - start + 1; + QList 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 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 +#include +#include + +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& topics); + QDateTime lastTopPostUpdate(); + QList 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 _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 +#include +#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 app(createApplication(argc, argv)); + QScopedPointer 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("com.javispedro.tapasboard", 1, 0, "BoardModel"); + qmlRegisterType("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 +#include +#include +#include +#include +#include + +#include // MEEGO_EDITION_HARMATTAN + +#ifdef HARMATTAN_BOOSTER +#include +#endif + +#if defined(QMLJSDEBUGGER) && QT_VERSION < 0x040800 + +#include + +#if !defined(NO_JSDEBUGGER) +#include +#endif +#if !defined(NO_QMLOBSERVER) +#include +#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(128); + break; + case ScreenOrientationLockLandscape: + attribute = static_cast(129); + break; + default: + case ScreenOrientationAuto: + attribute = static_cast(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 + +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 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 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 +Build-Depends: debhelper (>= 5), libqt4-dev +Standards-Version: 3.7.3 +Homepage: + +Package: tapasboard +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: + +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 on +Fri, 29 Mar 2013 14:59:58 +0100. + +It was downloaded from + +Upstream Author(s): + + + + +Copyright: + + + + +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 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/tapasboard64.png b/tapasboard64.png new file mode 100644 index 0000000..707d5c4 Binary files /dev/null and b/tapasboard64.png differ diff --git a/tapasboard80.png b/tapasboard80.png new file mode 100644 index 0000000..6ad8096 Binary files /dev/null and b/tapasboard80.png 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 +#include +#include + +#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 &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 &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 +#include +#include +#include +#include + +#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& args); + +private: + static QByteArray encodeCall(const QString& method, const QList& 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 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 +#include +#include + +#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(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 list; + if (!r->readNextStartElement() || r->name() != "data") { + qWarning() << "Unexpected element inside :" << 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 :" << r->name(); + return QVariant(); + } + value = QVariant::fromValue(list); + } else if (r->name() == "struct") { + QMap 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 " << 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 + qWarning() << "More than element inside "; + return QVariant(); + } else if (r->isEndElement() && r->name() == "value") { + // Everything OK + return value; + } else { + qWarning() << "Expected 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 +#include +#include + +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 +class XmlRpcReply +{ +public: + XmlRpcReply(XmlRpcPendingCall *call); + + bool isValid() const; + + operator T () const; + +private: + bool _valid; + T _value; +}; + +template +XmlRpcReply::XmlRpcReply(XmlRpcPendingCall *call) + : _valid(false) +{ + call->waitForFinished(); + if (call->isValid()) { + QVariant v = call->value(); + if (v.canConvert()) { + _value = v.value(); + _valid = true; + } + } +} + +template +inline bool XmlRpcReply::isValid() const +{ + return _valid; +} + +template +inline XmlRpcReply::operator T () const +{ + return _value; +} + +#endif // XMLRPCREPLY_H -- cgit v1.2.3