moving on to v2 in C++ and QML

This commit is contained in:
Jeena 2015-02-18 00:05:13 +01:00
commit b456f588fc
28 changed files with 1255 additions and 766 deletions

7
.gitignore vendored
View file

@ -1,6 +1,3 @@
build
dist
.DS_Store
FeedTheMonkey.app
FeedTheMonkey.egg-info/
feedthemonkey.egg-info/
FeedTheMonkey.pro.user*

91
Content.qml Normal file
View file

@ -0,0 +1,91 @@
import QtWebKit 3.0
import QtWebKit.experimental 1.0
import QtQuick 2.0
import QtQuick.Controls 1.3
import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.3
import QtQuick.Controls 1.3
import TTRSS 1.0
ScrollView {
id: content
property Post post
property ApplicationWindow app
property int textFontSize: 14
property int scrollJump: 48
property int pageJump: parent.height
Layout.minimumWidth: 400
onTextFontSizeChanged: webView.setDefaults()
style: ScrollViewStyle {
transientScrollBars: true
}
function scrollDown(jump) {
if(!jump) {
webView.experimental.evaluateJavaScript("window.scrollTo(0, document.body.scrollHeight - " + height + ");")
} else {
webView.experimental.evaluateJavaScript("window.scrollBy(0, " + jump + ");")
}
}
function scrollUp(jump) {
if(!jump) {
webView.experimental.evaluateJavaScript("window.scrollTo(0, 0);")
} else {
webView.experimental.evaluateJavaScript("window.scrollBy(0, -" + jump + ");")
}
}
function loggedOut() {
post = null
}
Label { id: fontLabel }
WebView {
id: webView
url: "content.html"
// Enable communication between QML and WebKit
experimental.preferences.navigatorQtObjectEnabled: true;
property Post post: content.post
function setPost() {
if(post) {
experimental.evaluateJavaScript("setArticle(" + post.jsonString + ")")
} else {
experimental.evaluateJavaScript("setArticle('logout')")
}
}
function setDefaults() {
// font name needs to be enclosed in single quotes
experimental.evaluateJavaScript("document.body.style.fontFamily = \"'" + fontLabel.font.family + "'\";");
experimental.evaluateJavaScript("document.body.style.fontSize = '" + content.textFontSize + "pt';");
}
onNavigationRequested: {
if (request.navigationType != WebView.LinkClickedNavigation) {
request.action = WebView.AcceptRequest;
} else {
request.action = WebView.IgnoreRequest;
Qt.openUrlExternally(request.url);
}
}
onLoadingChanged: {
if(loadRequest.status === WebView.LoadSucceededStatus) {
setPost()
setDefaults()
}
}
onPostChanged: setPost()
Keys.onPressed: app.keyPressed(event)
}
}

32
FeedTheMonkey.pro Normal file
View file

@ -0,0 +1,32 @@
TEMPLATE = app
QT += qml quick
CONFIG += c++11
SOURCES += main.cpp \
tinytinyrss.cpp \
tinytinyrsslogin.cpp \
post.cpp
RESOURCES += qml.qrc \
images.qrc \
html.qrc
RC_FILE = Icon.icns
# Needed for bringing browser from background to foreground using QDesktopServices: http://bugreports.qt-project.org/browse/QTBUG-8336
TARGET.CAPABILITY += SwEvent
# Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH =
# Default rules for deployment.
include(deployment.pri)
OTHER_FILES +=
HEADERS += \
tinytinyrss.h \
tinytinyrsslogin.h \
post.h

View file

@ -1,30 +0,0 @@
BSD license
===========
Copyright (c) 2013, Jeena Paradies
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- Neither the name of Bungloo nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

65
Login.qml Normal file
View file

@ -0,0 +1,65 @@
import QtQuick 2.0
import QtQuick.Controls 1.2
Rectangle {
color: "transparent"
anchors.fill: parent
property string serverUrl: serverUrl.text
property string userName: userName.text
property string password: password.text
Column {
anchors.centerIn: parent
width: parent.width / 2
anchors.margins: parent.width / 4
spacing: 10
Text {
text: qsTr("Please specify a server url, a username and a password.")
wrapMode: Text.WordWrap
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 20
font.pointSize: 20
}
TextField {
id: serverUrl
placeholderText: "http://example.com/ttrss/"
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 20
validator: RegExpValidator { regExp: /https?:\/\/.+/ }
onAccepted: login()
}
TextField {
id: userName
placeholderText: qsTr("username")
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 20
onAccepted: login()
}
TextField {
id: password
placeholderText: qsTr("password")
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 20
echoMode: TextInput.Password
onAccepted: login()
}
Button {
id: loginButton
text: "Ok"
anchors.right: parent.right
anchors.margins: 20
onClicked: login()
}
}
}

89
PostListItem.qml Normal file
View file

@ -0,0 +1,89 @@
import QtQuick 2.0
import QtQuick.Controls 1.3
Item {
property int textFontSize: 14
property int smallfontSize: 11
Component.onCompleted: fixFontSize()
onTextFontSizeChanged: fixFontSize()
function fixFontSize() {
smallfontSize = textFontSize * 0.8
}
id: item
height: column.height + 20
width: parent.parent.parent.width
Rectangle {
anchors.fill: parent
color: "transparent"
Rectangle {
anchors.fill: parent
anchors.leftMargin: 15
anchors.rightMargin: 15
anchors.topMargin: 10
anchors.bottomMargin: 10
color: "transparent"
Column {
id: column
width: parent.width
Row {
spacing: 10
Label {
text: feedTitle
font.pointSize: smallfontSize
textFormat: Text.PlainText
color: "gray"
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
renderType: Text.NativeRendering
}
Label {
text: date.toLocaleString(Qt.locale(), Locale.ShortFormat)
font.pointSize: smallfontSize
textFormat: Text.PlainText
color: "gray"
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
renderType: Text.NativeRendering
}
}
Label {
text: title
color: read ? "gray" : "black"
font.pointSize: textFontSize
textFormat: Text.RichText
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
renderType: Text.NativeRendering
width: parent.width
}
Label {
text: excerpt
font.pointSize: smallfontSize
textFormat: Text.RichText
color: "gray"
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
renderType: Text.NativeRendering
width: parent.width
}
}
}
Rectangle {
anchors.top: parent.bottom
width: parent.width
height: 1
color: "lightgray"
}
}
MouseArea {
anchors.fill: parent
onClicked: {
parent.parent.parent.currentIndex = index
}
}
}

View file

