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