diff options
author | Javier S. Pedro <maemo@javispedro.com> | 2013-04-01 15:04:58 +0200 |
---|---|---|
committer | Javier S. Pedro <maemo@javispedro.com> | 2013-04-01 15:04:58 +0200 |
commit | 5ef8b38e55c1883224fe1f01f47aba45b7b42666 (patch) | |
tree | 67a873c6a7c5263d202793314c3b3a61543fbb40 | |
download | tapasboard-5ef8b38e55c1883224fe1f01f47aba45b7b42666.tar.gz tapasboard-5ef8b38e55c1883224fe1f01f47aba45b7b42666.zip |
initial import
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); +} @@ -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 Binary files differnew file mode 100644 index 0000000..707d5c4 --- /dev/null +++ b/tapasboard64.png diff --git a/tapasboard80.png b/tapasboard80.png Binary files differnew file mode 100644 index 0000000..6ad8096 --- /dev/null +++ b/tapasboard80.png 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 |