aboutsummaryrefslogtreecommitdiff
path: root/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader
diff options
context:
space:
mode:
authorWeblate <noreply@weblate.org>2017-01-04 21:47:56 +0000
committerWeblate <noreply@weblate.org>2017-01-04 21:47:56 +0000
commit5840eb59b26d1dab6f43b2e2f9e4ca6ce8b70e0f (patch)
tree931c2fe0927dcf25cac4031374db4b0c6dcdc727 /src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader
parentTranslated using Weblate (lumina_SEARCH@fr (generated)) (diff)
parentMerge branch 'master' of github.com:trueos/lumina (diff)
downloadlumina-5840eb59b26d1dab6f43b2e2f9e4ca6ce8b70e0f.tar.gz
lumina-5840eb59b26d1dab6f43b2e2f9e4ca6ce8b70e0f.tar.bz2
lumina-5840eb59b26d1dab6f43b2e2f9e4ca6ce8b70e0f.zip
Merge remote-tracking branch 'origin/master'
Diffstat (limited to 'src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader')
-rw-r--r--src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSFeedPlugin.cpp363
-rw-r--r--src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSFeedPlugin.h72
-rw-r--r--src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSFeedPlugin.ui552
-rw-r--r--src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSObjects.cpp287
-rw-r--r--src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSObjects.h105
5 files changed, 1379 insertions, 0 deletions
diff --git a/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSFeedPlugin.cpp b/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSFeedPlugin.cpp
new file mode 100644
index 00000000..c330d6c0
--- /dev/null
+++ b/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSFeedPlugin.cpp
@@ -0,0 +1,363 @@
+//===========================================
+// Lumina-DE source code
+// Copyright (c) 2016, Ken Moore
+// Available under the 3-clause BSD license
+// See the LICENSE file for full details
+//===========================================
+#include "RSSFeedPlugin.h"
+#include "ui_RSSFeedPlugin.h"
+
+#include <LuminaXDG.h>
+#include "LSession.h"
+#include <LUtils.h>
+#include <QDir>
+#include <QFileDialog>
+#include <QInputDialog>
+#include <QtConcurrent>
+
+RSSFeedPlugin::RSSFeedPlugin(QWidget* parent, QString ID) : LDPlugin(parent, ID), ui(new Ui::RSSFeedPlugin()){
+ ui->setupUi(this);
+ //Load the global settings
+ setprefix = "rssreader/"; //this structure/prefix should be used for *all* plugins of this type
+ RSS = new RSSReader(this, setprefix);
+ ui->text_feed->setContextMenuPolicy(Qt::NoContextMenu);
+ //Create the options menu
+ optionsMenu = new QMenu(this);
+ ui->tool_options->setMenu(optionsMenu);
+ presetMenu = new QMenu(this);
+ ui->tool_add_preset->setMenu(presetMenu);
+
+ //Setup any signal/slot connections
+ connect(ui->push_back1, SIGNAL(clicked()), this, SLOT(backToFeeds()) );
+ connect(ui->push_back2, SIGNAL(clicked()), this, SLOT(backToFeeds()) );
+ connect(ui->push_back3, SIGNAL(clicked()), this, SLOT(backToFeeds()) );
+ connect(ui->push_save_settings, SIGNAL(clicked()), this, SLOT(saveSettings()) );
+ connect(RSS, SIGNAL(rssChanged(QString)), this, SLOT(RSSItemChanged(QString)) );
+ connect(RSS, SIGNAL(newChannelsAvailable()), this, SLOT(UpdateFeedList()));
+ connect(ui->tool_gotosite, SIGNAL(clicked()), this, SLOT(openFeedPage()) );
+ connect(ui->push_rm_feed, SIGNAL(clicked()), this, SLOT(removeFeed()) );
+ connect(ui->push_add_url, SIGNAL(clicked()), this, SLOT(addNewFeed()) );
+ connect(ui->combo_feed, SIGNAL(currentIndexChanged(int)), this, SLOT(currentFeedChanged()) );
+
+ connect(presetMenu, SIGNAL(triggered(QAction*)), this, SLOT(loadPreset(QAction*)) );
+
+ updateOptionsMenu();
+ QTimer::singleShot(0,this, SLOT(ThemeChange()) );
+ //qDebug() << " - Done with init";
+ QStringList feeds;
+ if( !LSession::handle()->DesktopPluginSettings()->contains(setprefix+"currentfeeds") ){
+ //First-time run of the plugin - automatically load the default feeds
+ feeds = LOS::RSSFeeds();
+ for(int i=0; i<feeds.length(); i++){ feeds[i] = feeds[i].section("::::",1,-1); } //just need url right now
+ feeds << "http://lumina-desktop.org/?feed=rss2"; //Lumina Desktop blog feed
+ LSession::handle()->DesktopPluginSettings()->setValue(setprefix+"currentfeeds", feeds);
+ }else{
+ feeds = LSession::handle()->DesktopPluginSettings()->value(setprefix+"currentfeeds",QStringList()).toStringList();
+ }
+ RSS->addUrls(feeds);
+ backToFeeds(); //always load the first page
+}
+
+RSSFeedPlugin::~RSSFeedPlugin(){
+
+}
+
+//================
+// PRIVATE
+//================
+void RSSFeedPlugin::updateOptionsMenu(){
+ optionsMenu->clear();
+ optionsMenu->addAction(LXDG::findIcon("list-add",""), tr("Add RSS Feed"), this, SLOT(openFeedNew()) );
+ optionsMenu->addAction(LXDG::findIcon("help-about",""), tr("View Feed Details"), this, SLOT(openFeedInfo()) );
+ optionsMenu->addAction(LXDG::findIcon("configure",""), tr("Settings"), this, SLOT(openSettings()) );
+ optionsMenu->addSeparator();
+ optionsMenu->addAction(LXDG::findIcon("download",""), tr("Update Feeds Now"), this, SLOT(resyncFeeds()) );
+
+ presetMenu->clear();
+ QStringList feeds = LOS::RSSFeeds();
+ feeds << tr("Lumina Desktop RSS")+"::::http://lumina-desktop.org/?feed=rss2";
+ feeds.sort();
+ for(int i=0; i<feeds.length(); i++){
+ QAction *tmp = presetMenu->addAction(feeds[i].section("::::",0,0) );
+ tmp->setWhatsThis( feeds[i].section("::::",1,-1) );
+ }
+}
+
+void RSSFeedPlugin::checkFeedNotify(){
+ bool notify = false;
+ for(int i=0; i<ui->combo_feed->count() && !notify; i++){
+ if( !ui->combo_feed->itemData(i, Qt::WhatsThisRole).toString().isEmpty()){ notify = true; }
+ }
+ QString style;
+ if(notify){ style = "QComboBox{ background-color: rgba(255,0,0,120); }"; }
+ ui->combo_feed->setStyleSheet(style);
+}
+
+//Simplification functions for loading feed info onto widgets
+void RSSFeedPlugin::updateFeed(QString ID){
+ //Now clear/update the feed viewer (HTML)
+ ui->text_feed->clear();
+ if(ID.isEmpty()){ return; } //nothing to show
+
+ //Save the datetime this feed was read
+ LSession::handle()->DesktopPluginSettings()->setValue(setprefix+"feedReads/"+ID, QDateTime::currentDateTime() );
+ //Get the color to use for hyperlinks (need to specify in html)
+ QString color = ui->text_feed->palette().text().color().name(); //keep the hyperlinks the same color as the main text (different formatting still applies)
+ QString html;
+ RSSchannel data = RSS->dataForID(ID);
+ ui->label_lastupdate->setText( data.lastsync.toString(Qt::DefaultLocaleShortDate) );
+ // - generate the html
+ // html.append("<ul style=\"margin-left: 3px;\">\n");
+ for(int i=0; i<data.items.length(); i++){
+ //html.append("<li>");
+ html.append("<h4><a href=\""+data.items[i].link+"\" style=\"color: "+color+";\">"+data.items[i].title+"</a></h4>");
+ if(!data.items[i].pubdate.isNull() || !data.items[i].author.isEmpty()){
+ html.append("<i>(");
+ if(!data.items[i].pubdate.isNull()){ html.append(data.items[i].pubdate.toString(Qt::DefaultLocaleShortDate)); }
+ if(!data.items[i].author.isEmpty()){
+ if(!html.endsWith("(")){ html.append(", "); } //spacing between date/author
+ if(!data.items[i].author_email.isEmpty()){ html.append("<a href=\"mailto:"+data.items[i].author_email+"\" style=\"color: "+color+";\">"+data.items[i].author+"</a>"); }
+ else{ html.append(data.items[i].author); }
+ }
+ html.append(")</i><br>");
+ }
+ html.append(data.items[i].description);
+ //html.append("</li>\n");
+ if(i+1 < data.items.length()){ html.append("<br>"); }
+ }
+ //html.append("</ul>\n");
+ // - load that html into the viewer
+ ui->text_feed->setHtml(html);
+}
+
+void RSSFeedPlugin::updateFeedInfo(QString ID){
+ ui->page_feed_info->setWhatsThis(ID);
+ ui->text_feed_info->clear();
+ if(ID.isEmpty()){ return; }
+ //Get the color to use for hyperlinks (need to specify in html)
+ QString color = ui->text_feed->palette().text().color().name(); //keep the hyperlinks the same color as the main text (different formatting still applies)
+ QString html;
+ RSSchannel data = RSS->dataForID(ID);
+ // - generate the html
+ // <a href=\""+LINK+"\" style=\"color: "+color+";\">"+TEXT+"</a>
+ html.append( QString(tr("Feed URL: %1")).arg("<a href=\""+data.originalURL+"\" style=\"color: "+color+";\">"+data.originalURL+"</a>") +"<br><hr>");
+ html.append( QString(tr("Title: %1")).arg(data.title) +"<br>");
+ html.append( QString(tr("Description: %1")).arg(data.description) +"<br>");
+ html.append( QString(tr("Website: %1")).arg("<a href=\""+data.link+"\" style=\"color: "+color+";\">"+data.link+"</a>") +"<br><hr>");
+ if(!data.lastBuildDate.isNull()){ html.append( QString(tr("Last Build Date: %1")).arg(data.lastBuildDate.toString(Qt::DefaultLocaleShortDate)) +"<br>"); }
+ html.append( QString(tr("Last Sync: %1")).arg(data.lastsync.toString(Qt::DefaultLocaleShortDate)) +"<br>");
+ html.append( QString(tr("Next Sync: %1")).arg(data.nextsync.toString(Qt::DefaultLocaleShortDate)) +"<br>");
+ // - load that html into the viewer
+ ui->text_feed_info->setHtml(html);
+}
+
+//================
+// PRIVATE SLOTS
+//================
+void RSSFeedPlugin::loadIcons(){
+ ui->tool_options->setIcon( LXDG::findIcon("configure","") );
+ ui->tool_gotosite->setIcon( LXDG::findIcon("applications-internet","") );
+ ui->push_back1->setIcon( LXDG::findIcon("go-previous","") );
+ ui->push_back2->setIcon( LXDG::findIcon("go-previous","") );
+ ui->push_back3->setIcon( LXDG::findIcon("go-previous","") );
+ ui->push_rm_feed->setIcon( LXDG::findIcon("list-remove","") );
+ ui->push_add_url->setIcon( LXDG::findIcon("list-add","") );
+ ui->push_save_settings->setIcon( LXDG::findIcon("document-save","") );
+ ui->tool_add_preset->setIcon( LXDG::findIcon("bookmark-new-list","") );
+}
+
+//GUI slots
+// - Page management
+void RSSFeedPlugin::backToFeeds(){
+ ui->stackedWidget->setCurrentWidget(ui->page_feed);
+}
+
+void RSSFeedPlugin::openFeedInfo(){
+ QString ID = ui->combo_feed->currentData().toString();
+ if(ID.isEmpty()){ return; }
+ updateFeedInfo(ID);
+ ui->stackedWidget->setCurrentWidget(ui->page_feed_info);
+
+}
+
+void RSSFeedPlugin::openFeedNew(){
+ ui->line_new_url->setText("");
+ ui->stackedWidget->setCurrentWidget(ui->page_new_feed);
+}
+
+void RSSFeedPlugin::openSettings(){
+ //Sync the widget with the current settings
+ QSettings *set = LSession::handle()->DesktopPluginSettings();
+
+ ui->check_manual_sync->setChecked( set->value(setprefix+"manual_sync_only", false).toBool() );
+ int DI = set->value(setprefix+"default_interval_minutes", 60).toInt();
+ if(DI<1){ DI = 60; }
+ if( (DI%60) == 0 ){DI = DI/60; ui->combo_sync_units->setCurrentIndex(1); } //hourly setting
+ else{ ui->combo_sync_units->setCurrentIndex(1); } //minutes setting
+ ui->spin_synctime->setValue(DI);
+
+ //Now show the page
+ ui->stackedWidget->setCurrentWidget(ui->page_settings);
+}
+
+// - Feed Management
+void RSSFeedPlugin::addNewFeed(){
+ if(ui->line_new_url->text().isEmpty()){ return; } //nothing to add
+ //Validate the URL
+ QUrl url(ui->line_new_url->text());
+ if(!url.isValid()){
+ ui->line_new_url->setFocus();
+ return;
+ }
+ //Add the URL to the settings file for next login
+ QStringList feeds = LSession::handle()->DesktopPluginSettings()->value(setprefix+"currentfeeds",QStringList()).toStringList();
+ feeds << url.toString();
+ LSession::handle()->DesktopPluginSettings()->setValue(setprefix+"currentfeeds", feeds);
+
+ //Set this URL as the current selection
+ ui->combo_feed->setWhatsThis(url.toString()); //hidden field - will trigger an update in a moment
+ //Add the URL to the backend
+ RSS->addUrls(QStringList() << url.toString());
+ //UpdateFeedList(); //now re-load the feeds which are available
+
+ //Now go back to the main page
+ backToFeeds();
+}
+
+void RSSFeedPlugin::loadPreset(QAction *act){
+ ui->line_new_url->setText( act->whatsThis() );
+}
+
+void RSSFeedPlugin::removeFeed(){
+ QString ID = ui->page_feed_info->whatsThis();
+ if(ID.isEmpty()){ return; }
+ //Remove from the RSS feed object
+ RSSchannel info = RSS->dataForID(ID);
+ RSS->removeUrl(ID);
+ //Remove the URL from the settings file for next login
+ QStringList feeds = LSession::handle()->DesktopPluginSettings()->value(setprefix+"currentfeeds",QStringList()).toStringList();
+ feeds.removeAll(info.originalURL);
+ LSession::handle()->DesktopPluginSettings()->setValue(setprefix+"currentfeeds", feeds);
+ LSession::handle()->DesktopPluginSettings()->remove(setprefix+"feedReads/"+ID);
+ //Now go back to the main page
+ backToFeeds();
+}
+
+void RSSFeedPlugin::resyncFeeds(){
+ RSS->addUrls( LSession::handle()->DesktopPluginSettings()->value(setprefix+"currentfeeds",QStringList()).toStringList() );
+ RSS->syncNow();
+}
+
+// - Feed Interactions
+void RSSFeedPlugin::currentFeedChanged(){
+ QString ID = ui->combo_feed->currentData().toString();
+ //Remove the "unread" color/flag from the feed
+ ui->combo_feed->setItemData( ui->combo_feed->currentIndex(), QBrush(Qt::transparent) , Qt::BackgroundRole);
+ ui->combo_feed->setItemData( ui->combo_feed->currentIndex(), "", Qt::WhatsThisRole);
+ checkFeedNotify();
+ updateFeed(ID);
+}
+
+void RSSFeedPlugin::openFeedPage(){ //Open main website for feed
+ QString ID = ui->combo_feed->currentData().toString();
+ //Find the data associated with this feed
+ RSSchannel data = RSS->dataForID(ID);
+ QString url = data.link;
+ //qDebug() << "Open Feed Page:" << url;
+ //Now launch the browser
+ if(!url.isEmpty()){
+ LSession::LaunchApplication("lumina-open \""+url+"\"");
+ }
+}
+
+void RSSFeedPlugin::saveSettings(){
+ QSettings *set = LSession::handle()->DesktopPluginSettings();
+ set->setValue(setprefix+"manual_sync_only", ui->check_manual_sync->isChecked() );
+ int DI = ui->spin_synctime->value();
+ if(ui->combo_sync_units->currentIndex()==1){ DI = DI*60; } //convert from hours to minutes
+ set->setValue(setprefix+"default_interval_minutes", DI);
+ set->sync();
+
+ //Now go back to the feeds
+ backToFeeds();
+}
+
+//Feed Object interactions
+void RSSFeedPlugin::UpdateFeedList(){
+
+ QString activate = ui->combo_feed->whatsThis();
+ if(!activate.isEmpty()){ ui->combo_feed->setWhatsThis(""); }
+ if(activate.isEmpty()){ activate = ui->combo_feed->currentData().toString(); } // keep current item selected
+ //Now get/list all the available feeds
+ QStringList IDS = RSS->channels(); //this is pre-sorted by title of the feed
+ //qDebug() << "Update RSS Feed List:" << IDS << activate;
+ for(int i=0; i<IDS.length(); i++){
+ bool newitem = false;
+ if(ui->combo_feed->count()<=i){ newitem = true; }
+ else{
+ QString cid = ui->combo_feed->itemData(i).toString();
+ if(IDS[i]!=cid){
+ if(IDS.contains(cid)){ newitem = true; } //this item is just out of order
+ else{ ui->combo_feed->removeItem(i); } //item no longer is valid
+ }
+ }
+ if(newitem){
+ //Need to add a new item at this point in the menu
+ RSSchannel info = RSS->dataForID(IDS[i]);
+ if(info.title.isEmpty()){
+ //invalid/empty channel
+ ui->combo_feed->insertItem(i, IDS[i], IDS[i]); //just show the URL
+ }else{
+ ui->combo_feed->insertItem(i, info.icon, info.title, IDS[i]);
+ }
+ }
+ }
+ //Remove any extra items on the end of the list
+ for(int i=IDS.length(); i<ui->combo_feed->count(); i++){
+ ui->combo_feed->removeItem(i); i--;
+ }
+ //Now activate the proper item as needed
+ if(IDS.contains(activate)){
+ ui->combo_feed->setCurrentIndex( IDS.indexOf(activate) );
+ }
+ checkFeedNotify();
+}
+
+void RSSFeedPlugin::RSSItemChanged(QString ID){
+ for(int i=0; i<ui->combo_feed->count(); i++){
+ if(ui->combo_feed->itemData(i).toString()!=ID){ continue; }
+ RSSchannel info = RSS->dataForID(ID);
+ if(info.title.isEmpty()){
+ ui->combo_feed->setItemText(i, ID);
+ ui->combo_feed->setItemIcon(i, LXDG::findIcon("dialog-cancel","") );
+ }else{
+ ui->combo_feed->setItemText(i, info.title);
+ ui->combo_feed->setItemIcon(i, info.icon );
+ QColor color(Qt::transparent);
+ if( info.lastBuildDate > LSession::handle()->DesktopPluginSettings()->value(setprefix+"feedReads/"+ID,QDateTime()).toDateTime() ){
+ color = QColor(255,10,10,100); //semi-transparent red
+ ui->combo_feed->setItemData(i, "notify", Qt::WhatsThisRole);
+ }else{
+ ui->combo_feed->setItemData(i, "", Qt::WhatsThisRole);
+ }
+ ui->combo_feed->setItemData(i, QBrush(color) , Qt::BackgroundRole);
+ }
+ }
+ if(ID == ui->combo_feed->currentData().toString()){
+ currentFeedChanged(); //re-load the current feed
+ }else{
+ checkFeedNotify();
+ }
+}
+
+//==================
+// PUBLIC SLOTS
+//==================
+void RSSFeedPlugin::LocaleChange(){
+ ui->retranslateUi(this);
+ updateOptionsMenu();
+}
+void RSSFeedPlugin::ThemeChange(){
+ QTimer::singleShot(0,this, SLOT(loadIcons()));
+ updateOptionsMenu();
+}
diff --git a/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSFeedPlugin.h b/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSFeedPlugin.h
new file mode 100644
index 00000000..68b36760
--- /dev/null
+++ b/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSFeedPlugin.h
@@ -0,0 +1,72 @@
+//===========================================
+// Lumina-DE source code
+// Copyright (c) 2016, Ken Moore
+// Available under the 3-clause BSD license
+// See the LICENSE file for full details
+//===========================================
+// This plugin is a simple RSS feed reader for the desktop
+//===========================================
+#ifndef _LUMINA_DESKTOP_RSS_FEEDER_PLUGIN_H
+#define _LUMINA_DESKTOP_RSS_FEEDER_PLUGIN_H
+
+#include <QTimer>
+#include "../LDPlugin.h"
+
+#include "RSSObjects.h"
+
+namespace Ui{
+ class RSSFeedPlugin;
+};
+
+class RSSFeedPlugin : public LDPlugin{
+ Q_OBJECT
+public:
+ RSSFeedPlugin(QWidget* parent, QString ID);
+ ~RSSFeedPlugin();
+
+ virtual QSize defaultPluginSize(){
+ // The returned QSize is in grid points (typically 100 or 200 pixels square)
+ return QSize(3,3);
+ }
+private:
+ Ui::RSSFeedPlugin *ui;
+ QMenu *optionsMenu, *presetMenu;
+ QString setprefix; //settings prefix
+ RSSReader *RSS;
+
+ void updateOptionsMenu();
+ void checkFeedNotify(); //check if unread feeds are available and change the styling a bit as needed
+
+ //Simplification functions for loading feed info onto widgets
+ void updateFeed(QString ID);
+ void updateFeedInfo(QString ID);
+
+private slots:
+ void loadIcons();
+
+ //GUI slots
+ // - Page management
+ void backToFeeds();
+ void openFeedInfo();
+ void openFeedNew();
+ void openSettings();
+ // - Feed Management
+ void addNewFeed(); // the "add" button (current url in widget on page)
+ void loadPreset(QAction*); //the add-preset menu
+ void removeFeed(); // the "remove" button (current feed for page)
+ void resyncFeeds();
+ // - Feed Interactions
+ void currentFeedChanged();
+ void openFeedPage(); //Open the website in a browser
+ void saveSettings();
+
+ //Feed Object interactions
+ void UpdateFeedList();
+ void RSSItemChanged(QString ID);
+
+public slots:
+ void LocaleChange();
+ void ThemeChange();
+
+};
+#endif
diff --git a/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSFeedPlugin.ui b/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSFeedPlugin.ui
new file mode 100644
index 00000000..dc7acd0b
--- /dev/null
+++ b/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSFeedPlugin.ui
@@ -0,0 +1,552 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>RSSFeedPlugin</class>
+ <widget class="QWidget" name="RSSFeedPlugin">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>238</width>
+ <height>278</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="leftMargin">
+ <number>3</number>
+ </property>
+ <property name="topMargin">
+ <number>3</number>
+ </property>
+ <property name="rightMargin">
+ <number>3</number>
+ </property>
+ <property name="bottomMargin">
+ <number>3</number>
+ </property>
+ <item>
+ <widget class="QStackedWidget" name="stackedWidget">
+ <property name="currentIndex">
+ <number>1</number>
+ </property>
+ <widget class="QWidget" name="page_feed">
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="spacing">
+ <number>3</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QComboBox" name="combo_feed"/>
+ </item>
+ <item>
+ <widget class="QToolButton" name="tool_options">
+ <property name="toolTip">
+ <string>View Options</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="popupMode">
+ <enum>QToolButton::InstantPopup</enum>
+ </property>
+ <property name="autoRaise">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLabel" name="label_lastupdate">
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="tool_gotosite">
+ <property name="toolTip">
+ <string>Open Website</string>
+ </property>
+ <property name="text">
+ <string>More</string>
+ </property>
+ <property name="autoRaise">
+ <bool>true</bool>
+ </property>
+ <property name="arrowType">
+ <enum>Qt::NoArrow</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTextBrowser" name="text_feed">
+ <property name="undoRedoEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="html">
+ <string notr="true">&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="openLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="page_feed_info">
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <property name="spacing">
+ <number>4</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>5</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QPushButton" name="push_back1">
+ <property name="text">
+ <string>Back to Feeds</string>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="title">
+ <string>Feed Information</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTextBrowser" name="text_feed_info">
+ <property name="html">
+ <string notr="true">&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <item>
+ <widget class="QPushButton" name="push_rm_feed">
+ <property name="text">
+ <string>Remove Feed</string>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="page_new_feed">
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <property name="spacing">
+ <number>4</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <item>
+ <widget class="QPushButton" name="push_back2">
+ <property name="text">
+ <string>Back to Feeds</string>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="title">
+ <string>New Feed Subscription</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <property name="spacing">
+ <number>2</number>
+ </property>
+ <property name="leftMargin">
+ <number>2</number>
+ </property>
+ <property name="topMargin">
+ <number>2</number>
+ </property>
+ <property name="rightMargin">
+ <number>2</number>
+ </property>
+ <property name="bottomMargin">
+ <number>2</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>RSS URL</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_10">
+ <item>
+ <widget class="QLineEdit" name="line_new_url"/>
+ </item>
+ <item>
+ <widget class="QToolButton" name="tool_add_preset">
+ <property name="toolTip">
+ <string>Load a preset RSS Feed</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="popupMode">
+ <enum>QToolButton::InstantPopup</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="push_add_url">
+ <property name="text">
+ <string>Add to Feeds</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="page_settings">
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <property name="spacing">
+ <number>4</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_6">
+ <item>
+ <widget class="QPushButton" name="push_back3">
+ <property name="text">
+ <string>Back to Feeds</string>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_3">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="title">
+ <string>Feed Reader Settings</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <property name="spacing">
+ <number>2</number>
+ </property>
+ <property name="leftMargin">
+ <number>2</number>
+ </property>
+ <property name="topMargin">
+ <number>2</number>
+ </property>
+ <property name="rightMargin">
+ <number>2</number>
+ </property>
+ <property name="bottomMargin">
+ <number>2</number>
+ </property>
+ <item>
+ <widget class="QCheckBox" name="check_manual_sync">
+ <property name="text">
+ <string>Manual Sync Only</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_4">
+ <property name="toolTip">
+ <string>Some RSS feeds may request custom update intervals instead of using this setting</string>
+ </property>
+ <property name="title">
+ <string>Default Sync Interval</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_7">
+ <property name="spacing">
+ <number>2</number>
+ </property>
+ <property name="leftMargin">
+ <number>2</number>
+ </property>
+ <property name="topMargin">
+ <number>2</number>
+ </property>
+ <property name="rightMargin">
+ <number>2</number>
+ </property>
+ <property name="bottomMargin">
+ <number>2</number>
+ </property>
+ <item>
+ <widget class="QSpinBox" name="spin_synctime">
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>60</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="combo_sync_units">
+ <property name="currentText">
+ <string>Hour(s)</string>
+ </property>
+ <property name="currentIndex">
+ <number>1</number>
+ </property>
+ <item>
+ <property name="text">
+ <string>Minutes</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Hour(s)</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9">
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="push_save_settings">
+ <property name="text">
+ <string>Save Settings</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSObjects.cpp b/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSObjects.cpp
new file mode 100644
index 00000000..0a805252
--- /dev/null
+++ b/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSObjects.cpp
@@ -0,0 +1,287 @@
+//===========================================
+// Lumina-DE source code
+// Copyright (c) 2016, Ken Moore
+// Available under the 3-clause BSD license
+// See the LICENSE file for full details
+//===========================================
+#include "RSSObjects.h"
+#include <QNetworkRequest>
+#include <QXmlStreamReader>
+
+#include "LSession.h"
+
+//============
+// PUBLIC
+//============
+RSSReader::RSSReader(QObject *parent, QString settingsPrefix) : QObject(parent){
+ NMAN = new QNetworkAccessManager(this);
+ connect(NMAN, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*)) );
+ connect(NMAN, SIGNAL(sslErrors(QNetworkReply*, const QList<QSslError>&)), this, SLOT(sslErrors(QNetworkReply*, const QList<QSslError>&)) );
+
+ setprefix = settingsPrefix;
+ syncTimer = new QTimer(this);
+ syncTimer->setInterval(300000); //5 minutes
+ connect(syncTimer, SIGNAL(timeout()), this, SLOT(checkTimes()));
+ syncTimer->start();
+}
+
+RSSReader::~RSSReader(){
+
+}
+
+//Information retrieval
+QStringList RSSReader::channels(){
+ QStringList urls = hash.keys();
+ QStringList ids;
+ //sort all the channels by title before output
+ for(int i=0; i<urls.length(); i++){
+ QString title = hash[urls[i]].title;
+ if(title.isEmpty()){ title = "ZZZ"; } //put currently-invalid ones at the end of the list
+ ids << title+" : "+hash[urls[i]].originalURL;
+ }
+ ids.sort();
+ //Now strip off all the titles again to just get the IDs
+ for(int i=0; i<ids.length(); i++){
+ ids[i] = ids[i].section(" : ",-1);
+ }
+ return ids;
+}
+
+RSSchannel RSSReader::dataForID(QString ID){
+ QString key = keyForUrl(ID);
+ if(hash.contains(key)){ return hash[key]; }
+ else{ return RSSchannel(); }
+}
+
+//Initial setup
+void RSSReader::addUrls(QStringList urls){
+ //qDebug() << "Add URLS:" << urls;
+ for(int i=0; i<urls.length(); i++){
+ //Note: Make sure we get the complete URL form for accurate comparison later
+ QString url = QUrl(urls[i]).toString();
+ QString key = keyForUrl(url);
+ if(hash.contains(key)){ continue; } //already handled
+ RSSchannel blank;
+ blank.originalURL = url;
+ hash.insert(url, blank); //put the empty struct into the hash for now
+ requestRSS(url); //startup the initial request for this url
+ }
+ emit newChannelsAvailable();
+}
+
+void RSSReader::removeUrl(QString ID){
+ QString key = keyForUrl(ID);
+ if(hash.contains(key)){ hash.remove(key); }
+ emit newChannelsAvailable();
+}
+
+//=================
+// PUBLIC SLOTS
+//=================
+void RSSReader::syncNow(){
+ QStringList urls = hash.keys();
+ for(int i=0; i<urls.length(); i++){
+ requestRSS(hash[urls[i]].originalURL);
+ }
+}
+
+//=================
+// PRIVATE
+//=================
+QString RSSReader::keyForUrl(QString url){
+ //get current hash key for this URL
+ QStringList keys = hash.keys();
+ if(keys.contains(url)){ return url; } //this is already a valid key
+ for(int i=0; i<keys.length(); i++){
+ if(hash[keys[i]].originalURL == url){ return keys[i]; } //this was an original URL
+ }
+ return "";
+}
+
+void RSSReader::requestRSS(QString url){
+ if(!outstandingURLS.contains(url)){
+ //qDebug() << "Request URL:" << url;
+ NMAN->get( QNetworkRequest( QUrl(url) ) );
+ outstandingURLS << url;
+ }
+}
+
+//RSS parsing functions
+RSSchannel RSSReader::readRSS(QByteArray bytes){
+ //Note: We could expand this later to support multiple "channel"s per Feed
+ // but it seems like there is normally only one channel anyway
+ //qDebug() << "Read RSS:" << bytes.left(100);
+ QXmlStreamReader xml(bytes);
+ RSSchannel rssinfo;
+ //qDebug() << "Can Read XML Stream:" << !xml.hasError();
+ if(xml.readNextStartElement()){
+ //qDebug() << " - RSS Element:" << xml.name();
+ if(xml.name() == "rss" && (xml.attributes().value("version") =="2.0" || xml.attributes().value("version") =="0.91") ){
+ while(xml.readNextStartElement()){
+ //qDebug() << " - RSS Element:" << xml.name();
+ if(xml.name()=="channel"){ rssinfo = readRSSChannel(&xml); }
+ else{ xml.skipCurrentElement(); }
+ }
+ }
+ }
+ if(xml.hasError()){ qDebug() << " - XML Read Error:" << xml.errorString() << "\n" << bytes; }
+ return rssinfo;
+}
+RSSchannel RSSReader::readRSSChannel(QXmlStreamReader *rss){
+ RSSchannel info;
+ info.timetolive = -1;
+ while(rss->readNextStartElement()){
+ //qDebug() << " - RSS Element (channel):" <<rss->name();
+ if(rss->name()=="item"){ info.items << readRSSItem(rss); }
+ else if(rss->name()=="title"){ info.title = rss->readElementText(); }
+ else if(rss->name()=="link"){
+ QString txt = rss->readElementText();
+ if(!txt.isEmpty()){ info.link = txt; }
+ }
+ else if(rss->name()=="description"){ info.description = rss->readElementText(); }
+ else if(rss->name()=="lastBuildDate"){ info.lastBuildDate = RSSDateTime(rss->readElementText()); }
+ else if(rss->name()=="pubDate"){ info.lastPubDate = RSSDateTime(rss->readElementText()); }
+ else if(rss->name()=="image"){ readRSSImage(&info, rss); }
+ //else if(rss->name()=="skipHours"){ info.link = rss->readElementText(); }
+ //else if(rss->name()=="skipDays"){ info.link = rss->readElementText(); }
+ else if(rss->name()=="ttl"){ info.timetolive = rss->readElementText().toInt(); }
+ else{ rss->skipCurrentElement(); }
+ }
+ return info;
+}
+
+RSSitem RSSReader::readRSSItem(QXmlStreamReader *rss){
+ RSSitem item;
+ while(rss->readNextStartElement()){
+ //qDebug() << " - RSS Element (Item):" << rss->name();
+ if(rss->name()=="title"){ item.title = rss->readElementText(); }
+ else if(rss->name()=="link"){ item.link = rss->readElementText(); }
+ else if(rss->name()=="description"){ item.description = rss->readElementText(); }
+ else if(rss->name()=="comments"){ item.comments_url = rss->readElementText(); }
+ else if(rss->name()=="author"){
+ //Special handling - this field can contain both email and name
+ QString raw = rss->readElementText();
+ if(raw.contains("@")){
+ item.author_email = raw.split(" ").filter("@").first();
+ item.author = raw.remove(item.author_email).remove("(").remove(")").simplified(); //the name is often put within parentheses after the email
+ }else{ item.author = raw; }
+ }
+ else if(rss->name()=="guid"){ item.guid = rss->readElementText(); }
+ else if(rss->name()=="pubDate"){ item.pubdate = RSSDateTime(rss->readElementText()); }
+ else{ rss->skipCurrentElement(); }
+ }
+ return item;
+}
+
+void RSSReader::readRSSImage(RSSchannel *item, QXmlStreamReader *rss){
+ while(rss->readNextStartElement()){
+ //qDebug() << " - RSS Element (Image):" << rss->name();
+ if(rss->name()=="url"){ item->icon_url = rss->readElementText(); }
+ else if(rss->name()=="title"){ item->icon_title = rss->readElementText(); }
+ else if(rss->name()=="link"){ item->icon_link = rss->readElementText(); }
+ else if(rss->name()=="width"){ item->icon_size.setWidth(rss->readElementText().toInt()); }
+ else if(rss->name()=="height"){ item->icon_size.setHeight(rss->readElementText().toInt()); }
+ else if(rss->name()=="description"){ item->icon_description = rss->readElementText(); }
+ }
+ //Go ahead and kick off the request for the icon
+ if(!item->icon_url.isEmpty()){ requestRSS(item->icon_url); }
+}
+
+QDateTime RSSReader::RSSDateTime(QString datetime){
+ return QDateTime::fromString(datetime, Qt::RFC2822Date);
+}
+
+//=================
+// PRIVATE SLOTS
+//=================
+void RSSReader::replyFinished(QNetworkReply *reply){
+ QString url = reply->request().url().toString();
+ //qDebug() << "Got Reply:" << url;
+ QString key = keyForUrl(url); //current hash key for this URL
+ QByteArray data = reply->readAll();
+ outstandingURLS.removeAll(url);
+ if(data.isEmpty()){
+ //qDebug() << "No data returned:" << url;
+ //see if the URL can be adjusted for known issues
+ bool handled = false;
+ QUrl redirecturl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
+ if(redirecturl.isValid() && (redirecturl.toString() != url )){
+ //New URL redirect - make the change and send a new request
+ QString newurl = redirecturl.toString();
+ //qDebug() << " - Redirect to:" << newurl;
+ if(hash.contains(key) && !hash.contains(newurl)){
+ hash.insert(newurl, hash.take(key) ); //just move the data over to the new url
+ requestRSS(newurl);
+ emit newChannelsAvailable();
+ handled = true;
+ }
+ }
+ if(!handled && hash.contains(key) ){
+ emit rssChanged(hash[key].originalURL);
+ }
+ return;
+ }
+
+ if(!hash.contains(key)){
+ //qDebug() << " - hash does not contain URL:" << url;
+ //URL removed from list while a request was outstanding?
+ //Could also be an icon fetch response
+ QStringList keys = hash.keys();
+ for(int i=0; i<keys.length(); i++){
+ //qDebug() << " - Check for icon URL:" << hash[keys[i]].icon_url;
+ if(hash[keys[i]].icon_url.toLower() == url.toLower()){ //needs to be case-insensitive
+ //Icon fetch response
+ RSSchannel info = hash[keys[i]];
+ QImage img = QImage::fromData(data);
+ info.icon = QIcon( QPixmap::fromImage(img) );
+ //qDebug() << "Got Icon response:" << url << info.icon;
+ hash.insert(keys[i], info); //insert back into the hash
+ emit rssChanged( hash[keys[i]].originalURL );
+ break;
+ }
+ }
+ reply->deleteLater();
+ }else{
+ //RSS reply
+ RSSchannel info = readRSS(data); //QNetworkReply can be used as QIODevice
+ reply->deleteLater(); //clean up
+ //Validate the info and announce any changes
+ if(info.title.isEmpty() || info.link.isEmpty() || info.description.isEmpty()){
+ qDebug() << "Missing XML Information:" << url << info.title << info.link << info.description;
+ return;
+ } //bad info/read
+ //Update the bookkeeping elements of the info
+ if(info.timetolive<=0){ info.timetolive = LSession::handle()->DesktopPluginSettings()->value(setprefix+"default_interval_minutes", 60).toInt(); }
+ if(info.timetolive <=0){ info.timetolive = 60; } //error in integer conversion from settings?
+ info.lastsync = QDateTime::currentDateTime(); info.nextsync = info.lastsync.addSecs(info.timetolive * 60);
+ //Now see if anything changed and save the info into the hash
+ bool changed = (hash[key].lastBuildDate.isNull() || (hash[key].lastBuildDate < info.lastBuildDate) );
+ bool newinfo = false;
+ if(changed){ newinfo = hash[key].title.isEmpty(); } //no previous info from this URL
+ info.originalURL = hash[key].originalURL; //make sure this info gets preserved across updates
+ if(!hash[key].icon.isNull()){ info.icon = hash[key].icon; } //copy over the icon from the previous reply
+ hash.insert(key, info);
+ if(newinfo){ emit newChannelsAvailable(); } //new channel
+ else if(changed){ emit rssChanged(info.originalURL); } //update to existing channel
+ }
+}
+
+void RSSReader::sslErrors(QNetworkReply *reply, const QList<QSslError> &errors){
+ int ok = 0;
+ qDebug() << "SSL Errors Detected (RSS Reader):" << reply->url();
+ for(int i=0; i<errors.length(); i++){
+ if(errors[i].error()==QSslError::SelfSignedCertificate || errors[i].error()==QSslError::SelfSignedCertificateInChain){ ok++; }
+ else{ qDebug() << "Unhandled SSL Error:" << errors[i].errorString(); }
+ }
+ if(ok==errors.length()){ qDebug() << " - Permitted:" << reply->url(); reply->ignoreSslErrors(); }
+ else{ qDebug() << " - Denied:" << reply->url(); }
+}
+
+void RSSReader::checkTimes(){
+ if(LSession::handle()->DesktopPluginSettings()->value(setprefix+"manual_sync_only", false).toBool()){ return; }
+ QStringList urls = hash.keys();
+ QDateTime cdt = QDateTime::currentDateTime();
+ for(int i=0; i<urls.length(); i++){
+ if(hash[urls[i]].nextsync < cdt){ requestRSS(urls[i]); }
+ }
+}
diff --git a/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSObjects.h b/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSObjects.h
new file mode 100644
index 00000000..3069bf8d
--- /dev/null
+++ b/src-qt5/core/lumina-desktop-unified/src-DE/desktop-plugins/rssreader/RSSObjects.h
@@ -0,0 +1,105 @@
+//===========================================
+// Lumina-DE source code
+// Copyright (c) 2016, Ken Moore
+// Available under the 3-clause BSD license
+// See the LICENSE file for full details
+//===========================================
+#ifndef _LUMINA_DESKTOP_RSS_READER_PLUGIN_OBJECT_H
+#define _LUMINA_DESKTOP_RSS_READER_PLUGIN_OBJECT_H
+
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QString>
+#include <QDateTime>
+#include <QList>
+#include <QIcon>
+#include <QTimer>
+#include <QXmlStreamReader> //Contained in the Qt "core" module - don't need the full "xml" module for this
+#include <QSslError>
+
+struct RSSitem{
+ //Required Fields
+ QString title, link, description;
+
+ //Optional Fields
+ QString comments_url, author_email, author, guid;
+ QDateTime pubdate; //when the item was published
+ //IGNORED INFO from RSS2 spec: "category", "source", "enclosure"
+};
+
+struct RSSchannel{
+ //Required fields
+ QString title, link, description;
+
+ //Optional Fields
+ QDateTime lastBuildDate, lastPubDate; //last build/publish dates
+ // - channel refresh information
+ int timetolive; //in minutes - time until next sync should be performed
+ //QList<int> skiphours;
+ //QStringList skipdays;
+ // - icon info
+ QIcon icon;
+ QString icon_url, icon_title, icon_link, icon_description;
+ QSize icon_size;
+ //All items within this channel
+ QList<RSSitem> items;
+
+ //Optional RSS2 elements ignored:
+ // "cloud", "textInput", "rating", "language", "copyright", "managingEditor", "webMaster",
+ // "category", "generator", "docs"
+
+ //Internal data for bookkeeping
+ QDateTime lastsync, nextsync;
+ QString originalURL; //in case it was redirected to some "fixed" url later
+};
+
+class RSSReader : public QObject{
+ Q_OBJECT
+public:
+ RSSReader(QObject *parent, QString settingsPrefix);
+ ~RSSReader();
+
+ //Information retrieval
+ QStringList channels(); //returns all ID's
+ RSSchannel dataForID(QString ID);
+
+ //Initial setup
+ void addUrls(QStringList urls);
+ void removeUrl(QString ID);
+
+public slots:
+ void syncNow(); //not generally needed
+
+private:
+ //Internal data objects
+ QHash<QString, RSSchannel> hash; // ID/data
+ QString setprefix;
+ QTimer *syncTimer;
+ QNetworkAccessManager *NMAN;
+ QStringList outstandingURLS;
+
+
+ //Simple hash data search functions
+ QString keyForUrl(QString url);
+
+ //Network request function
+ void requestRSS(QString url);
+
+ //RSS parsing functions
+ RSSchannel readRSS(QByteArray bytes);
+ RSSchannel readRSSChannel(QXmlStreamReader *rss);
+ RSSitem readRSSItem(QXmlStreamReader *rss);
+ void readRSSImage(RSSchannel *item, QXmlStreamReader *rss);
+ QDateTime RSSDateTime(QString datetime);
+
+private slots:
+ void replyFinished(QNetworkReply *reply);
+ void sslErrors(QNetworkReply *reply, const QList<QSslError> &errors);
+ void checkTimes();
+
+signals:
+ void rssChanged(QString); //ID
+ void newChannelsAvailable();
+};
+
+#endif
bgstack15