added initial files

This commit is contained in:
2025-07-31 00:48:48 +02:00
parent 411cc26582
commit 2a3dc51ce9
193 changed files with 110293 additions and 0 deletions
+7
View File
@@ -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;
+3
View File
@@ -0,0 +1,3 @@
import Root from './app/desktop/DesktopRoot';
export default Root;
+3
View File
@@ -0,0 +1,3 @@
import Root from './app/browser/BrowserRoot';
export default Root;
+7
View File
@@ -0,0 +1,7 @@
import React from 'react';
function BrowserRoot() {
return <div>This is a browser app</div>;
}
export default BrowserRoot;
View File
View File
+207
View File
@@ -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);
+54
View File
@@ -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);
+252
View File
@@ -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;
+437
View File
@@ -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 &nbsp;&nbsp;
</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 &nbsp;&nbsp;
<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>
// );
// }
+68
View File
@@ -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;
+13
View File
@@ -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;
+29
View File
@@ -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'
];
+161
View File
@@ -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();
});
});
};
+70
View File
@@ -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;
+124
View File
@@ -0,0 +1,124 @@
import path from 'path';
import fse from 'fs-extra';
import { promises as fs } from 'fs';
import pMap from 'p-map';
import { getDirectories } from '.';
import { CURSEFORGE } from '../../../common/utils/constants';
const getInstances = async instancesPath => {
const mapFolderToInstance = async instance => {
try {
const configPath = path.join(
path.join(instancesPath, instance, 'config.json')
);
const rawConfig = await fs.readFile(configPath);
// Remove temp config if present
try {
const tempConfigPath = path.join(
path.join(instancesPath, instance, 'config_new_temp.json')
);
await fs.unlink(tempConfigPath);
} catch {
// Nothing
}
// Restore config in case of crash
if (rawConfig.every(v => v === 0)) {
const backupConfigPath = path.join(
path.join(instancesPath, instance, 'config.bak.json')
);
const backupConfig = await fs.readFile(backupConfigPath);
JSON.parse(backupConfig);
await fs.rename(backupConfigPath, configPath);
}
const newRawConfig = await fs.readFile(configPath);
const config = JSON.parse(newRawConfig);
// if the launcher has the modloader as an array, convert it to object
if (Array.isArray(config.modloader)) {
// source is the source where the modpack comes from example: curseforge
// loaderType is the modloader example: forge
const [
loaderType,
mcVersion,
loaderVersion,
projectID,
fileID,
source
] = config.modloader;
const patchedConfig = {
...config,
loader: {
loaderType,
mcVersion,
...(loaderVersion && { loaderVersion }),
...(fileID && { fileID }),
...(projectID && { projectID }),
...(!source && fileID && projectID && { source: CURSEFORGE })
}
};
delete patchedConfig.modloader;
await fse.writeFile(configPath, JSON.stringify(patchedConfig));
return { ...patchedConfig, name: instance };
}
if (
config.loader?.fileId ||
config.loader?.addonId ||
config.loader?.addonID
) {
const { fileId, addonId, addonID } = config.loader;
const patchedConfig = {
...config,
loader: {
...config.loader,
...(fileId && { fileID: fileId }),
...(addonId && { projectID: addonId }),
...(addonID && { projectID: addonID })
}
};
delete patchedConfig.loader.fileId;
delete patchedConfig.loader.addonId;
await fse.writeFile(configPath, JSON.stringify(patchedConfig));
return { ...patchedConfig, name: instance };
}
return {
...config,
name: instance
};
} catch (err) {
console.error(err);
}
return null;
};
const folders = await getDirectories(instancesPath);
const instances = await pMap(
folders.filter(folder => folder !== '.DS_Store'),
mapFolderToInstance,
{
concurrency: 5
}
);
const hashMap = {};
// eslint-disable-next-line
for (const instance of instances) {
// eslint-disable-next-line
if (!instance) continue;
hashMap[instance.name] = instance;
}
return hashMap;
};
export default getInstances;
File diff suppressed because it is too large Load Diff
+27
View File
@@ -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;
+25
View File
@@ -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;
+24
View File
@@ -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;
+34
View File
@@ -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;
+125
View File
@@ -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);
+322
View File
@@ -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);
+302
View File
@@ -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&apos;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);
+83
View File
@@ -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};
`}
>
WERE 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;
}
}
+182
View File
@@ -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) }
}
`;
+362
View File
@@ -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;
};
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

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 elses 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>&#105;&#110;&#102;&#111;&#64;&#103;de&#118;&#115;&#46;&#105;o</p>
</div>
</div>
<p>This document was last updated on August 16, 2021</p>
</>
);
};
export default memo(AcceptableUsePolicy);
+12
View File
@@ -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;
+25
View File
@@ -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};
}
`;
+93
View File
@@ -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);
+187
View File
@@ -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;
+738
View File
@@ -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 devices 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 childs Personal Information from our
Services.
</p>
<p>
We encourage parents and legal guardians to monitor their childrens
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>Users 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&#64;&#103;d&#101;&#118;&#115;&#46;&#105;o</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);
+17
View File
@@ -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;
+75
View File
@@ -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);
+420
View File
@@ -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&#64;&#103;&#100;&#101;&#118;s.i&#111;</p>
</div>
</div>
<p>This document was last updated on August 16, 2021</p>
</>
);
};
export default memo(TermsAndConditions);
+210
View File
@@ -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;
`;
+84
View File
@@ -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>
);
}
+171
View File
@@ -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;
`;
+209
View File
@@ -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;
`;
+199
View File
@@ -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);
+54
View File
@@ -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};
`;
+81
View File
@@ -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 &nbsp;
<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};
`;
+44
View File
@@ -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' }
}
]
};
+421
View File
@@ -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>
&bull; {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;
`;
+32
View File
@@ -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;
+164
View File
@@ -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 &nbsp;
<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;
+122
View File
@@ -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;
+157
View File
@@ -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&apos;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;
}
`;
+890
View File
@@ -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);
+266
View File
@@ -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&nbsp;
<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
}
}))
);
}}
/>
&nbsp;X&nbsp;
<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;
}
}
`;
+435
View File
@@ -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);
+105
View File
@@ -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};
`}
>
&#10084;
</span>
<div>Thank you!</div>
</div>
</div>
</div>
</Modal>
);
};
export default memo(InstanceStartupAd);
+629
View File
@@ -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&apos;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)}%
);
`;
+219
View File
@@ -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 -&gt; fabric...) will result in the
loss of the modpack metadata. You won&apos;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;
`;
+126
View File
@@ -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;
}
`;
+475
View File
@@ -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;
}
`;
+416
View File
@@ -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;
}
`;
+618
View File
@@ -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;
`;
+111
View File
@@ -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;
`;
+100
View File
@@ -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;
// `;
+356
View File
@@ -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&apos;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;
+78
View File
@@ -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);
+37
View File
@@ -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 &nbsp; <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 &nbsp; <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 &nbsp; <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 &nbsp; <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 &nbsp; <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 &nbsp; <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 &nbsp; <FontAwesomeIcon icon={faToilet} />
</Title>
<Content>
<p>
You got a potato PC? Don&apos;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&nbsp; <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&nbsp; <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.'
: 'Youre 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 &nbsp;
<FontAwesomeIcon icon={faDownload} />
</Button>
) : (
<div
css={`
width: 96px;
height: 36px;
padding: 6px 8px;
`}
>
Up to date
</div>
)}
</div>
</LauncherVersion>
</>
);
};
export default memo(General);

Some files were not shown because too many files have changed in this diff Show More