--- /dev/null
+The Debian Package grr
+----------------------------
+
+Comments regarding the Package
+
+ -- unknown <jan@unknown> Tue, 23 Feb 2010 22:26:11 +0100
--- /dev/null
+grr (0.0.2) unstable; urgency=low
+
+ * Added 'Starred' tags
+
+ * Fixed race condition at startup which causes an empty feed list.
+
+ -- Jan Dumon <jan@crossbar.net> Fri, 26 Feb 2010 23:12:00 +0100
+
+grr (0.0.1) unstable; urgency=low
+
+ * Initial Release.
+
+ -- Jan Dumon <jan@crossbar.net> Tue, 23 Feb 2010 22:26:11 +0100
--- /dev/null
+Source: grr
+Section: user/network
+Priority: optional
+Maintainer: Jan Dumon <jan@crossbar.net>
+Build-Depends: debhelper (>= 4.0.0), libqt4-maemo5-dev (>= 4.6.2~git20100212), libgconf2-dev (>= 2.13.5)
+Standards-Version: 3.6.0
+
+Package: grr
+Architecture: any
+Depends: ${shlibs:Depends}, ${misc:Depends}
+Description: Grr is a simple application for accessing Google Reader.
+XSBC-Bugtracker: http://garage.maemo.org/projects/grr
+XB-Maemo-Icon-26:
+ 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
+ YII=
--- /dev/null
+This package was debianized by Jan Dumon <jan@crossbar.net> on
+Tue, 23 Feb 2010 22:26:11 +0100.
+
+It was downloaded from http://dufo.be
+
+Upstream Author(s):
+
+ Jan Dumon <jan@crossbar.net>
+
+Copyright:
+
+ Copyright (C) 2010 Jan Dumon
+
+License:
+
+ This package 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 2 of the License, or (at your option) any later version.
+
+ This package 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 this package; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+On Debian systems, the complete text of the GNU Lesser General
+Public License can be found in `/usr/share/common-licenses/LGPL'.
+
+The Debian packaging is (C) 2010, Jan Dumon <jan@crossbar.net> and
+is licensed under the GPL, see `/usr/share/common-licenses/GPL'.
+
+# Please also look if there are files or directories which have a
+# different copyright/license attached and list them here.
+src/grr.png:
+ License: http://creativecommons.org/licenses/by-sa/3.0/
+ Author: http://lopagof.deviantart.com/
--- /dev/null
+?package(grr):needs="X11|text|vc|wm" section="Applications/see-menu-manual"\
+ title="grr" command="/opt/grr/grr"
--- /dev/null
+#!/usr/bin/make -f
+# -*- makefile -*-
+# Sample debian/rules that uses debhelper.
+# This file was originally written by Joey Hess and Craig Small.
+# As a special exception, when this file is copied by dh-make into a
+# dh-make output file, you may use that output file without restriction.
+# This special exception was added by Craig Small in version 0.37 of dh-make.
+
+# Uncomment this to turn on verbose mode.
+export DH_VERBOSE=1
+
+
+
+
+configure: configure-stamp
+configure-stamp:
+ dh_testdir
+ # Add here commands to configure the package.
+
+ /opt/qt4-maemo5/bin/qmake
+ touch configure-stamp
+
+
+build: build-stamp
+
+build-stamp: configure-stamp
+ dh_testdir
+
+ $(MAKE)
+
+ touch $@
+
+clean:
+ dh_testdir
+ dh_testroot
+ rm -f build-stamp configure-stamp
+
+ -$(MAKE) clean
+
+ dh_clean
+
+install: build
+ dh_testdir
+ dh_testroot
+ dh_clean -k
+ dh_installdirs
+
+ $(MAKE) DESTDIR="$(CURDIR)"/debian/grr install
+
+
+# Build architecture-independent files here.
+binary-indep: build install
+# We have nothing to do by default.
+
+# Build architecture-dependent files here.
+binary-arch: build install
+ dh_testdir
+ dh_testroot
+ dh_installchangelogs
+# dh_installdocs
+# dh_installexamples
+# dh_install
+# dh_installmenu
+# dh_installdebconf
+# dh_installlogrotate
+# dh_installemacsen
+# dh_installpam
+# dh_installmime
+# dh_python
+# dh_installinit
+# dh_installcron
+# dh_installinfo
+# dh_installman
+ dh_link
+ dh_strip
+ dh_compress
+ dh_fixperms
+# dh_perl
+# dh_makeshlibs
+ dh_installdeb
+ dh_shlibdeps
+ dh_gencontrol
+ dh_md5sums
+ dh_builddeb
+
+binary: binary-indep binary-arch
+.PHONY: build clean binary-indep binary-arch binary install configure
--- /dev/null
+TARGET = grr
+HEADERS += googlereader.h feedswindow.h entrieswindow.h contentwindow.h
+SOURCES += qtmain.cpp googlereader.cpp feedswindow.cpp entrieswindow.cpp contentwindow.cpp
+RESOURCES += grr.qrc
+FORMS +=
+LEXSOURCES += #LEXS#
+YACCSOURCES += #YACCS#
+
+INCLUDEPATH +=
+LIBS +=
+DEFINES +=
+
+# All generated files goes same directory
+OBJECTS_DIR = build
+MOC_DIR = build
+UI_DIR = build
+
+DESTDIR = build
+TEMPLATE = app
+DEPENDPATH +=
+VPATH += src
+CONFIG -=
+CONFIG += debug
+QT = core gui network xml webkit maemo5
+
+#
+# Targets for debian source and binary package creation
+#
+#debian-src.commands = dpkg-buildpackage -S -r -us -uc -d
+debian-src.commands = dpkg-buildpackage -sa -S
+debian-bin.commands = dpkg-buildpackage -b -r -uc -d
+debian-all.depends = debian-src debian-bin
+
+#
+# Clean all but Makefile
+#
+compiler_clean.commands = -$(DEL_FILE) $(TARGET)
+
+QMAKE_EXTRA_TARGETS += debian-all debian-src debian-bin install compiler_clean
+
+
+unix {
+ PREFIX = debian/grr
+ BINDIR = $$PREFIX/opt/grr
+ DATADIR = $$PREFIX/usr/share
+
+ DEFINES += DATADIR=\"$$DATADIR\" PKGDATADIR=\"$$PKGDATADIR\"
+
+ INSTALLS += target desktop icon64
+
+ target.path = $$BINDIR
+
+ desktop.path = $$DATADIR/applications/hildon
+ desktop.files += grr.desktop
+
+ icon64.path = $$DATADIR/icons/hicolor/64x64/apps
+ icon64.files += grr.png
+}
--- /dev/null
+#include <QMenuBar>
+#include <QDebug>
+#include <QDesktopServices>
+#include <QtGui>
+#include <QtWebKit>
+#include "contentwindow.h"
+
+/* Got ViewportItem and GraphicsView from maemobrowser in the qt examples. The
+ * whole point of this is to get a finger friendly web widget */
+
+class ViewportItem : public QGraphicsWidget, public QAbstractKineticScroller {
+ Q_OBJECT
+
+ public:
+ ViewportItem() : QGraphicsWidget(), QAbstractKineticScroller(), m_widget(0), m_ignoreEvents(false) {
+ setFlag(QGraphicsItem::ItemHasNoContents, true);
+ setFlag(QGraphicsItem::ItemClipsChildrenToShape, true);
+ setFlag(QGraphicsItem::ItemClipsToShape, true);
+ setAttribute(Qt::WA_OpaquePaintEvent, true);
+ setFiltersChildEvents(true);
+ }
+
+ void setWidget(QGraphicsWidget *widget) {
+ if (m_widget) {
+ m_widget->setParentItem(0);
+ delete m_widget;
+ }
+ m_widget = widget;
+ if (m_widget) {
+ m_widget->setParentItem(this);
+ m_widget->setAttribute(Qt::WA_OpaquePaintEvent, true);
+
+ if (qgraphicsitem_cast<QGraphicsWebView *>(m_widget)) {
+ connect(m_widget, SIGNAL(loadProgress(int)), this, SLOT(resizeWebViewToFrame()));
+ connect(m_widget, SIGNAL(loadFinished(bool)), this, SLOT(resizeWebViewToFrame()));
+ resizeWebViewToFrame();
+ }
+ }
+ }
+
+ protected:
+ bool sceneEventFilter(QGraphicsItem *i, QEvent *e) {
+ bool res = false;
+ if (i && (i == m_widget) && !m_ignoreEvents && m_widget->isEnabled()) {
+ switch (e->type()) {
+ case QEvent::GraphicsSceneMousePress:
+ case QEvent::GraphicsSceneMouseMove:
+ case QEvent::GraphicsSceneMouseRelease:
+ /*case QEvent::GraphicsSceneMouseDoubleClick:*/ {
+ res = handleMouseEvent(static_cast<QGraphicsSceneMouseEvent *>(e));
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ // prevent text selection and image dragging
+ if (e->type() == QEvent::GraphicsSceneMouseMove)
+ return true;
+ return res ? true : QGraphicsWidget::sceneEventFilter(i, e);
+ }
+
+ QSize viewportSize() const {
+ return size().toSize();
+ }
+
+ QPoint maximumScrollPosition() const {
+ QSizeF s = m_widget ? m_widget->size() - size() : QSize(0, 0);
+ return QPoint(qMax(0, int(s.width())), qMax(0, int(s.height())));
+ }
+
+ QPoint scrollPosition() const {
+ return m_widget ? -m_widget->pos().toPoint() + m_overShoot : QPoint();
+ }
+
+ void setScrollPosition(const QPoint &p, const QPoint &overShoot) {
+ m_overShoot = overShoot;
+ if (m_widget)
+ m_widget->setPos(-p + m_overShoot);
+ }
+
+ void cancelLeftMouseButtonPress(const QPoint & /*globalPressPos*/) {
+ }
+
+ void sendEvent(QGraphicsItem *i, QEvent *e) {
+ m_ignoreEvents = true;
+ scene()->sendEvent(i, e);
+ m_ignoreEvents = false;
+ }
+
+ private slots:
+ void resizeWebViewToFrame() {
+ if (QGraphicsWebView *view = qgraphicsitem_cast<QGraphicsWebView *>(m_widget)) {
+ if (view->page() && view->page()->mainFrame()) {
+ QSizeF s = view->page()->mainFrame()->contentsSize();
+ /* TODO: Figure out the proper way to
+ * get this 800 pixels. */
+ QSizeF s2 = size();
+ s2.setWidth(800);
+ s = s.expandedTo(s2);
+ view->setGeometry(QRectF(view->geometry().topLeft(), s));
+ }
+ }
+ }
+
+ private:
+ QGraphicsWidget *m_widget;
+ bool m_ignoreEvents;
+ QPoint m_overShoot;
+};
+
+class GraphicsView : public QGraphicsView {
+ Q_OBJECT
+
+ public:
+ GraphicsView() : QGraphicsView(new QGraphicsScene()), viewport(0) {
+ setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate);
+ setOptimizationFlags(QGraphicsView::DontSavePainterState);
+
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+
+ setFrameShape(QFrame::NoFrame);
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+
+ viewport = new ViewportItem();
+ scene()->addItem(viewport);
+ }
+
+ ViewportItem *viewportItem() const {
+ return viewport;
+ }
+
+ protected:
+ void resizeEvent(QResizeEvent *e) {
+ QGraphicsView::resizeEvent(e);
+ setUpdatesEnabled(false);
+
+ if (!viewport)
+ return;
+
+ QRectF rect(QPointF(0, 0), size());
+ scene()->setSceneRect(rect);
+
+ viewport->setGeometry(rect);
+ setUpdatesEnabled(true);
+ update();
+ }
+
+ private:
+ ViewportItem *viewport;
+};
+
+ContentWindow::ContentWindow(QWidget *parent, Entry *e) : QMainWindow(parent) {
+ setAttribute(Qt::WA_Maemo5StackedWindow);
+
+ QWebSettings::globalSettings()->setAttribute(QWebSettings::PluginsEnabled, true);
+
+ entry = e;
+
+ starred = new QAction(tr("Starred"), this);
+ starred->setCheckable(true);
+ starred->setChecked((entry->flags & ENTRY_FLAG_STARRED));
+ menuBar()->addAction(starred);
+
+ keepUnread = new QAction(tr("Keep unread"), this);
+ keepUnread->setCheckable(true);
+ keepUnread->setEnabled((entry->flags & ENTRY_FLAG_LOCKED) == 0);
+ menuBar()->addAction(keepUnread);
+
+ menuBar()->addAction(tr("See original"), this, SLOT(seeOriginal()));
+
+ setWindowTitle(entry->title);
+
+ GraphicsView *gv = new GraphicsView();
+ webview = new QGraphicsWebView();
+ gv->viewportItem()->setWidget(webview);
+
+ /* TODO: Configurable text size ?? */
+ webview->settings()->setFontSize(QWebSettings::MinimumFontSize, 22);
+
+ connect(webview, SIGNAL(loadFinished(bool)), SLOT(loadFinished(bool)));
+ connect(webview, SIGNAL(loadStarted()), SLOT(loadStarted()));
+
+ webview->setHtml(entry->content);
+
+ setCentralWidget(gv);
+}
+
+ContentWindow::~ContentWindow() {
+ delete(webview);
+}
+
+void ContentWindow::loadStarted() {
+ setAttribute(Qt::WA_Maemo5ShowProgressIndicator, true);
+}
+
+void ContentWindow::loadFinished(bool) {
+ setAttribute(Qt::WA_Maemo5ShowProgressIndicator, false);
+}
+
+void ContentWindow::seeOriginal() {
+ /* Attempt to launch external browser */
+ if(!QDesktopServices::openUrl(entry->link))
+ webview->setUrl(entry->link); /* Failed... Show inline */
+}
+
+void ContentWindow::closeEvent(QCloseEvent *event) {
+ entry->markRead(!keepUnread->isChecked());
+ entry->markStar(starred->isChecked());
+ QMainWindow::closeEvent(event);
+}
+
+#include "contentwindow.moc"
--- /dev/null
+#ifndef _CONTENT_WINDOW_H
+#define _CONTENT_WINDOW_H
+
+#include <QMainWindow>
+#include <QWebView>
+#include <QtGui>
+#include <QtWebKit>
+#include "googlereader.h"
+
+class ContentWindow : public QMainWindow {
+ Q_OBJECT
+
+ public:
+ ContentWindow(QWidget *parent = 0, Entry *e = 0);
+ virtual ~ContentWindow();
+ void closeEvent(QCloseEvent *event);
+
+ public slots:
+ void loadFinished(bool);
+ void loadStarted();
+ void seeOriginal();
+
+ private:
+ Entry *entry;
+ QGraphicsWebView *webview;
+ QAction *starred;
+ QAction *keepUnread;
+};
+
+#endif
+
--- /dev/null
+#include <QMenuBar>
+#include <QDebug>
+#include <QPainter>
+#include "entrieswindow.h"
+#include "contentwindow.h"
+
+EntriesWindow::EntriesWindow(QWidget *parent, Feed *f) : QMainWindow(parent) {
+ setAttribute(Qt::WA_Maemo5StackedWindow);
+
+ feed = f;
+
+ menuBar()->addAction(tr("Fetch more"), this, SLOT(sync()));
+ menuBar()->addAction(tr("Mark all as read"), this, SLOT(markRead()));
+
+ QActionGroup *filter_group = new QActionGroup(this);
+ show_all = new QAction(tr("Show All"), filter_group);
+ show_all->setCheckable(true);
+ show_all->setChecked(true);
+ show_updated = new QAction(tr("Show Updated"), filter_group);
+ show_updated->setCheckable(true);
+ menuBar()->addActions(filter_group->actions());
+
+ setWindowTitle(f->title);
+
+ list = new QListView();
+ setCentralWidget(list);
+ list->setItemDelegate(new EntryListDelegate(this));
+
+ connect(list, SIGNAL(activated(const QModelIndex &)),
+ SLOT(entrySelected(const QModelIndex &)));
+
+ connect(feed, SIGNAL(updateFeedComplete()),
+ SLOT(entriesUpdated()));
+
+ if(feed->getEntriesSize() == 0 || feed->lastUpdated < feed->reader->lastUpdated)
+ sync();
+ else
+ entriesUpdated();
+}
+
+EntriesWindow::~EntriesWindow() {
+}
+
+void EntriesWindow::sync() {
+ setAttribute(Qt::WA_Maemo5ShowProgressIndicator, true);
+ feed->fetch(feed->lastUpdated > feed->reader->lastUpdated);
+}
+
+void EntriesWindow::markRead() {
+ feed->markRead();
+}
+
+void EntriesWindow::entriesUpdated() {
+ QList<Entry *>entries = feed->getEntries();
+ QAbstractItemModel *old_model = list->model();
+ EntryListModel *new_model = new EntryListModel(this, entries, show_updated->isChecked());
+ list->setModel(new_model);
+ connect(show_updated, SIGNAL(toggled(bool)), new_model, SLOT(showUpdated(bool)));
+ delete(old_model);
+ setAttribute(Qt::WA_Maemo5ShowProgressIndicator, false);
+}
+
+void EntriesWindow::entrySelected(const QModelIndex &index) {
+ Entry *e = qVariantValue<Entry *>(index.data());
+
+ ContentWindow *w = new ContentWindow(this, e);
+ w->show();
+}
+
+int EntryListModel::rowCount(const QModelIndex &) const {
+ int size = 0;
+
+ if(show_updated) {
+ for(int i = 0; i < entry_list.size(); i++) {
+ if((entry_list.at(i)->flags & ENTRY_FLAG_READ) == 0)
+ size++;
+ }
+ }
+ else
+ size = entry_list.size();
+
+ return size;;
+}
+
+QVariant EntryListModel::data(const QModelIndex &index, int role) const {
+ if(!index.isValid())
+ return QVariant();
+
+ if(index.row() >= rowCount() || index.row() < 0) {
+ return QVariant();
+ }
+
+ if(role == Qt::DisplayRole) {
+ if(show_updated) {
+ int i, j;
+ for(i = 0, j = 0; i < entry_list.size(); i++) {
+ if((entry_list.at(i)->flags & ENTRY_FLAG_READ) == 0) j++;
+ if(j > index.row())
+ return qVariantFromValue(entry_list.at(i));
+ }
+ }
+ else
+ return qVariantFromValue(entry_list.at(index.row()));
+ }
+
+ return QVariant();
+}
+
+void EntryListModel::showUpdated(bool updated) {
+ beginResetModel();
+ show_updated = updated;
+ endResetModel();
+}
+
+void EntryListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const {
+
+ QStyledItemDelegate::paint(painter, option, index);
+
+ Entry *e = qVariantValue<Entry *>(index.data());
+
+ QFont font = option.font;
+ QRect rect = option.rect;
+ rect.adjust(20, 8, -20, -8);
+ QPoint topleft = rect.topLeft();
+ topleft.ry() += 2;
+ rect.adjust(36, 0, 0, 0);
+
+ painter->save();
+
+ if(((e->flags & ENTRY_FLAG_READ) == 0) &&
+ !(option.state & QStyle::State_Selected)) {
+ painter->setPen(option.palette.highlight().color());
+ }
+
+ painter->drawText(rect, Qt::AlignTop | Qt::AlignLeft, e->title);
+
+ painter->setPen(option.palette.mid().color());
+ font.setPointSizeF(font.pointSizeF() * 0.70);
+ painter->setFont(font);
+
+ painter->drawText(rect, Qt::AlignBottom | Qt::AlignLeft, e->author);
+
+ painter->drawText(rect, Qt::AlignBottom | Qt::AlignRight, e->published.toString());
+
+ if(e->flags & ENTRY_FLAG_STARRED) {
+ QImage img = QImage(QLatin1String(":/images/star-1"));
+ painter->drawImage(topleft, img);
+ }
+
+ painter->restore();
+}
+
--- /dev/null
+#ifndef _ENTRIES_WINDOW_H
+#define _ENTRIES_WINDOW_H
+
+#include <QMainWindow>
+#include <QListView>
+#include <QStyledItemDelegate>
+#include "googlereader.h"
+
+class EntriesWindow : public QMainWindow {
+ Q_OBJECT
+
+ public:
+ EntriesWindow(QWidget *parent = 0, Feed *f = 0);
+ virtual ~EntriesWindow();
+
+ private slots:
+ void sync();
+ void markRead();
+ void entriesUpdated();
+ void entrySelected(const QModelIndex &);
+
+ private:
+ QListView *list;
+ QAction *show_all;
+ QAction *show_updated;
+ Feed *feed;
+};
+
+class EntryListModel : public QAbstractListModel {
+ Q_OBJECT
+
+ public:
+ EntryListModel(QObject *parent = 0, QList<Entry *>list = QList<Entry *>(), bool updated = false)
+ : QAbstractListModel(parent) {
+ entry_list = list;
+ show_updated = updated;
+ }
+
+ int rowCount(const QModelIndex &model = QModelIndex()) const;
+
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
+
+ public slots:
+ void showUpdated(bool);
+
+ private:
+ QList<Entry *> entry_list;
+ bool show_updated;
+};
+
+class EntryListDelegate : public QStyledItemDelegate {
+ Q_OBJECT
+
+ public:
+ EntryListDelegate(QObject *parent = 0)
+ : QStyledItemDelegate(parent) {};
+
+ void paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const;
+};
+
+#endif
+
--- /dev/null
+#include <QMenuBar>
+#include <QDebug>
+#include <QPainter>
+#include <QHBoxLayout>
+#include <QMaemo5InformationBox>
+#include <QMessageBox>
+#include "feedswindow.h"
+#include "entrieswindow.h"
+
+#define APPNAME "grr"
+#define APPVERSION "0.0.2"
+
+FeedsWindow::FeedsWindow(QWidget *) : QMainWindow(0) {
+ setAttribute(Qt::WA_Maemo5StackedWindow);
+
+ setWindowIcon(QIcon("/usr/share/icons/hicolor/64x64/apps/grr.png"));
+
+ settings = new QSettings("dufo.be", "grr");
+
+ menuBar()->addAction(tr("Settings"), this, SLOT(showSettings()));
+ menuBar()->addAction(tr("Sync now"), this, SLOT(sync()));
+ menuBar()->addAction(tr("About"), this, SLOT(about()));
+
+ QActionGroup *filter_group = new QActionGroup(this);
+ show_all = new QAction(tr("Show All"), filter_group);
+ show_all->setCheckable(true);
+ show_updated= new QAction(tr("Show Updated"), filter_group);
+ show_updated->setCheckable(true);
+
+ if(settings->value("show_updated", false).toBool())
+ show_updated->setChecked(true);
+ else
+ show_all->setChecked(true);
+
+ menuBar()->addActions(filter_group->actions());
+
+ setWindowTitle(APPNAME);
+
+ list = new QListView();
+ setCentralWidget(list);
+ list->setItemDelegate(new FeedListDelegate(this));
+
+ connect(list, SIGNAL(activated(const QModelIndex &)),
+ SLOT(feedSelected(const QModelIndex &)));
+
+ reader = new GoogleReader();
+
+ connect(reader, SIGNAL(updateUnreadComplete()), SLOT(feedsUpdated()));
+ connect(reader, SIGNAL(allReadChanged()), SLOT(refreshModel()));
+ connect(reader, SIGNAL(loginFailed(QString)), SLOT(loginFailed(QString)));
+
+ connect(show_updated, SIGNAL(toggled(bool)), SLOT(showUpdated(bool)));
+
+ if((settings->value("login", "").toString() == "") ||
+ (settings->value("passwd", "").toString() == "")) {
+ emit loginFailed("Incomplete username or password");
+ }
+ else {
+ reader->setLogin(settings->value("login", "").toString());
+ reader->setPasswd(settings->value("passwd", "").toString());
+ sync();
+ }
+}
+
+FeedsWindow::~FeedsWindow() {
+}
+
+void FeedsWindow::showSettings() {
+ SettingsDialog settingsDialog(this, settings);
+ if(settingsDialog.exec() == QDialog::Accepted) {
+ reader->logOut();
+ reader->setLogin(settings->value("login", "").toString());
+ reader->setPasswd(settings->value("passwd", "").toString());
+ sync();
+ }
+}
+
+void FeedsWindow::sync() {
+ setAttribute(Qt::WA_Maemo5ShowProgressIndicator, true);
+ reader->updateSubscriptions();
+ reader->updateUnread();
+}
+
+void FeedsWindow::refreshModel() {
+ QList<Feed *>feeds = reader->getFeeds();
+ QAbstractItemModel *old_model = list->model();
+ FeedListModel *new_model = new FeedListModel(this, feeds, show_updated->isChecked());
+ list->setModel(new_model);
+ connect(show_updated, SIGNAL(toggled(bool)), new_model, SLOT(showUpdated(bool)));
+ delete(old_model);
+}
+
+void FeedsWindow::feedsUpdated() {
+ refreshModel();
+ setAttribute(Qt::WA_Maemo5ShowProgressIndicator, false);
+}
+
+void FeedsWindow::feedSelected(const QModelIndex &index) {
+ Feed *f = qVariantValue<Feed *>(index.data());
+ EntriesWindow *w = new EntriesWindow(this, f);
+ w->show();
+}
+
+void FeedsWindow::showUpdated(bool updated) {
+ settings->setValue("show_updated", updated);
+}
+
+void FeedsWindow::loginFailed(QString message) {
+ setAttribute(Qt::WA_Maemo5ShowProgressIndicator, false);
+ QMaemo5InformationBox::information(this, message, QMaemo5InformationBox::DefaultTimeout);
+ showSettings();
+}
+
+void FeedsWindow::about() {
+ QMessageBox::about(this, APPNAME " " APPVERSION, "");
+}
+
+int FeedListModel::rowCount(const QModelIndex &) const {
+ int size = 0;
+
+ if(show_updated) {
+ for(int i = 0; i < feed_list.size(); i++) {
+ if(feed_list.at(i)->unread || feed_list.at(i)->special)
+ size++;
+ }
+ }
+ else
+ size = feed_list.size();
+
+ return size;;
+}
+
+QVariant FeedListModel::data(const QModelIndex &index, int role) const {
+ if(!index.isValid())
+ return QVariant();
+
+ if(index.row() >= rowCount() || index.row() < 0) {
+ return QVariant();
+ }
+
+ if(role == Qt::DisplayRole) {
+ if(show_updated) {
+ int i, j;
+ for(i = 0, j = 0; i < feed_list.size(); i++) {
+ if(feed_list.at(i)->unread || feed_list.at(i)->special) j++;
+ if(j > index.row())
+ return qVariantFromValue(feed_list.at(i));
+ }
+ }
+ else
+ return qVariantFromValue(feed_list.at(index.row()));
+ }
+
+ return QVariant();
+}
+
+void FeedListModel::showUpdated(bool updated) {
+ beginResetModel();
+ show_updated = updated;
+ endResetModel();
+}
+
+void FeedListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const {
+
+ QStyledItemDelegate::paint(painter, option, index);
+
+ Feed *f = qVariantValue<Feed *>(index.data());
+
+ QRect rect = option.rect;
+ rect.adjust(20, 8, -20, -8);
+ QFont font = option.font;
+ int flags = Qt::AlignVCenter;
+
+ painter->save();
+
+ if(f->cat_label != "")
+ flags = Qt::AlignTop;
+
+ if(f->unread) {
+ if(!(option.state & QStyle::State_Selected))
+ painter->setPen(option.palette.highlight().color());
+
+ painter->drawText(rect, flags | Qt::AlignRight, QString("%1").arg(f->unread));
+ }
+
+ painter->drawText(rect, flags | Qt::AlignLeft, f->title);
+
+ if(f->cat_label != "") {
+ painter->setPen(option.palette.mid().color());
+ font.setPointSizeF(font.pointSizeF() * 0.70);
+ painter->setFont(font);
+ painter->drawText(rect, Qt::AlignBottom | Qt::AlignLeft, f->cat_label);
+ }
+
+ painter->restore();
+}
+
+SettingsDialog::SettingsDialog(QWidget *parent, QSettings *s) : QDialog(parent) {
+ settings = s;
+
+ loginLabel = new QLabel(tr("Login:"));
+ loginEdit = new QLineEdit(settings->value("login", "").toString());
+ loginLabel->setBuddy(loginEdit);
+
+ passwdLabel = new QLabel(tr("Password:"));
+ passwdEdit = new QLineEdit(settings->value("passwd", "").toString());
+ passwdEdit->setEchoMode(QLineEdit::PasswordEchoOnEdit);
+ passwdLabel->setBuddy(passwdEdit);
+
+ saveButton = new QPushButton(tr("Save"));
+ saveButton->setDefault(true);
+ cancelButton = new QPushButton(tr("Cancel"));
+ cancelButton->setDefault(false);
+
+ buttonBox = new QDialogButtonBox(Qt::Vertical);
+ buttonBox->addButton(cancelButton, QDialogButtonBox::ActionRole);
+ buttonBox->addButton(saveButton, QDialogButtonBox::ActionRole);
+
+ QHBoxLayout *loginLayout = new QHBoxLayout;
+ loginLayout->addWidget(loginLabel);
+ loginLayout->addWidget(loginEdit);
+
+ QHBoxLayout *passwdLayout = new QHBoxLayout;
+ passwdLayout->addWidget(passwdLabel);
+ passwdLayout->addWidget(passwdEdit);
+
+ QVBoxLayout *leftLayout = new QVBoxLayout;
+ leftLayout->addLayout(loginLayout);
+ leftLayout->addLayout(passwdLayout);
+ leftLayout->addStretch(1);
+
+ QGridLayout *mainLayout = new QGridLayout;
+ mainLayout->setSizeConstraint(QLayout::SetFixedSize);
+ mainLayout->addLayout(leftLayout, 0, 0);
+ mainLayout->addWidget(buttonBox, 0, 1);
+ setLayout(mainLayout);
+
+ setWindowTitle(tr("Settings"));
+
+ connect(saveButton, SIGNAL(clicked()), SLOT(accept()));
+ connect(cancelButton, SIGNAL(clicked()), SLOT(reject()));
+}
+
+void SettingsDialog::accept() {
+ settings->setValue("login", loginEdit->text());
+ settings->setValue("passwd", passwdEdit->text());
+ QDialog::accept();
+}
--- /dev/null
+#ifndef _WINDOW_H
+#define _WINDOW_H
+
+#include <QMainWindow>
+#include <QListView>
+#include <QStyledItemDelegate>
+#include <QLabel>
+#include <QLineEdit>
+#include <QDialogButtonBox>
+#include <QPushButton>
+#include <QDialog>
+#include <QSettings>
+#include "googlereader.h"
+
+class FeedsWindow : public QMainWindow {
+ Q_OBJECT
+
+ public:
+ FeedsWindow(QWidget *parent = 0);
+ virtual ~FeedsWindow();
+
+ private slots:
+ void showUpdated(bool);
+ void showSettings();
+ void sync();
+ void feedsUpdated();
+ void feedSelected(const QModelIndex &);
+ void refreshModel();
+ void loginFailed(QString);
+ void about();
+
+ private:
+ QListView *list;
+ GoogleReader *reader;
+ QAction *show_all;
+ QAction *show_updated;
+ QSettings *settings;
+};
+
+class FeedListModel : public QAbstractListModel {
+ Q_OBJECT
+
+ public:
+ FeedListModel(QObject *parent = 0, QList<Feed *>list = QList<Feed *>(), bool updated = false)
+ : QAbstractListModel(parent) {
+ feed_list = list;
+ show_updated = updated;
+ }
+
+ int rowCount(const QModelIndex &model = QModelIndex()) const;
+
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
+
+ public slots:
+ void showUpdated(bool);
+
+ private:
+ QList<Feed *> feed_list;
+ bool show_updated;
+};
+
+class FeedListDelegate : public QStyledItemDelegate {
+ Q_OBJECT
+
+ public:
+ FeedListDelegate(QObject *parent = 0)
+ : QStyledItemDelegate(parent) {};
+
+ void paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const;
+};
+
+class SettingsDialog : public QDialog {
+ Q_OBJECT
+
+ public:
+ SettingsDialog(QWidget *parent = 0, QSettings *s = 0);
+
+ public slots:
+ void accept();
+
+ private:
+ QLabel *loginLabel;
+ QLineEdit *loginEdit;
+ QLabel *passwdLabel;
+ QLineEdit *passwdEdit;
+ QDialogButtonBox *buttonBox;
+ QPushButton *saveButton;
+ QPushButton *cancelButton;
+ QSettings *settings;
+};
+
+#endif
+
--- /dev/null
+#include <QtWebKit>
+#include <QApplication>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QNetworkCookie>
+#include <QBuffer>
+#include <QTimer>
+#include <QDateTime>
+#include <QDomDocument>
+#include <QDebug>
+
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+
+#include "googlereader.h"
+
+void Feed::updateSubscription(Feed *feed) {
+ title = feed->title;
+ sortid = feed->sortid;
+ firstitemmsec = feed->firstitemmsec;
+ cat_id = feed->cat_id;
+ cat_label = feed->cat_label;
+ subscription_updated = true;
+}
+
+void Feed::fetch(bool cont) {
+ QNetworkRequest request;
+ QByteArray ba = "http://www.google.com/reader/atom/";
+
+ ba.append(QUrl::toPercentEncoding(id));
+ QUrl url = QUrl::fromEncoded(ba);
+
+ if(continuation != "" && cont)
+ url.addEncodedQueryItem("c", continuation.toUtf8());
+
+ request.setUrl(url);
+ reader->getManager()->get(request);
+}
+
+GoogleReader::GoogleReader() {
+ connect(&manager, SIGNAL(finished(QNetworkReply*)),
+ SLOT(downloadFinished(QNetworkReply*)));
+
+ SID = NULL;
+ updateSubscriptionsPending = false;
+ updateUnreadPending = false;
+ SIDPending = false;
+
+ login_url.setUrl("https://www.google.com/accounts/ClientLogin");
+ subscriptions_url.setUrl("http://www.google.com/reader/api/0/subscription/list");
+ unread_url.setUrl("http://www.google.com/reader/api/0/unread-count");
+ edittag_url.setUrl("http://www.google.com/reader/api/0/edit-tag?client=-");
+ token_url.setUrl("http://www.google.com/reader/api/0/token");
+ markallread_url.setUrl("http://www.google.com/reader/api/0/mark-all-as-read?client=-");
+
+ /* Add the virtual 'Starred items' feed */
+ Feed *feed = new Feed(this);
+ feed->id = "user/-/state/com.google/starred";
+ feed->title = "Starred items";
+ feed->special = true;
+ feeds.insert(feed->id, feed);
+ connect(feed, SIGNAL(allReadChanged()), SIGNAL(allReadChanged()));
+}
+
+void GoogleReader::downloadFinished(QNetworkReply *reply) {
+ QUrl url = reply->url();
+
+ /* TODO: Instead of comparing against the url, use the signal from the
+ * QNetworkReply... */
+
+ if (reply->error()) {
+ qDebug() << "Download of" << url << "failed:" << qPrintable(reply->errorString());
+ if(url == login_url) {
+ SIDPending = false;
+ emit loginFailed("Incorrect username or password");
+ }
+ else if(url == edittag_url)
+ getToken();
+ return;
+ }
+ else if(url == login_url) {
+ QByteArray data = reply->readAll();
+ data.remove(0, data.indexOf("SID=", 0) + 4);
+ data.remove(data.indexOf("\n", 0), 1024);
+ SID = strdup(data.data());
+
+ qDebug() << "SID:" << SID;
+
+ manager.cookieJar()->setCookiesFromUrl(
+ QList<QNetworkCookie>() << QNetworkCookie("SID", SID),
+ QUrl("http://www.google.com"));
+
+ SIDPending = false;
+
+ getToken();
+
+ /* TODO: Replace this with a proper state machine */
+ if(updateSubscriptionsPending) {
+ updateSubscriptionsPending = false;
+ updateSubscriptions();
+ }
+ }
+ else if(url == token_url) {
+ token = reply->readAll();
+ qDebug() << "token:" << token;
+ }
+ else if(url == subscriptions_url) {
+ QByteArray data = reply->readAll();
+ QDomDocument dom;
+ dom.setContent(data);
+ parseSubscriptions(dom);
+ emit updateSubscriptionsComplete();
+
+ /* TODO: Replace this with a proper state machine */
+ if(updateUnreadPending) {
+ updateUnreadPending = false;
+ updateUnread();
+ }
+ }
+ else if(url == unread_url) {
+ QByteArray data = reply->readAll();
+ QDomDocument dom;
+ dom.setContent(data);
+ parseUnread(dom);
+ emit updateUnreadComplete();
+ }
+ else if(url == edittag_url) {
+ QByteArray data = reply->readAll();
+ //qDebug() << "Result:" << data;
+ }
+ else if(url == markallread_url) {
+ QByteArray data = reply->readAll();
+ //qDebug() << "Result:" << data;
+ }
+ else {
+ QByteArray data = reply->readAll();
+ QDomDocument dom;
+ dom.setContent(data);
+ parseFeed(dom);
+ }
+
+ reply->deleteLater();
+}
+
+void GoogleReader::parseFeed(QDomDocument dom) {
+ QDomElement set, e;
+ QDomNode n, c;
+ QString continuation, feedsource;
+ Feed *feed = NULL;
+
+ set = dom.firstChildElement();
+
+ for(n = set.firstChild(); !n.isNull(); n = n.nextSibling()) {
+ e = n.toElement();
+ QString name = e.tagName();
+ if(name == "entry") {
+ Entry entry;
+ QString content, summary;
+
+ QString locked = e.attribute("gr:is-read-state-locked", "false");
+ if(locked == "true")
+ entry.flags |= ENTRY_FLAG_LOCKED | ENTRY_FLAG_READ;
+
+ entry.crawled = e.attribute("gr:crawl-timestamp-msec", "0").toULongLong();
+
+ for(c = n.firstChild(); !c.isNull(); c = c.nextSibling()) {
+ e = c.toElement();
+ name = e.tagName();
+ if(name == "id")
+ entry.id = e.text();
+ else if(name == "title") {
+ QWebPage p;
+ p.mainFrame()->setHtml(e.text());
+ entry.title = p.mainFrame()->toPlainText();
+ }
+ else if(name == "published")
+ entry.published = QDateTime::fromString(e.text(), "yyyy-MM-dd'T'HH:mm:ss'Z'");
+ else if(name == "link")
+ entry.link = QUrl(e.attribute("href", ""));
+ else if(name == "source")
+ entry.source = e.attribute("gr:stream-id", "");
+ else if(name == "content")
+ content = e.text();
+ else if(name == "summary")
+ summary = e.text();
+ else if(name == "author") {
+ e = c.firstChild().toElement();
+ entry.author = e.text();
+ if(entry.author == "(author unknown)")
+ entry.author = "";
+ }
+ else if(name == "category") {
+ QString label = e.attribute("label", "");
+ if(label == "read")
+ entry.flags |= ENTRY_FLAG_READ;
+ else if(label == "starred")
+ entry.flags |= ENTRY_FLAG_STARRED;
+ }
+ }
+
+ if(content != "")
+ entry.content = content;
+ else if(summary != "")
+ entry.content = summary;
+
+ if(!feed)
+ feed = feeds.value(feedsource == "" ? entry.source : feedsource);
+
+ if(feed) {
+ entry.feed = feed;
+ feed->addEntry(new Entry(entry));
+ }
+ }
+ else if(name == "gr:continuation") {
+ continuation = e.text();
+ }
+ else if(name == "id") {
+ if(e.text().endsWith("/state/com.google/starred"))
+ feedsource = "user/-/state/com.google/starred";
+ }
+ }
+
+ if(feed) {
+ feed->lastUpdated = QDateTime::currentDateTime();
+ feed->continuation = continuation;
+ feed->signalUpdated();
+ }
+}
+
+void GoogleReader::parseSubscriptions(QDomDocument dom) {
+ QDomElement set, e;
+ QDomNode n, c;
+
+ /* Clear the subscription updated flag */
+ QHash<QString, Feed *>::iterator i;
+ for(i = feeds.begin(); i != feeds.end(); ++i)
+ i.value()->subscription_updated = false;
+
+ set = dom.firstChildElement();
+ set = set.firstChildElement("list");
+ set = set.firstChildElement("object");
+
+ for (; !set.isNull(); set = set.nextSiblingElement("object")) {
+ Feed *feed = new Feed(this);
+ Feed *existing_feed;
+
+ for(n = set.firstChild(); !n.isNull(); n = n.nextSibling()) {
+ e = n.toElement();
+ QString name = e.attribute("name");
+ if(name == "id")
+ feed->id = e.text();
+ else if(name == "title")
+ feed->title = e.text();
+ else if(name == "sortid")
+ feed->sortid = e.text();
+ else if(name == "firstitemmsec")
+ feed->firstitemmsec = e.text();
+ else if(name == "categories") {
+ for(c = n.firstChild().firstChild(); !c.isNull(); c = c.nextSibling()) {
+ e = c.toElement();
+ QString name = e.attribute("name");
+ if(name == "id")
+ feed->cat_id = e.text();
+ else if(name == "label")
+ feed->cat_label = e.text();
+ }
+ }
+ }
+
+ existing_feed = feeds.value(feed->id);
+ if(existing_feed) {
+ existing_feed->updateSubscription(feed);
+ delete(feed);
+ feed = existing_feed;
+
+ }
+ else {
+ feed->subscription_updated = true;
+ feeds.insert(feed->id, feed);
+ connect(feed, SIGNAL(allReadChanged()), SIGNAL(allReadChanged()));
+ }
+ }
+
+ /* Delete feeds no longer subscribed to */
+ for(i = feeds.begin(); i != feeds.end(); ++i) {
+ if(i.value()->subscription_updated == false && i.value()->special == false) {
+ printf("DELETED: %s\n", i.value()->title.toLatin1().data());
+ i = feeds.erase(i);
+ }
+ }
+
+ lastUpdated = QDateTime::currentDateTime();
+}
+
+void GoogleReader::parseUnread(QDomDocument dom) {
+ QDomElement set, e;
+ QDomNode n, c;
+
+ set = dom.firstChildElement();
+ set = set.firstChildElement("list");
+ set = set.firstChildElement("object");
+
+ for (; !set.isNull(); set = set.nextSiblingElement("object")) {
+ QString id;
+ int count = 0;
+ ulong newestitem = 0;
+
+ for(n = set.firstChild(); !n.isNull(); n = n.nextSibling()) {
+ e = n.toElement();
+ QString name = e.attribute("name");
+ if(name == "id")
+ id = e.text();
+ else if(name == "count")
+ count = e.text().toInt();
+ else if(name == "newestItemTimestampUsec")
+ newestitem = e.text().toULong();
+ }
+
+ Feed *f = feeds.value(id);
+ if(f) {
+ f->unread = count;
+ f->newestitem = newestitem;
+ //qDebug() << f->title << "->" << count;
+ }
+ else {
+ //printf("%s not found\n", id.toLatin1().data());
+ }
+ }
+
+ lastUpdated = QDateTime::currentDateTime();
+}
+
+void GoogleReader::getSID() {
+
+ if(SIDPending)
+ return;
+
+ SIDPending = true;
+
+ QNetworkRequest request;
+ request.setUrl(login_url);
+
+ buffer.open(QBuffer::ReadWrite | QBuffer::Truncate);
+ buffer.write("Email=");
+ buffer.write(QUrl::toPercentEncoding(login));
+ buffer.write("&Passwd=");
+ buffer.write(QUrl::toPercentEncoding(passwd));
+ buffer.write("&service=reader&source=grms&continue=http://www.google.com");
+
+ //buffer.seek(0);
+ //qDebug() << buffer.readAll();
+
+ buffer.seek(0);
+ manager.post(request, &buffer);
+}
+
+void GoogleReader::getToken() {
+ QNetworkRequest request;
+ request.setUrl(token_url);
+ manager.get(request);
+}
+
+void GoogleReader::updateSubscriptions() {
+ QNetworkRequest request;
+
+ if(updateSubscriptionsPending)
+ return;
+
+ if(!SID) {
+ updateSubscriptionsPending = true;
+ getSID();
+ return;
+ }
+
+ request.setUrl(subscriptions_url);
+ manager.get(request);
+}
+
+void GoogleReader::updateUnread() {
+ QNetworkRequest request;
+
+ if(updateUnreadPending)
+ return;
+
+ if(!SID) {
+ updateUnreadPending = true;
+ getSID();
+ return;
+ }
+
+ request.setUrl(unread_url);
+ manager.get(request);
+}
+
+static bool compareFeedItems(const Feed *f1, const Feed *f2) {
+ if(f1->special && !f2->special)
+ return true;
+
+ if(f2->special && !f1->special)
+ return false;
+
+ if(f1->cat_label == f2->cat_label)
+ return f1->title.toLower() < f2->title.toLower();
+
+ if(f1->cat_label.length() == 0)
+ return false;
+
+ if(f2->cat_label.length() == 0)
+ return true;
+
+ return f1->cat_label.toLower() < f2->cat_label.toLower();
+}
+
+QList<Feed *> GoogleReader::getFeeds() {
+ QList<Feed *> list = feeds.values();
+ qSort(list.begin(), list.end(), compareFeedItems);
+ return list;
+}
+
+void Feed::addEntry(Entry *entry) {
+ entries.insert(entry->id, entry);
+}
+
+void Feed::delEntry(Entry *entry) {
+ entries.remove(entry->id);
+}
+
+void Feed::signalUpdated() {
+ // TODO: Clean this up
+ emit updateFeedComplete();
+}
+
+void Feed::updateUnread(int i) {
+ bool allRead = (unread == 0);
+
+ unread += i;
+ if(unread <= 0) unread = 0;
+
+ if(allRead != (unread == 0))
+ emit allReadChanged();
+}
+
+void Feed::markRead() {
+ if(unread > 0) {
+ /* Mark all the remaining items read */
+
+ QNetworkRequest request;
+ request.setUrl(reader->markallread_url);
+
+ buffer.open(QBuffer::ReadWrite | QBuffer::Truncate);
+ buffer.write("s=");
+ buffer.write(QUrl::toPercentEncoding(id));
+ //buffer.write("&ts=");
+ //buffer.write(QByteArray::number(oldest));
+ buffer.write("&T=");
+ buffer.write(QUrl::toPercentEncoding(reader->token));
+
+ //buffer.seek(0);
+ //qDebug() << buffer.readAll();
+
+ buffer.seek(0);
+ reader->manager.post(request, &buffer);
+
+ unread = 0;
+
+ /* Go over all the entries and mark them read */
+ QHash<QString, Entry *>::iterator i;
+ for(i = entries.begin(); i != entries.end(); ++i)
+ i.value()->flags |= ENTRY_FLAG_READ | ENTRY_FLAG_LOCKED;
+ }
+
+ emit allReadChanged();
+}
+
+static bool compareEntryItems(const Entry *e1, const Entry *e2) {
+ return e1->published > e2->published;
+}
+
+QList<Entry *> Feed::getEntries() {
+ QList<Entry *> list = entries.values();
+ qSort(list.begin(), list.end(), compareEntryItems);
+ return list;
+}
+
+/* TODO: Remove the duplicate code in changing stated */
+
+void Entry::markRead(bool mark_read) {
+ /* Check if the read flag differs from the requested state */
+ if(((flags & ENTRY_FLAG_READ) != 0) == mark_read)
+ return;
+
+ /* Cannot mark an item unread if it's locked */
+ if((flags & ENTRY_FLAG_LOCKED) && !mark_read)
+ return;
+
+ QNetworkRequest request;
+ request.setUrl(feed->reader->edittag_url);
+
+ postread.open(QBuffer::ReadWrite | QBuffer::Truncate);
+ postread.write("i=");
+ postread.write(QUrl::toPercentEncoding(id));
+ if(mark_read)
+ postread.write("&a=");
+ else
+ postread.write("&r=");
+ postread.write(QUrl::toPercentEncoding("user/-/state/com.google/read"));
+ postread.write("&ac=edit-tags&T=");
+ postread.write(QUrl::toPercentEncoding(feed->reader->token));
+ postread.seek(0);
+ feed->reader->manager.post(request, &postread);
+
+ feed->updateUnread(mark_read ? -1 : 1);
+
+ if(mark_read)
+ flags |= ENTRY_FLAG_READ;
+ else
+ flags &= ~ENTRY_FLAG_READ;
+}
+
+void Entry::markStar(bool mark_star) {
+ /* Check if the starred flag differs from the requested state */
+ if(((flags & ENTRY_FLAG_STARRED) != 0) == mark_star)
+ return;
+
+ QNetworkRequest request;
+ request.setUrl(feed->reader->edittag_url);
+
+ poststar.open(QBuffer::ReadWrite | QBuffer::Truncate);
+ poststar.write("i=");
+ poststar.write(QUrl::toPercentEncoding(id));
+ if(mark_star)
+ poststar.write("&a=");
+ else
+ poststar.write("&r=");
+ poststar.write(QUrl::toPercentEncoding("user/-/state/com.google/starred"));
+ poststar.write("&ac=edit-tags&T=");
+ poststar.write(QUrl::toPercentEncoding(feed->reader->token));
+ poststar.seek(0);
+ feed->reader->manager.post(request, &poststar);
+
+ Feed *starred = feed->reader->feeds.value("user/-/state/com.google/starred");
+
+ if(mark_star) {
+ starred->addEntry(this);
+ flags |= ENTRY_FLAG_STARRED;
+ }
+ else {
+ starred->delEntry(this);
+ flags &= ~ENTRY_FLAG_STARRED;
+ }
+}
--- /dev/null
+#ifndef _QTMAIN_H
+#define _QTMAIN_H
+
+#include <QObject>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QDomDocument>
+#include <QBuffer>
+#include <QDateTime>
+
+class GoogleReader;
+class Entry;
+
+class Feed : public QObject {
+ Q_OBJECT
+
+ public:
+ bool subscription_updated;
+
+ QString id;
+ QString title;
+ QString sortid;
+ QString firstitemmsec;
+ QString cat_id;
+ QString cat_label;
+
+ int unread;
+ uint newestitem;
+
+ QString continuation;
+
+ QDateTime lastUpdated;
+
+ GoogleReader *reader;
+
+ bool special;
+
+ void updateSubscription(Feed *feed);
+
+ Feed(GoogleReader *gr = NULL) : QObject() {
+ unread = 0;
+ reader = gr;
+ special = false;
+ }
+
+ void fetch(bool);
+ void addEntry(Entry *);
+ void delEntry(Entry *);
+ void signalUpdated(); // TODO: Clean this up...
+ QList<Entry *> getEntries();
+ int getEntriesSize() { return entries.size(); }
+ void markRead();
+ void updateUnread(int);
+
+ signals:
+ void updateFeedComplete();
+ void allReadChanged();
+
+ private:
+ QHash<QString, Entry *> entries;
+ QBuffer buffer;
+};
+
+Q_DECLARE_METATYPE(Feed *)
+
+#define ENTRY_FLAG_READ 0x00000001
+#define ENTRY_FLAG_STARRED 0x00000002
+#define ENTRY_FLAG_LOCKED 0x00000004
+
+class Entry : public QObject {
+ Q_OBJECT
+
+ public:
+ QString id;
+ QString title;
+ QString author;
+ QDateTime published;
+ qulonglong crawled;
+ QUrl link;
+ QString source;
+ QString content;
+ unsigned int flags;
+ Feed *feed;
+
+ Entry(Feed *f = NULL) : QObject() {
+ feed = f;
+ flags = 0;
+ }
+
+ Entry(Entry &e) : QObject() {
+ id = e.id;
+ title = e.title;
+ author = e.author;
+ published = e.published;
+ link = e.link;
+ source = e.source;
+ content = e.content;
+ flags = e.flags;
+ feed = e.feed;
+ }
+
+ void markRead(bool);
+ void markStar(bool);
+
+ private:
+ QBuffer postread;
+ QBuffer poststar;
+};
+
+Q_DECLARE_METATYPE(Entry *)
+
+class GoogleReader: public QObject {
+ Q_OBJECT
+
+ public:
+ GoogleReader();
+ void updateSubscriptions();
+ void updateUnread();
+ QList<Feed *> getFeeds();
+ QNetworkAccessManager *getManager() {
+ return &manager;
+ }
+ void getToken();
+
+ QUrl edittag_url;
+ QUrl markallread_url;
+ QByteArray token;
+ QNetworkAccessManager manager;
+ QDateTime lastUpdated;
+ QHash<QString, Feed *> feeds;
+
+ void setLogin(QString l) { login = l; }
+ void setPasswd(QString p) { passwd = p; }
+ void logOut() {
+ SID = NULL;
+ token = NULL;
+ updateSubscriptionsPending = false;
+ updateUnreadPending = false;
+ SIDPending = false;
+ }
+
+ private slots:
+ void downloadFinished(QNetworkReply *reply);
+
+ private:
+ char *SID;
+ QBuffer buffer;
+ bool updateSubscriptionsPending;
+ bool updateUnreadPending;
+ bool SIDPending;
+
+ void getSID();
+ void parseSubscriptions(QDomDocument dom);
+ void parseUnread(QDomDocument dom);
+ void parseFeed(QDomDocument dom);
+
+ QUrl login_url;
+ QUrl subscriptions_url;
+ QUrl unread_url;
+ QUrl token_url;
+
+ QString login;
+ QString passwd;
+
+ signals:
+ void updateSubscriptionsComplete();
+ void updateUnreadComplete();
+ void allReadChanged();
+ void loginFailed(QString);
+};
+
+#endif
+
--- /dev/null
+[Desktop Entry]
+Encoding=UTF-8
+Version=0.0.2
+Type=Application
+Name=grr
+Exec=/opt/grr/grr
+Icon=grr
+X-HildonDesk-ShowInToolbar=true
+X-Osso-Type=application/x-executable
+
--- /dev/null
+<!DOCTYPE RCC><RCC version="1.0">
+ <qresource>
+ <file>images/star-0.png</file>
+ <file>images/star-1.png</file>
+ </qresource>
+</RCC>
+
--- /dev/null
+#include <QApplication>
+
+#include "feedswindow.h"
+
+int main(int argc, char **argv) {
+
+ QApplication app(argc, argv);
+ FeedsWindow feed_window;
+ feed_window.show();
+
+ return app.exec();
+}
+