From 5982b1d337d0bbdedec675279c3f264a30f7ab26 Mon Sep 17 00:00:00 2001 From: YiFang Xiao <YiFang.xiao@foxmail.com> Date: Wed, 4 Dec 2024 12:03:06 +0800 Subject: [PATCH 01/10] Revert "fix: <script> without src is not delayed like <script src="..." defer>" This reverts commit 00dbc74bb78a199227a5dfa0a89252f9f85ea2a5. --- src/dict/mdx.cc | 6 ++---- src/scripts/gd-builtin.js | 8 -------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/dict/mdx.cc b/src/dict/mdx.cc index a48338e2..49a15bb1 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,8 +953,7 @@ void MdxDictionary::replaceLinks( QString & id, QString & article ) articleNewText += linkTxt; match = RX::Mdx::closeScriptTagRe.match( article, linkPos ); if ( match.hasMatch() ) { - articleNewText += QString( QStringLiteral( "gdOnReady(()=>{%1});</script>" ) ) - .arg( article.mid( linkPos, match.capturedStart() - linkPos ) ); + articleNewText += article.mid( linkPos, match.capturedEnd() - linkPos ); linkPos = match.capturedEnd(); } continue; 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; From 612dc8db0fff6871d95c2ff113dd344667c5dd20 Mon Sep 17 00:00:00 2001 From: YiFang Xiao <YiFang.xiao@foxmail.com> Date: Wed, 4 Dec 2024 12:04:21 +0800 Subject: [PATCH 02/10] Revert "opt: [mdx]js script with defer attribute (#1869)" This reverts commit 939e78633165c1cb1908ece5448139d8bea49b41. --- src/dict/mdx.cc | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/dict/mdx.cc b/src/dict/mdx.cc index 49a15bb1..35c769f3 100644 --- a/src/dict/mdx.cc +++ b/src/dict/mdx.cc @@ -959,7 +959,7 @@ void MdxDictionary::replaceLinks( QString & id, QString & article ) 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; @@ -971,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 { From f9d44f97c5a51e8f0d4e5d774974fea119577794 Mon Sep 17 00:00:00 2001 From: shenleban tongying <shenlebantongying@gmail.com> Date: Sun, 8 Dec 2024 10:19:52 -0500 Subject: [PATCH 03/10] dev: force install deps in macOS workflows --- .github/workflows/PR-check-cmake.yml | 4 ++-- .github/workflows/Release-all.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 From a45a3092b0cf9f8703e1e93ff589b33315f1a661 Mon Sep 17 00:00:00 2001 From: xiaoyifang <105986+xiaoyifang@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:57:21 +0800 Subject: [PATCH 04/10] opt: beautify the layout of dictserver output (#2012) * opt: beautify the layout of dictserver output --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/common/utils.hh | 13 +++++++++ src/dict/dictserver.cc | 65 +++++++++++++++++++++++++++++++++--------- 2 files changed, 64 insertions(+), 14 deletions(-) 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/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( "</div([^>]*)><div([^>]*)>", @@ -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"(<a href="gdlookup://localhost/\1">\1</a>)" ); + _articleText = _articleText.replace( refs, R"(<a href="gdlookup://localhost/\1">\1</a>)" ); 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"(<span class="dictd_phonetic">)" + phonetic_text + "</span>"; } 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( "<div class=\"dictd_article\">" ) + articleText.toUtf8().data() + "<br></div>"; + articleData += string( "<div class=\"dictd_article\">" ) + _articleText.toUtf8().data() + "<br></div>"; if ( !articleData.empty() ) { From a5337770da7573e4f1c2549dda10b0bdcaebbcbb Mon Sep 17 00:00:00 2001 From: xiaoyifang <105986+xiaoyifang@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:09:44 +0800 Subject: [PATCH 05/10] fix: cjk expand character can not be searched (#2016) * fix: cjk expand character can not be searched * Update src/common/folding.cc * Update src/common/globalregex.hh --- src/common/folding.cc | 5 ++--- src/common/globalregex.hh | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/common/folding.cc b/src/common/folding.cc index a5bceffb..b28397b4 100644 --- a/src/common/folding.cc +++ b/src/common/folding.cc @@ -22,8 +22,8 @@ std::u32string apply( std::u32string const & in, bool preserveWildcards ) { // remove diacritics (normalization), white space, punt, auto temp = QString::fromStdU32String( in ) - .normalized( QString::NormalizationForm_KD ) .remove( RX::markSpace ) + .normalized( QString::NormalizationForm_KD ) .removeIf( [ preserveWildcards ]( const QChar & ch ) -> bool { return ch.isPunct() && !( preserveWildcards && ( ch == '\\' || ch == '?' || ch == '*' || ch == '[' || ch == ']' ) ); @@ -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/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+" ); From 6cd878fdcb200e069021bc44871c3c970d00dd07 Mon Sep 17 00:00:00 2001 From: xiaoyifang <105986+xiaoyifang@users.noreply.github.com> Date: Fri, 13 Dec 2024 10:44:54 +0800 Subject: [PATCH 06/10] opt: reorder the action (#2018) --- src/common/folding.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/folding.cc b/src/common/folding.cc index b28397b4..f8b99d9d 100644 --- a/src/common/folding.cc +++ b/src/common/folding.cc @@ -22,8 +22,8 @@ std::u32string apply( std::u32string const & in, bool preserveWildcards ) { // remove diacritics (normalization), white space, punt, auto temp = QString::fromStdU32String( in ) - .remove( RX::markSpace ) .normalized( QString::NormalizationForm_KD ) + .remove( RX::markSpace ) .removeIf( [ preserveWildcards ]( const QChar & ch ) -> bool { return ch.isPunct() && !( preserveWildcards && ( ch == '\\' || ch == '?' || ch == '*' || ch == '[' || ch == ']' ) ); From 13a50dbb6c21593ee2be11ba42660793ed908b3b Mon Sep 17 00:00:00 2001 From: shenleban tongying <shenlebantongying@gmail.com> Date: Thu, 12 Dec 2024 23:20:39 -0500 Subject: [PATCH 07/10] fix: regression in finding {id}-{id} lang pair from dict name --- src/langcoder.cc | 10 +++++++--- website/docs/manage_groups.md | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) 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])((?<lang1>[a-z]{2,3})-(?<lang2>[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/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. From e86e99e72ce4e2a3ab0efc7c7887ce7bb1f37e52 Mon Sep 17 00:00:00 2001 From: xiaoyifang <105986+xiaoyifang@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:59:19 +0800 Subject: [PATCH 08/10] opt: combine the file filters as one entry (#2022) * opt: combine the file filters as one entry * Update src/ui/mainwindow.cc Co-authored-by: shenleban tongying <shenlebantongying@gmail.com> --------- Co-authored-by: shenleban tongying <shenlebantongying@gmail.com> --- src/ui/mainwindow.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; } From 3cc379e2d57af559fa8e7a460914a1f8b2e0bc8c Mon Sep 17 00:00:00 2001 From: xiaoyifang <105986+xiaoyifang@users.noreply.github.com> Date: Tue, 17 Dec 2024 14:31:30 +0800 Subject: [PATCH 09/10] opt: replace the stardict icon (#2023) --- icons/icon32_stardict.png | Bin 2371 -> 8253 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/icons/icon32_stardict.png b/icons/icon32_stardict.png index 29fcffb55937c74a8834df5b9420a075853395ab..c46032fd683c24da57aae1aef6782cf17c81d9f2 100644 GIT binary patch literal 8253 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 U+TBL*e}CYbsyZrlO4i~32Z^?-fdBvi literal 2371 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 From b7a27d724dc13406c65ae42c4ea726f4264964de Mon Sep 17 00:00:00 2001 From: xiaoyifang <105986+xiaoyifang@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:30:04 +0800 Subject: [PATCH 10/10] opt: generate dictionary icon from dictionary name (#2025) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: shenleban tongying <shenlebantongying@gmail.com> --- src/common/dictionary_icon_name.cc | 32 ++++++++++++++ src/common/dictionary_icon_name.hh | 24 ++++++++++ src/common/globalbroadcaster.cc | 18 ++++++++ src/common/globalbroadcaster.hh | 3 ++ src/dict/dictionary.cc | 71 +++++++++++++++++------------- src/dict/dictionary.hh | 6 +-- 6 files changed, 120 insertions(+), 34 deletions(-) create mode 100644 src/common/dictionary_icon_name.cc create mode 100644 src/common/dictionary_icon_name.hh 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 <QMutexLocker> + + +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 <string> +#include <QMap> +#include <vector> +#include <mutex> +#include <QString> +#include <QMutex> + +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/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 <QCache> +#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/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 <QRegularExpression> #include "utils.hh" #include "zipfile.hh" +#include <array> 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() );