Fix missing translation
[geeqie.git] / src / collect-io.cc
1 /*
2  * Copyright (C) 2004 John Ellis
3  * Copyright (C) 2008 - 2016 The Geeqie Team
4  *
5  * Author: John Ellis
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 along
18  * with this program; if not, write to the Free Software Foundation, Inc.,
19  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20  */
21
22 #include "collect-io.h"
23
24 #include <config.h>
25
26 #include "collect.h"
27 #include "debug.h"
28 #include "filedata.h"
29 #include "intl.h"
30 #include "layout-util.h"
31 #include "main-defines.h"
32 #include "secure-save.h"
33 #include "thumb.h"
34 #include "ui-fileops.h"
35
36 #define GQ_COLLECTION_MARKER "#" GQ_APPNAME
37
38 enum {
39         GQ_COLLECTION_FAIL_MIN =     300,
40         GQ_COLLECTION_FAIL_PERCENT = 98,
41         GQ_COLLECTION_READ_BUFSIZE = 4096
42 };
43
44 struct CollectManagerEntry;
45
46 static void collection_load_thumb_step(CollectionData *cd);
47 static gboolean collection_save_private(CollectionData *cd, const gchar *path);
48
49 static CollectManagerEntry *collect_manager_get_entry(const gchar *path);
50 static void collect_manager_entry_reset(CollectManagerEntry *entry);
51 static gint collect_manager_process_action(CollectManagerEntry *entry, gchar **path_ptr);
52
53 namespace
54 {
55
56 gboolean scan_geometry(gchar *buffer, GdkRectangle &window)
57 {
58         gint nx;
59         gint ny;
60         gint nw;
61         gint nh;
62
63         if (sscanf(buffer, "%d %d %d %d", &nx, &ny, &nw, &nh) != 4) return FALSE;
64
65         window.x = nx;
66         window.y = ny;
67         window.width = nw;
68         window.height = nh;
69
70         return TRUE;
71 }
72
73 }
74
75 static gboolean collection_load_private(CollectionData *cd, const gchar *path, CollectionLoadFlags flags)
76 {
77         gchar s_buf[GQ_COLLECTION_READ_BUFSIZE];
78         FILE *f;
79         gchar *pathl;
80         gboolean limit_failures = TRUE;
81         gboolean success = TRUE;
82         gboolean has_official_header = FALSE;
83         gboolean has_geometry_header = FALSE;
84         gboolean has_gqview_header   = FALSE;
85         gboolean need_header     = TRUE;
86         guint total = 0;
87         guint fail = 0;
88         gboolean changed = FALSE;
89         CollectManagerEntry *entry = nullptr;
90         guint flush = !!(flags & COLLECTION_LOAD_FLUSH);
91         guint append = !!(flags & COLLECTION_LOAD_APPEND);
92         guint only_geometry = !!(flags & COLLECTION_LOAD_GEOMETRY);
93         gboolean reading_extended_filename = FALSE;
94         gchar *buffer2;
95
96         if (!only_geometry)
97                 {
98                 collection_load_stop(cd);
99
100                 if (flush)
101                         collect_manager_flush();
102                 else
103                         entry = collect_manager_get_entry(path);
104
105                 if (!append)
106                         {
107                         g_list_free_full(cd->list, reinterpret_cast<GDestroyNotify>(collection_info_free));
108                         cd->list = nullptr;
109                         }
110                 }
111
112         if (!path && !cd->path) return FALSE;
113
114         if (!path) path = cd->path;
115
116         pathl = path_from_utf8(path);
117
118         DEBUG_1("collection load: append=%d flush=%d only_geometry=%d path=%s",
119                           append, flush, only_geometry, pathl);
120
121         /* load it */
122         f = fopen(pathl, "r");
123         g_free(pathl);
124         if (!f)
125                 {
126                 log_printf("Failed to open collection file: \"%s\"\n", path);
127                 return FALSE;
128                 }
129
130         GString *extended_filename_buffer = g_string_new(nullptr);
131         while (fgets(s_buf, sizeof(s_buf), f))
132                 {
133                 gchar *buf;
134                 gchar *p = s_buf;
135
136                 if (!reading_extended_filename)
137                         {
138                         /* Skip whitespaces and empty lines */
139                         while (*p && g_ascii_isspace(*p)) p++;
140                         if (*p == '\n' || *p == '\r') continue;
141
142                         /* Parse comments */
143                         if (*p == '#')
144                                 {
145                                 if (!need_header) continue;
146                                 if (g_ascii_strncasecmp(p, GQ_COLLECTION_MARKER, strlen(GQ_COLLECTION_MARKER)) == 0)
147                                         {
148                                         /* Looks like an official collection, allow unchecked input.
149                                          * All this does is allow adding files that may not exist,
150                                          * which is needed for the collection manager to work.
151                                          * Also unofficial files abort after too many invalid entries.
152                                          */
153                                         has_official_header = TRUE;
154                                         limit_failures = FALSE;
155                                         }
156                                 else if (strncmp(p, "#geometry:", 10 ) == 0 &&
157                                          scan_geometry(p + 10, cd->window))
158                                         {
159                                         has_geometry_header = TRUE;
160                                         cd->window_read = TRUE;
161                                         if (only_geometry) break;
162                                         }
163                                 else if (g_ascii_strncasecmp(p, "#GQview collection", strlen("#GQview collection")) == 0)
164                                         {
165                                         /* As 2008/04/15 there is no difference between our collection file format
166                                          * and GQview 2.1.5 collection file format so ignore failures as well. */
167                                         has_gqview_header = TRUE;
168                                         limit_failures = FALSE;
169                                         }
170                                 need_header = (!has_official_header && !has_gqview_header) || !has_geometry_header;
171                                 continue;
172                                 }
173
174                         if (only_geometry) continue;
175                         }
176
177                 /* Read filenames */
178                 /** @todo This is not safe! */
179                 /* Updated: anything within double quotes is considered a filename */
180                 if (!reading_extended_filename)
181                         {
182                         /* not yet reading filename */
183                         while (*p && *p != '"') p++;
184                         if (*p) p++;
185                         /* start of filename read */
186                         buf = p;
187                         while (*p && *p != '"') p++;
188                         if (p[0] != '"')
189                                 {
190                                 /* first part of extended filename */
191                                 g_string_append(extended_filename_buffer, buf);
192                                 reading_extended_filename = TRUE;
193                                 continue;
194                                 }
195                         }
196                 else
197                         {
198                         buf = p;
199                         while (*p && *p != '"') p++;
200                         if (p[0] != '"')
201                                 {
202                                 /* end of extended filename still not found */
203                                 g_string_append(extended_filename_buffer, buf);
204                                 continue;
205                                 }
206
207                         /* end of extended filename found */
208                         g_string_append_len(extended_filename_buffer, buf, p - buf);
209                         reading_extended_filename = FALSE;
210                         }
211
212                 if (extended_filename_buffer->len > 0)
213                         {
214                         buffer2 = g_strdup(extended_filename_buffer->str);
215                         g_string_erase(extended_filename_buffer, 0, -1);
216                         }
217                 else
218                         {
219                         *p = 0;
220                         buffer2 = g_strdup(buf);
221                         }
222
223                 if (*buffer2)
224                         {
225                         gboolean valid;
226
227                         if (!flush)
228                                 changed |= collect_manager_process_action(entry, &buffer2);
229
230                         valid = (buffer2[0] == G_DIR_SEPARATOR && collection_add_check(cd, file_data_new_simple(buffer2), FALSE, TRUE));
231                         if (!valid) DEBUG_1("collection invalid file: %s", buffer2);
232
233                         total++;
234                         if (!valid)
235                                 {
236                                 fail++;
237                                 if (limit_failures &&
238                                     fail > GQ_COLLECTION_FAIL_MIN &&
239                                     fail * 100 / total > GQ_COLLECTION_FAIL_PERCENT)
240                                         {
241                                         log_printf("%d invalid filenames in unofficial collection file, closing: %s\n", fail, path);
242                                         success = FALSE;
243                                         break;
244                                         }
245                                 }
246                         }
247                 g_free(buffer2);
248                 }
249
250         g_string_free(extended_filename_buffer, TRUE);
251
252         DEBUG_1("collection files: total = %d fail = %d official=%d gqview=%d geometry=%d",
253                           total, fail, has_official_header, has_gqview_header, has_geometry_header);
254
255         fclose(f);
256         if (only_geometry) return has_geometry_header;
257
258         if (!flush)
259                 {
260                 gchar *buf = nullptr;
261                 while (collect_manager_process_action(entry, &buf))
262                         {
263                         collection_add_check(cd, file_data_new_group(buf), FALSE, TRUE);
264                         changed = TRUE;
265                         g_free(buf);
266                         buf = nullptr;
267                         }
268                 }
269
270         cd->list = collection_list_sort(cd->list, cd->sort_method);
271
272         if (!flush && changed && success)
273                 collection_save_private(cd, path);
274
275         if (!flush)
276                 collect_manager_entry_reset(entry);
277
278         if (!append) cd->changed = FALSE;
279
280         return success;
281 }
282
283 gboolean collection_load(CollectionData *cd, const gchar *path, CollectionLoadFlags flags)
284 {
285         if (collection_load_private(cd, path, static_cast<CollectionLoadFlags>(flags | COLLECTION_LOAD_FLUSH)))
286                 {
287                 layout_recent_add_path(cd->path);
288                 return TRUE;
289                 }
290
291         return FALSE;
292 }
293
294 static void collection_load_thumb_do(CollectionData *cd)
295 {
296         GdkPixbuf *pixbuf;
297
298         if (!cd->thumb_loader || !g_list_find(cd->list, cd->thumb_info)) return;
299
300         pixbuf = thumb_loader_get_pixbuf(cd->thumb_loader);
301         collection_info_set_thumb(cd->thumb_info, pixbuf);
302         g_object_unref(pixbuf);
303
304         if (cd->info_updated_func) cd->info_updated_func(cd, cd->thumb_info, cd->info_updated_data);
305 }
306
307 static void collection_load_thumb_error_cb(ThumbLoader *, gpointer data)
308 {
309         auto cd = static_cast<CollectionData *>(data);
310
311         collection_load_thumb_do(cd);
312         collection_load_thumb_step(cd);
313 }
314
315 static void collection_load_thumb_done_cb(ThumbLoader *, gpointer data)
316 {
317         auto cd = static_cast<CollectionData *>(data);
318
319         collection_load_thumb_do(cd);
320         collection_load_thumb_step(cd);
321 }
322
323 static void collection_load_thumb_step(CollectionData *cd)
324 {
325         GList *work;
326         CollectInfo *ci;
327
328         if (!cd->list)
329                 {
330                 collection_load_stop(cd);
331                 return;
332                 }
333
334         work = cd->list;
335         ci = static_cast<CollectInfo *>(work->data);
336         work = work->next;
337         /* find first unloaded thumb */
338         while (work && ci->pixbuf)
339                 {
340                 ci = static_cast<CollectInfo *>(work->data);
341                 work = work->next;
342                 }
343
344         if (!ci || ci->pixbuf)
345                 {
346                 /* done */
347                 collection_load_stop(cd);
348
349                 /* send a NULL CollectInfo to notify end */
350                 if (cd->info_updated_func) cd->info_updated_func(cd, nullptr, cd->info_updated_data);
351
352                 return;
353                 }
354
355         /* setup loader and call it */
356         cd->thumb_info = ci;
357         thumb_loader_free(cd->thumb_loader);
358         cd->thumb_loader = thumb_loader_new(options->thumbnails.max_width, options->thumbnails.max_height);
359         thumb_loader_set_callbacks(cd->thumb_loader,
360                                    collection_load_thumb_done_cb,
361                                    collection_load_thumb_error_cb,
362                                    nullptr,
363                                    cd);
364
365         /* start it */
366         if (!thumb_loader_start(cd->thumb_loader, ci->fd))
367                 {
368                 /* error, handle it, do next */
369                 DEBUG_1("error loading thumb for %s", ci->fd->path);
370                 collection_load_thumb_do(cd);
371                 collection_load_thumb_step(cd);
372                 }
373 }
374
375 void collection_load_thumb_idle(CollectionData *cd)
376 {
377         if (!cd->thumb_loader) collection_load_thumb_step(cd);
378 }
379
380 gboolean collection_load_begin(CollectionData *cd, const gchar *path, CollectionLoadFlags flags)
381 {
382         if (!collection_load(cd, path, flags)) return FALSE;
383
384         collection_load_thumb_idle(cd);
385
386         return TRUE;
387 }
388
389 void collection_load_stop(CollectionData *cd)
390 {
391         if (!cd->thumb_loader) return;
392
393         thumb_loader_free(cd->thumb_loader);
394         cd->thumb_loader = nullptr;
395 }
396
397 static gboolean collection_save_private(CollectionData *cd, const gchar *path)
398 {
399         SecureSaveInfo *ssi;
400         GList *work;
401         gchar *pathl;
402
403         if (!path && !cd->path) return FALSE;
404
405         if (!path)
406                 {
407                 path = cd->path;
408                 }
409
410
411         pathl = path_from_utf8(path);
412         ssi = secure_open(pathl);
413         g_free(pathl);
414         if (!ssi)
415                 {
416                 log_printf(_("failed to open collection (write) \"%s\"\n"), path);
417                 return FALSE;
418                 }
419
420         secure_fprintf(ssi, "%s collection\n", GQ_COLLECTION_MARKER);
421         secure_fprintf(ssi, "#created with %s version %s\n", GQ_APPNAME, VERSION);
422
423         collection_update_geometry(cd);
424         if (cd->window_read)
425                 {
426                 secure_fprintf(ssi, "#geometry: %d %d %d %d\n", cd->window.x, cd->window.y, cd->window.width, cd->window.height);
427                 }
428
429         work = cd->list;
430         while (work && secsave_errno == SS_ERR_NONE)
431                 {
432                 auto ci = static_cast<CollectInfo *>(work->data);
433                 secure_fprintf(ssi, "\"%s\"\n", ci->fd->path);
434                 work = work->next;
435                 }
436
437         secure_fprintf(ssi, "#end\n");
438
439         if (secure_close(ssi))
440                 {
441                 log_printf(_("error saving collection file: %s\nerror: %s\n"), path,
442                             secsave_strerror(secsave_errno));
443                 return FALSE;
444                 }
445
446         if (!cd->path || strcmp(path, cd->path) != 0)
447                 {
448                 gchar *buf = cd->path;
449                 cd->path = g_strdup(path);
450                 path = cd->path;
451                 g_free(buf);
452
453                 g_free(cd->name);
454                 cd->name = g_strdup(filename_from_path(cd->path));
455
456                 collection_path_changed(cd);
457                 }
458
459         cd->changed = FALSE;
460
461         return TRUE;
462 }
463
464 gboolean collection_save(CollectionData *cd, const gchar *path)
465 {
466         if (collection_save_private(cd, path))
467                 {
468                 layout_recent_add_path(cd->path);
469                 return TRUE;
470                 }
471
472         return FALSE;
473 }
474
475 gboolean collection_load_only_geometry(CollectionData *cd, const gchar *path)
476 {
477         return collection_load(cd, path, COLLECTION_LOAD_GEOMETRY);
478 }
479
480
481 /*
482  *-------------------------------------------------------------------
483  * collection manager
484  *-------------------------------------------------------------------
485  */
486
487 enum {
488         COLLECT_MANAGER_ACTIONS_PER_IDLE = 1000,
489         COLLECT_MANAGER_FLUSH_DELAY =      10000
490 };
491
492 struct CollectManagerEntry
493 {
494         gchar *path;
495         GList *add_list;
496         GHashTable *oldpath_hash;
497         GHashTable *newpath_hash;
498         gboolean empty;
499 };
500
501 enum CollectManagerType {
502         COLLECTION_MANAGER_UPDATE,
503         COLLECTION_MANAGER_ADD,
504         COLLECTION_MANAGER_REMOVE
505 };
506
507 struct CollectManagerAction
508 {
509         gchar *oldpath;
510         gchar *newpath;
511
512         CollectManagerType type;
513
514         gint ref;
515 };
516
517
518 static GList *collection_manager_entry_list = nullptr;
519 static GList *collection_manager_action_list = nullptr;
520 static GList *collection_manager_action_tail = nullptr;
521 static guint collection_manager_timer_id = 0; /* event source id */
522
523
524 static CollectManagerAction *collect_manager_action_new(const gchar *oldpath, const gchar *newpath,
525                                                         CollectManagerType type)
526 {
527         CollectManagerAction *action;
528
529         action = g_new0(CollectManagerAction, 1);
530         action->ref = 1;
531
532         action->oldpath = g_strdup(oldpath);
533         action->newpath = g_strdup(newpath);
534
535         action->type = type;
536
537         return action;
538 }
539
540 static void collect_manager_action_ref(CollectManagerAction *action)
541 {
542         action->ref++;
543 }
544
545 static void collect_manager_action_unref(CollectManagerAction *action)
546 {
547         action->ref--;
548
549         if (action->ref > 0) return;
550
551         g_free(action->oldpath);
552         g_free(action->newpath);
553         g_free(action);
554 }
555
556 static void collect_manager_entry_free_data(CollectManagerEntry *entry)
557 {
558         g_list_free_full(entry->add_list, reinterpret_cast<GDestroyNotify>(collect_manager_action_unref));
559         if (g_hash_table_size(entry->oldpath_hash) > 0)
560                 g_hash_table_destroy(entry->oldpath_hash);
561         else
562                 g_hash_table_unref(entry->oldpath_hash);
563         if (g_hash_table_size(entry->newpath_hash) > 0)
564                 g_hash_table_destroy(entry->newpath_hash);
565         else
566                 g_hash_table_unref(entry->newpath_hash);
567 }
568
569 static void collect_manager_entry_init_data(CollectManagerEntry *entry)
570 {
571         entry->add_list = nullptr;
572         entry->oldpath_hash = g_hash_table_new_full(g_str_hash, g_str_equal, nullptr, reinterpret_cast<GDestroyNotify>(collect_manager_action_unref));
573         entry->newpath_hash = g_hash_table_new(g_str_hash, g_str_equal);
574         entry->empty = TRUE;
575
576 }
577
578 static CollectManagerEntry *collect_manager_entry_new(const gchar *path)
579 {
580         CollectManagerEntry *entry;
581
582         entry = g_new0(CollectManagerEntry, 1);
583         entry->path = g_strdup(path);
584         collect_manager_entry_init_data(entry);
585
586         collection_manager_entry_list = g_list_append(collection_manager_entry_list, entry);
587
588         return entry;
589 }
590
591
592 static void collect_manager_entry_free(CollectManagerEntry *entry)
593 {
594         collection_manager_entry_list = g_list_remove(collection_manager_entry_list, entry);
595
596         collect_manager_entry_free_data(entry);
597
598         g_free(entry->path);
599         g_free(entry);
600 }
601
602 static void collect_manager_entry_reset(CollectManagerEntry *entry)
603 {
604         collect_manager_entry_free_data(entry);
605         collect_manager_entry_init_data(entry);
606 }
607
608 static CollectManagerEntry *collect_manager_get_entry(const gchar *path)
609 {
610         GList *work;
611
612         work = collection_manager_entry_list;
613         while (work)
614                 {
615                 CollectManagerEntry *entry;
616
617                 entry = static_cast<CollectManagerEntry *>(work->data);
618                 work = work->next;
619                 if (strcmp(entry->path, path) == 0)
620                         {
621                         return entry;
622                         }
623                 }
624         return nullptr;
625
626 }
627
628 static void collect_manager_entry_add_action(CollectManagerEntry *entry, CollectManagerAction *action)
629 {
630
631         CollectManagerAction *orig_action;
632
633         entry->empty = FALSE;
634
635         if (action->oldpath == nullptr)
636                 {
637                 /* add file */
638                 if (action->newpath == nullptr)
639                         {
640                         return;
641                         }
642
643                 orig_action = static_cast<CollectManagerAction *>(g_hash_table_lookup(entry->newpath_hash, action->newpath));
644                 if (orig_action)
645                         {
646                         /* target already exists */
647                         log_printf("collection manager failed to add another action for target %s in collection %s\n",
648                                 action->newpath, entry->path);
649                         return;
650                         }
651                 entry->add_list = g_list_append(entry->add_list, action);
652                 g_hash_table_insert(entry->newpath_hash, action->newpath, action);
653                 collect_manager_action_ref(action);
654                 return;
655                 }
656
657         orig_action = static_cast<CollectManagerAction *>(g_hash_table_lookup(entry->newpath_hash, action->oldpath));
658         if (orig_action)
659                 {
660                 /* new action with the same file */
661                 CollectManagerAction *new_action = collect_manager_action_new(orig_action->oldpath, action->newpath, action->type);
662
663                 if (new_action->oldpath)
664                         {
665                         g_hash_table_steal(entry->oldpath_hash, orig_action->oldpath);
666                         g_hash_table_insert(entry->oldpath_hash, new_action->oldpath, new_action);
667                         }
668                 else
669                         {
670                         GList *work = g_list_find(entry->add_list, orig_action);
671                         work->data = new_action;
672                         }
673
674                 g_hash_table_steal(entry->newpath_hash, orig_action->newpath);
675                 if (new_action->newpath)
676                         {
677                         g_hash_table_insert(entry->newpath_hash, new_action->newpath, new_action);
678                         }
679                 collect_manager_action_unref(orig_action);
680                 return;
681                 }
682
683
684         orig_action = static_cast<CollectManagerAction *>(g_hash_table_lookup(entry->oldpath_hash, action->oldpath));
685         if (orig_action)
686                 {
687                 /* another action for the same source, ignore */
688                 log_printf("collection manager failed to add another action for source %s in collection %s\n",
689                         action->oldpath, entry->path);
690                 return;
691                 }
692
693         g_hash_table_insert(entry->oldpath_hash, action->oldpath, action);
694         if (action->newpath)
695                 {
696                 g_hash_table_insert(entry->newpath_hash, action->newpath, action);
697                 }
698         collect_manager_action_ref(action);
699 }
700
701 static gboolean collect_manager_process_action(CollectManagerEntry *entry, gchar **path_ptr)
702 {
703         gchar *path = *path_ptr;
704         CollectManagerAction *action;
705
706         if (path == nullptr)
707                 {
708                 /* get new files */
709                 if (entry->add_list)
710                         {
711                         action = static_cast<CollectManagerAction *>(entry->add_list->data);
712                         g_assert(action->oldpath == nullptr);
713                         entry->add_list = g_list_remove(entry->add_list, action);
714                         path = g_strdup(action->newpath);
715                         g_hash_table_remove(entry->newpath_hash, path);
716                         collect_manager_action_unref(action);
717                         }
718                 *path_ptr = path;
719                 return (path != nullptr);
720                 }
721
722         action = static_cast<CollectManagerAction *>(g_hash_table_lookup(entry->oldpath_hash, path));
723
724         if (action)
725                 {
726                 strcpy(*path_ptr, action->newpath);
727                 return TRUE;
728                 }
729
730         return FALSE; /* no change */
731 }
732
733 static void collect_manager_refresh()
734 {
735         GList *list;
736         GList *work;
737         FileData *dir_fd;
738
739         dir_fd = file_data_new_dir(get_collections_dir());
740         filelist_read(dir_fd, &list, nullptr);
741         file_data_unref(dir_fd);
742
743         work = collection_manager_entry_list;
744         while (work && list)
745                 {
746                 CollectManagerEntry *entry;
747                 GList *list_step;
748
749                 entry = static_cast<CollectManagerEntry *>(work->data);
750                 work = work->next;
751
752                 list_step = list;
753                 while (list_step && entry)
754                         {
755                         FileData *fd;
756
757                         fd = static_cast<FileData *>(list_step->data);
758                         list_step = list_step->next;
759
760                         if (strcmp(fd->path, entry->path) == 0)
761                                 {
762                                 list = g_list_remove(list, fd);
763                                 file_data_unref(fd);
764
765                                 entry = nullptr;
766                                 }
767                         else
768                                 {
769                                 collect_manager_entry_free(entry);
770
771                                 entry = nullptr;
772                                 }
773                         }
774                 }
775
776         work = list;
777         while (work)
778                 {
779                 FileData *fd;
780
781                 fd = static_cast<FileData *>(work->data);
782                 work = work->next;
783
784                 collect_manager_entry_new(fd->path);
785                 }
786
787         filelist_free(list);
788 }
789
790 static void collect_manager_process_actions(gint max)
791 {
792         if (collection_manager_action_list) DEBUG_1("collection manager processing actions");
793
794         while (collection_manager_action_list != nullptr && max > 0)
795                 {
796                 CollectManagerAction *action;
797                 GList *work;
798
799                 action = static_cast<CollectManagerAction *>(collection_manager_action_list->data);
800                 work = collection_manager_entry_list;
801                 while (work)
802                         {
803                         CollectManagerEntry *entry;
804
805                         entry = static_cast<CollectManagerEntry *>(work->data);
806                         work = work->next;
807
808                         if (action->type == COLLECTION_MANAGER_UPDATE)
809                                 {
810                                 collect_manager_entry_add_action(entry, action);
811                                 }
812                         else if (action->oldpath && action->newpath &&
813                                  strcmp(action->newpath, entry->path) == 0)
814                                 {
815                                 /* convert action to standard add format */
816                                 g_free(action->newpath);
817                                 if (action->type == COLLECTION_MANAGER_ADD)
818                                         {
819                                         action->newpath = action->oldpath;
820                                         action->oldpath = nullptr;
821                                         }
822                                 else if (action->type == COLLECTION_MANAGER_REMOVE)
823                                         {
824                                         action->newpath = nullptr;
825                                         }
826                                 collect_manager_entry_add_action(entry, action);
827                                 }
828
829                         max--;
830                         }
831
832                 if (action->type != COLLECTION_MANAGER_UPDATE &&
833                     action->oldpath && action->newpath)
834                         {
835                         log_printf("collection manager failed to %s %s for collection %s\n",
836                                 (action->type == COLLECTION_MANAGER_ADD) ? "add" : "remove",
837                                 action->oldpath, action->newpath);
838                         }
839
840                 if (collection_manager_action_tail == collection_manager_action_list)
841                         {
842                         collection_manager_action_tail = nullptr;
843                         }
844                 collection_manager_action_list = g_list_remove(collection_manager_action_list, action);
845                 collect_manager_action_unref(action);
846                 }
847 }
848
849 static gboolean collect_manager_process_entry(CollectManagerEntry *entry)
850 {
851         CollectionData *cd;
852
853         if (entry->empty) return FALSE;
854
855         cd = collection_new(entry->path);
856         (void) collection_load_private(cd, entry->path, COLLECTION_LOAD_NONE);
857
858         collection_unref(cd);
859
860         return TRUE;
861 }
862
863 static gboolean collect_manager_process_entry_list()
864 {
865         GList *work;
866
867         work = collection_manager_entry_list;
868         while (work)
869                 {
870                 CollectManagerEntry *entry;
871
872                 entry = static_cast<CollectManagerEntry *>(work->data);
873                 work = work->next;
874                 if (collect_manager_process_entry(entry)) return TRUE;
875                 }
876
877         return FALSE;
878 }
879
880
881
882 static gboolean collect_manager_process_cb(gpointer)
883 {
884         if (collection_manager_action_list) collect_manager_refresh();
885         collect_manager_process_actions(COLLECT_MANAGER_ACTIONS_PER_IDLE);
886         if (collection_manager_action_list) return G_SOURCE_CONTINUE;
887
888         if (collect_manager_process_entry_list()) return G_SOURCE_CONTINUE;
889
890         DEBUG_1("collection manager is up to date");
891         return G_SOURCE_REMOVE;
892 }
893
894 static gboolean collect_manager_timer_cb(gpointer)
895 {
896         DEBUG_1("collection manager timer expired");
897
898         g_idle_add_full(G_PRIORITY_LOW, collect_manager_process_cb, nullptr, nullptr);
899
900         collection_manager_timer_id = 0;
901         return FALSE;
902 }
903
904 static void collect_manager_timer_push(gint stop)
905 {
906         if (collection_manager_timer_id)
907                 {
908                 if (!stop) return;
909
910                 g_source_remove(collection_manager_timer_id);
911                 collection_manager_timer_id = 0;
912                 }
913
914         if (!stop)
915                 {
916                 collection_manager_timer_id = g_timeout_add(COLLECT_MANAGER_FLUSH_DELAY,
917                                                             collect_manager_timer_cb, nullptr);
918                 DEBUG_1("collection manager timer started");
919                 }
920 }
921
922 static void collect_manager_add_action(CollectManagerAction *action)
923 {
924         if (!action) return;
925
926         /* we keep track of the list's tail to keep this a n(1) operation */
927
928         if (collection_manager_action_tail)
929                 {
930                 collection_manager_action_tail = g_list_append(collection_manager_action_tail, action);
931                 collection_manager_action_tail = collection_manager_action_tail->next;
932                 }
933         else
934                 {
935                 collection_manager_action_list = g_list_append(collection_manager_action_list, action);
936                 collection_manager_action_tail = collection_manager_action_list;
937                 }
938
939         collect_manager_timer_push(FALSE);
940 }
941
942 void collect_manager_moved(FileData *fd)
943 {
944         CollectManagerAction *action;
945         const gchar *oldpath = fd->change->source;
946         const gchar *newpath = fd->change->dest;
947
948         action = collect_manager_action_new(oldpath, newpath, COLLECTION_MANAGER_UPDATE);
949         collect_manager_add_action(action);
950 }
951
952 void collect_manager_add(FileData *fd, const gchar *collection)
953 {
954         CollectManagerAction *action;
955         CollectWindow *cw;
956
957         if (!fd || !collection) return;
958
959         cw = collection_window_find_by_path(collection);
960         if (cw)
961                 {
962                 if (collection_list_find_fd(cw->cd->list, fd) == nullptr)
963                         {
964                         collection_add(cw->cd, fd, FALSE);
965                         }
966                 return;
967                 }
968
969         action = collect_manager_action_new(fd->path, collection, COLLECTION_MANAGER_ADD);
970         collect_manager_add_action(action);
971 }
972
973 void collect_manager_remove(FileData *fd, const gchar *collection)
974 {
975         CollectManagerAction *action;
976         CollectWindow *cw;
977
978         if (!fd || !collection) return;
979
980         cw = collection_window_find_by_path(collection);
981         if (cw)
982                 {
983                 while (collection_remove(cw->cd, fd));
984                 return;
985                 }
986
987         action = collect_manager_action_new(fd->path, collection, COLLECTION_MANAGER_REMOVE);
988         collect_manager_add_action(action);
989 }
990
991 void collect_manager_flush()
992 {
993         collect_manager_timer_push(TRUE);
994
995         DEBUG_1("collection manager flushing");
996         while (collect_manager_process_cb(nullptr));
997 }
998
999 void collect_manager_notify_cb(FileData *fd, NotifyType type, gpointer)
1000 {
1001         if (!(type & NOTIFY_CHANGE) || !fd->change) return;
1002
1003         DEBUG_1("Notify collect_manager: %s %04x", fd->path, type);
1004         switch (fd->change->type)
1005                 {
1006                 case FILEDATA_CHANGE_MOVE:
1007                         collect_manager_moved(fd);
1008                         break;
1009                 case FILEDATA_CHANGE_COPY:
1010                         break;
1011                 case FILEDATA_CHANGE_RENAME:
1012                         collect_manager_moved(fd);
1013                         break;
1014                 case FILEDATA_CHANGE_DELETE:
1015                 case FILEDATA_CHANGE_UNSPECIFIED:
1016                 case FILEDATA_CHANGE_WRITE_METADATA:
1017                         break;
1018                 }
1019 }
1020
1021 static gint collection_manager_sort_cb(gconstpointer a, gconstpointer b)
1022 {
1023         auto char_a = static_cast<const gchar *>(a);
1024         auto char_b = static_cast<const gchar *>(b);
1025
1026         return g_strcmp0(char_a, char_b);
1027 }
1028
1029 /**
1030  * @brief Creates sorted list of collections
1031  * @param[out] names_exc sorted list of collections names excluding extension
1032  * @param[out] names_inc sorted list of collections names including extension
1033  * @param[out] paths sorted list of collection paths
1034  *
1035  * Lists of type gchar.
1036  * Used lists must be freed with string_list_free()
1037  */
1038 void collect_manager_list(GList **names_exc, GList **names_inc, GList **paths)
1039 {
1040         FileData *dir_fd;
1041         GList *list = nullptr;
1042         gchar *name;
1043         FileData *fd;
1044         gchar *filename;
1045
1046         if (names_exc == nullptr && names_inc == nullptr && paths == nullptr)
1047                 {
1048                 return;
1049                 }
1050
1051         dir_fd = file_data_new_dir((get_collections_dir()));
1052
1053         filelist_read(dir_fd, &list, nullptr);
1054
1055         while (list)
1056                 {
1057                 fd = static_cast<FileData *>(list->data);
1058                 filename = g_strdup(filename_from_path(fd->path));
1059
1060                 if (file_extension_match(filename, GQ_COLLECTION_EXT))
1061                         {
1062                         name = remove_extension_from_path(filename);
1063
1064                         if (names_exc != nullptr)
1065                                 {
1066                                 *names_exc = g_list_insert_sorted(*names_exc, g_strdup(name),
1067                                                                                         collection_manager_sort_cb);
1068                                 *names_exc = g_list_first(*names_exc);
1069                                 }
1070                         if (names_inc != nullptr)
1071                                 {
1072                                 *names_inc = g_list_insert_sorted(*names_inc,filename,
1073                                                                                         collection_manager_sort_cb);
1074                                 *names_inc = g_list_first(*names_inc);
1075                                 }
1076                         if (paths != nullptr)
1077                                 {
1078                                 *paths = g_list_insert_sorted(*paths, g_strdup(fd->path),
1079                                                                                         collection_manager_sort_cb);
1080                                 *paths = g_list_first(*paths);
1081                                 }
1082                         g_free(name);
1083                         }
1084                 list = list->next;
1085                 g_free(filename);
1086                 }
1087
1088         filelist_free(list);
1089 }
1090 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */