";
+ // If the user has enabled Anki integration in settings,
+ // Show a (+) button that lets the user add a new Anki card.
+ if ( ankiConnectEnabled() ) {
+ QString link{ R"EOF(
+
+
+
+ )EOF" };
+ head += link.arg( Html::escape( dictId ).c_str(), tr( "Make a new Anki note" ) ).toStdString();
+ }
+
head += "
getLangFrom() ).toLatin1().data();
head += "\" lang=\"";
diff --git a/article_maker.hh b/article_maker.hh
index 4d30adde..5a875ef1 100644
--- a/article_maker.hh
+++ b/article_maker.hh
@@ -87,11 +87,12 @@ class ArticleRequest: public Dictionary::DataRequest
std::set< gd::wstring > alts; // Accumulated main forms
std::list< sptr< Dictionary::WordSearchRequest > > altSearches;
- bool altsDone, bodyDone;
std::list< sptr< Dictionary::DataRequest > > bodyRequests;
- bool foundAnyDefinitions;
- bool closePrevSpan; // Indicates whether the last opened article span is to
- // be closed after the article ends.
+ bool altsDone{ false };
+ bool bodyDone{ false };
+ bool foundAnyDefinitions{ false };
+ bool closePrevSpan{ false }; // Indicates whether the last opened article span is to
+ // be closed after the article ends.
sptr< WordFinder > stemmedWordFinder; // Used when there're no results
/// A sequence of words and spacings between them, including the initial
diff --git a/config.cc b/config.cc
index bdd510a6..5a1ce677 100644
--- a/config.cc
+++ b/config.cc
@@ -976,7 +976,7 @@ 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.port = ankiConnectServer.namedItem( "port" ).toElement().text().toInt();
c.preferences.ankiConnectServer.deck = ankiConnectServer.namedItem( "deck" ).toElement().text();
c.preferences.ankiConnectServer.model = ankiConnectServer.namedItem( "model" ).toElement().text();
diff --git a/config.hh b/config.hh
index 69db5d0f..2dd81d17 100644
--- a/config.hh
+++ b/config.hh
@@ -143,7 +143,8 @@ struct AnkiConnectServer
bool enabled;
QString host;
- unsigned port;
+ int port; // Port will be passed to QUrl::setPort() which expects an int.
+
QString deck;
QString model;
diff --git a/icons/add-anki-icon.svg b/icons/add-anki-icon.svg
new file mode 100644
index 00000000..4309930d
--- /dev/null
+++ b/icons/add-anki-icon.svg
@@ -0,0 +1,24 @@
+
+
\ No newline at end of file
diff --git a/resources.qrc b/resources.qrc
index 24046f73..1a1a5c47 100644
--- a/resources.qrc
+++ b/resources.qrc
@@ -1,5 +1,6 @@
+ icons/add-anki-icon.svgversion.txticons/arrow.pngicons/prefix.png
diff --git a/src/ui/articleview.cpp b/src/ui/articleview.cpp
index 5a45bf68..8a4ad182 100644
--- a/src/ui/articleview.cpp
+++ b/src/ui/articleview.cpp
@@ -391,6 +391,13 @@ ArticleView::ArticleView( QWidget * parent, ArticleNetworkAccessManager & nm, Au
&AnkiConnector::errorText,
this,
[ this ]( QString const & errorText ) { emit statusBarMessage( errorText ); } );
+
+ // Set up an Anki action if Anki integration is enabled in settings.
+ if ( cfg.preferences.ankiConnectServer.enabled ) {
+ sendToAnkiAction.setShortcut( QKeySequence( "Ctrl+Shift+N" ) );
+ webview->addAction( &sendToAnkiAction );
+ connect( &sendToAnkiAction, &QAction::triggered, this, &ArticleView::handleAnkiAction );
+ }
}
// explicitly report the minimum size, to avoid
@@ -532,8 +539,9 @@ void ArticleView::showDefinition( QString const & word, QStringList const & dict
webview->setCursor( Qt::WaitCursor );
}
-void ArticleView::sendToAnki(QString const & word, QString const & text, QString const & sentence ){
- ankiConnector->sendToAnki(word,text,sentence);
+void ArticleView::sendToAnki( QString const & word, QString const & dict_definition, QString const & sentence )
+{
+ ankiConnector->sendToAnki( word, dict_definition, sentence );
}
void ArticleView::showAnticipation()
@@ -1124,6 +1132,15 @@ void ArticleView::linkClickedInHtml( QUrl const & url_ )
linkClicked( url_ );
}
}
+
+void ArticleView::makeAnkiCardFromArticle( QString const & article_id )
+{
+ auto const js_code = QString( R"EOF(document.getElementById("gdarticlefrom-%1").innerText)EOF" ).arg( article_id );
+ webview->page()->runJavaScript( js_code, [ this ]( const QVariant & article_text ) {
+ sendToAnki( webview->title(), article_text.toString(), translateLine->text() );
+ } );
+}
+
void ArticleView::openLink( QUrl const & url, QUrl const & ref, QString const & scrollTo, Contexts const & contexts_ )
{
audioPlayer->stop();
@@ -1142,6 +1159,19 @@ void ArticleView::openLink( QUrl const & url, QUrl const & ref, QString const &
load( url );
else if( url.scheme().compare( "ankisearch" ) == 0 ) {
ankiConnector->ankiSearch( url.path() );
+ return;
+ }
+ else if ( url.scheme().compare( "ankicard" ) == 0 ) {
+ // If article id is set in path and selection is empty, use text from the current article.
+ // Otherwise, grab currently selected text and use it as the definition.
+ if ( !url.path().isEmpty() && webview->selectedText().isEmpty() ) {
+ makeAnkiCardFromArticle( url.path() );
+ }
+ else {
+ sendToAnki( webview->title(), webview->selectedText(), translateLine->text() );
+ }
+ qDebug() << "requested to make Anki card.";
+ return;
}
else if( url.scheme().compare( "bword" ) == 0 || url.scheme().compare( "entry" ) == 0 ) {
if( Utils::Url::hasQueryItem( ref, "dictionaries" ) )
@@ -1617,6 +1647,18 @@ void ArticleView::forward()
webview->forward();
}
+void ArticleView::handleAnkiAction()
+{
+ // React to the "send *word* to anki" action.
+ // If selected text is empty, use the whole article as the definition.
+ if ( webview->selectedText().isEmpty() ) {
+ makeAnkiCardFromArticle( getActiveArticleId() );
+ }
+ else {
+ sendToAnki( webview->title(), webview->selectedText(), translateLine->text() );
+ }
+}
+
void ArticleView::reload() { webview->reload(); }
void ArticleView::hasSound( const std::function< void( bool ) > & callback )
@@ -1706,8 +1748,7 @@ void ArticleView::contextMenuRequested( QPoint const & pos )
QAction * followLink = 0;
QAction * followLinkExternal = 0;
QAction * followLinkNewTab = 0;
- QAction * lookupSelection = 0;
- QAction * sendToAnkiAction = 0 ;
+ QAction * lookupSelection = 0;
QAction * lookupSelectionGr = 0;
QAction * lookupSelectionNewTab = 0;
QAction * lookupSelectionNewTabGr = 0;
@@ -1773,7 +1814,7 @@ void ArticleView::contextMenuRequested( QPoint const & pos )
menu.addAction( saveSoundAction );
}
- QString selectedText = webview->selectedText();
+ QString const selectedText = webview->selectedText();
QString text = Utils::trimNonChar( selectedText );
if ( text.size() && text.size() < 60 )
@@ -1844,12 +1885,12 @@ void ArticleView::contextMenuRequested( QPoint const & pos )
menu.addAction( saveBookmark );
}
- // add anki menu
- if( !text.isEmpty() && cfg.preferences.ankiConnectServer.enabled )
- {
- QString txt = webview->title();
- sendToAnkiAction = new QAction( tr( "&Send \"%1\" to anki with selected text." ).arg( txt ), &menu );
- menu.addAction( sendToAnkiAction );
+ // Add anki menu (if enabled)
+ // If there is no selected text, it will extract text from the current article.
+ if ( cfg.preferences.ankiConnectServer.enabled ) {
+ menu.addAction( &sendToAnkiAction );
+ sendToAnkiAction.setText( webview->selectedText().isEmpty() ? tr( "&Send Current Article to Anki" ) :
+ tr( "&Send selected text to Anki" ) );
}
if( text.isEmpty() && !cfg.preferences.storeHistory)
@@ -1946,8 +1987,9 @@ void ArticleView::contextMenuRequested( QPoint const & pos )
else if( result == saveBookmark ) {
emit saveBookmarkSignal( text.left( 60 ) );
}
- else if( result == sendToAnkiAction ) {
- sendToAnki( webview->title(), webview->selectedText(), translateLine->text() );
+ else if( result == &sendToAnkiAction ) {
+ // This action is handled by a slot.
+ return;
}
else
if ( result == lookupSelectionGr && groupComboBox )
diff --git a/src/ui/articleview.h b/src/ui/articleview.h
index edcc1079..fdfc96f2 100644
--- a/src/ui/articleview.h
+++ b/src/ui/articleview.h
@@ -55,6 +55,9 @@ class ArticleView: public QWidget
bool expandOptionalParts;
QString rangeVarName;
+ /// An action used to create Anki notes.
+ QAction sendToAnkiAction{ tr( "&Create Anki note" ), this };
+
/// Any resource we've decided to download off the dictionary gets stored here.
/// Full vector capacity is used for search requests, where we have to make
/// a multitude of requests.
@@ -148,6 +151,10 @@ public:
/// which will be restored when some article loads eventually.
void showAnticipation();
+ /// Create a new Anki card from a currently displayed article with the provided id.
+ /// This function will call QWebEnginePage::runJavaScript() to fetch the corresponding HTML.
+ void makeAnkiCardFromArticle( QString const & article_id );
+
/// Opens the given link. Supposed to be used in response to
/// openLinkInNewTab() signal. The link scheme is therefore supposed to be
/// one of the internal ones.
@@ -187,6 +194,9 @@ public:
/// Takes the focus to the view
void focus() { webview->setFocus( Qt::ShortcutFocusReason ); }
+ /// Sends *word* to Anki.
+ void handleAnkiAction();
+
public:
/// Reloads the view