Skip to content

Commit

Permalink
Add ankicard link and button
Browse files Browse the repository at this point in the history
  • Loading branch information
GenjiFujimoto authored and tatsumoto-ren committed Apr 4, 2023
1 parent 58a1f9b commit bcc0920
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 53 deletions.
42 changes: 22 additions & 20 deletions ankiconnector.cpp
Expand Up @@ -17,9 +17,12 @@ AnkiConnector::AnkiConnector( QObject * parent, Config::Class const & _cfg ) : Q
connect( mgr, &QNetworkAccessManager::finished, this, &AnkiConnector::finishedSlot );
}

void AnkiConnector::sendToAnki( QString const & word, QString const & text, QString const & sentence )
void AnkiConnector::sendToAnki( QString const & word, QString text, QString const & sentence )
{
QString postTemplate = R"anki({
// Anki doesn't understand the newline character, so it should be escaped.
text = text.replace( "\n", "<br>" );

QString const postTemplate = R"anki({
"action": "addNote",
"version": 6,
"params": {
Expand Down Expand Up @@ -48,7 +51,7 @@ void AnkiConnector::sendToAnki( QString const & word, QString const & text, QStr
Utils::json2String( fields ) );

// qDebug().noquote() << postData;
postToAnki( postData );
postToAnki( postData );
}

void AnkiConnector::ankiSearch( QString const & word )
Expand Down Expand Up @@ -91,26 +94,25 @@ void AnkiConnector::postToAnki( QString const & postData )

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();
if ( reply->error() == QNetworkReply::NoError ) {
QByteArray const bytes = reply->readAll();
QJsonDocument const json = QJsonDocument::fromJson( bytes );
auto const obj = json.object();

qDebug() << "anki result:" << result;
// Normally AnkiConnect always returns result and error,
// unless Anki is not running.
if ( obj.size() == 2 && obj.contains( "result" ) && obj.contains( "error" ) && obj[ "error" ].isNull() ) {
emit errorText( tr( "anki: post to anki success" ) );
}
else {
emit errorText( tr( "anki: post to anki failed" ) );
}

emit errorText( tr( "anki: post to anki success" ) );
qDebug().noquote() << "anki response:" << Utils::json2String( obj );
}
else
{
qDebug() << "anki connect error" << reply->errorString();
emit errorText( "anki:" + reply->errorString() );
else {
qDebug() << "anki connect error" << reply->errorString();
emit errorText( "anki:" + reply->errorString() );
}

reply->deleteLater();
Expand Down
2 changes: 1 addition & 1 deletion ankiconnector.h
Expand Up @@ -13,7 +13,7 @@ class AnkiConnector : public QObject
public:
explicit AnkiConnector( QObject * parent, Config::Class const & cfg );

void sendToAnki( QString const & word, QString const & text, QString const & sentence );
void sendToAnki( QString const & word, QString text, QString const & sentence );
void ankiSearch( QString const & word);

private:
Expand Down
18 changes: 18 additions & 0 deletions article-style.css
Expand Up @@ -45,6 +45,24 @@ pre
padding-left: 0.5em;
background: #def;
user-select: none;

display: flex;
flex-flow: row nowrap;
align-items: center;
}

/* The anki plus button, which is shown if enabled. */
.ankibutton {
display: grid;
place-items: center;
margin-inline-start: auto;
cursor: pointer;
border-radius: 4px;
padding: 3px;
transition: background-color 0.2s;
}
.ankibutton:active {
background-color: hsl(0deg 0% 70%);
}

.gddicttitle
Expand Down
95 changes: 69 additions & 26 deletions article_maker.cc
Expand Up @@ -22,6 +22,8 @@ using gd::wstring;
using std::set;
using std::list;

inline bool ankiConnectEnabled() { return GlobalBroadcaster::instance()->getPreference()->ankiConnectServer.enabled; }

ArticleMaker::ArticleMaker( vector< sptr< Dictionary::Class > > const & dictionaries_,
vector< Instances::Group > const & groups_,
const Config::Preferences & cfg_ ):
Expand Down Expand Up @@ -428,19 +430,21 @@ bool ArticleMaker::adjustFilePath( QString & fileName )

//////// ArticleRequest

ArticleRequest::ArticleRequest(
Config::InputPhrase const & phrase, QString const & group_,
QMap< QString, QString > const & contexts_,
vector< sptr< Dictionary::Class > > const & activeDicts_,
string const & header,
int sizeLimit, bool needExpandOptionalParts_, bool ignoreDiacritics_ ):
word( phrase.phrase ), group( group_ ), contexts( contexts_ ),
activeDicts( activeDicts_ ),
altsDone( false ), bodyDone( false ), foundAnyDefinitions( false ),
closePrevSpan( false )
, articleSizeLimit( sizeLimit )
, needExpandOptionalParts( needExpandOptionalParts_ )
, ignoreDiacritics( ignoreDiacritics_ )
ArticleRequest::ArticleRequest( Config::InputPhrase const & phrase,
QString const & group_,
QMap< QString, QString > const & contexts_,
vector< sptr< Dictionary::Class > > const & activeDicts_,
string const & header,
int sizeLimit,
bool needExpandOptionalParts_,
bool ignoreDiacritics_ ):
word( phrase.phrase ),
group( group_ ),
contexts( contexts_ ),
activeDicts( activeDicts_ ),
articleSizeLimit( sizeLimit ),
needExpandOptionalParts( needExpandOptionalParts_ ),
ignoreDiacritics( ignoreDiacritics_ )
{
if ( !phrase.punctuationSuffix.isEmpty() )
alts.insert( gd::toWString( phrase.phraseWithSuffix() ) );
Expand Down Expand Up @@ -554,6 +558,56 @@ int ArticleRequest::findEndOfCloseDiv( const QString &str, int pos )
}
}

