diff --git a/discover/qml/ReviewDelegate.qml b/discover/qml/ReviewDelegate.qml index 803284c8..94699e17 100644 --- a/discover/qml/ReviewDelegate.qml +++ b/discover/qml/ReviewDelegate.qml @@ -1,89 +1,109 @@ /* * Copyright (C) 2012 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library/Lesser General Public License * version 2, or (at your option) any later version, as published by the * Free Software Foundation * * This program 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 Library/Lesser General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.1 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.1 import org.kde.discover 2.0 import org.kde.kirigami 2.0 as Kirigami -ColumnLayout -{ +RowLayout { id: item visible: model.shouldShow property bool compact: false property bool separator: true signal markUseful(bool useful) function usefulnessToString(favorable, total) { return total===0 ? i18n("Tell us about this review!") : i18n("%1 out of %2 people found this review useful", favorable, total) } - RowLayout { - Layout.fillWidth: true + Repeater { + model: depth + delegate: Rectangle { + Layout.fillHeight: true + Layout.minimumWidth: Kirigami.Units.largeSpacing + Layout.maximumWidth: Kirigami.Units.largeSpacing + color: Qt.tint(Kirigami.Theme.textColor, Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.8)) + Rectangle { + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + } + width: 1 + color: Kirigami.Theme.backgroundColor + } + } + } + ColumnLayout + { + RowLayout { + Layout.fillWidth: true + Label { + id: content + Layout.fillWidth: true + elide: Text.ElideRight + readonly property string author: reviewer ? reviewer : i18n("unknown reviewer") + text: summary ? i18n("%1 by %2", summary, author) : i18n("Comment by %1", author) + } + Rating { + id: rating + rating: model.rating + starSize: content.font.pointSize + } + } Label { - id: content Layout.fillWidth: true - elide: Text.ElideRight - readonly property string author: reviewer ? reviewer : i18n("unknown reviewer") - text: summary ? i18n("%1 by %2", summary, author) : i18n("Comment by %1", author) + text: display + maximumLineCount: item.compact ? 3 : undefined + wrapMode: Text.Wrap } - Rating { - id: rating - rating: model.rating - starSize: content.font.pointSize + Label { + visible: !item.compact + text: item.usefulnessToString(usefulnessFavorable, usefulnessTotal) } - } - Label { - Layout.fillWidth: true - text: display - maximumLineCount: item.compact ? 3 : undefined - wrapMode: Text.Wrap - } - Label { - visible: !item.compact - text: usefulnessToString(usefulnessFavorable, usefulnessTotal) - } - Label { - visible: !item.compact - Layout.alignment: Qt.AlignRight - text: { - switch(usefulChoice) { - case ReviewsModel.Yes: - i18n("Useful? Yes/No") - break; - case ReviewsModel.No: - i18n("Useful? Yes/No") - break; - default: - i18n("Useful? Yes/No") - break; + Label { + visible: !item.compact + Layout.alignment: Qt.AlignRight + text: { + switch(usefulChoice) { + case ReviewsModel.Yes: + i18n("Useful? Yes/No") + break; + case ReviewsModel.No: + i18n("Useful? Yes/No") + break; + default: + i18n("Useful? Yes/No") + break; + } } + onLinkActivated: item.markUseful(link=='true') + } + Kirigami.Separator { + visible: item.separator + Layout.fillWidth: true } - onLinkActivated: item.markUseful(link=='true') - } - Kirigami.Separator { - visible: item.separator - Layout.fillWidth: true } } diff --git a/libdiscover/ReviewsBackend/ReviewsModel.cpp b/libdiscover/ReviewsBackend/ReviewsModel.cpp index 0210ee3b..548df69b 100644 --- a/libdiscover/ReviewsBackend/ReviewsModel.cpp +++ b/libdiscover/ReviewsBackend/ReviewsModel.cpp @@ -1,181 +1,184 @@ /*************************************************************************** * Copyright © 2012 Aleix Pol Gonzalez * * * * This program 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) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program 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 program. If not, see . * ***************************************************************************/ #include #include #include #include #include #include "libdiscover_debug.h" ReviewsModel::ReviewsModel(QObject* parent) : QAbstractListModel(parent) , m_app(nullptr) , m_backend(nullptr) , m_lastPage(0) , m_canFetchMore(true) {} ReviewsModel::~ReviewsModel() = default; QHash< int, QByteArray > ReviewsModel::roleNames() const { QHash roles = QAbstractItemModel::roleNames(); roles.insert(ShouldShow, "shouldShow"); roles.insert(Reviewer, "reviewer"); roles.insert(CreationDate, "date"); roles.insert(UsefulnessTotal, "usefulnessTotal"); roles.insert(UsefulnessFavorable, "usefulnessFavorable"); roles.insert(UsefulChoice, "usefulChoice"); roles.insert(Rating, "rating"); roles.insert(Summary, "summary"); + roles.insert(Depth, "depth"); return roles; } QVariant ReviewsModel::data(const QModelIndex& index, int role) const { if(!index.isValid()) return QVariant(); switch(role) { case Qt::DisplayRole: return m_reviews.at(index.row())->reviewText(); case ShouldShow: return m_reviews.at(index.row())->shouldShow(); case Reviewer: return m_reviews.at(index.row())->reviewer(); case CreationDate: return m_reviews.at(index.row())->creationDate(); case UsefulnessTotal: return m_reviews.at(index.row())->usefulnessTotal(); case UsefulnessFavorable: return m_reviews.at(index.row())->usefulnessFavorable(); case UsefulChoice: return m_reviews.at(index.row())->usefulChoice(); case Rating: return m_reviews.at(index.row())->rating(); case Summary: return m_reviews.at(index.row())->summary(); + case Depth: + return m_reviews.at(index.row())->getMetadata(QStringLiteral("NumberOfParents")).toInt(); } return QVariant(); } int ReviewsModel::rowCount(const QModelIndex& parent) const { if(parent.isValid()) return 0; return m_reviews.count(); } AbstractResource* ReviewsModel::resource() const { return m_app; } AbstractReviewsBackend* ReviewsModel::backend() const { return m_backend; } void ReviewsModel::setResource(AbstractResource* app) { if(m_app!=app) { beginResetModel(); m_reviews.clear(); m_lastPage = 0; if(m_backend) { disconnect(m_backend, &AbstractReviewsBackend::reviewsReady, this, &ReviewsModel::addReviews); } m_app = app; m_backend = app ? app->backend()->reviewsBackend() : nullptr; if(m_backend) { connect(m_backend, &AbstractReviewsBackend::reviewsReady, this, &ReviewsModel::addReviews); QMetaObject::invokeMethod(this, "restartFetching", Qt::QueuedConnection); } endResetModel(); emit rowsChanged(); emit resourceChanged(); } } void ReviewsModel::restartFetching() { if(!m_app || !m_backend) return; m_canFetchMore=true; m_lastPage = 0; fetchMore(); emit rowsChanged(); } void ReviewsModel::fetchMore(const QModelIndex& parent) { if(!m_backend || !m_app || parent.isValid() || m_backend->isFetching() || !m_canFetchMore) return; m_lastPage++; m_backend->fetchReviews(m_app, m_lastPage); // qCDebug(LIBDISCOVER_LOG) << "fetching reviews... " << m_lastPage; } void ReviewsModel::addReviews(AbstractResource* app, const QVector& reviews, bool canFetchMore) { if(app!=m_app) return; m_canFetchMore = canFetchMore; // qCDebug(LIBDISCOVER_LOG) << "reviews arrived..." << m_lastPage << reviews.size(); if(!reviews.isEmpty()) { beginInsertRows(QModelIndex(), rowCount(), rowCount()+reviews.size()-1); m_reviews += reviews; endInsertRows(); emit rowsChanged(); } } bool ReviewsModel::canFetchMore(const QModelIndex& /*parent*/) const { return m_canFetchMore; } void ReviewsModel::markUseful(int row, bool useful) { Review* r = m_reviews[row].data(); r->setUsefulChoice(useful ? Yes : No); // qCDebug(LIBDISCOVER_LOG) << "submitting usefulness" << r->applicationName() << r->id() << useful; m_backend->submitUsefulness(r, useful); const QModelIndex ind = index(row, 0, QModelIndex()); emit dataChanged(ind, ind, {UsefulnessTotal, UsefulnessFavorable, UsefulChoice}); } void ReviewsModel::deleteReview(int row) { Review* r = m_reviews[row].data(); m_backend->deleteReview(r); } void ReviewsModel::flagReview(int row, const QString& reason, const QString& text) { Review* r = m_reviews[row].data(); m_backend->flagReview(r, reason, text); } diff --git a/libdiscover/ReviewsBackend/ReviewsModel.h b/libdiscover/ReviewsBackend/ReviewsModel.h index c3ba337f..12c195e3 100644 --- a/libdiscover/ReviewsBackend/ReviewsModel.h +++ b/libdiscover/ReviewsBackend/ReviewsModel.h @@ -1,91 +1,92 @@ /*************************************************************************** * Copyright © 2012 Aleix Pol Gonzalez * * * * This program 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) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program 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 program. If not, see . * ***************************************************************************/ #ifndef REVIEWSMODEL_H #define REVIEWSMODEL_H #include #include #include "discovercommon_export.h" class Review; typedef QSharedPointer ReviewPtr; class AbstractResource; class AbstractReviewsBackend; class DISCOVERCOMMON_EXPORT ReviewsModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(AbstractReviewsBackend* backend READ backend NOTIFY resourceChanged) Q_PROPERTY(AbstractResource* resource READ resource WRITE setResource NOTIFY resourceChanged) Q_PROPERTY(int count READ rowCount NOTIFY rowsChanged) public: enum Roles { ShouldShow=Qt::UserRole+1, Reviewer, CreationDate, UsefulnessTotal, UsefulnessFavorable, UsefulChoice, Rating, - Summary + Summary, + Depth }; enum UserChoice { None, Yes, No }; Q_ENUM(UserChoice) explicit ReviewsModel(QObject* parent = nullptr); ~ReviewsModel() override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; AbstractReviewsBackend* backend() const; void setResource(AbstractResource* app); AbstractResource* resource() const; void fetchMore(const QModelIndex& parent=QModelIndex()) override; bool canFetchMore(const QModelIndex& /*parent*/) const override; QHash roleNames() const override; public Q_SLOTS: void deleteReview(int row); void flagReview(int row, const QString& reason, const QString& text); void markUseful(int row, bool useful); private Q_SLOTS: void addReviews(AbstractResource* app, const QVector& reviews, bool canFetchMore); void restartFetching(); Q_SIGNALS: void rowsChanged(); void resourceChanged(); private: AbstractResource* m_app; AbstractReviewsBackend* m_backend; QVector m_reviews; int m_lastPage; bool m_canFetchMore; }; #endif // REVIEWSMODEL_H diff --git a/libdiscover/backends/KNSBackend/KNSReviews.cpp b/libdiscover/backends/KNSBackend/KNSReviews.cpp index 14a690d0..68a0c7e2 100644 --- a/libdiscover/backends/KNSBackend/KNSReviews.cpp +++ b/libdiscover/backends/KNSBackend/KNSReviews.cpp @@ -1,183 +1,191 @@ /*************************************************************************** * Copyright © 2012 Aleix Pol Gonzalez * * * * This program 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) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program 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 program. If not, see . * ***************************************************************************/ #include "KNSReviews.h" #include "KNSBackend.h" #include "KNSResource.h" #include #include #include #include #include #include #include #include #include #include class SharedManager : public QObject { Q_OBJECT public: SharedManager() { atticaManager.loadDefaultProviders(); } public: Attica::ProviderManager atticaManager; }; Q_GLOBAL_STATIC(SharedManager, s_shared) KNSReviews::KNSReviews(KNSBackend* backend) : AbstractReviewsBackend(backend) , m_backend(backend) { } Rating* KNSReviews::ratingForApplication(AbstractResource* app) const { KNSResource *resource = qobject_cast(app); if (!resource) { qDebug() << app->packageName() << "<= couldn't find resource"; return nullptr; } return resource->ratingInstance(); } void KNSReviews::fetchReviews(AbstractResource* app, int page) { Attica::ListJob< Attica::Comment >* job = provider().requestComments(Attica::Comment::ContentComment, app->packageName(), QStringLiteral("0"), page - 1, 10); if (!job) { emit reviewsReady(app, {}, false); return; } job->setProperty("app", QVariant::fromValue(app)); connect(job, &Attica::BaseJob::finished, this, &KNSReviews::commentsReceived); job->start(); m_fetching++; } -void KNSReviews::commentsReceived(Attica::BaseJob* j) -{ - m_fetching--; - Attica::ListJob* job = static_cast*>(j); - Attica::Comment::List comments = job->itemList(); - +static QVector createReviewList(AbstractResource* app, Attica::Comment::List comments, int depth = 0) { QVector reviews; - AbstractResource* app = job->property("app").value(); foreach(const Attica::Comment& comment, comments) { //TODO: language lookup? ReviewPtr r(new Review(app->name(), app->packageName(), QStringLiteral("en"), comment.subject(), comment.text(), comment.user(), comment.date(), true, comment.id().toInt(), comment.score()/10, 0, 0, QString() )); + r->addMetadata(QStringLiteral("NumberOfParents"), depth); reviews += r; + if (comment.childCount() > 0) { + reviews += createReviewList(app, comment.children(), depth + 1); + } } + return reviews; +} + +void KNSReviews::commentsReceived(Attica::BaseJob* j) +{ + m_fetching--; + Attica::ListJob* job = static_cast*>(j); + + AbstractResource* app = job->property("app").value(); + QVector reviews = createReviewList(app, job->itemList()); emit reviewsReady(app, reviews, !reviews.isEmpty()); } bool KNSReviews::isFetching() const { return m_fetching > 0; } void KNSReviews::flagReview(Review* /*r*/, const QString& /*reason*/, const QString& /*text*/) { qWarning() << "cannot flag reviews"; } void KNSReviews::deleteReview(Review* /*r*/) { qWarning() << "cannot delete comments"; } void KNSReviews::submitReview(AbstractResource* app, const QString& summary, const QString& review_text, const QString& rating) { provider().voteForContent(app->packageName(), rating.toUInt() * 20); if (!summary.isEmpty()) provider().addNewComment(Attica::Comment::ContentComment, app->packageName(), QString(), QString(), summary, review_text); } void KNSReviews::submitUsefulness(Review* r, bool useful) { provider().voteForComment(QString::number(r->id()), useful*5); } void KNSReviews::logout() { bool b = provider().saveCredentials(QString(), QString()); if (!b) qWarning() << "couldn't log out"; } void KNSReviews::registerAndLogin() { QDesktopServices::openUrl(provider().baseUrl()); } void KNSReviews::login() { KPasswordDialog* dialog = new KPasswordDialog; dialog->setPrompt(i18n("Log in information for %1", provider().name())); connect(dialog, &KPasswordDialog::gotUsernameAndPassword, this, &KNSReviews::credentialsReceived); } void KNSReviews::credentialsReceived(const QString& user, const QString& password) { bool b = provider().saveCredentials(user, password); if (!b) qWarning() << "couldn't save" << user << "credentials for" << provider().name(); } bool KNSReviews::hasCredentials() const { return provider().hasCredentials(); } QString KNSReviews::userName() const { QString user, password; provider().loadCredentials(user, password); return user; } void KNSReviews::setProviderUrl(const QUrl& url) { m_providerUrl = url; if(!m_providerUrl.isEmpty() && !s_shared->atticaManager.providerFiles().contains(url)) { s_shared->atticaManager.addProviderFile(url); } } Attica::Provider KNSReviews::provider() const { return s_shared->atticaManager.providerFor(m_providerUrl); } bool KNSReviews::isResourceSupported(AbstractResource* res) const { return qobject_cast(res); } #include "KNSReviews.moc"