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 }