moving on to v2 in C++ and QML
This commit is contained in:
commit
b456f588fc
28 changed files with 1255 additions and 766 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,6 +1,3 @@
|
|||
build
|
||||
dist
|
||||
.DS_Store
|
||||
FeedTheMonkey.app
|
||||
FeedTheMonkey.egg-info/
|
||||
feedthemonkey.egg-info/
|
||||
FeedTheMonkey.pro.user*
|
||||
|
||||
|
|
91
Content.qml
Normal file
91
Content.qml
Normal 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
32
FeedTheMonkey.pro
Normal 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
|
30
LICENSE.txt
30
LICENSE.txt
|
@ -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
65
Login.qml
Normal 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
89
PostListItem.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
58
README.md
58
README.md
|
@ -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
|
||||
|
||||

|
71
Sidebar.qml
Normal file
71
Sidebar.qml
Normal 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
99
TheMenuBar.qml
Normal 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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
17
build-osx.sh
17
build-osx.sh
|
@ -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
46
content.css
Normal 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
18
content.html
Normal 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
49
content.js
Normal 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>…</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 = "– " + 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
27
deployment.pri
Normal 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)
|
570
feedthemonkey
570
feedthemonkey
|
@ -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>…</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 = "– " + 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_())
|
|
@ -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
7
html.qrc
Normal 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
6
images.qrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>feedthemonkey.xpm</file>
|
||||
<file>Icon.icns</file>
|
||||
</qresource>
|
||||
</RCC>
|
27
main.cpp
Normal file
27
main.cpp
Normal 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
184
main.qml
Normal 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
53
post.cpp
Normal 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("…", " ...").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
69
post.h
Normal 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
10
qml.qrc
Normal 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>
|
74
setup.py
74
setup.py
|
@ -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
132
tinytinyrss.cpp
Normal 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
49
tinytinyrss.h
Normal 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
92
tinytinyrsslogin.cpp
Normal 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
37
tinytinyrsslogin.h
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue