added initial files
@@ -0,0 +1,7 @@
|
||||
import isElectron from 'is-electron';
|
||||
import DesktopRoot from './app/desktop/DesktopRoot';
|
||||
import BrowserRoot from './app/browser/BrowserRoot';
|
||||
|
||||
const Root = isElectron() ? DesktopRoot : BrowserRoot;
|
||||
|
||||
export default Root;
|
||||
@@ -0,0 +1,3 @@
|
||||
import Root from './app/desktop/DesktopRoot';
|
||||
|
||||
export default Root;
|
||||
@@ -0,0 +1,3 @@
|
||||
import Root from './app/browser/BrowserRoot';
|
||||
|
||||
export default Root;
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
function BrowserRoot() {
|
||||
return <div>This is a browser app</div>;
|
||||
}
|
||||
|
||||
export default BrowserRoot;
|
||||
@@ -0,0 +1,207 @@
|
||||
import React, { useEffect, memo } from 'react';
|
||||
import { useDidMount } from 'rooks';
|
||||
import styled from 'styled-components';
|
||||
import { Switch } from 'react-router';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { push } from 'connected-react-router';
|
||||
import { message } from 'antd';
|
||||
import RouteWithSubRoutes from '../../common/components/RouteWithSubRoutes';
|
||||
import {
|
||||
loginWithAccessToken,
|
||||
initManifests,
|
||||
initNews,
|
||||
loginThroughNativeLauncher,
|
||||
switchToFirstValidAccount,
|
||||
checkClientToken,
|
||||
updateUserData,
|
||||
loginWithOAuthAccessToken
|
||||
} from '../../common/reducers/actions';
|
||||
import {
|
||||
load,
|
||||
received,
|
||||
requesting
|
||||
} from '../../common/reducers/loading/actions';
|
||||
import features from '../../common/reducers/loading/features';
|
||||
import GlobalStyles from '../../common/GlobalStyles';
|
||||
import RouteBackground from '../../common/components/RouteBackground';
|
||||
import ga from '../../common/utils/analytics';
|
||||
import routes from './utils/routes';
|
||||
import { _getCurrentAccount } from '../../common/utils/selectors';
|
||||
import { isLatestJavaDownloaded } from './utils';
|
||||
import SystemNavbar from './components/SystemNavbar';
|
||||
import useTrackIdle from './utils/useTrackIdle';
|
||||
import { openModal } from '../../common/reducers/modals/actions';
|
||||
import Message from './components/Message';
|
||||
import {
|
||||
ACCOUNT_MICROSOFT,
|
||||
LATEST_JAVA_VERSION
|
||||
} from '../../common/utils/constants';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
position: absolute;
|
||||
top: ${props => props.theme.sizes.height.systemNavbar}px;
|
||||
height: calc(100vh - ${props => props.theme.sizes.height.systemNavbar}px);
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.2s;
|
||||
transition-timing-function: cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
will-change: transform;
|
||||
`;
|
||||
|
||||
function DesktopRoot({ store }) {
|
||||
const dispatch = useDispatch();
|
||||
const currentAccount = useSelector(_getCurrentAccount);
|
||||
const clientToken = useSelector(state => state.app.clientToken);
|
||||
const javaPath = useSelector(state => state.settings.java.path);
|
||||
const javaLatestPath = useSelector(state => state.settings.java.pathLatest);
|
||||
const location = useSelector(state => state.router.location);
|
||||
// const modals = useSelector(state => state.modals);
|
||||
const shouldShowDiscordRPC = useSelector(state => state.settings.discordRPC);
|
||||
// const [contentStyle, setContentStyle] = useState({ transform: 'scale(1)' });
|
||||
|
||||
message.config({
|
||||
top: 45,
|
||||
maxCount: 1
|
||||
});
|
||||
|
||||
const init = async () => {
|
||||
dispatch(requesting(features.mcAuthentication));
|
||||
const userDataStatic = await ipcRenderer.invoke('getUserData');
|
||||
const userData = dispatch(updateUserData(userDataStatic));
|
||||
await dispatch(checkClientToken());
|
||||
dispatch(initNews());
|
||||
|
||||
const manifests = await dispatch(initManifests());
|
||||
|
||||
let isJava8OK = javaPath;
|
||||
let isJavaLatestOk = javaLatestPath;
|
||||
|
||||
if (!javaPath) {
|
||||
({ isValid: isJava8OK } = await isLatestJavaDownloaded(
|
||||
manifests,
|
||||
userData,
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
if (!isJavaLatestOk) {
|
||||
({ isValid: isJavaLatestOk } = await isLatestJavaDownloaded(
|
||||
manifests,
|
||||
userData,
|
||||
true,
|
||||
LATEST_JAVA_VERSION
|
||||
));
|
||||
}
|
||||
|
||||
if (!isJava8OK || !isJavaLatestOk) {
|
||||
dispatch(openModal('JavaSetup', { preventClose: true }));
|
||||
|
||||
// Super duper hacky solution to await the modal to be closed...
|
||||
// Please forgive me
|
||||
await new Promise(resolve => {
|
||||
function checkModalStillOpen(state) {
|
||||
return state.modals.find(v => v.modalType === 'JavaSetup');
|
||||
}
|
||||
|
||||
let currentValue;
|
||||
const unsubscribe = store.subscribe(() => {
|
||||
const previousValue = currentValue;
|
||||
currentValue = store.getState().modals.length;
|
||||
if (previousValue !== currentValue) {
|
||||
const stillOpen = checkModalStillOpen(store.getState());
|
||||
|
||||
if (!stillOpen) {
|
||||
unsubscribe();
|
||||
return resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development' && currentAccount) {
|
||||
dispatch(received(features.mcAuthentication));
|
||||
dispatch(push('/home'));
|
||||
} else if (currentAccount) {
|
||||
dispatch(
|
||||
load(
|
||||
features.mcAuthentication,
|
||||
dispatch(
|
||||
currentAccount.accountType === ACCOUNT_MICROSOFT
|
||||
? loginWithOAuthAccessToken()
|
||||
: loginWithAccessToken()
|
||||
)
|
||||
)
|
||||
).catch(() => {
|
||||
dispatch(switchToFirstValidAccount());
|
||||
});
|
||||
} else {
|
||||
dispatch(
|
||||
load(features.mcAuthentication, dispatch(loginThroughNativeLauncher()))
|
||||
).catch(console.error);
|
||||
}
|
||||
|
||||
if (shouldShowDiscordRPC) {
|
||||
ipcRenderer.invoke('init-discord-rpc');
|
||||
}
|
||||
|
||||
ipcRenderer.on('custom-protocol-event', (e, data) => {
|
||||
console.log(data);
|
||||
});
|
||||
};
|
||||
|
||||
// Handle already logged in account redirect
|
||||
useDidMount(init);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentAccount) {
|
||||
dispatch(push('/'));
|
||||
}
|
||||
}, [currentAccount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (clientToken && process.env.NODE_ENV !== 'development') {
|
||||
ga.setUserId(clientToken);
|
||||
ga.trackPage(location.pathname);
|
||||
}
|
||||
}, [location.pathname, clientToken]);
|
||||
|
||||
useTrackIdle(location.pathname);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (
|
||||
// modals[0] &&
|
||||
// modals[0].modalType === 'Settings' &&
|
||||
// !modals[0].unmounting
|
||||
// ) {
|
||||
// setContentStyle({ transform: 'scale(0.4)' });
|
||||
// } else {
|
||||
// setContentStyle({ transform: 'scale(1)' });
|
||||
// }
|
||||
// }, [modals]);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<SystemNavbar />
|
||||
<Message />
|
||||
<Container>
|
||||
<GlobalStyles />
|
||||
<RouteBackground />
|
||||
<Switch>
|
||||
{routes.map((route, i) => (
|
||||
<RouteWithSubRoutes key={i} {...route} /> // eslint-disable-line
|
||||
))}
|
||||
</Switch>
|
||||
</Container>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DesktopRoot);
|
||||
@@ -0,0 +1,466 @@
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import { transparentize } from 'polished';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import { promises as fs } from 'fs';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import path from 'path';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { Portal } from 'react-portal';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faPlay,
|
||||
faClock,
|
||||
faWrench,
|
||||
faFolder,
|
||||
faTrash,
|
||||
faStop,
|
||||
faBoxOpen,
|
||||
faCopy,
|
||||
faServer,
|
||||
faHammer
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import psTree from 'ps-tree';
|
||||
import { ContextMenuTrigger, ContextMenu, MenuItem } from 'react-contextmenu';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import {
|
||||
_getInstance,
|
||||
_getInstancesPath,
|
||||
_getDownloadQueue
|
||||
} from '../../../../common/utils/selectors';
|
||||
import {
|
||||
addStartedInstance,
|
||||
addToQueue,
|
||||
launchInstance
|
||||
} from '../../../../common/reducers/actions';
|
||||
import { openModal } from '../../../../common/reducers/modals/actions';
|
||||
import instanceDefaultBackground from '../../../../common/assets/instance_default.png';
|
||||
import { convertMinutesToHumanTime } from '../../../../common/utils';
|
||||
import { FABRIC, FORGE, VANILLA } from '../../../../common/utils/constants';
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
width: 180px;
|
||||
height: 100px;
|
||||
transform: ${p =>
|
||||
p.isHovered && !p.installing
|
||||
? 'scale3d(1.1, 1.1, 1.1)'
|
||||
: 'scale3d(1, 1, 1)'};
|
||||
margin-right: 20px;
|
||||
margin-top: 20px;
|
||||
transition: transform 150ms ease-in-out;
|
||||
&:hover {
|
||||
${p => (p.installing ? '' : 'transform: scale3d(1.1, 1.1, 1.1);')}
|
||||
}
|
||||
`;
|
||||
|
||||
const Spinner = keyframes`
|
||||
0% {
|
||||
transform: translate3d(-50%, -50%, 0) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(-50%, -50%, 0) rotate(360deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const PlayButtonAnimation = keyframes`
|
||||
from {
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const InstanceContainer = styled.div`
|
||||
display: flex;
|
||||
position: absolute;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.8)),
|
||||
url('${props => props.background}') center no-repeat;
|
||||
background-position: center;
|
||||
color: ${props => props.theme.palette.text.secondary};
|
||||
font-weight: 600;
|
||||
background-size: cover;
|
||||
border-radius: 4px;
|
||||
margin: 10px;
|
||||
`;
|
||||
|
||||
const HoverContainer = styled.div`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
border-radius: 4px;
|
||||
transition: opacity 150ms ease-in-out;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: ${p => (p.installing || p.isHovered ? '1' : '0')};
|
||||
backdrop-filter: blur(4px);
|
||||
will-change: opacity;
|
||||
background: ${p => transparentize(0.5, p.theme.palette.grey[800])};
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.spinner:before {
|
||||
animation: 1.5s linear infinite ${Spinner};
|
||||
animation-play-state: inherit;
|
||||
border: solid 3px transparent;
|
||||
border-bottom-color: ${props => props.theme.palette.colors.yellow};
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
will-change: transform;
|
||||
}
|
||||
`;
|
||||
|
||||
const MCVersion = styled.div`
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
font-size: 11px;
|
||||
color: ${props => props.theme.palette.text.third};
|
||||
`;
|
||||
|
||||
const TimePlayed = styled.div`
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
font-size: 11px;
|
||||
color: ${props => props.theme.palette.text.third};
|
||||
`;
|
||||
|
||||
const MenuInstanceName = styled.div`
|
||||
background: ${props => props.theme.palette.grey[800]};
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
padding: 0 20px;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
const Instance = ({ instanceName }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [background, setBackground] = useState(`${instanceDefaultBackground}`);
|
||||
const instance = useSelector(state => _getInstance(state)(instanceName));
|
||||
const downloadQueue = useSelector(_getDownloadQueue);
|
||||
const currentDownload = useSelector(state => state.currentDownload);
|
||||
const startedInstances = useSelector(state => state.startedInstances);
|
||||
const instancesPath = useSelector(_getInstancesPath);
|
||||
const isInQueue = downloadQueue[instanceName];
|
||||
|
||||
const isPlaying = startedInstances[instanceName];
|
||||
|
||||
useEffect(() => {
|
||||
if (instance.background) {
|
||||
fs.readFile(path.join(instancesPath, instanceName, instance.background))
|
||||
.then(res =>
|
||||
setBackground(`data:image/png;base64,${res.toString('base64')}`)
|
||||
)
|
||||
.catch(console.warning);
|
||||
} else {
|
||||
setBackground(`${instanceDefaultBackground}`);
|
||||
}
|
||||
}, [instance.background, instancesPath, instanceName]);
|
||||
|
||||
const startInstance = () => {
|
||||
if (isInQueue || isPlaying) return;
|
||||
dispatch(addStartedInstance({ instanceName }));
|
||||
dispatch(launchInstance(instanceName));
|
||||
};
|
||||
const openFolder = () => {
|
||||
ipcRenderer.invoke('openFolder', path.join(instancesPath, instance.name));
|
||||
};
|
||||
const openConfirmationDeleteModal = () => {
|
||||
dispatch(openModal('InstanceDeleteConfirmation', { instanceName }));
|
||||
};
|
||||
const manageInstance = () => {
|
||||
dispatch(openModal('InstanceManager', { instanceName }));
|
||||
};
|
||||
const openBisectModal = () => {
|
||||
dispatch(openModal('BisectHosting'));
|
||||
};
|
||||
const instanceExportCurseForge = () => {
|
||||
dispatch(openModal('InstanceExportCurseForge', { instanceName }));
|
||||
};
|
||||
const openDuplicateNameDialog = () => {
|
||||
dispatch(openModal('InstanceDuplicateName', { instanceName }));
|
||||
};
|
||||
const killProcess = () => {
|
||||
psTree(isPlaying.pid, (err, children) => {
|
||||
process.kill(isPlaying.pid);
|
||||
if (children?.length) {
|
||||
children.forEach(el => {
|
||||
if (el) {
|
||||
try {
|
||||
process.kill(el.PID);
|
||||
} catch {
|
||||
// No-op
|
||||
}
|
||||
try {
|
||||
process.kill(el.PPID);
|
||||
} catch {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
process.kill(isPlaying.pid);
|
||||
} catch {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenuTrigger id={instanceName}>
|
||||
<Container
|
||||
installing={isInQueue}
|
||||
onClick={startInstance}
|
||||
isHovered={isHovered || isPlaying}
|
||||
>
|
||||
<InstanceContainer installing={isInQueue} background={background}>
|
||||
<TimePlayed>
|
||||
<FontAwesomeIcon
|
||||
icon={faClock}
|
||||
css={`
|
||||
margin-right: 5px;
|
||||
`}
|
||||
/>
|
||||
|
||||
{convertMinutesToHumanTime(instance.timePlayed)}
|
||||
</TimePlayed>
|
||||
<MCVersion>{instance.loader?.mcVersion}</MCVersion>
|
||||
{instanceName}
|
||||
</InstanceContainer>
|
||||
<HoverContainer
|
||||
installing={isInQueue}
|
||||
isHovered={isHovered || isPlaying}
|
||||
>
|
||||
{currentDownload === instanceName ? (
|
||||
<>
|
||||
<div
|
||||
css={`
|
||||
font-size: 14px;
|
||||
`}
|
||||
>
|
||||
{isInQueue ? isInQueue.status : null}
|
||||
</div>
|
||||
{`${isInQueue.percentage}%`}
|
||||
<LoadingOutlined
|
||||
css={`
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
`}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{isPlaying && (
|
||||
<div
|
||||
css={`
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
`}
|
||||
>
|
||||
{isPlaying.initialized && (
|
||||
<FontAwesomeIcon
|
||||
css={`
|
||||
color: ${({ theme }) => theme.palette.colors.green};
|
||||
font-size: 27px;
|
||||
position: absolute;
|
||||
margin-left: -6px;
|
||||
margin-top: -2px;
|
||||
animation: ${PlayButtonAnimation} 0.5s
|
||||
cubic-bezier(0.75, -1.5, 0, 2.75);
|
||||
`}
|
||||
icon={faPlay}
|
||||
/>
|
||||
)}
|
||||
{!isPlaying.initialized && <div className="spinner" />}
|
||||
</div>
|
||||
)}
|
||||
{isInQueue && 'In Queue'}
|
||||
{!isInQueue && !isPlaying && <span>PLAY</span>}
|
||||
</>
|
||||
)}
|
||||
</HoverContainer>
|
||||
</Container>
|
||||
</ContextMenuTrigger>
|
||||
<Portal>
|
||||
<ContextMenu
|
||||
id={instance.name}
|
||||
onShow={() => setIsHovered(true)}
|
||||
onHide={() => setIsHovered(false)}
|
||||
>
|
||||
<MenuInstanceName>{instanceName}</MenuInstanceName>
|
||||
{isPlaying && (
|
||||
<MenuItem onClick={killProcess}>
|
||||
<FontAwesomeIcon
|
||||
icon={faStop}
|
||||
css={`
|
||||
margin-right: 10px;
|
||||
width: 25px !important;
|
||||
`}
|
||||
/>
|
||||
Kill
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem disabled={Boolean(isInQueue)} onClick={manageInstance}>
|
||||
<FontAwesomeIcon
|
||||
icon={faWrench}
|
||||
css={`
|
||||
margin-right: 10px;
|
||||
width: 25px !important;
|
||||
`}
|
||||
/>
|
||||
Manage
|
||||
</MenuItem>
|
||||
<MenuItem onClick={openFolder}>
|
||||
<FontAwesomeIcon
|
||||
icon={faFolder}
|
||||
css={`
|
||||
margin-right: 10px;
|
||||
width: 25px !important;
|
||||
`}
|
||||
/>
|
||||
Open Folder
|
||||
</MenuItem>
|
||||
|
||||
{/* // TODO - Support other export options besides curseforge forge. */}
|
||||
<MenuItem
|
||||
onClick={instanceExportCurseForge}
|
||||
disabled={
|
||||
Boolean(isInQueue) ||
|
||||
!(
|
||||
instance.loader?.loaderType === FORGE ||
|
||||
instance.loader?.loaderType === FABRIC ||
|
||||
instance.loader?.loaderType === VANILLA
|
||||
)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faBoxOpen}
|
||||
css={`
|
||||
margin-right: 10px;
|
||||
width: 25px !important;
|
||||
`}
|
||||
/>
|
||||
Export Pack
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={Boolean(isInQueue)}
|
||||
onClick={openDuplicateNameDialog}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faCopy}
|
||||
css={`
|
||||
margin-right: 10px;
|
||||
width: 25px !important;
|
||||
`}
|
||||
/>
|
||||
Duplicate
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
<MenuItem
|
||||
disabled={Boolean(isInQueue) || Boolean(isPlaying)}
|
||||
onClick={async () => {
|
||||
let manifest = null;
|
||||
try {
|
||||
manifest = JSON.parse(
|
||||
await fs.readFile(
|
||||
path.join(instancesPath, instanceName, 'manifest.json')
|
||||
)
|
||||
);
|
||||
} catch {
|
||||
// NO-OP
|
||||
}
|
||||
|
||||
dispatch(
|
||||
addToQueue(
|
||||
instanceName,
|
||||
instance.loader,
|
||||
manifest,
|
||||
instance.background,
|
||||
instance.timePlayed,
|
||||
{},
|
||||
{ isUpdate: true }
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faHammer}
|
||||
css={`
|
||||
margin-right: 10px;
|
||||
width: 25px !important;
|
||||
`}
|
||||
/>
|
||||
Repair
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={Boolean(isInQueue) || Boolean(isPlaying)}
|
||||
onClick={openConfirmationDeleteModal}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faTrash}
|
||||
css={`
|
||||
margin-right: 10px;
|
||||
width: 25px !important;
|
||||
`}
|
||||
/>
|
||||
Delete
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
<MenuItem
|
||||
onClick={openBisectModal}
|
||||
preventClose
|
||||
css={`
|
||||
border: 2px solid #04cbeb;
|
||||
border-radius: 5px;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faServer}
|
||||
css={`
|
||||
margin-right: 10px;
|
||||
width: 25px !important;
|
||||
`}
|
||||
/>
|
||||
Create Server
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Instance);
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { _getInstances } from '../../../../common/utils/selectors';
|
||||
import Instance from './Instance';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
margin-bottom: 2rem;
|
||||
`;
|
||||
|
||||
const NoInstance = styled.div`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 25px;
|
||||
margin-top: 100px;
|
||||
`;
|
||||
|
||||
const SubNoInstance = styled.div`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
margin-top: 20px;
|
||||
`;
|
||||
const sortAlphabetical = instances =>
|
||||
instances.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||
|
||||
const sortByLastPlayed = instances =>
|
||||
instances.sort((a, b) => (a.lastPlayed < b.lastPlayed ? 1 : -1));
|
||||
|
||||
const sortByMostPlayed = instances =>
|
||||
instances.sort((a, b) => (a.timePlayed < b.timePlayed ? 1 : -1));
|
||||
|
||||
const getInstances = (instances, sortOrder) => {
|
||||
// Data normalization for missing fields
|
||||
const inst = instances.map(instance => {
|
||||
return {
|
||||
...instance,
|
||||
timePlayed: instance.timePlayed || 0,
|
||||
lastPlayed: instance.lastPlayed || 0
|
||||
};
|
||||
});
|
||||
|
||||
switch (sortOrder) {
|
||||
case 0:
|
||||
return sortAlphabetical(inst);
|
||||
case 1:
|
||||
return sortByLastPlayed(inst);
|
||||
case 2:
|
||||
return sortByMostPlayed(inst);
|
||||
default:
|
||||
return inst;
|
||||
}
|
||||
};
|
||||
|
||||
const Instances = () => {
|
||||
const instanceSortOrder = useSelector(
|
||||
state => state.settings.instanceSortOrder
|
||||
);
|
||||
const instances = useSelector(_getInstances);
|
||||
const memoInstances = useMemo(
|
||||
() => getInstances(instances || [], instanceSortOrder),
|
||||
[instances, instanceSortOrder]
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{memoInstances.length > 0 ? (
|
||||
memoInstances.map(i => <Instance key={i.name} instanceName={i.name} />)
|
||||
) : (
|
||||
<NoInstance>
|
||||
No Instance has been installed
|
||||
<SubNoInstance>
|
||||
Click on the icon in the bottom left corner to add new instances
|
||||
</SubNoInstance>
|
||||
</NoInstance>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Instances);
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
|
||||
const MessageContainer = styled.div`
|
||||
width: 280px;
|
||||
height: 30px;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
background: ${props => props.theme.palette.grey[800]};
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
z-index: 10000000;
|
||||
border-radius: 5px;
|
||||
transition: opacity 200ms ease-in-out;
|
||||
opacity: ${props => props.visible};
|
||||
visibility: ${props => (props.visible ? 'visible' : 'hidden')};
|
||||
`;
|
||||
|
||||
const Message = () => {
|
||||
const currentState = useSelector(state => state.message);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentState) {
|
||||
setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setVisible(true);
|
||||
if (currentState.duration !== 0) {
|
||||
setTimeout(() => {
|
||||
setVisible(false);
|
||||
}, currentState.duration);
|
||||
}
|
||||
}, [currentState]);
|
||||
|
||||
return (
|
||||
<MessageContainer visible={visible ? 1 : 0}>
|
||||
{currentState?.content} <LoadingOutlined />
|
||||
</MessageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Message);
|
||||
@@ -0,0 +1,252 @@
|
||||
import React, { useState, useEffect, useRef, useContext } from 'react';
|
||||
import ContentLoader from 'react-content-loader';
|
||||
import styled, { ThemeContext } from 'styled-components';
|
||||
import { shell } from 'electron';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
const Carousel = styled.div`
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
overflow: hidden;
|
||||
border-radius: ${props => props.theme.shape.borderRadius};
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
const ImageSlider = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
object-fit: covert;
|
||||
overflow: hidden;
|
||||
border-radius: ${props => props.theme.shape.borderRadius};
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin: 0 auto 0 auto;
|
||||
width: 1000%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
transform: translate(${props => `${props.currentImageIndex}px`});
|
||||
transition: transform 0.3s ease-in-out;
|
||||
`;
|
||||
|
||||
// padding: 200px;
|
||||
const ImageSlide = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: ${props => props.theme.shape.borderRadius};
|
||||
background-image: url('${props => (props.image ? props.image : null)}');
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
z-index: -1;
|
||||
`;
|
||||
|
||||
const Slide = styled.div`
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
border-radius: 2px;
|
||||
z-index: 0;
|
||||
&:hover ${ImageSlide} {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
`;
|
||||
|
||||
const Gradient = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: ${props => props.theme.shape.borderRadius};
|
||||
background-image: linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 0, 0, 1) 0%,
|
||||
rgba(165, 165, 165, 0) 80%
|
||||
);
|
||||
opacity: 0.99;
|
||||
z-index: 1;
|
||||
&&:hover {
|
||||
}
|
||||
`;
|
||||
|
||||
const Select = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
top: 160px;
|
||||
left: 50%;
|
||||
margin-left: -100px;
|
||||
padding: 0;
|
||||
width: 200px;
|
||||
height: 5px;
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
const SelectElement = styled.div`
|
||||
width: 16px;
|
||||
height: 5px;
|
||||
flex: 1;
|
||||
margin: 0 2px 0 2px;
|
||||
cursor: pointer;
|
||||
background: ${props => props.theme.palette.grey[50]};
|
||||
opacity: 0.6;
|
||||
transition: flex-grow 0.2s ease-in-out;
|
||||
border-radius: 2px;
|
||||
&:hover {
|
||||
margin: 0 2px 0 2px;
|
||||
flex-grow: 2;
|
||||
background: ${props => props.theme.palette.grey[50]};
|
||||
opacity: 0.79;
|
||||
vertical-align: middle;
|
||||
}
|
||||
&:active {
|
||||
margin: 0 2px 0 2px;
|
||||
flex-grow: 2;
|
||||
background: ${props => props.theme.palette.grey[50]};
|
||||
opacity: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&:nth-child(${props => props.currentImageIndex}) {
|
||||
margin: 0 2px 0 2px;
|
||||
flex-grow: 2;
|
||||
background: ${props => props.theme.palette.grey[50]};
|
||||
opacity: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
left: 15px;
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
const SubTitle = styled.p`
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: 15px;
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
function openNews(e, inf) {
|
||||
e.preventDefault();
|
||||
shell.openExternal(inf.url);
|
||||
}
|
||||
|
||||
function ImageList({ currentImageIndex, news }) {
|
||||
const listImages = news.map(inf => (
|
||||
<Slide key={inf.guid} onClick={e => openNews(e, inf)}>
|
||||
<Title>{inf.title}</Title>
|
||||
<SubTitle>{inf.description}</SubTitle>
|
||||
<Gradient />
|
||||
<ImageSlide image={inf.image} />
|
||||
</Slide>
|
||||
));
|
||||
|
||||
return (
|
||||
<ImageSlider currentImageIndex={-1000 * currentImageIndex}>
|
||||
{listImages}
|
||||
</ImageSlider>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectNews(props) {
|
||||
const { news } = props;
|
||||
const { setCurrentImageIndex } = props;
|
||||
const selectElementList = news.map((inf, i) => (
|
||||
<SelectElement
|
||||
key={inf.url}
|
||||
onClick={() => setCurrentImageIndex(i)}
|
||||
currentImageIndex={props.currentImageIndex + 1}
|
||||
/>
|
||||
));
|
||||
|
||||
return <Select>{selectElementList}</Select>;
|
||||
}
|
||||
|
||||
function useInterval(callback, delay) {
|
||||
const savedCallback = useRef();
|
||||
|
||||
// Remember the latest function.
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
// Set up the interval.
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
savedCallback.current();
|
||||
}
|
||||
if (delay !== null) {
|
||||
const id = setInterval(tick, delay);
|
||||
return () => clearInterval(id);
|
||||
}
|
||||
}, [delay]);
|
||||
}
|
||||
|
||||
function News({ style, news }) {
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||
const ContextTheme = useContext(ThemeContext);
|
||||
const showNews = useSelector(state => state.settings.showNews);
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
if (currentImageIndex < 9) {
|
||||
setCurrentImageIndex(currentImageIndex + 1);
|
||||
} else setCurrentImageIndex(0);
|
||||
},
|
||||
showNews ? 5000 : null
|
||||
);
|
||||
|
||||
if (!showNews) return null;
|
||||
return news.length !== 0 ? (
|
||||
<Carousel style={style}>
|
||||
<SelectNews
|
||||
news={news}
|
||||
setCurrentImageIndex={setCurrentImageIndex}
|
||||
currentImageIndex={currentImageIndex}
|
||||
/>
|
||||
<ImageList news={news} currentImageIndex={currentImageIndex} />
|
||||
</Carousel>
|
||||
) : (
|
||||
<ContentLoader
|
||||
speed={2}
|
||||
width={1000}
|
||||
height={180}
|
||||
viewBox="0 0 1000 180"
|
||||
foregroundColor={ContextTheme.palette.grey[900]}
|
||||
backgroundColor={ContextTheme.palette.grey[800]}
|
||||
title={false}
|
||||
>
|
||||
{/* <rect x="0" y="0" rx="0" ry="0" width="1000" height="1080" /> */}
|
||||
<rect width="20" height="180" />
|
||||
<rect x="980" width="20" height="180" />
|
||||
<rect
|
||||
x="490"
|
||||
y="-490"
|
||||
transform="matrix(-1.836970e-16 1 -1 -1.836970e-16 510 -490)"
|
||||
width="20"
|
||||
height="1000"
|
||||
/>
|
||||
<rect
|
||||
x="490"
|
||||
y="-330"
|
||||
transform="matrix(-1.836970e-16 1 -1 -1.836970e-16 670 -330)"
|
||||
width="20"
|
||||
height="1000"
|
||||
/>
|
||||
|
||||
<rect x="40.5" y="100" width="304" height="14.4" />
|
||||
<rect x="40.5" y="125.6" width="304" height="14.4" />
|
||||
</ContentLoader>
|
||||
);
|
||||
}
|
||||
|
||||
export default News;
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Options = styled.div`
|
||||
position: relative;
|
||||
padding: 1px 18px 17px;
|
||||
margin: 0px -20px 20px;
|
||||
border-bottom: 2px solid rgb(238, 238, 238);
|
||||
`;
|
||||
|
||||
const Overview = () => {
|
||||
return (
|
||||
<>
|
||||
<Options>
|
||||
<div>Bold</div>
|
||||
</Options>
|
||||
<textarea placeholder="Type your notes here!!" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Overview;
|
||||
@@ -0,0 +1,437 @@
|
||||
import React, { useEffect, useState, memo } from 'react';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import styled from 'styled-components';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faWindowMinimize,
|
||||
faWindowMaximize,
|
||||
faWindowRestore,
|
||||
faTimes,
|
||||
faTerminal,
|
||||
faCog,
|
||||
faDownload
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { openModal } from '../../../common/reducers/modals/actions';
|
||||
import {
|
||||
checkForPortableUpdates,
|
||||
updateUpdateAvailable,
|
||||
isNewVersionAvailable
|
||||
} from '../../../common/reducers/actions';
|
||||
import BisectHosting from '../../../ui/BisectHosting';
|
||||
import Logo from '../../../ui/Logo';
|
||||
import ga from '../../../common/utils/analytics';
|
||||
|
||||
const isOsx = process.platform === 'darwin';
|
||||
const isLinux = process.platform === 'linux';
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
const DevtoolButton = () => {
|
||||
const openDevTools = () => {
|
||||
ipcRenderer.invoke('open-devtools');
|
||||
};
|
||||
|
||||
return (
|
||||
<TerminalButton
|
||||
css={`
|
||||
margin: 0 10px;
|
||||
`}
|
||||
onClick={openDevTools}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTerminal} />
|
||||
</TerminalButton>
|
||||
);
|
||||
};
|
||||
|
||||
const SettingsButton = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const modals = useSelector(state => state.modals);
|
||||
|
||||
const areSettingsOpen = modals.find(
|
||||
v => v.modalType === 'Settings' && !v.unmounting
|
||||
);
|
||||
|
||||
return (
|
||||
<TerminalButton
|
||||
areSettingsOpen={areSettingsOpen}
|
||||
css={`
|
||||
margin: 0 20px 0 10px;
|
||||
${props =>
|
||||
props.areSettingsOpen
|
||||
? `background: ${props.theme.palette.grey[700]};`
|
||||
: null}
|
||||
`}
|
||||
onClick={() => {
|
||||
dispatch(openModal('Settings'));
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</TerminalButton>
|
||||
);
|
||||
};
|
||||
|
||||
const UpdateButton = ({ isAppImage }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<TerminalButton
|
||||
onClick={() => {
|
||||
if (isAppImage || isWindows) {
|
||||
ipcRenderer.invoke('installUpdateAndQuitOrRestart');
|
||||
} else {
|
||||
dispatch(openModal('AutoUpdatesNotAvailable'));
|
||||
}
|
||||
}}
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.green};
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</TerminalButton>
|
||||
);
|
||||
};
|
||||
|
||||
const SystemNavbar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
const isUpdateAvailable = useSelector(state => state.updateAvailable);
|
||||
const location = useSelector(state => state.router.location.pathname);
|
||||
const [isAppImage, setIsAppImage] = useState(false);
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
const isAppImageVar = await ipcRenderer.invoke('isAppImage');
|
||||
setIsAppImage(isAppImageVar);
|
||||
if (
|
||||
process.env.REACT_APP_RELEASE_TYPE === 'setup' &&
|
||||
(isAppImageVar || process.platform === 'win32')
|
||||
) {
|
||||
ipcRenderer.invoke('checkForUpdates');
|
||||
ipcRenderer.on('updateAvailable', () => {
|
||||
dispatch(updateUpdateAvailable(true));
|
||||
});
|
||||
} else if (
|
||||
process.platform === 'win32' &&
|
||||
process.env.REACT_APP_RELEASE_TYPE !== 'setup'
|
||||
) {
|
||||
dispatch(checkForPortableUpdates())
|
||||
.then(v => dispatch(updateUpdateAvailable(Boolean(v))))
|
||||
.catch(console.error);
|
||||
} else {
|
||||
isNewVersionAvailable()
|
||||
.then(v => dispatch(updateUpdateAvailable(Boolean(v))))
|
||||
.catch(console.error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
ipcRenderer
|
||||
.invoke('getIsWindowMaximized')
|
||||
.then(setIsMaximized)
|
||||
.catch(console.error);
|
||||
ipcRenderer.on('window-maximized', () => {
|
||||
setIsMaximized(true);
|
||||
});
|
||||
ipcRenderer.on('window-minimized', () => {
|
||||
setIsMaximized(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'development') return;
|
||||
setTimeout(() => {
|
||||
checkForUpdates();
|
||||
setInterval(() => {
|
||||
checkForUpdates();
|
||||
}, 600000);
|
||||
}, 1500);
|
||||
}, []);
|
||||
|
||||
const quitApp = () => {
|
||||
if (isUpdateAvailable && (isAppImage || !isLinux)) {
|
||||
ipcRenderer.invoke('installUpdateAndQuitOrRestart', true);
|
||||
} else {
|
||||
ipcRenderer.invoke('quit-app');
|
||||
}
|
||||
};
|
||||
|
||||
const isLocation = loc => {
|
||||
if (loc === location) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<MainContainer
|
||||
onDoubleClick={() => {
|
||||
if (process.platform === 'darwin') {
|
||||
ipcRenderer.invoke('min-max-window');
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!isOsx && (
|
||||
<>
|
||||
<div
|
||||
css={`
|
||||
cursor: auto !important;
|
||||
-webkit-app-region: drag;
|
||||
margin-left: 10px;
|
||||
`}
|
||||
>
|
||||
<a
|
||||
href="https://gdevs.io/"
|
||||
rel="noopener noreferrer"
|
||||
css={`
|
||||
margin-top: 5px;
|
||||
margin-right: 5px;
|
||||
-webkit-app-region: no-drag;
|
||||
`}
|
||||
>
|
||||
<Logo size={35} pointerCursor />
|
||||
</a>
|
||||
<DevtoolButton />
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
white-space: nowrap;
|
||||
`}
|
||||
>
|
||||
Partnered with
|
||||
</div>
|
||||
<BisectHosting
|
||||
showPointerCursor
|
||||
onClick={() => {
|
||||
ga.sendCustomEvent('BHAdViewNavbar');
|
||||
dispatch(openModal('BisectHosting'));
|
||||
}}
|
||||
/>
|
||||
{/* <PulsatingCircle /> */}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Container os={isOsx}>
|
||||
{!isOsx ? (
|
||||
<>
|
||||
{isUpdateAvailable && <UpdateButton isAppImage={isAppImage} />}
|
||||
{!isLocation('/') && !isLocation('/onboarding') && (
|
||||
<SettingsButton />
|
||||
)}
|
||||
<div
|
||||
onClick={() => ipcRenderer.invoke('minimize-window')}
|
||||
css={`
|
||||
-webkit-app-region: no-drag;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faWindowMinimize} />
|
||||
</div>
|
||||
<div
|
||||
onClick={() => ipcRenderer.invoke('min-max-window')}
|
||||
css={`
|
||||
-webkit-app-region: no-drag;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={isMaximized ? faWindowRestore : faWindowMaximize}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
font-size: 18px;
|
||||
-webkit-app-region: no-drag;
|
||||
`}
|
||||
onClick={quitApp}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
css={`
|
||||
font-size: 18px;
|
||||
-webkit-app-region: no-drag;
|
||||
`}
|
||||
onClick={quitApp}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} />
|
||||
</div>
|
||||
<div
|
||||
onClick={() => ipcRenderer.invoke('min-max-window')}
|
||||
css={`
|
||||
-webkit-app-region: no-drag;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={isMaximized ? faWindowRestore : faWindowMaximize}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => ipcRenderer.invoke('minimize-window')}
|
||||
css={`
|
||||
-webkit-app-region: no-drag;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faWindowMinimize} />
|
||||
</div>
|
||||
{!isLocation('/') && !isLocation('/onboarding') && (
|
||||
<SettingsButton />
|
||||
)}
|
||||
{isUpdateAvailable && <UpdateButton isAppImage={isAppImage} />}
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
{isOsx && (
|
||||
<>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
Partnered with
|
||||
<BisectHosting
|
||||
showPointerCursor
|
||||
onClick={() => dispatch(openModal('BisectHosting'))}
|
||||
/>
|
||||
{/* <PulsatingCircle /> */}
|
||||
</div>
|
||||
<div>
|
||||
<DevtoolButton />
|
||||
<a
|
||||
href="https://gdevs.io/"
|
||||
rel="noopener noreferrer"
|
||||
css={`
|
||||
margin-top: 5px;
|
||||
margin-right: 5px;
|
||||
-webkit-app-region: no-drag;
|
||||
`}
|
||||
>
|
||||
<Logo size={35} pointerCursor />
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</MainContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SystemNavbar);
|
||||
|
||||
const MainContainer = styled.div`
|
||||
width: 100%;
|
||||
height: ${({ theme }) => theme.sizes.height.systemNavbar}px;
|
||||
background: ${({ theme }) => theme.palette.grey[900]};
|
||||
-webkit-app-region: drag;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
z-index: 100000;
|
||||
& > * {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s ease-in-out;
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
-webkit-app-region: drag;
|
||||
& > * {
|
||||
width: ${({ theme }) => theme.sizes.height.systemNavbar}px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.palette.grey[700]};
|
||||
}
|
||||
&:active {
|
||||
background: ${({ theme }) => theme.palette.grey[600]};
|
||||
}
|
||||
}
|
||||
${props => (props.os ? '& > *:first-child' : '& > *:last-child')} {
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.palette.colors.red};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const TerminalButton = styled.div`
|
||||
transition: background 0.1s ease-in-out;
|
||||
display: flex;
|
||||
-webkit-app-region: no-drag;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
width: ${({ theme }) => theme.sizes.height.systemNavbar}px;
|
||||
height: 100%;
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.palette.grey[700]};
|
||||
}
|
||||
&:active {
|
||||
background: ${({ theme }) => theme.palette.grey[600]};
|
||||
}
|
||||
`;
|
||||
|
||||
// const opacityPulse = keyframes`
|
||||
// 0% {
|
||||
// -webkit-box-shadow: 0 0 0 0 rgba(39, 174, 96, 0.4);
|
||||
// }
|
||||
// 70% {
|
||||
// -webkit-box-shadow: 0 0 0 10px rgba(39, 174, 96, 0);
|
||||
// }
|
||||
// 100% {
|
||||
// -webkit-box-shadow: 0 0 0 0 rgba(39, 174, 96, 0);
|
||||
// }
|
||||
// `;
|
||||
// const PulsingCircleInner = styled.div`
|
||||
// animation: ${opacityPulse} 1s ease-out;
|
||||
// animation-delay: ${props => props.delay};
|
||||
// animation-iteration-count: infinite;
|
||||
// background: ${props => props.theme.palette.colors.green};
|
||||
// opacity: ${props => props.opacity || 1};
|
||||
// border-radius: 50%;
|
||||
// height: ${props => props.height};
|
||||
// width: ${props => props.width};
|
||||
// position: absolute;
|
||||
// display: inline-block;
|
||||
// text-align: center;
|
||||
// `;
|
||||
// const PulsingCircleContainer = styled.div`
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
// position: relative;
|
||||
// margin-left: 1rem;
|
||||
// margin-right: 1.5rem;
|
||||
// top: -1px;
|
||||
// `;
|
||||
|
||||
// function PulsatingCircle() {
|
||||
// return (
|
||||
// <PulsingCircleContainer>
|
||||
// <PulsingCircleInner delay="0s" height="17px" width="17px" opacity={0.1} />
|
||||
// <PulsingCircleInner
|
||||
// delay="0.5s"
|
||||
// height="12.75"
|
||||
// width="12.75"
|
||||
// opacity={0.15}
|
||||
// />
|
||||
// <PulsingCircleInner delay="1s" height="8.5px" width="8.5px" />
|
||||
// </PulsingCircleContainer>
|
||||
// );
|
||||
// }
|
||||
@@ -0,0 +1,68 @@
|
||||
// This class is used to create a queue of operations when working with instances configs, to avoid data corruption.
|
||||
// Each instance will have its own queue. This can also be used in more generic situations where a queue structure is needed
|
||||
class PromiseQueue {
|
||||
constructor() {
|
||||
this.queue = [];
|
||||
this.isPending = false;
|
||||
this.listeners = {};
|
||||
}
|
||||
|
||||
add(promise) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.queue.push({
|
||||
promise,
|
||||
resolve,
|
||||
reject
|
||||
});
|
||||
this.execute();
|
||||
});
|
||||
}
|
||||
|
||||
on(eventName, handler) {
|
||||
switch (eventName) {
|
||||
case 'executed':
|
||||
this.listeners.executed = () => handler(this.queue.length + 1);
|
||||
break;
|
||||
case 'start':
|
||||
this.listeners.start = () => handler(this.queue.length + 1);
|
||||
break;
|
||||
case 'end':
|
||||
this.listeners.end = () => handler();
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const startHandler = this.listeners.start;
|
||||
if (this.isPending) return false;
|
||||
if (startHandler) {
|
||||
setTimeout(startHandler, 0);
|
||||
}
|
||||
while (this.queue[0]) {
|
||||
const item = this.queue.shift();
|
||||
this.isPending = true;
|
||||
try {
|
||||
// eslint-disable-next-line
|
||||
const value = await item.promise();
|
||||
const executedHandler = this.listeners.executed;
|
||||
if (executedHandler) {
|
||||
setTimeout(executedHandler, 0);
|
||||
}
|
||||
item.resolve(value);
|
||||
} catch (e) {
|
||||
item.reject(e);
|
||||
}
|
||||
this.isPending = false;
|
||||
}
|
||||
const endHandler = this.listeners.end;
|
||||
if (endHandler) {
|
||||
setTimeout(endHandler, 0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default PromiseQueue;
|
||||
@@ -0,0 +1,13 @@
|
||||
import fs from 'fs';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const computeFileHash = (filePath, algorithm = 'sha1') =>
|
||||
new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash(algorithm);
|
||||
fs.createReadStream(filePath)
|
||||
.on('data', data => hash.update(data))
|
||||
.on('end', () => resolve(hash.digest('hex')))
|
||||
.on('error', reject);
|
||||
});
|
||||
|
||||
export default computeFileHash;
|
||||
@@ -0,0 +1,29 @@
|
||||
import path from 'path';
|
||||
import { platform, homedir } from 'os';
|
||||
|
||||
export const WINDOWS = 'win32';
|
||||
export const LINUX = 'linux';
|
||||
export const DARWIN = 'darwin';
|
||||
export const DESKTOP_PATH = path.join(homedir(), 'Desktop');
|
||||
export const CLASSPATH_DIVIDER_CHAR = platform() === WINDOWS ? ';' : ':';
|
||||
export const DEFAULT_JAVA_ARGS = `${
|
||||
platform() === WINDOWS
|
||||
? '-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump'
|
||||
: ''
|
||||
} -Xms256m`;
|
||||
export const REQUIRED_JAVA_ARGS =
|
||||
'-Dfml.ignorePatchDiscrepancies=true -Dfml.ignoreInvalidMinecraftCertificates=true';
|
||||
export const DEFAULT_MEMORY = 4096;
|
||||
|
||||
export const resolutionPresets = [
|
||||
'854x480',
|
||||
'800x600',
|
||||
'1024x768',
|
||||
'1280x1024',
|
||||
'1366x768',
|
||||
'1600x900',
|
||||
'1920x1080',
|
||||
'2560x1440',
|
||||
'3440x1440',
|
||||
'3440x1500'
|
||||
];
|
||||
@@ -0,0 +1,161 @@
|
||||
import makeDir from 'make-dir';
|
||||
import fss from 'fs';
|
||||
import axios from 'axios';
|
||||
import pMap from 'p-map';
|
||||
import path from 'path';
|
||||
import adapter from 'axios/lib/adapters/http';
|
||||
import computeFileHash from './computeFileHash';
|
||||
|
||||
const fs = fss.promises;
|
||||
|
||||
function getUri(url) {
|
||||
return new URL(url).href;
|
||||
}
|
||||
|
||||
export const downloadInstanceFiles = async (
|
||||
arr,
|
||||
updatePercentage,
|
||||
threads = 4
|
||||
) => {
|
||||
let downloaded = 0;
|
||||
await pMap(
|
||||
arr,
|
||||
async item => {
|
||||
let counter = 0;
|
||||
let res = false;
|
||||
if (!item.path || !item.url) {
|
||||
console.warn('Skipping', item);
|
||||
return;
|
||||
}
|
||||
do {
|
||||
counter += 1;
|
||||
if (counter !== 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
|
||||
try {
|
||||
res = await downloadFileInstance(
|
||||
item.path,
|
||||
item.url,
|
||||
item.sha1,
|
||||
item.legacyPath
|
||||
);
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
} while (!res && counter < 3);
|
||||
downloaded += 1;
|
||||
if (
|
||||
updatePercentage &&
|
||||
(downloaded % 5 === 0 || downloaded === arr.length)
|
||||
) {
|
||||
updatePercentage(downloaded);
|
||||
}
|
||||
},
|
||||
{ concurrency: threads }
|
||||
);
|
||||
};
|
||||
|
||||
const downloadFileInstance = async (fileName, url, sha1, legacyPath) => {
|
||||
let encodedUrl;
|
||||
try {
|
||||
const filePath = path.dirname(fileName);
|
||||
try {
|
||||
await fs.access(fileName);
|
||||
if (legacyPath) await fs.access(legacyPath);
|
||||
const checksum = await computeFileHash(fileName);
|
||||
const legacyChecksum = legacyPath && (await computeFileHash(legacyPath));
|
||||
if (checksum === sha1 && (!legacyPath || legacyChecksum === sha1)) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
await makeDir(filePath);
|
||||
if (legacyPath) await makeDir(path.dirname(legacyPath));
|
||||
}
|
||||
|
||||
encodedUrl = getUri(url);
|
||||
|
||||
const { data } = await axios.get(encodedUrl, {
|
||||
responseType: 'stream',
|
||||
responseEncoding: null,
|
||||
adapter,
|
||||
timeout: 60000 * 20
|
||||
});
|
||||
|
||||
const wStream = fss.createWriteStream(fileName, {
|
||||
encoding: null
|
||||
});
|
||||
|
||||
data.pipe(wStream);
|
||||
let wStreamLegacy;
|
||||
if (legacyPath) {
|
||||
wStreamLegacy = fss.createWriteStream(legacyPath, {
|
||||
encoding: null
|
||||
});
|
||||
data.pipe(wStreamLegacy);
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
data.on('error', err => {
|
||||
console.error(err);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
data.on('end', () => {
|
||||
wStream.end();
|
||||
wStream.close();
|
||||
if (legacyPath) {
|
||||
wStreamLegacy.end();
|
||||
wStreamLegacy.close();
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Error while downloading <${url} | ${encodedUrl}> to <${fileName}> --> ${e.message}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadFile = async (fileName, url, onProgress) => {
|
||||
await makeDir(path.dirname(fileName));
|
||||
|
||||
const encodedUrl = getUri(url);
|
||||
|
||||
const { data, headers } = await axios.get(encodedUrl, {
|
||||
responseType: 'stream',
|
||||
responseEncoding: null,
|
||||
adapter,
|
||||
timeout: 60000 * 20
|
||||
});
|
||||
|
||||
const out = fss.createWriteStream(fileName, { encoding: null });
|
||||
data.pipe(out);
|
||||
|
||||
// Save variable to know progress
|
||||
let receivedBytes = 0;
|
||||
const totalBytes = parseInt(headers['content-length'], 10);
|
||||
|
||||
data.on('data', chunk => {
|
||||
// Update the received bytes
|
||||
receivedBytes += chunk.length;
|
||||
if (onProgress) {
|
||||
onProgress(parseInt(((receivedBytes * 100) / totalBytes).toFixed(1), 10));
|
||||
}
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
data.on('end', () => {
|
||||
out.end();
|
||||
out.close();
|
||||
resolve();
|
||||
});
|
||||
|
||||
data.on('error', () => {
|
||||
reject();
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
const fmlLibsMapping = {};
|
||||
// 1.3.*
|
||||
const libs13 = [
|
||||
['argo-2.25.jar', 'bb672829fde76cb163004752b86b0484bd0a7f4b', false],
|
||||
['guava-12.0.1.jar', 'b8e78b9af7bf45900e14c6f958486b6ca682195f', false],
|
||||
['asm-all-4.0.jar', '98308890597acb64047f7e896638e0d98753ae82', false]
|
||||
];
|
||||
|
||||
fmlLibsMapping['1.3.2'] = libs13;
|
||||
|
||||
// 1.4.*
|
||||
const libs14 = [
|
||||
['argo-2.25.jar', 'bb672829fde76cb163004752b86b0484bd0a7f4b', false],
|
||||
['guava-12.0.1.jar', 'b8e78b9af7bf45900e14c6f958486b6ca682195f', false],
|
||||
['asm-all-4.0.jar', '98308890597acb64047f7e896638e0d98753ae82', false],
|
||||
['bcprov-jdk15on-147.jar', 'b6f5d9926b0afbde9f4dbe3db88c5247be7794bb', false]
|
||||
];
|
||||
|
||||
fmlLibsMapping['1.4'] = libs14;
|
||||
fmlLibsMapping['1.4.1'] = libs14;
|
||||
fmlLibsMapping['1.4.2'] = libs14;
|
||||
fmlLibsMapping['1.4.3'] = libs14;
|
||||
fmlLibsMapping['1.4.4'] = libs14;
|
||||
fmlLibsMapping['1.4.5'] = libs14;
|
||||
fmlLibsMapping['1.4.6'] = libs14;
|
||||
fmlLibsMapping['1.4.7'] = libs14;
|
||||
|
||||
// 1.5
|
||||
fmlLibsMapping['1.5'] = [
|
||||
['argo-small-3.2.jar', '58912ea2858d168c50781f956fa5b59f0f7c6b51', false],
|
||||
['guava-14.0-rc3.jar', '931ae21fa8014c3ce686aaa621eae565fefb1a6a', false],
|
||||
['asm-all-4.1.jar', '054986e962b88d8660ae4566475658469595ef58', false],
|
||||
['bcprov-jdk15on-148.jar', '960dea7c9181ba0b17e8bab0c06a43f0a5f04e65', true],
|
||||
[
|
||||
'deobfuscation_data_1.5.zip',
|
||||
'5f7c142d53776f16304c0bbe10542014abad6af8',
|
||||
false
|
||||
],
|
||||
['scala-library.jar', '458d046151ad179c85429ed7420ffb1eaf6ddf85', true]
|
||||
];
|
||||
|
||||
// 1.5.1
|
||||
fmlLibsMapping['1.5.1'] = [
|
||||
['argo-small-3.2.jar', '58912ea2858d168c50781f956fa5b59f0f7c6b51', false],
|
||||
['guava-14.0-rc3.jar', '931ae21fa8014c3ce686aaa621eae565fefb1a6a', false],
|
||||
['asm-all-4.1.jar', '054986e962b88d8660ae4566475658469595ef58', false],
|
||||
['bcprov-jdk15on-148.jar', '960dea7c9181ba0b17e8bab0c06a43f0a5f04e65', true],
|
||||
[
|
||||
'deobfuscation_data_1.5.1.zip',
|
||||
'22e221a0d89516c1f721d6cab056a7e37471d0a6',
|
||||
false
|
||||
],
|
||||
['scala-library.jar', '458d046151ad179c85429ed7420ffb1eaf6ddf85', true]
|
||||
];
|
||||
|
||||
// 1.5.2
|
||||
fmlLibsMapping['1.5.2'] = [
|
||||
['argo-small-3.2.jar', '58912ea2858d168c50781f956fa5b59f0f7c6b51', false],
|
||||
['guava-14.0-rc3.jar', '931ae21fa8014c3ce686aaa621eae565fefb1a6a', false],
|
||||
['asm-all-4.1.jar', '054986e962b88d8660ae4566475658469595ef58', false],
|
||||
['bcprov-jdk15on-148.jar', '960dea7c9181ba0b17e8bab0c06a43f0a5f04e65', true],
|
||||
[
|
||||
'deobfuscation_data_1.5.2.zip',
|
||||
'446e55cd986582c70fcf12cb27bc00114c5adfd9',
|
||||
false
|
||||
],
|
||||
['scala-library.jar', '458d046151ad179c85429ed7420ffb1eaf6ddf85', true]
|
||||
];
|
||||
|
||||
export default fmlLibsMapping;
|
||||
@@ -0,0 +1,124 @@
|
||||
import path from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import { promises as fs } from 'fs';
|
||||
import pMap from 'p-map';
|
||||
import { getDirectories } from '.';
|
||||
import { CURSEFORGE } from '../../../common/utils/constants';
|
||||
|
||||
const getInstances = async instancesPath => {
|
||||
const mapFolderToInstance = async instance => {
|
||||
try {
|
||||
const configPath = path.join(
|
||||
path.join(instancesPath, instance, 'config.json')
|
||||
);
|
||||
const rawConfig = await fs.readFile(configPath);
|
||||
|
||||
// Remove temp config if present
|
||||
try {
|
||||
const tempConfigPath = path.join(
|
||||
path.join(instancesPath, instance, 'config_new_temp.json')
|
||||
);
|
||||
await fs.unlink(tempConfigPath);
|
||||
} catch {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
// Restore config in case of crash
|
||||
if (rawConfig.every(v => v === 0)) {
|
||||
const backupConfigPath = path.join(
|
||||
path.join(instancesPath, instance, 'config.bak.json')
|
||||
);
|
||||
const backupConfig = await fs.readFile(backupConfigPath);
|
||||
JSON.parse(backupConfig);
|
||||
await fs.rename(backupConfigPath, configPath);
|
||||
}
|
||||
|
||||
const newRawConfig = await fs.readFile(configPath);
|
||||
const config = JSON.parse(newRawConfig);
|
||||
|
||||
// if the launcher has the modloader as an array, convert it to object
|
||||
if (Array.isArray(config.modloader)) {
|
||||
// source is the source where the modpack comes from example: curseforge
|
||||
// loaderType is the modloader example: forge
|
||||
const [
|
||||
loaderType,
|
||||
mcVersion,
|
||||
loaderVersion,
|
||||
projectID,
|
||||
fileID,
|
||||
source
|
||||
] = config.modloader;
|
||||
|
||||
const patchedConfig = {
|
||||
...config,
|
||||
loader: {
|
||||
loaderType,
|
||||
mcVersion,
|
||||
...(loaderVersion && { loaderVersion }),
|
||||
...(fileID && { fileID }),
|
||||
...(projectID && { projectID }),
|
||||
...(!source && fileID && projectID && { source: CURSEFORGE })
|
||||
}
|
||||
};
|
||||
|
||||
delete patchedConfig.modloader;
|
||||
|
||||
await fse.writeFile(configPath, JSON.stringify(patchedConfig));
|
||||
|
||||
return { ...patchedConfig, name: instance };
|
||||
}
|
||||
|
||||
if (
|
||||
config.loader?.fileId ||
|
||||
config.loader?.addonId ||
|
||||
config.loader?.addonID
|
||||
) {
|
||||
const { fileId, addonId, addonID } = config.loader;
|
||||
|
||||
const patchedConfig = {
|
||||
...config,
|
||||
loader: {
|
||||
...config.loader,
|
||||
...(fileId && { fileID: fileId }),
|
||||
...(addonId && { projectID: addonId }),
|
||||
...(addonID && { projectID: addonID })
|
||||
}
|
||||
};
|
||||
|
||||
delete patchedConfig.loader.fileId;
|
||||
delete patchedConfig.loader.addonId;
|
||||
|
||||
await fse.writeFile(configPath, JSON.stringify(patchedConfig));
|
||||
|
||||
return { ...patchedConfig, name: instance };
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
name: instance
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const folders = await getDirectories(instancesPath);
|
||||
const instances = await pMap(
|
||||
folders.filter(folder => folder !== '.DS_Store'),
|
||||
mapFolderToInstance,
|
||||
{
|
||||
concurrency: 5
|
||||
}
|
||||
);
|
||||
const hashMap = {};
|
||||
// eslint-disable-next-line
|
||||
for (const instance of instances) {
|
||||
// eslint-disable-next-line
|
||||
if (!instance) continue;
|
||||
hashMap[instance.name] = instance;
|
||||
}
|
||||
|
||||
return hashMap;
|
||||
};
|
||||
|
||||
export default getInstances;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { push } from 'connected-react-router';
|
||||
|
||||
const middleware = store => next => action => {
|
||||
const currState = store.getState();
|
||||
const result = next(action);
|
||||
const nextState = store.getState();
|
||||
const { dispatch } = store;
|
||||
|
||||
const currentAccountIdChanged =
|
||||
currState.app.currentAccountId !== nextState.app.currentAccountId;
|
||||
|
||||
if (currentAccountIdChanged && !nextState.app.currentAccountId) {
|
||||
dispatch(push('/'));
|
||||
}
|
||||
|
||||
if (currState.settings.potatoPcMode !== nextState.settings.potatoPcMode) {
|
||||
if (nextState.settings.potatoPcMode) {
|
||||
document.getElementById('root').classList.add('disable-animations');
|
||||
} else {
|
||||
document.getElementById('root').classList.remove('disable-animations');
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default middleware;
|
||||
@@ -0,0 +1,62 @@
|
||||
// import watch from "node-watch";
|
||||
import makeDir from 'make-dir';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { notification } from 'antd';
|
||||
import * as ActionTypes from '../../../common/reducers/actionTypes';
|
||||
import getInstances from './getInstances';
|
||||
import modsFingerprintsScan from './modsFingerprintsScan';
|
||||
import { startListener } from '../../../common/reducers/actions';
|
||||
import { _getInstancesPath } from '../../../common/utils/selectors';
|
||||
|
||||
const middleware = store => next => action => {
|
||||
const currState = store.getState();
|
||||
const result = next(action);
|
||||
const nextState = store.getState();
|
||||
const { dispatch } = store;
|
||||
if (!nextState.userData) return result;
|
||||
const instancesPath = _getInstancesPath(nextState);
|
||||
|
||||
const userDataChanged = currState.userData !== nextState.userData;
|
||||
|
||||
// If not initialized yet, start listener and do a first-time read
|
||||
if (!nextState.instances.started || userDataChanged) {
|
||||
const startInstancesListener = async () => {
|
||||
await ipcRenderer.invoke('stop-listener');
|
||||
await makeDir(instancesPath);
|
||||
const instances = await getInstances(instancesPath);
|
||||
dispatch({
|
||||
type: ActionTypes.UPDATE_INSTANCES,
|
||||
instances
|
||||
});
|
||||
const instances1 = await modsFingerprintsScan(instancesPath);
|
||||
dispatch({
|
||||
type: ActionTypes.UPDATE_INSTANCES,
|
||||
instances: instances1
|
||||
});
|
||||
try {
|
||||
await makeDir(instancesPath);
|
||||
await dispatch(startListener());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// eslint-disable-next-line
|
||||
notification.open({
|
||||
key: 'nsfwNotWorking',
|
||||
message: 'NSFW Error',
|
||||
description: 'Node Sentinel File Watcher could not be initialized',
|
||||
top: 47,
|
||||
duration: 10
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.UPDATE_INSTANCES_STARTED,
|
||||
started: true
|
||||
});
|
||||
startInstancesListener();
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default middleware;
|
||||
@@ -0,0 +1,163 @@
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import fse from 'fs-extra';
|
||||
import pMap from 'p-map';
|
||||
import { getDirectories, normalizeModData, isMod } from '.';
|
||||
import { getFileMurmurHash2 } from '../../../common/utils';
|
||||
import { getAddonsByFingerprint, getAddon } from '../../../common/api';
|
||||
|
||||
const modsFingerprintsScan = async instancesPath => {
|
||||
const mapFolderToInstance = async instance => {
|
||||
try {
|
||||
const configPath = path.join(
|
||||
path.join(instancesPath, instance, 'config.json')
|
||||
);
|
||||
const config = await fse.readJSON(configPath);
|
||||
|
||||
if (!config.loader) {
|
||||
throw new Error(`Config for ${instance} could not be parsed`);
|
||||
}
|
||||
|
||||
const modsFolder = path.join(instancesPath, instance, 'mods');
|
||||
const modsFolderExists = await fse.pathExists(modsFolder);
|
||||
|
||||
if (!modsFolderExists) return { ...config, name: instance };
|
||||
|
||||
// Check if config.mods has a different number of mods than the actual number of mods
|
||||
|
||||
// Count the actual mods inside the folder
|
||||
const files = await fs.readdir(modsFolder);
|
||||
|
||||
const fileNamesToRemove = [];
|
||||
const missingMods = {};
|
||||
/* eslint-disable */
|
||||
// Check for new mods in local storage that are not present in config
|
||||
for (const file of files) {
|
||||
try {
|
||||
const completeFilePath = path.join(modsFolder, file);
|
||||
const stat = await fs.lstat(completeFilePath);
|
||||
if (stat.isFile() && isMod(completeFilePath, instancesPath)) {
|
||||
// Check if file is in config
|
||||
if (!(config?.mods || []).find(mod => mod.fileName === file)) {
|
||||
const murmurHash = await getFileMurmurHash2(completeFilePath);
|
||||
console.log(
|
||||
'[MODS SCANNER] Local mod not found in config',
|
||||
file,
|
||||
murmurHash
|
||||
);
|
||||
missingMods[file] = murmurHash;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Check for old mods in config that are not present on local storage
|
||||
for (const configMod of config?.mods || []) {
|
||||
if (!files.includes(configMod.fileName)) {
|
||||
fileNamesToRemove.push(configMod.fileName);
|
||||
console.log(
|
||||
`[MODS SCANNER] Removing ${configMod.fileName} from config`
|
||||
);
|
||||
}
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
let newMods = config?.mods || [];
|
||||
|
||||
if (Object.values(missingMods).length !== 0) {
|
||||
let success = false;
|
||||
let tries = 10;
|
||||
|
||||
while (!success || tries > 10) {
|
||||
try {
|
||||
const data = await getAddonsByFingerprint(
|
||||
Object.values(missingMods)
|
||||
);
|
||||
|
||||
const matches = await Promise.all(
|
||||
Object.entries(missingMods).map(async ([fileName, hash]) => {
|
||||
const exactMatch = (data.exactMatches || []).find(
|
||||
v => v.file.fileFingerprint === hash
|
||||
);
|
||||
if (exactMatch?.file) {
|
||||
let addonData = null;
|
||||
try {
|
||||
addonData = await getAddon(exactMatch.file.modId);
|
||||
return {
|
||||
...normalizeModData(
|
||||
exactMatch.file,
|
||||
exactMatch.file.modId,
|
||||
addonData.name
|
||||
),
|
||||
fileName
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
fileName,
|
||||
displayName: fileName,
|
||||
packageFingerprint: hash
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
fileName,
|
||||
displayName: fileName,
|
||||
packageFingerprint: hash
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
newMods = [...newMods, ...matches];
|
||||
success = true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
tries += 1;
|
||||
await new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filterMods = newMods
|
||||
.filter(_ => _)
|
||||
.filter(v => !fileNamesToRemove.includes(v.fileName));
|
||||
|
||||
const newConfig = {
|
||||
...config,
|
||||
mods: filterMods
|
||||
};
|
||||
|
||||
if (JSON.stringify(config) !== JSON.stringify(newConfig)) {
|
||||
await fse.outputJson(configPath, newConfig);
|
||||
return { ...newConfig, name: instance };
|
||||
}
|
||||
return { ...config, name: instance };
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const folders = await getDirectories(instancesPath);
|
||||
const instances = await pMap(
|
||||
folders.filter(folder => folder !== '.DS_Store'),
|
||||
mapFolderToInstance,
|
||||
{
|
||||
concurrency: 5
|
||||
}
|
||||
);
|
||||
const hashMap = {};
|
||||
// eslint-disable-next-line
|
||||
for (const instance of instances) {
|
||||
// eslint-disable-next-line
|
||||
if (!instance) continue;
|
||||
hashMap[instance.name] = instance;
|
||||
}
|
||||
|
||||
return hashMap;
|
||||
};
|
||||
|
||||
export default modsFingerprintsScan;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { lazy } from 'react';
|
||||
import AsyncComponent from '../../../common/components/AsyncComponent';
|
||||
import withScroll from './withScroll';
|
||||
|
||||
const Login = lazy(() => import('../views/Login'));
|
||||
const Home = lazy(() => import('../views/Home'));
|
||||
const Onboarding = lazy(() => import('../views/Onboarding'));
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: AsyncComponent(Login)
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
component: withScroll(AsyncComponent(Home))
|
||||
},
|
||||
{
|
||||
path: '/onboarding',
|
||||
component: AsyncComponent(Onboarding)
|
||||
}
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import analytics from '../../../common/utils/analytics';
|
||||
|
||||
const INTERVAL_DURATION = 5 * 60 * 1000;
|
||||
|
||||
const useTrackIdle = pathname => {
|
||||
const clientToken = useSelector(state => state.app.clientToken);
|
||||
useEffect(() => {
|
||||
let interval;
|
||||
if (clientToken && process.env.NODE_ENV !== 'development') {
|
||||
interval = setInterval(() => {
|
||||
analytics.idle(pathname);
|
||||
}, INTERVAL_DURATION);
|
||||
}
|
||||
return () => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
};
|
||||
}, [pathname, clientToken]);
|
||||
};
|
||||
|
||||
export default useTrackIdle;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
const withScroll = Component => {
|
||||
return props => {
|
||||
return (
|
||||
<div
|
||||
css={`
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px 0;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
width: 1000px;
|
||||
`}
|
||||
>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<Component {...props} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default withScroll;
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Button } from 'antd';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import axios from 'axios';
|
||||
// import { promises as fs } from 'fs';
|
||||
// import path from 'path';
|
||||
import Instances from '../components/Instances';
|
||||
import News from '../components/News';
|
||||
import { openModal } from '../../../common/reducers/modals/actions';
|
||||
import {
|
||||
_getCurrentAccount
|
||||
// _getInstances
|
||||
} from '../../../common/utils/selectors';
|
||||
import { extractFace } from '../utils';
|
||||
import { updateLastUpdateVersion } from '../../../common/reducers/actions';
|
||||
|
||||
const AddInstanceIcon = styled(Button)`
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
`;
|
||||
|
||||
const AccountContainer = styled(Button)`
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Home = () => {
|
||||
const dispatch = useDispatch();
|
||||
const account = useSelector(_getCurrentAccount);
|
||||
const news = useSelector(state => state.news);
|
||||
const lastUpdateVersion = useSelector(state => state.app.lastUpdateVersion);
|
||||
// const instances = useSelector(_getInstances);
|
||||
|
||||
const openAddInstanceModal = defaultPage => {
|
||||
dispatch(openModal('AddInstance', { defaultPage }));
|
||||
};
|
||||
|
||||
const openAccountModal = () => {
|
||||
dispatch(openModal('AccountsManager'));
|
||||
};
|
||||
|
||||
const [profileImage, setProfileImage] = useState(null);
|
||||
const [annoucement, setAnnoucement] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
const appVersion = await ipcRenderer.invoke('getAppVersion');
|
||||
if (lastUpdateVersion !== appVersion) {
|
||||
dispatch(updateLastUpdateVersion(appVersion));
|
||||
dispatch(openModal('ChangeLogs'));
|
||||
}
|
||||
try {
|
||||
const { data } = await axios.get(
|
||||
null
|
||||
);
|
||||
setAnnoucement(data || null);
|
||||
} catch (e) {
|
||||
console.log('No announcement to show');
|
||||
}
|
||||
};
|
||||
|
||||
init();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
extractFace(account.skin).then(setProfileImage).catch(console.error);
|
||||
}, [account]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<News news={news} />
|
||||
{annoucement ? (
|
||||
<div
|
||||
css={`
|
||||
margin-top: 10px;
|
||||
padding: 30px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: ${props => props.theme.palette.colors.yellow};
|
||||
`}
|
||||
>
|
||||
{annoucement}
|
||||
</div>
|
||||
) : null}
|
||||
<Instances />
|
||||
<AddInstanceIcon type="primary" onClick={() => openAddInstanceModal(0)}>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</AddInstanceIcon>
|
||||
<AccountContainer type="primary" onClick={openAccountModal}>
|
||||
{profileImage ? (
|
||||
<img
|
||||
src={`data:image/jpeg;base64,${profileImage}`}
|
||||
css={`
|
||||
width: 15px;
|
||||
cursor: pointer;
|
||||
height: 15px;
|
||||
margin-right: 10px;
|
||||
`}
|
||||
alt="profile"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
css={`
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: ${props => props.theme.palette.grey[100]};
|
||||
margin-right: 10px;
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
{account && account.selectedProfile.name}
|
||||
</AccountContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Home);
|
||||
@@ -0,0 +1,322 @@
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import styled from 'styled-components';
|
||||
import { Transition } from 'react-transition-group';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faArrowRight,
|
||||
faExternalLinkAlt
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { Input, Button } from 'antd';
|
||||
import { useKey } from 'rooks';
|
||||
import { login, loginOAuth } from '../../../common/reducers/actions';
|
||||
import { load, requesting } from '../../../common/reducers/loading/actions';
|
||||
import features from '../../../common/reducers/loading/features';
|
||||
import backgroundVideo from '../../../common/assets/background.webm';
|
||||
import HorizontalLogo from '../../../ui/HorizontalLogo';
|
||||
import { openModal } from '../../../common/reducers/modals/actions';
|
||||
|
||||
const LoginButton = styled(Button)`
|
||||
border-radius: 4px;
|
||||
font-size: 22px;
|
||||
background: ${props =>
|
||||
props.active ? props.theme.palette.grey[600] : 'transparent'};
|
||||
border: 0;
|
||||
height: auto;
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
&:hover {
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
background: ${props => props.theme.palette.grey[600]};
|
||||
}
|
||||
&:focus {
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
background: ${props => props.theme.palette.grey[600]};
|
||||
}
|
||||
`;
|
||||
|
||||
const MicrosoftLoginButton = styled(LoginButton)`
|
||||
margin-top: 10px;
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const LeftSide = styled.div`
|
||||
position: relative;
|
||||
width: 300px;
|
||||
padding: 40px;
|
||||
height: 100%;
|
||||
transition: 0.3s ease-in-out;
|
||||
transform: translateX(
|
||||
${({ transitionState }) =>
|
||||
transitionState === 'entering' || transitionState === 'entered'
|
||||
? -300
|
||||
: 0}px
|
||||
);
|
||||
background: ${props => props.theme.palette.secondary.main};
|
||||
& div {
|
||||
margin: 10px 0;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
color: ${props => props.theme.palette.text.third};
|
||||
}
|
||||
`;
|
||||
|
||||
const Form = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
margin: 20px 0 !important;
|
||||
`;
|
||||
|
||||
const Background = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
video {
|
||||
transition: 0.3s ease-in-out;
|
||||
transform: translateX(
|
||||
${({ transitionState }) =>
|
||||
transitionState === 'entering' || transitionState === 'entered'
|
||||
? -300
|
||||
: 0}px
|
||||
);
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
height: 150%;
|
||||
top: -30%;
|
||||
}
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Footer = styled.div`
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: calc(100% - 80px);
|
||||
`;
|
||||
|
||||
const FooterLinks = styled.div`
|
||||
font-size: 0.75rem;
|
||||
margin: 0 !important;
|
||||
a {
|
||||
color: ${props => props.theme.palette.text.third};
|
||||
}
|
||||
a:hover {
|
||||
color: ${props => props.theme.palette.text.secondary};
|
||||
}
|
||||
`;
|
||||
|
||||
const Loading = styled.div`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: -1;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(8px) brightness(60%);
|
||||
font-size: 40px;
|
||||
transition: 0.3s ease-in-out;
|
||||
opacity: ${({ transitionState }) =>
|
||||
transitionState === 'entering' || transitionState === 'entered' ? 1 : 0};
|
||||
`;
|
||||
const LoginFailMessage = styled.div`
|
||||
color: ${props => props.theme.palette.colors.red};
|
||||
`;
|
||||
|
||||
const Login = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [email, setEmail] = useState(null);
|
||||
const [password, setPassword] = useState(null);
|
||||
const [version, setVersion] = useState(null);
|
||||
const [loginFailed, setLoginFailed] = useState(false);
|
||||
const loading = useSelector(
|
||||
state => state.loading.accountAuthentication.isRequesting
|
||||
);
|
||||
|
||||
const authenticate = () => {
|
||||
if (!email || !password) return;
|
||||
dispatch(requesting('accountAuthentication'));
|
||||
setTimeout(() => {
|
||||
dispatch(
|
||||
load(features.mcAuthentication, dispatch(login(email, password)))
|
||||
).catch(e => {
|
||||
console.error(e);
|
||||
setLoginFailed(e);
|
||||
setPassword(null);
|
||||
});
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const authenticateMicrosoft = () => {
|
||||
dispatch(requesting('accountAuthentication'));
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch(load(features.mcAuthentication, dispatch(loginOAuth()))).catch(
|
||||
e => {
|
||||
console.error(e);
|
||||
setLoginFailed(e);
|
||||
}
|
||||
);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
useKey(['Enter'], authenticate);
|
||||
|
||||
useEffect(() => {
|
||||
ipcRenderer.invoke('getAppVersion').then(setVersion).catch(console.error);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Transition in={loading} timeout={300}>
|
||||
{transitionState => (
|
||||
<Container>
|
||||
<LeftSide transitionState={transitionState}>
|
||||
<Header>
|
||||
<HorizontalLogo size={200} />
|
||||
</Header>
|
||||
<Form>
|
||||
<div>
|
||||
<Input
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={({ target: { value } }) => setEmail(value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={({ target: { value } }) => setPassword(value)}
|
||||
/>
|
||||
</div>
|
||||
{loginFailed && (
|
||||
<LoginFailMessage>{loginFailed?.message}</LoginFailMessage>
|
||||
)}
|
||||
<LoginButton color="primary" onClick={authenticate}>
|
||||
Sign In
|
||||
<FontAwesomeIcon
|
||||
css={`
|
||||
margin-left: 6px;
|
||||
`}
|
||||
icon={faArrowRight}
|
||||
/>
|
||||
</LoginButton>
|
||||
<MicrosoftLoginButton
|
||||
color="primary"
|
||||
onClick={authenticateMicrosoft}
|
||||
>
|
||||
Sign in with Microsoft
|
||||
<FontAwesomeIcon
|
||||
css={`
|
||||
margin-left: 6px;
|
||||
`}
|
||||
icon={faExternalLinkAlt}
|
||||
/>
|
||||
</MicrosoftLoginButton>
|
||||
</Form>
|
||||
<Footer>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
`}
|
||||
>
|
||||
<FooterLinks>
|
||||
<div>
|
||||
<a href="https://www.minecraft.net/it-it/password/forgot">
|
||||
FORGOT PASSWORD
|
||||
</a>
|
||||
</div>
|
||||
</FooterLinks>
|
||||
<div
|
||||
css={`
|
||||
cursor: pointer;
|
||||
`}
|
||||
onClick={() => dispatch(openModal('ChangeLogs'))}
|
||||
>
|
||||
v{version}
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
css={`
|
||||
font-size: 10px;
|
||||
`}
|
||||
>
|
||||
Sign in with your Mojang Account. By doing so, you accept all
|
||||
our policies and terms stated below.
|
||||
</p>
|
||||
<div
|
||||
css={`
|
||||
margin-top: 20px;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
flex-direction: row;
|
||||
span {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
onClick={() =>
|
||||
dispatch(openModal('PolicyModal', { policy: 'privacy' }))
|
||||
}
|
||||
>
|
||||
Privacy Policy
|
||||
</span>
|
||||
<span
|
||||
onClick={() =>
|
||||
dispatch(openModal('PolicyModal', { policy: 'tos' }))
|
||||
}
|
||||
>
|
||||
Terms and Conditions
|
||||
</span>
|
||||
<span
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
openModal('PolicyModal', { policy: 'acceptableuse' })
|
||||
)
|
||||
}
|
||||
>
|
||||
Acceptable Use Policy
|
||||
</span>
|
||||
</div>
|
||||
</Footer>
|
||||
</LeftSide>
|
||||
<Background transitionState={transitionState}>
|
||||
<video autoPlay muted loop>
|
||||
<source src={backgroundVideo} type="video/webm" />
|
||||
</video>
|
||||
</Background>
|
||||
<Loading transitionState={transitionState}>Loading...</Loading>
|
||||
</Container>
|
||||
)}
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Login);
|
||||
@@ -0,0 +1,302 @@
|
||||
import React, { useRef, useState, memo, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { push } from 'connected-react-router';
|
||||
import styled from 'styled-components';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faLongArrowAltRight,
|
||||
faLongArrowAltUp,
|
||||
faLongArrowAltDown
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import backgroundVideo from '../../../common/assets/onboarding.webm';
|
||||
import { _getCurrentAccount } from '../../../common/utils/selectors';
|
||||
import BisectHosting from '../../../ui/BisectHosting';
|
||||
import KoFiButton from '../../../common/assets/ko-fi.png';
|
||||
import { openModal } from '../../../common/reducers/modals/actions';
|
||||
|
||||
const Background = styled.div`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: ${props => props.theme.palette.colors.darkBlue};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const scrollToRef = ref =>
|
||||
ref.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
||||
const Home = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const [initScrolled, setInitScrolled] = useState(false);
|
||||
const account = useSelector(_getCurrentAccount);
|
||||
|
||||
const firstSlideRef = useRef(null);
|
||||
const secondSlideRef = useRef(null);
|
||||
const thirdSlideRef = useRef(null);
|
||||
const forthSlideRef = useRef(null);
|
||||
const fifthSlideRef = useRef(null);
|
||||
const executeScroll = type => {
|
||||
if (currentSlide + type < 0 || currentSlide + type > 5) return;
|
||||
setCurrentSlide(currentSlide + type);
|
||||
switch (currentSlide + type) {
|
||||
case 0:
|
||||
scrollToRef(firstSlideRef);
|
||||
break;
|
||||
case 1:
|
||||
scrollToRef(secondSlideRef);
|
||||
break;
|
||||
case 2:
|
||||
scrollToRef(thirdSlideRef);
|
||||
break;
|
||||
case 3:
|
||||
scrollToRef(forthSlideRef);
|
||||
break;
|
||||
case 4:
|
||||
scrollToRef(fifthSlideRef);
|
||||
break;
|
||||
default:
|
||||
scrollToRef(firstSlideRef);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setInitScrolled(true);
|
||||
executeScroll(1);
|
||||
}, 4800);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Background>
|
||||
<div
|
||||
ref={firstSlideRef}
|
||||
css={`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: ${props => props.theme.palette.grey[700]};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
font-size: 40px;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
css={`
|
||||
height: 100vh;
|
||||
`}
|
||||
>
|
||||
<source src={backgroundVideo} type="video/webm" />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={secondSlideRef}
|
||||
css={`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: ${props => props.theme.palette.grey[800]};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
padding: 0 120px;
|
||||
`}
|
||||
>
|
||||
{account.selectedProfile.name}, welcome to GDLauncher!
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={thirdSlideRef}
|
||||
css={`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: ${props => props.theme.palette.grey[700]};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin: 20% 10%;
|
||||
`}
|
||||
>
|
||||
GDlauncher is completely free and open source. <br />
|
||||
If you want to support us, consider renting a server on BisectHosting,
|
||||
our official partner!
|
||||
<br />
|
||||
<br />
|
||||
<div
|
||||
css={`
|
||||
cursor: pointer;
|
||||
`}
|
||||
>
|
||||
<BisectHosting
|
||||
showPointerCursor
|
||||
size={100}
|
||||
onClick={() => dispatch(openModal('BisectHosting'))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={forthSlideRef}
|
||||
css={`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: ${props => props.theme.palette.grey[800]};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin: 20%;
|
||||
`}
|
||||
>
|
||||
Or you can also support us through Ko-Fi.
|
||||
<div
|
||||
css={`
|
||||
margin: 40px;
|
||||
`}
|
||||
>
|
||||
<a href="https://ko-fi.com/gdlauncher">
|
||||
<img src={KoFiButton} alt="Ko-Fi" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={fifthSlideRef}
|
||||
css={`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: ${props => props.theme.palette.grey[700]};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 20%;
|
||||
`}
|
||||
>
|
||||
Also, don't forget to join us on Discord! This is where our
|
||||
community is!
|
||||
<iframe
|
||||
css={`
|
||||
margin-top: 40px;
|
||||
`}
|
||||
src="https://discordapp.com/widget?id=398091532881756161&theme=dark"
|
||||
width="350"
|
||||
height="410"
|
||||
allowTransparency="true"
|
||||
frameBorder="0"
|
||||
title="discordFrame"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{currentSlide !== 0 && currentSlide !== 1 && initScrolled && (
|
||||
<div
|
||||
css={`
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
top: 40px;
|
||||
transition: 0.1s ease-in-out;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
font-size: 40px;
|
||||
cursor: pointer;
|
||||
width: 70px;
|
||||
height: 40px;
|
||||
color: ${props => props.theme.palette.text.icon};
|
||||
&:hover {
|
||||
background: ${props => props.theme.action.hover};
|
||||
}
|
||||
`}
|
||||
onClick={() => executeScroll(-1)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faLongArrowAltUp} />
|
||||
</div>
|
||||
)}
|
||||
{currentSlide !== 0 && initScrolled && (
|
||||
<div
|
||||
css={`
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
transition: 0.1s ease-in-out;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
font-size: 40px;
|
||||
cursor: pointer;
|
||||
width: 70px;
|
||||
height: 40px;
|
||||
color: ${props => props.theme.palette.text.icon};
|
||||
&:hover {
|
||||
background: ${props => props.theme.action.hover};
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
if (currentSlide === 4) {
|
||||
dispatch(push('/home'));
|
||||
} else {
|
||||
executeScroll(1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={currentSlide === 4 ? faLongArrowAltRight : faLongArrowAltDown}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Background>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Home);
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { Button } from 'antd';
|
||||
import creeper from './assets/creeper.png';
|
||||
|
||||
export default class ErrorBoundary extends React.Component {
|
||||
static getDerivedStateFromError(error) {
|
||||
// Update state so the next render will show the fallback UI.
|
||||
return { error: error.message };
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { error: null, info: null };
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
this.setState(prevState => {
|
||||
return {
|
||||
...prevState,
|
||||
error: prevState.error
|
||||
? `${prevState.error} / ${error.message}`
|
||||
: error.message,
|
||||
info: info.componentStack || prevState.info
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error, info } = this.state;
|
||||
const { children } = this.props;
|
||||
if (error) {
|
||||
// You can render any custom fallback UI
|
||||
return (
|
||||
<div
|
||||
css={`
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 60px;
|
||||
`}
|
||||
>
|
||||
<img src={creeper} alt="creeper" />
|
||||
<h1
|
||||
css={`
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
`}
|
||||
>
|
||||
WE’RE SSSSSSORRY. GDLauncher ran into a creeper and blew up..
|
||||
</h1>
|
||||
<div
|
||||
css={`
|
||||
margin-top: 20px;
|
||||
`}
|
||||
>
|
||||
{error} <br />
|
||||
{info}
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
if (process?.env?.APP_TYPE !== 'web') {
|
||||
// eslint-disable-next-line global-require
|
||||
require('electron').ipcRenderer.invoke('appRestart');
|
||||
}
|
||||
}}
|
||||
css={`
|
||||
margin-top: 30px;
|
||||
`}
|
||||
>
|
||||
Restart GDLauncher
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return children;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
export default createGlobalStyle`
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
-webkit-transform:translate3d(0,0,0);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
#root {
|
||||
font-family: Inter, Roboto, Helvetica, sans-serif;
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.disable-animations *,
|
||||
.disable-animations *::before,
|
||||
.disable-animations *::after {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
a {
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:focus { outline: none; }
|
||||
|
||||
hr {
|
||||
border-width: 1px 0 0;
|
||||
border-top-style: solid;
|
||||
border-right-style: initial;
|
||||
border-bottom-style: initial;
|
||||
border-left-style: initial;
|
||||
border-top-color: rgba(255, 255, 255, 0.4);
|
||||
border-right-color: initial;
|
||||
border-bottom-color: initial;
|
||||
border-left-color: initial;
|
||||
border-image: initial;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
img {
|
||||
-webkit-user-drag: none;
|
||||
-webkit-transform:translate3d(0,0,0);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: ${props => props.theme.palette.grey[200]};
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
:not(input):not(textarea):not(button):not(span):not(div):not(a):not(i):not(span):not(svg):not(path),
|
||||
:not(input):not(textarea):not(button):not(span):not(div):not(a):not(i):not(span):not(svg):not(path)::after,
|
||||
:not(input):not(textarea):not(button):not(span):not(div):not(a):not(i):not(span):not(svg):not(path)::before {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.react-contextmenu {
|
||||
background: ${props => props.theme.palette.grey[700]};
|
||||
background-clip: padding-box;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
font-size: 16px;
|
||||
min-width: 220px;
|
||||
outline: none;
|
||||
transform-origin: top left;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: opacity 150ms;
|
||||
}
|
||||
|
||||
.react-contextmenu.react-contextmenu--visible {
|
||||
transition: opacity 150ms;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
.react-contextmenu-item {
|
||||
background: 0 0;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
span {
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
width: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.react-contextmenu-item:not(.react-contextmenu-item--divider):hover{
|
||||
background: ${({ theme }) => theme.palette.grey[600]};
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.react-contextmenu-item:not(.react-contextmenu-item--divider):active{
|
||||
background: ${({ theme }) => theme.palette.grey[500]};
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.react-contextmenu-item:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-contextmenu-item.react-contextmenu-item--disabled,
|
||||
.react-contextmenu-item.react-contextmenu-item--disabled:hover {
|
||||
background-color: transparent;
|
||||
color: ${props => props.theme.palette.text.disabled};
|
||||
}
|
||||
|
||||
.react-contextmenu-item--divider {
|
||||
border-bottom: 1px solid ${props => props.theme.palette.grey[600]};
|
||||
cursor: inherit;
|
||||
margin-bottom: 3px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.ant-select-dropdown, .ant-dropdown, .ant-cascader-menus {
|
||||
z-index: 99999999 !important;
|
||||
}
|
||||
|
||||
.ant-radio-button-wrapper {
|
||||
color: rgba(255, 255, 255, 0.65) !important;
|
||||
}
|
||||
|
||||
.ant-radio-button-wrapper:hover, .ant-radio-button-wrapper-checked {
|
||||
color: rgba(255, 255, 255, 0.95) !important;
|
||||
}
|
||||
|
||||
.ant-radio-button-wrapper-checked {
|
||||
border-color: rgba(255, 255, 255, 0.95) !important;
|
||||
}
|
||||
|
||||
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled)::before,
|
||||
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover::before {
|
||||
background-color: rgba(255, 255, 255, 0.95) !important;
|
||||
}
|
||||
|
||||
|
||||
@keyframes modalShake {
|
||||
0% { transform: scale(1.01) }
|
||||
30% { transform: scale(0.99) }
|
||||
60% { transform: scale(1.01) }
|
||||
90% { transform: scale(0.99) }
|
||||
100% { transform: scale(1) }
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,362 @@
|
||||
// @flow
|
||||
import axios from 'axios';
|
||||
import qs from 'querystring';
|
||||
import {
|
||||
MOJANG_APIS,
|
||||
FORGESVC_URL,
|
||||
FABRIC_APIS,
|
||||
JAVA_MANIFEST_URL,
|
||||
IMGUR_CLIENT_ID,
|
||||
MICROSOFT_LIVE_LOGIN_URL,
|
||||
MICROSOFT_XBOX_LOGIN_URL,
|
||||
MICROSOFT_XSTS_AUTH_URL,
|
||||
MINECRAFT_SERVICES_URL,
|
||||
JAVA_LATEST_MANIFEST_URL
|
||||
} from './utils/constants';
|
||||
import { sortByDate, getMcManifestUrl } from './utils';
|
||||
import ga from './utils/analytics';
|
||||
|
||||
const axioInstance = axios.create({
|
||||
headers: {
|
||||
'X-API-KEY': '$2a$10$5BgCleD8.rLQ5Ix17Xm2lOjgfoeTJV26a1BXmmpwrOemgI517.nuC',
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const trackCurseForgeAPI = () => {
|
||||
ga.sendCustomEvent('CurseForgeAPICall');
|
||||
};
|
||||
|
||||
// Microsoft Auth
|
||||
export const msExchangeCodeForAccessToken = (
|
||||
clientId,
|
||||
redirectUrl,
|
||||
code,
|
||||
codeVerifier
|
||||
) => {
|
||||
return axios.post(
|
||||
`${MICROSOFT_LIVE_LOGIN_URL}/oauth20_token.srf`,
|
||||
qs.stringify({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: clientId,
|
||||
scope: 'offline_access xboxlive.signin xboxlive.offline_access',
|
||||
redirect_uri: redirectUrl,
|
||||
code,
|
||||
code_verifier: codeVerifier
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Skip-Origin': 'skip'
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const msAuthenticateXBL = accessToken => {
|
||||
return axios.post(
|
||||
`${MICROSOFT_XBOX_LOGIN_URL}/user/authenticate`,
|
||||
{
|
||||
Properties: {
|
||||
AuthMethod: 'RPS',
|
||||
SiteName: 'user.auth.xboxlive.com',
|
||||
RpsTicket: `d=${accessToken}` // your access token from step 2 here
|
||||
},
|
||||
RelyingParty: 'http://auth.xboxlive.com',
|
||||
TokenType: 'JWT'
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'x-xbl-contract-version': 1
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const msAuthenticateXSTS = xblToken => {
|
||||
return axios.post(`${MICROSOFT_XSTS_AUTH_URL}/xsts/authorize`, {
|
||||
Properties: {
|
||||
SandboxId: 'RETAIL',
|
||||
UserTokens: [xblToken]
|
||||
},
|
||||
RelyingParty: 'rp://api.minecraftservices.com/',
|
||||
TokenType: 'JWT'
|
||||
});
|
||||
};
|
||||
|
||||
export const msAuthenticateMinecraft = (uhsToken, xstsToken) => {
|
||||
return axios.post(
|
||||
`${MINECRAFT_SERVICES_URL}/authentication/login_with_xbox`,
|
||||
{
|
||||
identityToken: `XBL3.0 x=${uhsToken};${xstsToken}`
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const msMinecraftProfile = mcAccessToken => {
|
||||
return axios.get(`${MINECRAFT_SERVICES_URL}/minecraft/profile`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${mcAccessToken}`
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const msOAuthRefresh = (clientId, refreshToken) => {
|
||||
return axios.post(
|
||||
`${MICROSOFT_LIVE_LOGIN_URL}/oauth20_token.srf`,
|
||||
qs.stringify({
|
||||
grant_type: 'refresh_token',
|
||||
scope: 'offline_access xboxlive.signin xboxlive.offline_access',
|
||||
client_id: clientId,
|
||||
refresh_token: refreshToken
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Skip-Origin': 'skip'
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Minecraft API
|
||||
|
||||
export const mcAuthenticate = (username, password, clientToken) => {
|
||||
return axios.post(
|
||||
`${MOJANG_APIS}/authenticate`,
|
||||
{
|
||||
agent: {
|
||||
name: 'Minecraft',
|
||||
version: 1
|
||||
},
|
||||
username,
|
||||
password,
|
||||
clientToken,
|
||||
requestUser: true
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
};
|
||||
|
||||
export const mcValidate = (accessToken, clientToken) => {
|
||||
return axios.post(
|
||||
`${MOJANG_APIS}/validate`,
|
||||
{
|
||||
accessToken,
|
||||
clientToken
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
};
|
||||
|
||||
export const mcRefresh = (accessToken, clientToken) => {
|
||||
return axios.post(
|
||||
`${MOJANG_APIS}/refresh`,
|
||||
{
|
||||
accessToken,
|
||||
clientToken,
|
||||
requestUser: true
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
};
|
||||
|
||||
export const mcGetPlayerSkin = uuid => {
|
||||
return axios.get(
|
||||
`https://sessionserver.mojang.com/session/minecraft/profile/${uuid}`
|
||||
);
|
||||
};
|
||||
|
||||
export const imgurPost = (image, onProgress) => {
|
||||
const bodyFormData = new FormData();
|
||||
bodyFormData.append('image', image);
|
||||
|
||||
return axios.post('https://api.imgur.com/3/image', bodyFormData, {
|
||||
headers: {
|
||||
Authorization: `Client-ID ${IMGUR_CLIENT_ID}`
|
||||
},
|
||||
...(onProgress && { onUploadProgress: onProgress })
|
||||
});
|
||||
};
|
||||
|
||||
export const mcInvalidate = (accessToken, clientToken) => {
|
||||
return axios.post(
|
||||
`${MOJANG_APIS}/invalidate`,
|
||||
{
|
||||
accessToken,
|
||||
clientToken
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
};
|
||||
|
||||
export const getMcManifest = () => {
|
||||
const url = `${getMcManifestUrl()}?timestamp=${new Date().getTime()}`;
|
||||
return axios.get(url);
|
||||
};
|
||||
|
||||
export const getForgeManifest = () => {
|
||||
const url = `https://files.minecraftforge.net/net/minecraftforge/forge/maven-metadata.json?timestamp=${new Date().getTime()}`;
|
||||
return axios.get(url);
|
||||
};
|
||||
|
||||
export const getFabricManifest = () => {
|
||||
const url = `${FABRIC_APIS}/versions`;
|
||||
return axios.get(url);
|
||||
};
|
||||
|
||||
export const getJavaManifest = () => {
|
||||
const url = JAVA_MANIFEST_URL;
|
||||
return axios.get(url);
|
||||
};
|
||||
|
||||
export const getJavaLatestManifest = () => {
|
||||
const url = JAVA_LATEST_MANIFEST_URL;
|
||||
return axios.get(url);
|
||||
};
|
||||
|
||||
export const getFabricJson = ({ mcVersion, loaderVersion }) => {
|
||||
return axios.get(
|
||||
`${FABRIC_APIS}/versions/loader/${encodeURIComponent(
|
||||
mcVersion
|
||||
)}/${encodeURIComponent(loaderVersion)}/profile/json`
|
||||
);
|
||||
};
|
||||
|
||||
// FORGE ADDONS
|
||||
|
||||
export const getAddon = async projectID => {
|
||||
trackCurseForgeAPI();
|
||||
const url = `${FORGESVC_URL}/mods/${projectID}`;
|
||||
const { data } = await axioInstance.get(url);
|
||||
return data?.data;
|
||||
};
|
||||
|
||||
export const getMultipleAddons = async addons => {
|
||||
trackCurseForgeAPI();
|
||||
const url = `${FORGESVC_URL}/mods`;
|
||||
const { data } = await axioInstance.post(
|
||||
url,
|
||||
JSON.stringify({
|
||||
modIds: addons
|
||||
})
|
||||
);
|
||||
return data?.data;
|
||||
};
|
||||
|
||||
export const getAddonFiles = async projectID => {
|
||||
trackCurseForgeAPI();
|
||||
// Aggregate results in case of multiple pages
|
||||
const results = [];
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const url = `${FORGESVC_URL}/mods/${projectID}/files?pageSize=400&index=${results.length}`;
|
||||
const { data } = await axioInstance.get(url);
|
||||
results.push(...(data.data || []));
|
||||
|
||||
hasMore = data.pagination.totalCount > results.length;
|
||||
}
|
||||
|
||||
return results.sort(sortByDate);
|
||||
};
|
||||
|
||||
export const getAddonDescription = async projectID => {
|
||||
trackCurseForgeAPI();
|
||||
const url = `${FORGESVC_URL}/mods/${projectID}/description`;
|
||||
const { data } = await axioInstance.get(url);
|
||||
return data?.data;
|
||||
};
|
||||
|
||||
export const getAddonFile = async (projectID, fileID) => {
|
||||
trackCurseForgeAPI();
|
||||
const url = `${FORGESVC_URL}/mods/${projectID}/files/${fileID}`;
|
||||
const { data } = await axioInstance.get(url);
|
||||
return data?.data;
|
||||
};
|
||||
|
||||
export const getAddonsByFingerprint = async fingerprints => {
|
||||
trackCurseForgeAPI();
|
||||
const url = `${FORGESVC_URL}/fingerprints`;
|
||||
const { data } = await axioInstance.post(url, { fingerprints });
|
||||
|
||||
return data?.data;
|
||||
};
|
||||
|
||||
export const getAddonFileChangelog = async (projectID, fileID) => {
|
||||
trackCurseForgeAPI();
|
||||
const url = `${FORGESVC_URL}/mods/${projectID}/files/${fileID}/changelog`;
|
||||
const { data } = await axioInstance.get(url);
|
||||
|
||||
return data?.data;
|
||||
};
|
||||
|
||||
export const getAddonCategories = async () => {
|
||||
trackCurseForgeAPI();
|
||||
const url = `${FORGESVC_URL}/categories?gameId=432`;
|
||||
const { data } = await axioInstance.get(url);
|
||||
return data.data;
|
||||
};
|
||||
|
||||
export const getCFVersionIds = async () => {
|
||||
trackCurseForgeAPI();
|
||||
const url = `${FORGESVC_URL}/games/432/versions`;
|
||||
const { data } = await axioInstance.get(url);
|
||||
return data.data;
|
||||
};
|
||||
|
||||
export const getSearch = async (
|
||||
type,
|
||||
searchFilter,
|
||||
pageSize,
|
||||
index,
|
||||
sort,
|
||||
isSortDescending,
|
||||
gameVersion,
|
||||
categoryId,
|
||||
modLoaderType
|
||||
) => {
|
||||
trackCurseForgeAPI();
|
||||
const url = `${FORGESVC_URL}/mods/search`;
|
||||
|
||||
// Map sort to sortField
|
||||
let sortField = 1;
|
||||
switch (sort) {
|
||||
case 'Popularity':
|
||||
sortField = 2;
|
||||
break;
|
||||
case 'LastUpdated':
|
||||
sortField = 3;
|
||||
break;
|
||||
case 'Name':
|
||||
sortField = 4;
|
||||
break;
|
||||
case 'Author':
|
||||
sortField = 5;
|
||||
break;
|
||||
case 'TotalDownloads':
|
||||
sortField = 6;
|
||||
break;
|
||||
case 'Featured':
|
||||
default:
|
||||
sortField = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
const params = {
|
||||
gameId: 432,
|
||||
categoryId: categoryId || 0,
|
||||
pageSize,
|
||||
index,
|
||||
sortField,
|
||||
sortOrder: isSortDescending ? 'desc' : 'asc',
|
||||
gameVersion: gameVersion || '',
|
||||
...(modLoaderType === 'fabric' && { modLoaderType: 'Fabric' }),
|
||||
classId: type === 'mods' ? 6 : 4471,
|
||||
searchFilter
|
||||
};
|
||||
|
||||
const { data } = await axioInstance.get(url, { params });
|
||||
return data?.data;
|
||||
};
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 816 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 850 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,332 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
const AcceptableUsePolicy = () => {
|
||||
return (
|
||||
<>
|
||||
<h1>Acceptable use policy</h1>
|
||||
<div>
|
||||
<h2>Introduction</h2>
|
||||
<div>
|
||||
<p>
|
||||
This acceptable use policy (“Policy”) sets forth the general
|
||||
guidelines and acceptable and prohibited uses of the{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
href="https://gdlauncher.com"
|
||||
>
|
||||
gdlauncher.com
|
||||
</a>{' '}
|
||||
website (“Website”), “GDLauncher” application (“Application”) and
|
||||
any of their related products and services (collectively,
|
||||
“Services”). This Policy is a legally binding agreement between you
|
||||
(“User”, “you” or “your”) and this Website operator and Application
|
||||
developer (“Operator”, “we”, “us” or “our”). If you are entering
|
||||
into this agreement on behalf of a business or other legal entity,
|
||||
you represent that you have the authority to bind such entity to
|
||||
this agreement, in which case the terms “User”, “you” or “your”
|
||||
shall refer to such entity. If you do not have such authority, or if
|
||||
you do not agree with the terms of this agreement, you must not
|
||||
accept this agreement and may not access and use the Services. By
|
||||
accessing and using the Services, you acknowledge that you have
|
||||
read, understood, and agree to be bound by the terms of this
|
||||
Agreement. You acknowledge that this Agreement is a contract between
|
||||
you and the Operator, even though it is electronic and is not
|
||||
physically signed by you, and it governs your use of the Services.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Prohibited activities and uses</h2>
|
||||
<div>
|
||||
<p>
|
||||
You may not use the Services to publish content or engage in
|
||||
activity that is illegal under applicable law, that is harmful to
|
||||
others, or that would subject us to liability, including, without
|
||||
limitation, in connection with any of the following, each of which
|
||||
is prohibited under this Policy:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Distributing malware or other malicious code.</li>
|
||||
<li>Disclosing sensitive personal information about others.</li>
|
||||
<li>
|
||||
Collecting, or attempting to collect, personal information about
|
||||
third parties without their knowledge or consent.
|
||||
</li>
|
||||
<li>Distributing pornography or adult related content.</li>
|
||||
<li>
|
||||
Promoting or facilitating prostitution or any escort services.
|
||||
</li>
|
||||
<li>
|
||||
Hosting, distributing or linking to child pornography or content
|
||||
that is harmful to minors.
|
||||
</li>
|
||||
<li>
|
||||
Promoting or facilitating gambling, violence, terrorist activities
|
||||
or selling weapons or ammunition.
|
||||
</li>
|
||||
<li>
|
||||
Engaging in the unlawful distribution of controlled substances,
|
||||
drug contraband or prescription medications.
|
||||
</li>
|
||||
<li>
|
||||
Managing payment aggregators or facilitators such as processing
|
||||
payments on behalf of other businesses or charities.
|
||||
</li>
|
||||
<li>
|
||||
Facilitating pyramid schemes or other models intended to seek
|
||||
payments from public actors.
|
||||
</li>
|
||||
<li>
|
||||
Threatening harm to persons or property or otherwise harassing
|
||||
behavior.
|
||||
</li>
|
||||
<li>
|
||||
Purchasing any of the offered Services on someone else’s behalf.
|
||||
</li>
|
||||
<li>
|
||||
Misrepresenting or fraudulently representing products or services.
|
||||
</li>
|
||||
<li>
|
||||
Infringing the intellectual property or other proprietary rights
|
||||
of others.
|
||||
</li>
|
||||
<li>
|
||||
Facilitating, aiding, or encouraging any of the above activities
|
||||
through the Services.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>System abuse</h2>
|
||||
<div>
|
||||
<p>
|
||||
Any User in violation of the Services security is subject to
|
||||
criminal and civil liability, as well as immediate account
|
||||
termination. Examples include, but are not limited to the following:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Use or distribution of tools designed for compromising security of
|
||||
the Services.
|
||||
</li>
|
||||
<li>
|
||||
Intentionally or negligently transmitting files containing a
|
||||
computer virus or corrupted data.
|
||||
</li>
|
||||
<li>
|
||||
Accessing another network without permission, including to probe
|
||||
or scan for vulnerabilities or breach security or authentication
|
||||
measures.
|
||||
</li>
|
||||
<li>
|
||||
Unauthorized scanning or monitoring of data on any network or
|
||||
system without proper authorization of the owner of the system or
|
||||
network.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Service resources</h2>
|
||||
<div>
|
||||
<p>
|
||||
You may not consume excessive amounts of the resources of the
|
||||
Services or use the Services in any way which results in performance
|
||||
issues or which interrupts the Services for other Users. Prohibited
|
||||
activities that contribute to excessive use, include without
|
||||
limitation:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Deliberate attempts to overload the Services and broadcast attacks
|
||||
(i.e. denial of service attacks).
|
||||
</li>
|
||||
<li>
|
||||
Engaging in any other activities that degrade the usability and
|
||||
performance of the Services.
|
||||
</li>
|
||||
<li>
|
||||
Hosting or running malicious code or other scripts or processes
|
||||
that adversely impact the Services.
|
||||
</li>
|
||||
<li>
|
||||
Operating a file sharing site or scripts for BitTorrent or
|
||||
similar, which includes sending or receiving files containing
|
||||
these mechanisms.
|
||||
</li>
|
||||
<li>
|
||||
Web proxy scripts, such as those that allow anyone to browse to a
|
||||
third party website anonymously, are prohibited.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>No spam policy</h2>
|
||||
<div>
|
||||
<p>
|
||||
You may not use the Services to send spam or bulk unsolicited
|
||||
messages. We maintain a zero tolerance policy for use of the
|
||||
Services in any manner associated with the transmission,
|
||||
distribution or delivery of any bulk e-mail, including unsolicited
|
||||
bulk or unsolicited commercial e-mail, or the sending, assisting, or
|
||||
commissioning the transmission of commercial e-mail that does not
|
||||
comply with the U.S. CAN-SPAM Act of 2003 (“SPAM”).
|
||||
</p>
|
||||
<p>
|
||||
Your products or services advertised via SPAM (i.e. Spamvertised)
|
||||
may not be used in conjunction with the Services. This provision
|
||||
includes, but is not limited to, SPAM sent via fax, phone, postal
|
||||
mail, email, instant messaging, or newsgroups.
|
||||
</p>
|
||||
<p>
|
||||
Sending emails through the Services to purchased email lists (“safe
|
||||
lists”) will be treated as SPAM.
|
||||
</p>
|
||||
<h2>Defamation and objectionable content</h2>
|
||||
<p>
|
||||
We value the freedom of expression and encourage Users to be
|
||||
respectful with the content they post. We are not a publisher of
|
||||
User content and are not in a position to investigate the veracity
|
||||
of individual defamation claims or to determine whether certain
|
||||
material, which we may find objectionable, should be censored.
|
||||
However, we reserve the right to moderate, disable or remove any
|
||||
content to prevent harm to others or to us or the Services, as
|
||||
determined in our sole discretion.
|
||||
</p>
|
||||
<h2>Copyrighted content</h2>
|
||||
<p>
|
||||
Copyrighted material must not be published via the Services without
|
||||
the explicit permission of the copyright owner or a person
|
||||
explicitly authorized to give such permission by the copyright
|
||||
owner. Upon receipt of a claim for copyright infringement, or a
|
||||
notice of such violation, we will immediately run full investigation
|
||||
and, upon confirmation, will notify the person or persons
|
||||
responsible for publishing it and, in our sole discretion, will
|
||||
remove the infringing material from the Services. We may terminate
|
||||
the Service of Users with repeated copyright infringements. Further
|
||||
procedures may be carried out if necessary. We will assume no
|
||||
liability to any User of the Services for the removal of any such
|
||||
material. If you believe your copyright is being infringed by a
|
||||
person or persons using the Services, please get in touch with us to
|
||||
report copyright infringement.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Security</h2>
|
||||
<div>
|
||||
<p>
|
||||
You take full responsibility for maintaining reasonable security
|
||||
precautions for your account. You are responsible for protecting and
|
||||
updating any login account provided to you for the Services. You
|
||||
must protect the confidentiality of your login details, and you
|
||||
should change your password periodically. You are responsible for
|
||||
ensuring all User provided software installed by you on the Services
|
||||
is updated and patched following industry best practice. We make no
|
||||
warranty express or implied for the security and operability of 3rd
|
||||
party software or scripts installed or run by you on the Services.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Enforcement</h2>
|
||||
<div>
|
||||
<p>
|
||||
We reserve our right to be the sole arbiter in determining the
|
||||
seriousness of each infringement and to immediately take corrective
|
||||
actions, including but not limited to:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Suspending or terminating your Service with or without notice upon
|
||||
any violation of this Policy. Any violations may also result in
|
||||
the immediate suspension or termination of your account.
|
||||
</li>
|
||||
<li>
|
||||
Disabling or removing any content which is prohibited by this
|
||||
Policy, including to prevent harm to others or to us or the
|
||||
Services, as determined by us in our sole discretion.
|
||||
</li>
|
||||
<li>
|
||||
Reporting violations to law enforcement as determined by us in our
|
||||
sole discretion.
|
||||
</li>
|
||||
<li>
|
||||
A failure to respond to an email from our abuse team within 2
|
||||
days, or as otherwise specified in the communication to you, may
|
||||
result in the suspension or termination of your account.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Suspended and terminated User accounts due to violations will not be
|
||||
re-activated.
|
||||
</p>
|
||||
<p>
|
||||
Nothing contained in this Policy shall be construed to limit our
|
||||
actions or remedies in any way with respect to any of the prohibited
|
||||
activities. In addition, we reserve at all times all rights and
|
||||
remedies available to us with respect to such activities at law or
|
||||
in equity.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Reporting violations</h2>
|
||||
<div>
|
||||
<p>
|
||||
If you have discovered and would like to report a violation of this
|
||||
Policy, please contact us immediately. We will investigate the
|
||||
situation and provide you with full assistance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Changes and amendments</h2>
|
||||
<div>
|
||||
<p>
|
||||
We reserve the right to modify this Policy or its terms related to
|
||||
the Services at any time at our discretion. When we do, we will
|
||||
revise the updated date at the bottom of this page. We may also
|
||||
provide notice to you in other ways at our discretion, such as
|
||||
through the contact information you have provided.
|
||||
</p>
|
||||
<p>
|
||||
An updated version of this Policy will be effective immediately upon
|
||||
the posting of the revised Policy unless otherwise specified. Your
|
||||
continued use of the Services after the effective date of the
|
||||
revised Policy (or such other act specified at that time) will
|
||||
constitute your consent to those changes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Acceptance of this policy</h2>
|
||||
<div>
|
||||
<p>
|
||||
You acknowledge that you have read this Policy and agree to all its
|
||||
terms and conditions. By accessing and using the Services you agree
|
||||
to be bound by this Policy. If you do not agree to abide by the
|
||||
terms of this Policy, you are not authorized to access or use the
|
||||
Services.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Contacting us</h2>
|
||||
<div>
|
||||
<p>
|
||||
If you have any questions, concerns, or complaints regarding this
|
||||
Policy, we encourage you to contact us using the details below:
|
||||
</p>
|
||||
<p>info@gdevs.io</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>This document was last updated on August 16, 2021</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AcceptableUsePolicy);
|
||||
@@ -0,0 +1,12 @@
|
||||
import React, { Suspense } from 'react';
|
||||
|
||||
function WaitingComponent(MyComponent) {
|
||||
return props => (
|
||||
<Suspense fallback={null}>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<MyComponent {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
export default WaitingComponent;
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faWindowClose } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
const CloseButton = props => {
|
||||
return (
|
||||
// eslint-disable-next-line
|
||||
<CloseIcon {...props}>
|
||||
<FontAwesomeIcon icon={faWindowClose} />
|
||||
</CloseIcon>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloseButton;
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease-in-out;
|
||||
&:hover {
|
||||
color: ${props => props.theme.palette.error.main};
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { memo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { useKey } from 'rooks';
|
||||
import CloseButton from './CloseButton';
|
||||
import { closeModal } from '../reducers/modals/actions';
|
||||
|
||||
const HeaderComponent = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
height: 40px;
|
||||
background: ${props => props.theme.palette.grey[800]};
|
||||
border-radius: 4px;
|
||||
h3 {
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Modal = ({
|
||||
transparentBackground,
|
||||
header,
|
||||
title,
|
||||
backBtn,
|
||||
children,
|
||||
className,
|
||||
removePadding,
|
||||
closeCallback,
|
||||
preventClose
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const closeFunc = () => {
|
||||
if (closeCallback) closeCallback();
|
||||
dispatch(closeModal());
|
||||
};
|
||||
|
||||
useKey(['Escape'], () => {
|
||||
if (!preventClose) closeFunc();
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
transparentBackground={transparentBackground}
|
||||
className={className}
|
||||
css={`
|
||||
background: ${props =>
|
||||
props.transparentBackground
|
||||
? 'transparent'
|
||||
: props.theme.palette.grey[700]};
|
||||
position: absolute;
|
||||
border-radius: 4px;
|
||||
`}
|
||||
>
|
||||
{(header === undefined || header === true) && (
|
||||
<HeaderComponent>
|
||||
<h3>{title || 'Modal'}</h3>
|
||||
{!preventClose && <CloseButton onClick={closeFunc} />}
|
||||
</HeaderComponent>
|
||||
)}
|
||||
<div
|
||||
header={header}
|
||||
removePadding={removePadding}
|
||||
css={`
|
||||
height: ${header === undefined || header === true
|
||||
? 'calc(100% - 40px)'
|
||||
: '100%'};
|
||||
width: 100%;
|
||||
padding: ${props =>
|
||||
(props.header === undefined || props.header === true) &&
|
||||
!props.removePadding
|
||||
? 20
|
||||
: 0}px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
`}
|
||||
>
|
||||
<span onClick={closeFunc}>{backBtn !== undefined && backBtn}</span>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Modal);
|
||||
@@ -0,0 +1,187 @@
|
||||
import React, { useState, useEffect, lazy } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { closeModal } from '../reducers/modals/actions';
|
||||
import AsyncComponent from './AsyncComponent';
|
||||
import Settings from '../modals/Settings';
|
||||
|
||||
const Overlay = styled.div`
|
||||
position: absolute;
|
||||
top: ${props => props.theme.sizes.height.systemNavbar}px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
backdrop-filter: blur(4px);
|
||||
will-change: opacity;
|
||||
transition: opacity 300ms cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
z-index: 9999999;
|
||||
`;
|
||||
|
||||
const Modal = styled.div`
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
transition: transform 300ms;
|
||||
will-change: transform;
|
||||
transition-timing-function: cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
z-index: 9999999;
|
||||
`;
|
||||
|
||||
const modalsComponentLookupTable = {
|
||||
AddInstance: AsyncComponent(lazy(() => import('../modals/AddInstance'))),
|
||||
AccountsManager: AsyncComponent(
|
||||
lazy(() => import('../modals/AccountsManager'))
|
||||
),
|
||||
Settings,
|
||||
Screenshot: AsyncComponent(lazy(() => import('../modals/Screenshot'))),
|
||||
InstanceDeleteConfirmation: AsyncComponent(
|
||||
lazy(() => import('../modals/InstanceDeleteConfirmation'))
|
||||
),
|
||||
ActionConfirmation: AsyncComponent(
|
||||
lazy(() => import('../modals/ActionConfirmation'))
|
||||
),
|
||||
AddAccount: AsyncComponent(lazy(() => import('../modals/AddAccount'))),
|
||||
ModpackDescription: AsyncComponent(
|
||||
lazy(() => import('../modals/ModpackDescription'))
|
||||
),
|
||||
InstanceManager: AsyncComponent(
|
||||
lazy(() => import('../modals/InstanceManager'))
|
||||
),
|
||||
InstanceExportCurseForge: AsyncComponent(
|
||||
lazy(() => import('../modals/InstanceExport/CurseForge'))
|
||||
),
|
||||
InstanceDuplicateName: AsyncComponent(
|
||||
lazy(() => import('../modals/InstanceDuplicateName'))
|
||||
),
|
||||
AutoUpdatesNotAvailable: AsyncComponent(
|
||||
lazy(() => import('../modals/AutoUpdatesNotAvailable'))
|
||||
),
|
||||
OptedOutModsList: AsyncComponent(
|
||||
lazy(() => import('../modals/OptedOutModsList'))
|
||||
),
|
||||
BisectHosting: AsyncComponent(lazy(() => import('../modals/BisectHosting'))),
|
||||
Onboarding: AsyncComponent(lazy(() => import('../modals/Onboarding'))),
|
||||
ModOverview: AsyncComponent(lazy(() => import('../modals/ModOverview'))),
|
||||
ModChangelog: AsyncComponent(lazy(() => import('../modals/ModChangelog'))),
|
||||
ModsBrowser: AsyncComponent(lazy(() => import('../modals/ModsBrowser'))),
|
||||
JavaSetup: AsyncComponent(lazy(() => import('../modals/JavaSetup'))),
|
||||
ModsUpdater: AsyncComponent(lazy(() => import('../modals/ModsUpdater'))),
|
||||
InstanceCrashed: AsyncComponent(
|
||||
lazy(() => import('../modals/InstanceCrashed'))
|
||||
),
|
||||
ChangeLogs: AsyncComponent(lazy(() => import('../modals/ChangeLogs'))),
|
||||
McVersionChanger: AsyncComponent(
|
||||
lazy(() => import('../modals/McVersionChanger'))
|
||||
),
|
||||
PolicyModal: AsyncComponent(lazy(() => import('../modals/PolicyModal'))),
|
||||
InstanceStartupAd: AsyncComponent(
|
||||
lazy(() => import('../modals/InstanceStartupAd'))
|
||||
),
|
||||
InstanceDownloadFailed: AsyncComponent(
|
||||
lazy(() => import('../modals/InstanceDownloadFailed'))
|
||||
),
|
||||
InfoModal: AsyncComponent(lazy(() => import('../modals/InfoModal')))
|
||||
};
|
||||
|
||||
const ModalContainer = ({
|
||||
unmounting,
|
||||
children,
|
||||
preventClose,
|
||||
abortCallback
|
||||
}) => {
|
||||
const [modalStyle, setModalStyle] = useState({
|
||||
opacity: 0
|
||||
});
|
||||
const [bgStyle, setBgStyle] = useState({
|
||||
background: 'rgba(0, 0, 0, 0.70)',
|
||||
opacity: 0
|
||||
});
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(mountStyle, 0);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (unmounting) unMountStyle();
|
||||
}, [unmounting]);
|
||||
|
||||
const back = async e => {
|
||||
e.stopPropagation();
|
||||
if (preventClose) {
|
||||
setModalStyle({
|
||||
animation: `modalShake 0.25s linear infinite`
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
setModalStyle({
|
||||
transform: 'scale(1)'
|
||||
});
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
if (abortCallback) await abortCallback();
|
||||
dispatch(closeModal());
|
||||
};
|
||||
|
||||
const unMountStyle = () => {
|
||||
// css for unmount animation
|
||||
setModalStyle({
|
||||
opacity: 1
|
||||
});
|
||||
setBgStyle({
|
||||
background: 'rgba(0, 0, 0, 0.70)',
|
||||
opacity: 0
|
||||
});
|
||||
};
|
||||
|
||||
const mountStyle = () => {
|
||||
// css for mount animation
|
||||
setModalStyle({
|
||||
opacity: 1
|
||||
});
|
||||
|
||||
setBgStyle({
|
||||
background: 'rgba(0, 0, 0, 0.70)',
|
||||
opacity: 1
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Overlay onMouseDown={back} style={bgStyle}>
|
||||
<Modal style={modalStyle}>{children}</Modal>
|
||||
</Overlay>
|
||||
);
|
||||
};
|
||||
|
||||
const ModalsManager = () => {
|
||||
const currentModals = useSelector(state => state.modals);
|
||||
|
||||
const renderedModals = currentModals.map(modalDescription => {
|
||||
const { modalType, modalProps = {}, unmounting = false } = modalDescription;
|
||||
const ModalComponent = modalsComponentLookupTable[modalType];
|
||||
|
||||
return (
|
||||
<ModalContainer
|
||||
unmounting={unmounting}
|
||||
key={modalType}
|
||||
preventClose={modalProps.preventClose}
|
||||
abortCallback={modalProps.abortCallback}
|
||||
modalType={modalType}
|
||||
>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<ModalComponent {...modalProps} />
|
||||
</ModalContainer>
|
||||
);
|
||||
});
|
||||
|
||||
return renderedModals;
|
||||
};
|
||||
|
||||
export default ModalsManager;
|
||||
@@ -0,0 +1,738 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
const PrivacyPolicy = () => {
|
||||
return (
|
||||
<>
|
||||
<h1>Privacy Policy</h1>
|
||||
<div>
|
||||
<h2>Introduction</h2>
|
||||
<div>
|
||||
<p>
|
||||
We respect your privacy and are committed to protecting it through
|
||||
our compliance with this privacy policy (“Policy”). This Policy
|
||||
describes the types of information we may collect from you or that
|
||||
you may provide (“Personal Information”) on the{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
href="https://gdlauncher.com"
|
||||
>
|
||||
gdlauncher.com
|
||||
</a>{' '}
|
||||
website (“Website”), “GDLauncher” application (“Application”), and
|
||||
any of their related products and services (collectively,
|
||||
“Services”), and our practices for collecting, using, maintaining,
|
||||
protecting, and disclosing that Personal Information. It also
|
||||
describes the choices available to you regarding our use of your
|
||||
Personal Information and how you can access and update it.
|
||||
</p>
|
||||
<p>
|
||||
This Policy is a legally binding agreement between you (“User”,
|
||||
“you” or “your”) and this Website operator and Application developer
|
||||
(“Operator”, “we”, “us” or “our”). If you are entering into this
|
||||
agreement on behalf of a business or other legal entity, you
|
||||
represent that you have the authority to bind such entity to this
|
||||
agreement, in which case the terms “User”, “you” or “your” shall
|
||||
refer to such entity. If you do not have such authority, or if you
|
||||
do not agree with the terms of this agreement, you must not accept
|
||||
this agreement and may not access and use the Services. By accessing
|
||||
and using the Services, you acknowledge that you have read,
|
||||
understood, and agree to be bound by the terms of this Policy. This
|
||||
Policy does not apply to the practices of companies that we do not
|
||||
own or control, or to individuals that we do not employ or manage.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Automatic collection of information</h2>
|
||||
<div>
|
||||
<p>
|
||||
When you open the Website or use the Application, our servers
|
||||
automatically record information that your browser or device sends.
|
||||
This data may include information such as your device’s IP address
|
||||
and location, browser and device name and version, operating system
|
||||
type and version, language preferences, the webpage you were
|
||||
visiting before you came to the Services, pages of the Services that
|
||||
you visit, the time spent on those pages, the information you search
|
||||
for on the Services, access times and dates, and other statistics.
|
||||
</p>
|
||||
<p>
|
||||
Information collected automatically is used only to identify
|
||||
potential cases of abuse and establish statistical information
|
||||
regarding the usage and traffic of the Services. This statistical
|
||||
information is not otherwise aggregated in such a way that would
|
||||
identify any particular User of the system.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Collection of personal information</h2>
|
||||
<div>
|
||||
<p>
|
||||
You can access and use the Services without telling us who you are
|
||||
or revealing any information by which someone could identify you as
|
||||
a specific, identifiable individual. If, however, you wish to use
|
||||
some of the features offered on the Services, you may be asked to
|
||||
provide certain Personal Information (for example, your name and
|
||||
e-mail address).
|
||||
</p>
|
||||
<p>
|
||||
We receive and store any information you knowingly provide to us
|
||||
when you create an account, publish content, make a purchase, or
|
||||
fill any online forms on the Services. When required, this
|
||||
information may include the following:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Account details (such as user name, unique user ID, password, etc)
|
||||
</li>
|
||||
<li>
|
||||
Contact information (such as email address, phone number, etc)
|
||||
</li>
|
||||
<li>
|
||||
Any other materials you willingly submit to us (such as articles,
|
||||
images, feedback, etc)
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Some of the information we collect is directly from you via the
|
||||
Services. However, we may also collect Personal Information about
|
||||
you from other sources such as public databases, social media
|
||||
platforms, third-party data providers, and our joint marketing
|
||||
partners. Personal Information we collect from other sources may
|
||||
include demographic information, such as age and gender, device
|
||||
information, such as IP addresses, location, such as city and state,
|
||||
and online behavioral data, such as information about your use of
|
||||
social media websites, page view information and search results and
|
||||
links.
|
||||
</p>
|
||||
<p>
|
||||
You can choose not to provide us with your Personal Information, but
|
||||
then you may not be able to take advantage of some of the features
|
||||
on the Services. Users who are uncertain about what information is
|
||||
mandatory are welcome to contact us.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Privacy of children</h2>
|
||||
<div>
|
||||
<p>
|
||||
We do not knowingly collect any Personal Information from children
|
||||
under the age of 13. If you are under the age of 13, please do not
|
||||
submit any Personal Information through the Services. If you have
|
||||
reason to believe that a child under the age of 13 has provided
|
||||
Personal Information to us through the Services, please contact us
|
||||
to request that we delete that child’s Personal Information from our
|
||||
Services.
|
||||
</p>
|
||||
<p>
|
||||
We encourage parents and legal guardians to monitor their children’s
|
||||
Internet usage and to help enforce this Policy by instructing their
|
||||
children never to provide Personal Information through the Services
|
||||
without their permission. We also ask that all parents and legal
|
||||
guardians overseeing the care of children take the necessary
|
||||
precautions to ensure that their children are instructed to never
|
||||
give out Personal Information when online without their permission.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Use and processing of collected information</h2>
|
||||
<div>
|
||||
<p>
|
||||
We act as a data controller and a data processor in terms of the
|
||||
GDPR when handling Personal Information, unless we have entered into
|
||||
a data processing agreement with you in which case you would be the
|
||||
data controller and we would be the data processor.
|
||||
</p>
|
||||
<p>
|
||||
Our role may also differ depending on the specific situation
|
||||
involving Personal Information. We act in the capacity of a data
|
||||
controller when we ask you to submit your Personal Information that
|
||||
is necessary to ensure your access and use of the Services. In such
|
||||
instances, we are a data controller because we determine the
|
||||
purposes and means of the processing of Personal Information and we
|
||||
comply with data controllers’ obligations set forth in the GDPR.
|
||||
</p>
|
||||
<p>
|
||||
We act in the capacity of a data processor in situations when you
|
||||
submit Personal Information through the Services. We do not own,
|
||||
control, or make decisions about the submitted Personal Information,
|
||||
and such Personal Information is processed only in accordance with
|
||||
your instructions. In such instances, the User providing Personal
|
||||
Information acts as a data controller in terms of the GDPR.
|
||||
</p>
|
||||
<p>
|
||||
In order to make the Services available to you, or to meet a legal
|
||||
obligation, we may need to collect and use certain Personal
|
||||
Information. If you do not provide the information that we request,
|
||||
we may not be able to provide you with the requested products or
|
||||
services. Any of the information we collect from you may be used for
|
||||
the following purposes:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Create and manage user accounts</li>
|
||||
<li>Deliver products or services</li>
|
||||
<li>Improve products and services</li>
|
||||
<li>Send product and service updates</li>
|
||||
<li>Respond to inquiries and offer support</li>
|
||||
<li>Request user feedback</li>
|
||||
<li>Improve user experience</li>
|
||||
<li>Protect from abuse and malicious users</li>
|
||||
<li>Run and operate the Services</li>
|
||||
</ul>
|
||||
<p>
|
||||
Processing your Personal Information depends on how you interact
|
||||
with the Services, where you are located in the world and if one of
|
||||
the following applies: (i) you have given your consent for one or
|
||||
more specific purposes; this, however, does not apply, whenever the
|
||||
processing of Personal Information is subject to California Consumer
|
||||
Privacy Act or European data protection law; (ii) provision of
|
||||
information is necessary for the performance of an agreement with
|
||||
you and/or for any pre-contractual obligations thereof; (iii)
|
||||
processing is necessary for compliance with a legal obligation to
|
||||
which you are subject; (iv) processing is related to a task that is
|
||||
carried out in the public interest or in the exercise of official
|
||||
authority vested in us; (v) processing is necessary for the purposes
|
||||
of the legitimate interests pursued by us or by a third party.
|
||||
</p>
|
||||
<p>
|
||||
We rely on the following legal bases as defined in the GDPR upon
|
||||
which we collect and process your Personal Information:
|
||||
</p>
|
||||
<ul>
|
||||
<li>User’s consent</li>
|
||||
<li>Performance of a contract</li>
|
||||
<li>Our own legitimate interests</li>
|
||||
</ul>
|
||||
<p>
|
||||
Note that under some legislations we may be allowed to process
|
||||
information until you object to such processing by opting out,
|
||||
without having to rely on consent or any other of the legal bases
|
||||
above. In any case, we will be happy to clarify the specific legal
|
||||
basis that applies to the processing, and in particular whether the
|
||||
provision of Personal Information is a statutory or contractual
|
||||
requirement, or a requirement necessary to enter into a contract.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Payment processing</h2>
|
||||
<div>
|
||||
<p>
|
||||
In case of Services requiring payment, you may need to provide your
|
||||
credit card details or other payment account information, which will
|
||||
be used solely for processing payments. We use third-party payment
|
||||
processors (“Payment Processors”) to assist us in processing your
|
||||
payment information securely.
|
||||
</p>
|
||||
<p>
|
||||
Payment Processors adhere to the latest security standards as
|
||||
managed by the PCI Security Standards Council, which is a joint
|
||||
effort of brands like Visa, MasterCard, American Express and
|
||||
Discover. Sensitive and private data exchange happens over a SSL
|
||||
secured communication channel and is encrypted and protected with
|
||||
digital signatures, and the Services are also in compliance with
|
||||
strict vulnerability standards in order to create as secure of an
|
||||
environment as possible for Users. We will share payment data with
|
||||
the Payment Processors only to the extent necessary for the purposes
|
||||
of processing your payments, refunding such payments, and dealing
|
||||
with complaints and queries related to such payments and refunds.
|
||||
</p>
|
||||
<p>
|
||||
Please note that the Payment Processors may collect some Personal
|
||||
Information from you, which allows them to process your payments
|
||||
(e.g., your email address, address, credit card details, and bank
|
||||
account number) and handle all the steps in the payment process
|
||||
through their systems, including data collection and data
|
||||
processing. The Payment Processors’ use of your Personal Information
|
||||
is governed by their respective privacy policies which may or may
|
||||
not contain privacy protections as protective as this Policy. We
|
||||
suggest that you review their respective privacy policies.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Managing information</h2>
|
||||
<div>
|
||||
<p>
|
||||
You are able to delete certain Personal Information we have about
|
||||
you. The Personal Information you can delete may change as the
|
||||
Services change. When you delete Personal Information, however, we
|
||||
may maintain a copy of the unrevised Personal Information in our
|
||||
records for the duration necessary to comply with our obligations to
|
||||
our affiliates and partners, and for the purposes described below.
|
||||
If you would like to delete your Personal Information or permanently
|
||||
delete your account, you can do so by contacting us.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Disclosure of information</h2>
|
||||
<div>
|
||||
<p>
|
||||
Depending on the requested Services or as necessary to complete any
|
||||
transaction or provide any Service you have requested, we may share
|
||||
your information with our affiliates, contracted companies, and
|
||||
service providers (collectively, “Service Providers”) we rely upon
|
||||
to assist in the operation of the Services available to you and
|
||||
whose privacy policies are consistent with ours or who agree to
|
||||
abide by our policies with respect to Personal Information. We will
|
||||
not share any personally identifiable information with third parties
|
||||
and will not share any information with unaffiliated third parties.
|
||||
</p>
|
||||
<p>
|
||||
Service Providers are not authorized to use or disclose your
|
||||
information except as necessary to perform services on our behalf or
|
||||
comply with legal requirements. Service Providers are given the
|
||||
information they need only in order to perform their designated
|
||||
functions, and we do not authorize them to use or disclose any of
|
||||
the provided information for their own marketing or other purposes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Retention of information</h2>
|
||||
<div>
|
||||
<p>
|
||||
We will retain and use your Personal Information for the period
|
||||
necessary to comply with our legal obligations, as long as your user
|
||||
account remains active, to enforce our agreements, resolve disputes,
|
||||
and unless a longer retention period is required or permitted by
|
||||
law.
|
||||
</p>
|
||||
<p>
|
||||
We may use any aggregated data derived from or incorporating your
|
||||
Personal Information after you update or delete it, but not in a
|
||||
manner that would identify you personally. Once the retention period
|
||||
expires, Personal Information shall be deleted. Therefore, the right
|
||||
to access, the right to erasure, the right to rectification, and the
|
||||
right to data portability cannot be enforced after the expiration of
|
||||
the retention period.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Transfer of information</h2>
|
||||
<div>
|
||||
<p>
|
||||
Depending on your location, data transfers may involve transferring
|
||||
and storing your information in a country other than your own. The
|
||||
transfer of your Personal Information to countries outside the
|
||||
European Union will be made only if you have explicitly consented to
|
||||
it or in the cases provided for by the GDPR and will be processed in
|
||||
your interest.
|
||||
</p>
|
||||
<p>
|
||||
You are entitled to learn about the legal basis of information
|
||||
transfers to a country outside the European Union or to any
|
||||
international organization governed by public international law or
|
||||
set up by two or more countries, such as the UN, and about the
|
||||
security measures taken by us to safeguard your information. If any
|
||||
such transfer takes place, you can find out more by checking the
|
||||
relevant sections of this Policy or inquire with us using the
|
||||
information provided in the contact section.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Data protection rights under the GDPR</h2>
|
||||
<div>
|
||||
<p>
|
||||
If you are a resident of the European Economic Area (“EEA”), you
|
||||
have certain data protection rights and we aim to take reasonable
|
||||
steps to allow you to correct, amend, delete, or limit the use of
|
||||
your Personal Information. If you wish to be informed what Personal
|
||||
Information we hold about you and if you want it to be removed from
|
||||
our systems, please contact us. In certain circumstances, you have
|
||||
the following data protection rights:
|
||||
</p>
|
||||
<p>
|
||||
(i) You have the right to withdraw consent where you have previously
|
||||
given your consent to the processing of your Personal Information.
|
||||
To the extent that the legal basis for our processing of your
|
||||
Personal Information is consent, you have the right to withdraw that
|
||||
consent at any time. Withdrawal will not affect the lawfulness of
|
||||
processing before the withdrawal.
|
||||
</p>
|
||||
<p>
|
||||
(ii) You have the right to learn if your Personal Information is
|
||||
being processed by us, obtain disclosure regarding certain aspects
|
||||
of the processing, and obtain a copy of your Personal Information
|
||||
undergoing processing.
|
||||
</p>
|
||||
<p>
|
||||
(iii) You have the right to verify the accuracy of your information
|
||||
and ask for it to be updated or corrected. You also have the right
|
||||
to request us to complete the Personal Information you believe is
|
||||
incomplete.
|
||||
</p>
|
||||
<p>
|
||||
(iv) You have the right to object to the processing of your
|
||||
information if the processing is carried out on a legal basis other
|
||||
than consent. Where Personal Information is processed for the public
|
||||
interest, in the exercise of an official authority vested in us, or
|
||||
for the purposes of the legitimate interests pursued by us, you may
|
||||
object to such processing by providing a ground related to your
|
||||
particular situation to justify the objection. You must know that,
|
||||
however, should your Personal Information be processed for direct
|
||||
marketing purposes, you can object to that processing at any time
|
||||
without providing any justification. To learn whether we are
|
||||
processing Personal Information for direct marketing purposes, you
|
||||
may refer to the relevant sections of this Policy.
|
||||
</p>
|
||||
<p>
|
||||
(v) You have the right, under certain circumstances, to restrict the
|
||||
processing of your Personal Information. These circumstances
|
||||
include: the accuracy of your Personal Information is contested by
|
||||
you and we must verify its accuracy; the processing is unlawful, but
|
||||
you oppose the erasure of your Personal Information and request the
|
||||
restriction of its use instead; we no longer need your Personal
|
||||
Information for the purposes of processing, but you require it to
|
||||
establish, exercise or defend your legal claims; you have objected
|
||||
to processing pending the verification of whether our legitimate
|
||||
grounds override your legitimate grounds. Where processing has been
|
||||
restricted, such Personal Information will be marked accordingly
|
||||
and, with the exception of storage, will be processed only with your
|
||||
consent or for the establishment, to exercise or defense of legal
|
||||
claims, for the protection of the rights of another natural, or
|
||||
legal person or for reasons of important public interest.
|
||||
</p>
|
||||
<p>
|
||||
(vi) You have the right, under certain circumstances, to obtain the
|
||||
erasure of your Personal Information from us. These circumstances
|
||||
include: the Personal Information is no longer necessary in relation
|
||||
to the purposes for which it was collected or otherwise processed;
|
||||
you withdraw consent to consent-based processing; you object to the
|
||||
processing under certain rules of applicable data protection law;
|
||||
the processing is for direct marketing purposes; and the personal
|
||||
data have been unlawfully processed. However, there are exclusions
|
||||
of the right to erasure such as where processing is necessary: for
|
||||
exercising the right of freedom of expression and information; for
|
||||
compliance with a legal obligation; or for the establishment, to
|
||||
exercise or defense of legal claims.
|
||||
</p>
|
||||
<p>
|
||||
(vii) You have the right to receive your Personal Information that
|
||||
you have provided to us in a structured, commonly used, and
|
||||
machine-readable format and, if technically feasible, to have it
|
||||
transmitted to another controller without any hindrance from us,
|
||||
provided that such transmission does not adversely affect the rights
|
||||
and freedoms of others.
|
||||
</p>
|
||||
<p>
|
||||
(viii) You have the right to complain to a data protection authority
|
||||
about our collection and use of your Personal Information. If you
|
||||
are not satisfied with the outcome of your complaint directly with
|
||||
us, you have the right to lodge a complaint with your local data
|
||||
protection authority. For more information, please contact your
|
||||
local data protection authority in the EEA. This provision is
|
||||
applicable provided that your Personal Information is processed by
|
||||
automated means and that the processing is based on your consent, on
|
||||
a contract which you are part of, or on pre-contractual obligations
|
||||
thereof.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>California privacy rights</h2>
|
||||
<div>
|
||||
<p>
|
||||
Consumers residing in California are afforded certain additional
|
||||
rights with respect to their Personal Information under the
|
||||
California Consumer Privacy Act (“CCPA”). If you are a California
|
||||
resident, this section applies to you.
|
||||
</p>
|
||||
<p>
|
||||
In addition to the rights as explained in this Policy, California
|
||||
residents who provide Personal Information as defined in the statute
|
||||
to obtain Services for personal, family, or household use are
|
||||
entitled to request and obtain from us, once a calendar year,
|
||||
information about the categories and specific pieces of Personal
|
||||
Information we have collected and disclosed.
|
||||
</p>
|
||||
<p>
|
||||
Furthermore, California residents have the right to request deletion
|
||||
of their Personal Information or opt-out of the sale of their
|
||||
Personal Information which may include selling, disclosing, or
|
||||
transferring Personal Information to another business or a third
|
||||
party for monetary or other valuable consideration. To do so, simply
|
||||
contact us. We will not discriminate against you if you exercise
|
||||
your rights under the CCPA.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>How to exercise your rights</h2>
|
||||
<div>
|
||||
<p>
|
||||
Any requests to exercise your rights can be directed to us through
|
||||
the contact details provided in this document. Please note that we
|
||||
may ask you to verify your identity before responding to such
|
||||
requests. Your request must provide sufficient information that
|
||||
allows us to verify that you are the person you are claiming to be
|
||||
or that you are the authorized representative of such person. If we
|
||||
receive your request from an authorized representative, we may
|
||||
request evidence that you have provided such an authorized
|
||||
representative with power of attorney or that the authorized
|
||||
representative otherwise has valid written authority to submit
|
||||
requests on your behalf.
|
||||
</p>
|
||||
<p>
|
||||
You must include sufficient details to allow us to properly
|
||||
understand the request and respond to it. We cannot respond to your
|
||||
request or provide you with Personal Information unless we first
|
||||
verify your identity or authority to make such a request and confirm
|
||||
that the Personal Information relates to you.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Cookies</h2>
|
||||
<div>
|
||||
<p>
|
||||
Our Services use “cookies” to help personalize your online
|
||||
experience. A cookie is a text file that is placed on your hard disk
|
||||
by a web page server. Cookies cannot be used to run programs or
|
||||
deliver viruses to your computer. Cookies are uniquely assigned to
|
||||
you, and can only be read by a web server in the domain that issued
|
||||
the cookie to you. If you choose to decline cookies, you may not be
|
||||
able to fully experience the features of the Services. You may learn
|
||||
more about cookies and how they work{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://www.websitepolicies.com/blog/cookies"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
We may use cookies to collect, store, and track information for
|
||||
security and personalization, to operate the Services, and for
|
||||
statistical purposes. Please note that you have the ability to
|
||||
accept or decline cookies. Most web browsers automatically accept
|
||||
cookies by default, but you can modify your browser settings to
|
||||
decline cookies if you prefer.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Data analytics</h2>
|
||||
<div>
|
||||
<p>
|
||||
Our Services may use third-party analytics tools that use cookies,
|
||||
web beacons, or other similar information-gathering technologies to
|
||||
collect standard internet activity and usage information. The
|
||||
information gathered is used to compile statistical reports on User
|
||||
activity such as how often Users visit our Services, what pages they
|
||||
visit and for how long, etc. We use the information obtained from
|
||||
these analytics tools to monitor the performance and improve our
|
||||
Services.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Do Not Track signals</h2>
|
||||
<div>
|
||||
<p>
|
||||
Some browsers incorporate a Do Not Track feature that signals to
|
||||
websites you visit that you do not want to have your online activity
|
||||
tracked. Tracking is not the same as using or collecting information
|
||||
in connection with a website. For these purposes, tracking refers to
|
||||
collecting personally identifiable information from consumers who
|
||||
use or visit a website or online service as they move across
|
||||
different websites over time. How browsers communicate the Do Not
|
||||
Track signal is not yet uniform. As a result, the Services are not
|
||||
yet set up to interpret or respond to Do Not Track signals
|
||||
communicated by your browser. Even so, as described in more detail
|
||||
throughout this Policy, we limit our use and collection of your
|
||||
Personal Information.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Advertisements</h2>
|
||||
<div>
|
||||
<p>
|
||||
We may display online advertisements and we may share aggregated and
|
||||
non-identifying information about our customers that we or our
|
||||
advertisers collect through your use of the Services. We do not
|
||||
share personally identifiable information about individual customers
|
||||
with advertisers. In some instances, we may use this aggregated and
|
||||
non-identifying information to deliver tailored advertisements to
|
||||
the intended audience.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Social media features</h2>
|
||||
<div>
|
||||
<p>
|
||||
Our Services may include social media features, such as the Facebook
|
||||
and Twitter buttons, Share This buttons, etc (collectively, “Social
|
||||
Media Features”). These Social Media Features may collect your IP
|
||||
address, what page you are visiting on our Services, and may set a
|
||||
cookie to enable Social Media Features to function properly. Social
|
||||
Media Features are hosted either by their respective providers or
|
||||
directly on our Services. Your interactions with these Social Media
|
||||
Features are governed by the privacy policy of their respective
|
||||
providers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Email marketing</h2>
|
||||
<div>
|
||||
<p>
|
||||
We offer electronic newsletters to which you may voluntarily
|
||||
subscribe at any time. We are committed to keeping your e-mail
|
||||
address confidential and will not disclose your email address to any
|
||||
third parties except as allowed in the information use and
|
||||
processing section or for the purposes of utilizing a third-party
|
||||
provider to send such emails. We will maintain the information sent
|
||||
via e-mail in accordance with applicable laws and regulations.
|
||||
</p>
|
||||
<p>
|
||||
In compliance with the CAN-SPAM Act, all e-mails sent from us will
|
||||
clearly state who the e-mail is from and provide clear information
|
||||
on how to contact the sender. You may choose to stop receiving our
|
||||
newsletter or marketing emails by following the unsubscribe
|
||||
instructions included in these emails or by contacting us. However,
|
||||
you will continue to receive essential transactional emails.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Affiliate links</h2>
|
||||
<div>
|
||||
<p>
|
||||
We may engage in affiliate marketing and have affiliate links
|
||||
present on the Services for the purpose of being able to offer you
|
||||
related or additional products and services. If you click on an
|
||||
affiliate link, a cookie will be placed on your browser to track any
|
||||
sales for purposes of commissions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Links to other resources</h2>
|
||||
<div>
|
||||
<p>
|
||||
The Services contain links to other resources that are not owned or
|
||||
controlled by us. Please be aware that we are not responsible for
|
||||
the privacy practices of such other resources or third parties. We
|
||||
encourage you to be aware when you leave the Services and to read
|
||||
the privacy statements of each and every resource that may collect
|
||||
Personal Information.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Information security</h2>
|
||||
<div>
|
||||
<p>
|
||||
We secure information you provide on computer servers in a
|
||||
controlled, secure environment, protected from unauthorized access,
|
||||
use, or disclosure. We maintain reasonable administrative,
|
||||
technical, and physical safeguards in an effort to protect against
|
||||
unauthorized access, use, modification, and disclosure of Personal
|
||||
Information in our control and custody. However, no data
|
||||
transmission over the Internet or wireless network can be
|
||||
guaranteed.
|
||||
</p>
|
||||
<p>
|
||||
Therefore, while we strive to protect your Personal Information, you
|
||||
acknowledge that (i) there are security and privacy limitations of
|
||||
the Internet which are beyond our control; (ii) the security,
|
||||
integrity, and privacy of any and all information and data exchanged
|
||||
between you and the Services cannot be guaranteed; and (iii) any
|
||||
such information and data may be viewed or tampered with in transit
|
||||
by a third party, despite best efforts.
|
||||
</p>
|
||||
<p>
|
||||
As the security of Personal Information depends in part on the
|
||||
security of the device you use to communicate with us and the
|
||||
security you use to protect your credentials, please take
|
||||
appropriate measures to protect this information.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Data breach</h2>
|
||||
<div>
|
||||
<p>
|
||||
In the event we become aware that the security of the Services has
|
||||
been compromised or Users’ Personal Information has been disclosed
|
||||
to unrelated third parties as a result of external activity,
|
||||
including, but not limited to, security attacks or fraud, we reserve
|
||||
the right to take reasonably appropriate measures, including, but
|
||||
not limited to, investigation and reporting, as well as notification
|
||||
to and cooperation with law enforcement authorities. In the event of
|
||||
a data breach, we will make reasonable efforts to notify affected
|
||||
individuals if we believe that there is a reasonable risk of harm to
|
||||
the User as a result of the breach or if notice is otherwise
|
||||
required by law. When we do, we will send you an email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Changes and amendments</h2>
|
||||
<div>
|
||||
<p>
|
||||
We reserve the right to modify this Policy or its terms related to
|
||||
the Services at any time at our discretion. When we do, we will
|
||||
revise the updated date at the bottom of this page. We may also
|
||||
provide notice to you in other ways at our discretion, such as
|
||||
through the contact information you have provided.
|
||||
</p>
|
||||
<p>
|
||||
An updated version of this Policy will be effective immediately upon
|
||||
the posting of the revised Policy unless otherwise specified. Your
|
||||
continued use of the Services after the effective date of the
|
||||
revised Policy (or such other act specified at that time) will
|
||||
constitute your consent to those changes. However, we will not,
|
||||
without your consent, use your Personal Information in a manner
|
||||
materially different than what was stated at the time your Personal
|
||||
Information was collected.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Acceptance of this policy</h2>
|
||||
<div>
|
||||
<p>
|
||||
You acknowledge that you have read this Policy and agree to all its
|
||||
terms and conditions. By accessing and using the Services and
|
||||
submitting your information you agree to be bound by this Policy. If
|
||||
you do not agree to abide by the terms of this Policy, you are not
|
||||
authorized to access or use the Services.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Contacting us</h2>
|
||||
<div>
|
||||
<p>
|
||||
If you have any questions, concerns, or complaints regarding this
|
||||
Policy, the information we hold about you, or if you wish to
|
||||
exercise your rights, we encourage you to contact us using the
|
||||
details below:
|
||||
</p>
|
||||
<p>info@gdevs.io</p>
|
||||
<p>
|
||||
We will attempt to resolve complaints and disputes and make every
|
||||
reasonable effort to honor your wish to exercise your rights as
|
||||
quickly as possible and in any event, within the timescales provided
|
||||
by applicable data protection laws.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>This document was last updated on August 16, 2021</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PrivacyPolicy);
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
const RouteBackground = () => {
|
||||
return (
|
||||
<div
|
||||
css={`
|
||||
position: absolute;
|
||||
background: ${props => props.theme.palette.secondary.main};
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RouteBackground;
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router';
|
||||
|
||||
function RouteWithSubRoutes(route) {
|
||||
return (
|
||||
<Route
|
||||
// eslint-disable-next-line react/destructuring-assignment
|
||||
path={route.path}
|
||||
render={props => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<route.component {...props} routes={route.routes} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default RouteWithSubRoutes;
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
faDiscord,
|
||||
faFacebook,
|
||||
faGithub,
|
||||
faInstagram,
|
||||
faTwitter
|
||||
} from '@fortawesome/free-brands-svg-icons';
|
||||
import { faGlobe } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
const SocialButtons = () => {
|
||||
return (
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-right: 30px;
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
div {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
transition: background 0.1s ease-in-out, transform 0.1s ease-in-out;
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-3px);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
div:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<a href="https://discord.gdlauncher.com">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faDiscord} size="lg" />
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://github.com/gorilla-devs/GDLauncher">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faGithub} size="lg" />
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://twitter.com/gdlauncher">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faTwitter} size="lg" />
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://facebook.com/gorilladevs">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faFacebook} size="lg" />
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://instagram.com/gdlauncher">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faInstagram} size="lg" />
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://gdevs.io">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faGlobe} size="lg" />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SocialButtons);
|
||||
@@ -0,0 +1,420 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
const TermsAndConditions = () => {
|
||||
return (
|
||||
<>
|
||||
<h1>Terms and Conditions</h1>
|
||||
<div>
|
||||
<h2>Introduction</h2>
|
||||
<div>
|
||||
<p>
|
||||
These terms and conditions (“Agreement”) set forth the general terms
|
||||
and conditions of your use of the{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
href="https://gdlauncher.com"
|
||||
>
|
||||
gdlauncher.com
|
||||
</a>{' '}
|
||||
website (“Website”), “GDLauncher” Application (“Application”) and
|
||||
any of their related products and services (collectively,
|
||||
“Services”). This Agreement is legally binding between you (“User”,
|
||||
“you” or “your”) and this Website operator and Application developer
|
||||
(“Operator”, “we”, “us” or “our”). If you are entering into this
|
||||
agreement on behalf of a business or other legal entity, you
|
||||
represent that you have the authority to bind such entity to this
|
||||
agreement, in which case the terms “User”, “you” or “your” shall
|
||||
refer to such entity. If you do not have such authority, or if you
|
||||
do not agree with the terms of this agreement, you must not accept
|
||||
this agreement and may not access and use the Services. By accessing
|
||||
and using the Services, you acknowledge that you have read,
|
||||
understood, and agree to be bound by the terms of this Agreement.
|
||||
You acknowledge that this Agreement is a contract between you and
|
||||
the Operator, even though it is electronic and is not physically
|
||||
signed by you, and it governs your use of the Services.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Accounts and membership</h2>
|
||||
<div>
|
||||
<p>
|
||||
If you create an account on the Services, you are responsible for
|
||||
maintaining the security of your account and you are fully
|
||||
responsible for all activities that occur under the account and any
|
||||
other actions taken in connection with it. We may, but have no
|
||||
obligation to, monitor and review new accounts before you may sign
|
||||
in and start using the Services. Providing false contact information
|
||||
of any kind may result in the termination of your account. You must
|
||||
immediately notify us of any unauthorized uses of your account or
|
||||
any other breaches of security. We will not be liable for any acts
|
||||
or omissions by you, including any damages of any kind incurred as a
|
||||
result of such acts or omissions. We may suspend, disable, or delete
|
||||
your account (or any part thereof) if we determine that you have
|
||||
violated any provision of this Agreement or that your conduct or
|
||||
content would tend to damage our reputation and goodwill. If we
|
||||
delete your account for the foregoing reasons, you may not
|
||||
re-register for our Services. We may block your email address and
|
||||
Internet protocol address to prevent further registration.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>User content</h2>
|
||||
<div>
|
||||
<p>
|
||||
We do not own any data, information or material (collectively,
|
||||
“Content”) that you submit on the Services in the course of using
|
||||
the Service. You shall have sole responsibility for the accuracy,
|
||||
quality, integrity, legality, reliability, appropriateness, and
|
||||
intellectual property ownership or right to use of all submitted
|
||||
Content. We may, but have no obligation to, monitor and review the
|
||||
Content on the Services submitted or created using our Services by
|
||||
you. You grant us permission to access, copy, distribute, store,
|
||||
transmit, reformat, display and perform the Content of your user
|
||||
account solely as required for the purpose of providing the Services
|
||||
to you. Without limiting any of those representations or warranties,
|
||||
we have the right, though not the obligation, to, in our own sole
|
||||
discretion, refuse or remove any Content that, in our reasonable
|
||||
opinion, violates any of our policies or is in any way harmful or
|
||||
objectionable. You also grant us the license to use, reproduce,
|
||||
adapt, modify, publish or distribute the Content created by you or
|
||||
stored in your user account for commercial, marketing or any similar
|
||||
purpose.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Billing and payments</h2>
|
||||
<div>
|
||||
<p>
|
||||
You shall pay all fees or charges to your account in accordance with
|
||||
the fees, charges, and billing terms in effect at the time a fee or
|
||||
charge is due and payable. Where Services are offered on a free
|
||||
trial basis, payment may be required after the free trial period
|
||||
ends, and not when you enter your billing details (which may be
|
||||
required prior to the commencement of the free trial period). If
|
||||
auto-renewal is enabled for the Services you have subscribed for,
|
||||
you will be charged automatically in accordance with the term you
|
||||
selected. If, in our judgment, your purchase constitutes a high-risk
|
||||
transaction, we will require you to provide us with a copy of your
|
||||
valid government-issued photo identification, and possibly a copy of
|
||||
a recent bank statement for the credit or debit card used for the
|
||||
purchase. We reserve the right to change products and product
|
||||
pricing at any time. We also reserve the right to refuse any order
|
||||
you place with us. We may, in our sole discretion, limit or cancel
|
||||
quantities purchased per person, per household or per order. These
|
||||
restrictions may include orders placed by or under the same customer
|
||||
account, the same credit card, and/or orders that use the same
|
||||
billing and/or shipping address. In the event that we make a change
|
||||
to or cancel an order, we may attempt to notify you by contacting
|
||||
the e-mail and/or billing address/phone number provided at the time
|
||||
the order was made.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Accuracy of information</h2>
|
||||
<div>
|
||||
<p>
|
||||
Occasionally there may be information on the Services that contains
|
||||
typographical errors, inaccuracies or omissions that may relate to
|
||||
product descriptions, pricing, availability, promotions and offers.
|
||||
We reserve the right to correct any errors, inaccuracies or
|
||||
omissions, and to change or update information or cancel orders if
|
||||
any information on the Services or Services is inaccurate at any
|
||||
time without prior notice (including after you have submitted your
|
||||
order). We undertake no obligation to update, amend or clarify
|
||||
information on the Services including, without limitation, pricing
|
||||
information, except as required by law. No specified update or
|
||||
refresh date applied on the Services should be taken to indicate
|
||||
that all information on the Services or Services has been modified
|
||||
or updated.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Third party services</h2>
|
||||
<div>
|
||||
<p>
|
||||
If you decide to enable, access or use third party services, be
|
||||
advised that your access and use of such other services are governed
|
||||
solely by the terms and conditions of such other services, and we do
|
||||
not endorse, are not responsible or liable for, and make no
|
||||
representations as to any aspect of such other services, including,
|
||||
without limitation, their content or the manner in which they handle
|
||||
data (including your data) or any interaction between you and the
|
||||
provider of such other services. You irrevocably waive any claim
|
||||
against the Operator with respect to such other services. The
|
||||
Operator is not liable for any damage or loss caused or alleged to
|
||||
be caused by or in connection with your enablement, access or use of
|
||||
any such other services, or your reliance on the privacy practices,
|
||||
data security processes or other policies of such other services.
|
||||
You may be required to register for or log into such other services
|
||||
on their respective platforms. By enabling any other services, you
|
||||
are expressly permitting the Operator to disclose your data as
|
||||
necessary to facilitate the use or enablement of such other service.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Backups</h2>
|
||||
<div>
|
||||
<p>
|
||||
We are not responsible for the Content residing on the Services. In
|
||||
no event shall we be held liable for any loss of any Content. It is
|
||||
your sole responsibility to maintain appropriate backup of your
|
||||
Content. Notwithstanding the foregoing, on some occasions and in
|
||||
certain circumstances, with absolutely no obligation, we may be able
|
||||
to restore some or all of your data that has been deleted as of a
|
||||
certain date and time when we may have backed up data for our own
|
||||
purposes. We make no guarantee that the data you need will be
|
||||
available.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Advertisements</h2>
|
||||
<div>
|
||||
<p>
|
||||
During your use of the Services, you may enter into correspondence
|
||||
with or participate in promotions of advertisers or sponsors showing
|
||||
their goods or services through the Services. Any such activity, and
|
||||
any terms, conditions, warranties or representations associated with
|
||||
such activity, is solely between you and the applicable third party.
|
||||
We shall have no liability, obligation or responsibility for any
|
||||
such correspondence, purchase or promotion between you and any such
|
||||
third party.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Links to other resources</h2>
|
||||
<div>
|
||||
<p>
|
||||
Although the Services may link to other resources (such as websites,
|
||||
Applications, etc.), we are not, directly or indirectly, implying
|
||||
any approval, association, sponsorship, endorsement, or affiliation
|
||||
with any linked resource, unless specifically stated herein. Some of
|
||||
the links on the Services may be “affiliate links”. This means if
|
||||
you click on the link and purchase an item, the Operator will
|
||||
receive an affiliate commission. We are not responsible for
|
||||
examining or evaluating, and we do not warrant the offerings of, any
|
||||
businesses or individuals or the content of their resources. We do
|
||||
not assume any responsibility or liability for the actions,
|
||||
products, services, and content of any other third parties. You
|
||||
should carefully review the legal statements and other conditions of
|
||||
use of any resource which you access through a link on the Services.
|
||||
Your linking to any other off-site resources is at your own risk.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Prohibited uses</h2>
|
||||
<div>
|
||||
<p>
|
||||
In addition to other terms as set forth in the Agreement, you are
|
||||
prohibited from using the Services or Content: (a) for any unlawful
|
||||
purpose; (b) to solicit others to perform or participate in any
|
||||
unlawful acts; (c) to violate any international, federal, provincial
|
||||
or state regulations, rules, laws, or local ordinances; (d) to
|
||||
infringe upon or violate our intellectual property rights or the
|
||||
intellectual property rights of others; (e) to harass, abuse,
|
||||
insult, harm, defame, slander, disparage, intimidate, or
|
||||
discriminate based on gender, sexual orientation, religion,
|
||||
ethnicity, race, age, national origin, or disability; (f) to submit
|
||||
false or misleading information; (g) to upload or transmit viruses
|
||||
or any other type of malicious code that will or may be used in any
|
||||
way that will affect the functionality or operation of the Services,
|
||||
third party products and services, or the Internet; (h) to spam,
|
||||
phish, pharm, pretext, spider, crawl, or scrape; (i) for any obscene
|
||||
or immoral purpose; or (j) to interfere with or circumvent the
|
||||
security features of the Services, third party products and
|
||||
services, or the Internet. We reserve the right to terminate your
|
||||
use of the Services for violating any of the prohibited uses.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Intellectual property rights</h2>
|
||||
<div>
|
||||
<p>
|
||||
“Intellectual Property Rights” means all present and future rights
|
||||
conferred by statute, common law or equity in or in relation to any
|
||||
copyright and related rights, trademarks, designs, patents,
|
||||
inventions, goodwill and the right to sue for passing off, rights to
|
||||
inventions, rights to use, and all other intellectual property
|
||||
rights, in each case whether registered or unregistered and
|
||||
including all applications and rights to apply for and be granted,
|
||||
rights to claim priority from, such rights and all similar or
|
||||
equivalent rights or forms of protection and any other results of
|
||||
intellectual activity which subsist or will subsist now or in the
|
||||
future in any part of the world. This Agreement does not transfer to
|
||||
you any intellectual property owned by the Operator or third
|
||||
parties, and all rights, titles, and interests in and to such
|
||||
property will remain (as between the parties) solely with the
|
||||
Operator. All trademarks, service marks, graphics and logos used in
|
||||
connection with the Services, are trademarks or registered
|
||||
trademarks of the Operator or its licensors. Other trademarks,
|
||||
service marks, graphics and logos used in connection with the
|
||||
Services may be the trademarks of other third parties. Your use of
|
||||
the Services grants you no right or license to reproduce or
|
||||
otherwise use any of the Operator or third party trademarks.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Disclaimer of warranty</h2>
|
||||
<div>
|
||||
<p>
|
||||
You agree that such Service is provided on an “as is” and “as
|
||||
available” basis and that your use of the Services is solely at your
|
||||
own risk. We expressly disclaim all warranties of any kind, whether
|
||||
express or implied, including but not limited to the implied
|
||||
warranties of merchantability, fitness for a particular purpose and
|
||||
non-infringement. We make no warranty that the Services will meet
|
||||
your requirements, or that the Service will be uninterrupted,
|
||||
timely, secure, or error-free; nor do we make any warranty as to the
|
||||
results that may be obtained from the use of the Service or as to
|
||||
the accuracy or reliability of any information obtained through the
|
||||
Service or that defects in the Service will be corrected. You
|
||||
understand and agree that any material and/or data downloaded or
|
||||
otherwise obtained through the use of Service is done at your own
|
||||
discretion and risk and that you will be solely responsible for any
|
||||
damage or loss of data that results from the download of such
|
||||
material and/or data. We make no warranty regarding any goods or
|
||||
services purchased or obtained through the Service or any
|
||||
transactions entered into through the Service unless stated
|
||||
otherwise. No advice or information, whether oral or written,
|
||||
obtained by you from us or through the Service shall create any
|
||||
warranty not expressly made herein.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Limitation of liability</h2>
|
||||
<div>
|
||||
<p>
|
||||
To the fullest extent permitted by applicable law, in no event will
|
||||
the Operator, its affiliates, directors, officers, employees,
|
||||
agents, suppliers or licensors be liable to any person for any
|
||||
indirect, incidental, special, punitive, cover or consequential
|
||||
damages (including, without limitation, damages for lost profits,
|
||||
revenue, sales, goodwill, use of content, impact on business,
|
||||
business interruption, loss of anticipated savings, loss of business
|
||||
opportunity) however caused, under any theory of liability,
|
||||
including, without limitation, contract, tort, warranty, breach of
|
||||
statutory duty, negligence or otherwise, even if the liable party
|
||||
has been advised as to the possibility of such damages or could have
|
||||
foreseen such damages. To the maximum extent permitted by applicable
|
||||
law, the aggregate liability of the Operator and its affiliates,
|
||||
officers, employees, agents, suppliers and licensors relating to the
|
||||
services will be limited to an amount greater of one dollar or any
|
||||
amounts actually paid in cash by you to the Operator for the prior
|
||||
one month period prior to the first event or occurrence giving rise
|
||||
to such liability. The limitations and exclusions also apply if this
|
||||
remedy does not fully compensate you for any losses or fails of its
|
||||
essential purpose.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Indemnification</h2>
|
||||
<div>
|
||||
<p>
|
||||
You agree to indemnify and hold the Operator and its affiliates,
|
||||
directors, officers, employees, agents, suppliers and licensors
|
||||
harmless from and against any liabilities, losses, damages or costs,
|
||||
including reasonable attorneys’ fees, incurred in connection with or
|
||||
arising from any third party allegations, claims, actions, disputes,
|
||||
or demands asserted against any of them as a result of or relating
|
||||
to your Content, your use of the Services or any willful misconduct
|
||||
on your part.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Severability</h2>
|
||||
<div>
|
||||
<p>
|
||||
All rights and restrictions contained in this Agreement may be
|
||||
exercised and shall be applicable and binding only to the extent
|
||||
that they do not violate any applicable laws and are intended to be
|
||||
limited to the extent necessary so that they will not render this
|
||||
Agreement illegal, invalid or unenforceable. If any provision or
|
||||
portion of any provision of this Agreement shall be held to be
|
||||
illegal, invalid or unenforceable by a court of competent
|
||||
jurisdiction, it is the intention of the parties that the remaining
|
||||
provisions or portions thereof shall constitute their agreement with
|
||||
respect to the subject matter hereof, and all such remaining
|
||||
provisions or portions thereof shall remain in full force and
|
||||
effect.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Dispute resolution</h2>
|
||||
<div>
|
||||
<p>
|
||||
The formation, interpretation, and performance of this Agreement and
|
||||
any disputes arising out of it shall be governed by the substantive
|
||||
and procedural laws of Italy without regard to its rules on
|
||||
conflicts or choice of law and, to the extent applicable, the laws
|
||||
of Italy. The exclusive jurisdiction and venue for actions related
|
||||
to the subject matter hereof shall be the courts located in Italy,
|
||||
and you hereby submit to the personal jurisdiction of such courts.
|
||||
You hereby waive any right to a jury trial in any proceeding arising
|
||||
out of or related to this Agreement. The United Nations Convention
|
||||
on Contracts for the International Sale of Goods does not apply to
|
||||
this Agreement.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Changes and amendments</h2>
|
||||
<div>
|
||||
<p>
|
||||
We reserve the right to modify this Agreement or its terms related
|
||||
to the Services at any time at our discretion. When we do, we will
|
||||
revise the updated date at the bottom of this page. We may also
|
||||
provide notice to you in other ways at our discretion, such as
|
||||
through the contact information you have provided.
|
||||
</p>
|
||||
<p>
|
||||
An updated version of this Agreement will be effective immediately
|
||||
upon the posting of the revised Agreement unless otherwise
|
||||
specified. Your continued use of the Services after the effective
|
||||
date of the revised Agreement (or such other act specified at that
|
||||
time) will constitute your consent to those changes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Acceptance of these terms</h2>
|
||||
<div>
|
||||
<p>
|
||||
You acknowledge that you have read this Agreement and agree to all
|
||||
its terms and conditions. By accessing and using the Services you
|
||||
agree to be bound by this Agreement. If you do not agree to abide by
|
||||
the terms of this Agreement, you are not authorized to access or use
|
||||
the Services.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Contacting us</h2>
|
||||
<div>
|
||||
<p>
|
||||
If you have any questions, concerns, or complaints regarding this
|
||||
Agreement, we encourage you to contact us using the details below:
|
||||
</p>
|
||||
<p>info@gdevs.io</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>This document was last updated on August 16, 2021</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(TermsAndConditions);
|
||||
@@ -0,0 +1,210 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Spin, message } from 'antd';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import Modal from '../components/Modal';
|
||||
import { _getAccounts, _getCurrentAccount } from '../utils/selectors';
|
||||
import { openModal, closeModal } from '../reducers/modals/actions';
|
||||
import {
|
||||
updateCurrentAccountId,
|
||||
loginWithAccessToken,
|
||||
updateAccount,
|
||||
removeAccount,
|
||||
loginWithOAuthAccessToken
|
||||
} from '../reducers/actions';
|
||||
import { load } from '../reducers/loading/actions';
|
||||
import features from '../reducers/loading/features';
|
||||
import { ACCOUNT_MICROSOFT } from '../utils/constants';
|
||||
|
||||
const ProfileSettings = () => {
|
||||
const dispatch = useDispatch();
|
||||
const accounts = useSelector(_getAccounts);
|
||||
const currentAccount = useSelector(_getCurrentAccount);
|
||||
const isLoading = useSelector(state => state.loading.accountAuthentication);
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 70%;
|
||||
width: 400px;
|
||||
max-height: 700px;
|
||||
`}
|
||||
title="Account Manager"
|
||||
>
|
||||
<Container>
|
||||
<AccountsContainer>
|
||||
{accounts.map(account => {
|
||||
if (!account || !currentAccount) return;
|
||||
return (
|
||||
<AccountContainer key={account.selectedProfile.id}>
|
||||
<AccountItem
|
||||
active={
|
||||
account.selectedProfile.id ===
|
||||
currentAccount.selectedProfile.id
|
||||
}
|
||||
onClick={() => {
|
||||
if (
|
||||
isLoading.isRequesting ||
|
||||
account.selectedProfile.id ===
|
||||
currentAccount.selectedProfile.id ||
|
||||
!account.accessToken
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const currentId = currentAccount.selectedProfile.id;
|
||||
dispatch(
|
||||
updateCurrentAccountId(account.selectedProfile.id)
|
||||
);
|
||||
dispatch(
|
||||
load(
|
||||
features.mcAuthentication,
|
||||
dispatch(
|
||||
account.accountType === ACCOUNT_MICROSOFT
|
||||
? loginWithOAuthAccessToken(false)
|
||||
: loginWithAccessToken(false)
|
||||
)
|
||||
)
|
||||
).catch(() => {
|
||||
dispatch(updateCurrentAccountId(currentId));
|
||||
dispatch(
|
||||
updateAccount(account.selectedProfile.id, {
|
||||
...account,
|
||||
accessToken: null
|
||||
})
|
||||
);
|
||||
message.error('Account not valid');
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{account.selectedProfile.name}{' '}
|
||||
<span
|
||||
css={`
|
||||
color: ${props => props.theme.palette.error.main};
|
||||
`}
|
||||
>
|
||||
{!account.accessToken && '(EXPIRED)'}
|
||||
</span>
|
||||
</div>
|
||||
{!account.accessToken && (
|
||||
<HoverContainer
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
openModal('AddAccount', {
|
||||
username: account.user.username
|
||||
})
|
||||
)
|
||||
}
|
||||
>
|
||||
Login again
|
||||
</HoverContainer>
|
||||
)}
|
||||
{account.selectedProfile.id ===
|
||||
currentAccount.selectedProfile.id && (
|
||||
<Spin spinning={isLoading.isRequesting} />
|
||||
)}
|
||||
</AccountItem>
|
||||
<div
|
||||
css={`
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: color 0.1s ease-in-out;
|
||||
&:hover {
|
||||
color: ${props => props.theme.palette.error.main};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
onClick={async () => {
|
||||
const result = await dispatch(
|
||||
removeAccount(account.selectedProfile.id)
|
||||
);
|
||||
if (!result) {
|
||||
dispatch(closeModal());
|
||||
}
|
||||
}}
|
||||
icon={faTrash}
|
||||
/>
|
||||
</div>
|
||||
</AccountContainer>
|
||||
);
|
||||
})}
|
||||
</AccountsContainer>
|
||||
<AccountContainer>
|
||||
<AccountItem onClick={() => dispatch(openModal('AddAccount'))}>
|
||||
Add Account
|
||||
</AccountItem>
|
||||
</AccountContainer>
|
||||
</Container>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileSettings;
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: space-between;
|
||||
`;
|
||||
|
||||
const AccountItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
${props =>
|
||||
props.active ? `background: ${props.theme.palette.primary.main};` : ''}
|
||||
transition: background 0.1s ease-in-out;
|
||||
&:hover {
|
||||
${props =>
|
||||
props.active ? '' : `background: ${props.theme.palette.grey[600]};`}
|
||||
}
|
||||
`;
|
||||
|
||||
const HoverContainer = styled.div`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
border-radius: 4px;
|
||||
transition: opacity 150ms ease-in-out;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
backdrop-filter: blur(4px);
|
||||
will-change: opacity;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const AccountsContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding-right: 2px;
|
||||
`;
|
||||
|
||||
const AccountContainer = styled.div`
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import { Button } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Modal from '../components/Modal';
|
||||
import { closeModal } from '../reducers/modals/actions';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const Buttons = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const applyChoice = async (
|
||||
choiceType,
|
||||
callback,
|
||||
fileName,
|
||||
dispatch,
|
||||
delay = 500
|
||||
) => {
|
||||
if (choiceType === 'abort') {
|
||||
if (callback) {
|
||||
callback();
|
||||
setTimeout(() => dispatch(closeModal()), delay);
|
||||
} else dispatch(closeModal());
|
||||
} else {
|
||||
callback(fileName);
|
||||
setTimeout(() => dispatch(closeModal()), delay);
|
||||
}
|
||||
};
|
||||
|
||||
export default function ActionConfirmation({
|
||||
confirmCallback,
|
||||
abortCallback = () => {},
|
||||
message,
|
||||
fileName,
|
||||
delay,
|
||||
title
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 40%;
|
||||
width: 50%;
|
||||
max-width: 550px;
|
||||
max-height: 260px;
|
||||
overflow: hidden;
|
||||
`}
|
||||
title={title}
|
||||
closeCallback={abortCallback}
|
||||
>
|
||||
<Container>
|
||||
{message}
|
||||
<Buttons>
|
||||
<Button
|
||||
onClick={() => {
|
||||
applyChoice('abort', abortCallback, fileName, dispatch, delay);
|
||||
}}
|
||||
>
|
||||
Abort
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
applyChoice('confirm', confirmCallback, fileName, dispatch, delay)
|
||||
}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</Buttons>
|
||||
</Container>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Input, Button, Menu } from 'antd';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import Modal from '../components/Modal';
|
||||
import { load } from '../reducers/loading/actions';
|
||||
import features from '../reducers/loading/features';
|
||||
import { login, loginOAuth } from '../reducers/actions';
|
||||
import { closeModal } from '../reducers/modals/actions';
|
||||
import { ACCOUNT_MICROSOFT, ACCOUNT_MOJANG } from '../utils/constants';
|
||||
|
||||
const AddAccount = ({ username }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [email, setEmail] = useState(username || '');
|
||||
const [password, setPassword] = useState('');
|
||||
const [accountType, setAccountType] = useState(ACCOUNT_MOJANG);
|
||||
const [loginFailed, setloginFailed] = useState();
|
||||
|
||||
const addAccount = () => {
|
||||
dispatch(
|
||||
load(features.mcAuthentication, dispatch(login(email, password, false)))
|
||||
)
|
||||
.then(() => dispatch(closeModal()))
|
||||
.catch(console.error);
|
||||
};
|
||||
|
||||
const addMicrosoftAccount = () => {
|
||||
dispatch(load(features.mcAuthentication, dispatch(loginOAuth(false))))
|
||||
.then(() => dispatch(closeModal()))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
setloginFailed(error);
|
||||
});
|
||||
};
|
||||
|
||||
const renderAddMojangAccount = () => (
|
||||
<Container>
|
||||
<FormContainer>
|
||||
<h1
|
||||
css={`
|
||||
height: 80px;
|
||||
`}
|
||||
>
|
||||
Mojang Login
|
||||
</h1>
|
||||
<StyledInput
|
||||
disabled={!!username}
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
/>
|
||||
<StyledInput
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
</FormContainer>
|
||||
<FormContainer>
|
||||
<StyledButton onClick={addAccount}>Add Account</StyledButton>
|
||||
</FormContainer>
|
||||
</Container>
|
||||
);
|
||||
|
||||
const renderAddMicrosoftAccount = () => (
|
||||
<Container>
|
||||
<FormContainer>
|
||||
<h1
|
||||
css={`
|
||||
height: 80px;
|
||||
`}
|
||||
>
|
||||
Microsoft Login
|
||||
</h1>
|
||||
<FormContainer>
|
||||
<h2>External Login</h2>
|
||||
{loginFailed ? (
|
||||
<>
|
||||
<LoginFailMessage>{loginFailed?.message}</LoginFailMessage>
|
||||
<StyledButton
|
||||
css={`
|
||||
margin-top: 12px;
|
||||
`}
|
||||
onClick={addMicrosoftAccount}
|
||||
>
|
||||
Retry
|
||||
</StyledButton>
|
||||
</>
|
||||
) : (
|
||||
<FontAwesomeIcon spin size="3x" icon={faSpinner} />
|
||||
)}
|
||||
</FormContainer>
|
||||
</FormContainer>
|
||||
</Container>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 450px;
|
||||
width: 420px;
|
||||
`}
|
||||
title=" "
|
||||
>
|
||||
<Container>
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
selectedKeys={[accountType]}
|
||||
overflowedIndicator={null}
|
||||
>
|
||||
<StyledAccountMenuItem
|
||||
key={ACCOUNT_MOJANG}
|
||||
onClick={() => setAccountType(ACCOUNT_MOJANG)}
|
||||
>
|
||||
Mojang Account
|
||||
</StyledAccountMenuItem>
|
||||
<StyledAccountMenuItem
|
||||
key={ACCOUNT_MICROSOFT}
|
||||
onClick={() => {
|
||||
setAccountType(ACCOUNT_MICROSOFT);
|
||||
addMicrosoftAccount();
|
||||
}}
|
||||
>
|
||||
Microsoft Account
|
||||
</StyledAccountMenuItem>
|
||||
</Menu>
|
||||
{accountType === ACCOUNT_MOJANG ? renderAddMojangAccount() : null}
|
||||
{accountType === ACCOUNT_MICROSOFT ? renderAddMicrosoftAccount() : null}
|
||||
</Container>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAccount;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
width: 40%;
|
||||
`;
|
||||
|
||||
const StyledInput = styled(Input)`
|
||||
margin-bottom: 20px !important;
|
||||
`;
|
||||
|
||||
const LoginFailMessage = styled.div`
|
||||
color: ${props => props.theme.palette.colors.red};
|
||||
`;
|
||||
|
||||
const StyledAccountMenuItem = styled(Menu.Item)`
|
||||
width: auto;
|
||||
height: auto;
|
||||
font-size: 18px;
|
||||
`;
|
||||
|
||||
const FormContainer = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: space-between;
|
||||
justify-content: center;
|
||||
`;
|
||||
@@ -0,0 +1,209 @@
|
||||
/* eslint-disable */
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Transition } from 'react-transition-group';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faLongArrowAltRight,
|
||||
faArchive
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Spin, Radio } from 'antd';
|
||||
import CurseForgeModpacks from './CurseForgeModpacks';
|
||||
import Import from './Import';
|
||||
import NewInstance from './NewInstance';
|
||||
import minecraftIcon from '../../assets/minecraftIcon.png';
|
||||
import curseForgeIcon from '../../assets/curseforgeIcon.webp';
|
||||
|
||||
const Content = ({
|
||||
in: inProp,
|
||||
setStep,
|
||||
page,
|
||||
setPage,
|
||||
setVersion,
|
||||
version,
|
||||
setModpack,
|
||||
importZipPath,
|
||||
setImportZipPath
|
||||
}) => {
|
||||
const [overrideNextStepOnClick, setOverrideNextStepOnClick] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
let pages = [
|
||||
<NewInstance setVersion={setVersion} setModpack={setModpack} />,
|
||||
<CurseForgeModpacks
|
||||
setVersion={setVersion}
|
||||
setStep={setStep}
|
||||
setModpack={setModpack}
|
||||
/>,
|
||||
<Import
|
||||
setVersion={setVersion}
|
||||
setModpack={setModpack}
|
||||
importZipPath={importZipPath}
|
||||
setImportZipPath={setImportZipPath}
|
||||
setOverrideNextStepOnClick={setOverrideNextStepOnClick}
|
||||
/>
|
||||
];
|
||||
|
||||
return (
|
||||
<Transition in={inProp} timeout={200}>
|
||||
{state => (
|
||||
<Animation state={state}>
|
||||
<div
|
||||
css={`
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
display: flex;
|
||||
margin: 20px;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
flex: 5;
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
`}
|
||||
>
|
||||
<Radio.Group
|
||||
defaultValue={page}
|
||||
onChange={e => setPage(e.target.value)}
|
||||
>
|
||||
<Radio.Button value={0}>
|
||||
<img
|
||||
src={minecraftIcon}
|
||||
css={`
|
||||
margin-right: 4px;
|
||||
cursor: pointer;
|
||||
width: 22px;
|
||||
`}
|
||||
/>
|
||||
Vanilla
|
||||
</Radio.Button>
|
||||
<Radio.Button value={1}>
|
||||
<img
|
||||
src={curseForgeIcon}
|
||||
css={`
|
||||
margin-right: 4px;
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
`}
|
||||
/>
|
||||
CurseForge
|
||||
</Radio.Button>
|
||||
<Radio.Button value={2}>
|
||||
<FontAwesomeIcon
|
||||
icon={faArchive}
|
||||
css={`
|
||||
margin-right: 4px;
|
||||
cursor: pointer;
|
||||
`}
|
||||
/>
|
||||
Import Zip
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
height: calc(100% - 50px);
|
||||
`}
|
||||
>
|
||||
{pages[page]}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
page={page}
|
||||
css={`
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
opacity: ${props =>
|
||||
props.page === 0 || props.page === 2 ? 1 : 0};
|
||||
`}
|
||||
>
|
||||
<div
|
||||
version={version}
|
||||
importZipPath={importZipPath}
|
||||
css={`
|
||||
width: 70px;
|
||||
height: 40px;
|
||||
transition: 0.1s ease-in-out;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
font-size: 40px;
|
||||
color: ${props =>
|
||||
props.version || props.importZipPath
|
||||
? props.theme.palette.text.icon
|
||||
: props.theme.palette.text.disabled};
|
||||
${props =>
|
||||
props.version || props.importZipPath
|
||||
? 'cursor: pointer;'
|
||||
: ''}
|
||||
&:hover {
|
||||
background-color: ${props =>
|
||||
props.version || props.importZipPath
|
||||
? props.theme.action.hover
|
||||
: 'transparent'};
|
||||
}
|
||||
`}
|
||||
onClick={async () => {
|
||||
if (overrideNextStepOnClick) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await overrideNextStepOnClick();
|
||||
} catch {
|
||||
return;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
if (version || importZipPath) {
|
||||
setStep(1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<Spin
|
||||
indicator={
|
||||
<LoadingOutlined style={{ fontSize: 24 }} spin />
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faLongArrowAltRight} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Animation>
|
||||
)}
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
|
||||
export default Content;
|
||||
|
||||
const Animation = styled.div`
|
||||
transition: 0.2s ease-in-out;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
transform: translateX(
|
||||
${({ state }) => (state === 'exiting' || state === 'exited' ? -100 : 0)}%
|
||||
);
|
||||
`;
|
||||
@@ -0,0 +1,233 @@
|
||||
import React, { forwardRef, memo, useContext, useEffect } from 'react';
|
||||
import styled, { ThemeContext } from 'styled-components';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import InfiniteLoader from 'react-window-infinite-loader';
|
||||
import ContentLoader from 'react-content-loader';
|
||||
import { transparentize } from 'polished';
|
||||
import { openModal } from '../../../reducers/modals/actions';
|
||||
import { CURSEFORGE } from '../../../utils/constants';
|
||||
|
||||
const ModpacksListWrapper = ({
|
||||
// Are there more items to load?
|
||||
// (This information comes from the most recent API request.)
|
||||
hasNextPage,
|
||||
|
||||
// Are we currently loading a page of items?
|
||||
// (This may be an in-flight flag in your Redux store for example.)
|
||||
isNextPageLoading,
|
||||
|
||||
// Array of items loaded so far.
|
||||
items,
|
||||
|
||||
height,
|
||||
|
||||
width,
|
||||
|
||||
setStep,
|
||||
|
||||
setVersion,
|
||||
// Callback function responsible for loading the next page of items.
|
||||
loadNextPage,
|
||||
|
||||
setModpack,
|
||||
|
||||
infiniteLoaderRef
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
// If there are more items to be loaded then add an extra row to hold a loading indicator.
|
||||
const itemCount = hasNextPage ? items.length + 1 : items.length;
|
||||
// Only load 1 page of items at a time.
|
||||
// Pass an empty callback to InfiniteLoader in case it asks us to load more than once.
|
||||
const loadMoreItems = isNextPageLoading ? () => {} : loadNextPage;
|
||||
// Every row is loaded except for our loading indicator row.
|
||||
const isItemLoaded = index => !hasNextPage || index < items.length;
|
||||
|
||||
// Render an item or a loading indicator.
|
||||
const Item = memo(({ index, style }) => {
|
||||
const modpack = items[index];
|
||||
|
||||
if (!modpack) {
|
||||
return (
|
||||
<ModpackLoader
|
||||
hasNextPage={hasNextPage}
|
||||
isNextPageLoading={isNextPageLoading}
|
||||
loadNextPage={loadNextPage}
|
||||
top={style.top + (index === 0 ? 0 : 8)}
|
||||
width={width}
|
||||
height={style.height - (index === 0 ? 0 : 8)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const primaryImage = modpack?.logo;
|
||||
return (
|
||||
<div
|
||||
// eslint-disable-next-line
|
||||
style={{
|
||||
...style,
|
||||
top: style.top + (index === 0 ? 0 : 8),
|
||||
height: style.height - (index === 0 ? 0 : 8),
|
||||
background: `url('${primaryImage?.thumbnailUrl}')`,
|
||||
position: 'absolute',
|
||||
width: width - 8,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
margin: 0,
|
||||
borderRadius: 4
|
||||
}}
|
||||
key={modpack.id}
|
||||
>
|
||||
<Modpack>
|
||||
<div>{modpack.name}</div>
|
||||
</Modpack>
|
||||
<ModpackHover>
|
||||
<div
|
||||
onClick={() => {
|
||||
setVersion({
|
||||
projectID: modpack.id,
|
||||
fileID: modpack.latestFiles[modpack.latestFiles.length - 1].id,
|
||||
source: CURSEFORGE
|
||||
});
|
||||
setModpack(modpack);
|
||||
setStep(1);
|
||||
}}
|
||||
>
|
||||
Download Latest
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
openModal('ModpackDescription', {
|
||||
modpack,
|
||||
setVersion,
|
||||
setModpack,
|
||||
setStep,
|
||||
type: 'curseforge'
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
Explore / Versions
|
||||
</div>
|
||||
</ModpackHover>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const innerElementType = forwardRef(({ style, ...rest }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
// eslint-disable-next-line react/forbid-dom-props
|
||||
style={{
|
||||
...style,
|
||||
paddingTop: 0
|
||||
}}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...rest}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<InfiniteLoader
|
||||
isItemLoaded={isItemLoaded}
|
||||
itemCount={itemCount !== 0 ? itemCount : 40}
|
||||
loadMoreItems={() => loadMoreItems()}
|
||||
>
|
||||
{({ onItemsRendered }) => (
|
||||
<List
|
||||
height={height}
|
||||
width={width}
|
||||
itemCount={itemCount !== 0 ? itemCount : 40}
|
||||
itemSize={100}
|
||||
onItemsRendered={onItemsRendered}
|
||||
innerElementType={innerElementType}
|
||||
ref={list => {
|
||||
// Manually bind ref to reset scroll
|
||||
// eslint-disable-next-line
|
||||
infiniteLoaderRef.current = list;
|
||||
}}
|
||||
>
|
||||
{Item}
|
||||
</List>
|
||||
)}
|
||||
</InfiniteLoader>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ModpacksListWrapper);
|
||||
|
||||
const Modpack = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 20px;
|
||||
padding: 0 10px;
|
||||
font-weight: 700;
|
||||
background: ${props => transparentize(0.2, props.theme.palette.grey[700])};
|
||||
`;
|
||||
|
||||
const ModpackHover = styled.div`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: ${props => transparentize(0.4, props.theme.palette.grey[900])};
|
||||
opacity: 0;
|
||||
padding-left: 40%;
|
||||
will-change: opacity;
|
||||
transition: opacity 0.1s ease-in-out, background 0.1s ease-in-out;
|
||||
div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.1s ease-in-out;
|
||||
&:hover {
|
||||
background-color: ${props => props.theme.palette.primary.main};
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const ModpackLoader = memo(
|
||||
({ height, width, top, isNextPageLoading, hasNextPage, loadNextPage }) => {
|
||||
const ContextTheme = useContext(ThemeContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasNextPage && isNextPageLoading) {
|
||||
loadNextPage();
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<ContentLoader
|
||||
speed={2}
|
||||
foregroundColor={ContextTheme.palette.grey[900]}
|
||||
backgroundColor={ContextTheme.palette.grey[800]}
|
||||
title={false}
|
||||
height={height}
|
||||
style={{
|
||||
width: width - 8,
|
||||
height,
|
||||
position: 'absolute',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
top,
|
||||
borderRadius: 4
|
||||
}}
|
||||
>
|
||||
<rect x="0" y="0" width="100%" height={height} />
|
||||
</ContentLoader>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,250 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Select, Input } from 'antd';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faBomb, faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { getSearch } from '../../../api';
|
||||
import ModpacksListWrapper from './ModpacksListWrapper';
|
||||
|
||||
let lastRequest;
|
||||
const CurseForgeModpacks = ({ setStep, setVersion, setModpack }) => {
|
||||
const mcVersions = useSelector(state => state.app.vanillaManifest?.versions);
|
||||
const categories = useSelector(state => state.app.curseforgeCategories);
|
||||
const infiniteLoaderRef = useRef(null);
|
||||
const [modpacks, setModpacks] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [minecraftVersion, setMinecraftVersion] = useState(null);
|
||||
const [categoryId, setCategoryId] = useState(null);
|
||||
const [sortBy, setSortBy] = useState('Featured');
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [hasNextPage, setHasNextPage] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const updateModpacks = useDebouncedCallback(() => {
|
||||
if (infiniteLoaderRef?.current?.scrollToItem) {
|
||||
infiniteLoaderRef.current.scrollToItem(0);
|
||||
}
|
||||
loadMoreModpacks(true);
|
||||
}, 250);
|
||||
|
||||
const loadMoreModpacks = async (reset = false) => {
|
||||
const reqObj = {};
|
||||
lastRequest = reqObj;
|
||||
if (!loading) {
|
||||
setLoading(true);
|
||||
}
|
||||
if (reset && (modpacks.length !== 0 || hasNextPage)) {
|
||||
setModpacks([]);
|
||||
setHasNextPage(false);
|
||||
}
|
||||
let data = null;
|
||||
try {
|
||||
if (error) {
|
||||
setError(false);
|
||||
}
|
||||
data = await getSearch(
|
||||
'modpacks',
|
||||
searchText,
|
||||
40,
|
||||
reset ? 0 : modpacks.length,
|
||||
sortBy,
|
||||
true,
|
||||
minecraftVersion,
|
||||
categoryId
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
return;
|
||||
}
|
||||
const newModpacks = reset ? data : [...modpacks, ...data];
|
||||
if (lastRequest === reqObj) {
|
||||
setLoading(false);
|
||||
setHasNextPage(newModpacks.length % 40 === 0 && newModpacks.length !== 0);
|
||||
setModpacks(newModpacks);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateModpacks();
|
||||
}, [searchText, sortBy, minecraftVersion, categoryId]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<HeaderContainer>
|
||||
<StyledSelect
|
||||
placeholder="Minecraft Version"
|
||||
onChange={setMinecraftVersion}
|
||||
defaultValue={null}
|
||||
virtual={false}
|
||||
>
|
||||
<Select.Option value={null}>All Versions</Select.Option>
|
||||
{(mcVersions || [])
|
||||
.filter(v => v?.type === 'release')
|
||||
.map(v => (
|
||||
<Select.Option key={v?.id} value={v?.id}>
|
||||
{v?.id}
|
||||
</Select.Option>
|
||||
))}
|
||||
</StyledSelect>
|
||||
<StyledSelect
|
||||
placeholder="Minecraft Category"
|
||||
onChange={setCategoryId}
|
||||
defaultValue={null}
|
||||
virtual={false}
|
||||
>
|
||||
<Select.Option key="allcategories" value={null}>
|
||||
All Categories
|
||||
</Select.Option>
|
||||
{(categories || [])
|
||||
.filter(v => v?.classId === 4471)
|
||||
.sort((a, b) => a?.name.localeCompare(b?.name))
|
||||
.map(v => (
|
||||
<Select.Option value={v?.id} key={v?.id}>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<img
|
||||
src={v?.iconUrl}
|
||||
css={`
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-right: 10px;
|
||||
`}
|
||||
alt="icon"
|
||||
/>
|
||||
{v?.name}
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</StyledSelect>
|
||||
<StyledSelect
|
||||
placeholder="Sort by"
|
||||
defaultValue="Featured"
|
||||
onChange={setSortBy}
|
||||
virtual={false}
|
||||
>
|
||||
<Select.Option key="Featured" value="Featured">
|
||||
Featured
|
||||
</Select.Option>
|
||||
<Select.Option key="Popularity" value="Popularity">
|
||||
Popularity
|
||||
</Select.Option>
|
||||
<Select.Option key="LastUpdated" value="LastUpdated">
|
||||
Last Updated
|
||||
</Select.Option>
|
||||
<Select.Option key="Name" value="Name">
|
||||
Name
|
||||
</Select.Option>
|
||||
<Select.Option key="Author" value="Author">
|
||||
Author
|
||||
</Select.Option>
|
||||
<Select.Option key="TotalDownloads" value="TotalDownloads">
|
||||
Total Downloads
|
||||
</Select.Option>
|
||||
</StyledSelect>
|
||||
<StyledInput
|
||||
placeholder="Search..."
|
||||
onSearch={setSearchText}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
</HeaderContainer>
|
||||
<ModpacksContainer>
|
||||
{!error ? (
|
||||
!loading && modpacks.length === 0 ? (
|
||||
<div
|
||||
css={`
|
||||
margin-top: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 150px;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faExclamationCircle} />
|
||||
<div
|
||||
css={`
|
||||
font-size: 20px;
|
||||
margin-top: 70px;
|
||||
`}
|
||||
>
|
||||
No modpack has been found with the current filters.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<ModpacksListWrapper
|
||||
hasNextPage={hasNextPage}
|
||||
isNextPageLoading={loading}
|
||||
items={modpacks}
|
||||
loadNextPage={loadMoreModpacks}
|
||||
width={width}
|
||||
height={height}
|
||||
setStep={setStep}
|
||||
setVersion={setVersion}
|
||||
setModpack={setModpack}
|
||||
infiniteLoaderRef={infiniteLoaderRef}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
css={`
|
||||
margin-top: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 150px;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBomb} />
|
||||
<div
|
||||
css={`
|
||||
font-size: 20px;
|
||||
margin-top: 70px;
|
||||
`}
|
||||
>
|
||||
An error occurred while loading the modpacks list...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ModpacksContainer>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CurseForgeModpacks);
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledSelect = styled(Select)`
|
||||
width: 170px;
|
||||
margin-right: 20px;
|
||||
`;
|
||||
|
||||
const StyledInput = styled(Input.Search)``;
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const ModpacksContainer = styled.div`
|
||||
height: calc(100% - 15px);
|
||||
overflow: hidden;
|
||||
padding: 10px 0;
|
||||
`;
|
||||
@@ -0,0 +1,199 @@
|
||||
/* eslint-disable */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import path from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import { promises as fs } from 'fs';
|
||||
import { extractAll } from '../../../app/desktop/utils';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { Button, Input } from 'antd';
|
||||
import { _getTempPath } from '../../utils/selectors';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getAddon } from '../../api';
|
||||
import { downloadFile } from '../../../app/desktop/utils/downloader';
|
||||
import { CURSEFORGE, FABRIC, FORGE, VANILLA } from '../../utils/constants';
|
||||
import { transparentize } from 'polished';
|
||||
|
||||
const Import = ({
|
||||
setModpack,
|
||||
setVersion,
|
||||
importZipPath,
|
||||
setImportZipPath,
|
||||
setOverrideNextStepOnClick
|
||||
}) => {
|
||||
const [localValue, setLocalValue] = useState(null);
|
||||
const tempPath = useSelector(_getTempPath);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setError(false);
|
||||
}, [importZipPath]);
|
||||
|
||||
useEffect(() => {
|
||||
setImportZipPath(localValue?.length > 0 ? localValue : null);
|
||||
setVersion(null);
|
||||
}, [localValue]);
|
||||
|
||||
const openFileDialog = async () => {
|
||||
const dialog = await ipcRenderer.invoke('openFileDialog');
|
||||
if (dialog.canceled) return;
|
||||
setLocalValue(dialog.filePaths[0]);
|
||||
};
|
||||
|
||||
const onClick = async () => {
|
||||
if (loading || !localValue) return;
|
||||
setLoading(true);
|
||||
const urlRegex =
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*).zip$/;
|
||||
const isUrlRegex = urlRegex.test(localValue);
|
||||
|
||||
const tempFilePath = path.join(tempPath, path.basename(localValue));
|
||||
|
||||
if (isUrlRegex) {
|
||||
try {
|
||||
await fs.access(tempFilePath);
|
||||
} catch {
|
||||
await fse.remove(tempFilePath);
|
||||
}
|
||||
try {
|
||||
await downloadFile(tempFilePath, localValue);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(path.join(tempPath, 'manifest.json'));
|
||||
} catch {
|
||||
await fse.remove(path.join(tempPath, 'manifest.json'));
|
||||
}
|
||||
await extractAll(
|
||||
isUrlRegex ? tempFilePath : localValue,
|
||||
tempPath,
|
||||
{
|
||||
recursive: true,
|
||||
yes: true,
|
||||
$cherryPick: 'manifest.json'
|
||||
},
|
||||
{
|
||||
error: () => {
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
const manifest = await fse.readJson(path.join(tempPath, 'manifest.json'));
|
||||
await fse.remove(path.join(tempPath, 'manifest.json'));
|
||||
let addon = null;
|
||||
|
||||
if (manifest.projectID) {
|
||||
const data = await getAddon(manifest.projectID);
|
||||
addon = data;
|
||||
setModpack(addon);
|
||||
} else {
|
||||
setModpack({ name: manifest.name, logo: null });
|
||||
}
|
||||
const isForge = (manifest?.minecraft?.modLoaders || []).find(
|
||||
v => v.id.includes(FORGE) && v.primary
|
||||
);
|
||||
|
||||
const isFabric = (manifest?.minecraft?.modLoaders || []).find(
|
||||
v => v.id.includes(FABRIC) && v.primary
|
||||
);
|
||||
|
||||
const isVanilla = (manifest?.minecraft?.modLoaders || []).find(
|
||||
v => v.id.includes(VANILLA) && v.primary
|
||||
);
|
||||
|
||||
if (!isForge && !isFabric && !isVanilla) {
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const loader = { loaderType: VANILLA };
|
||||
|
||||
if (manifest.manifestType === 'minecraftModpack') {
|
||||
loader.source = CURSEFORGE;
|
||||
}
|
||||
|
||||
if (isForge) loader.loaderType = FORGE;
|
||||
else if (isFabric) loader.loaderType = FABRIC;
|
||||
|
||||
if (manifest.projectID) {
|
||||
loader.projectID = manifest.projectID;
|
||||
}
|
||||
|
||||
setVersion(loader);
|
||||
if (isUrlRegex) {
|
||||
setImportZipPath(tempFilePath);
|
||||
}
|
||||
setLoading(false);
|
||||
setError(false);
|
||||
};
|
||||
|
||||
setOverrideNextStepOnClick(() => onClick);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div>
|
||||
Local file or link to a direct download
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
`}
|
||||
>
|
||||
<Input
|
||||
disabled={loading}
|
||||
placeholder="http://.../file.zip"
|
||||
value={localValue}
|
||||
onChange={e => setLocalValue(e.target.value)}
|
||||
css={`
|
||||
width: 400px !important;
|
||||
margin-right: 10px !important;
|
||||
`}
|
||||
/>
|
||||
<Button disabled={loading} type="primary" onClick={openFileDialog}>
|
||||
Browse
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
show={error}
|
||||
css={`
|
||||
opacity: ${props => (props.show ? 1 : 0)};
|
||||
color: ${props => props.theme.palette.error.main};
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
padding: 3px;
|
||||
height: 30px;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
border-radius: ${props => props.theme.shape.borderRadius};
|
||||
background: ${props =>
|
||||
transparentize(0.7, props.theme.palette.grey[700])};
|
||||
`}
|
||||
>
|
||||
{error && 'There was an issue while importing.'}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Import);
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
`;
|
||||
@@ -0,0 +1,540 @@
|
||||
/* eslint-disable */
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import fse from 'fs-extra';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { Transition } from 'react-transition-group';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faLongArrowAltLeft,
|
||||
faLongArrowAltRight
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { Input } from 'antd';
|
||||
import { transparentize } from 'polished';
|
||||
import { addToQueue } from '../../reducers/actions';
|
||||
import { closeModal, openModal } from '../../reducers/modals/actions';
|
||||
import {
|
||||
downloadAddonZip,
|
||||
importAddonZip,
|
||||
convertcurseForgeToCanonical,
|
||||
extractFabricVersionFromManifest
|
||||
} from '../../../app/desktop/utils';
|
||||
import { _getInstancesPath, _getTempPath } from '../../utils/selectors';
|
||||
import bgImage from '../../assets/mcCube.jpg';
|
||||
import { downloadFile } from '../../../app/desktop/utils/downloader';
|
||||
import { FABRIC, VANILLA, FORGE, CURSEFORGE } from '../../utils/constants';
|
||||
|
||||
const InstanceName = ({
|
||||
in: inProp,
|
||||
setStep,
|
||||
version,
|
||||
modpack,
|
||||
setVersion,
|
||||
setModpack,
|
||||
importZipPath,
|
||||
step
|
||||
}) => {
|
||||
const mcName = (
|
||||
modpack?.name.replace(/\W/g, ' ') ||
|
||||
(version && `Minecraft ${version?.loaderType}`) ||
|
||||
''
|
||||
).trim();
|
||||
const originalMcName =
|
||||
modpack?.name || (version && `Minecraft ${version?.loaderType}`);
|
||||
const dispatch = useDispatch();
|
||||
const instancesPath = useSelector(_getInstancesPath);
|
||||
const tempPath = useSelector(_getTempPath);
|
||||
const forgeManifest = useSelector(state => state.app.forgeManifest);
|
||||
const [instanceName, setInstanceName] = useState(mcName);
|
||||
const [alreadyExists, setAlreadyExists] = useState(false);
|
||||
const [invalidName, setInvalidName] = useState(true);
|
||||
const [clicked, setClicked] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (instanceName || mcName) {
|
||||
const regex = /^[\sa-zA-Z0-9_.-]+$/;
|
||||
const finalWhiteSpace = /[^\s]$/;
|
||||
if (
|
||||
!regex.test(instanceName || mcName) ||
|
||||
!finalWhiteSpace.test(instanceName || mcName) ||
|
||||
(instanceName || mcName).length >= 45
|
||||
) {
|
||||
setInvalidName(true);
|
||||
setAlreadyExists(false);
|
||||
return;
|
||||
}
|
||||
fse
|
||||
.pathExists(path.join(instancesPath, instanceName || mcName))
|
||||
.then(exists => {
|
||||
setAlreadyExists(exists);
|
||||
setInvalidName(false);
|
||||
});
|
||||
}
|
||||
}, [instanceName, step]);
|
||||
|
||||
const imageURL = useMemo(() => {
|
||||
if (!modpack) return null;
|
||||
// Curseforge
|
||||
if (!modpack.synopsis) {
|
||||
return modpack?.logo?.thumbnailUrl;
|
||||
}
|
||||
}, [modpack]);
|
||||
|
||||
const wait = t => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(), t);
|
||||
});
|
||||
};
|
||||
|
||||
const createInstance = async localInstanceName => {
|
||||
if (!version || !localInstanceName) return;
|
||||
|
||||
const initTimestamp = Date.now();
|
||||
|
||||
const isCurseForgeModpack = Boolean(version?.source === CURSEFORGE);
|
||||
let manifest;
|
||||
|
||||
// If it's a curseforge modpack grab the manfiest and detect the loader
|
||||
// type as we don't yet know what it is.
|
||||
if (isCurseForgeModpack) {
|
||||
if (importZipPath) {
|
||||
manifest = await importAddonZip(
|
||||
importZipPath,
|
||||
path.join(instancesPath, localInstanceName),
|
||||
path.join(tempPath, localInstanceName),
|
||||
tempPath
|
||||
);
|
||||
} else {
|
||||
manifest = await downloadAddonZip(
|
||||
version?.projectID,
|
||||
version?.fileID,
|
||||
path.join(instancesPath, localInstanceName),
|
||||
path.join(tempPath, localInstanceName)
|
||||
);
|
||||
}
|
||||
|
||||
const isForgeModpack = (manifest?.minecraft?.modLoaders || []).some(
|
||||
v => v.id.includes(FORGE) && v.primary
|
||||
);
|
||||
|
||||
const isFabricModpack = (manifest?.minecraft?.modLoaders || []).some(
|
||||
v => v.id.includes(FABRIC) && v.primary
|
||||
);
|
||||
|
||||
if (isForgeModpack) {
|
||||
version.loaderType = FORGE;
|
||||
} else if (isFabricModpack) {
|
||||
version.loaderType = FABRIC;
|
||||
} else {
|
||||
version.loaderType = VANILLA;
|
||||
}
|
||||
}
|
||||
|
||||
const isVanilla = version?.loaderType === VANILLA;
|
||||
const isFabric = version?.loaderType === FABRIC;
|
||||
const isForge = version?.loaderType === FORGE;
|
||||
|
||||
if (isCurseForgeModpack) {
|
||||
if (imageURL) {
|
||||
await downloadFile(
|
||||
path.join(
|
||||
instancesPath,
|
||||
localInstanceName,
|
||||
`background${path.extname(imageURL)}`
|
||||
),
|
||||
imageURL
|
||||
);
|
||||
}
|
||||
|
||||
if (isForge) {
|
||||
const loader = {
|
||||
loaderType: FORGE,
|
||||
mcVersion: manifest.minecraft.version,
|
||||
loaderVersion: convertcurseForgeToCanonical(
|
||||
manifest.minecraft.modLoaders.find(v => v.primary).id,
|
||||
manifest.minecraft.version,
|
||||
forgeManifest
|
||||
),
|
||||
fileID: version?.fileID,
|
||||
projectID: version?.projectID,
|
||||
source: version?.source,
|
||||
sourceName: manifest.name
|
||||
};
|
||||
|
||||
dispatch(
|
||||
addToQueue(
|
||||
localInstanceName,
|
||||
loader,
|
||||
manifest,
|
||||
imageURL ? `background${path.extname(imageURL)}` : null
|
||||
)
|
||||
);
|
||||
} else if (isFabric) {
|
||||
const loader = {
|
||||
loaderType: FABRIC,
|
||||
mcVersion: manifest.minecraft.version,
|
||||
loaderVersion: extractFabricVersionFromManifest(manifest),
|
||||
fileID: version?.fileID,
|
||||
projectID: version?.projectID,
|
||||
source: version?.source,
|
||||
sourceName: manifest.name
|
||||
};
|
||||
dispatch(
|
||||
addToQueue(
|
||||
localInstanceName,
|
||||
loader,
|
||||
manifest,
|
||||
imageURL ? `background${path.extname(imageURL)}` : null
|
||||
)
|
||||
);
|
||||
} else if (isVanilla) {
|
||||
const loader = {
|
||||
loaderType: VANILLA,
|
||||
mcVersion: manifest.minecraft.version,
|
||||
loaderVersion: version?.loaderVersion,
|
||||
fileID: version?.fileID
|
||||
};
|
||||
|
||||
dispatch(
|
||||
addToQueue(
|
||||
localInstanceName,
|
||||
loader,
|
||||
manifest,
|
||||
imageURL ? `background${path.extname(imageURL)}` : null
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (importZipPath) {
|
||||
manifest = await importAddonZip(
|
||||
importZipPath,
|
||||
path.join(instancesPath, localInstanceName),
|
||||
path.join(tempPath, localInstanceName),
|
||||
tempPath
|
||||
);
|
||||
|
||||
let loader = {};
|
||||
|
||||
if (version?.loaderType === FORGE) {
|
||||
Object.assign(loader, {
|
||||
loaderType: version?.loaderType,
|
||||
mcVersion: manifest.minecraft.version,
|
||||
loaderVersion: convertcurseForgeToCanonical(
|
||||
manifest.minecraft.modLoaders.find(v => v.primary).id,
|
||||
manifest.minecraft.version,
|
||||
forgeManifest
|
||||
)
|
||||
});
|
||||
|
||||
dispatch(addToQueue(localInstanceName, loader, manifest));
|
||||
} else if (version?.loaderType === FABRIC) {
|
||||
Object.assign(loader, {
|
||||
loaderType: version?.loaderType,
|
||||
mcVersion: manifest.minecraft.version,
|
||||
loaderVersion: manifest.minecraft.modLoaders[0].yarn,
|
||||
fileID: manifest.minecraft.modLoaders[0].loader
|
||||
});
|
||||
|
||||
dispatch(addToQueue(localInstanceName, loader, manifest));
|
||||
} else if (version?.loaderType === VANILLA) {
|
||||
Object.assign(loader, {
|
||||
loaderType: version?.loaderType,
|
||||
mcVersion: manifest.minecraft.version
|
||||
});
|
||||
|
||||
dispatch(addToQueue(localInstanceName, loader, manifest));
|
||||
}
|
||||
} else if (isVanilla) {
|
||||
dispatch(
|
||||
addToQueue(localInstanceName, {
|
||||
loaderType: version?.loaderType,
|
||||
mcVersion: version?.mcVersion
|
||||
})
|
||||
);
|
||||
} else if (isFabric) {
|
||||
dispatch(
|
||||
addToQueue(localInstanceName, {
|
||||
loaderType: FABRIC,
|
||||
mcVersion: version?.mcVersion,
|
||||
loaderVersion: version?.loaderVersion
|
||||
})
|
||||
);
|
||||
} else if (isForge) {
|
||||
dispatch(
|
||||
addToQueue(localInstanceName, {
|
||||
loaderType: version?.loaderType,
|
||||
mcVersion: version?.mcVersion,
|
||||
loaderVersion: version?.loaderVersion
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (Date.now() - initTimestamp < 2000) {
|
||||
await wait(2000 - (Date.now() - initTimestamp));
|
||||
}
|
||||
|
||||
dispatch(closeModal());
|
||||
};
|
||||
return (
|
||||
<Transition in={inProp} timeout={200}>
|
||||
{state => (
|
||||
<Animation state={state} bg={imageURL || bgImage}>
|
||||
<Transition in={clicked} timeout={200}>
|
||||
{state1 => (
|
||||
<>
|
||||
<BackgroundOverlay />
|
||||
<div
|
||||
state={state1}
|
||||
css={`
|
||||
opacity: ${({ state }) =>
|
||||
state === 'entering' || state === 'entered' ? 0 : 1};
|
||||
flex: 1;
|
||||
transition: 0.1s ease-in-out;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
font-size: 40px;
|
||||
cursor: pointer;
|
||||
z-index: 100001;
|
||||
margin: 20px;
|
||||
&:hover {
|
||||
background-color: ${props => props.theme.action.hover};
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
setStep(0);
|
||||
}}
|
||||
>
|
||||
{clicked ? '' : <FontAwesomeIcon icon={faLongArrowAltLeft} />}
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
position: relative;
|
||||
flex: 10;
|
||||
align-self: center;
|
||||
font-size: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 100001;
|
||||
`}
|
||||
>
|
||||
<ModpackName state={state1} name={mcName}>
|
||||
{originalMcName}
|
||||
</ModpackName>
|
||||
<div
|
||||
css={`
|
||||
margin-top: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
`}
|
||||
>
|
||||
<Input
|
||||
state={state1}
|
||||
size="large"
|
||||
placeholder={mcName}
|
||||
onChange={e => setInstanceName(e.target.value)}
|
||||
css={`
|
||||
opacity: ${({ state }) =>
|
||||
state === 'entering' || state === 'entered'
|
||||
? 0
|
||||
: 1} !important;
|
||||
transition: 0.1s ease-in-out !important;
|
||||
width: 300px !important;
|
||||
align-self: center !important;
|
||||
`}
|
||||
/>
|
||||
<div
|
||||
show={invalidName || alreadyExists}
|
||||
css={`
|
||||
opacity: ${props => (props.show ? 1 : 0)};
|
||||
color: ${props => props.theme.palette.error.main};
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
padding: 3px;
|
||||
height: 30px;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
border-radius: ${props =>
|
||||
props.theme.shape.borderRadius};
|
||||
background: ${props =>
|
||||
transparentize(0.7, props.theme.palette.grey[700])};
|
||||
`}
|
||||
>
|
||||
{invalidName &&
|
||||
'Instance name is not valid or too long. Please try another one'}
|
||||
{alreadyExists &&
|
||||
'An instance with this name already exists!'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
state={state1}
|
||||
css={`
|
||||
opacity: ${({ state }) =>
|
||||
state === 'entering' || state === 'entered' ? 0 : 1};
|
||||
flex: 1;
|
||||
transition: 0.1s ease-in-out;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
font-size: 40px;
|
||||
cursor: pointer;
|
||||
z-index: 100001;
|
||||
margin: 20px;
|
||||
&:hover {
|
||||
background-color: ${props => props.theme.action.hover};
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
createInstance(instanceName || mcName);
|
||||
setClicked(true);
|
||||
}}
|
||||
>
|
||||
{clicked || alreadyExists || invalidName ? (
|
||||
''
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faLongArrowAltRight} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Transition>
|
||||
</Animation>
|
||||
)}
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(InstanceName);
|
||||
|
||||
const Animation = styled.div`
|
||||
transition: 0.2s ease-in-out;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: url(${props => props.bg});
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
will-change: transform;
|
||||
transform: translateX(
|
||||
${({ state }) => (state === 'entering' || state === 'entered' ? 0 : 101)}%
|
||||
);
|
||||
`;
|
||||
|
||||
const BackgroundOverlay = styled.div`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backdrop-filter: blur(12px);
|
||||
background: ${props => transparentize(0.4, props.theme.palette.grey[900])};
|
||||
`;
|
||||
|
||||
const ModpackNameKeyframe = props => keyframes`
|
||||
from {
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
35% {
|
||||
transform: scale(1) translateY(65%);
|
||||
}
|
||||
to {
|
||||
transform: scale(${props.name.length < 17 ? 2 : 1}) translateY(65%);
|
||||
}
|
||||
`;
|
||||
|
||||
const ModpackNameBorderKeyframe = keyframes`
|
||||
0% {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
25% {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
}
|
||||
50% {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
100% {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const ModpackNameBorderColorKeyframe = keyframes`
|
||||
0% {
|
||||
border-bottom-color: white;
|
||||
border-left-color: white;
|
||||
}
|
||||
50% {
|
||||
border-bottom-color: white;
|
||||
border-left-color: white;
|
||||
}
|
||||
51% {
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
100% {
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
const ModpackName = styled.span`
|
||||
position: relative;
|
||||
font-weight: bold;
|
||||
font-size: 45px;
|
||||
animation: ${({ state }) =>
|
||||
state === 'entering' || state === 'entered' ? ModpackNameKeyframe : null}
|
||||
0.2s ease-in-out forwards;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-transform: capitalize;
|
||||
padding: 20px;
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
border: ${({ state }) =>
|
||||
state === 'entering' || state === 'entered' ? 4 : 0}px
|
||||
solid transparent;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
&::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-top-color: white;
|
||||
border-right-color: white;
|
||||
animation: ${({ state }) =>
|
||||
state === 'entering' || state === 'entered'
|
||||
? ModpackNameBorderKeyframe
|
||||
: null}
|
||||
2s infinite;
|
||||
}
|
||||
&::after {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
animation: ${({ state }) =>
|
||||
state === 'entering' || state === 'entered'
|
||||
? ModpackNameBorderKeyframe
|
||||
: null}
|
||||
2s 1s infinite,
|
||||
${({ state }) =>
|
||||
state === 'entering' || state === 'entered'
|
||||
? ModpackNameBorderColorKeyframe
|
||||
: null}
|
||||
2s 1s infinite;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,59 @@
|
||||
/* eslint-disable */
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Cascader } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { getFilteredVersions } from '../../../app/desktop/utils';
|
||||
import { FABRIC, FORGE, VANILLA } from '../../utils/constants';
|
||||
|
||||
const NewInstance = ({ setVersion, setModpack }) => {
|
||||
const vanillaManifest = useSelector(state => state.app.vanillaManifest);
|
||||
const fabricManifest = useSelector(state => state.app.fabricManifest);
|
||||
const forgeManifest = useSelector(state => state.app.forgeManifest);
|
||||
|
||||
const filteredVers = useMemo(() => {
|
||||
return getFilteredVersions(vanillaManifest, forgeManifest, fabricManifest);
|
||||
}, [vanillaManifest, forgeManifest, fabricManifest]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Cascader
|
||||
options={filteredVers}
|
||||
onChange={v => {
|
||||
if (!v) {
|
||||
setVersion(null);
|
||||
} else if (v[0] === VANILLA) {
|
||||
setVersion({ loaderType: v[0], mcVersion: v[2] });
|
||||
} else if (v[0] === FORGE) {
|
||||
setVersion({
|
||||
loaderType: v[0],
|
||||
mcVersion: v[1],
|
||||
loaderVersion: v[2]
|
||||
});
|
||||
} else if (v[0] === FABRIC) {
|
||||
setVersion({
|
||||
loaderType: v[0],
|
||||
mcVersion: v[2],
|
||||
loaderVersion: v[3]
|
||||
});
|
||||
}
|
||||
setModpack(null);
|
||||
}}
|
||||
placeholder="Select a version"
|
||||
size="large"
|
||||
css={`
|
||||
width: 400px !important;
|
||||
`}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export default React.memo(NewInstance);
|
||||
@@ -0,0 +1,54 @@
|
||||
/* eslint-disable */
|
||||
import React, { useState, lazy, Suspense } from 'react';
|
||||
import Modal from '../../components/Modal';
|
||||
import AsyncComponent from '../../components/AsyncComponent';
|
||||
|
||||
const InstanceName = AsyncComponent(lazy(() => import('./InstanceName')));
|
||||
const Content = AsyncComponent(lazy(() => import('./Content')));
|
||||
|
||||
const AddInstance = ({ defaultPage }) => {
|
||||
const [version, setVersion] = useState(null);
|
||||
const [step, setStep] = useState(0);
|
||||
const [modpack, setModpack] = useState(null);
|
||||
const [importZipPath, setImportZipPath] = useState('');
|
||||
const [page, setPage] = useState(defaultPage);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 85%;
|
||||
width: 80%;
|
||||
max-width: 1000px;
|
||||
overflow: hidden;
|
||||
`}
|
||||
title="Add New Instance"
|
||||
>
|
||||
<Suspense>
|
||||
<Content
|
||||
in={step === 0}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
setStep={setStep}
|
||||
setVersion={setVersion}
|
||||
version={version}
|
||||
setModpack={setModpack}
|
||||
modpack={modpack}
|
||||
setImportZipPath={setImportZipPath}
|
||||
importZipPath={importZipPath}
|
||||
/>
|
||||
<InstanceName
|
||||
version={version}
|
||||
in={step === 1}
|
||||
setStep={setStep}
|
||||
modpack={modpack}
|
||||
setVersion={setVersion}
|
||||
setModpack={setModpack}
|
||||
importZipPath={importZipPath}
|
||||
step={step}
|
||||
/>
|
||||
</Suspense>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AddInstance);
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { memo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Modal from '../components/Modal';
|
||||
|
||||
const AutoUpdatesNotAvailable = () => {
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 200px;
|
||||
width: 400px;
|
||||
`}
|
||||
title="Auto Updates Not Available"
|
||||
>
|
||||
<Container>
|
||||
<div>Auto updates are not available on this platform.</div>
|
||||
<div
|
||||
css={`
|
||||
margin-top: 20px;
|
||||
`}
|
||||
>
|
||||
Please, update GDLauncher through your package manager or download the
|
||||
new version from our website <a href="https://gdevs.io">here</a>
|
||||
</div>
|
||||
</Container>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AutoUpdatesNotAvailable);
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
`;
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, { memo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Button } from 'antd';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import Modal from '../components/Modal';
|
||||
import BisectHostingLogo from '../../ui/BisectHosting';
|
||||
import ga from '../utils/analytics';
|
||||
|
||||
const BisectHosting = () => {
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 360px;
|
||||
width: 500px;
|
||||
font-size: 10px;
|
||||
line-height: 1.8;
|
||||
`}
|
||||
title="We teamed up with BisectHosting"
|
||||
>
|
||||
<Container>
|
||||
<BisectHostingLogo size={70} hover />
|
||||
<h2
|
||||
css={`
|
||||
margin-top: 20px;
|
||||
`}
|
||||
>
|
||||
Grab a server from our official partner{' '}
|
||||
<span
|
||||
css={`
|
||||
font-weight: 800;
|
||||
`}
|
||||
>
|
||||
BisectHosting
|
||||
</span>{' '}
|
||||
<span>for effortless modded server installs and updates.</span> New
|
||||
customers can save{' '}
|
||||
<span
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.green};
|
||||
`}
|
||||
>
|
||||
25%
|
||||
</span>{' '}
|
||||
off their first month using the promo code{' '}
|
||||
<span
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.green};
|
||||
`}
|
||||
>
|
||||
GDL
|
||||
</span>{' '}
|
||||
at checkout.
|
||||
</h2>
|
||||
<a href="https://bisecthosting.com/gdl">
|
||||
<Button
|
||||
type="primary"
|
||||
css={`
|
||||
margin-top: 25px;
|
||||
`}
|
||||
onClick={() => {
|
||||
ga.sendCustomEvent('BHClickAdLink');
|
||||
}}
|
||||
>
|
||||
Go to BisectHosting.com
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
</Button>
|
||||
</a>
|
||||
</Container>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(BisectHosting);
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
`;
|
||||
@@ -0,0 +1,44 @@
|
||||
module.exports = {
|
||||
new: [
|
||||
{
|
||||
header: 'Support ARM',
|
||||
content: 'Architecture.',
|
||||
advanced: { cm: '4fd9a4', pr: '1451' }
|
||||
},
|
||||
{
|
||||
header: 'Add manual download',
|
||||
content: 'option for failed opted out mods.',
|
||||
advanced: { cm: 'a8dfa1', pr: '1512' }
|
||||
}
|
||||
],
|
||||
improvements: [
|
||||
{
|
||||
header: 'Updated url',
|
||||
content: 'for minecraft news images.',
|
||||
advanced: { cm: 'efa324', pr: '1443' }
|
||||
},
|
||||
{
|
||||
header: 'Simplifications',
|
||||
content:
|
||||
' to the codebase, napi and nsfw now get automatically compiled on build.',
|
||||
advanced: { cm: '20148d', pr: '1446' }
|
||||
},
|
||||
{
|
||||
header: 'Add restore option',
|
||||
content: 'to failed updates.',
|
||||
advanced: { cm: '9d84085', pr: '1531' }
|
||||
}
|
||||
],
|
||||
bugfixes: [
|
||||
{
|
||||
header: 'Fix asset downloading',
|
||||
content: 'now enforcing https.',
|
||||
advanced: { cm: '73b3f4', pr: '1514' }
|
||||
},
|
||||
{
|
||||
header: 'Fix deprecated warnings',
|
||||
content: 'for dropped file handles.',
|
||||
advanced: { cm: '9d84085', pr: '1531' }
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,421 @@
|
||||
/* eslint-disable react/no-unescaped-entities */
|
||||
import React, { memo, useState, useEffect, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import TypeAnimation from 'react-type-animation';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faBug, faStar, faWrench } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import Modal from '../../components/Modal';
|
||||
import SocialButtons from '../../components/SocialButtons';
|
||||
import KoFiButton from '../../assets/ko-fi.png';
|
||||
import UpdateIllustration from '../../assets/update_illustration.png';
|
||||
import UpdateIllustrationChristmas from '../../assets/update_illustration_christmas.png';
|
||||
import { openModal } from '../../reducers/modals/actions';
|
||||
import ga from '../../utils/analytics';
|
||||
import changelog from './changeLog';
|
||||
|
||||
const UpdateRow = ({ header, content, advanced }) => {
|
||||
const prSplit = advanced?.pr?.split('/');
|
||||
return (
|
||||
<li>
|
||||
• {header}{' '}
|
||||
<span
|
||||
css={`
|
||||
color: ${props => props.theme.palette.text.third};
|
||||
`}
|
||||
>
|
||||
{content}
|
||||
</span>
|
||||
{advanced && (
|
||||
<div
|
||||
css={`
|
||||
color: ${props => props.theme.palette.text.third};
|
||||
font-size: 12px;
|
||||
a {
|
||||
color: ${props => props.theme.palette.primary.light};
|
||||
}
|
||||
a:hover {
|
||||
color: ${props => props.theme.palette.primary.main};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<a
|
||||
href={`https://github.com/gorilla-devs/GDLauncher/commit/${advanced.cm}`}
|
||||
>
|
||||
{advanced.cm}
|
||||
</a>
|
||||
{prSplit && (
|
||||
<>
|
||||
{' | '}
|
||||
{/* Yes, this was the best (and shortest) version to do this I could come up with */}
|
||||
<a
|
||||
href={`https://github.com/gorilla-devs/GDLauncher/pull/${
|
||||
prSplit[0]
|
||||
}${prSplit.length > 1 ? `/commits/${prSplit[1]}` : ''}`}
|
||||
>
|
||||
#{advanced.pr}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
{advanced.ms && <> | {advanced.ms}</>}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const ChangeLogs = () => {
|
||||
const [version, setVersion] = useState(null);
|
||||
const [skipIObserver, setSkipIObserver] = useState(true);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const { ref: intersectionObserverRef, inView: insectionObserverInView } =
|
||||
useInView({
|
||||
threshold: 0.3,
|
||||
initialInView: false,
|
||||
triggerOnce: true,
|
||||
skip: skipIObserver
|
||||
});
|
||||
|
||||
const typingSequence = useMemo(() => {
|
||||
const completText = 'Merry Christmas!';
|
||||
let textSoFar = '';
|
||||
const newArr = [];
|
||||
let timeToWait = 1300;
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < completText.length; i++) {
|
||||
textSoFar += completText[i];
|
||||
newArr.push(textSoFar, 50);
|
||||
timeToWait += 50;
|
||||
}
|
||||
|
||||
const completTextGDL = 'from the GDL Team';
|
||||
let textSoFarGDL = '';
|
||||
const fromGDLArr = [timeToWait];
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < completTextGDL.length; i++) {
|
||||
textSoFarGDL += completTextGDL[i];
|
||||
fromGDLArr.push(textSoFarGDL, 50);
|
||||
}
|
||||
|
||||
return {
|
||||
merryChristmas: newArr,
|
||||
fromGDL: fromGDLArr
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
ipcRenderer
|
||||
.invoke('getAppVersion')
|
||||
.then(v => {
|
||||
setVersion(v);
|
||||
if (!v.includes('beta')) {
|
||||
setTimeout(() => {
|
||||
setSkipIObserver(false);
|
||||
}, 300);
|
||||
}
|
||||
return v;
|
||||
})
|
||||
.catch(console.error);
|
||||
ga.sendCustomEvent('changelogModalOpen');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (insectionObserverInView) {
|
||||
ga.sendCustomEvent('changelogModalReadAll');
|
||||
}
|
||||
}, [insectionObserverInView]);
|
||||
|
||||
const openBisectModal = () => {
|
||||
dispatch(openModal('BisectHosting'));
|
||||
ga.sendCustomEvent('changelogModalOpenBisect');
|
||||
};
|
||||
|
||||
const isChristmas =
|
||||
new Date().getMonth() === 11 &&
|
||||
[21, 22, 23, 24, 25, 26, 27, 28, 29].includes(new Date().getDate());
|
||||
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 550px;
|
||||
width: 475px;
|
||||
`}
|
||||
title={`What's new in ${version}`}
|
||||
removePadding
|
||||
>
|
||||
<Container>
|
||||
<Header>
|
||||
{isChristmas ? (
|
||||
<div
|
||||
css={`
|
||||
width: 430px;
|
||||
`}
|
||||
>
|
||||
<h1
|
||||
css={`
|
||||
font-weight: bold;
|
||||
`}
|
||||
>
|
||||
<TypeAnimation
|
||||
cursor={false}
|
||||
sequence={typingSequence.merryChristmas}
|
||||
wrapper="span"
|
||||
/>{' '}
|
||||
<span
|
||||
css={`
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
`}
|
||||
>
|
||||
<TypeAnimation
|
||||
cursor={false}
|
||||
sequence={typingSequence.fromGDL}
|
||||
wrapper="span"
|
||||
/>
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<img
|
||||
css={`
|
||||
border-radius: 5px;
|
||||
width: 401px;
|
||||
`}
|
||||
src={isChristmas ? UpdateIllustrationChristmas : UpdateIllustration}
|
||||
alt="New Version"
|
||||
/>
|
||||
<div
|
||||
css={`
|
||||
margin-top: 20px;
|
||||
color: ${props => props.theme.palette.text.third};
|
||||
span {
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
`}
|
||||
>
|
||||
If you appreciate our work, please consider supporting us through a
|
||||
donation or grab a server from our official partner{' '}
|
||||
<span onClick={openBisectModal}>BisectHosting</span>
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 20px;
|
||||
a:nth-child(1) {
|
||||
margin-right: 20px;
|
||||
}
|
||||
img {
|
||||
border-radius: 30px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
<a href="https://ko-fi.com/gdlauncher">
|
||||
<img src={KoFiButton} alt="Ko-Fi" />
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
css={`
|
||||
margin-top: 20px;
|
||||
color: ${props => props.theme.palette.primary.light};
|
||||
`}
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
>
|
||||
{showAdvanced
|
||||
? 'Hide extended information'
|
||||
: 'Show extended information'}
|
||||
</a>
|
||||
</Header>
|
||||
<Section>
|
||||
{changelog.new.length ? (
|
||||
<SectionTitle
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.green};
|
||||
`}
|
||||
>
|
||||
<span
|
||||
css={`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faStar}
|
||||
css={`
|
||||
margin-right: 10px;
|
||||
font-size: 20px;
|
||||
`}
|
||||
/>
|
||||
New
|
||||
</span>
|
||||
</SectionTitle>
|
||||
) : null}
|
||||
<ul>
|
||||
{changelog.new.map((item, index) => (
|
||||
<UpdateRow
|
||||
/* eslint-disable-next-line react/no-array-index-key */
|
||||
key={index}
|
||||
header={item.header}
|
||||
content={item.content}
|
||||
advanced={showAdvanced && item.advanced}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</Section>
|
||||
<Section>
|
||||
{changelog.improvements.length ? (
|
||||
<SectionTitle
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.yellow};
|
||||
`}
|
||||
>
|
||||
<span
|
||||
css={`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faWrench}
|
||||
css={`
|
||||
margin-right: 10px;
|
||||
font-size: 20px;
|
||||
`}
|
||||
/>
|
||||
Improved
|
||||
</span>
|
||||
</SectionTitle>
|
||||
) : null}
|
||||
<ul>
|
||||
{changelog.improvements.map((item, index) => (
|
||||
<UpdateRow
|
||||
/* eslint-disable-next-line react/no-array-index-key */
|
||||
key={index}
|
||||
header={item.header}
|
||||
content={item.content}
|
||||
advanced={showAdvanced && item.advanced}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</Section>
|
||||
<Section>
|
||||
{changelog.bugfixes.length ? (
|
||||
<SectionTitle
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.red};
|
||||
`}
|
||||
>
|
||||
<span
|
||||
css={`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faBug}
|
||||
css={`
|
||||
margin-right: 10px;
|
||||
font-size: 20px;
|
||||
`}
|
||||
/>
|
||||
Bug Fixes
|
||||
</span>
|
||||
</SectionTitle>
|
||||
) : null}
|
||||
<ul ref={intersectionObserverRef}>
|
||||
{changelog.bugfixes.map((item, index) => (
|
||||
<UpdateRow
|
||||
/* eslint-disable-next-line react/no-array-index-key */
|
||||
key={index}
|
||||
header={item.header}
|
||||
content={item.content}
|
||||
advanced={showAdvanced && item.advanced}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</Section>
|
||||
</Container>
|
||||
<div
|
||||
css={`
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
background: ${props => props.theme.palette.grey[800]};
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
`}
|
||||
>
|
||||
<SocialButtons />
|
||||
<span
|
||||
css={`
|
||||
padding-left: 20px;
|
||||
color: ${props => props.theme.palette.text.secondary};
|
||||
`}
|
||||
>
|
||||
Follow us for more updates
|
||||
</span>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChangeLogs);
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
height: calc(100% - 60px);
|
||||
overflow-y: auto;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
const SectionTitle = styled.h2`
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-size: 22px;
|
||||
`;
|
||||
|
||||
const Section = styled.div`
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
p {
|
||||
margin: 20px 0 !important;
|
||||
}
|
||||
ul {
|
||||
padding: 0px;
|
||||
list-style-position: inside;
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
li {
|
||||
text-align: start;
|
||||
list-style-type: none;
|
||||
margin: 10px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import Modal from '../components/Modal';
|
||||
|
||||
const InfoModal = ({ modName, error, preventClose }) => {
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
width: 50%;
|
||||
max-width: 550px;
|
||||
overflow-x: hidden;
|
||||
`}
|
||||
preventClose={preventClose}
|
||||
title="Mod failed to download"
|
||||
>
|
||||
<div>
|
||||
The mod ${modName || ''} failed to download
|
||||
<div
|
||||
css={`
|
||||
background: ${props => props.theme.palette.grey[900]};
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
`}
|
||||
>
|
||||
{'> '}
|
||||
{error.toString()}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfoModal;
|
||||
@@ -0,0 +1,164 @@
|
||||
import React, { memo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { clipboard } from 'electron';
|
||||
import { Tooltip, Collapse } from 'antd';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCopy } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import Modal from '../components/Modal';
|
||||
import Logo from '../../ui/LogoSad';
|
||||
|
||||
const calcError = code => {
|
||||
switch (code) {
|
||||
case 1:
|
||||
return 'Uncaught Fatal Exception';
|
||||
case 3:
|
||||
return 'Internal JavaScript Parse Error';
|
||||
case 4:
|
||||
return 'Internal JavaScript Evaluation Failure';
|
||||
case 5:
|
||||
return 'Fatal Error';
|
||||
case 6:
|
||||
return 'Non-function Internal Exception Handler ';
|
||||
case 7:
|
||||
return 'Internal Exception Handler Run-Time Failure';
|
||||
case 9:
|
||||
return 'Invalid Argument';
|
||||
case 10:
|
||||
return 'Internal JavaScript Run-Time Failure';
|
||||
case 12:
|
||||
return 'Invalid Debug Argument';
|
||||
default:
|
||||
return code > 128 ? 'Signal Exits' : 'Unknown Error';
|
||||
}
|
||||
};
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
const InstanceCrashed = ({ code, errorLogs }) => {
|
||||
const [copiedLog, setCopiedLog] = useState(null);
|
||||
|
||||
function copy(e) {
|
||||
e.stopPropagation();
|
||||
setCopiedLog(true);
|
||||
clipboard.writeText(errorLogs);
|
||||
setTimeout(() => {
|
||||
setCopiedLog(false);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 450px;
|
||||
width: 500px;
|
||||
`}
|
||||
title="The instance could not be launched"
|
||||
>
|
||||
<Container>
|
||||
<InnerContainer>
|
||||
<Logo size={100} />
|
||||
<h3>
|
||||
OOPSIE WOOPSIE!!
|
||||
<br /> A creeper blew this instance up!
|
||||
</h3>
|
||||
</InnerContainer>
|
||||
<Card
|
||||
css={`
|
||||
margin: 10px 0 20px 0;
|
||||
`}
|
||||
>
|
||||
<h3>Error: </h3>
|
||||
<ErrorContainer>{calcError(code)}</ErrorContainer>
|
||||
<h3>code: </h3>
|
||||
<ErrorContainer>{code}</ErrorContainer>
|
||||
</Card>
|
||||
<Collapse
|
||||
css={`
|
||||
width: 100%;
|
||||
`}
|
||||
defaultActiveKey={['1']}
|
||||
>
|
||||
<Panel
|
||||
header={
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
Error Log
|
||||
<Tooltip title={copiedLog ? 'Copied' : 'Copy'} placement="top">
|
||||
<div
|
||||
css={`
|
||||
margin: 0;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} onClick={e => copy(e)} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
key="1"
|
||||
>
|
||||
<p
|
||||
css={`
|
||||
height: 110px;
|
||||
word-break: break-all;
|
||||
overflow-y: auto;
|
||||
`}
|
||||
>
|
||||
{errorLogs}
|
||||
</p>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</Container>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(InstanceCrashed);
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-conter: space-between;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
`;
|
||||
|
||||
const InnerContainer = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-conter: space-between;
|
||||
align-items: center;
|
||||
h3 {
|
||||
margin-left: 10px;
|
||||
text-align: start;
|
||||
}
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
`;
|
||||
|
||||
const Card = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-conter: space-between;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
h3 {
|
||||
margin: 0 10px 0 10px;
|
||||
text-align: start;
|
||||
font-weight: 900;
|
||||
}
|
||||
background: ${props => props.theme.palette.grey[900]};
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
`;
|
||||
|
||||
const ErrorContainer = styled.div`
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
`;
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { useState } from 'react';
|
||||
import fse from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { Button } from 'antd';
|
||||
import { useInterval } from 'rooks';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import Modal from '../components/Modal';
|
||||
import { _getInstancesPath, _getInstances } from '../utils/selectors';
|
||||
import { closeModal } from '../reducers/modals/actions';
|
||||
|
||||
const InstanceDeleteConfirmation = ({ instanceName }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const instancesPath = useSelector(_getInstancesPath);
|
||||
const instances = useSelector(_getInstances);
|
||||
|
||||
const { start, stop } = useInterval(() => {
|
||||
if (!instances.find(instance => instance.name === instanceName)) {
|
||||
stop();
|
||||
dispatch(closeModal());
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const deleteInstance = async () => {
|
||||
setLoading(true);
|
||||
start();
|
||||
fse.remove(path.join(instancesPath, instanceName));
|
||||
};
|
||||
const closeModalWindow = () => dispatch(closeModal());
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 40%;
|
||||
width: 50%;
|
||||
max-width: 550px;
|
||||
max-height: 260px;
|
||||
overflow-x: hidden;
|
||||
`}
|
||||
title="Confirm Instance Deletion"
|
||||
>
|
||||
<div>
|
||||
Are you sure you want to delete:
|
||||
<h4
|
||||
css={`
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
color: ${props => props.theme.palette.error.main};
|
||||
`}
|
||||
>
|
||||
{instanceName}
|
||||
</h4>
|
||||
This action is permanent and cannot be undone. You will lose all the
|
||||
data you have in this instance
|
||||
<div
|
||||
css={`
|
||||
margin-top: 50px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
`}
|
||||
>
|
||||
<Button
|
||||
onClick={closeModalWindow}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={loading}
|
||||
>
|
||||
No, Abort
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
type="primary"
|
||||
onClick={deleteInstance}
|
||||
loading={loading}
|
||||
>
|
||||
Yes, Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstanceDeleteConfirmation;
|
||||
@@ -0,0 +1,122 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Modal from '../components/Modal';
|
||||
import {
|
||||
addNextInstanceToCurrentDownload,
|
||||
downloadInstance,
|
||||
removeDownloadFromQueue,
|
||||
updateInstanceConfig
|
||||
} from '../reducers/actions';
|
||||
import { openModal, closeModal } from '../reducers/modals/actions';
|
||||
import { _getInstancesPath, _getTempPath } from '../utils/selectors';
|
||||
import { rollBackInstanceZip } from '../utils';
|
||||
|
||||
const InstanceDownloadFailed = ({
|
||||
instanceName,
|
||||
error,
|
||||
isUpdate,
|
||||
preventClose
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const instancesPath = useSelector(_getInstancesPath);
|
||||
const tempPath = useSelector(_getTempPath);
|
||||
|
||||
const ellipsedName =
|
||||
instanceName.length > 20
|
||||
? `${instanceName.substring(0, 20)}...`
|
||||
: instanceName;
|
||||
|
||||
const deleteDownload = async () => {
|
||||
await dispatch(removeDownloadFromQueue(instanceName, true));
|
||||
|
||||
dispatch(closeModal());
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
dispatch(openModal('InstanceDeleteConfirmation', { instanceName }));
|
||||
};
|
||||
|
||||
const restoreDownload = async () => {
|
||||
await dispatch(removeDownloadFromQueue(instanceName, true));
|
||||
setLoading(true);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
await rollBackInstanceZip(
|
||||
isUpdate,
|
||||
instancesPath,
|
||||
instanceName,
|
||||
tempPath,
|
||||
dispatch,
|
||||
updateInstanceConfig
|
||||
);
|
||||
|
||||
setLoading(false);
|
||||
dispatch(addNextInstanceToCurrentDownload());
|
||||
dispatch(closeModal());
|
||||
};
|
||||
|
||||
const retry = async () => {
|
||||
// Reset current download state
|
||||
dispatch(closeModal());
|
||||
dispatch(downloadInstance(instanceName));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
width: 50%;
|
||||
max-width: 550px;
|
||||
overflow-x: hidden;
|
||||
`}
|
||||
preventClose={preventClose}
|
||||
title={`Instance Download Failed - ${ellipsedName}`}
|
||||
>
|
||||
<div>
|
||||
The download for {instanceName} failed.
|
||||
<div
|
||||
css={`
|
||||
background: ${props => props.theme.palette.grey[900]};
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
`}
|
||||
>
|
||||
{'> '}
|
||||
{error.toString()}
|
||||
</div>
|
||||
<div>What do you want to do?</div>
|
||||
<div
|
||||
css={`
|
||||
margin-top: 50px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
`}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={deleteDownload}
|
||||
disabled={loading}
|
||||
>
|
||||
Delete Instance
|
||||
</Button>
|
||||
{isUpdate && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={restoreDownload}
|
||||
loading={loading}
|
||||
>
|
||||
Restore instance
|
||||
</Button>
|
||||
)}
|
||||
<Button danger type="primary" onClick={retry} disabled={loading}>
|
||||
Retry Download
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstanceDownloadFailed;
|
||||
@@ -0,0 +1,157 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Input } from 'antd';
|
||||
import fse from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { transparentize } from 'polished';
|
||||
import { useInterval } from 'rooks';
|
||||
import makeDir from 'make-dir';
|
||||
import Modal from '../components/Modal';
|
||||
import { closeModal } from '../reducers/modals/actions';
|
||||
import { _getInstance, _getInstancesPath } from '../utils/selectors';
|
||||
import { updateInstanceConfig } from '../reducers/actions';
|
||||
|
||||
const InstanceDuplicateName = ({ instanceName }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const oldInstanceName = instanceName;
|
||||
const instancesPath = useSelector(_getInstancesPath);
|
||||
const oldInstance = useSelector(state => _getInstance(state)(instanceName));
|
||||
const [newInstanceName, setNewInstanceName] = useState(
|
||||
`${oldInstanceName} copy`
|
||||
);
|
||||
const [alreadyExists, setAlreadyExists] = useState(false);
|
||||
const [invalidName, setInvalidName] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (newInstanceName) {
|
||||
const regex = /^[\sa-zA-Z0-9_.-]+$/;
|
||||
const finalWhiteSpace = /[^\s]$/;
|
||||
if (
|
||||
!regex.test(newInstanceName) ||
|
||||
!finalWhiteSpace.test(newInstanceName) ||
|
||||
newInstanceName.length >= 45
|
||||
) {
|
||||
setInvalidName(true);
|
||||
setAlreadyExists(false);
|
||||
return;
|
||||
}
|
||||
fse
|
||||
.pathExists(path.join(instancesPath, newInstanceName))
|
||||
.then(exists => {
|
||||
setAlreadyExists(exists);
|
||||
setInvalidName(false);
|
||||
return exists;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}, [newInstanceName]);
|
||||
|
||||
const { start, stop } = useInterval(() => {
|
||||
if (!loading) {
|
||||
stop();
|
||||
dispatch(closeModal());
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const duplicateInstance = async () => {
|
||||
setLoading(true);
|
||||
start();
|
||||
await makeDir(path.join(instancesPath, newInstanceName));
|
||||
|
||||
// Copy the old instance to the new instance folder
|
||||
await fse.copy(
|
||||
path.join(instancesPath, oldInstanceName),
|
||||
path.join(instancesPath, newInstanceName)
|
||||
);
|
||||
|
||||
// Reset the time played back to 0
|
||||
dispatch(
|
||||
updateInstanceConfig(newInstanceName, () => ({
|
||||
...oldInstance,
|
||||
name: newInstanceName,
|
||||
timePlayed: 0,
|
||||
lastPlayed: 0
|
||||
}))
|
||||
);
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 230px;
|
||||
width: 550px;
|
||||
max-width: 550px;
|
||||
max-height: 235px;
|
||||
overflow-x: hidden;
|
||||
`}
|
||||
title={`Duplicate Instance "${oldInstanceName}"`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
`}
|
||||
>
|
||||
<p>
|
||||
Please enter a new name for your copy of <b>{oldInstanceName}</b>
|
||||
</p>
|
||||
<Input
|
||||
size="large"
|
||||
placeholder={newInstanceName}
|
||||
onChange={e => setNewInstanceName(e.target.value)}
|
||||
readOnly={loading}
|
||||
css={`
|
||||
opacity: ${({ state }) =>
|
||||
state === 'entering' || state === 'entered' ? 0 : 1};
|
||||
transition: 0.1s ease-in-out;
|
||||
width: 300px;
|
||||
align-self: center;
|
||||
`}
|
||||
/>
|
||||
<div
|
||||
show={invalidName || alreadyExists}
|
||||
css={`
|
||||
opacity: ${props => (props.show ? 1 : 0)};
|
||||
color: ${props => props.theme.palette.error.main};
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
padding: 3px;
|
||||
height: 30px;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
border-radius: ${props => props.theme.shape.borderRadius};
|
||||
background: ${props =>
|
||||
transparentize(0.7, props.theme.palette.grey[700])};
|
||||
`}
|
||||
>
|
||||
{invalidName &&
|
||||
'Instance name is not valid or too long. Please try another one'}
|
||||
{alreadyExists && 'An instance with this name already exists!'}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
`}
|
||||
>
|
||||
<Button
|
||||
onClick={duplicateInstance}
|
||||
loading={loading}
|
||||
disabled={invalidName || alreadyExists}
|
||||
type="primary"
|
||||
>
|
||||
Duplicate Instance
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstanceDuplicateName;
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faLongArrowAltLeft } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export default function BackButton({ onClick, disabled = false }) {
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
onClick(s => s - 1);
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
css={`
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
bottom: 20px;
|
||||
width: 70px;
|
||||
height: 40px;
|
||||
transition: 0.1s ease-in-out;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
font-size: 40px;
|
||||
color: ${disabled
|
||||
? props => props.theme.palette.text.disabled
|
||||
: props => props.theme.palette.text.icon};
|
||||
${disabled ? '' : 'cursor: pointer;'}
|
||||
&:hover {
|
||||
background-color: ${disabled
|
||||
? 'transparent'
|
||||
: props => props.theme.action.hover};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faLongArrowAltLeft} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faLongArrowAltRight } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export default function ContinueButton({ onClick, disabled = false }) {
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
onClick(s => s + 1);
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
css={`
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
width: 70px;
|
||||
height: 40px;
|
||||
transition: 0.1s ease-in-out;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
font-size: 40px;
|
||||
color: ${disabled
|
||||
? props => props.theme.palette.text.disabled
|
||||
: props => props.theme.palette.text.icon};
|
||||
${disabled ? '' : 'cursor: pointer;'}
|
||||
&:hover {
|
||||
background-color: ${disabled
|
||||
? 'transparent'
|
||||
: props => props.theme.action.hover};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faLongArrowAltRight} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import dirTree from 'directory-tree';
|
||||
import path from 'path';
|
||||
import { Button, Input } from 'antd';
|
||||
import { Transition } from 'react-transition-group';
|
||||
import styled from 'styled-components';
|
||||
import { faFolder } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import ContinueButton from './ContinueButton';
|
||||
|
||||
const InlineBlock = styled.span`
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
`;
|
||||
|
||||
export default function FirstStep({
|
||||
instancePath,
|
||||
setTreeData,
|
||||
filePath,
|
||||
showFileDialog,
|
||||
setPackVersion,
|
||||
packVersion,
|
||||
setPage,
|
||||
page,
|
||||
packAuthor,
|
||||
setPackAuthor,
|
||||
packZipName,
|
||||
setPackZipName,
|
||||
setSelectedFiles,
|
||||
inProp
|
||||
}) {
|
||||
const fileBlackList = [
|
||||
path.join(instancePath, 'config.json'),
|
||||
path.join(instancePath, 'natives'),
|
||||
path.join(instancePath, 'thumbnail.png'),
|
||||
path.join(instancePath, 'usercache.json'),
|
||||
path.join(instancePath, 'usernamecache.json'),
|
||||
path.join(instancePath, 'logs'),
|
||||
path.join(instancePath, '.mixin.out'),
|
||||
path.join(instancePath, '.fabric'),
|
||||
path.join(instancePath, 'screenshots'),
|
||||
path.join(instancePath, 'crash-reports'),
|
||||
path.join(instancePath, 'manifest.json')
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (page !== 1) return;
|
||||
const getTreeData = async () => {
|
||||
const arr = dirTree(instancePath);
|
||||
|
||||
const flatDirArray = objectIn => {
|
||||
const arrayResult = [];
|
||||
function innerObjectLoop(innerObject) {
|
||||
if (!innerObject || innerObject.length === 0) return;
|
||||
// eslint-disable-next-line array-callback-return
|
||||
innerObject.map(child => {
|
||||
if (fileBlackList.some(file => file === child.path)) return;
|
||||
arrayResult.push(child.path);
|
||||
innerObjectLoop(child.children);
|
||||
});
|
||||
}
|
||||
if (!objectIn || objectIn.length === 0) return arrayResult;
|
||||
innerObjectLoop(objectIn);
|
||||
return arrayResult;
|
||||
};
|
||||
|
||||
const mapObject = (children, disableChildren = false) => {
|
||||
if (!children || children.length === 0) return [];
|
||||
const files = [];
|
||||
const dirs = [];
|
||||
// eslint-disable-next-line array-callback-return
|
||||
children.map(child => {
|
||||
const disableBool =
|
||||
disableChildren || fileBlackList.some(file => file === child.path);
|
||||
const childResult = {
|
||||
title: child.name,
|
||||
key: child.path,
|
||||
selectable: false,
|
||||
disableCheckbox: disableBool,
|
||||
children: mapObject(child.children, disableBool)
|
||||
};
|
||||
if (child.type === 'file') files.push(childResult);
|
||||
if (child.type === 'directory') dirs.push(childResult);
|
||||
});
|
||||
|
||||
function arrSort(innerArrayToSort) {
|
||||
return innerArrayToSort
|
||||
.map((el, i) => {
|
||||
return { index: i, value: el.title.toLowerCase() };
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.value > b.value) {
|
||||
return 1;
|
||||
}
|
||||
if (a.value < b.value) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.map(el => {
|
||||
return innerArrayToSort[el.index];
|
||||
});
|
||||
}
|
||||
return arrSort(dirs).concat(arrSort(files));
|
||||
};
|
||||
|
||||
function rootNode(localName, localPath, children = []) {
|
||||
return [
|
||||
{
|
||||
title: localName,
|
||||
key: localPath,
|
||||
selectable: false,
|
||||
expanded: true,
|
||||
children
|
||||
}
|
||||
];
|
||||
}
|
||||
await setTreeData(
|
||||
rootNode('Instance content', instancePath, mapObject(arr.children))
|
||||
);
|
||||
await setSelectedFiles(flatDirArray(arr.children));
|
||||
};
|
||||
|
||||
getTreeData();
|
||||
}, [page]);
|
||||
|
||||
function filePathDisplay() {
|
||||
if (!filePath) return '';
|
||||
let fileName = `${packZipName}-${packVersion}.zip`;
|
||||
if (fileName.length >= 25)
|
||||
fileName = `...${fileName.slice(fileName.length - 25)}`;
|
||||
const joinedPath = path.join(filePath, fileName);
|
||||
|
||||
if (joinedPath.length >= 45)
|
||||
return `...${joinedPath.slice(joinedPath.length - 45)}`;
|
||||
return joinedPath;
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition in={inProp} timeout={200}>
|
||||
{state => (
|
||||
<Animation state={state}>
|
||||
<div
|
||||
css={`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
margin-top: 40px;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
flex: 5;
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
height: 85%;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
width: calc(100% - 40px);
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
`}
|
||||
>
|
||||
<InlineBlock>
|
||||
<h3>Name</h3>
|
||||
</InlineBlock>
|
||||
<span>
|
||||
<Input
|
||||
type="text"
|
||||
name="inputPackAuthor"
|
||||
allowClear="true"
|
||||
defaultValue={packZipName}
|
||||
maxLength={50}
|
||||
css={`
|
||||
width: 300px !important;
|
||||
`}
|
||||
onChange={e => setPackZipName(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
`}
|
||||
>
|
||||
<InlineBlock>
|
||||
<h3>Version</h3>
|
||||
</InlineBlock>
|
||||
<span>
|
||||
<Input
|
||||
type="text"
|
||||
name="inputPackVersion"
|
||||
defaultValue={packVersion}
|
||||
maxLength={10}
|
||||
allowClear="true"
|
||||
css={`
|
||||
width: 300px !important;
|
||||
`}
|
||||
onChange={e => setPackVersion(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 40px;
|
||||
`}
|
||||
>
|
||||
<InlineBlock>
|
||||
<h3>Author</h3>
|
||||
</InlineBlock>
|
||||
<span
|
||||
css={`
|
||||
display: inline-block;
|
||||
`}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
name="inputPackAuthor"
|
||||
defaultValue={packAuthor}
|
||||
maxLength={50}
|
||||
allowClear="true"
|
||||
css={`
|
||||
width: 300px !important;
|
||||
`}
|
||||
onChange={e => setPackAuthor(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
name="filePathDisplay"
|
||||
disabled
|
||||
value={filePathDisplay()}
|
||||
css={`
|
||||
margin-right: 10px !important;
|
||||
`}
|
||||
/>
|
||||
<Button type="primary" onClick={showFileDialog}>
|
||||
<FontAwesomeIcon icon={faFolder} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ContinueButton
|
||||
onClick={setPage}
|
||||
disabled={
|
||||
!(packZipName && packVersion && packAuthor && filePath)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Animation>
|
||||
)}
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
const Animation = styled.div`
|
||||
transition: 0.2s ease-in-out;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
transform: translateX(
|
||||
${({ state }) => (state === 'exiting' || state === 'exited' ? -100 : 0)}%
|
||||
);
|
||||
`;
|
||||
@@ -0,0 +1,107 @@
|
||||
import React from 'react';
|
||||
import { Tree } from 'antd';
|
||||
import { Transition } from 'react-transition-group';
|
||||
import styled from 'styled-components';
|
||||
import BackButton from './BackButton';
|
||||
import ContinueButton from './ContinueButton';
|
||||
|
||||
export default function SecondStep({
|
||||
setSelectedFiles,
|
||||
setPage,
|
||||
treeData,
|
||||
instancePath,
|
||||
selectedFiles,
|
||||
inProp,
|
||||
page
|
||||
}) {
|
||||
const onCheck = LcheckedKeys => {
|
||||
setSelectedFiles(LcheckedKeys);
|
||||
};
|
||||
const computeTranslate = state => {
|
||||
if (page === 0 || !page) {
|
||||
if (state === 'exiting' || state === 'exited') {
|
||||
return 100;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (state === 'exiting' || state === 'exited') {
|
||||
return -100;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition in={inProp} timeout={200}>
|
||||
{state => (
|
||||
<Animation state={state} computeTranslate={computeTranslate}>
|
||||
<div
|
||||
css={`
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
display: flex;
|
||||
margin: 20px;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
flex: 5;
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
text-align: center;
|
||||
height: calc(100% - 40px);
|
||||
`}
|
||||
>
|
||||
<h2>Files to include in export</h2>
|
||||
<div
|
||||
css={`
|
||||
overflow-y: auto;
|
||||
height: calc(100% - 45px);
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-color: ${props => props.theme.palette.primary.dark};
|
||||
background-color: ${props => props.theme.palette.grey[800]};
|
||||
`}
|
||||
>
|
||||
{treeData.length && (
|
||||
<Tree
|
||||
checkable
|
||||
selectable
|
||||
onCheck={onCheck}
|
||||
treeData={treeData}
|
||||
defaultExpandedKeys={[instancePath]}
|
||||
defaultCheckedKeys={selectedFiles}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<BackButton onClick={setPage} />
|
||||
<ContinueButton onClick={setPage} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Animation>
|
||||
)}
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
const Animation = styled.div`
|
||||
transition: 0.2s ease-in-out;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
transform: translateX(
|
||||
${({ state, computeTranslate }) => computeTranslate(state)}%
|
||||
);
|
||||
`;
|
||||
@@ -0,0 +1,345 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { Button } from 'antd';
|
||||
import path from 'path';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import fse from 'fs-extra';
|
||||
import { add as add7z } from 'node-7z';
|
||||
import makeDir from 'make-dir';
|
||||
import { Transition } from 'react-transition-group';
|
||||
import styled from 'styled-components';
|
||||
import pMap from 'p-map';
|
||||
import { get7zPath } from '../../../../app/desktop/utils';
|
||||
import { FABRIC, VANILLA, FORGE } from '../../../utils/constants';
|
||||
import { getAddon } from '../../../api';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} archiveName Name of archive without file extension.
|
||||
* @param {String} zipDestPath Destination path (cwd of 7z).
|
||||
* @param {Array} filesArray Array of files to include. Relative to current working directory unless full path is passed for each file.
|
||||
*/
|
||||
const createZip = async (archiveName, zipDestPath, filesArray) => {
|
||||
const sevenZipPath = await get7zPath();
|
||||
const zipCreation = add7z(
|
||||
path.join(zipDestPath, `${archiveName}.zip`),
|
||||
filesArray,
|
||||
{
|
||||
$bin: sevenZipPath,
|
||||
$raw: ['-tzip'],
|
||||
$spawnOptions: { cwd: zipDestPath, shell: true }
|
||||
}
|
||||
);
|
||||
await new Promise((resolve, reject) => {
|
||||
zipCreation.on('end', () => {
|
||||
resolve();
|
||||
});
|
||||
zipCreation.on('error', err => {
|
||||
reject(err.stderr);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default function ThirdStep({
|
||||
instanceName,
|
||||
instancesPath,
|
||||
instanceConfig,
|
||||
filePath, // Destionation path for zip
|
||||
packVersion,
|
||||
tempPath,
|
||||
packAuthor,
|
||||
selectedFiles,
|
||||
closeModal,
|
||||
packZipName,
|
||||
inProp,
|
||||
page
|
||||
}) {
|
||||
const [isCompleted, setIsCompleted] = useState(false);
|
||||
const { loader, mods } = instanceConfig;
|
||||
const mcVersion = loader?.mcVersion;
|
||||
const modloaderName = loader?.loaderType;
|
||||
const dispatch = useDispatch();
|
||||
const tempExport = path.join(tempPath, instanceName);
|
||||
|
||||
const openExportLocation = async () => {
|
||||
await ipcRenderer.invoke('openFolder', filePath);
|
||||
};
|
||||
|
||||
// Construct manifest contents
|
||||
const createManifest = async (modsArray = mods) => {
|
||||
let loaderObj = {};
|
||||
switch (modloaderName) {
|
||||
case FORGE:
|
||||
loaderObj = {
|
||||
id: `${modloaderName}-${loader?.loaderVersion.slice(
|
||||
mcVersion.length + 1
|
||||
)}`,
|
||||
primary: true
|
||||
};
|
||||
break;
|
||||
case FABRIC:
|
||||
loaderObj = {
|
||||
id: `${modloaderName}-${loader?.loaderVersion}`,
|
||||
loader: loader?.fileID,
|
||||
primary: true
|
||||
};
|
||||
break;
|
||||
case VANILLA:
|
||||
loaderObj = {
|
||||
id: modloaderName,
|
||||
primary: true
|
||||
};
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown loader type. Cannot export modloaderName: ${modloaderName}`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
minecraft: {
|
||||
version: mcVersion,
|
||||
modLoaders: [loaderObj]
|
||||
},
|
||||
manifestType: 'minecraftModpack',
|
||||
overrides: 'overrides',
|
||||
manifestVersion: 1,
|
||||
version: packVersion,
|
||||
author: packAuthor,
|
||||
projectID:
|
||||
modloaderName === 'forge' && loader.length > 3
|
||||
? parseInt(loader?.fileID, 10)
|
||||
: undefined,
|
||||
name: packZipName,
|
||||
files: modsArray
|
||||
.filter(mod => mod?.projectID)
|
||||
.map(mod => ({
|
||||
projectID: mod.projectID,
|
||||
fileID: mod.fileID,
|
||||
required: true
|
||||
}))
|
||||
};
|
||||
};
|
||||
|
||||
const createModListHtml = async () => {
|
||||
const mappedMods = await Promise.all(
|
||||
mods
|
||||
.filter(mod => mod.projectID)
|
||||
.map(async mod => {
|
||||
let ok = false;
|
||||
let tries = 0;
|
||||
|
||||
do {
|
||||
try {
|
||||
tries += 1;
|
||||
if (tries !== 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
const data = await getAddon(mod.projectID);
|
||||
|
||||
ok = true;
|
||||
return {
|
||||
name: data.name,
|
||||
url: data.websiteUrl,
|
||||
author: data.authors[0].name
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} while (!ok && tries <= 3);
|
||||
})
|
||||
);
|
||||
|
||||
return `<ul>${mappedMods
|
||||
.map(
|
||||
mod => `<li><a href=${mod?.url}>${mod?.name}(${mod?.author})</a></li>`
|
||||
)
|
||||
.join('')}</ul>`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (page !== 2) return;
|
||||
const workOnFiles = async () => {
|
||||
// Make sure mod with curseforge ids gets removed from mods folder if included.
|
||||
const filteredFiles = mods
|
||||
? selectedFiles.filter(file => {
|
||||
const match = mods.find(
|
||||
mod => mod.fileName === path.basename(file)
|
||||
);
|
||||
if (match && match.projectID) return false;
|
||||
return true;
|
||||
})
|
||||
: selectedFiles;
|
||||
|
||||
// Filter only selected curseforge mods for use in manifest.
|
||||
const filteredCurseforgeMods = mods
|
||||
? mods.filter(mod => {
|
||||
const match = selectedFiles.find(
|
||||
file => mod.fileName === path.basename(file)
|
||||
);
|
||||
if (match && mod.projectID) return true;
|
||||
return false;
|
||||
})
|
||||
: selectedFiles;
|
||||
|
||||
// Process files from selection
|
||||
await makeDir(path.join(tempExport, 'overrides'));
|
||||
|
||||
await pMap(
|
||||
filteredFiles,
|
||||
async file => {
|
||||
const stats = await fse.stat(file);
|
||||
if (stats.isFile()) {
|
||||
const slicedFile = file.slice(
|
||||
path.join(instancesPath, instanceName).length + 1
|
||||
);
|
||||
try {
|
||||
await fse.ensureLink(
|
||||
file,
|
||||
path.join(tempExport, 'overrides', slicedFile)
|
||||
);
|
||||
} catch {
|
||||
await fse.copy(
|
||||
file,
|
||||
path.join(tempExport, 'overrides', slicedFile)
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ concurrency: 3 }
|
||||
);
|
||||
|
||||
// Create manifest file
|
||||
const manifestPath = path.join(path.join(tempExport, 'manifest.json'));
|
||||
const manifestString = await createManifest(filteredCurseforgeMods);
|
||||
await fse.outputJson(manifestPath, manifestString);
|
||||
|
||||
// Create modlist.html file
|
||||
const modlistHtmlPath = path.join(path.join(tempExport, 'modlist.html'));
|
||||
const modlistHtmlContent = await createModListHtml();
|
||||
await fse.writeFile(modlistHtmlPath, modlistHtmlContent);
|
||||
|
||||
// Create zipped export file
|
||||
const filesToZip = [
|
||||
path.join(tempExport, 'modlist.html'),
|
||||
path.join(tempExport, 'overrides'),
|
||||
path.join(tempExport, 'manifest.json')
|
||||
];
|
||||
|
||||
await fse.remove(
|
||||
path.join(filePath, `${packZipName}-${packVersion}.zip`)
|
||||
);
|
||||
await createZip(`${packZipName}-${packVersion}`, filePath, filesToZip);
|
||||
|
||||
// Clean up temp folder
|
||||
await fse.remove(tempExport);
|
||||
|
||||
setIsCompleted(true);
|
||||
};
|
||||
|
||||
workOnFiles();
|
||||
}, [page]);
|
||||
|
||||
return (
|
||||
<Transition in={inProp} timeout={200}>
|
||||
{state => (
|
||||
<Animation state={state}>
|
||||
<div
|
||||
css={`
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
display: flex;
|
||||
margin: 20px;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
flex: 5;
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
height: 85%;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
`}
|
||||
>
|
||||
{isCompleted ? (
|
||||
<div>
|
||||
<h1>
|
||||
All Done!{' '}
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.green};
|
||||
`}
|
||||
/>
|
||||
</h1>
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={openExportLocation}
|
||||
css={`
|
||||
margin-top: 20px;
|
||||
`}
|
||||
>
|
||||
Open Export Location
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => dispatch(closeModal())}
|
||||
css={`
|
||||
margin-top: 20px;
|
||||
`}
|
||||
>
|
||||
Go Back To Instances
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<h2>We're doing some magical stuff</h2>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Animation>
|
||||
)}
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
const Animation = styled.div`
|
||||
transition: 0.2s ease-in-out;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
transform: translateX(
|
||||
${({ state }) => (state === 'exiting' || state === 'exited' ? 100 : 0)}%
|
||||
);
|
||||
`;
|
||||
@@ -0,0 +1,96 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import path from 'path';
|
||||
import {
|
||||
_getInstance,
|
||||
_getCurrentAccount,
|
||||
_getInstancesPath,
|
||||
_getTempPath
|
||||
} from '../../../utils/selectors';
|
||||
import Modal from '../../../components/Modal';
|
||||
import { closeModal } from '../../../reducers/modals/actions';
|
||||
import FirstStep from './FirstStep';
|
||||
import SecondStep from './SecondStep';
|
||||
import ThirdStep from './ThirdStep';
|
||||
|
||||
const InstanceExportCurseForge = ({ instanceName }) => {
|
||||
const [page, setPage] = useState(0);
|
||||
const instanceConfig = useSelector(state =>
|
||||
_getInstance(state)(instanceName)
|
||||
);
|
||||
|
||||
const currentAccount = useSelector(_getCurrentAccount);
|
||||
const username = currentAccount.selectedProfile.name;
|
||||
const [filePath, setFilePath] = useState(null);
|
||||
const [packVersion, setPackVersion] = useState('1.0');
|
||||
const [packAuthor, setPackAuthor] = useState(username);
|
||||
const [packZipName, setPackZipName] = useState(instanceName);
|
||||
const [selectedFiles, setSelectedFiles] = useState([]);
|
||||
const instancesPath = useSelector(_getInstancesPath);
|
||||
const tempPath = useSelector(_getTempPath);
|
||||
const [treeData, setTreeData] = useState([]);
|
||||
const instancePath = path.join(instancesPath, instanceName);
|
||||
|
||||
const openFolderDialog = async () => {
|
||||
const dialog = await ipcRenderer.invoke('openFolderDialog', instancesPath);
|
||||
if (dialog.canceled) return;
|
||||
setFilePath(dialog.filePaths[0]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 400px;
|
||||
width: 500px;
|
||||
overflow: hidden;
|
||||
vertial-align: middle;
|
||||
`}
|
||||
title="Export Instance"
|
||||
>
|
||||
<FirstStep
|
||||
setPackZipName={setPackZipName}
|
||||
packZipName={packZipName}
|
||||
filePath={filePath}
|
||||
showFileDialog={openFolderDialog}
|
||||
setPackVersion={setPackVersion}
|
||||
packVersion={packVersion}
|
||||
packAuthor={packAuthor}
|
||||
setPackAuthor={setPackAuthor}
|
||||
setPage={setPage}
|
||||
page={page}
|
||||
instancePath={instancePath}
|
||||
setTreeData={setTreeData}
|
||||
treeData={treeData}
|
||||
setSelectedFiles={setSelectedFiles}
|
||||
selectedFiles={selectedFiles}
|
||||
inProp={page === 0}
|
||||
/>
|
||||
<SecondStep
|
||||
treeData={treeData}
|
||||
setSelectedFiles={setSelectedFiles}
|
||||
selectedFiles={selectedFiles}
|
||||
setPage={setPage}
|
||||
page={page}
|
||||
instancePath={instancePath}
|
||||
inProp={page === 1}
|
||||
/>
|
||||
<ThirdStep
|
||||
packZipName={packZipName}
|
||||
filePath={filePath}
|
||||
page={page}
|
||||
instanceName={instanceName}
|
||||
instanceConfig={instanceConfig}
|
||||
selectedFiles={selectedFiles}
|
||||
closeModal={closeModal}
|
||||
packVersion={packVersion}
|
||||
tempPath={tempPath}
|
||||
packAuthor={packAuthor}
|
||||
instancesPath={instancesPath}
|
||||
inProp={page === 2}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(InstanceExportCurseForge);
|
||||
@@ -0,0 +1,243 @@
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Select, Button } from 'antd';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import ReactHtmlParser from 'react-html-parser';
|
||||
import path from 'path';
|
||||
import { getAddonFiles, getAddonFileChangelog } from '../../api';
|
||||
import { changeModpackVersion } from '../../reducers/actions';
|
||||
import { closeModal } from '../../reducers/modals/actions';
|
||||
import { _getInstancesPath, _getTempPath } from '../../utils/selectors';
|
||||
import { makeInstanceRestorePoint } from '../../utils';
|
||||
|
||||
const Modpack = ({ modpackId, instanceName, manifest }) => {
|
||||
const [files, setFiles] = useState([]);
|
||||
const [versionName, setVersionName] = useState(null);
|
||||
const [selectedIndex, setSelectedIndex] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const tempPath = useSelector(_getTempPath);
|
||||
const instancesPath = useSelector(_getInstancesPath);
|
||||
|
||||
const initData = async () => {
|
||||
setLoading(true);
|
||||
if (manifest) {
|
||||
setVersionName(`${manifest?.name} - ${manifest?.version}`);
|
||||
const data = await getAddonFiles(modpackId);
|
||||
const mappedFiles = await Promise.all(
|
||||
data.map(async v => {
|
||||
const changelog = await getAddonFileChangelog(modpackId, v.id);
|
||||
return {
|
||||
...v,
|
||||
changelog
|
||||
};
|
||||
})
|
||||
);
|
||||
setFiles(mappedFiles);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initData().catch(console.error);
|
||||
}, []);
|
||||
|
||||
const getReleaseType = id => {
|
||||
switch (id) {
|
||||
case 1:
|
||||
return (
|
||||
<span
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.green};
|
||||
`}
|
||||
>
|
||||
[Stable]
|
||||
</span>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<span
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.yellow};
|
||||
`}
|
||||
>
|
||||
[Beta]
|
||||
</span>
|
||||
);
|
||||
case 3:
|
||||
default:
|
||||
return (
|
||||
<span
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.red};
|
||||
`}
|
||||
>
|
||||
[Alpha]
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = value => setSelectedIndex(value);
|
||||
const newInstancePath = path.join(tempPath, `${instanceName}__RESTORE`);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
Installed version: {versionName}
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`}
|
||||
>
|
||||
<StyledSelect
|
||||
placeholder={loading ? 'Loading Versions' : 'Select a version'}
|
||||
onChange={handleChange}
|
||||
listItemHeight={50}
|
||||
listHeight={400}
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
virtual={false}
|
||||
>
|
||||
{(files || []).map((file, index) => (
|
||||
<Select.Option title={file.displayName} key={file.id} value={index}>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
height: 50px;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
flex: 7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
{file.displayName}
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
flex: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
`}
|
||||
>
|
||||
<div>{file.gameVersions[0]}</div>
|
||||
<div>{getReleaseType(file.releaseType)}</div>
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
flex: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
{new Date(file.fileDate).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</StyledSelect>
|
||||
</div>
|
||||
<Changelog>
|
||||
<div>{files[selectedIndex]?.displayName}</div>
|
||||
{files[selectedIndex]?.changelog &&
|
||||
ReactHtmlParser(files[selectedIndex]?.changelog)}
|
||||
</Changelog>
|
||||
<Button
|
||||
loading={installing}
|
||||
type="primary"
|
||||
disabled={selectedIndex === null}
|
||||
onClick={async () => {
|
||||
setInstalling(true);
|
||||
await makeInstanceRestorePoint(
|
||||
newInstancePath,
|
||||
instancesPath,
|
||||
instanceName
|
||||
);
|
||||
await dispatch(
|
||||
changeModpackVersion(instanceName, files[selectedIndex])
|
||||
);
|
||||
setInstalling(false);
|
||||
dispatch(closeModal());
|
||||
}}
|
||||
>
|
||||
Switch Version
|
||||
</Button>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Modpack);
|
||||
|
||||
const Container = styled.div`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledSelect = styled(Select)`
|
||||
width: 650px;
|
||||
height: 50px;
|
||||
margin-top: 20px;
|
||||
.ant-select-selection-placeholder {
|
||||
height: 50px !important;
|
||||
line-height: 50px !important;
|
||||
}
|
||||
.ant-select-selector {
|
||||
height: 50px !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
.ant-select-selection-item {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
& > div {
|
||||
& > div:nth-child(2) {
|
||||
& > div:last-child {
|
||||
height: 10px;
|
||||
line-height: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Changelog = styled.div`
|
||||
perspective: 1px;
|
||||
transform-style: preserve-3d;
|
||||
height: calc(100% - 160px);
|
||||
background: ${props => props.theme.palette.grey[900]};
|
||||
width: calc(100% - 80px);
|
||||
word-break: break-all;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
margin: 20px 40px;
|
||||
padding: 20px;
|
||||
font-size: 20px;
|
||||
* {
|
||||
color: ${props => props.theme.palette.text.primary} !important;
|
||||
}
|
||||
& > div:first-child {
|
||||
font-size: 24px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,890 @@
|
||||
import React, { memo, useState, useEffect, useMemo } from 'react';
|
||||
import { clipboard, ipcRenderer } from 'electron';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import memoize from 'memoize-one';
|
||||
import { ContextMenuTrigger, ContextMenu, MenuItem } from 'react-contextmenu';
|
||||
import { Portal } from 'react-portal';
|
||||
import path from 'path';
|
||||
import pMap from 'p-map';
|
||||
import { FixedSizeList as List, areEqual } from 'react-window';
|
||||
import { Checkbox, Input, Button, Switch, Spin, Dropdown, Menu } from 'antd';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faTrash,
|
||||
faArrowDown,
|
||||
faDownload,
|
||||
faEllipsisV,
|
||||
faCopy,
|
||||
faFolder
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { Transition } from 'react-transition-group';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import fse from 'fs-extra';
|
||||
import makeDir from 'make-dir';
|
||||
import curseForgeIcon from '../../assets/curseforgeIcon.webp';
|
||||
import {
|
||||
_getInstance,
|
||||
_getInstancesPath,
|
||||
_getTempPath
|
||||
} from '../../utils/selectors';
|
||||
import {
|
||||
updateInstanceConfig,
|
||||
deleteMod,
|
||||
updateMod,
|
||||
initLatestMods
|
||||
} from '../../reducers/actions';
|
||||
import { openModal } from '../../reducers/modals/actions';
|
||||
import { makeModRestorePoint } from '../../utils';
|
||||
|
||||
const Header = styled.div`
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
background: ${props => props.theme.palette.grey[700]};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const RowContainer = styled.div.attrs(props => ({
|
||||
style: props.override
|
||||
}))`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: ${props =>
|
||||
props.disabled || props.selected
|
||||
? 'transparent'
|
||||
: props.theme.palette.grey[800]};
|
||||
${props =>
|
||||
props.disabled &&
|
||||
!props.selected &&
|
||||
`box-shadow: inset 0 0 0 3px ${props.theme.palette.colors.red};`}
|
||||
${props =>
|
||||
props.selected &&
|
||||
`box-shadow: inset 0 0 0 3px ${props.theme.palette.primary.main};`}
|
||||
transition: border 0.1s ease-in-out;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
box-sizing: content-box;
|
||||
padding: 0 10px;
|
||||
&:hover {
|
||||
.rowCenterContent {
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
}
|
||||
}
|
||||
.leftPartContent {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
& > * {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
.rowCenterContent {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: color 0.1s ease-in-out;
|
||||
height: 100%;
|
||||
${props =>
|
||||
props.isHovered ? `color: ${props.theme.palette.text.primary};` : ''}
|
||||
cursor: pointer;
|
||||
svg {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.rightPartContent {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
& > * {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const RowContainerBackground = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
|
||||
${props =>
|
||||
props.selected &&
|
||||
` background: repeating-linear-gradient(
|
||||
45deg,
|
||||
${props.theme.palette.primary.main},
|
||||
${props.theme.palette.primary.main} 10px,
|
||||
${props.theme.palette.primary.dark} 10px,
|
||||
${props.theme.palette.primary.dark} 20px
|
||||
);`};
|
||||
|
||||
${props =>
|
||||
props.disabled &&
|
||||
!props.selected &&
|
||||
`background: repeating-linear-gradient(
|
||||
45deg,
|
||||
${props.theme.palette.colors.red},
|
||||
${props.theme.palette.colors.red} 10px,
|
||||
${props.theme.palette.colors.maximumRed} 10px,
|
||||
${props.theme.palette.colors.maximumRed} 20px
|
||||
);`};
|
||||
filter: brightness(60%);
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
opacity: ${props => (props.disabled || props.selected ? 1 : 0)};
|
||||
`;
|
||||
|
||||
const NotItemsAvailable = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const DragEnterEffect = styled.div`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: solid 5px ${props => props.theme.palette.primary.main};
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: 3px;
|
||||
z-index: ${props =>
|
||||
props.transitionState !== 'entering' && props.transitionState !== 'entered'
|
||||
? -1
|
||||
: 2};
|
||||
backdrop-filter: blur(4px);
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 0, 0, 0.3) 40%,
|
||||
rgba(0, 0, 0, 0.3) 40%
|
||||
);
|
||||
opacity: ${({ transitionState }) =>
|
||||
transitionState === 'entering' || transitionState === 'entered' ? 1 : 0};
|
||||
`;
|
||||
|
||||
const StyledDropdown = styled.div`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: ${props => props.theme.palette.grey[400]};
|
||||
}
|
||||
`;
|
||||
|
||||
export const keyFrameMoveUpDown = keyframes`
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
const OpenFolderButton = styled(FontAwesomeIcon)`
|
||||
transition: color 0.1s ease-in-out;
|
||||
cursor: pointer;
|
||||
margin: 0 10px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
path {
|
||||
cursor: pointer;
|
||||
transition: color 0.1s ease-in-out;
|
||||
color: ${props => props.theme.palette.primary.main};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const DragArrow = styled(FontAwesomeIcon)`
|
||||
${props =>
|
||||
props.fileDrag ? props.theme.palette.primary.main : 'transparent'};
|
||||
|
||||
color: ${props => props.theme.palette.primary.main};
|
||||
|
||||
animation: ${keyFrameMoveUpDown} 1.5s linear infinite;
|
||||
`;
|
||||
|
||||
const CopyTitle = styled.h1`
|
||||
${props =>
|
||||
props.fileDrag ? props.theme.palette.primary.main : 'transparent'};
|
||||
|
||||
color: ${props => props.theme.palette.primary.main};
|
||||
|
||||
animation: ${keyFrameMoveUpDown} 1.5s linear infinite;
|
||||
`;
|
||||
|
||||
const DeleteSelectedMods = styled(({ selectedMods, ...props }) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<FontAwesomeIcon {...props} />
|
||||
))`
|
||||
margin: 0 10px;
|
||||
${props =>
|
||||
props.selectedMods > 0 &&
|
||||
`&:hover {
|
||||
cursor: pointer;
|
||||
path {
|
||||
cursor: pointer;
|
||||
transition: color 0.1s ease-in-out;
|
||||
color: ${props.theme.palette.error.main};
|
||||
}
|
||||
}`}
|
||||
`;
|
||||
|
||||
const deleteMods = async (
|
||||
instanceName,
|
||||
instancePath,
|
||||
selectedMods,
|
||||
dispatch
|
||||
) => {
|
||||
await dispatch(
|
||||
updateInstanceConfig(instanceName, prev => ({
|
||||
...prev,
|
||||
mods: prev.mods.filter(m => !selectedMods.includes(m.fileName))
|
||||
}))
|
||||
);
|
||||
await Promise.all(
|
||||
selectedMods.map(fileName =>
|
||||
fse.remove(path.join(instancePath, 'mods', fileName))
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const toggleModDisabled = async (
|
||||
c,
|
||||
instanceName,
|
||||
instancePath,
|
||||
mod,
|
||||
dispatch
|
||||
) => {
|
||||
const destFileName = c
|
||||
? mod.fileName.replace('.disabled', '')
|
||||
: `${mod.fileName}.disabled`;
|
||||
await dispatch(
|
||||
updateInstanceConfig(instanceName, prev => ({
|
||||
...prev,
|
||||
mods: prev.mods.map(m => {
|
||||
if (m.fileName === mod.fileName) {
|
||||
return {
|
||||
...m,
|
||||
fileName: destFileName
|
||||
};
|
||||
}
|
||||
return m;
|
||||
})
|
||||
}))
|
||||
);
|
||||
await fse.move(
|
||||
path.join(instancePath, 'mods', mod.fileName),
|
||||
path.join(instancePath, 'mods', destFileName)
|
||||
);
|
||||
};
|
||||
|
||||
const Row = memo(({ index, style, data }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [updateLoading, setUpdateLoading] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const curseReleaseChannel = useSelector(
|
||||
state => state.settings.curseReleaseChannel
|
||||
);
|
||||
const {
|
||||
items,
|
||||
instanceName,
|
||||
instancePath,
|
||||
gameVersions,
|
||||
selectedMods,
|
||||
setSelectedMods,
|
||||
latestMods
|
||||
} = data;
|
||||
const item = items[index];
|
||||
const isUpdateAvailable =
|
||||
latestMods[item.projectID] &&
|
||||
latestMods[item.projectID].id !== item.fileID &&
|
||||
latestMods[item.projectID].releaseType <= curseReleaseChannel;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const tempPath = useSelector(_getTempPath);
|
||||
const newModPath = path.join(tempPath, `${item.fileName}__RESTORE`);
|
||||
const modsPath = path.join(instancePath, 'mods');
|
||||
|
||||
const name = item.fileName
|
||||
.replace('.jar', '')
|
||||
.replace('.zip', '')
|
||||
.replace('.disabled', '');
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenuTrigger id={item.displayName}>
|
||||
<RowContainer
|
||||
index={index}
|
||||
name={item.fileName}
|
||||
isHovered={isHovered}
|
||||
selected={selectedMods.includes(item.fileName)}
|
||||
disabled={path.extname(item.fileName) === '.disabled'}
|
||||
override={{
|
||||
...style,
|
||||
top: style.top + 15,
|
||||
height: style.height - 15,
|
||||
position: 'absolute',
|
||||
width: '97%',
|
||||
margin: '15px 0',
|
||||
transition: 'height 0.2s ease-in-out'
|
||||
}}
|
||||
>
|
||||
<div className="leftPartContent">
|
||||
<Checkbox
|
||||
checked={selectedMods.includes(item.fileName)}
|
||||
onChange={e => {
|
||||
if (e.target.checked) {
|
||||
setSelectedMods([...selectedMods, item.fileName]);
|
||||
} else {
|
||||
setSelectedMods(
|
||||
selectedMods.filter(v => v !== item.fileName)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{item.fileID && (
|
||||
<img src={curseForgeIcon} height="15px" alt="curseforge" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!item.fileID) return;
|
||||
dispatch(
|
||||
openModal('ModOverview', {
|
||||
projectID: item.projectID,
|
||||
fileID: item.fileID,
|
||||
fileName: item.fileName,
|
||||
gameVersions,
|
||||
instanceName
|
||||
})
|
||||
);
|
||||
}}
|
||||
className="rowCenterContent"
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<div className="rightPartContent">
|
||||
{isUpdateAvailable &&
|
||||
(updateLoading ? (
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
css={`
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
path {
|
||||
cursor: pointer;
|
||||
transition: all 0.1s ease-in-out;
|
||||
color: ${props => props.theme.palette.colors.green};
|
||||
}
|
||||
}
|
||||
`}
|
||||
icon={faDownload}
|
||||
onClick={async () => {
|
||||
setUpdateLoading(true);
|
||||
await makeModRestorePoint(
|
||||
newModPath,
|
||||
modsPath,
|
||||
item.fileName
|
||||
);
|
||||
|
||||
await dispatch(
|
||||
updateMod(
|
||||
instanceName,
|
||||
item,
|
||||
latestMods[item.projectID].id,
|
||||
gameVersions
|
||||
)
|
||||
);
|
||||
await fse.remove(newModPath);
|
||||
setUpdateLoading(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<Switch
|
||||
size="small"
|
||||
checked={path.extname(item.fileName) !== '.disabled'}
|
||||
disabled={loading || updateLoading}
|
||||
onChange={async c => {
|
||||
setLoading(true);
|
||||
const destFileName = c
|
||||
? item.fileName.replace('.disabled', '')
|
||||
: `${item.fileName}.disabled`;
|
||||
const isCurrentlySelected = selectedMods.find(
|
||||
v => v === item.fileName
|
||||
);
|
||||
|
||||
if (isCurrentlySelected) {
|
||||
setSelectedMods(prev => [...prev, destFileName]);
|
||||
}
|
||||
|
||||
await toggleModDisabled(
|
||||
c,
|
||||
instanceName,
|
||||
instancePath,
|
||||
item,
|
||||
dispatch
|
||||
);
|
||||
if (isCurrentlySelected) {
|
||||
setSelectedMods(prev =>
|
||||
prev.filter(v => v !== item.fileName)
|
||||
);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 500);
|
||||
}}
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
css={`
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
path {
|
||||
cursor: pointer;
|
||||
transition: all 0.1s ease-in-out;
|
||||
color: ${props => props.theme.palette.error.main};
|
||||
}
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
if (!loading && !updateLoading) {
|
||||
dispatch(deleteMod(instanceName, item));
|
||||
}
|
||||
}}
|
||||
icon={faTrash}
|
||||
/>
|
||||
</div>
|
||||
<RowContainerBackground
|
||||
selected={selectedMods.includes(item.fileName)}
|
||||
disabled={path.extname(item.fileName) === '.disabled'}
|
||||
/>
|
||||
</RowContainer>
|
||||
</ContextMenuTrigger>
|
||||
<Portal>
|
||||
<ContextMenu
|
||||
id={item.displayName}
|
||||
onShow={() => {
|
||||
setSelectedMods([item.fileName]);
|
||||
setIsHovered(true);
|
||||
}}
|
||||
onHide={() => setIsHovered(false)}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
clipboard.writeText(item.displayName);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faCopy}
|
||||
css={`
|
||||
margin-right: 10px;
|
||||
`}
|
||||
/>
|
||||
Copy Name
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
}, areEqual);
|
||||
|
||||
const createItemData = memoize(
|
||||
(
|
||||
items,
|
||||
instanceName,
|
||||
instancePath,
|
||||
gameVersions,
|
||||
selectedMods,
|
||||
setSelectedMods,
|
||||
latestMods
|
||||
) => ({
|
||||
items,
|
||||
instanceName,
|
||||
instancePath,
|
||||
gameVersions,
|
||||
selectedMods,
|
||||
setSelectedMods,
|
||||
latestMods
|
||||
})
|
||||
);
|
||||
|
||||
const sort = arr =>
|
||||
arr.slice().sort((a, b) => a.fileName?.localeCompare(b.fileName));
|
||||
|
||||
const filter = (arr, search) =>
|
||||
arr.filter(
|
||||
mod =>
|
||||
mod.fileName?.toLowerCase()?.includes(search?.toLowerCase()) ||
|
||||
mod.displayName?.toLowerCase()?.includes(search?.toLowerCase())
|
||||
);
|
||||
|
||||
const getFileType = file => {
|
||||
const fileName = file.name;
|
||||
let fileType = '';
|
||||
|
||||
const splitFileName = fileName?.split('.');
|
||||
if (splitFileName.length) {
|
||||
fileType = splitFileName[splitFileName.length - 1];
|
||||
}
|
||||
|
||||
return fileType;
|
||||
};
|
||||
|
||||
const Mods = ({ instanceName }) => {
|
||||
const instance = useSelector(state => _getInstance(state)(instanceName));
|
||||
const instancesPath = useSelector(_getInstancesPath);
|
||||
const curseReleaseChannel = useSelector(
|
||||
state => state.settings.curseReleaseChannel
|
||||
);
|
||||
const latestMods = useSelector(state => state.latestModManifests);
|
||||
const [mods, setMods] = useState(sort(instance.mods));
|
||||
const [selectedMods, setSelectedMods] = useState([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [fileDrag, setFileDrag] = useState(false);
|
||||
const [fileDrop, setFileDrop] = useState(false);
|
||||
const [loadingModUpdates, setLoadingModUpdates] = useState(false);
|
||||
const [numOfDraggedFiles, setNumOfDraggedFiles] = useState(0);
|
||||
const [dragCompleted, setDragCompleted] = useState({});
|
||||
const [dragCompletedPopulated, setDragCompletedPopulated] = useState(false);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const openFolder = async p => {
|
||||
await makeDir(p);
|
||||
ipcRenderer.invoke('openFolder', p);
|
||||
};
|
||||
|
||||
const antIcon = (
|
||||
<LoadingOutlined
|
||||
css={`
|
||||
font-size: 24px;
|
||||
`}
|
||||
spin
|
||||
/>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const modList = instance.mods;
|
||||
|
||||
if (dragCompletedPopulated) {
|
||||
const AllFilesAreCompleted = Object.keys(dragCompleted).every(x =>
|
||||
modList.find(y => y.fileName === x)
|
||||
);
|
||||
setNumOfDraggedFiles(numOfDraggedFiles - 1);
|
||||
|
||||
if (AllFilesAreCompleted) {
|
||||
setFileDrop(false);
|
||||
setFileDrag(false);
|
||||
}
|
||||
}
|
||||
}, [dragCompleted, instance.mods]);
|
||||
|
||||
useEffect(() => {
|
||||
setMods(filter(sort(instance.mods), search));
|
||||
setSelectedMods(prev => {
|
||||
return prev.filter(v => instance.mods.find(m => m.fileName === v));
|
||||
});
|
||||
}, [search, instance.mods]);
|
||||
|
||||
const hasModUpdates = useMemo(() => {
|
||||
return instance?.mods?.find(v => {
|
||||
const isUpdateAvailable =
|
||||
latestMods[v.projectID] &&
|
||||
latestMods[v.projectID].id !== v.fileID &&
|
||||
latestMods[v.projectID].releaseType <= curseReleaseChannel;
|
||||
return isUpdateAvailable;
|
||||
});
|
||||
}, [instance.mods, latestMods]);
|
||||
|
||||
const itemData = createItemData(
|
||||
mods,
|
||||
instanceName,
|
||||
path.join(instancesPath, instanceName),
|
||||
instance.loader?.mcVersion,
|
||||
selectedMods,
|
||||
setSelectedMods,
|
||||
latestMods
|
||||
);
|
||||
|
||||
const onDragOver = e => {
|
||||
setFileDrag(true);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const onDrop = async e => {
|
||||
setFileDrop(true);
|
||||
const dragComp = {};
|
||||
const { files } = e.dataTransfer;
|
||||
|
||||
await pMap(
|
||||
Object.values(files),
|
||||
async file => {
|
||||
const fileName = file.name;
|
||||
const fileType = getFileType(file);
|
||||
const existingMods = itemData.items.map(item => item.fileName);
|
||||
|
||||
dragComp[fileName] = false;
|
||||
|
||||
setNumOfDraggedFiles(files.length);
|
||||
|
||||
const { path: filePath } = file;
|
||||
|
||||
if (existingMods.includes(fileName)) {
|
||||
console.error(
|
||||
'A mod with this name already exists in the instance.',
|
||||
file.name
|
||||
);
|
||||
setFileDrop(false);
|
||||
setFileDrag(false);
|
||||
} else if (fileType === 'jar' || fileType === 'disabled') {
|
||||
await fse.copy(
|
||||
filePath,
|
||||
path.join(instancesPath, instanceName, 'mods', fileName)
|
||||
);
|
||||
dragComp[fileName] = true;
|
||||
} else {
|
||||
console.error('This file is not a mod!', file);
|
||||
setFileDrop(false);
|
||||
setFileDrag(false);
|
||||
}
|
||||
},
|
||||
{ concurrency: 10 }
|
||||
);
|
||||
setDragCompletedPopulated(files.length === Object.values(dragComp).length);
|
||||
setDragCompleted(dragComp);
|
||||
};
|
||||
|
||||
const onDragEnter = e => {
|
||||
setFileDrag(true);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const onDragLeave = () => {
|
||||
setFileDrag(false);
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: '0',
|
||||
disabled: !hasModUpdates,
|
||||
label: (
|
||||
<div
|
||||
onClick={() => {
|
||||
dispatch(openModal('ModsUpdater', { instanceName }));
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
Update All Mods
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
css={`
|
||||
flex: 1;
|
||||
`}
|
||||
onClick={() => {
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<Header>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<Checkbox
|
||||
checked={
|
||||
selectedMods.length === mods.length && selectedMods.length !== 0
|
||||
}
|
||||
indeterminate={
|
||||
selectedMods.length !== 0 && selectedMods.length !== mods.length
|
||||
}
|
||||
onChange={() =>
|
||||
selectedMods.length !== mods.length
|
||||
? setSelectedMods(mods.map(v => v.fileName))
|
||||
: setSelectedMods([])
|
||||
}
|
||||
>
|
||||
Select All
|
||||
</Checkbox>
|
||||
<DeleteSelectedMods
|
||||
onClick={async () => {
|
||||
if (selectedMods.length === 0) return;
|
||||
await deleteMods(
|
||||
instanceName,
|
||||
path.join(instancesPath, instanceName),
|
||||
selectedMods,
|
||||
dispatch
|
||||
);
|
||||
setSelectedMods([]);
|
||||
}}
|
||||
selectedMods={selectedMods.length}
|
||||
icon={faTrash}
|
||||
/>
|
||||
<OpenFolderButton
|
||||
onClick={() =>
|
||||
openFolder(path.join(instancesPath, instanceName, 'mods'))
|
||||
}
|
||||
icon={faFolder}
|
||||
/>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (instance.name && instance?.mods?.length) {
|
||||
try {
|
||||
setLoadingModUpdates(true);
|
||||
await dispatch(initLatestMods(instance.name));
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
} finally {
|
||||
setLoadingModUpdates(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
loading={loadingModUpdates}
|
||||
>
|
||||
Check for Updates
|
||||
</Button>
|
||||
<span
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setIsMenuOpen(prev => !prev);
|
||||
}}
|
||||
>
|
||||
<StyledDropdown>
|
||||
<span>
|
||||
<Dropdown
|
||||
overlay={menu}
|
||||
visible={isMenuOpen}
|
||||
trigger={['click']}
|
||||
>
|
||||
<span
|
||||
css={`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</span>
|
||||
</Dropdown>
|
||||
</span>
|
||||
</StyledDropdown>
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
openModal('ModsBrowser', {
|
||||
gameVersions: instance.loader?.mcVersion,
|
||||
instanceName
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
Add Mod
|
||||
</Button>
|
||||
<Input
|
||||
allowClear
|
||||
value={search}
|
||||
defaultValue={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
css={`
|
||||
width: 200px !important;
|
||||
`}
|
||||
placeholder={`Search ${mods.length} mods`}
|
||||
/>
|
||||
</Header>
|
||||
<div
|
||||
onDragEnter={onDragEnter}
|
||||
css={`
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
`}
|
||||
>
|
||||
<Transition timeout={300} in={fileDrag}>
|
||||
{transitionState => (
|
||||
<DragEnterEffect
|
||||
onDrop={onDrop}
|
||||
transitionState={transitionState}
|
||||
onDragLeave={onDragLeave}
|
||||
fileDrag={fileDrag}
|
||||
onDragOver={onDragOver}
|
||||
>
|
||||
{fileDrop ? (
|
||||
<Spin
|
||||
indicator={antIcon}
|
||||
css={`
|
||||
width: 30px;
|
||||
`}
|
||||
>
|
||||
{numOfDraggedFiles > 0 ? numOfDraggedFiles : 1}
|
||||
</Spin>
|
||||
) : (
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`}
|
||||
onDragLeave={e => e.stopPropagation()}
|
||||
>
|
||||
<CopyTitle>Copy</CopyTitle>
|
||||
<DragArrow icon={faArrowDown} size="3x" />
|
||||
</div>
|
||||
)}
|
||||
</DragEnterEffect>
|
||||
)}
|
||||
</Transition>
|
||||
{mods.length === 0 && (
|
||||
<NotItemsAvailable>No Mods Available</NotItemsAvailable>
|
||||
)}
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
height={height}
|
||||
itemData={itemData}
|
||||
itemCount={mods.length}
|
||||
itemSize={60}
|
||||
width={width}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Mods);
|
||||
@@ -0,0 +1,266 @@
|
||||
/* eslint-disable */
|
||||
import React, { useCallback, useMemo, useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Editable, withReact, useSlate, Slate } from 'slate-react';
|
||||
import { Editor, Transforms, createEditor } from 'slate';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { withHistory } from 'slate-history';
|
||||
import { Button } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faBold,
|
||||
faItalic,
|
||||
faUnderline,
|
||||
faCode,
|
||||
faQuoteRight,
|
||||
faListOl,
|
||||
faList
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { updateInstanceConfig } from '../../reducers/actions';
|
||||
import { _getInstancesPath, _getInstance } from '../../utils/selectors';
|
||||
|
||||
const LIST_TYPES = ['numbered-list', 'bulleted-list'];
|
||||
|
||||
const Notes = ({ instanceName }) => {
|
||||
const renderElement = useCallback(props => <Element {...props} />, []);
|
||||
const renderLeaf = useCallback(props => <Leaf {...props} />, []);
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), []);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const instance = useSelector(state => _getInstance(state)(instanceName));
|
||||
const [value, setValue] = useState(instance.notes || initialValue);
|
||||
|
||||
const updateNotes = useDebouncedCallback(
|
||||
v => {
|
||||
dispatch(
|
||||
updateInstanceConfig(instanceName, config => ({ ...config, notes: v }))
|
||||
);
|
||||
},
|
||||
1500,
|
||||
{ maxWait: 4000 }
|
||||
);
|
||||
|
||||
return (
|
||||
<MainContainer>
|
||||
<Container>
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
onChange={notes => {
|
||||
setValue(notes);
|
||||
updateNotes(notes);
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<MarkButton format="bold" icon={faBold} />
|
||||
<MarkButton format="italic" icon={faItalic} />
|
||||
<MarkButton format="underline" icon={faUnderline} />
|
||||
<MarkButton format="code" icon={faCode} />
|
||||
<BlockButton format="heading-one" icon="h1" />
|
||||
<BlockButton format="heading-two" icon="h2" />
|
||||
<BlockButton format="block-quote" icon={faQuoteRight} />
|
||||
<BlockButton format="numbered-list" icon={faListOl} />
|
||||
<BlockButton format="bulleted-list" icon={faList} />
|
||||
</Toolbar>
|
||||
<TextEditorContainer>
|
||||
<TextEditor
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
placeholder="Enter some notes..."
|
||||
spellCheck
|
||||
autoFocus
|
||||
/>
|
||||
</TextEditorContainer>
|
||||
</Slate>
|
||||
</Container>
|
||||
</MainContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const toggleBlock = (editor, format) => {
|
||||
const isActive = isBlockActive(editor, format);
|
||||
const isList = LIST_TYPES.includes(format);
|
||||
|
||||
Transforms.unwrapNodes(editor, {
|
||||
match: n => LIST_TYPES.includes(n.type),
|
||||
split: true
|
||||
});
|
||||
|
||||
Transforms.setNodes(editor, {
|
||||
type: isActive ? 'paragraph' : isList ? 'list-item' : format
|
||||
});
|
||||
|
||||
if (!isActive && isList) {
|
||||
const block = { type: format, children: [] };
|
||||
Transforms.wrapNodes(editor, block);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMark = (editor, format) => {
|
||||
const isActive = isMarkActive(editor, format);
|
||||
|
||||
if (isActive) {
|
||||
Editor.removeMark(editor, format);
|
||||
} else {
|
||||
Editor.addMark(editor, format, true);
|
||||
}
|
||||
};
|
||||
|
||||
const isBlockActive = (editor, format) => {
|
||||
const [match] = Editor.nodes(editor, {
|
||||
match: n => n.type === format
|
||||
});
|
||||
|
||||
return !!match;
|
||||
};
|
||||
|
||||
const isMarkActive = (editor, format) => {
|
||||
const marks = Editor.marks(editor);
|
||||
return marks ? marks[format] === true : false;
|
||||
};
|
||||
|
||||
const Element = ({ attributes, children, element }) => {
|
||||
switch (element.type) {
|
||||
case 'block-quote':
|
||||
return <blockquote {...attributes}>{children}</blockquote>;
|
||||
case 'bulleted-list':
|
||||
return <ul {...attributes}>{children}</ul>;
|
||||
case 'heading-one':
|
||||
return <h1 {...attributes}>{children}</h1>;
|
||||
case 'heading-two':
|
||||
return <h2 {...attributes}>{children}</h2>;
|
||||
case 'list-item':
|
||||
return <li {...attributes}>{children}</li>;
|
||||
case 'numbered-list':
|
||||
return <ol {...attributes}>{children}</ol>;
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>;
|
||||
}
|
||||
};
|
||||
|
||||
const Leaf = ({ attributes, children, leaf }) => {
|
||||
if (leaf.bold) {
|
||||
children = <strong>{children}</strong>;
|
||||
}
|
||||
|
||||
if (leaf.code) {
|
||||
children = <code>{children}</code>;
|
||||
}
|
||||
|
||||
if (leaf.italic) {
|
||||
children = <em>{children}</em>;
|
||||
}
|
||||
|
||||
if (leaf.underline) {
|
||||
children = <u>{children}</u>;
|
||||
}
|
||||
|
||||
return <span {...attributes}>{children}</span>;
|
||||
};
|
||||
|
||||
const BlockButton = ({ format, icon }) => {
|
||||
const editor = useSlate();
|
||||
return (
|
||||
<BlockInnerButton
|
||||
css={`
|
||||
margin: 0 2px;
|
||||
border: ${props => `solid 2px ${props.theme.palette.primary.main}`};
|
||||
`}
|
||||
active={isBlockActive(editor, format)}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault();
|
||||
toggleBlock(editor, format);
|
||||
}}
|
||||
>
|
||||
{typeof icon === 'string' ? (
|
||||
<div>{icon}</div>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
)}
|
||||
</BlockInnerButton>
|
||||
);
|
||||
};
|
||||
|
||||
const MarkButton = ({ format, icon }) => {
|
||||
const editor = useSlate();
|
||||
return (
|
||||
<MarkInnerButton
|
||||
css={`
|
||||
margin: 0 2px;
|
||||
border: ${props => `solid 2px ${props.theme.palette.primary.main}`};
|
||||
`}
|
||||
active={isMarkActive(editor, format)}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault();
|
||||
toggleMark(editor, format);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
</MarkInnerButton>
|
||||
);
|
||||
};
|
||||
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: '' }]
|
||||
}
|
||||
];
|
||||
|
||||
export default Notes;
|
||||
|
||||
const MainContainer = styled.div`
|
||||
height: 100%;
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const Toolbar = styled.div`
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const TextEditorContainer = styled.div`
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
overflow-x: hidden;
|
||||
`;
|
||||
|
||||
const TextEditor = styled(Editable)`
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
overflow-x: auto;
|
||||
word-break: break-word;
|
||||
border: ${props => `solid 2px ${props.theme.palette.primary.main}`};
|
||||
`;
|
||||
|
||||
const MarkInnerButton = styled(({ active, ...props }) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<Button {...props} />
|
||||
))``;
|
||||
|
||||
const BlockInnerButton = styled(({ active, ...props }) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<Button {...props} />
|
||||
))``;
|
||||
@@ -0,0 +1,609 @@
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import fss from 'fs-extra';
|
||||
import path from 'path';
|
||||
import omit from 'lodash/omit';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import styled from 'styled-components';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faSave,
|
||||
faUndo,
|
||||
faCog,
|
||||
faFolder
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { Input, Button, Switch, Slider, Select } from 'antd';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import {
|
||||
_getInstancesPath,
|
||||
_getInstance,
|
||||
_getJavaPath
|
||||
} from '../../utils/selectors';
|
||||
import instanceDefaultBackground from '../../assets/instance_default.png';
|
||||
import {
|
||||
DEFAULT_JAVA_ARGS,
|
||||
resolutionPresets
|
||||
} from '../../../app/desktop/utils/constants';
|
||||
import {
|
||||
getJavaVersionForMCVersion,
|
||||
updateInstanceConfig
|
||||
} from '../../reducers/actions';
|
||||
import { openModal } from '../../reducers/modals/actions';
|
||||
import {
|
||||
convertMinutesToHumanTime,
|
||||
marks,
|
||||
scaleMem,
|
||||
scaleMemInv,
|
||||
sysMemScaled
|
||||
} from '../../utils';
|
||||
import { CURSEFORGE } from '../../utils/constants';
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 0 50px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
`;
|
||||
|
||||
const Column = styled.div``;
|
||||
|
||||
const RenameRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
margin: 60px 0 30px 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const RenameButton = styled(Button)`
|
||||
margin-left: 20px;
|
||||
`;
|
||||
|
||||
const CardBox = styled.div`
|
||||
flex: 1;
|
||||
height: 60px;
|
||||
font-weight: 500;
|
||||
border-radius: ${props => props.theme.shape.borderRadius};
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
position: relative;
|
||||
padding: 0 10px;
|
||||
`;
|
||||
|
||||
const OverviewCard = styled.div`
|
||||
margin-bottom: 30px;
|
||||
padding: 0;
|
||||
${CardBox} {
|
||||
margin: 0 20px;
|
||||
}
|
||||
${CardBox}:first-child {
|
||||
margin-right: 20px;
|
||||
margin-left: 0;
|
||||
}
|
||||
${CardBox}:last-child {
|
||||
margin-left: 20px;
|
||||
margin-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const JavaManagerRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
margin: 0 500px 20px 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const JavaMemorySlider = styled(Slider)`
|
||||
margin: 10px 40px 55px 40px !important;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const JavaResetButton = styled(Button)`
|
||||
margin-left: 20px;
|
||||
`;
|
||||
|
||||
const ResolutionInputContainer = styled.div`
|
||||
margin: 10px 0 30px 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
div {
|
||||
width: 200px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const Card = memo(
|
||||
({ title, children, color, icon, instanceName, defaultValue }) => {
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<CardBox
|
||||
css={`
|
||||
background: ${color};
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 10px;
|
||||
font-size: 10px;
|
||||
color: ${props => props.theme.palette.text.secondary};
|
||||
`}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
{icon && (
|
||||
<div
|
||||
css={`
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 10px;
|
||||
font-size: 10px;
|
||||
color: ${props => props.theme.palette.text.secondary};
|
||||
cursor: pointer;
|
||||
`}
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
openModal('McVersionChanger', { instanceName, defaultValue })
|
||||
);
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>{children}</div>
|
||||
</CardBox>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const Overview = ({ instanceName, background, manifest }) => {
|
||||
const dispatch = useDispatch();
|
||||
const instancesPath = useSelector(_getInstancesPath);
|
||||
const config = useSelector(state => _getInstance(state)(instanceName));
|
||||
const javaVersion = dispatch(
|
||||
getJavaVersionForMCVersion(config?.loader?.mcVersion)
|
||||
);
|
||||
const defaultJavaPath = useSelector(state =>
|
||||
_getJavaPath(state)(javaVersion)
|
||||
);
|
||||
const [javaLocalMemory, setJavaLocalMemory] = useState(config?.javaMemory);
|
||||
const [javaLocalArguments, setJavaLocalArguments] = useState(
|
||||
config?.javaArgs
|
||||
);
|
||||
const [customJavaPath, setCustomJavaPath] = useState(config?.customJavaPath);
|
||||
const [newName, setNewName] = useState(instanceName);
|
||||
const [screenResolution, setScreenResolution] = useState(null);
|
||||
const [height, setHeight] = useState(config?.resolution?.height);
|
||||
const [width, setWidth] = useState(config?.resolution?.width);
|
||||
|
||||
useEffect(() => {
|
||||
ipcRenderer
|
||||
.invoke('getAllDisplaysBounds')
|
||||
.then(setScreenResolution)
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const updateJavaMemory = v => {
|
||||
dispatch(
|
||||
updateInstanceConfig(instanceName, prev => ({
|
||||
...prev,
|
||||
javaMemory: Math.round(scaleMemInv(v))
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const updateJavaArguments = v => {
|
||||
dispatch(
|
||||
updateInstanceConfig(instanceName, prev => ({
|
||||
...prev,
|
||||
javaArgs: v
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const updateCustomJavaPath = v => {
|
||||
dispatch(
|
||||
updateInstanceConfig(instanceName, prev => ({
|
||||
...prev,
|
||||
customJavaPath: v
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const updateGameResolution = (w, h) => {
|
||||
dispatch(
|
||||
updateInstanceConfig(instanceName, prev => ({
|
||||
...prev,
|
||||
resolution: { height: h, width: w }
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const debouncedArgumentsUpdate = useDebouncedCallback(
|
||||
v => {
|
||||
updateJavaArguments(v);
|
||||
},
|
||||
400,
|
||||
{ maxWait: 700, leading: false }
|
||||
);
|
||||
|
||||
const debouncedJavaPathUpdate = useDebouncedCallback(
|
||||
v => {
|
||||
updateCustomJavaPath(v);
|
||||
},
|
||||
400,
|
||||
{ maxWait: 700, leading: false }
|
||||
);
|
||||
|
||||
const resetJavaArguments = () => {
|
||||
setJavaLocalArguments(DEFAULT_JAVA_ARGS);
|
||||
updateJavaArguments(DEFAULT_JAVA_ARGS);
|
||||
};
|
||||
|
||||
const resetCustomJavaPath = () => {
|
||||
setCustomJavaPath(defaultJavaPath);
|
||||
updateCustomJavaPath(defaultJavaPath);
|
||||
};
|
||||
|
||||
const renameInstance = () => {
|
||||
fss.rename(
|
||||
path.join(instancesPath, instanceName),
|
||||
path.join(instancesPath, newName)
|
||||
);
|
||||
};
|
||||
|
||||
const computeLastPlayed = timestamp => {
|
||||
const lastPlayed = new Date(timestamp);
|
||||
const timeDiff = lastPlayed.getTime() - new Date(Date.now()).getTime();
|
||||
const diffDays = Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||
switch (diffDays) {
|
||||
case 0:
|
||||
return 'Today';
|
||||
case -1:
|
||||
return 'Yesterday';
|
||||
default:
|
||||
return lastPlayed.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Column>
|
||||
<OverviewCard
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
`}
|
||||
>
|
||||
<Card
|
||||
title="Minecraft Version"
|
||||
color={props => props.theme.palette.colors.jungleGreen}
|
||||
instanceName={instanceName}
|
||||
defaultValue={config?.loader}
|
||||
icon={<FontAwesomeIcon icon={faCog} />}
|
||||
>
|
||||
{config?.loader?.mcVersion}
|
||||
</Card>
|
||||
<Card
|
||||
title="Modloader"
|
||||
color={props => props.theme.palette.colors.darkYellow}
|
||||
instanceName={instanceName}
|
||||
defaultValue={config?.loader}
|
||||
icon={<FontAwesomeIcon icon={faCog} />}
|
||||
>
|
||||
{config?.loader?.loaderType}
|
||||
</Card>
|
||||
<Card
|
||||
title="Modloader Version"
|
||||
color={props => props.theme.palette.colors.lightBlue}
|
||||
instanceName={instanceName}
|
||||
defaultValue={config?.loader}
|
||||
icon={
|
||||
(config?.loader?.loaderVersion || '-') !== '-' ? (
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{config?.loader?.loaderType === 'forge'
|
||||
? config?.loader?.loaderVersion?.split('-')[1]
|
||||
: config?.loader?.loaderVersion || '-'}
|
||||
</Card>
|
||||
</OverviewCard>
|
||||
<OverviewCard
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
`}
|
||||
>
|
||||
<Card
|
||||
title="Mods"
|
||||
color={props => props.theme.palette.colors.maximumRed}
|
||||
>
|
||||
{config?.mods?.length || '-'}
|
||||
</Card>
|
||||
<Card
|
||||
title="Played Time"
|
||||
color={props => props.theme.palette.colors.liberty}
|
||||
>
|
||||
{convertMinutesToHumanTime(config?.timePlayed)}
|
||||
</Card>
|
||||
<Card
|
||||
title="Last Played"
|
||||
color={props => props.theme.palette.colors.orange}
|
||||
>
|
||||
{config?.lastPlayed ? computeLastPlayed(config?.lastPlayed) : '-'}
|
||||
</Card>
|
||||
</OverviewCard>
|
||||
{config?.loader.source === CURSEFORGE && manifest && (
|
||||
<Card
|
||||
title="Curse Modpack"
|
||||
color={`linear-gradient(to bottom, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), ${
|
||||
background
|
||||
? `url(${background})`
|
||||
: `url(${instanceDefaultBackground})`
|
||||
}`}
|
||||
>
|
||||
{manifest?.name} - {manifest?.version}
|
||||
</Card>
|
||||
)}
|
||||
<RenameRow>
|
||||
<Input value={newName} onChange={e => setNewName(e.target.value)} />
|
||||
<RenameButton onClick={() => renameInstance()} type="primary">
|
||||
Rename
|
||||
<FontAwesomeIcon icon={faSave} />
|
||||
</RenameButton>
|
||||
</RenameRow>
|
||||
<OverviewCard>
|
||||
<JavaManagerRow>
|
||||
<div>Override Game Resolution</div>
|
||||
<Switch
|
||||
checked={height && width}
|
||||
onChange={v => {
|
||||
if (!v) {
|
||||
setHeight(null);
|
||||
setWidth(null);
|
||||
dispatch(
|
||||
updateInstanceConfig(instanceName, prev =>
|
||||
omit(prev, ['resolution'])
|
||||
)
|
||||
);
|
||||
} else {
|
||||
updateGameResolution(854, 480);
|
||||
setHeight(480);
|
||||
setWidth(854);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</JavaManagerRow>
|
||||
{height && width && (
|
||||
<ResolutionInputContainer>
|
||||
<div>
|
||||
<Input
|
||||
placeholder="Width"
|
||||
value={width}
|
||||
onChange={e => {
|
||||
const w = parseInt(e.target.value, 10) || 854;
|
||||
setWidth(w);
|
||||
dispatch(
|
||||
updateInstanceConfig(instanceName, prev => ({
|
||||
...prev,
|
||||
resolution: {
|
||||
height,
|
||||
width: w
|
||||
}
|
||||
}))
|
||||
);
|
||||
}}
|
||||
/>
|
||||
X
|
||||
<Input
|
||||
placeholder="Height"
|
||||
value={height}
|
||||
onChange={e => {
|
||||
const h = parseInt(e.target.value, 10) || 480;
|
||||
setHeight(h);
|
||||
dispatch(
|
||||
updateInstanceConfig(instanceName, prev => ({
|
||||
...prev,
|
||||
resolution: {
|
||||
height: h,
|
||||
width
|
||||
}
|
||||
}))
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
placeholder="Presets"
|
||||
onChange={v => {
|
||||
const w = parseInt(v.split('x')[0], 10);
|
||||
const h = parseInt(v.split('x')[1], 10);
|
||||
setHeight(h);
|
||||
setWidth(w);
|
||||
dispatch(
|
||||
updateInstanceConfig(instanceName, prev => ({
|
||||
...prev,
|
||||
resolution: {
|
||||
height: h,
|
||||
width: w
|
||||
}
|
||||
}))
|
||||
);
|
||||
}}
|
||||
virtual={false}
|
||||
>
|
||||
{resolutionPresets.map(v => {
|
||||
const w = parseInt(v.split('x')[0], 10);
|
||||
const h = parseInt(v.split('x')[1], 10);
|
||||
|
||||
const isBiggerThanScreen = (screenResolution || []).every(
|
||||
bounds => {
|
||||
return bounds.width < w || bounds.height < h;
|
||||
}
|
||||
);
|
||||
if (isBiggerThanScreen) return null;
|
||||
return <Select.Option value={v}>{v}</Select.Option>;
|
||||
})}
|
||||
</Select>
|
||||
</ResolutionInputContainer>
|
||||
)}
|
||||
<JavaManagerRow>
|
||||
<div>Override Java Memory</div>
|
||||
<Switch
|
||||
checked={javaLocalMemory}
|
||||
onChange={v => {
|
||||
if (!v) {
|
||||
setJavaLocalMemory(null);
|
||||
dispatch(
|
||||
updateInstanceConfig(instanceName, prev =>
|
||||
omit(prev, ['javaMemory'])
|
||||
)
|
||||
);
|
||||
} else if (v) {
|
||||
setJavaLocalMemory(4096);
|
||||
updateJavaMemory(4096);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</JavaManagerRow>
|
||||
{(javaLocalMemory || null) && (
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
<JavaMemorySlider
|
||||
onAfterChange={updateJavaMemory}
|
||||
onChange={v => setJavaLocalMemory(Math.round(scaleMemInv(v)))}
|
||||
value={scaleMem(javaLocalMemory)}
|
||||
min={0}
|
||||
max={sysMemScaled}
|
||||
step={0.1}
|
||||
marks={marks}
|
||||
valueLabelDisplay="auto"
|
||||
/>
|
||||
<div
|
||||
css={`
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 100px;
|
||||
`}
|
||||
>
|
||||
{javaLocalMemory} MB
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<JavaManagerRow>
|
||||
<div>Override Java Arguments</div>
|
||||
<Switch
|
||||
checked={javaLocalArguments}
|
||||
onChange={v => {
|
||||
if (!v) {
|
||||
setJavaLocalArguments(null);
|
||||
dispatch(
|
||||
updateInstanceConfig(instanceName, prev =>
|
||||
omit(prev, ['javaArgs'])
|
||||
)
|
||||
);
|
||||
} else if (v) {
|
||||
resetJavaArguments();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</JavaManagerRow>
|
||||
{javaLocalArguments && (
|
||||
<JavaManagerRow>
|
||||
<Input
|
||||
value={javaLocalArguments}
|
||||
onChange={e => {
|
||||
setJavaLocalArguments(e.target.value);
|
||||
debouncedArgumentsUpdate(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<JavaResetButton onClick={resetJavaArguments}>
|
||||
<FontAwesomeIcon icon={faUndo} />
|
||||
</JavaResetButton>
|
||||
</JavaManagerRow>
|
||||
)}
|
||||
<JavaManagerRow>
|
||||
<div>Custom Java Path {`<Java ${javaVersion}>`} </div>
|
||||
<Switch
|
||||
checked={customJavaPath}
|
||||
onChange={v => {
|
||||
if (!v) {
|
||||
setCustomJavaPath(null);
|
||||
dispatch(
|
||||
updateInstanceConfig(instanceName, prev =>
|
||||
omit(prev, ['customJavaPath'])
|
||||
)
|
||||
);
|
||||
} else if (v) {
|
||||
resetCustomJavaPath();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</JavaManagerRow>
|
||||
{customJavaPath && (
|
||||
<JavaManagerRow>
|
||||
<Input
|
||||
value={customJavaPath}
|
||||
onChange={e => {
|
||||
setCustomJavaPath(e.target.value);
|
||||
debouncedJavaPathUpdate(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
const { filePaths, canceled } = await ipcRenderer.invoke(
|
||||
'openFileDialog',
|
||||
defaultJavaPath
|
||||
);
|
||||
if (!filePaths[0] || canceled) return;
|
||||
setCustomJavaPath(filePaths[0]);
|
||||
updateCustomJavaPath(filePaths[0]);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFolder} />
|
||||
</Button>
|
||||
<JavaResetButton onClick={resetCustomJavaPath}>
|
||||
<FontAwesomeIcon icon={faUndo} />
|
||||
</JavaResetButton>
|
||||
</JavaManagerRow>
|
||||
)}
|
||||
</OverviewCard>
|
||||
</Column>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Overview;
|
||||
@@ -0,0 +1,429 @@
|
||||
import React, { memo, useState, useEffect, useCallback } from 'react';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import memoize from 'memoize-one';
|
||||
import path from 'path';
|
||||
import { promises as fs, watch } from 'fs';
|
||||
import makeDir from 'make-dir';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { FixedSizeList as List, areEqual } from 'react-window';
|
||||
import { Checkbox, Button, Switch } from 'antd';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faFolder, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import fse from 'fs-extra';
|
||||
import curseForgeIcon from '../../assets/curseforgeIcon.webp';
|
||||
import { _getInstancesPath } from '../../utils/selectors';
|
||||
import DragnDropEffect from '../../../ui/DragnDropEffect';
|
||||
|
||||
const Header = styled.div`
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
background: ${props => props.theme.palette.grey[700]};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const TrashIcon = styled(({ selectedMods, ...props }) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<FontAwesomeIcon {...props} />
|
||||
))`
|
||||
margin: 0 10px;
|
||||
${props =>
|
||||
props.selectedMods > 0 &&
|
||||
`&:hover {
|
||||
cursor: pointer;
|
||||
path {
|
||||
cursor: pointer;
|
||||
transition: color 0.1s ease-in-out;
|
||||
color: ${props.theme.palette.error.main};
|
||||
}
|
||||
}`}
|
||||
`;
|
||||
|
||||
const RowContainer = styled.div.attrs(props => ({
|
||||
style: props.override
|
||||
}))`
|
||||
width: 100%;
|
||||
background: ${props =>
|
||||
props.disabled || props.selected
|
||||
? 'transparent'
|
||||
: props.theme.palette.grey[800]};
|
||||
|
||||
${props =>
|
||||
props.disabled &&
|
||||
!props.selected &&
|
||||
`box-shadow: inset 0 0 0 3px ${props.theme.palette.colors.red};`}
|
||||
${props =>
|
||||
props.selected &&
|
||||
`box-shadow: inset 0 0 0 3px ${props.theme.palette.primary.main};`}
|
||||
|
||||
transition: border 0.1s ease-in-out;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
padding: 0 10px;
|
||||
.leftPartContent {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
& > * {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
.rowCenterContent {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: color 0.1s ease-in-out;
|
||||
cursor: pointer;
|
||||
svg {
|
||||
margin-right: 10px;
|
||||
}
|
||||
&:hover {
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
}
|
||||
}
|
||||
.rightPartContent {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
& > * {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const RowContainerBackground = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
|
||||
${props =>
|
||||
props.selected &&
|
||||
` background: repeating-linear-gradient(
|
||||
45deg,
|
||||
${props.theme.palette.primary.main},
|
||||
${props.theme.palette.primary.main} 10px,
|
||||
${props.theme.palette.primary.dark} 10px,
|
||||
${props.theme.palette.primary.dark} 20px
|
||||
);`};
|
||||
|
||||
${props =>
|
||||
props.disabled &&
|
||||
!props.selected &&
|
||||
`background: repeating-linear-gradient(
|
||||
45deg,
|
||||
${props.theme.palette.colors.red},
|
||||
${props.theme.palette.colors.red} 10px,
|
||||
${props.theme.palette.colors.maximumRed} 10px,
|
||||
${props.theme.palette.colors.maximumRed} 20px
|
||||
);`};
|
||||
filter: brightness(60%);
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
opacity: ${props => (props.disabled || props.selected ? 1 : 0)};
|
||||
`;
|
||||
|
||||
export const keyFrameMoveUpDown = keyframes`
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
const OpenFolderButton = styled(FontAwesomeIcon)`
|
||||
transition: color 0.1s ease-in-out;
|
||||
cursor: pointer;
|
||||
margin: 0 10px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
path {
|
||||
cursor: pointer;
|
||||
transition: color 0.1s ease-in-out;
|
||||
color: ${props => props.theme.palette.primary.main};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let watcher;
|
||||
|
||||
const toggleResourcePackDisabled = async (c, instancePath, item) => {
|
||||
const destFileName = c ? item.replace('.disabled', '') : `${item}.disabled`;
|
||||
|
||||
await fse.move(
|
||||
path.join(instancePath, 'resourcepacks', item),
|
||||
path.join(instancePath, 'resourcepacks', destFileName)
|
||||
);
|
||||
};
|
||||
|
||||
const createItemData = memoize(
|
||||
(items, instanceName, instancePath, selectedItems, setSelectedItems) => ({
|
||||
items,
|
||||
instanceName,
|
||||
instancePath,
|
||||
selectedItems,
|
||||
setSelectedItems
|
||||
})
|
||||
);
|
||||
const NotItemsAvailable = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const ResourcePacks = ({ instanceName }) => {
|
||||
const instancesPath = useSelector(_getInstancesPath);
|
||||
const [resourcePacks, setResourcePacks] = useState([]);
|
||||
const [selectedItems, setSelectedItems] = useState([]);
|
||||
const resourcePacksPath = path.join(
|
||||
instancesPath,
|
||||
instanceName,
|
||||
'resourcepacks'
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const deleteFile = useCallback(
|
||||
async (
|
||||
item,
|
||||
instancesPathh,
|
||||
selectedItemss,
|
||||
rscPacksPath,
|
||||
instanceNamee
|
||||
) => {
|
||||
if (item) {
|
||||
await fse.remove(
|
||||
path.join(instancesPathh, instanceNamee, 'resourcepacks', item)
|
||||
);
|
||||
} else if (selectedItemss.length > 0) {
|
||||
await Promise.all(
|
||||
selectedItemss.map(async file => {
|
||||
await fse.remove(path.join(rscPacksPath, file));
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[selectedItems, instancesPath, instanceName]
|
||||
);
|
||||
|
||||
const Row = memo(({ index, style, data }) => {
|
||||
const {
|
||||
items,
|
||||
instanceName: name,
|
||||
instancePath,
|
||||
selectedItems: slcItems,
|
||||
setSelectedItems: setSlcItems,
|
||||
resourcePacksPath: rscPacksPath
|
||||
} = data;
|
||||
const item = items[index];
|
||||
return (
|
||||
<RowContainer
|
||||
index={index}
|
||||
override={{
|
||||
...style,
|
||||
top: style.top + 15,
|
||||
height: style.height - 15,
|
||||
position: 'absolute',
|
||||
width: '97%',
|
||||
margin: '15px 0',
|
||||
transition: 'height 0.2s ease-in-out'
|
||||
}}
|
||||
selected={slcItems.includes(item)}
|
||||
disabled={path.extname(item) === '.disabled'}
|
||||
>
|
||||
<div className="leftPartContent">
|
||||
<Checkbox
|
||||
checked={slcItems.includes(item)}
|
||||
onChange={e => {
|
||||
if (e.target.checked) {
|
||||
setSlcItems([...slcItems, item]);
|
||||
} else {
|
||||
setSlcItems(slcItems.filter(v => v !== item));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{item.fileID && <img src={curseForgeIcon} alt="curseforge" />}
|
||||
</div>
|
||||
<div className="rowCenterContent">{item.replace('.disabled', '')}</div>
|
||||
<div className="rightPartContent">
|
||||
<Switch
|
||||
size="small"
|
||||
checked={path.extname(item) !== '.disabled'}
|
||||
disabled={loading}
|
||||
onChange={async c => {
|
||||
setLoading(true);
|
||||
await toggleResourcePackDisabled(c, instancePath, item);
|
||||
setTimeout(() => setLoading(false), 500);
|
||||
}}
|
||||
/>
|
||||
<TrashIcon
|
||||
selectedMods
|
||||
onClick={() => {
|
||||
deleteFile(item, instancesPath, slcItems, rscPacksPath, name);
|
||||
}}
|
||||
icon={faTrash}
|
||||
/>
|
||||
</div>
|
||||
<RowContainerBackground
|
||||
selected={slcItems.includes(item)}
|
||||
disabled={path.extname(item) === '.disabled'}
|
||||
/>
|
||||
</RowContainer>
|
||||
);
|
||||
}, areEqual);
|
||||
|
||||
const openFolderDialog = async () => {
|
||||
const dialog = await ipcRenderer.invoke('openFileDialog', [
|
||||
{ name: 'Resource Pack', extensions: ['zip'] },
|
||||
{ name: 'All', extensions: ['*'] }
|
||||
]);
|
||||
if (dialog.canceled) return;
|
||||
const fileName = path.basename(dialog.filePaths[0]);
|
||||
await fse.copy(
|
||||
dialog.filePaths[0],
|
||||
path.join(instancesPath, instanceName, 'resourcepacks', fileName)
|
||||
);
|
||||
};
|
||||
|
||||
const openFolder = async p => {
|
||||
await makeDir(p);
|
||||
ipcRenderer.invoke('openFolder', p);
|
||||
};
|
||||
|
||||
const startListener = async () => {
|
||||
await makeDir(resourcePacksPath);
|
||||
const files = await fs.readdir(resourcePacksPath);
|
||||
setResourcePacks(files);
|
||||
watcher = watch(resourcePacksPath, async (event, filename) => {
|
||||
if (filename) {
|
||||
const resourcePackFiles = await fs.readdir(resourcePacksPath);
|
||||
setResourcePacks(resourcePackFiles);
|
||||
setSelectedItems(prev => {
|
||||
return prev.filter(v => resourcePackFiles.includes(v));
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
startListener();
|
||||
return () => watcher?.close();
|
||||
}, []);
|
||||
|
||||
const itemData = createItemData(
|
||||
resourcePacks,
|
||||
instanceName,
|
||||
path.join(instancesPath, instanceName),
|
||||
selectedItems,
|
||||
setSelectedItems,
|
||||
resourcePacksPath
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
css={`
|
||||
flex: 1;
|
||||
`}
|
||||
>
|
||||
<Header>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<Checkbox
|
||||
checked={
|
||||
selectedItems.length === resourcePacks.length &&
|
||||
selectedItems.length !== 0
|
||||
}
|
||||
indeterminate={
|
||||
selectedItems.length !== 0 &&
|
||||
selectedItems.length !== resourcePacks.length
|
||||
}
|
||||
onChange={() =>
|
||||
selectedItems.length !== resourcePacks.length
|
||||
? setSelectedItems(resourcePacks)
|
||||
: setSelectedItems([])
|
||||
}
|
||||
>
|
||||
Select All
|
||||
</Checkbox>
|
||||
<TrashIcon
|
||||
selectedMods={selectedItems.length}
|
||||
onClick={async () => {
|
||||
deleteFile(
|
||||
null,
|
||||
instancesPath,
|
||||
selectedItems,
|
||||
resourcePacksPath,
|
||||
instanceName
|
||||
);
|
||||
}}
|
||||
icon={faTrash}
|
||||
/>
|
||||
<OpenFolderButton
|
||||
onClick={async () => {
|
||||
await makeDir(
|
||||
path.join(instancesPath, instanceName, 'resourcepacks')
|
||||
);
|
||||
openFolder(
|
||||
path.join(instancesPath, instanceName, 'resourcepacks')
|
||||
);
|
||||
}}
|
||||
icon={faFolder}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
css={`
|
||||
margin: 0 10px;
|
||||
`}
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
openFolderDialog();
|
||||
}}
|
||||
>
|
||||
Add ResourcePack
|
||||
</Button>
|
||||
</Header>
|
||||
|
||||
<DragnDropEffect
|
||||
instancesPath={instancesPath}
|
||||
instanceName={instanceName}
|
||||
fileList={resourcePacks}
|
||||
>
|
||||
{resourcePacks.length === 0 && (
|
||||
<NotItemsAvailable>No ResourcePacks Available</NotItemsAvailable>
|
||||
)}
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
height={height}
|
||||
itemData={itemData}
|
||||
itemCount={resourcePacks.length}
|
||||
itemSize={60}
|
||||
width={width}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</DragnDropEffect>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ResourcePacks);
|
||||
@@ -0,0 +1,624 @@
|
||||
/* eslint-disable */
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { promises as fs, watch, createReadStream } from 'fs';
|
||||
import { clipboard, ipcRenderer } from 'electron';
|
||||
import fse from 'fs-extra';
|
||||
import path from 'path';
|
||||
import base64 from 'base64-stream';
|
||||
import getStream from 'get-stream';
|
||||
import styled from 'styled-components';
|
||||
import makeDir from 'make-dir';
|
||||
import { Checkbox } from 'antd';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import {
|
||||
ContextMenuTrigger,
|
||||
ContextMenu,
|
||||
MenuItem,
|
||||
hideMenu
|
||||
} from 'react-contextmenu';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faTrash,
|
||||
faCopy,
|
||||
faLink,
|
||||
faFolder,
|
||||
faImage
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { _getInstancesPath } from '../../utils/selectors';
|
||||
import { openModal } from '../../reducers/modals/actions';
|
||||
import { imgurPost } from '../../api';
|
||||
|
||||
const getScreenshots = async screenshotsPath => {
|
||||
const files = await fs.readdir(screenshotsPath);
|
||||
const screenshots = [];
|
||||
try {
|
||||
await Promise.all(
|
||||
files.map(async element => {
|
||||
const stats = await fs.stat(path.join(screenshotsPath, element));
|
||||
const fileBirthdate = new Date(stats.birthtimeMs);
|
||||
const timeDiff = Date.now() - fileBirthdate;
|
||||
const days = parseInt(Math.floor(timeDiff / 1000) / 60 / 60 / 24, 10);
|
||||
screenshots.push({
|
||||
name: element,
|
||||
days,
|
||||
timestamp: fileBirthdate,
|
||||
size: stats.size
|
||||
});
|
||||
})
|
||||
);
|
||||
return screenshots.sort((a, b) => a.timestamp - b.timestamp);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const getImgurLink = async (imagePath, fileSize, setProgressUpdate) => {
|
||||
const updateProgress = progressEvent => {
|
||||
setProgressUpdate(
|
||||
Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||
);
|
||||
};
|
||||
|
||||
const imageReadStream = createReadStream(imagePath);
|
||||
const encodedData = new base64.Base64Encode();
|
||||
const b64s = imageReadStream.pipe(encodedData);
|
||||
const base64String = await getStream(b64s);
|
||||
|
||||
if (fileSize < 10485760) {
|
||||
const res = await imgurPost(base64String, updateProgress);
|
||||
|
||||
if (res.status == 200) {
|
||||
clipboard.writeText(res.data.data.link);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openFolder = screenshotsPath => {
|
||||
ipcRenderer.invoke('openFolder', screenshotsPath);
|
||||
};
|
||||
|
||||
const getTitle = days => {
|
||||
const parsedDays = Number.parseInt(days, 10);
|
||||
if (parsedDays === 0) return 'Today';
|
||||
else if (parsedDays === 1) return 'Yesterday';
|
||||
else if (parsedDays > 1 && parsedDays < 30) return `${days} days ago`;
|
||||
else if (parsedDays >= 30 && parsedDays < 365)
|
||||
return `${Math.floor(days / 30)} months ago`;
|
||||
else if (parsedDays >= 365) return `${Math.floor(days / 365)} years ago`;
|
||||
};
|
||||
|
||||
const getScreenshotsCount = groups =>
|
||||
Object.values(groups).reduce((prev, curr) => (prev += curr.length), 0);
|
||||
|
||||
const getScreenshotsList = groups =>
|
||||
Object.values(groups).reduce((prev, curr) => prev.concat(curr), []);
|
||||
|
||||
let watcher;
|
||||
|
||||
const Screenshots = ({ instanceName }) => {
|
||||
const instancesPath = useSelector(_getInstancesPath);
|
||||
const screenshotsPath = path.join(instancesPath, instanceName, 'screenshots');
|
||||
const [dateGroups, setDateGroups] = useState({});
|
||||
const [selectedItems, setSelectedItems] = useState([]);
|
||||
const [progressUpdate, setProgressUpdate] = useState(null);
|
||||
const [uploadingFileName, setUploadingFileName] = useState(null);
|
||||
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isImageCopied = progressUpdate => {
|
||||
if (
|
||||
progressUpdate === 100 &&
|
||||
uploadingFileName !== null &&
|
||||
selectedItems.includes(uploadingFileName)
|
||||
) {
|
||||
return 'Image copied to clipboard!';
|
||||
} else if (
|
||||
uploadingFileName != null &&
|
||||
selectedItems[0] != uploadingFileName
|
||||
) {
|
||||
return 'Busy! Wait before uploading another image';
|
||||
} else return 'Share the image via url';
|
||||
};
|
||||
|
||||
const containerRef = useRef(null);
|
||||
|
||||
const selectAll = useCallback(() => {
|
||||
if (
|
||||
isEqual(
|
||||
sortBy(getScreenshotsList(dateGroups).map(x => x.name)),
|
||||
sortBy(selectedItems)
|
||||
)
|
||||
) {
|
||||
setSelectedItems([]);
|
||||
} else {
|
||||
setSelectedItems(getScreenshotsList(dateGroups).map(x => x.name));
|
||||
}
|
||||
}, [selectedItems, dateGroups, setSelectedItems]);
|
||||
|
||||
const deleteFile = useCallback(
|
||||
async fileName => {
|
||||
if (selectedItems.length === 1) {
|
||||
await fse.remove(
|
||||
path.join(
|
||||
instancesPath,
|
||||
instanceName,
|
||||
'screenshots',
|
||||
selectedItems[0]
|
||||
)
|
||||
);
|
||||
} else if (selectedItems.length > 1) {
|
||||
await Promise.all(
|
||||
selectedItems.map(async screenShot => {
|
||||
await fse.remove(
|
||||
path.join(instancesPath, instanceName, 'screenshots', screenShot)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[selectedItems, instancesPath, instanceName]
|
||||
);
|
||||
|
||||
const startListener = async () => {
|
||||
await makeDir(screenshotsPath);
|
||||
const screenshots = await getScreenshots(screenshotsPath);
|
||||
setDateGroups(groupBy(screenshots, 'days'));
|
||||
watcher = watch(screenshotsPath, async (event, filename) => {
|
||||
if (filename) {
|
||||
const sortedScreens = await getScreenshots(screenshotsPath);
|
||||
setDateGroups(groupBy(sortedScreens, 'days'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
startListener();
|
||||
|
||||
return () => watcher?.close();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
const eventHandler = e => {
|
||||
if (contextMenuOpen) {
|
||||
e.preventDefault();
|
||||
containerRef.current.scrollTop = 0;
|
||||
}
|
||||
};
|
||||
containerRef?.current?.addEventListener('wheel', eventHandler);
|
||||
return () =>
|
||||
containerRef?.current?.removeEventListener('wheel', eventHandler);
|
||||
}
|
||||
}, [containerRef.current, contextMenuOpen]);
|
||||
|
||||
return (
|
||||
<ExternalContainer ref={containerRef}>
|
||||
<Bar>
|
||||
<GlobalCheckbox
|
||||
onChange={selectAll}
|
||||
indeterminate={
|
||||
selectedItems.length > 0 &&
|
||||
selectedItems.length < getScreenshotsCount(dateGroups)
|
||||
}
|
||||
checked={
|
||||
getScreenshotsCount(dateGroups) > 0 &&
|
||||
getScreenshotsCount(dateGroups) === selectedItems.length
|
||||
}
|
||||
>
|
||||
{`${selectedItems.length} selected`}
|
||||
</GlobalCheckbox>
|
||||
|
||||
<DeleteButton
|
||||
onClick={() => {
|
||||
if (selectedItems.length) {
|
||||
dispatch(
|
||||
openModal('ActionConfirmation', {
|
||||
message: 'Are you sure you want to delete this image(s)?',
|
||||
confirmCallback: deleteFile,
|
||||
title: 'Confirm'
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
selectedItems={selectedItems}
|
||||
icon={faTrash}
|
||||
/>
|
||||
<OpenFolderButton
|
||||
onClick={() => openFolder(screenshotsPath)}
|
||||
icon={faFolder}
|
||||
/>
|
||||
</Bar>
|
||||
<Container groupsCount={Object.entries(dateGroups).length}>
|
||||
{Object.entries(dateGroups).length > 0 ? (
|
||||
Object.entries(dateGroups).map(([key, group]) => {
|
||||
return (
|
||||
<DataSectionContainer key={key}>
|
||||
<TitleDataSection>{getTitle(key.toString())}</TitleDataSection>
|
||||
<DateSection groupsCount={Object.entries(dateGroups).length}>
|
||||
{group.map(file => (
|
||||
<span key={file.name}>
|
||||
<ContextMenuTrigger id={file.name}>
|
||||
<PhotoContainer
|
||||
selectedItems={selectedItems}
|
||||
name={file.name}
|
||||
>
|
||||
<SelectCheckBoxContainer>
|
||||
<SelectCheckBox
|
||||
onClick={() => {
|
||||
setSelectedItems(
|
||||
selectedItems.indexOf(file.name) > -1
|
||||
? selectedItems.filter(x => x != file.name)
|
||||
: selectedItems.concat([file.name])
|
||||
);
|
||||
}}
|
||||
checked={selectedItems.indexOf(file.name) > -1}
|
||||
selected={selectedItems.indexOf(file.name) > -1}
|
||||
/>
|
||||
</SelectCheckBoxContainer>
|
||||
<Photo
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
openModal('Screenshot', {
|
||||
screenshotsPath,
|
||||
file
|
||||
})
|
||||
)
|
||||
}
|
||||
selected={selectedItems.indexOf(file.name) > -1}
|
||||
src={`file:///${path.join(
|
||||
screenshotsPath,
|
||||
file.name
|
||||
)}`}
|
||||
/>
|
||||
</PhotoContainer>
|
||||
</ContextMenuTrigger>
|
||||
<StyledContexMenu
|
||||
id={file.name}
|
||||
onShow={() => {
|
||||
setContextMenuOpen(true);
|
||||
if (
|
||||
selectedItems.length === 0 ||
|
||||
!selectedItems.includes(file.name)
|
||||
) {
|
||||
setSelectedItems([file.name]);
|
||||
} else if (
|
||||
selectedItems.length === 1 &&
|
||||
!selectedItems.includes(file.name)
|
||||
) {
|
||||
setSelectedItems([...selectedItems, file.name]);
|
||||
}
|
||||
}}
|
||||
onHide={() => {
|
||||
setContextMenuOpen(false);
|
||||
if (!selectedItems.includes(file.name)) {
|
||||
setSelectedItems([file.name]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedItems.length > 1 &&
|
||||
selectedItems.length <
|
||||
getScreenshotsCount(dateGroups) ? (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
openModal('ActionConfirmation', {
|
||||
message:
|
||||
'Are you sure you want to delete this image?',
|
||||
fileName: file.name,
|
||||
confirmCallback: deleteFile,
|
||||
title: 'Confirm'
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
{`Delete ${selectedItems.length} items`}
|
||||
</MenuItem>
|
||||
) : (
|
||||
selectedItems.length ===
|
||||
getScreenshotsCount(dateGroups) &&
|
||||
getScreenshotsCount(dateGroups) > 1 && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
openModal('ActionConfirmation', {
|
||||
message:
|
||||
'Are you sure you want to delete this image?',
|
||||
fileName: file.name,
|
||||
confirmCallback: deleteFile,
|
||||
title: 'Confirm'
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
Delete all
|
||||
</MenuItem>
|
||||
)
|
||||
)}
|
||||
|
||||
{selectedItems.length < 2 && (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
openModal('Screenshot', {
|
||||
screenshotsPath,
|
||||
file
|
||||
})
|
||||
)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faImage} />
|
||||
Preview
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
clipboard.writeImage(
|
||||
path.join(screenshotsPath, file.name)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
Copy the image
|
||||
</MenuItem>
|
||||
<ImgurShareMenuItem
|
||||
disabled={
|
||||
uploadingFileName != null &&
|
||||
selectedItems.includes(uploadingFileName)
|
||||
}
|
||||
preventClose
|
||||
onClick={async () => {
|
||||
if (file.size < 10485760) {
|
||||
setUploadingFileName(file.name);
|
||||
try {
|
||||
await getImgurLink(
|
||||
path.join(screenshotsPath, file.name),
|
||||
file.size,
|
||||
setProgressUpdate
|
||||
);
|
||||
} finally {
|
||||
setUploadingFileName(null);
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
hideMenu();
|
||||
}, 1000);
|
||||
}}
|
||||
>
|
||||
<MenuShareLink>
|
||||
<FontAwesomeIcon icon={faLink} />
|
||||
{file.size < 10485760
|
||||
? isImageCopied(
|
||||
progressUpdate,
|
||||
uploadingFileName,
|
||||
selectedItems
|
||||
)
|
||||
: `Image too big... ${Math.floor(
|
||||
file.size / 1024 / 1024
|
||||
)}MB`}
|
||||
</MenuShareLink>
|
||||
<LoadingSlider
|
||||
selectedItems={selectedItems}
|
||||
uploadingFileName={uploadingFileName}
|
||||
translateAmount={
|
||||
uploadingFileName != null &&
|
||||
selectedItems.includes(uploadingFileName)
|
||||
? -(100 - progressUpdate)
|
||||
: -100
|
||||
}
|
||||
/>
|
||||
</ImgurShareMenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
openModal('ActionConfirmation', {
|
||||
message:
|
||||
'Are you sure you want to delete this image?',
|
||||
fileName: file.name,
|
||||
confirmCallback: deleteFile,
|
||||
title: 'Confirm'
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
Delete
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</StyledContexMenu>
|
||||
</span>
|
||||
))}
|
||||
</DateSection>
|
||||
</DataSectionContainer>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<NoScreenAvailable>No Screenshot Available</NoScreenAvailable>
|
||||
)}
|
||||
</Container>
|
||||
</ExternalContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Screenshots;
|
||||
|
||||
const ExternalContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
background: ${props => props.theme.palette.secondary.main};
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: ${props => (props.groupsCount !== 0 ? 'auto' : '100%')};
|
||||
`;
|
||||
|
||||
const Bar = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
min-height: 40px;
|
||||
max-height: 40px;
|
||||
width: 100%;
|
||||
background: ${props => props.theme.palette.secondary.main};
|
||||
`;
|
||||
|
||||
const GlobalCheckbox = styled(Checkbox)`
|
||||
margin: 7px;
|
||||
`;
|
||||
|
||||
const DateSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap-reverse;
|
||||
padding: 50px 10px 20px 10px;
|
||||
background: ${props => props.theme.palette.secondary.dark};
|
||||
margin: 10px 0 0 0;
|
||||
`;
|
||||
|
||||
const NoScreenAvailable = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledContexMenu = styled(ContextMenu)`
|
||||
svg {
|
||||
margin: 0 7px 0 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const DeleteButton = styled(({ selectedItems, ...props }) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<FontAwesomeIcon {...props} />
|
||||
))`
|
||||
margin: 0 10px;
|
||||
transition: color 0.3s ease-in-out;
|
||||
&:hover {
|
||||
path {
|
||||
color: ${props =>
|
||||
props.selectedItems.length > 0 ? props.theme.palette.colors.red : ''};
|
||||
}
|
||||
}
|
||||
cursor: ${props => (props.selectedItems.length > 0 ? 'pointer' : '')};
|
||||
`;
|
||||
|
||||
const OpenFolderButton = styled(FontAwesomeIcon)`
|
||||
transition: color 0.1s ease-in-out;
|
||||
cursor: pointer;
|
||||
margin: 0 10px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
path {
|
||||
cursor: pointer;
|
||||
transition: color 0.1s ease-in-out;
|
||||
color: ${props => props.theme.palette.primary.main};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const DataSectionContainer = styled.span`
|
||||
&:first-child {
|
||||
margin-top: -45px;
|
||||
}
|
||||
`;
|
||||
|
||||
const TitleDataSection = styled.h2`
|
||||
position: relative;
|
||||
top: 50px;
|
||||
left: 20px;
|
||||
`;
|
||||
|
||||
const LoadingSlider = styled.div`
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: ${props =>
|
||||
props.uploadingFileName != null
|
||||
? `translate(${props.translateAmount}%)`
|
||||
: 'translate(-100%)'};
|
||||
transition: transform 0.1s ease-in-out;
|
||||
background: ${props => props.theme.palette.primary.main};
|
||||
`;
|
||||
|
||||
const Photo = styled.img`
|
||||
height: 100px;
|
||||
max-height: 100px;
|
||||
width: 100px;
|
||||
max-width: 200px;
|
||||
margin: 10px;
|
||||
object-fit: cover;
|
||||
background: ${props => props.theme.palette.secondary.light};
|
||||
border-radius: 5px;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
filter: brightness(80%);
|
||||
border: ${props =>
|
||||
props.selected ? `solid 2px ${props.theme.palette.colors.blue}` : ''};
|
||||
`;
|
||||
|
||||
const SelectCheckBoxContainer = styled.div`
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
`;
|
||||
|
||||
const SelectCheckBox = styled(Checkbox)`
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 15px;
|
||||
z-index: 2;
|
||||
opacity: ${props => (props.selected ? 1 : 0)};
|
||||
`;
|
||||
|
||||
const ImgurShareMenuItem = styled(MenuItem)`
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 0 !important;
|
||||
`;
|
||||
|
||||
const MenuShareLink = styled.div`
|
||||
padding: 4px 10px;
|
||||
position: relative;
|
||||
svg {
|
||||
margin: 0 7px 0 0;
|
||||
}
|
||||
`;
|
||||
const PhotoContainer = styled.div`
|
||||
position: relative;
|
||||
height: 100px;
|
||||
max-height: 100px;
|
||||
width: 100px;
|
||||
max-width: 200px;
|
||||
margin: 10px;
|
||||
background: transparent;
|
||||
border-radius: 5px;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
filter: brightness(80%);
|
||||
transform: ${props =>
|
||||
props.selectedItems.indexOf(props.name) > -1 ? 'scale(1.1)' : 'scale(1)'};
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
${SelectCheckBox} {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,435 @@
|
||||
import React, { useState, useEffect, lazy } from 'react';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import { Button } from 'antd';
|
||||
import fse from 'fs-extra';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faPlay,
|
||||
faStop,
|
||||
faTimesCircle
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import omit from 'lodash/omit';
|
||||
import psTree from 'ps-tree';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import Modal from '../../components/Modal';
|
||||
import AsyncComponent from '../../components/AsyncComponent';
|
||||
import { _getInstance, _getInstancesPath } from '../../utils/selectors';
|
||||
import { FORGE, FABRIC, CURSEFORGE } from '../../utils/constants';
|
||||
import {
|
||||
addStartedInstance,
|
||||
clearLatestModManifests,
|
||||
launchInstance,
|
||||
updateInstanceConfig
|
||||
} from '../../reducers/actions';
|
||||
import instanceDefaultBackground from '../../assets/instance_default.png';
|
||||
|
||||
const SideMenu = styled.div`
|
||||
display: flex;
|
||||
flex: 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
const SideMenuContainer = styled.div`
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
flex-grow: 3;
|
||||
background: ${props => props.theme.palette.grey[800]};
|
||||
`;
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
const SettingsButton = styled(({ active, ...props }) => <Button {...props} />)`
|
||||
align-items: left;
|
||||
justify-content: left;
|
||||
text-align: left;
|
||||
width: 170px;
|
||||
height: 40px;
|
||||
border-radius: 4px 0 0 4px;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
white-space: nowrap;
|
||||
background: ${props =>
|
||||
props.active
|
||||
? props.theme.palette.grey[600]
|
||||
: props.theme.palette.grey[800]};
|
||||
border: 0px;
|
||||
text-align: left;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
&:hover {
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
background: ${props => props.theme.palette.grey[700]};
|
||||
}
|
||||
&:focus {
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
background: ${props => props.theme.palette.grey[600]};
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const Overlay = styled.div`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 10%;
|
||||
background: ${props => props.theme.palette.grey[800]};
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
`;
|
||||
|
||||
const InstanceBackground = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 130px;
|
||||
height: 100px;
|
||||
border-radius: 10%;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 10px;
|
||||
background: ${props =>
|
||||
props.imagePath
|
||||
? `url(${props.imagePath}) center no-repeat`
|
||||
: `url(${instanceDefaultBackground}) center no-repeat`};
|
||||
background-size: 180px;
|
||||
transition: opacity 0.2s ease;
|
||||
&:hover svg {
|
||||
opacity: 1;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:hover p {
|
||||
opacity: 1;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:hover ${Overlay} {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-top: 10px;
|
||||
width: 30px;
|
||||
color: ${props => props.theme.palette.colors.red};
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Spinner = keyframes`
|
||||
0% {
|
||||
transform: translate3d(-50%, -50%, 0) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(-50%, -50%, 0) rotate(360deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const PlayButtonAnimation = keyframes`
|
||||
from {
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const menuEntries = {
|
||||
overview: {
|
||||
name: 'Overview',
|
||||
component: AsyncComponent(lazy(() => import('./Overview')))
|
||||
},
|
||||
mods: {
|
||||
name: 'Mods',
|
||||
component: AsyncComponent(lazy(() => import('./Mods')))
|
||||
},
|
||||
modpack: {
|
||||
name: 'Modpack',
|
||||
component: AsyncComponent(lazy(() => import('./Modpack')))
|
||||
},
|
||||
notes: {
|
||||
name: 'Notes',
|
||||
component: AsyncComponent(lazy(() => import('./Notes')))
|
||||
},
|
||||
resourcePacks: {
|
||||
name: 'Resource Packs',
|
||||
component: AsyncComponent(lazy(() => import('./ResourcePacks')))
|
||||
},
|
||||
// resourcePacks: { name: "Resource Packs", component: Overview },
|
||||
// worlds: { name: "Worlds", component: Overview },
|
||||
screenshots: {
|
||||
name: 'Screenshots',
|
||||
component: AsyncComponent(lazy(() => import('./Screenshots')))
|
||||
}
|
||||
// settings: { name: "Settings", component: Overview },
|
||||
// servers: { name: "Servers", component: Overview }
|
||||
};
|
||||
|
||||
const InstanceManager = ({ instanceName }) => {
|
||||
const dispatch = useDispatch();
|
||||
const instancesPath = useSelector(_getInstancesPath);
|
||||
const [page, setPage] = useState(Object.keys(menuEntries)[0]);
|
||||
const instance = useSelector(state => _getInstance(state)(instanceName));
|
||||
const startedInstances = useSelector(state => state.startedInstances);
|
||||
const [background, setBackground] = useState(instance?.background);
|
||||
const [manifest, setManifest] = useState(null);
|
||||
const ContentComponent = menuEntries[page].component;
|
||||
|
||||
const isPlaying = startedInstances[instanceName];
|
||||
|
||||
const updateBackground = v => {
|
||||
if (v) {
|
||||
dispatch(
|
||||
updateInstanceConfig(instanceName, prev => ({
|
||||
...prev,
|
||||
background: v
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
updateInstanceConfig(instanceName, prev => ({
|
||||
...omit(prev, ['background'])
|
||||
}))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const openFileDialog = async () => {
|
||||
const dialog = await ipcRenderer.invoke('openFileDialog', [
|
||||
{ name: 'Image', extensions: ['png', 'jpg', 'jpeg'] }
|
||||
]);
|
||||
if (dialog.canceled) return;
|
||||
const instancePath = path.join(instancesPath, instanceName);
|
||||
const ext = path.extname(dialog.filePaths[0]);
|
||||
const filePath = path.join(instancePath, `background${ext}`);
|
||||
await fse.copy(dialog.filePaths[0], filePath);
|
||||
const res = await fs.readFile(filePath);
|
||||
setBackground(`data:image/png;base64,${res.toString('base64')}`);
|
||||
updateBackground(`background${ext}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (instance?.background) {
|
||||
fs.readFile(path.join(instancesPath, instanceName, instance.background))
|
||||
.then(res =>
|
||||
setBackground(`data:image/png;base64,${res.toString('base64')}`)
|
||||
)
|
||||
.catch(console.warning);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(clearLatestModManifests());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (instance?.loader.source === CURSEFORGE) {
|
||||
fse
|
||||
.readJson(path.join(instancesPath, instanceName, 'manifest.json'))
|
||||
.then(setManifest)
|
||||
.catch(console.error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 85%;
|
||||
width: 85%;
|
||||
max-width: 1500px;
|
||||
`}
|
||||
title={`Instance Manager - ${instanceName}`}
|
||||
removePadding
|
||||
>
|
||||
<Container>
|
||||
<SideMenuContainer>
|
||||
<SideMenu>
|
||||
<InstanceBackground onClick={openFileDialog} imagePath={background}>
|
||||
<Overlay />
|
||||
<p>Change Icon</p>
|
||||
{background && (
|
||||
<FontAwesomeIcon
|
||||
icon={faTimesCircle}
|
||||
css={`
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
updateBackground(null);
|
||||
setBackground(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</InstanceBackground>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
position: relative;
|
||||
background: ${props => props.theme.palette.colors.green};
|
||||
padding: 5px 10px;
|
||||
border-radius: 10px 0 0 10px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
width: 80px;
|
||||
height: 35px;
|
||||
text-align: center;
|
||||
cursor: ${props => (props.isPlaying ? 'default' : 'pointer')};
|
||||
|
||||
.spinner:before {
|
||||
animation: 1.5s linear infinite ${Spinner};
|
||||
animation-play-state: inherit;
|
||||
border: solid 3px transparent;
|
||||
border-bottom-color: ${props =>
|
||||
props.theme.palette.common.white};
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
will-change: transform;
|
||||
}
|
||||
`}
|
||||
isPlaying={isPlaying}
|
||||
onClick={() => {
|
||||
if (isPlaying) return;
|
||||
dispatch(addStartedInstance({ instanceName }));
|
||||
dispatch(launchInstance(instanceName));
|
||||
}}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<div
|
||||
css={`
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
{isPlaying.initialized && (
|
||||
<FontAwesomeIcon
|
||||
css={`
|
||||
color: ${({ theme }) => theme.palette.common.white};
|
||||
position: absolute;
|
||||
margin-left: 6px;
|
||||
animation: ${PlayButtonAnimation} 0.5s
|
||||
cubic-bezier(0.75, -1.5, 0, 2.75);
|
||||
`}
|
||||
icon={faPlay}
|
||||
/>
|
||||
)}
|
||||
{!isPlaying.initialized && <div className="spinner" />}
|
||||
</div>
|
||||
) : (
|
||||
<span>PLAY</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
padding: 5px 15px;
|
||||
display: grid;
|
||||
font-size: 16px;
|
||||
place-items: center;
|
||||
background: ${props => props.theme.palette.colors.red};
|
||||
border-radius: 0 10px 10px 0;
|
||||
opacity: ${props => (props.isPlaying ? 1 : 0.3)};
|
||||
cursor: ${props => (props.isPlaying ? 'pointer' : 'default')};
|
||||
`}
|
||||
isPlaying={isPlaying}
|
||||
onClick={() => {
|
||||
if (!isPlaying) return;
|
||||
psTree(isPlaying.pid, (err, children) => {
|
||||
if (children?.length) {
|
||||
children.forEach(el => {
|
||||
if (el) {
|
||||
try {
|
||||
process.kill(el.PID);
|
||||
} catch {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
process.kill(isPlaying.pid);
|
||||
} catch {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faStop} />
|
||||
</div>
|
||||
</div>
|
||||
{Object.entries(menuEntries).map(([k, tab]) => {
|
||||
if (
|
||||
(tab.name === menuEntries.mods.name &&
|
||||
instance?.loader?.loaderType !== FORGE &&
|
||||
instance?.loader?.loaderType !== FABRIC) ||
|
||||
(tab.name === menuEntries.modpack.name &&
|
||||
!instance?.loader?.fileID)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SettingsButton
|
||||
key={tab.name}
|
||||
onClick={() => setPage(k)}
|
||||
active={k === page}
|
||||
>
|
||||
{tab.name}
|
||||
</SettingsButton>
|
||||
);
|
||||
})}
|
||||
</SideMenu>
|
||||
</SideMenuContainer>
|
||||
<Content>
|
||||
<ContentComponent
|
||||
instanceName={instanceName}
|
||||
modpackId={instance?.loader?.projectID}
|
||||
fileID={instance?.loader?.fileID}
|
||||
background={background}
|
||||
manifest={manifest}
|
||||
/>
|
||||
</Content>
|
||||
</Container>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(InstanceManager);
|
||||
@@ -0,0 +1,105 @@
|
||||
import React, { memo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import Modal from '../components/Modal';
|
||||
import { closeModal, openModal } from '../reducers/modals/actions';
|
||||
import BisectHosting from '../../ui/BisectHosting';
|
||||
import ga from '../utils/analytics';
|
||||
|
||||
let timer;
|
||||
|
||||
const InstanceStartupAd = ({ instanceName }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const openBisectHostingModal = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
dispatch(closeModal());
|
||||
setTimeout(() => {
|
||||
ga.sendCustomEvent('BHAdViewNavbar');
|
||||
dispatch(openModal('BisectHosting'));
|
||||
}, 225);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 330px;
|
||||
width: 650px;
|
||||
overflow-x: hidden;
|
||||
`}
|
||||
title={`Starting up ${instanceName}`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
`}
|
||||
>
|
||||
<span
|
||||
css={`
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
margin-top: 20px;
|
||||
`}
|
||||
>
|
||||
Your instance is starting...
|
||||
<LoadingOutlined
|
||||
css={`
|
||||
margin-left: 30px;
|
||||
font-size: 50px;
|
||||
`}
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& > * {
|
||||
margin: 0 20px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
css={`
|
||||
font-size: 14px;
|
||||
`}
|
||||
>
|
||||
Grab a server from <br /> our official partner
|
||||
</span>
|
||||
<div
|
||||
css={`
|
||||
cursor: pointer;
|
||||
`}
|
||||
>
|
||||
<BisectHosting
|
||||
onClick={openBisectHostingModal}
|
||||
size={60}
|
||||
showPointerCursor
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
css={`
|
||||
font-size: 70px;
|
||||
color: ${({ theme }) => theme.palette.colors.red};
|
||||
`}
|
||||
>
|
||||
❤
|
||||
</span>
|
||||
<div>Thank you!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(InstanceStartupAd);
|
||||
@@ -0,0 +1,629 @@
|
||||
/* eslint-disable no-loop-func */
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import { Button, Progress, Input } from 'antd';
|
||||
import { Transition } from 'react-transition-group';
|
||||
import styled, { useTheme } from 'styled-components';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import fse from 'fs-extra';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import path from 'path';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faFolder } from '@fortawesome/free-solid-svg-icons';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import Modal from '../components/Modal';
|
||||
import { downloadFile } from '../../app/desktop/utils/downloader';
|
||||
import {
|
||||
convertOSToJavaFormat,
|
||||
convertArchToJavaFormat,
|
||||
extractAll,
|
||||
isLatestJavaDownloaded
|
||||
} from '../../app/desktop/utils';
|
||||
import { _getTempPath } from '../utils/selectors';
|
||||
import { closeModal } from '../reducers/modals/actions';
|
||||
import {
|
||||
updateJavaLatestPath,
|
||||
updateJavaPath
|
||||
} from '../reducers/settings/actions';
|
||||
import { UPDATE_MODAL } from '../reducers/modals/actionTypes';
|
||||
import { LATEST_JAVA_VERSION } from '../utils/constants';
|
||||
|
||||
const JavaSetup = () => {
|
||||
const [step, setStep] = useState(0);
|
||||
const [choice, setChoice] = useState(null);
|
||||
const [isJava8Downloaded, setIsJava8Downloaded] = useState(null);
|
||||
const [isJavaLatestDownloaded, setIsJavaLatestDownloaded] = useState(null);
|
||||
const [java8Log, setJava8Log] = useState(null);
|
||||
const [javaLatestLog, setJavaLatestLog] = useState(null);
|
||||
const javaManifest = useSelector(state => state.app.javaManifest);
|
||||
const javaLatestManifest = useSelector(state => state.app.javaLatestManifest);
|
||||
const userData = useSelector(state => state.userData);
|
||||
const manifests = {
|
||||
javaLatest: javaLatestManifest,
|
||||
java: javaManifest
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
isLatestJavaDownloaded(manifests, userData, true, 8)
|
||||
.then(e => {
|
||||
setIsJava8Downloaded(e?.isValid);
|
||||
return setJava8Log(e?.log);
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
isLatestJavaDownloaded(manifests, userData, true, LATEST_JAVA_VERSION)
|
||||
.then(e => {
|
||||
setIsJavaLatestDownloaded(e?.isValid);
|
||||
return setJavaLatestLog(e?.log);
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Java Setup"
|
||||
css={`
|
||||
height: 380px;
|
||||
width: 600px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
`}
|
||||
header={false}
|
||||
>
|
||||
<Transition in={step === 0} timeout={200}>
|
||||
{state => (
|
||||
<FirstStep state={state}>
|
||||
<div
|
||||
css={`
|
||||
font-size: 28px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
`}
|
||||
>
|
||||
Java Setup
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
margin-bottom: 20px;
|
||||
font-size: 18px;
|
||||
text-align: justify;
|
||||
`}
|
||||
>
|
||||
For an optimal experience, we suggest letting us take care of java
|
||||
for you. Only manually manage java if you know what you're
|
||||
doing, it may result in GDLauncher not working!
|
||||
</div>
|
||||
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
margin-bottom: 40px;
|
||||
opacity: 0;
|
||||
opacity: ${isJava8Downloaded !== null &&
|
||||
isJavaLatestDownloaded !== null &&
|
||||
(!isJava8Downloaded || !isJavaLatestDownloaded) &&
|
||||
'1'};
|
||||
* > h3 {
|
||||
border-radius: 5px;
|
||||
padding: 2px 4px;
|
||||
background: ${props => props.theme.palette.colors.red};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<h3>Missing Versions:</h3>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 40px;
|
||||
h3 {
|
||||
width: 71px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
padding: 2px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{!isJava8Downloaded && isJava8Downloaded !== null && (
|
||||
<h3
|
||||
css={`
|
||||
margin-right: 20px;
|
||||
`}
|
||||
>
|
||||
Java 8
|
||||
</h3>
|
||||
)}
|
||||
{!isJavaLatestDownloaded && isJavaLatestDownloaded !== null && (
|
||||
<h3>Java {LATEST_JAVA_VERSION}</h3>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
css={`
|
||||
& > div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
css={`
|
||||
width: 150px;
|
||||
`}
|
||||
onClick={() => {
|
||||
setStep(1);
|
||||
setChoice(0);
|
||||
}}
|
||||
>
|
||||
Automatic Setup
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="text"
|
||||
css={`
|
||||
width: 150px;
|
||||
`}
|
||||
onClick={() => {
|
||||
setStep(1);
|
||||
setChoice(1);
|
||||
}}
|
||||
>
|
||||
Manual Setup
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FirstStep>
|
||||
)}
|
||||
</Transition>
|
||||
<Transition in={step === 1} timeout={200}>
|
||||
{state => (
|
||||
<SecondStep state={state}>
|
||||
<div
|
||||
css={`
|
||||
font-size: 28px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
`}
|
||||
>
|
||||
{choice === 0 ? 'Automatic' : 'Manual'} Setup
|
||||
</div>
|
||||
{choice === 0 ? (
|
||||
<AutomaticSetup
|
||||
isJava8Downloaded={isJava8Downloaded}
|
||||
isJavaLatestDownloaded={isJavaLatestDownloaded}
|
||||
java8Log={java8Log}
|
||||
javaLatestLog={javaLatestLog}
|
||||
/>
|
||||
) : (
|
||||
<ManualSetup setStep={setStep} />
|
||||
)}
|
||||
</SecondStep>
|
||||
)}
|
||||
</Transition>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const ManualSetup = ({ setStep }) => {
|
||||
const [javaPath, setJavaPath] = useState('');
|
||||
const [javaLatestPath, setJavaLatestPath] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const selectFolder = async version => {
|
||||
const { filePaths, canceled } = await ipcRenderer.invoke('openFileDialog');
|
||||
if (!canceled) {
|
||||
if (version === LATEST_JAVA_VERSION) {
|
||||
setJavaLatestPath(filePaths[0]);
|
||||
} else setJavaPath(filePaths[0]);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div
|
||||
css={`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
margin-bottom: 50px;
|
||||
font-size: 18px;
|
||||
`}
|
||||
>
|
||||
Enter the required paths to java. Java 8 will be used for all the
|
||||
versions {'<'} 1.17, java {LATEST_JAVA_VERSION} for versions {'>='}{' '}
|
||||
1.17. You can also use the same executable but some versions might not
|
||||
run.
|
||||
</div>
|
||||
|
||||
<div
|
||||
css={`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
`}
|
||||
>
|
||||
<Input
|
||||
placeholder="Select your Java8 executable (MC < 1.17)"
|
||||
onChange={e => setJavaPath(e.target.value)}
|
||||
value={javaPath}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => selectFolder(8)}
|
||||
css={`
|
||||
margin-left: 10px;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFolder} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
css={`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
<Input
|
||||
placeholder={`Select your Java ${LATEST_JAVA_VERSION} executable (MC >= 1.17)`}
|
||||
onChange={e => setJavaLatestPath(e.target.value)}
|
||||
value={javaLatestPath}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => selectFolder(LATEST_JAVA_VERSION)}
|
||||
css={`
|
||||
margin-left: 10px;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFolder} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
css={`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 45px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
`}
|
||||
>
|
||||
<Button type="primary" onClick={() => setStep(0)}>
|
||||
Go Back
|
||||
</Button>
|
||||
<Button
|
||||
type="danger"
|
||||
disabled={javaPath === '' || javaLatestPath === ''}
|
||||
onClick={() => {
|
||||
dispatch(updateJavaPath(javaPath));
|
||||
dispatch(updateJavaLatestPath(javaLatestPath));
|
||||
dispatch(closeModal());
|
||||
}}
|
||||
>
|
||||
Continue with custom java
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AutomaticSetup = ({
|
||||
isJava8Downloaded,
|
||||
isJavaLatestDownloaded,
|
||||
java8Log,
|
||||
javaLatestLog
|
||||
}) => {
|
||||
const [downloadPercentage, setDownloadPercentage] = useState(0);
|
||||
const [currentSubStep, setCurrentSubStep] = useState('Downloading Java');
|
||||
const [currentStepPercentage, setCurrentStepPercentage] = useState(0);
|
||||
const javaManifest = useSelector(state => state.app.javaManifest);
|
||||
const javaLatestManifest = useSelector(state => state.app.javaLatestManifest);
|
||||
const userData = useSelector(state => state.userData);
|
||||
const tempFolder = useSelector(_getTempPath);
|
||||
const modals = useSelector(state => state.modals);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const theme = useTheme();
|
||||
const javaToInstall = [];
|
||||
useEffect(() => {
|
||||
if (javaToInstall.length > 0) {
|
||||
const instanceManagerModalIndex = modals.findIndex(
|
||||
x => x.modalType === 'JavaSetup'
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_MODAL,
|
||||
modals: [
|
||||
...modals.slice(0, instanceManagerModalIndex),
|
||||
{
|
||||
modalType: 'JavaSetup',
|
||||
modalProps: { preventClose: true }
|
||||
},
|
||||
...modals.slice(instanceManagerModalIndex + 1)
|
||||
]
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!isJava8Downloaded) javaToInstall.push(8);
|
||||
|
||||
if (!isJavaLatestDownloaded) javaToInstall.push(LATEST_JAVA_VERSION);
|
||||
|
||||
const installJava = async () => {
|
||||
const javaOs = convertOSToJavaFormat(process.platform);
|
||||
const javaArch = convertArchToJavaFormat(process.arch);
|
||||
const java8Meta = javaManifest.find(
|
||||
v =>
|
||||
v.os === javaOs &&
|
||||
v.architecture === javaArch &&
|
||||
(v.binary_type === 'jre' || v.binary_type === 'jdk')
|
||||
);
|
||||
const javaLatestMeta = javaLatestManifest.find(
|
||||
v =>
|
||||
v.os === javaOs &&
|
||||
v.architecture === javaArch &&
|
||||
(v.binary_type === 'jre' || v.binary_type === 'jdk')
|
||||
);
|
||||
|
||||
const totalExtractionSteps = process.platform !== 'win32' ? 2 : 1;
|
||||
const totalSteps = (totalExtractionSteps + 1) * javaToInstall.length;
|
||||
|
||||
const setStepPercentage = (stepNumber, percentage) => {
|
||||
setCurrentStepPercentage(
|
||||
parseInt(percentage / totalSteps + (stepNumber * 100) / totalSteps, 10)
|
||||
);
|
||||
};
|
||||
|
||||
let index = 0;
|
||||
for (const javaVersion of javaToInstall) {
|
||||
const {
|
||||
version_data: { openjdk_version: version },
|
||||
binary_link: url
|
||||
} = javaVersion === 8 ? java8Meta : javaLatestMeta;
|
||||
const javaBaseFolder = path.join(userData, 'java');
|
||||
|
||||
await fse.remove(path.join(javaBaseFolder, version));
|
||||
const downloadLocation = path.join(tempFolder, path.basename(url));
|
||||
|
||||
setCurrentSubStep(`Java ${javaVersion} - Downloading`);
|
||||
await downloadFile(downloadLocation, url, p => {
|
||||
ipcRenderer.invoke('update-progress-bar', p);
|
||||
setDownloadPercentage(p);
|
||||
setStepPercentage(index, p);
|
||||
});
|
||||
|
||||
ipcRenderer.invoke('update-progress-bar', -1);
|
||||
index += 1;
|
||||
setDownloadPercentage(0);
|
||||
setStepPercentage(index, 0);
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
setCurrentSubStep(
|
||||
`Java ${javaVersion} - Extracting 1 / ${totalExtractionSteps}`
|
||||
);
|
||||
let { extractedParentDir } = await extractAll(
|
||||
downloadLocation,
|
||||
tempFolder,
|
||||
{
|
||||
$progress: true
|
||||
},
|
||||
{
|
||||
update: percent => {
|
||||
ipcRenderer.invoke('update-progress-bar', percent);
|
||||
setDownloadPercentage(percent);
|
||||
setStepPercentage(index, percent);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
index += 1;
|
||||
setDownloadPercentage(0);
|
||||
setStepPercentage(index, 0);
|
||||
|
||||
await fse.remove(downloadLocation);
|
||||
|
||||
// If NOT windows then tar.gz instead of zip, so we need to extract 2 times.
|
||||
if (process.platform !== 'win32') {
|
||||
ipcRenderer.invoke('update-progress-bar', -1);
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
setCurrentSubStep(
|
||||
`Java ${javaVersion} - Extracting 2 / ${totalExtractionSteps}`
|
||||
);
|
||||
|
||||
const tempTarName = path.join(
|
||||
tempFolder,
|
||||
path.basename(url).replace('.tar.gz', '.tar')
|
||||
);
|
||||
|
||||
({ extractedParentDir } = await extractAll(
|
||||
tempTarName,
|
||||
tempFolder,
|
||||
{
|
||||
$progress: true
|
||||
},
|
||||
{
|
||||
update: percent => {
|
||||
ipcRenderer.invoke('update-progress-bar', percent);
|
||||
setDownloadPercentage(percent);
|
||||
setStepPercentage(index, percent);
|
||||
}
|
||||
}
|
||||
));
|
||||
await fse.remove(tempTarName);
|
||||
index += 1;
|
||||
setDownloadPercentage(0);
|
||||
setStepPercentage(index, 0);
|
||||
}
|
||||
|
||||
const directoryToMove =
|
||||
process.platform === 'darwin'
|
||||
? path.join(tempFolder, extractedParentDir, 'Contents', 'Home')
|
||||
: path.join(tempFolder, extractedParentDir);
|
||||
await fse.move(directoryToMove, path.join(javaBaseFolder, version));
|
||||
|
||||
await fse.remove(path.join(tempFolder, extractedParentDir));
|
||||
|
||||
const ext = process.platform === 'win32' ? '.exe' : '';
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
const execPath = path.join(
|
||||
javaBaseFolder,
|
||||
version,
|
||||
'bin',
|
||||
`java${ext}`
|
||||
);
|
||||
|
||||
await promisify(exec)(`chmod +x "${execPath}"`);
|
||||
await promisify(exec)(`chmod 755 "${execPath}"`);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(updateJavaPath(null));
|
||||
dispatch(updateJavaLatestPath(null));
|
||||
setCurrentSubStep(`Java is ready!`);
|
||||
ipcRenderer.invoke('update-progress-bar', -1);
|
||||
setDownloadPercentage(100);
|
||||
setCurrentStepPercentage(100);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
if (!javaLatestLog || !java8Log) dispatch(closeModal());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
installJava();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
css={`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
{javaToInstall.length > 0 ? (
|
||||
<>
|
||||
<div
|
||||
css={`
|
||||
margin-top: -15px; //cheaty way to get up to the Modal title :P
|
||||
margin-bottom: 50px;
|
||||
width: 50%;
|
||||
`}
|
||||
>
|
||||
<Progress
|
||||
percent={currentStepPercentage}
|
||||
strokeColor={theme.palette.primary.main}
|
||||
status="normal"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
margin-bottom: 20px;
|
||||
font-size: 18px;
|
||||
`}
|
||||
>
|
||||
{currentSubStep}
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
padding: 0 10px;
|
||||
width: 100%;
|
||||
`}
|
||||
>
|
||||
{downloadPercentage ? (
|
||||
<Progress
|
||||
percent={downloadPercentage}
|
||||
strokeColor={theme.palette.primary.main}
|
||||
status="normal"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<h2>Java is already installed!</h2>
|
||||
<div
|
||||
css={`
|
||||
margin-bottom: 10px;
|
||||
`}
|
||||
>
|
||||
<h3>Java 8 details:</h3>
|
||||
<code>{java8Log}</code>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Java {LATEST_JAVA_VERSION} details:</h3>
|
||||
<code>{javaLatestLog}</code>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{javaLatestLog && java8Log && (
|
||||
<Button
|
||||
css={`
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
`}
|
||||
type="primary"
|
||||
onClick={() => dispatch(closeModal())}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(JavaSetup);
|
||||
|
||||
const FirstStep = styled.div`
|
||||
transition: 0.2s ease-in-out;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
transform: translateX(
|
||||
${({ state }) => (state === 'exiting' || state === 'exited' ? -100 : 0)}%
|
||||
);
|
||||
`;
|
||||
|
||||
const SecondStep = styled.div`
|
||||
transition: 0.2s ease-in-out;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
transform: translateX(
|
||||
${({ state }) => (state === 'entering' || state === 'entered' ? 0 : 101)}%
|
||||
);
|
||||
`;
|
||||
@@ -0,0 +1,219 @@
|
||||
import React, { memo, useMemo, useState } from 'react';
|
||||
import { Cascader } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faLongArrowAltRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import path from 'path';
|
||||
import { isEqual } from 'lodash';
|
||||
import Modal from '../components/Modal';
|
||||
import { addToQueue } from '../reducers/actions';
|
||||
import { _getInstance } from '../utils/selectors';
|
||||
import { closeAllModals } from '../reducers/modals/actions';
|
||||
import { FABRIC, VANILLA, FORGE, CURSEFORGE } from '../utils/constants';
|
||||
import { getFilteredVersions } from '../../app/desktop/utils';
|
||||
|
||||
const McVersionChanger = ({ instanceName, defaultValue }) => {
|
||||
const vanillaManifest = useSelector(state => state.app.vanillaManifest);
|
||||
const fabricManifest = useSelector(state => state.app.fabricManifest);
|
||||
const forgeManifest = useSelector(state => state.app.forgeManifest);
|
||||
const config = useSelector(state => _getInstance(state)(instanceName));
|
||||
const [selectedVersion, setSelectedVersion] = useState(null);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const filteredVers = useMemo(() => {
|
||||
return getFilteredVersions(vanillaManifest, forgeManifest, fabricManifest);
|
||||
}, [vanillaManifest, forgeManifest, fabricManifest]);
|
||||
|
||||
const patchedDefaultValue = useMemo(() => {
|
||||
const isFabric = defaultValue?.loaderType === FABRIC;
|
||||
const isForge = defaultValue?.loaderType === FORGE;
|
||||
|
||||
if (isForge)
|
||||
return [
|
||||
defaultValue?.loaderType,
|
||||
defaultValue?.mcVersion,
|
||||
defaultValue?.loaderVersion
|
||||
];
|
||||
|
||||
const type = filteredVers.find(v => v.value === defaultValue?.loaderType);
|
||||
for (const releaseType of type.children) {
|
||||
const match = releaseType.children.find(
|
||||
v => v.value === defaultValue?.mcVersion
|
||||
);
|
||||
if (match) {
|
||||
return [
|
||||
defaultValue?.loaderType,
|
||||
releaseType.value,
|
||||
...(isFabric
|
||||
? [defaultValue?.mcVersion, defaultValue?.loaderVersion]
|
||||
: [defaultValue?.mcVersion])
|
||||
];
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}, [defaultValue, instanceName, filteredVers]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Minecraft Version Changer"
|
||||
css={`
|
||||
height: 380px;
|
||||
width: 600px;
|
||||
`}
|
||||
removePadding
|
||||
>
|
||||
<Container>
|
||||
{selectedVersion &&
|
||||
selectedVersion[0] !== patchedDefaultValue[0] &&
|
||||
defaultValue?.source === CURSEFORGE && (
|
||||
<div
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.yellow};
|
||||
font-weight: 900;
|
||||
width: 400px;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
margin-top: -50px;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
font-size: 20px;
|
||||
margin-bottom: 10px;
|
||||
`}
|
||||
>
|
||||
DISCLAIMER
|
||||
</div>
|
||||
<div>
|
||||
Changing modloader (forge -> fabric...) will result in the
|
||||
loss of the modpack metadata. You won't be able to change
|
||||
the modpack version or recognize this instance as a modpack
|
||||
anymore.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Cascader
|
||||
options={filteredVers}
|
||||
defaultValue={patchedDefaultValue}
|
||||
onChange={setSelectedVersion}
|
||||
allowClear={false}
|
||||
placeholder="Select a version"
|
||||
size="large"
|
||||
css={`
|
||||
width: 400px;
|
||||
`}
|
||||
/>
|
||||
<div
|
||||
css={`
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
isVersionDifferent={
|
||||
selectedVersion && !isEqual(patchedDefaultValue, selectedVersion)
|
||||
}
|
||||
css={`
|
||||
width: 70px;
|
||||
height: 40px;
|
||||
transition: 0.1s ease-in-out;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
font-size: 40px;
|
||||
color: ${props =>
|
||||
props.isVersionDifferent
|
||||
? props.theme.palette.text.icon
|
||||
: props.theme.palette.text.disabled};
|
||||
${props => (props.isVersionDifferent ? 'cursor: pointer;' : '')}
|
||||
&:hover {
|
||||
background-color: ${props =>
|
||||
props.isVersionDifferent
|
||||
? props.theme.action.hover
|
||||
: 'transparent'};
|
||||
}
|
||||
`}
|
||||
onClick={async () => {
|
||||
if (
|
||||
!selectedVersion ||
|
||||
isEqual(patchedDefaultValue, selectedVersion)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const background = config?.background
|
||||
? `background${path.extname(config?.background)}`
|
||||
: null;
|
||||
|
||||
const isVanilla = selectedVersion[0] === VANILLA;
|
||||
const isFabric = selectedVersion[0] === FABRIC;
|
||||
const isForge = selectedVersion[0] === FORGE;
|
||||
|
||||
if (isVanilla) {
|
||||
dispatch(
|
||||
addToQueue(
|
||||
instanceName,
|
||||
{
|
||||
...defaultValue,
|
||||
loaderType: selectedVersion[0],
|
||||
mcVersion: selectedVersion[2]
|
||||
},
|
||||
null,
|
||||
background
|
||||
)
|
||||
);
|
||||
} else if (isForge) {
|
||||
dispatch(
|
||||
addToQueue(
|
||||
instanceName,
|
||||
{
|
||||
...defaultValue,
|
||||
loaderType: FORGE,
|
||||
mcVersion: selectedVersion[1],
|
||||
loaderVersion: selectedVersion[2]
|
||||
},
|
||||
null,
|
||||
background
|
||||
)
|
||||
);
|
||||
} else if (isFabric) {
|
||||
dispatch(
|
||||
addToQueue(
|
||||
instanceName,
|
||||
{
|
||||
...defaultValue,
|
||||
loaderType: FABRIC,
|
||||
mcVersion: selectedVersion[2],
|
||||
loaderVersion: selectedVersion[3]
|
||||
},
|
||||
null,
|
||||
background
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(closeAllModals());
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faLongArrowAltRight} />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(McVersionChanger);
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
`;
|
||||
@@ -0,0 +1,126 @@
|
||||
/* eslint-disable react/no-unescaped-entities */
|
||||
import React, { memo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ReactHtmlParser from 'react-html-parser';
|
||||
import { Select } from 'antd';
|
||||
import Modal from '../components/Modal';
|
||||
import { getAddonFileChangelog } from '../api';
|
||||
|
||||
let latest = {};
|
||||
const ModChangelog = ({ modpackId, files }) => {
|
||||
const [changelog, setChangelog] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState(null);
|
||||
|
||||
const loadChangelog = async id => {
|
||||
const myLatest = {};
|
||||
latest = myLatest;
|
||||
setLoading(true);
|
||||
let data;
|
||||
try {
|
||||
data = await getAddonFileChangelog(modpackId, id);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
if (latest === myLatest) {
|
||||
setChangelog(data);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStateText = () => {
|
||||
if (!selectedId) {
|
||||
return '';
|
||||
}
|
||||
if (loading) {
|
||||
return 'Loading';
|
||||
}
|
||||
if (!changelog) {
|
||||
return 'Missing changelog';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 500px;
|
||||
width: 650px;
|
||||
`}
|
||||
title="Changelog"
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<Select
|
||||
css={`
|
||||
width: 400px;
|
||||
margin: 10px;
|
||||
`}
|
||||
onChange={v => {
|
||||
setSelectedId(v);
|
||||
loadChangelog(v);
|
||||
}}
|
||||
placeholder="Select a version"
|
||||
virtual={false}
|
||||
>
|
||||
{(files || []).map(v => (
|
||||
<Select.Option title={v.displayName} key={v.id} value={v.id}>
|
||||
{v.displayName}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Changelog>
|
||||
{changelog && !loading && selectedId ? (
|
||||
<>
|
||||
<div
|
||||
css={`
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
`}
|
||||
>
|
||||
{(files || []).find(v => v.id === selectedId)?.displayName}
|
||||
</div>
|
||||
{ReactHtmlParser(changelog)}
|
||||
</>
|
||||
) : (
|
||||
<h2
|
||||
css={`
|
||||
text-align: center;
|
||||
`}
|
||||
>
|
||||
{getStateText()}
|
||||
</h2>
|
||||
)}
|
||||
</Changelog>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ModChangelog);
|
||||
|
||||
const Changelog = styled.div`
|
||||
perspective: 1px;
|
||||
transform-style: preserve-3d;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: ${props => props.theme.palette.grey[900]};
|
||||
word-break: break-all;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
font-size: 20px;
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,475 @@
|
||||
/* eslint-disable */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import ReactHtmlParser from 'react-html-parser';
|
||||
import path from 'path';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExternalLinkAlt, faInfo } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Button, Select } from 'antd';
|
||||
import Modal from '../components/Modal';
|
||||
import { transparentize } from 'polished';
|
||||
import { getAddonDescription, getAddonFiles, getAddon } from '../api';
|
||||
import CloseButton from '../components/CloseButton';
|
||||
import { closeModal, openModal } from '../reducers/modals/actions';
|
||||
import { installMod, updateInstanceConfig } from '../reducers/actions';
|
||||
import { remove } from 'fs-extra';
|
||||
import { _getInstancesPath, _getInstance } from '../utils/selectors';
|
||||
import { FABRIC, FORGE, CURSEFORGE_URL } from '../utils/constants';
|
||||
import { formatNumber, formatDate } from '../utils';
|
||||
import {
|
||||
filterFabricFilesByVersion,
|
||||
filterForgeFilesByVersion,
|
||||
getPatchedInstanceType
|
||||
} from '../../app/desktop/utils';
|
||||
|
||||
const ModOverview = ({
|
||||
projectID,
|
||||
fileID,
|
||||
gameVersions,
|
||||
instanceName,
|
||||
fileName
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const [description, setDescription] = useState(null);
|
||||
const [addon, setAddon] = useState(null);
|
||||
const [files, setFiles] = useState([]);
|
||||
const [selectedItem, setSelectedItem] = useState(fileID);
|
||||
const [installedData, setInstalledData] = useState({ fileID, fileName });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingFiles, setLoadingFiles] = useState(true);
|
||||
const instancesPath = useSelector(_getInstancesPath);
|
||||
const instance = useSelector(state => _getInstance(state)(instanceName));
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
setLoadingFiles(true);
|
||||
await Promise.all([
|
||||
getAddon(projectID).then(data => setAddon(data)),
|
||||
getAddonDescription(projectID).then(data => {
|
||||
// Replace the beginning of all relative URLs with the Curseforge URL
|
||||
const modifiedData = data.replace(
|
||||
/href="(?!http)/g,
|
||||
`href="${CURSEFORGE_URL}`
|
||||
);
|
||||
setDescription(modifiedData);
|
||||
}),
|
||||
getAddonFiles(projectID).then(async data => {
|
||||
const isFabric =
|
||||
getPatchedInstanceType(instance) === FABRIC && projectID !== 361988;
|
||||
const isForge =
|
||||
getPatchedInstanceType(instance) === FORGE || projectID === 361988;
|
||||
let filteredFiles = [];
|
||||
if (isFabric) {
|
||||
filteredFiles = filterFabricFilesByVersion(data, gameVersions);
|
||||
} else if (isForge) {
|
||||
filteredFiles = filterForgeFilesByVersion(data, gameVersions);
|
||||
}
|
||||
|
||||
setFiles(filteredFiles);
|
||||
setLoadingFiles(false);
|
||||
})
|
||||
]);
|
||||
};
|
||||
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const getPlaceholderText = () => {
|
||||
if (loadingFiles) {
|
||||
return 'Loading files';
|
||||
} else if (files.length === 0 && !loadingFiles) {
|
||||
return 'Mod not available';
|
||||
} else {
|
||||
return 'Select a version';
|
||||
}
|
||||
};
|
||||
|
||||
const getReleaseType = id => {
|
||||
switch (id) {
|
||||
case 1:
|
||||
return (
|
||||
<span
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.green};
|
||||
`}
|
||||
>
|
||||
[Stable]
|
||||
</span>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<span
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.yellow};
|
||||
`}
|
||||
>
|
||||
[Beta]
|
||||
</span>
|
||||
);
|
||||
case 3:
|
||||
default:
|
||||
return (
|
||||
<span
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.red};
|
||||
`}
|
||||
>
|
||||
[Alpha]
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = value => setSelectedItem(JSON.parse(value));
|
||||
const primaryImage = addon?.logo;
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 85%;
|
||||
width: 85%;
|
||||
max-width: 1500px;
|
||||
`}
|
||||
header={false}
|
||||
>
|
||||
<>
|
||||
<StyledCloseButton>
|
||||
<CloseButton onClick={() => dispatch(closeModal())} />
|
||||
</StyledCloseButton>
|
||||
<Container>
|
||||
<Parallax bg={primaryImage?.url}>
|
||||
<ParallaxContent>
|
||||
<ParallaxInnerContent>
|
||||
{addon?.name}
|
||||
<ParallaxContentInfos>
|
||||
<div>
|
||||
<label>Author: </label>
|
||||
{addon?.authors[0].name}
|
||||
</div>
|
||||
{addon?.downloadCount && (
|
||||
<div>
|
||||
<label>Downloads: </label>
|
||||
{formatNumber(addon?.downloadCount)}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label>Last Update: </label>{' '}
|
||||
{formatDate(addon?.dateModified)}
|
||||
</div>
|
||||
<div>
|
||||
<label>MC version: </label>
|
||||
{addon?.latestFilesIndexes[0]?.gameVersion}
|
||||
</div>
|
||||
</ParallaxContentInfos>
|
||||
<Button
|
||||
href={addon?.links?.websiteUrl}
|
||||
css={`
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`}
|
||||
type="primary"
|
||||
>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={loadingFiles}
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
openModal('ModChangelog', {
|
||||
modpackId: projectID,
|
||||
files
|
||||
})
|
||||
);
|
||||
}}
|
||||
css={`
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 60px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`}
|
||||
type="primary"
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfo} />
|
||||
</Button>
|
||||
</ParallaxInnerContent>
|
||||
</ParallaxContent>
|
||||
</Parallax>
|
||||
<Content>{ReactHtmlParser(description)}</Content>
|
||||
</Container>
|
||||
<Footer>
|
||||
{installedData.fileID &&
|
||||
files.length !== 0 &&
|
||||
!files.find(v => v.id === installedData.fileID) && (
|
||||
<div
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.yellow};
|
||||
font-weight: 700;
|
||||
`}
|
||||
>
|
||||
The installed version of this mod has been removed from
|
||||
CurseForge, so you will only be able to get it as part of legacy
|
||||
modpacks.
|
||||
</div>
|
||||
)}
|
||||
<StyledSelect
|
||||
placeholder={getPlaceholderText()}
|
||||
loading={loadingFiles}
|
||||
disabled={loadingFiles}
|
||||
value={
|
||||
files.length !== 0 &&
|
||||
files.find(v => v.id === installedData.fileID) &&
|
||||
selectedItem
|
||||
}
|
||||
onChange={handleChange}
|
||||
listItemHeight={50}
|
||||
listHeight={400}
|
||||
virtual={false}
|
||||
>
|
||||
{(files || []).map(file => (
|
||||
<Select.Option
|
||||
title={file.displayName}
|
||||
key={file.id}
|
||||
value={file.id}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
height: 50px;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
flex: 7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
{file.displayName}
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
flex: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
`}
|
||||
>
|
||||
<div>{gameVersions}</div>
|
||||
<div>{getReleaseType(file.releaseType)}</div>
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
flex: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
{new Date(file.fileDate).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</StyledSelect>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={
|
||||
(!selectedItem || installedData.fileID === selectedItem) && addon
|
||||
}
|
||||
loading={loading}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
if (installedData.fileID) {
|
||||
await dispatch(
|
||||
updateInstanceConfig(instanceName, prev => ({
|
||||
...prev,
|
||||
mods: prev.mods.filter(
|
||||
v => v.fileName !== installedData.fileName
|
||||
)
|
||||
}))
|
||||
);
|
||||
await remove(
|
||||
path.join(
|
||||
instancesPath,
|
||||
instanceName,
|
||||
'mods',
|
||||
installedData.fileName
|
||||
)
|
||||
);
|
||||
}
|
||||
const newFile = await dispatch(
|
||||
installMod(
|
||||
projectID,
|
||||
selectedItem,
|
||||
instanceName,
|
||||
gameVersions,
|
||||
!installedData.fileID,
|
||||
null,
|
||||
null,
|
||||
addon
|
||||
)
|
||||
);
|
||||
setInstalledData({ fileID: selectedItem, fileName: newFile });
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
{installedData.fileID ? 'Switch Version' : 'Download'}
|
||||
</Button>
|
||||
</Footer>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ModOverview);
|
||||
|
||||
const StyledSelect = styled(Select)`
|
||||
width: 650px;
|
||||
height: 50px;
|
||||
.ant-select-selection-placeholder {
|
||||
height: 50px !important;
|
||||
line-height: 50px !important;
|
||||
}
|
||||
.ant-select-selector {
|
||||
height: 50px !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
.ant-select-selection-item {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
& > div {
|
||||
& > div:nth-child(2) {
|
||||
& > div:last-child {
|
||||
height: 10px;
|
||||
line-height: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledCloseButton = styled.div`
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
perspective: 1px;
|
||||
transform-style: preserve-3d;
|
||||
height: calc(100% - 70px);
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
`;
|
||||
|
||||
const Parallax = styled.div`
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transform: translateZ(-1px) scale(2);
|
||||
z-index: -1;
|
||||
background: url('${props => props.bg}');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
`;
|
||||
|
||||
const ParallaxInnerContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ParallaxContent = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: 60px;
|
||||
color: ${props => props.theme.palette.text.secondary};
|
||||
font-weight: 700;
|
||||
padding: 0 30px;
|
||||
text-align: center;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
`;
|
||||
|
||||
const ParallaxContentInfos = styled.div`
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
div {
|
||||
margin: 0 5px;
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
min-height: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
padding: 30px 30px 90px 30px;
|
||||
font-size: 18px;
|
||||
position: relative;
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
pre {
|
||||
background: ${props => transparentize(0.2, props.theme.palette.grey[900])};
|
||||
}
|
||||
z-index: 1;
|
||||
backdrop-filter: blur(20px);
|
||||
background: ${props => transparentize(0.4, props.theme.palette.grey[900])};
|
||||
`;
|
||||
|
||||
const Footer = styled.div`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 70px;
|
||||
width: 100%;
|
||||
background: ${props => props.theme.palette.grey[700]};
|
||||
&& > * {
|
||||
margin: 0 10px;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,416 @@
|
||||
/* eslint-disable */
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import ReactHtmlParser from 'react-html-parser';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { shell } from 'electron';
|
||||
import { faExternalLinkAlt, faInfo } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Checkbox, TextField, Cascader, Button, Input, Select } from 'antd';
|
||||
import Modal from '../components/Modal';
|
||||
import { transparentize } from 'polished';
|
||||
import { getAddonDescription, getAddonFiles } from '../api';
|
||||
import CloseButton from '../components/CloseButton';
|
||||
import { closeModal, openModal } from '../reducers/modals/actions';
|
||||
import { FORGE, CURSEFORGE_URL } from '../utils/constants';
|
||||
import { formatNumber, formatDate } from '../utils';
|
||||
|
||||
const ModpackDescription = ({
|
||||
modpack,
|
||||
setStep,
|
||||
setModpack,
|
||||
setVersion,
|
||||
type
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const [description, setDescription] = useState('');
|
||||
const [files, setFiles] = useState(null);
|
||||
const [selectedId, setSelectedId] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
setLoading(true);
|
||||
if (type === 'curseforge') {
|
||||
await Promise.all([
|
||||
getAddonDescription(modpack.id).then(data => {
|
||||
// Replace the beginning of all relative URLs with the Curseforge URL
|
||||
const modifiedData = data.replace(
|
||||
/href="(?!http)/g,
|
||||
`href="${CURSEFORGE_URL}`
|
||||
);
|
||||
|
||||
setDescription(modifiedData);
|
||||
}),
|
||||
getAddonFiles(modpack.id).then(async data => {
|
||||
setFiles(data);
|
||||
setLoading(false);
|
||||
})
|
||||
]);
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const handleChange = value => setSelectedId(value);
|
||||
|
||||
const getReleaseType = id => {
|
||||
switch (id) {
|
||||
case 1:
|
||||
case 'Release':
|
||||
return (
|
||||
<span
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.green};
|
||||
`}
|
||||
>
|
||||
[Stable]
|
||||
</span>
|
||||
);
|
||||
case 2:
|
||||
case 'Beta':
|
||||
return (
|
||||
<span
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.yellow};
|
||||
`}
|
||||
>
|
||||
[Beta]
|
||||
</span>
|
||||
);
|
||||
case 3:
|
||||
case 'Alpha':
|
||||
default:
|
||||
return (
|
||||
<span
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.red};
|
||||
`}
|
||||
>
|
||||
[Alpha]
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const primaryImage = useMemo(() => {
|
||||
if (type === 'curseforge') {
|
||||
return modpack.logo.thumbnailUrl;
|
||||
}
|
||||
}, [modpack, type]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 85%;
|
||||
width: 85%;
|
||||
max-width: 1500px;
|
||||
`}
|
||||
header={false}
|
||||
>
|
||||
<>
|
||||
<StyledCloseButton>
|
||||
<CloseButton onClick={() => dispatch(closeModal())} />
|
||||
</StyledCloseButton>
|
||||
<Container>
|
||||
<Parallax bg={primaryImage}>
|
||||
<ParallaxContent>
|
||||
<ParallaxInnerContent>
|
||||
{modpack.name}
|
||||
<ParallaxContentInfos>
|
||||
<div>
|
||||
<label>Author: </label>
|
||||
{modpack.authors[0].name}
|
||||
</div>
|
||||
<div>
|
||||
<label>Downloads: </label>
|
||||
{formatNumber(modpack.downloadCount)}
|
||||
</div>
|
||||
<div>
|
||||
<label>Last Update: </label>
|
||||
{formatDate(modpack.dateModified)}
|
||||
</div>
|
||||
<div>
|
||||
<label>MC version: </label>
|
||||
{modpack.latestFilesIndexes[0].gameVersion}
|
||||
</div>
|
||||
</ParallaxContentInfos>
|
||||
<Button
|
||||
href={modpack.websiteUrl
|
||||
}
|
||||
css={`
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`}
|
||||
type="primary"
|
||||
>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
openModal('ModChangelog', {
|
||||
modpackId: modpack.id,
|
||||
modpackName: modpack.name,
|
||||
files,
|
||||
type
|
||||
})
|
||||
);
|
||||
}}
|
||||
css={`
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 60px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`}
|
||||
type="primary"
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfo} />
|
||||
</Button>
|
||||
</ParallaxInnerContent>
|
||||
</ParallaxContent>
|
||||
</Parallax>
|
||||
<Content>
|
||||
{ReactHtmlParser(description)}
|
||||
</Content>
|
||||
</Container>
|
||||
<Footer>
|
||||
<div
|
||||
css={`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`}
|
||||
>
|
||||
<StyledSelect
|
||||
placeholder={loading ? 'Loading Versions' : 'Select a version'}
|
||||
onChange={handleChange}
|
||||
listItemHeight={50}
|
||||
listHeight={400}
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
virtual={false}
|
||||
>
|
||||
{(files || []).map(file => (
|
||||
<Select.Option
|
||||
title={file.displayName}
|
||||
key={file.id}
|
||||
value={file.id}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
height: 50px;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
flex: 7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
{file.displayName}
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
flex: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
{file.gameVersions[0]}
|
||||
</div>
|
||||
<div>
|
||||
{getReleaseType(file.releaseType)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
flex: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
{new Date(file.fileDate).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</StyledSelect>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!selectedId}
|
||||
onClick={() => {
|
||||
const modpackFile = files.find(file => file.id === selectedId);
|
||||
setVersion({
|
||||
loaderType: FORGE,
|
||||
projectID: modpack.id,
|
||||
fileID: modpackFile.id,
|
||||
source: type
|
||||
});
|
||||
setModpack(modpack);
|
||||
setStep(1);
|
||||
dispatch(closeModal());
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</Footer>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ModpackDescription);
|
||||
|
||||
const StyledSelect = styled(Select)`
|
||||
width: 650px;
|
||||
height: 50px;
|
||||
.ant-select-selection-placeholder {
|
||||
height: 50px !important;
|
||||
line-height: 50px !important;
|
||||
}
|
||||
.ant-select-selector {
|
||||
height: 50px !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
.ant-select-selection-item {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
& > div {
|
||||
& > div:nth-child(2) {
|
||||
& > div:last-child {
|
||||
height: 10px;
|
||||
line-height: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledCloseButton = styled.div`
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
perspective: 1px;
|
||||
transform-style: preserve-3d;
|
||||
height: calc(100% - 70px);
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
`;
|
||||
|
||||
const Parallax = styled.div`
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transform: translateZ(-1px) scale(2);
|
||||
z-index: -1;
|
||||
background: url('${props => props.bg}');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
`;
|
||||
|
||||
const ParallaxInnerContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ParallaxContent = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: 60px;
|
||||
text-align: center;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
`;
|
||||
|
||||
const ParallaxContentInfos = styled.div`
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
div {
|
||||
margin: 0 5px;
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
min-height: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
padding: 30px 30px 90px 30px;
|
||||
font-size: 18px;
|
||||
position: relative;
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
z-index: 1;
|
||||
backdrop-filter: blur(20px);
|
||||
background: ${props => transparentize(0.4, props.theme.palette.grey[900])};
|
||||
`;
|
||||
|
||||
const Footer = styled.div`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 70px;
|
||||
width: 100%;
|
||||
background: ${props => props.theme.palette.grey[700]};
|
||||
&& > * {
|
||||
margin: 0 10px;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,618 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import React, {
|
||||
memo,
|
||||
useEffect,
|
||||
useState,
|
||||
forwardRef,
|
||||
useContext
|
||||
} from 'react';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import styled, { ThemeContext } from 'styled-components';
|
||||
import memoize from 'memoize-one';
|
||||
import InfiniteLoader from 'react-window-infinite-loader';
|
||||
import ContentLoader from 'react-content-loader';
|
||||
import { Input, Select, Button } from 'antd';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCheckCircle } from '@fortawesome/free-regular-svg-icons';
|
||||
import {
|
||||
faBomb,
|
||||
faExclamationCircle,
|
||||
faWrench,
|
||||
faDownload
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import Modal from '../components/Modal';
|
||||
import { getSearch, getAddonFiles } from '../api';
|
||||
import { openModal } from '../reducers/modals/actions';
|
||||
import { _getInstance } from '../utils/selectors';
|
||||
import { installMod } from '../reducers/actions';
|
||||
import { FABRIC, FORGE } from '../utils/constants';
|
||||
import {
|
||||
getFirstPreferredCandidate,
|
||||
filterFabricFilesByVersion,
|
||||
filterForgeFilesByVersion,
|
||||
getPatchedInstanceType
|
||||
} from '../../app/desktop/utils';
|
||||
|
||||
const RowContainer = styled.div`
|
||||
display: flex;
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: calc(100% - 30px) !important;
|
||||
border-radius: 4px;
|
||||
padding: 11px 21px;
|
||||
background: ${props => props.theme.palette.grey[800]};
|
||||
${props =>
|
||||
props.isInstalled &&
|
||||
`border: 2px solid ${props.theme.palette.colors.green};`}
|
||||
`;
|
||||
|
||||
const RowInnerContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
color: ${props => props.theme.palette.text.secondary};
|
||||
`;
|
||||
|
||||
const RowContainerImg = styled.div`
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border-radius: 5px;
|
||||
margin-right: 20px;
|
||||
`;
|
||||
|
||||
const ModInstalledIcon = styled(FontAwesomeIcon)`
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -10px;
|
||||
color: ${props => props.theme.palette.colors.green};
|
||||
font-size: 25px;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const ModsIconBg = styled.div`
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -10px;
|
||||
background: ${props => props.theme.palette.grey[800]};
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
z-index: 0;
|
||||
`;
|
||||
|
||||
const ModsListWrapper = ({
|
||||
// Are there more items to load?
|
||||
// (This information comes from the most recent API request.)
|
||||
hasNextPage,
|
||||
|
||||
// Are we currently loading a page of items?
|
||||
// (This may be an in-flight flag in your Redux store for example.)
|
||||
isNextPageLoading,
|
||||
|
||||
// Array of items loaded so far.
|
||||
items,
|
||||
|
||||
// Callback function responsible for loading the next page of items.
|
||||
loadNextPage,
|
||||
searchQuery,
|
||||
width,
|
||||
height,
|
||||
itemData
|
||||
}) => {
|
||||
// If there are more items to be loaded then add an extra row to hold a loading indicator.
|
||||
const itemCount = hasNextPage ? items.length + 3 : items.length;
|
||||
|
||||
// Only load 1 page of items at a time.
|
||||
// Pass an empty callback to InfiniteLoader in case it asks us to load more than once.
|
||||
|
||||
// const loadMoreItems = loadNextPage;
|
||||
const loadMoreItems = isNextPageLoading ? () => {} : loadNextPage;
|
||||
|
||||
// Every row is loaded except for our loading indicator row.
|
||||
const isItemLoaded = index => !hasNextPage || index < items.length;
|
||||
|
||||
const innerElementType = forwardRef(({ style, ...rest }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
// eslint-disable-next-line react/forbid-dom-props
|
||||
style={{
|
||||
...style,
|
||||
paddingTop: 8
|
||||
}}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...rest}
|
||||
/>
|
||||
));
|
||||
|
||||
const Row = memo(({ index, style, data }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const curseReleaseChannel = useSelector(
|
||||
state => state.settings.curseReleaseChannel
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
const { instanceName, gameVersions, installedMods, instance } = data;
|
||||
|
||||
const item = items[index];
|
||||
|
||||
const isInstalled = installedMods.find(v => v.projectID === item?.id);
|
||||
const primaryImage = item?.logo;
|
||||
|
||||
if (!item) {
|
||||
return (
|
||||
<ModsLoader
|
||||
hasNextPage={hasNextPage}
|
||||
isNextPageLoading={isNextPageLoading}
|
||||
width={width - 10}
|
||||
loadNextPage={loadNextPage}
|
||||
top={style.top + 15}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RowContainer
|
||||
isInstalled={isInstalled}
|
||||
style={{
|
||||
...style,
|
||||
top: style.top + 15,
|
||||
height: style.height - 15,
|
||||
position: 'absolute',
|
||||
margin: '15px 10px',
|
||||
transition: 'height 0.2s ease-in-out'
|
||||
}}
|
||||
>
|
||||
{isInstalled && <ModInstalledIcon icon={faCheckCircle} />}
|
||||
{isInstalled && <ModsIconBg />}
|
||||
|
||||
<RowInnerContainer>
|
||||
<RowContainerImg
|
||||
style={{
|
||||
backgroundImage: `url('${primaryImage?.thumbnailUrl}')`
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
css={`
|
||||
color: ${props => props.theme.palette.text.third};
|
||||
&:hover {
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
}
|
||||
transition: color 0.1s ease-in-out;
|
||||
cursor: pointer;
|
||||
`}
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
openModal('ModOverview', {
|
||||
gameVersions,
|
||||
projectID: item.id,
|
||||
...(isInstalled && { fileID: isInstalled.fileID }),
|
||||
...(isInstalled && { fileName: isInstalled.fileName }),
|
||||
instanceName
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
</RowInnerContainer>
|
||||
{!isInstalled ? (
|
||||
error || (
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={async e => {
|
||||
setLoading(true);
|
||||
e.stopPropagation();
|
||||
const files = await getAddonFiles(item?.id);
|
||||
|
||||
const isFabric = getPatchedInstanceType(instance) === FABRIC;
|
||||
const isForge = getPatchedInstanceType(instance) === FORGE;
|
||||
|
||||
let filteredFiles = [];
|
||||
|
||||
if (isFabric) {
|
||||
filteredFiles = filterFabricFilesByVersion(
|
||||
files,
|
||||
gameVersions
|
||||
);
|
||||
} else if (isForge) {
|
||||
filteredFiles = filterForgeFilesByVersion(
|
||||
files,
|
||||
gameVersions
|
||||
);
|
||||
}
|
||||
|
||||
const preferredFile = getFirstPreferredCandidate(
|
||||
filteredFiles,
|
||||
curseReleaseChannel
|
||||
);
|
||||
|
||||
if (preferredFile === null) {
|
||||
setLoading(false);
|
||||
setError('Mod Not Available');
|
||||
console.error(
|
||||
`Could not find any release candidate for addon: ${item?.id} / ${gameVersions}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let prev = 0;
|
||||
await dispatch(
|
||||
installMod(
|
||||
item?.id,
|
||||
preferredFile?.id,
|
||||
instanceName,
|
||||
gameVersions,
|
||||
true,
|
||||
p => {
|
||||
if (parseInt(p, 10) !== prev) {
|
||||
prev = parseInt(p, 10);
|
||||
ipcRenderer.invoke(
|
||||
'update-progress-bar',
|
||||
parseInt(p, 10) / 100
|
||||
);
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
item
|
||||
)
|
||||
);
|
||||
ipcRenderer.invoke('update-progress-bar', 0);
|
||||
setLoading(false);
|
||||
}}
|
||||
loading={loading}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
openModal('ModOverview', {
|
||||
gameVersions,
|
||||
projectID: item.id,
|
||||
...(isInstalled && { fileID: isInstalled.fileID }),
|
||||
...(isInstalled && { fileName: isInstalled.fileName }),
|
||||
instanceName
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faWrench} />
|
||||
</Button>
|
||||
)}
|
||||
</RowContainer>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<InfiniteLoader
|
||||
isItemLoaded={isItemLoaded}
|
||||
itemCount={itemCount !== 0 ? itemCount : 40}
|
||||
loadMoreItems={() => loadMoreItems(searchQuery)}
|
||||
threshold={20}
|
||||
>
|
||||
{({ onItemsRendered, ref }) => (
|
||||
<List
|
||||
ref={ref}
|
||||
height={height}
|
||||
width={width}
|
||||
isNextPageLoading={isNextPageLoading}
|
||||
items={items}
|
||||
itemData={itemData}
|
||||
itemCount={items.length}
|
||||
itemSize={80}
|
||||
useIsScrolling
|
||||
onItemsRendered={onItemsRendered}
|
||||
innerElementType={innerElementType}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
)}
|
||||
</InfiniteLoader>
|
||||
);
|
||||
};
|
||||
|
||||
const createItemData = memoize(
|
||||
(
|
||||
items,
|
||||
instanceName,
|
||||
gameVersions,
|
||||
installedMods,
|
||||
instance,
|
||||
isNextPageLoading
|
||||
) => ({
|
||||
items,
|
||||
instanceName,
|
||||
gameVersions,
|
||||
installedMods,
|
||||
instance,
|
||||
isNextPageLoading
|
||||
})
|
||||
);
|
||||
|
||||
let lastRequest;
|
||||
const ModsBrowser = ({ instanceName, gameVersions }) => {
|
||||
const itemsNumber = 50;
|
||||
|
||||
const [mods, setMods] = useState([]);
|
||||
const [areModsLoading, setAreModsLoading] = useState(true);
|
||||
const [filterType, setFilterType] = useState('Featured');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [hasNextPage, setHasNextPage] = useState(false);
|
||||
const [categoryId, setCategoryId] = useState(null);
|
||||
const [error, setError] = useState(false);
|
||||
const instance = useSelector(state => _getInstance(state)(instanceName));
|
||||
const categories = useSelector(state => state.app.curseforgeCategories);
|
||||
|
||||
const installedMods = instance?.mods;
|
||||
|
||||
const loadMoreModsDebounced = useDebouncedCallback(
|
||||
(s, reset) => {
|
||||
loadMoreMods(s, reset);
|
||||
},
|
||||
500,
|
||||
{ leading: false, trailing: true }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadMoreMods(searchQuery, true);
|
||||
}, [filterType, categoryId]);
|
||||
|
||||
useEffect(() => {
|
||||
loadMoreMods();
|
||||
}, []);
|
||||
|
||||
const loadMoreMods = async (searchP = '', reset) => {
|
||||
const reqObj = {};
|
||||
lastRequest = reqObj;
|
||||
if (!areModsLoading) {
|
||||
setAreModsLoading(true);
|
||||
}
|
||||
|
||||
const isReset = reset !== undefined ? reset : false;
|
||||
let data = null;
|
||||
try {
|
||||
if (error) {
|
||||
setError(false);
|
||||
}
|
||||
data = await getSearch(
|
||||
'mods',
|
||||
searchP,
|
||||
itemsNumber,
|
||||
isReset ? 0 : mods.length,
|
||||
filterType,
|
||||
filterType !== 'Author' && filterType !== 'Name',
|
||||
gameVersions,
|
||||
categoryId,
|
||||
getPatchedInstanceType(instance)
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
|
||||
const newMods = reset ? data : mods.concat(data);
|
||||
if (lastRequest === reqObj) {
|
||||
setAreModsLoading(false);
|
||||
setMods(newMods || []);
|
||||
setHasNextPage((newMods || []).length % itemsNumber === 0);
|
||||
}
|
||||
};
|
||||
|
||||
const itemData = createItemData(
|
||||
mods,
|
||||
instanceName,
|
||||
gameVersions,
|
||||
installedMods,
|
||||
instance,
|
||||
areModsLoading
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 85%;
|
||||
width: 90%;
|
||||
max-width: 1500px;
|
||||
`}
|
||||
title="Instance Manager"
|
||||
>
|
||||
<Container>
|
||||
<Header>
|
||||
<Select
|
||||
css={`
|
||||
width: 160px !important;
|
||||
margin: 0 10px !important;
|
||||
`}
|
||||
defaultValue={filterType}
|
||||
onChange={setFilterType}
|
||||
disabled={areModsLoading}
|
||||
virtual={false}
|
||||
>
|
||||
<Select.Option value="Featured">Featured</Select.Option>
|
||||
<Select.Option value="Popularity">Popularity</Select.Option>
|
||||
<Select.Option value="LastUpdated">Last Updated</Select.Option>
|
||||
<Select.Option value="Name">Name</Select.Option>
|
||||
<Select.Option value="Author">Author</Select.Option>
|
||||
<Select.Option value="TotalDownloads">Downloads</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="Minecraft Category"
|
||||
onChange={setCategoryId}
|
||||
defaultValue={null}
|
||||
virtual={false}
|
||||
css={`
|
||||
width: 500px !important;
|
||||
margin-right: 10px !important;
|
||||
`}
|
||||
>
|
||||
<Select.Option key="allcategories" value={null}>
|
||||
All Categories
|
||||
</Select.Option>
|
||||
{(categories || [])
|
||||
.filter(v => v?.classId === 6)
|
||||
.sort((a, b) => a?.name.localeCompare(b?.name))
|
||||
.map(v => (
|
||||
<Select.Option value={v?.id} key={v?.id}>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<img
|
||||
src={v?.iconUrl}
|
||||
css={`
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-right: 10px;
|
||||
`}
|
||||
alt="icon"
|
||||
/>
|
||||
{v?.name}
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
css={`
|
||||
height: 32px !important;
|
||||
`}
|
||||
placeholder="Search..."
|
||||
value={searchQuery}
|
||||
onChange={e => {
|
||||
setSearchQuery(e.target.value);
|
||||
loadMoreModsDebounced(e.target.value, true);
|
||||
}}
|
||||
allowClear
|
||||
/>
|
||||
</Header>
|
||||
|
||||
{!error ? (
|
||||
!areModsLoading && mods.length === 0 ? (
|
||||
<div
|
||||
css={`
|
||||
margin-top: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 150px;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faExclamationCircle} />
|
||||
<div
|
||||
css={`
|
||||
font-size: 20px;
|
||||
margin-top: 70px;
|
||||
`}
|
||||
>
|
||||
No mods has been found with the current filters.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<ModsListWrapper
|
||||
hasNextPage={hasNextPage}
|
||||
isNextPageLoading={areModsLoading}
|
||||
items={mods}
|
||||
width={width}
|
||||
height={height - 50}
|
||||
loadNextPage={loadMoreMods}
|
||||
searchQuery={searchQuery}
|
||||
version={gameVersions}
|
||||
installedMods={installedMods}
|
||||
instanceName={instanceName}
|
||||
itemData={itemData}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
css={`
|
||||
margin-top: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 150px;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBomb} />
|
||||
<div
|
||||
css={`
|
||||
font-size: 20px;
|
||||
margin-top: 70px;
|
||||
`}
|
||||
>
|
||||
An error occurred while loading the mods list...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ModsBrowser);
|
||||
|
||||
const ModsLoader = memo(
|
||||
({ width, top, isNextPageLoading, hasNextPage, loadNextPage }) => {
|
||||
const ContextTheme = useContext(ThemeContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasNextPage && isNextPageLoading) {
|
||||
loadNextPage();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ContentLoader
|
||||
style={{
|
||||
width: width - 10,
|
||||
height: '62px',
|
||||
paddingTop: 8,
|
||||
position: 'absolute',
|
||||
top
|
||||
}}
|
||||
speed={2}
|
||||
foregroundColor={ContextTheme.palette.grey[900]}
|
||||
backgroundColor={ContextTheme.palette.grey[800]}
|
||||
title={false}
|
||||
>
|
||||
<rect x="0" y="0" width="100%" height="65px" />
|
||||
</ContentLoader>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const Container = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
`;
|
||||
@@ -0,0 +1,111 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Progress } from 'antd';
|
||||
import path from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import Modal from '../components/Modal';
|
||||
import { updateMod } from '../reducers/actions';
|
||||
import { closeModal } from '../reducers/modals/actions';
|
||||
import {
|
||||
_getInstance,
|
||||
_getInstancesPath,
|
||||
_getTempPath
|
||||
} from '../utils/selectors';
|
||||
import { makeModRestorePoint } from '../utils';
|
||||
|
||||
const ModsUpdater = ({ instanceName }) => {
|
||||
const dispatch = useDispatch();
|
||||
const latestMods = useSelector(state => state.latestModManifests);
|
||||
const instance = useSelector(state => _getInstance(state)(instanceName));
|
||||
const curseReleaseChannel = useSelector(
|
||||
state => state.settings.curseReleaseChannel
|
||||
);
|
||||
const [computedMods, setComputedMods] = useState(0);
|
||||
const [installProgress, setInstallProgress] = useState(null);
|
||||
|
||||
const filterAvailableUpdates = () => {
|
||||
return instance.mods.filter(mod => {
|
||||
return (
|
||||
latestMods[mod.projectID] &&
|
||||
latestMods[mod.projectID].id !== mod.fileID &&
|
||||
latestMods[mod.projectID].releaseType <= curseReleaseChannel
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const totalMods = useMemo(() => filterAvailableUpdates(), []);
|
||||
|
||||
const tempPath = useSelector(_getTempPath);
|
||||
const instancesPath = useSelector(_getInstancesPath);
|
||||
const instancePath = path.join(instancesPath, instanceName);
|
||||
const modsPath = path.join(instancePath, 'mods');
|
||||
|
||||
useEffect(() => {
|
||||
let cancel = false;
|
||||
const updateMods = async () => {
|
||||
let i = 0;
|
||||
while (!cancel && i < totalMods.length) {
|
||||
const mod = totalMods[i];
|
||||
const restoreModPath = path.join(tempPath, `${mod.fileName}__RESTORE`);
|
||||
await makeModRestorePoint(restoreModPath, modsPath, mod.fileName);
|
||||
|
||||
await dispatch(
|
||||
updateMod(
|
||||
instanceName,
|
||||
mod,
|
||||
latestMods[mod.projectID].id,
|
||||
instance.loader?.mcVersion,
|
||||
// eslint-disable-next-line
|
||||
p => {
|
||||
if (!cancel) setInstallProgress(p);
|
||||
}
|
||||
)
|
||||
);
|
||||
if (!cancel) {
|
||||
await fse.remove(restoreModPath);
|
||||
setComputedMods(p => p + 1);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if (!cancel) {
|
||||
dispatch(closeModal());
|
||||
}
|
||||
};
|
||||
|
||||
updateMods();
|
||||
return () => {
|
||||
cancel = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 160px;
|
||||
width: 350px;
|
||||
`}
|
||||
title="Mods Updater"
|
||||
>
|
||||
<Container>
|
||||
Updating mod {computedMods} / {totalMods.length}
|
||||
{installProgress !== null && (
|
||||
<Progress percent={parseInt(installProgress, 10)} />
|
||||
)}
|
||||
</Container>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModsUpdater;
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: space-between;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
`;
|
||||
@@ -0,0 +1,100 @@
|
||||
// import React from "react";
|
||||
// import styled from "styled-components";
|
||||
// import { Spin, message } from "antd";
|
||||
// import { useSelector, useDispatch } from "react-redux";
|
||||
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
// import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
// import Modal from "../../components/Modal";
|
||||
// import { _getAccounts, _getCurrentAccount } from "../../utils/selectors";
|
||||
// import { openModal, closeModal } from "../../reducers/modals/actions";
|
||||
// import {
|
||||
// updateCurrentAccountId,
|
||||
// loginWithAccessToken,
|
||||
// updateAccount,
|
||||
// removeAccount
|
||||
// } from "../../reducers/actions";
|
||||
// import { load } from "../../reducers/loading/actions";
|
||||
// import features from "../../reducers/loading/features";
|
||||
|
||||
// const Onboarding = () => {
|
||||
// const dispatch = useDispatch();
|
||||
// const accounts = useSelector(_getAccounts);
|
||||
// const currentAccount = useSelector(_getCurrentAccount);
|
||||
// const isLoading = useSelector(state => state.loading.accountAuthentication);
|
||||
// return (
|
||||
// <Modal
|
||||
// css={`
|
||||
// height: 70%;
|
||||
// width: 400px;
|
||||
// max-height: 700px;
|
||||
// `}
|
||||
// >
|
||||
// <Container>Hello</Container>
|
||||
// </Modal>
|
||||
// );
|
||||
// };
|
||||
|
||||
// export default Onboarding;
|
||||
|
||||
// const Container = styled.div`
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// align-content: space-between;
|
||||
// `;
|
||||
|
||||
// const AccountItem = styled.div`
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// position: relative;
|
||||
// flex: 1;
|
||||
// justify-content: space-between;
|
||||
// height: 40px;
|
||||
// padding: 0 10px;
|
||||
// color: white;
|
||||
// border-radius: 4px;
|
||||
// cursor: pointer;
|
||||
// ${props =>
|
||||
// props.active ? `background: ${props.theme.palette.primary.main};` : ""}
|
||||
// transition: background 0.1s ease-in-out;
|
||||
// &:hover {
|
||||
// ${props =>
|
||||
// props.active ? "" : `background: ${props.theme.palette.grey[500]};`}
|
||||
// }
|
||||
// `;
|
||||
|
||||
// const HoverContainer = styled.div`
|
||||
// position: absolute;
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// justify-content: center;
|
||||
// left: 0;
|
||||
// align-items: center;
|
||||
// cursor: pointer;
|
||||
// font-size: 18px;
|
||||
// font-weight: 800;
|
||||
// border-radius: 4px;
|
||||
// transition: opacity 150ms ease-in-out;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// opacity: 0;
|
||||
// backdrop-filter: blur(4px);
|
||||
// will-change: opacity;
|
||||
// &:hover {
|
||||
// opacity: 1;
|
||||
// }
|
||||
// `;
|
||||
|
||||
// const AccountsContainer = styled.div`
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// `;
|
||||
|
||||
// const AccountContainer = styled.div`
|
||||
// display: flex;
|
||||
// position: relative;
|
||||
// width: 100%;
|
||||
// justify-content: space-between;
|
||||
// align-items: center;
|
||||
// `;
|
||||
@@ -0,0 +1,356 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Button, Spin } from 'antd';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faExclamationTriangle,
|
||||
faFileDownload
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import styled from 'styled-components';
|
||||
import Modal from '../components/Modal';
|
||||
import { UPDATE_MODAL } from '../reducers/modals/actionTypes';
|
||||
import { closeModal } from '../reducers/modals/actions';
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-conter: space-between;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
`;
|
||||
|
||||
const ModsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 250px;
|
||||
`;
|
||||
|
||||
const RowContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
padding: 20px 10px;
|
||||
background: ${props => props.theme.palette.grey[800]};
|
||||
|
||||
&:hover {
|
||||
.rowCenterContent {
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
}
|
||||
}
|
||||
|
||||
.dot {
|
||||
border-radius: 50%;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
background: ${props => props.theme.palette.colors.green};
|
||||
}
|
||||
`;
|
||||
|
||||
const ModRow = ({
|
||||
mod,
|
||||
loadedMods,
|
||||
currentMod,
|
||||
missingMods,
|
||||
cloudflareBlock,
|
||||
downloadUrl
|
||||
}) => {
|
||||
const { modManifest, addon } = mod;
|
||||
const loaded = loadedMods.includes(modManifest.id);
|
||||
const missing = missingMods.includes(modManifest.id);
|
||||
const ref = useRef();
|
||||
|
||||
const isCurrentMod = currentMod?.modManifest?.id === modManifest.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (!loaded && isCurrentMod) {
|
||||
ref.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'end',
|
||||
inline: 'nearest'
|
||||
});
|
||||
}
|
||||
}, [isCurrentMod, loaded]);
|
||||
|
||||
return (
|
||||
<RowContainer ref={ref}>
|
||||
<div>{`${addon?.name} - ${modManifest?.displayName}`}</div>
|
||||
{loaded && !missing && !cloudflareBlock && <div className="dot" />}
|
||||
{loaded && missing && !cloudflareBlock && (
|
||||
<FontAwesomeIcon
|
||||
icon={faExclamationTriangle}
|
||||
css={`
|
||||
color: ${props => props.theme.palette.colors.yellow};
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
{loaded && !missing && cloudflareBlock && (
|
||||
<Button href={downloadUrl}>
|
||||
<FontAwesomeIcon icon={faFileDownload} />
|
||||
</Button>
|
||||
)}
|
||||
{!loaded && isCurrentMod && (
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} />
|
||||
)}
|
||||
</RowContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const OptedOutModsList = ({
|
||||
optedOutMods,
|
||||
instancePath,
|
||||
resolve,
|
||||
reject,
|
||||
preventClose
|
||||
}) => {
|
||||
const [loadedMods, setLoadedMods] = useState([]);
|
||||
const [missingMods, setMissingMods] = useState([]);
|
||||
const [cloudflareBlock, setCloudflareBlock] = useState(false);
|
||||
const [manualDownloadUrls, setManualDownloadUrls] = useState([]);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const modals = useSelector(state => state.modals);
|
||||
|
||||
const optedOutModalIndex = modals.findIndex(
|
||||
x => x.modalType === 'OptedOutModsList'
|
||||
);
|
||||
|
||||
const currentMod = downloading ? optedOutMods[loadedMods.length] : null;
|
||||
|
||||
useEffect(() => {
|
||||
const listener = () => {
|
||||
dispatch(closeModal());
|
||||
setTimeout(() => {
|
||||
reject('Download window closed unexpectedly');
|
||||
}, 300);
|
||||
};
|
||||
|
||||
ipcRenderer.once('opted-out-window-closed-unexpected', listener);
|
||||
|
||||
return () => {
|
||||
ipcRenderer.removeListener(
|
||||
'opted-out-window-closed-unexpected',
|
||||
listener
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (e, status) => {
|
||||
if (!status.error) {
|
||||
if (optedOutMods.length === loadedMods.length + 1) {
|
||||
if (missingMods.length === 0 && !cloudflareBlock) {
|
||||
resolve();
|
||||
dispatch(closeModal());
|
||||
}
|
||||
setDownloading(false);
|
||||
}
|
||||
setLoadedMods(prev => [...prev, status.modId]);
|
||||
if (status.warning) {
|
||||
if (!status.cloudflareBlock) {
|
||||
setMissingMods(prev => [...prev, status.modId]);
|
||||
} else {
|
||||
setCloudflareBlock(true);
|
||||
setManualDownloadUrls(prev => [...prev, status.modId]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dispatch(closeModal());
|
||||
setTimeout(() => {
|
||||
reject(status.error);
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
ipcRenderer.once('opted-out-download-mod-status', listener);
|
||||
|
||||
return () => {
|
||||
ipcRenderer.removeListener(
|
||||
'opted-out-window-closed-unexpected',
|
||||
listener
|
||||
);
|
||||
};
|
||||
}, [loadedMods, missingMods, cloudflareBlock, manualDownloadUrls]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 400px;
|
||||
width: 800px;
|
||||
overflow-x: hidden;
|
||||
`}
|
||||
preventClose={preventClose}
|
||||
closeCallback={() => {
|
||||
setTimeout(
|
||||
() => reject(new Error('Download Aborted by the user')),
|
||||
300
|
||||
);
|
||||
}}
|
||||
title="Opted out mods list"
|
||||
>
|
||||
<Container>
|
||||
{!cloudflareBlock && (
|
||||
<div
|
||||
css={`
|
||||
text-align: left;
|
||||
margin-bottom: 2rem;
|
||||
`}
|
||||
>
|
||||
Hey oh! It looks like some developers opted out from showing their
|
||||
mods on third-party launchers. We can still attempt to download them
|
||||
automatically. Please click continue and wait for all downloads to
|
||||
finish. Please don't click anything inside the browser.
|
||||
</div>
|
||||
)}
|
||||
<ModsContainer>
|
||||
{optedOutMods &&
|
||||
optedOutMods.map(mod => {
|
||||
return (
|
||||
<ModRow
|
||||
mod={mod}
|
||||
loadedMods={loadedMods}
|
||||
currentMod={currentMod}
|
||||
missingMods={missingMods}
|
||||
cloudflareBlock={cloudflareBlock}
|
||||
downloadUrl={`${mod.addon.links.websiteUrl}/download/${mod.modManifest.id}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ModsContainer>
|
||||
{cloudflareBlock && (
|
||||
<p
|
||||
css={`
|
||||
margin: 20px auto 0 auto;
|
||||
`}
|
||||
>
|
||||
Cloudflare is currently blocking automated downloads. You can
|
||||
manually download the mods and place them in the mods folder to
|
||||
continue. Use the download buttons in the rows above, and the button
|
||||
below to open the instance folder.
|
||||
</p>
|
||||
)}
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
`}
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
type="text"
|
||||
disabled={
|
||||
(missingMods.length > 0 && !cloudflareBlock) || downloading
|
||||
}
|
||||
onClick={() => {
|
||||
dispatch(closeModal());
|
||||
setTimeout(
|
||||
() => reject(new Error('Download Aborted by the user')),
|
||||
300
|
||||
);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{missingMods.length === 0 && !cloudflareBlock && (
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={downloading}
|
||||
onClick={() => {
|
||||
setDownloading(true);
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_MODAL,
|
||||
modals: [
|
||||
...modals.slice(0, optedOutModalIndex),
|
||||
{
|
||||
modalType: 'OptedOutModsList',
|
||||
modalProps: {
|
||||
...modals[optedOutModalIndex].modalProps,
|
||||
preventClose: true
|
||||
}
|
||||
},
|
||||
...modals.slice(optedOutModalIndex + 1)
|
||||
]
|
||||
});
|
||||
ipcRenderer.invoke('download-optedout-mods', {
|
||||
mods: optedOutMods,
|
||||
instancePath
|
||||
});
|
||||
setDownloading(false);
|
||||
}}
|
||||
css={`
|
||||
background-color: ${props => props.theme.palette.colors.green};
|
||||
`}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
)}
|
||||
{missingMods.length > 0 && !cloudflareBlock && (
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={downloading}
|
||||
onClick={() => {
|
||||
resolve();
|
||||
dispatch(closeModal());
|
||||
}}
|
||||
css={`
|
||||
background-color: ${props => props.theme.palette.colors.green};
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
{cloudflareBlock && (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={downloading}
|
||||
onClick={() => {
|
||||
ipcRenderer.invoke('openFolder', instancePath);
|
||||
}}
|
||||
css={`
|
||||
background-color: ${props => props.theme.palette.colors.blue};
|
||||
`}
|
||||
>
|
||||
Open folder
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={downloading}
|
||||
onClick={() => {
|
||||
resolve();
|
||||
dispatch(closeModal());
|
||||
}}
|
||||
css={`
|
||||
background-color: ${props =>
|
||||
props.theme.palette.colors.green};
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default OptedOutModsList;
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { lazy, memo, useMemo } from 'react';
|
||||
|
||||
import Modal from '../components/Modal';
|
||||
import AsyncComponent from '../components/AsyncComponent';
|
||||
|
||||
const policies = {
|
||||
privacy: {
|
||||
component: AsyncComponent(
|
||||
lazy(() => import('../components/PrivacyPolicy'))
|
||||
),
|
||||
title: 'Privacy Policy'
|
||||
},
|
||||
tos: {
|
||||
component: AsyncComponent(
|
||||
lazy(() => import('../components/TermsAndConditions'))
|
||||
),
|
||||
title: 'Terms and Conditions'
|
||||
},
|
||||
acceptableuse: {
|
||||
component: AsyncComponent(
|
||||
lazy(() => import('../components/AcceptableUsePolicy'))
|
||||
),
|
||||
title: 'Acceptable Use Policy'
|
||||
}
|
||||
};
|
||||
|
||||
const PolicyModal = ({ policy }) => {
|
||||
const PolicyComponent = useMemo(() => policies[policy].component, [policy]);
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 550px;
|
||||
width: 900px;
|
||||
`}
|
||||
title="Policy"
|
||||
removePadding
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
|
||||
& > h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
margin: 40px 0;
|
||||
|
||||
& > h2 {
|
||||
flex: 1;
|
||||
font-weight: bold;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
& > div {
|
||||
p {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
flex: 2;
|
||||
color: ${props => props.theme.palette.text.third};
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
<PolicyComponent />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PolicyModal);
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import path from 'path';
|
||||
import styled from 'styled-components';
|
||||
import Modal from '../components/Modal';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const Img = styled.img`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export default function Screenshot({ screenshotsPath, file }) {
|
||||
const image = `file:///${path.join(screenshotsPath, file.name)}`;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
css={`
|
||||
height: 85%;
|
||||
width: 85%;
|
||||
max-width: 1500px;
|
||||
overflow: hidden;
|
||||
`}
|
||||
title="ScreenShot"
|
||||
>
|
||||
<Container>
|
||||
<Img src={image} />
|
||||
</Container>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,650 @@
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ipcRenderer, clipboard } from 'electron';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import path from 'path';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import fsa from 'fs-extra';
|
||||
import { promises as fs } from 'fs';
|
||||
import {
|
||||
faCopy,
|
||||
faDownload,
|
||||
faTachometerAlt,
|
||||
faTrash,
|
||||
faPlay,
|
||||
faToilet,
|
||||
faNewspaper,
|
||||
faFolder,
|
||||
faFire,
|
||||
faSort
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { Select, Tooltip, Button, Switch, Input, Checkbox } from 'antd';
|
||||
import { faDiscord } from '@fortawesome/free-brands-svg-icons';
|
||||
import {
|
||||
_getCurrentAccount,
|
||||
_getDataStorePath,
|
||||
_getInstancesPath,
|
||||
_getTempPath
|
||||
} from '../../../utils/selectors';
|
||||
import {
|
||||
updateDiscordRPC,
|
||||
updateHideWindowOnGameLaunch,
|
||||
updatePotatoPcMode,
|
||||
updateInstanceSortType,
|
||||
updateShowNews,
|
||||
updateCurseReleaseChannel
|
||||
} from '../../../reducers/settings/actions';
|
||||
import { updateConcurrentDownloads } from '../../../reducers/actions';
|
||||
import { openModal } from '../../../reducers/modals/actions';
|
||||
import HorizontalLogo from '../../../../ui/HorizontalLogo';
|
||||
import { extractFace } from '../../../../app/desktop/utils';
|
||||
|
||||
const Title = styled.div`
|
||||
margin-top: 30px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
z-index: 1;
|
||||
text-align: left;
|
||||
-webkit-backface-visibility: hidden;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
*:first-child {
|
||||
margin-right: 15px;
|
||||
}
|
||||
`;
|
||||
|
||||
const PersonalData = styled.div`
|
||||
margin-top: 38px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const MainTitle = styled.h1`
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
margin: 0 500px 20px 0;
|
||||
`;
|
||||
|
||||
const ProfileImage = styled.img`
|
||||
position: relative;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background: #212b36;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
`;
|
||||
|
||||
const Uuid = styled.div`
|
||||
font-size: smaller;
|
||||
font-weight: 200;
|
||||
color: ${props => props.theme.palette.grey[100]};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const Username = styled.div`
|
||||
font-size: smaller;
|
||||
font-weight: 200;
|
||||
color: ${props => props.theme.palette.grey[100]};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const PersonalDataContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
background: ${props => props.theme.palette.grey[900]};
|
||||
border-radius: ${props => props.theme.shape.borderRadius};
|
||||
`;
|
||||
|
||||
const LauncherVersion = styled.div`
|
||||
margin: 30px 0;
|
||||
p {
|
||||
text-align: left;
|
||||
color: ${props => props.theme.palette.text.third};
|
||||
margin: 0 0 0 6px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const CustomDataPathContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
border-radius: ${props => props.theme.shape.borderRadius};
|
||||
|
||||
h1 {
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: ${props => props.theme.palette.text.primary};
|
||||
z-index: 1;
|
||||
text-align: left;
|
||||
}
|
||||
`;
|
||||
|
||||
function copy(setCopied, copyText) {
|
||||
setCopied(true);
|
||||
clipboard.writeText(copyText);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function dashUuid(UUID) {
|
||||
// UUID is segmented into: 8 - 4 - 4 - 4 - 12
|
||||
// Then dashes are added between.
|
||||
|
||||
// eslint-disable-next-line
|
||||
return `${UUID.substring(0, 8)}-${UUID.substring(8, 12)}-${UUID.substring(
|
||||
12,
|
||||
16
|
||||
)}-${UUID.substring(16, 20)}-${UUID.substring(20, 32)}`;
|
||||
}
|
||||
|
||||
const General = () => {
|
||||
/* eslint-disable prettier/prettier */
|
||||
const tempPath = useSelector(_getTempPath);
|
||||
const dataStorePath = useSelector(_getDataStorePath);
|
||||
const instancesPath = useSelector(_getInstancesPath);
|
||||
const currentAccount = useSelector(_getCurrentAccount);
|
||||
const userData = useSelector(state => state.userData);
|
||||
const isPlaying = useSelector(state => state.startedInstances);
|
||||
const queuedInstances = useSelector(state => state.downloadQueue);
|
||||
const updateAvailable = useSelector(state => state.updateAvailable);
|
||||
const showNews = useSelector(state => state.settings.showNews);
|
||||
const DiscordRPC = useSelector(state => state.settings.discordRPC);
|
||||
const potatoPcMode = useSelector(state => state.settings.potatoPcMode);
|
||||
const concurrentDownloads = useSelector(
|
||||
state => state.settings.concurrentDownloads
|
||||
);
|
||||
const curseReleaseChannel = useSelector(
|
||||
state => state.settings.curseReleaseChannel
|
||||
);
|
||||
const hideWindowOnGameLaunch = useSelector(
|
||||
state => state.settings.hideWindowOnGameLaunch
|
||||
);
|
||||
const instanceSortMethod = useSelector(
|
||||
state => state.settings.instanceSortOrder
|
||||
);
|
||||
/* eslint-enable */
|
||||
|
||||
const [dataPath, setDataPath] = useState(userData);
|
||||
const [copiedUuid, setCopiedUuid] = useState(false);
|
||||
const [moveUserData, setMoveUserData] = useState(false);
|
||||
const [deletingInstances, setDeletingInstances] = useState(false);
|
||||
const [loadingMoveUserData, setLoadingMoveUserData] = useState(false);
|
||||
const [version, setVersion] = useState(null);
|
||||
const [profileImage, setProfileImage] = useState(null);
|
||||
const [releaseChannel, setReleaseChannel] = useState(null);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const disableInstancesActions =
|
||||
Object.keys(queuedInstances).length > 0 ||
|
||||
Object.keys(isPlaying).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
ipcRenderer.invoke('getAppVersion').then(setVersion).catch(console.error);
|
||||
extractFace(currentAccount.skin).then(setProfileImage).catch(console.error);
|
||||
ipcRenderer
|
||||
.invoke('getAppdataPath')
|
||||
.then(appData =>
|
||||
fsa
|
||||
.readFile(path.join(appData, 'gdlauncher_next', 'rChannel'))
|
||||
.then(v => setReleaseChannel(parseInt(v.toString(), 10)))
|
||||
.catch(() => setReleaseChannel(0))
|
||||
)
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const clearSharedData = async () => {
|
||||
setDeletingInstances(true);
|
||||
try {
|
||||
await fsa.emptyDir(dataStorePath);
|
||||
await fsa.emptyDir(instancesPath);
|
||||
await fsa.emptyDir(tempPath);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
setDeletingInstances(false);
|
||||
};
|
||||
|
||||
const changeDataPath = async () => {
|
||||
setLoadingMoveUserData(true);
|
||||
const appData = await ipcRenderer.invoke('getAppdataPath');
|
||||
const appDataPath = path.join(appData, 'gdlauncher_next');
|
||||
|
||||
const notCopiedFiles = [
|
||||
'Cache',
|
||||
'Code Cache',
|
||||
'Dictionaries',
|
||||
'GPUCache',
|
||||
'Cookies',
|
||||
'Cookies-journal'
|
||||
];
|
||||
await fsa.writeFile(path.join(appDataPath, 'override.data'), dataPath);
|
||||
|
||||
if (moveUserData) {
|
||||
try {
|
||||
const files = await fs.readdir(userData);
|
||||
await Promise.all(
|
||||
files.map(async name => {
|
||||
if (!notCopiedFiles.includes(name)) {
|
||||
await fsa.copy(
|
||||
path.join(userData, name),
|
||||
path.join(dataPath, name),
|
||||
{
|
||||
overwrite: true
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
setLoadingMoveUserData(false);
|
||||
await ipcRenderer.invoke('appRestart');
|
||||
};
|
||||
|
||||
const openFolder = async () => {
|
||||
const { filePaths, canceled } = await ipcRenderer.invoke(
|
||||
'openFolderDialog',
|
||||
userData
|
||||
);
|
||||
if (!filePaths[0] || canceled) return;
|
||||
setDataPath(filePaths[0]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PersonalData>
|
||||
<MainTitle>General</MainTitle>
|
||||
<PersonalDataContainer>
|
||||
<ProfileImage
|
||||
src={profileImage ? `data:image/jpeg;base64,${profileImage}` : null}
|
||||
/>
|
||||
<div
|
||||
css={`
|
||||
margin: 20px 20px 20px 40px;
|
||||
width: 330px;
|
||||
* {
|
||||
text-align: left;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
Username <br />
|
||||
<Username>{currentAccount.selectedProfile.name}</Username>
|
||||
</div>
|
||||
<div>
|
||||
UUID
|
||||
<br />
|
||||
<Uuid>
|
||||
{dashUuid(currentAccount.selectedProfile.id)}
|
||||
<Tooltip title={copiedUuid ? 'Copied' : 'Copy'} placement="top">
|
||||
<div
|
||||
css={`
|
||||
width: 13px;
|
||||
height: 14px;
|
||||
margin: 0 0 0 10px;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faCopy}
|
||||
onClick={() =>
|
||||
copy(
|
||||
setCopiedUuid,
|
||||
dashUuid(currentAccount.selectedProfile.id)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Uuid>
|
||||
</div>
|
||||
</div>
|
||||
</PersonalDataContainer>
|
||||
</PersonalData>
|
||||
<Title>Release Channel</Title>
|
||||
<Content>
|
||||
<p>
|
||||
Stable updates once a month. Beta updates more often, but it may have
|
||||
more bugs.
|
||||
</p>
|
||||
<Select
|
||||
css={`
|
||||
width: 100px;
|
||||
`}
|
||||
onChange={async e => {
|
||||
const appData = await ipcRenderer.invoke('getAppdataPath');
|
||||
setReleaseChannel(e);
|
||||
await fsa.writeFile(
|
||||
path.join(appData, 'gdlauncher_next', 'rChannel'),
|
||||
e.toString()
|
||||
);
|
||||
}}
|
||||
value={releaseChannel}
|
||||
virtual={false}
|
||||
>
|
||||
<Select.Option value={0}>Stable</Select.Option>
|
||||
<Select.Option value={1}>Beta</Select.Option>
|
||||
</Select>
|
||||
</Content>
|
||||
<Title>
|
||||
Concurrent Downloads <FontAwesomeIcon icon={faTachometerAlt} />
|
||||
</Title>
|
||||
<Content>
|
||||
<p>
|
||||
Select the number of concurrent downloads. If you have a slow
|
||||
connection, select at most 3.
|
||||
</p>
|
||||
<Select
|
||||
onChange={v => dispatch(updateConcurrentDownloads(v))}
|
||||
value={concurrentDownloads}
|
||||
css={`
|
||||
width: 70px;
|
||||
text-align: start;
|
||||
`}
|
||||
virtual={false}
|
||||
>
|
||||
{[...Array(20).keys()]
|
||||
.map(x => x + 1)
|
||||
.map(x => (
|
||||
<Select.Option key={x} value={x}>
|
||||
{x}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Content>
|
||||
<Title>
|
||||
Instance Sorting <FontAwesomeIcon icon={faSort} />
|
||||
</Title>
|
||||
<Content>
|
||||
<p
|
||||
css={`
|
||||
margin: 0;
|
||||
width: 400px;
|
||||
`}
|
||||
>
|
||||
Select the method in which instances should be sorted.
|
||||
</p>
|
||||
|
||||
<Select
|
||||
onChange={v => dispatch(updateInstanceSortType(v))}
|
||||
value={instanceSortMethod}
|
||||
css={`
|
||||
width: 136px;
|
||||
text-align: start;
|
||||
`}
|
||||
>
|
||||
<Select.Option value={0}>Alphabetical</Select.Option>
|
||||
<Select.Option value={1}>Last Played</Select.Option>
|
||||
<Select.Option value={2}>Most Played</Select.Option>
|
||||
</Select>
|
||||
</Content>
|
||||
<Title>
|
||||
Preferred Curse Release Channel <FontAwesomeIcon icon={faFire} />
|
||||
</Title>
|
||||
<Content>
|
||||
<p>
|
||||
Select the preferred release channel for downloading Curse projects.
|
||||
This also applies for mod updates.
|
||||
</p>
|
||||
<Select
|
||||
css={`
|
||||
width: 100px;
|
||||
text-align: start;
|
||||
`}
|
||||
onChange={e => dispatch(updateCurseReleaseChannel(e))}
|
||||
value={curseReleaseChannel}
|
||||
virtual={false}
|
||||
>
|
||||
<Select.Option value={1}>Stable</Select.Option>
|
||||
<Select.Option value={2}>Beta</Select.Option>
|
||||
<Select.Option value={3}>Alpha</Select.Option>
|
||||
</Select>
|
||||
</Content>
|
||||
<Title>
|
||||
Discord Integration <FontAwesomeIcon icon={faDiscord} />
|
||||
</Title>
|
||||
<Content>
|
||||
<p>
|
||||
Enable / disable Discord Integration. This displays what you are
|
||||
playing in Discord.
|
||||
</p>
|
||||
<Switch
|
||||
onChange={e => {
|
||||
dispatch(updateDiscordRPC(e));
|
||||
if (e) {
|
||||
ipcRenderer.invoke('init-discord-rpc');
|
||||
} else {
|
||||
ipcRenderer.invoke('shutdown-discord-rpc');
|
||||
}
|
||||
}}
|
||||
checked={DiscordRPC}
|
||||
/>
|
||||
</Content>
|
||||
<Title>
|
||||
Minecraft News <FontAwesomeIcon icon={faNewspaper} />
|
||||
</Title>
|
||||
<Content>
|
||||
<p>Enable / disable Minecraft news.</p>
|
||||
<Switch
|
||||
onChange={e => {
|
||||
dispatch(updateShowNews(e));
|
||||
}}
|
||||
checked={showNews}
|
||||
/>
|
||||
</Content>
|
||||
<Title>
|
||||
Hide Launcher While Playing <FontAwesomeIcon icon={faPlay} />
|
||||
</Title>
|
||||
<Content>
|
||||
<p>
|
||||
Automatically hide the launcher when launching an instance. You will
|
||||
still be able to open it from the icon tray.
|
||||
</p>
|
||||
<Switch
|
||||
onChange={e => {
|
||||
dispatch(updateHideWindowOnGameLaunch(e));
|
||||
}}
|
||||
checked={hideWindowOnGameLaunch}
|
||||
/>
|
||||
</Content>
|
||||
<Title>
|
||||
Potato PC Mode <FontAwesomeIcon icon={faToilet} />
|
||||
</Title>
|
||||
<Content>
|
||||
<p>
|
||||
You got a potato PC? Don't worry! We got you covered. Enable this
|
||||
and all animations and special effects will be disabled.
|
||||
</p>
|
||||
<Switch
|
||||
onChange={e => {
|
||||
dispatch(updatePotatoPcMode(e));
|
||||
}}
|
||||
checked={potatoPcMode}
|
||||
/>
|
||||
</Content>
|
||||
<Title>
|
||||
Clear Shared Data <FontAwesomeIcon icon={faTrash} />
|
||||
</Title>
|
||||
<Content>
|
||||
<p>
|
||||
Deletes all the shared files between instances. Doing this will remove
|
||||
ALL instance data.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
openModal('ActionConfirmation', {
|
||||
message: 'Are you sure you want to delete shared data?',
|
||||
confirmCallback: clearSharedData,
|
||||
title: 'Confirm'
|
||||
})
|
||||
);
|
||||
}}
|
||||
disabled={disableInstancesActions}
|
||||
loading={deletingInstances}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</Content>
|
||||
<Title>
|
||||
User Data Path <FontAwesomeIcon icon={faFolder} />
|
||||
<a
|
||||
css={`
|
||||
margin-left: 30px;
|
||||
`}
|
||||
onClick={async () => {
|
||||
const appData = await ipcRenderer.invoke('getAppdataPath');
|
||||
const appDataPath = path.join(appData, 'gdlauncher_next');
|
||||
setDataPath(appDataPath);
|
||||
}}
|
||||
>
|
||||
Reset Path
|
||||
</a>
|
||||
</Title>
|
||||
<CustomDataPathContainer>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
margin-bottom: 10px;
|
||||
p {
|
||||
text-align: left;
|
||||
color: ${props => props.theme.palette.text.third};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Input
|
||||
value={dataPath}
|
||||
onChange={e => setDataPath(e.target.value)}
|
||||
disabled={
|
||||
loadingMoveUserData ||
|
||||
deletingInstances ||
|
||||
disableInstancesActions
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
css={`
|
||||
margin-left: 20px;
|
||||
`}
|
||||
onClick={openFolder}
|
||||
disabled={loadingMoveUserData || deletingInstances}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFolder} />
|
||||
</Button>
|
||||
<Button
|
||||
css={`
|
||||
margin-left: 20px;
|
||||
`}
|
||||
onClick={changeDataPath}
|
||||
disabled={
|
||||
disableInstancesActions ||
|
||||
userData === dataPath ||
|
||||
!dataPath ||
|
||||
dataPath.length === 0 ||
|
||||
deletingInstances
|
||||
}
|
||||
loading={loadingMoveUserData}
|
||||
>
|
||||
Apply & Restart
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
`}
|
||||
>
|
||||
<Checkbox
|
||||
onChange={e => {
|
||||
setMoveUserData(e.target.checked);
|
||||
}}
|
||||
>
|
||||
Copy current data to the new directory
|
||||
</Checkbox>
|
||||
</div>
|
||||
</CustomDataPathContainer>
|
||||
<LauncherVersion>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin: 10px 0;
|
||||
`}
|
||||
>
|
||||
<HorizontalLogo
|
||||
size={200}
|
||||
onClick={() => dispatch(openModal('ChangeLogs'))}
|
||||
/>{' '}
|
||||
<div
|
||||
css={`
|
||||
margin-left: 10px;
|
||||
`}
|
||||
>
|
||||
v {version}
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
{updateAvailable
|
||||
? 'There is an update available to be installed. Click on update to install it and restart the launcher.'
|
||||
: 'You’re currently on the latest version. We automatically check for updates and we will inform you whenever one is available.'}
|
||||
</p>
|
||||
<div
|
||||
css={`
|
||||
margin-top: 20px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`}
|
||||
>
|
||||
{updateAvailable ? (
|
||||
<Button
|
||||
onClick={() =>
|
||||
ipcRenderer.invoke('installUpdateAndQuitOrRestart')
|
||||
}
|
||||
css={`
|
||||
margin-right: 10px;
|
||||
`}
|
||||
type="primary"
|
||||
>
|
||||
Update
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</Button>
|
||||
) : (
|
||||
<div
|
||||
css={`
|
||||
width: 96px;
|
||||
height: 36px;
|
||||
padding: 6px 8px;
|
||||
`}
|
||||
>
|
||||
Up to date
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</LauncherVersion>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(General);
|
||||