538 lines
20 KiB
Python
538 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright (C) 2018 Andy Stewart
|
|
#
|
|
# Author: Andy Stewart <lazycat.manatee@gmail.com>
|
|
# Maintainer: Andy Stewart <lazycat.manatee@gmail.com>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from PyQt5.QtCore import QUrl, Qt
|
|
from PyQt5.QtGui import QColor, QFont
|
|
from PyQt5.QtWidgets import QPushButton, QHBoxLayout, QWidget, QWidget, QListWidget, QVBoxLayout, QLabel, QListWidgetItem, QStackedWidget
|
|
from core.buffer import Buffer
|
|
from PyQt5 import QtCore
|
|
from core.browser import BrowserView
|
|
from core.utils import touch, interactive
|
|
import feedparser
|
|
import json
|
|
import os
|
|
|
|
class AppBuffer(Buffer):
|
|
def __init__(self, buffer_id, url, config_dir, arguments, emacs_var_dict, module_path):
|
|
Buffer.__init__(self, buffer_id, url, arguments, emacs_var_dict, module_path, True)
|
|
|
|
self.add_widget(RSSReaderWidget(buffer_id, config_dir))
|
|
self.buffer_widget.browser.buffer = self
|
|
|
|
self.build_all_methods(self.buffer_widget)
|
|
self.build_all_methods(self.buffer_widget.browser)
|
|
|
|
def add_subscription(self):
|
|
self.send_input_message("Subscribe to RSS feed: ", "add_subscription")
|
|
|
|
def delete_subscription(self):
|
|
self.send_input_message("Are you sure you want to delete the current feed? (y or n): ", "delete_subscription")
|
|
|
|
def handle_input_message(self, result_type, result_content):
|
|
if result_type == "search_text_forward":
|
|
self.buffer_widget.browser._search_text(str(result_content))
|
|
elif result_type == "search_text_backward":
|
|
self.buffer_widget.browser._search_text(str(result_content), True)
|
|
elif result_type == "jump_link":
|
|
self.buffer_widget.browser.jump_to_link(str(result_content))
|
|
elif result_type == "jump_link_new_buffer":
|
|
self.buffer_widget.browser.jump_to_link(str(result_content), "true")
|
|
elif result_type == "add_subscription":
|
|
self.buffer_widget.add_subscription(result_content)
|
|
elif result_type == "delete_subscription":
|
|
if result_content == "y":
|
|
self.buffer_widget.delete_subscription()
|
|
|
|
def cancel_input_message(self, result_type):
|
|
if result_type == "jump_link" or result_type == "jump_link_new_buffer":
|
|
self.buffer_widget.browser.cleanup_links()
|
|
|
|
def open_link(self):
|
|
self.buffer_widget.browser.open_link()
|
|
self.send_input_message("Open Link: ", "jump_link");
|
|
|
|
def open_link_new_buffer(self):
|
|
self.buffer_widget.browser.open_link_new_buffer()
|
|
self.send_input_message("Open Link in New Buffer: ", "jump_link_new_buffer");
|
|
|
|
class RSSReaderWidget(QWidget):
|
|
|
|
def __init__(self, buffer_id, config_dir):
|
|
super(RSSReaderWidget, self).__init__()
|
|
|
|
self.feed_file_path = os.path.join(config_dir, "rss-reader", "feeds.json")
|
|
|
|
self.feed_area = QWidget()
|
|
self.feed_list = QListWidget()
|
|
self.feed_list.setStyleSheet( """QListView {background: #4D5250; show-decoration-selected: 1; selection-background-color: #464646;}""")
|
|
panel_layout = QVBoxLayout()
|
|
panel_layout.setSpacing(0)
|
|
panel_layout.setContentsMargins(0, 0, 0, 0)
|
|
panel_layout.addWidget(self.feed_list)
|
|
self.feed_area.setLayout(panel_layout)
|
|
|
|
self.article_area = QWidget()
|
|
self.article_list = QListWidget()
|
|
self.article_list.setStyleSheet( """QListView {background: #FFF; show-decoration-selected: 1; selection-background-color: #EEE;}""")
|
|
self.article_list.verticalScrollBar().setStyleSheet("QScrollBar {width:0px;}");
|
|
article_layout = QVBoxLayout()
|
|
article_layout.setSpacing(0)
|
|
article_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.browser = BrowserView(buffer_id, config_dir)
|
|
|
|
article_layout.addWidget(self.article_list)
|
|
article_layout.addWidget(self.browser)
|
|
|
|
article_layout.setStretchFactor(self.article_list, 1)
|
|
article_layout.setStretchFactor(self.browser, 3)
|
|
|
|
self.article_area.setLayout(article_layout)
|
|
|
|
self.welcome_page = QWidget()
|
|
self.welcome_page_box = QVBoxLayout()
|
|
self.welcome_page_box.setSpacing(10)
|
|
self.welcome_page_box.setContentsMargins(0, 0, 0, 0)
|
|
|
|
welcome_title_label = QLabel("Welcome to EAF RSS Reader!")
|
|
welcome_title_label.setFont(QFont('Arial', 24))
|
|
welcome_title_label.setStyleSheet("QLabel {color: #333; font-weight: bold; margin: 20px;}")
|
|
welcome_title_label.setAlignment(Qt.AlignHCenter)
|
|
|
|
add_subscription_label = QLabel("Press 'a' to subscribe to a feed!")
|
|
add_subscription_label.setFont(QFont('Arial', 20))
|
|
add_subscription_label.setStyleSheet("QLabel {color: #grey;}")
|
|
add_subscription_label.setAlignment(Qt.AlignHCenter)
|
|
|
|
self.welcome_page_box.addStretch(1)
|
|
self.welcome_page_box.addWidget(welcome_title_label)
|
|
self.welcome_page_box.addWidget(add_subscription_label)
|
|
self.welcome_page_box.addStretch(1)
|
|
|
|
self.welcome_page.setLayout(self.welcome_page_box)
|
|
|
|
self.right_area = QStackedWidget()
|
|
self.right_area.addWidget(self.welcome_page)
|
|
self.right_area.addWidget(self.article_area)
|
|
|
|
if self.has_feed():
|
|
self.right_area.setCurrentIndex(1)
|
|
else:
|
|
self.right_area.setCurrentIndex(0)
|
|
|
|
hbox = QHBoxLayout()
|
|
hbox.setSpacing(0)
|
|
hbox.setContentsMargins(0, 0, 0, 0)
|
|
|
|
hbox.addWidget(self.feed_area)
|
|
hbox.addWidget(self.right_area)
|
|
|
|
hbox.setStretchFactor(self.feed_area, 1)
|
|
hbox.setStretchFactor(self.right_area, 3)
|
|
|
|
self.setLayout(hbox)
|
|
|
|
self.feed_list.itemActivated.connect(self.handle_feed)
|
|
self.article_list.itemActivated.connect(self.handle_article)
|
|
|
|
self.feed_object_dict = {}
|
|
|
|
self.init_select_line = False
|
|
|
|
self.fetch_feeds()
|
|
|
|
def has_feed(self):
|
|
if os.path.exists(self.feed_file_path):
|
|
try:
|
|
with open(self.feed_file_path, "r") as feed_file:
|
|
feed_dict = json.loads(feed_file.read())
|
|
return len(feed_dict.keys()) > 0
|
|
except Exception:
|
|
return False
|
|
|
|
return False
|
|
|
|
def fetch_feeds(self):
|
|
if os.path.exists(self.feed_file_path):
|
|
try:
|
|
with open(self.feed_file_path, "r") as feed_file:
|
|
feed_dict = json.loads(feed_file.read())
|
|
for index, feed_link in enumerate(feed_dict):
|
|
self.fetch_feed(feed_link, index == 0)
|
|
except Exception:
|
|
pass
|
|
|
|
def handle_feed(self, feed_item):
|
|
if feed_item.feed_link in self.feed_object_dict:
|
|
self.init_article_area(self.feed_object_dict[feed_item.feed_link])
|
|
|
|
def handle_article(self, article_item):
|
|
article_item.mark_as_read()
|
|
|
|
self.browser.setUrl(QUrl(article_item.post_link))
|
|
|
|
def fetch_feed(self, feed_link, refresh_ui):
|
|
fetchThread = FetchRSSThread(feed_link)
|
|
fetchThread.fetch_rss.connect(lambda f_object, f_link, f_title: self.handle_rss(f_object, f_link, f_title, refresh_ui))
|
|
fetchThread.invalid_rss.connect(self.handle_invalid_rss)
|
|
|
|
object_name = "feed_thread_" + feed_link
|
|
setattr(self, object_name, fetchThread)
|
|
getattr(self, object_name).start()
|
|
|
|
def add_subscription(self, feed_link):
|
|
if not self.feed_exists(feed_link):
|
|
self.fetch_feed(feed_link, True)
|
|
else:
|
|
self.buffer.message_to_emacs.emit("Feed already exists: " + feed_link)
|
|
|
|
def delete_subscription(self):
|
|
feed_count = self.feed_list.count()
|
|
current_row = self.feed_list.currentRow()
|
|
feed_link = self.feed_list.currentItem().feed_link
|
|
feed_title = self.feed_list.currentItem().feed_title
|
|
|
|
self.feed_list.takeItem(current_row)
|
|
|
|
with open(self.feed_file_path, "r") as feed_file:
|
|
feed_dict = json.loads(feed_file.read())
|
|
if feed_link in feed_dict:
|
|
del feed_dict[feed_link]
|
|
|
|
with open(self.feed_file_path, "w") as f:
|
|
f.write(json.dumps(feed_dict))
|
|
|
|
if feed_count <= 1:
|
|
self.feed_list.clear()
|
|
self.article_list.clear()
|
|
self.browser.setUrl(QUrl(""))
|
|
self.right_area.setCurrentIndex(0)
|
|
else:
|
|
if current_row < feed_count - 1:
|
|
self.feed_list.setCurrentRow(current_row)
|
|
else:
|
|
self.feed_list.setCurrentRow(feed_count - 2)
|
|
self.handle_feed(self.feed_list.currentItem())
|
|
self.buffer.message_to_emacs.emit("Removed feed: " + feed_title)
|
|
|
|
def feed_exists(self, feed_link):
|
|
if not os.path.exists(self.feed_file_path):
|
|
return False
|
|
|
|
try:
|
|
with open(self.feed_file_path, "r") as feed_file:
|
|
feed_dict = json.loads(feed_file.read())
|
|
return feed_link in feed_dict
|
|
except Exception:
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
return False
|
|
|
|
def save_feed(self, feed_object, feed_link, feed_title):
|
|
touch(self.feed_file_path)
|
|
|
|
article_ids = list(map(lambda post: post.id if hasattr(post, 'id') else post.link, feed_object.entries))
|
|
|
|
try:
|
|
with open(self.feed_file_path, "r") as feed_file:
|
|
feed_dict = json.loads(feed_file.read())
|
|
if feed_link not in feed_dict:
|
|
feed_dict[feed_link] = {
|
|
"title": feed_title,
|
|
"unread_articles": article_ids
|
|
}
|
|
|
|
self.save_feed_dict(feed_dict)
|
|
self.buffer.message_to_emacs.emit("Add feed: " + feed_link)
|
|
except Exception:
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
self.save_feed_dict({feed_link : {
|
|
"title": feed_title,
|
|
"unread_articles": article_ids
|
|
}})
|
|
self.buffer.message_to_emacs.emit("Add feed: " + feed_link)
|
|
|
|
def save_feed_dict(self, feed_dict):
|
|
with open(self.feed_file_path, "w") as f:
|
|
f.write(json.dumps(feed_dict))
|
|
|
|
def handle_rss(self, feed_object, feed_link, feed_title, refresh_ui):
|
|
feed_object.feed_link = feed_link
|
|
self.feed_object_dict[feed_link] = feed_object
|
|
|
|
self.save_feed(feed_object, feed_link, feed_title)
|
|
|
|
self.right_area.setCurrentIndex(1)
|
|
|
|
feed_item = QListWidgetItem(self.feed_list)
|
|
feed_item.feed_link = feed_link
|
|
feed_item.feed_title = feed_title
|
|
feed_item_widget = RSSFeedItem(feed_object, len(feed_object.entries))
|
|
feed_item.update_article_num = feed_item_widget.update_article_num
|
|
feed_item.setSizeHint(feed_item_widget.sizeHint())
|
|
self.feed_list.addItem(feed_item)
|
|
self.feed_list.setItemWidget(feed_item, feed_item_widget)
|
|
|
|
unread_articles = []
|
|
with open(self.feed_file_path, "r") as feed_file:
|
|
feed_dict = json.loads(feed_file.read())
|
|
if feed_object.feed_link in feed_dict:
|
|
if "unread_articles" in feed_dict[feed_object.feed_link]:
|
|
unread_articles = ["unread_articles"]
|
|
|
|
for index in range(self.feed_list.count()):
|
|
feed_item = self.feed_list.item(index)
|
|
if feed_item.feed_link == feed_object.feed_link:
|
|
feed_item.update_article_num(len(unread_articles))
|
|
break
|
|
|
|
if refresh_ui:
|
|
self.init_article_area(feed_object)
|
|
|
|
def init_article_area(self, feed_object):
|
|
self.browser.setUrl(QUrl(feed_object.entries[0].link))
|
|
|
|
self.article_list.clear()
|
|
|
|
unread_articles = []
|
|
with open(self.feed_file_path, "r") as feed_file:
|
|
feed_dict = json.loads(feed_file.read())
|
|
if feed_object.feed_link in feed_dict and "unread_articles" in feed_dict[feed_object.feed_link]:
|
|
unread_articles = feed_dict[feed_object.feed_link]["unread_articles"]
|
|
|
|
for index, post in enumerate(feed_object.entries):
|
|
item_widget = RSSArticleItemWidget(feed_object, post, unread_articles)
|
|
item = QListWidgetItem(self.article_list)
|
|
item.mark_as_read = item_widget.mark_as_read
|
|
item.post_link = item_widget.post_link
|
|
item.setSizeHint(item_widget.sizeHint())
|
|
item_widget.mark_article_read.connect(self.mark_article_read)
|
|
self.article_list.addItem(item)
|
|
self.article_list.setItemWidget(item, item_widget)
|
|
|
|
if index == 0:
|
|
item.mark_as_read()
|
|
|
|
self.article_list.setCurrentRow(0)
|
|
|
|
if not self.init_select_line:
|
|
self.init_select_line = True
|
|
self.feed_list.setCurrentRow(0)
|
|
|
|
def handle_invalid_rss(self, feed_link):
|
|
self.buffer.message_to_emacs.emit("Invalid feed link: " + feed_link)
|
|
|
|
def mark_article_read(self, feed_link, post_link):
|
|
if os.path.exists(self.feed_file_path):
|
|
try:
|
|
with open(self.feed_file_path, "r") as feed_file:
|
|
feed_dict = json.loads(feed_file.read())
|
|
if feed_link in feed_dict:
|
|
unread_articles = feed_dict[feed_link]["unread_articles"]
|
|
|
|
if post_link in unread_articles:
|
|
unread_articles.remove(post_link)
|
|
feed_dict[feed_link]["unread_articles"] = unread_articles
|
|
|
|
with open(self.feed_file_path, "w") as f:
|
|
f.write(json.dumps(feed_dict))
|
|
|
|
for index in range(self.feed_list.count()):
|
|
feed_item = self.feed_list.item(index)
|
|
if feed_item.feed_link == feed_link:
|
|
feed_item.update_article_num(len(unread_articles))
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
@interactive()
|
|
def next_subscription(self):
|
|
feed_count = self.feed_list.count()
|
|
current_row = self.feed_list.currentRow()
|
|
|
|
if current_row < feed_count - 1:
|
|
self.feed_list.setCurrentRow(current_row + 1)
|
|
self.feed_list.scrollToItem(self.feed_list.currentItem())
|
|
self.handle_feed(self.feed_list.currentItem())
|
|
else:
|
|
self.buffer.message_to_emacs.emit("End of subscribed feeds")
|
|
|
|
@interactive()
|
|
def prev_subscription(self):
|
|
current_row = self.feed_list.currentRow()
|
|
|
|
if current_row > 0:
|
|
self.feed_list.setCurrentRow(current_row - 1)
|
|
self.feed_list.scrollToItem(self.feed_list.currentItem())
|
|
self.handle_feed(self.feed_list.currentItem())
|
|
else:
|
|
self.buffer.message_to_emacs.emit("Beginning of subscribed feeds")
|
|
|
|
@interactive()
|
|
def first_subscription(self):
|
|
self.feed_list.setCurrentRow(0)
|
|
self.feed_list.scrollToItem(self.feed_list.currentItem())
|
|
self.handle_feed(self.feed_list.currentItem())
|
|
|
|
@interactive()
|
|
def last_subscription(self):
|
|
feed_count = self.feed_list.count()
|
|
|
|
self.feed_list.setCurrentRow(feed_count - 1)
|
|
self.feed_list.scrollToItem(self.feed_list.currentItem())
|
|
self.handle_feed(self.feed_list.currentItem())
|
|
|
|
@interactive()
|
|
def next_article(self):
|
|
article_count = self.article_list.count()
|
|
current_row = self.article_list.currentRow()
|
|
|
|
if current_row < article_count - 1:
|
|
self.article_list.setCurrentRow(current_row + 1)
|
|
self.article_list.scrollToItem(self.article_list.currentItem())
|
|
self.handle_article(self.article_list.currentItem())
|
|
else:
|
|
self.buffer.message_to_emacs.emit("End of articles")
|
|
|
|
@interactive()
|
|
def prev_article(self):
|
|
current_row = self.article_list.currentRow()
|
|
|
|
if current_row > 0:
|
|
self.article_list.setCurrentRow(current_row - 1)
|
|
self.article_list.scrollToItem(self.article_list.currentItem())
|
|
self.handle_article(self.article_list.currentItem())
|
|
else:
|
|
self.buffer.message_to_emacs.emit("Beginning of articles")
|
|
|
|
@interactive()
|
|
def first_article(self):
|
|
self.article_list.setCurrentRow(0)
|
|
self.article_list.scrollToItem(self.article_list.currentItem())
|
|
self.handle_article(self.article_list.currentItem())
|
|
|
|
@interactive()
|
|
def last_article(self):
|
|
article_count = self.article_list.count()
|
|
|
|
self.article_list.setCurrentRow(article_count - 1)
|
|
self.article_list.scrollToItem(self.article_list.currentItem())
|
|
self.handle_article(self.article_list.currentItem())
|
|
|
|
class FetchRSSThread(QtCore.QThread):
|
|
fetch_rss = QtCore.pyqtSignal(object, str, str)
|
|
invalid_rss = QtCore.pyqtSignal(str)
|
|
|
|
def __init__(self, feed_link):
|
|
super().__init__()
|
|
self.feed_link = feed_link
|
|
|
|
def run(self):
|
|
try:
|
|
d = feedparser.parse(self.feed_link)
|
|
self.fetch_rss.emit(d, self.feed_link, d.feed.title)
|
|
except Exception:
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
self.invalid_rss.emit(self.feed_link)
|
|
|
|
class RSSFeedItem(QWidget):
|
|
|
|
def __init__(self, feed_object, post_num):
|
|
super(RSSFeedItem, self).__init__()
|
|
|
|
layout = QHBoxLayout()
|
|
layout.setContentsMargins(10, 10, 10, 10)
|
|
|
|
feed_title_label = QLabel(feed_object.feed.title)
|
|
feed_title_label.setFont(QFont('Arial', 18))
|
|
feed_title_label.setStyleSheet("color: #DDD")
|
|
layout.addWidget(feed_title_label)
|
|
|
|
self.number_label = QLabel(str(post_num))
|
|
self.number_label.setFont(QFont('Arial', 16))
|
|
self.number_label.setStyleSheet("color: #AAA")
|
|
layout.addStretch(1)
|
|
layout.addWidget(self.number_label)
|
|
|
|
self.setLayout(layout)
|
|
|
|
def update_article_num(self, num):
|
|
self.number_label.setText(str(num))
|
|
|
|
class RSSArticleItemWidget(QWidget):
|
|
mark_article_read = QtCore.pyqtSignal(str, str)
|
|
|
|
def __init__(self, feed_object, post, unread_articles):
|
|
super(RSSArticleItemWidget, self).__init__()
|
|
|
|
self.feed_object = feed_object
|
|
self.post_id = post.id if hasattr(post, 'id') else post.link
|
|
self.post_link = post.link
|
|
|
|
date = ""
|
|
try:
|
|
date = "[%d-%02d-%02d]" % (post.published_parsed.tm_year, post.published_parsed.tm_mon, post.published_parsed.tm_mday)
|
|
except Exception:
|
|
pass
|
|
|
|
article_layout = QHBoxLayout()
|
|
article_layout.setContentsMargins(10, 10, 0, 0)
|
|
|
|
self.article_date_label = QLabel(date)
|
|
self.article_date_label.setFont(QFont('Arial', 16))
|
|
|
|
self.article_title_label = QLabel(post.title)
|
|
self.article_title_label.setFont(QFont('Arial', 16))
|
|
|
|
self.status_label = QLabel("*")
|
|
self.status_label.setFont(QFont('Arial', 20))
|
|
|
|
if self.post_id in unread_articles:
|
|
self.status_label.setStyleSheet("color: #008DFF")
|
|
self.article_date_label.setStyleSheet("color: #464646")
|
|
self.article_title_label.setStyleSheet("color: #464646")
|
|
else:
|
|
self.status_label.setStyleSheet("color: #9e9e9e")
|
|
self.article_date_label.setStyleSheet("color: #9e9e9e")
|
|
self.article_title_label.setStyleSheet("color: #9e9e9e")
|
|
|
|
article_layout.addWidget(self.status_label)
|
|
article_layout.addWidget(self.article_date_label)
|
|
article_layout.addWidget(self.article_title_label)
|
|
article_layout.addStretch(1)
|
|
|
|
self.setLayout(article_layout)
|
|
|
|
def truncate_description(self, text):
|
|
return (text[:90] + ' ...') if len(text) > 90 else text
|
|
|
|
def mark_as_read(self):
|
|
self.status_label.setStyleSheet("color: #9e9e9e")
|
|
self.article_date_label.setStyleSheet("color: #9e9e9e")
|
|
self.article_title_label.setStyleSheet("color: #9e9e9e")
|
|
|
|
self.mark_article_read.emit(self.feed_object.feed_link, self.post_id)
|