1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2  *
3  *  Copyright (C) 2010 Jonathan Matthew <jonathan@d14n.org>
4  *
5  *  This program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2 of the License, or
8  *  (at your option) any later version.
9  *
10  *  The Rhythmbox authors hereby grant permission for non-GPL compatible
11  *  GStreamer plugins to be used and distributed together with GStreamer
12  *  and Rhythmbox. This permission is above and beyond the permissions granted
13  *  by the GPL license by which Rhythmbox is covered. If you modify this code
14  *  you may extend this exception to your version of the code, but you are not
15  *  obligated to do so. If you do not wish to do so, delete this exception
16  *  statement from your version.
17  *
18  *  This program is distributed in the hope that it will be useful,
19  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
20  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  *  GNU General Public License for more details.
22  *
23  *  You should have received a copy of the GNU General Public License
24  *  along with this program; if not, write to the Free Software
25  *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
26  *
27  */
28 
29 #include "config.h"
30 
31 #include <glib/gi18n.h>
32 #include <gtk/gtk.h>
33 
34 #include "rb-shell.h"
35 #include "rb-shell-player.h"
36 #include "rb-podcast-add-dialog.h"
37 #include "rb-podcast-search.h"
38 #include "rb-podcast-entry-types.h"
39 #include "rb-builder-helpers.h"
40 #include "rb-debug.h"
41 #include "rb-util.h"
42 #include "rb-cut-and-paste-code.h"
43 #include "rb-search-entry.h"
44 #include "nautilus-floating-bar.h"
45 
46 static void rb_podcast_add_dialog_class_init (RBPodcastAddDialogClass *klass);
47 static void rb_podcast_add_dialog_init (RBPodcastAddDialog *dialog);
48 
49 enum {
50 	PROP_0,
51 	PROP_PODCAST_MANAGER,
52 	PROP_SHELL,
53 };
54 
55 enum {
56 	CLOSE,
57 	CLOSED,
58 	LAST_SIGNAL
59 };
60 
61 enum {
62 	FEED_COLUMN_TITLE = 0,
63 	FEED_COLUMN_AUTHOR,
64 	FEED_COLUMN_IMAGE,
65 	FEED_COLUMN_IMAGE_FILE,
66 	FEED_COLUMN_EPISODE_COUNT,
67 	FEED_COLUMN_PARSED_FEED,
68 	FEED_COLUMN_DATE,
69 };
70 
71 struct RBPodcastAddDialogPrivate
72 {
73 	RBPodcastManager *podcast_mgr;
74 	RhythmDB *db;
75 	RBShell *shell;
76 
77 	GtkWidget *feed_view;
78 	GtkListStore *feed_model;
79 	GtkWidget *feed_status;
80 
81 	GtkWidget *episode_view;
82 
83 	GtkWidget *text_entry;
84 	GtkWidget *subscribe_button;
85 	GtkWidget *info_bar;
86 	GtkWidget *info_bar_message;
87 
88 	RBSearchEntry *search_entry;
89 
90 	gboolean paned_size_set;
91 	gboolean have_selection;
92 	gboolean clearing;
93 	GtkTreeIter selected_feed;
94 
95 	int running_searches;
96 	gboolean search_successful;
97 	int reset_count;
98 };
99 
100 /* various prefixes that identify things we treat as feed URLs rather than search terms */
101 static const char *podcast_uri_prefixes[] = {
102 	"http://",
103 	"https://",
104 	"feed://",
105 	"zcast://",
106 	"zune://",
107 	"itpc://",
108 	"itms://",
109 	"itmss://",
110 	"www.",
111 };
112 
113 /* number of search results to request from each available search */
114 #define PODCAST_SEARCH_LIMIT		25
115 
116 #define PODCAST_IMAGE_SIZE		50
117 
118 static guint signals[LAST_SIGNAL] = {0,};
119 
120 G_DEFINE_TYPE (RBPodcastAddDialog, rb_podcast_add_dialog, GTK_TYPE_BOX);
121 
122 
123 static gboolean
remove_all_feeds_cb(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,RBPodcastAddDialog * dialog)124 remove_all_feeds_cb (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, RBPodcastAddDialog *dialog)
125 {
126 	RBPodcastChannel *channel;
127 	gtk_tree_model_get (model, iter, FEED_COLUMN_PARSED_FEED, &channel, -1);
128 	rb_podcast_parse_channel_free (channel);
129 	return FALSE;
130 }
131 
132 static void
remove_all_feeds(RBPodcastAddDialog * dialog)133 remove_all_feeds (RBPodcastAddDialog *dialog)
134 {
135 	/* remove all feeds from the model and free associated data */
136 	gtk_tree_model_foreach (GTK_TREE_MODEL (dialog->priv->feed_model),
137 				(GtkTreeModelForeachFunc) remove_all_feeds_cb,
138 				dialog);
139 
140 	dialog->priv->clearing = TRUE;
141 	gtk_list_store_clear (dialog->priv->feed_model);
142 	dialog->priv->clearing = FALSE;
143 
144 	dialog->priv->have_selection = FALSE;
145 	gtk_widget_set_sensitive (dialog->priv->subscribe_button, FALSE);
146 	gtk_widget_hide (dialog->priv->feed_status);
147 }
148 
149 static void
add_posts_for_feed(RBPodcastAddDialog * dialog,RBPodcastChannel * channel)150 add_posts_for_feed (RBPodcastAddDialog *dialog, RBPodcastChannel *channel)
151 {
152 	GList *l;
153 
154 	for (l = channel->posts; l != NULL; l = l->next) {
155 		RBPodcastItem *item = (RBPodcastItem *) l->data;
156 
157 		rb_podcast_manager_add_post (dialog->priv->db,
158 					     TRUE,
159 					     channel->title ? channel->title : channel->url,
160 					     item->title,
161 					     channel->url,
162 					     (item->author ? item->author : channel->author),
163 					     item->url,
164 					     item->description,
165 					     (item->pub_date > 0 ? item->pub_date : channel->pub_date),
166 					     item->duration,
167 					     item->filesize);
168 	}
169 
170 	rhythmdb_commit (dialog->priv->db);
171 }
172 
173 static void
image_file_read_cb(GObject * file,GAsyncResult * result,RBPodcastAddDialog * dialog)174 image_file_read_cb (GObject *file, GAsyncResult *result, RBPodcastAddDialog *dialog)
175 {
176 	GFileInputStream *stream;
177 	GdkPixbuf *pixbuf;
178 	GError *error = NULL;
179 
180 	stream = g_file_read_finish (G_FILE (file), result, &error);
181 	if (error != NULL) {
182 		rb_debug ("podcast image read failed: %s", error->message);
183 		g_clear_error (&error);
184 		g_object_unref (dialog);
185 		return;
186 	}
187 
188 	pixbuf = gdk_pixbuf_new_from_stream_at_scale (G_INPUT_STREAM (stream), PODCAST_IMAGE_SIZE, PODCAST_IMAGE_SIZE, TRUE, NULL, &error);
189 	if (error != NULL) {
190 		rb_debug ("podcast image load failed: %s", error->message);
191 		g_clear_error (&error);
192 	} else {
193 		GtkTreeIter iter;
194 
195 		if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (dialog->priv->feed_model), &iter)) {
196 			do {
197 				GFile *feedfile;
198 				gtk_tree_model_get (GTK_TREE_MODEL (dialog->priv->feed_model), &iter,
199 						    FEED_COLUMN_IMAGE_FILE, &feedfile,
200 						    -1);
201 				if (feedfile == G_FILE (file)) {
202 					gtk_list_store_set (dialog->priv->feed_model,
203 							    &iter,
204 							    FEED_COLUMN_IMAGE, g_object_ref (pixbuf),
205 							    -1);
206 					break;
207 				}
208 			} while (gtk_tree_model_iter_next (GTK_TREE_MODEL (dialog->priv->feed_model), &iter));
209 		}
210 		g_object_unref (pixbuf);
211 	}
212 
213 	g_object_unref (dialog);
214 	g_object_unref (stream);
215 }
216 
217 static void
insert_search_result(RBPodcastAddDialog * dialog,RBPodcastChannel * channel,gboolean select)218 insert_search_result (RBPodcastAddDialog *dialog, RBPodcastChannel *channel, gboolean select)
219 {
220 	GtkTreeIter iter;
221 	GFile *image_file;
222 	int episodes;
223 
224 	if (channel->posts) {
225 		episodes = g_list_length (channel->posts);
226 	} else {
227 		episodes = channel->num_posts;
228 	}
229 
230 	/* if there's an image to load, fetch it */
231 	if (channel->img) {
232 		rb_debug ("fetching image %s", channel->img);
233 		image_file = g_file_new_for_uri (channel->img);
234 	} else {
235 		image_file = NULL;
236 	}
237 
238 	gtk_list_store_insert_with_values (dialog->priv->feed_model,
239 					   &iter,
240 					   G_MAXINT,
241 					   FEED_COLUMN_TITLE, channel->title,
242 					   FEED_COLUMN_AUTHOR, channel->author,
243 					   FEED_COLUMN_EPISODE_COUNT, episodes,
244 					   FEED_COLUMN_IMAGE, NULL,
245 					   FEED_COLUMN_IMAGE_FILE, image_file,
246 					   FEED_COLUMN_PARSED_FEED, channel,
247 					   -1);
248 
249 	if (image_file != NULL) {
250 		g_file_read_async (image_file,
251 				   G_PRIORITY_DEFAULT,
252 				   NULL,
253 				   (GAsyncReadyCallback) image_file_read_cb,
254 				   g_object_ref (dialog));
255 	}
256 
257 	if (select) {
258 		GtkTreeSelection *selection;
259 		selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (dialog->priv->feed_view));
260 		gtk_tree_selection_select_iter (selection, &iter);
261 	}
262 }
263 
264 static void
update_feed_status(RBPodcastAddDialog * dialog)265 update_feed_status (RBPodcastAddDialog *dialog)
266 {
267 	int feeds;
268 	char *text;
269 
270 	feeds = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (dialog->priv->feed_model), NULL);
271 	text = g_strdup_printf (ngettext ("%d feed", "%d feeds", feeds), feeds);
272 	nautilus_floating_bar_set_primary_label (NAUTILUS_FLOATING_BAR (dialog->priv->feed_status), text);
273 	g_free (text);
274 
275 	nautilus_floating_bar_set_show_spinner (NAUTILUS_FLOATING_BAR (dialog->priv->feed_status),
276 						dialog->priv->running_searches > 0);
277 	gtk_widget_show (dialog->priv->feed_status);
278 }
279 
280 
281 typedef struct {
282 	RBPodcastAddDialog *dialog;
283 	char *url;
284 	RBPodcastChannel *channel;
285 	gboolean existing;
286 	gboolean single;
287 	GError *error;
288 	int reset_count;
289 } ParseThreadData;
290 
291 static gboolean
parse_finished(ParseThreadData * data)292 parse_finished (ParseThreadData *data)
293 {
294 	if (data->reset_count != data->dialog->priv->reset_count) {
295 		rb_debug ("dialog reset while parsing");
296 		rb_podcast_parse_channel_free (data->channel);
297 		g_object_unref (data->dialog);
298 		g_clear_error (&data->error);
299 		g_free (data->url);
300 		g_free (data);
301 		return FALSE;
302 	}
303 
304 	if (data->error != NULL) {
305 		gtk_label_set_label (GTK_LABEL (data->dialog->priv->info_bar_message),
306 				     _("Unable to load the feed. Check your network connection."));
307 		gtk_widget_show (data->dialog->priv->info_bar);
308 	} else {
309 		gtk_widget_hide (data->dialog->priv->info_bar);
310 	}
311 
312 	if (data->channel->is_opml) {
313 		GList *l;
314 		/* convert each item into its own channel */
315 		for (l = data->channel->posts; l != NULL; l = l->next) {
316 			RBPodcastChannel *channel;
317 			RBPodcastItem *item;
318 
319 			item = l->data;
320 			channel = g_new0 (RBPodcastChannel, 1);
321 			channel->url = g_strdup (item->url);
322 			channel->title = g_strdup (item->title);
323 			/* none of the other fields get populated anyway */
324 			insert_search_result (data->dialog, channel, FALSE);
325 		}
326 		update_feed_status (data->dialog);
327 		rb_podcast_parse_channel_free (data->channel);
328 	} else if (data->existing) {
329 		/* find the row for the feed, replace the channel */
330 		GtkTreeIter iter;
331 		gboolean found = FALSE;
332 
333 		if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (data->dialog->priv->feed_model), &iter)) {
334 			do {
335 				RBPodcastChannel *channel;
336 				gtk_tree_model_get (GTK_TREE_MODEL (data->dialog->priv->feed_model), &iter,
337 						    FEED_COLUMN_PARSED_FEED, &channel,
338 						    -1);
339 				if (g_strcmp0 (channel->url, data->url) == 0) {
340 					gtk_list_store_set (data->dialog->priv->feed_model,
341 							    &iter,
342 							    FEED_COLUMN_PARSED_FEED, data->channel,
343 							    -1);
344 					found = TRUE;
345 					rb_podcast_parse_channel_free (channel);
346 					break;
347 				}
348 			} while (gtk_tree_model_iter_next (GTK_TREE_MODEL (data->dialog->priv->feed_model), &iter));
349 		}
350 
351 		/* if the row is selected, create entries for the channel contents */
352 		if (found == FALSE) {
353 			rb_podcast_parse_channel_free (data->channel);
354 		} else if (data->dialog->priv->have_selection) {
355 			GtkTreePath *a;
356 			GtkTreePath *b;
357 
358 			a = gtk_tree_model_get_path (GTK_TREE_MODEL (data->dialog->priv->feed_model), &iter);
359 			b = gtk_tree_model_get_path (GTK_TREE_MODEL (data->dialog->priv->feed_model), &data->dialog->priv->selected_feed);
360 			if (gtk_tree_path_compare (a, b) == 0) {
361 				add_posts_for_feed (data->dialog, data->channel);
362 			}
363 
364 			gtk_tree_path_free (a);
365 			gtk_tree_path_free (b);
366 		}
367 	} else {
368 		/* model owns data->channel now */
369 		insert_search_result (data->dialog, data->channel, data->single);
370 		update_feed_status (data->dialog);
371 	}
372 
373 	g_object_unref (data->dialog);
374 	g_clear_error (&data->error);
375 	g_free (data->url);
376 	g_free (data);
377 	return FALSE;
378 }
379 
380 static gpointer
parse_thread(ParseThreadData * data)381 parse_thread (ParseThreadData *data)
382 {
383 	if (rb_podcast_parse_load_feed (data->channel, data->url, FALSE, &data->error) == FALSE) {
384 		/* fake up a channel with just the url as the title, allowing the user
385 		 * to subscribe to the podcast anyway.
386 		 */
387 		data->channel->url = g_strdup (data->url);
388 		data->channel->title = g_strdup (data->url);
389 	}
390 
391 	g_idle_add ((GSourceFunc) parse_finished, data);
392 	return NULL;
393 }
394 
395 static void
parse_in_thread(RBPodcastAddDialog * dialog,const char * text,gboolean existing,gboolean single)396 parse_in_thread (RBPodcastAddDialog *dialog, const char *text, gboolean existing, gboolean single)
397 {
398 	ParseThreadData *data;
399 
400 	data = g_new0 (ParseThreadData, 1);
401 	data->dialog = g_object_ref (dialog);
402 	data->url = g_strdup (text);
403 	data->channel = g_new0 (RBPodcastChannel, 1);
404 	data->existing = existing;
405 	data->single = single;
406 	data->reset_count = dialog->priv->reset_count;
407 
408 	g_thread_new ("podcast parser", (GThreadFunc) parse_thread, data);
409 }
410 
411 static void
podcast_search_result_cb(RBPodcastSearch * search,RBPodcastChannel * feed,RBPodcastAddDialog * dialog)412 podcast_search_result_cb (RBPodcastSearch *search, RBPodcastChannel *feed, RBPodcastAddDialog *dialog)
413 {
414 	rb_debug ("got result %s from podcast search %s", feed->url, G_OBJECT_TYPE_NAME (search));
415 	insert_search_result (dialog, rb_podcast_parse_channel_copy (feed), FALSE);
416 }
417 
418 static void
podcast_search_finished_cb(RBPodcastSearch * search,gboolean successful,RBPodcastAddDialog * dialog)419 podcast_search_finished_cb (RBPodcastSearch *search, gboolean successful, RBPodcastAddDialog *dialog)
420 {
421 	rb_debug ("podcast search %s finished", G_OBJECT_TYPE_NAME (search));
422 	g_object_unref (search);
423 
424 	dialog->priv->search_successful |= successful;
425 	dialog->priv->running_searches--;
426 	update_feed_status (dialog);
427 
428 	if (dialog->priv->running_searches == 0) {
429 		if (dialog->priv->search_successful == FALSE) {
430 			gtk_label_set_label (GTK_LABEL (dialog->priv->info_bar_message),
431 					     _("Unable to search for podcasts. Check your network connection."));
432 			gtk_widget_show (dialog->priv->info_bar);
433 			gtk_widget_hide (dialog->priv->feed_status);
434 		}
435 	}
436 }
437 
438 static void
search_cb(RBSearchEntry * entry,const char * text,RBPodcastAddDialog * dialog)439 search_cb (RBSearchEntry *entry, const char *text, RBPodcastAddDialog *dialog)
440 {
441 	GList *searches;
442 	GList *s;
443 	int i;
444 
445 	/* remove previous feeds */
446 	remove_all_feeds (dialog);
447 	rhythmdb_entry_delete_by_type (dialog->priv->db, RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH);
448 	rhythmdb_commit (dialog->priv->db);
449 
450 	gtk_widget_hide (dialog->priv->info_bar);
451 
452 	if (text == NULL || text[0] == '\0') {
453 		return;
454 	}
455 
456 	/* if the entered text looks like a feed URL, parse it directly */
457 	for (i = 0; i < G_N_ELEMENTS (podcast_uri_prefixes); i++) {
458 		if (g_str_has_prefix (text, podcast_uri_prefixes[i])) {
459 			parse_in_thread (dialog, text, FALSE, TRUE);
460 			return;
461 		}
462 	}
463 
464 	/* not really sure about this one */
465 	if (g_path_is_absolute (text)) {
466 		parse_in_thread (dialog, text, FALSE, TRUE);
467 		return;
468 	}
469 
470 	/* otherwise, try podcast searches */
471 	dialog->priv->search_successful = FALSE;
472 	searches = rb_podcast_manager_get_searches (dialog->priv->podcast_mgr);
473 	for (s = searches; s != NULL; s = s->next) {
474 		RBPodcastSearch *search = s->data;
475 
476 		g_signal_connect_object (search, "result", G_CALLBACK (podcast_search_result_cb), dialog, 0);
477 		g_signal_connect_object (search, "finished", G_CALLBACK (podcast_search_finished_cb), dialog, 0);
478 		rb_podcast_search_start (search, text, PODCAST_SEARCH_LIMIT);
479 		dialog->priv->running_searches++;
480 	}
481 }
482 
483 static void
subscribe_selected_feed(RBPodcastAddDialog * dialog)484 subscribe_selected_feed (RBPodcastAddDialog *dialog)
485 {
486 	RBPodcastChannel *channel;
487 
488 	g_assert (dialog->priv->have_selection);
489 
490 	rhythmdb_entry_delete_by_type (dialog->priv->db, RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH);
491 	rhythmdb_commit (dialog->priv->db);
492 
493 	/* subscribe selected feed */
494 	gtk_tree_model_get (GTK_TREE_MODEL (dialog->priv->feed_model),
495 			    &dialog->priv->selected_feed,
496 			    FEED_COLUMN_PARSED_FEED, &channel,
497 			    -1);
498 	if (channel->posts != NULL) {
499 		rb_podcast_manager_add_parsed_feed (dialog->priv->podcast_mgr, channel);
500 	} else {
501 		rb_podcast_manager_subscribe_feed (dialog->priv->podcast_mgr, channel->url, TRUE);
502 	}
503 }
504 
505 static void
subscribe_clicked_cb(GtkButton * button,RBPodcastAddDialog * dialog)506 subscribe_clicked_cb (GtkButton *button, RBPodcastAddDialog *dialog)
507 {
508 	if (dialog->priv->have_selection == FALSE) {
509 		rb_debug ("no selection");
510 		return;
511 	}
512 
513 	subscribe_selected_feed (dialog);
514 
515 	dialog->priv->clearing = TRUE;
516 	gtk_list_store_remove (GTK_LIST_STORE (dialog->priv->feed_model), &dialog->priv->selected_feed);
517 	dialog->priv->clearing = FALSE;
518 
519 	gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (GTK_TREE_VIEW (dialog->priv->feed_view)));
520 	gtk_widget_set_sensitive (dialog->priv->subscribe_button, FALSE);
521 }
522 
523 static void
close_clicked_cb(GtkButton * button,RBPodcastAddDialog * dialog)524 close_clicked_cb (GtkButton *button, RBPodcastAddDialog *dialog)
525 {
526 	g_signal_emit (dialog, signals[CLOSED], 0);
527 }
528 
529 static void
feed_activated_cb(GtkTreeView * view,GtkTreePath * path,GtkTreeViewColumn * column,RBPodcastAddDialog * dialog)530 feed_activated_cb (GtkTreeView *view, GtkTreePath *path, GtkTreeViewColumn *column, RBPodcastAddDialog *dialog)
531 {
532 	gtk_tree_model_get_iter (GTK_TREE_MODEL (dialog->priv->feed_model), &dialog->priv->selected_feed, path);
533 	dialog->priv->have_selection = TRUE;
534 
535 	subscribe_selected_feed (dialog);
536 
537 	dialog->priv->have_selection = FALSE;
538 
539 	g_signal_emit (dialog, signals[CLOSED], 0);
540 }
541 
542 static void
feed_selection_changed_cb(GtkTreeSelection * selection,RBPodcastAddDialog * dialog)543 feed_selection_changed_cb (GtkTreeSelection *selection, RBPodcastAddDialog *dialog)
544 {
545 	GtkTreeModel *model;
546 
547 	if (dialog->priv->clearing)
548 		return;
549 
550 	dialog->priv->have_selection =
551 		gtk_tree_selection_get_selected (selection, &model, &dialog->priv->selected_feed);
552 	gtk_widget_set_sensitive (dialog->priv->subscribe_button, dialog->priv->have_selection);
553 
554 	rhythmdb_entry_delete_by_type (dialog->priv->db, RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH);
555 	rhythmdb_commit (dialog->priv->db);
556 
557 	if (dialog->priv->have_selection) {
558 		RBPodcastChannel *channel = NULL;
559 
560 		gtk_tree_model_get (model,
561 				    &dialog->priv->selected_feed,
562 				    FEED_COLUMN_PARSED_FEED, &channel,
563 				    -1);
564 
565 		if (channel->posts == NULL) {
566 			rb_debug ("parsing feed %s to get posts", channel->url);
567 			parse_in_thread (dialog, channel->url, TRUE, FALSE);
568 		} else {
569 			add_posts_for_feed (dialog, channel);
570 		}
571 	}
572 }
573 
574 static void
episode_count_column_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * model,GtkTreeIter * iter,gpointer data)575 episode_count_column_cell_data_func (GtkTreeViewColumn *column,
576 				     GtkCellRenderer *renderer,
577 				     GtkTreeModel *model,
578 				     GtkTreeIter *iter,
579 				     gpointer data)
580 {
581 	GtkTreeIter parent;
582 	if (gtk_tree_model_iter_parent (model, &parent, iter)) {
583 		g_object_set (renderer, "visible", FALSE, NULL);
584 	} else {
585 		int count;
586 		char *text;
587 		gtk_tree_model_get (model, iter, FEED_COLUMN_EPISODE_COUNT, &count, -1);
588 		text = g_strdup_printf ("%d", count);
589 		g_object_set (renderer, "visible", TRUE, "text", text, NULL);
590 		g_free (text);
591 	}
592 }
593 
594 static void
podcast_post_date_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,gpointer data)595 podcast_post_date_cell_data_func (GtkTreeViewColumn *column,
596 				  GtkCellRenderer *renderer,
597 				  GtkTreeModel *tree_model,
598 				  GtkTreeIter *iter,
599 				  gpointer data)
600 {
601 	RhythmDBEntry *entry;
602 	gulong value;
603 	char *str;
604 
605 	gtk_tree_model_get (tree_model, iter, 0, &entry, -1);
606 
607 	value = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_POST_TIME);
608         if (value == 0) {
609 		str = g_strdup (_("Unknown"));
610 	} else {
611 		str = rb_utf_friendly_time (value);
612 	}
613 
614 	g_object_set (G_OBJECT (renderer), "text", str, NULL);
615 	g_free (str);
616 
617 	rhythmdb_entry_unref (entry);
618 }
619 
620 static gint
podcast_post_date_sort_func(RhythmDBEntry * a,RhythmDBEntry * b,RhythmDBQueryModel * model)621 podcast_post_date_sort_func (RhythmDBEntry *a,
622 			     RhythmDBEntry *b,
623 			     RhythmDBQueryModel *model)
624 {
625 	gulong a_val, b_val;
626 	gint ret;
627 
628 	a_val = rhythmdb_entry_get_ulong (a, RHYTHMDB_PROP_POST_TIME);
629 	b_val = rhythmdb_entry_get_ulong (b, RHYTHMDB_PROP_POST_TIME);
630 
631 	if (a_val != b_val)
632 		ret = (a_val > b_val) ? 1 : -1;
633 	else
634 		ret = rhythmdb_query_model_title_sort_func (a, b, model);
635 
636         return ret;
637 }
638 
639 static void
episodes_sort_changed_cb(GObject * object,GParamSpec * pspec,RBPodcastAddDialog * dialog)640 episodes_sort_changed_cb (GObject *object, GParamSpec *pspec, RBPodcastAddDialog *dialog)
641 {
642 	rb_entry_view_resort_model (RB_ENTRY_VIEW (object));
643 }
644 
645 static void
impl_close(RBPodcastAddDialog * dialog)646 impl_close (RBPodcastAddDialog *dialog)
647 {
648 	g_signal_emit (dialog, signals[CLOSED], 0);
649 }
650 
651 static gboolean
set_paned_position(GtkWidget * paned)652 set_paned_position (GtkWidget *paned)
653 {
654 	gtk_paned_set_position (GTK_PANED (paned), gtk_widget_get_allocated_height (paned) / 2);
655 	g_object_unref (paned);
656 	return FALSE;
657 }
658 
659 static void
paned_size_allocate_cb(GtkWidget * widget,GdkRectangle * allocation,RBPodcastAddDialog * dialog)660 paned_size_allocate_cb (GtkWidget *widget, GdkRectangle *allocation, RBPodcastAddDialog *dialog)
661 {
662 	if (dialog->priv->paned_size_set == FALSE) {
663 		dialog->priv->paned_size_set = TRUE;
664 		g_idle_add ((GSourceFunc) set_paned_position, g_object_ref (widget));
665 	}
666 }
667 
668 static void
episode_entry_activated_cb(RBEntryView * entry_view,RhythmDBEntry * entry,RBPodcastAddDialog * dialog)669 episode_entry_activated_cb (RBEntryView *entry_view, RhythmDBEntry *entry, RBPodcastAddDialog *dialog)
670 {
671 	rb_debug ("search result podcast entry %s activated", rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION));
672 	rb_shell_load_uri (dialog->priv->shell,
673 			   rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION),
674 			   TRUE,
675 			   NULL);
676 }
677 
678 static void
impl_constructed(GObject * object)679 impl_constructed (GObject *object)
680 {
681 	RBPodcastAddDialog *dialog;
682 	GtkBuilder *builder;
683 	GtkWidget *widget;
684 	GtkWidget *paned;
685 	GtkWidget *overlay;
686 	GtkTreeViewColumn *column;
687 	GtkCellRenderer *renderer;
688 	RBEntryView *episodes;
689 	RBShellPlayer *shell_player;
690 	RhythmDBQuery *query;
691 	RhythmDBQueryModel *query_model;
692 	const char *episode_strings[3];
693 
694 	RB_CHAIN_GOBJECT_METHOD (rb_podcast_add_dialog_parent_class, constructed, object);
695 	dialog = RB_PODCAST_ADD_DIALOG (object);
696 
697 	g_object_get (dialog->priv->podcast_mgr, "db", &dialog->priv->db, NULL);
698 
699 	builder = rb_builder_load ("podcast-add-dialog.ui", NULL);
700 
701 	dialog->priv->info_bar_message = gtk_label_new ("");
702 	dialog->priv->info_bar = gtk_info_bar_new ();
703 	g_object_set (dialog->priv->info_bar, "spacing", 0, NULL);
704 	gtk_container_add (GTK_CONTAINER (gtk_info_bar_get_content_area (GTK_INFO_BAR (dialog->priv->info_bar))),
705 			   dialog->priv->info_bar_message);
706 	gtk_widget_set_no_show_all (dialog->priv->info_bar, TRUE);
707 	gtk_box_pack_start (GTK_BOX (dialog), dialog->priv->info_bar, FALSE, FALSE, 0);
708 	gtk_widget_show (dialog->priv->info_bar_message);
709 
710 	dialog->priv->subscribe_button = GTK_WIDGET (gtk_builder_get_object (builder, "subscribe-button"));
711 	g_signal_connect_object (dialog->priv->subscribe_button, "clicked", G_CALLBACK (subscribe_clicked_cb), dialog, 0);
712 	gtk_widget_set_sensitive (dialog->priv->subscribe_button, FALSE);
713 
714 	dialog->priv->feed_view = GTK_WIDGET (gtk_builder_get_object (builder, "feed-view"));
715 	g_signal_connect (dialog->priv->feed_view, "row-activated", G_CALLBACK (feed_activated_cb), dialog);
716 	g_signal_connect (gtk_tree_view_get_selection (GTK_TREE_VIEW (dialog->priv->feed_view)),
717 			  "changed",
718 			  G_CALLBACK (feed_selection_changed_cb),
719 			  dialog);
720 
721 	dialog->priv->search_entry = rb_search_entry_new (FALSE);
722 	gtk_widget_set_size_request (GTK_WIDGET (dialog->priv->search_entry), 400, -1);
723 	g_object_set (dialog->priv->search_entry,"explicit-mode", TRUE, NULL);
724 	g_signal_connect (dialog->priv->search_entry, "search", G_CALLBACK (search_cb), dialog);
725 	g_signal_connect (dialog->priv->search_entry, "activate", G_CALLBACK (search_cb), dialog);
726 	gtk_container_add (GTK_CONTAINER (gtk_builder_get_object (builder, "search-entry-box")),
727 			   GTK_WIDGET (dialog->priv->search_entry));
728 
729 	g_signal_connect (gtk_builder_get_object (builder, "close-button"),
730 			  "clicked",
731 			  G_CALLBACK (close_clicked_cb),
732 			  dialog);
733 
734 	dialog->priv->feed_model = gtk_list_store_new (7,
735 						       G_TYPE_STRING,	/* name */
736 						       G_TYPE_STRING,	/* author */
737 						       GDK_TYPE_PIXBUF, /* image */
738 						       G_TYPE_FILE,	/* image file */
739 						       G_TYPE_INT,	/* episode count */
740 						       G_TYPE_POINTER,	/* RBPodcastChannel */
741 						       G_TYPE_ULONG);	/* date */
742 	gtk_tree_view_set_model (GTK_TREE_VIEW (dialog->priv->feed_view), GTK_TREE_MODEL (dialog->priv->feed_model));
743 
744 	column = gtk_tree_view_column_new_with_attributes (_("Title"), gtk_cell_renderer_pixbuf_new (), "pixbuf", FEED_COLUMN_IMAGE, NULL);
745 	renderer = gtk_cell_renderer_text_new ();
746 	g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
747 	gtk_tree_view_column_pack_start (column, renderer, TRUE);
748 	gtk_tree_view_column_set_attributes (column, renderer, "text", FEED_COLUMN_TITLE, NULL);
749 
750 	gtk_tree_view_column_set_expand (column, TRUE);
751 	gtk_tree_view_append_column (GTK_TREE_VIEW (dialog->priv->feed_view), column);
752 
753 	renderer = gtk_cell_renderer_text_new ();
754 	g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
755 	column = gtk_tree_view_column_new_with_attributes (_("Author"), renderer, "text", FEED_COLUMN_AUTHOR, NULL);
756 	gtk_tree_view_column_set_expand (column, TRUE);
757 	gtk_tree_view_append_column (GTK_TREE_VIEW (dialog->priv->feed_view), column);
758 
759 	renderer = gtk_cell_renderer_text_new ();
760 	column = gtk_tree_view_column_new_with_attributes (_("Episodes"), renderer, NULL);
761 	gtk_tree_view_column_set_cell_data_func (column, renderer, episode_count_column_cell_data_func, NULL, NULL);
762 	episode_strings[0] = "0000";
763 	episode_strings[1] = _("Episodes");
764 	episode_strings[2] = NULL;
765 	rb_set_tree_view_column_fixed_width (dialog->priv->feed_view, column, renderer, episode_strings, 6);
766 	gtk_tree_view_append_column (GTK_TREE_VIEW (dialog->priv->feed_view), column);
767 
768 	overlay = GTK_WIDGET (gtk_builder_get_object (builder, "overlay"));
769 	gtk_widget_add_events (overlay, GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
770 	dialog->priv->feed_status = nautilus_floating_bar_new (NULL, NULL, FALSE);
771 	gtk_widget_set_no_show_all (dialog->priv->feed_status, TRUE);
772 	gtk_widget_set_halign (dialog->priv->feed_status, GTK_ALIGN_END);
773 	gtk_widget_set_valign (dialog->priv->feed_status, GTK_ALIGN_END);
774 	gtk_overlay_add_overlay (GTK_OVERLAY (overlay), dialog->priv->feed_status);
775 
776 	widget = GTK_WIDGET (gtk_builder_get_object (builder, "podcast-add-dialog"));
777 	gtk_box_pack_start (GTK_BOX (dialog), widget, TRUE, TRUE, 0);
778 
779 	/* set up episode view */
780 	g_object_get (dialog->priv->shell, "shell-player", &shell_player, NULL);
781 	episodes = rb_entry_view_new (dialog->priv->db, G_OBJECT (shell_player), TRUE, FALSE);
782 	g_object_unref (shell_player);
783 
784 	g_signal_connect (episodes, "entry-activated", G_CALLBACK (episode_entry_activated_cb), dialog);
785 
786 	/* date column */
787 	column = gtk_tree_view_column_new ();
788 	renderer = gtk_cell_renderer_text_new();
789 
790 	gtk_tree_view_column_pack_start (column, renderer, TRUE);
791 
792 	gtk_tree_view_column_set_clickable (column, TRUE);
793 	gtk_tree_view_column_set_resizable (column, TRUE);
794 	gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
795 	{
796 		const char *sample_strings[3];
797 		sample_strings[0] = _("Date");
798 		sample_strings[1] = rb_entry_view_get_time_date_column_sample ();
799 		sample_strings[2] = NULL;
800 		rb_entry_view_set_fixed_column_width (episodes, column, renderer, sample_strings);
801 	}
802 
803 	gtk_tree_view_column_set_cell_data_func (column, renderer,
804 						 (GtkTreeCellDataFunc) podcast_post_date_cell_data_func,
805 						 dialog, NULL);
806 
807 	rb_entry_view_append_column_custom (episodes, column,
808 					    _("Date"), "Date",
809 					    (GCompareDataFunc) podcast_post_date_sort_func,
810 					    0, NULL);
811 	rb_entry_view_append_column (episodes, RB_ENTRY_VIEW_COL_TITLE, TRUE);
812 	rb_entry_view_append_column (episodes, RB_ENTRY_VIEW_COL_DURATION, TRUE);
813 	rb_entry_view_set_sorting_order (RB_ENTRY_VIEW (episodes), "Date", GTK_SORT_DESCENDING);
814 	g_signal_connect (episodes,
815 			  "notify::sort-order",
816 			  G_CALLBACK (episodes_sort_changed_cb),
817 			  dialog);
818 
819 	query = rhythmdb_query_parse (dialog->priv->db,
820 				      RHYTHMDB_QUERY_PROP_EQUALS,
821 				      RHYTHMDB_PROP_TYPE,
822 				      RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH,
823 				      RHYTHMDB_QUERY_END);
824 	query_model = rhythmdb_query_model_new_empty (dialog->priv->db);
825 	rb_entry_view_set_model (episodes, query_model);
826 
827 	rhythmdb_do_full_query_async_parsed (dialog->priv->db, RHYTHMDB_QUERY_RESULTS (query_model), query);
828 	rhythmdb_query_free (query);
829 
830 	g_object_unref (query_model);
831 
832 	paned = GTK_WIDGET (gtk_builder_get_object (builder, "paned"));
833 	g_signal_connect (paned, "size-allocate", G_CALLBACK (paned_size_allocate_cb), dialog);
834 	gtk_paned_pack2 (GTK_PANED (paned),
835 			 GTK_WIDGET (episodes),
836 			 TRUE,
837 			 FALSE);
838 
839 	gtk_widget_show_all (GTK_WIDGET (dialog));
840 	g_object_unref (builder);
841 }
842 
843 static void
impl_dispose(GObject * object)844 impl_dispose (GObject *object)
845 {
846 	RBPodcastAddDialog *dialog = RB_PODCAST_ADD_DIALOG (object);
847 
848 	if (dialog->priv->podcast_mgr != NULL) {
849 		g_object_unref (dialog->priv->podcast_mgr);
850 		dialog->priv->podcast_mgr = NULL;
851 	}
852 	if (dialog->priv->db != NULL) {
853 		g_object_unref (dialog->priv->db);
854 		dialog->priv->db = NULL;
855 	}
856 
857 	G_OBJECT_CLASS (rb_podcast_add_dialog_parent_class)->dispose (object);
858 }
859 
860 static void
impl_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)861 impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
862 {
863 	RBPodcastAddDialog *dialog = RB_PODCAST_ADD_DIALOG (object);
864 
865 	switch (prop_id) {
866 	case PROP_PODCAST_MANAGER:
867 		dialog->priv->podcast_mgr = g_value_dup_object (value);
868 		break;
869 	case PROP_SHELL:
870 		dialog->priv->shell = g_value_dup_object (value);
871 		break;
872 	default:
873 		g_assert_not_reached ();
874 		break;
875 	}
876 }
877 
878 static void
impl_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)879 impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
880 {
881 	RBPodcastAddDialog *dialog = RB_PODCAST_ADD_DIALOG (object);
882 
883 	switch (prop_id) {
884 	case PROP_PODCAST_MANAGER:
885 		g_value_set_object (value, dialog->priv->podcast_mgr);
886 		break;
887 	case PROP_SHELL:
888 		g_value_set_object (value, dialog->priv->shell);
889 		break;
890 	default:
891 		g_assert_not_reached ();
892 		break;
893 	}
894 }
895 
896 static void
rb_podcast_add_dialog_init(RBPodcastAddDialog * dialog)897 rb_podcast_add_dialog_init (RBPodcastAddDialog *dialog)
898 {
899 	dialog->priv = G_TYPE_INSTANCE_GET_PRIVATE (dialog,
900 						    RB_TYPE_PODCAST_ADD_DIALOG,
901 						    RBPodcastAddDialogPrivate);
902 }
903 
904 static void
rb_podcast_add_dialog_class_init(RBPodcastAddDialogClass * klass)905 rb_podcast_add_dialog_class_init (RBPodcastAddDialogClass *klass)
906 {
907 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
908 
909 	object_class->constructed = impl_constructed;
910 	object_class->dispose = impl_dispose;
911 	object_class->set_property = impl_set_property;
912 	object_class->get_property = impl_get_property;
913 
914 	klass->close = impl_close;
915 
916 	g_object_class_install_property (object_class,
917 					 PROP_PODCAST_MANAGER,
918 					 g_param_spec_object ("podcast-manager",
919 							      "podcast-manager",
920 							      "RBPodcastManager instance",
921 							      RB_TYPE_PODCAST_MANAGER,
922 							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
923 	g_object_class_install_property (object_class,
924 					 PROP_SHELL,
925 					 g_param_spec_object ("shell",
926 							      "shell",
927 							      "RBShell instance",
928 							      RB_TYPE_SHELL,
929 							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
930 
931 	signals[CLOSE] = g_signal_new ("close",
932 				       RB_TYPE_PODCAST_ADD_DIALOG,
933 				       G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
934 				       G_STRUCT_OFFSET (RBPodcastAddDialogClass, close),
935 				       NULL, NULL,
936 				       NULL,
937 				       G_TYPE_NONE,
938 				       0);
939 	signals[CLOSED] = g_signal_new ("closed",
940 					RB_TYPE_PODCAST_ADD_DIALOG,
941 					G_SIGNAL_RUN_LAST,
942 					G_STRUCT_OFFSET (RBPodcastAddDialogClass, closed),
943 					NULL, NULL,
944 					NULL,
945 					G_TYPE_NONE,
946 					0);
947 
948 	g_type_class_add_private (object_class, sizeof (RBPodcastAddDialogPrivate));
949 
950 	gtk_binding_entry_add_signal (gtk_binding_set_by_class (klass),
951 				      GDK_KEY_Escape,
952 				      0,
953 				      "close",
954 				      0);
955 }
956 
957 void
rb_podcast_add_dialog_reset(RBPodcastAddDialog * dialog,const char * text,gboolean load)958 rb_podcast_add_dialog_reset (RBPodcastAddDialog *dialog, const char *text, gboolean load)
959 {
960 	dialog->priv->reset_count++;
961 	remove_all_feeds (dialog);
962 	rhythmdb_entry_delete_by_type (dialog->priv->db, RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH);
963 	rhythmdb_commit (dialog->priv->db);
964 
965 	rb_search_entry_set_text (dialog->priv->search_entry, text);
966 
967 	if (load) {
968 		search_cb (dialog->priv->search_entry, text, dialog);
969 	} else {
970 		rb_search_entry_grab_focus (dialog->priv->search_entry);
971 	}
972 }
973 
974 GtkWidget *
rb_podcast_add_dialog_new(RBShell * shell,RBPodcastManager * podcast_mgr)975 rb_podcast_add_dialog_new (RBShell *shell, RBPodcastManager *podcast_mgr)
976 {
977 	return GTK_WIDGET (g_object_new (RB_TYPE_PODCAST_ADD_DIALOG,
978 					 "shell", shell,
979 					 "podcast-manager", podcast_mgr,
980 					 "orientation", GTK_ORIENTATION_VERTICAL,
981 					 NULL));
982 }
983 
984