diff --git a/.github/workflows/PR-check-cmake.yml b/.github/workflows/PR-check-cmake.yml index 7d55c488..5607ad6c 100644 --- a/.github/workflows/PR-check-cmake.yml +++ b/.github/workflows/PR-check-cmake.yml @@ -56,7 +56,7 @@ jobs: brew update - name: Install dependencies run: | - brew install \ + brew install --force --overwrite \ ninja \ opencc \ ffmpeg \ @@ -68,7 +68,7 @@ jobs: hunspell \ xapian \ libzim \ - qt + qt || true - name: Install eb run: | wget https://github.com/mistydemeo/eb/releases/download/v4.4.3/eb-4.4.3.tar.bz2 diff --git a/.github/workflows/Release-all.yml b/.github/workflows/Release-all.yml index b91737b7..46d8b4de 100644 --- a/.github/workflows/Release-all.yml +++ b/.github/workflows/Release-all.yml @@ -31,7 +31,7 @@ jobs: brew update - name: Install dependencies run: | - brew install \ + brew install --force --overwrite \ bzip2 \ create-dmg \ hunspell \ @@ -42,7 +42,7 @@ jobs: lzip \ ninja \ opencc \ - xapian + xapian || true - name: Install eb run: | git clone https://github.com/xiaoyifang/eb.git diff --git a/icons/icon32_stardict.png b/icons/icon32_stardict.png index 29fcffb5..c46032fd 100644 Binary files a/icons/icon32_stardict.png and b/icons/icon32_stardict.png differ diff --git a/src/common/dictionary_icon_name.cc b/src/common/dictionary_icon_name.cc new file mode 100644 index 00000000..add0fc7c --- /dev/null +++ b/src/common/dictionary_icon_name.cc @@ -0,0 +1,32 @@ +#include "dictionary_icon_name.hh" +#include + + +QString Icons::DictionaryIconName::getIconName( const QString & dictionaryName ) +{ + if ( dictionaryName.isEmpty() ) { + return {}; + } + QMutexLocker _( &_mutex ); + + auto it = _dictionaryIconNames.contains( dictionaryName ); + if ( it ) { + return _dictionaryIconNames.value( dictionaryName ); + } + //get the first character of the dictionary name + QString name = dictionaryName.at( 0 ).toUpper(); + auto it1 = _iconDictionaryNames.contains( name ); + std::vector< QString > vector = {}; + if ( it1 ) { + vector = _iconDictionaryNames.value( name ); + vector.emplace_back( dictionaryName ); + } + else { + vector.emplace_back( dictionaryName ); + _iconDictionaryNames.insert( name, vector ); + } + + name = name + QString::number( vector.size() ); + _dictionaryIconNames.insert( dictionaryName, name ); + return name; +} \ No newline at end of file diff --git a/src/common/dictionary_icon_name.hh b/src/common/dictionary_icon_name.hh new file mode 100644 index 00000000..6906044b --- /dev/null +++ b/src/common/dictionary_icon_name.hh @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Icons { +//use dictionary name's (first character + the order number) to represent the dictionary name in the icon image. +class DictionaryIconName +{ + //map icon name to dictionary names; + QMap< QString, std::vector< QString > > _iconDictionaryNames; + //map dictionary name to icon name; + QMap< QString, QString > _dictionaryIconNames; + + QMutex _mutex; + +public: + QString getIconName( const QString & dictionaryName ); +}; +} // namespace Icons \ No newline at end of file diff --git a/src/common/folding.cc b/src/common/folding.cc index a5bceffb..f8b99d9d 100644 --- a/src/common/folding.cc +++ b/src/common/folding.cc @@ -155,8 +155,7 @@ std::u32string applyWhitespaceAndPunctOnly( std::u32string const & in ) bool isWhitespace( char32_t ch ) { - //invisible character should be treated as whitespace as well. - return QChar::isSpace( ch ) || !QChar::isPrint( ch ); + return QChar::isSpace( ch ); } bool isWhitespaceOrPunct( char32_t ch ) diff --git a/src/common/globalbroadcaster.cc b/src/common/globalbroadcaster.cc index 5fb63979..c5accf79 100644 --- a/src/common/globalbroadcaster.cc +++ b/src/common/globalbroadcaster.cc @@ -38,4 +38,22 @@ bool GlobalBroadcaster::existedInWhitelist( QString url ) const { return whitelist.contains( url ); } + + +QString GlobalBroadcaster::getAbbrName( QString const & text ) +{ + if ( text.isEmpty() ) { + return {}; + } + //remove whitespace,number,mark,puncuation,symbol + QString simplified = text; + simplified.remove( + QRegularExpression( R"([\p{Z}\p{N}\p{M}\p{P}\p{S}])", QRegularExpression::UseUnicodePropertiesOption ) ); + + if ( simplified.isEmpty() ) { + return {}; + } + + return _icon_names.getIconName( simplified ); +} // namespace global diff --git a/src/common/globalbroadcaster.hh b/src/common/globalbroadcaster.hh index c1d985d0..0be64a5c 100644 --- a/src/common/globalbroadcaster.hh +++ b/src/common/globalbroadcaster.hh @@ -5,6 +5,7 @@ #include "config.hh" #include "pronounceengine.hh" #include +#include "dictionary_icon_name.hh" struct ActiveDictIds { @@ -25,6 +26,7 @@ class GlobalBroadcaster: public QObject Config::Preferences * preference; QSet< QString > whitelist; + Icons::DictionaryIconName _icon_names; public: void setPreference( Config::Preferences * _pre ); @@ -40,6 +42,7 @@ public: QMap< QString, QSet< QString > > folderFavoritesMap; QMap< unsigned, QString > groupFolderMap; PronounceEngine pronounce_engine; + QString getAbbrName( QString const & text ); signals: void dictionaryChanges( ActiveDictIds ad ); void dictionaryClear( ActiveDictIds ad ); diff --git a/src/common/globalregex.hh b/src/common/globalregex.hh index 85fbc460..c9068960 100644 --- a/src/common/globalregex.hh +++ b/src/common/globalregex.hh @@ -71,8 +71,8 @@ const static QRegularExpression accentMark( R"(\p{M})", QRegularExpression::UseU //contain unicode space mark,invisible, and punctuation const static QRegularExpression markPuncSpace( R"([\p{M}\p{Z}\p{C}\p{P}])", QRegularExpression::UseUnicodePropertiesOption ); -//contain unicode space and mark.invisible -const static QRegularExpression markSpace( R"([\p{M}\p{Z}\p{C}])", QRegularExpression::UseUnicodePropertiesOption ); +//contain unicode space and mark. +const static QRegularExpression markSpace( R"([\p{M}\p{Z}])", QRegularExpression::UseUnicodePropertiesOption ); const static QRegularExpression whiteSpace( "\\s+" ); diff --git a/src/common/utils.hh b/src/common/utils.hh index dfa7301e..15a4d35e 100644 --- a/src/common/utils.hh +++ b/src/common/utils.hh @@ -39,6 +39,19 @@ inline QString rstrip( const QString & str ) return {}; } +inline uint32_t leadingSpaceCount( const QString & str ) +{ + for ( int i = 0; i < str.size(); i++ ) { + if ( str.at( i ).isSpace() ) { + continue; + } + else { + return i; + } + } + return 0; +} + std::string c_string( const QString & str ); bool endsWithIgnoreCase( QByteArrayView str, QByteArrayView extension ); /** diff --git a/src/dict/dictionary.cc b/src/dict/dictionary.cc index 1181b7f7..d05df52c 100644 --- a/src/dict/dictionary.cc +++ b/src/dict/dictionary.cc @@ -19,6 +19,7 @@ #include #include "utils.hh" #include "zipfile.hh" +#include namespace Dictionary { @@ -291,7 +292,7 @@ bool Class::loadIconFromFile( QString const & _filename, bool isFullName ) return false; } -bool Class::loadIconFromText( QString iconUrl, QString const & text ) +bool Class::loadIconFromText( const QString & iconUrl, QString const & text ) { if ( text.isEmpty() ) { return false; @@ -308,7 +309,7 @@ bool Class::loadIconFromText( QString iconUrl, QString const & text ) painter.setCompositionMode( QPainter::CompositionMode_SourceAtop ); QFont font = painter.font(); - //the text should be a little smaller than the icon + //the orderNum should be a little smaller than the icon font.setPixelSize( iconSize * 0.6 ); font.setWeight( QFont::Bold ); painter.setFont( font ); @@ -318,8 +319,21 @@ bool Class::loadIconFromText( QString iconUrl, QString const & text ) //select a single char. auto abbrName = getAbbrName( text ); - painter.setPen( QColor( 4, 57, 108, 200 ) ); - painter.drawText( rectangle, Qt::AlignCenter, abbrName ); + painter.setPen( intToFixedColor( qHash( abbrName ) ) ); + + // Draw first character + painter.drawText( rectangle, Qt::AlignCenter, abbrName.at( 0 ) ); + + //the orderNum should be a little smaller than the icon + font.setPixelSize( iconSize * 0.4 ); + QFontMetrics fm1( font ); + const QString & orderNum = abbrName.mid( 1 ); + int orderNumberWidth = fm1.horizontalAdvance( orderNum ); + + painter.setFont( font ); + painter.drawText( rectangle.x() + rectangle.width() - orderNumberWidth * 1.2, + rectangle.y() + rectangle.height(), + orderNum ); painter.end(); @@ -330,35 +344,30 @@ bool Class::loadIconFromText( QString iconUrl, QString const & text ) return false; } +QColor Class::intToFixedColor( int index ) +{ + // Predefined list of colors + static const std::array colors = { + QColor( 255, 0, 0, 200 ), // Red + QColor( 4, 57, 108, 200 ), //Custom + QColor( 0, 255, 0, 200 ), // Green + QColor( 0, 0, 255, 200 ), // Blue + QColor( 255, 255, 0, 200 ), // Yellow + QColor( 0, 255, 255, 200 ), // Cyan + QColor( 255, 0, 255, 200 ), // Magenta + QColor( 192, 192, 192, 200 ), // Gray + QColor( 255, 165, 0, 200 ), // Orange + QColor( 128, 0, 128, 200 ), // Violet + QColor( 128, 128, 0, 200 ) // Olive + }; + + // Use modulo operation to ensure index is within the range of the color list + return colors[ index % colors.size() ]; +} + QString Class::getAbbrName( QString const & text ) { - if ( text.isEmpty() ) { - return {}; - } - //remove whitespace,number,mark,puncuation,symbol - QString simplified = text; - simplified.remove( - QRegularExpression( R"([\p{Z}\p{N}\p{M}\p{P}\p{S}])", QRegularExpression::UseUnicodePropertiesOption ) ); - - if ( simplified.isEmpty() ) { - return {}; - } - int index = qHash( simplified ) % simplified.size(); - - QString abbrName; - if ( !Utils::isCJKChar( simplified.at( index ).unicode() ) ) { - // take two chars. - abbrName = simplified.mid( index, 2 ); - if ( abbrName.size() == 1 ) { - //make up two characters. - abbrName = abbrName + simplified.at( 0 ); - } - } - else { - abbrName = simplified.mid( index, 1 ); - } - - return abbrName; + return GlobalBroadcaster::instance()->getAbbrName( text ); } void Class::isolateCSS( QString & css, QString const & wrapperSelector ) diff --git a/src/dict/dictionary.hh b/src/dict/dictionary.hh index e290162d..71e13eb1 100644 --- a/src/dict/dictionary.hh +++ b/src/dict/dictionary.hh @@ -318,10 +318,10 @@ protected: // Load icon from filename directly if isFullName == true // else treat filename as name without extension bool loadIconFromFile( QString const & filename, bool isFullName = false ); - bool loadIconFromText( QString iconUrl, QString const & text ); - - QString getAbbrName( QString const & text ); + bool loadIconFromText( const QString & iconUrl, QString const & text ); + static QString getAbbrName( QString const & text ); + static QColor intToFixedColor( int index ); /// Make css content usable only for articles from this dictionary void isolateCSS( QString & css, QString const & wrapperSelector = QString() ); diff --git a/src/dict/dictserver.cc b/src/dict/dictserver.cc index 2dff23b2..7ca2aad5 100644 --- a/src/dict/dictserver.cc +++ b/src/dict/dictserver.cc @@ -592,12 +592,49 @@ public: cancel(); } ); - connect( this, &DictServerArticleRequest::finishedArticle, this, [ this ]( QString articleText ) { + connect( this, &DictServerArticleRequest::finishedArticle, this, [ this ]( QString _articleText ) { if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) { cancel(); return; } + //modify the _articleText,remove extra lines[start with 15X etc.] + QList< QString > lines = _articleText.split( "\n", Qt::SkipEmptyParts ); + + QString resultStr; + + // process the line + static QRegularExpression leadingRespCode( "^\\d{3} " ); + uint32_t leadingSpaceCount = 0; + uint32_t firstLeadingSpaceCount = 0; + for ( const QString & line : lines ) { + //ignore 15X lines + if ( leadingRespCode.match( line ).hasMatch() ) { + continue; + } + // ignore dot(.),the end line character + if ( line.trimmed() == "." ) { + break; + } + + auto lsc = Utils::leadingSpaceCount( line ); + + if ( firstLeadingSpaceCount == 0 && lsc > firstLeadingSpaceCount ) { + firstLeadingSpaceCount = lsc; + } + + if ( lsc >= leadingSpaceCount && lsc > firstLeadingSpaceCount ) { + //extra space + resultStr.append( " " ); + resultStr.append( line.trimmed() ); + } + else { + resultStr.append( "\n" ); + resultStr.append( line ); + } + leadingSpaceCount = lsc; + } + static QRegularExpression phonetic( R"(\\([^\\]+)\\)", QRegularExpression::CaseInsensitiveOption ); // phonetics: \stuff\ ... static QRegularExpression divs_inside_phonetic( "]*)>]*)>", @@ -610,26 +647,26 @@ public: string articleStr; if ( contentInHtml ) { - articleStr = articleText.toUtf8().data(); + articleStr = resultStr.toUtf8().data(); } else { - articleStr = Html::preformat( articleText.toUtf8().data() ); + articleStr = Html::preformat( resultStr.toUtf8().data() ); } - articleText = QString::fromUtf8( articleStr.c_str(), articleStr.size() ); + _articleText = QString::fromUtf8( articleStr.c_str(), articleStr.size() ); int pos; if ( !contentInHtml ) { - articleText = articleText.replace( refs, R"(\1)" ); + _articleText = _articleText.replace( refs, R"(\1)" ); pos = 0; QString articleNewText; // Handle phonetics - QRegularExpressionMatchIterator it = phonetic.globalMatch( articleText ); + QRegularExpressionMatchIterator it = phonetic.globalMatch( _articleText ); while ( it.hasNext() ) { QRegularExpressionMatch match = it.next(); - articleNewText += articleText.mid( pos, match.capturedStart() - pos ); + articleNewText += _articleText.mid( pos, match.capturedStart() - pos ); pos = match.capturedEnd(); QString phonetic_text = match.captured( 1 ); @@ -638,18 +675,18 @@ public: articleNewText += R"()" + phonetic_text + ""; } if ( pos ) { - articleNewText += articleText.mid( pos ); - articleText = articleNewText; + articleNewText += _articleText.mid( pos ); + _articleText = articleNewText; articleNewText.clear(); } // Handle links pos = 0; - it = links.globalMatch( articleText ); + it = links.globalMatch( _articleText ); while ( it.hasNext() ) { QRegularExpressionMatch match = it.next(); - articleNewText += articleText.mid( pos, match.capturedStart() - pos ); + articleNewText += _articleText.mid( pos, match.capturedStart() - pos ); pos = match.capturedEnd(); QString link = match.captured( 1 ); @@ -663,13 +700,13 @@ public: articleNewText += newLink; } if ( pos ) { - articleNewText += articleText.mid( pos ); - articleText = articleNewText; + articleNewText += _articleText.mid( pos ); + _articleText = articleNewText; articleNewText.clear(); } } - articleData += string( "
" ) + articleText.toUtf8().data() + "
"; + articleData += string( "
" ) + _articleText.toUtf8().data() + "
"; if ( !articleData.empty() ) { diff --git a/src/dict/mdx.cc b/src/dict/mdx.cc index a48338e2..35c769f3 100644 --- a/src/dict/mdx.cc +++ b/src/dict/mdx.cc @@ -877,8 +877,7 @@ QString & MdxDictionary::filterResource( QString & article ) void MdxDictionary::replaceLinks( QString & id, QString & article ) { QString articleNewText; - qsizetype linkPos = 0; - + int linkPos = 0; QRegularExpressionMatchIterator it = RX::Mdx::allLinksRe.globalMatch( article ); while ( it.hasNext() ) { QRegularExpressionMatch allLinksMatch = it.next(); @@ -954,14 +953,13 @@ void MdxDictionary::replaceLinks( QString & id, QString & article ) articleNewText += linkTxt; match = RX::Mdx::closeScriptTagRe.match( article, linkPos ); if ( match.hasMatch() ) { - articleNewText += QString( QStringLiteral( "gdOnReady(()=>{%1});" ) ) - .arg( article.mid( linkPos, match.capturedStart() - linkPos ) ); + articleNewText += article.mid( linkPos, match.capturedEnd() - linkPos ); linkPos = match.capturedEnd(); } continue; } else { - //audio ,script,video ,html5 tags fall here. + //audio ,video ,html5 tags fall here. match = RX::Mdx::srcRe.match( linkTxt ); if ( match.hasMatch() ) { QString newText; @@ -973,15 +971,9 @@ void MdxDictionary::replaceLinks( QString & id, QString & article ) else { scheme = "bres://"; } - newText = match.captured( 1 ) + match.captured( 2 ) + scheme + id + "/" + match.captured( 3 ) + match.captured( 2 ); - //add defer to script tag - if ( linkType.compare( "script" ) == 0 ) { - newText = newText + " defer "; - } - newLink = linkTxt.replace( match.capturedStart(), match.capturedLength(), newText ); } else { diff --git a/src/langcoder.cc b/src/langcoder.cc index 95c33581..da22abe1 100644 --- a/src/langcoder.cc +++ b/src/langcoder.cc @@ -275,14 +275,18 @@ quint32 LangCoder::guessId( const QString & lang ) std::pair< quint32, quint32 > LangCoder::findLangIdPairFromName( QString const & name ) { - static QRegularExpression reg( "(?=([a-z]{2,3})-([a-z]{2,3}))", QRegularExpression::CaseInsensitiveOption ); + static QRegularExpression reg( "(^|[^a-z])((?[a-z]{2,3})-(?[a-z]{2,3}))($|[^a-z])", + QRegularExpression::CaseInsensitiveOption ); auto matches = reg.globalMatch( name ); while ( matches.hasNext() ) { auto m = matches.next(); + if ( matches.hasNext() ) { + continue; // We use only the last match, skip previous ones + } - auto fromId = guessId( m.captured( 1 ).toLower() ); - auto toId = guessId( m.captured( 2 ).toLower() ); + auto fromId = guessId( m.captured( "lang1" ).toLower() ); + auto toId = guessId( m.captured( "lang2" ).toLower() ); if ( code2Exists( intToCode2( fromId ) ) && code2Exists( intToCode2( toId ) ) ) { return { fromId, toId }; diff --git a/src/scripts/gd-builtin.js b/src/scripts/gd-builtin.js index 45c5971a..d9421c0f 100644 --- a/src/scripts/gd-builtin.js +++ b/src/scripts/gd-builtin.js @@ -1,11 +1,3 @@ -function gdOnReady(func) { - if (document.readyState !== "loading") { - func(); - } else { - document.addEventListener("DOMContentLoaded", func); - } -} - function gdMakeArticleActive(newId, noEvent) { const gdCurrentArticle = document.querySelector(".gdactivearticle").attributes.id; diff --git a/src/ui/mainwindow.cc b/src/ui/mainwindow.cc index f614cbee..1f8f7484 100644 --- a/src/ui/mainwindow.cc +++ b/src/ui/mainwindow.cc @@ -4025,7 +4025,7 @@ void MainWindow::on_importFavorites_triggered() QString fileName = QFileDialog::getOpenFileName( this, tr( "Import Favorites from file" ), importPath, - tr( "XML files (*.xml);;Txt files (*.txt);;All files (*.*)" ) ); + tr( "Text and XML files (*.txt *.xml);;All files (*.*)" ) ); if ( fileName.size() == 0 ) { return; } diff --git a/website/docs/manage_groups.md b/website/docs/manage_groups.md index 623af88f..e5c89ec2 100644 --- a/website/docs/manage_groups.md +++ b/website/docs/manage_groups.md @@ -10,9 +10,9 @@ Additionally, multiple strategies of automatic grouping are provided: ## Auto groups by dictionary language -For formats like DSL, which has embedded language from / to metadata, GoldenDict will use the dictionary's built-in metadata. +For formats like DSL, which has embedded language from / to metadata, GD will use the dictionary's built-in metadata. -For other formats, GoldenDict will try to extract languages from the dictionary's name or its file name by finding `{id}-{id}` pair. The `{id}` is 2 or 3 letters ISO 639 codes. For example, if a dictionary named `some name en-zh`, it will be automatically grouped into `en-zh`. +For other formats, GD will try finding the last `{id}-{id}` pair delimited by non-alphabets in dictionary name or main file name to extract languages. The `{id}` is 2 or 3 letters ISO 639 codes. For example, if a dictionary named `some name en-zh`, it will be automatically grouped into `en-zh`. Groups created in this method also include a context menu when right-click the group name, in which you can do additional dictionaries grouping by source or target language and combine dictionaries in more large groups.