diff --git a/.travis.yml b/.travis.yml index 88017900a..9fb391d81 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,7 +36,9 @@ matrix: # Removing unpacked builds before deploy - sudo rm -rf $TRAVIS_BUILD_DIR/release/win-unpacked - sudo rm -rf $TRAVIS_BUILD_DIR/release/linux-unpacked - - sudo chown -R travis:travis $TRAVIS_BUILD_DIR/release + - ls + - cd release + - ls # - node $TRAVIS_BUILD_DIR/scripts/preDeploy.js deploy: provider: releases diff --git a/antd/lib/style/themes/default.less b/antd/lib/style/themes/default.less index 9b6019667..cd8701413 100644 --- a/antd/lib/style/themes/default.less +++ b/antd/lib/style/themes/default.less @@ -233,7 +233,7 @@ @zindex-modal : 1000; @zindex-notification : 1010; @zindex-message : 10012; -@zindex-popover : 1030; +@zindex-popover : 10000; @zindex-picker : 1050; @zindex-dropdown : 10010; @zindex-tooltip : 10011; @@ -490,7 +490,7 @@ // Skeleton // --- -@skeleton-color: #f2f2f2; +@skeleton-color: @midnight; // Message // --- diff --git a/app/actions/downloadManager.js b/app/actions/downloadManager.js index 800b51ebd..dbd1180ed 100644 --- a/app/actions/downloadManager.js +++ b/app/actions/downloadManager.js @@ -1,18 +1,20 @@ import path from 'path'; -import { remote, ipcRenderer } from 'electron'; import { message } from 'antd'; -import { APPPATH, PACKS_PATH, INSTANCES_PATH, MAVEN_REPO } from '../constants'; import { promisify } from 'util'; import axios from 'axios'; -import request from 'request'; import makeDir from 'make-dir'; import fs from 'fs'; import _ from 'lodash'; -import zip from 'adm-zip'; +import Zip from 'adm-zip'; import { downloadFile, downloadArr } from '../utils/downloader'; -import { extractAssets, extractMainJar, extractNatives, computeLibraries } from '../utils/getMCFilesList'; -//Getting colors from scss theme file -import colors from '../style/theme/index.scss'; +import { PACKS_PATH, INSTANCES_PATH, META_PATH } from '../constants'; +import { + extractAssets, + extractMainJar, + extractNatives, + computeVanillaAndForgeLibraries +} from '../utils/getMCFilesList'; +import { arraify } from '../utils/strings'; export const START_DOWNLOAD = 'START_DOWNLOAD'; export const CLEAR_QUEUE = 'CLEAR_QUEUE'; @@ -45,7 +47,9 @@ export function clearQueue() { // This needs to clear any instance that is already installed return (dispatch, getState) => { const { downloadManager } = getState(); - const completed = Object.keys(downloadManager.downloadQueue).filter(act => downloadManager.downloadQueue[act].status === 'Completed'); + const completed = Object.keys(downloadManager.downloadQueue).filter( + act => downloadManager.downloadQueue[act].status === 'Completed' + ); // It makes no sense to dispatch if no instance is to remove if (completed.length !== 0) { dispatch({ @@ -60,53 +64,126 @@ export function downloadPack(pack) { return async (dispatch, getState) => { const { downloadManager, packCreator } = getState(); const currPack = downloadManager.downloadQueue[pack]; + let vnlJSON = null; + try { + vnlJSON = JSON.parse( + await promisify(fs.readFile)( + path.join( + META_PATH, + 'net.minecraft', + currPack.version, + `${currPack.version}.json` + ) + ) + ); + } catch (err) { + const versionURL = packCreator.versionsManifest.find( + v => v.id === currPack.version + ).url; + vnlJSON = (await axios.get(versionURL)).data; + await makeDir(path.join(META_PATH, 'net.minecraft', currPack.version)); + await promisify(fs.writeFile)( + path.join( + META_PATH, + 'net.minecraft', + currPack.version, + `${currPack.version}.json` + ), + JSON.stringify(vnlJSON) + ); + } - const versionURL = packCreator.versionsManifest.find((v) => v.id === currPack.version).url; - console.log(versionURL); - - const vnlJSON = (await axios.get(versionURL)).data; let forgeJSON = null; - let forgeFileName = null; const assets = await extractAssets(vnlJSON); const mainJar = await extractMainJar(vnlJSON); + let forgeFileName = null; + if (currPack.forgeVersion !== null) { - forgeFileName = `${currPack.version}-${currPack.forgeVersion}`; - const forgeUrl = `https://files.minecraftforge.net/maven/net/minecraftforge/forge/${forgeFileName}/forge-${forgeFileName}-universal.jar`; - - // Checks whether the filename is version-forge or version-forge-version - try { await axios.head(forgeUrl) } - catch (err) { forgeFileName = `${currPack.version}-${currPack.forgeVersion}-${currPack.version}-` } - const forgePath = path.join(INSTANCES_PATH, 'libraries', 'net', 'minecraftforge', 'forge', forgeFileName, `forge-${forgeFileName}.jar`); - try { await promisify(fs.access)(forgePath); } - catch (e) { - await makeDir(path.dirname(forgePath)); - await downloadFile(forgePath, forgeUrl, (p) => { - dispatch({ type: UPDATE_PROGRESS, payload: { pack, percentage: p } }); - }); + const { branch } = packCreator.forgeManifest[ + Object.keys(packCreator.forgeManifest).find(v => v === currPack.version) + ].find(v => Object.keys(v)[0] === currPack.forgeVersion)[ + currPack.forgeVersion + ]; + + forgeFileName = `${currPack.version}-${currPack.forgeVersion}${ + branch !== null ? `-${branch}` : '' + }`; + console.log( + `https://files.minecraftforge.net/maven/net/minecraftforge/forge/${forgeFileName}/forge-${forgeFileName}-installer.jar` + ); + try { + forgeJSON = JSON.parse( + await promisify(fs.readFile)( + path.join( + META_PATH, + 'net.minecraftforge', + forgeFileName, + `${forgeFileName}.json` + ) + ) + ); + await promisify(fs.access)( + path.join( + INSTANCES_PATH, + 'libraries', + ...arraify(forgeJSON.versionInfo.libraries[0].name) + ) + ); + } catch (err) { + await downloadFile( + path.join(INSTANCES_PATH, 'temp', `${forgeFileName}.jar`), + `https://files.minecraftforge.net/maven/net/minecraftforge/forge/${forgeFileName}/forge-${forgeFileName}-installer.jar`, + p => { + dispatch({ + type: UPDATE_PROGRESS, + payload: { pack, percentage: ((p * 18) / 100).toFixed(1) } + }); + } + ); + const zipFile = new Zip( + path.join(INSTANCES_PATH, 'temp', `${forgeFileName}.jar`) + ); + forgeJSON = JSON.parse(zipFile.readAsText('install_profile.json')); + await makeDir( + path.dirname( + path.join( + INSTANCES_PATH, + 'libraries', + ...arraify(forgeJSON.versionInfo.libraries[0].name) + ) + ) + ); + await promisify(fs.unlink)( + path.join(INSTANCES_PATH, 'temp', `${forgeFileName}.jar`) + ); + await makeDir( + path.join(META_PATH, 'net.minecraftforge', forgeFileName) + ); + await promisify(fs.writeFile)( + path.join( + META_PATH, + 'net.minecraftforge', + forgeFileName, + `${forgeFileName}.json` + ), + JSON.stringify(forgeJSON) + ); } - const zipFile = new zip(forgePath); - forgeJSON = JSON.parse(zipFile.readAsText("version.json")); } - console.log(forgeJSON) - console.log(vnlJSON) - const libraries = await computeLibraries(vnlJSON, forgeJSON); + const libraries = await computeVanillaAndForgeLibraries(vnlJSON, forgeJSON); // This is the main config file for the instance - await promisify(fs.writeFile)(path.join(PACKS_PATH, pack, 'config.json'), JSON.stringify({ - instanceName: pack, - version: currPack.version, - forgeID: currPack.forgeVersion !== null ? forgeJSON.id : null, - forgeVersion: currPack.forgeVersion !== null ? forgeFileName : null, - minecraftArguments: currPack.forgeVersion !== null ? forgeJSON.minecraftArguments : - _.has(vnlJSON, 'arguments') ? vnlJSON.arguments.game.filter(lib => typeof lib === 'string' || lib instanceof String).join(' ') : vnlJSON.minecraftArguments, - mainClass: currPack.forgeVersion !== null ? forgeJSON.mainClass : vnlJSON.mainClass, - assets: vnlJSON.assets, - type: currPack.forgeVersion !== null ? "Forge" : vnlJSON.type, - libraries - })); + await promisify(fs.writeFile)( + path.join(PACKS_PATH, pack, 'config.json'), + JSON.stringify({ + instanceName: pack, + version: currPack.version, + forgeVersion: forgeFileName + }) + ); dispatch({ type: UPDATE_TOTAL_FILES_TO_DOWNLOAD, @@ -116,11 +193,27 @@ export function downloadPack(pack) { } }); - await downloadArr(libraries.filter(lib => !lib.path.includes('minecraftforge')), path.join(INSTANCES_PATH, 'libraries'), dispatch, pack); - - await downloadArr(assets, path.join(INSTANCES_PATH, 'assets'), dispatch, pack, 10); - - await downloadArr(mainJar, path.join(INSTANCES_PATH, 'versions'), dispatch, pack); + await downloadArr( + libraries, + path.join(INSTANCES_PATH, 'libraries'), + dispatch, + pack + ); + + await downloadArr( + assets, + path.join(INSTANCES_PATH, 'assets'), + dispatch, + pack, + 10 + ); + + await downloadArr( + mainJar, + path.join(INSTANCES_PATH, 'versions'), + dispatch, + pack + ); await extractNatives(libraries.filter(lib => 'natives' in lib), pack); @@ -138,7 +231,7 @@ function addNextPackToActualDownload() { const { downloadManager } = getState(); const queueArr = Object.keys(downloadManager.downloadQueue); queueArr.some(pack => { - if (!downloadManager.downloadQueue[pack].status === 'Completed') { + if (downloadManager.downloadQueue[pack].status !== 'Completed') { dispatch({ type: START_DOWNLOAD, payload: pack @@ -146,6 +239,7 @@ function addNextPackToActualDownload() { dispatch(downloadPack(pack)); return true; } + return false; }); }; -} \ No newline at end of file +} diff --git a/app/actions/instancesManager.js b/app/actions/instancesManager.js index e0b0b9c70..6215e8fc1 100644 --- a/app/actions/instancesManager.js +++ b/app/actions/instancesManager.js @@ -1,5 +1,6 @@ import { message } from 'antd'; import log from 'electron-log'; +import { exec } from 'child_process'; import launchCommand from '../utils/MCLaunchCommand'; export const SELECT_INSTANCE = 'SELECT_INSTANCE'; @@ -35,22 +36,28 @@ export function selectInstance(name) { export function startInstance(instanceName) { return async (dispatch, getState) => { const { auth } = getState(); - const util = require('util'); - const exec = util.promisify(require('child_process').exec); - try { - dispatch({ - type: START_INSTANCE, - payload: instanceName - }); - const name = await exec(await launchCommand(instanceName, auth)); - } catch (error) { - message.error('There was an error while starting the instance'); - log.error(error); - } finally { + const start = exec(await launchCommand(instanceName, auth), (error, stdout, stderr) => { + if (error) { + console.error(`exec error: ${error}`); + return; + } + console.log(`stdout: ${stdout}`); + console.log(`stderr: ${stderr}`); + }); + dispatch({ + type: START_INSTANCE, + payload: instanceName, + pid: start.pid + }); + start.on('exit', () => { dispatch({ type: STOP_INSTANCE, payload: instanceName }); - } + }); + start.on('error', (err) => { + message.error('There was an error while starting the instance'); + log.error(err); + }); }; } diff --git a/app/actions/packCreator.js b/app/actions/packCreator.js index 6cdcf65dd..27ff394cb 100644 --- a/app/actions/packCreator.js +++ b/app/actions/packCreator.js @@ -5,6 +5,8 @@ import path from 'path'; import _ from 'lodash'; import { goBack } from 'react-router-redux'; import { promisify } from 'util'; +import { message } from 'antd'; +import vSort from 'version-sort'; import { PACKS_PATH, GAME_VERSIONS_URL, FORGE_PROMOS } from '../constants'; import { addToQueue } from './downloadManager'; @@ -15,33 +17,49 @@ export const START_PACK_CREATION = 'START_PACK_CREATION'; export const GET_FORGE_MANIFEST = 'GET_FORGE_MANIFEST'; export function getVanillaMCVersions() { - return async (dispatch) => { + return async dispatch => { const versions = await axios.get(GAME_VERSIONS_URL); dispatch({ type: GET_MC_VANILLA_VERSIONS, payload: versions }); const promos = (await axios.get(FORGE_PROMOS)).data; - let forgeVersions = {}; + const forgeVersions = {}; // This reads all the numbers for each version. It replaces each number // with the correct forge version. It filters numbers which do not have the "installer" // file. It then omits empty versions (not even one valid forge version for that mc version) Object.keys(promos.mcversion).forEach(v => { - forgeVersions[v] = promos.mcversion[v].filter(ver => { - const files = promos.number[ver].files; - for (let i = 0; i < files.length; i++) { - if (files[i].includes("installer")) { - return true; + if (v === '1.7.10_pre4') return; + forgeVersions[v] = promos.mcversion[v] + .filter(ver => { + const { files } = promos.number[ver]; + for (let i = 0; i < files.length; i++) { + if (files[i][1] === 'installer' && files[i][0] === 'jar') { + return true; + } } - } - return false; - }).map(ver => promos.number[ver].version); + return false; + }) + .map(ver => { + const { files } = promos.number[ver]; + let md5; + for (let i = 0; i < files.length; i++) { + if (files[i].includes('installer')) { + [,, md5] = files[i]; + } + } + return { + [promos.number[ver].version]: { + branch: promos.number[ver].branch, + md5 + } + }; + }); }); + dispatch({ type: GET_FORGE_MANIFEST, - payload: _.omitBy(forgeVersions, (v, k) => { - return v.length === 0; - }) + payload: _.omitBy(forgeVersions, v => v.length === 0) }); }; } @@ -52,17 +70,41 @@ export function createPack(version, packName, forgeVersion = null) { dispatch({ type: START_PACK_CREATION }); - // CREATE PACK FOLDER IF iT DOES NOT EXISt try { await promisify(fs.access)(path.join(PACKS_PATH, packName)); + message.warning('An instance with this name already exists.'); } catch (e) { await makeDir(path.join(PACKS_PATH, packName)); - } - dispatch(addToQueue(packName, version, forgeVersion)); - dispatch({ type: CREATION_COMPLETE }); - if (router.location.state && router.location.state.modal) { - setTimeout(dispatch(goBack()), 160); + dispatch(addToQueue(packName, version, forgeVersion)); + if (router.location.state && router.location.state.modal) { + setTimeout(dispatch(goBack()), 160); + } + } finally { + dispatch({ type: CREATION_COMPLETE }); } }; } +export function instanceDownloadOverride( + version, + packName, + forgeVersion = null +) { + return async (dispatch, getState) => { + const { router } = getState(); + + dispatch({ type: START_PACK_CREATION }); + + try { + await promisify(fs.access)(path.join(PACKS_PATH, packName)); + } catch (e) { + await makeDir(path.join(PACKS_PATH, packName)); + } finally { + dispatch(addToQueue(packName, version, forgeVersion)); + if (router.location.state && router.location.state.modal) { + setTimeout(dispatch(goBack()), 160); + } + dispatch({ type: CREATION_COMPLETE }); + } + }; +} diff --git a/app/actions/settings.js b/app/actions/settings.js index 302fa0dcf..70d7cdca4 100644 --- a/app/actions/settings.js +++ b/app/actions/settings.js @@ -25,7 +25,7 @@ export function loadSettings() { } } -export function saveSettings(notification = true) { +export function saveSettings(notification = false) { return (dispatch, getState) => { try { const { settings } = getState(); diff --git a/app/app.global.scss b/app/app.global.scss index 0fdef7988..5762cdd1a 100644 --- a/app/app.global.scss +++ b/app/app.global.scss @@ -3,36 +3,36 @@ * See https://github.com/webpack-contrib/sass-loader#imports */ -@import "./antd.css"; -@import "./style/theme/index"; +@import './antd.css'; +@import './style/theme/index'; @font-face { font-family: 'GlacialIndifferenceBold'; src: url('./assets/fonts/GlacialIndifferenceBold.otf') format('opentype'); font-weight: normal; font-style: normal; - } +} @font-face { font-family: 'GlacialIndifferenceItalic'; src: url('./assets/fonts/GlacialIndifferenceItalic.otf') format('opentype'); font-weight: normal; font-style: italic; - } +} @font-face { font-family: 'GlacialIndifferenceMedium'; src: url('./assets/fonts/GlacialIndifferenceMedium.otf') format('opentype'); font-weight: normal; font-style: normal; - } +} @font-face { font-family: 'GlacialIndifferenceRegular'; src: url('./assets/fonts/GlacialIndifferenceRegular.otf') format('opentype'); font-weight: normal; font-style: normal; - } +} html, body { @@ -105,14 +105,27 @@ a:hover { /* padding: 5px 0; */ pointer-events: none; text-align: center; - box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); - transition: transform 200ms !important; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); + transition: transform 2000ms !important; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + // animation: scale-up-tl 1s linear both; +} + +@keyframes scale-up-tl { + 0% { + transform: scale(0); + transform-origin: 0% 0%; + } + 100% { + transform: scale(1); + transform-origin: 0% 0%; + } } .react-contextmenu span { display: inline-block; - padding-bottom: 15px; + color: $text-hover-color; + padding-bottom: 4px; } .react-contextmenu.react-contextmenu--visible { @@ -126,7 +139,7 @@ a:hover { cursor: pointer; font-weight: 400; line-height: 1.5; - padding: 8px 10px; + padding: 4px 10px; text-align: left; white-space: nowrap; } @@ -163,8 +176,9 @@ a:hover { padding: 0; } -.react-contextmenu-item.react-contextmenu-submenu > .react-contextmenu-item::after { - content: "▶"; +.react-contextmenu-item.react-contextmenu-submenu + > .react-contextmenu-item::after { + content: '▶'; display: inline-block; position: absolute; right: 7px; @@ -175,7 +189,6 @@ a:hover { display: block; } - /* Overwrites ANTD CSS */ .ant-btn-primary { background-color: $primary-color; @@ -189,12 +202,16 @@ a:hover { border-right-width: 2px !important; } -.ant-btn-ghost:hover, .ant-btn-ghost:focus, .ant-btn-ghost:active, .ant-btn-ghost.active { +.ant-btn-ghost:hover, +.ant-btn-ghost:focus, +.ant-btn-ghost:active, +.ant-btn-ghost.active { color: $primary-color; border-color: $primary-color; } -.ant-btn-primary:hover, .ant-btn-primary:focus { +.ant-btn-primary:hover, +.ant-btn-primary:focus { background-color: darken($primary-color, 10); border-color: darken($primary-color, 10); } @@ -213,10 +230,12 @@ a:hover { background-color: $primary-color; } -.ant-switch-loading-icon, .ant-switch:after { +.ant-switch-loading-icon, +.ant-switch:after { background-color: #fff; } -.ant-input, .ant-input-group-addon { +.ant-input, +.ant-input-group-addon { border-color: $secondary-color-2; border-width: 2px !important; border-right-width: 2px !important; @@ -227,12 +246,13 @@ a:hover { border-right-width: 2px !important; } -.ant-input:focus, .ant-cascader-picker:focus .ant-cascader-input { +.ant-input:focus, +.ant-cascader-picker:focus .ant-cascader-input { box-shadow: none; } -.ant-input-group-addon:hover, -.ant-input-group-addon:focus, +.ant-input-group-addon:hover, +.ant-input-group-addon:focus, .ant-input-group-addon:active { background-color: $secondary-color-1; cursor: pointer; @@ -244,12 +264,61 @@ a:hover { } } +.ant-input-affix-wrapper .ant-input-prefix :not(.anticon), +.ant-input-affix-wrapper .ant-input-suffix :not(.anticon), +.ant-select-arrow .ant-select-arrow-icon svg { + cursor: pointer; + path { + cursor: pointer; + } +} + .ant-input-affix-wrapper:hover .ant-input:not(.ant-input-disabled), .ant-input:focus, .ant-input:hover, .ant-cascader-picker:focus .ant-cascader-input, -.ant-checkbox-checked .ant-checkbox-inner, .ant-checkbox-indeterminate .ant-checkbox-inner { +.ant-checkbox-checked .ant-checkbox-inner { border-color: $primary-color; border-width: 2px !important; border-right-width: 2px !important; +} + +.ant-select-selection { + border-color: $secondary-color-2; + background-color: $secondary-color-2; + border-width: 2px !important; + border-right-width: 2px !important; +} + +.ant-select-selection:hover, +.ant-select-focused .ant-select-selection, +.ant-select-selection:focus, +.ant-select-selection:active { + box-shadow: none; + border-color: $primary-color; + border-width: 2px !important; + border-right-width: 2px !important; +} + +.ant-select-dropdown-menu-item-selected, +.ant-select-dropdown-menu-item-selected:hover { + background-color: $secondary-color-3; +} + +// Table + +.ant-table-thead > tr > th { + background: rgba(0, 0, 0, 0.15); +} + +.ant-table-thead > tr.ant-table-row-hover > td, +.ant-table-tbody > tr.ant-table-row-hover > td, +.ant-table-thead > tr:hover > td, +.ant-table-tbody > tr:hover > td, +.ant-table-tbody > tr.ant-table-row-selected td { + background: rgba(0, 0, 0, 0.15); +} + +.ant-popover-title { + color: $text-dark; } \ No newline at end of file diff --git a/app/components/Common/Card/Card.scss b/app/components/Common/Card/Card.scss index 7ca199627..abab6182a 100644 --- a/app/components/Common/Card/Card.scss +++ b/app/components/Common/Card/Card.scss @@ -4,7 +4,7 @@ display: block; width: 100%; height: 200px; - background: $secondary-color-light; + background: $secondary-color-2; color: $text-hover-color; border-radius: 4px; flex-grow: 1; diff --git a/app/components/Common/CopyIcon/CopyIcon.scss b/app/components/Common/CopyIcon/CopyIcon.scss index f73dca54a..1f7c29ae1 100644 --- a/app/components/Common/CopyIcon/CopyIcon.scss +++ b/app/components/Common/CopyIcon/CopyIcon.scss @@ -4,4 +4,10 @@ display: inline-block; color: $text-main-color; cursor: pointer; + svg { + cursor: pointer; + path { + cursor: pointer; + } + } } \ No newline at end of file diff --git a/app/components/Common/Modal/Modal.js b/app/components/Common/Modal/Modal.js index c1497d62e..394905557 100644 --- a/app/components/Common/Modal/Modal.js +++ b/app/components/Common/Modal/Modal.js @@ -70,7 +70,7 @@ export default class Modal extends Component { ...this.props.style }, bgStyle: { - background: 'rgba(0, 0, 0, 0.6)', + background: 'rgba(0, 0, 0, 0.7)', transition: 'all 200ms ease-in-out' } diff --git a/app/components/Common/PageContent/PageContent.js b/app/components/Common/PageContent/PageContent.js index adb02ef4d..d44700dcb 100644 --- a/app/components/Common/PageContent/PageContent.js +++ b/app/components/Common/PageContent/PageContent.js @@ -1,7 +1,7 @@ import React from 'react'; import { Route } from 'react-router'; -import HomePage from '../../..//components/Home/containers/HomePage'; -import DManager from '../../..//components/DManager/containers/DManagerPage'; +import HomePage from '../../Home/containers/HomePage'; +import DManager from '../../DManager/containers/DManagerPage'; import ServerManager from '../../ServerManager/ServerManager'; import styles from './PageContent.scss'; diff --git a/app/components/Common/SideBar/SideBar.js b/app/components/Common/SideBar/SideBar.js index f805382cc..450657db9 100644 --- a/app/components/Common/SideBar/SideBar.js +++ b/app/components/Common/SideBar/SideBar.js @@ -1,9 +1,9 @@ // @flow import React, { Component } from 'react'; -import { Avatar, Button, Popover } from 'antd'; +import { Icon, Button, Popover } from 'antd'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import CIcon from '../../Common/Icon/Icon'; +import CIcon from '../Icon/Icon'; import styles from './SideBar.scss'; @@ -25,7 +25,7 @@ class SideBar extends Component { } this.props.checkForUpdates(); } - + render() { return ( ); } diff --git a/app/components/Common/SideBar/SideBar.scss b/app/components/Common/SideBar/SideBar.scss index 520713373..f10e06453 100644 --- a/app/components/Common/SideBar/SideBar.scss +++ b/app/components/Common/SideBar/SideBar.scss @@ -55,10 +55,53 @@ .playingServer { color: $green; } + .socialActions { + display: flex; + justify-content: space-between; + height: 25px; + line-height: 25px; + div:first-child { + position: relative; + right: 10px; + top: 2px; + i { + font-size: 20px; + color: $text-main-color; + cursor: pointer; + transition: all .2s ease-in-out; + &:hover { + color: $text-hover-color; + transition: all .2s ease-in-out; + } + svg { + cursor: pointer; + path { + cursor: pointer; + } + path:hover { + color: $text-hover-color; + transition: all .2s ease-in-out; + } + } + } + } + } .scroller { - height: calc(100% - 60px - 120px); + height: calc(100% - 60px - 140px - 25px); width: 200px; overflow: auto; + .serv { + position: relative; + padding: 9px 3px; + transition: all .3s; + display: flex; + justify-content: space-between; + border-bottom: 1px solid rgba(255, 255, 255, 0.3); + &:hover { + background: rgba(0, 0, 0, 0.3); + transition: all .3s; + } + } } } diff --git a/app/components/Settings/components/SideMenu/MenuItem/MenuItem.js b/app/components/Common/SideMenu/MenuItem/MenuItem.js similarity index 92% rename from app/components/Settings/components/SideMenu/MenuItem/MenuItem.js rename to app/components/Common/SideMenu/MenuItem/MenuItem.js index 9b600f5f7..9866b9099 100644 --- a/app/components/Settings/components/SideMenu/MenuItem/MenuItem.js +++ b/app/components/Common/SideMenu/MenuItem/MenuItem.js @@ -6,7 +6,7 @@ import styles from './MenuItem.scss'; const MenuItem = (props) => { return ( { + return ( +
+
+
+ {props.children} +
+
+
+ ); +}; + +export default SideMenu; \ No newline at end of file diff --git a/app/components/Settings/components/SideMenu/SideMenu.scss b/app/components/Common/SideMenu/SideMenu.scss similarity index 81% rename from app/components/Settings/components/SideMenu/SideMenu.scss rename to app/components/Common/SideMenu/SideMenu.scss index 0bf209225..f9423fb46 100644 --- a/app/components/Settings/components/SideMenu/SideMenu.scss +++ b/app/components/Common/SideMenu/SideMenu.scss @@ -1,4 +1,4 @@ -@import '../../../../style/theme/index'; +@import '../../../style/theme/index'; .container { grid-column: 1; diff --git a/app/components/Common/WindowNavigation/components/HorizontalMenu/HorizontalMenu.js b/app/components/Common/WindowNavigation/components/HorizontalMenu/HorizontalMenu.js index 508ed7090..295f75010 100644 --- a/app/components/Common/WindowNavigation/components/HorizontalMenu/HorizontalMenu.js +++ b/app/components/Common/WindowNavigation/components/HorizontalMenu/HorizontalMenu.js @@ -9,7 +9,7 @@ type Props = {}; export default class NavigationBar extends Component { props: Props; - isLocation = (loc) => { + isLocation = loc => { if (loc === this.props.location) { return true; } @@ -21,25 +21,46 @@ export default class NavigationBar extends Component {
  • - + HOME
  • - - + + INSTANCES
  • {/*
  • - + SERVERS -
  • */} + */}
); diff --git a/app/components/DInstance/DInstance.js b/app/components/DInstance/DInstance.js index 4bece5c99..15addd133 100644 --- a/app/components/DInstance/DInstance.js +++ b/app/components/DInstance/DInstance.js @@ -1,13 +1,17 @@ // @flow import React, { Component } from 'react'; -import { Button, Icon, Progress, message } from 'antd'; -import { Link } from 'react-router-dom'; +import { message } from 'antd'; +import psTree from 'ps-tree'; import { ContextMenu, MenuItem, ContextMenuTrigger } from 'react-contextmenu'; import fsa from 'fs-extra'; import path from 'path'; +import fs from 'fs'; +import os from 'os'; import log from 'electron-log'; +import { promisify } from 'util'; +import { exec } from 'child_process'; import { hideMenu } from 'react-contextmenu/es6/actions'; -import { PACKS_PATH } from '../../constants'; +import { PACKS_PATH, APPPATH } from '../../constants'; import { history } from '../../store/configureStore'; import styles from './DInstance.scss'; @@ -26,14 +30,27 @@ export default class DInstance extends Component { constructor(props) { super(props); this.state = { - deleting: false - } + deleting: false, + version: null + }; this.percentage = this.updatePercentage(); } + componentDidMount = async () => { + if (!this.isInstalling()) { + this.setState({ + version: JSON.parse( + await promisify(fs.readFile)( + path.join(PACKS_PATH, this.props.name, 'config.json') + ) + ).version + }); + } + }; + componentDidUpdate = () => { this.percentage = this.updatePercentage(); - } + }; isInstalling() { if (this.props.installingQueue[this.props.name]) { @@ -71,11 +88,25 @@ export default class DInstance extends Component { } } - handleClickPlay = async (e) => { - e.stopPropagation(); - this.props.startInstance(this.props.name); - this.props.selectInstance(this.props.name); - } + handleClickPlay = async e => { + if (!this.isInstalling()) { + e.stopPropagation(); + if (this.props.playing.find(el => el.name === this.props.name)) { + psTree( + this.props.playing.find(el => el.name === this.props.name).pid, + (err, children) => { + children.forEach(el => { + process.kill(el.PID); + }); + } + ); + message.info('Instance terminated'); + } else { + this.props.startInstance(this.props.name); + this.props.selectInstance(this.props.name); + } + } + }; deleteInstance = async () => { try { @@ -87,60 +118,157 @@ export default class DInstance extends Component { message.error('Error deleting instance'); log.error(err); } - } + }; render() { + const { name } = this.props; return (
- document.documentElement.style.setProperty('--instanceName', `"${this.props.name}"`) - } - onClick={(e) => { e.stopPropagation(); this.props.selectInstance(this.props.name); }} - onDoubleClick={this.handleClickPlay} - onKeyPress={this.handleKeyPress} - role="button" - tabIndex={0} + className={`${ + this.props.selectedInstance === name ? styles.selectedItem : '' + } ${styles.main}`} > - - {this.props.playing.find(el => el === this.props.name) && - } - {this.isInstalling() && - } -
-
#grayscale")' : '' }} - /> - - - {this.props.name} + +
+ document.documentElement.style.setProperty( + '--instanceName', + `"${name}"` + ) + } + onClick={e => { + e.stopPropagation(); + this.props.selectInstance(name); + }} + onDoubleClick={this.handleClickPlay} + onKeyPress={this.handleKeyPress} + role="button" + tabIndex={0} + > + {this.props.playing.find(el => el.name === name) && ( + + + + )} + {this.isInstalling() && ( + + - - {this.isInstalling() && ` (${this.updatePercentage()}%)`} + )} +
+
#grayscale\")" + : '' + }} + /> + + + {name} + + + {this.isInstalling() && ` (${this.updatePercentage()}%)`} + - +
- { e.stopPropagation(); this.props.selectInstance(this.props.name); }}> - {this.props.name} - + { + e.stopPropagation(); + this.props.selectInstance(name); + }} + > + + {name} ({this.state.version}) + + - Play + {this.props.playing.find(el => el.name === name) + ? 'Kill' + : 'Launch'} history.push({ pathname: `/editInstance/${this.props.name}`, state: { modal: true } })}> + onClick={() => + history.push({ + pathname: `/editInstance/${name}/settings/`, + state: { modal: true } + }) + } + > Manage - + exec(`start "" "${path.join(PACKS_PATH, name)}"`)} + > + + Open Folder + + { + exec( + `powershell $s=(New-Object -COM WScript.Shell).CreateShortcut('%userprofile%\\Desktop\\${ + this.props.name + }.lnk');$s.TargetPath='${path.join( + APPPATH, + 'GDLauncher.exe' + )}';$s.Arguments='-i ${this.props.name}';$s.Save()`, + (error) => { + if (error) { + log.error(`Error creating instance symlink: ${error}`); + message.error( + + Error while crerating the shortcut. Click{' '} +
+ here + {' '} + to know more + + ); + } + } + ); + }} + disabled={this.isInstalling() || process.platform !== 'win32'} + > + + {console.log(os.type())} + Create Shortcut + + exec(`start "" "${path.join(PACKS_PATH, name)}"`)} + > + + Export Instance + + {this.state.deleting ? 'Deleting...' : 'Delete'} -
+
); } } diff --git a/app/components/DInstance/DInstance.scss b/app/components/DInstance/DInstance.scss index 08a6762f6..6bb9b2e80 100644 --- a/app/components/DInstance/DInstance.scss +++ b/app/components/DInstance/DInstance.scss @@ -6,11 +6,16 @@ position: relative; width: 130px; height: 130px; - padding: 10px 20px; overflow: hidden; color: $text-main-color; } +.innerMain { + margin: 10px 20px; + width: 100%; + height: 100%; +} + .icon { display: inline-block; text-align: center; diff --git a/app/components/DManager/DManager.js b/app/components/DManager/DManager.js index c321dd90c..cfa811272 100644 --- a/app/components/DManager/DManager.js +++ b/app/components/DManager/DManager.js @@ -7,14 +7,17 @@ import makeDir from 'make-dir'; import { Promise } from 'bluebird'; import Link from 'react-router-dom/Link'; import log from 'electron-log'; -import { SortableContainer, SortableElement, arrayMove } from 'react-sortable-hoc'; +import { + SortableContainer, + SortableElement, + arrayMove +} from 'react-sortable-hoc'; import { ContextMenu, MenuItem, ContextMenuTrigger } from 'react-contextmenu'; import { hideMenu } from 'react-contextmenu/es6/actions'; import styles from './DManager.scss'; import DInstance from '../../containers/DInstance'; import { history } from '../../store/configureStore'; import { PACKS_PATH } from '../../constants'; -import store from '../../localStore'; type Props = { selectInstance: () => void @@ -22,9 +25,7 @@ type Props = { let watcher; -const SortableItem = SortableElement(({ value }) => - -); +const SortableItem = SortableElement(({ value }) => ); const SortableList = SortableContainer(({ items }) => { return ( @@ -38,13 +39,26 @@ const SortableList = SortableContainer(({ items }) => { const fs = Promise.promisifyAll(fss); - export default class DManager extends Component { props: Props; constructor(props) { super(props); this.state = { - instances: [] + instances: [], + checkingInstances: true + }; + } + + componentDidMount = async () => { + this.watchRoutine(); + }; + + componentWillUnmount() { + // Stop watching for changes when this component is unmounted + try { + watcher.close(); + } catch (err) { + log.error(err); } } @@ -69,7 +83,7 @@ export default class DManager extends Component { instances: await this.getDirectories(PACKS_PATH) }); }); - watcher.on('error', async (err) => { + watcher.on('error', async err => { try { await fs.accessAsync(PACKS_PATH); } catch (e) { @@ -83,51 +97,52 @@ export default class DManager extends Component { message.error( There was an error with inotify limit. see - here + + {' '} + here + ); } else { message.error('Cannot update instances in real time'); } + } finally { + this.setState({ + checkingInstances: false + }); } - } - - componentDidMount = () => { - this.watchRoutine(); - } - - - - componentWillUnmount() { - // Stop watching for changes when this component is unmounted - try { - watcher.close(); - } catch (err) { - log.error(err); - } - } + }; handleScroll = () => { hideMenu(); - } + }; /* eslint-disable */ openLink(url) { - require('electron').shell.openExternal(url) + require('electron').shell.openExternal(url); } onSortEnd = ({ oldIndex, newIndex }) => { this.setState({ - instances: arrayMove(this.state.instances, oldIndex, newIndex), + instances: arrayMove(this.state.instances, oldIndex, newIndex) }); }; - isDirectory = source => fss.lstatSync(source).isDirectory(); - getDirectories = async source => await fs.readdirAsync(source) - .map(name => join(source, name)) - .filter(this.isDirectory) - .map(dir => basename(dir)); + onSortStart = ({ node, index, collection }) => { + hideMenu(); + }; + isDirectory = source => fss.lstatSync(source).isDirectory(); + getDirectories = async source => + await fs + .readdirAsync(source) + .map(name => join(source, name)) + .filter(this.isDirectory) + .map(dir => basename(dir)); /* eslint-enable */ @@ -135,41 +150,72 @@ export default class DManager extends Component { return (
{ e.stopPropagation(); this.props.selectInstance(null) }} + onClick={e => { + e.stopPropagation(); + this.props.selectInstance(null); + }} >
- +
- - + +
- {this.state.instances.length !== 0 ? + {this.state.instances.length === 0 && + !this.state.checkingInstances ? ( +

+ YOU HAVEN'T ADDED ANY INSTANCE YET +

+ ) : ( : -

YOU HAVEN'T ADDED ANY INSTANCE YET

- } + /> + )}
- { e.stopPropagation(); this.props.selectInstance(null); }}> - history.push({ pathname: '/InstanceCreatorModal', state: { modal: true } })}> + { + e.stopPropagation(); + this.props.selectInstance(null); + }} + > + + history.push({ + pathname: '/InstanceCreatorModal', + state: { modal: true } + }) + } + > Add New Instance -
+ ); } } diff --git a/app/components/Home/Home.js b/app/components/Home/Home.js index 27ab3aa9b..25acf0c01 100644 --- a/app/components/Home/Home.js +++ b/app/components/Home/Home.js @@ -51,7 +51,6 @@ export default class Home extends Component {
{ constructor(props) { super(props); + const { forgeManifest, versionsManifest } = this.props; this.state = { loading: false, - checked: false, - versions: [{ - value: 'vanilla', - label: 'Vanilla', - children: [{ - value: 'releases', - label: 'Releases', - children: this.props.versionsManifest.filter(v => v.type === 'release').map((v) => { return { value: v.id, label: v.id } }), + versions: [ + { + value: 'vanilla', + label: 'Vanilla', + children: [ + { + value: 'releases', + label: 'Releases', + children: versionsManifest + .filter(v => v.type === 'release') + .map(v => ({ + value: v.id, + label: v.id + })) + }, + { + value: 'snapshots', + label: 'Snapshots', + children: versionsManifest + .filter(v => v.type === 'snapshot') + .map(v => ({ + value: v.id, + label: v.id + })) + } + ] }, { - value: 'snapshots', - label: 'Snapshots', - children: this.props.versionsManifest.filter(v => v.type === 'snapshot').map((v) => { return { value: v.id, label: v.id } }), - }] - }, - { - value: 'forge', - label: 'Forge', - children: _.reverse(vSort(_.without(Object.keys(this.props.forgeManifest), '1.7.10_pre4'))).map( - (v) => { - return { + value: 'forge', + label: 'Forge', + // _.reverse mutates arrays so we make a copy of it first using .slice() and then we reverse it + children: _.reverse(vSort(Object.keys(forgeManifest).slice())).map( + v => ({ value: v, label: v, - children: _.reverse(this.props.forgeManifest[v]).map(ver => { - return { - value: ver, - label: ver, - } - }) - } - } - ), - }] + // same as above + children: _.reverse(forgeManifest[v].slice()).map(ver => ({ + value: Object.keys(ver)[0], + label: Object.keys(ver)[0] + })) + }) + ) + } + ] }; } - handleSubmit = (e) => { + handleSubmit = e => { e.preventDefault(); this.props.form.validateFields((err, values) => { if (!err) { if (values.version[0] === 'vanilla') { this.props.createPack(values.version[2], values.packName); } else if (values.version[0] === 'forge') { - this.props.createPack(values.version[1], values.packName, values.version[2]); + this.props.createPack( + values.version[1], + values.packName, + values.version[2] + ); } - console.log(values) this.setState({ loading: true }); + setTimeout(() => { + this.setState({ loading: false }); + }, 100); } }); - } + }; render() { const { getFieldDecorator } = this.props.form; return ( - -
+ +
{getFieldDecorator('packName', { - rules: [{ required: true, message: 'Please input a name' }], + rules: [{ required: true, message: 'Please input a name' }] })( } + style={{ + width: '50vw', + display: 'inline-block', + height: '60px' + }} + prefix={ + + } placeholder="Instance Name" /> )} @@ -92,7 +138,7 @@ class InstanceCreatorModal extends Component {
{getFieldDecorator('version', { - rules: [{ required: true, message: 'Please select a version' }], + rules: [{ required: true, message: 'Please select a version' }] })( {
-
diff --git a/app/components/InstanceManagerModal/InstanceManagerModal.js b/app/components/InstanceManagerModal/InstanceManagerModal.js index c5ebe48fd..1118ddf71 100644 --- a/app/components/InstanceManagerModal/InstanceManagerModal.js +++ b/app/components/InstanceManagerModal/InstanceManagerModal.js @@ -1,11 +1,15 @@ // @flow import React, { Component } from 'react'; -import { Select, Form, Input, Icon, Button, Checkbox } from 'antd'; +import { Form } from 'antd'; +import { Route } from 'react-router-dom'; import styles from './InstanceManagerModal.scss'; import Modal from '../Common/Modal/Modal'; +import SideMenu from '../Common/SideMenu/SideMenu'; +import MenuItem from '../Common/SideMenu/MenuItem/MenuItem'; +import Settings from './Settings/Settings'; +import ModsManager from './ModsManager/ModsManager'; type Props = {}; -const FormItem = Form.Item; let pack; class InstanceManagerModal extends Component { @@ -25,44 +29,24 @@ class InstanceManagerModal extends Component { } render() { - const { getFieldDecorator } = this.props.form; return ( - - -
- - {getFieldDecorator('packName', { - rules: [{ required: true, message: 'Please input a name' }], - initialValue: this.props.match.params.instance, - })( - } - placeholder="Instance Name" - /> - )} - + +
+ + Settings + Mods Manager + Resource Packs + Worlds + Screenshots + +
+ } /> +
-
- - {getFieldDecorator('snapshots', { - valuePropName: 'checked', - initialValue: false, - })( - Show Snapshots - )} - -
-
- -
- +
); } } -export default Form.create()(InstanceManagerModal); +export default InstanceManagerModal; diff --git a/app/components/InstanceManagerModal/InstanceManagerModal.scss b/app/components/InstanceManagerModal/InstanceManagerModal.scss index e69de29bb..e8ced0455 100644 --- a/app/components/InstanceManagerModal/InstanceManagerModal.scss +++ b/app/components/InstanceManagerModal/InstanceManagerModal.scss @@ -0,0 +1,19 @@ +.container{ + display: grid; + grid-template-columns: 1.1fr 3fr; + height: 100%; + width: 100%; + font-family: 'GlacialIndifferenceRegular'; +} + +.content { + overflow-y: auto; + display: flex; + justify-content: center; +} + +.createInstance { + position: absolute; + bottom: 15px; + right: 15px; +} \ No newline at end of file diff --git a/app/components/InstanceManagerModal/ModsManager/LocalMods/LocalMods.js b/app/components/InstanceManagerModal/ModsManager/LocalMods/LocalMods.js new file mode 100644 index 000000000..ae07235a9 --- /dev/null +++ b/app/components/InstanceManagerModal/ModsManager/LocalMods/LocalMods.js @@ -0,0 +1,139 @@ +import React, { Component } from 'react'; +import { bindActionCreators } from 'redux'; +import { remote } from 'electron'; +import fss from 'fs'; +import path from 'path'; +import Promise from 'bluebird'; +import makeDir from 'make-dir'; +import log from 'electron-log'; +import { connect } from 'react-redux'; +import { List, Icon, Avatar, Upload, Transfer, Button, Table, Switch } from 'antd'; +import { PACKS_PATH } from '../../../../constants'; + +import styles from './LocalMods.scss'; + +const fs = Promise.promisifyAll(fss); + +type Props = {}; + +let watcher; + +class LocalMods extends Component { + props: Props; + + state = { + mods: [], + selectedRowKeys: [], // Check here to configure the default column + loading: false, + } + + componentDidMount = async () => { + try { + await fs.accessAsync(path.join(PACKS_PATH, this.props.match.params.instance, 'mods')); + } catch (err) { + await makeDir(path.join(PACKS_PATH, this.props.match.params.instance, 'mods')); + } + this.getMods(); + } + + componentWillUnmount() { + // Stop watching for changes when this component is unmounted + try { + watcher.close(); + } catch (err) { + log.error(err); + } + } + + columns = [{ + title: 'Name', + dataIndex: 'name', + width: 200, + render: (title) => path.parse(title.replace('.disabled', '')).name + }, { + title: 'State', + dataIndex: 'state', + width: 200, + render: (state, record) => this.handleChange(checked, record)} /> + }]; + + getMods = async () => { + let mods = (await fs.readdirAsync(path.join(PACKS_PATH, this.props.match.params.instance, 'mods'))) + .filter(el => el !== 'GDLCompanion.jar') + .map(el => { return { name: el, state: path.extname(el) !== '.disabled', key: el } }); + this.setState({ + mods + }); + // Watches for any changes in the packs dir. TODO: Optimize + watcher = fss.watch(path.join(PACKS_PATH, this.props.match.params.instance, 'mods'), async () => { + mods = (await fs.readdirAsync(path.join(PACKS_PATH, this.props.match.params.instance, 'mods'))) + .filter(el => el !== 'GDLCompanion.jar') + .map(el => { return { name: el, state: path.extname(el) !== '.disabled', key: el } }); + this.setState({ + mods + }); + }); + } + + modStateChanger = (selectedRowKeys) => { + this.setState({ selectedRowKeys }); + } + + + handleChange = async (checked, record) => { + if (checked) { + await fs.renameAsync(path.join(PACKS_PATH, this.props.match.params.instance, 'mods', record.name), path.join(PACKS_PATH, this.props.match.params.instance, 'mods', record.name.replace('.disabled', ''))); + } else { + await fs.renameAsync(path.join(PACKS_PATH, this.props.match.params.instance, 'mods', record.name), path.join(PACKS_PATH, this.props.match.params.instance, 'mods', `${record.name}.disabled`)); + } + } + + selectFiles = async () => { + const mods = remote.dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'] }); + }; + + delete = async () => { + this.setState({ loading: true }); + await Promise.each(this.state.selectedRowKeys, async el => fs.unlinkAsync(path.join(PACKS_PATH, this.props.match.params.instance, 'mods', el))); + this.setState({ loading: false, selectedRowKeys: [] }); + }; + + render() { + const { loading, selectedRowKeys } = this.state; + const rowSelection = { + selectedRowKeys, + onChange: this.modStateChanger, + }; + const hasSelected = selectedRowKeys.length > 0; + return ( +
+
+ + + {hasSelected ? `${selectedRowKeys.length} ${selectedRowKeys.length === 1 ? "mod" : "mods"} selected` : ''} + +
+ + + ) + } +} + +function mapStateToProps(state) { + return {}; +} + + +export default connect(mapStateToProps)(LocalMods); diff --git a/app/components/InstanceManagerModal/ModsManager/LocalMods/LocalMods.scss b/app/components/InstanceManagerModal/ModsManager/LocalMods/LocalMods.scss new file mode 100644 index 000000000..c25c49094 --- /dev/null +++ b/app/components/InstanceManagerModal/ModsManager/LocalMods/LocalMods.scss @@ -0,0 +1,4 @@ +.transfer { + position: relative; + top: 20px; +} \ No newline at end of file diff --git a/app/components/InstanceManagerModal/ModsManager/ModsBrowser/ModPage.js b/app/components/InstanceManagerModal/ModsManager/ModsBrowser/ModPage.js new file mode 100644 index 000000000..b249422ab --- /dev/null +++ b/app/components/InstanceManagerModal/ModsManager/ModsBrowser/ModPage.js @@ -0,0 +1,37 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import Link from 'react-router-dom/Link'; + +import styles from './ModPage.scss'; + +type Props = {}; + +class ModPage extends Component { + props: Props; + + render() { + return ( +
+ + Back + + {this.props.match.params.mod} + ASDASD +
+ ); + } +} + +function mapStateToProps(state) { + return {}; +} + +export default connect(mapStateToProps)(ModPage); diff --git a/app/components/InstanceManagerModal/ModsManager/ModsBrowser/ModPage.scss b/app/components/InstanceManagerModal/ModsManager/ModsBrowser/ModPage.scss new file mode 100644 index 000000000..e69de29bb diff --git a/app/components/InstanceManagerModal/ModsManager/ModsBrowser/ModsList.js b/app/components/InstanceManagerModal/ModsManager/ModsBrowser/ModsList.js new file mode 100644 index 000000000..77e08c2f5 --- /dev/null +++ b/app/components/InstanceManagerModal/ModsManager/ModsBrowser/ModsList.js @@ -0,0 +1,349 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import Link from 'react-router-dom/Link'; +import axios from 'axios'; +import path from 'path'; +import log from 'electron-log'; +import { + List, + Avatar, + Button, + Skeleton, + Input, + Select, + Icon, + Popover +} from 'antd'; +import { PACKS_PATH, CURSEMETA_API_URL } from '../../../../constants'; +import { downloadFile } from '../../../../utils/downloader'; +import { numberToRoundedWord } from '../../../../utils/numbers'; + +import styles from './ModsList.scss'; + +type Props = {}; + +class ModsList extends Component { + props: Props; + + constructor(props) { + super(props); + this.state = { + initLoading: true, + loading: false, + list: [], + data: [], + installing: [], + searchText: '', + filterType: 'Featured' + }; + console.log(this); + } + + componentDidMount = async () => { + try { + await this.getMods(); + } catch (err) { + log.error(err.message); + } + }; + + getMods = async () => { + this.setState({ + initLoading: true, + list: [...new Array(10)].map(() => ({ loading: true, name: {} })), + data: [] + }); + const res = await axios.get( + `${CURSEMETA_API_URL}/direct/addon/search?gameId=432&pageSize=10&index=0&sort=${ + this.state.filterType + }&searchFilter=${this.state.searchText}&gameVersion=${ + this.props.match.params.version + }&categoryId=0§ionId=6&sortDescending=${this.state.filterType !== + 'author' && this.state.filterType !== 'name'}` + ); + this.setState({ + initLoading: false, + list: res.data, + data: res.data + }); + }; + + onLoadMore = async () => { + this.setState({ + loading: true, + // Adding 10 fakes elements to the list to simulate a loading + list: this.state.data.concat( + [...new Array(10)].map(() => ({ loading: true, name: {} })) + ) + }); + const res = await axios.get( + `${CURSEMETA_API_URL}/direct/addon/search?gameId=432&pageSize=10&index=${ + this.state.list.length + }&sort=${this.state.filterType}&searchFilter=${ + this.state.searchText + }&gameVersion=${ + this.props.match.params.version + }&categoryId=0§ionId=6&sortDescending=${this.state.filterType !== + 'author' && this.state.filterType !== 'name'}` + ); + // We now remove the previous 10 elements and add the real 10 + const data = this.state.data.concat(res.data); + this.setState( + { + list: data, + data, + loading: false + }, + () => { + // Resetting window's offsetTop so as to display react-virtualized demo underfloor. + // In a real scene, you can use the public method of react-virtualized: + // https://stackoverflow.com/questions/46700726/how-to-use-public-method-updateposition-of-react-virtualized + window.dispatchEvent(new Event('resize')); + } + ); + }; + + installMod = async (data, parent = null) => { + const { projectFileId, projectFileName } = data.gameVersionLatestFiles.find( + n => n.gameVersion === this.props.match.params.version + ); + if (parent === null) { + this.setState(prevState => ({ + installing: { + ...prevState.installing, + [projectFileName]: { + installing: true, + completed: false + } + } + })); + } + + const url = await axios.get( + `${CURSEMETA_API_URL}/direct/addon/${data.id}/file/${projectFileId}` + ); + + await downloadFile( + path.join( + PACKS_PATH, + this.props.match.params.instance, + 'mods', + url.data.fileNameOnDisk + ), + url.data.downloadUrl, + () => {} + ); + if (url.data.dependencies.length !== 0) { + url.data.dependencies.forEach(async dep => { + // It looks like type 1 are required dependancies and type 3 are dependancies that are already embedded in the parent one + if (dep.type === 1) { + const depData = await axios.get( + `${CURSEMETA_API_URL}/direct/addon/${dep.addonId}` + ); + await this.installMod(depData.data, projectFileName); + } + }); + } + this.setState(prevState => ({ + installing: { + ...prevState.installing, + [parent === null ? projectFileName : parent]: { + installing: false, + completed: true + } + } + })); + }; + + isDownloadCompleted = data => { + const mod = Object.keys(this.state.installing).find( + n => + n === + data.gameVersionLatestFiles.find( + x => x.gameVersion === this.props.match.params.version + ).projectFileName + ); + return this.state.installing[mod] && this.state.installing[mod].completed; + }; + + isInstalling = data => { + const mod = Object.keys(this.state.installing).find( + n => + n === + data.gameVersionLatestFiles.find( + x => x.gameVersion === this.props.match.params.version + ).projectFileName + ); + return this.state.installing[mod] && this.state.installing[mod].installing; + }; + + filterChanged = async value => { + this.setState({ filterType: value }, async () => { + try { + await this.getMods(); + } catch (err) { + log.error(err.message); + } + }); + }; + + onSearchChange = e => { + this.setState({ searchText: encodeURI(e.target.value) }); + }; + + onSearchSubmit = async () => { + this.getMods(); + }; + + emitEmptySearchText = () => { + this.setState({ searchText: '' }, () => { + this.getMods(); + }); + }; + + render() { + const { initLoading, loading, list } = this.state; + const loadMore = + !initLoading && !loading ? ( +
+ +
+ ) : null; + return ( +
+
+ + + + ) : null + } + /> +
+ Sort By{' '} + +
+
+ ( + this.installMod(item)} + > + {this.isInstalling(item) + ? 'Installing' + : this.isDownloadCompleted(item) + ? 'Installed' + : 'Install'} + + ) + ]} + > + + + } + title={ + + {item.name} + + } + description={ + item.loading ? ( + '' + ) : ( +
+ {item.summary} +
+ + Downloads: {numberToRoundedWord(item.downloadCount)} + + + Updated:{' '} + {new Date( + item.latestFiles[0].fileDate + ).toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric' + })} + +
+
+ ) + } + /> +
+
+ )} + /> +
+ ); + } +} + +function mapStateToProps(state) { + return {}; +} + +export default connect(mapStateToProps)(ModsList); diff --git a/app/components/InstanceManagerModal/ModsManager/ModsBrowser/ModsList.scss b/app/components/InstanceManagerModal/ModsManager/ModsBrowser/ModsList.scss new file mode 100644 index 000000000..580cf76ce --- /dev/null +++ b/app/components/InstanceManagerModal/ModsManager/ModsBrowser/ModsList.scss @@ -0,0 +1,25 @@ +@import '../../../../style/theme/colors.scss'; + +.header { + position: relative; + top: 10px; + margin-bottom: 10px; + display: flex; + justify-content: space-evenly; +} + +.modsContainer { + position: relative; + top: 10px; + overflow-y: scroll; + height: 100%; + width: 100%; + max-width: 800px; + .modFooter { + display: flex; + justify-content: space-evenly; + font-style: italic; + font-size: 14px; + color: $text-dark; + } +} \ No newline at end of file diff --git a/app/components/InstanceManagerModal/ModsManager/ModsManager.js b/app/components/InstanceManagerModal/ModsManager/ModsManager.js new file mode 100644 index 000000000..2a66c29fc --- /dev/null +++ b/app/components/InstanceManagerModal/ModsManager/ModsManager.js @@ -0,0 +1,119 @@ +import React, { Component } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { Route, Switch } from 'react-router-dom'; +import Link from 'react-router-dom/Link'; +import { promisify } from 'util'; +import path from 'path'; +import fs from 'fs'; +import log from 'electron-log'; +import { List, Icon, Avatar, Radio } from 'antd'; +import { PACKS_PATH } from '../../../constants'; +import ModsList from './ModsBrowser/ModsList'; +import LocalMods from './LocalMods/LocalMods'; +import ModPage from './ModsBrowser/ModPage'; + +import styles from './ModsManager.scss'; + +type Props = {}; + +class ModsManager extends Component { + props: Props; + + constructor(props) { + super(props); + this.state = { + isForge: false, + version: null, + checkingForge: true + }; + } + + componentDidMount = async () => { + try { + const config = JSON.parse( + await promisify(fs.readFile)( + path.join(PACKS_PATH, this.props.match.params.instance, 'config.json') + ) + ); + + this.setState({ version: config.version }); + + if (config.forgeVersion !== null) { + this.setState({ isForge: true }); + } + } catch (err) { + log.error(err.message); + } finally { + this.setState({ checkingForge: false }); + } + }; + + render() { + if (this.state.checkingForge) { + return null; + } + if (!this.state.isForge) { + return ( +
+

+ This instance does not allow mods.
Install forge if you want + to use them +

+
+ ); + } + return ( +
+ + + Local + + + Browse + + + + + + + +
+ ); + } +} + +function mapStateToProps(state) { + return {}; +} + +export default connect(mapStateToProps)(ModsManager); diff --git a/app/components/InstanceManagerModal/ModsManager/ModsManager.scss b/app/components/InstanceManagerModal/ModsManager/ModsManager.scss new file mode 100644 index 000000000..e69de29bb diff --git a/app/components/InstanceManagerModal/Settings/ForgeManager.js b/app/components/InstanceManagerModal/Settings/ForgeManager.js new file mode 100644 index 000000000..8d6df5aba --- /dev/null +++ b/app/components/InstanceManagerModal/Settings/ForgeManager.js @@ -0,0 +1,170 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Button, Icon, Tooltip, Select, message, Switch } from 'antd'; +import path from 'path'; +import fs from 'fs'; +import { promisify } from 'util'; +import _ from 'lodash'; +import makeDir from 'make-dir'; +import { bindActionCreators } from 'redux'; +import * as packCreatorActions from '../../../actions/packCreator'; +import * as downloadManagerActions from '../../../actions/downloadManager'; +import { downloadFile } from '../../../utils/downloader'; +import { PACKS_PATH, GDL_COMPANION_MOD_URL } from '../../../constants'; +import colors from '../../../style/theme/colors.scss'; +import styles from './ForgeManager.scss'; + +type Props = {}; + +class Instances extends Component { + props: Props; + + state = { + forgeSelectVersion: null, + loadingCompanionDownload: false, + companionModState: false + }; + + componentDidMount = async () => { + try { + await promisify(fs.access)( + path.join(PACKS_PATH, this.props.name, 'mods', 'GDLCompanion.jar') + ); + this.setState({ companionModState: true }); + } catch (err) {} + }; + + removeForge = async () => { + const config = JSON.parse( + await promisify(fs.readFile)( + path.join(PACKS_PATH, this.props.name, 'config.json') + ) + ); + await promisify(fs.writeFile)( + path.join(PACKS_PATH, this.props.name, 'config.json'), + JSON.stringify({ ...config, forgeVersion: null }) + ); + }; + + installForge = () => { + if (this.state.forgeSelectVersion === null) { + message.warning('You need to select a version.'); + } else { + this.props.instanceDownloadOverride( + this.props.data.version, + this.props.name, + this.state.forgeSelectVersion + ); + } + }; + + handleForgeVersionChange = value => { + this.setState({ forgeSelectVersion: value }); + }; + + companionModSwitchChange = async value => { + this.setState({ loadingCompanionDownload: true }); + if (value) { + await makeDir(path.join(PACKS_PATH, this.props.name, 'mods')); + await downloadFile( + path.join(PACKS_PATH, this.props.name, 'mods', 'GDLCompanion.jar'), + GDL_COMPANION_MOD_URL, + () => {} + ); + this.setState({ companionModState: true }); + } else { + await promisify(fs.unlink)( + path.join(PACKS_PATH, this.props.name, 'mods', 'GDLCompanion.jar') + ); + this.setState({ companionModState: false }); + } + this.setState({ loadingCompanionDownload: false }); + }; + + render() { + if (this.props.data.forgeVersion === null) { + return ( +
+ Forge is not installed
+ +
+ +
+ ); + } + return ( +
+
+ {this.props.data.forgeVersion}
+ +
+
+ Companion Mod{' '} + + + +
+ +
+
+ ); + } +} + +function mapStateToProps(state) { + return { + forgeVersions: state.packCreator.forgeManifest + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators( + { ...packCreatorActions, ...downloadManagerActions }, + dispatch + ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Instances); diff --git a/app/components/InstanceManagerModal/Settings/ForgeManager.scss b/app/components/InstanceManagerModal/Settings/ForgeManager.scss new file mode 100644 index 000000000..195cb0dc1 --- /dev/null +++ b/app/components/InstanceManagerModal/Settings/ForgeManager.scss @@ -0,0 +1,9 @@ +.companionModInfo { + cursor: pointer; + svg { + cursor: pointer; + path { + cursor: pointer; + } + } +} diff --git a/app/components/InstanceManagerModal/Settings/Settings.js b/app/components/InstanceManagerModal/Settings/Settings.js new file mode 100644 index 000000000..cfc4ad47c --- /dev/null +++ b/app/components/InstanceManagerModal/Settings/Settings.js @@ -0,0 +1,133 @@ +import React, { Component } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { Select, Form, Input, Icon, Button, Checkbox } from 'antd'; +import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; +import fsa from 'fs-extra'; +import path from 'path'; +import { promisify } from 'util'; +import fs from 'fs'; +import log from 'electron-log'; +import Card from '../../Common/Card/Card'; +import { PACKS_PATH } from '../../../constants'; +import styles from './Settings.scss'; +import ForgeManager from './ForgeManager'; + +const FormItem = Form.Item; +type Props = {}; + +let watcher = null; + +class Instances extends Component { + props: Props; + + constructor(props) { + super(props); + this.state = { + instanceConfig: null, + checkingForge: true + }; + } + + componentDidMount = async () => { + try { + let config = JSON.parse( + await promisify(fs.readFile)( + path.join(PACKS_PATH, this.props.instance, 'config.json') + ) + ); + this.setState({ instanceConfig: config }); + watcher = fs.watch( + path.join(PACKS_PATH, this.props.instance, 'config.json'), + { encoding: 'utf8' }, + async (eventType, filename) => { + config = JSON.parse( + await promisify(fs.readFile)( + path.join(PACKS_PATH, this.props.instance, 'config.json') + ) + ); + this.setState({ instanceConfig: config }); + } + ); + } catch (err) { + log.error(err.message); + } finally { + this.setState({ checkingForge: false }); + } + }; + + componentWillUnmount = () => { + watcher.close(); + }; + + render() { + const { getFieldDecorator } = this.props.form; + return ( +
+

Edit Instance Settings

+
+
+ + {getFieldDecorator('packName', { + rules: [{ required: true, message: 'Please input a name' }], + initialValue: this.props.instance + })( +
+ + } + placeholder="Instance Name" + /> + +
+ )} +
+
+ + {!this.state.checkingForge ? ( + + ) : null} + + +
+ ); + } +} + +function mapStateToProps(state) { + return {}; +} + +export default Form.create()(connect(mapStateToProps)(Instances)); diff --git a/app/components/InstanceManagerModal/Settings/Settings.scss b/app/components/InstanceManagerModal/Settings/Settings.scss new file mode 100644 index 000000000..e69de29bb diff --git a/app/components/Settings/Settings.js b/app/components/Settings/Settings.js index 4697a58ec..a9254ba3b 100644 --- a/app/components/Settings/Settings.js +++ b/app/components/Settings/Settings.js @@ -3,7 +3,8 @@ import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; import { Button } from 'antd'; import Modal from '../Common/Modal/Modal'; import styles from './Settings.scss'; -import SideMenu from './components/SideMenu/SideMenu'; +import SideMenu from '../Common/SideMenu/SideMenu'; +import MenuItem from '../Common/SideMenu/MenuItem/MenuItem'; import Content from './components/Content/Content'; const Settings = ({ match, history }) => { @@ -17,7 +18,12 @@ const Settings = ({ match, history }) => { } >
- + + My Account & Preferences + Java + Instances + User Interface +
diff --git a/app/components/Settings/components/ButtonSetting/ButtonSetting.js b/app/components/Settings/components/ButtonSetting/ButtonSetting.js index aecb90580..8a7ebdc5a 100644 --- a/app/components/Settings/components/ButtonSetting/ButtonSetting.js +++ b/app/components/Settings/components/ButtonSetting/ButtonSetting.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Button, Divider } from 'antd'; +import { Button, Divider, Icon } from 'antd'; import styles from './ButtonSetting.scss'; const ButtonSetting = (props) => { @@ -7,7 +7,7 @@ const ButtonSetting = (props) => {
-
{props.mainText}
+
{props.mainText}
{props.description}
diff --git a/app/components/Settings/components/Content/Content.js b/app/components/Settings/components/Content/Content.js index 97c9f09e8..6d78d3b4c 100644 --- a/app/components/Settings/components/Content/Content.js +++ b/app/components/Settings/components/Content/Content.js @@ -4,12 +4,14 @@ import { Button } from 'antd'; import styles from './Content.scss'; import MyAccount_Preferences from '../MyAccount_Preferences/MyAccount_Preferences'; import Instances from '../Instances/Instances'; +import JavaManager from '../JavaManager/JavaManager'; const Content = ({ match }) => { return (
- - + + +
); }; diff --git a/app/components/Settings/components/Instances/Instances.js b/app/components/Settings/components/Instances/Instances.js index bd7ae17cb..596934768 100644 --- a/app/components/Settings/components/Instances/Instances.js +++ b/app/components/Settings/components/Instances/Instances.js @@ -16,12 +16,14 @@ type Props = {}; class Instances extends Component { props: Props; + constructor(props) { super(props); this.state = { deletingInstances: false }; } + deleteShareData = async () => { try { this.setState({ deletingInstances: true }); @@ -35,6 +37,7 @@ class Instances extends Component { message.error('Error while clearing data.'); } }; + render() { return (
@@ -43,6 +46,7 @@ class Instances extends Component { { + return ( +
+ {/* Java Manager +
+
+ {props.username.charAt(0).toUpperCase()} +
+
+ USERNAME + {props.username}{' '} + +
+ EMAIL + {props.email}{' '} + +
+
+ Preferences + + + */} +
+ ); +}; + +function mapStateToProps(state) { + return { + username: state.auth.displayName, + email: state.auth.email, + settings: state.settings + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators(SettingsActions, dispatch); +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(MyAccount); diff --git a/app/components/Settings/components/JavaManager/JavaManager.scss b/app/components/Settings/components/JavaManager/JavaManager.scss new file mode 100644 index 000000000..2f36a517a --- /dev/null +++ b/app/components/Settings/components/JavaManager/JavaManager.scss @@ -0,0 +1,36 @@ +@import '../../../../style/theme/index'; + + +.container { + +} + +.accountInfo { + display: grid; + grid-template-columns: 100px auto; + background: $secondary-color-3; + height: 120px; + width: 100%; + div:first-child { + align-self: center; + justify-self: center; + } + div:nth-child(2) { + display: block; + color: $text-dark; + margin: 10px; + *:not(i) { + display: block; + } + .divider { + height: 10px; + } + .info { + display: inline-block; + color: $text-main-color; + -webkit-user-select: text; + user-select: text; + cursor: text; + } + } +} \ No newline at end of file diff --git a/app/components/Settings/components/MyAccount_Preferences/MyAccount_Preferences.js b/app/components/Settings/components/MyAccount_Preferences/MyAccount_Preferences.js index d3e8058bc..55b2c6105 100644 --- a/app/components/Settings/components/MyAccount_Preferences/MyAccount_Preferences.js +++ b/app/components/Settings/components/MyAccount_Preferences/MyAccount_Preferences.js @@ -23,10 +23,10 @@ const MyAccount = (props) => {
USERNAME - {props.username} + {props.username}
EMAIL - {props.email} + {props.email}
Preferences @@ -34,6 +34,7 @@ const MyAccount = (props) => { diff --git a/app/components/Settings/components/SettingInput/SettingInput.js b/app/components/Settings/components/SettingInput/SettingInput.js new file mode 100644 index 000000000..ae59ba516 --- /dev/null +++ b/app/components/Settings/components/SettingInput/SettingInput.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { Switch, Divider, Icon, InputNumber } from 'antd'; +import styles from './SettingInput.scss'; + +const SettingInput = props => { + return ( +
+
+
+
+ {props.mainText}{' '} + +
+
{props.description}
+
+
+ +
+
+ +
+ ); +}; + +export default SettingInput; diff --git a/app/components/Settings/components/SettingInput/SettingInput.scss b/app/components/Settings/components/SettingInput/SettingInput.scss new file mode 100644 index 000000000..1ab2cf0fd --- /dev/null +++ b/app/components/Settings/components/SettingInput/SettingInput.scss @@ -0,0 +1,21 @@ +@import '../../../../style/theme/index'; + +.container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.action { +} + +.mainText { + font-size: 18px; +} + +.description { + font-size: 14px; + color: $text-dark; + padding: 5px; +} \ No newline at end of file diff --git a/app/components/Settings/components/SideMenu/SideMenu.js b/app/components/Settings/components/SideMenu/SideMenu.js deleted file mode 100644 index ac2d6a98e..000000000 --- a/app/components/Settings/components/SideMenu/SideMenu.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; -import { Button } from 'antd'; -import styles from './SideMenu.scss'; -import MenuItem from './MenuItem/MenuItem'; - -const SideMenu = ({ match }) => { - return ( -
-
-
- My Account & Preferences - Java - Instances - User Interface -
-
-
- ); -}; - -export default SideMenu; \ No newline at end of file diff --git a/app/components/Settings/components/SwitchSetting/SwitchSetting.js b/app/components/Settings/components/SwitchSetting/SwitchSetting.js index 8c69e152a..f1f62043a 100644 --- a/app/components/Settings/components/SwitchSetting/SwitchSetting.js +++ b/app/components/Settings/components/SwitchSetting/SwitchSetting.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Switch, Divider } from 'antd'; +import { Switch, Divider, Icon } from 'antd'; import styles from './SwitchSetting.scss'; const SwitchSetting = (props) => { @@ -7,7 +7,7 @@ const SwitchSetting = (props) => {
-
{props.mainText}
+
{props.mainText}
{props.description}
diff --git a/app/constants.js b/app/constants.js index 7eb965c19..6b0b086fd 100644 --- a/app/constants.js +++ b/app/constants.js @@ -2,23 +2,40 @@ import path from 'path'; import getAppPath from './utils/getAppPath'; export const APPPATH = getAppPath.getAppPath(); +export const DATAPATH = path.join( + process.env.APPDATA || + (process.platform === DARWIN + ? path.join(process.env.HOME, 'Library/Preferences') + : '/var/local'), + 'GDLauncher' +); export const LAUNCHER_FOLDER = 'launcherData'; export const INSTANCES_FOLDER = 'instances'; -export const SERVERS_PATH = path.join(APPPATH, LAUNCHER_FOLDER, 'servers'); -export const INSTANCES_PATH = path.join(APPPATH, LAUNCHER_FOLDER, INSTANCES_FOLDER); -export const PACKS_PATH = path.join(APPPATH, LAUNCHER_FOLDER, INSTANCES_FOLDER, 'packs'); -export const GAME_VERSIONS_URL = 'https://launchermeta.mojang.com/mc/game/version_manifest.json'; -export const FORGE_PROMOS = 'http://files.minecraftforge.net/maven/net/minecraftforge/forge/json'; -export const ACCESS_TOKEN_VALIDATION_URL = 'https://authserver.mojang.com/validate'; +export const SERVERS_PATH = path.join(DATAPATH, 'servers'); +export const INSTANCES_PATH = path.join(DATAPATH, INSTANCES_FOLDER); +export const PACKS_PATH = path.join(DATAPATH, INSTANCES_FOLDER, 'packs'); +export const META_PATH = path.join(DATAPATH, 'meta'); +export const GAME_VERSIONS_URL = + 'https://launchermeta.mojang.com/mc/game/version_manifest.json'; +export const FORGE_PROMOS = + 'http://files.minecraftforge.net/maven/net/minecraftforge/forge/json'; +export const ACCESS_TOKEN_VALIDATION_URL = + 'https://authserver.mojang.com/validate'; export const ACCESS_TOKEN_REFRESH_URL = 'https://authserver.mojang.com/refresh'; export const MAVEN_REPO = 'http://central.maven.org/maven2'; export const MC_LIBRARIES_URL = 'https://libraries.minecraft.net'; export const LOGIN_PROXY_API = 'https://api.gdevs.io/auth'; export const LOGIN_TOKEN_PROXY_API = 'https://api.gdevs.io/authToken'; +export const GDL_COMPANION_MOD_URL = 'https://gdevs.io/GDLCompanion.jar'; +export const CURSEMETA_API_URL = `https://staging_cursemeta.dries007.net/api/v3`; +export const CURSEFORGE_MODLOADERS_API = 'https://modloaders.cursecdn.com/647622546/maven'; export const WINDOWS = 'win32'; export const LINUX = 'linux'; export const DARWIN = 'darwin'; -export const NEWS_URL = 'https://minecraft.net/en-us/api/tiles/channel/not_set,Community%20content/region/None/category/Culture,Insider,News/page/1'; +export const NEWS_URL = + 'https://minecraft.net/en-us/api/tiles/channel/not_set,Community%20content/region/None/category/Culture,Insider,News/page/1'; export const JAVA_URL = 'https://java.com/download'; -export const UPDATE_URL = 'https://raw.githubusercontent.com/gorilla-devs/GDLauncher/master/package.json'; -export const UPDATE_URL_CHECKSUMS = 'https://dl.gorilladevs.com/releases/latestChecksums.json'; +export const UPDATE_URL = + 'https://raw.githubusercontent.com/gorilla-devs/GDLauncher/master/package.json'; +export const UPDATE_URL_CHECKSUMS = + 'https://dl.gorilladevs.com/releases/latestChecksums.json'; diff --git a/app/main.dev.js b/app/main.dev.js index b89e3bdad..a29ce59e8 100644 --- a/app/main.dev.js +++ b/app/main.dev.js @@ -12,8 +12,10 @@ */ import { app, BrowserWindow, crashReporter, ipcMain } from 'electron'; import fs from 'fs'; +import minimist from 'minimist'; import log from 'electron-log'; import MenuBuilder from './menu'; +import cli from './utils/cli'; // This gets rid of this: https://github.com/electron/electron/issues/13186 process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = true; @@ -21,122 +23,126 @@ process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = true; let mainWindow = null; let splash = null; -if (process.env.NODE_ENV === 'production') { - const sourceMapSupport = require('source-map-support'); - sourceMapSupport.install(); -} - -if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') { - require('electron-debug')({ enabled: true }); - const path = require('path'); - const p = path.join(__dirname, '..', 'app', 'node_modules'); - require('module').globalPaths.push(p); -} - -const installExtensions = async () => { - const installer = require('electron-devtools-installer'); - const forceDownload = !!process.env.UPGRADE_EXTENSIONS; - const extensions = [ - 'REACT_DEVELOPER_TOOLS', - 'REDUX_DEVTOOLS' - ]; - - return Promise - .all(extensions.map(name => installer.default(installer[name], forceDownload))) - .catch(log.error); -}; - - -/** - * Add event listeners... - */ - -app.on('window-all-closed', () => { - // Respect the OSX convention of having the application in memory even - // after all windows have been closed - app.quit(); -}); - +if (minimist(process.argv.slice(1))['i']) { + cli(process.argv, () => app.quit()); +} else { + if (process.env.NODE_ENV === 'production') { + const sourceMapSupport = require('source-map-support'); + sourceMapSupport.install(); + } -app.on('ready', async () => { - if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') { - await installExtensions(); + if ( + process.env.NODE_ENV === 'development' || + process.env.DEBUG_PROD === 'true' + ) { + require('electron-debug')({ enabled: true }); + const path = require('path'); + const p = path.join(__dirname, '..', 'app', 'node_modules'); + require('module').globalPaths.push(p); } - // create a new `splash`-WindowF - splash = new BrowserWindow({ - show: true, - width: 850, - height: 600, - frame: false, - backgroundColor: '#34495e', - resizable: false - }); - splash.loadURL(`file://${__dirname}/splash.html`); + const installExtensions = async () => { + const installer = require('electron-devtools-installer'); + const forceDownload = !!process.env.UPGRADE_EXTENSIONS; + const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']; + return Promise.all( + extensions.map(name => installer.default(installer[name], forceDownload)) + ).catch(log.error); + }; - mainWindow = new BrowserWindow({ - show: false, - width: 850, - height: 600, - minHeight: 600, - minWidth: 780, - frame: false, - backgroundColor: '#34495e', - }); + /** + * Add event listeners... + */ - mainWindow.webContents.on('new-window', (e, url) => { - e.preventDefault(); - require('electron').shell.openExternal(url); + app.on('window-all-closed', () => { + // Respect the OSX convention of having the application in memory even + // after all windows have been closed + app.quit(); }); - mainWindow.loadURL(`file://${__dirname}/app.html`); - - // @TODO: Use 'ready-to-show' event - // https://github.com/electron/electron/blob/master/docs/api/browser-window.md#using-ready-to-show-event - mainWindow.webContents.on('did-finish-load', () => { - if (!mainWindow) { - throw new Error('"mainWindow" is not defined'); + app.on('ready', async () => { + if ( + process.env.NODE_ENV === 'development' || + process.env.DEBUG_PROD === 'true' + ) { + await installExtensions(); } - splash.destroy(); - // Same as for console transport - log.transports.file.level = 'silly'; - log.transports.file.format = '{h}:{i}:{s}:{ms} {text}'; - // Set approximate maximum log size in bytes. When it exceeds, - // the archived log will be saved as the log.old.log file - log.transports.file.maxSize = 5 * 1024 * 1024; - - // Write to this file, must be set before first logging - log.transports.file.file = __dirname + '/log.txt'; - - // fs.createWriteStream options, must be set before first logging - // you can find more information at - // https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options - log.transports.file.streamConfig = { flags: 'w' }; - - // set existed file stream - log.transports.file.stream = fs.createWriteStream('log.txt'); - - - mainWindow.show(); - mainWindow.focus(); - log.log(process.argv); - - }); - - ipcMain.on('open-devTools', () => { - mainWindow.webContents.openDevTools({ mode: 'undocked' }); - }); - - ipcMain.on('setProgressTaskBar', (p) => { - mainWindow.setProgressBar(p); + // create a new `splash`-WindowF + splash = new BrowserWindow({ + show: true, + width: 850, + height: 600, + frame: false, + backgroundColor: '#34495e', + resizable: false + }); + splash.loadURL(`file://${__dirname}/splash.html`); + + mainWindow = new BrowserWindow({ + show: false, + width: 850, + height: 600, + minHeight: 600, + minWidth: 780, + frame: false, + backgroundColor: '#34495e', + webPreferences: { + experimentalFeatures: true + } + }); + + mainWindow.webContents.on('new-window', (e, url) => { + e.preventDefault(); + require('electron').shell.openExternal(url); + }); + + mainWindow.loadURL(`file://${__dirname}/app.html`); + + // @TODO: Use 'ready-to-show' event + // https://github.com/electron/electron/blob/master/docs/api/browser-window.md#using-ready-to-show-event + mainWindow.webContents.on('did-finish-load', () => { + if (!mainWindow) { + throw new Error('"mainWindow" is not defined'); + } + splash.destroy(); + // Same as for console transport + log.transports.file.level = 'silly'; + log.transports.file.format = '{h}:{i}:{s}:{ms} {text}'; + + // Set approximate maximum log size in bytes. When it exceeds, + // the archived log will be saved as the log.old.log file + log.transports.file.maxSize = 5 * 1024 * 1024; + + // Write to this file, must be set before first logging + log.transports.file.file = __dirname + '/log.txt'; + + // fs.createWriteStream options, must be set before first logging + // you can find more information at + // https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options + log.transports.file.streamConfig = { flags: 'w' }; + + // set existed file stream + log.transports.file.stream = fs.createWriteStream('log.txt'); + + mainWindow.show(); + mainWindow.focus(); + }); + + ipcMain.on('open-devTools', () => { + mainWindow.webContents.openDevTools({ mode: 'undocked' }); + }); + + ipcMain.on('setProgressTaskBar', p => { + mainWindow.setProgressBar(p); + }); + + mainWindow.on('closed', () => { + mainWindow = null; + }); + + const menuBuilder = new MenuBuilder(mainWindow); + menuBuilder.buildMenu(); }); - - mainWindow.on('closed', () => { - mainWindow = null; - }); - - const menuBuilder = new MenuBuilder(mainWindow); - menuBuilder.buildMenu(); -}); +} diff --git a/app/package-lock.json b/app/package-lock.json index c14320382..fecd88141 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,5 +1,5 @@ { "name": "gdlauncher", - "version": "0.6.7", + "version": "0.6.8", "lockfileVersion": 1 } diff --git a/app/package.json b/app/package.json index f06399b50..dc3e392b6 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,7 @@ { "name": "gdlauncher", "productName": "GDLauncher", - "version": "0.6.8", + "version": "0.6.9", "description": "GDLauncher is simple, yet powerful Minecraft custom launcher with a strong focus on the user experience", "main": "./main.prod.js", "author": { diff --git a/app/reducers/instancesManager.js b/app/reducers/instancesManager.js index 58cd2de62..b8cccbca9 100644 --- a/app/reducers/instancesManager.js +++ b/app/reducers/instancesManager.js @@ -19,12 +19,15 @@ export default function instancesManager(state = initialState, action) { case `${START_INSTANCE}`: return { ...state, - startedInstances: [...state.startedInstances, action.payload] + startedInstances: [...state.startedInstances, { + name: action.payload, + pid: action.pid + }], }; case `${STOP_INSTANCE}`: return { ...state, - startedInstances: state.startedInstances.filter(el => el !== action.payload) + startedInstances: state.startedInstances.filter(el => el.name !== action.payload) }; default: return state; diff --git a/app/routes.js b/app/routes.js index ce0e41ad2..b3d99e63b 100644 --- a/app/routes.js +++ b/app/routes.js @@ -73,12 +73,11 @@ class RouteDef extends Component { return ( - {location.pathname !== '/' && location.pathname !== '/loginHelperModal' ? + {location.pathname !== '/' && location.pathname !== '/loginHelperModal' &&
-
- : null} +
} @@ -91,7 +90,7 @@ class RouteDef extends Component { {isModal ? : null} {isModal ? : null} {isModal ? : null} - {isModal ? : null} + {isModal ? : null} {isModal ? : null} ); diff --git a/app/style/theme/colors.scss b/app/style/theme/colors.scss index 0077f83d0..4a0a92a88 100644 --- a/app/style/theme/colors.scss +++ b/app/style/theme/colors.scss @@ -36,4 +36,6 @@ $btn-bg-color: $primary-color; // This is needed to import these variables in JS :export { primary: $primary-color; + red: $red; + green: $green; } \ No newline at end of file diff --git a/app/utils/MCLaunchCommand.js b/app/utils/MCLaunchCommand.js index c7f32ec69..01faebe84 100644 --- a/app/utils/MCLaunchCommand.js +++ b/app/utils/MCLaunchCommand.js @@ -3,52 +3,68 @@ import fs from 'fs'; import path from 'path'; import { promisify } from 'util'; import findJavaHome from './javaLocationFinder'; -import { PACKS_PATH, INSTANCES_PATH, WINDOWS } from '../constants'; -import { parseConfigLibraries } from './getMCFilesList'; -import store from '../localStore'; +import { PACKS_PATH, INSTANCES_PATH, WINDOWS, META_PATH } from '../constants'; +import { computeVanillaAndForgeLibraries } from './getMCFilesList'; const getStartCommand = async (packName, userData) => { - const packJson = JSON.parse(await promisify(fs.readFile)(path.join(PACKS_PATH, packName, 'config.json'))); - let forge = packJson.forgeID; + const instanceConfigJSON = JSON.parse(await promisify(fs.readFile)(path.join(PACKS_PATH, packName, 'config.json'))); + const vanillaJSON = JSON.parse(await promisify(fs.readFile)(path.join(META_PATH, 'net.minecraft', instanceConfigJSON.version, `${instanceConfigJSON.version}.json`))); + const forge = instanceConfigJSON.forgeVersion; + const forgeJSON = forge === null ? null : JSON.parse(await promisify(fs.readFile)(path.join(META_PATH, 'net.minecraftforge', forge, `${forge}.json`))); + + const javaPath = await findJavaHome(); const dosName = os.release().substr(0, 2) === 10 ? '"-Dos.name=Windows 10" -Dos.version=10.0 ' : ''; - const version = packJson.forgeID === null ? packJson.version : packJson.version; + const version = forge === null ? vanillaJSON.id : forgeJSON.versionInfo.id; // It concatenates vanilla and forge libraries. If the instance does not contain forge, it concatenates an empty array - const libs = packJson.libraries; - const Arguments = getMCArguments(packJson, packName, userData); + const libs = await computeVanillaAndForgeLibraries(vanillaJSON, forgeJSON); + const Arguments = getMCArguments(vanillaJSON, forgeJSON, packName, userData); + const mainClass = forge === null ? vanillaJSON.mainClass : forgeJSON.versionInfo.mainClass; const dividerChar = os.platform() === WINDOWS ? ';' : ':'; const completeCMD = ` "${javaPath}" ${dosName} ${os.platform() === WINDOWS ? '-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump' : ''} -Djava.library.path="${path.join(PACKS_PATH, packName, 'natives')}" --Dminecraft.client.jar="${path.join(INSTANCES_PATH, 'versions', version, `${version}.jar`)}" +-Dminecraft.client.jar="${path.join(INSTANCES_PATH, 'versions', vanillaJSON.id, `${vanillaJSON.id}.jar`)}" -cp ${libs .filter(lib => !lib.natives) .map(lib => `"${path.join(INSTANCES_PATH, 'libraries', lib.path)}"`) - .join(dividerChar)}${dividerChar}${`"${path.join(INSTANCES_PATH, 'versions', packJson.version, `${packJson.version}.jar`)}"`} -${packJson.mainClass} ${Arguments} + .join(dividerChar)}${dividerChar}${`"${path.join(INSTANCES_PATH, 'versions', vanillaJSON.id, `${vanillaJSON.id}.jar`)}"`} +${mainClass} ${Arguments} `; + + console.log(completeCMD.replace(/\n|\r/g, '')) return completeCMD.replace(/\n|\r/g, ''); }; -const getMCArguments = (json, packName, userData) => { - let Arguments = json.minecraftArguments; +const getMCArguments = (vanilla, forge, packName, userData) => { + let Arguments = ''; + if (forge !== null && forge.versionInfo.minecraftArguments) { + Arguments = forge.versionInfo.minecraftArguments; + } + else if (vanilla.minecraftArguments) { + // Up to 1.13 + Arguments = vanilla.minecraftArguments; + } else if (vanilla.arguments) { + // From 1.13 + Arguments = vanilla.arguments.game.filter(arg => typeof arg === 'string').join(' '); + } // Replaces the arguments and returns the result return Arguments .replace('${auth_player_name}', userData.displayName) .replace('${auth_session}', userData.accessToken) // Legacy check for really old versions .replace('${game_directory}', path.join(PACKS_PATH, packName)) - .replace('${game_assets}', path.join(INSTANCES_PATH, 'assets', json.assets === 'legacy' ? '/virtual/legacy' : '')) // Another check for really old versions - .replace('${version_name}', json.forgeID !== null ? json.forgeID : json.version) - .replace('${assets_root}', path.join(INSTANCES_PATH, 'assets', json.assets === 'legacy' ? '/virtual/legacy' : '')) - .replace('${assets_index_name}', json.assets) + .replace('${game_assets}', path.join(INSTANCES_PATH, 'assets', vanilla.assets === 'legacy' ? '/virtual/legacy' : '')) // Another check for really old versions + .replace('${version_name}', forge !== null ? forge.versionInfo.id : vanilla.id) + .replace('${assets_root}', path.join(INSTANCES_PATH, 'assets', vanilla.assets === 'legacy' ? '/virtual/legacy' : '')) + .replace('${assets_index_name}', vanilla.assets) .replace('${auth_uuid}', userData.uuid) .replace('${auth_access_token}', userData.accessToken) .replace('${user_properties}', "{}") .replace('${user_type}', userData.legacy ? 'legacy' : 'mojang') - .replace('${version_type}', json.type); + .replace('${version_type}', vanilla.type); } export default getStartCommand; diff --git a/app/utils/cli.js b/app/utils/cli.js new file mode 100644 index 000000000..4f6c4a6fd --- /dev/null +++ b/app/utils/cli.js @@ -0,0 +1,25 @@ +import minimist from 'minimist'; +import { exec } from 'child_process'; +import launchCommand from './MCLaunchCommand'; +import store from '../localStore'; + +const parseCLI = async (data, callback) => { + const instanceName = minimist(data.slice(1))['i']; + const auth = store.get('user'); + const start = exec( + await launchCommand(instanceName, auth), + (error, stdout, stderr) => { + if (error) { + console.error(`exec error: ${error}`); + return; + } + console.log(`stdout: ${stdout}`); + console.log(`stderr: ${stderr}`); + } + ); + start.on('exit', () => { + callback(); + }); +}; + +export default parseCLI; diff --git a/app/utils/downloader.js b/app/utils/downloader.js index 44824a4d0..db4df6076 100644 --- a/app/utils/downloader.js +++ b/app/utils/downloader.js @@ -3,10 +3,10 @@ import fss from 'fs'; import reqCall from 'request'; import path from 'path'; import assert from 'assert'; -import os from'os'; +import os from 'os'; import log from 'electron-log'; -import Promise from'bluebird'; -import request from'request-promise-native'; +import Promise from 'bluebird'; +import request from 'request-promise-native'; import { promisify } from 'util'; import { DOWNLOAD_FILE_COMPLETED } from '../actions/downloadManager'; @@ -14,9 +14,16 @@ const fs = Promise.promisifyAll(fss); export const downloadArr = async (arr, folderPath, dispatch, pack, threads = os.cpus().length) => { await Promise.map(arr, async item => { - // TODO: item.legacyPath ? path.join(folderPath, item.legacyPath) : null - // Handle legacyPaths better (own function) - await downloadFileInstance(path.join(folderPath, item.path), item.url); + let toDownload = true; + try { + await fs.accessAsync(path.join(folderPath, item.path)); + toDownload = false; + } catch (err) { + // It needs to be downloaded + } + if (toDownload) { + await downloadFileInstance(path.join(folderPath, item.path), item.url); + } dispatch({ type: DOWNLOAD_FILE_COMPLETED, payload: { pack } @@ -54,7 +61,7 @@ const downloadFileInstance = async (filename, url, legacyPath = null) => { } export const downloadFile = (filename, url, onProgress) => { - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { // Save variable to know progress var received_bytes = 0; var total_bytes = 0; @@ -63,8 +70,8 @@ export const downloadFile = (filename, url, onProgress) => { method: 'GET', uri: url, }); - - var out = fss.createWriteStream(filename); + await makeDir(path.dirname(filename)); + const out = fss.createWriteStream(filename); req.pipe(out); req.on('response', (data) => { @@ -75,7 +82,7 @@ export const downloadFile = (filename, url, onProgress) => { req.on('data', (chunk) => { // Update the received bytes received_bytes += chunk.length; - onProgress(((received_bytes * 18) / total_bytes).toFixed(1)); + onProgress(((received_bytes * 100) / total_bytes).toFixed(1)); }); req.on('end', () => { diff --git a/app/utils/getMCFilesList.js b/app/utils/getMCFilesList.js index 99ee71fe6..642148009 100644 --- a/app/utils/getMCFilesList.js +++ b/app/utils/getMCFilesList.js @@ -5,35 +5,55 @@ import _ from 'lodash'; import makeDir from 'make-dir'; import SysOS from 'os'; import { promisify } from 'util'; -import { INSTANCES_PATH, PACKS_PATH, MAVEN_REPO, MC_LIBRARIES_URL } from '../constants'; -import { pathify, arraify, arraifyModules } from '../utils/strings'; +import { + INSTANCES_PATH, + PACKS_PATH, + MAVEN_REPO, + CURSEFORGE_MODLOADERS_API, + MC_LIBRARIES_URL +} from '../constants'; +import { pathify, arraify, arraifyModules } from './strings'; const extract = promisify(require('extract-zip')); -export const extractMainJar = async (json) => { - return [{ - url: json.downloads.client.url, - path: `${json.id}/${json.id}.jar` - }]; +export const extractMainJar = async json => { + return [ + { + url: json.downloads.client.url, + path: `${json.id}/${json.id}.jar` + } + ]; }; -export const extractVanillaLibs = async (json) => { +export const extractVanillaLibs = async json => { const libs = []; - await Promise.all(json.libraries.filter(lib => !parseLibRules(lib.rules)).map(async (lib) => { - if ('artifact' in lib.downloads) { - libs.push({ - url: lib.downloads.artifact.url, - path: lib.downloads.artifact.path - }); - } - if ('classifiers' in lib.downloads && `natives-${convertOSToMCFormat(SysOS.type())}` in lib.downloads.classifiers) { - libs.push({ - url: lib.downloads.classifiers[`natives-${convertOSToMCFormat(SysOS.type())}`].url, - path: lib.downloads.classifiers[`natives-${convertOSToMCFormat(SysOS.type())}`].path, - natives: true - }); - } - })); + await Promise.all( + json.libraries.filter(lib => !parseLibRules(lib.rules)).map(async lib => { + if ('artifact' in lib.downloads) { + libs.push({ + url: lib.downloads.artifact.url, + path: lib.downloads.artifact.path + }); + } + if ( + 'classifiers' in lib.downloads && + `natives-${convertOSToMCFormat(SysOS.type())}` in + lib.downloads.classifiers + ) { + libs.push({ + url: + lib.downloads.classifiers[ + `natives-${convertOSToMCFormat(SysOS.type())}` + ].url, + path: + lib.downloads.classifiers[ + `natives-${convertOSToMCFormat(SysOS.type())}` + ].path, + natives: true + }); + } + }) + ); return libs; }; @@ -45,24 +65,40 @@ export const extractNatives = async (libs, packName) => { await makeDir(nativesPath); } - await Promise.all(libs.map(lib => - extract(path.join(INSTANCES_PATH, 'libraries', lib.path), { dir: `${nativesPath}` }))); + await Promise.all( + libs.map(lib => + extract(path.join(INSTANCES_PATH, 'libraries', lib.path), { + dir: `${nativesPath}` + }) + ) + ); }; -export const extractAssets = async (json) => { +export const extractAssets = async json => { const assets = []; const res = await axios.get(json.assetIndex.url); // It saves the json into a file on /assets/indexes/${version}.json - const assetsFile = path.join(INSTANCES_PATH, 'assets', 'indexes', `${json.assets}.json`); + const assetsFile = path.join( + INSTANCES_PATH, + 'assets', + 'indexes', + `${json.assets}.json` + ); await makeDir(path.dirname(assetsFile)); - try { await promisify(fs.access)(assetsFile) } - catch (e) { await promisify(fs.writeFile)(assetsFile, JSON.stringify(res.data)) } + try { + await promisify(fs.access)(assetsFile); + } catch (e) { + await promisify(fs.writeFile)(assetsFile, JSON.stringify(res.data)); + } // Returns the list of assets if they don't already exist - Object.keys(res.data.objects).map((asset) => { + Object.keys(res.data.objects).map(asset => { const assetCont = res.data.objects[asset]; assets.push({ - url: `http://resources.download.minecraft.net/${assetCont.hash.substring(0, 2)}/${assetCont.hash}`, + url: `http://resources.download.minecraft.net/${assetCont.hash.substring( + 0, + 2 + )}/${assetCont.hash}`, path: `objects/${assetCont.hash.substring(0, 2)}/${assetCont.hash}`, legacyPath: `virtual/legacy/${asset}` }); @@ -70,31 +106,44 @@ export const extractAssets = async (json) => { return assets; }; -export const forgeLibCalculator = async (library) => { - const baseUrl = _.has(library, 'url') ? MAVEN_REPO : MC_LIBRARIES_URL; - let completeUrl = `${baseUrl}/${arraify(library.name).join('/')}`; - const insert = (n, ins, arr) => [...arr.slice(0, n), ins, ...arr.slice(n + 1)] +export const getForgeLibraries = async forge => { + const forgeLibCalculator = async library => { + let completeUrl; + if (_.has(library, 'url')) { + completeUrl = `${CURSEFORGE_MODLOADERS_API}/${arraify( + library.name + ).join('/')}`; + } else { + completeUrl = `${MC_LIBRARIES_URL}/${arraify(library.name).join('/')}`; + } - // The url can have a "modules" path in the middle, but we do not know which ones do. We try a head request without, - // if it fails it means it needs the modules path - try { await axios.head(completeUrl) } - catch (e) { completeUrl = `${baseUrl}/${arraifyModules(library.name).join('/')}` } - return { - url: completeUrl, - path: path.join(...arraify(library.name)) + return { + url: completeUrl, + path: arraify(library.name).join('/') + }; }; -} -export const computeLibraries = async (vnl, forge) => { + let libraries = []; + libraries = await Promise.all( + forge.versionInfo.libraries + .filter( + lib => + (_.has(lib, 'clientreq') && lib.clientreq) || !_.has(lib, 'clientreq') + ) + .filter(lib => !parseLibRules(lib.rules)) + .map(async lib => forgeLibCalculator(lib)) + ); + return libraries; +}; + +export const computeVanillaAndForgeLibraries = async (vnl, forge) => { let libraries = []; if (forge !== null) { - libraries = await Promise.all(forge.libraries - .filter(lib => (_.has(lib, 'clientreq') && lib.clientreq) || (!_.has(lib, 'clientreq'))) - .map(async lib => await forgeLibCalculator(lib))); + libraries = await getForgeLibraries(forge); } libraries = libraries.concat(await extractVanillaLibs(vnl)); - - return _.uniq(libraries); + + return _.uniqBy(libraries, 'path'); }; // Returns whether the rules allow the file to be downloaded or not @@ -103,8 +152,18 @@ function parseLibRules(rules) { if (rules) { skip = true; rules.forEach(({ action, os }) => { - if (action === 'allow' && ((os && SysOS.name === convertOSToMCFormat(SysOS.type())) || !os)) { skip = false; } - if (action === 'disallow' && ((os && SysOS.name === convertOSToMCFormat(SysOS.type())) || !os)) { skip = true; } + if ( + action === 'allow' && + ((os && SysOS.name === convertOSToMCFormat(SysOS.type())) || !os) + ) { + skip = false; + } + if ( + action === 'disallow' && + ((os && SysOS.name === convertOSToMCFormat(SysOS.type())) || !os) + ) { + skip = true; + } }); } return skip; diff --git a/app/utils/javaLocationFinder.js b/app/utils/javaLocationFinder.js index a6a8a1da4..4627e2bc7 100644 --- a/app/utils/javaLocationFinder.js +++ b/app/utils/javaLocationFinder.js @@ -12,16 +12,17 @@ const findJavaHome = async () => { switch (os.platform()) { case LINUX: case DARWIN: - command = 'which java'; + command = 'which javaw'; break; case WINDOWS: - command = 'where java'; + command = 'where javaw'; break; default: break; } const { stdout } = await exec(command); - return stdout; + // This returns the first path found + return stdout.split('\n')[0]; }; export default findJavaHome; diff --git a/app/utils/numbers.js b/app/utils/numbers.js new file mode 100644 index 000000000..3520444f6 --- /dev/null +++ b/app/utils/numbers.js @@ -0,0 +1,18 @@ +export const numberToRoundedWord = number => { + // Alter numbers larger than 1k + if (number >= 1e3) { + var units = ['k', 'M', 'B', 'T']; + + // Divide to get SI Unit engineering style numbers (1e3,1e6,1e9, etc) + let unit = Math.floor((number.toFixed(0).length - 1) / 3) * 3; + // Calculate the remainder + var num = (number / ('1e' + unit)).toFixed(0); + var unitname = units[Math.floor(unit / 3) - 1]; + + // output number remainder + unitname + return num + unitname; + } + + // return formatted original number + return number.toLocaleString(); +}; diff --git a/package-lock.json b/package-lock.json index 49940976b..6d5cc4d36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gdlauncher", - "version": "0.6.7", + "version": "0.6.8", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -5812,8 +5812,7 @@ "duplexer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", - "dev": true + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" }, "duplexer2": { "version": "0.1.4", @@ -6568,9 +6567,9 @@ } }, "eslint": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.5.0.tgz", - "integrity": "sha512-m+az4vYehIJgl1Z0gb25KnFXeqQRdNreYsei1jdvkd9bB+UNQD3fsuiC2AWSQ56P+/t++kFSINZXFbfai+krOw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.6.0.tgz", + "integrity": "sha512-/eVYs9VVVboX286mBK7bbKnO1yamUy2UCRjiY6MryhQL2PaaXCExsCQ2aO83OeYRhU2eCU/FMFP+tVMoOrzNrA==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -6706,6 +6705,23 @@ "object.entries": "^1.0.4" } }, + "eslint-config-prettier": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-3.0.1.tgz", + "integrity": "sha512-vA0TB8HCx/idHXfKHYcg9J98p0Q8nkfNwNAoP7e+ywUidn6ScaFS5iqncZAHPz+/a0A/tp657ulFHFx/2JDP4Q==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + }, + "dependencies": { + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + } + } + }, "eslint-formatter-pretty": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/eslint-formatter-pretty/-/eslint-formatter-pretty-1.3.0.tgz", @@ -6853,9 +6869,9 @@ } }, "eslint-plugin-flowtype": { - "version": "2.50.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.50.0.tgz", - "integrity": "sha512-10FnBXCp8odYcpUFXGAh+Zko7py0hUWutTd3BN/R9riukH360qNPLYPR3/xV9eu9K7OJDjJrsflBnL6RwxFnlw==", + "version": "2.50.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.50.1.tgz", + "integrity": "sha512-9kRxF9hfM/O6WGZcZPszOVPd2W0TLHBtceulLTsGfwMPtiCCLnCW0ssRiOOiXyqrCA20pm1iXdXm7gQeN306zQ==", "dev": true, "requires": { "lodash": "^4.17.10" @@ -7075,6 +7091,21 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "dev": true }, + "event-stream": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.6.tgz", + "integrity": "sha512-dGXNg4F/FgVzlApjzItL+7naHutA3fDqbV/zAZqDDlXTjiMnQmZKu+prImWKszeBM5UQeGvAl3u1wBiKeDh61g==", + "requires": { + "duplexer": "^0.1.1", + "flatmap-stream": "^0.1.0", + "from": "^0.1.7", + "map-stream": "0.0.7", + "pause-stream": "^0.0.11", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through": "^2.3.8" + } + }, "eventemitter3": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", @@ -7977,6 +8008,11 @@ "write": "^0.2.1" } }, + "flatmap-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/flatmap-stream/-/flatmap-stream-0.1.0.tgz", + "integrity": "sha512-Nlic4ZRYxikqnK5rj3YoxDVKGGtUjcNDUtvQ7XsdGLZmMwdUYnXf10o1zcXtzEZTBgc6GxeRpQxV/Wu3WPIIHA==" + }, "flatten": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", @@ -8232,6 +8268,11 @@ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", "dev": true }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=" + }, "from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -8319,7 +8360,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -8340,12 +8382,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8360,17 +8404,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -8487,7 +8534,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -8499,6 +8547,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -8513,6 +8562,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -8520,12 +8570,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -8544,6 +8596,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -8624,7 +8677,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -8636,6 +8690,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -8721,7 +8776,8 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -8757,6 +8813,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -8776,6 +8833,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -8819,12 +8877,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -11910,6 +11970,11 @@ "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", "dev": true }, + "map-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", + "integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=" + }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", @@ -16591,6 +16656,14 @@ } } }, + "pause-stream": { + "version": "0.0.11", + "resolved": "http://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "requires": { + "through": "~2.3" + } + }, "pbkdf2": { "version": "3.0.16", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", @@ -17669,6 +17742,14 @@ "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" }, + "ps-tree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", + "integrity": "sha1-tCGyQUDWID8e08dplrRCewjowBQ=", + "requires": { + "event-stream": "~3.3.0" + } + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -18475,6 +18556,14 @@ "shallowequal": "^1.0.2" } }, + "react-infinite-scroller": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.1.tgz", + "integrity": "sha512-AZ8huYD8uHeDpgRilHmn8OE5qrXssH+L9ZlHWyCrPbEnY1k+kEYQHnsjtss7Whs4t/xBMh5/LcVOCzAR/loHkw==", + "requires": { + "prop-types": "^15.5.8" + } + }, "react-is": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.5.0.tgz", @@ -20566,7 +20655,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "dev": true, "requires": { "through": "2" } @@ -20774,7 +20862,6 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", "integrity": "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=", - "dev": true, "requires": { "duplexer": "~0.1.1", "through": "~2.3.4" @@ -22421,8 +22508,7 @@ "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { "version": "0.2.3", diff --git a/package.json b/package.json index f66a40b02..69726f812 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gdlauncher", "productName": "GDLauncher", - "version": "0.6.8", + "version": "0.6.9", "description": "GDLauncher is simple, yet powerful Minecraft custom launcher with a strong focus on the user experience", "scripts": { "build": "concurrently \"npm run build-main\" \"npm run build-renderer\"", @@ -156,12 +156,13 @@ "enzyme": "^3.6.0", "enzyme-adapter-react-16": "^1.5.0", "enzyme-to-json": "^3.3.4", - "eslint": "^5.5.0", + "eslint": "^5.6.0", "eslint-config-airbnb": "^17.1.0", + "eslint-config-prettier": "^3.0.1", "eslint-formatter-pretty": "^1.3.0", "eslint-import-resolver-webpack": "^0.10.1", "eslint-plugin-compat": "^2.5.1", - "eslint-plugin-flowtype": "^2.50.0", + "eslint-plugin-flowtype": "^2.50.1", "eslint-plugin-import": "^2.14.0", "eslint-plugin-jest": "^21.22.0", "eslint-plugin-jsx-a11y": "6.1.1", @@ -216,11 +217,13 @@ "less": "^3.8.1", "lodash": "^4.17.10", "make-dir": "^1.3.0", + "ps-tree": "^1.1.0", "react": "^16.5.0", "react-content-loader": "^3.1.2", "react-contextmenu": "^2.9.3", "react-dom": "^16.5.0", "react-hot-loader": "^4.3.6", + "react-infinite-scroller": "^1.2.1", "react-redux": "^5.0.7", "react-router": "^4.3.1", "react-router-dom": "^4.3.1", @@ -237,4 +240,4 @@ "node": ">=10.x", "npm": ">=6.x" } -} \ No newline at end of file +}