You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
342 lines
12 KiB
342 lines
12 KiB
#!/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, QPainter, QFont, QTextDocument |
|
from PyQt5.QtWidgets import QPushButton, QHBoxLayout, QWidget, QApplication, QWidget, QListWidget, QVBoxLayout, QLabel, QPushButton, QListWidgetItem, QStackedWidget, QSizePolicy |
|
from core.buffer import Buffer |
|
from PyQt5 import QtWidgets, QtCore |
|
from core.browser import BrowserView |
|
import feedparser |
|
import json |
|
import os |
|
|
|
class AppBuffer(Buffer): |
|
def __init__(self, buffer_id, url, arguments): |
|
Buffer.__init__(self, buffer_id, url, arguments, True, QColor(0, 0, 0, 255)) |
|
|
|
self.add_widget(RSSReaderWidget()) |
|
|
|
def handle_input_message(self, result_type, result_content): |
|
if result_type == "add_subscription": |
|
self.buffer_widget.add_subscription(result_content) |
|
|
|
def add_subscription(self): |
|
self.buffer_widget.send_input_message("Subscribe to RSS feed: ", "add_subscription") |
|
|
|
class RSSReaderWidget(QWidget): |
|
|
|
def __init__(self): |
|
super(RSSReaderWidget, self).__init__() |
|
|
|
self.feed_file_path = os.path.expanduser("~/.emacs.d/eaf/rss-reader/feeds.json") |
|
|
|
self.feed_area = QWidget() |
|
self.feed_list = QListWidget() |
|
self.feed_list.setStyleSheet( """QListWidget{background: #4D5250;}""") |
|
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.verticalScrollBar().setStyleSheet("QScrollBar {width:0px;}"); |
|
article_layout = QVBoxLayout() |
|
article_layout.setSpacing(0) |
|
article_layout.setContentsMargins(0, 0, 0, 0) |
|
|
|
self.browser = BrowserView() |
|
|
|
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: black; font-weight: bold; margin: 20px;}") |
|
welcome_title_label.setAlignment(Qt.AlignHCenter) |
|
|
|
add_subscription_label = QLabel("Press key 'a' to add subscription") |
|
add_subscription_label.setFont(QFont('Arial', 20)) |
|
add_subscription_label.setStyleSheet("QLabel {color: #333;}") |
|
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) |
|
|
|
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.fetch_feeds() |
|
|
|
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.update_article_area(self.feed_object_dict[feed_item.feed_link]) |
|
|
|
def handle_article(self, article_item): |
|
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_is_exits(feed_link): |
|
self.fetch_feed(feed_link, True) |
|
else: |
|
self.message_to_emacs.emit("Feed has exists: " + feed_link) |
|
|
|
def feed_is_exits(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_link, feed_title): |
|
if not os.path.exists(self.feed_file_path): |
|
basedir = os.path.dirname(self.feed_file_path) |
|
if not os.path.exists(basedir): |
|
os.makedirs(basedir) |
|
|
|
with open(self.feed_file_path, "a"): |
|
os.utime(self.feed_file_path, None) |
|
|
|
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 |
|
} |
|
|
|
with open(self.feed_file_path, "w") as f: |
|
f.write(json.dumps(feed_dict)) |
|
|
|
self.message_to_emacs.emit("Add feed: " + feed_link) |
|
except Exception: |
|
import traceback |
|
traceback.print_exc() |
|
|
|
with open(self.feed_file_path, "w") as f: |
|
f.write(json.dumps({feed_link : { |
|
"title": feed_title |
|
}})) |
|
|
|
self.message_to_emacs.emit("Add feed: " + feed_link) |
|
|
|
def handle_rss(self, feed_object, feed_link, feed_title, refresh_ui): |
|
self.feed_object_dict[feed_link] = feed_object |
|
|
|
self.save_feed(feed_link, feed_title) |
|
|
|
self.right_area.setCurrentIndex(1) |
|
|
|
feed_item = QListWidgetItem(self.feed_list) |
|
feed_item.feed_link = feed_link |
|
feed_item_widget = RSSFeedItem(feed_object, len(feed_object.entries)) |
|
feed_item.setSizeHint(feed_item_widget.sizeHint()) |
|
self.feed_list.addItem(feed_item) |
|
self.feed_list.setItemWidget(feed_item, feed_item_widget) |
|
|
|
if refresh_ui: |
|
self.update_article_area(feed_object) |
|
|
|
def update_article_area(self, feed_object): |
|
self.browser.setUrl(QUrl(feed_object.entries[0].link)) |
|
|
|
self.article_list.clear() |
|
for post in feed_object.entries: |
|
item_widget = RSSArticleItem(post) |
|
item = QListWidgetItem(self.article_list) |
|
item.post_link = item_widget.post_link |
|
item.setSizeHint(item_widget.sizeHint()) |
|
self.article_list.addItem(item) |
|
self.article_list.setItemWidget(item, item_widget) |
|
|
|
def handle_invalid_rss(self, feed_link): |
|
self.message_to_emacs.emit("Invalid feed link: " + feed_link) |
|
|
|
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) |
|
|
|
title_label = QLabel(feed_object.feed.title) |
|
title_label.setFont(QFont('Arial', 18)) |
|
title_label.setStyleSheet("color: #DDD") |
|
layout.addWidget(title_label) |
|
|
|
number_label = QLabel(str(post_num)) |
|
number_label.setFont(QFont('Arial', 16)) |
|
number_label.setStyleSheet("color: #AAA") |
|
layout.addStretch(1) |
|
layout.addWidget(number_label) |
|
|
|
self.setLayout(layout) |
|
|
|
class RSSArticleItem(QWidget): |
|
|
|
def __init__(self, post): |
|
super(RSSArticleItem, self).__init__() |
|
|
|
self.post_id = post.id |
|
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 |
|
|
|
layout = QVBoxLayout() |
|
layout.setSpacing(0) |
|
layout.setContentsMargins(10, 10, 0, 0) |
|
|
|
post_info_widget = QWidget() |
|
post_box = QHBoxLayout() |
|
post_box.setSpacing(10) |
|
post_box.setContentsMargins(0, 0, 0, 0) |
|
|
|
date_label = QLabel(date) |
|
date_label.setFont(QFont('Arial', 18)) |
|
post_box.addWidget(date_label) |
|
|
|
title_label = QLabel(post.title) |
|
title_label.setFont(QFont('Arial', 18)) |
|
post_box.addWidget(title_label) |
|
|
|
author_label = QLabel("(" + post.author + ")") |
|
author_label.setFont(QFont('Arial', 16)) |
|
post_box.addWidget(author_label) |
|
|
|
post_box.addStretch(1) |
|
|
|
post_info_widget.setLayout(post_box) |
|
|
|
description_doc = QTextDocument() |
|
description_doc.setHtml(post.description) |
|
description_label = QLabel(self.truncate_description(description_doc.toPlainText())) |
|
description_label.setWordWrap(True) |
|
description_label.setStyleSheet("color: #333") |
|
description_label.setFont(QFont("Arial", 16)) |
|
|
|
layout.addWidget(post_info_widget) |
|
layout.addWidget(description_label) |
|
|
|
self.setLayout(layout) |
|
|
|
def truncate_description(self, text): |
|
return (text[:90] + ' ...') if len(text) > 90 else text |
|
|
|
if __name__ == '__main__': |
|
import sys |
|
from PyQt5.QtWidgets import QApplication |
|
|
|
app = QApplication(sys.argv) |
|
|
|
w = RSSReaderWidget() |
|
w.resize(1920, 1080) |
|
w.show() |
|
|
|
fetchThread = FetchRSSThread() |
|
fetchThread.fetch_rss.connect(w.handle_rss) |
|
fetchThread.start() |
|
|
|
sys.exit(app.exec_())
|
|
|