#target photoshop

/*
    BATCH SET LAYER COLOR.jsx

    Batch-processes Photoshop files inside a selected folder.

    Supported files:
    - PSD
    - PSB
    - TIF
    - TIFF

    Supported layer types:
    - Solid Color Fill layers: changes the fill color non-destructively
    - Normal pixel layers: fills the entire layer with the chosen color

    The script:
    1. Opens a settings window
    2. Lets you pick a saved color preset, or enter and save your own
    3. Lets you select a source folder
    4. Optionally searches all subfolders
    5. Opens each supported file
    6. Changes the matching layer
    7. Saves over the original file
    8. Closes the document
    9. Separates problems from successful files inside every folder section
*/

(function () {
    app.bringToFront();

    var settings = showSettingsDialog();

    if (!settings) {
        return;
    }

    var sourceFolder = Folder.selectDialog(
        "Select the folder containing the Photoshop files"
    );

    if (!sourceFolder) {
        return;
    }

    var oldDialogs = app.displayDialogs;
    app.displayDialogs = DialogModes.NO;

    function showSettingsDialog() {
        var dialog = new Window("dialog", "Batch Set Layer Color");
        dialog.orientation = "column";
        dialog.alignChildren = "fill";
        dialog.spacing = 12;
        dialog.margins = 16;

        var layerGroup = dialog.add("group");
        layerGroup.orientation = "row";
        layerGroup.add("statictext", undefined, "Layer name:");

        var layerNameInput = layerGroup.add(
            "edittext",
            undefined,
            "BACKGROUND"
        );
        layerNameInput.characters = 26;

        // Presets are loaded from disk so anything the user saves
        // is still there the next time the script runs.
        var colorPresets = loadCustomPresets();

        colorPresets.push({
            name: "Custom HEX",
            hex: null
        });

        function buildPresetDropdownItems() {
            var items = [];

            for (var i = 0; i < colorPresets.length; i++) {
                var preset = colorPresets[i];

                items.push(
                    preset.hex === null
                        ? preset.name
                        : preset.name + "  -  #" + preset.hex.toLowerCase()
                );
            }

            return items;
        }

        function getPersistablePresets() {
            var toSave = [];

            for (var i = 0; i < colorPresets.length; i++) {
                if (colorPresets[i].hex !== null) {
                    toSave.push({
                        name: colorPresets[i].name,
                        hex: colorPresets[i].hex
                    });
                }
            }

            return toSave;
        }

        function refreshPresetDropdown(selectIndex) {
            presetDropdown.removeAll();

            var items = buildPresetDropdownItems();

            for (var i = 0; i < items.length; i++) {
                presetDropdown.add("item", items[i]);
            }

            presetDropdown.selection = selectIndex;
        }

        function promptForPresetName(defaultName) {
            var nameDialog = new Window("dialog", "Save Color Preset");
            nameDialog.orientation = "column";
            nameDialog.alignChildren = "fill";
            nameDialog.spacing = 10;
            nameDialog.margins = 16;

            nameDialog.add("statictext", undefined, "Preset name:");

            var nameInput = nameDialog.add(
                "edittext",
                undefined,
                defaultName || ""
            );
            nameInput.characters = 26;
            nameInput.active = true;

            var nameButtonGroup = nameDialog.add("group");
            nameButtonGroup.alignment = "right";

            nameButtonGroup.add(
                "button",
                undefined,
                "Cancel",
                {name: "cancel"}
            );

            var saveNameButton = nameButtonGroup.add(
                "button",
                undefined,
                "Save",
                {name: "ok"}
            );

            var enteredName = null;

            saveNameButton.onClick = function () {
                var value = trimString(nameInput.text);

                if (!value) {
                    alert("Please enter a preset name.");
                    return;
                }

                enteredName = value;
                nameDialog.close(1);
            };

            nameDialog.center();

            if (nameDialog.show() !== 1) {
                return null;
            }

            return enteredName;
        }

        var presetGroup = dialog.add("group");
        presetGroup.orientation = "row";
        presetGroup.add("statictext", undefined, "Color preset:");

        var presetDropdown = presetGroup.add(
            "dropdownlist",
            undefined,
            buildPresetDropdownItems()
        );
        presetDropdown.preferredSize.width = 200;
        presetDropdown.selection = colorPresets.length - 1;

        var removePresetButton = presetGroup.add(
            "button",
            undefined,
            "Remove"
        );
        removePresetButton.preferredSize.width = 60;
        removePresetButton.enabled = false;

        var colorGroup = dialog.add("group");
        colorGroup.orientation = "row";
        colorGroup.add("statictext", undefined, "HEX color:");

        var hexInput = colorGroup.add(
            "edittext",
            undefined,
            "#e6e6e6"
        );
        hexInput.characters = 26;

        // The HEX field is editable whenever "Custom HEX" is selected.
        // Saved presets are read-only until removed.
        hexInput.enabled = true;

        var addPresetButton = colorGroup.add(
            "button",
            undefined,
            "Save As Preset..."
        );

        var updatingHexFromPreset = false;

        presetDropdown.onChange = function () {
            if (!presetDropdown.selection) {
                return;
            }

            var presetIndex = presetDropdown.selection.index;
            var selectedPreset = colorPresets[presetIndex];

            if (selectedPreset.hex === null) {
                hexInput.enabled = true;
                hexInput.active = true;
                removePresetButton.enabled = false;
                return;
            }

            updatingHexFromPreset = true;
            hexInput.text = "#" + selectedPreset.hex.toLowerCase();
            hexInput.enabled = false;
            updatingHexFromPreset = false;
            removePresetButton.enabled = true;
        };

        hexInput.onChanging = function () {
            // Only Custom HEX is editable, so no automatic preset switching is needed.
            if (updatingHexFromPreset) {
                return;
            }
        };

        addPresetButton.onClick = function () {
            var hex = normalizeHex(hexInput.text);

            if (!hex) {
                alert(
                    "Enter a valid HEX color before saving a preset.\n" +
                    "Example: #e6e6e6"
                );
                return;
            }

            var name = promptForPresetName("");

            if (!name) {
                return;
            }

            var existingIndex = -1;

            for (var i = 0; i < colorPresets.length; i++) {
                if (
                    colorPresets[i].hex !== null &&
                    colorPresets[i].name.toLowerCase() === name.toLowerCase()
                ) {
                    existingIndex = i;
                    break;
                }
            }

            var newPreset = {name: name, hex: hex};
            var selectIndex;

            if (existingIndex !== -1) {
                colorPresets[existingIndex] = newPreset;
                selectIndex = existingIndex;
            } else {
                colorPresets.splice(colorPresets.length - 1, 0, newPreset);
                selectIndex = colorPresets.length - 2;
            }

            saveCustomPresets(getPersistablePresets());
            refreshPresetDropdown(selectIndex);
            hexInput.enabled = false;
            removePresetButton.enabled = true;
        };

        removePresetButton.onClick = function () {
            if (!presetDropdown.selection) {
                return;
            }

            var presetIndex = presetDropdown.selection.index;
            var preset = colorPresets[presetIndex];

            if (preset.hex === null) {
                return;
            }

            if (!confirm('Remove the preset "' + preset.name + '"?')) {
                return;
            }

            colorPresets.splice(presetIndex, 1);
            saveCustomPresets(getPersistablePresets());
            refreshPresetDropdown(colorPresets.length - 1);
            hexInput.enabled = true;
            removePresetButton.enabled = false;
        };

        var presetInfo = dialog.add(
            "statictext",
            undefined,
            "Select a saved preset, or enter a HEX color and click " +
            '"Save As Preset..." to add it to the list for next time.',
            {multiline: true}
        );
        presetInfo.preferredSize.width = 390;

        var includeSubfoldersCheckbox = dialog.add(
            "checkbox",
            undefined,
            "Include subfolders"
        );
        includeSubfoldersCheckbox.value = false;

        var infoText = dialog.add(
            "statictext",
            undefined,
            "The selected files will be overwritten.",
            {multiline: true}
        );
        infoText.preferredSize.width = 360;

        var buttonGroup = dialog.add("group");
        buttonGroup.alignment = "right";

        buttonGroup.add(
            "button",
            undefined,
            "Cancel",
            {name: "cancel"}
        );

        var startButton = buttonGroup.add(
            "button",
            undefined,
            "Select Folder",
            {name: "ok"}
        );

        var result = null;

        startButton.onClick = function () {
            var layerName = trimString(layerNameInput.text);
            var hex = normalizeHex(hexInput.text);

            if (!layerName) {
                alert("Please enter a layer name.");
                return;
            }

            if (!hex) {
                alert(
                    "Please enter a valid 3-digit or 6-digit HEX color.\n" +
                    "Example: #e6e6e6"
                );
                return;
            }

            var selectedPresetIndex =
                presetDropdown.selection
                    ? presetDropdown.selection.index
                    : colorPresets.length - 1;

            result = {
                layerName: layerName,
                hex: hex,
                presetName:
                    colorPresets[selectedPresetIndex].name,
                includeSubfolders: includeSubfoldersCheckbox.value
            };

            dialog.close(1);
        };

        var creditGroup = dialog.add("group");
        creditGroup.alignment = "center";

        var creditText = creditGroup.add(
            "statictext",
            undefined,
            "Developed by ralphletsgo.com"
        );

        try {
            creditText.graphics.font = ScriptUI.newFont(
                creditText.graphics.font.name,
                "ITALIC",
                10
            );
        } catch (fontError) {}

        layerNameInput.active = true;
        dialog.center();

        if (dialog.show() !== 1) {
            return null;
        }

        return result;
    }

    function isSupportedFile(file) {
        if (!(file instanceof File)) {
            return false;
        }

        var name = file.name;
        var lowerName = name.toLowerCase();

        // Skip hidden files, macOS sidecars and temporary files.
        if (
            name.charAt(0) === "." ||
            name.indexOf("._") === 0 ||
            name.indexOf("~$") === 0 ||
            lowerName.indexOf(".ds_store") !== -1
        ) {
            return false;
        }

        try {
            if (file.length === 0) {
                return false;
            }
        } catch (lengthError) {}

        return (
            /\.psd$/.test(lowerName) ||
            /\.psb$/.test(lowerName) ||
            /\.tif$/.test(lowerName) ||
            /\.tiff$/.test(lowerName)
        );
    }

    function collectFiles(folder, output, includeSubfolders) {
        var items = folder.getFiles();

        for (var i = 0; i < items.length; i++) {
            var item = items[i];

            if (item instanceof File && isSupportedFile(item)) {
                output.push(item);
            } else if (
                includeSubfolders &&
                item instanceof Folder &&
                item.name.charAt(0) !== "."
            ) {
                collectFiles(item, output, true);
            }
        }
    }

    function findLayerByName(container, wantedName) {
        var wantedLower = wantedName.toLowerCase();

        for (var i = 0; i < container.layers.length; i++) {
            var layer = container.layers[i];

            if (layer.name.toLowerCase() === wantedLower) {
                return layer;
            }

            if (layer.typename === "LayerSet") {
                var nestedMatch = findLayerByName(layer, wantedName);

                if (nestedMatch) {
                    return nestedMatch;
                }
            }
        }

        return null;
    }

    function unlockLayer(layer) {
        try {
            if (layer.isBackgroundLayer) {
                layer.isBackgroundLayer = false;
            }
        } catch (backgroundError) {}

        try {
            layer.allLocked = false;
        } catch (allLockedError) {}

        try {
            layer.pixelsLocked = false;
        } catch (pixelsLockedError) {}

        try {
            layer.positionLocked = false;
        } catch (positionLockedError) {}

        try {
            layer.transparentPixelsLocked = false;
        } catch (transparentPixelsLockedError) {}
    }

    function setSolidFillLayerColor(red, green, blue) {
        var setDescriptor = new ActionDescriptor();
        var targetReference = new ActionReference();

        targetReference.putEnumerated(
            stringIDToTypeID("contentLayer"),
            charIDToTypeID("Ordn"),
            charIDToTypeID("Trgt")
        );

        setDescriptor.putReference(
            charIDToTypeID("null"),
            targetReference
        );

        var solidColorDescriptor = new ActionDescriptor();
        var rgbDescriptor = new ActionDescriptor();

        rgbDescriptor.putDouble(
            charIDToTypeID("Rd  "),
            red
        );

        rgbDescriptor.putDouble(
            charIDToTypeID("Grn "),
            green
        );

        rgbDescriptor.putDouble(
            charIDToTypeID("Bl  "),
            blue
        );

        solidColorDescriptor.putObject(
            charIDToTypeID("Clr "),
            charIDToTypeID("RGBC"),
            rgbDescriptor
        );

        setDescriptor.putObject(
            charIDToTypeID("T   "),
            stringIDToTypeID("solidColorLayer"),
            solidColorDescriptor
        );

        executeAction(
            charIDToTypeID("setd"),
            setDescriptor,
            DialogModes.NO
        );
    }

    function fillPixelLayer(doc, hex) {
        var fillColor = new SolidColor();
        fillColor.rgb.hexValue = hex;

        var oldForeground = app.foregroundColor;
        app.foregroundColor = fillColor;

        try {
            doc.selection.selectAll();

            doc.selection.fill(
                fillColor,
                ColorBlendMode.NORMAL,
                100,
                false
            );
        } finally {
            try {
                doc.selection.deselect();
            } catch (selectionError) {}

            app.foregroundColor = oldForeground;
        }
    }

    function changeLayerColor(doc, targetLayer, hex) {
        doc.activeLayer = targetLayer;
        unlockLayer(targetLayer);

        var rgb = hexToRgb(hex);

        if (targetLayer.kind === LayerKind.SOLIDFILL) {
            setSolidFillLayerColor(
                rgb.r,
                rgb.g,
                rgb.b
            );

            return "solid fill";
        }

        if (targetLayer.kind === LayerKind.NORMAL) {
            fillPixelLayer(doc, hex);
            return "pixel";
        }

        throw new Error(
            'Layer "' + targetLayer.name +
            '" is not a Solid Color Fill layer or a normal pixel layer.'
        );
    }

    function createTimestamp() {
        var now = new Date();

        function pad(value) {
            return value < 10 ? "0" + value : value;
        }

        return (
            now.getFullYear() +
            pad(now.getMonth() + 1) +
            pad(now.getDate()) +
            "_" +
            pad(now.getHours()) +
            pad(now.getMinutes()) +
            pad(now.getSeconds())
        );
    }

    function formatRunDate() {
        var now = new Date();

        function pad(value) {
            return value < 10 ? "0" + value : value;
        }

        return (
            now.getFullYear() + "-" +
            pad(now.getMonth() + 1) + "-" +
            pad(now.getDate()) + " " +
            pad(now.getHours()) + ":" +
            pad(now.getMinutes()) + ":" +
            pad(now.getSeconds())
        );
    }

    function normalizePath(pathValue) {
        return pathValue.replace(/\\/g, "/");
    }

    function getFolderLabel(folder, rootFolder) {
        var folderPath = normalizePath(folder.fsName);
        var rootPath = normalizePath(rootFolder.fsName);

        if (folderPath.toLowerCase() === rootPath.toLowerCase()) {
            return "[SELECTED FOLDER]";
        }

        if (
            folderPath.toLowerCase().indexOf(
                rootPath.toLowerCase() + "/"
            ) === 0
        ) {
            return folderPath.substring(rootPath.length + 1);
        }

        return folder.name;
    }

    function sortFilesByFolderAndName(files) {
        files.sort(function (a, b) {
            var folderA = normalizePath(
                a.parent.fsName
            ).toLowerCase();

            var folderB = normalizePath(
                b.parent.fsName
            ).toLowerCase();

            if (folderA < folderB) {
                return -1;
            }

            if (folderA > folderB) {
                return 1;
            }

            var nameA = a.name.toLowerCase();
            var nameB = b.name.toLowerCase();

            if (nameA < nameB) {
                return -1;
            }

            if (nameA > nameB) {
                return 1;
            }

            return 0;
        });
    }

    function groupResultsByFolder(results) {
        var groups = [];

        for (var i = 0; i < results.length; i++) {
            var folderPath = results[i].file.parent.fsName;
            var currentGroup = null;

            if (groups.length > 0) {
                currentGroup = groups[groups.length - 1];
            }

            if (
                currentGroup === null ||
                currentGroup.folderPath !== folderPath
            ) {
                currentGroup = {
                    folder: results[i].file.parent,
                    folderPath: folderPath,
                    results: []
                };

                groups.push(currentGroup);
            }

            currentGroup.results.push(results[i]);
        }

        return groups;
    }

    function appendFolderSection(
        logEntries,
        group,
        folderNumber,
        totalFolders,
        globalStartIndex,
        totalFiles,
        rootFolder,
        settings
    ) {
        var folderProcessed = 0;
        var folderMissing = 0;
        var folderSkipped = 0;
        var folderFailed = 0;
        var problems = [];
        var successes = [];

        for (var i = 0; i < group.results.length; i++) {
            var result = group.results[i];

            if (result.status === "SUCCESS") {
                successes.push({
                    result: result,
                    localIndex: i
                });
                folderProcessed++;
            } else {
                problems.push({
                    result: result,
                    localIndex: i
                });

                if (result.status === "MISSING LAYER") {
                    folderMissing++;
                } else if (result.status === "SKIPPED") {
                    folderSkipped++;
                } else if (result.status === "FAILED") {
                    folderFailed++;
                }
            }
        }

        logEntries.push(
            "============================================================"
        );
        logEntries.push(
            "FOLDER " + folderNumber + " OF " + totalFolders
        );
        logEntries.push(
            "Folder: " + getFolderLabel(group.folder, rootFolder)
        );
        logEntries.push(
            "Full path: " + group.folder.fsName
        );
        logEntries.push(
            "Files in this folder: " + group.results.length
        );
        logEntries.push("");

        logEntries.push(
            "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
        );
        logEntries.push(
            "PROBLEMS IN THIS FOLDER: " + problems.length
        );
        logEntries.push(
            "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
        );
        logEntries.push("");

        if (problems.length === 0) {
            logEntries.push(
                "No errors, failures, missing layers or skipped files."
            );
            logEntries.push("");
        } else {
            for (var p = 0; p < problems.length; p++) {
                var problemItem = problems[p];
                var problemResult = problemItem.result;
                var problemGlobalIndex =
                    globalStartIndex +
                    problemItem.localIndex +
                    1;

                logEntries.push(
                    "[" + problemGlobalIndex + "/" + totalFiles + "] " +
                    problemResult.status
                );

                logEntries.push(
                    "    File: " + problemResult.file.name
                );

                if (problemResult.status === "MISSING LAYER") {
                    logEntries.push(
                        '    Error: Layer "' +
                        settings.layerName +
                        '" was not found.'
                    );
                } else if (problemResult.status === "SKIPPED") {
                    logEntries.push(
                        "    Reason: " + problemResult.message
                    );
                } else if (problemResult.status === "FAILED") {
                    logEntries.push(
                        "    Error: " + problemResult.message
                    );
                }

                logEntries.push("");
            }
        }

        logEntries.push(
            "------------------------------------------------------------"
        );
        logEntries.push(
            "SUCCESSFUL FILES: " + successes.length
        );
        logEntries.push(
            "------------------------------------------------------------"
        );
        logEntries.push("");

        if (successes.length === 0) {
            logEntries.push(
                "No files were successfully processed in this folder."
            );
            logEntries.push("");
        } else {
            for (var s = 0; s < successes.length; s++) {
                var successItem = successes[s];
                var successResult = successItem.result;
                var successGlobalIndex =
                    globalStartIndex +
                    successItem.localIndex +
                    1;

                logEntries.push(
                    "[" + successGlobalIndex + "/" + totalFiles + "] SUCCESS"
                );

                logEntries.push(
                    "    File: " + successResult.file.name
                );

                logEntries.push(
                    "    Layer: " + settings.layerName
                );

                logEntries.push(
                    "    Layer type: " + successResult.layerType
                );

                logEntries.push(
                    "    Color: #" + settings.hex.toLowerCase()
                );

                logEntries.push("");
            }
        }

        logEntries.push(
            "------------------------------------------------------------"
        );
        logEntries.push("FOLDER SUMMARY");
        logEntries.push(
            "    Processed and saved: " + folderProcessed
        );
        logEntries.push(
            "    Layer not found: " + folderMissing
        );
        logEntries.push(
            "    Skipped: " + folderSkipped
        );
        logEntries.push(
            "    Failed: " + folderFailed
        );

        logEntries.push("");
        logEntries.push("");
        logEntries.push("");
    }

    function normalizeHex(value) {
        var cleaned = trimString(value).replace(/^#/, "");

        if (/^[0-9a-fA-F]{3}$/.test(cleaned)) {
            cleaned =
                cleaned.charAt(0) + cleaned.charAt(0) +
                cleaned.charAt(1) + cleaned.charAt(1) +
                cleaned.charAt(2) + cleaned.charAt(2);
        }

        if (/^[0-9a-fA-F]{6}$/.test(cleaned)) {
            return cleaned.toUpperCase();
        }

        return null;
    }

    function hexToRgb(hex) {
        return {
            r: parseInt(hex.substring(0, 2), 16),
            g: parseInt(hex.substring(2, 4), 16),
            b: parseInt(hex.substring(4, 6), 16)
        };
    }

    function trimString(value) {
        return value.replace(/^\s+|\s+$/g, "");
    }

    function getPresetsFile() {
        return new File(
            Folder.userData.fsName + "/BatchSetLayerColorPresets.json"
        );
    }

    function loadCustomPresets() {
        var presets = [];
        var presetsFile = getPresetsFile();

        if (!presetsFile.exists) {
            return presets;
        }

        try {
            presetsFile.encoding = "UTF8";
            presetsFile.open("r");
            var contents = presetsFile.read();
            presetsFile.close();

            var parsed = JSON.parse(contents);

            if (parsed && parsed.length) {
                for (var i = 0; i < parsed.length; i++) {
                    var entry = parsed[i];
                    var hex = entry ? normalizeHex(entry.hex) : null;

                    if (entry && entry.name && hex) {
                        presets.push({
                            name: trimString(entry.name),
                            hex: hex
                        });
                    }
                }
            }
        } catch (loadError) {
            // If the presets file is missing or corrupt, just start empty.
        }

        return presets;
    }

    function saveCustomPresets(presets) {
        try {
            var presetsFile = getPresetsFile();
            presetsFile.encoding = "UTF8";
            presetsFile.open("w");
            presetsFile.write(JSON.stringify(presets));
            presetsFile.close();
        } catch (saveError) {
            // Non-fatal: the script still works, presets just won't persist.
        }
    }

    var files = [];
    collectFiles(
        sourceFolder,
        files,
        settings.includeSubfolders
    );

    sortFilesByFolderAndName(files);

    if (files.length === 0) {
        app.displayDialogs = oldDialogs;

        alert(
            "No valid PSD, PSB, TIF or TIFF files were found."
        );

        return;
    }

    var processed = 0;
    var missingLayer = 0;
    var skipped = 0;
    var failed = 0;
    var results = [];

    for (var i = 0; i < files.length; i++) {
        var doc = null;

        try {
            if (!files[i].exists) {
                skipped++;

                results.push({
                    file: files[i],
                    status: "SKIPPED",
                    message: "The file could not be found."
                });

                continue;
            }

            doc = app.open(files[i]);

            var targetLayer = findLayerByName(
                doc,
                settings.layerName
            );

            if (!targetLayer) {
                missingLayer++;

                results.push({
                    file: files[i],
                    status: "MISSING LAYER",
                    message: ""
                });

                doc.close(SaveOptions.DONOTSAVECHANGES);
                doc = null;
                continue;
            }

            if (targetLayer.typename !== "ArtLayer") {
                throw new Error(
                    '"' + settings.layerName +
                    '" is a layer group, not an editable layer.'
                );
            }

            var changedLayerType = changeLayerColor(
                doc,
                targetLayer,
                settings.hex
            );

            doc.close(SaveOptions.SAVECHANGES);
            doc = null;
            processed++;

            results.push({
                file: files[i],
                status: "SUCCESS",
                layerType:
                    changedLayerType === "solid fill"
                        ? "Solid Color Fill"
                        : "Normal pixel layer",
                message: ""
            });

        } catch (error) {
            failed++;

            results.push({
                file: files[i],
                status: "FAILED",
                message: error.message
            });

            try {
                if (doc !== null) {
                    doc.close(SaveOptions.DONOTSAVECHANGES);
                }
            } catch (closeError) {}

            doc = null;
        }
    }

    app.displayDialogs = oldDialogs;

    var logEntries = [];
    var folderGroups = groupResultsByFolder(results);

    logEntries.push("BATCH SET LAYER COLOR LOG");
    logEntries.push(
        "Run date: " + formatRunDate()
    );
    logEntries.push(
        "Selected folder: " + sourceFolder.fsName
    );
    logEntries.push(
        'Layer name: "' + settings.layerName + '"'
    );
    logEntries.push(
        "Color preset: " + settings.presetName
    );
    logEntries.push(
        "HEX color: #" + settings.hex.toLowerCase()
    );
    logEntries.push(
        "Include subfolders: " +
        (settings.includeSubfolders ? "Yes" : "No")
    );
    logEntries.push(
        "Folders processed: " + folderGroups.length
    );
    logEntries.push(
        "Files found: " + files.length
    );
    logEntries.push("");
    logEntries.push("");

    var globalStartIndex = 0;

    for (var groupIndex = 0;
         groupIndex < folderGroups.length;
         groupIndex++) {

        appendFolderSection(
            logEntries,
            folderGroups[groupIndex],
            groupIndex + 1,
            folderGroups.length,
            globalStartIndex,
            files.length,
            sourceFolder,
            settings
        );

        globalStartIndex +=
            folderGroups[groupIndex].results.length;
    }

    logEntries.push(
        "============================================================"
    );
    logEntries.push("OVERALL SUMMARY");
    logEntries.push(
        "Processed and saved: " + processed
    );
    logEntries.push(
        "Layer not found: " + missingLayer
    );
    logEntries.push(
        "Skipped: " + skipped
    );
    logEntries.push(
        "Failed: " + failed
    );
    logEntries.push(
        "Total files: " + files.length
    );

    var message =
        "Finished.\n\n" +
        "Preset: " + settings.presetName + "\n" +
        "Color: #" + settings.hex.toLowerCase() + "\n" +
        'Layer: "' + settings.layerName + '"\n' +
        "Include subfolders: " +
        (settings.includeSubfolders ? "Yes" : "No") +
        "\n\n" +
        "Processed and saved: " + processed + "\n" +
        "Layer not found: " + missingLayer + "\n" +
        "Skipped: " + skipped + "\n" +
        "Failed: " + failed;

    var timestamp = createTimestamp();
    var logFile = new File(
        sourceFolder.fsName +
        "/BATCH_SET_LAYER_COLOR_LOG_" +
        timestamp +
        ".txt"
    );

    try {
        logFile.encoding = "UTF8";
        logFile.open("w");
        logFile.write(logEntries.join("\n"));
        logFile.close();

        message +=
            "\n\nA structured log was saved in the selected folder:\n" +
            logFile.name;
    } catch (logError) {
        message +=
            "\n\nThe log file could not be written.\n" +
            "Log error: " + logError.message;
    }

    alert(message);
})();
