diff --git a/electron/constants.ts b/electron/constants.ts index 125814f6d4..bcab7be015 100644 --- a/electron/constants.ts +++ b/electron/constants.ts @@ -33,8 +33,22 @@ function getLegendaryBin() { return bin } +function getGOGdlBin() { + const bin = fixAsarPath( + join( + __dirname, + '/bin/', + process.platform, + isWindows ? '/gogdl.exe' : '/gogdl' + ) + ) + logInfo(`Location: ${bin}`, LogPrefix.Gog) + return bin +} + const isMac = platform() === 'darwin' const isWindows = platform() === 'win32' +const isLinux = platform() == 'linux' const isFlatpak = execPath === '/app/main/heroic' const currentGameConfigVersion: GameConfigVersion = 'v0' const currentGlobalConfigVersion: GlobalConfigVersion = 'v0' @@ -57,13 +71,17 @@ const heroicInstallPath = isWindows ? `${home}\\Games\\Heroic` : `${home}/Games/Heroic` const legendaryBin = getLegendaryBin() +const gogdlBin = getGOGdlBin() const icon = fixAsarPath(join(__dirname, '/icon.png')) const iconDark = fixAsarPath(join(__dirname, '/icon-dark.png')) const iconLight = fixAsarPath(join(__dirname, '/icon-light.png')) const installed = `${legendaryConfigPath}/installed.json` const libraryPath = `${legendaryConfigPath}/metadata/` -const loginUrl = +const fallBackImage = 'fallback' +const epicLoginUrl = 'https://www.epicgames.com/id/login?redirectUrl=https%3A%2F%2Fwww.epicgames.com%2Fid%2Fapi%2Fredirect' +const gogLoginUrl = + 'https://auth.gog.com/auth?client_id=46899977096215655&redirect_uri=https%3A%2F%2Fembed.gog.com%2Fon_login_success%3Forigin%3Dclient&response_type=code&layout=galaxy' const sidInfoUrl = 'https://github.com/flavioislima/HeroicGamesLauncher/issues/42' const heroicGithubURL = @@ -139,13 +157,17 @@ export { installed, isMac, isWindows, + isLinux, legendaryBin, + gogdlBin, legendaryConfigPath, libraryPath, - loginUrl, + epicLoginUrl, + gogLoginUrl, patreonPage, sidInfoUrl, supportURL, + fallBackImage, userInfo, weblateUrl, wikiLink diff --git a/electron/games.ts b/electron/games.ts index 94383c62f6..39e54648ab 100644 --- a/electron/games.ts +++ b/electron/games.ts @@ -5,17 +5,19 @@ import { GameSettings, InstallArgs, InstallInfo, - LaunchResult + LaunchResult, + Runner } from './types' -type Runner = 'legendary' | 'gog' abstract class Game { - public static get(appName: string, runner: Runner = 'legendary') { + public static get( + appName: string, + runner: Runner = 'legendary' + ): LegendaryGame | GOGGame { if (runner === 'legendary') { return LegendaryGame.get(appName) } else if (runner === 'gog') { - logWarning('GOG integration is unimplemented.', LogPrefix.Gog) - return null + return GOGGame.get(appName) } } @@ -39,7 +41,7 @@ abstract class Game { } import { LegendaryGame } from './legendary/games' -import { LogPrefix, logWarning } from './logger/logger' import { BrowserWindow } from 'electron' +import { GOGGame } from './gog/games' export { Game, Runner } diff --git a/electron/gog/games.ts b/electron/gog/games.ts new file mode 100644 index 0000000000..ec0a0fe545 --- /dev/null +++ b/electron/gog/games.ts @@ -0,0 +1,391 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { GOGLibrary } from './library' +import { BrowserWindow } from 'electron' +import Store from 'electron-store' +import { spawn } from 'child_process' +import { join } from 'path' +import prettyBytes from 'pretty-bytes' +import { Game } from '../games' +import { GameConfig } from '../game_config' +import { GlobalConfig } from '../config' +import { + ExtraInfo, + GameInfo, + InstallInfo, + GameSettings, + ExecResult, + InstallArgs, + LaunchResult, + GOGLoginData, + InstalledInfo +} from 'types' +import { existsSync, rmSync } from 'graceful-fs' +import { + gogdlBin, + heroicGamesConfigPath, + isWindows, + execOptions, + isMac, + isLinux +} from '../constants' +import { logError, logInfo, LogPrefix } from '../logger/logger' +import { errorHandler, execAsync } from '../utils' +import { GOGUser } from './user' +import { launch } from '../launcher' +import { addShortcuts, removeShortcuts } from '../shortcuts' + +const configStore = new Store({ + cwd: 'gog_store' +}) + +const installedGamesStore = new Store({ + cwd: 'gog_store', + name: 'installed' +}) + +class GOGGame extends Game { + public appName: string + public window = BrowserWindow.getAllWindows()[0] + private static instances = new Map() + private constructor(appName: string) { + super() + this.appName = appName + } + public static get(appName: string) { + if (!this.instances.get(appName)) { + this.instances.set(appName, new GOGGame(appName)) + } + return this.instances.get(appName) + } + public async getExtraInfo(namespace: string): Promise { + const gameInfo = GOGLibrary.get().getGameInfo(this.appName) + let targetPlatform: 'windows' | 'osx' | 'linux' = 'windows' + + if (isMac && gameInfo.is_mac_native) { + targetPlatform = 'osx' + } else if (isLinux && gameInfo.is_linux_native) { + targetPlatform = 'linux' + } else { + targetPlatform = 'windows' + } + + const extra: ExtraInfo = { + about: gameInfo.extra.about, + reqs: await GOGLibrary.get().createReqsArray(this.appName, targetPlatform) + } + return extra + } + public async getGameInfo(): Promise { + return GOGLibrary.get().getGameInfo(this.appName) + } + async getInstallInfo(): Promise { + return await GOGLibrary.get().getInstallInfo(this.appName) + } + async getSettings(): Promise { + return ( + GameConfig.get(this.appName).config || + (await GameConfig.get(this.appName).getSettings()) + ) + } + hasUpdate(): Promise { + throw new Error('Method not implemented.') + } + public async import(path: string): Promise { + const command = `"${gogdlBin}" import "${path}"` + + logInfo( + [`Importing ${this.appName} from ${path} with:`, command], + LogPrefix.Gog + ) + + return execAsync(command, execOptions).then(async (value) => { + await GOGLibrary.get().importGame(JSON.parse(value.stdout), path) + return value + }) + // throw new Error('Method not implemented.') + } + public async install({ + path, + installDlcs, + platformToInstall, + installLanguage + }: InstallArgs): Promise<{ status: string }> { + const { maxWorkers } = await GlobalConfig.get().getSettings() + const workers = maxWorkers === 0 ? '' : `--max-workers ${maxWorkers}` + const withDlcs = installDlcs ? '--with-dlcs' : '--skip-dlcs' + if (GOGUser.isTokenExpired()) { + await GOGUser.refreshToken() + } + const credentials = configStore.get('credentials') as GOGLoginData + + let installPlatform = platformToInstall.toLowerCase() + if (installPlatform == 'mac') { + installPlatform = 'osx' + } + + const logPath = `"${heroicGamesConfigPath}${this.appName}.log"` + const writeLog = isWindows ? `2>&1 > ${logPath}` : `|& tee ${logPath}` + + // In the future we need to add Language select option + const command = `${gogdlBin} download ${this.appName} --platform ${installPlatform} --path="${path}" --token="${credentials.access_token}" ${withDlcs} --lang="${installLanguage}" ${workers} ${writeLog}` + logInfo([`Installing ${this.appName} with:`, command], LogPrefix.Gog) + return execAsync(command, execOptions) + .then(async ({ stdout, stderr }) => { + if ( + stdout.includes('ERROR') || + stdout.includes('Failed to execute script') + ) { + errorHandler({ error: { stdout, stderr }, logPath }) + return { status: 'error' } + } + // Installation succeded + // Save new game info to installed games store + const installInfo = await this.getInstallInfo() + const gameInfo = GOGLibrary.get().getGameInfo(this.appName) + const isLinuxNative = installPlatform == 'linux' + const additionalInfo = isLinuxNative + ? await GOGLibrary.getLinuxInstallerInfo(this.appName) + : null + const installedData: InstalledInfo = { + platform: installPlatform, + executable: '', + install_path: join(path, gameInfo.folder_name), + install_size: prettyBytes(installInfo.manifest.disk_size), + is_dlc: false, + version: additionalInfo + ? additionalInfo.version + : installInfo.game.version, + appName: this.appName, + installedWithDLCs: installDlcs, + language: installLanguage, + versionEtag: isLinuxNative ? '' : installInfo.manifest.versionEtag, + buildId: isLinuxNative ? '' : installInfo.game.buildId + } + const array: Array = + (installedGamesStore.get('installed') as Array) || [] + array.push(installedData) + installedGamesStore.set('installed', array) + GOGLibrary.get().refreshInstalled() + return { status: 'done' } + }) + .catch(() => { + logInfo('Installaton canceled', LogPrefix.Gog) + return { status: 'error' } + }) + } + public async addShortcuts(fromMenu?: boolean) { + return addShortcuts(await this.getGameInfo(), fromMenu) + } + public async removeShortcuts() { + return removeShortcuts(this.appName, 'gog') + } + launch(launchArguments?: string): Promise { + return launch(this.appName, launchArguments, 'gog') + } + public async moveInstall(newInstallPath: string): Promise { + const { + install: { install_path }, + title + } = await this.getGameInfo() + + if (isWindows) { + newInstallPath += '\\' + install_path.split('\\').slice(-1)[0] + } else { + newInstallPath += '/' + install_path.split('/').slice(-1)[0] + } + + logInfo(`Moving ${title} to ${newInstallPath}`, LogPrefix.Gog) + await execAsync(`mv -f '${install_path}' '${newInstallPath}'`, execOptions) + .then(() => { + GOGLibrary.get().changeGameInstallPath(this.appName, newInstallPath) + logInfo(`Finished Moving ${title}`, LogPrefix.Gog) + }) + .catch((error) => logError(`${error}`, LogPrefix.Gog)) + return newInstallPath + } + /** + * Literally installing game, since gogdl verifies files at runtime + */ + public async repair(): Promise { + const { + installPlatform, + gameData, + credentials, + withDlcs, + writeLog, + workers + } = await this.getCommandParameters() + // In the future we need to add Language select option + const command = `${gogdlBin} repair ${ + this.appName + } --platform ${installPlatform} --path="${ + gameData.install.install_path + }" --token="${credentials.access_token}" ${withDlcs} --lang="${ + gameData.install.language || 'en-US' + }" -b=${gameData.install.buildId} ${workers} ${writeLog}` + logInfo([`Repairing ${this.appName} with:`, command], LogPrefix.Gog) + + return execAsync(command, execOptions) + .then((value) => value) + .catch((error) => { + logError(`${error}`, LogPrefix.Gog) + return null + }) + } + public async stop(): Promise { + const pattern = isLinux ? this.appName : 'gogdl' + logInfo(['killing', pattern], LogPrefix.Gog) + + if (isWindows) { + try { + await execAsync(`Stop-Process -name ${pattern}`, execOptions) + return logInfo(`${pattern} killed`, LogPrefix.Gog) + } catch (error) { + return logError( + [`not possible to kill ${pattern}`, `${error}`], + LogPrefix.Gog + ) + } + } + + const child = spawn('pkill', ['-f', pattern]) + child.on('exit', () => { + return logInfo(`${pattern} killed`, LogPrefix.Gog) + }) + } + syncSaves(arg: string, path: string): Promise { + throw new Error( + "GOG integration doesn't support syncSaves yet. How did you managed to call that function?" + ) + } + public async uninstall(): Promise { + const array: Array = + (installedGamesStore.get('installed') as Array) || [] + const index = array.findIndex((game) => game.appName == this.appName) + if (index == -1) { + throw Error("Game isn't installed") + } + + const [object] = array.splice(index, 1) + logInfo(['Removing', object.install_path], LogPrefix.Gog) + // TODO: Run unins000.exe /verysilent /dir=Z:/path/to/game + const uninstallerPath = join(object.install_path, 'unins000.exe') + if (existsSync(uninstallerPath)) { + const { + winePrefix, + wineVersion: { bin, name }, + wineCrossoverBottle + } = GameConfig.get(this.appName).config + let commandPrefix = `WINEPREFIX="${winePrefix}" ${bin}` + if (name.includes('CrossOver')) { + commandPrefix = `CX_BOTTLE=${wineCrossoverBottle} ${bin}` + } + const command = `${ + isWindows ? '' : commandPrefix + } "${uninstallerPath}" /verysilent /dir="${isWindows ? '' : 'Z:'}${ + object.install_path + }"` + logInfo(['Executing uninstall command', command], LogPrefix.Gog) + await execAsync(command) + } else rmSync(object.install_path, { recursive: true }) + installedGamesStore.set('installed', array) + GOGLibrary.get().refreshInstalled() + // This is to satisfy Typescript (we neeed to change it probably) + return { stdout: '', stderr: '' } + } + public async update(): Promise { + const { + installPlatform, + gameData, + credentials, + withDlcs, + writeLog, + workers + } = await this.getCommandParameters() + const command = `${gogdlBin} update ${ + this.appName + } --platform ${installPlatform} --path="${ + gameData.install.install_path + }" --token="${credentials.access_token}" ${withDlcs} --lang="${ + gameData.install.language || 'en-US' + }" ${workers} ${writeLog}` + logInfo([`Updating ${this.appName} with:`, command], LogPrefix.Gog) + + return execAsync(command, execOptions) + .then(async () => { + const installedArray = installedGamesStore.get( + 'installed' + ) as InstalledInfo[] + const gameIndex = installedArray.findIndex( + (value) => this.appName == value.appName + ) + const gameObject = installedArray[gameIndex] + + if (gameData.install.platform != 'linux') { + const installInfo = await GOGLibrary.get().getInstallInfo( + this.appName + ) + gameObject.buildId = installInfo.game.buildId + gameObject.version = installInfo.game.version + gameObject.versionEtag = installInfo.manifest.versionEtag + gameObject.install_size = prettyBytes(installInfo.manifest.disk_size) + } else { + const installerInfo = await GOGLibrary.getLinuxInstallerInfo( + this.appName + ) + gameObject.version = installerInfo.version + } + installedGamesStore.set('installed', installedArray) + GOGLibrary.get().refreshInstalled() + this.window.webContents.send('setGameStatus', { + appName: this.appName, + runner: 'gog', + status: 'done' + }) + return { status: 'done' } + }) + .catch((error) => { + logError(`${error}`, LogPrefix.Gog) + this.window.webContents.send('setGameStatus', { + appName: this.appName, + runner: 'gog', + status: 'done' + }) + return { status: 'error' } + }) + } + + /** + * Reads game installed data and returns proper parameters + * Useful for Update and Repair + * @returns + */ + public async getCommandParameters() { + const { maxWorkers } = await GlobalConfig.get().getSettings() + const workers = maxWorkers === 0 ? '' : `--max-workers ${maxWorkers}` + const gameData = GOGLibrary.get().getGameInfo(this.appName) + + const withDlcs = gameData.install.installedWithDLCs + ? '--with-dlcs' + : '--skip-dlcs' + if (GOGUser.isTokenExpired()) { + await GOGUser.refreshToken() + } + const credentials = configStore.get('credentials') as GOGLoginData + + const installPlatform = gameData.install.platform + const logPath = `"${heroicGamesConfigPath}${this.appName}.log"` + const writeLog = isWindows ? `2>&1 > ${logPath}` : `|& tee ${logPath}` + + return { + withDlcs, + workers, + installPlatform, + writeLog, + credentials, + gameData + } + } +} + +export { GOGGame } diff --git a/electron/gog/library.ts b/electron/gog/library.ts new file mode 100644 index 0000000000..17724bbcdb --- /dev/null +++ b/electron/gog/library.ts @@ -0,0 +1,585 @@ +import axios, { AxiosError, AxiosResponse } from 'axios' +import Store from 'electron-store' +import { GOGUser } from './user' +import { + GOGLoginData, + GOGGameInfo, + GameInfo, + InstallInfo, + InstalledInfo, + GOGImportData +} from '../types' +import { join } from 'node:path' +import { existsSync, readFileSync } from 'graceful-fs' +import prettyBytes from 'pretty-bytes' +import { logError, logInfo, LogPrefix, logWarning } from '../logger/logger' +import { execAsync } from '../utils' +import { fallBackImage, gogdlBin, isMac } from '../constants' + +const userStore = new Store({ + cwd: 'gog_store' +}) +const apiInfoCache = new Store({ cwd: 'gog_store', name: 'api_info_cache' }) +const libraryStore = new Store({ cwd: 'gog_store', name: 'library' }) +const installedGamesStore = new Store({ + cwd: 'gog_store', + name: 'installed' +}) + +export class GOGLibrary { + private static globalInstance: GOGLibrary = null + private library: Map = new Map() + private installedGames: Map = new Map() + + private constructor() { + this.refreshInstalled() + } + + public async sync() { + if (!GOGUser.isLoggedIn()) { + return + } + if (GOGUser.isTokenExpired()) { + await GOGUser.refreshToken() + } + // This gets games ibrary + // Handles multiple pages + this.refreshInstalled() + const credentials: GOGLoginData = userStore.get( + 'credentials' + ) as GOGLoginData + const headers = { Authorization: 'Bearer ' + credentials.access_token } + logInfo('Getting GOG library', LogPrefix.Gog) + let gameApiArray: Array = [] + const games = await axios + .get( + 'https://embed.gog.com/account/getFilteredProducts?mediaType=1&sortBy=title', + { headers } + ) + .catch((e: AxiosError) => { + logError( + ['There was an error getting games library data', e.message], + LogPrefix.Gog + ) + return null + }) + + if (!games) { + logError('There was an error Loading games library', LogPrefix.Gog) + return + } + + if (games?.data?.products) { + const numberOfPages = games?.data.totalPages + logInfo(['Number of library pages:', numberOfPages], LogPrefix.Gog) + gameApiArray = [...games.data.products] + for (let page = 2; page <= numberOfPages; page++) { + logInfo(['Getting data for page', String(page)], LogPrefix.Gog) + const pageData = await axios.get( + `https://embed.gog.com/account/getFilteredProducts?mediaType=1&sortBy=title&page=${page}`, + { headers } + ) + if (pageData.data?.products) { + gameApiArray = [...gameApiArray, ...pageData.data.products] + } + } + } + + const gamesObjects: GameInfo[] = [] + const gamesArray = libraryStore.get('games') as GameInfo[] + for (const game of gameApiArray as GOGGameInfo[]) { + let unifiedObject = gamesArray + ? gamesArray.find((value) => value.app_name == String(game.id)) + : null + if (!unifiedObject) { + let apiData = apiInfoCache.get(String(game.id)) as { + isUpdated: boolean + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any + } + if (!apiData) { + const { data } = await GOGLibrary.getGamesdbData( + 'gog', + String(game.id) + ) + apiData = data + apiInfoCache.set(String(game.id), apiData) + } + unifiedObject = await this.gogToUnifiedInfo(game, apiData) + } + gamesObjects.push(unifiedObject) + const installedInfo = this.installedGames.get(String(game.id)) + // Create new object to not write install data into library store + const copyObject = Object.assign({}, unifiedObject) + if (installedInfo) { + copyObject.is_installed = true + copyObject.install = installedInfo + } + this.library.set(String(game.id), copyObject) + } + libraryStore.set('games', gamesObjects) + libraryStore.set('totalGames', games.data.totalProducts) + libraryStore.set('totalMovies', games.data.moviesCount) + logInfo('Saved games data', LogPrefix.Gog) + } + + public static get() { + if (this.globalInstance == null) { + GOGLibrary.globalInstance = new GOGLibrary() + } + return this.globalInstance + } + + public getGameInfo(slug: string): GameInfo { + return this.library.get(slug) || null + } + + /** + * Gets data metadata about game using gogdl info for current system, + * when os is Linux: gets Windows build data. + * Contains data like download size + * @param appName + * @returns InstallInfo object + */ + public async getInstallInfo(appName: string) { + if (GOGUser.isTokenExpired()) { + await GOGUser.refreshToken() + } + const credentials = userStore.get('credentials') as GOGLoginData + const gameData = this.library.get(appName) + const { stdout } = await execAsync( + `${gogdlBin} info ${appName} --token="${ + credentials.access_token + }" --lang=en-US --os ${ + isMac && gameData.is_mac_native ? 'osx' : 'windows' + }` + ) + const gogInfo = JSON.parse(stdout) + const libraryArray = libraryStore.get('games') as GameInfo[] + const gameObjectIndex = libraryArray.findIndex( + (value) => value.app_name == appName + ) + libraryArray[gameObjectIndex].folder_name = gogInfo.folder_name + gameData.folder_name = gogInfo.folder_name + libraryStore.set('games', libraryArray) + this.library.set(appName, gameData) + const info: InstallInfo = { + game: { + app_name: appName, + title: gameData.title, + owned_dlc: gogInfo.dlcs, + version: gogInfo.versionName, + launch_options: [], + platform_versions: null, + buildId: gogInfo.buildId + }, + manifest: { + disk_size: Number(gogInfo.disk_size), + download_size: Number(gogInfo.download_size), + app_name: appName, + install_tags: [], + launch_exe: '', + prerequisites: null, + languages: gogInfo.languages, + versionEtag: gogInfo.versionEtag + } + } + return info + } + + /** + * Loads installed data and adds it into a Map + */ + public refreshInstalled() { + const installedArray = + (installedGamesStore.get('installed') as Array) || [] + this.installedGames.clear() + installedArray.forEach((value) => { + this.installedGames.set(value.appName, value) + }) + } + + public changeGameInstallPath(appName: string, newInstallPath: string) { + const cachedGameData = this.library.get(appName) + + const installedArray = + (installedGamesStore.get('installed') as Array) || [] + + const gameIndex = installedArray.findIndex( + (value) => value.appName == appName + ) + + installedArray[gameIndex].install_path = newInstallPath + cachedGameData.install.install_path = newInstallPath + installedGamesStore.set('installed', installedArray) + } + public async importGame(data: GOGImportData, path: string) { + const installInfo: InstalledInfo = { + appName: data.appName, + install_path: path, + executable: '', + install_size: prettyBytes( + (await this.getInstallInfo(data.appName)).manifest.disk_size + ), + is_dlc: false, + version: data.versionName, + platform: data.platform, + buildId: data.buildId, + installedWithDLCs: data.installedWithDlcs + } + this.installedGames.set(data.appName, installInfo) + const gameData = this.library.get(data.appName) + gameData.install = installInfo + gameData.is_installed = true + this.library.set(data.appName, gameData) + installedGamesStore.set( + 'installed', + Array.from(this.installedGames.values()) + ) + } + + // This checks for updates of Windows and Mac titles + // Linux installers need to be checked differenly + public async listUpdateableGames(): Promise { + const installed = Array.from(this.installedGames.values()) + const updateable: Array = [] + for (const game of installed) { + // use different check for linux games + if (game.platform === 'linux') { + if ( + !(await this.checkForLinuxInstallerUpdate(game.appName, game.version)) + ) + updateable.push(game.appName) + continue + } + const hasUpdate = await this.checkForGameUpdate( + game.appName, + game?.versionEtag, + game.platform + ) + if (hasUpdate) { + updateable.push(game.appName) + } + } + logInfo(`Found ${updateable.length} game(s) to update`, LogPrefix.Gog) + return updateable + } + + public async checkForLinuxInstallerUpdate( + appName: string, + version: string + ): Promise { + const response = await GOGLibrary.getProductApi(appName, ['downloads']) + if (!response) return false + + const installers = response.data?.downloads?.installers + for (const installer of installers) { + if (installer.os == 'linux') { + return installer.version == version + } + } + } + + public async checkForGameUpdate( + appName: string, + etag: string, + platform: string + ) { + const buildData = await axios.get( + `https://content-system.gog.com/products/${appName}/os/${platform}/builds?generation=2` + ) + const metaUrl = buildData.data?.items[0]?.link + const headers = etag + ? { + 'If-None-Match': etag + } + : null + const metaResponse = await axios.get(metaUrl, { + headers, + validateStatus: (status) => status == 200 || status == 304 + }) + + return metaResponse.status == 200 + } + + /** + * Convert GOGGameInfo object to GameInfo + * That way it will be easly accessible on frontend + */ + public async gogToUnifiedInfo( + info: GOGGameInfo, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + gamesdbData: any + ): Promise { + let developer: string + let verticalCover: string + let horizontalCover: string + let description: string + if (gamesdbData.game) { + const developers: Array = [] + for (const developer of gamesdbData.game.developers) { + developers.push(developer.name) + } + developer = developers.join(', ') + verticalCover = gamesdbData.game.vertical_cover.url_format + .replace('{formatter}', '') + .replace('{ext}', 'jpg') + horizontalCover = `https:${info.image}.jpg` + description = gamesdbData.game.summary['*'] + // horizontalCover = gamesdbData._links.logo.href + // horizontalCover = gamesdbData.game.background.url_format + // .replace('{formatter}', '') + // .replace('{ext}', 'webp') + } else { + logWarning( + `Unable to get covers from gamesdb for ${info.title}. Trying to get it from api.gog.com`, + LogPrefix.Gog + ) + const apiData = await this.getGamesData(String(info.id)) + if (apiData._links) { + verticalCover = apiData._links.boxArtImage.href + } else { + logWarning( + "Couldn't get info from api.gog.com, Using fallback vertical image", + LogPrefix.Gog + ) + verticalCover = fallBackImage + } + horizontalCover = `https:${info.image}.jpg` + } + + const object: GameInfo = { + runner: 'gog', + store_url: `https://gog.com${info.url}`, + developer: developer || '', + app_name: String(info.id), + art_logo: null, + art_cover: horizontalCover, + art_square: verticalCover, + cloud_save_enabled: false, + compatible_apps: [], + extra: { + about: { description: description, shortDescription: '' }, + reqs: [] + }, + folder_name: '', + install: { + version: null, + executable: '', + install_path: '', + install_size: '', + is_dlc: false, + platform: '' + }, + is_game: true, + is_installed: false, + is_ue_asset: false, + is_ue_plugin: false, + is_ue_project: false, + namespace: info.slug, + save_folder: '', + title: info.title, + canRunOffline: true, + is_mac_native: info.worksOn.Mac, + is_linux_native: info.worksOn.Linux + } + + return object + } + /** + * Fetches data from gog about game + * https://api.gog.com/v2/games + * @param appName + * @param lang optional language (falls back to english if is not supported) + * @returns plain API response + */ + public async getGamesData(appName: string, lang?: string) { + const url = `https://api.gog.com/v2/games/${appName}${ + lang ?? '?locale=' + lang + }` + const response: AxiosResponse | null = await axios.get(url).catch(() => { + return null + }) + if (!response) { + return null + } + + return response.data + } + /** + * Creates Array based on returned from API + * If no recommended data is present it just stays empty + * There always should be minumum requirements + * @param apiData + * @param os + * @returns parsed data used when rendering requirements on GamePage + */ + public async createReqsArray( + appName: string, + os: 'windows' | 'linux' | 'osx' + ) { + const apiData = await this.getGamesData(appName) + const operatingSystems = apiData._embedded.supportedOperatingSystems + let requirements = operatingSystems.find( + (value: { operatingSystem: { name: string } }) => + value.operatingSystem.name === os + ) + + if (!requirements) { + return [] + } else { + requirements = requirements.systemRequirements + } + if (requirements.length == 0) { + return [] + } + const minimum = requirements[0] + const recommended = requirements.length > 1 ? requirements[1] : null + const returnValue = [] + for (let i = 0; i < minimum.requirements.length; i++) { + const object = { + title: minimum.requirements[i].name.replace(':', ''), + minimum: minimum.requirements[i].description, + recommended: recommended && recommended.requirements[i]?.description + } + if (!object.minimum) { + continue + } + returnValue.push(object) + } + return returnValue + } + + public getExecutable(appName: string): string { + const gameInfo = this.getGameInfo(appName) + const infoFileName = `goggame-${appName}.info` + const infoFilePath = join(gameInfo.install.install_path, infoFileName) + + if (existsSync(infoFilePath)) { + logInfo(`Loading playTask data from ${infoFilePath}`, LogPrefix.Backend) + const fileData = readFileSync(infoFilePath, { encoding: 'utf-8' }) + + const jsonData = JSON.parse(fileData) + const playTasks = jsonData.playTasks + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const primary = playTasks.find((value: any) => value?.isPrimary) + + const workingDir = primary?.workingDir + + if (workingDir) { + return join(workingDir, primary.path) + } + return primary.path + } + + return '' + } + + /** + * This function can be also used with outher stores + * This endpoint doesn't require user to be authenticated. + * @param store Indicates a store we have game_id from, like: epic, itch, humble, gog, uplay + * @param game_id ID of a game + * @param etag (optional) value returned in response, works as checksum so we can check if we have up to date data + * @returns object {isUpdated, data}, where isUpdated is true when Etags match + */ + public static async getGamesdbData( + store: string, + game_id: string, + etag?: string + ) { + const url = `https://gamesdb.gog.com/platforms/${store}/external_releases/${game_id}` + const headers = { + 'If-None-Match': etag + } + + const response = await axios + .get(url, { headers: etag ? headers : {} }) + .catch((err) => { + if (err.response.status == 404) { + return null + } + }) + if (!response) { + return { isUpdated: false, data: {} } + } + const resEtag = response.headers.etag + const isUpdated = etag == resEtag + const data = response.data + + data.etag = resEtag + return { + isUpdated, + data + } + } + + /** + * Handler of https://api.gog.com/products/ endpoint + * @param appName id of game + * @param expand expanded results to be returned + * @returns raw axios response null when there was a error + */ + public static async getProductApi(appName: string, expand?: string[]) { + const isExpanded = expand?.length > 0 + let expandString = '?expand=' + if (isExpanded) { + expandString += expand.join(',') + } + const url = `https://api.gog.com/products/${appName}${ + isExpanded ? expandString : '' + }` + const response: AxiosResponse = await axios.get(url).catch(() => null) + + return response + } + + /** + * Gets array of possible installer languages + * @param appName + */ + public static async getLinuxInstallersLanguages(appName: string) { + const response = await GOGLibrary.getProductApi(appName, ['downloads']) + if (response) { + const installers = response.data?.downloads?.installers + const linuxInstallers = installers.filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (value: any) => value.os == 'linux' + ) + const possibleLanguages = [] + + for (const installer of linuxInstallers) { + possibleLanguages.push(installer.language) + } + + return possibleLanguages + } else { + return ['en-US'] + } + } + + /** + * For now returns a version (we can extend it later) + * @param appName + * @returns + */ + public static async getLinuxInstallerInfo(appName: string): Promise<{ + version: string + } | null> { + const response = await GOGLibrary.getProductApi(appName, ['downloads']) + if (response) { + const installers = response.data?.downloads?.installers + + for (const installer of installers) { + if (installer.os == 'linux') + return { + version: installer.version + } + } + } else { + logError("Couldn't get installer info") + return null + } + } +} diff --git a/electron/gog/setup.ts b/electron/gog/setup.ts new file mode 100644 index 0000000000..4274a9bd86 --- /dev/null +++ b/electron/gog/setup.ts @@ -0,0 +1,254 @@ +import axios from 'axios' +import { + copyFileSync, + existsSync, + mkdirSync, + readFileSync, + constants, + writeFileSync +} from 'graceful-fs' +import path from 'node:path' +import { GOGLibrary } from './library' +import { GameInfo } from '../types' +import { execAsync } from '../utils' +import { GameConfig } from '../game_config' +import { logError, logInfo, LogPrefix, logWarning } from '../logger/logger' +import { home, isWindows } from '../constants' +import ini from 'ini' +/** + * Handles setup instructions like create folders, move files, run exe, create registry entry etc... + * For Galaxy games only (Mac and Windows for now) + * @param appName + */ +async function setup(appName: string): Promise { + const gameInfo = GOGLibrary.get().getGameInfo(appName) + if (!gameInfo || gameInfo.install.platform == 'linux') { + return + } + const instructions = await obtainSetupInstructions(gameInfo) + if (!instructions) { + return + } + logWarning( + 'Running setup instructions, if you notice issues with launching a game, please report it on our Discord server', + LogPrefix.Gog + ) + + const gameSettings = GameConfig.get(appName).config + + const isCrossover = gameSettings.wineVersion.name.includes('CrossOver') + const crossoverBottle = gameSettings.wineCrossoverBottle + const crossoverEnv = + isCrossover && crossoverBottle ? `CX_BOTTLE=${crossoverBottle}` : '' + const isProton = + gameSettings.wineVersion.name.includes('Proton') || + gameSettings.wineVersion.name.includes('Steam') + const prefix = isProton + ? `STEAM_COMPAT_CLIENT_INSTALL_PATH=${home}/.steam/steam STEAM_COMPAT_DATA_PATH='${gameSettings.winePrefix + .replaceAll("'", '') + .replace('~', home)}'` + : `WINEPREFIX="${gameSettings.winePrefix + .replaceAll("'", '') + .replace('~', home)}"` + + const commandPrefix = isWindows + ? '' + : `${isCrossover ? crossoverEnv : prefix} ${gameSettings.wineVersion.bin}` + // Funny part begins here + + // Deterimine if it's basicly from .script file or from manifest + if (instructions[0]?.install) { + // It's from .script file + // Parse actions + for (const action of instructions) { + const actionArguments = action.install?.arguments + switch (action.install.action) { + case 'setRegistry': { + const registryPath = + actionArguments.root + '\\' + actionArguments.subkey + // If deleteSubkeys is true remove path first + if (actionArguments.deleteSubkeys) { + const command = `${commandPrefix} reg delete "${registryPath}" /f` + logInfo( + ['Setup: Deleting a registry key', registryPath], + LogPrefix.Gog + ) + await execAsync(command) + } + // Now create a key + const command = `${commandPrefix} reg add "${registryPath}" /f` + logInfo(['Setup: Adding a registry key', registryPath], LogPrefix.Gog) + await execAsync(command) + break + } + case 'Execute': { + const executableName = actionArguments.executable + const infoPath = path.join( + gameInfo.install.install_path, + `goggame-${appName}.info` + ) + let Language = 'english' + // Load game language data + if (existsSync(infoPath)) { + const contents = readFileSync(infoPath, 'utf-8') + Language = JSON.parse(contents).language + Language = Language.toLowerCase() + } + + // Please don't fix any typos here, everything is intended + const exeArguments = `/VERYSILENT /DIR="${!isWindows ? 'Z:' : ''}${ + gameInfo.install.install_path + }" /Language=${Language} /LANG=${Language} /ProductId=${appName} /galaxyclient /buildId=${ + gameInfo.install.buildId + } /versionName="${ + gameInfo.install.version + }" /nodesktopshorctut /nodesktopshortcut` + const workingDir = actionArguments?.workingDir?.replace( + '%SUPPORT%', + `"${path.join(gameInfo.install.install_path, 'support', appName)}"` + ) + const executablePath = path.join( + gameInfo.install.install_path, + 'support', + appName, + executableName + ) + if (!existsSync(executablePath)) { + logError( + ['Executable', executablePath, "doesn't exsist"], + LogPrefix.Gog + ) + break + } + let command = `${ + workingDir ? 'cd ' + workingDir + ' &&' : '' + } ${commandPrefix} "${executablePath}" ${exeArguments}` + // Requires testing + if (isWindows) { + command = `${ + workingDir ? 'cd ' + workingDir + ' &&' : '' + } Start-Process -FilePath "${executablePath}" -Verb RunAs -ArgumentList "${exeArguments}"` + } + logInfo(['Setup: Executing', command], LogPrefix.Gog) + await execAsync(command) + break + } + case 'supportData': { + const targetPath = actionArguments.target.replace( + '{app}', + gameInfo.install.install_path + ) + const type = actionArguments.type + if (type == 'folder') { + mkdirSync(targetPath, { recursive: true }) + } else if (type == 'file') { + const sourcePath = actionArguments.source + .replace( + '{supportDir}', + path.join(gameInfo.install.install_path, 'support', appName) + ) + .replace('{app}', gameInfo.install.install_path) + if (existsSync(sourcePath)) { + copyFileSync(sourcePath, targetPath, constants.COPYFILE_FICLONE) + } + } else { + logError( + ['Setup: Unsupported supportData type:', type], + LogPrefix.Gog + ) + } + break + } + case 'setIni': { + const filePath = actionArguments?.filename.replace( + '{app}', + gameInfo.install.install_path + ) + if (!filePath || !existsSync(filePath)) { + logError("Setup: setIni file doesn't exists", LogPrefix.Gog) + break + } + const encoding = actionArguments?.utf8 ? 'utf-8' : 'ascii' + const fileData = readFileSync(filePath, { + encoding + }) + const config = ini.parse(fileData) + // TODO: Do something + const section = actionArguments?.section + const keyName = actionArguments?.keyName + if (!section || !keyName) { + logError( + "Missing section and key values, this message shouldn't appear for you. Please report it on our Discord or GitHub" + ) + break + } + + config[section][keyName] = actionArguments.keyValue.replace( + '{app}', + isWindows ? '' : 'Z:' + gameInfo.install.install_path + ) + writeFileSync(filePath, ini.stringify(config), { encoding }) + break + } + default: { + logError( + [ + 'Setup: Looks like you have found new setup instruction, please report it on our Discord or GitHub', + `appName: ${appName}, action: ${action.install.action}` + ], + LogPrefix.Gog + ) + } + } + } + } else { + // I's from V1 game manifest + // Sample + /* + "support_commands": [ + { + "languages": [ + "Neutral" + ], + "executable": "/galaxy_akalabeth_2.0.0.1.exe", + "gameID": "1207666073", + "systems": [ + "Windows" + ], + "argument": "" + } + ], + */ + } + logInfo('Setup: Finished', LogPrefix.Gog) +} + +async function obtainSetupInstructions(gameInfo: GameInfo) { + const { buildId, appName, install_path } = gameInfo.install + + const scriptPath = path.join(install_path, `goggame-${appName}.script`) + if (existsSync(scriptPath)) { + const data = readFileSync(scriptPath, { encoding: 'utf-8' }) + return JSON.parse(data).actions + } + // No .script is present, check for support_commands in repository.json of V1 games + + const buildResponse = await axios.get( + `https://content-system.gog.com/products/${appName}/os/windows/builds` + ) + const buildData = buildResponse.data + const buildItem = buildData.items.find( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (value: any) => value.build_id == buildId + ) + // Get data only if it's V1 depot game + if (buildItem?.generation == 1) { + const metaResponse = await axios.get(buildItem.link) + metaResponse.data.support_commands + } + + // TODO: find if there are V2 games with something like support_commands in manifest + return null +} + +export default setup diff --git a/electron/gog/user.ts b/electron/gog/user.ts new file mode 100644 index 0000000000..75d9d680f1 --- /dev/null +++ b/electron/gog/user.ts @@ -0,0 +1,112 @@ +import axios from 'axios' +import Store from 'electron-store' +import { logError, logInfo, LogPrefix } from '../logger/logger' +import { GOGLoginData } from '../types' + +const configStore = new Store({ + cwd: 'gog_store' +}) + +const gogAuthenticateUrl = + 'https://auth.gog.com/token?client_id=46899977096215655&client_secret=9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fembed.gog.com%2Fon_login_success%3Forigin%3Dclient&code=' +const gogRefreshTokenUrl = + 'https://auth.gog.com/token?client_id=46899977096215655&client_secret=9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9&grant_type=refresh_token' + +export class GOGUser { + static async login(code: string) { + logInfo('Logging using GOG credentials', LogPrefix.Gog) + + // Gets token from GOG basaed on authorization code + const response = await axios + .get(gogAuthenticateUrl + code) + .catch((error) => { + // Handle fetching error + logError(['Failed to get access_token', `${error}`], LogPrefix.Gog) + return null + }) + if (!response?.data) { + logError('Failed to get access_token', LogPrefix.Gog) + return { status: 'error' } + } + + const data: GOGLoginData = response.data + data.loginTime = Date.now() + configStore.set('credentials', data) + logInfo('Login Successful', LogPrefix.Gog) + await this.getUserDetails() + return { status: 'done' } + } + + public static async getUserDetails() { + logInfo('Getting data about the user', LogPrefix.Gog) + if (!this.isLoggedIn()) { + return + } + if (this.isTokenExpired()) { + this.refreshToken() + } + const user: GOGLoginData = configStore.get('credentials') as GOGLoginData + const response = await axios + .get(`https://embed.gog.com/userData.json`, { + headers: { Authorization: `Bearer ${user.access_token}` } + }) + .catch((error) => { + logError(['Error getting user Data', `${error}`], LogPrefix.Gog) + return null + }) + + const data = response.data + + //Exclude email, it won't be needed + delete data.email + + configStore.set('userData', data) + logInfo('Saved user data to config', LogPrefix.Gog) + } + + public static async refreshToken() { + const user: GOGLoginData = configStore.get('credentials') as GOGLoginData + logInfo('Refreshing access_token', LogPrefix.Gog) + if (user) { + const response = await axios + .get(`${gogRefreshTokenUrl}&refresh_token=${user.refresh_token}`) + .catch(() => { + logError( + 'Error with refreshing token, reauth required', + LogPrefix.Gog + ) + return null + }) + + if (!response) { + return + } + + const data: GOGLoginData = response.data + data.loginTime = Date.now() + configStore.set('credentials', data) + logInfo('Token refreshed successfully', LogPrefix.Gog) + } else { + logError('No credentials, auth required', LogPrefix.Gog) + } + } + + public static isTokenExpired() { + const user: GOGLoginData = configStore.get('credentials') as GOGLoginData + if (!user) { + return true + } + const isExpired = Date.now() >= user.loginTime + user.expires_in * 1000 + return isExpired + } + public static logout() { + const libraryStore = new Store({ cwd: 'gog_store', name: 'library' }) + configStore.clear() + libraryStore.clear() + logInfo('Logging user out', LogPrefix.Gog) + } + + public static isLoggedIn() { + return configStore.has('credentials') + } +} diff --git a/electron/launcher.ts b/electron/launcher.ts new file mode 100644 index 0000000000..a6faf27381 --- /dev/null +++ b/electron/launcher.ts @@ -0,0 +1,353 @@ +// This handles launching games, prefix creation etc.. + +import { dialog } from 'electron' +import makeClient from 'discord-rich-presence-typescript' +import i18next from 'i18next' +import { existsSync, mkdirSync } from 'graceful-fs' +import { + isWindows, + isMac, + isLinux, + home, + execOptions, + legendaryBin, + gogdlBin +} from './constants' +import { execAsync, isEpicServiceOffline, isOnline } from './utils' +import { logError, logInfo, LogPrefix, logWarning } from './logger/logger' +import { GlobalConfig } from './config' +import { GameConfig } from './game_config' +import { DXVK } from './dxvk' +import { Runner } from './types' +import { GOGLibrary } from './gog/library' +import { LegendaryLibrary } from './legendary/library' +import setup from './gog/setup' + +function getGameInfo(appName: string, runner: Runner) { + switch (runner) { + case 'legendary': + return LegendaryLibrary.get().getGameInfo(appName) + case 'gog': + return GOGLibrary.get().getGameInfo(appName) + default: + throw Error(`Launching ${runner} is not implemented`) + } +} + +async function launch( + appName: string, + launchArguments?: string, + runner: Runner = 'legendary' +) { + const isLegendary = runner == 'legendary' + const isGOG = runner == 'gog' + // const isExternal = runner == 'heroic' + const epicOffline = isLegendary && (await isEpicServiceOffline()) + const isOffline = isLegendary && (!(await isOnline()) || epicOffline) + let envVars = '' + let gameMode: string + const gameSettings = + GameConfig.get(appName).config || + (await GameConfig.get(appName).getSettings()) + const gameInfo = await getGameInfo(appName, runner) + + const { + winePrefix, + wineVersion, + wineCrossoverBottle, + otherOptions, + useGameMode, + showFps, + nvidiaPrime, + launcherArgs = '', + showMangohud, + audioFix, + autoInstallDxvk, + offlineMode, + enableFSR, + maxSharpness, + enableResizableBar, + enableEsync, + enableFsync, + targetExe, + useSteamRuntime + } = gameSettings + + const { discordRPC } = await GlobalConfig.get().getSettings() + const DiscordRPC = discordRPC ? makeClient('852942976564723722') : null + let runOffline = '' + if (isOffline || offlineMode) { + if (gameInfo.canRunOffline) { + runOffline = '--offline' + } else { + dialog.showErrorBox( + i18next.t( + 'box.error.no-offline-mode.title', + 'Offline mode not supported.' + ), + i18next.t( + 'box.error.no-offline-mode.message', + 'Launch aborted! The game requires a internet connection to run it.' + ) + ) + return + } + } + const exe = targetExe && isLegendary ? `--override-exe ${targetExe}` : '' + const isMacNative = gameInfo.is_mac_native + // const isLinuxNative = gameInfo.is_linux_native + const mangohud = showMangohud ? 'mangohud --dlsym' : '' + let runWithGameMode = '' + if (discordRPC) { + // Show DiscordRPC + // This seems to run when a game is updated, even though the game doesn't start after updating. + let os: string + + switch (process.platform) { + case 'linux': + os = 'Linux' + break + case 'win32': + os = 'Windows' + break + case 'darwin': + os = 'macOS' + break + default: + os = 'Unknown OS' + break + } + + logInfo('Updating Discord Rich Presence information...', LogPrefix.Backend) + DiscordRPC.updatePresence({ + details: gameInfo.title, + instance: true, + largeImageKey: 'icon', + large_text: gameInfo.title, + startTimestamp: Date.now(), + state: 'via Heroic on ' + os + }) + } + if (isLinux) { + // check if Gamemode is installed + await execAsync(`which gamemoderun`) + .then(({ stdout }) => (gameMode = stdout.split('\n')[0])) + .catch(() => logWarning('GameMode not installed', LogPrefix.Backend)) + + runWithGameMode = useGameMode && gameMode ? gameMode : '' + } + if ( + isWindows || + (isMac && isMacNative) || + (isLinux && gameInfo.install.platform == 'linux') + ) { + let command = '' + if (runner == 'legendary') { + command = `${legendaryBin} launch ${appName} ${exe} ${runOffline} ${ + launchArguments ?? '' + } ${launcherArgs}` + logInfo(['Launch Command:', command], LogPrefix.Legendary) + } else if (runner == 'gog') { + // MangoHud,Gamemode, nvidia prime, audio fix can be used in Linux native titles + if (isLinux) { + let steamRuntime: string + // Finds a existing runtime path wether it's flatpak or not and set's a variable + if (useSteamRuntime) { + const nonFlatpakPath = + '~/.local/share/Steam/ubuntu12_32/steam-runtime/run.sh'.replace( + '~', + home + ) + const FlatpakPath = + '~/.var/app/com.valvesoftware.Steam/data/Steam/ubuntu12_32/steam-runtime/run.sh'.replace( + '~', + home + ) + + if (existsSync(nonFlatpakPath)) { + // Escape path in quotes to avoid issues with spaces + steamRuntime = `"${nonFlatpakPath}"` + logInfo( + ['Using non flatpak Steam runtime', steamRuntime], + LogPrefix.Backend + ) + } else if (existsSync(FlatpakPath)) { + steamRuntime = `"${FlatpakPath}"` + logInfo( + ['Using flatpak Steam runtime', steamRuntime], + LogPrefix.Backend + ) + } else { + logWarning("Couldn't find a valid runtime path", LogPrefix.Backend) + } + } + const options = [ + mangohud, + runWithGameMode, + nvidiaPrime + ? 'DRI_PRIME=1 __NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia' + : '', + audioFix ? `PULSE_LATENCY_MSEC=60` : '', + // This must be always last + steamRuntime + ] + envVars = options.join(' ') + } + command = `${envVars} ${gogdlBin} launch "${ + gameInfo.install.install_path + }" ${gameInfo.app_name} --platform=${gameInfo.install.platform} ${ + launchArguments ?? '' + } ${launcherArgs}` + logInfo(['Launch Command:', command], LogPrefix.Gog) + } + + return await execAsync(command, execOptions).then(({ stderr }) => { + if (discordRPC) { + logInfo( + 'Stopping Discord Rich Presence if running...', + LogPrefix.Backend + ) + DiscordRPC.disconnect() + logInfo('Stopped Discord Rich Presence.', LogPrefix.Backend) + } + + return { stderr, command, gameSettings } + }) + } + + if (!wineVersion.bin) { + dialog.showErrorBox( + i18next.t('box.error.wine-not-found.title', 'Wine Not Found'), + i18next.t( + 'box.error.wine-not-found.message', + 'No Wine Version Selected. Check Game Settings!' + ) + ) + } + + const fixedWinePrefix = winePrefix.replace('~', home) + let wineCommand = `--wine ${wineVersion.bin}` + + // We need to keep replacing the ' to keep compatibility with old configs + let prefix = `--wine-prefix '${fixedWinePrefix.replaceAll("'", '')}'` + + const isProton = + wineVersion.name.includes('Proton') || wineVersion.name.includes('Steam') + const isCrossover = wineVersion.name.includes('CrossOver') + prefix = isProton || isCrossover ? '' : prefix + const x = wineVersion.bin.split('/') + x.pop() + const winePath = x.join('/').replaceAll("'", '') + const options = { + audio: audioFix ? `PULSE_LATENCY_MSEC=60` : '', + crossoverBottle: + isCrossover && wineCrossoverBottle != '' + ? `CX_BOTTLE=${wineCrossoverBottle}` + : '', + fps: showFps ? `DXVK_HUD=fps` : '', + fsr: enableFSR ? 'WINE_FULLSCREEN_FSR=1' : '', + esync: enableEsync ? 'WINEESYNC=1' : '', + fsync: enableFsync ? 'WINEFSYNC=1' : '', + sharpness: enableFSR ? `WINE_FULLSCREEN_FSR_STRENGTH=${maxSharpness}` : '', + resizableBar: enableResizableBar ? `VKD3D_CONFIG=upload_hvv` : '', + other: otherOptions ? otherOptions : '', + prime: nvidiaPrime + ? 'DRI_PRIME=1 __NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia' + : '', + proton: isProton + ? `STEAM_COMPAT_CLIENT_INSTALL_PATH=${home}/.steam/steam STEAM_COMPAT_DATA_PATH='${winePrefix + .replaceAll("'", '') + .replace('~', home)}'` + : '' + } + + envVars = Object.values(options).join(' ') + if (isProton) { + logWarning( + [ + `You are using Proton, this can lead to some bugs, + please do not open issues with bugs related with games`, + wineVersion.name + ], + LogPrefix.Backend + ) + } + + await createNewPrefix(isProton, fixedWinePrefix, winePath, appName) + + // Install DXVK for non Proton/CrossOver Prefixes + if (!isProton && !isCrossover && autoInstallDxvk) { + await DXVK.installRemove(winePrefix, wineVersion.bin, 'dxvk', 'backup') + } + + if (wineVersion.name !== 'Wine Default') { + const { bin } = wineVersion + wineCommand = isProton + ? `--no-wine --wrapper "${bin} run"` + : `--wine ${bin}` + } + + let command = '' + if (isLegendary) { + command = `${envVars} ${runWithGameMode} ${mangohud} ${legendaryBin} launch ${appName} ${exe} ${runOffline} ${wineCommand} ${prefix} ${ + launchArguments ?? '' + } ${launcherArgs}` + logInfo(['Launch Command:', command], LogPrefix.Legendary) + } else if (isGOG) { + command = `${envVars} ${runWithGameMode} ${mangohud} ${gogdlBin} launch "${ + gameInfo.install.install_path + }" ${exe} ${appName} ${wineCommand} ${prefix} --os ${gameInfo.install.platform.toLowerCase()} ${ + launchArguments ?? '' + } ${launcherArgs}` + logInfo(['Launch Command:', command], LogPrefix.Gog) + } + + const startLaunch = await execAsync(command, execOptions) + .then(({ stderr }) => { + if (discordRPC) { + logInfo( + 'Stopping Discord Rich Presence if running...', + LogPrefix.Backend + ) + DiscordRPC.disconnect() + logInfo('Stopped Discord Rich Presence.', LogPrefix.Backend) + } + return { stderr, command, gameSettings } + }) + .catch((error) => { + logError(`${error}`, LogPrefix.Legendary) + const { stderr } = error + return { stderr, command, gameSettings } + }) + return startLaunch +} + +async function createNewPrefix( + isProton: boolean, + fixedWinePrefix: string, + winePath: string, + appName: string +) { + if (isMac) { + return + } + + if (isProton && !existsSync(fixedWinePrefix)) { + mkdirSync(fixedWinePrefix, { recursive: true }) + await setup(appName) + } + + if (!existsSync(fixedWinePrefix)) { + mkdirSync(fixedWinePrefix, { recursive: true }) + const initPrefixCommand = `WINEPREFIX='${fixedWinePrefix}' '${winePath}/wineboot' -i && '${winePath}/wineserver' --wait` + logInfo(['creating new prefix', fixedWinePrefix], LogPrefix.Backend) + return execAsync(initPrefixCommand) + .then(async () => { + logInfo('Prefix created succesfuly!', LogPrefix.Backend) + await setup(appName) + }) + .catch((error) => logError(`${error}`, LogPrefix.Backend)) + } +} + +export { launch } diff --git a/electron/legendary/games.ts b/electron/legendary/games.ts index 17d69525ba..e6cd3d2b49 100644 --- a/electron/legendary/games.ts +++ b/electron/legendary/games.ts @@ -1,36 +1,26 @@ -import { existsSync, mkdirSync, unlink, writeFile } from 'graceful-fs' +import { existsSync, mkdirSync } from 'graceful-fs' import axios from 'axios' -import { app, BrowserWindow, dialog, shell } from 'electron' -import { DXVK } from '../dxvk' -import { ExtraInfo, InstallArgs } from '../types' +import { BrowserWindow } from 'electron' +import { ExecResult, ExtraInfo, InstallArgs, LaunchResult } from '../types' import { Game } from '../games' import { GameConfig } from '../game_config' import { GlobalConfig } from '../config' import { LegendaryLibrary } from './library' import { LegendaryUser } from './user' -import { - errorHandler, - execAsync, - isEpicServiceOffline, - isOnline, - removeSpecialcharacters -} from '../utils' +import { errorHandler, execAsync, isOnline } from '../utils' import { execOptions, heroicGamesConfigPath, - heroicIconFolder, home, - isMac, isWindows, legendaryBin } from '../constants' -import { logError, logInfo, LogPrefix, logWarning } from '../logger/logger' +import { logError, logInfo, LogPrefix } from '../logger/logger' import { spawn } from 'child_process' import Store from 'electron-store' -import makeClient from 'discord-rich-presence-typescript' -import { platform } from 'os' -import i18next from 'i18next' +import { launch } from '../launcher' +import { addShortcuts, removeShortcuts } from '../shortcuts' const store = new Store({ cwd: 'lib-cache', @@ -235,6 +225,7 @@ class LegendaryGame extends Game { public async update() { this.window.webContents.send('setGameStatus', { appName: this.appName, + runner: 'legendary', status: 'updating' }) const { maxWorkers } = await GlobalConfig.get().getSettings() @@ -246,6 +237,7 @@ class LegendaryGame extends Game { .then(() => { this.window.webContents.send('setGameStatus', { appName: this.appName, + runner: 'legendary', status: 'done' }) return { status: 'done' } @@ -254,57 +246,13 @@ class LegendaryGame extends Game { logError(`${error}`, LogPrefix.Legendary) this.window.webContents.send('setGameStatus', { appName: this.appName, + runner: 'legendary', status: 'done' }) return { status: 'error' } }) } - public async getIcon(appName: string) { - if (!existsSync(heroicIconFolder)) { - mkdirSync(heroicIconFolder) - } - - const gameInfo = await this.getGameInfo() - const image = gameInfo.art_square.replaceAll(' ', '%20') - let ext = image.split('.').reverse()[0] - if (ext !== 'jpg' && ext !== 'png') { - ext = 'jpg' - } - const icon = `${heroicIconFolder}/${appName}.${ext}` - if (!existsSync(icon)) { - await execAsync(`curl '${image}' --output ${icon}`) - } - return icon - } - - private shortcutFiles(gameTitle: string) { - let desktopFile - let menuFile - - switch (process.platform) { - case 'linux': { - desktopFile = `${app.getPath('desktop')}/${gameTitle}.desktop` - menuFile = `${home}/.local/share/applications/${gameTitle}.desktop` - break - } - case 'win32': { - desktopFile = `${app.getPath('desktop')}\\${gameTitle}.lnk` - menuFile = `${app.getPath( - 'appData' - )}\\Microsoft\\Windows\\Start Menu\\Programs\\${gameTitle}.lnk` - break - } - default: - logError( - "Shortcuts haven't been implemented in the current platform.", - LogPrefix.Backend - ) - } - - return [desktopFile, menuFile] - } - /** * Adds a desktop shortcut to $HOME/Desktop and to /usr/share/applications * so that the game can be opened from the start menu and the desktop folder. @@ -313,63 +261,7 @@ class LegendaryGame extends Game { * @public */ public async addShortcuts(fromMenu?: boolean) { - if (process.platform === 'darwin') { - return - } - - const gameInfo = await this.getGameInfo() - const launchWithProtocol = `heroic://launch/${gameInfo.app_name}` - const [desktopFile, menuFile] = this.shortcutFiles(gameInfo.title) - const { addDesktopShortcuts, addStartMenuShortcuts } = - await GlobalConfig.get().getSettings() - - switch (process.platform) { - case 'linux': { - const icon = await this.getIcon(gameInfo.app_name) - const shortcut = `[Desktop Entry] -Name=${removeSpecialcharacters(gameInfo.title)} -Exec=xdg-open ${launchWithProtocol} -Terminal=false -Type=Application -MimeType=x-scheme-handler/heroic; -Icon=${icon} -Categories=Game; -` - - if (addDesktopShortcuts || fromMenu) { - writeFile(desktopFile, shortcut, () => { - logInfo('Shortcut saved on ' + desktopFile, LogPrefix.Backend) - }) - } - if (addStartMenuShortcuts || fromMenu) { - writeFile(menuFile, shortcut, () => { - logInfo('Shortcut saved on ' + menuFile, LogPrefix.Backend) - }) - } - break - } - case 'win32': { - const shortcutOptions = { - target: launchWithProtocol, - icon: `${gameInfo.install.install_path}\\${gameInfo.install.executable}`, - iconIndex: 0 - } - - if (addDesktopShortcuts || fromMenu) { - shell.writeShortcutLink(desktopFile, shortcutOptions) - } - - if (addStartMenuShortcuts || fromMenu) { - shell.writeShortcutLink(menuFile, shortcutOptions) - } - break - } - default: - logError( - "Shortcuts haven't been implemented in the current platform.", - LogPrefix.Backend - ) - } + return addShortcuts(await this.getGameInfo(), fromMenu) } /** @@ -378,19 +270,7 @@ Categories=Game; * @public */ public async removeShortcuts() { - const gameInfo = await this.getGameInfo() - const [desktopFile, menuFile] = this.shortcutFiles(gameInfo.title) - - if (desktopFile) { - unlink(desktopFile, () => - logInfo('Desktop shortcut removed', LogPrefix.Backend) - ) - } - if (menuFile) { - unlink(menuFile, () => - logInfo('Applications shortcut removed', LogPrefix.Backend) - ) - } + return removeShortcuts(this.appName, 'legendary') } private getSdlList(sdlList: Array) { @@ -447,12 +327,14 @@ Categories=Game; ) LegendaryLibrary.get().installState(this.appName, false) return await execAsync(command, execOptions) - .then((v) => { - return v + .then((value) => { + return value + }) + .catch((error) => { + logError(`${error}`, LogPrefix.Legendary) + return null }) - .catch((error) => logError(`${error}`, LogPrefix.Legendary)) } - /** * Repair game. * Does NOT check for online connectivity. @@ -471,11 +353,14 @@ Categories=Game; logInfo([`Repairing ${this.appName} with:`, command], LogPrefix.Legendary) return await execAsync(command, execOptions) - .then((v) => { + .then((value) => { // this.state.status = 'done' - return v + return value + }) + .catch((error) => { + logError(`${error}`, LogPrefix.Legendary) + return null }) - .catch((error) => logError(`${error}`, LogPrefix.Legendary)) } public async import(path: string) { @@ -486,10 +371,13 @@ Categories=Game; LogPrefix.Legendary ) return await execAsync(command, execOptions) - .then((v) => { - return v + .then((value) => { + return value + }) + .catch((error) => { + logError(`${error}`, LogPrefix.Legendary) + return null }) - .catch((error) => logError(`${error}`, LogPrefix.Legendary)) } /** @@ -515,247 +403,10 @@ Categories=Game; return await execAsync(command, execOptions) } - public async launch(launchArguments?: string) { - const epicOffline = await isEpicServiceOffline() - const isOffline = !(await isOnline()) || epicOffline - let envVars = '' - let gameMode: string - const gameSettings = await this.getSettings() - const gameInfo = await this.getGameInfo() - - const { - winePrefix, - wineVersion, - wineCrossoverBottle, - otherOptions, - useGameMode, - showFps, - nvidiaPrime, - launcherArgs = '', - showMangohud, - audioFix, - autoInstallDxvk, - autoInstallVkd3d, - offlineMode, - enableFSR, - maxSharpness, - enableResizableBar, - enableEsync, - enableFsync, - targetExe - } = gameSettings - - const { discordRPC } = await GlobalConfig.get().getSettings() - const DiscordRPC = discordRPC ? makeClient('852942976564723722') : null - - let runOffline = '' - if (isOffline || offlineMode) { - if (gameInfo.canRunOffline) { - runOffline = '--offline' - } else { - return dialog.showErrorBox( - i18next.t( - 'box.error.no-offline-mode.title', - 'Offline mode not supported.' - ), - i18next.t( - 'box.error.no-offline-mode.message', - 'Launch aborted! The game requires a internet connection to run it.' - ) - ) - } - } - - const exe = targetExe ? `--override-exe ${targetExe}` : '' - const isMacNative = gameInfo.is_mac_native - const mangohud = showMangohud ? 'mangohud --dlsym' : '' - - if (discordRPC) { - // Show DiscordRPC - // This seems to run when a game is updated, even though the game doesn't start after updating. - let os: string - - switch (process.platform) { - case 'linux': - os = 'Linux' - break - case 'win32': - os = 'Windows' - break - case 'darwin': - os = 'macOS' - break - default: - os = 'Unknown OS' - break - } - - logInfo( - 'Updating Discord Rich Presence information...', - LogPrefix.Backend - ) - DiscordRPC.updatePresence({ - details: gameInfo.title, - instance: true, - largeImageKey: 'icon', - large_text: gameInfo.title, - startTimestamp: Date.now(), - state: 'via Heroic on ' + os - }) - } - - if (isWindows || (isMac && isMacNative)) { - const command = `${legendaryBin} launch ${ - this.appName - } ${exe} ${runOffline} ${launchArguments ?? ''} ${launcherArgs}` - logInfo(['Launch Command:', command], LogPrefix.Legendary) - const v = await execAsync(command, execOptions) - if (discordRPC) { - logInfo( - 'Stopping Discord Rich Presence if running...', - LogPrefix.Backend - ) - DiscordRPC.disconnect() - logInfo('Stopped Discord Rich Presence.', LogPrefix.Backend) - } - - return v - } - - if (!existsSync(wineVersion.bin.replaceAll("'", ''))) { - return dialog.showErrorBox( - i18next.t('box.error.wine-not-found.title', 'Wine Not Found'), - i18next.t( - 'box.error.wine-not-found.message', - 'No Wine Version Selected. Check Game Settings!' - ) - ) - } - - const fixedWinePrefix = winePrefix.replace('~', home) - let wineCommand = `--wine ${wineVersion.bin}` - - // We need to keep replacing the ' to keep compatibility with old configs - let prefix = `--wine-prefix '${fixedWinePrefix.replaceAll("'", '')}'` - - const isProton = - wineVersion.name.includes('Proton') || wineVersion.name.includes('Steam') - const isCrossover = wineVersion.name.includes('CrossOver') - prefix = isProton || isCrossover ? '' : prefix - const x = wineVersion.bin.split('/') - x.pop() - const winePath = x.join('/').replaceAll("'", '') - const options = { - audio: audioFix ? `PULSE_LATENCY_MSEC=60` : '', - crossoverBottle: - isCrossover && wineCrossoverBottle != '' - ? `CX_BOTTLE=${wineCrossoverBottle}` - : '', - fps: showFps ? `DXVK_HUD=fps` : '', - fsr: enableFSR ? 'WINE_FULLSCREEN_FSR=1' : '', - esync: enableEsync ? 'WINEESYNC=1' : '', - fsync: enableFsync ? 'WINEFSYNC=1' : '', - sharpness: enableFSR - ? `WINE_FULLSCREEN_FSR_STRENGTH=${maxSharpness}` - : '', - resizableBar: enableResizableBar ? `VKD3D_CONFIG=upload_hvv` : '', - other: otherOptions ? otherOptions : '', - prime: nvidiaPrime - ? 'DRI_PRIME=1 __NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia' - : '', - proton: isProton - ? `STEAM_COMPAT_CLIENT_INSTALL_PATH=${home}/.steam/steam STEAM_COMPAT_DATA_PATH='${winePrefix - .replaceAll("'", '') - .replace('~', home)}'` - : '' - } - - envVars = Object.values(options).join(' ') - if (isProton) { - logWarning( - [ - `You are using Proton, this can lead to some bugs, - please do not open issues with bugs related with games`, - wineVersion.name - ], - LogPrefix.Backend - ) - } - - await this.createNewPrefix(isProton, fixedWinePrefix, winePath) - - // Install DXVK for non Proton/CrossOver Prefixes - if (!isProton && !isCrossover && autoInstallDxvk) { - await DXVK.installRemove(winePrefix, wineVersion.bin, 'dxvk', 'backup') - } - - // Install VKD3D for non Proton/CrossOver Prefixes - if (!isProton && !isCrossover && autoInstallVkd3d) { - await DXVK.installRemove(winePrefix, winePath, 'vkd3d', 'backup') - } - - if (wineVersion.name !== 'Wine Default') { - const { bin } = wineVersion - wineCommand = isProton - ? `--no-wine --wrapper "${bin} run"` - : `--wine ${bin}` - } - - // check if Gamemode is installed - await execAsync(`which gamemoderun`) - .then(({ stdout }) => (gameMode = stdout.split('\n')[0])) - .catch(() => logWarning('GameMode not installed', LogPrefix.Backend)) - - const runWithGameMode = useGameMode && gameMode ? gameMode : '' - - const command = `${envVars} ${runWithGameMode} ${mangohud} ${legendaryBin} launch ${ - this.appName - } ${exe} ${runOffline} ${wineCommand} ${prefix} ${ - launchArguments ?? '' - } ${launcherArgs}` - logInfo(['Launch Command:', command], LogPrefix.Legendary) - - const startLaunch = await execAsync(command, execOptions) - .then(({ stderr }) => { - if (discordRPC) { - logInfo( - 'Stopping Discord Rich Presence if running...', - LogPrefix.Backend - ) - DiscordRPC.disconnect() - logInfo('Stopped Discord Rich Presence.', LogPrefix.Backend) - } - return { stderr, command, gameSettings } - }) - .catch((error) => { - logError(`${error}`, LogPrefix.Legendary) - const { stderr } = error - return { stderr, command, gameSettings } - }) - return startLaunch - } - - private async createNewPrefix( - isProton: boolean, - fixedWinePrefix: string, - winePath: string - ) { - if (platform() === 'darwin') { - return - } - - if (isProton && !existsSync(fixedWinePrefix)) { - mkdirSync(fixedWinePrefix, { recursive: true }) - } - - if (!existsSync(fixedWinePrefix)) { - mkdirSync(fixedWinePrefix, { recursive: true }) - const initPrefixCommand = `WINEPREFIX='${fixedWinePrefix}' '${winePath}/wineboot' -i` - logInfo(['creating new prefix', fixedWinePrefix], LogPrefix.Backend) - return execAsync(initPrefixCommand) - .then(() => logInfo('Prefix created succesfuly!', LogPrefix.Backend)) - .catch((error) => logError(`${error}`, LogPrefix.Backend)) - } + public async launch( + launchArguments?: string + ): Promise { + return launch(this.appName, launchArguments, 'legendary') } public async stop() { diff --git a/electron/legendary/library.ts b/electron/legendary/library.ts index e5c7c3e87a..b0572f568d 100644 --- a/electron/legendary/library.ts +++ b/electron/legendary/library.ts @@ -18,6 +18,7 @@ import { LegendaryGame } from './games' import { LegendaryUser } from './user' import { execAsync, isEpicServiceOffline, isOnline } from '../utils' import { + fallBackImage, installed, legendaryBin, legendaryConfigPath, @@ -424,9 +425,6 @@ class LegendaryLibrary { )[0] : keyImages.filter(({ type }: KeyImage) => type === 'Thumbnail')[0] - const fallBackImage = - 'https://user-images.githubusercontent.com/26871415/103480183-1fb00680-4dd3-11eb-9171-d8c4cc601fba.jpg' - const art_cover = gameBox ? gameBox.url : null const art_logo = logo ? logo.url : null const art_square = gameBoxTall ? gameBoxTall.url : null @@ -481,7 +479,9 @@ class LegendaryLibrary { : releaseInfo[0]?.platform.includes('Mac'), save_folder: saveFolder, title, - canRunOffline + canRunOffline, + is_linux_native: false, + runner: 'legendary' } as GameInfo) return app_name diff --git a/electron/legendary/user.ts b/electron/legendary/user.ts index 56daac7f3e..88778a2410 100644 --- a/electron/legendary/user.ts +++ b/electron/legendary/user.ts @@ -39,7 +39,7 @@ export class LegendaryUser { }) child.on('close', () => { logInfo('finished login', LogPrefix.Legendary) - res('finished') + this.getUserInfo().then(() => res('finished')) }) }) } diff --git a/electron/main.ts b/electron/main.ts index 2e2e0f9055..785bbca800 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -3,7 +3,8 @@ import { LaunchResult, GamepadInputEventKey, GamepadInputEventWheel, - GamepadInputEventMouse + GamepadInputEventMouse, + Runner } from './types' import * as path from 'path' import { @@ -35,6 +36,8 @@ import { GameConfig } from './game_config' import { GlobalConfig } from './config' import { LegendaryLibrary } from './legendary/library' import { LegendaryUser } from './legendary/user' +import { GOGUser } from './gog/user' +import { GOGLibrary } from './gog/library' import { checkForUpdates, clearCache, @@ -64,7 +67,7 @@ import { installed, kofiPage, legendaryBin, - loginUrl, + epicLoginUrl, patreonPage, sidInfoUrl, supportURL, @@ -136,6 +139,7 @@ async function createWindow(): Promise { GlobalConfig.get() LegendaryLibrary.get() + GOGLibrary.get() mainWindow.setIcon(icon) app.setAppUserModelId('Heroic') @@ -265,6 +269,11 @@ if (!gotTheLock) { store.delete('userinfo') } + // Update user details + if (GOGUser.isLoggedIn()) { + GOGUser.getUserDetails() + } + await i18next.use(Backend).init({ backend: { addPath: path.join(__dirname, '/locales/{{lng}}/{{ns}}'), @@ -397,8 +406,8 @@ ipcMain.on('unlock', () => { } }) -ipcMain.handle('kill', async (event, appName) => { - return await Game.get(appName).stop() +ipcMain.handle('kill', async (event, appName, runner) => { + return await Game.get(appName, runner).stop() }) ipcMain.on('quit', async () => handleExit()) @@ -422,7 +431,7 @@ ipcMain.on('openSupportPage', () => openUrlOrFile(supportURL)) ipcMain.on('openReleases', () => openUrlOrFile(heroicGithubURL)) ipcMain.on('openWeblate', () => openUrlOrFile(weblateUrl)) ipcMain.on('showAboutWindow', () => showAboutWindow()) -ipcMain.on('openLoginPage', () => openUrlOrFile(loginUrl)) +ipcMain.on('openLoginPage', () => openUrlOrFile(epicLoginUrl)) ipcMain.on('openDiscordLink', () => openUrlOrFile(discordLink)) ipcMain.on('openPatreonPage', () => openUrlOrFile(patreonPage)) ipcMain.on('openKofiPage', () => openUrlOrFile(kofiPage)) @@ -507,9 +516,11 @@ ipcMain.handle( /// IPC handlers begin here. -ipcMain.handle('checkGameUpdates', () => - LegendaryLibrary.get().listUpdateableGames() -) +ipcMain.handle('checkGameUpdates', async () => { + const legendaryUpdates = await LegendaryLibrary.get().listUpdateableGames() + const gogUpdates = await GOGLibrary.get().listUpdateableGames() + return [...legendaryUpdates, ...gogUpdates] +}) ipcMain.handle('getEpicGamesStatus', () => isEpicServiceOffline()) @@ -551,36 +562,49 @@ ipcMain.on('resetHeroic', async () => { } }) +ipcMain.handle('authGOG', (event, code) => + GOGUser.login(code).then(() => + mainWindow.webContents.send('updateLoginState') + ) +) + ipcMain.on('createNewWindow', (e, url) => new BrowserWindow({ height: 700, width: 1200 }).loadURL(url) ) -ipcMain.handle('getGameInfo', async (event, game) => { +ipcMain.handle('getGameInfo', async (event, game, runner) => { try { - const info = await Game.get(game).getGameInfo() - info.extra = await Game.get(game).getExtraInfo(info.namespace) + const info = await Game.get(game, runner).getGameInfo() + if (!info) { + return null + } + info.extra = await Game.get(game, runner).getExtraInfo(info.namespace) return info } catch (error) { logError(`${error}`, LogPrefix.Backend) } }) -ipcMain.handle('getGameSettings', async (event, game) => { +ipcMain.handle('getGameSettings', async (event, game, runner) => { try { - const settings = await Game.get(game).getSettings() + const settings = await Game.get(game, runner).getSettings() return settings } catch (error) { logError(`${error}`, LogPrefix.Backend) } }) -ipcMain.handle('getInstallInfo', async (event, game) => { +ipcMain.handle('getGOGLinuxInstallersLangs', async (event, appName) => { + return await GOGLibrary.getLinuxInstallersLanguages(appName) +}) + +ipcMain.handle('getInstallInfo', async (event, game, runner) => { const online = await isOnline() if (!online) { return { game: {}, metadata: {} } } try { - const info = await Game.get(game).getInstallInfo() + const info = await Game.get(game, runner).getInstallInfo() return info } catch (error) { logError(`${error}`, LogPrefix.Backend) @@ -593,9 +617,15 @@ ipcMain.handle('getUserInfo', async () => await LegendaryUser.getUserInfo()) // Checks if the user have logged in with Legendary already ipcMain.handle('isLoggedIn', async () => await LegendaryUser.isLoggedIn()) -ipcMain.handle('login', async (event, sid) => await LegendaryUser.login(sid)) +ipcMain.handle('login', async (event, sid) => + LegendaryUser.login(sid).then((value) => { + mainWindow.webContents.send('updateLoginState') + return value + }) +) -ipcMain.handle('logout', async () => await LegendaryUser.logout()) +ipcMain.handle('logoutLegendary', async () => await LegendaryUser.logout()) +ipcMain.handle('logoutGOG', async () => GOGUser.logout()) ipcMain.handle('getAlternativeWine', () => GlobalConfig.get().getAlternativeWine() @@ -651,6 +681,7 @@ if (existsSync(installed)) { } ipcMain.handle('refreshLibrary', async (e, fullRefresh) => { + await GOGLibrary.get().sync() return await LegendaryLibrary.get().getGames('info', fullRefresh) }) @@ -665,19 +696,22 @@ type RecentGame = { type LaunchParams = { appName: string launchArguments: string + runner: Runner } ipcMain.handle( 'launch', - async (event, { appName, launchArguments }: LaunchParams) => { + async (event, { appName, launchArguments, runner }: LaunchParams) => { const window = BrowserWindow.getAllWindows()[0] window.webContents.send('setGameStatus', { appName, + runner, status: 'playing' }) const recentGames = (store.get('games.recent') as Array) || [] const game = appName.split(' ')[0] - const { title } = await Game.get(game).getGameInfo() + const gameData = await Game.get(game, runner).getGameInfo() + const { title } = gameData const MAX_RECENT_GAMES = GlobalConfig.get().config.maxRecentGames || 5 const startPlayingDate = new Date() @@ -705,7 +739,7 @@ ipcMain.handle( store.set('games.recent', [{ game, title: title }]) } - return Game.get(appName) + return Game.get(appName, runner) .launch(launchArguments) .then(async ({ stderr, command, gameSettings }: LaunchResult) => { const finishedPlayingDate = new Date() @@ -724,7 +758,7 @@ ipcMain.handle( ${systemInfo} Game Settings: ${JSON.stringify(gameSettings, null, '\t')} - Legendary Log: + Game Log: ${stderr} ` @@ -739,6 +773,7 @@ ipcMain.handle( } window.webContents.send('setGameStatus', { appName, + runner, status: 'done' }) @@ -759,6 +794,7 @@ ipcMain.handle( logError(stderr, LogPrefix.Backend) window.webContents.send('setGameStatus', { appName, + runner, status: 'done' }) return stderr @@ -801,10 +837,18 @@ ipcMain.handle( ) ipcMain.handle('install', async (event, params) => { - const { appName, path, installDlcs, sdlList } = params as InstallParams - const { title, is_mac_native } = await Game.get(appName).getGameInfo() + const { appName, path, installDlcs, sdlList, runner, installLanguage } = + params as InstallParams + const { title, is_mac_native, is_linux_native } = await Game.get( + appName, + runner + ).getGameInfo() const platformToInstall = - platform() === 'darwin' && is_mac_native ? 'Mac' : 'Windows' + platform() === 'darwin' && is_mac_native + ? 'Mac' + : platform() === 'linux' && is_linux_native + ? 'Linux' + : 'Windows' if (!(await isOnline())) { logWarning( @@ -815,7 +859,7 @@ ipcMain.handle('install', async (event, params) => { } const epicOffline = await isEpicServiceOffline() - if (epicOffline) { + if (epicOffline && runner === 'legendary') { dialog.showErrorBox( i18next.t('box.warning.title', 'Warning'), i18next.t( @@ -828,6 +872,7 @@ ipcMain.handle('install', async (event, params) => { mainWindow.webContents.send('setGameStatus', { appName, + runner, status: 'installing', folder: path }) @@ -836,8 +881,8 @@ ipcMain.handle('install', async (event, params) => { title, body: i18next.t('notify.install.startInstall', 'Installation Started') }) - return Game.get(appName) - .install({ path, installDlcs, sdlList, platformToInstall }) + return Game.get(appName, runner) + .install({ path, installDlcs, sdlList, platformToInstall, installLanguage }) .then(async (res) => { notify({ title, @@ -849,6 +894,7 @@ ipcMain.handle('install', async (event, params) => { logInfo('finished installing', LogPrefix.Backend) mainWindow.webContents.send('setGameStatus', { appName, + runner, status: 'done' }) return res @@ -857,6 +903,7 @@ ipcMain.handle('install', async (event, params) => { notify({ title, body: i18next.t('notify.install.canceled') }) mainWindow.webContents.send('setGameStatus', { appName, + runner, status: 'done' }) return res @@ -864,15 +911,20 @@ ipcMain.handle('install', async (event, params) => { }) ipcMain.handle('uninstall', async (event, args) => { - const title = (await Game.get(args[0]).getGameInfo()).title - const winePrefix = (await Game.get(args[0]).getSettings()).winePrefix + const [appName, shouldRemovePrefix, runner] = args + + const title = (await Game.get(appName, runner).getGameInfo()).title + const winePrefix = (await Game.get(appName, runner).getSettings()).winePrefix - return Game.get(args[0]) + return Game.get(appName, runner) .uninstall() .then(() => { - if (args[1]) { + if (shouldRemovePrefix) { logInfo(`Removing prefix ${winePrefix}`) - rmSync(winePrefix, { recursive: true }) // remove prefix + if (existsSync(winePrefix)) { + // remove prefix if exists + rmSync(winePrefix, { recursive: true }) + } } notify({ title, body: i18next.t('notify.uninstalled') }) logInfo('finished uninstalling', LogPrefix.Backend) @@ -880,7 +932,7 @@ ipcMain.handle('uninstall', async (event, args) => { .catch((error) => logError(`${error}`, LogPrefix.Backend)) }) -ipcMain.handle('repair', async (event, game) => { +ipcMain.handle('repair', async (event, game, runner) => { if (!(await isOnline())) { logWarning( `App offline, skipping repair for game '${game}'.`, @@ -888,9 +940,9 @@ ipcMain.handle('repair', async (event, game) => { ) return } - const title = (await Game.get(game).getGameInfo()).title + const title = (await Game.get(game, runner).getGameInfo()).title - return Game.get(game) + return Game.get(game, runner) .repair() .then(() => { notify({ title, body: i18next.t('notify.finished.reparing') }) @@ -905,10 +957,10 @@ ipcMain.handle('repair', async (event, game) => { }) }) -ipcMain.handle('moveInstall', async (event, [appName, path]: string[]) => { - const title = (await Game.get(appName).getGameInfo()).title +ipcMain.handle('moveInstall', async (event, [appName, path, runner]) => { + const title = (await Game.get(appName, runner).getGameInfo()).title try { - const newPath = await Game.get(appName).moveInstall(path) + const newPath = await Game.get(appName, runner).moveInstall(path) notify({ title, body: i18next.t('notify.moved') }) logInfo(`Finished moving ${appName} to ${newPath}.`, LogPrefix.Backend) } catch (error) { @@ -921,8 +973,9 @@ ipcMain.handle('moveInstall', async (event, [appName, path]: string[]) => { }) ipcMain.handle('importGame', async (event, args) => { + const { appName, path, runner } = args const epicOffline = await isEpicServiceOffline() - if (epicOffline) { + if (epicOffline && runner === 'legendary') { dialog.showErrorBox( i18next.t('box.warning.title', 'Warning'), i18next.t( @@ -932,13 +985,13 @@ ipcMain.handle('importGame', async (event, args) => { ) return { status: 'error' } } - const { appName, path } = args - const title = (await Game.get(appName).getGameInfo()).title + const title = (await Game.get(appName, runner).getGameInfo()).title mainWindow.webContents.send('setGameStatus', { appName, + runner, status: 'installing' }) - Game.get(appName) + Game.get(appName, runner) .import(path) .then(() => { notify({ @@ -947,6 +1000,7 @@ ipcMain.handle('importGame', async (event, args) => { }) mainWindow.webContents.send('setGameStatus', { appName, + runner, status: 'done' }) logInfo(`imported ${title}`, LogPrefix.Backend) @@ -955,13 +1009,14 @@ ipcMain.handle('importGame', async (event, args) => { notify({ title, body: i18next.t('notify.install.canceled') }) mainWindow.webContents.send('setGameStatus', { appName, + runner, status: 'done' }) logInfo(err, LogPrefix.Backend) }) }) -ipcMain.handle('updateGame', async (e, game) => { +ipcMain.handle('updateGame', async (e, game, runner) => { if (!(await isOnline())) { logWarning( `App offline, skipping install for game '${game}'.`, @@ -971,7 +1026,7 @@ ipcMain.handle('updateGame', async (e, game) => { } const epicOffline = await isEpicServiceOffline() - if (epicOffline) { + if (epicOffline && runner === 'legendary') { dialog.showErrorBox( i18next.t('box.warning.title', 'Warning'), i18next.t( @@ -982,10 +1037,10 @@ ipcMain.handle('updateGame', async (e, game) => { return { status: 'error' } } - const title = (await Game.get(game).getGameInfo()).title + const title = (await Game.get(game, runner).getGameInfo()).title notify({ title, body: i18next.t('notify.update.started', 'Update Started') }) - return Game.get(game) + return Game.get(game, runner) .update() .then(({ status }) => { notify({ @@ -1056,8 +1111,20 @@ ipcMain.handle('requestGameProgress', async (event, appName) => { ipcMain.handle( 'changeInstallPath', - async (event, [appName, newPath]: string[]) => { - LegendaryLibrary.get().changeGameInstallPath(appName, newPath) + async (event, [appName, newPath, runner]: string[]) => { + let instance = null + switch (runner) { + case 'legendary': + instance = LegendaryLibrary.get() + break + case 'gog': + instance = GOGLibrary.get() + break + default: + logError(`Unsupported runner ${runner}`, LogPrefix.Backend) + return + } + instance.changeGameInstallPath(appName, newPath) logInfo(`Finished moving ${appName} to ${newPath}.`, LogPrefix.Backend) } ) @@ -1095,21 +1162,24 @@ ipcMain.handle('egsSync', async (event, args) => { } }) -ipcMain.on('addShortcut', async (event, appName: string, fromMenu: boolean) => { - const game = Game.get(appName) - game.addShortcuts(fromMenu) - openMessageBox({ - buttons: [i18next.t('box.ok', 'Ok')], - message: i18next.t( - 'box.shortcuts.message', - 'Shortcuts were created on Desktop and Start Menu' - ), - title: i18next.t('box.shortcuts.title', 'Shortcuts') - }) -}) +ipcMain.on( + 'addShortcut', + async (event, appName: string, runner: Runner, fromMenu: boolean) => { + const game = Game.get(appName, runner) + game.addShortcuts(fromMenu) + openMessageBox({ + buttons: [i18next.t('box.ok', 'Ok')], + message: i18next.t( + 'box.shortcuts.message', + 'Shortcuts were created on Desktop and Start Menu' + ), + title: i18next.t('box.shortcuts.title', 'Shortcuts') + }) + } +) -ipcMain.on('removeShortcut', async (event, appName: string) => { - const game = Game.get(appName) +ipcMain.on('removeShortcut', async (event, appName: string, runner: Runner) => { + const game = Game.get(appName, runner) game.removeShortcuts() }) diff --git a/electron/protocol.ts b/electron/protocol.ts index bf4e8b7cea..1efd37d1f6 100644 --- a/electron/protocol.ts +++ b/electron/protocol.ts @@ -1,5 +1,5 @@ import { BrowserWindow, dialog } from 'electron' -import { Game } from './games' +import { Game, Runner } from './games' import { logInfo, LogPrefix } from './logger/logger' import i18next from 'i18next' @@ -19,8 +19,13 @@ export async function handleProtocol(window: BrowserWindow, url: string) { return logInfo(['Received ping! Arg:', arg], LogPrefix.ProtocolHandler) } if (command === 'launch') { - const game = Game.get(arg) - const { is_installed, title, app_name } = await game.getGameInfo() + let runner: Runner = 'legendary' + let game = await Game.get(arg, runner).getGameInfo() + if (!game) { + runner = 'gog' + game = await Game.get(arg, runner).getGameInfo() + } + const { is_installed, title, app_name } = game setTimeout(async () => { // wait for the frontend to be ready if (!is_installed) { @@ -46,6 +51,7 @@ export async function handleProtocol(window: BrowserWindow, url: string) { if (filePaths[0]) { return window.webContents.send('installGame', { appName: app_name, + runner, installPath: filePaths[0] }) } @@ -55,7 +61,7 @@ export async function handleProtocol(window: BrowserWindow, url: string) { } } mainWindow.hide() - window.webContents.send('launchGame', arg) + window.webContents.send('launchGame', arg, runner) }, 3000) } } diff --git a/electron/shortcuts.ts b/electron/shortcuts.ts new file mode 100644 index 0000000000..1f045ac244 --- /dev/null +++ b/electron/shortcuts.ts @@ -0,0 +1,158 @@ +import { app, shell } from 'electron' +import { existsSync, mkdirSync, unlink, writeFile } from 'graceful-fs' +import { logError, logInfo, LogPrefix } from './logger/logger' +import { GlobalConfig } from './config' +import { execAsync, removeSpecialcharacters } from './utils' +import { Game } from './games' +import { Runner, GameInfo } from './types' +import { heroicIconFolder, home } from './constants' +import { GOGLibrary } from './gog/library' + +/** + * Adds a desktop shortcut to $HOME/Desktop and to /usr/share/applications + * so that the game can be opened from the start menu and the desktop folder. + * Both can be disabled with addDesktopShortcuts and addStartMenuShortcuts + * @async + * @public + */ +async function addShortcuts(gameInfo: GameInfo, fromMenu?: boolean) { + if (process.platform === 'darwin') { + return + } + + const launchWithProtocol = `heroic://launch/${gameInfo.app_name}` + const [desktopFile, menuFile] = shortcutFiles(gameInfo.title) + const { addDesktopShortcuts, addStartMenuShortcuts } = + await GlobalConfig.get().getSettings() + + switch (process.platform) { + case 'linux': { + const icon = await getIcon(gameInfo.app_name, gameInfo) + const shortcut = `[Desktop Entry] +Name=${removeSpecialcharacters(gameInfo.title)} +Exec=xdg-open ${launchWithProtocol} +Terminal=false +Type=Application +MimeType=x-scheme-handler/heroic; +Icon=${icon} +Categories=Game; +` + + if (addDesktopShortcuts || fromMenu) { + writeFile(desktopFile, shortcut, () => { + logInfo(`Shortcut saved on ${desktopFile}`, LogPrefix.Backend) + }) + } + if (addStartMenuShortcuts || fromMenu) { + writeFile(menuFile, shortcut, () => { + logInfo(`Shortcut saved on ${menuFile}`, LogPrefix.Backend) + }) + } + break + } + case 'win32': { + let executable = gameInfo.install.executable + if (gameInfo.runner === 'gog') { + executable = GOGLibrary.get().getExecutable(gameInfo.app_name) + } + const icon = `${gameInfo.install.install_path}\\${executable}` + + const shortcutOptions = { + target: launchWithProtocol, + icon, + iconIndex: 0 + } + + if (addDesktopShortcuts || fromMenu) { + shell.writeShortcutLink(desktopFile, shortcutOptions) + } + + if (addStartMenuShortcuts || fromMenu) { + shell.writeShortcutLink(menuFile, shortcutOptions) + } + break + } + default: + logError( + "Shortcuts haven't been implemented in the current platform.", + LogPrefix.Backend + ) + } +} + +/** + * Removes a desktop shortcut from $HOME/Desktop and to $HOME/.local/share/applications + * @async + * @public + */ +async function removeShortcuts(appName: string, runner: Runner) { + const gameInfo = await Game.get(appName, runner).getGameInfo() + const [desktopFile, menuFile] = shortcutFiles(gameInfo.title) + + if (desktopFile) { + unlink(desktopFile, () => + logInfo('Desktop shortcut removed', LogPrefix.Backend) + ) + } + if (menuFile) { + unlink(menuFile, () => + logInfo('Applications shortcut removed', LogPrefix.Backend) + ) + } +} + +function shortcutFiles(gameTitle: string) { + let desktopFile + let menuFile + + switch (process.platform) { + case 'linux': { + desktopFile = `${app.getPath('desktop')}/${gameTitle}.desktop` + menuFile = `${home}/.local/share/applications/${gameTitle}.desktop` + break + } + case 'win32': { + desktopFile = `${app.getPath('desktop')}\\${gameTitle}.lnk` + menuFile = `${app.getPath( + 'appData' + )}\\Microsoft\\Windows\\Start Menu\\Programs\\${gameTitle}.lnk` + break + } + default: + logError( + "Shortcuts haven't been implemented in the current platform.", + LogPrefix.Backend + ) + } + + return [desktopFile, menuFile] +} + +async function getIcon(appName: string, gameInfo: GameInfo) { + if (!existsSync(heroicIconFolder)) { + mkdirSync(heroicIconFolder) + } + if (gameInfo.runner == 'legendary') { + const image = gameInfo.art_square.replaceAll(' ', '%20') + let ext = image.split('.').reverse()[0] + if (ext !== 'jpg' && ext !== 'png') { + ext = 'jpg' + } + const icon = `${heroicIconFolder}/${appName}.${ext}` + if (!existsSync(icon)) { + await execAsync(`curl '${image}' --output ${icon}`) + } + return icon + } else if (gameInfo.runner == 'gog') { + const apiData = await GOGLibrary.get().getGamesData(appName) + let iconUrl = apiData?._links?.icon.href + iconUrl = iconUrl.replace('{ext}', 'png') + const icon = `${heroicIconFolder}/${appName}.png` + if (!existsSync(icon)) { + await execAsync(`curl '${iconUrl}' --output ${icon}`) + } + return icon + } +} + +export { removeShortcuts, addShortcuts } diff --git a/electron/types.ts b/electron/types.ts index a6bd7b0bab..ab91848a4b 100644 --- a/electron/types.ts +++ b/electron/types.ts @@ -1,3 +1,5 @@ +export type Runner = 'legendary' | 'gog' | 'heroic' + interface About { description: string shortDescription: string @@ -44,9 +46,10 @@ export interface AppSettings { winePrefix: string defaultWinePrefix: string wineVersion: WineInstallation + useSteamRuntime: boolean } -export type ExecResult = void | { stderr: string; stdout: string } +export type ExecResult = { stderr: string; stdout: string } export type LaunchResult = { stderr: string @@ -61,6 +64,8 @@ export interface ExtraInfo { export type GameConfigVersion = 'auto' | 'v0' | 'v0.1' export interface GameInfo { + runner: Runner + store_url: string app_name: string art_cover: string art_logo: string @@ -81,6 +86,7 @@ export interface GameInfo { title: string canRunOffline: boolean is_mac_native: boolean + is_linux_native: boolean } type DLCInfo = { @@ -95,6 +101,7 @@ type GameInstallInfo = { title: string version: string platform_versions: { Mac: string; Windows: string } + buildId?: string } type Prerequisites = { @@ -110,6 +117,8 @@ type GameManifest = { install_tags: Array launch_exe: string prerequisites: Prerequisites + languages?: Array + versionEtag?: string } export interface InstallInfo { game: GameInstallInfo @@ -135,6 +144,7 @@ export interface GameSettings { showMangohud: boolean targetExe: string useGameMode: boolean + useSteamRuntime: boolean wineCrossoverBottle: string winePrefix: string wineVersion: WineInstallation @@ -169,6 +179,11 @@ export interface InstalledInfo { is_dlc: boolean version: string | null platform: string + appName?: string + installedWithDLCs?: boolean // For verifing GOG games + language?: string // For verifing GOG games + versionEtag?: string // Checksum for checking GOG updates + buildId?: string // For verifing GOG games } export interface KeyImage { type: string @@ -222,7 +237,8 @@ export interface InstallArgs { path: string installDlcs?: boolean sdlList?: Array - platformToInstall: 'Windows' | 'Mac' + platformToInstall: 'Windows' | 'Mac' | 'Linux' + installLanguage?: string } export interface InstallParams { @@ -230,6 +246,71 @@ export interface InstallParams { path: string installDlcs?: boolean sdlList?: Array + installLanguage?: string + runner: Runner +} + +export interface GOGLoginData { + expires_in: number + access_token: string + refresh_token: string + user_id: string + loginTime: number +} + +export interface GOGGameInfo { + tags: string[] + id: number + image: string + availability: { + isAvailable: boolean + isAvailableInAccount: boolean + } + title: string + url: string + worksOn: { + Windows: boolean + Mac: boolean + Linux: boolean + } + category: string + rating: number + isComingSoom: boolean + isGame: boolean + slug: string + isNew: boolean + dlcCount: number + releaseDate: { + date: string + timezone_type: number + timezone: string + } + isBaseProductMissing: boolean + isHidingDisabled: boolean + isInDevelopment: boolean + // eslint-disable-next-line @typescript-eslint/no-explicit-any + extraInfo: any[] + isHidden: boolean +} + +export interface GOGImportData { + // "appName": "1441974651", "buildId": "55136646198962890", "title": "Prison Architect", "tasks": [{"category": "launcher", "isPrimary": true, "languages": ["en-US"], "name": "Prison Architect", "osBitness": ["64"], "path": "Launcher/dowser.exe", "type": "FileTask"}, {"category": "game", "isHidden": true, "languages": ["en-US"], "name": "Prison Architect - launcher process Prison Architect64_exe", "osBitness": ["64"], "path": "Prison Architect64.exe", "type": "FileTask"}, {"category": "document", "languages": ["en-US"], "link": "http://www.gog.com/support/prison_architect", "name": "Support", "type": "URLTask"}, {"category": "other", "languages": ["en-US"], "link": "http://www.gog.com/forum/prison_architect/prison_break_escape_map_megathread/post1", "name": "Escape Map Megathread", "type": "URLTask"}], "installedLanguage": "en-US"} + appName: string + buildId: string + title: string + tasks: Array<{ + category: string + isPrimary?: boolean + languages?: Array + arguments?: Array | string + path: string + name: string + type: string + }> + installedLanguage: string + platform: string + versionName: string + installedWithDlcs: boolean } export interface GamepadInputEventKey { diff --git a/electron/utils.ts b/electron/utils.ts index 48dc0d260a..50a88fc0a7 100644 --- a/electron/utils.ts +++ b/electron/utils.ts @@ -279,6 +279,13 @@ function clearCache() { cwd: 'lib-cache', name: 'gameinfo' }) + const GOGapiInfoCache = new Store({ + cwd: 'gog_store', + name: 'api_info_cache' + }) + const GOGlibraryStore = new Store({ cwd: 'gog_store', name: 'library' }) + GOGapiInfoCache.clear() + GOGlibraryStore.clear() installCache.clear() libraryCache.clear() gameInfoCache.clear() diff --git a/package.json b/package.json index bc48c1f723..15d125416b 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "win": { "icon": "build/win_icon.ico", "asarUnpack": [ - "build/bin/win32/legendary.exe" + "build/bin/win32/legendary.exe", + "build/bin/win32/gogdl.exe" ], "files": [ "build/bin/win32/*" @@ -55,7 +56,8 @@ "category": "public.app-category.games", "icon": "build/icon.icns", "asarUnpack": [ - "build/bin/darwin/legendary" + "build/bin/darwin/legendary", + "build/bin/darwin/gogdl" ], "files": [ "build/bin/darwin/*" @@ -89,7 +91,8 @@ "Name": "Heroic Games Launcher" }, "asarUnpack": [ - "build/bin/linux/legendary" + "build/bin/linux/legendary", + "build/bin/linux/gogdl" ], "files": [ "build/bin/linux/*" @@ -140,6 +143,7 @@ "i18next": "^21.6.6", "i18next-fs-backend": "^1.1.4", "i18next-http-backend": "^1.3.1", + "ini": "^2.0.0", "plist": "^3.0.4", "pretty-bytes": "^5.6.0", "react": "^17.0.2", @@ -192,6 +196,7 @@ "@testing-library/user-event": "^13.1.9", "@types/classnames": "^2.2.11", "@types/i18next-fs-backend": "^1.0.0", + "@types/ini": "^1.3.31", "@types/jest": "^26.0.23", "@types/node": "^17.0.10", "@types/plist": "^3.0.2", diff --git a/public/bin/darwin/gogdl b/public/bin/darwin/gogdl new file mode 100755 index 0000000000..2ff5caa5c7 Binary files /dev/null and b/public/bin/darwin/gogdl differ diff --git a/public/bin/linux/gogdl b/public/bin/linux/gogdl new file mode 100755 index 0000000000..058faf4c29 Binary files /dev/null and b/public/bin/linux/gogdl differ diff --git a/public/bin/win32/gogdl.exe b/public/bin/win32/gogdl.exe new file mode 100644 index 0000000000..002d0551ce Binary files /dev/null and b/public/bin/win32/gogdl.exe differ diff --git a/public/locales/bg/gamepage.json b/public/locales/bg/gamepage.json index d5a057a42b..18df9ab136 100644 --- a/public/locales/bg/gamepage.json +++ b/public/locales/bg/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Размер за сваляне", "firstPlayed": "Пусната за пръв път", "installSize": "Размер на инсталацията", + "language": "Language", "lastPlayed": "Последно пускане", "neverPlayed": "Никога", "totalPlayed": "Изиграно време" diff --git a/public/locales/bg/login.json b/public/locales/bg/login.json index 21544f4380..4e7d69e13f 100644 --- a/public/locales/bg/login.json +++ b/public/locales/bg/login.json @@ -2,9 +2,6 @@ "button": { "login": "Вход" }, - "input": { - "placeholder": "Въведете своя SID тук" - }, "message": { "part1": "За да можете да влезете и инсталирате игрите си, трябва първо да изпълните следните стъпки:", "part2": "", @@ -17,8 +14,7 @@ }, "status": { "error": "Грешка", - "loading": "Зареждане на списъка с игри. Моля, изчакайте", - "logging": "Влизане…" + "loading": "Зареждане на списъка с игри. Моля, изчакайте" }, "welcome": "Добре дошли!" } diff --git a/public/locales/bg/translation.json b/public/locales/bg/translation.json index c3e647308a..9cca4c83a0 100644 --- a/public/locales/bg/translation.json +++ b/public/locales/bg/translation.json @@ -88,6 +88,7 @@ "yes": "ДА" }, "button": { + "continue": "Continue", "login": "Вход", "sync": "Синхронизиране", "syncing": "Синхронизиране", @@ -104,6 +105,7 @@ }, "Filter": "Филтриране", "globalSettings": "Глобални настройки", + "GOG": "GOG", "help": { "general": "Синхронизирайте с EGS, в случай че вече имате работеща инсталация на Epic Games Store и искате да внесете игрите си от там, за да избегнете повторното им сваляне.", "other": { @@ -141,8 +143,7 @@ "website": "Зареждане на уеб сайта" }, "login": { - "loginWithEpic": "Вход чрез Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Синхронизацията е завършена", @@ -253,6 +254,7 @@ "showfps": "Показване на кадрите/сек (DX9, 10 и 11)", "showUnrealMarket": "Показване на Пазара на Unreal (изисква рестартиране)", "start-in-tray": "Стартиране в минимизирано състояние", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "Папка за префикс на Wine", "wineversion": "Версия на Wine" @@ -274,7 +276,6 @@ }, "Settings": "Настройки", "status": { - "loading": "Зареждане на списъка с игри. Моля, изчакайте", "logging": "Влизане…" }, "store": "Магазин", @@ -312,6 +313,7 @@ "discord": "Дискорд", "logout": "Излизане от акаунта", "logout_confirmation": "Наистина ли искате да излезете от акаунта си?", + "manageaccounts": "Manage Accounts", "quit": "Изход" }, "wiki": "Уики", diff --git a/public/locales/ca/gamepage.json b/public/locales/ca/gamepage.json index 5bf957d38c..bd0add8f5c 100644 --- a/public/locales/ca/gamepage.json +++ b/public/locales/ca/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Mida de la baixada", "firstPlayed": "Primera partida", "installSize": "Mida d'instal·lació", + "language": "Language", "lastPlayed": "Última partida", "neverPlayed": "Mai", "totalPlayed": "Temps jugat" diff --git a/public/locales/ca/login.json b/public/locales/ca/login.json index c82c167776..a14439507b 100644 --- a/public/locales/ca/login.json +++ b/public/locales/ca/login.json @@ -2,9 +2,6 @@ "button": { "login": "Inicia la sessió" }, - "input": { - "placeholder": "Enganxa el nombre SID aquí" - }, "message": { "part1": "Per a iniciar sessió i instal·lar els teus jocs, cal que seguiu primer les passes de sota:", "part2": "Obre el", @@ -17,8 +14,7 @@ }, "status": { "error": "Error", - "loading": "S'està carregant la llista de jocs, espera", - "logging": "S'està iniciant la sessió…" + "loading": "S'està carregant la llista de jocs, espera" }, "welcome": "Et donem la benvinguda!" } diff --git a/public/locales/ca/translation.json b/public/locales/ca/translation.json index 3d3ebb92e2..a690fa551c 100644 --- a/public/locales/ca/translation.json +++ b/public/locales/ca/translation.json @@ -88,6 +88,7 @@ "yes": "SÍ" }, "button": { + "continue": "Continue", "login": "Inicia la sessió", "sync": "Sincronitza", "syncing": "S'està sincronitzant", @@ -104,6 +105,7 @@ }, "Filter": "Filtra", "globalSettings": "Configuració global", + "GOG": "GOG", "help": { "general": "Sincronitza amb l'EGL si ja tens instal·lat l'Epic Games Laucher en qualsevol altre lloc i vols importar els jocs sense haver de baixar-los de nou.", "other": { @@ -141,8 +143,7 @@ "website": "S'està carregant el lloc web" }, "login": { - "loginWithEpic": "Inicia la sessió amb Epic", - "loginWithSid": "Inicia la sessió amb SID" + "externalLogin": "External Login" }, "message": { "sync": "S'ha completat la sincronització", @@ -253,6 +254,7 @@ "showfps": "Mostra els FPS (DX9, 10 i 11)", "showUnrealMarket": "Mostra la llibreria d'Unreal (cal reiniciar)", "start-in-tray": "Inicia minimitzat", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "Ampolla CrossOver", "wineprefix": "Directori WinePrefix", "wineversion": "Versió del Wine" @@ -274,7 +276,6 @@ }, "Settings": "Configuració", "status": { - "loading": "S'està carregant la llista de jocs, espera", "logging": "S'està iniciant la sessió..." }, "store": "Botiga", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Tanca la sessió", "logout_confirmation": "Segur que vols tancar la sessió?", + "manageaccounts": "Manage Accounts", "quit": "Surt" }, "wiki": "Wiki", diff --git a/public/locales/cs/gamepage.json b/public/locales/cs/gamepage.json index ba5e7020e4..4279046818 100644 --- a/public/locales/cs/gamepage.json +++ b/public/locales/cs/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Download Size", "firstPlayed": "Poprvé hráno", "installSize": "Install Size", + "language": "Language", "lastPlayed": "Naposledy hráno", "neverPlayed": "Nikdy", "totalPlayed": "Odehraný čas" diff --git a/public/locales/cs/login.json b/public/locales/cs/login.json index c9dadef7e0..8b0bc4fd2c 100644 --- a/public/locales/cs/login.json +++ b/public/locales/cs/login.json @@ -2,9 +2,6 @@ "button": { "login": "Přihlásit" }, - "input": { - "placeholder": "Sem vložte číslo SID" - }, "message": { "part1": "Aby jste se mohli přihlásit a instalovat vaše hry, musíte postupovat podle kroků níže:", "part2": "Otevřte", @@ -17,8 +14,7 @@ }, "status": { "error": "Chyba", - "loading": "Načítám seznam her, čekejte prosím", - "logging": "Přihlašuji…" + "loading": "Načítám seznam her, čekejte prosím" }, "welcome": "Vítejte!" } diff --git a/public/locales/cs/translation.json b/public/locales/cs/translation.json index 3e7f33a87c..36121e0033 100644 --- a/public/locales/cs/translation.json +++ b/public/locales/cs/translation.json @@ -88,6 +88,7 @@ "yes": "ANO" }, "button": { + "continue": "Continue", "login": "Login", "sync": "Synchronizovat", "syncing": "Synchronizuji", @@ -104,6 +105,7 @@ }, "Filter": "Filtr", "globalSettings": "Globální nastavení", + "GOG": "GOG", "help": { "general": "Synchronizujte s EGS v případě, že máte funkční instalaci Epic Games Store jinde a chcete své hry importovat, abyste se vyhnuli jejich opětovnému stahování.", "other": { @@ -141,8 +143,7 @@ "website": "Loading Website" }, "login": { - "loginWithEpic": "Login With Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Synchronizace dokončena", @@ -253,6 +254,7 @@ "showfps": "Zobrazit FPS (DX9, 10 a 11)", "showUnrealMarket": "Show Unreal Marketplace (needs restart)", "start-in-tray": "Spustit minimalizované", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "Složka WinePrefix", "wineversion": "Verze Wine" @@ -274,7 +276,6 @@ }, "Settings": "Nastavení", "status": { - "loading": "Loading Game list, please wait", "logging": "Logging In..." }, "store": "Obchod", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Odhlásit", "logout_confirmation": "Logout?", + "manageaccounts": "Manage Accounts", "quit": "Ukončit" }, "wiki": "Wiki", diff --git a/public/locales/de/gamepage.json b/public/locales/de/gamepage.json index 1251f75cbc..c3bca1d8c9 100644 --- a/public/locales/de/gamepage.json +++ b/public/locales/de/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Downloadgröße", "firstPlayed": "Erstmals gespielt", "installSize": "Installierte Größe", + "language": "Language", "lastPlayed": "Zuletzt gespielt", "neverPlayed": "Nie", "totalPlayed": "Spielzeit" diff --git a/public/locales/de/login.json b/public/locales/de/login.json index 70fd063ff7..1e06be461f 100644 --- a/public/locales/de/login.json +++ b/public/locales/de/login.json @@ -2,9 +2,6 @@ "button": { "login": "Anmelden" }, - "input": { - "placeholder": "Fügen Sie hier ihre SID-Nummer ein" - }, "message": { "part1": "Um sich anzumelden und Ihre Spiele zu installieren, müssen Sie zunächst die folgenden Schritte ausführen:", "part2": "Öffnen Sie den", @@ -17,8 +14,7 @@ }, "status": { "error": "Fehler", - "loading": "Lade Spieleliste, bitte warten", - "logging": "Anmelden…" + "loading": "Lade Spieleliste, bitte warten" }, "welcome": "Willkommen!" } diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index a69fc6fbd6..f7140237dd 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -88,6 +88,7 @@ "yes": "JA" }, "button": { + "continue": "Continue", "login": "Anmelden", "sync": "Synchronisieren", "syncing": "Synchronisiere", @@ -104,6 +105,7 @@ }, "Filter": "Filter", "globalSettings": "Globale Einstellungen", + "GOG": "GOG", "help": { "general": "Synchronisiere mit EGL für den Fall, dass Sie anderswo eine funktioniere Installation des Epic Games Launcher haben und Ihre Spiele importieren möchten, um ein erneutes Herunterladen zu vermeiden.", "other": { @@ -141,8 +143,7 @@ "website": "Lade Webseite" }, "login": { - "loginWithEpic": "Anmelden mit Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Synchronisation abgeschlossen", @@ -253,6 +254,7 @@ "showfps": "FPS anzeigen (DX9, 10 und 11)", "showUnrealMarket": "Unreal Marketplace anzeigen (benötigt Neustart)", "start-in-tray": "Minimiert starten", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Flasche", "wineprefix": "Wine Prefix Ordner", "wineversion": "Wine-Version" @@ -274,7 +276,6 @@ }, "Settings": "Einstellungen", "status": { - "loading": "Lade Spieleliste, bitte warten", "logging": "Anmelden…" }, "store": "Store", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Abmelden", "logout_confirmation": "Möchten Sie sich wirklich abmelden?", + "manageaccounts": "Manage Accounts", "quit": "Beenden" }, "wiki": "Wiki", diff --git a/public/locales/el/gamepage.json b/public/locales/el/gamepage.json index f216b25db1..7d67c4b13c 100644 --- a/public/locales/el/gamepage.json +++ b/public/locales/el/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Μέγεθος λήψης", "firstPlayed": "Παίχτηκε πρώτη φορά", "installSize": "Μέγεθος Εγκατάστασης", + "language": "Language", "lastPlayed": "Παίχτηκε Τελευταία φορά", "neverPlayed": "Ποτέ", "totalPlayed": "Συνολικός Χρόνος Παιχνιδιού" diff --git a/public/locales/el/login.json b/public/locales/el/login.json index b0347ad33b..8d8a152de7 100644 --- a/public/locales/el/login.json +++ b/public/locales/el/login.json @@ -2,9 +2,6 @@ "button": { "login": "Σύνδεση" }, - "input": { - "placeholder": "Επικολλήστε τον αριθμό SID εδώ" - }, "message": { "part1": "Για να μπορέσετε να συνδεθείτε και να εγκαταστήσετε τα παιχνίδια σας, πρέπει πρώτα να ακολουθήσετε τα παρακάτω βήματα:", "part2": "Ανοίξτε το", @@ -17,8 +14,7 @@ }, "status": { "error": "Σφάλμα", - "loading": "Φόρτωση λίστας παιχνιδιών, παρακαλώ περιμένετε", - "logging": "Σύνδεση…" + "loading": "Φόρτωση λίστας παιχνιδιών, παρακαλώ περιμένετε" }, "welcome": "Καλώς ορίσατε!" } diff --git a/public/locales/el/translation.json b/public/locales/el/translation.json index 04dd406d26..cf0c44e1a9 100644 --- a/public/locales/el/translation.json +++ b/public/locales/el/translation.json @@ -88,6 +88,7 @@ "yes": "ΝΑΙ" }, "button": { + "continue": "Continue", "login": "Login", "sync": "Συγχρονισμός", "syncing": "Συγχρονισμός", @@ -104,6 +105,7 @@ }, "Filter": "Φίλτρο", "globalSettings": "Παγκόσμιες Ρυθμίσεις", + "GOG": "GOG", "help": { "general": "Συγχρονισμός με EGS σε περίπτωση εγκατάστασης του Καταστήματος Epic Games κάπου αλλού και θέλετε να εισαγάγετε τα παιχνίδια σας για αποφυγή λήψης ξανά.", "other": { @@ -141,8 +143,7 @@ "website": "Loading Website" }, "login": { - "loginWithEpic": "Login With Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Ο συγχρονισμός ολοκληρώθηκε", @@ -253,6 +254,7 @@ "showfps": "Ένδειξη ταχύτητας καρέ (DX9, 10 και 11)", "showUnrealMarket": "Show Unreal Marketplace (needs restart)", "start-in-tray": "Εκκίνηση ελαχιστοποιημένα", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "φάκελος προθεμάτων Wine", "wineversion": "Έκδοση Wine" @@ -274,7 +276,6 @@ }, "Settings": "Ρυθμίσεις", "status": { - "loading": "Loading Game list, please wait", "logging": "Logging In..." }, "store": "Κατάστημα", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Αποσύνδεση", "logout_confirmation": "Θέλετε πράγματι να αποσυνδεθείτε;", + "manageaccounts": "Manage Accounts", "quit": "Έξοδος" }, "wiki": "Wiki", diff --git a/public/locales/en/gamepage.json b/public/locales/en/gamepage.json index 68dba4bf70..435dd735ce 100644 --- a/public/locales/en/gamepage.json +++ b/public/locales/en/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Download Size", "firstPlayed": "First Played", "installSize": "Install Size", + "language": "Language", "lastPlayed": "Last Played", "neverPlayed": "Never", "totalPlayed": "Time Played" diff --git a/public/locales/en/login.json b/public/locales/en/login.json index 86c6a4a71d..eb29d9a9e8 100644 --- a/public/locales/en/login.json +++ b/public/locales/en/login.json @@ -2,9 +2,6 @@ "button": { "login": "Log in" }, - "input": { - "placeholder": "Paste your SID number here" - }, "message": { "part1": "In order to log in and install your games, you first need to follow the steps below:", "part2": "Open the", @@ -17,8 +14,7 @@ }, "status": { "error": "Error", - "loading": "Loading Game list, please wait", - "logging": "Logging In…" + "loading": "Loading Game list, please wait" }, "welcome": "Welcome!" } diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index dbdcbb66c6..fc6def2a22 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -88,6 +88,7 @@ "yes": "YES" }, "button": { + "continue": "Continue", "login": "Log in", "sync": "Sync", "syncing": "Syncing", @@ -104,6 +105,7 @@ }, "Filter": "Filter", "globalSettings": "Global Settings", + "GOG": "GOG", "help": { "general": "Sync with EGL in case you have a working installation of the Epic Games Launcher elsewhere and want to import your games to avoid downloading them again.", "other": { @@ -141,8 +143,7 @@ "website": "Loading Website" }, "login": { - "loginWithEpic": "Login With Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Sync Complete", @@ -253,6 +254,7 @@ "showfps": "Show FPS (DX9, 10 and 11)", "showUnrealMarket": "Show Unreal Marketplace (needs restart)", "start-in-tray": "Start Minimized", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "WinePrefix folder", "wineversion": "Wine Version" @@ -274,7 +276,6 @@ }, "Settings": "Settings", "status": { - "loading": "Loading Game list, please wait", "logging": "Logging In..." }, "store": "Store", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Log out", "logout_confirmation": "Are you sure you want to log out?", + "manageaccounts": "Manage Accounts", "quit": "Quit" }, "wiki": "Wiki", diff --git a/public/locales/es/gamepage.json b/public/locales/es/gamepage.json index d35a6b6844..10c4bf87ea 100644 --- a/public/locales/es/gamepage.json +++ b/public/locales/es/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Tamaño de Descarga", "firstPlayed": "Jugado por Primera Vez", "installSize": "Tamaño de Instalación", + "language": "Language", "lastPlayed": "Jugado por Última Vez", "neverPlayed": "Nunca", "totalPlayed": "Tiempo Jugado" diff --git a/public/locales/es/login.json b/public/locales/es/login.json index 1f51c04895..fa4d817cd2 100644 --- a/public/locales/es/login.json +++ b/public/locales/es/login.json @@ -2,9 +2,6 @@ "button": { "login": "Iniciar sesión" }, - "input": { - "placeholder": "Pega tu número SID aquí" - }, "message": { "part1": "Para iniciar la sesión e instalar los juegos, primero hay que seguir los siguientes pasos:", "part2": "Abrir el", @@ -17,8 +14,7 @@ }, "status": { "error": "Error", - "loading": "Cargando la lista de juegos, por favor espere", - "logging": "Iniciando sesión…" + "loading": "Cargando la lista de juegos, por favor espere" }, "welcome": "¡Bienvenido!" } diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index b7e6643e41..a2123da089 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -88,6 +88,7 @@ "yes": "SÍ" }, "button": { + "continue": "Continue", "login": "Iniciar sesión", "sync": "Sincronizar", "syncing": "Sincronizando", @@ -104,6 +105,7 @@ }, "Filter": "Filtrar", "globalSettings": "Ajustes globales", + "GOG": "GOG", "help": { "general": "Sincronice con EGL en caso de que tenga una instalación funcional de Epic Games Launcher en otro lugar y desee importar sus juegos para evitar descargarlos nuevamente.", "other": { @@ -141,8 +143,7 @@ "website": "Cargando el sitio web" }, "login": { - "loginWithEpic": "Iniciar sesión con Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Sincronización completa", @@ -253,6 +254,7 @@ "showfps": "Mostrar FPS (DX9, 10 y 11)", "showUnrealMarket": "Mostrar la tienda de Unreal (requiere reiniciar)", "start-in-tray": "Iniciar minimizado", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "Carpeta WinePrefix", "wineversion": "Versión de Wine" @@ -274,7 +276,6 @@ }, "Settings": "Ajustes", "status": { - "loading": "Cargando la lista de juegos, por favor espere", "logging": "Iniciando sesión…" }, "store": "Tienda", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Cerrar sesión", "logout_confirmation": "¿Estás seguro de que quieres cerrar la sesión?", + "manageaccounts": "Manage Accounts", "quit": "Salir" }, "wiki": "Wiki", diff --git a/public/locales/et/gamepage.json b/public/locales/et/gamepage.json index ac4df9d75f..d0905bc2b4 100644 --- a/public/locales/et/gamepage.json +++ b/public/locales/et/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Allalaadimise suurus", "firstPlayed": "Esimest korda mängitud", "installSize": "Paigaldamise suurus", + "language": "Language", "lastPlayed": "Viimati mängitud", "neverPlayed": "Mitte kunagi", "totalPlayed": "Mängitud aeg" diff --git a/public/locales/et/login.json b/public/locales/et/login.json index 59c89e33f2..9a1a091727 100644 --- a/public/locales/et/login.json +++ b/public/locales/et/login.json @@ -2,9 +2,6 @@ "button": { "login": "Logi sisse" }, - "input": { - "placeholder": "Sisestage oma SID-number siia" - }, "message": { "part1": "Sisse logimiseks ja mängude paigaldamiseks peate kõigepealt järgima alljärgnevaid samme:", "part2": "Ava", @@ -17,8 +14,7 @@ }, "status": { "error": "Viga", - "loading": "Mängude loendi laadimine, palun oodake", - "logging": "Sisselogimine…" + "loading": "Mängude loendi laadimine, palun oodake" }, "welcome": "Tere tulemast!" } diff --git a/public/locales/et/translation.json b/public/locales/et/translation.json index d3073129c2..7390203f60 100644 --- a/public/locales/et/translation.json +++ b/public/locales/et/translation.json @@ -88,6 +88,7 @@ "yes": "JAH" }, "button": { + "continue": "Continue", "login": "Logi sisse", "sync": "Sünkrooni", "syncing": "Sünkroonimine", @@ -104,6 +105,7 @@ }, "Filter": "Filtreeri", "globalSettings": "Globaalsed seaded", + "GOG": "GOG", "help": { "general": "Sünkroonige EGS-iga, kui teil on Epic Games Store'i töötav installatsioon mujal ja soovite oma mänge importida, et vältida nende uuesti allalaadimist.", "other": { @@ -141,8 +143,7 @@ "website": "Veebilehe laadimine" }, "login": { - "loginWithEpic": "Logi sisse Epic'uga", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Sünkroonimine lõpetatud", @@ -253,6 +254,7 @@ "showfps": "Näita FPS-i (DX9, 10 ja 11)", "showUnrealMarket": "Näita Unreal Marketplace'i (vajab taaskäivitamist)", "start-in-tray": "Käivita minimeeritud", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOveri pudel", "wineprefix": "WinePrefiksi kaust", "wineversion": "Wine'i versioon" @@ -274,7 +276,6 @@ }, "Settings": "Seaded", "status": { - "loading": "Mängude loendi laadimine, palun oodake", "logging": "Sisselogimine..." }, "store": "Pood", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Logi välja", "logout_confirmation": "Kas olete kindel, et soovite välja logida?", + "manageaccounts": "Manage Accounts", "quit": "Sulge" }, "wiki": "Viki", diff --git a/public/locales/fa/gamepage.json b/public/locales/fa/gamepage.json index 62569e051e..11484cc7fb 100644 --- a/public/locales/fa/gamepage.json +++ b/public/locales/fa/gamepage.json @@ -23,13 +23,13 @@ "title": "توقف نصب" }, "uninstall": { - "checkbox": "آیا می\u200cخواهید مسیر را هم پاک کنید؟ این کار بازگشت پذیر نیست.", + "checkbox": "آیا می‌خواهید مسیر را هم پاک کنید؟ این کار بازگشت پذیر نیست.", "checkbox_prefix": "Prefix", "message": "آیا میخواهید این بازی را حذف کنید؟", "title": "حذف" }, "update": { - "message": "این بازی به‌روزرسانی دارد، آیا مایل به به‌روزرسانی هستید؟", + "message": "این بازی بهروزرسانی دارد، آیا مایل به بهروزرسانی هستید؟", "title": "بازی نیاز به بهروزرسانی دارد" }, "wineprefix": "انتخاب پوشه Wine Prefix", @@ -53,6 +53,7 @@ "downloadSize": "سایز دانلود", "firstPlayed": "اولین اجرا", "installSize": "سایز نصب", + "language": "Language", "lastPlayed": "آخرین اجرا", "neverPlayed": "هرگز", "totalPlayed": "زمان بازی" @@ -112,4 +113,4 @@ "store": "صفحه فروشگاه", "verify": "بررسی و تعمیر" } -} +} \ No newline at end of file diff --git a/public/locales/fa/login.json b/public/locales/fa/login.json index 421cfa17c4..cc38b21e03 100644 --- a/public/locales/fa/login.json +++ b/public/locales/fa/login.json @@ -2,9 +2,6 @@ "button": { "login": "ورود" }, - "input": { - "placeholder": "شماره SID خود را اینجا جایگذاری کنید" - }, "message": { "part1": "برای اینکه بتوانید وارد شوید و بازیهای خود را نصب کنید، باید ابتدا باید مراحل زیر را دنبال کنید:", "part2": "باز کردن", @@ -17,8 +14,7 @@ }, "status": { "error": "خطا", - "loading": "در حال بارگیری لیست بازی، لطفا منتظر بمانید", - "logging": "در حال وارد شدن…" + "loading": "در حال بارگیری لیست بازی، لطفا منتظر بمانید" }, "welcome": "خوش آمدید!" } diff --git a/public/locales/fa/translation.json b/public/locales/fa/translation.json index 6d36dca3b5..4293c0ec91 100644 --- a/public/locales/fa/translation.json +++ b/public/locales/fa/translation.json @@ -3,7 +3,7 @@ "Assets": "داراییها", "box": { "appupdate": { - "message": "نسخه جدید Heroic موجود است، آیا میخواهید الان به‌روزرسانی کنید؟", + "message": "نسخه جدید Heroic موجود است، آیا میخواهید الان به\u200cروزرسانی کنید؟", "title": "بهروزرسانی موجود است" }, "cache-cleared": { @@ -88,6 +88,7 @@ "yes": "بله" }, "button": { + "continue": "Continue", "login": "ورود", "sync": "همگامسازی", "syncing": "در حال همگامسازی", @@ -104,8 +105,9 @@ }, "Filter": "فیلتر", "globalSettings": "تنظیمات عمومی", + "GOG": "GOG", "help": { - "general": "در صورتی که در جایی لانچر اپیک گیمز را به صورت فعال نصب کرده‌اید و میخواهید بازی‌های خود را وارد کنید با EGL همگام‌سازی کنید تا از دانلود مجدد آنها جلوگیری کنید.", + "general": "در صورتی که در جایی لانچر اپیک گیمز را به صورت فعال نصب کرده\u200cاید و میخواهید بازی\u200cهای خود را وارد کنید با EGL همگام\u200cسازی کنید تا از دانلود مجدد آنها جلوگیری کنید.", "other": { "part1": "استفاده از ", "part2": "گزینههای پیشرفته", @@ -141,8 +143,7 @@ "website": "درحال بارگیری وبسایت" }, "login": { - "loginWithEpic": "ورود با اپیک", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "همگامسازی کامل شد", @@ -204,7 +205,7 @@ "audiofix": "تنظیم صدا (تاخیر Pulse Audio)", "autodxvk": "نصب/بهروزرسانی خودکار DXVK در Prefix", "autosync": "همگامسازی خودکار ذخیرهها", - "autovkd3d": "نصب/به‌روزرسانی خودکار VKD3D در مسیر", + "autovkd3d": "نصب/به\u200cروزرسانی خودکار VKD3D در مسیر", "change-target-exe": "انتخاب EXE جایگزین برای اجرا", "checkForUpdatesOnStartup": "چک کردن برای به روزرسانی در هنگام راه اندازی", "customWineProton": "مسیرهای سفارشی Wine/Proton", @@ -253,6 +254,7 @@ "showfps": "نمایش تعداد فریم (11 و DX9, 10)", "showUnrealMarket": "نمایش فروشگاه Unreal (نیازمند راهاندازی مجدد)", "start-in-tray": "شروع در حالت پسزمینه", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "پوشه محل Wine", "wineversion": "نسخه Wine" @@ -262,7 +264,7 @@ "copiedToClipboard": "در کليپ بورد کپی شد!", "copyToClipboard": "کپی کردن همه تنظیمات در کليپ بورد", "log": { - "long-log-hint": "لاگ کوتاه شد، ١٠٠٠ خط آخر نمایش داده شده‌اند!" + "long-log-hint": "لاگ کوتاه شد، ١٠٠٠ خط آخر نمایش داده شده\u200cاند!" }, "navbar": { "general": "عمومی", @@ -274,7 +276,6 @@ }, "Settings": "تنظیمات", "status": { - "loading": "در حال بارگیری لیست بازی، لطفا منتظر بمانید", "logging": "در حال وارد شدن..." }, "store": "فروشگاه", @@ -291,7 +292,7 @@ }, "toolbox": { "settings": { - "default-wineprefix": "انتخاب پوشه مسیر پیش‌فرض برای تنظیمات جدید", + "default-wineprefix": "انتخاب پوشه مسیر پیش\u200cفرض برای تنظیمات جدید", "wineprefix": "انتخاب پوشه برای مسیر Wine جدید" } }, @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "خروج از حساب", "logout_confirmation": "آیا میخواهید از حساب خود خارج شوید؟", + "manageaccounts": "Manage Accounts", "quit": "خروج" }, "wiki": "ویکی", diff --git a/public/locales/fi/gamepage.json b/public/locales/fi/gamepage.json index ff24123427..574fcfa7f4 100644 --- a/public/locales/fi/gamepage.json +++ b/public/locales/fi/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Download Size", "firstPlayed": "Ensimmäisen kerran pelattu", "installSize": "Install Size", + "language": "Language", "lastPlayed": "Viimeksi pelattu", "neverPlayed": "Ei koskaan", "totalPlayed": "Pelattu aika" diff --git a/public/locales/fi/login.json b/public/locales/fi/login.json index eee0d08795..f163817e33 100644 --- a/public/locales/fi/login.json +++ b/public/locales/fi/login.json @@ -2,9 +2,6 @@ "button": { "login": "Kirjaudu sisään" }, - "input": { - "placeholder": "Liitä SID numero tähän" - }, "message": { "part1": "Ennen kuin voit kirjautua sisään ja asentaa pelejäsi, seuraa alla olevia vaiheita:", "part2": "Avaa", @@ -17,8 +14,7 @@ }, "status": { "error": "Virhe", - "loading": "Ladataan pelilistaa, odota hetki", - "logging": "Kirjaudutaan sisään…" + "loading": "Ladataan pelilistaa, odota hetki" }, "welcome": "Tervetuloa!" } diff --git a/public/locales/fi/translation.json b/public/locales/fi/translation.json index 4b461eabf4..f6e2dccd84 100644 --- a/public/locales/fi/translation.json +++ b/public/locales/fi/translation.json @@ -88,6 +88,7 @@ "yes": "KYLLÄ" }, "button": { + "continue": "Continue", "login": "Login", "sync": "Synkronoi", "syncing": "Synkronoidaan", @@ -104,6 +105,7 @@ }, "Filter": "Suodata", "globalSettings": "Globaalit asetukset", + "GOG": "GOG", "help": { "general": "Synkronoi EGS:n kanssa, jos sinulla on toimiva Epic Games Store asennus ja haluat tuoda pelisi välttääksesi niiden uudelleenlatauksen.", "other": { @@ -141,8 +143,7 @@ "website": "Loading Website" }, "login": { - "loginWithEpic": "Login With Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Synkronointi valmis", @@ -253,6 +254,7 @@ "showfps": "Näytä kuvataajuus (DX9, 10 ja 11)", "showUnrealMarket": "Show Unreal Marketplace (needs restart)", "start-in-tray": "Käynnistä minimoituna ilmoitusalueelle", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "WinePrefixin kansio", "wineversion": "Winen versio" @@ -274,7 +276,6 @@ }, "Settings": "Asetukset", "status": { - "loading": "Loading Game list, please wait", "logging": "Logging In..." }, "store": "Kauppa", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Kirjaudu ulos", "logout_confirmation": "Haluatko varmasti kirjautua ulos?", + "manageaccounts": "Manage Accounts", "quit": "Poistu" }, "wiki": "Wiki", diff --git a/public/locales/fr/gamepage.json b/public/locales/fr/gamepage.json index 34cb582dea..af702667b6 100644 --- a/public/locales/fr/gamepage.json +++ b/public/locales/fr/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Taille du téléchargement", "firstPlayed": "Joué pour la première fois", "installSize": "Taille de l'installation", + "language": "Language", "lastPlayed": "Joué la dernière fois", "neverPlayed": "Jamais", "totalPlayed": "Temps passé à jouer" diff --git a/public/locales/fr/login.json b/public/locales/fr/login.json index 73384aa1e8..be48445321 100644 --- a/public/locales/fr/login.json +++ b/public/locales/fr/login.json @@ -2,9 +2,6 @@ "button": { "login": "Connexion" }, - "input": { - "placeholder": "Collez le code SID ici" - }, "message": { "part1": "Afin de pouvoir vous connecter et installer vos jeux, vous devez d’abord suivre les étapes ci-dessous:", "part2": "Ouvrez", @@ -17,8 +14,7 @@ }, "status": { "error": "Erreur", - "loading": "Chargement de la liste des jeux, veuillez patienter", - "logging": "Connexion en cours…" + "loading": "Chargement de la liste des jeux, veuillez patienter" }, "welcome": "Bienvenue !" } diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index f92fe4d7f1..db887cd0d9 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -88,6 +88,7 @@ "yes": "OUI" }, "button": { + "continue": "Continue", "login": "Connexion", "sync": "Synchroniser", "syncing": "Synchronisation en cours", @@ -104,6 +105,7 @@ }, "Filter": "Filtre", "globalSettings": "Paramètres généraux", + "GOG": "GOG", "help": { "general": "Synchronisation avec EGS si vous avez un dossier d'installation de l'Epic Games Store ailleurs et que vous voulez importer vos jeux, afin d'éviter de les retélécharger.", "other": { @@ -141,8 +143,7 @@ "website": "Chargement du site internet" }, "login": { - "loginWithEpic": "Login With Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Synchronisation Complète", @@ -253,6 +254,7 @@ "showfps": "Afficher les FPS (DX9, 10 et 11)", "showUnrealMarket": "Afficher Unreal Marketplace (nécessite le redémarrage d'Heroic)", "start-in-tray": "Réduire au démarrage", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "Dossier WinePrefix", "wineversion": "Version de Wine" @@ -274,7 +276,6 @@ }, "Settings": "Options", "status": { - "loading": "Chargement de la liste des jeux, veuillez patienter", "logging": "Connexion en cours..." }, "store": "Boutique", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Déconnexion", "logout_confirmation": "Êtes-vous certain(e) de vouloir vous déconnecter?", + "manageaccounts": "Manage Accounts", "quit": "Quitter" }, "wiki": "Wiki", diff --git a/public/locales/gl/gamepage.json b/public/locales/gl/gamepage.json index 23712ae49a..e3d7c01a21 100644 --- a/public/locales/gl/gamepage.json +++ b/public/locales/gl/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Tamaño da descarga", "firstPlayed": "Xogado por primeira vez", "installSize": "Tamaño da instalación", + "language": "Language", "lastPlayed": "Xogado por última vez", "neverPlayed": "Nunca", "totalPlayed": "Tempo xogado" diff --git a/public/locales/gl/login.json b/public/locales/gl/login.json index 823aab9378..b11bb9d9a5 100644 --- a/public/locales/gl/login.json +++ b/public/locales/gl/login.json @@ -2,9 +2,6 @@ "button": { "login": "Iniciar sesión" }, - "input": { - "placeholder": "Pega o número SID aquí" - }, "message": { "part1": "Para que poidas iniciar sesión e instalar os teus xogos, primeiro debes seguir os pasos a continuación:", "part2": "", @@ -17,8 +14,7 @@ }, "status": { "error": "Erro", - "loading": "Cargando lista de xogos, agarde", - "logging": "Iniciando sesión…" + "loading": "Cargando lista de xogos, agarde" }, "welcome": "Bendida/o!" } diff --git a/public/locales/gl/translation.json b/public/locales/gl/translation.json index fe6a403a48..80a349afca 100644 --- a/public/locales/gl/translation.json +++ b/public/locales/gl/translation.json @@ -88,6 +88,7 @@ "yes": "Sí" }, "button": { + "continue": "Continue", "login": "Iniciar sesión", "sync": "Sincronizar", "syncing": "Sincronizando", @@ -104,6 +105,7 @@ }, "Filter": "Filtrar", "globalSettings": "Preferencias globais", + "GOG": "GOG", "help": { "general": "Sincronizar con EGS no caso que teña unha instalación funcional de EGS noutro lugar e desexa importar os seus xogos para evitar descargalos novamente.", "other": { @@ -141,8 +143,7 @@ "website": "Cargando sitio web" }, "login": { - "loginWithEpic": "Login With Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Sincronización completa", @@ -253,6 +254,7 @@ "showfps": "Mostrar FPS (DX9, 10 e 11)", "showUnrealMarket": "Mostrar a tenda de Unreal (require reinicio)", "start-in-tray": "Iniciar minimizado", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "", "wineversion": "" @@ -274,7 +276,6 @@ }, "Settings": "Preferencias", "status": { - "loading": "Cargando lista de xogos, agarde", "logging": "Inciando sesión…" }, "store": "Tenda", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Pechar sesión", "logout_confirmation": "Está seguro que quere pechar a sesión?", + "manageaccounts": "Manage Accounts", "quit": "Saír" }, "wiki": "Wiki", diff --git a/public/locales/hr/gamepage.json b/public/locales/hr/gamepage.json index d9ec8e6a57..bf46c8cce8 100644 --- a/public/locales/hr/gamepage.json +++ b/public/locales/hr/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Veličina Preuzimanja", "firstPlayed": "Prvo odigrano", "installSize": "Veličina Instalacije", + "language": "Language", "lastPlayed": "Zadnje odigrano", "neverPlayed": "Nikad", "totalPlayed": "Vrijeme igranja" diff --git a/public/locales/hr/login.json b/public/locales/hr/login.json index 6f5f426126..5a86ecedfc 100644 --- a/public/locales/hr/login.json +++ b/public/locales/hr/login.json @@ -2,9 +2,6 @@ "button": { "login": "Prijava" }, - "input": { - "placeholder": "Zalijepite vaš SID broj ovdje" - }, "message": { "part1": "Ukoliko se želite prijaviti i instalirati svoje igre, morate slijediti sljedeće korake:", "part2": "Otvori", @@ -17,8 +14,7 @@ }, "status": { "error": "Greška", - "loading": "Učitavanje liste Igrica, pričekajte", - "logging": "Prijavljivljanje…" + "loading": "Učitavanje liste Igrica, pričekajte" }, "welcome": "Dobrodošli!" } diff --git a/public/locales/hr/translation.json b/public/locales/hr/translation.json index 5bc22e8e33..2ba91f34ec 100644 --- a/public/locales/hr/translation.json +++ b/public/locales/hr/translation.json @@ -1,36 +1,36 @@ { - "All": "Sve", - "Assets": "Dodaci", + "All": "", + "Assets": "Assets", "box": { "appupdate": { - "message": "Izašla je nova verzija Heroic-a, želite li ažurirati?", - "title": "Ažuriranje dostupno" + "message": "There is a new version of Heroic Available, do you want to update now?", + "title": "Update Available" }, "cache-cleared": { - "message": "Heroic Cache je očisten.", - "title": "Cache Očisten" + "message": "Heroic Cache Was Cleared!", + "title": "Cache Cleared" }, - "choose": "Odaberi", - "choose-egs-prefix": "Odaberite lokaciju gdje je EGS instaliran", - "choose-legendary-binary": "Odaberite Legendary Aplikaciju", - "customWine": "Odaberite WINE ili PROTON aplikaciju", - "default-install-path": "Odaberite zadanu lokaciju instalacije", + "choose": "", + "choose-egs-prefix": "", + "choose-legendary-binary": "Select Legendary Binary (needs restart)", + "customWine": "Select the Wine or Proton Binary", + "default-install-path": "", "error": { "credentials": { - "message": "Vaša prijava je istekla, molimo vas odjavite se i prijavite opet.", - "title": "Istekla prijava" + "message": "Your Crendentials have expired, Logout and Login Again!", + "title": "Expired Credentials" }, "diskspace": { - "message": "Nema dovoljno prostora na disku", - "title": "Nema prostora" + "message": "Not enough available disk space", + "title": "No Space" }, "dxvk": { - "message": "Pogreška pri instaliranju DXVK/VKD3D! Provjerite svoju konekcije ili ako imate zstd/libzstd1 instaliran", - "title": "DXVK/VKD3D greška" + "message": "Error installing DXVK/VKD3D! Check your connection or if you have zstd/libzstd1 installed", + "title": "DXVK/VKD3D error" }, "generic": { - "message": "Nepoznata greška se dogodila", - "title": "Nepoznata greška" + "message": "An Unknown Error has occurred", + "title": "Unknown Error" }, "launch": "Greška kod pokretanja igrice, provjerite dnevnik!", "no-offline-mode": { @@ -39,245 +39,246 @@ }, "title": "Greška", "wine-not-found": { - "message": "Wine nije odabran. Provjerite postavke igre!", - "title": "Wine Nije Pronađen" + "message": "No Wine Version Selected. Check Game Settings!", + "title": "Wine Not Found" } }, - "no": "NE", + "no": "", "ok": "Ok", "protocol": { "install": { - "not_installed": "Nije instalirana, želite ju instalirati?" + "not_installed": "Is Not Installed, do you wish to Install it?" } }, "quit": { - "message": "Operacije su u tijeku, želite li nastaviti?", - "title": "Izlaz" + "message": "There are pending operations, are you sure?", + "title": "Exit" }, "reset-heroic": { "question": { - "message": "Jeste li sigurni da želite resetirati Heroic? Ovo če obrisati sve postavke i Cache ali ne igrice i vašu prijavu", + "message": "Are you sure you want to reset Heroic? This will remove all Settings and Caching but won't remove your Installed games or your Epic credentials", "title": "Reset Heroic" } }, "runexe": { - "title": "Odaberi EXE za pokrenuti" + "title": "" }, "select": { - "button": "Odaberi", - "exe": "Odaberi EXE" + "button": "Select", + "exe": "Select EXE" }, "shortcuts": { - "message": "Prečaci su napravljeni na Radnoj površini i Start meniu", - "title": "Prečaci" + "message": "Shortcuts were created on Desktop and Start Menu", + "title": "Shortcuts" }, "sync": { - "button": "Odaberi", - "error": "Pogrešni put", - "title": "Odaberi mapu za podatke" + "button": "", + "error": "", + "title": "" }, "warning": { "epic": { - "import": "Epic Serveri imaju velike probleme, igra se nemoče importati", - "install": "Epic Serveri imaju velike probleme, igra se nemože instalirati!", - "update": "Epic Serveri imaju velike probleme, igra se nemože ažurirati" + "import": "Epic Servers are having major outage right now, the game cannot be imported!", + "install": "Epic Servers are having major outage right now, the game cannot be installed!", + "update": "Epic Servers are having major outage right now, the game cannot be updated!" }, - "title": "Upozorenje" + "title": "Warning" }, - "wineprefix": "Odaberi Wine Prefix mapu", - "yes": "DA" + "wineprefix": "", + "yes": "" }, "button": { - "login": "Prijava", - "sync": "Sinkroniziraj", - "syncing": "Sinkroniziranje", - "unsync": "Desinkroniziraj" + "continue": "Continue", + "login": "Login", + "sync": "", + "syncing": "", + "unsync": "" }, - "Downloading": "Preuzima se", + "Downloading": "", "epic": { - "offline-notification-body": "Heroic vjerojatno neče raditi", + "offline-notification-body": "Heroic will maybe not work probably!", "offline-notification-title": "offline" }, "Epic Games": "Epic Games", "filter": { "noFilter": "No Filter" }, - "Filter": "Filter", - "globalSettings": "Globalne Postavke", + "Filter": "", + "globalSettings": "Global Settings", + "GOG": "GOG", "help": { - "general": "Sinkronizirajte sa EGS u slučaju da imate instalaciju Epic Games trgovine negdje drugdje i želiš uvesti igrice da ih nemoraš preuzimati opet.", + "general": "", "other": { - "part1": "Koristi ", - "part2": "Napredne Postavke", - "part3": "za pokrenuti prije pokretanja igrice; ", - "part4": "Koristi ", - "part5": "Argumenti igrice", - "part6": " pokrenuti če se poslije komande za pokretanje, naprimjer ", - "part7": " za preskoćiti pokretač u nekim igricama, itd." + "part1": "", + "part2": "", + "part3": "", + "part4": "", + "part5": "", + "part6": "", + "part7": "" }, "sync": { - "part1": "Heroic pokušava pogoditi točnu lokaciju podata i ovo če raditi u večini sličaja. U sličaju da je mapa kriva, koristi premjesti polje da ju promijeniš.", - "part2": "U sličaju da promijeti mapu podata za WINE/PROTON, morat če te provjeriti mapu opet pošto PROTON koristi drugu mapu (/pfx) i korisničko ime (steamuser). Tako da možete jednostavno obrisati trenutni mapu i pokušati ponovno u postavke sinkronizacije da Heroic pogodi pravi put.", - "part3": "Manualna sinkronizacija: Odaberi Preuzimanje da preuzmeš podatke od igrice sa Cloud-a. Prebacivanje za prebaciti lokalne datoteke u Cloud. Forsirano preuzimanje i prebacivanje če ignorirati lokalne verzije i one u Cloud-u.", - "part4": "Sinkroniziranje se automatski radi svaki put kada pokrenete igricu i završtite sa igrom." + "part1": "", + "part2": "", + "part3": "", + "part4": "" }, "wine": { - "part1": "Heroic traži verzije za WIne, Proton i CrossOver u sljedečim mapama:", - "part2": "Za druga mjesta, koristite simbolični link do tih mapa" + "part1": "", + "part2": "" } }, "info": { - "settings": "Postavke se automatski spremaju" + "settings": "" }, "infobox": { - "help": "Pomoč", - "requirements": "Zahtjevi sustava" + "help": "", + "requirements": "" }, "install": { - "path": "Odaberite put instalacije" + "path": "Select Install Path" }, - "Library": "Knižnjica", + "Library": "", "loading": { - "website": "Učitavanje Stranice" + "website": "Loading Website" }, "login": { - "loginWithEpic": "Prijava sa Epic-om", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { - "sync": "Sinkroniziranje završeno", - "unsync": "Desinkronizacija završena" + "sync": "", + "unsync": "" }, - "nogames": "Igrice nisu pronađene", - "Not Ready": "Nije Spremno", + "nogames": "", + "Not Ready": "", "notify": { "error": { - "move": "Pogreška premješanje igrice", - "reparing": "Pogreška u popravljanju" + "move": "Error Moving the Game", + "reparing": "Error Repairing" }, "finished": { - "reparing": "Zavšen popravak" + "reparing": "" }, "install": { - "canceled": "Instalacije prekinuta", + "canceled": "", "error": "", - "finished": "Instalacija gotova", - "imported": "Igrica učitana", - "startInstall": "Instalacija pokrenuta" + "finished": "", + "imported": "Game Imported", + "startInstall": "Installation Started" }, - "moved": "Premještanje gotovo", - "uninstalled": "Deinstalirano", + "moved": "", + "uninstalled": "", "update": { - "canceled": "Ažuriranje prekinuto", - "finished": "Ažuriranje gotovo", - "started": "Ažuriranje pokrenuto" + "canceled": "", + "finished": "", + "started": "Update Started" } }, "options": { "advanced": { - "placeholder": "Ovjde stavi druge postavke", - "title": "Napradne postavke (Environmment Variables):" + "placeholder": "", + "title": "" }, "gameargs": { - "placeholder": "Ovjde upiši argumente launchera", - "title": "Argumenti igrice (Za pokrenuti poslije komande):" + "placeholder": "", + "title": "" } }, "other": { - "legendary-version": "Legendary Verzije: ", - "weblate": "Pomogni prevesti Heroic." + "legendary-version": "Legendary Version: ", + "weblate": "Help Improve this translation." }, "placeholder": { - "alt-legendary-bin": "Koristi ugrađenu Legendary verziju...", - "egs-prefix": "Put gdje je EGS instaliran" + "alt-legendary-bin": "Using built-in Legendary binary...", + "egs-prefix": "" }, - "Plugins": "Dodaci", - "Projects": "Projekti", - "Ready": "Spremno", - "Recent": "Nedavno igrano", - "Return": "Nazad", - "search": "Upiši ime igrice ovdje...", + "Plugins": "Plugins", + "Projects": "Projects", + "Ready": "", + "Recent": "Played Recently", + "Return": "", + "search": "", "setting": { - "adddesktopshortcuts": "Dodaje prečace automatski", - "addgamestostartmenu": "Dodaje igrice u izbornik start automatski", - "alt-legendary-bin": "Odaberi drugu Legendary verziju za koristiti (Potrebno ponovno pokrenuti)", - "audiofix": "Popravak zvuka (Pulse audio latencija)", - "autodxvk": "Automatski instaliraj/ažuriraj DXVK na put", - "autosync": "Automatsko sinkroniziranje podataka", - "autovkd3d": "Automatski install/ažuriraj VKD3D", - "change-target-exe": "Odaberi drugi EXE za pokrenuti", - "checkForUpdatesOnStartup": "Provjeri za ažuriranjem na pokretanju", - "customWineProton": "Prilagođen put za WINE/PROTON", - "darktray": "Koristi tamnu ikonu", - "default-install-path": "Zadani put instalacije", + "adddesktopshortcuts": "Add desktop shortcuts automatically", + "addgamestostartmenu": "Add games to start menu automatically", + "alt-legendary-bin": "Choose an Alternative Legendary Binary (needs restart)to use", + "audiofix": "", + "autodxvk": "Auto Install/Update DXVK on Prefix", + "autosync": "", + "autovkd3d": "Auto Install/Update VKD3D on Prefix", + "change-target-exe": "Select an alternative EXE to run", + "checkForUpdatesOnStartup": "Check For Updates On Startup", + "customWineProton": "Custom Wine/Proton Paths", + "darktray": "Use Dark Tray Icon", + "default-install-path": "", "defaultWinePrefix": "Set Folder for new Wine Prefixes", - "discordRPC": "Omogući Discord Rich Presence", - "egs-sync": "Sinkroniziraj sa instaliranim Epic Games", - "enableFSRHack": "Omogući FSR Hack (Wine potreban za koristiti)", - "esync": "Omoguči Esync", - "exit-to-tray": "Izađi u ikonu na sistemsku ladicu", - "FsrSharpnessStrenght": "Snaga FSR Oštrine", - "fsync": "Omoguči Fsync", - "gamemode": "Koristi GameMode(Feral Game Mode mora biti instaliran)", - "language": "Odaberi Jezik", + "discordRPC": "Enable Discord Rich Presence", + "egs-sync": "", + "enableFSRHack": "Enable FSR Hack (Wine version needs to support it)", + "esync": "Enable Esync", + "exit-to-tray": "", + "FsrSharpnessStrenght": "FSR Sharpness Strength", + "fsync": "Enable Fsync", + "gamemode": "", + "language": "", "log": { - "copy-to-clipboard": "Copy log content to clipboard", - "current-log": "Current Log", + "copy-to-clipboard": "Copy log content to clipboard.", + "current-log": "Current log", "last-log": "Last Log", - "no-file": "No log file found", + "no-file": "No log file found.", "show-in-folder": "Show log file in folder" }, - "mangohud": "Omogući Mangohud (Mangohud mora biti instaliran)", + "mangohud": "", "manualsync": { - "download": "Preuzmi", - "forcedownload": "Forsiraj preuzimanje", - "forceupload": "Forsijaj prebacivanje podataka", - "sync": "Sinkroniziraj", - "syncing": "Sinkroniziranje", - "title": "Manualna sinkronizacija podataka", - "upload": "Prebaci" + "download": "", + "forcedownload": "", + "forceupload": "", + "sync": "", + "syncing": "", + "title": "", + "upload": "" }, - "maxRecentGames": "Nedavno igrano za pokazati", - "maxworkers": "Maksimalan broj radnika za preuzimanje", - "offlinemode": "Pokreni igrice bez pristupa internetu", - "primerun": "Omogući Nvidia Prime Render", - "resizableBar": "Omogući Resizable BAR (samo NVIDIA RTX 30xx)", + "maxRecentGames": "Recent Games to Show", + "maxworkers": "", + "offlinemode": "", + "primerun": "Enable Nvidia Prime Render", + "resizableBar": "Enable Resizable BAR (NVIDIA RTX only)", "runexe": { - "message": "Povicute i pustite datoteke ovjde", - "title": "Pokreni EXE na putu" + "message": "", + "title": "" }, "savefolder": { - "placeholder": "Odaberi točnu mapu podataka za igricu", - "title": "Forsiraj prebacivanje lokalnih podataka" + "placeholder": "", + "title": "" }, - "showfps": "Pokaži FPS (DX9, 10 i 11)", + "showfps": "", "showUnrealMarket": "Show Unreal Marketplace (needs restart)", - "start-in-tray": "Pokreni minimizirano", + "start-in-tray": "Start Minimized", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", - "wineprefix": "WinePut mapa", - "wineversion": "Wine Verzija" + "wineprefix": "", + "wineversion": "" }, "settings": { "clear-cache": "Clear Heroic Cache", "copiedToClipboard": "Copied to Clipboard!", - "copyToClipboard": "Kopiraj sve postavke", + "copyToClipboard": "Copy All Settings to Clipboard", "log": { "long-log-hint": "Log truncated, last 1000 lines are shown!" }, "navbar": { - "general": "Generalno", + "general": "", "log": "Log", - "other": "Ostalo", - "sync": "Spremi-Podatke" + "other": "", + "sync": "" }, "reset-heroic": "Reset Heroic" }, - "Settings": "Postavke", + "Settings": "", "status": { - "loading": "Loading Game list, please wait", "logging": "Logging In..." }, - "store": "Trgovina", + "store": "Store", "title": { "allGames": "All Games", "allUnreal": "Unreal - Everything", @@ -296,23 +297,23 @@ } }, "tooltip": { - "addpath": "Dodaj novi put", - "removepath": "Obriši put" + "addpath": "Add New Path", + "removepath": "Remove Path" }, - "Total Games": "Sveukupno igrica", + "Total Games": "", "tray": { - "about": "O projektu", - "quit": "Izađi", - "reload": "Ponovno učitaj", - "show": "Pokaži" + "about": "About", + "quit": "Quit", + "reload": "Reload", + "show": "" }, - "Unreal Marketplace": "Unreal tržište", - "Updates": "Ažuriranja", + "Unreal Marketplace": "Unreal Marketplace", + "Updates": "Updates", "userselector": { "discord": "Discord", - "logout": "Odjavi se", - "logout_confirmation": "Jeste li sigurni da se želite odjaviti?", - "quit": "Izađi" + "logout": "Logout", + "manageaccounts": "Manage Accounts", + "quit": "Quit" }, "wiki": "Wiki", "wine": { @@ -323,4 +324,4 @@ "unzipping": "Unzipping" } } -} +} \ No newline at end of file diff --git a/public/locales/hu/gamepage.json b/public/locales/hu/gamepage.json index efbf4a8d73..da1083a385 100644 --- a/public/locales/hu/gamepage.json +++ b/public/locales/hu/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Letöltési méret", "firstPlayed": "Először játszott", "installSize": "Telepítési méret", + "language": "Language", "lastPlayed": "Legutóbb játszott", "neverPlayed": "Soha", "totalPlayed": "Játszott idő" diff --git a/public/locales/hu/login.json b/public/locales/hu/login.json index 347fe18f2a..e584722c99 100644 --- a/public/locales/hu/login.json +++ b/public/locales/hu/login.json @@ -2,9 +2,6 @@ "button": { "login": "Bejelentkezés" }, - "input": { - "placeholder": "Illeszd be ide az SID számodat" - }, "message": { "part1": "Annak érdekében, hogy be tudj jelentkezni és le tudd tölteni a játékaid, először követned kell az itt lent felsoroltakat:", "part2": "Nyisd meg az", @@ -17,8 +14,7 @@ }, "status": { "error": "Hiba", - "loading": "Játéklista betöltése, kérlek várj", - "logging": "Bejelentkezés…" + "loading": "Játéklista betöltése, kérlek várj" }, "welcome": "Üdvözlünk!" } diff --git a/public/locales/hu/translation.json b/public/locales/hu/translation.json index f2b066eea7..6f54eda67d 100644 --- a/public/locales/hu/translation.json +++ b/public/locales/hu/translation.json @@ -88,6 +88,7 @@ "yes": "IGEN" }, "button": { + "continue": "Continue", "login": "Bejelentkezés", "sync": "Szinkronizálás", "syncing": "Szinkronizálás folyamatban", @@ -104,6 +105,7 @@ }, "Filter": "Szűrő", "globalSettings": "Globális beállítások", + "GOG": "GOG", "help": { "general": "Abban az esetben szinkronizálj EGL-lel , ha van egy működő, Epic Games Launcher telepítésed máshol, és importálni szeretnéd a játékaid, hogy elkerüld az újbóli letöltésüket.", "other": { @@ -141,8 +143,7 @@ "website": "Weboldal betöltése" }, "login": { - "loginWithEpic": "Bejelentkezés Epic-kel", - "loginWithSid": "Bejelentkezés SID-vel" + "externalLogin": "External Login" }, "message": { "sync": "Szinkronizálás befejezve", @@ -253,6 +254,7 @@ "showfps": "FPS megjelenítése (DX9, 10 és 11)", "showUnrealMarket": "Unreal piactér mutatása (újraindítás szükséges)", "start-in-tray": "Indítás tálcára téve", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver tároló", "wineprefix": "Wine prefix mappa", "wineversion": "Wine verzió" @@ -274,7 +276,6 @@ }, "Settings": "Beállítások", "status": { - "loading": "Játéklista betöltése, kérlek várj", "logging": "Bejelentkezés..." }, "store": "Áruház", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Kijelentkezés", "logout_confirmation": "Biztos, hogy ki akarsz jelentkezni?", + "manageaccounts": "Manage Accounts", "quit": "Kilépés" }, "wiki": "Wiki", diff --git a/public/locales/id/gamepage.json b/public/locales/id/gamepage.json index 8cdf75c953..0b840a6012 100644 --- a/public/locales/id/gamepage.json +++ b/public/locales/id/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Ukuran Unduhan", "firstPlayed": "Pertama Dimainkan", "installSize": "Ukuran Pemasangan", + "language": "Language", "lastPlayed": "Terakhir Dimainkan", "neverPlayed": "Tak pernah", "totalPlayed": "Waktu Dimainkan" diff --git a/public/locales/id/login.json b/public/locales/id/login.json index 557bbfdac8..8d8ba572ef 100644 --- a/public/locales/id/login.json +++ b/public/locales/id/login.json @@ -2,9 +2,6 @@ "button": { "login": "Masuk" }, - "input": { - "placeholder": "Tempel nomor SID disini" - }, "message": { "part1": "Untuk bisa masuk ke akunmu dan menginstall permainanmu, kamu harus mengikuti langkah-langkah di bawah:", "part2": "Buka", @@ -17,8 +14,7 @@ }, "status": { "error": "Kesalahan", - "loading": "Memuat daftar permainan, mohon tunggu", - "logging": "Masuk…" + "loading": "Memuat daftar permainan, mohon tunggu" }, "welcome": "Selamat Datang!" } diff --git a/public/locales/id/translation.json b/public/locales/id/translation.json index 7c61c80c52..2487268d42 100644 --- a/public/locales/id/translation.json +++ b/public/locales/id/translation.json @@ -88,6 +88,7 @@ "yes": "YA" }, "button": { + "continue": "Continue", "login": "Login", "sync": "Sinkronkan", "syncing": "", @@ -104,6 +105,7 @@ }, "Filter": "Saring", "globalSettings": "Pengaturan Global", + "GOG": "GOG", "help": { "general": "", "other": { @@ -141,8 +143,7 @@ "website": "Loading Website" }, "login": { - "loginWithEpic": "Login With Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "", @@ -253,6 +254,7 @@ "showfps": "", "showUnrealMarket": "Show Unreal Marketplace (needs restart)", "start-in-tray": "Start Minimized", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "", "wineversion": "" @@ -274,7 +276,6 @@ }, "Settings": "", "status": { - "loading": "Loading Game list, please wait", "logging": "Logging In..." }, "store": "Store", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "", "logout_confirmation": "Logout?", + "manageaccounts": "Manage Accounts", "quit": "" }, "wiki": "Wiki", diff --git a/public/locales/it/gamepage.json b/public/locales/it/gamepage.json index d4c182af4d..df88eec308 100644 --- a/public/locales/it/gamepage.json +++ b/public/locales/it/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Dimensione del download", "firstPlayed": "Giocato per la prima volta", "installSize": "Dimensione dell'installazione", + "language": "Language", "lastPlayed": "Giocato recentemente", "neverPlayed": "Mai", "totalPlayed": "Tempo giocato" diff --git a/public/locales/it/login.json b/public/locales/it/login.json index 5fd1bf9a9a..cc0e4d2aeb 100644 --- a/public/locales/it/login.json +++ b/public/locales/it/login.json @@ -2,9 +2,6 @@ "button": { "login": "Accedi" }, - "input": { - "placeholder": "Incolla il tuo SID qui" - }, "message": { "part1": "Per poter effettuare il login ed installare i tuoi giochi, dovrai prima seguire i passaggi indicati qui sotto:", "part2": "Apri", @@ -17,8 +14,7 @@ }, "status": { "error": "Errore", - "loading": "Caricamento elenco dei giochi, attendere", - "logging": "Sto accedendo…" + "loading": "Caricamento elenco dei giochi, attendere" }, "welcome": "Benvenuto!" } diff --git a/public/locales/it/translation.json b/public/locales/it/translation.json index 1becd6c618..9debdb2e96 100644 --- a/public/locales/it/translation.json +++ b/public/locales/it/translation.json @@ -88,6 +88,7 @@ "yes": "SI" }, "button": { + "continue": "Continue", "login": "Accedi", "sync": "Sincronizza", "syncing": "Sincronizzazione in corso", @@ -104,6 +105,7 @@ }, "Filter": "Filtra", "globalSettings": "Impostazioni globali", + "GOG": "GOG", "help": { "general": "Sincronizza con EGS nel caso tu abbia già un'installazione funzionante con l'Epic Games Store, in modo da importare i giochi senza dover riscaricarli di nuovo.", "other": { @@ -141,8 +143,7 @@ "website": "Caricamento sito web incorso" }, "login": { - "loginWithEpic": "Accedi con Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Sincronizzazione completata", @@ -253,6 +254,7 @@ "showfps": "Mostra FPS (DX9, 10 e 11)", "showUnrealMarket": "Mostra Unreal Marketplace (richiede riavvio)", "start-in-tray": "Avvio Minimizzato", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "Bottiglia CrossOver", "wineprefix": "Cartella del prefix di Wine", "wineversion": "Versione di Wine" @@ -274,7 +276,6 @@ }, "Settings": "Impostazioni", "status": { - "loading": "Caricamento elenco dei giochi, attendere", "logging": "Sto accedendo..." }, "store": "Negozio", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Disconnetti", "logout_confirmation": "Sei sicuro di voler uscire?", + "manageaccounts": "Manage Accounts", "quit": "Esci" }, "wiki": "Wiki", diff --git a/public/locales/ja/gamepage.json b/public/locales/ja/gamepage.json index 40389b3543..c34e27d1cc 100644 --- a/public/locales/ja/gamepage.json +++ b/public/locales/ja/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "ダウンロードサイズ", "firstPlayed": "最初にプレイした", "installSize": "インストールサイズ", + "language": "Language", "lastPlayed": "最後にプレイした", "neverPlayed": "一度もない", "totalPlayed": "プレイ時間" diff --git a/public/locales/ja/login.json b/public/locales/ja/login.json index 2854ca0cfa..eccc180dfc 100644 --- a/public/locales/ja/login.json +++ b/public/locales/ja/login.json @@ -2,9 +2,6 @@ "button": { "login": "ログイン" }, - "input": { - "placeholder": "ここにSID番号を貼り付けます" - }, "message": { "part1": "ログインしてゲームをインストールできるようにするには、最初に以下の手順に従う必要があります:", "part2": "", @@ -17,8 +14,7 @@ }, "status": { "error": "エラー", - "loading": "ゲームリストを読み込んでいます、お待ちください", - "logging": "ログインしています。。。" + "loading": "ゲームリストを読み込んでいます、お待ちください" }, "welcome": "ようこそ!" } diff --git a/public/locales/ja/translation.json b/public/locales/ja/translation.json index 496893693b..90e75805ab 100644 --- a/public/locales/ja/translation.json +++ b/public/locales/ja/translation.json @@ -88,6 +88,7 @@ "yes": "はい" }, "button": { + "continue": "Continue", "login": "Login", "sync": "同期", "syncing": "同期しています", @@ -104,6 +105,7 @@ }, "Filter": "フィルター", "globalSettings": "全体設定", + "GOG": "GOG", "help": { "general": "Epic Games Storeが他の場所に正常にインストールされていて、ゲームをインポートして再度ダウンロードしないようにする場合は、EGSと同期します。", "other": { @@ -141,8 +143,7 @@ "website": "Loading Website" }, "login": { - "loginWithEpic": "Login With Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "同期が完了しました", @@ -253,6 +254,7 @@ "showfps": "FPSを表示(DX9、10、11)", "showUnrealMarket": "Show Unreal Marketplace (needs restart)", "start-in-tray": "最小化を開始", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver ボトル", "wineprefix": "Wineプレフィックスフォルダー", "wineversion": "Wineバージョン" @@ -274,7 +276,6 @@ }, "Settings": "設定", "status": { - "loading": "Loading Game list, please wait", "logging": "Logging In..." }, "store": "ストア", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "ログアウト", "logout_confirmation": "ログアウトしてもよろしいですか?", + "manageaccounts": "Manage Accounts", "quit": "終了" }, "wiki": "Wiki", diff --git a/public/locales/ko/gamepage.json b/public/locales/ko/gamepage.json index 804cfe29da..974d9c9752 100644 --- a/public/locales/ko/gamepage.json +++ b/public/locales/ko/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "다운로드 크기", "firstPlayed": "처음 플레이", "installSize": "설치 크기", + "language": "Language", "lastPlayed": "마지막 플레이", "neverPlayed": "플레이 한 적 없음", "totalPlayed": "플레이 시간" diff --git a/public/locales/ko/login.json b/public/locales/ko/login.json index f6dcc1cc8d..ed8356d49f 100644 --- a/public/locales/ko/login.json +++ b/public/locales/ko/login.json @@ -2,9 +2,6 @@ "button": { "login": "로그인" }, - "input": { - "placeholder": "여기에 SID 번호를 붙여넣으세요" - }, "message": { "part1": "로그인하고 게임을 설치하려면, 먼저 아래 단계를 따라야 합니다:", "part2": "열기", @@ -17,8 +14,7 @@ }, "status": { "error": "오류", - "loading": "게임 목록을 불러오고 있습니다, 잠시만 기다려 주세요", - "logging": "로그인 중…" + "loading": "게임 목록을 불러오고 있습니다, 잠시만 기다려 주세요" }, "welcome": "환영합니다!" } diff --git a/public/locales/ko/translation.json b/public/locales/ko/translation.json index 3c5569319b..a8da87ebfe 100644 --- a/public/locales/ko/translation.json +++ b/public/locales/ko/translation.json @@ -88,6 +88,7 @@ "yes": "예" }, "button": { + "continue": "Continue", "login": "로그인", "sync": "싱크", "syncing": "싱크중", @@ -104,6 +105,7 @@ }, "Filter": "필터", "globalSettings": "글로벌 설정", + "GOG": "GOG", "help": { "general": "Epic Games Store가 다른 곳에 설치되어 있고 게임을 다시 다운로드하지 않도록 가져오려는 경우 Epic Games Store와 동기화 하십시오.", "other": { @@ -141,8 +143,7 @@ "website": "웹사이트 로드 중" }, "login": { - "loginWithEpic": "Epic으로 로그인", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "동기화 완료", @@ -253,6 +254,7 @@ "showfps": "FPS 표시 (DX9, 10 및 11)", "showUnrealMarket": "Unreal Marketplace 표시 (다시 시작 필요)", "start-in-tray": "최소화로 시작", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "Wine Prefix 폴더", "wineversion": "Wine 버전" @@ -274,7 +276,6 @@ }, "Settings": "설정", "status": { - "loading": "게임 목록을 불러오고 있습니다, 잠시만 기다려 주세요", "logging": "로그인 중…" }, "store": "스토어", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "로그아웃", "logout_confirmation": "정말로 로그아웃 하시겠습니까?", + "manageaccounts": "Manage Accounts", "quit": "종료" }, "wiki": "위키", diff --git a/public/locales/ml/gamepage.json b/public/locales/ml/gamepage.json index cf029c4678..83758ec214 100644 --- a/public/locales/ml/gamepage.json +++ b/public/locales/ml/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "ഇറക്കിയെടുപ്പിന്റെ വലുപ്പം", "firstPlayed": "ആദ്യം കളിച്ചത്", "installSize": "നടീലിന്റെ വലുപ്പം", + "language": "Language", "lastPlayed": "ഒടുക്കം കളിച്ചത്", "neverPlayed": "കളിച്ചിട്ടേയില്ല", "totalPlayed": "കളിച്ച നേരം" diff --git a/public/locales/ml/login.json b/public/locales/ml/login.json index 13e1e8beb2..d322cb41f1 100644 --- a/public/locales/ml/login.json +++ b/public/locales/ml/login.json @@ -2,9 +2,6 @@ "button": { "login": "കയറുക" }, - "input": { - "placeholder": "എസ്ഐഡി അക്കം ഇവിടെ പതിപ്പിക്കൂ" - }, "message": { "part1": "കയറി കളികള് സ്ഥാപിക്കാനായി ആദ്യം താഴെപ്പറയുന്നവ പടിക്ക് ചെയ്യുക:", "part2": "തുറക്കൂ", @@ -17,8 +14,7 @@ }, "status": { "error": "കുഴപ്പമായല്ലോ", - "loading": "കളികള് എടുത്തുകൊണ്ടിരിക്കുന്നു, ഒന്നു കാക്കൂ", - "logging": "പ്രവേശിക്കുന്നു…" + "loading": "കളികള് എടുത്തുകൊണ്ടിരിക്കുന്നു, ഒന്നു കാക്കൂ" }, "welcome": "സ്വാഗതം!" } diff --git a/public/locales/ml/translation.json b/public/locales/ml/translation.json index b73319ada7..3dd0226ee4 100644 --- a/public/locales/ml/translation.json +++ b/public/locales/ml/translation.json @@ -88,6 +88,7 @@ "yes": "ശരി" }, "button": { + "continue": "Continue", "login": "Login", "sync": "ഒന്നിപ്പിക്കൂ", "syncing": "ഒന്നിപ്പിക്കുന്നു", @@ -104,6 +105,7 @@ }, "Filter": "", "globalSettings": "ആഗോള ക്രമീകരണങ്ങള്", + "GOG": "GOG", "help": { "general": "എപിക് ഗെയിം കടയുടെ കുഴപ്പമൊന്നുില്ലാത്ത ഒരു നടൂല് മറ്റെവിടെയെങ്കിലും ഉണ്ടെങ്കില് നിങ്ങളുടെ കളികള് വീണ്ടും ഇറക്കിയെടുക്കുന്നത് ഒഴിവാക്കാനായി EGSുമായി ഒന്നിപ്പിക്കുക.", "other": { @@ -141,8 +143,7 @@ "website": "Loading Website" }, "login": { - "loginWithEpic": "Login With Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "ഒന്നിപ്പിച്ചു", @@ -253,6 +254,7 @@ "showfps": "ചിത്രനിരക്ക് കാണിക്കൂ (DX9ലും 10ലും 11ലും)", "showUnrealMarket": "Show Unreal Marketplace (needs restart)", "start-in-tray": "ചെറിയ കളമാക്കി തുറക്കൂ", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "ക്രോസ്സോവര് കുപ്പി", "wineprefix": "വൈനിരിക്കുന്ന അറ", "wineversion": "വൈനിന്റെ പതിപ്പ്" @@ -274,7 +276,6 @@ }, "Settings": "ക്രമീകരണങ്ങള്", "status": { - "loading": "Loading Game list, please wait", "logging": "Logging In..." }, "store": "കട", @@ -312,6 +313,7 @@ "discord": "ഡിസ്കോഡ഻്", "logout": "ഇറങ്ങൂ(ലോഗൌട്ട്)", "logout_confirmation": "Logout?", + "manageaccounts": "Manage Accounts", "quit": "പുറത്തുകടക്കൂ" }, "wiki": "അറിവിന്കൂട്ടം", diff --git a/public/locales/nl/gamepage.json b/public/locales/nl/gamepage.json index a0b10ac1d5..786ac21129 100644 --- a/public/locales/nl/gamepage.json +++ b/public/locales/nl/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Downloadgrootte", "firstPlayed": "Eerst gespeeld", "installSize": "Installatie grootte", + "language": "Language", "lastPlayed": "Laatst gespeeld", "neverPlayed": "Nooit", "totalPlayed": "Tijd gespeeld" diff --git a/public/locales/nl/login.json b/public/locales/nl/login.json index 90e38d02d0..4103b29a87 100644 --- a/public/locales/nl/login.json +++ b/public/locales/nl/login.json @@ -2,9 +2,6 @@ "button": { "login": "Inloggen" }, - "input": { - "placeholder": "Plak het SID nummer hier" - }, "message": { "part1": "Om in te loggen en je spellen te installeren, moet je de volgende stappen volgen:", "part2": "Open de", @@ -17,8 +14,7 @@ }, "status": { "error": "Error", - "loading": "Spellen lijst word geladen, geduld alstublieft", - "logging": "Inloggen…" + "loading": "Spellen lijst word geladen, geduld alstublieft" }, "welcome": "Welkom!" } diff --git a/public/locales/nl/translation.json b/public/locales/nl/translation.json index dda590f858..47b508162f 100644 --- a/public/locales/nl/translation.json +++ b/public/locales/nl/translation.json @@ -88,6 +88,7 @@ "yes": "JA" }, "button": { + "continue": "Continue", "login": "Login", "sync": "Synchroniseer", "syncing": "Synchroniseren", @@ -104,6 +105,7 @@ }, "Filter": "Filter", "globalSettings": "Algemene instellingen", + "GOG": "GOG", "help": { "general": "Synchroniseer met EGS als u ergens anders een werkende installatie van de Epic Games Store heeft en u uw games wilt importeren om te vermijden ze opnieuw te downloaden.", "other": { @@ -141,8 +143,7 @@ "website": "Loading Website" }, "login": { - "loginWithEpic": "Login With Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Synchronisatie compleet", @@ -253,6 +254,7 @@ "showfps": "Toon FPS (DX9, DX10 en DX11)", "showUnrealMarket": "Show Unreal Marketplace (needs restart)", "start-in-tray": "Start Geminimaliseerd", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "WinePrefix map", "wineversion": "Wine versie" @@ -274,7 +276,6 @@ }, "Settings": "Instellingen", "status": { - "loading": "Loading Game list, please wait", "logging": "Logging In..." }, "store": "Winkel", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Log uit", "logout_confirmation": "weet u zeker dat u wil uitloggen?", + "manageaccounts": "Manage Accounts", "quit": "Sluiten" }, "wiki": "Wiki", diff --git a/public/locales/pl/gamepage.json b/public/locales/pl/gamepage.json index 0d8c12fb22..03949e91de 100644 --- a/public/locales/pl/gamepage.json +++ b/public/locales/pl/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Rozmiar Pobierania", "firstPlayed": "Pierwsza Gra", "installSize": "Rozmiar Instalacji", + "language": "Language", "lastPlayed": "Ostatnia Gra", "neverPlayed": "Nigdy", "totalPlayed": "Czas Gry" diff --git a/public/locales/pl/login.json b/public/locales/pl/login.json index 9adde10f3f..e8f3de8a52 100644 --- a/public/locales/pl/login.json +++ b/public/locales/pl/login.json @@ -2,9 +2,6 @@ "button": { "login": "Zaloguj" }, - "input": { - "placeholder": "Wklej tutaj numer SID" - }, "message": { "part1": "Żeby zainstalować gry wykonaj następujące kroki :", "part2": "Otwórz", @@ -17,8 +14,7 @@ }, "status": { "error": "Błąd", - "loading": "Ładowanie listy Gier, proszę czekać", - "logging": "Logowanie…" + "loading": "Ładowanie listy Gier, proszę czekać" }, "welcome": "Witaj!" } diff --git a/public/locales/pl/translation.json b/public/locales/pl/translation.json index b995e48362..cac07fb300 100644 --- a/public/locales/pl/translation.json +++ b/public/locales/pl/translation.json @@ -88,6 +88,7 @@ "yes": "TAK" }, "button": { + "continue": "Continue", "login": "Zaloguj", "sync": "Synchronizuj", "syncing": "Synchronizacja", @@ -104,6 +105,7 @@ }, "Filter": "Filtr", "globalSettings": "Ustawienia Globalne", + "GOG": "GOG", "help": { "general": "Synchronizuj z EGS, gdy masz zainstalowane Epic Games Store w innym miejscu i chcesz zaimportować swoje gry, aby uniknąć ich ponownego pobierania.", "other": { @@ -141,8 +143,7 @@ "website": "Ładowanie Strony" }, "login": { - "loginWithEpic": "Zaloguj Przez Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Synchronizacja zakończona", @@ -253,6 +254,7 @@ "showfps": "Pokaż FPS (DX9, 10 and 11)", "showUnrealMarket": "Pokaż Unreal Marketplace (wymaga restartu)", "start-in-tray": "Uruchom zminimalizowany", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "Butelki w CrossOver", "wineprefix": "Folder prefiksu Wine", "wineversion": "Wersja Wine" @@ -274,7 +276,6 @@ }, "Settings": "Ustawienia", "status": { - "loading": "Ładowanie listy Gier, proszę czekać", "logging": "Logowanie..." }, "store": "Sklep", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Wyloguj", "logout_confirmation": "Czy na pewno chcesz się wylogować?", + "manageaccounts": "Manage Accounts", "quit": "Wyjdź" }, "wiki": "Wiki", diff --git a/public/locales/pt/gamepage.json b/public/locales/pt/gamepage.json index b4c4b07970..b2684aea55 100644 --- a/public/locales/pt/gamepage.json +++ b/public/locales/pt/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Tamanho do Download", "firstPlayed": "Jogado pela primeira vez", "installSize": "Tamanho Instalado", + "language": "Language", "lastPlayed": "Jogado pela última vez", "neverPlayed": "Nunca", "totalPlayed": "Tempo de Jogo" diff --git a/public/locales/pt/login.json b/public/locales/pt/login.json index 2976bd1956..5132de9650 100644 --- a/public/locales/pt/login.json +++ b/public/locales/pt/login.json @@ -2,9 +2,6 @@ "button": { "login": "Entrar" }, - "input": { - "placeholder": "Cole o número SID aqui" - }, "message": { "part1": "Para que você possa entrar e instalar os teus jogos, é necessário seguir os seguintes passos:", "part2": "Abra a", @@ -17,8 +14,7 @@ }, "status": { "error": "Erro", - "loading": "Carregando lista de jogos", - "logging": "Entrando…" + "loading": "Carregando lista de jogos" }, "welcome": "Bem Vindo!" } diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json index 0a61b6ef86..24452c3b94 100644 --- a/public/locales/pt/translation.json +++ b/public/locales/pt/translation.json @@ -88,6 +88,7 @@ "yes": "SIM" }, "button": { + "continue": "Continue", "login": "Login", "sync": "Sincronizar", "syncing": "Sincronizando", @@ -104,6 +105,7 @@ }, "Filter": "Filtro", "globalSettings": "Configurações Globais", + "GOG": "GOG", "help": { "general": "Caso você tenha a Epic Games Store instalada em um Prefix Wine, você pode usar a sincronização para importar e exportar jogos entre ela e o Heroic.", "other": { @@ -141,8 +143,7 @@ "website": "Loading Website" }, "login": { - "loginWithEpic": "Login With Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Sync Concluído", @@ -253,6 +254,7 @@ "showfps": "Mostrar FPS (DX9, 10 e 11)", "showUnrealMarket": "Show Unreal Marketplace (needs restart)", "start-in-tray": "Iniciar Minimizado", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "Prefixo Wine", "wineversion": "Versão do Wine" @@ -274,7 +276,6 @@ }, "Settings": "Configurações", "status": { - "loading": "Loading Game list, please wait", "logging": "Logging In..." }, "store": "Loja", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Sair", "logout_confirmation": "Logout?", + "manageaccounts": "Manage Accounts", "quit": "Fechar" }, "wiki": "Wiki", diff --git a/public/locales/pt_BR/gamepage.json b/public/locales/pt_BR/gamepage.json index b83bb21be2..a913677867 100644 --- a/public/locales/pt_BR/gamepage.json +++ b/public/locales/pt_BR/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Tamanho do Download", "firstPlayed": "Jogado inicialmente em", "installSize": "Tamanho Instalado", + "language": "Language", "lastPlayed": "Jogado pela última vez em", "neverPlayed": "Nunca", "totalPlayed": "Tempo de jogo" diff --git a/public/locales/pt_BR/login.json b/public/locales/pt_BR/login.json index 77292d4aec..7fbabd2fd5 100644 --- a/public/locales/pt_BR/login.json +++ b/public/locales/pt_BR/login.json @@ -2,9 +2,6 @@ "button": { "login": "Conecte-se" }, - "input": { - "placeholder": "Cole seu número SID aqui" - }, "message": { "part1": "Para fazer login e instalar seus jogos, primeiro você precisa seguir as etapas abaixo:", "part2": "Abrir o", @@ -17,8 +14,7 @@ }, "status": { "error": "Erro", - "loading": "Carregando lista de jogos, aguarde por favor", - "logging": "Logando…" + "loading": "Carregando lista de jogos, aguarde por favor" }, "welcome": "Bem-vindo(a)!" } diff --git a/public/locales/pt_BR/translation.json b/public/locales/pt_BR/translation.json index bbd3c35390..d2f20aa5f2 100644 --- a/public/locales/pt_BR/translation.json +++ b/public/locales/pt_BR/translation.json @@ -88,6 +88,7 @@ "yes": "SIM" }, "button": { + "continue": "Continue", "login": "Conecte-se", "sync": "Sincronizar", "syncing": "Sincronizando", @@ -104,6 +105,7 @@ }, "Filter": "Filtrar", "globalSettings": "Configurações globais", + "GOG": "GOG", "help": { "general": "Se você tiver uma instalação funcional da Epic Games Launcher e quiser importar seus jogos use a sincronização para evitar baixá-los novamente.", "other": { @@ -141,8 +143,7 @@ "website": "Carregando site" }, "login": { - "loginWithEpic": "Login With Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Sincronização Completa", @@ -253,6 +254,7 @@ "showfps": "Exibir contador de FPS (DX9, 10 e 11)", "showUnrealMarket": "Mostrar Unreal Marketplace (requer reinício)", "start-in-tray": "Iniciar minimizado", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "Pasta do prefixo Wine", "wineversion": "Versão do WINE" @@ -274,7 +276,6 @@ }, "Settings": "Configurações", "status": { - "loading": "Carregando lista de jogos, aguarde por favor", "logging": "Logando..." }, "store": "Loja", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Sair", "logout_confirmation": "Tem certeza que deseja sair da sua conta?", + "manageaccounts": "Manage Accounts", "quit": "Sair" }, "wiki": "Wiki", diff --git a/public/locales/ru/gamepage.json b/public/locales/ru/gamepage.json index 60c1ef1d30..5284983561 100644 --- a/public/locales/ru/gamepage.json +++ b/public/locales/ru/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Размер загрузки", "firstPlayed": "Первый запуск", "installSize": "Размер установки", + "language": "Language", "lastPlayed": "Последний запуск", "neverPlayed": "Никогда", "totalPlayed": "Проведено в игре" diff --git a/public/locales/ru/login.json b/public/locales/ru/login.json index 2f7f921653..5023ebf03d 100644 --- a/public/locales/ru/login.json +++ b/public/locales/ru/login.json @@ -2,9 +2,6 @@ "button": { "login": "Вход" }, - "input": { - "placeholder": "Вставьте сюда свой SID" - }, "message": { "part1": "Чтобы войти и установить игры, необходимо выполнить следующие действия:", "part2": "Откройте", @@ -17,8 +14,7 @@ }, "status": { "error": "Ошибка", - "loading": "Загружается список игр. Пожалуйста, подождите", - "logging": "Вход…" + "loading": "Загружается список игр. Пожалуйста, подождите" }, "welcome": "Добро пожаловать!" } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index f55b472219..cfa2dba34f 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -88,6 +88,7 @@ "yes": "ДА" }, "button": { + "continue": "Continue", "login": "Вход", "sync": "Синхронизировать", "syncing": "Синхронизация", @@ -104,6 +105,7 @@ }, "Filter": "Фильтр", "globalSettings": "Глобальные настройки", + "GOG": "GOG", "help": { "general": "Синхронизируйте с EGL, в случае если у вас есть работающий Epic Games Launcher в другом месте и вы хотите импортировать свои игры, чтобы избежать их повторного скачивания.", "other": { @@ -141,8 +143,7 @@ "website": "Загрузка веб-сайта" }, "login": { - "loginWithEpic": "Вход через Epic", - "loginWithSid": "Вход через SID" + "externalLogin": "External Login" }, "message": { "sync": "Синхронизация завершена", @@ -253,6 +254,7 @@ "showfps": "Показывать FPS (DX9, 10 и 11)", "showUnrealMarket": "Показать Unreal Marketplace (требуется перезапуск)", "start-in-tray": "Запускать свернутым в трей", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "Бутылка CrossOver", "wineprefix": "Папка префикса Wine", "wineversion": "Версия Wine" @@ -274,7 +276,6 @@ }, "Settings": "Настройки", "status": { - "loading": "Загружается список игр. Пожалуйста, подождите", "logging": "Вход…" }, "store": "Магазин", @@ -312,6 +313,7 @@ "discord": "Дискорд", "logout": "Выйти из аккаунта", "logout_confirmation": "Вы уверены, что хотите выйти?", + "manageaccounts": "Manage Accounts", "quit": "Закрыть Heroic" }, "wiki": "Вики", diff --git a/public/locales/sv/gamepage.json b/public/locales/sv/gamepage.json index 1f78dd5cea..2cdb8711a1 100644 --- a/public/locales/sv/gamepage.json +++ b/public/locales/sv/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Nedladdningsstorlek", "firstPlayed": "Först spelat", "installSize": "Installationsstorlek", + "language": "Language", "lastPlayed": "Senast spelat", "neverPlayed": "Aldrig", "totalPlayed": "Speltid" diff --git a/public/locales/sv/login.json b/public/locales/sv/login.json index 05caa1702d..beb9d8988a 100644 --- a/public/locales/sv/login.json +++ b/public/locales/sv/login.json @@ -2,9 +2,6 @@ "button": { "login": "Logga in" }, - "input": { - "placeholder": "Klistra in ditt SID nummer här" - }, "message": { "part1": "För att logga in och installera spel måste du följa instruktionerna nedan:", "part2": "Öppna", @@ -17,8 +14,7 @@ }, "status": { "error": "Fel", - "loading": "Laddar spellistan, var god dröj", - "logging": "Loggar in…" + "loading": "Laddar spellistan, var god dröj" }, "welcome": "Välkommen!" } diff --git a/public/locales/sv/translation.json b/public/locales/sv/translation.json index 21b6c5ce0f..61e9ab1204 100644 --- a/public/locales/sv/translation.json +++ b/public/locales/sv/translation.json @@ -88,6 +88,7 @@ "yes": "Ja" }, "button": { + "continue": "Continue", "login": "Logga in", "sync": "Synkronisering", "syncing": "Synkroniserar", @@ -104,6 +105,7 @@ }, "Filter": "Filtrera", "globalSettings": "Globala inställningar", + "GOG": "GOG", "help": { "general": "Synkronisera med EGS om du har en fungerande installation av Epic Games Store någon annanstans och vill importera dina spel för att undvika att ladda ner dem på nytt.", "other": { @@ -141,8 +143,7 @@ "website": "Loading Website" }, "login": { - "loginWithEpic": "Login With Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Synkronisering klar", @@ -253,6 +254,7 @@ "showfps": "Visa FPS (DX9, 10 och 11)", "showUnrealMarket": "Show Unreal Marketplace (needs restart)", "start-in-tray": "Starta minimerad", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "WinePrefix mapp", "wineversion": "Wine version" @@ -274,7 +276,6 @@ }, "Settings": "Inställningar", "status": { - "loading": "Loading Game list, please wait", "logging": "Logging In..." }, "store": "Butik", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Logga ut", "logout_confirmation": "Är du säker på att du vill logga ut?", + "manageaccounts": "Manage Accounts", "quit": "Avsluta" }, "wiki": "Wiki", diff --git a/public/locales/ta/gamepage.json b/public/locales/ta/gamepage.json index 8a3c2f494f..eccb6fc7e6 100644 --- a/public/locales/ta/gamepage.json +++ b/public/locales/ta/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Download Size", "firstPlayed": "First Played", "installSize": "Install Size", + "language": "Language", "lastPlayed": "Last Played", "neverPlayed": "Never", "totalPlayed": "Time Played" diff --git a/public/locales/ta/login.json b/public/locales/ta/login.json index e7b299a2a7..e72d64547e 100644 --- a/public/locales/ta/login.json +++ b/public/locales/ta/login.json @@ -2,9 +2,6 @@ "button": { "login": "உள்நுழை" }, - "input": { - "placeholder": "SID எண்ணை இங்கே ஒட்டவும்" - }, "message": { "part1": "நீங்கள் உள்நுழைந்து உங்கள் விளையாட்டுகளை நிறுவ, முதலில் நீங்கள் கீழே உள்ள வழிமுறைகளை பின்பற்ற வேண்டும்:", "part2": " ", @@ -17,8 +14,7 @@ }, "status": { "error": "பிழை", - "loading": "விளையாட்டு பட்டியல் ஏற்றப்படுகிறது, தயவுசெய்து காத்திருக்கவும்", - "logging": "உள்நுழைந்து கொண்டிருக்கிறது…" + "loading": "விளையாட்டு பட்டியல் ஏற்றப்படுகிறது, தயவுசெய்து காத்திருக்கவும்" }, "welcome": "உங்களை வரவேற்கிறோம்!" } diff --git a/public/locales/ta/translation.json b/public/locales/ta/translation.json index cb9e15f837..c183307600 100644 --- a/public/locales/ta/translation.json +++ b/public/locales/ta/translation.json @@ -88,6 +88,7 @@ "yes": "ஆம்" }, "button": { + "continue": "Continue", "login": "Login", "sync": "ஒத்திசைவு செய்", "syncing": "ஒத்திசைவு செய்யப்படுகிறது", @@ -104,6 +105,7 @@ }, "Filter": "வடிகட்டி", "globalSettings": "உலகளாவிய விருப்பத்தேர்வுகள்", + "GOG": "GOG", "help": { "general": "நீங்கள் எபிக் கேம்ஸ் ஸ்டோரை வேறு இடத்தில் நிறுவியிருந்தால், அதை மீண்டும் பதிவிறக்குவதைத் தவிர்க்க மற்றும் உங்கள் விளையாட்டுகளை இறக்குமதி செய்ய, EGS உடன் ஒத்திசைக்கவும்.", "other": { @@ -141,8 +143,7 @@ "website": "Loading Website" }, "login": { - "loginWithEpic": "Login With Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "ஒத்திசைவு நிறைப்படைந்தது", @@ -253,6 +254,7 @@ "showfps": "FPS ஐக் காட்டு (DX9, 10 and 11)", "showUnrealMarket": "Show Unreal Marketplace (needs restart)", "start-in-tray": "Start Minimized", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "WinePrefix கோப்புறை", "wineversion": "Wine பதிப்பு" @@ -274,7 +276,6 @@ }, "Settings": "அமைப்புகள்", "status": { - "loading": "Loading Game list, please wait", "logging": "Logging In..." }, "store": "விற்பனைக்கூடம்", @@ -312,6 +313,7 @@ "discord": "டிஸ்கார்ட் (Discord)", "logout": "வெளியேறு", "logout_confirmation": "நீங்கள் நிச்சயமாக வெளியேற விரும்புகிறீர்களா?", + "manageaccounts": "Manage Accounts", "quit": "வெளியேறு" }, "wiki": "விக்கி", diff --git a/public/locales/tr/gamepage.json b/public/locales/tr/gamepage.json index de92754954..5dd31cbc4f 100644 --- a/public/locales/tr/gamepage.json +++ b/public/locales/tr/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "İndirme Boyutu", "firstPlayed": "İlk Oynanan", "installSize": "Kurulum Boyutu", + "language": "Language", "lastPlayed": "Son Oynanan", "neverPlayed": "Hiç Oynanmadı", "totalPlayed": "Oynanan Zaman" diff --git a/public/locales/tr/login.json b/public/locales/tr/login.json index 5a5da310c9..0b1846a6aa 100644 --- a/public/locales/tr/login.json +++ b/public/locales/tr/login.json @@ -2,9 +2,6 @@ "button": { "login": "Oturum Aç" }, - "input": { - "placeholder": "SID numaranızı buraya yapıştırın" - }, "message": { "part1": "Oturum açabilmeniz ve oyunlarınızı kurabilmeniz için öncelikle aşağıdaki adımları uygulamanız gerekiyor:", "part2": "Epic Store'u", @@ -17,8 +14,7 @@ }, "status": { "error": "Hata", - "loading": "Oyun listesi yükleniyor, lütfen bekleyin", - "logging": "Oturum Açılıyor…" + "loading": "Oyun listesi yükleniyor, lütfen bekleyin" }, "welcome": "Hoş Geldiniz!" } diff --git a/public/locales/tr/translation.json b/public/locales/tr/translation.json index e2fa215061..3946d4b19a 100644 --- a/public/locales/tr/translation.json +++ b/public/locales/tr/translation.json @@ -88,6 +88,7 @@ "yes": "EVET" }, "button": { + "continue": "Continue", "login": "Oturum Aç", "sync": "Eşzamanla", "syncing": "Eşzamanlanıyor", @@ -104,6 +105,7 @@ }, "Filter": "Filtre", "globalSettings": "Genel Ayarlar", + "GOG": "GOG", "help": { "general": "Başka bir yerde çalışan bir Epic Games Launcher kurulumunuz varsa ve oyunlarınızı tekrar indirmemek için onları içeri aktarmak istiyorsanız EGL ile eşzamanlayın.", "other": { @@ -141,8 +143,7 @@ "website": "Web Sitesi Yükleniyor" }, "login": { - "loginWithEpic": "Epic ile Oturum Aç", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "Eşzamanlama Tamamlandı", @@ -253,6 +254,7 @@ "showfps": "FPS'i Göster (DX9, 10 and 11)", "showUnrealMarket": "Unreal Pazar Yerini Göster (yeniden başlatma gerekir)", "start-in-tray": "Simge Durumunda Başlat", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Şişesi", "wineprefix": "WinePrefix klasörü", "wineversion": "Wine Sürümü" @@ -274,7 +276,6 @@ }, "Settings": "Ayarlar", "status": { - "loading": "Oyun listesi yükleniyor, lütfen bekleyin", "logging": "Oturum Açılıyor…" }, "store": "Mağaza", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "Oturumu kapat", "logout_confirmation": "Oturumu kapatmak istediğinizden emin misiniz?", + "manageaccounts": "Manage Accounts", "quit": "Çık" }, "wiki": "Wiki", diff --git a/public/locales/zh_Hans/gamepage.json b/public/locales/zh_Hans/gamepage.json index a2ccd4269a..a7735945d7 100644 --- a/public/locales/zh_Hans/gamepage.json +++ b/public/locales/zh_Hans/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "下载大小", "firstPlayed": "首次游玩", "installSize": "安装大小", + "language": "Language", "lastPlayed": "上次游玩", "neverPlayed": "从未游玩", "totalPlayed": "游玩时间" diff --git a/public/locales/zh_Hans/login.json b/public/locales/zh_Hans/login.json index aa8ff82125..ad7fae7ea9 100644 --- a/public/locales/zh_Hans/login.json +++ b/public/locales/zh_Hans/login.json @@ -2,9 +2,6 @@ "button": { "login": "登录" }, - "input": { - "placeholder": "在此处粘贴SID号" - }, "message": { "part1": "为了登录并安装游戏,你需要按照以下步骤操作:", "part2": "打开", @@ -17,8 +14,7 @@ }, "status": { "error": "错误", - "loading": "正在加载游戏列表,请稍后", - "logging": "正在登录…" + "loading": "正在加载游戏列表,请稍后" }, "welcome": "欢迎你!" } diff --git a/public/locales/zh_Hans/translation.json b/public/locales/zh_Hans/translation.json index 88082c8711..da5dd2ece8 100644 --- a/public/locales/zh_Hans/translation.json +++ b/public/locales/zh_Hans/translation.json @@ -88,6 +88,7 @@ "yes": "是" }, "button": { + "continue": "Continue", "login": "登录", "sync": "同步", "syncing": "正在同步", @@ -104,6 +105,7 @@ }, "Filter": "筛选", "globalSettings": "全局设置", + "GOG": "GOG", "help": { "general": "与 Epic Games 启动器同步,如果你在其他地方安装了Epic Games 启动器,想要导入你的游戏,以免再次下载它们。", "other": { @@ -141,8 +143,7 @@ "website": "正在加载网站" }, "login": { - "loginWithEpic": "通过 Epic 网页登录", - "loginWithSid": "通过 Epic SID 登录" + "externalLogin": "External Login" }, "message": { "sync": "同步完成", @@ -253,6 +254,7 @@ "showfps": "显示帧数(Directx 9,10和11)", "showUnrealMarket": "显示 Unreal 市场 (需重启)", "start-in-tray": "启动时最小化", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver 容器", "wineprefix": "WINEPREFIX文件夹", "wineversion": "Wine 版本" @@ -274,7 +276,6 @@ }, "Settings": "设置", "status": { - "loading": "正在加载游戏列表,请稍后", "logging": "正在登录..." }, "store": "商店", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "退出登录", "logout_confirmation": "你确定要退出登录吗?", + "manageaccounts": "Manage Accounts", "quit": "退出" }, "wiki": "Wiki百科", diff --git a/public/locales/zh_Hant/gamepage.json b/public/locales/zh_Hant/gamepage.json index 051708a690..b853533200 100644 --- a/public/locales/zh_Hant/gamepage.json +++ b/public/locales/zh_Hant/gamepage.json @@ -53,6 +53,7 @@ "downloadSize": "Download Size", "firstPlayed": "首次遊玩", "installSize": "Install Size", + "language": "Language", "lastPlayed": "上次遊玩", "neverPlayed": "從未遊玩", "totalPlayed": "遊玩時間" diff --git a/public/locales/zh_Hant/login.json b/public/locales/zh_Hant/login.json index 246cb2ade6..0ad142f17c 100644 --- a/public/locales/zh_Hant/login.json +++ b/public/locales/zh_Hant/login.json @@ -2,9 +2,6 @@ "button": { "login": "登入" }, - "input": { - "placeholder": "在此處貼上SID號碼" - }, "message": { "part1": "為了讓您能夠登入並安裝您的遊戲,您首先需要按照以下步驟操作:", "part2": "打開", @@ -17,8 +14,7 @@ }, "status": { "error": "錯誤", - "loading": "正在讀取遊戲列表,請稍候", - "logging": "正在登入…" + "loading": "正在讀取遊戲列表,請稍候" }, "welcome": "歡迎您!" } diff --git a/public/locales/zh_Hant/translation.json b/public/locales/zh_Hant/translation.json index 68120fe79a..c54c3bb497 100644 --- a/public/locales/zh_Hant/translation.json +++ b/public/locales/zh_Hant/translation.json @@ -88,6 +88,7 @@ "yes": "是" }, "button": { + "continue": "Continue", "login": "Login", "sync": "同步", "syncing": "正在同步", @@ -104,6 +105,7 @@ }, "Filter": "篩選", "globalSettings": "全域設定", + "GOG": "GOG", "help": { "general": "與Epic Games商城同步,如果您在其他地方已經安裝了Epic Games啟動器,可以導入您的遊戲以避免再次下載它們。", "other": { @@ -141,8 +143,7 @@ "website": "Loading Website" }, "login": { - "loginWithEpic": "Login With Epic", - "loginWithSid": "Login with SID" + "externalLogin": "External Login" }, "message": { "sync": "同步完成", @@ -253,6 +254,7 @@ "showfps": "顯示幀數(Directx 9,10和11)", "showUnrealMarket": "Show Unreal Marketplace (needs restart)", "start-in-tray": "啟動時最小化", + "steamruntime": "Use Steam Runtime", "winecrossoverbottle": "CrossOver Bottle", "wineprefix": "WINEPREFIX文件夾", "wineversion": "WINE版本" @@ -274,7 +276,6 @@ }, "Settings": "設定", "status": { - "loading": "Loading Game list, please wait", "logging": "Logging In..." }, "store": "商店", @@ -312,6 +313,7 @@ "discord": "Discord", "logout": "登出", "logout_confirmation": "您確定要登出嗎?", + "manageaccounts": "Manage Accounts", "quit": "退出" }, "wiki": "Wiki百科", diff --git a/src/App.tsx b/src/App.tsx index 3342d7122f..93761fab8e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,13 +21,19 @@ function App() { const configStore: ElectronStore = new Store({ cwd: 'store' }) - const user = configStore.get('userInfo') - const { data: library, recentGames, category } = context + const gogStore: ElectronStore = new Store({ + cwd: 'gog_store' + }) - const dlcCount = library.filter((lib) => lib.install.is_dlc) - const numberOfGames = library.length - dlcCount.length - const showRecentGames = !!recentGames.length && category === 'games' + const user = configStore.has('userInfo') || gogStore.has('credentials') + const { epicLibrary, gogLibrary, recentGames, category } = context + const dlcCount = epicLibrary.filter((lib) => lib.install.is_dlc) + const numberOfGames = + category == 'epic' + ? epicLibrary.length - dlcCount.length + : gogLibrary.length + const showRecentGames = !!recentGames.length && category !== 'unreal' return (
@@ -35,7 +41,7 @@ function App() {
- {user ? ( + {user && ( <>
)} - +
- ) : ( - )} + diff --git a/src/assets/epic-logo.svg b/src/assets/epic-logo.svg new file mode 100644 index 0000000000..993d677439 --- /dev/null +++ b/src/assets/epic-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/fallback-image.jpg b/src/assets/fallback-image.jpg new file mode 100644 index 0000000000..e33240d382 Binary files /dev/null and b/src/assets/fallback-image.jpg differ diff --git a/src/assets/gog-logo.svg b/src/assets/gog-logo.svg new file mode 100644 index 0000000000..3639a2a495 --- /dev/null +++ b/src/assets/gog-logo.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/components/UI/Header/index.tsx b/src/components/UI/Header/index.tsx index 34b6731400..4311ed10c3 100644 --- a/src/components/UI/Header/index.tsx +++ b/src/components/UI/Header/index.tsx @@ -4,7 +4,7 @@ import { Link, useHistory } from 'react-router-dom' import React, { useContext } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faWindows, faApple } from '@fortawesome/free-brands-svg-icons' +import { faWindows, faApple, faLinux } from '@fortawesome/free-brands-svg-icons' import { UE_VERSIONS } from './constants' import { useTranslation } from 'react-i18next' @@ -45,6 +45,7 @@ export default function Header({ ).length const hasUpdates = gameUpdates.length const isMac = platform === 'darwin' + const isLinux = platform === 'linux' const link = goTo ? goTo : '' function handleClick() { @@ -78,9 +79,9 @@ export default function Header({ return ( <>
- {category === 'games' && ( + {category !== 'unreal' && ( - {isMac && ( + {(isMac || (isLinux && category === 'gog')) && (
)} setInstallLanguage(e.target.value)} + > + {installLanguages && + installLanguages.map((value) => ( + + ))} + +
+ )} {t('install.path', 'Select Install Path')}: @@ -225,7 +283,7 @@ export default function InstallModal({ appName, backdropClick }: Props) { title: t('install.path') }) .then(({ path }: Path) => - setInstallPath(path ? `'${path}'` : defaultPath) + setInstallPath(path ? path : defaultPath) ) } > @@ -237,7 +295,7 @@ export default function InstallModal({ appName, backdropClick }: Props) { {getDownloadedProgress()} - {isLinux && ( + {isLinux && !isLinuxNative && ( {t('install.wineprefix', 'WinePrefix')}: diff --git a/src/screens/Library/constants.ts b/src/screens/Library/constants.ts index 42e3252cd1..40688cd491 100644 --- a/src/screens/Library/constants.ts +++ b/src/screens/Library/constants.ts @@ -5,7 +5,7 @@ export function getLibraryTitle( filter: string, t: TFunction<'translation'> ) { - if (category === 'games') { + if (category === 'epic' || category === 'gog') { switch (filter) { case 'installed': return t('title.installedGames', 'Installed Games') diff --git a/src/screens/Library/index.tsx b/src/screens/Library/index.tsx index 56d947abe6..73978404cc 100644 --- a/src/screens/Library/index.tsx +++ b/src/screens/Library/index.tsx @@ -2,7 +2,7 @@ import './index.css' import React, { lazy, useContext, useEffect, useRef, useState } from 'react' -import { GameInfo } from 'src/types' +import { GameInfo, Runner } from 'src/types' import ContextProvider from 'src/state/ContextProvider' import cx from 'classnames' @@ -25,7 +25,11 @@ interface Props { export const Library = ({ library, showRecentsOnly }: Props) => { const { layout, gameUpdates, refreshing, category, filter } = useContext(ContextProvider) - const [showModal, setShowModal] = useState({ game: '', show: false }) + const [showModal, setShowModal] = useState({ + game: '', + show: false, + runner: 'legendary' as Runner + }) const { t } = useTranslation() const backToTopElement = useRef(null) @@ -52,8 +56,8 @@ export const Library = ({ library, showRecentsOnly }: Props) => { } } - function handleModal(appName: string) { - setShowModal({ game: appName, show: true }) + function handleModal(appName: string, runner: Runner) { + setShowModal({ game: appName, show: true, runner }) } if (refreshing && !showRecentsOnly) { @@ -74,7 +78,10 @@ export const Library = ({ library, showRecentsOnly }: Props) => { {showModal.show && ( setShowModal({ game: '', show: false })} + runner={showModal.runner} + backdropClick={() => + setShowModal({ game: '', show: false, runner: 'legendary' }) + } /> )}

@@ -97,6 +104,8 @@ export const Library = ({ library, showRecentsOnly }: Props) => { app_name, is_installed, is_mac_native, + is_linux_native, + runner, is_game, install: { version, install_size, is_dlc } }: GameInfo) => { @@ -107,6 +116,7 @@ export const Library = ({ library, showRecentsOnly }: Props) => { return ( { version={`${version}`} size={`${install_size}`} hasUpdate={hasUpdate} - buttonClick={() => handleModal(app_name)} + buttonClick={() => handleModal(app_name, runner)} forceCard={showRecentsOnly} isMacNative={is_mac_native} + isLinuxNative={is_linux_native} /> ) } diff --git a/src/screens/Login/components/Runner/index.css b/src/screens/Login/components/Runner/index.css new file mode 100644 index 0000000000..0dcd376998 --- /dev/null +++ b/src/screens/Login/components/Runner/index.css @@ -0,0 +1,65 @@ +.runnerWrapper { + background-color: var(--background-darker-80); + padding: 20px; + border-radius: 15px; + min-width: min(500px, 30%); + display: flex; + flex-direction: column; + justify-content: space-between; + backdrop-filter: blur(10px); +} + +.runnerWrapper > h1 { + color: var(--text-default); +} + +.runnerLogin { + padding: 10px; + font-size: 25px; + border-radius: 15px; + background-color: var(--background-secondary); + border: none; + box-shadow: 4px 4px 4px rgba(0, 0, 0, 0.25); + transition: 0.3s; +} + +.logged { + color: var(--danger); + cursor: pointer; +} + +.logged:hover { + background-color: var(--danger-hover); + color: var(--text-secondary); +} + +.runnerLogin.alternative { + margin-top: 20px; + cursor: pointer; + color: var(--text-secondary); +} +.runnerLogin:hover { + color: var(--text-hover); +} + +.runnerWrapper img, +.runnerWrapper svg { + width: 100px; + margin: 20px; + fill: #fff; +} + +.userData { + color: var(--text-secondary); + /* For longer nicknames */ + overflow: hidden; + font-size: 20px; + margin: 10px 0; +} + +.runnerWrapper.epic { + margin-right: 32px; +} +.runnerWrapper.gog { + margin-left: 32px; +} diff --git a/src/screens/Login/components/Runner/index.tsx b/src/screens/Login/components/Runner/index.tsx new file mode 100644 index 0000000000..151b3aeacb --- /dev/null +++ b/src/screens/Login/components/Runner/index.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' +import './index.css' + +interface RunnerProps { + loginUrl: string + class: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + icon: any + isLoggedIn: boolean + // eslint-disable-next-line @typescript-eslint/no-explicit-any + user: any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + logoutAction: () => any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + alternativeLoginAction?: () => any + refresh: () => void +} + +export default function Runner(props: RunnerProps) { + const { t } = useTranslation() + async function handleLogout() { + await props.logoutAction() + window.localStorage.clear() + props.refresh() + } + return ( +
+
{props.icon()}
+ {props.isLoggedIn && ( +
+ {props.user?.displayName || props.user?.username} +
+ )} +
+ {!props.isLoggedIn ? ( + +
{t('button.login', 'Login')}
+ + ) : ( +
{ + handleLogout() + }} + > + {t('userselector.logout', 'Logout')} +
+ )} +
+ {props.alternativeLoginAction && !props.isLoggedIn && ( +
+ {t('login.externalLogin', 'External Login')} +
+ )} +
+ ) +} diff --git a/src/screens/Login/components/SIDLogin/index.css b/src/screens/Login/components/SIDLogin/index.css new file mode 100644 index 0000000000..d3ec5aa756 --- /dev/null +++ b/src/screens/Login/components/SIDLogin/index.css @@ -0,0 +1,77 @@ +.SIDLoginModal { + display: grid; + place-items: center; + width: var(--content-width); + height: 100%; + position: fixed; + top: 0px; + z-index: 8; +} + +.backdrop { + display: grid; + place-items: center; + position: fixed; + background: var(--background-darker-80); + width: 100%; + height: 100%; + z-index: 6; +} + +.sid-modal { + position: relative; + display: grid; + place-items: center; + min-width: fit-content; + background: var(--background-darker); + border-radius: 10px; + width: 48vw; + max-width: 560px; + z-index: 7; + padding: 32px; + height: fit-content; + min-height: 225px; + border: solid 1px var(--background); + overflow: auto; + max-height: 100vh; + font-size: 17px; +} + +.loginInstructions > strong { + color: var(--text-hover); + font-size: 1.5em; +} + +.loginInstructions .epicLink { + color: var(--text-hover); + cursor: pointer; +} +.loginInstructions .epicLink:hover { + color: var(--link-highlight); +} + +.loginInstructions .material-icons { + color: var(--link-highlight); +} + +.loginInstructions li { + text-align: left; +} + +.sid-modal > .sid-input { + color: var(--text-default); + font-size: 17px; + padding: 5px; + background-color: var(--background); + border: none; + height: 50px; + width: 250px; + border-radius: 10px; + margin: 10px; +} + +.SIDLoginModal .message > .material-icons { + color: var(--text-hover); + animation: 2s refreshing; + animation-iteration-count: infinite; +} diff --git a/src/screens/Login/components/SIDLogin/index.tsx b/src/screens/Login/components/SIDLogin/index.tsx new file mode 100644 index 0000000000..934e101334 --- /dev/null +++ b/src/screens/Login/components/SIDLogin/index.tsx @@ -0,0 +1,105 @@ +import React, { useState } from 'react' +import Info from '@mui/icons-material/Info' +import { useTranslation } from 'react-i18next' +import { loginPage, sidInfoPage } from 'src/helpers' +import './index.css' +import { Autorenew } from '@mui/icons-material' + +const { ipcRenderer, clipboard } = window.require('electron') + +interface Props { + backdropClick: () => void + refresh: () => void +} + +export default function SIDLogin({ backdropClick, refresh }: Props) { + const { t } = useTranslation('login') + const [input, setInput] = useState('') + const [status, setStatus] = useState({ + loading: false, + message: '' + }) + + const { loading, message } = status + + const handleLogin = async (sid: string) => { + await ipcRenderer.invoke('login', sid).then(async (res) => { + setStatus({ + loading: true, + message: t('status.loading', 'Loading Game list, please wait') + }) + ipcRenderer.send('logInfo', 'Called Login') + console.log(res) + if (res !== 'error') { + await ipcRenderer.invoke('getUserInfo') + setStatus({ loading: false, message: '' }) + backdropClick() + refresh() + } else { + setStatus({ loading: true, message: t('status.error', 'Error') }) + setTimeout(() => { + setStatus({ ...status, loading: false }) + }, 2500) + } + }) + } + return ( +
+ +
+
+ {t('welcome', 'Welcome!')} +

+ {t( + 'message.part1', + 'In order for you to be able to log in and install your games, we first need you to follow the steps below:' + )} +

+
    +
  1. + {`${t('message.part2')} `} + loginPage()} className="epicLink"> + {t('message.part3')} + + {`${t('message.part4')} `} + sidInfoPage()} className="sid"> + {`${t('message.part5')}`} + + +
  2. +
  3. + {`${t('message.part6')} `} + sidInfoPage()} className="sid"> + {`${t('message.part7')}`} + + {` ${t('message.part8')}`} +
  4. +
+
+ setInput(clipboard.readText('clipboard'))} + onChange={(e) => setInput(e.target.value)} + /> + {loading && ( +

+ {message} + {' '} +

+ )} + +
+
+ ) +} diff --git a/src/screens/Login/index.css b/src/screens/Login/index.css index 384fc9b339..a682bc92ef 100644 --- a/src/screens/Login/index.css +++ b/src/screens/Login/index.css @@ -1,7 +1,7 @@ -.Login { - display: grid; +.loginPage { width: 100%; height: 100%; + overflow-y: hidden; } .loginBackground { @@ -11,195 +11,58 @@ background: url('../../assets/login-background-1536x864.jpg'); background-size: cover; z-index: -1; - opacity: 0.4; + opacity: 0.3; } -.language { - cursor: pointer; -} - -.selectedLanguage { - color: #07c5ef; - font-weight: 600; -} - -.heroicLogo { - width: 205px; - height: 48px; - place-self: baseline; - margin-bottom: 14px; - display: flex; -} - -.heroicText { - width: 150px; - height: 47px; - margin-left: 57px; - display: flex; - flex-direction: column; - justify-content: center; - text-align: left; -} - -.heroicTitle { - font-family: Rubik; - font-style: normal; - font-weight: 500; - font-size: 25px; - line-height: 30px; - color: var(--download-button); -} - -.heroicSubTitle { - font-family: Rubik; - font-style: normal; - font-weight: 400; - font-size: 18px; - line-height: 24.3px; - color: var(--text-default); -} - -.logo { - position: absolute; - width: 46px; - height: 46px; - border-radius: 5px; -} - -.aboutWrapper { - display: grid; - place-items: center; - place-content: center; +.loginContentWrapper { width: 100%; height: 100%; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; } -.aboutContainer { - max-width: 550px; - padding-top: 12px; -} - -.loginFormWrapper { - display: grid; - place-items: center; - place-content: center; - width: 100%; - height: 100%; - background: var(--background-darker); -} - -.loginInstructions { - font-family: Cabin; - font-style: normal; - font-weight: 500; - font-size: 18px; - line-height: 27px; - color: var(--text-default); - text-align: left; -} - -.loginInstructions strong { - color: var(--text-default); -} - -.loginInstructions .epicLink { - color: var(--download-button); - cursor: pointer; - text-decoration: underline; -} - -.loginInstructions .sid { - color: var(--text-hover); - text-decoration: underline; - cursor: pointer; -} - -.loginInstructions ol { - padding-left: 20px; -} - -.loginInstructions ol li { - margin-bottom: 10px; -} - -.loginInstructions p { - margin-bottom: 35px; - margin-top: 10px; -} - -.pastesidtext { - color: var(--text-default); - font-weight: 400; - font-size: 16px; - margin-bottom: 13px; -} - -.loginForm > button { - margin-top: 16px; - font-weight: 700; -} - -.loginForm { +.runnerList { display: flex; - flex-direction: column; + flex-direction: row; align-items: center; - justify-content: space-around; - width: 400px; - align-self: center; + justify-content: center; + position: relative; + user-select: none; + width: 100%; + flex-wrap: wrap; } -.loginInput { - width: 388px; - height: 63px; - background: var(--background); - border-radius: 10px; - font-family: Cabin; - font-style: normal; - font-weight: normal; - font-size: 16px; - line-height: 19px; - outline: none; +.continueLogin { + position: relative; + font-family: var(--font-primary-bold); + top: 10%; + background-color: var(--background-secondary); color: var(--text-default); - text-align: center; border: none; - margin-bottom: 16px; + font-size: 24px; + padding: 15px; + border-radius: 10px; + cursor: pointer; + transition: 0.3s; + box-shadow: 4px 4px 4px rgba(0, 0, 0, 0.25); + width: fit-content; } -.loginForm > .message { - font-family: Cabin; - font-style: normal; - font-weight: normal; - font-size: 14px; - line-height: 17px; - color: var(--text-hover); - display: flex; - margin: 0; +.continueLogin:hover { + color: var(--text-tertiary); + background-color: var(--link-highlight); } -.message > .material-icons { - font-size: 16px; - margin-left: 4px; - animation: refreshing 1.5s infinite; +.continueLogin.disabled { + color: var(--text-secondary); + opacity: 0.8; + cursor: default; } -.settingSelect.language-login { - width: 140px; - height: 30px; - text-indent: 10px; +.language-login { position: absolute; - bottom: 12px; - right: 10px; -} - -.helpWrapper { - margin-top: 12px; - text-align: center; - color: var(--download-button); - width: 96%; -} - -.helpWrapper > .buttonWrapper { - display: flex; - flex-wrap: wrap; - justify-content: space-between; + width: 300px; + top: 10vh; } diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index 0ccab72f37..d21a2b3286 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -1,157 +1,137 @@ +import React, { useContext, useEffect, useState } from 'react' import './index.css' - -import React, { useContext, useState } from 'react' - -import { loginPage, sidInfoPage } from 'src/helpers' +import EpicLogo from '../../assets/epic-logo.svg' +import Runner from './components/Runner' +import ElectronStore from 'electron-store' import { useTranslation } from 'react-i18next' -import LanguageSelector, { - FlagPosition -} from 'src/components/UI/LanguageSelector' +import cx from 'classnames' +import { useHistory } from 'react-router' -import { Clipboard, IpcRenderer } from 'electron' -import Autorenew from '@mui/icons-material/Autorenew' -import Info from '@mui/icons-material/Info' -import logo from 'src/assets/heroic-icon.png' import ContextProvider from 'src/state/ContextProvider' -import { useHistory } from 'react-router-dom' - -const storage: Storage = window.localStorage - -export default function Login() { - const { t, i18n } = useTranslation('login') - const { refreshLibrary } = useContext(ContextProvider) - const history = useHistory() - const { ipcRenderer, clipboard } = window.require('electron') as { - ipcRenderer: IpcRenderer - clipboard: Clipboard - } +import GOGLogo from 'src/assets/gog-logo.svg' +import { LanguageSelector, UpdateComponent } from 'src/components/UI' +import { FlagPosition } from 'src/components/UI/LanguageSelector' +import SIDLogin from './components/SIDLogin' - const [input, setInput] = useState('') - const [status, setStatus] = useState({ - loading: false, - message: '' - }) - const { loading, message } = status +const { ipcRenderer } = window.require('electron') +const Store = window.require('electron-store') +const storage: Storage = window.localStorage +export default function NewLogin() { + const { t, i18n } = useTranslation() + const currentLanguage = i18n.language const handleChangeLanguage = (language: string) => { storage.setItem('language', language) i18n.changeLanguage(language) } + const history = useHistory() + const { refreshLibrary, handleCategory } = useContext(ContextProvider) + const [epicLogin, setEpicLogin] = useState({}) + const [gogLogin, setGOGLogin] = useState({}) + const [loading, setLoading] = useState(true) + const [showSidLogin, setShowSidLogin] = useState(false) - const currentLanguage = i18n.language - - const handleLogin = async (sid: string) => { - setStatus({ - loading: true, - message: t('status.logging', 'Logging In...') + function refreshUserInfo() { + const configStore: ElectronStore = new Store({ + cwd: 'store' + }) + const gogStore: ElectronStore = new Store({ + cwd: 'gog_store' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setEpicLogin(configStore.get('userInfo') as any) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setGOGLogin(gogStore.get('userData') as any) + } - await ipcRenderer.invoke('login', sid).then(async (res) => { - ipcRenderer.send('logInfo', 'Called Login') - console.log(res) - if (res !== 'error') { - setStatus({ - loading: true, - message: t('status.loading', 'Loading Game list, please wait') - }) - await ipcRenderer.invoke('getUserInfo') - await ipcRenderer.invoke('refreshLibrary', true) - await ipcRenderer.invoke('refreshWineVersionInfo', true) - await refreshLibrary({ - fullRefresh: true, - runInBackground: false - }) - return history.push('/') - } else { - ipcRenderer.send('logError', res) - } + function eventHandler() { + setTimeout(refreshUserInfo, 1000) + console.log('Caught signal') + } - setStatus({ loading: true, message: t('status.error', 'Error') }) - setTimeout(() => { - setStatus({ ...status, loading: false }) - }, 2500) + useEffect(() => { + refreshUserInfo() + setLoading(false) + ipcRenderer.on('updateLoginState', () => eventHandler) + return () => { + ipcRenderer.removeListener('updateLoginState', () => eventHandler) + } + }, []) + async function continueLogin() { + setLoading(true) + await refreshLibrary({ + fullRefresh: true, + runInBackground: false }) + //Make sure we cannot get to library that we can't see + handleCategory(epicLogin ? 'epic' : 'gog') + setLoading(false) + history.push('/') } - return ( -
-
-
-
- -
- Heroic - Games Launcher -
-
-
- {t('welcome', 'Welcome!')} -

- {t( - 'message.part1', - 'In order for you to be able to log in and install your games, we first need you to follow the steps below:' - )} -

-
    -
  1. - {`${t('message.part2')} `} - loginPage()} className="epicLink"> - {t('message.part3')} - - {`${t('message.part4')} `} - sidInfoPage()} className="sid"> - {`${t('message.part5')}`} - - - . -
  2. -
  3. - {`${t('message.part6')} `} - sidInfoPage()} className="sid"> - {`${t('message.part7')}`} - - {` ${t('message.part8')}`} -
  4. -
-
+
+ {loading && ( +
+
- { + setShowSidLogin(false) + }} /> -
-
-
-
- - {t('input.placeholder', 'Paste the SID number here')} - - setInput(event.target.value)} - onAuxClick={() => setInput(clipboard.readText('clipboard'))} - value={input} + )} +
+ +
+ {!loading && ( + + )} +
+ } + isLoggedIn={Boolean(epicLogin)} + user={epicLogin} + refresh={refreshUserInfo} + logoutAction={() => { + ipcRenderer.invoke('logoutLegendary') + console.log('Logging out') + window.location.reload() + }} + alternativeLoginAction={() => { + setShowSidLogin(true) + }} + /> + } + loginUrl="/login/gog" + isLoggedIn={Boolean(gogLogin)} + user={gogLogin} + refresh={refreshUserInfo} + logoutAction={() => { + ipcRenderer.invoke('logoutGOG') + setGOGLogin({}) + }} /> - {loading && ( -

- {message} - {' '} -

- )} -
+
) diff --git a/src/screens/Settings/components/OtherSettings/index.tsx b/src/screens/Settings/components/OtherSettings/index.tsx index 9f10951dfa..2e7b908785 100644 --- a/src/screens/Settings/components/OtherSettings/index.tsx +++ b/src/screens/Settings/components/OtherSettings/index.tsx @@ -16,6 +16,7 @@ interface Props { audioFix: boolean isDefault: boolean isMacNative: boolean + isLinuxNative: boolean launcherArgs: string canRunOffline: boolean offlineMode: boolean @@ -42,6 +43,8 @@ interface Props { toggleDiscordRPC: () => void targetExe: string useGameMode: boolean + useSteamRuntime: boolean + toggleUseSteamRuntime: () => void } export default function OtherSettings({ @@ -73,7 +76,10 @@ export default function OtherSettings({ maxRecentGames, setTargetExe, targetExe, - isMacNative + isMacNative, + isLinuxNative, + toggleUseSteamRuntime, + useSteamRuntime }: Props) { const handleOtherOptions = (event: ChangeEvent) => setOtherOptions(event.currentTarget.value) @@ -84,7 +90,7 @@ export default function OtherSettings({ const isWin = platform === 'win32' const isLinux = platform === 'linux' const supportsShortcuts = isWin || isLinux - const shouldRenderFpsOption = !isMacNative && !isWin + const shouldRenderFpsOption = !isMacNative && !isWin && !isLinuxNative return ( <> @@ -185,6 +191,18 @@ export default function OtherSettings({ {t('setting.mangohud')} + {isLinuxNative && ( + + + + {t('setting.steamruntime', 'Use Steam Runtime')} + + + )} )} {canRunOffline && ( diff --git a/src/screens/Settings/index.tsx b/src/screens/Settings/index.tsx index 961f121534..9abd34d934 100644 --- a/src/screens/Settings/index.tsx +++ b/src/screens/Settings/index.tsx @@ -3,14 +3,14 @@ import './index.css' import React, { useContext, useEffect, useState } from 'react' import classNames from 'classnames' -import { AppSettings, WineInstallation } from 'src/types' +import { AppSettings, Runner, WineInstallation } from 'src/types' import { Clipboard, IpcRenderer } from 'electron' import { NavLink, useLocation, useParams } from 'react-router-dom' -import { getGameInfo, writeConfig } from 'src/helpers' +import { getGameInfo, getPlatform, writeConfig } from 'src/helpers' import { useToggle } from 'src/hooks' import { useTranslation } from 'react-i18next' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faWindows, faApple } from '@fortawesome/free-brands-svg-icons' +import { faWindows, faApple, faLinux } from '@fortawesome/free-brands-svg-icons' import { ContentCopyOutlined, CleaningServicesOutlined, @@ -40,6 +40,7 @@ interface RouteParams { interface LocationState { fromGameCard: boolean + runner: Runner } function Settings() { @@ -88,6 +89,11 @@ function Settings() { toggle: toggleUseGameMode, setOn: setUseGameMode } = useToggle(false) + const { + on: useSteamRuntime, + toggle: toggleUseSteamRuntime, + setOn: setUseSteamRuntime + } = useToggle(false) const { on: checkForUpdatesOnStartup, toggle: toggleCheckForUpdatesOnStartup, @@ -178,6 +184,7 @@ function Settings() { const [altWine, setAltWine] = useState([] as WineInstallation[]) const [isMacNative, setIsMacNative] = useState(false) + const [isLinuxNative, setIsLinuxNative] = useState(false) const [isCopiedToClipboard, setCopiedToClipboard] = useState(false) @@ -233,18 +240,20 @@ function Settings() { setAltLegendaryBin(config.altLegendaryBin || '') setShowUnrealMarket(config.showUnrealMarket || false) setDefaultWinePrefix(config.defaultWinePrefix) - + setUseSteamRuntime(config.useSteamRuntime || false) if (!isDefault) { const { cloud_save_enabled: cloudSaveEnabled, save_folder: saveFolder, title: gameTitle, canRunOffline: can_run_offline, - is_mac_native - } = await getGameInfo(appName) + is_mac_native, + is_linux_native + } = await getGameInfo(appName, state.runner) setCanRunOffline(can_run_offline) setTitle(gameTitle) - setIsMacNative(is_mac_native) + setIsMacNative(is_mac_native && (await getPlatform()) == 'darwin') + setIsLinuxNative(is_linux_native && (await getPlatform()) == 'linux') return setHaveCloudSaving({ cloudSaveEnabled, saveFolder }) } return setTitle(t('globalSettings', 'Global Settings')) @@ -310,11 +319,12 @@ function Settings() { useGameMode, wineCrossoverBottle, winePrefix, - wineVersion + wineVersion, + useSteamRuntime } as AppSettings const settingsToSave = isDefault ? GlobalSettings : GameSettings - const shouldRenderWineSettings = !isWin && !isMacNative + const shouldRenderWineSettings = !isWin && !isMacNative && !isLinuxNative let returnPath: string | null = '/' if (state && !state.fromGameCard) { returnPath = `/gameconfig/${appName}` @@ -350,25 +360,43 @@ function Settings() { )} {shouldRenderWineSettings && ( - + Wine )} {!isDefault && haveCloudSaving.cloudSaveEnabled && ( {t('settings.navbar.sync')} )} { - + {t('settings.navbar.other')} } { - + {t('settings.navbar.log', 'Log')} } @@ -382,7 +410,11 @@ function Settings() { > {title} {!isDefault && ( - + )} )} @@ -536,7 +568,10 @@ function Settings() { discordRPC={discordRPC} targetExe={targetExe} setTargetExe={setTargetExe} + useSteamRuntime={useSteamRuntime} + toggleUseSteamRuntime={toggleUseSteamRuntime} isMacNative={isMacNative} + isLinuxNative={isLinuxNative} /> )} {isSyncSettings && ( diff --git a/src/screens/WebView/index.tsx b/src/screens/WebView/index.tsx index 5d87803e3f..2c4699c6bc 100644 --- a/src/screens/WebView/index.tsx +++ b/src/screens/WebView/index.tsx @@ -1,32 +1,29 @@ import React, { useContext, useLayoutEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useLocation } from 'react-router' +import { useLocation, useParams, useHistory } from 'react-router' import { UpdateComponent } from 'src/components/UI' import WebviewControls from 'src/components/UI/WebviewControls' import ContextProvider from 'src/state/ContextProvider' -import { Webview } from 'src/types' +import { Runner, Webview } from 'src/types' const { clipboard, ipcRenderer } = window.require('electron') import './index.css' -type Props = { - isLogin?: boolean -} - type SID = { sid: string } -export default function WebView({ isLogin }: Props) { +export default function WebView() { const { i18n } = useTranslation() const { pathname } = useLocation() const { t } = useTranslation() - const { refreshLibrary, handleFilter } = useContext(ContextProvider) + const { handleFilter, handleCategory } = useContext(ContextProvider) const [loading, setLoading] = useState<{ refresh: boolean message: string }>({ refresh: true, message: t('loading.website', 'Loading Website') }) + const history = useHistory() let lang = i18n.language if (i18n.language === 'pt') { @@ -36,17 +33,27 @@ export default function WebView({ isLogin }: Props) { const loginUrl = 'https://www.epicgames.com/id/login?redirectUrl=https%3A%2F%2Fwww.epicgames.com%2Fid%2Fapi%2Fredirect' const epicStore = `https://www.epicgames.com/store/${lang}/` + const gogStore = `https://gog.com` const wikiURL = 'https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher/wiki' + const gogEmbedRegExp = new RegExp('https://embed.gog.com/on_login_success?') + const gogLoginUrl = + 'https://auth.gog.com/auth?client_id=46899977096215655&redirect_uri=https%3A%2F%2Fembed.gog.com%2Fon_login_success%3Forigin%3Dclient&response_type=code&layout=galaxy' const trueAsStr = 'true' as unknown as boolean | undefined const webview = document.querySelector('webview') as Webview - - const startUrl = isLogin ? '/login' : pathname + const { runner } = useParams() as { runner: Runner } + const startUrl = runner + ? runner == 'legendary' + ? '/loginEpic' + : '/loginGOG' + : pathname const urls = { '/epicstore': epicStore, + '/gogstore': gogStore, '/wiki': wikiURL, - '/login': loginUrl + '/loginEpic': loginUrl, + '/loginGOG': gogLoginUrl } useLayoutEffect(() => { @@ -55,8 +62,26 @@ export default function WebView({ isLogin }: Props) { const loadstop = () => { setLoading({ ...loading, refresh: false }) // Ignore the login handling if not on login page - if (pathname !== '/') { + if (!runner) { return + } else if (runner === 'gog') { + const pageUrl = webview.getURL() + if (pageUrl.match(gogEmbedRegExp)) { + const parsedURL = new URL(pageUrl) + const code = parsedURL.searchParams.get('code') + setLoading({ + refresh: true, + message: t('status.logging', 'Logging In...') + }) + ipcRenderer.invoke('authGOG', code).then(() => { + setLoading({ + refresh: false, + message: t('status.logging', 'Logging In...') + }) + handleCategory('gog') + history.push('/login') + }) + } } // Deals with Login else { @@ -74,21 +99,12 @@ export default function WebView({ isLogin }: Props) { refresh: true, message: t('status.logging', 'Logging In...') }) - await ipcRenderer.invoke('login', sid) - handleFilter('all') - - setLoading({ - refresh: true, - message: t( - 'status.loading', - 'Loading Game list, please wait' - ) - }) - await refreshLibrary({ - fullRefresh: true, - runInBackground: false + await ipcRenderer.invoke('login', sid).then(() => { + handleFilter('all') + handleCategory('epic') + setLoading({ ...loading, refresh: false }) + history.push('/login') }) - setLoading({ ...loading, refresh: false }) } catch (error) { console.error(error) ipcRenderer.send('logError', error) diff --git a/src/state/ContextProvider.tsx b/src/state/ContextProvider.tsx index 8bb8b2a030..3279aa6e0a 100644 --- a/src/state/ContextProvider.tsx +++ b/src/state/ContextProvider.tsx @@ -3,8 +3,9 @@ import React from 'react' import { ContextType } from 'src/types' const initialContext: ContextType = { - category: 'games', - data: [], + category: 'epic', + epicLibrary: [], + gogLibrary: [], wineVersions: [], error: false, filter: 'all', diff --git a/src/state/GlobalState.tsx b/src/state/GlobalState.tsx index b01b004197..b30da6613a 100644 --- a/src/state/GlobalState.tsx +++ b/src/state/GlobalState.tsx @@ -3,6 +3,7 @@ import React, { PureComponent } from 'react' import { GameInfo, GameStatus, + InstalledInfo, RefreshOptions, WineVersionInfo } from 'src/types' @@ -29,6 +30,14 @@ const wineDownloaderInfoStore: ElectronStore = new Store({ name: 'wine-downloader-info' }) +const gogLibraryStore = new Store({ cwd: 'gog_store', name: 'library' }) +const gogInstalledGamesStore = new Store({ + cwd: 'gog_store', + name: 'installed' +}) +const gogConfigStore = new Store({ + cwd: 'gog_store' +}) const RTL_LANGUAGES = ['fa'] type T = TFunction<'gamepage'> & TFunction<'translations'> @@ -41,7 +50,8 @@ interface Props { interface StateProps { category: string - data: GameInfo[] + epicLibrary: GameInfo[] + gogLibrary: GameInfo[] wineVersions: WineVersionInfo[] error: boolean filter: string @@ -56,11 +66,29 @@ interface StateProps { } export class GlobalState extends PureComponent { + loadGOGLibrary = (): Array => { + const games = gogLibraryStore.has('games') + ? (gogLibraryStore.get('games') as GameInfo[]) + : [] + const installedGames = + (gogInstalledGamesStore.get('installed') as Array) || [] + for (const igame in games) { + for (const installedGame in installedGames) { + if (installedGames[installedGame].appName == games[igame].app_name) { + games[igame].install = installedGames[installedGame] + games[igame].is_installed = true + } + } + } + + return games + } state: StateProps = { - category: 'games', - data: libraryStore.has('library') + category: 'epic', + epicLibrary: libraryStore.has('library') ? (libraryStore.get('library') as GameInfo[]) : [], + gogLibrary: this.loadGOGLibrary(), wineVersions: wineDownloaderInfoStore.has('wine-releases') ? (wineDownloaderInfoStore.get('wine-releases') as WineVersionInfo[]) : [], @@ -79,17 +107,18 @@ export class GlobalState extends PureComponent { refresh = async (checkUpdates?: boolean): Promise => { let updates = this.state.gameUpdates console.log('refreshing') - const currentLibraryLength = this.state.data?.length - let library: Array = + const currentLibraryLength = this.state.epicLibrary?.length + let epicLibrary: Array = (libraryStore.get('library') as Array) || [] - if (!library.length || !this.state.data.length) { + const gogLibrary: Array = this.loadGOGLibrary() + if (!epicLibrary.length || !this.state.epicLibrary.length) { ipcRenderer.send( 'logInfo', 'No cache found, getting data from legendary...' ) const { library: legendaryLibrary } = await getLegendaryConfig() - library = legendaryLibrary + epicLibrary = legendaryLibrary } try { @@ -102,12 +131,13 @@ export class GlobalState extends PureComponent { this.setState({ filterText: '', - data: library, + epicLibrary, + gogLibrary, gameUpdates: updates, refreshing: false }) - if (currentLibraryLength !== library.length) { + if (currentLibraryLength !== epicLibrary.length) { ipcRenderer.send('logInfo', 'Force Update') this.forceUpdate() } @@ -212,9 +242,15 @@ export class GlobalState extends PureComponent { filterPlatform = (library: GameInfo[], filter: string) => { switch (filter) { case 'win': - return library.filter((game) => !game.is_mac_native) + return library.filter((game) => + process.platform == 'darwin' + ? !game.is_mac_native + : !game.is_linux_native + ) case 'mac': return library.filter((game) => game.is_mac_native) + case 'linux': + return library.filter((game) => game.is_linux_native) default: return library.filter((game) => game.is_game) } @@ -224,7 +260,8 @@ export class GlobalState extends PureComponent { appName, status, folder, - progress + progress, + runner }: GameStatus) => { const { libraryStatus, gameUpdates } = this.state const currentApp = @@ -304,7 +341,7 @@ export class GlobalState extends PureComponent { (game) => game.appName !== appName ) this.setState({ libraryStatus: updatedLibraryStatus }) - ipcRenderer.send('removeShortcut', appName) + ipcRenderer.send('removeShortcut', appName, runner) return this.refreshLibrary({}) } @@ -354,15 +391,16 @@ export class GlobalState extends PureComponent { async componentDidMount() { const { i18n, t } = this.props - const { data, gameUpdates = [], libraryStatus } = this.state + const { epicLibrary, gameUpdates = [], libraryStatus } = this.state // Deals launching from protocol. Also checks if the game is already running - ipcRenderer.on('launchGame', async (e, appName) => { + ipcRenderer.on('launchGame', async (e, appName, runner) => { const currentApp = libraryStatus.filter( (game) => game.appName === appName )[0] if (!currentApp) { - return launch({ appName, t }) + // Add finding a runner for games + return launch({ appName, t, runner }) } }) @@ -370,7 +408,7 @@ export class GlobalState extends PureComponent { const currentApp = libraryStatus.filter( (game) => game.appName === appName )[0] - const { appName, installPath } = args + const { appName, installPath, runner } = args if (!currentApp || (currentApp && currentApp.status !== 'installing')) { return install({ appName, @@ -383,7 +421,8 @@ export class GlobalState extends PureComponent { eta: '00:00:00', percent: '0.00%' }, - t + t, + runner }) } }) @@ -392,15 +431,15 @@ export class GlobalState extends PureComponent { const { libraryStatus } = this.state this.handleGameStatus({ ...libraryStatus, ...args }) }) - - const user = configStore.get('userInfo') + const legendaryUser = configStore.get('userInfo') + const gogUser = gogConfigStore.get('userData') const platform = await getPlatform() - const category = storage.getItem('category') || 'games' + const category = storage.getItem('category') || 'epic' const filter = storage.getItem('filter') || 'all' const layout = storage.getItem('layout') || 'grid' const language = storage.getItem('language') || 'en' - if (!user) { + if (!legendaryUser) { await ipcRenderer.invoke('getUserInfo') } @@ -412,11 +451,11 @@ export class GlobalState extends PureComponent { i18n.changeLanguage(language) this.setState({ category, filter, language, layout, platform }) - if (user) { + if (legendaryUser || gogUser) { this.refreshLibrary({ checkForUpdates: true, fullRefresh: true, - runInBackground: Boolean(data.length) + runInBackground: Boolean(epicLibrary.length) }) } @@ -445,9 +484,17 @@ export class GlobalState extends PureComponent { render() { const { children } = this.props - const { data, wineVersions, filterText, filter, platform, filterPlatform } = - this.state - let filteredLibrary = data + const { + epicLibrary, + wineVersions, + gogLibrary, + filterText, + filter, + platform, + filterPlatform + } = this.state + let filteredEpicLibrary = epicLibrary + let filteredGOGLibrary = gogLibrary const language = storage.getItem('language') || 'en' const isRTL = RTL_LANGUAGES.includes(language) @@ -455,19 +502,33 @@ export class GlobalState extends PureComponent { const filterRegex = new RegExp(filterText, 'i') const textFilter = ({ title, app_name }: GameInfo) => filterRegex.test(title) || filterRegex.test(app_name) - filteredLibrary = this.filterPlatform( - this.filterLibrary(data, filter).filter(textFilter), + filteredEpicLibrary = this.filterPlatform( + this.filterLibrary(epicLibrary, filter).filter(textFilter), + filterPlatform + ) + filteredGOGLibrary = this.filterPlatform( + this.filterLibrary(gogLibrary, filter).filter(textFilter), filterPlatform ) } catch (error) { console.log(error) } + let recentGames: GameInfo[] = [] + + if (epicLibrary.length > 0) { + recentGames = [...getRecentGames(epicLibrary)] + } + if (gogLibrary.length > 0) { + recentGames = [...recentGames, ...getRecentGames(gogLibrary)] + } + return ( { refresh: this.refresh, refreshLibrary: this.refreshLibrary, refreshWineVersionInfo: this.refreshWineVersionInfo, - recentGames: getRecentGames(data) + recentGames }} > {children} diff --git a/src/types.ts b/src/types.ts index ef66cd4d60..6b49855064 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,11 +41,13 @@ export interface AppSettings { defaultWinePrefix: string winePrefix: string wineVersion: WineInstallation + useSteamRuntime: boolean } export interface ContextType { category: string - data: GameInfo[] + epicLibrary: GameInfo[] + gogLibrary: GameInfo[] wineVersions: WineVersionInfo[] recentGames: GameInfo[] error: boolean @@ -75,6 +77,8 @@ interface ExtraInfo { } export interface GameInfo { + runner: Runner + store_url: string app_name: string art_cover: string art_logo: string @@ -87,6 +91,7 @@ export interface GameInfo { install: InstalledInfo is_game: boolean is_mac_native: boolean + is_linux_native: boolean is_installed: boolean is_ue_asset: boolean is_ue_plugin: boolean @@ -118,6 +123,7 @@ export interface GameSettings { wineCrossoverBottle: string winePrefix: string wineVersion: WineInstallation + useSteamRuntime: boolean } type DLCInfo = { @@ -152,6 +158,7 @@ type GameManifest = { install_tags: Array launch_exe: string prerequisites: Prerequisites + languages?: Array } export interface InstallInfo { game: GameInstallInfo @@ -162,6 +169,7 @@ export interface GameStatus { appName: string progress?: string folder?: string + runner?: Runner status: | 'installing' | 'updating' @@ -188,6 +196,12 @@ export interface InstalledInfo { install_size: string | null is_dlc: boolean | null version: string | null + platform?: string + appName?: string + installedWithDLCs?: boolean // For verifing GOG games + language?: string // For verifing GOG games + versionEtag?: string // Checksum for checking GOG updates + buildId?: string // For verifing GOG games } export interface KeyImage { @@ -241,9 +255,44 @@ export type ElWebview = { export type Webview = HTMLWebViewElement & ElWebview +export interface GOGGameInfo { + tags: string[] + id: number + availability: { + isAvailable: boolean + isAvailableInAccount: boolean + } + title: string + url: string + worksOn: { + Windows: boolean + Mac: boolean + Linux: boolean + } + category: string + rating: number + isComingSoom: boolean + isGame: boolean + slug: string + isNew: boolean + dlcCount: number + releaseDate: { + date: string + timezone_type: number + timezone: string + } + isBaseProductMissing: boolean + isHidingDisabled: boolean + isInDevelopment: boolean + // eslint-disable-next-line @typescript-eslint/no-explicit-any + extraInfo: any[] + isHidden: boolean +} export interface GamepadActionStatus { [key: string]: { triggeredAt: { [key: number]: number } repeatDelay: false | number } } + +export type Runner = 'legendary' | 'gog' | 'heroic' diff --git a/yarn.lock b/yarn.lock index 512ba26dda..462478614a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2034,6 +2034,11 @@ dependencies: i18next "^21.0.1" +"@types/ini@^1.3.31": + version "1.3.31" + resolved "https://registry.yarnpkg.com/@types/ini/-/ini-1.3.31.tgz#c78541a187bd88d5c73e990711c9d85214800d1b" + integrity sha512-8ecxxaG4AlVEM1k9+BsziMw8UsX0qy3jYI1ad/71RrDZ+rdL6aZB0wLfAuflQiDhkD5o4yJ0uPK3OSUic3fG0w== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -6138,7 +6143,7 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -ini@2.0.0: +ini@2.0.0, ini@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==