1 /*
2 * @file feedlist.c subscriptions as an hierarchic tree
3 *
4 * Copyright (C) 2005-2013 Lars Windolf <lars.windolf@gmx.de>
5 * Copyright (C) 2005-2006 Nathan J. Conrad <t98502@users.sourceforge.net>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 */
21
22 #include <libxml/uri.h>
23
24 #include "comments.h"
25 #include "common.h"
26 #include "conf.h"
27 #include "db.h"
28 #include "debug.h"
29 #include "feed.h"
30 #include "feedlist.h"
31 #include "folder.h"
32 #include "itemlist.h"
33 #include "net_monitor.h"
34 #include "node.h"
35 #include "update.h"
36 #include "vfolder.h"
37 #include "ui/feed_list_view.h"
38 #include "ui/itemview.h"
39 #include "ui/liferea_shell.h"
40 #include "ui/feed_list_node.h"
41 #include "fl_sources/node_source.h"
42
43 static void feedlist_save (void);
44
45 #define FEEDLIST_GET_PRIVATE(object)(G_TYPE_INSTANCE_GET_PRIVATE ((object), FEEDLIST_TYPE, FeedListPrivate))
46
47 struct FeedListPrivate {
48 guint newCount; /*<< overall new item count */
49
50 nodePtr rootNode; /*<< the feed list root node */
51 nodePtr selectedNode; /*<< matches the node selected in the feed list tree view, which
52 is not necessarily the displayed one (e.g. folders without recursive
53 display enabled) */
54
55 guint saveTimer; /*<< timer id for delayed feed list saving */
56 guint autoUpdateTimer; /*<< timer id for auto update */
57
58 gboolean loading; /*<< prevents the feed list being saved before it is completely loaded */
59 };
60
61 enum {
62 NEW_ITEMS, /*<< node has new items after update */
63 NODE_UPDATED, /*<< node display info (title, unread count) has changed */
64 LAST_SIGNAL
65 };
66
67 #define ROOTNODE feedlist->priv->rootNode
68 #define SELECTED feedlist->priv->selectedNode
69
70 static guint feedlist_signals[LAST_SIGNAL] = { 0 };
71
72 static GObjectClass *parent_class = NULL;
73 FeedList *feedlist = NULL;
74
75 G_DEFINE_TYPE (FeedList, feedlist, G_TYPE_OBJECT);
76
77 static void
feedlist_free_node(nodePtr node)78 feedlist_free_node (nodePtr node)
79 {
80 if (node->children)
81 node_foreach_child (node, feedlist_free_node);
82
83 node->parent->children = g_slist_remove (node->parent->children, node);
84 node_free (node);
85 }
86
87 static void
feedlist_finalize(GObject * object)88 feedlist_finalize (GObject *object)
89 {
90 /* Stop all timer based activity */
91 if (feedlist->priv->autoUpdateTimer) {
92 g_source_remove (feedlist->priv->autoUpdateTimer);
93 feedlist->priv->autoUpdateTimer = 0;
94 }
95 if (feedlist->priv->saveTimer) {
96 g_source_remove (feedlist->priv->saveTimer);
97 feedlist->priv->saveTimer = 0;
98 }
99
100 /* Enforce synchronous save upon exit */
101 feedlist_save ();
102
103 /* Save last selection for next start */
104 if (feedlist->priv->selectedNode)
105 conf_set_str_value (LAST_NODE_SELECTED, feedlist->priv->selectedNode->id);
106
107
108 /* And destroy everything */
109 feedlist_foreach (feedlist_free_node);
110 node_free (ROOTNODE);
111 ROOTNODE = NULL;
112
113 /* This might also be a good place to get for some other cleanup */
114 comments_deinit ();
115
116 G_OBJECT_CLASS (parent_class)->finalize (object);
117 }
118
119 static void
feedlist_class_init(FeedListClass * klass)120 feedlist_class_init (FeedListClass *klass)
121 {
122 GObjectClass *object_class = G_OBJECT_CLASS (klass);
123
124 parent_class = g_type_class_peek_parent (klass);
125
126 object_class->finalize = feedlist_finalize;
127
128 feedlist_signals[NEW_ITEMS] =
129 g_signal_new ("new-items",
130 G_OBJECT_CLASS_TYPE (object_class),
131 (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION),
132 0,
133 NULL,
134 NULL,
135 g_cclosure_marshal_VOID__POINTER,
136 G_TYPE_NONE,
137 1,
138 G_TYPE_POINTER);
139
140 feedlist_signals[NODE_UPDATED] =
141 g_signal_new ("node-updated",
142 G_OBJECT_CLASS_TYPE (object_class),
143 (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION),
144 0,
145 NULL,
146 NULL,
147 g_cclosure_marshal_VOID__STRING,
148 G_TYPE_NONE,
149 1,
150 G_TYPE_STRING);
151
152 g_type_class_add_private (object_class, sizeof(FeedListPrivate));
153 }
154
155 static gboolean
feedlist_auto_update(void * data)156 feedlist_auto_update (void *data)
157 {
158 debug_enter ("feedlist_auto_update");
159
160 if (network_monitor_is_online ())
161 node_auto_update_subscription (ROOTNODE);
162 else
163 debug0 (DEBUG_UPDATE, "no update processing because we are offline!");
164
165 debug_exit ("feedlist_auto_update");
166
167 return TRUE;
168 }
169
170 static void
on_network_status_changed(gpointer instance,gboolean online,gpointer data)171 on_network_status_changed (gpointer instance, gboolean online, gpointer data)
172 {
173 if (online) feedlist_auto_update (NULL);
174 }
175
176 /* This method is used to initialize the node states in the feed list */
177 static void
feedlist_init_node(nodePtr node)178 feedlist_init_node (nodePtr node)
179 {
180 if (node->expanded)
181 feed_list_node_set_expansion (node, TRUE);
182
183 if (node->subscription)
184 db_subscription_load (node->subscription);
185
186 node_update_counters (node);
187 feed_list_node_update (node->id); /* Necessary to initially set folder unread counters */
188
189 node_foreach_child (node, feedlist_init_node);
190 }
191
192 static void
feedlist_init(FeedList * fl)193 feedlist_init (FeedList *fl)
194 {
195 gint startup_feed_action;
196
197 debug_enter ("feedlist_init");
198
199 /* 1. Prepare globally accessible singleton */
200 g_assert (NULL == feedlist);
201 feedlist = fl;
202
203 feedlist->priv = FEEDLIST_GET_PRIVATE (fl);
204 feedlist->priv->loading = TRUE;
205
206 /* 2. Set up a root node and import the feed list source structure. */
207 debug0 (DEBUG_CACHE, "Setting up root node");
208 ROOTNODE = node_source_setup_root ();
209
210 /* 3. Ensure folder expansion and unread count*/
211 debug0 (DEBUG_CACHE, "Initializing node state");
212 feedlist_foreach (feedlist_init_node);
213
214 /* 4. Check if feeds do need updating. */
215 debug0 (DEBUG_UPDATE, "Performing initial feed update");
216 conf_get_int_value (STARTUP_FEED_ACTION, &startup_feed_action);
217 if (0 == startup_feed_action) {
218 /* Update all feeds */
219 if (network_monitor_is_online ()) {
220 debug0 (DEBUG_UPDATE, "initial update: updating all feeds");
221 node_update_subscription (feedlist_get_root (), GUINT_TO_POINTER (0));
222 } else {
223 debug0 (DEBUG_UPDATE, "initial update: prevented because we are offline");
224 }
225 } else {
226 debug0 (DEBUG_UPDATE, "initial update: resetting feed counter");
227 feedlist_reset_update_counters (NULL);
228 }
229
230 /* 5. Purge old nodes from the database */
231 db_node_cleanup (feedlist_get_root ());
232
233 /* 6. Start automatic updating */
234 feedlist->priv->autoUpdateTimer = g_timeout_add_seconds (10, feedlist_auto_update, NULL);
235 g_signal_connect (network_monitor_get (), "online-status-changed", G_CALLBACK (on_network_status_changed), NULL);
236
237 /* 7. Finally save the new feed list state */
238 feedlist->priv->loading = FALSE;
239 feedlist_schedule_save ();
240
241 debug_exit ("feedlist_init");
242 }
243
244 static void feedlist_unselect(void);
245
246 nodePtr
feedlist_get_root(void)247 feedlist_get_root (void)
248 {
249 return ROOTNODE;
250 }
251
252 nodePtr
feedlist_get_selected(void)253 feedlist_get_selected (void)
254 {
255 return SELECTED;
256 }
257
258 static nodePtr
feedlist_get_parent_node(void)259 feedlist_get_parent_node (void)
260 {
261
262 g_assert (NULL != ROOTNODE);
263
264 if (!SELECTED)
265 return ROOTNODE;
266
267 if (IS_FOLDER (SELECTED))
268 return SELECTED;
269
270 if (SELECTED->parent)
271 return SELECTED->parent;
272
273 return ROOTNODE;
274 }
275
276 nodePtr
feedlist_find_node(nodePtr parent,feedListFindType type,const gchar * str)277 feedlist_find_node (nodePtr parent, feedListFindType type, const gchar *str)
278 {
279 GSList *iter;
280
281 g_assert (str);
282
283 iter = parent->children;
284 while (iter) {
285 gboolean found = FALSE;
286 nodePtr result, node = (nodePtr)iter->data;
287
288 /* Check child node */
289 switch (type) {
290 case NODE_BY_URL:
291 if (node->subscription)
292 found = g_str_equal (str, subscription_get_source (node->subscription));
293 break;
294 case NODE_BY_ID:
295 found = g_str_equal (str, node->id);
296 break;
297 case FOLDER_BY_TITLE:
298 if (IS_FOLDER (node))
299 found = g_str_equal (str, node->title);
300 break;
301
302 default:
303 break;
304 }
305 if (found)
306 return node;
307
308 /* And recurse */
309 result = feedlist_find_node (node, type, str);
310 if (result)
311 return result;
312
313 iter = g_slist_next (iter);
314 }
315
316 return NULL;
317 }
318
319 gboolean
feedlist_is_writable(void)320 feedlist_is_writable (void)
321 {
322 nodePtr node;
323
324 node = feedlist_get_parent_node ();
325
326 return (0 != (NODE_TYPE (node->source->root)->capabilities & NODE_CAPABILITY_ADD_CHILDS));
327 }
328
329 static void
feedlist_update_node_counters(nodePtr node)330 feedlist_update_node_counters (nodePtr node)
331 {
332 node_update_counters (node); /* update with parent propagation */
333
334 if (node->needsUpdate)
335 feed_list_node_update (node->id);
336 if (node->children)
337 node_foreach_child (node, feedlist_update_node_counters);
338 }
339
340 void
feedlist_mark_all_read(nodePtr node)341 feedlist_mark_all_read (nodePtr node)
342 {
343 if (!node)
344 return;
345
346 feedlist_reset_new_item_count ();
347
348 if (node != ROOTNODE)
349 node_mark_all_read (node);
350 else
351 node_foreach_child (ROOTNODE, node_mark_all_read);
352
353 feedlist_foreach (feedlist_update_node_counters);
354 itemview_select_item (NULL);
355 itemview_update_all_items ();
356 itemview_update ();
357 }
358
359 /* statistic handling methods */
360
361 guint
feedlist_get_unread_item_count(void)362 feedlist_get_unread_item_count (void)
363 {
364 if (!feedlist)
365 return 0;
366
367 return (ROOTNODE->unreadCount > 0)?ROOTNODE->unreadCount:0;
368 }
369
370 guint
feedlist_get_new_item_count(void)371 feedlist_get_new_item_count (void)
372 {
373 if (!feedlist)
374 return 0;
375
376 return (feedlist->priv->newCount > 0)?feedlist->priv->newCount:0;
377 }
378
379 void
feedlist_reset_new_item_count(void)380 feedlist_reset_new_item_count (void)
381 {
382 if (feedlist->priv->newCount)
383 feedlist->priv->newCount = 0;
384
385 feedlist_new_items (0);
386 }
387
388 void
feedlist_add_folder(const gchar * title)389 feedlist_add_folder (const gchar *title)
390 {
391 nodePtr parent;
392
393 g_assert (NULL != title);
394
395 parent = feedlist_get_parent_node ();
396
397 if(0 == (NODE_TYPE (parent->source->root)->capabilities & NODE_CAPABILITY_ADD_CHILDS))
398 return;
399
400 node_source_add_folder (parent->source->root, title);
401 }
402
403 void
feedlist_add_subscription(const gchar * source,const gchar * filter,updateOptionsPtr options,gint flags)404 feedlist_add_subscription (const gchar *source, const gchar *filter, updateOptionsPtr options, gint flags)
405 {
406 nodePtr parent;
407
408 g_assert (NULL != source);
409
410 parent = feedlist_get_parent_node ();
411
412 if (0 == (NODE_TYPE (parent->source->root)->capabilities & NODE_CAPABILITY_ADD_CHILDS)) {
413 g_warning ("feedlist_add_subscription: this should never happen!");
414 return;
415 }
416
417 node_source_add_subscription (parent->source->root, subscription_new (source, filter, options));
418 }
419
420 void
feedlist_node_imported(nodePtr node)421 feedlist_node_imported (nodePtr node)
422 {
423 feed_list_node_add (node);
424
425 feedlist_schedule_save ();
426 }
427
428 void
feedlist_node_added(nodePtr node)429 feedlist_node_added (nodePtr node)
430 {
431 gint position = -1;
432
433 g_assert (NULL == node->parent);
434
435 if (SELECTED && !IS_FOLDER (SELECTED)) {
436 position = g_slist_index (SELECTED->parent->children, SELECTED);
437 if (position > -1)
438 position++; /* insert after selected child index */
439 }
440
441 node_set_parent (node, feedlist_get_parent_node (), position);
442
443 if (node->subscription)
444 db_subscription_update (node->subscription);
445
446 db_node_update (node);
447
448 feedlist_node_imported (node);
449
450 feed_list_view_select (node);
451 }
452
453 void
feedlist_remove_node(nodePtr node)454 feedlist_remove_node (nodePtr node)
455 {
456 if (node->source->root != node)
457 node_source_remove_node (node->source->root, node);
458 else
459 feedlist_node_removed (node);
460 }
461
462 void
feedlist_node_removed(nodePtr node)463 feedlist_node_removed (nodePtr node)
464 {
465 if (node == SELECTED)
466 feedlist_unselect ();
467
468 /* First remove all children */
469 node_foreach_child (node, feedlist_node_removed);
470
471 node_remove (node);
472
473 feed_list_node_remove_node (node);
474
475 node->parent->children = g_slist_remove (node->parent->children, node);
476
477 node_free (node);
478
479 feedlist_schedule_save ();
480 }
481
482 /* Checks if the given node is a subscription node and
483 has at least one unread item or is selected, if yes it
484 is added to the list ref passed as user_data */
485 static void
feedlist_collect_unread(nodePtr node,gpointer user_data)486 feedlist_collect_unread (nodePtr node, gpointer user_data)
487 {
488 GSList **list = (GSList **)user_data;
489
490 if (node->children) {
491 node_foreach_child_data (node, feedlist_collect_unread, user_data);
492 return;
493 }
494 if (!node->subscription)
495 return;
496 if (!node->unreadCount && !g_str_equal (node->id, SELECTED->id))
497 return;
498
499 *list = g_slist_append (*list, g_strdup (node->id));
500 }
501
502 nodePtr
feedlist_find_unread_feed(nodePtr folder)503 feedlist_find_unread_feed (nodePtr folder)
504 {
505 GSList *list = NULL;
506 nodePtr result = NULL;
507
508 feedlist_foreach_data (feedlist_collect_unread, &list);
509
510 if (list) {
511 // Pass 1: try after selected node in list
512 if (SELECTED) {
513 GSList *s = g_slist_find_custom (list, SELECTED->id, g_strcmp0);
514 if (s)
515 s = g_slist_next (s);
516 if (s)
517 result = node_from_id (s->data);
518 }
519
520 // Pass 2: just return first node in list
521 if (!result)
522 result = node_from_id (list->data);
523
524 g_slist_free_full (list, g_free);
525 }
526 return result;
527 }
528
529 /* selection handling */
530
531 static void
feedlist_unselect(void)532 feedlist_unselect (void)
533 {
534 SELECTED = NULL;
535
536 itemview_set_displayed_node (NULL);
537 itemview_update ();
538
539 itemlist_unload (FALSE /* mark all read */);
540 feed_list_view_select (NULL);
541 liferea_shell_update_feed_menu (TRUE, FALSE, FALSE);
542 liferea_shell_update_allitems_actions (FALSE, FALSE);
543 }
544
545 void
feedlist_selection_changed(nodePtr node)546 feedlist_selection_changed (nodePtr node)
547 {
548 debug_enter ("feedlist_selection_changed");
549
550 debug1 (DEBUG_GUI, "new selected node: %s", node?node_get_title (node):"none");
551 if (node != SELECTED) {
552
553 /* When the user selects a feed in the feed list we
554 assume that he got notified of the new items or
555 isn't interested in the event anymore... */
556 if (0 != feedlist->priv->newCount)
557 feedlist_reset_new_item_count ();
558
559 /* Unload visible items. */
560 itemlist_unload (TRUE);
561
562 /* Load items of new selected node. */
563 SELECTED = node;
564 if (SELECTED) {
565 itemlist_set_view_mode (node_get_view_mode (SELECTED));
566 itemlist_load (SELECTED);
567 } else {
568 itemview_clear ();
569 }
570 }
571
572 debug_exit ("feedlist_selection_changed");
573 }
574
575 static gboolean
feedlist_schedule_save_cb(gpointer user_data)576 feedlist_schedule_save_cb (gpointer user_data)
577 {
578 /* step 1: request each node to save its state, that is
579 mostly needed for nodes that are node sources */
580 feedlist_foreach (node_save);
581
582 /* step 2: request saving for the root node and thereby
583 forcing the default source to write an OPML file */
584 NODE_SOURCE_TYPE (ROOTNODE)->source_export (ROOTNODE);
585
586 feedlist->priv->saveTimer = 0;
587
588 return FALSE;
589 }
590
591 void
feedlist_schedule_save(void)592 feedlist_schedule_save (void)
593 {
594 if (feedlist->priv->loading || feedlist->priv->saveTimer)
595 return;
596
597 debug0 (DEBUG_CONF, "Scheduling feedlist save");
598
599 /* By waiting here 5s and checking feedlist_save_time
600 we hope to catch bulks of feed list changes and save
601 less often */
602 feedlist->priv->saveTimer = g_timeout_add_seconds (5, feedlist_schedule_save_cb, NULL);
603 }
604
605 /* Handling updates */
606
607 void
feedlist_new_items(guint newCount)608 feedlist_new_items (guint newCount)
609 {
610 feedlist->priv->newCount += newCount;
611
612 /* On subsequent feed updates with cache drops
613 more new items can be reported than effectively
614 were merged. The simplest way to catch this case
615 is by checking for new count > unread count here. */
616 if (feedlist->priv->newCount > ROOTNODE->unreadCount)
617 feedlist->priv->newCount = ROOTNODE->unreadCount;
618
619 g_signal_emit_by_name (feedlist, "new-items", feedlist->priv->newCount);
620 }
621
622 void
feedlist_node_was_updated(nodePtr node)623 feedlist_node_was_updated (nodePtr node)
624 {
625 node_update_counters (node);
626 feedlist_schedule_save ();
627
628 g_signal_emit_by_name (feedlist, "node-updated", node->title);
629 }
630
631 /* This method is only to be used when exiting the program! */
632 static void
feedlist_save(void)633 feedlist_save (void)
634 {
635 debug0 (DEBUG_CONF, "Forced feed list save");
636 feedlist_schedule_save_cb (NULL);
637 }
638
639 void
feedlist_reset_update_counters(nodePtr node)640 feedlist_reset_update_counters (nodePtr node)
641 {
642 GTimeVal now;
643
644 if (!node)
645 node = feedlist_get_root ();
646
647 g_get_current_time (&now);
648 node_foreach_child_data (node, node_reset_update_counter, &now);
649 }
650
651 FeedList *
feedlist_create(void)652 feedlist_create (void)
653 {
654 return FEEDLIST (g_object_new (FEEDLIST_TYPE, NULL));
655 }
656