diff options
-rw-r--r-- | board.cpp | 96 | ||||
-rw-r--r-- | board.h | 18 | ||||
-rw-r--r-- | fetchboardconfigaction.cpp | 10 | ||||
-rw-r--r-- | fetchpostsaction.cpp | 7 | ||||
-rw-r--r-- | fetchpostsaction.h | 1 | ||||
-rw-r--r-- | fetchtopicsaction.cpp | 5 | ||||
-rw-r--r-- | main.cpp | 18 | ||||
-rw-r--r-- | qml/tapasboard/TopicPage.qml | 31 | ||||
-rw-r--r-- | topicmodel.cpp | 38 | ||||
-rw-r--r-- | topicmodel.h | 11 |
10 files changed, 190 insertions, 45 deletions
@@ -1,5 +1,4 @@ #include <QtCore/QRegExp> -#include <QtCore/QDateTime> #include <QtCore/QDir> #include <QtCore/QDebug> #include <QtSql/QSqlQuery> @@ -12,6 +11,8 @@ #include "xmlrpcinterface.h" #include "board.h" +const QLatin1String Board::CURRENT_DB_VERSION("testing2"); + Board::Board(const QString& forumUrl, QObject *parent) : QObject(parent), _url(forumUrl), _slug(createSlug(forumUrl)), _db(QSqlDatabase::addDatabase("QSQLITE", _slug)), @@ -22,10 +23,21 @@ Board::Board(const QString& forumUrl, QObject *parent) : if (!_db.open()) { qWarning() << "Could not open database file" << _db.databaseName() << ":" << _db.lastError().text(); + _db.setDatabaseName(QDir::toNativeSeparators(getTempDbPathFor(_slug))); + if (!_db.open()) { + qWarning() << "Could not open temp database file" + << _db.databaseName() << ":" << _db.lastError().text(); + return; // Give up + } + } + if (!checkCompatibleDb()) { + qDebug() << "Database version incompatible, reinitializing"; + eraseDb(); + initializeDb(); } - initializeDb(); fetchConfigIfOutdated(); fetchForumsIfOutdated(); + initializeBbCode(); // TODO This might depend on board config } Board::~Board() @@ -85,17 +97,50 @@ void Board::setConfig(const QString &key, const QString &value) notifyConfigChanged(); } -int Board::rootForumId() const +QString Board::removeHtml(QString text) const { - QSqlQuery query(_db); - query.exec("SELECT forum_id FROM forums WHERE parent_id = -1"); - if (query.next()) { - return query.value(0).toInt(); + static const QRegExp regexp("<[a-zA-Z\\/][^>]*>"); + text.replace(regexp, ""); + return text; +} + +QString Board::removeBbcode(QString text) const +{ + static const QRegExp regexp("\\[[a-zA-Z\\/][^]]*\\]"); + text.replace(regexp, ""); + return text; +} + +QString Board::bbcodeToRichText(QString text) const +{ + typedef QPair<QRegExp, QString> Pair; // Workaround for ',' in Q_FOREACH + foreach (const Pair& pair, _bbcodes) { + text.replace(pair.first, pair.second); + } + + return text; +} + +QString Board::renderHumanDate(const QDateTime &dateTime) +{ + QDate date = dateTime.toLocalTime().date(); + QDate today = QDate::currentDate(); + if (date == today) { + return tr("Today"); + } else if (date.daysTo(today) == 1) { + return tr("Yesterday"); + } else if (date.daysTo(today) < 5) { + return QDate::longDayName(date.dayOfWeek(), QDate::StandaloneFormat); } else { - return -1; + return date.toString(Qt::DefaultLocaleShortDate); } } +QString Board::renderHumanTime(const QDateTime &dateTime) +{ + return dateTime.toLocalTime().time().toString(Qt::DefaultLocaleShortDate); +} + void Board::notifyConfigChanged() { emit configChanged(); @@ -150,6 +195,17 @@ QString Board::getDbPathFor(const QString &slug) return getDbDir() + "/" + slug + ".sqlite"; } +QString Board::getTempDbPathFor(const QString& slug) +{ + return QDir::tempPath() + "/" + slug + ".sqlite"; +} + +bool Board::checkCompatibleDb() +{ + QString version = getConfig("tapasboard_db_version"); + return version == CURRENT_DB_VERSION; +} + bool Board::initializeDb() { QSqlQuery q(_db); @@ -158,16 +214,20 @@ bool Board::initializeDb() 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)")) { + 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)")) { 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(); + qWarning() << "Could not create forums_parent index:" << q.lastError().text(); + return false; + } + if (!q.exec("CREATE UNIQUE INDEX IF NOT EXISTS forums_order ON forums (sort_index ASC)")) { + qWarning() << "Could not create forums_order index:" << 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)")) { + 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, reply_number INT, new_post BOOL, last_update_time TEXT)")) { qWarning() << "Could not create topics table:" << q.lastError().text(); return false; } @@ -258,6 +318,20 @@ void Board::executeActionFromQueue() } } +void Board::initializeBbCode() +{ + _bbcodes.clear(); + _bbcodes << qMakePair(QRegExp("\\[(/?[bius])\\]", Qt::CaseInsensitive), QString("<\\1>")); + + _bbcodes << qMakePair(QRegExp("\\[(/?)quote\\]", Qt::CaseInsensitive), QString("<\\1blockquote>")); + + _bbcodes << qMakePair(QRegExp("\\[url\\]([^[]*)\\[/url\\]", Qt::CaseInsensitive), QString("<a href=\"\\1\">\\1</a>")); + _bbcodes << qMakePair(QRegExp("\\[url=([^]]*)\\]", Qt::CaseInsensitive), QString("<a href=\"\\1\">")); + _bbcodes << qMakePair(QRegExp("\\[/url\\]", Qt::CaseInsensitive), QString("</a>")); + + _bbcodes << qMakePair(QRegExp("\n"), QString("<br>")); +} + void Board::fetchConfigIfOutdated() { if (_iface->isAccessible()) { @@ -1,7 +1,9 @@ #ifndef BOARD_H #define BOARD_H +#include <QtCore/QDateTime> #include <QtCore/QObject> +#include <QtCore/QPair> #include <QtCore/QQueue> #include <QtSql/QSqlDatabase> @@ -15,17 +17,27 @@ public: explicit Board(const QString& boardUrl, QObject *parent = 0); ~Board(); + static const QLatin1String CURRENT_DB_VERSION; + bool busy() const; void enqueueAction(Action* action); QSqlDatabase database(); XmlRpcInterface *service(); + // Configuration table QString getConfig(const QString& key) const; void setConfig(const QString& key, const QString &value); - int rootForumId() const; + // Some helper functions + QString removeHtml(QString text) const; + QString removeBbcode(QString text) const; + QString bbcodeToRichText(QString text) const; + + QString renderHumanDate(const QDateTime& dateTime); + QString renderHumanTime(const QDateTime& dateTime); + // These functions wrap emitting the signals below void notifyConfigChanged(); void notifyForumsChanged(); void notifyForumTopicsChanged(int forumId, int start, int end); @@ -42,11 +54,14 @@ private: static QString createSlug(const QString& forumUrl); static QString getDbDir(); static QString getDbPathFor(const QString& slug); + static QString getTempDbPathFor(const QString& slug); + bool checkCompatibleDb(); bool initializeDb(); bool eraseDb(); bool cleanDb(); bool removeFromActionQueue(Action *action); void executeActionFromQueue(); + void initializeBbCode(); void fetchConfigIfOutdated(); void fetchForumsIfOutdated(); @@ -60,6 +75,7 @@ private: QSqlDatabase _db; XmlRpcInterface *_iface; QQueue<Action*> _queue; + QList< QPair<QRegExp, QString> > _bbcodes; }; inline bool Board::busy() const diff --git a/fetchboardconfigaction.cpp b/fetchboardconfigaction.cpp index 0dd5e4c..2364dd1 100644 --- a/fetchboardconfigaction.cpp +++ b/fetchboardconfigaction.cpp @@ -32,6 +32,11 @@ void FetchBoardConfigAction::handleFinishedCall() QSqlDatabase db = _board->database(); db.transaction(); QSqlQuery query(db); + + // Let's add some of our config settings + map["last_config_fetch"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + map["tapasboard_db_version"] = Board::CURRENT_DB_VERSION; + 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()); @@ -40,11 +45,6 @@ void FetchBoardConfigAction::handleFinishedCall() 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 { diff --git a/fetchpostsaction.cpp b/fetchpostsaction.cpp index 16802fa..426f766 100644 --- a/fetchpostsaction.cpp +++ b/fetchpostsaction.cpp @@ -63,7 +63,7 @@ void FetchPostsAction::handleFinishedCall() query.bindValue(":topic_id", topic_id); query.bindValue(":post_id", post_id); query.bindValue(":post_title", unencodePostText(post["post_title"])); - query.bindValue(":post_content", unencodePostText(post["post_content"])); + query.bindValue(":post_content", unencodePostContent(post["post_content"])); query.bindValue(":post_author_id", post["post_author_id"].toInt()); query.bindValue(":post_author_name", unencodePostText(post["post_author_name"])); query.bindValue(":can_edit", post["can_edit"].toBool() ? 1 : 0); @@ -96,3 +96,8 @@ QString FetchPostsAction::unencodePostText(const QVariant &v) QByteArray ba = v.toByteArray(); return QString::fromUtf8(ba.constData(), ba.length()); } + +QString FetchPostsAction::unencodePostContent(const QVariant &v) +{ + return _board->bbcodeToRichText(unencodePostText(v)); +} diff --git a/fetchpostsaction.h b/fetchpostsaction.h index 63cb5f8..9951989 100644 --- a/fetchpostsaction.h +++ b/fetchpostsaction.h @@ -22,6 +22,7 @@ private slots: private: static QString unencodePostText(const QVariant& v); + QString unencodePostContent(const QVariant& v); private: XmlRpcPendingCall *_call; diff --git a/fetchtopicsaction.cpp b/fetchtopicsaction.cpp index dd59b4d..31a507a 100644 --- a/fetchtopicsaction.cpp +++ b/fetchtopicsaction.cpp @@ -43,8 +43,8 @@ void FetchTopicsAction::handleFinishedCall() 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)"); + 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, reply_number, 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, :reply_number, :new_post, :last_update_time)"); foreach (const QVariant& topic_v, topics) { QVariantMap topic = topic_v.toMap(); @@ -69,6 +69,7 @@ void FetchTopicsAction::handleFinishedCall() 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(":reply_number", topic["reply_number"].toInt()); query.bindValue(":new_post", topic["new_post"].toBool() ? 1 : 0); query.bindValue(":last_update_time", QDateTime::currentDateTime()); @@ -8,29 +8,23 @@ #include "forummodel.h" #include "topicmodel.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; + QApplication::setOrganizationDomain("com.javispedro.tapasboard"); + QApplication::setOrganizationName("tapasboard"); + QApplication::setApplicationName("tapasboard"); - 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"); + QScopedPointer<BoardManager> manager(new BoardManager); + board_manager = manager.data(); // Set the global pointer to this singleton qmlRegisterType<BoardModel>("com.javispedro.tapasboard", 1, 0, "BoardModel"); qmlRegisterType<ForumModel>("com.javispedro.tapasboard", 1, 0, "ForumModel"); qmlRegisterType<TopicModel>("com.javispedro.tapasboard", 1, 0, "TopicModel"); + QmlApplicationViewer viewer; viewer.setOrientation(QmlApplicationViewer::ScreenOrientationAuto); viewer.setMainQmlFile(QLatin1String("qml/tapasboard/main.qml")); viewer.showExpanded(); diff --git a/qml/tapasboard/TopicPage.qml b/qml/tapasboard/TopicPage.qml index 9a305f8..d3b2a35 100644 --- a/qml/tapasboard/TopicPage.qml +++ b/qml/tapasboard/TopicPage.qml @@ -28,6 +28,13 @@ Page { boardUrl: topicPage.boardUrl topicId: topicPage.topicId } + section.property: "humanDate" + section.criteria: ViewSection.FullString + section.delegate: GroupHeader { + width: parent.width + text: section + } + delegate: Item { id: postItem @@ -49,19 +56,43 @@ Page { anchors.right: parent.right anchors.margins: UiConstants.DefaultMargin anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + Item { + width: parent.width + height: childrenRect.height + + Text { + anchors.top: parent.top + anchors.left: parent.left + text: model.userName + font: UiConstants.SmallTitleFont + textFormat: Text.PlainText + } + Text { + anchors.top: parent.top + anchors.right: parent.right + text: model.humanTime + font: UiConstants.SubtitleFont + textFormat: Text.PlainText + } + } Text { text: model.title width: parent.width font: UiConstants.TitleFont visible: text != "" + textFormat: Text.PlainText } Text { text: model.content width: parent.width font: UiConstants.SubtitleFont + textFormat: Text.RichText wrapMode: Text.Wrap + onLinkActivated: Qt.openUrlExternally(link) } } } diff --git a/topicmodel.cpp b/topicmodel.cpp index bbc2314..0c7541e 100644 --- a/topicmodel.cpp +++ b/topicmodel.cpp @@ -14,7 +14,12 @@ TopicModel::TopicModel(QObject *parent) : roles[TitleRole] = QByteArray("title"); roles[ContentRole] = QByteArray("content"); roles[IconRole] = QByteArray("icon"); - roles[PostIdRole] = QByteArray("postcId"); + roles[PostIdRole] = QByteArray("postId"); + roles[UserIdRole] = QByteArray("userId"); + roles[UserNameRole] = QByteArray("userName"); + roles[DateTimeRole] = QByteArray("dateTime"); + roles[HumanDateRole] = QByteArray("humanDate"); + roles[HumanTimeRole] = QByteArray("humanTime"); setRoleNames(roles); } @@ -72,22 +77,31 @@ int TopicModel::rowCount(const QModelIndex &parent) const QVariant TopicModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); + if (!_board) return QVariant(); + const int row = index.row(); if (row >= _data.size()) { qWarning() << "Could not seek to" << row; return QVariant(); } - switch (role) { + switch (role) { // Mind the lack of break statements case TitleRole: return _data[row].title; - break; case ContentRole: return _data[row].content; - break; case PostIdRole: return _data[row].post_id; - break; + case UserIdRole: + return _data[row].user_id; + case UserNameRole: + return _data[row].user_name; + case DateTimeRole: + return _data[row].time; + case HumanDateRole: + return _board->renderHumanDate(_data[row].time); + case HumanTimeRole: + return _board->renderHumanTime(_data[row].time); } return QVariant(); @@ -143,7 +157,7 @@ void TopicModel::fetchMore(const QModelIndex &parent) } } -QDateTime TopicModel::parseDateTime(const QVariant &v) +QDateTime TopicModel::parseDbDateTime(const QVariant &v) { QString s = v.toString(); return QDateTime::fromString(s, Qt::ISODate); @@ -171,10 +185,10 @@ QDateTime TopicModel::lastTopPostUpdate() query.bindValue(":topic_id", _topicId); if (query.exec()) { if (query.next()) { - return parseDateTime(query.value(0)); + return parseDbDateTime(query.value(0)); } } else { - qWarning() << "Could not fetch posts:" << query.lastError().text(); + qWarning() << "Could not load top posts:" << query.lastError().text(); } return QDateTime(); } @@ -185,7 +199,7 @@ QList<TopicModel::Post> TopicModel::loadPosts(int start, int end) const int rows = end - start + 1; QList<Post> posts; QSqlQuery query(_board->database()); - query.prepare("SELECT post_id, post_title, post_content, post_time, last_update_time FROM posts " + query.prepare("SELECT post_id, post_title, post_content, post_author_id, post_author_name, post_time, last_update_time FROM posts " "WHERE topic_id = :topic_id " "ORDER by post_time ASC " "LIMIT :start, :limit"); @@ -199,8 +213,10 @@ QList<TopicModel::Post> TopicModel::loadPosts(int start, int end) post.post_id = query.value(0).toInt(); post.title = query.value(1).toString(); post.content = query.value(2).toString(); - post.time = parseDateTime(query.value(3)); - post.last_update_time = parseDateTime(query.value(4)); + post.user_id = query.value(3).toInt(); + post.user_name = query.value(4).toString(); + post.time = parseDbDateTime(query.value(5)); + post.last_update_time = parseDbDateTime(query.value(6)); posts.append(post); } } else { diff --git a/topicmodel.h b/topicmodel.h index 95a3f78..39dcdee 100644 --- a/topicmodel.h +++ b/topicmodel.h @@ -21,7 +21,12 @@ public: IconRole = Qt::DecorationRole, ContentRole = Qt::ToolTipRole, - PostIdRole = Qt::UserRole + PostIdRole = Qt::UserRole, + UserIdRole, + UserNameRole, + DateTimeRole, + HumanDateRole, + HumanTimeRole }; QString boardUrl() const; @@ -45,12 +50,14 @@ protected: int post_id; QString title; QString content; + int user_id; + QString user_name; QDateTime time; QDateTime last_update_time; }; private: - static QDateTime parseDateTime(const QVariant& v); + static QDateTime parseDbDateTime(const QVariant& v); static QDateTime oldestPostUpdate(const QList<Post>& posts); QDateTime lastTopPostUpdate(); QList<Post> loadPosts(int start, int end); |