diff --git a/ankiconnector.cpp b/ankiconnector.cpp new file mode 100644 index 000000000..b7e11fc0b --- /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 000000000..e52a0f807 --- /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 d8382f983..88b1f71f9 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 921d640f3..00d92a646 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 2e9262482..f4311f551 100644 --- a/config.cc +++ b/config.cc @@ -89,6 +89,10 @@ ProxyServer::ProxyServer(): enabled( false ), useSystemProxy( false ), type( Soc { } +AnkiConnectServer::AnkiConnectServer(): enabled( false ), host("127.0.0.1"), port( 8765 ) +{ +} + HotKey::HotKey(): modifiers( 0 ), key1( 0 ), key2( 0 ) { } @@ -937,6 +941,17 @@ Class load() c.preferences.proxyServer.systemProxyPassword = proxy.namedItem( "systemProxyPassword" ).toElement().text(); } + QDomNode ankiConnectServer = preferences.namedItem( "ankiConnectServer" ); + + if ( !ankiConnectServer.isNull() ) + { + 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() ) c.preferences.checkForNewReleases = ( preferences.namedItem( "checkForNewReleases" ).toElement().text() == "1" ); @@ -1872,6 +1887,32 @@ void save( Class const & c ) proxy.appendChild( opt ); } + //anki connect + { + QDomElement proxy = dd.createElement( "ankiConnectServer" ); + preferences.appendChild( proxy ); + + QDomAttr enabled = dd.createAttribute( "enabled" ); + enabled.setValue( c.preferences.ankiConnectServer.enabled ? "1" : "0" ); + proxy.setAttributeNode( enabled ); + + opt = dd.createElement( "host" ); + opt.appendChild( dd.createTextNode( c.preferences.ankiConnectServer.host ) ); + proxy.appendChild( opt ); + + 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" ); opt.appendChild( dd.createTextNode( c.preferences.checkForNewReleases ? "1" : "0" ) ); preferences.appendChild( opt ); diff --git a/config.hh b/config.hh index 774dbdef1..9d1a0ae92 100644 --- a/config.hh +++ b/config.hh @@ -137,6 +137,18 @@ struct ProxyServer ProxyServer(); }; +struct AnkiConnectServer +{ + bool enabled; + + QString host; + unsigned port; + QString deck; + QString model; + + AnkiConnectServer(); +}; + // A hotkey -- currently qt modifiers plus one or two keys struct HotKey { @@ -329,6 +341,7 @@ struct Preferences QString audioPlaybackProgram; ProxyServer proxyServer; + AnkiConnectServer ankiConnectServer; bool checkForNewReleases; bool disallowContentFromOtherSites; diff --git a/goldendict.pro b/goldendict.pro index a2c5133cc..40eba3057 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 9c9557ca8..832cea287 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 6bafa8b7c..8a8e68729 100644 --- a/preferences.cc +++ b/preferences.cc @@ -323,6 +323,13 @@ Preferences::Preferences( QWidget * parent, Config::Class & cfg_ ): ui.customSettingsGroup->setEnabled( p.proxyServer.enabled ); } + //anki connect + 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 ) ) ); @@ -466,6 +473,13 @@ Config::Preferences Preferences::getPreferences() p.proxyServer.user = ui.proxyUser->text(); p.proxyServer.password = ui.proxyPassword->text(); + //anki connect + 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(); p.enableWebPlugins = ui.enableWebPlugins->isChecked(); diff --git a/preferences.ui b/preferences.ui index 8ce260f2b..f4df1e9fc 100644 --- a/preferences.ui +++ b/preferences.ui @@ -993,6 +993,9 @@ for all program's network requests. + + false + Custom settings @@ -1011,6 +1014,9 @@ for all program's network requests. + + false + Host: @@ -1073,20 +1079,110 @@ for all program's network requests. - - - Qt::Vertical + + + true - - QSizePolicy::Fixed + + Anki Connect - - - 20 - 10 - + + true - + + false + + + + + + + + Host: + + + + + + + http:// + + + + + + + + + + Port: + + + + + + + 65535 + + + 8080 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Deck: + + + + + + + + + + Model: + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + @@ -1176,22 +1272,6 @@ clears its network cache from disk during exit. - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 10 - - - - diff --git a/utils.hh b/utils.hh index 2021223c8..36318167a 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 {