add 'send to anki' function

users can configure the ankiconnect to use together with anki
This commit is contained in:
Xiao YiFang 2022-05-21 14:03:26 +08:00
parent fbfc2d7f43
commit 0a2661f986
11 changed files with 297 additions and 47 deletions

85
ankiconnector.cpp Normal file
View file

@ -0,0 +1,85 @@
#include "ankiconnector.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#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();
}

28
ankiconnector.h Normal file
View file

@ -0,0 +1,28 @@
#ifndef ANKICONNECTOR_H
#define ANKICONNECTOR_H
#include "config.hh"
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QObject>
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

View file

@ -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() );

View file

@ -19,6 +19,7 @@
#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
#include <QtCore5Compat/QRegExp>
#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();

View file

@ -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" );

View file

@ -143,6 +143,8 @@ struct AnkiConnectServer
QString host;
unsigned port;
QString deck;
QString model;
AnkiConnectServer();
};

View file

@ -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 \

View file

@ -39,6 +39,20 @@
<translation>(c) 2008-2013 Konstantin Isakov (ikm@goldendict.org)</translation>
</message>
</context>
<context>
<name>AnkiConnector</name>
<message>
<location filename="../ankiconnector.cpp" line="56"/>
<source>anki: post to anki failed</source>
<translatorcomment>anki:发布成功</translatorcomment>
<translation>anki:发布失败</translation>
</message>
<message>
<location filename="../ankiconnector.cpp" line="76"/>
<source>anki: post to anki success</source>
<translation>anki: 发布成功</translation>
</message>
</context>
<context>
<name>ArticleInspector</name>
<message>
@ -315,7 +329,12 @@
<translation></translation>
</message>
<message>
<location filename="../articleview.cc" line="1971"/>
<location filename="../articleview.cc" line="1867"/>
<source>&amp;Send &quot;%1&quot; to anki with selected text.</source>
<translation>%1anki并附带选择的文本</translation>
</message>
<message>
<location filename="../articleview.cc" line="2016"/>
<source>Sound files (*.wav *.ogg *.oga *.mp3 *.mp4 *.aac *.flac *.mid *.wv *.ape);;All files (*.*)</source>
<translation>(*.wav *.ogg *.oga *.mp3 *.mp4 *.aac *.flac *.mid *.wv *.ape);;*.*</translation>
</message>
@ -3903,7 +3922,27 @@ however, the article from the topmost dictionary is shown.</source>
<translation></translation>
</message>
<message>
<location filename="../preferences.ui" line="1147"/>
<location filename="../preferences.ui" line="1087"/>
<source>Anki Connect</source>
<translation>Anki连接</translation>
</message>
<message>
<location filename="../preferences.ui" line="1108"/>
<source>http://</source>
<translation>http://</translation>
</message>
<message>
<location filename="../preferences.ui" line="1152"/>
<source>Deck:</source>
<translation></translation>
</message>
<message>
<location filename="../preferences.ui" line="1162"/>
<source>Model:</source>
<translation></translation>
</message>
<message>
<location filename="../preferences.ui" line="1214"/>
<source>Some sites detect GoldenDict via HTTP headers and block the requests.
Enable this option to workaround the problem.</source>
<translation>使 GoldenDict UA</translation>
@ -4420,6 +4459,12 @@ from Stardict, Babylon and GLS dictionaries</source>
<source>Date: %1%2</source>
<translation>%1%2</translation>
</message>
<message>
<location filename="../ankiconnector.cpp" line="55"/>
<location filename="../ankiconnector.cpp" line="69"/>
<source>anki: post to anki failed</source>
<translation>anki:发布失败</translation>
</message>
</context>
<context>
<name>QuickFilterLine</name>

View file

@ -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();

View file

@ -24,7 +24,7 @@
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>4</number>
<number>0</number>
</property>
<property name="iconSize">
<size>
@ -1092,53 +1092,94 @@ for all program's network requests.</string>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_17">
<layout class="QVBoxLayout" name="verticalLayout_13">
<item>
<widget class="QLabel" name="label_22">
<property name="text">
<string>Host:</string>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout_21">
<item>
<widget class="QLabel" name="label_22">
<property name="text">
<string>Host:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_24">
<property name="text">
<string>http://</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="ankiHost"/>
</item>
<item>
<widget class="QLabel" name="label_23">
<property name="text">
<string>Port:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="ankiPort">
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>8080</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_16">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_24">
<property name="text">
<string>http://</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="ankiHost"/>
</item>
<item>
<widget class="QLabel" name="label_23">
<property name="text">
<string>Port:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="ankiPort">
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>8080</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_16">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
<layout class="QHBoxLayout" name="horizontalLayout_22">
<item>
<widget class="QLabel" name="label_28">
<property name="text">
<string>Deck:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="ankiDeck"/>
</item>
<item>
<widget class="QLabel" name="label_25">
<property name="text">
<string>Model:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="ankiModel"/>
</item>
<item>
<spacer name="horizontalSpacer_17">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>

View file

@ -9,6 +9,8 @@
#include <QKeyEvent>
#include <QUrl>
#include <QUrlQuery>
#include <QJsonObject>
#include <QJsonDocument>
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
{