Major changes
authorTofe <chris.chapuis+gitorious@gmail.com>
Sun, 4 Mar 2012 22:33:40 +0000 (23:33 +0100)
committerTofe <chris.chapuis+gitorious@gmail.com>
Sun, 4 Mar 2012 22:33:40 +0000 (23:33 +0100)
 - Configuration dialog
 - Google Reader (source model + javascript adapted from NewsFlow)
 - bumped version to 0.4.0

24 files changed:
qml/QuickNewsReader/content/images/borderStripes.png [new file with mode: 0644]
qml/QuickNewsReader/content/images/lineedit.png [new file with mode: 0644]
qml/QuickNewsReader/content/images/stripes.png [new file with mode: 0644]
qml/QuickNewsReader/content/js/GoogleReaderAPI.js [new file with mode: 0644]
qml/QuickNewsReader/content/modelimpl/FavoriteFeedsSourceModel.qml
qml/QuickNewsReader/content/modelimpl/GoogleReaderCategories.qml [new file with mode: 0644]
qml/QuickNewsReader/content/modelimpl/GoogleReaderNews.qml [new file with mode: 0644]
qml/QuickNewsReader/content/modelimpl/GoogleReaderSourceModel.qml [new file with mode: 0644]
qml/QuickNewsReader/content/modelimpl/LeMondeSourceModel.qml
qml/QuickNewsReader/content/modelimpl/YahooSourceModel.qml
qml/QuickNewsReader/content/view/Background.qml [new file with mode: 0644]
qml/QuickNewsReader/content/view/Categories.qml
qml/QuickNewsReader/content/view/CategoryDelegate.qml
qml/QuickNewsReader/content/view/GoogleReaderConfig.qml [new file with mode: 0644]
qml/QuickNewsReader/content/view/LineInput.qml [new file with mode: 0644]
qml/QuickNewsReader/content/view/News.qml
qml/QuickNewsReader/content/view/NewsComments.qml
qml/QuickNewsReader/content/view/NewsDelegate.qml
qml/QuickNewsReader/content/view/NewsDetail.qml
qml/QuickNewsReader/content/view/SourceConfigDialog.qml
qml/QuickNewsReader/content/view/SourceDelegate.qml
qml/QuickNewsReader/content/view/Sources.qml
qml/QuickNewsReader/main.qml
qtc_packaging/debian_fremantle/changelog

