diff --git a/libs/vtextedit b/libs/vtextedit index c53fc8db..ac58baf5 160000 --- a/libs/vtextedit +++ b/libs/vtextedit @@ -1 +1 @@ -Subproject commit c53fc8dbf6df14b9e1327de0a4907700ffee6049 +Subproject commit ac58baf5f07643f57a2a208629b7056c24516ffc diff --git a/src/core/buffer/markdownbuffer.cpp b/src/core/buffer/markdownbuffer.cpp index bd5b90b6..025a61cf 100644 --- a/src/core/buffer/markdownbuffer.cpp +++ b/src/core/buffer/markdownbuffer.cpp @@ -35,10 +35,11 @@ QString MarkdownBuffer::insertImage(const QImage &p_image, const QString &p_imag void MarkdownBuffer::fetchInitialImages() { Q_ASSERT(m_initialImages.isEmpty()); - vte::MarkdownLink::TypeFlags linkFlags = vte::MarkdownLink::TypeFlag::LocalRelativeInternal | vte::MarkdownLink::TypeFlag::Remote; + // There is compilation error on Linux and macOS using TypeFlags directly. + int linkFlags = vte::MarkdownLink::TypeFlag::LocalRelativeInternal | vte::MarkdownLink::TypeFlag::Remote; m_initialImages = vte::MarkdownUtils::fetchImagesFromMarkdownText(getContent(), getResourcePath(), - linkFlags); + static_cast(linkFlags)); } void MarkdownBuffer::addInsertedImage(const QString &p_imagePath, const QString &p_urlInLink) @@ -57,11 +58,11 @@ QHash MarkdownBuffer::clearObsoleteImages() Q_ASSERT(!isModified()); const bool discarded = state() & StateFlag::Discarded; - const vte::MarkdownLink::TypeFlags linkFlags = vte::MarkdownLink::TypeFlag::LocalRelativeInternal | vte::MarkdownLink::TypeFlag::Remote; + const int linkFlags = vte::MarkdownLink::TypeFlag::LocalRelativeInternal | vte::MarkdownLink::TypeFlag::Remote; const auto latestImages = vte::MarkdownUtils::fetchImagesFromMarkdownText(!discarded ? getContent() : m_provider->read(), getResourcePath(), - linkFlags); + static_cast(linkFlags)); QSet latestImagesPath; for (const auto &link : latestImages) { if (link.m_type & vte::MarkdownLink::TypeFlag::Remote) { diff --git a/src/imagehost/giteeimagehost.cpp b/src/imagehost/giteeimagehost.cpp new file mode 100644 index 00000000..bb037351 --- /dev/null +++ b/src/imagehost/giteeimagehost.cpp @@ -0,0 +1,203 @@ +#include "giteeimagehost.h" + +#include +#include +#include + +#include +#include + +using namespace vnotex; + +const QString GiteeImageHost::c_apiUrl = "https://gitee.com/api/v5"; + +GiteeImageHost::GiteeImageHost(QObject *p_parent) + : ImageHost(p_parent) +{ +} + +bool GiteeImageHost::ready() const +{ + return !m_personalAccessToken.isEmpty() && !m_userName.isEmpty() && !m_repoName.isEmpty(); +} + +ImageHost::Type GiteeImageHost::getType() const +{ + return Type::Gitee; +} + +QJsonObject GiteeImageHost::getConfig() const +{ + QJsonObject obj; + obj[QStringLiteral("personal_access_token")] = m_personalAccessToken; + obj[QStringLiteral("user_name")] = m_userName; + obj[QStringLiteral("repository_name")] = m_repoName; + return obj; +} + +void GiteeImageHost::setConfig(const QJsonObject &p_jobj) +{ + parseConfig(p_jobj, m_personalAccessToken, m_userName, m_repoName); + + m_imageUrlPrefix = QString("https://gitee.com/%1/%2/raw/master/").arg(m_userName, m_repoName); +} + +bool GiteeImageHost::testConfig(const QJsonObject &p_jobj, QString &p_msg) +{ + p_msg.clear(); + + QString token, userName, repoName; + parseConfig(p_jobj, token, userName, repoName); + + if (token.isEmpty() || userName.isEmpty() || repoName.isEmpty()) { + p_msg = tr("PersonalAccessToken/UserName/RepositoryName should not be empty."); + return false; + } + + auto reply = getRepoInfo(token, userName, repoName); + p_msg = QString::fromUtf8(reply.m_data); + return reply.m_error == QNetworkReply::NoError; +} + +vte::NetworkAccess::RawHeaderPairs GiteeImageHost::prepareCommonHeaders() +{ + vte::NetworkAccess::RawHeaderPairs rawHeader; + rawHeader.push_back(qMakePair(QByteArray("Content-Type"), QByteArray("application/json;charset=UTF-8"))); + return rawHeader; +} + +QString GiteeImageHost::addAccessToken(const QString &p_token, QString p_url) +{ + if (p_url.contains(QLatin1Char('?'))) { + p_url += QString("&access_token=%1").arg(p_token); + } else { + p_url += QString("?access_token=%1").arg(p_token); + } + return p_url; +} + +vte::NetworkReply GiteeImageHost::getRepoInfo(const QString &p_token, const QString &p_userName, const QString &p_repoName) const +{ + auto rawHeader = prepareCommonHeaders(); + auto urlStr = QString("%1/repos/%2/%3").arg(c_apiUrl, p_userName, p_repoName); + auto reply = vte::NetworkAccess::request(QUrl(addAccessToken(p_token, urlStr)), rawHeader); + return reply; +} + +void GiteeImageHost::parseConfig(const QJsonObject &p_jobj, + QString &p_token, + QString &p_userName, + QString &p_repoName) +{ + p_token = p_jobj[QStringLiteral("personal_access_token")].toString(); + p_userName = p_jobj[QStringLiteral("user_name")].toString(); + p_repoName = p_jobj[QStringLiteral("repository_name")].toString(); +} + +static bool isEmptyResponse(const QByteArray &p_data) +{ + return p_data == QByteArray("[]"); +} + +QString GiteeImageHost::create(const QByteArray &p_data, const QString &p_path, QString &p_msg) +{ + QString destUrl; + + if (p_path.isEmpty()) { + p_msg = tr("Failed to create image with empty path."); + return destUrl; + } + + if (!ready()) { + p_msg = tr("Invalid GitHub image host configuration."); + return QString(); + } + + auto rawHeader = prepareCommonHeaders(); + const auto urlStr = QString("%1/repos/%2/%3/contents/%4").arg(c_apiUrl, m_userName, m_repoName, p_path); + + // Check if @p_path already exists. + auto reply = vte::NetworkAccess::request(QUrl(addAccessToken(m_personalAccessToken, urlStr)), rawHeader); + if (reply.m_error == QNetworkReply::NoError) { + if (!isEmptyResponse(reply.m_data)) { + p_msg = tr("The resource already exists at the image host (%1).").arg(p_path); + return QString(); + } + } else if (reply.m_error != QNetworkReply::ContentNotFoundError) { + p_msg = tr("Failed to query the resource at the image host (%1) (%2) (%3).").arg(urlStr, reply.errorStr(), reply.m_data); + return QString(); + } + + // Create the content. + QJsonObject requestDataObj; + requestDataObj[QStringLiteral("access_token")] = m_personalAccessToken; + requestDataObj[QStringLiteral("message")] = QString("VX_ADD: %1").arg(p_path); + requestDataObj[QStringLiteral("content")] = QString::fromUtf8(p_data.toBase64()); + auto requestData = Utils::toJsonString(requestDataObj); + reply = vte::NetworkAccess::post(QUrl(urlStr), rawHeader, requestData); + if (reply.m_error != QNetworkReply::NoError) { + p_msg = tr("Failed to create resource at the image host (%1) (%2) (%3).").arg(urlStr, reply.errorStr(), reply.m_data); + return QString(); + } else { + auto replyObj = Utils::fromJsonString(reply.m_data); + Q_ASSERT(!replyObj.isEmpty()); + auto targetUrl = replyObj[QStringLiteral("content")].toObject().value(QStringLiteral("download_url")).toString(); + if (targetUrl.isEmpty()) { + p_msg = tr("Failed to create resource at the image host (%1) (%2) (%3).").arg(urlStr, reply.errorStr(), reply.m_data); + } else { + qDebug() << "created resource" << targetUrl; + } + return targetUrl; + } +} + +bool GiteeImageHost::ownsUrl(const QString &p_url) const +{ + return p_url.startsWith(m_imageUrlPrefix); +} + +bool GiteeImageHost::remove(const QString &p_url, QString &p_msg) +{ + Q_ASSERT(ownsUrl(p_url)); + + if (!ready()) { + p_msg = tr("Invalid GitHub image host configuration."); + return false; + } + + const QString resourcePath = WebUtils::purifyUrl(p_url.mid(m_imageUrlPrefix.size())); + + auto rawHeader = prepareCommonHeaders(); + const auto urlStr = QString("%1/repos/%2/%3/contents/%4").arg(c_apiUrl, m_userName, m_repoName, resourcePath); + + // Get the SHA of the resource. + auto reply = vte::NetworkAccess::request(QUrl(addAccessToken(m_personalAccessToken, urlStr)), rawHeader); + if (reply.m_error != QNetworkReply::NoError || isEmptyResponse(reply.m_data)) { + p_msg = tr("Failed to fetch information about the resource (%1).").arg(resourcePath); + return false; + } + + auto replyObj = Utils::fromJsonString(reply.m_data); + Q_ASSERT(!replyObj.isEmpty()); + const auto sha = replyObj[QStringLiteral("sha")].toString(); + if (sha.isEmpty()) { + p_msg = tr("Failed to fetch SHA about the resource (%1) (%2).").arg(resourcePath, QString::fromUtf8(reply.m_data)); + return false; + } + + // Delete. + QJsonObject requestDataObj; + requestDataObj[QStringLiteral("access_token")] = m_personalAccessToken; + requestDataObj[QStringLiteral("message")] = QString("VX_DEL: %1").arg(resourcePath); + requestDataObj[QStringLiteral("sha")] = sha; + auto requestData = Utils::toJsonString(requestDataObj); + reply = vte::NetworkAccess::deleteResource(QUrl(urlStr), rawHeader, requestData); + if (reply.m_error != QNetworkReply::NoError) { + p_msg = tr("Failed to delete resource (%1) (%2).").arg(resourcePath, QString::fromUtf8(reply.m_data)); + return false; + } + + qDebug() << "deleted resource" << resourcePath; + + return true; +} diff --git a/src/imagehost/giteeimagehost.h b/src/imagehost/giteeimagehost.h new file mode 100644 index 00000000..47ee86a1 --- /dev/null +++ b/src/imagehost/giteeimagehost.h @@ -0,0 +1,57 @@ +#ifndef GITEEIMAGEHOST_H +#define GITEEIMAGEHOST_H + +#include "imagehost.h" + +#include + +namespace vnotex +{ + class GiteeImageHost : public ImageHost + { + Q_OBJECT + public: + explicit GiteeImageHost(QObject *p_parent); + + bool ready() const Q_DECL_OVERRIDE; + + Type getType() const Q_DECL_OVERRIDE; + + QJsonObject getConfig() const Q_DECL_OVERRIDE; + + void setConfig(const QJsonObject &p_jobj) Q_DECL_OVERRIDE; + + bool testConfig(const QJsonObject &p_jobj, QString &p_msg) Q_DECL_OVERRIDE; + + QString create(const QByteArray &p_data, const QString &p_path, QString &p_msg) Q_DECL_OVERRIDE; + + bool remove(const QString &p_url, QString &p_msg) Q_DECL_OVERRIDE; + + bool ownsUrl(const QString &p_url) const Q_DECL_OVERRIDE; + + private: + // Used to test. + vte::NetworkReply getRepoInfo(const QString &p_token, const QString &p_userName, const QString &p_repoName) const; + + static void parseConfig(const QJsonObject &p_jobj, + QString &p_token, + QString &p_userName, + QString &p_repoName); + + static vte::NetworkAccess::RawHeaderPairs prepareCommonHeaders(); + + static QString addAccessToken(const QString &p_token, QString p_url); + + QString m_personalAccessToken; + + QString m_userName; + + QString m_repoName; + + QString m_imageUrlPrefix; + + static const QString c_apiUrl; + }; +} + +#endif // GITEEIMAGEHOST_H diff --git a/src/imagehost/githubimagehost.cpp b/src/imagehost/githubimagehost.cpp index dc34fb50..4a8aff81 100644 --- a/src/imagehost/githubimagehost.cpp +++ b/src/imagehost/githubimagehost.cpp @@ -105,14 +105,6 @@ QString GitHubImageHost::create(const QByteArray &p_data, const QString &p_path, return destUrl; } - destUrl = createResource(p_data, p_path, p_msg); - return destUrl; -} - -QString GitHubImageHost::createResource(const QByteArray &p_content, const QString &p_path, QString &p_msg) const -{ - Q_ASSERT(!p_path.isEmpty()); - if (!ready()) { p_msg = tr("Invalid GitHub image host configuration."); return QString(); @@ -134,7 +126,7 @@ QString GitHubImageHost::createResource(const QByteArray &p_content, const QStri // Create the content. QJsonObject requestDataObj; requestDataObj[QStringLiteral("message")] = QString("VX_ADD: %1").arg(p_path); - requestDataObj[QStringLiteral("content")] = QString::fromUtf8(p_content.toBase64()); + requestDataObj[QStringLiteral("content")] = QString::fromUtf8(p_data.toBase64()); auto requestData = Utils::toJsonString(requestDataObj); reply = vte::NetworkAccess::put(QUrl(urlStr), rawHeader, requestData); if (reply.m_error != QNetworkReply::NoError) { diff --git a/src/imagehost/githubimagehost.h b/src/imagehost/githubimagehost.h index ef082d5c..42400051 100644 --- a/src/imagehost/githubimagehost.h +++ b/src/imagehost/githubimagehost.h @@ -29,12 +29,19 @@ namespace vnotex bool ownsUrl(const QString &p_url) const Q_DECL_OVERRIDE; + protected: + QString m_personalAccessToken; + + QString m_userName; + + QString m_repoName; + + QString m_imageUrlPrefix; + private: // Used to test. vte::NetworkReply getRepoInfo(const QString &p_token, const QString &p_userName, const QString &p_repoName) const; - QString createResource(const QByteArray &p_content, const QString &p_path, QString &p_msg) const; - static void parseConfig(const QJsonObject &p_jobj, QString &p_token, QString &p_userName, @@ -46,14 +53,6 @@ namespace vnotex static vte::NetworkAccess::RawHeaderPairs prepareCommonHeaders(const QString &p_token); - QString m_personalAccessToken; - - QString m_userName; - - QString m_repoName; - - QString m_imageUrlPrefix; - static const QString c_apiUrl; }; } diff --git a/src/imagehost/imagehost.cpp b/src/imagehost/imagehost.cpp index 11bbdde0..5d4766e8 100644 --- a/src/imagehost/imagehost.cpp +++ b/src/imagehost/imagehost.cpp @@ -23,6 +23,9 @@ QString ImageHost::typeString(ImageHost::Type p_type) case Type::GitHub: return tr("GitHub"); + case Type::Gitee: + return tr("Gitee"); + default: Q_ASSERT(false); return QString("Unknown"); diff --git a/src/imagehost/imagehost.h b/src/imagehost/imagehost.h index c5d6dc7b..55ace6cd 100644 --- a/src/imagehost/imagehost.h +++ b/src/imagehost/imagehost.h @@ -18,6 +18,7 @@ namespace vnotex enum Type { GitHub = 0, + Gitee, MaxHost }; diff --git a/src/imagehost/imagehost.pri b/src/imagehost/imagehost.pri index 46dc2611..3b479345 100644 --- a/src/imagehost/imagehost.pri +++ b/src/imagehost/imagehost.pri @@ -1,12 +1,14 @@ QT += widgets HEADERS += \ + $$PWD/giteeimagehost.h \ $$PWD/githubimagehost.h \ $$PWD/imagehost.h \ $$PWD/imagehostmgr.h \ $$PWD/imagehostutils.h SOURCES += \ + $$PWD/giteeimagehost.cpp \ $$PWD/githubimagehost.cpp \ $$PWD/imagehost.cpp \ $$PWD/imagehostmgr.cpp \ diff --git a/src/imagehost/imagehostmgr.cpp b/src/imagehost/imagehostmgr.cpp index c6b09248..af73e563 100644 --- a/src/imagehost/imagehostmgr.cpp +++ b/src/imagehost/imagehostmgr.cpp @@ -6,6 +6,7 @@ #include #include "githubimagehost.h" +#include "giteeimagehost.h" using namespace vnotex; @@ -64,6 +65,9 @@ ImageHost *ImageHostMgr::createImageHost(ImageHost::Type p_type, QObject *p_pare case ImageHost::Type::GitHub: return new GitHubImageHost(p_parent); + case ImageHost::Type::Gitee: + return new GiteeImageHost(p_parent); + default: return nullptr; } diff --git a/src/imagehost/imagehostutils.h b/src/imagehost/imagehostutils.h index 23e7c3cf..5844b085 100644 --- a/src/imagehost/imagehostutils.h +++ b/src/imagehost/imagehostutils.h @@ -16,7 +16,6 @@ namespace vnotex ImageHostUtils() = delete; // According to @p_buffer, generate the relative path on image host for images. - // Return the relative path folder. static QString generateRelativePath(const Buffer *p_buffer); }; } diff --git a/src/widgets/editors/markdowneditor.cpp b/src/widgets/editors/markdowneditor.cpp index c03a4553..86fa8d25 100644 --- a/src/widgets/editors/markdowneditor.cpp +++ b/src/widgets/editors/markdowneditor.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -1337,16 +1338,22 @@ void MarkdownEditor::setImageHost(ImageHost *p_host) m_imageHost = p_host; } -QString MarkdownEditor::saveToImageHost(const QByteArray &p_imageData, const QString &p_destFileName) +static QString generateImageHostFileName(const Buffer *p_buffer, const QString &p_destFileName) { - Q_ASSERT(m_imageHost); - - auto destPath = ImageHostUtils::generateRelativePath(m_buffer); + auto destPath = ImageHostUtils::generateRelativePath(p_buffer); if (destPath.isEmpty()) { destPath = p_destFileName; } else { destPath += "/" + p_destFileName; } + return destPath; +} + +QString MarkdownEditor::saveToImageHost(const QByteArray &p_imageData, const QString &p_destFileName) +{ + Q_ASSERT(m_imageHost); + + const auto destPath = generateImageHostFileName(m_buffer, p_destFileName); QString errMsg; @@ -1390,4 +1397,79 @@ void MarkdownEditor::uploadImagesToImageHost() auto act = static_cast(sender()); auto host = ImageHostMgr::getInst().find(act->data().toString()); Q_ASSERT(host); + + // Only LocalRelativeInternal images. + // Descending order of the link position. + auto images = vte::MarkdownUtils::fetchImagesFromMarkdownText(m_buffer->getContent(), + m_buffer->getResourcePath(), + vte::MarkdownLink::TypeFlag::LocalRelativeInternal); + if (images.isEmpty()) { + return; + } + + QProgressDialog proDlg(tr("Uploading local images..."), + tr("Abort"), + 0, + images.size(), + this); + proDlg.setWindowModality(Qt::WindowModal); + proDlg.setWindowTitle(tr("Upload Images To Image Host")); + + int cnt = 0; + auto cursor = m_textEdit->textCursor(); + cursor.beginEditBlock(); + for (int i = 0; i < images.size(); ++i) { + const auto &link = images[i]; + + proDlg.setValue(i + 1); + if (proDlg.wasCanceled()) { + break; + } + proDlg.setLabelText(tr("Upload image (%1)").arg(link.m_path)); + + Q_ASSERT(i == 0 || link.m_urlInLinkPos < images[i - 1].m_urlInLinkPos); + + QByteArray ba; + try { + ba = FileUtils::readFile(link.m_path); + } catch (Exception &e) { + MessageBoxHelper::notify(MessageBoxHelper::Warning, + QString("Failed to read local image file (%1) (%2).").arg(link.m_path, e.what()), + this); + continue; + } + + if (ba.isEmpty()) { + qWarning() << "Skipped uploading empty image" << link.m_path; + continue; + } + + const auto destPath = generateImageHostFileName(m_buffer, PathUtils::fileName(link.m_path)); + QString errMsg; + QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); + const auto targetUrl = host->create(ba, destPath, errMsg); + QApplication::restoreOverrideCursor(); + + if (targetUrl.isEmpty()) { + MessageBoxHelper::notify(MessageBoxHelper::Warning, + QString("Failed to upload image to image host (%1) as (%2).").arg(host->getName(), destPath), + QString(), + errMsg, + this); + continue; + } + + // Update the link URL. + cursor.setPosition(link.m_urlInLinkPos); + cursor.setPosition(link.m_urlInLinkPos + link.m_urlInLink.size(), QTextCursor::KeepAnchor); + cursor.insertText(targetUrl); + ++cnt; + } + cursor.endEditBlock(); + + proDlg.setValue(images.size()); + + if (cnt > 0) { + m_textEdit->setTextCursor(cursor); + } }