diff --git a/hosts/common/user/configs/gui/darktable/better-copy-and-import.patch b/hosts/common/user/configs/gui/darktable/better-copy-and-import.patch
new file mode 100644
index 0000000..b5179b9
--- /dev/null
+++ b/hosts/common/user/configs/gui/darktable/better-copy-and-import.patch
@@ -0,0 +1,265 @@
+diff --git a/data/darktableconfig.xml.in b/data/darktableconfig.xml.in
+index 83eadf8a35..39ed8d43d7 100644
+--- a/data/darktableconfig.xml.in
++++ b/data/darktableconfig.xml.in
+@@ -1524,6 +1524,22 @@
+ file naming pattern used for a import session
+
+
++
++ session/conflict_padding
++ int
++ 2
++ burst file name conflict number padding
++ set the padding length for conflict resolution (e.g., 001, 002).
++
++
++
++ session/import_existing_sidecar
++ bool
++ true
++ import existing sidecar files
++ import existing sidecar files (XMP, IPTC, etc.) when importing images
++
++
+
+ plugins/lighttable/layout
+ int
+diff --git a/src/common/import_session.c b/src/common/import_session.c
+index e83ef4de62..4d0c4efa0c 100644
+--- a/src/common/import_session.c
++++ b/src/common/import_session.c
+@@ -266,48 +266,42 @@ const char *dt_import_session_filename(struct dt_import_session_t *self, gboolea
+ char *pattern = _import_session_filename_pattern();
+ if(pattern == NULL)
+ {
+- dt_print(DT_DEBUG_ALWAYS, "[import_session] Failed to get session filaname pattern.\n");
++ dt_print(DT_DEBUG_ALWAYS, "[import_session] Failed to get session filename pattern.\n");
+ return NULL;
+ }
+
++ self->vp->retry_count = 0;
++
+ /* verify that expanded path and filename yields a unique file */
+ const char *path = dt_import_session_path(self, TRUE);
+
+ if(use_filename)
+ result_fname = g_strdup(self->vp->filename);
+ else
+- result_fname = _import_session_filename_from_pattern(self, pattern);
+-
+- char *fname = g_build_path(G_DIR_SEPARATOR_S, path, result_fname, (char *)NULL);
+- char *previous_fname = fname;
+- if(g_file_test(fname, G_FILE_TEST_EXISTS) == TRUE)
+ {
+- dt_print(DT_DEBUG_ALWAYS, "[import_session] File %s exists.\n", fname);
+ do
+ {
+- /* file exists, yield a new filename */
++ /* generate filename based on the current retry_count */
+ g_free(result_fname);
+ result_fname = _import_session_filename_from_pattern(self, pattern);
+- fname = g_build_path(G_DIR_SEPARATOR_S, path, result_fname, (char *)NULL);
+
+- dt_print(DT_DEBUG_ALWAYS, "[import_session] Testing %s.\n", fname);
+- /* check if same filename was yielded as before */
+- if(strcmp(previous_fname, fname) == 0)
++ char *test_path = g_build_path(G_DIR_SEPARATOR_S, path, result_fname, (char *)NULL);
++
++ if(g_file_test(test_path, G_FILE_TEST_EXISTS) == TRUE)
+ {
+- g_free(previous_fname);
+- g_free(fname);
+- dt_control_log(_(
+- "couldn't expand to a unique filename for session, please check your import session settings."));
+- return NULL;
++ dt_print(DT_DEBUG_ALWAYS, "[import_session] File %s exists, retrying.\n", test_path);
++ self->vp->retry_count++;
++ g_free(test_path);
++ }
++ else
++ {
++ g_free(test_path);
++ break;
+ }
+
+- g_free(previous_fname);
+- previous_fname = fname;
+-
+- } while(g_file_test(fname, G_FILE_TEST_EXISTS) == TRUE);
++ } while(TRUE);
+ }
+
+- g_free(previous_fname);
+ g_free(pattern);
+
+ self->current_filename = result_fname;
+diff --git a/src/common/variables.c b/src/common/variables.c
+index 1474cc32e8..820f88414b 100644
+--- a/src/common/variables.c
++++ b/src/common/variables.c
+@@ -914,6 +914,14 @@ static char *_get_base_value(dt_variables_params_t *params, char **variable)
+ else if(_has_prefix(variable, "DARKTABLE.NAME")
+ || _has_prefix(variable, "DARKTABLE_NAME"))
+ result = g_strdup(PACKAGE_NAME);
++
++ else if(_has_prefix(variable, "CONFLICT_PADDING"))
++ {
++ int pad_length = dt_conf_get_int("session/conflict_padding");
++ if(pad_length < 0) pad_length = 0;
++ result = g_strdup_printf("%0*u", pad_length, params->retry_count);
++ }
++
+ else
+ {
+ // go past what looks like an invalid variable. we only expect to
+diff --git a/src/common/variables.h b/src/common/variables.h
+index 86052a9a3d..a5d616a94c 100644
+--- a/src/common/variables.h
++++ b/src/common/variables.h
+@@ -29,6 +29,9 @@ typedef struct dt_variables_params_t
+ /** used for expanding variables that uses filename $(FILE_FOLDER) $(FILE_NAME) and $(FILE_EXTENSION). */
+ const gchar *filename;
+
++ /** used for conflict resolution in filename expansion */
++ int retry_count;
++
+ /** used for expanding variable $(JOBCODE) */
+ const gchar *jobcode;
+
+diff --git a/src/control/jobs/control_jobs.c b/src/control/jobs/control_jobs.c
+index a9fab6f0ea..27bceab782 100644
+--- a/src/control/jobs/control_jobs.c
++++ b/src/control/jobs/control_jobs.c
+@@ -1566,7 +1566,7 @@ static int32_t dt_control_export_job_run(dt_job_t *job)
+ {
+ // IPTC character encoding not set by user, so we set the default utf8 here
+ settings->metadata_export = dt_util_dstrcat(settings->metadata_export,
+- "\1%s\1%s",
++ "\1%s\1%s",
+ iptc_envelope_characterset,
+ "\x1b%G"); // ESC % G
+ }
+@@ -2265,6 +2265,59 @@ void dt_control_write_sidecar_files()
+ FALSE));
+ }
+
++static gboolean _copy_file(const char *source, const char *destination)
++{
++ gchar *data = NULL;
++ gsize size = 0;
++
++ if(!g_file_get_contents(source, &data, &size, NULL))
++ {
++ dt_print(DT_DEBUG_CONTROL, "[import_from] failed to read file `%s`", source);
++ return FALSE;
++ }
++
++ if(!g_file_set_contents(destination, data, size, NULL))
++ {
++ dt_print(DT_DEBUG_CONTROL, "[import_from] failed to write file `%s`", destination);
++ g_free(data);
++ return FALSE;
++ }
++
++ g_free(data);
++ return TRUE;
++}
++
++static void _copy_timestamps(const char *source, const char *destination)
++{
++ struct stat statbuf;
++ if(stat(source, &statbuf) == 0)
++ {
++#ifdef _WIN32
++ struct utimbuf times;
++ times.actime = statbuf.st_atime;
++ times.modtime = statbuf.st_mtime;
++ utime(destination, ×);
++#else
++ struct timeval times[2];
++ times[0].tv_sec = statbuf.st_atime;
++ times[1].tv_sec = statbuf.st_mtime;
++#ifdef __APPLE__
++#ifndef _POSIX_SOURCE
++ times[0].tv_usec = statbuf.st_atimespec.tv_nsec * 0.001;
++ times[1].tv_usec = statbuf.st_mtimespec.tv_nsec * 0.001;
++#else
++ times[0].tv_usec = statbuf.st_atimensec * 0.001;
++ times[1].tv_usec = statbuf.st_mtimensec * 0.001;
++#endif
++#else
++ times[0].tv_usec = statbuf.st_atim.tv_nsec * 0.001;
++ times[1].tv_usec = statbuf.st_mtim.tv_nsec * 0.001;
++#endif
++ utimes(destination, times);
++#endif
++ }
++}
++
+ static int _control_import_image_copy(const char *filename,
+ char **prev_filename,
+ char **prev_output,
+@@ -2308,37 +2361,37 @@ static int _control_import_image_copy(const char *filename,
+ g_free(basename);
+ }
+
+- if(!g_file_set_contents(output, data, size, NULL))
++ if(!_copy_file(filename, output))
+ {
+- dt_print(DT_DEBUG_CONTROL, "[import_from] failed to write file %s\n", output);
++ dt_print(DT_DEBUG_CONTROL, "[import_from] failed to copy file %s", filename);
+ res = FALSE;
+ }
+ else
+ {
+-#ifdef _WIN32
+- struct utimbuf times;
+- times.actime = statbuf.st_atime;
+- times.modtime = statbuf.st_mtime;
+- utime(output, ×); // set origin file timestamps
+-#else
+- struct timeval times[2];
+- times[0].tv_sec = statbuf.st_atime;
+- times[1].tv_sec = statbuf.st_mtime;
+-#ifdef __APPLE__
+-#ifndef _POSIX_SOURCE
+- times[0].tv_usec = statbuf.st_atimespec.tv_nsec * 0.001;
+- times[1].tv_usec = statbuf.st_mtimespec.tv_nsec * 0.001;
+-#else
+- times[0].tv_usec = statbuf.st_atimensec * 0.001;
+- times[1].tv_usec = statbuf.st_mtimensec * 0.001;
+-#endif
+-#else
+- times[0].tv_usec = statbuf.st_atim.tv_nsec * 0.001;
+- times[1].tv_usec = statbuf.st_mtim.tv_nsec * 0.001;
+-#endif
+- utimes(output, times); // set origin file timestamps
+-#endif
++ _copy_timestamps(filename, output);
++ }
+
++ gboolean import_existing_sidecar = dt_conf_get_bool("session/import_existing_sidecar");
++ if(import_existing_sidecar)
++ {
++ char *xml_filename = g_strdup_printf("%s.xmp", filename);
++ if(g_file_test(xml_filename, G_FILE_TEST_EXISTS))
++ {
++ char *xml_output = g_strdup_printf("%s.xmp", output);
++ if(_copy_file(xml_filename, xml_output))
++ _copy_timestamps(xml_filename, xml_output);
++ else
++ {
++ dt_print(DT_DEBUG_CONTROL, "[import_from] failed to copy sidecar %s", xml_filename);
++ res = FALSE;
++ }
++ g_free(xml_output);
++ }
++ g_free(xml_filename);
++ }
++
++ if(res)
++ {
+ const dt_imgid_t imgid = dt_image_import(dt_import_session_film_id(session),
+ output, FALSE, FALSE);
+ if(!imgid) dt_control_log(_("error loading file `%s'"), output);
diff --git a/hosts/common/user/configs/gui/darktable/default.nix b/hosts/common/user/configs/gui/darktable/default.nix
index 22b65c5..42d7c61 100644
--- a/hosts/common/user/configs/gui/darktable/default.nix
+++ b/hosts/common/user/configs/gui/darktable/default.nix
@@ -4,6 +4,14 @@
}:
{ pkgs, ... }:
{
+ nixpkgs.overlays = [
+ (final: prev: {
+ darktable = prev.darktable.overrideAttrs (oldAttrs: {
+ patches = oldAttrs.patches or [ ] ++ [ ./better-copy-and-import.patch ];
+ });
+ })
+ ];
+
environment.persistence = {
"/persist"."${home}/.config/darktable" = { };
"/cache"."${home}/.cache/darktable" = { };
@@ -28,6 +36,17 @@
"rating_one_double_tap" = true;
"run_crawler_on_start" = true;
"ui_last/theme" = "darktable-elegant-darker";
+ "ui_last/grouping" = true;
+ "plugins/darkroom/lut3d/def_path" = "${home}/.config/darktable/luts";
+ "opencl" = false;
+ "plugins/lighttable/overlays/1/0" = 0;
+ "plugins/lighttable/overlays/1/1" = 3;
+ "plugins/lighttable/overlays/1/2" = 3;
+ "plugins/darkroom/modulegroups/last_preset" = "modules: all";
+ "session/base_directory_pattern" = "${home}/Pictures/Darktable";
+ "session/filename_pattern" = "$(EXIF.YEAR)-$(EXIF.MONTH)-$(EXIF.DAY)_$(EXIF.HOUR)-$(EXIF.MINUTE)-$(EXIF.SECOND)_$(CONFLICT_PADDING).$(FILE_EXTENSION)";
+ "session/sub_directory_pattern" = "";
+ "setup_import_directory" = true;
};
"darktable/luts".source = "${hald-clut}/HaldCLUT";