File: NewsFeed.qml

package info (click to toggle)
kdevelop 4%3A5.3.1-3
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 52,544 kB
  • sloc: cpp: 254,897; python: 3,380; sh: 1,271; ansic: 657; xml: 221; php: 95; makefile: 36; lisp: 13; sed: 12
file content (209 lines) | stat: -rw-r--r-- 6,364 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/* KDevelop
 *
 * Copyright 2017 Kevin Funk <kfunk@kde.org>
 *
 * 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 org.kde.kdevplatform 1.0

import "storage.js" as Storage

ListView {
    id: root

    /// Update interval (in seconds) in which the news feed is polled
    property int updateInterval: 24 * 3600 // 24 hours
    /// Max age (in seconds) of a news entry so it is shown in the list view
    /// TODO: Implement me
    property int maxNewsAge: 3 * 30 * 24 * 3600 // 3 months
    /// Max age (in seconds) of a news entry so it is considered 'new' (thus highlighted with a bold font)
    property int maxHighlightedNewsAge: 30 * 24 * 3600 // 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 secondsSince(date) {
        return !isNaN(date) ? Math.floor(Number((new Date() - date)) / 1000) : -1;
    }

    function loadEntriesFromCache() {
        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 saveEntriesToCache() {
        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));
    }

    spacing: 10

    // Note: this model is *not* attached to 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) {
                saveEntriesToCache();
                loadEntriesFromCache();

                Storage.set("newsFeedLastFetchDate", JSON.stringify(new Date()));
            } else if (status == XmlListModel.Error) {
                Logger.log("Failed to fetch news feed: " + errorString());
            }
        }
    }

    ListModel {
        id: newsFeedOfflineModel
    }

    // detach from model while fetching feed to make space for the busy indicator
    model: root.loading ? undefined : newsFeedOfflineModel

    delegate: Column {
        id: feedDelegate

        readonly property date publicationDate: parsePubDate(model.pubDate)
        /// in seconds
        readonly property int age: secondsSince(publicationDate)
        readonly property bool isNew: age != -1 && age < 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
        }
    }

    Label {
        id: placeHolderLabel

        x: 10
        width: parent.width - 2*x

        text: root.loading ?
            i18n("Fetching feeds...") :
            i18n("No recent news")
        color: disabledPalette.windowText
        visible: root.count === 0

        Behavior on opacity { NumberAnimation {} }
    }

    SystemPalette {
        id: disabledPalette
        colorGroup: SystemPalette.Disabled
    }

    function fetchFeed() {
        Logger.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)));
            Logger.log("Last fetch of news feed was on " + lastFetchDate);
            if (secondsSince(lastFetchDate) > root.updateInterval) {
                root.fetchFeed();
            }
        }
    }

    Timer {
        id: reloadFeedTimer

        interval: root.updateInterval * 1000
        running: true
        repeat: true

        onTriggered: root.fetchFeed()
    }

    Component.onCompleted: loadEntriesFromCache()
}