[rhythmbox] podcast: new podcast subscription dialog



commit df4f01f0b1168011176aeb544f4f695b260648ae
Author: Jonathan Matthew <jonathan d14n org>
Date:   Sat May 26 13:55:28 2012 +1000

    podcast: new podcast subscription dialog
    
    This uses the previously added podcast searches to find podcasts,
    previews podcasts before subscription (including playback of episodes)
    and makes importing feeds via OPML a bit easier too.

 data/ui/Makefile.am             |    1 +
 data/ui/podcast-add-dialog.ui   |  183 ++++++++
 data/ui/rhythmbox-ui.xml        |    1 -
 po/POTFILES.in                  |    2 +
 podcast/Makefile.am             |    2 +
 podcast/rb-podcast-add-dialog.c |  941 +++++++++++++++++++++++++++++++++++++++
 podcast/rb-podcast-add-dialog.h |   73 +++
 podcast/rb-podcast-manager.c    |   16 -
 podcast/rb-podcast-manager.h    |    1 -
 podcast/rb-podcast-source.c     |   96 +++--
 podcast/rb-podcast-source.h     |    2 +
 shell/rb-shell.c                |    5 +-
 12 files changed, 1275 insertions(+), 48 deletions(-)
---
diff --git a/data/ui/Makefile.am b/data/ui/Makefile.am
index 061e640..d0bd573 100644
--- a/data/ui/Makefile.am
+++ b/data/ui/Makefile.am
@@ -9,6 +9,7 @@ GTK_BUILDER_FILES =					\
 	media-player-properties.ui			\
 	playback-prefs.ui				\
 	playlist-save.ui				\
+	podcast-add-dialog.ui				\
 	podcast-feed-properties.ui			\
 	podcast-prefs.ui				\
 	podcast-properties.ui				\
diff --git a/data/ui/podcast-add-dialog.ui b/data/ui/podcast-add-dialog.ui
new file mode 100644
index 0000000..37caaf6
--- /dev/null
+++ b/data/ui/podcast-add-dialog.ui
@@ -0,0 +1,183 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.0 -->
+  <object class="GtkAction" id="subscribe">
+    <property name="label" translatable="yes">Subscribe</property>
+  </object>
+  <object class="GtkGrid" id="podcast-add-dialog">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="row_spacing">6</property>
+    <child>
+      <object class="GtkVPaned" id="paned">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <child>
+          <object class="GtkScrolledWindow" id="scrolledwindow1">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="hexpand">True</property>
+            <property name="vexpand">True</property>
+            <child>
+              <object class="GtkTreeView" id="feed-view">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="border_width">1</property>
+                <child internal-child="selection">
+                  <object class="GtkTreeSelection" id="treeview-selection1"/>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="resize">False</property>
+            <property name="shrink">True</property>
+          </packing>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">3</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="label1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label" translatable="yes">Search for podcasts in the iTunes Store and on Miroguide.com, or enter a podcast feed URL.
+Subscribe to podcasts to download new episodes as they are published.</property>
+        <property name="justify">center</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <object class="GtkGrid" id="grid1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <property name="column_spacing">6</property>
+        <child>
+          <object class="GtkBox" id="search-entry-box">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="orientation">vertical</property>
+            <child>
+              <placeholder/>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">1</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="close-button">
+            <property name="label" translatable="yes">Close</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_action_appearance">False</property>
+          </object>
+          <packing>
+            <property name="left_attach">2</property>
+            <property name="top_attach">1</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="subscribe-button">
+            <property name="label" translatable="yes">Subscribe</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_action_appearance">False</property>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">1</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkBox" id="info-bar-container">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">2</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/data/ui/rhythmbox-ui.xml b/data/ui/rhythmbox-ui.xml
index 2aa169a..10896b2 100644
--- a/data/ui/rhythmbox-ui.xml
+++ b/data/ui/rhythmbox-ui.xml
@@ -17,7 +17,6 @@
 	<menuitem name="MusicPlaylistDeletePlaylistMenu" action="MusicPlaylistDeletePlaylist"/>
       </menu>
       <placeholder name="PluginPlaceholder" />
-      <menuitem name="MusicNewPodcastMenu" action="MusicNewPodcast"/>
       <separator/>
       <menuitem name="MusicPropertiesMenu" action="MusicProperties"/>
       <separator/>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 29b41c8..3132280 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -12,6 +12,7 @@ data/rhythmbox-device.desktop.in.in
 [type: gettext/glade]data/ui/media-player-properties.ui
 [type: gettext/glade]data/ui/playback-prefs.ui
 [type: gettext/glade]data/ui/playlist-save.ui
+[type: gettext/glade]data/ui/podcast-add-dialog.ui
 [type: gettext/glade]data/ui/podcast-feed-properties.ui
 [type: gettext/glade]data/ui/podcast-prefs.ui
 [type: gettext/glade]data/ui/podcast-properties.ui
@@ -137,6 +138,7 @@ plugins/visualizer/rb-visualizer-page.c
 plugins/visualizer/rb-visualizer-plugin.c
 [type: gettext/ini]plugins/visualizer/visualizer.plugin.in
 podcast/rb-feed-podcast-properties-dialog.c
+podcast/rb-podcast-add-dialog.c
 podcast/rb-podcast-main-source.c
 podcast/rb-podcast-manager.c
 podcast/rb-podcast-parse.c
diff --git a/podcast/Makefile.am b/podcast/Makefile.am
index 18cf01f..938fb60 100644
--- a/podcast/Makefile.am
+++ b/podcast/Makefile.am
@@ -19,6 +19,8 @@ librbpodcast_la_SOURCES =				\
 	$(podcastinclude_HEADERS)			\
 	rb-feed-podcast-properties-dialog.c		\
 	rb-feed-podcast-properties-dialog.h		\
+	rb-podcast-add-dialog.c				\
+	rb-podcast-add-dialog.h				\
 	rb-podcast-properties-dialog.c			\
 	rb-podcast-properties-dialog.h			\
 	rb-podcast-main-source.c			\
