mirror of
https://github.com/zealdocs/zeal.git
synced 2026-08-31 01:20:55 +08:00
903 lines
30 KiB
C++
903 lines
30 KiB
C++
// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>
|
|
// Copyright (C) 2013-2014 Jerzy Kozera
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include "docsetsdialog.h"
|
|
#include "ui_docsetsdialog.h"
|
|
|
|
#include "docsetlistitemdelegate.h"
|
|
|
|
#include <core/application.h>
|
|
#include <core/filemanager.h>
|
|
#include <core/settings.h>
|
|
#include <registry/docset.h>
|
|
#include <registry/docsetregistry.h>
|
|
#include <registry/itemdatarole.h>
|
|
#include <util/humanizer.h>
|
|
|
|
#include <QClipboard>
|
|
#include <QDateTime>
|
|
#include <QDir>
|
|
#include <QInputDialog>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QLocale>
|
|
#include <QLoggingCategory>
|
|
#include <QMessageBox>
|
|
#include <QNetworkReply>
|
|
#include <QNetworkRequest>
|
|
#include <QTemporaryFile>
|
|
#include <QUrl>
|
|
|
|
#include <memory>
|
|
|
|
namespace Zeal::WidgetUi {
|
|
|
|
namespace {
|
|
Q_LOGGING_CATEGORY(log, "zeal.widgetui.docsetsdialog")
|
|
|
|
enum DownloadType {
|
|
DownloadDashFeed,
|
|
DownloadDocset,
|
|
DownloadDocsetList
|
|
};
|
|
|
|
constexpr char ApiServerUrl[] = "https://api.zealdocs.org/v1";
|
|
constexpr char RedirectServerUrl[] = "https://go.zealdocs.org/d/%1/%2/latest";
|
|
// TODO: Each source plugin should have its own cache
|
|
constexpr char DocsetListCacheFileName[] = "com.kapeli.json";
|
|
|
|
// TODO: Make the timeout period configurable
|
|
constexpr int CacheTimeout = 24 * 60 * 60; // 24 hours in seconds
|
|
|
|
// QNetworkReply properties
|
|
constexpr char DocsetNameProperty[] = "docsetName";
|
|
constexpr char DownloadTypeProperty[] = "downloadType";
|
|
constexpr char ListItemIndexProperty[] = "listItem";
|
|
} // namespace
|
|
|
|
DocsetsDialog::DocsetsDialog(Core::Application *app, QWidget *parent)
|
|
: QDialog(parent)
|
|
, ui(new Ui::DocsetsDialog())
|
|
, m_application(app)
|
|
, m_docsetRegistry(app->docsetRegistry())
|
|
{
|
|
ui->setupUi(this);
|
|
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
|
|
|
loadDocsetList();
|
|
|
|
m_isStorageReadOnly = !isDirWritable(m_application->settings()->docsetPath);
|
|
|
|
#ifdef Q_OS_MACOS
|
|
ui->availableDocsetList->setAttribute(Qt::WA_MacShowFocusRect, false);
|
|
ui->installedDocsetList->setAttribute(Qt::WA_MacShowFocusRect, false);
|
|
#endif
|
|
|
|
ui->statusLabel->clear(); // Clear text shown in the designer mode.
|
|
ui->storageStatusLabel->setVisible(m_isStorageReadOnly);
|
|
|
|
const QFileInfo fi(m_application->settings()->docsetPath);
|
|
ui->storageStatusLabel->setText(fi.exists() ? tr("<b>Docset storage is read only.</b>")
|
|
: tr("<b>Docset storage does not exist.</b>"));
|
|
|
|
connect(m_application, &Core::Application::extractionCompleted, this, &DocsetsDialog::extractionCompleted);
|
|
connect(m_application, &Core::Application::extractionError, this, &DocsetsDialog::extractionError);
|
|
connect(m_application, &Core::Application::extractionProgress, this, &DocsetsDialog::extractionProgress);
|
|
|
|
// Setup signals & slots
|
|
connect(ui->buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton *button) {
|
|
if (button == ui->buttonBox->button(QDialogButtonBox::Cancel)) {
|
|
cancelDownloads();
|
|
return;
|
|
}
|
|
|
|
if (button == ui->buttonBox->button(QDialogButtonBox::Close)) {
|
|
close();
|
|
return;
|
|
}
|
|
});
|
|
|
|
setupInstalledDocsetsTab();
|
|
setupAvailableDocsetsTab();
|
|
|
|
if (m_isStorageReadOnly) {
|
|
disableControls();
|
|
}
|
|
}
|
|
|
|
DocsetsDialog::~DocsetsDialog()
|
|
{
|
|
delete ui;
|
|
}
|
|
|
|
void DocsetsDialog::addDashFeed()
|
|
{
|
|
QString clipboardText = QApplication::clipboard()->text();
|
|
if (!clipboardText.startsWith(QLatin1String("dash-feed://"))) {
|
|
clipboardText.clear();
|
|
}
|
|
|
|
QString feedUrl = QInputDialog::getText(this,
|
|
QStringLiteral("Zeal"),
|
|
tr("Feed URL:"),
|
|
QLineEdit::Normal,
|
|
clipboardText);
|
|
feedUrl = feedUrl.trimmed();
|
|
if (feedUrl.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
if (feedUrl.startsWith(QLatin1String("dash-feed://"))) {
|
|
feedUrl = feedUrl.remove(0, 12);
|
|
feedUrl = QUrl::fromPercentEncoding(feedUrl.toUtf8());
|
|
}
|
|
|
|
QNetworkReply *reply = download(QUrl(feedUrl));
|
|
reply->setProperty(DownloadTypeProperty, DownloadType::DownloadDashFeed);
|
|
}
|
|
|
|
void DocsetsDialog::updateSelectedDocsets()
|
|
{
|
|
const auto selectedRows = ui->installedDocsetList->selectionModel()->selectedRows();
|
|
for (const QModelIndex &index : selectedRows) {
|
|
if (!index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) {
|
|
continue;
|
|
}
|
|
|
|
downloadDashDocset(index);
|
|
}
|
|
}
|
|
|
|
void DocsetsDialog::updateAllDocsets()
|
|
{
|
|
QAbstractItemModel *model = ui->installedDocsetList->model();
|
|
for (int i = 0; i < model->rowCount(); ++i) {
|
|
const QModelIndex index = model->index(i, 0);
|
|
if (!index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) {
|
|
continue;
|
|
}
|
|
|
|
downloadDashDocset(index);
|
|
}
|
|
}
|
|
|
|
void DocsetsDialog::removeSelectedDocsets()
|
|
{
|
|
QItemSelectionModel *selectionModel = ui->installedDocsetList->selectionModel();
|
|
if (!selectionModel->hasSelection()) {
|
|
return;
|
|
}
|
|
|
|
int ret;
|
|
|
|
const QModelIndexList selectedIndexes = selectionModel->selectedRows();
|
|
if (selectedIndexes.size() == 1) {
|
|
const QString docsetTitle = selectedIndexes.first().data().toString();
|
|
ret = QMessageBox::question(this, QStringLiteral("Zeal"), tr("Remove <b>%1</b> docset?").arg(docsetTitle));
|
|
} else {
|
|
ret = QMessageBox::question(this,
|
|
QStringLiteral("Zeal"),
|
|
tr("Remove <b>%n</b> docset(s)?", nullptr, selectedIndexes.size()));
|
|
}
|
|
|
|
if (ret == QMessageBox::No) {
|
|
return;
|
|
}
|
|
|
|
// Gather names first, because model indicies become invalid when docsets are removed.
|
|
QStringList names;
|
|
for (const QModelIndex &index : selectedIndexes) {
|
|
names.append(index.data(Registry::ItemDataRole::DocsetNameRole).toString());
|
|
}
|
|
|
|
for (const QString &name : names) {
|
|
removeDocset(name);
|
|
}
|
|
}
|
|
|
|
void DocsetsDialog::updateDocsetFilter(const QString &filterString)
|
|
{
|
|
const bool doSearch = !filterString.simplified().isEmpty();
|
|
|
|
for (int i = 0; i < ui->availableDocsetList->count(); ++i) {
|
|
QListWidgetItem *item = ui->availableDocsetList->item(i);
|
|
|
|
// Skip installed docsets
|
|
if (m_docsetRegistry->contains(item->data(Registry::ItemDataRole::DocsetNameRole).toString())) {
|
|
continue;
|
|
}
|
|
|
|
item->setHidden(doSearch && !item->text().contains(filterString, Qt::CaseInsensitive));
|
|
}
|
|
}
|
|
|
|
void DocsetsDialog::downloadSelectedDocsets()
|
|
{
|
|
QItemSelectionModel *selectionModel = ui->availableDocsetList->selectionModel();
|
|
const auto selectedRows = selectionModel->selectedRows();
|
|
for (const QModelIndex &index : selectedRows) {
|
|
selectionModel->select(index, QItemSelectionModel::Deselect);
|
|
|
|
// Do nothing if a download is already in progress.
|
|
if (index.data(DocsetListItemDelegate::ShowProgressRole).toBool()) {
|
|
continue;
|
|
}
|
|
|
|
QAbstractItemModel *model = ui->availableDocsetList->model();
|
|
model->setData(index, tr("Downloading: %p%"), DocsetListItemDelegate::FormatRole);
|
|
model->setData(index, 0, DocsetListItemDelegate::ValueRole);
|
|
model->setData(index, true, DocsetListItemDelegate::ShowProgressRole);
|
|
|
|
downloadDashDocset(index);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
\internal
|
|
Should be connected to all \l QNetworkReply::finished signals in order to process possible
|
|
HTTP-redirects correctly.
|
|
*/
|
|
void DocsetsDialog::downloadCompleted()
|
|
{
|
|
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(qobject_cast<QNetworkReply *>(sender()));
|
|
|
|
m_replies.removeOne(reply.data());
|
|
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
if (reply->error() != QNetworkReply::OperationCanceledError) {
|
|
const QString msg = tr("Download failed!<br><br><b>Error:</b> %1<br><b>URL:</b> %2")
|
|
.arg(reply->errorString(), reply->request().url().toString());
|
|
const int ret = QMessageBox::warning(this,
|
|
QStringLiteral("Zeal"),
|
|
msg,
|
|
QMessageBox::Retry | QMessageBox::Cancel);
|
|
|
|
if (ret == QMessageBox::Retry) {
|
|
QNetworkReply *newReply = download(reply->request().url());
|
|
|
|
// Copy properties
|
|
newReply->setProperty(DocsetNameProperty, reply->property(DocsetNameProperty));
|
|
newReply->setProperty(DownloadTypeProperty, reply->property(DownloadTypeProperty));
|
|
newReply->setProperty(ListItemIndexProperty, reply->property(ListItemIndexProperty));
|
|
return;
|
|
}
|
|
|
|
bool ok;
|
|
QListWidgetItem *listItem = ui->availableDocsetList->item(
|
|
reply->property(ListItemIndexProperty).toInt(&ok));
|
|
if (ok && listItem) {
|
|
listItem->setData(DocsetListItemDelegate::ShowProgressRole, false);
|
|
}
|
|
}
|
|
|
|
updateStatus();
|
|
return;
|
|
}
|
|
|
|
switch (reply->property(DownloadTypeProperty).toUInt()) {
|
|
case DownloadType::DownloadDocsetList: {
|
|
const QByteArray replyData = reply->readAll();
|
|
|
|
QScopedPointer<QFile> file(new QFile(cacheLocation(DocsetListCacheFileName)));
|
|
if (file->open(QIODevice::WriteOnly)) {
|
|
file->write(replyData);
|
|
file->close(); // Flush to ensure timestamp update on all systems.
|
|
}
|
|
|
|
updateDocsetListDownloadTimeLabel(QFileInfo(file->fileName()).lastModified());
|
|
|
|
QJsonParseError jsonError;
|
|
const QJsonDocument jsonDoc = QJsonDocument::fromJson(replyData, &jsonError);
|
|
|
|
if (jsonError.error != QJsonParseError::NoError) {
|
|
qCWarning(log,
|
|
"Failed to parse docset list JSON at offset %lld: %s.",
|
|
static_cast<long long>(jsonError.offset),
|
|
qPrintable(jsonError.errorString()));
|
|
const QMessageBox::StandardButton rc = QMessageBox::warning(this,
|
|
QStringLiteral("Zeal"),
|
|
tr("Server returned a corrupted docset list."),
|
|
QMessageBox::Retry | QMessageBox::Cancel);
|
|
|
|
if (rc == QMessageBox::Retry) {
|
|
downloadDocsetList();
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
processDocsetList(jsonDoc.array());
|
|
break;
|
|
}
|
|
|
|
case DownloadType::DownloadDashFeed: {
|
|
Registry::DocsetMetadata metadata = Registry::DocsetMetadata::fromDashFeed(reply->request().url(),
|
|
reply->readAll());
|
|
|
|
if (metadata.urls().isEmpty()) {
|
|
QMessageBox::warning(this, QStringLiteral("Zeal"), tr("Invalid docset feed!"));
|
|
break;
|
|
}
|
|
|
|
m_userFeeds[metadata.name()] = metadata;
|
|
Registry::Docset *docset = m_docsetRegistry->docset(metadata.name());
|
|
if (docset == nullptr) {
|
|
// Fetch docset only on first feed download,
|
|
// since further downloads are only update checks
|
|
QNetworkReply *mdReply = download(metadata.url());
|
|
mdReply->setProperty(DocsetNameProperty, metadata.name());
|
|
mdReply->setProperty(DownloadTypeProperty, DownloadType::DownloadDocset);
|
|
} else {
|
|
// Check for feed update
|
|
if (metadata.latestVersion() != docset->version() || metadata.revision() > docset->revision()) {
|
|
docset->hasUpdate = true;
|
|
|
|
if (!m_isStorageReadOnly) {
|
|
ui->updateAllDocsetsButton->setEnabled(true);
|
|
}
|
|
|
|
ui->installedDocsetList->reset();
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case DownloadType::DownloadDocset: {
|
|
const QString docsetName = reply->property(DocsetNameProperty).toString();
|
|
const QString docsetDirectoryName = docsetName + QLatin1String(".docset");
|
|
|
|
if (QDir(m_application->settings()->docsetPath).exists(docsetDirectoryName)) {
|
|
removeDocset(docsetName);
|
|
}
|
|
|
|
QTemporaryFile *tmpFile = m_tmpFiles[docsetName];
|
|
if (!tmpFile) {
|
|
tmpFile = new QTemporaryFile(QStringLiteral("%1/%2.XXXXXX.tmp")
|
|
.arg(Core::Application::cacheLocation(), docsetName),
|
|
this);
|
|
if (!tmpFile->open()) {
|
|
return;
|
|
}
|
|
m_tmpFiles.insert(docsetName, tmpFile);
|
|
}
|
|
|
|
while (reply->bytesAvailable()) {
|
|
tmpFile->write(reply->read(1024 * 1024)); // Use small chunks.
|
|
}
|
|
|
|
tmpFile->close();
|
|
|
|
QListWidgetItem *item = findDocsetListItem(docsetName);
|
|
if (item) {
|
|
item->setData(DocsetListItemDelegate::ValueRole, 0);
|
|
item->setData(DocsetListItemDelegate::FormatRole, tr("Installing: %p%"));
|
|
}
|
|
|
|
m_application->extract(tmpFile->fileName(), m_application->settings()->docsetPath, docsetDirectoryName);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If all enqueued downloads have finished executing.
|
|
updateStatus();
|
|
}
|
|
|
|
// creates a total download progress for multiple QNetworkReplies
|
|
void DocsetsDialog::downloadProgress(qint64 received, qint64 total)
|
|
{
|
|
// Don't show progress for non-docset pages
|
|
if (total == -1 || received < 10240) {
|
|
return;
|
|
}
|
|
|
|
auto *reply = qobject_cast<QNetworkReply *>(sender());
|
|
if (!reply || !reply->isOpen()) {
|
|
return;
|
|
}
|
|
|
|
if (reply->property(DownloadTypeProperty).toInt() == DownloadType::DownloadDocset) {
|
|
const QString docsetName = reply->property(DocsetNameProperty).toString();
|
|
|
|
QTemporaryFile *tmpFile = m_tmpFiles[docsetName];
|
|
if (!tmpFile) {
|
|
tmpFile = new QTemporaryFile(QStringLiteral("%1/%2.XXXXXX.tmp")
|
|
.arg(Core::Application::cacheLocation(), docsetName),
|
|
this);
|
|
if (!tmpFile->open()) {
|
|
return;
|
|
}
|
|
m_tmpFiles.insert(docsetName, tmpFile);
|
|
}
|
|
|
|
tmpFile->write(reply->read(received));
|
|
}
|
|
|
|
// Try to get the item associated to the request
|
|
QListWidgetItem *item = ui->availableDocsetList->item(reply->property(ListItemIndexProperty).toInt());
|
|
if (item) {
|
|
item->setData(DocsetListItemDelegate::ValueRole, percent(received, total));
|
|
}
|
|
}
|
|
|
|
void DocsetsDialog::extractionCompleted(const QString &filePath)
|
|
{
|
|
const QString docsetName = docsetNameForTmpFilePath(filePath);
|
|
|
|
const QDir dataDir(m_application->settings()->docsetPath);
|
|
const QString docsetPath = dataDir.filePath(docsetName + QLatin1String(".docset"));
|
|
|
|
// Write metadata about docset
|
|
Registry::DocsetMetadata metadata = m_availableDocsets.count(docsetName) ? m_availableDocsets[docsetName]
|
|
: m_userFeeds[docsetName];
|
|
metadata.save(docsetPath, metadata.latestVersion());
|
|
|
|
m_docsetRegistry->loadDocset(docsetPath);
|
|
|
|
QListWidgetItem *listItem = findDocsetListItem(docsetName);
|
|
if (listItem) {
|
|
listItem->setHidden(true);
|
|
listItem->setData(DocsetListItemDelegate::ShowProgressRole, false);
|
|
}
|
|
|
|
delete m_tmpFiles.take(docsetName);
|
|
|
|
updateStatus();
|
|
}
|
|
|
|
void DocsetsDialog::extractionError(const QString &filePath, const QString &errorString)
|
|
{
|
|
const QString docsetName = docsetNameForTmpFilePath(filePath);
|
|
|
|
QMessageBox::warning(this,
|
|
QStringLiteral("Zeal"),
|
|
tr("Cannot extract docset <b>%1</b>: %2").arg(docsetName, errorString));
|
|
|
|
QListWidgetItem *listItem = findDocsetListItem(docsetName);
|
|
if (listItem) {
|
|
listItem->setData(DocsetListItemDelegate::ShowProgressRole, false);
|
|
}
|
|
|
|
delete m_tmpFiles.take(docsetName);
|
|
}
|
|
|
|
void DocsetsDialog::extractionProgress(const QString &filePath, qint64 extracted, qint64 total)
|
|
{
|
|
const QString docsetName = docsetNameForTmpFilePath(filePath);
|
|
QListWidgetItem *listItem = findDocsetListItem(docsetName);
|
|
if (listItem) {
|
|
listItem->setData(DocsetListItemDelegate::ValueRole, percent(extracted, total));
|
|
}
|
|
}
|
|
|
|
void DocsetsDialog::loadDocsetList()
|
|
{
|
|
loadUserFeedList();
|
|
|
|
const QFileInfo fi(cacheLocation(DocsetListCacheFileName));
|
|
if (!fi.exists()) {
|
|
downloadDocsetList();
|
|
return;
|
|
}
|
|
|
|
const auto age = fi.lastModified().secsTo(QDateTime::currentDateTime());
|
|
if (age < 0 || age >= CacheTimeout) {
|
|
downloadDocsetList();
|
|
return;
|
|
}
|
|
|
|
QScopedPointer<QFile> file(new QFile(fi.filePath()));
|
|
if (!file->open(QIODevice::ReadOnly)) {
|
|
downloadDocsetList();
|
|
return;
|
|
}
|
|
|
|
QJsonParseError jsonError;
|
|
const QJsonDocument jsonDoc = QJsonDocument::fromJson(file->readAll(), &jsonError);
|
|
|
|
if (jsonError.error != QJsonParseError::NoError) {
|
|
downloadDocsetList();
|
|
return;
|
|
}
|
|
|
|
updateDocsetListDownloadTimeLabel(fi.lastModified());
|
|
processDocsetList(jsonDoc.array());
|
|
}
|
|
|
|
void DocsetsDialog::setupInstalledDocsetsTab()
|
|
{
|
|
ui->installedDocsetList->setItemDelegate(new DocsetListItemDelegate(this));
|
|
ui->installedDocsetList->setModel(m_docsetRegistry->model());
|
|
|
|
ui->installedDocsetList->setItemsExpandable(false);
|
|
ui->installedDocsetList->setRootIsDecorated(false);
|
|
|
|
ui->installedDocsetList->header()->setStretchLastSection(true);
|
|
ui->installedDocsetList->header()->setSectionsMovable(false);
|
|
ui->installedDocsetList->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
|
ui->installedDocsetList->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
|
|
|
if (m_isStorageReadOnly) {
|
|
return;
|
|
}
|
|
|
|
connect(ui->installedDocsetList, &QTreeView::activated, this, [this](const QModelIndex &index) {
|
|
if (!index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) {
|
|
return;
|
|
}
|
|
|
|
downloadDashDocset(index);
|
|
});
|
|
|
|
QItemSelectionModel *selectionModel = ui->installedDocsetList->selectionModel();
|
|
connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this, selectionModel]() {
|
|
ui->removeDocsetsButton->setEnabled(selectionModel->hasSelection());
|
|
|
|
const auto selectedRows = selectionModel->selectedRows();
|
|
for (const QModelIndex &index : selectedRows) {
|
|
if (index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) {
|
|
ui->updateSelectedDocsetsButton->setEnabled(true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
ui->updateSelectedDocsetsButton->setEnabled(false);
|
|
});
|
|
|
|
connect(ui->addFeedButton, &QPushButton::clicked, this, &DocsetsDialog::addDashFeed);
|
|
|
|
connect(ui->updateSelectedDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::updateSelectedDocsets);
|
|
connect(ui->updateAllDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::updateAllDocsets);
|
|
connect(ui->removeDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::removeSelectedDocsets);
|
|
}
|
|
|
|
void DocsetsDialog::setupAvailableDocsetsTab()
|
|
{
|
|
using Registry::DocsetRegistry;
|
|
|
|
ui->availableDocsetList->setItemDelegate(new DocsetListItemDelegate(this));
|
|
|
|
connect(m_docsetRegistry, &DocsetRegistry::docsetUnloaded, this, [this](const QString &name) {
|
|
QListWidgetItem *item = findDocsetListItem(name);
|
|
if (!item) {
|
|
return;
|
|
}
|
|
|
|
item->setHidden(false);
|
|
});
|
|
connect(m_docsetRegistry, &DocsetRegistry::docsetLoaded, this, [this](const QString &name) {
|
|
QListWidgetItem *item = findDocsetListItem(name);
|
|
if (!item) {
|
|
return;
|
|
}
|
|
|
|
item->setHidden(true);
|
|
});
|
|
|
|
connect(ui->refreshButton, &QPushButton::clicked, this, &DocsetsDialog::downloadDocsetList);
|
|
|
|
connect(ui->docsetFilterInput, &QLineEdit::textEdited, this, &DocsetsDialog::updateDocsetFilter);
|
|
|
|
if (m_isStorageReadOnly) {
|
|
return;
|
|
}
|
|
|
|
connect(ui->availableDocsetList, &QListView::activated, this, [this](const QModelIndex &index) {
|
|
// TODO: Cancel download if it's already in progress.
|
|
if (index.data(DocsetListItemDelegate::ShowProgressRole).toBool()) {
|
|
return;
|
|
}
|
|
|
|
ui->availableDocsetList->selectionModel()->select(index, QItemSelectionModel::Deselect);
|
|
|
|
QAbstractItemModel *model = ui->availableDocsetList->model();
|
|
model->setData(index, tr("Downloading: %p%"), DocsetListItemDelegate::FormatRole);
|
|
model->setData(index, 0, DocsetListItemDelegate::ValueRole);
|
|
model->setData(index, true, DocsetListItemDelegate::ShowProgressRole);
|
|
|
|
downloadDashDocset(index);
|
|
});
|
|
|
|
QItemSelectionModel *selectionModel = ui->availableDocsetList->selectionModel();
|
|
connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this, selectionModel]() {
|
|
const auto selectedRows = selectionModel->selectedRows();
|
|
for (const QModelIndex &index : selectedRows) {
|
|
if (!index.data(DocsetListItemDelegate::ShowProgressRole).toBool()) {
|
|
ui->downloadDocsetsButton->setEnabled(true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
ui->downloadDocsetsButton->setEnabled(false);
|
|
});
|
|
|
|
connect(ui->downloadDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::downloadSelectedDocsets);
|
|
}
|
|
|
|
void DocsetsDialog::enableControls()
|
|
{
|
|
if (m_isStorageReadOnly || !m_replies.isEmpty() || !m_tmpFiles.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// Dialog buttons.
|
|
ui->buttonBox->setStandardButtons(QDialogButtonBox::Close);
|
|
|
|
// Available docsets
|
|
ui->refreshButton->setEnabled(true);
|
|
|
|
// Installed docsets
|
|
ui->addFeedButton->setEnabled(true);
|
|
QItemSelectionModel *selectionModel = ui->installedDocsetList->selectionModel();
|
|
bool hasSelectedUpdates = false;
|
|
const auto selectedRows = selectionModel->selectedRows();
|
|
for (const QModelIndex &index : selectedRows) {
|
|
if (index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) {
|
|
hasSelectedUpdates = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ui->updateSelectedDocsetsButton->setEnabled(hasSelectedUpdates);
|
|
ui->updateAllDocsetsButton->setEnabled(updatesAvailable());
|
|
ui->removeDocsetsButton->setEnabled(selectionModel->hasSelection());
|
|
}
|
|
|
|
void DocsetsDialog::disableControls()
|
|
{
|
|
// Dialog buttons.
|
|
if (!m_isStorageReadOnly) {
|
|
// Always show the close button if storage is read only.
|
|
ui->buttonBox->setStandardButtons(QDialogButtonBox::Cancel);
|
|
}
|
|
|
|
// Installed docsets
|
|
ui->addFeedButton->setEnabled(false);
|
|
ui->updateSelectedDocsetsButton->setEnabled(false);
|
|
ui->updateAllDocsetsButton->setEnabled(false);
|
|
ui->downloadDocsetsButton->setEnabled(false);
|
|
ui->removeDocsetsButton->setEnabled(false);
|
|
|
|
// Available docsets
|
|
ui->refreshButton->setEnabled(false);
|
|
}
|
|
|
|
QListWidgetItem *DocsetsDialog::findDocsetListItem(const QString &name) const
|
|
{
|
|
for (int i = 0; i < ui->availableDocsetList->count(); ++i) {
|
|
QListWidgetItem *item = ui->availableDocsetList->item(i);
|
|
|
|
if (item->data(Registry::ItemDataRole::DocsetNameRole).toString() == name) {
|
|
return item;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool DocsetsDialog::updatesAvailable() const
|
|
{
|
|
const auto docsets = m_docsetRegistry->docsets();
|
|
for (Registry::Docset *docset : docsets) {
|
|
if (docset->hasUpdate) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
QNetworkReply *DocsetsDialog::download(const QUrl &url)
|
|
{
|
|
QNetworkReply *reply = m_application->download(url);
|
|
connect(reply, &QNetworkReply::downloadProgress, this, &DocsetsDialog::downloadProgress);
|
|
connect(reply, &QNetworkReply::finished, this, &DocsetsDialog::downloadCompleted);
|
|
m_replies.append(reply);
|
|
|
|
disableControls();
|
|
updateStatus();
|
|
|
|
return reply;
|
|
}
|
|
|
|
void DocsetsDialog::cancelDownloads()
|
|
{
|
|
for (QNetworkReply *reply : std::as_const(m_replies)) {
|
|
// Hide progress bar
|
|
QListWidgetItem *listItem = ui->availableDocsetList->item(reply->property(ListItemIndexProperty).toInt());
|
|
if (listItem) {
|
|
listItem->setData(DocsetListItemDelegate::ShowProgressRole, false);
|
|
}
|
|
|
|
if (reply->property(DownloadTypeProperty).toInt() == DownloadType::DownloadDocset) {
|
|
delete m_tmpFiles.take(reply->property(DocsetNameProperty).toString());
|
|
}
|
|
|
|
reply->abort();
|
|
}
|
|
|
|
updateStatus();
|
|
}
|
|
|
|
void DocsetsDialog::loadUserFeedList()
|
|
{
|
|
const auto docsets = m_docsetRegistry->docsets();
|
|
for (Registry::Docset *docset : docsets) {
|
|
if (!docset->feedUrl().isEmpty()) {
|
|
QNetworkReply *reply = download(QUrl(docset->feedUrl()));
|
|
reply->setProperty(DownloadTypeProperty, DownloadType::DownloadDashFeed);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DocsetsDialog::downloadDocsetList()
|
|
{
|
|
ui->availableDocsetList->clear();
|
|
m_availableDocsets.clear();
|
|
|
|
QNetworkReply *reply = download(QUrl(ApiServerUrl + QLatin1String("/docsets")));
|
|
reply->setProperty(DownloadTypeProperty, DownloadType::DownloadDocsetList);
|
|
}
|
|
|
|
void DocsetsDialog::processDocsetList(const QJsonArray &list)
|
|
{
|
|
for (const QJsonValue &v : list) {
|
|
QJsonObject docsetJson = v.toObject();
|
|
|
|
Registry::DocsetMetadata metadata(docsetJson);
|
|
m_availableDocsets.insert({metadata.name(), metadata});
|
|
}
|
|
|
|
// TODO: Move into dedicated method
|
|
for (const auto &kv : m_availableDocsets) {
|
|
const auto &metadata = kv.second;
|
|
|
|
auto *listItem = new QListWidgetItem(metadata.icon(), metadata.title(), ui->availableDocsetList);
|
|
listItem->setData(Registry::ItemDataRole::DocsetNameRole, metadata.name());
|
|
|
|
if (!m_docsetRegistry->contains(metadata.name())) {
|
|
continue;
|
|
}
|
|
|
|
listItem->setHidden(true);
|
|
|
|
Registry::Docset *docset = m_docsetRegistry->docset(metadata.name());
|
|
|
|
if (metadata.latestVersion() != docset->version() || metadata.revision() > docset->revision()) {
|
|
docset->hasUpdate = true;
|
|
|
|
if (!m_isStorageReadOnly) {
|
|
ui->updateAllDocsetsButton->setEnabled(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
ui->installedDocsetList->reset();
|
|
|
|
// Reapply the filter after repopulating the list.
|
|
updateDocsetFilter(ui->docsetFilterInput->text());
|
|
}
|
|
|
|
void DocsetsDialog::updateDocsetListDownloadTimeLabel(const QDateTime &modifiedTime)
|
|
{
|
|
if (!modifiedTime.isValid()) {
|
|
ui->lastUpdatedLabel->clear();
|
|
ui->lastUpdatedLabel->setToolTip(QString());
|
|
return;
|
|
}
|
|
|
|
ui->lastUpdatedLabel->setText(tr("Last updated %1.").arg(Util::Humanizer::fromNow(modifiedTime)));
|
|
|
|
const QString updateTime = modifiedTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
|
|
ui->lastUpdatedLabel->setToolTip(updateTime);
|
|
}
|
|
|
|
void DocsetsDialog::downloadDashDocset(const QModelIndex &index)
|
|
{
|
|
const QString name = index.data(Registry::ItemDataRole::DocsetNameRole).toString();
|
|
|
|
if (m_availableDocsets.count(name) == 0 && !m_userFeeds.contains(name)) {
|
|
return;
|
|
}
|
|
|
|
// Skip if an extraction is already in progress for this docset.
|
|
if (m_tmpFiles.contains(name)) {
|
|
return;
|
|
}
|
|
|
|
// Skip if a download is already in progress for this docset.
|
|
for (QNetworkReply *reply : std::as_const(m_replies)) {
|
|
if (reply->property(DownloadTypeProperty).toInt() == DownloadType::DownloadDocset
|
|
&& reply->property(DocsetNameProperty).toString() == name) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
QUrl url;
|
|
if (!m_userFeeds.contains(name)) {
|
|
// No feed present means that this is a Kapeli docset
|
|
QString urlString = QString(RedirectServerUrl).arg("com.kapeli").arg(name);
|
|
url = QUrl(urlString);
|
|
} else {
|
|
url = m_userFeeds[name].url();
|
|
}
|
|
|
|
QNetworkReply *reply = download(url);
|
|
reply->setProperty(DocsetNameProperty, name);
|
|
reply->setProperty(DownloadTypeProperty, DownloadType::DownloadDocset);
|
|
reply->setProperty(ListItemIndexProperty, ui->availableDocsetList->row(findDocsetListItem(name)));
|
|
}
|
|
|
|
void DocsetsDialog::removeDocset(const QString &name)
|
|
{
|
|
if (!m_docsetRegistry->contains(name)) {
|
|
return;
|
|
}
|
|
|
|
const QString docsetPath = m_docsetRegistry->docset(name)->path();
|
|
m_docsetRegistry->unloadDocset(name);
|
|
if (!m_application->fileManager()->removeRecursively(docsetPath)) {
|
|
const QString error = tr("Cannot remove directory <b>%1</b>! It might be in use"
|
|
" by another process.")
|
|
.arg(docsetPath);
|
|
QMessageBox::warning(this, QStringLiteral("Zeal"), error);
|
|
return;
|
|
}
|
|
|
|
QListWidgetItem *listItem = findDocsetListItem(name);
|
|
if (listItem) {
|
|
listItem->setHidden(false);
|
|
}
|
|
}
|
|
|
|
void DocsetsDialog::updateStatus()
|
|
{
|
|
QString text;
|
|
|
|
if (!m_replies.isEmpty()) {
|
|
text = tr("Downloading: %n.", nullptr, m_replies.size());
|
|
}
|
|
|
|
if (!m_tmpFiles.isEmpty()) {
|
|
text += QLatin1String(" ") + tr("Installing: %n.", nullptr, m_tmpFiles.size());
|
|
}
|
|
|
|
ui->statusLabel->setText(text);
|
|
|
|
enableControls();
|
|
}
|
|
|
|
QString DocsetsDialog::docsetNameForTmpFilePath(const QString &filePath) const
|
|
{
|
|
for (auto it = m_tmpFiles.cbegin(), end = m_tmpFiles.cend(); it != end; ++it) {
|
|
if (it.value()->fileName() == filePath) {
|
|
return it.key();
|
|
}
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
int DocsetsDialog::percent(qint64 fraction, qint64 total)
|
|
{
|
|
if (!total) {
|
|
return 0;
|
|
}
|
|
|
|
return static_cast<int>(fraction / static_cast<double>(total) * 100);
|
|
}
|
|
|
|
QString DocsetsDialog::cacheLocation(const QString &fileName)
|
|
{
|
|
return QDir(Core::Application::cacheLocation()).filePath(fileName);
|
|
}
|
|
|
|
bool DocsetsDialog::isDirWritable(const QString &path)
|
|
{
|
|
auto file = std::make_unique<QTemporaryFile>(path + QLatin1String("/.zeal_writable_check_XXXXXX.tmp"));
|
|
return file->open();
|
|
}
|
|
|
|
} // namespace Zeal::WidgetUi
|