Browse Source

editor: add supports for GitHub and WeChat image hosting (#1007)

* 增加了github imagebed的配置窗口

* 已经完成批量上传

* 基本上可以使用

* 基本上可以使用

* 加入进度条

* 就差开始的认证过程

* 差不多就这样了

* 修改中文的readme

* 修改了一下readme

* 找到不能传gif的问题所在

* 修改gif不能上传的bug

* 增加图床使用说明限制

* 增加一些注释

* 将进度条delete调

* 增加大文件上传错误提示

* 修复cancel的问题

* 已知问题: 上传多张图片时, 较大的图片的进度条不一定能出来

* 将进度条弹出来的时候修改成1s后

* 加入wechat设置, 改成QTabWidget

* wechat设置成功

* 成功获取了token

* 先保存一下, 文件上传有bug,替换好像也有bug

* 解决了boundary的引号导致图片不能上传的问题

* 注释掉一些输出

* 加入ip不在白名单的提示

* 增加图片大小大于1M的提示

* 将ip地址设置进剪切板, 并显示在弹框上

* 添加markdown转微信的url设置

* 增加自动打开openwrite的链接, 修复文件大小应为1024*1024

* 改成clear

* 将类型校验放到开始的地方

* 加入openwrite默认为空的判断

* Delete github-imagebed.md

* Apply suggestions from code review

Co-Authored-By: Le Tan <[email protected]>

* Apply suggestions from code review

Co-Authored-By: Le Tan <[email protected]>

* Apply suggestions from code review

Co-Authored-By: Le Tan <[email protected]>

* 根据要求修改了一下

* changed indentation and deleted empty line

* add some tr()

* Delete .DS_Store

* delete some comment and use CamelCase

* resolved sth
冯文华 6 years ago
parent
commit
c828ef00c4
12 changed files with 1104 additions and 1 deletions
  1. 1 0
      .gitignore
  2. 1 0
      README.md
  3. 1 0
      README_zh.md
  4. 178 1
      src/dialog/vsettingsdialog.cpp
  5. 42 0
      src/dialog/vsettingsdialog.h
  6. 9 0
      src/resources/vnote.ini
  7. 8 0
      src/vconfigmanager.cpp
  8. 119 0
      src/vconfigmanager.h
  9. 666 0
      src/vmdtab.cpp
  10. 65 0
      src/vmdtab.h
  11. 10 0
      src/vwebview.cpp
  12. 4 0
      src/vwebview.h

+ 1 - 0
.gitignore

@@ -1,2 +1,3 @@
 VNote.pro.user*
 CMakeLists.txt.user
+.DS_Store

+ 1 - 0
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.

+ 1 - 0
README_zh.md

@@ -110,6 +110,7 @@ VNote 不是一个简单的 Markdown 编辑器。通过提供笔记管理功能
 - 笔记附件;
 - 主题以及深色模式;
 - 丰富、可扩展的导出,包括 HTML、PDF、PDF(多合一)和图片;
+- GitHub和微信图床;
 
 # 捐赠
 您可以通过很多途径帮助 VNote 的开发。

+ 178 - 1
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<VImageHostingTab *>(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<VImageHostingTab *>(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;
+}

+ 42 - 0
src/dialog/vsettingsdialog.h

@@ -4,6 +4,7 @@
 #include <QDialog>
 #include <QVector>
 #include <QString>
+#include <QTabWidget>
 
 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

+ 9 - 0
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
 

+ 8 - 0
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();

+ 119 - 0
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

+ 666 - 0
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<ImageLink> 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;i<images.size() ;i++)
+                {
+                    if(images[i].m_url.contains(".png") || images[i].m_url.contains(".jpg")|| images[i].m_url.contains(".gif")){
+                        imageUrlMap.insert(images[i].m_url,"");
+                    }else{
+                        delete proDlg;
+                        imageUrlMap.clear();
+                        qDebug() << "Unsupported type...";
+                        QFileInfo fileInfo(images[i].m_path.toLocal8Bit());
+                        QString fileSuffix = fileInfo.suffix();
+                        QString info = tr("Unsupported type: ") + fileSuffix;
+                        QMessageBox::warning(NULL, tr("Wechat Image Hosting"), info);
+                        return;
+                    }
+                }
+                githubImageBedUploadManager();
+            }
+            else
+            {
+                qDebug() << m_file->getName() << " 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<QString, QString> 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<QString> klist =  imageUrlMap.keys();
+                                QString temp;
+                                for(int i=0;i<klist.count();i++)
+                                {
+
+                                    temp = klist[i].split("/")[1];
+                                    if(imageName.contains(temp))
+                                    {
+                                        // You can assign values in the map
+                                        imageUrlMap.insert(klist[i], downloadUrl);
+
+                                        // Replace the link in the original
+                                        newFileContent.replace(klist[i], downloadUrl);
+
+                                        break;
+                                    }
+                                }
+                                // Start calling the method. Whether the value in the map is empty determines whether to stop
+                                githubImageBedUploadManager();
+                            }
+                        }
+                    }
+                }
+                else{
+                    delete proDlg;
+                    imageUrlMap.clear();
+                    qDebug() << "Resolution failure!";
+                    qDebug() << "Resolution failure's json: " << bytes;
+                    if(imageUploaded){
+                        githubImageBedReplaceLink(newFileContent, m_file->fetchPath());
+                    }
+                    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<ImageLink> 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;i<images.size() ;i++)
+                                {
+                                    if(images[i].m_url.contains(".png") || images[i].m_url.contains(".jpg")){
+                                        imageUrlMap.insert(images[i].m_url,"");
+                                    }else{
+                                        delete proDlg;
+                                        imageUrlMap.clear();
+                                        qDebug() << "Unsupported type...";
+                                        QFileInfo file_info(images[i].m_path.toLocal8Bit());
+                                        QString file_suffix = file_info.suffix();
+                                        QString info = tr("Unsupported type: ") + file_suffix;
+                                        QMessageBox::warning(NULL, tr("Wechat Image Hosting"), info);
+                                        return;
+                                    }
+                                }
+                                wechatImageBedUploadManager();
+                            }
+                            else
+                            {
+                                qDebug() << m_file->getName() << " 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<QString, QString> 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) {

+ 65 - 0
src/vmdtab.h

@@ -1,9 +1,12 @@
 #ifndef VMDTAB_H
 #define VMDTAB_H
 
+#include <QtNetwork>
 #include <QString>
 #include <QPointer>
 #include <QSharedPointer>
+#include <QProgressDialog>
+#include <QDesktopServices>
 #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<QString, QString> 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()

+ 10 - 0
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);
         }
     }
 

+ 4 - 0
src/vwebview.h

@@ -24,6 +24,10 @@ signals:
 
     void requestExpandRestorePreviewArea();
 
+    void requestUploadImageToGithub();
+
+    void requestUploadImageToWechat();
+
 protected:
     void contextMenuEvent(QContextMenuEvent *p_event);