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