diff --git a/package-lock.json b/package-lock.json index b34870508..f765ebe05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "felnullgdlauncher", - "version": "1.0.96", + "version": "1.0.97", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -16419,9 +16419,9 @@ "dev": true }, "prettier": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", - "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz", + "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==", "dev": true }, "prettier-linter-helpers": { diff --git a/package.json b/package.json index 8c03c1049..ef42f991b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "felnullgdlauncher", - "version": "1.0.96", + "version": "1.0.97", "description": "FelNullGDlauncher is simple, yet powerful Minecraft custom launcher with a strong focus on the user experience", "keywords": [ "minecraft", @@ -154,7 +154,7 @@ "husky": "^4.2.5", "native-ext-loader": "^2.3.0", "neon-cli": "^0.4.0", - "prettier": "^2.0.5", + "prettier": "^2.1.2", "react-scripts": "3.4.3", "rimraf": "^3.0.2", "terser-webpack-plugin": "^4.1.0", diff --git a/src/app/desktop/components/Instances/Instance.js b/src/app/desktop/components/Instances/Instance.js index 5d8b01432..a2695dfb8 100644 --- a/src/app/desktop/components/Instances/Instance.js +++ b/src/app/desktop/components/Instances/Instance.js @@ -76,8 +76,8 @@ const InstanceContainer = styled.div` 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: 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; diff --git a/src/app/desktop/components/News.js b/src/app/desktop/components/News.js index 23c6aaa2e..b302d63dc 100644 --- a/src/app/desktop/components/News.js +++ b/src/app/desktop/components/News.js @@ -38,7 +38,7 @@ const ImageSlide = styled.div` height: 100%; width: 100%; border-radius: ${props => props.theme.shape.borderRadius}; - background-image: url("${props => (props.image ? props.image : null)}"); + background-image: url('${props => (props.image ? props.image : null)}'); background-position: center; background-size: cover; transition: transform 0.2s ease-in-out; diff --git a/src/common/components/ModalsManager.js b/src/common/components/ModalsManager.js index d268be551..1f01b1c78 100644 --- a/src/common/components/ModalsManager.js +++ b/src/common/components/ModalsManager.js @@ -62,6 +62,9 @@ const modalsComponentLookupTable = { BisectHosting: AsyncComponent(lazy(() => import('../modals/BisectHosting'))), Onboarding: AsyncComponent(lazy(() => import('../modals/Onboarding'))), ModOverview: AsyncComponent(lazy(() => import('../modals/ModOverview'))), + ModsChangeLogs: AsyncComponent( + lazy(() => import('../modals/ModsChangelogs')) + ), ModsBrowser: AsyncComponent(lazy(() => import('../modals/ModsBrowser'))), JavaSetup: AsyncComponent(lazy(() => import('../modals/JavaSetup'))), ModsUpdater: AsyncComponent(lazy(() => import('../modals/ModsUpdater'))), diff --git a/src/common/modals/InstanceManager/Mods.js b/src/common/modals/InstanceManager/Mods.js index 833025616..c8d690a67 100644 --- a/src/common/modals/InstanceManager/Mods.js +++ b/src/common/modals/InstanceManager/Mods.js @@ -44,7 +44,22 @@ const RowContainer = styled.div.attrs(props => ({ style: props.override }))` width: 100%; - background: ${props => props.theme.palette.grey[props.index % 2 ? 700 : 800]}; + height: 100%; + background: ${props => + props.disabled || props.selected + ? 'transparent' + : props.theme.palette.grey[800]}; + ${props => + props.disabled && + !props.selected && + `border: 2px solid + ${props.theme.palette.colors.red};`} + ${props => + props.selected && + `border: 2px solid + ${props.theme.palette.primary.main};`} + transition: border 0.1s ease-in-out; + border-radius: 4px; display: flex; justify-content: space-between; align-items: center; @@ -60,17 +75,17 @@ const RowContainer = styled.div.attrs(props => ({ } .rowCenterContent { flex: 1; - height: 100%; display: flex; justify-content: center; align-items: center; transition: color 0.1s ease-in-out; + color: ${props => props.isHovered && props.theme.palette.primary.main}; cursor: pointer; svg { margin-right: 10px; } &:hover { - color: ${props => props.theme.palette.primary.main}; + color: ${props => props.theme.palette.text.primary}; } } .rightPartContent { @@ -83,6 +98,36 @@ const RowContainer = styled.div.attrs(props => ({ } `; +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: all 0.1s ease-in-out; + opacity: ${props => (props.disabled || props.selected ? 1 : 0)}; +`; + const DragEnterEffect = styled.div` position: absolute; display: flex; @@ -221,6 +266,7 @@ const toggleModDisabled = async ( 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 ); @@ -240,10 +286,30 @@ const Row = memo(({ index, style, data }) => { latestMods[item.projectID].releaseType <= curseReleaseChannel; const dispatch = useDispatch(); + const name = item.fileName + .replace('.jar', '') + .replace('.zip', '') + .replace('.disabled', ''); + return ( <> - +
{ }} className="rowCenterContent" > - {item.fileName} + {name}
{isUpdateAvailable && @@ -342,10 +408,18 @@ const Row = memo(({ index, style, data }) => { icon={faTrash} />
+
- + setIsHovered(true)} + onHide={() => setIsHovered(false)} + > { clipboard.writeText(item.displayName); diff --git a/src/common/modals/InstanceManager/ResourcePacks.js b/src/common/modals/InstanceManager/ResourcePacks.js new file mode 100644 index 000000000..41bb133ed --- /dev/null +++ b/src/common/modals/InstanceManager/ResourcePacks.js @@ -0,0 +1,337 @@ +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 { faTrash } from '@fortawesome/free-solid-svg-icons'; +import { useSelector, useDispatch } from 'react-redux'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { faTwitch } from '@fortawesome/free-brands-svg-icons'; +import fse from 'fs-extra'; +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(FontAwesomeIcon)` + margin: 0 10px; + &:hover { + cursor: pointer; + path { + cursor: pointer; + transition: all 0.1s ease-in-out; + color: ${props => props.theme.palette.error.main}; + } + } +`; + +const RowContainer = styled.div.attrs(props => ({ + style: props.override +}))` + width: 100%; + background: ${props => props.theme.palette.grey[800]}; + border: solid 5px ${props => props.theme.palette.grey[700]}; + border-style: solid none solid none; + 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; + } + } +`; + +export const keyFrameMoveUpDown = keyframes` + 0% { + transform: translateY(0); + } + 50% { + transform: translateY(-15px); + } + + `; + +let watcher; + +const toggleModDisabled = 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 dispatch = useDispatch(); + + const deleteFile = useCallback( + async ( + item, + instancesPathh, + selectedItemss, + rscPacksPath, + instanceNamee + ) => { + if (selectedItemss.length === 0 && item) { + await fse.remove( + path.join(instancesPathh, instanceNamee, 'resourcepacks', item) + ); + } else if (selectedItemss.length === 1) { + await fse.remove( + path.join( + instancesPathh, + instanceNamee, + 'resourcepacks', + selectedItemss[0] + ) + ); + } else if (selectedItemss.length > 1 && !item) { + 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 ( + +
+ { + if (e.target.checked) { + setSlcItems([...slcItems, item]); + } else { + setSlcItems(slcItems.filter(v => v !== item)); + } + }} + /> + {item.fileID && } +
+
{item}
+
+ { + setLoading(true); + await toggleModDisabled(c, name, instancePath, item, dispatch); + setTimeout(() => setLoading(false), 500); + }} + /> + + deleteFile(item, instancesPath, slcItems, rscPacksPath, name) + } + icon={faTrash} + /> +
+
+ ); + }, areEqual); + + const openFolderDialog = async () => { + const dialog = await ipcRenderer.invoke('openFileDialog', [ + { extensions: ['7zip', 'zip'] } + ]); + if (dialog.canceled) return; + const fileName = path.basename(dialog.filePaths[0]); + await fse.copy( + dialog.filePaths[0], + path.join(instancesPath, instanceName, 'resourcepacks', fileName) + ); + }; + + 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); + } + }); + }; + + useEffect(() => { + startListener(); + return () => watcher?.close(); + }, []); + + const itemData = createItemData( + resourcePacks, + instanceName, + path.join(instancesPath, instanceName), + selectedItems, + setSelectedItems, + resourcePacksPath + ); + + return ( +
+
+
+ + selectedItems.length !== resourcePacks.length + ? setSelectedItems(resourcePacks.map(v => v)) + : setSelectedItems([]) + } + > + Select All + + + deleteFile( + null, + instancesPath, + selectedItems, + resourcePacksPath, + instanceName + ) + } + icon={faTrash} + /> +
+ +
+ + + {resourcePacks.length === 0 && ( + No ResourcePacks Available + )} + + {({ height, width }) => ( + + {Row} + + )} + + +
+ ); +}; + +export default memo(ResourcePacks); diff --git a/src/common/modals/InstanceManager/Screenshots.js b/src/common/modals/InstanceManager/Screenshots.js index 6a0b880a4..5f9bdf9e0 100644 --- a/src/common/modals/InstanceManager/Screenshots.js +++ b/src/common/modals/InstanceManager/Screenshots.js @@ -486,8 +486,10 @@ const DateSection = styled.div` const NoScreenAvailable = styled.div` height: 100%; - text-align: center; - padding-top: 25%; + width: 100%; + display: flex; + align-items: center; + justify-content: center; `; const StyledContexMenu = styled(ContextMenu)` diff --git a/src/common/modals/InstanceManager/index.js b/src/common/modals/InstanceManager/index.js index 6c7833e0e..497b15ea9 100644 --- a/src/common/modals/InstanceManager/index.js +++ b/src/common/modals/InstanceManager/index.js @@ -9,6 +9,7 @@ import Modal from '../../components/Modal'; import Overview from './Overview'; import { ipcRenderer } from 'electron'; import Screenshots from './Screenshots'; +import ResourcePacks from './ResourcePacks'; import Notes from './Notes'; import Mods from './Mods'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -140,13 +141,14 @@ const InstanceBackground = styled.div` `; const menuEntries = { - overview: { name: 'Overview', component: Overview }, - mods: { name: 'Mods', component: Mods }, - modpack: { name: 'Modpack', component: Modpack }, - notes: { name: 'Notes', component: Notes }, + overview: {name: 'Overview', component: Overview}, + mods: {name: 'Mods', component: Mods}, + modpack: {name: 'Modpack', component: Modpack}, + notes: {name: 'Notes', component: Notes}, + ResourcePacks: {name: 'ResourcePacks', component: ResourcePacks}, // resourcePacks: { name: "Resource Packs", component: Overview }, // worlds: { name: "Worlds", component: Overview }, - screenshots: { name: 'Screenshots', component: Screenshots } + screenshots: {name: 'Screenshots', component: Screenshots} // settings: { name: "Settings", component: Overview }, // servers: { name: "Servers", component: Overview } }; diff --git a/src/common/modals/ModOverview.js b/src/common/modals/ModOverview.js index 9d1a67884..fc2c76333 100644 --- a/src/common/modals/ModOverview.js +++ b/src/common/modals/ModOverview.js @@ -1,19 +1,27 @@ /* eslint-disable */ -import React, { useState, useEffect } from 'react'; +import React, {useState, useEffect} from 'react'; import styled from 'styled-components'; -import { useDispatch, useSelector } from 'react-redux'; +import {useDispatch, useSelector} from 'react-redux'; import ReactHtmlParser from 'react-html-parser'; import path from 'path'; -import { Checkbox, TextField, Cascader, Button, Input, Select } from 'antd'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faExternalLinkAlt, faInfo} from '@fortawesome/free-solid-svg-icons'; +import {Checkbox, TextField, Cascader, Button, Input, Select} from 'antd'; import Modal from '../components/Modal'; -import { transparentize } from 'polished'; -import { getAddonDescription, getAddonFiles, getAddon } from '../api'; +import {transparentize} from 'polished'; +import { + getAddonDescription, + getAddonFiles, + getAddon, + getAddonFileChangelog +} from '../api'; import CloseButton from '../components/CloseButton'; -import { closeModal } 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 {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, @@ -21,43 +29,67 @@ import { } from '../../app/desktop/utils'; const ModOverview = ({ - projectID, - fileID, - gameVersion, - instanceName, - fileName -}) => { + projectID, + fileID, + gameVersion, + instanceName, + fileName + }) => { const dispatch = useDispatch(); const [description, setDescription] = useState(null); const [addon, setAddon] = useState(null); + const [changeLog, setChangeLog] = useState(null); const [files, setFiles] = useState([]); - const [selectedItem, setSelectedItem] = useState({ fileID, fileName }); - const [installedData, setInstalledData] = useState({ fileID, fileName }); + const [selectedItem, setSelectedItem] = useState({fileID, fileName}); + 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)); + const initScreenShots = async data => { + if (data) { + const mappedFiles = await Promise.all( + data.map(async v => { + const {data: changelog} = await getAddonFileChangelog( + projectID, + v.id + ); + return { + ...v, + changelog + }; + }) + ); + setChangeLog(mappedFiles); + } + }; + useEffect(() => { setLoadingFiles(true); getAddon(projectID).then(data => setAddon(data.data)); getAddonDescription(projectID).then(data => { // Replace the beginning of all relative URLs with the Curseforge URL - const modifiedData = data.data.replace(/href="(?!http)/g, `href="${CURSEFORGE_URL}`) + const modifiedData = data.data.replace( + /href="(?!http)/g, + `href="${CURSEFORGE_URL}` + ); - setDescription(modifiedData) + setDescription(modifiedData); }); getAddonFiles(projectID).then(data => { const isFabric = - getPatchedInstanceType(instance) === FABRIC && projectID !== 361988; + getPatchedInstanceType(instance) === FABRIC && projectID !== 361988; const isForge = - getPatchedInstanceType(instance) === FORGE || projectID === 361988; + getPatchedInstanceType(instance) === FORGE || projectID === 361988; let filteredFiles = []; if (isFabric) { filteredFiles = filterFabricFilesByVersion(data.data, gameVersion); } else if (isForge) { filteredFiles = filterForgeFilesByVersion(data.data, gameVersion); } + + initScreenShots(data.data); setFiles(filteredFiles); setLoadingFiles(false); }); @@ -111,6 +143,7 @@ const ModOverview = ({ const handleChange = value => setSelectedItem(JSON.parse(value)); + // ReactHtmlParser(files[selectedIndex]?.changelog) const primaryImage = (addon?.attachments || []).find(v => v.isDefault); return ( - {addon?.name} + + + {addon?.name} + +
+ + {addon?.authors[0].name} +
+ {addon?.downloadCount && ( +
+ + {formatNumber(addon?.downloadCount)} +
+ )} +
+ {' '} + {formatDate(addon?.dateModified)} +
+
+ + {addon?.gameVersionLatestFiles[0].gameVersion} +
+
+ + +
+
{ReactHtmlParser(description)}
@@ -316,6 +409,21 @@ const Parallax = styled.div` 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%; @@ -331,6 +439,23 @@ const ParallaxContent = styled.div` 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; diff --git a/src/common/modals/ModpackDescription.js b/src/common/modals/ModpackDescription.js index 4f1a3027d..f5d8466f0 100644 --- a/src/common/modals/ModpackDescription.js +++ b/src/common/modals/ModpackDescription.js @@ -1,33 +1,64 @@ /* eslint-disable */ -import React, { useState, useEffect } from 'react'; +import React, {useState, useEffect} from 'react'; import styled from 'styled-components'; -import { useDispatch } from 'react-redux'; +import {useDispatch} from 'react-redux'; import ReactHtmlParser from 'react-html-parser'; -import { Checkbox, TextField, Cascader, Button, Input, Select } from 'antd'; +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 {transparentize} from 'polished'; +import { + getAddonDescription, + getAddonFiles, + getAddonFileChangelog +} from '../api'; import CloseButton from '../components/CloseButton'; -import { closeModal } from '../reducers/modals/actions'; -import { FORGE, CURSEFORGE_URL } from '../utils/constants'; +import {closeModal, openModal} from '../reducers/modals/actions'; +import {FORGE, CURSEFORGE_URL} from '../utils/constants'; +import {formatNumber, formatDate} from '../utils'; -const AddInstance = ({ modpack, setStep, setModpack, setVersion }) => { +const AddInstance = ({modpack, setStep, setModpack, setVersion}) => { const dispatch = useDispatch(); const [description, setDescription] = useState(null); const [files, setFiles] = useState(null); + const [changeLog, setChangeLog] = useState(null); const [selectedId, setSelectedId] = useState(false); const [loading, setLoading] = useState(false); + const initScreenShots = async data => { + if (data) { + const mappedFiles = await Promise.all( + data.map(async v => { + const {data: changelog} = await getAddonFileChangelog( + modpack.id, + v.id + ); + return { + ...v, + changelog + }; + }) + ); + setChangeLog(mappedFiles); + } + }; + useEffect(() => { setLoading(true); getAddonDescription(modpack.id).then(data => { // Replace the beginning of all relative URLs with the Curseforge URL - const modifiedData = data.data.replace(/href="(?!http)/g, `href="${CURSEFORGE_URL}`) + const modifiedData = data.data.replace( + /href="(?!http)/g, + `href="${CURSEFORGE_URL}` + ); - setDescription(modifiedData) + setDescription(modifiedData); }); getAddonFiles(modpack.id).then(data => { setFiles(data.data); + initScreenShots(data.data); setLoading(false); }); }, []); @@ -86,7 +117,65 @@ const AddInstance = ({ modpack, setStep, setModpack, setVersion }) => { - {modpack.name} + + + {modpack.name} + +
+ + {modpack.authors[0].name} +
+
+ + {formatNumber(modpack.downloadCount)} +
+
+ + {formatDate(modpack.dateModified)} +
+
+ + {modpack.gameVersionLatestFiles[0].gameVersion} +
+
+ + +
+
{ReactHtmlParser(description)}
@@ -151,7 +240,6 @@ const AddInstance = ({ modpack, setStep, setModpack, setVersion }) => { month: 'long', day: 'numeric' })} - @@ -234,6 +322,21 @@ const Parallax = styled.div` 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%; @@ -243,10 +346,26 @@ const ParallaxContent = styled.div` font-weight: bold; font-size: 60px; text-align: center; - padding-top: 20%; 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; diff --git a/src/common/modals/ModsChangelogs.js b/src/common/modals/ModsChangelogs.js new file mode 100644 index 000000000..52286d0c5 --- /dev/null +++ b/src/common/modals/ModsChangelogs.js @@ -0,0 +1,57 @@ +/* eslint-disable react/no-unescaped-entities */ +import React, { memo } from 'react'; +import styled from 'styled-components'; +import ReactHtmlParser from 'react-html-parser'; +import Modal from '../components/Modal'; + +const ModsChangeLogs = ({ changeLog }) => { + return ( + + + {changeLog ? ( + ReactHtmlParser(changeLog) + ) : ( +

+ Missing Changelog +

+ )} +
+
+ ); +}; + +export default memo(ModsChangeLogs); + +const Changelog = styled.div` + perspective: 1px; + transform-style: preserve-3d; + height: calc(100% - 10px); + background: ${props => props.theme.palette.grey[900]}; + word-break: break-all; + overflow-x: hidden; + overflow-y: scroll; + font-size: 20px; + & > div:first-child { + font-size: 24px; + width: 100%; + text-align: center; + margin-bottom: 30px; + } + p { + text-align: center; + } + img { + max-width: 100%; + height: auto; + } +`; diff --git a/src/common/utils/index.js b/src/common/utils/index.js index 546b44929..fdbe3822a 100644 --- a/src/common/utils/index.js +++ b/src/common/utils/index.js @@ -24,6 +24,33 @@ export function sortByForgeVersionDesc(a, b) { return 0; } +export const formatNumber = number => { + // Alter numbers larger than 1k + if (number >= 1e3) { + const units = ['k', 'M', 'B', 'T']; + + const unit = Math.floor((number.toFixed(0).length - 1) / 3) * 3; + // Calculate the remainder + const num = (number / `1e${unit}`).toFixed(0); + const unitname = units[Math.floor(unit / 3) - 1]; + + // output number remainder + unitname + return num + unitname; + } + + // return formatted original number + return number.toLocaleString(); +}; + +export const formatDate = date => { + const parsedDate = Date.parse(date); + return new Date(parsedDate).toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric' + }); +}; + export const getForgeFileIDFromAddonVersion = async (files, addonVersion) => { const foundID = files.find(a => a.fileName.includes(addonVersion)); return foundID ? foundID.id : null; diff --git a/src/ui/DragnDropEffect.js b/src/ui/DragnDropEffect.js new file mode 100644 index 000000000..9bf304aa8 --- /dev/null +++ b/src/ui/DragnDropEffect.js @@ -0,0 +1,239 @@ +import React, { useEffect, useState } from 'react'; +import { Spin } from 'antd'; +import path from 'path'; +import pMap from 'p-map'; +import fse from 'fs-extra'; +import { Transition } from 'react-transition-group'; +import { faArrowDown } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import styled, { keyframes } from 'styled-components'; +import { LoadingOutlined } from '@ant-design/icons'; + +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, .3) 40%, + rgba(0, 0, 0, .3) 40% + ); + opacity: ${({ transitionState }) => + transitionState === 'entering' || transitionState === 'entered' ? 1 : 0}; +`; + +const keyFrameMoveUpDown = keyframes` + 0% { + transform: translateY(0); + } + 50% { + transform: translateY(-15px); + } +`; + +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` + font-weight: bold; + ${props => + props.fileDrag ? props.theme.palette.primary.main : 'transparent'}; + color: ${props => props.theme.palette.primary.main}; + animation: ${keyFrameMoveUpDown} 1.5s linear infinite; +`; + +const antIcon = ( + +); + +const DragnDropEffect = ({ + instancesPath, + instanceName, + fileList, + children +}) => { + const [fileDrag, setFileDrag] = useState(false); + const [fileDrop, setFileDrop] = useState(false); + const [numOfDraggedFiles, setNumOfDraggedFiles] = useState(0); + const [dragCompleted, setDragCompleted] = useState({}); + const [dragCompletedPopulated, setDragCompletedPopulated] = useState(false); + + const onDragOver = e => { + setFileDrag(true); + e.preventDefault(); + }; + + const onDrop = async e => { + setFileDrop(true); + const dragComp = {}; + const { files } = e.dataTransfer; + const arrTypes = Object.values(files).map(file => { + const fileName = file.name; + const fileType = path.extname(fileName); + return fileType; + }); + + await pMap( + Object.values(files), + async file => { + const fileName = file.name; + const fileType = path.extname(fileName); + + dragComp[fileName] = false; + + setNumOfDraggedFiles(files.length); + + const { path: filePath } = file; + + if (fileList && fileList?.includes(fileName)) { + console.error( + 'A resourcepack with this name already exists in the instance.', + file.name + ); + setFileDrop(false); + setFileDrag(false); + } else if (Object.values(files).length === 1) { + if ( + fileType === '.zip' || + fileType === '.7z' || + fileType === '.disabled' + ) { + await fse.copy( + filePath, + path.join(instancesPath, instanceName, 'resourcepacks', fileName) + ); + dragComp[fileName] = true; + setFileDrop(false); + } else { + console.error('This file is not a zip'); + setFileDrop(false); + setFileDrag(false); + } + } else { + /* eslint-disable */ + if (arrTypes.includes('7z') || arrTypes.includes('zip')) { + if (fileType === 'zip' || fileType === '7z') { + await fse.copy( + filePath, + path.join( + instancesPath, + instanceName, + 'resourcepacks', + fileName + ) + ); + dragComp[fileName] = true; + } else { + setFileDrop(false); + setFileDrag(false); + } + } else { + console.error('The files are not a zips!'); + 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); + }; + + useEffect(() => { + if (dragCompletedPopulated) { + const AllFilesAreCompleted = Object.keys(dragCompleted).every(x => + fileList.find(y => y === x) + ); + + setNumOfDraggedFiles(numOfDraggedFiles - 1); + + if (AllFilesAreCompleted) { + setFileDrop(false); + setFileDrag(false); + } + } + }, [dragCompleted, fileList]); + + return ( + <> +
+ + {transitionState => ( + + {fileDrop ? ( + + {numOfDraggedFiles > 0 ? numOfDraggedFiles : 1} + + ) : ( +
e.stopPropagation()} + > + Copy + +
+ )} +
+ )} +
+ {children} +
+ + ); +}; + +export default DragnDropEffect; \ No newline at end of file