diff --git a/qml/QuickNewsReader/content/images/borderStripes.png b/qml/QuickNewsReader/content/images/borderStripes.png
new file mode 100644 (file)
index 0000000..eeb6c09
Binary files /dev/null and b/qml/QuickNewsReader/content/images/borderStripes.png differ
diff --git a/qml/QuickNewsReader/content/images/lineedit.png b/qml/QuickNewsReader/content/images/lineedit.png
new file mode 100644 (file)
index 0000000..2cc38dc
Binary files /dev/null and b/qml/QuickNewsReader/content/images/lineedit.png differ
diff --git a/qml/QuickNewsReader/content/images/stripes.png b/qml/QuickNewsReader/content/images/stripes.png
new file mode 100644 (file)
index 0000000..9f36727
Binary files /dev/null and b/qml/QuickNewsReader/content/images/stripes.png differ
diff --git a/qml/QuickNewsReader/content/js/GoogleReaderAPI.js b/qml/QuickNewsReader/content/js/GoogleReaderAPI.js
new file mode 100644 (file)
index 0000000..c1fc132
--- /dev/null
@@ -0,0 +1,530 @@
+/*
+    Copyright 2011 - Tommi Laukkanen (www.substanceofcode.com)
+
+    This file is part of NewsFlow.
+
+    NewsFlow is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    NewsFlow 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 Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with NewsFlow. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+var sid = "";
+var sidToken = "";
+
+// UI components
+//var waiting;
+//var done;
+//var model;
+//var tagsModel;
+//var logo;
+//var error;
+//var navigation;
+
+var continuation = "";
+var actionID = "";
+var actionFeedUrl = "";
+var accessToken = "";
+var action = "";
+var tags = "";
+
+var itemsURL = "";
+
+function doWebRequest(method, url, params, callback) {
+    var doc = new XMLHttpRequest();
+    //console.log(method + " " + url);
+
+    doc.onreadystatechange = function() {
+        if (doc.readyState == XMLHttpRequest.HEADERS_RECEIVED) {
+            var status = doc.status;
+            if(status!=200) {
+                showError("Google API returned " + status + " " + doc.statusText);
+            }
+        } else if (doc.readyState == XMLHttpRequest.DONE) {
+            var data;
+            var contentType = doc.getResponseHeader("Content-Type");
+            data = doc.responseText;
+            callback(data);
+        }
+    }
+
+    doc.open(method, url);
+    if(sid.length>0) {
+        //console.log("Authorization GoogleLogin auth=" + sid);
+        doc.setRequestHeader("Authorization", "GoogleLogin auth=" + sid);
+        doc.setRequestHeader("Cookie", "SID=" + sidToken);
+    }
+    if(params.length>0) {
+        //console.log("Sending: " + params);
+        doc.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+        doc.setRequestHeader("Content-Length", String(params.length));
+        doc.send(params);
+    } else {
+        doc.send();
+    }
+}
+
+/** Parse parameter from given URL */
+function parseAuth(data, parameterName) {
+    var parameterIndex = data.indexOf(parameterName + "=");
+    if(parameterIndex<0) {
+        // We didn't find parameter
+        console.log("Didn't find Auth");
+        addError("Didn't find Auth");
+        return "";
+    }
+    var equalIndex = data.indexOf("=", parameterIndex);
+    if(equalIndex<0) {
+        return "";
+    }
+
+    var lineBreakIndex = data.indexOf("\n", equalIndex+1)
+
+    var value = "";
+    value = data.substring(equalIndex+1, lineBreakIndex);
+    return value;
+}
+
+function addError(msg) {
+    console.log(msg)
+    /*
+    model.append({
+                 "title": "Error",
+                 "desc": msg,
+                 "author": "",
+                 "published": "",
+                 "more": true,
+                 "source": ""})
+                 */
+}
+
+function login(email, password) {
+    try {
+        // waiting.state = "shown";
+        var url = "https://www.google.com/accounts/ClientLogin?Email=" + encodeURIComponent(email) + "&Passwd=" + encodeURIComponent(password) + "&service=reader";
+        doWebRequest("GET", url, "", parseToken);
+    }catch(err) {
+        showError("Error while logging in.");
+    }
+}
+
+function showError(msg) {
+    console.log("ERROR: " + msg)
+//    waiting.state = "hidden";
+//    error.reason = msg;
+//    error.state = "shown";
+}
+
+function removeLinks(original) {
+    var txt = original;
+    txt = txt.replace(/<a /g, "<span ");
+    txt = txt.replace(/<\/a>/g, "</span>");
+    return txt;
+}
+
+function parseToken(data) {
+    sid = parseAuth(data, "Auth");
+    //console.log("Auth=" + sid);
+    sidToken = parseAuth(data, "SID");
+    //console.log("SID=" + sidToken);
+    // logo.state = "hidden"; //.visible = false;
+    if(sid.length>0) {
+        //navigation.state = "menu";
+        //waiting.state = "hidden";
+
+        WorkerScript.sendMessage({"sid": sid, "sidToken": sidToken});
+
+        //loadUnreadNews();
+    } else {
+        addError("Couldn't parse SID");
+        //waiting.state = "hidden";
+    }
+}
+
+function loadSubscriptions() {
+    //waiting.state = "shown";
+    var url = "http://www.google.com/reader/api/0/subscription/list?output=json";
+    doWebRequest("GET", url, "", parseSubscriptions);
+}
+
+function parseSubscriptions(data) {
+    //console.log("Subscriptions: " + data);
+
+    var tags = eval("[" + data + "]")[0];
+    for(var i in tags.subscriptions) {
+        var tag = tags.subscriptions[i];
+        var id = tag.id;
+        var title = tag.title;
+
+        WorkerScript.sendMessage({"title": title, "published": '',"tag": tag, "id":id});
+    }
+    //navigation.state = "tags";
+    //waiting.state = "hidden";
+}
+
+function loadTags() {
+    //waiting.state = "shown";
+    var url = "http://www.google.com/reader/api/0/tag/list?output=json";
+    doWebRequest("GET", url, "", parseTags);
+}
+
+function parseTags(data) {
+    var tags = eval("[" + data + "]")[0];
+    for(var i in tags.tags) {
+        var tag = tags.tags[i];
+        var id = tag.id;
+        var title = id;
+        while(title.indexOf("/")>0) {
+            var index = title.indexOf("/");
+            title = title.substring(index+1);
+        }
+
+        WorkerScript.sendMessage({"title": title, "published": '', "tag": tag, "id":id});
+    }
+    //navigation.state = "tags";
+    //waiting.state = "hidden";
+}
+
+function loadAllNews() {
+    itemsURL = "http://www.google.com/reader/api/0/stream/contents/user/-/state/com.google/reading-list";
+    loadNews();
+}
+
+function loadUnreadNews() {
+    itemsURL = "http://www.google.com/reader/api/0/stream/contents/user/-/state/com.google/reading-list?xt=user/-/state/com.google/read";
+    loadNews();
+}
+
+function loadStarred() {
+    itemsURL = "http://www.google.com/reader/api/0/stream/contents/user/-/state/com.google/starred";
+    loadNews();
+}
+
+function loadTaggedItems(tag) {
+    itemsURL = "http://www.google.com/reader/api/0/stream/contents/" + tag;
+    loadNews();
+}
+
+function loadSubscriptionItems(subscription) {
+    itemsURL = "http://www.google.com/reader/api/0/stream/contents/" + subscription;
+    loadNews();
+}
+
+function loadNews() {
+    try {
+        //waiting.state = "shown";
+        doWebRequest("GET", itemsURL, "", parseNews);
+    } catch(err) {
+        showError("Error while loading news: " + err);
+    }
+}
+
+function getNodeValue(node, name) {
+    var nodeValue = "";
+    for(var i=0; i<node.childNodes.length; i++) {
+        var nodeName = node.childNodes[i].nodeName;
+        if(nodeName==name) {
+            nodeValue = node.childNodes[i].firstChild.nodeValue;
+        }
+    }
+    return nodeValue;
+}
+
+function parseEntry(item) {
+    var content = ""
+    if(typeof(item.content)!=undefined && item.content!=null) {
+        content = item.content.content;
+    } else if(typeof(item.summary)!=undefined && item.summary!=null) {
+        content = item.summary.content;
+    } else {
+        content = "";
+    }
+    content = removeLinks(content);
+    var milliseconds = parseInt(parseInt(item.published)*1000);
+    var published = new Date(parseInt(milliseconds));
+    var link = item.alternate[0].href;
+    //console.log("Link: " + link);
+    var isRead = true;
+    for(var i in item.categories) {
+        var category = item.categories[i];
+        if(category.indexOf("/reading-list")>0) {
+            isRead = false;
+        }
+    }
+
+
+    WorkerScript.sendMessage({
+                                 "id": item.id,
+                                 "title": item.title,
+                                 "description": content,
+                                 "author": item.origin.title,
+                                 "published": prettyDate(published),
+                                 "more": false,
+                                 "source": item.origin.title,
+                                 "link": link,
+                                 "feedUrl": item.origin.streamId,
+                                 "isRead": isRead
+                    });
+}
+
+function parseNews(data) {
+    //try {
+        //console.log("DATA: " + data);
+        var doc = eval("[" + data + "]")[0];
+        if(doc==null || typeof(doc)==undefined) {
+            WorkerScript.sendMessage({
+                         "title": "Error",
+                         "description": "",
+                         "author": "",
+                         "published": "",
+                         "more": true,
+                         "isRead": false,
+                         "source": ""});
+            //waiting.state = "hidden";
+            return;
+        }
+
+        //var moreIndex = model.count - 1;
+
+        continuation = doc.continuation;
+        for(var i in doc.items) {
+            var item = doc.items[i];
+            parseEntry(item);
+        }
+/*
+        if(moreIndex>0) {
+            if(model.get(moreIndex).title.indexOf("Loading...")>-1) {
+                model.remove(moreIndex);
+            }
+        }
+
+        model.append({
+                     "title": "Load more...<br/><br/>",
+                     "description": "",
+                     "author": "",
+                     "published": "",
+                     "isRead": false,
+                     "more": true,
+                     "source": ""});
+                     */
+    //}catch(err) {
+    //    addError("Error: " + err);
+    //}
+   // navigation.state = "items";
+   // waiting.state = "hidden";
+}
+
+function loadMore() {
+    //waiting.state = "shown";
+    var url = itemsURL;
+    if(itemsURL.indexOf("?")>0) {
+        url += "&";
+    } else {
+        url += "?";
+    }
+    url += "c=" + continuation;
+    doWebRequest("GET", url, "", parseNews);
+}
+
+function getToken() {
+    var url = "http://www.google.com/reader/api/0/token";
+    doWebRequest("GET", url, "", parseAccessToken, null);
+}
+
+function parseAccessToken(data) {
+    accessToken = data;
+    if(action=="read") {
+        var url = "http://www.google.com/reader/api/0/edit-tag";
+        var dd = "ac=edit-tags&a=user/-/state/com.google/read&i=" + encodeURIComponent(actionID) + "&s=" + encodeURIComponent(actionFeedUrl) + "&T=" + accessToken;
+        doWebRequest("POST", url, dd, showDone, null);
+    } else if(action=="unread") {
+        var url = "http://www.google.com/reader/api/0/edit-tag";
+        var dd = "ac=edit-tags&a=user/-/state/com.google/kept-unread&r=user/-/state/com.google/read&i=" + encodeURIComponent(actionID) + "&s=" + encodeURIComponent(actionFeedUrl) + "&T=" + accessToken;
+        doWebRequest("POST", url, dd, showDone, null);
+    } else if(action=="fav") {
+        var url = "http://www.google.com/reader/api/0/edit-tag?client=-";
+        var dd = "a=user/-/state/com.google/starred&i=" + encodeURIComponent(actionID) + "&s=" + encodeURIComponent(actionFeedUrl) + "&T=" + accessToken;
+        doWebRequest("POST", url, dd, showDone, null);
+    } else if(action=="tags") {
+        var url = "http://www.google.com/reader/api/0/edit-tag?client=-";
+        var dd =
+            "a=user/-/label/" + encodeURIComponent(tags) +
+            "&i=" + encodeURIComponent(actionID) +
+            "&s=" + encodeURIComponent(actionFeedUrl) +
+            "&T=" + accessToken;
+        doWebRequest("POST", url, dd, showDone, null);
+    }
+}
+
+function markAsRead(id, feedUrl, showLoadingIndicator) {
+    actionID = id;
+    actionFeedUrl = feedUrl;
+    if(showLoadingIndicator) {
+        waiting.state = "shown";
+    }
+    action = "read";
+    getToken();
+}
+
+function markAsUnread(id, feedUrl) {
+    actionID = id;
+    actionFeedUrl = feedUrl;
+    //waiting.state = "shown";
+    action = "unread";
+    //done.status = "Marked as unread";
+    getToken();
+}
+
+function addTags(id, feedUrl, newTags) {
+    actionID = id;
+    actionFeedUrl = feedUrl;
+    //waiting.state = "shown";
+    action = "tags";
+    //done.status = "Added tags";
+    tags = newTags;
+    getToken();
+}
+
+function markAsFavourite(id, feedUrl) {
+    actionID = id;
+    actionFeedUrl = feedUrl;
+    //waiting.state = "shown";
+    action = "fav";
+    //done.status = "Marked as favourite";
+    getToken();
+}
+
+function showDone(data) {
+    console.log("DONE: " + data);
+    //if(waiting.state!="shown") {
+    //    return;
+    //}
+    //waiting.state = "hidden";
+    //done.status = "";
+    if(typeof(data)!=undefined && data!=null) {
+        if(action=="read") {
+            //done.status = "Marked as read " + data;
+        } else if(action=="unread") {
+            //done.status = "Marked as unread " + data;
+        } else {
+            //done.status = "" + data;
+        }
+    }
+    //done.state = "shown";
+}
+
+function doNothing(data) {
+    // Nothing...
+}
+
+function prettyDate(date){
+    try {
+        var diff = (((new Date()).getTime() - date.getTime()) / 1000);
+        var day_diff = Math.floor(diff / 86400);
+
+        if ( isNaN(day_diff) || day_diff >= 31 ) {
+            //console.log("Days: " + day_diff);
+            return "some time ago";
+        } else if (day_diff < 0) {
+            //console.log("day_diff: " + day_diff);
+            return "just now";
+        }
+
+        return day_diff == 0 && (
+                    diff < 60 && "just now" ||
+                    diff < 120 && "1 minute ago" ||
+                    diff < 3600 && Math.floor( diff / 60 ) + " min ago" ||
+                    diff < 7200 && "1 hour ago" ||
+                    diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") ||
+                day_diff == 1 && "Yesterday" ||
+                day_diff < 7 && day_diff + " days ago" ||
+                day_diff < 31 && Math.ceil( day_diff / 7 ) + " weeks ago";
+        day_diff >= 31 && Math.ceil( day_diff / 30 ) + " months ago";
+    } catch(err) {
+        console.log("Error: " + err);
+        return "some time ago";
+    }
+}
+
+// 2011-01-24T18:48:00Z
+function parseDate(stamp)
+{
+    try {
+        //console.log("stamp: " + stamp);
+        var parts = stamp.split("T");
+        var day;
+        var time;
+        var hours;
+        var minutes;
+        var seconds = 0;
+        var year;
+        var month;
+
+        var dates = parts[0].split("-");
+        year = parseInt(dates[0]);
+        month = parseInt(dates[1])-1;
+        day = parseInt(dates[2]);
+
+        var times = parts[1].split(":");
+        hours = parseInt(times[0]);
+        minutes = parseInt(times[1]);
+
+        var dt = new Date();
+        dt.setUTCDate(day);
+        dt.setYear(year);
+        dt.setUTCMonth(month);
+        dt.setUTCHours(hours);
+        dt.setUTCMinutes(minutes);
+        dt.setUTCSeconds(seconds);
+
+        //console.log("day: " + day + " year: " + year + " month " + month + " hour " + hours);
+
+        return dt;
+    } catch(err) {
+        console.log("Error while parsing date: " + err);
+        return new Date();
+    }
+}
+
+/// ======== WorkerScript related functions ========
+WorkerScript.onMessage = function(message) {
+
+     // the structure of "message" object is:
+     //   - attribute 'action' --> what to do
+     //   - other attributes: will be passed on to the right function
+
+     if(message.action === 'login') {
+         login(message.email, message.password)
+     }
+     else {
+         sid = message.sid; sidToken = message.sidToken;
+
+         if(message.action === 'getCategoryContent') {
+             // read the feeds in the right category
+             switch(message.category) {
+                 case 0: loadAllNews(); break;
+                 case 1: loadUnreadNews(); break;
+                 case 2: loadStarred(); break;
+                 case 3: loadSubscriptions(); break;
+                 case 4: loadTags(); break;
+             }
+         }
+         else if(message.action === 'getSubscriptionItems') {
+             // read the feeds of that subscription
+             loadSubscriptionItems(message.subscription)
+         }
+         else if(message.action === 'getTaggedItems') {
+             // read the feeds of that subscription
+             loadTaggedItems(message.tag)
+         }
+     }
+}
+
index cd6b9a0..00c0685 100644 (file)
@@ -5,8 +5,8 @@ SourceModel {
     name: "Favorite Feeds"
 
     listViews: [
-        { viewComponent: 'content/view/Categories.qml', viewId: 'categoriesRect' } ,
-        { viewComponent: 'content/view/News.qml', viewId: 'newsRect' }
+        { viewComponent: 'content/view/Categories.qml' } ,
+        { viewComponent: 'content/view/News.qml' }
     ]
     listModels: [
         categoriesModel,
diff --git a/qml/QuickNewsReader/content/modelimpl/GoogleReaderCategories.qml b/qml/QuickNewsReader/content/modelimpl/GoogleReaderCategories.qml
new file mode 100644 (file)
index 0000000..73efd83
--- /dev/null
@@ -0,0 +1,14 @@
+import QtQuick 1.0
+import "../modelitf"
+
+ListModel {
+    id: googleReaderCategories
+    property int sourceDepth: 1
+
+    ListElement { name: "All items"; feed: "rss.news.yahoo.com/rss/topstories" }
+    ListElement { name: "Unread items"; feed: "rss.news.yahoo.com/rss/world" }
+    ListElement { name: "Starred items"; feed: "rss.news.yahoo.com/rss/europe" }
+    ListElement { name: "Subscriptions"; feed: "rss.news.yahoo.com/rss/oceania" }
+    ListElement { name: "Tags"; feed: "rss.news.yahoo.com/rss/us" }
+}
+
diff --git a/qml/QuickNewsReader/content/modelimpl/GoogleReaderNews.qml b/qml/QuickNewsReader/content/modelimpl/GoogleReaderNews.qml
new file mode 100644 (file)
index 0000000..4cb44aa
--- /dev/null
@@ -0,0 +1,8 @@
+import QtQuick 1.0
+
+ListModel {
+    id: googleReaderNewsList
+    property int sourceDepth: 2
+
+    // required elements: title and description. Optional: image.
+}
diff --git a/qml/QuickNewsReader/content/modelimpl/GoogleReaderSourceModel.qml b/qml/QuickNewsReader/content/modelimpl/GoogleReaderSourceModel.qml
new file mode 100644 (file)
index 0000000..1d9f7c1
--- /dev/null
@@ -0,0 +1,169 @@
+import QtQuick 1.0
+import "../modelitf"
+import "../js/SettingsStorage.js" as Storage
+
+SourceModel {
+    id: googleReaderModel
+    name: "Google Reader"
+
+    listViews: [
+        { viewComponent: 'content/view/Categories.qml' },
+        { viewComponent: 'content/view/News.qml' }
+    ]
+    listModels: [
+        categoriesModel,
+        categoryContentModel
+    ]
+
+    property variant categoriesModel: GoogleReaderCategories { }
+    property variant categoryContentModel: GoogleReaderNews { sourceDepth: 2 }
+    property variant categorySubContentModel: GoogleReaderNews { sourceDepth: 3 }
+    property variant newsDetailModel: QtObject {
+        property int sourceDepth: 3
+
+        property string htmlcontent: ""
+        property string title: ""
+        property string image: ""
+    }
+    property variant sid;
+    property variant sidToken;
+
+    loading: false
+    hasSettings: true
+    settingsComponent: "GoogleReaderConfig.qml"
+    function storeConfiguration(configUI) {
+        // save the values in the database
+        Storage.setSetting("GoogleReader.login", configUI.loginValue)
+        Storage.setSetting("GoogleReader.password", configUI.passwordValue)
+
+        tryLogin()
+    }
+    function loadConfiguration(configUI) {
+        // retrieve the values from the database
+        configUI.loginValue = Storage.getSetting("GoogleReader.login")
+        configUI.passwordValue = Storage.getSetting("GoogleReader.password")
+    }
+
+    property variant googleReaderLoginWorker: WorkerScript {
+             id: googleReaderLoginWorker
+             source: "../js/GoogleReaderAPI.js"
+
+             onMessage: {
+                 sid = messageObject.sid
+                 sidToken = messageObject.sidToken
+
+                 loading = false
+             }
+         }
+
+    property variant googleReaderLoadCategoryWorker: WorkerScript {
+             id: googleReaderLoadCategoryWorker
+             source: "../js/GoogleReaderAPI.js"
+
+             onMessage: {
+                 categoryContentModel.append({ 'title': messageObject.title, 'description': messageObject.published, 'image': '', 'id': messageObject.id })
+
+                 loading = false
+             }
+         }
+    property variant googleReaderLoadSubscriptionOrTagWorker: WorkerScript {
+             id: googleReaderLoadSubscriptionOrTagWorker
+             source: "../js/GoogleReaderAPI.js"
+
+             onMessage: {
+                 categorySubContentModel.append({ 'title': messageObject.title, 'description': messageObject.published, 'image': '', 'content': messageObject.description })
+
+                 loading = false
+             }
+         }
+    property variant googleReaderLoadItemWorker: WorkerScript {
+             id: googleReaderLoadItemWorker
+             source: "../js/GoogleReaderAPI.js"
+
+             onMessage: {
+                 newsDetailModel.htmlcontent = messageObject.newsContent
+
+                 loading = false
+             }
+         }
+
+    function tryLogin() {
+        var loginValue = Storage.getSetting("GoogleReader.login")
+        var passwordValue = Storage.getSetting("GoogleReader.password")
+
+        loading = true
+
+        googleReaderLoginWorker.sendMessage({
+                                           'action': 'login',
+                                           'email': loginValue,
+                                           'password': passwordValue
+                                       })
+    }
+    onCurrentPathChanged: {
+        // build the right model. currentPath[1] => category
+        var selectionDepth = 0;
+        while(typeof currentPath[selectionDepth+1] !== "undefined")
+            selectionDepth++;
+
+        if( typeof currentPath[1] !== "undefined" ) {
+            if( selectionDepth == 1 ) {
+                // the category has been selected, so fill in the right content
+                categoryContentModel.clear()
+
+                // reshape the views and the models to fit the chosen path
+                var newsDetailIndex = 2;
+                var tmpListModels = listModels
+                var tmpListViews = listViews
+                if( currentPath[1] === 3 || currentPath[1] === 4 )
+                {
+                    tmpListModels[2] = categorySubContentModel;
+                    tmpListViews[2] = { viewComponent: 'content/view/News.qml' }
+                    newsDetailIndex = 3;
+                }
+                tmpListModels[newsDetailIndex] = newsDetailModel;
+                tmpListViews[newsDetailIndex] = { viewComponent: 'content/view/NewsDetail.qml' };
+                tmpListModels[newsDetailIndex+1] = null
+                tmpListViews[newsDetailIndex+1] = null
+                listModels = tmpListModels
+                listViews = tmpListViews
+
+                googleReaderLoadCategoryWorker.sendMessage({
+                                                            'action': 'getCategoryContent',
+                                                            'sid': sid, 'sidToken': sidToken,
+                                                            'category': currentPath[1]
+                                                        })
+            }
+            else if( selectionDepth == 2 && currentPath[1] === 3 ) {
+                // subscriptions selected
+                categorySubContentModel.clear()
+                googleReaderLoadSubscriptionOrTagWorker.sendMessage({
+                                                            'action': 'getSubscriptionItems',
+                                                            'sid': sid, 'sidToken': sidToken,
+                                                            'subscription': categoryContentModel.get(currentPath[2]).id
+                                                        })
+            }
+            else if( selectionDepth == 2 && currentPath[1] === 4 ) {
+                // tags selected
+                categorySubContentModel.clear()
+                googleReaderLoadSubscriptionOrTagWorker.sendMessage({
+                                                            'action': 'getTaggedItems',
+                                                            'sid': sid, 'sidToken': sidToken,
+                                                            'tag': categoryContentModel.get(currentPath[2]).id
+                                                        })
+            }
+            else if( selectionDepth == 3 ) {
+                // subscription or tagged item selected
+                newsDetailModel.htmlcontent = categorySubContentModel.get(currentPath[3]).content
+            }
+            else if( selectionDepth == 2 ) {
+                // simply get the chosen news
+                newsDetailModel.htmlcontent = categoryContentModel.get(currentPath[2]).content
+            }
+        }
+    }
+
+    Component.onCompleted: {
+        Storage.initialize()
+        tryLogin()
+    }
+}
index 7a2b54d..4bdb4bb 100644 (file)
@@ -5,10 +5,10 @@ SourceModel {
     name: "Le Monde"
 
     listViews: [
-        { viewComponent: 'content/view/Categories.qml', viewId: 'categoriesRect' } ,
-        { viewComponent: 'content/view/News.qml', viewId: 'newsRect' },
-        { viewComponent: 'content/view/NewsDetail.qml', viewId: 'newsDetailRect' },
-        { viewComponent: 'content/view/NewsComments.qml', viewId: 'newsCommentsRect' }
+        { viewComponent: 'content/view/Categories.qml' } ,
+        { viewComponent: 'content/view/News.qml' },
+        { viewComponent: 'content/view/NewsDetail.qml' },
+        { viewComponent: 'content/view/NewsComments.qml' }
     ]
     listModels: [
         categoriesModel,
index 971a99a..e88accf 100644 (file)
@@ -5,8 +5,8 @@ SourceModel {
     name: "Yahoo! News"
 
     listViews: [
-        { viewComponent: 'content/view/Categories.qml', viewId: 'categoriesRect' } ,
-        { viewComponent: 'content/view/News.qml', viewId: 'newsRect' }
+        { viewComponent: 'content/view/Categories.qml' } ,
+        { viewComponent: 'content/view/News.qml' }
     ]
     listModels: [
         categoriesModel,
diff --git a/qml/QuickNewsReader/content/view/Background.qml b/qml/QuickNewsReader/content/view/Background.qml
new file mode 100644 (file)
index 0000000..4be2231
--- /dev/null
@@ -0,0 +1,31 @@
+import QtQuick 1.0
+
+Rectangle {
+    id: background
+    color: "#80343434"
+    radius: 30
+
+    BorderImage {
+        id: borderBackgroundImage
+        border.left: 30
+        border.top: 30
+        border.bottom: 30
+        border.right: 30
+        horizontalTileMode: BorderImage.Repeat
+        verticalTileMode: BorderImage.Repeat
+        source: "../images/borderStripes.png"
+        anchors.fill: parent
+    }
+
+    Rectangle {
+        anchors.fill: parent;
+        anchors.margins: 30;
+        color: "transparent"
+        Image {
+            source: "../images/stripes.png";
+            fillMode: Image.Tile;
+            anchors.fill: parent;
+            //opacity: 0.3
+        }
+    }
+}
index 695b752..742b6c5 100644 (file)
@@ -3,49 +3,21 @@ import QtQuick 1.0
 Rectangle {
 
     id: categoriesRect
-    width: 220; height: window.height
+    width: window.width; height: window.height
     color: "#efefef"
 
     ListView {
         focus: true
         id: categories
         x: 0; y: 0
-        width: 220; height: window.height
+        width: window.width; height: window.height
         model: currentSource.listModels[componentDepth-1]
-        footer: getFooter()
+        //footer: getFooter()
         delegate: CategoryDelegate { }
         highlight: Rectangle { color: "steelblue" }
         highlightMoveSpeed: 9999999
-
-        function getFooter()
-        {
-            return componentDepth === 1 && currentSource.hasSettings ? settingsButtonDelegate : null
-        }
     }
     ScrollBar { scrollArea: categories; height: categories.height; width: 8; anchors.right: categories.right }
 
     Component.onCompleted: categories.currentIndex = -1
-
-    Component {
-        id: settingsButtonDelegate
-        Item {
-            width: categories.width; height: 70
-
-            FancyButton {
-                icon: "../images/settings.png"
-                anchors.horizontalCenter: parent.horizontalCenter
-                anchors.bottom: parent.bottom
-                anchors.bottomMargin: -2
-
-                /*
-                onClicked: {
-                    if (editMenu.visible) {
-                        editMenu.opacity = 0.0
-                    } else {
-                        editMenu.opacity = 0.8
-                    }
-                }*/
-            }
-        }
-    }
 }
index 617846d..13afcba 100644 (file)
@@ -77,20 +77,20 @@ Item {
         onClicked: {
             var currentSourceDepth = delegate.ListView.view.model.sourceDepth
 
-            // here we remove everything in viewsModel after index "nextSourceDepth"
-            while(window.windowViewsModel.count>currentSourceDepth+1)
-                window.windowViewsModel.remove(window.windowViewsModel.count-1)
+            if (listSourceModel[window.currentSourceIndex].listViews.length >= currentSourceDepth+1)
+            {
+                // here we remove everything in viewsModel after index "nextSourceDepth"
+                while(window.windowViewsModel.count>currentSourceDepth+1)
+                    window.windowViewsModel.remove(window.windowViewsModel.count-1)
 
-            delegate.ListView.view.currentIndex = index
-            var path = listSourceModel[window.currentSourceIndex].currentPath
-            path[currentSourceDepth] = index
-            listSourceModel[window.currentSourceIndex].currentPath = path
+                delegate.ListView.view.currentIndex = index
+                var path = listSourceModel[window.currentSourceIndex].currentPath
+                path[currentSourceDepth] = index
+                listSourceModel[window.currentSourceIndex].currentPath = path.slice(0,currentSourceDepth+1)
 
-            window.windowViewsModel.append({ component: listSourceModel[window.currentSourceIndex].listViews[currentSourceDepth].viewComponent,
-                                             componentId: listSourceModel[window.currentSourceIndex].listViews[currentSourceDepth].viewId,
-                                             componentDepth: currentSourceDepth+1 })
-
-            window.windowViewsList.currentIndex = currentSourceDepth+1;
+                window.windowViewsModel.append({ component: listSourceModel[window.currentSourceIndex].listViews[currentSourceDepth].viewComponent,
+                                                 componentDepth: currentSourceDepth+1 })
+            }
         }
     }
 }
diff --git a/qml/QuickNewsReader/content/view/GoogleReaderConfig.qml b/qml/QuickNewsReader/content/view/GoogleReaderConfig.qml
new file mode 100644 (file)
index 0000000..c8eecc9
--- /dev/null
@@ -0,0 +1,64 @@
+import QtQuick 1.0
+
+Rectangle {
+    // Login : [text entry]
+    // Password : [pwd entry]
+    color: "transparent"
+
+    property alias loginValue: inputLogin.text
+    property alias passwordValue: inputPwd.text
+
+    Column {
+        anchors.fill: parent
+        anchors.margins: 30
+        spacing: 10
+
+        Row {
+            anchors.left: parent.left
+            anchors.leftMargin: 10
+            anchors.right: parent.right
+            anchors.rightMargin: 10
+
+            Text {
+                id: labelLogin
+                color: "white"
+                anchors.verticalCenter: parent.verticalCenter
+                width: 150
+                text: qsTr("Login")
+                font.pixelSize: 24
+                font.bold: true
+            }
+
+            LineInput {
+                id: inputLogin
+                width: parent.width - 150 - 10 - 10
+                anchors.verticalCenter: parent.verticalCenter
+            }
+        }
+
+        Row {
+            anchors.left: parent.left
+            anchors.leftMargin: 10
+            anchors.right: parent.right
+            anchors.rightMargin: 10
+
+            Text {
+                id: labelPwd
+                color: "white"
+                anchors.verticalCenter: parent.verticalCenter
+                width: 150
+                text: qsTr("Password")
+                font.pixelSize: 24
+                font.bold: true
+            }
+
+            LineInput {
+                id: inputPwd
+                width: parent.width - 150 - 10 - 10
+                anchors.verticalCenter: parent.verticalCenter
+                // password specific
+                input.echoMode: TextInput.Password
+            }
+        }
+    }
+}
diff --git a/qml/QuickNewsReader/content/view/LineInput.qml b/qml/QuickNewsReader/content/view/LineInput.qml
new file mode 100644 (file)
index 0000000..a687a10
--- /dev/null
@@ -0,0 +1,26 @@
+import QtQuick 1.0
+
+Item {
+  id: lineInput
+  property alias text: input.text
+  property alias input: input
+  height: 50
+
+  BorderImage {
+      id: borderImage
+      border.left: 10
+      border.top: 10
+      border.bottom: 10
+      border.right: 10
+      source: "../images/lineedit.png"
+      anchors.fill: parent
+  }
+  TextInput {
+      id: input
+      color: "#151515"; selectionColor: "green"
+      font.pixelSize: 24; font.bold: true
+      width: borderImage.width-16
+      anchors.centerIn: parent
+      focus: true
+  }
+}
index cc71822..a7cf7f8 100644 (file)
@@ -11,6 +11,7 @@ Item {
         model: currentSource.listModels[componentDepth-1]
         delegate: NewsDelegate { }
         highlight: Rectangle { color: "steelblue" }
+        highlightMoveDuration: 600
     }
     ScrollBar { scrollArea: list; height: list.height; width: 8; anchors.right: list.right }
 
index 3da859c..8b136d6 100644 (file)
@@ -23,8 +23,8 @@ Item {
 
         Column {
             id: column
-            x: 0; y: 0
-            width: newsCommentsRect.width
+            x: 10; y: 10
+            width: newsCommentsRect.width - 20
 //            height: newsCommentsRect.height
 
             Row {
@@ -37,7 +37,7 @@ Item {
                 }
 
                 Text {
-                    anchors.verticalCenter: detailImage.verticalCenter
+                    anchors.verticalCenter: titleRow.verticalCenter
                     text: title; width: column.width - detailImage.width - 10; wrapMode: Text.WordWrap
                     font { bold: true; family: "Helvetica"; pointSize: 16 }
                 }
@@ -45,9 +45,9 @@ Item {
 
             WebView {
                 id: newsCommentsWebView
-                width: newsCommentsRect.width
+                width: column.width
                 url: commentURL
-                preferredWidth: window.width
+                preferredWidth: column.width
 //                preferredHeight: parent.height - titleRow.height
             }
         }
index d51f3ee..bc5c88b 100644 (file)
@@ -59,6 +59,7 @@ Item {
 
 
         Row {
+            id: descriptionRow
             spacing: 5
 
             Image {
@@ -67,7 +68,7 @@ Item {
             }
 
             Text {
-                anchors.verticalCenter: detailImage.verticalCenter
+                anchors.verticalCenter: descriptionRow.verticalCenter
                 text: description; width: column.width - detailImage.width - 10; wrapMode: Text.WordWrap
                 font.family: "Helvetica"
             }
@@ -78,9 +79,10 @@ Item {
         anchors.fill: delegate
 
         onClicked: {
-            if (typeof detailedContent != "undefined") {
-                var currentSourceDepth = delegate.ListView.view.model.sourceDepth
+            var currentSourceDepth = delegate.ListView.view.model.sourceDepth
 
+            if (listSourceModel[window.currentSourceIndex].listViews.length >= currentSourceDepth+1)
+            {
                 // here we remove everything in viewsModel after index "nextSourceDepth"
                 while(window.windowViewsModel.count>currentSourceDepth+1)
                     window.windowViewsModel.remove(window.windowViewsModel.count-1)
@@ -88,18 +90,10 @@ Item {
                 delegate.ListView.view.currentIndex = index
                 var path = listSourceModel[window.currentSourceIndex].currentPath
                 path[currentSourceDepth] = index
-                listSourceModel[window.currentSourceIndex].currentPath = path
+                listSourceModel[window.currentSourceIndex].currentPath = path.slice(0,currentSourceDepth+1)
 
                 window.windowViewsModel.append({ component: listSourceModel[window.currentSourceIndex].listViews[currentSourceDepth].viewComponent,
-                                                 componentId: listSourceModel[window.currentSourceIndex].listViews[currentSourceDepth].viewId,
                                                  componentDepth: currentSourceDepth+1 })
-
-                window.windowViewsList.currentIndex = currentSourceDepth+1;
-
-                //newsDetailRect.webViewAction.enabled = false
-           ////     sourcesRect.x -= window.width + newsRect.x
-                //newsDetailRect.webViewAction.enabled = true
-                //newsDetailRect.webViewAction.trigger()
             }
         }
     }
index da55341..295b440 100644 (file)
@@ -25,11 +25,11 @@ Item {
         Item {
             x: 0; y: 0
             width: newsDetailRect.width
-            height: column.height
+            height: column.height + 10
 
             Column {
                 id: column
-                x: 0; y: 0
+                x: 10; y: 10
                 width: parent.width - 20
 
                 Row {
@@ -50,6 +50,7 @@ Item {
                 Text {
                     id: detailText
                     text: htmlcontent; width: newsDetailRect.width - 20; wrapMode: Text.WordWrap
+                    horizontalAlignment: Text.AlignJustify;
                     font.family: "Helvetica"
                 }
             }
@@ -60,19 +61,19 @@ Item {
                 onClicked: {
                     var currentSourceDepth = currentSource.listModels[componentDepth-1].sourceDepth
 
-                    // here we remove everything in viewsModel after index "currentSourceDepth"
-                    while(window.windowViewsModel.count>currentSourceDepth+1)
-                        window.windowViewsModel.remove(window.windowViewsModel.count-1)
+                    if (listSourceModel[window.currentSourceIndex].listViews.length >= currentSourceDepth+1)
+                    {
+                        // here we remove everything in viewsModel after index "currentSourceDepth"
+                        while(window.windowViewsModel.count>currentSourceDepth+1)
+                            window.windowViewsModel.remove(window.windowViewsModel.count-1)
 
-                    var path = listSourceModel[window.currentSourceIndex].currentPath
-                    path[currentSourceDepth] = index
-                    listSourceModel[window.currentSourceIndex].currentPath = path
+                        var path = listSourceModel[window.currentSourceIndex].currentPath
+                        path[currentSourceDepth] = index
+                        listSourceModel[window.currentSourceIndex].currentPath = path.slice(0,currentSourceDepth+1)
 
-                    window.windowViewsModel.append({ component: listSourceModel[window.currentSourceIndex].listViews[currentSourceDepth].viewComponent,
-                                                     componentId: listSourceModel[window.currentSourceIndex].listViews[currentSourceDepth].viewId,
-                                                     componentDepth: currentSourceDepth+1 })
-
-                    window.windowViewsList.currentIndex = currentSourceDepth+1;
+                        window.windowViewsModel.append({ component: listSourceModel[window.currentSourceIndex].listViews[currentSourceDepth].viewComponent,
+                                                         componentDepth: currentSourceDepth+1 })
+                    }
                 }
             }
         }
index 72e53b3..973c906 100644 (file)
@@ -8,7 +8,7 @@ Rectangle {
     visible: false
     state: "hidden"
 
-    property string configViewComponent
+    property SourceModel configModel;
 
     //property SourceModel model;
     //property SourceConfigComponentView viewComponent;
@@ -18,9 +18,10 @@ Rectangle {
             name: "showSourceConfig"
 
             // In this state, we bring the configuration UI of the source
-            PropertyChanges { target: configDialog; color: "#d0000000" }
+            PropertyChanges { target: configDialog; color: "#80000000" }
             PropertyChanges { target: sourceConfigLoader; opacity: 1 }
-            PropertyChanges { target: sourceConfigLoader; source: configViewComponent }
+            PropertyChanges { target: sourceConfigLoader; source: configModel.settingsComponent }
+            PropertyChanges { target: configTitle; text: configModel.name + " Settings"}
 
             AnchorChanges { target: quitApplyConfigButton; anchors.left: undefined; anchors.right: configDialog.right }
             AnchorChanges { target: quitCancelConfigButton; anchors.right: undefined; anchors.left: configDialog.left }
@@ -58,10 +59,28 @@ Rectangle {
         }
     ]
 
+    Background {
+        anchors.top: parent.top
+        anchors.left: parent.left
+        anchors.right: parent.right
+        anchors.bottom: quitApplyConfigButton.top
+    }
+
+    Text {
+        id: configTitle
+        color: "white"
+        anchors.top: parent.top
+        anchors.left: parent.left
+        anchors.right: parent.right
+        horizontalAlignment: Text.AlignHCenter
+        font.pixelSize: 24
+        font.bold: true
+    }
+
     Loader {
         id: sourceConfigLoader
         opacity: 0
-        anchors.top: parent.top
+        anchors.top: configTitle.bottom
         anchors.left: parent.left
         anchors.right: parent.right
         anchors.bottom: quitApplyConfigButton.top
@@ -69,6 +88,11 @@ Rectangle {
         Behavior on opacity {
             NumberAnimation { duration: 1000; easing.type: Easing.InOutQuad }
         }
+
+        onLoaded: {
+            // fill the UI with information from the model
+            configModel.loadConfiguration(sourceConfigLoader.item)
+        }
     }
 
     FancyButton {
@@ -78,6 +102,9 @@ Rectangle {
         anchors.left: parent.right
 
         onClicked: {
+            // ask the model to store the configuration
+            configModel.storeConfiguration(sourceConfigLoader.item)
+
             // Store the configuration of this source, and disappear
             configDialog.state = "hidden"
         }
index 8824282..913f49d 100644 (file)
@@ -78,7 +78,7 @@ Item {
             // show the configuration for this journal
             if( listSourceModel[index].hasSettings )
             {
-                window.showConfigDialog(listSourceModel[index].settingsComponent)
+                window.showConfigDialog(index)
             }
         }
 
@@ -91,15 +91,12 @@ Item {
 
                 delegate.ListView.view.currentIndex = index
 
+                listSourceModel[index].currentPath = [index]
+
                 window.windowViewsModel.append({ component: listSourceModel[index].listViews[0].viewComponent,
-                                                 componentId: listSourceModel[index].listViews[0].viewId,
                                                  componentDepth: 1 })
 
-                listSourceModel[index].currentPath = [index]
-
                 window.currentSourceIndex = index
-
-                window.windowViewsList.currentIndex = 1;
             }
         }
     }
index ab3e640..0838360 100644 (file)
@@ -2,22 +2,24 @@ import QtQuick 1.0
 
 Rectangle {
     id: sourcesRect
-    width: 150; height: window.height
+    width: window.width; height: window.height
     color: "#dfdfdf"
 
+    function populateSourcesModel() {
+        for(var i=0; typeof window.listSourceModel[i] !== "undefined"; i++ ) {
+            sourceList.append({})
+        }
+    }
+
     ListModel {
         id: sourceList
-
-        ListElement {  }
-        ListElement {  }
-        ListElement {  }
     }
 
     ListView {
         focus: true
         id: sources
         x: 0; y: 0
-        width: 150; height: window.height
+        width: window.width; height: window.height
         currentIndex: currentSourceIndex
         model: sourceList
         footer: quitButtonDelegate
@@ -27,5 +29,8 @@ Rectangle {
     }
     ScrollBar { scrollArea: sources; height: sources.height; width: 8; anchors.right: sources.right }
 
-    Component.onCompleted: sources.currentIndex = -1
+    Component.onCompleted: {
+        sources.currentIndex = -1
+        populateSourcesModel()
+    }
 }
index 0c41d95..b5c3eaf 100644 (file)
@@ -5,26 +5,29 @@ import "content/modelitf"
 
 Rectangle {
     id: window
+    //anchors.fill: parent // use this little trick to always adapt itself to the screen
     width: 800; height: 480
 
     property int currentSourceIndex: 0
 
     property list<SourceModel> listSourceModel: [
         LeMondeSourceModel{},
+        GoogleReaderSourceModel{},
         FavoriteFeedsSourceModel{},
         YahooSourceModel{}
     ]
 
+    property variant currentSource: listSourceModel[currentSourceIndex]
+    property bool loading: currentSource.loading
+    property ListModel windowViewsModel: viewsModel
+    property ListView windowViewsList: viewsList
+
     ListModel {
         id: viewsModel
 
-        ListElement { component: "content/view/Sources.qml"; componentId: "sourcesRect"; componentDepth: 0 }
+        ListElement { component: "content/view/Sources.qml"; componentDepth: 0 }
     }
 
-    property variant currentSource: listSourceModel[currentSourceIndex]
-    property bool loading: currentSource.loading
-    property ListModel windowViewsModel: viewsModel
-    property ListView windowViewsList: viewsList
 
     ListView {
         id: viewsList
@@ -33,16 +36,25 @@ Rectangle {
         orientation: ListView.Horizontal
         snapMode: ListView.SnapOneItem
         flickDeceleration: 500
+        cacheBuffer: 1600 // so that the next delegate gets actually loaded...
+        preferredHighlightBegin: window.x
+        preferredHighlightEnd: window.width
+        highlightRangeMode: ListView.StrictlyEnforceRange
+        boundsBehavior: Flickable.StopAtBounds
 
         model: viewsModel
         delegate: Loader {
-            id: componentId
+            id: modelLoader
             source: component
+
+            ListView.onAdd: {
+                viewsList.currentIndex = componentDepth
+            }
         }
     }
 
-    function showConfigDialog(settingsComponent) {
-        configDialog.configViewComponent = settingsComponent
+    function showConfigDialog(index) {
+        configDialog.configModel = listSourceModel[index]
         configDialog.state = "showSourceConfig"
     }
 
index e64bd84..a497c05 100644 (file)
@@ -1,3 +1,13 @@
+quicknewsreader (0.4-0) unstable; urgency=low
+
+  * Switched to a ListView page swapper
+  * More robust model that can be easily adapted for various sources
+  * Added comments for LeMonde.fr website
+  * Added Configuration dialog with long-press on a configurable source (like Google Reader)
+  * Added Google Reader (read-only)
+
+ -- Christophe CHAPUIS <chris@unknown>  Sat, 18 Feb 2012 22:01:32 +0100
+
 quicknewsreader (0.3-0) unstable; urgency=low
 
   * Initial Release.