diff --git a/plugins/welcomepage/qml/NewsFeed.qml b/plugins/welcomepage/qml/NewsFeed.qml new file mode 100644 index 000000000..9bdc250d2 --- /dev/null +++ b/plugins/welcomepage/qml/NewsFeed.qml @@ -0,0 +1,208 @@ +/* KDevelop + * + * Copyright 2017 Kevin Funk + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +import QtQuick 2.0 +import QtQuick.Controls 1.3 +import QtQuick.Layouts 1.2 +import QtQuick.XmlListModel 2.0 + +import "storage.js" as Storage + +ListView { + id: root + + /// Update interval (in minutes) in which the news feed is polled + property int updateInterval: 24 * 60 * 1000 // 24 hours + /// Max age (in minutes) of a news entry so it is shown in the list view + /// TODO: Implement me + property int maxNewsAge: 3 * 30 * 24 * 60 // 3 months + /// Max age (in minutes) of a news entry so it is considered 'new' (thus highlighted with a bold font) + property int maxHighlightedNewsAge: 30 * 24 * 60 // a month + + readonly property string feedUrl: "https://www.kdevelop.org/news/feed" + readonly property bool loading: newsFeedSyncModel.status === XmlListModel.Loading + + /// Returns a date parsed from the pubDate + function parsePubDate(pubDate) { + // We need to modify the pubDate read from the RSS feed + // so the JavaScript Date object can interpret it + var d = pubDate.replace(',','').split(' '); + if (d.length != 6) + return new Date(NaN); + + return new Date([d[0], d[2], d[1], d[3], d[4], 'GMT' + d[5]].join(' ')); + } + + // there's no builtin function for this(?) + function toMap(obj) { + var map = {}; + for (var k in obj) { + map[k] = obj[k]; + } + return map; + } + + function minutesSince(date) { + return !isNaN(date) ? Math.floor(Number((new Date() - date)) / 60000) : -1; + } + + function loadFromCache() { + newsFeedOfflineModel.clear() + + var data = Storage.get("newsFeedOfflineModelData", null); + if (data) { + var newsEntries = JSON.parse(data); + for (var i = 0; i < newsEntries.length; ++i) { + newsFeedOfflineModel.append(newsEntries[i]); + } + } + root.positionViewAtBeginning() + } + function saveToCache() { + var newsEntries = []; + for (var i = 0; i < newsFeedSyncModel.count; ++i) { + var entry = newsFeedSyncModel.get(i); + newsEntries.push(toMap(entry)); + } + Storage.set("newsFeedOfflineModelData", JSON.stringify(newsEntries)); + Storage.set("newsFeedLastFetchDate", JSON.stringify(new Date())); + } + + spacing: 10 + + // Note: this model is *not* attached to the the view -- it's merely used for fetching the RSS feed + XmlListModel { + id: newsFeedSyncModel + + property bool active: false + + source: active ? feedUrl : "" + query: "/rss/channel/item" + + XmlRole { name: "title"; query: "title/string()" } + XmlRole { name: "link"; query: "link/string()" } + XmlRole { name: "pubDate"; query: "pubDate/string()" } + + onStatusChanged: { + if (status == XmlListModel.Ready) { + saveToCache(); + loadFromCache(); + } else if (status == XmlListModel.Error) { + console.log("Failed to fetch news feed: " + errorString()); + } + } + } + + model: ListModel { + id: newsFeedOfflineModel + } + + delegate: Column { + id: feedDelegate + + readonly property date publicationDate: parsePubDate(model.pubDate) + readonly property int ageInMinutes: minutesSince(publicationDate) + readonly property bool isNew: ageInMinutes != -1 && ageInMinutes < maxHighlightedNewsAge + readonly property string dateString: isNaN(publicationDate.getDate()) ? model.pubDate : publicationDate.toLocaleDateString() + + x: 10 + width: parent.width - 2*x + + Link { + width: parent.width + + text: model.title + + onClicked: Qt.openUrlExternally(model.link) + } + + Label { + width: parent.width + + font.bold: isNew + font.pointSize: 8 + color: disabledPalette.windowText + + text: isNew ? i18nc("Example: Tue, 03 Jan 2017 10:00:00 (new)", "%1 (new)", dateString) : dateString + } + } + + BusyIndicator { + id: busyIndicator + + height: newsHeading.height + + running: newsFeed.loading + } + + Label { + id: placeHolderLabel + + x: 10 + width: parent.width - 2*x + + text: i18n("No recent news") + color: disabledPalette.windowText + visible: root.count === 0 && !root.loading + + Behavior on opacity { NumberAnimation {} } + } + + SystemPalette { + id: disabledPalette + colorGroup: SystemPalette.Disabled + } + + function fetchFeed() { + console.log("Fetching news feed") + + newsFeedSyncModel.active = true + newsFeedSyncModel.reload() + } + + Timer { + id: delayedStartupTimer + + // delay loading a bit so it has no effect on the KDevelop startup + interval: 3000 + running: true + + onTriggered: { + // only fetch feed if items are out of date + var lastFetchDate = new Date(JSON.parse(Storage.get("newsFeedLastFetchDate", null))); + if (minutesSince(lastFetchDate) > root.updateInterval) { + console.log("Last fetch of news feed was on " + lastFetchDate + ", updating now"); + root.fetchFeed(); + } + } + } + + Timer { + id: reloadFeedTimer + + interval: root.updateInterval + running: true + repeat: true + + onTriggered: root.fetchFeed() + } + + Component.onCompleted: loadFromCache() +} diff --git a/plugins/welcomepage/qml/area_code.qml b/plugins/welcomepage/qml/area_code.qml index 007b4d40c..b1788780b 100644 --- a/plugins/welcomepage/qml/area_code.qml +++ b/plugins/welcomepage/qml/area_code.qml @@ -1,101 +1,125 @@ /* KDevelop * * Copyright 2011 Aleix Pol * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ import QtQuick 2.0 import QtQuick.Controls 1.3 import QtQuick.Layouts 1.2 StandardBackground { id: root state: "develop" tools: ColumnLayout { spacing: 10 Row { Layout.fillWidth: true spacing: 5 Image { id: icon horizontalAlignment: Image.AlignHCenter verticalAlignment: Image.AlignVCenter source: "image://icon/kdevelop" smooth: true fillMode: Image.PreserveAspectFit } Label { verticalAlignment: Text.AlignVCenter height: icon.height text: "KDevelop" font { pointSize: 20 weight: Font.ExtraLight } } } Item { Layout.fillWidth: true Layout.fillHeight: true } + Heading { + id: newsHeading + + Layout.fillWidth: true + text: i18n("News") + } + + NewsFeed { + id: newsFeed + + readonly property int maxEntries: 3 + + Layout.fillWidth: true + Layout.minimumHeight: !loading ? (Math.min(count, maxEntries) * 40) : 40 + + Behavior on Layout.minimumHeight { PropertyAnimation {} } + } + + // add some spacing + Item { + Layout.fillWidth: true + height: 10 + } + Heading { text: i18n("Need Help?") } Column { spacing: 10 Link { x: 10 text: i18n("KDevelop.org") onClicked: { Qt.openUrlExternally("https://kdevelop.org") } } Link { x: 10 text: i18n("Learn about KDevelop") onClicked: Qt.openUrlExternally("https://userbase.kde.org/KDevelop") } Link { x: 10 text: i18n("Join KDevelop's team!") onClicked: Qt.openUrlExternally("https://kdevelop.org/contribute-kdevelop") } Link { x: 10 text: i18n("Handbook") onClicked: kdev.retrieveMenuAction("help/help_contents").trigger() } } } Develop { anchors { fill: parent leftMargin: root.marginLeft+root.margins } } } diff --git a/plugins/welcomepage/qml/storage.js b/plugins/welcomepage/qml/storage.js new file mode 100644 index 000000000..26f33a86a --- /dev/null +++ b/plugins/welcomepage/qml/storage.js @@ -0,0 +1,64 @@ +/* KDevelop + * + * Copyright 2017 Kevin Funk + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +.import QtQuick.LocalStorage 2.0 as Storage + +function getDatabase() { + return Storage.LocalStorage.openDatabaseSync("WelcomePage", "0.1", "HelloAppDatabase", 100); +} + +function createTable(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS settings(setting TEXT UNIQUE, value TEXT)'); +} + +function set(setting, value) { + var db = getDatabase(); + var res = false; + db.transaction(function(tx) { + createTable(tx); + var rs = tx.executeSql('INSERT OR REPLACE INTO settings VALUES (?,?);', [setting,value]); + if (rs.rowsAffected > 0) { + res = true; + } else { + res = false; + } + }); + return res; +} + +function get(setting, defaultValue) { + var db = getDatabase(); + var res = ""; + try { + db.transaction(function(tx) { + createTable(tx); + var rs = tx.executeSql('SELECT value FROM settings WHERE setting=?;', [setting]); + if (rs.rows.length > 0) { + res = rs.rows.item(0).value; + } else { + res = defaultValue; + } + }); + } catch (err) { + console.log("Database error:" + err); + res = defaultValue; + }; + return res +} diff --git a/plugins/welcomepage/welcomepage.qrc b/plugins/welcomepage/welcomepage.qrc index dedd07c07..10ec0c118 100644 --- a/plugins/welcomepage/welcomepage.qrc +++ b/plugins/welcomepage/welcomepage.qrc @@ -1,23 +1,26 @@ qml/plugins/Branches.qml qml/plugins/Projects.qml qml/Develop.qml qml/GettingStarted.qml qml/Link.qml qml/StandardPage.qml + qml/NewsFeed.qml qml/Heading.qml qml/StandardBackground.qml qml/ProjectsDashboard.qml qml/area_code.qml qml/area_debug.qml qml/area_review.qml + qml/storage.js + qml/main.qml