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