Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Feat] Add support for Sideloading Browser Apps and Games #2739

Merged
merged 12 commits into from
May 29, 2023
4 changes: 3 additions & 1 deletion public/locales/en/gamepage.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,15 @@
"title": "Title"
},
"info": {
"broser": "BrowserURL",
"exe": "Select Executable",
"image": "App Image",
"title": "Game/App Title"
},
"placeholder": {
"image": "Paste an Image URL here",
"title": "Add a title to your Game/App"
"title": "Add a title to your Game/App",
"url": "Paste the Game URL here"
}
},
"specs": {
Expand Down
1 change: 1 addition & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@
"prefered_language": "2-char code (i.e.: \"en\" or \"fr\")"
},
"platforms": {
"browser": "Browser",
"linux": "Linux",
"mac": "Mac",
"win": "Windows"
Expand Down
51 changes: 2 additions & 49 deletions src/backend/storeManagers/sideload/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
} from 'common/types'
import { libraryStore } from './electronStores'
import { GameConfig } from '../../game_config'
import { isWindows, isMac, isLinux, icon } from '../../constants'
import { isWindows, isMac, isLinux } from '../../constants'
import { killPattern, shutdownWine } from '../../utils'
import { logInfo, LogPrefix, logWarning } from '../../logger/logger'
import path, { dirname, resolve } from 'path'
import { dirname } from 'path'
import { existsSync, rmSync } from 'graceful-fs'
import i18next from 'i18next'
import {
Expand All @@ -20,11 +20,9 @@ import {
} from '../../shortcuts/shortcuts/shortcuts'
import { notify } from '../../dialog/dialog'
import { sendFrontendMessage } from '../../main_window'
import { app, BrowserWindow } from 'electron'
import { launchGame } from 'backend/storeManagers/storeManagerCommon/games'
import { GOGCloudSavesLocation } from 'common/types/gog'
import { InstallResult, RemoveArgs } from 'common/types/game_manager'
const buildDir = resolve(__dirname, '../../build')

export function getGameInfo(appName: string): GameInfo {
const store = libraryStore.get('games', [])
Expand Down Expand Up @@ -67,51 +65,6 @@ export function isGameAvailable(appName: string): boolean {
return false
}

if (Object.hasOwn(app, 'on'))
app.on('web-contents-created', (_, contents) => {
// Check for a webview
if (contents.getType() === 'webview') {
contents.setWindowOpenHandler(({ url }) => {
const protocol = new URL(url).protocol
if (['https:', 'http:'].includes(protocol)) {
openNewBrowserGameWindow(url)
}
return { action: 'deny' }
})
}
})

const openNewBrowserGameWindow = async (
browserUrl: string
): Promise<boolean> => {
return new Promise((res) => {
const browserGame = new BrowserWindow({
icon: icon,
webPreferences: {
webviewTag: true,
contextIsolation: true,
nodeIntegration: true,
preload: path.join(__dirname, 'preload.js')
}
})

const url = !app.isPackaged
? 'http://localhost:5173?view=BrowserGame&browserUrl=' +
encodeURIComponent(browserUrl)
: `file://${path.join(
buildDir,
'./index.html?view=BrowserGame&browserUrl=' +
encodeURIComponent(browserUrl)
)}`

browserGame.loadURL(url)
setTimeout(() => browserGame.focus(), 200)
browserGame.on('close', () => {
res(true)
})
})
}

export async function launch(
appName: string,
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
Expand Down
33 changes: 16 additions & 17 deletions src/backend/storeManagers/storeManagerCommon/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { GameInfo, GameSettings, Runner } from 'common/types'
import { GameConfig } from '../../game_config'
import { isMac, isLinux, gamesConfigPath, icon } from '../../constants'
import { logInfo, LogPrefix, logWarning } from '../../logger/logger'
import path, { dirname, join, resolve } from 'path'
import { dirname, join } from 'path'
import { appendFileSync, constants as FS_CONSTANTS } from 'graceful-fs'
import i18next from 'i18next'
import {
Expand All @@ -17,9 +17,8 @@ import { access, chmod } from 'fs/promises'
import shlex from 'shlex'
import { showDialogBoxModalAuto } from '../../dialog/dialog'
import { createAbortController } from '../../utils/aborthandler/aborthandler'
import { app, BrowserWindow } from 'electron'
import { BrowserWindow } from 'electron'
import { gameManagerMap } from '../index'
const buildDir = resolve(__dirname, '../../build')

async function getAppSettings(appName: string): Promise<GameSettings> {
return (
Expand All @@ -33,30 +32,30 @@ export function logFileLocation(appName: string) {
}

const openNewBrowserGameWindow = async (
browserUrl: string
browserUrl: string,
abortController: AbortController
): Promise<boolean> => {
const mainUrlName = browserUrl.split('://')[1].split('/')[0]
flavioislima marked this conversation as resolved.
Show resolved Hide resolved

return new Promise((res) => {
const browserGame = new BrowserWindow({
icon: icon,
fullscreen: true,
webPreferences: {
partition: `persist:${mainUrlName}`,
webviewTag: true,
contextIsolation: true,
nodeIntegration: true,
preload: path.join(__dirname, 'preload.js')
nodeIntegration: true
}
})

const url = !app.isPackaged
? 'http://localhost:5173?view=BrowserGame&browserUrl=' +
encodeURIComponent(browserUrl)
: `file://${path.join(
buildDir,
'./index.html?view=BrowserGame&browserUrl=' +
encodeURIComponent(browserUrl)
)}`

browserGame.loadURL(url)
browserGame.loadURL(browserUrl)
setTimeout(() => browserGame.focus(), 200)
flavioislima marked this conversation as resolved.
Show resolved Hide resolved

abortController.signal.addEventListener('abort', () => {
browserGame.close()
})

browserGame.on('close', () => {
res(true)
})
Expand Down Expand Up @@ -87,7 +86,7 @@ export async function launchGame(
}

if (browserUrl) {
return openNewBrowserGameWindow(browserUrl)
return openNewBrowserGameWindow(browserUrl, createAbortController(appName))
}

const gameSettings = await getAppSettings(appName)
Expand Down
14 changes: 14 additions & 0 deletions src/frontend/components/UI/PlatformFilter/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { faApple, faLinux, faWindows } from '@fortawesome/free-brands-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import ContextProvider from 'frontend/state/ContextProvider'
import './index.css'
import { faGlobe } from '@fortawesome/free-solid-svg-icons'

export default function PlatformFilter() {
const { t } = useTranslation()
Expand Down Expand Up @@ -77,6 +78,19 @@ export default function PlatformFilter() {
/>
</button>
)}
<button
onClick={() => handlePlatformFilter('browser')}
className={cx('FormControl__button', {
active: filterPlatform === 'browser'
})}
title={`${t('header.platform')}: ${t('platforms.browser')}`}
>
<FontAwesomeIcon
className="FormControl__segmentedFaIcon"
icon={faGlobe}
tabIndex={-1}
/>
</button>
</FormControl>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default function SideloadDialog({
t('sideload.field.title', 'Title')
)
const [selectedExe, setSelectedExe] = useState('')
const [gameUrl, setGameUrl] = useState('')
const [imageUrl, setImageUrl] = useState('')
const [searching, setSearching] = useState(false)
const [app_name, setApp_name] = useState(appName ?? '')
Expand All @@ -77,13 +78,18 @@ export default function SideloadDialog({
art_cover,
art_square,
install: { executable, platform },
title
title,
browserUrl
} = info

if (executable && platform) {
setSelectedExe(executable)
}

if (browserUrl) {
setGameUrl(browserUrl)
}

setTitle(title)
setImageUrl(art_cover ? art_cover : art_square)
})
Expand Down Expand Up @@ -150,7 +156,8 @@ export default function SideloadDialog({
art_cover: imageUrl ? imageUrl : fallbackImage,
is_installed: true,
art_square: imageUrl ? imageUrl : fallbackImage,
canRunOffline: true
canRunOffline: true,
browserUrl: gameUrl
})
const gameSettings = await getGameSettings(app_name, 'sideload')
if (!gameSettings) {
Expand Down Expand Up @@ -231,14 +238,34 @@ export default function SideloadDialog({
return
}

const platformIcon = availablePlatforms.filter(
(p) => p.value === platformToInstall
)[0]?.icon
function handleGameUrl(url: string) {
if (!url.startsWith('https://')) {
return setGameUrl(`https://${url}`)
}

setGameUrl(url)
}

function platformIcon() {
const platformIcon = availablePlatforms.filter(
(p) => p.name === platformToInstall
)[0]?.icon

return (
<FontAwesomeIcon
className="InstallModal__platformIcon"
icon={platformIcon}
/>
)
}

const showSideloadExe = platformToInstall !== 'Browser'

const shouldShowRunExe =
platform !== 'win32' &&
platformToInstall !== 'Mac' &&
platformToInstall !== 'linux'
platformToInstall !== 'linux' &&
platformToInstall !== 'Browser'

return (
<>
Expand All @@ -251,10 +278,7 @@ export default function SideloadDialog({
/>
<span className="titleIcon">
{title}
<FontAwesomeIcon
className="InstallModal__platformIcon"
icon={platformIcon}
/>
{platformIcon()}
</span>
</div>
<div className="sideloadForm">
Expand All @@ -281,18 +305,32 @@ export default function SideloadDialog({
value={imageUrl}
/>
{!editMode && children}
<PathSelectionBox
type="file"
onPathChange={setSelectedExe}
path={selectedExe}
placeholder={t('sideload.info.exe', 'Select Executable')}
pathDialogTitle={t('box.sideload.exe', 'Select Executable')}
pathDialogDefaultPath={winePrefix}
pathDialogFilters={fileFilters[platformToInstall]}
htmlId="sideload-exe"
label={t('sideload.info.exe', 'Select Executable')}
noDeleteButton
/>
{showSideloadExe && (
<PathSelectionBox
type="file"
onPathChange={setSelectedExe}
path={selectedExe}
placeholder={t('sideload.info.exe', 'Select Executable')}
pathDialogTitle={t('box.sideload.exe', 'Select Executable')}
pathDialogDefaultPath={winePrefix}
pathDialogFilters={fileFilters[platformToInstall]}
htmlId="sideload-exe"
label={t('sideload.info.exe', 'Select Executable')}
noDeleteButton
/>
)}
{!showSideloadExe && (
<TextInputField
label={t('sideload.info.broser', 'BrowserURL')}
placeholder={t(
'sideload.placeholder.url',
'Paste the Game URL here'
)}
onChange={(e) => handleGameUrl(e.target.value)}
htmlId="sideload-game-url"
value={gameUrl}
/>
)}
</div>
</div>
</DialogContent>
Expand All @@ -311,7 +349,7 @@ export default function SideloadDialog({
<button
onClick={async () => handleInstall()}
className={`button is-success`}
disabled={!selectedExe.length || addingApp || searching}
disabled={(!selectedExe.length && !gameUrl) || addingApp || searching}
>
{addingApp && <FontAwesomeIcon icon={faSpinner} spin />}
{!addingApp && t('button.finish', 'Finish')}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { faApple, faLinux, faWindows } from '@fortawesome/free-brands-svg-icons'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { IconDefinition, faGlobe } from '@fortawesome/free-solid-svg-icons'

import React, { useContext, useEffect, useState } from 'react'

Expand Down Expand Up @@ -74,6 +74,12 @@ export default React.memo(function InstallModal({
available: true,
value: 'Windows',
icon: faWindows
},
{
name: 'Browser',
available: true,
value: 'Browser',
icon: faGlobe
}
]

Expand Down
4 changes: 4 additions & 0 deletions src/frontend/screens/Library/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ export default React.memo(function Library(): JSX.Element {
? game?.install?.platform === 'linux'
: game?.is_linux_native
})
case 'browser':
return library.filter((game) => {
return game?.install?.platform === 'Browser'
})
default:
return library
}
Expand Down