#!/usr/bin/env python # # Copyright (C) 2012 Johannes 'josch' Schauer # # 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 # (at your option) 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 . #TODO # gettext # custom css/javascript # get addressbar/title/tabtitle right # adjust date/time correctly # no double entries for smbc # drag&drop doesnt create new items from gettext import gettext as _ from gi.repository import Gtk, GLib, GObject, GdkPixbuf, Pango, WebKit, Soup import yaml from urlparse import urlparse, urlunparse, urljoin import feedparser from lxml import etree from cStringIO import StringIO import shelve import time from datetime import datetime def get_time_pretty(time): """ return a pretty string representation of time given in unix time """ time = datetime.fromtimestamp(time) diff = datetime.now() - time if diff.days == 0: return _("Today")+" "+time.strftime("%H:%M") elif diff.days == 1: return _("Yesterday")+" "+time.strftime("%H:%M") elif diff.days < 7: return time.strftime("%a %H:%M") elif diff.days < 365: return time.strftime("%b %d %H:%M") else: return time.strftime("%b %d %Y") def pixbuf_new_from_file_in_memory(data, size=None): """ return a pixbuf of imagedata given by data optionally resize image to width/height tuple given by size """ loader = GdkPixbuf.PixbufLoader() if size: loader.set_size(*size) loader.write(data) loader.close() return loader.get_pixbuf() def find_shortcut_icon_link_in_html(data): """ data is a html document that will be parsed by lxml.etree.HTMLParser() returns the href attribute of the first link tag containing a rel attribute that lists icon as one of its types """ tree = etree.parse(StringIO(data), etree.HTMLParser()) #for link in tree.xpath("//link[@rel='icon' or @rel='shortcut icon']/@href"): # return link links = tree.findall("//link") for link in links: rel = link.attrib.get('rel') if not rel: continue if 'icon' not in rel.split(): continue href = link.attrib.get('href') if not href: continue return href def markup_escape_text(text): """ use GLib.markup_escape_text to escape text for usage in pango markup fields. it will escape <, >, &, ' and " and some html entities """ if not text: return "" return GLib.markup_escape_text(text.encode('utf-8')) class TabLabel(Gtk.HBox): """A class for Tab labels""" __gsignals__ = { "close": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_OBJECT,)) } def __init__ (self, title, child): """initialize the tab label""" Gtk.HBox.__init__(self) self.set_homogeneous(False) self.set_spacing(4) self.title = title self.child = child self.label = Gtk.Label(title) self.label.props.max_width_chars = 30 self.label.set_ellipsize(Pango.EllipsizeMode.MIDDLE) self.label.set_alignment(0.0, 0.5) icon = Gtk.Image.new_from_stock(Gtk.STOCK_ORIENTATION_PORTRAIT, Gtk.IconSize.BUTTON) close_image = Gtk.Image.new_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.MENU) close_button = Gtk.Button() close_button.set_relief(Gtk.ReliefStyle.NONE) def _close_tab (widget, child): self.emit("close", child) close_button.connect("clicked", _close_tab, child) close_button.set_image(close_image) self.pack_start(icon, False, False, 0) self.pack_start(self.label, True, True, 0) self.pack_start(close_button, False, False, 0) self.set_data("label", self.label) self.set_data("close-button", close_button) def tab_label_style_set_cb (tab_label, style): context = tab_label.get_pango_context() metrics = context.get_metrics(tab_label.get_style().font_desc, context.get_language()) char_width = metrics.get_approximate_digit_width() (_, width, height) = Gtk.icon_size_lookup(Gtk.IconSize.MENU) tab_label.set_size_request(20 * char_width/1024.0 + 2 * width, (metrics.get_ascent() + metrics.get_descent())/1024.0) self.connect("style-set", tab_label_style_set_cb) def set_label (self, text): """sets the text of this label""" self.label.set_label(text) class ContentPane (Gtk.Notebook): __gsignals__ = { "focus-view-title-changed": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_OBJECT, GObject.TYPE_STRING,)) } def __init__ (self): """initialize the content pane""" Gtk.Notebook.__init__(self) self.props.scrollable = True def _switch_page (notebook, page, page_num): child = self.get_nth_page(page_num) view = child.get_child() frame = view.get_main_frame() self.emit("focus-view-title-changed", frame, frame.props.title) self.connect("switch-page", _switch_page) self.show_all() self._hovered_uri = None def load_uri (self, text): """load the given uri in the current web view""" #child = self.get_nth_page(self.get_current_page()) child = self.get_nth_page(0); view = child.get_child() view.load_uri(text) def load_string (self, text): """load the given uri in the current web view""" #child = self.get_nth_page(self.get_current_page()) child = self.get_nth_page(0); view = child.get_child() view.load_string(text, "text/html", "utf-8", "") def back(self): child = self.get_nth_page(self.get_current_page()) view = child.get_child() view.go_back() def forward(self): child = self.get_nth_page(self.get_current_page()) view = child.get_child() view.go_forward() def refresh(self): child = self.get_nth_page(self.get_current_page()) view = child.get_child() view.reload() def zoom_in(self): child = self.get_nth_page(self.get_current_page()) view = child.get_child() view.zoom_in() def zoom_out(self): child = self.get_nth_page(self.get_current_page()) view = child.get_child() view.zoom_out() def zoom_100(self): child = self.get_nth_page(self.get_current_page()) view = child.get_child() if not (view.get_zoom_level() == 1.0): view.set_zoom_level(1.0) def set_focus(self): child = self.get_nth_page(self.get_current_page()) view = child.get_child() view.grab_focus() def print_page(self): child = self.get_nth_page(self.get_current_page()) view = child.get_child() mainframe = view.get_main_frame() mainframe.print_full(Gtk.PrintOperation(), Gtk.PrintOperationAction.PRINT_DIALOG); def new_tab (self, url=None): """creates a new page in a new tab""" # create the tab content web_view = WebKit.WebView() web_view.set_full_content_zoom(True) def _hovering_over_link_cb (view, title, uri): self._hovered_uri = uri web_view.connect("hovering-over-link", _hovering_over_link_cb) def _populate_page_popup_cb(view, menu): if self._hovered_uri: open_in_new_tab = Gtk.MenuItem() open_in_new_tab.set_label(_("Open Link in New Tab")) def _open_in_new_tab (menuitem, view): self.new_tab(self._hovered_uri) open_in_new_tab.connect("activate", _open_in_new_tab, view) menu.insert(open_in_new_tab, 0) menu.show_all() web_view.connect("populate-popup", _populate_page_popup_cb) def _view_load_finished_cb(view, frame): child = self.get_nth_page(self.get_current_page()) label = self.get_tab_label(child) title = frame.get_title() if not title: title = frame.get_uri() if title: label.set_label(title) #view.execute_script(open("youtube_html5_everywhere.user.js", "r").read()) view.execute_script(open("ythtml5.js", "r").read()) """ dom = view.get_dom_document() head = dom.get_head() if not head: return style = dom.create_element("style") style.set_attribute("type", "text/css") style.set_text_content("* {color:green;}") head.append_child(style) """ web_view.connect("load-finished", _view_load_finished_cb) def _title_changed_cb (view, frame, title): child = self.get_nth_page(self.get_current_page()) label = self.get_tab_label(child) label.set_label(title) self.emit("focus-view-title-changed", frame, title) web_view.connect("title-changed", _title_changed_cb) scrolled_window = Gtk.ScrolledWindow() scrolled_window.props.hscrollbar_policy = Gtk.PolicyType.AUTOMATIC scrolled_window.props.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC scrolled_window.add(web_view) # create the tab label = TabLabel(url, scrolled_window) def _close_tab (label, child): page_num = self.page_num(child) if page_num != -1: view = child.get_child() view.destroy() self.remove_page(page_num) self.set_show_tabs(self.get_n_pages() > 1) label.connect("close", _close_tab) label.show_all() new_tab_number = self.append_page(scrolled_window, label) #self.set_tab_label_packing(scrolled_window, False, False, Gtk.PACK_START) self.set_tab_label(scrolled_window, label) # hide the tab if there's only one self.set_show_tabs(self.get_n_pages() > 1) self.show_all() self.set_current_page(new_tab_number) # load the content self._hovered_uri = None if not url: web_view.load_uri("about:blank") else: web_view.load_uri(url) class WebToolbar(Gtk.Toolbar): __gsignals__ = { "load-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)), "back-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), "forward-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), "refresh-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), "new-tab-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), "zoom-in-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), "zoom-out-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), "zoom-100-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), "print-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), } def __init__(self): Gtk.Toolbar.__init__(self) self.set_style(Gtk.ToolbarStyle.ICONS) backButton = Gtk.ToolButton() backButton.set_stock_id(Gtk.STOCK_GO_BACK) def back_cb(button): self.emit("back-requested") backButton.connect("clicked", back_cb) self.insert(backButton, -1) forwardButton = Gtk.ToolButton() forwardButton.set_stock_id(Gtk.STOCK_GO_FORWARD) def forward_cb(button): self.emit("forward-requested") forwardButton.connect("clicked", forward_cb) self.insert(forwardButton, -1) self._entry = Gtk.Entry() def entry_activate_cb(entry): self.emit("load-requested", entry.props.text) self._entry.connect('activate', entry_activate_cb) entry_item = Gtk.ToolItem() entry_item.set_expand(True) entry_item.add(self._entry) self._entry.show() self.insert(entry_item, -1) refreshButton = Gtk.ToolButton() refreshButton.set_stock_id(Gtk.STOCK_REFRESH) def refresh_cb(button): self.emit("refresh-requested") refreshButton.connect("clicked", refresh_cb) self.insert(refreshButton, -1) zoom_in_button = Gtk.ToolButton() zoom_in_button.set_stock_id(Gtk.STOCK_ZOOM_IN) def zoom_in_cb(button): self.emit("zoom-in-requested") zoom_in_button.connect('clicked', zoom_in_cb) self.insert(zoom_in_button, -1) zoom_out_button = Gtk.ToolButton() zoom_out_button.set_stock_id(Gtk.STOCK_ZOOM_OUT) def zoom_out_cb(button): self.emit("zoom-out-requested") zoom_out_button.connect('clicked', zoom_out_cb) self.insert(zoom_out_button, -1) zoom_100_button = Gtk.ToolButton() zoom_100_button.set_stock_id(Gtk.STOCK_ZOOM_100) def zoom_hundred_cb(button): self.emit("zoom-100-requested") zoom_100_button.connect('clicked', zoom_hundred_cb) self.insert(zoom_100_button, -1) print_button = Gtk.ToolButton() print_button.set_stock_id(Gtk.STOCK_PRINT) def print_cb(button): self.emit("print-requested") print_button.connect('clicked', print_cb) self.insert(print_button, -1) addTabButton = Gtk.ToolButton() addTabButton.set_stock_id(Gtk.STOCK_ADD) def add_tab_cb(button): self.emit("new-tab-requested") addTabButton.connect("clicked", add_tab_cb) self.insert(addTabButton, -1) def location_set_text (self, text): self._entry.set_text(text) class EntryTree(Gtk.TreeView): __gsignals__ = { "item-selected": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_STRING, GObject.TYPE_STRING)) } def __init__(self, config, feeddb): Gtk.TreeView.__init__(self) self.feeddb = feeddb def on_cursor_changed_cb(treeview): _, it = self.get_selection().get_selected() if not it: return item = self.get_model().get_value(it, 0) entry = self.feeddb[self.feedurl] if entry['items'][item]['unread']: title = markup_escape_text(entry['items'][item]['title']) date = get_time_pretty(entry['items'][item]['date']) self.get_model().set_value(it, 1, title) self.get_model().set_value(it, 2, date) entry['items'][item]['unread'] = False entry['unread'] -= 1 self.feeddb[self.feedurl] = entry #self.feeddb.sync() # dont sync on every unread item - makes stuff extremely slow over time self.emit("item-selected", self.feedurl, item) self.connect("cursor-changed", on_cursor_changed_cb) self.models = dict() # id, date, label self.empty_model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT, GObject.TYPE_STRING) cell = Gtk.CellRendererText() column1 = Gtk.TreeViewColumn("Date", cell, markup=2) column1.set_sort_column_id(0) column2 = Gtk.TreeViewColumn("Headline", cell, markup=1) column2.set_sort_column_id(1) self.append_column(column1) self.append_column(column2) self.set_model(self.empty_model) self.feedurl = None for feedurl in config: if self.feeddb.get(feedurl): self.update(feedurl) def display(self, feedurl): if not feedurl or feedurl not in self.feeddb: self.set_model(self.empty_model) self.feedurl = None else: self.set_model(self.models[feedurl]) self.feedurl = feedurl def update(self, feedurl): model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING) # using model.set_sort_column_id is horribly slow, so append them # sorted instead items = sorted(self.feeddb[feedurl]['items'].iteritems(), key=lambda x: x[1]['date'], reverse=True) for guid, value in items: title = markup_escape_text(value.get('title', "")) date = get_time_pretty(value['date']) if value['unread']: title = ""+title+"" date = ""+date+"" model.append([guid, title, date]) def compare_date(model, a, b, data): item1 = model.get_value(a, 0) item2 = model.get_value(b, 0) items = self.feeddb[feedurl]['items'] return -1 if items[item1]['date'] < items[item2]['date'] else 1 model.set_sort_func(0, compare_date) def compare_title(model, a, b, data): item1 = model.get_value(a, 0) item2 = model.get_value(b, 0) items = self.feeddb[feedurl]['items'] return -1 if items[item1]['title'] < items[item2]['title'] else 1 model.set_sort_func(1, compare_title) # this takes loooooong #model.set_sort_column_id(0, Gtk.SortType.DESCENDING) self.models[feedurl] = model if self.feedurl == feedurl: self.set_model(model) class FeedTree(Gtk.TreeView): __gsignals__ = { "refresh-begin": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), "refresh-complete": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), "feed-selected": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)), "update-feed": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) } def __init__(self, config, feeddb): Gtk.TreeView.__init__(self) self.updating = set() self.feeddb = feeddb def on_button_press_event(treeview, event): if event.button != 3: return False pthinfo = self.get_path_at_pos(event.x, event.y) if pthinfo is None: return False path, col, cellx, celly = pthinfo #self.grab_focus() #self.set_cursor(path, col, 0) it = self.model.get_iter(path) popup = self.model.get_value(it, 3) popup.popup(None, None, None, None, event.button, event.time) #return True self.connect("button_press_event", on_button_press_event) def on_cursor_changed_cb(treeview): _, it = self.get_selection().get_selected() if it: self.emit("feed-selected", self.model.get_value(it, 0)) self.connect("cursor-changed", on_cursor_changed_cb) error_icon = self.render_icon(Gtk.STOCK_DIALOG_ERROR, Gtk.IconSize.MENU, None) folder_icon = self.render_icon(Gtk.STOCK_DIRECTORY, Gtk.IconSize.MENU, None) # url, label, icon, popup self.model = Gtk.TreeStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GdkPixbuf.Pixbuf, Gtk.Menu) # reorganize configuration data into categories categories = dict() for feedurl, feedprops in config.items(): if feedprops['category'] not in categories: categories[feedprops['category']] = list() categories[feedprops['category']].append(feedurl) for category, feeds in categories.items(): it = self.model.append(None, [None, category, folder_icon, None]) for feedurl in feeds: if self.feeddb.get(feedurl): feed_icon = self.feeddb[feedurl].get('favicon') if feed_icon: feed_icon = pixbuf_new_from_file_in_memory(feed_icon, (16, 16)) else: feed_icon = self.render_icon(Gtk.STOCK_FILE, Gtk.IconSize.MENU, None) label = markup_escape_text(self.feeddb[feedurl].get('title', feedurl)) unread = self.feeddb[feedurl].get('unread') if unread > 0: label = ""+label+" (%d)"%unread+"" else: feed_icon = error_icon label = feedurl # append new item to category # use resulting iter to update popup menu entry itc = self.model.append(it, [feedurl, label, feed_icon, None]) self.model.set_value(itc, 3, self.get_popup_menu(itc)) column = Gtk.TreeViewColumn("Feeds") col_cell_img = Gtk.CellRendererPixbuf() col_cell_text = Gtk.CellRendererText() column.pack_start(col_cell_img, False) column.pack_start(col_cell_text, True) column.add_attribute(col_cell_text, "markup", 1) column.add_attribute(col_cell_img, "pixbuf", 2) self.set_model(self.model) self.append_column(column) self.set_headers_visible(False) self.expand_all() self.show() self.session = Soup.SessionAsync.new() def mark_read_all(self): it = self.model.get_iter_first() while (it): itc = self.model.iter_children(it) while (itc): self.mark_read(itc, sync=False) itc = self.model.iter_next(itc) it = self.model.iter_next(it) self.feeddb.sync() def mark_read(self, it, sync=True): feedurl = self.model.get_value(it, 0) entry = self.feeddb.get(feedurl) if not entry: return entry['unread'] = 0 for item in entry['items'].values(): item['unread'] = False self.model.set_value(it, 1, markup_escape_text(entry['title'])) self.feeddb[feedurl] = entry self.emit("update-feed", feedurl) if sync: self.feeddb.sync() def disable_context_update(self): it = self.model.get_iter_first() while (it): itc = self.model.iter_children(it) while (itc): self.model.get_value(itc, 3).deactivate_update() itc = self.model.iter_next(itc) it = self.model.iter_next(it) def get_popup_menu(self, it): popup = Gtk.Menu() feedurl = self.model.get_value(it, 0) update_item = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_REFRESH, None) update_item.set_label(_("Update")) def on_update_item_activate_cb(menuitem): self.emit("refresh-begin") self.disable_context_update() self.updating.add(feedurl) self.update_feed(it) update_item.connect("activate", on_update_item_activate_cb) mark_item = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_APPLY, None) mark_item.set_label(_("Mark As Read")) def on_mark_item_activate_cb(menuitem): self.mark_read(it, sync=True) self.emit("update-feed", feedurl) mark_item.connect("activate", on_mark_item_activate_cb) popup.deactivate_update = lambda: update_item.set_sensitive(False) popup.activate_update = lambda: update_item.set_sensitive(True) popup.append(update_item) popup.append(mark_item) popup.show_all() return popup def update_view_all(self): it = self.model.get_iter_first() while (it): itc = self.model.iter_children(it) while (itc): feedurl = self.model.get_value(itc, 0) title = markup_escape_text(self.feeddb[feedurl]['title']) unread = self.feeddb[feedurl]['unread'] if unread > 0: title = ""+title+" (%d)"%unread+"" self.model.set_value(itc, 1, title) itc = self.model.iter_next(itc) it = self.model.iter_next(it) def update_feed_all(self): self.emit("refresh-begin") # disable updating via context menu self.disable_context_update() it = self.model.get_iter_first() while (it): # add feedurls to self.updating so that each feed can remove itself # from it once it is done and the last feed knows to take cleanup # actions itc = self.model.iter_children(it) while (itc): self.updating.add(self.model.get_value(itc, 0)) itc = self.model.iter_next(itc) itc = self.model.iter_children(it) while (itc): self.update_feed(itc) itc = self.model.iter_next(itc) it = self.model.iter_next(it) def update_feed_done(self, feedurl): self.updating.remove(feedurl) if self.updating: return self.feeddb.sync() # enable updating via context menu it = self.model.get_iter_first() while (it): itc = self.model.iter_children(it) while (itc): self.model.get_value(itc, 3).activate_update() itc = self.model.iter_next(itc) it = self.model.iter_next(it) # enable updating self.emit("refresh-complete") def update_feed(self, it): feedurl = self.model.get_value(it, 0) msg = Soup.Message.new("GET", feedurl) if self.feeddb.get(feedurl) and self.feeddb[feedurl].get('etag'): msg.request_headers.append('If-None-Match', self.feeddb[feedurl]['etag']) if self.feeddb.get(feedurl) and self.feeddb[feedurl].get('lastmodified'): msg.request_headers.append('If-Modified-Since', self.feeddb[feedurl]['lastmodified']) def complete_cb(session, msg, it): if msg.status_code not in [200, 304]: error_icon = self.render_icon(Gtk.STOCK_DIALOG_ERROR, Gtk.IconSize.MENU, None) self.model.set_value(it, 2, error_icon) self.update_feed_done(feedurl) return # get existing feedentry or create new one entry = self.feeddb.get(feedurl, dict()) if entry.get('favicon'): icon = pixbuf_new_from_file_in_memory(entry['favicon'], (16, 16)) else: icon = self.render_icon(Gtk.STOCK_FILE, Gtk.IconSize.MENU, None) self.model.set_value(it, 2, icon) if msg.status_code == 304: self.update_feed_done(feedurl) return # filling default values if not entry.has_key('unread'): entry['unread'] = 0 if not entry.has_key('items'): entry['items'] = dict() # updating etag and lastmodified if msg.response_headers.get_one('ETag'): entry['etag'] = msg.response_headers.get_one('ETag') if msg.response_headers.get_one('Last-Modified'): entry['lastmodified'] = msg.response_headers.get_one('Last-Modified') feed = feedparser.parse(msg.response_body.flatten().get_data()) # TODO check if parsing succeeded entry['title'] = feed.feed.get('title') self.model.set_value(it, 1, markup_escape_text(entry['title'])) # assumption: favicon never changes if not entry.has_key('favicon'): self.updating.add(feedurl+"_icon") self.update_icon(it, feedurl) for item in feed.entries: # use guid with fallback to link as identifier itemid = item.get("id", item.get("link")) if not itemid: # TODO: display error "cannot identify feeditems" break if entry['items'].has_key(itemid): # already exists continue new_item = { 'link': item.get('link'), 'title': item.get('title'), 'date': item.get('published_parsed'), 'content': item.get('content'), 'categories': [cat for _, cat in item.get('categories', [])] or None, 'unread': True } if not new_item['date']: new_item['date'] = item.get('updated_parsed') if new_item['date']: new_item['date'] = int(time.mktime(new_item['date'])) else: new_item['date'] = int(time.time()) if new_item['content']: new_item['content'] = new_item['content'][0] else: new_item['content'] = item.get('summary_detail') if new_item['content']: new_item['content'] = new_item['content']['value'] else: new_item['content'] = "" entry['items'][itemid] = new_item entry['unread'] += 1 if entry['unread'] > 0: self.model.set_value(it, 1, ''+markup_escape_text(entry['title'])+" (%d)"%entry['unread']+'') self.feeddb[feedurl] = entry self.emit("update-feed", feedurl) self.update_feed_done(feedurl) self.session.queue_message(msg, complete_cb, it) def update_icon(self, it, feedurl): msg = Soup.Message.new("GET", feedurl) def complete_cb(session, msg, it): if msg.status_code == 200: icon_url = find_shortcut_icon_link_in_html(msg.response_body.flatten().get_data()) if icon_url: icon_url = urljoin(feedurl, icon_url) self.update_icon_link(it, feedurl, icon_url) else: self.update_icon_favicon(it, feedurl) else: self.update_icon_favicon(it, feedurl) self.session.queue_message(msg, complete_cb, it) # get shortcut icon from link rel def update_icon_link(self, it, feedurl, url): msg = Soup.Message.new("GET", url) def complete_cb(session, msg, it): if msg.status_code == 200: data = msg.response_body.flatten().get_data() if len(data): icon = pixbuf_new_from_file_in_memory(data, (16, 16)) entry = self.feeddb[feedurl] entry['favicon'] = data self.feeddb[feedurl] = entry self.model.set_value(it, 2, icon) self.update_feed_done(feedurl) else: self.update_icon_favicon(it, feedurl) else: self.update_icon_favicon(it, feedurl) self.session.queue_message(msg, complete_cb, it) # get /favicon.ico def update_icon_favicon(self, it, feedurl): url = urlparse(feedurl) url = urlunparse((url.scheme, url.netloc, 'favicon.ico', '', '', '')) msg = Soup.Message.new("GET", url) def complete_cb(session, msg, it): data = None if msg.status_code == 200: data = msg.response_body.flatten().get_data() if len(data): icon = pixbuf_new_from_file_in_memory(data, (16, 16)) else: icon = self.render_icon(Gtk.STOCK_FILE, Gtk.IconSize.MENU, None) else: icon = self.render_icon(Gtk.STOCK_FILE, Gtk.IconSize.MENU, None) entry = self.feeddb[feedurl] entry['favicon'] = data self.feeddb[feedurl] = entry self.model.set_value(it, 2, icon) self.update_feed_done(feedurl+"_icon") self.session.queue_message(msg, complete_cb, it) class FeedReaderWindow(Gtk.Window): def __init__(self): Gtk.Window.__init__(self) feeddb = shelve.open("pyferea.db") with open('feeds.yaml') as f: config = yaml.load(f) toolbar = WebToolbar() def load_requested_cb(widget, text): if not text: return content_pane.load_uri(text) toolbar.connect("load-requested", load_requested_cb) def new_tab_requested_cb(toolbar): content_pane.new_tab("about:blank") toolbar.connect("new-tab-requested", new_tab_requested_cb) def back_requested_cb(toolbar): content_pane.back() toolbar.connect("back-requested", back_requested_cb) def forward_requested_cb(toolbar): content_pane.forward() toolbar.connect("forward-requested", forward_requested_cb) def refresh_requested_cb(toolbar): content_pane.refresh() toolbar.connect("refresh-requested", refresh_requested_cb) def zoom_in_requested_cb(toolbar): content_pane.zoom_in() toolbar.connect("zoom-in-requested", zoom_in_requested_cb) def zoom_out_requested_cb(toolbar): content_pane.zoom_out() toolbar.connect("zoom-out-requested", zoom_out_requested_cb) def zoom_100_requested_cb(toolbar): content_pane.zoom_100() toolbar.connect("zoom-100-requested", zoom_100_requested_cb) def print_requested_cb(toolbar): content_pane.print_page() toolbar.connect("print-requested", print_requested_cb) content_pane = ContentPane() def title_changed_cb (tabbed_pane, frame, title): if not title: title = frame.get_uri() self.set_title(_("PyFeRea - %s") % title) uri = frame.get_uri() if uri: toolbar.location_set_text(uri) content_pane.connect("focus-view-title-changed", title_changed_cb) entries = EntryTree(config, feeddb) def item_selected_cb(entry, feedurl, item): item = feeddb[feedurl]['items'][item] if config[feedurl]['loadlink']: content_pane.load_uri(item['link']) else: if item.get('categories'): content_string = "

