js/AddonUtils.js

const pLimit	= require("p-limit");
/**
 * Represents a utility class for addons.
 * @constructor
 * @param {Object} objectToApply - The object to apply the utility methods and properties to.
 * @param {boolean} [override=false] - Indicates whether to override existing properties in the object.
 */
const AddonUtils = function(objectToApply={}, override = false) {
    if(new.target === undefined) {
        // if called directly
        return new AddonUtils(objectToApply, override)
    }

    this.alwaysExportFromStage = false;
    this.workerNum  = 4;
    this.defaultInjectCopyOption = "copyIfNotExist"

    if (objectToApply) {
        for (let i in this) {
            if (!override) {
                if (typeof objectToApply[i] !== "undefined") continue;
            } else {
                if (typeof objectToApply[i] !== "undefined") console.log("%cOverriding", "color:yellow", i)
            }
            if (typeof this[i] == "function") {
                objectToApply[i] = this[i].bind(objectToApply);
            } else {
                objectToApply[i] = this[i];
            }
        }
    }
}
/**
 * Object representing translation data.
 * @typedef {Object} TransData
 * @property {string[][]} data - An array of arrays of strings representing translation data.
 * @property {string[][]} context - An array of arrays of strings representing context data.
 * @property {string[][]} tags - An array of arrays of strings representing tags data.
 * @property {Object[]} parameters - An array of objects representing parameters data.
 * @property {Object} indexIds - An object representing index IDs.
 */

/**
 * Retrieves translation data for a given file.
 * @async
 * @function getTransData
 * @memberof AddonUtils
 * @param {string} file - The name of the file.
 * @param {string} basePath - The base path for the file.
 * @param {Object} [options={}] - Additional options.
 * @returns {Promise<TransData>} A promise resolving to the file data object containing data, context, tags, parameters, and indexIds.
 */
AddonUtils.prototype.getTransData = async function(file, basePath, options={}) {
    // replace with your logic
    // should return fileData object
    const transData = {
        "data": [[]],
        "context": [[]],
        "tags": [],
        "parameters": [],
        "indexIds": {}
    }
    return transData;
}

/**
 * Writes a file with given translation.
 * @async
 * @function writeFile
 * @memberof AddonUtils
 * @param {string} file - The name of the file.
 * @param {string} basePath - The base path for the file.
 * @param {string} targetDir - The target directory.
 * @param {Object} [options={}] - Additional options.
 * @returns {Promise<string>} A promise resolving to the path of the written file.
 */
AddonUtils.prototype.writeFile = async function(file, basePath, targetDir, options={}) {
    // replace with your logic
    return nwPath.join(basePath, file)
}

/**
 * Creates translation data for multiple files.
 * @async
 * @function createTransData
 * @memberof AddonUtils
 * @param {string[]} files - An array of file names.
 * @param {string} basePath - The base path for the files.
 * @param {Object} [options={}] - Additional options.
 * @returns {Promise<TransData>} A promise resolving to the translation data object.
 */
AddonUtils.prototype.createTransData = async function(files, basePath, options={}) {
    const transFiles = {}
    const queue = []
    const limit = pLimit(this.workerNum || 4);
    ui.log(`Processing with ${this.workerNum || 4} concurrent worker(s).`);
    for (let file of files) {
        queue.push(limit(async ()=>{
            let thisFileData = await this.getTransData(file, basePath, options) || {};
            // skip if no value is presents
            if (!thisFileData) return;
            let thisFileObj = trans.createFileData(file, {
                    originalFormat: await this.setFileFormatName(file, basePath, options) || "Translator++ Original Format"
                }, 
                {
                    leadSlash:true
                }
            );
            transFiles[thisFileObj.path] = {...thisFileObj, ...thisFileData};
        }));
    }

    await Promise.all(queue);


    const transData = trans.initTransData()
    console.log("%c initializing trans data", "color:aqua");
    transData.project.gameEngine    = await this.setEngineName(transData, basePath, options) || this.gameEngine,
    transData.project.files         = common.sortObjectByKey(transFiles);
    transData.project.parser        = this.parserName || this.package?.name,
    transData.project.parserVersion = this.parserVersion || this.package?.version,
    transData.project.options.init  = options.init;

    transData.project.gameTitle = await this.setProjectTitle(transData, basePath, options);

    return transData
}

AddonUtils.prototype.setFileFormatName = async function(file, basePath, options={}) {
    const ext = nwPath.extname(file);
    if (ext) {
        return  ext.substr(1).toUpperCase()+" file"
    }
    return;
}

AddonUtils.prototype.setEngineName = async function(transData, basePath, options={}) {
    return;
}