@ -1,58 +0,0 @@
<img align=right src="http://jabs.nu/feedthemonkey/feedthemonkey-icon.png" width='256' alt='Icon'>
# Feed the Monkey
Feed the Monkey is a desktop client for [TinyTinyRSS](http://tt-rss.org). That means that it doesn't work as a standalone feed reader but only as a client for the TinyTinyRSS API which it uses to get the normalized feeds and to synchronize the "article read" marks.
It is written in PyQt and uses WebKit to show the contents.
You need to have PyQt installed and a account on a TinyTinyRSS server.
License: BSD
## Installation
### Linux + Windows
Download the [ZIP](https://github.com/jeena/feedthemonkey/archive/master.zip)-file, unzip it and then run:
On Linux you can just do (if you have PyQt4, python2 and python2-autotools already installed):
`sudo python2 setup.py install`
On Windows you need to install (those are links to the binary packages):
- [Python2 x32](http://www.python.org/ftp/python/2.7.4/python-2.7.4.msi) or [Python x64](http://www.python.org/ftp/python/2.7.4/python-2.7.4.amd64.msi)
- [PyQt4 x32](http://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.10.1/PyQt4-4.10.1-gpl-Py2.7-Qt4.8.4-x32.exe) or [PyQt x64](http://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.10.1/PyQt4-4.10.1-gpl-Py2.7-Qt4.8.4-x64.exe)
Then rename `feedthemonkey` to `feedthemonkey.pyw` and then you can run it by double-clicking.
### OS X
Download [FeedTheMonkey.app.zip](http://jabs.nu/feedthemonkey/download/FeedTheMonkey.app.zip) unzip it and move it to your Applications folder. After that just run it like every other app.
## Keyboard shortcuts
The keyboard shortcuts are inspired by other feed readers which are inspired by the text editor vi.
`j` or `→` show nex article
`k` or `←` show previous article
`n` or `Return` open current article in the default browser
`r` reload articles
`s` star current article
`Ctrl S` show starred articles
`Ctrl Q` quit
`Ctrl +` zoom in
`Ctrl -` zoom out
`Ctrl 0` reset zoom
On OS X use `Cmd` instead of `Ctrl`.
## Trivia
I just hacked together this one within one day so it is not feature complete yet and has no real error handling.
Right now it only loads unread articles and shows them one after another. I might add a sidebar in the future, we will see.
## Screenshot
![Feed the Monkey screenshot](http://jabs.nu/feedthemonkey/feedthemonkey-screenshot.png)

71
Sidebar.qml Normal file
View file

@ -0,0 +1,71 @@
import QtQuick 2.0
import TTRSS 1.0
import QtQuick.Controls 1.3
import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.3
ScrollView {
id: item
property Server server
property Content content
property Post previousPost
property int textFontSize: 14
style: ScrollViewStyle {
transientScrollBars: true
}
function next() {
if(listView.count > listView.currentIndex) {
listView.currentIndex++;
}
}
function previous() {
if(listView.currentIndex > 0) {
listView.currentIndex--;
}
}
ListView {
id: listView
focus: true
anchors.fill: parent
spacing: 1
model: item.server.posts
delegate: Component {
PostListItem {
textFontSize: item.textFontSize
}
}
highlightFollowsCurrentItem: false
highlight: Component {
Rectangle {
width: listView.currentItem.width
height: listView.currentItem.height
color: "lightblue"
opacity: 0.5
y: listView.currentItem.y
}
}
onCurrentItemChanged: {
if(previousPost) {
if(!previousPost.dontChangeRead) {
previousPost.read = true;
} else {
previousPost.dontChangeRead = false;
}
}
item.content.post = server.posts[currentIndex]
content.flickableItem.contentY = 0
previousPost = item.content.post
}
}
}

99
TheMenuBar.qml Normal file
View file

@ -0,0 +1,99 @@
import QtQuick.Controls 1.2
import QtQuick 2.0
import TTRSS 1.0
MenuBar {
id: menuBar
property bool loggedIn: false
property ServerLogin serverLogin
property Server server
property Sidebar sidebar
property Content content
property bool visible: true
Menu {
visible: menuBar.visible
title: qsTr("File")
MenuItem {
text: qsTr("Exit")
shortcut: "Ctrl+Q"
onTriggered: Qt.quit()
}
}
Menu {
visible: menuBar.visible
title: qsTr("Action")
MenuItem {
text: qsTr("Reload")
shortcut: "R"
enabled: loggedIn
onTriggered: server.reload()
}
MenuItem {
text: qsTr("Set &Unread")
shortcut: "U"
enabled: loggedIn
onTriggered: {
content.post.dontChangeRead = true
content.post.read = false
}
}
MenuItem {
text: qsTr("Next")
shortcut: "J"
enabled: loggedIn
onTriggered: sidebar.next()
}
MenuItem {
text: qsTr("Previous")
shortcut: "K"
enabled: loggedIn
onTriggered: sidebar.previous()
}
MenuItem {
text: qsTr("Open in Browser")
shortcut: "N"
enabled: loggedIn
onTriggered: Qt.openUrlExternally(content.post.link)
}
MenuItem {
text: qsTr("Log Out")
enabled: loggedIn
onTriggered: serverLogin.logout()
}
}
Menu {
visible: menuBar.visible
title: qsTr("View")
MenuItem {
text: qsTr("Zoom In")
shortcut: "Ctrl++"
enabled: loggedIn
onTriggered: app.zoomIn()
}
MenuItem {
text: qsTr("Zoom Out")
shortcut: "Ctrl+-"
enabled: loggedIn
onTriggered: app.zoomOut()
}
MenuItem {
text: qsTr("Reset")
shortcut: "Ctrl+0"
enabled: loggedIn
onTriggered: app.zoomReset()
}
}
Menu {
visible: menuBar.visible
title: qsTr("Help")
MenuItem {
text: qsTr("About")
onTriggered: Qt.openUrlExternally("http://jabs.nu/feedthemonkey");
}
}
}

View file

@ -1,17 +0,0 @@
#!/bin/bash
HERE=`pwd`
TMP="/tmp"
rm -rf FeedTheMonkey.app
rm -rf $TMP/feedthemonkey
mkdir $TMP/feedthemonkey
cp Icon.icns $TMP/feedthemonkey/
cp setup.py $TMP/feedthemonkey
cp feedthemonkey $TMP/feedthemonkey/
cd $TMP/feedthemonkey
python setup.py py2app
mv $TMP/feedthemonkey/dist/FeedTheMonkey.app $HERE
cd $HERE
rm -rf $TMP/feedthemonkey
FeedTheMonkey.app/Contents/MacOS/FeedTheMonkey

46
content.css Normal file
View file

@ -0,0 +1,46 @@
body {
background: #eee;
font-family: sans-serif;
padding: 1em 1.5em;
font-weight: lighter;
}
h1 {
font-weight: lighter;
font-size: 1.4em;
margin: 0;
padding: 0;
}
#date:not(:empty) {
border-bottom: 1px solid #aaa;
margin-bottom: 1em;
padding-bottom: 1em;
display: block;
}
.starred:after {
content: "*";
}
header p {
color: #aaa;
margin: 0;
padding: 0;
font-size: 0.8em;
}
a {
color: #333;
text-decoration: none;
}
img {
max-width: 100%;
height: auto;
}
article {
line-height: 1.6;
font-size: 0.8em;
}

18
content.html Normal file
View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>TTRSS</title>
<link href="content.css" media="all" rel="stylesheet">
<script type="text/javascript" src="content.js"></script>
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
</head>
<body class=''>
<header>
<p><span id="feed_title"></span> <span id="author"></span></p>
<h1><a id="title" href=""></a></h1>
<p><timedate id="date"></timedate></p>
</header>
<article id="article"></article>
</body>
</html>

49
content.js Normal file
View file

@ -0,0 +1,49 @@
function $(id) {
return document.getElementById(id);
}
function setArticle(article) {
window.scrollTo(0, 0);
$("date").innerHTML = "";
$("title").innerHTML = "";
$("title").href = "";
$("title").title = "";
$("feed_title").innerHTML = "";
$("author").innerHTML = "";
$("article").innerHTML = "";
if(article === "empty") {
$("article").innerHTML = "No unread articles to display.";
} else if(article === "loading") {
$("article").innerHTML = "Loading <blink>&hellip;</blink>";
} else if (article === "logout") {
} else if(article) {
$("date").innerHTML = (new Date(parseInt(article.updated, 10) * 1000));
$("title").innerHTML = article.title;
$("title").href = article.link;
$("title").title = article.link;
$("feed_title").innerHTML = article.feed_title;
$("title").className = article.marked ? "starred" : "";
$("author").innerHTML = "";
if(article.author && article.author.length > 0)
$("author").innerHTML = "&ndash; " + article.author
$("article").innerHTML = article.content;
var as = $("article").getElementsByTagName("a");
for(var i = 0; i <= as.length; i++) {
as[i].target = "";
}
}
}
function setFont(font, size) {
document.body.style.fontFamily = font;
document.body.style.fontSize = size + "pt";
}

27
deployment.pri Normal file
View file

@ -0,0 +1,27 @@
android-no-sdk {
target.path = /data/user/qt
export(target.path)
INSTALLS += target
} else:android {
x86 {
target.path = /libs/x86
} else: armeabi-v7a {
target.path = /libs/armeabi-v7a
} else {
target.path = /libs/armeabi
}
export(target.path)
INSTALLS += target
} else:unix {
isEmpty(target.path) {
qnx {
target.path = /tmp/$${TARGET}/bin
} else {
target.path = /opt/$${TARGET}/bin
}
export(target.path)
}
INSTALLS += target
}
export(INSTALLS)

View file

@ -1,570 +0,0 @@
#!/usr/bin/env python2
try:
import urllib.request as urllib2
except:
import urllib2
import sys, os, json, tempfile, urllib, json
from PyQt4 import QtGui, QtCore, QtWebKit, QtNetwork
from PyQt4.QtNetwork import *
from threading import Thread
from sys import platform as _platform
settings = QtCore.QSettings("jabs.nu", "feedthemonkey")
class MainWindow(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.setWindowIcon(QtGui.QIcon("feedmonkey"))
self.addAction(QtGui.QAction("Full Screen", self, checkable=True, toggled=lambda v: self.showFullScreen() if v else self.showNormal(), shortcut="F11"))
self.history = self.get("history", [])
self.restoreGeometry(QtCore.QByteArray.fromRawData(settings.value("geometry").toByteArray()))
self.restoreState(QtCore.QByteArray.fromRawData(settings.value("state").toByteArray()))
self.initUI()
session_id = self.get("session_id")
server_url = self.get("server_url")
if not (session_id and server_url):
self.authenticate()
else:
self.initApp()
def initUI(self):
self.list = List(self)
self.content = Content(self)
self.splitter = QtGui.QSplitter(QtCore.Qt.Vertical, self)
self.splitter.setHandleWidth(1)
self.splitter.addWidget(self.list)
self.splitter.addWidget(self.content)
self.splitter.restoreState(settings.value("splitterSizes").toByteArray());
self.splitter.splitterMoved.connect(self.splitterMoved)
self.setCentralWidget(self.splitter)
def mkAction(name, connect, shortcut=None):
action = QtGui.QAction(name, self)
action.triggered.connect(connect)
if shortcut:
action.setShortcut(shortcut)
return action
mb = self.menuBar()
fileMenu = mb.addMenu("&File")
fileMenu.addAction(mkAction("&Close", self.close, "Ctrl+W"))
fileMenu.addAction(mkAction("&Log Out", self.logOut))
fileMenu.addSeparator()
fileMenu.addAction(mkAction("&Exit", self.close, "Ctrl+Q"))
actionMenu = mb.addMenu("&Action")
actionMenu.addAction(mkAction("&Reload", self.content.reload, "R"))
actionMenu.addAction(mkAction("Show &Starred", self.content.showStarred, "Ctrl+S"))
actionMenu.addAction(mkAction("Set &Starred", self.content.setStarred, "S"))
actionMenu.addAction(mkAction("Set &Unread", self.content.setUnread, "U"))
actionMenu.addAction(mkAction("&Next", self.content.showNext, "J"))
actionMenu.addAction(mkAction("&Previous", self.content.showPrevious, "K"))
actionMenu.addAction(mkAction("&Open in Browser", self.content.openCurrent, "N"))
viewMenu = mb.addMenu("&View")
viewMenu.addAction(mkAction("Zoom &In", lambda: self.content.wb.setZoomFactor(self.content.wb.zoomFactor() + 0.2), "Ctrl++"))
viewMenu.addAction(mkAction("Zoom &Out", lambda: self.content.wb.setZoomFactor(self.content.wb.zoomFactor() - 0.2), "Ctrl+-"))
viewMenu.addAction(mkAction("&Reset", lambda: self.content.wb.setZoomFactor(1), "Ctrl+0"))
windowMenu = mb.addMenu("&Window")
windowMenu.addAction(mkAction("Reset to Default", self.resetSplitter, "Ctrl+D"))
helpMenu = mb.addMenu("&Help")
helpMenu.addAction(mkAction("&About", lambda: QtGui.QDesktopServices.openUrl(QtCore.QUrl("http://jabs.nu/feedthemonkey", QtCore.QUrl.TolerantMode)) ))
def initApp(self):
session_id = self.get("session_id")
server_url = self.get("server_url")
self.tinyTinyRSS = TinyTinyRSS(self, server_url, session_id)
self.content.evaluateJavaScript("setArticle('loading')")
self.content.reload()
self.show()
def closeEvent(self, ev):
settings.setValue("geometry", self.saveGeometry())
settings.setValue("state", self.saveState())
return QtGui.QMainWindow.closeEvent(self, ev)
def put(self, key, value):
"Persist an object somewhere under a given key"
settings.setValue(key, json.dumps(value))
settings.sync()
def get(self, key, default=None):
"Get the object stored under 'key' in persistent storage, or the default value"
v = settings.value(key)
return json.loads(unicode(v.toString())) if v.isValid() else default
def setWindowTitle(self, t):
super(QtGui.QMainWindow, self).setWindowTitle("Feed the Monkey" + t)
def splitterMoved(self, pos, index):
settings.setValue("splitterSizes", self.splitter.saveState());
def resetSplitter(self):
sizes = self.splitter.sizes()
top = sizes[0]
bottom = sizes[1]
sizes[0] = 200
sizes[1] = bottom + top - 200
self.splitter.setSizes(sizes)
def authenticate(self):
dialog = Login()
def callback():
server_url = str(dialog.textServerUrl.text())
user = str(dialog.textName.text())
password = str(dialog.textPass.text())
session_id = TinyTinyRSS.login(server_url, user, password)
if session_id:
self.put("session_id", session_id)
self.put("server_url", server_url)
self.initApp()
else:
self.authenticate()
dialog.accepted.connect(callback)
dialog.exec_()
def logOut(self):
self.hide()
self.content.evaluateJavaScript("setArticle('logout')")
self.tinyTinyRSS.logOut()
self.tinyTinyRSS = None
self.put("session_id", None)
self.put("server_url", None)
self.authenticate()
class List(QtGui.QTableWidget):
def __init__(self, container):
QtGui.QTableWidget.__init__(self)
self.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
self.app = container
self.itemSelectionChanged.connect(self.rowSelected)
self.setShowGrid(False)
self.setAlternatingRowColors(True)
def initHeader(self):
self.clear()
self.setColumnCount(5)
self.setHorizontalHeaderLabels(("*", "Feed", "Title", "Date", "Author"))
self.horizontalHeader().setResizeMode(0, QtGui.QHeaderView.ResizeToContents)
self.horizontalHeader().setResizeMode(1, QtGui.QHeaderView.ResizeToContents)
self.horizontalHeader().setResizeMode(2, QtGui.QHeaderView.Stretch)
self.horizontalHeader().setResizeMode(3, QtGui.QHeaderView.ResizeToContents)
self.horizontalHeader().setResizeMode(4, QtGui.QHeaderView.ResizeToContents)
self.verticalHeader().hide()
def setItems(self, articles):
self.initHeader()
self.setRowCount(len(articles))
row = 0
for article in articles:
if "marked" in article:
starred = QtGui.QTableWidgetItem("*" if article["marked"] else "")
starred.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.setItem(row, 0, starred)
if "feed_title" in article:
feed_title = QtGui.QTableWidgetItem(article["feed_title"])
feed_title.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.setItem(row, 1, feed_title)
if "title" in article:
title = QtGui.QTableWidgetItem(article["title"])
title.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.setItem(row, 2, title)
if "updated" in article:
date = QtCore.QDateTime.fromTime_t(article["updated"]).toString(QtCore.Qt.SystemLocaleShortDate)
d = QtGui.QTableWidgetItem(date)
d.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.setItem(row, 3, d)
if "author" in article:
author = QtGui.QTableWidgetItem(article["author"])
author.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.setItem(row, 4, author)
self.resizeRowToContents(row)
row += 1
self.selectRow(0)
def rowSelected(self):
indexes = self.selectedIndexes()
if len(indexes) > 0:
row = indexes[0].row()
self.app.content.showIndex(row)
def updateRead(self):
for row, article in enumerate(self.app.content.unread_articles):
for x in xrange(0,5):
item = self.item(row, x)
font = item.font()
font.setBold(article["unread"])
item.setFont(font)
def setStarred(self, index, starred):
widget = self.itemAt(index, 0)
widget.setText("*" if starred else "")
class Content(QtGui.QWidget):
def __init__(self, container):
QtGui.QWidget.__init__(self)
self.app = container
self.index = 0
self.wb = QtWebKit.QWebView(titleChanged=lambda t: container.setWindowTitle(t))
self.wb.page().setLinkDelegationPolicy(QtWebKit.QWebPage.DelegateAllLinks)
self.wb.linkClicked.connect(lambda url: self.openLink(url))
diskCache = QNetworkDiskCache()
diskCache.setCacheDirectory("/tmp/feedmonkey")
diskCache.setMaximumCacheSize(5*124*124)
self.wb.page().networkAccessManager().setCache(diskCache)
self.setLayout(QtGui.QVBoxLayout(spacing=0))
self.layout().setContentsMargins(0, 0, 0, 0)
self.layout().addWidget(self.wb)
self.do_show_next = QtGui.QShortcut(QtCore.Qt.Key_Right, self, activated=self.showNext)
self.do_show_previous = QtGui.QShortcut(QtCore.Qt.Key_Left, self, activated=self.showPrevious)
self.do_open = QtGui.QShortcut("Return", self, activated=self.openCurrent)
self.wb.settings().setAttribute(QtWebKit.QWebSettings.PluginsEnabled, True)
self.wb.settings().setIconDatabasePath(tempfile.mkdtemp())
self.wb.setHtml(self.templateString())
self.unread_articles = []
def openLink(self, url):
QtGui.QDesktopServices.openUrl(url)
def reload(self):
w = WorkerThread(self.app, self._reload)
self.connect(w, QtCore.SIGNAL("reload_done()"), self.reload_done)
w.start()
def showStarred(self):
w = WorkerThread(self.app, self._showStarred)
self.connect(w, QtCore.SIGNAL("reload_done()"), self.reload_done)
w.start()
def setUnread(self):
article = self.unread_articles[self.index]
article["unread"] = True
article["set_unread"] = True
self.app.list.updateRead()
self.app.tinyTinyRSS.setArticleRead(article["id"], False)
def setStarred(self):
article = self.unread_articles[self.index]
article["marked"] = not article["marked"]
self.app.tinyTinyRSS.setArticleStarred(article["id"], article["marked"])
self.app.list.setStarred(self.index, article["marked"])
self.setArticle(article)
def _reload(self):
self.unread_articles = self.app.tinyTinyRSS.getUnreadFeeds()
self.index = -1
def _showStarred(self):
self.unread_articles = self.app.tinyTinyRSS.getStarredFeeds()
self.index = -1
def reload_done(self):
self.setUnreadCount()
self.app.list.setItems(self.unread_articles)
def showIndex(self, index):
if self.index > -1:
previous = self.unread_articles[self.index]
if not "set_unread" in previous or not previous["set_unread"]:
self.app.tinyTinyRSS.setArticleRead(previous["id"])
previous["unread"] = False
else:
previous["set_unread"] = False
self.app.list.updateRead()
self.index = index
current = self.unread_articles[self.index]
self.setArticle(current)
self.setUnreadCount()
def showNext(self):
if self.index + 1 < len(self.unread_articles):
self.app.list.selectRow(self.index + 1)
def showPrevious(self):
if self.index > 0:
self.app.list.selectRow(self.index - 1)
def openCurrent(self):
current = self.unread_articles[self.index]
url = QtCore.QUrl(current["link"])
self.openLink(url)
def setArticle(self, article):
func = u"setArticle({});".format(json.dumps(article))
self.evaluateJavaScript(func)
def evaluateJavaScript(self, func):
return self.wb.page().mainFrame().evaluateJavaScript(func)
def setUnreadCount(self):
length = len(self.unread_articles)
i = 0
if self.index > 0:
i = self.index
unread = length - i
self.app.setWindowTitle(" (" + str(unread) + "/" + str(length) + ")")
if unread < 1:
self.evaluateJavaScript("setArticle('empty')")
def templateString(self):
html="""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ttrssl</title>
<script type="text/javascript">
function $(id) {
return document.getElementById(id);
}
function setArticle(article) {
window.scrollTo(0, 0);
$("date").innerHTML = "";
$("title").innerHTML = "";
$("title").href = "";
$("title").title = "";
$("feed_title").innerHTML = "";
$("author").innerHTML = "";
$("article").innerHTML = "";
if(article == "empty") {
$("article").innerHTML = "No unread articles to display.";
} else if(article == "loading") {
$("article").innerHTML = "Loading <blink>&hellip;</blink>";
} else if (article == "logout") {
} else if(article) {
$("date").innerHTML = (new Date(parseInt(article.updated, 10) * 1000));
$("title").innerHTML = article.title;
$("title").href = article.link;
$("title").title = article.link;
$("feed_title").innerHTML = article.feed_title;
$("title").className = article.marked ? "starred" : "";
$("author").innerHTML = "";
if(article.author && article.author.length > 0)
$("author").innerHTML = "&ndash; " + article.author
$("article").innerHTML = article.content;
}
}
</script>
<style type="text/css">
body {
font-family: "Ubuntu", "Lucida Grande", "Tahoma", sans-serif;
padding: 1em 2em 1em 2em;
}
body.darwin {
font-family: "LucidaGrande", sans-serif;
}
h1 {
font-weight: normal;
margin: 0;
padding: 0;
}
header {
margin-bottom: 1em;
border-bottom: 1px solid #aaa;
padding-bottom: 1em;
}
.starred:after {
content: "*";
}
header p {
color: #aaa;
margin: 0;
padding: 0
}
a {
color: #772953;
text-decoration: none;
}
img {
max-width: 100%;
height: auto;
}
article {
line-height: 1.6;
}
</style>
</head>
<body class='""" + _platform + """''>
<header>
<p><span id="feed_title"></span> <span id="author"></span></p>
<h1><a id="title" href=""></a></h1>
<p><timedate id="date"></timedate></p>
</header>
<article id="article"></article>
</body>
</html>"""
return html
class TinyTinyRSS:
def __init__(self, app, server_url, session_id):
self.app = app
if server_url and session_id:
self.server_url = server_url
self.session_id = session_id
else:
self.app.authenticate()
def doOperation(self, operation, options=None):
url = self.server_url + "/api/"
default_options = {'sid': self.session_id, 'op': operation}
if options:
options = dict(default_options.items() + options.items())
else:
options = default_options
json_string = json.dumps(options)
req = urllib2.Request(url)
fd = urllib2.urlopen(req, json_string)
body = ""
while True:
data = fd.read(1024)
if not len(data):
break
body += data
return json.loads(body)["content"]
def getUnreadFeeds(self, view_mode="unread"):
unread_articles = []
def more(skip):
return self.doOperation("getHeadlines", {"show_excerpt": False, "view_mode": view_mode, "show_content": True, "feed_id": -4, "skip": skip})
skip = 0
while True:
new = more( skip)
unread_articles += new
length = len(new)
if length < 1:
break
skip += length
return unread_articles
def getStarredFeeds(self):
return self.getUnreadFeeds("marked")
def setArticleRead(self, article_id, read=True):
self.updateArticle(article_id, 2, not read)
def setArticleStarred(self, article_id, starred=True):
self.updateArticle(article_id, 0, starred)
def updateArticle(self, article_id, field, true=True):
l = lambda: self.doOperation("updateArticle", {'article_ids':article_id, 'mode': 1 if true else 0, 'field': field})
t = Thread(target=l)
t.start()
def logOut(self):
self.doOperation("logout")
@classmethod
def login(self, server_url, user, password):
url = server_url + "/api/"
options = {"op": "login", "user": user, "password": password}
json_string = json.dumps(options)
req = urllib2.Request(url)
fd = urllib2.urlopen(req, json_string)
body = ""
while 1:
data = fd.read(1024)
if not len(data):
break
body += data
body = json.loads(body)["content"]
if body.has_key("error"):
msgBox = QtGui.QMessageBox()
msgBox.setText(body["error"])
msgBox.exec_()
return None
return body["session_id"]
class Login(QtGui.QDialog):
def __init__(self):
QtGui.QDialog.__init__(self)
self.setWindowIcon(QtGui.QIcon("feedmonkey.png"))
self.setWindowTitle("Feed the Monkey - Login")
self.label = QtGui.QLabel(self)
self.label.setText("Please specify a server url, a username and a password.")
self.textServerUrl = QtGui.QLineEdit(self)
self.textServerUrl.setPlaceholderText("http://example.com/ttrss/")
self.textServerUrl.setText("http://")
self.textName = QtGui.QLineEdit(self)
self.textName.setPlaceholderText("username")
self.textPass = QtGui.QLineEdit(self)
self.textPass.setEchoMode(QtGui.QLineEdit.Password);
self.textPass.setPlaceholderText("password")
self.buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok)
self.buttons.accepted.connect(self.accept)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.label)
layout.addWidget(self.textServerUrl)
layout.addWidget(self.textName)
layout.addWidget(self.textPass)
layout.addWidget(self.buttons)
class WorkerThread(QtCore.QThread):
def __init__(self, parent, do_reload):
super(WorkerThread, self).__init__(parent)
self.do_reload = do_reload
def run(self):
self.do_reload()
self.emit(QtCore.SIGNAL("reload_done()"))
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
wb = MainWindow()
wb.show()
sys.exit(app.exec_())

View file

@ -1,12 +0,0 @@
[Desktop Entry]
Version=0.1.0
Comment=A desktop client for the TinyTinyRSS feed reader.
Exec=feedthemonkey
GenericName=Feed Reader
Icon=feedthemonkey
Name=Feed the Monkey
NoDisplay=false
StartupNotify=true
Terminal=false
Type=Application
Categories=Network;Qt

7
html.qrc Normal file
View file

@ -0,0 +1,7 @@
<RCC>
<qresource prefix="/">
<file>content.css</file>
<file>content.html</file>
<file>content.js</file>
</qresource>
</RCC>

6
images.qrc Normal file
View file

@ -0,0 +1,6 @@
<RCC>
<qresource prefix="/">
<file>feedthemonkey.xpm</file>
<file>Icon.icns</file>
</qresource>
</RCC>

27
main.cpp Normal file
View file

@ -0,0 +1,27 @@
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <qdebug.h>
#include <QMetaType>
#include <QtQml>
#include <QIcon>
#include "tinytinyrsslogin.h"
#include "tinytinyrss.h"
#include "post.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
app.setOrganizationName("Jeena");
app.setOrganizationDomain("jeena.net");
app.setApplicationName("FeedTheMonkey");
qmlRegisterType<TinyTinyRSSLogin>("TTRSS", 1, 0, "ServerLogin");
qmlRegisterType<TinyTinyRSS>("TTRSS", 1, 0, "Server");
qmlRegisterType<Post>("TTRSS", 1, 0, "Post");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}

184
main.qml Normal file
View file

@ -0,0 +1,184 @@
import QtQuick 2.3
import QtQuick.Controls 1.3
import QtQuick.Window 2.0
import QtQuick.Layouts 1.1
import Qt.labs.settings 1.0
import TTRSS 1.0
ApplicationWindow {
id: app
title: "FeedTheMonkey"
visible: true
minimumWidth: 480
minimumHeight: 320
width: 800
height: 640
x: 200
y: 200
property Server server: server
property Sidebar sidebar: sidebar
property Content content: content
property variant fontSizes: [7,9,11,13,15,17,19,21,23,25,27,29,31]
property int defaultTextFontSizeIndex: 4
property int textFontSizeIndex: defaultTextFontSizeIndex
property int textFontSize: fontSizes[textFontSizeIndex]
Settings {
id: settings
category: "window"
property alias x: app.x
property alias y: app.y
property alias width: app.width
property alias height: app.height
property alias sidebarWidth: sidebar.width
property alias textFontSizeIndex: app.textFontSizeIndex
}
property TheMenuBar menu: TheMenuBar {
id: menu
serverLogin: serverLogin
server: server
sidebar: sidebar
content: content
}
function loggedIn() {
if(serverLogin.loggedIn()) {
menu.loggedIn = true;
contentView.visible = true
login.visible = false;
server.initialize(serverLogin.serverUrl, serverLogin.sessionId);
} else {
menu.loggedIn = false
contentView.visible = false
login.visible = true
server.loggedOut()
content.loggedOut()
}
}
function zoomIn() {
if(textFontSizeIndex + 1 < fontSizes.length) {
textFontSize = fontSizes[++textFontSizeIndex]
}
}
function zoomOut() {
if(textFontSizeIndex - 1 > 0) {
textFontSize = fontSizes[--textFontSizeIndex]
}
}
function zoomReset() {
textFontSizeIndex = defaultTextFontSizeIndex
textFontSize = fontSizes[textFontSizeIndex]
}
function keyPressed(event) {
switch (event.key) {
case Qt.Key_Right:
case Qt.Key_J:
case Qt.Key_j:
sidebar.next()
break
case Qt.Key_Left:
case Qt.Key_K:
case Qt.Key_k:
sidebar.previous()
break
case Qt.Key_Home:
content.scrollUp()
break
case Qt.Key_End:
content.scrollDown()
break
case Qt.Key_PageUp:
content.scrollUp(content.pageJump)
break
case Qt.Key_PageDown:
case Qt.Key_Space:
content.scrollDown(content.pageJump)
break
case Qt.Key_Down:
content.scrollDown(content.scrollJump)
break
case Qt.Key_Up:
content.scrollUp(content.scrollJump)
break
case Qt.Key_Enter:
case Qt.Key_Return:
Qt.openUrlExternally(content.post.link)
break
default:
break
}
}
SplitView {
id: contentView
anchors.fill: parent
orientation: Qt.Horizontal
visible: serverLogin.loggedIn()
focus: true
Sidebar {
id: sidebar
content: content
server: server
Layout.minimumWidth: 200
implicitWidth: 300
textFontSize: app.textFontSize
}
Content {
id: content
app: app
Layout.minimumWidth: 200
implicitWidth: 624
textFontSize: app.textFontSize
}
Keys.onPressed: keyPressed(event)
Keys.onReleased: {
switch (event.key) {
case Qt.Key_Alt:
app.menuBar = menu
break
default:
break
}
}
}
Login {
id: login
anchors.fill: parent
visible: !serverLogin.loggedIn()
function login() {
console.log("FOO")
serverLogin.login(serverUrl, userName, password)
}
}
ServerLogin {
id: serverLogin
onSessionIdChanged: app.loggedIn()
}
Server {
id: server
}
Component.onCompleted: {
if(serverLogin.loggedIn()) {
loggedIn();
}
}
}

53
post.cpp Normal file
View file

@ -0,0 +1,53 @@
#include "post.h"
#include <QDebug>
#include <QJsonDocument>
Post::Post(QObject *parent) : QObject(parent)
{
}
Post::Post(QJsonObject post, QObject *parent) : QObject(parent)
{
mTitle = post.value("title").toString().trimmed();
mFeedTitle = post.value("feed_title").toString().trimmed();
mId = post.value("id").toInt();
mFeedId = post.value("feed_id").toString().trimmed();
mAuthor = post.value("author").toString().trimmed();
QUrl url(post.value("link").toString().trimmed());
mLink = url;
QDateTime timestamp;
timestamp.setTime_t(post.value("updated").toInt());
mDate = timestamp;
mContent = post.value("content").toString().trimmed();
mExcerpt = post.value("excerpt").toString().remove(QRegExp("<[^>]*>")).replace("&hellip;", " ...").trimmed().replace("(\\s+)", " ").replace("\n", "");
mStarred = post.value("marked").toBool();
mRead = !post.value("unread").toBool();
mDontChangeRead = false;
QJsonDocument doc(post);
QString result(doc.toJson(QJsonDocument::Indented));
mJsonString = result;
}
Post::~Post()
{
}
void Post::setRead(bool r)
{
if(mRead == r) return;
mRead = r;
emit readChanged(mRead);
}
void Post::setDontChangeRead(bool r)
{
qDebug() << "setDontChangeRead " << r << " " << mDontChangeRead;
if(mDontChangeRead == r) return;
mDontChangeRead = r;
emit dontChangeReadChanged(mDontChangeRead);
}

69
post.h Normal file
View file

@ -0,0 +1,69 @@
#ifndef POST_H
#define POST_H
#include <QObject>
#include <QUrl>
#include <QDate>
#include <QJsonObject>
class Post : public QObject
{
Q_OBJECT
Q_PROPERTY(QString title READ title CONSTANT)
Q_PROPERTY(QString feedTitle READ feedTitle CONSTANT)
Q_PROPERTY(int id READ id CONSTANT)
Q_PROPERTY(QString feedId READ feedId CONSTANT)
Q_PROPERTY(QString author READ author CONSTANT)
Q_PROPERTY(QUrl link READ link CONSTANT)
Q_PROPERTY(QDateTime date READ date CONSTANT)
Q_PROPERTY(QString content READ content CONSTANT)
Q_PROPERTY(QString excerpt READ excerpt CONSTANT)
Q_PROPERTY(bool starred READ starred NOTIFY starredChanged)
Q_PROPERTY(bool read READ read WRITE setRead NOTIFY readChanged)
Q_PROPERTY(bool dontChangeRead READ dontChangeRead WRITE setDontChangeRead NOTIFY dontChangeReadChanged)
Q_PROPERTY(QString jsonString READ jsonString CONSTANT)
public:
Post(QObject *parent = 0);
Post(QJsonObject post, QObject *parent = 0);
~Post();
QString title() const { return mTitle; }
QString feedTitle() const { return mFeedTitle; }
int id() const { return mId; }
QString feedId() const { return mFeedId; }
QString author() const { return mAuthor; }
QUrl link() const { return mLink; }
QDateTime date() const { return mDate; }
QString content() const { return mContent; }
QString excerpt() const { return mExcerpt; }
bool starred() const { return mStarred; }
bool read() { return mRead; }
void setRead(bool r);
bool dontChangeRead() const { return mDontChangeRead; }
void setDontChangeRead(bool r);
QString jsonString() const { return mJsonString; }
signals:
void starredChanged(bool);
void readChanged(bool);
void dontChangeReadChanged(bool);
public slots:
private:
QString mTitle;
QString mFeedTitle;
int mId;
QString mFeedId;
QString mAuthor;
QUrl mLink;
QDateTime mDate;
QString mContent;
QString mExcerpt;
bool mStarred;
bool mRead;
bool mDontChangeRead;
QString mJsonString;
};
#endif // POST_H

10
qml.qrc Normal file
View file

@ -0,0 +1,10 @@
<RCC>
<qresource prefix="/">
<file>main.qml</file>
<file>TheMenuBar.qml</file>
<file>Content.qml</file>
<file>Login.qml</file>
<file>PostListItem.qml</file>
<file>Sidebar.qml</file>
</qresource>
</RCC>

View file

@ -1,74 +0,0 @@
#!/usr/bin/env python2
import os, PyQt4
from setuptools import setup
from sys import platform as _platform
VERSION = "0.1.2"
files = []
options = {}
setup_requires = []
is_osx = _platform == "darwin"
is_win = os.name == "nt"
is_linux = not is_osx and not is_win
if is_linux:
setup(
name = "FeedTheMonkey",
version = VERSION,
author = "Jeena Paradies",
author_email = "spam@jeenaparadies.net",
url = "http://jabs.nu/feedthemonkey",
license = "BSD license",
scripts = ["feedthemonkey"],
data_files=[
('/usr/share/applications', ["feedthemonkey.desktop"]),
('/usr/share/pixmaps', ["feedthemonkey.xpm"])
]
)
if is_osx:
options = {
'py2app': {
'argv_emulation': False,
'iconfile': 'Icon.icns',
'plist': {
'CFBundleShortVersionString': VERSION,
'CFBundleIdentifier': "nu.jabs.apps.feedthemonkey",
'LSMinimumSystemVersion': "10.4",
'CFBundleURLTypes': [
{
'CFBundleURLName': 'nu.jabs.apps.feedthemonkey.handler',
'CFBundleURLSchemes': ['feedthemonkey']
}
]
},
'includes':['PyQt4.QtWebKit', 'PyQt4', 'PyQt4.QtCore', 'PyQt4.QtGui', 'PyQt4.QtNetwork'],
'excludes': ['PyQt4.QtDesigner', 'PyQt4.QtOpenGL', 'PyQt4.QtScript', 'PyQt4.QtSql', 'PyQt4.QtTest', 'PyQt4.QtXml', 'PyQt4.phonon', 'simplejson'],
'qt_plugins': 'imageformats',
}
}
setup_requires = ["py2app"]
APP = ["feedthemonkey"]
for dirname, dirnames, filenames in os.walk('.'):
for filename in filenames:
if filename == "Icon.icns":
files += [(dirname, [os.path.join(dirname, filename)])]
setup(
app = APP,
name = "FeedTheMonkey",
options = options,
version = VERSION,
author = "Jeena Paradies",
author_email = "spam@jeenaparadies.net",
url = "http://jabs.nu/feedthemonkey",
license = "BSD license",
scripts = ["feedthemonkey"],
data_files = files,
setup_requires = setup_requires
)

132
tinytinyrss.cpp Normal file
View file

@ -0,0 +1,132 @@
#include "tinytinyrss.h"
#include <QJsonDocument>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QJsonArray>
TinyTinyRSS::TinyTinyRSS(QObject *parent) :
QObject(parent)
{
qRegisterMetaType<QList<Post *> >();
mNetworkManager = new QNetworkAccessManager(this);
mPosts = QList<Post *>();
}
TinyTinyRSS::~TinyTinyRSS()
{
mPosts.clear();
delete mNetworkManager;
}
void TinyTinyRSS::initialize(const QString serverUrl, const QString sessionId)
{
mServerUrl = serverUrl;
mSessionId = sessionId;
reload();
}
void TinyTinyRSS::reload()
{
QVariantMap opts;
opts.insert("show_excerpt", false);
opts.insert("view_mode", "unread");
opts.insert("show_content", true);
opts.insert("feed_id", -4);
opts.insert("skip", 0);
doOperation("getHeadlines", opts, [this] (const QJsonObject &json) {
mPosts.clear();
QJsonArray posts = json.value("content").toArray();
for(int i = 0; i <= posts.count(); i++)
{
QJsonObject postJson = posts.at(i).toObject();
Post *post = new Post(postJson, this);
connect(post, SIGNAL(readChanged(bool)), this, SLOT(onPostReadChanged(bool)));
mPosts.append(post);
}
emit postsChanged(mPosts);
});
}
void TinyTinyRSS::loggedOut()
{
mServerUrl = nullptr;
mSessionId = nullptr;
mPosts.clear();
emit postsChanged(mPosts);
}
void TinyTinyRSS::doOperation(QString operation, QVariantMap opts, std::function<void (const QJsonObject &json)> callback)
{
QVariantMap options;
options.insert("sid", mSessionId);
options.insert("op", operation);
QMapIterator<QString, QVariant> i(opts);
while (i.hasNext()) {
i.next();
options.insert(i.key(), i.value());
}
QJsonObject jsonobj = QJsonObject::fromVariantMap(options);
QJsonDocument json = QJsonDocument(jsonobj);
QNetworkRequest request(mServerUrl);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *reply = mNetworkManager->post(request, json.toJson());
connect(reply, &QNetworkReply::finished, [callback, reply] () {
if (reply) {
if (reply->error() == QNetworkReply::NoError) {
QString jsonString = QString(reply->readAll());
QJsonDocument json = QJsonDocument::fromJson(jsonString.toUtf8());
callback(json.object());
} else {
int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
//do some error management
qWarning() << "HTTP error: " << httpStatus;
}
reply->deleteLater();
}
});
}
void TinyTinyRSS::onPostReadChanged(bool r)
{
Post *post = (Post *)sender();
updateArticle(post->id(), 2, !r, [post] (const QJsonObject &json) {
qDebug() << json;
// not doing anything with this yet.
});
}
void TinyTinyRSS::updateArticle(int articleId, int field, bool trueFalse, std::function<void (const QJsonObject &json)> callback)
{
QVariantMap opts;
opts.insert("article_ids", articleId);
opts.insert("field", field);
opts.insert("mode", trueFalse ? 1 : 0);
doOperation("updateArticle", opts, callback);
}
QQmlListProperty<Post> TinyTinyRSS::posts()
{
return QQmlListProperty<Post>(this, mPosts);
}
int TinyTinyRSS::postsCount() const
{
return mPosts.count();
}
Post *TinyTinyRSS::post(int index) const
{
return mPosts.at(index);
}

49
tinytinyrss.h Normal file
View file

@ -0,0 +1,49 @@
#ifndef TINYTINYRSS_H
#define TINYTINYRSS_H
#include <QObject>
#include <QMap>
#include <QNetworkReply>
#include <QList>
#include <QQmlListProperty>
#include <QJsonObject>
#include <functional>
#include "post.h"
class TinyTinyRSS : public QObject
{
Q_OBJECT
Q_PROPERTY(QQmlListProperty<Post> posts READ posts NOTIFY postsChanged)
public:
TinyTinyRSS(QObject *parent = 0);
~TinyTinyRSS();
Q_INVOKABLE void initialize(const QString serverUrl, const QString sessionId);
Q_INVOKABLE void reload();
Q_INVOKABLE void loggedOut();
QQmlListProperty<Post> posts();
int postsCount() const;
Post *post(int) const;
signals:
void postsChanged(QList<Post *>);
private slots:
void onPostReadChanged(bool);
private:
void doOperation(QString operation, QVariantMap opts, std::function<void (const QJsonObject &json)> callback);
void updateArticle(int articleId, int field, bool trueFalse, std::function<void (const QJsonObject &json)> callback);
QString mServerUrl;
QString mSessionId;
QList<Post*> mPosts;
QNetworkAccessManager *mNetworkManager;
};
#endif // TINYTINYRSS_H

92
tinytinyrsslogin.cpp Normal file
View file

@ -0,0 +1,92 @@
#include "tinytinyrsslogin.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QSettings>
#define APP_URL "net.jeena"
#define APP_NAME "FeedTheMonkey"
TinyTinyRSSLogin::TinyTinyRSSLogin(QObject *parent) :
QObject(parent)
{
mNetworkManager = new QNetworkAccessManager(this);
QSettings settings;
mSessionId = settings.value("sessionId").toString();
mServerUrl = settings.value("serverUrl").toString();
}
TinyTinyRSSLogin::~TinyTinyRSSLogin()
{
delete mNetworkManager;
}
bool TinyTinyRSSLogin::loggedIn()
{
return !mSessionId.isEmpty();
}
void TinyTinyRSSLogin::login(const QString serverUrl, const QString user, const QString password)
{
mServerUrl = QUrl(serverUrl + "/api/");
QVariantMap options;
options.insert("op", "login");
options.insert("user", user);
options.insert("password", password);
QJsonObject jsonobj = QJsonObject::fromVariantMap(options);
QJsonDocument json = QJsonDocument(jsonobj);
QNetworkRequest request(mServerUrl);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *reply = mNetworkManager->post(request, json.toJson());
connect(reply, SIGNAL(finished()), this, SLOT(reply()));
}
void TinyTinyRSSLogin::logout()
{
if(mSessionId.length() > 0 && mServerUrl.toString().length() > 0) {
QVariantMap options;
options.insert("op", "logout");
options.insert("sid", mSessionId);
QJsonObject jsonobj = QJsonObject::fromVariantMap(options);
QJsonDocument json = QJsonDocument(jsonobj);
QNetworkRequest request(mServerUrl);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *reply = mNetworkManager->post(request, json.toJson());
connect(reply, SIGNAL(finished()), this, SLOT(reply()));
}
}
void TinyTinyRSSLogin::reply()
{
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
if (reply) {
if (reply->error() == QNetworkReply::NoError) {
QString jsonString = QString(reply->readAll());
QJsonDocument json = QJsonDocument::fromJson(jsonString.toUtf8());
mSessionId = json.object().value("content").toObject().value("session_id").toString();
emit sessionIdChanged(mSessionId);
QSettings settings;
settings.setValue("sessionId", mSessionId);
settings.setValue("serverUrl", mServerUrl);
settings.sync();
} else {
int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
//do some error management
qWarning() << "HTTP error: " << httpStatus << " :: " << reply->error();
}
reply->deleteLater();
}
}

37
tinytinyrsslogin.h Normal file
View file

@ -0,0 +1,37 @@
#ifndef TINYTINYRSSLOGIN_H
#define TINYTINYRSSLOGIN_H
#include <QObject>
#include <QMetaType>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
class TinyTinyRSSLogin : public QObject
{
Q_OBJECT
Q_PROPERTY(QString sessionId READ sessionId NOTIFY sessionIdChanged)
Q_PROPERTY(QUrl serverUrl READ serverUrl)
public:
TinyTinyRSSLogin(QObject *parent = 0);
~TinyTinyRSSLogin();
QString sessionId() const { return mSessionId; }
QUrl serverUrl() const { return mServerUrl; }
Q_INVOKABLE bool loggedIn();
Q_INVOKABLE void login(const QString serverUrl, const QString user, const QString password);
Q_INVOKABLE void logout();
signals:
void sessionIdChanged(QString);
private slots:
void reply();
private:
QString mSessionId;
QUrl mServerUrl;
QNetworkAccessManager *mNetworkManager;
};
#endif // TINYTINYRSSLOGIN_H