From 32893374174606156ea54b3f0b8147dba9989804 Mon Sep 17 00:00:00 2001 From: George Karagkiaouris Date: Tue, 18 Aug 2020 11:14:51 -0400 Subject: [PATCH 01/14] WIP --- .gitignore | 1 + main/common/plugins.js | 13 +- main/common/remote-state.d.ts | 50 ++ main/common/remote-state.js | 108 +++++ main/common/{settings.js => settings.ts} | 48 +- main/config.js | 5 +- main/converters/h264.ts | 191 ++++++++ main/converters/index.ts | 31 ++ main/converters/process.ts | 131 ++++++ main/converters/utils.ts | 67 +++ main/editor.js | 40 +- main/index.js | 3 + main/kap-window.js | 72 +++ main/plugin.js | 16 +- main/remote-states/conversions/conversion.js | 50 ++ main/remote-states/conversions/index.js | 0 main/remote-states/conversions/video.js | 70 +++ main/remote-states/editor-options.js | 168 +++++++ main/remote-states/index.js | 9 + main/service-context.js | 20 +- main/tray.js | 6 +- main/utils/{ajv.js => ajv.ts} | 18 +- main/utils/errors.js | 2 + main/utils/plugin-config.js | 4 +- package.json | 17 +- .../action-bar/controls/advanced.js | 2 - renderer/components/editor/controls/left.js | 61 --- renderer/components/editor/controls/left.tsx | 47 ++ .../components/editor/controls/play-bar.js | 237 ---------- .../components/editor/controls/play-bar.tsx | 436 ++++++++++++++++++ .../components/editor/controls/preview.js | 73 --- .../components/editor/controls/preview.tsx | 137 ++++++ renderer/components/editor/controls/right.js | 65 --- renderer/components/editor/controls/right.tsx | 54 +++ renderer/components/editor/editor-options.tsx | 5 + renderer/components/editor/editor-preview.tsx | 73 +++ .../components/editor/options-container.tsx | 106 +++++ .../editor/options/{index.js => index.tsx} | 26 +- renderer/components/editor/options/left.js | 137 ------ renderer/components/editor/options/left.tsx | 385 ++++++++++++++++ renderer/components/editor/options/right.js | 182 -------- renderer/components/editor/options/right.tsx | 344 ++++++++++++++ renderer/components/editor/options/select.js | 119 ----- renderer/components/editor/options/select.tsx | 128 +++++ renderer/components/editor/options/slider.js | 154 ------- renderer/components/editor/options/slider.tsx | 336 ++++++++++++++ .../editor/video-controls-container.tsx | 110 +++++ .../editor/video-metadata-container.tsx | 49 ++ renderer/components/editor/video-player.js | 67 --- renderer/components/editor/video-player.tsx | 131 ++++++ .../editor/video-time-container.tsx | 79 ++++ renderer/components/editor/video.js | 86 ---- renderer/components/editor/video.tsx | 156 +++++++ renderer/components/keyboard-number-input.js | 4 +- .../{traffic-lights.js => traffic-lights.tsx} | 117 +++-- renderer/hooks/dark-mode.tsx | 16 + renderer/hooks/window-args.tsx | 24 + renderer/next-env.d.ts | 2 + renderer/next.config.js | 16 +- renderer/pages/_app.tsx | 39 ++ renderer/pages/editor.js | 405 ++++++++-------- renderer/pages/editor2.tsx | 74 +++ renderer/tsconfig.json | 30 ++ .../utils/combine-unstated-containers.tsx | 20 + .../_app.js => utils/global-styles.tsx} | 282 ++++++----- renderer/utils/inputs.js | 13 +- renderer/utils/sentry-error-boundary.tsx | 40 ++ tsconfig.json | 22 + yarn.lock | 41 +- 69 files changed, 4412 insertions(+), 1658 deletions(-) create mode 100644 main/common/remote-state.d.ts create mode 100644 main/common/remote-state.js rename main/common/{settings.js => settings.ts} (72%) create mode 100644 main/converters/h264.ts create mode 100644 main/converters/index.ts create mode 100644 main/converters/process.ts create mode 100644 main/converters/utils.ts create mode 100644 main/kap-window.js create mode 100644 main/remote-states/conversions/conversion.js create mode 100644 main/remote-states/conversions/index.js create mode 100644 main/remote-states/conversions/video.js create mode 100644 main/remote-states/editor-options.js create mode 100644 main/remote-states/index.js rename main/utils/{ajv.js => ajv.ts} (57%) delete mode 100644 renderer/components/editor/controls/left.js create mode 100644 renderer/components/editor/controls/left.tsx delete mode 100644 renderer/components/editor/controls/play-bar.js create mode 100644 renderer/components/editor/controls/play-bar.tsx delete mode 100644 renderer/components/editor/controls/preview.js create mode 100644 renderer/components/editor/controls/preview.tsx delete mode 100644 renderer/components/editor/controls/right.js create mode 100644 renderer/components/editor/controls/right.tsx create mode 100644 renderer/components/editor/editor-options.tsx create mode 100644 renderer/components/editor/editor-preview.tsx create mode 100644 renderer/components/editor/options-container.tsx rename renderer/components/editor/options/{index.js => index.tsx} (55%) delete mode 100644 renderer/components/editor/options/left.js create mode 100644 renderer/components/editor/options/left.tsx delete mode 100644 renderer/components/editor/options/right.js create mode 100644 renderer/components/editor/options/right.tsx delete mode 100644 renderer/components/editor/options/select.js create mode 100644 renderer/components/editor/options/select.tsx delete mode 100644 renderer/components/editor/options/slider.js create mode 100644 renderer/components/editor/options/slider.tsx create mode 100644 renderer/components/editor/video-controls-container.tsx create mode 100644 renderer/components/editor/video-metadata-container.tsx delete mode 100644 renderer/components/editor/video-player.js create mode 100644 renderer/components/editor/video-player.tsx create mode 100644 renderer/components/editor/video-time-container.tsx delete mode 100644 renderer/components/editor/video.js create mode 100644 renderer/components/editor/video.tsx rename renderer/components/{traffic-lights.js => traffic-lights.tsx} (51%) create mode 100644 renderer/hooks/dark-mode.tsx create mode 100644 renderer/hooks/window-args.tsx create mode 100644 renderer/next-env.d.ts create mode 100644 renderer/pages/_app.tsx create mode 100644 renderer/pages/editor2.tsx create mode 100644 renderer/tsconfig.json create mode 100644 renderer/utils/combine-unstated-containers.tsx rename renderer/{pages/_app.js => utils/global-styles.tsx} (53%) create mode 100644 renderer/utils/sentry-error-boundary.tsx create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 1bea176d7..d82b0f3a3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules /renderer/.next /app/dist /dist +/dist-js diff --git a/main/common/plugins.js b/main/common/plugins.js index 1b859f9c9..73f856c56 100644 --- a/main/common/plugins.js +++ b/main/common/plugins.js @@ -17,13 +17,15 @@ const {notify} = require('./notifications'); const {track} = require('./analytics'); const {InstalledPlugin, NpmPlugin, recordPluginServiceState} = require('../plugin'); const {showError} = require('../utils/errors'); +const {EventEmitter} = require('events'); // Need to persist the notification, otherwise it is garbage collected and the actions don't trigger // https://github.com/electron/electron/issues/12690 let pluginNotification; -class Plugins { +class Plugins extends EventEmitter { constructor() { + super(); this.yarnBin = path.join(__dirname, '../../node_modules/yarn/bin/yarn.js'); this._makePluginsDir(); this.appVersion = app.getVersion(); @@ -179,6 +181,7 @@ class Plugins { pluginNotification.show(); this.updateExportOptions(); this.refreshRecordPluginServices(); + this.emit('installed', plugin); return plugin; } catch (error) { @@ -210,6 +213,7 @@ class Plugins { } plugin.config.clear(); + this.emit('uninstalled', name); this.updateExportOptions(); return new NpmPlugin(plugin.json, { // Keeping for backwards compatibility @@ -256,15 +260,15 @@ class Plugins { getBuiltIn() { return [{ - pluginPath: './plugins/copy-to-clipboard-plugin', + pluginPath: path.resolve(__dirname, '..', 'plugins', 'copy-to-clipboard-plugin'), isCompatible: true, name: '_copyToClipboard' }, { - pluginPath: './plugins/save-file-plugin', + pluginPath: path.resolve(__dirname, '..', 'plugins', 'save-file-plugin'), isCompatible: true, name: '_saveToDisk' }, { - pluginPath: './plugins/open-with-plugin', + pluginPath: path.resolve(__dirname, '..', 'plugins', 'open-with-plugin'), isCompatible: true, name: '_openWith' }]; @@ -296,6 +300,7 @@ class Plugins { async openPluginConfig(name) { await openConfigWindow(name); const plugin = new InstalledPlugin(name); + this.emit('config-changed', plugin); return plugin.isValid; } } diff --git a/main/common/remote-state.d.ts b/main/common/remote-state.d.ts new file mode 100644 index 000000000..a3c830f5d --- /dev/null +++ b/main/common/remote-state.d.ts @@ -0,0 +1,50 @@ +import {string, number} from 'prop-types'; + +export type FormatName = 'gif' | 'av1' | 'mp4' | 'webm' | 'apng'; + +export interface App { + isDefault: boolean; + icon: string; + url: string; + name: string; +} + +export interface Plugin { + title: string; + pluginName: string; + pluginPath: string; + apps?: App[]; + lastUsed: number +} + +export interface Format { + format: FormatName; + prettyFormat: string; + plugins: Plugin[] + lastUsed: number; +}; + +export interface EditService { + title: string; + pluginName: string; + pluginPath: string; + hasConfig: boolean; +} + +export interface EditorOptionsState { + formats: Format[]; + editServices: EditService[]; + fpsHistory: { + [key: FormatName]: number; + } +} + +export interface EditorOptionsActions { + updatePluginState: (args: {format: FormatName, plugin: string}) => void; + updateFpsUsage: (args: {format: FormatName, fps: number}) => void; +} + +export type UseRemoteStateFunction = (name: Name, initialState?: State) => () => Actions & {state: State, isLoading: boolean}; + +export const useRemoteState: UseRemoteStateFunction<'editor-options', EditorOptionsState, EditorOptionsActions>; + diff --git a/main/common/remote-state.js b/main/common/remote-state.js new file mode 100644 index 000000000..d63d43059 --- /dev/null +++ b/main/common/remote-state.js @@ -0,0 +1,108 @@ +'use strict'; +const getChannelName = (name, action) => `kap-remote-state-${name}-${action}`; + +const getChannelNames = name => ({ + subscribe: getChannelName(name, 'subscribe'), + getState: getChannelName(name, 'get-state'), + callAction: getChannelName(name, 'call-action'), + stateUpdated: getChannelName(name, 'state-updated') +}); + +const useRemoteState = (name, initialState) => { + const {useState, useEffect, useRef} = require('react'); + const {ipcRenderer} = require('electron-better-ipc'); + + const channelNames = getChannelNames(name); + + return id => { + const [state, setState] = useState(initialState); + const [isLoading, setIsLoading] = useState(true); + const actionsRef = useRef({}); + + useEffect(() => { + const cleanup = ipcRenderer.answerMain(channelNames.stateUpdated, setState); + + (async () => { + const actionKeys = await ipcRenderer.callMain(channelNames.subscribe, id); + console.log(actionKeys); + const actions = actionKeys.reduce((acc, key) => ({ + ...acc, + [key]: data => ipcRenderer.callMain(channelNames.callAction, {key, data, id}) + }), {}); + + console.log(actions); + + const getState = async () => { + const newState = await ipcRenderer.callMain(channelNames.getState, id); + setState(newState); + } + + actionsRef.current = { + ...actions, + refreshState: getState + }; + + await getState(); + setIsLoading(false); + })(); + + return cleanup; + }, []); + + console.log(actionsRef.current); + + return { + ...actionsRef.current, + isLoading, + state + }; + }; +} + +const setupRemoteState = (name, callback) => { + const channelNames = getChannelNames(name); + + return async () => { + const {ipcMain} = require('electron-better-ipc'); + + const renderers = new Map(); + + const sendUpdate = (state, id) => { + if (id) { + return ipcMain.callRenderer(renderers.get(id), channelNames.stateUpdated, state); + } + + for (const [windowId, renderer] of renderers.entries()) { + ipcMain.callRenderer(renderer, channelNames.stateUpdated, state || (getState && getState(windowId))); + } + }; + + const {getState, actions = {}} = await callback(sendUpdate); + + ipcMain.answerRenderer(channelNames.subscribe, async (customId, window) => { + const id = customId || window.id; + renderers.set(id, window); + + window.on('closed', () => { + renderers.delete(id); + }); + + return Object.keys(actions); + }); + + ipcMain.answerRenderer(channelNames.getState, async (customId, window) => { + const id = customId || window.id; + return getState(id); + }); + + ipcMain.answerRenderer(channelNames.callAction, async ({key, data, id: customId}, window) => { + const id = customId || window.id; + return actions[key](data, id); + }); + } +} + +module.exports = { + useRemoteState, + setupRemoteState +}; diff --git a/main/common/settings.js b/main/common/settings.ts similarity index 72% rename from main/common/settings.js rename to main/common/settings.ts index aad2c8460..0ef19bf53 100644 --- a/main/common/settings.js +++ b/main/common/settings.ts @@ -1,15 +1,15 @@ 'use strict'; -const {homedir} = require('os'); -const Store = require('electron-store'); +import {homedir} from 'os'; +import Store from 'electron-store'; const {defaultInputDeviceId} = require('./constants'); const {hasMicrophoneAccess} = require('./system-permissions'); const {getAudioDevices, getDefaultInputDevice} = require('../utils/devices'); const shortcutToAccelerator = require('../utils/shortcut-to-accelerator'); -const shortcuts = { - triggerCropper: 'Toggle Kap' +export const shortcuts = { + triggerCropper: 'Toggle Kap', }; const shortcutSchema = { @@ -17,7 +17,31 @@ const shortcutSchema = { default: '' }; -const store = new Store({ +interface Settings { + kapturesDir: string; + allowAnalytics: boolean; + showCursor: boolean; + highlightClicks: boolean; + record60fps: boolean; + loopExports: boolean; + recordKeyboardShortcut: boolean; + recordAudio: boolean; + audioInputDeviceId?: string; + cropperShortcut: { + metaKey: boolean, + altKey: boolean, + ctrlKey: boolean, + shiftKey: boolean, + character: string + }; + lossyCompression: boolean; + enableShortcuts: boolean; + shortcuts: { + [key in keyof typeof shortcuts]: string + } +} + +const store = new Store({ schema: { kapturesDir: { type: 'string', @@ -99,8 +123,7 @@ const store = new Store({ } }); -module.exports = store; -module.exports.shortcuts = shortcuts; +export default store; // TODO: Remove this when we feel like everyone has migrated if (store.has('recordKeyboardShortcut')) { @@ -110,12 +133,13 @@ if (store.has('recordKeyboardShortcut')) { // TODO: Remove this when we feel like everyone has migrated if (store.has('cropperShortcut')) { - store.set('shortcuts.triggerCropper', shortcutToAccelerator(store.get('cropperShortcut'))); + // TODO: Investigate type for dot notation + store.set('shortcuts.triggerCropper' as any, shortcutToAccelerator(store.get('cropperShortcut'))); store.delete('cropperShortcut'); } -store.set('cropper', {}); -store.set('actionBar', {}); +store.set('cropper' as any, {}); +store.set('actionBar' as any, {}); const audioInputDeviceId = store.get('audioInputDeviceId'); @@ -123,13 +147,13 @@ if (hasMicrophoneAccess()) { (async () => { const devices = await getAudioDevices(); - if (!devices.some(device => device.id === audioInputDeviceId)) { + if (!devices.some((device: any) => device.id === audioInputDeviceId)) { store.set('audioInputDeviceId', defaultInputDeviceId); } })(); } -module.exports.getSelectedInputDeviceId = () => { +export const getSelectedInputDeviceId = () => { const audioInputDeviceId = store.get('audioInputDeviceId', defaultInputDeviceId); if (audioInputDeviceId === defaultInputDeviceId) { diff --git a/main/config.js b/main/config.js index eac0ec1a4..5396aa77a 100644 --- a/main/config.js +++ b/main/config.js @@ -6,7 +6,6 @@ const pEvent = require('p-event'); const loadRoute = require('./utils/routes'); const {openPrefsWindow} = require('./preferences'); -const {getEditor} = require('./editor'); const openConfigWindow = async pluginName => { const prefsWindow = await openPrefsWindow(); @@ -65,8 +64,8 @@ const openEditorConfigWindow = async (pluginName, serviceTitle, editorWindow) => await pEvent(configWindow, 'closed'); }; -ipc.answerRenderer('open-edit-config', async ({pluginName, serviceTitle, filePath}) => { - return openEditorConfigWindow(pluginName, serviceTitle, getEditor(filePath)); +ipc.answerRenderer('open-edit-config', async ({pluginName, serviceTitle}, window) => { + return openEditorConfigWindow(pluginName, serviceTitle, window); }); module.exports = { diff --git a/main/converters/h264.ts b/main/converters/h264.ts new file mode 100644 index 000000000..d2b0246b4 --- /dev/null +++ b/main/converters/h264.ts @@ -0,0 +1,191 @@ +import PCancelable from 'p-cancelable'; +import tempy from 'tempy'; +import {compress, convert} from './process'; +import {areDimensionsEven, conditionalArgs, ConvertOptions, makeEven} from './utils'; +import settings from '../common/settings'; +import os from 'os'; + +// `time ffmpeg -i original.mp4 -vf fps=30,scale=480:-1::flags=lanczos,palettegen palette.png` +// `time ffmpeg -i original.mp4 -i palette.png -filter_complex 'fps=30,scale=-1:-1:flags=lanczos[x]; [x][1:v]paletteuse' palette.gif` +const convertToGif = PCancelable.fn(async (options: ConvertOptions, onCancel: PCancelable.OnCancelFunction) => { + const palettePath = tempy.file({extension: 'png'}); + + const paletteProcess = convert(palettePath, {shouldTrack: false}, conditionalArgs( + '-i', options.inputPath, + '-vf', `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''},palettegen`, + { + args: [ + '-ss', options.startTime.toString(), + '-to', options.endTime.toString() + ], + if: options.shouldCrop + }, + palettePath + )); + + onCancel(() => { + paletteProcess.cancel(); + }); + + await paletteProcess; + + const shouldLoop = settings.get('loopExports'); + + const conversionProcess = convert(options.outputPath, { + onProgress: (progress, estimate) => options.onProgress('Converting', progress, estimate), + startTime: options.startTime, + endTime: options.endTime + }, conditionalArgs( + '-i', options.inputPath, + '-i', palettePath, + '-filter_complex', `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''}[x]; [x][1:v]paletteuse`, + '-loop', shouldLoop ? '0' : '-1', // 0 == forever; -1 == no loop + { + args: [ + '-ss', options.startTime.toString(), + '-to', options.endTime.toString() + ], + if: options.shouldCrop + }, + options.outputPath + )); + + onCancel(() => { + conversionProcess.cancel(); + }); + + await conversionProcess; + + const compressProcess = compress(options.outputPath, { + onProgress: (progress, estimate) => options.onProgress('Compressing', progress, estimate), + startTime: options.startTime, + endTime: options.endTime + }, [ + '--batch', + options.outputPath + ]); + + onCancel(() => { + compressProcess.cancel(); + }); + + await compressProcess; + + return options.outputPath; +}); + +const convertToMp4 = (options: ConvertOptions) => convert(options.outputPath, { + onProgress: (progress, estimate) => options.onProgress('Converting', progress, estimate), + startTime: options.startTime, + endTime: options.endTime +}, conditionalArgs( + '-i', options.inputPath, + '-r', options.fps.toString(), + { + args: ['-an'], + if: options.shouldMute + }, + { + args: [ + '-s', `${makeEven(options.width)}x${makeEven(options.height)}`, + '-ss', options.startTime.toString(), + '-to', options.endTime.toString() + ], + if: options.shouldCrop || !areDimensionsEven(options) + }, + options.outputPath +)); + +const convertToWebm = (options: ConvertOptions) => convert(options.outputPath, { + onProgress: (progress, estimate) => options.onProgress('Converting', progress, estimate), + startTime: options.startTime, + endTime: options.endTime +}, conditionalArgs( + '-i', options.inputPath, + // http://wiki.webmproject.org/ffmpeg + // https://trac.ffmpeg.org/wiki/Encode/VP9 + '-threads', Math.max(os.cpus().length - 1, 1).toString(), + '-deadline', 'good', // `best` is twice as slow and only slighty better + '-b:v', '1M', // Bitrate (same as the MP4) + '-codec:v', 'vp9', + '-codec:a', 'vorbis', + '-ac', '2', // https://stackoverflow.com/questions/19004762/ffmpeg-covert-from-mp4-to-webm-only-working-on-some-files + '-strict', '-2', // Needed because `vorbis` is experimental + '-r', options.fps.toString(), + { + args: ['-an'], + if: options.shouldMute + }, + { + args: [ + '-s', `${makeEven(options.width)}x${makeEven(options.height)}`, + '-ss', options.startTime.toString(), + '-to', options.endTime.toString() + ], + if: options.shouldCrop || !areDimensionsEven(options) + }, + options.outputPath +)); + +const convertToAv1 = (options: ConvertOptions) => convert(options.outputPath, { + onProgress: (progress, estimate) => options.onProgress('Converting', progress, estimate), + startTime: options.startTime, + endTime: options.endTime +}, conditionalArgs( + '-i', options.inputPath, + '-r', options.fps.toString(), + '-c:v', 'libaom-av1', + '-c:a', 'libopus', + '-crf', '34', + '-b:v', '0', + '-strict', 'experimental', + // Enables row-based multi-threading which maximizes CPU usage + // https://trac.ffmpeg.org/wiki/Encode/AV1 + '-cpu-used', '4', + '-row-mt', '1', + '-tiles', '2x2', + { + args: ['-an'], + if: options.shouldMute + }, + { + args: [ + '-s', `${makeEven(options.width)}x${makeEven(options.height)}`, + '-ss', options.startTime.toString(), + '-to', options.endTime.toString() + ], + if: options.shouldCrop || !areDimensionsEven(options) + }, + options.outputPath +)); + +const convertToApng = (options: ConvertOptions) => convert(options.outputPath, { + onProgress: (progress, estimate) => options.onProgress('Converting', progress, estimate), + startTime: options.startTime, + endTime: options.endTime +}, conditionalArgs( + '-i', options.inputPath, + '-vf', `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''}`, + // Strange for APNG instead of -loop it uses -plays see: https://stackoverflow.com/questions/43795518/using-ffmpeg-to-create-looping-apng + '-plays', settings.get('loopExports') ? '0' : '1', // 0 == forever; 1 == no loop + { + args: ['-an'], + if: options.shouldMute + }, + { + args: [ + '-ss', options.startTime.toString(), + '-to', options.endTime.toString() + ], + if: options.shouldCrop + }, + options.outputPath +)); + +export default new Map([ + ['gif', convertToGif], + ['mp4', convertToMp4], + ['webm', convertToWebm], + ['apng', convertToApng], + ['av1', convertToAv1] +]); diff --git a/main/converters/index.ts b/main/converters/index.ts new file mode 100644 index 000000000..fa5540053 --- /dev/null +++ b/main/converters/index.ts @@ -0,0 +1,31 @@ +import path from 'path'; +import tempy from 'tempy'; +import {track} from '../common/analytics'; +import h264Converters from './h264'; +import {ConvertOptions} from './utils'; + +const converters = new Map([ + ['h264', h264Converters] +]); + +export const convertTo = (format: string, options: Omit & {defaultFileName: string}, encoding: string = 'h264') => { + if (!converters.has(encoding)) { + throw new Error(`Unsupported encoding: ${encoding}`); + } + + const converter = converters.get(encoding)?.get(format); + + if (!converter) { + throw new Error(`Unsupported file format for ${encoding}: ${format}`); + } + + track(`file/export/encoding/${encoding}`); + track(`file/export/format/${format}`); + + // TODO: fill in edit service + + return converter({ + outputPath: path.join(tempy.directory(), options.defaultFileName), + ...options, + }); +}; diff --git a/main/converters/process.ts b/main/converters/process.ts new file mode 100644 index 000000000..35bdb1f1e --- /dev/null +++ b/main/converters/process.ts @@ -0,0 +1,131 @@ +import util from 'electron-util'; +import execa from 'execa'; +import moment from 'moment'; +import PCancelable from 'p-cancelable'; +import tempy from 'tempy'; +import path from 'path'; + +import {track} from '../common/analytics'; +import {conditionalArgs, extractProgressFromStderr} from './utils'; +import settings from '../common/settings'; + +const ffmpeg = require('@ffmpeg-installer/ffmpeg'); +const gifsicle = require('gifsicle'); + +const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path); +const gifsiclePath = util.fixPathForAsarUnpack(gifsicle); + +enum Mode { + convert, + compress +} + +const modes = new Map([ + [Mode.convert, ffmpegPath], + [Mode.compress, gifsiclePath] +]); + +interface ProcessOptions { + shouldTrack?: boolean; + startTime?: number; + endTime?: number; + onProgress?: (progress: number, estimate?: string) => void; +} + +const defaultProcessOptions = { + shouldTrack: true, +} + +const createProcess = (mode: Mode) => { + const program = modes.get(mode) as string; + + return (outputPath: string, options: ProcessOptions, args: string[]) => { + const { + shouldTrack, + startTime = 0, + endTime = 0, + onProgress + } = { + ...defaultProcessOptions, + ...options + }; + + const modeName = Mode[mode]; + const trackConversionEvent = (eventName: string) => { + if (shouldTrack) { + track(`file/export/${modeName}/${eventName}`); + } + } + + return new PCancelable((resolve, reject, onCancel) => { + const runner = execa(program, args); + + onCancel(() => { + trackConversionEvent('canceled'); + runner.kill(); + }); + + const durationMs = moment.duration(endTime - startTime, 'seconds').asMilliseconds(); + + let stderr = ''; + runner.stderr?.setEncoding('utf8'); + runner.stderr?.on('data', data => { + stderr += data; + + const progressData = extractProgressFromStderr(data, durationMs); + + if (progressData) { + onProgress?.(progressData.progress, progressData.estimate); + } + }); + + const failWithError = (reason: unknown) => { + trackConversionEvent('failed'); + reject(reason); + } + + runner.on('error', failWithError); + + runner.on('exit', code => { + if (code === 0) { + trackConversionEvent('completed'); + resolve(outputPath); + } else { + failWithError(new Error(`${program} exited with code: ${code}\n\n${stderr}`)); + } + }); + + runner.catch(failWithError); + }); + } +} + +export const convert = createProcess(Mode.convert); +const compressFunction = createProcess(Mode.compress); + +export const compress = (outputPath: string, options: ProcessOptions, args: string[]) => { + const useLossy = settings.get('lossyCompression', false); + + return compressFunction( + outputPath, + options, + conditionalArgs(args, {args: ['--lossy=50'], if: useLossy}) + ); +}; + +export const mute = PCancelable.fn(async (inputPath: string, onCancel: PCancelable.OnCancelFunction) => { + const mutedPath = tempy.file({extension: path.extname(inputPath)}); + + const converter = convert(mutedPath, {shouldTrack: false}, [ + '-i', inputPath, + '-an', + '-vcodec', 'copy', + mutedPath + ]); + + onCancel(() => { + converter.cancel(); + }); + + return converter; +}); diff --git a/main/converters/utils.ts b/main/converters/utils.ts new file mode 100644 index 000000000..41b4a52d8 --- /dev/null +++ b/main/converters/utils.ts @@ -0,0 +1,67 @@ +import moment from 'moment'; +import prettyMilliseconds from 'pretty-ms'; + +export interface ConvertOptions { + inputPath: string; + outputPath: string; + shouldCrop: boolean; + startTime: number; + endTime: number; + width: number; + height: number; + fps: number; + shouldMute: boolean; + onProgress: (action: string, progress: number, estimate?: string) => void; +} + +export const makeEven = (n: number) => 2 * Math.round(n / 2); + +export const areDimensionsEven = ({width, height}: {width: number, height: number}) => width % 2 === 0 && height % 2 === 0; + +const timeRegex = /time=\s*(\d\d:\d\d:\d\d.\d\d)/gm; +const speedRegex = /speed=\s*(-?\d+(,\d+)*(\.\d+(e\d+)?)?)/gm; + +export const extractProgressFromStderr = (stderr: string, durationMs: number) => { + const data = stderr.trim(); + + const speed = Number.parseFloat(speedRegex.exec(data)?.[1] ?? '0'); + const processedMs = moment.duration(timeRegex.exec(data)?.[1] ?? 0).asMilliseconds(); + + if (speed > 0 && processedMs > 0) { + const progress = processedMs / durationMs; + + // Wait 2 seconds in the conversion for speed to be stable + if (processedMs > 2 * 1000) { + const msRemaining = (durationMs - processedMs) / speed; + + return { + progress, + estimate: prettyMilliseconds(Math.max(msRemaining, 1000), {compact: true}) + }; + } + + return {progress}; + } + + return undefined; +}; + +type ArgType = string[] | string | { args: string[], if: boolean }; + +// Resolve conditional args +// +// conditionalArgs(['default', 'args'], {args: ['ignore', 'these'], if: false}); +// => ['default', 'args'] +export const conditionalArgs = (...args: ArgType[]): string[] => { + return args.flatMap(arg => { + if (typeof arg === 'string') { + return [arg]; + } + + if (Array.isArray(arg)) { + return arg; + } + + return arg.if ? arg.args : []; + }); +}; diff --git a/main/editor.js b/main/editor.js index 719aadeba..1e2dd8ee4 100644 --- a/main/editor.js +++ b/main/editor.js @@ -11,6 +11,7 @@ const {is} = require('electron-util'); const getFps = require('./utils/fps'); const loadRoute = require('./utils/routes'); const {generateTimestampedName} = require('./utils/timestamped-name'); +const KapWindow = require('./kap-window'); const editors = new Map(); let allOptions; @@ -41,7 +42,7 @@ const openEditorWindow = async ( const fps = recordedFps || await getFps(filePath); const title = recordingName || getEditorName(originalFilePath || filePath, isNewRecording); - const editorWindow = new BrowserWindow({ + const editorWindow = new KapWindow({ title, minWidth: MIN_VIDEO_WIDTH, minHeight: MIN_WINDOW_HEIGHT, @@ -54,12 +55,20 @@ const openEditorWindow = async ( frame: false, transparent: true, vibrancy: 'window', - show: false + route: 'editor2', + args: { + filePath, + fps, + originalFilePath, + isNewRecording, + recordingName, + title + } }); editors.set(filePath, editorWindow); - loadRoute(editorWindow, 'editor'); + // loadRoute(editorWindow, 'editor2'); if (isNewRecording) { editorWindow.setDocumentEdited(true); @@ -98,18 +107,19 @@ const openEditorWindow = async ( ipc.callRenderer(editorWindow, 'focus'); }); - editorWindow.webContents.on('did-finish-load', async () => { - ipc.callRenderer(editorWindow, 'export-options', allOptions); - await ipc.callRenderer(editorWindow, 'file', { - filePath, - fps, - originalFilePath, - isNewRecording, - recordingName, - title - }); - editorWindow.show(); - }); + // editorWindow.webContents.on('did-finish-load', async () => { + // ipc.callRenderer(editorWindow, 'kap-window-args', {filePath}); + // ipc.callRenderer(editorWindow, 'export-options', allOptions); + // await ipc.callRenderer(editorWindow, 'file', { + // filePath, + // fps, + // originalFilePath, + // isNewRecording, + // recordingName, + // title + // }); + // editorWindow.show(); + // }); }; const setOptions = options => { diff --git a/main/index.js b/main/index.js index f9d66095c..65b62f3e9 100644 --- a/main/index.js +++ b/main/index.js @@ -74,6 +74,9 @@ const checkForUpdates = () => { (async () => { await app.whenReady(); + // Initialize remote states + require('./remote-states'); + app.dock.hide(); app.setAboutPanelOptions({copyright: 'Copyright © Wulkano'}); diff --git a/main/kap-window.js b/main/kap-window.js new file mode 100644 index 000000000..b8ee6d1ce --- /dev/null +++ b/main/kap-window.js @@ -0,0 +1,72 @@ +const electron = require('electron'); +const loadRoute = require('./utils/routes'); +const {ipcMain: ipc} = require('electron-better-ipc'); + +// Has to be named BrowserWindow because of +// https://github.com/electron/electron/blob/master/lib/browser/api/browser-window.ts#L82 +class BrowserWindow extends electron.BrowserWindow { + _readyPromise = new Promise(resolve => { + this._readyPromiseResolve = resolve; + }); + + cleanupMethods = [] + + constructor(props) { + const { + route, + args, + waitForMount, + ...rest + } = props; + + super({ + webPreferences: { + nodeIntegration: true, + }, + ...rest, + show: false + }); + + this.options = props; + loadRoute(this, route); + this.setupWindow(); + } + + setupWindow() { + const {args, waitForMount} = this.options; + + this.on('close', () => { + for (const method of this.cleanupMethods) { + method(); + } + }); + + this.webContents.on('did-finish-load', async () => { + if (args) { + ipc.callRenderer(this, 'kap-window-args', args); + } + + if (waitForMount) { + this.answerRenderer('kap-window-mount', () => { + this.show(); + this._readyPromiseResolve(); + }); + } else { + this.show(); + this._readyPromiseResolve(); + } + }); + } + + answerRenderer(channel, callback) { + this.cleanupMethods.push(ipc.answerRenderer(this, channel, callback)); + } + + async whenReady() { + return this._readyPromise; + } +} + +const KapWindow = BrowserWindow; + +module.exports = KapWindow; diff --git a/main/plugin.js b/main/plugin.js index 780c6ba0f..f446b5e1c 100644 --- a/main/plugin.js +++ b/main/plugin.js @@ -13,7 +13,7 @@ const {showError} = require('./utils/errors'); const {app, shell} = electron; -const recordPluginServiceState = new Store({ +export const recordPluginServiceState = new Store({ name: 'record-plugin-state', defaults: {} }); @@ -38,7 +38,7 @@ class BasePlugin { } } -class InstalledPlugin extends BasePlugin { +export class InstalledPlugin extends BasePlugin { constructor(pluginName) { super(pluginName); @@ -129,7 +129,7 @@ class InstalledPlugin extends BasePlugin { } } -class NpmPlugin extends BasePlugin { +export class NpmPlugin extends BasePlugin { constructor(json, kap = {}) { super(json.name); @@ -150,8 +150,8 @@ class NpmPlugin extends BasePlugin { } } -module.exports = { - InstalledPlugin, - NpmPlugin, - recordPluginServiceState -}; +// module.exports = { +// InstalledPlugin, +// NpmPlugin, +// recordPluginServiceState +// }; diff --git a/main/remote-states/conversions/conversion.js b/main/remote-states/conversions/conversion.js new file mode 100644 index 000000000..19d2df13c --- /dev/null +++ b/main/remote-states/conversions/conversion.js @@ -0,0 +1,50 @@ +const {convertTo} = require('../../convert'); + +const queue = new ConversionQueue(); + +class Conversion { + exports = [] + + constructor(options) { + this.video = options.video; + this.format = options.format; + this.exportOptions = options.exportOptions; + } + + convert = async ({fileType} = {}) => { + if (this.filePath) { + return this.filePath; + } + + this.convertProcess = queue.queueConversion( + () => { + if (this.canceled) { + return; + } + + return convertTo({ + ...this.exportOptions, + // FIXME + }); + } + ); + + this.filePath = await this.convertProcess; + return this.filePath; + } + + run = () => { + + } + + addExport = (newExport) => { + this.exports.push(newExport); + newExport.run(this); + } +} + +class Export { + constructor(options) { + + } +} diff --git a/main/remote-states/conversions/index.js b/main/remote-states/conversions/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/main/remote-states/conversions/video.js b/main/remote-states/conversions/video.js new file mode 100644 index 000000000..ba9ce01bc --- /dev/null +++ b/main/remote-states/conversions/video.js @@ -0,0 +1,70 @@ +const path = require('path'); +const getFps = require('../../utils/fps'); +const {getEncoding, convertToH264} = require('../../utils/encoding'); + +class Video { + constructor(options) { + this.filePath = options.filePath; + this.title = options.title || path.basename(this.filePath); + this.fps = options.fps; + this.encoding = options.encoding; + this.pixelDensity = options.pixelDensity || 1; + + this.whenReady = this._collectInfo(); + } + + async _collectInfo() { + return Promise.all([ + this.getFps(), + this.getEncoding(), + this.getPreviewPath() + ]); + } + + async getFps() { + if (!this.fps) { + this.fps = await getFps(this.filePath); + } + + return this.fps; + } + + async getEncoding() { + if (!this.encoding) { + this.encoding = getEncoding(this.filePath); + } + } + + async getPreviewPath() { + if (!this.previewPath) { + const encoding = await this.getEncoding(); + + if (encoding === 'h264') { + this.previewPath = this.filePath; + } else { + this.previewPath = await convertToH264(this.filePath) + } + } + + return this.previewPath; + } +} + +class Recording extends Video { + constructor(options) { + super({ + ...options, + fps: options.recordingOptions.fps, + encoding: options.recordingOptions.encoding, + pixelDensity: options.recordingOptions.pixelDensity, + title: options.recordingName + }); + + this.recordingOptions = options.recordingOptions; + } +} + +module.exports = { + Video, + Recording +}; diff --git a/main/remote-states/editor-options.js b/main/remote-states/editor-options.js new file mode 100644 index 000000000..aad98cb90 --- /dev/null +++ b/main/remote-states/editor-options.js @@ -0,0 +1,168 @@ +'use strict'; +const Store = require('electron-store'); + +const plugins = require('../common/plugins'); +const {converters} = require('../convert'); +const {apps} = require('../plugins/open-with-plugin'); +const {showError} = require('../utils/errors'); + +const exportUsageHistory = new Store({ + name: 'export-usage-history', + defaults: { + apng: {lastUsed: 1, plugins: {default: 1}}, + webm: {lastUsed: 2, plugins: {default: 1}}, + mp4: {lastUsed: 3, plugins: {default: 1}}, + gif: {lastUsed: 4, plugins: {default: 1}}, + av1: {lastUsed: 5, plugins: {default: 1}} + } +}); + +const fpsUsageHistory = new Store({ + name: 'fps-usage-history', + schema: { + apng: { + type: 'number', + minimum: 0, + default: 60 + }, + webm: { + type: 'number', + minimum: 0, + default: 60 + }, + mp4: { + type: 'number', + minimum: 0, + default: 60 + }, + gif: { + type: 'number', + minimum: 0, + default: 60 + }, + av1: { + type: 'number', + minimum: 0, + default: 60 + } + } +}); + + +const prettifyFormat = format => { + const formats = new Map([ + ['apng', 'APNG'], + ['gif', 'GIF'], + ['mp4', 'MP4 (H264)'], + ['av1', 'MP4 (AV1)'], + ['webm', 'WebM'] + ]); + + return formats.get(format); +}; + +const getEditOptions = () => { + console.log(plugins.getEditPlugins()); + return plugins.getEditPlugins().flatMap( + plugin => plugin.editServices + .filter(service => plugin.config.validServices.includes(service.title)) + .map(service => ({ + title: service.title, + pluginName: plugin.name, + pluginPath: plugin.pluginPath, + hasConfig: Object.keys(service.config || {}).length > 0 + })) + ); +}; + +const getExportOptions = () => { + const installed = plugins.getSharePlugins(); + const builtIn = plugins.getBuiltIn(); + + const options = []; + for (const format of converters.keys()) { + options.push({ + format, + prettyFormat: prettifyFormat(format), + plugins: [] + }); + } + + for (const json of [...installed, ...builtIn]) { + if (!json.isCompatible) { + continue; + } + + try { + const plugin = require(json.pluginPath); + + for (const service of plugin.shareServices) { + for (const format of service.formats) { + options.find(option => option.format === format).plugins.push({ + title: service.title, + pluginName: json.name, + pluginPath: json.pluginPath, + apps: json.name === '_openWith' ? apps.get(format) : undefined + }); + } + } + } catch (error) { + showError(error, {title: `Something went wrong while loading “${json.name}”`, plugin: json}); + const Sentry = require('../utils/sentry'); + Sentry.captureException(error); + } + } + + const sortFunc = (a, b) => b.lastUsed - a.lastUsed; + + for (const option of options) { + const {lastUsed, plugins} = exportUsageHistory.get(option.format); + option.lastUsed = lastUsed; + option.plugins = option.plugins.map(plugin => ({...plugin, lastUsed: plugins[plugin.pluginName] || 0})).sort(sortFunc); + } + + return options.sort(sortFunc); +}; + +module.exports = sendUpdate => { + const state = { + formats: getExportOptions(), + editServices: getEditOptions(), + fpsHistory: fpsUsageHistory.store + }; + + const updatePlugins = () => { + state.formats = getExportOptions(); + state.editServices = getEditOptions(); + sendUpdate(state); + }; + + plugins.on('installed', updatePlugins); + plugins.on('uninstalled', updatePlugins); + plugins.on('config-changed', updatePlugins); + + const actions = { + updatePluginUsage: ({format, plugin}) => { + const usage = exportUsageHistory.get(format); + const now = Date.now(); + + usage.plugins[plugin] = now; + usage.lastUsed = now; + exportUsageHistory.set(format, usage); + + state.formats = getExportOptions(); + sendUpdate(state); + }, + updateFpsUsage: ({format, fps}) => { + fpsUsageHistory.set(format, fps); + + state.fpsHistory = fpsUsageHistory.store; + sendUpdate(state); + } + }; + + return { + actions, + getState: () => state + }; +}; diff --git a/main/remote-states/index.js b/main/remote-states/index.js new file mode 100644 index 000000000..c0b7500b9 --- /dev/null +++ b/main/remote-states/index.js @@ -0,0 +1,9 @@ +'use strict'; + +const {setupRemoteState} = require('../common/remote-state'); + +const remoteStateNames = ['editor-options']; + +for(const name of remoteStateNames) { + setupRemoteState(name, require(`./${name}`))(); +} diff --git a/main/service-context.js b/main/service-context.js index 808f264b8..4ef70a4b7 100644 --- a/main/service-context.js +++ b/main/service-context.js @@ -1,6 +1,8 @@ 'use strict'; const {Notification, clipboard} = require('electron'); const got = require('got'); +const {EventEmitter} = require('events'); + const {addPluginPromise} = require('./utils/deep-linking'); const prettifyFormat = format => { @@ -14,7 +16,7 @@ const prettifyFormat = format => { return formats.get(format); }; -class ServiceContext { +class ServiceContext extends EventEmitter { constructor(options) { this._isBuiltin = options._isBuiltin; this.config = options.config; @@ -100,7 +102,7 @@ class ServiceContext { } } -class ShareServiceContext extends ServiceContext { +export class ShareServiceContext extends ServiceContext { constructor(options) { super(options); @@ -135,7 +137,7 @@ class ShareServiceContext extends ServiceContext { } } -class RecordServiceContext extends ServiceContext { +export class RecordServiceContext extends ServiceContext { constructor(options) { super(options); @@ -145,7 +147,7 @@ class RecordServiceContext extends ServiceContext { } } -class EditServiceContext extends ServiceContext { +export class EditServiceContext extends ServiceContext { constructor(options) { super(options); @@ -178,8 +180,8 @@ class EditServiceContext extends ServiceContext { } } -module.exports = { - ShareServiceContext, - RecordServiceContext, - EditServiceContext -}; +// module.exports = { +// ShareServiceContext, +// RecordServiceContext, +// EditServiceContext +// }; diff --git a/main/tray.js b/main/tray.js index 1949144a7..ab6749a0a 100644 --- a/main/tray.js +++ b/main/tray.js @@ -17,7 +17,11 @@ const openContextMenu = async () => { const initializeTray = () => { tray = new Tray(path.join(__dirname, '..', 'static', 'menubarDefaultTemplate.png')); - tray.on('click', openCropperWindow); + // tray.on('click', openCropperWindow); + tray.on('click', () => { + const {openEditorWindow} = require('./editor'); + openEditorWindow('/Users/b008822/Kaptures/Kapture 2020-07-10 at 9.48.45.mp4'); + }); tray.on('right-click', openContextMenu); tray.on('drop-files', (_, files) => { track('editor/opened/tray'); diff --git a/main/utils/ajv.js b/main/utils/ajv.ts similarity index 57% rename from main/utils/ajv.js rename to main/utils/ajv.ts index 937f5e991..05f55751d 100644 --- a/main/utils/ajv.js +++ b/main/utils/ajv.ts @@ -1,4 +1,4 @@ -const Ajv = require('ajv'); +import Ajv, {Options} from 'ajv'; const hexColorValidator = () => { return { @@ -13,18 +13,24 @@ const keyboardShortcutValidator = () => { }; }; -const validators = new Map([ +const validators = new Map object>([ ['hexColor', hexColorValidator], ['keyboardShortcut', keyboardShortcutValidator] ]); -class CustomAjv extends Ajv { - constructor(options) { +export default class CustomAjv extends Ajv { + constructor(options: Options) { super(options); this.addKeyword('customType', { macro: (schema, parentSchema) => { - return validators.get(schema)(parentSchema); + const validator = validators.get(schema); + + if (!validator) { + throw new Error(`No custom type found for ${schema}`); + } + + return validator(parentSchema); }, metaSchema: { type: 'string', @@ -33,5 +39,3 @@ class CustomAjv extends Ajv { }); } } - -module.exports = CustomAjv; diff --git a/main/utils/errors.js b/main/utils/errors.js index c8e39993c..933f6d20b 100644 --- a/main/utils/errors.js +++ b/main/utils/errors.js @@ -66,6 +66,8 @@ const showError = async (error, {title: customTitle, plugin} = {}) => { const title = customTitle || ensuredError.name; const detail = getPrettyStack(ensuredError); + console.log(error); + const mainButtons = [ 'Don\'t Report', { diff --git a/main/utils/plugin-config.js b/main/utils/plugin-config.js index 65260038a..cfaa54f9e 100644 --- a/main/utils/plugin-config.js +++ b/main/utils/plugin-config.js @@ -1,5 +1,5 @@ -const Store = require('electron-store'); -const Ajv = require('./ajv'); +import Store from 'electron-store'; +import Ajv from './ajv'; class PluginConfig extends Store { constructor(plugin) { diff --git a/package.json b/package.json index 52d1ee03d..3a6047276 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,14 @@ "url": "https://wulkano.com" }, "private": true, - "main": "main/index.js", + "main": "dist-js/index.js", "scripts": { "lint": "xo", "test": "xo && ava", - "start": "yarn && electron .", - "build": "next build renderer && next export renderer", + "start": "tsc && electron .", + "build-main": "tsc", + "build-renderer": "next build renderer && next export renderer", + "build": "yarn build-main && yarn build-renderer", "dist": "npm run build && electron-builder", "pack": "npm run build && electron-builder --dir", "postinstall": "electron-builder install-app-deps", @@ -37,7 +39,7 @@ "classnames": "^2.2.6", "clean-stack": "^3.0.0", "delay": "^4.3.0", - "electron-better-ipc": "^1.0.1", + "electron-better-ipc": "^1.1.0", "electron-log": "^4.1.1", "electron-next": "^3.1.5", "electron-notarize": "^0.3.0", @@ -81,9 +83,12 @@ "tildify": "^2.0.0", "tmp": "^0.2.0", "unstated": "^1.2.0", + "unstated-next": "^1.1.0", "yarn": "^1.22.4" }, "devDependencies": { + "@sindresorhus/tsconfig": "^0.7.0", + "@types/react": "^16.9.46", "ava": "^3.9.0", "babel-eslint": "^10.1.0", "electron": "8.2.4", @@ -96,6 +101,7 @@ "module-alias": "^2.2.2", "next": "^9.3.6", "sinon": "^9.0.2", + "typescript": "^4.0.3", "unique-string": "^2.0.0", "xo": "^0.30.0" }, @@ -156,7 +162,8 @@ ] }, "files": [ - "**/*", + "static", + "dist-js/**/*", "!renderer", "renderer/out" ], diff --git a/renderer/components/action-bar/controls/advanced.js b/renderer/components/action-bar/controls/advanced.js index f2770ecf6..bcedd8abf 100644 --- a/renderer/components/action-bar/controls/advanced.js +++ b/renderer/components/action-bar/controls/advanced.js @@ -262,7 +262,6 @@ class Right extends React.Component { tabIndex={advanced ? 0 : -1} onChange={this.onWidthChange} onBlur={this.onWidthBlur} - onKeyDown={this.onWidthChange} onMouseDown={stopPropagation} />
@@ -280,7 +279,6 @@ class Right extends React.Component { tabIndex={advanced ? 0 : -1} onChange={this.onHeightChange} onBlur={this.onHeightBlur} - onKeyDown={this.onHeightChange} onMouseDown={stopPropagation} /> {keyboardInputStyles} diff --git a/renderer/components/editor/controls/left.js b/renderer/components/editor/controls/left.js deleted file mode 100644 index 5ff01ea7a..000000000 --- a/renderer/components/editor/controls/left.js +++ /dev/null @@ -1,61 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import {PlayIcon, PauseIcon} from '../../../vectors'; -import {connect, VideoContainer} from '../../../containers'; -import formatTime from '../../../utils/format-time'; - -class LeftControls extends React.Component { - render() { - const {play, pause, isPaused, currentTime} = this.props; - - return ( -
-
- { - isPaused ? - : - - } -
-
{formatTime(currentTime, {showMilliseconds: false})}
- -
- ); - } -} - -LeftControls.propTypes = { - play: PropTypes.elementType, - pause: PropTypes.elementType, - isPaused: PropTypes.bool, - currentTime: PropTypes.number -}; - -export default connect( - [VideoContainer], - ({isPaused, currentTime}) => ({isPaused, currentTime}), - ({play, pause}) => ({play, pause}) -)(LeftControls); diff --git a/renderer/components/editor/controls/left.tsx b/renderer/components/editor/controls/left.tsx new file mode 100644 index 000000000..7ea302e56 --- /dev/null +++ b/renderer/components/editor/controls/left.tsx @@ -0,0 +1,47 @@ +import VideoControlsContainer from '../video-controls-container' +import VideoTimeContainer from '../video-time-container'; +import {PlayIcon, PauseIcon} from '../../../vectors'; +import formatTime from '../../../utils/format-time'; + +const LeftControls = () => { + const {isPaused, play, pause} = VideoControlsContainer.useContainer(); + const {currentTime} = VideoTimeContainer.useContainer(); + + return ( +
+
+ { + isPaused ? + : + + } +
+
{formatTime(currentTime, {showMilliseconds: false})}
+ +
+ ); +} + +export default LeftControls; diff --git a/renderer/components/editor/controls/play-bar.js b/renderer/components/editor/controls/play-bar.js deleted file mode 100644 index cb7f761bb..000000000 --- a/renderer/components/editor/controls/play-bar.js +++ /dev/null @@ -1,237 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import classNames from 'classnames'; - -import {connect, VideoContainer} from '../../../containers'; -import Preview from './preview'; - -class PlayBar extends React.Component { - state = { - hoverTime: 0 - }; - - progress = React.createRef(); - - getTimeFromEvent = event => { - const {startTime, endTime} = this.props; - - const cursorX = event.clientX; - const {x, width} = this.progress.current.getBoundingClientRect(); - - const percent = (cursorX - x) / width; - const time = startTime + ((endTime - startTime) * percent); - - return Math.max(0, time); - } - - seek = event => { - const {startTime, endTime, seek} = this.props; - const time = this.getTimeFromEvent(event); - - if (startTime <= time && time <= endTime) { - seek(time); - } - } - - updatePreview = event => { - const time = this.getTimeFromEvent(event); - this.setState({hoverTime: time}); - } - - startResizing = () => { - const {pause} = this.props; - this.setState({resizing: true}); - pause(); - } - - stopResizing = () => { - const {play} = this.props; - this.setState({resizing: false}); - play(); - } - - setStartTime = event => this.props.setStartTime(Number.parseFloat(event.target.value)) - - setEndTime = event => this.props.setEndTime(Number.parseFloat(event.target.value)) - - render() { - const {currentTime = 0, duration, startTime, endTime, hover, src} = this.props; - - if (!src) { - return null; - } - - const {hoverTime, resizing} = this.state; - - const total = endTime - startTime; - const current = currentTime - startTime; - - const previewTime = resizing ? currentTime : hoverTime; - const previewLabelTime = resizing ? currentTime : (startTime <= hoverTime && hoverTime <= endTime ? hoverTime - startTime : hoverTime); - const previewDuration = resizing ? total : (startTime <= hoverTime && hoverTime <= endTime ? total : undefined); - - const className = classNames('progress-bar-container', {hover}); - - return ( -
-
-
- -
- -
- - -
-
- -
- ); - } -} - -PlayBar.propTypes = { - startTime: PropTypes.number, - endTime: PropTypes.number, - seek: PropTypes.elementType, - currentTime: PropTypes.number, - duration: PropTypes.number, - src: PropTypes.string, - setStartTime: PropTypes.elementType, - setEndTime: PropTypes.elementType, - pause: PropTypes.elementType, - play: PropTypes.elementType, - hover: PropTypes.bool -}; - -export default connect( - [VideoContainer], - ({currentTime, duration, startTime, endTime, src}) => ({currentTime, duration, startTime, endTime, src}), - ({seek, setStartTime, setEndTime, pause, play}) => ({seek, setStartTime, setEndTime, pause, play}) -)(PlayBar); diff --git a/renderer/components/editor/controls/play-bar.tsx b/renderer/components/editor/controls/play-bar.tsx new file mode 100644 index 000000000..534c82fb5 --- /dev/null +++ b/renderer/components/editor/controls/play-bar.tsx @@ -0,0 +1,436 @@ +import VideoTimeContainer from '../video-time-container'; +import {useState, useRef} from 'react'; +import VideoControlsContainer from '../video-controls-container'; +import Preview from './preview'; + +const PlayBar = () => { + const [resizing, setResizing] = useState(false); + const [hoverTime, setHoverTime] = useState(0); + const progress = useRef(); + + const {play, pause} = VideoControlsContainer.useContainer(); + const { + currentTime, + duration, + startTime, + endTime, + updateTime, + updateStartTime, + updateEndTime + } = VideoTimeContainer.useContainer(); + + const total = endTime - startTime; + const current = currentTime - startTime; + + const getTimeFromEvent = event => { + const cursorX = event.clientX; + const {x, width} = progress.current.getBoundingClientRect(); + + const percent = (cursorX - x) / width; + const time = startTime + ((endTime - startTime) * percent); + + return Math.max(0, time); + }; + + const seek = event => { + const time = getTimeFromEvent(event); + + if (startTime <= time && time <= endTime) { + updateTime(time); + } + } + + const updatePreview = event => { + setHoverTime(getTimeFromEvent(event)); + } + + const startResizing = () => { + setResizing(true); + pause(); + } + + const stopResizing = () => { + setResizing(false); + play(); + } + + const setStartTime = event => updateStartTime(Number.parseFloat(event.target.value)) + + const setEndTime = event => updateEndTime(Number.parseFloat(event.target.value)) + + const previewTime = resizing ? currentTime : hoverTime; + const previewLabelTime = resizing ? currentTime : (startTime <= hoverTime && hoverTime <= endTime ? hoverTime - startTime : hoverTime); + const previewDuration = resizing ? total : (startTime <= hoverTime && hoverTime <= endTime ? total : undefined); + + return ( +
+
+
+ +
+ +
+ + +
+
+ +
+ ); +} + +export default PlayBar; + +// import PropTypes from 'prop-types'; +// import React from 'react'; +// import classNames from 'classnames'; + +// import {connect, VideoContainer} from '../../../containers'; +// import Preview from './preview'; + +// class PlayBar extends React.Component { +// state = { +// hoverTime: 0 +// }; + +// progress = React.createRef(); + + // getTimeFromEvent = event => { + // const {startTime, endTime} = this.props; + + // const cursorX = event.clientX; + // const {x, width} = this.progress.current.getBoundingClientRect(); + + // const percent = (cursorX - x) / width; + // const time = startTime + ((endTime - startTime) * percent); + + // return Math.max(0, time); + // } + + // seek = event => { + // const {startTime, endTime, seek} = this.props; + // const time = this.getTimeFromEvent(event); + + // if (startTime <= time && time <= endTime) { + // seek(time); + // } + // } + + // updatePreview = event => { + // const time = this.getTimeFromEvent(event); + // this.setState({hoverTime: time}); + // } + + // startResizing = () => { + // const {pause} = this.props; + // this.setState({resizing: true}); + // pause(); + // } + + // stopResizing = () => { + // const {play} = this.props; + // this.setState({resizing: false}); + // play(); + // } + + // setStartTime = event => this.props.setStartTime(Number.parseFloat(event.target.value)) + + // setEndTime = event => this.props.setEndTime(Number.parseFloat(event.target.value)) + +// render() { + // const {currentTime = 0, duration, startTime, endTime, hover, src} = this.props; + + // if (!src) { + // return null; + // } + + // const {hoverTime, resizing} = this.state; + + // const total = endTime - startTime; + // const current = currentTime - startTime; + + // const previewTime = resizing ? currentTime : hoverTime; + // const previewLabelTime = resizing ? currentTime : (startTime <= hoverTime && hoverTime <= endTime ? hoverTime - startTime : hoverTime); + // const previewDuration = resizing ? total : (startTime <= hoverTime && hoverTime <= endTime ? total : undefined); + + // const className = classNames('progress-bar-container', {hover}); + + // return ( + //
+ //
+ //
+ // + //
+ // + //
+ // + // + //
+ //
+ // + //
+ // ); +// } +// } + +// PlayBar.propTypes = { +// startTime: PropTypes.number, +// endTime: PropTypes.number, +// seek: PropTypes.elementType, +// currentTime: PropTypes.number, +// duration: PropTypes.number, +// src: PropTypes.string, +// setStartTime: PropTypes.elementType, +// setEndTime: PropTypes.elementType, +// pause: PropTypes.elementType, +// play: PropTypes.elementType, +// hover: PropTypes.bool +// }; + +// export default connect( +// [VideoContainer], +// ({currentTime, duration, startTime, endTime, src}) => ({currentTime, duration, startTime, endTime, src}), +// ({seek, setStartTime, setEndTime, pause, play}) => ({seek, setStartTime, setEndTime, pause, play}) +// )(PlayBar); diff --git a/renderer/components/editor/controls/preview.js b/renderer/components/editor/controls/preview.js deleted file mode 100644 index 9e2f390b7..000000000 --- a/renderer/components/editor/controls/preview.js +++ /dev/null @@ -1,73 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import formatTime from '../../../utils/format-time'; - -class Preview extends React.Component { - constructor(props) { - super(props); - this.videoRef = React.createRef(); - } - - shouldComponentUpdate(nextProps) { - return nextProps.time !== this.props.time || nextProps.hidePreview !== this.props.hidePreview; - } - - componentDidUpdate(previousProps) { - if (previousProps.time !== this.props.time) { - this.videoRef.current.currentTime = this.props.time; - } - } - - render() { - const {labelTime, duration, hidePreview, src} = this.props; - - return ( -
event.stopPropagation()}> -
- ); - } -} - -Preview.propTypes = { - time: PropTypes.number, - labelTime: PropTypes.number, - duration: PropTypes.number, - hidePreview: PropTypes.bool, - src: PropTypes.string -}; - -export default Preview; diff --git a/renderer/components/editor/controls/preview.tsx b/renderer/components/editor/controls/preview.tsx new file mode 100644 index 000000000..34197c366 --- /dev/null +++ b/renderer/components/editor/controls/preview.tsx @@ -0,0 +1,137 @@ +import VideoTimeContainer from '../video-time-container'; +import useWindowArgs from '../../../hooks/window-args'; +import formatTime from '../../../utils/format-time'; +import {useRef, useEffect} from 'react'; + +type Props = { + time: number, + labelTime: number, + duration: number, + hidePreview: boolean +}; + +const Preview = ({time, labelTime, duration, hidePreview}: Props) => { + const videoRef = useRef(); + const {filePath} = useWindowArgs(); + const src = `file://${filePath}`; + + useEffect(() => { + if (!hidePreview) { + videoRef.current.currentTime = time; + } + }, [time, hidePreview]); + + return ( +
event.stopPropagation()}> +
+ ); +}; + +export default Preview; + +// import PropTypes from 'prop-types'; +// import React from 'react'; + +// import formatTime from '../../../utils/format-time'; + +// class Preview extends React.Component { +// constructor(props) { +// super(props); +// this.videoRef = React.createRef(); +// } + +// shouldComponentUpdate(nextProps) { +// return nextProps.time !== this.props.time || nextProps.hidePreview !== this.props.hidePreview; +// } + +// componentDidUpdate(previousProps) { +// if (previousProps.time !== this.props.time) { +// this.videoRef.current.currentTime = this.props.time; +// } +// } + +// render() { +// const {labelTime, duration, hidePreview, src} = this.props; + + // return ( + //
event.stopPropagation()}> + //
+ // ); +// } +// } + +// Preview.propTypes = { +// time: PropTypes.number, +// labelTime: PropTypes.number, +// duration: PropTypes.number, +// hidePreview: PropTypes.bool, +// src: PropTypes.string +// }; + +// export default Preview; diff --git a/renderer/components/editor/controls/right.js b/renderer/components/editor/controls/right.js deleted file mode 100644 index 4a8814da7..000000000 --- a/renderer/components/editor/controls/right.js +++ /dev/null @@ -1,65 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import {VolumeHighIcon, VolumeOffIcon} from '../../../vectors'; -import {connect, VideoContainer, EditorContainer} from '../../../containers'; - -import formatTime from '../../../utils/format-time'; - -class RightControls extends React.Component { - render() { - const {isMuted, mute, unmute, format, duration, hasAudio} = this.props; - const canUnmute = !['gif', 'apng'].includes(format) && hasAudio; - const unmuteColor = canUnmute ? '#fff' : 'rgba(255, 255, 255, 0.40)'; - return ( -
-
{formatTime(duration)}
-
- { - isMuted || !hasAudio ? - : - - } -
- -
- ); - } -} - -RightControls.propTypes = { - hasAudio: PropTypes.bool, - isMuted: PropTypes.bool, - mute: PropTypes.elementType, - unmute: PropTypes.elementType, - format: PropTypes.string, - duration: PropTypes.number -}; - -export default connect( - [VideoContainer, EditorContainer], - ({isMuted, duration, hasAudio}, {format}) => ({hasAudio, isMuted, format, duration}), - ({mute, unmute}) => ({mute, unmute}) -)(RightControls); diff --git a/renderer/components/editor/controls/right.tsx b/renderer/components/editor/controls/right.tsx new file mode 100644 index 000000000..0c52176e9 --- /dev/null +++ b/renderer/components/editor/controls/right.tsx @@ -0,0 +1,54 @@ +import {VolumeHighIcon, VolumeOffIcon} from '../../../vectors'; +import VideoControlsContainer from '../video-controls-container'; +import VideoMetadataContainer from '../video-metadata-container'; + +import formatTime from '../../../utils/format-time'; + +const RightControls = () => { + const {isMuted, mute, unmute} = VideoControlsContainer.useContainer(); + const {hasAudio, duration} = VideoMetadataContainer.useContainer(); + + // FIXME + const format = 'mp4'; + + const canUnmute = !['gif', 'apng'].includes(format) && hasAudio; + const unmuteColor = canUnmute ? '#fff' : 'rgba(255, 255, 255, 0.40)'; + + return ( +
+
{formatTime(duration)}
+
+ { + isMuted || !hasAudio ? + : + + } +
+ +
+ ); +}; + +export default RightControls; diff --git a/renderer/components/editor/editor-options.tsx b/renderer/components/editor/editor-options.tsx new file mode 100644 index 000000000..2be3bf6fc --- /dev/null +++ b/renderer/components/editor/editor-options.tsx @@ -0,0 +1,5 @@ +import {useRemoteState} from '../../../main/common/remote-state'; + +const useEditorOptions = useRemoteState('editor-options', {formats: [], editServices: [], fpsHistory: {}}); + +export default useEditorOptions; diff --git a/renderer/components/editor/editor-preview.tsx b/renderer/components/editor/editor-preview.tsx new file mode 100644 index 000000000..d170eaa60 --- /dev/null +++ b/renderer/components/editor/editor-preview.tsx @@ -0,0 +1,73 @@ +import useWindowArgs from '../../hooks/window-args'; +import TrafficLights from '../traffic-lights'; +import VideoPlayer from './video-player'; +import Options from './options'; + +const EditorPreview = () => { + const {title = 'Editor'} = useWindowArgs(); + + return ( +
+
+
+
+ +
{title}
+
+
+ +
+ + +
+ ); +}; + +export default EditorPreview; diff --git a/renderer/components/editor/options-container.tsx b/renderer/components/editor/options-container.tsx new file mode 100644 index 000000000..b9b8bcec5 --- /dev/null +++ b/renderer/components/editor/options-container.tsx @@ -0,0 +1,106 @@ +import {useState, useEffect} from 'react' +import {createContainer} from 'unstated-next'; + +import {FormatName, EditService} from '../../../main/common/remote-state'; +import useWindowArgs from '../../hooks/window-args'; +import VideoMetadataContainer from './video-metadata-container'; +import VideoControlsContainer from './video-controls-container'; +import useEditorOptions from './editor-options'; + + +const isFormatMuted = (format: FormatName) => ['gif', 'apng'].includes(format); + +const useOptions = () => { + const {fps: originalFps} = useWindowArgs(); + const { + state: { + formats, + fpsHistory, + editServices + }, + updateFpsUsage, + isLoading + } = useEditorOptions(); + + const metadata = VideoMetadataContainer.useContainer(); + const {isMuted, mute, unmute} = VideoControlsContainer.useContainer(); + + const [format, setFormat] = useState(); + const [fps, setFps] = useState(); + const [width, setWidth] = useState(); + const [height, setHeight] = useState(); + const [editPlugin, setEditPlugin] = useState(); + + const [wasMuted, setWasMuted] = useState(false); + + const updateFps = (newFps: number, formatName = format) => { + updateFpsUsage({format: formatName, fps: newFps}); + setFps(newFps); + } + + const updateFormat = (formatName: FormatName) => { + if (metadata.hasAudio) { + if (isFormatMuted(formatName) && !isFormatMuted(format)) { + setWasMuted(isMuted); + mute(); + } else if (!isFormatMuted(formatName) && isFormatMuted(format) && !wasMuted) { + unmute(); + } + } + + setFormat(formatName); + updateFps(Math.min(originalFps, fpsHistory[formatName]), formatName); + } + + useEffect(() => { + if (isLoading) { + return; + } + + const formatName = formats[0].format + + setFormat(formatName); + updateFps(Math.min(originalFps, fpsHistory[formatName]), formatName); + }, [isLoading]); + + useEffect(() => { + setWidth(metadata.width); + setHeight(metadata.height); + }, [metadata]); + + useEffect(() => { + if (!editPlugin) { + return; + } + + const newPlugin = editServices.find(service => service.pluginName === editPlugin.pluginName && service.title === editPlugin.title); + setEditPlugin(newPlugin); + }, [editServices]); + + const setDimensions = (dimensions: {width: number, height: number}) => { + setWidth(dimensions.width); + setHeight(dimensions.height); + } + + const res = { + width, + height, + format, + fps, + originalFps, + editPlugin, + formats, + editServices, + updateFps, + updateFormat, + setEditPlugin, + setDimensions + }; + + console.log(res); + return res; +} + +const OptionsContainer = createContainer(useOptions); + +export default OptionsContainer; diff --git a/renderer/components/editor/options/index.js b/renderer/components/editor/options/index.tsx similarity index 55% rename from renderer/components/editor/options/index.js rename to renderer/components/editor/options/index.tsx index 63ac1b568..9e328fec8 100644 --- a/renderer/components/editor/options/index.js +++ b/renderer/components/editor/options/index.tsx @@ -1,15 +1,12 @@ -import React from 'react'; - import LeftOptions from './left'; import RightOptions from './right'; -export default class Options extends React.Component { - render() { - return ( -
- - - -
- ); - } +
+ ); } + +export default Options; diff --git a/renderer/components/editor/options/left.js b/renderer/components/editor/options/left.js deleted file mode 100644 index 717306b4c..000000000 --- a/renderer/components/editor/options/left.js +++ /dev/null @@ -1,137 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import css from 'styled-jsx/css'; -import {connect, EditorContainer} from '../../../containers'; -import KeyboardNumberInput from '../../keyboard-number-input'; -import Slider from './slider'; - -const {className: keyboardInputClass, styles: keyboardInputStyles} = css.resolve` - height: 24px; - background: rgba(255, 255, 255, 0.1); - text-align: center; - font-size: 12px; - box-sizing: border-box; - border: none; - padding: 4px; - border-bottom-left-radius: 4px; - border-top-left-radius: 4px; - width: 48px; - color: white; - box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px 1px 2px 0px rgba(0, 0, 0, 0.2); - - input + input { - border-bottom-left-radius: 0; - border-top-left-radius: 0; - border-bottom-right-radius: 4px; - border-top-right-radius: 4px; - margin-left: 1px; - margin-right: 16px; - } - - :focus, :hover { - outline: none; - background: hsla(0, 0%, 100%, 0.2); - } -`; - -class LeftOptions extends React.Component { - handleBlur = event => { - const {changeDimension} = this.props; - changeDimension(event, {ignoreEmpty: false}); - } - - render() { - const {width, height, changeDimension, fps, originalFps, setFps, original} = this.props; - - return ( -
-
Size
- - -
FPS
-
- -
- {keyboardInputStyles} - -
- ); - } -} - -LeftOptions.propTypes = { - width: PropTypes.number, - height: PropTypes.number, - changeDimension: PropTypes.elementType, - fps: PropTypes.number, - setFps: PropTypes.elementType, - originalFps: PropTypes.number, - original: PropTypes.shape({ - width: PropTypes.number, - height: PropTypes.number - }) -}; - -export default connect( - [EditorContainer], - ({width, height, fps, originalFps, original}) => ({width, height, fps, originalFps, original}), - ({changeDimension, setFps}) => ({changeDimension, setFps}) -)(LeftOptions); diff --git a/renderer/components/editor/options/left.tsx b/renderer/components/editor/options/left.tsx new file mode 100644 index 000000000..c6b8e54ee --- /dev/null +++ b/renderer/components/editor/options/left.tsx @@ -0,0 +1,385 @@ +import css from 'styled-jsx/css'; +import KeyboardNumberInput from '../../keyboard-number-input'; +import Slider from './slider'; +import OptionsContainer from '../options-container'; +import {useState, useEffect, useMemo} from 'react'; +import * as stringMath from 'string-math'; +import VideoMetadataContainer from '../video-metadata-container'; +import {shake} from '../../../utils/inputs'; +import Select from './select'; + +const percentValues = [100, 75, 50, 33, 25, 20, 10]; + +const {className: keyboardInputClass, styles: keyboardInputStyles} = css.resolve` + height: 24px; + background: rgba(255, 255, 255, 0.1); + text-align: center; + font-size: 12px; + box-sizing: border-box; + border: none; + padding: 4px; + border-bottom-left-radius: 4px; + border-top-left-radius: 4px; + width: 48px; + color: white; + box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px 1px 2px 0px rgba(0, 0, 0, 0.2); + + input + input { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + border-bottom-right-radius: 4px; + border-top-right-radius: 4px; + margin-left: 1px; + margin-right: 16px; + } + + :focus, :hover { + outline: none; + background: hsla(0, 0%, 100%, 0.2); + } +`; + +const LeftOptions = () => { + const {width, height, setDimensions, fps, updateFps, originalFps} = OptionsContainer.useContainer(); + const metadata = VideoMetadataContainer.useContainer(); + + const [widthValue, setWidthValue] = useState(); + const [heightValue, setHeightValue] = useState(); + + const onChange = (event, {ignoreEmpty = true}: {ignoreEmpty?: boolean} = {}) => { + if (!ignoreEmpty) { + return onBlur(event); + } + + const {currentTarget: {name, value}} = event; + if (name === 'width') { + setWidthValue(value); + } else { + setHeightValue(value); + } + }; + + const onBlur = event => { + const {currentTarget} = event; + const {name} = currentTarget; + + let value: number; + try { + value = stringMath(currentTarget.value); + } catch {} + + // Fallback to last valid + const updates = {width, height}; + + if (value) { + value = Math.round(value); + const ratio = metadata.width / metadata.height; + console.log(ratio); + + if (name === 'width') { + const min = Math.max(1, Math.ceil(ratio)); + + if (value < min) { + shake(currentTarget, {className: 'shake-left'}); + updates.width = min; + } else if (value > metadata.width) { + shake(currentTarget, {className: 'shake-left'}); + updates.width = metadata.width; + } else { + updates.width = value; + } + + updates.height = Math[ratio > 1 ? 'ceil' : 'floor'](updates.width / ratio); + } else { + const min = Math.max(1, Math.ceil(1 / ratio)); + + if (value < min) { + shake(currentTarget, {className: 'shake-right'}); + updates.height = min; + } else if (value > metadata.height) { + shake(currentTarget, {className: 'shake-right'}); + updates.height = metadata.height; + } else { + updates.height = value; + } + + updates.width = Math[ratio > 1 ? 'floor' : 'ceil'](updates.height * ratio); + } + } else if (name === 'width') { + shake(currentTarget, {className: 'shake-left'}); + } else { + shake(currentTarget, {className: 'shake-right'}); + } + + setDimensions(updates); + setWidthValue(updates.width.toString()); + setHeightValue(updates.height.toString()); + }; + + useEffect(() => { + if (width && height) { + setWidthValue(width.toString()); + setHeightValue(height.toString()); + } + }, [width, height]); + + const percentOptions = useMemo(() => { + const ratio = metadata.width / metadata.height; + + const options = percentValues.map(percent => { + const adjustedWidth = Math.round(metadata.width * (percent / 100)); + const adjustedHeight = Math[ratio > 1 ? 'ceil' : 'floor'](adjustedWidth / ratio); + + return { + label: `${adjustedWidth} x ${adjustedHeight} (${percent === 100 ? 'Original' : `${percent}%`})`, + value: {width: adjustedWidth, height: adjustedHeight}, + checked: width === adjustedWidth + }; + }); + + if (options.every(opt => !opt.checked)) { + return [ + { + label: 'Custom', + value: {width, height}, + checked: true + }, + { + separator: true + }, + ...options + ]; + } + + return options; + }, [metadata, width]); + + const selectPercentage = updates => { + setDimensions(updates); + setWidthValue(updates.width.toString()); + setHeightValue(updates.height.toString()); + }; + + const percentLabel = `${Math.round((width / metadata.width) * 100)}%`; + + return ( +
+
Size
+ + +
+ -
- ) - } - - ) - } -
- -
- - -
- ); - } -} - -RightOptions.propTypes = { - options: PropTypes.arrayOf(PropTypes.object), - format: PropTypes.string, - plugin: PropTypes.string, - selectFormat: PropTypes.elementType, - selectPlugin: PropTypes.elementType, - startExport: PropTypes.elementType, - openWithApp: PropTypes.object, - selectOpenWithApp: PropTypes.elementType, - editPlugin: PropTypes.object, - editOptions: PropTypes.arrayOf(PropTypes.object), - selectEditPlugin: PropTypes.elementType, - openEditPluginConfig: PropTypes.elementType -}; - -export default connect( - [EditorContainer], - ({options, format, plugin, openWithApp, editOptions, editPlugin}) => ({options, format, plugin, openWithApp, editOptions, editPlugin}), - ({selectFormat, selectPlugin, startExport, selectOpenWithApp, selectEditPlugin, openEditPluginConfig}) => ({selectFormat, selectPlugin, startExport, selectOpenWithApp, selectEditPlugin, openEditPluginConfig}) -)(RightOptions); diff --git a/renderer/components/editor/options/right.tsx b/renderer/components/editor/options/right.tsx new file mode 100644 index 000000000..19a8290e3 --- /dev/null +++ b/renderer/components/editor/options/right.tsx @@ -0,0 +1,344 @@ +import {GearIcon} from '../../../vectors'; +import OptionsContainer from '../options-container'; +import Select from './select'; +import {ipcRenderer as ipc} from 'electron-better-ipc'; + +const FormatSelect = () => { + const {formats, format, updateFormat} = OptionsContainer.useContainer(); + const options = formats.map(format => ({label: format.prettyFormat, value: format.format})); + + return + + + + ); +} + +const RightOptions = () => { + return ( +
+ +
+ + +
+ ); +} + +export default RightOptions; + +// import electron from 'electron'; +// import React from 'react'; +// import PropTypes from 'prop-types'; + +// import {connect, EditorContainer} from '../../../containers'; +// import Select from './select'; +// import {GearIcon} from '../../../vectors'; + +// class RightOptions extends React.Component { +// render() { +// const { +// options, +// format, +// plugin, +// selectFormat, +// selectPlugin, +// startExport, +// openWithApp, +// selectOpenWithApp, +// selectEditPlugin, +// editOptions, +// editPlugin, +// openEditPluginConfig +// } = this.props; + +// const formatOptions = options ? options.map(({format, prettyFormat}) => ({value: format, label: prettyFormat})) : []; +// const pluginOptions = options ? options.find(option => option.format === format).plugins.map(plugin => { +// if (plugin.apps) { +// const submenu = plugin.apps.map(app => ({ +// label: app.isDefault ? `${app.name} (default)` : app.name, +// type: 'radio', +// checked: openWithApp && app.url === openWithApp.url, +// click: () => selectOpenWithApp(app), +// icon: electron.remote.nativeImage.createFromDataURL(app.icon).resize({width: 16, height: 16}) +// })); + +// if (plugin.apps[0].isDefault) { +// submenu.splice(1, 0, {type: 'separator'}); +// } + +// return { +// isBuiltIn: false, +// submenu, +// value: plugin.title, +// label: openWithApp ? openWithApp.name : '' +// }; +// } + +// return { +// type: openWithApp ? 'normal' : 'radio', +// value: plugin.title, +// label: plugin.title, +// isBuiltIn: plugin.pluginName.startsWith('_') +// }; +// }) : []; + +// if (pluginOptions.every(opt => opt.isBuiltIn)) { +// pluginOptions.push({ +// separator: true +// }, { +// type: 'normal', +// label: 'Get Plugins…', +// value: 'open-plugins' +// }); +// } + +// const editPluginOptions = editOptions && editOptions.map(option => ({label: option.title, value: option})); +// const buttonAction = editPlugin ? openEditPluginConfig : () => selectEditPlugin(editOptions[0]); + + // return ( + //
+ // { + // editPluginOptions && editPluginOptions.length > 0 && ( + // <> + // { + // (!editPlugin || editPlugin.hasConfig) && ( + // + // ) + // } + // { + // editPlugin && ( + //
+ // + //
+ //
+ // - { - isOpen && ( -
event.stopPropagation()}> - -
- -
-
- ) - } - -
- ); - } -} - -Slider.propTypes = { - value: PropTypes.number, - max: PropTypes.number, - min: PropTypes.number, - onChange: PropTypes.elementType -}; - -export default Slider; diff --git a/renderer/components/editor/options/slider.tsx b/renderer/components/editor/options/slider.tsx new file mode 100644 index 000000000..5f763c73f --- /dev/null +++ b/renderer/components/editor/options/slider.tsx @@ -0,0 +1,336 @@ +import {TooltipIcon} from '../../../vectors'; +import {useState, useEffect} from 'react'; +import {shake} from '../../../utils/inputs'; + +interface Props { + value: number; + onChange: (newValue: number) => void; + min: number; + max: number; +} + +const Slider = (props: Props) => { + const [isOpen, setIsOpen] = useState(false); + const [valueText, setValueText] = useState(props.value?.toString()); + + useEffect(() => { + setValueText(props.value?.toString()) + }, [props.value]); + + const onChange = event => { + setValueText(event.currentTarget.value); + }; + + const onBlur = event => { + const {currentTarget} = event; + const value = parseInt(currentTarget.value, 10); + + if (value && value >= props.min && value <= props.max) { + props.onChange(value); + setValueText(value.toString()); + } else if (!value) { + setValueText(props.value.toString()); + shake(currentTarget); + } else { + const newValue = Math.min(Math.max(value, props.min), props.max); + props.onChange(newValue); + setValueText(newValue.toString()); + shake(currentTarget); + } + }; + + const onSliderChange = event => { + const value = parseInt(event.currentTarget.value, 10); + props.onChange(value); + setValueText(value.toString()); + }; + + return ( +
+ {isOpen &&
setIsOpen(false)} />} + setIsOpen(true)} + /> + { + isOpen && ( +
event.stopPropagation()}> + setIsOpen(false)} + /> +
+ +
+
+ ) + } + +
+ ); +} + +export default Slider; + +// import React from 'react'; +// import PropTypes from 'prop-types'; + + + +// class Slider extends React.Component { +// state = { +// isOpen: false +// } + +// show = () => this.setState({isOpen: true}) + +// hide = () => this.setState({isOpen: false}) + +// handleChange = event => { +// const {onChange} = this.props; +// onChange(event.target.value, event.target); +// } + +// handleBlur = event => { +// const {onChange} = this.props; +// onChange(event.target.value, event.target, {ignoreEmpty: false}); +// } + +// render() { +// const {value, max, min} = this.props; +// const {isOpen} = this.state; + + // return ( + //
+ // { isOpen &&
} + // + // { + // isOpen && ( + //
event.stopPropagation()}> + // + //
+ // + //
+ //
+ // ) + // } + // + //
+ // ); +// } +// } + +// Slider.propTypes = { +// value: PropTypes.number, +// max: PropTypes.number, +// min: PropTypes.number, +// onChange: PropTypes.elementType +// }; + +// export default Slider; diff --git a/renderer/components/editor/video-controls-container.tsx b/renderer/components/editor/video-controls-container.tsx new file mode 100644 index 000000000..d21d612f6 --- /dev/null +++ b/renderer/components/editor/video-controls-container.tsx @@ -0,0 +1,110 @@ +import {createContainer} from 'unstated-next'; +import electron from 'electron'; +import {useRef, useState, useEffect} from 'react'; + +const useVideoControls = () => { + const videoRef = useRef(); + const currentWindow = electron.remote.getCurrentWindow(); + const wasPaused = useRef(true); + const transitioningPauseState = useRef>(); + + const [hasStarted, setHasStarted] = useState(false); + const [isMuted, setIsMuted] = useState(false); + const [isPaused, setIsPaused] = useState(true); + + const play = async () => { + if (videoRef.current?.paused) { + transitioningPauseState.current = videoRef.current.play(); + try { + await transitioningPauseState.current; + setIsPaused(false); + } catch {} + } + }; + + const pause = async () => { + if (videoRef.current && !videoRef.current.paused) { + try { + await transitioningPauseState.current; + } catch { + } finally { + videoRef.current.pause(); + setIsPaused(true); + } + } + }; + + const mute = () => { + setIsMuted(true); + videoRef.current.muted = true; + } + + const unmute = () => { + setIsMuted(false); + videoRef.current.muted = false; + } + + const setVideoRef = (video: HTMLVideoElement) => { + videoRef.current = video; + }; + + const videoProps = { + onCanPlayThrough: hasStarted ? undefined : () => { + setHasStarted(true); + if (currentWindow.isFocused()) { + play(); + } + }, + onLoadedData: () => { + const hasAudio = (videoRef.current as any).webkitAudioDecodedByteCount > 0 || Boolean( + (videoRef.current as any).audioTracks && + (videoRef.current as any).audioTracks.length > 0 + ); + + if (!hasAudio) { + mute(); + } + }, + onEnded: () => { + play(); + } + }; + + useEffect(() => { + const blurListener = () => { + wasPaused.current = videoRef.current?.paused; + if (!wasPaused.current) { + pause(); + } + } + + const focusListener = () => { + if (!wasPaused.current) { + play(); + } + } + + currentWindow.addListener('blur', blurListener); + currentWindow.addListener('focus', focusListener); + + return () => { + currentWindow.removeListener('blur', blurListener); + currentWindow.removeListener('focus', focusListener); + }; + }, []); + + return { + isPaused, + isMuted, + setVideoRef, + pause, + play, + mute, + unmute, + videoProps + }; +} + +const VideoControlsContainer = createContainer(useVideoControls); + +export default VideoControlsContainer; diff --git a/renderer/components/editor/video-metadata-container.tsx b/renderer/components/editor/video-metadata-container.tsx new file mode 100644 index 000000000..f7775d048 --- /dev/null +++ b/renderer/components/editor/video-metadata-container.tsx @@ -0,0 +1,49 @@ +import {createContainer} from 'unstated-next'; +import {useRef, useState} from 'react'; + +const useVideoMetadata = () => { + const videoRef = useRef(); + + const [width, setWidth] = useState(0); + const [height, setHeight] = useState(0); + const [hasAudio, setHasAudio] = useState(false); + const [duration, setDuration] = useState(0); + + const setVideoRef = (video: HTMLVideoElement) => { + videoRef.current = video; + }; + + const videoProps = { + onLoadedMetadata: () => { + setWidth(videoRef.current?.videoWidth); + setHeight(videoRef.current?.videoHeight); + setDuration(videoRef.current?.duration); + }, + onLoadedData: () => { + const hasAudio = (videoRef.current as any).webkitAudioDecodedByteCount > 0 || Boolean( + (videoRef.current as any).audioTracks && + (videoRef.current as any).audioTracks.length > 0 + ); + + if (!hasAudio) { + videoRef.current.muted = true; + } + + setHasAudio(hasAudio); + } + }; + + return { + width, + height, + hasAudio, + duration, + setVideoRef, + videoProps + }; +}; + +const VideoMetadataContainer = createContainer(useVideoMetadata); + +export default VideoMetadataContainer; + diff --git a/renderer/components/editor/video-player.js b/renderer/components/editor/video-player.js deleted file mode 100644 index fb5695e36..000000000 --- a/renderer/components/editor/video-player.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import classNames from 'classnames'; - -import Video from './video'; -import LeftControls from './controls/left'; -import RightControls from './controls/right'; -import PlayBar from './controls/play-bar'; - -export default class VideoPlayer extends React.Component { - render() { - const {hover} = this.props; - - const className = classNames('video-controls', {hover}); - - return ( -
-
- ); - } -} - -VideoPlayer.propTypes = { - hover: PropTypes.bool -}; diff --git a/renderer/components/editor/video-player.tsx b/renderer/components/editor/video-player.tsx new file mode 100644 index 000000000..458a53439 --- /dev/null +++ b/renderer/components/editor/video-player.tsx @@ -0,0 +1,131 @@ +import Video from './video'; +import LeftControls from './controls/left'; +import RightControls from './controls/right'; +import PlayBar from './controls/play-bar'; + +const VideoPlayer = () => { + return ( +
+
+ ); +} + +export default VideoPlayer; + + + + + + + + +// import PropTypes from 'prop-types'; +// import React from 'react'; +// import classNames from 'classnames'; + +// import Video from './video'; +// import LeftControls from './controls/left'; +// import RightControls from './controls/right'; +// import PlayBar from './controls/play-bar'; + +// export default class VideoPlayer extends React.Component { +// render() { +// const {hover} = this.props; + +// const className = classNames('video-controls', {hover}); + +// return ( +//
+//
+// ); +// } +// } + +// VideoPlayer.propTypes = { +// hover: PropTypes.bool +// }; diff --git a/renderer/components/editor/video-time-container.tsx b/renderer/components/editor/video-time-container.tsx new file mode 100644 index 000000000..b88592109 --- /dev/null +++ b/renderer/components/editor/video-time-container.tsx @@ -0,0 +1,79 @@ +import {createContainer} from 'unstated-next'; +import {useRef, useState, useEffect} from 'react'; + +const useVideoTime = () => { + const videoRef = useRef(); + + const [startTime, setStartTime] = useState(0); + const [endTime, setEndTime] = useState(0); + const [duration, setDuration] = useState(0); + const [currentTime, setCurrentTime] = useState(0); + + const setVideoRef = (video: HTMLVideoElement) => { + videoRef.current = video; + } + + const videoProps = { + onLoadedMetadata: () => { + setDuration(videoRef.current?.duration); + setEndTime(videoRef.current?.duration); + }, + onEnded: () => { + updateTime(startTime); + } + }; + + const updateTime = (time: number, ignoreElement = false) => { + if (time >= endTime && !videoRef.current.paused) { + videoRef.current.currentTime = startTime; + setCurrentTime(startTime) + } else { + if (!ignoreElement) { + videoRef.current.currentTime = time; + } + setCurrentTime(time); + } + }; + + const updateStartTime = (time: number) => { + if (time < endTime) { + videoRef.current.currentTime = time; + setStartTime(time); + setCurrentTime(time); + } + }; + + const updateEndTime = (time: number) => { + if (time > startTime) { + videoRef.current.currentTime = time; + setEndTime(time); + setCurrentTime(time); + } + }; + + useEffect(() => { + const interval = setInterval(() => { + updateTime(videoRef.current.currentTime, true); + }, 1000 / 30); + + return () => { + clearInterval(interval); + }; + }, [startTime, endTime]) + + return { + startTime, + endTime, + duration, + currentTime, + updateTime, + updateStartTime, + updateEndTime, + setVideoRef, + videoProps + }; +}; + +const VideoTimeContainer = createContainer(useVideoTime); + +export default VideoTimeContainer; diff --git a/renderer/components/editor/video.js b/renderer/components/editor/video.js deleted file mode 100644 index f4e41a5bb..000000000 --- a/renderer/components/editor/video.js +++ /dev/null @@ -1,86 +0,0 @@ -import electron from 'electron'; -import PropTypes from 'prop-types'; -import React from 'react'; - -import {connect, VideoContainer, EditorContainer} from '../../containers'; - -class Video extends React.Component { - constructor(props) { - super(props); - this.videoRef = React.createRef(); - } - - componentDidMount() { - const {remote} = electron; - const {Menu, MenuItem} = remote; - const {getSnapshot} = this.props; - - this.menu = new Menu(); - this.menu.append(new MenuItem({label: 'Snapshot', click: getSnapshot})); - } - - componentDidUpdate(previousProps) { - const {setVideo, src} = this.props; - - if (!previousProps.src && src) { - setVideo(this.videoRef.current); - } - } - - contextMenu = () => { - const {play, pause} = this.props; - const video = this.videoRef.current; - const wasPaused = video.paused; - - if (!wasPaused) { - pause(); - } - - this.menu.popup({ - callback: () => { - if (!wasPaused) { - play(); - } - } - }); - } - - render() { - const {src} = this.props; - - if (!src) { - return null; - } - - return ( -
-
- ); - } -} - -Video.propTypes = { - src: PropTypes.string, - setVideo: PropTypes.elementType, - getSnapshot: PropTypes.elementType, - play: PropTypes.elementType, - pause: PropTypes.elementType -}; - -export default connect( - [VideoContainer, EditorContainer], - ({src}) => ({src}), - ({setVideo, play, pause}, {getSnapshot}) => ({setVideo, getSnapshot, play, pause}) -)(Video); diff --git a/renderer/components/editor/video.tsx b/renderer/components/editor/video.tsx new file mode 100644 index 000000000..5d6192727 --- /dev/null +++ b/renderer/components/editor/video.tsx @@ -0,0 +1,156 @@ +import {useRef, useMemo, useEffect, RefObject} from 'react'; +import useWindowArgs from '../../hooks/window-args'; +import VideoTimeContainer from './video-time-container'; +import VideoMetadataContainer from './video-metadata-container'; +import VideoControlsContainer from './video-controls-container'; + +const getVideoProps = (propsArray: React.DetailedHTMLProps, HTMLVideoElement>[]) => { + const handlers = new Map(); + + for (const props of propsArray) { + for (const [key, handler] of Object.entries(props)) { + if (!handlers.has(key)) { + handlers.set(key, []); + } + + handlers.get(key).push(handler); + } + } + + return [...handlers.entries()].reduce((acc, [key, handlerList]) => ({ + ...acc, + [key]: () => { + for (const handler of handlerList) { + handler?.() + } + } + }), {}); +}; + +const Video = () => { + const videoRef = useRef(); + const {filePath} = useWindowArgs(); + const src = `file://${filePath}`; + + const videoTimeContainer = VideoTimeContainer.useContainer(); + const videoMetadataContainer = VideoMetadataContainer.useContainer(); + const videoControlsContainer = VideoControlsContainer.useContainer(); + + useEffect(() => { + videoTimeContainer.setVideoRef(videoRef.current); + videoMetadataContainer.setVideoRef(videoRef.current); + videoControlsContainer.setVideoRef(videoRef.current); + }, []); + + const videoProps = getVideoProps([ + videoTimeContainer.videoProps, + videoMetadataContainer.videoProps, + videoControlsContainer.videoProps + ]); + + return ( +
+
+ ); +} + +export default Video; + +// import electron from 'electron'; +// import PropTypes from 'prop-types'; +// import React, {useRef} from 'react'; + +// import {connect, VideoContainer, EditorContainer} from '../../containers'; + +// class Video extends React.Component { +// constructor(props) { +// super(props); +// this.videoRef = React.createRef(); +// } + +// componentDidMount() { +// const {remote} = electron; +// const {Menu, MenuItem} = remote; +// const {getSnapshot} = this.props; + +// this.menu = new Menu(); +// this.menu.append(new MenuItem({label: 'Snapshot', click: getSnapshot})); +// } + +// componentDidUpdate(previousProps) { +// const {setVideo, src} = this.props; + +// if (!previousProps.src && src) { +// setVideo(this.videoRef.current); +// } +// } + +// contextMenu = () => { +// const {play, pause} = this.props; +// const video = this.videoRef.current; +// const wasPaused = video.paused; + +// if (!wasPaused) { +// pause(); +// } + +// this.menu.popup({ +// callback: () => { +// if (!wasPaused) { +// play(); +// } +// } +// }); +// } + +// render() { +// const {src} = this.props; + +// if (!src) { +// return null; +// } + +// return ( +//
+//
+// ); +// } +// } + +// Video.propTypes = { +// src: PropTypes.string, +// setVideo: PropTypes.elementType, +// getSnapshot: PropTypes.elementType, +// play: PropTypes.elementType, +// pause: PropTypes.elementType +// }; + +// export default connect( +// [VideoContainer, EditorContainer], +// ({src}) => ({src}), +// ({setVideo, play, pause}, {getSnapshot}) => ({setVideo, getSnapshot, play, pause}) +// )(Video); diff --git a/renderer/components/keyboard-number-input.js b/renderer/components/keyboard-number-input.js index a9a903d3c..af8d53477 100644 --- a/renderer/components/keyboard-number-input.js +++ b/renderer/components/keyboard-number-input.js @@ -13,10 +13,10 @@ class KeyboardNumberInput extends React.Component { } render() { - const {onKeyDown, min, max, ...rest} = this.props; + const {onChange, min, max, ...rest} = this.props; return ( - + ); } } diff --git a/renderer/components/traffic-lights.js b/renderer/components/traffic-lights.tsx similarity index 51% rename from renderer/components/traffic-lights.js rename to renderer/components/traffic-lights.tsx index 85bb5d94b..9f346ce45 100644 --- a/renderer/components/traffic-lights.js +++ b/renderer/components/traffic-lights.tsx @@ -1,64 +1,50 @@ -import electron from 'electron'; -import React from 'react'; - -export default class TrafficLights extends React.Component { - state = { - tint: 'blue' - }; - - componentDidMount() { - this.tintSubscription = electron.remote.systemPreferences.subscribeNotification('AppleAquaColorVariantChanged', this.onTintChange); - this.setState({tint: this.getTintColor()}); - } - - componentWillUnmount() { - electron.remote.systemPreferences.unsubscribeNotification(this.tintSubscription); - } - - getTintColor = () => electron.remote.systemPreferences.getUserDefault('AppleAquaColorVariant', 'string') === '6' ? 'graphite' : 'blue'; - - onTintChange = () => { - this.setState({tint: this.getTintColor()}); - } - - close = () => { - electron.remote.BrowserWindow.getFocusedWindow().close(); - } - - minimize = () => { - electron.remote.BrowserWindow.getFocusedWindow().minimize(); - } - - maximize = () => { - // TODO: When we get to Electron 4 use this API https://github.com/electron/electron/commit/a42ca9eecc6e82c087604f92a3e6581de66ece5a - const win = electron.remote.BrowserWindow.getFocusedWindow(); - win.setFullScreen(!win.isFullScreen()); - } - - render() { - return ( -
-
- - - - - -
-
- - - - -
-
- - - - - -
- -
- ); - } -} +
+ ); +}; + +export default TrafficLights; diff --git a/renderer/hooks/dark-mode.tsx b/renderer/hooks/dark-mode.tsx new file mode 100644 index 000000000..212fadb75 --- /dev/null +++ b/renderer/hooks/dark-mode.tsx @@ -0,0 +1,16 @@ +import {useState, useEffect} from 'react'; + +const useDarkMode = () => { + const {darkMode} = require('electron-util'); + const [isDarkMode, setIsDarkMode] = useState(darkMode.isEnabled); + + useEffect(() => { + return darkMode.onChange(() => { + setIsDarkMode(darkMode.isEnabled); + }); + }, []); + + return isDarkMode; +}; + +export default useDarkMode; diff --git a/renderer/hooks/window-args.tsx b/renderer/hooks/window-args.tsx new file mode 100644 index 000000000..7bf9f0e4f --- /dev/null +++ b/renderer/hooks/window-args.tsx @@ -0,0 +1,24 @@ +import {createContext, useContext, useState, useEffect, ReactNode} from 'react'; +import {ipcRenderer as ipc} from 'electron-better-ipc'; + +const ArgsContext = createContext(undefined); + +export const WindowArgsProvider = (props: {children: ReactNode}) => { + const [args, setArgs] = useState(); + + useEffect(() => { + return ipc.answerMain('kap-window-args', (newArgs: any) => { + setArgs(newArgs); + }); + }, []); + + return ( + + {props.children} + + ); +}; + +const useWindowArgs = () => useContext(ArgsContext); + +export default useWindowArgs; diff --git a/renderer/next-env.d.ts b/renderer/next-env.d.ts new file mode 100644 index 000000000..7b7aa2c77 --- /dev/null +++ b/renderer/next-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/renderer/next.config.js b/renderer/next.config.js index 302032f82..94cbe839a 100644 --- a/renderer/next.config.js +++ b/renderer/next.config.js @@ -4,11 +4,11 @@ exports.webpack = config => Object.assign(config, { plugins: config.plugins.filter(p => p.constructor.name !== 'UglifyJsPlugin') }); -exports.exportPathMap = () => ({ - '/cropper': {page: '/cropper'}, - '/editor': {page: '/editor'}, - '/preferences': {page: '/preferences'}, - '/exports': {page: '/exports'}, - '/config': {page: '/config'}, - '/dialog': {page: '/dialog'} -}); +// exports.exportPathMap = () => ({ +// '/cropper': {page: '/cropper'}, +// '/editor': {page: '/editor'}, +// '/preferences': {page: '/preferences'}, +// '/exports': {page: '/exports'}, +// '/config': {page: '/config'}, +// '/dialog': {page: '/dialog'} +// }); diff --git a/renderer/pages/_app.tsx b/renderer/pages/_app.tsx new file mode 100644 index 000000000..0ceaec8d2 --- /dev/null +++ b/renderer/pages/_app.tsx @@ -0,0 +1,39 @@ +import {AppProps} from 'next/app'; +import {useState, useEffect} from 'react'; +import useDarkMode from '../hooks/dark-mode'; +import GlobalStyles from '../utils/global-styles'; +import SentryErrorBoundary from '../utils/sentry-error-boundary'; +import {WindowArgsProvider} from '../hooks/window-args'; +import classNames from 'classnames'; + +function Kap(props: AppProps) { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + if (!isMounted) { + return null; + } + + return +} + +const MainApp = ({Component, pageProps}: AppProps) => { + const isDarkMode = useDarkMode(); + const className = classNames('cover-window', {dark: isDarkMode}); + + return ( +
+ + + + + + +
+ ); +}; + +export default Kap; diff --git a/renderer/pages/editor.js b/renderer/pages/editor.js index f60d8e1ad..5bd66b4bc 100644 --- a/renderer/pages/editor.js +++ b/renderer/pages/editor.js @@ -1,202 +1,203 @@ -import React from 'react'; -import Head from 'next/head'; -import {Provider} from 'unstated'; -import {ipcRenderer as ipc} from 'electron-better-ipc'; - -import Editor from '../components/editor'; -import Options from '../components/editor/options'; -import {EditorContainer, VideoContainer} from '../containers'; - -const editorContainer = new EditorContainer(); -const videoContainer = new VideoContainer(); - -videoContainer.setEditorContainer(editorContainer); -editorContainer.setVideoContainer(videoContainer); - -export default class EditorPage extends React.Component { - wasPaused = false; - - componentDidMount() { - ipc.answerMain('file', async ({filePath, fps, originalFilePath, isNewRecording, recordingName, title}) => { - await new Promise((resolve, reject) => { - editorContainer.mount({filePath, fps: Number.parseInt(fps, 10), originalFilePath, isNewRecording, recordingName, title}, resolve, reject); - }); - return true; - }); - - ipc.answerMain('export-options', editorContainer.setOptions); - ipc.answerMain('save-original', editorContainer.saveOriginal); - - ipc.answerMain('blur', () => { - this.wasPaused = videoContainer.state.isPaused; - videoContainer.pause(); - }); - ipc.answerMain('focus', () => { - if (!this.wasPaused) { - videoContainer.play(); - } - }); - } - - render() { - return ( -
- - - -
- -
- -
-
- -
-
-
- -
- ); - } -} +export default () =>
Hi
+// import React from 'react'; +// import Head from 'next/head'; +// import {Provider} from 'unstated'; +// import {ipcRenderer as ipc} from 'electron-better-ipc'; + +// import Editor from '../components/editor'; +// import Options from '../components/editor/options'; +// import {EditorContainer, VideoContainer} from '../containers'; + +// const editorContainer = new EditorContainer(); +// const videoContainer = new VideoContainer(); + +// videoContainer.setEditorContainer(editorContainer); +// editorContainer.setVideoContainer(videoContainer); + +// export default class EditorPage extends React.Component { +// wasPaused = false; + +// componentDidMount() { +// ipc.answerMain('file', async ({filePath, fps, originalFilePath, isNewRecording, recordingName, title}) => { +// await new Promise((resolve, reject) => { +// editorContainer.mount({filePath, fps: Number.parseInt(fps, 10), originalFilePath, isNewRecording, recordingName, title}, resolve, reject); +// }); +// return true; +// }); + +// ipc.answerMain('export-options', editorContainer.setOptions); +// ipc.answerMain('save-original', editorContainer.saveOriginal); + +// ipc.answerMain('blur', () => { +// this.wasPaused = videoContainer.state.isPaused; +// videoContainer.pause(); +// }); +// ipc.answerMain('focus', () => { +// if (!this.wasPaused) { +// videoContainer.play(); +// } +// }); +// } + +// render() { +// return ( +//
+// +// +// +//
+// +//
+// +//
+//
+// +//
+//
+//
+// +//
+// ); +// } +// } diff --git a/renderer/pages/editor2.tsx b/renderer/pages/editor2.tsx new file mode 100644 index 000000000..e0f51df94 --- /dev/null +++ b/renderer/pages/editor2.tsx @@ -0,0 +1,74 @@ +import useWindowArgs from '../hooks/window-args'; +import Head from 'next/head'; +import EditorPreview from '../components/editor/editor-preview'; +import combineUnstatedContainers from '../utils/combine-unstated-containers'; +import VideoMetadataContainer from '../components/editor/video-metadata-container'; +import VideoTimeContainer from '../components/editor/video-time-container'; +import VideoControlsContainer from '../components/editor/video-controls-container'; +import OptionsContainer from '../components/editor/options-container'; + +const ContainerProvider = combineUnstatedContainers([ + OptionsContainer, + VideoMetadataContainer, + VideoTimeContainer, + VideoControlsContainer +]); + +const Editor = () => { + const args = useWindowArgs(); + + console.log('HERE', args); + + if (!args) { + return null; + } + + return ( +
+ + + + + + + +
+ ); +}; + +export default Editor; diff --git a/renderer/tsconfig.json b/renderer/tsconfig.json new file mode 100644 index 000000000..e5a057a66 --- /dev/null +++ b/renderer/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "downlevelIteration": true + }, + "exclude": [ + "node_modules" + ], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx" + ] +} diff --git a/renderer/utils/combine-unstated-containers.tsx b/renderer/utils/combine-unstated-containers.tsx new file mode 100644 index 000000000..600a9c0fa --- /dev/null +++ b/renderer/utils/combine-unstated-containers.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import {Container} from 'unstated-next'; + +type ContainerOrWithInitialState = Container | [Container, T]; + +const combineUnstatedContainers = (containers: ContainerOrWithInitialState[]) => ({children}: React.PropsWithChildren<{}>) => { + return containers.reduce( + (tree, ContainerOrWithInitialState) => { + if (Array.isArray(ContainerOrWithInitialState)) { + const [Container, initialState] = ContainerOrWithInitialState; + return {tree} + } else { + return {tree} + } + }, + children as React.ReactElement + ); +}; + +export default combineUnstatedContainers; diff --git a/renderer/pages/_app.js b/renderer/utils/global-styles.tsx similarity index 53% rename from renderer/pages/_app.js rename to renderer/utils/global-styles.tsx index f19610515..70be2c773 100644 --- a/renderer/pages/_app.js +++ b/renderer/utils/global-styles.tsx @@ -1,113 +1,46 @@ -import electron from 'electron'; -import React from 'react'; -import App from 'next/app'; -import * as Sentry from '@sentry/browser'; - -const SENTRY_PUBLIC_DSN = 'https://2dffdbd619f34418817f4db3309299ce@sentry.io/255536'; - -const remote = electron.remote || false; - -const systemColorNames = [ - 'control-background', - 'control', - 'control-text', - 'disabled-control-text', - 'find-highlight', - 'grid', - 'header-text', - 'highlight', - 'keyboard-focus-indicator', - 'label', - 'link', - 'placeholder-text', - 'quaternary-label', - 'scrubber-textured-background', - 'secondary-label', - 'selected-content-background', - 'selected-control', - 'selected-control-text', - 'selected-menu-item-text', - 'selected-text-background', - 'selected-text', - 'separator', - 'shadow', - 'tertiary-label', - 'text-background', - 'text', - 'under-page-background', - 'unemphasized-selected-content-background', - 'unemphasized-selected-text-background', - 'unemphasized-selected-text', - 'window-background', - 'window-frame-text' -]; - -export default class Kap extends App { - state = {isDark: false} - - constructor(...args) { - super(...args); - - if (remote) { - // TODO: When we disable SSR, this can be a normal import - const {is, darkMode, api} = require('electron-util'); - const settings = remote.require('./common/settings'); - this.systemPreferences = api.systemPreferences; - - if (!is.development && settings.get('allowAnalytics')) { - const release = `${api.app.name}@${api.app.getVersion()}`.toLowerCase(); - Sentry.init({dsn: SENTRY_PUBLIC_DSN, release}); +import {useState, useEffect, useMemo} from 'react' +import useDarkMode from '../hooks/dark-mode'; +import {remote} from 'electron'; + +const GlobalStyles = () => { + const [accentColor, setAccentColor] = useState(remote.systemPreferences.getAccentColor()); + const isDarkMode = useDarkMode(); + + const systemColors = useMemo(() => { + return systemColorNames + .map(name => `--system-${name}: ${remote.systemPreferences.getColor(name as any)};`) + .join('\n'); + }, [isDarkMode]); + + const updateAccentColor = (_, accentColor) => setAccentColor(accentColor); + + useEffect(() => { + remote.systemPreferences.on('accent-color-changed', updateAccentColor); + + // return () => { + // api.systemPreferences.off('accent-color-changed', updateAccentColor); + // }; + }, []); + + return ( + -
- ); - } + + @keyframes shake-left { + 10%, + 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, + 80% { + transform: translate3d(0, 0, 0); + } + + 30%, + 50%, + 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, + 60% { + transform: translate3d(0, 0, 0); + } + } + + @keyframes shake-right { + 10%, + 90% { + transform: translate3d(1px, 0, 0); + } + + 20%, + 80% { + transform: translate3d(0, 0, 0); + } + + 30%, + 50%, + 70% { + transform: translate3d(4px, 0, 0); + } + + 40%, + 60% { + transform: translate3d(0, 0, 0); + } + } + + .shake-left { + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + perspective: 1000px; + animation: shake-left 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; + } + + .shake-right { + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + perspective: 1000px; + animation: shake-right 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; + } + + @keyframes shake { + 10%, + 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, + 80% { + transform: translate3d(2px, 0, 0); + } + + 30%, + 50%, + 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, + 60% { + transform: translate3d(4px, 0, 0); + } + } + + .shake { + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + perspective: 1000px; + animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; + } + + * { box-sizing: border-box; } + `} + ) } + +export default GlobalStyles; + +const systemColorNames = [ + 'control-background', + 'control', + 'control-text', + 'disabled-control-text', + 'find-highlight', + 'grid', + 'header-text', + 'highlight', + 'keyboard-focus-indicator', + 'label', + 'link', + 'placeholder-text', + 'quaternary-label', + 'scrubber-textured-background', + 'secondary-label', + 'selected-content-background', + 'selected-control', + 'selected-control-text', + 'selected-menu-item-text', + 'selected-text-background', + 'selected-text', + 'separator', + 'shadow', + 'tertiary-label', + 'text-background', + 'text', + 'under-page-background', + 'unemphasized-selected-content-background', + 'unemphasized-selected-text-background', + 'unemphasized-selected-text', + 'window-background', + 'window-frame-text' +]; diff --git a/renderer/utils/inputs.js b/renderer/utils/inputs.js index 114beea54..ef5685c17 100644 --- a/renderer/utils/inputs.js +++ b/renderer/utils/inputs.js @@ -177,8 +177,16 @@ const handleInputKeyPress = (onChange, min, max) => event => { return onChange(event, {ignoreEmpty: false}); } + // Don't let shift key lock aspect ratio + if (event.key === 'Shift') { + event.stopPropagation(); + } + const multiplier = event.shiftKey ? 10 : 1; const parsedValue = Number.parseInt(event.currentTarget.value, 10); + if (parsedValue === NaN) { + return; + } // Fake an onChange event if (event.key === 'ArrowUp') { @@ -188,11 +196,6 @@ const handleInputKeyPress = (onChange, min, max) => event => { event.currentTarget.value = `${Math.max(parsedValue - multiplier, min)}`; onChange(event); } - - // Don't let shift key lock aspect ratio - if (event.key === 'Shift') { - event.stopPropagation(); - } }; const handleKeyboardActivation = (onClick, {isMenu} = {}) => event => { diff --git a/renderer/utils/sentry-error-boundary.tsx b/renderer/utils/sentry-error-boundary.tsx new file mode 100644 index 000000000..0c4bae91e --- /dev/null +++ b/renderer/utils/sentry-error-boundary.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import * as Sentry from '@sentry/browser'; +import electron from 'electron'; + +const SENTRY_PUBLIC_DSN = 'https://2dffdbd619f34418817f4db3309299ce@sentry.io/255536'; + +class SentryErrorBoundary extends React.Component<{children: React.ReactNode}> { + constructor(props) { + super(props); + const settings = electron.remote.require('./common/settings'); + // Done in-line because this is used in _app + const {is, api} = require('electron-util'); + + if (!is.development && settings.get('allowAnalytics')) { + const release = `${api.app.name}@${api.app.getVersion()}`.toLowerCase(); + console.log('Did it', release); + Sentry.init({dsn: SENTRY_PUBLIC_DSN, release}); + } + } + + componentDidCatch(error, errorInfo) { + console.log(error, errorInfo); + Sentry.configureScope(scope => { + for (const [key, value] of Object.entries(errorInfo)) { + scope.setExtra(key, value); + } + }); + + Sentry.captureException(error); + + // This is needed to render errors correctly in development / production + super.componentDidCatch(error, errorInfo); + } + + render() { + return this.props.children; + } +} + +export default SentryErrorBoundary; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..aa506698b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "@sindresorhus/tsconfig", + "compilerOptions": { + "outDir": "dist-js", + "target": "es2019", + "module": "commonjs", + "esModuleInterop": true, + "downlevelIteration": true, + "allowJs": true, + "sourceMap": true, + "inlineSources": true, + "lib": [ + "esnext" + ] + }, + "include": [ + "main/**/*" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/yarn.lock b/yarn.lock index 433d867f9..c531ef11f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1391,6 +1391,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/to-milliseconds/-/to-milliseconds-1.2.0.tgz#3453e58b9cc973ccc98fe571dbb7a1d9737f06e7" integrity sha512-nHpLEF6oRZJZ0ym8hmxz4jeSdnOqwWd5GC75GNQqNjfSG1IY55RE3AaGEC/QUDElLTuaPSBVa1rnV/C/rUkAUw== +"@sindresorhus/tsconfig@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/tsconfig/-/tsconfig-0.7.0.tgz#8388e864b5e6d72547175ed98152b736df39b20a" + integrity sha512-on7+0FFpUH7+g5iLFkmk0oruqzE6uUnpKK0D6HM637aIvQ/mlXz33VlamsS4TSU6pifWSNIrUxrNKjAReU2Juw== + "@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.7.2": version "1.8.0" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d" @@ -1500,11 +1505,24 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/prop-types@*": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + "@types/q@^1.5.1": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== +"@types/react@^16.9.46": + version "16.9.46" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.46.tgz#f0326cd7adceda74148baa9bff6e918632f5069e" + integrity sha512-dbHzO3aAq1lB3jRQuNpuZ/mnu+CdD3H0WVaaBQA8LTT3S33xhVBUj232T8M3tAhSWJs/D/UqORYUlJNl/8VQZg== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@types/semver@^7.1.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.2.0.tgz#0d72066965e910531e1db4621c15d0ca36b8d83b" @@ -3621,6 +3639,11 @@ csso@^4.0.2: dependencies: css-tree "1.0.0-alpha.39" +csstype@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.2.tgz#ee5ff8f208c8cd613b389f7b222c9801ca62b3f7" + integrity sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw== + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -4077,10 +4100,10 @@ ejs@^3.1.2: dependencies: jake "^10.6.1" -electron-better-ipc@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/electron-better-ipc/-/electron-better-ipc-1.0.1.tgz#ec5f397d44ce1225d826461ff93881912fe68490" - integrity sha512-4okAJoeY2+NXsPLYE+ccVn2NSDRCqMAAV1Kp5YzNRGki505p9nXuM6+d2QAlZ1OWyQxXmDm0B+eezjQL55uF/A== +electron-better-ipc@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/electron-better-ipc/-/electron-better-ipc-1.1.0.tgz#0419c39b57fbf03e78f07c46d5331c4d2ad85a1b" + integrity sha512-f+vNMAmSOWycmmW6+DCGkq5Y8Cgod9RyvgCKVmci+dxK+xF5iUAXJlriLJM5CKhxeC6b8dI52vkblXaeHsTGlA== dependencies: serialize-error "^5.0.0" @@ -10789,6 +10812,11 @@ typescript@^3.0.0: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a" integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ== +typescript@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5" + integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg== + uc.micro@^1.0.1: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" @@ -10901,6 +10929,11 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +unstated-next@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unstated-next/-/unstated-next-1.1.0.tgz#7bb4911a12fdf3cc8ad3eb11a0b315e4a8685ea8" + integrity sha512-AAn47ZncPvgBGOvMcn8tSRxsrqwf2VdAPxLASTuLJvZt4rhKfDvUkmYZLGfclImSfTVMv7tF4ynaVxin0JjDCA== + unstated@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/unstated/-/unstated-1.2.0.tgz#5c57cc077473d2cce411ec0930da285cef3df306" From b73a37a61cb2379ceaa0f7242b64e7b4d71fc1c6 Mon Sep 17 00:00:00 2001 From: George Karagkiaouris Date: Thu, 5 Nov 2020 12:00:48 -0500 Subject: [PATCH 02/14] WIP --- main/common/{analytics.js => analytics.ts} | 20 +- main/common/aperture.js | 2 +- main/common/constants.js | 15 - main/common/constants.ts | 14 + main/common/plugins.js | 618 +-- main/common/remote-state.d.ts | 50 - main/common/remote-state.js | 108 - main/common/settings.ts | 3 +- main/common/types.ts | 15 + main/conversion.ts | 203 + main/convert.js | 2 +- main/converters/h264.ts | 11 +- main/converters/index.ts | 5 +- main/cropper.js | 2 +- main/editor.js | 9 +- main/export-list.js | 2 +- main/export-options.js | 342 +- main/export.ts | 60 + main/{export.js => export1.js} | 0 main/global-accelerators.js | 2 +- main/index.js | 15 +- main/kap-window.js | 72 - main/kap-window.ts | 124 + main/menus.js | 2 +- .../copy-to-clipboard-plugin.js | 0 main/plugins/built-in/open-with-plugin.ts | 48 + .../{ => built-in}/save-file-plugin.js | 0 main/plugins/config.ts | 84 + main/plugins/index.ts | 209 + main/plugins/open-with-plugin.js | 38 - main/plugins/plugin.ts | 169 + main/plugins/service-context.ts | 100 + main/plugins/service.ts | 27 + main/recording-history.js | 2 +- main/remote-states/conversions/conversion.js | 50 - main/remote-states/conversions/index.js | 0 main/remote-states/conversions/video.js | 70 - main/remote-states/editor-options.js | 168 - main/remote-states/editor-options.ts | 159 + main/remote-states/index.js | 9 - main/remote-states/index.ts | 10 + main/remote-states/setup-remote-state.ts | 44 + main/remote-states/use-remote-state.ts | 60 + main/remote-states/utils.ts | 14 + main/utils/ajv.ts | 18 +- main/utils/devices.js | 2 +- main/utils/{encoding.js => encoding.ts} | 28 +- main/utils/errors.js | 5 +- main/utils/formats.ts | 13 + main/utils/fps.js | 13 - main/utils/fps.ts | 16 + main/utils/notifications.ts | 70 + main/utils/sentry.js | 21 - main/utils/sentry.ts | 20 + ...elerator.js => shortcut-to-accelerator.ts} | 5 +- main/utils/timestamped-name.js | 6 - main/utils/timestamped-name.ts | 3 + main/video.ts | 123 + package.json | 31 +- renderer/common/remote-state-types.ts | 52 + renderer/common/types.ts | 43 + .../components/editor/controls/preview.tsx | 4 +- .../components/editor/conversion/index.tsx | 5 + renderer/components/editor/editor-options.tsx | 5 - renderer/components/editor/editor-preview.tsx | 4 +- renderer/components/editor/index.js | 88 - renderer/components/editor/index.tsx | 110 + .../components/editor/options-container.tsx | 55 +- renderer/components/editor/options/left.tsx | 4 +- renderer/components/editor/options/right.tsx | 106 +- renderer/components/editor/options/select.tsx | 56 +- renderer/components/editor/video.tsx | 4 +- renderer/containers/action-bar.js | 2 +- renderer/containers/cropper.js | 2 +- renderer/containers/preferences.js | 6 +- renderer/hooks/editor/use-conversion.tsx | 36 + renderer/hooks/editor/use-editor-options.tsx | 17 + .../hooks/editor/use-editor-window-state.tsx | 14 + renderer/hooks/editor/use-share-plugins.tsx | 84 + renderer/hooks/use-remote-state.tsx | 74 + renderer/hooks/window-args.tsx | 24 - renderer/hooks/window-state.tsx | 25 + renderer/next.config.js | 93 +- renderer/pages/_app.tsx | 14 +- renderer/pages/editor2.tsx | 19 +- renderer/tsconfig.json | 10 +- renderer/utils/sentry-error-boundary.tsx | 2 +- test/convert.js | 2 +- test/recording-history.js | 2 +- yarn.lock | 3628 ++++++++--------- 90 files changed, 4690 insertions(+), 3231 deletions(-) rename main/common/{analytics.js => analytics.ts} (56%) delete mode 100644 main/common/constants.js create mode 100644 main/common/constants.ts delete mode 100644 main/common/remote-state.d.ts delete mode 100644 main/common/remote-state.js create mode 100644 main/common/types.ts create mode 100644 main/conversion.ts create mode 100644 main/export.ts rename main/{export.js => export1.js} (100%) delete mode 100644 main/kap-window.js create mode 100644 main/kap-window.ts rename main/plugins/{ => built-in}/copy-to-clipboard-plugin.js (100%) create mode 100644 main/plugins/built-in/open-with-plugin.ts rename main/plugins/{ => built-in}/save-file-plugin.js (100%) create mode 100644 main/plugins/config.ts create mode 100644 main/plugins/index.ts delete mode 100644 main/plugins/open-with-plugin.js create mode 100644 main/plugins/plugin.ts create mode 100644 main/plugins/service-context.ts create mode 100644 main/plugins/service.ts delete mode 100644 main/remote-states/conversions/conversion.js delete mode 100644 main/remote-states/conversions/index.js delete mode 100644 main/remote-states/conversions/video.js delete mode 100644 main/remote-states/editor-options.js create mode 100644 main/remote-states/editor-options.ts delete mode 100644 main/remote-states/index.js create mode 100644 main/remote-states/index.ts create mode 100644 main/remote-states/setup-remote-state.ts create mode 100644 main/remote-states/use-remote-state.ts create mode 100644 main/remote-states/utils.ts rename main/utils/{encoding.js => encoding.ts} (54%) create mode 100644 main/utils/formats.ts delete mode 100644 main/utils/fps.js create mode 100644 main/utils/fps.ts create mode 100644 main/utils/notifications.ts delete mode 100644 main/utils/sentry.js create mode 100644 main/utils/sentry.ts rename main/utils/{shortcut-to-accelerator.js => shortcut-to-accelerator.ts} (80%) delete mode 100644 main/utils/timestamped-name.js create mode 100644 main/utils/timestamped-name.ts create mode 100644 main/video.ts create mode 100644 renderer/common/remote-state-types.ts create mode 100644 renderer/common/types.ts create mode 100644 renderer/components/editor/conversion/index.tsx delete mode 100644 renderer/components/editor/editor-options.tsx delete mode 100644 renderer/components/editor/index.js create mode 100644 renderer/components/editor/index.tsx create mode 100644 renderer/hooks/editor/use-conversion.tsx create mode 100644 renderer/hooks/editor/use-editor-options.tsx create mode 100644 renderer/hooks/editor/use-editor-window-state.tsx create mode 100644 renderer/hooks/editor/use-share-plugins.tsx create mode 100644 renderer/hooks/use-remote-state.tsx delete mode 100644 renderer/hooks/window-args.tsx create mode 100644 renderer/hooks/window-state.tsx diff --git a/main/common/analytics.js b/main/common/analytics.ts similarity index 56% rename from main/common/analytics.js rename to main/common/analytics.ts index 9ffbdd5e2..08baca5a8 100644 --- a/main/common/analytics.js +++ b/main/common/analytics.ts @@ -1,25 +1,26 @@ 'use strict'; -const util = require('electron-util'); +import util from 'electron-util'; +import {parse} from 'semver'; +import settings from './settings'; + const Insight = require('insight'); -const {parse} = require('semver'); const pkg = require('../../package'); -const settings = require('./settings'); const trackingCode = 'UA-84705099-2'; const insight = new Insight({trackingCode, pkg}); const version = parse(pkg.version); -const track = (...paths) => { +export const track = (...paths: string[]) => { const allowAnalytics = settings.get('allowAnalytics'); if (allowAnalytics) { - console.log('Tracking', `v${version.major}.${version.minor}`, ...paths); - insight.track(`v${version.major}.${version.minor}`, ...paths); + console.log('Tracking', `v${version?.major}.${version?.minor}`, ...paths); + insight.track(`v${version?.major}.${version?.minor}`, ...paths); } }; -const initializeAnalytics = () => { +export const initializeAnalytics = () => { if (util.isFirstAppLaunch()) { insight.track('install'); } @@ -29,8 +30,3 @@ const initializeAnalytics = () => { settings.set('version', pkg.version); } }; - -module.exports = { - initializeAnalytics, - track -}; diff --git a/main/common/aperture.js b/main/common/aperture.js index 61563ac24..61c3ae146 100644 --- a/main/common/aperture.js +++ b/main/common/aperture.js @@ -11,7 +11,7 @@ const {setCropperShortcutAction} = require('../global-accelerators'); // eslint-disable-next-line no-unused-vars const {convertToH264} = require('../utils/encoding'); -const settings = require('./settings'); +const settings = require('./settings').default; const {track} = require('./analytics'); const plugins = require('./plugins'); const {getAudioDevices} = require('../utils/devices'); diff --git a/main/common/constants.js b/main/common/constants.js deleted file mode 100644 index 2cd79833f..000000000 --- a/main/common/constants.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -const supportedVideoExtensions = ['mp4', 'mov', 'm4v']; - -const formatExtensions = new Map([ - ['av1', 'mp4'] -]); - -const getFormatExtension = format => formatExtensions.get(format) || format; - -module.exports = { - supportedVideoExtensions, - getFormatExtension, - defaultInputDeviceId: 'SYSTEM_DEFAULT' -}; diff --git a/main/common/constants.ts b/main/common/constants.ts new file mode 100644 index 000000000..876d2ac7d --- /dev/null +++ b/main/common/constants.ts @@ -0,0 +1,14 @@ +import {Format} from './types'; + +export const supportedVideoExtensions = ['mp4', 'mov', 'm4v']; + +const formatExtensions = new Map([ + ['av1', 'mp4'] +]); + +export const formats = [Format.mp4, Format.av1, Format.gif, Format.apng, Format.webm]; + +export const getFormatExtension = (format: Format) => formatExtensions.get(format) ?? format; + +export const defaultInputDeviceId = 'SYSTEM_DEFAULT'; + diff --git a/main/common/plugins.js b/main/common/plugins.js index 73f856c56..af84e0cee 100644 --- a/main/common/plugins.js +++ b/main/common/plugins.js @@ -1,309 +1,309 @@ -'use strict'; - -const path = require('path'); -const fs = require('fs'); -const electron = require('electron'); -const got = require('got'); -const execa = require('execa'); -const makeDir = require('make-dir'); -const packageJson = require('package-json'); - -const {app, Notification} = electron; - -const {refreshRecordPluginItems} = require('../menus'); -const {openConfigWindow} = require('../config'); -const {openPrefsWindow} = require('../preferences'); -const {notify} = require('./notifications'); -const {track} = require('./analytics'); -const {InstalledPlugin, NpmPlugin, recordPluginServiceState} = require('../plugin'); -const {showError} = require('../utils/errors'); -const {EventEmitter} = require('events'); - -// Need to persist the notification, otherwise it is garbage collected and the actions don't trigger -// https://github.com/electron/electron/issues/12690 -let pluginNotification; - -class Plugins extends EventEmitter { - constructor() { - super(); - this.yarnBin = path.join(__dirname, '../../node_modules/yarn/bin/yarn.js'); - this._makePluginsDir(); - this.appVersion = app.getVersion(); - } - - setUpdateExportOptions(updateExportOptions) { - this.updateExportOptions = updateExportOptions; - } - - async enableService(service, plugin) { - const wasEnabled = recordPluginServiceState.get(service.title) || false; - - if (wasEnabled) { - recordPluginServiceState.set(service.title, false); - return this.refreshRecordPluginServices(); - } - - if (!plugin.config.validServices.includes(service.title)) { - openPrefsWindow({target: {name: plugin.name, action: 'configure'}}); - return; - } - - if (service.willEnable) { - try { - const canEnable = await service.willEnable(); - - if (canEnable) { - recordPluginServiceState.set(service.title, true); - } - } catch (error) { - showError(error, {title: `Something went wrong while enabling “${service.title}”`}); - const Sentry = require('./utils/sentry'); - Sentry.captureException(error); - } - - this.refreshRecordPluginServices(); - return; - } - - recordPluginServiceState.set(service.title, true); - this.refreshRecordPluginServices(); - } - - refreshRecordPluginServices = () => { - refreshRecordPluginItems( - this.getRecordingPlugins().flatMap( - plugin => plugin.recordServices.map(service => ({ - ...service, - isEnabled: recordPluginServiceState.get(service.title) || false, - toggleEnabled: () => this.enableService(service, plugin) - })) - ) - ); - } - - _makePluginsDir() { - const cwd = path.join(app.getPath('userData'), 'plugins'); - const fp = path.join(cwd, 'package.json'); - - if (!fs.existsSync(fp)) { - makeDir.sync(cwd); - fs.writeFileSync(fp, '{"dependencies":{}}'); - } - - this.cwd = cwd; - this.pkgPath = fp; - } - - _modifyMainPackageJson(modifier) { - const pkg = JSON.parse(fs.readFileSync(this.pkgPath, 'utf8')); - modifier(pkg); - fs.writeFileSync(this.pkgPath, JSON.stringify(pkg, null, 2)); - } - - async _runYarn(...commands) { - await execa(process.execPath, [this.yarnBin, ...commands], { - cwd: this.cwd, - env: { - ELECTRON_RUN_AS_NODE: 1 - } - }); - } - - _pluginNames() { - const pkg = fs.readFileSync(path.join(this.cwd, 'package.json'), 'utf8'); - return Object.keys(JSON.parse(pkg).dependencies || {}); - } - - async _yarnInstall() { - await this._runYarn('install', '--no-lockfile', '--registry', 'https://registry.npmjs.org'); - } - - async install(name) { - track(`plugin/installed/${name}`); - // We manually add it to the package.json here so we're able to set the version to `latest` - this._modifyMainPackageJson(pkg => { - if (!pkg.dependencies) { - pkg.dependencies = {}; - } - - pkg.dependencies[name] = 'latest'; - }); - - try { - await this._yarnInstall(); - - const plugin = new InstalledPlugin(name); - - if (plugin.plugin.didInstall && typeof plugin.plugin.didInstall === 'function') { - try { - await plugin.plugin.didInstall(plugin.config); - } catch (error) { - showError(error, {plugin}); - } - } - - const {isValid, hasConfig} = plugin; - - const options = (isValid && !hasConfig) ? { - title: 'Plugin installed', - body: `"${plugin.prettyName}" is ready for use` - } : { - title: plugin.isValid ? 'Plugin installed' : 'Configure plugin', - body: `"${plugin.prettyName}" ${plugin.isValid ? 'can be configured' : 'requires configuration'}`, - actions: [ - {type: 'button', text: 'Configure'}, - {type: 'button', text: 'Later'} - ] - }; - - pluginNotification = new Notification(options); - - if (!isValid || hasConfig) { - const openConfig = () => openPrefsWindow({target: {name, action: 'configure'}}); - - pluginNotification.on('click', openConfig); - - pluginNotification.on('action', (_, index) => { - if (index === 0) { - openConfig(); - } else { - pluginNotification.close(); - } - }); - } - - for (const service of plugin.config.validServices) { - if (!service.willEnable) { - recordPluginServiceState.set(service, true); - } - } - - pluginNotification.show(); - this.updateExportOptions(); - this.refreshRecordPluginServices(); - this.emit('installed', plugin); - - return plugin; - } catch (error) { - notify(`Something went wrong while installing ${name}`); - this._modifyMainPackageJson(pkg => { - delete pkg.dependencies[name]; - }); - console.log(error); - } - } - - async upgrade() { - await this._yarnInstall(); - } - - async uninstall(name) { - track(`plugin/uninstalled/${name}`); - this._modifyMainPackageJson(pkg => { - delete pkg.dependencies[name]; - }); - const plugin = new InstalledPlugin(name); - - if (plugin.plugin.willUninstall && typeof plugin.plugin.willUninstall === 'function') { - try { - await plugin.plugin.willUninstall(plugin.config); - } catch (error) { - showError(error, {plugin}); - } - } - - plugin.config.clear(); - this.emit('uninstalled', name); - this.updateExportOptions(); - return new NpmPlugin(plugin.json, { - // Keeping for backwards compatibility - version: plugin.json.kapVersion, - ...plugin.json.kap - }); - } - - async prune() { - await this._yarnInstall(); - } - - getServices(pluginName) { - const { - shareServices = [], - recordServices = [] - } = require(path.join(this.cwd, 'node_modules', pluginName)); - - return [...shareServices, ...recordServices]; - } - - getInstalled() { - try { - return this._pluginNames().map(name => new InstalledPlugin(name)); - } catch (error) { - showError(error); - const Sentry = require('../utils/sentry'); - Sentry.captureException(error); - return []; - } - } - - getSharePlugins() { - return this.getInstalled().filter(plugin => plugin.shareServices.length > 0); - } - - getRecordingPlugins() { - return this.getInstalled().filter(plugin => plugin.recordServices.length > 0); - } - - getEditPlugins() { - return this.getInstalled().filter(plugin => plugin.editServices.length > 0); - } - - getBuiltIn() { - return [{ - pluginPath: path.resolve(__dirname, '..', 'plugins', 'copy-to-clipboard-plugin'), - isCompatible: true, - name: '_copyToClipboard' - }, { - pluginPath: path.resolve(__dirname, '..', 'plugins', 'save-file-plugin'), - isCompatible: true, - name: '_saveToDisk' - }, { - pluginPath: path.resolve(__dirname, '..', 'plugins', 'open-with-plugin'), - isCompatible: true, - name: '_openWith' - }]; - } - - async getFromNpm() { - const url = 'https://api.npms.io/v2/search?q=keywords:kap-plugin+not:deprecated'; - const response = await got(url, {json: true}); - const installed = this._pluginNames(); - - 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(async x => { - const {kap, kapVersion} = await packageJson(x.name, {fullMetadata: true}); - return new NpmPlugin(x, { - // Keeping for backwards compatibility - version: kapVersion, - ...kap - }); - })); - } - - getPluginService(pluginName, serviceTitle) { - return this.getServices(pluginName).find(shareService => shareService.title === serviceTitle); - } - - async openPluginConfig(name) { - await openConfigWindow(name); - const plugin = new InstalledPlugin(name); - this.emit('config-changed', plugin); - return plugin.isValid; - } -} - -const plugins = new Plugins(); -module.exports = plugins; +// 'use strict'; + +// const path = require('path'); +// const fs = require('fs'); +// const electron = require('electron'); +// const got = require('got'); +// const execa = require('execa'); +// const makeDir = require('make-dir'); +// const packageJson = require('package-json'); + +// const {app, Notification} = electron; + +// const {refreshRecordPluginItems} = require('../menus'); +// const {openConfigWindow} = require('../config'); +// const {openPrefsWindow} = require('../preferences'); +// const {notify} = require('./notifications'); +// const {track} = require('./analytics'); +// const {InstalledPlugin, NpmPlugin, recordPluginServiceState} = require('../plugin'); +// const {showError} = require('../utils/errors'); +// const {EventEmitter} = require('events'); + +// // Need to persist the notification, otherwise it is garbage collected and the actions don't trigger +// // https://github.com/electron/electron/issues/12690 +// let pluginNotification; + +// class Plugins extends EventEmitter { +// constructor() { +// super(); +// this.yarnBin = path.join(__dirname, '../../node_modules/yarn/bin/yarn.js'); +// this._makePluginsDir(); +// this.appVersion = app.getVersion(); +// } + +// setUpdateExportOptions(updateExportOptions) { +// this.updateExportOptions = updateExportOptions; +// } + +// async enableService(service, plugin) { +// const wasEnabled = recordPluginServiceState.get(service.title) || false; + +// if (wasEnabled) { +// recordPluginServiceState.set(service.title, false); +// return this.refreshRecordPluginServices(); +// } + +// if (!plugin.config.validServices.includes(service.title)) { +// openPrefsWindow({target: {name: plugin.name, action: 'configure'}}); +// return; +// } + +// if (service.willEnable) { +// try { +// const canEnable = await service.willEnable(); + +// if (canEnable) { +// recordPluginServiceState.set(service.title, true); +// } +// } catch (error) { +// showError(error, {title: `Something went wrong while enabling “${service.title}”`}); +// const Sentry = require('./utils/sentry').default; +// Sentry.captureException(error); +// } + +// this.refreshRecordPluginServices(); +// return; +// } + +// recordPluginServiceState.set(service.title, true); +// this.refreshRecordPluginServices(); +// } + +// refreshRecordPluginServices = () => { +// refreshRecordPluginItems( +// this.getRecordingPlugins().flatMap( +// plugin => plugin.recordServices.map(service => ({ +// ...service, +// isEnabled: recordPluginServiceState.get(service.title) || false, +// toggleEnabled: () => this.enableService(service, plugin) +// })) +// ) +// ); +// } + +// _makePluginsDir() { +// const cwd = path.join(app.getPath('userData'), 'plugins'); +// const fp = path.join(cwd, 'package.json'); + +// if (!fs.existsSync(fp)) { +// makeDir.sync(cwd); +// fs.writeFileSync(fp, '{"dependencies":{}}'); +// } + +// this.cwd = cwd; +// this.pkgPath = fp; +// } + +// _modifyMainPackageJson(modifier) { +// const pkg = JSON.parse(fs.readFileSync(this.pkgPath, 'utf8')); +// modifier(pkg); +// fs.writeFileSync(this.pkgPath, JSON.stringify(pkg, null, 2)); +// } + +// async _runYarn(...commands) { +// await execa(process.execPath, [this.yarnBin, ...commands], { +// cwd: this.cwd, +// env: { +// ELECTRON_RUN_AS_NODE: 1 +// } +// }); +// } + +// _pluginNames() { +// const pkg = fs.readFileSync(path.join(this.cwd, 'package.json'), 'utf8'); +// return Object.keys(JSON.parse(pkg).dependencies || {}); +// } + +// async _yarnInstall() { +// await this._runYarn('install', '--no-lockfile', '--registry', 'https://registry.npmjs.org'); +// } + +// async install(name) { +// track(`plugin/installed/${name}`); +// // We manually add it to the package.json here so we're able to set the version to `latest` +// this._modifyMainPackageJson(pkg => { +// if (!pkg.dependencies) { +// pkg.dependencies = {}; +// } + +// pkg.dependencies[name] = 'latest'; +// }); + +// try { +// await this._yarnInstall(); + +// const plugin = new InstalledPlugin(name); + +// if (plugin.plugin.didInstall && typeof plugin.plugin.didInstall === 'function') { +// try { +// await plugin.plugin.didInstall(plugin.config); +// } catch (error) { +// showError(error, {plugin}); +// } +// } + +// const {isValid, hasConfig} = plugin; + +// const options = (isValid && !hasConfig) ? { +// title: 'Plugin installed', +// body: `"${plugin.prettyName}" is ready for use` +// } : { +// title: plugin.isValid ? 'Plugin installed' : 'Configure plugin', +// body: `"${plugin.prettyName}" ${plugin.isValid ? 'can be configured' : 'requires configuration'}`, +// actions: [ +// {type: 'button', text: 'Configure'}, +// {type: 'button', text: 'Later'} +// ] +// }; + +// pluginNotification = new Notification(options); + +// if (!isValid || hasConfig) { +// const openConfig = () => openPrefsWindow({target: {name, action: 'configure'}}); + +// pluginNotification.on('click', openConfig); + +// pluginNotification.on('action', (_, index) => { +// if (index === 0) { +// openConfig(); +// } else { +// pluginNotification.close(); +// } +// }); +// } + +// for (const service of plugin.config.validServices) { +// if (!service.willEnable) { +// recordPluginServiceState.set(service, true); +// } +// } + +// pluginNotification.show(); +// this.updateExportOptions(); +// this.refreshRecordPluginServices(); +// this.emit('installed', plugin); + +// return plugin; +// } catch (error) { +// notify(`Something went wrong while installing ${name}`); +// this._modifyMainPackageJson(pkg => { +// delete pkg.dependencies[name]; +// }); +// console.log(error); +// } +// } + +// async upgrade() { +// await this._yarnInstall(); +// } + +// async uninstall(name) { +// track(`plugin/uninstalled/${name}`); +// this._modifyMainPackageJson(pkg => { +// delete pkg.dependencies[name]; +// }); +// const plugin = new InstalledPlugin(name); + +// if (plugin.plugin.willUninstall && typeof plugin.plugin.willUninstall === 'function') { +// try { +// await plugin.plugin.willUninstall(plugin.config); +// } catch (error) { +// showError(error, {plugin}); +// } +// } + +// plugin.config.clear(); +// this.emit('uninstalled', name); +// this.updateExportOptions(); +// return new NpmPlugin(plugin.json, { +// // Keeping for backwards compatibility +// version: plugin.json.kapVersion, +// ...plugin.json.kap +// }); +// } + +// async prune() { +// await this._yarnInstall(); +// } + +// getServices(pluginName) { +// const { +// shareServices = [], +// recordServices = [] +// } = require(path.join(this.cwd, 'node_modules', pluginName)); + +// return [...shareServices, ...recordServices]; +// } + +// getInstalled() { +// try { +// return this._pluginNames().map(name => new InstalledPlugin(name)); +// } catch (error) { +// showError(error); +// const Sentry = require('../utils/sentry').default; +// Sentry.captureException(error); +// return []; +// } +// } + +// getSharePlugins() { +// return this.getInstalled().filter(plugin => plugin.shareServices.length > 0); +// } + +// getRecordingPlugins() { +// return this.getInstalled().filter(plugin => plugin.recordServices.length > 0); +// } + +// getEditPlugins() { +// return this.getInstalled().filter(plugin => plugin.editServices.length > 0); +// } + +// getBuiltIn() { +// return [{ +// pluginPath: path.resolve(__dirname, '..', 'plugins', 'copy-to-clipboard-plugin'), +// isCompatible: true, +// name: '_copyToClipboard' +// }, { +// pluginPath: path.resolve(__dirname, '..', 'plugins', 'save-file-plugin'), +// isCompatible: true, +// name: '_saveToDisk' +// }, { +// pluginPath: path.resolve(__dirname, '..', 'plugins', 'open-with-plugin'), +// isCompatible: true, +// name: '_openWith' +// }]; +// } + +// async getFromNpm() { +// const url = 'https://api.npms.io/v2/search?q=keywords:kap-plugin+not:deprecated'; +// const response = await got(url, {json: true}); +// const installed = this._pluginNames(); + +// 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(async x => { +// const {kap, kapVersion} = await packageJson(x.name, {fullMetadata: true}); +// return new NpmPlugin(x, { +// // Keeping for backwards compatibility +// version: kapVersion, +// ...kap +// }); +// })); +// } + +// getPluginService(pluginName, serviceTitle) { +// return this.getServices(pluginName).find(shareService => shareService.title === serviceTitle); +// } + +// async openPluginConfig(name) { +// await openConfigWindow(name); +// const plugin = new InstalledPlugin(name); +// this.emit('config-changed', plugin); +// return plugin.isValid; +// } +// } + +// const plugins = new Plugins(); +// module.exports = plugins; diff --git a/main/common/remote-state.d.ts b/main/common/remote-state.d.ts deleted file mode 100644 index a3c830f5d..000000000 --- a/main/common/remote-state.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -import {string, number} from 'prop-types'; - -export type FormatName = 'gif' | 'av1' | 'mp4' | 'webm' | 'apng'; - -export interface App { - isDefault: boolean; - icon: string; - url: string; - name: string; -} - -export interface Plugin { - title: string; - pluginName: string; - pluginPath: string; - apps?: App[]; - lastUsed: number -} - -export interface Format { - format: FormatName; - prettyFormat: string; - plugins: Plugin[] - lastUsed: number; -}; - -export interface EditService { - title: string; - pluginName: string; - pluginPath: string; - hasConfig: boolean; -} - -export interface EditorOptionsState { - formats: Format[]; - editServices: EditService[]; - fpsHistory: { - [key: FormatName]: number; - } -} - -export interface EditorOptionsActions { - updatePluginState: (args: {format: FormatName, plugin: string}) => void; - updateFpsUsage: (args: {format: FormatName, fps: number}) => void; -} - -export type UseRemoteStateFunction = (name: Name, initialState?: State) => () => Actions & {state: State, isLoading: boolean}; - -export const useRemoteState: UseRemoteStateFunction<'editor-options', EditorOptionsState, EditorOptionsActions>; - diff --git a/main/common/remote-state.js b/main/common/remote-state.js deleted file mode 100644 index d63d43059..000000000 --- a/main/common/remote-state.js +++ /dev/null @@ -1,108 +0,0 @@ -'use strict'; -const getChannelName = (name, action) => `kap-remote-state-${name}-${action}`; - -const getChannelNames = name => ({ - subscribe: getChannelName(name, 'subscribe'), - getState: getChannelName(name, 'get-state'), - callAction: getChannelName(name, 'call-action'), - stateUpdated: getChannelName(name, 'state-updated') -}); - -const useRemoteState = (name, initialState) => { - const {useState, useEffect, useRef} = require('react'); - const {ipcRenderer} = require('electron-better-ipc'); - - const channelNames = getChannelNames(name); - - return id => { - const [state, setState] = useState(initialState); - const [isLoading, setIsLoading] = useState(true); - const actionsRef = useRef({}); - - useEffect(() => { - const cleanup = ipcRenderer.answerMain(channelNames.stateUpdated, setState); - - (async () => { - const actionKeys = await ipcRenderer.callMain(channelNames.subscribe, id); - console.log(actionKeys); - const actions = actionKeys.reduce((acc, key) => ({ - ...acc, - [key]: data => ipcRenderer.callMain(channelNames.callAction, {key, data, id}) - }), {}); - - console.log(actions); - - const getState = async () => { - const newState = await ipcRenderer.callMain(channelNames.getState, id); - setState(newState); - } - - actionsRef.current = { - ...actions, - refreshState: getState - }; - - await getState(); - setIsLoading(false); - })(); - - return cleanup; - }, []); - - console.log(actionsRef.current); - - return { - ...actionsRef.current, - isLoading, - state - }; - }; -} - -const setupRemoteState = (name, callback) => { - const channelNames = getChannelNames(name); - - return async () => { - const {ipcMain} = require('electron-better-ipc'); - - const renderers = new Map(); - - const sendUpdate = (state, id) => { - if (id) { - return ipcMain.callRenderer(renderers.get(id), channelNames.stateUpdated, state); - } - - for (const [windowId, renderer] of renderers.entries()) { - ipcMain.callRenderer(renderer, channelNames.stateUpdated, state || (getState && getState(windowId))); - } - }; - - const {getState, actions = {}} = await callback(sendUpdate); - - ipcMain.answerRenderer(channelNames.subscribe, async (customId, window) => { - const id = customId || window.id; - renderers.set(id, window); - - window.on('closed', () => { - renderers.delete(id); - }); - - return Object.keys(actions); - }); - - ipcMain.answerRenderer(channelNames.getState, async (customId, window) => { - const id = customId || window.id; - return getState(id); - }); - - ipcMain.answerRenderer(channelNames.callAction, async ({key, data, id: customId}, window) => { - const id = customId || window.id; - return actions[key](data, id); - }); - } -} - -module.exports = { - useRemoteState, - setupRemoteState -}; diff --git a/main/common/settings.ts b/main/common/settings.ts index 932a0f75a..b6f235233 100644 --- a/main/common/settings.ts +++ b/main/common/settings.ts @@ -38,7 +38,8 @@ interface Settings { enableShortcuts: boolean; shortcuts: { [key in keyof typeof shortcuts]: string - } + }, + version: string; } const store = new Store({ diff --git a/main/common/types.ts b/main/common/types.ts new file mode 100644 index 000000000..255589410 --- /dev/null +++ b/main/common/types.ts @@ -0,0 +1,15 @@ + +export enum Format { + gif = 'gif', + mp4 = 'mp4', + webm = 'webm', + apng = 'apng', + av1 = 'av1' +} + +export enum Encoding { + h264 = 'h264', + hevc = 'hevc', + proRes422 = 'proRes422', + proRes4444 = 'proRes4444' +} diff --git a/main/conversion.ts b/main/conversion.ts new file mode 100644 index 000000000..2569bb64c --- /dev/null +++ b/main/conversion.ts @@ -0,0 +1,203 @@ +import {EventEmitter} from 'events'; +import {Format} from './common/types'; +import {Video} from './video'; +import {convertTo} from './converters'; +import Export, {ExportOptions} from './export'; +import hash from 'object-hash'; +import {ipcMain} from 'electron-better-ipc'; +import plugins from './plugins'; + +interface ConversionOptions { + startTime: number; + endTime: number; + width: number; + height: number; + fps: number; + shouldCrop: boolean; + shouldMute: boolean; +} + +enum Status { + idle, + inProgress, + failed, + canceled, + completed +} + +// A conversion object describes the process of converting a video or recording +// using ffmpeg that can then be shared multiple times using Share plugins +export default class Conversion extends EventEmitter { + static all = new Map(); + + static fromId(id: string) { + return this.all.get(id); + } + + id: string; + video: Video; + format: Format; + options: ConversionOptions; + + text: string = ''; + percentage?: number; + error?: Error; + + private _status: Status = Status.idle; + + get status() { + return this._status; + } + + set status(newStatus: Status) { + this._status = newStatus; + this.emit('updated'); + } + + private conversionProcess?: Promise; + + constructor(video: Video, format: Format, options: ConversionOptions) { + super(); + this.video = video; + this.format = format; + this.options = options; + + this.id = hash({ + filePath: video.filePath, + format, + options + }); + + Conversion.all.set(this.id, this); + } + + onProgress = (text: string, progress: number) => { + this.text = text; + this.percentage = progress; + this.emit('updated'); + } + + private onConversionProgress = (action: string, progress: number, estimate?: string) => { + const text = estimate ? `${action} — ${estimate} remaining` : `${action}…`; + this.onProgress(text, progress); + } + + private onExportProgress = (text: string, progress: number) => { + this.onProgress(text, progress); + } + + filePath = async ({fileType}: {fileType?: Format} = {}) => { + console.log(fileType); + if (!this.conversionProcess) { + this.start(); + } + + try { + const filePath = await this.conversionProcess; + return filePath as string; + } catch (error) { + // Ensure we re-try the conversion if it fails + this.conversionProcess = undefined; + throw error; + } + } + + addExport(exportOptions: ExportOptions) { + this.status = Status.inProgress; + this.error = undefined; + this.text = ''; + this.percentage = 0; + + const newExport = new Export(this, exportOptions); + + newExport.on('progress', this.onExportProgress); + const cleanup = () => { + newExport.off('progress', this.onExportProgress); + } + + newExport.once('canceled', () => { + cleanup(); + this.status = Status.canceled; + }); + + newExport.once('finished', () => { + cleanup(); + this.status = Status.completed; + }); + + newExport.once('error', (error: Error) => { + // showError(error, {plugin: exportOptions.plugin}); + cleanup(); + this.error = error; + this.status = Status.failed; + }); + + newExport.start(); + } + + private start = () => { + console.log('STart called'); + this.conversionProcess = convertTo( + this.format, + { + ...this.options, + defaultFileName: this.video.title, + inputPath: this.video.filePath, + onProgress: this.onConversionProgress + }, + this.video.encoding + ); + } +} + +export const setupConversionHook = () => { + ipcMain.answerRenderer('create-conversion', ({ + filePath, options, format, plugins: pluginOptions + }: { + filePath: string; + options: ConversionOptions, + format: Format, + plugins: { + share: { + pluginName: string; + serviceTitle: string; + }, + edit?: { + pluginName: string; + serviceTitle: string; + } + } + }) => { + console.log('HERE WITH', filePath, options, format, pluginOptions); + const video = Video.fromId(filePath); + + if (!video) { + return; + } + + console.log('Here with', video); + + const exportPlugin = plugins.sharePlugins.find(plugin => { + return plugin.name === pluginOptions.share.pluginName + }); + + const exportService = exportPlugin?.shareServices.find(service => { + return service.title === pluginOptions.share.serviceTitle + }); + + if (!exportPlugin || !exportService) { + return; + } + + console.log('here', exportPlugin); + + const conversion = new Conversion(video, format, options); + conversion.addExport({ + plugin: exportPlugin, + service: exportService + }); + + console.log('queueed'); + return conversion.id; + }); +} diff --git a/main/convert.js b/main/convert.js index f33a97cd5..fe76bce5c 100644 --- a/main/convert.js +++ b/main/convert.js @@ -14,7 +14,7 @@ const tempy = require('tempy'); const gifsicle = require('gifsicle'); const {track} = require('./common/analytics'); const {EditServiceContext} = require('./service-context'); -const settings = require('./common/settings'); +const settings = require('./common/settings').default; const gifsiclePath = util.fixPathForAsarUnpack(gifsicle); const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path); diff --git a/main/converters/h264.ts b/main/converters/h264.ts index d2b0246b4..283fc8a7b 100644 --- a/main/converters/h264.ts +++ b/main/converters/h264.ts @@ -4,6 +4,7 @@ import {compress, convert} from './process'; import {areDimensionsEven, conditionalArgs, ConvertOptions, makeEven} from './utils'; import settings from '../common/settings'; import os from 'os'; +import {Format} from '../common/types'; // `time ffmpeg -i original.mp4 -vf fps=30,scale=480:-1::flags=lanczos,palettegen palette.png` // `time ffmpeg -i original.mp4 -i palette.png -filter_complex 'fps=30,scale=-1:-1:flags=lanczos[x]; [x][1:v]paletteuse' palette.gif` @@ -183,9 +184,9 @@ const convertToApng = (options: ConvertOptions) => convert(options.outputPath, { )); export default new Map([ - ['gif', convertToGif], - ['mp4', convertToMp4], - ['webm', convertToWebm], - ['apng', convertToApng], - ['av1', convertToAv1] + [Format.gif, convertToGif], + [Format.mp4, convertToMp4], + [Format.webm, convertToWebm], + [Format.apng, convertToApng], + [Format.av1, convertToAv1] ]); diff --git a/main/converters/index.ts b/main/converters/index.ts index fa5540053..94043b8a4 100644 --- a/main/converters/index.ts +++ b/main/converters/index.ts @@ -1,14 +1,15 @@ import path from 'path'; import tempy from 'tempy'; +import {Encoding, Format} from '../common/types'; import {track} from '../common/analytics'; import h264Converters from './h264'; import {ConvertOptions} from './utils'; const converters = new Map([ - ['h264', h264Converters] + [Encoding.h264, h264Converters] ]); -export const convertTo = (format: string, options: Omit & {defaultFileName: string}, encoding: string = 'h264') => { +export const convertTo = (format: Format, options: Omit & {defaultFileName: string}, encoding: Encoding = Encoding.h264) => { if (!converters.has(encoding)) { throw new Error(`Unsupported encoding: ${encoding}`); } diff --git a/main/cropper.js b/main/cropper.js index a4c86fab8..7a7994285 100644 --- a/main/cropper.js +++ b/main/cropper.js @@ -3,7 +3,7 @@ const electron = require('electron'); const delay = require('delay'); -const settings = require('./common/settings'); +const settings = require('./common/settings').default; const {hasMicrophoneAccess, ensureMicrophonePermissions, openSystemPreferences, ensureScreenCapturePermissions} = require('./common/system-permissions'); const loadRoute = require('./utils/routes'); const {checkForAnyBlockingEditors} = require('./editor'); diff --git a/main/editor.js b/main/editor.js index 1e2dd8ee4..4d424b44e 100644 --- a/main/editor.js +++ b/main/editor.js @@ -8,16 +8,17 @@ const pify = require('pify'); const {ipcMain: ipc} = require('electron-better-ipc'); const {is} = require('electron-util'); -const getFps = require('./utils/fps'); +const getFps = require('./utils/fps').default; const loadRoute = require('./utils/routes'); const {generateTimestampedName} = require('./utils/timestamped-name'); const KapWindow = require('./kap-window'); +const {Video} = require('./video'); const editors = new Map(); let allOptions; const OPTIONS_BAR_HEIGHT = 48; const VIDEO_ASPECT = 9 / 16; -const MIN_VIDEO_WIDTH = 768; +const MIN_VIDEO_WIDTH = 900; const MIN_VIDEO_HEIGHT = MIN_VIDEO_WIDTH * VIDEO_ASPECT; const MIN_WINDOW_HEIGHT = MIN_VIDEO_HEIGHT + OPTIONS_BAR_HEIGHT; const editorEmitter = new EventEmitter(); @@ -39,6 +40,8 @@ const openEditorWindow = async ( return; } + const video = new Video({filePath}); + const fps = recordedFps || await getFps(filePath); const title = recordingName || getEditorName(originalFilePath || filePath, isNewRecording); @@ -56,7 +59,7 @@ const openEditorWindow = async ( transparent: true, vibrancy: 'window', route: 'editor2', - args: { + initialState: { filePath, fps, originalFilePath, diff --git a/main/export-list.js b/main/export-list.js index d4e46e69f..b9b56e234 100644 --- a/main/export-list.js +++ b/main/export-list.js @@ -11,7 +11,7 @@ const util = require('electron-util'); const execa = require('execa'); const makeDir = require('make-dir'); -const settings = require('./common/settings'); +const settings = require('./common/settings').default; const {track} = require('./common/analytics'); const {openPrefsWindow} = require('./preferences'); const {getExportsWindow, openExportsWindow} = require('./exports'); diff --git a/main/export-options.js b/main/export-options.js index 2e0447c31..bc6934428 100644 --- a/main/export-options.js +++ b/main/export-options.js @@ -1,171 +1,171 @@ -'use strict'; - -const Store = require('electron-store'); -const {ipcMain: ipc} = require('electron-better-ipc'); - -const plugins = require('./common/plugins'); -const {converters} = require('./convert'); -const {setOptions, getEditors} = require('./editor'); -const {apps} = require('./plugins/open-with-plugin'); -const {showError} = require('./utils/errors'); - -const exportUsageHistory = new Store({ - name: 'export-usage-history', - defaults: { - apng: {lastUsed: 1, plugins: {default: 1}}, - webm: {lastUsed: 2, plugins: {default: 1}}, - mp4: {lastUsed: 3, plugins: {default: 1}}, - gif: {lastUsed: 4, plugins: {default: 1}}, - av1: {lastUsed: 5, plugins: {default: 1}} - } -}); - -const fpsUsageHistory = new Store({ - name: 'fps-usage-history', - schema: { - apng: { - type: 'number', - minimum: 0, - default: 60 - }, - webm: { - type: 'number', - minimum: 0, - default: 60 - }, - mp4: { - type: 'number', - minimum: 0, - default: 60 - }, - gif: { - type: 'number', - minimum: 0, - default: 60 - }, - av1: { - type: 'number', - minimum: 0, - default: 60 - } - } -}); - -const prettifyFormat = format => { - const formats = new Map([ - ['apng', 'APNG'], - ['gif', 'GIF'], - ['mp4', 'MP4 (H264)'], - ['av1', 'MP4 (AV1)'], - ['webm', 'WebM'] - ]); - - return formats.get(format); -}; - -const getEditOptions = () => { - const installed = plugins.getEditPlugins(); - - return installed.flatMap( - plugin => plugin.editServices - .filter(service => plugin.config.validServices.includes(service.title)) - .map(service => ({ - title: service.title, - pluginName: plugin.name, - pluginPath: plugin.pluginPath, - hasConfig: Object.keys(service.config || {}).length > 0 - })) - ); -}; - -const getExportOptions = () => { - const installed = plugins.getSharePlugins(); - const builtIn = plugins.getBuiltIn(); - - const options = []; - for (const format of converters.keys()) { - options.push({ - format, - prettyFormat: prettifyFormat(format), - plugins: [] - }); - } - - for (const json of [...installed, ...builtIn]) { - if (!json.isCompatible) { - continue; - } - - try { - const plugin = require(json.pluginPath); - - for (const service of plugin.shareServices) { - for (const format of service.formats) { - options.find(option => option.format === format).plugins.push({ - title: service.title, - pluginName: json.name, - pluginPath: json.pluginPath, - apps: json.name === '_openWith' ? apps.get(format) : undefined - }); - } - } - } catch (error) { - showError(error, {title: `Something went wrong while loading “${json.name}”`, plugin: json}); - const Sentry = require('./utils/sentry'); - Sentry.captureException(error); - } - } - - const sortFunc = (a, b) => b.lastUsed - a.lastUsed; - - for (const option of options) { - const {lastUsed, plugins} = exportUsageHistory.get(option.format); - option.lastUsed = lastUsed; - option.plugins = option.plugins.map(plugin => ({...plugin, lastUsed: plugins[plugin.pluginName] || 0})).sort(sortFunc); - } - - return options.sort(sortFunc); -}; - -const updateExportOptions = () => { - const editors = getEditors(); - const exportOptions = getExportOptions(); - const editOptions = getEditOptions(); - for (const editor of editors) { - ipc.callRenderer(editor, 'export-options', {exportOptions, editOptions, fps: fpsUsageHistory.store}); - } - - setOptions({exportOptions, editOptions, fps: fpsUsageHistory.store}); -}; - -plugins.setUpdateExportOptions(updateExportOptions); - -ipc.answerRenderer('update-usage', ({format, plugin, fps}) => { - if (plugin) { - const usage = exportUsageHistory.get(format); - const now = Date.now(); - - usage.plugins[plugin] = now; - usage.lastUsed = now; - exportUsageHistory.set(format, usage); - } - - fpsUsageHistory.set(format, fps); - updateExportOptions(); -}); - -ipc.answerRenderer('refresh-usage', updateExportOptions); - -const initializeExportOptions = () => { - setOptions({ - exportOptions: getExportOptions(), - editOptions: getEditOptions(), - fps: fpsUsageHistory.store - }); -}; - -module.exports = { - getExportOptions, - updateExportOptions, - initializeExportOptions -}; +// 'use strict'; + +// const Store = require('electron-store'); +// const {ipcMain: ipc} = require('electron-better-ipc'); + +// const plugins = require('./common/plugins'); +// const {converters} = require('./convert'); +// const {setOptions, getEditors} = require('./editor'); +// const {apps} = require('./plugins/built-in/open-with-plugin'); +// const {showError} = require('./utils/errors'); + +// const exportUsageHistory = new Store({ +// name: 'export-usage-history', +// defaults: { +// apng: {lastUsed: 1, plugins: {default: 1}}, +// webm: {lastUsed: 2, plugins: {default: 1}}, +// mp4: {lastUsed: 3, plugins: {default: 1}}, +// gif: {lastUsed: 4, plugins: {default: 1}}, +// av1: {lastUsed: 5, plugins: {default: 1}} +// } +// }); + +// const fpsUsageHistory = new Store({ +// name: 'fps-usage-history', +// schema: { +// apng: { +// type: 'number', +// minimum: 0, +// default: 60 +// }, +// webm: { +// type: 'number', +// minimum: 0, +// default: 60 +// }, +// mp4: { +// type: 'number', +// minimum: 0, +// default: 60 +// }, +// gif: { +// type: 'number', +// minimum: 0, +// default: 60 +// }, +// av1: { +// type: 'number', +// minimum: 0, +// default: 60 +// } +// } +// }); + +// const prettifyFormat = format => { +// const formats = new Map([ +// ['apng', 'APNG'], +// ['gif', 'GIF'], +// ['mp4', 'MP4 (H264)'], +// ['av1', 'MP4 (AV1)'], +// ['webm', 'WebM'] +// ]); + +// return formats.get(format); +// }; + +// const getEditOptions = () => { +// const installed = plugins.getEditPlugins(); + +// return installed.flatMap( +// plugin => plugin.editServices +// .filter(service => plugin.config.validServices.includes(service.title)) +// .map(service => ({ +// title: service.title, +// pluginName: plugin.name, +// pluginPath: plugin.pluginPath, +// hasConfig: Object.keys(service.config || {}).length > 0 +// })) +// ); +// }; + +// const getExportOptions = () => { +// const installed = plugins.getSharePlugins(); +// const builtIn = plugins.getBuiltIn(); + +// const options = []; +// for (const format of converters.keys()) { +// options.push({ +// format, +// prettyFormat: prettifyFormat(format), +// plugins: [] +// }); +// } + +// for (const json of [...installed, ...builtIn]) { +// if (!json.isCompatible) { +// continue; +// } + +// try { +// const plugin = require(json.pluginPath); + +// for (const service of plugin.shareServices) { +// for (const format of service.formats) { +// options.find(option => option.format === format).plugins.push({ +// title: service.title, +// pluginName: json.name, +// pluginPath: json.pluginPath, +// apps: json.name === '_openWith' ? apps.get(format) : undefined +// }); +// } +// } +// } catch (error) { +// showError(error, {title: `Something went wrong while loading “${json.name}”`, plugin: json}); +// const Sentry = require('./utils/sentry').default; +// Sentry.captureException(error); +// } +// } + +// const sortFunc = (a, b) => b.lastUsed - a.lastUsed; + +// for (const option of options) { +// const {lastUsed, plugins} = exportUsageHistory.get(option.format); +// option.lastUsed = lastUsed; +// option.plugins = option.plugins.map(plugin => ({...plugin, lastUsed: plugins[plugin.pluginName] || 0})).sort(sortFunc); +// } + +// return options.sort(sortFunc); +// }; + +// const updateExportOptions = () => { +// const editors = getEditors(); +// const exportOptions = getExportOptions(); +// const editOptions = getEditOptions(); +// for (const editor of editors) { +// ipc.callRenderer(editor, 'export-options', {exportOptions, editOptions, fps: fpsUsageHistory.store}); +// } + +// setOptions({exportOptions, editOptions, fps: fpsUsageHistory.store}); +// }; + +// plugins.setUpdateExportOptions(updateExportOptions); + +// ipc.answerRenderer('update-usage', ({format, plugin, fps}) => { +// if (plugin) { +// const usage = exportUsageHistory.get(format); +// const now = Date.now(); + +// usage.plugins[plugin] = now; +// usage.lastUsed = now; +// exportUsageHistory.set(format, usage); +// } + +// fpsUsageHistory.set(format, fps); +// updateExportOptions(); +// }); + +// ipc.answerRenderer('refresh-usage', updateExportOptions); + +// const initializeExportOptions = () => { +// setOptions({ +// exportOptions: getExportOptions(), +// editOptions: getEditOptions(), +// fps: fpsUsageHistory.store +// }); +// }; + +// module.exports = { +// getExportOptions, +// updateExportOptions, +// initializeExportOptions +// }; diff --git a/main/export.ts b/main/export.ts new file mode 100644 index 000000000..6bb9d776f --- /dev/null +++ b/main/export.ts @@ -0,0 +1,60 @@ +import {EventEmitter} from 'events'; +import PCancelable, {OnCancelFunction} from 'p-cancelable'; +import Conversion from './conversion'; +import {InstalledPlugin} from './plugins/plugin'; +import {ShareService} from './plugins/service'; +import {ShareServiceContext} from './plugins/service-context'; +import {prettifyFormat} from './utils/formats'; + +export interface ExportOptions { + plugin: InstalledPlugin; + service: ShareService; +} + +export default class Export extends EventEmitter { + conversion: Conversion; + options: ExportOptions; + context: ShareServiceContext; + + constructor(conversion: Conversion, options: ExportOptions) { + super(); + this.conversion = conversion; + this.options = options; + + this.context = new ShareServiceContext({ + plugin: options.plugin, + format: conversion.format, + prettyFormat: prettifyFormat(conversion.format), + defaultFileName: conversion.video.title, + filePath: conversion.filePath, + onProgress: this.onProgress, + onCancel: this.onCancel + }); + } + + start = PCancelable.fn(async (onCancel: OnCancelFunction) => { + const action = this.options.service.action(this.context) as any; + + onCancel(() => { + if (action.cancel && typeof action.cancel === 'function') { + action.cancel(); + } + this.context.isCanceled = true; + }); + + try { + await action; + this.emit('finished'); + } catch (error) { + this.emit('error', error); + } + }); + + onProgress = (text: string, percentage: number) => { + this.emit('progress', {text, percentage}); + } + + onCancel = () => { + this.emit('canceled'); + } +} diff --git a/main/export.js b/main/export1.js similarity index 100% rename from main/export.js rename to main/export1.js diff --git a/main/global-accelerators.js b/main/global-accelerators.js index e3d9fe9d8..43003efdd 100644 --- a/main/global-accelerators.js +++ b/main/global-accelerators.js @@ -1,7 +1,7 @@ 'use strict'; const {globalShortcut} = require('electron'); const {ipcMain: ipc} = require('electron-better-ipc'); -const settings = require('./common/settings'); +const settings = require('./common/settings').default; const {openCropperWindow, isCropperOpen} = require('./cropper'); const openCropper = () => { diff --git a/main/index.js b/main/index.js index 238c08cd5..bf3183950 100644 --- a/main/index.js +++ b/main/index.js @@ -7,12 +7,13 @@ const log = require('electron-log'); const {autoUpdater} = require('electron-updater'); const toMilliseconds = require('@sindresorhus/to-milliseconds'); -const settings = require('./common/settings'); +const settings = require('./common/settings').default; -require('./utils/sentry'); +require('./utils/sentry').default; require('./utils/errors').setupErrorHandling(); const {initializeTray} = require('./tray'); +const {setupConversionHook} = require('./conversion'); const {initializeAnalytics} = require('./common/analytics'); const initializeExportList = require('./export-list'); const {openCropperWindow, isCropperOpen, closeAllCroppers} = require('./cropper'); @@ -21,11 +22,12 @@ const plugins = require('./common/plugins'); const {initializeGlobalAccelerators} = require('./global-accelerators'); const {setApplicationMenu} = require('./menus'); const openFiles = require('./utils/open-files'); -const {initializeExportOptions} = require('./export-options'); const {hasMicrophoneAccess, ensureScreenCapturePermissions} = require('./common/system-permissions'); const {handleDeepLink} = require('./utils/deep-linking'); const {hasActiveRecording, cleanPastRecordings} = require('./recording-history'); +const {setupRemoteStates} = require('./remote-states'); + const filesToOpen = []; app.commandLine.appendSwitch('--enable-features', 'OverlayScrollbar'); @@ -42,8 +44,6 @@ app.on('open-file', (event, path) => { }); const initializePlugins = async () => { - plugins.refreshRecordPluginServices(); - if (!is.development) { try { await plugins.prune(); @@ -81,7 +81,8 @@ const checkForUpdates = () => { await app.whenReady(); // Initialize remote states - require('./remote-states'); + setupRemoteStates(); + setupConversionHook(); app.dock.hide(); app.setAboutPanelOptions({copyright: 'Copyright © Wulkano'}); @@ -98,7 +99,7 @@ const checkForUpdates = () => { initializeTray(); initializeExportList(); initializeGlobalAccelerators(); - initializeExportOptions(); + // initializeExportOptions(); setApplicationMenu(); if (!app.isDefaultProtocolClient('kap')) { diff --git a/main/kap-window.js b/main/kap-window.js deleted file mode 100644 index b8ee6d1ce..000000000 --- a/main/kap-window.js +++ /dev/null @@ -1,72 +0,0 @@ -const electron = require('electron'); -const loadRoute = require('./utils/routes'); -const {ipcMain: ipc} = require('electron-better-ipc'); - -// Has to be named BrowserWindow because of -// https://github.com/electron/electron/blob/master/lib/browser/api/browser-window.ts#L82 -class BrowserWindow extends electron.BrowserWindow { - _readyPromise = new Promise(resolve => { - this._readyPromiseResolve = resolve; - }); - - cleanupMethods = [] - - constructor(props) { - const { - route, - args, - waitForMount, - ...rest - } = props; - - super({ - webPreferences: { - nodeIntegration: true, - }, - ...rest, - show: false - }); - - this.options = props; - loadRoute(this, route); - this.setupWindow(); - } - - setupWindow() { - const {args, waitForMount} = this.options; - - this.on('close', () => { - for (const method of this.cleanupMethods) { - method(); - } - }); - - this.webContents.on('did-finish-load', async () => { - if (args) { - ipc.callRenderer(this, 'kap-window-args', args); - } - - if (waitForMount) { - this.answerRenderer('kap-window-mount', () => { - this.show(); - this._readyPromiseResolve(); - }); - } else { - this.show(); - this._readyPromiseResolve(); - } - }); - } - - answerRenderer(channel, callback) { - this.cleanupMethods.push(ipc.answerRenderer(this, channel, callback)); - } - - async whenReady() { - return this._readyPromise; - } -} - -const KapWindow = BrowserWindow; - -module.exports = KapWindow; diff --git a/main/kap-window.ts b/main/kap-window.ts new file mode 100644 index 000000000..e6d0dc0cf --- /dev/null +++ b/main/kap-window.ts @@ -0,0 +1,124 @@ +import electron from 'electron'; +import {ipcMain as ipc} from 'electron-better-ipc'; +import pEvent from 'p-event'; +import loadRoute from './utils/routes'; + +interface KapWindowOptions extends Electron.BrowserWindowConstructorOptions { + route: string; + waitForMount?: boolean; + initialState?: State; +} + +// Has to be named BrowserWindow because of +// https://github.com/electron/electron/blob/master/lib/browser/api/browser-window.ts#L82 +class BrowserWindow extends electron.BrowserWindow { + private static windows = new Map(); + + static getAllWindows() { + return [...this.windows.values()]; + } + + static fromId(id: number) { + return this.windows.get(id) as BrowserWindow; + } + + static defaultOptions = { + waitForMount: true + }; + + private readyPromise: Promise; + + options: KapWindowOptions; + cleanupMethods: Function[]; + state?: State; + + constructor(props: KapWindowOptions) { + const { + route, + waitForMount, + initialState, + ...rest + } = props; + + super({ + ...rest, + webPreferences: { + nodeIntegration: true, + ...rest.webPreferences + }, + show: false + }); + + this.cleanupMethods = []; + this.options = { + ...BrowserWindow.defaultOptions, + ...props + }; + + this.state = initialState; + loadRoute(this, route); + this.readyPromise = this.setupWindow(); + } + + private async setupWindow() { + const {waitForMount} = this.options; + + BrowserWindow.windows.set(this.id, this); + + this.on('closed', this.cleanup); + + this.webContents.on('did-finish-load', async () => { + if (this.state) { + console.log('sending state', this.state); + this.callRenderer('kap-window-state', this.state); + } + }); + + await pEvent(this.webContents, 'did-finish-load'); + + if (waitForMount) { + return new Promise(resolve => { + console.log('SET IT UP'); + this.answerRenderer('kap-window-mount', () => { + console.log('GOT IT'); + this.show(); + resolve(); + }); + }); + } else { + this.show(); + } + } + + cleanup() { + BrowserWindow.windows.delete(this.id); + for (const method of this.cleanupMethods) { + method?.(); + } + } + + callRenderer(channel: string, data: T) { + return ipc.callRenderer(this, channel, data); + } + + answerRenderer(channel: string, callback: (data: T, window: electron.BrowserWindow) => R) { + this.cleanupMethods.push(ipc.answerRenderer(this, channel, callback)); + } + + setState(partialState: State) { + this.state = { + ...this.state, + ...partialState + }; + + this.callRenderer('kap-window-state', this.state); + } + + async whenReady() { + return this.readyPromise; + } +} + +const KapWindow = BrowserWindow; + +module.exports = KapWindow; diff --git a/main/menus.js b/main/menus.js index 0f7ead6f3..a1290ce43 100644 --- a/main/menus.js +++ b/main/menus.js @@ -7,7 +7,7 @@ const delay = require('delay'); const macosRelease = require('macos-release'); const {supportedVideoExtensions, defaultInputDeviceId} = require('./common/constants'); -const settings = require('./common/settings'); +const settings = require('./common/settings').default; const {hasMicrophoneAccess} = require('./common/system-permissions'); const {getAudioDevices, getDefaultInputDevice} = require('./utils/devices'); const {ensureDockIsShowing} = require('./utils/dock'); diff --git a/main/plugins/copy-to-clipboard-plugin.js b/main/plugins/built-in/copy-to-clipboard-plugin.js similarity index 100% rename from main/plugins/copy-to-clipboard-plugin.js rename to main/plugins/built-in/copy-to-clipboard-plugin.js diff --git a/main/plugins/built-in/open-with-plugin.ts b/main/plugins/built-in/open-with-plugin.ts new file mode 100644 index 000000000..51961b8bc --- /dev/null +++ b/main/plugins/built-in/open-with-plugin.ts @@ -0,0 +1,48 @@ +import {ShareServiceContext} from '../../service-context'; +import path from 'path'; +import {getFormatExtension} from '../../common/constants'; +import {Format} from '../../common/types'; + +const {getAppsThatOpenExtension, openFileWithApp} = require('mac-open-with'); + +const action = async (context: ShareServiceContext & {appUrl: string}) => { + const filePath = await context.filePath(); + openFileWithApp(filePath, context.appUrl); +}; + +export interface App { + url: string; + isDefault: boolean; + icon: string; + name: string; +} + +const getAppsForFormat = (format: Format) => { + return (getAppsThatOpenExtension.sync(getFormatExtension(format)) as App[]) + .map(app => ({...app, name: decodeURI(path.parse(app.url).name)})) + .filter(app => !['Kap', 'Kap Beta'].includes(app.name)) + .sort((a, b) => { + if (a.isDefault !== b.isDefault) { + return Number(b.isDefault) - Number(a.isDefault) + } + + return Number(b.name === 'Gifski') - Number(a.name === 'Gifski'); + }); +}; + +const appsForFormat = (['mp4', 'gif', 'apng', 'webm', 'av1'] as Format[]) + .map(format => ({ + format, + apps: getAppsForFormat(format) + })) + .filter(({apps}) => apps.length > 0) + +export const apps = new Map(appsForFormat.map(({format, apps}) => [format, apps])); + +export const shareServices = [{ + title: 'Open With', + formats: [...apps.keys()], + action +}]; + + diff --git a/main/plugins/save-file-plugin.js b/main/plugins/built-in/save-file-plugin.js similarity index 100% rename from main/plugins/save-file-plugin.js rename to main/plugins/built-in/save-file-plugin.js diff --git a/main/plugins/config.ts b/main/plugins/config.ts new file mode 100644 index 000000000..d9341a71e --- /dev/null +++ b/main/plugins/config.ts @@ -0,0 +1,84 @@ +import {ValidateFunction} from 'ajv'; +import Store, {Schema as JSONSchema} from 'electron-store'; +import Ajv, {Schema} from '../utils/ajv'; +import {Service} from './service'; + +export default class PluginConfig extends Store { + servicesWithNoConfig: Service[]; + validators: { + title: string; + description?: string; + config: {[key: string]: Schema} + validate: ValidateFunction + }[] + + constructor(name: string, services: Service[]) { + const defaults = {}; + + const validators = services + .filter(({config}) => Boolean(config)) + .map(service => { + const config = service.config as {[key: string]: Schema}; + const schema: {[key: string]: JSONSchema} = {}; + const requiredKeys = []; + + for (const key of Object.keys(config)) { + if (!config[key].title) { + throw new Error('Config schema items should have a `title`'); + } + + const {required, ...rest} = config[key]; + + if (required) { + requiredKeys.push(key); + } + + schema[key] = rest; + } + + const ajv = new Ajv({ + format: 'full', + useDefaults: true, + errorDataPath: 'property', + allErrors: true + }); + + const validator = ajv.compile({ + type: 'object', + properties: schema, + required: requiredKeys + }); + + validator(defaults); + return { + validate: validator, + title: service.title, + description: service.configDescription, + config: config + } + }); + + super({ + name, + cwd: 'plugins', + defaults + }); + + this.servicesWithNoConfig = services.filter(({config}) => !config); + this.validators = validators; + } + + get isValid() { + return this.validators.reduce( + (isValid, validator) => isValid && (validator.validate(this.store) as boolean), + true + ); + } + + get validServices() { + return [ + ...this.validators.filter(validator => validator.validate(this.store)), + ...this.servicesWithNoConfig + ].map(service => service.title); + } +} diff --git a/main/plugins/index.ts b/main/plugins/index.ts new file mode 100644 index 000000000..59a696490 --- /dev/null +++ b/main/plugins/index.ts @@ -0,0 +1,209 @@ +import {app} from 'electron'; +import {EventEmitter} from 'events'; +import path from 'path'; +import fs from 'fs'; +import makeDir from 'make-dir'; +import execa from 'execa'; +import {track} from '../common/analytics'; +import {InstalledPlugin, NpmPlugin} from './plugin'; +import {showError} from '../utils/errors'; +import {openPrefsWindow} from '../preferences'; +import {notify} from '../utils/notifications'; +import packageJson from 'package-json'; +import {NormalizedPackageJson} from 'read-pkg'; + +const got = require('got'); + +type PackageJson = { + dependencies: {[key: string]: string} +} + +class Plugins extends EventEmitter { + yarnBin = path.join(__dirname, '../../node_modules/yarn/bin/yarn.js'); + appVersion = app.getVersion(); + pluginsDir = path.join(app.getPath('userData'), 'plugins'); + builtInDir = path.join(__dirname, 'built-in'); + packageJsonPath = path.join(this.pluginsDir, 'package.json'); + installedPlugins: InstalledPlugin[] = []; + + constructor() { + super(); + this.makePluginsDir(); + this.loadPlugins(); + } + + private makePluginsDir() { + if (!fs.existsSync(this.packageJsonPath)) { + makeDir.sync(this.pluginsDir); + fs.writeFileSync(this.packageJsonPath, JSON.stringify({dependencies: {}}, null, 2)); + } + } + + private modifyMainPackageJson(modifier: (pkg: PackageJson) => void) { + const pkg = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf8')); + modifier(pkg); + fs.writeFileSync(this.packageJsonPath, JSON.stringify(pkg, null, 2)); + } + + private async runYarn(...args: string[]) { + await execa(process.execPath, [this.yarnBin, ...args], { + cwd: this.pluginsDir, + env: { + ELECTRON_RUN_AS_NODE: '1', + NODE_ENV: 'development' + } + }); + } + + private get pluginNames() { + const pkg = fs.readFileSync(this.packageJsonPath, 'utf8'); + return Object.keys(JSON.parse(pkg).dependencies || {}); + } + + private async yarnInstall() { + await this.runYarn('install', '--no-lockfile', '--registry', 'https://registry.npmjs.org'); + } + + private loadPlugins() { + this.installedPlugins = this.pluginNames.map(name => new InstalledPlugin(name)); + } + + async install(name: string) { + track(`plugin/installed/${name}`); + + this.modifyMainPackageJson(pkg => { + if (!pkg.dependencies) { + pkg.dependencies = {}; + } + + pkg.dependencies[name] = 'latest'; + }); + + try { + await this.yarnInstall(); + + const plugin = new InstalledPlugin(name); + this.installedPlugins.push(plugin); + + if (plugin.content.didInstall && typeof plugin.content.didInstall === 'function') { + try { + await plugin.content.didInstall?.(plugin.config); + } catch (error) { + showError(error, {plugin} as any); + } + } + + const {isValid, hasConfig} = plugin; + + const openConfig = () => openPrefsWindow({target: {name, action: 'configure'}}); + + const options = (isValid && !hasConfig) ? { + title: 'Plugin installed', + body: `"${plugin.prettyName}" is ready for use` + } : { + title: plugin.isValid ? 'Plugin installed' : 'Configure plugin', + body: `"${plugin.prettyName}" ${plugin.isValid ? 'can be configured' : 'requires configuration'}`, + click: openConfig, + actions: [ + {type: 'button' as const, text: 'Configure', action: openConfig}, + {type: 'button' as const, text: 'Later'} + ] + }; + + notify(options) + + const validServices = plugin.config.validServices; + + for (const service of plugin.recordServices) { + if (!service.willEnable && validServices.includes(service.title)) { + plugin.enableService(service); + } + } + + this.emit('installed', plugin); + } catch (error) { + notify.simple(`Something went wrong while installing ${name}`); + this.modifyMainPackageJson(pkg => { + delete pkg.dependencies[name]; + }); + console.log(error); + } + } + + async uninstall(name: string) { + track(`plugin/uninstalled/${name}`); + this.modifyMainPackageJson(pkg => { + delete pkg.dependencies[name]; + }); + const plugin = new InstalledPlugin(name); + + if (plugin.content.willUninstall && typeof plugin.content.willUninstall === 'function') { + try { + await plugin.content.willUninstall?.(plugin.config); + } catch (error) { + showError(error, {plugin} as any); + } + } + + this.installedPlugins = this.installedPlugins.filter(plugin => plugin.name !== name); + plugin.config.clear(); + this.emit('uninstalled', name); + + const json = plugin.json as NormalizedPackageJson; + + return new NpmPlugin(json, { + version: json.kapVersion, + ...json.kap + }); + } + + async upgrade() { + return this.yarnInstall(); + } + + async getFromNpm() { + const url = 'https://api.npms.io/v2/search?q=keywords:kap-plugin+not:deprecated'; + const response = (await got(url, {json: true})) as { + body: {results: {package: NormalizedPackageJson}[]} + }; + const installed = this.pluginNames; + + 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(async x => { + const {kap, kapVersion} = await packageJson(x.name, {fullMetadata: true}) as any; + return new NpmPlugin(x, { + // Keeping for backwards compatibility + version: kapVersion, + ...kap + }); + })); + } + + get allPlugins() { + return [ + ...this.installedPlugins, + new InstalledPlugin('_copyToClipboard', path.resolve(this.builtInDir, 'copy-to-clipboard-plugin')), + new InstalledPlugin('_saveToDisk', path.resolve(this.builtInDir, 'save-file-plugin')), + new InstalledPlugin('_openWith', path.resolve(this.builtInDir, 'open-with-plugin')), + ]; + } + + get sharePlugins() { + return this.allPlugins.filter(plugin => plugin.shareServices.length > 0); + } + + get editPlugins() { + return this.allPlugins.filter(plugin => plugin.editServices.length > 0); + } + + get recordingPlugins() { + return this.allPlugins.filter(plugin => plugin.recordServices.length > 0); + } +} + +const plugins = new Plugins(); + +export default plugins; diff --git a/main/plugins/open-with-plugin.js b/main/plugins/open-with-plugin.js deleted file mode 100644 index e07ceb9ce..000000000 --- a/main/plugins/open-with-plugin.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; -const path = require('path'); -const {getAppsThatOpenExtension, openFileWithApp} = require('mac-open-with'); -const {getFormatExtension} = require('../common/constants'); - -const action = async context => { - const filePath = await context.filePath(); - openFileWithApp(filePath, context.appUrl); -}; - -const apps = new Map( - ['mp4', 'gif', 'apng', 'webm', 'av1'] - .map(format => [ - format, - getAppsThatOpenExtension.sync(getFormatExtension(format)) - .map(app => ({ - ...app, - name: decodeURI(path.parse(app.url).name) - })) - .filter(app => !['Kap', 'Kap Beta'].includes(app.name)) - .sort((a, b) => { - if (a.isDefault !== b.isDefault) { - return b.isDefault - a.isDefault; - } - - return (b.name === 'Gifski') - (a.name === 'Gifski'); - }) - ]) - .filter(([_, apps]) => apps.length > 0) -); - -module.exports.shareServices = [{ - title: 'Open With', - formats: [...apps.keys()], - action -}]; - -module.exports.apps = apps; diff --git a/main/plugins/plugin.ts b/main/plugins/plugin.ts new file mode 100644 index 000000000..17d0e0e6b --- /dev/null +++ b/main/plugins/plugin.ts @@ -0,0 +1,169 @@ +import {app, shell} from 'electron'; +import macosVersion from 'macos-version'; +import semver from 'semver'; +import path from 'path'; +import fs from 'fs'; +import readPkg from 'read-pkg'; +import {RecordService, ShareService, EditService} from './service'; +import {showError} from '../utils/errors'; +import PluginConfig from './config'; +import Store from 'electron-store'; + +export const recordPluginServiceState = new Store<{[key: string]: boolean}>({ + name: 'record-plugin-state', + defaults: {} +}); + +class BasePlugin { + name: string; + kapVersion?: string; + macosVersion?: string; + link?: string; + json?: readPkg.NormalizedPackageJson; + + constructor(pluginName: string) { + this.name = pluginName + } + + get prettyName() { + return this.name.replace(/^kap-/, ''); + } + + get isCompatible() { + return semver.satisfies(app.getVersion(), this.kapVersion || '*') && macosVersion.is(this.macosVersion || '*'); + } + + get repoUrl() { + if (!this.link) { + return; + } + + const url = new URL(this.link); + url.hash = ''; + return url.href; + } + + get version() { + return this.json?.version; + } + + get description() { + return this.json?.description; + } + + viewOnGithub() { + if (this.link) { + shell.openExternal(this.link) + } + } +} + +export interface KapPlugin { + shareServices?: ShareService[]; + editServices?: EditService[]; + recordServices?: RecordService[]; + + didConfigChange?: (newValue: Readonly | undefined, oldValue: Readonly | undefined, config: Store) => void | Promise; + didInstall?: (config: Store) => void | Promise; + willUninstall?: (config: Store) => void | Promise; +} + +export class InstalledPlugin extends BasePlugin { + isInstalled = true; + pluginsPath = path.join(app.getPath('userData'), 'plugins'); + + pluginPath: string; + json?: readPkg.NormalizedPackageJson; + content: KapPlugin; + config: PluginConfig; + hasConfig: boolean; + isBuiltIn: boolean; + + constructor(pluginName: string, customPath?: string) { + super(pluginName); + + this.pluginPath = customPath || path.join(this.pluginsPath, 'node_modules', pluginName); + this.isBuiltIn = Boolean(customPath); + + if (!this.isBuiltIn) { + this.json = readPkg.sync({cwd: this.pluginPath}); + this.link = this.json.homepage ?? this.json.links?.homepage; + + // Keeping for backwards compatibility + this.kapVersion = this.json.kap?.version ?? this.json.kapVersion; + this.macosVersion = this.json.kap?.macosVersion; + } + + try { + this.content = require(this.pluginPath); + this.config = new PluginConfig(pluginName, this.allServices); + this.hasConfig = this.allServices.some(({config = {}}) => Object.keys(config).length > 0); + + if (this.content.didConfigChange && typeof this.content.didConfigChange === 'function') { + this.config.onDidAnyChange((newValue, oldValue) => this.content.didConfigChange?.(newValue, oldValue, this.config)); + } + } catch (error) { + showError(error, {title: `Something went wrong while loading “${pluginName}”`, plugin: this}); + + this.content = {}; + this.config = new PluginConfig(pluginName, []); + this.hasConfig = false; + } + } + + get isSymLink() { + return fs.lstatSync(this.pluginPath).isSymbolicLink(); + } + + get shareServices() { + return this.content.shareServices || []; + } + + get editServices() { + return this.content.editServices || []; + } + + get recordServices() { + return this.content.recordServices || []; + } + + get allServices() { + return [ + ...this.shareServices, + ...this.editServices, + ...this.recordServices + ]; + } + + get isValid() { + return this.config.isValid; + } + + get recordServicesWithStatus() { + return this.recordServices.map(service => ({ + ...service, + isEnabled: recordPluginServiceState.get(`${this.name}-${service.title}`, false) + })); + } + + enableService(service: RecordService) { + recordPluginServiceState.set(`${this.name}-${service.title}`, true); + } + + openConfig() { + this.config.openInEditor(); + } +} + +export class NpmPlugin extends BasePlugin { + isInstalled = false; + + constructor(json: readPkg.NormalizedPackageJson, kap: {version?: string, macosVersion?: string} = {}) { + super(json.name); + + this.json = json; + this.kapVersion = kap.version; + this.macosVersion = kap.macosVersion; + this.link = this.json.homepage ?? this.json.links?.homepage; + } +} diff --git a/main/plugins/service-context.ts b/main/plugins/service-context.ts new file mode 100644 index 000000000..2b6affd07 --- /dev/null +++ b/main/plugins/service-context.ts @@ -0,0 +1,100 @@ +import {app, clipboard} from 'electron'; +import Store from 'electron-store'; +import got, {GotFn, GotPromise} from 'got'; +import {Format} from '../common/types'; +import {InstalledPlugin} from './plugin'; +import {addPluginPromise} from '../utils/deep-linking'; +import {notify} from '../utils/notifications'; + +interface ServiceContextOptions { + plugin: InstalledPlugin; +} + +class ServiceContext { + private plugin: InstalledPlugin; + requests: GotPromise[] = []; + + config: Store; + + constructor(options: ServiceContextOptions) { + this.plugin = options.plugin; + this.config = this.plugin.config; + } + + request = (...args: Parameters) => { + const request = got(...args); + this.requests.push(request); + return request; + } + + copyToClipboard = (text: string) => { + clipboard.writeText(text); + } + + notify = (text: string, action?: () => any) => { + return notify({ + body: text, + title: this.plugin.isBuiltIn ? app.name : this.plugin.prettyName, + click: action + }); + } + + openConfigFile = () => { + this.config.openInEditor(); + } + + waitForDeepLink = async () => { + return new Promise(resolve => { + addPluginPromise(this.plugin.name, resolve); + }); + } +} + +interface ShareServiceContextOptions extends ServiceContextOptions { + onProgress: (text: string, percentage: number) => void; + filePath: (options?: {fileType?: Format}) => Promise; + format: Format; + prettyFormat: string; + defaultFileName: string; + onCancel: () => void; +} + +export class ShareServiceContext extends ServiceContext { + private options: ShareServiceContextOptions; + + isCanceled = false; + + constructor(options: ShareServiceContextOptions) { + super(options); + this.options = options; + } + + get format() { + return this.options.format; + } + + get prettyFormat() { + return this.options.prettyFormat; + } + + get defaultFileName() { + return this.options.defaultFileName; + } + + filePath = (options: {fileType?: Format}) => { + return this.options.filePath(options); + } + + setProgress = (text: string, percentage: number) => { + this.options.onProgress(text, percentage); + } + + cancel = () => { + this.isCanceled = true; + this.options.onCancel(); + + for (const request of this.requests) { + request.cancel(); + } + } +} diff --git a/main/plugins/service.ts b/main/plugins/service.ts new file mode 100644 index 000000000..98f44e2b0 --- /dev/null +++ b/main/plugins/service.ts @@ -0,0 +1,27 @@ + +import {Format} from '../common/types'; +import {Schema} from '../utils/ajv'; +import {ShareServiceContext} from './service-context'; + +export interface Service { + title: string; + configDescription?: string; + config?: {[P in keyof Config]: Schema}; +} + +export interface ShareService extends Service { + formats: Format[]; + action: (context: ShareServiceContext) => void; +} + +export interface EditService extends Service { + action: () => {}; +} + +export interface RecordService extends Service { + willStartRecording?: () => {}; + didStartRecording?: () => {}; + didStopRecording?: () => {}; + willEnable?: () => {}; + cleanUp?: () => {}; +} diff --git a/main/recording-history.js b/main/recording-history.js index 0ea249395..82d01c617 100644 --- a/main/recording-history.js +++ b/main/recording-history.js @@ -224,7 +224,7 @@ const handleCorruptRecording = async (recording, error) => { const applicableErrors = knownErrors.filter(({test}) => test(error)); if (applicableErrors.length === 0) { - const Sentry = require('./utils/sentry'); + const Sentry = require('./utils/sentry').default; if (Sentry.isSentryEnabled) { // Collect info about possible unknown errors, to see if we can implement fixes using ffmpeg Sentry.captureException(new Error(`Corrupt recording: ${error}`)); diff --git a/main/remote-states/conversions/conversion.js b/main/remote-states/conversions/conversion.js deleted file mode 100644 index 19d2df13c..000000000 --- a/main/remote-states/conversions/conversion.js +++ /dev/null @@ -1,50 +0,0 @@ -const {convertTo} = require('../../convert'); - -const queue = new ConversionQueue(); - -class Conversion { - exports = [] - - constructor(options) { - this.video = options.video; - this.format = options.format; - this.exportOptions = options.exportOptions; - } - - convert = async ({fileType} = {}) => { - if (this.filePath) { - return this.filePath; - } - - this.convertProcess = queue.queueConversion( - () => { - if (this.canceled) { - return; - } - - return convertTo({ - ...this.exportOptions, - // FIXME - }); - } - ); - - this.filePath = await this.convertProcess; - return this.filePath; - } - - run = () => { - - } - - addExport = (newExport) => { - this.exports.push(newExport); - newExport.run(this); - } -} - -class Export { - constructor(options) { - - } -} diff --git a/main/remote-states/conversions/index.js b/main/remote-states/conversions/index.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/main/remote-states/conversions/video.js b/main/remote-states/conversions/video.js deleted file mode 100644 index ba9ce01bc..000000000 --- a/main/remote-states/conversions/video.js +++ /dev/null @@ -1,70 +0,0 @@ -const path = require('path'); -const getFps = require('../../utils/fps'); -const {getEncoding, convertToH264} = require('../../utils/encoding'); - -class Video { - constructor(options) { - this.filePath = options.filePath; - this.title = options.title || path.basename(this.filePath); - this.fps = options.fps; - this.encoding = options.encoding; - this.pixelDensity = options.pixelDensity || 1; - - this.whenReady = this._collectInfo(); - } - - async _collectInfo() { - return Promise.all([ - this.getFps(), - this.getEncoding(), - this.getPreviewPath() - ]); - } - - async getFps() { - if (!this.fps) { - this.fps = await getFps(this.filePath); - } - - return this.fps; - } - - async getEncoding() { - if (!this.encoding) { - this.encoding = getEncoding(this.filePath); - } - } - - async getPreviewPath() { - if (!this.previewPath) { - const encoding = await this.getEncoding(); - - if (encoding === 'h264') { - this.previewPath = this.filePath; - } else { - this.previewPath = await convertToH264(this.filePath) - } - } - - return this.previewPath; - } -} - -class Recording extends Video { - constructor(options) { - super({ - ...options, - fps: options.recordingOptions.fps, - encoding: options.recordingOptions.encoding, - pixelDensity: options.recordingOptions.pixelDensity, - title: options.recordingName - }); - - this.recordingOptions = options.recordingOptions; - } -} - -module.exports = { - Video, - Recording -}; diff --git a/main/remote-states/editor-options.js b/main/remote-states/editor-options.js deleted file mode 100644 index aad98cb90..000000000 --- a/main/remote-states/editor-options.js +++ /dev/null @@ -1,168 +0,0 @@ -'use strict'; -const Store = require('electron-store'); - -const plugins = require('../common/plugins'); -const {converters} = require('../convert'); -const {apps} = require('../plugins/open-with-plugin'); -const {showError} = require('../utils/errors'); - -const exportUsageHistory = new Store({ - name: 'export-usage-history', - defaults: { - apng: {lastUsed: 1, plugins: {default: 1}}, - webm: {lastUsed: 2, plugins: {default: 1}}, - mp4: {lastUsed: 3, plugins: {default: 1}}, - gif: {lastUsed: 4, plugins: {default: 1}}, - av1: {lastUsed: 5, plugins: {default: 1}} - } -}); - -const fpsUsageHistory = new Store({ - name: 'fps-usage-history', - schema: { - apng: { - type: 'number', - minimum: 0, - default: 60 - }, - webm: { - type: 'number', - minimum: 0, - default: 60 - }, - mp4: { - type: 'number', - minimum: 0, - default: 60 - }, - gif: { - type: 'number', - minimum: 0, - default: 60 - }, - av1: { - type: 'number', - minimum: 0, - default: 60 - } - } -}); - - -const prettifyFormat = format => { - const formats = new Map([ - ['apng', 'APNG'], - ['gif', 'GIF'], - ['mp4', 'MP4 (H264)'], - ['av1', 'MP4 (AV1)'], - ['webm', 'WebM'] - ]); - - return formats.get(format); -}; - -const getEditOptions = () => { - console.log(plugins.getEditPlugins()); - return plugins.getEditPlugins().flatMap( - plugin => plugin.editServices - .filter(service => plugin.config.validServices.includes(service.title)) - .map(service => ({ - title: service.title, - pluginName: plugin.name, - pluginPath: plugin.pluginPath, - hasConfig: Object.keys(service.config || {}).length > 0 - })) - ); -}; - -const getExportOptions = () => { - const installed = plugins.getSharePlugins(); - const builtIn = plugins.getBuiltIn(); - - const options = []; - for (const format of converters.keys()) { - options.push({ - format, - prettyFormat: prettifyFormat(format), - plugins: [] - }); - } - - for (const json of [...installed, ...builtIn]) { - if (!json.isCompatible) { - continue; - } - - try { - const plugin = require(json.pluginPath); - - for (const service of plugin.shareServices) { - for (const format of service.formats) { - options.find(option => option.format === format).plugins.push({ - title: service.title, - pluginName: json.name, - pluginPath: json.pluginPath, - apps: json.name === '_openWith' ? apps.get(format) : undefined - }); - } - } - } catch (error) { - showError(error, {title: `Something went wrong while loading “${json.name}”`, plugin: json}); - const Sentry = require('../utils/sentry'); - Sentry.captureException(error); - } - } - - const sortFunc = (a, b) => b.lastUsed - a.lastUsed; - - for (const option of options) { - const {lastUsed, plugins} = exportUsageHistory.get(option.format); - option.lastUsed = lastUsed; - option.plugins = option.plugins.map(plugin => ({...plugin, lastUsed: plugins[plugin.pluginName] || 0})).sort(sortFunc); - } - - return options.sort(sortFunc); -}; - -module.exports = sendUpdate => { - const state = { - formats: getExportOptions(), - editServices: getEditOptions(), - fpsHistory: fpsUsageHistory.store - }; - - const updatePlugins = () => { - state.formats = getExportOptions(); - state.editServices = getEditOptions(); - sendUpdate(state); - }; - - plugins.on('installed', updatePlugins); - plugins.on('uninstalled', updatePlugins); - plugins.on('config-changed', updatePlugins); - - const actions = { - updatePluginUsage: ({format, plugin}) => { - const usage = exportUsageHistory.get(format); - const now = Date.now(); - - usage.plugins[plugin] = now; - usage.lastUsed = now; - exportUsageHistory.set(format, usage); - - state.formats = getExportOptions(); - sendUpdate(state); - }, - updateFpsUsage: ({format, fps}) => { - fpsUsageHistory.set(format, fps); - - state.fpsHistory = fpsUsageHistory.store; - sendUpdate(state); - } - }; - - return { - actions, - getState: () => state - }; -}; diff --git a/main/remote-states/editor-options.ts b/main/remote-states/editor-options.ts new file mode 100644 index 000000000..5b306151b --- /dev/null +++ b/main/remote-states/editor-options.ts @@ -0,0 +1,159 @@ +import Store from 'electron-store'; +import {Format} from '../common/types'; +import {formats} from '../common/constants'; + +import plugins from '../plugins'; +import {apps, App} from '../plugins/built-in/open-with-plugin'; +import {prettifyFormat} from '../utils/formats'; + +const exportUsageHistory = new Store<{[key in Format]: {lastUsed: number, plugins: {[key: string]: number}}}>({ + name: 'export-usage-history', + defaults: { + apng: {lastUsed: 1, plugins: {default: 1}}, + webm: {lastUsed: 2, plugins: {default: 1}}, + mp4: {lastUsed: 3, plugins: {default: 1}}, + gif: {lastUsed: 4, plugins: {default: 1}}, + av1: {lastUsed: 5, plugins: {default: 1}} + } +}); + +const fpsUsageHistory = new Store<{[key in Format]: number}>({ + name: 'fps-usage-history', + schema: { + apng: { + type: 'number', + minimum: 0, + default: 60 + }, + webm: { + type: 'number', + minimum: 0, + default: 60 + }, + mp4: { + type: 'number', + minimum: 0, + default: 60 + }, + gif: { + type: 'number', + minimum: 0, + default: 60 + }, + av1: { + type: 'number', + minimum: 0, + default: 60 + } + } +}); + +const getEditOptions = () => { + // console.log(plugins.getEditPlugins()); + return plugins.editPlugins.flatMap( + plugin => plugin.editServices + .filter(service => plugin.config.validServices.includes(service.title)) + .map(service => ({ + title: service.title, + pluginName: plugin.name, + pluginPath: plugin.pluginPath, + hasConfig: Object.keys(service.config || {}).length > 0 + })) + ); +}; + +interface ExportOptionsPlugin { + title: string; + pluginName: string; + pluginPath: string; + apps?: App[]; + lastUsed: number; +} + +const getExportOptions = () => { + const installed = plugins.sharePlugins; + + const options = formats.map(format => ({ + format, + prettyFormat: prettifyFormat(format), + plugins: [] as ExportOptionsPlugin[], + lastUsed: exportUsageHistory.get(format).lastUsed + })); + + const sortFunc = (a: T, b: T) => b.lastUsed - a.lastUsed; + + for (const plugin of installed) { + if (!plugin.isCompatible) { + continue; + } + + for (const service of plugin.shareServices) { + for (const format of service.formats) { + options.find(option => option.format === format)?.plugins.push({ + title: service.title, + pluginName: plugin.name, + pluginPath: plugin.pluginPath, + apps: plugin.name === '_openWith' ? apps.get(format) : undefined, + lastUsed: exportUsageHistory.get(format).plugins?.[plugin.name] ?? 0 + }); + } + } + } + + return options.map(option => ({...option, plugins: option.plugins.sort(sortFunc)})).sort(sortFunc); +}; + + +export interface ExportOptions { + formats: ReturnType; + editServices: ReturnType; + fpsHistory: typeof fpsUsageHistory.store +} + + +const editorOptionsRemoteState = (sendUpdate: (state: ExportOptions) => void) => { + const state: ExportOptions = { + formats: getExportOptions(), + editServices: getEditOptions(), + fpsHistory: fpsUsageHistory.store + }; + + const updatePlugins = () => { + state.formats = getExportOptions(); + state.editServices = getEditOptions(); + sendUpdate(state); + } + + plugins.on('installed', updatePlugins); + plugins.on('uninstalled', updatePlugins); + plugins.on('config-changed', updatePlugins); + + const actions = { + updatePluginUsage: ({format, plugin}: {format: Format, plugin: string}) => { + const usage = exportUsageHistory.get(format); + const now = Date.now(); + + usage.plugins[plugin] = now; + usage.lastUsed = now; + exportUsageHistory.set(format, usage); + + state.formats = getExportOptions(); + sendUpdate(state); + }, + updateFpsUsage: ({format, fps}: {format: Format, fps: number}) => { + fpsUsageHistory.set(format, fps); + state.fpsHistory = fpsUsageHistory.store; + sendUpdate(state); + } + }; + + console.log(state); + return { + actions, + getState: () => state + } +}; + +export default editorOptionsRemoteState; +export type EditorOptions = typeof editorOptionsRemoteState; +export const name = 'editor-options'; diff --git a/main/remote-states/index.js b/main/remote-states/index.js deleted file mode 100644 index c0b7500b9..000000000 --- a/main/remote-states/index.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const {setupRemoteState} = require('../common/remote-state'); - -const remoteStateNames = ['editor-options']; - -for(const name of remoteStateNames) { - setupRemoteState(name, require(`./${name}`))(); -} diff --git a/main/remote-states/index.ts b/main/remote-states/index.ts new file mode 100644 index 000000000..2546df4f6 --- /dev/null +++ b/main/remote-states/index.ts @@ -0,0 +1,10 @@ +import setupRemoteState from './setup-remote-state'; + +const remoteStateNames = ['editor-options']; + +export const setupRemoteStates = async () => { + return Promise.all(remoteStateNames.map(async fileName => { + const state = require(`./${fileName}`); + setupRemoteState(state.name, state.default); + })); +}; diff --git a/main/remote-states/setup-remote-state.ts b/main/remote-states/setup-remote-state.ts new file mode 100644 index 000000000..0177d5a77 --- /dev/null +++ b/main/remote-states/setup-remote-state.ts @@ -0,0 +1,44 @@ +import {RemoteState, getChannelNames} from './utils'; +import {ipcMain} from 'electron-better-ipc'; +import {BrowserWindow} from 'electron'; + +const setupRemoteState = async (name: string, callback: RemoteState) => { + const channelNames = getChannelNames(name); + + const renderers = new Map(); + + const sendUpdate = async (state?: State, id?: string) => { + if (id) { + return ipcMain.callRenderer(renderers.get(id), channelNames.stateUpdated, state); + } + + for (const [windowId, renderer] of renderers.entries()) { + ipcMain.callRenderer(renderer, channelNames.stateUpdated, state ?? (await getState?.(windowId))); + } + } + + const {getState, actions = {}} = await callback(sendUpdate); + + ipcMain.answerRenderer(channelNames.subscribe, (customId: string, window: BrowserWindow) => { + const id = customId ?? window.id.toString(); + renderers.set(id, window); + + window.on('closed', () => { + renderers.delete(id); + }); + + return Object.keys(actions); + }); + + ipcMain.answerRenderer(channelNames.getState, async (customId: string, window: BrowserWindow) => { + const id = customId ?? window.id.toString(); + return getState(id); + }); + + ipcMain.answerRenderer(channelNames.callAction, ({key, data, id: customId}: any, window: BrowserWindow) => { + const id = customId || window.id.toString(); + return (actions as any)[key]?.(data, id); + }); +} + +export default setupRemoteState; diff --git a/main/remote-states/use-remote-state.ts b/main/remote-states/use-remote-state.ts new file mode 100644 index 000000000..92ba07cf4 --- /dev/null +++ b/main/remote-states/use-remote-state.ts @@ -0,0 +1,60 @@ +import {RemoteState, getChannelNames} from './utils'; +import {useState, useEffect, useRef} from 'react'; +import {ipcRenderer} from 'electron-better-ipc'; + +const useRemoteState = >( + name: string, + initialState?: Callback extends RemoteState ? State : never +): (id?: string) => ( + Callback extends RemoteState ? ( + Actions & { + state: State; + isLoading: false; + refreshState: () => void; + } + ) : never +) => { + const channelNames = getChannelNames(name); + + return (id?: string) => { + const [state, setState] = useState(initialState); + const [isLoading, setIsLoading] = useState(true); + const actionsRef = useRef({}); + + useEffect(() => { + const cleanup = ipcRenderer.answerMain(channelNames.stateUpdated, setState); + + (async () => { + const actionKeys = (await ipcRenderer.callMain(channelNames.subscribe, id)) as string[]; + + const actions = actionKeys.reduce((acc, key) => ({ + ...acc, + [key]: (data: any) => ipcRenderer.callMain(channelNames.callAction, {key, data, id}) + }), {}); + + const getState = async () => { + const newState = (await ipcRenderer.callMain(channelNames.getState, id)) as typeof state; + setState(newState); + } + + actionsRef.current = { + ...actions, + refreshState: getState + }; + + await getState(); + setIsLoading(false); + })(); + + return cleanup; + }, []); + + return { + ...actionsRef.current, + isLoading, + state + }; + } +} + +export default useRemoteState; diff --git a/main/remote-states/utils.ts b/main/remote-states/utils.ts new file mode 100644 index 000000000..82db9a565 --- /dev/null +++ b/main/remote-states/utils.ts @@ -0,0 +1,14 @@ + +export const getChannelName = (name: string, action: string) => `kap-remote-state-${name}-${action}`; + +export const getChannelNames = (name: string) => ({ + subscribe: getChannelName(name, 'subscribe'), + getState: getChannelName(name, 'get-state'), + callAction: getChannelName(name, 'call-action'), + stateUpdated: getChannelName(name, 'state-updated') +}); + +export type RemoteState = (sendUpdate: (state?: State, id?: string) => void) => { + getState: (id?: string) => State, + actions: Actions +} diff --git a/main/utils/ajv.ts b/main/utils/ajv.ts index 05f55751d..62ae54714 100644 --- a/main/utils/ajv.ts +++ b/main/utils/ajv.ts @@ -1,4 +1,10 @@ import Ajv, {Options} from 'ajv'; +import {Schema as JSONSchema} from 'electron-store'; + +export type Schema = Omit & { + required?: boolean; + customType?: string; +} const hexColorValidator = () => { return { @@ -13,10 +19,10 @@ const keyboardShortcutValidator = () => { }; }; -const validators = new Map object>([ - ['hexColor', hexColorValidator], - ['keyboardShortcut', keyboardShortcutValidator] -]); +const validators: {[key: string]: (parentSchema: object) => object} = { + 'hexColor': hexColorValidator, + 'keyboardShortcut': keyboardShortcutValidator +}; export default class CustomAjv extends Ajv { constructor(options: Options) { @@ -24,7 +30,7 @@ export default class CustomAjv extends Ajv { this.addKeyword('customType', { macro: (schema, parentSchema) => { - const validator = validators.get(schema); + const validator = validators[schema]; if (!validator) { throw new Error(`No custom type found for ${schema}`); @@ -34,7 +40,7 @@ export default class CustomAjv extends Ajv { }, metaSchema: { type: 'string', - enum: [...validators.keys()] + enum: [Object.keys(validators)] } }); } diff --git a/main/utils/devices.js b/main/utils/devices.js index e7d14f6b9..75f6844bf 100644 --- a/main/utils/devices.js +++ b/main/utils/devices.js @@ -34,7 +34,7 @@ const getAudioDevices = async () => { const devices = await aperture.audioDevices(); if (!Array.isArray(devices)) { - const Sentry = require('./sentry'); + const Sentry = require('./sentry').default; Sentry.captureException(new Error(`devices is not an array: ${JSON.stringify(devices)}`)); showError(error); return []; diff --git a/main/utils/encoding.js b/main/utils/encoding.ts similarity index 54% rename from main/utils/encoding.js rename to main/utils/encoding.ts index 000cbb0ab..5be357582 100644 --- a/main/utils/encoding.js +++ b/main/utils/encoding.ts @@ -1,26 +1,27 @@ /* eslint-disable array-element-newline */ -'use strict'; -const path = require('path'); -const tmp = require('tmp'); -const ffmpeg = require('@ffmpeg-installer/ffmpeg'); -const util = require('electron-util'); -const execa = require('execa'); -const {track} = require('../common/analytics'); +import path from 'path'; +import util from 'electron-util'; +import execa from 'execa'; +import tempy from 'tempy'; +import {track} from '../common/analytics'; +const ffmpeg = require('@ffmpeg-installer/ffmpeg'); const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path); -const getEncoding = async filePath => { +export const getEncoding = async (filePath: string) => { try { await execa(ffmpegPath, ['-i', filePath]); + return undefined; } catch (error) { - return /.*: Video: (.*?) \(.*/.exec(error.stderr)[1]; + return /.*: Video: (.*?) \(.*/.exec(error.stderr)?.[1]; } }; // `ffmpeg -i original.mp4 -vcodec libx264 -crf 27 -preset veryfast -c:a copy output.mp4` -const convertToH264 = async inputPath => { - const outputPath = tmp.tmpNameSync({postfix: path.extname(inputPath)}); +export const convertToH264 = async (inputPath: string) => { + const outputPath = tempy.file({extension: path.extname(inputPath)}); + track('encoding/converted/hevc'); await execa(ffmpegPath, [ @@ -34,8 +35,3 @@ const convertToH264 = async inputPath => { return outputPath; }; - -module.exports = { - getEncoding, - convertToH264 -}; diff --git a/main/utils/errors.js b/main/utils/errors.js index 5f636bcde..d02342712 100644 --- a/main/utils/errors.js +++ b/main/utils/errors.js @@ -71,6 +71,7 @@ ${errorStack} `; const showError = async (error, {title: customTitle, plugin} = {}) => { + await app.whenReady(); const ensuredError = ensureError(error); const title = customTitle || ensuredError.name; const detail = getPrettyStack(ensuredError); @@ -111,12 +112,12 @@ const showError = async (error, {title: customTitle, plugin} = {}) => { } // Avoids circular dependency - const Sentry = require('./sentry'); + const {default: Sentry, isSentryEnabled} = require('./sentry'); let message; const buttons = [...mainButtons]; - if (isOnline && Sentry.isSentryEnabled) { + if (isOnline && isSentryEnabled) { const eventId = Sentry.captureException(ensuredError); const sentryIssuePromise = getSentryIssue(eventId); diff --git a/main/utils/formats.ts b/main/utils/formats.ts new file mode 100644 index 000000000..28f22bd4b --- /dev/null +++ b/main/utils/formats.ts @@ -0,0 +1,13 @@ +import {Format} from '../common/types'; + +const formats = new Map([ + [Format.gif, 'GIF'], + [Format.mp4, 'MP4 (H264)'], + [Format.av1, 'MP4 (AV1)'], + [Format.webm, 'WebM'], + [Format.apng, 'APNG'] +]); + +export const prettifyFormat = (format: Format): string => { + return formats.get(format) as string; +} diff --git a/main/utils/fps.js b/main/utils/fps.js deleted file mode 100644 index 35ff0fa6f..000000000 --- a/main/utils/fps.js +++ /dev/null @@ -1,13 +0,0 @@ -const ffmpeg = require('@ffmpeg-installer/ffmpeg'); -const util = require('electron-util'); -const execa = require('execa'); - -const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path); - -module.exports = async filePath => { - try { - await execa(ffmpegPath, ['-i', filePath]); - } catch (error) { - return /.*, (.*) fp.*/.exec(error.stderr)[1]; - } -}; diff --git a/main/utils/fps.ts b/main/utils/fps.ts new file mode 100644 index 000000000..9fb50ce24 --- /dev/null +++ b/main/utils/fps.ts @@ -0,0 +1,16 @@ +import util from 'electron-util'; +import execa from 'execa'; + +const ffmpeg = require('@ffmpeg-installer/ffmpeg'); +const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path); + +const getFps = async (filePath: string) => { + try { + await execa(ffmpegPath, ['-i', filePath]); + return undefined; + } catch (error) { + return /.*, (.*) fp.*/.exec(error.stderr)?.[1]; + } +}; + +export default getFps; diff --git a/main/utils/notifications.ts b/main/utils/notifications.ts new file mode 100644 index 000000000..927866226 --- /dev/null +++ b/main/utils/notifications.ts @@ -0,0 +1,70 @@ +import {Notification, NotificationConstructorOptions, NotificationAction, app} from 'electron'; + +// Need to persist the notifications, otherwise it is garbage collected and the actions don't trigger +// https://github.com/electron/electron/issues/12690 +const notifications = new Set(); + +interface Action extends NotificationAction { + action?: () => void | Promise; +} + +interface NotificationOptions extends NotificationConstructorOptions { + actions?: Action[]; + click?: () => void | Promise; + show?: boolean; +} + +type NotificationPromise = Promise & { + show: () => void; + close: () => void; +} + +export const notify = (options: NotificationOptions): NotificationPromise => { + const notification = new Notification(options); + + notifications.add(notification); + + const promise = new Promise(resolve => { + if (options.click && typeof options.click === 'function') { + notification.on('click', () => { + resolve(options.click?.()) + }); + } + + if (options.actions && options.actions.length > 0) { + notification.on('action', (_, index) => { + const button = options.actions?.[index]; + + if (button?.action && typeof button?.action === 'function') { + resolve(button?.action?.()) + } else { + resolve(index); + } + }); + } + + notification.on('close', () => { + resolve(); + }); + }); + + promise.then(() => { + notifications.delete(notification) + }); + + (promise as NotificationPromise).show = () => { + notification.show(); + } + + (promise as NotificationPromise).close = () => { + notification.close(); + } + + if (options.show ?? true) { + notification.show(); + } + + return promise as NotificationPromise; +}; + +notify.simple = (text: string) => notify({title: app.name, body: text}); diff --git a/main/utils/sentry.js b/main/utils/sentry.js deleted file mode 100644 index f73535283..000000000 --- a/main/utils/sentry.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -const {app} = require('electron'); -const {is} = require('electron-util'); -const Sentry = require('@sentry/electron'); -const settings = require('../common/settings'); - -const SENTRY_PUBLIC_DSN = 'https://2dffdbd619f34418817f4db3309299ce@sentry.io/255536'; - -const isSentryEnabled = !is.development && settings.get('allowAnalytics'); - -if (isSentryEnabled) { - const release = `${app.name}@${app.getVersion()}`.toLowerCase(); - Sentry.init({ - dsn: SENTRY_PUBLIC_DSN, - release - }); -} - -module.exports = Sentry; -module.exports.isSentryEnabled = isSentryEnabled; diff --git a/main/utils/sentry.ts b/main/utils/sentry.ts new file mode 100644 index 000000000..d044e0644 --- /dev/null +++ b/main/utils/sentry.ts @@ -0,0 +1,20 @@ +'use strict'; + +import {app} from 'electron'; +import {is} from 'electron-util'; +import Sentry from '@sentry/electron'; +import settings from '../common/settings'; + +const SENTRY_PUBLIC_DSN = 'https://2dffdbd619f34418817f4db3309299ce@sentry.io/255536'; + +export const isSentryEnabled = !is.development && settings.get('allowAnalytics'); + +if (isSentryEnabled) { + const release = `${app.name}@${app.getVersion()}`.toLowerCase(); + Sentry.init({ + dsn: SENTRY_PUBLIC_DSN, + release + }); +} + +export default Sentry; diff --git a/main/utils/shortcut-to-accelerator.js b/main/utils/shortcut-to-accelerator.ts similarity index 80% rename from main/utils/shortcut-to-accelerator.js rename to main/utils/shortcut-to-accelerator.ts index 1926ea618..9876e47fd 100644 --- a/main/utils/shortcut-to-accelerator.js +++ b/main/utils/shortcut-to-accelerator.ts @@ -1,4 +1,5 @@ -const shortcutToAccelerator = shortcut => { + +export const shortcutToAccelerator = (shortcut: any) => { const {metaKey, altKey, ctrlKey, shiftKey, character} = shortcut; if (!character) { throw new Error(`shortcut needs character ${JSON.stringify(shortcut)}`); @@ -13,5 +14,3 @@ const shortcutToAccelerator = shortcut => { ].filter(Boolean); return keys.join('+'); }; - -module.exports = shortcutToAccelerator; diff --git a/main/utils/timestamped-name.js b/main/utils/timestamped-name.js deleted file mode 100644 index 079e438d8..000000000 --- a/main/utils/timestamped-name.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; -const moment = require('moment'); - -module.exports = { - generateTimestampedName: (title = 'New Recording', extension = '') => `${title} ${moment().format('YYYY-MM-DD')} at ${moment().format('HH.mm.ss')}${extension}` -}; diff --git a/main/utils/timestamped-name.ts b/main/utils/timestamped-name.ts new file mode 100644 index 000000000..237aefb74 --- /dev/null +++ b/main/utils/timestamped-name.ts @@ -0,0 +1,3 @@ +import moment from 'moment'; + +export const generateTimestampedName = (title = 'New Recording', extension = '') => `${title} ${moment().format('YYYY-MM-DD')} at ${moment().format('HH.mm.ss')}${extension}` diff --git a/main/video.ts b/main/video.ts new file mode 100644 index 000000000..9badf5f59 --- /dev/null +++ b/main/video.ts @@ -0,0 +1,123 @@ +import path from 'path'; +import getFps from './utils/fps'; +import {getEncoding, convertToH264} from './utils/encoding'; +import {Rectangle, screen} from 'electron'; +import {Encoding} from './common/types'; +import {generateTimestampedName} from './utils/timestamped-name'; + +interface VideoOptions { + filePath: string; + title?: string; + fps?: number; + encoding?: Encoding; + pixelDensity?: number; +} + +export class Video { + static all = new Map(); + + static fromId(id: string) { + return this.all.get(id); + } + + filePath: string; + title: string; + fps?: number; + encoding?: Encoding; + pixelDensity: number; + previewPath?: string; + + isNewRecording = false; + + isReady = false; + private readyPromise: Promise; + private previewReadyPromise: Promise; + + constructor(options: VideoOptions) { + this.filePath = options.filePath; + this.title = options.title ?? path.basename(this.filePath); + this.fps = options.fps; + this.encoding = options.encoding; + this.pixelDensity = options.pixelDensity ?? 1; + + Video.all.set(this.filePath, this); + + this.readyPromise = this.collectInfo(); + this.previewReadyPromise = this.readyPromise.then(() => this.getPreviewPath()); + } + + private async collectInfo() { + await Promise.all([ + this.getFps(), + this.getEncoding(), + ]); + + this.isReady = true; + } + + async getFps() { + if (!this.fps) { + this.fps = Math.round(Number.parseFloat((await getFps(this.filePath)) ?? '0')); + } + + return this.fps; + } + + async getEncoding() { + if (!this.encoding) { + this.encoding = (await getEncoding(this.filePath)) as Encoding; + } + + return this.encoding; + } + + async getPreviewPath() { + if (!this.previewPath) { + if (this.encoding === 'h264') { + this.previewPath = this.filePath; + } else { + this.previewPath = await convertToH264(this.filePath); + } + } + + return this.encoding; + } + + async whenReady() { + return this.readyPromise; + } + + async whenPreviewReady() { + return this.previewReadyPromise; + } +} + +interface ApertureOptions { + fps: number; + cropArea: Rectangle; + showCursor: boolean; + highlightClicks: boolean; + screenId: number; + audioDeviceId: string; + videoCodec: Encoding; +} + +export class Recording extends Video { + apertureOptions: ApertureOptions; + + constructor(options: VideoOptions & { apertureOptions: ApertureOptions }) { + const displays = screen.getAllDisplays(); + const pixelDensity = displays.find(display => display.id === options.apertureOptions.screenId)?.scaleFactor; + + super({ + filePath: options.filePath, + title: options.title || generateTimestampedName(), + fps: options.apertureOptions.fps, + encoding: options.apertureOptions.videoCodec, + pixelDensity + }); + + this.apertureOptions = options.apertureOptions; + this.isNewRecording = true; + } +} diff --git a/package.json b/package.json index 3861432ba..513f4ac1d 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "dist": "npm run build && electron-builder", "pack": "npm run build && electron-builder --dir", "postinstall": "electron-builder install-app-deps", - "sentry-version": "echo \"$npm_package_name@$npm_package_version\"" + "sentry-version": "echo \"$npm_package_name@$npm_package_version\"", + "dev": "next dev renderer" }, "bundle": { "name": "Kap" @@ -39,7 +40,7 @@ "classnames": "^2.2.6", "clean-stack": "^3.0.0", "delay": "^4.3.0", - "electron-better-ipc": "^1.1.0", + "electron-better-ipc": "^1.1.1", "electron-log": "^4.1.1", "electron-next": "^3.1.5", "electron-notarize": "^0.3.0", @@ -65,6 +66,7 @@ "move-file": "^2.0.0", "nearest-normal-aspect-ratio": "^1.2.1", "node-mac-app-icon": "^1.4.0", + "object-hash": "^2.0.3", "p-cancelable": "^2.0.0", "p-event": "^4.1.0", "package-json": "^6.5.0", @@ -87,22 +89,30 @@ }, "devDependencies": { "@sindresorhus/tsconfig": "^0.7.0", + "@types/got": "9.6.0", + "@types/insight": "^0.8.0", + "@types/node": "^14.11.10", + "@types/object-hash": "^1.3.4", "@types/react": "^16.9.46", + "@typescript-eslint/eslint-plugin": "^4.4.1", + "@typescript-eslint/parser": "^4.4.1", "ava": "^3.9.0", "babel-eslint": "^10.1.0", "electron": "8.2.4", "electron-builder": "^22.6.0", "electron-builder-notarize": "^1.1.2", + "eslint-config-xo": "^0.33.1", "eslint-config-xo-react": "^0.23.0", + "eslint-config-xo-typescript": "^0.35.0", "eslint-plugin-react": "^7.19.0", "eslint-plugin-react-hooks": "^3.0.0", "husky": "^4.2.5", "module-alias": "^2.2.2", - "next": "^9.3.6", + "next": "^9.5.5", "sinon": "^9.0.2", "typescript": "^4.0.3", "unique-string": "^2.0.0", - "xo": "^0.30.0" + "xo": "^0.34.1" }, "_moduleAliases": { "electron": "test/mocks/electron.js" @@ -121,13 +131,21 @@ ] }, "xo": { - "parser": "babel-eslint", - "extends": "xo-react", + "extends": [ + "xo-react", + "xo-typescript" + ], "space": 2, "envs": [ "node", "browser" ], + "parserOptions": { + "project": [ + "tsconfig.json", + "renderer/tsconfig.json" + ] + }, "rules": { "import/no-extraneous-dependencies": "off", "import/no-unassigned-import": "off", @@ -140,6 +158,7 @@ "ava/use-test": "off" }, "ignores": [ + "dist-js", "dist", "renderer/.next", "renderer/out" diff --git a/renderer/common/remote-state-types.ts b/renderer/common/remote-state-types.ts new file mode 100644 index 000000000..53e478c5b --- /dev/null +++ b/renderer/common/remote-state-types.ts @@ -0,0 +1,52 @@ +import {Format} from './types' + +// TODO: import these from main/remote-states files when we can integrate common TS files + +export type App = { + url: string; + isDefault: boolean; + icon: string; + name: string; +} + +type ExportOptionsPlugin = { + title: string; + pluginName: string; + pluginPath: string; + apps?: App[]; + lastUsed: number; +} + +export type ExportOptionsFormat = { + plugins: ExportOptionsPlugin[]; + format: Format; + prettyFormat: string; + lastUsed: number; +} + +type ExportOptionsEditService = { + title: string; + pluginName: string; + pluginPath: string; + hasConfig: boolean; +} + +export type ExportOptions = { + formats: ExportOptionsFormat[]; + editServices: ExportOptionsEditService[]; + fpsHistory: {[key in Format]: number}; +} + +export type EditorOptions = (sendUpdate: (state: ExportOptions) => void) => { + actions: { + updatePluginUsage: ({ format, plugin } : { + format: Format; + plugin: string; + }) => void; + updateFpsUsage: ({ format, fps }: { + format: Format; + fps: number; + }) => void; + }; + getState: () => ExportOptions; +} diff --git a/renderer/common/types.ts b/renderer/common/types.ts new file mode 100644 index 000000000..821e9401a --- /dev/null +++ b/renderer/common/types.ts @@ -0,0 +1,43 @@ +import {App} from './remote-state-types' + +export type CreateConversionOptions = { + filePath: string; + options: ConversionOptions; + format: Format; + plugins: { + share: { + pluginName: string; + serviceTitle: string; + app?: App + }, + edit?: { + pluginName: string; + serviceTitle: string; + } + } +} + +export type ConversionOptions = { + startTime: number; + endTime: number; + width: number; + height: number; + fps: number; + shouldCrop: boolean; + shouldMute: boolean; +} + +export enum Format { + gif = 'gif', + mp4 = 'mp4', + webm = 'webm', + apng = 'apng', + av1 = 'av1' +} + +export enum Encoding { + h264 = 'h264', + hevc = 'hevc', + proRes422 = 'proRes422', + proRes4444 = 'proRes4444' +} diff --git a/renderer/components/editor/controls/preview.tsx b/renderer/components/editor/controls/preview.tsx index 34197c366..0da3778bb 100644 --- a/renderer/components/editor/controls/preview.tsx +++ b/renderer/components/editor/controls/preview.tsx @@ -1,5 +1,5 @@ import VideoTimeContainer from '../video-time-container'; -import useWindowArgs from '../../../hooks/window-args'; +import useWindowState from '../../../hooks/window-state'; import formatTime from '../../../utils/format-time'; import {useRef, useEffect} from 'react'; @@ -12,7 +12,7 @@ type Props = { const Preview = ({time, labelTime, duration, hidePreview}: Props) => { const videoRef = useRef(); - const {filePath} = useWindowArgs(); + const {filePath} = useWindowState(); const src = `file://${filePath}`; useEffect(() => { diff --git a/renderer/components/editor/conversion/index.tsx b/renderer/components/editor/conversion/index.tsx new file mode 100644 index 000000000..66779bbc2 --- /dev/null +++ b/renderer/components/editor/conversion/index.tsx @@ -0,0 +1,5 @@ +const EditorConversionView = () => { + return
Hello there!
+}; + +export default EditorConversionView; diff --git a/renderer/components/editor/editor-options.tsx b/renderer/components/editor/editor-options.tsx deleted file mode 100644 index 2be3bf6fc..000000000 --- a/renderer/components/editor/editor-options.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import {useRemoteState} from '../../../main/common/remote-state'; - -const useEditorOptions = useRemoteState('editor-options', {formats: [], editServices: [], fpsHistory: {}}); - -export default useEditorOptions; diff --git a/renderer/components/editor/editor-preview.tsx b/renderer/components/editor/editor-preview.tsx index d170eaa60..7ec6c1386 100644 --- a/renderer/components/editor/editor-preview.tsx +++ b/renderer/components/editor/editor-preview.tsx @@ -1,10 +1,10 @@ -import useWindowArgs from '../../hooks/window-args'; +import useWindowState from '../../hooks/window-state'; import TrafficLights from '../traffic-lights'; import VideoPlayer from './video-player'; import Options from './options'; const EditorPreview = () => { - const {title = 'Editor'} = useWindowArgs(); + const {title = 'Editor'} = useWindowState(); return (
diff --git a/renderer/components/editor/index.js b/renderer/components/editor/index.js deleted file mode 100644 index 9149c70ae..000000000 --- a/renderer/components/editor/index.js +++ /dev/null @@ -1,88 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import {connect, EditorContainer} from '../../containers'; -import TrafficLights from '../traffic-lights'; -import VideoPlayer from './video-player'; - -class Editor extends React.Component { - state = { - hover: false - } - - mouseEnter = () => { - this.setState({hover: true}); - } - - mouseLeave = () => { - this.setState({hover: false}); - } - - render() { - const {hover} = this.state; - const {title = 'Editor'} = this.props; - - return ( -
-
-
- -
{title}
-
-
- - -
- ); - } -} - -Editor.propTypes = { - title: PropTypes.string -}; - -export default connect( - [EditorContainer], - ({title}) => ({title}) -)(Editor); diff --git a/renderer/components/editor/index.tsx b/renderer/components/editor/index.tsx new file mode 100644 index 000000000..3fabf1d9c --- /dev/null +++ b/renderer/components/editor/index.tsx @@ -0,0 +1,110 @@ +import useConversionContext from 'hooks/editor/use-conversion'; +import useEditorWindowState from 'hooks/editor/use-editor-window-state'; +import {useEffect} from 'react'; +import EditorConversionView from './conversion'; +import EditorPreview from './editor-preview'; + +const Editor = () => { + const {conversionId, setConversionId} = useConversionContext(); + const state = useEditorWindowState(); + + useEffect(() => { + if (state.conversionId && !conversionId) { + setConversionId(state.conversionId); + } + }, [state.conversionId]); + + return conversionId ? : ; +} + +export default Editor; + + +// import PropTypes from 'prop-types'; +// import React from 'react'; + +// import {connect, EditorContainer} from '../../containers'; +// import TrafficLights from '../traffic-lights'; +// import VideoPlayer from './video-player'; + +// class Editor extends React.Component { +// state = { +// hover: false +// } + +// mouseEnter = () => { +// this.setState({hover: true}); +// } + +// mouseLeave = () => { +// this.setState({hover: false}); +// } + +// render() { +// const {hover} = this.state; +// const {title = 'Editor'} = this.props; + +// return ( +//
+//
+//
+// +//
{title}
+//
+//
+// +// +//
+// ); +// } +// } + +// Editor.propTypes = { +// title: PropTypes.string +// }; + +// export default connect( +// [EditorContainer], +// ({title}) => ({title}) +// )(Editor); diff --git a/renderer/components/editor/options-container.tsx b/renderer/components/editor/options-container.tsx index b9b8bcec5..bc3f34b71 100644 --- a/renderer/components/editor/options-container.tsx +++ b/renderer/components/editor/options-container.tsx @@ -1,17 +1,25 @@ import {useState, useEffect} from 'react' import {createContainer} from 'unstated-next'; -import {FormatName, EditService} from '../../../main/common/remote-state'; -import useWindowArgs from '../../hooks/window-args'; +import useWindowState from 'hooks/window-state'; import VideoMetadataContainer from './video-metadata-container'; import VideoControlsContainer from './video-controls-container'; -import useEditorOptions from './editor-options'; +import useEditorOptions, {EditorOptionsState} from 'hooks/editor/use-editor-options'; +import {Format} from 'common/types'; +import {App} from 'common/remote-state-types'; +type EditService = EditorOptionsState["editServices"][0] -const isFormatMuted = (format: FormatName) => ['gif', 'apng'].includes(format); +type SharePlugin = { + pluginName: string; + serviceTitle: string; + app?: App; +} + +const isFormatMuted = (format: Format) => ['gif', 'apng'].includes(format); const useOptions = () => { - const {fps: originalFps} = useWindowArgs(); + const {fps: originalFps} = useWindowState(); const { state: { formats, @@ -21,15 +29,17 @@ const useOptions = () => { updateFpsUsage, isLoading } = useEditorOptions(); + console.log(formats, fpsHistory); const metadata = VideoMetadataContainer.useContainer(); const {isMuted, mute, unmute} = VideoControlsContainer.useContainer(); - const [format, setFormat] = useState(); + const [format, setFormat] = useState(); const [fps, setFps] = useState(); const [width, setWidth] = useState(); const [height, setHeight] = useState(); const [editPlugin, setEditPlugin] = useState(); + const [sharePlugin, setSharePlugin] = useState(); const [wasMuted, setWasMuted] = useState(false); @@ -38,7 +48,11 @@ const useOptions = () => { setFps(newFps); } - const updateFormat = (formatName: FormatName) => { + const updateSharePlugin = (plugin: SharePlugin) => { + setSharePlugin(plugin); + } + + const updateFormat = (formatName: Format) => { if (metadata.hasAudio) { if (isFormatMuted(formatName) && !isFormatMuted(format)) { setWasMuted(isMuted); @@ -48,7 +62,21 @@ const useOptions = () => { } } + const formatOption = formats.find(f => f.format === formatName); + const selectedSharePlugin = formatOption.plugins.find(plugin => { + return ( + plugin.pluginName === sharePlugin.pluginName && + plugin.title === sharePlugin.serviceTitle && + (plugin.apps?.some(app => app.url === sharePlugin.app?.url) ?? true) + ); + }) ?? formatOption.plugins.find(plugin => plugin.pluginName !== '_openWith'); + setFormat(formatName); + setSharePlugin({ + pluginName: selectedSharePlugin.pluginName, + serviceTitle: selectedSharePlugin.title, + app: selectedSharePlugin.apps ? sharePlugin.app : undefined + }); updateFps(Math.min(originalFps, fpsHistory[formatName]), formatName); } @@ -57,9 +85,18 @@ const useOptions = () => { return; } - const formatName = formats[0].format + const firstFormat = formats[0]; + const formatName = firstFormat.format; setFormat(formatName); + + const firstPlugin = firstFormat.plugins.find(plugin => plugin.pluginName !== '_openWith'); + + setSharePlugin(firstPlugin && { + pluginName: firstPlugin.pluginName, + serviceTitle: firstPlugin.title + }); + updateFps(Math.min(originalFps, fpsHistory[formatName]), formatName); }, [isLoading]); @@ -91,6 +128,8 @@ const useOptions = () => { editPlugin, formats, editServices, + sharePlugin, + updateSharePlugin, updateFps, updateFormat, setEditPlugin, diff --git a/renderer/components/editor/options/left.tsx b/renderer/components/editor/options/left.tsx index c6b8e54ee..dfc874fff 100644 --- a/renderer/components/editor/options/left.tsx +++ b/renderer/components/editor/options/left.tsx @@ -6,7 +6,7 @@ import {useState, useEffect, useMemo} from 'react'; import * as stringMath from 'string-math'; import VideoMetadataContainer from '../video-metadata-container'; import {shake} from '../../../utils/inputs'; -import Select from './select'; +import Select, {Separator} from './select'; const percentValues = [100, 75, 50, 33, 25, 20, 10]; @@ -146,7 +146,7 @@ const LeftOptions = () => { }, { separator: true - }, + } as Separator, ...options ]; } diff --git a/renderer/components/editor/options/right.tsx b/renderer/components/editor/options/right.tsx index 19a8290e3..1a5f0cba7 100644 --- a/renderer/components/editor/options/right.tsx +++ b/renderer/components/editor/options/right.tsx @@ -2,6 +2,12 @@ import {GearIcon} from '../../../vectors'; import OptionsContainer from '../options-container'; import Select from './select'; import {ipcRenderer as ipc} from 'electron-better-ipc'; +import useConversionContext from 'hooks/editor/use-conversion'; +import Options from '.'; +import useEditorWindowState from 'hooks/editor/use-editor-window-state'; +import VideoTimeContainer from '../video-time-container'; +import VideoControlsContainer from '../video-controls-container'; +import useSharePlugins from 'hooks/editor/use-share-plugins'; const FormatSelect = () => { const {formats, format, updateFormat} = OptionsContainer.useContainer(); @@ -10,6 +16,11 @@ const FormatSelect = () => { return ; +} + const EditPluginsControl = () => { const {editServices, editPlugin, setEditPlugin} = OptionsContainer.useContainer(); @@ -106,12 +117,72 @@ const EditPluginsControl = () => { ); } +const ConvertButton = () => { + const {startConversion} = useConversionContext(); + const options = OptionsContainer.useContainer(); + const {filePath} = useEditorWindowState(); + const {startTime, endTime} = VideoTimeContainer.useContainer(); + const {isMuted} = VideoControlsContainer.useContainer(); + + const onClick = () => { + const shouldCrop = true; + console.log('HERE'); + startConversion({ + filePath, + options: { + width: options.width, + height: options.height, + startTime, + endTime, + fps: options.fps, + shouldMute: isMuted, + shouldCrop + }, + format: options.format, + plugins: { + share: options.sharePlugin + } + }); + } + + return ( + + ) +} + const RightOptions = () => { return (
- +
+
@@ -283,11 +337,11 @@ export default RightOptions; // width: 128px; // } - // .plugin { - // height: 24px; - // width: 128px; - // margin-right: 8px; - // } + // .plugin { + // height: 24px; + // width: 128px; + // margin-right: 8px; + // } // button { // padding: 4px 8px; diff --git a/renderer/components/editor/options/select.tsx b/renderer/components/editor/options/select.tsx index 07880d7a0..7c09be9f3 100644 --- a/renderer/components/editor/options/select.tsx +++ b/renderer/components/editor/options/select.tsx @@ -1,20 +1,32 @@ import {DropdownArrowIcon, CancelIcon} from '../../../vectors'; import classNames from 'classnames'; import {useRef} from 'react'; -import {remote, MenuItemConstructorOptions} from 'electron'; +import {remote, MenuItemConstructorOptions, NativeImage} from 'electron'; type Option = { label: string; value: T; - subMenu?: MenuItemConstructorOptions[]; - separator?: boolean; + subMenu?: Option[]; type?: string; checked?: boolean; + click?: () => void; + separator?: false; + icon?: NativeImage; +} + +export type Separator = { + value: never; + label: never; + subMenu: never; + type: never; + checked: never; + icon: never; + separator: true; } interface Props { value?: T; - options: Option[]; + options: (Option | Separator)[]; onChange: (newValue?: T) => void; clearable?: boolean; customLabel?: string; @@ -37,27 +49,29 @@ function Select(props: Props) { const {Menu} = remote; - const menu = Menu.buildFromTemplate( - options.map(option => { - if (option.separator) { - return {type: 'separator'}; - } - - if (option.subMenu) { - return { - label: option.label, - submenu: option.subMenu - }; - } + const convertToMenuTemplate = (option: Option): MenuItemConstructorOptions => { + if (option.separator) { + return {type: 'separator'}; + } + if (option.subMenu) { return { label: option.label, - type: option.type as any || 'checkbox', - checked: option.checked ?? (option.value === value), - click: () => props.onChange(option.value) + submenu: option.subMenu.map(convertToMenuTemplate), + checked: option.checked }; - }) - ); + } + + return { + label: option.label, + type: option.type as any || 'checkbox', + checked: option.checked ?? (option.value === value), + click: option.click ?? (() => props.onChange(option.value)), + icon: option.icon + }; + } + + const menu = Menu.buildFromTemplate(options.map(convertToMenuTemplate)); menu.popup({ x: Math.round(boundingRect.left), diff --git a/renderer/components/editor/video.tsx b/renderer/components/editor/video.tsx index 5d6192727..d166fa71b 100644 --- a/renderer/components/editor/video.tsx +++ b/renderer/components/editor/video.tsx @@ -1,5 +1,5 @@ import {useRef, useMemo, useEffect, RefObject} from 'react'; -import useWindowArgs from '../../hooks/window-args'; +import useWindowState from '../../hooks/window-state'; import VideoTimeContainer from './video-time-container'; import VideoMetadataContainer from './video-metadata-container'; import VideoControlsContainer from './video-controls-container'; @@ -29,7 +29,7 @@ const getVideoProps = (propsArray: React.DetailedHTMLProps { const videoRef = useRef(); - const {filePath} = useWindowArgs(); + const {filePath} = useWindowState(); const src = `file://${filePath}`; const videoTimeContainer = VideoTimeContainer.useContainer(); diff --git a/renderer/containers/action-bar.js b/renderer/containers/action-bar.js index 42c4f3f41..1b50108e6 100644 --- a/renderer/containers/action-bar.js +++ b/renderer/containers/action-bar.js @@ -15,7 +15,7 @@ export default class ActionBarContainer extends Container { return; } - this.settings = this.remote.require('./common/settings'); + this.settings = this.remote.require('./common/settings').default; this.state = { cropperWidth: '', cropperHeight: '' diff --git a/renderer/containers/cropper.js b/renderer/containers/cropper.js index 131e3fad3..32ad86286 100644 --- a/renderer/containers/cropper.js +++ b/renderer/containers/cropper.js @@ -47,7 +47,7 @@ export default class CropperContainer extends Container { return; } - this.settings = this.remote.require('./common/settings'); + this.settings = this.remote.require('./common/settings').default; this.state = { isRecording: false, diff --git a/renderer/containers/preferences.js b/renderer/containers/preferences.js index a91cc9213..599225c8c 100644 --- a/renderer/containers/preferences.js +++ b/renderer/containers/preferences.js @@ -1,7 +1,9 @@ import electron from 'electron'; import {Container} from 'unstated'; import {ipcRenderer as ipc} from 'electron-better-ipc'; -import {defaultInputDeviceId} from '../../main/common/constants'; +// import {defaultInputDeviceId} from 'common/constants'; + +const defaultInputDeviceId = 'asd'; const SETTINGS_ANALYTICS_BLACKLIST = ['kapturesDir']; @@ -16,7 +18,7 @@ export default class PreferencesContainer extends Container { mount = async setOverlay => { this.setOverlay = setOverlay; - this.settings = this.remote.require('./common/settings'); + this.settings = this.remote.require('./common/settings').default; this.systemPermissions = this.remote.require('./common/system-permissions'); this.plugins = this.remote.require('./common/plugins'); this.track = this.remote.require('./common/analytics').track; diff --git a/renderer/hooks/editor/use-conversion.tsx b/renderer/hooks/editor/use-conversion.tsx new file mode 100644 index 000000000..ae9a98fed --- /dev/null +++ b/renderer/hooks/editor/use-conversion.tsx @@ -0,0 +1,36 @@ +import {CreateConversionOptions} from 'common/types'; +import {ipcRenderer} from 'electron-better-ipc'; +import {createContext, PropsWithChildren, useContext, useEffect, useState} from 'react'; + +const ConversionContext = createContext<{ + conversionId: string; + setConversionId: (id: string) => void; + startConversion: (options: CreateConversionOptions) => Promise +}>(undefined); + +export const ConversionContextProvider = (props: PropsWithChildren<{}>) => { + const [conversionId, setConversionId] = useState(); + + const startConversion = async (options: CreateConversionOptions) => { + console.log('HERE with', options); + const id = await ipcRenderer.callMain('create-conversion', options); + console.log('Got back', id); + setConversionId(id); + }; + + const value = { + conversionId, + setConversionId, + startConversion + }; + + return ( + + {props.children} + + ); +}; + +const useConversionContext = () => useContext(ConversionContext); + +export default useConversionContext; diff --git a/renderer/hooks/editor/use-editor-options.tsx b/renderer/hooks/editor/use-editor-options.tsx new file mode 100644 index 000000000..b16c638a6 --- /dev/null +++ b/renderer/hooks/editor/use-editor-options.tsx @@ -0,0 +1,17 @@ +import {EditorOptions} from 'common/remote-state-types'; +import useRemoteState from 'hooks/use-remote-state'; + +const useEditorOptions = useRemoteState('editor-options', { + formats: [], + editServices: [], + fpsHistory: { + gif: 60, + mp4: 60, + av1: 60, + webm: 60, + apng: 60 + } +}); + +export type EditorOptionsState = ReturnType["state"]; +export default useEditorOptions; diff --git a/renderer/hooks/editor/use-editor-window-state.tsx b/renderer/hooks/editor/use-editor-window-state.tsx new file mode 100644 index 000000000..41232e0e4 --- /dev/null +++ b/renderer/hooks/editor/use-editor-window-state.tsx @@ -0,0 +1,14 @@ +import useWindowState from 'hooks/window-state'; + +interface EditorWindowState { + fps: number; + filePath: string; + originalFilePath: string; + isNewRecording: boolean; + recordingName: string; + title: string; + conversionId?: string; +} + +const useEditorWindowState = () => useWindowState(); +export default useEditorWindowState; diff --git a/renderer/hooks/editor/use-share-plugins.tsx b/renderer/hooks/editor/use-share-plugins.tsx new file mode 100644 index 000000000..de2a777db --- /dev/null +++ b/renderer/hooks/editor/use-share-plugins.tsx @@ -0,0 +1,84 @@ +import {ExportOptionsFormat} from 'common/remote-state-types'; +import OptionsContainer from 'components/editor/options-container' +import {remote} from 'electron'; +import {ipcRenderer} from 'electron-better-ipc'; +import {useMemo} from 'react'; + +const useSharePlugins = () => { + const { + formats, + format, + sharePlugin, + updateSharePlugin + } = OptionsContainer.useContainer(); + + const menuOptions = useMemo(() => { + const selectedFormat = formats.find(f => f.format === format); + + let onlyBuiltIn = true; + const options = selectedFormat?.plugins?.map(plugin => { + if (plugin.apps && plugin.apps.length > 0) { + const subMenu = plugin.apps.map(app => ({ + label: app.isDefault ? `${app.name} (default)` : app.name, + type: 'radio', + checked: sharePlugin.app?.url === app.url, + value: { + pluginName: plugin.pluginName, + serviceTitle: plugin.title, + app + }, + icon: remote.nativeImage.createFromDataURL(app.icon).resize({width: 16, height: 16}) + })); + + if (plugin.apps[0].isDefault) { + subMenu.splice(1, 0, {type: 'separator'} as any); + } + + return { + isBuiltIn: true, + subMenu, + value: { + pluginName: plugin.pluginName, + serviceTitle: plugin.title, + app: plugin.apps[0] + }, + checked: sharePlugin.pluginName === plugin.pluginName, + label: 'Open with…' + } + } + + if (!plugin.pluginName.startsWith('_')) { + onlyBuiltIn = false; + } + + return { + value: { + pluginName: plugin.pluginName, + serviceTitle: plugin.title + }, + checked: sharePlugin.pluginName === plugin.pluginName, + label: plugin.title + } + }); + + if (onlyBuiltIn) { + options?.push({ + separator: true + } as any, { + label: 'Get Plugins…', + checked: false, + click: () => { + ipcRenderer.callMain('open-preferences', {category: 'plugins', tab: 'discover'}); + } + } as any) + } + + return options ?? []; + }, [formats, format, sharePlugin]); + + const label = sharePlugin?.app ? sharePlugin.app.name : sharePlugin?.serviceTitle; + + return {menuOptions, label, onChange: updateSharePlugin}; +} + +export default useSharePlugins; diff --git a/renderer/hooks/use-remote-state.tsx b/renderer/hooks/use-remote-state.tsx new file mode 100644 index 000000000..8cd3cc2bc --- /dev/null +++ b/renderer/hooks/use-remote-state.tsx @@ -0,0 +1,74 @@ +import {useState, useEffect, useRef} from 'react'; +import {ipcRenderer} from 'electron-better-ipc'; + +// TODO: Import these util exports from the `main/remote-states/utils` file once we figure out the correct TS configuration +export const getChannelName = (name: string, action: string) => `kap-remote-state-${name}-${action}`; + +export const getChannelNames = (name: string) => ({ + subscribe: getChannelName(name, 'subscribe'), + getState: getChannelName(name, 'get-state'), + callAction: getChannelName(name, 'call-action'), + stateUpdated: getChannelName(name, 'state-updated') +}); + +export type RemoteState = (sendUpdate: (state?: State, id?: string) => void) => { + getState: (id?: string) => State, + actions: Actions +} + +const useRemoteState = >( + name: string, + initialState?: Callback extends RemoteState ? State : never +): (id?: string) => ( + Callback extends RemoteState ? ( + Actions & { + state: State; + isLoading: false; + refreshState: () => void; + } + ) : never +) => { + const channelNames = getChannelNames(name); + + return (id?: string) => { + const [state, setState] = useState(initialState); + const [isLoading, setIsLoading] = useState(true); + const actionsRef = useRef({}); + + useEffect(() => { + const cleanup = ipcRenderer.answerMain(channelNames.stateUpdated, setState); + + (async () => { + const actionKeys = (await ipcRenderer.callMain(channelNames.subscribe, id)) as string[]; + + const actions = actionKeys.reduce((acc, key) => ({ + ...acc, + [key]: (data: any) => ipcRenderer.callMain(channelNames.callAction, {key, data, id}) + }), {}); + + const getState = async () => { + const newState = (await ipcRenderer.callMain(channelNames.getState, id)) as typeof state; + setState(newState); + } + + actionsRef.current = { + ...actions, + refreshState: getState + }; + + await getState(); + setIsLoading(false); + })(); + + return cleanup; + }, []); + + return { + ...actionsRef.current, + isLoading, + state + }; + } +} + +export default useRemoteState; diff --git a/renderer/hooks/window-args.tsx b/renderer/hooks/window-args.tsx deleted file mode 100644 index 7bf9f0e4f..000000000 --- a/renderer/hooks/window-args.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import {createContext, useContext, useState, useEffect, ReactNode} from 'react'; -import {ipcRenderer as ipc} from 'electron-better-ipc'; - -const ArgsContext = createContext(undefined); - -export const WindowArgsProvider = (props: {children: ReactNode}) => { - const [args, setArgs] = useState(); - - useEffect(() => { - return ipc.answerMain('kap-window-args', (newArgs: any) => { - setArgs(newArgs); - }); - }, []); - - return ( - - {props.children} - - ); -}; - -const useWindowArgs = () => useContext(ArgsContext); - -export default useWindowArgs; diff --git a/renderer/hooks/window-state.tsx b/renderer/hooks/window-state.tsx new file mode 100644 index 000000000..b423aa46f --- /dev/null +++ b/renderer/hooks/window-state.tsx @@ -0,0 +1,25 @@ +import {createContext, useContext, useState, useEffect, ReactNode} from 'react'; +import {ipcRenderer as ipc} from 'electron-better-ipc'; + +const WindowStateContext = createContext(undefined); + +export const WindowStateProvider = (props: {children: ReactNode}) => { + const [windowState, setWindowState] = useState(); + + useEffect(() => { + return ipc.answerMain('kap-window-state', (newState: any) => { + setWindowState(newState); + console.log('GOt new state', newState); + }); + }, []); + + return ( + + {props.children} + + ); +}; + +const useWindowState = () => useContext(WindowStateContext); + +export default useWindowState; diff --git a/renderer/next.config.js b/renderer/next.config.js index 94cbe839a..c932ba31f 100644 --- a/renderer/next.config.js +++ b/renderer/next.config.js @@ -1,14 +1,79 @@ -exports.webpack = config => Object.assign(config, { - target: 'electron-renderer', - devtool: 'cheap-module-source-map', - plugins: config.plugins.filter(p => p.constructor.name !== 'UglifyJsPlugin') -}); - -// exports.exportPathMap = () => ({ -// '/cropper': {page: '/cropper'}, -// '/editor': {page: '/editor'}, -// '/preferences': {page: '/preferences'}, -// '/exports': {page: '/exports'}, -// '/config': {page: '/config'}, -// '/dialog': {page: '/dialog'} -// }); +const path = require('path'); +module.exports = (nextConfig) => { + return Object.assign({}, nextConfig, { + webpack(config, options) { + config.module.rules.push({ + test: /\.+(js|jsx|mjs|ts|tsx)$/, + loader: options.defaultLoaders.babel, + include: [ + path.join(__dirname, '..', 'main', 'common', 'constants.ts'), + path.join(__dirname, '..', 'main', 'remote-states', 'use-remote-state.ts') + ] + }); + + config.target = 'electron-renderer'; + config.devtool = 'cheap-module-source-map'; + + if (typeof nextConfig.webpack === 'function') { + return nextConfig.webpack(config, options); + } + + return config; + } + }) +} + +// exports.webpack = (nextConfig, options) => { +// console.log(nextConfig); +// // Fix for allowing TS files from a parent location to be transpiled +// // https://github.com/vercel/next.js/issues/5666 +// // config.module.rules.forEach((rule) => { +// // console.log('\n\n\nPLZ\n', rule); +// // const ruleContainsTs = rule.test && rule.test.toString().includes('tsx|ts'); + +// // if (ruleContainsTs && rule.use && rule.use.loader === 'next-babel-loader') { +// // rule.include = undefined; +// // } +// // }); + +// // config.module.rules.push({ +// // test: /\.+(js|jsx|mjs|ts|tsx)$/, +// // use: options.defaultLoaders.babel, +// // include: includes +// // }); + +// x = Object.assign({}, nextConfig, { +// target: 'electron-renderer', +// devtool: 'cheap-module-source-map', +// plugins: nextConfig.plugins.filter(p => p.constructor.name !== 'UglifyJsPlugin'), +// webpack(config, options) { +// config.module.rules.push({ +// test: /\.+(js|jsx|mjs|ts|tsx)$/, +// loader: options.defaultLoaders.babel, +// include: [ +// path.join(__dirname, '..', 'main', 'common', 'constants.ts'), +// path.join(__dirname, '..', 'main', 'remote-states', 'use-remote-state.ts') +// ] +// }); + +// if (typeof nextConfig.webpack === 'function') { +// return nextConfig.webpack(config, options); +// } + +// return config; +// } +// }); + +// console.log(x); +// console.log(JSON.stringify(x.module, null, 2)) +// return x; +// }; + +// // exports.exportPathMap = () => ({ +// // '/cropper': {page: '/cropper'}, +// // '/editor': {page: '/editor'}, +// // '/preferences': {page: '/preferences'}, +// // '/exports': {page: '/exports'}, +// // '/config': {page: '/config'}, +// // '/dialog': {page: '/dialog'} +// // }); diff --git a/renderer/pages/_app.tsx b/renderer/pages/_app.tsx index 0ceaec8d2..bccdc6839 100644 --- a/renderer/pages/_app.tsx +++ b/renderer/pages/_app.tsx @@ -3,8 +3,9 @@ import {useState, useEffect} from 'react'; import useDarkMode from '../hooks/dark-mode'; import GlobalStyles from '../utils/global-styles'; import SentryErrorBoundary from '../utils/sentry-error-boundary'; -import {WindowArgsProvider} from '../hooks/window-args'; +import {WindowStateProvider} from '../hooks/window-state'; import classNames from 'classnames'; +import {ipcRenderer} from 'electron-better-ipc'; function Kap(props: AppProps) { const [isMounted, setIsMounted] = useState(false); @@ -13,6 +14,13 @@ function Kap(props: AppProps) { setIsMounted(true); }, []); + useEffect(() => { + if (isMounted) { + console.log('SENDING'); + ipcRenderer.callMain('kap-window-mount'); + } + }, [isMounted]) + if (!isMounted) { return null; } @@ -27,10 +35,10 @@ const MainApp = ({Component, pageProps}: AppProps) => { return (
- + - +
); diff --git a/renderer/pages/editor2.tsx b/renderer/pages/editor2.tsx index e0f51df94..8b5bb9d8f 100644 --- a/renderer/pages/editor2.tsx +++ b/renderer/pages/editor2.tsx @@ -1,4 +1,3 @@ -import useWindowArgs from '../hooks/window-args'; import Head from 'next/head'; import EditorPreview from '../components/editor/editor-preview'; import combineUnstatedContainers from '../utils/combine-unstated-containers'; @@ -6,6 +5,8 @@ import VideoMetadataContainer from '../components/editor/video-metadata-containe import VideoTimeContainer from '../components/editor/video-time-container'; import VideoControlsContainer from '../components/editor/video-controls-container'; import OptionsContainer from '../components/editor/options-container'; +import useEditorWindowState from 'hooks/editor/use-editor-window-state'; +import {ConversionContextProvider} from 'hooks/editor/use-conversion'; const ContainerProvider = combineUnstatedContainers([ OptionsContainer, @@ -14,10 +15,8 @@ const ContainerProvider = combineUnstatedContainers([ VideoControlsContainer ]); -const Editor = () => { - const args = useWindowArgs(); - - console.log('HERE', args); +const EditorPage = () => { + const args = useEditorWindowState(); if (!args) { return null; @@ -28,9 +27,11 @@ const Editor = () => { - - - + + + + + +
+ ); +}; + +export default ConversionDetails; diff --git a/renderer/components/editor/conversion/index.tsx b/renderer/components/editor/conversion/index.tsx index 66779bbc2..a1a41b297 100644 --- a/renderer/components/editor/conversion/index.tsx +++ b/renderer/components/editor/conversion/index.tsx @@ -1,5 +1,49 @@ -const EditorConversionView = () => { - return
Hello there!
+import {ConversionStatus} from 'common/types'; +import useConversion from 'hooks/editor/use-conversion'; +import useConversionIdContext from 'hooks/editor/use-conversion-id'; +import {useConfirmation} from 'hooks/use-confirmation'; +import ConversionDetails from './conversion-details'; +import TitleBar from './title-bar' +import VideoPreview from './video-preview'; + +const dialogOptions = { + message: 'Are you sure you want to discard this conversion?', + detail: 'Any progress will be lost.', + confirmButtonText: 'Discard' +}; + +const EditorConversionView = ({conversionId}: {conversionId: string}) => { + const {setConversionId} = useConversionIdContext(); + const conversion = useConversion(conversionId); + + const cancel = () => conversion.cancel(); + const safeCancel = useConfirmation(cancel, dialogOptions); + + const cancelAndGoBack = () => { + cancel(); + setConversionId(''); + }; + const safeCancelAndGoBack = useConfirmation(cancelAndGoBack, dialogOptions); + + const inProgress = conversion.state?.status === ConversionStatus.inProgress; + + const finalCancel = inProgress ? safeCancel : cancel; + const finalCancelAndGoBack = inProgress ? safeCancelAndGoBack : cancelAndGoBack; + + return ( +
+ conversion.copy()}/> + + + +
+ ) }; export default EditorConversionView; diff --git a/renderer/components/editor/conversion/title-bar.tsx b/renderer/components/editor/conversion/title-bar.tsx new file mode 100644 index 000000000..f8eb234ea --- /dev/null +++ b/renderer/components/editor/conversion/title-bar.tsx @@ -0,0 +1,71 @@ +import TrafficLights from 'components/traffic-lights' +import {BackPlainIcon} from 'vectors' +import {UseConversionState} from 'hooks/editor/use-conversion'; + +const TitleBar = ({conversion, cancel, copy}: {conversion: UseConversionState, cancel: () => any, copy: () => any}) => { + + return ( +
+
+ +
+ +
+
+
+ {conversion?.canCopy &&
Copy
} +
+ +
+ ) +} + +export default TitleBar; diff --git a/renderer/components/editor/conversion/video-preview.tsx b/renderer/components/editor/conversion/video-preview.tsx new file mode 100644 index 000000000..cf50cfca6 --- /dev/null +++ b/renderer/components/editor/conversion/video-preview.tsx @@ -0,0 +1,135 @@ +import {CancelIcon, SpinnerIcon} from 'vectors'; +import useWindowState from 'hooks/window-state'; +import {UseConversion, UseConversionState} from 'hooks/editor/use-conversion'; +import {ConversionStatus} from 'common/types'; + +const VideoPreview = ({conversion, cancel}: {conversion: UseConversionState, cancel: () => any}) => { + const {filePath} = useWindowState(); + const src = `file://${filePath}`; + + const percentage = conversion?.progress ?? 0; + const done = conversion?.status !== ConversionStatus.inProgress; + + return ( +
+
+ ); +} + +const IndeterminateSpinner = () => ( +
+ + +
+); + +const ProgressCircle = ({percent}: {percent: number}) => { + const circumference = 12 * 2 * Math.PI; + const offset = circumference * (1 - percent); + + return ( + + + + + ); +}; + +export default VideoPreview; diff --git a/renderer/components/editor/editor-preview.tsx b/renderer/components/editor/editor-preview.tsx index 7ec6c1386..264da247a 100644 --- a/renderer/components/editor/editor-preview.tsx +++ b/renderer/components/editor/editor-preview.tsx @@ -15,7 +15,7 @@ const EditorPreview = () => {
{title}
- + + + ); } export default Editor; diff --git a/renderer/components/editor/options-container.tsx b/renderer/components/editor/options-container.tsx index bc3f34b71..8f6d8e6c4 100644 --- a/renderer/components/editor/options-container.tsx +++ b/renderer/components/editor/options-container.tsx @@ -5,8 +5,7 @@ import useWindowState from 'hooks/window-state'; import VideoMetadataContainer from './video-metadata-container'; import VideoControlsContainer from './video-controls-container'; import useEditorOptions, {EditorOptionsState} from 'hooks/editor/use-editor-options'; -import {Format} from 'common/types'; -import {App} from 'common/remote-state-types'; +import {Format, App} from 'common/types'; type EditService = EditorOptionsState["editServices"][0] @@ -29,7 +28,6 @@ const useOptions = () => { updateFpsUsage, isLoading } = useEditorOptions(); - console.log(formats, fpsHistory); const metadata = VideoMetadataContainer.useContainer(); const {isMuted, mute, unmute} = VideoControlsContainer.useContainer(); @@ -119,7 +117,7 @@ const useOptions = () => { setHeight(dimensions.height); } - const res = { + return { width, height, format, @@ -135,9 +133,6 @@ const useOptions = () => { setEditPlugin, setDimensions }; - - console.log(res); - return res; } const OptionsContainer = createContainer(useOptions); diff --git a/renderer/components/editor/options/right.tsx b/renderer/components/editor/options/right.tsx index 1a5f0cba7..3509b2ddb 100644 --- a/renderer/components/editor/options/right.tsx +++ b/renderer/components/editor/options/right.tsx @@ -2,12 +2,12 @@ import {GearIcon} from '../../../vectors'; import OptionsContainer from '../options-container'; import Select from './select'; import {ipcRenderer as ipc} from 'electron-better-ipc'; -import useConversionContext from 'hooks/editor/use-conversion'; -import Options from '.'; +import useConversionIdContext from 'hooks/editor/use-conversion-id'; import useEditorWindowState from 'hooks/editor/use-editor-window-state'; import VideoTimeContainer from '../video-time-container'; import VideoControlsContainer from '../video-controls-container'; import useSharePlugins from 'hooks/editor/use-share-plugins'; +import useEditorOptions from 'hooks/editor/use-editor-options'; const FormatSelect = () => { const {formats, format, updateFormat} = OptionsContainer.useContainer(); @@ -118,11 +118,12 @@ const EditPluginsControl = () => { } const ConvertButton = () => { - const {startConversion} = useConversionContext(); + const {startConversion} = useConversionIdContext(); const options = OptionsContainer.useContainer(); const {filePath} = useEditorWindowState(); const {startTime, endTime} = VideoTimeContainer.useContainer(); const {isMuted} = VideoControlsContainer.useContainer(); + const {updatePluginUsage} = useEditorOptions(); const onClick = () => { const shouldCrop = true; @@ -136,13 +137,22 @@ const ConvertButton = () => { endTime, fps: options.fps, shouldMute: isMuted, - shouldCrop + shouldCrop, + editService: options.editPlugin ? { + pluginName: options.editPlugin.pluginName, + serviceTitle: options.editPlugin.title + } : undefined }, format: options.format, plugins: { - share: options.sharePlugin + share: options.sharePlugin, } }); + + updatePluginUsage({ + format: options.format, + plugin: options.sharePlugin.pluginName + }); } return ( diff --git a/renderer/components/editor/video-controls-container.tsx b/renderer/components/editor/video-controls-container.tsx index d21d612f6..52d0218fa 100644 --- a/renderer/components/editor/video-controls-container.tsx +++ b/renderer/components/editor/video-controls-container.tsx @@ -46,6 +46,11 @@ const useVideoControls = () => { const setVideoRef = (video: HTMLVideoElement) => { videoRef.current = video; + setIsPaused(video.paused); + + if (video.paused) { + play(); + } }; const videoProps = { diff --git a/renderer/components/editor/video-time-container.tsx b/renderer/components/editor/video-time-container.tsx index b88592109..d3f59811f 100644 --- a/renderer/components/editor/video-time-container.tsx +++ b/renderer/components/editor/video-time-container.tsx @@ -52,8 +52,12 @@ const useVideoTime = () => { }; useEffect(() => { + if (!videoRef.current) { + return; + } + const interval = setInterval(() => { - updateTime(videoRef.current.currentTime, true); + updateTime(videoRef.current.currentTime ?? 0, true); }, 1000 / 30); return () => { diff --git a/renderer/components/editor/video.tsx b/renderer/components/editor/video.tsx index d166fa71b..df384c819 100644 --- a/renderer/components/editor/video.tsx +++ b/renderer/components/editor/video.tsx @@ -1,4 +1,4 @@ -import {useRef, useMemo, useEffect, RefObject} from 'react'; +import {useRef, useEffect} from 'react'; import useWindowState from '../../hooks/window-state'; import VideoTimeContainer from './video-time-container'; import VideoMetadataContainer from './video-metadata-container'; @@ -57,100 +57,9 @@ const Video = () => { height: 100%; max-height: calc(100vh - 48px); } - - .container { - flex: 1; - } `} ); } export default Video; - -// import electron from 'electron'; -// import PropTypes from 'prop-types'; -// import React, {useRef} from 'react'; - -// import {connect, VideoContainer, EditorContainer} from '../../containers'; - -// class Video extends React.Component { -// constructor(props) { -// super(props); -// this.videoRef = React.createRef(); -// } - -// componentDidMount() { -// const {remote} = electron; -// const {Menu, MenuItem} = remote; -// const {getSnapshot} = this.props; - -// this.menu = new Menu(); -// this.menu.append(new MenuItem({label: 'Snapshot', click: getSnapshot})); -// } - -// componentDidUpdate(previousProps) { -// const {setVideo, src} = this.props; - -// if (!previousProps.src && src) { -// setVideo(this.videoRef.current); -// } -// } - -// contextMenu = () => { -// const {play, pause} = this.props; -// const video = this.videoRef.current; -// const wasPaused = video.paused; - -// if (!wasPaused) { -// pause(); -// } - -// this.menu.popup({ -// callback: () => { -// if (!wasPaused) { -// play(); -// } -// } -// }); -// } - -// render() { -// const {src} = this.props; - -// if (!src) { -// return null; -// } - -// return ( -//
-//
-// ); -// } -// } - -// Video.propTypes = { -// src: PropTypes.string, -// setVideo: PropTypes.elementType, -// getSnapshot: PropTypes.elementType, -// play: PropTypes.elementType, -// pause: PropTypes.elementType -// }; - -// export default connect( -// [VideoContainer, EditorContainer], -// ({src}) => ({src}), -// ({setVideo, play, pause}, {getSnapshot}) => ({setVideo, getSnapshot, play, pause}) -// )(Video); diff --git a/renderer/components/icon-menu.js b/renderer/components/icon-menu.js deleted file mode 100644 index 2bc5c1d53..000000000 --- a/renderer/components/icon-menu.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -class IconMenu extends React.Component { - container = React.createRef(); - - openMenu = () => { - const boundingRect = this.container.current.children[0].getBoundingClientRect(); - const {bottom, left} = boundingRect; - const {onOpen} = this.props; - - if (onOpen) { - onOpen({ - x: Math.round(left), - y: Math.round(bottom) - }); - } - } - - render() { - const {icon: Icon, ...iconProps} = this.props; - return ( -
- - -
- ); - } -} - -IconMenu.propTypes = { - onOpen: PropTypes.elementType, - icon: PropTypes.elementType.isRequired, - fillParent: PropTypes.bool -}; - -export default IconMenu; diff --git a/renderer/components/icon-menu.tsx b/renderer/components/icon-menu.tsx new file mode 100644 index 000000000..02252501c --- /dev/null +++ b/renderer/components/icon-menu.tsx @@ -0,0 +1,37 @@ +import React, {FunctionComponent, useRef} from 'react'; +import {SvgProps} from 'vectors/svg'; + +interface IconMenuProps extends SvgProps { + onOpen: (options: {x: number, y: number}) => void, + icon: FunctionComponent, + fillParent?: boolean +} + +const IconMenu: FunctionComponent = ({onOpen, icon: Icon, fillParent, ...iconProps}) => { + const container = useRef(null); + + const openMenu = () => { + const boundingRect = container.current.children[0].getBoundingClientRect(); + const {bottom, left} = boundingRect; + + onOpen({ + x: Math.round(left), + y: Math.round(bottom) + }); + } + + return ( +
+ + +
+ ) +} + +export default IconMenu; diff --git a/renderer/containers/cropper.js b/renderer/containers/cropper.js index 32ad86286..69169444c 100644 --- a/renderer/containers/cropper.js +++ b/renderer/containers/cropper.js @@ -47,7 +47,10 @@ export default class CropperContainer extends Container { return; } - this.settings = this.remote.require('./common/settings').default; + const {default: settings, getSelectedInputDeviceId} = this.remote.require('./common/settings'); + this.settings = settings; + console.log(getSelectedInputDeviceId); + this.settings.getSelectedInputDeviceId = getSelectedInputDeviceId; this.state = { isRecording: false, diff --git a/renderer/containers/preferences.js b/renderer/containers/preferences.js index 599225c8c..e6597b4c5 100644 --- a/renderer/containers/preferences.js +++ b/renderer/containers/preferences.js @@ -18,10 +18,13 @@ export default class PreferencesContainer extends Container { mount = async setOverlay => { this.setOverlay = setOverlay; - this.settings = this.remote.require('./common/settings').default; + const {default: settings, shortcuts} = this.remote.require('./common/settings'); + this.settings = settings; + this.settings.shortcuts = shortcuts; this.systemPermissions = this.remote.require('./common/system-permissions'); this.plugins = this.remote.require('./common/plugins'); this.track = this.remote.require('./common/analytics').track; + console.log(this.track); this.showError = this.remote.require('./utils/errors').showError; const pluginsInstalled = this.plugins.getInstalled().sort((a, b) => a.prettyName.localeCompare(b.prettyName)); diff --git a/renderer/hooks/editor/use-conversion-id.tsx b/renderer/hooks/editor/use-conversion-id.tsx new file mode 100644 index 000000000..9dc27caf5 --- /dev/null +++ b/renderer/hooks/editor/use-conversion-id.tsx @@ -0,0 +1,41 @@ +import {CreateConversionOptions} from 'common/types'; +import {ipcRenderer} from 'electron-better-ipc'; +import {createContext, PropsWithChildren, useContext, useState} from 'react'; + +const ConversionIdContext = createContext<{ + conversionId: string; + setConversionId: (id: string) => void; + startConversion: (options: CreateConversionOptions) => Promise +}>(undefined); + +let savedConversionId: string; + +export const ConversionIdContextProvider = (props: PropsWithChildren<{}>) => { + const [conversionId, setConversionId] = useState(); + + const startConversion = async (options: CreateConversionOptions) => { + const id = await ipcRenderer.callMain('create-conversion', options); + setConversionId(id); + }; + + const setConvId = (id: string) => { + savedConversionId = savedConversionId || id; + setConversionId(id || savedConversionId); + } + + const value = { + conversionId, + setConversionId: setConvId, + startConversion + }; + + return ( + + {props.children} + + ); +}; + +const useConversionIdContext = () => useContext(ConversionIdContext); + +export default useConversionIdContext; diff --git a/renderer/hooks/editor/use-conversion.tsx b/renderer/hooks/editor/use-conversion.tsx index ae9a98fed..c4e26429d 100644 --- a/renderer/hooks/editor/use-conversion.tsx +++ b/renderer/hooks/editor/use-conversion.tsx @@ -1,36 +1,8 @@ -import {CreateConversionOptions} from 'common/types'; -import {ipcRenderer} from 'electron-better-ipc'; -import {createContext, PropsWithChildren, useContext, useEffect, useState} from 'react'; +import {ConversionRemoteState} from 'common/types'; +import useRemoteState from 'hooks/use-remote-state'; -const ConversionContext = createContext<{ - conversionId: string; - setConversionId: (id: string) => void; - startConversion: (options: CreateConversionOptions) => Promise -}>(undefined); +const useConversion = useRemoteState('conversion'); -export const ConversionContextProvider = (props: PropsWithChildren<{}>) => { - const [conversionId, setConversionId] = useState(); - - const startConversion = async (options: CreateConversionOptions) => { - console.log('HERE with', options); - const id = await ipcRenderer.callMain('create-conversion', options); - console.log('Got back', id); - setConversionId(id); - }; - - const value = { - conversionId, - setConversionId, - startConversion - }; - - return ( - - {props.children} - - ); -}; - -const useConversionContext = () => useContext(ConversionContext); - -export default useConversionContext; +export type UseConversion = ReturnType; +export type UseConversionState = UseConversion["state"]; +export default useConversion; diff --git a/renderer/hooks/editor/use-editor-options.tsx b/renderer/hooks/editor/use-editor-options.tsx index b16c638a6..6fc61fd8c 100644 --- a/renderer/hooks/editor/use-editor-options.tsx +++ b/renderer/hooks/editor/use-editor-options.tsx @@ -1,7 +1,7 @@ -import {EditorOptions} from 'common/remote-state-types'; +import {EditorOptionsRemoteState} from 'common/types'; import useRemoteState from 'hooks/use-remote-state'; -const useEditorOptions = useRemoteState('editor-options', { +const useEditorOptions = useRemoteState('editor-options', { formats: [], editServices: [], fpsHistory: { diff --git a/renderer/hooks/editor/use-share-plugins.tsx b/renderer/hooks/editor/use-share-plugins.tsx index de2a777db..3231b062e 100644 --- a/renderer/hooks/editor/use-share-plugins.tsx +++ b/renderer/hooks/editor/use-share-plugins.tsx @@ -1,4 +1,3 @@ -import {ExportOptionsFormat} from 'common/remote-state-types'; import OptionsContainer from 'components/editor/options-container' import {remote} from 'electron'; import {ipcRenderer} from 'electron-better-ipc'; diff --git a/renderer/hooks/editor/use-window-size.tsx b/renderer/hooks/editor/use-window-size.tsx new file mode 100644 index 000000000..ecb24a6ed --- /dev/null +++ b/renderer/hooks/editor/use-window-size.tsx @@ -0,0 +1,37 @@ +import {remote} from 'electron'; +import {useEffect, useRef} from 'react'; +import {resizeKeepingCenter} from 'utils/window'; + +const CONVERSION_WIDTH = 360; +const CONVERSION_HEIGHT = 392; +const DEFAULT_EDITOR_WIDTH = 768; +const DEFAULT_EDITOR_HEIGHT = 480; + +export const useEditorWindowSizeEffect = (isConversionWindowState: boolean) => { + const previousWindowSizeRef = useRef<{width: number, height: number}>(); + + useEffect(() => { + console.log('In with', isConversionWindowState, previousWindowSizeRef.current); + if (!previousWindowSizeRef.current) { + previousWindowSizeRef.current = { + width: DEFAULT_EDITOR_WIDTH, + height: DEFAULT_EDITOR_HEIGHT + }; + return; + } + + const window = remote.getCurrentWindow(); + const bounds = window.getBounds(); + + if (isConversionWindowState) { + previousWindowSizeRef.current = { + width: bounds.width, + height: bounds.height + }; + + window.setBounds(resizeKeepingCenter(bounds, {width: CONVERSION_WIDTH, height: CONVERSION_HEIGHT})); + } else { + window.setBounds(resizeKeepingCenter(bounds, previousWindowSizeRef.current)); + } + }, [isConversionWindowState]); +} diff --git a/renderer/hooks/use-confirmation.tsx b/renderer/hooks/use-confirmation.tsx new file mode 100644 index 000000000..912501019 --- /dev/null +++ b/renderer/hooks/use-confirmation.tsx @@ -0,0 +1,33 @@ +import {useCallback} from 'react' + +interface UseConfirmationOptions { + message: string, + detail?: string, + confirmButtonText: string, + cancelButtonText?: string +} + +export const useConfirmation = ( + callback: () => void, + options: UseConfirmationOptions +) => { + return useCallback(() => { + const {dialog, remote} = require('electron-util').api; + + const buttonIndex = dialog.showMessageBoxSync(remote.getCurrentWindow(), { + type: 'question', + buttons: [ + options.confirmButtonText, + options.cancelButtonText ?? 'Cancel' + ], + defaultId: 0, + cancelId: 1, + message: options.message, + detail: options.detail + }); + + if (buttonIndex === 0) { + callback(); + } + }, [callback]); +} diff --git a/renderer/hooks/use-current-window.tsx b/renderer/hooks/use-current-window.tsx new file mode 100644 index 000000000..1b4f4d1b4 --- /dev/null +++ b/renderer/hooks/use-current-window.tsx @@ -0,0 +1,5 @@ +import {remote} from 'electron' + +export const useCurrentWindow = () => { + return remote.getCurrentWindow(); +} diff --git a/renderer/hooks/window-state.tsx b/renderer/hooks/window-state.tsx index b423aa46f..c7dd39fd7 100644 --- a/renderer/hooks/window-state.tsx +++ b/renderer/hooks/window-state.tsx @@ -9,7 +9,6 @@ export const WindowStateProvider = (props: {children: ReactNode}) => { useEffect(() => { return ipc.answerMain('kap-window-state', (newState: any) => { setWindowState(newState); - console.log('GOt new state', newState); }); }, []); diff --git a/renderer/next.config.js b/renderer/next.config.js index c932ba31f..a221740e8 100644 --- a/renderer/next.config.js +++ b/renderer/next.config.js @@ -1,4 +1,5 @@ const path = require('path'); + module.exports = (nextConfig) => { return Object.assign({}, nextConfig, { webpack(config, options) { @@ -6,7 +7,7 @@ module.exports = (nextConfig) => { test: /\.+(js|jsx|mjs|ts|tsx)$/, loader: options.defaultLoaders.babel, include: [ - path.join(__dirname, '..', 'main', 'common', 'constants.ts'), + path.join(__dirname, '..', 'main', 'common'), path.join(__dirname, '..', 'main', 'remote-states', 'use-remote-state.ts') ] }); @@ -23,6 +24,25 @@ module.exports = (nextConfig) => { }) } +// module.exports = { +// webpack: function (config, {defaultLoaders}) { +// console.log('CONTEXT IS ', config.context); +// const resolvedBaseUrl = path.resolve(config.context, '..') +// config.module.rules = [ +// ...config.module.rules, +// { +// test: /\.(tsx|ts|js|mjs|jsx)$/, +// include: [resolvedBaseUrl], +// use: defaultLoaders.babel, +// exclude: (excludePath) => { +// return /node_modules/.test(excludePath) +// }, +// }, +// ] +// return config +// } +// } + // exports.webpack = (nextConfig, options) => { // console.log(nextConfig); // // Fix for allowing TS files from a parent location to be transpiled diff --git a/renderer/pages/editor2.tsx b/renderer/pages/editor2.tsx index 8b5bb9d8f..4fc48ad66 100644 --- a/renderer/pages/editor2.tsx +++ b/renderer/pages/editor2.tsx @@ -1,12 +1,13 @@ import Head from 'next/head'; -import EditorPreview from '../components/editor/editor-preview'; +// import EditorPreview from '../components/editor/editor-preview'; import combineUnstatedContainers from '../utils/combine-unstated-containers'; import VideoMetadataContainer from '../components/editor/video-metadata-container'; import VideoTimeContainer from '../components/editor/video-time-container'; import VideoControlsContainer from '../components/editor/video-controls-container'; import OptionsContainer from '../components/editor/options-container'; import useEditorWindowState from 'hooks/editor/use-editor-window-state'; -import {ConversionContextProvider} from 'hooks/editor/use-conversion'; +import {ConversionIdContextProvider} from 'hooks/editor/use-conversion-id'; +import Editor from 'components/editor'; const ContainerProvider = combineUnstatedContainers([ OptionsContainer, @@ -27,11 +28,11 @@ const EditorPage = () => { - + - + - + diff --git a/renderer/pages/preferences.js b/renderer/pages/preferences.js index a0365571f..955193c93 100644 --- a/renderer/pages/preferences.js +++ b/renderer/pages/preferences.js @@ -51,7 +51,6 @@ export default class PreferencesPage extends React.Component { .cover-window { background-color: var(--window-background-color); - z-index: -2; display: flex; flex-direction: column; font-size: 1.4rem; diff --git a/renderer/tsconfig.json b/renderer/tsconfig.json index c315ff78a..99485562f 100644 --- a/renderer/tsconfig.json +++ b/renderer/tsconfig.json @@ -11,6 +11,7 @@ "strict": false, "forceConsistentCasingInFileNames": true, "noEmit": true, + "preserveSymlinks": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", @@ -24,11 +25,13 @@ "components/*": ["./components/*"], "containers/*": ["./containers/*"], "hooks/*": ["./hooks/*"], - "common/*": ["./common/*"] + "common/*": ["./common/*"], + "vectors": ["./vectors"] } }, "exclude": [ - "node_modules" + "node_modules", + "common-remote-states" ], "include": [ "next-env.d.ts", diff --git a/renderer/utils/window.ts b/renderer/utils/window.ts new file mode 100644 index 000000000..e7ab84ae6 --- /dev/null +++ b/renderer/utils/window.ts @@ -0,0 +1,15 @@ + +export const resizeKeepingCenter = ( + bounds: Electron.Rectangle, + newSize: {width: number, height: number} +): Electron.Rectangle => { + const cx = Math.round(bounds.x + (bounds.width / 2)); + const cy = Math.round(bounds.y + (bounds.height / 2)); + + return { + x: Math.round(cx - (newSize.width / 2)), + y: Math.round(cy - (newSize.height / 2)), + width: newSize.width, + height: newSize.height + }; +}; diff --git a/renderer/vectors/back-plain.tsx b/renderer/vectors/back-plain.tsx new file mode 100644 index 000000000..a3afb8e10 --- /dev/null +++ b/renderer/vectors/back-plain.tsx @@ -0,0 +1,11 @@ +import React, {FunctionComponent} from 'react'; +import Svg, {SvgProps} from './svg'; + +const BackPlainIcon: FunctionComponent = props => ( + + + + +); + +export default BackPlainIcon; diff --git a/renderer/vectors/index.js b/renderer/vectors/index.js index 3fbeaac8d..b91394867 100644 --- a/renderer/vectors/index.js +++ b/renderer/vectors/index.js @@ -23,6 +23,7 @@ import ErrorIcon from './error'; import OpenConfigIcon from './open-config'; import OpenOnGithubIcon from './open-on-github'; import HelpIcon from './help'; +import BackPlainIcon from './back-plain'; export { ApplicationsIcon, @@ -49,5 +50,6 @@ export { ErrorIcon, OpenConfigIcon, OpenOnGithubIcon, - HelpIcon + HelpIcon, + BackPlainIcon }; diff --git a/renderer/vectors/svg.js b/renderer/vectors/svg.js deleted file mode 100644 index 619bcd1b8..000000000 --- a/renderer/vectors/svg.js +++ /dev/null @@ -1,117 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; -import PropTypes from 'prop-types'; - -import {handleKeyboardActivation} from '../utils/inputs'; - -class Svg extends React.Component { - static defaultProps = { - fill: 'var(--icon-color)', - activeFill: 'var(--kap)', - hoverFill: 'var(--icon-hover-color)', - size: '24px', - active: false, - viewBox: '0 0 24 24', - tabIndex: -1 - } - - onClick = () => { - const {onClick} = this.props; - if (onClick) { - onClick(); - } - } - - stopPropagation = event => { - event.stopPropagation(); - } - - render() { - const { - fill, - size, - activeFill, - hoverFill, - active, - onClick, - children, - viewBox, - shadow, - tabIndex, - isMenu - } = this.props; - - const className = classNames({active, shadow, focusable: tabIndex >= 0}); - - return ( -
= 0 ? handleKeyboardActivation(onClick, {isMenu}) : undefined}> - - { children } - - -
- ); - } -} - -Svg.propTypes = { - fill: PropTypes.string, - size: PropTypes.string, - activeFill: PropTypes.string, - hoverFill: PropTypes.string, - active: PropTypes.bool, - children: PropTypes.any, - viewBox: PropTypes.string, - onClick: PropTypes.elementType, - shadow: PropTypes.bool, - tabIndex: PropTypes.number, - isMenu: PropTypes.bool -}; - -export default Svg; diff --git a/renderer/vectors/svg.tsx b/renderer/vectors/svg.tsx new file mode 100644 index 000000000..b43f58c02 --- /dev/null +++ b/renderer/vectors/svg.tsx @@ -0,0 +1,109 @@ +import React, {FunctionComponent} from 'react'; +import classNames from 'classnames'; + +import {handleKeyboardActivation} from '../utils/inputs'; + +const defaultProps: SvgProps = { + fill: 'var(--icon-color)', + activeFill: 'var(--kap)', + hoverFill: 'var(--icon-hover-color)', + size: '24px', + active: false, + viewBox: '0 0 24 24', + tabIndex: -1 +} + +const stopPropagation = event => { + event.stopPropagation(); +}; + +const Svg: FunctionComponent = props => { + const { + fill, + size, + activeFill, + hoverFill, + active, + onClick, + children, + viewBox, + shadow, + tabIndex, + isMenu + } = { + ...defaultProps, + ...props + }; + + const className = classNames({active, shadow, focusable: tabIndex >= 0}); + + return ( +
= 0 ? handleKeyboardActivation(onClick, {isMenu}) : undefined}> + + {children} + + +
+ ) +} + +export interface SvgProps { + fill?: string; + size?: string; + activeFill?: string; + hoverFill?: string; + active?: boolean; + viewBox?: string; + onClick?: () => void; + shadow?: boolean; + tabIndex?: number; + isMenu?: boolean; +} + +export default Svg; diff --git a/yarn.lock b/yarn.lock index ba1a4e300..46ea77411 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,97 +7,72 @@ resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.0.3.tgz#bc5b5532ecafd923a61f2fb097e3b108c0106a3f" integrity sha512-GLyWIFBbGvpKPGo55JyRZAo4lVbnBiD52cKlw/0Vt+wnmKvWJkpZvsjVoaIolyBXDeAQKSicRtqFNPem9w0WYA== -"@ampproject/toolbox-core@^2.6.0": - version "2.6.1" - resolved "https://registry.yarnpkg.com/@ampproject/toolbox-core/-/toolbox-core-2.6.1.tgz#af97ec253bf39e5fe5121b8ec28f1f35d1878446" - integrity sha512-hTsd9J2yy3JPMClG8BuUhUfMDtd3oDhCuRe/SyZJYQfNMN8hQHt7LNXtdOzZr0Kw7nTepHmn7GODS68fZN4OQQ== +"@ampproject/toolbox-core@2.7.4", "@ampproject/toolbox-core@^2.7.1-alpha.0": + version "2.7.4" + resolved "https://registry.yarnpkg.com/@ampproject/toolbox-core/-/toolbox-core-2.7.4.tgz#8355136f16301458ce942acf6c55952c9a415627" + integrity sha512-qpBhcS4urB7IKc+jx2kksN7BuvvwCo7Y3IstapWo+EW+COY5EYAUwb2pil37v3TsaqHKgX//NloFP1SKzGZAnw== dependencies: cross-fetch "3.0.6" lru-cache "6.0.0" -"@ampproject/toolbox-optimizer@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@ampproject/toolbox-optimizer/-/toolbox-optimizer-2.6.0.tgz#e1bde0697d0fb25ab888bc0d0422998abaf6bad1" - integrity sha512-saToXVopb15a6zKK6kW4B1N/sYZZddkECcqmfTotRxJ2DaLE+wFB6jgWLbaPkgHwvLPQyA2IjV9BHJ/KUFuGzg== +"@ampproject/toolbox-optimizer@2.7.1-alpha.0": + version "2.7.1-alpha.0" + resolved "https://registry.yarnpkg.com/@ampproject/toolbox-optimizer/-/toolbox-optimizer-2.7.1-alpha.0.tgz#1571dcd02608223ff68f6b7223102a123e381197" + integrity sha512-WGPZKVQvHgNYJk1XVJCCmY+NVGTGJtvn0OALDyiegN4FJWOcilQUhCIcjMkZN59u1flz/u+sEKccM5qsROqVyg== dependencies: - "@ampproject/toolbox-core" "^2.6.0" - "@ampproject/toolbox-runtime-version" "^2.6.0" + "@ampproject/toolbox-core" "^2.7.1-alpha.0" + "@ampproject/toolbox-runtime-version" "^2.7.1-alpha.0" "@ampproject/toolbox-script-csp" "^2.5.4" - "@ampproject/toolbox-validator-rules" "^2.5.4" + "@ampproject/toolbox-validator-rules" "^2.7.1-alpha.0" abort-controller "3.0.0" - cross-fetch "3.0.5" - cssnano-simple "1.0.5" - dom-serializer "1.0.1" - domhandler "3.0.0" - domutils "2.1.0" - htmlparser2 "4.1.0" + cross-fetch "3.0.6" + cssnano-simple "1.2.1" + dom-serializer "1.1.0" + domhandler "3.3.0" + domutils "2.4.2" + htmlparser2 "5.0.1" https-proxy-agent "5.0.0" lru-cache "6.0.0" - node-fetch "2.6.0" + node-fetch "2.6.1" normalize-html-whitespace "1.0.0" postcss "7.0.32" postcss-safe-parser "4.0.2" - terser "4.8.0" + terser "5.5.1" -"@ampproject/toolbox-runtime-version@^2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@ampproject/toolbox-runtime-version/-/toolbox-runtime-version-2.6.0.tgz#c2a310840a6c60a7f5046d2ccaf45646a761bd4f" - integrity sha512-wT+Ehsoq2PRXqpgjebygHD01BpSlaAE4HfDEVxgPVT8oAsLzE4ywZgzI2VQZfaCdb8qLyO5+WXrLSoJXxDBo2Q== +"@ampproject/toolbox-runtime-version@^2.7.1-alpha.0": + version "2.7.4" + resolved "https://registry.yarnpkg.com/@ampproject/toolbox-runtime-version/-/toolbox-runtime-version-2.7.4.tgz#f49da0dab122101ef75ed3caed3a0142487b73e1" + integrity sha512-SAdOUOERp42thVNWaBJlnFvFVvnacMVnz5z9LyUZHSnoL1EqrAW5Sz5jv+Ly+gkA8NYsEaUxAdSCBAzE9Uzb4Q== dependencies: - "@ampproject/toolbox-core" "^2.6.0" + "@ampproject/toolbox-core" "2.7.4" "@ampproject/toolbox-script-csp@^2.5.4": version "2.5.4" resolved "https://registry.yarnpkg.com/@ampproject/toolbox-script-csp/-/toolbox-script-csp-2.5.4.tgz#d8b7b91a678ae8f263cb36d9b74e441b7d633aad" integrity sha512-+knTYetI5nWllRZ9wFcj7mYxelkiiFVRAAW/hl0ad8EnKHMH82tRlk40CapEnUHhp6Er5sCYkumQ8dngs3Q4zQ== -"@ampproject/toolbox-validator-rules@^2.5.4": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@ampproject/toolbox-validator-rules/-/toolbox-validator-rules-2.5.4.tgz#7dee3a3edceefea459d060571db8cc6e7bbf0dd6" - integrity sha512-bS7uF+h0s5aiklc/iRaujiSsiladOsZBLrJ6QImJDXvubCAQtvE7om7ShlGSXixkMAO0OVMDWyuwLlEy8V1Ing== +"@ampproject/toolbox-validator-rules@^2.7.1-alpha.0": + version "2.7.4" + resolved "https://registry.yarnpkg.com/@ampproject/toolbox-validator-rules/-/toolbox-validator-rules-2.7.4.tgz#a58b5eca723f6c3557ac83b696de0247f5f03ce4" + integrity sha512-z3JRcpIZLLdVC9XVe7YTZuB3a/eR9s2SjElYB9AWRdyJyL5Jt7+pGNv4Uwh1uHVoBXsWEVQzNOWSNtrO3mSwZA== dependencies: - cross-fetch "3.0.5" + cross-fetch "3.0.6" -"@babel/code-frame@7.10.4", "@babel/code-frame@^7.10.4": +"@babel/code-frame@7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5", "@babel/code-frame@^7.8.3": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== dependencies: "@babel/highlight" "^7.8.3" -"@babel/compat-data@^7.11.0", "@babel/compat-data@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.1.tgz#d7386a689aa0ddf06255005b4b991988021101a0" - integrity sha512-725AQupWJZ8ba0jbKceeFblZTY90McUBWMwHhkFQ9q1zKPJ95GUktljFcgcsIVwRnTnRKlcYzfiNImg5G9m6ZQ== - -"@babel/core@7.7.7": - version "7.7.7" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.7.tgz#ee155d2e12300bcc0cff6a8ad46f2af5063803e9" - integrity sha512-jlSjuj/7z138NLZALxVgrx13AOtqip42ATZP7+kYl53GvDV6+4dCek1mVUo8z8c8Xnw/mx2q3d9HWh3griuesQ== - dependencies: - "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.7.7" - "@babel/helpers" "^7.7.4" - "@babel/parser" "^7.7.7" - "@babel/template" "^7.7.4" - "@babel/traverse" "^7.7.4" - "@babel/types" "^7.7.4" - convert-source-map "^1.7.0" - debug "^4.1.0" - json5 "^2.1.0" - lodash "^4.17.13" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - -"@babel/generator@^7.10.5", "@babel/generator@^7.9.0": +"@babel/generator@^7.9.0": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.5.tgz#1b903554bc8c583ee8d25f1e8969732e6b829a69" integrity sha512-3vXxr3FEW7E7lJZiWQ3bM4+v/Vyr9C+hpolQ8BGFr9Y8Ri2tFLWTixmwKBafDujO1WVah4fhZBeU1bieKdghig== @@ -106,129 +81,7 @@ jsesc "^2.5.1" source-map "^0.5.0" -"@babel/generator@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.1.tgz#0d70be32bdaa03d7c51c8597dda76e0df1f15468" - integrity sha512-DB+6rafIdc9o72Yc3/Ph5h+6hUjeOp66pF0naQBgUFFuPqzQwIlPTm3xZR7YNvduIMtkDIj2t21LSQwnbCrXvg== - dependencies: - "@babel/types" "^7.12.1" - jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/generator@^7.7.7", "@babel/generator@^7.9.6": - version "7.9.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.6.tgz#5408c82ac5de98cda0d77d8124e99fa1f2170a43" - integrity sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ== - dependencies: - "@babel/types" "^7.9.6" - jsesc "^2.5.1" - lodash "^4.17.13" - source-map "^0.5.0" - -"@babel/helper-annotate-as-pure@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" - integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-annotate-as-pure@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee" - integrity sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz#bb0b75f31bf98cbf9ff143c1ae578b87274ae1a3" - integrity sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-builder-react-jsx-experimental@^7.12.1": - version "7.12.4" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.12.4.tgz#55fc1ead5242caa0ca2875dcb8eed6d311e50f48" - integrity sha512-AjEa0jrQqNk7eDQOo0pTfUOwQBMF+xVqrausQwT9/rTKy0g04ggFNaJpaE09IQMn9yExluigWMJcj0WC7bq+Og== - dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-module-imports" "^7.12.1" - "@babel/types" "^7.12.1" - -"@babel/helper-builder-react-jsx@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz#8095cddbff858e6fa9c326daee54a2f2732c1d5d" - integrity sha512-5nPcIZ7+KKDxT1427oBivl9V9YTal7qk0diccnh7RrcgrT/pGFOjgGw1dgryyx1GvHEpXVfoDF6Ak3rTiWh8Rg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-compilation-targets@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.1.tgz#310e352888fbdbdd8577be8dfdd2afb9e7adcf50" - integrity sha512-jtBEif7jsPwP27GPHs06v4WBV0KrE8a/P7n0N0sSvHn2hwUCYnolP/CLmz51IzAW4NlN+HuoBtb9QcwnRo9F/g== - dependencies: - "@babel/compat-data" "^7.12.1" - "@babel/helper-validator-option" "^7.12.1" - browserslist "^4.12.0" - semver "^5.5.0" - -"@babel/helper-create-class-features-plugin@^7.10.4", "@babel/helper-create-class-features-plugin@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz#3c45998f431edd4a9214c5f1d3ad1448a6137f6e" - integrity sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w== - dependencies: - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-member-expression-to-functions" "^7.12.1" - "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/helper-replace-supers" "^7.12.1" - "@babel/helper-split-export-declaration" "^7.10.4" - -"@babel/helper-create-regexp-features-plugin@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.1.tgz#18b1302d4677f9dc4740fe8c9ed96680e29d37e8" - integrity sha512-rsZ4LGvFTZnzdNZR5HZdmJVuXK8834R5QkF3WvcnBhrlVtF0HSIUC6zbreL9MgjTywhKokn8RIYRiq99+DLAxA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-regex" "^7.10.4" - regexpu-core "^4.7.1" - -"@babel/helper-create-regexp-features-plugin@^7.8.3", "@babel/helper-create-regexp-features-plugin@^7.8.8": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz#5d84180b588f560b7864efaeea89243e58312087" - integrity sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-regex" "^7.8.3" - regexpu-core "^4.7.0" - -"@babel/helper-define-map@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" - integrity sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ== - dependencies: - "@babel/helper-function-name" "^7.10.4" - "@babel/types" "^7.10.5" - lodash "^4.17.19" - -"@babel/helper-explode-assignable-expression@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz#8006a466695c4ad86a2a5f2fb15b5f2c31ad5633" - integrity sha512-dmUwH8XmlrUpVqgtZ737tK88v07l840z9j3OEhCLwKTkjlvKpfqXVIZ0wpK3aeOxspwGrf/5AP5qLx4rO3w5rA== - dependencies: - "@babel/types" "^7.12.1" - -"@babel/helper-function-name@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" - integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== - dependencies: - "@babel/helper-get-function-arity" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-function-name@^7.8.3", "@babel/helper-function-name@^7.9.5": +"@babel/helper-function-name@^7.8.3": version "7.9.5" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz#2b53820d35275120e1874a82e5aabe1376920a5c" integrity sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw== @@ -237,13 +90,6 @@ "@babel/template" "^7.8.3" "@babel/types" "^7.9.5" -"@babel/helper-get-function-arity@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" - integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== - dependencies: - "@babel/types" "^7.10.4" - "@babel/helper-get-function-arity@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" @@ -251,120 +97,6 @@ dependencies: "@babel/types" "^7.8.3" -"@babel/helper-hoist-variables@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" - integrity sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-member-expression-to-functions@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz#fba0f2fcff3fba00e6ecb664bb5e6e26e2d6165c" - integrity sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ== - dependencies: - "@babel/types" "^7.12.1" - -"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.1.tgz#1644c01591a15a2f084dd6d092d9430eb1d1216c" - integrity sha512-ZeC1TlMSvikvJNy1v/wPIazCu3NdOwgYZLIkmIyAsGhqkNpiDoQQRmaCK8YP4Pq3GPTLPV9WXaPCJKvx06JxKA== - dependencies: - "@babel/types" "^7.12.1" - -"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c" - integrity sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w== - dependencies: - "@babel/helper-module-imports" "^7.12.1" - "@babel/helper-replace-supers" "^7.12.1" - "@babel/helper-simple-access" "^7.12.1" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/helper-validator-identifier" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.12.1" - "@babel/types" "^7.12.1" - lodash "^4.17.19" - -"@babel/helper-optimise-call-expression@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" - integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" - integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ== - -"@babel/helper-plugin-utils@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" - integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== - -"@babel/helper-regex@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0" - integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg== - dependencies: - lodash "^4.17.19" - -"@babel/helper-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.8.3.tgz#139772607d51b93f23effe72105b319d2a4c6965" - integrity sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ== - dependencies: - lodash "^4.17.13" - -"@babel/helper-remap-async-to-generator@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz#8c4dbbf916314f6047dc05e6a2217074238347fd" - integrity sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A== - dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-wrap-function" "^7.10.4" - "@babel/types" "^7.12.1" - -"@babel/helper-replace-supers@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.1.tgz#f15c9cc897439281891e11d5ce12562ac0cf3fa9" - integrity sha512-zJjTvtNJnCFsCXVi5rUInstLd/EIVNmIKA1Q9ynESmMBWPWd+7sdR+G4/wdu+Mppfep0XLyG2m7EBPvjCeFyrw== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.12.1" - "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/traverse" "^7.12.1" - "@babel/types" "^7.12.1" - -"@babel/helper-simple-access@^7.10.4", "@babel/helper-simple-access@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz#32427e5aa61547d38eb1e6eaf5fd1426fdad9136" - integrity sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA== - dependencies: - "@babel/types" "^7.12.1" - -"@babel/helper-skip-transparent-expression-wrappers@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf" - integrity sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA== - dependencies: - "@babel/types" "^7.12.1" - -"@babel/helper-split-export-declaration@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz#2c70576eaa3b5609b24cb99db2888cc3fc4251d1" - integrity sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-split-export-declaration@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" - integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== - dependencies: - "@babel/types" "^7.11.0" - "@babel/helper-split-export-declaration@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9" @@ -382,30 +114,6 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g== -"@babel/helper-validator-option@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz#175567380c3e77d60ff98a54bb015fe78f2178d9" - integrity sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A== - -"@babel/helper-wrap-function@^7.10.4": - version "7.12.3" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.12.3.tgz#3332339fc4d1fbbf1c27d7958c27d34708e990d9" - integrity sha512-Cvb8IuJDln3rs6tzjW3Y8UeelAOdnpB8xtQ4sme2MSZ9wOxrbThporC0y/EtE16VAtoyEfLM404Xr1e0OOp+ow== - dependencies: - "@babel/helper-function-name" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helpers@^7.7.4": - version "7.9.6" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.6.tgz#092c774743471d0bb6c7de3ad465ab3d3486d580" - integrity sha512-tI4bUbldloLcHWoRUMAj4g1bF313M/o6fBKhIsb3QnGVPwRm9JsNf/gqMkQ7zjqReABiffPV6RWj7hEglID5Iw== - dependencies: - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.9.6" - "@babel/types" "^7.9.6" - "@babel/highlight@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" @@ -424,737 +132,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.10.4", "@babel/parser@^7.10.5", "@babel/parser@^7.9.0": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b" - integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ== - -"@babel/parser@^7.12.1": - version "7.12.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.3.tgz#a305415ebe7a6c7023b40b5122a0662d928334cd" - integrity sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw== - "@babel/parser@^7.7.0", "@babel/parser@^7.8.6": version "7.9.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== -"@babel/parser@^7.7.7", "@babel/parser@^7.9.6": - version "7.9.6" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.6.tgz#3b1bbb30dabe600cd72db58720998376ff653bc7" - integrity sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q== - -"@babel/plugin-proposal-async-generator-functions@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz#dc6c1170e27d8aca99ff65f4925bd06b1c90550e" - integrity sha512-d+/o30tJxFxrA1lhzJqiUcEJdI6jKlNregCv5bASeGf2Q4MXmnwH7viDo7nhx1/ohf09oaH8j1GVYG/e3Yqk6A== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-remap-async-to-generator" "^7.12.1" - "@babel/plugin-syntax-async-generators" "^7.8.0" - -"@babel/plugin-proposal-class-properties@7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" - integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-proposal-class-properties@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz#a082ff541f2a29a4821065b8add9346c0c16e5de" - integrity sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-proposal-dynamic-import@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz#43eb5c2a3487ecd98c5c8ea8b5fdb69a2749b2dc" - integrity sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-dynamic-import" "^7.8.0" - -"@babel/plugin-proposal-export-namespace-from@7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz#570d883b91031637b3e2958eea3c438e62c05f54" - integrity sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-proposal-export-namespace-from@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz#8b9b8f376b2d88f5dd774e4d24a5cc2e3679b6d4" - integrity sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-proposal-json-strings@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz#d45423b517714eedd5621a9dfdc03fa9f4eb241c" - integrity sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-json-strings" "^7.8.0" - -"@babel/plugin-proposal-logical-assignment-operators@^7.11.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz#f2c490d36e1b3c9659241034a5d2cd50263a2751" - integrity sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz#3ed4fff31c015e7f3f1467f190dbe545cd7b046c" - integrity sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" - -"@babel/plugin-proposal-numeric-separator@7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06" - integrity sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-proposal-numeric-separator@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.1.tgz#0e2c6774c4ce48be412119b4d693ac777f7685a6" - integrity sha512-MR7Ok+Af3OhNTCxYVjJZHS0t97ydnJZt/DbR4WISO39iDnhiD8XHrY12xuSJ90FFEGjir0Fzyyn7g/zY6hxbxA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-proposal-object-rest-spread@7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af" - integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-transform-parameters" "^7.10.4" - -"@babel/plugin-proposal-object-rest-spread@^7.11.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz#def9bd03cea0f9b72283dac0ec22d289c7691069" - integrity sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-transform-parameters" "^7.12.1" - -"@babel/plugin-proposal-optional-catch-binding@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz#ccc2421af64d3aae50b558a71cede929a5ab2942" - integrity sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" - -"@babel/plugin-proposal-optional-chaining@^7.11.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.1.tgz#cce122203fc8a32794296fc377c6dedaf4363797" - integrity sha512-c2uRpY6WzaVDzynVY9liyykS+kVU+WRZPMPYpkelXH8KBt1oXoI89kPbZKKG/jDT5UK92FTW2fZkZaJhdiBabw== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" - "@babel/plugin-syntax-optional-chaining" "^7.8.0" - -"@babel/plugin-proposal-private-methods@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz#86814f6e7a21374c980c10d38b4493e703f4a389" - integrity sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-proposal-unicode-property-regex@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz#2a183958d417765b9eae334f47758e5d6a82e072" - integrity sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz#ee3a95e90cdc04fe8cd92ec3279fa017d68a0d1d" - integrity sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.8" - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-async-generators@^7.8.0": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-bigint@7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" - integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz#bcb297c5366e79bebadef509549cd93b04f19978" - integrity sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-dynamic-import@7.8.3", "@babel/plugin-syntax-dynamic-import@^7.8.0": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-json-strings@^7.8.0": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-jsx@7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.4.tgz#39abaae3cbf710c4373d8429484e6ba21340166c" - integrity sha512-KCg9mio9jwiARCB7WAcQ7Y1q+qicILjoK8LP/VkPkEKaf5dkaZZK1EcTe91a3JJlZ3qy6L5s9X52boEYi8DM9g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-jsx@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz#9d9d357cc818aa7ae7935917c1257f67677a0926" - integrity sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.0": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.0": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.0": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-top-level-await@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz#dd6c0b357ac1bb142d98537450a319625d13d2a0" - integrity sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-typescript@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.1.tgz#460ba9d77077653803c3dd2e673f76d66b4029e5" - integrity sha512-UZNEcCY+4Dp9yYRCAHrHDU+9ZXLYaY9MgBXSRLkB9WjYFRR6quJBumfVrEkUxrePPBwFcpWfNKXqVRQQtm7mMA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-arrow-functions@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz#8083ffc86ac8e777fbe24b5967c4b2521f3cb2b3" - integrity sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-async-to-generator@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz#3849a49cc2a22e9743cbd6b52926d30337229af1" - integrity sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A== - dependencies: - "@babel/helper-module-imports" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-remap-async-to-generator" "^7.12.1" - -"@babel/plugin-transform-block-scoped-functions@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz#f2a1a365bde2b7112e0a6ded9067fdd7c07905d9" - integrity sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-block-scoping@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.1.tgz#f0ee727874b42a208a48a586b84c3d222c2bbef1" - integrity sha512-zJyAC9sZdE60r1nVQHblcfCj29Dh2Y0DOvlMkcqSo0ckqjiCwNiUezUKw+RjOCwGfpLRwnAeQ2XlLpsnGkvv9w== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-classes@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz#65e650fcaddd3d88ddce67c0f834a3d436a32db6" - integrity sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog== - dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-define-map" "^7.10.4" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-replace-supers" "^7.12.1" - "@babel/helper-split-export-declaration" "^7.10.4" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz#d68cf6c9b7f838a8a4144badbe97541ea0904852" - integrity sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-destructuring@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz#b9a570fe0d0a8d460116413cb4f97e8e08b2f847" - integrity sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-dotall-regex@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz#a1d16c14862817b6409c0a678d6f9373ca9cd975" - integrity sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e" - integrity sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-transform-duplicate-keys@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz#745661baba295ac06e686822797a69fbaa2ca228" - integrity sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-exponentiation-operator@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz#b0f2ed356ba1be1428ecaf128ff8a24f02830ae0" - integrity sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-for-of@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz#07640f28867ed16f9511c99c888291f560921cfa" - integrity sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-function-name@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz#2ec76258c70fe08c6d7da154003a480620eba667" - integrity sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw== - dependencies: - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-literals@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz#d73b803a26b37017ddf9d3bb8f4dc58bfb806f57" - integrity sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-member-expression-literals@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz#496038602daf1514a64d43d8e17cbb2755e0c3ad" - integrity sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-modules-amd@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz#3154300b026185666eebb0c0ed7f8415fefcf6f9" - integrity sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ== - dependencies: - "@babel/helper-module-transforms" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-commonjs@7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" - integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w== - dependencies: - "@babel/helper-module-transforms" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-simple-access" "^7.10.4" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-commonjs@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz#fa403124542636c786cf9b460a0ffbb48a86e648" - integrity sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag== - dependencies: - "@babel/helper-module-transforms" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-simple-access" "^7.12.1" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-systemjs@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz#663fea620d593c93f214a464cd399bf6dc683086" - integrity sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q== - dependencies: - "@babel/helper-hoist-variables" "^7.10.4" - "@babel/helper-module-transforms" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-validator-identifier" "^7.10.4" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-umd@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz#eb5a218d6b1c68f3d6217b8fa2cc82fec6547902" - integrity sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q== - dependencies: - "@babel/helper-module-transforms" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz#b407f5c96be0d9f5f88467497fa82b30ac3e8753" - integrity sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.1" - -"@babel/plugin-transform-new-target@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz#80073f02ee1bb2d365c3416490e085c95759dec0" - integrity sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-object-super@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz#4ea08696b8d2e65841d0c7706482b048bed1066e" - integrity sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-replace-supers" "^7.12.1" - -"@babel/plugin-transform-parameters@^7.10.4", "@babel/plugin-transform-parameters@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz#d2e963b038771650c922eff593799c96d853255d" - integrity sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-property-literals@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz#41bc81200d730abb4456ab8b3fbd5537b59adecd" - integrity sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-react-display-name@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz#1cbcd0c3b1d6648c55374a22fc9b6b7e5341c00d" - integrity sha512-cAzB+UzBIrekfYxyLlFqf/OagTvHLcVBb5vpouzkYkBclRPraiygVnafvAoipErZLI8ANv8Ecn6E/m5qPXD26w== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-react-jsx-development@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.1.tgz#0b8f8cd531dcf7991f1e5f2c10a2a4f1cfc78e36" - integrity sha512-IilcGWdN1yNgEGOrB96jbTplRh+V2Pz1EoEwsKsHfX1a/L40cUYuD71Zepa7C+ujv7kJIxnDftWeZbKNEqZjCQ== - dependencies: - "@babel/helper-builder-react-jsx-experimental" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-jsx" "^7.12.1" - -"@babel/plugin-transform-react-jsx-self@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.12.1.tgz#ef43cbca2a14f1bd17807dbe4376ff89d714cf28" - integrity sha512-FbpL0ieNWiiBB5tCldX17EtXgmzeEZjFrix72rQYeq9X6nUK38HCaxexzVQrZWXanxKJPKVVIU37gFjEQYkPkA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-react-jsx-source@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.12.1.tgz#d07de6863f468da0809edcf79a1aa8ce2a82a26b" - integrity sha512-keQ5kBfjJNRc6zZN1/nVHCd6LLIHq4aUKcVnvE/2l+ZZROSbqoiGFRtT5t3Is89XJxBQaP7NLZX2jgGHdZvvFQ== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-react-jsx@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.1.tgz#c2d96c77c2b0e4362cc4e77a43ce7c2539d478cb" - integrity sha512-RmKejwnT0T0QzQUzcbP5p1VWlpnP8QHtdhEtLG55ZDQnJNalbF3eeDyu3dnGKvGzFIQiBzFhBYTwvv435p9Xpw== - dependencies: - "@babel/helper-builder-react-jsx" "^7.10.4" - "@babel/helper-builder-react-jsx-experimental" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-jsx" "^7.12.1" - -"@babel/plugin-transform-react-pure-annotations@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz#05d46f0ab4d1339ac59adf20a1462c91b37a1a42" - integrity sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-regenerator@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz#5f0a28d842f6462281f06a964e88ba8d7ab49753" - integrity sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng== - dependencies: - regenerator-transform "^0.14.2" - -"@babel/plugin-transform-reserved-words@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz#6fdfc8cc7edcc42b36a7c12188c6787c873adcd8" - integrity sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-runtime@7.11.5": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.11.5.tgz#f108bc8e0cf33c37da031c097d1df470b3a293fc" - integrity sha512-9aIoee+EhjySZ6vY5hnLjigHzunBlscx9ANKutkeWTJTx6m5Rbq6Ic01tLvO54lSusR+BxV7u4UDdCmXv5aagg== - dependencies: - "@babel/helper-module-imports" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - resolve "^1.8.1" - semver "^5.5.1" - -"@babel/plugin-transform-shorthand-properties@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz#0bf9cac5550fce0cfdf043420f661d645fdc75e3" - integrity sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-spread@^7.11.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz#527f9f311be4ec7fdc2b79bb89f7bf884b3e1e1e" - integrity sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" - -"@babel/plugin-transform-sticky-regex@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.1.tgz#5c24cf50de396d30e99afc8d1c700e8bce0f5caf" - integrity sha512-CiUgKQ3AGVk7kveIaPEET1jNDhZZEl1RPMWdTBE1799bdz++SwqDHStmxfCtDfBhQgCl38YRiSnrMuUMZIWSUQ== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-regex" "^7.10.4" - -"@babel/plugin-transform-template-literals@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz#b43ece6ed9a79c0c71119f576d299ef09d942843" - integrity sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-typeof-symbol@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.1.tgz#9ca6be343d42512fbc2e68236a82ae64bc7af78a" - integrity sha512-EPGgpGy+O5Kg5pJFNDKuxt9RdmTgj5sgrus2XVeMp/ZIbOESadgILUbm50SNpghOh3/6yrbsH+NB5+WJTmsA7Q== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-typescript@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.1.tgz#d92cc0af504d510e26a754a7dbc2e5c8cd9c7ab4" - integrity sha512-VrsBByqAIntM+EYMqSm59SiMEf7qkmI9dqMt6RbD/wlwueWmYcI0FFK5Fj47pP6DRZm+3teXjosKlwcZJ5lIMw== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-typescript" "^7.12.1" - -"@babel/plugin-transform-unicode-escapes@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz#5232b9f81ccb07070b7c3c36c67a1b78f1845709" - integrity sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-unicode-regex@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz#cc9661f61390db5c65e3febaccefd5c6ac3faecb" - integrity sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/preset-env@7.11.5": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.5.tgz#18cb4b9379e3e92ffea92c07471a99a2914e4272" - integrity sha512-kXqmW1jVcnB2cdueV+fyBM8estd5mlNfaQi6lwLgRwCby4edpavgbFhiBNjmWA3JpB/yZGSISa7Srf+TwxDQoA== - dependencies: - "@babel/compat-data" "^7.11.0" - "@babel/helper-compilation-targets" "^7.10.4" - "@babel/helper-module-imports" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-proposal-async-generator-functions" "^7.10.4" - "@babel/plugin-proposal-class-properties" "^7.10.4" - "@babel/plugin-proposal-dynamic-import" "^7.10.4" - "@babel/plugin-proposal-export-namespace-from" "^7.10.4" - "@babel/plugin-proposal-json-strings" "^7.10.4" - "@babel/plugin-proposal-logical-assignment-operators" "^7.11.0" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4" - "@babel/plugin-proposal-numeric-separator" "^7.10.4" - "@babel/plugin-proposal-object-rest-spread" "^7.11.0" - "@babel/plugin-proposal-optional-catch-binding" "^7.10.4" - "@babel/plugin-proposal-optional-chaining" "^7.11.0" - "@babel/plugin-proposal-private-methods" "^7.10.4" - "@babel/plugin-proposal-unicode-property-regex" "^7.10.4" - "@babel/plugin-syntax-async-generators" "^7.8.0" - "@babel/plugin-syntax-class-properties" "^7.10.4" - "@babel/plugin-syntax-dynamic-import" "^7.8.0" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.0" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" - "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-syntax-top-level-await" "^7.10.4" - "@babel/plugin-transform-arrow-functions" "^7.10.4" - "@babel/plugin-transform-async-to-generator" "^7.10.4" - "@babel/plugin-transform-block-scoped-functions" "^7.10.4" - "@babel/plugin-transform-block-scoping" "^7.10.4" - "@babel/plugin-transform-classes" "^7.10.4" - "@babel/plugin-transform-computed-properties" "^7.10.4" - "@babel/plugin-transform-destructuring" "^7.10.4" - "@babel/plugin-transform-dotall-regex" "^7.10.4" - "@babel/plugin-transform-duplicate-keys" "^7.10.4" - "@babel/plugin-transform-exponentiation-operator" "^7.10.4" - "@babel/plugin-transform-for-of" "^7.10.4" - "@babel/plugin-transform-function-name" "^7.10.4" - "@babel/plugin-transform-literals" "^7.10.4" - "@babel/plugin-transform-member-expression-literals" "^7.10.4" - "@babel/plugin-transform-modules-amd" "^7.10.4" - "@babel/plugin-transform-modules-commonjs" "^7.10.4" - "@babel/plugin-transform-modules-systemjs" "^7.10.4" - "@babel/plugin-transform-modules-umd" "^7.10.4" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.4" - "@babel/plugin-transform-new-target" "^7.10.4" - "@babel/plugin-transform-object-super" "^7.10.4" - "@babel/plugin-transform-parameters" "^7.10.4" - "@babel/plugin-transform-property-literals" "^7.10.4" - "@babel/plugin-transform-regenerator" "^7.10.4" - "@babel/plugin-transform-reserved-words" "^7.10.4" - "@babel/plugin-transform-shorthand-properties" "^7.10.4" - "@babel/plugin-transform-spread" "^7.11.0" - "@babel/plugin-transform-sticky-regex" "^7.10.4" - "@babel/plugin-transform-template-literals" "^7.10.4" - "@babel/plugin-transform-typeof-symbol" "^7.10.4" - "@babel/plugin-transform-unicode-escapes" "^7.10.4" - "@babel/plugin-transform-unicode-regex" "^7.10.4" - "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.11.5" - browserslist "^4.12.0" - core-js-compat "^3.6.2" - invariant "^2.2.2" - levenary "^1.1.1" - semver "^5.5.0" - -"@babel/preset-modules@0.1.4": - version "0.1.4" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e" - integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/preset-modules@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72" - integrity sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/preset-react@7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.10.4.tgz#92e8a66d816f9911d11d4cc935be67adfc82dbcf" - integrity sha512-BrHp4TgOIy4M19JAfO1LhycVXOPWdDbTRep7eVyatf174Hff+6Uk53sDyajqZPu8W1qXRBiYOfIamek6jA7YVw== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-transform-react-display-name" "^7.10.4" - "@babel/plugin-transform-react-jsx" "^7.10.4" - "@babel/plugin-transform-react-jsx-development" "^7.10.4" - "@babel/plugin-transform-react-jsx-self" "^7.10.4" - "@babel/plugin-transform-react-jsx-source" "^7.10.4" - "@babel/plugin-transform-react-pure-annotations" "^7.10.4" - -"@babel/preset-typescript@7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.10.4.tgz#7d5d052e52a682480d6e2cc5aa31be61c8c25e36" - integrity sha512-SdYnvGPv+bLlwkF2VkJnaX/ni1sMNetcGI1+nThF1gyv6Ph8Qucc4ZZAjM5yZcE/AKRXIOTZz7eSRDWOEjPyRQ== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-transform-typescript" "^7.10.4" +"@babel/parser@^7.9.0": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b" + integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ== "@babel/runtime-corejs3@^7.8.3": version "7.9.6" @@ -1164,30 +150,21 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@7.11.2": - version "7.11.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" - integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== +"@babel/runtime@7.12.5": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.8.7": version "7.9.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" - integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/parser" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/template@^7.7.4", "@babel/template@^7.8.3": +"@babel/template@^7.8.3": version "7.8.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== @@ -1196,36 +173,6 @@ "@babel/parser" "^7.8.6" "@babel/types" "^7.8.6" -"@babel/traverse@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.5.tgz#77ce464f5b258be265af618d8fddf0536f20b564" - integrity sha512-yc/fyv2gUjPqzTz0WHeRJH2pv7jA9kA7mBX2tXl/x5iOE81uaVPuGPtaYk7wmkx4b67mQ7NqI8rmT2pF47KYKQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.10.5" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.10.4" - "@babel/parser" "^7.10.5" - "@babel/types" "^7.10.5" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.19" - -"@babel/traverse@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.1.tgz#941395e0c5cc86d5d3e75caa095d3924526f0c1e" - integrity sha512-MA3WPoRt1ZHo2ZmoGKNqi20YnPt0B1S0GTZEPhhd+hw2KGUzBlHuVunj6K4sNuK+reEvyiPwtp0cpaqLzJDmAw== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.12.1" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.12.1" - "@babel/types" "^7.12.1" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.19" - "@babel/traverse@^7.7.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.0.tgz#d3882c2830e513f4fe4cec9fe76ea1cc78747892" @@ -1241,30 +188,6 @@ globals "^11.1.0" lodash "^4.17.13" -"@babel/traverse@^7.7.4", "@babel/traverse@^7.9.6": - version "7.9.6" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.6.tgz#5540d7577697bf619cc57b92aa0f1c231a94f442" - integrity sha512-b3rAHSjbxy6VEAvlxM8OV/0X4XrG72zoxme6q1MOoe2vd0bEc+TwayhuC1+Dfgqh1QEG+pj7atQqvUprHIccsg== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.6" - "@babel/helper-function-name" "^7.9.5" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/parser" "^7.9.6" - "@babel/types" "^7.9.6" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.13" - -"@babel/types@7.11.5": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" - integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.19" - to-fast-properties "^2.0.0" - "@babel/types@7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c" @@ -1274,7 +197,7 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@babel/types@^7.10.4", "@babel/types@^7.10.5": +"@babel/types@^7.10.5": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.5.tgz#d88ae7e2fde86bfbfe851d4d81afa70a997b5d15" integrity sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q== @@ -1283,16 +206,7 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.1.tgz#e109d9ab99a8de735be287ee3d6a9947a190c4ae" - integrity sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.19" - to-fast-properties "^2.0.0" - -"@babel/types@^7.4.4", "@babel/types@^7.7.0", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0": +"@babel/types@^7.7.0", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.0.tgz#00b064c3df83ad32b2dbf5ff07312b15c7f1efb5" integrity sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng== @@ -1301,7 +215,7 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@babel/types@^7.7.4", "@babel/types@^7.9.5", "@babel/types@^7.9.6": +"@babel/types@^7.9.5": version "7.9.6" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.6.tgz#2c5502b427251e9de1bd2dff95add646d95cc9f7" integrity sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA== @@ -1433,20 +347,20 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" -"@next/env@9.5.5": - version "9.5.5" - resolved "https://registry.yarnpkg.com/@next/env/-/env-9.5.5.tgz#db993649ec6e619e34a36de90dc2baa52fc5280f" - integrity sha512-N9wdjU6XoqLqNQWtrGiWtp1SUuJsYK1cNrZ24A6YD+4w5CNV5SkZX6aewKZCCLP5Y8UNfTij2FkJiSYUfBjX8g== +"@next/env@10.0.4": + version "10.0.4" + resolved "https://registry.yarnpkg.com/@next/env/-/env-10.0.4.tgz#ac759094d021853616af56a7bd6720e44a92a303" + integrity sha512-U+XIL75XM1pCmY4+9kYbst/0IptlfDnkFfKdgADBZulQlfng4RB3zirdzkoBtod0lVcrGgDryzOi1mM23RiiVQ== -"@next/polyfill-module@9.5.5": - version "9.5.5" - resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-9.5.5.tgz#d9c65679a66664ab4859078f58997113c9d01f10" - integrity sha512-itqYFeHo3yN4ccpHq2uNFC2UVQm12K6DxUVwYdui9MJiiueT0pSGb2laYEjf/G5+vVq7M2vb+DkjkOkPMBVfeg== +"@next/polyfill-module@10.0.4": + version "10.0.4" + resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-10.0.4.tgz#c34391a12ad80d6e373c403f96c8e2bbd793dca1" + integrity sha512-i2gLUa3YuZ2eQg+d91n+jS4YbPVKg1v0HHIUeJFJMMtpG/apBkTuTLBQGJXe4nKNf7/41NWLDft4ihC3Zfd+Yw== -"@next/react-dev-overlay@9.5.5": - version "9.5.5" - resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-9.5.5.tgz#11b36813d75c43b7bd9d5e478bded1ed5391d03a" - integrity sha512-B1nDANxjXr2oyohv+tX0OXZTmJtO5qEWmisNPGnqQ2Z32IixfaAgyNYVuCVf20ap6EUz5elhgNUwRIFh/e26mQ== +"@next/react-dev-overlay@10.0.4": + version "10.0.4" + resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-10.0.4.tgz#c578a3c71e2f8a8fe2aae8007cc40d1cf10bc768" + integrity sha512-8pKN0PspEtfVFqeSpNQymfXWyV95OTIT0xP9IqILJX2+52ICdU5D+YNuNIwpc4ZOZ0CssM/uYsz6K1FHbCaU7A== dependencies: "@babel/code-frame" "7.10.4" ally.js "1.4.1" @@ -1459,10 +373,10 @@ stacktrace-parser "0.1.10" strip-ansi "6.0.0" -"@next/react-refresh-utils@9.5.5": - version "9.5.5" - resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-9.5.5.tgz#fe559b5ca51c038cb7840e0d669a6d7ef01fe4eb" - integrity sha512-Gz5z0+ID+KAGto6Tkgv1a340damEw3HG6ANLKwNi5/QSHqQ3JUAVxMuhz3qnL54505I777evpzL89ofWEMIWKw== +"@next/react-refresh-utils@10.0.4": + version "10.0.4" + resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-10.0.4.tgz#5ad753572891aa7cb1010b358cc4241d7be20d20" + integrity sha512-kZ/37aSQaR0GCZVqh7WDLkeEZqzjPQoZUDdo6TOWiIEb+089AmfYp8A4/1ra9Fu8T4b4wnB76TRl6tp6DeJLXg== "@nodelib/fs.scandir@2.1.3": version "2.1.3" @@ -1490,12 +404,17 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" -"@npmcli/move-file@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.0.1.tgz#de103070dac0f48ce49cf6693c23af59c0f70464" - integrity sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw== +"@opentelemetry/api@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-0.14.0.tgz#4e17d8d2f1da72b19374efa7b6526aa001267cae" + integrity sha512-L7RMuZr5LzMmZiQSQDy9O1jo0q+DaLy6XpYJfIGfYSfoJA5qzYwUP3sP1uMIQ549DvxAgM3ng85EaPTM/hUHwQ== dependencies: - mkdirp "^1.0.4" + "@opentelemetry/context-base" "^0.14.0" + +"@opentelemetry/context-base@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.14.0.tgz#c67fc20a4d891447ca1a855d7d70fa79a3533001" + integrity sha512-sDOAZcYwynHFTbLo6n8kIbLiVF3a3BLkrmehJUyEbT9F+Smbi47kLGS2gG2g0fjBLR/Lr1InPD7kXL7FaTqEkw== "@sentry/apm@5.13.2": version "5.13.2" @@ -1755,7 +674,7 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== -"@types/json-schema@^7.0.5": +"@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== @@ -2099,16 +1018,13 @@ acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -adjust-sourcemap-loader@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-2.0.0.tgz#6471143af75ec02334b219f54bc7970c52fb29a4" - integrity sha512-4hFsTsn58+YjrU9qKzML2JSSDqKvN8mUGQ0nNIrfPi8hmIONT4L3uUaT6MKdMsZ9AjsU6D2xDkZxCkbQPxChrA== +adjust-sourcemap-loader@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz#5ae12fb5b7b1c585e80bbb5a63ec163a1a45e61e" + integrity sha512-YBrGyT2/uVQ/c6Rr+t6ZJXniY03YtHGMJQYal368burRGYKqhx9qGTWqcBU5s1CwYY9E/ri63RYyG1IacMZtqw== dependencies: - assert "1.4.1" - camelcase "5.0.0" - loader-utils "1.2.3" - object-path "0.11.4" - regex-parser "2.2.10" + loader-utils "^2.0.0" + regex-parser "^2.2.11" agent-base@5: version "5.1.1" @@ -2173,7 +1089,7 @@ ajv@^6.12.2: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^6.12.4: +ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2325,7 +1241,7 @@ app-root-path@>=2.0.1: resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad" integrity sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw== -aproba@^1.1.1: +aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== @@ -2342,6 +1258,14 @@ archive-type@^4.0.0: dependencies: file-type "^4.2.0" +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -2454,13 +1378,6 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -assert@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" - integrity sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE= - dependencies: - util "0.10.3" - assert@^1.1.1: version "1.5.0" resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" @@ -2609,13 +1526,6 @@ babel-eslint@^10.1.0: eslint-visitor-keys "^1.0.0" resolve "^1.12.0" -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - babel-plugin-syntax-jsx@6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" @@ -2652,6 +1562,11 @@ base64-js@^1.0.2, base64-js@^1.2.3: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -2750,6 +1665,15 @@ bl@^1.0.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" +bl@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" + integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + bluebird-lst@^1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/bluebird-lst/-/bluebird-lst-1.0.9.tgz#a64a0e4365658b9ab5fe875eb9dfb694189bb41c" @@ -2893,35 +1817,15 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@4.13.0: - version "4.13.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.13.0.tgz#42556cba011e1b0a2775b611cba6a8eca18e940d" - integrity sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ== +browserslist@4.14.6: + version "4.14.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.6.tgz#97702a9c212e0c6b6afefad913d3a1538e348457" + integrity sha512-zeFYcUo85ENhc/zxHbiIp0LGzzTrE2Pv2JhxvS7kpUb9Q9D38kUX6Bie7pGutJ/5iF5rOxE7CepAuWD56xJ33A== dependencies: - caniuse-lite "^1.0.30001093" - electron-to-chromium "^1.3.488" - escalade "^3.0.1" - node-releases "^1.1.58" - -browserslist@^4.12.0: - version "4.14.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015" - integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA== - dependencies: - caniuse-lite "^1.0.30001135" - electron-to-chromium "^1.3.571" - escalade "^3.1.0" - node-releases "^1.1.61" - -browserslist@^4.8.5: - version "4.12.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.0.tgz#06c6d5715a1ede6c51fc39ff67fd647f740b656d" - integrity sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg== - dependencies: - caniuse-lite "^1.0.30001043" - electron-to-chromium "^1.3.413" - node-releases "^1.1.53" - pkg-up "^2.0.0" + caniuse-lite "^1.0.30001154" + electron-to-chromium "^1.3.585" + escalade "^3.1.1" + node-releases "^1.1.65" buf-compare@^1.0.0: version "1.0.1" @@ -2986,6 +1890,14 @@ buffer@^5.2.1: base64-js "^1.0.2" ieee754 "^1.1.4" +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + builder-util-runtime@8.7.0: version "8.7.0" resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.7.0.tgz#e48ad004835c8284662e8eaf47a53468c66e8e8d" @@ -3019,28 +1931,10 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= -cacache@15.0.5: - version "15.0.5" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0" - integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A== - dependencies: - "@npmcli/move-file" "^1.0.1" - chownr "^2.0.0" - fs-minipass "^2.0.0" - glob "^7.1.4" - infer-owner "^1.0.4" - lru-cache "^6.0.0" - minipass "^3.1.1" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.2" - mkdirp "^1.0.3" - p-map "^4.0.0" - promise-inflight "^1.0.1" - rimraf "^3.0.2" - ssri "^8.0.0" - tar "^6.0.2" - unique-filename "^1.1.1" +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== cacache@^12.0.2: version "12.0.4" @@ -3131,11 +2025,6 @@ camelcase-keys@^6.2.2: map-obj "^4.0.0" quick-lru "^4.0.1" -camelcase@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" - integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== - camelcase@5.3.1, camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -3151,15 +2040,10 @@ camelcase@^6.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.1.0.tgz#27dc176173725fb0adf8a48b647f4d7871944d78" integrity sha512-WCMml9ivU60+8rEJgELlFp1gxFcEGxwYleE3bziHEDeqsqAWGHdimB7beBFGjLzVNgPGyDsfgXLQEYMpmIFnVQ== -caniuse-lite@^1.0.30001043: - version "1.0.30001066" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001066.tgz#0a8a58a10108f2b9bf38e7b65c237b12fd9c5f04" - integrity sha512-Gfj/WAastBtfxLws0RCh2sDbTK/8rJuSeZMecrSkNGYxPcv7EzblmDGfWQCFEQcSqYE2BRgQiJh8HOD07N5hIw== - -caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001113, caniuse-lite@^1.0.30001135: - version "1.0.30001148" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001148.tgz#dc97c7ed918ab33bf8706ddd5e387287e015d637" - integrity sha512-E66qcd0KMKZHNJQt9hiLZGE3J4zuTqE1OnU53miEVtylFbwOEmeA5OsRu90noZful+XGSQOni1aT2tiqu/9yYw== +caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001113, caniuse-lite@^1.0.30001154: + version "1.0.30001185" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001185.tgz" + integrity sha512-Fpi4kVNtNvJ15H0F6vwmXtb3tukv3Zg3qhKkOGUq7KJ1J6b9kf4dnNgtEAFXhRsJo0gNj9W60+wBvn0JcTvdTg== caseless@~0.12.0: version "0.12.0" @@ -3176,6 +2060,15 @@ caw@^2.0.0, caw@^2.0.1: tunnel-agent "^0.6.0" url-to-options "^1.0.1" +chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@4.0.0, chalk@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72" @@ -3195,15 +2088,6 @@ chalk@^1.0.0: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - chalk@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -3225,7 +2109,22 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -chokidar@2.1.8, chokidar@^2.1.8: +chokidar@3.4.3, chokidar@^3.4.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" + integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.5.0" + optionalDependencies: + fsevents "~2.1.2" + +chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== @@ -3259,31 +2158,11 @@ chokidar@^3.4.0: optionalDependencies: fsevents "~2.1.2" -chokidar@^3.4.1: - version "3.4.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" - integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.1.2" - chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - chrome-trace-event@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" @@ -3423,6 +2302,11 @@ code-excerpt@^2.1.1: dependencies: convert-to-spaces "^1.0.1" +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -3431,7 +2315,7 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" -color-convert@^1.9.0: +color-convert@^1.9.0, color-convert@^1.9.1: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -3450,11 +2334,32 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-string@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" + integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" + integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== + dependencies: + color-convert "^1.9.1" + color-string "^1.5.4" + +colorette@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" + integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== + combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -3587,6 +2492,11 @@ console-browserify@^1.1.0: resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + console-stream@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/console-stream/-/console-stream-0.1.1.tgz#a095fe07b20465955f2fafd28b5d72bccd949d44" @@ -3656,14 +2566,6 @@ core-assert@^0.2.0: buf-compare "^1.0.0" is-error "^2.2.0" -core-js-compat@^3.6.2: - version "3.6.5" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.5.tgz#2a51d9a4e25dfd6e690251aa81f99e3c05481f1c" - integrity sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng== - dependencies: - browserslist "^4.8.5" - semver "7.0.0" - core-js-pure@^3.0.0: version "3.6.5" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" @@ -3737,13 +2639,6 @@ create-react-context@^0.1.5: resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.1.6.tgz#0f425931d907741127acc6e31acb4f9015dd9fdc" integrity sha512-eCnYYEUEc5i32LHwpE/W7NlddOB9oHwsPaWtWzYtflNkkwa3IfindIcoXdVWs12zCbwaMCavKNu84EXogVIWHw== -cross-fetch@3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.5.tgz#2739d2981892e7ab488a7ad03b92df2816e03f4c" - integrity sha512-FFLcLtraisj5eteosnX1gf01qYDCOc4fDy0+euOt8Kn9YBY2NtXL/pCoYPavw24NIQkQqm5ZOLsGD5Zzj0gyew== - dependencies: - node-fetch "2.6.0" - cross-fetch@3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c" @@ -3845,35 +2740,20 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -cssnano-preset-simple@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/cssnano-preset-simple/-/cssnano-preset-simple-1.1.4.tgz#7b287a31df786348565d02342df71af8f758ac82" - integrity sha512-EYKDo65W+AxMViUijv/hvhbEnxUjmu3V7omcH1MatPOwjRLrAgVArUOE8wTUyc1ePFEtvV8oCT4/QSRJDorm/A== - dependencies: - postcss "^7.0.32" - -cssnano-preset-simple@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/cssnano-preset-simple/-/cssnano-preset-simple-1.2.0.tgz#afcf13eb076e8ebd91c4f311cd449781c14c7371" - integrity sha512-zojGlY+KasFeQT/SnD/WqYXHcKddz2XHRDtIwxrWpGqGHp5IyLWsWFS3UW7pOf3AWvfkpYSRdxOSlYuJPz8j8g== +cssnano-preset-simple@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/cssnano-preset-simple/-/cssnano-preset-simple-1.2.1.tgz#8976013114b1fc4718253d30f21aaed1780fb80e" + integrity sha512-B2KahOIFTV6dw5Ioy9jHshTh/vAYNnUB2enyWRgnAEg3oJBjI/035ExpePaMqS2SwpbH7gCgvQqwpMBH6hTJSw== dependencies: caniuse-lite "^1.0.30001093" postcss "^7.0.32" -cssnano-simple@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/cssnano-simple/-/cssnano-simple-1.0.5.tgz#66ee528f3a4e60754e2625ea9f51ac315f5f0a92" - integrity sha512-NJjx2Er1C3pa75v1GwMKm0w6xAp1GsW2Ql1As4CWPNFxTgYFN5e8wblYeHfna13sANAhyIdSIPqKJjBO4CU5Eg== - dependencies: - cssnano-preset-simple "1.1.4" - postcss "^7.0.32" - -cssnano-simple@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/cssnano-simple/-/cssnano-simple-1.2.0.tgz#b8cc5f52c2a52e6513b4636d0da165ec9d48d327" - integrity sha512-pton9cZ70/wOCWMAbEGHO1ACsW1KggTB6Ikj7k71uOEsz6SfByH++86+WAmXjRSc9q/g9gxkpFP9bDX9vRotdA== +cssnano-simple@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/cssnano-simple/-/cssnano-simple-1.2.1.tgz#6de5d9dd75774bc8f31767573410a952c7dd8a12" + integrity sha512-9vOyjw8Dj/T12kIOnXPZ5VnEIo6F3YMaIn0wqJXmn277R58cWpI3AvtdlCBtohX7VAUNYcyk2d0dKcXXkb5I6Q== dependencies: - cssnano-preset-simple "1.2.0" + cssnano-preset-simple "1.2.1" postcss "^7.0.32" csstype@^3.0.2: @@ -3975,6 +2855,20 @@ decompress-response@^3.2.0, decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" +decompress-response@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" + integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== + dependencies: + mimic-response "^2.0.0" + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" @@ -4110,6 +3004,16 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + des.js@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" @@ -4118,6 +3022,11 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + detect-node@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" @@ -4199,21 +3108,30 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-serializer@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.0.1.tgz#79695eb49af3cd8abc8d93a73da382deb1ca0795" - integrity sha512-1Aj1Qy3YLbdslkI75QEOfdp9TkQ3o8LRISAzxOibjBs/xWwr1WxZFOQphFkZuepHFGo+kB8e5FVJSS0faAJ4Rw== +dom-helpers@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b" + integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + +dom-serializer@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.1.0.tgz#5f7c828f1bfc44887dc2a315ab5c45691d544b58" + integrity sha512-ox7bvGXt2n+uLWtCRLybYx60IrOlWL/aCebWJk1T0d4m3y2tzf4U3ij9wBMUb6YJZpz06HCCYuyCDveE2xXmzQ== dependencies: domelementtype "^2.0.1" domhandler "^3.0.0" entities "^2.0.0" -dom-serializer@^0.2.1: - version "0.2.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" - integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== +dom-serializer@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1" + integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA== dependencies: domelementtype "^2.0.1" + domhandler "^4.0.0" entities "^2.0.0" domain-browser@^1.1.1: @@ -4226,21 +3144,49 @@ domelementtype@^2.0.1: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== -domhandler@3.0.0, domhandler@^3.0.0: +domelementtype@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" + integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== + +domhandler@3.3.0, domhandler@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.3.0.tgz#6db7ea46e4617eb15cf875df68b2b8524ce0037a" + integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA== + dependencies: + domelementtype "^2.0.1" + +domhandler@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.0.0.tgz#51cd13efca31da95bbb0c5bee3a48300e333b3e9" integrity sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw== dependencies: domelementtype "^2.0.1" -domutils@2.1.0, domutils@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.1.0.tgz#7ade3201af43703fde154952e3a868eb4b635f16" - integrity sha512-CD9M0Dm1iaHfQ1R/TI+z3/JWp/pgub0j4jIQKH89ARR4ATAV2nbaOQS5XxU9maJP5jHaPdDDQSEHuE2UmpUTKg== +domhandler@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e" + integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA== + dependencies: + domelementtype "^2.1.0" + +domutils@2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.2.tgz#7ee5be261944e1ad487d9aa0616720010123922b" + integrity sha512-NKbgaM8ZJOecTZsIzW5gSuplsX2IWW2mIK7xVr8hTQF2v1CJWTmLZ1HOCh5sH+IzVPAGE5IucooOkvwBRAdowA== dependencies: - dom-serializer "^0.2.1" + dom-serializer "^1.0.1" domelementtype "^2.0.1" - domhandler "^3.0.0" + domhandler "^3.3.0" + +domutils@^2.4.2: + version "2.4.4" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3" + integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.0.1" + domhandler "^4.0.0" dot-prop@^4.1.0: version "4.2.0" @@ -4434,15 +3380,10 @@ electron-store@^5.1.1: conf "^6.2.1" type-fest "^0.7.1" -electron-to-chromium@^1.3.413: - version "1.3.451" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.451.tgz#0c075af3e2f06d706670bde0279432802ca8c83f" - integrity sha512-2fvco0F2bBIgqzO8GRP0Jt/91pdrf9KfZ5FsmkYkjERmIJG585cFeFZV4+CO6oTmU3HmCTgfcZuEa7kW8VUh3A== - -electron-to-chromium@^1.3.488, electron-to-chromium@^1.3.571: - version "1.3.582" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.582.tgz#1adfac5affce84d85b3d7b3dfbc4ade293a6ffc4" - integrity sha512-0nCJ7cSqnkMC+kUuPs0YgklFHraWGl/xHqtZWWtOeVtyi+YqkoAOMGuZQad43DscXCQI/yizcTa3u6B5r+BLww== +electron-to-chromium@^1.3.585: + version "1.3.633" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.633.tgz#16dd5aec9de03894e8d14a1db4cda8a369b9b7fe" + integrity sha512-bsVCsONiVX1abkWdH7KtpuDAhsQ3N3bjPYhROSAXE78roJKet0Y5wznA14JE9pzbwSZmSMAW6KiKYf1RvbTJkA== electron-updater@^4.3.1: version "4.3.1" @@ -4555,7 +3496,7 @@ encoding@^0.1.12: dependencies: iconv-lite "~0.4.13" -end-of-stream@^1.0.0, end-of-stream@^1.1.0: +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -4695,7 +3636,7 @@ es6-symbol@^3.1.1, es6-symbol@~3.1.3: d "^1.0.1" ext "^1.1.2" -escalade@^3.0.1, escalade@^3.1.0: +escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== @@ -5088,6 +4029,11 @@ esutils@^2.0.2, esutils@^2.0.3: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +etag@1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" @@ -5210,6 +4156,11 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + ext-list@^2.0.0: version "2.2.2" resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37" @@ -5647,13 +4598,6 @@ fs-extra@^9.0.1: jsonfile "^6.0.1" universalify "^1.0.0" -fs-minipass@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - dependencies: - minipass "^3.0.0" - fs-write-stream-atomic@^1.0.8: version "1.0.10" resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" @@ -5692,6 +4636,20 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + get-caller-file@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -5773,6 +4731,11 @@ gifsicle@^5.0.0: execa "^1.0.0" logalot "^2.0.0" +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= + glob-parent@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" @@ -6026,6 +4989,11 @@ has-to-string-tag-x@^1.2.0: dependencies: has-symbol-support-x "^1.4.1" +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -6112,14 +5080,14 @@ hosted-git-info@^3.0.4: dependencies: lru-cache "^5.1.1" -htmlparser2@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" - integrity sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q== +htmlparser2@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-5.0.1.tgz#7daa6fc3e35d6107ac95a4fc08781f091664f6e7" + integrity sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ== dependencies: domelementtype "^2.0.1" - domhandler "^3.0.0" - domutils "^2.0.0" + domhandler "^3.3.0" + domutils "^2.4.2" entities "^2.0.0" http-cache-semantics@3.8.1: @@ -6132,6 +5100,17 @@ http-cache-semantics@^4.0.0: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== +http-errors@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -6183,7 +5162,7 @@ husky@^4.2.5: slash "^3.0.0" which-pm-runs "^1.0.0" -iconv-lite@^0.4.24, iconv-lite@~0.4.13: +iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -6204,6 +5183,11 @@ icss-utils@^4.0.0, icss-utils@^4.1.1: dependencies: postcss "^7.0.14" +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ieee754@^1.1.4: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" @@ -6292,7 +5276,7 @@ indexes-of@^1.0.1: resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= -infer-owner@^1.0.3, infer-owner@^1.0.4: +infer-owner@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== @@ -6305,7 +5289,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -6381,13 +5365,6 @@ into-stream@^3.1.0: from2 "^2.1.1" p-is-promise "^1.1.0" -invariant@^2.2.2: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -6435,6 +5412,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-binary-path@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" @@ -6530,6 +5512,13 @@ is-finite@^1.0.0: resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" @@ -6868,11 +5857,6 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= - json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" @@ -6915,7 +5899,7 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.1.0, json5@^2.1.2, json5@^2.1.3: +json5@^2.1.2, json5@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== @@ -6999,7 +5983,7 @@ kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -klona@^2.0.3: +klona@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== @@ -7016,18 +6000,6 @@ lazy-val@^1.0.4: resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.4.tgz#882636a7245c2cfe6e0a4e3ba6c5d68a137e5c65" integrity sha512-u93kb2fPbIrfzBuLjZE+w+fJbUUMhNDXxNmMfaqNgpfQf1CO5ZSe2LfsnBqVAk7i/2NF48OSoRj+Xe2VT+lE8Q== -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - -levenary@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77" - integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ== - dependencies: - leven "^3.1.0" - levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -7043,6 +6015,14 @@ line-column-path@^2.0.0: dependencies: type-fest "^0.4.1" +line-column@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/line-column/-/line-column-1.0.2.tgz#d25af2936b6f4849172b312e4792d1d987bc34a2" + integrity sha1-0lryk2tvSEkXKzEuR5LR2Ye8NKI= + dependencies: + isarray "^1.0.0" + isobject "^2.0.0" + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -7216,7 +6196,7 @@ longest@^1.0.0: resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc= -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -7256,7 +6236,7 @@ lpad-align@^1.0.1: longest "^1.0.0" meow "^3.3.0" -lru-cache@6.0.0, lru-cache@^6.0.0: +lru-cache@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== @@ -7567,6 +6547,16 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== +mimic-response@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" @@ -7598,47 +6588,11 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minipass-collect@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" - integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== - dependencies: - minipass "^3.0.0" - -minipass-flush@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" - integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== - dependencies: - minipass "^3.0.0" - -minipass-pipeline@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.3.tgz#55f7839307d74859d6e8ada9c3ebe72cec216a34" - integrity sha512-cFOknTvng5vqnwOpDsZTWhNll6Jf8o2x+/diplafmxpuIymAjzoOolZG0VvQf3V2HgqzJNhnuKHYp2BqDgz8IQ== - dependencies: - minipass "^3.0.0" - -minipass@^3.0.0, minipass@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" - integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== - dependencies: - yallist "^4.0.0" - -minizlib@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - mississippi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" @@ -7663,12 +6617,10 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.5.3: +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: version "0.5.3" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.3.tgz#5a514b7179259287952881e94410ec5465659f8c" - integrity sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg== - dependencies: - minimist "^1.2.5" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== mkdirp@^0.5.1, mkdirp@^0.5.4: version "0.5.4" @@ -7684,11 +6636,6 @@ mkdirp@^0.5.3: dependencies: minimist "^1.2.5" -mkdirp@^1.0.3, mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - module-alias@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0" @@ -7748,6 +6695,11 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== +nanoid@^3.1.16: + version "3.1.20" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" + integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -7765,6 +6717,11 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + native-url@0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/native-url/-/native-url-0.3.4.tgz#29c943172aed86c63cee62c8c04db7f5756661f8" @@ -7782,7 +6739,7 @@ nearest-normal-aspect-ratio@^1.2.1: resolved "https://registry.yarnpkg.com/nearest-normal-aspect-ratio/-/nearest-normal-aspect-ratio-1.2.1.tgz#64af91f2f2b1b99c2b4fda83f84d0eb4c3354f36" integrity sha512-Yi34fBfbffpLZXBmLBOcleAC1m256K8l/KM7ZvJLP6633cibYxbKjzgaewgZD482waUNfLqqq+DBaR/aG37VHA== -neo-async@2.6.1, neo-async@^2.5.0, neo-async@^2.6.1: +neo-async@^2.5.0, neo-async@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== @@ -7802,71 +6759,59 @@ next-tick@~1.0.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= -next@^9.5.5: - version "9.5.5" - resolved "https://registry.yarnpkg.com/next/-/next-9.5.5.tgz#37a37095e7c877ed6c94ba82e34ab9ed02b4eb33" - integrity sha512-KF4MIdTYeI6YIGODNw27w9HGzCll4CXbUpkP6MNvyoHlpsunx8ybkQHm/hYa7lWMozmsn58LwaXJOhe4bSrI0g== +next@^10.0.4: + version "10.0.4" + resolved "https://registry.yarnpkg.com/next/-/next-10.0.4.tgz#0d256f58a57d6bab7db7e533900c15f322960b4a" + integrity sha512-WXEYr1FuR2cLuWGN8peYGM6ykmbtwaHvrI6RqR2qrTXUNsW+KU5pzIMK5WPcpqP+xOuMhlykOCJvwJH8qU9FZQ== dependencies: - "@ampproject/toolbox-optimizer" "2.6.0" - "@babel/code-frame" "7.10.4" - "@babel/core" "7.7.7" - "@babel/plugin-proposal-class-properties" "7.10.4" - "@babel/plugin-proposal-export-namespace-from" "7.10.4" - "@babel/plugin-proposal-numeric-separator" "7.10.4" - "@babel/plugin-proposal-object-rest-spread" "7.11.0" - "@babel/plugin-syntax-bigint" "7.8.3" - "@babel/plugin-syntax-dynamic-import" "7.8.3" - "@babel/plugin-syntax-jsx" "7.10.4" - "@babel/plugin-transform-modules-commonjs" "7.10.4" - "@babel/plugin-transform-runtime" "7.11.5" - "@babel/preset-env" "7.11.5" - "@babel/preset-modules" "0.1.4" - "@babel/preset-react" "7.10.4" - "@babel/preset-typescript" "7.10.4" - "@babel/runtime" "7.11.2" - "@babel/types" "7.11.5" + "@ampproject/toolbox-optimizer" "2.7.1-alpha.0" + "@babel/runtime" "7.12.5" "@hapi/accept" "5.0.1" - "@next/env" "9.5.5" - "@next/polyfill-module" "9.5.5" - "@next/react-dev-overlay" "9.5.5" - "@next/react-refresh-utils" "9.5.5" + "@next/env" "10.0.4" + "@next/polyfill-module" "10.0.4" + "@next/react-dev-overlay" "10.0.4" + "@next/react-refresh-utils" "10.0.4" + "@opentelemetry/api" "0.14.0" ast-types "0.13.2" babel-plugin-transform-define "2.0.0" babel-plugin-transform-react-remove-prop-types "0.4.24" - browserslist "4.13.0" + browserslist "4.14.6" buffer "5.6.0" - cacache "15.0.5" caniuse-lite "^1.0.30001113" - chokidar "2.1.8" + chalk "2.4.2" + chokidar "3.4.3" crypto-browserify "3.12.0" css-loader "4.3.0" - cssnano-simple "1.2.0" + cssnano-simple "1.2.1" + etag "1.8.1" find-cache-dir "3.3.1" jest-worker "24.9.0" loader-utils "2.0.0" - mkdirp "0.5.3" native-url "0.3.4" - neo-async "2.6.1" - node-html-parser "^1.2.19" + node-fetch "2.6.1" + node-html-parser "1.4.9" + p-limit "3.1.0" path-browserify "1.0.1" pnp-webpack-plugin "1.6.4" - postcss "7.0.32" + postcss "8.1.7" process "0.11.10" prop-types "15.7.2" + raw-body "2.4.1" react-is "16.13.1" react-refresh "0.8.3" - resolve-url-loader "3.1.1" - sass-loader "10.0.2" + resolve-url-loader "3.1.2" + sass-loader "10.0.5" schema-utils "2.7.1" stream-browserify "3.0.0" style-loader "1.2.1" - styled-jsx "3.3.0" - use-subscription "1.4.1" + styled-jsx "3.3.2" + use-subscription "1.5.1" vm-browserify "1.1.2" watchpack "2.0.0-beta.13" - web-vitals "0.2.4" webpack "4.44.1" webpack-sources "1.4.3" + optionalDependencies: + sharp "0.26.2" nice-try@^1.0.4: version "1.0.5" @@ -7884,20 +6829,27 @@ nise@^4.0.1: just-extend "^4.0.2" path-to-regexp "^1.7.0" -node-fetch@2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" - integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== +node-abi@^2.7.0: + version "2.19.3" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.19.3.tgz#252f5dcab12dad1b5503b2d27eddd4733930282d" + integrity sha512-9xZrlyfvKhWme2EXFKQhZRp1yNWT/uI1luYPr3sFl+H4keYY4xR+1jO7mvTTijIsHf1M+QDe9uWuKeEpLInIlg== + dependencies: + semver "^5.4.1" + +node-addon-api@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239" + integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw== node-fetch@2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -node-html-parser@^1.2.19: - version "1.3.1" - resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-1.3.1.tgz#f58e55a029b51deae8924312be9817ef5cf5ed96" - integrity sha512-AwYVI6GyEKj9NGoyMfSx4j5l7Axf7obQgLWGxtasLjED6RggTTQoq5ZRzjwSUfgSZ+Mv8Nzbi3pID0gFGqNUsA== +node-html-parser@1.4.9: + version "1.4.9" + resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-1.4.9.tgz#3c8f6cac46479fae5800725edb532e9ae8fd816c" + integrity sha512-UVcirFD1Bn0O+TSmloHeHqZZCxHjvtIeGdVdGMhyZ8/PWlEiZaZ5iJzR189yKZr8p0FXN58BUeC7RHRkf/KYGw== dependencies: he "1.2.0" @@ -7938,15 +6890,15 @@ node-mac-app-icon@^1.4.0: electron-util "^0.4.1" execa "^0.8.0" -node-releases@^1.1.53: - version "1.1.53" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.53.tgz#2d821bfa499ed7c5dffc5e2f28c88e78a08ee3f4" - integrity sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ== +node-releases@^1.1.65: + version "1.1.67" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12" + integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg== -node-releases@^1.1.58, node-releases@^1.1.61: - version "1.1.63" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.63.tgz#db6dbb388544c31e888216304e8fd170efee3ff5" - integrity sha512-ukW3iCfQaoxJkSPN+iK7KznTeqDGVJatAEuXsJERYHa9tn/KaT5lBdIyxQjLEVTzSkyjJEuQ17/vaEjrOauDkg== +noop-logger@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" + integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI= normalize-html-whitespace@1.0.0: version "1.0.0" @@ -8018,6 +6970,21 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.0.0" +npmlog@^4.0.1, npmlog@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" @@ -8057,11 +7024,6 @@ object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object-path@0.11.4: - version "0.11.4" - resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949" - integrity sha1-NwrnUvvzfePqcKhhwju6iRVpGUk= - object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" @@ -8286,6 +7248,13 @@ p-is-promise@^1.1.0: resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= +p-limit@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -8783,6 +7752,16 @@ postcss@7.0.32: source-map "^0.6.1" supports-color "^6.1.0" +postcss@8.1.7: + version "8.1.7" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.1.7.tgz#ff6a82691bd861f3354fd9b17b2332f88171233f" + integrity sha512-llCQW1Pz4MOPwbZLmOddGM9eIJ8Bh7SZ2Oj5sxZva77uVaotYDsYTch1WBTNu7fUY0fpWp0fdt7uW40D4sRiiQ== + dependencies: + colorette "^1.2.1" + line-column "^1.0.2" + nanoid "^3.1.16" + source-map "^0.6.1" + postcss@^7.0.14, postcss@^7.0.26, postcss@^7.0.5, postcss@^7.0.6: version "7.0.27" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9" @@ -8801,6 +7780,27 @@ postcss@^7.0.32: source-map "^0.6.1" supports-color "^6.1.0" +prebuild-install@^5.3.5: + version "5.3.6" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.6.tgz#7c225568d864c71d89d07f8796042733a3f54291" + integrity sha512-s8Aai8++QQGi4sSbs/M1Qku62PFK49Jm1CbgXklGz4nmHveDq0wzJkg7Na5QbnO1uNH8K7iqx2EQ/mV0MZEmOg== + dependencies: + detect-libc "^1.0.3" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^2.7.0" + noop-logger "^0.1.1" + npmlog "^4.0.1" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^3.0.3" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + which-pm-runs "^1.0.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -8828,6 +7828,11 @@ prettier@2.0.4: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.4.tgz#2d1bae173e355996ee355ec9830a7a1ee05457ef" integrity sha512-SVJIQ51spzFDvh4fIbCLvciiDMCrRhlN3mbZvv/+ycjvmF5E73bKdGfU8QDLNmjYJf+lsGnDBC4UUnvTe5OO0w== +pretty-bytes@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.5.0.tgz#0cecda50a74a941589498011cf23275aa82b339e" + integrity sha512-p+T744ZyjjiaFlMUZZv6YPC5JrkNj8maRmPaQCWFJFplUAzpIUTRaTcS+7wmZtUoFXHtESJb23ISliaWyz3SHA== + pretty-ms@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-7.0.0.tgz#45781273110caf35f55cab21a8a9bd403a233dc0" @@ -8835,11 +7840,6 @@ pretty-ms@^7.0.0: dependencies: parse-ms "^2.1.0" -private@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== - process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -9006,7 +8006,17 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" -rc@^1.2.8: +raw-body@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" + integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== + dependencies: + bytes "3.1.0" + http-errors "1.7.3" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@^1.2.7, rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -9016,6 +8026,14 @@ rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-css-transition-replace@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/react-css-transition-replace/-/react-css-transition-replace-4.0.4.tgz#08dece706f94da23dba49a687dd8860027f13448" + integrity sha512-kAr2Tcnw1QAb/r+QfkK1NkuDiTKkrJlyPziDWq7WmlRwKAmgGCWe9RC9Nv+pPVWXTUxqqIskfw+Qh+T/4wE9sw== + dependencies: + dom-helpers "^5.2.0" + prop-types "^15.7.2" + react-dom@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" @@ -9118,7 +8136,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -9131,7 +8149,7 @@ read-pkg@^5.2.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.5.0, readable-stream@^3.6.0: +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -9179,31 +8197,11 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -regenerate-unicode-properties@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" - integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA== - dependencies: - regenerate "^1.4.0" - -regenerate@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" - integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== - regenerator-runtime@^0.13.4: version "0.13.5" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== -regenerator-transform@^0.14.2: - version "0.14.4" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.4.tgz#5266857896518d1616a78a0479337a30ea974cc7" - integrity sha512-EaJaKPBI9GvKpvUz2mz4fhx7WPgvwRLY9v3hlNHWmAuJHI13T4nwKnNvm5RWJzEdnI5g5UwtOww+S8IdoUC2bw== - dependencies: - "@babel/runtime" "^7.8.4" - private "^0.1.8" - regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" @@ -9212,10 +8210,10 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regex-parser@2.2.10: - version "2.2.10" - resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.10.tgz#9e66a8f73d89a107616e63b39d4deddfee912b37" - integrity sha512-8t6074A68gHfU8Neftl0Le6KTDwfGAj7IyjPIMSfikI2wJUTHDMaIq42bUsfVnj8mhx0R+45rdUXHGpN164avA== +regex-parser@^2.2.11: + version "2.2.11" + resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" + integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== regexp-tree@^0.1.21, regexp-tree@~0.1.1: version "0.1.21" @@ -9235,30 +8233,6 @@ regexpp@^3.0.0, regexpp@^3.1.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== -regexpu-core@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938" - integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ== - dependencies: - regenerate "^1.4.0" - regenerate-unicode-properties "^8.2.0" - regjsgen "^0.5.1" - regjsparser "^0.6.4" - unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.2.0" - -regexpu-core@^4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" - integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== - dependencies: - regenerate "^1.4.0" - regenerate-unicode-properties "^8.2.0" - regjsgen "^0.5.1" - regjsparser "^0.6.4" - unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.2.0" - registry-auth-token@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.1.1.tgz#40a33be1e82539460f94328b0f7f0f84c16d9479" @@ -9273,18 +8247,6 @@ registry-url@^5.0.0: dependencies: rc "^1.2.8" -regjsgen@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c" - integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg== - -regjsparser@^0.6.4: - version "0.6.4" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272" - integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw== - dependencies: - jsesc "~0.5.0" - remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -9365,12 +8327,12 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve-url-loader@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-3.1.1.tgz#28931895fa1eab9be0647d3b2958c100ae3c0bf0" - integrity sha512-K1N5xUjj7v0l2j/3Sgs5b8CjrrgtC70SmdCuZiJ8tSyb5J+uk3FoeZ4b7yTnH6j7ngI+Bc5bldHJIa8hYdu2gQ== +resolve-url-loader@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-3.1.2.tgz#235e2c28e22e3e432ba7a5d4e305c59a58edfc08" + integrity sha512-QEb4A76c8Mi7I3xNKXlRKQSlLBwjUV/ULFMP+G7n3/7tJZ8MG5wsZ3ucxP1Jz8Vevn6fnJsxDx9cIls+utGzPQ== dependencies: - adjust-sourcemap-loader "2.0.0" + adjust-sourcemap-loader "3.0.0" camelcase "5.3.1" compose-function "3.0.3" convert-source-map "1.7.0" @@ -9386,7 +8348,7 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.15.1, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.8.1: +resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.15.1, resolve@^1.17.0: version "1.17.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== @@ -9453,7 +8415,7 @@ rimraf@^2.5.4, rimraf@^2.6.3: dependencies: glob "^7.1.3" -rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -9542,15 +8504,15 @@ sanitize-filename@^1.6.2, sanitize-filename@^1.6.3: dependencies: truncate-utf8-bytes "^1.0.0" -sass-loader@10.0.2: - version "10.0.2" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.0.2.tgz#c7b73010848b264792dd45372eea0b87cba4401e" - integrity sha512-wV6NDUVB8/iEYMalV/+139+vl2LaRFlZGEd5/xmdcdzQcgmis+npyco6NsDTVOlNA3y2NV9Gcz+vHyFMIT+ffg== +sass-loader@10.0.5: + version "10.0.5" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.0.5.tgz#f53505b5ddbedf43797470ceb34066ded82bb769" + integrity sha512-2LqoNPtKkZq/XbXNQ4C64GFEleSEHKv6NPSI+bMC/l+jpEXGJhiRYkAQToO24MR7NU4JRY2RpLpJ/gjo2Uf13w== dependencies: - klona "^2.0.3" + klona "^2.0.4" loader-utils "^2.0.0" neo-async "^2.6.2" - schema-utils "^2.7.1" + schema-utils "^3.0.0" semver "^7.3.2" sax@^1.2.4: @@ -9592,6 +8554,15 @@ schema-utils@^2.6.6: ajv "^6.12.0" ajv-keywords "^3.4.1" +schema-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef" + integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA== + dependencies: + "@types/json-schema" "^7.0.6" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + seek-bzip@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.5.tgz#cfe917cb3d274bcffac792758af53173eb1fabdc" @@ -9623,16 +8594,11 @@ semver-truncate@^1.1.2: dependencies: semver "^5.3.0" -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== - semver@^6.0.0, semver@^6.1.0, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -9660,7 +8626,7 @@ serialize-javascript@^2.1.2: resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== -set-blocking@^2.0.0: +set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= @@ -9680,6 +8646,11 @@ setimmediate@^1.0.4: resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + sha.js@^2.4.0, sha.js@^2.4.8: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" @@ -9688,6 +8659,21 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" +sharp@0.26.2: + version "0.26.2" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.26.2.tgz#3d5777d246ae32890afe82a783c1cbb98456a88c" + integrity sha512-bGBPCxRAvdK9bX5HokqEYma4j/Q5+w8Nrmb2/sfgQCLEUx/HblcpmOfp59obL3+knIKnOhyKmDb4tEOhvFlp6Q== + dependencies: + color "^3.1.2" + detect-libc "^1.0.3" + node-addon-api "^3.0.2" + npmlog "^4.1.2" + prebuild-install "^5.3.5" + semver "^7.3.2" + simple-get "^4.0.0" + tar-fs "^2.1.0" + tunnel-agent "^0.6.0" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -9730,6 +8716,36 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" + integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== + dependencies: + decompress-response "^4.2.0" + once "^1.3.1" + simple-concat "^1.0.0" + +simple-get@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.0.tgz#73fa628278d21de83dadd5512d2cc1f4872bd675" + integrity sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + sinon@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.0.2.tgz#b9017e24633f4b1c98dfb6e784a5f0509f5fd85d" @@ -9838,7 +8854,7 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.19, source-map-support@~0.5.12: +source-map-support@^0.5.19, source-map-support@~0.5.12, source-map-support@~0.5.19: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== @@ -9856,7 +8872,7 @@ source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@0.7.3: +source-map@0.7.3, source-map@~0.7.2: version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== @@ -9947,13 +8963,6 @@ ssri@^6.0.1: dependencies: figgy-pudding "^3.5.1" -ssri@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.0.tgz#79ca74e21f8ceaeddfcb4b90143c458b8d988808" - integrity sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA== - dependencies: - minipass "^3.1.1" - stack-utils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.2.tgz#5cf48b4557becb4638d0bc4f21d23f5d19586593" @@ -9981,6 +8990,11 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" +"statuses@>= 1.5.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + stream-browserify@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" @@ -10036,7 +9050,16 @@ string-math@^1.2.1: resolved "https://registry.yarnpkg.com/string-math/-/string-math-1.2.1.tgz#3abe7ce4225ee14f8ddf10037e7a44ab2aa79c1c" integrity sha512-r5ouWjy+qzWW4UsKSa8Annd5w6n1Z+7LyN3NySTW5UDBG9oeM3aq2+IL7yy+XkCpQIaEK8AcDyIdwI2B2td5Aw== -string-width@^2.1.0: +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2", string-width@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -10129,7 +9152,7 @@ strip-ansi@6.0.0, strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-ansi@^3.0.0: +strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= @@ -10218,10 +9241,10 @@ style-loader@1.2.1: loader-utils "^2.0.0" schema-utils "^2.6.6" -styled-jsx@3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-3.3.0.tgz#32335c1a3ecfc923ba4f9c056eeb3d4699006b09" - integrity sha512-sh8BI5eGKyJlwL4kNXHjb27/a/GJV8wP4ElRIkRXrGW3sHKOsY9Pa1VZRNxyvf3+lisdPwizD9JDkzVO9uGwZw== +styled-jsx@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-3.3.2.tgz#2474601a26670a6049fb4d3f94bd91695b3ce018" + integrity sha512-daAkGd5mqhbBhLd6jYAjYBa9LpxYCzsgo/f6qzPdFxVB8yoGbhxvzQgkC0pfmCVvW3JuAEBn0UzFLBfkHVZG1g== dependencies: "@babel/types" "7.8.3" babel-plugin-syntax-jsx "6.18.0" @@ -10314,6 +9337,16 @@ tapable@^1.0.0, tapable@^1.1.3: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== +tar-fs@^2.0.0, tar-fs@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + tar-stream@^1.5.2: version "1.6.2" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" @@ -10327,17 +9360,16 @@ tar-stream@^1.5.2: to-buffer "^1.1.1" xtend "^4.0.0" -tar@^6.0.2: - version "6.0.5" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f" - integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^3.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" temp-dir@^1.0.0: version "1.0.0" @@ -10403,14 +9435,14 @@ terser-webpack-plugin@^1.4.3: webpack-sources "^1.4.0" worker-farm "^1.7.0" -terser@4.8.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" - integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== +terser@5.5.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.5.1.tgz#540caa25139d6f496fdea056e414284886fb2289" + integrity sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ== dependencies: commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" + source-map "~0.7.2" + source-map-support "~0.5.19" terser@^4.1.2: version "4.6.9" @@ -10540,6 +9572,11 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + tough-cookie@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" @@ -10744,29 +9781,6 @@ unc-path-regex@^0.1.2: resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= -unicode-canonical-property-names-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" - integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== - -unicode-match-property-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" - integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== - dependencies: - unicode-canonical-property-names-ecmascript "^1.0.4" - unicode-property-aliases-ecmascript "^1.0.4" - -unicode-match-property-value-ecmascript@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531" - integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== - -unicode-property-aliases-ecmascript@^1.0.4: - version "1.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" - integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== - union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -10820,6 +9834,11 @@ universalify@^1.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" @@ -10923,10 +9942,10 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" -use-subscription@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.4.1.tgz#edcbcc220f1adb2dd4fa0b2f61b6cc308e620069" - integrity sha512-7+IIwDG/4JICrWHL/Q/ZPK5yozEnvRm6vHImu0LKwQlmWGKeiF7mbAenLlK/cTNXrTtXHU/SFASQHzB6+oSJMQ== +use-subscription@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1" + integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA== dependencies: object-assign "^4.1.1" @@ -11039,11 +10058,6 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" -web-vitals@0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-0.2.4.tgz#ec3df43c834a207fd7cdefd732b2987896e08511" - integrity sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg== - webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" @@ -11124,6 +10138,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + widest-line@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" @@ -11343,3 +10364,8 @@ yauzl@^2.10.0, yauzl@^2.4.2: dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From a2d0d65e8646d24ed2864ad09796c7b98e676678 Mon Sep 17 00:00:00 2001 From: George Karagkiaouris Date: Sun, 14 Feb 2021 15:39:58 -0500 Subject: [PATCH 04/14] Finish the main process TS rewrite --- .circleci/config.yml | 2 +- main/{common/aperture.js => aperture.ts} | 131 +- .../accelerator-validator.ts} | 19 +- main/common/analytics.ts | 2 +- main/common/logger.js | 35 - main/common/notifications.js | 16 - main/common/plugins.js | 309 ---- main/common/settings.ts | 61 +- ...m-permissions.js => system-permissions.ts} | 21 +- main/common/types/base.ts | 20 + main/common/types/conversion-options.ts | 14 +- main/common/types/index.ts | 1 + main/common/types/remote-states.ts | 18 +- main/common/types/window-states.ts | 8 + main/common/{windows.js => windows.ts} | 56 +- main/conversion.ts | 223 ++- main/convert.js | 391 ----- main/converters/h264.ts | 102 +- main/converters/index.ts | 27 +- main/converters/process.ts | 26 +- main/converters/utils.ts | 6 +- main/editor.js | 167 -- main/export-list.js | 295 ---- main/export-options.js | 171 --- main/export.ts | 130 +- main/export1.js | 157 -- ...accelerators.js => global-accelerators.ts} | 38 +- main/{index.js => index.ts} | 80 +- main/menus.js | 277 ---- main/menus/application.ts | 70 + main/menus/cog.ts | 101 ++ main/menus/common.ts | 97 ++ main/menus/utils.ts | 31 + main/plugin.js | 157 -- ...-plugin.js => copy-to-clipboard-plugin.ts} | 11 +- main/plugins/built-in/open-with-plugin.ts | 7 +- main/plugins/built-in/save-file-plugin.ts | 16 +- main/plugins/config.ts | 21 +- main/plugins/index.ts | 111 +- main/plugins/plugin.ts | 70 +- main/plugins/service-context.ts | 89 +- main/plugins/service.ts | 17 +- ...ording-history.js => recording-history.ts} | 110 +- main/remote-states/conversion.ts | 21 +- main/remote-states/editor-options.ts | 17 +- main/remote-states/setup-remote-state.ts | 8 +- main/remote-states/use-remote-state.ts | 63 - main/remote-states/utils.ts | 12 +- main/service-context.js | 186 --- main/{tray.js => tray.ts} | 49 +- main/utils/ajv.ts | 21 +- .../{deep-linking.js => deep-linking.ts} | 22 +- main/utils/{devices.js => devices.ts} | 38 +- main/utils/{dock.js => dock.ts} | 12 +- main/utils/{errors.js => errors.ts} | 108 +- main/utils/formats.ts | 4 +- main/utils/{icon.js => icon.ts} | 7 +- main/utils/image-preview.ts | 59 + main/utils/macos-version.js | 3 - main/utils/notifications.ts | 12 +- main/utils/open-files.js | 25 - main/utils/open-files.ts | 19 + main/utils/plugin-config.js | 67 - main/utils/routes.js | 18 - main/utils/routes.ts | 14 + main/utils/sentry.ts | 2 +- main/utils/timestamped-name.ts | 2 +- main/video.ts | 100 +- main/{config.js => windows/config.ts} | 22 +- main/{cropper.js => windows/cropper.ts} | 91 +- main/{dialog.js => windows/dialog.ts} | 35 +- main/windows/editor.ts | 149 ++ main/{exports.js => windows/exports.ts} | 20 +- main/{ => windows}/kap-window.ts | 138 +- main/windows/load.ts | 6 + main/windows/manager.ts | 72 + .../preferences.ts} | 33 +- package.json | 114 +- plz.png | Bin 0 -> 809 bytes .../action-bar/controls/advanced.js | 12 +- .../components/action-bar/controls/main.js | 4 +- renderer/components/action-bar/index.js | 2 +- .../components/action-bar/record-button.js | 6 +- renderer/components/cropper/cursor.js | 2 +- renderer/components/cropper/handles.js | 4 +- renderer/components/cropper/overlay.js | 2 +- renderer/components/dialog/actions.js | 2 +- renderer/components/dialog/icon.js | 4 +- renderer/components/editor/controls/left.tsx | 8 +- .../components/editor/controls/play-bar.tsx | 406 ++--- .../components/editor/controls/preview.tsx | 96 +- renderer/components/editor/controls/right.tsx | 4 +- .../editor/conversion/conversion-details.tsx | 7 +- .../components/editor/conversion/index.tsx | 16 +- .../editor/conversion/title-bar.tsx | 14 +- .../editor/conversion/video-preview.tsx | 22 +- renderer/components/editor/editor-preview.tsx | 6 +- renderer/components/editor/index.tsx | 105 +- .../components/editor/options-container.tsx | 22 +- renderer/components/editor/options/index.tsx | 6 +- renderer/components/editor/options/left.tsx | 160 +- renderer/components/editor/options/right.tsx | 209 +-- renderer/components/editor/options/select.tsx | 35 +- renderer/components/editor/options/slider.tsx | 271 ++-- .../editor/video-controls-container.tsx | 13 +- renderer/components/editor/video-player.tsx | 17 +- .../editor/video-time-container.tsx | 7 +- renderer/components/editor/video.tsx | 48 +- renderer/components/exports/export.js | 8 +- renderer/components/icon-menu.tsx | 14 +- renderer/components/keyboard-number-input.js | 7 +- .../preferences/categories/general.js | 8 +- .../preferences/categories/index.js | 2 +- .../preferences/categories/plugins/index.js | 2 +- .../preferences/item/color-picker.js | 4 +- renderer/components/preferences/item/index.js | 4 +- .../components/preferences/item/select.js | 6 +- renderer/components/preferences/navigation.js | 2 +- .../components/preferences/shortcut-input.js | 6 +- renderer/components/traffic-lights.tsx | 30 +- renderer/containers/action-bar.js | 34 +- renderer/containers/config.js | 16 +- renderer/containers/cropper.js | 59 +- renderer/containers/cursor.js | 6 +- renderer/containers/editor.js | 38 +- renderer/containers/exports.js | 15 +- renderer/containers/preferences.js | 59 +- renderer/containers/video.js | 26 +- renderer/hooks/editor/use-conversion-id.tsx | 6 +- renderer/hooks/editor/use-conversion.tsx | 6 +- renderer/hooks/editor/use-editor-options.tsx | 6 +- .../hooks/editor/use-editor-window-state.tsx | 11 +- renderer/hooks/editor/use-share-plugins.tsx | 10 +- renderer/hooks/editor/use-window-size.tsx | 13 +- renderer/hooks/use-confirmation.tsx | 12 +- renderer/hooks/use-current-window.tsx | 4 +- renderer/hooks/use-remote-state.tsx | 28 +- renderer/hooks/window-state.tsx | 5 +- renderer/next.config.js | 74 - renderer/pages/_app.tsx | 13 +- renderer/pages/config.js | 2 +- renderer/pages/cropper.js | 4 +- renderer/pages/dialog.js | 12 +- renderer/pages/editor.js | 203 --- renderer/pages/{editor2.tsx => editor.tsx} | 4 +- renderer/pages/exports.js | 1 - renderer/pages/preferences.js | 8 +- renderer/tsconfig.eslint.json | 9 + .../utils/combine-unstated-containers.tsx | 16 +- renderer/utils/global-styles.tsx | 12 +- renderer/utils/inputs.js | 2 +- renderer/utils/sentry-error-boundary.tsx | 9 +- renderer/utils/window.ts | 2 +- renderer/vectors/back-plain.tsx | 4 +- renderer/vectors/svg.tsx | 6 +- test.svg | 25 + test/{convert.js => convert.ts} | 143 +- test/helpers/{assertions.js => assertions.ts} | 3 +- test/helpers/mocks.js | 24 - test/helpers/mocks.ts | 37 + test/helpers/video-utils.js | 45 - test/helpers/video-utils.ts | 51 + test/mocks/analytics.js | 6 - test/mocks/analytics.ts | 3 + test/mocks/{dialog.js => dialog.ts} | 30 +- test/mocks/editor.js | 5 - test/mocks/electron-store.js | 23 - test/mocks/electron-store.ts | 59 + test/mocks/electron.js | 23 - test/mocks/electron.ts | 24 + test/mocks/plugins.js | 5 - test/mocks/plugins.ts | 9 + test/mocks/sentry.js | 6 - test/mocks/sentry.ts | 7 + test/mocks/service-context.js | 11 - test/mocks/service-context.ts | 4 + test/mocks/settings.js | 15 - test/mocks/settings.ts | 14 + test/mocks/video.ts | 18 + test/mocks/window-manager.ts | 23 + ...ording-history.js => recording-history.ts} | 87 +- test/tsconfig.json | 10 + testa.svg | 1291 ++++++++++++++++ tsconfig.eslint.json | 9 + tsconfig.json | 1 + yarn.lock | 1347 +++++++++++------ 186 files changed, 5623 insertions(+), 5353 deletions(-) rename main/{common/aperture.js => aperture.ts} (56%) rename main/{utils/accelerator-validator.js => common/accelerator-validator.ts} (82%) delete mode 100644 main/common/logger.js delete mode 100644 main/common/notifications.js delete mode 100644 main/common/plugins.js rename main/common/{system-permissions.js => system-permissions.ts} (73%) create mode 100644 main/common/types/window-states.ts rename main/common/{windows.js => windows.ts} (56%) delete mode 100644 main/convert.js delete mode 100644 main/editor.js delete mode 100644 main/export-list.js delete mode 100644 main/export-options.js delete mode 100644 main/export1.js rename main/{global-accelerators.js => global-accelerators.ts} (64%) rename main/{index.js => index.ts} (55%) delete mode 100644 main/menus.js create mode 100644 main/menus/application.ts create mode 100644 main/menus/cog.ts create mode 100644 main/menus/common.ts create mode 100644 main/menus/utils.ts delete mode 100644 main/plugin.js rename main/plugins/built-in/{copy-to-clipboard-plugin.js => copy-to-clipboard-plugin.ts} (60%) rename main/{recording-history.js => recording-history.ts} (71%) delete mode 100644 main/remote-states/use-remote-state.ts delete mode 100644 main/service-context.js rename main/{tray.js => tray.ts} (58%) rename main/utils/{deep-linking.js => deep-linking.ts} (53%) rename main/utils/{devices.js => devices.ts} (53%) rename main/utils/{dock.js => dock.ts} (61%) rename main/utils/{errors.js => errors.ts} (60%) rename main/utils/{icon.js => icon.ts} (61%) create mode 100644 main/utils/image-preview.ts delete mode 100644 main/utils/macos-version.js delete mode 100644 main/utils/open-files.js create mode 100644 main/utils/open-files.ts delete mode 100644 main/utils/plugin-config.js delete mode 100644 main/utils/routes.js create mode 100644 main/utils/routes.ts rename main/{config.js => windows/config.ts} (73%) rename main/{cropper.js => windows/cropper.ts} (67%) rename main/{dialog.js => windows/dialog.ts} (65%) create mode 100644 main/windows/editor.ts rename main/{exports.js => windows/exports.ts} (71%) rename main/{ => windows}/kap-window.ts (56%) create mode 100644 main/windows/load.ts create mode 100644 main/windows/manager.ts rename main/{preferences.js => windows/preferences.ts} (65%) create mode 100644 plz.png delete mode 100644 renderer/pages/editor.js rename renderer/pages/{editor2.tsx => editor.tsx} (96%) create mode 100644 renderer/tsconfig.eslint.json create mode 100644 test.svg rename test/{convert.js => convert.ts} (63%) rename test/helpers/{assertions.js => assertions.ts} (67%) delete mode 100644 test/helpers/mocks.js create mode 100644 test/helpers/mocks.ts delete mode 100644 test/helpers/video-utils.js create mode 100644 test/helpers/video-utils.ts delete mode 100644 test/mocks/analytics.js create mode 100644 test/mocks/analytics.ts rename test/mocks/{dialog.js => dialog.ts} (51%) delete mode 100644 test/mocks/editor.js delete mode 100644 test/mocks/electron-store.js create mode 100644 test/mocks/electron-store.ts delete mode 100644 test/mocks/electron.js create mode 100644 test/mocks/electron.ts delete mode 100644 test/mocks/plugins.js create mode 100644 test/mocks/plugins.ts delete mode 100644 test/mocks/sentry.js create mode 100644 test/mocks/sentry.ts delete mode 100644 test/mocks/service-context.js create mode 100644 test/mocks/service-context.ts delete mode 100644 test/mocks/settings.js create mode 100644 test/mocks/settings.ts create mode 100644 test/mocks/video.ts create mode 100644 test/mocks/window-manager.ts rename test/{recording-history.js => recording-history.ts} (79%) create mode 100644 test/tsconfig.json create mode 100644 testa.svg create mode 100644 tsconfig.eslint.json diff --git a/.circleci/config.yml b/.circleci/config.yml index 005dfc248..1e388c3f1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ jobs: steps: - checkout - run: yarn - # - run: yarn test + - run: yarn test - run: yarn run dist - run: mv dist/*.dmg dist/Kap.dmg - store_artifacts: diff --git a/main/common/aperture.js b/main/aperture.ts similarity index 56% rename from main/common/aperture.js rename to main/aperture.ts index bca0f44b7..8221402fc 100644 --- a/main/common/aperture.js +++ b/main/aperture.ts @@ -1,63 +1,53 @@ -'use strict'; +import {windowManager} from './windows/manager'; +import {setRecordingTray, disableTray, resetTray} from './tray'; +import {setCropperShortcutAction} from './global-accelerators'; +import {settings} from './common/settings'; +import {track} from './common/analytics'; +import {plugins} from './plugins'; +import {getAudioDevices, getSelectedInputDeviceId} from './utils/devices'; +import {showError} from './utils/errors'; +import {RecordServiceContext, RecordServiceState} from './plugins/service-context'; +import {setCurrentRecording, updatePluginState, stopCurrentRecording} from './recording-history'; +import {Recording} from './video'; +import {ApertureOptions, StartRecordingOptions} from './common/types'; +import {InstalledPlugin} from './plugins/plugin'; +import {RecordService, RecordServiceHook} from './plugins/service'; const createAperture = require('aperture'); - -const {openEditorWindow} = require('../editor'); -const {closePrefsWindow} = require('../preferences'); -const {setRecordingTray, disableTray, resetTray} = require('../tray'); -const {disableCroppers, setRecordingCroppers, closeAllCroppers} = require('../cropper'); -const {setCropperShortcutAction} = require('../global-accelerators'); - -// eslint-disable-next-line no-unused-vars -const {convertToH264} = require('../utils/encoding'); - -const {default: settings, getSelectedInputDeviceId} = require('./settings'); -const {track} = require('./analytics'); -const plugins = require('./plugins'); -const {getAudioDevices} = require('../utils/devices'); -const {showError} = require('../utils/errors'); -const {RecordServiceContext} = require('../service-context'); -const {setCurrentRecording, updatePluginState, stopCurrentRecording} = require('../recording-history'); - const aperture = createAperture(); -const {videoCodecs} = createAperture; - -// eslint-disable-next-line no-unused-vars -const recordHevc = videoCodecs.has('hevc'); -let lastUsedSettings; -let recordingPlugins = []; -const serviceState = new Map(); -let apertureOptions; -let recordingName; -let past; +let recordingPlugins: Array<{plugin: InstalledPlugin; service: RecordService}> = []; +const serviceState = new Map(); +let apertureOptions: ApertureOptions; +let recordingName: string | undefined; +let past: number | undefined; -const setRecordingName = name => { +const setRecordingName = (name: string) => { recordingName = name; }; const serializeEditPluginState = () => { - const result = {}; + const result: Record | undefined>> = {}; for (const {plugin, service} of recordingPlugins) { if (!result[plugin.name]) { result[plugin.name] = {}; } - result[plugin.name][service.title] = serviceState.get(service.title).persistedState; + result[plugin.name][service.title] = serviceState.get(service.title)?.persistedState; } return result; }; -const callPlugins = async method => Promise.all(recordingPlugins.map(async ({plugin, service}) => { +const callPlugins = async (method: RecordServiceHook) => Promise.all(recordingPlugins.map(async ({plugin, service}) => { if (service[method] && typeof service[method] === 'function') { try { - await service[method]( + await service[method]?.( new RecordServiceContext({ + plugin, apertureOptions, - state: serviceState.get(service.title), - config: plugin.config, + state: serviceState.get(service.title) ?? {}, setRecordingName }) ); @@ -68,7 +58,7 @@ const callPlugins = async method => Promise.all(recordingPlugins.map(async ({plu })); const cleanup = async () => { - closeAllCroppers(); + windowManager.cropper?.close(); resetTray(); await callPlugins('didStopRecording'); @@ -77,7 +67,7 @@ const cleanup = async () => { setCropperShortcutAction(); }; -const startRecording = async options => { +export const startRecording = async (options: StartRecordingOptions) => { if (past) { return; } @@ -85,9 +75,9 @@ const startRecording = async options => { past = Date.now(); recordingName = undefined; - closePrefsWindow(); + windowManager.preferences?.close(); + windowManager.cropper?.disable(); disableTray(); - disableCroppers(); const {cropperBounds, screenBounds, displayId} = options; @@ -108,11 +98,7 @@ const startRecording = async options => { screenId: displayId }; - lastUsedSettings = { - recordedFps: apertureOptions.fps - }; - - if (recordAudio === true) { + if (recordAudio) { // In case for some reason the default audio device is not set // use the first available device for recording const audioInputDeviceId = getSelectedInputDeviceId(); @@ -120,7 +106,7 @@ const startRecording = async options => { apertureOptions.audioDeviceId = audioInputDeviceId; } else { const [defaultAudioDevice] = await getAudioDevices(); - apertureOptions.audioDeviceId = defaultAudioDevice && defaultAudioDevice.id; + apertureOptions.audioDeviceId = defaultAudioDevice?.id; } } @@ -132,12 +118,15 @@ const startRecording = async options => { console.log(`Collected settings after ${(Date.now() - past) / 1000}s`); recordingPlugins = plugins - .getRecordingPlugins() + .recordingPlugins .flatMap( - plugin => plugin.recordServicesWithStatus - // Make sure service is valid and enabled - .filter(({title, isEnabled}) => isEnabled && plugin.config.validServices.includes(title)) - .map(service => ({plugin, service})) + plugin => { + const validServices = plugin.config.validServices; + return plugin.recordServicesWithStatus + // Make sure service is valid and enabled + .filter(({title, isEnabled}) => isEnabled && validServices.includes(title)) + .map(service => ({plugin, service})); + } ); for (const {service, plugin} of recordingPlugins) { @@ -154,12 +143,12 @@ const startRecording = async options => { filePath, name: recordingName, apertureOptions, - editPlugins: serializeEditPluginState() + plugins: serializeEditPluginState() }); } catch (error) { track('recording/stopped/error'); - showError(error, {title: 'Recording error'}); - past = null; + showError(error, {title: 'Recording error', plugin: undefined}); + past = undefined; cleanup(); return; } @@ -172,18 +161,18 @@ const startRecording = async options => { } console.log(`Started recording after ${startTime}s`); - setRecordingCroppers(); + windowManager.cropper?.setRecording(); setRecordingTray(stopRecording); setCropperShortcutAction(stopRecording); past = Date.now(); // Track aperture errors after recording has started, to avoid kap freezing if something goes wrong - aperture.recorder.catch(error => { + aperture.recorder.catch((error: any) => { // Make sure it doesn't catch the error of ending the recording if (past) { track('recording/stopped/error'); - showError(error, {title: 'Recording error'}); - past = null; + showError(error, {title: 'Recording error', plugin: undefined}); + past = undefined; cleanup(); } }); @@ -192,14 +181,14 @@ const startRecording = async options => { updatePluginState(serializeEditPluginState()); }; -const stopRecording = async () => { +export const stopRecording = async () => { // Ensure we only stop recording once if (!past) { return; } console.log(`Stopped recording after ${(Date.now() - past) / 1000}s`); - past = null; + past = undefined; let filePath; @@ -207,31 +196,23 @@ const stopRecording = async () => { filePath = await aperture.stopRecording(); } catch (error) { track('recording/stopped/error'); - showError(error, {title: 'Recording error'}); + showError(error, {title: 'Recording error', plugin: undefined}); cleanup(); return; } - const {recordedFps} = lastUsedSettings; - try { cleanup(); } finally { track('editor/opened/recording'); - // TODO: bring this back when we figure out how to convert hevc files - // if (recordHevc) { - // openEditorWindow(await convertToH264(filePath), {recordedFps, isNewRecording: true, originalFilePath: filePath}); - // } else { - await openEditorWindow(filePath, {recordedFps, isNewRecording: true, recordingName}); - // } + const recording = new Recording({ + filePath, + title: recordingName, + apertureOptions + }); + await recording.openEditorWindow(); stopCurrentRecording(recordingName); } }; - -module.exports = { - startRecording, - stopRecording, - getAudioDevices -}; diff --git a/main/utils/accelerator-validator.js b/main/common/accelerator-validator.ts similarity index 82% rename from main/utils/accelerator-validator.js rename to main/common/accelerator-validator.ts index 4ebca4b12..5d59bef2f 100644 --- a/main/utils/accelerator-validator.js +++ b/main/common/accelerator-validator.ts @@ -1,5 +1,3 @@ -'use strict'; - // The goal of this file is validating accelerator values we receive from the user // to make sure that they are can be used with the electron api https://www.electronjs.org/docs/api/accelerator @@ -46,9 +44,9 @@ const codes = [ 'numsub', 'nummult', 'numdiv' -]; +] as const; -const keyCodeRegex = new RegExp('^([\\dA-Z~`!@#$%^&*()_+=.,<>?;:\'"\\-\\/\\\\\\[\\]\\{\\}\\|]|F([1-9]|1[\\d]|2[0-4])|' + codes.join('|') + ')$'); +const getKeyCodeRegex = () => new RegExp('^([\\dA-Z~`!@#$%^&*()_+=.,<>?;:\'"\\-\\/\\\\\\[\\]\\{\\}\\|]|F([1-9]|1[\\d]|2[0-4])|' + codes.join('|') + ')$'); const shiftKeyMap = new Map([ ['~', '`'], @@ -102,7 +100,7 @@ const namedKeyCodeMap = new Map([ ['Clear', 'Numlock'] ]); -const checkAccelerator = accelerator => { +export const checkAccelerator = (accelerator: string) => { if (!accelerator) { return true; } @@ -113,7 +111,7 @@ const checkAccelerator = accelerator => { return false; } - if (!keyCodeRegex.test(parts[parts.length - 1])) { + if (!getKeyCodeRegex().test(parts[parts.length - 1])) { return false; } @@ -121,15 +119,10 @@ const checkAccelerator = accelerator => { return metaKeys.every(part => modifiers.includes(part)) && metaKeys.some(part => part !== 'Shift'); }; -const eventKeyToAccelerator = (key, location) => { +export const eventKeyToAccelerator = (key: string, location: number) => { if (location === 3) { return numpadKeyMap.get(key); } - return namedKeyCodeMap.get(key) || shiftKeyMap.get(key) || key.toUpperCase(); -}; - -module.exports = { - checkAccelerator, - eventKeyToAccelerator + return namedKeyCodeMap.get(key) ?? shiftKeyMap.get(key) ?? key.toUpperCase(); }; diff --git a/main/common/analytics.ts b/main/common/analytics.ts index 08baca5a8..4b2f948ba 100644 --- a/main/common/analytics.ts +++ b/main/common/analytics.ts @@ -2,7 +2,7 @@ import util from 'electron-util'; import {parse} from 'semver'; -import settings from './settings'; +import {settings} from './settings'; const Insight = require('insight'); const pkg = require('../../package'); diff --git a/main/common/logger.js b/main/common/logger.js deleted file mode 100644 index 66c3695ed..000000000 --- a/main/common/logger.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -let window; -let windowIsReady = false; -let pendingMessages = []; - -function init(mainWindow) { - window = mainWindow; - - window.on('show', () => { - if (windowIsReady === false) { - windowIsReady = true; - for (const chunk of pendingMessages) { - window.webContents.send('log', chunk); - } - - pendingMessages = []; - } - }); -} - -function log(...msgs) { - if (process.type === 'browser') { // Main process - if (window && windowIsReady) { - window.webContents.send('log', msgs); - } else { - pendingMessages.push(msgs); - } - } else { - console.log(...msgs); - } -} - -exports.init = init; -exports.log = log; diff --git a/main/common/notifications.js b/main/common/notifications.js deleted file mode 100644 index 9136585bb..000000000 --- a/main/common/notifications.js +++ /dev/null @@ -1,16 +0,0 @@ -const electron = require('electron'); - -const {app, Notification} = electron; - -const notify = text => { - const notification = new Notification({ - title: app.name, - body: text - }); - - notification.show(); -}; - -module.exports = { - notify -}; diff --git a/main/common/plugins.js b/main/common/plugins.js deleted file mode 100644 index c492bda1b..000000000 --- a/main/common/plugins.js +++ /dev/null @@ -1,309 +0,0 @@ -'use strict'; - -const path = require('path'); -const fs = require('fs'); -const electron = require('electron'); -const got = require('got'); -const execa = require('execa'); -const makeDir = require('make-dir'); -const packageJson = require('package-json'); - -const {app, Notification} = electron; - -const {refreshRecordPluginItems} = require('../menus'); -const {openConfigWindow} = require('../config'); -const {openPrefsWindow} = require('../preferences'); -const {notify} = require('./notifications'); -const {track} = require('./analytics'); -const {InstalledPlugin, NpmPlugin, recordPluginServiceState} = require('../plugin'); -const {showError} = require('../utils/errors'); -const {EventEmitter} = require('events'); - -// Need to persist the notification, otherwise it is garbage collected and the actions don't trigger -// https://github.com/electron/electron/issues/12690 -let pluginNotification; - -class Plugins extends EventEmitter { - constructor() { - super(); - this.yarnBin = path.join(__dirname, '../../node_modules/yarn/bin/yarn.js'); - this._makePluginsDir(); - this.appVersion = app.getVersion(); - } - - setUpdateExportOptions(updateExportOptions) { - this.updateExportOptions = updateExportOptions; - } - - async enableService(service, plugin) { - const wasEnabled = recordPluginServiceState.get(service.title) || false; - - if (wasEnabled) { - recordPluginServiceState.set(service.title, false); - return this.refreshRecordPluginServices(); - } - - if (!plugin.config.validServices.includes(service.title)) { - openPrefsWindow({target: {name: plugin.name, action: 'configure'}}); - return; - } - - if (service.willEnable) { - try { - const canEnable = await service.willEnable(); - - if (canEnable) { - recordPluginServiceState.set(service.title, true); - } - } catch (error) { - showError(error, {title: `Something went wrong while enabling “${service.title}”`}); - const Sentry = require('./utils/sentry').default; - Sentry.captureException(error); - } - - this.refreshRecordPluginServices(); - return; - } - - recordPluginServiceState.set(service.title, true); - this.refreshRecordPluginServices(); - } - - refreshRecordPluginServices = () => { - refreshRecordPluginItems( - this.getRecordingPlugins().flatMap( - plugin => plugin.recordServices.map(service => ({ - ...service, - isEnabled: recordPluginServiceState.get(service.title) || false, - toggleEnabled: () => this.enableService(service, plugin) - })) - ) - ); - } - - _makePluginsDir() { - const cwd = path.join(app.getPath('userData'), 'plugins'); - const fp = path.join(cwd, 'package.json'); - - if (!fs.existsSync(fp)) { - makeDir.sync(cwd); - fs.writeFileSync(fp, '{"dependencies":{}}'); - } - - this.cwd = cwd; - this.pkgPath = fp; - } - - _modifyMainPackageJson(modifier) { - const pkg = JSON.parse(fs.readFileSync(this.pkgPath, 'utf8')); - modifier(pkg); - fs.writeFileSync(this.pkgPath, JSON.stringify(pkg, null, 2)); - } - - async _runYarn(...commands) { - await execa(process.execPath, [this.yarnBin, ...commands], { - cwd: this.cwd, - env: { - ELECTRON_RUN_AS_NODE: 1 - } - }); - } - - _pluginNames() { - const pkg = fs.readFileSync(path.join(this.cwd, 'package.json'), 'utf8'); - return Object.keys(JSON.parse(pkg).dependencies || {}); - } - - async _yarnInstall() { - await this._runYarn('install', '--no-lockfile', '--registry', 'https://registry.npmjs.org'); - } - - async install(name) { - track(`plugin/installed/${name}`); - // We manually add it to the package.json here so we're able to set the version to `latest` - this._modifyMainPackageJson(pkg => { - if (!pkg.dependencies) { - pkg.dependencies = {}; - } - - pkg.dependencies[name] = 'latest'; - }); - - try { - await this._yarnInstall(); - - const plugin = new InstalledPlugin(name); - - if (plugin.plugin.didInstall && typeof plugin.plugin.didInstall === 'function') { - try { - await plugin.plugin.didInstall(plugin.config); - } catch (error) { - showError(error, {plugin}); - } - } - - const {isValid, hasConfig} = plugin; - - const options = (isValid && !hasConfig) ? { - title: 'Plugin installed', - body: `"${plugin.prettyName}" is ready for use` - } : { - title: plugin.isValid ? 'Plugin installed' : 'Configure plugin', - body: `"${plugin.prettyName}" ${plugin.isValid ? 'can be configured' : 'requires configuration'}`, - actions: [ - {type: 'button', text: 'Configure'}, - {type: 'button', text: 'Later'} - ] - }; - - pluginNotification = new Notification(options); - - if (!isValid || hasConfig) { - const openConfig = () => openPrefsWindow({target: {name, action: 'configure'}}); - - pluginNotification.on('click', openConfig); - - pluginNotification.on('action', (_, index) => { - if (index === 0) { - openConfig(); - } else { - pluginNotification.close(); - } - }); - } - - for (const service of plugin.config.validServices) { - if (!service.willEnable) { - recordPluginServiceState.set(service, true); - } - } - - pluginNotification.show(); - this.updateExportOptions(); - this.refreshRecordPluginServices(); - this.emit('installed', plugin); - - return plugin; - } catch (error) { - notify(`Something went wrong while installing ${name}`); - this._modifyMainPackageJson(pkg => { - delete pkg.dependencies[name]; - }); - console.log(error); - } - } - - async upgrade() { - await this._yarnInstall(); - } - - async uninstall(name) { - track(`plugin/uninstalled/${name}`); - this._modifyMainPackageJson(pkg => { - delete pkg.dependencies[name]; - }); - const plugin = new InstalledPlugin(name); - - if (plugin.plugin.willUninstall && typeof plugin.plugin.willUninstall === 'function') { - try { - await plugin.plugin.willUninstall(plugin.config); - } catch (error) { - showError(error, {plugin}); - } - } - - plugin.config.clear(); - this.emit('uninstalled', name); - this.updateExportOptions(); - return new NpmPlugin(plugin.json, { - // Keeping for backwards compatibility - version: plugin.json.kapVersion, - ...plugin.json.kap - }); - } - - async prune() { - await this._yarnInstall(); - } - - getServices(pluginName) { - const { - shareServices = [], - recordServices = [] - } = require(path.join(this.cwd, 'node_modules', pluginName)); - - return [...shareServices, ...recordServices]; - } - - getInstalled() { - try { - return this._pluginNames().map(name => new InstalledPlugin(name)); - } catch (error) { - showError(error); - const Sentry = require('../utils/sentry').default; - Sentry.captureException(error); - return []; - } - } - - getSharePlugins() { - return this.getInstalled().filter(plugin => plugin.shareServices.length > 0); - } - - getRecordingPlugins() { - return this.getInstalled().filter(plugin => plugin.recordServices.length > 0); - } - - getEditPlugins() { - return this.getInstalled().filter(plugin => plugin.editServices.length > 0); - } - - getBuiltIn() { - return [{ - pluginPath: path.resolve(__dirname, '..', 'plugins', 'copy-to-clipboard-plugin'), - isCompatible: true, - name: '_copyToClipboard' - }, { - pluginPath: path.resolve(__dirname, '..', 'plugins', 'save-file-plugin'), - isCompatible: true, - name: '_saveToDisk' - }, { - pluginPath: path.resolve(__dirname, '..', 'plugins', 'open-with-plugin'), - isCompatible: true, - name: '_openWith' - }]; - } - - async getFromNpm() { - const url = 'https://api.npms.io/v2/search?q=keywords:kap-plugin+not:deprecated'; - const response = await got(url, {json: true}); - const installed = this._pluginNames(); - - 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(async x => { - const {kap, kapVersion} = await packageJson(x.name, {fullMetadata: true}); - return new NpmPlugin(x, { - // Keeping for backwards compatibility - version: kapVersion, - ...kap - }); - })); - } - - getPluginService(pluginName, serviceTitle) { - return this.getServices(pluginName).find(shareService => shareService.title === serviceTitle); - } - - async openPluginConfig(name) { - await openConfigWindow(name); - const plugin = new InstalledPlugin(name); - this.emit('config-changed', plugin); - return plugin.isValid; - } -} - -const plugins = new Plugins(); -module.exports = plugins; diff --git a/main/common/settings.ts b/main/common/settings.ts index b6f235233..c6577520b 100644 --- a/main/common/settings.ts +++ b/main/common/settings.ts @@ -4,12 +4,10 @@ import {homedir} from 'os'; import Store from 'electron-store'; const {defaultInputDeviceId} = require('./constants'); -const {hasMicrophoneAccess} = require('./system-permissions'); -const {getAudioDevices, getDefaultInputDevice} = require('../utils/devices'); const shortcutToAccelerator = require('../utils/shortcut-to-accelerator'); export const shortcuts = { - triggerCropper: 'Toggle Kap', + triggerCropper: 'Toggle Kap' }; const shortcutSchema = { @@ -28,21 +26,21 @@ interface Settings { recordAudio: boolean; audioInputDeviceId?: string; cropperShortcut: { - metaKey: boolean, - altKey: boolean, - ctrlKey: boolean, - shiftKey: boolean, - character: string + metaKey: boolean; + altKey: boolean; + ctrlKey: boolean; + shiftKey: boolean; + character: string; }; lossyCompression: boolean; enableShortcuts: boolean; shortcuts: { [key in keyof typeof shortcuts]: string - }, + }; version: string; } -const store = new Store({ +export const settings = new Store({ schema: { kapturesDir: { type: 'string', @@ -118,6 +116,7 @@ const store = new Store({ }, shortcuts: { type: 'object', + // eslint-disable-next-line unicorn/no-array-reduce properties: Object.keys(shortcuts).reduce((acc, key) => ({...acc, [key]: shortcutSchema}), {}), default: {} }, @@ -128,43 +127,17 @@ const store = new Store({ } }); -export default store; - // TODO: Remove this when we feel like everyone has migrated -if (store.has('recordKeyboardShortcut')) { - store.set('enableShortcuts', store.get('recordKeyboardShortcut')); - store.delete('recordKeyboardShortcut'); +if (settings.has('recordKeyboardShortcut')) { + settings.set('enableShortcuts', settings.get('recordKeyboardShortcut')); + settings.delete('recordKeyboardShortcut'); } // TODO: Remove this when we feel like everyone has migrated -if (store.has('cropperShortcut')) { - // TODO: Investigate type for dot notation - store.set('shortcuts.triggerCropper' as any, shortcutToAccelerator(store.get('cropperShortcut'))); - store.delete('cropperShortcut'); -} - -store.set('cropper' as any, {}); -store.set('actionBar' as any, {}); - -const audioInputDeviceId = store.get('audioInputDeviceId'); - -if (hasMicrophoneAccess()) { - (async () => { - const devices = await getAudioDevices(); - - if (!devices.some((device: any) => device.id === audioInputDeviceId)) { - store.set('audioInputDeviceId', defaultInputDeviceId); - } - })(); +if (settings.has('cropperShortcut')) { + settings.set('shortcuts.triggerCropper', shortcutToAccelerator(settings.get('cropperShortcut'))); + settings.delete('cropperShortcut'); } -export const getSelectedInputDeviceId = () => { - const audioInputDeviceId = store.get('audioInputDeviceId', defaultInputDeviceId); - - if (audioInputDeviceId === defaultInputDeviceId) { - const device = getDefaultInputDevice(); - return device && device.id; - } - - return audioInputDeviceId; -}; +settings.set('cropper' as any, {}); +settings.set('actionBar' as any, {}); diff --git a/main/common/system-permissions.js b/main/common/system-permissions.ts similarity index 73% rename from main/common/system-permissions.js rename to main/common/system-permissions.ts index 1aa407a55..c60f59716 100644 --- a/main/common/system-permissions.js +++ b/main/common/system-permissions.ts @@ -1,10 +1,10 @@ -const {systemPreferences, shell, dialog, app} = require('electron'); +import {systemPreferences, shell, dialog, app} from 'electron'; const {hasScreenCapturePermission, hasPromptedForPermission} = require('mac-screen-capture-permissions'); const {ensureDockIsShowing} = require('../utils/dock'); let isDialogShowing = false; -const promptSystemPreferences = options => async ({hasAsked} = {}) => { +const promptSystemPreferences = (options: {message: string; detail: string; systemPreferencesPath: string}) => async ({hasAsked}: {hasAsked?: boolean} = {}) => { if (hasAsked || isDialogShowing) { return false; } @@ -30,7 +30,7 @@ const promptSystemPreferences = options => async ({hasAsked} = {}) => { return false; }; -const openSystemPreferences = path => shell.openExternal(`x-apple.systempreferences:com.apple.preference.security?${path}`); +export const openSystemPreferences = async (path: string) => shell.openExternal(`x-apple.systempreferences:com.apple.preference.security?${path}`); // Microphone @@ -42,7 +42,7 @@ const microphoneFallback = promptSystemPreferences({ systemPreferencesPath: 'Privacy_Microphone' }); -const ensureMicrophonePermissions = async (fallback = microphoneFallback) => { +export const ensureMicrophonePermissions = async (fallback = microphoneFallback) => { const access = getMicrophoneAccess(); if (access === 'granted') { @@ -62,7 +62,7 @@ const ensureMicrophonePermissions = async (fallback = microphoneFallback) => { return fallback(); }; -const hasMicrophoneAccess = () => getMicrophoneAccess() === 'granted'; +export const hasMicrophoneAccess = () => getMicrophoneAccess() === 'granted'; // Screen Capture (10.15 and newer) @@ -72,7 +72,7 @@ const screenCaptureFallback = promptSystemPreferences({ systemPreferencesPath: 'Privacy_ScreenCapture' }); -const ensureScreenCapturePermissions = (fallback = screenCaptureFallback) => { +export const ensureScreenCapturePermissions = (fallback = screenCaptureFallback) => { const hadAsked = hasPromptedForPermission(); const hasAccess = hasScreenCapturePermission(); @@ -85,12 +85,5 @@ const ensureScreenCapturePermissions = (fallback = screenCaptureFallback) => { return false; }; -const hasScreenCaptureAccess = () => hasScreenCapturePermission(); +export const hasScreenCaptureAccess = () => hasScreenCapturePermission(); -module.exports = { - ensureMicrophonePermissions, - hasMicrophoneAccess, - openSystemPreferences, - ensureScreenCapturePermissions, - hasScreenCaptureAccess -}; diff --git a/main/common/types/base.ts b/main/common/types/base.ts index 603c47c26..692075914 100644 --- a/main/common/types/base.ts +++ b/main/common/types/base.ts @@ -1,3 +1,5 @@ +import {Rectangle} from 'electron'; + export enum Format { gif = 'gif', mp4 = 'mp4', @@ -9,7 +11,9 @@ export enum Format { export enum Encoding { h264 = 'h264', hevc = 'hevc', + // eslint-disable-next-line unicorn/prevent-abbreviations proRes422 = 'proRes422', + // eslint-disable-next-line unicorn/prevent-abbreviations proRes4444 = 'proRes4444' } @@ -18,4 +22,20 @@ export type App = { isDefault: boolean; icon: string; name: string; +}; + +export interface ApertureOptions { + fps: number; + cropArea: Rectangle; + showCursor: boolean; + highlightClicks: boolean; + screenId: number; + audioDeviceId?: string; + videoCodec?: Encoding; +} + +export interface StartRecordingOptions { + cropperBounds: Rectangle; + screenBounds: Rectangle; + displayId: number; } diff --git a/main/common/types/conversion-options.ts b/main/common/types/conversion-options.ts index a6bca1fb2..e5c3c97c4 100644 --- a/main/common/types/conversion-options.ts +++ b/main/common/types/conversion-options.ts @@ -1,4 +1,4 @@ -import {App, Format} from './base' +import {App, Format} from './base'; export type CreateConversionOptions = { filePath: string; @@ -8,15 +8,15 @@ export type CreateConversionOptions = { share: { pluginName: string; serviceTitle: string; - app?: App - } - } -} + app?: App; + }; + }; +}; export type EditServiceInfo = { pluginName: string; serviceTitle: string; -} +}; export type ConversionOptions = { startTime: number; @@ -27,7 +27,7 @@ export type ConversionOptions = { shouldCrop: boolean; shouldMute: boolean; editService?: EditServiceInfo; -} +}; export enum ConversionStatus { idle = 'idle', diff --git a/main/common/types/index.ts b/main/common/types/index.ts index 2036318b6..e3ab891ef 100644 --- a/main/common/types/index.ts +++ b/main/common/types/index.ts @@ -1,3 +1,4 @@ export * from './base'; export * from './remote-states'; export * from './conversion-options'; +export * from './window-states'; diff --git a/main/common/types/remote-states.ts b/main/common/types/remote-states.ts index db991fb84..8617edf0e 100644 --- a/main/common/types/remote-states.ts +++ b/main/common/types/remote-states.ts @@ -14,20 +14,20 @@ export type ExportOptionsFormat = { format: Format; prettyFormat: string; lastUsed: number; -} +}; export type ExportOptionsEditService = { title: string; pluginName: string; pluginPath: string; hasConfig: boolean; -} +}; export type ExportOptions = { formats: ExportOptionsFormat[]; editServices: ExportOptionsEditService[]; fpsHistory: {[key in Format]: number}; -} +}; export type EditorOptionsRemoteState = (sendUpdate: (state: ExportOptions) => void) => { actions: { @@ -41,7 +41,7 @@ export type EditorOptionsRemoteState = (sendUpdate: (state: ExportOptions) => vo }) => void; }; getState: () => ExportOptions; -} +}; export interface ConversionState { title: string; @@ -55,8 +55,8 @@ export interface ConversionState { export type ConversionRemoteState = (sendUpdate: (state: ConversionState, id: string) => void) => { actions: { - copy: (_?: undefined, conversionId?: string) => void - cancel: (_?: undefined, conversionId?: string) => void - }, - getState: (conversionId: string) => ConversionState | undefined -} + copy: (_?: undefined, conversionId?: string) => void; + cancel: (_?: undefined, conversionId?: string) => void; + }; + getState: (conversionId: string) => ConversionState | undefined; +}; diff --git a/main/common/types/window-states.ts b/main/common/types/window-states.ts new file mode 100644 index 000000000..876ddc4fd --- /dev/null +++ b/main/common/types/window-states.ts @@ -0,0 +1,8 @@ + +export interface EditorWindowState { + fps: number; + previewFilePath: string; + filePath: string; + title: string; + conversionId?: string; +} diff --git a/main/common/windows.js b/main/common/windows.ts similarity index 56% rename from main/common/windows.js rename to main/common/windows.ts index 52bf5a815..1c6241402 100644 --- a/main/common/windows.js +++ b/main/common/windows.ts @@ -1,42 +1,59 @@ -'use strict'; +import {Menu, MenuItem, nativeImage} from 'electron'; +import Store from 'electron-store'; +import {windowManager} from '../windows/manager'; -const {Menu, MenuItem, nativeImage} = require('electron'); const {getWindows, activateWindow} = require('mac-windows'); const {getAppIconListByPid} = require('node-mac-app-icon'); -const Store = require('electron-store'); -const {selectApp} = require('../cropper'); +export interface MacWindow { + pid: number; + ownerName: string; + name: string; + width: number; + height: number; + x: number; + y: number; + number: number; +} const APP_BLACKLIST = [ 'Kap', 'Kap Beta' ]; -const store = new Store({ +const store = new Store<{ + appUsageHistory: Record; +}>({ name: 'usage-history' }); const usageHistory = store.get('appUsageHistory', {}); -const isValidApp = ({ownerName}) => !APP_BLACKLIST.includes(ownerName); +const isValidApp = ({ownerName}: MacWindow) => !APP_BLACKLIST.includes(ownerName); const getWindowList = async () => { - const windows = await getWindows(); + const windows = await getWindows() as MacWindow[]; const images = await getAppIconListByPid(windows.map(win => win.pid), { size: 16, failOnError: false - }); + }) as Array<{ + pid: number; + icon: Buffer; + }>; let maxLastUsed = 0; return windows.filter(window => isValidApp(window)).map(win => { const iconImage = images.find(img => img.pid === win.pid); - const icon = iconImage.icon ? nativeImage.createFromBuffer(iconImage.icon) : null; + const icon = iconImage?.icon ? nativeImage.createFromBuffer(iconImage.icon) : undefined; const window = { ...win, icon2x: icon, - icon: icon ? icon.resize({width: 16, height: 16}) : null, + icon: icon?.resize({width: 16, height: 16}), count: 0, lastUsed: 0, ...usageHistory[win.pid] @@ -57,7 +74,7 @@ const getWindowList = async () => { }); }; -const buildWindowsMenu = async selected => { +export const buildWindowsMenu = async (selected: string) => { const menu = new Menu(); const windows = await getWindowList(); @@ -68,7 +85,9 @@ const buildWindowsMenu = async selected => { icon: win.icon, type: 'checkbox', checked: win.ownerName === selected, - click: () => activateApp(win) + click: () => { + activateApp(win); + } }) ); } @@ -76,8 +95,8 @@ const buildWindowsMenu = async selected => { return menu; }; -const updateAppUsageHistory = app => { - const {count = 0} = usageHistory[app.pid] || {}; +const updateAppUsageHistory = (app: MacWindow) => { + const {count = 0} = usageHistory[app.pid] ?? {}; usageHistory[app.pid] = { count: count + 1, @@ -87,12 +106,7 @@ const updateAppUsageHistory = app => { store.set('appUsageHistory', usageHistory); }; -const activateApp = window => { +export const activateApp = (window: MacWindow) => { updateAppUsageHistory(window); - selectApp(window, activateWindow); -}; - -module.exports = { - buildWindowsMenu, - activateApp + windowManager.cropper?.selectApp(window, activateWindow); }; diff --git a/main/conversion.ts b/main/conversion.ts index 74ce9c923..562b1b7b2 100644 --- a/main/conversion.ts +++ b/main/conversion.ts @@ -1,39 +1,41 @@ import fs from 'fs'; -import {app, clipboard} from 'electron'; +import {app, clipboard, dialog} from 'electron'; import {EventEmitter} from 'events'; import {ConversionOptions, ConversionStatus, CreateConversionOptions, Format} from './common/types'; import {Video} from './video'; import {convertTo} from './converters'; import Export, {ExportOptions} from './export'; import hash from 'object-hash'; -import {ipcMain} from 'electron-better-ipc'; -import plugins from './plugins'; +import {ipcMain as ipc} from 'electron-better-ipc'; +import {plugins} from './plugins'; import {askForTargetFilePath} from './plugins/built-in/save-file-plugin'; import path from 'path'; import {showError} from './utils/errors'; import {notify} from './utils/notifications'; import PCancelable from 'p-cancelable'; import prettyBytes from 'pretty-bytes'; +import {ensureDockIsShowingSync} from './utils/dock'; +import {windowManager} from './windows/manager'; const plist = require('plist'); +// TODO: remove this when exports window is rewritten +const callExportsWindow = (channel: string, data: any) => { + const exportsWindow = windowManager.exports?.get(); + + if (exportsWindow) { + // TODO(karaggeorge): Investigate why `ipc.callRenderer(exportsWindow, channel, data);` is not working here. + ipc.callRenderer(exportsWindow, channel, data); + } +}; + // A conversion object describes the process of converting a video or recording // using ffmpeg that can then be shared multiple times using Share plugins export default class Conversion extends EventEmitter { - static all = new Map(); + static conversionMap = new Map(); - static fromId(id: string) { - return this.all.get(id); - } - - static getOrCreate(video: Video, format: Format, options: ConversionOptions) { - const id = hash({ - filePath: video.filePath, - format, - options - }); - - return this.fromId(id) ?? new Conversion(video, format, options); + static get all() { + return [...this.conversionMap.values()]; } id: string; @@ -41,40 +43,31 @@ export default class Conversion extends EventEmitter { format: Format; options: ConversionOptions; - text: string = ''; + text = ''; percentage?: number; error?: Error; description: string; title: string; finalSize?: string; - private currentExport?: Export; - private convertedFilePath?: string; + convertedFilePath?: string; + requestedFileType?: Format; + private currentExport?: Export; private _status: ConversionStatus = ConversionStatus.idle; get status() { return this._status; } - get canCopy() { - console.log('Can copy,', this.convertedFilePath, this.format); - return Boolean(this.convertedFilePath && [Format.gif, Format.apng].includes(this.format)); - } - - copy = () => { - console.log('Copy was called', this.convertedFilePath); - clipboard.writeBuffer('NSFilenamesPboardType', Buffer.from(plist.build([this.convertedFilePath]))); - notify({ - body: 'The file has been copied to the clipboard', - title: app.name - }); - } - set status(newStatus: ConversionStatus) { this._status = newStatus; this.emit('updated'); } + get canCopy() { + return Boolean(this.convertedFilePath && [Format.gif, Format.apng].includes(this.format)); + } + private conversionProcess?: PCancelable; constructor(video: Video, format: Format, options: ConversionOptions) { @@ -84,7 +77,7 @@ export default class Conversion extends EventEmitter { this.options = options; this.description = `${this.options.width} x ${this.options.height} at ${this.options.fps} FPS`; - this.title = path.parse(this.video.filePath).name; + this.title = video.title; this.id = hash({ filePath: video.filePath, @@ -92,34 +85,75 @@ export default class Conversion extends EventEmitter { options }); - Conversion.all.set(this.id, this); + Conversion.conversionMap.set(this.id, this); + + // TODO: remove this when exports window is rewritten + this.on('updated', () => { + callExportsWindow('update-export-data', this.currentExport?.data); + }); } - onProgress = (text: string, progress: number) => { - this.text = text; - this.percentage = Math.max(Math.min(progress, 1), 0); - this.emit('updated'); + static fromId(id: string) { + return this.conversionMap.get(id); } - private onConversionProgress = (action: string, progress: number, estimate?: string) => { - console.log('OnConversionProgress was called'); - const text = estimate ? `${action} — ${estimate} remaining` : `${action}…`; - this.onProgress(text, progress); + static getOrCreate(video: Video, format: Format, options: ConversionOptions) { + const id = hash({ + filePath: video.filePath, + format, + options + }); + + return this.fromId(id) ?? new Conversion(video, format, options); } - private onExportProgress = (text: string, progress: number) => { - this.onProgress(text, progress); + copy = () => { + clipboard.writeBuffer('NSFilenamesPboardType', Buffer.from(plist.build([this.convertedFilePath]))); + notify({ + body: 'The file has been copied to the clipboard', + title: app.name + }); + }; + + async filePathExists() { + if (!this.convertedFilePath) { + return false; + } + + try { + await fs.promises.access(this.convertedFilePath, fs.constants.F_OK); + return true; + } catch { + return false; + } } + onProgress = (text: string, progress: number) => { + this.text = text; + this.percentage = Math.max(Math.min(progress, 1), 0); + + if (this.currentExport) { + this.currentExport.text = this.text; + this.currentExport.percentage = this.percentage; + } + + this.emit('updated'); + }; + filePath = async ({fileType}: {fileType?: Format} = {}) => { - console.log(fileType); - if (!this.conversionProcess) { + if (fileType) { + this.currentExport?.disableActions(); + } + + if (!this.conversionProcess || (this.requestedFileType !== fileType)) { this.start(); } + this.requestedFileType = fileType; + try { this.convertedFilePath = await this.conversionProcess; - return this.convertedFilePath as string; + return this.convertedFilePath!; } catch (error) { // Ensure we re-try the conversion if it fails this.conversionProcess = undefined; @@ -130,7 +164,7 @@ export default class Conversion extends EventEmitter { return ''; } - } + }; addExport(exportOptions: ExportOptions) { this.status = ConversionStatus.inProgress; @@ -140,37 +174,45 @@ export default class Conversion extends EventEmitter { const newExport = new Export(this, exportOptions); - newExport.on('progress', ({text, percentage}) => this.onExportProgress(text, percentage)); + const onProgress = ({text, percentage}: {text: string; percentage: number}) => { + newExport.status = 'processing'; + this.onExportProgress(text, percentage); + }; + + newExport.on('progress', onProgress); + const cleanup = () => { this.currentExport = undefined; - newExport.off('progress', this.onExportProgress); - } + newExport.off('progress', onProgress); + }; newExport.once('canceled', () => { - this.onExportProgress('Export canceled', 1); - cleanup(); + newExport.onProgress('Export canceled', 1); + newExport.status = 'canceled'; this.status = ConversionStatus.canceled; this.emit('updated'); + cleanup(); }); newExport.once('finished', () => { - this.onExportProgress('Export completed', 1); - cleanup(); + newExport.onProgress('Export completed', 1); + newExport.status = 'completed'; this.status = ConversionStatus.completed; this.emit('updated'); + cleanup(); }); newExport.once('error', (error: Error) => { showError(error, {plugin: exportOptions.plugin} as any); - this.onExportProgress('Export failed', 1); - cleanup(); + newExport.onProgress('Export failed', 1); + newExport.status = 'failed'; this.error = error; this.status = ConversionStatus.failed; this.emit('updated'); + cleanup(); }); this.currentExport = newExport; - console.log('Starting export', newExport); newExport.start(); } @@ -179,11 +221,20 @@ export default class Conversion extends EventEmitter { if (!this.conversionProcess?.isCanceled) { this.conversionProcess?.cancel(); } + this.currentExport?.onCancel(); - } + }; + + private readonly onConversionProgress = (action: string, progress: number, estimate?: string) => { + const text = estimate ? `${action} — ${estimate} remaining` : `${action}…`; + this.onProgress(text, progress); + }; + + private readonly onExportProgress = (text: string, progress: number) => { + this.onProgress(text, progress); + }; - private start = () => { - console.log('STart called'); + private readonly start = () => { this.conversionProcess = convertTo( this.format, { @@ -203,17 +254,17 @@ export default class Conversion extends EventEmitter { this.emit('updated'); } catch {} }); - } + }; } -export const setupConversionHook = () => { - ipcMain.answerRenderer('create-conversion', async ({ +export const setUpConversionListeners = () => { + ipc.answerRenderer('create-conversion', async ({ filePath, options, format, plugins: pluginOptions }: CreateConversionOptions, window) => { - console.log('HERE WITH', filePath, options, format, pluginOptions); - console.log(window); const video = Video.fromId(filePath); - const extras: {[key: string]: any} = {}; + const extras: Record = { + appUrl: pluginOptions.share.app?.url + }; if (!video) { return; @@ -228,28 +279,23 @@ export const setupConversionHook = () => { if (targetFilePath) { extras.targetFilePath = targetFilePath; - } else { return; } } - console.log('Here with', video); - const exportPlugin = plugins.sharePlugins.find(plugin => { - return plugin.name === pluginOptions.share.pluginName + return plugin.name === pluginOptions.share.pluginName; }); const exportService = exportPlugin?.shareServices.find(service => { - return service.title === pluginOptions.share.serviceTitle + return service.title === pluginOptions.share.serviceTitle; }); if (!exportPlugin || !exportService) { return; } - console.log('here', exportPlugin); - const conversion = Conversion.getOrCreate(video, format, options); if (extras.targetFilePath) { @@ -262,7 +308,30 @@ export const setupConversionHook = () => { extras }); - console.log('queueed', conversion.id); return conversion.id; }); -} + + app.on('before-quit', event => { + if (Conversion.all.some(conversion => conversion.status === ConversionStatus.inProgress)) { + windowManager.exports?.open(); + + ensureDockIsShowingSync(() => { + const buttonIndex = dialog.showMessageBoxSync({ + type: 'question', + buttons: [ + 'Continue', + 'Quit' + ], + defaultId: 0, + cancelId: 1, + message: 'Do you want to continue exporting?', + detail: 'Kap is currently exporting files. If you quit, the export task will be canceled.' + }); + + if (buttonIndex === 0) { + event.preventDefault(); + } + }); + } + }); +}; diff --git a/main/convert.js b/main/convert.js deleted file mode 100644 index fe76bce5c..000000000 --- a/main/convert.js +++ /dev/null @@ -1,391 +0,0 @@ -/* eslint-disable array-element-newline */ -'use strict'; - -const os = require('os'); -const path = require('path'); -const execa = require('execa'); -const moment = require('moment'); -const prettyMs = require('pretty-ms'); -const tmp = require('tmp'); -const ffmpeg = require('@ffmpeg-installer/ffmpeg'); -const util = require('electron-util'); -const PCancelable = require('p-cancelable'); -const tempy = require('tempy'); -const gifsicle = require('gifsicle'); -const {track} = require('./common/analytics'); -const {EditServiceContext} = require('./service-context'); -const settings = require('./common/settings').default; - -const gifsiclePath = util.fixPathForAsarUnpack(gifsicle); -const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path); -const timeRegex = /time=\s*(\d\d:\d\d:\d\d.\d\d)/gm; -const speedRegex = /speed=\s*(-?\d+(,\d+)*(\.\d+(e\d+)?)?)/gm; - -// https://trac.ffmpeg.org/ticket/309 -const makeEven = n => 2 * Math.round(n / 2); - -const areDimensionsEven = ({width, height}) => width % 2 === 0 && height % 2 === 0; - -const getRunFunction = (shouldTrack, mode = 'convert') => (outputPath, options, args) => { - const modes = new Map([ - ['convert', ffmpegPath], - ['compress', gifsiclePath] - ]); - const program = modes.get(mode); - - return new PCancelable((resolve, reject, onCancel) => { - const runner = execa(program, args); - const durationMs = moment.duration(options.endTime - options.startTime, 'seconds').asMilliseconds(); - let speed; - - onCancel(() => { - if (shouldTrack) { - track('file/export/convert/canceled'); - } - - runner.kill(); - }); - - let stderr = ''; - runner.stderr.setEncoding('utf8'); - runner.stderr.on('data', data => { - stderr += data; - - data = data.trim(); - - const processingSpeed = speedRegex.exec(data); - - if (processingSpeed) { - speed = Number.parseFloat(processingSpeed[1]); - } - - const timeProccessed = timeRegex.exec(data); - - if (timeProccessed && speed > 0) { - const processedMs = moment.duration(timeProccessed[1]).asMilliseconds(); - const progress = processedMs / durationMs; - - // Wait 2 second in the conversion for the speed to be stable - if (processedMs > 2 * 1000) { - const msRemaining = (durationMs - processedMs) / speed; - - options.onProgress(progress, prettyMs(Math.max(msRemaining, 1000), {compact: true})); - } else { - options.onProgress(progress); - } - } - }); - - runner.on('error', reject); - - runner.on('exit', code => { - if (code === 0) { - if (shouldTrack) { - track(`file/export/${mode}/completed`); - } - - resolve(outputPath); - } else { - if (shouldTrack) { - track(`file/export/${mode}/failed`); - } - - reject(new Error(`${program} exited with code: ${code}\n\n${stderr}`)); - } - }); - - runner.catch(reject); - }); -}; - -const mute = PCancelable.fn(async (inputPath, onCancel) => { - const mutedPath = tmp.tmpNameSync({postfix: path.extname(inputPath)}); - const converter = execa(ffmpegPath, [ - '-i', inputPath, - '-an', - '-vcodec', 'copy', - mutedPath - ]); - - onCancel(() => { - converter.kill(); - }); - - await converter; - - return mutedPath; -}); - -const convert = getRunFunction(true); -const compress = (outputPath, options, args) => { - options.onProgress(0, '', 'Compressing'); - - if (settings.get('lossyCompression')) { - args = [ - '--lossy=50', - ...args - ]; - } - - return getRunFunction(true, 'compress')(outputPath, options, args); -}; - -const convertToMp4 = PCancelable.fn(async (options, onCancel) => { - if (options.isMuted) { - const muteProcess = mute(options.inputPath); - - onCancel(() => { - muteProcess.cancel(); - }); - - options.inputPath = await muteProcess; - } - - return convert(options.outputPath, options, [ - '-i', options.inputPath, - '-r', options.fps, - ...( - options.shouldCrop || !areDimensionsEven(options) ? [ - '-s', `${makeEven(options.width)}x${makeEven(options.height)}`, - '-ss', options.startTime, - '-to', options.endTime - ] : [] - ), - options.outputPath - ]); -}); - -const convertToWebm = PCancelable.fn(async (options, onCancel) => { - if (options.isMuted) { - const muteProcess = mute(options.inputPath); - - onCancel(() => { - muteProcess.cancel(); - }); - - options.inputPath = await muteProcess; - } - - return convert(options.outputPath, options, [ - '-i', options.inputPath, - // http://wiki.webmproject.org/ffmpeg - // https://trac.ffmpeg.org/wiki/Encode/VP9 - '-threads', Math.max(os.cpus().length - 1, 1), - '-deadline', 'good', // `best` is twice as slow and only slighty better - '-b:v', '1M', // Bitrate (same as the MP4) - '-codec:v', 'vp9', - '-codec:a', 'vorbis', - '-ac', '2', // https://stackoverflow.com/questions/19004762/ffmpeg-covert-from-mp4-to-webm-only-working-on-some-files - '-strict', '-2', // Needed because `vorbis` is experimental - '-r', options.fps, - ...( - options.shouldCrop ? [ - '-s', `${options.width}x${options.height}`, - '-ss', options.startTime, - '-to', options.endTime - ] : [] - ), - options.outputPath - ]); -}); - -const convertToAv1 = PCancelable.fn(async (options, onCancel) => { - if (options.isMuted) { - const muteProcess = mute(options.inputPath); - - onCancel(() => { - muteProcess.cancel(); - }); - - options.inputPath = await muteProcess; - } - - return convert(options.outputPath, options, [ - '-i', options.inputPath, - '-r', options.fps, - ...( - options.shouldCrop || !areDimensionsEven(options) ? [ - '-s', `${makeEven(options.width)}x${makeEven(options.height)}`, - '-ss', options.startTime, - '-to', options.endTime - ] : [] - ), - '-c:v', 'libaom-av1', - '-c:a', 'libopus', - '-crf', '34', - '-b:v', '0', - '-strict', 'experimental', - // Enables row-based multi-threading which maximizes CPU usage - // https://trac.ffmpeg.org/wiki/Encode/AV1 - '-cpu-used', '4', - '-row-mt', '1', - '-tiles', '2x2', - options.outputPath - ]); -}); - -// Should be similiar to the Gif generation -const convertToApng = options => { - return convert(options.outputPath, options, [ - '-i', options.inputPath, - '-vf', `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''}`, - // Strange for APNG instead of -loop it uses -plays see: https://stackoverflow.com/questions/43795518/using-ffmpeg-to-create-looping-apng - '-plays', options.loop === true ? '0' : '1', // 0 == forever; 1 == no loop - ...( - options.shouldCrop ? [ - '-ss', options.startTime, - '-to', options.endTime - ] : [] - ), - options.outputPath - ]); -}; - -// `time ffmpeg -i original.mp4 -vf fps=30,scale=480:-1::flags=lanczos,palettegen palette.png` -// `time ffmpeg -i original.mp4 -i palette.png -filter_complex 'fps=30,scale=-1:-1:flags=lanczos[x]; [x][1:v]paletteuse' palette.gif` -const convertToGif = PCancelable.fn(async (options, onCancel) => { - const palettePath = tmp.tmpNameSync({postfix: '.png'}); - const paletteProcessor = execa(ffmpegPath, [ - ...( - options.shouldCrop ? [ - '-ss', options.startTime, - '-to', options.endTime - ] : [] - ), - '-i', options.inputPath, - '-vf', `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''},palettegen`, - palettePath - ]); - - onCancel(() => { - paletteProcessor.kill(); - }); - - await paletteProcessor; - - await convert(options.outputPath, options, [ - '-i', options.inputPath, - '-i', palettePath, - '-filter_complex', `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''}[x]; [x][1:v]paletteuse`, - '-loop', options.loop === true ? '0' : '-1', // 0 == forever; -1 == no loop - ...( - options.shouldCrop ? [ - '-ss', options.startTime, - '-to', options.endTime - ] : [] - ), - options.outputPath - ]); - - return compress(options.outputPath, options, [ - '--batch', - options.outputPath - ]); -}); - -const converters = new Map([ - ['gif', convertToGif], - ['mp4', convertToMp4], - ['webm', convertToWebm], - ['apng', convertToApng], - ['av1', convertToAv1] -]); - -const convertTo = (options, format) => { - const outputPath = path.join(tempy.directory(), options.defaultFileName); - const converter = converters.get(format); - - if (!converter) { - throw new Error(`Unsupported file format: ${format}`); - } - - options.onProgress(0); - track(`file/export/format/${format}`); - - if (options.editService) { - return convertUsingPlugin({outputPath, format, converter, ...options}); - } - - return converter({outputPath, ...options}); -}; - -const convertUsingPlugin = PCancelable.fn(async ({editService, converter, ...options}, onCancel) => { - let croppedPath; - - if (options.shouldCrop) { - croppedPath = tmp.tmpNameSync({postfix: path.extname(options.inputPath)}); - - editService.setProgress('Cropping…'); - - const cropProcess = execa(ffmpegPath, [ - '-i', options.inputPath, - '-s', `${makeEven(options.width)}x${makeEven(options.height)}`, - '-ss', options.startTime, - '-to', options.endTime, - croppedPath - ]); - - onCancel(() => { - cropProcess.kill(); - }); - - await cropProcess; - } else { - croppedPath = options.inputPath; - } - - let canceled = false; - const convertFunction = getRunFunction(false); - - const editPath = tmp.tmpNameSync({postfix: path.extname(croppedPath)}); - - console.log('Export options', { - ...options, - inputPath: croppedPath, - outputPath: editPath - }); - - const editProcess = editService.service.action( - new EditServiceContext({ - onCancel: editService.cancel, - config: editService.config, - setProgress: editService.setProgress, - convert: (args, progressText = 'Converting') => convertFunction(undefined, { - endTime: options.endTime, - startTime: options.startTime, - onProgress: (percentage, estimate) => editService.setProgress(estimate ? `${progressText} — ${estimate} remaining` : `${progressText}…`, percentage) - }, args), - exportOptions: { - ...options, - inputPath: croppedPath, - outputPath: editPath - } - }) - ); - - if (editProcess.cancel && typeof editProcess.cancel === 'function') { - onCancel(() => { - canceled = true; - editProcess.cancel(); - }); - } - - await editProcess; - - if (canceled) { - return; - } - - track(`plugins/used/edit/${editService.pluginName}`); - - return converter({ - ...options, - shouldCrop: false, - inputPath: editPath - }); -}); - -module.exports = { - convertTo, - converters -}; diff --git a/main/converters/h264.ts b/main/converters/h264.ts index 5177a4a6b..565f070e0 100644 --- a/main/converters/h264.ts +++ b/main/converters/h264.ts @@ -2,9 +2,10 @@ import PCancelable from 'p-cancelable'; import tempy from 'tempy'; import {compress, convert} from './process'; import {areDimensionsEven, conditionalArgs, ConvertOptions, makeEven} from './utils'; -import settings from '../common/settings'; +import {settings} from '../common/settings'; import os from 'os'; import {Format} from '../common/types'; +import fs from 'fs'; // `time ffmpeg -i original.mp4 -vf fps=30,scale=480:-1::flags=lanczos,palettegen palette.png` // `time ffmpeg -i original.mp4 -i palette.png -filter_complex 'fps=30,scale=-1:-1:flags=lanczos[x]; [x][1:v]paletteuse' palette.gif` @@ -16,8 +17,10 @@ const convertToGif = PCancelable.fn(async (options: ConvertOptions, onCancel: PC '-vf', `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''},palettegen`, { args: [ - '-ss', options.startTime.toString(), - '-to', options.endTime.toString() + '-ss', + options.startTime.toString(), + '-to', + options.endTime.toString() ], if: options.shouldCrop }, @@ -30,21 +33,42 @@ const convertToGif = PCancelable.fn(async (options: ConvertOptions, onCancel: PC await paletteProcess; + // Sometimes if the clip is too short or fps too low, the palette is not generated + const hasPalette = fs.existsSync(palettePath); + const shouldLoop = settings.get('loopExports'); const conversionProcess = convert(options.outputPath, { - onProgress: (progress, estimate) => options.onProgress('Converting', progress, estimate), + onProgress: (progress, estimate) => { + options.onProgress('Converting', progress, estimate); + }, startTime: options.startTime, endTime: options.endTime }, conditionalArgs( '-i', options.inputPath, - '-i', palettePath, - '-filter_complex', `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''}[x]; [x][1:v]paletteuse`, + { + args: [ + '-i', + palettePath, + '-filter_complex', + `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''}[x]; [x][1:v]paletteuse` + ], + if: hasPalette + }, + { + args: [ + '-vf', + `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''}` + ], + if: !hasPalette + }, '-loop', shouldLoop ? '0' : '-1', // 0 == forever; -1 == no loop { args: [ - '-ss', options.startTime.toString(), - '-to', options.endTime.toString() + '-ss', + options.startTime.toString(), + '-to', + options.endTime.toString() ], if: options.shouldCrop }, @@ -58,7 +82,9 @@ const convertToGif = PCancelable.fn(async (options: ConvertOptions, onCancel: PC await conversionProcess; const compressProcess = compress(options.outputPath, { - onProgress: (progress, estimate) => options.onProgress('Compressing', progress, estimate), + onProgress: (progress, estimate) => { + options.onProgress('Compressing', progress, estimate); + }, startTime: options.startTime, endTime: options.endTime }, [ @@ -75,8 +101,11 @@ const convertToGif = PCancelable.fn(async (options: ConvertOptions, onCancel: PC return options.outputPath; }); +// eslint-disable-next-line @typescript-eslint/promise-function-async const convertToMp4 = (options: ConvertOptions) => convert(options.outputPath, { - onProgress: (progress, estimate) => options.onProgress('Converting', progress, estimate), + onProgress: (progress, estimate) => { + options.onProgress('Converting', progress, estimate); + }, startTime: options.startTime, endTime: options.endTime }, conditionalArgs( @@ -88,17 +117,23 @@ const convertToMp4 = (options: ConvertOptions) => convert(options.outputPath, { }, { args: [ - '-s', `${makeEven(options.width)}x${makeEven(options.height)}`, - '-ss', options.startTime.toString(), - '-to', options.endTime.toString() + '-s', + `${makeEven(options.width)}x${makeEven(options.height)}`, + '-ss', + options.startTime.toString(), + '-to', + options.endTime.toString() ], if: options.shouldCrop || !areDimensionsEven(options) }, options.outputPath )); +// eslint-disable-next-line @typescript-eslint/promise-function-async const convertToWebm = (options: ConvertOptions) => convert(options.outputPath, { - onProgress: (progress, estimate) => options.onProgress('Converting', progress, estimate), + onProgress: (progress, estimate) => { + options.onProgress('Converting', progress, estimate); + }, startTime: options.startTime, endTime: options.endTime }, conditionalArgs( @@ -119,17 +154,23 @@ const convertToWebm = (options: ConvertOptions) => convert(options.outputPath, { }, { args: [ - '-s', `${makeEven(options.width)}x${makeEven(options.height)}`, - '-ss', options.startTime.toString(), - '-to', options.endTime.toString() + '-s', + `${makeEven(options.width)}x${makeEven(options.height)}`, + '-ss', + options.startTime.toString(), + '-to', + options.endTime.toString() ], if: options.shouldCrop || !areDimensionsEven(options) }, options.outputPath )); +// eslint-disable-next-line @typescript-eslint/promise-function-async const convertToAv1 = (options: ConvertOptions) => convert(options.outputPath, { - onProgress: (progress, estimate) => options.onProgress('Converting', progress, estimate), + onProgress: (progress, estimate) => { + options.onProgress('Converting', progress, estimate); + }, startTime: options.startTime, endTime: options.endTime }, conditionalArgs( @@ -151,17 +192,23 @@ const convertToAv1 = (options: ConvertOptions) => convert(options.outputPath, { }, { args: [ - '-s', `${makeEven(options.width)}x${makeEven(options.height)}`, - '-ss', options.startTime.toString(), - '-to', options.endTime.toString() + '-s', + `${makeEven(options.width)}x${makeEven(options.height)}`, + '-ss', + options.startTime.toString(), + '-to', + options.endTime.toString() ], if: options.shouldCrop || !areDimensionsEven(options) }, options.outputPath )); +// eslint-disable-next-line @typescript-eslint/promise-function-async const convertToApng = (options: ConvertOptions) => convert(options.outputPath, { - onProgress: (progress, estimate) => options.onProgress('Converting', progress, estimate), + onProgress: (progress, estimate) => { + options.onProgress('Converting', progress, estimate); + }, startTime: options.startTime, endTime: options.endTime }, conditionalArgs( @@ -175,16 +222,21 @@ const convertToApng = (options: ConvertOptions) => convert(options.outputPath, { }, { args: [ - '-ss', options.startTime.toString(), - '-to', options.endTime.toString() + '-ss', + options.startTime.toString(), + '-to', + options.endTime.toString() ], if: options.shouldCrop }, options.outputPath )); +// eslint-disable-next-line @typescript-eslint/promise-function-async export const crop = (options: ConvertOptions) => convert(options.outputPath, { - onProgress: (progress, estimate) => options.onProgress('Cropping', progress, estimate), + onProgress: (progress, estimate) => { + options.onProgress('Cropping', progress, estimate); + }, startTime: options.startTime, endTime: options.endTime }, conditionalArgs( diff --git a/main/converters/index.ts b/main/converters/index.ts index 7dd920396..d79e63e58 100644 --- a/main/converters/index.ts +++ b/main/converters/index.ts @@ -7,9 +7,10 @@ import {ConvertOptions} from './utils'; import {getFormatExtension} from '../common/constants'; import PCancelable, {OnCancelFunction} from 'p-cancelable'; import {convert} from './process'; -import plugins from '../plugins'; +import {plugins} from '../plugins'; import {EditServiceContext} from '../plugins/service-context'; -import settings from '../common/settings'; +import {settings} from '../common/settings'; +import {Except} from 'type-fest'; const converters = new Map([ [Encoding.h264, h264Converters] @@ -19,9 +20,10 @@ const croppingHandlers = new Map([ [Encoding.h264, h264Crop] ]); +// eslint-disable-next-line @typescript-eslint/promise-function-async export const convertTo = ( format: Format, - options: Omit & {defaultFileName: string}, + options: Except & {defaultFileName: string}, encoding: Encoding = Encoding.h264 ) => { if (!converters.has(encoding)) { @@ -39,7 +41,7 @@ export const convertTo = ( const conversionOptions = { outputPath: path.join(tempy.directory(), `${options.defaultFileName}.${getFormatExtension(format)}`), - ...options, + ...options }; if (options.editService) { @@ -49,11 +51,9 @@ export const convertTo = ( throw new Error(`Unsupported encoding: ${encoding}`); } - return convertWithEditPlugin({...conversionOptions, format, croppingHandler, converter}) + return convertWithEditPlugin({...conversionOptions, format, croppingHandler, converter}); } - console.log('Converting with no edit plugin', options); - return converter(conversionOptions); }; @@ -89,14 +89,17 @@ const convertWithEditPlugin = PCancelable.fn( let canceled = false; - const convertFunction = (args: string[], text: string = 'Converting') => new PCancelable(async (resolve, reject, onCancel) => { + // eslint-disable-next-line @typescript-eslint/promise-function-async + const convertFunction = (args: string[], text = 'Converting') => new PCancelable(async (resolve, reject, onCancel) => { try { const process = convert( '', { shouldTrack: false, startTime: options.startTime, endTime: options.endTime, - onProgress: (progress, estimate) => options.onProgress(text, progress, estimate) + onProgress: (progress, estimate) => { + options.onProgress(text, progress, estimate); + } }, args ); @@ -113,11 +116,11 @@ const convertWithEditPlugin = PCancelable.fn( const editPath = tempy.file({extension: path.extname(croppedPath)}); const editPlugin = plugins.editPlugins.find(plugin => { - return plugin.name === options.editService?.pluginName + return plugin.name === options.editService?.pluginName; }); const editService = editPlugin?.editServices.find(service => { - return service.title === options.editService?.serviceTitle + return service.title === options.editService?.serviceTitle; }); if (!editService || !editPlugin) { @@ -146,7 +149,7 @@ const convertWithEditPlugin = PCancelable.fn( onCancel(() => { canceled = true; - // @ts-ignore + // @ts-expect-error if (editProcess.cancel && typeof editProcess.cancel === 'function') { (editProcess as PCancelable).cancel(); } diff --git a/main/converters/process.ts b/main/converters/process.ts index e1b05dbf8..20b1e1e3d 100644 --- a/main/converters/process.ts +++ b/main/converters/process.ts @@ -7,7 +7,7 @@ import path from 'path'; import {track} from '../common/analytics'; import {conditionalArgs, extractProgressFromStderr} from './utils'; -import settings from '../common/settings'; +import {settings} from '../common/settings'; const ffmpeg = require('@ffmpeg-installer/ffmpeg'); const gifsicle = require('gifsicle'); @@ -33,12 +33,13 @@ export interface ProcessOptions { } const defaultProcessOptions = { - shouldTrack: true, -} + shouldTrack: true +}; const createProcess = (mode: Mode) => { - const program = modes.get(mode) as string; + const program = modes.get(mode)!; + // eslint-disable-next-line @typescript-eslint/promise-function-async return (outputPath: string, options: ProcessOptions, args: string[]) => { const { shouldTrack, @@ -55,7 +56,7 @@ const createProcess = (mode: Mode) => { if (shouldTrack) { track(`file/export/${modeName}/${eventName}`); } - } + }; return new PCancelable((resolve, reject, onCancel) => { const runner = execa(program, args); @@ -83,7 +84,7 @@ const createProcess = (mode: Mode) => { const failWithError = (reason: unknown) => { trackConversionEvent('failed'); reject(reason); - } + }; runner.on('error', failWithError); @@ -92,18 +93,19 @@ const createProcess = (mode: Mode) => { trackConversionEvent('completed'); resolve(outputPath); } else { - failWithError(new Error(`${program} exited with code: ${code}\n\n${stderr}`)); + failWithError(new Error(`${program} exited with code: ${code ?? 0}\n\n${stderr}`)); } }); runner.catch(failWithError); }); - } -} + }; +}; export const convert = createProcess(Mode.convert); const compressFunction = createProcess(Mode.compress); +// eslint-disable-next-line @typescript-eslint/promise-function-async export const compress = (outputPath: string, options: ProcessOptions, args: string[]) => { const useLossy = settings.get('lossyCompression', false); @@ -118,9 +120,11 @@ export const mute = PCancelable.fn(async (inputPath: string, onCancel: PCancelab const mutedPath = tempy.file({extension: path.extname(inputPath)}); const converter = convert(mutedPath, {shouldTrack: false}, [ - '-i', inputPath, + '-i', + inputPath, '-an', - '-vcodec', 'copy', + '-vcodec', + 'copy', mutedPath ]); diff --git a/main/converters/utils.ts b/main/converters/utils.ts index a6e4b2132..561b789d6 100644 --- a/main/converters/utils.ts +++ b/main/converters/utils.ts @@ -16,12 +16,12 @@ export interface ConvertOptions { editService?: { pluginName: string; serviceTitle: string; - } + }; } export const makeEven = (n: number) => 2 * Math.round(n / 2); -export const areDimensionsEven = ({width, height}: {width: number, height: number}) => width % 2 === 0 && height % 2 === 0; +export const areDimensionsEven = ({width, height}: {width: number; height: number}) => width % 2 === 0 && height % 2 === 0; export const extractProgressFromStderr = (stderr: string, conversionStartTime: number, durationMs: number) => { const conversionDuration = Date.now() - conversionStartTime; @@ -50,7 +50,7 @@ export const extractProgressFromStderr = (stderr: string, conversionStartTime: n return undefined; }; -type ArgType = string[] | string | { args: string[], if: boolean }; +type ArgType = string[] | string | { args: string[]; if: boolean }; // Resolve conditional args // diff --git a/main/editor.js b/main/editor.js deleted file mode 100644 index 6b927b4ba..000000000 --- a/main/editor.js +++ /dev/null @@ -1,167 +0,0 @@ -'use strict'; - -const {BrowserWindow, dialog} = require('electron'); -const path = require('path'); -const fs = require('fs'); -const EventEmitter = require('events'); -const pify = require('pify'); -const {ipcMain: ipc} = require('electron-better-ipc'); -const {is} = require('electron-util'); - -const getFps = require('./utils/fps').default; -const loadRoute = require('./utils/routes'); -const {generateTimestampedName} = require('./utils/timestamped-name'); -const KapWindow = require('./kap-window').default; -const {Video} = require('./video'); - -const editors = new Map(); -let allOptions; -const OPTIONS_BAR_HEIGHT = 48; -const VIDEO_ASPECT = 9 / 16; -const MIN_VIDEO_WIDTH = 900; -const MIN_VIDEO_HEIGHT = MIN_VIDEO_WIDTH * VIDEO_ASPECT; -const MIN_WINDOW_HEIGHT = MIN_VIDEO_HEIGHT + OPTIONS_BAR_HEIGHT; -const editorEmitter = new EventEmitter(); -const editorsWithNotSavedDialogs = new Map(); - -const getEditorName = (filePath, isNewRecording) => isNewRecording ? generateTimestampedName() : path.basename(filePath); - -const openEditorWindow = async ( - filePath, - { - recordedFps, - isNewRecording, - originalFilePath, - recordingName - } = {} -) => { - if (editors.has(filePath)) { - editors.get(filePath).show(); - return; - } - - const fps = recordedFps || await getFps(filePath); - const title = recordingName || getEditorName(originalFilePath || filePath, isNewRecording); - const video = new Video({filePath, fps, title}); - - console.log(MIN_VIDEO_HEIGHT); - console.log(MIN_VIDEO_WIDTH); - - const editorKapWindow = new KapWindow({ - title, - // minWidth: MIN_VIDEO_WIDTH, - // minHeight: MIN_WINDOW_HEIGHT, - width: MIN_VIDEO_WIDTH, - height: MIN_WINDOW_HEIGHT, - webPreferences: { - nodeIntegration: true, - webSecurity: !is.development // Disable webSecurity in dev to load video over file:// protocol while serving over insecure http, this is not needed in production where we use file:// protocol for html serving. - }, - frame: false, - transparent: true, - vibrancy: 'window', - route: 'editor2', - initialState: { - filePath, - fps, - originalFilePath, - isNewRecording, - recordingName, - title - } - }); - - const editorWindow = editorKapWindow.browserWindow; - - editors.set(filePath, editorWindow); - - // loadRoute(editorWindow, 'editor2'); - - if (isNewRecording) { - editorWindow.setDocumentEdited(true); - editorWindow.on('close', event => { - editorsWithNotSavedDialogs.set(filePath, true); - const buttonIndex = dialog.showMessageBoxSync(editorWindow, { - type: 'question', - buttons: [ - 'Discard', - 'Cancel' - ], - defaultId: 0, - cancelId: 1, - message: 'Are you sure that you want to discard this recording?', - detail: 'You will no longer be able to edit and export the original recording.' - }); - - if (buttonIndex === 1) { - event.preventDefault(); - } - - editorsWithNotSavedDialogs.delete(filePath); - }); - } - - editorWindow.on('closed', () => { - editors.delete(filePath); - }); - - editorWindow.on('blur', () => { - editorEmitter.emit('blur'); - ipc.callRenderer(editorWindow, 'blur'); - }); - editorWindow.on('focus', () => { - editorEmitter.emit('focus'); - ipc.callRenderer(editorWindow, 'focus'); - }); - - // editorWindow.webContents.on('did-finish-load', async () => { - // ipc.callRenderer(editorWindow, 'kap-window-args', {filePath}); - // ipc.callRenderer(editorWindow, 'export-options', allOptions); - // await ipc.callRenderer(editorWindow, 'file', { - // filePath, - // fps, - // originalFilePath, - // isNewRecording, - // recordingName, - // title - // }); - // editorWindow.show(); - // }); -}; - -const setOptions = options => { - allOptions = options; -}; - -const getEditors = () => editors.values(); - -const getEditor = path => editors.get(path); - -ipc.answerRenderer('save-original', async ({inputPath}) => { - const {filePath} = await dialog.showSaveDialog(BrowserWindow.getFocusedWindow(), { - defaultPath: generateTimestampedName('Kapture', '.mp4') - }); - - if (filePath) { - await pify(fs.copyFile)(inputPath, filePath, fs.constants.COPYFILE_FICLONE); - } -}); - -const checkForAnyBlockingEditors = () => { - if (editorsWithNotSavedDialogs.size > 0) { - const [path] = editorsWithNotSavedDialogs.keys(); - editors.get(path).focus(); - return true; - } - - return false; -}; - -module.exports = { - openEditorWindow, - setOptions, - getEditors, - getEditor, - editorEmitter, - checkForAnyBlockingEditors -}; diff --git a/main/export-list.js b/main/export-list.js deleted file mode 100644 index b9b56e234..000000000 --- a/main/export-list.js +++ /dev/null @@ -1,295 +0,0 @@ -/* eslint-disable array-element-newline */ -'use strict'; -const {dialog, BrowserWindow, app, Notification} = require('electron'); -const fs = require('fs'); -const {dirname} = require('path'); -const {ipcMain: ipc} = require('electron-better-ipc'); -const base64Img = require('base64-img'); -const tmp = require('tmp'); -const ffmpeg = require('@ffmpeg-installer/ffmpeg'); -const util = require('electron-util'); -const execa = require('execa'); -const makeDir = require('make-dir'); - -const settings = require('./common/settings').default; -const {track} = require('./common/analytics'); -const {openPrefsWindow} = require('./preferences'); -const {getExportsWindow, openExportsWindow} = require('./exports'); -const {openEditorWindow} = require('./editor'); -const {toggleExportMenuItem} = require('./menus'); -const Export = require('./export'); -const {ensureDockIsShowingSync} = require('./utils/dock'); -const {generateTimestampedName} = require('./utils/timestamped-name'); - -const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path); -let lastSavedDirectory; - -const filterMap = new Map([ - ['mp4', [{name: 'Movies', extensions: ['mp4']}]], - ['webm', [{name: 'Movies', extensions: ['webm']}]], - ['gif', [{name: 'Images', extensions: ['gif']}]], - ['apng', [{name: 'Images', extensions: ['apng']}]] -]); - -const getPreview = async inputPath => { - const previewPath = tmp.tmpNameSync({postfix: '.jpg'}); - await execa(ffmpegPath, [ - '-ss', 0, - '-i', inputPath, - '-t', 1, - '-vframes', 1, - '-f', 'image2', - previewPath - ]); - - return previewPath; -}; - -const getDragIcon = async inputPath => { - const iconPath = tmp.tmpNameSync({postfix: '.jpg'}); - await execa(ffmpegPath, [ - '-i', inputPath, - // Scale the largest dimension to 64px maintaining aspect ratio - '-vf', 'scale=if(gte(iw\\,ih)\\,min(64\\,iw)\\,-2):if(lt(iw\\,ih)\\,min(64\\,ih)\\,-2)', - iconPath - ]); - - return iconPath; -}; - -const saveSnapshot = async ({inputPath, time}) => { - const {filePath: outputPath} = await dialog.showSaveDialog(BrowserWindow.getFocusedWindow(), { - defaultPath: generateTimestampedName('Snapshot', '.jpg') - }); - - if (outputPath) { - await execa(ffmpegPath, [ - '-i', inputPath, - '-ss', time, - '-vframes', 1, - outputPath - ]); - } -}; - -class ExportList { - constructor() { - this.exports = []; - this.queue = []; - } - - _startNext() { - if (this.queue.length === 0) { - return; - } - - this.currentExport = this.queue.shift(); - if (this.currentExport.canceled) { - delete this.currentExport; - this._startNext(); - return; - } - - (async () => { - try { - await this.currentExport.run(); - delete this.currentExport; - this._startNext(); - } catch (error) { - console.log(error); - this.currentExport.updateExport({ - status: 'failed', - text: 'Export failed', - error: {stack: error.stack, message: error.message} - }); - - const notification = new Notification({ - title: this.currentExport.pluginName, - body: error.message - }); - notification.show(); - - delete this.currentExport; - this._startNext(); - } - })(); - } - - cancelExport(createdAt) { - if (this.currentExport && this.currentExport.createdAt === createdAt) { - track('export/canceled/current'); - this.currentExport.cancel(); - } else { - const exportToCancel = this.exports.find(exp => exp.createdAt === createdAt); - if (exportToCancel) { - track('export/canceled/waiting'); - exportToCancel.cancel(); - } - } - } - - async addExport(options) { - options.exportOptions.loop = settings.get('loopExports'); - const newExport = new Export(options); - const createdAt = (new Date()).toISOString(); - - if (options.sharePlugin.pluginName === '_saveToDisk') { - const wasExportsWindowOpen = Boolean(getExportsWindow()); - const exportsWindow = await openExportsWindow(); - const kapturesDir = settings.get('kapturesDir'); - await makeDir(kapturesDir); - - const filters = filterMap.get(options.format); - - const {filePath} = await dialog.showSaveDialog(exportsWindow, { - title: newExport.defaultFileName, - defaultPath: `${lastSavedDirectory || kapturesDir}/${newExport.defaultFileName}`, - filters - }); - - if (filePath) { - newExport.context.targetFilePath = filePath; - lastSavedDirectory = dirname(filePath); - } else { - if (!wasExportsWindowOpen) { - exportsWindow.close(); - } - - return; - } - } else if (options.sharePlugin.pluginName === '_openWith') { - newExport.context.appUrl = options.openWithApp.url; - } - - if (!newExport.shareConfig.isConfigValid()) { - const result = dialog.showMessageBoxSync({ - type: 'error', - buttons: ['Configure', 'Cancel'], - defaultId: 0, - message: 'Error in plugin config', - detail: `Review the config for the "${options.sharePlugin.pluginName}" plugin to continue exporting`, - cancelId: 1 - }); - - track(`export/plugin/invalid/${options.sharePlugin.pluginName}`); - - if (result === 0) { - const prefsWindow = await openPrefsWindow(); - ipc.callRenderer(prefsWindow, 'open-plugin-config', options.sharePlugin.pluginName); - } - - return; - } - - toggleExportMenuItem(true); - - newExport.status = 'waiting'; - newExport.text = 'Waiting…'; - newExport.imagePath = await getPreview(options.inputPath); - newExport.image = base64Img.base64Sync(newExport.imagePath); - newExport.createdAt = createdAt; - newExport.originalFps = options.originalFps; - - callExportsWindow('update-export', {...newExport.data, createdAt}); - openExportsWindow(); - - newExport.updateExport = updates => { - if (newExport.canceled) { - return; - } - - for (const key in updates) { - if (updates[key] !== undefined) { - newExport[key] = updates[key]; - } - } - - callExportsWindow('update-export', {...newExport.data, createdAt}); - }; - - this.exports.push(newExport); - this.queue.push(newExport); - track(`export/queued/${this.queue.length}`); - - if (!this.currentExport) { - this._startNext(); - } - } - - getExports() { - return this.exports.map(exp => exp.data); - } - - getExport(createdAt) { - return this.exports.find(exp => exp.createdAt === createdAt); - } - - openExport(createdAt) { - track('export/history/opened/recording'); - const exp = this.getExport(createdAt); - if (exp) { - openEditorWindow(exp.previewPath, {recordedFps: exp.originalFps, originalFilePath: exp.inputPath}); - } - } -} - -let exportList; - -ipc.answerRenderer('get-exports', () => exportList.getExports()); - -ipc.answerRenderer('export', options => exportList.addExport(options)); - -ipc.answerRenderer('cancel-export', createdAt => exportList.cancelExport(createdAt)); - -ipc.answerRenderer('open-export', createdAt => exportList.openExport(createdAt)); - -ipc.answerRenderer('export-snapshot', saveSnapshot); - -ipc.on('drag-export', async (event, createdAt) => { - const exportItem = exportList.getExport(createdAt); - const file = exportItem && exportItem.data.filePath; - - if (file && fs.existsSync(file)) { - event.sender.startDrag({ - file, - icon: await getDragIcon(exportItem.imagePath) - }); - } -}); - -const callExportsWindow = (channel, data) => { - const exportsWindow = getExportsWindow(); - - if (exportsWindow) { - // TODO(karaggeorge): Investigate why `ipc.callRenderer(exportsWindow, channel, data);` is not working here. - exportsWindow.webContents.send(channel, data); - } -}; - -module.exports = () => { - exportList = new ExportList(); - - app.on('before-quit', event => { - if (exportList.currentExport) { - openExportsWindow(); - - ensureDockIsShowingSync(() => { - const buttonIndex = dialog.showMessageBoxSync({ - type: 'question', - buttons: [ - 'Continue', - 'Quit' - ], - defaultId: 0, - cancelId: 1, - message: 'Do you want to continue exporting?', - detail: 'Kap is currently exporting files. If you quit, the export task will be canceled.' - }); - - if (buttonIndex === 0) { - event.preventDefault(); - } - }); - } - }); -}; diff --git a/main/export-options.js b/main/export-options.js deleted file mode 100644 index bc6934428..000000000 --- a/main/export-options.js +++ /dev/null @@ -1,171 +0,0 @@ -// 'use strict'; - -// const Store = require('electron-store'); -// const {ipcMain: ipc} = require('electron-better-ipc'); - -// const plugins = require('./common/plugins'); -// const {converters} = require('./convert'); -// const {setOptions, getEditors} = require('./editor'); -// const {apps} = require('./plugins/built-in/open-with-plugin'); -// const {showError} = require('./utils/errors'); - -// const exportUsageHistory = new Store({ -// name: 'export-usage-history', -// defaults: { -// apng: {lastUsed: 1, plugins: {default: 1}}, -// webm: {lastUsed: 2, plugins: {default: 1}}, -// mp4: {lastUsed: 3, plugins: {default: 1}}, -// gif: {lastUsed: 4, plugins: {default: 1}}, -// av1: {lastUsed: 5, plugins: {default: 1}} -// } -// }); - -// const fpsUsageHistory = new Store({ -// name: 'fps-usage-history', -// schema: { -// apng: { -// type: 'number', -// minimum: 0, -// default: 60 -// }, -// webm: { -// type: 'number', -// minimum: 0, -// default: 60 -// }, -// mp4: { -// type: 'number', -// minimum: 0, -// default: 60 -// }, -// gif: { -// type: 'number', -// minimum: 0, -// default: 60 -// }, -// av1: { -// type: 'number', -// minimum: 0, -// default: 60 -// } -// } -// }); - -// const prettifyFormat = format => { -// const formats = new Map([ -// ['apng', 'APNG'], -// ['gif', 'GIF'], -// ['mp4', 'MP4 (H264)'], -// ['av1', 'MP4 (AV1)'], -// ['webm', 'WebM'] -// ]); - -// return formats.get(format); -// }; - -// const getEditOptions = () => { -// const installed = plugins.getEditPlugins(); - -// return installed.flatMap( -// plugin => plugin.editServices -// .filter(service => plugin.config.validServices.includes(service.title)) -// .map(service => ({ -// title: service.title, -// pluginName: plugin.name, -// pluginPath: plugin.pluginPath, -// hasConfig: Object.keys(service.config || {}).length > 0 -// })) -// ); -// }; - -// const getExportOptions = () => { -// const installed = plugins.getSharePlugins(); -// const builtIn = plugins.getBuiltIn(); - -// const options = []; -// for (const format of converters.keys()) { -// options.push({ -// format, -// prettyFormat: prettifyFormat(format), -// plugins: [] -// }); -// } - -// for (const json of [...installed, ...builtIn]) { -// if (!json.isCompatible) { -// continue; -// } - -// try { -// const plugin = require(json.pluginPath); - -// for (const service of plugin.shareServices) { -// for (const format of service.formats) { -// options.find(option => option.format === format).plugins.push({ -// title: service.title, -// pluginName: json.name, -// pluginPath: json.pluginPath, -// apps: json.name === '_openWith' ? apps.get(format) : undefined -// }); -// } -// } -// } catch (error) { -// showError(error, {title: `Something went wrong while loading “${json.name}”`, plugin: json}); -// const Sentry = require('./utils/sentry').default; -// Sentry.captureException(error); -// } -// } - -// const sortFunc = (a, b) => b.lastUsed - a.lastUsed; - -// for (const option of options) { -// const {lastUsed, plugins} = exportUsageHistory.get(option.format); -// option.lastUsed = lastUsed; -// option.plugins = option.plugins.map(plugin => ({...plugin, lastUsed: plugins[plugin.pluginName] || 0})).sort(sortFunc); -// } - -// return options.sort(sortFunc); -// }; - -// const updateExportOptions = () => { -// const editors = getEditors(); -// const exportOptions = getExportOptions(); -// const editOptions = getEditOptions(); -// for (const editor of editors) { -// ipc.callRenderer(editor, 'export-options', {exportOptions, editOptions, fps: fpsUsageHistory.store}); -// } - -// setOptions({exportOptions, editOptions, fps: fpsUsageHistory.store}); -// }; - -// plugins.setUpdateExportOptions(updateExportOptions); - -// ipc.answerRenderer('update-usage', ({format, plugin, fps}) => { -// if (plugin) { -// const usage = exportUsageHistory.get(format); -// const now = Date.now(); - -// usage.plugins[plugin] = now; -// usage.lastUsed = now; -// exportUsageHistory.set(format, usage); -// } - -// fpsUsageHistory.set(format, fps); -// updateExportOptions(); -// }); - -// ipc.answerRenderer('refresh-usage', updateExportOptions); - -// const initializeExportOptions = () => { -// setOptions({ -// exportOptions: getExportOptions(), -// editOptions: getEditOptions(), -// fps: fpsUsageHistory.store -// }); -// }; - -// module.exports = { -// getExportOptions, -// updateExportOptions, -// initializeExportOptions -// }; diff --git a/main/export.ts b/main/export.ts index 66e760d8b..65140b7b1 100644 --- a/main/export.ts +++ b/main/export.ts @@ -1,3 +1,4 @@ +import {ipcMain} from 'electron'; import {EventEmitter} from 'events'; import PCancelable, {OnCancelFunction} from 'p-cancelable'; import Conversion from './conversion'; @@ -5,25 +6,62 @@ import {InstalledPlugin} from './plugins/plugin'; import {ShareService} from './plugins/service'; import {ShareServiceContext} from './plugins/service-context'; import {prettifyFormat} from './utils/formats'; +import {ipcMain as ipc} from 'electron-better-ipc'; +import {setExportMenuItemState} from './menus/utils'; export interface ExportOptions { plugin: InstalledPlugin; service: ShareService; - extras: object; + extras: Record; } export default class Export extends EventEmitter { + static exportsMap = new Map(); + + static get all() { + return [...this.exportsMap.values()]; + } + conversion: Conversion; options: ExportOptions; context: ShareServiceContext; + createdAt: number = Date.now(); + status = 'waiting'; + text = ''; + percentage = 0; + + private process?: PCancelable; + + private disableOutputActions = false; + + private readonly _start = PCancelable.fn(async (onCancel: OnCancelFunction) => { + const action = this.options.service.action(this.context) as any; + + onCancel(() => { + if (action.cancel && typeof action.cancel === 'function') { + action.cancel(); + } - private process?: PCancelable + this.context.isCanceled = true; + }); + + try { + await action; + this.emit('finished'); + } catch (error) { + if (!error.isCanceled) { + this.emit('error', error); + } + } + }); constructor(conversion: Conversion, options: ExportOptions) { super(); this.conversion = conversion; this.options = options; + this.conversion.video.generatePreviewImage(); + this.context = new ShareServiceContext({ plugin: options.plugin, format: conversion.format, @@ -38,43 +76,77 @@ export default class Export extends EventEmitter { for (const [key, value] of Object.entries(options.extras)) { (this.context as any)[key] = value; } - } - start = () => { - this.process = this._start(); - return this.process; + Export.addExport(this); + setExportMenuItemState(true); } - private _start = PCancelable.fn(async (onCancel: OnCancelFunction) => { - const action = this.options.service.action(this.context) as any; + static addExport = (newExport: Export) => { + Export.exportsMap.set(newExport.createdAt, newExport); + }; - onCancel(() => { - if (action.cancel && typeof action.cancel === 'function') { - action.cancel(); - } - this.context.isCanceled = true; - }); + get data() { + return { + defaultFileName: this.conversion.title, + status: this.status, + text: this.text, + percentage: this.percentage ?? 0, + image: this.conversion.video.previewImage?.data, + createdAt: this.createdAt, + filePath: this.conversion.convertedFilePath, + error: this.conversion.error, + disableOutputActions: this.disableOutputActions + }; + } - try { - console.log('In here plz'); - await action; - console.log('Donezo!'); - this.emit('finished'); - } catch (error) { - console.error('GOT ERROR PLZ', error); - if (!error.isCanceled) { - this.emit('error', error); - } - } - }); + disableActions = () => { + this.disableOutputActions = true; + }; + + start = async () => { + this.process = this._start(); + return this.process; + }; onProgress = (text: string, percentage: number) => { - console.log('Prgressing!'); this.emit('progress', {text, percentage}); - } + }; onCancel = () => { this.process?.cancel(); this.emit('canceled'); - } + }; } + +export const setUpExportsListeners = () => { + ipc.answerRenderer('get-exports', () => Export.all.map(exp => exp.data)); + ipc.answerRenderer('cancel-export', (createdAt: number) => { + Export.exportsMap.get(createdAt)?.onCancel(); + }); + + ipc.answerRenderer('open-export', (createdAt: number) => { + Export.exportsMap.get(createdAt)?.conversion?.video?.openEditorWindow?.(); + }); + + ipcMain.on('drag-export', async (event: any, createdAt: number) => { + const conversion = Export.exportsMap.get(createdAt)?.conversion; + + if (conversion && (await conversion.filePathExists())) { + event.sender.startDrag({ + file: conversion.convertedFilePath, + icon: await conversion.video.getDragIcon(conversion.options) + }); + } + }); + + ipcMain.on('drag-conversion', async (event: any, id: string) => { + const conversion = Conversion.fromId(id); + + if (conversion && (await conversion.filePathExists())) { + event.sender.startDrag({ + file: conversion.convertedFilePath, + icon: await conversion.video.getDragIcon(conversion.options) + }); + } + }); +}; diff --git a/main/export1.js b/main/export1.js deleted file mode 100644 index 510ccf7ce..000000000 --- a/main/export1.js +++ /dev/null @@ -1,157 +0,0 @@ -'use strict'; - -const path = require('path'); -const PCancelable = require('p-cancelable'); - -const {track} = require('./common/analytics'); -const {convertTo} = require('./convert'); -const {ShareServiceContext} = require('./service-context'); -const {getFormatExtension} = require('./common/constants'); -const PluginConfig = require('./utils/plugin-config'); -const {generateTimestampedName} = require('./utils/timestamped-name'); - -class Export { - constructor(options) { - this.exportOptions = options.exportOptions; - this.inputPath = options.inputPath; - this.previewPath = options.previewPath; - - this.sharePluginName = options.sharePlugin.pluginName; - this.sharePlugin = require(options.sharePlugin.pluginPath); - this.shareService = this.sharePlugin.shareServices.find(shareService => shareService.title === options.sharePlugin.serviceTitle); - - this.shareConfig = new PluginConfig({ - allServices: [this.shareService], - name: this.sharePluginName - }); - - if (options.editPlugin) { - this.editPluginName = options.editPlugin.pluginName; - this.editPlugin = require(options.editPlugin.pluginPath); - this.editService = this.editPlugin.editServices.find(editService => editService.title === options.editPlugin.title); - - this.editConfig = new PluginConfig({ - allServices: [this.editService], - name: this.editPluginName - }); - } - - this.format = options.format; - this.image = ''; - this.isSaveFileService = options.sharePlugin.pluginName === '_saveToDisk'; - this.disableOutputActions = false; - - const fileName = options.recordingName || (options.isNewRecording ? generateTimestampedName('Kapture') : path.parse(this.inputPath).name); - this.defaultFileName = `${fileName}.${getFormatExtension(this.format)}`; - - this.context = new ShareServiceContext({ - _isBuiltin: options.sharePlugin.pluginName.startsWith('_'), - format: this.format, - defaultFileName: this.defaultFileName, - config: this.shareConfig, - onCancel: this.cancel.bind(this), - onProgress: this.setProgress.bind(this), - convert: this.convert.bind(this), - pluginName: this.sharePluginName - }); - - this.run = this.run.bind(this); - - this.setProgress = this.setProgress.bind(this); - this.cancel = this.cancel.bind(this); - } - - get data() { - return { - defaultFileName: this.isSaveFileService ? path.basename(this.context.targetFilePath) : this.defaultFileName, - text: this.text, - status: this.status, - percentage: this.percentage || 0, - image: this.image, - createdAt: this.createdAt, - filePath: this.filePath && (this.isSaveFileService ? this.context.targetFilePath : this.filePath), - error: this.error, - disableOutputActions: this.disableOutputActions - }; - } - - run() { - track(`export/started/${this.sharePluginName}`); - track(`plugins/used/share/${this.sharePluginName}`); - return new PCancelable(async (resolve, reject, onCancel) => { - this.resolve = resolve; - this.reject = reject; - - onCancel(() => this.context.clear()); - try { - await this.shareService.action(this.context); - if (!this.canceled) { - this.updateExport({ - text: 'Export completed', - status: 'completed', - percentage: undefined - }); - } - - resolve(); - } catch (error) { - reject(error); - } - }); - } - - cancel() { - this.updateExport({ - text: 'Export canceled', - status: 'canceled', - percentage: undefined - }); - this.canceled = true; - - if (this.resolve) { - this.context.clear(); - - if (this.convertProcess) { - this.convertProcess.cancel(); - } - - this.resolve(); - } - } - - setProgress(text, percentage = 0) { - this.updateExport({ - text, percentage, - status: 'processing' - }); - } - - async convert({fileType} = {}) { - if (fileType) { - this.disableOutputActions = true; - } - - this.convertProcess = convertTo( - { - ...this.exportOptions, - defaultFileName: fileType ? `${path.parse(this.defaultFileName).name}.${getFormatExtension(fileType)}` : this.defaultFileName, - inputPath: this.inputPath, - onProgress: (percentage, estimate, action = 'Converting') => this.setProgress(estimate ? `${action} — ${estimate} remaining` : `${action}…`, percentage), - editService: this.editService ? { - service: this.editService, - config: this.editConfig, - cancel: this.cancel, - setProgress: this.setProgress, - pluginName: this.editPluginName - } : undefined - }, - fileType || this.format - ); - - this.filePath = await this.convertProcess; - this.resolve(); - return this.filePath; - } -} - -module.exports = Export; diff --git a/main/global-accelerators.js b/main/global-accelerators.ts similarity index 64% rename from main/global-accelerators.js rename to main/global-accelerators.ts index 43003efdd..d7970beb8 100644 --- a/main/global-accelerators.js +++ b/main/global-accelerators.ts @@ -1,26 +1,25 @@ -'use strict'; -const {globalShortcut} = require('electron'); -const {ipcMain: ipc} = require('electron-better-ipc'); -const settings = require('./common/settings').default; -const {openCropperWindow, isCropperOpen} = require('./cropper'); +import {globalShortcut} from 'electron'; +import {ipcMain as ipc} from 'electron-better-ipc'; +import {settings} from './common/settings'; +import {windowManager} from './windows/manager'; const openCropper = () => { - if (!isCropperOpen()) { - openCropperWindow(); + if (!windowManager.cropper?.isOpen()) { + windowManager.cropper?.open(); } }; // All settings that should be loaded and handled as global accelerators -const handlers = new Map([ +const handlers = new Map void>([ ['triggerCropper', openCropper] ]); // If no action is passed, it resets -const setCropperShortcutAction = (action = openCropper) => { +export const setCropperShortcutAction = (action = openCropper) => { if (settings.get('enableShortcuts') && settings.get('shortcuts.triggerCropper')) { handlers.set('cropperShortcut', action); - const shortcut = settings.get('shortcuts.triggerCropper'); + const shortcut = settings.get('shortcuts.triggerCropper'); if (globalShortcut.isRegistered(shortcut)) { globalShortcut.unregister(shortcut); } @@ -29,7 +28,7 @@ const setCropperShortcutAction = (action = openCropper) => { } }; -const registerShortcut = (shortcut, action) => { +const registerShortcut = (shortcut: string, action: () => void) => { try { globalShortcut.register(shortcut, action); } catch (error) { @@ -40,7 +39,7 @@ const registerShortcut = (shortcut, action) => { const registerFromStore = () => { if (settings.get('enableShortcuts')) { for (const [setting, action] of handlers.entries()) { - const shortcut = settings.get(`shortcuts.${setting}`); + const shortcut = settings.get(`shortcuts.${setting}`); if (shortcut) { registerShortcut(shortcut, action); } @@ -50,9 +49,9 @@ const registerFromStore = () => { } }; -const initializeGlobalAccelerators = () => { +export const initializeGlobalAccelerators = () => { ipc.answerRenderer('update-shortcut', ({setting, shortcut}) => { - const oldShortcut = settings.get(`shortcuts.${setting}`); + const oldShortcut = settings.get(`shortcuts.${setting}`); try { if (oldShortcut && oldShortcut !== shortcut && globalShortcut.isRegistered(oldShortcut)) { @@ -63,11 +62,13 @@ const initializeGlobalAccelerators = () => { } finally { if (shortcut && shortcut !== oldShortcut) { settings.set(`shortcuts.${setting}`, shortcut); + const handler = handlers.get(setting); - if (settings.get('enableShortcuts')) { - registerShortcut(shortcut, handlers.get(setting)); + if (settings.get('enableShortcuts') && handler) { + registerShortcut(shortcut, handler); } } else if (!shortcut) { + // @ts-expect-error settings.delete(`shortcuts.${setting}`); } } @@ -84,8 +85,3 @@ const initializeGlobalAccelerators = () => { // Register keyboard shortcuts from store registerFromStore(); }; - -module.exports = { - initializeGlobalAccelerators, - setCropperShortcutAction -}; diff --git a/main/index.js b/main/index.ts similarity index 55% rename from main/index.js rename to main/index.ts index c2ee593bf..591ce9f9c 100644 --- a/main/index.js +++ b/main/index.ts @@ -1,34 +1,30 @@ -'use strict'; +import {app} from 'electron'; +import {is, enforceMacOSAppLocation} from 'electron-util'; +import log from 'electron-log'; +import {autoUpdater} from 'electron-updater'; +import toMilliseconds from '@sindresorhus/to-milliseconds'; + +import './windows/load'; +import './utils/sentry'; + +import {settings} from './common/settings'; +import {plugins} from './plugins'; +import {initializeTray} from './tray'; +import {initializeDevices} from './utils/devices'; +import {setUpConversionListeners} from './conversion'; +import {initializeAnalytics, track} from './common/analytics'; +import {initializeGlobalAccelerators} from './global-accelerators'; +import {openFiles} from './utils/open-files'; +import {hasMicrophoneAccess, ensureScreenCapturePermissions} from './common/system-permissions'; +import {handleDeepLink} from './utils/deep-linking'; +import {hasActiveRecording, cleanPastRecordings} from './recording-history'; +import {setupRemoteStates} from './remote-states'; +import {setUpExportsListeners} from './export'; +import {windowManager} from './windows/manager'; -const {app} = require('electron'); const prepareNext = require('electron-next'); -const {is, enforceMacOSAppLocation} = require('electron-util'); -const log = require('electron-log'); -const {autoUpdater} = require('electron-updater'); -const toMilliseconds = require('@sindresorhus/to-milliseconds'); - -const settings = require('./common/settings').default; - -require('./utils/sentry').default; -require('./utils/errors').setupErrorHandling(); - -const {initializeTray} = require('./tray'); -const {setupConversionHook} = require('./conversion'); -const {initializeAnalytics} = require('./common/analytics'); -const initializeExportList = require('./export-list'); -const {openCropperWindow, isCropperOpen, closeAllCroppers} = require('./cropper'); -const {track} = require('./common/analytics'); -const plugins = require('./common/plugins'); -const {initializeGlobalAccelerators} = require('./global-accelerators'); -const {setApplicationMenu} = require('./menus'); -const openFiles = require('./utils/open-files'); -const {hasMicrophoneAccess, ensureScreenCapturePermissions} = require('./common/system-permissions'); -const {handleDeepLink} = require('./utils/deep-linking'); -const {hasActiveRecording, cleanPastRecordings} = require('./recording-history'); - -const {setupRemoteStates} = require('./remote-states'); - -const filesToOpen = []; + +const filesToOpen: string[] = []; app.commandLine.appendSwitch('--enable-features', 'OverlayScrollbar'); @@ -46,7 +42,6 @@ app.on('open-file', (event, path) => { const initializePlugins = async () => { if (!is.development) { try { - await plugins.prune(); await plugins.upgrade(); } catch (error) { console.log(error); @@ -63,26 +58,29 @@ const checkForUpdates = () => { try { await autoUpdater.checkForUpdates(); } catch (error) { - autoUpdater.logger.error(error); + autoUpdater.logger?.error(error); } }; // For auto-update debugging in Console.app autoUpdater.logger = log; + // @ts-expect-error autoUpdater.logger.transports.file.level = 'info'; setInterval(checkForUpdates, toMilliseconds({hours: 1})); checkForUpdates(); + return true; }; // Prepare the renderer once the app is ready (async () => { await app.whenReady(); + require('./utils/errors').setupErrorHandling(); // Initialize remote states setupRemoteStates(); - setupConversionHook(); + setUpConversionListeners(); app.dock.hide(); app.setAboutPanelOptions({copyright: 'Copyright © Wulkano'}); @@ -94,13 +92,11 @@ const checkForUpdates = () => { // Ensure all plugins are up to date initializePlugins(); - + initializeDevices(); initializeAnalytics(); initializeTray(); - initializeExportList(); initializeGlobalAccelerators(); - // initializeExportOptions(); - setApplicationMenu(); + setUpExportsListeners(); if (!app.isDefaultProtocolClient('kap')) { app.setAsDefaultProtocolClient('kap'); @@ -116,25 +112,17 @@ const checkForUpdates = () => { ensureScreenCapturePermissions() && (!settings.get('recordAudio') || hasMicrophoneAccess()) ) { - // openCropperWindow(); + windowManager.cropper?.open(); } checkForUpdates(); })(); -app.on('before-quit', closeAllCroppers); - -app.on('window-all-closed', event => { +app.on('window-all-closed', (event: any) => { app.dock.hide(); event.preventDefault(); }); -app.on('browser-window-created', () => { - if (!isCropperOpen()) { - app.dock.show(); - } -}); - app.on('will-finish-launching', () => { app.on('open-url', (event, url) => { event.preventDefault(); diff --git a/main/menus.js b/main/menus.js deleted file mode 100644 index a1290ce43..000000000 --- a/main/menus.js +++ /dev/null @@ -1,277 +0,0 @@ -'use strict'; - -const {Menu, app, dialog, BrowserWindow} = require('electron'); -const {openNewGitHubIssue, appMenu} = require('electron-util'); -const {ipcMain: ipc} = require('electron-better-ipc'); -const delay = require('delay'); -const macosRelease = require('macos-release'); - -const {supportedVideoExtensions, defaultInputDeviceId} = require('./common/constants'); -const settings = require('./common/settings').default; -const {hasMicrophoneAccess} = require('./common/system-permissions'); -const {getAudioDevices, getDefaultInputDevice} = require('./utils/devices'); -const {ensureDockIsShowing} = require('./utils/dock'); -const {openPrefsWindow} = require('./preferences'); -const {openExportsWindow} = require('./exports'); -const {closeAllCroppers} = require('./cropper'); -const {editorEmitter} = require('./editor'); -const openFiles = require('./utils/open-files'); - -const release = macosRelease(); -const issueBody = ` - - -**macOS version:** ${release.name} (${release.version}) -**Kap version:** ${app.getVersion()} - -#### Steps to reproduce - -#### Current behavior - -#### Expected behavior - -#### Workaround - - -`; - -const openFileItem = { - label: 'Open Video…', - accelerator: 'Command+O', - click: async () => { - closeAllCroppers(); - - await delay(200); - - await ensureDockIsShowing(async () => { - const {canceled, filePaths} = await dialog.showOpenDialog({ - filters: [{name: 'Videos', extensions: supportedVideoExtensions}], - properties: ['openFile', 'multiSelections'] - }); - - if (!canceled && filePaths) { - openFiles(...filePaths); - } - }); - } -}; - -const sendFeedbackItem = { - label: 'Send Feedback…', - click() { - openNewGitHubIssue({ - user: 'wulkano', - repo: 'kap', - body: issueBody - }); - } -}; - -const aboutItem = { - label: `About ${app.name}`, - click: () => { - closeAllCroppers(); - ensureDockIsShowing(() => { - app.showAboutPanel(); - }); - } -}; - -let isExportsItemEnabled = false; - -const getExportHistoryItem = () => ({ - label: 'Export History', - click: openExportsWindow, - enabled: isExportsItemEnabled, - id: 'exports' -}); - -const preferencesItem = { - label: 'Preferences…', - accelerator: 'Command+,', - click: () => openPrefsWindow() -}; - -let pluginsItems = []; - -const getPluginsItem = () => ({ - id: 'plugins', - label: 'Plugins', - submenu: pluginsItems, - visible: pluginsItems.length > 0 -}); - -const getMicrophoneItem = async () => { - const devices = await getAudioDevices(); - const isRecordAudioEnabled = settings.get('recordAudio'); - const currentDefaultDevice = getDefaultInputDevice(); - - let audioInputDeviceId = settings.get('audioInputDeviceId'); - if (!devices.some(device => device.id === audioInputDeviceId)) { - settings.set('audioInputDeviceId', defaultInputDeviceId); - audioInputDeviceId = defaultInputDeviceId; - } - - return { - id: 'devices', - label: 'Microphone', - submenu: [ - { - label: 'None', - type: 'checkbox', - checked: !isRecordAudioEnabled, - click: () => settings.set('recordAudio', false) - }, - ...[ - {name: `System Default${currentDefaultDevice ? ` (${currentDefaultDevice.name})` : ''}`, id: defaultInputDeviceId}, - ...devices - ].map(device => ({ - label: device.name, - type: 'checkbox', - checked: isRecordAudioEnabled && audioInputDeviceId === device.id, - click: () => { - settings.set('recordAudio', true); - settings.set('audioInputDeviceId', device.id); - } - })) - ], - visible: hasMicrophoneAccess() - }; -}; - -const getCogMenuTemplate = async () => [ - aboutItem, - { - type: 'separator' - }, - preferencesItem, - { - type: 'separator' - }, - getPluginsItem(), - await getMicrophoneItem(), - { - type: 'separator' - }, - openFileItem, - getExportHistoryItem(), - { - type: 'separator' - }, - sendFeedbackItem, - { - type: 'separator' - }, - { - role: 'quit', - accelerator: 'Command+Q' - } -]; - -const appMenuItem = appMenu([preferencesItem]); - -appMenuItem.submenu[0] = aboutItem; - -const appMenuTemplate = [ - appMenuItem, - { - role: 'fileMenu', - submenu: [ - openFileItem, - { - type: 'separator' - }, - { - label: 'Save Original…', - id: 'saveOriginal', - accelerator: 'Command+S', - click: () => { - ipc.callRenderer(BrowserWindow.getFocusedWindow(), 'save-original'); - } - }, - { - type: 'separator' - }, - { - role: 'close' - } - ] - }, - { - role: 'editMenu' - }, - { - role: 'windowMenu', - submenu: [ - { - role: 'minimize' - }, - { - role: 'zoom' - }, - { - type: 'separator' - }, - getExportHistoryItem(), - { - type: 'separator' - }, - { - role: 'front' - } - ] - }, - { - label: 'Help', - role: 'help', - submenu: [sendFeedbackItem] - } -]; - -const refreshRecordPluginItems = services => { - pluginsItems = services.map(service => ({ - label: service.title, - type: 'checkbox', - checked: service.isEnabled, - click: service.toggleEnabled - })); -}; - -const appMenu_ = Menu.buildFromTemplate(appMenuTemplate); -const appExportsItem = appMenu_.getMenuItemById('exports'); -const appSaveOriginalItem = appMenu_.getMenuItemById('saveOriginal'); - -const toggleExportMenuItem = enabled => { - isExportsItemEnabled = enabled; - appExportsItem.enabled = enabled; -}; - -const setAppMenu = () => { - Menu.setApplicationMenu(appMenu_); -}; - -editorEmitter.on('blur', () => { - appSaveOriginalItem.visible = false; -}); - -editorEmitter.on('focus', () => { - appSaveOriginalItem.visible = true; -}); - -const getCogMenu = async () => Menu.buildFromTemplate(await getCogMenuTemplate()); - -module.exports = { - getCogMenu, - toggleExportMenuItem, - setApplicationMenu: setAppMenu, - refreshRecordPluginItems -}; diff --git a/main/menus/application.ts b/main/menus/application.ts new file mode 100644 index 000000000..27b87b210 --- /dev/null +++ b/main/menus/application.ts @@ -0,0 +1,70 @@ +import {appMenu} from 'electron-util'; +import {getAboutMenuItem, getExportHistoryMenuItem, getOpenFileMenuItem, getPreferencesMenuItem, getSendFeedbackMenuItem} from './common'; +import {MenuItemId, MenuOptions} from './utils'; + +const getAppMenuItem = () => { + const appMenuItem = appMenu([getPreferencesMenuItem()]); + + // @ts-expect-error + appMenuItem.submenu[0] = getAboutMenuItem(); + return {...appMenuItem, id: MenuItemId.app}; +}; + +// eslint-disable-next-line unicorn/prevent-abbreviations +export const defaultApplicationMenu = (): MenuOptions => [ + getAppMenuItem(), + { + role: 'fileMenu', + id: MenuItemId.file, + submenu: [ + getOpenFileMenuItem(), + { + type: 'separator' + }, + { + role: 'close' + } + ] + }, + { + role: 'editMenu', + id: MenuItemId.edit + }, + { + role: 'windowMenu', + id: MenuItemId.window, + submenu: [ + { + role: 'minimize' + }, + { + role: 'zoom' + }, + { + type: 'separator' + }, + getExportHistoryMenuItem(), + { + type: 'separator' + }, + { + role: 'front' + } + ] + }, + { + id: MenuItemId.help, + label: 'Help', + role: 'help', + submenu: [getSendFeedbackMenuItem()] + } +]; + +// eslint-disable-next-line unicorn/prevent-abbreviations +export const customApplicationMenu = (modifier: (defaultMenu: ReturnType) => void) => { + const menu = defaultApplicationMenu(); + modifier(menu); + return menu; +}; + +export type MenuModifier = Parameters[0]; diff --git a/main/menus/cog.ts b/main/menus/cog.ts new file mode 100644 index 000000000..4c80bef1f --- /dev/null +++ b/main/menus/cog.ts @@ -0,0 +1,101 @@ +import {Menu} from 'electron'; +import {MenuItemId, MenuOptions} from './utils'; +import {getAboutMenuItem, getExportHistoryMenuItem, getOpenFileMenuItem, getPreferencesMenuItem, getSendFeedbackMenuItem} from './common'; +import {plugins} from '../plugins'; +import {getAudioDevices, getDefaultInputDevice} from '../utils/devices'; +import {settings} from '../common/settings'; +import {defaultInputDeviceId} from '../common/constants'; +import {hasMicrophoneAccess} from '../common/system-permissions'; + +const getCogMenuTemplate = async (): Promise => [ + getAboutMenuItem(), + { + type: 'separator' + }, + getPreferencesMenuItem(), + { + type: 'separator' + }, + getPluginsItem(), + await getMicrophoneItem(), + { + type: 'separator' + }, + getOpenFileMenuItem(), + getExportHistoryMenuItem(), + { + type: 'separator' + }, + getSendFeedbackMenuItem(), + { + type: 'separator' + }, + { + role: 'quit', + accelerator: 'Command+Q' + } +]; + +const getPluginsItem = (): MenuOptions[number] => { + const items = plugins.recordingPlugins.flatMap(plugin => + plugin.recordServicesWithStatus.map(service => ({ + label: service.title, + type: 'checkbox' as const, + checked: service.isEnabled, + click: async () => service.setEnabled(!service.isEnabled) + })) + ); + + return { + id: MenuItemId.plugins, + label: 'Plugins', + submenu: items, + visible: items.length > 0 + }; +}; + +const getMicrophoneItem = async (): Promise => { + const devices = await getAudioDevices(); + const isRecordAudioEnabled = settings.get('recordAudio'); + const currentDefaultDevice = getDefaultInputDevice(); + + let audioInputDeviceId = settings.get('audioInputDeviceId'); + if (!devices.some(device => device.id === audioInputDeviceId)) { + settings.set('audioInputDeviceId', defaultInputDeviceId); + audioInputDeviceId = defaultInputDeviceId; + } + + return { + id: MenuItemId.audioDevices, + label: 'Microphone', + submenu: [ + { + label: 'None', + type: 'checkbox', + checked: !isRecordAudioEnabled, + click: () => { + settings.set('recordAudio', false); + } + }, + ...[ + {name: `System Default${currentDefaultDevice ? ` (${currentDefaultDevice.name})` : ''}`, id: defaultInputDeviceId}, + ...devices + ].map(device => ({ + label: device.name, + type: 'checkbox' as const, + checked: isRecordAudioEnabled && (audioInputDeviceId === device.id), + click: () => { + settings.set('recordAudio', true); + settings.set('audioInputDeviceId', device.id); + } + })) + ], + visible: hasMicrophoneAccess() + }; +}; + +export const getCogMenu = async () => { + return Menu.buildFromTemplate( + await getCogMenuTemplate() + ); +}; diff --git a/main/menus/common.ts b/main/menus/common.ts new file mode 100644 index 000000000..38f717b46 --- /dev/null +++ b/main/menus/common.ts @@ -0,0 +1,97 @@ +import delay from 'delay'; +import {app, dialog} from 'electron'; +import {openNewGitHubIssue} from 'electron-util'; +import macosRelease from 'macos-release'; +import {supportedVideoExtensions} from '../common/constants'; +import {ensureDockIsShowing} from '../utils/dock'; +import {getCurrentMenuItem, MenuItemId} from './utils'; +import {openFiles} from '../utils/open-files'; +import {windowManager} from '../windows/manager'; + +export const getPreferencesMenuItem = () => ({ + id: MenuItemId.preferences, + label: 'Preferences…', + accelerator: 'Command+,', + click: () => windowManager.preferences?.open() +}); + +export const getAboutMenuItem = () => ({ + id: MenuItemId.about, + label: `About ${app.name}`, + click: () => { + windowManager.cropper?.close(); + ensureDockIsShowing(() => { + app.showAboutPanel(); + }); + } +}); + +export const getOpenFileMenuItem = () => ({ + id: MenuItemId.openVideo, + label: 'Open Video…', + accelerator: 'Command+O', + click: async () => { + windowManager.cropper?.close(); + + await delay(200); + + await ensureDockIsShowing(async () => { + const {canceled, filePaths} = await dialog.showOpenDialog({ + filters: [{name: 'Videos', extensions: supportedVideoExtensions}], + properties: ['openFile', 'multiSelections'] + }); + + if (!canceled && filePaths) { + openFiles(...filePaths); + } + }); + } +}); + +export const getExportHistoryMenuItem = () => ({ + label: 'Export History', + click: () => windowManager.exports?.open(), + enabled: getCurrentMenuItem(MenuItemId.exportHistory)?.enabled ?? false, + id: MenuItemId.exportHistory +}); + +export const getSendFeedbackMenuItem = () => ({ + id: MenuItemId.sendFeedback, + label: 'Send Feedback…', + click() { + openNewGitHubIssue({ + user: 'wulkano', + repo: 'kap', + body: issueBody + }); + } +}); + +const release = macosRelease(); + +const issueBody = ` + + +**macOS version:** ${release.name} (${release.version}) +**Kap version:** ${app.getVersion()} + +#### Steps to reproduce + +#### Current behavior + +#### Expected behavior + +#### Workaround + + +`; + diff --git a/main/menus/utils.ts b/main/menus/utils.ts new file mode 100644 index 000000000..34324fa87 --- /dev/null +++ b/main/menus/utils.ts @@ -0,0 +1,31 @@ +import {Menu} from 'electron'; + +export type MenuOptions = Parameters[0]; + +export enum MenuItemId { + exportHistory = 'exportHistory', + sendFeedback = 'sendFeedback', + openVideo = 'openVideo', + about = 'about', + preferences = 'preferences', + file = 'file', + edit = 'edit', + window = 'window', + help = 'help', + app = 'app', + saveOriginal = 'saveOriginal', + plugins = 'plugins', + audioDevices = 'audioDevices' +} + +export const getCurrentMenuItem = (id: MenuItemId) => { + return Menu.getApplicationMenu()?.getMenuItemById(id); +}; + +export const setExportMenuItemState = (enabled: boolean) => { + const menuItem = Menu.getApplicationMenu()?.getMenuItemById(MenuItemId.exportHistory); + + if (menuItem) { + menuItem.enabled = enabled; + } +}; diff --git a/main/plugin.js b/main/plugin.js deleted file mode 100644 index f446b5e1c..000000000 --- a/main/plugin.js +++ /dev/null @@ -1,157 +0,0 @@ -'use strict'; - -const path = require('path'); -const fs = require('fs'); -const electron = require('electron'); -const semver = require('semver'); -const Store = require('electron-store'); -const readPkg = require('read-pkg'); -const macosVersion = require('macos-version'); - -const PluginConfig = require('./utils/plugin-config'); -const {showError} = require('./utils/errors'); - -const {app, shell} = electron; - -export const recordPluginServiceState = new Store({ - name: 'record-plugin-state', - defaults: {} -}); - -class BasePlugin { - constructor(pluginName) { - this.name = pluginName; - } - - get prettyName() { - return this.name.replace(/^kap-/, ''); - } - - get isCompatible() { - return semver.satisfies(app.getVersion(), this.kapVersion || '*') && macosVersion.is(this.macosVersion || '*'); - } - - get repoUrl() { - const url = new URL(this.link); - url.hash = ''; - return url.href; - } -} - -export class InstalledPlugin extends BasePlugin { - constructor(pluginName) { - super(pluginName); - - this.isInstalled = true; - this.cwd = path.join(app.getPath('userData'), 'plugins'); - this.pkgPath = path.join(this.cwd, 'package.json'); - this.isSymlink = fs.lstatSync(this.pluginPath).isSymbolicLink(); - - this.json = readPkg.sync({cwd: this.pluginPath}); - - const {homepage, links, kap = {}} = this.json; - this.link = homepage || (links && links.homepage); - - // Keeping for backwards compatibility - this.kapVersion = kap.version || this.json.kapVersion; - this.macosVersion = kap.macosVersion; - - try { - this.plugin = require(this.pluginPath); - this.config = new PluginConfig(this); - - if (this.plugin.didConfigChange && typeof this.plugin.didConfigChange === 'function') { - this.config.onDidAnyChange((newValue, oldValue) => this.plugin.didConfigChange(newValue, oldValue, this.config)); - } - } catch (error) { - showError(error, {title: `Something went wrong while loading “${pluginName}”`, plugin: this}); - this.plugin = {}; - } - } - - getPath(subPath = '') { - return path.join(this.cwd, 'node_modules', this.name, subPath); - } - - get version() { - return this.json.version; - } - - get description() { - return this.json.description; - } - - get pluginPath() { - return this.getPath(); - } - - get isValid() { - return this.config.isConfigValid(); - } - - get hasConfig() { - return this.allServices.some(({config = {}}) => Object.keys(config).length > 0); - } - - get recordServices() { - return this.plugin.recordServices || []; - } - - get recordServicesWithStatus() { - return this.recordServices.map(service => ({ - ...service, - isEnabled: recordPluginServiceState.get(service.title) || false - })); - } - - get shareServices() { - return this.plugin.shareServices || []; - } - - get editServices() { - return this.plugin.editServices || []; - } - - get allServices() { - return [ - ...this.recordServices, - ...this.shareServices, - ...this.editServices - ]; - } - - openConfig() { - this.config.openInEditor(); - } - - viewOnGithub() { - shell.openExternal(this.link); - } -} - -export class NpmPlugin extends BasePlugin { - constructor(json, kap = {}) { - super(json.name); - - this.kapVersion = kap.version; - this.macosVersion = kap.macosVersion; - this.isInstalled = false; - this.json = json; - - this.version = json.version; - this.description = json.description; - - const {homepage, links} = json; - this.link = homepage || (links && links.homepage); - } - - viewOnGithub() { - shell.openExternal(this.link); - } -} - -// module.exports = { -// InstalledPlugin, -// NpmPlugin, -// recordPluginServiceState -// }; diff --git a/main/plugins/built-in/copy-to-clipboard-plugin.js b/main/plugins/built-in/copy-to-clipboard-plugin.ts similarity index 60% rename from main/plugins/built-in/copy-to-clipboard-plugin.js rename to main/plugins/built-in/copy-to-clipboard-plugin.ts index 499fec14b..16801bf69 100644 --- a/main/plugins/built-in/copy-to-clipboard-plugin.js +++ b/main/plugins/built-in/copy-to-clipboard-plugin.ts @@ -1,12 +1,13 @@ -'use strict'; -const {clipboard} = require('electron'); +import {clipboard} from 'electron'; +import {ShareServiceContext} from '../service-context'; + const plist = require('plist'); -const copyFileReferencesToClipboard = filePaths => { +const copyFileReferencesToClipboard = (filePaths: string[]) => { clipboard.writeBuffer('NSFilenamesPboardType', Buffer.from(plist.build(filePaths))); }; -const action = async context => { +const action = async (context: ShareServiceContext) => { const filePath = await context.filePath(); copyFileReferencesToClipboard([filePath]); context.notify(`The ${context.prettyFormat} has been copied to the clipboard`); @@ -21,4 +22,4 @@ const copyToClipboard = { action }; -module.exports.shareServices = [copyToClipboard]; +export const shareServices = [copyToClipboard]; diff --git a/main/plugins/built-in/open-with-plugin.ts b/main/plugins/built-in/open-with-plugin.ts index 51961b8bc..bdf8919c8 100644 --- a/main/plugins/built-in/open-with-plugin.ts +++ b/main/plugins/built-in/open-with-plugin.ts @@ -1,4 +1,4 @@ -import {ShareServiceContext} from '../../service-context'; +import {ShareServiceContext} from '../service-context'; import path from 'path'; import {getFormatExtension} from '../../common/constants'; import {Format} from '../../common/types'; @@ -23,7 +23,7 @@ const getAppsForFormat = (format: Format) => { .filter(app => !['Kap', 'Kap Beta'].includes(app.name)) .sort((a, b) => { if (a.isDefault !== b.isDefault) { - return Number(b.isDefault) - Number(a.isDefault) + return Number(b.isDefault) - Number(a.isDefault); } return Number(b.name === 'Gifski') - Number(a.name === 'Gifski'); @@ -35,7 +35,7 @@ const appsForFormat = (['mp4', 'gif', 'apng', 'webm', 'av1'] as Format[]) format, apps: getAppsForFormat(format) })) - .filter(({apps}) => apps.length > 0) + .filter(({apps}) => apps.length > 0); export const apps = new Map(appsForFormat.map(({format, apps}) => [format, apps])); @@ -45,4 +45,3 @@ export const shareServices = [{ action }]; - diff --git a/main/plugins/built-in/save-file-plugin.ts b/main/plugins/built-in/save-file-plugin.ts index bab8d1cf3..53da44341 100644 --- a/main/plugins/built-in/save-file-plugin.ts +++ b/main/plugins/built-in/save-file-plugin.ts @@ -1,14 +1,14 @@ 'use strict'; import {BrowserWindow, dialog} from 'electron'; -import {ShareServiceContext} from '../../service-context'; -import settings from '../../common/settings'; +import {ShareServiceContext} from '../service-context'; +import {settings} from '../../common/settings'; import makeDir from 'make-dir'; import {Format} from '../../common/types'; import path from 'path'; const {Notification, shell} = require('electron'); -const moveFile = require('move-file'); +const cpFile = require('cp-file'); const action = async (context: ShareServiceContext & {targetFilePath: string}) => { const temporaryFilePath = await context.filePath(); @@ -18,7 +18,9 @@ const action = async (context: ShareServiceContext & {targetFilePath: string}) = return; } - await moveFile(temporaryFilePath, context.targetFilePath); + // Copy the file, so we can still use the temporary source for future exports + // The temporary file will be cleaned up on app exit, or automatic system cleanup + await cpFile(temporaryFilePath, context.targetFilePath); const notification = new Notification({ title: 'File saved successfully!', @@ -66,10 +68,6 @@ export const askForTargetFilePath = async ( const defaultPath = path.join(lastSavedDirectory ?? kapturesDir, fileName); - console.log('y u no work', fileName); - console.log(window); - console.log(defaultPath); - const filters = filterMap.get(format); const {filePath} = await dialog.showSaveDialog(window, { @@ -84,5 +82,5 @@ export const askForTargetFilePath = async ( } return undefined; -} +}; diff --git a/main/plugins/config.ts b/main/plugins/config.ts index d9341a71e..78b5923ca 100644 --- a/main/plugins/config.ts +++ b/main/plugins/config.ts @@ -5,12 +5,12 @@ import {Service} from './service'; export default class PluginConfig extends Store { servicesWithNoConfig: Service[]; - validators: { + validators: Array<{ title: string; description?: string; - config: {[key: string]: Schema} - validate: ValidateFunction - }[] + config: Record; + validate: ValidateFunction; + }>; constructor(name: string, services: Service[]) { const defaults = {}; @@ -18,8 +18,8 @@ export default class PluginConfig extends Store { const validators = services .filter(({config}) => Boolean(config)) .map(service => { - const config = service.config as {[key: string]: Schema}; - const schema: {[key: string]: JSONSchema} = {}; + const config = service.config as Record; + const schema: Record> = {}; const requiredKeys = []; for (const key of Object.keys(config)) { @@ -54,8 +54,8 @@ export default class PluginConfig extends Store { validate: validator, title: service.title, description: service.configDescription, - config: config - } + config + }; }); super({ @@ -69,10 +69,7 @@ export default class PluginConfig extends Store { } get isValid() { - return this.validators.reduce( - (isValid, validator) => isValid && (validator.validate(this.store) as boolean), - true - ); + return this.validators.every(validator => validator.validate(this.store)); } get validServices() { diff --git a/main/plugins/index.ts b/main/plugins/index.ts index 59a696490..f95e884d2 100644 --- a/main/plugins/index.ts +++ b/main/plugins/index.ts @@ -7,24 +7,29 @@ import execa from 'execa'; import {track} from '../common/analytics'; import {InstalledPlugin, NpmPlugin} from './plugin'; import {showError} from '../utils/errors'; -import {openPrefsWindow} from '../preferences'; import {notify} from '../utils/notifications'; import packageJson from 'package-json'; import {NormalizedPackageJson} from 'read-pkg'; +import {windowManager} from '../windows/manager'; const got = require('got'); type PackageJson = { - dependencies: {[key: string]: string} -} + dependencies: Record; +}; -class Plugins extends EventEmitter { +export class Plugins extends EventEmitter { yarnBin = path.join(__dirname, '../../node_modules/yarn/bin/yarn.js'); appVersion = app.getVersion(); pluginsDir = path.join(app.getPath('userData'), 'plugins'); builtInDir = path.join(__dirname, 'built-in'); packageJsonPath = path.join(this.pluginsDir, 'package.json'); installedPlugins: InstalledPlugin[] = []; + builtInPlugins = [ + new InstalledPlugin('_copyToClipboard', path.resolve(this.builtInDir, 'copy-to-clipboard-plugin')), + new InstalledPlugin('_saveToDisk', path.resolve(this.builtInDir, 'save-file-plugin')), + new InstalledPlugin('_openWith', path.resolve(this.builtInDir, 'open-with-plugin')) + ]; constructor() { super(); @@ -32,43 +37,7 @@ class Plugins extends EventEmitter { this.loadPlugins(); } - private makePluginsDir() { - if (!fs.existsSync(this.packageJsonPath)) { - makeDir.sync(this.pluginsDir); - fs.writeFileSync(this.packageJsonPath, JSON.stringify({dependencies: {}}, null, 2)); - } - } - - private modifyMainPackageJson(modifier: (pkg: PackageJson) => void) { - const pkg = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf8')); - modifier(pkg); - fs.writeFileSync(this.packageJsonPath, JSON.stringify(pkg, null, 2)); - } - - private async runYarn(...args: string[]) { - await execa(process.execPath, [this.yarnBin, ...args], { - cwd: this.pluginsDir, - env: { - ELECTRON_RUN_AS_NODE: '1', - NODE_ENV: 'development' - } - }); - } - - private get pluginNames() { - const pkg = fs.readFileSync(this.packageJsonPath, 'utf8'); - return Object.keys(JSON.parse(pkg).dependencies || {}); - } - - private async yarnInstall() { - await this.runYarn('install', '--no-lockfile', '--registry', 'https://registry.npmjs.org'); - } - - private loadPlugins() { - this.installedPlugins = this.pluginNames.map(name => new InstalledPlugin(name)); - } - - async install(name: string) { + async install(name: string): Promise { track(`plugin/installed/${name}`); this.modifyMainPackageJson(pkg => { @@ -95,7 +64,8 @@ class Plugins extends EventEmitter { const {isValid, hasConfig} = plugin; - const openConfig = () => openPrefsWindow({target: {name, action: 'configure'}}); + // Const openConfig = () => openPrefsWindow({target: {name, action: 'configure'}}); + const openConfig = () => plugin.openConfig(); const options = (isValid && !hasConfig) ? { title: 'Plugin installed', @@ -110,7 +80,7 @@ class Plugins extends EventEmitter { ] }; - notify(options) + notify(options); const validServices = plugin.config.validServices; @@ -121,12 +91,13 @@ class Plugins extends EventEmitter { } this.emit('installed', plugin); + return plugin; } catch (error) { notify.simple(`Something went wrong while installing ${name}`); this.modifyMainPackageJson(pkg => { delete pkg.dependencies[name]; }); - console.log(error); + showError(error); } } @@ -149,7 +120,7 @@ class Plugins extends EventEmitter { plugin.config.clear(); this.emit('uninstalled', name); - const json = plugin.json as NormalizedPackageJson; + const json = plugin.json!; return new NpmPlugin(json, { version: json.kapVersion, @@ -164,7 +135,7 @@ class Plugins extends EventEmitter { async getFromNpm() { const url = 'https://api.npms.io/v2/search?q=keywords:kap-plugin+not:deprecated'; const response = (await got(url, {json: true})) as { - body: {results: {package: NormalizedPackageJson}[]} + body: {results: Array<{package: NormalizedPackageJson}>}; }; const installed = this.pluginNames; @@ -185,9 +156,7 @@ class Plugins extends EventEmitter { get allPlugins() { return [ ...this.installedPlugins, - new InstalledPlugin('_copyToClipboard', path.resolve(this.builtInDir, 'copy-to-clipboard-plugin')), - new InstalledPlugin('_saveToDisk', path.resolve(this.builtInDir, 'save-file-plugin')), - new InstalledPlugin('_openWith', path.resolve(this.builtInDir, 'open-with-plugin')), + ...this.builtInPlugins ]; } @@ -202,8 +171,46 @@ class Plugins extends EventEmitter { get recordingPlugins() { return this.allPlugins.filter(plugin => plugin.recordServices.length > 0); } -} -const plugins = new Plugins(); + openPluginConfig = async (pluginName: string) => { + return windowManager.config?.open(pluginName); + }; + + private makePluginsDir() { + if (!fs.existsSync(this.packageJsonPath)) { + makeDir.sync(this.pluginsDir); + fs.writeFileSync(this.packageJsonPath, JSON.stringify({dependencies: {}}, null, 2)); + } + } + + private modifyMainPackageJson(modifier: (pkg: PackageJson) => void) { + const pkg = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf8')); + modifier(pkg); + fs.writeFileSync(this.packageJsonPath, JSON.stringify(pkg, null, 2)); + } + + private async runYarn(...args: string[]) { + await execa(process.execPath, [this.yarnBin, ...args], { + cwd: this.pluginsDir, + env: { + ELECTRON_RUN_AS_NODE: '1', + NODE_ENV: 'development' + } + }); + } + + private get pluginNames() { + const pkg = fs.readFileSync(this.packageJsonPath, 'utf8'); + return Object.keys(JSON.parse(pkg).dependencies || {}); + } + + private async yarnInstall() { + await this.runYarn('install', '--no-lockfile', '--registry', 'https://registry.npmjs.org'); + } + + private loadPlugins() { + this.installedPlugins = this.pluginNames.map(name => new InstalledPlugin(name)); + } +} -export default plugins; +export const plugins = new Plugins(); diff --git a/main/plugins/plugin.ts b/main/plugins/plugin.ts index 854d2bc30..3efe04609 100644 --- a/main/plugins/plugin.ts +++ b/main/plugins/plugin.ts @@ -8,8 +8,9 @@ import {RecordService, ShareService, EditService} from './service'; import {showError} from '../utils/errors'; import PluginConfig from './config'; import Store from 'electron-store'; +import {windowManager} from '../windows/manager'; -export const recordPluginServiceState = new Store<{[key: string]: boolean}>({ +export const recordPluginServiceState = new Store>({ name: 'record-plugin-state', defaults: {} }); @@ -22,7 +23,7 @@ class BasePlugin { json?: readPkg.NormalizedPackageJson; constructor(pluginName: string) { - this.name = pluginName + this.name = pluginName; } get prettyName() { @@ -30,12 +31,12 @@ class BasePlugin { } get isCompatible() { - return semver.satisfies(app.getVersion(), this.kapVersion || '*') && macosVersion.is(this.macosVersion || '*'); + return semver.satisfies(app.getVersion(), this.kapVersion ?? '*') && macosVersion.is(this.macosVersion ?? '*'); } get repoUrl() { if (!this.link) { - return; + return ''; } const url = new URL(this.link); @@ -53,15 +54,15 @@ class BasePlugin { viewOnGithub() { if (this.link) { - shell.openExternal(this.link) + shell.openExternal(this.link); } } } export interface KapPlugin { - shareServices?: ShareService[]; - editServices?: EditService[]; - recordServices?: RecordService[]; + shareServices?: Array>; + editServices?: Array>; + recordServices?: Array>; didConfigChange?: (newValue: Readonly | undefined, oldValue: Readonly | undefined, config: Store) => void | Promise; didInstall?: (config: Store) => void | Promise; @@ -82,7 +83,7 @@ export class InstalledPlugin extends BasePlugin { constructor(pluginName: string, customPath?: string) { super(pluginName); - this.pluginPath = customPath || path.join(this.pluginsPath, 'node_modules', pluginName); + this.pluginPath = customPath ?? path.join(this.pluginsPath, 'node_modules', pluginName); this.isBuiltIn = Boolean(customPath); if (!this.isBuiltIn) { @@ -142,23 +143,60 @@ export class InstalledPlugin extends BasePlugin { get recordServicesWithStatus() { return this.recordServices.map(service => ({ ...service, - isEnabled: recordPluginServiceState.get(`${this.name}-${service.title}`, false) + isEnabled: recordPluginServiceState.get(this.getRecordServiceKey(service), false), + setEnabled: this.getSetEnableFunction(service) })); } - enableService(service: RecordService) { - recordPluginServiceState.set(`${this.name}-${service.title}`, true); - } + enableService = (service: RecordService) => { + recordPluginServiceState.set(this.getRecordServiceKey(service), true); + }; + + openConfig = () => windowManager.config?.open(this.name); - openConfig() { + openConfigInEditor = () => { this.config.openInEditor(); - } + }; + + private readonly getSetEnableFunction = (service: RecordService) => async (enabled: boolean) => { + const isEnabled = recordPluginServiceState.get(this.getRecordServiceKey(service), false); + + if (isEnabled === enabled) { + return; + } + + if (!enabled) { + recordPluginServiceState.set(this.getRecordServiceKey(service), false); + return; + } + + if (!this.config.validServices.includes(service.title)) { + windowManager.preferences?.open({target: {name: this.name, action: 'configure'}}); + return; + } + + if (service.willEnable && typeof service.willEnable === 'function') { + try { + const canEnable = await service.willEnable(); + + if (canEnable) { + recordPluginServiceState.set(this.getRecordServiceKey(service), true); + } + } catch (error) { + showError(error, {title: `Something went wrong while enabling "${service.title}`, plugin: this}); + } + } else { + recordPluginServiceState.set(this.getRecordServiceKey(service), true); + } + }; + + private readonly getRecordServiceKey = (service: RecordService) => `${this.name}-${service.title}`; } export class NpmPlugin extends BasePlugin { isInstalled = false; - constructor(json: readPkg.NormalizedPackageJson, kap: {version?: string, macosVersion?: string} = {}) { + constructor(json: readPkg.NormalizedPackageJson, kap: {version?: string; macosVersion?: string} = {}) { super(json.name); this.json = json; diff --git a/main/plugins/service-context.ts b/main/plugins/service-context.ts index 5c5ba4fc2..feac75094 100644 --- a/main/plugins/service-context.ts +++ b/main/plugins/service-context.ts @@ -1,7 +1,7 @@ import {app, clipboard} from 'electron'; import Store from 'electron-store'; import got, {GotFn, GotPromise} from 'got'; -import {Format} from '../common/types'; +import {ApertureOptions, Format} from '../common/types'; import {InstalledPlugin} from './plugin'; import {addPluginPromise} from '../utils/deep-linking'; import {notify} from '../utils/notifications'; @@ -13,11 +13,11 @@ interface ServiceContextOptions { } class ServiceContext { - private plugin: InstalledPlugin; - requests: GotPromise[] = []; - + requests: Array> = []; config: Store; + private readonly plugin: InstalledPlugin; + constructor(options: ServiceContextOptions) { this.plugin = options.plugin; this.config = this.plugin.config; @@ -27,11 +27,11 @@ class ServiceContext { const request = got(...args); this.requests.push(request); return request; - } + }; copyToClipboard = (text: string) => { clipboard.writeText(text); - } + }; notify = (text: string, action?: () => any) => { return notify({ @@ -39,17 +39,17 @@ class ServiceContext { title: this.plugin.isBuiltIn ? app.name : this.plugin.prettyName, click: action }); - } + }; openConfigFile = () => { this.config.openInEditor(); - } + }; waitForDeepLink = async () => { return new Promise(resolve => { addPluginPromise(this.plugin.name, resolve); }); - } + }; } interface ShareServiceContextOptions extends ServiceContextOptions { @@ -62,10 +62,10 @@ interface ShareServiceContextOptions extends ServiceContextOptions { } export class ShareServiceContext extends ServiceContext { - private options: ShareServiceContextOptions; - isCanceled = false; + private readonly options: ShareServiceContextOptions; + constructor(options: ShareServiceContextOptions) { super(options); this.options = options; @@ -83,13 +83,13 @@ export class ShareServiceContext extends ServiceContext { return `${this.options.defaultFileName}.${getFormatExtension(this.options.format)}`; } - filePath = (options: {fileType?: Format}) => { + filePath = async (options?: {fileType?: Format}) => { return this.options.filePath(options); - } + }; setProgress = (text: string, percentage: number) => { this.options.onProgress(text, percentage); - } + }; cancel = () => { this.isCanceled = true; @@ -98,31 +98,31 @@ export class ShareServiceContext extends ServiceContext { for (const request of this.requests) { request.cancel(); } - } + }; } interface EditServiceContextOptions extends ServiceContextOptions { onProgress: (text: string, percentage: number) => void; - inputPath: string, - outputPath: string, + inputPath: string; + outputPath: string; exportOptions: { - width: number, - height: number, - format: Format, - fps: number, - duration: number, - isMuted: boolean, - loop: boolean - }, - convert: (args: string[], text?: string) => PCancelable, + width: number; + height: number; + format: Format; + fps: number; + duration: number; + isMuted: boolean; + loop: boolean; + }; + convert: (args: string[], text?: string) => PCancelable; onCancel: () => void; } export class EditServiceContext extends ServiceContext { - private options: EditServiceContextOptions; - isCanceled = false; + private readonly options: EditServiceContextOptions; + constructor(options: EditServiceContextOptions) { super(options); this.options = options; @@ -146,7 +146,7 @@ export class EditServiceContext extends ServiceContext { setProgress = (text: string, percentage: number) => { this.options.onProgress(text, percentage); - } + }; cancel = () => { this.isCanceled = true; @@ -155,5 +155,36 @@ export class EditServiceContext extends ServiceContext { for (const request of this.requests) { request.cancel(); } + }; +} + +export type RecordServiceState = Record> = { + persistedState?: PersistedState; +}; + +export interface RecordServiceContextOptions extends ServiceContextOptions { + apertureOptions: ApertureOptions; + state: State; + setRecordingName: (name: string) => void; +} + +export class RecordServiceContext extends ServiceContext { + private readonly options: RecordServiceContextOptions; + + constructor(options: RecordServiceContextOptions) { + super(options); + this.options = options; + } + + get state() { + return this.options.state; + } + + get apertureOptions() { + return this.options.apertureOptions; + } + + get setRecordingName() { + return this.options.setRecordingName; } } diff --git a/main/plugins/service.ts b/main/plugins/service.ts index 9f121668b..a270dcba3 100644 --- a/main/plugins/service.ts +++ b/main/plugins/service.ts @@ -2,7 +2,7 @@ import PCancelable from 'p-cancelable'; import {Format} from '../common/types'; import {Schema} from '../utils/ajv'; -import {EditServiceContext, ShareServiceContext} from './service-context'; +import {EditServiceContext, RecordServiceContext, ShareServiceContext} from './service-context'; export interface Service { title: string; @@ -19,10 +19,11 @@ export interface EditService extends Service { action: (context: EditServiceContext) => PromiseLike | PCancelable; } -export interface RecordService extends Service { - willStartRecording?: () => {}; - didStartRecording?: () => {}; - didStopRecording?: () => {}; - willEnable?: () => {}; - cleanUp?: () => {}; -} +export type RecordServiceHook = 'willStartRecording' | 'didStartRecording' | 'didStopRecording'; + +export type RecordService = Service & { + [key in RecordServiceHook]: ((context: RecordServiceContext) => PromiseLike) | undefined; +} & { + willEnable?: () => PromiseLike; + cleanUp?: (persistedState: Record) => void; +}; diff --git a/main/recording-history.js b/main/recording-history.ts similarity index 71% rename from main/recording-history.js rename to main/recording-history.ts index 82d01c617..d534106da 100644 --- a/main/recording-history.js +++ b/main/recording-history.ts @@ -1,22 +1,39 @@ /* eslint-disable array-element-newline */ 'use strict'; -const {shell, clipboard} = require('electron'); -const fs = require('fs'); -const Store = require('electron-store'); -const ffmpeg = require('@ffmpeg-installer/ffmpeg'); -const util = require('electron-util'); -const execa = require('execa'); -const tempy = require('tempy'); - -const {showDialog} = require('./dialog'); -const {openEditorWindow} = require('./editor'); -const plugins = require('./common/plugins'); -const {generateTimestampedName} = require('./utils/timestamped-name'); +import {shell, clipboard} from 'electron'; +import fs from 'fs'; +import Store from 'electron-store'; +import util from 'electron-util'; +import execa from 'execa'; +import tempy from 'tempy'; +import {SetOptional} from 'type-fest'; + +import {windowManager} from './windows/manager'; +import {plugins} from './plugins'; +import {generateTimestampedName} from './utils/timestamped-name'; +import {Video} from './video'; +import {ApertureOptions} from './common/types'; +import Sentry, {isSentryEnabled} from './utils/sentry'; +const ffmpeg = require('@ffmpeg-installer/ffmpeg'); const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path); -const recordingHistory = new Store({ +export interface PastRecording { + filePath: string; + name: string; + date: string; +} + +export interface ActiveRecording extends PastRecording { + apertureOptions: ApertureOptions; + plugins: Record>; +} + +export const recordingHistory = new Store<{ + activeRecording: ActiveRecording; + recordings: PastRecording[]; +}>({ name: 'recording-history', schema: { activeRecording: { @@ -60,13 +77,13 @@ const recordingHistory = new Store({ } }); -const setCurrentRecording = ({ +export const setCurrentRecording = ({ filePath, name = generateTimestampedName(), date = new Date().toISOString(), apertureOptions, plugins = {} -}) => { +}: SetOptional) => { recordingHistory.set('activeRecording', { filePath, name, @@ -76,35 +93,35 @@ const setCurrentRecording = ({ }); }; -const updatePluginState = state => { +export const updatePluginState = (state: ActiveRecording['plugins']) => { recordingHistory.set('activeRecording.plugins', state); }; -const stopCurrentRecording = recordingName => { +export const stopCurrentRecording = (recordingName?: string) => { const {filePath, name} = recordingHistory.get('activeRecording'); addRecording({ filePath, - name: recordingName || name, + name: recordingName ?? name, date: new Date().toISOString() }); recordingHistory.delete('activeRecording'); }; -const getPastRecordings = () => { +export const getPastRecordings = (): PastRecording[] => { const recordings = recordingHistory.get('recordings', []); const validRecordings = recordings.filter(({filePath}) => fs.existsSync(filePath)); recordingHistory.set('recordings', validRecordings); return validRecordings; }; -const addRecording = newRecording => { +export const addRecording = (newRecording: PastRecording): PastRecording[] => { const recordings = [newRecording, ...recordingHistory.get('recordings', [])]; const validRecordings = recordings.filter(({filePath}) => fs.existsSync(filePath)); recordingHistory.set('recordings', validRecordings); return validRecordings; }; -const cleanPastRecordings = () => { +export const cleanPastRecordings = () => { const recordings = getPastRecordings(); for (const recording of recordings) { fs.unlinkSync(recording.filePath); @@ -113,22 +130,22 @@ const cleanPastRecordings = () => { recordingHistory.set('recordings', []); }; -const cleanUpRecordingPlugins = usedPlugins => { - const recordingPlugins = plugins.getRecordingPlugins(); +export const cleanUpRecordingPlugins = (usedPlugins: ActiveRecording['plugins']) => { + const recordingPlugins = plugins.recordingPlugins; for (const pluginName of Object.keys(usedPlugins)) { const plugin = recordingPlugins.find(p => p.name === pluginName); for (const [serviceTitle, persistedState] of Object.entries(usedPlugins[pluginName])) { - const service = plugin && plugin.recordServices.find(s => s.title === serviceTitle); + const service = plugin?.recordServices.find(s => s.title === serviceTitle); - if (service && service.cleanUp) { + if (service?.cleanUp) { service.cleanUp(persistedState); } } } }; -const handleIncompleteRecording = async recording => { +export const handleIncompleteRecording = async (recording: ActiveRecording) => { cleanUpRecordingPlugins(recording.plugins); try { @@ -147,14 +164,14 @@ const handleIncompleteRecording = async recording => { return handleRecording(recording); }; -const handleRecording = async recording => { +const handleRecording = async (recording: ActiveRecording) => { addRecording({ filePath: recording.filePath, name: recording.name, date: recording.date }); - return showDialog({ + return windowManager.dialog?.open({ title: 'Kap didn\'t shut down correctly.', detail: 'Looks like Kap crashed during a recording. Kap was able to locate the file and it appears to be playable.', buttons: [ @@ -167,15 +184,15 @@ const handleRecording = async recording => { }, { label: 'Show in Editor', - action: () => openEditorWindow(recording.filePath, {recordingName: recording.name}) + action: async () => Video.getOrCreate({filePath: recording.filePath, title: recording.name}).openEditorWindow() } ] }); }; const knownErrors = [{ - test: error => error.includes('moov atom not found'), - fix: async filePath => { + test: (error: string) => error.includes('moov atom not found'), + fix: async (filePath: string): Promise => { try { const outputPath = tempy.file({extension: 'mp4'}); @@ -194,12 +211,12 @@ const knownErrors = [{ ]); return outputPath; - } catch { } + } catch {} } }]; -const handleCorruptRecording = async (recording, error) => { - const options = { +const handleCorruptRecording = async (recording: ActiveRecording, error: string) => { + const options: any = { title: 'Kap didn\'t shut down correctly.', detail: `Looks like Kap crashed during a recording. We were able to locate the file. Unfortunately, it appears to be corrupt.\n\n${error}`, cancelId: 0, @@ -224,13 +241,12 @@ const handleCorruptRecording = async (recording, error) => { const applicableErrors = knownErrors.filter(({test}) => test(error)); if (applicableErrors.length === 0) { - const Sentry = require('./utils/sentry').default; - if (Sentry.isSentryEnabled) { + if (isSentryEnabled) { // Collect info about possible unknown errors, to see if we can implement fixes using ffmpeg Sentry.captureException(new Error(`Corrupt recording: ${error}`)); } - return showDialog(options); + return windowManager.dialog?.open(options); } options.message = 'We can attempt to repair the recording.'; @@ -238,9 +254,8 @@ const handleCorruptRecording = async (recording, error) => { options.buttons.push({ label: 'Attempt to Fix', activeLabel: 'Attempting to Fix…', - action: async (_, updateUi) => { + action: async (_: any, updateUi: any) => { for (const {fix} of applicableErrors) { - // eslint-disable-next-line no-await-in-loop const outputPath = await fix(recording.filePath); if (outputPath) { @@ -263,7 +278,7 @@ const handleCorruptRecording = async (recording, error) => { }, { label: 'Show in Editor', - action: () => openEditorWindow(outputPath, {recordingName: recording.name}) + action: async () => Video.getOrCreate({filePath: outputPath, title: recording.name}).openEditorWindow() } ] }); @@ -292,10 +307,10 @@ const handleCorruptRecording = async (recording, error) => { } }); - return showDialog(options); + return windowManager.dialog?.open(options); }; -const hasActiveRecording = async () => { +export const hasActiveRecording = async () => { const activeRecording = recordingHistory.get('activeRecording'); if (activeRecording) { @@ -306,14 +321,3 @@ const hasActiveRecording = async () => { return false; }; - -module.exports = { - recordingHistory, - getPastRecordings, - addRecording, - hasActiveRecording, - setCurrentRecording, - updatePluginState, - stopCurrentRecording, - cleanPastRecordings -}; diff --git a/main/remote-states/conversion.ts b/main/remote-states/conversion.ts index 9051fbd1c..fbd5e8bcf 100644 --- a/main/remote-states/conversion.ts +++ b/main/remote-states/conversion.ts @@ -1,9 +1,7 @@ import {ConversionRemoteState, ConversionState} from '../common/types'; -import Conversion from '../conversion' +import Conversion from '../conversion'; const getConversionState = (conversion: Conversion): ConversionState => { - console.log(conversion); - return { title: conversion.title, description: conversion.description, @@ -13,11 +11,10 @@ const getConversionState = (conversion: Conversion): ConversionState => { status: conversion.status, canCopy: conversion.canCopy }; -} +}; const conversionRemoteState: ConversionRemoteState = (sendUpdate: (state: ConversionState, id: string) => void) => { const getState = (conversionId: string) => { - console.log('Conversion', conversionId, 'requested'); const conversion = Conversion.fromId(conversionId); if (!conversion) { @@ -25,7 +22,7 @@ const conversionRemoteState: ConversionRemoteState = (sendUpdate: (state: Conver } return getConversionState(conversion); - } + }; const subscribe = (conversionId: string) => { const conversion = Conversion.fromId(conversionId); @@ -34,33 +31,31 @@ const conversionRemoteState: ConversionRemoteState = (sendUpdate: (state: Conver return; } - const callback = () => sendUpdate(getConversionState(conversion), conversionId); + const callback = () => { + sendUpdate(getConversionState(conversion), conversionId); + }; conversion.on('updated', callback); return () => { - console.log('Unsubscribing'); conversion.off('updated', callback); }; }; const actions = { cancel: (_: any, conversionId: string) => { - console.log('Getting conversion', conversionId); Conversion.fromId(conversionId)?.cancel(); - console.log(Conversion.fromId(conversionId)); }, copy: (_: any, conversionId: string) => { - console.log('GETTING COPY CALL', conversionId); Conversion.fromId(conversionId)?.copy(); } - } as any + } as any; return { subscribe, getState, actions }; -} +}; export default conversionRemoteState; export const name = 'conversion'; diff --git a/main/remote-states/editor-options.ts b/main/remote-states/editor-options.ts index d27bd4fb6..22de5fa8f 100644 --- a/main/remote-states/editor-options.ts +++ b/main/remote-states/editor-options.ts @@ -2,11 +2,11 @@ import Store from 'electron-store'; import {EditorOptionsRemoteState, ExportOptions, ExportOptionsPlugin, Format} from '../common/types'; import {formats} from '../common/constants'; -import plugins from '../plugins'; +import {plugins} from '../plugins'; import {apps} from '../plugins/built-in/open-with-plugin'; import {prettifyFormat} from '../utils/formats'; -const exportUsageHistory = new Store<{[key in Format]: {lastUsed: number, plugins: {[key: string]: number}}}>({ +const exportUsageHistory = new Store<{[key in Format]: {lastUsed: number; plugins: Record}}>({ name: 'export-usage-history', defaults: { apng: {lastUsed: 1, plugins: {default: 1}}, @@ -56,13 +56,11 @@ const getEditOptions = () => { title: service.title, pluginName: plugin.name, pluginPath: plugin.pluginPath, - hasConfig: Object.keys(service.config || {}).length > 0 + hasConfig: Object.keys(service.config ?? {}).length > 0 })) ); }; - - const getExportOptions = () => { const installed = plugins.sharePlugins; @@ -107,14 +105,14 @@ const editorOptionsRemoteState: EditorOptionsRemoteState = (sendUpdate: (state: state.formats = getExportOptions(); state.editServices = getEditOptions(); sendUpdate(state); - } + }; plugins.on('installed', updatePlugins); plugins.on('uninstalled', updatePlugins); plugins.on('config-changed', updatePlugins); const actions = { - updatePluginUsage: ({format, plugin}: {format: Format, plugin: string}) => { + updatePluginUsage: ({format, plugin}: {format: Format; plugin: string}) => { const usage = exportUsageHistory.get(format); const now = Date.now(); @@ -125,18 +123,17 @@ const editorOptionsRemoteState: EditorOptionsRemoteState = (sendUpdate: (state: state.formats = getExportOptions(); sendUpdate(state); }, - updateFpsUsage: ({format, fps}: {format: Format, fps: number}) => { + updateFpsUsage: ({format, fps}: {format: Format; fps: number}) => { fpsUsageHistory.set(format, fps); state.fpsHistory = fpsUsageHistory.store; sendUpdate(state); } }; - console.log(state); return { actions, getState: () => state - } + }; }; export default editorOptionsRemoteState; diff --git a/main/remote-states/setup-remote-state.ts b/main/remote-states/setup-remote-state.ts index 7688dcc32..438a3c109 100644 --- a/main/remote-states/setup-remote-state.ts +++ b/main/remote-states/setup-remote-state.ts @@ -2,14 +2,14 @@ import {RemoteState, getChannelNames} from './utils'; import {ipcMain} from 'electron-better-ipc'; import {BrowserWindow} from 'electron'; -const setupRemoteState = async (name: string, callback: RemoteState) => { +// eslint-disable-next-line @typescript-eslint/ban-types +const setupRemoteState = async >(name: string, callback: RemoteState) => { const channelNames = getChannelNames(name); const renderers = new Map(); const sendUpdate = async (state?: State, id?: string) => { if (id) { - console.log('got update', state); const renderer = renderers.get(id); if (renderer) { @@ -24,7 +24,7 @@ const setupRemoteState = async >( - name: string, - initialState?: Callback extends RemoteState ? State : never -): (id?: string) => ( - Callback extends RemoteState ? ( - Actions & { - state: State; - isLoading: false; - refreshState: () => void; - } - ) : never -) => { - const channelNames = getChannelNames(name); - - return (id?: string) => { - const [state, setState] = useState(initialState); - const [isLoading, setIsLoading] = useState(true); - const actionsRef = useRef({}); - - useEffect(() => { - const cleanup = ipcRenderer.answerMain(channelNames.stateUpdated, setState); - - (async () => { - const actionKeys = (await ipcRenderer.callMain(channelNames.subscribe, id)) as string[]; - - const actions = actionKeys.reduce((acc, key) => ({ - ...acc, - [key]: (data: any) => { - console.log('Action', key, 'called with', data); - return ipcRenderer.callMain(channelNames.callAction, {key, data, id}) - } - }), {}); - - const getState = async () => { - const newState = (await ipcRenderer.callMain(channelNames.getState, id)) as typeof state; - setState(newState); - } - - actionsRef.current = { - ...actions, - refreshState: getState - }; - - await getState(); - setIsLoading(false); - })(); - - return cleanup; - }, []); - - return { - ...actionsRef.current, - isLoading, - state - }; - } -} - -export default useRemoteState; diff --git a/main/remote-states/utils.ts b/main/remote-states/utils.ts index 058a827bb..55402bc0d 100644 --- a/main/remote-states/utils.ts +++ b/main/remote-states/utils.ts @@ -1,3 +1,4 @@ +import {Promisable} from 'type-fest'; export const getChannelName = (name: string, action: string) => `kap-remote-state-${name}-${action}`; @@ -8,8 +9,9 @@ export const getChannelNames = (name: string) => ({ stateUpdated: getChannelName(name, 'state-updated') }); -export type RemoteState = (sendUpdate: (state?: State, id?: string) => void) => { - getState: (id?: string) => State, - actions: Actions, - subscribe?: (id?: string) => undefined | (() => void) -} +// eslint-disable-next-line @typescript-eslint/ban-types +export type RemoteState> = (sendUpdate: (state?: State, id?: string) => void) => Promisable<{ + getState: (id?: string) => Promisable; + actions: Actions; + subscribe?: (id?: string) => undefined | (() => void); +}>; diff --git a/main/service-context.js b/main/service-context.js deleted file mode 100644 index 29494371d..000000000 --- a/main/service-context.js +++ /dev/null @@ -1,186 +0,0 @@ -'use strict'; -const {Notification, clipboard} = require('electron'); -const got = require('got'); - -const {addPluginPromise} = require('./utils/deep-linking'); - -const prettifyFormat = format => { - const formats = new Map([ - ['apng', 'APNG'], - ['gif', 'GIF'], - ['mp4', 'MP4'], - ['webm', 'WebM'] - ]); - - return formats.get(format); -}; - -class ServiceContext { - constructor(options) { - this._isBuiltin = options._isBuiltin; - this.config = options.config; - - this.requests = []; - this.isCanceled = false; - - this.request = this.request.bind(this); - this.copyToClipboard = this.copyToClipboard.bind(this); - this.notify = this.notify.bind(this); - this.openConfigFile = this.openConfigFile.bind(this); - this.waitForDeepLink = this.waitForDeepLink.bind(this); - } - - request(url, options) { - if (this.isCanceled) { - return; - } - - const request = got(url, options); - - this.requests.push(request); - - return request; - } - - cancel() { - this.isCanceled = true; - if (this.onCancel) { - this.onCancel(); - } - - for (const request of this.requests) { - request.cancel(); - } - } - - copyToClipboard(text) { - if (this.isCanceled) { - return; - } - - clipboard.writeText(text); - } - - notify(text, action) { - if (this.isCanceled) { - return; - } - - let options = { - title: this.pluginName, - body: text - }; - - if (this._isBuiltin) { - options = { - body: text - }; - } - - const notification = new Notification(options); - - if (action) { - notification.on('click', action); - } - - notification.show(); - } - - openConfigFile() { - if (this.isCanceled) { - return; - } - - this.config.openInEditor(); - } - - async waitForDeepLink() { - return new Promise(resolve => { - addPluginPromise(this.pluginName, resolve); - }); - } -} - -export class ShareServiceContext extends ServiceContext { - constructor(options) { - super(options); - - this.format = options.format; - this.prettyFormat = prettifyFormat(this.format); - this.defaultFileName = options.defaultFileName; - this.filePath = options.convert; - this.onCancel = options.onCancel; - this.onProgress = options.onProgress; - this.pluginName = options.pluginName; - - this.isCanceled = false; - - this.cancel = this.cancel.bind(this); - this.setProgress = this.setProgress.bind(this); - } - - clear() { - this.isCanceled = true; - - for (const request of this.requests) { - request.cancel(); - } - } - - setProgress(text, percentage) { - if (this.isCanceled) { - return; - } - - this.onProgress(text, percentage); - } -} - -export class RecordServiceContext extends ServiceContext { - constructor(options) { - super(options); - - this.apertureOptions = options.apertureOptions; - this.state = options.state; - this.setRecordingName = options.setRecordingName; - } -} - -export class EditServiceContext extends ServiceContext { - constructor(options) { - super(options); - - const { - inputPath, - outputPath, - width, - height, - format, - fps, - startTime, - endTime, - isMuted, - loop - } = options.exportOptions; - - this.inputPath = inputPath; - this.outputPath = outputPath; - - this.exportOptions = { - width, - height, - format, - fps, - duration: endTime - startTime, - isMuted, - loop - }; - this.convert = options.convert; - } -} - -// module.exports = { -// ShareServiceContext, -// RecordServiceContext, -// EditServiceContext -// }; diff --git a/main/tray.js b/main/tray.ts similarity index 58% rename from main/tray.js rename to main/tray.ts index 910a4a00a..a4b46a3ad 100644 --- a/main/tray.js +++ b/main/tray.ts @@ -1,27 +1,24 @@ 'use strict'; -const {Tray} = require('electron'); -const path = require('path'); +import {Tray} from 'electron'; +import path from 'path'; +import {getCogMenu} from './menus/cog'; +import {track} from './common/analytics'; +import {openFiles} from './utils/open-files'; +import {windowManager} from './windows/manager'; -const {openCropperWindow} = require('./cropper'); -const {getCogMenu} = require('./menus'); -const {track} = require('./common/analytics'); -const openFiles = require('./utils/open-files'); - -let tray = null; -let trayAnimation = null; +let tray: Tray; +let trayAnimation: NodeJS.Timeout | undefined; const openContextMenu = async () => { tray.popUpContextMenu(await getCogMenu()); }; -const initializeTray = () => { +const openCropperWindow = () => windowManager.cropper?.open(); + +export const initializeTray = () => { tray = new Tray(path.join(__dirname, '..', 'static', 'menubarDefaultTemplate.png')); tray.on('click', openCropperWindow); - // tray.on('click', () => { - // const {openEditorWindow} = require('./editor'); - // openEditorWindow('/Users/b008822/Kaptures/Kapture 2020-07-10 at 9.48.45.mp4'); - // }); tray.on('right-click', openContextMenu); tray.on('drop-files', (_, files) => { track('editor/opened/tray'); @@ -31,28 +28,33 @@ const initializeTray = () => { return tray; }; -const disableTray = () => { +export const disableTray = () => { tray.removeListener('click', openCropperWindow); tray.removeListener('right-click', openContextMenu); }; -const resetTray = () => { +export const resetTray = () => { if (trayAnimation) { clearTimeout(trayAnimation); } + // TODO: figure out why it's marked like this. Tray extends EventEmitter, so removeAllListeners should be available + // @ts-expect-error tray.removeAllListeners('click'); + tray.setImage(path.join(__dirname, '..', 'static', 'menubarDefaultTemplate.png')); tray.on('click', openCropperWindow); tray.on('right-click', openContextMenu); }; -const setRecordingTray = stopRecording => { +export const setRecordingTray = (stopRecording: () => void) => { animateIcon(); + + // TODO: figure out why this is marked as missing. It's defined properly in the electron.d.ts file tray.once('click', stopRecording); }; -const animateIcon = () => new Promise(resolve => { +const animateIcon = async () => new Promise(resolve => { const interval = 20; let i = 0; @@ -64,8 +66,8 @@ const animateIcon = () => new Promise(resolve => { try { tray.setImage(path.join(__dirname, '..', 'static', 'menubar-loading', filename)); next(); - } catch (_) { - trayAnimation = null; + } catch { + trayAnimation = undefined; resolve(); } }, interval); @@ -73,10 +75,3 @@ const animateIcon = () => new Promise(resolve => { next(); }); - -module.exports = { - initializeTray, - disableTray, - setRecordingTray, - resetTray -}; diff --git a/main/utils/ajv.ts b/main/utils/ajv.ts index 62ae54714..23f50d4d3 100644 --- a/main/utils/ajv.ts +++ b/main/utils/ajv.ts @@ -1,10 +1,11 @@ import Ajv, {Options} from 'ajv'; import {Schema as JSONSchema} from 'electron-store'; +import {Except} from 'type-fest'; -export type Schema = Omit & { +export type Schema = Except, 'required'> & { required?: boolean; customType?: string; -} +}; const hexColorValidator = () => { return { @@ -19,18 +20,20 @@ const keyboardShortcutValidator = () => { }; }; -const validators: {[key: string]: (parentSchema: object) => object} = { - 'hexColor': hexColorValidator, - 'keyboardShortcut': keyboardShortcutValidator -}; +// eslint-disable-next-line @typescript-eslint/ban-types +const validators = new Map object>([ + ['hexColor', hexColorValidator], + ['keyboardShortcut', keyboardShortcutValidator] +]); export default class CustomAjv extends Ajv { constructor(options: Options) { super(options); this.addKeyword('customType', { - macro: (schema, parentSchema) => { - const validator = validators[schema]; + // eslint-disable-next-line @typescript-eslint/ban-types + macro: (schema: string, parentSchema: object) => { + const validator = validators.get(schema); if (!validator) { throw new Error(`No custom type found for ${schema}`); @@ -40,7 +43,7 @@ export default class CustomAjv extends Ajv { }, metaSchema: { type: 'string', - enum: [Object.keys(validators)] + enum: [...validators.keys()] } }); } diff --git a/main/utils/deep-linking.js b/main/utils/deep-linking.ts similarity index 53% rename from main/utils/deep-linking.js rename to main/utils/deep-linking.ts index b02f3c33d..13174957f 100644 --- a/main/utils/deep-linking.js +++ b/main/utils/deep-linking.ts @@ -1,13 +1,12 @@ -'use strict'; +import {windowManager} from '../windows/manager'; -const {openPrefsWindow} = require('../preferences'); -const pluginPromises = new Map(); +const pluginPromises = new Map void>(); -const handlePluginsDeepLink = path => { +const handlePluginsDeepLink = (path: string) => { const [plugin, ...rest] = path.split('/'); if (pluginPromises.has(plugin)) { - pluginPromises.get(plugin)(rest.join('/')); + pluginPromises.get(plugin)?.(rest.join('/')); pluginPromises.delete(plugin); return; } @@ -15,11 +14,11 @@ const handlePluginsDeepLink = path => { console.error(`Received link for plugin “${plugin}” but there was no registered listener.`); }; -const addPluginPromise = (plugin, resolveFunction) => { +export const addPluginPromise = (plugin: string, resolveFunction: (path: string) => void) => { pluginPromises.set(plugin, resolveFunction); }; -const triggerPluginAction = action => name => openPrefsWindow({target: {name, action}}); +const triggerPluginAction = (action: string) => (name: string) => windowManager.preferences?.open({target: {name, action}}); const routes = new Map([ ['plugins', handlePluginsDeepLink], @@ -27,17 +26,12 @@ const routes = new Map([ ['configure-plugin', triggerPluginAction('configure')] ]); -const handleDeepLink = url => { +export const handleDeepLink = (url: string) => { const {host, pathname} = new URL(url); if (routes.has(host)) { - return routes.get(host)(pathname.slice(1)); + return routes.get(host)?.(pathname.slice(1)); } console.error(`Route not recognized: ${host} (${url}).`); }; - -module.exports = { - handleDeepLink, - addPluginPromise -}; diff --git a/main/utils/devices.js b/main/utils/devices.ts similarity index 53% rename from main/utils/devices.js rename to main/utils/devices.ts index 75f6844bf..667ced430 100644 --- a/main/utils/devices.js +++ b/main/utils/devices.ts @@ -1,12 +1,13 @@ -'use strict'; - -const audioDevices = require('macos-audio-devices'); +import {hasMicrophoneAccess} from '../common/system-permissions'; +import * as audioDevices from 'macos-audio-devices'; +import {settings} from '../common/settings'; +import {defaultInputDeviceId} from '../common/constants'; +import Sentry from './sentry'; const aperture = require('aperture'); const {showError} = require('./errors'); -const {hasMicrophoneAccess} = require('../common/system-permissions'); -const getAudioDevices = async () => { +export const getAudioDevices = async () => { if (!hasMicrophoneAccess()) { return []; } @@ -34,7 +35,6 @@ const getAudioDevices = async () => { const devices = await aperture.audioDevices(); if (!Array.isArray(devices)) { - const Sentry = require('./sentry').default; Sentry.captureException(new Error(`devices is not an array: ${JSON.stringify(devices)}`)); showError(error); return []; @@ -48,7 +48,7 @@ const getAudioDevices = async () => { } }; -const getDefaultInputDevice = () => { +export const getDefaultInputDevice = () => { try { const device = audioDevices.getDefaultInputDevice.sync(); return { @@ -57,7 +57,29 @@ const getDefaultInputDevice = () => { }; } catch { // Running on 10.13 and don't have swift support libs. No need to report + return undefined; + } +}; + +export const getSelectedInputDeviceId = () => { + const audioInputDeviceId = settings.get('audioInputDeviceId', defaultInputDeviceId); + + if (audioInputDeviceId === defaultInputDeviceId) { + const device = getDefaultInputDevice(); + return device?.id; } + + return audioInputDeviceId; }; -module.exports = {getAudioDevices, getDefaultInputDevice}; +export const initializeDevices = async () => { + const audioInputDeviceId = settings.get('audioInputDeviceId'); + + if (hasMicrophoneAccess()) { + const devices = await getAudioDevices(); + + if (!devices.some((device: any) => device.id === audioInputDeviceId)) { + settings.set('audioInputDeviceId', defaultInputDeviceId); + } + } +}; diff --git a/main/utils/dock.js b/main/utils/dock.ts similarity index 61% rename from main/utils/dock.js rename to main/utils/dock.ts index c2dbf132a..fbf10d546 100644 --- a/main/utils/dock.js +++ b/main/utils/dock.ts @@ -1,6 +1,7 @@ -const {app} = require('electron'); +import {app} from 'electron'; +import {Promisable} from 'type-fest'; -const ensureDockIsShowing = async action => { +export const ensureDockIsShowing = async (action: () => Promisable) => { const wasDockShowing = app.dock.isVisible(); if (!wasDockShowing) { await app.dock.show(); @@ -13,7 +14,7 @@ const ensureDockIsShowing = async action => { } }; -const ensureDockIsShowingSync = action => { +export const ensureDockIsShowingSync = (action: () => void) => { const wasDockShowing = app.dock.isVisible(); if (!wasDockShowing) { app.dock.show(); @@ -25,8 +26,3 @@ const ensureDockIsShowingSync = action => { app.dock.hide(); } }; - -module.exports = { - ensureDockIsShowing, - ensureDockIsShowingSync -}; diff --git a/main/utils/errors.js b/main/utils/errors.ts similarity index 60% rename from main/utils/errors.js rename to main/utils/errors.ts index d02342712..3f59598ca 100644 --- a/main/utils/errors.js +++ b/main/utils/errors.ts @@ -1,16 +1,16 @@ -'use strict'; - -const path = require('path'); -const {clipboard, shell, app} = require('electron'); -const ensureError = require('ensure-error'); -const cleanStack = require('clean-stack'); -const isOnline = require('is-online'); -const {openNewGitHubIssue} = require('electron-util'); -const got = require('got'); -const delay = require('delay'); -const macosRelease = require('macos-release'); - -const {showDialog} = require('../dialog'); +import path from 'path'; +import {clipboard, shell, app} from 'electron'; +import ensureError from 'ensure-error'; +import cleanStack from 'clean-stack'; +import isOnline from 'is-online'; +import {openNewGitHubIssue} from 'electron-util'; +import got from 'got'; +import delay from 'delay'; +import macosRelease from 'macos-release'; + +import {windowManager} from '../windows/manager'; +import Sentry, {isSentryEnabled} from './sentry'; +import {InstalledPlugin} from '../plugins/plugin'; const MAX_RETRIES = 10; @@ -20,9 +20,23 @@ const ERRORS_TO_IGNORE = [ /net::ERR_CONNECTION_CLOSED/ ]; -const shouldIgnoreError = errorText => ERRORS_TO_IGNORE.some(regex => regex.test(errorText)); +const shouldIgnoreError = (errorText: string) => ERRORS_TO_IGNORE.some(regex => regex.test(errorText)); + +type SentryIssue = { + issueId: string; + shortId: string; + permalink: string; + ghUrl: string; +} | { + issueId: string; + shortId: string; + permalink: string; + ghIssueTemplate: string; +} | { + error: string; +}; -const getSentryIssue = async (eventId, tries = 0) => { +const getSentryIssue = async (eventId: string, tries = 0): Promise => { if (tries > MAX_RETRIES) { return; } @@ -36,7 +50,7 @@ const getSentryIssue = async (eventId, tries = 0) => { if (body.pending) { await delay(2000); - return getSentryIssue(eventId, tries + 1); + return await getSentryIssue(eventId, tries + 1); } return body; @@ -46,14 +60,14 @@ const getSentryIssue = async (eventId, tries = 0) => { } }; -const getPrettyStack = error => { +const getPrettyStack = (error: Error) => { const pluginsPath = path.join(app.getPath('userData'), 'plugins', 'node_modules'); - return cleanStack(error.stack, {pretty: true, basePath: pluginsPath}); + return cleanStack(error.stack ?? '', {pretty: true, basePath: pluginsPath}); }; const release = macosRelease(); -const getIssueBody = (title, errorStack, sentryTemplate) => ` +const getIssueBody = (title: string, errorStack: string, sentryTemplate = '') => ` ${sentryTemplate} @@ -70,10 +84,19 @@ ${errorStack} `; -const showError = async (error, {title: customTitle, plugin} = {}) => { +export const showError = async ( + error: Error, + { + title: customTitle, + plugin + }: { + title?: string; + plugin?: InstalledPlugin; + } = {} +) => { await app.whenReady(); const ensuredError = ensureError(error); - const title = customTitle || ensuredError.name; + const title = customTitle ?? ensuredError.name; const detail = getPrettyStack(ensuredError); console.log(error); @@ -85,7 +108,9 @@ const showError = async (error, {title: customTitle, plugin} = {}) => { 'Don\'t Report', { label: 'Copy Error', - action: () => clipboard.writeText(`${title}\n${detail}`) + action: () => { + clipboard.writeText(`${title}\n${detail}`); + } } ]; @@ -102,7 +127,7 @@ const showError = async (error, {title: customTitle, plugin} = {}) => { } }; - return showDialog({ + return windowManager.dialog?.open({ title, detail, cancelId: 0, @@ -111,11 +136,8 @@ const showError = async (error, {title: customTitle, plugin} = {}) => { }); } - // Avoids circular dependency - const {default: Sentry, isSentryEnabled} = require('./sentry'); - let message; - const buttons = [...mainButtons]; + const buttons: any[] = [...mainButtons]; if (isOnline && isSentryEnabled) { const eventId = Sentry.captureException(ensuredError); @@ -126,21 +148,21 @@ const showError = async (error, {title: customTitle, plugin} = {}) => { buttons.push({ label: 'Collect Info and Report', activeLabel: 'Collecting Info…', - action: async (_, updateUi) => { + action: async (_: unknown, updateUi: any) => { const issue = await sentryIssuePromise; - if (!issue || issue.error) { + if (!issue || 'error' in issue) { updateUi({ message: 'Something went wrong while collecting the information.', buttons: mainButtons }); - } else if (issue.ghUrl) { + } else if ('ghUrl' in issue) { updateUi({ message: 'This issue is already being tracked!', buttons: [ ...mainButtons, { label: 'View Issue', - action: () => shell.openExternal(issue.ghUrl) + action: async () => shell.openExternal(issue.ghUrl) } ] }); @@ -150,13 +172,15 @@ const showError = async (error, {title: customTitle, plugin} = {}) => { ...mainButtons, { label: 'Open Issue', - action: () => openNewGitHubIssue({ - user: 'wulkano', - repo: 'kap', - title, - body: getIssueBody(title, detail, issue.ghIssueTemplate), - labels: ['sentry'] - }) + action: () => { + openNewGitHubIssue({ + user: 'wulkano', + repo: 'kap', + title, + body: getIssueBody(title, detail, issue.ghIssueTemplate), + labels: ['sentry'] + }); + } } ] }); @@ -165,7 +189,7 @@ const showError = async (error, {title: customTitle, plugin} = {}) => { }); } - return showDialog({ + return windowManager.dialog?.open({ title, detail, buttons, @@ -175,14 +199,12 @@ const showError = async (error, {title: customTitle, plugin} = {}) => { }); }; -const setupErrorHandling = () => { +export const setupErrorHandling = () => { process.on('uncaughtException', error => { showError(error, {title: 'Unhandled Error'}); }); process.on('unhandledRejection', error => { - showError(error, {title: 'Unhandled Promise Rejection'}); + showError(ensureError(error), {title: 'Unhandled Promise Rejection'}); }); }; - -module.exports = {showError, setupErrorHandling}; diff --git a/main/utils/formats.ts b/main/utils/formats.ts index 28f22bd4b..8a157ee93 100644 --- a/main/utils/formats.ts +++ b/main/utils/formats.ts @@ -9,5 +9,5 @@ const formats = new Map([ ]); export const prettifyFormat = (format: Format): string => { - return formats.get(format) as string; -} + return formats.get(format)!; +}; diff --git a/main/utils/icon.js b/main/utils/icon.ts similarity index 61% rename from main/utils/icon.js rename to main/utils/icon.ts index 854a7b31f..d11ed30e0 100644 --- a/main/utils/icon.js +++ b/main/utils/icon.ts @@ -1,12 +1,7 @@ -'use strict'; const fileIcon = require('file-icon'); -const getAppIcon = async () => { +export const getAppIcon = async (): Promise => { const buffer = await fileIcon.buffer(process.pid); return buffer.toString('base64'); }; - -module.exports = { - getAppIcon -}; diff --git a/main/utils/image-preview.ts b/main/utils/image-preview.ts new file mode 100644 index 000000000..37b46ffb5 --- /dev/null +++ b/main/utils/image-preview.ts @@ -0,0 +1,59 @@ +/* eslint-disable array-element-newline */ + +import {BrowserWindow, dialog} from 'electron'; +import util from 'electron-util'; +import execa from 'execa'; +import tempy from 'tempy'; +import {promisify} from 'util'; +import type {Video} from '../video'; +import {generateTimestampedName} from './timestamped-name'; + +const base64Img = require('base64-img'); +const ffmpeg = require('@ffmpeg-installer/ffmpeg'); +const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path); + +const getBase64 = promisify(base64Img.base64); + +export const generatePreviewImage = async (filePath: string): Promise<{path: string; data: string} | undefined> => { + const previewPath = tempy.file({extension: '.jpg'}); + + try { + await execa(ffmpegPath, [ + '-ss', '0', + '-i', filePath, + '-t', '1', + '-vframes', '1', + '-f', 'image2', + previewPath + ]); + } catch { + return; + } + + try { + return { + path: previewPath, + data: await getBase64(previewPath) + }; + } catch { + return { + path: previewPath, + data: '' + }; + } +}; + +export const saveSnapshot = async (video: Video, time: number) => { + const {filePath: outputPath} = await dialog.showSaveDialog(BrowserWindow.getFocusedWindow()!, { + defaultPath: generateTimestampedName('Snapshot', '.jpg') + }); + + if (outputPath) { + await execa(ffmpegPath, [ + '-i', video.filePath, + '-ss', time.toString(), + '-vframes', '1', + outputPath + ]); + } +}; diff --git a/main/utils/macos-version.js b/main/utils/macos-version.js deleted file mode 100644 index 4448a987e..000000000 --- a/main/utils/macos-version.js +++ /dev/null @@ -1,3 +0,0 @@ -const macosVersion = require('macos-version'); - -module.exports = macosVersion; diff --git a/main/utils/notifications.ts b/main/utils/notifications.ts index adafd0b26..202af8f32 100644 --- a/main/utils/notifications.ts +++ b/main/utils/notifications.ts @@ -17,7 +17,7 @@ interface NotificationOptions extends NotificationConstructorOptions { type NotificationPromise = Promise & { show: () => void; close: () => void; -} +}; export const notify = (options: NotificationOptions): NotificationPromise => { const notification = new Notification(options); @@ -27,7 +27,7 @@ export const notify = (options: NotificationOptions): NotificationPromise => { const promise = new Promise(resolve => { if (options.click && typeof options.click === 'function') { notification.on('click', () => { - resolve(options.click?.()) + resolve(options.click?.()); }); } @@ -36,7 +36,7 @@ export const notify = (options: NotificationOptions): NotificationPromise => { const button = options.actions?.[index]; if (button?.action && typeof button?.action === 'function') { - resolve(button?.action?.()) + resolve(button?.action?.()); } else { resolve(index); } @@ -49,16 +49,16 @@ export const notify = (options: NotificationOptions): NotificationPromise => { }); promise.then(() => { - notifications.delete(notification) + notifications.delete(notification); }); (promise as NotificationPromise).show = () => { notification.show(); - } + }; (promise as NotificationPromise).close = () => { notification.close(); - } + }; if (options.show ?? true) { notification.show(); diff --git a/main/utils/open-files.js b/main/utils/open-files.js deleted file mode 100644 index a43b33bdd..000000000 --- a/main/utils/open-files.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; -const path = require('path'); - -const {supportedVideoExtensions} = require('../common/constants'); -const {openEditorWindow} = require('../editor'); -const {getEncoding, convertToH264} = require('./encoding'); - -const fileExtensions = supportedVideoExtensions.map(ext => `.${ext}`); - -const openFiles = (...filePaths) => { - return Promise.all( - filePaths - .filter(filePath => fileExtensions.includes(path.extname(filePath).toLowerCase())) - .map(async filePath => { - const encoding = await getEncoding(filePath); - if (encoding.toLowerCase() === 'hevc') { - openEditorWindow(await convertToH264(filePath), {originalFilePath: filePath}); - } else { - openEditorWindow(filePath); - } - }) - ); -}; - -module.exports = openFiles; diff --git a/main/utils/open-files.ts b/main/utils/open-files.ts new file mode 100644 index 000000000..73e2f0e6a --- /dev/null +++ b/main/utils/open-files.ts @@ -0,0 +1,19 @@ +'use strict'; +import path from 'path'; +import {supportedVideoExtensions} from '../common/constants'; +import {Video} from '../video'; + +const fileExtensions = supportedVideoExtensions.map(ext => `.${ext}`); + +export const openFiles = async (...filePaths: string[]) => { + return Promise.all( + filePaths + .filter(filePath => fileExtensions.includes(path.extname(filePath).toLowerCase())) + .map(async filePath => { + return Video.getOrCreate({ + filePath + }).openEditorWindow(); + }) + ); +}; + diff --git a/main/utils/plugin-config.js b/main/utils/plugin-config.js deleted file mode 100644 index cfaa54f9e..000000000 --- a/main/utils/plugin-config.js +++ /dev/null @@ -1,67 +0,0 @@ -import Store from 'electron-store'; -import Ajv from './ajv'; - -class PluginConfig extends Store { - constructor(plugin) { - const defaults = {}; - - const validators = plugin.allServices.filter(({config}) => Boolean(config)).map(service => { - const schemaProps = JSON.parse(JSON.stringify(service.config)); - const requiredKeys = []; - for (const key of Object.keys(schemaProps)) { - if (!schemaProps[key].title) { - throw new Error('Config schema items should have a `title`'); - } - - if (schemaProps[key].required === true) { - delete schemaProps[key].required; - requiredKeys.push(key); - } - } - - const schema = { - type: 'object', - properties: schemaProps, - required: requiredKeys - }; - - const ajv = new Ajv({ - format: 'full', - useDefaults: true, - errorDataPath: 'property', - allErrors: true - }); - - const validator = ajv.compile(schema); - - validator(defaults); - validator.title = service.title; - validator.description = service.configDescription; - validator.config = service.config; - - return validator; - }); - - super({ - name: plugin.name, - cwd: 'plugins', - defaults - }); - - this.servicesWithNoConfig = plugin.allServices.filter(({config}) => !config); - this.validators = validators; - } - - isConfigValid() { - return this.validators.reduce((isValid, validator) => isValid && validator(this.store), true); - } - - get validServices() { - return [ - ...this.validators.filter(validator => validator(this.store)), - ...this.servicesWithNoConfig - ].map(service => service.title); - } -} - -module.exports = PluginConfig; diff --git a/main/utils/routes.js b/main/utils/routes.js deleted file mode 100644 index 07e15019b..000000000 --- a/main/utils/routes.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -const {app} = require('electron'); -const {is} = require('electron-util'); - -const loadRoute = (win, routeName, {openDevTools} = {}) => { - if (is.development) { - win.loadURL(`http://localhost:8000/${routeName}`); - win.openDevTools({mode: 'detach'}); - } else { - win.loadFile(`${app.getAppPath()}/renderer/out/${routeName}.html`); - if (openDevTools) { - win.openDevTools({mode: 'detach'}); - } - } -}; - -module.exports = loadRoute; diff --git a/main/utils/routes.ts b/main/utils/routes.ts new file mode 100644 index 000000000..f456637e0 --- /dev/null +++ b/main/utils/routes.ts @@ -0,0 +1,14 @@ +import {app, BrowserWindow} from 'electron'; +import {is} from 'electron-util'; + +export const loadRoute = (window: BrowserWindow, routeName: string, {openDevTools}: {openDevTools?: boolean} = {}) => { + if (is.development) { + window.loadURL(`http://localhost:8000/${routeName}`); + window.webContents.openDevTools({mode: 'detach'}); + } else { + window.loadFile(`${app.getAppPath()}/renderer/out/${routeName}.html`); + if (openDevTools) { + window.webContents.openDevTools({mode: 'detach'}); + } + } +}; diff --git a/main/utils/sentry.ts b/main/utils/sentry.ts index dcb6b9c22..4ca7c6c23 100644 --- a/main/utils/sentry.ts +++ b/main/utils/sentry.ts @@ -3,7 +3,7 @@ import {app} from 'electron'; import {is} from 'electron-util'; import * as Sentry from '@sentry/electron'; -import settings from '../common/settings'; +import {settings} from '../common/settings'; const SENTRY_PUBLIC_DSN = 'https://2dffdbd619f34418817f4db3309299ce@sentry.io/255536'; diff --git a/main/utils/timestamped-name.ts b/main/utils/timestamped-name.ts index 237aefb74..d61105cb5 100644 --- a/main/utils/timestamped-name.ts +++ b/main/utils/timestamped-name.ts @@ -1,3 +1,3 @@ import moment from 'moment'; -export const generateTimestampedName = (title = 'New Recording', extension = '') => `${title} ${moment().format('YYYY-MM-DD')} at ${moment().format('HH.mm.ss')}${extension}` +export const generateTimestampedName = (title = 'Kapture', extension = '') => `${title} ${moment().format('YYYY-MM-DD')} at ${moment().format('HH.mm.ss')}${extension}`; diff --git a/main/video.ts b/main/video.ts index 1782c01d7..3475b521a 100644 --- a/main/video.ts +++ b/main/video.ts @@ -1,37 +1,39 @@ import path from 'path'; import getFps from './utils/fps'; import {getEncoding, convertToH264} from './utils/encoding'; -import {Rectangle, screen} from 'electron'; -import {Encoding} from './common/types'; +import {nativeImage, NativeImage, screen} from 'electron'; +import {ApertureOptions, Encoding} from './common/types'; import {generateTimestampedName} from './utils/timestamped-name'; +import fs from 'fs'; +import {generatePreviewImage} from './utils/image-preview'; +import {windowManager} from './windows/manager'; interface VideoOptions { filePath: string; title?: string; fps?: number; encoding?: Encoding; + previewPath?: string; pixelDensity?: number; + isNewRecording?: boolean; } export class Video { static all = new Map(); - static fromId(id: string) { - return this.all.get(id); - } - filePath: string; title: string; fps?: number; encoding?: Encoding; pixelDensity: number; previewPath?: string; - + dragIcon?: NativeImage; isNewRecording = false; - isReady = false; - private readyPromise: Promise; - private previewReadyPromise: Promise; + previewImage?: {path: string; data: string}; + + private readonly readyPromise: Promise; + private readonly previewReadyPromise: Promise; constructor(options: VideoOptions) { this.filePath = options.filePath; @@ -39,20 +41,21 @@ export class Video { this.fps = options.fps; this.encoding = options.encoding; this.pixelDensity = options.pixelDensity ?? 1; + this.isNewRecording = options.isNewRecording ?? false; + this.previewPath = options.previewPath; Video.all.set(this.filePath, this); this.readyPromise = this.collectInfo(); - this.previewReadyPromise = this.readyPromise.then(() => this.getPreviewPath()); + this.previewReadyPromise = this.readyPromise.then(async () => this.getPreviewPath()); } - private async collectInfo() { - await Promise.all([ - this.getFps(), - this.getEncoding(), - ]); + static fromId(id: string) { + return this.all.get(id); + } - this.isReady = true; + static getOrCreate(options: VideoOptions) { + return Video.fromId(options.filePath) ?? new Video(options); } async getFps() { @@ -63,6 +66,15 @@ export class Video { return this.fps; } + async exists() { + try { + await fs.promises.access(this.filePath, fs.constants.F_OK); + return true; + } catch { + return false; + } + } + async getEncoding() { if (!this.encoding) { this.encoding = (await getEncoding(this.filePath)) as Encoding; @@ -72,6 +84,10 @@ export class Video { } async getPreviewPath() { + if (!await this.exists()) { + return; + } + if (!this.previewPath) { if (this.encoding === 'h264') { this.previewPath = this.filePath; @@ -80,7 +96,26 @@ export class Video { } } - return this.encoding; + return this.previewPath; + } + + async getDragIcon({width, height}: {width: number; height: number}) { + const previewImagePath = (await this.generatePreviewImage())?.path; + + if (previewImagePath) { + const resizeOptions = width > height ? {width: 64} : {height: 64}; + return nativeImage.createFromPath(previewImagePath).resize(resizeOptions); + } + + return nativeImage.createEmpty(); + } + + async generatePreviewImage() { + if (!this.previewImage) { + this.previewImage = await generatePreviewImage(this.filePath); + } + + return this.previewImage; } async whenReady() { @@ -90,16 +125,23 @@ export class Video { async whenPreviewReady() { return this.previewReadyPromise; } -} -interface ApertureOptions { - fps: number; - cropArea: Rectangle; - showCursor: boolean; - highlightClicks: boolean; - screenId: number; - audioDeviceId: string; - videoCodec: Encoding; + async openEditorWindow() { + return windowManager.editor?.open(this); + } + + private async collectInfo() { + if (!await this.exists()) { + return; + } + + await Promise.all([ + this.getFps(), + this.getEncoding() + ]); + + this.isReady = true; + } } export class Recording extends Video { @@ -111,9 +153,9 @@ export class Recording extends Video { super({ filePath: options.filePath, - title: options.title || generateTimestampedName(), + title: options.title ?? generateTimestampedName(), fps: options.apertureOptions.fps, - encoding: options.apertureOptions.videoCodec, + encoding: options.apertureOptions.videoCodec ?? Encoding.h264, pixelDensity }); diff --git a/main/config.js b/main/windows/config.ts similarity index 73% rename from main/config.js rename to main/windows/config.ts index 5396aa77a..05e61d8bb 100644 --- a/main/config.js +++ b/main/windows/config.ts @@ -1,14 +1,14 @@ 'use strict'; -const {BrowserWindow} = require('electron'); -const {ipcMain: ipc} = require('electron-better-ipc'); -const pEvent = require('p-event'); +import {BrowserWindow} from 'electron'; +import {ipcMain as ipc} from 'electron-better-ipc'; +import pEvent from 'p-event'; -const loadRoute = require('./utils/routes'); -const {openPrefsWindow} = require('./preferences'); +import {loadRoute} from '../utils/routes'; +import {windowManager} from './manager'; -const openConfigWindow = async pluginName => { - const prefsWindow = await openPrefsWindow(); +const openConfigWindow = async (pluginName: string) => { + const prefsWindow = await windowManager.preferences?.open(); const configWindow = new BrowserWindow({ width: 320, height: 436, @@ -36,7 +36,7 @@ const openConfigWindow = async pluginName => { await pEvent(configWindow, 'closed'); }; -const openEditorConfigWindow = async (pluginName, serviceTitle, editorWindow) => { +const openEditorConfigWindow = async (pluginName: string, serviceTitle: string, editorWindow: BrowserWindow) => { const configWindow = new BrowserWindow({ width: 480, height: 420, @@ -68,6 +68,6 @@ ipc.answerRenderer('open-edit-config', async ({pluginName, serviceTitle}, window return openEditorConfigWindow(pluginName, serviceTitle, window); }); -module.exports = { - openConfigWindow -}; +windowManager.setConfig({ + open: openConfigWindow +}); diff --git a/main/cropper.js b/main/windows/cropper.ts similarity index 67% rename from main/cropper.js rename to main/windows/cropper.ts index 7a7994285..7aefde331 100644 --- a/main/cropper.js +++ b/main/windows/cropper.ts @@ -1,22 +1,18 @@ -'use strict'; -const electron = require('electron'); -const delay = require('delay'); +import {windowManager} from './manager'; +import {BrowserWindow, systemPreferences, dialog, screen, Display, app} from 'electron'; +import delay from 'delay'; -const settings = require('./common/settings').default; -const {hasMicrophoneAccess, ensureMicrophonePermissions, openSystemPreferences, ensureScreenCapturePermissions} = require('./common/system-permissions'); -const loadRoute = require('./utils/routes'); -const {checkForAnyBlockingEditors} = require('./editor'); +import {settings} from '../common/settings'; +import {hasMicrophoneAccess, ensureMicrophonePermissions, openSystemPreferences, ensureScreenCapturePermissions} from '../common/system-permissions'; +import {loadRoute} from '../utils/routes'; +import {MacWindow} from '../common/windows'; -const {BrowserWindow, systemPreferences, dialog} = electron; - -const croppers = new Map(); -let notificationId = null; +const croppers = new Map(); +let notificationId: number | undefined; let isOpen = false; const closeAllCroppers = () => { - const {screen} = electron; - screen.removeAllListeners('display-removed'); screen.removeAllListeners('display-added'); @@ -27,13 +23,13 @@ const closeAllCroppers = () => { isOpen = false; - if (notificationId !== null) { + if (notificationId !== undefined) { systemPreferences.unsubscribeWorkspaceNotification(notificationId); - notificationId = null; + notificationId = undefined; } }; -const openCropper = (display, activeDisplayId) => { +const openCropper = (display: Display, activeDisplayId?: number) => { const {id, bounds} = display; const {x, y, width, height} = bounds; @@ -45,7 +41,7 @@ const openCropper = (display, activeDisplayId) => { hasShadow: false, enableLargerThanScreen: true, resizable: false, - moveable: false, + movable: false, frame: false, transparent: true, show: false, @@ -71,7 +67,9 @@ const openCropper = (display, activeDisplayId) => { if (isActive) { const savedCropper = settings.get('cropper', {}); + // @ts-expect-error if (savedCropper.displayId === id) { + // @ts-expect-error displayInfo.cropper = savedCropper; } } @@ -86,7 +84,7 @@ const openCropper = (display, activeDisplayId) => { const openCropperWindow = async () => { closeAllCroppers(); - if (checkForAnyBlockingEditors()) { + if (windowManager.editor?.areAnyBlocking()) { return; } @@ -108,7 +106,7 @@ const openCropperWindow = async () => { }); if (response === 0) { - openSystemPreferences(); + openSystemPreferences('Privacy_Microphone'); return false; } @@ -127,7 +125,6 @@ const openCropperWindow = async () => { isOpen = true; - const {screen} = electron; const displays = screen.getAllDisplays(); const activeDisplayId = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()).id; @@ -139,17 +136,24 @@ const openCropperWindow = async () => { cropper.showInactive(); } - croppers.get(activeDisplayId).focus(); - notificationId = systemPreferences.subscribeWorkspaceNotification('NSWorkspaceActiveSpaceDidChangeNotification', () => { + croppers.get(activeDisplayId)?.focus(); + + // Electron typing issue, this should be marked as returning a number + notificationId = (systemPreferences as any).subscribeWorkspaceNotification('NSWorkspaceActiveSpaceDidChangeNotification', () => { closeAllCroppers(); }); - screen.on('display-removed', (event, oldDisplay) => { + screen.on('display-removed', (_, oldDisplay) => { const {id} = oldDisplay; const cropper = croppers.get(id); + if (!cropper) { + return; + } + const wasFocused = cropper.isFocused(); + // @ts-expect-error cropper.removeAllListeners('closed'); cropper.destroy(); croppers.delete(id); @@ -157,27 +161,27 @@ const openCropperWindow = async () => { if (wasFocused) { const activeDisplayId = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()).id; if (croppers.has(activeDisplayId)) { - croppers.get(activeDisplayId).focus(); + croppers.get(activeDisplayId)?.focus(); } } }); - screen.on('display-added', (event, newDisplay) => { + screen.on('display-added', (_, newDisplay) => { const cropper = openCropper(newDisplay); cropper.showInactive(); }); }; -const preventDefault = event => event.preventDefault(); +const preventDefault = (event: any) => event.preventDefault(); -const selectApp = async (window, activateWindow) => { +const selectApp = async (window: MacWindow, activateWindow: (ownerName: string) => Promise) => { for (const cropper of croppers.values()) { + // @ts-expect-error cropper.prependListener('blur', preventDefault); } await activateWindow(window.ownerName); - const {screen} = electron; const {x, y, width, height, ownerName} = window; const display = screen.getDisplayMatching({x, y, width, height}); @@ -191,9 +195,9 @@ const selectApp = async (window, activateWindow) => { cropper.webContents.send('blur'); } - croppers.get(id).focus(); + croppers.get(id)?.focus(); - croppers.get(id).webContents.send('select-app', { + croppers.get(id)?.webContents.send('select-app', { ownerName, x: x - screenX, y: y - screenY, @@ -203,12 +207,13 @@ const selectApp = async (window, activateWindow) => { }; const disableCroppers = () => { - if (notificationId !== null) { + if (notificationId !== undefined) { systemPreferences.unsubscribeWorkspaceNotification(notificationId); - notificationId = null; + notificationId = undefined; } for (const cropper of croppers.values()) { + // @ts-expect-error cropper.removeAllListeners('blur'); cropper.setIgnoreMouseEvents(true); cropper.setVisibleOnAllWorkspaces(true); @@ -223,11 +228,19 @@ const setRecordingCroppers = () => { const isCropperOpen = () => isOpen; -module.exports = { - openCropperWindow, - closeAllCroppers, +app.on('before-quit', closeAllCroppers); + +app.on('browser-window-created', () => { + if (!isCropperOpen()) { + app.dock.show(); + } +}); + +windowManager.setCropper({ + open: openCropperWindow, + close: closeAllCroppers, selectApp, - setRecordingCroppers, - disableCroppers, - isCropperOpen -}; + setRecording: setRecordingCroppers, + isOpen: isCropperOpen, + disable: disableCroppers +}); diff --git a/main/dialog.js b/main/windows/dialog.ts similarity index 65% rename from main/dialog.js rename to main/windows/dialog.ts index 365573f49..c77118f06 100644 --- a/main/dialog.js +++ b/main/windows/dialog.ts @@ -1,11 +1,16 @@ 'use strict'; -const {BrowserWindow} = require('electron'); -const {ipcMain: ipc} = require('electron-better-ipc'); +import {BrowserWindow, Rectangle} from 'electron'; +import {ipcMain as ipc} from 'electron-better-ipc'; +import {loadRoute} from '../utils/routes'; +import {windowManager} from './manager'; -const loadRoute = require('./utils/routes'); +const DIALOG_MIN_WIDTH = 420; +const DIALOG_MIN_HEIGHT = 150; -const showDialog = options => new Promise(resolve => { +export type DialogOptions = any; + +const showDialog = async (options: DialogOptions) => new Promise(resolve => { const dialogWindow = new BrowserWindow({ width: 1, height: 1, @@ -26,12 +31,12 @@ const showDialog = options => new Promise(resolve => { loadRoute(dialogWindow, 'dialog'); - let buttons; + let buttons: any[]; let wasActionTaken; - const updateUi = async newOptions => { + const updateUi = async (newOptions: DialogOptions) => { wasActionTaken = true; - buttons = newOptions.buttons.map(button => { + buttons = newOptions.buttons.map((button: any) => { if (typeof button === 'string') { return {label: button}; } @@ -41,7 +46,7 @@ const showDialog = options => new Promise(resolve => { const cancelButton = buttons.findIndex(({label}) => label === 'Cancel'); - const {width, height} = await ipc.callRenderer(dialogWindow, 'data', { + const {width, height} = await ipc.callRenderer(dialogWindow, 'data', { cancelId: cancelButton > 0 ? cancelButton : undefined, ...options, ...newOptions, @@ -53,12 +58,12 @@ const showDialog = options => new Promise(resolve => { const titleBarHeight = dialogWindow.getSize()[1] - dialogWindow.getContentSize()[1]; dialogWindow.setBounds({ - width: Math.max(width, bounds.width), - height: Math.max(height + titleBarHeight, bounds.height) + width: Math.max(width, bounds.width, DIALOG_MIN_WIDTH), + height: Math.max(height + titleBarHeight, bounds.height, DIALOG_MIN_HEIGHT) }); }; - const unsubscribe = ipc.answerRenderer(`dialog-action-${dialogWindow.id}`, async index => { + const unsubscribe = ipc.answerRenderer(`dialog-action-${dialogWindow.id}`, async (index: number) => { if (buttons[index]) { if (buttons[index].action) { wasActionTaken = false; @@ -75,7 +80,7 @@ const showDialog = options => new Promise(resolve => { } }); - const cleanup = value => { + const cleanup = (value?: number) => { wasActionTaken = true; unsubscribe(); dialogWindow.close(); @@ -88,6 +93,6 @@ const showDialog = options => new Promise(resolve => { }); }); -module.exports = { - showDialog -}; +windowManager.setDialog({ + open: showDialog +}); diff --git a/main/windows/editor.ts b/main/windows/editor.ts new file mode 100644 index 000000000..672416b38 --- /dev/null +++ b/main/windows/editor.ts @@ -0,0 +1,149 @@ +import {EditorWindowState} from '../common/types'; +import type {Video} from '../video'; +import KapWindow from './kap-window'; +import {MenuItemId} from '../menus/utils'; +import {BrowserWindow, dialog} from 'electron'; +import {is} from 'electron-util'; +import fs from 'fs'; +import {saveSnapshot} from '../utils/image-preview'; +import {windowManager} from './manager'; + +const pify = require('pify'); + +const OPTIONS_BAR_HEIGHT = 48; +const VIDEO_ASPECT = 9 / 16; +const MIN_VIDEO_WIDTH = 900; +const MIN_VIDEO_HEIGHT = MIN_VIDEO_WIDTH * VIDEO_ASPECT; +const MIN_WINDOW_HEIGHT = MIN_VIDEO_HEIGHT + OPTIONS_BAR_HEIGHT; + +const editors = new Map(); +const editorsWithNotSavedDialogs = new Map(); + +const open = async (video: Video) => { + if (editors.has(video.filePath)) { + editors.get(video.filePath).show(); + return; + } + + // TODO: Make this smarter so the editor can show with a spinner while the preview is generated for longer preview conversions (like ProRes) + await video.whenPreviewReady(); + + const editorKapWindow = new KapWindow({ + title: video.title, + // TODO: Return those to the original values when we are able to resize below min size + // Upstream issue: https://github.com/electron/electron/issues/27025 + // minWidth: MIN_VIDEO_WIDTH, + // minHeight: MIN_WINDOW_HEIGHT, + minWidth: 360, + minHeight: 392, + width: MIN_VIDEO_WIDTH, + height: MIN_WINDOW_HEIGHT, + webPreferences: { + nodeIntegration: true, + webSecurity: !is.development // Disable webSecurity in dev to load video over file:// protocol while serving over insecure http, this is not needed in production where we use file:// protocol for html serving. + }, + frame: false, + transparent: true, + vibrancy: 'window', + route: 'editor', + initialState: { + previewFilePath: video.previewPath!, + filePath: video.filePath, + fps: video.fps!, + title: video.title + }, + menu: defaultMenu => { + if (!video.isNewRecording) { + return; + } + + const fileMenu = defaultMenu.find(item => item.id === MenuItemId.file); + + if (fileMenu) { + const submenu = fileMenu.submenu as Electron.MenuItemConstructorOptions[]; + + const index = submenu.findIndex(item => item.id === MenuItemId.openVideo); + + if (index > 0) { + submenu.splice(index, 0, { + type: 'separator' + }, { + label: 'Save Original…', + id: MenuItemId.saveOriginal, + accelerator: 'Command+S', + click: async () => saveOriginal(video) + }); + } + } + } + }); + + const editorWindow = editorKapWindow.browserWindow; + + editors.set(video.filePath, editorWindow); + + if (video.isNewRecording) { + editorWindow.setDocumentEdited(true); + editorWindow.on('close', (event: any) => { + editorsWithNotSavedDialogs.set(video.filePath, true); + const buttonIndex = dialog.showMessageBoxSync(editorWindow, { + type: 'question', + buttons: [ + 'Discard', + 'Cancel' + ], + defaultId: 0, + cancelId: 1, + message: 'Are you sure that you want to discard this recording?', + detail: 'You will no longer be able to edit and export the original recording.' + }); + + if (buttonIndex === 1) { + event.preventDefault(); + } + + editorsWithNotSavedDialogs.delete(video.filePath); + }); + } + + editorWindow.on('closed', () => { + editors.delete(video.filePath); + }); + + editorWindow.on('blur', () => { + editorKapWindow.callRenderer('blur'); + }); + + editorWindow.on('focus', () => { + editorKapWindow.callRenderer('focus'); + }); + + editorKapWindow.answerRenderer('save-snapshot', (time: number) => { + saveSnapshot(video, time); + }); +}; + +const saveOriginal = async (video: Video) => { + const {filePath} = await dialog.showSaveDialog(BrowserWindow.getFocusedWindow()!, { + defaultPath: `${video.title}.mp4` + }); + + if (filePath) { + await pify(fs.copyFile)(video.filePath, filePath, fs.constants.COPYFILE_FICLONE); + } +}; + +const areAnyBlocking = () => { + if (editorsWithNotSavedDialogs.size > 0) { + const [path] = editorsWithNotSavedDialogs.keys(); + editors.get(path).focus(); + return true; + } + + return false; +}; + +windowManager.setEditor({ + open, + areAnyBlocking +}); diff --git a/main/exports.js b/main/windows/exports.ts similarity index 71% rename from main/exports.js rename to main/windows/exports.ts index 66de1735a..83026d104 100644 --- a/main/exports.js +++ b/main/windows/exports.ts @@ -1,11 +1,11 @@ 'use strict'; -const {BrowserWindow, ipcMain} = require('electron'); -const pEvent = require('p-event'); +import {BrowserWindow, ipcMain} from 'electron'; +import pEvent from 'p-event'; +import {loadRoute} from '../utils/routes'; +import {windowManager} from './manager'; -const loadRoute = require('./utils/routes'); - -let exportsWindow = null; +let exportsWindow: BrowserWindow | undefined; const openExportsWindow = async () => { if (exportsWindow) { @@ -34,7 +34,7 @@ const openExportsWindow = async () => { loadRoute(exportsWindow, 'exports'); exportsWindow.on('close', () => { - exportsWindow = null; + exportsWindow = undefined; }); await pEvent(ipcMain, 'exports-ready'); @@ -46,7 +46,7 @@ const openExportsWindow = async () => { const getExportsWindow = () => exportsWindow; -module.exports = { - openExportsWindow, - getExportsWindow -}; +windowManager.setExports({ + open: openExportsWindow, + get: getExportsWindow +}); diff --git a/main/kap-window.ts b/main/windows/kap-window.ts similarity index 56% rename from main/kap-window.ts rename to main/windows/kap-window.ts index c7d5e2c9a..b48f4346c 100644 --- a/main/kap-window.ts +++ b/main/windows/kap-window.ts @@ -1,37 +1,42 @@ -import electron, {BrowserWindow} from 'electron'; +import electron, {app, BrowserWindow, Menu} from 'electron'; import {ipcMain as ipc} from 'electron-better-ipc'; import pEvent from 'p-event'; -import loadRoute from './utils/routes'; +import {customApplicationMenu, defaultApplicationMenu, MenuModifier} from '../menus/application'; +import {loadRoute} from '../utils/routes'; interface KapWindowOptions extends Electron.BrowserWindowConstructorOptions { route: string; waitForMount?: boolean; initialState?: State; + menu?: MenuModifier; + dock?: boolean; } +// TODO: remove this when all windows use KapWindow +app.on('browser-window-focus', (_, window) => { + if (!KapWindow.fromId(window.id)) { + Menu.setApplicationMenu(Menu.buildFromTemplate(defaultApplicationMenu())); + } +}); + // Has to be named BrowserWindow because of // https://github.com/electron/electron/blob/master/lib/browser/api/browser-window.ts#L82 export default class KapWindow { - private static windows = new Map(); - - static getAllWindows() { - return [...this.windows.values()]; - } - - static fromId(id: number) { - return this.windows.get(id); - } - - static defaultOptions = { - waitForMount: true + static defaultOptions: Partial> = { + waitForMount: true, + dock: true, + menu: defaultMenu => defaultMenu }; - private readyPromise: Promise - private cleanupMethods: Function[] = []; - private options: KapWindowOptions; + private static readonly windows = new Map(); browserWindow: BrowserWindow; state?: State; + menu: Menu = Menu.buildFromTemplate(defaultApplicationMenu()); + + private readonly readyPromise: Promise; + private readonly cleanupMethods: Array<() => void> = []; + private readonly options: KapWindowOptions; constructor(props: KapWindowOptions) { const { @@ -57,10 +62,19 @@ export default class KapWindow { }; this.state = initialState; + this.generateMenu(); loadRoute(this.browserWindow, route); this.readyPromise = this.setupWindow(); } + static getAllWindows() { + return [...this.windows.values()]; + } + + static fromId(id: number) { + return this.windows.get(id); + } + get id() { return this.browserWindow.id; } @@ -69,17 +83,66 @@ export default class KapWindow { return this.browserWindow.webContents; } + cleanup = () => { + this.executeIfNotDestroyed(() => { + KapWindow.windows.delete(this.id); + }); + + for (const method of this.cleanupMethods) { + method(); + } + }; + + callRenderer = async (channel: string, data?: T) => { + return ipc.callRenderer(this.browserWindow, channel, data); + }; + + answerRenderer = (channel: string, callback: (data: T, window: electron.BrowserWindow) => R) => { + this.cleanupMethods.push(ipc.answerRenderer(this.browserWindow, channel, callback)); + }; + + setState = (partialState: State) => { + this.state = { + ...this.state, + ...partialState + }; + + this.callRenderer('kap-window-state', this.state); + }; + + whenReady = async () => { + return this.readyPromise; + }; + + private readonly generateMenu = () => { + this.menu = Menu.buildFromTemplate( + customApplicationMenu(this.options.menu!) + ); + }; + private async setupWindow() { const {waitForMount} = this.options; KapWindow.windows.set(this.id, this); + this.browserWindow.on('show', () => { + if (this.options.dock && !app.dock.isVisible) { + app.dock.show(); + } else if (!this.options.dock && app.dock.isVisible) { + app.dock.hide(); + } + }); + this.browserWindow.on('close', this.cleanup); this.browserWindow.on('closed', this.cleanup); + this.browserWindow.on('focus', () => { + this.generateMenu(); + Menu.setApplicationMenu(this.menu); + }); + this.webContents.on('did-finish-load', async () => { if (this.state) { - console.log('sending state', this.state); this.callRenderer('kap-window-state', this.state); } }); @@ -91,38 +154,17 @@ export default class KapWindow { resolve(); }); }); - } else { - await pEvent(this.webContents, 'did-finish-load'); - this.browserWindow.show(); } - } - cleanup = () => { - console.log('Cleaning up', this.cleanupMethods); - KapWindow.windows.delete(this.id); - for (const method of this.cleanupMethods) { - method?.(); - } + await pEvent(this.webContents, 'did-finish-load'); + this.browserWindow.show(); } - callRenderer = (channel: string, data: T) => { - return ipc.callRenderer(this.browserWindow, channel, data); - } - - answerRenderer = (channel: string, callback: (data: T, window: electron.BrowserWindow) => R) => { - this.cleanupMethods.push(ipc.answerRenderer(this.browserWindow, channel, callback)); - } - - setState = (partialState: State) => { - this.state = { - ...this.state, - ...partialState - }; - - this.callRenderer('kap-window-state', this.state); - } - - whenReady = async () => { - return this.readyPromise; - } + // Use this around any call that causes: + // TypeError: Object has been destroyed + private readonly executeIfNotDestroyed = (callback: () => void) => { + if (!this.browserWindow.isDestroyed()) { + callback(); + } + }; } diff --git a/main/windows/load.ts b/main/windows/load.ts new file mode 100644 index 000000000..9f52b620a --- /dev/null +++ b/main/windows/load.ts @@ -0,0 +1,6 @@ +import './editor'; +import './cropper'; +import './config'; +import './dialog'; +import './exports'; +import './preferences'; diff --git a/main/windows/manager.ts b/main/windows/manager.ts new file mode 100644 index 000000000..93e060072 --- /dev/null +++ b/main/windows/manager.ts @@ -0,0 +1,72 @@ +import type {BrowserWindow} from 'electron'; +import {MacWindow} from '../common/windows'; +import type {Video} from '../video'; +import type {DialogOptions} from './dialog'; +import type {PreferencesWindowOptions} from './preferences'; + +export interface EditorManager { + open: (video: Video) => Promise; + areAnyBlocking: () => boolean; +} + +export interface CropperManager { + open: () => Promise; + close: () => void; + disable: () => void; + setRecording: () => void; + isOpen: () => boolean; + selectApp: (window: MacWindow, activateWindow: (ownerName: string) => Promise) => void; +} + +export interface ConfigManager { + open: (pluginName: string) => Promise; +} + +export interface DialogManager { + open: (options: DialogOptions) => Promise; +} + +export interface ExportsManager { + open: () => Promise; + get: () => BrowserWindow | undefined; +} + +export interface PreferencesManager { + open: (options?: PreferencesWindowOptions) => Promise; + close: () => void; +} + +export class WindowManager { + editor?: EditorManager; + cropper?: CropperManager; + config?: ConfigManager; + dialog?: DialogManager; + exports?: ExportsManager; + preferences?: PreferencesManager; + + setEditor = (editorManager: EditorManager) => { + this.editor = editorManager; + }; + + setCropper = (cropperManager: CropperManager) => { + this.cropper = cropperManager; + }; + + setConfig = (configManager: ConfigManager) => { + this.config = configManager; + }; + + setDialog = (dialogManager: DialogManager) => { + this.dialog = dialogManager; + }; + + setExports = (exportsManager: ExportsManager) => { + this.exports = exportsManager; + }; + + setPreferences = (preferencesManager: PreferencesManager) => { + this.preferences = preferencesManager; + }; +} + +export const windowManager = new WindowManager(); diff --git a/main/preferences.js b/main/windows/preferences.ts similarity index 65% rename from main/preferences.js rename to main/windows/preferences.ts index d2c12a1be..a144a265a 100644 --- a/main/preferences.js +++ b/main/windows/preferences.ts @@ -1,19 +1,19 @@ -'use strict'; +import {BrowserWindow} from 'electron'; +import {promisify} from 'util'; +import pEvent from 'p-event'; -const {BrowserWindow} = require('electron'); -const {promisify} = require('util'); -const pEvent = require('p-event'); +import {ipcMain as ipc} from 'electron-better-ipc'; +import {loadRoute} from '../utils/routes'; +import {track} from '../common/analytics'; +import {windowManager} from './manager'; -const {ipcMain: ipc} = require('electron-better-ipc'); -const {closeAllCroppers} = require('./cropper'); -const loadRoute = require('./utils/routes'); -const {track} = require('./common/analytics'); +let prefsWindow: BrowserWindow | undefined; -let prefsWindow = null; +export type PreferencesWindowOptions = any; -const openPrefsWindow = async options => { +const openPrefsWindow = async (options?: PreferencesWindowOptions) => { track('preferences/opened'); - closeAllCroppers(); + windowManager.cropper?.close(); if (prefsWindow) { if (options) { @@ -46,7 +46,7 @@ const openPrefsWindow = async options => { prefsWindow.setSheetOffset(titlebarHeight); prefsWindow.on('close', () => { - prefsWindow = null; + prefsWindow = undefined; }); loadRoute(prefsWindow, 'preferences'); @@ -59,6 +59,7 @@ const openPrefsWindow = async options => { ipc.callRenderer(prefsWindow, 'mount'); + // @ts-expect-error await promisify(ipc.answerRenderer)('preferences-ready'); prefsWindow.show(); @@ -73,7 +74,7 @@ const closePrefsWindow = () => { ipc.answerRenderer('open-preferences', openPrefsWindow); -module.exports = { - openPrefsWindow, - closePrefsWindow -}; +windowManager.setPreferences({ + open: openPrefsWindow, + close: closePrefsWindow +}); diff --git a/package.json b/package.json index 772dc8c54..0223e90d9 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,10 @@ "main": "dist-js/index.js", "scripts": { "lint": "xo", - "test": "xo && ava", - "start": "tsc && electron .", + "lint:fix": "xo --fix", + "test:main": "TS_NODE_PROJECT=test/tsconfig.json ava", + "test": "yarn lint && yarn test:main", + "start": "tsc && run-electron .", "build-main": "tsc", "build-renderer": "next build renderer && next export renderer", "build": "yarn build-main && yarn build-renderer", @@ -39,12 +41,13 @@ "base64-img": "^1.0.4", "classnames": "^2.2.6", "clean-stack": "^3.0.0", + "cp-file": "^9.0.0", "delay": "^4.3.0", "electron-better-ipc": "^1.1.1", "electron-log": "^4.1.1", "electron-next": "^3.1.5", "electron-notarize": "^0.3.0", - "electron-store": "^5.1.1", + "electron-store": "^7.0.2", "electron-updater": "^4.3.1", "electron-util": "^0.14.1", "ensure-error": "^2.0.0", @@ -59,7 +62,7 @@ "mac-screen-capture-permissions": "^1.1.0", "mac-windows": "^0.7.1", "macos-audio-devices": "^1.3.2", - "macos-release": "^2.4.0", + "macos-release": "^2.4.1", "macos-version": "^5.2.0", "make-dir": "^3.1.0", "moment": "^2.24.0", @@ -76,7 +79,6 @@ "pretty-ms": "^7.0.0", "prop-types": "^15.7.2", "react": "^16.13.1", - "react-css-transition-replace": "^4.0.4", "react-dom": "^16.13.1", "react-linkify": "^0.2.2", "read-pkg": "^5.2.0", @@ -90,80 +92,140 @@ "yarn": "^1.22.4" }, "devDependencies": { + "@babel/core": "^7.12.16", + "@babel/eslint-parser": "^7.12.16", "@sindresorhus/tsconfig": "^0.7.0", "@types/got": "9.6.0", "@types/insight": "^0.8.0", + "@types/module-alias": "^2.0.0", "@types/node": "^14.11.10", "@types/object-hash": "^1.3.4", "@types/react": "^16.9.46", - "@typescript-eslint/eslint-plugin": "^4.4.1", - "@typescript-eslint/parser": "^4.4.1", - "ava": "^3.9.0", + "@types/sinon": "^9.0.10", + "@typescript-eslint/eslint-plugin": "^4.15.0", + "@typescript-eslint/parser": "^4.15.0", + "ava": "^3.15.0", "babel-eslint": "^10.1.0", "electron": "8.2.4", "electron-builder": "^22.6.0", "electron-builder-notarize": "^1.1.2", - "eslint-config-xo": "^0.33.1", + "eslint-config-xo": "^0.34.0", "eslint-config-xo-react": "^0.23.0", - "eslint-config-xo-typescript": "^0.35.0", - "eslint-plugin-react": "^7.19.0", - "eslint-plugin-react-hooks": "^3.0.0", + "eslint-config-xo-typescript": "^0.37.0", + "eslint-plugin-react": "^7.22.0", + "eslint-plugin-react-hooks": "^4.2.0", "husky": "^4.2.5", "module-alias": "^2.2.2", "next": "^10.0.4", + "run-electron": "^1.0.0", "sinon": "^9.0.2", + "ts-node": "^9.1.1", + "type-fest": "^0.21.0", + "typed-emitter": "^1.3.1", "typescript": "^4.0.3", "unique-string": "^2.0.0", - "xo": "^0.34.1" + "xo": "^0.37.1" }, "_moduleAliases": { - "electron": "test/mocks/electron.js" + "electron": "test/mocks/electron.ts" }, "ava": { "files": [ + "test/**/*.ts", "test/**/*.js", "!test/helpers", "!test/mocks" ], + "extensions": [ + "ts" + ], "verbose": true, "timeout": "5m", "failFast": true, "require": [ + "ts-node/register", "module-alias/register" ] }, "xo": { - "extends": [ - "xo-react", - "xo-typescript" - ], + "extends": "xo-react", "space": 2, "envs": [ "node", "browser" ], - "parserOptions": { - "project": [ - "tsconfig.json", - "renderer/tsconfig.json" - ] - }, "rules": { + "template-curly-spacing": "off", "import/no-extraneous-dependencies": "off", "import/no-unassigned-import": "off", "react/jsx-closing-tag-location": "off", "react/require-default-props": "off", "react/jsx-curly-brace-presence": "off", "react/static-property-placement": "off", + "react/react-in-jsx-scope": "off", "react/boolean-prop-naming": "off", "unicorn/prefer-set-has": "off", - "ava/use-test": "off" + "ava/use-test": "off", + "import/extensions": "off" }, "ignores": [ "dist-js", "dist", "renderer/.next", - "renderer/out" + "renderer/out", + "renderer/next.config.js" + ], + "overrides": [ + { + "files": [ + "**/*.js", + "**/*.jsx" + ], + "parser": "babel-eslint" + }, + { + "files": [ + "**/*.ts", + "**/*.tsx" + ], + "extends": [ + "xo-react", + "xo-typescript" + ], + "parserOptions": { + "project": [ + "tsconfig.json", + "renderer/tsconfig.json", + "test/tsconfig.json" + ] + }, + "rules": { + "react-hooks/exhaustive-deps": "off", + "@typescript-eslint/no-dynamic-delete": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-implicit-any-catch": "off", + "@typescript-eslint/restrict-template-expressions": "off", + "no-await-in-loop": "off", + "react/prop-types": "off", + "@typescript-eslint/indent": [ + "error", + 2 + ] + } + }, + { + "files": [ + "test/**/*.ts" + ], + "rules": { + "@typescript-eslint/consistent-type-assertions": "off", + "@typescript-eslint/member-ordering": "off", + "import/no-anonymous-default-export": "off", + "@typescript-eslint/no-extraneous-class": "off", + "@typescript-eslint/no-empty-function": "off" + } + } ] }, "husky": { diff --git a/plz.png b/plz.png new file mode 100644 index 0000000000000000000000000000000000000000..130a56dcb12ace69fe8902e68bdf29588e8d677c GIT binary patch literal 809 zcmV+^1J?YBP)@3}ko9?ZyHw`rQugSu5}R|a9N2cu>K z2W#oL!JH5I5b7Ty&3{CtLKiC-wLeA{My&m@6p?g-BZjNR*dS05y?1v{ci!FU<`V=4 z;=Sj5dN2IrbwA(l@Ao^u-#Pajvw4AR0hZuJyn@wu4R7IntVb>4*o>Xni_c((_&F-s zH)Jb*q;4mF#c$|FF9sODOkOkD^(SPBC`#BM@hrv5vSc|bWJ8r;ajF`1vf&17#~#^8 zqipmLj>*QpMF$&<{~$}B!g>6TKV=gGNXc$o0;guC@ULw0o-CV_ll4`Q49P9zpm1HF zNGz|&MqiPJoFYf4KWD6&zLV_7RZRKv4(8>ZCI7%x$-6#J<|S(6CEt`cyj32zxdVW7E5M^7&I^;BYxqA%nWEq$aSPAyuSS8>u*GUV_A7Us2>#Y>oYj)CWgOiSlW zr72TcG*H>odA$^sR?H8yqDWeqaGR+HuoIEZVRrr#i%`pwsc6Jq0XgAHC3DZ6_j)!&NhUrMg)Rb}6G->)OYiiEAYN2kW zZku%VJ!!|YA)TQ3d1{ucUv9eUpgxIHw_KgM>8PtMq6@c7z0;<@rbFtVF%8lW&V=~5 zW*l9)ZMt^b(dZph`mUp?yQXRCdo2a}hx#F7xpf7~?RJ&BZ^~2WYr_9CKOUA}@5(=1 zApdMgexJ!71g8Ztl;d@*#k;6-6jYNN$vBS?G|+6uHhhY`*pEgW#9@4iCVcHGXv1;p z6KJ2^pp)VpyLFM>xQKrGOXL+Y0ltkO{TP32z}+e4NY#8u+Cj6A<_oeJE!5wkmHH?0 nGKjFE;xRdm?JST((B400000NkvXXu0mjfDxHXj literal 0 HcmV?d00001 diff --git a/renderer/components/action-bar/controls/advanced.js b/renderer/components/action-bar/controls/advanced.js index bcedd8abf..41e64bdc2 100644 --- a/renderer/components/action-bar/controls/advanced.js +++ b/renderer/components/action-bar/controls/advanced.js @@ -62,7 +62,7 @@ const AdvancedControls = {}; const stopPropagation = event => event.stopPropagation(); class Left extends React.Component { - state = {} + state = {}; select = React.createRef(); @@ -92,7 +92,7 @@ class Left extends React.Component { y: Math.round(top) + 6, positioningItem }); - } + }; render() { const {advanced, toggleAdvanced, toggleRatioLock, ratioLocked, ratio = []} = this.props; @@ -214,7 +214,7 @@ class Right extends React.Component { heightInput: heightInput.current.getRef(), ignoreEmpty }); - } + }; onHeightChange = (event, {ignoreEmpty} = {}) => { const {bounds, width, setBounds, ratioLocked, ratio, setHeight} = this.props; @@ -233,17 +233,17 @@ class Right extends React.Component { heightInput: heightInput.current.getRef(), ignoreEmpty }); - } + }; onWidthBlur = event => { this.onWidthChange(event, {ignoreEmpty: false}); handleWidthInput.flush(); - } + }; onHeightBlur = event => { this.onHeightChange(event, {ignoreEmpty: false}); handleHeightInput.flush(); - } + }; render() { const {swapDimensions, width, height, screenWidth, screenHeight, advanced} = this.props; diff --git a/renderer/components/action-bar/controls/main.js b/renderer/components/action-bar/controls/main.js index 64f8c42bd..c1c4cca76 100644 --- a/renderer/components/action-bar/controls/main.js +++ b/renderer/components/action-bar/controls/main.js @@ -86,9 +86,9 @@ MainControls.Left = connect( class Right extends React.Component { onCogMenuClick = async () => { - const cogMenu = await electron.remote.require('./menus').getCogMenu(); + const cogMenu = await electron.remote.require('./menus/cog').getCogMenu(); cogMenu.popup(); - } + }; render() { const {enterFullscreen, exitFullscreen, isFullscreen, advanced} = this.props; diff --git a/renderer/components/action-bar/index.js b/renderer/components/action-bar/index.js index 7d0c3ab36..31a4f2f1a 100644 --- a/renderer/components/action-bar/index.js +++ b/renderer/components/action-bar/index.js @@ -18,7 +18,7 @@ class ActionBar extends React.Component { height: 0, x: 0, y: 0 - } + }; render() { const { diff --git a/renderer/components/action-bar/record-button.js b/renderer/components/action-bar/record-button.js index 9a4e10221..f7480b958 100644 --- a/renderer/components/action-bar/record-button.js +++ b/renderer/components/action-bar/record-button.js @@ -6,7 +6,7 @@ import classNames from 'classnames'; import {connect, CropperContainer} from '../../containers'; import {handleKeyboardActivation} from '../../utils/inputs'; -const getMediaNode = deviceId => new Promise((resolve, reject) => { +const getMediaNode = async deviceId => new Promise((resolve, reject) => { navigator.getUserMedia({ audio: {deviceId} }, stream => { @@ -53,7 +53,7 @@ const RecordButton = ({ javascriptNode.onaudioprocess = () => { const array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); - + // eslint-disable-next-line unicorn/no-array-reduce const avg = array.reduce((p, c) => p + c) / array.length; if (avg >= 36) { setShowFirstRipple(true); @@ -98,7 +98,7 @@ const RecordButton = ({ if (cropperExists) { const {remote} = electron; - const {startRecording} = remote.require('./common/aperture'); + const {startRecording} = remote.require('./aperture'); willStartRecording(); diff --git a/renderer/components/cropper/cursor.js b/renderer/components/cropper/cursor.js index 01b333883..06eb86efa 100644 --- a/renderer/components/cropper/cursor.js +++ b/renderer/components/cropper/cursor.js @@ -6,7 +6,7 @@ import classNames from 'classnames'; import {connect, CursorContainer, CropperContainer} from '../../containers'; class Cursor extends React.Component { - remote = electron.remote || false + remote = electron.remote || false; render() { if (!this.remote) { diff --git a/renderer/components/cropper/handles.js b/renderer/components/cropper/handles.js index b7e3f5ccf..5d255766e 100644 --- a/renderer/components/cropper/handles.js +++ b/renderer/components/cropper/handles.js @@ -12,7 +12,7 @@ class Handle extends React.Component { left: false, right: false, ratioLocked: false - } + }; render() { const { @@ -94,7 +94,7 @@ class Handles extends React.Component { ratioLocked: false, width: 0, height: 0 - } + }; render() { const { diff --git a/renderer/components/cropper/overlay.js b/renderer/components/cropper/overlay.js index 580f10bb7..ce5b44cab 100644 --- a/renderer/components/cropper/overlay.js +++ b/renderer/components/cropper/overlay.js @@ -18,7 +18,7 @@ class Overlay extends React.Component { width: 0, height: 0, isReady: false - } + }; render() { const { diff --git a/renderer/components/dialog/actions.js b/renderer/components/dialog/actions.js index 51aa0d6ac..8e4f52c2a 100644 --- a/renderer/components/dialog/actions.js +++ b/renderer/components/dialog/actions.js @@ -26,7 +26,7 @@ const Actions = ({buttons, performAction, defaultId}) => { key={button.label} type="button" disabled={index === activeButton} - onClick={() => action(index)} + onClick={async () => action(index)} > {index === activeButton ? button.activeLabel || button.label : button.label} diff --git a/renderer/components/dialog/icon.js b/renderer/components/dialog/icon.js index 7344027f6..ab25d98fe 100644 --- a/renderer/components/dialog/icon.js +++ b/renderer/components/dialog/icon.js @@ -1,6 +1,6 @@ import React from 'react'; -export default () => { +const Icon = () => { return (
@@ -18,3 +18,5 @@ export default () => {
); }; + +export default Icon; diff --git a/renderer/components/editor/controls/left.tsx b/renderer/components/editor/controls/left.tsx index 7ea302e56..a857a9642 100644 --- a/renderer/components/editor/controls/left.tsx +++ b/renderer/components/editor/controls/left.tsx @@ -1,4 +1,4 @@ -import VideoControlsContainer from '../video-controls-container' +import VideoControlsContainer from '../video-controls-container'; import VideoTimeContainer from '../video-time-container'; import {PlayIcon, PauseIcon} from '../../../vectors'; import formatTime from '../../../utils/format-time'; @@ -12,8 +12,8 @@ const LeftControls = () => {
{ isPaused ? - : - + : + }
{formatTime(currentTime, {showMilliseconds: false})}
@@ -42,6 +42,6 @@ const LeftControls = () => { `} ); -} +}; export default LeftControls; diff --git a/renderer/components/editor/controls/play-bar.tsx b/renderer/components/editor/controls/play-bar.tsx index 534c82fb5..7e3d21585 100644 --- a/renderer/components/editor/controls/play-bar.tsx +++ b/renderer/components/editor/controls/play-bar.tsx @@ -38,25 +38,29 @@ const PlayBar = () => { if (startTime <= time && time <= endTime) { updateTime(time); } - } + }; const updatePreview = event => { setHoverTime(getTimeFromEvent(event)); - } + }; const startResizing = () => { setResizing(true); pause(); - } + }; const stopResizing = () => { setResizing(false); play(); - } + }; - const setStartTime = event => updateStartTime(Number.parseFloat(event.target.value)) + const setStartTime = event => { + updateStartTime(Number.parseFloat(event.target.value)); + }; - const setEndTime = event => updateEndTime(Number.parseFloat(event.target.value)) + const setEndTime = event => { + updateEndTime(Number.parseFloat(event.target.value)); + }; const previewTime = resizing ? currentTime : hoverTime; const previewLabelTime = resizing ? currentTime : (startTime <= hoverTime && hoverTime <= endTime ? hoverTime - startTime : hoverTime); @@ -66,9 +70,9 @@ const PlayBar = () => {
- +
- +
{ step={0.00001} onChange={setStartTime} onMouseDown={startResizing} - onMouseUp={stopResizing} /> + onMouseUp={stopResizing}/> { step={0.00001} onChange={setEndTime} onMouseDown={startResizing} - onMouseUp={stopResizing} /> + onMouseUp={stopResizing}/>
); -} +}; export default PlayBar; -// import PropTypes from 'prop-types'; +// Import PropTypes from 'prop-types'; // import React from 'react'; // import classNames from 'classnames'; @@ -211,207 +215,207 @@ export default PlayBar; // progress = React.createRef(); - // getTimeFromEvent = event => { - // const {startTime, endTime} = this.props; +// getTimeFromEvent = event => { +// const {startTime, endTime} = this.props; - // const cursorX = event.clientX; - // const {x, width} = this.progress.current.getBoundingClientRect(); +// const cursorX = event.clientX; +// const {x, width} = this.progress.current.getBoundingClientRect(); - // const percent = (cursorX - x) / width; - // const time = startTime + ((endTime - startTime) * percent); +// const percent = (cursorX - x) / width; +// const time = startTime + ((endTime - startTime) * percent); - // return Math.max(0, time); - // } +// return Math.max(0, time); +// } - // seek = event => { - // const {startTime, endTime, seek} = this.props; - // const time = this.getTimeFromEvent(event); +// seek = event => { +// const {startTime, endTime, seek} = this.props; +// const time = this.getTimeFromEvent(event); - // if (startTime <= time && time <= endTime) { - // seek(time); - // } - // } +// if (startTime <= time && time <= endTime) { +// seek(time); +// } +// } - // updatePreview = event => { - // const time = this.getTimeFromEvent(event); - // this.setState({hoverTime: time}); - // } +// updatePreview = event => { +// const time = this.getTimeFromEvent(event); +// this.setState({hoverTime: time}); +// } - // startResizing = () => { - // const {pause} = this.props; - // this.setState({resizing: true}); - // pause(); - // } +// startResizing = () => { +// const {pause} = this.props; +// this.setState({resizing: true}); +// pause(); +// } - // stopResizing = () => { - // const {play} = this.props; - // this.setState({resizing: false}); - // play(); - // } +// stopResizing = () => { +// const {play} = this.props; +// this.setState({resizing: false}); +// play(); +// } - // setStartTime = event => this.props.setStartTime(Number.parseFloat(event.target.value)) +// setStartTime = event => this.props.setStartTime(Number.parseFloat(event.target.value)) - // setEndTime = event => this.props.setEndTime(Number.parseFloat(event.target.value)) +// setEndTime = event => this.props.setEndTime(Number.parseFloat(event.target.value)) // render() { - // const {currentTime = 0, duration, startTime, endTime, hover, src} = this.props; - - // if (!src) { - // return null; - // } - - // const {hoverTime, resizing} = this.state; - - // const total = endTime - startTime; - // const current = currentTime - startTime; - - // const previewTime = resizing ? currentTime : hoverTime; - // const previewLabelTime = resizing ? currentTime : (startTime <= hoverTime && hoverTime <= endTime ? hoverTime - startTime : hoverTime); - // const previewDuration = resizing ? total : (startTime <= hoverTime && hoverTime <= endTime ? total : undefined); - - // const className = classNames('progress-bar-container', {hover}); - - // return ( - //
- //
- //
- // - //
- // - //
- // - // - //
- //
- // - //
- // ); +// const {currentTime = 0, duration, startTime, endTime, hover, src} = this.props; + +// if (!src) { +// return null; +// } + +// const {hoverTime, resizing} = this.state; + +// const total = endTime - startTime; +// const current = currentTime - startTime; + +// const previewTime = resizing ? currentTime : hoverTime; +// const previewLabelTime = resizing ? currentTime : (startTime <= hoverTime && hoverTime <= endTime ? hoverTime - startTime : hoverTime); +// const previewDuration = resizing ? total : (startTime <= hoverTime && hoverTime <= endTime ? total : undefined); + +// const className = classNames('progress-bar-container', {hover}); + +// return ( +//
+//
+//
+// +//
+// +//
+// +// +//
+//
+// +//
+// ); // } // } diff --git a/renderer/components/editor/controls/preview.tsx b/renderer/components/editor/controls/preview.tsx index 4372531df..90e927a31 100644 --- a/renderer/components/editor/controls/preview.tsx +++ b/renderer/components/editor/controls/preview.tsx @@ -1,17 +1,17 @@ -import useWindowState from '../../../hooks/window-state'; import formatTime from '../../../utils/format-time'; import {useRef, useEffect} from 'react'; +import useEditorWindowState from 'hooks/editor/use-editor-window-state'; type Props = { - time: number, - labelTime: number, - duration: number, - hidePreview: boolean + time: number; + labelTime: number; + duration: number; + hidePreview: boolean; }; const Preview = ({time, labelTime, duration, hidePreview}: Props) => { const videoRef = useRef(); - const {filePath} = useWindowState(); + const {filePath} = useEditorWindowState(); const src = `file://${filePath}`; useEffect(() => { @@ -21,8 +21,12 @@ const Preview = ({time, labelTime, duration, hidePreview}: Props) => { }, [time, hidePreview]); return ( -
event.stopPropagation()}> -
diff --git a/renderer/components/editor/conversion/index.tsx b/renderer/components/editor/conversion/index.tsx index a1a41b297..6b3843b89 100644 --- a/renderer/components/editor/conversion/index.tsx +++ b/renderer/components/editor/conversion/index.tsx @@ -3,7 +3,7 @@ import useConversion from 'hooks/editor/use-conversion'; import useConversionIdContext from 'hooks/editor/use-conversion-id'; import {useConfirmation} from 'hooks/use-confirmation'; import ConversionDetails from './conversion-details'; -import TitleBar from './title-bar' +import TitleBar from './title-bar'; import VideoPreview from './video-preview'; const dialogOptions = { @@ -16,13 +16,17 @@ const EditorConversionView = ({conversionId}: {conversionId: string}) => { const {setConversionId} = useConversionIdContext(); const conversion = useConversion(conversionId); - const cancel = () => conversion.cancel(); + const cancel = () => { + conversion.cancel(); + }; + const safeCancel = useConfirmation(cancel, dialogOptions); const cancelAndGoBack = () => { cancel(); setConversionId(''); }; + const safeCancelAndGoBack = useConfirmation(cancelAndGoBack, dialogOptions); const inProgress = conversion.state?.status === ConversionStatus.inProgress; @@ -32,7 +36,10 @@ const EditorConversionView = ({conversionId}: {conversionId: string}) => { return (
- conversion.copy()}/> + { + conversion.copy(); + }}/>
- ) + ); }; export default EditorConversionView; diff --git a/renderer/components/editor/conversion/title-bar.tsx b/renderer/components/editor/conversion/title-bar.tsx index f8eb234ea..262f44784 100644 --- a/renderer/components/editor/conversion/title-bar.tsx +++ b/renderer/components/editor/conversion/title-bar.tsx @@ -1,13 +1,12 @@ -import TrafficLights from 'components/traffic-lights' -import {BackPlainIcon} from 'vectors' +import TrafficLights from 'components/traffic-lights'; +import {BackPlainIcon} from 'vectors'; import {UseConversionState} from 'hooks/editor/use-conversion'; -const TitleBar = ({conversion, cancel, copy}: {conversion: UseConversionState, cancel: () => any, copy: () => any}) => { - +const TitleBar = ({conversion, cancel, copy}: {conversion: UseConversionState; cancel: () => any; copy: () => any}) => { return (
- +
@@ -22,6 +21,7 @@ const TitleBar = ({conversion, cancel, copy}: {conversion: UseConversionState, c display: flex; align-items: center; justify-content: space-between; + -webkit-app-region: drag; } .left { @@ -65,7 +65,7 @@ const TitleBar = ({conversion, cancel, copy}: {conversion: UseConversionState, c } `}
- ) -} + ); +}; export default TitleBar; diff --git a/renderer/components/editor/conversion/video-preview.tsx b/renderer/components/editor/conversion/video-preview.tsx index cf50cfca6..261f33c30 100644 --- a/renderer/components/editor/conversion/video-preview.tsx +++ b/renderer/components/editor/conversion/video-preview.tsx @@ -1,17 +1,26 @@ import {CancelIcon, SpinnerIcon} from 'vectors'; -import useWindowState from 'hooks/window-state'; import {UseConversion, UseConversionState} from 'hooks/editor/use-conversion'; import {ConversionStatus} from 'common/types'; +import useEditorWindowState from 'hooks/editor/use-editor-window-state'; +import useConversionIdContext from 'hooks/editor/use-conversion-id'; -const VideoPreview = ({conversion, cancel}: {conversion: UseConversionState, cancel: () => any}) => { - const {filePath} = useWindowState(); +const VideoPreview = ({conversion, cancel}: {conversion: UseConversionState; cancel: () => any}) => { + const {conversionId} = useConversionIdContext(); + const {filePath} = useEditorWindowState(); const src = `file://${filePath}`; const percentage = conversion?.progress ?? 0; const done = conversion?.status !== ConversionStatus.inProgress; + const onDragStart = (event: any) => { + event.preventDefault(); + // Has to be the electron one for this + const {ipcRenderer} = require('electron'); + ipcRenderer.send('drag-conversion', conversionId); + }; + return ( -
+