added initial files
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
// This class is used to create a queue of operations when working with instances configs, to avoid data corruption.
|
||||
// Each instance will have its own queue. This can also be used in more generic situations where a queue structure is needed
|
||||
class PromiseQueue {
|
||||
constructor() {
|
||||
this.queue = [];
|
||||
this.isPending = false;
|
||||
this.listeners = {};
|
||||
}
|
||||
|
||||
add(promise) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.queue.push({
|
||||
promise,
|
||||
resolve,
|
||||
reject
|
||||
});
|
||||
this.execute();
|
||||
});
|
||||
}
|
||||
|
||||
on(eventName, handler) {
|
||||
switch (eventName) {
|
||||
case 'executed':
|
||||
this.listeners.executed = () => handler(this.queue.length + 1);
|
||||
break;
|
||||
case 'start':
|
||||
this.listeners.start = () => handler(this.queue.length + 1);
|
||||
break;
|
||||
case 'end':
|
||||
this.listeners.end = () => handler();
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const startHandler = this.listeners.start;
|
||||
if (this.isPending) return false;
|
||||
if (startHandler) {
|
||||
setTimeout(startHandler, 0);
|
||||
}
|
||||
while (this.queue[0]) {
|
||||
const item = this.queue.shift();
|
||||
this.isPending = true;
|
||||
try {
|
||||
// eslint-disable-next-line
|
||||
const value = await item.promise();
|
||||
const executedHandler = this.listeners.executed;
|
||||
if (executedHandler) {
|
||||
setTimeout(executedHandler, 0);
|
||||
}
|
||||
item.resolve(value);
|
||||
} catch (e) {
|
||||
item.reject(e);
|
||||
}
|
||||
this.isPending = false;
|
||||
}
|
||||
const endHandler = this.listeners.end;
|
||||
if (endHandler) {
|
||||
setTimeout(endHandler, 0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default PromiseQueue;
|
||||
@@ -0,0 +1,13 @@
|
||||
import fs from 'fs';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const computeFileHash = (filePath, algorithm = 'sha1') =>
|
||||
new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash(algorithm);
|
||||
fs.createReadStream(filePath)
|
||||
.on('data', data => hash.update(data))
|
||||
.on('end', () => resolve(hash.digest('hex')))
|
||||
.on('error', reject);
|
||||
});
|
||||
|
||||
export default computeFileHash;
|
||||
@@ -0,0 +1,29 @@
|
||||
import path from 'path';
|
||||
import { platform, homedir } from 'os';
|
||||
|
||||
export const WINDOWS = 'win32';
|
||||
export const LINUX = 'linux';
|
||||
export const DARWIN = 'darwin';
|
||||
export const DESKTOP_PATH = path.join(homedir(), 'Desktop');
|
||||
export const CLASSPATH_DIVIDER_CHAR = platform() === WINDOWS ? ';' : ':';
|
||||
export const DEFAULT_JAVA_ARGS = `${
|
||||
platform() === WINDOWS
|
||||
? '-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump'
|
||||
: ''
|
||||
} -Xms256m`;
|
||||
export const REQUIRED_JAVA_ARGS =
|
||||
'-Dfml.ignorePatchDiscrepancies=true -Dfml.ignoreInvalidMinecraftCertificates=true';
|
||||
export const DEFAULT_MEMORY = 4096;
|
||||
|
||||
export const resolutionPresets = [
|
||||
'854x480',
|
||||
'800x600',
|
||||
'1024x768',
|
||||
'1280x1024',
|
||||
'1366x768',
|
||||
'1600x900',
|
||||
'1920x1080',
|
||||
'2560x1440',
|
||||
'3440x1440',
|
||||
'3440x1500'
|
||||
];
|
||||
@@ -0,0 +1,161 @@
|
||||
import makeDir from 'make-dir';
|
||||
import fss from 'fs';
|
||||
import axios from 'axios';
|
||||
import pMap from 'p-map';
|
||||
import path from 'path';
|
||||
import adapter from 'axios/lib/adapters/http';
|
||||
import computeFileHash from './computeFileHash';
|
||||
|
||||
const fs = fss.promises;
|
||||
|
||||
function getUri(url) {
|
||||
return new URL(url).href;
|
||||
}
|
||||
|
||||
export const downloadInstanceFiles = async (
|
||||
arr,
|
||||
updatePercentage,
|
||||
threads = 4
|
||||
) => {
|
||||
let downloaded = 0;
|
||||
await pMap(
|
||||
arr,
|
||||
async item => {
|
||||
let counter = 0;
|
||||
let res = false;
|
||||
if (!item.path || !item.url) {
|
||||
console.warn('Skipping', item);
|
||||
return;
|
||||
}
|
||||
do {
|
||||
counter += 1;
|
||||
if (counter !== 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
|
||||
try {
|
||||
res = await downloadFileInstance(
|
||||
item.path,
|
||||
item.url,
|
||||
item.sha1,
|
||||
item.legacyPath
|
||||
);
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
} while (!res && counter < 3);
|
||||
downloaded += 1;
|
||||
if (
|
||||
updatePercentage &&
|
||||
(downloaded % 5 === 0 || downloaded === arr.length)
|
||||
) {
|
||||
updatePercentage(downloaded);
|
||||
}
|
||||
},
|
||||
{ concurrency: threads }
|
||||
);
|
||||
};
|
||||
|
||||
const downloadFileInstance = async (fileName, url, sha1, legacyPath) => {
|
||||
let encodedUrl;
|
||||
try {
|
||||
const filePath = path.dirname(fileName);
|
||||
try {
|
||||
await fs.access(fileName);
|
||||
if (legacyPath) await fs.access(legacyPath);
|
||||
const checksum = await computeFileHash(fileName);
|
||||
const legacyChecksum = legacyPath && (await computeFileHash(legacyPath));
|
||||
if (checksum === sha1 && (!legacyPath || legacyChecksum === sha1)) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
await makeDir(filePath);
|
||||
if (legacyPath) await makeDir(path.dirname(legacyPath));
|
||||
}
|
||||
|
||||
encodedUrl = getUri(url);
|
||||
|
||||
const { data } = await axios.get(encodedUrl, {
|
||||
responseType: 'stream',
|
||||
responseEncoding: null,
|
||||
adapter,
|
||||
timeout: 60000 * 20
|
||||
});
|
||||
|
||||
const wStream = fss.createWriteStream(fileName, {
|
||||
encoding: null
|
||||
});
|
||||
|
||||
data.pipe(wStream);
|
||||
let wStreamLegacy;
|
||||
if (legacyPath) {
|
||||
wStreamLegacy = fss.createWriteStream(legacyPath, {
|
||||
encoding: null
|
||||
});
|
||||
data.pipe(wStreamLegacy);
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
data.on('error', err => {
|
||||
console.error(err);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
data.on('end', () => {
|
||||
wStream.end();
|
||||
wStream.close();
|
||||
if (legacyPath) {
|
||||
wStreamLegacy.end();
|
||||
wStreamLegacy.close();
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Error while downloading <${url} | ${encodedUrl}> to <${fileName}> --> ${e.message}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadFile = async (fileName, url, onProgress) => {
|
||||
await makeDir(path.dirname(fileName));
|
||||
|
||||
const encodedUrl = getUri(url);
|
||||
|
||||
const { data, headers } = await axios.get(encodedUrl, {
|
||||
responseType: 'stream',
|
||||
responseEncoding: null,
|
||||
adapter,
|
||||
timeout: 60000 * 20
|
||||
});
|
||||
|
||||
const out = fss.createWriteStream(fileName, { encoding: null });
|
||||
data.pipe(out);
|
||||
|
||||
// Save variable to know progress
|
||||
let receivedBytes = 0;
|
||||
const totalBytes = parseInt(headers['content-length'], 10);
|
||||
|
||||
data.on('data', chunk => {
|
||||
// Update the received bytes
|
||||
receivedBytes += chunk.length;
|
||||
if (onProgress) {
|
||||
onProgress(parseInt(((receivedBytes * 100) / totalBytes).toFixed(1), 10));
|
||||
}
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
data.on('end', () => {
|
||||
out.end();
|
||||
out.close();
|
||||
resolve();
|
||||
});
|
||||
|
||||
data.on('error', () => {
|
||||
reject();
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
const fmlLibsMapping = {};
|
||||
// 1.3.*
|
||||
const libs13 = [
|
||||
['argo-2.25.jar', 'bb672829fde76cb163004752b86b0484bd0a7f4b', false],
|
||||
['guava-12.0.1.jar', 'b8e78b9af7bf45900e14c6f958486b6ca682195f', false],
|
||||
['asm-all-4.0.jar', '98308890597acb64047f7e896638e0d98753ae82', false]
|
||||
];
|
||||
|
||||
fmlLibsMapping['1.3.2'] = libs13;
|
||||
|
||||
// 1.4.*
|
||||
const libs14 = [
|
||||
['argo-2.25.jar', 'bb672829fde76cb163004752b86b0484bd0a7f4b', false],
|
||||
['guava-12.0.1.jar', 'b8e78b9af7bf45900e14c6f958486b6ca682195f', false],
|
||||
['asm-all-4.0.jar', '98308890597acb64047f7e896638e0d98753ae82', false],
|
||||
['bcprov-jdk15on-147.jar', 'b6f5d9926b0afbde9f4dbe3db88c5247be7794bb', false]
|
||||
];
|
||||
|
||||
fmlLibsMapping['1.4'] = libs14;
|
||||
fmlLibsMapping['1.4.1'] = libs14;
|
||||
fmlLibsMapping['1.4.2'] = libs14;
|
||||
fmlLibsMapping['1.4.3'] = libs14;
|
||||
fmlLibsMapping['1.4.4'] = libs14;
|
||||
fmlLibsMapping['1.4.5'] = libs14;
|
||||
fmlLibsMapping['1.4.6'] = libs14;
|
||||
fmlLibsMapping['1.4.7'] = libs14;
|
||||
|
||||
// 1.5
|
||||
fmlLibsMapping['1.5'] = [
|
||||
['argo-small-3.2.jar', '58912ea2858d168c50781f956fa5b59f0f7c6b51', false],
|
||||
['guava-14.0-rc3.jar', '931ae21fa8014c3ce686aaa621eae565fefb1a6a', false],
|
||||
['asm-all-4.1.jar', '054986e962b88d8660ae4566475658469595ef58', false],
|
||||
['bcprov-jdk15on-148.jar', '960dea7c9181ba0b17e8bab0c06a43f0a5f04e65', true],
|
||||
[
|
||||
'deobfuscation_data_1.5.zip',
|
||||
'5f7c142d53776f16304c0bbe10542014abad6af8',
|
||||
false
|
||||
],
|
||||
['scala-library.jar', '458d046151ad179c85429ed7420ffb1eaf6ddf85', true]
|
||||
];
|
||||
|
||||
// 1.5.1
|
||||
fmlLibsMapping['1.5.1'] = [
|
||||
['argo-small-3.2.jar', '58912ea2858d168c50781f956fa5b59f0f7c6b51', false],
|
||||
['guava-14.0-rc3.jar', '931ae21fa8014c3ce686aaa621eae565fefb1a6a', false],
|
||||
['asm-all-4.1.jar', '054986e962b88d8660ae4566475658469595ef58', false],
|
||||
['bcprov-jdk15on-148.jar', '960dea7c9181ba0b17e8bab0c06a43f0a5f04e65', true],
|
||||
[
|
||||
'deobfuscation_data_1.5.1.zip',
|
||||
'22e221a0d89516c1f721d6cab056a7e37471d0a6',
|
||||
false
|
||||
],
|
||||
['scala-library.jar', '458d046151ad179c85429ed7420ffb1eaf6ddf85', true]
|
||||
];
|
||||
|
||||
// 1.5.2
|
||||
fmlLibsMapping['1.5.2'] = [
|
||||
['argo-small-3.2.jar', '58912ea2858d168c50781f956fa5b59f0f7c6b51', false],
|
||||
['guava-14.0-rc3.jar', '931ae21fa8014c3ce686aaa621eae565fefb1a6a', false],
|
||||
['asm-all-4.1.jar', '054986e962b88d8660ae4566475658469595ef58', false],
|
||||
['bcprov-jdk15on-148.jar', '960dea7c9181ba0b17e8bab0c06a43f0a5f04e65', true],
|
||||
[
|
||||
'deobfuscation_data_1.5.2.zip',
|
||||
'446e55cd986582c70fcf12cb27bc00114c5adfd9',
|
||||
false
|
||||
],
|
||||
['scala-library.jar', '458d046151ad179c85429ed7420ffb1eaf6ddf85', true]
|
||||
];
|
||||
|
||||
export default fmlLibsMapping;
|
||||
@@ -0,0 +1,124 @@
|
||||
import path from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import { promises as fs } from 'fs';
|
||||
import pMap from 'p-map';
|
||||
import { getDirectories } from '.';
|
||||
import { CURSEFORGE } from '../../../common/utils/constants';
|
||||
|
||||
const getInstances = async instancesPath => {
|
||||
const mapFolderToInstance = async instance => {
|
||||
try {
|
||||
const configPath = path.join(
|
||||
path.join(instancesPath, instance, 'config.json')
|
||||
);
|
||||
const rawConfig = await fs.readFile(configPath);
|
||||
|
||||
// Remove temp config if present
|
||||
try {
|
||||
const tempConfigPath = path.join(
|
||||
path.join(instancesPath, instance, 'config_new_temp.json')
|
||||
);
|
||||
await fs.unlink(tempConfigPath);
|
||||
} catch {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
// Restore config in case of crash
|
||||
if (rawConfig.every(v => v === 0)) {
|
||||
const backupConfigPath = path.join(
|
||||
path.join(instancesPath, instance, 'config.bak.json')
|
||||
);
|
||||
const backupConfig = await fs.readFile(backupConfigPath);
|
||||
JSON.parse(backupConfig);
|
||||
await fs.rename(backupConfigPath, configPath);
|
||||
}
|
||||
|
||||
const newRawConfig = await fs.readFile(configPath);
|
||||
const config = JSON.parse(newRawConfig);
|
||||
|
||||
// if the launcher has the modloader as an array, convert it to object
|
||||
if (Array.isArray(config.modloader)) {
|
||||
// source is the source where the modpack comes from example: curseforge
|
||||
// loaderType is the modloader example: forge
|
||||
const [
|
||||
loaderType,
|
||||
mcVersion,
|
||||
loaderVersion,
|
||||
projectID,
|
||||
fileID,
|
||||
source
|
||||
] = config.modloader;
|
||||
|
||||
const patchedConfig = {
|
||||
...config,
|
||||
loader: {
|
||||
loaderType,
|
||||
mcVersion,
|
||||
...(loaderVersion && { loaderVersion }),
|
||||
...(fileID && { fileID }),
|
||||
...(projectID && { projectID }),
|
||||
...(!source && fileID && projectID && { source: CURSEFORGE })
|
||||
}
|
||||
};
|
||||
|
||||
delete patchedConfig.modloader;
|
||||
|
||||
await fse.writeFile(configPath, JSON.stringify(patchedConfig));
|
||||
|
||||
return { ...patchedConfig, name: instance };
|
||||
}
|
||||
|
||||
if (
|
||||
config.loader?.fileId ||
|
||||
config.loader?.addonId ||
|
||||
config.loader?.addonID
|
||||
) {
|
||||
const { fileId, addonId, addonID } = config.loader;
|
||||
|
||||
const patchedConfig = {
|
||||
...config,
|
||||
loader: {
|
||||
...config.loader,
|
||||
...(fileId && { fileID: fileId }),
|
||||
...(addonId && { projectID: addonId }),
|
||||
...(addonID && { projectID: addonID })
|
||||
}
|
||||
};
|
||||
|
||||
delete patchedConfig.loader.fileId;
|
||||
delete patchedConfig.loader.addonId;
|
||||
|
||||
await fse.writeFile(configPath, JSON.stringify(patchedConfig));
|
||||
|
||||
return { ...patchedConfig, name: instance };
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
name: instance
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const folders = await getDirectories(instancesPath);
|
||||
const instances = await pMap(
|
||||
folders.filter(folder => folder !== '.DS_Store'),
|
||||
mapFolderToInstance,
|
||||
{
|
||||
concurrency: 5
|
||||
}
|
||||
);
|
||||
const hashMap = {};
|
||||
// eslint-disable-next-line
|
||||
for (const instance of instances) {
|
||||
// eslint-disable-next-line
|
||||
if (!instance) continue;
|
||||
hashMap[instance.name] = instance;
|
||||
}
|
||||
|
||||
return hashMap;
|
||||
};
|
||||
|
||||
export default getInstances;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
import { push } from 'connected-react-router';
|
||||
|
||||
const middleware = store => next => action => {
|
||||
const currState = store.getState();
|
||||
const result = next(action);
|
||||
const nextState = store.getState();
|
||||
const { dispatch } = store;
|
||||
|
||||
const currentAccountIdChanged =
|
||||
currState.app.currentAccountId !== nextState.app.currentAccountId;
|
||||
|
||||
if (currentAccountIdChanged && !nextState.app.currentAccountId) {
|
||||
dispatch(push('/'));
|
||||
}
|
||||
|
||||
if (currState.settings.potatoPcMode !== nextState.settings.potatoPcMode) {
|
||||
if (nextState.settings.potatoPcMode) {
|
||||
document.getElementById('root').classList.add('disable-animations');
|
||||
} else {
|
||||
document.getElementById('root').classList.remove('disable-animations');
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default middleware;
|
||||
@@ -0,0 +1,62 @@
|
||||
// import watch from "node-watch";
|
||||
import makeDir from 'make-dir';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { notification } from 'antd';
|
||||
import * as ActionTypes from '../../../common/reducers/actionTypes';
|
||||
import getInstances from './getInstances';
|
||||
import modsFingerprintsScan from './modsFingerprintsScan';
|
||||
import { startListener } from '../../../common/reducers/actions';
|
||||
import { _getInstancesPath } from '../../../common/utils/selectors';
|
||||
|
||||
const middleware = store => next => action => {
|
||||
const currState = store.getState();
|
||||
const result = next(action);
|
||||
const nextState = store.getState();
|
||||
const { dispatch } = store;
|
||||
if (!nextState.userData) return result;
|
||||
const instancesPath = _getInstancesPath(nextState);
|
||||
|
||||
const userDataChanged = currState.userData !== nextState.userData;
|
||||
|
||||
// If not initialized yet, start listener and do a first-time read
|
||||
if (!nextState.instances.started || userDataChanged) {
|
||||
const startInstancesListener = async () => {
|
||||
await ipcRenderer.invoke('stop-listener');
|
||||
await makeDir(instancesPath);
|
||||
const instances = await getInstances(instancesPath);
|
||||
dispatch({
|
||||
type: ActionTypes.UPDATE_INSTANCES,
|
||||
instances
|
||||
});
|
||||
const instances1 = await modsFingerprintsScan(instancesPath);
|
||||
dispatch({
|
||||
type: ActionTypes.UPDATE_INSTANCES,
|
||||
instances: instances1
|
||||
});
|
||||
try {
|
||||
await makeDir(instancesPath);
|
||||
await dispatch(startListener());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// eslint-disable-next-line
|
||||
notification.open({
|
||||
key: 'nsfwNotWorking',
|
||||
message: 'NSFW Error',
|
||||
description: 'Node Sentinel File Watcher could not be initialized',
|
||||
top: 47,
|
||||
duration: 10
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.UPDATE_INSTANCES_STARTED,
|
||||
started: true
|
||||
});
|
||||
startInstancesListener();
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default middleware;
|
||||
@@ -0,0 +1,163 @@
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import fse from 'fs-extra';
|
||||
import pMap from 'p-map';
|
||||
import { getDirectories, normalizeModData, isMod } from '.';
|
||||
import { getFileMurmurHash2 } from '../../../common/utils';
|
||||
import { getAddonsByFingerprint, getAddon } from '../../../common/api';
|
||||
|
||||
const modsFingerprintsScan = async instancesPath => {
|
||||
const mapFolderToInstance = async instance => {
|
||||
try {
|
||||
const configPath = path.join(
|
||||
path.join(instancesPath, instance, 'config.json')
|
||||
);
|
||||
const config = await fse.readJSON(configPath);
|
||||
|
||||
if (!config.loader) {
|
||||
throw new Error(`Config for ${instance} could not be parsed`);
|
||||
}
|
||||
|
||||
const modsFolder = path.join(instancesPath, instance, 'mods');
|
||||
const modsFolderExists = await fse.pathExists(modsFolder);
|
||||
|
||||
if (!modsFolderExists) return { ...config, name: instance };
|
||||
|
||||
// Check if config.mods has a different number of mods than the actual number of mods
|
||||
|
||||
// Count the actual mods inside the folder
|
||||
const files = await fs.readdir(modsFolder);
|
||||
|
||||
const fileNamesToRemove = [];
|
||||
const missingMods = {};
|
||||
/* eslint-disable */
|
||||
// Check for new mods in local storage that are not present in config
|
||||
for (const file of files) {
|
||||
try {
|
||||
const completeFilePath = path.join(modsFolder, file);
|
||||
const stat = await fs.lstat(completeFilePath);
|
||||
if (stat.isFile() && isMod(completeFilePath, instancesPath)) {
|
||||
// Check if file is in config
|
||||
if (!(config?.mods || []).find(mod => mod.fileName === file)) {
|
||||
const murmurHash = await getFileMurmurHash2(completeFilePath);
|
||||
console.log(
|
||||
'[MODS SCANNER] Local mod not found in config',
|
||||
file,
|
||||
murmurHash
|
||||
);
|
||||
missingMods[file] = murmurHash;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Check for old mods in config that are not present on local storage
|
||||
for (const configMod of config?.mods || []) {
|
||||
if (!files.includes(configMod.fileName)) {
|
||||
fileNamesToRemove.push(configMod.fileName);
|
||||
console.log(
|
||||
`[MODS SCANNER] Removing ${configMod.fileName} from config`
|
||||
);
|
||||
}
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
let newMods = config?.mods || [];
|
||||
|
||||
if (Object.values(missingMods).length !== 0) {
|
||||
let success = false;
|
||||
let tries = 10;
|
||||
|
||||
while (!success || tries > 10) {
|
||||
try {
|
||||
const data = await getAddonsByFingerprint(
|
||||
Object.values(missingMods)
|
||||
);
|
||||
|
||||
const matches = await Promise.all(
|
||||
Object.entries(missingMods).map(async ([fileName, hash]) => {
|
||||
const exactMatch = (data.exactMatches || []).find(
|
||||
v => v.file.fileFingerprint === hash
|
||||
);
|
||||
if (exactMatch?.file) {
|
||||
let addonData = null;
|
||||
try {
|
||||
addonData = await getAddon(exactMatch.file.modId);
|
||||
return {
|
||||
...normalizeModData(
|
||||
exactMatch.file,
|
||||
exactMatch.file.modId,
|
||||
addonData.name
|
||||
),
|
||||
fileName
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
fileName,
|
||||
displayName: fileName,
|
||||
packageFingerprint: hash
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
fileName,
|
||||
displayName: fileName,
|
||||
packageFingerprint: hash
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
newMods = [...newMods, ...matches];
|
||||
success = true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
tries += 1;
|
||||
await new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filterMods = newMods
|
||||
.filter(_ => _)
|
||||
.filter(v => !fileNamesToRemove.includes(v.fileName));
|
||||
|
||||
const newConfig = {
|
||||
...config,
|
||||
mods: filterMods
|
||||
};
|
||||
|
||||
if (JSON.stringify(config) !== JSON.stringify(newConfig)) {
|
||||
await fse.outputJson(configPath, newConfig);
|
||||
return { ...newConfig, name: instance };
|
||||
}
|
||||
return { ...config, name: instance };
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const folders = await getDirectories(instancesPath);
|
||||
const instances = await pMap(
|
||||
folders.filter(folder => folder !== '.DS_Store'),
|
||||
mapFolderToInstance,
|
||||
{
|
||||
concurrency: 5
|
||||
}
|
||||
);
|
||||
const hashMap = {};
|
||||
// eslint-disable-next-line
|
||||
for (const instance of instances) {
|
||||
// eslint-disable-next-line
|
||||
if (!instance) continue;
|
||||
hashMap[instance.name] = instance;
|
||||
}
|
||||
|
||||
return hashMap;
|
||||
};
|
||||
|
||||
export default modsFingerprintsScan;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { lazy } from 'react';
|
||||
import AsyncComponent from '../../../common/components/AsyncComponent';
|
||||
import withScroll from './withScroll';
|
||||
|
||||
const Login = lazy(() => import('../views/Login'));
|
||||
const Home = lazy(() => import('../views/Home'));
|
||||
const Onboarding = lazy(() => import('../views/Onboarding'));
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: AsyncComponent(Login)
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
component: withScroll(AsyncComponent(Home))
|
||||
},
|
||||
{
|
||||
path: '/onboarding',
|
||||
component: AsyncComponent(Onboarding)
|
||||
}
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import analytics from '../../../common/utils/analytics';
|
||||
|
||||
const INTERVAL_DURATION = 5 * 60 * 1000;
|
||||
|
||||
const useTrackIdle = pathname => {
|
||||
const clientToken = useSelector(state => state.app.clientToken);
|
||||
useEffect(() => {
|
||||
let interval;
|
||||
if (clientToken && process.env.NODE_ENV !== 'development') {
|
||||
interval = setInterval(() => {
|
||||
analytics.idle(pathname);
|
||||
}, INTERVAL_DURATION);
|
||||
}
|
||||
return () => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
};
|
||||
}, [pathname, clientToken]);
|
||||
};
|
||||
|
||||
export default useTrackIdle;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
const withScroll = Component => {
|
||||
return props => {
|
||||
return (
|
||||
<div
|
||||
css={`
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px 0;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
width: 1000px;
|
||||
`}
|
||||
>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<Component {...props} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default withScroll;
|
||||
Reference in New Issue
Block a user