--- /dev/null
+/*
+ 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)
+ }
+ }
+}
+
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,
--- /dev/null
+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" }
+}
+
--- /dev/null
+import QtQuick 1.0
+
+ListModel {
+ id: googleReaderNewsList
+ property int sourceDepth: 2
+
+ // required elements: title and description. Optional: image.
+}
--- /dev/null
+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()
+ }
+}
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,
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,
--- /dev/null
+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
+ }
+ }
+}
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
- }
- }*/
- }
- }
- }
}
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 })
+ }
}
}
}
--- /dev/null
+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
+ }
+ }
+ }
+}
--- /dev/null
+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
+ }
+}
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 }
Column {
id: column
- x: 0; y: 0
- width: newsCommentsRect.width
+ x: 10; y: 10
+ width: newsCommentsRect.width - 20
// height: newsCommentsRect.height
Row {
}
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 }
}
WebView {
id: newsCommentsWebView
- width: newsCommentsRect.width
+ width: column.width
url: commentURL
- preferredWidth: window.width
+ preferredWidth: column.width
// preferredHeight: parent.height - titleRow.height
}
}
Row {
+ id: descriptionRow
spacing: 5
Image {
}
Text {
- anchors.verticalCenter: detailImage.verticalCenter
+ anchors.verticalCenter: descriptionRow.verticalCenter
text: description; width: column.width - detailImage.width - 10; wrapMode: Text.WordWrap
font.family: "Helvetica"
}
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)
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()
}
}
}
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 {
Text {
id: detailText
text: htmlcontent; width: newsDetailRect.width - 20; wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignJustify;
font.family: "Helvetica"
}
}
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 })
+ }
}
}
}
visible: false
state: "hidden"
- property string configViewComponent
+ property SourceModel configModel;
//property SourceModel model;
//property SourceConfigComponentView viewComponent;
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 }
}
]
+ 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
Behavior on opacity {
NumberAnimation { duration: 1000; easing.type: Easing.InOutQuad }
}
+
+ onLoaded: {
+ // fill the UI with information from the model
+ configModel.loadConfiguration(sourceConfigLoader.item)
+ }
}
FancyButton {
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"
}
// show the configuration for this journal
if( listSourceModel[index].hasSettings )
{
- window.showConfigDialog(listSourceModel[index].settingsComponent)
+ window.showConfigDialog(index)
}
}
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;
}
}
}
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
}
ScrollBar { scrollArea: sources; height: sources.height; width: 8; anchors.right: sources.right }
- Component.onCompleted: sources.currentIndex = -1
+ Component.onCompleted: {
+ sources.currentIndex = -1
+ populateSourcesModel()
+ }
}
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
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"
}
+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.