1 // For license of this file, see <project-root-folder>/LICENSE.md.
2 
3 #include "miscellaneous/skinfactory.h"
4 
5 #include "exceptions/ioexception.h"
6 #include "miscellaneous/application.h"
7 
8 #include <QDir>
9 #include <QDomDocument>
10 #include <QDomElement>
11 #include <QStyleFactory>
12 
SkinFactory(QObject * parent)13 SkinFactory::SkinFactory(QObject* parent) : QObject(parent) {}
14 
loadCurrentSkin()15 void SkinFactory::loadCurrentSkin() {
16   QList<QString> skin_names_to_try;
17 
18   skin_names_to_try.append(selectedSkinName());
19   skin_names_to_try.append(QSL(APP_SKIN_DEFAULT));
20   bool skin_parsed;
21   Skin skin_data;
22   QString skin_name;
23 
24   while (!skin_names_to_try.isEmpty()) {
25     skin_name = skin_names_to_try.takeFirst();
26     skin_data = skinInfo(skin_name, &skin_parsed);
27 
28     if (skin_parsed) {
29       loadSkinFromData(skin_data);
30 
31       // Set this 'Skin' object as active one.
32       m_currentSkin = skin_data;
33       qDebugNN << LOGSEC_GUI << "Skin" << QUOTE_W_SPACE(skin_name) << "loaded.";
34       return;
35     }
36     else {
37       qWarningNN << LOGSEC_GUI << "Failed to load skin" << QUOTE_W_SPACE_DOT(skin_name);
38     }
39   }
40 
41   qCriticalNN << LOGSEC_GUI << "Failed to load selected or default skin. Quitting!";
42 }
43 
loadSkinFromData(const Skin & skin)44 void SkinFactory::loadSkinFromData(const Skin& skin) {
45   if (!skin.m_rawData.isEmpty()) {
46     if (qApp->styleSheet().simplified().isEmpty()) {
47       qApp->setStyleSheet(skin.m_rawData);
48     }
49     else {
50       qCriticalNN << LOGSEC_GUI
51                   << "Skipped setting of application style and skin because there is already some style set.";
52     }
53   }
54 
55   qApp->setStyle(qApp->settings()->value(GROUP(GUI), SETTING(GUI::Style)).toString());
56 }
57 
setCurrentSkinName(const QString & skin_name)58 void SkinFactory::setCurrentSkinName(const QString& skin_name) {
59   qApp->settings()->setValue(GROUP(GUI), GUI::Skin, skin_name);
60 }
61 
customSkinBaseFolder() const62 QString SkinFactory::customSkinBaseFolder() const {
63   return qApp->userDataFolder() + QDir::separator() + APP_SKIN_USER_FOLDER;
64 }
65 
selectedSkinName() const66 QString SkinFactory::selectedSkinName() const {
67   return qApp->settings()->value(GROUP(GUI), SETTING(GUI::Skin)).toString();
68 }
69 
adBlockedPage(const QString & url,const QString & filter)70 QString SkinFactory::adBlockedPage(const QString& url, const QString& filter) {
71   const QString& adblocked = currentSkin().m_adblocked.arg(tr("This page was blocked by AdBlock"),
72                                                            tr(R"(Blocked URL: "%1"<br/>Used filter: "%2")").arg(url,
73                                                                                                                 filter));
74 
75   return currentSkin().m_layoutMarkupWrapper.arg(tr("This page was blocked by AdBlock"), adblocked);
76 }
77 
skinInfo(const QString & skin_name,bool * ok) const78 Skin SkinFactory::skinInfo(const QString& skin_name, bool* ok) const {
79   Skin skin;
80   QStringList base_skin_folders;
81 
82   base_skin_folders.append(APP_SKIN_PATH);
83   base_skin_folders.append(customSkinBaseFolder());
84 
85   while (!base_skin_folders.isEmpty()) {
86     const QString skin_folder_no_sep = base_skin_folders.takeAt(0).replace(QDir::separator(),
87                                                                            QL1C('/')) + QL1C('/') + skin_name;
88     const QString skin_folder = skin_folder_no_sep + QDir::separator();
89     const QString metadata_file = skin_folder + APP_SKIN_METADATA_FILE;
90 
91     if (QFile::exists(metadata_file)) {
92       QFile skin_file(metadata_file);
93       QDomDocument dokument;
94 
95       if (!skin_file.open(QIODevice::OpenModeFlag::Text | QIODevice::OpenModeFlag::ReadOnly) ||
96           !dokument.setContent(&skin_file, true)) {
97         if (ok != nullptr) {
98           *ok = false;
99         }
100 
101         return skin;
102       }
103 
104       const QDomNode skin_node = dokument.namedItem(QSL("skin"));
105 
106       // Obtain visible skin name.
107       skin.m_visibleName = skin_name;
108 
109       // Obtain author.
110       skin.m_author = skin_node.namedItem(QSL("author")).namedItem(QSL("name")).toElement().text();
111 
112       // Obtain version.
113       skin.m_version = skin_node.attributes().namedItem(QSL("version")).toAttr().value();
114 
115       // Obtain other information.
116       skin.m_baseName = skin_name;
117 
118       // Obtain color palette.
119       QHash<Skin::PaletteColors, QColor> palette;
120       QDomNodeList colors_of_palette = skin_node.namedItem(QSL("palette")).toElement().elementsByTagName(QSL("color"));
121 
122       for (int i = 0; i < colors_of_palette.size(); i++) {
123         QDomElement elem_clr = colors_of_palette.item(i).toElement();
124 
125         Skin::PaletteColors key = Skin::PaletteColors(elem_clr.attribute(QSL("key")).toInt());
126         QColor value = elem_clr.text();
127 
128         if (value.isValid()) {
129           palette.insert(key, value);
130         }
131       }
132 
133       skin.m_colorPalette = palette;
134 
135       // Free resources.
136       skin_file.close();
137       skin_file.deleteLater();
138 
139       // Here we use "/" instead of QDir::separator() because CSS2.1 url field
140       // accepts '/' as path elements separator.
141       //
142       // USER_DATA_PLACEHOLDER is placeholder for the actual path to skin folder. This is needed for using
143       // images within the QSS file.
144       // So if one uses "%data%/images/border.png" in QSS then it is
145       // replaced by fully absolute path and target file can
146       // be safely loaded.
147       //
148       // %style% placeholder is used in main wrapper HTML file to be replaced with custom skin-wide CSS.
149       skin.m_layoutMarkupWrapper = QString::fromUtf8(IOFactory::readFile(skin_folder + QL1S("html_wrapper.html")));
150       skin.m_layoutMarkupWrapper = skin.m_layoutMarkupWrapper.replace(QSL(USER_DATA_PLACEHOLDER),
151                                                                       skin_folder_no_sep);
152 
153       try {
154         auto custom_css = IOFactory::readFile(skin_folder + QL1S("html_style.css"));
155 
156         skin.m_layoutMarkupWrapper = skin.m_layoutMarkupWrapper.replace(QSL(SKIN_STYLE_PLACEHOLDER),
157                                                                         QString::fromUtf8(custom_css));
158       }
159       catch (...) {
160         qWarningNN << "Skin"
161                    << QUOTE_W_SPACE(skin_name)
162                    << "does not support separated custom CSS.";
163       }
164 
165       skin.m_enclosureImageMarkup = QString::fromUtf8(IOFactory::readFile(skin_folder + QL1S("html_enclosure_image.html")));
166       skin.m_enclosureImageMarkup = skin.m_enclosureImageMarkup.replace(QSL(USER_DATA_PLACEHOLDER),
167                                                                         skin_folder_no_sep);
168       skin.m_layoutMarkup = QString::fromUtf8(IOFactory::readFile(skin_folder + QL1S("html_single_message.html")));
169       skin.m_layoutMarkup = skin.m_layoutMarkup.replace(QSL(USER_DATA_PLACEHOLDER),
170                                                         skin_folder_no_sep);
171       skin.m_enclosureMarkup = QString::fromUtf8(IOFactory::readFile(skin_folder + QL1S("html_enclosure_every.html")));
172       skin.m_enclosureMarkup = skin.m_enclosureMarkup.replace(QSL(USER_DATA_PLACEHOLDER),
173                                                               skin_folder_no_sep);
174       skin.m_rawData = QString::fromUtf8(IOFactory::readFile(skin_folder + QL1S("theme.css")));
175       skin.m_rawData = skin.m_rawData.replace(QSL(USER_DATA_PLACEHOLDER),
176                                               skin_folder_no_sep);
177       skin.m_adblocked = QString::fromUtf8(IOFactory::readFile(skin_folder + QL1S("html_adblocked.html")));
178       skin.m_adblocked = skin.m_adblocked.replace(QSL(USER_DATA_PLACEHOLDER),
179                                                   skin_folder_no_sep);
180 
181       if (ok != nullptr) {
182         *ok = !skin.m_author.isEmpty() && !skin.m_version.isEmpty() &&
183               !skin.m_baseName.isEmpty() &&
184               !skin.m_layoutMarkup.isEmpty();
185       }
186 
187       break;
188     }
189   }
190 
191   return skin;
192 }
193 
installedSkins() const194 QList<Skin> SkinFactory::installedSkins() const {
195   QList<Skin> skins;
196   bool skin_load_ok;
197   QStringList skin_directories = QDir(APP_SKIN_PATH).entryList(QDir::Filter::Dirs |
198                                                                QDir::Filter::NoDotAndDotDot |
199                                                                QDir::Filter::NoSymLinks |
200                                                                QDir::Filter::Readable);
201 
202   skin_directories.append(QDir(customSkinBaseFolder()).entryList(QDir::Filter::Dirs |
203                                                                  QDir::Filter::NoDotAndDotDot |
204                                                                  QDir::Filter::NoSymLinks |
205                                                                  QDir::Filter::Readable));
206 
207   for (const QString& base_directory : skin_directories) {
208     const Skin skin_info = skinInfo(base_directory, &skin_load_ok);
209 
210     if (skin_load_ok) {
211       skins.append(skin_info);
212     }
213   }
214 
215   return skins;
216 }
217 
qHash(const Skin::PaletteColors & key)218 uint qHash(const Skin::PaletteColors& key) {
219   return uint(key);
220 }
221