clean branch for qml reimplementation
This commit is contained in:
parent
c798e60300
commit
3ff4969946
8 changed files with 0 additions and 10548 deletions
BIN
Icon.icns
BIN
Icon.icns
Binary file not shown.
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.
|
|
||||||
|
|
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
|
|
||||||
|
|
||||||

|
|
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
|
|
564
feedthemonkey
564
feedthemonkey
|
@ -1,564 +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 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))
|
|
||||||
|
|
||||||
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
|
|
9793
feedthemonkey.xpm
9793
feedthemonkey.xpm
File diff suppressed because it is too large
Load diff
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
|
|
||||||
)
|
|
Loading…
Add table
Add a link
Reference in a new issue