31#include <QRegularExpression>
35#include "moc_qgsnewsfeedparser.cpp"
37using namespace Qt::StringLiterals;
58 , mBaseUrl( feedUrl.toString() )
69 QUrlQuery query( feedUrl );
76 if ( feedLanguage.isEmpty() )
80 if ( !feedLanguage.isEmpty() && feedLanguage !=
"C"_L1 )
81 query.addQueryItem( u
"lang"_s, feedLanguage.mid( 0, 2 ) );
89 if ( feedUrl.isLocalFile() )
91 query.addQueryItem( u
"lat"_s, QString::number(
static_cast< int >( feedLat ) ) );
92 query.addQueryItem( u
"lon"_s, QString::number(
static_cast< int >( feedLong ) ) );
102 if ( feedUrl.isLocalFile() )
104 if ( !query.toString().isEmpty() )
105 mFeedUrl = QUrl( mFeedUrl.toString() +
'_' + query.toString() );
109 mFeedUrl.setQuery( query );
134 const int beforeSize = mEntries.size();
139 [key, &dismissed](
const Entry &entry ) {
140 if ( entry.key == key )
150 if ( beforeSize == mEntries.size() )
155 sTreeNewsFeedEntries->deleteItem( QString::number( key ), { mFeedKey } );
163 if ( !dismissed.imageUrl.isEmpty() )
166 const QString imagePath = u
"%1/%2.png"_s.arg( previewDir ).arg( key );
167 if ( QFile::exists( imagePath ) )
169 QFile::remove( imagePath );
173 if ( !mBlockSignals )
174 emit entryDismissed( dismissed );
179 const QList< QgsNewsFeedParser::Entry >
entries = mEntries;
198 QNetworkRequest req( mFeedUrl );
201 mFetchStartTime = QDateTime::currentDateTimeUtc().toSecsSinceEpoch();
210 QNetworkReply *reply = task->
reply();
217 if ( reply->error() != QNetworkReply::NoError )
224 QMetaObject::invokeMethod(
this,
"onFetch", Qt::QueuedConnection, Q_ARG( QString, task->
contentAsString() ) );
233void QgsNewsFeedParser::onFetch(
const QString &content )
239 const QVariantList
entries = json.toList();
240 QList< QgsNewsFeedParser::Entry > fetchedEntries;
241 fetchedEntries.reserve(
entries.size() );
242 for (
const QVariant &e :
entries )
245 const QVariantMap entryMap = e.toMap();
246 incomingEntry.key = entryMap.value( u
"pk"_s ).toInt();
247 incomingEntry.title = entryMap.value( u
"title"_s ).toString().trimmed();
248 incomingEntry.imageUrl = entryMap.value( u
"image"_s ).toString();
249 incomingEntry.content = entryMap.value( u
"content"_s ).toString().trimmed();
250 incomingEntry.link = entryMap.value( u
"url"_s ).toString();
251 incomingEntry.sticky = entryMap.value( u
"sticky"_s ).toBool();
252 incomingEntry.published.setSecsSinceEpoch( entryMap.value( u
"publish_from"_s ).toLongLong() );
253 bool hasExpiry =
false;
254 const qlonglong expiry = entryMap.value( u
"publish_to"_s ).toLongLong( &hasExpiry );
256 incomingEntry.expiry.setSecsSinceEpoch( expiry );
258 fetchedEntries.append( incomingEntry );
261 const auto entryIter { std::find_if( mEntries.begin(), mEntries.end(), [incomingEntry](
const QgsNewsFeedParser::Entry &candidate ) { return candidate.key == incomingEntry.key; } ) };
262 const bool entryExists { entryIter != mEntries.end() };
265 if ( hasExpiry && expiry < mFetchStartTime )
270 else if ( entryExists )
272 const bool imageNeedsUpdate = ( entryIter->imageUrl != incomingEntry.imageUrl );
274 if ( imageNeedsUpdate && !entryIter->imageUrl.isEmpty() )
277 const QString imagePath = u
"%1/%2.png"_s.arg( previewDir ).arg( entryIter->key );
278 if ( QFile::exists( imagePath ) )
280 QFile::remove( imagePath );
283 *entryIter = incomingEntry;
284 if ( imageNeedsUpdate && !incomingEntry.imageUrl.isEmpty() )
285 fetchImageForEntry( incomingEntry );
288 storeEntryInSettings( incomingEntry );
292 else if ( !hasExpiry || expiry >= mFetchStartTime )
294 if ( !incomingEntry.imageUrl.isEmpty() )
295 fetchImageForEntry( incomingEntry );
297 mEntries.append( incomingEntry );
298 storeEntryInSettings( incomingEntry );
303 emit
fetched( fetchedEntries );
306void QgsNewsFeedParser::readStoredEntries()
308 QStringList existing;
313 catch ( QgsSettingsException &e )
318 std::sort( existing.begin(), existing.end(), [](
const QString &a,
const QString &b ) { return a.toInt() < b.toInt(); } );
319 mEntries.reserve( existing.size() );
320 for (
const QString &entry : existing )
322 const Entry e = readEntryFromSettings( entry.toInt() );
323 if ( !e.expiry.isValid() || e.expiry > QDateTime::currentDateTime() )
324 mEntries.append( e );
328 mBlockSignals =
true;
330 mBlockSignals =
false;
346 if ( !entry.imageUrl.isEmpty() )
349 const QString imagePath = u
"%1/%2.png"_s.arg( previewDir ).arg( entry.key );
350 if ( QFile::exists( imagePath ) )
352 const QImage img( imagePath );
353 entry.image = QPixmap::fromImage( img );
357 fetchImageForEntry( entry );
371 if ( entry.
expiry.isValid() )
378 QgsNetworkContentFetcher *fetcher =
new QgsNetworkContentFetcher();
380 const auto findIter = std::find_if( mEntries.begin(), mEntries.end(), [entry](
const QgsNewsFeedParser::Entry &candidate ) { return candidate.key == entry.key; } );
381 if ( findIter != mEntries.end() )
383 const int entryIndex =
static_cast< int >( std::distance( mEntries.begin(), findIter ) );
385 QImage img = QImage::fromData( fetcher->
reply()->readAll() );
387 QSize size = img.size();
389 if ( size.width() > 250 )
391 size.setHeight(
static_cast< int >( size.height() *
static_cast< double >( 250 ) / size.width() ) );
392 size.setWidth( 250 );
395 if ( size.height() > 177 )
397 size.setWidth(
static_cast< int >( size.width() *
static_cast< double >( 177 ) / size.height() ) );
398 size.setHeight( 177 );
402 img = img.scaled( size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
405 QImage previewImage( size, QImage::Format_ARGB32 );
406 previewImage.fill( Qt::transparent );
407 QPainter previewPainter( &previewImage );
408 previewPainter.setRenderHint( QPainter::Antialiasing,
true );
409 previewPainter.setRenderHint( QPainter::SmoothPixmapTransform,
true );
410 previewPainter.setPen( Qt::NoPen );
411 previewPainter.setBrush( Qt::black );
412 previewPainter.drawRoundedRect( 0, 0, size.width(), size.height(), 8, 8 );
413 previewPainter.setCompositionMode( QPainter::CompositionMode_SourceIn );
414 previewPainter.drawImage( 0, 0, img );
415 previewPainter.end();
419 QDir().mkdir( previewDir );
420 const QString imagePath = u
"%1/%2.png"_s.arg( previewDir ).arg( entry.
key );
421 previewImage.save( imagePath );
423 mEntries[entryIndex].image = QPixmap::fromImage( previewImage );
426 fetcher->deleteLater();
433 static const QRegularExpression sRegexp( u
"[^a-zA-Z0-9]"_s );
434 QString res = baseUrl;
435 res = res.replace( sRegexp, QString() );
QFlags< SettingsOption > SettingsOptions
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 ...
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true, const char *file=__builtin_FILE(), const char *function=__builtin_FUNCTION(), int line=__builtin_LINE(), Qgis::StringFormat format=Qgis::StringFormat::PlainText)
Adds a message to the log instance (and creates it if necessary).
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.
void fetchContent(const QUrl &url, const QString &authcfg=QString(), const QgsHttpHeaders &headers=QgsHttpHeaders())
Fetches content from a remote URL and handles redirects.
void finished()
Emitted when content has loaded.
QNetworkReply * reply()
Returns a reference to the network reply.
Represents a single entry from a news feed.
QString content
HTML content of news entry.
QDateTime published
Entry publication date.
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
void enabledChanged()
Emitted when the enabled/disabled state of the feed URL associated to the news parser changes.
static const QgsSettingsEntryString * settingsFeedEntryTitle
static const QgsSettingsEntryBool * settingsFeedDisabled
Q_INVOKABLE void dismissEntry(int key)
Dismisses an entry with matching key.
static const QgsSettingsEntryString * settingsFeedEntryLink
void fetch()
Fetches new entries from the feed's URL.
static const QgsSettingsEntryVariant * settingsFeedEntryPublished
void fetched(const QList< QgsNewsFeedParser::Entry > &entries)
Emitted when entries have been fetched from the feed.
void isFetchingChanged()
Emitted when the news parser's fetching state changes.
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
Q_INVOKABLE void dismissAll()
Dismisses all current news items.
static const QgsSettingsEntryDouble * settingsFeedLongitude
static const QgsSettingsEntryString * settingsFeedLanguage
static const QgsSettingsEntryString * settingsFeedEntryContent
void entryUpdated(const QgsNewsFeedParser::Entry &entry)
Emitted whenever an existing entry is available from the feed (as a result of a call to fetch()).
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.
void setEnabled(bool enabled)
Sets whether the feed URL associated with the news parser is enabled.
bool setValue(const T &value, const QString &dynamicKeyPart=QString()) const
Set settings value.
A boolean settings entry.
A 64 bits integer (long long) settings entry.
A variant settings entry.
Custom exception class for settings related exceptions.
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 QgsDebugError(str)
#define QgsSetRequestInitiatorClass(request, _class)