Skip to content

Commit

Permalink
Allow plugins to overwrite default file format (wulkano#700)
Browse files Browse the repository at this point in the history
  • Loading branch information
karaggeorge authored and sindresorhus committed Aug 8, 2019
1 parent 44d9a0d commit accb695
Show file tree
Hide file tree
Showing 12 changed files with 133 additions and 45 deletions.
2 changes: 2 additions & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Tip: You can use modern JavaScript features like async/await in your plugin.
- Use `context.setProgress()` whenever possible to keep the user updated on what's happening. The `.filePath()` method sets its own progress, so you should not do it for that step.
- The readme should follow the style of [`kap-giphy`](https://github.com/wulkano/kap-giphy).
- Your plugin must be tested, preferably using [`kap-plugin-test`](https://github.com/SamVerschueren/kap-plugin-test) and [`kap-plugin-mock-context`](https://github.com/samverschueren/kap-plugin-mock-context). [Example](https://github.com/wulkano/kap-giphy/blob/master/test/test.js).
- If your plugin only supports specific versions of Kap, include a `kapVersion` field in the package.json with a [semver range](https://nodesource.com/blog/semver-a-primer/).

## Development

Expand Down Expand Up @@ -84,6 +85,7 @@ The `action` function is where you implement the behavior of your service. The f
- `.prettyFormat`: Prettified version of `.format` for use in notifications. Can be: `GIF`, `MP4`, `WebM`, `APNG`
- `.defaultFileName`: Default file name for the recording. For example: `Kapture 2017-05-30 at 1.03.49.gif`
- `.filePath()`: Convert the screen recording to the user chosen format and return a Promise for the file path.
- If you want to overwrite the format that the user selected, you can pass a `fileType` option: `.filePath({fileType: 'mp4'})`. Can be one of `mp4`, `gif`, `apng`, `webm`. This can be useful if you, for example, need to handle the GIF conversion yourself.
- `.config`: Get and set config for you plugin. It’s an instance of [`electron-store`](https://github.com/sindresorhus/electron-store#instance).
- `.request()`: Do a network request, like uploading. It’s a wrapper around [`got`](https://github.com/sindresorhus/got).
- `.copyToClipboard(text)`: Copy text to the clipboard. If you for example copy a link to the uploaded recording to the clipboard, don’t forget to `.notify()` the user about it.
Expand Down
38 changes: 29 additions & 9 deletions main/common/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ const got = require('got');
const execa = require('execa');
const makeDir = require('make-dir');
const {ipcMain: ipc} = require('electron-better-ipc');
const packageJson = require('package-json');
const {satisfies} = require('semver');

const {app, Notification} = electron;

const Plugin = require('../plugin');
const {updateExportOptions} = require('../export-options');
const {openConfigWindow} = require('../config');
const {openPrefsWindow} = require('../preferences');
const {notify} = require('./notifications');
Expand All @@ -19,6 +20,11 @@ class Plugins {
constructor() {
this.npmBin = path.join(__dirname, '../../node_modules/npm/bin/npm-cli.js');
this._makePluginsDir();
this.appVersion = app.getVersion();
}

setUpdateExportOptions(updateExportOptions) {
this.updateExportOptions = updateExportOptions;
}

_makePluginsDir() {
Expand Down Expand Up @@ -110,7 +116,7 @@ class Plugins {
}

notification.show();
updateExportOptions();
this.updateExportOptions();

return {hasConfig, isValid};
} catch (error) {
Expand All @@ -133,7 +139,7 @@ class Plugins {
});
const plugin = new Plugin(name);
plugin.config.clear();
updateExportOptions();
this.updateExportOptions();
}

async prune() {
Expand All @@ -149,10 +155,16 @@ class Plugins {
const pluginPath = this._pluginPath(name, 'package.json');
const json = JSON.parse(fs.readFileSync(pluginPath, 'utf8'));
const plugin = new Plugin(name);
json.prettyName = this._getPrettyName(name);
json.hasConfig = this.getServices(name).some(({config = {}}) => Object.keys(config).length > 0);
json.isValid = plugin.isConfigValid();
return json;
return {
...json,
prettyName: this._getPrettyName(name),
hasConfig: this.getServices(name).some(({config = {}}) => Object.keys(config).length > 0),
isValid: plugin.isConfigValid(),
kapVersion: json.kapVersion || '*',
isCompatible: satisfies(this.appVersion, json.kapVersion || '*'),
isInstalled: true,
isSymlink: fs.lstatSync(this._pluginPath(name)).isSymbolicLink()
};
});
}

Expand All @@ -161,11 +173,19 @@ class Plugins {
const response = await got(url, {json: true});
const installed = this._pluginNames();

return response.body.results
return Promise.all(response.body.results
.map(x => x.package)
.filter(x => x.name.startsWith('kap-'))
.filter(x => !installed.includes(x.name)) // Filter out installed plugins
.map(x => ({...x, prettyName: this._getPrettyName(x.name)}));
.map(async x => {
const {kapVersion = '*'} = await packageJson(x.name, {fullMetadata: true});
return {
...x,
kapVersion,
prettyName: this._getPrettyName(x.name),
isCompatible: satisfies(this.appVersion, kapVersion)
};
}));
}

getPluginService(pluginName, serviceTitle) {
Expand Down
5 changes: 5 additions & 0 deletions main/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ const converters = new Map([
const convertTo = (opts, format) => {
const outputPath = path.join(tempy.directory(), opts.defaultFileName);
const converter = converters.get(format);

if (!converter) {
throw new Error(`Unsupported file format: ${format}`);
}

opts.onProgress(0);
track(`file/export/format/${format}`);

Expand Down
32 changes: 16 additions & 16 deletions main/export-options.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@

const path = require('path');
const fs = require('fs');
const electron = require('electron');
const Store = require('electron-store');
const {ipcMain: ipc} = require('electron-better-ipc');
const makeDir = require('make-dir');

const {app} = electron;
const plugins = require('./common/plugins');
const {converters} = require('./convert');
const {setOptions, getEditors} = require('./editor');

Expand All @@ -33,15 +32,7 @@ const prettifyFormat = format => {

const getExportOptions = () => {
const cwd = path.join(app.getPath('userData'), 'plugins');
const packageJsonPath = path.join(cwd, 'package.json');

if (!fs.existsSync(packageJsonPath)) {
makeDir.sync(cwd);
fs.writeFileSync(packageJsonPath, '{"dependencies":{}}');
}

const pkg = fs.readFileSync(packageJsonPath, 'utf8');
const pluginNames = Object.keys(JSON.parse(pkg).dependencies);
const installed = plugins.getInstalled();

const options = [];
for (const format of converters.keys()) {
Expand All @@ -56,11 +47,15 @@ const getExportOptions = () => {
});
}

for (const pluginName of pluginNames) {
const plugin = require(path.join(cwd, 'node_modules', pluginName));
for (const json of installed) {
if (!json.isCompatible) {
continue;
}

const plugin = require(path.join(cwd, 'node_modules', json.name));
for (const service of plugin.shareServices) {
for (const format of service.formats) {
options.find(option => option.format === format).plugins.push({title: service.title, pluginName});
options.find(option => option.format === format).plugins.push({title: service.title, pluginName: json.name});
}
}
}
Expand All @@ -86,6 +81,8 @@ const updateExportOptions = () => {
setOptions(exportOptions);
};

plugins.setUpdateExportOptions(updateExportOptions);

ipc.answerRenderer('update-usage', ({format, plugin}) => {
const usage = exportUsageHistory.get(format);
const now = Date.now();
Expand All @@ -96,9 +93,12 @@ ipc.answerRenderer('update-usage', ({format, plugin}) => {
updateExportOptions();
});

setOptions(getExportOptions());
const initializeExportOptions = () => {
setOptions(getExportOptions());
};

module.exports = {
getExportOptions,
updateExportOptions
updateExportOptions,
initializeExportOptions
};
14 changes: 10 additions & 4 deletions main/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Export {
this.format = options.format;
this.image = '';
this.isDefault = options.isDefault;
this.disableOutputActions = false;

const now = moment();
this.defaultFileName = options.isNewRecording ? `Kapture ${now.format('YYYY-MM-DD')} at ${now.format('H.mm.ss')}.${this.format}` : `${path.parse(this.inputPath).name}.${this.format}`;
Expand All @@ -46,7 +47,8 @@ class Export {
image: this.image,
createdAt: this.createdAt,
filePath: this.filePath && (this.isDefault ? this.context.targetFilePath : this.filePath),
error: this.error
error: this.error,
disableOutputActions: this.disableOutputActions
};
}

Expand Down Expand Up @@ -100,15 +102,19 @@ class Export {
});
}

async convert() {
async convert({fileType}) {
if (fileType) {
this.disableOutputActions = true;
}

this.convertProcess = convertTo(
{
...this.exportOptions,
defaultFileName: this.defaultFileName,
defaultFileName: fileType ? `${path.parse(this.defaultFileName).name}.${fileType}` : this.defaultFileName,
inputPath: this.inputPath,
onProgress: percentage => this.setProgress('Converting…', percentage)
},
this.format
fileType || this.format
);

this.filePath = await this.convertProcess;
Expand Down
14 changes: 9 additions & 5 deletions main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const {track} = require('./common/analytics');
const {initializeGlobalAccelerators} = require('./global-accelerators');
const {setApplicationMenu} = require('./menus');
const openFiles = require('./utils/open-files');
const {initializeExportOptions} = require('./export-options');

require('./utils/sentry');

Expand All @@ -35,11 +36,13 @@ app.on('open-file', (event, path) => {
});

const initializePlugins = async () => {
try {
await plugins.prune();
await plugins.upgrade();
} catch (error) {
console.log(error);
if (!is.development) {
try {
await plugins.prune();
await plugins.upgrade();
} catch (error) {
console.log(error);
}
}
};

Expand Down Expand Up @@ -77,6 +80,7 @@ const checkForUpdates = () => {
initializeTray();
initializeExportList();
initializeGlobalAccelerators();
initializeExportOptions();
setApplicationMenu();

if (filesToOpen.length > 0) {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@
"npm": "^6.10.0",
"p-cancelable": "^2.0.0",
"p-event": "^4.1.0",
"package-json": "^6.5.0",
"pify": "^4.0.1",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-linkify": "^0.2.2",
"semver": "^6.2.0",
"semver": "^6.3.0",
"tempy": "^0.3.0",
"tildify": "^2.0.0",
"tmp": "^0.1.0",
Expand Down
11 changes: 7 additions & 4 deletions renderer/components/exports/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,18 @@ export default class Export extends React.Component {
}

openFile = () => {
const {filePath} = this.props;
if (filePath) {
const {filePath, disableOutputActions} = this.props;
if (filePath && !disableOutputActions) {
electron.remote.shell.showItemInFolder(filePath);
}
}

onDragStart = event => {
const {createdAt} = this.props;
const {createdAt, disableOutputActions} = this.props;
event.preventDefault();
electron.ipcRenderer.send('drag-export', createdAt);
if (!disableOutputActions) {
electron.ipcRenderer.send('drag-export', createdAt);
}
}

render() {
Expand Down Expand Up @@ -153,6 +155,7 @@ export default class Export extends React.Component {

Export.propTypes = {
defaultFileName: PropTypes.string,
disableOutputActions: PropTypes.bool,
status: PropTypes.string,
text: PropTypes.string,
percentage: PropTypes.number,
Expand Down
24 changes: 21 additions & 3 deletions renderer/components/preferences/categories/plugins/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,24 @@ PluginTitle.propTypes = {
const getLink = ({homepage, links}) => homepage || (links && links.homepage);

const Plugin = ({plugin, checked, disabled, onTransitionEnd, onClick, loading, openConfig, tabIndex}) => {
const error = !plugin.isCompatible && (
<div className="invalid" title={`This plugin is not supported by the current Kap version. Requires ${plugin.kapVersion}`}>
<ErrorIcon fill="#ff6059" hoverFill="#ff6059" onClick={openConfig}/>
<style jsx>{`
.invalid {
height: 36px;
padding-right: 16px;
margin-right: 16px;
border-right: 1px solid var(--row-divider-color);
display: flex;
align-items: center;
justify-content: center;
align-self: center;
}
`}</style>
</div>
);

const warning = plugin.hasConfig && !plugin.isValid && (
<div className="invalid" title="This plugin requires configuration">
<ErrorIcon fill="#ff6059" hoverFill="#ff6059" onClick={openConfig}/>
Expand All @@ -64,7 +82,7 @@ const Plugin = ({plugin, checked, disabled, onTransitionEnd, onClick, loading, o
return (
<Item
key={plugin.name}
warning={warning}
warning={error || warning}
id={plugin.name}
title={
<PluginTitle
Expand All @@ -75,7 +93,7 @@ const Plugin = ({plugin, checked, disabled, onTransitionEnd, onClick, loading, o
subtitle={plugin.description}
>
{
openConfig && (
openConfig && plugin.isCompatible && (
<div className="config-icon">
<EditIcon size="18px" tabIndex={tabIndex} onClick={openConfig}/>
<style jsx>{`
Expand All @@ -90,7 +108,7 @@ const Plugin = ({plugin, checked, disabled, onTransitionEnd, onClick, loading, o
<Switch
tabIndex={tabIndex}
checked={checked}
disabled={disabled}
disabled={disabled || (!plugin.isCompatible && !plugin.isInstalled) || plugin.isSymlink}
loading={loading}
onTransitionEnd={onTransitionEnd}
onClick={onClick}/>
Expand Down
2 changes: 1 addition & 1 deletion renderer/components/preferences/item/switch.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Switch extends React.Component {
box-shadow: var(--switch-box-shadow);
}
.switch:focus {
.switch:not(.disabled):focus {
border-color: var(--kap);
}
Expand Down
8 changes: 7 additions & 1 deletion renderer/containers/preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ export default class PreferencesContainer extends Container {
const plugins = await this.plugins.getFromNpm();
this.setState({
npmError: false,
pluginsFromNpm: plugins.sort((a, b) => a.prettyName.localeCompare(b.prettyName))
pluginsFromNpm: plugins.sort((a, b) => {
if (a.isCompatible !== b.isCompatible) {
return b.isCompatible - a.isCompatible;
}

return a.prettyName.localeCompare(b.prettyName);
})
});
} catch (error) {
this.setState({npmError: true});
Expand Down
Loading

0 comments on commit accb695

Please sign in to comment.