Skip to content

Commit

Permalink
[Feat] Add DLCs manager for Epic Games (#2734)
Browse files Browse the repository at this point in the history
* chore: update types

* fix: types

* feat: add DLC list component

* feat: update uninstall modal

* fix_ possibly undefined

* feat: add install DLCs method

* feat: add dlc size and cancel button

* fix: lint

* i18n: updated keys

* UI: fixes and no DLC found message

* i18n: keys

* feat: add DLCs selector on install dialog

* feat: add Dlcs to the download queue after main game

* i18n: updated keys

* fix: remove dlcs from the queue if main game was removed

* fix: pr comments

* Update src/backend/downloadmanager/downloadqueue.ts

Co-authored-by: Mathis Dröge <mathis.droege@ewe.net>

* fix: hide DLCs submenu if game is not installed

* fix: do not pass with-dlcs flag to legendary anymore

* ui: use svg icons to keep consistency

* i18n: update keys

* fix: don't navigate to dlc gamepage from queue

---------

Co-authored-by: Mathis Dröge <mathis.droege@ewe.net>
  • Loading branch information
flavioislima and CommandMC committed May 29, 2023
1 parent 1bb0de2 commit 0ae766a
Show file tree
Hide file tree
Showing 23 changed files with 493 additions and 67 deletions.
2 changes: 2 additions & 0 deletions public/locales/en/gamepage.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
},
"uninstall": {
"checkbox": "Remove prefix: {{prefix}}{{newLine}}Note: This can't be undone and will also remove not backed up save files.",
"dlc": "Do you want to Uninstall this DLC?",
"message": "Do you want to uninstall this game?",
"prefix_warning": "The Wine prefix for this game is the default prefix. If you really want to delete it, you have to do it manually.",
"settingcheckbox": "Erase settings and remove log{{newLine}}Note: This can't be undone. Any modified settings will be forgotten and log will be deleted.",
Expand Down Expand Up @@ -78,6 +79,7 @@
},
"enabled": "Enabled",
"game": {
"dlcs": "DLCs",
"downloadSize": "Download Size",
"firstPlayed": "First Played",
"getting-download-size": "Getting download size",
Expand Down
7 changes: 7 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@
"update_game": "Update game"
}
},
"dlc": {
"actions": "Actions",
"installDlcs": "Install all DLCs",
"noDlcFound": "No DLCs found",
"size": "Size",
"title": "Title"
},
"docs": "Documentation",
"download-manager": {
"ETA": "Estimated time",
Expand Down
22 changes: 21 additions & 1 deletion src/backend/api/downloadmanager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,27 @@ export const install = async (args: InstallParams) => {
endTime: 0,
startTime: 0
}
ipcRenderer.invoke('addToDMQueue', dmQueueElement)

await ipcRenderer.invoke('addToDMQueue', dmQueueElement)

// Add Dlcs to the queue
if (Array.isArray(args.installDlcs) && args.installDlcs.length > 0) {
args.installDlcs.forEach(async (dlc) => {
const dlcArgs: InstallParams = {
...args,
appName: dlc,
installDlcs: false
}
const dlcQueueElement: DMQueueElement = {
params: dlcArgs,
type: 'install',
addToQueueTime: Date.now(),
endTime: 0,
startTime: 0
}
await ipcRenderer.invoke('addToDMQueue', dlcQueueElement)
})
}
}

