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
|
.DS_Store
|
||||||
FeedTheMonkey.app
|
FeedTheMonkey.pro.user*
|
||||||
FeedTheMonkey.egg-info/
|
|
||||||
feedthemonkey.egg-info/
|
|
||||||
|
|
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