diff --git a/lang/en.json b/lang/en.json index 312978928..d2770e60e 100644 --- a/lang/en.json +++ b/lang/en.json @@ -78,6 +78,8 @@ "optimizeDatabaseDesc": "Run maintenance tasks to increase database performance and reduce size", "showDeveloperTab": "Show Developer Tab", "showDeveloperTabDesc": "Show the 'Developer' tab. This is most likely only useful for developers and curators.", + "registerProtocol": "Register As Protocol Handler", + "registerProtocolDesc": "Registers the launcher to respond to 'flashpoint://' protocol requests.", "server": "Server", "serverDesc": "Which Server to run when playing games.", "curateServer": "Curate Server", diff --git a/src/main/Main.ts b/src/main/Main.ts index a81adc7cd..5abad7e85 100644 --- a/src/main/Main.ts +++ b/src/main/Main.ts @@ -93,15 +93,6 @@ export function main(init: Init): void { // -- Functions -- async function startup(opts: LaunchOptions) { - // Register flashpoint:// protocol - if (process.defaultApp) { - if (process.argv.length >= 2) { - app.setAsDefaultProtocolClient('flashpoint', process.execPath, [path.resolve(process.argv[1])]); - } - } else { - app.setAsDefaultProtocolClient('flashpoint'); - } - app.disableHardwareAcceleration(); // Single process @@ -131,6 +122,9 @@ export function main(init: Init): void { ipcMain.handle(CustomIPC.SHOW_SAVE_DIALOG, async (event, opts) => { return dialog.showSaveDialog(opts); }); + ipcMain.handle(CustomIPC.REGISTER_PROTOCOL, async (event, register) => { + return setProtocolRegistrationState(register); + }); // Add Socket event listener(s) state.socket.register(BackOut.QUIT, () => { @@ -250,6 +244,11 @@ export function main(init: Init): void { version: app.getVersion(), // @TODO Manually load this from the package.json file while in a dev environment (so it doesn't use Electron's version) }; state.backProc.send(JSON.stringify(msg)); + }) + .then(() => { + if (!state.preferences) { throw new Error('Preferences not loaded by backend.'); } + // Update flashpoint:// protocol registration state + setProtocolRegistrationState(state.preferences.registerProtocol); }); } @@ -512,6 +511,59 @@ export function main(init: Init): void { }); } + function setProtocolRegistrationState(registered: boolean) : boolean { + // Check how the Launcher was started + const procDefault = process.defaultApp; + const procArgCount = process.argv.length; + + if (procDefault && procArgCount < 2) { + return true; // Don't need to change + } + + /* + * The return value of app.removeAsDefaultProtocolClient() is really inconsistent + * between platforms, so we wrap it to consistently match: + * + * true - The app was set as the default handler for 'protocol' and was successfully + * removed, or was not the default handler in the first place. + * false - The app was set as the default handler for 'protocol' and there was an + * issue removing it. + */ + function normalizedRemove(protocol: string, path: string, args: string[]) : boolean { + const needsRemove = app.isDefaultProtocolClient(protocol, path, args); + if (!needsRemove || process.platform === 'linux') { + /* + * Electron has not implemented app.removeAsDefaultProtocolClient() on Linux so + * it always fails; however, for our purposes we return true as we've done all + * we can and nothing unexpected has occurred. + * + * https://github.com/electron/electron/blob/a867503af63bcf24f935ae32fc8d88fe5e7a786a/shell/browser/browser_linux.cc#L118 + */ + return true; + } + + return app.removeAsDefaultProtocolClient(protocol, path, args); + } + + // Add/remove protocol registration + type ProtocolFunction = (protocol: string, path: string, args: string[]) => boolean; + + const func: ProtocolFunction = registered ? app.setAsDefaultProtocolClient : normalizedRemove; + const verb = registered ? 'set' : 'unset'; + const scheme = 'flashpoint'; + const pPath = process.execPath; + const pArgs = procDefault ? [path.resolve(process.argv[1])] : []; + + const res = func(scheme, pPath, pArgs); + if (res) { + console.log('Successfully ' + verb + ' app as protocol handler.'); + } else { + console.warn('Could not ' + verb + ' app as protocol handler.'); + } + + return res; + } + function noop() { /* Do nothing. */ } } diff --git a/src/renderer/components/pages/ConfigPage.tsx b/src/renderer/components/pages/ConfigPage.tsx index 39e0df581..9e1a1970b 100644 --- a/src/renderer/components/pages/ConfigPage.tsx +++ b/src/renderer/components/pages/ConfigPage.tsx @@ -2,6 +2,8 @@ import { WithPreferencesProps } from '@renderer/containers/withPreferences'; import { WithTagCategoriesProps } from '@renderer/containers/withTagCategories'; import { BackIn } from '@shared/back/types'; import { AppExtConfigData } from '@shared/config/interfaces'; +import { CustomIPC } from '@shared/interfaces'; +import { ipcRenderer } from 'electron'; import { ExtConfigurationProp, ExtensionContribution, IExtensionDescription, ILogoSet } from '@shared/extensions/interfaces'; import { autoCode, LangContainer, LangFile } from '@shared/lang'; import { memoizeOne } from '@shared/memoize'; @@ -336,6 +338,12 @@ export class ConfigPage extends React.Component + {/* Register As Protocol Handler */} + {/* Server */} { + updatePreferencesData({ registerProtocol: isChecked }); + ipcRenderer.invoke(CustomIPC.REGISTER_PROTOCOL, isChecked) + .then((success) => { + if (!success) { + const regVerb = isChecked ? 'add' : 'remove'; + alert('Failed to ' + regVerb + ' protocol registration'); + } + }); + }; + onCurrentThemeChange = (value: string): void => { const selectedTheme = this.props.themeList.find(t => t.id === value); if (selectedTheme) { diff --git a/src/shared/interfaces.ts b/src/shared/interfaces.ts index f07530413..763b25887 100644 --- a/src/shared/interfaces.ts +++ b/src/shared/interfaces.ts @@ -153,7 +153,8 @@ export enum WindowIPC { export enum CustomIPC { SHOW_MESSAGE_BOX = 'show-message-box', SHOW_SAVE_DIALOG = 'show-save-dialog', - SHOW_OPEN_DIALOG = 'show-open-dialog' + SHOW_OPEN_DIALOG = 'show-open-dialog', + REGISTER_PROTOCOL = 'register-protocol' } /** IPC channels used to relay game manager events from */ diff --git a/src/shared/lang.ts b/src/shared/lang.ts index 1b42131b4..7ccbadcbe 100644 --- a/src/shared/lang.ts +++ b/src/shared/lang.ts @@ -84,6 +84,8 @@ const langTemplate = { 'optimizeDatabaseDesc', 'showDeveloperTab', 'showDeveloperTabDesc', + 'registerProtocol', + 'registerProtocolDesc', 'server', 'serverDesc', 'curateServer', diff --git a/src/shared/preferences/util.ts b/src/shared/preferences/util.ts index 0cf7b42b0..cbb48a2d2 100644 --- a/src/shared/preferences/util.ts +++ b/src/shared/preferences/util.ts @@ -54,6 +54,7 @@ const { num, str } = Coerce; /** Default Preferences Data used for values that are not found in the file */ export const defaultPreferencesData: Readonly = Object.freeze({ + registerProtocol: true, imageFolderPath: 'Data/Images', logoFolderPath: 'Data/Logos', playlistFolderPath: 'Data/Playlists', @@ -166,6 +167,7 @@ export function overwritePreferenceData( onError: onError && (e => onError(`Error while parsing Preferences: ${e.toString()}`)), }); // Parse root object + parser.prop('registerProtocol', v => source.registerProtocol = !!v, true); parser.prop('imageFolderPath', v => source.imageFolderPath = parseVarStr(str(v)), true); parser.prop('logoFolderPath', v => source.logoFolderPath = parseVarStr(str(v)), true); parser.prop('playlistFolderPath', v => source.playlistFolderPath = parseVarStr(str(v)), true); diff --git a/tests/unit/back/configuration.test.ts b/tests/unit/back/configuration.test.ts index 2a0ab37a0..fbc9ef763 100644 --- a/tests/unit/back/configuration.test.ts +++ b/tests/unit/back/configuration.test.ts @@ -42,6 +42,7 @@ describe('Configuration Files', () => { it('overwrite preferences data', () => { const data: AppPreferencesData = { 'onDemandImagesCompressed': false, + 'registerProtocol': true, 'imageFolderPath': 'test/Images', 'logoFolderPath': 'test/Logos', 'playlistFolderPath': 'test/Playlists', diff --git a/typings/flashpoint-launcher.d.ts b/typings/flashpoint-launcher.d.ts index 5e948d788..0cd74ab9f 100644 --- a/typings/flashpoint-launcher.d.ts +++ b/typings/flashpoint-launcher.d.ts @@ -991,6 +991,8 @@ declare module 'flashpoint-launcher' { */ type AppPreferencesData = { [key: string]: any; // TODO: Remove this! + /** If the launcher should register itself as the default handler for 'flashpoint://' requests. */ + registerProtocol: boolean; /** Path to the image folder (relative to the flashpoint path) */ imageFolderPath: string; /** Path to the logo folder (relative to the flashpoint path) */