diff --git a/.gitignore b/.gitignore index 494e6bb4..4ce739d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ VNote.pro.user* CMakeLists.txt.user +.DS_Store diff --git a/README.md b/README.md index 1b9e3a84..bd49cd12 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ Utilizing Qt, VNote could run on **Linux**, **Windows**, and **macOS**. - Attachments of notes; - Themes and dark mode; - Rich and extensible export, such as HTML, PDF, PDF (All In One), and images; +- GitHub and WeChat image hosting; # Donate You could help VNote's development in many ways. diff --git a/README_zh.md b/README_zh.md index 014386c9..bf4d9e30 100644 --- a/README_zh.md +++ b/README_zh.md @@ -110,6 +110,7 @@ VNote 不是一个简单的 Markdown 编辑器。通过提供笔记管理功能 - 笔记附件; - 主题以及深色模式; - 丰富、可扩展的导出,包括 HTML、PDF、PDF(多合一)和图片; +- GitHub和微信图床; # 捐赠 您可以通过很多途径帮助 VNote 的开发。 diff --git a/src/dialog/vsettingsdialog.cpp b/src/dialog/vsettingsdialog.cpp index 8f80e5a0..16a4c68d 100644 --- a/src/dialog/vsettingsdialog.cpp +++ b/src/dialog/vsettingsdialog.cpp @@ -66,6 +66,7 @@ VSettingsDialog::VSettingsDialog(QWidget *p_parent) addTab(new VNoteManagementTab(), tr("Note Management")); addTab(new VMarkdownTab(), tr("Markdown")); addTab(new VMiscTab(), tr("Misc")); + addTab(new VImageHostingTab(), tr("Image Hosting")); m_tabList->setMaximumWidth(m_tabList->sizeHintForColumn(0) + 5); @@ -206,6 +207,15 @@ void VSettingsDialog::loadConfiguration() } } + // ImageBed Tab + { + VImageHostingTab *imageBedTab = dynamic_cast(m_tabs->widget(idx++)); + Q_ASSERT(imageBedTab); + if (!imageBedTab->loadConfiguration()) { + goto err; + } + } + return; err: VUtils::showMessage(QMessageBox::Warning, tr("Warning"), @@ -271,6 +281,15 @@ void VSettingsDialog::saveConfiguration() } } + // Image Hosting Tab. + { + VImageHostingTab *imageBedTab = dynamic_cast(m_tabs->widget(idx++)); + Q_ASSERT(imageBedTab); + if (!imageBedTab->saveConfiguration()) { + goto err; + } + } + accept(); return; err: @@ -1547,7 +1566,6 @@ bool VMiscTab::loadConfiguration() if (!loadMatchesInPage()) { return false; } - return true; } @@ -1571,3 +1589,162 @@ bool VMiscTab::saveMatchesInPage() g_config->setHighlightMatchesInPage(m_matchesInPageCB->isChecked()); return true; } + +VImageHostingTab::VImageHostingTab(QWidget *p_parent) + : QWidget(p_parent) +{ + QTabWidget *imageHostingTabWeg = new QTabWidget(this); + QWidget *githubImageHostingTab = new QWidget(); + QWidget *wechatImageHostingTab = new QWidget(); + imageHostingTabWeg->addTab(githubImageHostingTab, tr("GitHub")); + imageHostingTabWeg->addTab(wechatImageHostingTab, tr("WeChat")); + imageHostingTabWeg->setCurrentIndex(0); + + // Set the tab of GitHub image Hosting + m_personalAccessTokenEdit = new VLineEdit(); + m_personalAccessTokenEdit->setToolTip(tr("GitHub personal access token")); + m_repoNameEdit = new VLineEdit(); + m_repoNameEdit->setToolTip(tr("Name of GitHub repository for image hosting")); + m_userNameEdit = new VLineEdit(); + m_userNameEdit->setToolTip(tr("User name of GitHub")); + + QFormLayout *githubLayout = new QFormLayout(); + githubLayout->addRow(tr("Personal access token:"), m_personalAccessTokenEdit); + githubLayout->addRow(tr("Repo name:"), m_repoNameEdit); + githubLayout->addRow(tr("User name:"), m_userNameEdit); + + githubImageHostingTab->setLayout(githubLayout); + + // Set the tab of GitHub image Hosting + m_appidEdit = new VLineEdit(); + m_appidEdit->setToolTip(tr("WeChat appid")); + m_secretEdit = new VLineEdit(); + m_secretEdit->setToolTip(tr("Please input wechat secret")); + m_markdown2WechatToolUrlEdit = new VLineEdit(); + m_markdown2WechatToolUrlEdit->setToolTip(tr("Please input markdown to wechat tool's url")); + + QFormLayout *wechatLayout = new QFormLayout(); + wechatLayout->addRow(tr("appid:"), m_appidEdit); + wechatLayout->addRow(tr("secret:"), m_secretEdit); + wechatLayout->addRow(tr("markdown2WechatToolUrl"), m_markdown2WechatToolUrlEdit); + + wechatImageHostingTab->setLayout(wechatLayout); +} + +bool VImageHostingTab::loadAppid() +{ + m_appidEdit->setText(g_config->getAppid()); + return true; +} + +bool VImageHostingTab::saveAppid() +{ + g_config->setAppid(m_appidEdit->text()); + return true; +} + +bool VImageHostingTab::loadSecret() +{ + m_secretEdit->setText(g_config->getSecret()); + return true; +} + +bool VImageHostingTab::saveSecret() +{ + g_config->setSecret(m_secretEdit->text()); + return true; +} + +bool VImageHostingTab::loadMarkdown2WechatToolUrl() +{ + m_markdown2WechatToolUrlEdit->setText(g_config->getMarkdown2WechatToolUrl()); + return true; +} + +bool VImageHostingTab::saveMarkdown2WechatToolUrl() +{ + g_config->setMarkdown2WechatToolUrl(m_markdown2WechatToolUrlEdit->text()); + return true; +} + +bool VImageHostingTab::loadpersonalAccessToken() +{ + m_personalAccessTokenEdit->setText(g_config->getpersonalAccessToken()); + return true; +} + +bool VImageHostingTab::savepersonalAccessToken() +{ + g_config->setpersonalAccessToken(m_personalAccessTokenEdit->text()); + return true; +} + +bool VImageHostingTab::loadReposName() +{ + m_repoNameEdit->setText(g_config->getReposName()); + return true; +} + +bool VImageHostingTab::saveReposName() +{ + g_config->setReposName(m_repoNameEdit->text()); + return true; +} + +bool VImageHostingTab::loadUserName() +{ + m_userNameEdit->setText(g_config->getUserName()); + return true; +} + +bool VImageHostingTab::saveUserName() +{ + g_config->setUserName(m_userNameEdit->text()); + return true; +} + +bool VImageHostingTab::loadConfiguration() +{ + if(!loadpersonalAccessToken()){ + return false; + } + if(!loadReposName()){ + return false; + } + if(!loadUserName()){ + return false; + } + if(!loadAppid()){ + return false; + } + if(!loadSecret()){ + return false; + } + if(!loadMarkdown2WechatToolUrl()){ + return false; + } + return true; +} + +bool VImageHostingTab::saveConfiguration() +{ + if(!savepersonalAccessToken()){ + return false; + } + if(!saveReposName()){ + return false; + } + if(!saveUserName()){ + return false; + } + if(!saveAppid()){ + return false; + } + if(!saveSecret()){ + return false; + } + if(!saveMarkdown2WechatToolUrl()){ + return false; + } + return true; +} diff --git a/src/dialog/vsettingsdialog.h b/src/dialog/vsettingsdialog.h index 256ba5cf..3df7b000 100644 --- a/src/dialog/vsettingsdialog.h +++ b/src/dialog/vsettingsdialog.h @@ -4,6 +4,7 @@ #include #include #include +#include class QDialogButtonBox; class QComboBox; @@ -270,6 +271,47 @@ private: QCheckBox *m_matchesInPageCB; }; +class VImageHostingTab : public QWidget +{ + Q_OBJECT +public: + explicit VImageHostingTab(QWidget *p_parent = 0); + bool loadConfiguration(); + bool saveConfiguration(); + +private: + bool loadpersonalAccessToken(); + bool savepersonalAccessToken(); + + bool loadReposName(); + bool saveReposName(); + + bool loadUserName(); + bool saveUserName(); + + bool loadAppid(); + bool saveAppid(); + + bool loadSecret(); + bool saveSecret(); + + bool loadMarkdown2WechatToolUrl(); + bool saveMarkdown2WechatToolUrl(); + + // personalAccessToken + VLineEdit *m_personalAccessTokenEdit; + // reposName + VLineEdit *m_repoNameEdit; + // userName + VLineEdit *m_userNameEdit; + // appid + VLineEdit *m_appidEdit; + // secret + VLineEdit *m_secretEdit; + // markdown to wechat tools url + VLineEdit *m_markdown2WechatToolUrlEdit; +}; + class VSettingsDialog : public QDialog { Q_OBJECT diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index 98ce82fd..c0f60fbb 100644 --- a/src/resources/vnote.ini +++ b/src/resources/vnote.ini @@ -1,4 +1,13 @@ [global] +; Wechat ImageBed +wechat_appid= +wechat_secret= +wechat_markdown_to_wechat_tool_url= +; Github ImageBed +github_personal_access_token= +github_repos_name= +github_user_name= + ; Theme name theme=v_pure diff --git a/src/vconfigmanager.cpp b/src/vconfigmanager.cpp index 1f2c3cba..34d4f5b6 100644 --- a/src/vconfigmanager.cpp +++ b/src/vconfigmanager.cpp @@ -79,6 +79,14 @@ void VConfigManager::initialize() initCodeBlockCssStyles(); + m_personalAccessToken = getConfigFromSettings("global", "github_personal_access_token").toString(); + m_reposName = getConfigFromSettings("global", "github_repos_name").toString(); + m_userName = getConfigFromSettings("global", "github_user_name").toString(); + + m_appid = getConfigFromSettings("global", "wechat_appid").toString(); + m_secret = getConfigFromSettings("global", "wechat_secret").toString(); + m_markdown2WechatToolUrl = getConfigFromSettings("global", "wechat_markdown_to_wechat_tool_url").toString(); + m_theme = getConfigFromSettings("global", "theme").toString(); m_editorStyle = getConfigFromSettings("global", "editor_style").toString(); diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h index 42238f09..28e3be38 100644 --- a/src/vconfigmanager.h +++ b/src/vconfigmanager.h @@ -648,6 +648,27 @@ public: bool getEnableCodeBlockCopyButton() const; + // github image hosting setting + const QString &getpersonalAccessToken() const; + void setpersonalAccessToken(const QString &p_token); + + const QString &getReposName() const; + void setReposName(const QString &p_reposName); + + const QString &getUserName() const; + void setUserName(const QString &p_userName); + + // wechat image hosting setting + const QString &getAppid() const; + void setAppid(const QString &p_appid); + + const QString &getSecret() const; + void setSecret(const QString &p_secret); + + const QString &getMarkdown2WechatToolUrl() const; + void setMarkdown2WechatToolUrl(const QString &p_markdown2WechatToolUrl); + + private: void initEditorConfigs(); @@ -1071,6 +1092,16 @@ private: QString m_plantUMLCmd; + // github imagebed + QString m_personalAccessToken; + QString m_reposName; + QString m_userName; + + // wechat imagebed + QString m_appid; + QString m_secret; + QString m_markdown2WechatToolUrl; + // Size of history. int m_historySize; @@ -2989,4 +3020,92 @@ inline bool VConfigManager::getEnableCodeBlockCopyButton() const return m_enableCodeBlockCopyButton; } +inline const QString &VConfigManager::getAppid() const +{ + return m_appid; +} + +inline void VConfigManager::setAppid(const QString &p_appid) +{ + if(m_appid == p_appid){ + return; + } + m_appid = p_appid; + setConfigToSettings("global", "wechat_appid", p_appid); +} + +inline const QString &VConfigManager::getSecret() const +{ + return m_secret; +} + +inline void VConfigManager::setSecret(const QString &p_secret) +{ + if(m_secret == p_secret){ + return; + } + m_secret = p_secret; + setConfigToSettings("global", "wechat_secret", p_secret); +} + +inline const QString &VConfigManager::getMarkdown2WechatToolUrl() const +{ + return m_markdown2WechatToolUrl; +} + +inline void VConfigManager::setMarkdown2WechatToolUrl(const QString &p_markdown2WechatToolUrl) +{ + if(m_markdown2WechatToolUrl == p_markdown2WechatToolUrl){ + return; + } + m_markdown2WechatToolUrl = p_markdown2WechatToolUrl; + setConfigToSettings("global", "wechat_markdown_to_wechat_tool_url", p_markdown2WechatToolUrl); +} + + +inline const QString &VConfigManager::getpersonalAccessToken() const +{ + return m_personalAccessToken; +} + +inline void VConfigManager::setpersonalAccessToken(const QString &p_token) +{ + if (m_personalAccessToken == p_token) { + return; + } + + m_personalAccessToken = p_token; + setConfigToSettings("global", "github_personal_access_token", p_token); +} + +inline const QString &VConfigManager::getReposName() const +{ + return m_reposName; +} + +inline void VConfigManager::setReposName(const QString &p_reposName) +{ + if (m_reposName == p_reposName) { + return; + } + + m_reposName = p_reposName; + setConfigToSettings("global", "github_repos_name", p_reposName); +} + +inline const QString &VConfigManager::getUserName() const +{ + return m_userName; +} + +inline void VConfigManager::setUserName(const QString &p_userName) +{ + if (m_userName == p_userName) { + return; + } + + m_userName = p_userName; + setConfigToSettings("global", "github_user_name", p_userName); +} + #endif // VCONFIGMANAGER_H diff --git a/src/vmdtab.cpp b/src/vmdtab.cpp index 800a3716..aa0429cc 100644 --- a/src/vmdtab.cpp +++ b/src/vmdtab.cpp @@ -113,6 +113,9 @@ void VMdTab::setupUI() // Setup editor when we really need it. m_editor = NULL; + reply = Q_NULLPTR; + imageUploaded = false; + QVBoxLayout *layout = new QVBoxLayout(); layout->addWidget(m_splitter); layout->setContentsMargins(0, 0, 0, 0); @@ -443,6 +446,10 @@ void VMdTab::setupMarkdownViewer() this, &VMdTab::handleWebSelectionChanged); connect(m_webViewer, &VWebView::requestExpandRestorePreviewArea, this, &VMdTab::expandRestorePreviewArea); + connect(m_webViewer, &VWebView::requestUploadImageToGithub, + this, &VMdTab::handleUploadImageToGithubRequested); + connect(m_webViewer, &VWebView::requestUploadImageToWechat, + this, &VMdTab::handleUploadImageToWechatRequested); VPreviewPage *page = new VPreviewPage(m_webViewer); m_webViewer->setPage(page); @@ -1503,6 +1510,665 @@ void VMdTab::handleSavePageRequested() m_webViewer->page()->save(fileName, format); } +void VMdTab::handleUploadImageToGithubRequested() +{ + qDebug() << "Start processing the image upload request to GitHub"; + + if(g_config->getpersonalAccessToken().isEmpty() || g_config->getReposName().isEmpty() || g_config->getUserName().isEmpty()) + { + qDebug() << "Please configure the GitHub image hosting first!"; + QMessageBox::warning(NULL, tr("Github Image Hosting"), tr("Please configure the GitHub image hosting first!")); + return; + } + + authenticateGithubImageHosting(g_config->getpersonalAccessToken()); +} + +void VMdTab::authenticateGithubImageHosting(QString p_token) +{ + qDebug() << "start the authentication process "; + QApplication::setOverrideCursor(Qt::WaitCursor); + QNetworkRequest request; + QUrl url = QUrl("https://api.github.com"); + QString ptoken = "token " + p_token; + request.setRawHeader("Authorization", ptoken.toLocal8Bit()); + request.setUrl(url); + if(reply != Q_NULLPTR) { + reply->deleteLater(); + } + reply = manager.get(request); + connect(reply, &QNetworkReply::finished, this, &VMdTab::githubImageBedAuthFinished); +} + +void VMdTab::githubImageBedAuthFinished() +{ + switch (reply->error()) { + case QNetworkReply::NoError: + { + QByteArray bytes = reply->readAll(); + + if(bytes.contains("Bad credentials")){ + qDebug() << "Authentication failed"; + QApplication::restoreOverrideCursor(); // Recovery pointer + QMessageBox::warning(NULL, tr("Github Image Hosting"), tr("Bad credentials!! Please check your Github Image Hosting parameters !!")); + return; + }else{ + qDebug() << "Authentication completed"; + + qDebug() << "The current article path is: " << m_file->fetchPath(); + imageBasePath = m_file->fetchBasePath(); + newFileContent = m_file->getContent(); + + QVector images = VUtils::fetchImagesFromMarkdownFile(m_file,ImageLink::LocalRelativeInternal); + QApplication::restoreOverrideCursor(); // Recovery pointer + if(images.size() > 0) + { + + proDlg = new QProgressDialog(tr("Uploading images to github..."), + tr("Abort"), + 0, + images.size(), + this); + proDlg->setWindowModality(Qt::WindowModal); + proDlg->setWindowTitle(tr("Uploading Images To Github")); + proDlg->setMinimumDuration(1); + uploadImageCount = images.size(); + uploadImageCountIndex = uploadImageCount; + for(int i=0;igetName() << " No images to upload"; + QString info = m_file->getName() + " No pictures to upload"; + QMessageBox::information(NULL, tr("Github Image Hosting"), info); + } + } + break; + } + default: + { + QApplication::restoreOverrideCursor(); // Recovery pointer + qDebug() << "Network error: " << reply->errorString() << " error " << reply->error(); + QString info = tr("Network error: ") + reply->errorString(); + QMessageBox::warning(NULL, tr("Github Image Hosting"), info); + } + } +} + +void VMdTab::githubImageBedUploadManager() +{ + uploadImageCountIndex--; + + QString imageToUpload = ""; + QMapIterator it(imageUrlMap); + while(it.hasNext()) + { + it.next(); + if(it.value() == ""){ + imageToUpload = it.key(); + proDlg->setValue(uploadImageCount - 1 - uploadImageCountIndex); + proDlg->setLabelText(tr("Uploaading image: %1").arg(imageToUpload)); + break; + } + } + + if(imageToUpload == ""){ + qDebug() << "All images have been uploaded"; + githubImageBedReplaceLink(newFileContent, m_file->fetchPath()); + return; + } + + if(g_config->getpersonalAccessToken().isEmpty() || g_config->getReposName().isEmpty() || g_config->getUserName().isEmpty()) + { + qDebug() << "Please configure the GitHub image hosting first!"; + QMessageBox::warning(NULL, tr("Github Image Hosting"), tr("Please configure the GitHub image hosting first!")); + imageUrlMap.clear(); + return; + } + + QString path = imageBasePath + QDir::separator(); + path += imageToUpload; + githubImageBedUploadImage(g_config->getUserName(), g_config->getReposName(), path, g_config->getpersonalAccessToken()); +} + +void VMdTab::githubImageBedUploadImage(QString username, QString repository, QString imagePath, QString token) +{ + QFileInfo fileInfo(imagePath.toLocal8Bit()); + if(!fileInfo.exists()){ + qDebug() << "The picture does not exist in this path: " << imagePath.toLocal8Bit(); + QString info = tr("The picture does not exist in this path: ") + imagePath.toLocal8Bit(); + QMessageBox::warning(NULL, tr("Github Image Hosting"), info); + imageUrlMap.clear(); + if(imageUploaded){ + githubImageBedReplaceLink(newFileContent, m_file->fetchPath()); + } + return; + } + + QString fileSuffix = fileInfo.suffix(); // file extension + QString fileName = fileInfo.fileName(); // filename + QString uploadUrl; // Image upload URL + uploadUrl = "https://api.github.com/repos/" + username + "/" + repository + "/contents/" + QString::number(QDateTime::currentDateTime().toTime_t()) +"_" + fileName; + if(fileSuffix != QString::fromLocal8Bit("jpg") && fileSuffix != QString::fromLocal8Bit("png") && fileSuffix != QString::fromLocal8Bit("gif")){ + qDebug() << "Unsupported type..."; + QString info = tr("Unsupported type: ") + fileSuffix; + QMessageBox::warning(NULL, tr("Github Image Hosting"), info); + imageUrlMap.clear(); + if(imageUploaded){ + githubImageBedReplaceLink(newFileContent, m_file->fetchPath()); + } + return; + } + + QNetworkRequest request; + QUrl url = QUrl(uploadUrl); + QString ptoken = "token " + token; + request.setRawHeader("Authorization", ptoken.toLocal8Bit()); + request.setUrl(url); + if(reply != Q_NULLPTR) { + reply->deleteLater(); + } + + QString param = githubImageBedGenerateParam(imagePath); + QByteArray postData; + postData.append(param); + reply = manager.put(request, postData); + qDebug() << "Start uploading images: " + imagePath + " Waiting for upload to complete"; + uploadImageStatus = true; + currentUploadImage = imagePath; + connect(reply, &QNetworkReply::finished, this, &VMdTab::githubImageBedUploadFinished); +} + +void VMdTab::githubImageBedUploadFinished() +{ + if (proDlg->wasCanceled()) { + qDebug() << "User stops uploading"; + reply->abort(); // Stop network request + imageUrlMap.clear(); + // The ones that have been uploaded successfully before still need to stay + if(imageUploaded){ + githubImageBedReplaceLink(newFileContent, m_file->fetchPath()); + } + return; + } + + switch (reply->error()) { + case QNetworkReply::NoError: + { + QByteArray bytes = reply->readAll(); + int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if(httpStatus == 201){ + qDebug() << "Upload success"; + + QString downloadUrl; + QString imageName; + QJsonDocument doucment = QJsonDocument::fromJson(bytes); + if (!doucment.isNull() ) + { + if (doucment.isObject()) { + QJsonObject object = doucment.object(); + if (object.contains("content")) { + QJsonValue value = object.value("content"); + if (value.isObject()) { + QJsonObject obj = value.toObject(); + if (obj.contains("download_url")) { + QJsonValue value = obj.value("download_url"); + if (value.isString()) { + downloadUrl = value.toString(); + qDebug() << "json decode: download_url : " << downloadUrl; + imageUploaded = true; // On behalf of successfully uploaded images + proDlg->setValue(uploadImageCount); + } + } + if(obj.contains("name")){ + QJsonValue value = obj.value("name"); + if(value.isString()){ + imageName = value.toString(); + } + } + + // Traverse key in imageurlmap + QList klist = imageUrlMap.keys(); + QString temp; + for(int i=0;ifetchPath()); + } + QString info = tr("Json decode error, Please contact the developer~"); + QMessageBox::warning(NULL, tr("Github Image Hosting"), info); + } + + + }else{ + // If status is not 201, it means there is a problem + delete proDlg; + imageUrlMap.clear(); + qDebug() << "Upload failure"; + if(imageUploaded){ + githubImageBedReplaceLink(newFileContent, m_file->fetchPath()); + } + QString info = tr("github status code != 201, Please contact the developer~"); + QMessageBox::warning(NULL, tr("Github Image Hosting"), info); + } + break; + } + default: + { + delete proDlg; + imageUrlMap.clear(); + qDebug()<<"network error: " << reply->errorString() << " error " << reply->error(); + QByteArray bytes = reply->readAll(); + qDebug() << bytes; + int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + qDebug() << "status: " << httpStatus; + + if(imageUploaded){ + githubImageBedReplaceLink(newFileContent, m_file->fetchPath()); + } + QString info = tr("Uploading ") + currentUploadImage + tr(" \n\nNetwork error: ") + reply->errorString() + tr("\n\nPlease check the network or image size"); + QMessageBox::warning(NULL, tr("Github Image Hosting"), info); + } + } +} + +void VMdTab::githubImageBedReplaceLink(QString fileContent, QString filePath) +{ + // This function must be executed when the upload is completed or fails in the middle + // Write content to file + QFile file(filePath); + file.open(QIODevice::WriteOnly | QIODevice::Text); + file.write(fileContent.toUtf8()); + file.close(); + // Reset + imageUrlMap.clear(); + imageUploaded = false; +} + +QString VMdTab::githubImageBedGenerateParam(QString imagePath){ + // According to the requirements of GitHub interface, pictures must be in Base64 format + // img to base64 + QByteArray hexed; + QFile imgFile(imagePath); + imgFile.open(QIODevice::ReadOnly); + hexed = imgFile.readAll().toBase64(); + + QString imgBase64 = hexed; // Base64 encoding of images + QJsonObject json; + json.insert("message", QString("updatetest")); + json.insert("content", imgBase64); + + QJsonDocument document; + document.setObject(json); + QByteArray byteArray = document.toJson(QJsonDocument::Compact); + QString jsonStr(byteArray); + return jsonStr; +} + +void VMdTab::handleUploadImageToWechatRequested() +{ + qDebug() << "Start processing image upload request to wechat"; + QString appid = g_config->getAppid(); + QString secret = g_config->getSecret(); + if(appid.isEmpty() || secret.isEmpty()) + { + qDebug() << "Please configure the Wechat image hosting first!"; + QMessageBox::warning(NULL, tr("Wechat Image Hosting"), tr("Please configure the Wechat image hosting first!")); + return; + } + + authenticateWechatImageHosting(appid, secret); +} + +void VMdTab::authenticateWechatImageHosting(QString appid, QString secret) +{ + qDebug() << "Start certification"; + QApplication::setOverrideCursor(Qt::WaitCursor); // Set the mouse to wait + QNetworkRequest request; + QString auth_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid="+ appid.toLocal8Bit() + "&secret=" + secret.toLocal8Bit(); + QUrl url = QUrl(auth_url); +// request.setRawHeader("grant_type", "client_credential"); +// request.setRawHeader("appid", appid.toLocal8Bit()); +// request.setRawHeader("secret", secret.toLocal8Bit()); + request.setUrl(url); + if(reply != Q_NULLPTR) { + reply->deleteLater(); + } + reply = manager.get(request); + connect(reply, &QNetworkReply::finished, this, &VMdTab::wechatImageBedAuthFinished); +} + +void VMdTab::wechatImageBedAuthFinished() +{ + switch (reply->error()) { + case QNetworkReply::NoError: + { + QByteArray bytes = reply->readAll(); + QJsonDocument document = QJsonDocument::fromJson(bytes); + if(!document.isNull()){ + if(document.isObject()){ + QJsonObject object = document.object(); + if(object.contains("access_token")){ + QJsonValue value = object.value("access_token"); + if(value.isString()){ + qDebug() << "Authentication successful, get token"; + // Parsing token + wechatAccessToken = value.toString(); + + qDebug() << "The current article path is: " << m_file->fetchPath(); + imageBasePath = m_file->fetchBasePath(); + newFileContent = m_file->getContent(); + + QVector images = VUtils::fetchImagesFromMarkdownFile(m_file,ImageLink::LocalRelativeInternal); + QApplication::restoreOverrideCursor(); // Recovery pointer + if(images.size() > 0) + { + + proDlg = new QProgressDialog(tr("Uploading images to github..."), + tr("Abort"), + 0, + images.size(), + this); + proDlg->setWindowModality(Qt::WindowModal); + proDlg->setWindowTitle(tr("Uploading Images To Github")); + proDlg->setMinimumDuration(1); + uploadImageCount = images.size(); + uploadImageCountIndex = uploadImageCount; + for(int i=0;igetName() << " No pictures to upload"; + QString info = m_file->getName() + tr(" No pictures to upload"); + QMessageBox::information(NULL, tr("Wechat Image Hosting"), info); + } + } + }else{ + qDebug() << "Authentication failed"; + QString string = bytes; + qDebug() << string; + // You can refine the error here + QApplication::restoreOverrideCursor(); + if(string.contains("invalid ip")){ + QString ip = string.split(" ")[2]; + QClipboard *board = QApplication::clipboard(); + board->setText(ip); + QMessageBox::warning(NULL, tr("Wechat Image Hosting"), tr("Your ip address was set to the Clipboard! \nPlease add the IP address: ") + ip + tr(" to the wechat ip whitelist!")); + }else{ + QMessageBox::warning(NULL, tr("Wechat Image Hosting"), tr("Please check your Wechat Image Hosting parameters !!\n") + string); + } + return; + } + } + }else{ + delete proDlg; + imageUrlMap.clear(); + qDebug() << "Resolution failure!"; + qDebug() << "Resolution failure's json: " << bytes; + QString info = tr("Json decode error, Please contact the developer~"); + QMessageBox::warning(NULL, tr("Wechat Image Hosting"), info); + } + + + break; + } + default: + { + QApplication::restoreOverrideCursor(); + qDebug() << "Network error: " << reply->errorString() << " error " << reply->error(); + QString info = tr("Network error: ") + reply->errorString(); + QMessageBox::warning(NULL, tr("Wechat Image Hosting"), info); + } + } +} + +void VMdTab::wechatImageBedUploadManager() +{ + uploadImageCountIndex--; + + QString image_to_upload = ""; + QMapIterator it(imageUrlMap); + while(it.hasNext()) + { + it.next(); + if(it.value() == ""){ + image_to_upload = it.key(); + proDlg->setValue(uploadImageCount - 1 - uploadImageCountIndex); + proDlg->setLabelText(tr("Uploaading image: %1").arg(image_to_upload)); + break; + } + + } + + if(image_to_upload == ""){ + qDebug() << "All pictures have been uploaded"; + // Copy content to clipboard + wechatImageBedReplaceLink(newFileContent, m_file->fetchPath()); + return; + } + + QString path = imageBasePath + QDir::separator(); + path += image_to_upload; + currentUploadRelativeImagePah = image_to_upload; + wechatImageBedUploadImage(path, wechatAccessToken); +} + +void VMdTab::wechatImageBedUploadImage(QString image_path, QString token) +{ + qDebug() << "To deal with: " << image_path; + QFileInfo fileInfo(image_path.toLocal8Bit()); + if(!fileInfo.exists()){ + delete proDlg; + imageUrlMap.clear(); + qDebug() << "The picture does not exist in this path: " << image_path.toLocal8Bit(); + QString info = tr("The picture does not exist in this path: ") + image_path.toLocal8Bit(); + QMessageBox::warning(NULL, tr("Wechat Image Hosting"), info); + return; + } + + QString file_suffix = fileInfo.suffix(); // File extension + QString file_name = fileInfo.fileName(); // filename + if(file_suffix != QString::fromLocal8Bit("jpg") && file_suffix != QString::fromLocal8Bit("png")){ + delete proDlg; + imageUrlMap.clear(); + qDebug() << "Unsupported type..."; + QString info = tr("Unsupported type: ") + file_suffix; + QMessageBox::warning(NULL, tr("Wechat Image Hosting"), info); + return; + } + + qint64 file_size = fileInfo.size(); // Unit is byte + qDebug() << "Image size: " << file_size; + if(file_size > 1024*1024){ + delete proDlg; + imageUrlMap.clear(); + qDebug() << "The size of the picture is more than 1M"; + QString info = tr("The size of the picture is more than 1M! Wechat API does not support!!"); + QMessageBox::warning(NULL, tr("Wechat Image Hosting"), info); + return; + } + + QString upload_img_url = "https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=" + token; + + QNetworkRequest request; + request.setUrl(upload_img_url); + if(reply != Q_NULLPTR){ + reply->deleteLater(); + } + + QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + QHttpPart imagePart; + imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); + QString filename = image_path.split(QDir::separator()).last(); + QString contentVariant = QString("form-data; name=\"media\"; filename=\"%1\";").arg(filename); + imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant(contentVariant)); + QFile *file = new QFile(image_path); + if(!file->open(QIODevice::ReadOnly)){ + qDebug() << "File open failed"; + } + imagePart.setBodyDevice(file); + file->setParent(multiPart); + multiPart->append(imagePart); + + // Set boundary + // Because boundary is quoted by QNetworkAccessManager, the wechat api is not recognized... + QByteArray m_boundary; + m_boundary.append("multipart/form-data; boundary="); + m_boundary.append(multiPart->boundary()); + request.setRawHeader(QByteArray("Content-Type"), m_boundary); + + reply = manager.post(request, multiPart); + multiPart->setParent(reply); + + + qDebug() << "Start uploading images: " + image_path + " Waiting for upload to complete"; + uploadImageStatus=true; + currentUploadImage = image_path; + connect(reply, &QNetworkReply::finished, this, &VMdTab::wechatImageBedUploadFinished); + +} + +void VMdTab::wechatImageBedUploadFinished() +{ + if(proDlg->wasCanceled()){ + qDebug() << "User stops uploading"; + reply->abort(); + // If the upload was successful, don't use it!!! + imageUrlMap.clear(); + return; + } + + switch (reply->error()) { + case QNetworkReply::NoError: + { + QByteArray bytes = reply->readAll(); + + //qDebug() << "The returned contents are as follows: "; + //QString a = bytes; + //qDebug() << qPrintable(a); + + QJsonDocument document = QJsonDocument::fromJson(bytes); + if(!document.isNull()){ + if(document.isObject()){ + QJsonObject object = document.object(); + if(object.contains("url")){ + QJsonValue value = object.value("url"); + if(value.isString()){ + qDebug() << "Authentication successful, get online link"; + imageUploaded = true; + proDlg->setValue(uploadImageCount); + + imageUrlMap.insert(currentUploadRelativeImagePah, value.toString()); + newFileContent.replace(currentUploadRelativeImagePah, value.toString()); + // Start calling the method. Whether the value in the map is empty determines whether to stop + wechatImageBedUploadManager(); + } + }else{ + delete proDlg; + imageUrlMap.clear(); + qDebug() << "Upload failure: "; + QString error = bytes; + qDebug() << bytes; + QString info = tr("upload failed! Please contact the developer~"); + QMessageBox::warning(NULL, tr("Wechat Image Hosting"), info); + } + } + }else{ + delete proDlg; + imageUrlMap.clear(); + qDebug() << "Resolution failure!"; + qDebug() << "Resolution failure's json: " << bytes; + QString info = tr("Json decode error, Please contact the developer~"); + QMessageBox::warning(NULL, tr("Wechat Image Hosting"), info); + } + + break; + } + default: + { + delete proDlg; + qDebug()<<"Network error: " << reply->errorString() << " error " << reply->error(); + + QString info = tr("Uploading ") + currentUploadImage + tr(" \n\nNetwork error: ") + reply->errorString() + tr("\n\nPlease check the network or image size"); + QMessageBox::warning(NULL, tr("Wechat Image Hosting"), info); + + } + + } +} +void VMdTab::wechatImageBedReplaceLink(QString file_content, QString file_path) +{ + // Write content to clipboard + QClipboard *board = QApplication::clipboard(); + board->setText(file_content); + QString url = g_config->getMarkdown2WechatToolUrl(); + if(url.isEmpty()){ + QMessageBox::warning(NULL, tr("Wechat Image Hosting"), tr("The article has been copied to the clipboard. Please find a text file and save it!!")); + }else{ + QMessageBox::StandardButton result; + result = QMessageBox::question(this, tr("Wechat Image Hosting"), tr("The article has been copied to the clipboard. Do you want to open the tool link of mark down to wechat?"), QMessageBox::Yes|QMessageBox::No,QMessageBox::Yes); + if(result == QMessageBox::Yes){ + QDesktopServices::openUrl(QUrl(url)); + } + } + imageUrlMap.clear(); + imageUploaded = false; // reset +} + VWordCountInfo VMdTab::fetchWordCountInfo(bool p_editMode) const { if (p_editMode) { diff --git a/src/vmdtab.h b/src/vmdtab.h index 7de0d442..49e230ec 100644 --- a/src/vmdtab.h +++ b/src/vmdtab.h @@ -1,9 +1,12 @@ #ifndef VMDTAB_H #define VMDTAB_H +#include #include #include #include +#include +#include #include "vedittab.h" #include "vconstants.h" #include "vmarkdownconverter.h" @@ -107,6 +110,27 @@ public: bool expandRestorePreviewArea(); + // github image hosting + // GitHub identity authentication + void authenticateGithubImageHosting(QString p_token); + // Upload a single image + void githubImageBedUploadImage(QString username,QString repository,QString image_path,QString token); + // Parameters needed to generate uploaded images + QString githubImageBedGenerateParam(QString image_path); + // Control image upload + void githubImageBedUploadManager(); + // Replace old links with new ones for images + void githubImageBedReplaceLink(QString file_content, QString file_path); + + // wechat image hosting + void authenticateWechatImageHosting(QString appid, QString secret); + // Control image upload + void wechatImageBedUploadManager(); + // Replace old links with new ones for images + void wechatImageBedReplaceLink(QString file_content, QString file_path); + // Upload a single image + void wechatImageBedUploadImage(QString image_path,QString token); + public slots: // Enter edit mode. void editFile() Q_DECL_OVERRIDE; @@ -161,6 +185,24 @@ private slots: // Selection changed in web. void handleWebSelectionChanged(); + // Process the image upload request to GitHub + void handleUploadImageToGithubRequested(); + + // GitHub image hosting identity authentication completed + void githubImageBedAuthFinished(); + + // GitHub image hosting upload completed + void githubImageBedUploadFinished(); + + // Process image upload request to wechat + void handleUploadImageToWechatRequested(); + + // Wechat mage hosting identity authentication completed + void wechatImageBedAuthFinished(); + + // Wechat image hosting upload completed + void wechatImageBedUploadFinished(); + private: enum TabReady { None = 0, ReadMode = 0x1, EditMode = 0x2 }; @@ -277,6 +319,29 @@ private: VMathJaxInplacePreviewHelper *m_mathjaxPreviewHelper; int m_documentID; + + QNetworkAccessManager manager; + QNetworkReply *reply; + QMap imageUrlMap; + // Similar to _v_image/ + QString imageBasePath; + // Replace the file content with the new link + QString newFileContent; + // Whether the picture has been uploaded successfully + bool imageUploaded; + // Image upload progress bar + QProgressDialog *proDlg; + // Total number of images to upload + int uploadImageCount; + int uploadImageCountIndex; + // Currently uploaded picture name + QString currentUploadImage; + // Image upload status + bool uploadImageStatus; + // Token returned after successful wechat authentication + QString wechatAccessToken; + // Relative image path currently Uploaded + QString currentUploadRelativeImagePah; }; inline VMdEditor *VMdTab::getEditor() diff --git a/src/vwebview.cpp b/src/vwebview.cpp index 2524ba1e..006b61f7 100644 --- a/src/vwebview.cpp +++ b/src/vwebview.cpp @@ -98,6 +98,16 @@ void VWebView::contextMenuEvent(QContextMenuEvent *p_event) connect(savePageAct, &QAction::triggered, this, &VWebView::requestSavePage); menu->addAction(savePageAct); + + // In preview mode, add the right-click menu and upload the image to GitHub image hosting + QAction *uploadImageToGithub = new QAction(tr("Upload Image To &GitHub"),menu); + connect(uploadImageToGithub, &QAction::triggered, this, &VWebView::requestUploadImageToGithub); + menu->addAction(uploadImageToGithub); + + // In preview mode, add the right-click menu and upload the image to Wechat image hosting + QAction *uploadImageToWechat = new QAction(tr("Upload Image To &Wechat"),menu); + connect(uploadImageToWechat, &QAction::triggered, this, &VWebView::requestUploadImageToWechat); + menu->addAction(uploadImageToWechat); } } diff --git a/src/vwebview.h b/src/vwebview.h index 08988bc6..27b6c586 100644 --- a/src/vwebview.h +++ b/src/vwebview.h @@ -24,6 +24,10 @@ signals: void requestExpandRestorePreviewArea(); + void requestUploadImageToGithub(); + + void requestUploadImageToWechat(); + protected: void contextMenuEvent(QContextMenuEvent *p_event);