mirror of
https://github.com/xiaoyifang/goldendict-ng.git
synced 2024-11-23 20:14:05 +00:00
Add ankicard link and button
make selectedText const add a new keyboard shortcut: ctrl+shift+n to make a card if word is empty, warn and exit rename return after ankisearch remove temp vars change the anki action's text depending on selected text reformat article maker the anki button is shown under the heading revert to the previous way of constructing gddictname to reduce size of the diff
This commit is contained in:
parent
25daf37f65
commit
16943ccab1
|
@ -17,9 +17,17 @@ 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({
|
||||
if ( word.isEmpty() ) {
|
||||
emit this->errorText( tr( "anki: can't create a card without a word" ) );
|
||||
return;
|
||||
}
|
||||
|
||||
// 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": {
|
||||
|
@ -48,7 +56,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 )
|
||||
|
@ -91,26 +99,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();
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -47,6 +47,20 @@ pre
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
/* The anki plus button, which is shown if enabled. */
|
||||
.ankibutton {
|
||||
float: right;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
padding: 3px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.ankibutton:active {
|
||||
background-color: hsl(0deg 0% 70%);
|
||||
}
|
||||
|
||||
.gddicttitle
|
||||
{
|
||||
user-select: none;
|
||||
|
|
|
@ -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_ ):
|
||||
|
@ -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() ) );
|
||||
|
@ -658,6 +662,17 @@ void ArticleRequest::bodyFinished()
|
|||
|
||||
head += "<div class=\"gddictnamebodyseparator\"></div>";
|
||||
|
||||
// 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(
|
||||
<a href="ankicard:%1" class="ankibutton" title="%2" >
|
||||
<img src="qrc:///icons/add-anki-icon.svg">
|
||||
</a>
|
||||
)EOF" };
|
||||
head += link.arg( Html::escape( dictId ).c_str(), tr( "Make a new Anki note" ) ).toStdString();
|
||||
}
|
||||
|
||||
head += "<div class=\"gdarticlebody gdlangfrom-";
|
||||
head += LangCoder::intToCode2( activeDict->getLangFrom() ).toLatin1().data();
|
||||
head += "\" lang=\"";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
24
icons/add-anki-icon.svg
Normal file
24
icons/add-anki-icon.svg
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient id="linearGradient4582" x1="-1.7198" x2="-1.7198" y1="3.5719" y2=".79375" gradientTransform="matrix(3.7795 0 0 3.7795 14.5 -6.308e-7)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6fb558" offset="0"/>
|
||||
<stop stop-color="#a5db9b" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient4758-7" x1="7.5406" x2="5.1594" y1="3.3073" y2=".92604" gradientTransform="matrix(3.7795 0 0 3.7795 -16 -6e-7)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#34812c" offset="0"/>
|
||||
<stop stop-color="#87b870" offset="1"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="radialGradient4683-3" cx="2.1167" cy="2.1167" r=".66146" gradientTransform="matrix(4.5354 8.0301e-7 -8.0301e-7 4.5354 -1.6 -1.6)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-opacity=".28986" offset="0"/>
|
||||
<stop stop-opacity="0" offset="1"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g>
|
||||
<circle cx="8" cy="8" r="6.5" fill="url(#linearGradient4582)"/>
|
||||
<circle cx="8" cy="8" r="5.75" fill="none" stroke="#fff" stroke-opacity=".50196" stroke-width="1.5"/>
|
||||
<circle cx="8" cy="8" r="6.5" fill="none" stroke="url(#linearGradient4758-7)"/>
|
||||
<circle cx="8" cy="8" r="3" fill="url(#radialGradient4683-3)"/>
|
||||
<path d="m5 7h2v-2h2v2h2v2h-2v2h-2v-2h-2v-2" fill="#fff"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -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>
|
||||
|
|
|
@ -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 )
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue