From d8fcff1a2d6eb61c97c44790dbdb920ba9f52980 Mon Sep 17 00:00:00 2001 From: "Javier S. Pedro" Date: Thu, 4 Apr 2013 15:49:31 +0200 Subject: add showing unread posts --- board.cpp | 42 ++++++++++-- board.h | 16 +++++ boardmodel.cpp | 5 +- boardmodel.h | 3 +- fetchpostsaction.cpp | 30 +++++++-- fetchpostsaction.h | 1 + forummodel.cpp | 15 +++-- forummodel.h | 4 +- main.cpp | 30 +++++++++ qml/BoardPage.qml | 108 +++++++++++++++++++++++++++++++ qml/EmptyListDelegate.qml | 35 ++++++++++ qml/ForumPage.qml | 95 +++++++++++++++++++++++++++ qml/GroupHeader.qml | 26 ++++++++ qml/MainPage.qml | 39 +++++++++++ qml/TopicPage.qml | 122 +++++++++++++++++++++++++++++++++++ qml/main.qml | 12 ++++ qml/tapasboard/BoardPage.qml | 106 ------------------------------ qml/tapasboard/EmptyListDelegate.qml | 29 --------- qml/tapasboard/ForumPage.qml | 93 -------------------------- qml/tapasboard/GroupHeader.qml | 26 -------- qml/tapasboard/MainPage.qml | 39 ----------- qml/tapasboard/TopicPage.qml | 122 ----------------------------------- qml/tapasboard/main.qml | 12 ---- tapasboard.pro | 14 +++- topicmodel.cpp | 67 +++++++++++++++++-- topicmodel.h | 11 +++- 26 files changed, 648 insertions(+), 454 deletions(-) create mode 100644 qml/BoardPage.qml create mode 100644 qml/EmptyListDelegate.qml create mode 100644 qml/ForumPage.qml create mode 100644 qml/GroupHeader.qml create mode 100644 qml/MainPage.qml create mode 100644 qml/TopicPage.qml create mode 100644 qml/main.qml delete mode 100644 qml/tapasboard/BoardPage.qml delete mode 100644 qml/tapasboard/EmptyListDelegate.qml delete mode 100644 qml/tapasboard/ForumPage.qml delete mode 100644 qml/tapasboard/GroupHeader.qml delete mode 100644 qml/tapasboard/MainPage.qml delete mode 100644 qml/tapasboard/TopicPage.qml delete mode 100644 qml/tapasboard/main.qml diff --git a/board.cpp b/board.cpp index fc5db62..7766088 100644 --- a/board.cpp +++ b/board.cpp @@ -21,7 +21,8 @@ Board::Board(QObject *parent) : Board::Board(const QUrl& url, const QString& username, const QString& password, QObject *parent) : QObject(parent), _url(url), _slug(createSlug(url)), _db(QSqlDatabase::addDatabase("QSQLITE", _slug)), - _iface(new XmlRpcInterface(QUrl(_url), this)) + _iface(new XmlRpcInterface(QUrl(_url), this)), + _markReadDelay(new QTimer(this)) { _db.setDatabaseName(QDir::toNativeSeparators(getDbPathFor(_slug))); qDebug() << "Opening database file" << _db.databaseName() << "for" << _url; @@ -47,6 +48,7 @@ Board::Board(const QUrl& url, const QString& username, const QString& password, fetchForumsIfOutdated(); initializeBbCode(); // TODO This might depend on board config initializeSmilies(); + initializeMarkRead(); } Board::~Board() @@ -210,7 +212,18 @@ QString Board::renderHumanDate(const QDateTime &dateTime) QString Board::renderHumanTime(const QDateTime &dateTime) { - return dateTime.toLocalTime().time().toString(Qt::DefaultLocaleShortDate); + QDateTime localDateTime = dateTime.toLocalTime(); + const int secs = localDateTime.secsTo(QDateTime::currentDateTime()); + if (secs < 1) { + return tr("Just now"); + } else if (secs < 60) { + return tr("%n second(s) ago", 0, secs); + } else if (secs < 3600) { + int mins = (secs + 10) / 3600; // + 10 to round a bit + return tr("%n minute(s) ago", 0, mins); + } else { + return localDateTime.time().toString(Qt::DefaultLocaleShortDate); + } } void Board::cancelAllActions() @@ -243,16 +256,22 @@ void Board::notifyForumsChanged() void Board::notifyForumTopicsChanged(int forumId, int start, int end) { - qDebug() << "ForumTopics Changed" << forumId << start << end; + qDebug() << "ForumTopics changed" << forumId << start << end; emit forumTopicsChanged(forumId, start, end); } void Board::notifyTopicPostsChanged(int topicId, int start, int end) { - qDebug() << "TopicPosts Changed" << topicId << start << end; + qDebug() << "TopicPosts changed" << topicId << start << end; emit topicPostsChanged(topicId, start, end); } +void Board::notifyTopicPostsUnread(int topicId, int position) +{ + qDebug() << "TopicPosts unread" << topicId << position; + emit topicPostsUnread(topicId, position); +} + void Board::notifyLogin(const QMap &info) { if (_loginInfo.empty()) { @@ -281,6 +300,14 @@ void Board::notifyLogout() } } +void Board::markPostAsRead(int postId) +{ + _postsToMarkRead.insert(postId); + if (!_markReadDelay->isActive()) { + _markReadDelay->start(); + } +} + QString Board::createSlug(const QUrl& url) { static const QRegExp regexp("[^a-z0-9]+"); @@ -490,6 +517,13 @@ void Board::initializeSmilies() Q_ASSERT(_smilieRegexp.isValid()); } +void Board::initializeMarkRead() +{ + _markReadDelay->setInterval(1000); // 1 sec + _markReadDelay->setSingleShot(true); + // TODO connect +} + void Board::fetchConfigIfOutdated() { if (_iface->isAccessible()) { diff --git a/board.h b/board.h index 588219c..698b4b6 100644 --- a/board.h +++ b/board.h @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -63,9 +65,13 @@ public slots: void notifyForumsChanged(); void notifyForumTopicsChanged(int forumId, int start, int end); void notifyTopicPostsChanged(int topicId, int start, int end); + void notifyTopicPostsUnread(int topicId, int position); void notifyLogin(const QMap& info); void notifyLogout(); + // Functions for marking posts as read + void markPostAsRead(int postId); + signals: void busyChanged(); void loggedInChanged(); @@ -73,6 +79,7 @@ signals: void forumsChanged(); void forumTopicsChanged(int forumId, int start, int end); void topicPostsChanged(int topicId, int start, int end); + void topicPostsUnread(int topicId, int position); private: static QString createSlug(const QUrl& url); @@ -86,6 +93,7 @@ private: void executeActionFromQueue(); void initializeBbCode(); void initializeSmilies(); + void initializeMarkRead(); void fetchConfigIfOutdated(); void fetchForumsIfOutdated(); @@ -98,13 +106,21 @@ private: QString _slug; QSqlDatabase _db; XmlRpcInterface *_iface; + /** The queue of pending actions. The first one is currently being run. */ QQueue _queue; /** Configuration cache */ mutable QHash _config; + /** Login information, which is obviously not persistent. */ QMap _loginInfo; + /** Bbcodes list and their HTML replacements for quick-and-dirty parsing. */ QList< QPair > _bbcodes; + /** List of smilies and their replacements. */ QHash _smilies; + /** A regular expression that matches every possibly smilie. */ QRegExp _smilieRegexp; + /** This timer helps delay marking topics/posts as read. */ + QTimer *_markReadDelay; + QSet _postsToMarkRead; }; inline bool Board::busy() const diff --git a/boardmodel.cpp b/boardmodel.cpp index dc1757e..3e24769 100644 --- a/boardmodel.cpp +++ b/boardmodel.cpp @@ -16,6 +16,7 @@ BoardModel::BoardModel(QObject *parent) : roles[ForumIdRole] = QByteArray("forumId"); roles[SubOnlyRole] = QByteArray("subOnly"); roles[CategoryRole] = QByteArray("category"); + roles[UnreadRole] = QByteArray("unread"); setRoleNames(roles); } @@ -78,6 +79,8 @@ QVariant BoardModel::data(const QModelIndex &index, int role) const return _query.value(4); case CategoryRole: return _query.value(5); + case UnreadRole: + return _query.value(6); } return QVariant(); @@ -134,7 +137,7 @@ void BoardModel::reload() if (_board && _forumId >= 0) { _query = QSqlQuery(_board->database()); - _query.prepare("SELECT f1.forum_id,f1.forum_name,f1.logo_url,f1.description,f1.sub_only,f2.forum_name AS cat_name FROM forums f1 " + _query.prepare("SELECT f1.forum_id,f1.forum_name,f1.logo_url,f1.description,f1.sub_only,f2.forum_name,f1.new_post AS cat_name FROM forums f1 " "LEFT JOIN forums f2 ON f2.forum_id = f1.parent_id " "WHERE (f1.parent_id=:parent_id_1 AND f1.sub_only = 0) OR f1.parent_id IN " "(SELECT forum_id from forums WHERE parent_id=:parent_id_2 AND sub_only=1) " diff --git a/boardmodel.h b/boardmodel.h index 56d127d..35ed447 100644 --- a/boardmodel.h +++ b/boardmodel.h @@ -22,7 +22,8 @@ public: ForumIdRole = Qt::UserRole, SubOnlyRole, - CategoryRole + CategoryRole, + UnreadRole }; Board * board() const; diff --git a/fetchpostsaction.cpp b/fetchpostsaction.cpp index 2493aa3..714718c 100644 --- a/fetchpostsaction.cpp +++ b/fetchpostsaction.cpp @@ -59,6 +59,16 @@ void FetchPostsAction::handleFinishedCall() QSqlDatabase db = _board->database(); db.transaction(); + bool ok = false; + int topic_id = map["topic_id"].toInt(&ok); + if (!ok) { + // Not fatal, just assume it's the one we requested + topic_id = _topicId; + } + QString topic_title = map["topic_title"].toString(); + int unread_position = map["position"].toInt(); + qDebug() << "unread_position" << unread_position; + QSqlQuery query(db); query.prepare("INSERT OR REPLACE INTO posts (topic_id, post_id, post_title, post_content, post_author_id, post_author_name, can_edit, icon_url, post_time, last_update_time) " "VALUES (:topic_id, :post_id, :post_title, :post_content, :post_author_id, :post_author_name, :can_edit, :icon_url, :post_time, :last_update_time)"); @@ -66,11 +76,6 @@ void FetchPostsAction::handleFinishedCall() foreach (const QVariant& post_v, posts) { QVariantMap post = post_v.toMap(); bool ok = false; - int topic_id = post["topic_id"].toInt(&ok); - if (!ok) { - // Not fatal, just assume it's the one we requested - topic_id = _topicId; - } int post_id = post["post_id"].toInt(&ok); if (!ok) { qWarning() << "No post_id in" << post; @@ -79,7 +84,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_title", unencodePostTitle(post["post_title"], topic_title)); 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"])); @@ -100,6 +105,10 @@ void FetchPostsAction::handleFinishedCall() _board->notifyTopicPostsChanged(_topicId, _start, _start + posts.size() - 1); } + if (unread_position > 0) { + // API says this is 1-indexed instead of 0-indexed. + _board->notifyTopicPostsUnread(_topicId, unread_position - 1); + } if (_end == FetchAllPosts && posts.size() == MAX_TOPIC_PAGE_SIZE) { // Ok, let's prepare to fetch the next block of posts because // there are probably more of them @@ -120,6 +129,15 @@ QString FetchPostsAction::unencodePostText(const QVariant &v) return QString::fromUtf8(ba.constData(), ba.length()); } +QString FetchPostsAction::unencodePostTitle(const QVariant &v, const QString &topicTitle) +{ + QString title = unencodePostText(v); + if (QString::compare(title, "Re: " + topicTitle, Qt::CaseInsensitive)) { + return QString(); + } + return title; +} + QString FetchPostsAction::unencodePostContent(const QVariant &v) { QString richText = _board->bbcodeToRichText(unencodePostText(v)); diff --git a/fetchpostsaction.h b/fetchpostsaction.h index 732c0d2..e122622 100644 --- a/fetchpostsaction.h +++ b/fetchpostsaction.h @@ -26,6 +26,7 @@ private slots: private: static QString unencodePostText(const QVariant& v); + static QString unencodePostTitle(const QVariant& v, const QString& topicTitle); QString unencodePostContent(const QVariant& v); private: diff --git a/forummodel.cpp b/forummodel.cpp index 63fb0b8..faf0b75 100644 --- a/forummodel.cpp +++ b/forummodel.cpp @@ -15,6 +15,7 @@ ForumModel::ForumModel(QObject *parent) : roles[IconRole] = QByteArray("icon"); roles[TopicIdRole] = QByteArray("topicId"); roles[NumRepliesRole] = QByteArray("numReplies"); + roles[UnreadRole] = QByteArray("unread"); setRoleNames(roles); } @@ -84,6 +85,8 @@ QVariant ForumModel::data(const QModelIndex &index, int role) const return _data[row].topic_id; case NumRepliesRole: return _data[row].num_replies; + case UnreadRole: + return _data[row].unread; } return QVariant(); @@ -103,7 +106,7 @@ void ForumModel::fetchMore(const QModelIndex &parent) const int start = _data.size(); QList topics = loadTopics(start, start + FORUM_PAGE_SIZE - 1); - const int new_end = start + _data.size() - 1; + const int new_end = start + topics.size() - 1; if (topics.empty()) { // We could not load anything more from DB! @@ -116,11 +119,12 @@ void ForumModel::fetchMore(const QModelIndex &parent) } if (_board->service()->isAccessible()) { - if (!_data.empty()) { + if (!topics.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"; + Q_ASSERT(new_end > 0); _board->enqueueAction(new FetchTopicsAction(_forumId, start, new_end, @@ -190,7 +194,7 @@ QList ForumModel::loadTopics(int start, int end) const int rows = end - start + 1; QList topics; QSqlQuery query(_board->database()); - query.prepare("SELECT topic_id, topic_title, reply_number, last_reply_time, last_update_time FROM topics " + query.prepare("SELECT topic_id, topic_title, reply_number, new_post, last_reply_time, last_update_time FROM topics " "WHERE forum_id = :forum_id " "ORDER by last_reply_time DESC " "LIMIT :start, :limit"); @@ -204,8 +208,9 @@ QList ForumModel::loadTopics(int start, int end) topic.topic_id = query.value(0).toInt(); topic.title = query.value(1).toString(); topic.num_replies = query.value(2).toInt(); - topic.last_reply_time = parseDateTime(query.value(3)); - topic.last_update_time = parseDateTime(query.value(4)); + topic.unread = query.value(3).toBool(); + topic.last_reply_time = parseDateTime(query.value(4)); + topic.last_update_time = parseDateTime(query.value(5)); topics.append(topic); } } else { diff --git a/forummodel.h b/forummodel.h index 4c00692..d0dd980 100644 --- a/forummodel.h +++ b/forummodel.h @@ -22,7 +22,8 @@ public: TopicIdRole = Qt::UserRole, TopicTypeRole, - NumRepliesRole + NumRepliesRole, + UnreadRole }; Board * board() const; @@ -49,6 +50,7 @@ protected: int topic_id; QString title; int num_replies; + bool unread; QDateTime last_reply_time; QDateTime last_update_time; }; diff --git a/main.cpp b/main.cpp index ccb6fbd..2d23694 100644 --- a/main.cpp +++ b/main.cpp @@ -13,12 +13,42 @@ BoardManager *board_manager; +static QString 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; +} + Q_DECL_EXPORT int main(int argc, char *argv[]) { QScopedPointer app(createApplication(argc, argv)); QApplication::setOrganizationDomain("com.javispedro.tapasboard"); QApplication::setOrganizationName("tapasboard"); QApplication::setApplicationName("tapasboard"); + QApplication::setApplicationVersion("0.0.1"); + + const QString locale_path = adjustPath("i18n"); + QString locale = QLocale::system().name(); + QTranslator translator; + + if (!(translator.load(locale, locale_path))) { + // Fallback to English + qDebug() << "Translation not available for" << locale; + translator.load("en", locale_path); + } + + app->installTranslator(&translator); QScopedPointer manager(new BoardManager); board_manager = manager.data(); // Set the global pointer to this singleton diff --git a/qml/BoardPage.qml b/qml/BoardPage.qml new file mode 100644 index 0000000..ca988f3 --- /dev/null +++ b/qml/BoardPage.qml @@ -0,0 +1,108 @@ +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 Board board : null; + property int forumId; + + tools: ToolBarLayout { + ToolIcon { + platformIconId: "toolbar-back" + onClicked: pageStack.pop() + } + ToolIcon { + platformIconId: board.busy ? "toolbar-cancle" : "toolbar-refresh" + onClicked: { + if (board.busy) { + board.cancelAllActions(); + } else { + boardModel.refresh(); + } + } + } + } + + ListView { + id: forumsView + anchors.fill: parent + model: BoardModel { + id: boardModel + board: boardPage.board + forumId: boardPage.forumId + } + section.criteria: ViewSection.FullString + section.property: "category" + section.delegate: GroupHeader { + width: parent.width + text: section + } + + delegate: EmptyListDelegate { + id: forumItem + + height: Math.max(forumItemColumn.height + UiConstants.ButtonSpacing * 2, UiConstants.ListItemHeightDefault) + + unread: model.unread + + Column { + id: forumItemColumn + anchors.left: parent.left + anchors.right: forumItemImage.left + anchors.verticalCenter: parent.verticalCenter + + Text { + text: model.title + width: parent.width + font: UiConstants.TitleFont + } + + Text { + text: model.description + width: parent.width + font: UiConstants.SubtitleFont + wrapMode: Text.Wrap + visible: text != "" + } + } + + Image { + id: forumItemImage + source: "image://theme/icon-m-common-drilldown-arrow" + (theme.inverted ? "-inverse" : "") + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + } + + onClicked: { + if (model.subOnly) { + pageStack.push(Qt.resolvedUrl("BoardPage.qml"), { + board: boardPage.board, + forumId: model.forumId + }); + } else { + pageStack.push(Qt.resolvedUrl("ForumPage.qml"), { + board: boardPage.board, + forumId: model.forumId + }); + } + } + } + } + + ScrollDecorator { + flickableItem: forumsView + } + + BusyIndicator { + anchors.centerIn: parent + platformStyle: BusyIndicatorStyle { size: "large" } + visible: forumsView.count == 0 && board.busy + running: visible + } +} diff --git a/qml/EmptyListDelegate.qml b/qml/EmptyListDelegate.qml new file mode 100644 index 0000000..71fa3dc --- /dev/null +++ b/qml/EmptyListDelegate.qml @@ -0,0 +1,35 @@ +import QtQuick 1.1 +import com.nokia.meego 1.1 + +Item { + id: listItem + + signal clicked + property alias pressed: mouseArea.pressed + property bool unread: false + + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: UiConstants.ButtonSpacing + height: UiConstants.ListItemHeightDefault + + BorderImage { + id: background + anchors.fill: parent + anchors.leftMargin: -(UiConstants.DefaultMargin+parent.anchors.leftMargin) + anchors.rightMargin: -UiConstants.DefaultMargin + border { left: 22; right: 22; top: 22; bottom: 22; } + visible: pressed || unread + source: "image://theme/meegotouch" + (unread|(!unread&&!pressed)?"-unread-inbox":"") + + "-panel" + (theme.inverted?"-inverted":"") + + "-background" + (pressed?"-pressed":"") + } + + MouseArea { + id: mouseArea; + anchors.fill: parent + onClicked: { + listItem.clicked(); + } + } +} diff --git a/qml/ForumPage.qml b/qml/ForumPage.qml new file mode 100644 index 0000000..ee04e43 --- /dev/null +++ b/qml/ForumPage.qml @@ -0,0 +1,95 @@ +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 Board board: null; + property int forumId; + + tools: ToolBarLayout { + ToolIcon { + platformIconId: "toolbar-back" + onClicked: pageStack.pop() + } + ToolIcon { + platformIconId: board.busy ? "toolbar-cancle" : "toolbar-refresh" + onClicked: { + if (board.busy) { + board.cancelAllActions(); + } else { + forumModel.refresh(); + } + } + } + } + + ListView { + id: topicsView + anchors.fill: parent + model: ForumModel { + id: forumModel + board: forumPage.board + forumId: forumPage.forumId + } + delegate: EmptyListDelegate { + id: topicItem + + height: Math.max(topicItemColumn.height + UiConstants.ButtonSpacing * 2, UiConstants.ListItemHeightDefault) + + unread: model.unread + + Column { + id: topicItemColumn + anchors.left: parent.left + anchors.right: topicRepliesCount.left + anchors.verticalCenter: parent.verticalCenter + + Text { + text: model.title + width: parent.width + font: UiConstants.TitleFont + wrapMode: Text.Wrap + } + } + + CountBubble { + id: topicRepliesCount + anchors.right: topicItemImage.left + anchors.verticalCenter: parent.verticalCenter + value: model.numReplies + visible: value > 0 + } + + Image { + id: topicItemImage + source: "image://theme/icon-m-common-drilldown-arrow" + (theme.inverted ? "-inverse" : "") + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + } + + onClicked: { + pageStack.push(Qt.resolvedUrl("TopicPage.qml"), { + board: forumPage.board, + topicId: model.topicId + }); + } + } + } + + ScrollDecorator { + flickableItem: topicsView + } + + BusyIndicator { + anchors.centerIn: parent + platformStyle: BusyIndicatorStyle { size: "large" } + visible: topicsView.count == 0 && board.busy + running: visible + } +} diff --git a/qml/GroupHeader.qml b/qml/GroupHeader.qml new file mode 100644 index 0000000..0350ee0 --- /dev/null +++ b/qml/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/MainPage.qml b/qml/MainPage.qml new file mode 100644 index 0000000..e0e8fb8 --- /dev/null +++ b/qml/MainPage.qml @@ -0,0 +1,39 @@ +import QtQuick 1.1 +import com.nokia.meego 1.1 +import com.nokia.extras 1.1 +import com.javispedro.tapasboard 1.0 + +Page { + id: mainPage + + anchors.leftMargin: UiConstants.DefaultMargin + anchors.rightMargin: UiConstants.DefaultMargin + + ListView { + id: favoritesView + anchors.fill: parent + model: FavoritesModel { + + } + delegate: ListDelegate { + id: favoriteItem + + Image { + id: topicItemImage + source: "image://theme/icon-m-common-drilldown-arrow" + (theme.inverted ? "-inverse" : "") + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + } + + onClicked: { + var board = boardManager.getBoard(model.boardUrl, + model.loginUsername, + model.loginPassword); + pageStack.push(Qt.resolvedUrl("BoardPage.qml"), { + board: board, + forumId: board.rootForumId + }); + } + } + } +} diff --git a/qml/TopicPage.qml b/qml/TopicPage.qml new file mode 100644 index 0000000..689dce7 --- /dev/null +++ b/qml/TopicPage.qml @@ -0,0 +1,122 @@ +import QtQuick 1.1 +import com.nokia.meego 1.1 +import com.nokia.extras 1.1 +import com.javispedro.tapasboard 1.0 + +Page { + id: topicPage + + anchors.leftMargin: UiConstants.DefaultMargin + anchors.rightMargin: UiConstants.DefaultMargin + + property Board board; + property int topicId; + + tools: ToolBarLayout { + ToolIcon { + platformIconId: "toolbar-back" + onClicked: pageStack.pop() + } + ToolIcon { + platformIconId: board.busy ? "toolbar-cancle" : "toolbar-refresh" + onClicked: { + if (board.busy) { + board.cancelAllActions(); + } else { + topicModel.refresh(); + } + } + } + } + + ListView { + id: postsView + anchors.fill: parent + model: TopicModel { + id: topicModel + board: topicPage.board + topicId: topicPage.topicId + } + section.property: "humanDate" + section.criteria: ViewSection.FullString + section.delegate: GroupHeader { + width: parent.width + text: section + } + + delegate: Item { + id: postItem + + height: postItemRectangle.height + UiConstants.ButtonSpacing * 2 + width: parent.width + + Rectangle { + id: postItemRectangle + width: parent.width + height: postItemColumn.height + UiConstants.DefaultMargin + anchors.centerIn: parent + + color: "white" + radius: 20 + + Column { + id: postItemColumn + anchors.left: parent.left + 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 + wrapMode: Text.Wrap + } + + Text { + text: model.content + width: parent.width + font: UiConstants.SubtitleFont + textFormat: Text.RichText + wrapMode: Text.Wrap + onLinkActivated: Qt.openUrlExternally(link) + } + } + } + } + } + + ScrollDecorator { + flickableItem: postsView + } + + BusyIndicator { + anchors.centerIn: parent + platformStyle: BusyIndicatorStyle { size: "large" } + visible: postsView.count == 0 && board.busy + running: visible + } +} diff --git a/qml/main.qml b/qml/main.qml new file mode 100644 index 0000000..11a7e44 --- /dev/null +++ b/qml/main.qml @@ -0,0 +1,12 @@ +import QtQuick 1.1 +import com.nokia.meego 1.0 + +PageStackWindow { + id: appWindow + + initialPage: mainPage + + MainPage { + id: mainPage + } +} diff --git a/qml/tapasboard/BoardPage.qml b/qml/tapasboard/BoardPage.qml deleted file mode 100644 index 7efc6db..0000000 --- a/qml/tapasboard/BoardPage.qml +++ /dev/null @@ -1,106 +0,0 @@ -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 Board board : null; - property int forumId; - - tools: ToolBarLayout { - ToolIcon { - platformIconId: "toolbar-back" - onClicked: pageStack.pop() - } - ToolIcon { - platformIconId: board.busy ? "toolbar-cancle" : "toolbar-refresh" - onClicked: { - if (board.busy) { - board.cancelAllActions(); - } else { - boardModel.refresh(); - } - } - } - } - - ListView { - id: forumsView - anchors.fill: parent - model: BoardModel { - id: boardModel - board: boardPage.board - forumId: boardPage.forumId - } - section.criteria: ViewSection.FullString - section.property: "category" - section.delegate: GroupHeader { - width: parent.width - text: section - } - - delegate: EmptyListDelegate { - id: forumItem - - height: Math.max(forumItemColumn.height + UiConstants.ButtonSpacing * 2, UiConstants.ListItemHeightDefault) - - Column { - id: forumItemColumn - anchors.left: parent.left - anchors.right: forumItemImage.left - anchors.verticalCenter: parent.verticalCenter - - Text { - text: model.title - width: parent.width - font: UiConstants.TitleFont - } - - Text { - text: model.description - width: parent.width - font: UiConstants.SubtitleFont - wrapMode: Text.Wrap - visible: text != "" - } - } - - Image { - id: forumItemImage - source: "image://theme/icon-m-common-drilldown-arrow" + (theme.inverted ? "-inverse" : "") - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - } - - onClicked: { - if (model.subOnly) { - pageStack.push(Qt.resolvedUrl("BoardPage.qml"), { - board: boardPage.board, - forumId: model.forumId - }); - } else { - pageStack.push(Qt.resolvedUrl("ForumPage.qml"), { - board: boardPage.board, - forumId: model.forumId - }); - } - } - } - } - - ScrollDecorator { - flickableItem: forumsView - } - - BusyIndicator { - anchors.centerIn: parent - platformStyle: BusyIndicatorStyle { size: "large" } - visible: forumsView.count == 0 && board.busy - running: visible - } -} diff --git a/qml/tapasboard/EmptyListDelegate.qml b/qml/tapasboard/EmptyListDelegate.qml deleted file mode 100644 index 9a9d63d..0000000 --- a/qml/tapasboard/EmptyListDelegate.qml +++ /dev/null @@ -1,29 +0,0 @@ -import QtQuick 1.1 -import com.nokia.meego 1.1 - -Item { - id: listItem - - signal clicked - property alias pressed: mouseArea.pressed - - height: UiConstants.ListItemHeightDefault - width: parent.width - - BorderImage { - id: background - anchors.fill: parent - anchors.leftMargin: -UiConstants.DefaultMargin - anchors.rightMargin: -UiConstants.DefaultMargin - visible: pressed - source: theme.inverted ? "image://theme/meegotouch-panel-inverted-background-pressed" : "image://theme/meegotouch-panel-background-pressed" - } - - MouseArea { - id: mouseArea; - anchors.fill: parent - onClicked: { - listItem.clicked(); - } - } -} diff --git a/qml/tapasboard/ForumPage.qml b/qml/tapasboard/ForumPage.qml deleted file mode 100644 index 96b8082..0000000 --- a/qml/tapasboard/ForumPage.qml +++ /dev/null @@ -1,93 +0,0 @@ -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 Board board: null; - property int forumId; - - tools: ToolBarLayout { - ToolIcon { - platformIconId: "toolbar-back" - onClicked: pageStack.pop() - } - ToolIcon { - platformIconId: board.busy ? "toolbar-cancle" : "toolbar-refresh" - onClicked: { - if (board.busy) { - board.cancelAllActions(); - } else { - forumModel.refresh(); - } - } - } - } - - ListView { - id: topicsView - anchors.fill: parent - model: ForumModel { - id: forumModel - board: forumPage.board - forumId: forumPage.forumId - } - delegate: EmptyListDelegate { - id: topicItem - - height: Math.max(topicItemColumn.height + UiConstants.ButtonSpacing * 2, UiConstants.ListItemHeightDefault) - - Column { - id: topicItemColumn - anchors.left: parent.left - anchors.right: topicRepliesCount.left - anchors.verticalCenter: parent.verticalCenter - - Text { - text: model.title - width: parent.width - font: UiConstants.TitleFont - wrapMode: Text.Wrap - } - } - - CountBubble { - id: topicRepliesCount - anchors.right: topicItemImage.left - anchors.verticalCenter: parent.verticalCenter - value: model.numReplies - visible: value > 0 - } - - Image { - id: topicItemImage - source: "image://theme/icon-m-common-drilldown-arrow" + (theme.inverted ? "-inverse" : "") - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - } - - onClicked: { - pageStack.push(Qt.resolvedUrl("TopicPage.qml"), { - board: forumPage.board, - topicId: model.topicId - }); - } - } - } - - ScrollDecorator { - flickableItem: topicsView - } - - BusyIndicator { - anchors.centerIn: parent - platformStyle: BusyIndicatorStyle { size: "large" } - visible: topicsView.count == 0 && board.busy - running: visible - } -} diff --git a/qml/tapasboard/GroupHeader.qml b/qml/tapasboard/GroupHeader.qml deleted file mode 100644 index 0350ee0..0000000 --- a/qml/tapasboard/GroupHeader.qml +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index e0e8fb8..0000000 --- a/qml/tapasboard/MainPage.qml +++ /dev/null @@ -1,39 +0,0 @@ -import QtQuick 1.1 -import com.nokia.meego 1.1 -import com.nokia.extras 1.1 -import com.javispedro.tapasboard 1.0 - -Page { - id: mainPage - - anchors.leftMargin: UiConstants.DefaultMargin - anchors.rightMargin: UiConstants.DefaultMargin - - ListView { - id: favoritesView - anchors.fill: parent - model: FavoritesModel { - - } - delegate: ListDelegate { - id: favoriteItem - - Image { - id: topicItemImage - source: "image://theme/icon-m-common-drilldown-arrow" + (theme.inverted ? "-inverse" : "") - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - } - - onClicked: { - var board = boardManager.getBoard(model.boardUrl, - model.loginUsername, - model.loginPassword); - pageStack.push(Qt.resolvedUrl("BoardPage.qml"), { - board: board, - forumId: board.rootForumId - }); - } - } - } -} diff --git a/qml/tapasboard/TopicPage.qml b/qml/tapasboard/TopicPage.qml deleted file mode 100644 index 689dce7..0000000 --- a/qml/tapasboard/TopicPage.qml +++ /dev/null @@ -1,122 +0,0 @@ -import QtQuick 1.1 -import com.nokia.meego 1.1 -import com.nokia.extras 1.1 -import com.javispedro.tapasboard 1.0 - -Page { - id: topicPage - - anchors.leftMargin: UiConstants.DefaultMargin - anchors.rightMargin: UiConstants.DefaultMargin - - property Board board; - property int topicId; - - tools: ToolBarLayout { - ToolIcon { - platformIconId: "toolbar-back" - onClicked: pageStack.pop() - } - ToolIcon { - platformIconId: board.busy ? "toolbar-cancle" : "toolbar-refresh" - onClicked: { - if (board.busy) { - board.cancelAllActions(); - } else { - topicModel.refresh(); - } - } - } - } - - ListView { - id: postsView - anchors.fill: parent - model: TopicModel { - id: topicModel - board: topicPage.board - topicId: topicPage.topicId - } - section.property: "humanDate" - section.criteria: ViewSection.FullString - section.delegate: GroupHeader { - width: parent.width - text: section - } - - delegate: Item { - id: postItem - - height: postItemRectangle.height + UiConstants.ButtonSpacing * 2 - width: parent.width - - Rectangle { - id: postItemRectangle - width: parent.width - height: postItemColumn.height + UiConstants.DefaultMargin - anchors.centerIn: parent - - color: "white" - radius: 20 - - Column { - id: postItemColumn - anchors.left: parent.left - 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 - wrapMode: Text.Wrap - } - - Text { - text: model.content - width: parent.width - font: UiConstants.SubtitleFont - textFormat: Text.RichText - wrapMode: Text.Wrap - onLinkActivated: Qt.openUrlExternally(link) - } - } - } - } - } - - ScrollDecorator { - flickableItem: postsView - } - - BusyIndicator { - anchors.centerIn: parent - platformStyle: BusyIndicatorStyle { size: "large" } - visible: postsView.count == 0 && board.busy - running: visible - } -} diff --git a/qml/tapasboard/main.qml b/qml/tapasboard/main.qml deleted file mode 100644 index 11a7e44..0000000 --- a/qml/tapasboard/main.qml +++ /dev/null @@ -1,12 +0,0 @@ -import QtQuick 1.1 -import com.nokia.meego 1.0 - -PageStackWindow { - id: appWindow - - initialPage: mainPage - - MainPage { - id: mainPage - } -} diff --git a/tapasboard.pro b/tapasboard.pro index 1cba839..4c16089 100644 --- a/tapasboard.pro +++ b/tapasboard.pro @@ -1,7 +1,9 @@ # Add more folders to ship with the application, here -folder_01.source = qml/tapasboard -folder_01.target = qml -DEPLOYMENTFOLDERS = folder_01 +qml_folder.source = qml +qml_folder.target = qml +i18n_folder.source = i18n/*.qm +i18n_folder.target = i18n +DEPLOYMENTFOLDERS = qml_folder i18n_folder # Additional import path used to resolve QML modules in Creator's code model QML_IMPORT_PATH = @@ -70,6 +72,12 @@ HEADERS += \ imagenetworkaccessmanager.h \ loginaction.h +TRANSLATIONS += i18n/en.ts i18n/es.ts + +evil_hack_to_fool_lupdate { + SOURCES += qml/*.qml +} + OTHER_FILES += \ qtc_packaging/debian_harmattan/rules \ qtc_packaging/debian_harmattan/README \ diff --git a/topicmodel.cpp b/topicmodel.cpp index c058a6a..6b4991d 100644 --- a/topicmodel.cpp +++ b/topicmodel.cpp @@ -8,7 +8,7 @@ #include "topicmodel.h" TopicModel::TopicModel(QObject *parent) : - QAbstractListModel(parent), _board(0), _topicId(-1), _eof(false) + QAbstractListModel(parent), _board(0), _topicId(-1), _eof(false), _firstUnread(-1) { QHash roles = roleNames(); roles[TitleRole] = QByteArray("title"); @@ -32,6 +32,7 @@ void TopicModel::setBoard(Board *board) { if (_board != board) { disconnect(this, SLOT(handleTopicPostsChanged(int,int,int))); + disconnect(this, SLOT(handleTopicPostsUnread(int,int))); clearModel(); _board = board; @@ -39,6 +40,8 @@ void TopicModel::setBoard(Board *board) if (_board) { connect(_board, SIGNAL(topicPostsChanged(int,int,int)), SLOT(handleTopicPostsChanged(int,int,int))); + connect(_board, SIGNAL(topicPostsUnread(int,int)), + SLOT(handleTopicPostsUnread(int,int))); if (_topicId >= 0) { update(); reload(); @@ -68,6 +71,11 @@ void TopicModel::setTopicId(const int id) } } +int TopicModel::firstUnreadPost() const +{ + return _firstUnread; +} + int TopicModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : _data.size(); @@ -84,6 +92,12 @@ QVariant TopicModel::data(const QModelIndex &index, int role) const return QVariant(); } + if (_data[row].post_id < 0) { + // This post is a unfetched stub + this->fetchPost(row); + return QVariant(); // Will update the model once the post arrives + } + switch (role) { // Mind the lack of break statements case TitleRole: return _data[row].title; @@ -120,7 +134,7 @@ void TopicModel::fetchMore(const QModelIndex &parent) const int start = _data.size(); QList posts = loadPosts(start, start + TOPIC_PAGE_SIZE - 1); - const int new_end = start + _data.size() - 1; + const int new_end = start + posts.size() - 1; if (posts.empty()) { // We could not load anything more from DB! @@ -133,11 +147,12 @@ void TopicModel::fetchMore(const QModelIndex &parent) } if (_board->service()->isAccessible()) { - if (!_data.empty()) { + if (!posts.empty()) { QDateTime last = oldestPostUpdate(posts); // If the posts we got from DB are too old, refresh online. if (last.secsTo(QDateTime::currentDateTime()) > TOPIC_POSTS_TLL) { qDebug() << "Fetching posts because of old"; + Q_ASSERT(new_end > 0); _board->enqueueAction(new FetchPostsAction(_topicId, start, new_end, @@ -215,6 +230,7 @@ QList TopicModel::loadPosts(int start, int end) query.bindValue(":start", start); query.bindValue(":limit", rows); if (query.exec()) { + int loaded = 0; posts.reserve(rows); while (query.next()) { Post post; @@ -226,6 +242,7 @@ QList TopicModel::loadPosts(int start, int end) post.time = parseDbDateTime(query.value(5)); post.last_update_time = parseDbDateTime(query.value(6)); posts.append(post); + loaded++; } } else { qWarning() << "Could not load posts:" << query.lastError().text(); @@ -233,6 +250,36 @@ QList TopicModel::loadPosts(int start, int end) return posts; } +void TopicModel::fetchPost(int position) const +{ + if (_board->service()->isAccessible()) { + // There are lest posts on the DB than we wanted + qDebug() << "Fetching posts because of unfetched"; + // Always fetch one page at least. + int fetch_start = position % FORUM_PAGE_SIZE; + int fetch_end = fetch_start + FORUM_PAGE_SIZE; + _board->enqueueAction(new FetchPostsAction(_topicId, + fetch_start, fetch_end, + _board)); + } +} + +void TopicModel::enlargeModel(int end) +{ + int start = _data.size(); + if (end > start) { + qDebug() << "Call insert rows (enlarge):" << start << end; + beginInsertRows(QModelIndex(), start, end); + Post post; + post.post_id = -1; + for (int i = start; i <= end; i++) { + Q_ASSERT(_data.size() == i); + _data.append(post); + } + endInsertRows(); + } +} + void TopicModel::clearModel() { beginResetModel(); @@ -245,7 +292,6 @@ void TopicModel::handleTopicPostsChanged(int topicId, int start, int end) { if (topicId == _topicId) { // Yep, our posts list changed. - qDebug() << "My posts changed" << start << end; if (end > _data.size()) { // If for any reason we have more posts now, it means we might // no longer be EOF... @@ -264,7 +310,7 @@ void TopicModel::handleTopicPostsChanged(int topicId, int start, int end) } if (end >= _data.size()) { - qDebug() << "Call insert rows" << _data.size() << end; + qDebug() << "Call insert rows (changed):" << _data.size() << end; beginInsertRows(QModelIndex(), _data.size(), end); _data.reserve(end + 1); for (int i = start; i < _data.size(); i++) { @@ -286,6 +332,17 @@ void TopicModel::handleTopicPostsChanged(int topicId, int start, int end) } } +void TopicModel::handleTopicPostsUnread(int topicId, int position) +{ + if (topicId == _topicId) { + if (position != _firstUnread) { + enlargeModel(position); + _firstUnread = position; + emit firstUnreadPostChanged(); + } + } +} + void TopicModel::update() { if (!_board || _topicId < 0) return; diff --git a/topicmodel.h b/topicmodel.h index 0151242..67e48c4 100644 --- a/topicmodel.h +++ b/topicmodel.h @@ -12,6 +12,7 @@ class TopicModel : public QAbstractListModel Q_OBJECT Q_PROPERTY(Board * board READ board WRITE setBoard NOTIFY boardChanged) Q_PROPERTY(int topicId READ topicId WRITE setTopicId NOTIFY topicIdChanged) + Q_PROPERTY(int firstUnreadPost READ firstUnreadPost NOTIFY firstUnreadPostChanged) public: TopicModel(QObject *parent = 0); @@ -35,6 +36,8 @@ public: int topicId() const; void setTopicId(const int id); + int firstUnreadPost() const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role) const; @@ -47,9 +50,11 @@ public slots: signals: void boardChanged(); void topicIdChanged(); + void firstUnreadPostChanged(); protected: struct Post { + /** Set 'post_id' to -1 for "not yet fetched" */ int post_id; QString title; QString content; @@ -64,10 +69,13 @@ private: static QDateTime oldestPostUpdate(const QList& posts); QDateTime lastTopPostUpdate(); QList loadPosts(int start, int end); + void fetchPost(int position) const; // const because data() calls this + void enlargeModel(int end); void clearModel(); private slots: - void handleTopicPostsChanged(int forumId, int start, int end); + void handleTopicPostsChanged(int topicId, int start, int end); + void handleTopicPostsUnread(int topicId, int position); void update(); void reload(); @@ -76,6 +84,7 @@ private: int _topicId; QList _data; bool _eof; + int _firstUnread; }; #endif // TOPICMODEL_H -- cgit v1.2.3