From 0a2661f9865b29720d7248dc1e04626a3cf08775 Mon Sep 17 00:00:00 2001 From: Xiao YiFang Date: Sat, 21 May 2022 14:03:26 +0800 Subject: [PATCH] add 'send to anki' function users can configure the ankiconnect to use together with anki --- ankiconnector.cpp | 85 ++++++++++++++++++++++++++++++ ankiconnector.h | 28 ++++++++++ articleview.cc | 22 ++++++++ articleview.hh | 4 ++ config.cc | 10 ++++ config.hh | 2 + goldendict.pro | 2 + locale/zh_CN.ts | 49 ++++++++++++++++- preferences.cc | 4 ++ preferences.ui | 131 ++++++++++++++++++++++++++++++---------------- utils.hh | 7 +++ 11 files changed, 297 insertions(+), 47 deletions(-) create mode 100644 ankiconnector.cpp create mode 100644 ankiconnector.h diff --git a/ankiconnector.cpp b/ankiconnector.cpp new file mode 100644 index 00000000..b7e11fc0 --- /dev/null +++ b/ankiconnector.cpp @@ -0,0 +1,85 @@ +#include "ankiconnector.h" +#include +#include +#include +#include "utils.hh" +AnkiConnector::AnkiConnector( QObject * parent, Config::Class const & _cfg ) : QObject{ parent }, cfg( _cfg ) +{ + mgr = new QNetworkAccessManager( this ); + connect( mgr, &QNetworkAccessManager::finished, this, &AnkiConnector::finishedSlot ); +} + +void AnkiConnector::sendToAnki( QString const & word, QString const & text ) +{ + //for simplicity. maybe use QJsonDocument in future? + QString postTemplate = QString( "{" + "\"action\": \"addNote\"," + "\"version\": 6," + "\"params\": {" + " \"note\": {" + " \"deckName\": \"%1\"," + " \"modelName\": \"%2\"," + " \"fields\":%3," + " \"options\": {" + " \"allowDuplicate\": true" + " }," + " \"tags\": []" + "}" + "}" + "}" + "" ); + + QJsonObject fields; + fields.insert( "Front", word ); + fields.insert( "Back", text ); + + QString postData = postTemplate.arg( cfg.preferences.ankiConnectServer.deck, + cfg.preferences.ankiConnectServer.model, + Utils::json2String( fields ) ); + +// qDebug().noquote() << postData; + QUrl url; + url.setScheme( "http" ); + url.setHost( cfg.preferences.ankiConnectServer.host ); + url.setPort( cfg.preferences.ankiConnectServer.port ); + QNetworkRequest request( url ); + request.setTransferTimeout( 3000 ); + // request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy ); + request.setHeader( QNetworkRequest::ContentTypeHeader, "applicaion/json" ); + auto reply = mgr->post( request, postData.toUtf8() ); + connect( reply, + &QNetworkReply::errorOccurred, + this, + [ this ]( QNetworkReply::NetworkError e ) + { + qWarning() << e; + emit this->errorText( tr( "anki: post to anki failed" ) ); + } ); +} + +void AnkiConnector::finishedSlot( QNetworkReply * reply ) +{ + if( reply->error() == QNetworkReply::NoError ) + { + QByteArray bytes = reply->readAll(); + QJsonDocument json = QJsonDocument::fromJson( bytes ); + auto obj = json.object(); + if( obj.size() != 2 || !obj.contains( "error" ) || !obj.contains( "result" ) || + obj[ "result" ].toString().isEmpty() ) + { + emit errorText( QObject::tr( "anki: post to anki failed" ) ); + } + QString result = obj[ "result" ].toString(); + + qDebug() << "anki result:" << result; + + emit errorText( tr( "anki: post to anki success" ) ); + } + else + { + qDebug() << "anki connect error" << reply->errorString(); + emit errorText( "anki:" + reply->errorString() ); + } + + reply->deleteLater(); +} diff --git a/ankiconnector.h b/ankiconnector.h new file mode 100644 index 00000000..e52a0f80 --- /dev/null +++ b/ankiconnector.h @@ -0,0 +1,28 @@ +#ifndef ANKICONNECTOR_H +#define ANKICONNECTOR_H + +#include "config.hh" + +#include +#include +#include + +class AnkiConnector : public QObject +{ + Q_OBJECT +public: + explicit AnkiConnector( QObject * parent, Config::Class const & cfg ); + + void sendToAnki( QString const & word, QString const & text ); + +private: + QNetworkAccessManager * mgr; + Config::Class const & cfg; +public : +signals: + void errorText( QString const & ); +private slots: + void finishedSlot(QNetworkReply * reply); +}; + +#endif // ANKICONNECTOR_H diff --git a/articleview.cc b/articleview.cc index d8382f98..88b1f71f 100644 --- a/articleview.cc +++ b/articleview.cc @@ -348,6 +348,11 @@ ArticleView::ArticleView( QWidget * parent, ArticleNetworkAccessManager & nm, Au channel = new QWebChannel(ui.definition->page()); agent = new ArticleViewAgent(this); attachWebChannelToHtml(); + ankiConnector = new AnkiConnector( this, cfg ); + connect( ankiConnector, + &AnkiConnector::errorText, + this, + [ this ]( QString const & errorText ) { emit statusBarMessage( errorText ); } ); } // explicitly report the minimum size, to avoid @@ -489,6 +494,10 @@ void ArticleView::showDefinition( QString const & word, QStringList const & dict ui.definition->setCursor( Qt::WaitCursor ); } +void ArticleView::sendToAnki(QString const & word, QString const & text ){ + ankiConnector->sendToAnki(word,text); +} + void ArticleView::showAnticipation() { ui.definition->setHtml( "" ); @@ -1721,6 +1730,7 @@ void ArticleView::contextMenuRequested( QPoint const & pos ) QAction * followLinkExternal = 0; QAction * followLinkNewTab = 0; QAction * lookupSelection = 0; + QAction * sendToAnkiAction = 0 ; QAction * lookupSelectionGr = 0; QAction * lookupSelectionNewTab = 0; QAction * lookupSelectionNewTabGr = 0; @@ -1850,6 +1860,14 @@ void ArticleView::contextMenuRequested( QPoint const & pos ) } } + // add anki menu + if( !text.isEmpty() && cfg.preferences.ankiConnectServer.enabled ) + { + QString txt = ui.definition->title(); + sendToAnkiAction = new QAction( tr( "&Send \"%1\" to anki with selected text." ).arg( txt ), &menu ); + menu.addAction( sendToAnkiAction ); + } + if( text.isEmpty() && !cfg.preferences.storeHistory) { QString txt = ui.definition->title(); @@ -1942,6 +1960,10 @@ void ArticleView::contextMenuRequested( QPoint const & pos ) else if ( result == lookupSelection ) showDefinition( selectedText, getGroup( ui.definition->url() ), getCurrentArticle() ); + else if( result = sendToAnkiAction ) + { + sendToAnki( ui.definition->title(), ui.definition->selectedText() ); + } else if ( result == lookupSelectionGr && groupComboBox ) showDefinition( selectedText, groupComboBox->getCurrentGroup(), QString() ); diff --git a/articleview.hh b/articleview.hh index 921d640f..00d92a64 100644 --- a/articleview.hh +++ b/articleview.hh @@ -19,6 +19,7 @@ #if (QT_VERSION >= QT_VERSION_CHECK(6,0,0)) #include #endif +#include "ankiconnector.h" class ResourceToSaveHandler; class ArticleViewAgent ; @@ -39,6 +40,8 @@ class ArticleView: public QFrame ArticleViewAgent * agent; Ui::ArticleView ui; + AnkiConnector * ankiConnector; + QAction pasteAction, articleUpAction, articleDownAction, goBackAction, goForwardAction, selectCurrentArticleAction, copyAsTextAction, inspectAction; @@ -129,6 +132,7 @@ public: QRegExp const & searchRegExp, unsigned group, bool ignoreDiacritics ); + void sendToAnki(QString const & word, QString const & text ); /// Clears the view and sets the application-global waiting cursor, /// which will be restored when some article loads eventually. void showAnticipation(); diff --git a/config.cc b/config.cc index d5f6d7fd..f4311f55 100644 --- a/config.cc +++ b/config.cc @@ -948,6 +948,8 @@ Class load() c.preferences.ankiConnectServer.enabled = ( ankiConnectServer.toElement().attribute( "enabled" ) == "1" ); c.preferences.ankiConnectServer.host = ankiConnectServer.namedItem( "host" ).toElement().text(); c.preferences.ankiConnectServer.port = ankiConnectServer.namedItem( "port" ).toElement().text().toULong(); + c.preferences.ankiConnectServer.deck = ankiConnectServer.namedItem( "deck" ).toElement().text(); + c.preferences.ankiConnectServer.model = ankiConnectServer.namedItem( "model" ).toElement().text(); } if ( !preferences.namedItem( "checkForNewReleases" ).isNull() ) @@ -1901,6 +1903,14 @@ void save( Class const & c ) opt = dd.createElement( "port" ); opt.appendChild( dd.createTextNode( QString::number( c.preferences.ankiConnectServer.port ) ) ); proxy.appendChild( opt ); + + opt = dd.createElement( "deck" ); + opt.appendChild( dd.createTextNode( c.preferences.ankiConnectServer.deck ) ); + proxy.appendChild( opt ); + + opt = dd.createElement( "model" ); + opt.appendChild( dd.createTextNode( c.preferences.ankiConnectServer.model ) ); + proxy.appendChild( opt ); } opt = dd.createElement( "checkForNewReleases" ); diff --git a/config.hh b/config.hh index de463bf7..9d1a0ae9 100644 --- a/config.hh +++ b/config.hh @@ -143,6 +143,8 @@ struct AnkiConnectServer QString host; unsigned port; + QString deck; + QString model; AnkiConnectServer(); }; diff --git a/goldendict.pro b/goldendict.pro index a2c5133c..40eba305 100644 --- a/goldendict.pro +++ b/goldendict.pro @@ -223,6 +223,7 @@ DEFINES += PROGRAM_VERSION=\\\"$$VERSION\\\" # Input HEADERS += folding.hh \ + ankiconnector.h \ article_inspect.h \ articlewebpage.h \ globalbroadcaster.h \ @@ -364,6 +365,7 @@ FORMS += groups.ui \ fulltextsearch.ui SOURCES += folding.cc \ + ankiconnector.cpp \ article_inspect.cpp \ articlewebpage.cpp \ globalbroadcaster.cpp \ diff --git a/locale/zh_CN.ts b/locale/zh_CN.ts index 9c9557ca..832cea28 100644 --- a/locale/zh_CN.ts +++ b/locale/zh_CN.ts @@ -39,6 +39,20 @@ (c) 2008-2013 Konstantin Isakov (ikm@goldendict.org) + + AnkiConnector + + + anki: post to anki failed + anki:发布成功 + anki:发布失败 + + + + anki: post to anki success + anki: 发布成功 + + ArticleInspector @@ -315,7 +329,12 @@ 引用的音频播放程序不存在。 - + + &Send "%1" to anki with selected text. + 将“%1”发送到anki并附带选择的文本。 + + + Sound files (*.wav *.ogg *.oga *.mp3 *.mp4 *.aac *.flac *.mid *.wv *.ape);;All files (*.*) 声音文件(*.wav *.ogg *.oga *.mp3 *.mp4 *.aac *.flac *.mid *.wv *.ape);;所有文件(*.*) @@ -3903,7 +3922,27 @@ however, the article from the topmost dictionary is shown. 自定义设置 - + + Anki Connect + Anki连接 + + + + http:// + http:// + + + + Deck: + 牌组: + + + + Model: + 模板: + + + Some sites detect GoldenDict via HTTP headers and block the requests. Enable this option to workaround the problem. 部分网站屏蔽了使用 GoldenDict 浏览器标识(UA)的请求,启用此选项以绕过该问题。 @@ -4420,6 +4459,12 @@ from Stardict, Babylon and GLS dictionaries Date: %1%2 日期:%1%2 + + + + anki: post to anki failed + anki:发布失败 + QuickFilterLine diff --git a/preferences.cc b/preferences.cc index 6af7f8b6..8a8e6872 100644 --- a/preferences.cc +++ b/preferences.cc @@ -327,6 +327,8 @@ Preferences::Preferences( QWidget * parent, Config::Class & cfg_ ): ui.useAnkiConnect->setChecked( p.ankiConnectServer.enabled ); ui.ankiHost->setText( p.ankiConnectServer.host ); ui.ankiPort->setValue( p.ankiConnectServer.port ); + ui.ankiModel->setText( p.ankiConnectServer.model ); + ui.ankiDeck->setText(p.ankiConnectServer.deck); connect( ui.customProxy, SIGNAL( toggled( bool ) ), this, SLOT( customProxyToggled( bool ) ) ); @@ -475,6 +477,8 @@ Config::Preferences Preferences::getPreferences() p.ankiConnectServer.enabled = ui.useAnkiConnect->isChecked(); p.ankiConnectServer.host = ui.ankiHost->text(); p.ankiConnectServer.port = (unsigned)ui.ankiPort->value(); + p.ankiConnectServer.deck = ui.ankiDeck->text(); + p.ankiConnectServer.model = ui.ankiModel->text(); p.checkForNewReleases = ui.checkForNewReleases->isChecked(); p.disallowContentFromOtherSites = ui.disallowContentFromOtherSites->isChecked(); diff --git a/preferences.ui b/preferences.ui index 4aa7e8ff..f4df1e9f 100644 --- a/preferences.ui +++ b/preferences.ui @@ -24,7 +24,7 @@ - 4 + 0 @@ -1092,53 +1092,94 @@ for all program's network requests. false - + - - - Host: - - + + + + + Host: + + + + + + + http:// + + + + + + + + + + Port: + + + + + + + 65535 + + + 8080 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + - - - http:// - - - - - - - - - - Port: - - - - - - - 65535 - - - 8080 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - + + + + + Deck: + + + + + + + + + + Model: + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + diff --git a/utils.hh b/utils.hh index 2021223c..36318167 100644 --- a/utils.hh +++ b/utils.hh @@ -9,6 +9,8 @@ #include #include #include +#include +#include namespace Utils { @@ -77,6 +79,11 @@ inline bool ignoreKeyEvent(QKeyEvent *keyEvent) { return false; } +inline QString json2String( const QJsonObject & json ) +{ + return QString( QJsonDocument( json ).toJson( QJsonDocument::Compact ) ); +} + namespace AtomicInt {