%s

%s

"%(item['title'], ', '.join(item['categories'])) else: content_string = "

%s

"%item['title'] content_pane.load_string(content_string+item['content']) toolbar.location_set_text(item['link']) self.set_title(_("PyFeRea - %s")%item['title']) feedtree.update_view_all() entries.connect("item-selected", item_selected_cb) feedtree = FeedTree(config, feeddb) def feed_selected_cb(feedtree, feedurl): entries.display(feedurl) feedtree.connect("feed-selected", feed_selected_cb) def update_feed_cb(feedtree, feedurl): entries.update(feedurl) feedtree.connect("update-feed", update_feed_cb) def refresh_begin_cb(feedtree): button_refresh.set_label(_("Updating...")) button_refresh.set_sensitive(False) feedtree.connect("refresh-begin", refresh_begin_cb) def refresh_complete_cb(feedtree): button_refresh.set_label(_("Update All")) button_refresh.set_sensitive(True) feedtree.connect("refresh-complete", refresh_complete_cb) def timeout_cb(foo): if feedtree.updating: return True feedtree.update_feed_all() return True GLib.timeout_add_seconds(3600, timeout_cb, None) button_refresh = Gtk.Button() button_refresh.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_REFRESH, Gtk.IconSize.MENU)) button_refresh.set_label(_("Update All")) def refresh_cb(button): feedtree.update_feed_all() button_refresh.connect("clicked", refresh_cb) button_mark_all = Gtk.Button() button_mark_all.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_APPLY, Gtk.IconSize.MENU)) button_mark_all.set_label(_("Mark All As Read")) def mark_all_cb(button): feedtree.mark_read_all() button_mark_all.connect("clicked", mark_all_cb) hbox = Gtk.HBox() hbox.pack_start(button_refresh, False, False, 0) hbox.pack_start(button_mark_all, False, False, 0) scrolled_feedtree = Gtk.ScrolledWindow() scrolled_feedtree.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled_feedtree.add(feedtree) vbox2 = Gtk.VBox() vbox2.pack_start(hbox, False, False, 0) vbox2.pack_start(scrolled_feedtree, True, True, 0) scrolled_entries = Gtk.ScrolledWindow() scrolled_entries.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled_entries.add(entries) vbox = Gtk.VBox() vbox.pack_start(toolbar, False, False, 0) vbox.pack_start(content_pane, True, True, 0) vpane2 = Gtk.HPaned() vpane2.add1(scrolled_entries) vpane2.add2(vbox) vpane1 = Gtk.HPaned() vpane1.add1(vbox2) vpane1.add2(vpane2) self.add(vpane1) self.set_default_size(800, 600) def destroy_cb(window): feeddb.close() self.destroy() Gtk.main_quit() self.connect('destroy', destroy_cb) def key_press_event(window, event): if event.keyval == 49: # 1 feedtree.grab_focus() return True elif event.keyval == 50: # 2 entries.grab_focus() return True elif event.keyval == 51: # 3 content_pane.set_focus() return True return False self.connect('key-press-event', key_press_event) feedtree.update_feed_all() self.show_all() content_pane.new_tab() if __name__ == "__main__": jar = Soup.CookieJarText.new("cookies.txt", False) session = WebKit.get_default_session() session.add_feature(jar) session.set_property("timeout", 60) feedreader = FeedReaderWindow() Gtk.main()