diff --git a/podcast/rb-podcast-add-dialog.c b/podcast/rb-podcast-add-dialog.c
new file mode 100644
index 0000000..54417d2
--- /dev/null
+++ b/podcast/rb-podcast-add-dialog.c
@@ -0,0 +1,941 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2010 Jonathan Matthew <jonathan d14n org>
+ *
+ *  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 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your 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, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "rb-shell.h"
+#include "rb-shell-player.h"
+#include "rb-podcast-add-dialog.h"
+#include "rb-podcast-search.h"
+#include "rb-podcast-entry-types.h"
+#include "rb-builder-helpers.h"
+#include "rb-debug.h"
+#include "rb-util.h"
+#include "rb-cut-and-paste-code.h"
+#include "rb-search-entry.h"
+
+static void rb_podcast_add_dialog_class_init (RBPodcastAddDialogClass *klass);
+static void rb_podcast_add_dialog_init (RBPodcastAddDialog *dialog);
+
+enum {
+	PROP_0,
+	PROP_PODCAST_MANAGER,
+	PROP_SHELL,
+};
+
+enum {
+	CLOSE,
+	CLOSED,
+	LAST_SIGNAL
+};
+
+enum {
+	FEED_COLUMN_TITLE = 0,
+	FEED_COLUMN_AUTHOR,
+	FEED_COLUMN_IMAGE,
+	FEED_COLUMN_IMAGE_FILE,
+	FEED_COLUMN_EPISODE_COUNT,
+	FEED_COLUMN_PARSED_FEED,
+	FEED_COLUMN_DATE,
+};
+
+struct RBPodcastAddDialogPrivate
+{
+	RBPodcastManager *podcast_mgr;
+	RhythmDB *db;
+	RBShell *shell;
+
+	GtkWidget *feed_view;
+	GtkListStore *feed_model;
+
+	GtkWidget *episode_view;
+
+	GtkWidget *text_entry;
+	GtkWidget *subscribe_button;
+	GtkWidget *info_bar;
+	GtkWidget *info_bar_message;
+
+	RBSearchEntry *search_entry;
+
+	gboolean paned_size_set;
+	gboolean have_selection;
+	gboolean clearing;
+	GtkTreeIter selected_feed;
+
+	int running_searches;
+	gboolean search_successful;
+};
+
+/* various prefixes that identify things we treat as feed URLs rather than search terms */
+static const char *podcast_uri_prefixes[] = {
+	"http://";,
+	"https://";,
+	"feed://",
+	"zcast://",
+	"zune://",
+	"itpc://",
+	"itms://",
+	"www.",
+};
+
+/* number of search results to request from each available search */
+#define PODCAST_SEARCH_LIMIT		25
+
+#define PODCAST_IMAGE_SIZE		50
+
+static guint signals[LAST_SIGNAL] = {0,};
+
+G_DEFINE_TYPE (RBPodcastAddDialog, rb_podcast_add_dialog, GTK_TYPE_VBOX);
+
+
+static gboolean
+remove_all_feeds_cb (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, RBPodcastAddDialog *dialog)
+{
+	RBPodcastChannel *channel;
+	gtk_tree_model_get (model, iter, FEED_COLUMN_PARSED_FEED, &channel, -1);
+	rb_podcast_parse_channel_free (channel);
+	return FALSE;
+}
+
+static void
+remove_all_feeds (RBPodcastAddDialog *dialog)
+{
+	/* remove all feeds from the model and free associated data */
+	gtk_tree_model_foreach (GTK_TREE_MODEL (dialog->priv->feed_model),
+				(GtkTreeModelForeachFunc) remove_all_feeds_cb,
+				dialog);
+
+	dialog->priv->clearing = TRUE;
+	gtk_list_store_clear (dialog->priv->feed_model);
+	dialog->priv->clearing = FALSE;
+
+	dialog->priv->have_selection = FALSE;
+	gtk_widget_set_sensitive (dialog->priv->subscribe_button, FALSE);
+}
+
+static void
+add_posts_for_feed (RBPodcastAddDialog *dialog, RBPodcastChannel *channel)
+{
+	GList *l;
+
+	for (l = channel->posts; l != NULL; l = l->next) {
+		RBPodcastItem *item = (RBPodcastItem *) l->data;
+
+		rb_podcast_manager_add_post (dialog->priv->db,
+					     TRUE,
+					     channel->title ? channel->title : channel->url,
+					     item->title,
+					     channel->url,
+					     (item->author ? item->author : channel->author),
+					     item->url,
+					     item->description,
+					     (item->pub_date > 0 ? item->pub_date : channel->pub_date),
+					     item->duration,
+					     item->filesize);
+	}
+
+	rhythmdb_commit (dialog->priv->db);
+}
+
+static void
+image_file_read_cb (GObject *file, GAsyncResult *result, RBPodcastAddDialog *dialog)
+{
+	GFileInputStream *stream;
+	GdkPixbuf *pixbuf;
+	GError *error = NULL;
+
+	stream = g_file_read_finish (G_FILE (file), result, &error);
+	if (error != NULL) {
+		rb_debug ("podcast image read failed: %s", error->message);
+		g_clear_error (&error);
+		g_object_unref (dialog);
+		return;
+	}
+
+	pixbuf = gdk_pixbuf_new_from_stream_at_scale (G_INPUT_STREAM (stream), PODCAST_IMAGE_SIZE, PODCAST_IMAGE_SIZE, TRUE, NULL, &error);
+	if (error != NULL) {
+		rb_debug ("podcast image load failed: %s", error->message);
+		g_clear_error (&error);
+	} else {
+		GtkTreeIter iter;
+
+		if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (dialog->priv->feed_model), &iter)) {
+			do {
+				GFile *feedfile;
+				gtk_tree_model_get (GTK_TREE_MODEL (dialog->priv->feed_model), &iter,
+						    FEED_COLUMN_IMAGE_FILE, &feedfile,
+						    -1);
+				if (feedfile == G_FILE (file)) {
+					gtk_list_store_set (dialog->priv->feed_model,
+							    &iter,
+							    FEED_COLUMN_IMAGE, g_object_ref (pixbuf),
+							    -1);
+					break;
+				}
+			} while (gtk_tree_model_iter_next (GTK_TREE_MODEL (dialog->priv->feed_model), &iter));
+		}
+		g_object_unref (pixbuf);
+	}
+
+	g_object_unref (dialog);
+	g_object_unref (stream);
+}
+
+static void
+insert_search_result (RBPodcastAddDialog *dialog, RBPodcastChannel *channel, gboolean select)
+{
+	GtkTreeIter iter;
+	GFile *image_file;
+	int episodes;
+
+	if (channel->posts) {
+		episodes = g_list_length (channel->posts);
+	} else {
+		episodes = channel->num_posts;
+	}
+
+	/* if there's an image to load, fetch it */
+	if (channel->img) {
+		rb_debug ("fetching image %s", channel->img);
+		image_file = g_file_new_for_uri (channel->img);
+	} else {
+		image_file = NULL;
+	}
+
+	gtk_list_store_insert_with_values (dialog->priv->feed_model,
+					   &iter,
+					   G_MAXINT,
+					   FEED_COLUMN_TITLE, channel->title,
+					   FEED_COLUMN_AUTHOR, channel->author,
+					   FEED_COLUMN_EPISODE_COUNT, episodes,
+					   FEED_COLUMN_IMAGE, NULL,
+					   FEED_COLUMN_IMAGE_FILE, image_file,
+					   FEED_COLUMN_PARSED_FEED, channel,
+					   -1);
+
+	if (image_file != NULL) {
+		g_file_read_async (image_file,
+				   G_PRIORITY_DEFAULT,
+				   NULL,
+				   (GAsyncReadyCallback) image_file_read_cb,
+				   g_object_ref (dialog));
+	}
+
+	if (select) {
+		GtkTreeSelection *selection;
+		selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (dialog->priv->feed_view));
+		gtk_tree_selection_select_iter (selection, &iter);
+	}
+}
+
+typedef struct {
+	RBPodcastAddDialog *dialog;
+	char *url;
+	RBPodcastChannel *channel;
+	gboolean existing;
+	gboolean single;
+	GError *error;
+} ParseThreadData;
+
+static gboolean
+parse_finished (ParseThreadData *data)
+{
+	if (data->error != NULL) {
+		gtk_label_set_label (GTK_LABEL (data->dialog->priv->info_bar_message),
+				     _("Unable to load the feed. Check your network connection."));
+		gtk_widget_show (data->dialog->priv->info_bar);
+	} else {
+		gtk_widget_hide (data->dialog->priv->info_bar);
+	}
+
+	if (data->channel->is_opml) {
+		GList *l;
+		/* convert each item into its own channel */
+		for (l = data->channel->posts; l != NULL; l = l->next) {
+			RBPodcastChannel *channel;
+			RBPodcastItem *item;
+
+			item = l->data;
+			channel = g_new0 (RBPodcastChannel, 1);
+			channel->url = g_strdup (item->url);
+			channel->title = g_strdup (item->title);
+			/* none of the other fields get populated anyway */
+			insert_search_result (data->dialog, channel, FALSE);
+		}
+		rb_podcast_parse_channel_free (data->channel);
+	} else if (data->existing) {
+		/* find the row for the feed, replace the channel */
+		GtkTreeIter iter;
+		gboolean found = FALSE;
+
+		if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (data->dialog->priv->feed_model), &iter)) {
+			do {
+				RBPodcastChannel *channel;
+				gtk_tree_model_get (GTK_TREE_MODEL (data->dialog->priv->feed_model), &iter,
+						    FEED_COLUMN_PARSED_FEED, &channel,
+						    -1);
+				if (g_strcmp0 (channel->url, data->url) == 0) {
+					gtk_list_store_set (data->dialog->priv->feed_model,
+							    &iter,
+							    FEED_COLUMN_PARSED_FEED, data->channel,
+							    -1);
+					found = TRUE;
+					rb_podcast_parse_channel_free (channel);
+					break;
+				}
+			} while (gtk_tree_model_iter_next (GTK_TREE_MODEL (data->dialog->priv->feed_model), &iter));
+		}
+
+		/* if the row is selected, create entries for the channel contents */
+		if (data->dialog->priv->have_selection && found) {
+			GtkTreePath *a;
+			GtkTreePath *b;
+
+			a = gtk_tree_model_get_path (GTK_TREE_MODEL (data->dialog->priv->feed_model), &iter);
+			b = gtk_tree_model_get_path (GTK_TREE_MODEL (data->dialog->priv->feed_model), &data->dialog->priv->selected_feed);
+			if (gtk_tree_path_compare (a, b) == 0) {
+				add_posts_for_feed (data->dialog, data->channel);
+			}
+
+			gtk_tree_path_free (a);
+			gtk_tree_path_free (b);
+		} else {
+			rb_podcast_parse_channel_free (data->channel);
+		}
+	} else {
+		/* model owns data->channel now */
+		insert_search_result (data->dialog, data->channel, data->single);
+	}
+
+	g_object_unref (data->dialog);
+	g_clear_error (&data->error);
+	g_free (data->url);
+	g_free (data);
+	return FALSE;
+}
+
+static gpointer
+parse_thread (ParseThreadData *data)
+{
+	if (rb_podcast_parse_load_feed (data->channel, data->url, FALSE, &data->error) == FALSE) {
+		/* fake up a channel with just the url as the title, allowing the user
+		 * to subscribe to the podcast anyway.
+		 */
+		data->channel->url = g_strdup (data->url);
+		data->channel->title = g_strdup (data->url);
+	}
+
+	g_idle_add ((GSourceFunc) parse_finished, data);
+	return NULL;
+}
+
+static void
+parse_in_thread (RBPodcastAddDialog *dialog, const char *text, gboolean existing, gboolean single)
+{
+	ParseThreadData *data;
+	GError *error = NULL;
+
+	data = g_new0 (ParseThreadData, 1);
+	data->dialog = g_object_ref (dialog);
+	data->url = g_strdup (text);
+	data->channel = g_new0 (RBPodcastChannel, 1);
+	data->existing = existing;
+	data->single = single;
+
+	g_thread_create ((GThreadFunc) parse_thread, data, TRUE, &error);
+	if (error != NULL) {
+		/* ugh.. */
+		g_warning ("Unable to create podcast parsing thread: %s", error->message);
+		g_clear_error (&error);
+	}
+}
+
+static void
+podcast_search_result_cb (RBPodcastSearch *search, RBPodcastChannel *feed, RBPodcastAddDialog *dialog)
+{
+	rb_debug ("got result %s from podcast search %s", feed->url, G_OBJECT_TYPE_NAME (search));
+	insert_search_result (dialog, rb_podcast_parse_channel_copy (feed), FALSE);
+}
+
+static void
+podcast_search_finished_cb (RBPodcastSearch *search, gboolean successful, RBPodcastAddDialog *dialog)
+{
+	rb_debug ("podcast search %s finished", G_OBJECT_TYPE_NAME (search));
+	g_object_unref (search);
+
+	dialog->priv->search_successful |= successful;
+
+	dialog->priv->running_searches--;
+	if (dialog->priv->running_searches == 0) {
+		if (dialog->priv->search_successful == FALSE) {
+			gtk_label_set_label (GTK_LABEL (dialog->priv->info_bar_message),
+					     _("Unable to search for podcasts. Check your network connection."));
+			gtk_widget_show (dialog->priv->info_bar);
+		}
+	}
+}
+
+static void
+search_cb (RBSearchEntry *entry, const char *text, RBPodcastAddDialog *dialog)
+{
+	GList *searches;
+	GList *s;
+	int i;
+
+	/* remove previous feeds */
+	remove_all_feeds (dialog);
+	rhythmdb_entry_delete_by_type (dialog->priv->db, RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH);
+	rhythmdb_commit (dialog->priv->db);
+
+	gtk_widget_hide (dialog->priv->info_bar);
+
+	if (text == NULL || text[0] == '\0') {
+		return;
+	}
+
+	/* if the entered text looks like a feed URL, parse it directly */
+	for (i = 0; i < G_N_ELEMENTS (podcast_uri_prefixes); i++) {
+		if (g_str_has_prefix (text, podcast_uri_prefixes[i])) {
+			parse_in_thread (dialog, text, FALSE, TRUE);
+			return;
+		}
+	}
+
+	/* not really sure about this one */
+	if (g_path_is_absolute (text)) {
+		parse_in_thread (dialog, text, FALSE, TRUE);
+		return;
+	}
+
+	/* otherwise, try podcast searches */
+	dialog->priv->search_successful = FALSE;
+	searches = rb_podcast_manager_get_searches (dialog->priv->podcast_mgr);
+	for (s = searches; s != NULL; s = s->next) {
+		RBPodcastSearch *search = s->data;
+
+		g_signal_connect_object (search, "result", G_CALLBACK (podcast_search_result_cb), dialog, 0);
+		g_signal_connect_object (search, "finished", G_CALLBACK (podcast_search_finished_cb), dialog, 0);
+		rb_podcast_search_start (search, text, PODCAST_SEARCH_LIMIT);
+		dialog->priv->running_searches++;
+	}
+}
+
+static void
+subscribe_selected_feed (RBPodcastAddDialog *dialog)
+{
+	RBPodcastChannel *channel;
+
+	g_assert (dialog->priv->have_selection);
+
+	rhythmdb_entry_delete_by_type (dialog->priv->db, RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH);
+	rhythmdb_commit (dialog->priv->db);
+
+	/* subscribe selected feed */
+	gtk_tree_model_get (GTK_TREE_MODEL (dialog->priv->feed_model),
+			    &dialog->priv->selected_feed,
+			    FEED_COLUMN_PARSED_FEED, &channel,
+			    -1);
+	if (channel->posts != NULL) {
+		rb_podcast_manager_add_parsed_feed (dialog->priv->podcast_mgr, channel);
+	} else {
+		rb_podcast_manager_subscribe_feed (dialog->priv->podcast_mgr, channel->url, TRUE);
+	}
+}
+
+static void
+subscribe_clicked_cb (GtkButton *button, RBPodcastAddDialog *dialog)
+{
+	if (dialog->priv->have_selection == FALSE) {
+		rb_debug ("no selection");
+		return;
+	}
+
+	subscribe_selected_feed (dialog);
+
+	dialog->priv->clearing = TRUE;
+	gtk_list_store_remove (GTK_LIST_STORE (dialog->priv->feed_model), &dialog->priv->selected_feed);
+	dialog->priv->clearing = FALSE;
+
+	gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (GTK_TREE_VIEW (dialog->priv->feed_view)));
+}
+
+static void
+close_clicked_cb (GtkButton *button, RBPodcastAddDialog *dialog)
+{
+	g_signal_emit (dialog, signals[CLOSED], 0);
+}
+
+static void
+feed_activated_cb (GtkTreeView *view, GtkTreePath *path, GtkTreeViewColumn *column, RBPodcastAddDialog *dialog)
+{
+	gtk_tree_model_get_iter (GTK_TREE_MODEL (dialog->priv->feed_model), &dialog->priv->selected_feed, path);
+	dialog->priv->have_selection = TRUE;
+
+	subscribe_selected_feed (dialog);
+
+	dialog->priv->have_selection = FALSE;
+
+	g_signal_emit (dialog, signals[CLOSED], 0);
+}
+
+static void
+feed_selection_changed_cb (GtkTreeSelection *selection, RBPodcastAddDialog *dialog)
+{
+	GtkTreeModel *model;
+
+	if (dialog->priv->clearing)
+		return;
+
+	dialog->priv->have_selection =
+		gtk_tree_selection_get_selected (selection, &model, &dialog->priv->selected_feed);
+	gtk_widget_set_sensitive (dialog->priv->subscribe_button, dialog->priv->have_selection);
+
+	rhythmdb_entry_delete_by_type (dialog->priv->db, RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH);
+	rhythmdb_commit (dialog->priv->db);
+
+	if (dialog->priv->have_selection) {
+		RBPodcastChannel *channel = NULL;
+
+		gtk_tree_model_get (model,
+				    &dialog->priv->selected_feed,
+				    FEED_COLUMN_PARSED_FEED, &channel,
+				    -1);
+
+		if (channel->posts == NULL) {
+			rb_debug ("parsing feed %s to get posts", channel->url);
+			parse_in_thread (dialog, channel->url, TRUE, FALSE);
+		} else {
+			add_posts_for_feed (dialog, channel);
+		}
+	}
+}
+
+static void
+episode_count_column_cell_data_func (GtkTreeViewColumn *column,
+				     GtkCellRenderer *renderer,
+				     GtkTreeModel *model,
+				     GtkTreeIter *iter,
+				     gpointer data)
+{
+	GtkTreeIter parent;
+	if (gtk_tree_model_iter_parent (model, &parent, iter)) {
+		g_object_set (renderer, "visible", FALSE, NULL);
+	} else {
+		int count;
+		char *text;
+		gtk_tree_model_get (model, iter, FEED_COLUMN_EPISODE_COUNT, &count, -1);
+		text = g_strdup_printf ("%d", count);
+		g_object_set (renderer, "visible", TRUE, "text", text, NULL);
+		g_free (text);
+	}
+}
+
+static void
+podcast_post_date_cell_data_func (GtkTreeViewColumn *column,
+				  GtkCellRenderer *renderer,
+				  GtkTreeModel *tree_model,
+				  GtkTreeIter *iter,
+				  gpointer data)
+{
+	RhythmDBEntry *entry;
+	gulong value;
+	char *str;
+
+	gtk_tree_model_get (tree_model, iter, 0, &entry, -1);
+
+	value = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_POST_TIME);
+        if (value == 0) {
+		str = g_strdup (_("Unknown"));
+	} else {
+		str = rb_utf_friendly_time (value);
+	}
+
+	g_object_set (G_OBJECT (renderer), "text", str, NULL);
+	g_free (str);
+
+	rhythmdb_entry_unref (entry);
+}
+
+static gint
+podcast_post_date_sort_func (RhythmDBEntry *a,
+			     RhythmDBEntry *b,
+			     RhythmDBQueryModel *model)
+{
+	gulong a_val, b_val;
+	gint ret;
+
+	a_val = rhythmdb_entry_get_ulong (a, RHYTHMDB_PROP_POST_TIME);
+	b_val = rhythmdb_entry_get_ulong (b, RHYTHMDB_PROP_POST_TIME);
+
+	if (a_val != b_val)
+		ret = (a_val > b_val) ? 1 : -1;
+	else
+		ret = rhythmdb_query_model_title_sort_func (a, b, model);
+
+        return ret;
+}
+
+static void
+episodes_sort_changed_cb (GObject *object, GParamSpec *pspec, RBPodcastAddDialog *dialog)
+{
+	rb_entry_view_resort_model (RB_ENTRY_VIEW (object));
+}
+
+static void
+impl_close (RBPodcastAddDialog *dialog)
+{
+	g_signal_emit (dialog, signals[CLOSED], 0);
+}
+
+static gboolean
+set_paned_position (GtkWidget *paned)
+{
+	gtk_paned_set_position (GTK_PANED (paned), gtk_widget_get_allocated_height (paned) / 2);
+	g_object_unref (paned);
+	return FALSE;
+}
+
+static void
+paned_size_allocate_cb (GtkWidget *widget, GdkRectangle *allocation, RBPodcastAddDialog *dialog)
+{
+	if (dialog->priv->paned_size_set == FALSE) {
+		dialog->priv->paned_size_set = TRUE;
+		g_idle_add ((GSourceFunc) set_paned_position, g_object_ref (widget));
+	}
+}
+
+static void
+episode_entry_activated_cb (RBEntryView *entry_view, RhythmDBEntry *entry, RBPodcastAddDialog *dialog)
+{
+	rb_debug ("search result podcast entry %s activated", rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION));
+	rb_shell_load_uri (dialog->priv->shell,
+			   rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION),
+			   TRUE,
+			   NULL);
+}
+
+static void
+impl_constructed (GObject *object)
+{
+	RBPodcastAddDialog *dialog;
+	GtkBuilder *builder;
+	GtkWidget *widget;
+	GtkWidget *paned;
+	GtkTreeViewColumn *column;
+	GtkCellRenderer *renderer;
+	RBEntryView *episodes;
+	RBShellPlayer *shell_player;
+	RhythmDBQuery *query;
+	RhythmDBQueryModel *query_model;
+	const char *episode_strings[3];
+
+	RB_CHAIN_GOBJECT_METHOD (rb_podcast_add_dialog_parent_class, constructed, object);
+	dialog = RB_PODCAST_ADD_DIALOG (object);
+
+	g_object_get (dialog->priv->podcast_mgr, "db", &dialog->priv->db, NULL);
+
+	builder = rb_builder_load ("podcast-add-dialog.ui", NULL);
+
+	dialog->priv->info_bar_message = gtk_label_new ("");
+	dialog->priv->info_bar = gtk_info_bar_new ();
+	gtk_container_add (GTK_CONTAINER (gtk_info_bar_get_content_area (GTK_INFO_BAR (dialog->priv->info_bar))),
+			   dialog->priv->info_bar_message);
+	gtk_widget_set_no_show_all (dialog->priv->info_bar, TRUE);
+	gtk_box_pack_start (GTK_BOX (gtk_builder_get_object (builder, "info-bar-container")), dialog->priv->info_bar, TRUE, TRUE, 0);
+	gtk_widget_show (dialog->priv->info_bar_message);
+
+	dialog->priv->subscribe_button = GTK_WIDGET (gtk_builder_get_object (builder, "subscribe-button"));
+	g_signal_connect_object (dialog->priv->subscribe_button, "clicked", G_CALLBACK (subscribe_clicked_cb), dialog, 0);
+	gtk_widget_set_sensitive (dialog->priv->subscribe_button, FALSE);
+
+	dialog->priv->feed_view = GTK_WIDGET (gtk_builder_get_object (builder, "feed-view"));
+	g_signal_connect (dialog->priv->feed_view, "row-activated", G_CALLBACK (feed_activated_cb), dialog);
+	g_signal_connect (gtk_tree_view_get_selection (GTK_TREE_VIEW (dialog->priv->feed_view)),
+			  "changed",
+			  G_CALLBACK (feed_selection_changed_cb),
+			  dialog);
+
+
+	dialog->priv->search_entry = rb_search_entry_new (FALSE);
+	gtk_widget_set_size_request (GTK_WIDGET (dialog->priv->search_entry), 400, -1);
+	g_object_set (dialog->priv->search_entry,"explicit-mode", TRUE, NULL);
+	g_signal_connect (dialog->priv->search_entry, "search", G_CALLBACK (search_cb), dialog);
+	g_signal_connect (dialog->priv->search_entry, "activate", G_CALLBACK (search_cb), dialog);
+	gtk_container_add (GTK_CONTAINER (gtk_builder_get_object (builder, "search-entry-box")),
+			   GTK_WIDGET (dialog->priv->search_entry));
+
+	g_signal_connect (gtk_builder_get_object (builder, "close-button"),
+			  "clicked",
+			  G_CALLBACK (close_clicked_cb),
+			  dialog);
+
+	dialog->priv->feed_model = gtk_list_store_new (7,
+						       G_TYPE_STRING,	/* name */
+						       G_TYPE_STRING,	/* author */
+						       GDK_TYPE_PIXBUF, /* image */
+						       G_TYPE_FILE,	/* image file */
+						       G_TYPE_INT,	/* episode count */
+						       G_TYPE_POINTER,	/* RBPodcastChannel */
+						       G_TYPE_ULONG);	/* date */
+	gtk_tree_view_set_model (GTK_TREE_VIEW (dialog->priv->feed_view), GTK_TREE_MODEL (dialog->priv->feed_model));
+
+	column = gtk_tree_view_column_new_with_attributes (_("Title"), gtk_cell_renderer_pixbuf_new (), "pixbuf", FEED_COLUMN_IMAGE, NULL);
+	renderer = gtk_cell_renderer_text_new ();
+	g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
+	gtk_tree_view_column_pack_start (column, renderer, TRUE);
+	gtk_tree_view_column_set_attributes (column, renderer, "text", FEED_COLUMN_TITLE, NULL);
+
+	gtk_tree_view_column_set_expand (column, TRUE);
+	gtk_tree_view_append_column (GTK_TREE_VIEW (dialog->priv->feed_view), column);
+
+	renderer = gtk_cell_renderer_text_new ();
+	g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
+	column = gtk_tree_view_column_new_with_attributes (_("Author"), renderer, "text", FEED_COLUMN_AUTHOR, NULL);
+	gtk_tree_view_column_set_expand (column, TRUE);
+	gtk_tree_view_append_column (GTK_TREE_VIEW (dialog->priv->feed_view), column);
+
+	renderer = gtk_cell_renderer_text_new ();
+	column = gtk_tree_view_column_new_with_attributes (_("Episodes"), renderer, NULL);
+	gtk_tree_view_column_set_cell_data_func (column, renderer, episode_count_column_cell_data_func, NULL, NULL);
+	episode_strings[0] = "0000";
+	episode_strings[1] = _("Episodes");
+	episode_strings[2] = NULL;
+	rb_set_tree_view_column_fixed_width (dialog->priv->feed_view, column, renderer, episode_strings, 6);
+	gtk_tree_view_append_column (GTK_TREE_VIEW (dialog->priv->feed_view), column);
+
+	widget = GTK_WIDGET (gtk_builder_get_object (builder, "podcast-add-dialog"));
+	gtk_box_pack_start (GTK_BOX (dialog), widget, TRUE, TRUE, 12);	/* 12? */
+
+	gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (dialog->priv->feed_view), TRUE);
+
+	/* set up episode view */
+	g_object_get (dialog->priv->shell, "shell-player", &shell_player, NULL);
+	episodes = rb_entry_view_new (dialog->priv->db, G_OBJECT (shell_player), TRUE, FALSE);
+	g_object_unref (shell_player);
+
+	g_signal_connect (episodes, "entry-activated", G_CALLBACK (episode_entry_activated_cb), dialog);
+
+	/* date column */
+	column = gtk_tree_view_column_new ();
+	renderer = gtk_cell_renderer_text_new();
+
+	gtk_tree_view_column_pack_start (column, renderer, TRUE);
+
+	gtk_tree_view_column_set_clickable (column, TRUE);
+	gtk_tree_view_column_set_resizable (column, TRUE);
+	gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
+	{
+		const char *sample_strings[3];
+		sample_strings[0] = _("Date");
+		sample_strings[1] = rb_entry_view_get_time_date_column_sample ();
+		sample_strings[2] = NULL;
+		rb_entry_view_set_fixed_column_width (episodes, column, renderer, sample_strings);
+	}
+
+	gtk_tree_view_column_set_cell_data_func (column, renderer,
+						 (GtkTreeCellDataFunc) podcast_post_date_cell_data_func,
+						 dialog, NULL);
+
+	rb_entry_view_append_column_custom (episodes, column,
+					    _("Date"), "Date",
+					    (GCompareDataFunc) podcast_post_date_sort_func,
+					    0, NULL);
+	rb_entry_view_append_column (episodes, RB_ENTRY_VIEW_COL_TITLE, TRUE);
+	rb_entry_view_append_column (episodes, RB_ENTRY_VIEW_COL_DURATION, TRUE);
+	rb_entry_view_set_sorting_order (RB_ENTRY_VIEW (episodes), "Date", GTK_SORT_DESCENDING);
+	g_signal_connect (episodes,
+			  "notify::sort-order",
+			  G_CALLBACK (episodes_sort_changed_cb),
+			  dialog);
+
+	query = rhythmdb_query_parse (dialog->priv->db,
+				      RHYTHMDB_QUERY_PROP_EQUALS,
+				      RHYTHMDB_PROP_TYPE,
+				      RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH,
+				      RHYTHMDB_QUERY_END);
+	query_model = rhythmdb_query_model_new_empty (dialog->priv->db);
+	rb_entry_view_set_model (episodes, query_model);
+
+	rhythmdb_do_full_query_async_parsed (dialog->priv->db, RHYTHMDB_QUERY_RESULTS (query_model), query);
+	rhythmdb_query_free (query);
+
+	g_object_unref (query_model);
+
+	paned = GTK_WIDGET (gtk_builder_get_object (builder, "paned"));
+	g_signal_connect (paned, "size-allocate", G_CALLBACK (paned_size_allocate_cb), dialog);
+	gtk_paned_pack2 (GTK_PANED (paned),
+			 GTK_WIDGET (episodes),
+			 TRUE,
+			 FALSE);
+
+	gtk_widget_show_all (GTK_WIDGET (dialog));
+	g_object_unref (builder);
+}
+
+static void
+impl_dispose (GObject *object)
+{
+	RBPodcastAddDialog *dialog = RB_PODCAST_ADD_DIALOG (object);
+
+	if (dialog->priv->podcast_mgr != NULL) {
+		g_object_unref (dialog->priv->podcast_mgr);
+		dialog->priv->podcast_mgr = NULL;
+	}
+	if (dialog->priv->db != NULL) {
+		g_object_unref (dialog->priv->db);
+		dialog->priv->db = NULL;
+	}
+
+	G_OBJECT_CLASS (rb_podcast_add_dialog_parent_class)->dispose (object);
+}
+
+static void
+impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+	RBPodcastAddDialog *dialog = RB_PODCAST_ADD_DIALOG (object);
+
+	switch (prop_id) {
+	case PROP_PODCAST_MANAGER:
+		dialog->priv->podcast_mgr = g_value_dup_object (value);
+		break;
+	case PROP_SHELL:
+		dialog->priv->shell = g_value_dup_object (value);
+		break;
+	default:
+		g_assert_not_reached ();
+		break;
+	}
+}
+
+static void
+impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+	RBPodcastAddDialog *dialog = RB_PODCAST_ADD_DIALOG (object);
+
+	switch (prop_id) {
+	case PROP_PODCAST_MANAGER:
+		g_value_set_object (value, dialog->priv->podcast_mgr);
+		break;
+	case PROP_SHELL:
+		g_value_set_object (value, dialog->priv->shell);
+		break;
+	default:
+		g_assert_not_reached ();
+		break;
+	}
+}
+
+static void
+rb_podcast_add_dialog_init (RBPodcastAddDialog *dialog)
+{
+	dialog->priv = G_TYPE_INSTANCE_GET_PRIVATE (dialog,
+						    RB_TYPE_PODCAST_ADD_DIALOG,
+						    RBPodcastAddDialogPrivate);
+}
+
+static void
+rb_podcast_add_dialog_class_init (RBPodcastAddDialogClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+	object_class->constructed = impl_constructed;
+	object_class->dispose = impl_dispose;
+	object_class->set_property = impl_set_property;
+	object_class->get_property = impl_get_property;
+
+	klass->close = impl_close;
+
+	g_object_class_install_property (object_class,
+					 PROP_PODCAST_MANAGER,
+					 g_param_spec_object ("podcast-manager",
+							      "podcast-manager",
+							      "RBPodcastManager instance",
+							      RB_TYPE_PODCAST_MANAGER,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+	g_object_class_install_property (object_class,
+					 PROP_SHELL,
+					 g_param_spec_object ("shell",
+							      "shell",
+							      "RBShell instance",
+							      RB_TYPE_SHELL,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+	signals[CLOSE] = g_signal_new ("close",
+				       RB_TYPE_PODCAST_ADD_DIALOG,
+				       G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+				       G_STRUCT_OFFSET (RBPodcastAddDialogClass, close),
+				       NULL, NULL,
+				       g_cclosure_marshal_VOID__VOID,
+				       G_TYPE_NONE,
+				       0);
+	signals[CLOSED] = g_signal_new ("closed",
+					RB_TYPE_PODCAST_ADD_DIALOG,
+					G_SIGNAL_RUN_LAST,
+					G_STRUCT_OFFSET (RBPodcastAddDialogClass, closed),
+					NULL, NULL,
+					g_cclosure_marshal_VOID__VOID,
+					G_TYPE_NONE,
+					0);
+
+	g_type_class_add_private (object_class, sizeof (RBPodcastAddDialogPrivate));
+
+	gtk_binding_entry_add_signal (gtk_binding_set_by_class (klass),
+				      GDK_KEY_Escape,
+				      0,
+				      "close",
+				      0);
+}
+
+void
+rb_podcast_add_dialog_reset (RBPodcastAddDialog *dialog, const char *text, gboolean load)
+{
+	remove_all_feeds (dialog);
+	rhythmdb_entry_delete_by_type (dialog->priv->db, RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH);
+	rhythmdb_commit (dialog->priv->db);
+
+	rb_search_entry_set_text (dialog->priv->search_entry, text);
+
+	if (load) {
+		search_cb (dialog->priv->search_entry, text, dialog);
+	} else {
+		rb_search_entry_grab_focus (dialog->priv->search_entry);
+	}
+}
+
+GtkWidget *
+rb_podcast_add_dialog_new (RBShell *shell, RBPodcastManager *podcast_mgr)
+{
+	return GTK_WIDGET (g_object_new (RB_TYPE_PODCAST_ADD_DIALOG,
+					 "shell", shell,
+					 "podcast-manager", podcast_mgr,
+					 NULL));
+}
+
diff --git a/podcast/rb-podcast-add-dialog.h b/podcast/rb-podcast-add-dialog.h
new file mode 100644
index 0000000..2c06ffc
--- /dev/null
+++ b/podcast/rb-podcast-add-dialog.h
@@ -0,0 +1,73 @@
+/*
+ *  Copyright (C) 2010 Jonathan Matthew  <jonathan d14n org>
+ *
+ *  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 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your 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, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#ifndef RB_PODCAST_ADD_DIALOG_H
+#define RB_PODCAST_ADD_DIALOG_H
+
+#include <gtk/gtk.h>
+
+#include "rb-podcast-manager.h"
+
+G_BEGIN_DECLS
+
+#define RB_TYPE_PODCAST_ADD_DIALOG         (rb_podcast_add_dialog_get_type ())
+#define RB_PODCAST_ADD_DIALOG(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_PODCAST_ADD_DIALOG, RBPodcastAddDialog))
+#define RB_PODCAST_ADD_DIALOG_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_PODCAST_ADD_DIALOG, RBPodcastAddDialogClass))
+#define RB_IS_PODCAST_ADD_DIALOG(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_PODCAST_ADD_DIALOG))
+#define RB_IS_PODCAST_ADD_DIALOG_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_PODCAST_ADD_DIALOG))
+#define RB_PODCAST_ADD_DIALOG_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_PODCAST_ADD_DIALOG, RBPodcastAddDialogClass))
+
+typedef struct _RBPodcastAddDialog RBPodcastAddDialog;
+typedef struct _RBPodcastAddDialogClass RBPodcastAddDialogClass;
+
+typedef struct RBPodcastAddDialogPrivate RBPodcastAddDialogPrivate;
+
+struct _RBPodcastAddDialog
+{
+	GtkHBox parent;
+
+	RBPodcastAddDialogPrivate *priv;
+};
+
+struct _RBPodcastAddDialogClass
+{
+	GtkHBoxClass parent;
+
+	/* signals */
+	void	(*close)	(RBPodcastAddDialog *dialog);
+	void	(*closed)	(RBPodcastAddDialog *dialog);
+};
+
+GType		rb_podcast_add_dialog_get_type		(void);
+
+GtkWidget *     rb_podcast_add_dialog_new		(RBShell *shell, RBPodcastManager *podcast_mgr);
+
+void		rb_podcast_add_dialog_reset		(RBPodcastAddDialog *dialog, const char *text, gboolean load);
+
+G_END_DECLS
+
+#endif /* RB_PODCAST_ADD_DIALOG_H */
diff --git a/podcast/rb-podcast-manager.c b/podcast/rb-podcast-manager.c
index 55758c2..3d5068b 100644
--- a/podcast/rb-podcast-manager.c
+++ b/podcast/rb-podcast-manager.c
@@ -61,7 +61,6 @@ enum
 
 enum
 {
-	STATUS_CHANGED,
 	START_DOWNLOAD,
 	FINISH_DOWNLOAD,
 	PROCESS_ERROR,
@@ -194,18 +193,6 @@ rb_podcast_manager_class_init (RBPodcastManagerClass *klass)
 							      RHYTHMDB_TYPE,
 							      G_PARAM_READWRITE));
 
-	rb_podcast_manager_signals[STATUS_CHANGED] =
-	       g_signal_new ("status_changed",
-		       		G_OBJECT_CLASS_TYPE (object_class),
-		 		G_SIGNAL_RUN_LAST,
-				G_STRUCT_OFFSET (RBPodcastManagerClass, status_changed),
-				NULL, NULL,
-				rb_marshal_VOID__BOXED_ULONG,
-				G_TYPE_NONE,
-				2,
-				RHYTHMDB_TYPE_ENTRY,
-				G_TYPE_ULONG);
-
 	rb_podcast_manager_signals[START_DOWNLOAD] =
 	       g_signal_new ("start_download",
 		       		G_OBJECT_CLASS_TYPE (object_class),
@@ -1500,9 +1487,6 @@ download_progress (RBPodcastManagerInfo *data, guint64 downloaded, guint64 total
 
 		rhythmdb_commit (data->pd->priv->db);
 
-		g_signal_emit (data->pd, rb_podcast_manager_signals[STATUS_CHANGED],
-			       0, data->entry, local_progress);
-
 		GDK_THREADS_LEAVE ();
 
 		data->progress = local_progress;
diff --git a/podcast/rb-podcast-manager.h b/podcast/rb-podcast-manager.h
index 6d0f3a0..bdde140 100644
--- a/podcast/rb-podcast-manager.h
+++ b/podcast/rb-podcast-manager.h
@@ -57,7 +57,6 @@ typedef struct
 	GObjectClass parent_class;
 
 	/* signals */
-	void        (*status_changed)    		(RBPodcastManager* pd, RhythmDBEntry *entry, glong value);
 	void        (*start_download)    		(RBPodcastManager* pd, RhythmDBEntry *entry);
 	void        (*finish_download)   		(RBPodcastManager* pd, RhythmDBEntry *entry);
 	void        (*feed_updates_available)   	(RBPodcastManager* pd, RhythmDBEntry *entry);
diff --git a/podcast/rb-podcast-source.c b/podcast/rb-podcast-source.c
index 4622b1b..279fcd0 100644
--- a/podcast/rb-podcast-source.c
+++ b/podcast/rb-podcast-source.c
@@ -40,6 +40,7 @@
 #include <glib.h>
 #include <glib/gi18n.h>
 #include <gtk/gtk.h>
+#include <libsoup/soup.h>
 
 #include "rb-podcast-source.h"
 #include "rb-podcast-settings.h"
@@ -54,7 +55,6 @@
 #include "rb-util.h"
 #include "rb-file-helpers.h"
 #include "rb-dialog.h"
-#include "rb-uri-dialog.h"
 #include "rb-podcast-properties-dialog.h"
 #include "rb-feed-podcast-properties-dialog.h"
 #include "rb-playlist-manager.h"
@@ -64,6 +64,7 @@
 #include "rb-cut-and-paste-code.h"
 #include "rb-source-search-basic.h"
 #include "rb-cell-renderer-pixbuf.h"
+#include "rb-podcast-add-dialog.h"
 #include "rb-source-toolbar.h"
 
 static void podcast_cmd_new_podcast		(GtkAction *action,
@@ -88,6 +89,9 @@ struct _RBPodcastSourcePrivate
 	guint prefs_notify_id;
 
 	GtkWidget *paned;
+	GtkWidget *add_dialog;
+	GtkAction *add_action;
+	RBSourceToolbar *toolbar;
 
 	RhythmDBPropertyModel *feed_model;
 	RBPropertyView *feeds;
@@ -107,8 +111,8 @@ struct _RBPodcastSourcePrivate
 
 static GtkActionEntry rb_podcast_source_actions [] =
 {
-	{ "MusicNewPodcast", RB_STOCK_PODCAST_NEW, N_("_New Podcast Feed..."), "<control>P",
-	  N_("Subscribe to a new Podcast Feed"),
+	{ "MusicNewPodcast", RB_STOCK_PODCAST_NEW, N_("_New Podcast Feed..."), NULL,
+	  N_("Subscribe to a new podcast feed"),
 	  G_CALLBACK (podcast_cmd_new_podcast) },
 	{ "PodcastSrcDownloadPost", NULL, N_("Download _Episode"), NULL,
 	  N_("Download Podcast Episode"),
@@ -340,35 +344,67 @@ posts_view_drag_data_received_cb (GtkWidget *widget,
 }
 
 static void
-podcast_location_added_cb (RBURIDialog *dialog,
-			   const char *location,
-			   RBPodcastSource *source)
+podcast_add_dialog_closed_cb (RBPodcastAddDialog *dialog, RBPodcastSource *source)
 {
-	rb_podcast_manager_subscribe_feed (source->priv->podcast_mgr,
-					   location,
-					   FALSE);
+	rb_podcast_source_do_query (source);
+	gtk_widget_hide (source->priv->add_dialog);
+	gtk_widget_show (GTK_WIDGET (source->priv->toolbar));
+	gtk_widget_show (source->priv->paned);
 }
 
 static void
-podcast_add_response_cb (GtkDialog *dialog, int response, gpointer meh)
+yank_clipboard_url (GtkClipboard *clipboard, const char *text, RBPodcastSource *source)
 {
-	gtk_widget_destroy (GTK_WIDGET (dialog));
+	SoupURI *uri;
+
+	if (text == NULL) {
+		return;
+	}
+
+	uri = soup_uri_new (text);
+	if (SOUP_URI_VALID_FOR_HTTP (uri)) {
+		rb_podcast_add_dialog_reset (RB_PODCAST_ADD_DIALOG (source->priv->add_dialog), text, FALSE);
+	}
+
+	if (uri != NULL) {
+		soup_uri_free (uri);
+	}
 }
 
 static void
 podcast_cmd_new_podcast (GtkAction *action, RBPodcastSource *source)
 {
-	GtkWidget *dialog;
+	RhythmDBQueryModel *query_model;
 
-	dialog = rb_uri_dialog_new (_("New Podcast Feed"), _("URL of podcast feed:"));
-	g_signal_connect_object (dialog,
-				 "location-added",
-				 G_CALLBACK (podcast_location_added_cb),
-				 source, 0);
-	g_signal_connect (dialog, "response", G_CALLBACK (podcast_add_response_cb), NULL);
-	gtk_widget_show_all (dialog);
+	rb_podcast_add_dialog_reset (RB_PODCAST_ADD_DIALOG (source->priv->add_dialog), NULL, FALSE);
+
+	/* if we can get a url from the clipboard, populate the dialog with that,
+	 * since there's a good chance that's what the user wants to do anyway.
+	 */
+	gtk_clipboard_request_text (gtk_clipboard_get (GDK_SELECTION_CLIPBOARD),
+				    (GtkClipboardTextReceivedFunc) yank_clipboard_url,
+				    source);
+	gtk_clipboard_request_text (gtk_clipboard_get (GDK_SELECTION_PRIMARY),
+				    (GtkClipboardTextReceivedFunc) yank_clipboard_url,
+				    source);
+
+	query_model = rhythmdb_query_model_new_empty (source->priv->db);
+	rb_entry_view_set_model (source->priv->posts, query_model);
+	g_object_set (source, "query-model", query_model, NULL);
+	g_object_unref (query_model);
+
+	gtk_widget_hide (source->priv->paned);
+	gtk_widget_hide (GTK_WIDGET (source->priv->toolbar));
+	gtk_widget_show (source->priv->add_dialog);
 }
 
+void
+rb_podcast_source_add_feed (RBPodcastSource *source, const char *text)
+{
+	gtk_action_activate (source->priv->add_action);
+
+	rb_podcast_add_dialog_reset (RB_PODCAST_ADD_DIALOG (source->priv->add_dialog), text, TRUE);
+}
 
 static void
 podcast_cmd_download_post (GtkAction *action, RBPodcastSource *source)
@@ -1283,7 +1319,6 @@ impl_constructed (GObject *object)
 	int position;
 	GtkUIManager *ui_manager;
 	GtkWidget *grid;
-	RBSourceToolbar *toolbar;
 
 	RB_CHAIN_GOBJECT_METHOD (rb_podcast_source_parent_class, constructed, object);
 	source = RB_PODCAST_SOURCE (object);
@@ -1305,11 +1340,11 @@ impl_constructed (GObject *object)
 						   rb_podcast_source_actions,
 						   G_N_ELEMENTS (rb_podcast_source_actions));
 
-	action = gtk_action_group_get_action (source->priv->action_group,
-					      "MusicNewPodcast");
+	source->priv->add_action = gtk_action_group_get_action (source->priv->action_group,
+								"MusicNewPodcast");
 	/* Translators: this is the toolbar button label
 	   for New Podcast Feed action. */
-	g_object_set (action, "short-label", C_("Podcast", "Add"), NULL);
+	g_object_set (source->priv->add_action, "short-label", C_("Podcast", "Add"), NULL);
 
 	action = gtk_action_group_get_action (source->priv->action_group,
 					      "PodcastFeedUpdate");
@@ -1334,7 +1369,6 @@ impl_constructed (GObject *object)
 
 	source->priv->paned = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
 
-	g_object_unref (shell);
 
 	/* set up posts view */
 	source->priv->posts = rb_entry_view_new (source->priv->db,
@@ -1535,8 +1569,8 @@ impl_constructed (GObject *object)
 			   GDK_ACTION_COPY | GDK_ACTION_MOVE);
 
 	/* set up toolbar */
-	toolbar = rb_source_toolbar_new (RB_SOURCE (source), ui_manager);
-	rb_source_toolbar_add_search_entry (toolbar, "/PodcastSourceSearchMenu", NULL);
+	source->priv->toolbar = rb_source_toolbar_new (RB_SOURCE (source), ui_manager);
+	rb_source_toolbar_add_search_entry (source->priv->toolbar, "/PodcastSourceSearchMenu", NULL);
 
 	/* pack the feed and post views into the source */
 	gtk_paned_pack1 (GTK_PANED (source->priv->paned),
@@ -1548,12 +1582,19 @@ impl_constructed (GObject *object)
 	gtk_widget_set_margin_top (GTK_WIDGET (grid), 6);
 	gtk_grid_set_column_spacing (GTK_GRID (grid), 6);
 	gtk_grid_set_row_spacing (GTK_GRID (grid), 6);
-	gtk_grid_attach (GTK_GRID (grid), GTK_WIDGET (toolbar), 0, 0, 1, 1);
+	gtk_grid_attach (GTK_GRID (grid), GTK_WIDGET (source->priv->toolbar), 0, 0, 1, 1);
 	gtk_grid_attach (GTK_GRID (grid), source->priv->paned, 0, 1, 1, 1);
 
 	gtk_container_add (GTK_CONTAINER (source), grid);
 
+	/* podcast add dialog */
+	source->priv->add_dialog = rb_podcast_add_dialog_new (shell, source->priv->podcast_mgr);
+	gtk_grid_attach (GTK_GRID (grid), GTK_WIDGET (source->priv->add_dialog), 0, 2, 1, 1);
+	gtk_widget_set_no_show_all (source->priv->add_dialog, TRUE);
+	g_signal_connect_object (source->priv->add_dialog, "closed", G_CALLBACK (podcast_add_dialog_closed_cb), source, 0);
+
 	gtk_widget_show_all (GTK_WIDGET (source));
+	gtk_widget_hide (source->priv->add_dialog);
 
 	g_object_get (source, "settings", &settings, NULL);
 
@@ -1569,6 +1610,7 @@ impl_constructed (GObject *object)
 
 	g_object_unref (settings);
 	g_object_unref (ui_manager);
+	g_object_unref (shell);
 
 	rb_podcast_source_do_query (source);
 }
diff --git a/podcast/rb-podcast-source.h b/podcast/rb-podcast-source.h
index bea1f1f..d363731 100644
--- a/podcast/rb-podcast-source.h
+++ b/podcast/rb-podcast-source.h
@@ -66,6 +66,8 @@ RBSource 	*rb_podcast_source_new		(RBShell *shell,
 						 const char *name,
 						 const char *icon_name);
 
+void		rb_podcast_source_add_feed	(RBPodcastSource *source, const char *url);
+
 G_END_DECLS
 
 #endif /* __RB_PODCAST_SOURCE_H */
diff --git a/shell/rb-shell.c b/shell/rb-shell.c
index 347daf4..b7c1939 100644
--- a/shell/rb-shell.c
+++ b/shell/rb-shell.c
@@ -3616,11 +3616,10 @@ rb_shell_load_uri (RBShell *shell,
 {
 	RhythmDBEntry *entry;
 
-	/* If the URI points to a Podcast, pass it on to
-	 * the Podcast source */
+	/* If the URI points to a Podcast, pass it on to the Podcast source */
 	if (rb_uri_could_be_podcast (uri, NULL)) {
-		rb_podcast_manager_subscribe_feed (shell->priv->podcast_manager, uri, FALSE);
 		rb_shell_select_page (shell, RB_DISPLAY_PAGE (shell->priv->podcast_source));
+		rb_podcast_source_add_feed (shell->priv->podcast_source, uri);
 		return TRUE;
 	}
 



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]