QString ArticleRequest::constructDictHeading( std::string const & dict_id_html,
std::string const & dict_name,
bool const collapse )
{
// Start .gddictname
// Resort to QString here to be able to use its arg() method.
QString gd_dict_name = QString( R"EOF(<div class="gddictname" id="gddictname-%1">)EOF" ).arg( dict_id_html.c_str() );

// Icon
gd_dict_name += [ &dict_name, &dict_id_html ]() {
auto html = QString( R"EOF(
<span class="gddicticon"><img src="gico://%1/dicticon.png"></span>
<span class="gdfromprefix">%2</span>
<span class="gddicttitle">%3</span>
)EOF" );
return html.arg( dict_id_html.c_str(), tr( "From " ), dict_name.c_str() );
}();

// Collapse/expand button (blue arrow)
gd_dict_name += [ collapse, &dict_id_html ]() {
auto html = QString( R"EOF(
<span class="collapse_expand_area" onclick="gdExpandArticle('%1');" >
<img src="qrc:///icons/blank.png" class="%2" id="expandicon-%3" title="%4">
</span>)EOF" );
return html.arg( dict_id_html.c_str(),
collapse ? "gdexpandicon" : "gdcollapseicon",
dict_id_html.c_str(),
collapse ? tr( "Expand article" ) : tr( "Collapse article" ) );
}();

// If the user has enabled Anki integration in settings,
// Show a (+) button that lets the user add a new Anki card.
gd_dict_name += [ &dict_id_html ]() {
if ( ankiConnectEnabled() ) {
auto link = QString( R"EOF(
<a href="ankicard:%1" class="ankibutton" title="%2" >
<img src="qrc:///icons/add-anki-icon.svg">
</a>
)EOF" );
return link.arg( dict_id_html.c_str(), tr( "Make a new Anki note" ) );
}
return QString{};
}();

// Close .gddictname
gd_dict_name += "</div>";

return gd_dict_name;
}

void ArticleRequest::bodyFinished()
{
if ( bodyDone )
Expand Down Expand Up @@ -642,19 +696,8 @@ void ArticleRequest::bodyFinished()

closePrevSpan = true;

head += string( R"(<div class="gddictname" onclick="gdExpandArticle(')" ) + dictId + "\');"
+ ( collapse ? "\" style=\"cursor:pointer;" : "" )
+ "\" id=\"gddictname-" + Html::escape( dictId ) + "\""
+ ( collapse ? string( " title=\"" ) + tr( "Expand article" ).toUtf8().data() + "\"" : "" )
+ R"(><span class="gddicticon"><img src="gico://)" + Html::escape( dictId )
+ R"(/dicticon.png"></span><span class="gdfromprefix">)" +
Html::escape( tr( "From " ).toUtf8().data() ) + "</span><span class=\"gddicttitle\">" +
Html::escape( activeDict->getName().c_str() ) + "</span>"
+ R"(<span class="collapse_expand_area"><img src="qrc:///icons/blank.png" class=")"
+ ( collapse ? "gdexpandicon" : "gdcollapseicon" )
+ "\" id=\"expandicon-" + Html::escape( dictId ) + "\""
+ ( collapse ? "" : string( " title=\"" ) + tr( "Collapse article" ).toUtf8().data() + "\"" )
+ "></span>" + "</div>";
// Add .gddictname to head.
head += constructDictHeading( Html::escape( dictId ), activeDict->getName(), collapse ).toStdString();

head += "<div class=\"gddictnamebodyseparator\"></div>";

Expand Down
13 changes: 9 additions & 4 deletions article_maker.hh
Expand Up @@ -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
Expand Down Expand Up @@ -151,6 +152,10 @@ private:

/// Find end of corresponding </div> tag
int findEndOfCloseDiv( QString const &, int pos );

// A method used for constructing a dictionary heading,
// e.g. <div class="gddictname" >...</div>
QString constructDictHeading( std::string const & dict_id_html, std::string const & dict_name, bool const collapse );
};


Expand Down
2 changes: 1 addition & 1 deletion config.cc
Expand Up @@ -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();

Expand Down
3 changes: 2 additions & 1 deletion config.hh
Expand Up @@ -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;

Expand Down
24 changes: 24 additions & 0 deletions icons/add-anki-icon.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources.qrc
@@ -1,5 +1,6 @@
<RCC>
<qresource prefix="/">
<file>icons/add-anki-icon.svg</file>
<file>version.txt</file>
<file>icons/arrow.png</file>
<file>icons/prefix.png</file>
Expand Down
22 changes: 22 additions & 0 deletions src/ui/articleview.cpp
Expand Up @@ -1124,6 +1124,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();
Expand All @@ -1143,6 +1152,19 @@ void ArticleView::openLink( QUrl const & url, QUrl const & ref, QString const &
else if( url.scheme().compare( "ankisearch" ) == 0 ) {
ankiConnector->ankiSearch( url.path() );
}
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 ( auto const selected_text = webview->selectedText(), article_id = url.path();
!article_id.isEmpty() && selected_text.isEmpty() ) {
makeAnkiCardFromArticle( article_id );
}
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" ) )
{
Expand Down
4 changes: 4 additions & 0 deletions src/ui/articleview.h
Expand Up @@ -148,6 +148,10 @@ class ArticleView: public QWidget
/// 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.
Expand Down

0 comments on commit bcc0920

Please sign in to comment.