Members of the KDE Community are recommended to subscribe to the kde-community mailing list at https://mail.kde.org/mailman/listinfo/kde-community to allow them to participate in important discussions and receive other important announcements

Commit 78d8377a authored by Anna Medonosová's avatar Anna Medonosová Committed by Anna Medonosová

Make appimages updateable

This merge request is implementing self-update capabilities for
AppImages inside Krita. Apart from integration of the tools into Krita's
codebase, there are also updated scripts for AppImage building and icons
for update channels.

There are multiple parts to this patch:

1. C++ code for integration of AppImageUpdate and changes to previous
update notifications Both of those options are called Updaters. There
is:
  * The Manual Updater, which is in fact a fancy name for checking for
new version on the website and displaying a message to the user together
with a link to the release notes. This is the original update
notification code, I have slightly modified version checking and moved
the code into it's own class.
  * The AppImage Updater, which is used if
Krita detects that it runs from an AppImage. This updater can actually
download new version of Krita, if it is available.

2. updated scripts for building appimages I have added code to bundle
AppImageUpdate into the AppImage being built. Also, the build scripts
infer the update channel (stable, Beta, Plus or Next) from a combination
of Krita version and git information. This info is used for setting up
proper update channel and selecting the right branding option. I have
also added helper scripts for signing the already built AppImage
(sign_appimage.sh, generate_zsync.sh; also strip_appimage_signature.sh and
validate_appimage_signature.sh for debugging purposes) and rewriting
update information of the built AppImage (update_updinfo.sh).

3. icons for update channels I have added icons I made for myself to
make sense of all my AppImages. The icons are in krita/pics/branding,
together with a script for generating all icon sizes from svg
(generate_icons.sh).

Changes to building - there are two new cmake flags:

  * ENABLE_UPDATERS (bool, can be ON or OFF) - this flag toggles all
    updater code, both the appimage updating and update notifications
  * BRANDING (string, can be default, Beta, Plus, or Next) - this flag
    controls which app icons will be installed
