File indexing completed on 2025-02-02 04:11:28
0001 /* 0002 * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best> 0003 * 0004 * SPDX-License-Identifier: GPL-3.0-or-later 0005 */ 0006 0007 #include "google_fonts_model.hpp" 0008 0009 #include <QJsonArray> 0010 #include <QJsonObject> 0011 #include <QJsonDocument> 0012 0013 #include <QNetworkReply> 0014 #include <QNetworkRequest> 0015 #include <QNetworkAccessManager> 0016 0017 #include "app/settings/settings.hpp" 0018 0019 0020 QString glaxnimate::gui::font::GoogleFontsModel::GoogleFont::css_url(const Style& style) const 0021 { 0022 QString fam = QUrl::toPercentEncoding(family); 0023 static QString base("https://fonts.googleapis.com/css2?family=%1:ital,wght@%2,%3&display=swap"); 0024 return base.arg(fam).arg(style.italic ? "1" : "0").arg(style.weight); 0025 } 0026 0027 0028 class glaxnimate::gui::font::GoogleFontsModel::Private 0029 { 0030 public: 0031 GoogleFontsModel* parent; 0032 QString url_base; 0033 QString token; 0034 std::vector<GoogleFont> fonts; 0035 // std::unordered_map<QString, std::size_t> font_names; 0036 QNetworkAccessManager downloader; 0037 std::unordered_map<QString, std::map<std::pair<int, bool>, model::CustomFont>> downloaded; 0038 std::set<QString> subsets; 0039 0040 bool response_has_error(QNetworkReply* reply) 0041 { 0042 Q_EMIT parent->max_progress_changed(0); 0043 0044 if ( reply->error() || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200 ) 0045 { 0046 auto reason = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); 0047 Q_EMIT parent->error(reason); 0048 return true; 0049 } 0050 0051 return false; 0052 } 0053 0054 void style_from_slug(const QString& slug, GoogleFont::Style& out ) 0055 { 0056 out.italic = slug.endsWith("italic"); 0057 if ( slug[2] == '0' ) 0058 out.weight = slug.left(3).toInt(); 0059 } 0060 0061 void parse_json(const QJsonDocument& doc) 0062 { 0063 Q_EMIT parent->beginResetModel(); 0064 0065 int pop = 0; 0066 auto items = doc.object()["items"].toArray(); 0067 // font_names.clear(); 0068 fonts.clear(); 0069 fonts.reserve(items.size()); 0070 for ( QJsonValue font_json : items ) 0071 { 0072 GoogleFont font; 0073 font.family = font_json["family"].toString(); 0074 font.popularity_index = ++pop; 0075 0076 std::map<std::pair<int, bool>, model::CustomFont>* downloaded_font = nullptr; 0077 auto downloaded_font_iter = downloaded.find(font.family); 0078 if ( downloaded_font_iter != downloaded.end() ) 0079 downloaded_font = &downloaded_font_iter->second; 0080 0081 auto items = font_json["files"].toObject(); 0082 for ( auto it = items.begin(); it != items.end(); ++it ) 0083 { 0084 GoogleFont::Style& style = font.styles.emplace_back(); 0085 style.url = QUrl(it->toString()); 0086 style_from_slug(it.key(), style); 0087 0088 if ( downloaded_font ) 0089 { 0090 auto downloaded_style_iter = downloaded_font->find({style.weight, style.italic}); 0091 if ( downloaded_style_iter != downloaded_font->end() ) 0092 style.font = downloaded_style_iter->second; 0093 } 0094 } 0095 0096 std::sort(font.styles.begin(), font.styles.end()); 0097 0098 if ( !font.styles.empty() ) 0099 { 0100 for ( const auto& val : font_json["subsets"].toArray() ) 0101 { 0102 auto subset = val.toString(); 0103 font.subsets.insert(subset); 0104 subsets.insert(subset); 0105 } 0106 0107 QString cat = font_json["category"].toString(); 0108 if ( cat == "sans-serif" ) 0109 font.category = GoogleFont::SansSerif; 0110 else if ( cat == "serif" ) 0111 font.category = GoogleFont::Serif; 0112 else if ( cat == "display" ) 0113 font.category = GoogleFont::Display; 0114 else if ( cat == "handwriting" ) 0115 font.category = GoogleFont::Handwriting; 0116 else if ( cat == "monospace" ) 0117 font.category = GoogleFont::Monospace; 0118 0119 fonts.push_back(font); 0120 } 0121 } 0122 0123 Q_EMIT parent->endResetModel(); 0124 Q_EMIT parent->refresh_finished(); 0125 } 0126 0127 void update_settings() 0128 { 0129 token = app::settings::get<QString>("api_credentials", "Google Fonts/Token"); 0130 url_base = app::settings::get<QString>("api_credentials", "Google Fonts/URL"); 0131 } 0132 0133 void font_changed(std::size_t font_index) 0134 { 0135 Q_EMIT parent->dataChanged(parent->createIndex(font_index, 0), parent->createIndex(font_index, Column::Count - 1)); 0136 } 0137 0138 void download_style(GoogleFont* font, std::size_t font_index, int style_index, GoogleFont::StyleList::iterator style) 0139 { 0140 if ( style == font->styles.end() ) 0141 { 0142 if ( font->status == GoogleFont::InProgress ) 0143 { 0144 font->status = GoogleFont::Downloaded; 0145 font_changed(font_index); 0146 Q_EMIT parent->download_finished(font_index); 0147 } 0148 return; 0149 } 0150 0151 QNetworkRequest request(style->url); 0152 request.setMaximumRedirectsAllowed(3); 0153 request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); 0154 0155 auto reply = downloader.get(request); 0156 0157 int base = style_index * 100; 0158 connect(reply, &QNetworkReply::downloadProgress, parent, [this, base](qint64 received, qint64 total){ 0159 Q_EMIT parent->progress_changed(received * 100 / total + base); 0160 }); 0161 connect(reply, &QNetworkReply::finished, parent, [this, reply, base, font, font_index, style, style_index]{ 0162 0163 Q_EMIT parent->progress_changed(base + 100); 0164 0165 if ( response_has_error(reply) ) 0166 { 0167 font->status = GoogleFont::Broken; 0168 font_changed(font_index); 0169 } 0170 else 0171 { 0172 auto data = reply->readAll(); 0173 reply->close(); 0174 /// \todo Should we pass the font name? 0175 style->font = model::CustomFontDatabase::instance().add_font("", data); 0176 if ( !style->font.is_valid() ) 0177 { 0178 font->status = GoogleFont::Broken; 0179 font_changed(font_index); 0180 Q_EMIT parent->error(i18n("Could not add font")); 0181 } 0182 else 0183 { 0184 downloaded[font->family][{style->weight, style->italic}] = style->font; 0185 style->font.set_source_url(style->url.toString()); 0186 style->font.set_css_url(font->css_url(*style)); 0187 } 0188 } 0189 0190 download_style(font, font_index, style_index+1, style+1); 0191 }); 0192 } 0193 0194 void download_font(GoogleFont& font, std::size_t font_index) 0195 { 0196 font.status = GoogleFont::InProgress; 0197 font_changed(font_index); 0198 Q_EMIT parent->progress_changed(0); 0199 Q_EMIT parent->max_progress_changed(100 * font.styles.size()); 0200 download_style(&font, font_index, 0, font.styles.begin()); 0201 } 0202 }; 0203 0204 glaxnimate::gui::font::GoogleFontsModel::GoogleFontsModel() 0205 : d(std::make_unique<Private>()) 0206 { 0207 d->parent = this; 0208 } 0209 0210 glaxnimate::gui::font::GoogleFontsModel::~GoogleFontsModel() 0211 { 0212 } 0213 0214 void glaxnimate::gui::font::GoogleFontsModel::response_progress(qint64 received, qint64 total) 0215 { 0216 Q_EMIT max_progress_changed(total); 0217 Q_EMIT progress_changed(received); 0218 } 0219 0220 0221 void glaxnimate::gui::font::GoogleFontsModel::refresh() 0222 { 0223 d->update_settings(); 0224 QNetworkRequest request(QUrl(d->url_base + "?sort=popularity&key=" + d->token)); 0225 request.setMaximumRedirectsAllowed(3); 0226 request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); 0227 Q_EMIT max_progress_changed(100); 0228 Q_EMIT progress_changed(0); 0229 0230 auto reply = d->downloader.get(request); 0231 0232 connect(reply, &QNetworkReply::downloadProgress, this, &GoogleFontsModel::response_progress); 0233 connect(reply, &QNetworkReply::finished, this, [this, reply]{ 0234 if ( d->response_has_error(reply) ) 0235 return; 0236 0237 QJsonParseError error; 0238 auto doc = QJsonDocument::fromJson(reply->readAll(), &error); 0239 reply->close(); 0240 0241 if ( error.error ) 0242 Q_EMIT this->error(error.errorString()); 0243 0244 d->parse_json(doc); 0245 }); 0246 } 0247 0248 int glaxnimate::gui::font::GoogleFontsModel::columnCount(const QModelIndex&) const 0249 { 0250 return Column::Count; 0251 } 0252 0253 int glaxnimate::gui::font::GoogleFontsModel::rowCount(const QModelIndex&) const 0254 { 0255 return d->fonts.size(); 0256 } 0257 0258 Qt::ItemFlags glaxnimate::gui::font::GoogleFontsModel::flags(const QModelIndex& index) const 0259 { 0260 return QAbstractTableModel::flags(index); 0261 } 0262 0263 QVariant glaxnimate::gui::font::GoogleFontsModel::headerData(int section, Qt::Orientation orientation, int role) const 0264 { 0265 if ( orientation == Qt::Vertical ) 0266 return {}; 0267 0268 if ( role != Qt::DisplayRole ) 0269 return {}; 0270 0271 switch ( section ) 0272 { 0273 case Column::Family: 0274 return i18n("Family"); 0275 case Column::Category: 0276 return i18n("Category"); 0277 case Column::Popularity: 0278 return i18n("Rank"); 0279 case Column::Status: 0280 return i18n("Status"); 0281 } 0282 0283 return {}; 0284 } 0285 0286 QVariant glaxnimate::gui::font::GoogleFontsModel::data(const QModelIndex& index, int role) const 0287 { 0288 if ( !index.isValid() ) 0289 return {}; 0290 0291 int row = index.row(); 0292 if ( row < 0 || row >= int(d->fonts.size()) ) 0293 return {}; 0294 0295 const auto& font = d->fonts[row]; 0296 0297 switch ( index.column() ) 0298 { 0299 case Column::Family: 0300 if ( role == Qt::DisplayRole || role == SortRole ) 0301 return font.family; 0302 break; 0303 0304 case Column::Category: 0305 if ( role == Qt::DisplayRole ) 0306 return category_name(font.category); 0307 break; 0308 0309 case Column::Popularity: 0310 if ( role == Qt::DisplayRole || role == SortRole ) 0311 return font.popularity_index; 0312 break; 0313 0314 case Column::Status: 0315 if ( role == Qt::DecorationRole ) 0316 { 0317 switch ( font.status ) 0318 { 0319 case GoogleFont::Downloaded: 0320 return QIcon::fromTheme("package-installed-updated"); 0321 case GoogleFont::Available: 0322 return QIcon::fromTheme("package-available"); 0323 case GoogleFont::InProgress: 0324 return QIcon::fromTheme("package-install"); 0325 case GoogleFont::Broken: 0326 return QIcon::fromTheme("package-broken"); 0327 } 0328 } 0329 else if ( role == SortRole ) 0330 { 0331 return font.status; 0332 } 0333 break; 0334 } 0335 0336 return {}; 0337 } 0338 0339 bool glaxnimate::gui::font::GoogleFontsModel::has_token() const 0340 { 0341 d->update_settings(); 0342 return !d->token.isEmpty(); 0343 } 0344 0345 void glaxnimate::gui::font::GoogleFontsModel::download_font(int row) 0346 { 0347 if ( row < 0 || row >= int(d->fonts.size()) ) 0348 return ; 0349 0350 d->download_font(d->fonts[row], row); 0351 } 0352 0353 const glaxnimate::gui::font::GoogleFontsModel::GoogleFont* glaxnimate::gui::font::GoogleFontsModel::font(int row) const 0354 { 0355 if ( row < 0 || row >= int(d->fonts.size()) ) 0356 return nullptr; 0357 0358 return &d->fonts[row]; 0359 } 0360 0361 bool glaxnimate::gui::font::GoogleFontsModel::has_subset(const QModelIndex& index, const QString& subset) const 0362 { 0363 if ( !index.isValid() ) 0364 return false; 0365 0366 int row = index.row(); 0367 if ( row < 0 || row >= int(d->fonts.size()) ) 0368 return false; 0369 0370 return d->fonts[row].subsets.count(subset); 0371 } 0372 0373 bool glaxnimate::gui::font::GoogleFontsModel::has_category(const QModelIndex& index, GoogleFont::Category cat) const 0374 { 0375 if ( !index.isValid() ) 0376 return false; 0377 0378 int row = index.row(); 0379 if ( row < 0 || row >= int(d->fonts.size()) ) 0380 return false; 0381 0382 return d->fonts[row].category == cat; 0383 } 0384 0385 QString glaxnimate::gui::font::GoogleFontsModel::category_name(GoogleFont::Category category) 0386 { 0387 switch ( category ) 0388 { 0389 default: 0390 return i18n("Any"); 0391 case GoogleFont::SansSerif: 0392 return i18n("Sans-Serif"); 0393 case GoogleFont::Serif: 0394 return i18n("Serif"); 0395 case GoogleFont::Monospace: 0396 return i18n("Monospace"); 0397 case GoogleFont::Display: 0398 return i18n("Display"); 0399 case GoogleFont::Handwriting: 0400 return i18n("Handwriting"); 0401 } 0402 } 0403 0404 const std::set<QString> & glaxnimate::gui::font::GoogleFontsModel::subsets() const 0405 { 0406 return d->subsets; 0407 }