From: Dale Whittaker Date: Sat, 25 Apr 2009 16:15:08 +0000 (+0200) Subject: Initial version of the Feed Panel extension X-Git-Url: https://spindle.queued.net/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=7419d17fb6a77a739873ed2774ff722dd3adcbaa;p=midori Initial version of the Feed Panel extension --- diff --git a/extensions/feed-panel/feed-atom.c b/extensions/feed-panel/feed-atom.c new file mode 100644 index 00000000..9731d688 --- /dev/null +++ b/extensions/feed-panel/feed-atom.c @@ -0,0 +1,343 @@ +/* + Copyright (C) 2009 Dale Whittaker + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + See the file COPYING for the full license text. +*/ + +#include "feed-atom.h" + +#define atom_get_link_attribute(item, attribute) \ + (gchar*)g_object_get_data (G_OBJECT (item), attribute) + +#define atom_set_link_attribute(item, attribute, value) \ + g_object_set_data (G_OBJECT (item), attribute, value) + +static gboolean +atom_is_valid (FeedParser* fparser) +{ + xmlNodePtr node; + + node = fparser->node; + + if (!(xmlStrcmp (node->name, BAD_CAST "feed")) && + !(xmlStrcmp (node->ns->href, BAD_CAST "http://www.w3.org/2005/Atom")) + ) + return TRUE; + + return FALSE; +} + +static gboolean +atom_update (FeedParser* fparser) +{ + xmlNodePtr node; + xmlNodePtr child; + gint64 date; + gint64 newdate; + + date = katze_item_get_added (fparser->item); + + node = fparser->node; + child = node->children; + while (child) + { + if (child->type == XML_ELEMENT_NODE) + { + if (!(xmlStrcmp (child->name, BAD_CAST "updated"))) + { + fparser->node = child; + newdate = feed_get_element_date (fparser); + fparser->node = node; + return (date != newdate); + } + } + child = child->next; + } + return TRUE; +} + +static gboolean +atom_preferred_link (const gchar* old, + const gchar* new) +{ + guint i; + gint iold; + gint inew; + gchar* rels[5] = + { + "enclosure", + "via", + "related", + "alternate", + "self", + }; + + iold = inew = -1; + for (i = 0; i < 5; i++) + { + if (old && g_str_equal (old, rels[i])) + iold = i; + if (new && g_str_equal (new, rels[i])) + inew = i; + } + return (inew > iold); +} + +static void +atom_get_link (KatzeItem* item, + xmlNodePtr node) +{ + gchar* oldtype; + gchar* newtype; + gchar* oldrel; + gchar* newrel; + gchar* href; + gboolean oldishtml; + gboolean newishtml; + gboolean newlink; + + newlink = FALSE; + oldtype = atom_get_link_attribute (item, "linktype"); + oldrel = atom_get_link_attribute (item, "linkrel"); + + newtype = (gchar*)xmlGetProp (node, BAD_CAST "type"); + newrel = (gchar*)xmlGetProp (node, BAD_CAST "rel"); + href = (gchar*)xmlGetProp (node, BAD_CAST "href"); + + if (!newrel) + newrel = g_strdup ("alternate"); + + oldishtml = (oldtype && g_str_equal (oldtype, "text/html")); + newishtml = (newtype && g_str_equal (newtype, "text/html")); + + /* prefer HTML links over anything else. + * if the previous link was already HTML, decide which link + * we prefer. + */ + if ((newishtml && oldishtml) || (!newishtml && !oldishtml)) + newlink = atom_preferred_link (oldrel, newrel); + else + newlink = newishtml; + + if (newlink) + { + katze_item_set_uri (item, href); + atom_set_link_attribute (item, "linkrel", newrel); + atom_set_link_attribute (item, "linktype", newrel); + } + else + { + xmlFree (href); + xmlFree (newrel); + xmlFree (newtype); + } +} + +static void +atom_preparse_entry (FeedParser* fparser) +{ + fparser->item = katze_item_new (); +} + +static void +atom_parse_entry (FeedParser* fparser) +{ + xmlNodePtr node; + gchar* content; + gint64 date; + + node = fparser->node; + content = NULL; + + if (!xmlStrcmp (node->name, BAD_CAST "id")) + { + content = feed_get_element_string (fparser); + katze_item_set_token (fparser->item, content); + } + else if (!xmlStrcmp (node->name, BAD_CAST "title")) + { + content = feed_get_element_string (fparser); + katze_item_set_name (fparser->item, content); + } + else if (!xmlStrcmp (node->name, BAD_CAST "summary")) + { + content = feed_get_element_string (fparser); + katze_item_set_text (fparser->item, content); + } + else if (!xmlStrcmp (node->name, BAD_CAST "updated")) + { + date = feed_get_element_date (fparser); + katze_item_set_added (fparser->item, date); + } + else if (!xmlStrcmp (node->name, BAD_CAST "icon")) + { + content = feed_get_element_string (fparser); + katze_item_set_icon (fparser->item, content); + } + /* FIXME content can be used in some cases where there + * is no summary, but it needs additional work, + * as it can be HTML, or base64 encoded. + * see the spec. + */ + else if (!xmlStrcmp (node->name, BAD_CAST "content")) + { + /* Only retrieve content if there is no summary */ + if (!katze_item_get_text (fparser->item)) + { + content = feed_get_element_string (fparser); + katze_item_set_text (fparser->item, content); + } + } + else if (!(xmlStrcmp (node->name, BAD_CAST "link"))) + atom_get_link (fparser->item, node); + + g_free (content); +} + +static void +atom_postparse_entry (FeedParser* fparser) +{ + if (!*fparser->error) + { + /* + * Verify that the required Atom elements are added + * (as per the spec) + */ + if (!katze_item_get_token (fparser->item) || + !katze_item_get_name (fparser->item) || + !katze_item_get_uri (fparser->item) || + !katze_item_get_added (fparser->item)) + { + feed_parser_set_error (fparser, FEED_PARSE_ERROR_MISSING_ELEMENT, + _("Failed to find required Atom entry elements in XML data.")); + } + } + + if (KATZE_IS_ITEM (fparser->item)) + { + atom_set_link_attribute (fparser->item, "linkrel", NULL); + atom_set_link_attribute (fparser->item, "linktype", NULL); + + if (*fparser->error) + { + g_object_unref (fparser->item); + fparser->item = NULL; + } + } +} + +static void +atom_parse_feed (FeedParser* fparser) +{ + FeedParser* eparser; + xmlNodePtr node; + gchar* content; + gint64 date; + + node = fparser->node; + content = NULL; + + if (!xmlStrcmp (node->name, BAD_CAST "id")) + { + content = feed_get_element_string (fparser); + katze_item_set_token (fparser->item, content); + } + else if (!xmlStrcmp (node->name, BAD_CAST "title")) + { + content = feed_get_element_string (fparser); + katze_item_set_name (fparser->item, content); + } + else if (!xmlStrcmp (node->name, BAD_CAST "subtitle")) + { + content = feed_get_element_string (fparser); + katze_item_set_text (fparser->item, content); + } + else if (!xmlStrcmp (node->name, BAD_CAST "updated")) + { + date = feed_get_element_date (fparser); + katze_item_set_added (fparser->item, date); + } + else if (!xmlStrcmp (node->name, BAD_CAST "icon")) + { + content = feed_get_element_string (fparser); + katze_item_set_icon (fparser->item, content); + } + else if (!(xmlStrcmp (node->name, BAD_CAST "link"))) + { + atom_get_link (fparser->item, node); + } + else if (!xmlStrcmp (node->name, BAD_CAST "entry")) + { + eparser = g_new0 (FeedParser, 1); + eparser->doc = fparser->doc; + eparser->node = fparser->node; + eparser->error = fparser->error; + eparser->preparse = atom_preparse_entry; + eparser->parse = atom_parse_entry; + eparser->postparse = atom_postparse_entry; + + feed_parse_node (eparser); + + if (KATZE_IS_ITEM (eparser->item)) + { + KatzeItem* item; + if (!(item = feed_item_exists (KATZE_ARRAY (fparser->item), eparser->item))) + katze_array_add_item (KATZE_ARRAY (fparser->item), eparser->item); + else + { + g_object_unref (eparser->item); + katze_array_move_item (KATZE_ARRAY (fparser->item), item, 0); + } + } + g_free (eparser); + + } + g_free (content); +} + +static void +atom_postparse_feed (FeedParser* fparser) +{ + if (KATZE_IS_ARRAY (fparser->item)) + { + atom_set_link_attribute (fparser->item, "linkrel", NULL); + atom_set_link_attribute (fparser->item, "linktype", NULL); + } + + if (!*fparser->error) + { + /* + * Verify that the required Atom elements are added + * (as per the spec) + */ + if (!katze_item_get_token (fparser->item) || + !katze_item_get_name (fparser->item) || + !katze_item_get_added (fparser->item)) + { + feed_parser_set_error (fparser, FEED_PARSE_ERROR_MISSING_ELEMENT, + _("Failed to find required Atom feed elements in XML data.")); + } + } +} + +FeedParser* +atom_init_parser (void) +{ + FeedParser* fparser; + + fparser = g_new0 (FeedParser, 1); + g_return_val_if_fail (fparser, NULL); + + fparser->isvalid = atom_is_valid; + fparser->update = atom_update; + fparser->parse = atom_parse_feed; + fparser->postparse = atom_postparse_feed; + + return fparser; +} + diff --git a/extensions/feed-panel/feed-atom.h b/extensions/feed-panel/feed-atom.h new file mode 100644 index 00000000..df73c8b5 --- /dev/null +++ b/extensions/feed-panel/feed-atom.h @@ -0,0 +1,25 @@ +/* + Copyright (C) 2009 Dale Whittaker + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + See the file COPYING for the full license text. +*/ + +#ifndef __FEED_ATOM_H__ +#define __FEED_ATOM_H__ + +#include "feed-parse.h" + +G_BEGIN_DECLS + +FeedParser* +atom_init_parser (void); + +G_END_DECLS + +#endif /* __FEED_ATOM_H__ */ + diff --git a/extensions/feed-panel/feed-panel.c b/extensions/feed-panel/feed-panel.c new file mode 100644 index 00000000..0abf8be6 --- /dev/null +++ b/extensions/feed-panel/feed-panel.c @@ -0,0 +1,850 @@ +/* + Copyright (C) 2009 Dale Whittaker + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + See the file COPYING for the full license text. +*/ + +#include "feed-panel.h" + +#include +#include +#include + +#if HAVE_CONFIG_H + #include +#endif + +#define STOCK_FEED_PANEL "feed-panel" + +struct _FeedPanel +{ + GtkVBox parent_instance; + + GtkWidget* toolbar; + GtkWidget* treeview; + GtkWidget* webview; + GtkWidget* delete; + GdkPixbuf* pixbuf; + KatzeNet* net; +}; + +struct _FeedPanelClass +{ + GtkVBoxClass parent_class; +}; + +static void +feed_panel_viewable_iface_init (MidoriViewableIface* iface); + +static void +feed_panel_insert_item (FeedPanel* panel, + GtkTreeStore* treestore, + GtkTreeIter* parent, + KatzeItem* item); + +static void +feed_panel_disconnect_feed (FeedPanel* panel, + KatzeArray* feed); + +G_DEFINE_TYPE_WITH_CODE (FeedPanel, feed_panel, GTK_TYPE_VBOX, + G_IMPLEMENT_INTERFACE (MIDORI_TYPE_VIEWABLE, + feed_panel_viewable_iface_init)); + +enum +{ + ADD_FEED, + REMOVE_FEED, + + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +static void +feed_panel_treeview_render_icon_cb (GtkTreeViewColumn* column, + GtkCellRenderer* renderer, + GtkTreeModel* model, + GtkTreeIter* iter, + FeedPanel* panel) +{ + GdkPixbuf* pixbuf; + KatzeItem* item; + KatzeItem* pitem; + const gchar* uri; + + gtk_tree_model_get (model, iter, 0, &item, -1); + g_assert (KATZE_IS_ITEM (item)); + + if (!KATZE_IS_ARRAY (item)) + { + pitem = katze_item_get_parent (item); + g_assert (KATZE_IS_ITEM (pitem)); + } + else + pitem = item; + + uri = katze_item_get_uri (pitem); + pixbuf = katze_net_load_icon (panel->net, uri, NULL, NULL, NULL); + if (!pixbuf) + pixbuf = panel->pixbuf; + + g_object_set (renderer, "pixbuf", pixbuf, NULL); + + if (pixbuf != panel->pixbuf) + g_object_unref (pixbuf); +} + +static void +feed_panel_treeview_render_text_cb (GtkTreeViewColumn* column, + GtkCellRenderer* renderer, + GtkTreeModel* model, + GtkTreeIter* iter, + GtkWidget* treeview) +{ + KatzeItem* item; + const gchar* title; + + gtk_tree_model_get (model, iter, 0, &item, -1); + g_assert (KATZE_IS_ITEM (item)); + + title = katze_item_get_name (item); + if (!title) + title = katze_item_get_text (item); + if (!title) + title = katze_item_get_uri (item); + + g_object_set (renderer, "text", title, NULL); + g_object_unref (item); +} + +static void +feed_panel_add_item_cb (KatzeArray* feed, + KatzeItem* child, + FeedPanel* panel) +{ + GtkTreeModel* model; + GtkTreeIter iter; + GtkTreeIter child_iter; + KatzeItem* item; + gint i; + + g_return_if_fail (KATZE_IS_ARRAY (feed)); + g_return_if_fail (KATZE_IS_ITEM (child)); + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (panel->treeview)); + + if (KATZE_IS_ARRAY (child)) + { + gtk_tree_store_insert_with_values (GTK_TREE_STORE (model), &child_iter, + NULL, G_MAXINT, 0, child, -1); + } + else + { + + i = 0; + while (gtk_tree_model_iter_nth_child (model, &iter, NULL, i++)) + { + gtk_tree_model_get (model, &iter, 0, &item, -1); + if (item == KATZE_ITEM (feed)) + { + gtk_tree_store_insert_with_values (GTK_TREE_STORE (model), &child_iter, + &iter, 0, 0, child, -1); + + g_object_unref (child); + g_object_unref (item); + break; + } + g_object_unref (item); + } + } + feed_panel_insert_item (panel, GTK_TREE_STORE (model), &child_iter, child); +} + +static void +feed_panel_remove_iter (GtkTreeModel* model, + KatzeItem* removed_item) +{ + guint i; + GtkTreeIter iter; + + i = 0; + while (gtk_tree_model_iter_nth_child (model, &iter, NULL, i)) + { + KatzeItem* item; + + gtk_tree_model_get (model, &iter, 0, &item, -1); + + if (item == removed_item) + { + gtk_tree_store_remove (GTK_TREE_STORE (model), &iter); + g_object_unref (item); + break; + } + g_object_unref (item); + i++; + } +} + +static void +feed_panel_remove_item_cb (KatzeArray* feed, + KatzeItem* child, + FeedPanel* panel) +{ + GtkTreeModel* model; + + g_assert (KATZE_IS_ARRAY (feed)); + g_assert (KATZE_IS_ITEM (child)); + + if (KATZE_IS_ARRAY (child)) + feed_panel_disconnect_feed (panel, KATZE_ARRAY (child)); + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (panel->treeview)); + feed_panel_remove_iter (model, child); + g_object_unref (child); +} + +static void +feed_panel_move_item_cb (KatzeArray* feed, + KatzeItem* child, + FeedPanel* panel) +{ + GtkTreeModel* model; + GtkTreeIter iter; + guint i; + + g_assert (KATZE_IS_ARRAY (feed)); + g_assert (KATZE_IS_ITEM (child)); + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (panel->treeview)); + + i = 0; + while (gtk_tree_model_iter_nth_child (model, &iter, NULL, i)) + { + KatzeItem* item; + + gtk_tree_model_get (model, &iter, 0, &item, -1); + + if (item == child) + { + gtk_tree_store_move_after (GTK_TREE_STORE (model), &iter, NULL); + g_object_unref (item); + break; + } + g_object_unref (item); + i++; + } +} + +static void +feed_panel_disconnect_feed (FeedPanel* panel, + KatzeArray* feed) +{ + KatzeItem* item; + guint i; + + g_return_if_fail (KATZE_IS_ARRAY (feed)); + + g_signal_handlers_disconnect_by_func (feed, + feed_panel_add_item_cb, panel); + g_signal_handlers_disconnect_by_func (feed, + feed_panel_remove_item_cb, panel); + g_signal_handlers_disconnect_by_func (feed, + feed_panel_move_item_cb, panel); + + i = 0; + while ((item = katze_array_get_nth_item (feed, i++))) + { + if (KATZE_IS_ARRAY (item)) + feed_panel_disconnect_feed (panel, KATZE_ARRAY (item)); + g_object_unref (item); + } +} + +static void +feed_panel_insert_item (FeedPanel* panel, + GtkTreeStore* treestore, + GtkTreeIter* parent, + KatzeItem* item) +{ + g_return_if_fail (KATZE_IS_ITEM (item)); + + if (KATZE_IS_ARRAY (item)) + { + g_signal_connect_after (item, "add-item", + G_CALLBACK (feed_panel_add_item_cb), panel); + g_signal_connect_after (item, "move-item", + G_CALLBACK (feed_panel_move_item_cb), panel); + + if (!parent) + { + g_signal_connect (item, "remove-item", + G_CALLBACK (feed_panel_remove_item_cb), panel); + } + } +} + +static void +feed_panel_row_activated_cb (GtkTreeView* treeview, + GtkTreePath* path, + GtkTreeViewColumn* column, + FeedPanel* panel) +{ + GtkTreeModel* model; + GtkTreeIter iter; + KatzeItem* item; + const gchar* uri; + + model = gtk_tree_view_get_model (treeview); + + if (gtk_tree_model_get_iter (model, &iter, path)) + { + gtk_tree_model_get (model, &iter, 0, &item, -1); + uri = katze_item_get_uri (item); + if (uri && *uri) + { + MidoriWebSettings* settings; + MidoriBrowser* browser; + gint n; + + browser = midori_browser_get_for_widget (GTK_WIDGET (panel)); + n = midori_browser_add_item (browser, item); + + settings = katze_object_get_object (browser, "settings"); + if (!katze_object_get_boolean (settings, "open-tabs-in-the-background")) + midori_browser_set_current_page (browser, n); + g_object_unref (settings); + + } + g_object_unref (item); + } +} + +static void +feed_panel_cursor_or_row_changed_cb (GtkTreeView* treeview, + FeedPanel* panel) +{ + GtkTreeModel* model; + GtkTreeIter iter; + KatzeItem* item; + gboolean sensitive = FALSE; + + if (katze_tree_view_get_selected_iter (treeview, &model, &iter)) + { + const gchar* uri; + const gchar* text; + + gtk_tree_model_get (model, &iter, 0, &item, -1); + + uri = katze_item_get_uri (item); + + if (uri) + { + if (KATZE_IS_ARRAY (item)) + { + gint64 date; + SoupDate* sdate; + + text = NULL; + date = katze_item_get_added (item); + if (date) + { + sdate = soup_date_new_from_time_t ((time_t) date); + text = g_strdup_printf ("Last updated %s.", soup_date_to_string (sdate, SOUP_DATE_HTTP)); + soup_date_free (sdate); + } + webkit_web_view_load_html_string ( + WEBKIT_WEB_VIEW (panel->webview), text ? text : "", uri); + g_free ((gchar*) text); + + sensitive = TRUE; + } + else + { + text = katze_item_get_text (item); + if (text) + { + webkit_web_view_load_html_string ( + WEBKIT_WEB_VIEW (panel->webview), text, uri); + } + } + g_object_unref (item); + } + } + if (GTK_IS_WIDGET (panel->delete)) + gtk_widget_set_sensitive (panel->delete, sensitive); +} + +static void +feed_panel_popup_item (GtkWidget* menu, + const gchar* stock_id, + const gchar* label, + KatzeItem* item, + gpointer callback, + FeedPanel* panel) +{ + const gchar* uri; + GtkWidget* menuitem; + + uri = katze_item_get_uri (item); + + menuitem = gtk_image_menu_item_new_from_stock (stock_id, NULL); + if (label) + gtk_label_set_text_with_mnemonic (GTK_LABEL (gtk_bin_get_child ( + GTK_BIN (menuitem))), label); + g_object_set_data (G_OBJECT (menuitem), "KatzeItem", item); + g_signal_connect (menuitem, "activate", G_CALLBACK (callback), panel); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), menuitem); + gtk_widget_show (menuitem); +} + +static void +feed_panel_open_activate_cb (GtkWidget* menuitem, + FeedPanel* panel) +{ + KatzeItem* item; + const gchar* uri; + + item = (KatzeItem*)g_object_get_data (G_OBJECT (menuitem), "KatzeItem"); + uri = katze_item_get_uri (item); + + if (uri && *uri) + { + MidoriBrowser* browser; + + browser = midori_browser_get_for_widget (GTK_WIDGET (panel)); + midori_browser_set_current_uri (browser, uri); + } +} + +static void +feed_panel_open_in_tab_activate_cb (GtkWidget* menuitem, + FeedPanel* panel) +{ + KatzeItem* item; + const gchar* uri; + guint n; + + item = (KatzeItem*)g_object_get_data (G_OBJECT (menuitem), "KatzeItem"); + + if ((uri = katze_item_get_uri (item)) && *uri) + { + MidoriWebSettings* settings; + MidoriBrowser* browser; + + browser = midori_browser_get_for_widget (GTK_WIDGET (panel)); + n = midori_browser_add_item (browser, item); + + settings = katze_object_get_object (browser, "settings"); + if (!katze_object_get_boolean (settings, "open-tabs-in-the-background")) + midori_browser_set_current_page (browser, n); + g_object_unref (settings); + } +} + +static void +feed_panel_open_in_window_activate_cb (GtkWidget* menuitem, + FeedPanel* panel) +{ + KatzeItem* item; + const gchar* uri; + + item = (KatzeItem*)g_object_get_data (G_OBJECT (menuitem), "KatzeItem"); + uri = katze_item_get_uri (item); + + if (uri && *uri) + { + MidoriBrowser* browser; + browser = midori_browser_get_for_widget (GTK_WIDGET (panel)); + g_signal_emit_by_name (browser, "new-window", uri); + } +} + +static void +feed_panel_delete_activate_cb (GtkWidget* menuitem, + FeedPanel* panel) +{ + KatzeItem* item; + + g_return_if_fail (FEED_IS_PANEL (panel)); + + item = (KatzeItem*)g_object_get_data (G_OBJECT (menuitem), "KatzeItem"); + g_signal_emit (panel, signals[REMOVE_FEED], 0, item); +} + +static void +feed_panel_popup (GtkWidget* widget, + GdkEventButton* event, + KatzeItem* item, + FeedPanel* panel) +{ + GtkWidget* menu; + + menu = gtk_menu_new (); + if (!KATZE_IS_ARRAY (item)) + { + feed_panel_popup_item (menu, GTK_STOCK_OPEN, NULL, + item, feed_panel_open_activate_cb, panel); + feed_panel_popup_item (menu, STOCK_TAB_NEW, _("Open in New _Tab"), + item, feed_panel_open_in_tab_activate_cb, panel); + feed_panel_popup_item (menu, STOCK_WINDOW_NEW, _("Open in New _Window"), + item, feed_panel_open_in_window_activate_cb, panel); + } + else + { + feed_panel_popup_item (menu, GTK_STOCK_DELETE, NULL, + item, feed_panel_delete_activate_cb, panel); + } + + sokoke_widget_popup (widget, GTK_MENU (menu), + event, SOKOKE_MENU_POSITION_CURSOR); +} + +static gboolean +feed_panel_button_release_event_cb (GtkWidget* widget, + GdkEventButton* event, + FeedPanel* panel) +{ + GtkTreeModel* model; + GtkTreeIter iter; + + if (event->button != 2 && event->button != 3) + return FALSE; + + if (katze_tree_view_get_selected_iter (GTK_TREE_VIEW (widget), &model, &iter)) + { + KatzeItem* item; + + gtk_tree_model_get (model, &iter, 0, &item, -1); + + if (event->button == 2) + { + const gchar* uri = katze_item_get_uri (item); + + if (uri && *uri) + { + MidoriWebSettings* settings; + MidoriBrowser* browser; + gint n; + + browser = midori_browser_get_for_widget (GTK_WIDGET (panel)); + n = midori_browser_add_item (browser, item); + + settings = katze_object_get_object (browser, "settings"); + if (!katze_object_get_boolean (settings, "open-tabs-in-the-background")) + midori_browser_set_current_page (browser, n); + g_object_unref (settings); + } + } + else + feed_panel_popup (widget, event, item, panel); + + g_object_unref (item); + return TRUE; + } + return FALSE; +} + +static void +feed_panel_popup_menu_cb (GtkWidget* widget, + FeedPanel* panel) +{ + GtkTreeModel* model; + GtkTreeIter iter; + KatzeItem* item; + + if (katze_tree_view_get_selected_iter (GTK_TREE_VIEW (widget), &model, &iter)) + { + gtk_tree_model_get (model, &iter, 0, &item, -1); + feed_panel_popup (widget, NULL, item, panel); + g_object_unref (item); + } +} + +void +feed_panel_add_feeds (FeedPanel* panel, + KatzeItem* feed) +{ + GtkTreeModel* model; + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (panel->treeview)); + g_assert (GTK_IS_TREE_MODEL (model)); + + feed_panel_insert_item (panel, GTK_TREE_STORE (model), NULL, feed); +} + +static gboolean +webview_button_press_event_cb (GtkWidget* widget, + GdkEventButton* event) +{ + /* Disable the popup menu */ + return (event->button == 3); +} + +static gboolean +webview_navigation_request_cb (WebKitWebView* web_view, + WebKitWebFrame* frame, + WebKitNetworkRequest* request, + WebKitWebNavigationAction* navigation_action, + WebKitWebPolicyDecision* policy_decision, + FeedPanel* panel) +{ + if (webkit_web_navigation_action_get_reason (navigation_action) == + WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) + { + MidoriBrowser* browser; + const gchar* uri; + gint n; + + browser = midori_browser_get_for_widget (GTK_WIDGET (panel)); + uri = webkit_network_request_get_uri (request); + n = midori_browser_add_uri (browser, uri); + midori_browser_set_current_page (browser, n); + } + return TRUE; +} + +static const gchar* +feed_panel_get_label (MidoriViewable* viewable) +{ + return _("Feeds"); +} + +static const gchar* +feed_panel_get_stock_id (MidoriViewable* viewable) +{ + return STOCK_FEED_PANEL; +} + +static void +feed_panel_add_clicked_cb (GtkWidget* toolitem, + FeedPanel* panel) +{ + g_return_if_fail (FEED_IS_PANEL (panel)); + + g_signal_emit (panel, signals[ADD_FEED], 0); +} + +static void +feed_panel_delete_clicked_cb (GtkWidget* toolitem, + FeedPanel* panel) +{ + GtkTreeModel* model; + GtkTreeIter iter; + + g_return_if_fail (FEED_IS_PANEL (panel)); + + if (katze_tree_view_get_selected_iter (GTK_TREE_VIEW (panel->treeview), + &model, &iter)) + { + KatzeItem* item; + + gtk_tree_model_get (model, &iter, 0, &item, -1); + g_signal_emit (panel, signals[REMOVE_FEED], 0, item); + g_object_unref (item); + } +} + +static GtkWidget* +feed_panel_get_toolbar (MidoriViewable* viewable) +{ + FeedPanel* panel = FEED_PANEL (viewable); + + if (!panel->toolbar) + { + GtkWidget* toolbar; + GtkToolItem* toolitem; + + toolbar = gtk_toolbar_new (); + gtk_toolbar_set_style (GTK_TOOLBAR (toolbar), GTK_TOOLBAR_BOTH_HORIZ); + gtk_toolbar_set_icon_size (GTK_TOOLBAR (toolbar), GTK_ICON_SIZE_BUTTON); + panel->toolbar = toolbar; + toolitem = gtk_tool_button_new_from_stock (GTK_STOCK_ADD); + gtk_widget_set_tooltip_text (GTK_WIDGET (toolitem), _("Add new feed")); + gtk_tool_item_set_is_important (toolitem, TRUE); + g_signal_connect (toolitem, "clicked", + G_CALLBACK (feed_panel_add_clicked_cb), panel); + gtk_toolbar_insert (GTK_TOOLBAR (toolbar), toolitem, -1); + gtk_widget_show (GTK_WIDGET (toolitem)); + toolitem = gtk_tool_button_new_from_stock (GTK_STOCK_DELETE); + gtk_widget_set_tooltip_text (GTK_WIDGET (toolitem), _("Delete feed")); + g_signal_connect (toolitem, "clicked", + G_CALLBACK (feed_panel_delete_clicked_cb), panel); + gtk_toolbar_insert (GTK_TOOLBAR (toolbar), toolitem, -1); + gtk_widget_show (GTK_WIDGET (toolitem)); + panel->delete = GTK_WIDGET (toolitem);; + + feed_panel_cursor_or_row_changed_cb ( + GTK_TREE_VIEW (panel->treeview), panel); + g_signal_connect (panel->delete, "destroy", + G_CALLBACK (gtk_widget_destroyed), &panel->delete); + } + + return panel->toolbar; +} + +static void +feed_panel_finalize (GObject* object) +{ + FeedPanel* panel = FEED_PANEL (object); + + g_object_unref (panel->pixbuf); + g_object_unref (panel->net); +} + +static void +feed_panel_viewable_iface_init (MidoriViewableIface* iface) +{ + iface->get_stock_id = feed_panel_get_stock_id; + iface->get_label = feed_panel_get_label; + iface->get_toolbar = feed_panel_get_toolbar; +} + +static void +feed_panel_class_init (FeedPanelClass* class) +{ + GObjectClass* gobject_class; + + signals[ADD_FEED] = g_signal_new ( + "add-feed", + G_TYPE_FROM_CLASS (class), + (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION), + 0, + 0, + NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[REMOVE_FEED] = g_signal_new ( + "remove-feed", + G_TYPE_FROM_CLASS (class), + (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION), + 0, + 0, + NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + + gobject_class = G_OBJECT_CLASS (class); + gobject_class->finalize = feed_panel_finalize; +} + +static void +feed_panel_init (FeedPanel* panel) +{ + GtkTreeStore* model; + GtkWidget* treewin; + GtkWidget* treeview; + GtkWidget* webview; + GtkWidget* paned; + GtkTreeViewColumn* column; + GtkCellRenderer* renderer_pixbuf; + GtkCellRenderer* renderer_text; + GtkIconFactory *factory; + GtkIconSource *icon_source; + GtkIconSet *icon_set; + WebKitWebSettings* settings; + PangoFontDescription* font_desc; + const gchar* family; + gint size; + GtkStockItem items[] = + { + { STOCK_FEED_PANEL, N_("_Feeds"), 0, 0, NULL } + }; + + factory = gtk_icon_factory_new (); + gtk_stock_add (items, G_N_ELEMENTS (items)); + icon_set = gtk_icon_set_new (); + icon_source = gtk_icon_source_new (); + gtk_icon_source_set_icon_name (icon_source, STOCK_NEWS_FEED); + gtk_icon_set_add_source (icon_set, icon_source); + gtk_icon_source_free (icon_source); + gtk_icon_factory_add (factory, STOCK_FEED_PANEL, icon_set); + gtk_icon_set_unref (icon_set); + gtk_icon_factory_add_default (factory); + g_object_unref (factory); + + panel->net = katze_net_new (); + + model = gtk_tree_store_new (1, KATZE_TYPE_ITEM); + treeview = gtk_tree_view_new_with_model (GTK_TREE_MODEL (model)); + panel->treeview = treeview; + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (treeview), FALSE); + column = gtk_tree_view_column_new (); + renderer_pixbuf = gtk_cell_renderer_pixbuf_new (); + gtk_tree_view_column_pack_start (column, renderer_pixbuf, FALSE); + gtk_tree_view_column_set_cell_data_func (column, renderer_pixbuf, + (GtkTreeCellDataFunc)feed_panel_treeview_render_icon_cb, + panel, NULL); + renderer_text = gtk_cell_renderer_text_new (); + gtk_tree_view_column_pack_start (column, renderer_text, FALSE); + gtk_tree_view_column_set_cell_data_func (column, renderer_text, + (GtkTreeCellDataFunc)feed_panel_treeview_render_text_cb, + treeview, NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column); + g_object_unref (model); + g_object_connect (treeview, + "signal::row-activated", + feed_panel_row_activated_cb, panel, + "signal::cursor-changed", + feed_panel_cursor_or_row_changed_cb, panel, + "signal::columns-changed", + feed_panel_cursor_or_row_changed_cb, panel, + "signal::button-release-event", + feed_panel_button_release_event_cb, panel, + "signal::popup-menu", + feed_panel_popup_menu_cb, panel, + NULL); + gtk_widget_show (treeview); + + webview = webkit_web_view_new (); + font_desc = treeview->style->font_desc; + family = pango_font_description_get_family (font_desc); + size = pango_font_description_get_size (font_desc) / PANGO_SCALE; + settings = webkit_web_settings_new (); + g_object_set (settings, "default-font-family", family, + "default-font-size", size, NULL); + g_object_set (webview, "settings", settings, NULL); + gtk_widget_set_size_request (webview, -1, 50); + g_object_connect (webview, + "signal::navigation-policy-decision-requested", + webview_navigation_request_cb, panel, + "signal::button-press-event", + webview_button_press_event_cb, NULL, + "signal::button-release-event", + webview_button_press_event_cb, NULL, + NULL); + panel->webview = webview; + + treewin = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (treewin), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (treewin), + GTK_SHADOW_IN); + gtk_container_add (GTK_CONTAINER (treewin), treeview); + gtk_widget_show (treewin); + + paned = gtk_vpaned_new (); + gtk_paned_pack1 (GTK_PANED (paned), treewin, TRUE, FALSE); + gtk_paned_pack2 (GTK_PANED (paned), webview, TRUE, FALSE); + gtk_box_pack_start (GTK_BOX (panel), paned, TRUE, TRUE, 0); + gtk_widget_show (webview); + gtk_widget_show (paned); + + panel->pixbuf = gtk_widget_render_icon (treeview, + STOCK_NEWS_FEED, GTK_ICON_SIZE_MENU, NULL); +} + +GtkWidget* +feed_panel_new (void) +{ + FeedPanel* panel = g_object_new (FEED_TYPE_PANEL, NULL); + + return GTK_WIDGET (panel); +} + diff --git a/extensions/feed-panel/feed-panel.h b/extensions/feed-panel/feed-panel.h new file mode 100644 index 00000000..2530feb3 --- /dev/null +++ b/extensions/feed-panel/feed-panel.h @@ -0,0 +1,51 @@ +/* + Copyright (C) 2009 Dale Whittaker + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + See the file COPYING for the full license text. +*/ + +#ifndef __FEED_PANEL_H__ +#define __FEED_PANEL_H__ + +#include + +G_BEGIN_DECLS + +#define FEED_TYPE_PANEL \ + (feed_panel_get_type ()) +#define FEED_PANEL(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), FEED_TYPE_PANEL, FeedPanel)) +#define FEED_PANEL_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST ((klass), FEED_TYPE_PANEL, FeedPanelClass)) +#define FEED_IS_PANEL(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), FEED_TYPE_PANEL)) +#define FEED_IS_PANEL_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), FEED_TYPE_PANEL)) +#define FEED_PANEL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), FEED_TYPE_PANEL, FeedPanelClass)) + +typedef struct _FeedPanel FeedPanel; +typedef struct _FeedPanelClass FeedPanelClass; + +void +feed_panel_add_feeds (FeedPanel* panel, + KatzeItem* feed); + +void +feed_panel_set_editable (FeedPanel* panel, + gboolean editable); + +GType +feed_panel_get_type (void); + +GtkWidget* +feed_panel_new (void); + +G_END_DECLS + +#endif /* __FEED_PANEL_H__ */ diff --git a/extensions/feed-panel/feed-parse.c b/extensions/feed-panel/feed-parse.c new file mode 100644 index 00000000..442e5ed5 --- /dev/null +++ b/extensions/feed-panel/feed-parse.c @@ -0,0 +1,218 @@ +/* + Copyright (C) 2009 Dale Whittaker + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + See the file COPYING for the full license text. +*/ + +#include "feed-parse.h" +#include + +gchar* +feed_get_element_string (FeedParser* fparser) +{ + xmlNodePtr node; + + node = fparser->node; + + if (!node->children || + xmlIsBlankNode (node->children) || + node->children->type != XML_TEXT_NODE + ) + { + /* Some servers add required elements with no content, + * create a dummy string to handle it. + */ + return g_strdup (" "); + } + + return (gchar* )xmlNodeListGetString (fparser->doc, node->children, 1); +} + +gint64 +feed_get_element_date (FeedParser* fparser) +{ + time_t date; + gchar* content; + + date = 0; + content = feed_get_element_string (fparser); + + if (content) + { + SoupDate* sdate; + + sdate = soup_date_new_from_string (content); + date = soup_date_to_time_t (sdate); + soup_date_free (sdate); + g_free (content); + } + return ((gint64)date); +} + +KatzeItem* +feed_item_exists (KatzeArray* array, + KatzeItem* item) +{ + const gchar* guid; + gchar* hstr; + guint hash; + + guid = katze_item_get_token (item); + if (!guid) + { + hstr = g_strjoin (NULL, + katze_item_get_name (item), + katze_item_get_uri (item), + katze_item_get_text (item), + NULL); + hash = g_str_hash (hstr); + g_free (hstr); + + hstr = g_strdup_printf ("%u", hash); + katze_item_set_token (item, hstr); + g_free (hstr); + + guid = katze_item_get_token (item); + } + + return (katze_array_find_token (array, guid)); +} + +void +feed_parse_node (FeedParser* fparser) +{ + xmlNodePtr node; + xmlNodePtr child; + + if (!*fparser->error) + { + if (fparser->preparse) + (*fparser->preparse) (fparser); + + if (fparser->parse) + { + node = fparser->node; + child = node->last; + + while (child) + { + if (child->type == XML_ELEMENT_NODE) + { + fparser->node = child; + + (*fparser->parse) (fparser); + + if (*fparser->error) + break; + } + child = child->prev; + } + fparser->node = node; + } + + if (fparser->postparse) + (*fparser->postparse) (fparser); + } +} + +static void +feed_parse_doc (xmlDocPtr doc, + GSList* parsers, + KatzeArray* array, + GError** error) +{ + FeedParser* fparser; + xmlNodePtr root; + gboolean isvalid; + + root = xmlDocGetRootElement (doc); + + if (!root) + { + *error = g_error_new (FEED_PARSE_ERROR, + FEED_PARSE_ERROR_MISSING_ELEMENT, + _("Failed to find root element in feed XML data.")); + return; + } + + while (parsers) + { + fparser = (FeedParser*)parsers->data; + fparser->error = error; + fparser->doc = doc; + fparser->node = root; + + if (fparser && fparser->isvalid) + { + isvalid = (*fparser->isvalid) (fparser); + + if (*fparser->error) + return; + + if (isvalid) + { + fparser->item = KATZE_ITEM (array); + + if (fparser->update && + (*fparser->update) (fparser)) + feed_parse_node (fparser); + } + } + + fparser->error = NULL; + fparser->doc = NULL; + fparser->node = NULL; + + if (isvalid) + return; + + parsers = g_slist_next (parsers); + } + + *error = g_error_new (FEED_PARSE_ERROR, + FEED_PARSE_ERROR_INVALID_FORMAT, + _("Unsupported feed format.")); +} + +gboolean +parse_feed (gchar* data, + gint64 length, + GSList* parsers, + KatzeArray* array, + GError** error) +{ + xmlDocPtr doc; + xmlErrorPtr xerror; + + LIBXML_TEST_VERSION + + doc = xmlReadMemory ( + data, length, "feedfile.xml", NULL, + XML_PARSE_NOWARNING | XML_PARSE_NOERROR /*| XML_PARSE_RECOVER*/ + ); + + if (doc) + { + feed_parse_doc (doc, parsers, array, error); + xmlFreeDoc (doc); + } + else + { + xerror = xmlGetLastError (); + *error = g_error_new (FEED_PARSE_ERROR, + FEED_PARSE_ERROR_PARSE, + _("Failed to parse XML feed: %s"), + xerror->message); + xmlResetLastError (); + } + xmlCleanupParser (); + xmlMemoryDump (); + + return *error ? FALSE : TRUE; +} + diff --git a/extensions/feed-panel/feed-parse.h b/extensions/feed-panel/feed-parse.h new file mode 100644 index 00000000..493b7ef7 --- /dev/null +++ b/extensions/feed-panel/feed-parse.h @@ -0,0 +1,77 @@ +/* + Copyright (C) 2009 Dale Whittaker + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + See the file COPYING for the full license text. +*/ + +#ifndef __FEED_PARSE_H__ +#define __FEED_PARSE_H__ + +#include + +#if HAVE_CONFIG_H + #include +#endif + +#include +#include + +G_BEGIN_DECLS + +#define FEED_PARSE_ERROR g_quark_from_string("FEED_PARSE_ERROR") + +typedef enum +{ + FEED_PARSE_ERROR_PARSE, + FEED_PARSE_ERROR_INVALID_FORMAT, + FEED_PARSE_ERROR_INVALID_VERSION, + FEED_PARSE_ERROR_MISSING_ELEMENT + +} FeedBarError; + +typedef struct _FeedParser +{ + xmlDocPtr doc; /* The XML document */ + xmlNodePtr node; /* The XML node at a specific point */ + KatzeItem* item; + GError** error; + + gboolean (*isvalid) (struct _FeedParser* fparser); + gboolean (*update) (struct _FeedParser* fparser); + void (*preparse) (struct _FeedParser* fparser); + void (*parse) (struct _FeedParser* fparser); + void (*postparse) (struct _FeedParser* fparser); + +} FeedParser; + +#define feed_parser_set_error(fparser, err, msg) \ + *(fparser)->error = g_error_new ( \ + FEED_PARSE_ERROR, (err), (msg)) +gchar* +feed_get_element_string (FeedParser* fparser); + +gint64 +feed_get_element_date (FeedParser* fparser); + +KatzeItem* +feed_item_exists (KatzeArray* array, + KatzeItem* item); +void +feed_parse_node (FeedParser* fparser); + +gboolean +parse_feed (gchar* data, + gint64 length, + GSList* parsers, + KatzeArray* array, + GError** error); + +G_END_DECLS + +#endif /* __FEED_PARSE_H__ */ + diff --git a/extensions/feed-panel/feed-rss.c b/extensions/feed-panel/feed-rss.c new file mode 100644 index 00000000..d830806e --- /dev/null +++ b/extensions/feed-panel/feed-rss.c @@ -0,0 +1,249 @@ +/* + Copyright (C) 2009 Dale Whittaker + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + See the file COPYING for the full license text. +*/ + +#include "feed-rss.h" + +static gboolean +rss_is_valid (FeedParser* fparser) +{ + xmlNodePtr node; + xmlNodePtr child; + xmlChar* str; + gboolean valid; + + node = fparser->node; + + if (!(xmlStrcmp (node->name, BAD_CAST "rss"))) + { + if ((str = xmlGetProp (node, BAD_CAST "version"))) + { + valid = !xmlStrcmp (str, BAD_CAST "2.0"); + xmlFree (str); + + if (valid) + { + child = node->children; + while (child) + { + if (child->type == XML_ELEMENT_NODE && + !(xmlStrcmp (child->name, BAD_CAST "channel"))) + { + fparser->node = child; + return TRUE; + } + child = child->next; + } + + feed_parser_set_error (fparser, FEED_PARSE_ERROR_MISSING_ELEMENT, + _("Failed to find channel element in RSS XML data.")); + } + else + { + feed_parser_set_error (fparser, FEED_PARSE_ERROR_INVALID_VERSION, + _("Unsupported RSS version found.")); + } + } + } + return FALSE; +} + +static gboolean +rss_update (FeedParser* fparser) +{ + xmlNodePtr node; + xmlNodePtr child; + gint64 date; + gint64 newdate; + + date = katze_item_get_added (fparser->item); + + node = fparser->node; + child = node->children; + while (child) + { + if (child->type == XML_ELEMENT_NODE) + { + if (!(xmlStrcmp (child->name, BAD_CAST "lastBuildDate"))) + { + fparser->node = child; + newdate = feed_get_element_date (fparser); + fparser->node = node; + return (date != newdate); + } + } + child = child->next; + } + return TRUE; +} + +static void +rss_preparse_item (FeedParser* fparser) +{ + fparser->item = katze_item_new (); +} + +static void +rss_parse_item (FeedParser* fparser) +{ + xmlNodePtr node; + gchar* content; + gint64 date; + + node = fparser->node; + content = NULL; + + if (!xmlStrcmp (node->name, BAD_CAST "guid")) + { + content = feed_get_element_string (fparser); + katze_item_set_token (fparser->item, content); + } + else if (!xmlStrcmp (node->name, BAD_CAST "title")) + { + content = feed_get_element_string (fparser); + katze_item_set_name (fparser->item, content); + } + else if (!xmlStrcmp (node->name, BAD_CAST "description")) + { + content = feed_get_element_string (fparser); + katze_item_set_text (fparser->item, content); + } + else if (!xmlStrcmp (node->name, BAD_CAST "pubDate")) + { + date = feed_get_element_date (fparser); + katze_item_set_added (fparser->item, date); + } + else if (!(xmlStrcmp (node->name, BAD_CAST "link"))) + { + content = feed_get_element_string (fparser); + katze_item_set_uri (fparser->item, content); + } + g_free (content); +} + +static void +rss_postparse_item (FeedParser* fparser) +{ + if (!*fparser->error) + { + /* + * Verify that the required Atom elements are added + * (as per the spec) + */ + if (!katze_item_get_name (fparser->item) && + !katze_item_get_text (fparser->item)) + { + feed_parser_set_error (fparser, FEED_PARSE_ERROR_MISSING_ELEMENT, + _("Failed to find required RSS item elements in XML data.")); + } + } + + if (*fparser->error && KATZE_IS_ITEM (fparser->item)) + { + g_object_unref (fparser->item); + fparser->item = NULL; + } +} + +static void +rss_parse_channel (FeedParser* fparser) +{ + FeedParser* eparser; + xmlNodePtr node; + gchar* content; + gint64 date; + + node = fparser->node; + content = NULL; + + if (!xmlStrcmp (node->name, BAD_CAST "title")) + { + content = feed_get_element_string (fparser); + katze_item_set_name (fparser->item, content); + } + else if (!xmlStrcmp (node->name, BAD_CAST "description")) + { + content = feed_get_element_string (fparser); + katze_item_set_text (fparser->item, content); + } + else if (!xmlStrcmp (node->name, BAD_CAST "lastBuildDate")) + { + date = feed_get_element_date (fparser); + katze_item_set_added (fparser->item, date); + } + else if (!(xmlStrcmp (node->name, BAD_CAST "link"))) + { + content = feed_get_element_string (fparser); + katze_item_set_uri (fparser->item, content); + } + else if (!xmlStrcmp (node->name, BAD_CAST "item")) + { + eparser = g_new0 (FeedParser, 1); + eparser->doc = fparser->doc; + eparser->node = fparser->node; + eparser->error = fparser->error; + eparser->preparse = rss_preparse_item; + eparser->parse = rss_parse_item; + eparser->postparse = rss_postparse_item; + + feed_parse_node (eparser); + + if (KATZE_IS_ITEM (eparser->item)) + { + KatzeItem* item; + if (!(item = feed_item_exists (KATZE_ARRAY (fparser->item), eparser->item))) + katze_array_add_item (KATZE_ARRAY (fparser->item), eparser->item); + else + { + g_object_unref (eparser->item); + katze_array_move_item (KATZE_ARRAY (fparser->item), item, 0); + } + } + g_free (eparser); + + } + g_free (content); +} + +static void +rss_postparse_channel (FeedParser* fparser) +{ + if (!*fparser->error) + { + /* + * Verify that the required Atom elements are added + * (as per the spec) + */ + if (!katze_item_get_name (fparser->item) || + !katze_item_get_text (fparser->item) || + !katze_item_get_uri (fparser->item)) + { + feed_parser_set_error (fparser, FEED_PARSE_ERROR_MISSING_ELEMENT, + _("Failed to find required RSS channel elements in XML data.")); + } + } +} + +FeedParser* +rss_init_parser (void) +{ + FeedParser* fparser; + + fparser = g_new0 (FeedParser, 1); + g_return_val_if_fail (fparser, NULL); + + fparser->isvalid = rss_is_valid; + fparser->update = rss_update; + fparser->parse = rss_parse_channel; + fparser->postparse = rss_postparse_channel; + + return fparser; +} + diff --git a/extensions/feed-panel/feed-rss.h b/extensions/feed-panel/feed-rss.h new file mode 100644 index 00000000..d5ccc047 --- /dev/null +++ b/extensions/feed-panel/feed-rss.h @@ -0,0 +1,25 @@ +/* + Copyright (C) 2009 Dale Whittaker + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + See the file COPYING for the full license text. +*/ + +#ifndef __FEED_RSS_H__ +#define __FEED_RSS_H__ + +#include "feed-parse.h" + +G_BEGIN_DECLS + +FeedParser* +rss_init_parser (void); + +G_END_DECLS + +#endif /* __FEED_RSS_H__ */ + diff --git a/extensions/feed-panel/main.c b/extensions/feed-panel/main.c new file mode 100644 index 00000000..e1c4d723 --- /dev/null +++ b/extensions/feed-panel/main.c @@ -0,0 +1,472 @@ +/* + Copyright (C) 2009 Dale Whittaker + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + See the file COPYING for the full license text. +*/ + +#include "feed-panel.h" +#include "feed-atom.h" +#include "feed-rss.h" + +#include + +#define EXTENSION_NAME "FeedPanel" +#define UPDATE_FREQ 10 + +#define feed_get_flags(feed) \ + GPOINTER_TO_INT (g_object_get_data (G_OBJECT ((feed)), "flags")) + +#define feed_set_flags(feed, flags) \ + g_object_set_data (G_OBJECT ((feed)), "flags", \ + GINT_TO_POINTER ((flags))) + +#define feed_has_flags(feed, flags) \ + ((flags) & feed_get_flags ((feed))) + +#define feed_add_flags(feed, flags) \ + feed_set_flags ((feed), (feed_get_flags ((feed)) | (flags))) + +#define feed_remove_flags(feed, flags) \ + feed_set_flags ((feed), (feed_get_flags ((feed)) & ~(flags))) + +typedef struct +{ + MidoriBrowser* browser; + MidoriExtension* extension; + GtkWidget* panel; + KatzeArray* feeds; + KatzeNet* net; + GSList* parsers; + + guint source_id; + gboolean is_running; + +} FeedPrivate; + +typedef struct +{ + MidoriExtension* extension; + GSList* parsers; + KatzeArray* feed; + +} FeedNetPrivate; + +enum +{ + FEED_NEW, + FEED_READ, + FEED_REMOVE +}; + +static void +feed_app_add_browser_cb (MidoriApp* app, + MidoriBrowser* browser, + MidoriExtension* extension); + +static void +feed_deactivate_cb (MidoriExtension* extension, + FeedPrivate* priv) +{ + if (priv) + { + MidoriApp* app = midori_extension_get_app (extension); + + g_signal_handlers_disconnect_by_func (app, + feed_app_add_browser_cb, extension); + g_signal_handlers_disconnect_by_func (extension, + feed_deactivate_cb, priv); + + if (priv->source_id) + g_source_remove (priv->source_id); + g_slist_foreach (priv->parsers, (GFunc)g_free, NULL); + g_slist_free (priv->parsers); + if (priv->feeds) + g_object_unref (priv->net); + if (priv->feeds) + g_object_unref (priv->feeds); + gtk_widget_destroy (priv->panel); + g_free (priv); + } +} + +static KatzeArray* +feed_add_item (KatzeArray* feeds, + const gchar* uri) +{ + KatzeArray* feed; + + feed = NULL; + + if (uri) + { + feed = katze_array_new (KATZE_TYPE_ITEM); + g_object_set_data_full (G_OBJECT (feed), "feeduri", + (gpointer) g_strdup ((uri)), g_free); + katze_array_add_item (feeds, feed); + } + return feed; +} + +static void +feed_save_items (MidoriExtension* extension, + KatzeArray* feed) +{ + KatzeItem* item; + gchar** sfeeds; + gint i; + gint n; + + g_return_if_fail (KATZE_IS_ARRAY (feed)); + + n = katze_array_get_length (feed); + sfeeds = g_new (gchar*, n + 1); + + for (i = 0; i < n; i++) + { + item = katze_array_get_nth_item (feed, i); + sfeeds[i] = (gchar*) g_object_get_data (G_OBJECT (item), "feeduri"); + } + sfeeds[n] = NULL; + + midori_extension_set_string_list (extension, "feeds", sfeeds, n); + g_free (sfeeds); +} + +static void +feed_handle_net_error (FeedNetPrivate* netpriv, + const gchar* msg) +{ + GtkWidget* dialog; + + dialog = gtk_message_dialog_new ( + NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + _("Error")); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", msg); + gtk_window_set_title (GTK_WINDOW (dialog), EXTENSION_NAME); + gtk_widget_show (dialog); + g_signal_connect_swapped (dialog, "response", + G_CALLBACK (gtk_widget_destroy), dialog); + + if (feed_has_flags (netpriv->feed, FEED_NEW)) + { + KatzeArray* parent; + KatzeItem* child; + + child = KATZE_ITEM (netpriv->feed); + parent = katze_item_get_parent (child); + katze_array_remove_item (parent, child); + feed_save_items (netpriv->extension, parent); + } + feed_remove_flags (netpriv->feed, FEED_READ); +} + +static gboolean +feed_status_cb (KatzeNetRequest* request, + FeedNetPrivate* netpriv) +{ + if (request->status == KATZE_NET_FAILED || + request->status == KATZE_NET_NOT_FOUND) + { + gchar* msg; + + msg = g_strdup_printf (_("Error loading feed %s"), + katze_item_get_uri (KATZE_ITEM (netpriv->feed))); + feed_handle_net_error (netpriv, msg); + g_free (msg); + + return FALSE; + } + return TRUE; +} + +static void +feed_transfer_cb (KatzeNetRequest* request, + FeedNetPrivate* netpriv) +{ + GError* error; + + if (request->status == KATZE_NET_MOVED) + return; + + g_return_if_fail (KATZE_IS_ARRAY (netpriv->feed)); + + error = NULL; + + if (request->data) + { + if (!parse_feed (request->data, request->length, + netpriv->parsers, netpriv->feed, &error)) + { + feed_handle_net_error (netpriv, error->message); + g_error_free (error); + } + else + { + if (feed_has_flags (netpriv->feed, FEED_REMOVE)) + { + KatzeArray* parent; + + /* deferred remove */ + parent = katze_item_get_parent (KATZE_ITEM (netpriv->feed)); + katze_array_remove_item (parent, netpriv->feed); + feed_save_items (netpriv->extension, parent); + } + else + feed_set_flags (netpriv->feed, 0); + } + } + + netpriv->parsers = NULL; + netpriv->feed = NULL; + g_free (netpriv); +} + +static void +update_feed (FeedPrivate* priv, + KatzeItem* feed) +{ + if (!(feed_has_flags (feed, FEED_READ))) + { + FeedNetPrivate* netpriv; + gchar* uri; + + uri = (gchar*) g_object_get_data (G_OBJECT (feed), "feeduri"); + feed_add_flags (feed, FEED_READ); + katze_item_set_uri (KATZE_ITEM (feed), uri); + netpriv = g_new0 (FeedNetPrivate, 1); + netpriv->parsers = priv->parsers; + netpriv->extension = priv->extension; + netpriv->feed = KATZE_ARRAY (feed); + + katze_net_load_uri (priv->net, + katze_item_get_uri (feed), + (KatzeNetStatusCb) feed_status_cb, + (KatzeNetTransferCb) feed_transfer_cb, + netpriv); + } +} + +static gboolean +update_feeds (FeedPrivate* priv) +{ + KatzeItem* feed; + gint i; + gint n; + + if (!priv->is_running) + { + priv->is_running = TRUE; + n = katze_array_get_length (priv->feeds); + + for (i = 0; i < n; i++) + { + feed = katze_array_get_nth_item (priv->feeds, i); + update_feed (priv, feed); + } + } + priv->is_running = FALSE; + return TRUE; +} + +static void +secondary_icon_released_cb (GtkAction* action, + GtkWidget* widget, + FeedPrivate* priv) +{ + const gchar* uri; + + g_assert (KATZE_IS_ARRAY (priv->feeds)); + + uri = midori_location_action_get_uri (MIDORI_LOCATION_ACTION (action)); + + if (uri && *uri) + { + KatzeArray* feed; + + feed = feed_add_item (priv->feeds, uri); + feed_save_items (priv->extension, priv->feeds); + feed_add_flags (feed, FEED_NEW); + update_feed (priv, KATZE_ITEM (feed)); + } +} + +static void +panel_add_feed_cb (FeedPanel* panel, + FeedPrivate* priv) +{ + GtkWidget* dialog; + GtkSizeGroup* sizegroup; + GtkWidget* hbox; + GtkWidget* label; + GtkWidget* entry; + + dialog = gtk_dialog_new_with_buttons ( + _("New feed"), GTK_WINDOW (priv->browser), + GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_NO_SEPARATOR, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_ADD, GTK_RESPONSE_ACCEPT, + NULL); + gtk_window_set_icon_name (GTK_WINDOW (dialog), GTK_STOCK_ADD); + gtk_container_set_border_width (GTK_CONTAINER (dialog), 5); + gtk_container_set_border_width (GTK_CONTAINER (GTK_DIALOG (dialog)->vbox), 5); + sizegroup = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + hbox = gtk_hbox_new (FALSE, 8); + gtk_container_set_border_width (GTK_CONTAINER (hbox), 5); + label = gtk_label_new_with_mnemonic (_("_Address:")); + gtk_size_group_add_widget (sizegroup, label); + gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0); + entry = gtk_entry_new (); + gtk_entry_set_activates_default (GTK_ENTRY (entry), TRUE); + gtk_entry_set_text (GTK_ENTRY (entry), ""); + gtk_box_pack_start (GTK_BOX (hbox), entry, TRUE, TRUE, 0); + gtk_container_add (GTK_CONTAINER (GTK_DIALOG (dialog)->vbox), hbox); + gtk_widget_show_all (hbox); + + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); + if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) + { + const gchar* uri; + + g_assert (KATZE_IS_ARRAY (priv->feeds)); + + uri = gtk_entry_get_text (GTK_ENTRY (entry)); + if (uri && *uri) + { + KatzeArray* feed; + + feed = feed_add_item (priv->feeds, uri); + feed_save_items (priv->extension, priv->feeds); + feed_add_flags (feed, FEED_NEW); + update_feed (priv, KATZE_ITEM (feed)); + } + } + gtk_widget_destroy (dialog); +} + +static void +panel_remove_feed_cb (FeedPanel* panel, + KatzeArray* feed, + FeedPrivate* priv) +{ + g_assert (KATZE_IS_ARRAY (priv->feeds)); + + if (feed_has_flags (feed, FEED_READ)) + feed_add_flags (feed, FEED_REMOVE); + else + { + feed_add_flags (feed, FEED_READ); + katze_array_remove_item (priv->feeds, feed); + feed_save_items (priv->extension, priv->feeds); + } +} + +static void +feed_app_add_browser_cb (MidoriApp* app, + MidoriBrowser* browser, + MidoriExtension* extension) +{ + GtkWidget* panel; + GtkWidget* addon; + GtkActionGroup* action_group; + GtkAction* action; + KatzeNet* net; + KatzeArray* feeds; + KatzeArray* feed; + FeedPrivate* priv; + gchar** sfeeds; + gsize i; + gsize n; + + priv = g_new0 (FeedPrivate, 1); + + panel = katze_object_get_object (browser, "panel"); + addon = feed_panel_new (); + gtk_widget_show (addon); + midori_panel_append_page (MIDORI_PANEL (panel), MIDORI_VIEWABLE (addon)); + g_object_unref (panel); + + net = katze_net_new (); + feeds = katze_array_new (KATZE_TYPE_ARRAY); + feed_panel_add_feeds (FEED_PANEL (addon), KATZE_ITEM (feeds)); + + priv->extension = extension; + priv->browser = browser; + priv->panel = addon; + priv->net = net; + priv->feeds = feeds; + priv->parsers = g_slist_prepend (priv->parsers, atom_init_parser ()); + priv->parsers = g_slist_prepend (priv->parsers, rss_init_parser ()); + + sfeeds = midori_extension_get_string_list (extension, "feeds", &n); + g_assert (n == 0 || sfeeds); + + for (i = 0; i < n; i++) + { + if (sfeeds[i]) + { + feed = feed_add_item (feeds, sfeeds[i]); + update_feed (priv, KATZE_ITEM (feed)); + } + } + action_group = midori_browser_get_action_group (browser); + action = gtk_action_group_get_action (action_group, "Location"); + + g_signal_connect (addon, "add-feed", + G_CALLBACK (panel_add_feed_cb), priv); + g_signal_connect (addon, "remove-feed", + G_CALLBACK (panel_remove_feed_cb), priv); + g_signal_connect (action, "secondary-icon-released", + G_CALLBACK (secondary_icon_released_cb), priv); + g_signal_connect (extension, "deactivate", + G_CALLBACK (feed_deactivate_cb), priv); + + priv->source_id = g_timeout_add_seconds (UPDATE_FREQ * 60, + (GSourceFunc) update_feeds, priv); +} + +static void +feed_activate_cb (MidoriExtension* extension, + MidoriApp* app) +{ + KatzeArray* browsers; + MidoriBrowser* browser; + guint i; + + browsers = katze_object_get_object (app, "browsers"); + i = 0; + while ((browser = katze_array_get_nth_item (browsers, i++))) + feed_app_add_browser_cb (app, browser, extension); + g_object_unref (browsers); + + g_signal_connect (app, "add-browser", + G_CALLBACK (feed_app_add_browser_cb), extension); +} + +MidoriExtension* +extension_init (void) +{ + MidoriExtension* extension; + gchar* sfeed[2]; + + extension = g_object_new (MIDORI_TYPE_EXTENSION, + "name", _("Feed Panel"), + "description", _("Read Atom/ RSS feeds"), + "version", "0.1", + "authors", "Dale Whittaker ", + NULL); + + sfeed[0] = NULL; + midori_extension_install_string_list (extension, "feeds", sfeed, 1); + + g_signal_connect (extension, "activate", + G_CALLBACK (feed_activate_cb), NULL); + + return extension; +}