30#include <QRegularExpression>
33#include "moc_qgsnewsfeedparser.cpp"
52 , mBaseUrl( feedUrl.toString() )
60 QUrlQuery query( feedUrl );
67 if ( feedLanguage.isEmpty() )
71 if ( !feedLanguage.isEmpty() && feedLanguage != QLatin1String(
"C" ) )
72 query.addQueryItem( QStringLiteral(
"lang" ), feedLanguage.mid( 0, 2 ) );
80 if ( feedUrl.isLocalFile() )
82 query.addQueryItem( QStringLiteral(
"lat" ), QString::number(
static_cast< int >( feedLat ) ) );
83 query.addQueryItem( QStringLiteral(
"lon" ), QString::number(
static_cast< int >( feedLong ) ) );
93 if ( feedUrl.isLocalFile() )
95 if ( !query.toString().isEmpty() )
96 mFeedUrl = QUrl( mFeedUrl.toString() +
'_' + query.toString() );
100 mFeedUrl.setQuery( query );
112 const int beforeSize = mEntries.size();
113 mEntries.erase( std::remove_if( mEntries.begin(), mEntries.end(),
114 [key, &dismissed](
const Entry & entry )
116 if ( entry.key == key )
122 } ), mEntries.end() );
123 if ( beforeSize == mEntries.size() )
128 sTreeNewsFeedEntries->deleteItem( QString::number( key ), {mFeedKey} );
132 QgsDebugError( QStringLiteral(
"Could not dismiss news feed entry: %1" ).arg( e.
what( ) ) );
136 if ( !dismissed.imageUrl.isEmpty() )
139 const QString imagePath = QStringLiteral(
"%1/%2.png" ).arg( previewDir ).arg( key );
140 if ( QFile::exists( imagePath ) )
142 QFile::remove( imagePath );
146 if ( !mBlockSignals )
147 emit entryDismissed( dismissed );
152 const QList< QgsNewsFeedParser::Entry >
entries = mEntries;
166 QNetworkRequest req( mFeedUrl );
169 mFetchStartTime = QDateTime::currentDateTimeUtc().toSecsSinceEpoch();
176 QNetworkReply *reply = task->
reply();
183 if ( reply->error() != QNetworkReply::NoError )
190 QMetaObject::invokeMethod(
this,
"onFetch", Qt::QueuedConnection, Q_ARG( QString, task->
contentAsString() ) );
196void QgsNewsFeedParser::onFetch(
const QString &content )
202 const QVariantList
entries = json.toList();
203 QList< QgsNewsFeedParser::Entry > fetchedEntries;
204 fetchedEntries.reserve(
entries.size() );
205 for (
const QVariant &e :
entries )
208 const QVariantMap entryMap = e.toMap();
209 incomingEntry.key = entryMap.value( QStringLiteral(
"pk" ) ).toInt();
210 incomingEntry.title = entryMap.value( QStringLiteral(
"title" ) ).toString();
211 incomingEntry.imageUrl = entryMap.value( QStringLiteral(
"image" ) ).toString();
212 incomingEntry.content = entryMap.value( QStringLiteral(
"content" ) ).toString();
213 incomingEntry.link = entryMap.value( QStringLiteral(
"url" ) ).toString();
214 incomingEntry.sticky = entryMap.value( QStringLiteral(
"sticky" ) ).toBool();
215 bool hasExpiry =
false;
216 const qlonglong expiry = entryMap.value( QStringLiteral(
"publish_to" ) ).toLongLong( &hasExpiry );
218 incomingEntry.expiry.setSecsSinceEpoch( expiry );
220 fetchedEntries.append( incomingEntry );
223 const auto entryIter { std::find_if( mEntries.begin(), mEntries.end(), [incomingEntry](
const QgsNewsFeedParser::Entry & candidate )
225 return candidate.key == incomingEntry.key;
227 const bool entryExists { entryIter != mEntries.end() };
230 if ( hasExpiry && expiry < mFetchStartTime )
235 else if ( entryExists )
237 const bool imageNeedsUpdate = ( entryIter->imageUrl != incomingEntry.imageUrl );
239 if ( imageNeedsUpdate && ! entryIter->imageUrl.isEmpty() )
242 const QString imagePath = QStringLiteral(
"%1/%2.png" ).arg( previewDir ).arg( entryIter->key );
243 if ( QFile::exists( imagePath ) )
245 QFile::remove( imagePath );
248 *entryIter = incomingEntry;
249 if ( imageNeedsUpdate && ! incomingEntry.imageUrl.isEmpty() )
250 fetchImageForEntry( incomingEntry );
253 storeEntryInSettings( incomingEntry );
257 else if ( !hasExpiry || expiry >= mFetchStartTime )
259 if ( !incomingEntry.imageUrl.isEmpty() )
260 fetchImageForEntry( incomingEntry );
262 mEntries.append( incomingEntry );
263 storeEntryInSettings( incomingEntry );
269 emit
fetched( fetchedEntries );
272void QgsNewsFeedParser::readStoredEntries()
274 QStringList existing;
279 catch ( QgsSettingsException &e )
281 QgsDebugError( QStringLiteral(
"Could not read news feed entries: %1" ).arg( e.
what( ) ) );
284 std::sort( existing.begin(), existing.end(), [](
const QString & a,
const QString & b )
286 return a.toInt() < b.toInt();
288 mEntries.reserve( existing.size() );
289 for (
const QString &entry : existing )
291 const Entry e = readEntryFromSettings( entry.toInt() );
292 if ( !e.expiry.isValid() || e.expiry > QDateTime::currentDateTime() )
293 mEntries.append( e );
297 mBlockSignals =
true;
299 mBlockSignals =
false;
314 if ( !entry.imageUrl.isEmpty() )
317 const QString imagePath = QStringLiteral(
"%1/%2.png" ).arg( previewDir ).arg( entry.key );
318 if ( QFile::exists( imagePath ) )
320 const QImage img( imagePath );
321 entry.image = QPixmap::fromImage( img );
325 fetchImageForEntry( entry );
338 if ( entry.
expiry.isValid() )
345 QgsNetworkContentFetcher *fetcher =
new QgsNetworkContentFetcher();
348 const auto findIter = std::find_if( mEntries.begin(), mEntries.end(), [entry](
const QgsNewsFeedParser::Entry & candidate )
350 return candidate.key == entry.key;
352 if ( findIter != mEntries.end() )
354 const int entryIndex =
static_cast< int >( std::distance( mEntries.begin(), findIter ) );
356 QImage img = QImage::fromData( fetcher->
reply()->readAll() );
358 QSize size = img.size();
360 if ( size.width() > 250 )
362 size.setHeight(
static_cast< int >( size.height() *
static_cast< double >( 250 ) / size.width() ) );
363 size.setWidth( 250 );
366 if ( size.height() > 177 )
368 size.setWidth(
static_cast< int >( size.width() *
static_cast< double >( 177 ) / size.height() ) );
369 size.setHeight( 177 );
373 img = img.scaled( size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
376 QImage previewImage( size, QImage::Format_ARGB32 );
377 previewImage.fill( Qt::transparent );
378 QPainter previewPainter( &previewImage );
379 previewPainter.setRenderHint( QPainter::Antialiasing,
true );
380 previewPainter.setRenderHint( QPainter::SmoothPixmapTransform,
true );
381 previewPainter.setPen( Qt::NoPen );
382 previewPainter.setBrush( Qt::black );
383 previewPainter.drawRoundedRect( 0, 0, size.width(), size.height(), 8, 8 );
384 previewPainter.setCompositionMode( QPainter::CompositionMode_SourceIn );
385 previewPainter.drawImage( 0, 0, img );
386 previewPainter.end();
390 QDir().mkdir( previewDir );
391 const QString imagePath = QStringLiteral(
"%1/%2.png" ).arg( previewDir ).arg( entry.
key );
392 previewImage.save( imagePath );
394 mEntries[ entryIndex ].image = QPixmap::fromImage( previewImage );
397 fetcher->deleteLater();
404 static const QRegularExpression sRegexp( QStringLiteral(
"[^a-zA-Z0-9]" ) );
405 QString res = baseUrl;
406 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())
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 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 been 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
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.
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)