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) */