From 2467e0dd45b80ddce746aff8b9abd89c8ca40f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Dohnal?= Date: Tue, 12 Jul 2022 00:12:01 +0200 Subject: [PATCH 1/3] Add technic modpacks --- src/common/api.js | 108 +++- src/common/assets/technicIcon.webp | Bin 0 -> 3078 bytes src/common/modals/AddInstance/Content.js | 28 +- src/common/modals/AddInstance/InstanceName.js | 172 ++++++- .../TechnicModpacks/ModpacksListWrapper.js | 243 +++++++++ .../AddInstance/TechnicModpacks/index.js | 209 ++++++++ src/common/modals/InstanceManager/Modpack.js | 100 +++- src/common/modals/InstanceManager/index.js | 7 +- src/common/modals/ModChangelog.js | 76 ++- src/common/modals/ModpackDescription.js | 277 +++++++--- src/common/reducers/actions.js | 486 +++++++++++++++++- src/common/utils/constants.js | 4 + 12 files changed, 1576 insertions(+), 134 deletions(-) create mode 100644 src/common/assets/technicIcon.webp create mode 100644 src/common/modals/AddInstance/TechnicModpacks/ModpacksListWrapper.js create mode 100644 src/common/modals/AddInstance/TechnicModpacks/index.js diff --git a/src/common/api.js b/src/common/api.js index a53cd955d..7e2806db5 100644 --- a/src/common/api.js +++ b/src/common/api.js @@ -2,18 +2,19 @@ import axios from 'axios'; import qs from 'querystring'; import { - MOJANG_APIS, - FORGESVC_URL, - MC_MANIFEST_URL, FABRIC_APIS, - JAVA_MANIFEST_URL, + FORGESVC_URL, + FTB_API_URL, IMGUR_CLIENT_ID, + JAVA_LATEST_MANIFEST_URL, + JAVA_MANIFEST_URL, + MC_MANIFEST_URL, MICROSOFT_LIVE_LOGIN_URL, MICROSOFT_XBOX_LOGIN_URL, MICROSOFT_XSTS_AUTH_URL, MINECRAFT_SERVICES_URL, - FTB_API_URL, - JAVA_LATEST_MANIFEST_URL + MOJANG_APIS, + TECHNIC_API_URL } from './utils/constants'; import { sortByDate } from './utils'; import ga from './utils/analytics'; @@ -26,6 +27,8 @@ const axioInstance = axios.create({ } }); +let technicClientBuild = '757'; // Requests to technic api need build id + const trackFTBAPI = () => { ga.sendCustomEvent('FTBAPICall'); }; @@ -34,6 +37,14 @@ const trackCurseForgeAPI = () => { ga.sendCustomEvent('CurseForgeAPICall'); }; +const trackTechnicAPI = () => { + ga.sendCustomEvent('TechnicAPICall'); +}; + +const trackTechnicSolderAPI = () => { + ga.sendCustomEvent('TechnicSolderAPICall'); +}; + // Microsoft Auth export const msExchangeCodeForAccessToken = ( clientId, @@ -367,6 +378,91 @@ export const getSearch = async ( return data?.data; }; +export const getTechnicSearch = searchFilter => { + const url = `${TECHNIC_API_URL}/search`; + const params = { + build: technicClientBuild, + q: searchFilter + }; + trackTechnicAPI(url, params); + return axios.get(url, { params }); +}; + +export const getTechnicClientBuild = () => { + try { + const url = `${TECHNIC_API_URL}/launcher/version/stable4`; + trackTechnicAPI(url, {}); + const { data } = axios.get(url); + technicClientBuild = data.build; + return data.build; + } catch { + return { status: 'error' }; + } +}; + +export const getTechnicChangelog = async changelogUrl => { + try { + trackTechnicAPI(changelogUrl, {}); + const { data } = await axios.get(changelogUrl); + return data; + } catch { + return { status: 'error' }; + } +}; + +export const getTechnicAddDownload = async name => { + const url = `${TECHNIC_API_URL}/modpack/${name}/stat/download`; + const params = { + build: technicClientBuild + }; + trackTechnicAPI(url, params); + try { + return await axios.get(url, { params }); + } catch (e) { + return null; + } +}; + +export const getTechnicAddRun = async name => { + const url = `${TECHNIC_API_URL}/modpack/${name}/stat/run`; + const params = { + build: technicClientBuild + }; + trackTechnicAPI(url, params); + try { + return await axios.get(url, { params }); + } catch (e) { + return null; + } +}; + +export const getTechnicModpackData = name => { + const url = `${TECHNIC_API_URL}/modpack/${name}`; + const params = { + build: technicClientBuild + }; + trackTechnicAPI(url, params); + return axios.get(url, { params }); +}; + +export const getTechnicSolderMultiple = (solder, type) => { + const url = `${solder}/${type}`; + const params = { + include: 'full' + }; + trackTechnicSolderAPI(url, params); + return axios.get(url, { params }); +}; + +export const getTechnicSolderData = (solder, type, name, version = null) => { + let url = `${solder}${type}/${name}`; + if (version) { + url += `/${version}`; + } + trackTechnicSolderAPI(url, {}); + return axios.get(url, {}); +}; + export const getFTBModpackData = async modpackId => { trackFTBAPI(); try { diff --git a/src/common/assets/technicIcon.webp b/src/common/assets/technicIcon.webp new file mode 100644 index 0000000000000000000000000000000000000000..568988a7364ae7a5668e1197a541e224d786b01d GIT binary patch literal 3078 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, + ]; @@ -103,8 +107,18 @@ const Content = ({ /> CurseForge - {/* ATLauncher - Technic */} + {/* ATLauncher */} + + + Technic + { if (!modpack) return null; - // Curseforge - if (!modpack.synopsis) { - return modpack?.logo?.thumbnailUrl; - } else { + if (modpack.synopsis) { // FTB const image = modpack?.art?.reduce((prev, curr) => { if (!prev || curr.size < prev.size) return curr; @@ -88,6 +82,20 @@ const InstanceName = ({ }); return image.url; } + if (Object.prototype.hasOwnProperty.call(modpack, 'solder')) { + // Technic + if (modpack.solder) { + return ( + modpack.solderLogo ?? + modpack.logo?.url ?? + modpack.solderBackground ?? + modpack.background?.url + ); + } + return modpack.logo?.url ?? modpack.background?.url; + } + // Curseforge + return modpack?.logo?.thumbnailUrl; }, [modpack]); const wait = t => { @@ -103,6 +111,11 @@ const InstanceName = ({ const isCurseForgeModpack = Boolean(version?.source === CURSEFORGE); const isFTBModpack = Boolean(modpack?.art); + const isTechnicModpack = Object.prototype.hasOwnProperty.call( + modpack, + 'solder' + ); + const isTechnicSolderModpack = isTechnicModpack && modpack.solder; let manifest; // If it's a curseforge modpack grab the manfiest and detect the loader @@ -288,6 +301,135 @@ const InstanceName = ({ ramAmount ? { javaMemory: ramAmount } : null ) ); + } else if (isTechnicSolderModpack) { + const modpackData = ( + await getTechnicSolderData( + modpack.solder, + 'modpack', + version.projectID, + version.fileID + ) + ).data; + + const newManifest = { + files: modpackData.mods.map(m => { + return { + path: '', + name: `${m.name}-${m.version}${path.extname(m.url.split('?')[0])}`, + url: m.url + }; + }) + }; + + const isVanillaLoader = version?.projectID === 'vanilla'; + let forgeVersion = null; + if (!isVanilla) { + forgeVersion = modpackData.forge + ? forgeManifest[modpackData.minecraft].find(v => + v.endsWith(modpackData.forge) + ) + : forgeManifest[modpackData.minecraft][0]; + } + + const loader = { + loaderType: isVanillaLoader ? VANILLA : FORGE, // Solder does not support other loaders at the moment + mcVersion: modpackData.minecraft, + loaderVersion: forgeVersion, + fileID: version?.fileID, + projectID: version?.projectID, + source: TECHNIC_SOLDER, + sourceName: modpack.name, + solder: modpack.solder + }; + + const backgroundFilename = `background${path.extname( + imageURL.split('?')[0] + )}`; + + await downloadFile( + path.join(instancesPath, localInstanceName, backgroundFilename), + imageURL + ); + + let ramAmount = null; + + const userMemory = Math.round(os.totalmem() / 1024 / 1024); + + if (userMemory < modpackData.memory) { + try { + await new Promise((resolve, reject) => { + dispatch( + openModal('ActionConfirmation', { + message: `${modpackData.memory}MB of RAM is recommended to play this modpack and you only have ${userMemory}MB. You might still be able to play it but probably with low performance. Do you want to continue?`, + confirmCallback: () => resolve(), + abortCallback: () => reject(), + title: 'Low Memory Warning' + }) + ); + }); + } catch { + setClicked(false); + return; + } + } + const recommendedMemory = Math.floor(modpackData.memory * 1.2); + if (userMemory >= recommendedMemory) { + ramAmount = recommendedMemory; + } else if (userMemory >= modpackData.memory) { + ramAmount = modpackData.memory; + } + + dispatch( + addToQueue( + localInstanceName, + loader, + newManifest, + backgroundFilename, + null, + modpackData.memory ? { javaMemory: ramAmount } : null + ) + ); + } else if (isTechnicModpack) { + const loader = { + loaderType: null, + mcVersion: modpack.minecraft, + loaderVersion: null, + fileID: version?.fileID, + projectID: version?.projectID, + source: TECHNIC, + sourceName: modpack.name + }; + const backgroundFilename = `background${path.extname( + imageURL.split('?')[0] + )}`; + + if (imageURL) { + await downloadFile( + path.join(instancesPath, localInstanceName, backgroundFilename), + imageURL + ); + } + + const newManifest = { + files: [ + { + path: '', + name: `modpack${path.extname(modpack.url.split('?')[0])}`, + url: modpack.url + } + ] + }; + + dispatch( + addToQueue( + localInstanceName, + loader, + newManifest, + imageURL ? backgroundFilename : null, + null, + null + ) + ); } else if (importZipPath) { manifest = await importAddonZip( importZipPath, diff --git a/src/common/modals/AddInstance/TechnicModpacks/ModpacksListWrapper.js b/src/common/modals/AddInstance/TechnicModpacks/ModpacksListWrapper.js new file mode 100644 index 000000000..34b128025 --- /dev/null +++ b/src/common/modals/AddInstance/TechnicModpacks/ModpacksListWrapper.js @@ -0,0 +1,243 @@ +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 { Tag } from 'antd'; +import { CheckCircleOutlined } from '@ant-design/icons'; +import { openModal } from '../../../reducers/modals/actions'; +import { TECHNIC, TECHNIC_SOLDER } 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 ( + + ); + } + + const primaryImage = modpack.background?.url; + return ( +
+ +
+ {modpack.displayName} +
+ {modpack.solder && ( + } color="geekblue"> + SOLDER + + )} +
+
+ +
{ + setVersion({ + projectID: modpack.name, + fileID: modpack.solder ? modpack.latest : modpack.version, + source: modpack.solder ? TECHNIC_SOLDER : TECHNIC + }); + setModpack({ ...modpack, name: modpack.displayName }); + setStep(1); + }} + > + Download Latest +
+
{ + dispatch( + openModal('ModpackDescription', { + modpack, + setVersion, + setModpack, + setStep, + type: modpack.solder ? TECHNIC_SOLDER : TECHNIC + }) + ); + }} + > + Explore / Versions +
+
+
+ ); + }); + + const innerElementType = forwardRef(({ style, ...rest }, ref) => ( +
+ )); + + return ( + loadMoreItems()} + > + {({ onItemsRendered }) => ( + { + // Manually bind ref to reset scroll + // eslint-disable-next-line + infiniteLoaderRef.current = list; + }} + > + {Item} + + )} + + ); +}; + +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 ( + + + + ); + } +); diff --git a/src/common/modals/AddInstance/TechnicModpacks/index.js b/src/common/modals/AddInstance/TechnicModpacks/index.js new file mode 100644 index 000000000..448ab97ee --- /dev/null +++ b/src/common/modals/AddInstance/TechnicModpacks/index.js @@ -0,0 +1,209 @@ +import React, { useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; +import { Input } from 'antd'; +import { useDebouncedCallback } from 'use-debounce'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faBomb, faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; +import ModpacksListWrapper from './ModpacksListWrapper'; +import { + getTechnicClientBuild, + getTechnicModpackData, + getTechnicSearch, + getTechnicSolderData, + getTechnicSolderMultiple +} from '../../../api'; +import { TECHNIC_SOLDER_API_URL } from '../../../utils/constants'; + +let lastRequest; +const TechnicModpacks = ({ setStep, setVersion, setModpack }) => { + const infiniteLoaderRef = useRef(null); + const [modpacks, setModpacks] = useState([]); + const [loading, setLoading] = useState(true); + const [searchText, setSearchText] = useState(''); + const [error, setError] = useState(false); + + const updateModpacks = useDebouncedCallback(() => { + if (infiniteLoaderRef?.current?.scrollToItem) { + infiniteLoaderRef.current.scrollToItem(0); + } + loadMoreModpacks(true); + }, 250); + + useEffect(async () => { + await getTechnicClientBuild(); + }); + + const loadMoreModpacks = async (reset = false) => { + const reqObj = {}; + lastRequest = reqObj; + if (!loading) { + setLoading(true); + } + if (reset && modpacks.length !== 0) { + setModpacks([]); + } + let data = null; + try { + if (error) { + setError(false); + } + if (searchText) { + const modpackNames = ( + await getTechnicSearch(searchText) + ).data.modpacks.map(m => m.slug); + data = await Promise.all( + modpackNames.map( + async name => (await getTechnicModpackData(name)).data + ) + ); + for (const modpack of data) { + if (modpack.solder) { + try { + const solderResponse = await getTechnicSolderData( + modpack.solder, + 'modpack', + modpack.name + ); + const solderData = solderResponse.data; + modpack.builds = solderData.builds; + modpack.latest = solderData.latest; + modpack.recommended = solderData.recommended; + modpack.homeUrl = solderData.url; + modpack.solderLogo = solderData.logo; + modpack.solderBackground = solderData.background; + } catch (e) { + // Broken solder link + console.log(e); + } + } + } + } else { + const { modpacks: solderModpacks } = ( + await getTechnicSolderMultiple(TECHNIC_SOLDER_API_URL, 'modpack') + ).data; + data = await Promise.all( + Object.values(solderModpacks).map(async modpack => { + return { + ...(await getTechnicModpackData(modpack.name)).data, + builds: modpack.builds, + latest: modpack.latest, + recommended: modpack.recommended, + homeUrl: modpack.url, + solderLogo: modpack.logo, + solderBackground: modpack.background + }; + }) + ); + } + } catch (err) { + setError(err); + return; + } + const newModpacks = reset ? data : [...modpacks, ...data]; + if (lastRequest === reqObj) { + setLoading(false); + setModpacks(newModpacks); + } + }; + + useEffect(() => { + updateModpacks(); + }, [searchText]); + + return ( + + + setSearchText(e.target.value)} + style={{ width: 200 }} + /> + + + {/* eslint-disable-next-line no-nested-ternary */} + {!error ? ( + !loading && modpacks.length === 0 ? ( +
+ +
+ No modpack has been found with the current filters. +
+
+ ) : ( + + {({ height, width }) => ( + + )} + + ) + ) : ( +
+ +
+ An error occurred while loading the modpacks list... +
+
+ )} +
+
+ ); +}; + +export default React.memo(TechnicModpacks); + +const Container = styled.div` + width: 100%; + height: 100%; +`; + +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; +`; diff --git a/src/common/modals/InstanceManager/Modpack.js b/src/common/modals/InstanceManager/Modpack.js index 378b71be5..4f388dfe0 100644 --- a/src/common/modals/InstanceManager/Modpack.js +++ b/src/common/modals/InstanceManager/Modpack.js @@ -1,22 +1,26 @@ -import React, { useState, useEffect, memo } from 'react'; +import React, { memo, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { Select, Button } from 'antd'; +import { Button, Select } from 'antd'; import { useDispatch, useSelector } from 'react-redux'; import ReactHtmlParser from 'react-html-parser'; import path from 'path'; import { - getAddonFiles, getAddonFileChangelog, - getFTBModpackData, + getAddonFiles, getFTBChangelog, - getFTBModpackVersionData + getFTBModpackData, + getFTBModpackVersionData, + getTechnicChangelog, + getTechnicModpackData, + getTechnicSolderData } from '../../api'; import { changeModpackVersion } from '../../reducers/actions'; import { closeModal } from '../../reducers/modals/actions'; +import { TECHNIC, TECHNIC_SOLDER } from '../../utils/constants'; import { _getInstancesPath, _getTempPath } from '../../utils/selectors'; import { makeInstanceRestorePoint } from '../../utils'; -const Modpack = ({ modpackId, instanceName, manifest, fileID }) => { +const Modpack = ({ modpackId, instanceName, manifest, fileID, loader }) => { const [files, setFiles] = useState([]); const [versionName, setVersionName] = useState(null); const [selectedIndex, setSelectedIndex] = useState(null); @@ -52,6 +56,82 @@ const Modpack = ({ modpackId, instanceName, manifest, fileID }) => { }) ); setFiles(mappedFiles); + } else if (loader?.source === TECHNIC_SOLDER) { + const solderModpack = ( + await getTechnicSolderData(loader.solder, 'modpack', modpackId) + ).data; + // Necessary to fetch data from technic api as well + const technicModpack = (await getTechnicModpackData(modpackId)).data; + setVersionName(`${solderModpack.display_name} - ${fileID}`); + setFiles( + await Promise.all( + solderModpack.builds + .slice(0) + .reverse() + .map(async b => { + const versionData = ( + await getTechnicSolderData( + loader.solder, + 'modpack', + modpackId, + b + ) + ).data; + let changelog = technicModpack.feed.filter( + f => f.content.replace('Updated to version ', '') === b + ); + changelog = changelog.length > 0 ? changelog[0] : ''; + let changelogData = ''; + if (changelog) { + changelogData = await getTechnicChangelog(changelog.url); + const tempEl = document.createElement('div'); + tempEl.innerHTML = changelogData; + changelogData = tempEl.querySelector('.changelog').innerHTML; + tempEl.remove(); + } + return { + releaseType: 1, + gameVersions: [versionData?.minecraft], + displayName: `${solderModpack.display_name} ${b}`, + id: b, + fileDate: changelog?.date ? changelog.date * 1000 : 'Unknown', + changelog: changelogData + }; + }) + ) + ); + } else if (loader?.source === TECHNIC) { + const technicModpack = (await getTechnicModpackData(modpackId)).data; + let changelog = technicModpack.feed.filter( + f => + f.content.replace('Updated to version ', '') === + technicModpack.version + ); + changelog = changelog.length > 0 ? changelog[0] : null; + let changelogData = ''; + if (changelog) { + changelogData = await getTechnicChangelog(changelog.url); + const tempEl = document.createElement('div'); + tempEl.innerHTML = changelogData; + changelogData = tempEl.querySelector('.changelog').innerHTML; + tempEl.remove(); + } + setVersionName( + `${technicModpack.displayName} - ${technicModpack.version}` + ); + setFiles([ + { + releaseType: 1, + gameVersions: [technicModpack.minecraft], + displayName: `${technicModpack.displayName} ${technicModpack.version}`, + id: technicModpack.version, + fileDate: technicModpack.feed?.[0]?.date + ? technicModpack.feed[0].date * 1000 + : 'Unknown', + url: technicModpack.url, + changelog: changelogData + } + ]); } else { const ftbModpack = await getFTBModpackData(modpackId); @@ -237,17 +317,21 @@ 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 { @@ -271,18 +355,22 @@ const Changelog = styled.div` 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; diff --git a/src/common/modals/InstanceManager/index.js b/src/common/modals/InstanceManager/index.js index 4e4dfa7c7..ec56b02d9 100644 --- a/src/common/modals/InstanceManager/index.js +++ b/src/common/modals/InstanceManager/index.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, lazy } from 'react'; +import React, { lazy, useEffect, useState } from 'react'; import styled, { keyframes } from 'styled-components'; import { Button } from 'antd'; import fse from 'fs-extra'; @@ -13,11 +13,11 @@ import { } from '@fortawesome/free-solid-svg-icons'; import omit from 'lodash/omit'; import psTree from 'ps-tree'; -import { useSelector, useDispatch } from 'react-redux'; +import { useDispatch, useSelector } 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 { CURSEFORGE, FABRIC, FORGE } from '../../utils/constants'; import { addStartedInstance, clearLatestModManifests, @@ -422,6 +422,7 @@ const InstanceManager = ({ instanceName }) => { { @@ -19,10 +24,30 @@ const ModChangelog = ({ modpackId, files, type, modpackName }) => { setLoading(true); let data; try { - if (type === 'ftb') { - data = await getFTBChangelog(modpackId, id); - } else { - data = await getAddonFileChangelog(modpackId, id); + switch (type) { + case CURSEFORGE: + data = await getAddonFileChangelog(modpackId, id); + break; + case FTB: + data = await getFTBChangelog(modpackId, id); + break; + case TECHNIC: + case TECHNIC_SOLDER: { + const file = files.find(f => f.id === id); + if (file === null) { + data = ''; + break; + } + data = await getTechnicChangelog(file.url); + const tempEl = document.createElement('div'); + tempEl.innerHTML = data; + data = tempEl.querySelector('.changelog').innerHTML; + tempEl.remove(); + break; + } + default: + data = ''; + break; } } catch (err) { console.error(err); @@ -45,6 +70,32 @@ const ModChangelog = ({ modpackId, files, type, modpackName }) => { } }; + const getChangelogData = () => { + let changelogData; + switch (type) { + case CURSEFORGE: + case TECHNIC: + case TECHNIC_SOLDER: + changelogData = ReactHtmlParser(changelog); + break; + case FTB: + changelogData = ( + + {changelog.content} + + ); + break; + default: + changelogData = changelog; + } + return changelogData; + }; + return ( { }` : (files || []).find(v => v.id === selectedId)?.displayName}
- {type === 'ftb' ? ( - - {changelog.content} - - ) : ( - ReactHtmlParser(changelog) - )} + {getChangelogData()} ) : (

f.content.replace("Updated to version ", "") === modpack.version); + changelog = changelog.length > 0 ? changelog[0] : null; + setFiles([ + { + releaseType: 1, + gameVersion: [modpack.minecraft], + displayName: `${modpack.displayName} ${modpack.version}`, + id: modpack.version, + fileDate: modpack.feed?.[0]?.date * 1000, + changelog: changelog, + } + ]); + setLoading(false); + } else if (type === TECHNIC_SOLDER) { + setDescription(modpack.description); + setFiles( + await Promise.all( + modpack.builds + .slice() + .reverse() + .map(async b => { + const versionData = ( + await getTechnicSolderData( + modpack.solder, + 'modpack', + modpack.name, + b + ) + ).data; + let changelog = modpack.feed.filter(f => f.content.replace("Updated to version ", "") === b); + changelog = changelog.length > 0 ? changelog[0] : null; + return { + releaseType: 1, + gameVersion: [versionData?.minecraft], + displayName: `${modpack.displayName} ${b}`, + id: b, + fileDate: changelog?.date ? changelog.date * 1000 : 'Unknown', + changelog: changelog, + }; + }) + ) + ); + setLoading(false); } }; init(); @@ -110,13 +154,74 @@ const ModpackDescription = ({ const primaryImage = useMemo(() => { if (type === 'curseforge') { return modpack.logo.thumbnailUrl; - } else if (type === 'ftb') { + } + if (type === 'ftb') { const image = modpack.art.reduce((prev, curr) => { if (!prev || curr.size < prev.size) return curr; return prev; }); return image.url; } + if (type === TECHNIC) { + return modpack.background?.url; + } + if (type === TECHNIC_SOLDER) { + return modpack.background?.url; + } + }, [modpack, type]); + + const downloads = useMemo(() => { + switch (type) { + case 'ftb': + case TECHNIC: + case TECHNIC_SOLDER: + return modpack.installs; + case 'curseforge': + return modpack.downloadCount; + } + }, [modpack, type]); + + const lastUpdate = useMemo(() => { + switch (type) { + case 'ftb': + return formatDate(modpack.refreshed * 1000); + case 'curseforge': + return formatDate(modpack.dateModified); + case TECHNIC: + case TECHNIC_SOLDER: + return (modpack.feed?.length > 0 && modpack.feed[0]?.date !== null) + ? formatDate(modpack.feed?.[0]?.date * 1000) + : 'Unknown'; + } + }, [modpack, type]); + + const mcVersion = useMemo(() => { + switch (type) { + case 'ftb': + return modpack.tags[0]?.name || '-'; + case 'curseforge': + return modpack.latestFilesIndexes[0].gameVersion; + case TECHNIC: + return modpack.minecraft; + case TECHNIC_SOLDER: + return modpack.name === 'vanilla' ? modpack.latest : modpack.minecraft; + } + }, [modpack, type]); + + const modpackUrl = useMemo(() => { + switch (type) { + case 'ftb': + return parseLink(modpack.name); + case 'curseforge': + return modpack.websiteUrl; + case TECHNIC: + case TECHNIC_SOLDER: + return ( + modpack.homeUrl ?? + modpack.platformUrl ?? + `https://www.technicpack.net/modpack/${modpack.name}` + ); + } }, [modpack, type]); return ( @@ -136,37 +241,31 @@ const ModpackDescription = ({ - {modpack.name} + {type === TECHNIC || type === TECHNIC_SOLDER + ? modpack.displayName + : modpack.name}
- {modpack.authors[0].name} + {type === TECHNIC || type === TECHNIC_SOLDER + ? modpack.user + : modpack.authors[0].name}
- {type === 'ftb' - ? formatNumber(modpack.installs) - : formatNumber(modpack.downloadCount)} + {formatNumber(downloads)}
- {type === 'ftb' - ? formatDate(modpack.refreshed * 1000) - : formatDate(modpack.dateModified)} + {lastUpdate}
- {type === 'ftb' - ? modpack.tags[0]?.name || '-' - : modpack.latestFilesIndexes[0].gameVersion} + {mcVersion}