AddonUtils.prototype.setProjectTitle = async function(transData, basePath, options={}) {
    return options?.init?.projectTitle || "untitled project";
}

AddonUtils.prototype.setProjectLocation = async function(transData, basePath, options={}) {
    // add with your logic
    return;
}
AddonUtils.prototype.finalizeTransData = async function(transData, basePath, options={}) {
    // add with your logic
    return transData;
}


/**
 * Creates a project based on provided files and options.
 * @async
 * @function createProject
 * @memberof AddonUtils
 * @param {string[]} files - An array of file names.
 * @param {string} basePath - The base path for the files.
 * @param {Object} [options={}] - Additional options.
 * @returns {Promise<Object>} A promise resolving to the created project data.
 */
AddonUtils.prototype.createProject = async function(files, basePath, options={}) {
    console.log("Creating project with args", arguments);

    var transData = await this.createTransData.call(this, files, basePath, options);

    ui.log("Create staging data")
    var stagingInfo;
    if (options.noCache) {
        stagingInfo = await common.initStaging(transData.project.projectId, options?.init?.projectTitle, {noCache:true});
    } else {
        stagingInfo = await common.initStaging(transData.project.projectId, options?.init?.projectTitle, {...options.init, ...{basePath:basePath, files:files}});
    }
    console.log("stagging Info :", stagingInfo);
    transData.project.cache = stagingInfo;
    transData.project.loc ||= (await this.setProjectLocation(transData, basePath, options)) || basePath;
    console.log("transData:", transData);

    transData = (await this.finalizeTransData(transData, basePath, options)) || transData;

    trans.openFromTransObj(transData, {isNew: true});
    return transData;
}

AddonUtils.prototype.selectTranslationData = function(translationDatas, path) {
    const blankResult = {
        info:{},
        translationPair:{}
    }
    if (!translationDatas?.translationData?.[path]) return blankResult;
    return translationDatas.translationData[path];
}

AddonUtils.prototype.beforeExportToFolder = async function(destinationDir, transData, options) {
    // replace with your logic
}
AddonUtils.prototype.afterExportToFolder = async function(exportResult, destinationDir, transData, options) {
    // replace with your logic
}

/**
 * Exports translation data to a folder.
 * @async
 * @function exportToFolder
 * @memberof AddonUtils
 * @param {string} destinationDir - The directory to export to.
 * @param {Object} [transData] - The translation data object. If not provided, fetched from the current context.
 * @param {Object} [options={}] - Additional options.
 * @returns {Promise<string[]>} A promise resolving to an array of exported file paths.
 */
AddonUtils.prototype.exportToFolder = async function(destinationDir, transData, options) {
    console.log("Export to folder with arguments", arguments);

	transData 			= transData || trans.getSaveData();
	options 			= options || {};
	options.translationDatas = options.translationDatas || trans.getTranslationData(transData, options);
    options.resourcePath ||= trans.getStagingDataPath(transData);

    // if none is selected then select all except references
    if (!options.files?.length) options.files = trans.getAllFiles(transData, true);
    await this.beforeExportToFolder(destinationDir, transData, options);

    const queue = []
    const limit = pLimit(this.workerNum || 4);
    const result = []
    for (let file of options.files) {
        if (!await common.isFileAsync(nwPath.join(options.resourcePath, file))) {
            ui.logError("File not found: "+nwPath.join(options.resourcePath, file));
            continue;
        }
        queue.push(limit(async ()=>{
            let thisOptions = {
                translationDatas: options.translationDatas,
                parserOptions: {
                    writeMode: true,
                },
                projectInfo: {
                    init: transData?.project?.options?.init
                },
                exportOptions: options.exportOptions
            }
            let thisResult = await this.writeFile(file, options.resourcePath, destinationDir, thisOptions);
            result.push(thisResult)
        }));
    }
    
    await Promise.all(queue);
    await this.afterExportToFolder(result, destinationDir, transData, options);
    return result;
}

/**
 * Exports translation data as a zip file.
 * @async
 * @function exportAsZip
 * @memberof AddonUtils
 * @param {string} targetPath - The target path for the zip file.
 * @param {Object} [transData] - The translation data object. If not provided, fetched from the current context.
 * @param {Object} [options={}] - Additional options.
 * @returns {Promise<string>} A promise resolving to the path of the created zip file.
 */
