diff options
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/main/linux-split-tunneling.ts | 23 | ||||
| -rw-r--r-- | gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx | 210 | ||||
| -rw-r--r-- | gui/src/renderer/components/Modal.tsx | 5 | ||||
| -rw-r--r-- | gui/src/shared/linux-split-tunneling-application.ts | 3 |
4 files changed, 174 insertions, 67 deletions
diff --git a/gui/src/main/linux-split-tunneling.ts b/gui/src/main/linux-split-tunneling.ts index f2ea40da13..5114829d1f 100644 --- a/gui/src/main/linux-split-tunneling.ts +++ b/gui/src/main/linux-split-tunneling.ts @@ -7,6 +7,11 @@ import path from 'path'; import ISplitTunnelingApplication from '../shared/linux-split-tunneling-application'; import { pascalCaseToCamelCase } from './transform-object-keys'; +const PROBLEMATIC_APPLICATIONS = { + launchingInExistingProcess: ['firefox', 'firefox-esr'], + launchingElsewhere: ['gnome-terminal'], +}; + type DirectoryDescription = string | RegExp; interface IApplication extends ISplitTunnelingApplication { @@ -39,6 +44,7 @@ export function getApplications(locale: string): Promise<ISplitTunnelingApplicat return pascalCaseToCamelCase<IApplication>(applications); }) .filter(shouldShowApplication) + .map(addApplicationWarnings) .sort((a, b) => a.name.localeCompare(b.name)) .map(async (app) => ({ ...app, icon: await replaceWithIconPath(app.icon) })); @@ -77,6 +83,23 @@ function shouldShowApplication(application: IApplication): boolean { ); } +function addApplicationWarnings(application: IApplication): IApplication { + const binaryBasename = path.basename(application.exec!.split(' ')[0]); + if (PROBLEMATIC_APPLICATIONS.launchingInExistingProcess.includes(binaryBasename)) { + return { + ...application, + warning: 'launches-in-existing-process', + }; + } else if (PROBLEMATIC_APPLICATIONS.launchingElsewhere.includes(binaryBasename)) { + return { + ...application, + warning: 'launches-elsewhere', + }; + } else { + return application; + } +} + function localizeNameAndIcon(application: AppData, locale: string) { if (application.lang) { // linux-app-list prefixes and suffixes the locale keys with a space for some reason. diff --git a/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx b/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx index 81991e83b4..b018221932 100644 --- a/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx +++ b/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx @@ -1,6 +1,7 @@ import { remote } from 'electron'; import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { useHistory } from 'react-router'; +import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; import { colors } from '../../config.json'; import { messages } from '../../shared/gettext'; @@ -11,6 +12,7 @@ import * as AppButton from './AppButton'; import * as Cell from './Cell'; import ImageView from './ImageView'; import { Container, Layout } from './Layout'; +import { ModalContainer, ModalAlert, ModalAlertType } from './Modal'; import { BackBarItem, NavigationBar, @@ -47,10 +49,24 @@ const StyledContent = styled.div({ flex: 1, }); -const StyledIcon = styled(Cell.UntintedIcon)({ +const StyledCellButton = styled(Cell.CellButton)((props: { lookDisabled: boolean }) => ({ + ':not(:disabled):hover': { + backgroundColor: props.lookDisabled ? colors.blue : undefined, + }, +})); + +const disabledApplication = (props: { lookDisabled: boolean }) => ({ + opacity: props.lookDisabled ? 0.6 : undefined, +}); + +const StyledIcon = styled(Cell.UntintedIcon)(disabledApplication, { marginRight: '12px', }); +const StyledCellLabel = styled(Cell.Label)(disabledApplication, { + fontWeight: 'normal', +}); + const StyledIconPlaceholder = styled.div({ width: '35px', marginRight: '12px', @@ -114,66 +130,68 @@ export default function LinuxSplitTunnelingSettings() { return ( <> <StyledPageCover show={browsing} /> - <Layout> - <StyledContainer> - <NavigationContainer> - <NavigationBar> - <NavigationItems> - <BackBarItem action={history.goBack}> - { - // TRANSLATORS: Back button in navigation bar - messages.pgettext('navigation-bar', 'Advanced') - } - </BackBarItem> - <TitleBarItem> - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('split-tunneling-nav', 'Split tunneling') - } - </TitleBarItem> - </NavigationItems> - </NavigationBar> + <ModalContainer> + <Layout> + <StyledContainer> + <NavigationContainer> + <NavigationBar> + <NavigationItems> + <BackBarItem action={history.goBack}> + { + // TRANSLATORS: Back button in navigation bar + messages.pgettext('navigation-bar', 'Advanced') + } + </BackBarItem> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('split-tunneling-nav', 'Split tunneling') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> - <StyledNavigationScrollbars> - <StyledContent> - <SettingsHeader> - <HeaderTitle> - {messages.pgettext('split-tunneling-view', 'Split tunneling')} - </HeaderTitle> - <HeaderSubTitle> - {messages.pgettext( - 'split-tunneling-view', - 'Click on an app to launch it. Its traffic will bypass the VPN tunnel until you close it.', - )} - </HeaderSubTitle> - </SettingsHeader> + <StyledNavigationScrollbars> + <StyledContent> + <SettingsHeader> + <HeaderTitle> + {messages.pgettext('split-tunneling-view', 'Split tunneling')} + </HeaderTitle> + <HeaderSubTitle> + {messages.pgettext( + 'split-tunneling-view', + 'Click on an app to launch it. Its traffic will bypass the VPN tunnel until you close it.', + )} + </HeaderSubTitle> + </SettingsHeader> - <StyledApplicationListAnimation height={applicationListHeight}> - <StyledApplicationListContent ref={applicationListRef}> - {applications === undefined ? ( - <StyledSpinnerRow> - <ImageView source="icon-spinner" height={60} width={60} /> - </StyledSpinnerRow> - ) : ( - applications.map((application) => ( - <ApplicationRow - key={application.absolutepath} - application={application} - launchApplication={launchExcludedApplication} - /> - )) - )} - </StyledApplicationListContent> - </StyledApplicationListAnimation> + <StyledApplicationListAnimation height={applicationListHeight}> + <StyledApplicationListContent ref={applicationListRef}> + {applications === undefined ? ( + <StyledSpinnerRow> + <ImageView source="icon-spinner" height={60} width={60} /> + </StyledSpinnerRow> + ) : ( + applications.map((application) => ( + <ApplicationRow + key={application.absolutepath} + application={application} + launchApplication={launchExcludedApplication} + /> + )) + )} + </StyledApplicationListContent> + </StyledApplicationListAnimation> - <StyledBrowseButton onClick={launchWithFilePicker}> - {messages.pgettext('split-tunneling-view', 'Browse')} - </StyledBrowseButton> - </StyledContent> - </StyledNavigationScrollbars> - </NavigationContainer> - </StyledContainer> - </Layout> + <StyledBrowseButton onClick={launchWithFilePicker}> + {messages.pgettext('split-tunneling-view', 'Browse')} + </StyledBrowseButton> + </StyledContent> + </StyledNavigationScrollbars> + </NavigationContainer> + </StyledContainer> + </Layout> + </ModalContainer> </> ); } @@ -184,18 +202,78 @@ interface IApplicationRowProps { } function ApplicationRow(props: IApplicationRowProps) { - const onClick = useCallback(() => { + const [showWarning, setShowWarning] = useState(false); + + const launch = useCallback(() => { + setShowWarning(false); props.launchApplication(props.application); }, [props.launchApplication, props.application]); + const showWarningDialog = useCallback(() => setShowWarning(true), []); + const hideWarningDialog = useCallback(() => setShowWarning(false), []); + + const disabled = props.application.warning === 'launches-elsewhere'; + const warningColor = disabled ? colors.red : colors.yellow; + const warningMessage = disabled + ? sprintf( + messages.pgettext( + 'split-tunneling-view', + '%(applicationName)s is problematic and cannot be excluded from the VPN tunnel.', + ), + { + applicationName: props.application.name, + }, + ) + : sprintf( + messages.pgettext( + 'split-tunneling-view', + '%(applicationName)s is problematic and might not be excluded from the VPN tunnel. Try closing all existing instances of %(applicationName)s before starting it from here.', + ), + { + applicationName: props.application.name, + }, + ); + const warningDialogButtons = disabled + ? [ + <AppButton.BlueButton key="cancel" onClick={hideWarningDialog}> + {messages.gettext('Back')} + </AppButton.BlueButton>, + ] + : [ + <AppButton.BlueButton key="launch" onClick={launch}> + {messages.pgettext('split-tunneling-view', 'Launch')} + </AppButton.BlueButton>, + <AppButton.BlueButton key="cancel" onClick={hideWarningDialog}> + {messages.gettext('Cancel')} + </AppButton.BlueButton>, + ]; + return ( - <Cell.CellButton onClick={onClick}> - {props.application.icon ? ( - <StyledIcon source={props.application.icon} width={35} height={35} /> - ) : ( - <StyledIconPlaceholder /> + <> + <StyledCellButton + onClick={props.application.warning ? showWarningDialog : launch} + lookDisabled={disabled}> + {props.application.icon ? ( + <StyledIcon + source={props.application.icon} + width={35} + height={35} + lookDisabled={disabled} + /> + ) : ( + <StyledIconPlaceholder /> + )} + <StyledCellLabel lookDisabled={disabled}>{props.application.name}</StyledCellLabel> + {props.application.warning && <Cell.Icon source="icon-alert" tintColor={warningColor} />} + </StyledCellButton> + {showWarning && ( + <ModalAlert + type={ModalAlertType.Warning} + iconColor={warningColor} + message={warningMessage} + buttons={warningDialogButtons} + /> )} - <Cell.Label>{props.application.name}</Cell.Label> - </Cell.CellButton> + </> ); } diff --git a/gui/src/renderer/components/Modal.tsx b/gui/src/renderer/components/Modal.tsx index ae46c414fc..3db4dcd626 100644 --- a/gui/src/renderer/components/Modal.tsx +++ b/gui/src/renderer/components/Modal.tsx @@ -82,6 +82,7 @@ const ModalAlertButtonContainer = styled.div({ interface IModalAlertProps { type?: ModalAlertType; + iconColor?: string; message?: string; buttons: React.ReactNode[]; children?: React.ReactNode; @@ -152,7 +153,9 @@ export class ModalAlert extends React.Component<IModalAlertProps> { color = colors.red; break; } - return <ImageView height={44} width={44} source={source} tintColor={color} />; + return ( + <ImageView height={44} width={44} source={source} tintColor={this.props.iconColor ?? color} /> + ); } } diff --git a/gui/src/shared/linux-split-tunneling-application.ts b/gui/src/shared/linux-split-tunneling-application.ts index f75406b301..a96152b70d 100644 --- a/gui/src/shared/linux-split-tunneling-application.ts +++ b/gui/src/shared/linux-split-tunneling-application.ts @@ -1,6 +1,9 @@ +type Warning = 'launches-in-existing-process' | 'launches-elsewhere'; + export default interface ISplitTunnelingApplication { absolutepath: string; name: string; exec: string; icon?: string; + warning?: Warning; } |
