From 060fb9c74ecd24b31503a42adeca8d0e0f6040c1 Mon Sep 17 00:00:00 2001 From: Ariel Juodziukynas Date: Sun, 1 Oct 2023 15:00:19 -0300 Subject: [PATCH 1/7] Add a widget to install components and fonts with winetricks witout its GUI --- src/backend/api/wine.ts | 31 +++- src/backend/main.ts | 31 +--- src/backend/tools.ts | 167 +++++++++++++++--- src/common/typedefs/ipcBridge.d.ts | 13 ++ src/frontend/components/UI/Header/index.tsx | 4 +- .../components/UI/LibrarySearchBar/index.tsx | 67 +++++++ .../components/UI/ProgressDialog/index.tsx | 5 +- .../components/UI/SearchBar/index.tsx | 88 +++------ .../UI/Winetricks/WinetricksSearch/index.tsx | 61 +++++++ .../components/UI/Winetricks/index.scss | 46 +++++ .../components/UI/Winetricks/index.tsx | 156 ++++++++++++++++ src/frontend/components/UI/index.tsx | 1 + .../Settings/components/Tools/index.tsx | 52 ++---- 13 files changed, 560 insertions(+), 162 deletions(-) create mode 100644 src/frontend/components/UI/LibrarySearchBar/index.tsx create mode 100644 src/frontend/components/UI/Winetricks/WinetricksSearch/index.tsx create mode 100644 src/frontend/components/UI/Winetricks/index.scss create mode 100644 src/frontend/components/UI/Winetricks/index.tsx diff --git a/src/backend/api/wine.ts b/src/backend/api/wine.ts index e53a119c5e..51166ab4f9 100644 --- a/src/backend/api/wine.ts +++ b/src/backend/api/wine.ts @@ -4,7 +4,8 @@ import { ToolArgs, WineVersionInfo, ProgressInfo, - State + State, + Runner } from 'common/types' export const toggleDXVK = async (args: ToolArgs) => @@ -67,3 +68,31 @@ export const handleWineVersionsUpdated = ( ipcRenderer.removeListener('wineVersionsUpdated', callback) } } + +export const winetricksListInstalled = async ( + runner: Runner, + appName: string +): Promise => + ipcRenderer.invoke('winetricksInstalled', { runner, appName }) + +export const winetricksListAvailable = async ( + runner: Runner, + appName: string +): Promise => + ipcRenderer.invoke('winetricksAvailable', { runner, appName }) + +export const winetricksInstall = async ( + runner: Runner, + appName: string, + component: string +): Promise => + ipcRenderer.send('winetricksInstall', { runner, appName, component }) + +export const handleWinetricksInstalling = ( + callback: (e: Electron.IpcRendererEvent, component: string) => void +): (() => void) => { + ipcRenderer.on('installing-winetricks-component', callback) + return () => { + ipcRenderer.removeListener('installing-winetricks-component', callback) + } +} diff --git a/src/backend/main.ts b/src/backend/main.ts index 54be864db9..6d5f2deef5 100644 --- a/src/backend/main.ts +++ b/src/backend/main.ts @@ -591,7 +591,7 @@ ipcMain.on('removeFolder', async (e, [path, folderName]) => { removeFolder(path, folderName) }) -async function runWineCommandOnGame( +export async function runWineCommandOnGame( runner: Runner, appName: string, { commandParts, wait = false, protonVerb, startFolder }: WineCommandArgs @@ -616,35 +616,6 @@ async function runWineCommandOnGame( }) } -// Calls WineCFG or Winetricks. If is WineCFG, use the same binary as wine to launch it to dont update the prefix -ipcMain.handle('callTool', async (event, { tool, exe, appName, runner }) => { - const gameSettings = await gameManagerMap[runner].getSettings(appName) - - switch (tool) { - case 'winetricks': - await Winetricks.run(runner, appName, event) - break - case 'winecfg': - runWineCommandOnGame(runner, appName, { - gameSettings, - commandParts: ['winecfg'], - wait: false - }) - break - case 'runExe': - if (exe) { - const workingDir = path.parse(exe).dir - runWineCommandOnGame(runner, appName, { - gameSettings, - commandParts: [exe], - wait: false, - startFolder: workingDir - }) - } - break - } -}) - ipcMain.handle('runWineCommand', async (e, args) => runWineCommand(args)) /// IPC handlers begin here. diff --git a/src/backend/tools.ts b/src/backend/tools.ts index 0045f2db30..5a3cd2506b 100644 --- a/src/backend/tools.ts +++ b/src/backend/tools.ts @@ -21,7 +21,7 @@ import { } from './constants' import { logError, logInfo, LogPrefix, logWarning } from './logger/logger' import i18next from 'i18next' -import { dirname, join } from 'path' +import path, { dirname, join } from 'path' import { isOnline } from './online_monitor' import { showDialogBoxModalAuto } from './dialog/dialog' import { @@ -38,6 +38,9 @@ import { } from './utils/graphics/vulkan' import { lt as semverLt } from 'semver' import { gameManagerMap } from './storeManagers' +import { ipcMain } from 'electron' +import { runWineCommandOnGame } from './main' +import { sendFrontendMessage } from './main_window' export const DXVK = { getLatest: async () => { @@ -429,7 +432,7 @@ export const Winetricks = { runner: Runner, appName: string, args: string[], - event?: Electron.IpcMainInvokeEvent + returnOutput = false ) => { const gameSettings = await gameManagerMap[runner].getSettings(appName) @@ -445,7 +448,7 @@ export const Winetricks = { await Winetricks.download() } - return new Promise((resolve) => { + return new Promise((resolve) => { const { winePrefix, wineBin } = getWineFromProton( wineVersion, baseWinePrefix @@ -487,14 +490,12 @@ export const Winetricks = { executeMessages.push(message) progressUpdated = true } - const sendProgress = - event && - setInterval(() => { - if (progressUpdated) { - event.sender.send('progressOfWinetricks', executeMessages) - progressUpdated = false - } - }, 1000) + const sendProgress = setInterval(() => { + if (progressUpdated) { + sendFrontendMessage('progressOfWinetricks', executeMessages) + progressUpdated = false + } + }, 1000) // check if winetricks dependencies are installed const dependencies = ['7z', 'cabextract', 'zenity', 'unzip', 'curl'] @@ -515,16 +516,24 @@ export const Winetricks = { }) logInfo( - `Running WINEPREFIX='${winePrefix}' PATH='${winepath}':$PATH ${winetricks} --force -q`, + `Running WINEPREFIX='${winePrefix}' PATH='${winepath}':$PATH ${winetricks} ${args.join( + ' ' + )}`, LogPrefix.WineTricks ) const child = spawn(winetricks, args, { env: envs }) + const output: string[] = [] + child.stdout.setEncoding('utf8') child.stdout.on('data', (data: string) => { - logInfo(data, LogPrefix.WineTricks) - appendMessage(data) + if (returnOutput) { + output.push(data) + } else { + appendMessage(data) + logInfo(data, LogPrefix.WineTricks) + } }) child.stderr.setEncoding('utf8') @@ -536,7 +545,6 @@ export const Winetricks = { child.on('error', (error) => { logError(['Winetricks threw Error:', error], LogPrefix.WineTricks) showDialogBoxModalAuto({ - event, title: i18next.t('box.error.winetricks.title', 'Winetricks error'), message: i18next.t('box.error.winetricks.message', { defaultValue: @@ -547,26 +555,78 @@ export const Winetricks = { type: 'ERROR' }) clearInterval(sendProgress) - resolve() + resolve(returnOutput ? output : null) }) child.on('exit', () => { + sendFrontendMessage('progressOfWinetricks', ['Done']) clearInterval(sendProgress) - resolve() + resolve(returnOutput ? output : null) }) child.on('close', () => { clearInterval(sendProgress) - resolve() + resolve(returnOutput ? output : null) }) }) }, - run: async ( - runner: Runner, - appName: string, - event: Electron.IpcMainInvokeEvent - ) => { - await Winetricks.runWithArgs(runner, appName, ['--force', '-q'], event) + run: async (runner: Runner, appName: string) => { + await Winetricks.runWithArgs(runner, appName, ['--force', '-q']) + }, + listAvailable: async (runner: Runner, appName: string) => { + try { + const dlls: string[] = [] + const outputDlls = await Winetricks.runWithArgs( + runner, + appName, + ['dlls', 'list'], + true + ) + if (outputDlls) { + outputDlls.forEach((component: string) => + dlls.push(component.split(' ', 1)[0]) + ) + } + + const fonts: string[] = [] + const outputFonts = await Winetricks.runWithArgs( + runner, + appName, + ['fonts', 'list'], + true + ) + if (outputFonts) { + outputFonts.forEach((font: string) => fonts.push(font.split(' ', 1)[0])) + } + return [...dlls, ...fonts] + } catch { + return [] + } + }, + listInstalled: async (runner: Runner, appName: string) => { + try { + const output = await Winetricks.runWithArgs( + runner, + appName, + ['list-installed'], + true + ) + if (!output) { + return [] + } else { + const last = output.pop() || '' + if ( + last === '' || + last.match('winetricks has not installed anything') + ) { + return [] + } else { + return last.split('\n').filter((component) => component.trim() !== '') + } + } + } catch { + return [] + } } } @@ -641,3 +701,62 @@ function getVkd3dUrl(): string { // that would also need bigger changes in the frontend return 'https://api.github.com/repos/Heroic-Games-Launcher/vkd3d-proton/releases/latest' } + +// Calls WineCFG or Winetricks. If is WineCFG, use the same binary as wine to launch it to dont update the prefix +ipcMain.handle('callTool', async (event, { tool, exe, appName, runner }) => { + const gameSettings = await gameManagerMap[runner].getSettings(appName) + + switch (tool) { + case 'winetricks': + await Winetricks.run(runner, appName) + break + case 'winecfg': + runWineCommandOnGame(runner, appName, { + gameSettings, + commandParts: ['winecfg'], + wait: false + }) + break + case 'runExe': + if (exe) { + const workingDir = path.parse(exe).dir + runWineCommandOnGame(runner, appName, { + gameSettings, + commandParts: [exe], + wait: false, + startFolder: workingDir + }) + } + break + } +}) + +ipcMain.on( + 'winetricksInstall', + async (event, { runner, appName, component }) => { + sendFrontendMessage('installing-winetricks-component', component) + try { + await Winetricks.runWithArgs(runner, appName, ['-q', component]) + } finally { + sendFrontendMessage('installing-winetricks-component', '') + } + } +) + +ipcMain.handle('winetricksAvailable', async (event, { runner, appName }) => { + try { + const x = await Winetricks.listAvailable(runner, appName) + return x || [] + } catch { + return [] + } +}) + +ipcMain.handle('winetricksInstalled', async (event, { runner, appName }) => { + try { + const x = await Winetricks.listInstalled(runner, appName) + return x || [] + } catch { + return [] + } +}) diff --git a/src/common/typedefs/ipcBridge.d.ts b/src/common/typedefs/ipcBridge.d.ts index e5568cbdee..00ffcb86b9 100644 --- a/src/common/typedefs/ipcBridge.d.ts +++ b/src/common/typedefs/ipcBridge.d.ts @@ -104,6 +104,11 @@ interface SyncIPCFunctions { pauseCurrentDownload: () => void cancelDownload: (removeDownloaded: boolean) => void copySystemInfoToClipboard: () => void + winetricksInstall: ({ + runner: Runner, + appName: string, + component: string + }) => void } // ts-prune-ignore-next @@ -115,6 +120,14 @@ interface AsyncIPCFunctions { runWineCommand: ( args: WineCommandArgs ) => Promise<{ stdout: string; stderr: string }> + winetricksInstalled: ({ + runner: Runner, + appName: string + }) => Promise + winetricksAvailable: ({ + runner: Runner, + appName: string + }) => Promise checkGameUpdates: () => Promise getEpicGamesStatus: () => Promise updateAll: () => Promise<({ status: 'done' | 'error' | 'abort' } | null)[]> diff --git a/src/frontend/components/UI/Header/index.tsx b/src/frontend/components/UI/Header/index.tsx index 2bbe69f9ce..ec1cda1160 100644 --- a/src/frontend/components/UI/Header/index.tsx +++ b/src/frontend/components/UI/Header/index.tsx @@ -1,16 +1,16 @@ import React from 'react' -import { SearchBar } from 'frontend/components/UI' import StoreFilter from 'frontend/components/UI/StoreFilter' import PlatformFilter from '../PlatformFilter' import './index.css' +import LibrarySearchBar from '../LibrarySearchBar' export default function Header() { return ( <>
- +
diff --git a/src/frontend/components/UI/LibrarySearchBar/index.tsx b/src/frontend/components/UI/LibrarySearchBar/index.tsx new file mode 100644 index 0000000000..639d8e1f88 --- /dev/null +++ b/src/frontend/components/UI/LibrarySearchBar/index.tsx @@ -0,0 +1,67 @@ +import React, { useContext, useMemo } from 'react' +import { useNavigate } from 'react-router-dom' +import ContextProvider from 'frontend/state/ContextProvider' +import { GameInfo } from '../../../../common/types' +import SearchBar from '../SearchBar' +import { useTranslation } from 'react-i18next' + +function fixFilter(text: string) { + const regex = new RegExp(/([?\\|*|+|(|)|[|]|])+/, 'g') + return text.replaceAll(regex, '') +} + +const RUNNER_TO_STORE = { + legendary: 'Epic', + gog: 'GOG', + nile: 'Amazon' +} + +export default function LibrarySearchBar() { + const { handleSearch, filterText, epic, gog, sideloadedLibrary, amazon } = + useContext(ContextProvider) + const navigate = useNavigate() + const { t } = useTranslation() + + const list = useMemo(() => { + return [ + ...(epic.library ?? []), + ...(gog.library ?? []), + ...(sideloadedLibrary ?? []), + ...(amazon.library ?? []) + ] + .filter(Boolean) + .filter((el) => { + return ( + !el.install.is_dlc && + new RegExp(fixFilter(filterText), 'i').test(el.title) + ) + }) + .sort((g1, g2) => (g1.title < g2.title ? -1 : 1)) + }, [amazon.library, epic.library, gog.library, filterText]) + + const handleClick = (game: GameInfo) => { + handleSearch('') + navigate(`/gamepage/${game.runner}/${game.app_name}`, { + state: { gameInfo: game } + }) + } + + const suggestions = list.map((game) => ( +
  • handleClick(game)} key={game.app_name}> + {game.title} ({RUNNER_TO_STORE[game.runner] || game.runner}) +
  • + )) + + const onInputChanged = (text: string) => { + handleSearch(text) + } + + return ( + + ) +} diff --git a/src/frontend/components/UI/ProgressDialog/index.tsx b/src/frontend/components/UI/ProgressDialog/index.tsx index b478b9563a..d6909a0fac 100644 --- a/src/frontend/components/UI/ProgressDialog/index.tsx +++ b/src/frontend/components/UI/ProgressDialog/index.tsx @@ -15,6 +15,8 @@ export function ProgressDialog(props: { progress: string[] showCloseButton: boolean onClose: () => void + children?: JSX.Element + className?: string }) { const { t } = useTranslation() const winetricksOutputBottomRef = useRef(null) @@ -52,11 +54,12 @@ export function ProgressDialog(props: {
    {props.title}
    + {props.children}
    {t('progress', 'Progress')}: diff --git a/src/frontend/components/UI/SearchBar/index.tsx b/src/frontend/components/UI/SearchBar/index.tsx index 3e9bfe6226..09ba4a0ab0 100644 --- a/src/frontend/components/UI/SearchBar/index.tsx +++ b/src/frontend/components/UI/SearchBar/index.tsx @@ -1,63 +1,32 @@ import { Search } from '@mui/icons-material' -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useRef -} from 'react' -import { useTranslation } from 'react-i18next' -import { useNavigate } from 'react-router-dom' -import ContextProvider from 'frontend/state/ContextProvider' +import React, { Fragment, useCallback, useEffect, useRef } from 'react' import './index.scss' import { faXmark } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { GameInfo } from '../../../../common/types' -function fixFilter(text: string) { - const regex = new RegExp(/([?\\|*|+|(|)|[|]|])+/, 'g') - return text.replaceAll(regex, '') +interface Props { + suggestionsListItems?: JSX.Element[] + onInputChanged: (text: string) => void + value: string + placeholder: string } -const RUNNER_TO_STORE = { - legendary: 'Epic', - gog: 'GOG', - nile: 'Amazon' -} - -export default React.memo(function SearchBar() { - const { handleSearch, filterText, epic, gog, sideloadedLibrary, amazon } = - useContext(ContextProvider) - const { t } = useTranslation() - const navigate = useNavigate() - +export default function SearchBar({ + suggestionsListItems, + onInputChanged, + value, + placeholder +}: Props) { const input = useRef(null) - const list = useMemo(() => { - return [ - ...(epic.library ?? []), - ...(gog.library ?? []), - ...(sideloadedLibrary ?? []), - ...(amazon.library ?? []) - ] - .filter(Boolean) - .filter((el) => { - return ( - !el.install.is_dlc && - new RegExp(fixFilter(filterText), 'i').test(el.title) - ) - }) - .sort((g1, g2) => (g1.title < g2.title ? -1 : 1)) - }, [amazon.library, epic.library, gog.library, filterText]) - // we have to use an event listener instead of the react // onChange callback so it works with the virtual keyboard useEffect(() => { if (input.current) { const element = input.current - element.value = filterText + element.value = value const handler = () => { - handleSearch(element.value) + onInputChanged(element.value) } element.addEventListener('input', handler) return () => { @@ -68,24 +37,13 @@ export default React.memo(function SearchBar() { }, [input]) const onClear = useCallback(() => { - handleSearch('') + onInputChanged('') if (input.current) { input.current.value = '' input.current.focus() } }, [input]) - const handleClick = (game: GameInfo) => { - handleSearch('') - if (input.current) { - input.current.value = '' - - navigate(`/gamepage/${game.runner}/${game.app_name}`, { - state: { gameInfo: game } - }) - } - } - return (
    @@ -94,22 +52,20 @@ export default React.memo(function SearchBar() { - {filterText.length > 0 && ( + {value.length > 0 && ( <>
      - {list.length > 0 && - list.map((game) => ( -
    • handleClick(game)} key={game.app_name}> - {game.title}{' '} - ({RUNNER_TO_STORE[game.runner] || game.runner}) -
    • + {suggestionsListItems && + suggestionsListItems.length > 0 && + suggestionsListItems.map((li, idx) => ( + {li} ))}
    @@ -120,4 +76,4 @@ export default React.memo(function SearchBar() { )}
    ) -}) +} diff --git a/src/frontend/components/UI/Winetricks/WinetricksSearch/index.tsx b/src/frontend/components/UI/Winetricks/WinetricksSearch/index.tsx new file mode 100644 index 0000000000..7c6fb2041f --- /dev/null +++ b/src/frontend/components/UI/Winetricks/WinetricksSearch/index.tsx @@ -0,0 +1,61 @@ +import React, { useEffect, useState } from 'react' +import SearchBar from '../../SearchBar' +import { useTranslation } from 'react-i18next' + +interface Props { + allComponents: string[] + installed: string[] + onInstallClicked: (component: string) => void +} + +export default function WinetricksSearchBar({ + allComponents, + installed, + onInstallClicked +}: Props) { + const { t } = useTranslation() + + // handles the search input and results list + const [search, setSearch] = useState('') + const onInputChanged = (text: string) => { + setSearch(text) + } + const [searchResults, setSearchResults] = useState([]) + + useEffect(() => { + if (search.length < 2) { + setSearchResults([]) + } else { + let filtered = allComponents.filter((component) => + component.match(search) + ) + filtered = filtered.filter((component) => !installed?.includes(component)) + setSearchResults(filtered) + } + }, [search]) + + const install = (component: string) => { + setSearch('') + onInstallClicked(component) + } + + const suggestions = searchResults.map((component) => { + return ( +
  • + {component} + +
  • + ) + }) + + return ( + + ) +} diff --git a/src/frontend/components/UI/Winetricks/index.scss b/src/frontend/components/UI/Winetricks/index.scss new file mode 100644 index 0000000000..33527890a9 --- /dev/null +++ b/src/frontend/components/UI/Winetricks/index.scss @@ -0,0 +1,46 @@ +.progressDialog.winetricksDialog { + .installedWrapper, + .installWrapper { + margin: 0 var(--dialog-margin-horizontal); + } + + .installedWrapper { + b { + margin-inline-end: 0.5rem; + } + } + + .installWrapper { + margin-block: 1rem; + .actions { + display: grid; + column-gap: 1rem; + grid-template-columns: 1fr 1fr; + align-items: center; + .SearchBar { + grid-area: none; + grid-column: 1 / 3; + grid-row: 1; + ul { + li { + display: flex; + justify-content: space-between; + align-items: center; + cursor: default; + span { + opacity: 1; + } + button { + display: inline; + margin-inline-start: 0.5rem; + font-size: 0.7rem; + } + } + } + } + & > button { + grid-column: -1; + } + } + } +} diff --git a/src/frontend/components/UI/Winetricks/index.tsx b/src/frontend/components/UI/Winetricks/index.tsx new file mode 100644 index 0000000000..648e8d09d7 --- /dev/null +++ b/src/frontend/components/UI/Winetricks/index.tsx @@ -0,0 +1,156 @@ +import React, { useContext, useEffect, useState } from 'react' +import './index.scss' +import { ProgressDialog } from '../ProgressDialog' +import WinetricksSearchBar from './WinetricksSearch' +import { useTranslation } from 'react-i18next' +import SettingsContext from 'frontend/screens/Settings/SettingsContext' + +interface Props { + onClose: () => void +} + +export default function Winetricks({ onClose }: Props) { + const { appName, runner } = useContext(SettingsContext) + const { t } = useTranslation() + + const [loading, setLoading] = useState(true) + + // keep track of all installed components for a game/app + const [installed, setInstalled] = useState([]) + async function listInstalled() { + setLoading(true) + try { + const components = await window.api.winetricksListInstalled( + runner, + appName + ) + setInstalled(components) + } catch { + setInstalled([]) + } + setLoading(false) + } + useEffect(() => { + listInstalled() + }, []) + + const [allComponents, setAllComponents] = useState([]) + useEffect(() => { + async function listComponents() { + try { + const components = await window.api.winetricksListAvailable( + runner, + appName + ) + setAllComponents(components) + } catch { + setAllComponents([]) + } + } + listComponents() + }, []) + + // handles the installation of components + const [installing, setInstalling] = useState(false) + const [logs, setLogs] = useState([]) + function install(component: string) { + window.api.winetricksInstall(runner, appName, component) + } + + useEffect(() => { + async function onInstallingChange( + e: Electron.IpcRendererEvent, + component: string + ) { + if (component === '') { + listInstalled() + } + setInstalling(false) + } + + async function onWinetricksProgress( + e: Electron.IpcRendererEvent, + newLogs: string[] + ) { + if (newLogs[0] && newLogs[0] === 'Done') { + setInstalling(false) + } else if (!installing) { + setInstalling(true) + } + setLogs((currentLogs) => [...currentLogs, ...newLogs]) + } + + const removeListener1 = + window.api.handleProgressOfWinetricks(onWinetricksProgress) + + const removeListener2 = + window.api.handleWinetricksInstalling(onInstallingChange) + + return () => { + removeListener1() + removeListener2() + } + }, []) + + function launchWinetricks() { + window.api.callTool({ + tool: 'winetricks', + appName, + runner + }) + } + + const dialogContent = ( + <> + {!loading && ( +
    + {!installing && ( +
    + + +
    + )} + {installing && ( +

    {t('winetricks.installing', 'Installation in progress')}

    + )} +
    + )} + +
    + {t('winetricks.installed', 'Installed components:')} + {loading && {t('winetricks.loading', 'Loading')}} + {!loading && installed.length === 0 && ( + + {t( + 'winetricks.nothingYet', + 'Nothing was installed by Winetricks yet' + )} + + )} + {!loading && {installed.join(', ')}} +
    + + ) + + return ( + + {dialogContent} + + ) +} diff --git a/src/frontend/components/UI/index.tsx b/src/frontend/components/UI/index.tsx index 3a9a262acd..d9cf9c199e 100644 --- a/src/frontend/components/UI/index.tsx +++ b/src/frontend/components/UI/index.tsx @@ -12,3 +12,4 @@ export { default as ControllerHints } from './ControllerHints' export { default as CachedImage } from './CachedImage' export { default as OfflineMessage } from './OfflineMessage' export { default as PathSelectionBox } from './PathSelectionBox' +export { default as Winetricks } from './Winetricks' diff --git a/src/frontend/screens/Settings/components/Tools/index.tsx b/src/frontend/screens/Settings/components/Tools/index.tsx index 9b2bf70554..9e4c62501d 100644 --- a/src/frontend/screens/Settings/components/Tools/index.tsx +++ b/src/frontend/screens/Settings/components/Tools/index.tsx @@ -1,20 +1,19 @@ import './index.scss' -import React, { useContext, useEffect, useState } from 'react' +import React, { useContext, useState } from 'react' import { useTranslation } from 'react-i18next' import classNames from 'classnames' import { getGameInfo } from 'frontend/helpers' -import { ProgressDialog } from 'frontend/components/UI/ProgressDialog' import SettingsContext from '../../SettingsContext' import ContextProvider from 'frontend/state/ContextProvider' +import { Winetricks } from 'frontend/components/UI' export default function Tools() { const { t } = useTranslation() const [winecfgRunning, setWinecfgRunning] = useState(false) const [winetricksRunning, setWinetricksRunning] = useState(false) - const [progress, setProgress] = useState([]) const { appName, runner, isDefault } = useContext(SettingsContext) const { platform } = useContext(ContextProvider) const isWindows = platform === 'win32' @@ -23,11 +22,8 @@ export default function Tools() { return <> } - type Tool = 'winecfg' | 'winetricks' | string + type Tool = 'winecfg' | string async function callTools(tool: Tool, exe?: string) { - if (tool === 'winetricks') { - setWinetricksRunning(true) - } if (tool === 'winecfg') { setWinecfgRunning(true) } @@ -37,26 +33,9 @@ export default function Tools() { appName, runner }) - setWinetricksRunning(false) setWinecfgRunning(false) } - useEffect(() => { - const onProgress = (e: Electron.IpcRendererEvent, messages: string[]) => { - setProgress(messages) - } - - const removeWinetricksProgressListener = - window.api.handleProgressOfWinetricks(onProgress) - - //useEffect unmount - return removeWinetricksProgressListener - }, []) - - useEffect(() => { - setProgress([]) - }, [winetricksRunning]) - const handleRunExe = async () => { let exe = '' const gameinfo = await getGameInfo(appName, runner) @@ -98,19 +77,18 @@ export default function Tools() { ev.preventDefault() } + function openWinetricksDialog() { + setWinetricksRunning(true) + } + + function winetricksDialogClosed() { + setWinetricksRunning(false) + } + return ( <>
    - {winetricksRunning && ( - { - return - }} - /> - )} + {winetricksRunning && }
    From 3a7e85825273363eb63772fad3969883fb4fb10d Mon Sep 17 00:00:00 2001 From: Ariel Juodziukynas Date: Sun, 1 Oct 2023 15:05:09 -0300 Subject: [PATCH 2/7] Add i18n and some comment --- public/locales/en/translation.json | 9 +++++++++ src/frontend/components/UI/Winetricks/index.tsx | 2 ++ 2 files changed, 11 insertions(+) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 866e161081..26de825976 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -718,5 +718,14 @@ }, "release": "Release Date", "size": "Size" + }, + "winetricks": { + "install": "Install", + "installed": "Installed components:", + "installing": "Installation in progress", + "loading": "Loading", + "nothingYet": "Nothing was installed by Winetricks yet", + "openGUI": "Open Winetricks GUI", + "search": "Search fonts or components" } } diff --git a/src/frontend/components/UI/Winetricks/index.tsx b/src/frontend/components/UI/Winetricks/index.tsx index 648e8d09d7..2c45948fff 100644 --- a/src/frontend/components/UI/Winetricks/index.tsx +++ b/src/frontend/components/UI/Winetricks/index.tsx @@ -72,6 +72,8 @@ export default function Winetricks({ onClose }: Props) { e: Electron.IpcRendererEvent, newLogs: string[] ) { + // this conditionals help to show the correct state if the dialog + // is closed during an installation and then re-opened if (newLogs[0] && newLogs[0] === 'Done') { setInstalling(false) } else if (!installing) { From da596ccee095671b01cc33dcb673080f95faf105 Mon Sep 17 00:00:00 2001 From: Ariel Juodziukynas Date: Sun, 1 Oct 2023 15:32:54 -0300 Subject: [PATCH 3/7] Small refactor and more comments --- src/backend/tools.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/backend/tools.ts b/src/backend/tools.ts index 5a3cd2506b..74ace36c43 100644 --- a/src/backend/tools.ts +++ b/src/backend/tools.ts @@ -583,6 +583,7 @@ export const Winetricks = { true ) if (outputDlls) { + // the output is an array of strings, the first word is the component name outputDlls.forEach((component: string) => dlls.push(component.split(' ', 1)[0]) ) @@ -596,6 +597,7 @@ export const Winetricks = { true ) if (outputFonts) { + // the output is an array of strings, the first word is the font name outputFonts.forEach((font: string) => fonts.push(font.split(' ', 1)[0])) } return [...dlls, ...fonts] @@ -614,6 +616,8 @@ export const Winetricks = { if (!output) { return [] } else { + // the last element of the result is a new-line separated list of installed components + // it can also be a message saying nothing was installed yet const last = output.pop() || '' if ( last === '' || @@ -745,8 +749,7 @@ ipcMain.on( ipcMain.handle('winetricksAvailable', async (event, { runner, appName }) => { try { - const x = await Winetricks.listAvailable(runner, appName) - return x || [] + return await Winetricks.listAvailable(runner, appName) } catch { return [] } @@ -754,8 +757,7 @@ ipcMain.handle('winetricksAvailable', async (event, { runner, appName }) => { ipcMain.handle('winetricksInstalled', async (event, { runner, appName }) => { try { - const x = await Winetricks.listInstalled(runner, appName) - return x || [] + return await Winetricks.listInstalled(runner, appName) } catch { return [] } From 126ae3957c239ccf14a3254b86f7d294266f6b03 Mon Sep 17 00:00:00 2001 From: Ariel Juodziukynas Date: Sun, 1 Oct 2023 16:37:03 -0300 Subject: [PATCH 4/7] Remove SearchBar import/export --- src/frontend/components/UI/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/frontend/components/UI/index.tsx b/src/frontend/components/UI/index.tsx index d9cf9c199e..164853086d 100644 --- a/src/frontend/components/UI/index.tsx +++ b/src/frontend/components/UI/index.tsx @@ -1,7 +1,6 @@ export { default as Header } from './Header' export { default as InfoBox } from './InfoBox' export { default as LanguageSelector } from './LanguageSelector' -export { default as SearchBar } from './SearchBar' export { default as SelectField } from './SelectField' export { default as SmallInfo } from './SmallInfo' export { default as TextInputField } from './TextInputField' From c04f991a8e3e17cfa3f1f319541d8b98f1cc2376 Mon Sep 17 00:00:00 2001 From: Ariel Juodziukynas Date: Sun, 1 Oct 2023 17:02:16 -0300 Subject: [PATCH 5/7] Move function to tools instead of importing from main --- src/backend/main.ts | 46 +------------------------------------------- src/backend/tools.ts | 45 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 47 deletions(-) diff --git a/src/backend/main.ts b/src/backend/main.ts index 6d5f2deef5..464301fc94 100644 --- a/src/backend/main.ts +++ b/src/backend/main.ts @@ -6,8 +6,6 @@ import { DiskSpaceData, StatusPromise, GamepadInputEvent, - WineCommandArgs, - ExecResult, Runner } from 'common/types' import * as path from 'path' @@ -51,7 +49,6 @@ import { GOGUser } from './storeManagers/gog/user' import { NileUser } from './storeManagers/nile/user' import { clearCache, - execAsync, isEpicServiceOffline, handleExit, openUrlOrFile, @@ -109,7 +106,7 @@ import { } from './logger/logger' import { gameInfoStore } from 'backend/storeManagers/legendary/electronStores' import { getFonts } from 'font-list' -import { prepareWineLaunch, runWineCommand } from './launcher' +import { runWineCommand } from './launcher' import shlex from 'shlex' import { initQueue } from './downloadmanager/downloadqueue' import { @@ -591,31 +588,6 @@ ipcMain.on('removeFolder', async (e, [path, folderName]) => { removeFolder(path, folderName) }) -export async function runWineCommandOnGame( - runner: Runner, - appName: string, - { commandParts, wait = false, protonVerb, startFolder }: WineCommandArgs -): Promise { - if (gameManagerMap[runner].isNative(appName)) { - logError('runWineCommand called on native game!', LogPrefix.Gog) - return { stdout: '', stderr: '' } - } - const { folder_name, install } = gameManagerMap[runner].getGameInfo(appName) - const gameSettings = await gameManagerMap[runner].getSettings(appName) - - await prepareWineLaunch(runner, appName) - - return runWineCommand({ - gameSettings, - installFolderName: folder_name, - gameInstallPath: install.install_path, - commandParts, - wait, - protonVerb, - startFolder - }) -} - ipcMain.handle('runWineCommand', async (e, args) => runWineCommand(args)) /// IPC handlers begin here. @@ -1548,22 +1520,6 @@ ipcMain.handle('getFonts', async (event, reload) => { return cachedFonts }) -ipcMain.handle( - 'runWineCommandForGame', - async (event, { appName, commandParts, runner }) => { - if (isWindows) { - return execAsync(commandParts.join(' ')) - } - - // FIXME: Why are we using `runinprefix` here? - return runWineCommandOnGame(runner, appName, { - commandParts, - wait: false, - protonVerb: 'runinprefix' - }) - } -) - ipcMain.handle('getShellPath', async (event, path) => getShellPath(path)) ipcMain.handle('clipboardReadText', () => clipboard.readText()) diff --git a/src/backend/tools.ts b/src/backend/tools.ts index 74ace36c43..213a9e67ae 100644 --- a/src/backend/tools.ts +++ b/src/backend/tools.ts @@ -1,4 +1,4 @@ -import { GameSettings, Runner } from 'common/types' +import { ExecResult, GameSettings, Runner, WineCommandArgs } from 'common/types' import axios from 'axios' import { existsSync, @@ -25,6 +25,7 @@ import path, { dirname, join } from 'path' import { isOnline } from './online_monitor' import { showDialogBoxModalAuto } from './dialog/dialog' import { + prepareWineLaunch, runWineCommand, setupEnvVars, setupWineEnvVars, @@ -39,7 +40,6 @@ import { import { lt as semverLt } from 'semver' import { gameManagerMap } from './storeManagers' import { ipcMain } from 'electron' -import { runWineCommandOnGame } from './main' import { sendFrontendMessage } from './main_window' export const DXVK = { @@ -706,6 +706,47 @@ function getVkd3dUrl(): string { return 'https://api.github.com/repos/Heroic-Games-Launcher/vkd3d-proton/releases/latest' } +ipcMain.handle( + 'runWineCommandForGame', + async (event, { appName, commandParts, runner }) => { + if (isWindows) { + return execAsync(commandParts.join(' ')) + } + + // FIXME: Why are we using `runinprefix` here? + return runWineCommandOnGame(runner, appName, { + commandParts, + wait: false, + protonVerb: 'runinprefix' + }) + } +) + +async function runWineCommandOnGame( + runner: Runner, + appName: string, + { commandParts, wait = false, protonVerb, startFolder }: WineCommandArgs +): Promise { + if (gameManagerMap[runner].isNative(appName)) { + logError('runWineCommand called on native game!', LogPrefix.Gog) + return { stdout: '', stderr: '' } + } + const { folder_name, install } = gameManagerMap[runner].getGameInfo(appName) + const gameSettings = await gameManagerMap[runner].getSettings(appName) + + await prepareWineLaunch(runner, appName) + + return runWineCommand({ + gameSettings, + installFolderName: folder_name, + gameInstallPath: install.install_path, + commandParts, + wait, + protonVerb, + startFolder + }) +} + // Calls WineCFG or Winetricks. If is WineCFG, use the same binary as wine to launch it to dont update the prefix ipcMain.handle('callTool', async (event, { tool, exe, appName, runner }) => { const gameSettings = await gameManagerMap[runner].getSettings(appName) From 1a096d6300167211da7d6e940cb4cb6767ffa348 Mon Sep 17 00:00:00 2001 From: Ariel Juodziukynas Date: Sun, 1 Oct 2023 17:25:38 -0300 Subject: [PATCH 6/7] Move ipc handlers into its own module to fix specs --- src/backend/main.ts | 1 + src/backend/{tools.ts => tools/index.ts} | 96 +++--------------------- src/backend/tools/ipc_handler.ts | 80 ++++++++++++++++++++ 3 files changed, 92 insertions(+), 85 deletions(-) rename src/backend/{tools.ts => tools/index.ts} (89%) create mode 100644 src/backend/tools/ipc_handler.ts diff --git a/src/backend/main.ts b/src/backend/main.ts index 464301fc94..5beae0ebcc 100644 --- a/src/backend/main.ts +++ b/src/backend/main.ts @@ -1624,3 +1624,4 @@ import './downloadmanager/ipc_handler' import './utils/ipc_handler' import './wiki_game_info/ipc_handler' import './recent_games/ipc_handler' +import './tools/ipc_handler' diff --git a/src/backend/tools.ts b/src/backend/tools/index.ts similarity index 89% rename from src/backend/tools.ts rename to src/backend/tools/index.ts index 213a9e67ae..a2dcff331c 100644 --- a/src/backend/tools.ts +++ b/src/backend/tools/index.ts @@ -10,7 +10,7 @@ import { rm } from 'graceful-fs' import { exec, spawn } from 'child_process' -import { execAsync, getWineFromProton } from './utils' +import { execAsync, getWineFromProton } from '../utils' import { execOptions, toolsPath, @@ -18,29 +18,28 @@ import { isWindows, userHome, isLinux -} from './constants' -import { logError, logInfo, LogPrefix, logWarning } from './logger/logger' +} from '../constants' +import { logError, logInfo, LogPrefix, logWarning } from '../logger/logger' import i18next from 'i18next' -import path, { dirname, join } from 'path' -import { isOnline } from './online_monitor' -import { showDialogBoxModalAuto } from './dialog/dialog' +import { dirname, join } from 'path' +import { isOnline } from '../online_monitor' +import { showDialogBoxModalAuto } from '../dialog/dialog' import { prepareWineLaunch, runWineCommand, setupEnvVars, setupWineEnvVars, validWine -} from './launcher' +} from '../launcher' import { chmod } from 'fs/promises' import { any_gpu_supports_version, get_nvngx_path, get_vulkan_instance_version -} from './utils/graphics/vulkan' +} from '../utils/graphics/vulkan' import { lt as semverLt } from 'semver' -import { gameManagerMap } from './storeManagers' -import { ipcMain } from 'electron' -import { sendFrontendMessage } from './main_window' +import { gameManagerMap } from '../storeManagers' +import { sendFrontendMessage } from '../main_window' export const DXVK = { getLatest: async () => { @@ -706,23 +705,7 @@ function getVkd3dUrl(): string { return 'https://api.github.com/repos/Heroic-Games-Launcher/vkd3d-proton/releases/latest' } -ipcMain.handle( - 'runWineCommandForGame', - async (event, { appName, commandParts, runner }) => { - if (isWindows) { - return execAsync(commandParts.join(' ')) - } - - // FIXME: Why are we using `runinprefix` here? - return runWineCommandOnGame(runner, appName, { - commandParts, - wait: false, - protonVerb: 'runinprefix' - }) - } -) - -async function runWineCommandOnGame( +export async function runWineCommandOnGame( runner: Runner, appName: string, { commandParts, wait = false, protonVerb, startFolder }: WineCommandArgs @@ -746,60 +729,3 @@ async function runWineCommandOnGame( startFolder }) } - -// Calls WineCFG or Winetricks. If is WineCFG, use the same binary as wine to launch it to dont update the prefix -ipcMain.handle('callTool', async (event, { tool, exe, appName, runner }) => { - const gameSettings = await gameManagerMap[runner].getSettings(appName) - - switch (tool) { - case 'winetricks': - await Winetricks.run(runner, appName) - break - case 'winecfg': - runWineCommandOnGame(runner, appName, { - gameSettings, - commandParts: ['winecfg'], - wait: false - }) - break - case 'runExe': - if (exe) { - const workingDir = path.parse(exe).dir - runWineCommandOnGame(runner, appName, { - gameSettings, - commandParts: [exe], - wait: false, - startFolder: workingDir - }) - } - break - } -}) - -ipcMain.on( - 'winetricksInstall', - async (event, { runner, appName, component }) => { - sendFrontendMessage('installing-winetricks-component', component) - try { - await Winetricks.runWithArgs(runner, appName, ['-q', component]) - } finally { - sendFrontendMessage('installing-winetricks-component', '') - } - } -) - -ipcMain.handle('winetricksAvailable', async (event, { runner, appName }) => { - try { - return await Winetricks.listAvailable(runner, appName) - } catch { - return [] - } -}) - -ipcMain.handle('winetricksInstalled', async (event, { runner, appName }) => { - try { - return await Winetricks.listInstalled(runner, appName) - } catch { - return [] - } -}) diff --git a/src/backend/tools/ipc_handler.ts b/src/backend/tools/ipc_handler.ts new file mode 100644 index 0000000000..d2692dbd0b --- /dev/null +++ b/src/backend/tools/ipc_handler.ts @@ -0,0 +1,80 @@ +import { gameManagerMap } from 'backend/storeManagers' +import { ipcMain } from 'electron' +import { Winetricks, runWineCommandOnGame } from '.' +import path from 'path' +import { sendFrontendMessage } from 'backend/main_window' +import { isWindows } from 'backend/constants' +import { execAsync } from 'backend/utils' + +ipcMain.handle( + 'runWineCommandForGame', + async (event, { appName, commandParts, runner }) => { + if (isWindows) { + return execAsync(commandParts.join(' ')) + } + + // FIXME: Why are we using `runinprefix` here? + return runWineCommandOnGame(runner, appName, { + commandParts, + wait: false, + protonVerb: 'runinprefix' + }) + } +) + +// Calls WineCFG or Winetricks. If is WineCFG, use the same binary as wine to launch it to dont update the prefix +ipcMain.handle('callTool', async (event, { tool, exe, appName, runner }) => { + const gameSettings = await gameManagerMap[runner].getSettings(appName) + + switch (tool) { + case 'winetricks': + await Winetricks.run(runner, appName) + break + case 'winecfg': + runWineCommandOnGame(runner, appName, { + gameSettings, + commandParts: ['winecfg'], + wait: false + }) + break + case 'runExe': + if (exe) { + const workingDir = path.parse(exe).dir + runWineCommandOnGame(runner, appName, { + gameSettings, + commandParts: [exe], + wait: false, + startFolder: workingDir + }) + } + break + } +}) + +ipcMain.on( + 'winetricksInstall', + async (event, { runner, appName, component }) => { + sendFrontendMessage('installing-winetricks-component', component) + try { + await Winetricks.runWithArgs(runner, appName, ['-q', component]) + } finally { + sendFrontendMessage('installing-winetricks-component', '') + } + } +) + +ipcMain.handle('winetricksAvailable', async (event, { runner, appName }) => { + try { + return await Winetricks.listAvailable(runner, appName) + } catch { + return [] + } +}) + +ipcMain.handle('winetricksInstalled', async (event, { runner, appName }) => { + try { + return await Winetricks.listInstalled(runner, appName) + } catch { + return [] + } +}) From 920dc996ea52de72c698eca06c8a95b9a0b3d024 Mon Sep 17 00:00:00 2001 From: Ariel Juodziukynas Date: Sun, 8 Oct 2023 14:28:09 -0300 Subject: [PATCH 7/7] Built-in Winetricks: show component in progress --- public/locales/en/translation.json | 2 +- src/backend/api/wine.ts | 5 ++++- src/backend/tools/index.ts | 21 ++++++++++++++++-- src/backend/tools/ipc_handler.ts | 13 ++--------- .../components/UI/Winetricks/index.tsx | 22 +++++++++++++------ 5 files changed, 41 insertions(+), 22 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 26de825976..a851a73f4b 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -722,7 +722,7 @@ "winetricks": { "install": "Install", "installed": "Installed components:", - "installing": "Installation in progress", + "installing": "Installation in progress: {{component}}", "loading": "Loading", "nothingYet": "Nothing was installed by Winetricks yet", "openGUI": "Open Winetricks GUI", diff --git a/src/backend/api/wine.ts b/src/backend/api/wine.ts index 51166ab4f9..fb42aa255f 100644 --- a/src/backend/api/wine.ts +++ b/src/backend/api/wine.ts @@ -36,7 +36,10 @@ export const refreshWineVersionInfo = async (fetch?: boolean): Promise => ipcRenderer.invoke('refreshWineVersionInfo', fetch) export const handleProgressOfWinetricks = ( - onProgress: (e: Electron.IpcRendererEvent, messages: string[]) => void + onProgress: ( + e: Electron.IpcRendererEvent, + payload: { messages: string[]; installingComponent: '' } + ) => void ): (() => void) => { ipcRenderer.on('progressOfWinetricks', onProgress) return () => { diff --git a/src/backend/tools/index.ts b/src/backend/tools/index.ts index a2dcff331c..439c6658ea 100644 --- a/src/backend/tools/index.ts +++ b/src/backend/tools/index.ts @@ -396,6 +396,7 @@ export const DXVK = { } } +let installingComponent = '' export const Winetricks = { download: async () => { if (isWindows) { @@ -491,7 +492,10 @@ export const Winetricks = { } const sendProgress = setInterval(() => { if (progressUpdated) { - sendFrontendMessage('progressOfWinetricks', executeMessages) + sendFrontendMessage('progressOfWinetricks', { + messages: executeMessages, + installingComponent + }) progressUpdated = false } }, 1000) @@ -558,7 +562,10 @@ export const Winetricks = { }) child.on('exit', () => { - sendFrontendMessage('progressOfWinetricks', ['Done']) + sendFrontendMessage('progressOfWinetricks', { + messages: ['Done'], + installingComponent + }) clearInterval(sendProgress) resolve(returnOutput ? output : null) }) @@ -630,6 +637,16 @@ export const Winetricks = { } catch { return [] } + }, + install: async (runner: Runner, appName: string, component: string) => { + sendFrontendMessage('installing-winetricks-component', component) + try { + installingComponent = component + await Winetricks.runWithArgs(runner, appName, ['-q', component]) + } finally { + installingComponent = '' + sendFrontendMessage('installing-winetricks-component', '') + } } } diff --git a/src/backend/tools/ipc_handler.ts b/src/backend/tools/ipc_handler.ts index d2692dbd0b..4a28962dd2 100644 --- a/src/backend/tools/ipc_handler.ts +++ b/src/backend/tools/ipc_handler.ts @@ -2,7 +2,6 @@ import { gameManagerMap } from 'backend/storeManagers' import { ipcMain } from 'electron' import { Winetricks, runWineCommandOnGame } from '.' import path from 'path' -import { sendFrontendMessage } from 'backend/main_window' import { isWindows } from 'backend/constants' import { execAsync } from 'backend/utils' @@ -51,16 +50,8 @@ ipcMain.handle('callTool', async (event, { tool, exe, appName, runner }) => { } }) -ipcMain.on( - 'winetricksInstall', - async (event, { runner, appName, component }) => { - sendFrontendMessage('installing-winetricks-component', component) - try { - await Winetricks.runWithArgs(runner, appName, ['-q', component]) - } finally { - sendFrontendMessage('installing-winetricks-component', '') - } - } +ipcMain.on('winetricksInstall', async (event, { runner, appName, component }) => + Winetricks.install(runner, appName, component) ) ipcMain.handle('winetricksAvailable', async (event, { runner, appName }) => { diff --git a/src/frontend/components/UI/Winetricks/index.tsx b/src/frontend/components/UI/Winetricks/index.tsx index 2c45948fff..2047fdcae7 100644 --- a/src/frontend/components/UI/Winetricks/index.tsx +++ b/src/frontend/components/UI/Winetricks/index.tsx @@ -52,6 +52,7 @@ export default function Winetricks({ onClose }: Props) { // handles the installation of components const [installing, setInstalling] = useState(false) + const [installingComponent, setInstallingComponent] = useState('') const [logs, setLogs] = useState([]) function install(component: string) { window.api.winetricksInstall(runner, appName, component) @@ -70,16 +71,17 @@ export default function Winetricks({ onClose }: Props) { async function onWinetricksProgress( e: Electron.IpcRendererEvent, - newLogs: string[] + payload: { messages: string[]; installingComponent: string } ) { // this conditionals help to show the correct state if the dialog // is closed during an installation and then re-opened - if (newLogs[0] && newLogs[0] === 'Done') { - setInstalling(false) - } else if (!installing) { - setInstalling(true) + if (payload.installingComponent.length) { + setInstalling(payload.messages[0] !== 'Done') } - setLogs((currentLogs) => [...currentLogs, ...newLogs]) + if (installingComponent !== payload.installingComponent) { + setInstallingComponent(payload.installingComponent) + } + setLogs((currentLogs) => [...currentLogs, ...payload.messages]) } const removeListener1 = @@ -123,7 +125,13 @@ export default function Winetricks({ onClose }: Props) {
    )} {installing && ( -

    {t('winetricks.installing', 'Installation in progress')}

    +

    + {t( + 'winetricks.installing', + 'Installation in progress: {{component}}', + { component: installingComponent } + )} +

    )}
    )}