Warning, /kdevelop/kdevelop/plugins/welcomepage/qml/NewsFeed.qml is written in an unsupported language. File is not indexed.

0001 /*
0002     SPDX-FileCopyrightText: 2017 Kevin Funk <kfunk@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 import QtQuick 2.7
0008 import QtQuick.Controls 2.0
0009 import QtQuick.Layouts 1.2
0010 import QtQuick.XmlListModel 2.0
0011 
0012 import org.kde.kdevplatform 1.0
0013 
0014 import "storage.js" as Storage
0015 
0016 ListView {
0017     id: root
0018 
0019     /// Update interval (in seconds) in which the news feed is polled
0020     property int updateInterval: 24 * 3600 // 24 hours
0021     /// Max age (in seconds) of a news entry so it is shown in the list view
0022     /// TODO: Implement me
0023     property int maxNewsAge: 3 * 30 * 24 * 3600 // 3 months
0024     /// Max age (in seconds) of a news entry so it is considered 'new' (thus highlighted with a bold font)
0025     property int maxHighlightedNewsAge: 30 * 24 * 3600 // a month
0026 
0027     readonly property string feedUrl: "https://www.kdevelop.org/news/feed"
0028     readonly property bool loading: newsFeedSyncModel.status === XmlListModel.Loading
0029 
0030     /// Returns a date parsed from the pubDate
0031     function parsePubDate(pubDate) {
0032         // We need to modify the pubDate read from the RSS feed
0033         // so the JavaScript Date object can interpret it
0034         var d = pubDate.replace(',','').split(' ');
0035         if (d.length != 6)
0036             return new Date(NaN);
0037 
0038         return new Date([d[0], d[2], d[1], d[3], d[4], 'GMT' + d[5]].join(' '));
0039     }
0040 
0041     // there's no builtin function for this(?)
0042     function toMap(obj) {
0043         var map = {};
0044         for (var k in obj) {
0045             map[k] = obj[k];
0046         }
0047         return map;
0048     }
0049 
0050     function secondsSince(date) {
0051         return !isNaN(date) ? Math.floor(Number((new Date() - date)) / 1000) : -1;
0052     }
0053 
0054     function loadEntriesFromCache() {
0055         newsFeedOfflineModel.clear()
0056 
0057         var data = Storage.get("newsFeedOfflineModelData", null);
0058         if (data) {
0059             var newsEntries = JSON.parse(data);
0060             for (var i = 0; i < newsEntries.length; ++i) {
0061                 newsFeedOfflineModel.append(newsEntries[i]);
0062             }
0063         }
0064         root.positionViewAtBeginning()
0065     }
0066     function saveEntriesToCache() {
0067         var newsEntries = [];
0068         for (var i = 0; i < newsFeedSyncModel.count; ++i) {
0069             var entry = newsFeedSyncModel.get(i);
0070             newsEntries.push(toMap(entry));
0071         }
0072         Storage.set("newsFeedOfflineModelData", JSON.stringify(newsEntries));
0073     }
0074 
0075     spacing: 10
0076 
0077     // Note: this model is *not* attached to the view -- it's merely used for fetching the RSS feed
0078     XmlListModel {
0079         id: newsFeedSyncModel
0080 
0081         property bool active: false
0082 
0083         source: active ? feedUrl : ""
0084         query: "/rss/channel/item"
0085 
0086         XmlRole { name: "title"; query: "title/string()" }
0087         XmlRole { name: "link"; query: "link/string()" }
0088         XmlRole { name: "pubDate"; query: "pubDate/string()" }
0089 
0090         onStatusChanged: {
0091             if (status == XmlListModel.Ready) {
0092                 saveEntriesToCache();
0093                 loadEntriesFromCache();
0094 
0095                 Storage.set("newsFeedLastFetchDate", JSON.stringify(new Date()));
0096             } else if (status == XmlListModel.Error) {
0097                 Logger.log("Failed to fetch news feed: " + errorString());
0098             }
0099         }
0100     }
0101 
0102     ListModel {
0103         id: newsFeedOfflineModel
0104     }
0105 
0106     // detach from model while fetching feed to make space for the busy indicator
0107     model: root.loading ? undefined : newsFeedOfflineModel
0108 
0109     delegate: Column {
0110         id: feedDelegate
0111 
0112         readonly property date publicationDate: parsePubDate(model.pubDate)
0113         /// in seconds
0114         readonly property int age: secondsSince(publicationDate)
0115         readonly property bool isNew: age != -1 && age < maxHighlightedNewsAge
0116         readonly property string dateString: isNaN(publicationDate.getDate()) ? model.pubDate : publicationDate.toLocaleDateString()
0117 
0118         x: 10
0119         width: parent.width - 2*x
0120 
0121         Link {
0122             width: parent.width
0123 
0124             text: model.title
0125 
0126             onClicked: Qt.openUrlExternally(model.link)
0127         }
0128 
0129         Label {
0130             width: parent.width
0131 
0132             font.bold: isNew
0133             font.pointSize: 8
0134             color: disabledPalette.windowText
0135 
0136             text: isNew ? i18nc("Example: Tue, 03 Jan 2017 10:00:00 (new)", "%1 (new)", dateString) : dateString
0137         }
0138     }
0139 
0140     Label {
0141         id: placeHolderLabel
0142 
0143         x: 10
0144         width: parent.width - 2*x
0145 
0146         text: root.loading ?
0147             i18n("Fetching feeds...") :
0148             i18n("No recent news")
0149         color: disabledPalette.windowText
0150         visible: root.count === 0
0151 
0152         Behavior on opacity { NumberAnimation {} }
0153     }
0154 
0155     SystemPalette {
0156         id: disabledPalette
0157         colorGroup: SystemPalette.Disabled
0158     }
0159 
0160     function fetchFeed() {
0161         Logger.log("Fetching news feed")
0162 
0163         newsFeedSyncModel.active = true
0164         newsFeedSyncModel.reload()
0165     }
0166 
0167     Timer {
0168         id: delayedStartupTimer
0169 
0170         // delay loading a bit so it has no effect on the KDevelop startup
0171         interval: 3000
0172         running: true
0173 
0174         onTriggered: {
0175             // only fetch feed if items are out of date
0176             var lastFetchDate = new Date(JSON.parse(Storage.get("newsFeedLastFetchDate", null)));
0177             Logger.log("Last fetch of news feed was on " + lastFetchDate);
0178             if (secondsSince(lastFetchDate) > root.updateInterval) {
0179                 root.fetchFeed();
0180             }
0181         }
0182     }
0183 
0184     Timer {
0185         id: reloadFeedTimer
0186 
0187         interval: root.updateInterval * 1000
0188         running: true
0189         repeat: true
0190 
0191         onTriggered: root.fetchFeed()
0192     }
0193 
0194     Component.onCompleted: loadEntriesFromCache()
0195 }