diff --git a/DEVELOP.md b/DEVELOP.md index 0d3c2a98..7178b6b0 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -64,13 +64,13 @@ This package provides a number of test cases: See if translation files match keys -`# npm run langtest` +`# npm run lang_test` ### Fix translations If translation files are mismatched, this will fix it (english is master) -`# npm run langfix` +`# npm run lang_fix` ### Check Dependencies diff --git a/lib/single-mod-checker.js b/lib/modCheckLib.js similarity index 57% rename from lib/single-mod-checker.js rename to lib/modCheckLib.js index 89445b30..e7199be2 100644 --- a/lib/single-mod-checker.js +++ b/lib/modCheckLib.js @@ -9,12 +9,474 @@ const fs = require('fs') const path = require('path') const admZip = require('adm-zip') -//const unzip = require('unzip-stream') const glob = require('glob') const xml2js = require('xml2js') const crypto = require('crypto') const { decodeDXT, parseDDS } = require('./ddsLibrary') -const PNG = require('pngjs').PNG +const JPEG = require('jpeg-js') + + +class modFileCollection { + #ignoreList = [ + '^npm-debug\\.log$', + '^\\..*\\.swp$', + '^Thumbs\\.db$', + '^thumbs\\.db$', + '^ehthumbs\\.db$', + '^Desktop\\.ini$', + '^desktop\\.ini$', + '@eaDir$', + ] + #junkRegex = new RegExp(this.#ignoreList.join('|')) + + #bindConflict = {} + #map_CollectionToFolder = {} + #map_CollectionToFolderRelative = {} + #map_CollectionToFullName = {} + #map_CollectionToName = {} + #map_FolderContents = {} + #map_FolderToCollection = {} + #map_ModUUIDToShortName = {} + #set_Collections = new Set() + + #modHubList = { 'mods' : {}, 'last' : [] } + #modHubVersion = {} + + #list_allMods = {} + #list_newMods = new Set() + + #log = null + #loadingWindow = {} + #localeFunction = null + #map_CollectionNotes = null + #modCache = {} + #modCacheStore = {} + + #userHome = '' + #useSyncSafeMode = false + #skipCache = false + + #scanPromise = [] + + constructor(logger, notes, cache, homeDir, loadingWindow, locale, skipCache = false) { + this.#modCache = cache + this.#log = logger + this.#map_CollectionNotes = notes + this.#userHome = homeDir + this.#skipCache = skipCache + this.#loadingWindow = loadingWindow + this.#localeFunction = locale + } + + mapFolderToCollection(folder) { + return this.#map_FolderToCollection[folder] || null + } + mapCollectionToFolder(collectKey) { + return this.#map_CollectionToFolder[collectKey] + } + mapCollectionToName(collectKey) { + return this.#map_CollectionToName[collectKey] + } + mapCollectionToFullName(collectKey) { + return this.#map_CollectionToFullName[collectKey] + } + + set newCollectionOrder(newSetOrder) { this.#set_Collections = newSetOrder } + + removeCollection(collectKey) { + const thisRealPath = this.#map_CollectionToFolder[collectKey] + + this.#set_Collections.delete(collectKey) + + delete this.#map_FolderToCollection[thisRealPath] + delete this.#list_allMods[collectKey] + delete this.#map_FolderContents[collectKey] + delete this.#map_CollectionToFolder[collectKey] + delete this.#map_CollectionToFolderRelative[collectKey] + delete this.#map_CollectionToName[collectKey] + delete this.#map_CollectionToFullName[collectKey] + } + + get bindConflict() { return this.#bindConflict } + get collections() { return this.#set_Collections } + + getModListFromCollection(collectKey) { + return Object.values(this.#list_allMods[collectKey].mods) + } + getModCollection(collectKey) { + return this.#list_allMods[collectKey] + } + + get modHubList() { return this.#modHubList } + set modHubList(newList) { this.#modHubList = newList } + get modHubVersion() { return this.#modHubVersion } + set modHubVersion(newList) { this.#modHubVersion = newList } + + modHubFullRecord(thisMod, asArray = true) { + /* Return [ID, Version, Recent] */ + const modHubID = this.modHubModRecord(thisMod) + + if ( asArray ) { + return [ + modHubID, + this.modHubVersionModHubId(modHubID), + this.#modHubList.last.includes(modHubID) + ] + } + + return { + id : modHubID, + version : this.modHubVersionModHubId(modHubID), + recent : this.#modHubList.last.includes(modHubID), + } + } + modHubModRecord(thisMod) { + return this.#modHubList.mods[thisMod.fileDetail.shortName] || null + } + modHubModUUID(modUUID) { + return this.#modHubList.mods[this.#map_ModUUIDToShortName[modUUID]] || null + } + modHubVersionUUID(modUUID) { + return this.#modHubVersion[this.modHubModUUID(modUUID)] || null + } + modHubVersionModRecord(thisMod) { + return this.#modHubVersion[this.modHubModRecord(thisMod)] || null + } + modHubVersionModHubId(modHubID) { + return this.#modHubVersion[modHubID] || null + } + + modColUUIDToRecord(ID) { + const idParts = ID.split('--') + return this.#list_allMods?.[idParts[0]]?.mods?.[idParts[1]] || null + } + + modColUUIDsToRecords(IDs) { + const theseMods = [] + IDs.forEach((thisColUUID) => { theseMods.push(this.modColUUIDToRecord(thisColUUID)) }) + return theseMods + } + + modColAndUUID(collectKey, modKey) { + return this.#list_allMods[collectKey].mods[modKey] + } + + clearAll() { + this.#scanPromise = [] + this.#map_FolderContents = {} + this.#map_CollectionToFolder = {} + this.#map_CollectionToFolderRelative = {} + this.#map_FolderToCollection = {} + this.#map_CollectionToName = {} + this.#map_CollectionToFullName = {} + this.#map_ModUUIDToShortName = {} + this.#list_allMods = {} + this.#list_newMods = new Set() + this.#set_Collections.clear() + } + + set syncSafe(mode) { this.#useSyncSafeMode = mode } + + addCollection(folder) { + const goodFolderContents = [] + const thisFolderKey = this.#getMD5Hash(folder, 'col_') + const thisRealPath = path.normalize(folder) + const thisShortName = path.basename(thisRealPath) + const thisFullName = this.#populateFullName(thisFolderKey, thisShortName) + + try { + const folderContents = fs.readdirSync(thisRealPath, {withFileTypes : true}) + + folderContents.forEach((thisFile) => { + if ( !this.#junkRegex.test(thisFile.name) ) { goodFolderContents.push(thisFile) } + }) + } catch (e) { + this.#log.log.danger(`Couldn't read folder: ${thisRealPath} :: ${e}`, 'collection-reader') + return null + } + + this.#set_Collections.add(thisFolderKey) + this.#list_allMods[thisFolderKey] = { + alphaSort : [], + dependSet : new Set(), + folderSize : 0, + fullName : thisFullName, + mods : {}, + modSet : new Set(), + name : thisShortName, + } + + this.#map_FolderContents [thisFolderKey] = goodFolderContents + this.#map_CollectionToFolder [thisFolderKey] = thisRealPath + this.#map_CollectionToFolderRelative [thisFolderKey] = this.#toHomeDir(thisRealPath) + this.#map_FolderToCollection [thisRealPath] = thisFolderKey + this.#map_CollectionToName [thisFolderKey] = thisShortName + this.#map_CollectionToFullName [thisFolderKey] = thisFullName + + return { + collectKey : thisFolderKey, + fileCount : goodFolderContents.length, + } + } + + async processMods() { + this.#modCacheStore = this.#modCache.store + + for ( const collectKey of this.#set_Collections ) { + for ( const thisFile of this.#map_FolderContents[collectKey] ) { + this.#scanPromise.push(this.#addMod(collectKey, thisFile)) + } + } + + Promise.all(this.#scanPromise).then(() => { + this.#modCache.store = this.#modCacheStore + this.#doAlphaSort() + this.#processBindConflicts() + }) + } + + get processPromise() { return Promise.all(this.#scanPromise) } + + #doAlphaSort() { + this.#set_Collections.forEach((collectKey) => { + this.#list_allMods[collectKey].alphaSort.sort() + }) + } + + #cacheHit(thisFileStats) { + if ( thisFileStats.folder || this.#skipCache || thisFileStats.hashString === null ) { return false } + + return this.#modCacheStore[thisFileStats.hashString] ?? false + } + + #fileStats(collectKey, thisFile) { + const folderLoc = this.#map_CollectionToFolder[collectKey] + const fullPath = path.join(folderLoc, thisFile.name) + const fileStats = { + isFolder : null, + date : null, + birthDate : null, + size : null, + error : false, + hashString : null, + fullPath : fullPath, + } + + try { + if ( thisFile.isSymbolicLink() ) { + const thisSymLink = fs.readlinkSync(fullPath) + const thisSymLinkStat = fs.lstatSync(path.join(folderLoc, thisSymLink)) + + fileStats.isFolder = thisSymLinkStat.isDirectory() + fileStats.date = thisSymLinkStat.ctime + fileStats.birthDate = thisSymLinkStat.birthtime + + if ( !fileStats.isFolder ) { fileStats.size = thisSymLinkStat.size } + } else { + const theseStats = fs.statSync(fullPath) + + fileStats.isFolder = thisFile.isDirectory() + fileStats.date = theseStats.ctime + fileStats.birthDate = theseStats.birthtime + + if ( !fileStats.isFolder ) { fileStats.size = theseStats.size } + } + + if ( fileStats.isFolder ) { + let bytes = 0 + glob.sync('**', { cwd : path.join(fullPath) }).forEach((file) => { + try { + const stats = fs.statSync(path.join(fullPath, file)) + if ( stats.isFile() ) { bytes += stats.size } + } catch { /* Do Nothing if we can't read it. */ } + }) + fileStats.size = bytes + } else { + fileStats.hashString = this.#getMD5Hash(`${thisFile.name}-${fileStats.size}-${(this.#useSyncSafeMode)?fileStats.birthDate.toISOString():fileStats.date.toISOString()}`) + } + } catch (e) { + this.#log.log.warning(`Unable to stat file ${thisFile.name} in ${folderLoc} : ${e}`, 'file-stat') + fileStats.isFolder = false + fileStats.size = 0 + fileStats.date = new Date(1969, 1, 1, 0, 0, 0, 0) + fileStats.birthDate = new Date(1969, 1, 1, 0, 0, 0, 0) + fileStats.error = true + } + return fileStats + } + + #addModToData(collectKey, modRecord) { + const thisModRecord = { ...modRecord } + + thisModRecord.currentCollection = collectKey + thisModRecord.colUUID = `${collectKey}--${thisModRecord.uuid}` + thisModRecord.modHub = this.modHubFullRecord(thisModRecord, false) + + this.#map_ModUUIDToShortName[thisModRecord.uuid] = thisModRecord.fileDetail.shortName + this.#list_allMods[collectKey].mods[thisModRecord.uuid] = thisModRecord + this.#list_allMods[collectKey].folderSize += thisModRecord.fileDetail.fileSize + this.#list_allMods[collectKey].modSet.add(thisModRecord.uuid) + this.#list_allMods[collectKey].dependSet.add(thisModRecord.fileDetail.shortName) + this.#list_allMods[collectKey].alphaSort.push(`${thisModRecord.fileDetail.shortName}::${thisModRecord.uuid}`) + } + + #processBindConflicts() { + this.#bindConflict = {} + + this.#set_Collections.forEach((collectKey) => { + this.#bindConflict[collectKey] = {} + + const collectionBinds = {} + + this.#list_allMods[collectKey].modSet.forEach((modUUID) => { + const thisMod = this.#list_allMods[collectKey].mods[modUUID] + + Object.keys(thisMod.modDesc.binds).forEach((actName) => { + thisMod.modDesc.binds[actName].forEach((keyCombo) => { + if ( keyCombo === '' ) { return } + + const safeCat = thisMod.modDesc.actions[actName] || 'UNKNOWN' + const thisCombo = `${safeCat}--${keyCombo}` + + collectionBinds[thisCombo] ??= [] + collectionBinds[thisCombo].push(thisMod.fileDetail.shortName) + }) + }) + }) + + Object.keys(collectionBinds).forEach((keyCombo) => { + if ( collectionBinds[keyCombo].length > 1 ) { + collectionBinds[keyCombo].forEach((modName) => { + this.#bindConflict[collectKey][modName] ??= {} + this.#bindConflict[collectKey][modName][keyCombo] = collectionBinds[keyCombo].filter((w) => w !== modName) + if ( this.#bindConflict[collectKey][modName][keyCombo].length === 0 ) { + delete this.#bindConflict[collectKey][modName][keyCombo] + } + }) + } + }) + }) + + Object.keys(this.#bindConflict).forEach((collectKey) => { + Object.keys(this.#bindConflict[collectKey]).forEach((modName) => { + if ( Object.keys(this.#bindConflict[collectKey][modName]).length === 0 ) { + delete this.#bindConflict[collectKey][modName] + } + }) + }) + } + + #addMod(collectKey, thisFile) { + return new Promise((resolve) => { + let isDone = false + const thisFileStats = this.#fileStats(collectKey, thisFile) + + // Check cache + const modInCache = this.#cacheHit(thisFileStats) + if ( modInCache ) { + this.#log.log.debug(`Adding mod FROM cache: ${modInCache.fileDetail.shortName}`, `mod-${modInCache.uuid}`) + this.#addModToData(collectKey, modInCache) + this.#loadingWindow.count() + isDone = true + resolve(true) + } + + if ( !isDone && !thisFileStats.isFolder && !thisFile.name.endsWith('.zip') ) { + const thisModRecord = new notModFileChecker( + thisFileStats.fullPath, + false, + thisFileStats.size, + thisFileStats.date, + this.#log + ) + this.#addModToData(collectKey, thisModRecord) + this.#loadingWindow.count() + isDone = true + resolve(true) + } + + if ( !isDone) { + try { + const thisModRecord = new modFileChecker( + thisFileStats.fullPath, + thisFileStats.isFolder, + thisFileStats.size, + (this.#useSyncSafeMode) ? thisFileStats.birthDate : thisFileStats.date, + thisFileStats.hashString, + this.#log, + this.#localeFunction + ) + + thisModRecord.doTests().then(() => { + const thisModRecordStore = thisModRecord.storable + + this.#addModToData(collectKey, thisModRecordStore) + + if ( thisFileStats.hashString !== null ) { + this.#log.log.info('Adding mod to cache', `mod-${thisModRecordStore.uuid}`) + this.#list_newMods.add(thisFileStats.hashString) + this.#modCacheStore[thisFileStats.hashString] = thisModRecordStore + } + }) + } catch (e) { + this.#log.log.danger(`Couldn't process file: ${thisFileStats.fullPath} :: ${e}`, 'collection-reader') + const thisModRecord = new notModFileChecker( + thisFileStats.fullPath, + false, + thisFileStats.size, + thisFileStats.date, + this.#log + ) + this.#addModToData(collectKey, thisModRecord) + } finally { + this.#loadingWindow.count() + resolve(true) + } + } + }) + } + + async toRenderer(extra = null) { + this.#log.log.debug('Collection Render Return Called', 'collection-reader') + + return Promise.all(this.#scanPromise).then(() => { + this.#log.log.debug('Collection Render Return Firing', 'collection-reader') + + return { + currentLocale : this.#localeFunction(), + opts : extra, + bindConflict : this.#bindConflict, + modList : this.#list_allMods, + set_Collections : this.#set_Collections, + collectionToFolder : this.#map_CollectionToFolder, + collectionToFolderRelative : this.#map_CollectionToFolderRelative, + folderToCollection : this.#map_FolderToCollection, + collectionToName : this.#map_CollectionToName, + collectionToFullName : this.#map_CollectionToFullName, + collectionNotes : this.#map_CollectionNotes.store, + newMods : this.#list_newMods, + modHub : { + list : this.#modHubList, + version : this.#modHubVersion, + }, + } + }) + } + + #toHomeDir(folder) { + return folder.replaceAll(this.#userHome, '~') + } + + #populateFullName(collectKey, shortName) { + const tagLine = this.#map_CollectionNotes.get(`${collectKey}.notes_tagline`, null) + + return `${shortName}${tagLine === null ? '' : ` [${tagLine}]`}` + } + + #getMD5Hash(text, prefix = '') { + return `${prefix}${crypto.createHash('md5').update(text).digest('hex')}` + } +} class modFileChecker { #maxFilesType = { grle : 10, png : 128, txt : 2, pdf : 1 } @@ -153,42 +615,41 @@ class modFileChecker { #log = null #logUUID = null - constructor( filePath, isFolder, size, date, log = null, locale = null ) { + constructor( filePath, isFolder, size, date, md5Pre = null, log = null, locale = null ) { this.fileDetail.fullPath = filePath this.fileDetail.isFolder = isFolder this.fileDetail.fileSize = size this.fileDetail.fileDate = date.toISOString() - this.uuid = crypto.createHash('md5').update(filePath).digest('hex') - this.#locale = locale this.#log = log - this.#logUUID = `mod-${this.uuid}` + + this.md5Sum = md5Pre this.fileDetail.shortName = path.parse(this.fileDetail.fullPath).name - this.#log.log.info(`Adding Mod File: ${this.fileDetail.shortName}`, this.#logUUID) - this.#failFlags.folder_needs_zip = this.fileDetail.isFolder + } + async doTests() { + this.uuid = crypto.createHash('md5').update(this.fileDetail.fullPath).digest('hex') + this.#logUUID = `mod-${this.uuid}` + this.#log.log.info(`Adding Mod File: ${this.fileDetail.shortName}`, this.#logUUID) + if ( ! this.#isFileNameBad() ) { if ( ! this.fileDetail.isFolder ) { - const hashString = `${path.basename(this.fileDetail.fullPath)}-${this.fileDetail.fileSize}-${this.fileDetail.fileDate}` - this.md5Sum = crypto.createHash('md5').update(hashString).digest('hex') + this.#testZip().then(() => { + this.#doneTest() + }) - this.#testZip() - - if ( this.#failFlags.no_modDesc ) { this.md5Sum = null } - } else { - this.#testFolder() + this.#testFolder().then(() => { + this.#doneTest() + }) } + } else { + this.#doneTest() } - - this.populateL10n() - this.badgeArray = this.#getBadges() - this.issues = this.#populateIssues() - this.currentLocal = this.#locale() } get debugDump() { @@ -457,8 +918,14 @@ class modFileChecker { return true } + #doneTest() { + this.populateL10n() + this.badgeArray = this.#getBadges() + this.issues = this.#populateIssues() + this.currentLocal = this.#locale() + } - #testZip() { + async #testZip() { let zipFile = null let zipEntries = null @@ -511,11 +978,12 @@ class modFileChecker { this.#log.log.notice(`Caught icon fail: ${e}`, this.#logUUID) } + if ( this.#failFlags.no_modDesc ) { this.md5Sum = null } + zipFile = null } - - #testFolder() { + async #testFolder() { if ( ! fs.existsSync(path.join(this.fileDetail.fullPath, 'modDesc.xml')) ) { this.#failFlags.no_modDesc = true return false @@ -560,7 +1028,6 @@ class modFileChecker { } } - #nestedXMLProperty (propertyPath, passedObj = false) { if (!propertyPath) { return false } @@ -672,16 +1139,15 @@ class modFileChecker { // convert the DXT texture to an Uint8Array containing RGBA data const rgbaData = decodeDXT(imageDataView, imageWidth, imageHeight, ddsData.format) - // make a new PNG image of same width and height, pipe in raw RGBA data - const pngData = new PNG({ width : imageWidth, height : imageHeight }) - - pngData.data = rgbaData + // convert to JPEG + const jpgData = JPEG.encode({ + width : imageWidth, + height : imageHeight, + data : rgbaData, + }, 70) try { - // Dump out PNG, base64 encode it. - const pngBuffer = PNG.sync.write(pngData) - - this.modDesc.iconImageCache = `data:image/png;base64, ${pngBuffer.toString('base64')}` + this.modDesc.iconImageCache = `data:image/jpeg;base64, ${jpgData.data.toString('base64')}` } catch { this.modDesc.iconImageCache = null return false @@ -764,6 +1230,7 @@ class notModFileChecker { } module.exports = { + modFileCollection : modFileCollection, modFileChecker : modFileChecker, notModFileChecker : notModFileChecker, } diff --git a/modAssist_main.js b/modAssist_main.js index f7be1837..109e4f1b 100644 --- a/modAssist_main.js +++ b/modAssist_main.js @@ -31,43 +31,26 @@ log.log.info(` - Electron Version: ${process.versions.electron}`) log.log.info(` - Chrome Version: ${process.versions.chrome}`) - -process.on('uncaughtException', (err, origin) => { +function handleUnhandled(type, err, origin) { const rightNow = new Date() fs.appendFileSync( crashLog, - `Exception Timestamp : ${rightNow.toISOString()}\n\nCaught exception: ${err}\n\nException origin: ${origin}\n\n${err.stack}` + `${type} Timestamp : ${rightNow.toISOString()}\n\nCaught ${type}: ${err}\n\nOrigin: ${origin}\n\n${err.stack}` ) if ( !isNetworkError(err) ) { dialog.showMessageBoxSync(null, { - title : 'Uncaught Error - Quitting', - message : `Caught exception: ${err}\n\nException origin: ${origin}\n\n${err.stack}\n\n\nCan't Continue, exiting now!\n\nTo send file, please see ${crashLog}`, + title : `Uncaught ${type} - Quitting`, + message : `Caught ${type}: ${err}\n\nOrigin: ${origin}\n\n${err.stack}\n\n\nCan't Continue, exiting now!\n\nTo send file, please see ${crashLog}`, type : 'error', }) - gameLogFile.close() + if ( gameLogFile ) { gameLogFile.close() } app.quit() } else { - log.log.debug(`Network error: ${err}`, 'net-error-exception') + log.log.debug(`Network error: ${err}`, `net-error-${type}`) } -}) -process.on('unhandledRejection', (err, origin) => { - const rightNow = new Date() - fs.appendFileSync( - crashLog, - `Rejection Timestamp : ${rightNow.toISOString()}\n\nCaught rejection: ${err}\n\nRejection origin: ${origin}\n\n${err.stack}` - ) - if ( !isNetworkError(err) ) { - dialog.showMessageBoxSync(null, { - title : 'Uncaught Error - Quitting', - message : `Caught rejection: ${err}\n\nRejection origin: ${origin}\n\n${err.stack}\n\n\nCan't Continue, exiting now!\n\nTo send file, please see ${crashLog}`, - type : 'error', - }) - gameLogFile.close() - app.quit() - } else { - log.log.debug(`Network error: ${err}`, 'net-error-rejection') - } -}) +} +process.on('uncaughtException', (err, origin) => { handleUnhandled('exception', err, origin) }) +process.on('unhandledRejection', (err, origin) => { handleUnhandled('rejection', err, origin) }) const translator = require('./lib/translate.js') const myTranslator = new translator.translator(translator.getSystemLocale()) @@ -106,7 +89,7 @@ if ( process.platform === 'win32' && app.isPackaged && gotTheLock && !isPortable dialog.showMessageBox(windows.main, dialogOpts).then((returnValue) => { if (returnValue.response === 0) { if ( tray ) { tray.destroy() } - gameLogFile.close() + if ( gameLogFile ) { gameLogFile.close() } Object.keys(windows).forEach((thisWin) => { if ( thisWin !== 'main' && windows[thisWin] !== null ) { windows[thisWin].destroy() @@ -124,10 +107,7 @@ if ( process.platform === 'win32' && app.isPackaged && gotTheLock && !isPortable }, ( 30 * 60 * 1000)) } -const glob = require('glob') -const fxml = require('fast-xml-parser') -const crypto = require('crypto') - +const fxml = require('fast-xml-parser') const userHome = require('os').homedir() const pathRender = path.join(app.getAppPath(), 'renderer') const pathPreload = path.join(pathRender, 'preload') @@ -151,14 +131,10 @@ const gameGuesses = [ 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\Farming Simulator 22' ] const pathGuesses = [ + path.join(app.getPath('documents'), 'My Games', 'FarmingSimulator2022'), path.join(userHome, 'OneDrive', 'Documents', 'My Games', 'FarmingSimulator2022'), path.join(userHome, 'Documents', 'My Games', 'FarmingSimulator2022') ] -try { - const winUtil = require('windows') - const userFolder = winUtil.registry('HKEY_CURRENT_USER/SOFTWARE/Microsoft/Windows/CurrentVersion/Explorer/User Shell Folders').Personal.value - pathGuesses.unshift(path.join(userFolder, 'My Games', 'FarmingSimulator2022')) -} catch { /* do nothing */ } gameGuesses.forEach((testPath) => { if ( fs.existsSync(path.join(testPath, gameExeName)) ) { @@ -173,7 +149,7 @@ pathGuesses.forEach((testPath) => { } }) -const { modFileChecker, notModFileChecker } = require('./lib/single-mod-checker.js') +const { modFileCollection } = require('./lib/modCheckLib.js') const winDef = (w, h) => { return { x : { type : 'number', default : -1 }, @@ -234,32 +210,24 @@ const mcStore = new Store({schema : settingsSchema, migrations : settingsMig, cl const maCache = new Store({name : 'mod_cache', clearInvalidConfig : true}) const modNote = new Store({name : 'col_notes', clearInvalidConfig : true}) -const newModsList = [] - -let modFolders = new Set() -let modFoldersMap = {} -let modList = {} -let bindConflict = {} -let countTotal = 0 -let countMods = 0 -let modHubList = {'mods' : {}, 'last' : []} -let modHubVersion = {} -let lastFolderLoc = null -let lastGameSettings = {} - +const modCollect = new modFileCollection( + log, + modNote, + maCache, + app.getPath('home'), + { + hide : loadingWindow_hide, + count : loadingWindow_current, + }, + myTranslator.deferCurrentLocale, + skipCache +) -const ignoreList = [ - '^npm-debug\\.log$', - '^\\..*\\.swp$', - '^Thumbs\\.db$', - '^thumbs\\.db$', - '^ehthumbs\\.db$', - '^Desktop\\.ini$', - '^desktop\\.ini$', - '@eaDir$', -] +const loadWindowCount = { total : 0, current : 0} -const junkRegex = new RegExp(ignoreList.join('|')) +let modFolders = new Set() +let lastFolderLoc = null +let lastGameSettings = {} let tray = null const windows = { @@ -332,14 +300,14 @@ mcStore.set('cache_version', app.getVersion()) function debugDangerCallback() { - if ( windows.main !== null ) { - windows.main.webContents.send('fromMain_debugLogDanger') - } + if ( windows.main !== null ) { windows.main.webContents.send('fromMain_debugLogDanger') } } + /* _ _ ____ _ _ ____ _____ _ _ ___ ( \/\/ )(_ _)( \( )( _ \ ( _ )( \/\/ )/ __) ) ( _)(_ ) ( )(_) ) )(_)( ) ( \__ \ (__/\__)(____)(_)\_)(____/ (_____)(__/\__)(___/ */ + function destroyAndFocus(winName) { windows[winName] = null if ( windows.main !== null ) { windows.main.focus() } @@ -408,7 +376,7 @@ function createSubWindow(winName, {noSelect = true, show = true, parent = null, event.preventDefault() } if ( input.alt && input.control && input.code === 'KeyD' ) { - createDebugWindow() + createNamedWindow('debug') event.preventDefault() } }) @@ -524,7 +492,7 @@ function createMainWindow () { event.preventDefault() } if ( input.alt && input.control && input.code === 'KeyD' ) { - createDebugWindow() + createNamedWindow('debug') event.preventDefault() } }) @@ -535,245 +503,151 @@ function createMainWindow () { }) } -function createConfirmFav(mods, destinations) { - if ( mods.length < 1 ) { return } - if ( windows.confirm ) { windows.confirm.focus(); return } - - windows.confirm = createSubWindow('confirm', { parent : 'main', preload : 'confirmMulti', fixed : true }) - - windows.confirm.webContents.on('did-finish-load', async (event) => { - event.sender.send('fromMain_confirmList', mods, destinations, modList) - }) - - windows.confirm.loadFile(path.join(pathRender, 'confirm-multi.html')) - - windows.confirm.on('closed', () => { destroyAndFocus('confirm') }) -} +function createNamedWindow(winName, windowArgs) { + const subWinDef = subWindows[winName] + const thisWindow = subWinDef.winName -function createConfirmWindow(type, modRecords, origList) { - if ( modRecords.length < 1 ) { return } - if ( windows.confirm ) { windows.confirm.focus(); return } - - const file_HTML = `confirm-file${type.charAt(0).toUpperCase()}${type.slice(1)}.html` - const file_JS = `confirm${type.charAt(0).toUpperCase()}${type.slice(1)}` - const collection = origList[0].split('--')[0] - - windows.confirm = createSubWindow('confirm', { parent : 'main', preload : file_JS, fixed : true }) - - windows.confirm.webContents.on('did-finish-load', async (event) => { - event.sender.send('fromMain_confirmList', { - records : modRecords, - list : modList, - foldersMap : modFoldersMap, - collection : collection, - }) - }) - - windows.confirm.loadFile(path.join(pathRender, file_HTML)) - - windows.confirm.on('closed', () => { destroyAndFocus('confirm') }) -} - -function createChangeLogWindow() { - if ( windows.change ) { - windows.change.focus() + if ( windows[thisWindow] ) { + windows[thisWindow].focus() + if ( subWinDef.refocusCallback ) { subWinDef.callback(windowArgs) } return } - windows.change = createSubWindow('change', { parent : 'main', fixed : true, preload : 'aChangelogWindow' }) + windows[thisWindow] = createSubWindow(subWinDef.winName, subWinDef.subWindowArgs) - windows.change.loadFile(path.join(pathRender, 'a_changelog.html')) - windows.change.on('closed', () => { destroyAndFocus('change') }) -} + windows[thisWindow].webContents.on('did-finish-load', async () => { + subWinDef.callback(windowArgs) -function createFolderWindow() { - if ( windows.folder ) { - windows.folder.focus() - windows.folder.webContents.send('fromMain_getFolders', modList) - return - } - - windows.folder = createSubWindow('folder', { parent : 'main', preload : 'folderWindow' }) - - windows.folder.webContents.on('did-finish-load', async (event) => { - event.sender.send('fromMain_getFolders', modList) - }) - - windows.folder.loadFile(path.join(pathRender, 'folders.html')) - windows.folder.on('closed', () => { destroyAndFocus('folder'); processModFolders() }) -} - -function createDetailWindow(thisModRecord) { - if ( thisModRecord === null ) { return } - const modhubRecord = modRecordToModHub(thisModRecord) - - if ( windows.detail ) { - windows.detail.focus() - windows.detail.webContents.send('fromMain_modRecord', thisModRecord, modhubRecord, bindConflict, myTranslator.currentLocale) - return - } - - windows.detail = createSubWindow('detail', { parent : 'main', preload : 'detailWindow' }) - - windows.detail.webContents.on('did-finish-load', async (event) => { - event.sender.send('fromMain_modRecord', thisModRecord, modhubRecord, bindConflict, myTranslator.currentLocale) - if ( devDebug ) { windows.detail.webContents.openDevTools() } - }) - - windows.detail.loadFile(path.join(pathRender, 'detail.html')) - windows.detail.on('closed', () => { destroyAndFocus('detail') }) - - windows.detail.webContents.setWindowOpenHandler(({ url }) => { - shell.openExternal(url) - return { action : 'deny' } - }) -} - -function createFindWindow() { - if ( windows.find ) { - windows.find.focus() - windows.find.webContents.send('fromMain_modRecords', modList) - return - } - - windows.find = createSubWindow('find', { preload : 'findWindow' }) - - windows.find.webContents.on('did-finish-load', async (event) => { - event.sender.send('fromMain_modRecords', modList) - if ( devDebug ) { windows.find.webContents.openDevTools() } - }) - - windows.find.loadFile(path.join(pathRender, 'find.html')) - windows.find.on('closed', () => { destroyAndFocus('find') }) -} - -function createDebugWindow() { - if ( windows.debug ) { - windows.debug.focus() - windows.debug.webContents.send('fromMain_debugLog', log.htmlLog) - return - } - - windows.debug = createSubWindow('debug', { preload : 'debugWindow' }) - - windows.debug.webContents.on('did-finish-load', (event) => { - event.sender.send('fromMain_debugLog', log.htmlLog) - }) - - windows.debug.loadFile(path.join(app.getAppPath(), 'renderer', 'debug.html')) - windows.debug.on('closed', () => { destroyAndFocus('debug') }) -} -function createGameLogWindow() { - if ( windows.gamelog ) { - windows.gamelog.focus() - readGameLog() - return - } - - windows.gamelog = createSubWindow('gamelog', { preload : 'gamelogWindow' }) - - windows.gamelog.webContents.on('did-finish-load', () => { - readGameLog() - if ( devDebug ) { windows.gamelog.webContents.openDevTools() } - }) - - windows.gamelog.loadFile(path.join(app.getAppPath(), 'renderer', 'gamelog.html')) - windows.gamelog.on('closed', () => { destroyAndFocus('gamelog') }) -} - -function createPrefsWindow() { - if ( windows.prefs ) { - windows.prefs.focus() - windows.prefs.webContents.send( 'fromMain_allSettings', mcStore.store, devControls ) - return - } - - windows.prefs = createSubWindow('prefs', { parent : 'main', preload : 'prefsWindow', title : myTranslator.syncStringLookup('user_pref_title_main') }) - - windows.prefs.webContents.on('did-finish-load', (event) => { - event.sender.send( 'fromMain_allSettings', mcStore.store, devControls ) - }) - - windows.prefs.loadFile(path.join(pathRender, 'prefs.html')) - windows.prefs.on('closed', () => { destroyAndFocus('prefs') }) - - windows.prefs.webContents.setWindowOpenHandler(({ url }) => { - shell.openExternal(url) - return { action : 'deny' } + if ( devDebug && subWindowDev.has(subWinDef.winName) ) { + windows[thisWindow].webContents.openDevTools() + } }) -} - -function createSavegameWindow(collection) { - if ( windows.save ) { - windows.save.focus() - windows.save.webContents.send('fromMain_collectionName', collection, modList) - return - } - windows.save = createSubWindow('save', { preload : 'savegameWindow' }) + windows[thisWindow].loadFile(path.join(pathRender, subWinDef.HTMLFile)) - windows.save.webContents.on('did-finish-load', async (event) => { - event.sender.send('fromMain_collectionName', collection, modList) - if ( devDebug ) { windows.save.webContents.openDevTools() } + windows[thisWindow].on('closed', () => { + destroyAndFocus(subWinDef.winName) + if ( typeof subWinDef.extraCloseFunc === 'function' ) { + subWinDef.extraCloseFunc() + } + }) - windows.save.loadFile(path.join(pathRender, 'savegame.html')) - windows.save.on('closed', () => { destroyAndFocus('save') }) -} - -function createNotesWindow(collection) { - if ( windows.notes ) { - windows.notes.focus() - windows.notes.webContents.send('fromMain_collectionName', collection, modList[collection].name, modNote.store, lastGameSettings) - return + if ( subWinDef.handleURLinWin ) { + windows[thisWindow].webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url) + return { action : 'deny' } + }) } - - windows.notes = createSubWindow('notes', { parent : 'main', preload : 'notesWindow' }) - - windows.notes.webContents.on('did-finish-load', async (event) => { - event.sender.send('fromMain_collectionName', collection, modList[collection].name, modNote.store, lastGameSettings) - if ( devDebug ) { windows.notes.webContents.openDevTools() } - }) - - windows.notes.loadFile(path.join(pathRender, 'notes.html')) - windows.notes.on('closed', () => { destroyAndFocus('notes'); processModFolders() }) } -function createResolveWindow(modSet, shortName) { - if ( windows.resolve ) { - windows.resolve.webContents.send('fromMain_modSet', modSet, shortName) - windows.resolve.focus() - return - } - - windows.resolve = createSubWindow('resolve', { parent : 'version', preload : 'resolveWindow', fixed : true }) - - windows.resolve.webContents.on('did-finish-load', async (event) => { - event.sender.send('fromMain_modSet', modSet, shortName) - if ( devDebug ) { windows.resolve.webContents.openDevTools() } - }) - - windows.resolve.loadFile(path.join(pathRender, 'resolve.html')) - windows.resolve.on('closed', () => { destroyAndFocus('resolve') }) +const subWindowDev = new Set(['save', 'find', 'detail', 'notes', 'version', 'resolve']) +const subWindows = { + confirmFav : { + winName : 'confirm', + HTMLFile : 'confirm-multi.html', + subWindowArgs : { parent : 'main', preload : 'confirmMulti', fixed : true }, + callback : (windowArgs) => { sendModList(windowArgs, 'fromMain_confirmList', 'confirm', false) }, + }, + confirmCopy : { + winName : 'confirm', + HTMLFile : 'confirm-fileCopy.html', + subWindowArgs : { parent : 'main', preload : 'confirmCopy', fixed : true }, + callback : (windowArgs) => { sendModList(windowArgs, 'fromMain_confirmList', 'confirm', false) }, + }, + confirmMove : { + winName : 'confirm', + HTMLFile : 'confirm-fileMove.html', + subWindowArgs : { parent : 'main', preload : 'confirmMove', fixed : true }, + callback : (windowArgs) => { sendModList(windowArgs, 'fromMain_confirmList', 'confirm', false) }, + }, + confirmDelete : { + winName : 'confirm', + HTMLFile : 'confirm-fileDelete.html', + subWindowArgs : { parent : 'main', preload : 'confirmDelete', fixed : true }, + callback : (windowArgs) => { sendModList(windowArgs, 'fromMain_confirmList', 'confirm', false) }, + }, + change : { + winName : 'change', + HTMLFile : 'a_changelog.html', + subWindowArgs : { parent : 'main', fixed : true, preload : 'aChangelogWindow' }, + callback : () => { return }, + }, + folder : { + winName : 'folder', + HTMLFile : 'folders.html', + subWindowArgs : { parent : 'main', preload : 'folderWindow' }, + callback : () => { sendModList({}, 'fromMain_getFolders', 'folder', false ) }, + refocusCallback : true, + extraCloseFunc : () => { processModFolders() }, + }, + debug : { + winName : 'debug', + HTMLFile : 'debug.html', + subWindowArgs : { preload : 'debugWindow' }, + callback : () => { windows.debug.webContents.send('fromMain_debugLog', log.htmlLog) }, + refocusCallback : true, + }, + gamelog : { + winName : 'gamelog', + HTMLFile : 'gamelog.html', + subWindowArgs : { preload : 'gamelogWindow' }, + callback : () => { readGameLog() }, + refocusCallback : true, + }, + prefs : { + winName : 'prefs', + HTMLFile : 'prefs.html', + subWindowArgs : { parent : 'main', preload : 'prefsWindow' }, + callback : () => { windows.prefs.webContents.send( 'fromMain_allSettings', mcStore.store, devControls ) }, + refocusCallback : true, + handleURLinWin : true, + }, + detail : { + winName : 'detail', + HTMLFile : 'detail.html', + subWindowArgs : { parent : 'main', preload : 'detailWindow' }, + callback : (windowArgs) => { sendModList(windowArgs, 'fromMain_modRecord', 'detail', false) }, + refocusCallback : true, + handleURLinWin : true, + }, + find : { + winName : 'find', + HTMLFile : 'find.html', + subWindowArgs : { preload : 'findWindow' }, + callback : () => { sendModList({}, 'fromMain_modRecords', 'find', false ) }, + refocusCallback : true, + }, + notes : { + winName : 'notes', + HTMLFile : 'notes.html', + subWindowArgs : { parent : 'main', preload : 'notesWindow' }, + callback : (windowArgs) => { sendModList(windowArgs, 'fromMain_collectionName', 'notes', false ) }, + refocusCallback : true, + }, + version : { + winName : 'version', + HTMLFile : 'versions.html', + subWindowArgs : { parent : 'main', preload : 'versionWindow' }, + callback : () => { sendModList({}, 'fromMain_modList', 'version', false ) }, + refocusCallback : true, + }, + resolve : { + winName : 'resolve', + HTMLFile : 'resolve.html', + subWindowArgs : { parent : 'version', preload : 'resolveWindow', fixed : true }, + callback : (windowArgs) => { windows.resolve.webContents.send('fromMain_modSet', windowArgs.modSet, windowArgs.shortName) }, + refocusCallback : true, + }, + save : { + winName : 'save', + HTMLFile : 'savegame.html', + subWindowArgs : { preload : 'savegameWindow' }, + callback : (windowArgs) => { sendModList(windowArgs, 'fromMain_collectionName', 'save', false ) }, + refocusCallback : true, + }, } -function createVersionWindow() { - if ( windows.version ) { - windows.version.webContents.send('fromMain_modList', modList) - windows.version.focus() - return - } - - windows.version = createSubWindow('version', { parent : 'main', preload : 'versionWindow' }) - - windows.version.webContents.on('did-finish-load', async (event) => { - event.sender.send('fromMain_modList', modList) - if ( devDebug ) { windows.version.webContents.openDevTools() } - }) - - windows.version.loadFile(path.join(pathRender, 'versions.html')) - windows.version.on('closed', () => { destroyAndFocus('version') }) -} function loadingWindow_open(l10n) { const newCenter = getRealCenter('load') @@ -796,19 +670,18 @@ function loadingWindow_open(l10n) { return } } -function loadingWindow_total(amount, reset = false, inMB = false) { - countTotal = ( reset ) ? amount : amount + countTotal +function loadingWindow_doCount(whichCount, amount, reset, inMB) { + loadWindowCount[whichCount] = ( reset ) ? amount : amount + loadWindowCount[whichCount] if ( ! windows.load.isDestroyed() ) { - windows.load.webContents.send('fromMain_loadingTotal', countTotal, inMB) + windows.load.webContents.send(`fromMain_loading_${whichCount}`, loadWindowCount[whichCount], inMB) } } +function loadingWindow_total(amount, reset = false, inMB = false) { + loadingWindow_doCount('total', amount, reset, inMB) +} function loadingWindow_current(amount = 1, reset = false, inMB = false) { - countMods = ( reset ) ? amount : amount + countMods - - if ( ! windows.load.isDestroyed() ) { - windows.load.webContents.send('fromMain_loadingCurrent', countMods, inMB) - } + loadingWindow_doCount('current', amount, reset, inMB) } function loadingWindow_hide(time = 1250) { setTimeout(() => { @@ -823,16 +696,7 @@ function loadingWindow_noCount() { } } -function isNetworkError(errorObject) { - return errorObject.message.startsWith('net::ERR_')// || - // errorObject.message === 'net::ERR_INTERNET_DISCONNECTED' || - // errorObject.message === 'net::ERR_PROXY_CONNECTION_FAILED' || - // errorObject.message === 'net::ERR_CONNECTION_RESET' || - // errorObject.message === 'net::ERR_CONNECTION_CLOSE' || - // errorObject.message === 'net::ERR_NAME_NOT_RESOLVED' || - // errorObject.message === 'net::ERR_CONNECTION_TIMED_OUT' || - // errorObject.message === 'net::ERR_SSL_PROTOCOL_ERROR' -} +function isNetworkError(errorObject) { return errorObject.message.startsWith('net::ERR_') } /* ____ ____ ___ (_ _)( _ \ / __) @@ -845,23 +709,25 @@ ipcMain.on('toMain_populateClipboard', (event, text) => { clipboard.writeText(te ipcMain.on('toMain_makeInactive', () => { parseSettings({ disable : true }) }) ipcMain.on('toMain_makeActive', (event, newList) => { parseSettings({ - newFolder : modFoldersMap[newList], + newFolder : modCollect.mapCollectionToFolder(newList), userName : modNote.get(`${newList}.notes_username`, null), password : modNote.get(`${newList}.notes_password`, null), serverName : modNote.get(`${newList}.notes_server`, null), }) }) + ipcMain.on('toMain_openMods', (event, mods) => { - const thisCollectionFolder = modFoldersMap[mods[0].split('--')[0]] - const thisMod = modIdToRecord(mods[0]) + const thisCollectionFolder = modCollect.mapCollectionToFolder(mods[0].split('--')[0]) + const thisMod = modCollect.modColUUIDToRecord(mods[0]) if ( thisMod !== null ) { shell.showItemInFolder(path.join(thisCollectionFolder, path.basename(thisMod.fileDetail.fullPath))) } }) + ipcMain.on('toMain_openHub', (event, mods) => { - const thisMod = modIdToRecord(mods[0]) - const thisModId = modHubList.mods[thisMod.fileDetail.shortName] || null + const thisMod = modCollect.modColUUIDToRecord(mods[0]) + const thisModId = thisMod.modHub.id if ( thisModId !== null ) { shell.openExternal(`https://www.farming-simulator.com/mod.php?mod_id=${thisModId}`) @@ -869,38 +735,64 @@ ipcMain.on('toMain_openHub', (event, mods) => { }) ipcMain.on('toMain_copyFavorites', () => { - const favCols = [] - const sourceFiles = [] - let destCols = Object.keys(modList) + const sourceCollections = [] + const destinationCollections = [] + const sourceFiles = [] + + modCollect.collections.forEach((collectKey) => { + const isFavorite = modNote.get(`${collectKey}.notes_favorite`, false) - Object.keys(modNote.store).forEach((collection) => { - if ( modNote.get(`${collection}.notes_favorite`, false) === true ) { favCols.push(collection) } + if ( isFavorite ) { + sourceCollections.push(collectKey) + } else { + destinationCollections.push(collectKey) + } }) - favCols.forEach((collection) => { - destCols = destCols.filter((item) => item !== collection ) - modList[collection].mods.forEach((thisMod) => { - sourceFiles.push([ - thisMod.fileDetail.fullPath, - collection, - thisMod.fileDetail.shortName, - thisMod.l10n.title - ]) + sourceCollections.forEach((collectKey) => { + const thisCollection = modCollect.getModCollection(collectKey) + thisCollection.modSet.forEach((modKey) => { + sourceFiles.push({ + fullPath : thisCollection.mods[modKey].fileDetail.fullPath, + collectKey : collectKey, + shortName : thisCollection.mods[modKey].fileDetail.shortName, + title : thisCollection.mods[modKey].l10n.title, + }) }) }) - createConfirmFav(sourceFiles, destCols) + if ( sourceFiles.length > 0 ) { + //createConfirmFav({ + createNamedWindow( + 'confirmFav', + { + sourceFiles : sourceFiles, + destinations : destinationCollections, + sources : sourceCollections, + } + ) + } }) -ipcMain.on('toMain_deleteMods', (event, mods) => { createConfirmWindow('delete', modIdsToRecords(mods), mods) }) -ipcMain.on('toMain_moveMods', (event, mods) => { createConfirmWindow('move', modIdsToRecords(mods), mods) }) -ipcMain.on('toMain_copyMods', (event, mods) => { createConfirmWindow('copy', modIdsToRecords(mods), mods) }) + +function handleCopyMoveDelete(windowName, modIDS, modRecords = null) { + if ( modIDS.length > 0 ) { + createNamedWindow(windowName, { + records : ( modRecords === null ) ? modCollect.modColUUIDsToRecords(modIDS) : modRecords, + originCollectKey : modIDS[0].split('--')[0], + }) + } +} + +ipcMain.on('toMain_deleteMods', (event, mods) => { handleCopyMoveDelete('confirmDelete', mods) }) +ipcMain.on('toMain_moveMods', (event, mods) => { handleCopyMoveDelete('confirmMove', mods) }) +ipcMain.on('toMain_copyMods', (event, mods) => { handleCopyMoveDelete('confirmCopy', mods) }) ipcMain.on('toMain_realFileDelete', (event, fileMap) => { fileOperation('delete', fileMap) }) ipcMain.on('toMain_realFileMove', (event, fileMap) => { fileOperation('move', fileMap) }) ipcMain.on('toMain_realFileCopy', (event, fileMap) => { fileOperation('copy', fileMap) }) ipcMain.on('toMain_realFileVerCP', (event, fileMap) => { fileOperation('copy', fileMap, 'resolve') setTimeout(() => { - windows.version.webContents.send('fromMain_modList', modList) + sendModList({}, 'fromMain_modList', 'version', false ) }, 1500) }) /** END: File operation buttons */ @@ -934,93 +826,77 @@ ipcMain.on('toMain_addFolder', () => { log.log.danger(`Could not read specified add folder : ${unknownError}`, 'folder-opts') }) }) -ipcMain.on('toMain_editFolders', () => { createFolderWindow() }) +ipcMain.on('toMain_editFolders', () => { createNamedWindow('folder') }) ipcMain.on('toMain_openFolder', (event, folder) => { shell.openPath(folder) }) ipcMain.on('toMain_refreshFolders', () => { foldersDirty = true; processModFolders() }) ipcMain.on('toMain_removeFolder', (event, folder) => { if ( modFolders.delete(folder) ) { log.log.notice(`Folder removed from list ${folder}`, 'folder-opts') mcStore.set('modFolders', Array.from(modFolders)) - Object.keys(modList).forEach((collection) => { - if ( modList[collection].fullPath === folder ) { delete modList[collection] } - }) - Object.keys(modFoldersMap).forEach((collection) => { - if ( modFoldersMap[collection] === folder ) { delete modFoldersMap[collection]} - }) - windows.folder.webContents.send('fromMain_getFolders', modList) - foldersDirty = true + const collectKey = modCollect.mapFolderToCollection(folder) + + modCollect.removeCollection(collectKey) + + sendModList({}, 'fromMain_getFolders', 'folder', false ) + + foldersDirty = true } else { log.log.warning(`Folder NOT removed from list ${folder}`, 'folder-opts') } }) - ipcMain.on('toMain_reorderFolder', (event, from, to) => { - const newOrder = Array.from(modFolders) - const item = newOrder.splice(from, 1)[0] - newOrder.splice(to, 0, item) - - const reorder_modList = {} - const reorder_modFoldersMap = {} + const newOrder = Array.from(modFolders) + const newSetOrder = new Set() + const item = newOrder.splice(from, 1)[0] + newOrder.splice(to, 0, item) newOrder.forEach((path) => { - Object.keys(modFoldersMap).forEach((collection) => { - if ( modFoldersMap[collection] === path ) { - reorder_modFoldersMap[collection] = modFoldersMap[collection] - } - }) - Object.keys(modList).forEach((collection) => { - if ( modList[collection].fullPath === path ) { - reorder_modList[collection] = modList[collection] - } - }) + newSetOrder.add(modCollect.mapFolderToCollection(path)) }) - modFolders = new Set(newOrder) - modList = reorder_modList - modFoldersMap = reorder_modFoldersMap + modFolders = new Set(newOrder) + modCollect.newCollectionOrder = newSetOrder mcStore.set('modFolders', Array.from(modFolders)) - windows.folder.webContents.send('fromMain_getFolders', modList) + sendModList({}, 'fromMain_getFolders', 'folder', false ) foldersDirty = true }) -/** END: Folder Window Operation */ - - ipcMain.on('toMain_reorderFolderAlpha', () => { const newOrder = [] const collator = new Intl.Collator() - Object.keys(modList).forEach((collection) => { - newOrder.push({name : modList[collection].name, collection : collection}) + modCollect.collections.forEach((collectKey) => { + newOrder.push({ + path : modCollect.mapCollectionToFolder(collectKey), + name : modCollect.mapCollectionToName(collectKey), + collectKey : collectKey, + }) }) newOrder.sort((a, b) => collator.compare(a.name, b.name) || - collator.compare(a.collection, b.collection) + collator.compare(a.collectKey, b.collectKey) ) const newModFolders = new Set() - const newModList = {} - const newModFoldersMap = {} + const newModSetOrder = new Set() - newOrder.forEach((order) => { - newModFolders.add(modFoldersMap[order.collection]) - newModList[order.collection] = modList[order.collection] - newModFoldersMap[order.collection] = modFoldersMap[order.collection] + newOrder.forEach((orderPart) => { + newModFolders.add(orderPart.path) + newModSetOrder.add(orderPart.collectKey) }) - modFolders = newModFolders - modList = newModList - modFoldersMap = newModFoldersMap + modFolders = newModFolders + modCollect.newCollectionOrder = newModSetOrder mcStore.set('modFolders', Array.from(modFolders)) - windows.folder.webContents.send('fromMain_getFolders', modList) + sendModList({}, 'fromMain_getFolders', 'folder', false ) foldersDirty = true }) - +/** END: Folder Window Operation */ /** Logging Operation */ ipcMain.on('toMain_log', (event, level, process, text) => { log.log[level](text, process) }) @@ -1043,11 +919,9 @@ ipcMain.on('toMain_langList_send', (event) => { event.sender.send('fromMain_langList_return', langList, myTranslator.deferCurrentLocale()) }) }) - ipcMain.on('toMain_getText_sync', (event, text) => { event.returnValue = myTranslator.syncStringLookup(text) }) - ipcMain.on('toMain_getText_send', (event, l10nSet) => { l10nSet.forEach((l10nEntry) => { if ( l10nEntry === 'app_version' ) { @@ -1080,24 +954,28 @@ ipcMain.on('toMain_getText_send', (event, l10nSet) => { /** Detail window operation */ -ipcMain.on('toMain_openModDetail', (event, thisMod) => { createDetailWindow(modIdToRecord(thisMod)) }) -ipcMain.on('toMain_showChangelog', () => { createChangeLogWindow() } ) +ipcMain.on('toMain_openModDetail', (event, thisMod) => { createNamedWindow('detail', {selected : modCollect.modColUUIDToRecord(thisMod) }) }) /** END: Detail window operation */ +/** Changelog window operation */ +ipcMain.on('toMain_showChangelog', () => { createNamedWindow('change') } ) +/** END: Changelog window operation */ + +/** Main window context menus */ ipcMain.on('toMain_modContextMenu', async (event, modID) => { - const thisMod = modIdToRecord(modID) - const thisModId = modHubList.mods[thisMod.fileDetail.shortName] || null + const thisMod = modCollect.modColUUIDToRecord(modID) const template = [ { label : thisMod.fileDetail.shortName}, { type : 'separator' }, { label : myTranslator.syncStringLookup('context_mod_detail'), click : () => { - createDetailWindow(thisMod) + createNamedWindow('detail', {selected : thisMod}) + //createDetailWindow({selected : thisMod}) }}, { type : 'separator' }, { label : myTranslator.syncStringLookup('open_folder'), click : () => { - const thisCollectionFolder = modFoldersMap[modID.split('--')[0]] + const thisCollectionFolder = modCollect.mapCollectionToFolder(modID.split('--')[0]) if ( thisMod !== null ) { shell.showItemInFolder(path.join(thisCollectionFolder, path.basename(thisMod.fileDetail.fullPath))) @@ -1105,44 +983,42 @@ ipcMain.on('toMain_modContextMenu', async (event, modID) => { }} ] - if ( thisModId !== null ) { + if ( thisMod.modHub.id !== null ) { template.push({ label : myTranslator.syncStringLookup('open_hub'), click : () => { - shell.openExternal(`https://www.farming-simulator.com/mod.php?mod_id=${thisModId}`) + shell.openExternal(`https://www.farming-simulator.com/mod.php?mod_id=${thisMod.modHub.id}`) }}) } template.push({ type : 'separator' }) template.push({ label : myTranslator.syncStringLookup('copy_to_list'), click : () => { - createConfirmWindow('copy', [thisMod], [modID]) + handleCopyMoveDelete('confirmCopy', [modID], [thisMod]) }}) template.push({ label : myTranslator.syncStringLookup('move_to_list'), click : () => { - createConfirmWindow('move', [thisMod], [modID]) + handleCopyMoveDelete('confirmMove', [modID], [thisMod]) }}) template.push({ label : myTranslator.syncStringLookup('remove_from_list'), click : () => { - createConfirmWindow('delete', [thisMod], [modID]) + handleCopyMoveDelete('confirmDelete', [modID], [thisMod]) }}) const menu = Menu.buildFromTemplate(template) menu.popup(BrowserWindow.fromWebContents(event.sender)) }) - ipcMain.on('toMain_mainContextMenu', async (event, collection) => { - const tagLine = modNote.get(`${collection}.notes_tagline`, null) - const subLabel = `${modList[collection].name}${tagLine === null ? '' : ` :: ${tagLine}`}` + const subLabel = modCollect.mapCollectionToFullName(collection) const template = [ { label : myTranslator.syncStringLookup('context_main_title').padEnd(subLabel.length, ' '), sublabel : subLabel }, - { type : 'separator' }, + { type : 'separator' }, { label : myTranslator.syncStringLookup('list-active'), click : () => { parseSettings({ - newFolder : modFoldersMap[collection], + newFolder : modCollect.mapCollectionToFolder(collection), userName : modNote.get(`${collection}.notes_username`, null), password : modNote.get(`${collection}.notes_password`, null), serverName : modNote.get(`${collection}.notes_server`, null), }) }}, - { type : 'separator' }, + { type : 'separator' }, { label : myTranslator.syncStringLookup('open_folder'), click : () => { - shell.openPath(modFoldersMap[collection]) + shell.openPath(modCollect.mapCollectionToFolder(collection)) }} ] @@ -1168,12 +1044,13 @@ ipcMain.on('toMain_mainContextMenu', async (event, collection) => { const menu = Menu.buildFromTemplate(template) menu.popup(BrowserWindow.fromWebContents(event.sender)) }) +/** END: Main window context menus */ -/** Debug window operation */ -ipcMain.on('toMain_openGameLog', () => { createGameLogWindow() }) +/** Game log window operation */ +ipcMain.on('toMain_openGameLog', () => { createNamedWindow('gamelog') }) ipcMain.on('toMain_openGameLogFolder', () => { shell.showItemInFolder(path.join(path.dirname(gameSettings), 'log.txt')) }) -ipcMain.on('toMain_getGameLog', () => { readGameLog() }) +ipcMain.on('toMain_getGameLog', () => { readGameLog() }) function readGameLog() { if ( windows.gamelog === null ) { return } @@ -1185,10 +1062,10 @@ function readGameLog() { log.log.warning(`Could not read game log file: ${e}`, 'game-log') } } -/** END: Debug window operation */ +/** END: Game log window operation */ /** Debug window operation */ -ipcMain.on('toMain_openDebugLog', () => { createDebugWindow() }) +ipcMain.on('toMain_openDebugLog', () => { createNamedWindow('debug') }) ipcMain.on('toMain_openDebugFolder', () => { shell.showItemInFolder(log.pathToLog) }) ipcMain.on('toMain_getDebugLog', (event) => { event.sender.send('fromMain_debugLog', log.htmlLog) }) /** END: Debug window operation */ @@ -1213,13 +1090,11 @@ function gameLauncher() { log.log.warning('Game path not set or invalid!', 'game-launcher') } } - ipcMain.on('toMain_startFarmSim', () => { gameLauncher() }) /** END: game launcher */ /** Find window operation */ -ipcMain.on('toMain_openFind', () => { createFindWindow() }) - +ipcMain.on('toMain_openFind', () => { createNamedWindow('find') }) ipcMain.on('toMain_findContextMenu', async (event, thisMod) => { const template = [ { label : myTranslator.syncStringLookup('select_in_main'), sublabel : thisMod.name }, @@ -1241,12 +1116,7 @@ ipcMain.on('toMain_findContextMenu', async (event, thisMod) => { /** END : Find window operation*/ /** Preferences window operation */ -ipcMain.on('toMain_getCollDesc', (event, collection) => { - const tagLine = modNote.get(`${collection}.notes_tagline`, null) - - event.returnValue = ( tagLine !== null ) ? ` [${tagLine}]` : '' -}) -ipcMain.on('toMain_openPrefs', () => { createPrefsWindow() }) +ipcMain.on('toMain_openPrefs', () => { createNamedWindow('prefs') }) ipcMain.on('toMain_getPref', (event, name) => { event.returnValue = mcStore.get(name) }) ipcMain.on('toMain_setPref', (event, name, value) => { if ( name === 'dev_mode' ) { @@ -1272,6 +1142,11 @@ ipcMain.on('toMain_resetWindows', () => { windows.main.center() windows.prefs.center() }) +ipcMain.on('toMain_clearCacheFile', () => { + maCache.clear() + foldersDirty = true + processModFolders() +}) ipcMain.on('toMain_cleanCacheFile', (event) => { const localStore = maCache.store const md5Set = new Set() @@ -1280,15 +1155,25 @@ ipcMain.on('toMain_cleanCacheFile', (event) => { Object.keys(localStore).forEach((md5) => { md5Set.add(md5) }) - Object.keys(modList).forEach((collection) => { - modList[collection].mods.forEach((mod) => { md5Set.delete(mod.md5Sum) }) + modCollect.collections.forEach((collectKey) => { + Object.values(modCollect.getModCollection(collectKey).mods).forEach((mod) => { + md5Set.delete(mod.md5Sum) + }) }) + loadingWindow_total(md5Set.size, true) + loadingWindow_current(0, true) + setTimeout(() => { loadingWindow_total(md5Set.size, true) loadingWindow_current(0, true) - md5Set.forEach((md5) => { maCache.delete(md5); loadingWindow_current() }) + md5Set.forEach((md5) => { + delete localStore[md5] + loadingWindow_current() + }) + + maCache.store = localStore loadingWindow_hide(1500) event.sender.send('fromMain_l10n_refresh') @@ -1337,8 +1222,13 @@ ipcMain.on('toMain_setGamePath', (event) => { /** Notes Operation */ -ipcMain.on('toMain_openNotes', (event, collection) => { createNotesWindow(collection) }) -ipcMain.on('toMain_setNote', (event, id, value, collection) => { +ipcMain.on('toMain_openNotes', (event, collectKey) => { + createNamedWindow('notes', { + collectKey : collectKey, + lastGameSettings : lastGameSettings, + }) +}) +ipcMain.on('toMain_setNote', (event, id, value, collectKey) => { const dirtyActions = [ 'notes_website', 'notes_websiteDL', @@ -1349,14 +1239,16 @@ ipcMain.on('toMain_setNote', (event, id, value, collection) => { if ( dirtyActions.includes(id) ) { foldersDirty = true } if ( value === '' ) { - modNote.delete(`${collection}.${id}`) + modNote.delete(`${collectKey}.${id}`) } else { - modNote.set(`${collection}.${id}`, value) + modNote.set(`${collectKey}.${id}`, value) } - createNotesWindow(collection) + createNamedWindow('notes', { + collectKey : collectKey, + lastGameSettings : lastGameSettings, + }) }) - /** END: Notes Operation */ /** Download operation */ @@ -1367,28 +1259,18 @@ ipcMain.on('toMain_downloadList', (event, collection) => { if ( thisSite === null || !thisDoDL ) { return } - dialog.showMessageBoxSync(windows.main, { - title : myTranslator.syncStringLookup('download_title'), - message : `${myTranslator.syncStringLookup('download_started')} :: ${modList[collection].name}\n${myTranslator.syncStringLookup('download_finished')}`, - type : 'info', - }) + doDialogBox('main', { titleL10n : 'download_title', message : `${myTranslator.syncStringLookup('download_started')} :: ${modCollect.mapCollectionToName(collection)}\n${myTranslator.syncStringLookup('download_finished')}` }) log.log.info(`Downloading Collection : ${collection}`, 'mod-download') log.log.info(`Download Link : ${thisLink}`, 'mod-download') - - const dlReq = net.request(thisLink) dlReq.on('response', (response) => { log.log.info(`Got download: ${response.statusCode}`, 'mod-download') if ( response.statusCode < 200 || response.statusCode >= 400 ) { - dialog.showMessageBoxSync(windows.main, { - title : myTranslator.syncStringLookup('download_title'), - message : `${myTranslator.syncStringLookup('download_failed')} :: ${modList[collection].name}`, - type : 'error', - }) + doDialogBox('main', { type : 'error', titleL10n : 'download_title', message : `${myTranslator.syncStringLookup('download_failed')} :: ${modCollect.mapCollectionToName(collection)}` }) } else { loadingWindow_open('download') @@ -1431,7 +1313,7 @@ ipcMain.on('toMain_downloadList', (event, collection) => { processModFolders() }) - zipReadStream.pipe(unzip.Extract({ path : modList[collection].fullPath })) + zipReadStream.pipe(unzip.Extract({ path : modCollect.mapCollectionToFolder(collection) })) } catch (e) { log.log.warning(`Download failed : (${response.statusCode}) ${e}`, 'mod-download') loadingWindow_hide() @@ -1442,53 +1324,53 @@ ipcMain.on('toMain_downloadList', (event, collection) => { dlReq.on('error', (error) => { log.log.warning(`Network error : ${error}`, 'mod-download'); loadingWindow_hide() }) dlReq.end() }) - /** END: download operation */ /** Export operation */ +const csvRow = (entries) => entries.map((entry) => `"${entry.replaceAll('"', '""')}"`).join(',') + ipcMain.on('toMain_exportList', (event, collection) => { const csvTable = [] - csvTable.push('"Mod","Title","Version","Author","ModHub","Link"') - modList[collection].mods.forEach((mod) => { - const modHubID = modHubList.mods[mod.fileDetail.shortName] || null + csvTable.push(csvRow(['Mod', 'Title', 'Version', 'Author', 'ModHub', 'Link'])) + + modCollect.getModListFromCollection(collection).forEach((mod) => { + const modHubID = mod.modHub.id const modHubLink = ( modHubID !== null ) ? `https://www.farming-simulator.com/mod.php?mod_id=${modHubID}` : '' const modHubYesNo = ( modHubID !== null ) ? 'yes' : 'no' - csvTable.push(`"${mod.fileDetail.shortName}.zip","${mod.l10n.title.replaceAll('"', '\'')}","${mod.modDesc.version}","${mod.modDesc.author.replaceAll('"', '\'')}","${modHubYesNo}","${modHubLink}"`) + csvTable.push(csvRow([ + `${mod.fileDetail.shortName}.zip`, + mod.l10n.title, + mod.modDesc.version, + mod.modDesc.author, + modHubYesNo, + modHubLink + ])) }) dialog.showSaveDialog(windows.main, { - defaultPath : path.join(app.getPath('desktop'), `${modList[collection].name}.csv`), - filters : [ - { name : 'CSV', extensions : ['csv'] }, - ], + defaultPath : path.join(app.getPath('desktop'), `${modCollect.mapCollectionToName(collection)}.csv`), + filters : [{ name : 'CSV', extensions : ['csv'] }], }).then(async (result) => { if ( result.canceled ) { log.log.debug('Save CSV Cancelled', 'csv-export') } else { try { fs.writeFileSync(result.filePath, csvTable.join('\n')) - dialog.showMessageBoxSync(windows.main, { - message : myTranslator.syncStringLookup('save_csv_worked'), - type : 'info', - }) + doDialogBox('main', { messageL10n : 'save_csv_worked' }) } catch (err) { log.log.warning(`Could not save csv file : ${err}`, 'csv-export') - dialog.showMessageBoxSync(windows.main, { - message : myTranslator.syncStringLookup('save_csv_failed'), - type : 'warning', - }) + doDialogBox('main', { type : 'warning', messageL10n : 'save_csv_failed' }) } } }).catch((unknownError) => { log.log.warning(`Could not save csv file : ${unknownError}`, 'csv-export') }) }) - ipcMain.on('toMain_exportZip', (event, selectedMods) => { const filePaths = [] - modIdsToRecords(selectedMods).forEach((mod) => { + modCollect.modColUUIDsToRecords(selectedMods).forEach((mod) => { filePaths.push([mod.fileDetail.shortName, mod.fileDetail.fullPath]) }) @@ -1518,10 +1400,7 @@ ipcMain.on('toMain_exportZip', (event, selectedMods) => { loadingWindow_hide() log.log.warning(`Could not create zip file : ${err}`, 'zip-export') setTimeout(() => { - dialog.showMessageBoxSync(windows.main, { - message : myTranslator.syncStringLookup('save_zip_failed'), - type : 'warning', - }) + doDialogBox('main', { type : 'warning', messageL10n : 'save_zip_failed' }) }, 1500) }) @@ -1544,10 +1423,7 @@ ipcMain.on('toMain_exportZip', (event, selectedMods) => { log.log.warning(`Could not create zip file : ${err}`, 'zip-export') loadingWindow_hide() setTimeout(() => { - dialog.showMessageBoxSync(windows.main, { - message : myTranslator.syncStringLookup('save_zip_failed'), - type : 'warning', - }) + doDialogBox('main', { type : 'warning', messageL10n : 'save_zip_failed' }) }, 1500) } } @@ -1558,7 +1434,7 @@ ipcMain.on('toMain_exportZip', (event, selectedMods) => { /** END: Export operation */ /** Savegame window operation */ -ipcMain.on('toMain_openSave', (event, collection) => { createSavegameWindow(collection) }) +ipcMain.on('toMain_openSave', (event, collection) => { createNamedWindow('save', { collectKey : collection }) }) ipcMain.on('toMain_selectInMain', (event, selectList) => { windows.main.focus() windows.main.webContents.send('fromMain_selectOnly', selectList) @@ -1579,7 +1455,8 @@ function openSaveGame(zipMode = false) { if ( !result.canceled ) { try { const thisSavegame = new saveFileChecker(result.filePaths[0], !zipMode, log) - windows.save.webContents.send('fromMain_saveInfo', modList, thisSavegame, modHubList) + + sendModList({ thisSaveGame : thisSavegame }, 'fromMain_saveInfo', 'save', false ) } catch (e) { log.log.danger(`Load failed: ${e}`, 'savegame') } @@ -1592,77 +1469,67 @@ function openSaveGame(zipMode = false) { /** Version window operation */ -ipcMain.on('toMain_versionCheck', () => { createVersionWindow() }) -ipcMain.on('toMain_refreshVersions', (event) => { event.sender.send('fromMain_modList', modList) } ) +ipcMain.on('toMain_versionCheck', () => { createNamedWindow('version') }) +ipcMain.on('toMain_refreshVersions', () => { sendModList({}, 'fromMain_modList', 'version', false ) } ) ipcMain.on('toMain_versionResolve', (event, shortName) => { const modSet = [] - Object.keys(modList).forEach((collection) => { - modList[collection].mods.forEach((mod) => { + + modCollect.collections.forEach((collectKey) => { + modCollect.getModCollection(collectKey).modSet.forEach((modKey) => { + const mod = modCollect.modColAndUUID(collectKey, modKey) + if ( mod.fileDetail.shortName === shortName && !mod.fileDetail.isFolder ) { - modSet.push([collection, mod.modDesc.version, mod, modList[collection].name]) + modSet.push({ + collectKey : collectKey, + version : mod.modDesc.version, + modRecord : mod, + collectName : modCollect.mapCollectionToName(collectKey), + }) } }) }) - createResolveWindow(modSet, shortName) + createNamedWindow('resolve', { + modSet : modSet, + shortName : shortName, + }) }) /** END: Version window operation */ /** Utility & Convenience Functions */ ipcMain.on('toMain_closeSubWindow', (event, thisWin) => { windows[thisWin].close() }) -ipcMain.on('toMain_homeDirRevamp', (event, thisPath) => { event.returnValue = thisPath.replaceAll(userHome, '~') }) -function refreshClientModList() { - windows.main.webContents.send( - 'fromMain_modList', +function sendModList(extraArgs = {}, eventName = 'fromMain_modList', toWindow = 'main', closeLoader = true) { + modCollect.toRenderer(extraArgs).then((modCollection) => { + windows[toWindow].webContents.send(eventName, modCollection) + + if ( toWindow === 'main' && windows.version && windows.version.isVisible() ) { + windows.version.webContents.send(eventName, modCollection) + } + if ( closeLoader ) { loadingWindow_hide(1500) } + }) +} + +function refreshClientModList(closeLoader = true) { + sendModList( { currentLocale : myTranslator.deferCurrentLocale(), - modList : modList, l10n : { disable : myTranslator.syncStringLookup('override_disabled'), unknown : myTranslator.syncStringLookup('override_unknown'), }, activeCollection : overrideIndex, - foldersMap : modFoldersMap, - newMods : newModsList, - modHub : { - list : modHubList, - version : modHubVersion, - }, - bindConflict : bindConflict, - notes : modNote.store, - } + }, + 'fromMain_modList', + 'main', + closeLoader ) } -function modRecordToModHub(mod) { - const modId = modHubList.mods[mod.fileDetail.shortName] || null - return [modId, (modHubVersion[modId] || null), modHubList.last.includes(modId)] -} -function modIdToRecord(id) { - const idParts = id.split('--') - let foundMod = null - let foundCol = null - - modList[idParts[0]].mods.forEach((mod) => { - if ( foundMod === null && mod.uuid === idParts[1] ) { - foundMod = mod - foundCol = idParts[0] - } - }) - foundMod.currentCollection = foundCol - return foundMod -} - -function modIdsToRecords(mods) { - const theseMods = [] - mods.forEach((inMod) => { theseMods.push(modIdToRecord(inMod)) }) - return theseMods -} /** END: Utility & Convenience Functions */ - +/** Business Functions */ function parseGameXML(devMode = null) { const gameXMLFile = gameSettings.replace('gameSettings.xml', 'game.xml') @@ -1708,7 +1575,6 @@ function parseGameXML(devMode = null) { parseGameXML(null) } } -/** Business Functions */ function parseSettings({disable = null, newFolder = null, userName = null, serverName = null, password = null } = {}) { if ( ! gameSettings.endsWith('.xml') ) { log.log.danger(`Game settings is not an xml file ${gameSettings}, fixing`, 'game-settings') @@ -1759,10 +1625,7 @@ function parseSettings({disable = null, newFolder = null, userName = null, serve if ( overrideActive === 'false' || overrideActive === false ) { overrideIndex = '0' } else { - overrideIndex = '999' - Object.keys(modFoldersMap).forEach((cleanName) => { - if ( modFoldersMap[cleanName] === overrideFolder ) { overrideIndex = cleanName } - }) + overrideIndex = modCollect.mapFolderToCollection(overrideFolder) || '999' } if ( disable !== null || newFolder !== null || userName !== null || password !== null || serverName !== null ) { @@ -1813,7 +1676,6 @@ function parseSettings({disable = null, newFolder = null, userName = null, serve parseSettings() refreshClientModList() - loadingWindow_hide(1500) } } @@ -1832,16 +1694,14 @@ function fileOperation(type, fileMap, srcWindow = 'confirm') { } }, 250) } - - function fileOperation_post(type, fileMap) { const fullPathMap = [] fileMap.forEach((file) => { const thisFileName = path.basename(file[2]) fullPathMap.push([ - path.join(modFoldersMap[file[1]], thisFileName), // source - path.join(modFoldersMap[file[0]], thisFileName), // dest + path.join(modCollect.mapCollectionToFolder(file[1]), thisFileName), // source + path.join(modCollect.mapCollectionToFolder(file[0]), thisFileName), // dest ]) }) @@ -1879,67 +1739,10 @@ function fileOperation_post(type, fileMap) { }) processModFolders() - if ( windows.version && windows.version.isVisible() ) { - windows.version.webContents.send('fromMain_modList', modList) - } - -} - -function fileGetStats(folder, thisFile) { - let isFolder = null - let date = null - let b_date = null - let size = null - let error = false - - try { - if ( thisFile.isSymbolicLink() ) { - const thisSymLink = fs.readlinkSync(path.join(folder, thisFile.name)) - const thisSymLinkStat = fs.lstatSync(path.join(folder, thisSymLink)) - isFolder = thisSymLinkStat.isDirectory() - date = thisSymLinkStat.ctime - b_date = thisSymLinkStat.birthtime - - if ( !isFolder ) { size = thisSymLinkStat.size } - } else { - isFolder = thisFile.isDirectory() - } - - if ( ! thisFile.isSymbolicLink() ) { - const theseStats = fs.statSync(path.join(folder, thisFile.name)) - if ( !isFolder ) { size = theseStats.size } - date = theseStats.ctime - b_date = theseStats.birthtime - - } - if ( isFolder ) { - let bytes = 0 - glob.sync('**', { cwd : path.join(folder, thisFile.name) }).forEach((file) => { - try { - const stats = fs.statSync(path.join(folder, thisFile.name, file)) - if ( stats.isFile() ) { bytes += stats.size } - } catch { /* Do Nothing if we can't read it. */ } - }) - size = bytes - } - } catch (e) { - log.log.warning(`Unable to stat file ${thisFile.name} in ${folder} : ${e}`, 'file-stat') - isFolder = false - size = 0 - date = new Date(1969, 1, 1, 0, 0, 0, 0) - error = true - } - return { - folder : isFolder, - size : size, - date : date, - b_date : b_date, - error : error, - } } let loadingWait = null -async function processModFolders(newFolder) { +async function processModFolders() { if ( !foldersDirty ) { loadingWindow_hide(); return } loadingWindow_open('mods', 'main') @@ -1949,206 +1752,53 @@ async function processModFolders(newFolder) { loadingWait = setInterval(() => { if ( windows.load.isVisible() ) { clearInterval(loadingWait) - const localStore = maCache.store - const processPromises = processModFolders_post(newFolder, localStore) - - Promise.all(processPromises).then(() => { - maCache.store = localStore - foldersDirty = false - - processModBindConflict() - processModFolders_post_after() - }) + processModFoldersOnDisk() } }, 250) } - -function processModFolders_post(newFolder = false, localStore) { - const useOneDrive = mcStore.get('use_one_drive', false) - const readingPromises = [] - if ( newFolder === false ) { modList = {}; modFoldersMap = {}} +function processModFoldersOnDisk() { + modCollect.syncSafe = mcStore.get('use_one_drive', false) + modCollect.clearAll() // Cleaner for no-longer existing folders, count contents of others modFolders.forEach((folder) => { - if ( ! fs.existsSync(folder) ) { - modFolders.delete(folder) - } else { - try { - const folderSize = fs.readdirSync(folder, {withFileTypes : true}) - loadingWindow_total(folderSize.length) - } catch (e) { - log.log.danger(`Couldn't count folder: ${folder} :: ${e}`, 'folder-reader') - } - } + if ( ! fs.existsSync(folder) ) { modFolders.delete(folder) } }) mcStore.set('modFolders', Array.from(modFolders)) modFolders.forEach((folder) => { - const cleanName = `col_${crypto.createHash('md5').update(folder).digest('hex')}` - const shortName = path.basename(folder) - - if ( folder === newFolder || newFolder === false ) { - modFoldersMap[cleanName] = folder - modList[cleanName] = { name : shortName, fullPath : folder, mods : [] } - - try { - const folderContents = fs.readdirSync(folder, {withFileTypes : true}) - - folderContents.forEach((thisFile) => { - if ( junkRegex.test(thisFile.name) ) { - loadingWindow_current() - return - } - - const thisFileStats = fileGetStats(folder, thisFile) + const thisCollectionStats = modCollect.addCollection(folder) - if ( thisFileStats.error ) { - loadingWindow_current() - return - } - - readingPromises.push( - new Promise((resolve) => { - setTimeout(() => { - processModFileSingleton(folder, thisFile, localStore, cleanName, thisFileStats, useOneDrive) - resolve(true) - }, 10) - }) - ) - }) - - } catch (e) { - log.log.danger(`Couldn't process folder: ${folder} :: ${e}`, 'folder-reader') - } - } + loadingWindow_total(thisCollectionStats.fileCount) }) - return readingPromises -} - -async function processModFileSingleton (folder, thisFile, localStore, cleanName, thisFileStats, useOneDrive) { - if ( !thisFileStats.folder && !skipCache ) { - const hashString = `${thisFile.name}-${thisFileStats.size}-${(useOneDrive)?thisFileStats.b_date.toISOString():thisFileStats.date.toISOString()}` - const thisMD5Sum = crypto.createHash('md5').update(hashString).digest('hex') - - if ( typeof localStore[thisMD5Sum] !== 'undefined') { - modList[cleanName].mods.push(localStore[thisMD5Sum]) - log.log.debug(`Adding mod FROM cache: ${localStore[thisMD5Sum].fileDetail.shortName}`, `mod-${localStore[thisMD5Sum].uuid}`) - loadingWindow_current() - return - } - } + modCollect.processMods() - if ( !thisFileStats.folder && !thisFile.name.endsWith('.zip') ) { - modList[cleanName].mods.push(new notModFileChecker( - path.join(folder, thisFile.name), - false, - thisFileStats.size, - thisFileStats.date, - log - )) - loadingWindow_current() - return - } - - try { - const thisModDetail = new modFileChecker( - path.join(folder, thisFile.name), - thisFileStats.folder, - thisFileStats.size, - (useOneDrive) ? thisFileStats.b_date : thisFileStats.date, - log, - myTranslator.deferCurrentLocale - ) - const storable = thisModDetail.storable - modList[cleanName].mods.push(thisModDetail) + modCollect.processPromise.then(() => { + parseSettings() + parseGameXML() + refreshClientModList() - if ( thisModDetail.md5Sum !== null ) { - log.log.info('Adding mod to cache', `mod-${thisModDetail.uuid}`) - newModsList.push(thisModDetail.md5Sum) - localStore[thisModDetail.md5Sum] = storable + if ( mcStore.get('rel_notes') !== app.getVersion() ) { + mcStore.set('rel_notes', app.getVersion() ) + log.log.info('New version detected, show changelog') + createNamedWindow('change') } - } catch (e) { - log.log.danger(`Couldn't process file: ${thisFile.name} :: ${e}`, 'folder-reader') - modList[cleanName].mods.push(new notModFileChecker( - path.join(folder, thisFile.name), - false, - thisFileStats.size, - thisFileStats.date, - log - )) - } - - loadingWindow_current() - -} - -function processModBindConflict() { - bindConflict = {} - - Object.keys(modList).forEach((collection) => { - bindConflict[collection] = {} - const collectionBinds = {} - - modList[collection].mods.forEach((thisMod) => { - Object.keys(thisMod.modDesc.binds).forEach((actName) => { - thisMod.modDesc.binds[actName].forEach((keyCombo) => { - if ( keyCombo === '' ) { return } - - const safeCat = thisMod.modDesc.actions[actName] || 'UNKNOWN' - const thisCombo = `${safeCat}--${keyCombo}` - - collectionBinds[thisCombo] ??= [] - collectionBinds[thisCombo].push(thisMod.fileDetail.shortName) - }) - }) - }) - Object.keys(collectionBinds).forEach((keyCombo) => { - if ( collectionBinds[keyCombo].length > 1 ) { - collectionBinds[keyCombo].forEach((modName) => { - bindConflict[collection][modName] ??= {} - bindConflict[collection][modName][keyCombo] = collectionBinds[keyCombo].filter((w) => w !== modName) - if ( bindConflict[collection][modName][keyCombo].length === 0 ) { - delete bindConflict[collection][modName][keyCombo] - } - }) - } - }) - Object.keys(bindConflict).forEach((collection) => { - Object.keys(bindConflict[collection]).forEach((modName) => { - if ( Object.keys(bindConflict[collection][modName]).length === 0 ) { - delete bindConflict[collection][modName] - } - }) - }) }) } -function processModFolders_post_after() { - parseSettings() - parseGameXML() - refreshClientModList() - loadingWindow_hide() - - if ( mcStore.get('rel_notes') !== app.getVersion() ) { - mcStore.set('rel_notes', app.getVersion() ) - log.log.info('New version detected, show changelog') - createChangeLogWindow() - } -} - function loadSaveFile(filename) { try { - const rawData = fs.readFileSync(path.join(app.getPath('userData'), filename)) + const rawData = fs.readFileSync(path.join(app.getPath('userData'), filename)) const jsonData = JSON.parse(rawData) switch (filename) { case 'modHubData.json' : - modHubList = jsonData + modCollect.modHubList = jsonData break case 'modHubVersion.json' : - modHubVersion = jsonData + modCollect.modHubVersion = jsonData break default : break @@ -2182,6 +1832,18 @@ function dlSaveFile(url, filename) { } /** END: Business Functions */ +function doDialogBox(attachTo, {type = 'info', message = null, messageL10n = null, title = null, titleL10n = null }) { + const attachWin = ( attachTo === null ) ? null : windows[attachTo] + + const thisTitle = ( title !== null ) ? title : myTranslator.syncStringLookup(( titleL10n === null ) ? 'app_name' : titleL10n) + const thisMessage = ( message !== null ) ? message : myTranslator.syncStringLookup(messageL10n) + + dialog.showMessageBoxSync(attachWin, { + title : thisTitle, + message : thisMessage, + type : type, + }) +} app.whenReady().then(() => { diff --git a/package.json b/package.json index f5bb0e8f..153a55a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fsg-mod-assistant", - "version": "1.10.2", + "version": "1.99.0", "description": "FSG Farm Sim Mod Assistant", "main": "modAssist_main.js", "homepage": "https://github.com/FSGModding/FSG_Mod_Assistant#readme", @@ -19,8 +19,8 @@ "pack": "electron-builder --dir", "dist": "electron-builder", "test": "node ./test/mod-reader-test.js && node ./test/save-reader-test.js && node ./test/translate-check.js", - "langtest": "node ./test/translate-check.js", - "langfix": "node ./test/translate-fix.js", + "lang_test": "node ./test/translate-check.js", + "lang_fix": "node ./test/translate-fix.js", "depends": "node ./test/outdated-deps.js" }, "devDependencies": { @@ -37,10 +37,9 @@ "electron-updater": "^5.2.1", "fast-xml-parser": "^4.0.11", "glob": "^8.0.3", - "pngjs": "^6.0.0", + "jpeg-js": "^0.4.4", "semver": "^7.3.8", "unzip-stream": "^0.3.1", - "windows": "^0.1.2", "xml2js": "^0.4.23", "yargs": "^17.5.1" }, diff --git a/renderer/a_changelog.html b/renderer/a_changelog.html index 9f67aa34..a6c3afd4 100644 --- a/renderer/a_changelog.html +++ b/renderer/a_changelog.html @@ -42,66 +42,81 @@
${printPath}
`) if ( selectedDest === '0' ) { confirmHTML.push(`${printPath}`) confirmHTML.push('