diff --git a/data/darktableconfig.xml.in b/data/darktableconfig.xml.in
index 6e108a7be2f7..dd6e1724bf1a 100644
--- a/data/darktableconfig.xml.in
+++ b/data/darktableconfig.xml.in
@@ -3456,6 +3456,13 @@
look for updated XMP files on startup
check file modification times of all XMP files on startup to check if any got updated in the meantime
+
+ xmp_full_scan_next_time
+ bool
+ false
+ scan all XMP files for changes on next startup
+
+
colorlabel/red
string
diff --git a/src/common/exif.cc b/src/common/exif.cc
index 8537b0eb338d..caef8c3397b4 100644
--- a/src/common/exif.cc
+++ b/src/common/exif.cc
@@ -5880,6 +5880,7 @@ gboolean dt_exif_xmp_write(const dt_imgid_t imgid,
fprintf(fout, "%s", xml_header);
fprintf(fout, "%s", xmpPacket.c_str());
fclose(fout);
+ dt_diratime_action(filename, "update");
}
else
{
diff --git a/src/control/control.c b/src/control/control.c
index bc54eba2ff12..ae8c7a918ddb 100644
--- a/src/control/control.c
+++ b/src/control/control.c
@@ -819,6 +819,90 @@ void dt_control_set_mouse_over_id(const dt_imgid_t imgid)
dt_pthread_mutex_unlock(&dc->global_mutex);
}
+time_t dt_diratime_action(const char *dir_path, const char *action)
+{
+ time_t timestamp = 0;
+ gchar *_dir = g_strdup(dir_path);
+ size_t name_len = strlen(dir_path);
+ const char *ext = dir_path + name_len - 4;
+ if (((!g_strcmp0(action, "update") && (!g_strcmp0(ext, ".xmp") || !g_strcmp0(ext, ".XMP")))
+ || !g_strcmp0(action, "delete"))
+ && name_len > 4)
+ {
+ size_t len = strlen(dir_path);
+ const char *c = dir_path + len;
+ while((c > dir_path) && ((*c) != G_DIR_SEPARATOR)) c--;
+ size_t vers_len = c - dir_path + 1;
+ g_strlcpy(_dir, dir_path, vers_len + 1);
+ }
+
+ GError *error = NULL;
+ GFile *_g_dir = g_file_new_for_path(_dir);
+ GFileInfo *info = g_file_query_info(_g_dir,
+ G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","
+ G_FILE_ATTRIBUTE_STANDARD_TYPE,
+ G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL);
+ const char *dirname = g_file_info_get_attribute_string(info, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME);
+
+ const char *dir_mark = g_strconcat(_dir, dirname, ".dt", NULL);
+ time_t dir_mark_time = 0;
+
+ GFile *_g_dir_mark = g_file_new_for_path(dir_mark);
+ if (!g_strcmp0(action, "create"))
+ {
+ if(!g_file_test(dir_mark, G_FILE_TEST_EXISTS))
+ {
+ GFileEnumerator *dir_files = g_file_enumerate_children(_g_dir,
+ G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","
+ G_FILE_ATTRIBUTE_TIME_MODIFIED ","
+ G_FILE_ATTRIBUTE_STANDARD_TYPE,
+ G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, &error);
+ if(dir_files)
+ {
+ while((info = g_file_enumerator_next_file(dir_files, NULL, &error)))
+ {
+ const char *filename = g_file_info_get_display_name(info);
+ if(!filename) continue;
+ const GFileType filetype = g_file_info_get_attribute_uint32(info, G_FILE_ATTRIBUTE_STANDARD_TYPE);
+ if(filetype == G_FILE_TYPE_REGULAR)
+ {
+ name_len = strlen(filename);
+ ext = filename + name_len - 4;
+ if ((strcmp(ext, ".xmp") == 0 || strcmp(ext, ".XMP") == 0) && name_len > 4)
+ {
+ time_t _timestamp = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
+ if (_timestamp > dir_mark_time)
+ dir_mark_time = _timestamp;
+ }
+ }
+ }
+ }
+ GFileOutputStream *out = g_file_replace(_g_dir_mark, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION, NULL, &error);
+ g_object_unref(out);
+ info = g_file_query_info(_g_dir_mark,
+ G_FILE_ATTRIBUTE_TIME_MODIFIED,
+ G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, &error);
+ g_file_set_attribute_uint64(_g_dir_mark, G_FILE_ATTRIBUTE_TIME_MODIFIED, dir_mark_time, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,FALSE,&error);
+ g_object_unref(dir_files);
+ }
+ }
+ else if (!g_strcmp0(action, "update") || !g_strcmp0(action, "delete"))
+ {
+ GFileOutputStream *out = g_file_replace(_g_dir_mark, NULL, FALSE,
+ G_FILE_CREATE_REPLACE_DESTINATION, NULL, &error);
+ g_object_unref(out);
+ }
+ info = g_file_query_info(_g_dir_mark,
+ G_FILE_ATTRIBUTE_TIME_MODIFIED,
+ G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, &error);
+ timestamp = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
+ g_object_unref(info);
+ g_object_unref(_g_dir_mark);
+ g_object_unref(_g_dir);
+
+ return timestamp;
+}
+
// clang-format off
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
diff --git a/src/control/control.h b/src/control/control.h
index a6fb139231de..74320754206f 100644
--- a/src/control/control.h
+++ b/src/control/control.h
@@ -256,6 +256,10 @@ gboolean dt_control_running(void);
dt_imgid_t dt_control_get_mouse_over_id(void);
void dt_control_set_mouse_over_id(const dt_imgid_t value);
+/** actions on directory access mark file.
+ 'action' must be one of "create", "update", "delete" */
+time_t dt_diratime_action(const char *dir_path, const char *action);
+
G_END_DECLS
// clang-format off
diff --git a/src/control/crawler.c b/src/control/crawler.c
index 659cd5206dc8..2eba6a6da05f 100644
--- a/src/control/crawler.c
+++ b/src/control/crawler.c
@@ -22,6 +22,7 @@
#include
#include
+#include "common/collection.h"
#include "common/darktable.h"
#include "common/database.h"
#include "common/debug.h"
@@ -53,12 +54,20 @@ typedef enum dt_control_crawler_cols_t
DT_CONTROL_CRAWLER_NUM_COLS
} dt_control_crawler_cols_t;
+typedef enum xmp_condition
+{
+ DT_XMP_CONDITION_MISSING = 0,
+ DT_XMP_CONDITION_CHANGED,
+ DT_XMP_CONDITION_NEW
+} xmp_condition;
+
typedef struct dt_control_crawler_result_t
{
- dt_imgid_t id;
+ dt_imgid_t id, version, count;
time_t timestamp_xmp;
time_t timestamp_db;
char *image_path, *xmp_path;
+ xmp_condition condition;
} dt_control_crawler_result_t;
static void _free_crawler_result(dt_control_crawler_result_t *entry)
@@ -110,6 +119,16 @@ static void _set_modification_time(char *filename,
#define FAST_UPDATE 0.2
#define SLOW_UPDATE 1.0
+GList *_get_list_dir(void);
+GList *_get_list_xmp(void);
+
+typedef struct _dir_
+{
+ dt_filmid_t id;
+ char *dir_path;
+} _dir_;
+
+
GList *dt_control_crawler_run(void)
{
sqlite3_stmt *stmt, *inner_stmt;
@@ -127,22 +146,6 @@ GList *dt_control_crawler_run(void)
sqlite3_finalize(stmt);
}
- // clang-format off
- sqlite3_prepare_v2(dt_database_get(darktable.db),
- "SELECT i.id, write_timestamp, version,"
- " folder || '" G_DIR_SEPARATOR_S "' || filename, flags"
- " FROM main.images i, main.film_rolls f"
- " ON i.film_id = f.id"
- " ORDER BY f.id, filename",
- -1, &stmt, NULL);
- sqlite3_prepare_v2(dt_database_get(darktable.db),
- "UPDATE main.images SET flags = ?1 WHERE id = ?2", -1,
- &inner_stmt, NULL);
- // clang-format on
-
- // let's wrap this into a transaction, it might make it a little faster.
- dt_database_start_transaction(darktable.db);
-
int image_count = 0;
const double start_time = dt_get_wtime();
// set the "previous update" time to 10ms after a notional previous
@@ -150,152 +153,313 @@ GList *dt_control_crawler_run(void)
// appear when done with zero delay) while minimizing the delay
double last_time = start_time - (FAST_UPDATE-0.01);
- while(sqlite3_step(stmt) == SQLITE_ROW)
+ typedef struct _xmp0_
{
- const dt_imgid_t id = sqlite3_column_int(stmt, 0);
- const time_t timestamp = sqlite3_column_int64(stmt, 1);
- const int version = sqlite3_column_int(stmt, 2);
- const gchar *image_path = (char *)sqlite3_column_text(stmt, 3);
- int flags = sqlite3_column_int(stmt, 4);
- ++image_count;
-
- // update the progress message - five times per second for first four seconds, then once per second
- const double curr_time = dt_get_wtime();
- if(curr_time >= last_time + ((curr_time - start_time > 4.0) ? SLOW_UPDATE : FAST_UPDATE))
- {
- const double fraction = image_count / (double)total_images;
- darktable_splash_screen_set_progress_percent(_("checking for updated sidecar files (%d%%)"),
- fraction,
- curr_time - start_time);
- last_time = curr_time;
- }
+ dt_imgid_t id;
+ char *image_path;
+ } _xmp0_;
- // if the image is missing we ignore it.
- if(!g_file_test(image_path, G_FILE_TEST_EXISTS))
- {
- dt_print(DT_DEBUG_CONTROL, "[crawler] `%s' (id: %d) is missing", image_path, id);
- continue;
- }
+ GList *_xmp_list = _get_list_xmp(); // get xmp files list
+ GList *_not_edited_list = NULL; // list for images w/o xmp file vers.0
+
+ GList *_dir_list = _get_list_dir();
- // no need to look for xmp files if none get written anyway.
- if(look_for_xmp)
+ if(_dir_list)
+ {
+ for(GList *_dir_iter = _dir_list; _dir_iter; _dir_iter = g_list_next(_dir_iter))
{
- // construct the xmp filename for this image
- gchar xmp_path[PATH_MAX] = { 0 };
- g_strlcpy(xmp_path, image_path, sizeof(xmp_path));
- dt_image_path_append_version_no_db(version, xmp_path, sizeof(xmp_path));
- size_t len = strlen(xmp_path);
- if(len + 4 >= PATH_MAX) continue;
- xmp_path[len++] = '.';
- xmp_path[len++] = 'x';
- xmp_path[len++] = 'm';
- xmp_path[len++] = 'p';
- xmp_path[len] = '\0';
-
- // on Windows the encoding might not be UTF8
- gchar *xmp_path_locale = dt_util_normalize_path(xmp_path);
- int stat_res = -1;
-#ifdef _WIN32
- // UTF8 paths fail in this context, but converting to UTF16 works
- struct _stati64 statbuf;
- if(xmp_path_locale) // in Windows dt_util_normalize_path returns
- // NULL if file does not exist
+ // clang-format off
+ DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
+ "SELECT i.id, write_timestamp, version,"
+ " folder || '" G_DIR_SEPARATOR_S "' || filename, flags"
+ " FROM main.images i, main.film_rolls f"
+ " ON i.film_id = f.id"
+ " WHERE f.id = ?1"
+ " ORDER BY f.id, filename",
+ -1, &stmt, 0);
+ sqlite3_prepare_v2(dt_database_get(darktable.db),
+ "UPDATE main.images SET flags = ?1 WHERE id = ?2", -1,
+ &inner_stmt, NULL);
+ // clang-format on
+
+ _dir_ *dir_path = _dir_iter->data;
+ sqlite3_bind_int(stmt, 1, dir_path->id);
+
+ // let's wrap this into a transaction, it might make it a little faster.
+ dt_database_start_transaction(darktable.db);
+ while(sqlite3_step(stmt) == SQLITE_ROW)
{
- wchar_t *wfilename = g_utf8_to_utf16(xmp_path_locale, -1, NULL, NULL, NULL);
- stat_res = _wstati64(wfilename, &statbuf);
- g_free(wfilename);
- }
- #else
- struct stat statbuf;
- stat_res = stat(xmp_path_locale, &statbuf);
+ const dt_imgid_t id = sqlite3_column_int(stmt, 0);
+ const time_t timestamp = sqlite3_column_int64(stmt, 1);
+ const int version = sqlite3_column_int(stmt, 2);
+ const gchar *image_path = (char *)sqlite3_column_text(stmt, 3);
+ int flags = sqlite3_column_int(stmt, 4);
+ ++image_count;
+
+ // update the progress message - five times per second for first four seconds, then once per second
+ const double curr_time = dt_get_wtime();
+ if(curr_time >= last_time + ((curr_time - start_time > 4.0) ? SLOW_UPDATE : FAST_UPDATE))
+ {
+ const double fraction = image_count / (double)total_images;
+ darktable_splash_screen_set_progress_percent(_("checking for updated sidecar files (%d%%)"),
+ fraction,
+ curr_time - start_time);
+ last_time = curr_time;
+ }
+
+ // if the image is missing we suggest removing it.
+ if(!g_file_test(image_path, G_FILE_TEST_EXISTS))
+ {
+ dt_control_crawler_result_t *item = malloc(sizeof(dt_control_crawler_result_t));
+ item->id = id;
+ item->timestamp_xmp = 0;
+ item->timestamp_db = timestamp;
+ item->image_path = g_strdup(image_path);
+ item->xmp_path = g_strdup("");
+ item->condition = DT_XMP_CONDITION_MISSING;
+ item->version = version;
+ result = g_list_prepend(result, item);
+
+ dt_print(DT_DEBUG_CONTROL, "[crawler] `%s' (id: %d) is missing", image_path, id);
+ continue;
+ }
+
+ // no need to look for xmp files if none get written anyway.
+ if(look_for_xmp)
+ {
+ // construct the xmp filename for this image
+ gchar xmp_path[PATH_MAX] = { 0 };
+ g_strlcpy(xmp_path, image_path, sizeof(xmp_path));
+ dt_image_path_append_version_no_db(version, xmp_path, sizeof(xmp_path));
+ size_t len = strlen(xmp_path);
+ if(len + 4 >= PATH_MAX) continue;
+ xmp_path[len++] = '.';
+ xmp_path[len++] = 'x';
+ xmp_path[len++] = 'm';
+ xmp_path[len++] = 'p';
+ xmp_path[len] = '\0';
+
+ // elements existing in the db are removed from the list.
+ // only new elements will remain in the list.
+ for(GList *list_rec = _xmp_list; list_rec; list_rec = g_list_next(list_rec))
+ {
+ char *_xmp_file = list_rec->data;
+ if(strcmp(_xmp_file, xmp_path) == 0 ? TRUE : FALSE)
+ {
+ _xmp_list = g_list_delete_link(_xmp_list, list_rec);
+ break;
+ }
+ }
+
+ // on Windows the encoding might not be UTF8
+ gchar *xmp_path_locale = dt_util_normalize_path(xmp_path);
+ int stat_res = -1;
+#ifdef _WIN32
+ // UTF8 paths fail in this context, but converting to UTF16 works
+ struct _stati64 statbuf;
+ if(xmp_path_locale) // in Windows dt_util_normalize_path returns
+ // NULL if file does not exist
+ {
+ wchar_t *wfilename = g_utf8_to_utf16(xmp_path_locale, -1, NULL, NULL, NULL);
+ stat_res = _wstati64(wfilename, &statbuf);
+ g_free(wfilename);
+ }
+#else
+ struct stat statbuf;
+ stat_res = stat(xmp_path_locale, &statbuf);
#endif
- g_free(xmp_path_locale);
- if(stat_res) continue; // TODO: shall we report these?
-
- // step 1: check if the xmp is newer than our db entry
- if(timestamp + MAX_TIME_SKEW < statbuf.st_mtime)
- {
- dt_control_crawler_result_t *item = malloc(sizeof(dt_control_crawler_result_t));
- item->id = id;
- item->timestamp_xmp = statbuf.st_mtime;
- item->timestamp_db = timestamp;
- item->image_path = g_strdup(image_path);
- item->xmp_path = g_strdup(xmp_path);
-
- result = g_list_prepend(result, item);
- dt_print(DT_DEBUG_CONTROL,
- "[crawler] `%s' (id: %d) is a newer XMP file", xmp_path, id);
+ g_free(xmp_path_locale);
+ if(stat_res)
+ {
+ if(version)
+ {
+ dt_control_crawler_result_t *item = malloc(sizeof(dt_control_crawler_result_t));
+ item->id = id;
+ item->timestamp_xmp = 0;
+ item->timestamp_db = timestamp;
+ item->image_path = g_strdup(image_path);
+ item->xmp_path = g_strdup("");
+ item->condition = DT_XMP_CONDITION_MISSING;
+ item->version = version;
+ result = g_list_prepend(result, item);
+
+ dt_print(DT_DEBUG_CONTROL,
+ "[crawler] duplicate of `%s' (id: %d) removed from storage", image_path, id);
+ }
+ else
+ {
+ _xmp0_ *_item = malloc(sizeof(_xmp0_));
+ _item->id = id;
+ _item->image_path = g_strdup(image_path);
+ _not_edited_list = g_list_append(_not_edited_list, _item); // put image in "black" list
+ }
+ continue;
+ }
+
+ // maybe it's duplicate of image from "black" list
+ for(GList *list_rec = _not_edited_list; list_rec; list_rec = g_list_next(list_rec))
+ {
+ _xmp0_ *_item = list_rec->data;
+ if(strcmp(_item->image_path, image_path) == 0 ? TRUE : FALSE)
+ {
+ _not_edited_list = g_list_delete_link(_not_edited_list, list_rec); // remove image from "black" list
+ // and mark [version=0] for delete from db
+ dt_control_crawler_result_t *item = malloc(sizeof(dt_control_crawler_result_t));
+ item->id = _item->id;
+ item->timestamp_xmp = 0;
+ item->timestamp_db = timestamp;
+ item->image_path = g_strdup(_item->image_path);
+ item->xmp_path = g_strdup("");
+ item->condition = DT_XMP_CONDITION_MISSING;
+ item->version = 0;
+ result = g_list_prepend(result, item);
+
+ dt_print(DT_DEBUG_CONTROL,
+ "[crawler] duplicate of `%s' (id: %d) removed from storage", image_path, version);
+ break;
+ }
+ }
+
+ // step 1: check if the xmp is newer than our db entry
+ if(timestamp + MAX_TIME_SKEW < statbuf.st_mtime)
+ {
+ dt_control_crawler_result_t *item = malloc(sizeof(dt_control_crawler_result_t));
+ item->id = id;
+ item->timestamp_xmp = statbuf.st_mtime;
+ item->timestamp_db = timestamp;
+ item->image_path = g_strdup(image_path);
+ item->xmp_path = g_strdup(xmp_path);
+ item->condition = DT_XMP_CONDITION_CHANGED;
+ item->version = version;
+ result = g_list_prepend(result, item);
+ dt_print(DT_DEBUG_CONTROL,
+ "[crawler] `%s' (id: %d) is a newer XMP file", xmp_path, id);
+ }
+ // older timestamps are the case for all images after the db
+ // upgrade. better not report these
+ }
+
+ // step 2: check if the image has associated files (.txt, .wav)
+ size_t len = strlen(image_path);
+ const char *c = image_path + len;
+ while((c > image_path) && (*c != '.')) c--;
+ len = c - image_path + 1;
+
+ char *extra_path = calloc(len + 3 + 1, sizeof(char));
+ if(extra_path)
+ {
+ g_strlcpy(extra_path, image_path, len + 1);
+
+ extra_path[len] = 't';
+ extra_path[len + 1] = 'x';
+ extra_path[len + 2] = 't';
+ gboolean has_txt = g_file_test(extra_path, G_FILE_TEST_EXISTS);
+
+ if(!has_txt)
+ {
+ extra_path[len] = 'T';
+ extra_path[len + 1] = 'X';
+ extra_path[len + 2] = 'T';
+ has_txt = g_file_test(extra_path, G_FILE_TEST_EXISTS);
+ }
+
+ extra_path[len] = 'w';
+ extra_path[len + 1] = 'a';
+ extra_path[len + 2] = 'v';
+ gboolean has_wav = g_file_test(extra_path, G_FILE_TEST_EXISTS);
+
+ if(!has_wav)
+ {
+ extra_path[len] = 'W';
+ extra_path[len + 1] = 'A';
+ extra_path[len + 2] = 'V';
+ has_wav = g_file_test(extra_path, G_FILE_TEST_EXISTS);
+ }
+
+ // TODO: decide if we want to remove the flag for images that lost
+ // their extra file. currently we do (the else cases)
+ int new_flags = flags;
+ if(has_txt)
+ new_flags |= DT_IMAGE_HAS_TXT;
+ else
+ new_flags &= ~DT_IMAGE_HAS_TXT;
+ if(has_wav)
+ new_flags |= DT_IMAGE_HAS_WAV;
+ else
+ new_flags &= ~DT_IMAGE_HAS_WAV;
+ if(flags != new_flags)
+ {
+ sqlite3_bind_int(inner_stmt, 1, new_flags);
+ sqlite3_bind_int(inner_stmt, 2, id);
+ sqlite3_step(inner_stmt);
+ sqlite3_reset(inner_stmt);
+ sqlite3_clear_bindings(inner_stmt);
+ }
+
+ free(extra_path);
+ }
}
- // older timestamps are the case for all images after the db
- // upgrade. better not report these
- }
-
- // step 2: check if the image has associated files (.txt, .wav)
- size_t len = strlen(image_path);
- const char *c = image_path + len;
- while((c > image_path) && (*c != '.')) c--;
- len = c - image_path + 1;
+ dt_database_release_transaction(darktable.db);
- char *extra_path = calloc(len + 3 + 1, sizeof(char));
- if(extra_path)
- {
- g_strlcpy(extra_path, image_path, len + 1);
+ sqlite3_finalize(stmt);
+ sqlite3_finalize(inner_stmt);
- extra_path[len] = 't';
- extra_path[len + 1] = 'x';
- extra_path[len + 2] = 't';
- gboolean has_txt = g_file_test(extra_path, G_FILE_TEST_EXISTS);
-
- if(!has_txt)
- {
- extra_path[len] = 'T';
- extra_path[len + 1] = 'X';
- extra_path[len + 2] = 'T';
- has_txt = g_file_test(extra_path, G_FILE_TEST_EXISTS);
- }
-
- extra_path[len] = 'w';
- extra_path[len + 1] = 'a';
- extra_path[len + 2] = 'v';
- gboolean has_wav = g_file_test(extra_path, G_FILE_TEST_EXISTS);
+ }
+ }
- if(!has_wav)
- {
- extra_path[len] = 'W';
- extra_path[len + 1] = 'A';
- extra_path[len + 2] = 'V';
- has_wav = g_file_test(extra_path, G_FILE_TEST_EXISTS);
- }
+ for(GList *list_rec = _xmp_list; list_rec; list_rec = g_list_next(list_rec))
+ {
+ char *xmp_item = (char *)list_rec->data;
+ gboolean img_exists = FALSE;
+
+ // check original image
+ size_t len = strlen(xmp_item);
+ const char *c = xmp_item + len;
+ while((c > xmp_item) && (*c != '.')) c--;
+ len = c - xmp_item;
+ char *img_path = calloc(len, sizeof(char));
+ if(img_path)
+ {
+ g_strlcpy(img_path, xmp_item, len + 1);
+ img_exists = g_file_test((const gchar *)img_path, G_FILE_TEST_EXISTS);
+ }
- // TODO: decide if we want to remove the flag for images that lost
- // their extra file. currently we do (the else cases)
- int new_flags = flags;
- if(has_txt)
- new_flags |= DT_IMAGE_HAS_TXT;
- else
- new_flags &= ~DT_IMAGE_HAS_TXT;
- if(has_wav)
- new_flags |= DT_IMAGE_HAS_WAV;
- else
- new_flags &= ~DT_IMAGE_HAS_WAV;
- if(flags != new_flags)
+ if(!img_exists) // maybe xmp is a duplicate
+ {
+ c = img_path + len;
+ while((c > img_path) && (*c != '.')) c--;
+ size_t len_c = strlen(c);
+
+ len = c - img_path;
+ while((c > img_path) && (*c != '_')) c--;
+ size_t vers_len = c - img_path;
+ char *img_vers_path = calloc(vers_len + len_c, sizeof(char));
+ g_strlcpy(img_vers_path, img_path, vers_len + 1);
+ while(len_c)
{
- sqlite3_bind_int(inner_stmt, 1, new_flags);
- sqlite3_bind_int(inner_stmt, 2, id);
- sqlite3_step(inner_stmt);
- sqlite3_reset(inner_stmt);
- sqlite3_clear_bindings(inner_stmt);
+ img_vers_path[vers_len++] = img_path[len++];
+ len_c--;
}
+ img_path = g_strdup(img_vers_path);
+ img_exists = g_file_test(img_path, G_FILE_TEST_EXISTS);
+ }
- free(extra_path);
+ if(img_exists)
+ {
+ dt_control_crawler_result_t *item = malloc(sizeof(dt_control_crawler_result_t));
+ item->id = 0;
+ item->timestamp_xmp = 0;
+ item->timestamp_db = 0;
+ item->image_path = g_strdup(img_path);
+ item->xmp_path = g_strdup(xmp_item);
+ item->condition = DT_XMP_CONDITION_NEW;
+ item->version = 0;
+ result = g_list_prepend(result, item);
+
+ dt_print(DT_DEBUG_CONTROL, "[crawler] `%s' found on storage but not in image library", xmp_item);
}
}
- dt_database_release_transaction(darktable.db);
-
- sqlite3_finalize(stmt);
- sqlite3_finalize(inner_stmt);
-
return g_list_reverse(result); // list was built in reverse order, so un-reverse it
}
@@ -309,25 +473,45 @@ typedef struct dt_control_crawler_gui_t
GtkWidget *log;
GtkWidget *spinner;
GList *rows_to_remove;
+ GtkTreeView *missing_tree;
+ GtkTreeModel *missing_model;
+ GList *missing_rows_to_remove;
+ GtkTreeView *new_dups_tree;
+ GtkTreeModel *new_dups_model;
+ GList *new_dups_rows_to_remove;
+ GtkNotebook *nb;
} dt_control_crawler_gui_t;
+
// close the window and clean up
static void dt_control_crawler_response_callback(GtkWidget *dialog,
const gint response_id,
gpointer user_data)
{
dt_control_crawler_gui_t *gui = (dt_control_crawler_gui_t *)user_data;
+ int _number = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(gui->missing_model), NULL);
+ _number = _number + gtk_tree_model_iter_n_children(GTK_TREE_MODEL(gui->model), NULL);
+ _number = _number + gtk_tree_model_iter_n_children(GTK_TREE_MODEL(gui->new_dups_model), NULL);
+
+ if(_number == 0) // db is synchronized
+ {
+ struct timeval time;
+ gettimeofday(&time, NULL);
+ time_t __time = time.tv_sec;
+ dt_conf_set_int64("db_synchronized", __time);
+ }
+
g_object_unref(G_OBJECT(gui->model));
+ g_object_unref(G_OBJECT(gui->missing_model));
+ g_object_unref(G_OBJECT(gui->new_dups_model));
gtk_widget_destroy(dialog);
free(gui);
}
-static void _delete_selected_rows(dt_control_crawler_gui_t *gui)
+static void _delete_selected_rows(GList *rr_list,
+ GtkTreeModel *model)
{
- GList *rr_list = gui->rows_to_remove;
- GtkTreeModel *model = gui->model;
-
// Remove TreeView rows from rr_list. It needs to be populated before
for(GList *node = rr_list; node != NULL; node = g_list_next(node))
{
@@ -351,7 +535,18 @@ static void _select_all_callback(GtkButton *button,
gpointer user_data)
{
dt_control_crawler_gui_t *gui = (dt_control_crawler_gui_t *)user_data;
- GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
+ GtkTreeSelection *selection = NULL;
+ switch (gtk_notebook_get_current_page(gui->nb))
+ {
+ case 0:
+ selection = gtk_tree_view_get_selection(gui->missing_tree);
+ break;
+ case 1:
+ selection = gtk_tree_view_get_selection(gui->tree);
+ break;
+ case 2:
+ selection = gtk_tree_view_get_selection(gui->new_dups_tree);
+ }
gtk_tree_selection_select_all(selection);
}
@@ -359,7 +554,18 @@ static void _select_all_callback(GtkButton *button,
static void _select_none_callback(GtkButton *button, gpointer user_data)
{
dt_control_crawler_gui_t *gui = (dt_control_crawler_gui_t *)user_data;
- GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
+ GtkTreeSelection *selection = NULL;
+ switch (gtk_notebook_get_current_page(gui->nb))
+ {
+ case 0:
+ selection = gtk_tree_view_get_selection(gui->missing_tree);
+ break;
+ case 1:
+ selection = gtk_tree_view_get_selection(gui->tree);
+ break;
+ case 2:
+ selection = gtk_tree_view_get_selection(gui->new_dups_tree);
+ }
gtk_tree_selection_unselect_all(selection);
}
@@ -367,10 +573,26 @@ static void _select_none_callback(GtkButton *button, gpointer user_data)
static void _select_invert_callback(GtkButton *button, gpointer user_data)
{
dt_control_crawler_gui_t *gui = (dt_control_crawler_gui_t *)user_data;
- GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
-
+ GtkTreeModel *model = NULL;
+ GtkTreeSelection *selection = NULL;
GtkTreeIter iter;
- gboolean valid = gtk_tree_model_get_iter_first(gui->model, &iter);
+ gboolean valid = FALSE;
+ switch (gtk_notebook_get_current_page(gui->nb))
+ {
+ case 0:
+ selection = gtk_tree_view_get_selection(gui->missing_tree);
+ model = gui->missing_model;
+ break;
+ case 1:
+ selection = gtk_tree_view_get_selection(gui->tree);
+ model = gui->model;
+ break;
+ case 2:
+ selection = gtk_tree_view_get_selection(gui->new_dups_tree);
+ model = gui->new_dups_model;
+ }
+
+ valid = gtk_tree_model_get_iter_first(model, &iter);
while(valid)
{
if(gtk_tree_selection_iter_is_selected(selection, &iter))
@@ -378,7 +600,7 @@ static void _select_invert_callback(GtkButton *button, gpointer user_data)
else
gtk_tree_selection_select_iter(selection, &iter);
- valid = gtk_tree_model_iter_next(gui->model, &iter);
+ valid = gtk_tree_model_iter_next(model, &iter);
}
}
@@ -439,6 +661,77 @@ static void _log_synchronization(dt_control_crawler_gui_t *gui,
g_free(message);
}
+static void _set_remove_flag(char *imgs)
+{
+ sqlite3_stmt *stmt = NULL;
+ DT_DEBUG_SQLITE3_PREPARE_V2
+ (dt_database_get(darktable.db),
+ "UPDATE main.images SET flags = (flags|?1) WHERE id IN (?2)", -1, &stmt, NULL);
+ DT_DEBUG_SQLITE3_BIND_INT(stmt, 1, DT_IMAGE_REMOVE);
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 2, imgs, -1, SQLITE_STATIC);
+ sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+}
+
+static void _remove_from_db(GtkTreeModel *model,
+ GtkTreePath *path,
+ GtkTreeIter *iter,
+ gpointer user_data)
+{
+ dt_control_crawler_gui_t *gui = (dt_control_crawler_gui_t *)user_data;
+ dt_control_crawler_result_t entry = { NO_IMGID };
+ gtk_tree_model_get(model, iter,
+ 0, &entry.id,
+ 1, &entry.image_path,
+ 2, &entry.version,
+ -1);
+
+ dt_image_remove(entry.id);
+
+ // update remove status
+ _set_remove_flag(g_strdup_printf("%i", entry.id));
+
+ dt_collection_update(darktable.collection);
+
+ dt_image_synch_all_xmp(entry.image_path);
+
+ dt_film_remove_empty();
+
+ GList *l = NULL;
+ l = g_list_append(l, g_strdup_printf("%i", entry.id));
+ dt_collection_update_query(darktable.collection,
+ DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_UNDEF,
+ g_list_copy(l));
+ DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_FILMROLLS_CHANGED);
+ dt_control_queue_redraw_center();
+
+ _append_row_to_remove(model, path, &gui->missing_rows_to_remove);
+ _log_synchronization(gui, _("SUCCESS: %s removed from DB"), entry.image_path);
+
+ _free_crawler_result(&entry);
+}
+
+static void _add_to_db(GtkTreeModel *model,
+ GtkTreePath *path,
+ GtkTreeIter *iter,
+ gpointer user_data)
+{
+ dt_control_crawler_gui_t *gui = (dt_control_crawler_gui_t *)user_data;
+ dt_control_crawler_result_t entry = { NO_IMGID };
+ gtk_tree_model_get(model, iter,
+ 0, &entry.xmp_path,
+ 1, &entry.image_path,
+ -1);
+
+ while(gtk_events_pending()) gtk_main_iteration(); // TODO hook for dialog refresh: make it better
+
+ dt_load_from_string(entry.image_path, FALSE, NULL);
+
+ _append_row_to_remove(model, path, &gui->new_dups_rows_to_remove);
+ _log_synchronization(gui, _("SUCCESS: %s added to DB"), entry.xmp_path);
+
+ _free_crawler_result(&entry);
+}
static void sync_xmp_to_db(GtkTreeModel *model,
GtkTreePath *path,
@@ -450,6 +743,8 @@ static void sync_xmp_to_db(GtkTreeModel *model,
_get_crawler_entry_from_model(model, iter, &entry);
_db_update_timestamp(entry.id, entry.timestamp_xmp);
+ while(gtk_events_pending()) gtk_main_iteration(); // TODO hook for dialog refresh: make it better
+
const gboolean error = dt_history_load_and_apply(entry.id, entry.xmp_path, 0);
if(error)
@@ -478,6 +773,8 @@ static void sync_db_to_xmp(GtkTreeModel *model,
dt_control_crawler_result_t entry = { NO_IMGID };
_get_crawler_entry_from_model(model, iter, &entry);
+ while(gtk_events_pending()) gtk_main_iteration(); // TODO hook for dialog refresh: make it better
+
// write the XMP and make sure it get the last modified timestamp of the db
const gboolean error = dt_image_write_sidecar_file(entry.id);
_set_modification_time(entry.xmp_path, entry.timestamp_db);
@@ -509,6 +806,8 @@ static void sync_newest_to_oldest(GtkTreeModel *model,
gboolean error = FALSE;
+ while(gtk_events_pending()) gtk_main_iteration(); // TODO hook for dialog refresh: make it better
+
if(entry.timestamp_xmp > entry.timestamp_db)
{
// WRITE XMP in DB
@@ -579,6 +878,8 @@ static void sync_oldest_to_newest(GtkTreeModel *model,
_get_crawler_entry_from_model(model, iter, &entry);
gboolean error = FALSE;
+ while(gtk_events_pending()) gtk_main_iteration(); // TODO hook for dialog refresh: make it better
+
if(entry.timestamp_xmp < entry.timestamp_db)
{
// WRITE XMP in DB
@@ -589,7 +890,7 @@ static void sync_oldest_to_newest(GtkTreeModel *model,
_log_synchronization(gui,
_("ERROR: %s NOT synced old (XMP) → new (DB)"),
entry.image_path);
- _log_synchronization(gui,
+ _log_synchronization(gui,
_("ERROR: cannot write the database."
" the destination may be full, offline or read-only."), NULL);
}
@@ -637,6 +938,28 @@ static void sync_oldest_to_newest(GtkTreeModel *model,
_free_crawler_result(&entry);
}
+static void _add_dups_button_clicked(GtkButton *button, gpointer user_data)
+{
+ dt_control_crawler_gui_t *gui = (dt_control_crawler_gui_t *)user_data;
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->new_dups_tree);
+ gui->new_dups_rows_to_remove = NULL;
+ gtk_spinner_start(GTK_SPINNER(gui->spinner));
+ gtk_tree_selection_selected_foreach(selection, _add_to_db, gui);
+ _delete_selected_rows(gui->new_dups_rows_to_remove, gui->new_dups_model);
+ gtk_spinner_stop(GTK_SPINNER(gui->spinner));
+}
+
+static void _remove_button_clicked(GtkButton *button, gpointer user_data)
+{
+ dt_control_crawler_gui_t *gui = (dt_control_crawler_gui_t *)user_data;
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->missing_tree);
+ gui->missing_rows_to_remove = NULL;
+ gtk_spinner_start(GTK_SPINNER(gui->spinner));
+ gtk_tree_selection_selected_foreach(selection, _remove_from_db, gui);
+ _delete_selected_rows(gui->missing_rows_to_remove, gui->missing_model);
+ gtk_spinner_stop(GTK_SPINNER(gui->spinner));
+}
+
// overwrite database with xmp
static void _reload_button_clicked(GtkButton *button, gpointer user_data)
{
@@ -645,7 +968,7 @@ static void _reload_button_clicked(GtkButton *button, gpointer user_data)
gui->rows_to_remove = NULL;
gtk_spinner_start(GTK_SPINNER(gui->spinner));
gtk_tree_selection_selected_foreach(selection, sync_xmp_to_db, gui);
- _delete_selected_rows(gui);
+ _delete_selected_rows(gui->rows_to_remove, gui->model);
gtk_spinner_stop(GTK_SPINNER(gui->spinner));
}
@@ -657,7 +980,7 @@ void _overwrite_button_clicked(GtkButton *button, gpointer user_data)
gui->rows_to_remove = NULL;
gtk_spinner_start(GTK_SPINNER(gui->spinner));
gtk_tree_selection_selected_foreach(selection, sync_db_to_xmp, gui);
- _delete_selected_rows(gui);
+ _delete_selected_rows(gui->rows_to_remove, gui->model);
gtk_spinner_stop(GTK_SPINNER(gui->spinner));
}
@@ -669,7 +992,7 @@ static void _newest_button_clicked(GtkButton *button, gpointer user_data)
gui->rows_to_remove = NULL;
gtk_spinner_start(GTK_SPINNER(gui->spinner));
gtk_tree_selection_selected_foreach(selection, sync_newest_to_oldest, gui);
- _delete_selected_rows(gui);
+ _delete_selected_rows(gui->rows_to_remove, gui->model);
gtk_spinner_stop(GTK_SPINNER(gui->spinner));
}
@@ -681,7 +1004,7 @@ static void _oldest_button_clicked(GtkButton *button, gpointer user_data)
gui->rows_to_remove = NULL;
gtk_spinner_start(GTK_SPINNER(gui->spinner));
gtk_tree_selection_selected_foreach(selection, sync_oldest_to_newest, gui);
- _delete_selected_rows(gui);
+ _delete_selected_rows(gui->rows_to_remove, gui->model);
gtk_spinner_stop(GTK_SPINNER(gui->spinner));
}
@@ -702,6 +1025,89 @@ static gchar* str_time_delta(const int time_delta)
return g_strdup_printf(_("%id %02dh %02dm %02ds"), days, hours, minutes, seconds);
}
+
+GList *_get_list_dir(void)
+{
+ GList *_list = NULL;
+
+ sqlite3_stmt *stmt;
+ const gboolean look_for_xmp = dt_image_get_xmp_mode() != DT_WRITE_XMP_NEVER;
+ time_t _db_synch = dt_conf_get_int64("db_synchronized");
+
+ if(look_for_xmp)
+ {
+ // clang-format off
+ DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
+ "SELECT id, folder || '" G_DIR_SEPARATOR_S "'"
+ " FROM main.film_rolls"
+ " ORDER BY folder ASC",
+ -1, &stmt, NULL);
+ // clang-format on
+
+ dt_database_start_transaction(darktable.db);
+ while(sqlite3_step(stmt) == SQLITE_ROW)
+ {
+ _dir_ *_item = malloc(sizeof(_dir_));
+ _item->id = sqlite3_column_int(stmt, 0);
+ _item->dir_path = g_strdup((char *)sqlite3_column_text(stmt, 1));
+
+ time_t dir_time_mark = dt_diratime_action(_item->dir_path, "create");
+
+ if(_item->dir_path && _db_synch <= dir_time_mark)
+ {
+ _list = g_list_append(_list, _item);
+ }
+ }
+ dt_database_release_transaction(darktable.db);
+ sqlite3_finalize(stmt);
+ }
+ return _list;
+}
+
+GList *_get_list_xmp(void)
+{
+ GList *_list = NULL;
+ GList *_dir = NULL;
+ const gboolean look_for_xmp = dt_image_get_xmp_mode() != DT_WRITE_XMP_NEVER;
+
+ _dir = _get_list_dir();
+
+ if(look_for_xmp)
+ {
+ for(GList *_dir_iter = _dir; _dir_iter; _dir_iter = g_list_next(_dir_iter))
+ {
+ _dir_ *_item = _dir_iter->data;
+ GError *error = NULL;
+ GFile *gfolder = g_file_new_for_path(_item->dir_path);
+ GFileEnumerator *dir_files = g_file_enumerate_children(gfolder,
+ G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","
+ G_FILE_ATTRIBUTE_STANDARD_TYPE,
+ G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+ NULL, &error);
+ if(dir_files)
+ {
+ GFileInfo *info = NULL;
+ while((info = g_file_enumerator_next_file(dir_files, NULL, &error)))
+ {
+ const char *filename = g_file_info_get_display_name(info);
+ if(!filename) continue;
+ const GFileType filetype = g_file_info_get_attribute_uint32(info, G_FILE_ATTRIBUTE_STANDARD_TYPE);
+ if(filetype == G_FILE_TYPE_REGULAR)
+ {
+ size_t name_len = strlen(filename);
+ const char *ext = filename + name_len - 4;
+ if ((strcmp(ext, ".xmp") == 0 || strcmp(ext, ".XMP") == 0) && name_len > 4)
+ {
+ _list = g_list_append(_list, g_strconcat(_item->dir_path, filename, NULL));
+ }
+ }
+ }
+ }
+ }
+ }
+ return _list;
+}
+
// show a popup window with a list of updated images/xmp files and allow the user to tell dt what to do about them
void dt_control_crawler_show_image_list(GList *images)
{
@@ -709,6 +1115,14 @@ void dt_control_crawler_show_image_list(GList *images)
dt_control_crawler_gui_t *gui = malloc(sizeof(dt_control_crawler_gui_t));
+ GtkNotebook *nb = GTK_NOTEBOOK(gtk_notebook_new());
+ GtkWidget *page1 = dt_ui_notebook_page(nb, N_("missing"), NULL);
+ GtkWidget *page2 = dt_ui_notebook_page(nb, N_("changed"), NULL);
+ GtkWidget *page3 = dt_ui_notebook_page(nb, N_("new"), NULL);
+ gtk_widget_show(gtk_notebook_get_nth_page(nb, 1));
+ gtk_notebook_set_current_page(nb, 1);
+ gui->nb = GTK_NOTEBOOK(nb);
+
// a list with all the images
GtkTreeViewColumn *column;
GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL);
@@ -723,9 +1137,23 @@ void dt_control_crawler_show_image_list(GList *images)
G_TYPE_INT,
G_TYPE_STRING, // report: newer version
G_TYPE_STRING);// time delta
-
gui->model = GTK_TREE_MODEL(store);
+ GtkWidget *missing_scroll = gtk_scrolled_window_new(NULL, NULL);
+ gtk_widget_set_vexpand(missing_scroll, TRUE);
+ GtkListStore *missing_store = gtk_list_store_new(3,
+ G_TYPE_INT, // id
+ G_TYPE_STRING, // image path
+ G_TYPE_INT); // version
+ gui->missing_model = GTK_TREE_MODEL(missing_store);
+
+ GtkWidget *new_dups_scroll = gtk_scrolled_window_new(NULL, NULL);
+ gtk_widget_set_vexpand(new_dups_scroll, TRUE);
+ GtkListStore *new_dups_store = gtk_list_store_new(2,
+ G_TYPE_STRING, // xmp path
+ G_TYPE_STRING); // image path
+ gui->new_dups_model = GTK_TREE_MODEL(new_dups_store);
+
for(GList *list_iter = images; list_iter; list_iter = g_list_next(list_iter))
{
GtkTreeIter iter;
@@ -740,26 +1168,86 @@ void dt_control_crawler_show_image_list(GList *images)
const time_t time_delta = llabs(item->timestamp_db - item->timestamp_xmp);
gchar *timestamp_delta = str_time_delta(time_delta);
- gtk_list_store_append(store, &iter);
- gtk_list_store_set
- (store, &iter,
- DT_CONTROL_CRAWLER_COL_ID, item->id,
- DT_CONTROL_CRAWLER_COL_IMAGE_PATH, item->image_path,
- DT_CONTROL_CRAWLER_COL_XMP_PATH, item->xmp_path,
- DT_CONTROL_CRAWLER_COL_TS_XMP, timestamp_xmp,
- DT_CONTROL_CRAWLER_COL_TS_DB, timestamp_db,
- DT_CONTROL_CRAWLER_COL_TS_XMP_INT, item->timestamp_xmp,
- DT_CONTROL_CRAWLER_COL_TS_DB_INT, item->timestamp_db,
- DT_CONTROL_CRAWLER_COL_REPORT, (item->timestamp_xmp > item->timestamp_db)
- ? _("XMP")
- : _("database"),
- DT_CONTROL_CRAWLER_COL_TIME_DELTA, timestamp_delta,
- -1);
+ if(item->condition == DT_XMP_CONDITION_CHANGED)
+ {
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set
+ (store, &iter,
+ DT_CONTROL_CRAWLER_COL_ID, item->id,
+ DT_CONTROL_CRAWLER_COL_IMAGE_PATH, item->image_path,
+ DT_CONTROL_CRAWLER_COL_XMP_PATH, item->xmp_path,
+ DT_CONTROL_CRAWLER_COL_TS_XMP, timestamp_xmp,
+ DT_CONTROL_CRAWLER_COL_TS_DB, timestamp_db,
+ DT_CONTROL_CRAWLER_COL_TS_XMP_INT, item->timestamp_xmp,
+ DT_CONTROL_CRAWLER_COL_TS_DB_INT, item->timestamp_db,
+ DT_CONTROL_CRAWLER_COL_REPORT, (item->timestamp_xmp > item->timestamp_db)
+ ? _("XMP")
+ : _("database"),
+ DT_CONTROL_CRAWLER_COL_TIME_DELTA, timestamp_delta,
+ -1);
+ }
+ else if(item->condition == DT_XMP_CONDITION_MISSING)
+ {
+ gtk_list_store_append(missing_store, &iter);
+ gtk_list_store_set
+ (missing_store, &iter,
+ 0, item->id,
+ 1, item->image_path,
+ 2, item->version,
+ -1);
+ }
+ else if(item->condition == DT_XMP_CONDITION_NEW)
+ {
+ gtk_list_store_append(new_dups_store, &iter);
+ gtk_list_store_set
+ (new_dups_store, &iter,
+ 0, item->xmp_path,
+ 1, item->image_path,
+ -1);
+ }
_free_crawler_result(item);
g_free(timestamp_delta);
}
g_list_free_full(images, g_free);
+ GtkWidget *new_dups_tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(new_dups_store));
+ GtkTreeSelection *new_dups_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(new_dups_tree));
+ gtk_tree_selection_set_mode(new_dups_selection, GTK_SELECTION_MULTIPLE);
+ gui->new_dups_tree = GTK_TREE_VIEW(new_dups_tree); // FIXME: do we need to free that later ?
+ GtkCellRenderer *new_dups_renderer_text = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes
+ (_("new images"), new_dups_renderer_text, "text", 0, NULL);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(new_dups_tree), column);
+ gtk_tree_view_column_set_expand(column, TRUE);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_min_width(column, DT_PIXEL_APPLY_DPI(200));
+ g_object_set(new_dups_renderer_text, "ellipsize", PANGO_ELLIPSIZE_MIDDLE, NULL);
+
+ gtk_container_add(GTK_CONTAINER(new_dups_scroll), new_dups_tree);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(new_dups_scroll),
+ GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
+
+ GtkWidget *missing_tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(missing_store));
+ GtkTreeSelection *missing_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(missing_tree));
+ gtk_tree_selection_set_mode(missing_selection, GTK_SELECTION_MULTIPLE);
+ gui->missing_tree = GTK_TREE_VIEW(missing_tree); // FIXME: do we need to free that later ?
+ GtkCellRenderer *missing_renderer_text = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes
+ (_("missing images"), missing_renderer_text, "text", 1, NULL);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(missing_tree), column);
+ gtk_tree_view_column_set_expand(column, TRUE);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_min_width(column, DT_PIXEL_APPLY_DPI(200));
+ g_object_set(missing_renderer_text, "ellipsize", PANGO_ELLIPSIZE_MIDDLE, NULL);
+
+ column = gtk_tree_view_column_new_with_attributes
+ (_("version (duplicate)"), gtk_cell_renderer_text_new(), "text", 2, NULL);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(missing_tree), column);
+
+ gtk_container_add(GTK_CONTAINER(missing_scroll), missing_tree);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(missing_scroll),
+ GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
+
GtkWidget *tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE);
@@ -805,7 +1293,7 @@ void dt_control_crawler_show_image_list(GList *images)
// build a dialog window that contains the list of images
GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
GtkWidget *dialog = gtk_dialog_new_with_buttons
- (_("updated XMP sidecar files found"), GTK_WINDOW(win),
+ (_("manage XMP sidecar files"), GTK_WINDOW(win),
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, _("_close"),
GTK_RESPONSE_CLOSE, NULL);
@@ -821,6 +1309,7 @@ void dt_control_crawler_show_image_list(GList *images)
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_box_pack_start(GTK_BOX(content_box), box, FALSE, FALSE, 0);
+ gtk_widget_set_margin_bottom(GTK_WIDGET(box), 10);
GtkWidget *select_all = gtk_button_new_with_label(_("select all"));
GtkWidget *select_none = gtk_button_new_with_label(_("select none"));
GtkWidget *select_invert = gtk_button_new_with_label(_("invert selection"));
@@ -831,10 +1320,20 @@ void dt_control_crawler_show_image_list(GList *images)
g_signal_connect(select_none, "clicked", G_CALLBACK(_select_none_callback), gui);
g_signal_connect(select_invert, "clicked", G_CALLBACK(_select_invert_callback), gui);
- gtk_box_pack_start(GTK_BOX(content_box), scroll, TRUE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(content_box), GTK_WIDGET(nb), TRUE, TRUE, 0);
+
+ gtk_container_add(GTK_CONTAINER(page1), missing_scroll);
+ GtkWidget *missing_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ missing_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_box_pack_start(GTK_BOX(page1), missing_box, FALSE, FALSE, 1);
+ GtkWidget *remove_button = gtk_button_new_with_label(
+ _("remove selected entries from image library"));
+ gtk_box_pack_start(GTK_BOX(missing_box), remove_button, FALSE, FALSE, 0);
+ g_signal_connect(remove_button, "clicked", G_CALLBACK(_remove_button_clicked), gui);
+ gtk_container_add(GTK_CONTAINER(page2), scroll);
box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
- gtk_box_pack_start(GTK_BOX(content_box), box, FALSE, FALSE, 1);
+ gtk_box_pack_start(GTK_BOX(page2), box, FALSE, FALSE, 1);
GtkWidget *label = gtk_label_new_with_mnemonic(_("on the selection:"));
GtkWidget *reload_button = gtk_button_new_with_label(_("keep the XMP edit"));
GtkWidget *overwrite_button = gtk_button_new_with_label(_("keep the database edit"));
@@ -850,9 +1349,14 @@ void dt_control_crawler_show_image_list(GList *images)
g_signal_connect(newest_button, "clicked", G_CALLBACK(_newest_button_clicked), gui);
g_signal_connect(oldest_button, "clicked", G_CALLBACK(_oldest_button_clicked), gui);
- /* Feedback spinner in case synch happens over network and stales */
- gui->spinner = gtk_spinner_new();
- gtk_box_pack_start(GTK_BOX(box), GTK_WIDGET(gui->spinner), FALSE, FALSE, 0);
+ gtk_container_add(GTK_CONTAINER(page3), new_dups_scroll);
+ GtkWidget *new_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ new_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_box_pack_start(GTK_BOX(page3), new_box, FALSE, FALSE, 1);
+ GtkWidget *add_dups_button = gtk_button_new_with_label(
+ _("add selected entries to image library"));
+ gtk_box_pack_start(GTK_BOX(new_box), add_dups_button, FALSE, FALSE, 0);
+ g_signal_connect(add_dups_button, "clicked", G_CALLBACK(_add_dups_button_clicked), gui);
/* Log report */
scroll = gtk_scrolled_window_new(NULL, NULL);
@@ -862,6 +1366,10 @@ void dt_control_crawler_show_image_list(GList *images)
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
+ /* Feedback spinner in case synch happens over network and stales */
+ gui->spinner = gtk_spinner_new();
+ gtk_box_pack_start(GTK_BOX(content_box), GTK_WIDGET(gui->spinner), FALSE, FALSE, 0);
+
gtk_tree_view_insert_column_with_attributes
(GTK_TREE_VIEW(gui->log), -1,
_("synchronization log"), renderer_text,
@@ -872,6 +1380,13 @@ void dt_control_crawler_show_image_list(GList *images)
gtk_tree_view_set_model(GTK_TREE_VIEW(gui->log), model_log);
g_object_unref(model_log);
+ GdkRectangle workarea;
+ GdkDisplay *display = gtk_widget_get_display(dt_ui_main_window(darktable.gui->ui));
+ GdkMonitor *mon = gdk_display_get_monitor_at_window(display, gtk_widget_get_window(win));
+ gdk_monitor_get_workarea(mon, &workarea);
+ gtk_window_resize(GTK_WINDOW(dialog),
+ workarea.width*3/4, workarea.height*3/4); //TODO save and restore
+ gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_CENTER);
gtk_widget_show_all(dialog);
g_signal_connect(dialog, "response",
diff --git a/src/control/jobs/control_jobs.c b/src/control/jobs/control_jobs.c
index 9d8c7488ffcd..65f9d1bd0f93 100644
--- a/src/control/jobs/control_jobs.c
+++ b/src/control/jobs/control_jobs.c
@@ -362,6 +362,7 @@ static int32_t _control_write_sidecar_files_job_run(dt_job_t *job)
sqlite3_step(stmt);
sqlite3_reset(stmt);
sqlite3_clear_bindings(stmt);
+ dt_diratime_action(dtfilename, "delete");
}
dt_image_cache_read_release(img);
}
@@ -1153,6 +1154,7 @@ static _dt_delete_status_t delete_file_from_disk
|| g_error_matches(gerror, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
{
delete_status = _DT_DELETE_STATUS_DELETED;
+ dt_diratime_action(filename, "delete");
}
else
{
diff --git a/src/libs/tools/global_toolbox.c b/src/libs/tools/global_toolbox.c
index 15aa4f43fdab..b83dc5c9b46d 100644
--- a/src/libs/tools/global_toolbox.c
+++ b/src/libs/tools/global_toolbox.c
@@ -532,6 +532,11 @@ void gui_cleanup(dt_lib_module_t *self)
void _lib_preferences_button_clicked(GtkWidget *widget, gpointer user_data)
{
dt_gui_preferences_show();
+ if (dt_conf_get_bool("xmp_full_scan_next_time"))
+ {
+ dt_conf_set_int64("db_synchronized", 0);
+ dt_conf_set_bool("xmp_full_scan_next_time", 0);
+ }
}
static void _lib_filter_grouping_button_clicked(GtkWidget *widget, gpointer user_data)