export const updateGame = (args: UpdateParams) => {
Expand Down
6 changes: 6 additions & 0 deletions src/backend/downloadmanager/downloadqueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@ function getQueueInformation() {

function cancelCurrentDownload({ removeDownloaded = false }) {
if (currentElement) {
if (Array.isArray(currentElement.params.installDlcs)) {
const dlcsToRemove = currentElement.params.installDlcs
for (const dlc of dlcsToRemove) {
removeFromQueue(dlc)
}
}
if (isRunning()) {
stopCurrentDownload()
}
Expand Down
2 changes: 1 addition & 1 deletion src/backend/storeManagers/gog/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ export async function install(
is_dlc: false,
version: additionalInfo ? additionalInfo.version : installInfo.game.version,
appName: appName,
installedWithDLCs: installDlcs,
installedWithDLCs: Boolean(installDlcs),
language: installLanguage,
versionEtag: isLinuxNative ? '' : installInfo.manifest.versionEtag,
buildId: isLinuxNative ? '' : installInfo.game.buildId
Expand Down
5 changes: 2 additions & 3 deletions src/backend/storeManagers/legendary/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ function getSdlList(sdlList: Array<string>) {
*/
export async function install(
appName: string,
{ path, installDlcs, sdlList, platformToInstall }: InstallArgs
{ path, sdlList, platformToInstall }: InstallArgs
): Promise<{
status: 'done' | 'error' | 'abort'
error?: string
Expand All @@ -559,7 +559,6 @@ export async function install(
const info = await getInstallInfo(appName, platformToInstall)
const workers = maxWorkers ? ['--max-workers', `${maxWorkers}`] : []
const noHttps = downloadNoHttps ? ['--no-https'] : []
const withDlcs = installDlcs ? '--with-dlcs' : '--skip-dlcs'
const installSdl = sdlList?.length ? getSdlList(sdlList) : ['--skip-sdl']

const logPath = join(gamesConfigPath, appName + '.log')
Expand All @@ -571,7 +570,7 @@ export async function install(
platformToInstall,
'--base-path',
path,
withDlcs,
'--skip-dlcs',
...installSdl,
...workers,
...noHttps,
Expand Down
1 change: 1 addition & 0 deletions src/backend/storeManagers/legendary/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,7 @@ function loadFile(fileName: string): boolean {
reqs: [],
storeUrl: formatEpicStoreUrl(title)
},
dlcList: dlcItemList,
folder_name: installFolder,
install: {
executable,
Expand Down
5 changes: 1 addition & 4 deletions src/common/typedefs/ipcBridge.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,7 @@ interface AsyncIPCFunctions {
showUpdateSetting: () => boolean
getLatestReleases: () => Promise<Release[]>
getCurrentChangelog: () => Promise<Release | null>
getGameInfo: (
appName: string,
runner: Runner
) => Promise<GameInfo | SideloadGame | null>
getGameInfo: (appName: string, runner: Runner) => Promise<GameInfo | null>
getExtraInfo: (appName: string, runner: Runner) => Promise<ExtraInfo | null>
getGameSettings: (
appName: string,
Expand Down
5 changes: 3 additions & 2 deletions src/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GOGCloudSavesLocation, GogInstallPlatform } from './types/gog'
import { LegendaryInstallPlatform } from './types/legendary'
import { LegendaryInstallPlatform, GameMetadataInner } from './types/legendary'
import { IpcRendererEvent } from 'electron'
import { ChildProcess } from 'child_process'
import { HowLongToBeatEntry } from 'howlongtobeat'
Expand Down Expand Up @@ -122,6 +122,7 @@ export interface GameInfo {
description?: string
//used for store release versions. if remote !== local, then update
version?: string
dlcList?: GameMetadataInner[]
}

export interface GameSettings {
Expand Down Expand Up @@ -234,7 +235,7 @@ export interface WineInstallation {
export interface InstallArgs {
path: string
platformToInstall: InstallPlatform
installDlcs?: boolean
installDlcs?: Array<string> | boolean
sdlList?: string[]
installLanguage?: string
}
Expand Down
5 changes: 3 additions & 2 deletions src/common/types/legendary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ interface AssetInfo {
namespace: string
}

interface GameMetadataInner {
export interface GameMetadataInner {
// TODO: So far every age gating has been {}
ageGatings: Record<string, unknown>
applicationId: string
Expand Down Expand Up @@ -142,9 +142,10 @@ interface GameInstallInfo {
version: string
}

interface DLCInfo {
export interface DLCInfo {
app_name: string
title: string
is_installed?: boolean
}

interface GameManifest {
Expand Down
35 changes: 35 additions & 0 deletions src/frontend/components/UI/DLCList/DLC/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.dlcItem {
display: grid;
grid-template-columns: 3fr 1fr 1fr;
grid-template-rows: auto;
grid-gap: var(--space-md-fixed);
grid-template-areas: 'title size action';
justify-content: space-between;
align-items: center;
padding: 1rem 0;
border-bottom: 1px solid #ccc;

&:last-child {
border-bottom: none;
}
.title {
grid-area: title;
font-weight: 400;
}
.size {
grid-area: size;
font-weight: 400;
text-align: center;
}
.action {
grid-area: action;
font-weight: 400;
text-align: center;
cursor: pointer;
color: var(--primary);
transition: color 0.2s ease-in-out;
&:hover {
color: var(--secondary);
}
}
}
141 changes: 141 additions & 0 deletions src/frontend/components/UI/DLCList/DLC/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React, { useContext, useEffect, useState } from 'react'
import { DLCInfo } from 'common/types/legendary'
import './index.scss'
import { useTranslation } from 'react-i18next'
import { getGameInfo, getInstallInfo, install, size } from 'frontend/helpers'
import { GameInfo, Runner } from 'common/types'
import UninstallModal from 'frontend/components/UI/UninstallModal'
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import ContextProvider from 'frontend/state/ContextProvider'
import { hasProgress } from 'frontend/hooks/hasProgress'
import { ReactComponent as DownIcon } from 'frontend/assets/down-icon.svg'
import { ReactComponent as StopIcon } from 'frontend/assets/stop-icon.svg'
import { ReactComponent as StopIconAlt } from 'frontend/assets/stop-icon-alt.svg'
import SvgButton from '../../SvgButton'

type Props = {
dlc: DLCInfo
runner: Runner
mainAppInfo: GameInfo
onClose: () => void
}

const DLC = ({ dlc, runner, mainAppInfo, onClose }: Props) => {
const { title, app_name } = dlc
const { libraryStatus, showDialogModal } = useContext(ContextProvider)
const { t } = useTranslation('gamepage')
const [showUninstallModal, setShowUninstallModal] = useState(false)
const [dlcInfo, setDlcInfo] = useState<GameInfo | null>(null)
const [dlcSize, setDlcSize] = useState<number>(0)
const [refreshing, setRefreshing] = useState(true)
const [progress] = hasProgress(app_name)

const isInstalled = dlcInfo?.is_installed

useEffect(() => {
const checkInstalled = async () => {
const info = await getGameInfo(app_name, runner)
if (!info) {
return
}
setDlcInfo(info)
}
checkInstalled()
}, [dlc, runner])

useEffect(() => {
setRefreshing(true)
const getDlcSize = async () => {
if (!mainAppInfo.install.platform) {
return
}
const info = await getInstallInfo(
app_name,
runner,
mainAppInfo.install.platform
)
if (!info) {
return
}
setDlcSize(info.manifest.download_size)
setRefreshing(false)
}
getDlcSize()
}, [dlc, runner])

const currentApp = libraryStatus.find((app) => app.appName === app_name)
const isInstalling = currentApp?.status === 'installing'
const showInstallButton = !isInstalling && !refreshing

function mainAction() {
if (isInstalled) {
setShowUninstallModal(true)
} else {
const {
install: { platform, install_path }
} = mainAppInfo

if (!dlcInfo || !platform || !install_path) {
return
}
onClose()
install({
isInstalling,
previousProgress: null,
progress,
showDialogModal,
t,
installPath: install_path,
gameInfo: dlcInfo,
platformToInstall: platform
})
}
}

return (
<>
{showUninstallModal && (
<UninstallModal
appName={app_name}
runner={runner}
onClose={() => setShowUninstallModal(false)}
isDlc
/>
)}
<div className="dlcItem">
<span className="title">{title}</span>
{refreshing ? '...' : <span className="size">{size(dlcSize)}</span>}
{showInstallButton && (
<SvgButton
className="action"
onClick={() => mainAction()}
title={`${
isInstalled
? t('button.uninstall', 'Uninstall')
: t('button.install', 'Install')
} (${title})`}
>
{isInstalled ? <StopIcon /> : <DownIcon />}
</SvgButton>
)}
{isInstalling && (
<SvgButton
className="action"
onClick={() => mainAction()}
title={`${t('button.cancel', 'Cancel')} (${title})`}
>
<StopIconAlt />
</SvgButton>
)}
{refreshing && (
<span className="action">
<FontAwesomeIcon className={'fa-spin-pulse'} icon={faSpinner} />
</span>
)}
</div>
</>
)
}

export default React.memo(DLC)
22 changes: 22 additions & 0 deletions src/frontend/components/UI/DLCList/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.dlcHeader {
display: grid;
grid-template-columns: 3fr 1fr 1fr;
grid-template-rows: auto;
grid-gap: 10px;
font-weight: 700;
color: var(--text-default);

.title {
grid-column: 1 / 2;
}

.size {
grid-column: 2 / 3;
text-align: center;
}

.actions {
grid-column: 3 / 4;
text-align: center;
}
}

0 comments on commit 0ae766a

Please sign in to comment.