29 #include <QRegularExpression>
33 , mBaseUrl( feedUrl.toString() )
36 , mSettingsKey( keyForFeed( mBaseUrl ) )
41 QUrlQuery query( feedUrl );
48 if ( feedLanguage.isEmpty() )
50 feedLanguage =
QgsSettings().
value( QStringLiteral(
"locale/userLocale" ), QStringLiteral(
"en_US" ) ).toString().left( 2 );
52 if ( !feedLanguage.isEmpty() && feedLanguage != QLatin1String(
"C" ) )
53 query.addQueryItem( QStringLiteral(
"lang" ), feedLanguage );
59 if ( latOk && longOk )
62 if ( feedUrl.isLocalFile() )
64 query.addQueryItem( QStringLiteral(
"lat" ), QString::number(
static_cast< int >( feedLat ) ) );
65 query.addQueryItem( QStringLiteral(
"lon" ), QString::number(
static_cast< int >( feedLong ) ) );
75 if ( feedUrl.isLocalFile() )
77 if ( !query.toString().isEmpty() )
78 mFeedUrl = QUrl( mFeedUrl.toString() +
'_' + query.toString() );
82 mFeedUrl.setQuery( query );
94 const int beforeSize = mEntries.size();
95 mEntries.erase( std::remove_if( mEntries.begin(), mEntries.end(),
96 [key, &dismissed](
const Entry & entry )
98 if ( entry.key == key )
104 } ), mEntries.end() );
105 if ( beforeSize == mEntries.size() )
111 if ( !dismissed.imageUrl.isEmpty() )
114 const QString imagePath = QStringLiteral(
"%1/%2.png" ).arg( previewDir ).arg( key );
115 if ( QFile::exists( imagePath ) )
117 QFile::remove( imagePath );
121 if ( !mBlockSignals )
122 emit entryDismissed( dismissed );
127 const QList< QgsNewsFeedParser::Entry >
entries = mEntries;
141 QNetworkRequest req( mFeedUrl );
144 mFetchStartTime = QDateTime::currentDateTimeUtc().toSecsSinceEpoch();
151 QNetworkReply *reply = task->
reply();
158 if ( reply->error() != QNetworkReply::NoError )
160 QgsMessageLog::logMessage( tr(
"News feed request failed [error: %1]" ).arg( reply->errorString() ) );
165 QMetaObject::invokeMethod(
this,
"onFetch", Qt::QueuedConnection, Q_ARG( QString, task->
contentAsString() ) );
171 void QgsNewsFeedParser::onFetch(
const QString &content )
177 const QVariantList
entries = json.toList();
178 QList< QgsNewsFeedParser::Entry > newEntries;
179 newEntries.reserve(
entries.size() );
180 for (
const QVariant &e :
entries )
183 const QVariantMap entryMap = e.toMap();
184 newEntry.key = entryMap.value( QStringLiteral(
"pk" ) ).toInt();
185 newEntry.title = entryMap.value( QStringLiteral(
"title" ) ).toString();
186 newEntry.imageUrl = entryMap.value( QStringLiteral(
"image" ) ).toString();
187 newEntry.content = entryMap.value( QStringLiteral(
"content" ) ).toString();
188 newEntry.link = entryMap.value( QStringLiteral(
"url" ) ).toString();
189 newEntry.sticky = entryMap.value( QStringLiteral(
"sticky" ) ).toBool();
191 const uint expiry = entryMap.value( QStringLiteral(
"publish_to" ) ).toUInt( &ok );
193 newEntry.expiry.setSecsSinceEpoch( expiry );
194 newEntries.append( newEntry );
196 if ( !newEntry.imageUrl.isEmpty() )
197 fetchImageForEntry( newEntry );
199 mEntries.append( newEntry );
200 storeEntryInSettings( newEntry );
207 void QgsNewsFeedParser::readStoredEntries()
213 std::sort( existing.begin(), existing.end(), [](
const QString & a,
const QString & b )
215 return a.toInt() < b.toInt();
217 mEntries.reserve( existing.size() );
218 for (
const QString &entry : existing )
220 const Entry e = readEntryFromSettings( entry.toInt() );
221 if ( !e.expiry.isValid() || e.expiry > QDateTime::currentDateTime() )
222 mEntries.append( e );
226 mBlockSignals =
true;
228 mBlockSignals =
false;
235 const QString baseSettingsKey = QStringLiteral(
"%1/%2" ).arg( mSettingsKey ).arg( key );
240 entry.title = settings.
value( QStringLiteral(
"title" ) ).toString();
241 entry.imageUrl = settings.
value( QStringLiteral(
"imageUrl" ) ).toString();
242 entry.content = settings.
value( QStringLiteral(
"content" ) ).toString();
243 entry.link = settings.
value( QStringLiteral(
"link" ) ).toString();
244 entry.sticky = settings.
value( QStringLiteral(
"sticky" ) ).toBool();
245 entry.expiry = settings.
value( QStringLiteral(
"expiry" ) ).toDateTime();
246 if ( !entry.imageUrl.isEmpty() )
249 const QString imagePath = QStringLiteral(
"%1/%2.png" ).arg( previewDir ).arg( entry.key );
250 if ( QFile::exists( imagePath ) )
252 const QImage img( imagePath );
253 entry.image = QPixmap::fromImage( img );
257 fetchImageForEntry( entry );
265 const QString baseSettingsKey = QStringLiteral(
"%1/%2" ).arg( mSettingsKey ).arg( entry.
key );
272 if ( entry.
expiry.isValid() )
282 auto findIter = std::find_if( mEntries.begin(), mEntries.end(), [entry](
const QgsNewsFeedParser::Entry & candidate )
284 return candidate.key == entry.key;
286 if ( findIter != mEntries.end() )
288 const int entryIndex =
static_cast< int >( std::distance( mEntries.begin(), findIter ) );
290 QImage img = QImage::fromData( fetcher->
reply()->readAll() );
292 QSize size = img.size();
294 if ( size.width() > 250 )
296 size.setHeight(
static_cast< int >( size.height() *
static_cast< double >( 250 ) / size.width() ) );
297 size.setWidth( 250 );
300 if ( size.height() > 177 )
302 size.setWidth(
static_cast< int >( size.width() *
static_cast< double >( 177 ) / size.height() ) );
303 size.setHeight( 177 );
307 img = img.scaled( size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
310 QImage previewImage( size, QImage::Format_ARGB32 );
311 previewImage.fill( Qt::transparent );
312 QPainter previewPainter( &previewImage );
313 previewPainter.setRenderHint( QPainter::Antialiasing,
true );
314 previewPainter.setRenderHint( QPainter::SmoothPixmapTransform,
true );
315 previewPainter.setPen( Qt::NoPen );
316 previewPainter.setBrush( Qt::black );
317 previewPainter.drawRoundedRect( 0, 0, size.width(), size.height(), 8, 8 );
318 previewPainter.setCompositionMode( QPainter::CompositionMode_SourceIn );
319 previewPainter.drawImage( 0, 0, img );
320 previewPainter.end();
324 QDir().mkdir( previewDir );
325 const QString imagePath = QStringLiteral(
"%1/%2.png" ).arg( previewDir ).arg( entry.
key );
326 previewImage.save( imagePath );
328 mEntries[ entryIndex ].image = QPixmap::fromImage( previewImage );
331 fetcher->deleteLater();
338 static QRegularExpression sRegexp( QStringLiteral(
"[^a-zA-Z0-9]" ) );
339 QString res = baseUrl;
340 res = res.replace( sRegexp, QString() );
341 return QStringLiteral(
"NewsFeed/%1" ).arg( res );
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.
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.
void dismissEntry(int key)
Dismisses an entry with matching key.
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.
QString authcfg() const
Returns the authentication configuration for the parser.
QgsNewsFeedParser(const QUrl &feedUrl, const QString &authcfg=QString(), QObject *parent=nullptr)
Constructor for QgsNewsFeedParser, parsing the specified feedUrl.
void dismissAll()
Dismisses all current news items.
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.
This class is a composition of two QSettings instances:
QStringList childGroups() const
Returns a list of all key top-level groups that contain keys that can be read using the QSettings obj...
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
void beginGroup(const QString &prefix, QgsSettings::Section section=QgsSettings::NoSection)
Appends prefix to the current group.
void remove(const QString &key, QgsSettings::Section section=QgsSettings::NoSection)
Removes the setting key and any sub-settings of key in a section.
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
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.
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)