AddonUtils.prototype.exportAsZip = async function(targetPath, transData, options) {
	transData 			= transData || trans.getSaveData();
	options 			= options || {};
	options.translationDatas = options.translationDatas || trans.getTranslationData(transData, options);
    options.resourcePath ||= trans.getStagingDataPath(transData);

	var tmpPath 	= nwPath.join(nw.process.env.TMP, transData.project.projectId);
	await ui.log("Generating a temporary folder", tmpPath);

    const fse = require("fs-extra");
	try {
		await fse.remove(tmpPath); 
		await common.mkDir(tmpPath, {recursive:true});
	} catch(e) {
		console.warn("Unable to create directory ", tmpPath);
		console.error(e);
		return;
	}

	await ui.log("Exporting to a temporary folder", tmpPath);
    await this.exportToFolder(tmpPath, transData, options);
	await ui.log("Data exported to the temp dir");

	await ui.log("Zipping temporary data");
	var _7z = require('7zip-min');
	return new Promise((resolve, reject) => {
		_7z.cmd(['a', '-tzip', targetPath, tmpPath+"\\*"], err => {
			resolve(targetPath);
		});	
	})
}


/**
 * Handles exporting translation data based on the specified mode.
 * @async
 * @function handleExport
 * @memberof AddonUtils
 * @param {string} targetPath - The target path for the exported data.
 * @param {Object} options - Export options.
 * @param {string} options.mode - The export mode ('zip' or 'directory').
 * @returns {Promise<void>} A promise resolving when export is completed.
 */
AddonUtils.prototype.handleExport = async function(targetPath, options) {
    ui.showLoading();

    if (options.mode == "zip") {
        ui.loadingProgress("Processing", "Exporting as a zip", {consoleOnly:false, mode:'consoleOutput'});
        ui.log("Target path: ", targetPath);
        ui.log("Exporting with options: ", options);

        await this.exportAsZip(targetPath, undefined, options);
    } else if (options.mode == "dir") {
        ui.loadingProgress("Processing", "Exporting to directory", {consoleOnly:false, mode:'consoleOutput'});
        ui.log("Target directory: ", targetPath);
        ui.log("Exporting with options: ", options);
    
        await this.exportToFolder(targetPath, undefined, options);
    } 
    ui.loadingProgress("Completed", "Exported completed!", {consoleOnly:false, mode:'consoleOutput'});
    ui.loadingEnd();

}

AddonUtils.prototype.injectCopyMaterials = async function(sourceDir, targetDir, injectOption) {
    const bCopy = require("better-copy");

    injectOption.copyOptions ||= this.defaultInjectCopyOption;
    if (injectOption.copyOptions == "copyNothing") return;

    await ui.log(`Copy ${sourceDir} to ${targetDir}`);
    await ui.log("Copy option:"+injectOption.copyOptions);

    var overWrite = true;
    if (injectOption.copyOptions == "copyIfNotExist") overWrite = false;
    await ui.log("Overwrite?: "+overWrite.toString());
    await bCopy(sourceDir, targetDir, {
        onBeforeCopy: async (from, to) => {
            await ui.log(`Start copying ${from}`);
        },
        onAfterCopy: async (from, to) => {
            await ui.log(`Successfully copied into ${to}`);
        },
        overwrite: overWrite
    });

}

AddonUtils.prototype.injectDone = async function(targetDir, sourceMaterial, options) {
    // do something here
    ui.loadingProgress("Completed", "Exported completed!", {consoleOnly:false, mode:'consoleOutput'});
    ui.loadingEnd();
    return common.halt();
}


// Static
AddonUtils.applyCommonHandlers = function(supportedEngines, thisAddon) {

    engines.addHandler(supportedEngines, 'exportHandler', async function(targetPath, options) {
        if (options.mode !== "dir" && options.mode !== "zip") return false;

        await thisAddon.handleExport(targetPath, options);
        return common.halt();
    })

    engines.addHandler(supportedEngines, 'onOpenInjectDialog', async function($dialogInject, options) {
        $dialogInject.find(".copyOptionsBlock").removeClass("hidden")
    })

    engines.addHandler(supportedEngines, 'injectHandler', async function(targetDir, sourceMaterial, options) {
        ui.showLoading();
        ui.loadingProgress("Processing", "Injecting to directory", {consoleOnly:false, mode:'consoleOutput'});
        ui.log("Target directory: ", targetDir);
        ui.log("Source Material: ", sourceMaterial);
        if (sourceMaterial) ui.log("Warning: The app will try to apply translations to the files with the exact match relative location.");
        
        await thisAddon.injectCopyMaterials(sourceMaterial, targetDir, options);
        ui.log("Injecting with options: ", options);
        
        console.log("self.alwaysExportFromStage", thisAddon.alwaysExportFromStage);
        if (!thisAddon.alwaysExportFromStage) {
            options.resourcePath = sourceMaterial;
        }

        await thisAddon.exportToFolder(targetDir, undefined, options);

        return await thisAddon.injectDone(targetDir, sourceMaterial, options)
	});
}

module.exports = AddonUtils;