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