parent 2b1946c8
......@@ -193,6 +193,16 @@ option(ENABLE_PYTHON_2 "Enables the compiler to look for Python 2.7 instead of P
option(BUILD_KRITA_QT_DESIGNER_PLUGINS "Build Qt Designer plugins for Krita widgets" OFF)
add_feature_info("Build Qt Designer plugins" BUILD_KRITA_QT_DESIGNER_PLUGINS "Builds Qt Designer plugins for Krita widgets (use -DBUILD_KRITA_QT_DESIGNER_PLUGINS=ON to enable).")
option(ENABLE_UPDATERS "Enable updaters/update notifications" OFF)
configure_file(config-updaters.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-updaters.h)
add_feature_info("Enable updaters" ENABLE_UPDATERS "Enable updaters/update notifications.")
# Branding. Available options: default, Beta, Plus, Next. Can be set from command line
if (NOT DEFINED BRANDING)
set(BRANDING "default")
endif()
message(STATUS "Branding selected: ${BRANDING}")
include(MacroJPEG)
#########################################################
......
/* config-updaters.h. Generated by cmake from config-updaters.h.cmake */
#cmakedefine ENABLE_UPDATERS 1
......@@ -9,7 +9,7 @@ add_subdirectory( dtd )
add_subdirectory( data )
add_subdirectory( integration )
# Install the application icons following the freedesktop icon theme spec
add_subdirectory( pics/app )
add_subdirectory( "pics/branding/${BRANDING}" )
if (ANDROID)
include_directories (${Qt5AndroidExtras_INCLUDE_DIRS})
......@@ -19,18 +19,18 @@ set(krita_SRCS main.cc)
# Set the application icon on the application
if (NOT APPLE)
file(GLOB ICON_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/pics/app/*-apps-krita.png")
file(GLOB ICON_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/*-apps-krita.png")
else()
set(ICON_SRCS
"${CMAKE_CURRENT_SOURCE_DIR}/pics/app/16-apps-krita.png"
"${CMAKE_CURRENT_SOURCE_DIR}/pics/app/32-apps-krita.png"
"${CMAKE_CURRENT_SOURCE_DIR}/pics/app/48-apps-krita.png"
"${CMAKE_CURRENT_SOURCE_DIR}/pics/app/128-apps-krita.png"
"${CMAKE_CURRENT_SOURCE_DIR}/pics/app/256-apps-krita.png"
"${CMAKE_CURRENT_SOURCE_DIR}/pics/app/512-apps-krita.png"
"${CMAKE_CURRENT_SOURCE_DIR}/pics/app/1024-apps-krita.png"
)
endif()
set(ICON_SRCS
"${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/16-apps-krita.png"
"${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/32-apps-krita.png"
"${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/48-apps-krita.png"
"${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/128-apps-krita.png"
"${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/256-apps-krita.png"
"${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/512-apps-krita.png"
"${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/1024-apps-krita.png"
)
endif()
ecm_add_app_icon(krita_SRCS ICONS ${ICON_SRCS})
# Install the mimetype icons
......@@ -77,9 +77,9 @@ if (ANDROID)
else()
add_executable(krita ${krita_SRCS})
endif()
target_link_libraries(krita
target_link_libraries(krita
PRIVATE
kritaui
kritaui
Qt5::Core
Qt5::Gui
Qt5::Widgets
......
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource>
<RCC>
<qresource prefix="/">
<file>dark_application-exit.svg</file>
<file>dark_application-pdf.svg</file>
<file>dark_applications-system.svg</file>
......@@ -27,7 +26,7 @@
<file>dark_document-open-recent.svg</file>
<file>dark_document-open.svg</file>
<file>dark_document-print-preview.svg</file>
<file alias="dark_document-properties.svg">dark_configure.svg</file>
<file alias="dark_document-properties.svg">dark_configure.svg</file>
<file>dark_document-save-as.svg</file>
<file>dark_document-save.svg</file>
<file>dark_download.svg</file>
......@@ -85,6 +84,6 @@
<file>dark_zoom-in.svg</file>
<file>dark_zoom-original.svg</file>
<file>dark_zoom-out.svg</file>
<file>dark_update-medium.svg</file>
</qresource>
</RCC>
<svg version="1.1" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
<defs>
<style type="text/css" id="current-color-scheme">.ColorScheme-Text {
color:#232629;
}
.ColorScheme-NeutralText {
color:#f67400;
}</style>
</defs>
<path class="ColorScheme-Text" fill="currentColor" d="m10.943359 3.0019531c-2.3096783 0.0179358-4.5777809 1.0323219-6.1289059 2.921875-2.4818 3.023337-2.4103312 7.4081339 0.1679688 10.34961l0.7519531-0.658204c-2.2602-2.578617-2.3221844-6.4042931-0.1464844-9.0546871 2.175699-2.650394 5.9425094-3.3375469 8.9121094-1.6230469l-0.5 0.8671875 2.480469-0.2988281-0.980469-2.2988282-0.5 0.8652344c-1.270312-0.7334062-2.670833-1.0810739-4.056641-1.0703125zm6.072266 2.7265625-0.751953 0.6582032c1.498615 1.7097401 2.023814 3.9662772 1.578125 6.0664062a3.9904534 4.0021063 0 0 1 0.878906 0.625c0.193608-0.721967 0.289408-1.46347 0.277344-2.205078-0.029962-1.8417764-0.693272-3.6737934-1.982422-5.1445314zm-6.017578 0.2714844-4.0000001 4h2v5h3.1406251a3.9904534 4.0021063 0 0 1 0.859375-1.626953v-3.373047h2l-4-4zm-2.998047 10.197266-2.4824219 0.298828 0.9824219 2.298828 0.5-0.865234c1.8980665 1.095835 4.087429 1.331359 6.082031 0.794921a3.9904534 4.0021063 0 0 1-0.623047-0.882812c-1.644174 0.350774-3.4090691 0.115547-4.958984-0.779297l0.5-0.865234z"/>
<path class="ColorScheme-NeutralText" fill="currentColor" d="m16 13a2.9999907 2.9999907 0 0 0-3 3 2.9999907 2.9999907 0 0 0 3 3 2.9999907 2.9999907 0 0 0 3-3 2.9999907 2.9999907 0 0 0-3-3z"/>
</svg>
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource>
<RCC>
<qresource prefix="/">
<file>light_application-exit.svg</file>
<file>light_application-pdf.svg</file>
<file>light_applications-system.svg</file>
......@@ -28,7 +27,7 @@
<file>light_document-open.svg</file>
<file>light_document-print-preview.svg</file>
<file>light_document-print.svg</file>
<file alias="light_document-properties.svg">light_configure.svg</file>
<file alias="light_document-properties.svg">light_configure.svg</file>
<file>light_document-save-as.svg</file>
<file>light_document-save.svg</file>
<file>light_download.svg</file>
......@@ -86,6 +85,7 @@
<file>light_zoom-in.svg</file>
<file>light_zoom-original.svg</file>
<file>light_zoom-out.svg</file>
</qresource>
<file>breeze-light-icons.qrc</file>
<file>light_update-medium.svg</file>
</qresource>
</RCC>
<svg version="1.1" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
<defs>
<style type="text/css" id="current-color-scheme">.ColorScheme-Text {
color:#eff0f1;
}
.ColorScheme-NeutralText {
color:#f67400;
}</style>
</defs>
<path class="ColorScheme-Text" fill="currentColor" d="m10.943359 3.0019531c-2.3096783 0.0179358-4.5777809 1.0323219-6.1289059 2.921875-2.4818 3.023337-2.4103312 7.4081339 0.1679688 10.34961l0.7519531-0.658204c-2.2602-2.578617-2.3221844-6.4042931-0.1464844-9.0546871 2.175699-2.650394 5.9425094-3.3375469 8.9121094-1.6230469l-0.5 0.8671875 2.480469-0.2988281-0.980469-2.2988282-0.5 0.8652344c-1.270312-0.7334062-2.670833-1.0810739-4.056641-1.0703125zm6.072266 2.7265625-0.751953 0.6582032c1.498615 1.7097401 2.023814 3.9662772 1.578125 6.0664062a3.9904534 4.0021063 0 0 1 0.878906 0.625c0.193608-0.721967 0.289408-1.46347 0.277344-2.205078-0.029962-1.8417764-0.693272-3.6737934-1.982422-5.1445314zm-6.017578 0.2714844-4.0000001 4h2v5h3.1406251a3.9904534 4.0021063 0 0 1 0.859375-1.626953v-3.373047h2l-4-4zm-2.998047 10.197266-2.4824219 0.298828 0.9824219 2.298828 0.5-0.865234c1.8980665 1.095835 4.087429 1.331359 6.082031 0.794921a3.9904534 4.0021063 0 0 1-0.623047-0.882812c-1.644174 0.350774-3.4090691 0.115547-4.958984-0.779297l0.5-0.865234z"/>
<path class="ColorScheme-NeutralText" fill="currentColor" d="m16 13a2.9999907 2.9999907 0 0 0-3 3 2.9999907 2.9999907 0 0 0 3 3 2.9999907 2.9999907 0 0 0 3-3 2.9999907 2.9999907 0 0 0-3-3z"/>
</svg>
ecm_install_icons(
ICONS
1024-apps-krita.png
128-apps-krita.png
16-apps-krita.png
22-apps-krita.png
256-apps-krita.png
32-apps-krita.png
48-apps-krita.png
512-apps-krita.png
64-apps-krita.png
sc-apps-krita.svgz
DESTINATION
${ICON_INSTALL_DIR} )
ecm_install_icons(
ICONS
1024-apps-krita.png
128-apps-krita.png
16-apps-krita.png
22-apps-krita.png
256-apps-krita.png
32-apps-krita.png
48-apps-krita.png
512-apps-krita.png
64-apps-krita.png
sc-apps-krita.svgz
DESTINATION
${ICON_INSTALL_DIR} )
ecm_install_icons(
ICONS
1024-apps-krita.png
128-apps-krita.png
16-apps-krita.png
22-apps-krita.png
256-apps-krita.png
32-apps-krita.png
48-apps-krita.png
512-apps-krita.png
64-apps-krita.png
sc-apps-krita.svgz
DESTINATION
${ICON_INSTALL_DIR} )
ecm_install_icons(
ICONS
1024-apps-krita.png
128-apps-krita.png
16-apps-krita.png
22-apps-krita.png
256-apps-krita.png
32-apps-krita.png
48-apps-krita.png
512-apps-krita.png
64-apps-krita.png
sc-apps-krita.svgz
DESTINATION
${ICON_INSTALL_DIR} )
ecm_install_icons(
ICONS
1024-apps-krita.png
128-apps-krita.png
16-apps-krita.png
22-apps-krita.png
256-apps-krita.png
32-apps-krita.png
48-apps-krita.png
512-apps-krita.png
64-apps-krita.png
sc-apps-krita.svgz
DESTINATION
${ICON_INSTALL_DIR} )
#!/usr/bin/env bash
set -x
for dir in $(find ./ -maxdepth 1 -mindepth 1 -type d); do
rm ${dir}/*.png ${dir}/*.ico
for i in $(echo "16 22 32 48 64 128 256 512 1024"); do
inkscape -z -e ${dir}/${i}-apps-krita.png -w ${i} -h ${i} ${dir}/sc-apps-krita.svgz
#convert ${dir}/sc-apps-krita.svgz -resize "${i}x" ${dir}/${i}-apps-krita.png
done
convert ${dir}/sc-apps-krita.svgz -alpha off -resize 256x256 \
-define icon:auto-resize="256,128,96,64,48,32,16" \
${dir}/krita.ico
done
......@@ -283,7 +283,6 @@ set(kritaui_LIB_SRCS
utils/KisSpinBoxSplineUnitConverter.cpp
utils/KisClipboardUtil.cpp
utils/KisDitherUtil.cpp
input/kis_input_manager.cpp
input/kis_input_manager_p.cpp
input/kis_extended_modifiers_mapper.cpp
......@@ -381,6 +380,7 @@ set(kritaui_LIB_SRCS
KisApplicationArguments.cpp
KisNetworkAccessManager.cpp
KisRssReader.cpp
KisMultiFeedRSSModel.cpp
KisRemoteFileFetcher.cpp
......@@ -453,6 +453,23 @@ if (UNIX)
)
endif()
if (ENABLE_UPDATERS)
if (UNIX)
set(kritaui_LIB_SRCS
${kritaui_LIB_SRCS}
utils/KisAppimageUpdater.cpp
)
endif()
set(kritaui_LIB_SRCS
${kritaui_LIB_SRCS}
utils/KisUpdaterBase.cpp
utils/KisManualUpdater.cpp
utils/KisUpdaterStatus.cpp
)
endif()
if(APPLE)
set(kritaui_LIB_SRCS
${kritaui_LIB_SRCS}
......@@ -565,7 +582,7 @@ add_library(kritaui SHARED ${kritaui_HEADERS_MOC} ${kritaui_LIB_SRCS} )
generate_export_header(kritaui BASE_NAME kritaui)
target_link_libraries(kritaui KF5::CoreAddons KF5::Completion KF5::I18n KF5::ItemViews Qt5::Network
kritaimpex kritacolor kritaimage kritalibbrush kritawidgets kritawidgetutils kritaresources ${PNG_LIBRARIES} LibExiv2::LibExiv2
kritaversion kritaimpex kritacolor kritaimage kritalibbrush kritawidgets kritawidgetutils kritaresources ${PNG_LIBRARIES} LibExiv2::LibExiv2
)
if (ANDROID)
......@@ -620,3 +637,9 @@ install(TARGETS kritaui ${INSTALL_TARGETS_DEFAULT_ARGS})
if (APPLE)
install(FILES osx.stylesheet DESTINATION ${DATA_INSTALL_DIR}/krita)
endif ()
if (UNIX AND BUILD_TESTING AND ENABLE_UPDATERS)
install(FILES tests/data/AppImageUpdateDummy
PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE
DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
endif ()
......@@ -37,104 +37,29 @@
#include <QXmlStreamReader>
#include <QCoreApplication>
#include <QLocale>
#include <QFile>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <KisNetworkAccessManager.h>
QString shortenHtml(QString html)
{
html.replace(QLatin1String("<a"), QLatin1String("<i"));
html.replace(QLatin1String("</a"), QLatin1String("</i"));
uint firstParaEndXhtml = (uint) html.indexOf(QLatin1String("</p>"));
uint firstParaEndHtml = (uint) html.indexOf(QLatin1String("<p>"), html.indexOf(QLatin1String("<p>"))+1);
uint firstParaEndBr = (uint) html.indexOf(QLatin1String("<br"));
uint firstParaEnd = qMin(firstParaEndXhtml, firstParaEndHtml);
firstParaEnd = qMin(firstParaEnd, firstParaEndBr);
return html.left(firstParaEnd);
}
class RssReader {
public:
RssItem parseItem() {
RssItem item;
item.source = requestUrl;
item.blogIcon = blogIcon;
item.blogName = blogName;
while (!streamReader.atEnd()) {
switch (streamReader.readNext()) {
case QXmlStreamReader::StartElement:
if (streamReader.name() == QLatin1String("title"))
item.title = streamReader.readElementText();
else if (streamReader.name() == QLatin1String("link"))
item.link = streamReader.readElementText();
else if (streamReader.name() == QLatin1String("pubDate")) {
QString dateStr = streamReader.readElementText();
item.pubDate = QDateTime::fromString(dateStr, Qt::RFC2822Date);
}
else if (streamReader.name() == QLatin1String("category"))
item.category = streamReader.readElementText();
else if (streamReader.name() == QLatin1String("description"))
item.description = streamReader.readElementText(); //shortenHtml(streamReader.readElementText());
break;
case QXmlStreamReader::EndElement:
if (streamReader.name() == QLatin1String("item"))
return item;
break;
default:
break;
}
}
return RssItem();
}
RssItemList parse(QNetworkReply *reply) {
QUrl source = reply->request().url();
requestUrl = source.toString();
streamReader.setDevice(reply);
RssItemList list;
while (!streamReader.atEnd()) {
switch (streamReader.readNext()) {
case QXmlStreamReader::StartElement:
if (streamReader.name() == QLatin1String("item"))
list.append(parseItem());
else if (streamReader.name() == QLatin1String("title"))
blogName = streamReader.readElementText();
else if (streamReader.name() == QLatin1String("link")) {
if (!streamReader.namespaceUri().isEmpty())
break;
QString favIconString(streamReader.readElementText());
QUrl favIconUrl(favIconString);
favIconUrl.setPath(QLatin1String("favicon.ico"));
blogIcon = favIconUrl.toString();
blogIcon = QString(); // XXX: fix the favicon on krita.org!
}
break;
default:
break;
}
}
return list;
}
private:
QXmlStreamReader streamReader;
QString requestUrl;
QString blogIcon;
QString blogName;
};
#include <KisRssReader.h>
MultiFeedRssModel::MultiFeedRssModel(QObject *parent) :
QAbstractListModel(parent),
m_networkAccessManager(new KisNetworkAccessManager),
m_articleCount(0)
{
connect(m_networkAccessManager, SIGNAL(finished(QNetworkReply*)),
SLOT(appendFeedData(QNetworkReply*)), Qt::QueuedConnection);
initialize();
}
MultiFeedRssModel::MultiFeedRssModel(KisNetworkAccessManager* nam, QObject* parent)
: QAbstractListModel(parent),
m_networkAccessManager(nam),
m_articleCount(0)
{
initialize();
}
MultiFeedRssModel::~MultiFeedRssModel()
......@@ -144,18 +69,24 @@ MultiFeedRssModel::~MultiFeedRssModel()
QHash<int, QByteArray> MultiFeedRssModel::roleNames() const
{
QHash<int, QByteArray> roleNames;
roleNames[TitleRole] = "title";
roleNames[DescriptionRole] = "description";
roleNames[PubDateRole] = "pubDate";
roleNames[LinkRole] = "link";
roleNames[CategoryRole] = "category";
roleNames[BlogNameRole] = "blogName";
roleNames[BlogIconRole] = "blogIcon";
roleNames[KisRssReader::RssRoles::TitleRole] = "title";
roleNames[KisRssReader::RssRoles::DescriptionRole] = "description";
roleNames[KisRssReader::RssRoles::PubDateRole] = "pubDate";
roleNames[KisRssReader::RssRoles::LinkRole] = "link";
roleNames[KisRssReader::RssRoles::CategoryRole] = "category";
roleNames[KisRssReader::RssRoles::BlogNameRole] = "blogName";
roleNames[KisRssReader::RssRoles::BlogIconRole] = "blogIcon";
return roleNames;
}
void MultiFeedRssModel::addFeed(const QString& feed)
{
if (m_sites.contains(feed)) {
// do not add the feed twice
return;
}
m_sites << feed;
const QUrl feedUrl(feed);
QMetaObject::invokeMethod(m_networkAccessManager, "getUrl",
Qt::QueuedConnection, Q_ARG(QUrl, feedUrl));
......@@ -168,9 +99,9 @@ bool sortForPubDate(const RssItem& item1, const RssItem& item2)
void MultiFeedRssModel::appendFeedData(QNetworkReply *reply)
{
RssReader reader;
KisRssReader reader;
m_aggregatedFeed.append(reader.parse(reply));
std::sort(m_aggregatedFeed.begin(), m_aggregatedFeed.end(), sortForPubDate);
sortAggregatedFeed();
setArticleCount(m_aggregatedFeed.size());
beginResetModel();
endResetModel();
......@@ -178,6 +109,17 @@ void MultiFeedRssModel::appendFeedData(QNetworkReply *reply)
emit feedDataChanged();
}
void MultiFeedRssModel::sortAggregatedFeed()
{
std::sort(m_aggregatedFeed.begin(), m_aggregatedFeed.end(), sortForPubDate);
}
void MultiFeedRssModel::initialize()
{
connect(m_networkAccessManager, SIGNAL(finished(QNetworkReply*)),
SLOT(appendFeedData(QNetworkReply*)), Qt::QueuedConnection);
}
void MultiFeedRssModel::removeFeed(const QString &feed)
{
QMutableListIterator<RssItem> it(m_aggregatedFeed);
......@@ -187,6 +129,8 @@ void MultiFeedRssModel::removeFeed(const QString &feed)
it.remove();
}
setArticleCount(m_aggregatedFeed.size());
m_sites.removeOne(feed);
}
int MultiFeedRssModel::rowCount(const QModelIndex &) const
......@@ -206,19 +150,19 @@ QVariant MultiFeedRssModel::data(const QModelIndex &index, int role) const
"<br><small>(" + item.pubDate.toLocalTime().toString(Qt::DefaultLocaleShortDate) + ") "
+ item.description.left(90).append("...") + "</small><hr>");
}
case TitleRole:
case KisRssReader::RssRoles::TitleRole:
return item.title;
case DescriptionRole:
case KisRssReader::RssRoles::DescriptionRole:
return item.description;
case PubDateRole:
case KisRssReader::RssRoles::PubDateRole:
return item.pubDate.toString("dd-MM-yyyy hh:mm");
case LinkRole:
case KisRssReader::RssRoles::LinkRole:
return item.link;
case CategoryRole:
case KisRssReader::RssRoles::CategoryRole:
return item.category;
case BlogNameRole:
case KisRssReader::RssRoles::BlogNameRole:
return item.blogName;
case BlogIconRole:
case KisRssReader::RssRoles::BlogIconRole:
return item.blogIcon;
}
......
......@@ -37,38 +37,24 @@
#include <QStringList>
#include <QDateTime>
#include <KisRssReader.h>
#include <kritaui_export.h>
class QThread;
class QNetworkReply;
class QNetworkAccessManager;
struct RssItem {
QString source;
QString title;
QString link;
QString description;
QString category;
QString blogName;
QString blogIcon;
QDateTime pubDate;
};
typedef QList<RssItem> RssItemList;
class KisNetworkAccessManager;