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: Make protocol registration a configurable user preference #411

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
70 changes: 61 additions & 9 deletions src/main/Main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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, () => {
Expand Down Expand Up @@ -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);
});
}

Expand Down Expand Up @@ -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])] : [];
colin969 marked this conversation as resolved.
Show resolved Hide resolved

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. */ }
}

Expand Down
19 changes: 19 additions & 0 deletions src/renderer/components/pages/ConfigPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -336,6 +338,12 @@ export class ConfigPage extends React.Component<ConfigPageProps, ConfigPageState
description={strings.showDeveloperTabDesc}
checked={this.props.preferencesData.showDeveloperTab}
onToggle={this.onShowDeveloperTab} />
{/* Register As Protocol Handler */}
<ConfigBoxCheckbox
title={strings.registerProtocol}
description={strings.registerProtocolDesc}
checked={this.props.preferencesData.registerProtocol}
onToggle={this.onRegisterProtocol} />
{/* Server */}
<ConfigBoxSelect
title={strings.server}
Expand Down Expand Up @@ -987,6 +995,17 @@ export class ConfigPage extends React.Component<ConfigPageProps, ConfigPageState
updatePreferencesData({ showDeveloperTab: isChecked });
};

onRegisterProtocol = (isChecked: boolean): void => {
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) {
Expand Down
3 changes: 2 additions & 1 deletion src/shared/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
2 changes: 2 additions & 0 deletions src/shared/lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ const langTemplate = {
'optimizeDatabaseDesc',
'showDeveloperTab',
'showDeveloperTabDesc',
'registerProtocol',
'registerProtocolDesc',
'server',
'serverDesc',
'curateServer',
Expand Down
2 changes: 2 additions & 0 deletions src/shared/preferences/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppPreferencesData> = Object.freeze<AppPreferencesData>({
registerProtocol: true,
imageFolderPath: 'Data/Images',
logoFolderPath: 'Data/Logos',
playlistFolderPath: 'Data/Playlists',
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions tests/unit/back/configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions typings/flashpoint-launcher.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand Down