29#include <QRegularExpression>
49 , mBaseUrl( feedUrl.toString() )
52 , mFeedKey( keyForFeed( mBaseUrl ) )
57 QUrlQuery query( feedUrl );
64 if ( feedLanguage.isEmpty() )
68 if ( !feedLanguage.isEmpty() && feedLanguage != QLatin1String(
"C" ) )
69 query.addQueryItem( QStringLiteral(
"lang" ), feedLanguage.mid( 0, 2 ) );
77 if ( feedUrl.isLocalFile() )
79 query.addQueryItem( QStringLiteral(
"lat" ), QString::number(
static_cast< int >( feedLat ) ) );
80 query.addQueryItem( QStringLiteral(
"lon" ), QString::number(
static_cast< int >( feedLong ) ) );
90 if ( feedUrl.isLocalFile() )
92 if ( !query.toString().isEmpty() )
93 mFeedUrl = QUrl( mFeedUrl.toString() +
'_' + query.toString() );
97 mFeedUrl.setQuery( query );
109 const int beforeSize = mEntries.size();
110 mEntries.erase( std::remove_if( mEntries.begin(), mEntries.end(),
111 [key, &dismissed](
const Entry & entry )
113 if ( entry.key == key )
119 } ), mEntries.end() );
120 if ( beforeSize == mEntries.size() )
123 sTreeNewsFeedEntries->deleteItem( QString::number( key ), {mFeedKey} );
126 if ( !dismissed.imageUrl.isEmpty() )
129 const QString imagePath = QStringLiteral(
"%1/%2.png" ).arg( previewDir ).arg( key );
130 if ( QFile::exists( imagePath ) )
132 QFile::remove( imagePath );
136 if ( !mBlockSignals )
137 emit entryDismissed( dismissed );
142 const QList< QgsNewsFeedParser::Entry >
entries = mEntries;
156 QNetworkRequest req( mFeedUrl );
159 mFetchStartTime = QDateTime::currentDateTimeUtc().toSecsSinceEpoch();
166 QNetworkReply *reply = task->
reply();
173 if ( reply->error() != QNetworkReply::NoError )
175 QgsMessageLog::logMessage( tr(
"News feed request failed [error: %1]" ).arg( reply->errorString() ) );
180 QMetaObject::invokeMethod(
this,
"onFetch", Qt::QueuedConnection, Q_ARG( QString, task->
contentAsString() ) );
186void QgsNewsFeedParser::onFetch(
const QString &content )
192 const QVariantList
entries = json.toList();
193 QList< QgsNewsFeedParser::Entry > newEntries;
194 newEntries.reserve(
entries.size() );
195 for (
const QVariant &e :
entries )
198 const QVariantMap entryMap = e.toMap();
199 newEntry.key = entryMap.value( QStringLiteral(
"pk" ) ).toInt();
200 newEntry.title = entryMap.value( QStringLiteral(
"title" ) ).toString();
201 newEntry.imageUrl = entryMap.value( QStringLiteral(
"image" ) ).toString();
202 newEntry.content = entryMap.value( QStringLiteral(
"content" ) ).toString();
203 newEntry.link = entryMap.value( QStringLiteral(
"url" ) ).toString();
204 newEntry.sticky = entryMap.value( QStringLiteral(
"sticky" ) ).toBool();
206 const uint expiry = entryMap.value( QStringLiteral(
"publish_to" ) ).toUInt( &ok );
208 newEntry.expiry.setSecsSinceEpoch( expiry );
209 newEntries.append( newEntry );
211 if ( !newEntry.imageUrl.isEmpty() )
212 fetchImageForEntry( newEntry );
214 mEntries.append( newEntry );
215 storeEntryInSettings( newEntry );
222void QgsNewsFeedParser::readStoredEntries()
225 std::sort( existing.begin(), existing.end(), [](
const QString & a,
const QString & b )
227 return a.toInt() < b.toInt();
229 mEntries.reserve( existing.size() );
230 for (
const QString &entry : existing )
232 const Entry e = readEntryFromSettings( entry.toInt() );
233 if ( !e.expiry.isValid() || e.expiry > QDateTime::currentDateTime() )
234 mEntries.append( e );
238 mBlockSignals =
true;
240 mBlockSignals =
false;
255 if ( !entry.imageUrl.isEmpty() )
258 const QString imagePath = QStringLiteral(
"%1/%2.png" ).arg( previewDir ).arg( entry.key );
259 if ( QFile::exists( imagePath ) )
261 const QImage img( imagePath );
262 entry.image = QPixmap::fromImage( img );
266 fetchImageForEntry( entry );
279 if ( entry.
expiry.isValid() )
289 const auto findIter = std::find_if( mEntries.begin(), mEntries.end(), [entry](
const QgsNewsFeedParser::Entry & candidate )
291 return candidate.key == entry.key;
293 if ( findIter != mEntries.end() )
295 const int entryIndex =
static_cast< int >( std::distance( mEntries.begin(), findIter ) );
297 QImage img = QImage::fromData( fetcher->
reply()->readAll() );
299 QSize size = img.size();
301 if ( size.width() > 250 )
303 size.setHeight(
static_cast< int >( size.height() *
static_cast< double >( 250 ) / size.width() ) );
304 size.setWidth( 250 );
307 if ( size.height() > 177 )
309 size.setWidth(
static_cast< int >( size.width() *
static_cast< double >( 177 ) / size.height() ) );
310 size.setHeight( 177 );
314 img = img.scaled( size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
317 QImage previewImage( size, QImage::Format_ARGB32 );
318 previewImage.fill( Qt::transparent );
319 QPainter previewPainter( &previewImage );
320 previewPainter.setRenderHint( QPainter::Antialiasing,
true );
321 previewPainter.setRenderHint( QPainter::SmoothPixmapTransform,
true );
322 previewPainter.setPen( Qt::NoPen );
323 previewPainter.setBrush( Qt::black );
324 previewPainter.drawRoundedRect( 0, 0, size.width(), size.height(), 8, 8 );
325 previewPainter.setCompositionMode( QPainter::CompositionMode_SourceIn );
326 previewPainter.drawImage( 0, 0, img );
327 previewPainter.end();
331 QDir().mkdir( previewDir );
332 const QString imagePath = QStringLiteral(
"%1/%2.png" ).arg( previewDir ).arg( entry.
key );
333 previewImage.save( imagePath );
335 mEntries[ entryIndex ].image = QPixmap::fromImage( previewImage );
338 fetcher->deleteLater();
345 static const QRegularExpression sRegexp( QStringLiteral(
"[^a-zA-Z0-9]" ) );
346 QString res = baseUrl;
347 res = res.replace( sRegexp, QString() );
static const QgsSettingsEntryString * settingsLocaleUserLocale
Settings entry locale user locale.
static QString qgisSettingsDirPath()
Returns the path to the settings directory in user's home dir.
static QgsTaskManager * taskManager()
Returns the application's task manager, used for managing application wide background task handling.
static QVariant parseJson(const std::string &jsonString)
Converts JSON jsonString to a QVariant, in case of parsing error an invalid QVariant is returned and ...
Handles HTTP network content fetching in a background task.
QString contentAsString() const
Returns the fetched content as a string.
void fetched()
Emitted when the network content has been fetched, regardless of whether the fetch was successful or ...
QNetworkReply * reply()
Returns the network reply.
HTTP network content fetcher.
void finished()
Emitted when content has loaded.
QNetworkReply * reply()
Returns a reference to the network reply.
void fetchContent(const QUrl &url, const QString &authcfg=QString())
Fetches content from a remote URL and handles redirects.
Represents a single entry from a news feed.
QString content
HTML content of news entry.
bool sticky
true if entry is "sticky" and should always be shown at the top
QUrl link
Optional URL link for entry.
QString imageUrl
Optional URL for image associated with entry.
QDateTime expiry
Optional auto-expiry time for entry.
int key
Unique entry identifier.
QString title
Entry title.
static QgsSettingsTreeNamedListNode * sTreeNewsFeedEntries
static const QgsSettingsEntryString * settingsFeedEntryTitle
void dismissEntry(int key)
Dismisses an entry with matching key.
static const QgsSettingsEntryString * settingsFeedEntryLink
void fetch()
Fetches new entries from the feed's URL.
void fetched(const QList< QgsNewsFeedParser::Entry > &entries)
Emitted when entries have fetched from the feed.
static const QgsSettingsEntryString * settingsFeedEntryImageUrl
static const QgsSettingsEntryDouble * settingsFeedLatitude
QString authcfg() const
Returns the authentication configuration for the parser.
static const QgsSettingsEntryInteger64 * settingsFeedLastFetchTime
QgsNewsFeedParser(const QUrl &feedUrl, const QString &authcfg=QString(), QObject *parent=nullptr)
Constructor for QgsNewsFeedParser, parsing the specified feedUrl.
static const QgsSettingsEntryBool * settingsFeedEntrySticky
void dismissAll()
Dismisses all current news items.
static const QgsSettingsEntryDouble * settingsFeedLongitude
static const QgsSettingsEntryString * settingsFeedLanguage
static const QgsSettingsEntryString * settingsFeedEntryContent
static const QgsSettingsEntryVariant * settingsFeedEntryExpiry
static QString keyForFeed(const QString &baseUrl)
Returns the settings key used for a feed with the given baseUrl.
void entryAdded(const QgsNewsFeedParser::Entry &entry)
Emitted whenever a new entry is available from the feed (as a result of a call to fetch()).
void imageFetched(int key, const QPixmap &pixmap)
Emitted when the image attached to the entry with the specified key has been fetched and is now avail...
QList< QgsNewsFeedParser::Entry > entries() const
Returns a list of existing entries in the feed.
T valueWithDefaultOverride(const T &defaultValueOverride, const QString &dynamicKeyPart=QString()) const
Returns the settings value with a defaultValueOverride and with an optional dynamicKeyPart.
T value(const QString &dynamicKeyPart=QString()) const
Returns settings value.
bool setValue(const T &value, const QString &dynamicKeyPart=QString()) const
Set settings value.
bool exists(const QString &dynamicKeyPart=QString()) const
Returns true if the settings is contained in the underlying QSettings.
A boolean settings entry.
A 64 bits integer (long long) settings entry.
A variant settings entry.
QStringList items(const QStringList &parentsNamedItems=QStringList()) const SIP_THROW(QgsSettingsException)
Returns the list of items.
long addTask(QgsTask *task, int priority=0)
Adds a task to the manager.
@ CanCancel
Task can be canceled.
@ CancelWithoutPrompt
Task can be canceled without any users prompts, e.g. when closing a project or QGIS.
@ Silent
Don't show task updates (such as completion/failure messages) as operating-system level notifications...
void setDescription(const QString &description)
Sets the task's description.
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double.
#define QgsSetRequestInitiatorClass(request, _class)