Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Add DLCs manager for Epic Games #2734

Merged
merged 23 commits into from
May 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
99254ea
chore: update types
flavioislima May 17, 2023
a5ddbe9
fix: types
flavioislima May 21, 2023
7b5f550
feat: add DLC list component
flavioislima May 21, 2023
02e3dbc
feat: update uninstall modal
flavioislima May 21, 2023
24de881
fix_ possibly undefined
flavioislima May 21, 2023
43793fd
feat: add install DLCs method
flavioislima May 22, 2023
c45d9ae
feat: add dlc size and cancel button
flavioislima May 27, 2023
dcf4338
fix: lint
flavioislima May 27, 2023
80b8ecb
i18n: updated keys
flavioislima May 27, 2023
43855c0
UI: fixes and no DLC found message
flavioislima May 27, 2023
3f302a0
i18n: keys
flavioislima May 27, 2023
2bc09a5
feat: add DLCs selector on install dialog
flavioislima May 27, 2023
a5b3afa
feat: add Dlcs to the download queue after main game
flavioislima May 27, 2023
96a37dc
i18n: updated keys
flavioislima May 27, 2023
ef18ba9
fix: remove dlcs from the queue if main game was removed
flavioislima May 27, 2023
24e84e4
fix: pr comments
flavioislima May 27, 2023
38c67e2
Update src/backend/downloadmanager/downloadqueue.ts
flavioislima May 27, 2023
a29faf7
fix: hide DLCs submenu if game is not installed
flavioislima May 28, 2023
8ec5d0c
Merge branch 'feat/epic_dlcs' of github.com:Heroic-Games-Launcher/Her…
flavioislima May 28, 2023
99b9d04
fix: do not pass with-dlcs flag to legendary anymore
flavioislima May 28, 2023
c0eb681
ui: use svg icons to keep consistency
flavioislima May 28, 2023
02c2996
i18n: update keys
flavioislima May 28, 2023
a409366
fix: don't navigate to dlc gamepage from queue
flavioislima May 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}
}