No issues found
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2 *
3 * Copyright (C) 2005 Renato Araujo Oliveira Filho <renato.filho@indt.org.br>
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 /*
30 * Base source for podcast sources. This provides the feed
31 * and post views, the search actions, and so on.
32 */
33
34 #include "config.h"
35
36 #include <string.h>
37 #define __USE_XOPEN
38 #include <time.h>
39
40 #include <glib.h>
41 #include <glib/gi18n.h>
42 #include <gtk/gtk.h>
43 #include <libsoup/soup.h>
44
45 #include "rb-podcast-source.h"
46 #include "rb-podcast-settings.h"
47 #include "rb-podcast-entry-types.h"
48
49 #include "rhythmdb.h"
50 #include "rhythmdb-query-model.h"
51 #include "rb-shell-player.h"
52 #include "rb-stock-icons.h"
53 #include "rb-entry-view.h"
54 #include "rb-property-view.h"
55 #include "rb-util.h"
56 #include "rb-file-helpers.h"
57 #include "rb-dialog.h"
58 #include "rb-podcast-properties-dialog.h"
59 #include "rb-feed-podcast-properties-dialog.h"
60 #include "rb-playlist-manager.h"
61 #include "rb-debug.h"
62 #include "rb-podcast-manager.h"
63 #include "rb-static-playlist-source.h"
64 #include "rb-cut-and-paste-code.h"
65 #include "rb-source-search-basic.h"
66 #include "rb-cell-renderer-pixbuf.h"
67 #include "rb-podcast-add-dialog.h"
68 #include "rb-source-toolbar.h"
69
70 static void podcast_cmd_new_podcast (GtkAction *action,
71 RBPodcastSource *source);
72 static void podcast_cmd_download_post (GtkAction *action,
73 RBPodcastSource *source);
74 static void podcast_cmd_cancel_download (GtkAction *action,
75 RBPodcastSource *source);
76 static void podcast_cmd_delete_feed (GtkAction *action,
77 RBPodcastSource *source);
78 static void podcast_cmd_update_feed (GtkAction *action,
79 RBPodcastSource *source);
80 static void podcast_cmd_update_all (GtkAction *action,
81 RBPodcastSource *source);
82 static void podcast_cmd_properties_feed (GtkAction *action,
83 RBPodcastSource *source);
84
85 struct _RBPodcastSourcePrivate
86 {
87 RhythmDB *db;
88
89 guint prefs_notify_id;
90
91 GtkWidget *grid;
92 GtkWidget *paned;
93 GtkWidget *add_dialog;
94 GtkAction *add_action;
95 RBSourceToolbar *toolbar;
96
97 RhythmDBPropertyModel *feed_model;
98 RBPropertyView *feeds;
99 RBEntryView *posts;
100 GtkActionGroup *action_group;
101
102 GList *selected_feeds;
103 RhythmDBQuery *base_query;
104 RhythmDBQuery *search_query;
105 RBSourceSearch *default_search;
106
107 RBPodcastManager *podcast_mgr;
108
109 GdkPixbuf *error_pixbuf;
110 };
111
112
113 static GtkActionEntry rb_podcast_source_actions [] =
114 {
115 { "MusicNewPodcast", RB_STOCK_PODCAST_NEW, N_("_New Podcast Feed..."), NULL,
116 N_("Subscribe to a new podcast feed"),
117 G_CALLBACK (podcast_cmd_new_podcast) },
118 { "PodcastSrcDownloadPost", NULL, N_("Download _Episode"), NULL,
119 N_("Download Podcast Episode"),
120 G_CALLBACK (podcast_cmd_download_post) },
121 { "PodcastSrcCancelDownload", GTK_STOCK_CANCEL, N_("_Cancel Download"), NULL,
122 N_("Cancel Episode Download"),
123 G_CALLBACK (podcast_cmd_cancel_download) },
124 { "PodcastFeedProperties", GTK_STOCK_PROPERTIES, N_("_Properties"), NULL,
125 N_("Episode Properties"),
126 G_CALLBACK (podcast_cmd_properties_feed) },
127 { "PodcastFeedUpdate", GTK_STOCK_REFRESH, N_("_Update Podcast Feed"), NULL,
128 N_("Update Feed"),
129 G_CALLBACK (podcast_cmd_update_feed) },
130 { "PodcastFeedDelete", GTK_STOCK_DELETE, N_("_Delete Podcast Feed"), NULL,
131 N_("Delete Feed"),
132 G_CALLBACK (podcast_cmd_delete_feed) },
133 { "PodcastUpdateAllFeeds", GTK_STOCK_REFRESH, N_("_Update All Feeds"), NULL,
134 N_("Update all feeds"),
135 G_CALLBACK (podcast_cmd_update_all) },
136 };
137
138 static GtkRadioActionEntry rb_podcast_source_radio_actions [] =
139 {
140 { "PodcastSearchAll", NULL, N_("Search all fields"), NULL, NULL, RHYTHMDB_PROP_SEARCH_MATCH },
141 { "PodcastSearchFeeds", NULL, N_("Search podcast feeds"), NULL, NULL, RHYTHMDB_PROP_ALBUM_FOLDED },
142 { "PodcastSearchEpisodes", NULL, N_("Search podcast episodes"), NULL, NULL, RHYTHMDB_PROP_TITLE_FOLDED }
143 };
144
145 static const GtkTargetEntry posts_view_drag_types[] = {
146 { "text/uri-list", 0, 0 },
147 { "_NETSCAPE_URL", 0, 1 },
148 { "application/rss+xml", 0, 2 },
149 };
150
151 enum
152 {
153 PROP_0,
154 PROP_PODCAST_MANAGER,
155 PROP_BASE_QUERY,
156 PROP_SHOW_BROWSER
157 };
158
159 G_DEFINE_TYPE (RBPodcastSource, rb_podcast_source, RB_TYPE_SOURCE)
160
161 static void
162 podcast_posts_view_sort_order_changed_cb (GObject *object,
163 GParamSpec *pspec,
164 RBPodcastSource *source)
165 {
166 rb_debug ("sort order changed");
167 rb_entry_view_resort_model (RB_ENTRY_VIEW (object));
168 }
169
170 static void
171 podcast_posts_show_popup_cb (RBEntryView *view,
172 gboolean over_entry,
173 RBPodcastSource *source)
174 {
175 if (G_OBJECT (source) == NULL) {
176 return;
177 } else if (!over_entry) {
178 _rb_display_page_show_popup (RB_DISPLAY_PAGE (source), "/PodcastSourcePopup");
179 } else {
180 GtkAction* action;
181 GList *lst;
182 gboolean downloadable = FALSE;
183 gboolean cancellable = FALSE;
184
185 lst = rb_entry_view_get_selected_entries (view);
186
187 while (lst) {
188 RhythmDBEntry *entry = (RhythmDBEntry*) lst->data;
189 gulong status = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS);
190
191 if (rb_podcast_manager_entry_in_download_queue (source->priv->podcast_mgr, entry)) {
192 cancellable = TRUE;
193 } else if (status != RHYTHMDB_PODCAST_STATUS_COMPLETE) {
194 downloadable = TRUE;
195 }
196
197 lst = lst->next;
198 }
199
200 g_list_foreach (lst, (GFunc)rhythmdb_entry_unref, NULL);
201 g_list_free (lst);
202
203 action = gtk_action_group_get_action (source->priv->action_group, "PodcastSrcDownloadPost");
204 gtk_action_set_sensitive (action, downloadable);
205
206 action = gtk_action_group_get_action (source->priv->action_group, "PodcastSrcCancelDownload");
207 gtk_action_set_sensitive (action, cancellable);
208
209 _rb_display_page_show_popup (RB_DISPLAY_PAGE (source), "/PodcastViewPopup");
210 }
211 }
212
213 static void
214 podcast_feeds_show_popup_cb (RBPropertyView *view,
215 RBPodcastSource *source)
216 {
217 if (G_OBJECT (source) == NULL) {
218 return;
219 } else {
220 GtkAction *act_update;
221 GtkAction *act_properties;
222 GtkAction *act_delete;
223 GList *lst;
224
225 lst = source->priv->selected_feeds;
226
227 act_update = gtk_action_group_get_action (source->priv->action_group, "PodcastFeedUpdate");
228 act_properties = gtk_action_group_get_action (source->priv->action_group, "PodcastFeedProperties");
229 act_delete = gtk_action_group_get_action (source->priv->action_group, "PodcastFeedDelete");
230
231 if (lst) {
232 gtk_action_set_visible (act_update, TRUE);
233 gtk_action_set_visible (act_properties, TRUE);
234 gtk_action_set_visible (act_delete, TRUE);
235 } else {
236 gtk_action_set_visible (act_update, FALSE);
237 gtk_action_set_visible (act_properties, FALSE);
238 gtk_action_set_visible (act_delete, FALSE);
239 }
240
241 _rb_display_page_show_popup (RB_DISPLAY_PAGE (source), "/PodcastFeedViewPopup");
242 }
243 }
244
245 static GPtrArray *
246 construct_query_from_selection (RBPodcastSource *source)
247 {
248 GPtrArray *query;
249 query = rhythmdb_query_copy (source->priv->base_query);
250
251 if (source->priv->search_query) {
252 rhythmdb_query_append (source->priv->db,
253 query,
254 RHYTHMDB_QUERY_SUBQUERY,
255 source->priv->search_query,
256 RHYTHMDB_QUERY_END);
257 }
258
259 if (source->priv->selected_feeds) {
260 GPtrArray *subquery = g_ptr_array_new ();
261 GList *l;
262
263 for (l = source->priv->selected_feeds; l != NULL; l = g_list_next (l)) {
264 const char *location;
265
266 location = (char *) l->data;
267 rb_debug ("subquery SUBTITLE equals %s", location);
268
269 rhythmdb_query_append (source->priv->db,
270 subquery,
271 RHYTHMDB_QUERY_PROP_EQUALS,
272 RHYTHMDB_PROP_SUBTITLE,
273 location,
274 RHYTHMDB_QUERY_END);
275 if (g_list_next (l))
276 rhythmdb_query_append (source->priv->db, subquery,
277 RHYTHMDB_QUERY_DISJUNCTION,
278 RHYTHMDB_QUERY_END);
279 }
280
281 rhythmdb_query_append (source->priv->db, query,
282 RHYTHMDB_QUERY_SUBQUERY, subquery,
283 RHYTHMDB_QUERY_END);
284
285 rhythmdb_query_free (subquery);
286 }
287
288 return query;
289 }
290
291 static void
292 rb_podcast_source_do_query (RBPodcastSource *source)
293 {
294 RhythmDBQueryModel *query_model;
295 GPtrArray *query;
296
297 /* set up new query model */
298 query_model = rhythmdb_query_model_new_empty (source->priv->db);
299
300 rb_entry_view_set_model (source->priv->posts, query_model);
301 g_object_set (source, "query-model", query_model, NULL);
302
303 /* build and run the query */
304 query = construct_query_from_selection (source);
305 rhythmdb_do_full_query_async_parsed (source->priv->db,
306 RHYTHMDB_QUERY_RESULTS (query_model),
307 query);
308
309 rhythmdb_query_free (query);
310
311 g_object_unref (query_model);
312 }
313
314 static void
315 feed_select_change_cb (RBPropertyView *propview,
316 GList *feeds,
317 RBPodcastSource *source)
318 {
319 if (rb_string_list_equal (feeds, source->priv->selected_feeds))
320 return;
321
322 if (source->priv->selected_feeds) {
323 g_list_foreach (source->priv->selected_feeds, (GFunc) g_free, NULL);
324 g_list_free (source->priv->selected_feeds);
325 }
326
327 source->priv->selected_feeds = rb_string_list_copy (feeds);
328
329 rb_podcast_source_do_query (source);
330 rb_source_notify_filter_changed (RB_SOURCE (source));
331 }
332
333
334 static void
335 posts_view_drag_data_received_cb (GtkWidget *widget,
336 GdkDragContext *dc,
337 gint x,
338 gint y,
339 GtkSelectionData *selection_data,
340 guint info,
341 guint time,
342 RBPodcastSource *source)
343 {
344 rb_display_page_receive_drag (RB_DISPLAY_PAGE (source), selection_data);
345 }
346
347 static void
348 podcast_add_dialog_closed_cb (RBPodcastAddDialog *dialog, RBPodcastSource *source)
349 {
350 rb_podcast_source_do_query (source);
351 gtk_widget_set_margin_top (GTK_WIDGET (source->priv->grid), 6);
352 gtk_widget_hide (source->priv->add_dialog);
353 gtk_widget_show (GTK_WIDGET (source->priv->toolbar));
354 gtk_widget_show (source->priv->paned);
355 }
356
357 static void
358 yank_clipboard_url (GtkClipboard *clipboard, const char *text, RBPodcastSource *source)
359 {
360 SoupURI *uri;
361
362 if (text == NULL) {
363 return;
364 }
365
366 uri = soup_uri_new (text);
367 if (SOUP_URI_VALID_FOR_HTTP (uri)) {
368 rb_podcast_add_dialog_reset (RB_PODCAST_ADD_DIALOG (source->priv->add_dialog), text, FALSE);
369 }
370
371 if (uri != NULL) {
372 soup_uri_free (uri);
373 }
374 }
375
376 static void
377 podcast_cmd_new_podcast (GtkAction *action, RBPodcastSource *source)
378 {
379 RhythmDBQueryModel *query_model;
380
381 rb_podcast_add_dialog_reset (RB_PODCAST_ADD_DIALOG (source->priv->add_dialog), NULL, FALSE);
382
383 /* if we can get a url from the clipboard, populate the dialog with that,
384 * since there's a good chance that's what the user wants to do anyway.
385 */
386 gtk_clipboard_request_text (gtk_clipboard_get (GDK_SELECTION_CLIPBOARD),
387 (GtkClipboardTextReceivedFunc) yank_clipboard_url,
388 source);
389 gtk_clipboard_request_text (gtk_clipboard_get (GDK_SELECTION_PRIMARY),
390 (GtkClipboardTextReceivedFunc) yank_clipboard_url,
391 source);
392
393 query_model = rhythmdb_query_model_new_empty (source->priv->db);
394 rb_entry_view_set_model (source->priv->posts, query_model);
395 g_object_set (source, "query-model", query_model, NULL);
396 g_object_unref (query_model);
397
398 gtk_widget_set_margin_top (GTK_WIDGET (source->priv->grid), 0);
399 gtk_widget_hide (source->priv->paned);
400 gtk_widget_hide (GTK_WIDGET (source->priv->toolbar));
401 gtk_widget_show (source->priv->add_dialog);
402 }
403
404 void
405 rb_podcast_source_add_feed (RBPodcastSource *source, const char *text)
406 {
407 gtk_action_activate (source->priv->add_action);
408
409 rb_podcast_add_dialog_reset (RB_PODCAST_ADD_DIALOG (source->priv->add_dialog), text, TRUE);
410 }
411
412 static void
413 podcast_cmd_download_post (GtkAction *action, RBPodcastSource *source)
414 {
415 GList *lst;
416 GValue val = {0, };
417 RBEntryView *posts;
418
419 rb_debug ("Add to download action");
420 posts = source->priv->posts;
421
422 lst = rb_entry_view_get_selected_entries (posts);
423 g_value_init (&val, G_TYPE_ULONG);
424
425 while (lst != NULL) {
426 RhythmDBEntry *entry = (RhythmDBEntry *) lst->data;
427 gulong status = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS);
428
429 if (status == RHYTHMDB_PODCAST_STATUS_PAUSED ||
430 status == RHYTHMDB_PODCAST_STATUS_ERROR) {
431 g_value_set_ulong (&val, RHYTHMDB_PODCAST_STATUS_WAITING);
432 rhythmdb_entry_set (source->priv->db, entry, RHYTHMDB_PROP_STATUS, &val);
433 rb_podcast_manager_download_entry (source->priv->podcast_mgr, entry);
434 }
435
436 lst = lst->next;
437 }
438 g_value_unset (&val);
439 rhythmdb_commit (source->priv->db);
440
441 g_list_foreach (lst, (GFunc)rhythmdb_entry_unref, NULL);
442 g_list_free (lst);
443 }
444
445 static void
446 podcast_cmd_cancel_download (GtkAction *action, RBPodcastSource *source)
447 {
448 GList *lst;
449 GValue val = {0, };
450 RBEntryView *posts;
451
452 posts = source->priv->posts;
453
454 lst = rb_entry_view_get_selected_entries (posts);
455 g_value_init (&val, G_TYPE_ULONG);
456
457 while (lst != NULL) {
458 RhythmDBEntry *entry = (RhythmDBEntry *) lst->data;
459 gulong status = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS);
460
461 if ((status > 0 && status < RHYTHMDB_PODCAST_STATUS_COMPLETE) ||
462 status == RHYTHMDB_PODCAST_STATUS_WAITING) {
463 g_value_set_ulong (&val, RHYTHMDB_PODCAST_STATUS_PAUSED);
464 rhythmdb_entry_set (source->priv->db, entry, RHYTHMDB_PROP_STATUS, &val);
465 rb_podcast_manager_cancel_download (source->priv->podcast_mgr, entry);
466 }
467
468 lst = lst->next;
469 }
470
471 g_value_unset (&val);
472 rhythmdb_commit (source->priv->db);
473
474 g_list_foreach (lst, (GFunc)rhythmdb_entry_unref, NULL);
475 g_list_free (lst);
476 }
477
478 static void
479 podcast_remove_response_cb (GtkDialog *dialog, int response, RBPodcastSource *source)
480 {
481 GList *feeds, *l;
482
483 gtk_widget_destroy (GTK_WIDGET (dialog));
484
485 if (response == GTK_RESPONSE_CANCEL || response == GTK_RESPONSE_DELETE_EVENT) {
486 return;
487 }
488
489 feeds = rb_string_list_copy (source->priv->selected_feeds);
490 for (l = feeds; l != NULL; l = g_list_next (l)) {
491 const char *location = l->data;
492
493 rb_debug ("Removing podcast location: %s", location);
494 rb_podcast_manager_remove_feed (source->priv->podcast_mgr,
495 location,
496 (response == GTK_RESPONSE_YES));
497 }
498
499 rb_list_deep_free (feeds);
500 }
501
502 static void
503 podcast_cmd_delete_feed (GtkAction *action, RBPodcastSource *source)
504 {
505 GtkWidget *dialog;
506 GtkWidget *button;
507 GtkWindow *window;
508 RBShell *shell;
509
510 rb_debug ("Delete feed action");
511
512 g_object_get (source, "shell", &shell, NULL);
513 g_object_get (shell, "window", &window, NULL);
514 g_object_unref (shell);
515
516 dialog = gtk_message_dialog_new (window,
517 GTK_DIALOG_DESTROY_WITH_PARENT,
518 GTK_MESSAGE_WARNING,
519 GTK_BUTTONS_NONE,
520 _("Delete the podcast feed and downloaded files?"));
521
522 gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
523 _("If you choose to delete the feed and files, "
524 "they will be permanently lost. Please note that "
525 "you can delete the feed but keep the downloaded "
526 "files by choosing to delete the feed only."));
527
528 gtk_window_set_title (GTK_WINDOW (dialog), "");
529
530 gtk_dialog_add_buttons (GTK_DIALOG (dialog),
531 _("Delete _Feed Only"),
532 GTK_RESPONSE_NO,
533 GTK_STOCK_CANCEL,
534 GTK_RESPONSE_CANCEL,
535 NULL);
536
537 button = gtk_dialog_add_button (GTK_DIALOG (dialog),
538 _("_Delete Feed And Files"),
539 GTK_RESPONSE_YES);
540
541 gtk_window_set_focus (GTK_WINDOW (dialog), button);
542 gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_YES);
543
544 gtk_widget_show_all (dialog);
545 g_signal_connect (dialog, "response", G_CALLBACK (podcast_remove_response_cb), source);
546 }
547
548 static void
549 podcast_cmd_properties_feed (GtkAction *action, RBPodcastSource *source)
550 {
551 RhythmDBEntry *entry;
552 GtkWidget *dialog;
553 const char *location;
554
555 location = (char *) source->priv->selected_feeds->data;
556
557 entry = rhythmdb_entry_lookup_by_location (source->priv->db,
558 location);
559
560 if (entry != NULL) {
561 dialog = rb_feed_podcast_properties_dialog_new (entry);
562 rb_debug ("in feed properties");
563 if (dialog)
564 gtk_widget_show_all (dialog);
565 else
566 rb_debug ("no selection!");
567 }
568 }
569
570 static void
571 podcast_cmd_update_feed (GtkAction *action, RBPodcastSource *source)
572 {
573 GList *feeds, *l;
574
575 rb_debug ("Update action");
576
577 feeds = rb_string_list_copy (source->priv->selected_feeds);
578 if (feeds == NULL) {
579 rb_podcast_manager_update_feeds (source->priv->podcast_mgr);
580 return;
581 }
582
583 for (l = feeds; l != NULL; l = g_list_next (l)) {
584 const char *location = l->data;
585
586 rb_podcast_manager_subscribe_feed (source->priv->podcast_mgr,
587 location,
588 FALSE);
589 }
590
591 rb_list_deep_free (feeds);
592 }
593
594 static void
595 podcast_cmd_update_all (GtkAction *action, RBPodcastSource *source)
596 {
597 rb_podcast_manager_update_feeds (source->priv->podcast_mgr);
598 }
599
600 static void
601 podcast_post_status_cell_data_func (GtkTreeViewColumn *column,
602 GtkCellRenderer *renderer,
603 GtkTreeModel *tree_model,
604 GtkTreeIter *iter,
605 RBPodcastSource *source)
606
607 {
608 RhythmDBEntry *entry;
609 guint value;
610 char *s;
611
612 gtk_tree_model_get (tree_model, iter, 0, &entry, -1);
613
614 switch (rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS)) {
615 case RHYTHMDB_PODCAST_STATUS_COMPLETE:
616 g_object_set (renderer, "text", _("Downloaded"), NULL);
617 value = 100;
618 break;
619 case RHYTHMDB_PODCAST_STATUS_ERROR:
620 g_object_set (renderer, "text", _("Failed"), NULL);
621 value = 0;
622 break;
623 case RHYTHMDB_PODCAST_STATUS_WAITING:
624 g_object_set (renderer, "text", _("Waiting"), NULL);
625 value = 0;
626 break;
627 case RHYTHMDB_PODCAST_STATUS_PAUSED:
628 g_object_set (renderer, "text", "", NULL);
629 value = 0;
630 break;
631 default:
632 value = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS);
633 s = g_strdup_printf ("%u %%", value);
634
635 g_object_set (renderer, "text", s, NULL);
636 g_free (s);
637 break;
638 }
639
640 g_object_set (renderer, "visible",
641 rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS) != RHYTHMDB_PODCAST_STATUS_PAUSED,
642 "value", value,
643 NULL);
644
645 rhythmdb_entry_unref (entry);
646 }
647
648 static void
649 podcast_post_feed_cell_data_func (GtkTreeViewColumn *column,
650 GtkCellRenderer *renderer,
651 GtkTreeModel *tree_model,
652 GtkTreeIter *iter,
653 RBPodcastSource *source)
654
655 {
656 RhythmDBEntry *entry;
657 const gchar *album;
658
659 gtk_tree_model_get (tree_model, iter, 0, &entry, -1);
660 album = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM);
661
662 g_object_set (renderer, "text", album, NULL);
663
664 rhythmdb_entry_unref (entry);
665 }
666
667 static gboolean
668 podcast_feed_title_search_func (GtkTreeModel *model,
669 gint column,
670 const gchar *key,
671 GtkTreeIter *iter,
672 RBPodcastSource *source)
673 {
674 char *title;
675 char *fold_key;
676 RhythmDBEntry *entry = NULL;
677 gboolean ret;
678
679 fold_key = rb_search_fold (key);
680 gtk_tree_model_get (model, iter,
681 RHYTHMDB_PROPERTY_MODEL_COLUMN_TITLE, &title,
682 -1);
683
684 entry = rhythmdb_entry_lookup_by_location (source->priv->db, title);
685 if (entry != NULL) {
686 g_free (title);
687 title = rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_TITLE_FOLDED);
688 }
689
690 ret = g_str_has_prefix (title, fold_key);
691
692 g_free (fold_key);
693 g_free (title);
694
695 return !ret;
696 }
697
698 static void
699 podcast_feed_title_cell_data_func (GtkTreeViewColumn *column,
700 GtkCellRenderer *renderer,
701 GtkTreeModel *tree_model,
702 GtkTreeIter *iter,
703 RBPodcastSource *source)
704 {
705 char *title;
706 char *str;
707 gboolean is_all;
708 guint number;
709 RhythmDBEntry *entry = NULL;
710
711 str = NULL;
712 gtk_tree_model_get (tree_model, iter,
713 RHYTHMDB_PROPERTY_MODEL_COLUMN_TITLE, &title,
714 RHYTHMDB_PROPERTY_MODEL_COLUMN_PRIORITY, &is_all,
715 RHYTHMDB_PROPERTY_MODEL_COLUMN_NUMBER, &number, -1);
716
717 entry = rhythmdb_entry_lookup_by_location (source->priv->db, title);
718 if (entry != NULL) {
719 g_free (title);
720 title = g_strdup (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE));
721 }
722
723 if (is_all) {
724 int nodes;
725 const char *fmt;
726
727 nodes = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (tree_model), NULL);
728 /* Subtract one for the All node */
729 nodes--;
730
731 fmt = ngettext ("%d feed", "All %d feeds", nodes);
732
733 str = g_strdup_printf (fmt, nodes, number);
734 } else {
735 str = g_strdup_printf ("%s", title);
736 }
737
738 g_object_set (G_OBJECT (renderer), "text", str,
739 "weight", G_UNLIKELY (is_all) ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
740 NULL);
741
742 g_free (str);
743 g_free (title);
744 }
745
746 static void
747 podcast_feed_error_cell_data_func (GtkTreeViewColumn *column,
748 GtkCellRenderer *renderer,
749 GtkTreeModel *tree_model,
750 GtkTreeIter *iter,
751 RBPodcastSource *source)
752 {
753 char *title;
754 RhythmDBEntry *entry = NULL;
755 GdkPixbuf *pixbuf = NULL;
756
757 gtk_tree_model_get (tree_model, iter,
758 RHYTHMDB_PROPERTY_MODEL_COLUMN_TITLE, &title,
759 -1);
760
761 entry = rhythmdb_entry_lookup_by_location (source->priv->db, title);
762 g_free (title);
763
764 if (entry != NULL) {
765 if (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_PLAYBACK_ERROR)) {
766 pixbuf = source->priv->error_pixbuf;
767 }
768 }
769 g_object_set (renderer, "pixbuf", pixbuf, NULL);
770 }
771
772 static void
773 podcast_post_date_cell_data_func (GtkTreeViewColumn *column,
774 GtkCellRenderer *renderer,
775 GtkTreeModel *tree_model,
776 GtkTreeIter *iter,
777 RBPodcastSource *source)
778 {
779 RhythmDBEntry *entry;
780 gulong value;
781 char *str;
782
783 gtk_tree_model_get (tree_model, iter, 0, &entry, -1);
784
785 value = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_POST_TIME);
786 if (value == 0) {
787 str = g_strdup (_("Unknown"));
788 } else {
789 str = rb_utf_friendly_time (value);
790 }
791
792 g_object_set (G_OBJECT (renderer), "text", str, NULL);
793 g_free (str);
794
795 rhythmdb_entry_unref (entry);
796 }
797
798
799 static gint
800 podcast_post_feed_sort_func (RhythmDBEntry *a,
801 RhythmDBEntry *b,
802 RhythmDBQueryModel *model)
803 {
804 const char *a_str, *b_str;
805 gulong a_val, b_val;
806 gint ret;
807
808 /* feeds */
809 a_str = rhythmdb_entry_get_string (a, RHYTHMDB_PROP_ALBUM_SORT_KEY);
810 b_str = rhythmdb_entry_get_string (b, RHYTHMDB_PROP_ALBUM_SORT_KEY);
811
812 ret = strcmp (a_str, b_str);
813 if (ret != 0)
814 return ret;
815
816 a_val = rhythmdb_entry_get_ulong (a, RHYTHMDB_PROP_POST_TIME);
817 b_val = rhythmdb_entry_get_ulong (b, RHYTHMDB_PROP_POST_TIME);
818
819 if (a_val != b_val)
820 return (a_val > b_val) ? 1 : -1;
821
822 /* titles */
823 a_str = rhythmdb_entry_get_string (a, RHYTHMDB_PROP_TITLE_SORT_KEY);
824 b_str = rhythmdb_entry_get_string (b, RHYTHMDB_PROP_TITLE_SORT_KEY);
825
826 ret = strcmp (a_str, b_str);
827 if (ret != 0)
828 return ret;
829
830 /* location */
831 a_str = rhythmdb_entry_get_string (a, RHYTHMDB_PROP_LOCATION);
832 b_str = rhythmdb_entry_get_string (b, RHYTHMDB_PROP_LOCATION);
833
834 ret = strcmp (a_str, b_str);
835 return ret;
836 }
837
838 static gint
839 podcast_post_date_sort_func (RhythmDBEntry *a,
840 RhythmDBEntry *b,
841 RhythmDBQueryModel *model)
842 {
843 gulong a_val, b_val;
844 gint ret;
845
846 a_val = rhythmdb_entry_get_ulong (a, RHYTHMDB_PROP_POST_TIME);
847 b_val = rhythmdb_entry_get_ulong (b, RHYTHMDB_PROP_POST_TIME);
848
849 if (a_val != b_val)
850 ret = (a_val > b_val) ? 1 : -1;
851 else
852 ret = podcast_post_feed_sort_func (a, b, model);
853
854 return ret;
855 }
856
857 static gint
858 podcast_post_status_sort_func (RhythmDBEntry *a,
859 RhythmDBEntry *b,
860 RhythmDBQueryModel *model)
861 {
862 gulong a_val, b_val;
863 gint ret;
864
865 a_val = rhythmdb_entry_get_ulong (a, RHYTHMDB_PROP_STATUS);
866 b_val = rhythmdb_entry_get_ulong (b, RHYTHMDB_PROP_STATUS);
867
868 if (a_val != b_val)
869 ret = (a_val > b_val) ? 1 : -1;
870 else
871 ret = podcast_post_feed_sort_func (a, b, model);
872
873 return ret;
874 }
875
876
877 static void
878 episode_activated_cb (RBEntryView *view,
879 RhythmDBEntry *entry,
880 RBPodcastSource *source)
881 {
882 GValue val = {0,};
883
884 /* check to see if it has already been downloaded */
885 if (rb_podcast_manager_entry_downloaded (entry))
886 return;
887
888 g_value_init (&val, G_TYPE_ULONG);
889 g_value_set_ulong (&val, RHYTHMDB_PODCAST_STATUS_WAITING);
890 rhythmdb_entry_set (source->priv->db, entry, RHYTHMDB_PROP_STATUS, &val);
891 rhythmdb_commit (source->priv->db);
892 g_value_unset (&val);
893
894 rb_podcast_manager_download_entry (source->priv->podcast_mgr, entry);
895 }
896
897 static void
898 podcast_entry_changed_cb (RhythmDB *db,
899 RhythmDBEntry *entry,
900 GArray *changes,
901 RBPodcastSource *source)
902 {
903 RhythmDBEntryType *entry_type;
904 gboolean feed_changed;
905 int i;
906
907 entry_type = rhythmdb_entry_get_entry_type (entry);
908 if (entry_type != RHYTHMDB_ENTRY_TYPE_PODCAST_FEED)
909 return;
910
911 feed_changed = FALSE;
912 for (i = 0; i < changes->len; i++) {
913 GValue *v = &g_array_index (changes, GValue, i);
914 RhythmDBEntryChange *change = g_value_get_boxed (v);
915
916 if (change->prop == RHYTHMDB_PROP_PLAYBACK_ERROR) {
917 feed_changed = TRUE;
918 break;
919 }
920 }
921
922 if (feed_changed) {
923 const char *loc;
924 GtkTreeIter iter;
925
926 loc = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
927 if (rhythmdb_property_model_iter_from_string (source->priv->feed_model,
928 loc,
929 &iter)) {
930 GtkTreePath *path;
931
932 path = gtk_tree_model_get_path (GTK_TREE_MODEL (source->priv->feed_model),
933 &iter);
934 gtk_tree_model_row_changed (GTK_TREE_MODEL (source->priv->feed_model),
935 path,
936 &iter);
937 gtk_tree_path_free (path);
938 }
939 }
940 }
941
942 static void
943 podcast_error_pixbuf_clicked_cb (RBCellRendererPixbuf *renderer,
944 const char *path_string,
945 RBPodcastSource *source)
946 {
947 GtkTreePath *path;
948 GtkTreeIter iter;
949
950 g_return_if_fail (path_string != NULL);
951
952 path = gtk_tree_path_new_from_string (path_string);
953 if (gtk_tree_model_get_iter (GTK_TREE_MODEL (source->priv->feed_model), &iter, path)) {
954 RhythmDBEntry *entry;
955 char *feed_url;
956
957 gtk_tree_model_get (GTK_TREE_MODEL (source->priv->feed_model),
958 &iter,
959 RHYTHMDB_PROPERTY_MODEL_COLUMN_TITLE, &feed_url,
960 -1);
961
962 entry = rhythmdb_entry_lookup_by_location (source->priv->db, feed_url);
963 if (entry != NULL) {
964 const gchar *error;
965
966 error = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_PLAYBACK_ERROR);
967 if (error) {
968 rb_error_dialog (NULL, _("Podcast Error"), "%s", error);
969 }
970 }
971
972 g_free (feed_url);
973 }
974
975 gtk_tree_path_free (path);
976 }
977
978 static void
979 settings_changed_cb (GSettings *settings, const char *key, RBPodcastSource *source)
980 {
981 if (g_strcmp0 (key, PODCAST_PANED_POSITION) == 0) {
982 gtk_paned_set_position (GTK_PANED (source->priv->paned),
983 g_settings_get_int (settings, key));
984 }
985 }
986
987 RBSource *
988 rb_podcast_source_new (RBShell *shell,
989 RBPodcastManager *podcast_manager,
990 RhythmDBQuery *base_query,
991 const char *name,
992 const char *icon_name)
993 {
994 RBSource *source;
995 GSettings *settings;
996
997 settings = g_settings_new (PODCAST_SETTINGS_SCHEMA);
998
999 source = RB_SOURCE (g_object_new (RB_TYPE_PODCAST_SOURCE,
1000 "name", name,
1001 "shell", shell,
1002 "entry-type", RHYTHMDB_ENTRY_TYPE_PODCAST_POST,
1003 "podcast-manager", podcast_manager,
1004 "base-query", base_query,
1005 "settings", g_settings_get_child (settings, "source"),
1006 "toolbar-path", "/PodcastSourceToolBar",
1007 NULL));
1008 g_object_unref (settings);
1009
1010 if (icon_name != NULL) {
1011 GdkPixbuf *pixbuf;
1012 gint size;
1013
1014 gtk_icon_size_lookup (RB_SOURCE_ICON_SIZE, &size, NULL);
1015 pixbuf = gtk_icon_theme_load_icon (gtk_icon_theme_get_default (),
1016 icon_name,
1017 size,
1018 0, NULL);
1019
1020 if (pixbuf != NULL) {
1021 g_object_set (source, "pixbuf", pixbuf, NULL);
1022 g_object_unref (pixbuf);
1023 }
1024 }
1025
1026 return source;
1027 }
1028
1029 static void
1030 impl_add_to_queue (RBSource *source, RBSource *queue)
1031 {
1032 RBEntryView *songs;
1033 GList *selection;
1034 GList *iter;
1035
1036 songs = rb_source_get_entry_view (source);
1037 selection = rb_entry_view_get_selected_entries (songs);
1038
1039 if (selection == NULL)
1040 return;
1041
1042 for (iter = selection; iter; iter = iter->next) {
1043 RhythmDBEntry *entry = (RhythmDBEntry *)iter->data;
1044 if (!rb_podcast_manager_entry_downloaded (entry))
1045 continue;
1046 rb_static_playlist_source_add_entry (RB_STATIC_PLAYLIST_SOURCE (queue),
1047 entry, -1);
1048 }
1049
1050 g_list_foreach (selection, (GFunc)rhythmdb_entry_unref, NULL);
1051 g_list_free (selection);
1052 }
1053
1054 static gboolean
1055 impl_can_add_to_queue (RBSource *source)
1056 {
1057 RBEntryView *songs;
1058 GList *selection;
1059 GList *iter;
1060 gboolean ok = FALSE;
1061
1062 songs = rb_source_get_entry_view (source);
1063 selection = rb_entry_view_get_selected_entries (songs);
1064
1065 if (selection == NULL)
1066 return FALSE;
1067
1068 /* If at least one entry has been downloaded, enable add to queue.
1069 * We'll filter out those that haven't when adding to the queue.
1070 */
1071 for (iter = selection; iter && !ok; iter = iter->next) {
1072 RhythmDBEntry *entry = (RhythmDBEntry *)iter->data;
1073 ok |= rb_podcast_manager_entry_downloaded (entry);
1074 }
1075
1076 g_list_foreach (selection, (GFunc)rhythmdb_entry_unref, NULL);
1077 g_list_free (selection);
1078
1079 return ok;
1080 }
1081
1082 static void
1083 delete_response_cb (GtkDialog *dialog, int response, RBPodcastSource *source)
1084 {
1085 GList *entries;
1086 GList *l;
1087
1088 gtk_widget_destroy (GTK_WIDGET (dialog));
1089
1090 if (response == GTK_RESPONSE_CANCEL || response == GTK_RESPONSE_DELETE_EVENT) {
1091 return;
1092 }
1093
1094 entries = rb_entry_view_get_selected_entries (source->priv->posts);
1095 for (l = entries; l != NULL; l = g_list_next (l)) {
1096 RhythmDBEntry *entry = l->data;
1097
1098 rb_podcast_manager_cancel_download (source->priv->podcast_mgr, entry);
1099 if (response == GTK_RESPONSE_YES) {
1100 rb_podcast_manager_delete_download (source->priv->podcast_mgr, entry);
1101 }
1102
1103 /* set podcast entries to invisible instead of deleted so they will
1104 * not reappear after the podcast has been updated
1105 */
1106 GValue v = {0,};
1107 g_value_init (&v, G_TYPE_BOOLEAN);
1108 g_value_set_boolean (&v, TRUE);
1109 rhythmdb_entry_set (source->priv->db, entry, RHYTHMDB_PROP_HIDDEN, &v);
1110 g_value_unset (&v);
1111 }
1112
1113 g_list_foreach (entries, (GFunc)rhythmdb_entry_unref, NULL);
1114 g_list_free (entries);
1115
1116 rhythmdb_commit (source->priv->db);
1117 }
1118
1119 static void
1120 impl_delete (RBSource *asource)
1121 {
1122 RBPodcastSource *source = RB_PODCAST_SOURCE (asource);
1123 GtkWidget *dialog;
1124 GtkWidget *button;
1125 GtkWindow *window;
1126 RBShell *shell;
1127
1128 rb_debug ("Delete episode action");
1129
1130 g_object_get (source, "shell", &shell, NULL);
1131 g_object_get (shell, "window", &window, NULL);
1132 g_object_unref (shell);
1133
1134 dialog = gtk_message_dialog_new (window,
1135 GTK_DIALOG_DESTROY_WITH_PARENT,
1136 GTK_MESSAGE_WARNING,
1137 GTK_BUTTONS_NONE,
1138 _("Delete the podcast episode and downloaded file?"));
1139
1140 gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
1141 _("If you choose to delete the episode and file, "
1142 "they will be permanently lost. Please note that "
1143 "you can delete the episode but keep the downloaded "
1144 "file by choosing to delete the episode only."));
1145
1146 gtk_window_set_title (GTK_WINDOW (dialog), "");
1147
1148 gtk_dialog_add_buttons (GTK_DIALOG (dialog),
1149 _("Delete _Episode Only"),
1150 GTK_RESPONSE_NO,
1151 GTK_STOCK_CANCEL,
1152 GTK_RESPONSE_CANCEL,
1153 NULL);
1154 button = gtk_dialog_add_button (GTK_DIALOG (dialog),
1155 _("_Delete Episode And File"),
1156 GTK_RESPONSE_YES);
1157
1158 gtk_window_set_focus (GTK_WINDOW (dialog), button);
1159 gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_YES);
1160 g_signal_connect (dialog, "response", G_CALLBACK (delete_response_cb), source);
1161 gtk_widget_show_all (dialog);
1162 }
1163
1164 static RBEntryView *
1165 impl_get_entry_view (RBSource *asource)
1166 {
1167 RBPodcastSource *source = RB_PODCAST_SOURCE (asource);
1168 return source->priv->posts;
1169 }
1170
1171 static RBSourceEOFType
1172 impl_handle_eos (RBSource *asource)
1173 {
1174 return RB_SOURCE_EOF_STOP;
1175 }
1176
1177
1178 static gboolean
1179 impl_receive_drag (RBDisplayPage *page, GtkSelectionData *selection_data)
1180 {
1181 GList *list, *i;
1182 RBPodcastSource *source = RB_PODCAST_SOURCE (page);
1183
1184 list = rb_uri_list_parse ((const char *) gtk_selection_data_get_data (selection_data));
1185
1186 for (i = list; i != NULL; i = i->next) {
1187 char *uri = NULL;
1188
1189 uri = i->data;
1190 if ((uri != NULL) && (!rhythmdb_entry_lookup_by_location (source->priv->db, uri))) {
1191 rb_podcast_manager_subscribe_feed (source->priv->podcast_mgr, uri, FALSE);
1192 }
1193
1194 if (gtk_selection_data_get_data_type (selection_data) == gdk_atom_intern ("_NETSCAPE_URL", FALSE)) {
1195 i = i->next;
1196 }
1197 }
1198
1199 rb_list_deep_free (list);
1200 return TRUE;
1201 }
1202
1203 static void
1204 impl_search (RBSource *asource, RBSourceSearch *search, const char *cur_text, const char *new_text)
1205 {
1206 RBPodcastSource *source = RB_PODCAST_SOURCE (asource);
1207
1208 if (search == NULL) {
1209 search = source->priv->default_search;
1210 }
1211
1212 if (source->priv->search_query != NULL) {
1213 rhythmdb_query_free (source->priv->search_query);
1214 source->priv->search_query = NULL;
1215 }
1216 source->priv->search_query = rb_source_search_create_query (search, source->priv->db, new_text);
1217 rb_podcast_source_do_query (source);
1218
1219 rb_source_notify_filter_changed (RB_SOURCE (source));
1220 }
1221
1222 static void
1223 impl_reset_filters (RBSource *asource)
1224 {
1225 RBPodcastSource *source = RB_PODCAST_SOURCE (asource);
1226 if (source->priv->search_query != NULL) {
1227 rhythmdb_query_free (source->priv->search_query);
1228 source->priv->search_query = NULL;
1229 }
1230 rb_source_toolbar_clear_search_entry (source->priv->toolbar);
1231
1232 rb_property_view_set_selection (source->priv->feeds, NULL);
1233 }
1234
1235 static gboolean
1236 impl_show_popup (RBDisplayPage *page)
1237 {
1238 _rb_display_page_show_popup (page, "/PodcastSourcePopup");
1239 return TRUE;
1240 }
1241
1242 static void
1243 impl_song_properties (RBSource *asource)
1244 {
1245 RBPodcastSource *source = RB_PODCAST_SOURCE (asource);
1246 GtkWidget *dialog = rb_podcast_properties_dialog_new (source->priv->posts);
1247 if (dialog)
1248 gtk_widget_show_all (dialog);
1249 }
1250
1251 static void
1252 impl_get_status (RBDisplayPage *page, char **text, char **progress_text, float *progress)
1253 {
1254 RhythmDBQueryModel *query_model;
1255
1256 /* hack to get these strings marked for translation */
1257 if (0) {
1258 ngettext ("%d episode", "%d episodes", 0);
1259 }
1260
1261 g_object_get (page, "query-model", &query_model, NULL);
1262 if (query_model != NULL) {
1263 *text = rhythmdb_query_model_compute_status_normal (query_model,
1264 "%d episode",
1265 "%d episodes");
1266 if (rhythmdb_query_model_has_pending_changes (query_model))
1267 *progress = -1.0f;
1268
1269 g_object_unref (query_model);
1270 } else {
1271 *text = g_strdup ("");
1272 }
1273 }
1274
1275
1276 static char *
1277 impl_get_delete_action (RBSource *source)
1278 {
1279 return g_strdup ("EditDelete");
1280 }
1281
1282 static void
1283 impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
1284 {
1285 RBPodcastSource *source = RB_PODCAST_SOURCE (object);
1286
1287 switch (prop_id) {
1288 case PROP_PODCAST_MANAGER:
1289 source->priv->podcast_mgr = g_value_dup_object (value);
1290 break;
1291 case PROP_BASE_QUERY:
1292 source->priv->base_query = rhythmdb_query_copy (g_value_get_pointer (value));
1293 break;
1294 case PROP_SHOW_BROWSER:
1295 gtk_widget_set_visible (GTK_WIDGET (source->priv->feeds), g_value_get_boolean (value));
1296 break;
1297 default:
1298 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
1299 break;
1300 }
1301 }
1302
1303 static void
1304 impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
1305 {
1306 RBPodcastSource *source = RB_PODCAST_SOURCE (object);
1307
1308 switch (prop_id) {
1309 case PROP_PODCAST_MANAGER:
1310 g_value_set_object (value, source->priv->podcast_mgr);
1311 break;
1312 case PROP_BASE_QUERY:
1313 g_value_set_pointer (value, source->priv->base_query);
1314 break;
1315 case PROP_SHOW_BROWSER:
1316 g_value_set_boolean (value, gtk_widget_get_visible (GTK_WIDGET (source->priv->feeds)));
1317 break;
1318 default:
1319 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
1320 break;
1321 }
1322 }
1323
1324 static void
1325 impl_constructed (GObject *object)
1326 {
1327 RBPodcastSource *source;
1328 GtkTreeViewColumn *column;
1329 GtkCellRenderer *renderer;
1330 RBShell *shell;
1331 RBShellPlayer *shell_player;
1332 RhythmDBQueryModel *query_model;
1333 GtkAction *action;
1334 GSettings *settings;
1335 int position;
1336 GtkUIManager *ui_manager;
1337
1338 RB_CHAIN_GOBJECT_METHOD (rb_podcast_source_parent_class, constructed, object);
1339 source = RB_PODCAST_SOURCE (object);
1340
1341 g_object_get (source, "shell", &shell, NULL);
1342 g_object_get (shell,
1343 "db", &source->priv->db,
1344 "shell-player", &shell_player,
1345 "ui-manager", &ui_manager,
1346 NULL);
1347
1348 source->priv->action_group = _rb_display_page_register_action_group (RB_DISPLAY_PAGE (source),
1349 "PodcastActions",
1350 NULL, 0,
1351 source);
1352
1353 _rb_action_group_add_display_page_actions (source->priv->action_group,
1354 G_OBJECT (shell),
1355 rb_podcast_source_actions,
1356 G_N_ELEMENTS (rb_podcast_source_actions));
1357
1358 source->priv->add_action = gtk_action_group_get_action (source->priv->action_group,
1359 "MusicNewPodcast");
1360 /* Translators: this is the toolbar button label
1361 for New Podcast Feed action. */
1362 g_object_set (source->priv->add_action, "short-label", C_("Podcast", "Add"), NULL);
1363
1364 action = gtk_action_group_get_action (source->priv->action_group,
1365 "PodcastFeedUpdate");
1366 /* Translators: this is the toolbar button label
1367 for Update Feed action. */
1368 g_object_set (action, "short-label", _("Update"), NULL);
1369
1370 if (gtk_action_group_get_action (source->priv->action_group,
1371 rb_podcast_source_radio_actions[0].name) == NULL) {
1372 gtk_action_group_add_radio_actions (source->priv->action_group,
1373 rb_podcast_source_radio_actions,
1374 G_N_ELEMENTS (rb_podcast_source_radio_actions),
1375 0,
1376 NULL,
1377 NULL);
1378 rb_source_search_basic_create_for_actions (source->priv->action_group,
1379 rb_podcast_source_radio_actions,
1380 G_N_ELEMENTS (rb_podcast_source_radio_actions));
1381 }
1382
1383 source->priv->default_search = rb_source_search_basic_new (RHYTHMDB_PROP_SEARCH_MATCH);
1384
1385 source->priv->paned = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
1386
1387
1388 /* set up posts view */
1389 source->priv->posts = rb_entry_view_new (source->priv->db,
1390 G_OBJECT (shell_player),
1391 TRUE, FALSE);
1392 g_object_unref (shell_player);
1393
1394 g_signal_connect_object (source->priv->posts,
1395 "entry-activated",
1396 G_CALLBACK (episode_activated_cb),
1397 source, 0);
1398
1399 /* Podcast date column */
1400 column = gtk_tree_view_column_new ();
1401 renderer = gtk_cell_renderer_text_new();
1402
1403 gtk_tree_view_column_pack_start (column, renderer, TRUE);
1404
1405 gtk_tree_view_column_set_clickable (column, TRUE);
1406 gtk_tree_view_column_set_resizable (column, TRUE);
1407 gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
1408 {
1409 const char *sample_strings[3];
1410 sample_strings[0] = _("Date");
1411 sample_strings[1] = rb_entry_view_get_time_date_column_sample ();
1412 sample_strings[2] = NULL;
1413 rb_entry_view_set_fixed_column_width (source->priv->posts, column, renderer, sample_strings);
1414 }
1415
1416 gtk_tree_view_column_set_cell_data_func (column, renderer,
1417 (GtkTreeCellDataFunc) podcast_post_date_cell_data_func,
1418 source, NULL);
1419
1420 rb_entry_view_append_column_custom (source->priv->posts, column,
1421 _("Date"), "Date",
1422 (GCompareDataFunc) podcast_post_date_sort_func,
1423 0, NULL);
1424
1425 rb_entry_view_append_column (source->priv->posts, RB_ENTRY_VIEW_COL_TITLE, TRUE);
1426
1427 /* COLUMN FEED */
1428 column = gtk_tree_view_column_new ();
1429 renderer = gtk_cell_renderer_text_new();
1430
1431 gtk_tree_view_column_pack_start (column, renderer, TRUE);
1432
1433 gtk_tree_view_column_set_clickable (column, TRUE);
1434 gtk_tree_view_column_set_resizable (column, TRUE);
1435 gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
1436 gtk_tree_view_column_set_expand (column, TRUE);
1437
1438 gtk_tree_view_column_set_cell_data_func (column, renderer,
1439 (GtkTreeCellDataFunc) podcast_post_feed_cell_data_func,
1440 source, NULL);
1441
1442 rb_entry_view_append_column_custom (source->priv->posts, column,
1443 _("Feed"), "Feed",
1444 (GCompareDataFunc) podcast_post_feed_sort_func,
1445 0, NULL);
1446
1447 rb_entry_view_append_column (source->priv->posts, RB_ENTRY_VIEW_COL_DURATION, FALSE);
1448 rb_entry_view_append_column (source->priv->posts, RB_ENTRY_VIEW_COL_RATING, FALSE);
1449 rb_entry_view_append_column (source->priv->posts, RB_ENTRY_VIEW_COL_PLAY_COUNT, FALSE);
1450 rb_entry_view_append_column (source->priv->posts, RB_ENTRY_VIEW_COL_LAST_PLAYED, FALSE);
1451
1452 /* Status column */
1453 column = gtk_tree_view_column_new ();
1454 renderer = gtk_cell_renderer_progress_new();
1455
1456 gtk_tree_view_column_pack_start (column, renderer, TRUE);
1457
1458 gtk_tree_view_column_set_clickable (column, TRUE);
1459 gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
1460
1461 {
1462 static const char *status_strings[6];
1463 status_strings[0] = _("Status");
1464 status_strings[1] = _("Downloaded");
1465 status_strings[2] = _("Waiting");
1466 status_strings[3] = _("Failed");
1467 status_strings[4] = "100 %";
1468 status_strings[5] = NULL;
1469
1470 rb_entry_view_set_fixed_column_width (source->priv->posts,
1471 column,
1472 renderer,
1473 status_strings);
1474 }
1475
1476 gtk_tree_view_column_set_cell_data_func (column, renderer,
1477 (GtkTreeCellDataFunc) podcast_post_status_cell_data_func,
1478 source, NULL);
1479
1480 rb_entry_view_append_column_custom (source->priv->posts, column,
1481 _("Status"), "Status",
1482 (GCompareDataFunc) podcast_post_status_sort_func,
1483 0, NULL);
1484
1485 g_signal_connect_object (source->priv->posts,
1486 "notify::sort-order",
1487 G_CALLBACK (podcast_posts_view_sort_order_changed_cb),
1488 source, 0);
1489
1490 g_signal_connect_object (source->priv->posts,
1491 "show_popup",
1492 G_CALLBACK (podcast_posts_show_popup_cb),
1493 source, 0);
1494
1495 /* configure feed view */
1496 source->priv->feeds = rb_property_view_new (source->priv->db,
1497 RHYTHMDB_PROP_SUBTITLE,
1498 _("Feed"));
1499 rb_property_view_set_selection_mode (RB_PROPERTY_VIEW (source->priv->feeds),
1500 GTK_SELECTION_MULTIPLE);
1501
1502 query_model = rhythmdb_query_model_new_empty (source->priv->db);
1503 source->priv->feed_model = rb_property_view_get_model (RB_PROPERTY_VIEW (source->priv->feeds));
1504 g_object_set (source->priv->feed_model, "query-model", query_model, NULL);
1505
1506 /* maybe do this async? */
1507 rhythmdb_do_full_query_parsed (source->priv->db,
1508 RHYTHMDB_QUERY_RESULTS (query_model),
1509 source->priv->base_query);
1510 g_object_unref (query_model);
1511
1512 /* error indicator column */
1513 column = gtk_tree_view_column_new ();
1514 renderer = rb_cell_renderer_pixbuf_new ();
1515 gtk_tree_view_column_pack_start (column, renderer, TRUE);
1516 gtk_tree_view_column_set_cell_data_func (column, renderer,
1517 (GtkTreeCellDataFunc) podcast_feed_error_cell_data_func,
1518 source, NULL);
1519 gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
1520 gtk_tree_view_column_set_fixed_width (column, gdk_pixbuf_get_width (source->priv->error_pixbuf) + 5);
1521 gtk_tree_view_column_set_reorderable (column, FALSE);
1522 gtk_tree_view_column_set_visible (column, TRUE);
1523 rb_property_view_append_column_custom (source->priv->feeds, column);
1524 g_signal_connect_object (renderer,
1525 "pixbuf-clicked",
1526 G_CALLBACK (podcast_error_pixbuf_clicked_cb),
1527 source, 0);
1528
1529 /* redraw error indicator when errors are set or cleared */
1530 g_signal_connect_object (source->priv->db,
1531 "entry_changed",
1532 G_CALLBACK (podcast_entry_changed_cb),
1533 source, 0);
1534
1535 /* title column */
1536 column = gtk_tree_view_column_new ();
1537 renderer = gtk_cell_renderer_text_new ();
1538
1539 gtk_tree_view_column_pack_start (column, renderer, TRUE);
1540
1541 gtk_tree_view_column_set_cell_data_func (column,
1542 renderer,
1543 (GtkTreeCellDataFunc) podcast_feed_title_cell_data_func,
1544 source, NULL);
1545
1546 gtk_tree_view_column_set_title (column, _("Feed"));
1547 gtk_tree_view_column_set_reorderable (column, FALSE);
1548 gtk_tree_view_column_set_visible (column, TRUE);
1549 rb_property_view_append_column_custom (source->priv->feeds, column);
1550
1551 g_signal_connect_object (source->priv->feeds, "show_popup",
1552 G_CALLBACK (podcast_feeds_show_popup_cb),
1553 source, 0);
1554
1555 g_signal_connect_object (source->priv->feeds,
1556 "properties-selected",
1557 G_CALLBACK (feed_select_change_cb),
1558 source, 0);
1559
1560 rb_property_view_set_search_func (source->priv->feeds,
1561 (GtkTreeViewSearchEqualFunc) podcast_feed_title_search_func,
1562 source,
1563 NULL);
1564
1565 /* set up drag and drop */
1566 g_signal_connect_object (source->priv->feeds,
1567 "drag_data_received",
1568 G_CALLBACK (posts_view_drag_data_received_cb),
1569 source, 0);
1570
1571 gtk_drag_dest_set (GTK_WIDGET (source->priv->feeds),
1572 GTK_DEST_DEFAULT_ALL,
1573 posts_view_drag_types, 2,
1574 GDK_ACTION_COPY | GDK_ACTION_MOVE);
1575
1576 g_signal_connect_object (G_OBJECT (source->priv->posts),
1577 "drag_data_received",
1578 G_CALLBACK (posts_view_drag_data_received_cb),
1579 source, 0);
1580
1581 gtk_drag_dest_set (GTK_WIDGET (source->priv->posts),
1582 GTK_DEST_DEFAULT_ALL,
1583 posts_view_drag_types, 2,
1584 GDK_ACTION_COPY | GDK_ACTION_MOVE);
1585
1586 /* set up toolbar */
1587 source->priv->toolbar = rb_source_toolbar_new (RB_DISPLAY_PAGE (source), ui_manager);
1588 rb_source_toolbar_add_search_entry (source->priv->toolbar, "/PodcastSourceSearchMenu", NULL);
1589
1590 /* pack the feed and post views into the source */
1591 gtk_paned_pack1 (GTK_PANED (source->priv->paned),
1592 GTK_WIDGET (source->priv->feeds), FALSE, FALSE);
1593 gtk_paned_pack2 (GTK_PANED (source->priv->paned),
1594 GTK_WIDGET (source->priv->posts), TRUE, FALSE);
1595
1596 source->priv->grid = gtk_grid_new ();
1597 gtk_widget_set_margin_top (GTK_WIDGET (source->priv->grid), 6);
1598 gtk_grid_set_column_spacing (GTK_GRID (source->priv->grid), 6);
1599 gtk_grid_set_row_spacing (GTK_GRID (source->priv->grid), 6);
1600 gtk_grid_attach (GTK_GRID (source->priv->grid), GTK_WIDGET (source->priv->toolbar), 0, 0, 1, 1);
1601 gtk_grid_attach (GTK_GRID (source->priv->grid), source->priv->paned, 0, 1, 1, 1);
1602
1603 gtk_container_add (GTK_CONTAINER (source), source->priv->grid);
1604
1605 /* podcast add dialog */
1606 source->priv->add_dialog = rb_podcast_add_dialog_new (shell, source->priv->podcast_mgr);
1607 gtk_widget_show_all (source->priv->add_dialog);
1608 gtk_widget_set_margin_top (source->priv->add_dialog, 0);
1609 gtk_grid_attach (GTK_GRID (source->priv->grid), GTK_WIDGET (source->priv->add_dialog), 0, 2, 1, 1);
1610 gtk_widget_set_no_show_all (source->priv->add_dialog, TRUE);
1611 g_signal_connect_object (source->priv->add_dialog, "closed", G_CALLBACK (podcast_add_dialog_closed_cb), source, 0);
1612
1613 gtk_widget_show_all (GTK_WIDGET (source));
1614 gtk_widget_hide (source->priv->add_dialog);
1615
1616 g_object_get (source, "settings", &settings, NULL);
1617
1618 g_signal_connect_object (settings, "changed", G_CALLBACK (settings_changed_cb), source, 0);
1619
1620 position = g_settings_get_int (settings, PODCAST_PANED_POSITION);
1621 gtk_paned_set_position (GTK_PANED (source->priv->paned), position);
1622
1623 rb_source_bind_settings (RB_SOURCE (source),
1624 GTK_WIDGET (source->priv->posts),
1625 source->priv->paned,
1626 GTK_WIDGET (source->priv->feeds));
1627
1628 g_object_unref (settings);
1629 g_object_unref (ui_manager);
1630 g_object_unref (shell);
1631
1632 rb_podcast_source_do_query (source);
1633 }
1634
1635 static void
1636 impl_dispose (GObject *object)
1637 {
1638 RBPodcastSource *source;
1639
1640 source = RB_PODCAST_SOURCE (object);
1641
1642 if (source->priv->db != NULL) {
1643 g_object_unref (source->priv->db);
1644 source->priv->db = NULL;
1645 }
1646
1647 if (source->priv->search_query != NULL) {
1648 rhythmdb_query_free (source->priv->search_query);
1649 source->priv->search_query = NULL;
1650 }
1651
1652 if (source->priv->action_group != NULL) {
1653 g_object_unref (source->priv->action_group);
1654 source->priv->action_group = NULL;
1655 }
1656
1657 if (source->priv->podcast_mgr != NULL) {
1658 g_object_unref (source->priv->podcast_mgr);
1659 source->priv->podcast_mgr = NULL;
1660 }
1661
1662 if (source->priv->error_pixbuf != NULL) {
1663 g_object_unref (source->priv->error_pixbuf);
1664 source->priv->error_pixbuf = NULL;
1665 }
1666
1667 G_OBJECT_CLASS (rb_podcast_source_parent_class)->dispose (object);
1668 }
1669
1670 static void
1671 impl_finalize (GObject *object)
1672 {
1673 RBPodcastSource *source;
1674
1675 g_return_if_fail (object != NULL);
1676 g_return_if_fail (RB_IS_PODCAST_SOURCE (object));
1677
1678 source = RB_PODCAST_SOURCE (object);
1679
1680 g_return_if_fail (source->priv != NULL);
1681
1682 if (source->priv->selected_feeds) {
1683 g_list_foreach (source->priv->selected_feeds, (GFunc) g_free, NULL);
1684 g_list_free (source->priv->selected_feeds);
1685 }
1686
1687 G_OBJECT_CLASS (rb_podcast_source_parent_class)->finalize (object);
1688 }
1689
1690 static void
1691 rb_podcast_source_init (RBPodcastSource *source)
1692 {
1693 GtkIconTheme *icon_theme;
1694 source->priv = G_TYPE_INSTANCE_GET_PRIVATE (source,
1695 RB_TYPE_PODCAST_SOURCE,
1696 RBPodcastSourcePrivate);
1697
1698 source->priv->selected_feeds = NULL;
1699
1700 icon_theme = gtk_icon_theme_get_default ();
1701 source->priv->error_pixbuf = gtk_icon_theme_load_icon (icon_theme,
1702 "dialog-error",
1703 16,
1704 0,
1705 NULL);
1706 }
1707
1708 static void
1709 rb_podcast_source_class_init (RBPodcastSourceClass *klass)
1710 {
1711 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1712 RBDisplayPageClass *page_class = RB_DISPLAY_PAGE_CLASS (klass);
1713 RBSourceClass *source_class = RB_SOURCE_CLASS (klass);
1714
1715 object_class->dispose = impl_dispose;
1716 object_class->finalize = impl_finalize;
1717 object_class->constructed = impl_constructed;
1718 object_class->set_property = impl_set_property;
1719 object_class->get_property = impl_get_property;
1720
1721 page_class->get_status = impl_get_status;
1722 page_class->receive_drag = impl_receive_drag;
1723 page_class->show_popup = impl_show_popup;
1724
1725 source_class->impl_add_to_queue = impl_add_to_queue;
1726 source_class->impl_can_add_to_queue = impl_can_add_to_queue;
1727 source_class->impl_can_copy = (RBSourceFeatureFunc) rb_false_function;
1728 source_class->impl_can_cut = (RBSourceFeatureFunc) rb_false_function;
1729 source_class->impl_can_delete = (RBSourceFeatureFunc) rb_true_function;
1730 source_class->impl_delete = impl_delete;
1731 source_class->impl_get_entry_view = impl_get_entry_view;
1732 source_class->impl_handle_eos = impl_handle_eos;
1733 source_class->impl_search = impl_search;
1734 source_class->impl_song_properties = impl_song_properties;
1735 source_class->impl_get_delete_action = impl_get_delete_action;
1736 source_class->impl_reset_filters = impl_reset_filters;
1737
1738 g_object_class_install_property (object_class,
1739 PROP_PODCAST_MANAGER,
1740 g_param_spec_object ("podcast-manager",
1741 "RBPodcastManager",
1742 "RBPodcastManager object",
1743 RB_TYPE_PODCAST_MANAGER,
1744 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
1745 g_object_class_install_property (object_class,
1746 PROP_BASE_QUERY,
1747 g_param_spec_pointer ("base-query",
1748 "Base query",
1749 "Base query for the source",
1750 G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
1751
1752 g_object_class_override_property (object_class, PROP_SHOW_BROWSER, "show-browser");
1753
1754 g_type_class_add_private (klass, sizeof (RBPodcastSourcePrivate));
1755 }